diff --git a/.claude/rules/feast-components.md b/.claude/rules/feast-components.md new file mode 100644 index 00000000000..02b1c6f4dd4 --- /dev/null +++ b/.claude/rules/feast-components.md @@ -0,0 +1,45 @@ +--- +paths: + - sdk/python/feast/infra/online_stores/** + - sdk/python/feast/infra/offline_stores/** + - sdk/python/feast/infra/registry/** + - sdk/python/tests/unit/infra/** + - go/** + - infra/feast-operator/** +--- + +Read `skills/feast-architecture/SKILL.md` for the relevant component section: + +| Working in… | Read section | +|---|---| +| `online_stores/` | Online Store — config pattern, entity key serialization, async support, registration | +| `offline_stores/` | Offline Store — interface, PIT join, pull_latest, adding a new backend | +| `infra/registry/` | Registry — proto vs SQL backend, caching, adding new object types | +| `go/` | Go Feature Server — entry point, serving path, online store interface, build commands | +| `infra/feast-operator/` | Feast Operator — CRD spec, reconcile loop, RBAC markers, dev workflow | + +For testing patterns and debugging, also read `skills/feast-testing/SKILL.md`. + +## When making any component change + +- **Unit tests**: add or update tests in `sdk/python/tests/unit/infra//` +- **Integration tests**: run `make test-python-integration-local`; add a universal test case in `sdk/python/tests/integration/` if the change affects retrieval or materialization behavior +- **SQL registry binary columns**: in `infra/registry/sql.py`, a new column that stores a serialized proto or blob metadata must use `ProtoBytes`, not `LargeBinary` directly — `LargeBinary` maps to MySQL `BLOB` (64 KB cap) and silently truncates large protos +- **Protos**: if you add a field to a proto message, recompile with `make protos` and update serialization helpers in `proto_registry_utils.py` +- **Both SDKs**: if the change affects online serving, check whether the Go server (`go/`) also needs updating +- **Skills/Rules**: if the change introduces new patterns, interfaces, or conventions that agents should follow, update the relevant section in `skills/feast-architecture/SKILL.md` (and `skills/feast-testing/SKILL.md` if testing patterns changed) + +## Documentation — where to add/update + +| Change type | Doc location | Also update | +|---|---|---| +| New **online store** | `docs/reference/online-stores/.md` (copy an existing one as template) | `docs/reference/online-stores/README.md`, `docs/SUMMARY.md` (under "Online stores") | +| New **offline store** | `docs/reference/offline-stores/.md` | `docs/reference/offline-stores/README.md`, `docs/reference/offline-stores/overview.md`, `docs/SUMMARY.md` | +| New **registry backend** | `docs/reference/registries/.md` | `docs/SUMMARY.md` | +| Config option change | `docs/reference/feature-store-yaml.md` | — | +| New CLI flag or command | `docs/reference/feast-cli-commands.md` | — | +| How-to / integration guide | `docs/how-to-guides/customizing-feast/` or `docs/how-to-guides/` | `docs/SUMMARY.md` | +| Architecture / concept | `docs/getting-started/architecture/` or `docs/getting-started/components/` | `docs/SUMMARY.md` | +| Blog post | `/infra/website/docs/blog/` (NOT `docs/blog/`) | — | + +All `docs/` pages are rendered by GitBook via `docs/SUMMARY.md`. Any new page must be added to `SUMMARY.md` or it won't appear in the site navigation. diff --git a/.claude/rules/feast-skills-maintenance.md b/.claude/rules/feast-skills-maintenance.md new file mode 100644 index 00000000000..8be40b3563b --- /dev/null +++ b/.claude/rules/feast-skills-maintenance.md @@ -0,0 +1,28 @@ +--- +paths: + - skills/** + - AGENTS.md + - .cursor/rules/** + - .claude/rules/** +--- + +## When editing skills or rules + +Skills and rules are only useful if they accurately reflect the real codebase. Before finalising any skill/rule edit: + +**Verify against source code:** +- Command examples (lint, test, type-check) — confirm they still match `Makefile` targets and `pyproject.toml` +- File paths and class names — confirm they exist in the repo +- Interface signatures (e.g. `OnlineStore`, `OfflineStore`, `BaseRegistry`) — confirm against the actual base class files +- Config field names — confirm against `RepoConfig` and `FeastConfigBaseModel` subclasses in `sdk/python/feast/repo_config.py` + +**Keep scope consistent:** +- `AGENTS.md` — entry point only; commands, skills table, code style. Max ~120 lines. +- `skills/feast-architecture/SKILL.md` — how each component works internally; data flows; adding new backends +- `skills/feast-testing/SKILL.md` — how to run, write, and debug tests +- `skills/feast-dev/SKILL.md` — contributor workflow; setup; Docker; docs locations; PR process +- `skills/feast-user-guide/SKILL.md` — how to use Feast as an end user; feature definitions; retrieval; RAG +- `.cursor/rules/feast-components.mdc` / `.claude/rules/feast-components.md` — component checklist (tests, docs, skills); keep in sync with each other + +**Keep the two rule files in sync:** +`.cursor/rules/feast-components.mdc` and `.claude/rules/feast-components.md` contain the same content with only different frontmatter (`globs:` vs `paths:`). Any content change must be applied to both. diff --git a/.claude/skills/feast-architecture/SKILL.md b/.claude/skills/feast-architecture/SKILL.md new file mode 120000 index 00000000000..2075b8d0450 --- /dev/null +++ b/.claude/skills/feast-architecture/SKILL.md @@ -0,0 +1 @@ +../../../skills/feast-architecture/SKILL.md \ No newline at end of file diff --git a/.claude/skills/feast-dev/SKILL.md b/.claude/skills/feast-dev/SKILL.md new file mode 120000 index 00000000000..ba44334bd0b --- /dev/null +++ b/.claude/skills/feast-dev/SKILL.md @@ -0,0 +1 @@ +../../../skills/feast-dev/SKILL.md \ No newline at end of file diff --git a/.claude/skills/feast-testing/SKILL.md b/.claude/skills/feast-testing/SKILL.md new file mode 120000 index 00000000000..9af0ba0de43 --- /dev/null +++ b/.claude/skills/feast-testing/SKILL.md @@ -0,0 +1 @@ +../../../skills/feast-testing/SKILL.md \ No newline at end of file diff --git a/.claude/skills/feast-user-guide/SKILL.md b/.claude/skills/feast-user-guide/SKILL.md new file mode 120000 index 00000000000..9fd50297875 --- /dev/null +++ b/.claude/skills/feast-user-guide/SKILL.md @@ -0,0 +1 @@ +../../../skills/feast-user-guide/SKILL.md \ No newline at end of file diff --git a/.codecov.yaml b/.codecov.yaml new file mode 100644 index 00000000000..2fa599642ec --- /dev/null +++ b/.codecov.yaml @@ -0,0 +1,48 @@ +codecov: + require_ci_to_pass: true + +coverage: + precision: 2 + round: down + range: "50...70" + + status: + project: + default: + informational: true + target: auto + threshold: 1% + patch: + default: + informational: true + target: 70% + +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: false + require_base: false + require_head: true + show_carryforward_flags: true + +flags: + python-unit: + paths: + - sdk/python/feast/ + carryforward: true + go-feature-server: + paths: + - go/ + carryforward: true + +ignore: + - "sdk/python/tests/**" + - "**/*_pb2.py" + - "**/*_pb2_grpc.py" + - "sdk/python/feast/protos/**" + - "sdk/python/feast/embedded_go/**" + - "protos/**" + - "docs/**" + - "ui/**" + - "java/**" + - "infra/feast-operator/test/**" diff --git a/.commitlintrc.yaml b/.commitlintrc.yaml new file mode 100644 index 00000000000..8b13a390116 --- /dev/null +++ b/.commitlintrc.yaml @@ -0,0 +1,45 @@ +extends: + - "@commitlint/config-conventional" + +# Single source of truth for commit/PR title conventions. +# Used by: +# - .pre-commit-config.yaml (commitlint hook on commit-msg) +# - .github/workflows/lint_pr.yml (validates PR titles in CI) +rules: + type-enum: + - 2 + - always + - - feat + - fix + - docs + - style + - refactor + - perf + - test + - build + - ci + - chore + - revert + + # Scope is optional — no enforcement on allowed values. + scope-empty: + - 0 + + # Feast convention: subjects start with an uppercase letter. + # Overrides base config which defaults to "never sentence-case". + subject-case: + - 2 + - always + - - sentence-case + + header-max-length: + - 2 + - always + - 100 + + # Relax body/footer line length from base config's strict 100-char limit. + # Commit bodies often contain URLs, stack traces, or formatted output. + body-max-line-length: + - 0 + footer-max-line-length: + - 0 diff --git a/.cursor/rules/feast-components.mdc b/.cursor/rules/feast-components.mdc new file mode 100644 index 00000000000..f015619020d --- /dev/null +++ b/.cursor/rules/feast-components.mdc @@ -0,0 +1,41 @@ +--- +description: Component-level guidance for Feast subsystems +globs: sdk/python/feast/infra/online_stores/**,sdk/python/feast/infra/offline_stores/**,sdk/python/feast/infra/registry/**,sdk/python/tests/unit/infra/**,go/**,infra/feast-operator/** +alwaysApply: false +--- + +Read `skills/feast-architecture/SKILL.md` for the relevant component section: + +| Working in… | Read section | +|---|---| +| `online_stores/` | Online Store — config pattern, entity key serialization, async support, registration | +| `offline_stores/` | Offline Store — interface, PIT join, pull_latest, adding a new backend | +| `infra/registry/` | Registry — proto vs SQL backend, caching, adding new object types | +| `go/` | Go Feature Server — entry point, serving path, online store interface, build commands | +| `infra/feast-operator/` | Feast Operator — CRD spec, reconcile loop, RBAC markers, dev workflow | + +For testing patterns and debugging, also read `skills/feast-testing/SKILL.md`. + +## When making any component change + +- **Unit tests**: add or update tests in `sdk/python/tests/unit/infra//` +- **Integration tests**: run `make test-python-integration-local`; add a universal test case in `sdk/python/tests/integration/` if the change affects retrieval or materialization behavior +- **SQL registry binary columns**: in `infra/registry/sql.py`, a new column that stores a serialized proto or blob metadata must use `ProtoBytes`, not `LargeBinary` directly — `LargeBinary` maps to MySQL `BLOB` (64 KB cap) and silently truncates large protos +- **Protos**: if you add a field to a proto message, recompile with `make protos` and update serialization helpers in `proto_registry_utils.py` +- **Both SDKs**: if the change affects online serving, check whether the Go server (`go/`) also needs updating +- **Skills/Rules**: if the change introduces new patterns, interfaces, or conventions that agents should follow, update the relevant section in `skills/feast-architecture/SKILL.md` (and `skills/feast-testing/SKILL.md` if testing patterns changed) + +## Documentation — where to add/update + +| Change type | Doc location | Also update | +|---|---|---| +| New **online store** | `docs/reference/online-stores/.md` (copy an existing one as template) | `docs/reference/online-stores/README.md`, `docs/SUMMARY.md` (under "Online stores") | +| New **offline store** | `docs/reference/offline-stores/.md` | `docs/reference/offline-stores/README.md`, `docs/reference/offline-stores/overview.md`, `docs/SUMMARY.md` | +| New **registry backend** | `docs/reference/registries/.md` | `docs/SUMMARY.md` | +| Config option change | `docs/reference/feature-store-yaml.md` | — | +| New CLI flag or command | `docs/reference/feast-cli-commands.md` | — | +| How-to / integration guide | `docs/how-to-guides/customizing-feast/` or `docs/how-to-guides/` | `docs/SUMMARY.md` | +| Architecture / concept | `docs/getting-started/architecture/` or `docs/getting-started/components/` | `docs/SUMMARY.md` | +| Blog post | `/infra/website/docs/blog/` (NOT `docs/blog/`) | — | + +All `docs/` pages are rendered by GitBook via `docs/SUMMARY.md`. Any new page must be added to `SUMMARY.md` or it won't appear in the site navigation. diff --git a/.cursor/rules/feast-skills-maintenance.mdc b/.cursor/rules/feast-skills-maintenance.mdc new file mode 100644 index 00000000000..588cce66b6d --- /dev/null +++ b/.cursor/rules/feast-skills-maintenance.mdc @@ -0,0 +1,26 @@ +--- +description: Guidance for keeping skills and rules accurate and up to date +globs: skills/**,AGENTS.md,.cursor/rules/**,.claude/rules/** +alwaysApply: false +--- + +## When editing skills or rules + +Skills and rules are only useful if they accurately reflect the real codebase. Before finalising any skill/rule edit: + +**Verify against source code:** +- Command examples (lint, test, type-check) — confirm they still match `Makefile` targets and `pyproject.toml` +- File paths and class names — confirm they exist in the repo +- Interface signatures (e.g. `OnlineStore`, `OfflineStore`, `BaseRegistry`) — confirm against the actual base class files +- Config field names — confirm against `RepoConfig` and `FeastConfigBaseModel` subclasses in `sdk/python/feast/repo_config.py` + +**Keep scope consistent:** +- `AGENTS.md` — entry point only; commands, skills table, code style. Max ~120 lines. +- `skills/feast-architecture/SKILL.md` — how each component works internally; data flows; adding new backends +- `skills/feast-testing/SKILL.md` — how to run, write, and debug tests +- `skills/feast-dev/SKILL.md` — contributor workflow; setup; Docker; docs locations; PR process +- `skills/feast-user-guide/SKILL.md` — how to use Feast as an end user; feature definitions; retrieval; RAG +- `.cursor/rules/feast-components.mdc` / `.claude/rules/feast-components.md` — component checklist (tests, docs, skills); keep in sync with each other + +**Keep the two rule files in sync:** +`.cursor/rules/feast-components.mdc` and `.claude/rules/feast-components.md` contain the same content with only different frontmatter (`globs:` vs `paths:`). Any content change must be applied to both. diff --git a/.cursor/rules/feast-ui.mdc b/.cursor/rules/feast-ui.mdc new file mode 100644 index 00000000000..9072cbe335f --- /dev/null +++ b/.cursor/rules/feast-ui.mdc @@ -0,0 +1,19 @@ +--- +description: Formatting and lint rules for the Feast UI (React/TypeScript) +globs: ui/src/** +alwaysApply: false +--- + +## After editing any file under `ui/src/` + +1. **Run Prettier** before considering the task complete: + ```bash + cd ui && yarn prettier --write + ``` +2. **Verify** formatting passes: + ```bash + cd ui && yarn format:check + ``` + CI runs `yarn format:check` and will reject PRs with style violations. + +3. Prettier config lives in `ui/package.json` (no separate `.prettierrc`). Do not override it. diff --git a/.gitbook.yaml b/.gitbook.yaml index bbdd0c57e3b..8441cf23dd7 100644 --- a/.gitbook.yaml +++ b/.gitbook.yaml @@ -6,3 +6,6 @@ structure: redirects: reference/telemetry: ./reference/usage.md quickstart: ./getting-started/quickstart.md + reference/feature-store-yaml: ./reference/feature-repository/feature-store-yaml.md + reference/feast-ignore: ./reference/feature-repository/feast-ignore.md + reference/feature-repository: ./reference/feature-repository/README.md diff --git a/.github/actions/get-semantic-release-version/action.yml b/.github/actions/get-semantic-release-version/action.yml index 89f6a8f81c1..a53bc337b44 100644 --- a/.github/actions/get-semantic-release-version/action.yml +++ b/.github/actions/get-semantic-release-version/action.yml @@ -2,7 +2,7 @@ name: Get semantic release version description: "" inputs: custom_version: # Optional input for a custom version - description: "Custom version to publish (e.g., v1.2.3) -- only edit if you know what you are doing" + description: "Custom version to publish (e.g., v1.2.3 or v1.2.3.dev4) -- only edit if you know what you are doing" required: false token: description: "Personal Access Token" @@ -10,10 +10,10 @@ inputs: default: "" outputs: release_version: - description: "The release version to use (e.g., v1.2.3)" + description: "The release version to use (e.g., v1.2.3 or v1.2.3.dev4)" value: ${{ steps.get_release_version.outputs.release_version }} version_without_prefix: - description: "The release version to use without 'v' (e.g., 1.2.3)" + description: "The release version to use without 'v' (e.g., 1.2.3 or 1.2.3.dev4)" value: ${{ steps.get_release_version_without_prefix.outputs.version_without_prefix }} highest_semver_tag: description: "The highest semantic version tag without the 'v' prefix (e.g., 1.2.3)" @@ -32,10 +32,10 @@ runs: GIT_COMMITTER_EMAIL: feast-ci-bot@willem.co run: | if [[ -n "${{ inputs.custom_version }}" ]]; then - VERSION_REGEX="^v[0-9]+\.[0-9]+\.[0-9]+$" + VERSION_REGEX="^v[0-9]+\.[0-9]+\.[0-9]+(\.dev[0-9]+)?$" echo "Using custom version: ${{ inputs.custom_version }}" if [[ ! "${{ inputs.custom_version }}" =~ $VERSION_REGEX ]]; then - echo "Error: custom_version must match semantic versioning (e.g., v1.2.3)." + echo "Error: custom_version must match semantic versioning (e.g., v1.2.3 or v1.2.3.dev4)." exit 1 fi echo "::set-output name=release_version::${{ inputs.custom_version }}" @@ -84,4 +84,4 @@ runs: run: | echo $RELEASE_VERSION echo $VERSION_WITHOUT_PREFIX - echo $HIGHEST_SEMVER_TAG \ No newline at end of file + echo $HIGHEST_SEMVER_TAG diff --git a/.github/fork_workflows/fork_pr_integration_tests_aws.yml b/.github/fork_workflows/fork_pr_integration_tests_aws.yml index d8a547f5359..6722e225b91 100644 --- a/.github/fork_workflows/fork_pr_integration_tests_aws.yml +++ b/.github/fork_workflows/fork_pr_integration_tests_aws.yml @@ -40,11 +40,11 @@ jobs: architecture: x64 - name: Setup Go id: setup-go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.18.0 - name: Set up AWS SDK - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/fork_workflows/fork_pr_integration_tests_gcp.yml b/.github/fork_workflows/fork_pr_integration_tests_gcp.yml index 563111727b9..c6810e1dac2 100644 --- a/.github/fork_workflows/fork_pr_integration_tests_gcp.yml +++ b/.github/fork_workflows/fork_pr_integration_tests_gcp.yml @@ -40,15 +40,15 @@ jobs: architecture: x64 - name: Setup Go id: setup-go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.18.0 - name: Authenticate to Google Cloud - uses: 'google-github-actions/auth@v1' + uses: 'google-github-actions/auth@v2' with: credentials_json: '${{ secrets.GCP_SA_KEY }}' - name: Set up gcloud SDK - uses: google-github-actions/setup-gcloud@v1 + uses: google-github-actions/setup-gcloud@v2 with: project_id: ${{ secrets.GCP_PROJECT_ID }} - name: Use gcloud CLI diff --git a/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml b/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml index 1983189b4ee..0db580ce7db 100644 --- a/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml +++ b/.github/fork_workflows/fork_pr_integration_tests_snowflake.yml @@ -40,7 +40,7 @@ jobs: architecture: x64 - name: Setup Go id: setup-go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: 1.18.0 - name: Install the latest version of uv @@ -72,7 +72,7 @@ jobs: SNOWFLAKE_CI_WAREHOUSE: ${{ secrets.SNOWFLAKE_CI_WAREHOUSE }} # Run only Snowflake BigQuery and File tests without dynamo and redshift tests. run: | - pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "Snowflake and not dynamo and not Redshift and not Bigquery and not gcp and not minio_registry" + pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "(Snowflake or snowflake_registry) and not dynamo and not Redshift and not Bigquery and not gcp and not minio_registry" pytest -n 8 --cov=./ --cov-report=xml --color=yes sdk/python/tests --integration --durations=5 --timeout=1200 --timeout_method=thread -k "File and not dynamo and not Redshift and not Bigquery and not gcp and not minio_registry" - name: Minimize uv cache run: uv cache prune --ci diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 40986a87db9..48bb592ed84 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,7 +11,7 @@ # What this PR does / why we need it: # Which issue(s) this PR fixes: @@ -20,6 +20,16 @@ Outline what you're doing Usage: `Fixes #`, or `Fixes (paste link of issue)`. --> +# Checks +- [ ] I've made sure the tests are passing. +- [ ] My commits are signed off (`git commit -s`) +- [ ] My PR title follows [conventional commits](https://www.conventionalcommits.org/) format + +## Testing Strategy +- [ ] Unit tests +- [ ] Integration tests +- [ ] Manual tests +- [ ] Testing is not required for this change # Misc |manages| Registry + Operator -->|manages| OnlineServer + OnlineServer -->|reads/writes| Redis + OnlineServer -->|reads metadata| Registry + MaterializationJob -->|reads source data| OfflineStore + MaterializationJob -->|writes features| Redis + NotebookPod -->|online features REST| OnlineServer + NotebookPod -->|metadata gRPC| Registry + + Client(["Client / ML Service"]) -->|REST port 6566| OnlineServer + + style Kubernetes fill:#f0f4ff,stroke:#3366cc,color:#000 + style BackingStores fill:#fafafa,stroke:#999,color:#000 + style Operator fill:#e8f5e9,stroke:#388e3c,color:#000 + style Registry fill:#fff3e0,stroke:#f57c00,color:#000 + style OnlineServer fill:#e3f2fd,stroke:#1976d2,color:#000 + style Redis fill:#fce4ec,stroke:#c62828,color:#000 + style OfflineStore fill:#f3e5f5,stroke:#7b1fa2,color:#000 + style MaterializationJob fill:#fff8e1,stroke:#f9a825,color:#000 + style NotebookPod fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +### Components + +| Component | Configuration | Notes | +|---|---|---| +| **Feast Operator** | Default install | Manages all Feast CRDs | +| **Registry** | REST, 1 replica | Single point of metadata | +| **Online Feature Server** | 1 replica, no autoscaling | Serves online features | +| **Online Store** | Redis standalone (example) | SQLite is simplest for development; Redis for production. See [supported online stores](../reference/online-stores/README.md) for all options | +| **Offline Store** | File-based or MinIO | DuckDB or file-based for development; MinIO/S3 for production. See [supported offline stores](../reference/offline-stores/README.md) for all options | +| **Compute Engine** | In-process (default) | Suitable for small datasets and development; use Spark, Ray, or Snowflake Engine for larger workloads | + +### Sample FeatureStore CR + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: minimal-production +spec: + feastProject: my_project + services: + onlineStore: + persistence: + store: + type: redis + secretRef: + name: feast-online-store + server: + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "1" + memory: 1Gi + offlineStore: + persistence: + file: + type: duckdb # Use type: file for generic file-based; swap for S3/MinIO in production + pvc: + create: + storageClassName: standard + resources: + requests: + storage: 10Gi + mountPath: /data/offline + registry: + local: + server: + restAPI: true + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi +``` + +### Limitations + +{% hint style="warning" %} +* **No high availability** — a single replica failure causes downtime +* **No automatic failover** — manual intervention required on failure +* **Manual scaling** — no HPA configured +* **Limited security** — no TLS, no ingress, no RBAC by default +{% endhint %} + +--- + +## 2. Standard Production (Recommended) + +### When to use + +* Most production ML workloads +* Teams with moderate traffic that need reliability +* Environments that require TLS, RBAC, and automated scaling + +### Architecture + +```mermaid +graph TD + Client(["Client / ML Service"]) -->|HTTPS| Ingress + + subgraph Kubernetes["Kubernetes"] + Ingress["Ingress Controller
(TLS termination)"] + Operator["Feast Operator"] + MaterializationJob["Materialization Job
(CronJob / batchEngine)"] + NotebookPod(["Notebook / Training Pod
(Feast SDK — remote online, offline, registry)"]) + + subgraph FeastDeployment["Feast Deployment (HPA autoscaled — all containers scale together)"] + Registry["Registry Server"] + OnlineServer["Online Feature Server"] + OfflineServer["Offline Feature Server"] + end + + Ingress -->|port 6566| OnlineServer + Operator -->|manages| Registry + Operator -->|manages| OnlineServer + Operator -->|manages| OfflineServer + OnlineServer -->|reads metadata| Registry + OfflineServer -->|reads metadata| Registry + MaterializationJob -->|reads metadata| Registry + NotebookPod -->|online features REST| OnlineServer + NotebookPod -->|historical features Arrow Flight| OfflineServer + NotebookPod -->|metadata gRPC| Registry + end + + subgraph BackingStores["Backing Stores (inside or outside Kubernetes)"] + RedisCluster["Online Store
(e.g. Redis Cluster, DynamoDB, etc.)"] + OfflineStore["Offline Store
(e.g. PostgreSQL, Redshift, BigQuery, etc.)"] + end + + OnlineServer -->|reads/writes| RedisCluster + MaterializationJob -->|writes features| RedisCluster + OfflineServer -->|historical features| OfflineStore + MaterializationJob -->|reads source data| OfflineStore + + style Kubernetes fill:#f0f4ff,stroke:#3366cc,color:#000 + style FeastDeployment fill:#e8f5e9,stroke:#388e3c,color:#000 + style BackingStores fill:#fafafa,stroke:#999,color:#000 + style Ingress fill:#fff9c4,stroke:#f9a825,color:#000 + style Operator fill:#e8f5e9,stroke:#388e3c,color:#000 + style Registry fill:#fff3e0,stroke:#f57c00,color:#000 + style OnlineServer fill:#e3f2fd,stroke:#1976d2,color:#000 + style OfflineServer fill:#ede7f6,stroke:#512da8,color:#000 + style RedisCluster fill:#fce4ec,stroke:#c62828,color:#000 + style OfflineStore fill:#f3e5f5,stroke:#7b1fa2,color:#000 + style MaterializationJob fill:#fff8e1,stroke:#f9a825,color:#000 + style NotebookPod fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +### Components + +**Core** + +| Component | Configuration | Notes | +|---|---|---| +| **Feast Operator** | Default install | Manages all Feast CRDs | +| **Registry** | SQL-backed (PostgreSQL) | Database-backed for consistency and concurrent access | +| **Online Feature Server** | HPA (min 2 replicas, max based on peak load) | Separate container — serves online features from the online store | +| **Offline Feature Server** | Scales with the same Deployment | Separate container — serves historical features and materialization source reads from the offline store | + +**Storage** + +| Component | Configuration | Notes | +|---|---|---| +| **Online Store** | Redis Cluster (example) | Multi-node for availability and low latency; other production stores are also supported — see [supported online stores](../reference/online-stores/README.md) | +| **Offline Store** | PostgreSQL (example) | Platform-agnostic DB-backed store; use Redshift/Athena for AWS, BigQuery for GCP, Spark for S3/MinIO pipelines — see [supported offline stores](../reference/offline-stores/README.md) for all options | +| **Compute Engine** | Spark, Ray (KubeRay), or Snowflake Engine | Distributed compute for materialization and historical retrieval at scale | + +**Networking & Security** + +| Component | Configuration | Notes | +|---|---|---| +| **Ingress** | TLS-terminated | Secure external access | +| **RBAC** | Kubernetes RBAC | Namespace-scoped permissions | +| **Secrets** | Kubernetes Secrets + `${ENV_VAR}` substitution | Store credentials via `secretRef` / `envFrom` in the FeatureStore CR; inject into `feature_store.yaml` with [environment variable syntax](./running-feast-in-production.md#5-using-environment-variables-in-your-yaml-configuration) | + +### Sample FeatureStore CR + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: standard-production +spec: + feastProject: my_project + authz: + kubernetes: + roles: + - feast-admin-role + - feast-user-role + batchEngine: + configMapRef: + name: feast-batch-engine + services: + scaling: + autoscaling: + minReplicas: 2 + maxReplicas: 10 # Set based on your peak load + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + podDisruptionBudgets: + maxUnavailable: 1 + onlineStore: + persistence: + store: + type: redis + secretRef: + name: feast-online-store + server: + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi + offlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-offline-store + server: + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "1" + memory: 1Gi + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-registry-store + server: + restAPI: true + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "1" + memory: 1Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: feast-batch-engine +data: + config: | + type: ray + address: auto # KubeRay cluster address; replace with explicit URL if not using auto-discovery +``` + +{% hint style="success" %} +**Key features:** + +* **High availability** — multi-replica deployment with auto-injected pod anti-affinity and topology spread constraints +* **Scalable serving** — HPA adjusts the shared deployment replicas (all services scale together) based on demand +* **Secure external access** — TLS-terminated ingress with RBAC +* **Persistent storage** — Online Store (Redis Cluster shown as example; see [supported online stores](../reference/online-stores/README.md) for all options) + Offline Store (PostgreSQL shown as example; see [supported offline stores](../reference/offline-stores/README.md) for all options) for durability + +See [Horizontal Scaling with the Feast Operator](./scaling-feast.md#horizontal-scaling-with-the-feast-operator) for full scaling configuration details. +{% endhint %} + +--- + +## 3. Enterprise Production + +### When to use + +* Large organizations with multiple ML teams +* Multi-tenant environments requiring strict isolation +* High-scale deployments with governance, compliance, and SLA requirements + +### Architecture — Isolated Registries (per namespace) + +Each team gets its own registry and online feature server in a dedicated namespace. This provides the strongest isolation but has notable trade-offs: feature discovery is siloed per team (no cross-project visibility), and each registry requires its own [Feast UI](../reference/alpha-web-ui.md) deployment — you cannot view multiple projects in a single UI instance. + +```mermaid +graph TD + Client(["Clients / ML Services"]) -->|HTTPS| Gateway + + subgraph Kubernetes["Kubernetes"] + Gateway["API Gateway / Ingress
(TLS + rate limiting)"] + Operator["Feast Operator
(cluster-scoped)"] + + subgraph NamespaceA["Namespace A — Team A"] + subgraph DeployA["Feast Deployment (HPA autoscaled)"] + RegistryA["Registry Server"] + OnlineServerA["Online Feature Server"] + OfflineServerA["Offline Feature Server"] + end + NotebookPodA(["Notebook — Team A
(Feast SDK)"]) + MaterializationJobA["Materialization Job"] + end + + subgraph NamespaceB["Namespace B — Team B"] + subgraph DeployB["Feast Deployment (HPA autoscaled)"] + RegistryB["Registry Server"] + OnlineServerB["Online Feature Server"] + OfflineServerB["Offline Feature Server"] + end + NotebookPodB(["Notebook — Team B
(Feast SDK)"]) + MaterializationJobB["Materialization Job"] + end + + Gateway -->|port 6566| OnlineServerA + Gateway -->|port 6566| OnlineServerB + Operator -->|manages| RegistryA + Operator -->|manages| OnlineServerA + Operator -->|manages| OfflineServerA + Operator -->|manages| RegistryB + Operator -->|manages| OnlineServerB + Operator -->|manages| OfflineServerB + OnlineServerA -->|metadata| RegistryA + OnlineServerB -->|metadata| RegistryB + OfflineServerA -->|metadata| RegistryA + OfflineServerB -->|metadata| RegistryB + NotebookPodA -.->|online REST| OnlineServerA + NotebookPodA -.->|Arrow Flight| OfflineServerA + NotebookPodA -.->|metadata| RegistryA + NotebookPodB -.->|online REST| OnlineServerB + NotebookPodB -.->|Arrow Flight| OfflineServerB + NotebookPodB -.->|metadata| RegistryB + MaterializationJobA -->|metadata| RegistryA + MaterializationJobB -->|metadata| RegistryB + end + + subgraph BackingStores["Backing Stores (inside or outside Kubernetes)"] + RedisA["Online Store — Team A
(e.g. Redis, DynamoDB, etc.)"] + RedisB["Online Store — Team B
(e.g. Redis, DynamoDB, etc.)"] + OfflineStore["Offline Store — shared instance
(e.g. BigQuery, Redshift, etc.)
isolated via per-team datasets / schemas"] + end + + subgraph Observability["Observability Stack"] + OTel["OpenTelemetry
(traces + metrics)"] + Prometheus["Prometheus"] + Grafana["Grafana"] + Jaeger["Jaeger"] + end + + OnlineServerA -->|reads/writes| RedisA + OnlineServerB -->|reads/writes| RedisB + OfflineServerA -->|Team A dataset| OfflineStore + OfflineServerB -->|Team B dataset| OfflineStore + MaterializationJobA -->|Team A data| OfflineStore + MaterializationJobA -->|writes| RedisA + MaterializationJobB -->|Team B data| OfflineStore + MaterializationJobB -->|writes| RedisB + + style Kubernetes fill:#f0f4ff,stroke:#3366cc,color:#000 + style NamespaceA fill:#e8f5e9,stroke:#388e3c,color:#000 + style NamespaceB fill:#e3f2fd,stroke:#1976d2,color:#000 + style DeployA fill:#c8e6c9,stroke:#2e7d32,color:#000 + style DeployB fill:#bbdefb,stroke:#1565c0,color:#000 + style BackingStores fill:#fafafa,stroke:#999,color:#000 + style Observability fill:#fff3e0,stroke:#f57c00,color:#000 + style Gateway fill:#fff9c4,stroke:#f9a825,color:#000 + style Operator fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +### Architecture — Shared Registry (cross-namespace) + +Alternatively, a single centralized registry server can serve multiple tenant namespaces. Tenant online feature servers connect to the shared registry via the [Remote Registry](../reference/registries/remote.md) gRPC client. This reduces operational overhead, enables cross-team feature discovery, and allows a single [Feast UI](../reference/alpha-web-ui.md) deployment to browse all projects — while Feast [permissions](#feast-permissions-and-rbac) enforce tenant isolation at the data level. + +```mermaid +graph TD + Client(["Clients / ML Services"]) -->|HTTPS| Gateway + + subgraph Kubernetes["Kubernetes"] + Gateway["API Gateway / Ingress
(TLS + rate limiting)"] + + subgraph SharedInfra["Shared Infrastructure Namespace"] + Operator["Feast Operator
(cluster-scoped)"] + subgraph SharedDeploy["Feast Deployment (3 replicas)"] + SharedRegistry["Registry Server
(gRPC + REST)"] + end + SharedDB[("SQL Database
(PostgreSQL)")] + SharedRegistry -->|persists| SharedDB + end + + subgraph NamespaceA["Namespace A — Team A"] + subgraph DeployA2["Feast Deployment (HPA autoscaled)"] + OnlineServerA["Online Feature Server"] + OfflineServerA["Offline Feature Server"] + end + NotebookPodA(["Notebook — Team A
(Feast SDK)"]) + MaterializationJobA["Materialization Job"] + ConfigA["registry_type: remote
path: shared-registry:6570"] + end + + subgraph NamespaceB["Namespace B — Team B"] + subgraph DeployB2["Feast Deployment (HPA autoscaled)"] + OnlineServerB["Online Feature Server"] + OfflineServerB["Offline Feature Server"] + end + NotebookPodB(["Notebook — Team B
(Feast SDK)"]) + MaterializationJobB["Materialization Job"] + ConfigB["registry_type: remote
path: shared-registry:6570"] + end + + Gateway -->|port 6566| OnlineServerA + Gateway -->|port 6566| OnlineServerB + OnlineServerA -->|gRPC port 6570| SharedRegistry + OnlineServerB -->|gRPC port 6570| SharedRegistry + OfflineServerA -->|gRPC port 6570| SharedRegistry + OfflineServerB -->|gRPC port 6570| SharedRegistry + Operator -->|manages| SharedRegistry + Operator -->|manages| OnlineServerA + Operator -->|manages| OfflineServerA + Operator -->|manages| OnlineServerB + Operator -->|manages| OfflineServerB + NotebookPodA -.->|online REST| OnlineServerA + NotebookPodA -.->|Arrow Flight| OfflineServerA + NotebookPodA -.->|metadata| SharedRegistry + NotebookPodB -.->|online REST| OnlineServerB + NotebookPodB -.->|Arrow Flight| OfflineServerB + NotebookPodB -.->|metadata| SharedRegistry + MaterializationJobA -->|gRPC port 6570| SharedRegistry + MaterializationJobB -->|gRPC port 6570| SharedRegistry + end + + subgraph BackingStores["Backing Stores (inside or outside Kubernetes)"] + RedisA["Online Store — Team A
(e.g. Redis, DynamoDB, etc.)"] + RedisB["Online Store — Team B
(e.g. Redis, DynamoDB, etc.)"] + OfflineStore["Offline Store — shared instance
(e.g. BigQuery, Redshift, etc.)
isolated via per-team datasets / schemas"] + end + + OnlineServerA -->|reads/writes| RedisA + OnlineServerB -->|reads/writes| RedisB + OfflineServerA -->|Team A dataset| OfflineStore + OfflineServerB -->|Team B dataset| OfflineStore + MaterializationJobA -->|Team A data| OfflineStore + MaterializationJobA -->|writes| RedisA + MaterializationJobB -->|Team B data| OfflineStore + MaterializationJobB -->|writes| RedisB + + style Kubernetes fill:#f0f4ff,stroke:#3366cc,color:#000 + style SharedInfra fill:#fff3e0,stroke:#f57c00,color:#000 + style SharedDeploy fill:#ffe0b2,stroke:#e65100,color:#000 + style NamespaceA fill:#e8f5e9,stroke:#388e3c,color:#000 + style NamespaceB fill:#e3f2fd,stroke:#1976d2,color:#000 + style DeployA2 fill:#c8e6c9,stroke:#2e7d32,color:#000 + style DeployB2 fill:#bbdefb,stroke:#1565c0,color:#000 + style BackingStores fill:#fafafa,stroke:#999,color:#000 + style Gateway fill:#fff9c4,stroke:#f9a825,color:#000 + style Operator fill:#e8f5e9,stroke:#388e3c,color:#000 + style OfflineStore fill:#f3e5f5,stroke:#7b1fa2,color:#000 +``` + +**Shared registry client configuration** — each tenant's `feature_store.yaml` points to the centralized registry: + +```yaml +registry: + registry_type: remote + path: shared-registry.feast-system.svc.cluster.local:6570 +``` + +{% hint style="info" %} +**Shared vs isolated registries:** + +| | Shared Registry | Isolated Registries | +|---|---|---| +| **Feature discovery** | Cross-team — all projects visible | Siloed — each team sees only its own | +| **Feast UI** | Single deployment serves all projects | Separate UI deployment per registry | +| **Isolation** | Logical (Feast permissions + tags) | Physical (separate metadata stores) | +| **Operational cost** | Lower — one registry to manage | Higher — N registries to maintain | +| **Best for** | Feature reuse, shared ML platform | Regulatory/compliance separation | + +Use a shared registry when teams need to discover and reuse features across projects, and rely on Feast permissions for access control. Use isolated registries when regulatory or compliance requirements demand physical separation of metadata. +{% endhint %} + +### Components + +**Multi-tenancy** + +| Aspect | Configuration | Notes | +|---|---|---| +| **Isolation model** | Namespace-per-team | Physical isolation via Kubernetes namespaces | +| **Registry strategy** | Shared (remote) or isolated (per-namespace) | See architecture variants above | +| **Network boundaries** | NetworkPolicy enforced | Cross-namespace traffic denied by default (allow-listed for shared registry) | + +**Storage** + +| Component | Configuration | Notes | +|---|---|---| +| **Online Store** | Managed Redis / DynamoDB / Elasticsearch | Cloud-managed, per-tenant instances; see [supported online stores](../reference/online-stores/README.md) for all options | +| **Offline Store** | External data warehouse (Snowflake, BigQuery) | Shared or per-tenant access controls; see [supported offline stores](../reference/offline-stores/README.md) for all options | + +**Scaling** + +| Component | Configuration | Notes | +|---|---|---| +| **FeatureStore Deployment** | HPA + Cluster Autoscaler | All services (Online Feature Server, Registry, Offline Feature Server) scale together per tenant; set `maxReplicas` based on your peak load. Independent scaling across tenants. | +| **Cluster** | Multi-zone node pools | Zone-aware scheduling with auto-injected topology spread constraints | + +**Security** + +| Component | Configuration | Notes | +|---|---|---| +| **Authentication** | OIDC via Keycloak | Centralized identity provider | +| **Authorization** | Feast permissions + Kubernetes RBAC | See [Permissions and RBAC](#feast-permissions-and-rbac) below | +| **Network** | NetworkPolicies per namespace | Microsegmentation | +| **Secrets** | Kubernetes Secrets (`secretRef` / `envFrom`) | Credentials injected via FeatureStore CR; use Kubernetes-native tooling (e.g. External Secrets Operator) to sync from external vaults if needed | + +**Observability** + +| Component | Purpose | Notes | +|---|---|---| +| **[OpenTelemetry](../getting-started/components/open-telemetry.md)** | Traces + metrics export | Built-in Feast integration; emits spans for feature retrieval, materialization, and registry operations | +| **Prometheus** | Metrics collection | Collects OpenTelemetry metrics from Online Feature Server + Online Store | +| **Grafana** | Dashboards + traces | Per-tenant and aggregate views; can display OpenTelemetry traces via Tempo or Jaeger data source | +| **Jaeger** | Distributed tracing | Visualize OpenTelemetry traces for request latency analysis and debugging | + +**Reliability & Disaster Recovery** + +| Aspect | Configuration | Notes | +|---|---|---| +| **PodDisruptionBudgets** | Configured per deployment | Protects against voluntary disruptions | +| **Multi-zone** | Topology spread constraints | Auto-injected by operator when scaling; survives single zone failures | +| **Backup / Restore** | See recovery priority below | Strategy depends on component criticality | + +**Recovery priority guidance** + +Not all Feast components carry the same recovery urgency. The table below ranks components by restoration priority and provides guidance for **RPO** (Recovery Point Objective — maximum acceptable data loss) and **RTO** (Recovery Time Objective — maximum acceptable downtime). Specific targets depend on your backing store SLAs and organizational requirements. + +| Priority | Component | RPO guidance | RTO guidance | Rationale | +|---|---|---|---|---| +| 1 — Critical | **Registry DB** (PostgreSQL / MySQL) | Minutes (continuous replication or frequent backups) | Minutes (failover to standby) | Contains all feature definitions and metadata; without it, no service can resolve features | +| 2 — High | **Online Store** (Redis / DynamoDB) | Reconstructible via materialization | Minutes to hours (depends on data volume) | Can be fully rebuilt by re-running materialization from the offline store; no unique data to lose | +| 3 — Medium | **Offline Store** (Redshift / BigQuery) | Per data warehouse SLA | Per data warehouse SLA | Source of truth for historical data; typically managed by the cloud provider with built-in replication | +| 4 — Low | **Feast Operator + CRDs** | N/A (declarative, stored in Git) | Minutes (re-apply manifests) | Stateless; redeployable from version-controlled manifests | + +{% hint style="info" %} +**Key insight:** The online store is *reconstructible* — it can always be rebuilt from the offline store by re-running materialization. This means its RPO is effectively zero (no unique data to lose), but RTO depends on how long full materialization takes for your dataset volume. For large datasets, consider maintaining Redis persistence (RDB snapshots or AOF) to reduce recovery time. +{% endhint %} + +**Backup recommendations by topology** + +| Topology | Registry | Online Store | Offline Store | +|---|---|---|---| +| **Minimal** | Manual file backups; accept downtime on failure | Not backed up (re-materialize) | N/A (file-based) | +| **Standard** | Automated PostgreSQL backups (daily + WAL archiving) | Redis RDB snapshots or AOF persistence | Per cloud provider SLA | +| **Enterprise** | Managed DB replication (multi-AZ); cross-region replicas for DR | Managed Redis with automatic failover (ElastiCache Multi-AZ, Memorystore HA) | Managed warehouse replication (Redshift cross-region, BigQuery cross-region) | + +### Sample FeatureStore CR (per tenant) + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: team-a-production + namespace: team-a +spec: + feastProject: team_a + authz: + oidc: + secretRef: + name: feast-oidc-secret # Secret keys: client_id, client_secret, auth_discovery_url + batchEngine: + configMapRef: + name: feast-batch-engine + services: + scaling: + autoscaling: + minReplicas: 3 + maxReplicas: 20 # Set based on your peak load + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 65 + podDisruptionBudgets: + minAvailable: 2 + onlineStore: + persistence: + store: + type: redis + secretRef: + name: feast-online-store + server: + resources: + requests: + cpu: "2" + memory: 2Gi + limits: + cpu: "4" + memory: 4Gi + offlineStore: + persistence: + store: + type: bigquery # Use snowflake.offline, redshift, etc. as alternatives — see supported offline stores + secretRef: + name: feast-offline-store + server: + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-registry-store + server: + restAPI: true + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: feast-batch-engine + namespace: team-a +data: + config: | + type: spark + spark_master: k8s://https://kubernetes.default.svc:443 + spark_app_name: feast-materialization +``` + +--- + +## Feast Permissions and RBAC + +Feast provides a built-in permissions framework that secures resources at the application level, independently of Kubernetes RBAC. Permissions are defined as Python objects in your feature repository and registered via `feast apply`. + +For full details, see the [Permission concept](../getting-started/concepts/permission.md) and [RBAC architecture](../getting-started/architecture/rbac.md) docs. + +### How it works + +```mermaid +graph LR + subgraph Client["Client Request"] + Token["Auth Token
(OIDC / K8s SA)"] + end + + subgraph Server["Feast Server"] + Extractor["Token Extractor"] + Parser["Token Parser"] + Enforcer["Policy Enforcer"] + end + + subgraph Registry["Registry"] + Permissions["Permission Objects"] + end + + Token --> Extractor + Extractor --> Parser + Parser -->|user roles/groups/ns| Enforcer + Enforcer -->|match resource + action| Permissions + Enforcer -->|allow / deny| Response(["Response"]) + + style Client fill:#e3f2fd,stroke:#1976d2,color:#000 + style Server fill:#fff3e0,stroke:#f57c00,color:#000 + style Registry fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +Permission enforcement happens on the server side (Online Feature Server, Offline Feature Server, Registry Server). There is no enforcement when using the Feast SDK with a local provider. + +### Actions + +Feast defines eight granular actions: + +| Action | Description | +|---|---| +| `CREATE` | Create a new Feast object | +| `DESCRIBE` | Read object metadata/state | +| `UPDATE` | Modify an existing object | +| `DELETE` | Remove an object | +| `READ_ONLINE` | Read from the online store | +| `READ_OFFLINE` | Read from the offline store | +| `WRITE_ONLINE` | Write to the online store | +| `WRITE_OFFLINE` | Write to the offline store | + +Convenience aliases are provided: + +| Alias | Includes | +|---|---| +| `ALL_ACTIONS` | All eight actions | +| `READ` | `READ_ONLINE` + `READ_OFFLINE` | +| `WRITE` | `WRITE_ONLINE` + `WRITE_OFFLINE` | +| `CRUD` | `CREATE` + `DESCRIBE` + `UPDATE` + `DELETE` | + +### Protected resource types + +Permissions can be applied to any of these Feast object types: + +`Project`, `Entity`, `FeatureView`, `OnDemandFeatureView`, `BatchFeatureView`, `StreamFeatureView`, `FeatureService`, `DataSource`, `ValidationReference`, `SavedDataset`, `Permission` + +The constant `ALL_RESOURCE_TYPES` includes all of the above. `ALL_FEATURE_VIEW_TYPES` includes all feature view subtypes. + +### Policy types + +| Policy | Match criteria | Use case | +|---|---|---| +| `RoleBasedPolicy(roles=[...])` | User must have at least one of the listed roles | Kubernetes RBAC roles, OIDC roles | +| `GroupBasedPolicy(groups=[...])` | User must belong to at least one of the listed groups | LDAP/OIDC group membership | +| `NamespaceBasedPolicy(namespaces=[...])` | User's service account must be in one of the listed namespaces | Kubernetes namespace-level isolation | +| `CombinedGroupNamespacePolicy(groups=[...], namespaces=[...])` | User must match at least one group **or** one namespace | Flexible cross-cutting policies | +| `AllowAll` | Always grants access | Development / unsecured resources | + +### Example: Role-based permissions + +This is the most common pattern — separate admin and read-only roles: + +```python +from feast.feast_object import ALL_RESOURCE_TYPES +from feast.permissions.action import READ, ALL_ACTIONS, AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy + +admin_perm = Permission( + name="feast_admin_permission", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=["feast-admin-role"]), + actions=ALL_ACTIONS, +) + +user_perm = Permission( + name="feast_user_permission", + types=ALL_RESOURCE_TYPES, + policy=RoleBasedPolicy(roles=["feast-user-role"]), + actions=[AuthzedAction.DESCRIBE] + READ, +) +``` + +### Example: Namespace-based isolation for multi-tenant deployments + +Use `NamespaceBasedPolicy` to restrict access based on the Kubernetes namespace of the calling service account — ideal for the shared-registry enterprise topology. + +Each team gets two permissions: full access to its own resources (matched by `team` tag), and read-only access to resources any team has explicitly published as shared (matched by `visibility: shared` tag). The two `required_tags` target **different** resources — a feature view tagged `team: team-b, visibility: shared` matches only the second permission for Team A, enabling cross-team discovery without granting write access: + +```python +from feast.feast_object import ALL_RESOURCE_TYPES +from feast.permissions.action import ALL_ACTIONS, READ, AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import NamespaceBasedPolicy + +# Team A: full access to its own resources +team_a_own = Permission( + name="team_a_full_access", + types=ALL_RESOURCE_TYPES, + required_tags={"team": "team-a"}, # matches only Team A's resources + policy=NamespaceBasedPolicy(namespaces=["team-a"]), + actions=ALL_ACTIONS, +) + +# Team A: read-only access to shared resources published by ANY team +# e.g. a Team B feature view tagged {team: team-b, visibility: shared} +# satisfies required_tags here but NOT team_a_own above +team_a_read_shared = Permission( + name="team_a_read_shared", + types=ALL_RESOURCE_TYPES, + required_tags={"visibility": "shared"}, # matches shared resources from any team + policy=NamespaceBasedPolicy(namespaces=["team-a"]), + actions=[AuthzedAction.DESCRIBE] + READ, +) + +# Team B: mirror of the above — full access to its own, read-only to shared +team_b_own = Permission( + name="team_b_full_access", + types=ALL_RESOURCE_TYPES, + required_tags={"team": "team-b"}, + policy=NamespaceBasedPolicy(namespaces=["team-b"]), + actions=ALL_ACTIONS, +) + +team_b_read_shared = Permission( + name="team_b_read_shared", + types=ALL_RESOURCE_TYPES, + required_tags={"visibility": "shared"}, + policy=NamespaceBasedPolicy(namespaces=["team-b"]), + actions=[AuthzedAction.DESCRIBE] + READ, +) +``` + +### Example: Combined group + namespace policy + +For organizations that use both OIDC groups and Kubernetes namespaces for identity — ideal when platform engineers lack a dedicated namespace but need cross-team visibility, or when OIDC group membership and namespace ownership should independently grant access: + +```python +from feast.feast_object import ALL_RESOURCE_TYPES +from feast.permissions.action import ALL_ACTIONS, READ, AuthzedAction +from feast.permissions.permission import Permission +from feast.permissions.policy import CombinedGroupNamespacePolicy + +# Platform engineers (OIDC group) OR any team namespace can read shared features. +# This covers platform engineers who have no dedicated K8s namespace of their own +# but need cross-team feature discovery. +platform_read_shared = Permission( + name="platform_read_shared", + types=ALL_RESOURCE_TYPES, + required_tags={"visibility": "shared"}, + policy=CombinedGroupNamespacePolicy( + groups=["ml-platform"], # OIDC group for platform/infra engineers + namespaces=["team-a", "team-b"], # team namespaces from enterprise topology + ), + actions=[AuthzedAction.DESCRIBE] + READ, +) + +# ML engineers (OIDC) OR team namespace owners have full write access. +# Either identity alone is sufficient — useful during namespace migration or +# when the same person holds both the OIDC role and the team namespace. +ml_engineer_write = Permission( + name="ml_engineer_full_access", + types=ALL_RESOURCE_TYPES, + policy=CombinedGroupNamespacePolicy( + groups=["ml-engineers"], + namespaces=["team-a", "team-b"], + ), + actions=ALL_ACTIONS, +) +``` + +### Example: Fine-grained resource filtering + +Permissions support `name_patterns` (regex) and `required_tags` for targeting specific resources: + +```python +from feast.feature_view import FeatureView +from feast.data_source import DataSource +from feast.permissions.action import AuthzedAction, READ +from feast.permissions.permission import Permission +from feast.permissions.policy import RoleBasedPolicy + +sensitive_fv_perm = Permission( + name="sensitive_feature_reader", + types=[FeatureView], + name_patterns=[".*sensitive.*", ".*pii.*"], + policy=RoleBasedPolicy(roles=["trusted-reader"]), + actions=[AuthzedAction.READ_OFFLINE], +) + +high_risk_ds_writer = Permission( + name="high_risk_ds_writer", + types=[DataSource], + required_tags={"risk_level": "high"}, + policy=RoleBasedPolicy(roles=["admin", "data_team"]), + actions=[AuthzedAction.WRITE_ONLINE, AuthzedAction.WRITE_OFFLINE], +) +``` + +### Authorization configuration + +Enable auth enforcement in `feature_store.yaml`: + +```yaml +auth: + type: kubernetes # or: oidc +``` + +For OIDC: + +```yaml +auth: + type: oidc + client_id: feast-client + auth_server_url: https://keycloak.example.com/realms/feast + auth_discovery_url: https://keycloak.example.com/realms/feast/.well-known/openid-configuration +``` + +{% hint style="warning" %} +**Permission granting order:** Feast uses an *affirmative* decision strategy — if **any** matching permission grants access, the request is allowed. Access is denied only when **all** matching permissions deny the user. If no permission matches a resource + action combination, access is **denied**. Resources that do not match any configured permission are unsecured. Always define explicit coverage for all critical resources. +{% endhint %} + +### Recommended RBAC by topology + +| Topology | Auth type | Policy type | Guidance | +|---|---|---|---| +| **Minimal** | `no_auth` or `kubernetes` | `RoleBasedPolicy` | Basic admin/reader roles | +| **Standard** | `kubernetes` | `RoleBasedPolicy` | K8s service account roles | +| **Enterprise (isolated)** | `oidc` or `kubernetes` | `RoleBasedPolicy` + `GroupBasedPolicy` | Per-team OIDC groups | +| **Enterprise (shared registry)** | `kubernetes` | `NamespaceBasedPolicy` or `CombinedGroupNamespacePolicy` | Namespace isolation with tag-based resource scoping | + +--- + +## Infrastructure-Specific Recommendations + +Choosing the right online store, offline store, and registry backend depends on your cloud environment and existing infrastructure. The table below maps common deployment environments to recommended Feast components. + +### Recommendation matrix + +```mermaid +graph TD + subgraph AWS["AWS / EKS / ROSA"] + A_Online["Online: ElastiCache Redis
or DynamoDB"] + A_Offline["Offline: Redshift
or Snowflake or Athena"] + A_Registry["Registry: RDS PostgreSQL (SQL)
or S3"] + A_Compute["Compute: Snowflake Engine
or Spark on EMR
or Ray (KubeRay)"] + end + + subgraph GCP["GCP / GKE"] + G_Online["Online: Memorystore Redis
or Bigtable or Datastore"] + G_Offline["Offline: BigQuery
or Snowflake"] + G_Registry["Registry: Cloud SQL PostgreSQL
or GCS"] + G_Compute["Compute: Snowflake Engine
or Spark on Dataproc
or Ray (KubeRay)"] + end + + subgraph OnPrem["On-Premise / OpenShift"] + O_Online["Online: Redis
or PostgreSQL"] + O_Offline["Offline: Spark + MinIO
or PostgreSQL or Trino or Oracle"] + O_Registry["Registry: PostgreSQL (SQL)"] + O_Compute["Compute: Spark
or Ray (KubeRay)"] + end + + style AWS fill:#fff3e0,stroke:#f57c00,color:#000 + style GCP fill:#e3f2fd,stroke:#1976d2,color:#000 + style OnPrem fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +### AWS / EKS / ROSA + +| Component | Recommended | Alternative | Notes | +|---|---|---|---| +| **Online Store** | **Redis** (ElastiCache) | DynamoDB | Redis offers TTL at retrieval, concurrent writes, Java/Go SDK support. DynamoDB is fully managed with zero ops. | +| **Offline Store** | **Redshift** | Snowflake, Athena (contrib), Spark | Redshift is the core AWS offline store. Use Snowflake if it's already your warehouse. Athena for S3-native query patterns. | +| **Registry** | **SQL** (RDS PostgreSQL) | S3 | SQL registry required for concurrent materialization writers. S3 registry is simpler but limited to single-writer. | +| **Compute Engine** | **Snowflake Engine** | Spark on EMR, [Ray (KubeRay)](../reference/compute-engine/ray.md) | Snowflake engine when your offline/online stores are Snowflake. Spark for S3-based pipelines. Ray with KubeRay for Kubernetes-native distributed processing. | + +{% hint style="info" %} +**ROSA (Red Hat OpenShift on AWS):** Same store recommendations as EKS. Use OpenShift Routes instead of Ingress for TLS termination. Leverage OpenShift's built-in OAuth for `auth.type: kubernetes` integration. +{% endhint %} + +### GCP / GKE + +| Component | Recommended | Alternative | Notes | +|---|---|---|---| +| **Online Store** | **Redis** (Memorystore) | Bigtable, Datastore | Redis for latency-sensitive workloads. Bigtable for very large-scale feature storage. Datastore is GCP-native and zero-ops. | +| **Offline Store** | **BigQuery** | Snowflake, Spark (Dataproc) | BigQuery is the core GCP offline store with full feature support. | +| **Registry** | **SQL** (Cloud SQL PostgreSQL) | GCS | SQL for multi-writer. GCS for simple single-writer setups. | +| **Compute Engine** | **Snowflake Engine** | Spark on Dataproc, [Ray (KubeRay)](../reference/compute-engine/ray.md) | Use Snowflake engine if your offline store is Snowflake. Spark for BigQuery + GCS pipelines. Ray with KubeRay for Kubernetes-native distributed processing. | + +### On-Premise / OpenShift / Self-Managed Kubernetes + +| Component | Recommended | Alternative | Notes | +|---|---|---|---| +| **Online Store** | **Redis** (self-managed or operator) | PostgreSQL (contrib) | Redis for best performance. PostgreSQL if you want to minimize infrastructure components. | +| **Offline Store** | **Spark** + MinIO (contrib) | PostgreSQL (contrib), Trino (contrib), Oracle (contrib), DuckDB | Spark for scale. PostgreSQL for simpler setups. Oracle for enterprise customers with existing Oracle infrastructure. DuckDB for development only. | +| **Registry** | **SQL** (PostgreSQL) | — | Always use SQL registry in production on-prem. File-based registries do not support concurrent writers. | +| **Compute Engine** | **Spark** | [Ray (KubeRay)](../reference/compute-engine/ray.md) | Run Spark on Kubernetes or standalone. Ray with KubeRay for Kubernetes-native distributed DAG execution. | + +{% hint style="warning" %} +**Multi-replica constraint:** When scaling any Feast service to multiple replicas (via the Feast Operator), you **must** use database-backed persistence for all enabled services. File-based stores (SQLite, DuckDB, `registry.db`) are incompatible with multi-replica deployments. See [Scaling Feast](./scaling-feast.md#horizontal-scaling-with-the-feast-operator) for details. +{% endhint %} + +--- + +## Air-Gapped / Disconnected Environment Deployments + +Production environments in regulated industries (finance, government, defense) often have no outbound internet access from the Kubernetes cluster. The Feast Operator supports air-gapped deployments through custom container images, init container controls, and standard Kubernetes image-pull mechanisms. + +### Default init container behavior + +When `feastProjectDir` is set on the FeatureStore CR, the operator creates up to two init containers unless `services.disableInitContainers` is `true`: + +1. **`feast-init`** — bootstraps the feature repository by running `git clone`, `feast init`, or copying a repository from `feastProjectDir.packaged.featureRepoPath`. It then writes the operator-generated `feature_store.yaml` into the initialized repository. +2. **`feast-apply`** — runs `feast apply` to register feature definitions in the registry. Controlled by `runFeastApplyOnInit` (defaults to `true`). Skipped when `disableInitContainers` is `true`. + +In air-gapped environments, use `feastProjectDir.packaged` to identify a feature repository baked into an image. The operator supports two lifecycle modes: + +* Keep init containers enabled to refresh shared storage from the image, generate configuration from the FeatureStore CR, and optionally run `feast apply`. +* Set `services.disableInitContainers: true` to run directly from the baked path and treat its `feature_store.yaml` as authoritative. + +### Air-gapped deployment workflow + +```mermaid +graph TD + subgraph BuildEnv["Build Environment (internet access)"] + Code["Feature repo source code"] + Base["Base Feast image
(feastdev/feature-server)"] + Custom["Custom image with
bundled feature repo"] + Code --> Custom + Base --> Custom + end + + subgraph InternalRegistry["Internal Container Registry"] + Mirror["registry.internal.example.com
/feast/feature-server:release"] + end + + subgraph AirGappedCluster["Air-Gapped Kubernetes Cluster"] + SA["ServiceAccount
(imagePullSecrets)"] + CR["FeatureStore CR
feastProjectDir.packaged
disableInitContainers: true"] + Deploy["Feast Deployment
(no init containers)"] + SA --> Deploy + CR --> Deploy + end + + Custom -->|push| Mirror + Mirror -->|pull| Deploy +``` + +**Steps:** + +1. **Build a custom container image** that bundles the feature repository and all Python dependencies into the Feast base image. +2. **Push** the image to your internal container registry. +3. **Configure `feastProjectDir.packaged`** with the image and the canonical absolute path to the bundled repository. Do not use `.`, `..`, repeated separators, or a trailing separator, and keep the path outside operator-mounted locations such as `/feast-data` so it cannot overlap the staged repository. +4. **Choose the lifecycle:** leave init containers enabled for operator-managed configuration and `feast apply`, or set `services.disableInitContainers: true` to use the baked repository and configuration directly. +5. **Set `imagePullPolicy: IfNotPresent`** (or `Never` if images are pre-loaded on nodes). +6. **Configure `imagePullSecrets`** on the namespace's ServiceAccount — the FeatureStore CRD does not expose an `imagePullSecrets` field, so use the standard Kubernetes approach of attaching secrets to the ServiceAccount that the pods run under. + +### Sample FeatureStore CR (air-gapped) + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: airgap-production +spec: + feastProject: my_project + feastProjectDir: + packaged: + image: registry.internal.example.com/feast/feature-server:release + featureRepoPath: /opt/feast/feature_repo + services: + disableInitContainers: true + onlineStore: + persistence: + store: + type: redis + secretRef: + name: feast-online-store + server: + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: "1" + memory: 1Gi + limits: + cpu: "2" + memory: 2Gi + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-registry-store + server: + imagePullPolicy: IfNotPresent +``` + +The packaged image is the default for every Feast service and for the `feast-init` and +`feast-apply` init containers. A per-service `image` still takes precedence for that +service, and `services.initImage` takes precedence for both init containers. Remove +`disableInitContainers: true` to use operator-managed staging and startup apply instead. + +{% hint style="info" %} +**Pre-populating the registry:** With init containers disabled, `feast apply` does not run on pod startup. You can populate the registry by: + +1. **Running `feast apply` from your CI/CD pipeline** that has network access to the registry DB. +2. **Using the FeatureStore CR's built-in CronJob** (`spec.cronJob`) — the operator creates a Kubernetes CronJob that runs `feast apply` and `feast materialize-incremental` on a schedule. The CronJob runs inside the cluster (no external access needed) and can use a custom image just like the main deployment. This is the recommended approach for air-gapped environments. +3. **Running `feast apply` manually** from the build environment before deploying the CR. +{% endhint %} + +### Air-gapped deployment checklist + +{% hint style="warning" %} +**Pre-stage the following artifacts before deploying Feast in an air-gapped environment:** + +* **Container images** — Feast feature server image (with bundled feature repo) pushed to internal registry +* **CRD manifests** — Feast Operator CRDs and operator deployment manifests available locally +* **Store credentials** — Kubernetes Secrets for online store, offline store, and registry DB connections created in the target namespace +* **Python packages** (if using custom on-demand transforms) — bundled into the custom image or available from an internal PyPI mirror +* **ServiceAccount configuration** — `imagePullSecrets` attached to the ServiceAccount used by the Feast deployment +{% endhint %} + +--- + +## Hybrid Store Configuration + +The hybrid store feature allows a single Feast deployment to route feature operations to multiple backends based on tags or data sources. This is useful when different feature views have different latency, cost, or compliance requirements. + +### Hybrid online store + +The `HybridOnlineStore` routes online operations to different backends based on a configurable tag on the `FeatureView`. + +```mermaid +graph LR + FS["Online Feature Server"] --> Router["HybridOnlineStore
(routes by tag)"] + Router -->|"tag: dynamodb"| DDB["DynamoDB"] + Router -->|"tag: redis"| RD["Redis"] + + style Router fill:#fff3e0,stroke:#f57c00,color:#000 + style DDB fill:#e3f2fd,stroke:#1976d2,color:#000 + style RD fill:#fce4ec,stroke:#c62828,color:#000 +``` + +**`feature_store.yaml` configuration:** + +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: hybrid + routing_tag: team + online_stores: + - type: dynamodb + conf: + region: us-east-1 + - type: redis + conf: + connection_string: "redis-cluster:6379" + redis_type: redis_cluster +``` + +**Feature view with routing tag:** + +```python +from feast import FeatureView + +user_features = FeatureView( + name="user_features", + entities=[user_entity], + source=user_source, + tags={"team": "dynamodb"}, # Routes to DynamoDB backend +) + +transaction_features = FeatureView( + name="transaction_features", + entities=[txn_entity], + source=txn_source, + tags={"team": "redis"}, # Routes to Redis backend +) +``` + +The tag value must match the online store `type` name (e.g. `dynamodb`, `redis`, `bigtable`). + +### Hybrid offline store + +The `HybridOfflineStore` routes offline operations to different backends based on the `batch_source` type of each `FeatureView`. + +```mermaid +graph LR + Client["Materialization /
Training Job"] --> Router["HybridOfflineStore
(routes by source type)"] + Router -->|"SparkSource"| Spark["Spark"] + Router -->|"RedshiftSource"| RS["Redshift"] + + style Router fill:#fff3e0,stroke:#f57c00,color:#000 + style Spark fill:#e8f5e9,stroke:#388e3c,color:#000 + style RS fill:#e3f2fd,stroke:#1976d2,color:#000 +``` + +**`feature_store.yaml` configuration:** + +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +offline_store: + type: hybrid_offline_store.HybridOfflineStore + offline_stores: + - type: spark + conf: + spark_master: local[*] + spark_app_name: feast_spark_app + - type: redshift + conf: + cluster_id: my-redshift-cluster + region: us-east-1 + database: feast_db + user: feast_user + s3_staging_location: s3://my-bucket/feast-staging + iam_role: arn:aws:iam::123456789012:role/FeastRedshiftRole +``` + +**Feature views with different sources:** + +```python +from feast import FeatureView, Entity, ValueType +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import SparkSource +from feast.infra.offline_stores.redshift_source import RedshiftSource + +user_features = FeatureView( + name="user_features", + entities=[user_entity], + source=SparkSource(path="s3://bucket/user_features"), # Routes to Spark +) + +activity_features = FeatureView( + name="user_activity", + entities=[user_entity], + source=RedshiftSource( # Routes to Redshift + table="user_activity", + event_timestamp_column="event_ts", + ), +) +``` + +{% hint style="warning" %} +**Hybrid offline store constraint:** `get_historical_features` requires all requested feature views to share the same `batch_source` type within a single call. You cannot join features across different offline engines in one retrieval request. +{% endhint %} + +--- + +## Performance Considerations + +For detailed server-level tuning (worker counts, timeouts, keep-alive, etc.), see the [Online Server Performance Tuning](./online-server-performance-tuning.md) guide. + +### Online feature server sizing + +| Traffic tier | Replicas | CPU (per pod) | Memory (per pod) | Notes | +|---|---|---|---|---| +| Low (<100 RPS) | 1–2 | 500m–1 | 512Mi–1Gi | Minimal production | +| Medium (100–1000 RPS) | 2–5 (HPA) | 1–2 | 1–2Gi | Standard production | +| High (>1000 RPS) | 5–20 (HPA) | 2–4 | 2–4Gi | Enterprise, per-tenant | + +### Online store latency guidelines + +| Store | p50 latency | p99 latency | Best for | +|---|---|---|---| +| **Redis** (single) | <1ms | <5ms | Lowest latency, small-medium datasets | +| **Redis Cluster** | <2ms | <10ms | High availability + low latency | +| **DynamoDB** | <5ms | <20ms | Serverless, variable traffic | +| **PostgreSQL** | <5ms | <30ms | On-prem, simplicity | +| **Remote (HTTP)** | <10ms | <50ms | Client-server separation | + +### Connection pooling for remote online store + +When using the [Remote Online Store](../reference/online-stores/remote.md) (client-server architecture), connection pooling significantly reduces latency by reusing TCP/TLS connections: + +```yaml +online_store: + type: remote + path: http://feast-feature-server:80 + connection_pool_size: 50 # Max connections in pool (default: 50) + connection_idle_timeout: 300 # Idle timeout in seconds (default: 300) + connection_retries: 3 # Retry count with exponential backoff +``` + +**Tuning by workload:** + +| Workload | `connection_pool_size` | `connection_idle_timeout` | `connection_retries` | +|---|---|---|---| +| High-throughput inference | 100 | 600 | 5 | +| Long-running batch service | 50 | 0 (never close) | 3 | +| Resource-constrained edge | 10 | 60 | 2 | + +### Registry performance + +* **SQL registry** (PostgreSQL, MySQL) is required for concurrent materialization jobs writing to the registry simultaneously. +* **File-based registries** (S3, GCS, local) serialize the entire registry on each write — suitable only for single-writer scenarios. +* For read-heavy workloads, scale the Registry Server to multiple replicas (all connecting to the same database). + +### Registry cache tuning at scale + +Each Feast server pod maintains its own in-memory copy of the registry metadata. With multiple Gunicorn workers per pod, the total number of independent registry copies is **replicas x workers**. For example, 5 replicas with 4 workers each means 20 copies of the registry in memory, each refreshing independently. + +With the default `cache_mode: sync`, the refresh is **synchronous** — when the TTL expires, the next request blocks until the full registry is re-downloaded. At scale, this causes periodic latency spikes across multiple pods simultaneously. + +**Recommendation:** Use `cache_mode: thread` with a higher TTL in production to avoid refresh storms: + +```yaml +# In the Operator secret for SQL/DB-backed registries: +registry: + registry_type: sql + path: postgresql://:@:5432/feast + cache_mode: thread + cache_ttl_seconds: 300 +``` + +For the server-side refresh interval, set `registryTTLSeconds` on the CR: + +```yaml +spec: + services: + onlineStore: + server: + workerConfigs: + registryTTLSeconds: 300 +``` + +| Scenario | `cache_mode` | `cache_ttl_seconds` | `registryTTLSeconds` | +|---|---|---|---| +| Development / iteration | `sync` (default) | 5–10 | 5 | +| Production (low-latency) | `thread` | 300 | 300 | +| Production (frequent schema changes) | `thread` | 60 | 60 | + +{% hint style="info" %} +`registryTTLSeconds` on the CR controls the **server-side** refresh interval. `cache_ttl_seconds` in the registry secret controls the **SDK client** refresh. In Operator deployments, the CR field is what matters for serving performance. For a deep dive into sync vs thread mode trade-offs, memory impact, and freshness considerations, see the [Registry Cache Tuning](./online-server-performance-tuning.md#registry-cache-tuning) section in the performance tuning guide. +{% endhint %} + +### Materialization performance + +| Data volume | Recommended engine | Notes | +|---|---|---| +| <1M rows | In-process (default) | Simple, no external dependencies | +| 1M–100M rows | Snowflake Engine, Spark, or Ray | Distributed processing | +| >100M rows | Spark on Kubernetes / EMR / Dataproc, or Ray via KubeRay | Full cluster-scale materialization with distributed DAG execution | + +For detailed engine configuration, see [Scaling Materialization](./scaling-feast.md#scaling-materialization). + +### Redis sizing guidelines + +| Metric | Guideline | +|---|---| +| **Memory** | ~100 bytes per feature value (varies by data type). For 1M entities x 50 features = ~5GB. | +| **Connections** | Each online feature server replica opens a connection pool. Plan for `replicas x pool_size`. | +| **TTL** | Set `key_ttl_seconds` in `feature_store.yaml` to auto-expire stale data and bound memory usage. | +| **Cluster mode** | Use Redis Cluster for >25GB datasets or >10K connections. | + +--- + +## Design Principles + +Understanding the following principles helps you choose and customize the right topology. + +### Control plane vs data plane + +```mermaid +graph LR + subgraph ControlPlane["Control Plane"] + Operator["Feast Operator"] + Registry["Registry Server"] + end + + subgraph DataPlane["Data Plane"] + OnlineServer["Online Feature Server"] + OfflineServer["Offline Feature Server"] + end + + subgraph BackingStores["Backing Stores (external)"] + OnlineStore["Online Store
(e.g. Redis, DynamoDB, PostgreSQL, etc.)"] + OfflineStore["Offline Store
(e.g. Redshift, BigQuery, Spark, etc.)"] + RegistryDB["Registry DB
(PostgreSQL / MySQL)"] + end + + ControlPlane -->|configures| DataPlane + DataPlane -->|reports status| ControlPlane + OnlineServer --> OnlineStore + OfflineServer --> OfflineStore + Registry --> RegistryDB + + style ControlPlane fill:#e8f5e9,stroke:#388e3c,color:#000 + style DataPlane fill:#e3f2fd,stroke:#1976d2,color:#000 + style BackingStores fill:#fce4ec,stroke:#c62828,color:#000 +``` + +* **Control plane** (Operator + Registry Server) manages feature definitions, metadata, and lifecycle. It changes infrequently and should be highly available. +* **Data plane** (Online Feature Server + Offline Feature Server) handles the actual feature reads/writes at request time. It must scale with traffic. +* **Backing stores** (databases, object storage) hold the actual data. These are stateful and managed independently. + +### Stateless vs stateful components + +The Feast Operator deploys all Feast services (Online Feature Server, Offline Feature Server, Registry Server) in a **single shared Deployment**. When scaling (`spec.replicas > 1` or HPA autoscaling), all services scale together. + +{% hint style="warning" %} +**Scaling requires DB-backed persistence for all enabled services.** The operator enforces this via CRD validation: + +* **Online Store** — must use DB persistence (e.g. `type: redis`, `type: dynamodb`, `type: postgres`) +* **Offline Store** — if enabled, must use DB persistence (e.g. `type: redshift`, `type: bigquery`, `type: spark`, `type: postgres`) +* **Registry** — must use SQL persistence (`type: sql`), a remote registry, or S3/GCS file-backed registry + +File-based stores (SQLite, DuckDB, `registry.db`) are **rejected** when `replicas > 1` or autoscaling is configured. +{% endhint %} + +| Component | Type | Scaling | DB-backed requirement | +|---|---|---|---| +| Online Feature Server | **Stateless** (server) | Scales with the shared Deployment (HPA or `spec.replicas`) | Online store must use DB persistence (e.g. Redis, DynamoDB, PostgreSQL) | +| Offline Feature Server | **Stateless** (server) | Scales with the shared Deployment (HPA or `spec.replicas`) | Offline store must use DB persistence (e.g. Redshift, BigQuery, Spark, PostgreSQL) | +| Registry Server | **Stateless** (server) | Scales with the shared Deployment (HPA or `spec.replicas`) | Registry must use SQL, remote, or S3/GCS persistence | +| Online Store (Redis, DynamoDB, etc.) | **Stateful** (backing store) | Scale via managed service or clustering | Managed independently of Feast services | +| Offline Store (Redshift, BigQuery, etc.) | **Stateful** (backing store) | Scale via cloud-managed infrastructure | Managed independently of Feast services | +| Registry DB (PostgreSQL, MySQL) | **Stateful** (backing store) | Scale via managed database service | Managed independently of Feast services | + +### Scalability guidelines + +```mermaid +graph TD + Read["Read traffic increase"] -->|scale| FS["Online Feature Server replicas (HPA)"] + Write["Write / materialization load"] -->|scale| Engine["Compute Engine
(Spark / Ray / Snowflake)"] + Storage["Data volume growth"] -->|scale| Store["Online / Offline Store capacity"] + + FS -.- Independent["Scale independently"] + Engine -.- Independent + Store -.- Independent + + style Read fill:#e3f2fd,stroke:#1976d2,color:#000 + style Write fill:#fff3e0,stroke:#f57c00,color:#000 + style Storage fill:#f3e5f5,stroke:#7b1fa2,color:#000 + style FS fill:#e3f2fd,stroke:#1976d2,color:#000 + style Engine fill:#fff3e0,stroke:#f57c00,color:#000 + style Store fill:#f3e5f5,stroke:#7b1fa2,color:#000 +``` + +* **Read scaling** — increase Online Feature Server replicas; they are stateless and scale linearly. +* **Write scaling** — use a distributed compute engine ([Spark](../reference/compute-engine/spark.md), [Ray/KubeRay](../reference/compute-engine/ray.md), or [Snowflake](../reference/compute-engine/snowflake.md)) for materialization. +* **Storage scaling** — scale online and offline stores independently based on data volume and query patterns. + +For detailed scaling configuration, see [Scaling Feast](./scaling-feast.md). + +--- + +## Topology Comparison + +| Capability | Minimal | Standard | Enterprise | +|---|:---:|:---:|:---:| +| **High availability** | No | Yes | Yes | +| **Autoscaling** | No | HPA | HPA + Cluster Autoscaler | +| **TLS / Ingress** | No | Yes | Yes + API Gateway | +| **RBAC** | No | Kubernetes RBAC | OIDC + fine-grained RBAC | +| **Multi-tenancy** | No | No | Namespace-per-team | +| **Shared registry** | N/A | N/A | Optional (remote registry) | +| **Hybrid stores** | No | Optional | Recommended for mixed backends | +| **Observability** | Logs only | Basic metrics | OpenTelemetry + Prometheus + Grafana + Jaeger | +| **Disaster recovery** | No | Partial | Full backup/restore | +| **Network policies** | No | Optional | Enforced | +| **Recommended team size** | 1–3 | 3–15 | 15+ | + +--- + +## Next Steps + +* [Feast on Kubernetes](./feast-on-kubernetes.md) — install the Feast Operator and deploy your first FeatureStore CR +* [Scaling Feast](./scaling-feast.md) — detailed HPA, registry scaling, and materialization engine configuration +* [Online Server Performance Tuning](./online-server-performance-tuning.md) — worker counts, timeouts, keep-alive, and server-level tuning +* [Starting Feast Servers in TLS Mode](./starting-feast-servers-tls-mode.md) — enable TLS for secure communication +* [Running Feast in Production](./running-feast-in-production.md) — CI/CD, materialization scheduling, and model serving patterns +* [Multi-Team Feature Store Setup](./federated-feature-store.md) — federated feature store for multi-team environments +* [Permission Concepts](../getting-started/concepts/permission.md) — full permission model reference +* [RBAC Architecture](../getting-started/architecture/rbac.md) — authorization architecture details +* [OpenTelemetry Integration](../getting-started/components/open-telemetry.md) — traces and metrics for Feast servers +* [Hybrid Online Store](../reference/online-stores/hybrid.md) — hybrid online store configuration reference +* [Hybrid Offline Store](../reference/offline-stores/hybrid.md) — hybrid offline store configuration reference diff --git a/docs/how-to-guides/running-feast-in-production.md b/docs/how-to-guides/running-feast-in-production.md index 06887499739..d26e0234b2e 100644 --- a/docs/how-to-guides/running-feast-in-production.md +++ b/docs/how-to-guides/running-feast-in-production.md @@ -18,6 +18,10 @@ For example, you might not have a stream source and, thus, no need to write feat Additionally, please check the how-to guide for some specific recommendations on [how to scale Feast](./scaling-feast.md). {% endhint %} +{% hint style="info" %} +**Looking for production deployment patterns?** See the [Feast Production Deployment Topologies](./production-deployment-topologies.md) guide for three Kubernetes-ready topologies (Minimal, Standard, Enterprise), sample FeatureStore CRs, RBAC policies, infrastructure recommendations, and scaling best practices. +{% endhint %} + In this guide we will show you how to: 1. Deploy your feature store and keep your infrastructure in sync with your feature repository @@ -71,6 +75,15 @@ Feast keeps the history of materialization in its registry so that the choice co However, the amount of work can quickly outgrow the resources of a single machine. That happens because the materialization job needs to repackage all rows before writing them to an online store. That leads to high utilization of CPU and memory. In this case, you might want to use a job orchestrator to run multiple jobs in parallel using several workers. Kubernetes Jobs or Airflow are good choices for more comprehensive job orchestration. +For large datasets, you can also reduce peak memory on the materialization worker by setting `online_write_batch_size` in `feature_store.yaml`. This breaks the proto conversion and write into chunks instead of loading the entire dataset into memory at once: + +```yaml +materialization: + online_write_batch_size: 10000 # rows per write batch; reduces peak memory proportionally +``` + +See the [Materialization write performance](./online-server-performance-tuning.md#materialization-write-performance) guide for sizing recommendations and the full option reference in [feature_store.yaml](../reference/feature-repository/feature-store-yaml.md#online_write_batch_size). + If you are using Airflow as a scheduler, Feast can be invoked through a [PythonOperator](https://airflow.apache.org/docs/apache-airflow/stable/howto/operator/python.html) after the [Python SDK](https://pypi.org/project/feast/) has been installed into a virtual environment and your feature repo has been synced: ```python @@ -204,54 +217,7 @@ feature_vector = fs.get_online_features( ``` ### 4.2. Deploy Feast feature servers on Kubernetes - -To deploy a Feast feature server on Kubernetes, you should use the included [feast-operator](../../infra/feast-operator). - -{% embed url="https://www.youtube.com/playlist?list=PLPzVNzik7rsAN-amQLZckd0so3cIr7blX" %} - -**Basic steps** -1. Install [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) -2. Install the Operator - - -Install the latest release -```sh -kubectl apply -f https://raw.githubusercontent.com/feast-dev/feast/refs/heads/stable/infra/feast-operator/dist/install.yaml -``` - -OR, install a specific version - -``` -kubectl apply -f https://raw.githubusercontent.com/feast-dev/feast/refs/tags//infra/feast-operator/dist/install.yaml -``` - -3. Deploy a Feature Store - -```sh -kubectl apply -f https://raw.githubusercontent.com/feast-dev/feast/refs/heads/stable/infra/feast-operator/config/samples/v1alpha1_featurestore.yaml -``` -Verify the status -``` -$ kubectl get feast -NAME STATUS AGE -sample Ready 2m21s -``` - -The above will install a simple [FeatureStore CR](../../infra/feast-operator/docs/api/markdown/ref.md) like the following. By default, it will run the [Online Store feature server](../reference/feature-servers/python-feature-server.md) - -```yaml -apiVersion: feast.dev/v1alpha1 -kind: FeatureStore -metadata: - name: sample -spec: - feastProject: my_project -``` -> _More advanced FeatureStore CR examples can be found in the feast-operator [samples directory](../../infra/feast-operator/config/samples)._ - -For first-time Operator users, it may be a good exercise to try the [Feast Operator Quickstart](../../examples/operator-quickstart). The quickstart will demonstrate some of the Operator's built-in features, e.g. git repos, `feast apply` jobs, etc. - -{% hint style="success" %} Important note: Scaling a Feature Store Deployment should only be done if the configured data store(s) will support it. - -Please check the how-to guide for some specific recommendations on [how to scale Feast](./scaling-feast.md). {% endhint %} +See [Feast on Kubernetes](./feast-on-kubernetes.md). ## 5. Using environment variables in your yaml configuration diff --git a/docs/how-to-guides/scaling-feast.md b/docs/how-to-guides/scaling-feast.md index d0bd6aef8a0..5982f917674 100644 --- a/docs/how-to-guides/scaling-feast.md +++ b/docs/how-to-guides/scaling-feast.md @@ -23,4 +23,226 @@ However, this process does not scale for large data sets, since it's executed on Feast supports pluggable [Compute Engines](../getting-started/components/compute-engine.md), that allow the materialization process to be scaled up. Aside from the local process, Feast supports a [Lambda-based materialization engine](https://rtd.feast.dev/en/master/#alpha-lambda-based-engine), and a [Bytewax-based materialization engine](https://rtd.feast.dev/en/master/#bytewax-engine). -Users may also be able to build an engine to scale up materialization using existing infrastructure in their organizations. \ No newline at end of file +Users may also be able to build an engine to scale up materialization using existing infrastructure in their organizations. + +### Horizontal Scaling with the Feast Operator + +When running Feast on Kubernetes with the [Feast Operator](./feast-on-kubernetes.md), you can horizontally scale the FeatureStore deployment using `spec.replicas` or HPA autoscaling. The FeatureStore CRD implements the Kubernetes [scale sub-resource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource), so you can also use `kubectl scale`: + +```bash +kubectl scale featurestore/my-feast --replicas=3 +``` + +**Prerequisites:** Horizontal scaling requires **DB-backed persistence** for all enabled services (online store, offline store, and registry). File-based persistence (SQLite, DuckDB, `registry.db`) is incompatible with multiple replicas because these backends do not support concurrent access from multiple pods. + +#### Static Replicas + +Set a fixed number of replicas via `spec.replicas`: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-scaling +spec: + feastProject: my_project + replicas: 3 + services: + onlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-data-stores + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-data-stores +``` + +#### Autoscaling with HPA + +Configure a HorizontalPodAutoscaler to dynamically scale based on metrics. HPA autoscaling is configured under `services.scaling.autoscaling` and is mutually exclusive with `spec.replicas > 1`: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-autoscaling +spec: + feastProject: my_project + services: + scaling: + autoscaling: + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + podDisruptionBudgets: + maxUnavailable: 1 + onlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-data-stores + server: + resources: + requests: + cpu: 200m + memory: 256Mi + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-data-stores +``` + +{% hint style="info" %} +When autoscaling is configured, the operator automatically sets the deployment strategy to `RollingUpdate` (instead of the default `Recreate`) to ensure zero-downtime scaling, and auto-injects soft pod anti-affinity and zone topology spread constraints. You can override any of these by explicitly setting `deploymentStrategy`, `affinity`, or `topologySpreadConstraints` in the CR. +{% endhint %} + +#### Validation Rules + +The operator enforces the following rules: +- `spec.replicas > 1` and `services.scaling.autoscaling` are **mutually exclusive** -- you cannot set both. +- Scaling with `replicas > 1` or any `autoscaling` config is **rejected** if any enabled service uses file-based persistence. +- S3 (`s3://`) and GCS (`gs://`) backed registry file persistence is allowed with scaling, since these object stores support concurrent readers. + +#### High Availability + +When scaling is enabled (`replicas > 1` or `autoscaling`), the operator provides HA features to improve resilience: + +**Pod Anti-Affinity** — The operator automatically injects a soft (`preferredDuringSchedulingIgnoredDuringExecution`) pod anti-affinity rule that prefers spreading pods across different nodes. This prevents multiple replicas from being co-located on the same node, improving resilience to node failures. You can override this by providing your own `affinity` configuration: + +```yaml +spec: + replicas: 3 + services: + # Override with custom affinity (e.g. strict anti-affinity) + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + feast.dev/name: my-feast + # ... +``` + +**Topology Spread Constraints** — The operator automatically injects a soft zone-spread constraint (`whenUnsatisfiable: ScheduleAnyway`) that distributes pods across availability zones. This is a best-effort spread — if zones are unavailable, pods will still be scheduled. You can override this with explicit constraints or disable it with an empty array: + +```yaml +spec: + replicas: 3 + services: + # Override with custom topology spread (e.g. strict zone spreading) + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + feast.dev/name: my-feast + # ... +``` + +To disable the auto-injected topology spread: + +```yaml +spec: + replicas: 3 + services: + topologySpreadConstraints: [] + # ... +``` + +**PodDisruptionBudget** — You can configure a PDB to limit voluntary disruptions (e.g. during node drains or cluster upgrades). The PDB is only created when scaling is enabled. Exactly one of `minAvailable` or `maxUnavailable` must be set: + +```yaml +spec: + replicas: 3 + services: + podDisruptionBudgets: + maxUnavailable: 1 # at most 1 pod unavailable during disruptions + # -- OR -- + # podDisruptionBudgets: + # minAvailable: "50%" # at least 50% of pods must remain available + # ... +``` + +{% hint style="info" %} +The PDB is not auto-injected — you must explicitly configure it. This is intentional because a misconfigured PDB (e.g. `minAvailable` equal to the replica count) can block node drains and cluster upgrades. +{% endhint %} + +#### Using KEDA (Kubernetes Event-Driven Autoscaling) + +[KEDA](https://keda.sh) is also supported as an external autoscaler. KEDA should target the FeatureStore's scale sub-resource directly (since it implements the Kubernetes scale API). This is the recommended approach because the operator manages the Deployment's replica count from `spec.replicas` — targeting the Deployment directly would conflict with the operator's reconciliation. + +When using KEDA, do **not** set `scaling.autoscaling` or `spec.replicas > 1` -- KEDA manages the replica count through the scale sub-resource. + +1. **Ensure DB-backed persistence** -- The CRD's CEL validation rules automatically enforce DB-backed persistence when KEDA scales `spec.replicas` above 1 via the scale sub-resource. The operator also automatically switches the deployment strategy to `RollingUpdate` when `replicas > 1`. + +2. **Configure the FeatureStore** with DB-backed persistence: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-keda +spec: + feastProject: my_project + services: + onlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-data-stores + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-data-stores +``` + +3. **Create a KEDA `ScaledObject`** targeting the FeatureStore resource: + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: feast-scaledobject +spec: + scaleTargetRef: + apiVersion: feast.dev/v1 + kind: FeatureStore + name: sample-keda + minReplicaCount: 1 + maxReplicaCount: 10 + triggers: + - type: prometheus + metadata: + serverAddress: http://prometheus.monitoring.svc:9090 + metricName: http_requests_total + query: sum(rate(http_requests_total{service="feast"}[2m])) + threshold: "100" +``` + +{% hint style="warning" %} +KEDA-created HPAs are not owned by the Feast operator. The operator will not interfere with them, but it also will not clean them up if the FeatureStore CR is deleted. You must manage the KEDA `ScaledObject` lifecycle independently. +{% endhint %} + +For the full API reference, see the [FeatureStore CRD reference](../../infra/feast-operator/docs/api/markdown/ref.md). \ No newline at end of file diff --git a/docs/how-to-guides/starting-feast-servers-tls-mode.md b/docs/how-to-guides/starting-feast-servers-tls-mode.md index ffc7e5d9e90..c3696a35532 100644 --- a/docs/how-to-guides/starting-feast-servers-tls-mode.md +++ b/docs/how-to-guides/starting-feast-servers-tls-mode.md @@ -128,6 +128,52 @@ auth: `cert` is an optional configuration to the public certificate path when the registry server starts in TLS(SSL) mode. Typically, this file ends with `*.crt`, `*.cer`, or `*.pem`. +### Feast client connecting to remote registry server with mTLS + +If the Registry Server requires mutual TLS (mTLS), the client must present a certificate and private key in addition to trusting the server's CA certificate. Add `client_cert` and `client_key` to the registry configuration: + +```yaml +project: feast-project +registry: + registry_type: remote + path: feature-registry.example.com:443 + cert: /path/to/ca.crt + client_cert: /path/to/tls.crt + client_key: /path/to/tls.key +provider: local +online_store: + path: http://localhost:6566 + type: remote +entity_key_serialization_version: 3 +auth: + type: no_auth +``` + +* `cert` — CA certificate used to verify the server (or use the `SSL_CERT_FILE` / `REQUESTS_CA_BUNDLE` environment variable). +* `client_cert` — Client certificate presented to the server. Must be paired with `client_key`. +* `client_key` — Private key for the client certificate. + +#### Connecting through a tunnel or proxy + +When connecting through a tunnel (e.g. `gcloud compute start-iap-tunnel`) the client connects to `localhost`, but the server certificate is issued for the real service hostname. Set the `authority` field so that gRPC's TLS hostname verification passes: + +```shell +# In one terminal — start the tunnel: +gcloud compute start-iap-tunnel feature-registry.example.com 443 --local-host-port=localhost:8443 +``` + +```yaml +registry: + registry_type: remote + path: localhost:8443 + cert: /path/to/ca.crt + client_cert: /path/to/tls.crt + client_key: /path/to/tls.key + authority: feature-registry.example.com +``` + +Without `authority`, the gRPC client would check the server certificate against `localhost`, which would fail because the certificate's Subject Alternative Name (SAN) is `feature-registry.example.com`. + ## Starting feast offline server in TLS mode To start the offline server in TLS mode, you need to provide the private and public keys using the `--key` and `--cert` arguments with the `feast serve_offline` command. diff --git a/docs/project/contributing.md b/docs/project/contributing.md index cded378951d..25ccd80703f 100644 --- a/docs/project/contributing.md +++ b/docs/project/contributing.md @@ -5,6 +5,8 @@ After familiarizing yourself with the documentation, the simplest way to get sta 1. Setup your developer environment by following [development guide](development-guide.md). 2. Either create a [GitHub issue](https://github.com/feast-dev/feast/issues) or make a draft PR (following [development guide](development-guide.md)) to get the ball rolling! +> **Reporting a security vulnerability?** Do not open an issue or PR. Report it privately through [GitHub's advisory form](https://github.com/feast-dev/feast/security/advisories/new); see the [security policy](https://github.com/feast-dev/feast/blob/master/SECURITY.md). + ## Decision making process *See [governance](../../community/governance.md) for more details here* @@ -22,9 +24,22 @@ PRs that are submitted by the general public need to be identified as `ok-to-tes See also [Making a pull request](development-guide.md#making-a-pull-request) for other guidelines on making pull requests in Feast. +## RFCs and Architecture Decision Records + +For substantial changes (new features, architecture changes, removing features), we use an RFC process. See the [governance document](../../community/governance.md#rfcs-process) for details. + +Once an RFC is finalized and approved, it should be recorded as an Architecture Decision Record (ADR) in the [`docs/adr/`](../adr/README.md) directory. This ensures that architectural decisions are version-controlled alongside the codebase and easily accessible to all contributors. + +To add a finalized RFC as an ADR: + +1. Copy the [ADR template](../adr/ADR-TEMPLATE.md) to a new file with the next sequential number. +2. Summarize the RFC's context, decision, and consequences. +3. Submit a pull request with the new ADR. + ## Resources - [Community](../community.md) for other ways to get involved with the community - [Development guide](development-guide.md) for tips on how to contribute - [Feast GitHub issues](https://github.com/feast-dev/feast/issues) to see what others are working on -- [Feast RFCs](https://drive.google.com/drive/u/0/folders/1msUsgmDbVBaysmhBlg9lklYLLTMk4bC3) for a folder of previously written RFCs \ No newline at end of file +- [Feast RFCs](https://drive.google.com/drive/u/0/folders/1msUsgmDbVBaysmhBlg9lklYLLTMk4bC3) for a folder of previously written RFCs +- [Architecture Decision Records](../adr/README.md) for documented architectural decisions \ No newline at end of file diff --git a/docs/project/development-guide.md b/docs/project/development-guide.md index 6912fd2c091..ee5cc8cfcce 100644 --- a/docs/project/development-guide.md +++ b/docs/project/development-guide.md @@ -91,13 +91,12 @@ See [Creating a pull request from a fork](https://docs.github.com/en/github/coll ### Pre-commit Hooks Setup [`pre-commit`](https://pre-commit.com/) to automatically lint and format the codebase on commit: -1. Ensure that you have Python (3.7 and above) with `pip`, installed. -2. Install `pre-commit` with `pip` & install pre-push hooks +1. Ensure that you have Python (3.10 and above) with [uv](https://docs.astral.sh/uv/) installed. +2. Install `pre-commit` and hooks: ```sh -pip install pre-commit -pre-commit install --hook-type pre-commit --hook-type pre-push +make install-precommit ``` -3. On push, the pre-commit hook will run. This runs `make format` and `make lint`. +3. On commit, the pre-commit hook will run. This runs `make format` and `make lint`. ### Signing off commits > :warning: Warning: using the default integrations with IDEs like VSCode or IntelliJ will not sign commits. @@ -218,7 +217,7 @@ make test-python-integration-local To test across clouds, on top of setting up Redis, you also need GCP / AWS / Snowflake setup. > Note: you can manually control what tests are run today by inspecting -> [RepoConfiguration](https://github.com/feast-dev/feast/blob/master/sdk/python/tests/integration/feature_repos/repo_configuration.py) +> [RepoConfiguration](https://github.com/feast-dev/feast/blob/master/sdk/python/tests/universal/feature_repos/repo_configuration.py) > and commenting out tests that are added to `DEFAULT_FULL_REPO_CONFIGS` **GCP** @@ -351,18 +350,18 @@ You can run `make test-python-integration-container` to run tests against the co ### Contrib integration tests #### (Contrib) Running tests for Spark offline store -You can run `make test-python-universal-spark` to run all tests against the Spark offline store. (Note: you'll have to run `pip install -e ".[dev]"` first). +You can run `make test-python-universal-spark` to run all tests against the Spark offline store. (Note: you'll have to run `make install-python-dependencies-dev` first). Not all tests are passing yet #### (Contrib) Running tests for Trino offline store -You can run `make test-python-universal-trino` to run all tests against the Trino offline store. (Note: you'll have to run `pip install -e ".[dev]"` first) +You can run `make test-python-universal-trino` to run all tests against the Trino offline store. (Note: you'll have to run `make install-python-dependencies-dev` first) #### (Contrib) Running tests for Postgres offline store -You can run `test-python-universal-postgres-offline` to run all tests against the Postgres offline store. (Note: you'll have to run `pip install -e ".[dev]"` first) +You can run `test-python-universal-postgres-offline` to run all tests against the Postgres offline store. (Note: you'll have to run `make install-python-dependencies-dev` first) #### (Contrib) Running tests for Postgres online store -You can run `test-python-universal-postgres-online` to run all tests against the Postgres offline store. (Note: you'll have to run `pip install -e ".[dev]"` first) +You can run `test-python-universal-postgres-online` to run all tests against the Postgres offline store. (Note: you'll have to run `make install-python-dependencies-dev` first) #### (Contrib) Running tests for HBase online store TODO diff --git a/docs/reference/alpha-feature-view-versioning.md b/docs/reference/alpha-feature-view-versioning.md new file mode 100644 index 00000000000..5cdf2845ebc --- /dev/null +++ b/docs/reference/alpha-feature-view-versioning.md @@ -0,0 +1,229 @@ +# \[Alpha\] Feature View Versioning + +{% hint style="warning" %} +**Warning**: This is an _experimental_ feature. It is stable but there are still rough edges. Contributions are welcome! +{% endhint %} + +## Overview + +Feature view versioning automatically tracks schema and UDF changes to feature views. Every time `feast apply` detects a change, a versioned snapshot is saved to the registry. This enables: + +- **Audit trail** — see what a feature view looked like at any point in time +- **Safe rollback** — pin serving to a prior version with `version="v0"` in your definition +- **Multi-version serving** — serve both old and new schemas simultaneously using `@v` syntax +- **Staged publishing** — use `feast apply --no-promote` to publish a new version without making it the default + +## How It Works + +Version tracking is fully automatic. You don't need to set any version parameter — just use `feast apply` as usual: + +1. **First apply** — Your feature view definition is saved as **v0**. +2. **Change something and re-apply** — Feast detects the change, saves the old definition as a snapshot, and saves the new one as **v1**. The version number auto-increments on each real change. +3. **Re-apply without changes** — Nothing happens. Feast compares the new definition against the active one and skips creating a version if they're identical (idempotent). +4. **Another change** — Creates **v2**, and so on. + +``` +feast apply # First apply → v0 +# ... edit schema ... +feast apply # Detects change → v1 +feast apply # No change detected → still v1 (no new version) +# ... edit source ... +feast apply # Detects change → v2 +``` + +**Key details:** + +* **Automatic snapshots**: Versions are created only when Feast detects an actual change to the feature view definition (schema or UDF). Metadata-only changes (description, tags, TTL) update in place without creating a new version. +* **Separate history storage**: Version history is stored separately from the active feature view definition, keeping the main registry lightweight. +* **Backward compatible**: The `version` parameter is fully optional. Omitting it (or setting `version="latest"`) preserves existing behavior — you get automatic versioning with zero changes to your code. + +## Configuration + +{% hint style="info" %} +Version history tracking is **always active** — no configuration needed. Every `feast apply` that changes a feature view automatically records a version snapshot. + +To enable **versioned online reads** (e.g., `fv@v2:feature`), add `enable_online_feature_view_versioning: true` to your registry config in `feature_store.yaml`: + +```yaml +registry: + path: data/registry.db + enable_online_feature_view_versioning: true +``` + +When this flag is off, version-qualified refs (e.g., `fv@v2:feature`) in online reads will raise errors, but version history, version listing, version pinning, and version lookups all work normally. +{% endhint %} + +## Pinning to a Specific Version + +You can pin a feature view to a specific historical version by setting the `version` parameter. When pinned, `feast apply` replaces the active feature view with the snapshot from that version. This is useful for reverting to a known-good definition. + +```python +from feast import FeatureView + +# Default behavior: always use the latest version (auto-increments on changes) +driver_stats = FeatureView( + name="driver_stats", + entities=[driver], + schema=[...], + source=my_source, +) + +# Pin to a specific version (reverts the active definition to v2's snapshot) +driver_stats = FeatureView( + name="driver_stats", + entities=[driver], + schema=[...], + source=my_source, + version="v2", # also accepts "version2" +) +``` + +When pinning, the feature view definition (schema, source, transformations, etc.) must match the currently active definition. If you've also modified the definition alongside the pin, `feast apply` will raise a `FeatureViewPinConflict` error. To apply changes, use `version="latest"`. To revert, only change the `version` parameter. + +The snapshot's content replaces the active feature view. Version history is not modified by a pin; the existing v0, v1, v2, etc. snapshots remain intact. + +After reverting with a pin, you can go back to normal auto-incrementing behavior by removing the `version` parameter (or setting it to `"latest"`) and running `feast apply` again. If the restored definition differs from the pinned snapshot, a new version will be created. + +### Version string formats + +| Format | Meaning | +|--------|---------| +| `"latest"` (or omitted) | Always use the latest version (auto-increments on changes) | +| `"v0"`, `"v1"`, `"v2"`, ... | Pin to a specific version number | +| `"version0"`, `"version1"`, ... | Equivalent long form (case-insensitive) | + +## Staged Publishing (`--no-promote`) + +By default, `feast apply` atomically saves a version snapshot **and** promotes it to the active definition. For breaking schema changes, you may want to stage the new version without disrupting unversioned consumers. + +The `--no-promote` flag saves the version snapshot without updating the active feature view definition. The new version is accessible only via explicit `@v` reads and `--version` materialization. + +**CLI usage:** + +```bash +feast apply --no-promote +``` + +**Python SDK equivalent:** + +```python +store.apply([entity, feature_view], no_promote=True) +``` + +### Phased rollout workflow + +1. **Stage the new version:** + ```bash + feast apply --no-promote + ``` + This publishes v2 without promoting it. All unversioned consumers continue using v1. + +2. **Populate the v2 online table:** + ```bash + feast materialize --views driver_stats --version v2 ... + ``` + +3. **Migrate consumers one at a time:** + - Consumer A switches to `driver_stats@v2:trips_today` + - Consumer B switches to `driver_stats@v2:avg_rating` + +4. **Promote v2 as the default:** + ```bash + feast apply + ``` + Or pin to v2: set `version="v2"` in the definition and run `feast apply`. + +## Listing Version History + +Use the CLI to inspect version history: + +```bash +feast feature-views list-versions driver_stats +``` + +```text +VERSION TYPE CREATED VERSION_ID +v0 feature_view 2024-01-15 10:30:00 a1b2c3d4-... +v1 feature_view 2024-01-16 14:22:00 e5f6g7h8-... +v2 feature_view 2024-01-20 09:15:00 i9j0k1l2-... +``` + +Or programmatically via the Python SDK: + +```python +store = FeatureStore(repo_path=".") +versions = store.list_feature_view_versions("driver_stats") +for v in versions: + print(f"{v['version']} created at {v['created_timestamp']}") +``` + +## Version-Qualified Feature References + +You can read features from a **specific version** of a feature view by using version-qualified feature references with the `@v` syntax: + +```python +online_features = store.get_online_features( + features=[ + "driver_stats:trips_today", # latest version (default) + "driver_stats@v2:trips_today", # specific version + "driver_stats@latest:trips_today", # explicit latest + ], + entity_rows=[{"driver_id": 1001}], +) +``` + +**How it works:** + +* `driver_stats:trips_today` is equivalent to `driver_stats@latest:trips_today` — it reads from the currently active version +* `driver_stats@v2:trips_today` reads from the v2 snapshot stored in version history, using a version-specific online store table +* Multiple versions of the same feature view can be queried in a single request (e.g., `driver_stats@v1:trips` and `driver_stats@v2:trips_daily`) + +**Backward compatibility:** + +* The unversioned online store table (e.g., `project_driver_stats`) is treated as v0 +* Only versions >= 1 get `_v{N}` suffixed tables (e.g., `project_driver_stats_v1`) +* Pre-versioning users' existing data continues to work without changes — `@latest` resolves to the active version, which for existing unversioned FVs is v0 + +**Materialization:** Each version requires its own materialization. After applying a new version, run `feast materialize` to populate the versioned table before querying it with `@v`. + +## Supported Feature View Types + +Versioning is supported on all three feature view types: + +* `FeatureView` (and `BatchFeatureView`) +* `StreamFeatureView` +* `OnDemandFeatureView` + +## Online Store Support + +{% hint style="info" %} +**Currently, version-qualified online reads (`@v`) are only supported with the SQLite online store.** Support for additional online stores (Redis, DynamoDB, Bigtable, Postgres, etc.) will be added based on community priority. + +If you need versioned online reads for a specific online store, please [open a GitHub issue](https://github.com/feast-dev/feast/issues/new) describing your use case and which store you need. This helps us prioritize development. +{% endhint %} + +Version history tracking in the registry (listing versions, pinning, `--no-promote`) works with **all** registry backends (file, SQL, Snowflake). + +## Full Details + +For the complete design, concurrency semantics, and feature service interactions, see the [Feature View Versioning RFC](../adr/feature-view-versioning.md). + +## Naming Restrictions + +Feature references use a structured format: `feature_view_name@v:feature_name`. To avoid +ambiguity, the following characters are reserved and must not appear in feature view or feature names: + +- **`@`** — Reserved as the version delimiter (e.g., `driver_stats@v2:trips_today`). `feast apply` + will reject feature views with `@` in their name. If you have existing feature views with `@` in + their names, they will continue to work for unversioned reads, but we recommend renaming them to + avoid ambiguity with the `@v` syntax. +- **`:`** — Reserved as the separator between feature view name and feature name in fully qualified + feature references (e.g., `driver_stats:trips_today`). + +## Known Limitations + +- **Online store coverage** — Version-qualified reads (`@v`) are SQLite-only today. Other online stores are follow-up work. +- **Offline store versioning** — Versioned historical retrieval is not yet supported. +- **Version deletion** — There is no mechanism to prune old versions from the registry. +- **Cross-version joins** — Joining features from different versions of the same feature view in `get_historical_features` is not supported. +- **Feature services** — Feature services always resolve to the active (promoted) version. `--no-promote` versions are not served until promoted. diff --git a/docs/reference/alpha-static-artifacts.md b/docs/reference/alpha-static-artifacts.md new file mode 100644 index 00000000000..627898f847f --- /dev/null +++ b/docs/reference/alpha-static-artifacts.md @@ -0,0 +1,314 @@ +# [Alpha] Static Artifacts Loading + +**Warning**: This is an experimental feature. To our knowledge, this is stable, but there are still rough edges in the experience. Contributions are welcome! + +## Overview + +Static Artifacts Loading allows you to load models, lookup tables, and other static resources once during feature server startup instead of loading them on each request. These artifacts are cached in memory and accessible to on-demand feature views for real-time inference. + +This feature optimizes the performance of on-demand feature views that require external resources by eliminating the overhead of repeatedly loading the same artifacts during request processing. + +### Why Use Static Artifacts Loading? + +Static artifacts loading enables data scientists and ML engineers to: + +1. **Improve performance**: Eliminate model loading overhead from each feature request +2. **Enable complex transformations**: Use pre-trained models in on-demand feature views without performance penalties +3. **Share resources**: Multiple feature views can access the same loaded artifacts +4. **Simplify deployment**: Package models and lookup tables with your feature repository + +Common use cases include: +- Sentiment analysis using pre-trained transformers models +- Text classification with small neural networks +- Lookup-based transformations using static dictionaries +- Embedding generation with pre-computed vectors + +## How It Works + +1. **Feature Repository Setup**: Create a `static_artifacts.py` file in your feature repository root +2. **Server Startup**: When `feast serve` starts, it automatically looks for and loads the artifacts +3. **Memory Storage**: Artifacts are stored in the FastAPI application state and accessible via global references +4. **Request Processing**: On-demand feature views access pre-loaded artifacts for fast transformations + +## Example 1: Basic Model Loading + +Create a `static_artifacts.py` file in your feature repository: + +```python +# static_artifacts.py +from fastapi import FastAPI +from transformers import pipeline + +def load_sentiment_model(): + """Load sentiment analysis model.""" + return pipeline( + "sentiment-analysis", + model="cardiffnlp/twitter-roberta-base-sentiment-latest", + device="cpu" + ) + +def load_artifacts(app: FastAPI): + """Load static artifacts into app.state.""" + app.state.sentiment_model = load_sentiment_model() + + # Update global references for access from feature views + import example_repo + example_repo._sentiment_model = app.state.sentiment_model +``` + +Use the pre-loaded model in your on-demand feature view: + +```python +# example_repo.py +import pandas as pd +from feast.on_demand_feature_view import on_demand_feature_view +from feast import Field +from feast.types import String, Float32 + +# Global reference for static artifacts +_sentiment_model = None + +@on_demand_feature_view( + sources=[text_input_request], + schema=[ + Field(name="predicted_sentiment", dtype=String), + Field(name="sentiment_confidence", dtype=Float32), + ], +) +def sentiment_prediction(inputs: pd.DataFrame) -> pd.DataFrame: + """Sentiment prediction using pre-loaded model.""" + global _sentiment_model + + results = [] + for text in inputs["input_text"]: + predictions = _sentiment_model(text) + best_pred = max(predictions, key=lambda x: x["score"]) + + results.append({ + "predicted_sentiment": best_pred["label"], + "sentiment_confidence": best_pred["score"], + }) + + return pd.DataFrame(results) +``` + +## Example 2: Multiple Artifacts with Lookup Tables + +Load multiple types of artifacts: + +```python +# static_artifacts.py +from fastapi import FastAPI +from transformers import pipeline +import json +from pathlib import Path + +def load_sentiment_model(): + """Load sentiment analysis model.""" + return pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english") + +def load_lookup_tables(): + """Load static lookup tables.""" + return { + "sentiment_labels": {"NEGATIVE": "negative", "POSITIVE": "positive"}, + "domain_categories": {"twitter.com": "social", "news.com": "news", "github.com": "tech"}, + "priority_users": {"user_123", "user_456", "user_789"} + } + +def load_config(): + """Load application configuration.""" + return { + "model_threshold": 0.7, + "max_text_length": 512, + "default_sentiment": "neutral" + } + +def load_artifacts(app: FastAPI): + """Load all static artifacts.""" + app.state.sentiment_model = load_sentiment_model() + app.state.lookup_tables = load_lookup_tables() + app.state.config = load_config() + + # Update global references + import example_repo + example_repo._sentiment_model = app.state.sentiment_model + example_repo._lookup_tables = app.state.lookup_tables + example_repo._config = app.state.config +``` + +Use multiple artifacts in feature transformations: + +```python +# example_repo.py +import pandas as pd +from feast.on_demand_feature_view import on_demand_feature_view + +# Global references for static artifacts +_sentiment_model = None +_lookup_tables: dict = {} +_config: dict = {} + +@on_demand_feature_view( + sources=[text_input_request, user_input_request], + schema=[ + Field(name="predicted_sentiment", dtype=String), + Field(name="is_priority_user", dtype=Bool), + Field(name="domain_category", dtype=String), + ], +) +def enriched_prediction(inputs: pd.DataFrame) -> pd.DataFrame: + """Multi-artifact feature transformation.""" + global _sentiment_model, _lookup_tables, _config + + results = [] + for i, row in inputs.iterrows(): + text = row["input_text"] + user_id = row["user_id"] + domain = row.get("domain", "") + + # Use pre-loaded model + predictions = _sentiment_model(text) + sentiment_scores = {pred["label"]: pred["score"] for pred in predictions} + + # Use lookup tables + predicted_sentiment = _lookup_tables["sentiment_labels"].get( + max(sentiment_scores, key=sentiment_scores.get), + _config["default_sentiment"] + ) + + is_priority = user_id in _lookup_tables["priority_users"] + category = _lookup_tables["domain_categories"].get(domain, "unknown") + + results.append({ + "predicted_sentiment": predicted_sentiment, + "is_priority_user": is_priority, + "domain_category": category, + }) + + return pd.DataFrame(results) +``` + +## Container Deployment + +Static artifacts work with containerized deployments. Include your artifacts in the container image: + +```dockerfile +FROM python:3.11-slim + +# Install dependencies +COPY requirements.txt . +RUN pip install -r requirements.txt + +# Copy feature repository including static_artifacts.py +COPY feature_repo/ /app/feature_repo/ + +WORKDIR /app/feature_repo + +# Start feature server +CMD ["feast", "serve", "--host", "0.0.0.0"] +``` + +The server will automatically load static artifacts during container startup. + +## Supported Artifact Types + +### Recommended Artifacts +- **Small ML models**: Sentiment analysis, text classification, small neural networks +- **Lookup tables**: Label mappings, category dictionaries, user segments +- **Configuration data**: Model parameters, feature mappings, business rules +- **Pre-computed embeddings**: User vectors, item features, static representations + +### Not Recommended +- **Large Language Models**: Use dedicated serving solutions (vLLM, TensorRT-LLM, TGI) +- **Models requiring specialized hardware**: GPU clusters, TPUs +- **Frequently updated models**: Consider model registries with versioning +- **Large datasets**: Use feature views with proper data sources instead + +## Error Handling + +Static artifacts loading includes graceful error handling: +- **Missing file**: Server starts normally without static artifacts +- **Loading errors**: Warnings are logged, feature views should implement fallback logic +- **Partial failures**: Successfully loaded artifacts remain available + +Always implement fallback behavior in your feature transformations: + +```python +@on_demand_feature_view(...) +def robust_prediction(inputs: pd.DataFrame) -> pd.DataFrame: + global _sentiment_model + + results = [] + for text in inputs["input_text"]: + if _sentiment_model is not None: + # Use pre-loaded model + predictions = _sentiment_model(text) + sentiment = max(predictions, key=lambda x: x["score"])["label"] + else: + # Fallback when artifacts aren't available + sentiment = "neutral" + + results.append({"predicted_sentiment": sentiment}) + + return pd.DataFrame(results) +``` + +## Starting the Feature Server + +Start the feature server as usual: + +```bash +feast serve +``` + +You'll see log messages indicating artifact loading: + +``` +INFO:fastapi:Loading static artifacts from static_artifacts.py +INFO:fastapi:Static artifacts loading completed +INFO:uvicorn:Application startup complete +``` + +## Template Example + +The PyTorch NLP template demonstrates static artifacts loading: + +```bash +feast init my-nlp-project -t pytorch_nlp +cd my-nlp-project/feature_repo +feast serve +``` + +This template includes a complete example with sentiment analysis model loading, lookup tables, and integration with on-demand feature views. + +## Performance Considerations + +- **Startup time**: Artifacts are loaded during server initialization, which may increase startup time +- **Memory usage**: All artifacts remain in memory for the server's lifetime +- **Concurrency**: Artifacts are shared across all request threads +- **Container resources**: Ensure sufficient memory allocation for your artifacts + +## Configuration + +Currently, static artifacts loading uses convention-based configuration: +- **File name**: Must be named `static_artifacts.py` +- **Location**: Must be in the feature repository root directory +- **Function name**: Must implement `load_artifacts(app: FastAPI)` function + +## Limitations + +- File name and location are currently fixed (not configurable) +- Artifacts are loaded synchronously during startup +- No built-in artifact versioning or hot reloading +- Limited to Python-based artifacts (no external binaries) + +## Contributing + +This is an alpha feature and we welcome contributions! Areas for improvement: +- Configurable artifact file locations +- Asynchronous artifact loading +- Built-in artifact versioning +- Performance monitoring and metrics +- Integration with model registries + +Please report issues and contribute improvements via the [Feast GitHub repository](https://github.com/feast-dev/feast). \ No newline at end of file diff --git a/docs/reference/alpha-vector-database.md b/docs/reference/alpha-vector-database.md index 861c3fcb114..61da02ce6f4 100644 --- a/docs/reference/alpha-vector-database.md +++ b/docs/reference/alpha-vector-database.md @@ -15,6 +15,7 @@ Below are supported vector databases and implemented features: | Faiss | [ ] | [ ] | [] | [] | | SQLite | [x] | [ ] | [x] | [x] | | Qdrant | [x] | [x] | [] | [] | +| ScyllaDB | [x] | [x] | [x] | [x] | *Note: V2 Support means the SDK supports retrieval of features along with vector embeddings from vector similarity search. @@ -30,7 +31,241 @@ Beyond that, we will then have `retrieve_online_documents` and `retrieve_online_ backwards compatibility and the adopt industry standard naming conventions. {% endhint %} -**Note**: Milvus and SQLite implement the v2 `retrieve_online_documents_v2` method in the SDK. This will be the longer-term solution so that Data Scientists can easily enable vector similarity search by just flipping a flag. +**Note**: Milvus, SQLite, and ScyllaDB implement the v2 `retrieve_online_documents_v2` method in the SDK. This will be the longer-term solution so that Data Scientists can easily enable vector similarity search by just flipping a flag. + +## Feature server search endpoints + +| Endpoint | Use when | +|----------|----------| +| `POST /search` | You have an embedding vector (or use `api_version: 2` with `query_string`) and want Feast's native online-features response format. | +| `GET /v1/vector_stores` | You want to discover available vector stores and their `vs_{hash}` IDs (OpenAI-compatible). | +| `GET /v1/vector_stores/{id}` | You want metadata for a specific vector store (OpenAI-compatible). | +| `POST /v1/vector_stores/{id}/search` | You want plain-text queries with server-side embedding and an OpenAI-compatible response. | + +`POST /retrieve-online-documents` is deprecated; use `POST /search` instead. + +## [Alpha] OpenAI-Compatible Vector Store API + +{% hint style="warning" %} +**Alpha feature.** This API surface is functional and tested, but may change in future releases. Feedback and contributions are welcome. +{% endhint %} + +Feast exposes a set of [OpenAI-compatible vector store endpoints](https://platform.openai.com/docs/api-reference/vector-stores) that let clients discover, inspect, and search vector stores using plain text queries with server-side embedding. This enables integration with AI agents, LLM tool-calling frameworks, and any OpenAI-compatible client without requiring the caller to produce raw embedding vectors. + +### Vector store IDs + +Each feature view with at least one `vector_index=True` field is automatically assigned a deterministic identifier of the form `vs_{hash}`, where `{hash}` is the first 24 characters of `SHA-256(project + ":" + feature_view_name)`. These IDs are stable across server restarts and registry refreshes. + +For example, a feature view named `product_catalog` in project `my_project` always maps to the same `vs_...` identifier. The listing endpoints return these IDs so clients can discover stores at runtime. + +### Endpoints + +| Method | Path | Permission | Description | +|--------|------|------------|-------------| +| `GET` | `/v1/vector_stores` | `DESCRIBE` | List all vector stores the caller has access to | +| `GET` | `/v1/vector_stores/{vector_store_id}` | `DESCRIBE` | Get metadata for a single vector store | +| `POST` | `/v1/vector_stores/{vector_store_id}/search` | `READ_ONLINE` | Search a vector store with a plain text query | + +All endpoints enforce RBAC when authentication is configured. The listing endpoint filters out stores the caller cannot `DESCRIBE`. + +### Requirements + +1. **Embedding model** — an `embedding_model` section in `feature_store.yaml`. Feast uses [Sentence Transformers](https://www.sbert.net/) by default for local embedding — no external API key required (`pip install sentence-transformers`): + + ```yaml + embedding_model: + provider: sentence_transformers # default; can be omitted + model: all-MiniLM-L6-v2 + ``` + +2. **Vector-indexed feature view** — at least one feature view with `vector_index=True` on a vector field, materialized to an online store that supports vector search. + +3. **Numeric filtering (optional)** — for metadata filters that use numeric or boolean comparisons, set `enable_openai_compatible_store: true` on your online store config and run `feast apply` to add the required `value_num` column. + +### Custom embedding providers + +The built-in Sentence Transformers provider works for most use cases. To use a different embedding backend (OpenAI, Cohere, a custom model, etc.), implement the `EmbeddingProvider` protocol and pass an instance to `FeatureStore`: + +```python +from feast.embedder import EmbeddingProvider + +class MyEmbeddingProvider: + def embed(self, texts: list[str]) -> list[list[float]]: + # Call your embedding API here + return my_model.encode(texts) + + async def aembed(self, texts: list[str]) -> list[list[float]]: + return await my_model.aencode(texts) + +store = FeatureStore( + repo_path=".", + embedding_provider=MyEmbeddingProvider(), +) +``` + +### Numeric storage (`enable_openai_compatible_store`) + +By default, feature values are stored as text in the online store. This means string-ordered comparisons apply (e.g., `'9' > '100'` is `true`). When `enable_openai_compatible_store: true` is set on the online store config, Feast adds a `value_num` column that stores `int`, `float`, `double`, and `bool` values natively so that numeric filters produce correct results. + +```yaml +online_store: + type: postgres # or sqlite + # ... connection settings ... + enable_openai_compatible_store: true +``` + +After changing this setting, run `feast apply` to update the database schema. + +### List vector stores + +```bash +curl http://localhost:6566/v1/vector_stores +``` + +```json +{ + "object": "list", + "data": [ + { + "id": "vs_a1b2c3d4e5f6a1b2c3d4e5f6", + "object": "vector_store", + "name": "product_catalog", + "status": "completed", + "created_at": 1717200000 + } + ] +} +``` + +### Get a single vector store + +```bash +curl http://localhost:6566/v1/vector_stores/vs_a1b2c3d4e5f6a1b2c3d4e5f6 +``` + +Returns the same object shape as a single entry in the list response. Returns `404` if the ID does not match any vector-indexed feature view. + +### Search + +Start the feature server with `feast serve`, then send a search request: + +```bash +curl -X POST http://localhost:6566/v1/vector_stores/vs_a1b2c3d4e5f6a1b2c3d4e5f6/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "wireless noise-cancelling headphones", + "max_num_results": 5 + }' +``` + +#### Request fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `query` | `string` or `list[string]` | (required) | Plain text search query. Lists are joined with spaces before embedding. | +| `max_num_results` | `int` | `10` | Maximum number of results to return. | +| `filters` | `object` | `null` | OpenAI-style filters (see below). | +| `ranking_options` | `object` | `null` | Accepted for forward compatibility, but currently ignored. Setting `score_threshold` or `ranker` inside it will return a 422 error. | +| `rewrite_query` | `bool` | `null` | `false` (the default/no-op) is accepted. `true` is not yet supported and will return a 422 error. | +| `metadata` | `object` | `null` | Optional. `metadata.features_to_retrieve` selects specific features. | + +### Filters + +The endpoint supports OpenAI-style filters for narrowing results beyond vector similarity. + +**Comparison operators:** `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` + +```json +{"type": "eq", "key": "category", "value": "Electronics"} +``` + +**Compound operators:** `and`, `or` (nest to arbitrary depth) + +```json +{ + "type": "and", + "filters": [ + {"type": "eq", "key": "category", "value": "Electronics"}, + {"type": "gte", "key": "rating", "value": 4.5} + ] +} +``` + +For Postgres and SQLite backends, all filtering (including string equality) requires `enable_openai_compatible_store: true` in the online store config. After enabling, run `feast apply` to update the database schema. + +ScyllaDB supports vector retrieval via `retrieve_online_documents_v2`, but OpenAI-style metadata filtering is not implemented yet. Passing `filters` raises `NotImplementedError`. + +### Response format + +Responses follow the OpenAI `vector_store.search_results.page` schema: + +```json +{ + "object": "vector_store.search_results.page", + "search_query": ["wireless noise-cancelling headphones"], + "data": [ + { + "file_id": "vs_a1b2c3d4e5f6a1b2c3d4e5f6_42", + "filename": "vs_a1b2c3d4e5f6a1b2c3d4e5f6", + "score": 0.92, + "attributes": {"name": "...", "category": "..."}, + "content": [ + {"type": "text", "text": "..."} + ] + } + ], + "has_more": false, + "next_page": null +} +``` + +The `file_id` and `filename` fields use the `vs_{hash}` identifier, not raw feature view names. + +The `score` field is a higher-is-better relevance score derived from the raw vector distance using a metric-dependent conversion: + +| Distance metric | Conversion | Range | +|----------------|------------|-------| +| L2 (default) | `1 / (1 + distance)` | (0, 1] | +| Cosine | `1 - distance` | [0, 1] | +| Inner product / dot | `-distance` | varies | + +The metric is determined by `vector_search_metric` on the feature view's vector field, not by an API parameter. When `features_to_retrieve` is omitted, all non-vector features are returned by default (vector embedding columns are excluded). + +Pagination is not yet implemented; `has_more` is always `false`. + +### SDK usage + +The OpenAI-compatible search is also available directly via the Python SDK: + +```python +import asyncio +from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +result = asyncio.run(store.openai_search( + vector_store_id="product_catalog", + query="wireless noise-cancelling headphones", + max_num_results=5, + filters={"type": "eq", "key": "category", "value": "Electronics"}, +)) + +for item in result["data"]: + print(f"{item['score']:.3f} {item['attributes']}") +``` + +### Supported online stores + +The OpenAI-compatible filtering has been implemented for the following online stores: + +| Online Store | Vector Search | Metadata Filtering | Notes | +|-------------|--------------|-------------------|-------| +| Milvus | Yes | Yes | Boolean expressions | +| Elasticsearch | Yes | Yes | Query DSL clauses | +| Postgres (pgvector) | Yes | Yes | Requires `enable_openai_compatible_store: true` | +| SQLite (sqlite-vec) | Yes | Yes | Requires `enable_openai_compatible_store: true` | +| MongoDB | Yes | Yes | Aggregation pipeline | +| ScyllaDB | Yes | No | Vector search only; metadata filters are not supported yet | ## Examples diff --git a/docs/reference/alpha-web-ui.md b/docs/reference/alpha-web-ui.md index 80c5b824c5a..3fe8ce052a8 100644 --- a/docs/reference/alpha-web-ui.md +++ b/docs/reference/alpha-web-ui.md @@ -35,6 +35,18 @@ Options: This will spin up a Web UI on localhost which automatically refreshes its view of the registry every `registry_ttl_sec` +#### Curl Generator Feature Server URL + +The Curl Generator uses a default Feature Server URL when building the example curl command. You can configure +this default at build time using: + +```bash +REACT_APP_FEAST_FEATURE_SERVER_URL="http://your-server:6566" +``` + +If this environment variable is not set, the UI falls back to `http://localhost:6566`. A user-edited value in +the UI is still stored in localStorage and will take precedence for that browser. + ### Importing as a module to integrate with an existing React App This is the recommended way to use Feast UI for teams maintaining their own internal UI for their deployment of Feast. @@ -141,3 +153,12 @@ const tabsRegistry = { ``` Examples of custom tabs can be found in the `ui/custom-tabs` folder. + +## Refreshing the registry + +The Feast UI caches registry data (projects, feature views, entities, etc.) using the registry cache. After running `feast apply` to make changes, it may take up to `cache_ttl_seconds` before the updates appear in the UI. + +To see changes faster: + +- **Lower the TTL**: Set `cache_ttl_seconds: 10` (or similar) in your `feature_store.yaml` registry config. This makes all registry consumers — including the UI — pick up changes within 10 seconds. +- **Refresh on demand**: The UI has a **Refresh** button that explicitly invalidates the server-side registry cache (`POST /api/v1/registry/refresh`) and reloads the UI without a full page refresh. diff --git a/docs/reference/beta-on-demand-feature-view.md b/docs/reference/beta-on-demand-feature-view.md index 2482bbc1c8f..684bc0ac4b2 100644 --- a/docs/reference/beta-on-demand-feature-view.md +++ b/docs/reference/beta-on-demand-feature-view.md @@ -69,6 +69,42 @@ def driver_aggregated_stats(inputs): Aggregated columns are automatically named using the pattern `{function}_{column}` (e.g., `sum_trips`, `mean_rating`). +### Using `input_schema` with Aggregations + +When the input data is not already stored as a feature view, use `input_schema` instead of `sources` to describe the fields that will be passed at request time. Feast will create an internal `RequestSource` automatically. + +```python +from datetime import timedelta +from feast import Field, on_demand_feature_view +from feast.aggregation import Aggregation +from feast.types import Float64, Int64 + +@on_demand_feature_view( + input_schema=[ + Field(name="txn_amount", dtype=Float64), + ], + schema=[ + Field(name="txn_count", dtype=Int64), + Field(name="total_txn_amount", dtype=Float64), + Field(name="avg_txn_amount", dtype=Float64), + ], + aggregations=[ + Aggregation(column="txn_amount", function="count", name="txn_count", + time_window=timedelta(days=30)), + Aggregation(column="txn_amount", function="sum", name="total_txn_amount", + time_window=timedelta(days=30)), + Aggregation(column="txn_amount", function="mean", name="avg_txn_amount", + time_window=timedelta(days=30)), + ], + entities=[user], +) +def user_transaction_stats(inputs): + # Aggregations replace the transformation function — no body needed. + pass +``` + +`input_schema` also accepts fields that are not aggregation columns — for example, thresholds, currency codes, or other contextual values passed at request time that your UDF needs but that are not stored as features. + ## Example See [https://github.com/feast-dev/on-demand-feature-views-demo](https://github.com/feast-dev/on-demand-feature-views-demo) for an example on how to use on demand feature views. @@ -309,4 +345,44 @@ There are new CLI commands to manage on demand feature views: feast on-demand-feature-views list: Lists all registered on demand feature views after feast apply is run. feast on-demand-feature-views describe [NAME]: Describes the definition of an on demand feature view. +## Troubleshooting + +### Validation Issues with Complex Transformations + +When defining On Demand Feature Views with complex transformations, you may encounter validation errors during `feast apply`. Feast validates ODFVs by constructing random inputs and running the transformation function to infer the output schema. This validation can sometimes be overly strict or fail for complex transformation logic. + +If you encounter validation errors that you believe are incorrect, you can skip feature view validation: + +**Python SDK:** +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") +store.apply([my_odfv], skip_feature_view_validation=True) +``` + +**CLI:** +```bash +feast apply --skip-feature-view-validation +``` + +{% hint style="warning" %} +Skipping validation bypasses important checks. Use this option only when the validation system is being overly strict. We encourage you to report validation issues on the [Feast GitHub repository](https://github.com/feast-dev/feast/issues) so the team can improve the validation system. +{% endhint %} + +**When to use skip_feature_view_validation:** +- Your ODFV transformation uses complex logic that fails the random input validation +- You've verified your transformation works correctly with real data +- The validation error doesn't reflect an actual issue with your transformation + +**What validation is skipped:** +- Feature view name uniqueness checks +- ODFV transformation validation via `_construct_random_input()` +- Schema inference for features + +**What is NOT skipped:** +- Data source validation (use `--skip-source-validation` to skip) +- Registry operations +- Infrastructure updates + diff --git a/docs/reference/codebase-structure.md b/docs/reference/codebase-structure.md index 80608b5929a..4783773c270 100644 --- a/docs/reference/codebase-structure.md +++ b/docs/reference/codebase-structure.md @@ -28,7 +28,7 @@ The majority of Feast logic lives in these Python files: There are also several important submodules: * `infra/` contains all the infrastructure components, such as the provider, offline store, online store, batch materialization engine, and registry. -* `dqm/` covers data quality monitoring, such as the dataset profiler. +* `dqm/` covers data quality monitoring. See [`monitoring/`](../../sdk/python/feast/monitoring/) for the built-in monitoring system. * `diff/` covers the logic for determining how to apply infrastructure changes upon feature repo changes (e.g. the output of `feast plan` and `feast apply`). * `embedded_go/` covers the Go feature server. * `ui/` contains the embedded Web UI, to be launched on the `feast ui` command. diff --git a/docs/reference/compute-engine/README.md b/docs/reference/compute-engine/README.md index dad2ede75a6..a570e5688ed 100644 --- a/docs/reference/compute-engine/README.md +++ b/docs/reference/compute-engine/README.md @@ -57,6 +57,22 @@ An example of built output from FeatureBuilder: - Supports point-in-time joins and large-scale materialization - Integrates with `SparkOfflineStore` and `SparkMaterializationJob` +### ☸️ SparkApplicationComputeEngine + +{% page-ref page="spark_application.md" %} + +- Batch materialization via Kubeflow Spark Operator `SparkApplication` CRs +- One SparkApplication per materialize call (multi–feature-view batching) +- Requires network-accessible online/offline/registry stores (no file-based backends) + +### 🌊 FlinkComputeEngine + +{% page-ref page="flink.md" %} + +- Distributed DAG execution through Apache Flink's PyFlink Table API +- Supports materialization and historical retrieval with Feast offline stores +- Integrates with `FlinkMaterializationJob` and `FlinkDAGRetrievalJob` + ### ⚡ RayComputeEngine (contrib) - Distributed DAG execution via Ray diff --git a/docs/reference/compute-engine/flink.md b/docs/reference/compute-engine/flink.md new file mode 100644 index 00000000000..0dd5560f70e --- /dev/null +++ b/docs/reference/compute-engine/flink.md @@ -0,0 +1,124 @@ +# Apache Flink + +## Description + +The Apache Flink compute engine provides a distributed execution engine for +feature pipelines through the PyFlink Table API. It implements Feast's unified +`ComputeEngine` interface and can be used for batch materialization operations +(`materialize` and `materialize-incremental`) and historical retrieval +(`get_historical_features`). + +The engine reads data through the configured Feast offline store and executes +the Feast DAG as PyFlink tables. Offline stores that expose a native +`to_flink_table(table_env)` retrieval job hand Flink tables directly to the +engine. Retrieval jobs that only expose the standard Arrow path are also +supported and are converted into Flink tables by the engine. The engine then +uses Flink Table/SQL operations for join, filter, aggregate, dedupe, and +projection steps, and writes materialization results to the configured online +and/or offline store. + +## Configuration + +Install the Flink extra from a Feast source checkout with `uv` before using the +engine: + +```bash +uv sync --extra flink --no-dev +``` + +The `flink` extra installs PyFlink directly. PyFlink currently requires +`pyarrow<21`, while the default Feast install keeps `pyarrow>=21`; Feast's uv +lock resolves the Flink extra in a separate dependency fork so normal Feast +installs do not downgrade Arrow. + +Configure the engine in `feature_store.yaml`: + +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: file +online_store: + type: sqlite + path: data/online_store.db +batch_engine: + type: flink.engine + execution_mode: batch + parallelism: 4 + table_config: + pipeline.name: "Feast Flink Compute Engine" + pandas_split_num: 4 +``` + +## Configuration Options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `type` | string | `flink.engine` | Must be `flink.engine`. | +| `execution_mode` | string | `batch` | PyFlink execution mode: `batch` or `streaming`. | +| `parallelism` | integer | `null` | Default Flink parallelism for jobs created by the engine. | +| `table_config` | map | `null` | Additional PyFlink table configuration entries. | +| `pandas_split_num` | integer | `1` | Number of PyFlink Arrow source splits when converting pandas entity DataFrames into Flink tables. | + +## Flink Transformations + +Use `mode="flink"` when a `BatchFeatureView` transformation should receive and +return PyFlink table objects: + +```python +from feast import BatchFeatureView, Field +from feast.types import Float32 + + +def double_rates(table): + # In production this can use PyFlink Table API operations and return a table. + return table + + +driver_stats = BatchFeatureView( + name="driver_stats", + entities=[driver], + mode="flink", + udf=double_rates, + schema=[Field(name="conv_rate", dtype=Float32)], + source=driver_stats_source, + online=True, +) +``` + +Flink transformations must return PyFlink table objects. pandas-returning UDFs +are not accepted by the Flink compute engine. + +## DAG Support + +The Flink engine implements Feast's compute DAG with Flink-specific nodes: + +- Source reads from Feast offline stores, preferring native Flink tables when a + retrieval job supports `to_flink_table(table_env)` and otherwise converting + Arrow results into Flink tables. +- Transform nodes pass PyFlink tables to `mode="flink"` UDFs and preserve native + Flink table outputs. +- Join nodes use Flink SQL temporary views for feature joins and entity joins. +- Filter nodes apply point-in-time, TTL, and custom filter expressions in Flink + SQL. +- Aggregate nodes support non-windowed Feast aggregations using Flink SQL + aggregate functions. +- Dedupe nodes use `ROW_NUMBER()` over entity keys or internal entity-row ids so + historical retrieval keeps one latest feature row per entity row. +- Validation nodes check required output columns. JSON value validation must be + handled upstream in Flink SQL. +- Output nodes write only for materialization tasks; historical retrieval is + read-only. +- Historical retrieval accepts pandas entity DataFrames and SQL-string entity + DataFrames. SQL strings are interpreted as Flink SQL queries against the + configured TableEnvironment/catalog and must select an `event_timestamp` + column. + +## Current Limitations + +- Windowed aggregations are not yet implemented in the Flink compute engine. Use + non-windowed Feast aggregations or pre-window upstream in Flink. +- JSON value validation is not implemented inside the Flink compute engine + because the engine does not collect intermediate data out of Flink for + validation. diff --git a/docs/reference/compute-engine/ray.md b/docs/reference/compute-engine/ray.md index 22b1e1a4700..19604e21788 100644 --- a/docs/reference/compute-engine/ray.md +++ b/docs/reference/compute-engine/ray.md @@ -28,6 +28,7 @@ The Ray compute engine provides: - **Lazy Evaluation**: Deferred execution for optimal performance - **Resource Management**: Automatic scaling and resource optimization - **Point-in-Time Joins**: Efficient temporal joins for historical feature retrieval +- **GPU Support**: Schedule transformation workers on GPU nodes via `num_gpus` config (all modes including KubeRay) ## Architecture @@ -87,6 +88,9 @@ batch_engine: | `enable_distributed_joins` | boolean | true | Enable distributed joins for large datasets | | `staging_location` | string | None | Remote path for batch materialization jobs | | `ray_conf` | dict | None | Ray configuration parameters (memory, CPU limits) | +| `num_gpus` | float | None | Number of GPUs to request per worker task. Requires GPU nodes in the Ray cluster. Fractional values (e.g. `0.5`) are supported. Supported in all modes including KubeRay. | +| `gpu_batch_format` | string | `"pandas"` | Batch format for `map_batches` when `num_gpus` is set. Use `"numpy"` or `"pyarrow"` for GPU-native libraries (e.g. cuDF, PyTorch). | +| `worker_task_options` | dict | None | Arbitrary Ray `.options()` kwargs applied to every worker task. See [Worker Resource Scheduling](#worker-resource-scheduling) for the full reference. | ### Mode Detection Precedence @@ -344,6 +348,7 @@ import ray # Check cluster resources resources = ray.cluster_resources() print(f"Available CPUs: {resources.get('CPU', 0)}") +print(f"Available GPUs: {resources.get('GPU', 0)}") print(f"Available memory: {resources.get('memory', 0) / 1e9:.2f} GB") # Monitor job progress @@ -351,6 +356,89 @@ job = store.get_historical_features(...) # Ray compute engine provides built-in progress tracking ``` +## Worker Resource Scheduling + +`worker_task_options` is a passthrough dict of [Ray `.options()` kwargs](https://docs.ray.io/en/latest/ray-core/api/doc/ray.remote_function.RemoteFunction.options.html) applied to every worker task Feast dispatches. It pairs with `ray_conf` (cluster-level `ray.init` options) — `worker_task_options` targets individual worker tasks. Options are forwarded at two levels so Ray schedules correctly: + +1. On the `@ray.remote` orchestration task via `.options(**worker_task_options)` — controls node selection. +2. Inside `map_batches` for the scheduling-relevant subset (`num_gpus`, `num_cpus`, `accelerator_type`, `resources`) — controls which nodes run the data workers. + +This is supported across **all execution modes**: local, remote, and KubeRay. + +### Common `worker_task_options` keys + +| Key | Type | Description | +|-----|------|-------------| +| `num_cpus` | float | CPUs per task (default: 1). Fractional values supported. | +| `memory` | int | Heap memory in **bytes** (e.g. `8589934592` for 8 GB). | +| `accelerator_type` | string | Pin tasks to a specific GPU model — `"A100"`, `"T4"`, `"V100"`, etc. Useful on KubeRay clusters with mixed GPU node pools. | +| `resources` | dict | Custom/Kubernetes extended resource labels, e.g. `{"intel.com/gpu": 1}`. | +| `runtime_env` | dict | Per-task [Ray runtime environment](https://docs.ray.io/en/latest/ray-core/handling-dependencies.html) — `pip`, `conda`, `env_vars`, `working_dir`, etc. For KubeRay, use this to install packages on worker pods without rebuilding images. | +| `max_retries` | int | Task retry count on worker failure (default: 3). | +| `scheduling_strategy` | string | `"DEFAULT"`, `"SPREAD"`, or a placement group strategy. | + +> For the full list of supported keys see the [Ray RemoteFunction.options() API docs](https://docs.ray.io/en/latest/ray-core/api/doc/ray.remote_function.RemoteFunction.options.html). + +### GPU support + +`num_gpus` is the only first-class GPU field because it also drives `gpu_batch_format` selection inside Feast. Set it directly rather than inside `worker_task_options`: + +```yaml +batch_engine: + type: ray.engine + num_gpus: 1 # GPUs per task (fractional values like 0.5 supported) + gpu_batch_format: numpy # numpy/pyarrow for GPU-native libs (cuDF, PyTorch) +``` + +When `num_gpus` is set your transformation UDF runs on a GPU worker: + +```python +import cudf # RAPIDS cuDF – GPU-accelerated DataFrame library + +def gpu_transform(batch): + gpu_df = cudf.from_pandas(batch) + gpu_df["score"] = gpu_df["raw_value"] * 2.0 + return gpu_df.to_pandas() +``` + +### Full example — KubeRay with GPU + all common options + +```yaml +batch_engine: + type: ray.engine + use_kuberay: true + kuberay_conf: + cluster_name: "feast-gpu-cluster" + namespace: "feast-system" + auth_token: "${RAY_AUTH_TOKEN}" + auth_server: "https://api.openshift.com:6443" + num_gpus: 1 + gpu_batch_format: numpy + worker_task_options: + num_cpus: 4 + memory: 8589934592 # 8 GB + accelerator_type: "A100" # pin to A100 nodes on mixed GPU pool + max_retries: 5 + runtime_env: + pip: + - cudf-cu12==24.10.0 + - torch==2.4.0 + env_vars: + CUDA_VISIBLE_DEVICES: "0" +``` + +### Checking cluster resources + +```python +import ray + +ray.init(address="auto") +resources = ray.cluster_resources() +print(f"Available CPUs: {resources.get('CPU', 0)}") +print(f"Available GPUs: {resources.get('GPU', 0)}") +print(f"Available memory: {resources.get('memory', 0) / 1e9:.2f} GB") +``` + ## Integration Examples ### With Spark Offline Store diff --git a/docs/reference/compute-engine/snowflake.md b/docs/reference/compute-engine/snowflake.md index e7b0dc5bd63..f6c633a4e40 100644 --- a/docs/reference/compute-engine/snowflake.md +++ b/docs/reference/compute-engine/snowflake.md @@ -24,5 +24,10 @@ batch_engine: role: sysadmin warehouse: demo_wh database: FEAST + python_udf_runtime_version: "3.10" ``` {% endcode %} + +## Configuration + +* `python_udf_runtime_version` *(optional, default: `"3.10"`)* -- The Snowflake Python UDF `RUNTIME_VERSION` used when Feast deploys its materialization UDFs. Snowflake periodically decommissions old Python UDF runtimes (for example, the 3.9 runtime was decommissioned, requiring Feast to bump its default to 3.10 -- see [#6606](https://github.com/feast-dev/feast/issues/6606)). If Snowflake decommissions the 3.10 runtime in the future, set this field to a still-supported version (e.g. `"3.11"`) instead of waiting for a new Feast release. diff --git a/docs/reference/compute-engine/spark_application.md b/docs/reference/compute-engine/spark_application.md new file mode 100644 index 00000000000..0c071d1ed4f --- /dev/null +++ b/docs/reference/compute-engine/spark_application.md @@ -0,0 +1,182 @@ +# SparkApplication Compute Engine + +## Description + +The **SparkApplication** compute engine runs Feast **batch materialization** on Kubernetes by creating a [Kubeflow Spark Operator](https://github.com/kubeflow/spark-operator) `SparkApplication` custom resource for each materialization job. + +Unlike the in-process [`spark.engine`](spark.md) compute engine (which uses a Spark session inside the Feast process), `spark_application` submits work to the Spark Operator. The operator starts a driver pod and executors from your configured image; Feast polls the SparkApplication until it completes. + +| Capability | Supported | +|------------|-----------| +| `materialize` / `materialize-incremental` | Yes | +| Multiple feature views in one job | Yes — one SparkApplication per materialize call | +| `get_historical_features` | Not yet | +| SparkConnect | Separate approach — not this engine | + +### Design + +1. Feast creates a ConfigMap with job tasks and a driver copy of `feature_store.yaml`. +2. Feast creates a `SparkApplication` CR pointing at the driver entrypoint (`main.py` in the image). +3. Inside the pod, the batch engine type is rewritten to `spark.engine` so materialization uses the Spark session created by `spark-submit` (avoids recursive SparkApplication creation). +4. The driver writes features to your configured **online store** and updates the **registry** (same network backends as the server). + +### Requirements + +- Kubeflow Spark Operator installed and watching the target namespace. +- A container **image** that includes the Feast SDK, PySpark, and clients for your stores. See the reference [Dockerfile](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/infra/compute_engines/spark_application/Dockerfile). +- **Network-accessible** online store, offline store, and registry. File-based backends are rejected because Spark pods have an ephemeral filesystem: + +| Rejected | Examples | Use instead | +|----------|----------|-------------| +| File online | `sqlite`, `faiss` | Redis, remote online, etc. | +| File offline | `dask`, `file`, `duckdb` | `spark`, Postgres, Snowflake, BigQuery, etc. | +| File registry | `file` | SQL registry, Snowflake | + +For distributed reads, configure `offline_store.type: spark` (or another store Spark can read efficiently). + +### Kubernetes / Feast Operator notes + +When using the Feast Operator: + +- Point `spec.batchEngine.configMapRef` at a ConfigMap whose `type` is `spark_application` (see [Guide 6 — Batch Engine & Scheduled Jobs](../../how-to-guides/feast-operator/06-batch-and-jobs.md)). +- The operator auto-creates RBAC for the `spark_application` batch engine (server and driver service accounts). +- Set `spec.services.initImage` if init / `feast-apply` containers need the Spark-capable image. + +--- + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_project +registry: + registry_type: sql + path: postgresql+psycopg://feast:****@postgres:5432/feast +online_store: + type: redis + connection_string: redis:6379 +offline_store: + type: spark + spark_conf: + spark.master: local[*] +batch_engine: + type: spark_application + image: my-registry.example.com/feast-spark-driver:latest + namespace: feast + spark_version: "4.0.1" + driver_cores: 1 + driver_memory: "2g" + executor_instances: 2 + executor_cores: 1 + executor_memory: "2g" + spark_conf: + spark.sql.shuffle.partitions: "100" +``` +{% endcode %} + +### Feast Operator ConfigMap + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: feast-spark-batch-engine + namespace: feast +data: + config: | + type: spark_application + image: my-registry.example.com/feast-spark-driver:latest + namespace: feast + executor_instances: 2 + driver_memory: "2g" + executor_memory: "2g" +--- +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: feast + namespace: feast +spec: + feastProject: my_project + batchEngine: + configMapRef: + name: feast-spark-batch-engine + configMapKey: config +``` + +--- + +## Remote materialization + +If the client uses a **remote** online store (`online_store.type: remote`), `FeatureStore.materialize()` delegates to the feature server HTTP API. The server runs the SparkApplication engine. + +- Default (`run_async=False`): block until the server finishes sync materialization. +- `run_async=True`: accept asynchronously (`?async=true`); poll feature-view state in the registry for completion. +- `force=True` (with `run_async=True`): override stuck `MATERIALIZING` state on the server. + +```python +from datetime import datetime, timedelta +from feast import FeatureStore + +store = FeatureStore(repo_path=".") # client feature_store.yaml with online_store.type: remote + +store.materialize( + start_date=datetime.utcnow() - timedelta(days=1), + end_date=datetime.utcnow(), +) +``` + +--- + +## Configuration reference + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `type` | string | `spark_application` | Engine type key | +| `image` | string | **required** | Container image for the Spark driver/executors | +| `image_pull_secrets` | list[str] | `[]` | Image pull secret names | +| `namespace` | string | `default` | Namespace for SparkApplication and ConfigMap | +| `service_account` | string | `""` | Driver service account; empty uses platform/operator default | +| `spark_version` | string | `4.0.1` | Spark version for the CR | +| `driver_cores` | int | `1` | Driver cores | +| `driver_memory` | string | `1g` | Driver memory | +| `executor_instances` | int | `1` | Number of executors | +| `executor_cores` | int | `1` | Cores per executor | +| `executor_memory` | string | `1g` | Memory per executor | +| `spark_conf` | dict | `null` | Extra Spark configuration | +| `hadoop_conf` | dict | `null` | Extra Hadoop configuration | +| `env` | list[dict] | `[]` | Driver env vars (`name` + `value` or `valueFrom`) | +| `env_from` | list[dict] | `[]` | EnvFrom sources | +| `queue_name` | string | `null` | Optional queue / Kueue label | +| `job_timeout_seconds` | int | `3600` | Max wait for SparkApplication completion | +| `poll_interval_seconds` | int | `10` | Status poll interval | +| `ttl_seconds_after_finished` | int | `3600` | CR TTL after finish | +| `restart_policy` | string | `Never` | SparkApplication restart policy | +| `max_retries` | int | `3` | Retries when restart policy allows | +| `concurrency` | int | `1` | Parallel feature views inside one driver | +| `labels` | dict | `{}` | Extra labels on the CR | +| `volumes` / `volume_mounts` | list | `[]` | Extra volumes for the driver | +| `py_files` | list[str] | `[]` | Additional Python files for Spark | +| `node_selector` | dict | `null` | Pod node selector | +| `tolerations` | list | `[]` | Pod tolerations | +| `staging_location` | string | `null` | Reserved for historical retrieval (ignored for materialize) | + +--- + +## Troubleshooting + +| Symptom | What to check | +|---------|----------------| +| SparkApplication Pending / insufficient CPU | Lower resource requests via `spark_conf` (for example `spark.kubernetes.driver.request.cores`) or free cluster capacity | +| ImagePullBackOff | Image name, tag, and `image_pull_secrets` | +| 403 on ConfigMap or SparkApplication | RBAC for the Feast server and Spark driver service accounts | +| Init `ValueError` about file-based stores | Switch online/offline/registry to network backends | +| Init / feast-apply failures missing Spark deps | Use a Spark-capable image (`initImage` with the Feast Operator) | + +--- + +## Related + +- [Spark compute engine (in-process)](spark.md) +- [Feast Operator — batch engine ConfigMap](../../how-to-guides/feast-operator/06-batch-and-jobs.md) +- [Creating a custom compute engine](../../how-to-guides/customizing-feast/creating-a-custom-compute-engine.md) diff --git a/docs/reference/data-sources/README.md b/docs/reference/data-sources/README.md index 151a948d0af..33e47672dcc 100644 --- a/docs/reference/data-sources/README.md +++ b/docs/reference/data-sources/README.md @@ -42,6 +42,10 @@ Please see [Data Source](../../getting-started/concepts/data-ingestion.md) for a [spark.md](spark.md) {% endcontent-ref %} +{% content-ref url="iceberg.md" %} +[iceberg.md](iceberg.md) +{% endcontent-ref %} + {% content-ref url="postgres.md" %} [postgres.md](postgres.md) {% endcontent-ref %} @@ -57,3 +61,19 @@ Please see [Data Source](../../getting-started/concepts/data-ingestion.md) for a {% content-ref url="clickhouse.md" %} [clickhouse.md](clickhouse.md) {% endcontent-ref %} + +{% content-ref url="athena.md" %} +[athena.md](athena.md) +{% endcontent-ref %} + +{% content-ref url="oracle.md" %} +[oracle.md](oracle.md) +{% endcontent-ref %} + +{% content-ref url="ray.md" %} +[ray.md](ray.md) +{% endcontent-ref %} + +{% content-ref url="mongodb.md" %} +[mongodb.md](mongodb.md) +{% endcontent-ref %} diff --git a/docs/reference/data-sources/athena.md b/docs/reference/data-sources/athena.md new file mode 100644 index 00000000000..d3ca67dcb10 --- /dev/null +++ b/docs/reference/data-sources/athena.md @@ -0,0 +1,37 @@ +# Athena source (contrib) + +## Description + +Athena data sources are AWS Athena tables or views. +These can be specified either by a table reference or a SQL query. + +## Disclaimer + +The Athena data source does not achieve full test coverage. +Please do not assume complete stability. + +## Examples + +Defining an Athena source: + +```python +from feast.infra.offline_stores.contrib.athena_offline_store.athena_source import ( + AthenaSource, +) + +driver_stats_source = AthenaSource( + name="driver_hourly_stats", + table="driver_hourly_stats", + database="my_database", + data_source="AwsDataCatalog", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) +``` + +The full set of configuration options is available [here](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.athena_offline_store.athena_source.AthenaSource). + +## Supported Types + +Athena data sources support standard Athena types mapped through the AWS Athena API. +For a comparison against other batch data sources, please see [here](overview.md#functionality-matrix). diff --git a/docs/reference/data-sources/iceberg.md b/docs/reference/data-sources/iceberg.md new file mode 100644 index 00000000000..6402a7ec419 --- /dev/null +++ b/docs/reference/data-sources/iceberg.md @@ -0,0 +1,161 @@ +# Iceberg source (contrib) + +## Description + +Iceberg data sources are tables managed by any supported Iceberg catalog. The `IcebergSource` class provides a unified interface with a configurable `catalog_type` parameter: + +- **`"rest"`** (default): [Apache Iceberg REST Catalog specification](https://iceberg.apache.org/concepts/catalog/#decoupling-using-the-rest-catalog) — Unity Catalog, Apache Polaris, Nessie, Snowflake Open Catalog +- **`"hive"`**: Hive Metastore catalog +- **`"glue"`**: AWS Glue catalog +- **`"sql"`**: SQL-based (JDBC) catalog +- **`"dynamodb"`**: DynamoDB-based catalog + +The data source carries catalog connection details (catalog_type, endpoint, warehouse, namespace, table, authentication). When the offline store (DuckDB, Spark) encounters this source, it resolves table metadata and credentials via the configured catalog at query time. + +## Examples + +### IcebergSource (REST catalog) + +Works with any Iceberg REST Catalog: + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import IcebergSource + +my_source = IcebergSource( + catalog_type="rest", # default + endpoint="http://localhost:8081/api/2.1/unity-catalog/iceberg", + warehouse="unity", + namespace="default", + table="driver_features", + timestamp_field="event_timestamp", + token_env_var="UC_TOKEN", +) +``` + +### IcebergSource (Hive Metastore) + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import IcebergSource + +my_source = IcebergSource( + catalog_type="hive", + catalog_properties={"uri": "thrift://metastore:9083"}, + warehouse="my_warehouse", + namespace="default", + table="driver_features", + timestamp_field="event_timestamp", +) +``` + +### IcebergSource (AWS Glue) + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import IcebergSource + +my_source = IcebergSource( + catalog_type="glue", + catalog_properties={"region_name": "us-east-1"}, + warehouse="my_account", + namespace="my_database", + table="driver_features", + timestamp_field="event_timestamp", +) +``` + +### UnityCatalogSource (with governance) {#unity-catalog-source} + +Extends `IcebergSource` with Unity Catalog governance: + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import ( + UnityCatalogSource, +) + +my_uc_source = UnityCatalogSource( + warehouse="production", + namespace="ml_features", + table="driver_stats", + timestamp_field="event_timestamp", + register_as_feature_table=True, # Register in UC on feast apply + sync_lineage=True, # Record lineage in UC +) +``` + +When `endpoint` is omitted, it defaults to `{DATABRICKS_HOST}/api/2.1/unity-catalog/iceberg`. +When `token_env_var` is omitted, it defaults to `DATABRICKS_TOKEN`. + +### Full Feature View Example + +```python +from datetime import timedelta + +from feast import Entity, FeatureView, Field +from feast.types import Float64, Int64 + +from feast.infra.data_sources.contrib.iceberg_catalog import ( + UnityCatalogSource, +) + +driver = Entity(name="driver_id", join_keys=["driver_id"]) + +driver_stats_source = UnityCatalogSource( + warehouse="production", + namespace="ml_features", + table="driver_hourly_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + source=driver_stats_source, + schema=[ + Field(name="conv_rate", dtype=Float64), + Field(name="acc_rate", dtype=Float64), + Field(name="avg_daily_trips", dtype=Int64), + ], + ttl=timedelta(days=1), + online=True, +) +``` + +## Configuration Reference + +### IcebergSource + +| Parameter | Type | Description | +| :--- | :--- | :--- | +| `catalog_type` | `str` | Catalog backend: `"rest"` (default), `"hive"`, `"glue"`, `"sql"`, `"dynamodb"` | +| `endpoint` | `str` | Catalog endpoint URL (required for `"rest"`, optional for others) | +| `warehouse` | `str` | Catalog/warehouse name | +| `namespace` | `str` | Schema/namespace within the catalog | +| `table` | `str` | Table name | +| `catalog_properties` | `dict` | Additional catalog-specific properties passed to PyIceberg | +| `timestamp_field` | `str` | Event timestamp column for point-in-time joins | +| `created_timestamp_column` | `str` | Optional column indicating row creation time | +| `token_env_var` | `str` | Environment variable name holding the auth token | +| `credential_vending` | `bool` | Whether to request scoped credentials (default: `True`) | +| `field_mapping` | `dict` | Column name mapping from source to feature names | + +### UnityCatalogSource (additional parameters) + +| Parameter | Type | Description | +| :--- | :--- | :--- | +| `register_as_feature_table` | `bool` | Register as UC feature table on `feast apply` (default: `True`) | +| `sync_lineage` | `bool` | Sync lineage metadata to Unity Catalog (default: `True`) | + +## Supported Types + +| Iceberg Type | Feast Type | +| :--- | :--- | +| `boolean` | `BOOL` | +| `int` | `INT32` | +| `long` | `INT64` | +| `float` | `FLOAT` | +| `double` | `DOUBLE` | +| `string` | `STRING` | +| `binary` | `BYTES` | +| `timestamp` / `timestamptz` | `INT64` | +| `decimal` | `DOUBLE` | +| `uuid` | `STRING` | diff --git a/docs/reference/data-sources/kafka.md b/docs/reference/data-sources/kafka.md index 8794c7a1e81..dd7203a6149 100644 --- a/docs/reference/data-sources/kafka.md +++ b/docs/reference/data-sources/kafka.md @@ -72,4 +72,4 @@ def driver_hourly_stats_stream(df: DataFrame): ``` ### Ingesting data -See [here](https://github.com/feast-dev/streaming-tutorial) for a example of how to ingest data from a Kafka source into Feast. +See [here](https://github.com/feast-dev/streaming-tutorial) for an example of how to ingest data from a Kafka source into Feast. diff --git a/docs/reference/data-sources/kinesis.md b/docs/reference/data-sources/kinesis.md index f2adadfec03..09706617da9 100644 --- a/docs/reference/data-sources/kinesis.md +++ b/docs/reference/data-sources/kinesis.md @@ -71,4 +71,4 @@ def driver_hourly_stats_stream(df: DataFrame): ``` ### Ingesting data -See [here](https://github.com/feast-dev/streaming-tutorial) for a example of how to ingest data from a Kafka source into Feast. The approach used in the tutorial can be easily adapted to work for Kinesis as well. +See [here](https://github.com/feast-dev/streaming-tutorial) for an example of how to ingest data from a Kafka source into Feast. The approach used in the tutorial can be easily adapted to work for Kinesis as well. diff --git a/docs/reference/data-sources/mongodb.md b/docs/reference/data-sources/mongodb.md new file mode 100644 index 00000000000..8902affd3c5 --- /dev/null +++ b/docs/reference/data-sources/mongodb.md @@ -0,0 +1,103 @@ +# MongoDB source (contrib) + +## Description + +MongoDB data sources are [MongoDB](https://www.mongodb.com/) collections that can be used as a source for feature data. The `MongoDBSource` points at a MongoDB collection and provides the metadata Feast needs to read historical features from the offline store's collection. + +## Examples + +Defining a MongoDB source: + +```python +from feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb import ( + MongoDBSource, +) + +driver_stats_source = MongoDBSource( + name="driver_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created_at", +) +``` + +The `name` field becomes the `feature_view` discriminator stored in every document in the `feature_history` collection. + +Configuration options such as `connection_string`, `database`, and `collection` are inherited from the offline store configuration in `feature_store.yaml`. + +The full set of configuration options is available [here](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb.MongoDBSource). + +## Vector Search + +The MongoDB online store supports [MongoDB Vector Search](https://www.mongodb.com/docs/atlas/atlas-vector-search/), enabling similarity search over feature embeddings stored in MongoDB. This is powered by the `$vectorSearch` aggregation stage and supports MongoDB Atlas, self-hosted MongoDB with Atlas Search indexes, and the `mongodb/mongodb-atlas-local` Docker image for local development. + +### Configuration + +Enable vector search in your `feature_store.yaml`: + +```yaml +project: my_project +provider: local +online_store: + type: mongodb + connection_string: mongodb+srv://:@cluster.mongodb.net # pragma: allowlist secret + vector_enabled: true + similarity: cosine # cosine | euclidean | dotProduct + vector_index_wait_timeout: 60 # seconds to wait for index to become queryable + vector_index_wait_poll_interval: 1.0 # seconds between polls +``` + +### Defining a Feature View with Vector Index + +Mark embedding fields with `vector_index=True` and specify `vector_length`: + +```python +from feast import Entity, FeatureView, Field, FileSource +from feast.types import Array, Float32, Int64, String +from datetime import timedelta + +item_embeddings = FeatureView( + name="item_embeddings", + entities=[Entity(name="item_id", join_keys=["item_id"])], + schema=[ + Field( + name="embedding", + dtype=Array(Float32), + vector_index=True, + vector_length=384, + vector_search_metric="cosine", + ), + Field(name="title", dtype=String), + Field(name="item_id", dtype=Int64), + ], + source=FileSource(path="items.parquet", timestamp_field="event_timestamp"), + ttl=timedelta(hours=24), +) +``` + +When `feast apply` (or `store.update()`) runs with `vector_enabled=True`, MongoDB vector search indexes are automatically created for any field with `vector_index=True`. Indexes are also automatically dropped when feature views are removed. + +### Retrieving Documents via Vector Search + +Use `retrieve_online_documents_v2()` to perform similarity search: + +```python +store = FeatureStore(repo_path=".") +results = store.retrieve_online_documents_v2( + features=["item_embeddings:embedding", "item_embeddings:title"], + query=[0.1, 0.2, ...], # query vector + top_k=5, +) +``` + +### How It Works + +- **Index creation**: `update()` creates a MongoDB vector search index named `____vs_index` for each vector-indexed field. It waits for the index to reach `READY` status before proceeding. +- **Query execution**: `retrieve_online_documents_v2()` builds a `$vectorSearch` aggregation pipeline with `numCandidates = max(top_k * 10, 100)` and the specified `limit`. +- **Score**: Results include a `distance` field populated from `$meta: "vectorSearchScore"`. +- **BSON compatibility**: Query vectors are coerced to native Python floats to avoid numpy serialization issues. +- **Idempotency**: Calling `update()` multiple times will not duplicate indexes. + +## Supported Types + +MongoDB data sources support all eight primitive types (`bytes`, `string`, `int32`, `int64`, `float32`, `float64`, `bool`, `timestamp`) and their corresponding array types. Complex types such as `Map` and `Struct` are preserved through the MongoDB document model. +For a comparison against other batch data sources, please see [here](overview.md#functionality-matrix). diff --git a/docs/reference/data-sources/oracle.md b/docs/reference/data-sources/oracle.md new file mode 100644 index 00000000000..48c249ba3b8 --- /dev/null +++ b/docs/reference/data-sources/oracle.md @@ -0,0 +1,36 @@ +# Oracle source (contrib) + +## Description + +Oracle data sources are Oracle database tables. +These are specified by a table reference (e.g. `"TRANSACTION_FEATURES"` or `"SCHEMA.TABLE"`). + +## Disclaimer + +The Oracle data source does not achieve full test coverage. +Please do not assume complete stability. + +## Examples + +Defining an Oracle source: + +```python +from feast.infra.offline_stores.contrib.oracle_offline_store.oracle_source import ( + OracleSource, +) + +driver_stats_source = OracleSource( + name="driver_hourly_stats", + table_ref="DRIVER_HOURLY_STATS", + event_timestamp_column="EVENT_TIMESTAMP", + created_timestamp_column="CREATED", +) +``` + +**Note:** Oracle stores unquoted identifiers in uppercase. Reference columns using the casing shown by Oracle (e.g. `USER_ID` for unquoted identifiers). + +## Supported Types + +Oracle data sources support standard Oracle numeric, string, date, and timestamp types mapped through the ibis Oracle backend. +For a comparison against other batch data sources, please see [here](overview.md#functionality-matrix). + diff --git a/docs/reference/data-sources/overview.md b/docs/reference/data-sources/overview.md index 7cffc4154dd..5cc5285f77e 100644 --- a/docs/reference/data-sources/overview.md +++ b/docs/reference/data-sources/overview.md @@ -5,7 +5,7 @@ In Feast, each batch data source is associated with corresponding offline stores. For example, a `SnowflakeSource` can only be processed by the Snowflake offline store, while a `FileSource` can be processed by both File and DuckDB offline stores. Otherwise, the primary difference between batch data sources is the set of supported types. -Feast has an internal type system, and aims to support eight primitive types (`bytes`, `string`, `int32`, `int64`, `float32`, `float64`, `bool`, and `timestamp`) along with the corresponding array types. +Feast has an internal type system that supports primitive types (`bytes`, `string`, `int32`, `int64`, `float32`, `float64`, `bool`, `timestamp`), array types, set types, map/JSON types, and struct types. However, not every batch data source supports all of these types. For more details on the Feast type system, see [here](../type-system.md). @@ -29,3 +29,9 @@ Below is a matrix indicating which data sources support which types. | `bool` | yes | yes | yes | yes | yes | yes | yes | yes | | `timestamp` | yes | yes | yes | yes | yes | yes | yes | yes | | array types | yes | yes | yes | no | yes | yes | yes | no | +| `Map` | yes | no | yes | yes | yes | yes | yes | no | +| `Json` | yes | yes | yes | yes | yes | no | no | no | +| `Struct` | yes | yes | no | no | yes | yes | no | no | +| set types | yes* | no | no | no | no | no | no | no | + +\* **Set types** are defined in Feast's proto and Python type system but are **not inferred** by any backend. They must be explicitly declared in the feature view schema and are best suited for online serving use cases. See [Type System](../type-system.md#set-types) for details. diff --git a/docs/reference/data-sources/ray.md b/docs/reference/data-sources/ray.md new file mode 100644 index 00000000000..30d4880a58a --- /dev/null +++ b/docs/reference/data-sources/ray.md @@ -0,0 +1,233 @@ +# Ray Data Source (contrib) + +> **⚠️ Contrib Plugin:** +> `RaySource` is a contributed plugin shipped alongside the [Ray offline store](../offline-stores/ray.md). It may not be as stable or fully supported as core data sources. + +`RaySource` is a pure-metadata descriptor that tells Feast **how** to load a +[Ray Dataset](https://docs.ray.io/en/latest/data/api/dataset.html) from any +source that Ray Data supports natively — Parquet, CSV, JSON, HuggingFace +Datasets, MongoDB, binary files, images, TFRecords, and more. + +It is the recommended data source when using the +[Ray offline store](../offline-stores/ray.md) and replaces the need for +`FileSource` for all non-Parquet and non-file-based data. + +--- + +## When to use RaySource vs FileSource + +| Scenario | Recommended source | +|---|---| +| Parquet files on disk / S3 / GCS (existing setup) | `FileSource` (backward compatible) | +| Parquet via Ray reader (pipelines, remote auth) | `RaySource(reader_type="parquet")` | +| CSV, JSON, text, images via Ray | `RaySource` | +| HuggingFace `datasets` library | `RaySource(reader_type="huggingface")` | +| MongoDB, SQL, TFRecords, WebDataset | `RaySource` | + +--- + +## Installation + +`RaySource` is bundled with the Ray offline store contrib package: + +```bash +pip install 'feast[ray]' +``` + +--- + +## Supported `reader_type` values + +| `reader_type` | Underlying Ray API | Notes | +|---|---|---| +| `parquet` | `ray.data.read_parquet` | S3, GCS, HDFS, local | +| `csv` | `ray.data.read_csv` | | +| `json` | `ray.data.read_json` | | +| `text` | `ray.data.read_text` | | +| `images` | `ray.data.read_images` | | +| `binary_files` | `ray.data.read_binary_files` | | +| `tfrecords` | `ray.data.read_tfrecords` | | +| `webdataset` | `ray.data.read_webdataset` | | +| `huggingface` | `ray.data.from_huggingface` | Wraps `datasets.load_dataset` | +| `mongo` | `ray.data.read_mongo` | | +| `sql` | `ray.data.read_sql` | Pass `connection_url` in `reader_options` | + +--- + +## Configuration + +### Parameters + +| Parameter | Type | Required | Description | +|---|---|---|---| +| `name` | `str` | Yes | Unique name for this data source | +| `reader_type` | `str` | Yes | One of the supported reader types above | +| `path` | `str` | No | File or directory path (required for file-based readers) | +| `reader_options` | `dict` | No | Extra keyword arguments forwarded to the Ray reader | +| `timestamp_field` | `str` | No | Column containing event timestamps | +| `created_timestamp_column` | `str` | No | Column containing row creation timestamps | +| `tags` | `dict` | No | Arbitrary key-value metadata | +| `description` | `str` | No | Human-readable description | +| `owner` | `str` | No | Owning team or contact | + +--- + +## Usage examples + +### Parquet on S3 + +```python +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +driver_stats = RaySource( + name="driver_stats_parquet", + reader_type="parquet", + path="s3://my-bucket/driver_stats/", + timestamp_field="event_timestamp", +) +``` + +### CSV + +```python +sensor_readings = RaySource( + name="sensor_readings_csv", + reader_type="csv", + path="/data/sensors/", + timestamp_field="ts", +) +``` + +### HuggingFace dataset + +Load a dataset from the [HuggingFace Hub](https://huggingface.co/datasets) +directly into Feast. + +```python +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +cheque_images = RaySource( + name="cheque_images_hf", + reader_type="huggingface", + reader_options={ + "dataset_name": "cheques_sample_data", + "split": "train", + }, + timestamp_field="event_timestamp", +) +``` + +### MongoDB + +```python +transaction_log = RaySource( + name="transactions_mongo", + reader_type="mongo", + reader_options={ + "uri": "mongodb://localhost:27017", + "database": "featuredb", + "collection": "transactions", + }, + timestamp_field="created_at", +) +``` + +### SQL (via connection URL) + +```python +user_features = RaySource( + name="user_features_sql", + reader_type="sql", + reader_options={ + "connection_url": "postgresql+psycopg2://user:password@host:5432/db", # pragma: allowlist secret + "query": "SELECT * FROM user_features", + }, + timestamp_field="event_timestamp", +) +``` + +--- + +## Using RaySource in a BatchFeatureView + +```python +from datetime import timedelta +from feast import BatchFeatureView, Entity, Field +from feast.types import Float32, Int64, String +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +cheque = Entity(name="cheque_id", description="Unique cheque identifier") + +cheque_source = RaySource( + name="cheque_images_hf", + reader_type="huggingface", + reader_options={ + "dataset_name": "cheques_sample_data", + "split": "train", + }, + timestamp_field="event_timestamp", +) + +cheque_ocr_fv = BatchFeatureView( + name="cheque_ocr_features", + entities=[cheque], + ttl=timedelta(days=365), + schema=[ + Field(name="cheque_id", dtype=Int64), + Field(name="payee_name", dtype=String), + Field(name="amount", dtype=String), + Field(name="bank_name", dtype=String), + Field(name="raw_text", dtype=String), + ], + source=cheque_source, +) +``` + +--- + +## Retrieving data as a Ray Dataset + +Once the feature view is materialised you can retrieve the offline features +directly as a Ray Dataset using the first-class `to_ray_dataset()` method: + +```python +from feast import FeatureStore + +store = FeatureStore(".") + +# Chain directly on the retrieval job — to_ray_dataset() is a first-class +# method on every RetrievalJobs. +ds = store.get_historical_features( + features=["cheque_ocr_features:payee_name", "cheque_ocr_features:amount"], + entity_df=entity_df, +).to_ray_dataset() + +# Use the dataset downstream in Ray or ML pipelines +ds.show(3) +``` + +--- + +## Proto serialisation + +`RaySource` is fully serialisable to Feast's protobuf registry format. The +`reader_type`, `path`, and `reader_options` dict are all persisted and can be +round-tripped via `to_proto()` / `from_proto()`. + +--- + +## Limitations + +* The Ray offline store (and therefore `RaySource`) requires `feast[ray]`. +* `reader_type="sql"` requires a serialisable `connection_url`; raw + `sqlalchemy.engine.Engine` objects cannot be pickled across Ray workers. +* Streaming sources (Kafka, Kinesis) are not supported via `RaySource`; use + the dedicated [Kafka](kafka.md) or [Kinesis](kinesis.md) data sources. + +--- + +## Related pages + +* [Ray Offline Store](../offline-stores/ray.md) +* [Ray Compute Engine](../compute-engine/ray.md) +* [Feature Retrieval](../../getting-started/concepts/feature-retrieval.md) diff --git a/docs/reference/data-sources/spark.md b/docs/reference/data-sources/spark.md index 99d5902667a..8967e8bd181 100644 --- a/docs/reference/data-sources/spark.md +++ b/docs/reference/data-sources/spark.md @@ -4,6 +4,8 @@ Spark data sources are tables or files that can be loaded from some Spark store (e.g. Hive or in-memory). They can also be specified by a SQL query. +**New in Feast:** SparkSource now supports advanced table formats including **Apache Iceberg**, **Delta Lake**, and **Apache Hudi**, enabling ACID transactions, time travel, and schema evolution capabilities. See the [Table Formats guide](table-formats.md) for detailed documentation. + ## Disclaimer The Spark data source does not achieve full test coverage. @@ -11,6 +13,8 @@ Please do not assume complete stability. ## Examples +### Basic Examples + Using a table reference from SparkSession (for example, either in-memory or a Hive Metastore): ```python @@ -51,8 +55,77 @@ my_spark_source = SparkSource( ) ``` +### Table Format Examples + +SparkSource supports advanced table formats for modern data lakehouse architectures. For detailed documentation, configuration options, and best practices, see the **[Table Formats guide](table-formats.md)**. + +#### Apache Iceberg + +```python +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import SparkSource +from feast.table_format import IcebergFormat + +iceberg_format = IcebergFormat( + catalog="my_catalog", + namespace="my_database" +) + +my_spark_source = SparkSource( + name="user_features", + path="my_catalog.my_database.user_table", + table_format=iceberg_format, + timestamp_field="event_timestamp" +) +``` + +#### Delta Lake + +```python +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import SparkSource +from feast.table_format import DeltaFormat + +delta_format = DeltaFormat() + +my_spark_source = SparkSource( + name="transaction_features", + path="s3://my-bucket/delta-tables/transactions", + table_format=delta_format, + timestamp_field="transaction_timestamp" +) +``` + +#### Apache Hudi + +```python +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import SparkSource +from feast.table_format import HudiFormat + +hudi_format = HudiFormat( + table_type="COPY_ON_WRITE", + record_key="user_id", + precombine_field="updated_at" +) + +my_spark_source = SparkSource( + name="user_profiles", + path="s3://my-bucket/hudi-tables/user_profiles", + table_format=hudi_format, + timestamp_field="event_timestamp" +) +``` + +For advanced configuration including time travel, incremental queries, and performance tuning, see the **[Table Formats guide](table-formats.md)**. + +## Configuration Options + The full set of configuration options is available [here](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.spark_offline_store.spark_source.SparkSource). +### Table Format Options + +- **IcebergFormat**: See [Table Formats - Iceberg](table-formats.md#apache-iceberg) +- **DeltaFormat**: See [Table Formats - Delta Lake](table-formats.md#delta-lake) +- **HudiFormat**: See [Table Formats - Hudi](table-formats.md#apache-hudi) + ## Supported Types Spark data sources support all eight primitive types and their corresponding array types. diff --git a/docs/reference/data-sources/table-formats.md b/docs/reference/data-sources/table-formats.md new file mode 100644 index 00000000000..f00e6f80c5f --- /dev/null +++ b/docs/reference/data-sources/table-formats.md @@ -0,0 +1,357 @@ +# Table Formats + +## Overview + +Table formats are metadata and transaction layers built on top of data storage formats (like Parquet). They provide advanced capabilities for managing large-scale data lakes, including ACID transactions, time travel, schema evolution, and efficient data management. + +Feast supports modern table formats to enable data lakehouse architectures with your feature store. + +## Supported Table Formats + +### Apache Iceberg + +[Apache Iceberg](https://iceberg.apache.org/) is an open table format designed for huge analytic datasets. It provides: +- **ACID transactions**: Atomic commits with snapshot isolation +- **Time travel**: Query data as of any snapshot +- **Schema evolution**: Add, drop, rename, or reorder columns safely +- **Hidden partitioning**: Partitioning is transparent to users +- **Performance**: Advanced pruning and filtering + +#### Basic Configuration + +```python +from feast.table_format import IcebergFormat + +iceberg_format = IcebergFormat( + catalog="my_catalog", + namespace="my_database" +) +``` + +#### Configuration Options + +| Parameter | Type | Description | +|-----------|------|-------------| +| `catalog` | `str` (optional) | Iceberg catalog name | +| `namespace` | `str` (optional) | Namespace/schema within the catalog | +| `properties` | `dict` (optional) | Additional Iceberg configuration properties | + +#### Common Properties + +```python +iceberg_format = IcebergFormat( + catalog="spark_catalog", + namespace="production", + properties={ + # Snapshot selection + "snapshot-id": "123456789", + "as-of-timestamp": "1609459200000", # Unix timestamp in ms + + # Performance tuning + "read.split.target-size": "134217728", # 128 MB splits + "read.parquet.vectorization.enabled": "true", + + # Advanced configuration + "io-impl": "org.apache.iceberg.hadoop.HadoopFileIO", + "warehouse": "s3://my-bucket/warehouse" + } +) +``` + +#### Time Travel Example + +```python +# Read from a specific snapshot +iceberg_format = IcebergFormat( + catalog="spark_catalog", + namespace="lakehouse" +) +iceberg_format.set_property("snapshot-id", "7896524153287651133") + +# Or read as of a timestamp +iceberg_format.set_property("as-of-timestamp", "1609459200000") +``` + +### Delta Lake + +[Delta Lake](https://delta.io/) is an open-source storage layer that brings ACID transactions to Apache Spark and big data workloads. It provides: +- **ACID transactions**: Serializable isolation for reads and writes +- **Time travel**: Access and revert to earlier versions +- **Schema enforcement**: Prevent bad data from corrupting tables +- **Unified batch and streaming**: Process data incrementally +- **Audit history**: Full history of all changes + +#### Basic Configuration + +```python +from feast.table_format import DeltaFormat + +delta_format = DeltaFormat() +``` + +#### Configuration Options + +| Parameter | Type | Description | +|-----------|------|-------------| +| `checkpoint_location` | `str` (optional) | Location for Delta transaction log checkpoints | +| `properties` | `dict` (optional) | Additional Delta configuration properties | + +#### Common Properties + +```python +delta_format = DeltaFormat( + checkpoint_location="s3://my-bucket/checkpoints", + properties={ + # Time travel + "versionAsOf": "5", + "timestampAsOf": "2024-01-01 00:00:00", + + # Performance optimization + "delta.autoOptimize.optimizeWrite": "true", + "delta.autoOptimize.autoCompact": "true", + + # Data skipping + "delta.dataSkippingNumIndexedCols": "32", + + # Z-ordering + "delta.autoOptimize.zOrderCols": "event_timestamp" + } +) +``` + +#### Time Travel Example + +```python +# Read from a specific version +delta_format = DeltaFormat() +delta_format.set_property("versionAsOf", "10") + +# Or read as of a timestamp +delta_format = DeltaFormat() +delta_format.set_property("timestampAsOf", "2024-01-15 12:00:00") +``` + +### Apache Hudi + +[Apache Hudi](https://hudi.apache.org/) (Hadoop Upserts Deletes and Incrementals) is a data lake storage framework for simplifying incremental data processing. It provides: +- **Upserts and deletes**: Efficient record-level updates +- **Incremental queries**: Process only changed data +- **Time travel**: Query historical versions +- **Multiple table types**: Optimize for read vs. write workloads +- **Change data capture**: Track data changes over time + +#### Basic Configuration + +```python +from feast.table_format import HudiFormat + +hudi_format = HudiFormat( + table_type="COPY_ON_WRITE", + record_key="user_id", + precombine_field="updated_at" +) +``` + +#### Configuration Options + +| Parameter | Type | Description | +|-----------|------|-------------| +| `table_type` | `str` (optional) | `COPY_ON_WRITE` or `MERGE_ON_READ` | +| `record_key` | `str` (optional) | Field(s) that uniquely identify a record | +| `precombine_field` | `str` (optional) | Field used to determine the latest version | +| `properties` | `dict` (optional) | Additional Hudi configuration properties | + +#### Table Types + +**COPY_ON_WRITE (COW)** +- Stores data in columnar format (Parquet) +- Updates create new file versions +- Best for **read-heavy workloads** +- Lower query latency + +```python +hudi_format = HudiFormat( + table_type="COPY_ON_WRITE", + record_key="id", + precombine_field="timestamp" +) +``` + +**MERGE_ON_READ (MOR)** +- Uses columnar + row-based formats +- Updates written to delta logs +- Best for **write-heavy workloads** +- Lower write latency + +```python +hudi_format = HudiFormat( + table_type="MERGE_ON_READ", + record_key="id", + precombine_field="timestamp" +) +``` + +#### Common Properties + +```python +hudi_format = HudiFormat( + table_type="COPY_ON_WRITE", + record_key="user_id", + precombine_field="updated_at", + properties={ + # Query type + "hoodie.datasource.query.type": "snapshot", # or "incremental" + + # Incremental queries + "hoodie.datasource.read.begin.instanttime": "20240101000000", + "hoodie.datasource.read.end.instanttime": "20240102000000", + + # Indexing + "hoodie.index.type": "BLOOM", + + # Compaction (for MOR tables) + "hoodie.compact.inline": "true", + "hoodie.compact.inline.max.delta.commits": "5", + + # Clustering + "hoodie.clustering.inline": "true" + } +) +``` + +#### Incremental Query Example + +```python +# Process only new/changed data +hudi_format = HudiFormat( + table_type="COPY_ON_WRITE", + record_key="id", + precombine_field="timestamp", + properties={ + "hoodie.datasource.query.type": "incremental", + "hoodie.datasource.read.begin.instanttime": "20240101000000", + "hoodie.datasource.read.end.instanttime": "20240102000000" + } +) +``` + +## Table Format vs File Format + +It's important to understand the distinction: + +| Aspect | File Format | Table Format | +|--------|-------------|--------------| +| **What it is** | Physical encoding of data | Metadata and transaction layer | +| **Examples** | Parquet, Avro, ORC, CSV | Iceberg, Delta Lake, Hudi | +| **Handles** | Data serialization | ACID, versioning, schema evolution | +| **Layer** | Storage layer | Metadata layer | + +### Can be used together + +```python +# Table format (metadata layer) built on top of file format (storage layer) +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import SparkSource +from feast.table_format import IcebergFormat + +iceberg = IcebergFormat(catalog="my_catalog", namespace="db") + +source = SparkSource( + name="features", + path="catalog.db.table", + file_format="parquet", # Underlying storage format + table_format=iceberg, # Table metadata format + timestamp_field="event_timestamp" +) +``` + +## Benefits of Table Formats + +### Reliability +- **ACID transactions**: Ensure data consistency across concurrent operations +- **Automatic retries**: Handle transient failures gracefully +- **Schema validation**: Prevent incompatible schema changes +- **Data quality**: Constraints and validation rules + +### Performance +- **Data skipping**: Read only relevant files based on metadata +- **Partition pruning**: Skip entire partitions based on predicates +- **Compaction**: Merge small files for better performance +- **Columnar pruning**: Read only necessary columns +- **Indexing**: Advanced indexing for fast lookups + +### Flexibility +- **Schema evolution**: Add, remove, or modify columns without rewriting data +- **Time travel**: Access historical data states for auditing or debugging +- **Incremental processing**: Process only changed data efficiently +- **Multiple readers/writers**: Concurrent access without conflicts + +## Choosing the Right Table Format + +| Use Case | Recommended Format | Why | +|----------|-------------------|-----| +| Large-scale analytics with frequent schema changes | **Iceberg** | Best schema evolution, hidden partitioning, mature ecosystem | +| Streaming + batch workloads | **Delta Lake** | Unified architecture, strong integration with Spark, good docs | +| CDC and upsert-heavy workloads | **Hudi** | Efficient record-level updates, incremental queries | +| Read-heavy analytics | **Iceberg or Delta** | Excellent query performance | +| Write-heavy transactional | **Hudi (MOR)** | Optimized for fast writes | +| Multi-engine support | **Iceberg** | Widest engine support (Spark, Flink, Trino, etc.) | + +## Best Practices + +### 1. Choose Appropriate Partitioning +```python +# Iceberg - hidden partitioning +iceberg_format.set_property("partition-spec", "days(event_timestamp)") + +# Delta - explicit partitioning in data source +# Hudi - configure via properties +hudi_format.set_property("hoodie.datasource.write.partitionpath.field", "date") +``` + +### 2. Enable Optimization Features +```python +# Delta auto-optimize +delta_format.set_property("delta.autoOptimize.optimizeWrite", "true") +delta_format.set_property("delta.autoOptimize.autoCompact", "true") + +# Hudi compaction +hudi_format.set_property("hoodie.compact.inline", "true") +``` + +### 3. Manage Table History +```python +# Regularly clean up old snapshots/versions +# For Iceberg: Use expire_snapshots() procedure +# For Delta: Use VACUUM command +# For Hudi: Configure retention policies +``` + +### 4. Monitor Metadata Size +- Table formats maintain metadata for all operations +- Monitor metadata size and clean up old versions +- Configure retention policies based on your needs + +### 5. Test Schema Evolution +```python +# Always test schema changes in non-production first +# Ensure backward compatibility +# Use proper migration procedures +``` + +## Data Source Support + +Currently, table formats are supported with: +- [Spark data source](spark.md) - Full support for Iceberg, Delta, and Hudi + +Future support planned for: +- BigQuery (Iceberg) +- Snowflake (Iceberg) +- Other data sources + +## See Also + +- [Spark Data Source](spark.md) +- [Apache Iceberg Documentation](https://iceberg.apache.org/docs/latest/) +- [Delta Lake Documentation](https://docs.delta.io/latest/index.html) +- [Apache Hudi Documentation](https://hudi.apache.org/docs/overview) +- [Python API Reference - TableFormat](https://rtd.feast.dev/en/master/#feast.table_format) \ No newline at end of file diff --git a/docs/reference/dqm.md b/docs/reference/dqm.md index 5a02413e534..47090b5dd1c 100644 --- a/docs/reference/dqm.md +++ b/docs/reference/dqm.md @@ -1,77 +1,81 @@ # Data Quality Monitoring -Data Quality Monitoring (DQM) is a Feast module aimed to help users to validate their data with the user-curated set of rules. -Validation could be applied during: -* Historical retrieval (training dataset generation) -* [planned] Writing features into an online store -* [planned] Reading features from an online store +Feast's Data Quality Monitoring (DQM) system computes, stores, and serves statistical metrics for every registered feature. It gives you visibility into feature health — distributions, null rates, percentiles, histograms — across batch data and feature serving logs. -Its goal is to address several complex data problems, namely: -* Data consistency - new training datasets can be significantly different from previous datasets. This might require a change in model architecture. -* Issues/bugs in the upstream pipeline - bugs in upstream pipelines can cause invalid values to overwrite existing valid values in an online store. -* Training/serving skew - distribution shift could significantly decrease the performance of the model. +Its goal is to address several complex data problems: -> To monitor data quality, we check that the characteristics of the tested dataset (aka the tested dataset's profile) are "equivalent" to the characteristics of the reference dataset. -> How exactly profile equivalency should be measured is up to the user. +* **Data consistency** — new training datasets can differ significantly from previous datasets, potentially requiring changes in model architecture. +* **Upstream pipeline bugs** — bugs in upstream pipelines can cause invalid values to overwrite existing valid values in an online store. +* **Training/serving skew** — distribution shift between training and serving data can decrease model performance. ### Overview -The validation process consists of the following steps: -1. User prepares reference dataset (currently only [saved datasets](../getting-started/concepts/dataset.md) from historical retrieval are supported). -2. User defines profiler function, which should produce profile by given dataset (currently only profilers based on [Great Expectations](https://docs.greatexpectations.io) are allowed). -3. Validation of tested dataset is performed with reference dataset and profiler provided as parameters. +Feast's DQM system works natively with your configured offline store — no additional infrastructure or external dependencies are required. The workflow is: -### Preparations -Feast with Great Expectations support can be installed via -```shell -pip install 'feast[ge]' +1. **Register features** — run `feast apply` to register feature views. If `auto_baseline: true` is configured, baseline metrics are computed automatically. +2. **Schedule monitoring** — run `feast monitor run` on a schedule (daily recommended) to compute metrics across multiple time windows. +3. **Read metrics** — query metrics via the REST API or view them in the Feast UI. + +### Configuration + +Enable DQM in your `feature_store.yaml`: + +```yaml +data_quality_monitoring: + auto_baseline: true ``` -### Dataset profile -Currently, Feast supports only [Great Expectation's](https://greatexpectations.io/) [ExpectationSuite](https://legacy.docs.greatexpectations.io/en/latest/autoapi/great_expectations/core/expectation_suite/index.html#great_expectations.core.expectation_suite.ExpectationSuite) -as dataset's profile. Hence, the user needs to define a function (profiler) that would receive a dataset and return an [ExpectationSuite](https://legacy.docs.greatexpectations.io/en/latest/autoapi/great_expectations/core/expectation_suite/index.html#great_expectations.core.expectation_suite.ExpectationSuite). +### Computing Metrics -Great Expectations supports automatic profiling as well as manually specifying expectations: -```python -from great_expectations.dataset import Dataset -from great_expectations.core.expectation_suite import ExpectationSuite +**Auto mode (recommended for production):** -from feast.dqm.profilers.ge_profiler import ge_profiler +```bash +feast monitor run +``` -@ge_profiler -def automatic_profiler(dataset: Dataset) -> ExpectationSuite: - from great_expectations.profile.user_configurable_profiler import UserConfigurableProfiler +This detects the latest event timestamp in the source data and computes metrics for 5 time windows: daily, weekly, biweekly, monthly, and quarterly. - return UserConfigurableProfiler( - profile_dataset=dataset, - ignored_columns=['conv_rate'], - value_set_threshold='few' - ).build_suite() +**Target a specific feature view:** + +```bash +feast monitor run --feature-view driver_stats ``` -However, from our experience capabilities of automatic profiler are quite limited. So we would recommend crafting your own expectations: -```python -@ge_profiler -def manual_profiler(dataset: Dataset) -> ExpectationSuite: - dataset.expect_column_max_to_be_between("column", 1, 2) - return dataset.get_expectation_suite() + +**Explicit date range:** + +```bash +feast monitor run \ + --feature-view driver_stats \ + --start-date 2025-01-01 \ + --end-date 2025-01-07 \ + --granularity weekly ``` +**Set a manual baseline:** +```bash +feast monitor run \ + --feature-view driver_stats \ + --start-date 2025-01-01 \ + --end-date 2025-03-31 \ + --granularity daily \ + --set-baseline +``` + +### Monitoring Feature Serving Logs + +If your feature services have logging configured, you can compute metrics from the actual features served to models in production: -### Validating Training Dataset -During retrieval of historical features, `validation_reference` can be passed as a parameter to methods `.to_df(validation_reference=...)` or `.to_arrow(validation_reference=...)` of RetrievalJob. -If parameter is provided Feast will run validation once dataset is materialized. In case if validation successful materialized dataset is returned. -Otherwise, `feast.dqm.errors.ValidationFailed` exception would be raised. It will consist of all details for expectations that didn't pass. +```bash +feast monitor run --source-type log +``` -```python -from feast import FeatureStore +### Reading Metrics -fs = FeatureStore(".") +Metrics are accessible via the REST API: -job = fs.get_historical_features(...) -job.to_df( - validation_reference=fs - .get_saved_dataset("my_reference_dataset") - .as_reference(profiler=manual_profiler) -) ``` +GET /monitoring/metrics/features?project=my_project&feature_view_name=driver_stats&granularity=daily +``` + +See the [Feature Quality Monitoring guide](../how-to-guides/feature-monitoring.md) for full API reference, UI integration, and orchestrator examples. diff --git a/docs/reference/feast-cli-commands.md b/docs/reference/feast-cli-commands.md index 15157863e9c..85781f6abc2 100644 --- a/docs/reference/feast-cli-commands.md +++ b/docs/reference/feast-cli-commands.md @@ -20,12 +20,15 @@ Options: Commands: apply Create or update a feature store deployment configuration Display Feast configuration + delete Delete a Feast object from the registry + demo-notebooks Generate demo Jupyter notebooks for the project entities Access entities feature-views Access feature views init Create a new Feast repository materialize Run a (non-incremental) materialization job to... materialize-incremental Run an incremental materialization job to ingest... permissions Access permissions + registry Manage the feature registry registry-dump Print contents of the metadata registry teardown Tear down deployed feature store infrastructure version Display Feast SDK version @@ -51,17 +54,40 @@ Creates or updates a feature store deployment feast apply ``` +**Options:** +* `--skip-source-validation`: Skip validation of data sources (don't check if tables exist) +* `--skip-feature-view-validation`: Skip validation of feature views. Use with caution as this skips important checks + +```bash +# Skip only data source validation +feast apply --skip-source-validation + +# Skip only feature view validation +feast apply --skip-feature-view-validation + +# Skip both validations +feast apply --skip-source-validation --skip-feature-view-validation +``` + **What does Feast apply do?** 1. Feast will scan Python files in your feature repository and find all Feast object definitions, such as feature views, entities, and data sources. -2. Feast will validate your feature definitions (e.g. for uniqueness of features) +2. Feast will validate your feature definitions (e.g. for uniqueness of features). This validation can be skipped using the `--skip-feature-view-validation` flag if the type/validation system is being overly strict. 3. Feast will sync the metadata about Feast objects to the registry. If a registry does not exist, then it will be instantiated. The standard registry is a simple protobuf binary file that is stored on disk \(locally or in an object store\). 4. Feast CLI will create all necessary feature store infrastructure. The exact infrastructure that is deployed or configured depends on the `provider` configuration that you have set in `feature_store.yaml`. For example, setting `local` as your provider will result in a `sqlite` online store being created. +{% hint style="info" %} +The `--skip-feature-view-validation` flag is particularly useful for On-Demand Feature Views (ODFVs) with complex transformations that may fail validation. However, use it with caution and please report any validation issues to the Feast team on GitHub. +{% endhint %} + {% hint style="warning" %} `feast apply` \(when configured to use cloud provider like `gcp` or `aws`\) will create cloud infrastructure. This may incur costs. {% endhint %} +{% hint style="info" %} +**Important:** `feast apply` only registers or updates objects found in your Python files. It does **not** delete objects that you've removed from your code. To delete objects from the registry, you must use the `feast delete` command or explicit delete methods in the Python SDK. See the [Delete command](#delete) below and the [Registry documentation](../getting-started/components/registry.md#deleting-objects-from-the-registry) for details. +{% endhint %} + ## Configuration Display the actual configuration being used by Feast, including both user-provided configurations and default configurations applied by Feast. @@ -84,6 +110,81 @@ auth: type: no_auth ``` +## Delete + +Delete a Feast object from the registry by its name. + +```bash +feast delete +``` + +**What does feast delete do?** + +The `feast delete` command removes a Feast object (such as a feature view, entity, data source, feature service, etc.) from the registry. The command will: + +1. Search for the object by name across all object types (entities, feature views, feature services, data sources, saved datasets, validation references, etc.) +2. Delete the first matching object found +3. Remove any associated infrastructure + +**Example:** + +```bash +# Delete a feature view named "driver_hourly_stats" +feast delete driver_hourly_stats + +# Delete an entity named "driver" +feast delete driver +``` + +{% hint style="warning" %} +The delete operation is permanent and will remove the object from the registry. Make sure you want to delete the object before running this command. +{% endhint %} + +{% hint style="info" %} +If multiple objects have the same name across different types, `feast delete` will delete the first one it finds. For programmatic deletion with more control, use the Python SDK methods like `store.delete_feature_view()`, `store.delete_feature_service()`, etc. +{% endhint %} + +## Demo Notebooks + +Generate tailored demo Jupyter notebooks for each Feast project found in the current directory. + +```bash +feast demo-notebooks +``` + +The command searches for `feature_store.yaml` in the current directory and every file inside the `feast-config/` directory. Each file is treated as a separate project config, and notebooks are created under `./feast-demo-notebooks//`. + +The generated notebooks adapt to your project configuration (online/offline store types, authentication, vector search) and cover: + +* **Feature store overview** — explore registered entities, feature views, and services. +* **Historical feature retrieval** — build training datasets with point-in-time correct joins. +* **Online feature serving** — materialize features and retrieve them at low latency. + +**Options:** + +* `-o, --output-dir` — Directory where the notebooks are written. Default: `./feast-demo-notebooks`. +* `--overwrite` — Overwrite existing notebooks if the output directory already exists. + +```bash +feast demo-notebooks -o ./my-notebooks --overwrite +``` + +You can also use the `--chdir` global option to point at a different feature repository: + +```bash +feast -c /path/to/feature_repo demo-notebooks +``` + +The same functionality is available via the Python SDK: + +```python +from feast import copy_demo_notebooks + +copy_demo_notebooks(output_dir="./feast-demo-notebooks", repo_path=".") +``` + +For more details see the [Demo Notebooks tutorial](../tutorials/demo-notebooks.md). + ## Entities List all registered entities @@ -118,6 +219,18 @@ NAME ENTITIES TYPE driver_hourly_stats {'driver'} FeatureView ``` +List version history for a feature view + +```text +feast feature-views list-versions FEATURE_VIEW_NAME +``` + +```text +VERSION TYPE CREATED VERSION_ID +v0 feature_view 2024-01-15 10:30:00 a1b2c3d4-... +v1 feature_view 2024-01-16 14:22:00 e5f6g7h8-... +``` + ## Init Creates a new feature repository @@ -371,6 +484,18 @@ reader driver_hourly_stats_fresh FeatureView DESCRIBE ``` +## Registry + +### create-schema + +Pre-create the SQL registry schema so the application does not need DDL privileges at runtime. Use this with `schema_mode: verify` or `schema_mode: skip` in your `feature_store.yaml`. + +```text +feast registry create-schema +``` + +This command only applies to SQL-based registries (`registry_type: sql`). It is safe to run multiple times — existing tables are not modified. + ## Teardown Tear down deployed feature store infrastructure diff --git a/docs/reference/feature-repository/README.md b/docs/reference/feature-repository/README.md index 2c1b112a783..38968825c1d 100644 --- a/docs/reference/feature-repository/README.md +++ b/docs/reference/feature-repository/README.md @@ -127,4 +127,4 @@ To declare new feature definitions, just add code to the feature repository, eit ### Next steps * See [Create a feature repository](../../how-to-guides/feast-snowflake-gcp-aws/create-a-feature-repository.md) to get started with an example feature repository. -* See [feature_store.yaml](feature-store-yaml.md), [.feastignore](feast-ignore.md), or [Feature Views](../../getting-started/concepts/feature-view.md) for more information on the configuration files that live in a feature registry. +* See [feature_store.yaml](feature-store-yaml.md), [.feastignore](feast-ignore.md), [Registration inferencing](registration-inferencing.md), or [Feature Views](../../getting-started/concepts/feature-view.md) for more information on the configuration files that live in a feature registry. diff --git a/docs/reference/feature-repository/feature-store-yaml.md b/docs/reference/feature-repository/feature-store-yaml.md index a87e09ba43e..aec6082b383 100644 --- a/docs/reference/feature-repository/feature-store-yaml.md +++ b/docs/reference/feature-repository/feature-store-yaml.md @@ -25,5 +25,69 @@ The following top-level configuration options exist in the `feature_store.yaml` * **offline_store** — Configures the offline store. * **project** — Defines a namespace for the entire feature store. Can be used to isolate multiple deployments in a single installation of Feast. Should only contain letters, numbers, and underscores. * **engine** - Configures the batch materialization engine. +* **materialization** - Configures materialization behavior (write batching, feature pull strategy). See below. Please see the [RepoConfig](https://rtd.feast.dev/en/latest/#feast.repo_config.RepoConfig) API reference for the full list of configuration options. + +--- + +## `materialization` configuration + +The `materialization` block controls how Feast reads from the offline store and writes to the online store during `feast materialize` / `feast materialize-incremental` runs. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: redis + connection_string: "localhost:6379" +materialization: + online_write_batch_size: 10000 # write rows in chunks of 10 000 + pull_latest_features: false # pull full time range (default) +``` +{% endcode %} + +### `online_write_batch_size` + +| Field | Type | Default | Supported engines | +| --- | --- | --- | --- | +| `online_write_batch_size` | `int` (positive) | `null` | local, spark, ray | + +Controls how many rows are converted to protobuf and written to the online store per batch during materialization. + +**Default behaviour (`null`):** All rows fetched from the offline store are converted to protobuf in a single in-memory operation before writing. This is fast but can exhaust memory for large datasets — every row must be held as a Python proto object simultaneously. + +**With `online_write_batch_size` set:** The Arrow table returned by the offline store is split into chunks of at most `online_write_batch_size` rows. Each chunk is converted and written independently, keeping peak memory proportional to the batch size rather than the full dataset size. + +```yaml +# Recommended for datasets > a few million rows or memory-constrained workers +materialization: + online_write_batch_size: 10000 +``` + +**Choosing a value:** + +| Dataset size | Worker memory | Recommended batch size | +| --- | --- | --- | +| < 1 M rows | Any | `null` (default — single batch is fine) | +| 1–10 M rows | ≥ 4 GB | `50000` | +| 10–100 M rows | ≥ 8 GB | `10000` | +| > 100 M rows | Any | `5000`–`10000` | + +A smaller batch size reduces peak memory at the cost of more `online_write_batch` calls to the online store. For Redis, each call is a pipelined batch, so the overhead is low. For stores with higher per-call latency (e.g. DynamoDB), prefer larger batch sizes. + +{% hint style="info" %} +`online_write_batch_size` is applied **per feature view** within a single materialization job. If you materialize five feature views in parallel, peak memory is `5 × batch_size × bytes_per_row`. +{% endhint %} + +### `pull_latest_features` + +| Field | Type | Default | +| --- | --- | --- | +| `pull_latest_features` | `bool` | `false` | + +When `false` (default), the offline store retrieves **all** feature values within the requested time range for each entity. + +When `true`, only the **latest** value per entity is retrieved. This reduces I/O and memory for feature views where historical values are not needed (e.g., slowly changing dimensions). It is equivalent to running a `GROUP BY entity, MAX(event_timestamp)` on the offline data before writing. diff --git a/docs/reference/feature-servers/mcp-feature-server.md b/docs/reference/feature-servers/mcp-feature-server.md new file mode 100644 index 00000000000..8bfc96b1891 --- /dev/null +++ b/docs/reference/feature-servers/mcp-feature-server.md @@ -0,0 +1,51 @@ +# MCP Feature Server + +## Overview + +Feast can expose the Python Feature Server as an MCP (Model Context Protocol) server using `fastapi_mcp`. When enabled, MCP clients can discover and call Feast tools such as online feature retrieval. + +## Installation + +```bash +pip install feast[mcp] +``` + +## Configuration + +Add an MCP `feature_server` block to your `feature_store.yaml`: + +```yaml +feature_server: + type: mcp + enabled: true + mcp_enabled: true + mcp_transport: http + mcp_server_name: "feast-feature-store" + mcp_server_version: "1.0.0" +``` + +### mcp_transport + +`mcp_transport` controls how MCP is mounted into the Feature Server: + +- `sse`: SSE-based transport. This is the default for backward compatibility. +- `http`: Streamable HTTP transport. This is recommended for improved compatibility with some MCP clients. + +If `mcp_transport: http` is configured but your installed `fastapi_mcp` version does not support Streamable HTTP mounting, Feast will fail fast with an error asking you to upgrade `fastapi_mcp` (or reinstall `feast[mcp]`). + +## Endpoints + +MCP is mounted at: + +- `/mcp` + +## Connecting an MCP client + +Use your MCP client’s “HTTP” configuration and point it to the Feature Server base URL. For example, if your Feature Server runs at `http://localhost:6566`, use: + +- `http://localhost:6566/mcp` + +## Troubleshooting + +- If you see a deprecation warning about `mount()` at runtime, upgrade `fastapi_mcp` and use `mcp_transport: http` or `mcp_transport: sse`. +- If your MCP client has intermittent connectivity issues with `mcp_transport: sse`, switch to `mcp_transport: http`. diff --git a/docs/reference/feature-servers/offline-feature-server.md b/docs/reference/feature-servers/offline-feature-server.md index 9fd429794f7..40d48b80604 100644 --- a/docs/reference/feature-servers/offline-feature-server.md +++ b/docs/reference/feature-servers/offline-feature-server.md @@ -17,7 +17,7 @@ See [this](../../how-to-guides/running-feast-in-production.md#id-4.2.-deploy-fea The Offline feature server can be deployed with a slight modification of the FeatureStore CR - ```yaml -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-offline-server diff --git a/docs/reference/feature-servers/python-feature-server.md b/docs/reference/feature-servers/python-feature-server.md index f8e121ad6af..b1b873cc7d2 100644 --- a/docs/reference/feature-servers/python-feature-server.md +++ b/docs/reference/feature-servers/python-feature-server.md @@ -8,6 +8,51 @@ The Python feature server is an HTTP endpoint that serves features with JSON I/O There is a CLI command that starts the server: `feast serve`. By default, Feast uses port 6566; the port be overridden with a `--port` flag. +### Performance Configuration + +For production deployments, the feature server supports several performance optimization options: + +```bash +# Basic usage +feast serve + +# Production configuration with multiple workers +feast serve --workers -1 --worker-connections 1000 --registry_ttl_sec 60 + +# Manual worker configuration +feast serve --workers 8 --worker-connections 2000 --max-requests 1000 +``` + +Key performance options: +- `--workers, -w`: Number of worker processes. Use `-1` to auto-calculate based on CPU cores (recommended for production) +- `--worker-connections`: Maximum simultaneous clients per worker process (default: 1000) +- `--max-requests`: Maximum requests before worker restart, prevents memory leaks (default: 1000) +- `--max-requests-jitter`: Jitter to prevent thundering herd on worker restart (default: 50) +- `--registry_ttl_sec, -r`: Registry refresh interval in seconds. Higher values reduce overhead but increase staleness (default: 60) +- `--keep-alive-timeout`: Keep-alive connection timeout in seconds (default: 30) + +### Performance Best Practices + +**Worker Configuration:** +- For production: Use `--workers -1` to auto-calculate optimal worker count (2 × CPU cores + 1) +- For development: Use default single worker (`--workers 1`) +- Monitor CPU and memory usage to tune worker count manually if needed + +**Registry TTL:** +- Production: Use `--registry_ttl_sec 60` or higher to reduce refresh overhead +- Development: Use lower values (5-10s) for faster iteration when schemas change frequently +- Balance between performance (higher TTL) and freshness (lower TTL) + +**Connection Tuning:** +- Increase `--worker-connections` for high-concurrency workloads +- Use `--max-requests` to prevent memory leaks in long-running deployments +- Adjust `--keep-alive-timeout` based on client connection patterns + +**Container Deployments:** +- Set appropriate CPU/memory limits in Kubernetes to match worker configuration +- Use HTTP health checks instead of TCP for better application-level monitoring +- Consider horizontal pod autoscaling based on request latency metrics + ## Deploying as a service See [this](../../how-to-guides/running-feast-in-production.md#id-4.2.-deploy-feast-feature-servers-on-kubernetes) for an example on how to run Feast on Kubernetes using the Operator. @@ -199,6 +244,26 @@ requests.post( "http://localhost:6566/push", data=json.dumps(push_data)) ``` +#### Offline write batching for `/push` + +The Python feature server supports configurable batching for the **offline** +portion of writes executed via the `/push` endpoint. + +Only the offline part of a push is affected: + +- `to: "offline"` → **fully batched** +- `to: "online_and_offline"` → **online written immediately**, **offline batched** +- `to: "online"` → unaffected, always immediate + +Enable batching in your `feature_store.yaml`: + +```yaml +feature_server: + type: local + offline_push_batching_enabled: true + offline_push_batching_batch_size: 1000 + offline_push_batching_batch_interval_seconds: 10 +``` ### Materializing features @@ -246,6 +311,258 @@ requests.post( data=json.dumps(materialize_data)) ``` +## Prometheus Metrics + +The Python feature server can expose Prometheus-compatible metrics on a dedicated +HTTP endpoint (default port `8000`). Metrics are **opt-in** and carry zero overhead +when disabled. + +### Enabling metrics + +**Option 1 — CLI flag** (useful for one-off runs): + +```bash +feast serve --metrics +``` + +**Option 2 — `feature_store.yaml`** (recommended for production): + +```yaml +feature_server: + type: local + metrics: + enabled: true +``` + +Either option is sufficient. When both are set, metrics are enabled. + +### Per-category control + +By default, enabling metrics turns on **all** categories. You can selectively +disable individual categories within the same `metrics` block: + +```yaml +feature_server: + type: local + metrics: + enabled: true + resource: true # CPU / memory gauges + request: false # disable endpoint latency & request counters + online_features: true # online feature retrieval counters + push: true # push request counters + materialization: true # materialization counters & duration + freshness: true # feature freshness gauges + offline_features: true # offline store retrieval counters & latency + audit_logging: false # structured JSON audit logs (see below) +``` + +Any category set to `false` will emit no metrics and start no background +threads (e.g., setting `freshness: false` prevents the registry polling +thread from starting). All categories default to `true` except +`audit_logging`, which defaults to `false`. + +### Available metrics + +| Metric | Type | Labels | Category | Description | +|--------|------|--------|----------|-------------| +| `feast_feature_server_cpu_usage` | Gauge | — | `resource` | Process CPU usage % | +| `feast_feature_server_memory_usage` | Gauge | — | `resource` | Process memory usage % | +| `feast_feature_server_request_total` | Counter | `endpoint`, `status` | `request` | Total requests per endpoint | +| `feast_feature_server_request_latency_seconds` | Histogram | `endpoint`, `feature_count`, `feature_view_count` | `request` | Request latency with p50/p95/p99 support | +| `feast_online_features_request_total` | Counter | — | `online_features` | Total online feature retrieval requests | +| `feast_online_features_entity_count` | Histogram | — | `online_features` | Entity rows per online feature request | +| `feast_feature_server_online_store_read_duration_seconds` | Histogram | — | `online_features` | Online store read phase duration (sync and async) | +| `feast_feature_server_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | `online_features` | ODFV read-path transformation duration (requires `track_metrics=True` on the ODFV) | +| `feast_feature_server_write_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | `online_features` | ODFV write-path transformation duration (requires `track_metrics=True` on the ODFV) | +| `feast_push_request_total` | Counter | `push_source`, `mode` | `push` | Push requests by source and mode | +| `feast_materialization_result_total` | Counter | `feature_view`, `status` | `materialization` | Materialization runs (success/failure) | +| `feast_materialization_duration_seconds` | Histogram | `feature_view` | `materialization` | Materialization duration per feature view | +| `feast_feature_freshness_seconds` | Gauge | `feature_view`, `project` | `freshness` | Seconds since last materialization | +| `feast_offline_store_request_total` | Counter | `method`, `status` | `offline_features` | Total offline store retrieval requests | +| `feast_offline_store_request_latency_seconds` | Histogram | `method` | `offline_features` | Latency of offline store retrieval operations | +| `feast_offline_store_row_count` | Histogram | `method` | `offline_features` | Rows returned by offline store retrieval | + +### Per-ODFV transformation metrics + +The `transformation_duration_seconds` and `write_transformation_duration_seconds` +metrics are gated behind **two** conditions — both must be true for any +instrumentation to run: + +1. **Server-level**: the `online_features` category must be enabled in the + metrics configuration. +2. **ODFV-level**: the `OnDemandFeatureView` must have `track_metrics=True`. + +This defaults to `False`, so no ODFV incurs timing overhead unless explicitly +opted in: + +```python +from feast.on_demand_feature_view import on_demand_feature_view + +@on_demand_feature_view( + sources=[my_feature_view, my_request_source], + schema=[Field(name="output", dtype=Float64)], + track_metrics=True, # opt in to transformation timing +) +def my_transform(inputs: pd.DataFrame) -> pd.DataFrame: + ... +``` + +The `odfv_name` label lets you filter or group by individual ODFV, +and the `mode` label (`python`, `pandas`, `substrait`) lets you compare +transformation engines. + +### Audit logging + +Feast can emit structured JSON audit log entries for every online and offline +feature retrieval. These are written via the standard `feast.audit` Python +logger, so you can route them to a dedicated file, SIEM, or log aggregator +independently of application logs. + +Audit logging is **disabled by default**. Enable it in `feature_store.yaml`: + +```yaml +feature_server: + type: local + metrics: + enabled: true + audit_logging: true +``` + +**Online audit log** (emitted per `/get-online-features` call): + +```json +{ + "event": "online_feature_request", + "timestamp": "2026-05-11T08:30:00.123456+00:00", + "requestor_id": "user@example.com", + "entity_keys": ["driver_id"], + "entity_count": 3, + "feature_views": ["driver_hourly_stats"], + "feature_count": 3, + "status": "success", + "latency_ms": 12.34 +} +``` + +**Offline audit log** (emitted per `RetrievalJob.to_arrow()` call): + +```json +{ + "event": "offline_feature_retrieval", + "timestamp": "2026-05-11T08:31:00.456789+00:00", + "method": "to_arrow", + "start_time": "2026-05-11T08:30:59.226789+00:00", + "end_time": "2026-05-11T08:31:00.456789+00:00", + "feature_views": ["driver_hourly_stats"], + "feature_count": 3, + "row_count": 500, + "status": "success", + "duration_ms": 1230.0 +} +``` + +The `requestor_id` field in online audit logs is populated from the +security manager's current user when authentication is configured, and +falls back to `"anonymous"` otherwise. + +To route audit logs to a separate file: + +```python +import logging + +handler = logging.FileHandler("/var/log/feast/audit.log") +handler.setFormatter(logging.Formatter("%(message)s")) +logging.getLogger("feast.audit").addHandler(handler) +``` + +### Scraping with Prometheus + +```yaml +scrape_configs: + - job_name: feast + static_configs: + - targets: ["localhost:8000"] +``` + +### Kubernetes / Feast Operator + +Set `metrics: true` in your FeatureStore CR: + +```yaml +spec: + services: + onlineStore: + server: + metrics: true +``` + +The operator automatically exposes port 8000 and creates the corresponding +Service port so Prometheus can discover it. + +### Multi-worker and multi-replica (HPA) support + +Feast uses Prometheus **multiprocess mode** so that metrics are correct +regardless of the number of Gunicorn workers or Kubernetes replicas. + +**How it works:** + +* Each Gunicorn worker writes metric values to shared files in a + temporary directory (`PROMETHEUS_MULTIPROCESS_DIR`). Feast creates + this directory automatically; you can override it by setting the + environment variable yourself. +* The metrics HTTP server on port 8000 aggregates all workers' + metric files using `MultiProcessCollector`, so a single scrape + returns accurate totals. +* Gunicorn hooks clean up dead-worker files automatically + (`child_exit` → `mark_process_dead`). +* CPU and memory gauges use `multiprocess_mode=liveall` — Prometheus + shows per-worker values distinguished by a `pid` label. +* Feature freshness gauges use `multiprocess_mode=max` — Prometheus + shows the worst-case staleness (all workers compute the same value). +* Counters and histograms (request counts, latency, materialization) + are automatically summed across workers. + +**Multiple replicas (HPA):** Each pod runs its own metrics endpoint. +Prometheus adds an `instance` label per pod, so there is no +duplication. Use `sum(rate(...))` or `histogram_quantile(...)` across +instances as usual. + +## Vector Search (`POST /search`) + +The feature server exposes `POST /search` for vector similarity search against online document embeddings. Pass a pre-computed embedding in `query`, or use `api_version: 2` with `query_string` for text-based search when the online store supports it. + +`POST /retrieve-online-documents` is a deprecated alias with the same request body and response; new integrations should use `/search`. + +## [Alpha] OpenAI-Compatible Vector Store API + +{% hint style="warning" %} +**Alpha feature.** This API surface is functional and tested, but may change in future releases. +{% endhint %} + +The feature server exposes OpenAI-compatible vector store endpoints. This allows clients (including LLM agents and tool-calling frameworks) to discover and search vector data with plain text queries, without computing embeddings client-side. + +Each feature view with vector-indexed fields gets a deterministic `vs_{hash}` identifier derived from `SHA-256(project + ":" + feature_view_name)`. These IDs are stable across server restarts. + +### Endpoints + +| Method | Path | RBAC | Description | +|---|---|---|---| +| `GET` | `/v1/vector_stores` | `DESCRIBE` | List all vector stores (filtered by caller permissions) | +| `GET` | `/v1/vector_stores/{vector_store_id}` | `DESCRIBE` | Get metadata for a single vector store | +| `POST` | `/v1/vector_stores/{vector_store_id}/search` | `READ_ONLINE` | Search a vector store with server-side embedding | + +### Configuration + +Add an `embedding_model` section to your `feature_store.yaml`: + +```yaml +embedding_model: + provider: sentence_transformers # default; can be omitted + model: all-MiniLM-L6-v2 +``` + +Feast uses **Sentence Transformers** (default) for local embedding inference — no external API key required. Custom embedding providers can be plugged in by implementing the `EmbeddingProvider` protocol. See [\[Alpha\] Vector Database](../alpha-vector-database.md#alpha-openai-compatible-vector-store-api) for full configuration, custom providers, filter details, and SDK usage. + ## Starting the feature server in TLS(SSL) mode Enabling TLS mode ensures that data between the Feast client and server is transmitted securely. For an ideal production environment, it is recommended to start the feature server in TLS mode. @@ -268,6 +585,48 @@ To start the feature server in TLS mode, you need to provide the private and pub feast serve --key /path/to/key.pem --cert /path/to/cert.pem ``` +# [Alpha] Static Artifacts Loading + +**Warning**: This is an experimental feature. To our knowledge, this is stable, but there are still rough edges in the experience. + +Static artifacts loading allows you to load models, lookup tables, and other static resources once during feature server startup instead of loading them on each request. This improves performance for on-demand feature views that require external resources. + +## Quick Example + +Create a `static_artifacts.py` file in your feature repository: + +```python +# static_artifacts.py +from fastapi import FastAPI +from transformers import pipeline + +def load_artifacts(app: FastAPI): + """Load static artifacts into app.state.""" + app.state.sentiment_model = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english") + + # Update global references for access from feature views + import example_repo + example_repo._sentiment_model = app.state.sentiment_model +``` + +Access pre-loaded artifacts in your on-demand feature views: + +```python +# example_repo.py +_sentiment_model = None + +@on_demand_feature_view(...) +def sentiment_prediction(inputs: pd.DataFrame) -> pd.DataFrame: + global _sentiment_model + return _sentiment_model(inputs["text"]) +``` + +## Documentation + +For comprehensive documentation, examples, and best practices, see the [Alpha Static Artifacts Loading](../alpha-static-artifacts.md) reference guide. + +The [PyTorch NLP template](https://github.com/feast-dev/feast/tree/main/sdk/python/feast/templates/pytorch_nlp) provides a complete working example. + # Online Feature Server Permissions and Access Control ## API Endpoints and Permissions @@ -275,7 +634,11 @@ feast serve --key /path/to/key.pem --cert /path/to/cert.pem | Endpoint | Resource Type | Permission | Description | |----------------------------|---------------------------------|-------------------------------------------------------|----------------------------------------------------------------| | /get-online-features | FeatureView,OnDemandFeatureView | Read Online | Get online features from the feature store | -| /retrieve-online-documents | FeatureView | Read Online | Retrieve online documents from the feature store for RAG | +| /search | FeatureView | Read Online | Vector similarity search for RAG (embedding vector or text query) | +| /retrieve-online-documents | FeatureView | Read Online | **Deprecated.** Use `/search` instead. | +| /v1/vector_stores | FeatureView | Describe | [Alpha] List all vector stores | +| /v1/vector_stores/{id} | FeatureView | Describe | [Alpha] Get a single vector store | +| /v1/vector_stores/{id}/search | FeatureView | Read Online | [Alpha] OpenAI-compatible vector search with server-side embedding | | /push | FeatureView | Write Online, Write Offline, Write Online and Offline | Push features to the feature store (online, offline, or both) | | /write-to-online-store | FeatureView | Write Online | Write features to the online store | | /materialize | FeatureView | Write Online | Materialize features within a specified time range | diff --git a/docs/reference/feature-servers/registry-server.md b/docs/reference/feature-servers/registry-server.md index dbcf661fc86..4558a10ce63 100644 --- a/docs/reference/feature-servers/registry-server.md +++ b/docs/reference/feature-servers/registry-server.md @@ -214,6 +214,7 @@ Most endpoints support these common query parameters: - `feature` (optional): Filter feature views by feature name - `feature_service` (optional): Filter feature views by feature service name - `data_source` (optional): Filter feature views by data source name + - `updated_since` (optional): Only return feature views updated at or after this ISO-8601 UTC timestamp (e.g. `2024-01-01T00:00:00Z`) - `page` (optional): Page number for pagination - `limit` (optional): Number of items per page - `sort_by` (optional): Field to sort by @@ -223,27 +224,31 @@ Most endpoints support these common query parameters: # Basic list curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project" - + # With pagination and relationships curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&include_relationships=true&page=1&limit=5&sort_by=name" - + # Filter by entity curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&entity=user" - + # Filter by feature curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&feature=age" - + # Filter by data source curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&data_source=user_profile_source" - + # Filter by feature service curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&feature_service=user_service" - + + # Filter by last-updated timestamp + curl -H "Authorization: Bearer " \ + "http://localhost:6572/api/v1/feature_views?project=my_project&updated_since=2024-06-01T00:00:00Z" + # Multiple filters combined curl -H "Authorization: Bearer " \ "http://localhost:6572/api/v1/feature_views?project=my_project&entity=user&feature=age" @@ -1207,28 +1212,33 @@ Please refer the [page](./../../../docs/getting-started/concepts/permission.md) "name": "user_id", "description": "Primary identifier for users", "project": "project1", - "match_score": 100 + "match_score": 100, + "matched_tags": {} }, { "type": "featureView", "name": "user_features", "description": "User demographic and behavioral features", "project": "project1", - "match_score": 100 + "match_score": 100, + "matched_tags": {"team": "user_analytics"} }, { "type": "feature", "name": "user_age", "description": "Age of the user in years", "project": "project1", - "match_score": 80 + "featureView": "user_features", + "match_score": 80, + "matched_tags": {} }, { "type": "dataSource", "name": "user_analytics", "description": "Analytics data for user behavior tracking", "project": "project2", - "match_score": 80 + "match_score": 80, + "matched_tags": {"source": "user_data"} } ], "pagination": { diff --git a/docs/reference/feature-store-yaml.md b/docs/reference/feature-store-yaml.md index 01f586c047b..1aac166bd8b 100644 --- a/docs/reference/feature-store-yaml.md +++ b/docs/reference/feature-store-yaml.md @@ -25,6 +25,54 @@ online_store: * **project\_id** — Optional parameter for the datastore online store. Sets the GCP project id used by Feast, if not set Feast will use the default GCP project id in the local environment. * **project** — Defines a namespace for the entire feature store. Can be used to isolate multiple deployments in a single installation of Feast. +### feature_server + +The `feature_server` block configures the Python Feature Server when it is used +to serve online features and handle `/push` requests. This section is optional +and only applies when running the Python feature server. + +An example configuration: + +```yaml +feature_server: + type: local + metrics: # Prometheus metrics configuration. Also achievable via `feast serve --metrics`. + enabled: true # Enable Prometheus metrics server on port 8000 + resource: true # CPU / memory gauges + request: true # endpoint latency histograms & request counters + online_features: true # online feature retrieval counters + store read & ODFV transform timing + push: true # push request counters + materialization: true # materialization counters & duration histograms + freshness: true # per-feature-view freshness gauges + offline_push_batching_enabled: true # Enables batching of offline writes processed by /push. Online writes are unaffected. + offline_push_batching_batch_size: 100 # Maximum number of buffered rows before writing to the offline store. + offline_push_batching_batch_interval_seconds: 5 # Maximum time rows may remain buffered before a forced flush. +``` + +### registry + +The `registry` field can be a simple path string or an object with additional +configuration. When using the REST registry server, MCP support can be enabled: + +```yaml +registry: + registry_type: sql + path: postgresql+psycopg://feast:feast@localhost:5432/feast #pragma: allowlist secret + mcp: + enabled: true +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `registry_type` | string | `file` | Registry backend (`file`, `sql`, etc.) | +| `path` | string | — | Connection string or file path | +| `schema_mode` | string | `auto` | SQL registry only. `auto`: create tables on startup; `verify`: check tables exist, error if missing; `skip`: no DDL or verification. See [SQL Registry docs](registries/sql.md#schema-management-schema_mode). | +| `mcp.enabled` | bool | `false` | Enable MCP (Model Context Protocol) on the REST registry server | + +When `registry.mcp.enabled` is `true`, the REST registry server exposes registry +metadata (entities, feature views, feature services) as MCP tool endpoints for +LLM agents. Requires `feast[mcp]` to be installed. + ## Providers The `provider` field defines the environment in which Feast will execute data flows. As a result, it also determines the default values for other fields. diff --git a/docs/reference/mlflow.md b/docs/reference/mlflow.md new file mode 100644 index 00000000000..6522478409c --- /dev/null +++ b/docs/reference/mlflow.md @@ -0,0 +1,347 @@ +# MLflow Integration + +Feast provides **native integration** with [MLflow](https://mlflow.org/) for automatic feature lineage tracking alongside ML experiments. When enabled, every feature retrieval is logged to the active MLflow run. + +## Overview + +- **Which features did this model use?** -- auto-logged on every `get_historical_features()` / `get_online_features()` call +- **Which feature service should I use to serve this model?** -- resolved from model URI via `store.mlflow.resolve_features()` +- **Can I reproduce the exact training data?** -- entity DataFrame saved as an MLflow artifact +- **Which models break if I change a feature view?** -- reverse index via the Feast UI `/api/mlflow-feature-usage` endpoint +- **When was the feature store last updated?** -- `feast apply` and `feast materialize` logged to a separate ops experiment + +### Capabilities + +| Capability | How | +|---|---| +| Auto-log feature metadata | Tags on every retrieval inside an active MLflow run | +| Entity DataFrame archival | `entity_df.parquet` artifact for full reproducibility | +| Model registration with lineage | `feast.feature_service` tag propagated to model versions | +| Training-to-prediction linkage | `store.mlflow.load_model()` links prediction runs back to training runs | +| Model-to-feature resolution | Map any model URI back to its Feast feature service | +| Operation audit trail | `feast apply` / `feast materialize` logged to `{project}-feast-ops` | +| `store.mlflow` API | Single entry point — zero `import mlflow`, zero client objects | +| Feast UI integration | Per-feature-view usage stats and registered model associations | + +## Installation + +MLflow is an optional dependency: + +```bash +pip install feast[mlflow] +``` + +## Configuration + +Add the `mlflow` section to your `feature_store.yaml`: + +```yaml +project: my_project +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db + +mlflow: + enabled: true + tracking_uri: http://127.0.0.1:5000 # optional, falls back to MLFLOW_TRACKING_URI env var + auto_log: true # default + auto_log_entity_df: false # default + entity_df_max_rows: 100000 # default + log_operations: false # default + ops_experiment_suffix: "-feast-ops" # default +``` + +### Configuration options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | bool | `false` | Master switch for the entire integration | +| `tracking_uri` | string | *(none)* | MLflow tracking server URI. Falls back to `MLFLOW_TRACKING_URI` env var, then MLflow default (`./mlruns`) | +| `auto_log` | bool | `true` | Automatically log feature metadata on every retrieval when an active MLflow run exists | +| `auto_log_entity_df` | bool | `false` | Save the entity DataFrame as `entity_df.parquet` artifact on historical retrieval | +| `entity_df_max_rows` | int | `100000` | Skip entity DataFrame artifact upload for DataFrames exceeding this limit | +| `log_operations` | bool | `false` | Log `feast apply` and `feast materialize` to a separate MLflow experiment | +| `ops_experiment_suffix` | string | `"-feast-ops"` | Suffix appended to project name for the operations experiment | + +### Tracking URI resolution + +The tracking URI is resolved in this order: + +1. `tracking_uri` field in `feature_store.yaml` +2. `MLFLOW_TRACKING_URI` environment variable +3. MLflow's default (`./mlruns` local directory) + +This means you can omit `tracking_uri` from the YAML and set `MLFLOW_TRACKING_URI` in your environment instead, or it would be pulled from `./mlruns` automatically when both are not set. + +## What gets logged + +### Tags on retrieval runs + +When `auto_log: true` and an active MLflow run exists, each `get_historical_features()` or `get_online_features()` call records: + +| Tag | Example | Description | +|-----|---------|-------------| +| `feast.project` | `my_project` | Feast project name | +| `feast.retrieval_type` | `historical` / `online` | Type of feature retrieval | +| `feast.feature_service` | `driver_activity_v1` | Auto-resolved feature service name (if matched) | +| `feast.feature_views` | `driver_hourly_stats` | Comma-separated feature view names | +| `feast.feature_refs` | `driver_hourly_stats:conv_rate,...` | All feature references | +| `feast.entity_count` | `200` | Number of entities in the request | +| `feast.feature_count` | `5` | Number of features retrieved | + +### Metrics + +| Metric | Example | Description | +|--------|---------|-------------| +| `feast.job_submission_sec` | `0.4321` | Feature retrieval duration in seconds | + +### Artifacts + +When `auto_log_entity_df: true` and the entity DataFrame has fewer than `entity_df_max_rows` rows: + +| Artifact | Description | +|----------|-------------| +| `entity_df.parquet` | Full entity DataFrame used in the retrieval | + +When a model is logged via `store.mlflow.log_model()`: + +| Artifact | Description | +|----------|-------------| +| `feast_features.json` | JSON list of feature references the model was trained on | + +### Entity DataFrame metadata + +Regardless of `auto_log_entity_df`, the following metadata is logged when present: + +| Tag / Param | When | Description | +|-------------|------|-------------| +| `feast.entity_df_type` | Always | `dataframe`, `sql`, or `range` | +| `feast.entity_df_rows` | DataFrame input | Row count | +| `feast.entity_df_columns` | DataFrame input | Column names | +| `feast.entity_df_query` | SQL input | The SQL query string | +| `feast.start_date` / `feast.end_date` | Range-based input | Date range | + +### Operation logs + +When `log_operations: true`, `feast apply` and `feast materialize` create self-contained runs in the `{project}{ops_experiment_suffix}` experiment (default: `my_project-feast-ops`): + +**Apply runs:** + +| Tag / Metric | Example | +|--------------|---------| +| `feast.operation` | `apply` | +| `feast.project` | `my_project` | +| `feast.feature_views_changed` | `driver_hourly_stats,order_stats` | +| `feast.feature_services_changed` | `driver_activity_v1` | +| `feast.entities_changed` | `driver,restaurant` | +| `feast.apply.feature_views_count` | `2` | +| `feast.apply.feature_services_count` | `1` | +| `feast.apply.entities_count` | `2` | + +**Materialize runs:** + +| Tag / Metric | Example | +|--------------|---------| +| `feast.operation` | `materialize` / `materialize_incremental` | +| `feast.project` | `my_project` | +| `feast.materialize.feature_views` | `driver_hourly_stats` | +| `feast.materialize.start_date` | `2024-01-01T00:00:00` | +| `feast.materialize.end_date` | `2024-01-02T00:00:00` | +| `feast.materialize.duration_sec` | `12.3456` | + +## Usage + +### Automatic logging (zero code) + +With the configuration above, feature metadata is logged automatically whenever there is an active MLflow run. No explicit `import mlflow` is needed — just use `store.mlflow`: + +```python +from feast import FeatureStore + +store = FeatureStore(".") + +with store.mlflow.start_run(run_name="my_training"): + training_df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + # The run is now tagged with feast.feature_refs, feast.feature_views, etc. + + model = train(training_df) + store.mlflow.log_model(model, "model") +``` + +No extra code needed — the tags are written automatically. + +### `store.mlflow` API (recommended) + +`store.mlflow` is the primary way to interact with the Feast–MLflow integration. It provides Feast-enhanced versions of common MLflow operations, and delegates everything else to the raw `mlflow` module: + +```python +from feast import FeatureStore +from sklearn.linear_model import LogisticRegression + +store = FeatureStore(".") + +# Training +with store.mlflow.start_run(run_name="v1_training"): + df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + model = LogisticRegression().fit(X, y) + store.mlflow.log_model(model, "model") # Feast-enhanced: saves feast_features.json + train_run_id = store.mlflow.active_run_id + +# Register model (auto-tags version with feast.feature_service) +store.mlflow.register_model(f"runs:/{train_run_id}/model", "driver_model") + +# Prediction (auto-links to training run) +with store.mlflow.start_run(run_name="prediction"): + model = store.mlflow.load_model("models:/driver_model/1") + online_features = store.get_online_features( + features=store.get_feature_service("driver_activity_v1"), + entity_rows=[{"driver_id": 1001}], + ) + predictions = model.predict(...) +``` + +### `feast.mlflow` module API (alternative) + +For users who prefer a module-level import, `feast.mlflow` is a **drop-in replacement for `import mlflow`** that delegates to the same `store.mlflow` client under the hood: + +```python +import feast.mlflow +from feast import FeatureStore + +store = FeatureStore(".") # auto-registers with feast.mlflow + +with feast.mlflow.start_run(run_name="training"): + df = store.get_historical_features(...).to_df() + feast.mlflow.log_params({"lr": "0.01"}) # plain passthrough + feast.mlflow.log_metrics({"f1": 0.85}) # plain passthrough + feast.mlflow.log_model(model, "model") # Feast-enhanced +``` + +#### Store resolution + +`feast.mlflow` resolves its `FeatureStore` in this order: + +1. **Explicit `feast.mlflow.init(store)`** — if called, overrides everything +2. **Auto-registered** — the most recently created `FeatureStore` with `mlflow.enabled=true` registers itself automatically +3. **Auto-discovery** — falls back to `FeatureStore(".")` from the current directory + +In most cases, simply creating a `FeatureStore(...)` is enough — no `init()` needed. + +#### Error handling + +`feast.mlflow` raises clear errors on first use if something is misconfigured: + +| Condition | Error | +|-----------|-------| +| No `feature_store.yaml` in cwd and no store created | `RuntimeError` with guidance to call `feast.mlflow.init(store)` | +| `mlflow.enabled` is not set to `true` | `RuntimeError` with guidance to set `mlflow.enabled=true` | +| `mlflow` pip package not installed | `ImportError` with guidance to run `pip install feast[mlflow]` | + +When `mlflow.enabled` is `false` (or omitted), `store.mlflow` returns `None`, allowing callers to guard with `if store.mlflow:`. The `feast.mlflow` module raises `RuntimeError` only when you attempt to use it without an enabled store. + +### Feast-enhanced functions + +These functions add automatic Feast tagging and lineage on top of their MLflow counterparts: + +| Function | Enhancement | +|----------|-------------| +| `store.mlflow.start_run(run_name, tags)` | Auto-tags run with `feast.project` | +| `store.mlflow.log_model(model, path, flavor)` | Auto-attaches `feast_features.json` artifact | +| `store.mlflow.register_model(model_uri, name)` | Auto-tags model version with `feast.feature_service` | +| `store.mlflow.load_model(model_uri)` | Auto-tags prediction run with training lineage | + +**Supported model flavors for `log_model()`:** `sklearn`, `pytorch`, `xgboost`, `lightgbm`, `tensorflow`, `keras`, `pyfunc`. + +### Feast-only functions + +These are unique to the Feast integration and have no `mlflow` equivalent: + +| Function | Description | +|----------|-------------| +| `store.mlflow.resolve_features(model_uri)` | Resolve model URI to Feast feature service name | +| `store.mlflow.get_training_entity_df(run_id, ...)` | Recover entity DataFrame from a past MLflow run | +| `store.mlflow.log_training_dataset(df, dataset_name)` | Log a training DataFrame as an MLflow dataset input | +| `store.mlflow.active_run_id` | Current active MLflow run ID (or `None`) | +| `store.mlflow.client` | The underlying `MlflowClient` instance for advanced queries | +| `feast.mlflow.init(store)` | Explicitly bind `feast.mlflow` module to a `FeatureStore` (optional) | + +### Passthrough behavior + +The `feast.mlflow` module delegates any attribute not listed above to the raw `mlflow` module. This means you can use `feast.mlflow` as a drop-in replacement for `import mlflow`: + +```python +feast.mlflow.log_params(params) # passes through to mlflow.log_params +feast.mlflow.log_metrics(metrics) +feast.mlflow.set_tag("env", "staging") +feast.mlflow.MlflowClient() +``` + +`store.mlflow` does **not** have this passthrough — it only exposes the Feast-enhanced and Feast-only methods listed above. To access raw `mlflow` functions from `store.mlflow`, use the escape hatches: + +```python +store.mlflow.client.log_param(run_id, "lr", "0.01") # via MlflowClient instance +store.mlflow.mlflow.log_params(params) # via raw mlflow module +``` + +### Resolve a model back to its feature service + +```python +from feast import FeatureStore + +store = FeatureStore(".") +fs_name = store.mlflow.resolve_features("models:/driver_model/1") +# Returns: "driver_activity_v1" +``` + +Resolution order: +1. Model version tag `feast.feature_service` (set by `register_model()`) +2. Training run tag `feast.feature_service` (set by auto-logging) + +### Reproduce training from a past run + +```python +from feast import FeatureStore + +store = FeatureStore(".") + +entity_df = store.mlflow.get_training_entity_df(run_id="abc123") + +with store.mlflow.start_run(run_name="retrain_v2"): + new_df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + model = train(new_df) + store.mlflow.log_model(model, "model") +``` + +This requires `auto_log_entity_df: true` to have been enabled when the original run was recorded. + +## Feast UI integration + +The Feast UI server exposes three API endpoints that aggregate data from MLflow: + +| Endpoint | Description | +|----------|-------------| +| `/api/mlflow-runs` | All Feast-tagged MLflow runs with linked registered models | +| `/api/mlflow-feature-usage` | Per-feature-view usage stats (run count, last used, associated models) | +| `/api/mlflow-feature-models` | Reverse index of feature refs to registered models | + +The feature view detail page in the Feast UI displays: +- **MLflow Training Runs** count and **Last Used** date in the header stats +- An **MLflow Usage** panel showing training run count, relative last-used time, and a table of registered models that depend on the feature view + +Start the Feast UI with: + +```bash +feast ui --host 127.0.0.1 --port 8888 +``` diff --git a/docs/reference/offline-stores/README.md b/docs/reference/offline-stores/README.md index b5e2bccbdd1..1c0d24c8d07 100644 --- a/docs/reference/offline-stores/README.md +++ b/docs/reference/offline-stores/README.md @@ -49,3 +49,27 @@ Please see [Offline Store](../../getting-started/components/offline-store.md) fo {% content-ref url="ray.md" %} [ray.md](ray.md) {% endcontent-ref %} + +{% content-ref url="oracle.md" %} +[oracle.md](oracle.md) +{% endcontent-ref %} + +{% content-ref url="athena.md" %} +[athena.md](athena.md) +{% endcontent-ref %} + +{% content-ref url="clickhouse.md" %} +[clickhouse.md](clickhouse.md) +{% endcontent-ref %} + +{% content-ref url="mongodb.md" %} +[mongodb.md](mongodb.md) +{% endcontent-ref %} + +{% content-ref url="remote-offline-store.md" %} +[remote-offline-store.md](remote-offline-store.md) +{% endcontent-ref %} + +{% content-ref url="hybrid.md" %} +[hybrid.md](hybrid.md) +{% endcontent-ref %} diff --git a/docs/reference/offline-stores/athena.md b/docs/reference/offline-stores/athena.md new file mode 100644 index 00000000000..3fbe77681ee --- /dev/null +++ b/docs/reference/offline-stores/athena.md @@ -0,0 +1,66 @@ +# Athena offline store (contrib) + +## Description + +The Athena offline store provides support for reading [AthenaSources](../data-sources/athena.md). +* Entity dataframes can be provided as a SQL query or can be provided as a Pandas dataframe. + +## Disclaimer + +The Athena offline store does not achieve full test coverage. +Please do not assume complete stability. + +## Getting started +In order to use this offline store, you'll need to run `pip install 'feast[aws]'`. + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: athena + data_source: AwsDataCatalog + region: us-east-1 + database: my_database + workgroup: primary +online_store: + path: data/online_store.db +``` +{% endcode %} + +The full set of configuration options is available in [AthenaOfflineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.athena_offline_store.athena.AthenaOfflineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by offline stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Athena offline store. + +| | Athena | +| :----------------------------------------------------------------- |:-------| +| `get_historical_features` (point-in-time correct join) | yes | +| `pull_latest_from_table_or_query` (retrieve latest feature values) | yes | +| `pull_all_from_table_or_query` (retrieve a saved dataset) | yes | +| `offline_write_batch` (persist dataframes to offline store) | no | +| `write_logged_features` (persist logged features to offline store) | yes | + +Below is a matrix indicating which functionality is supported by `AthenaRetrievalJob`. + +| | Athena | +| ----------------------------------------------------- |--------| +| export to dataframe | yes | +| export to arrow table | yes | +| export to arrow batches | no | +| export to SQL | yes | +| export to data lake (S3, GCS, etc.) | no | +| export to data warehouse | no | +| export as Spark dataframe | no | +| local execution of Python-based on-demand transforms | yes | +| remote execution of Python-based on-demand transforms | no | +| persist results in the offline store | yes | +| preview the query plan before execution | yes | +| read partitioned data | yes | + +To compare this set of functionality against other offline stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/offline-stores/mongodb.md b/docs/reference/offline-stores/mongodb.md new file mode 100644 index 00000000000..a41d43ca676 --- /dev/null +++ b/docs/reference/offline-stores/mongodb.md @@ -0,0 +1,101 @@ +# MongoDB offline store (contrib) + +## Description + +The MongoDB offline store provides support for reading [MongoDBSource](../data-sources/mongodb.md). + +## Getting started + +In order to use this offline store, you'll need to run `pip install 'feast[mongodb]'`. + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb.MongoDBOfflineStore + connection_string: "mongodb+srv://user:pass@cluster.mongodb.net" # pragma: allowlist secret + database: feast + collection: feature_history +online_store: + type: mongodb + connection_string: "mongodb+srv://user:pass@cluster.mongodb.net" # pragma: allowlist secret + database_name: feast_online_store + collection_suffix: latest + client_kwargs: {} +``` +{% endcode %} + +The full set of configuration options is available in [MongoDBOfflineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb.MongoDBOfflineStoreConfig). + +## Data Model + +The offline store uses a single shared collection (by default `feature_history`) that stores append-only historical feature rows for all feature views. Each document represents one observation of one entity for one FeatureView at a specific event timestamp: + +```json +{ + "entity_id": "Binary(...)", + "feature_view": "driver_stats", + "event_timestamp": "ISODate(2024-01-15T12:00:00Z)", + "created_at": "ISODate(2024-01-15T12:01:00Z)", + "features": { + "conv_rate": 0.72, + "acc_rate": 0.91, + "avg_daily_trips": 14 + } +} +``` + +Key properties: + +* **Append-only**: Historical data is treated as immutable; corrections are written as new rows with newer `created_at` timestamps rather than in-place updates. +* **Time-series friendly**: `event_timestamp` represents when the feature value was observed; `created_at` is used as a tie-breaker when multiple observations share the same event timestamp. +* **Feature grouping by FeatureView**: `feature_view` identifies which FeatureView the row belongs to, so a single collection can host multiple FVs. + +A single compound index supports all major query patterns: + +``` +(entity_id ASC, feature_view ASC, event_timestamp DESC, created_at DESC) +``` + +This index enables efficient range scans over entities and feature views, while ensuring that the most recent observation per `(entity_id, feature_view)` is seen first during aggregation. The index is created lazily on first use and cached per connection string. + +## Key Optimizations + +* **Scoring vs. training paths**: When each entity appears only once in `entity_df` (scoring/inference — one feature lookup per entity), server-side `$group $first` efficiently returns the single latest value per entity. When the same entity appears at multiple timestamps (training — building a dataset with many historical snapshots per entity), the store retrieves all candidate rows and uses `pd.merge_asof` to select the correct point-in-time value for each request timestamp. +* **Two-level chunking**: `CHUNK_SIZE` (50,000 rows) controls the size of intermediate DataFrames in memory; `MONGO_BATCH_SIZE` (10,000 entity IDs) limits the query size sent to MongoDB. + +## Functionality Matrix + +The set of functionality supported by offline stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the MongoDB offline store. + +| | MongoDB | +| :----------------------------------------------------------------- | :------ | +| `get_historical_features` (point-in-time correct join) | yes | +| `pull_latest_from_table_or_query` (retrieve latest feature values) | yes | +| `pull_all_from_table_or_query` (retrieve a saved dataset) | yes | +| `offline_write_batch` (persist dataframes to offline store) | yes | +| `write_logged_features` (persist logged features to offline store) | no | + +Below is a matrix indicating which functionality is supported by `MongoDBRetrievalJob`. + +| | MongoDB | +| ----------------------------------------------------- | ------- | +| export to dataframe | yes | +| export to arrow table | yes | +| export to arrow batches | no | +| export to SQL | no | +| export to data lake (S3, GCS, etc.) | no | +| export to data warehouse | no | +| export as Spark dataframe | no | +| local execution of Python-based on-demand transforms | yes | +| remote execution of Python-based on-demand transforms | no | +| persist results in the offline store | yes | +| preview the query plan before execution | no | +| read partitioned data | no | + +To compare this set of functionality against other offline stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/offline-stores/oracle.md b/docs/reference/offline-stores/oracle.md new file mode 100644 index 00000000000..12f5b761ce6 --- /dev/null +++ b/docs/reference/offline-stores/oracle.md @@ -0,0 +1,109 @@ +# Oracle offline store (contrib) + +## Description + +The Oracle offline store provides support for reading [OracleSources](../data-sources/oracle.md). +* Entity dataframes can be provided as a SQL query or as a Pandas dataframe. +* Uses the [ibis](https://ibis-project.org/) Oracle backend (`ibis.oracle`) for all database interactions. +* Only one of `service_name`, `sid`, or `dsn` may be set in the configuration. + +## Disclaimer + +The Oracle offline store does not achieve full test coverage. +Please do not assume complete stability. + +## Getting started + +Install the Oracle extras: + +```bash +pip install 'feast[oracle]' +``` + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: oracle + host: DB_HOST + port: 1521 + user: DB_USERNAME + password: DB_PASSWORD + service_name: ORCL +online_store: + path: data/online_store.db +``` +{% endcode %} + +Connection can alternatively use `sid` or `dsn` instead of `service_name`: + +```yaml +# Using SID +offline_store: + type: oracle + host: DB_HOST + port: 1521 + user: DB_USERNAME + password: DB_PASSWORD + sid: ORCL + +# Using DSN +offline_store: + type: oracle + host: DB_HOST + port: 1521 + user: DB_USERNAME + password: DB_PASSWORD + dsn: "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=DB_HOST)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))" +``` + +### Configuration reference + +| Parameter | Required | Default | Description | +| :------------- | :------- | :---------- | :------------------------------------------------------- | +| `type` | yes | — | Must be set to `oracle` | +| `user` | yes | — | Oracle database user | +| `password` | yes | — | Oracle database password | +| `host` | no | `localhost` | Oracle database host | +| `port` | no | `1521` | Oracle database port | +| `service_name` | no | — | Oracle service name (mutually exclusive with sid and dsn) | +| `sid` | no | — | Oracle SID (mutually exclusive with service_name and dsn) | +| `database` | no | — | Oracle database name | +| `dsn` | no | — | Oracle DSN string (mutually exclusive with service_name and sid) | + +## Functionality Matrix + +The set of functionality supported by offline stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Oracle offline store. + +| | Oracle | +| :----------------------------------------------------------------- | :----- | +| `get_historical_features` (point-in-time correct join) | yes | +| `pull_latest_from_table_or_query` (retrieve latest feature values) | yes | +| `pull_all_from_table_or_query` (retrieve a saved dataset) | yes | +| `offline_write_batch` (persist dataframes to offline store) | yes | +| `write_logged_features` (persist logged features to offline store) | yes | + +Below is a matrix indicating which functionality is supported by `OracleRetrievalJob`. + +| | Oracle | +| ----------------------------------------------------- | ------ | +| export to dataframe | yes | +| export to arrow table | yes | +| export to arrow batches | no | +| export to SQL | no | +| export to data lake (S3, GCS, etc.) | no | +| export to data warehouse | no | +| export as Spark dataframe | no | +| local execution of Python-based on-demand transforms | yes | +| remote execution of Python-based on-demand transforms | no | +| persist results in the offline store | yes | +| preview the query plan before execution | no | +| read partitioned data | no | + +To compare this set of functionality against other offline stores, please see the full [functionality matrix](overview.md#functionality-matrix). + diff --git a/docs/reference/offline-stores/postgres.md b/docs/reference/offline-stores/postgres.md index 321ddcf25e7..7e9f112b3b3 100644 --- a/docs/reference/offline-stores/postgres.md +++ b/docs/reference/offline-stores/postgres.md @@ -38,7 +38,7 @@ online_store: ``` {% endcode %} -Note that `sslmode`, `sslkey_path`, `sslcert_path`, and `sslrootcert_path` are optional parameters. +Note that `sslmode` defaults to `require`, which encrypts the connection without certificate verification. To disable SSL (e.g. for local development), set `sslmode: disable`. For certificate verification, set `sslmode` to `verify-ca` or `verify-full` and provide the corresponding `sslrootcert_path` (and optionally `sslcert_path` and `sslkey_path` for mutual TLS). The full set of configuration options is available in [PostgreSQLOfflineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.postgres_offline_store.postgres.PostgreSQLOfflineStoreConfig). Additionally, a new optional parameter `entity_select_mode` was added to tell how Postgres should load the entity data. By default(`temp_table`), a temporary table is created and the entity data frame or sql is loaded into that table. A new value of `embed_query` was added to allow directly loading the SQL query into a CTE, providing improved performance and skipping the need to CREATE and DROP the temporary table. diff --git a/docs/reference/offline-stores/ray.md b/docs/reference/offline-stores/ray.md index 89040f05c94..a9d8cd7ef08 100644 --- a/docs/reference/offline-stores/ray.md +++ b/docs/reference/offline-stores/ray.md @@ -27,10 +27,11 @@ The template provides a complete working example with sample datasets and demons The Ray offline store provides: - Ray-based data reading from file sources (Parquet, CSV, etc.) - Support for local, remote, and KubeRay (Kubernetes-managed) clusters -- Integration with various storage backends (local files, S3, GCS, HDFS) +- Integration with various storage backends (local files, S3, GCS, HDFS, Azure Blob) - Efficient data filtering and column selection - Timestamp-based data processing with timezone awareness - Enterprise-ready KubeRay cluster support via CodeFlare SDK +- **GPU support**: schedule worker tasks on GPU nodes via `num_gpus` config (all modes including KubeRay) ## Functionality Matrix @@ -246,6 +247,9 @@ batch_engine: | `max_parallelism_multiplier` | int | 2 | Parallelism as multiple of CPU cores | | `target_partition_size_mb` | int | 64 | Target partition size (MB) | | `window_size_for_joins` | string | "1H" | Time window for distributed joins | +| `num_gpus` | float | None | GPUs per worker task. Supported in all modes. See [Worker Resource Scheduling](../compute-engine/ray.md#worker-resource-scheduling). | +| `gpu_batch_format` | string | `"pandas"` | Batch format for `map_batches` when `num_gpus` is set (`"numpy"` or `"pyarrow"` for GPU-native libs). | +| `worker_task_options` | dict | None | Arbitrary Ray `.options()` kwargs (num_cpus, memory, accelerator_type, resources, runtime_env, …). See [Worker Resource Scheduling](../compute-engine/ray.md#worker-resource-scheduling) for the full reference. | #### Mode Detection Precedence @@ -463,6 +467,11 @@ job.persist(gcs_storage, allow_overwrite=True) # HDFS hdfs_storage = SavedDatasetFileStorage(path="hdfs://namenode:8020/datasets/driver_features.parquet") job.persist(hdfs_storage, allow_overwrite=True) + +# Azure Blob Storage / Azure Data Lake Storage Gen2 +# By setting AZURE_STORAGE_ANON=False it uses DefaultAzureCredential +az_storage = SavedDatasetFileStorage(path="abfss://container@stc_account.dfs.core.windows.net/datasets/driver_features.parquet") +job.persist(az_storage, allow_overwrite=True) ``` ### Using Ray Cluster @@ -537,6 +546,12 @@ python your_feast_script.py - Secure communication between client and Ray cluster - Automatic cluster discovery +### GPU Support + +The Ray offline store supports GPU scheduling via the `num_gpus` and `gpu_batch_format` config options. This works across all execution modes (local, remote, and KubeRay). + +For full configuration details, examples, and KubeRay GPU setup, see the [Ray Compute Engine GPU Support](../compute-engine/ray.md#gpu-support) section. + ### Data Source Validation The Ray offline store validates data sources to ensure compatibility: @@ -552,14 +567,45 @@ except Exception as e: print(f"Data source validation failed: {e}") ``` +## Data Sources + +[`RaySource`](../data-sources/ray.md) is the recommended data source for the +Ray offline store. It is a pure-metadata descriptor that tells Feast how to +load a Ray Dataset from any source Ray Data supports — Parquet, CSV, JSON, +HuggingFace datasets, MongoDB, binary files, images, TFRecords, WebDataset, +SQL, and more. + +```python +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +# Load directly from the HuggingFace Hub +cheque_source = RaySource( + name="cheque_images_hf", + reader_type="huggingface", + reader_options={ + "dataset_name": "cheques_sample_data", + "split": "train", + }, + timestamp_field="event_timestamp", +) +``` + +See the [RaySource reference](../data-sources/ray.md) for a full list of +`reader_type` values and configuration options. + +> **Note:** `FileSource` (Parquet) remains supported for backward compatibility +> but `RaySource(reader_type="parquet")` is preferred for new projects. + ## Limitations -The Ray offline store has the following limitations: +The Ray offline store has one known limitation: -1. **File Sources Only**: Currently supports only `FileSource` data sources -2. **No Direct SQL**: Does not support SQL query interfaces -3. **No Online Writes**: Cannot write directly to online stores -4. **No Complex Transformations**: The Ray offline store focuses on data I/O operations. For complex feature transformations (aggregations, joins, custom UDFs), use the [Ray Compute Engine](../compute-engine/ray.md) instead +* **`online_write_batch` not implemented**: The `OfflineStore.online_write_batch()` interface + is not supported by the Ray offline store. This does **not** affect materialization — + `feast materialize` writes to the online store correctly via the + [Ray Compute Engine](../compute-engine/ray.md). The restriction only applies to callers + that invoke `online_write_batch` on the offline store object directly, which is an + uncommon pattern outside of custom tooling. ## Integration with Ray Compute Engine diff --git a/docs/reference/offline-stores/spark.md b/docs/reference/offline-stores/spark.md index 2e2facba64a..7f2d4094785 100644 --- a/docs/reference/offline-stores/spark.md +++ b/docs/reference/offline-stores/spark.md @@ -32,9 +32,13 @@ offline_store: spark.sql.session.timeZone: "UTC" spark.sql.execution.arrow.fallback.enabled: "true" spark.sql.execution.arrow.pyspark.enabled: "true" + # Optional: spill large materializations to the staging location instead of collecting in the driver + staging_location: "s3://my-bucket/tmp/feast" online_store: path: data/online_store.db ``` + +> The `staging_location` can point to object storage (like S3, GCS, or Azure blobs) or a local filesystem directory (e.g., `/tmp/feast/staging`) to spill large materialization outputs before reading them back into Feast. {% endcode %} The full set of configuration options is available in [SparkOfflineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.offline_stores.contrib.spark_offline_store.spark.SparkOfflineStoreConfig). @@ -60,7 +64,7 @@ Below is a matrix indicating which functionality is supported by `SparkRetrieval | export to arrow table | yes | | export to arrow batches | no | | export to SQL | no | -| export to data lake (S3, GCS, etc.) | no | +| export to data lake (S3, GCS, etc.) | yes | | export to data warehouse | no | | export as Spark dataframe | yes | | local execution of Python-based on-demand transforms | no | diff --git a/docs/reference/online-stores/README.md b/docs/reference/online-stores/README.md index 5df4710434c..257864b9b30 100644 --- a/docs/reference/online-stores/README.md +++ b/docs/reference/online-stores/README.md @@ -22,8 +22,8 @@ Please see [Online Store](../../getting-started/components/online-store.md) for [dragonfly.md](dragonfly.md) {% endcontent-ref %} -{% content-ref url="ikv.md" %} -[ikv.md](ikv.md) +{% content-ref url="valkey.md" %} +[valkey.md](valkey.md) {% endcontent-ref %} {% content-ref url="datastore.md" %} @@ -35,13 +35,17 @@ Please see [Online Store](../../getting-started/components/online-store.md) for {% endcontent-ref %} {% content-ref url="bigtable.md" %} -[bigtable.md](mysql.md) +[bigtable.md](bigtable.md) {% endcontent-ref %} {% content-ref url="postgres.md" %} [postgres.md](postgres.md) {% endcontent-ref %} +{% content-ref url="hbase.md" %} +[hbase.md](hbase.md) +{% endcontent-ref %} + {% content-ref url="cassandra.md" %} [cassandra.md](cassandra.md) {% endcontent-ref %} @@ -54,6 +58,14 @@ Please see [Online Store](../../getting-started/components/online-store.md) for [mysql.md](mysql.md) {% endcontent-ref %} +{% content-ref url="mongodb.md" %} +[mongodb.md](mongodb.md) +{% endcontent-ref %} + +{% content-ref url="aerospike.md" %} +[aerospike.md](aerospike.md) +{% endcontent-ref %} + {% content-ref url="hazelcast.md" %} [hazelcast.md](hazelcast.md) {% endcontent-ref %} @@ -69,3 +81,23 @@ Please see [Online Store](../../getting-started/components/online-store.md) for {% content-ref url="singlestore.md" %} [singlestore.md](singlestore.md) {% endcontent-ref %} + +{% content-ref url="elasticsearch.md" %} +[elasticsearch.md](elasticsearch.md) +{% endcontent-ref %} + +{% content-ref url="qdrant.md" %} +[qdrant.md](qdrant.md) +{% endcontent-ref %} + +{% content-ref url="milvus.md" %} +[milvus.md](milvus.md) +{% endcontent-ref %} + +{% content-ref url="faiss.md" %} +[faiss.md](faiss.md) +{% endcontent-ref %} + +{% content-ref url="hybrid.md" %} +[hybrid.md](hybrid.md) +{% endcontent-ref %} diff --git a/docs/reference/online-stores/aerospike.md b/docs/reference/online-stores/aerospike.md new file mode 100644 index 00000000000..e5a9754796b --- /dev/null +++ b/docs/reference/online-stores/aerospike.md @@ -0,0 +1,389 @@ +# Aerospike online store (Preview) + +## Description + +The [Aerospike](https://aerospike.com/) online store provides support for materializing feature values into an Aerospike cluster for serving online features. + +{% hint style="warning" %} +The Aerospike online store is currently in **preview**. Some functionality may be unstable, and breaking changes may occur in future releases. +{% endhint %} + +## Features + +* Supports both synchronous and asynchronous read/write paths (`online_read` / `online_read_async`, `online_write_batch` / `online_write_batch_async`). Async methods wrap the blocking client in `run_in_executor`, keeping the event loop responsive in feature-server workloads. +* Partial, server-side upserts via Aerospike Map CDT operations — writing one feature view never clobbers another feature view stored on the same entity. +* Record-level TTL controlled by a single `ttl_seconds` config option (honours the namespace default, a "never expire" sentinel, or an explicit number of seconds). +* Per-feature-view **namespace overrides** and **set overrides** — pin individual feature views to RAM-only or SSD-backed namespaces, or isolate one view in its own set, without splitting projects. +* **Prewriting hook** — a configurable, import-string-resolved callable applied to every write batch for cross-cutting concerns like PII masking, application-side encryption, or value coercion. +* Authentication and TLS options for Aerospike Enterprise Edition passed straight through to the Aerospike Python client. +* `client_kwargs` escape hatch for any advanced client-config field not surfaced on `AerospikeOnlineStoreConfig`. +* Baseline: Aerospike Server **≥ 6.0** (uses batch-write / batch-operate APIs). The store has been developed against CE 8.x. + +## Getting started + +Install the Aerospike extra (alongside the dependency for the offline store of choice): + +```bash +pip install 'feast[aerospike]' +``` + +You can start from any of the standard templates (e.g. `feast init -t local` or `feast init -t aws`) and then swap in Aerospike as the online store as shown below. + +## Examples + +### Basic configuration — local Aerospike CE + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["127.0.0.1", 3000] + namespace: feast +``` +{% endcode %} + +### Multi-node cluster + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike-1.internal", 3000] + - ["aerospike-2.internal", 3000] + - ["aerospike-3.internal", 3000] + namespace: feast + ttl_seconds: 86400 # 24h record-level TTL + read_timeout_ms: 150 # hard deadline for a single-record get + write_timeout_ms: 300 # hard deadline for a single-record put/operate + batch_total_timeout_ms: 500 # hard deadline for online_read / online_write_batch + batch_max_records: 1000 # chunk size for batch_write / batch_operate + socket_timeout_ms: 50 # per-attempt deadline so max_retries can fire + max_retries: 2 +``` +{% endcode %} + +> **Timeout semantics.** The Aerospike client distinguishes per-attempt +> (`socket_timeout`) from total (`total_timeout`) deadlines. `*_timeout_ms` map +> to `total_timeout` — the overall budget for a call including retries. Set +> `socket_timeout_ms` as well so each individual attempt has its own (shorter) +> deadline; without it, `max_retries` effectively never fires because the +> first attempt is allowed to consume the entire total deadline. + +> **Batch chunking.** `online_read` and `online_write_batch` split large +> requests into chunks of at most `batch_max_records` (default `1000`). +> Aerospike enforces a per-node batch limit via the server `batch-max-requests` +> setting (historically `5000`). Lower `batch_max_records` if your cluster cap +> is tighter; raise it only when the server limit and client timeouts allow. + +### Aerospike Enterprise with authentication + +> Requires Aerospike Enterprise Edition. The Community Edition server has no built-in user/security model and will reject these config keys. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike.internal", 3000] + namespace: feast + user: feast_user + password: ${AEROSPIKE_PASSWORD} # pragma: allowlist secret + auth_mode: internal # internal | external | pki +``` +{% endcode %} + +### Aerospike Enterprise with TLS + +> Requires Aerospike Enterprise Edition. The Community Edition server does not implement TLS, so `tls` config is effective only against EE clusters. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike-1.internal", 4333, "aerospike-tls"] + namespace: feast + tls: + enable: true + cafile: /etc/aerospike/certs/ca.pem + certfile: /etc/aerospike/certs/client.pem + keyfile: /etc/aerospike/certs/client.key +``` +{% endcode %} + +### Per-feature-view namespace and set overrides + +Two `Dict[str, str]` config fields — `namespace_overrides` and `set_overrides` — let you place individual feature views on a different Aerospike namespace or set without splitting your project across stores. Anything not listed in either map falls back to the store-level default (`namespace` / `set_name_template`). + +Common reasons to reach for these: + +* A **hot, latency-sensitive view** belongs on a RAM-only namespace; a **wide, cold view** belongs on an SSD-backed namespace. Same project, different storage tiers. +* You want `feast apply` deletions or `truncate` on one feature view to be O(1) without scanning records of the others — give that view its own set. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike.internal", 3000] + namespace: feast # default namespace + set_name_template: "{project}_{collection_suffix}" + namespace_overrides: + driver_realtime_stats: feast_ram # in-memory namespace + driver_history_lookup: feast_ssd # device-backed namespace + set_overrides: + isolated_view: my_feature_repo_isolated +``` +{% endcode %} + +> **Tradeoffs.** +> +> * Every namespace listed in `namespace_overrides` MUST already exist on the cluster — Aerospike cannot create namespaces at runtime, and a missing namespace surfaces as an opaque `AEROSPIKE_ERR_PARAM` on the first read or write. +> * Putting feature views on different sets means a multi-feature-view read for the same entity becomes one Aerospike round trip per set, not one round trip total. Only opt in when the operational isolation is worth that cost. Reads that touch a single feature view are unaffected. +> * Admin operations honour the overrides automatically: `update()` (called by `feast apply`) groups dropped feature views by their resolved `(namespace, set)` and issues one background scan per group; `teardown()` truncates every unique `(namespace, set)` pair the project may have written to (including the store-level default). + +### Prewriting hooks + +`prewriting_hook` is the import path of a callable that is invoked once per `online_write_batch` call, receives the rows about to be written, and returns the rows that actually go on the wire. Use it for cross-cutting write-side concerns that you don't want sprinkled through every materialization job — PII masking, application-side encryption, dual-write fan-out, value coercion, etc. + +Hooks are referenced by import string (rather than as a Python `Callable` value) so the config survives YAML/JSON serialisation and remote-feature-server transport. The resolved callable is cached on the store instance, so import cost is paid once per store lifetime. + +**Hook signature:** + +```python +def hook( + config: RepoConfig, + table: FeatureView, + data: list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + datetime | None, + ] + ], +) -> list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + datetime | None, + ] +]: + ... +``` + +The hook MUST return a row list with the same schema as its input. Returning `[]` short-circuits the write — same path as an empty input, no wire call is issued. Hooks that raise will fail the whole batch; there is no per-row fallback. + +**1. Drop a hook function in your project.** Any module on the `PYTHONPATH` of every process that writes through Feast will do (the materialization workers, the registry CLI host, and the feature server, if you run one). + +{% code title="my_feature_repo/hooks.py" %} +```python +"""Prewriting hooks for the Aerospike online store.""" +from __future__ import annotations + +import hashlib +import os +from datetime import datetime +from typing import Optional + +from feast import FeatureView +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import RepoConfig + +# Names of features that must never reach the online store as plaintext. +# Matched by exact feature name; tweak to your project's conventions. +_SENSITIVE_FEATURES = {"email", "phone_number", "ssn"} + + +def hash_pii_string_features( + config: RepoConfig, + table: FeatureView, + data: list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + Optional[datetime], + ] + ], +) -> list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + Optional[datetime], + ] +]: + """Replace any sensitive string feature with a salted SHA-256 hex digest. + + The hash is deterministic (same input → same digest) so downstream lookups + that hash the candidate value the same way still hit. ``FEAST_PII_SALT`` + must be set on every process that materialises features; an unset salt + raises rather than silently falling back to plaintext. + """ + salt = os.environ.get("FEAST_PII_SALT") + if salt is None: + raise RuntimeError( + "FEAST_PII_SALT is not set; refusing to write feature batches " + "without a configured PII salt." + ) + salt_bytes = salt.encode("utf-8") + + def _digest(plaintext: str) -> str: + h = hashlib.sha256() + h.update(salt_bytes) + h.update(plaintext.encode("utf-8")) + return h.hexdigest() + + transformed: list[ + tuple[ + EntityKeyProto, + dict[str, ValueProto], + datetime, + Optional[datetime], + ] + ] = [] + for entity_key, values, event_ts, created_ts in data: + new_values = dict(values) + for feature_name in _SENSITIVE_FEATURES.intersection(new_values): + v = new_values[feature_name] + if v.HasField("string_val") and v.string_val: + new_values[feature_name] = ValueProto(string_val=_digest(v.string_val)) + transformed.append((entity_key, new_values, event_ts, created_ts)) + return transformed +``` +{% endcode %} + +**2. Reference the hook from `feature_store.yaml`:** + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: aerospike + hosts: + - ["aerospike.internal", 3000] + namespace: feast + prewriting_hook: my_feature_repo.hooks.hash_pii_string_features +``` +{% endcode %} + +> **Operational notes.** +> +> * The hook is **only invoked on the write path**; reads pass through the store untouched. If your hook is one-way (e.g. hashing) you have to apply the same transformation to the candidate value at read time yourself. +> * Hooks run inside the same process as the writer — they're not RPCs and not sandboxed. They can read environment variables, open files, call out to KMS, etc. Treat them as part of your trusted code base. +> * A misconfigured `prewriting_hook` (bad import path, missing function, non-callable target) raises `ValueError` / `TypeError` on the *first* `online_write_batch` call, not on store construction. Add a smoke test that writes one row at deploy time so misconfigurations surface before a real batch. + +The full set of configuration options is available in [`AerospikeOnlineStoreConfig`](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.aerospike_online_store.aerospike.AerospikeOnlineStoreConfig). + +## Data Model + +The Aerospike online store uses a **single set per project** with entity-key collocation. Features from multiple feature views for the same entity are stored together on a single Aerospike record, analogous to the MongoDB online store's "one document per entity" layout. + +| Aerospike concept | Feast mapping | +| :---------------- | :---------------------------------------------------------------------------- | +| Namespace | `online_store.namespace` (must be pre-configured on the cluster); per-feature-view override via `online_store.namespace_overrides` | +| Set | `online_store.set_name_template` → `"{project}_{collection_suffix}"` by default; per-feature-view override via `online_store.set_overrides` | +| Key | `serialize_entity_key(entity_key)` as `bytearray` user key | +| Bin `features` | Map CDT keyed by feature-view name, each value a map of `feature → native` | +| Bin `event_ts` | Map CDT keyed by feature-view name, each value an int64 epoch-ms timestamp | +| Bin `created_ts` | Top-level int64 epoch-ms timestamp (last `feast materialize`) | + +### Example record + +For a single entity carrying features from two feature views (`driver_stats` and `pricing`): + +```text +key: (ns="feast", set="my_feature_repo_latest", user_key=) +bins: + features: + driver_stats: + rating: 4.91 + trips_last_7d: 132 + pricing: + surge_multiplier: 1.2 + event_ts: + driver_stats: 1737374400000 # 2025-01-20T12:00:00Z + pricing: 1737447000000 # 2025-01-21T08:30:00Z + created_ts: 1737460805000 # 2025-01-21T12:00:05Z +``` + +### Key design decisions + +* **Record per entity, bin per concept.** `features` and `event_ts` are Aerospike Map CDT bins, not dynamic bins, which keeps the store within the 15-byte Aerospike bin-name limit regardless of how many feature views a project has. +* **Partial upserts via Map CDT ops.** Writes use `batch_write` with `map_put_items("features", {: {...}})` and `map_put("event_ts", , )`. Concurrent writes to different feature views on the same entity never clobber each other — each write mutates only its own map keys. +* **Entity-key bytes as the Aerospike user key.** Feast's `serialize_entity_key` output is passed as a `bytearray` user key (not `bytes` — the Python client hashes only the first byte of `bytes` keys, which would collapse distinct entities). +* **Timestamps as int64 epoch milliseconds.** Aerospike has no native datetime type; tz-naive timestamps are treated as UTC per the `OnlineStore` contract. + +### TTL and expiry + +`ttl_seconds` is written as record-level metadata on every `online_write_batch` call: + +| `ttl_seconds` | Aerospike TTL | Effect | +| :------------ | :-------------------------------- | :---------------------------------------------------------- | +| not set / `null` | `TTL_NAMESPACE_DEFAULT` | Record inherits the namespace's configured `default-ttl`. | +| `0` | `TTL_NEVER_EXPIRE` | Record is kept until explicitly deleted. | +| `>0` | that many seconds | Record is evicted by the server's `nsup` thread. | + +There is no per-feature-view TTL override in this version — the setting is applied uniformly for every write made by the online store. + +### Indexes + +No secondary indexes are created. All access goes through the primary key, which is the serialized entity key. + +## Async support + +Async read/write are provided by running the Aerospike Python client's blocking calls on the default thread-pool executor (`loop.run_in_executor`). The underlying C client releases the GIL during network I/O, so `await store.online_read_async(...)` keeps the event loop responsive. A native asyncio Aerospike client is not currently used. + +Both sync and async methods are fully supported: + +* `online_read` / `online_read_async` +* `online_write_batch` / `online_write_batch_async` +* `initialize` / `close` — `initialize(config)` eagerly opens the connection so feature servers pay the TCP/handshake cost at startup; `close()` releases the cached client. + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Aerospike online store. + +| | Aerospike | +| :-------------------------------------------------------- | :-------- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | yes | +| support for ttl (time to live) at retrieval | yes | +| support for deleting expired data | yes | +| collocated by feature view | no | +| collocated by feature service | no | +| collocated by entity key | yes | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/cassandra.md b/docs/reference/online-stores/cassandra.md index 198f15ca47f..5d95e526421 100644 --- a/docs/reference/online-stores/cassandra.md +++ b/docs/reference/online-stores/cassandra.md @@ -37,6 +37,51 @@ online_store: ``` {% endcode %} +### Example (Cassandra — multi-DC) + +Use `datacenters` instead of `hosts` when your cluster spans multiple datacenters. +Each entry gets a named Cassandra **execution profile** keyed by its `name` field, +enabling per-DC routing. The default profile is determined by `load_balancing.local_dc` +(or the first datacenter entry when `load_balancing` is absent). Use the optional +`routing` block to direct reads and writes to specific datacenters. The keyspace must +already exist; Feast does not create it automatically. + +`datacenters` is mutually exclusive with `hosts` and `secure_bundle_path`. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: cassandra + keyspace: KeyspaceName + datacenters: + - name: dc1 + hosts: + - 192.168.1.1 + - 192.168.1.2 + replication_factor: 3 # optional, informational only + replication_strategy: NetworkTopologyStrategy # optional, informational only + - name: dc2 + hosts: + - 10.0.0.1 + replication_factor: 2 # optional, informational only + routing: # optional + read_dc: dc2 # DC to use for reads (default: load_balancing.local_dc) + write_dc: dc1 # DC to use for writes (default: load_balancing.local_dc) + port: 9042 # optional + username: user # optional + password: secret # optional + protocol_version: 5 # optional + load_balancing: # optional + local_dc: 'dc1' # sets the default execution profile + load_balancing_policy: 'TokenAwarePolicy(DCAwareRoundRobinPolicy)' # optional + read_concurrency: 100 # optional + write_concurrency: 100 # optional +``` +{% endcode %} + ### Example (Astra DB) {% code title="feature_store.yaml" %} diff --git a/docs/reference/online-stores/dragonfly.md b/docs/reference/online-stores/dragonfly.md index bcd814ecc45..723761ed3bb 100644 --- a/docs/reference/online-stores/dragonfly.md +++ b/docs/reference/online-stores/dragonfly.md @@ -48,7 +48,7 @@ There are several options available to get Dragonfly up and running quickly. We `feast apply` -The `apply` command scans python files in the current directory (`example_repo.py` in this case) for feature view/entity definitions, registers the objects, and deploys infrastructure. +The `apply` command scans python files in the current directory (`feature_definitions.py` in this case) for feature view/entity definitions, registers the objects, and deploys infrastructure. You should see the following output: ``` diff --git a/docs/reference/online-stores/dynamodb.md b/docs/reference/online-stores/dynamodb.md index 344caccac1d..a7f6b7392c9 100644 --- a/docs/reference/online-stores/dynamodb.md +++ b/docs/reference/online-stores/dynamodb.md @@ -22,13 +22,54 @@ online_store: The full set of configuration options is available in [DynamoDBOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.dynamodb.DynamoDBOnlineStoreConfig). +## Configuration + +Below is an example with performance tuning options: + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: aws +online_store: + type: dynamodb + region: us-west-2 + batch_size: 100 + max_read_workers: 10 + consistent_reads: false +``` +{% endcode %} + +### Configuration Options + +| Option | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| `region` | string | | AWS region for DynamoDB | +| `table_name_template` | string | `{project}.{table_name}` | Template for table names | +| `batch_size` | int | `100` | Number of items per BatchGetItem/BatchWriteItem request (max 100) | +| `max_read_workers` | int | `10` | Maximum parallel threads for batch read operations. Higher values improve throughput for large batch reads but increase resource usage | +| `consistent_reads` | bool | `false` | Whether to use strongly consistent reads (higher latency, guaranteed latest data) | +| `tags` | dict | `null` | AWS resource tags added to each table | +| `session_based_auth` | bool | `false` | Use AWS session-based client authentication | + +### Performance Tuning + +**Parallel Batch Reads**: When reading features for many entities, DynamoDB's BatchGetItem is limited to 100 items per request. For 500 entities, this requires 5 batch requests. The `max_read_workers` option controls how many of these batches execute in parallel: + +- **Sequential (old behavior)**: 5 batches × 10ms = 50ms total +- **Parallel (with `max_read_workers: 10`)**: 5 batches in parallel ≈ 10ms total + +For high-throughput workloads with large entity counts, increase `max_read_workers` (up to 20-30) based on your DynamoDB capacity and network conditions. + +**Batch Size**: Increase `batch_size` up to 100 to reduce the number of API calls. However, larger batches may hit DynamoDB's 16MB response limit for tables with large feature values. + ## Permissions Feast requires the following permissions in order to execute commands for DynamoDB online store: | **Command** | Permissions | Resources | | ----------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------- | -| **Apply** |

dynamodb:CreateTable

dynamodb:DescribeTable

dynamodb:DeleteTable

| arn:aws:dynamodb:\:\:table/\* | +| **Apply** |

dynamodb:CreateTable

dynamodb:DescribeTable

dynamodb:DeleteTable

dynamodb:TagResource

| arn:aws:dynamodb:\:\:table/\* | | **Materialize** | dynamodb.BatchWriteItem | arn:aws:dynamodb:\:\:table/\* | | **Get Online Features** | dynamodb.BatchGetItem | arn:aws:dynamodb:\:\:table/\* | @@ -42,6 +83,7 @@ The following inline policy can be used to grant Feast the necessary permissions "dynamodb:CreateTable", "dynamodb:DescribeTable", "dynamodb:DeleteTable", + "dynamodb:TagResource", "dynamodb:BatchWriteItem", "dynamodb:BatchGetItem" ], diff --git a/docs/reference/online-stores/faiss.md b/docs/reference/online-stores/faiss.md new file mode 100644 index 00000000000..32c9242dd60 --- /dev/null +++ b/docs/reference/online-stores/faiss.md @@ -0,0 +1,57 @@ +# Faiss online store + +## Description + +The [Faiss](https://github.com/facebookresearch/faiss) online store provides support for materializing feature values and performing vector similarity search using Facebook AI Similarity Search (Faiss). Faiss is a library for efficient similarity search and clustering of dense vectors, making it well-suited for use cases involving embeddings and nearest-neighbor lookups. + +## Getting started +In order to use this online store, you'll need to install the Faiss dependency. E.g. + +`pip install 'feast[faiss]'` + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: feast.infra.online_stores.faiss_online_store.FaissOnlineStore + dimension: 128 + index_path: data/faiss_index + index_type: IVFFlat # optional, default: IVFFlat + nlist: 100 # optional, default: 100 +``` +{% endcode %} + +**Note:** Faiss is not registered as a named online store type. You must use the fully qualified class path as the `type` value. + +The full set of configuration options is available in [FaissOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.faiss_online_store.FaissOnlineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Faiss online store. + +| | Faiss | +|:----------------------------------------------------------|:------| +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | +| vector similarity search | yes | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/hbase.md b/docs/reference/online-stores/hbase.md new file mode 100644 index 00000000000..b53bfed5fe5 --- /dev/null +++ b/docs/reference/online-stores/hbase.md @@ -0,0 +1,56 @@ +# HBase online store + +## Description + +The [HBase](https://hbase.apache.org/) online store provides support for materializing feature values into an Apache HBase database for serving online features in real-time. + +* Each feature view is mapped to an HBase table +* Connects to HBase via the Thrift server using [happybase](https://happybase.readthedocs.io/) + +## Getting started +In order to use this online store, you'll need to run `pip install 'feast[hbase]'`. + +## Example + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: hbase + host: localhost + port: "9090" + connection_pool_size: 4 # optional + protocol: binary # optional + transport: buffered # optional +``` +{% endcode %} + +The full set of configuration options is available in [HbaseOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.hbase_online_store.hbase.HbaseOnlineStoreConfig). + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the HBase online store. + +| | HBase | +| :-------------------------------------------------------- | :---- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | no | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | yes | +| collocated by feature service | no | +| collocated by entity key | no | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/hybrid.md b/docs/reference/online-stores/hybrid.md index 38527d9a66e..a03cc4ae853 100644 --- a/docs/reference/online-stores/hybrid.md +++ b/docs/reference/online-stores/hybrid.md @@ -20,7 +20,7 @@ project: my_feature_repo registry: data/registry.db provider: local online_store: - type: hybrid_online_store.HybridOnlineStore + type: hybrid routing_tag: team # or any tag name you want to use in FeatureView's for routing online_stores: - type: bigtable diff --git a/docs/reference/online-stores/ikv.md b/docs/reference/online-stores/ikv.md deleted file mode 100644 index 79f21d17797..00000000000 --- a/docs/reference/online-stores/ikv.md +++ /dev/null @@ -1,69 +0,0 @@ -# IKV (Inlined Key-Value Store) online store - -## Description - -[IKV](https://github.com/inlinedio/ikv-store) is a fully-managed embedded key-value store, primarily designed for storing ML features. Most key-value stores (think Redis or Cassandra) need a remote database cluster, whereas IKV allows you to utilize your existing application infrastructure to store data (cost efficient) and access it without any network calls (better performance). See detailed performance benchmarks and cost comparison with Redis on [https://inlined.io](https://inlined.io). IKV can be used as an online-store in Feast, the rest of this guide goes over the setup. - -## Getting started -Make sure you have Python and `pip` installed. - -Install the Feast SDK and CLI: `pip install feast` - -In order to use this online store, you'll need to install the IKV extra (along with the dependency needed for the offline store of choice). E.g. -- `pip install 'feast[gcp, ikv]'` -- `pip install 'feast[snowflake, ikv]'` -- `pip install 'feast[aws, ikv]'` -- `pip install 'feast[azure, ikv]'` - -You can get started by using any of the other templates (e.g. `feast init -t gcp` or `feast init -t snowflake` or `feast init -t aws`), and then swapping in IKV as the online store as seen below in the examples. - -### 1. Provision an IKV store -Go to [https://inlined.io](https://inlined.io) or email onboarding[at]inlined.io - -### 2. Configure - -Update `my_feature_repo/feature_store.yaml` with the below contents: - -{% code title="feature_store.yaml" %} -```yaml -project: my_feature_repo -registry: data/registry.db -provider: local -online_store: - type: ikv - account_id: secret - account_passkey: secret - store_name: your-store-name - mount_directory: /absolute/path/on/disk/for/ikv/embedded/index -``` -{% endcode %} - -After provisioning an IKV account/store, you should have an account id, passkey and store-name. Additionally you must specify a mount-directory - where IKV will pull/update (maintain) a copy of the index for online reads (IKV is an embedded database). It can be skipped only if you don't plan to read any data from this container. The mount directory path usually points to a location on local/remote disk. - -The full set of configuration options is available in IKVOnlineStoreConfig at `sdk/python/feast/infra/online_stores/contrib/ikv_online_store/ikv.py` - -## Functionality Matrix - -The set of functionality supported by online stores is described in detail [here](overview.md#functionality). -Below is a matrix indicating which functionality is supported by the IKV online store. - -| | IKV | -| :-------------------------------------------------------- | :---- | -| write feature values to the online store | yes | -| read feature values from the online store | yes | -| update infrastructure (e.g. tables) in the online store | yes | -| teardown infrastructure (e.g. tables) in the online store | yes | -| generate a plan of infrastructure changes | no | -| support for on-demand transforms | yes | -| readable by Python SDK | yes | -| readable by Java | no | -| readable by Go | no | -| support for entityless feature views | yes | -| support for concurrent writing to the same key | yes | -| support for ttl (time to live) at retrieval | no | -| support for deleting expired data | no | -| collocated by feature view | no | -| collocated by feature service | no | -| collocated by entity key | yes | - -To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/milvus.md b/docs/reference/online-stores/milvus.md index 014c7bd68a5..58f7dbd167a 100644 --- a/docs/reference/online-stores/milvus.md +++ b/docs/reference/online-stores/milvus.md @@ -11,6 +11,14 @@ In order to use this online store, you'll need to install the Milvus extra (alon `pip install 'feast[milvus]'` +{% hint style="warning" %} +**Upgrading to milvus-lite 3.0.0+** + +Feast supports both milvus-lite 2.x and 3.x. However, if you upgrade from milvus-lite 2.x.x to 3.0.0+, the `.db` files created by the original storage format are **not compatible** with the milvus-lite 3.0.0+ engine. You will need to re-import your data into a new database — automatic migration is not available. + +See the [milvus-lite GitHub page](https://github.com/milvus-io/milvus-lite) for more details. +{% endhint %} + You can get started by using any of the other templates (e.g. `feast init -t gcp` or `feast init -t snowflake` or `feast init -t aws`), and then swapping in Redis as the online store as seen below in the examples. ## Examples diff --git a/docs/reference/online-stores/mongodb.md b/docs/reference/online-stores/mongodb.md new file mode 100644 index 00000000000..e5251747e77 --- /dev/null +++ b/docs/reference/online-stores/mongodb.md @@ -0,0 +1,177 @@ +# MongoDB online store + +## Description + +The [MongoDB](https://www.mongodb.com/) online store provides support for materializing feature values into MongoDB for serving online features. + +## Features + +* Supports both synchronous and asynchronous operations for high-performance feature retrieval +* Native async support uses PyMongo's `AsyncMongoClient` (no Motor dependency required) +* Flexible connection options supporting MongoDB Atlas, self-hosted MongoDB, and MongoDB replica sets +* Automatic index creation for optimized query performance +* Entity key collocation for efficient feature retrieval + +## Getting started + +In order to use this online store, you'll need to install the MongoDB extra (along with the dependency needed for the offline store of choice): + +```bash +pip install 'feast[mongodb]' +``` + +You can get started by using any of the other templates (e.g. `feast init -t gcp` or `feast init -t snowflake` or `feast init -t aws`), and then swapping in MongoDB as the online store as seen below in the examples. + +## Examples + +### Basic configuration with MongoDB Atlas + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: mongodb + connection_string: "mongodb+srv://username:password@cluster.mongodb.net/" # pragma: allowlist secret + database_name: feast_online_store +``` +{% endcode %} + +### Self-hosted MongoDB with authentication + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: mongodb + connection_string: "mongodb://username:password@localhost:27017/" # pragma: allowlist secret + database_name: feast_online_store + collection_suffix: features +``` +{% endcode %} + +### MongoDB replica set configuration + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: mongodb + connection_string: "mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplicaSet" + database_name: feast_online_store + client_kwargs: + retryWrites: true + w: majority +``` +{% endcode %} + +### Advanced configuration with custom client options + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: mongodb + connection_string: "mongodb+srv://cluster.mongodb.net/" + database_name: feast_online_store + collection_suffix: features + client_kwargs: + maxPoolSize: 50 + minPoolSize: 10 + serverSelectionTimeoutMS: 5000 + connectTimeoutMS: 10000 +``` +{% endcode %} + +The full set of configuration options is available in [MongoDBOnlineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.mongodb_online_store.mongodb.MongoDBOnlineStoreConfig). + +## Data Model + +The MongoDB online store uses a **single collection per project** with entity key collocation. Features from multiple feature views for the same entity are stored together in a single document. + +### Example Document Schema + +The example shows a single entity. It contains 3 features from 2 feature views: "rating" and "trips_last7d" from Feature +View "driver_stats", and "surge_multiplier" from "pricing" view. +Each feature view has its own event timestamp. +The "created_timestamp" marks when the entity was materialized. + +```javascript +{ + "_id": "", // Binary entity key (bytes) + "features": { + "driver_stats": { + "rating": 4.91, + "trips_last_7d": 132 + }, + "pricing": { + "surge_multiplier": 1.2 + } + }, + "event_timestamps": { + "driver_stats": ISODate("2026-01-20T12:00:00Z"), + "pricing": ISODate("2026-01-21T08:30:00Z") + }, + "created_timestamp": ISODate("2026-01-21T12:00:05Z") +} +``` + +### Key Design Decisions + +* **`_id` field**: Uses the serialized entity key (bytes) as the primary key for efficient lookups +* **Nested features**: Features are organized by feature view name, allowing multiple feature views per entity +* **Event timestamps**: Stored per feature view to track when each feature set was last updated +* **Created timestamp**: Global timestamp for the entire document + +### Indexes + +The online store automatically creates the following index: +* Primary key index on `_id` (automatic in MongoDB), set to the serialized entity key. + +No additional indexes are required for the online store operations. + +## Async Support + +The MongoDB online store provides native async support using PyMongo 4.13+'s stable `AsyncMongoClient`. This enables: + +* **High concurrency**: Handle thousands of concurrent feature requests without thread pool limitations +* **True async I/O**: Non-blocking operations for better performance in async applications +* **10-20x performance improvement**: For concurrent workloads compared to sequential sync operations + +Both sync and async methods are fully supported: +* `online_read` / `online_read_async` +* `online_write_batch` / `online_write_batch_async` + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the MongoDB online store. + +| | MongoDB | +| :-------------------------------------------------------- | :------ | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | no | +| readable by Go | no | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | yes | +| support for ttl (time to live) at retrieval | no | +| support for deleting expired data | no | +| collocated by feature view | no | +| collocated by feature service | no | +| collocated by entity key | yes | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). + diff --git a/docs/reference/online-stores/mysql.md b/docs/reference/online-stores/mysql.md index 8868e64279d..2f9650f8916 100644 --- a/docs/reference/online-stores/mysql.md +++ b/docs/reference/online-stores/mysql.md @@ -28,6 +28,28 @@ online_store: The full set of configuration options is available in [MySQLOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.mysql_online_store.MySQLOnlineStoreConfig). +## Batch write mode +By default, the MySQL online store performs row-by-row insert and commit for each feature record. While this ensures per-record atomicity, it can lead to significant overhead on write operations — especially on distributed SQL databases (for example, TiDB, which is MySQL-compatible and uses a consensus protocol). + +To improve writing performance, you can enable batch write mode by setting `batch_write` to `true` and `batch_size`, which executes multiple insert queries in batches and commits them together per batch instead of committing each record individually. + +{% code title="feature_store.yaml" %} +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local +online_store: + type: mysql + host: DB_HOST + port: DB_PORT + database: DB_NAME + user: DB_USERNAME + password: DB_PASSWORD + batch_write: true + batch_size: 100 +``` +{% endcode %} + ## Functionality Matrix The set of functionality supported by online stores is described in detail [here](overview.md#functionality). diff --git a/docs/reference/online-stores/overview.md b/docs/reference/online-stores/overview.md index b54329ad613..663a48836dc 100644 --- a/docs/reference/online-stores/overview.md +++ b/docs/reference/online-stores/overview.md @@ -29,26 +29,26 @@ See this [issue](https://github.com/feast-dev/feast/issues/2254) for a discussio ## Functionality Matrix There are currently five core online store implementations: `SqliteOnlineStore`, `RedisOnlineStore`, `DynamoDBOnlineStore`, `SnowflakeOnlineStore`, and `DatastoreOnlineStore`. -There are several additional implementations contributed by the Feast community (`PostgreSQLOnlineStore`, `HbaseOnlineStore`, `CassandraOnlineStore` and `IKVOnlineStore`), which are not guaranteed to be stable or to match the functionality of the core implementations. +There are several additional implementations contributed by the Feast community (`PostgreSQLOnlineStore`, `HbaseOnlineStore`, `CassandraOnlineStore` and `ScyllaDBOnlineStore`), which are not guaranteed to be stable or to match the functionality of the core implementations. Details for each specific online store, such as how to configure it in a `feature_store.yaml`, can be found [here](README.md). Below is a matrix indicating which online stores support what functionality. -| | Sqlite | Redis | DynamoDB | Snowflake | Datastore | Postgres | Hbase | [[Cassandra](https://cassandra.apache.org/_/index.html) / [Astra DB](https://www.datastax.com/products/datastax-astra?utm_source=feast)] | [IKV](https://inlined.io) | Milvus | -| :-------------------------------------------------------- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- |:-------| -| write feature values to the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| read feature values from the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| update infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| teardown infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| generate a plan of infrastructure changes | yes | no | no | no | no | no | no | yes | no | no | -| support for on-demand transforms | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| readable by Python SDK | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | -| readable by Java | no | yes | no | no | no | no | no | no | no | no | -| readable by Go | yes | yes | no | no | no | no | no | no | no | no | -| support for entityless feature views | yes | yes | yes | yes | yes | yes | yes | yes | yes | no | -| support for concurrent writing to the same key | no | yes | no | no | no | no | no | no | yes | no | -| support for ttl (time to live) at retrieval | no | yes | no | no | no | no | no | no | no | no | -| support for deleting expired data | no | yes | no | no | no | no | no | no | no | no | -| collocated by feature view | yes | no | yes | yes | yes | yes | yes | yes | no | no | +| | Sqlite | Redis | DynamoDB | Snowflake | Datastore | Postgres | Hbase | [[Cassandra](https://cassandra.apache.org/_/index.html) / [Astra DB](https://www.datastax.com/products/datastax-astra?utm_source=feast)] | Milvus | ScyllaDB | +| :-------------------------------------------------------- | :-- | :-- | :-- | :-- | :-- | :-- | :-- | :-- |:----| :-- | +| write feature values to the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| read feature values from the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| update infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| generate a plan of infrastructure changes | yes | no | no | no | no | no | no | yes | no | no | +| support for on-demand transforms | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| readable by Python SDK | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| readable by Java | no | yes | no | no | no | no | no | no | no | no | +| readable by Go | yes | yes | no | no | no | no | no | no | no | no | +| support for entityless feature views | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | +| support for concurrent writing to the same key | no | yes | no | no | no | no | no | no | yes | no | +| support for ttl (time to live) at retrieval | no | yes | no | no | no | no | no | no | no | yes | +| support for deleting expired data | no | yes | no | no | no | no | no | no | no | yes | +| collocated by feature view | yes | no | yes | yes | yes | yes | yes | yes | no | yes | | collocated by feature service | no | no | no | no | no | no | no | no | no | no | -| collocated by entity key | no | yes | no | no | no | no | no | no | yes | no | +| collocated by entity key | no | yes | no | no | no | no | no | no | yes | no | diff --git a/docs/reference/online-stores/postgres.md b/docs/reference/online-stores/postgres.md index fb2253d043e..10f3f871710 100644 --- a/docs/reference/online-stores/postgres.md +++ b/docs/reference/online-stores/postgres.md @@ -6,7 +6,7 @@ The PostgreSQL online store provides support for materializing feature values in * Only the latest feature values are persisted -* sslmode, sslkey_path, sslcert_path, and sslrootcert_path are optional +* `sslmode` defaults to `require`, which encrypts the connection without certificate verification. To disable SSL (e.g. for local development), set `sslmode: disable`. For certificate verification, set `sslmode` to `verify-ca` or `verify-full` and provide the corresponding `sslrootcert_path` (and optionally `sslcert_path` and `sslkey_path` for mutual TLS) ## Getting started In order to use this online store, you'll need to run `pip install 'feast[postgres]'`. You can get started by then running `feast init -t postgres`. diff --git a/docs/reference/online-stores/redis.md b/docs/reference/online-stores/redis.md index ae7f8b4c5ca..f212f03d943 100644 --- a/docs/reference/online-stores/redis.md +++ b/docs/reference/online-stores/redis.md @@ -7,7 +7,12 @@ The [Redis](https://redis.io) online store provides support for materializing fe * Both Redis and Redis Cluster are supported. * The data model used to store feature values in Redis is described in more detail [here](../../specs/online\_store\_format.md). +**Data model:** All feature views that share the same entity key are stored in a single Redis hash. The hash key is derived from the serialized entity key and the project name. Each feature's value is a hash field keyed by a murmur3 hash of `"feature_view_name:feature_name"`, and a separate `_ts:` field stores the event timestamp per feature view. + +This collocated-by-entity design enables an important performance optimization: `get_online_features()` requests that span multiple feature views for the same entity can issue all `HMGET` commands in a **single Redis pipeline execution**, regardless of how many feature views are requested. See [Performance characteristics](#performance-characteristics) below. + ## Getting started + In order to use this online store, you'll need to install the redis extra (along with the dependency needed for the offline store of choice). E.g. - `pip install 'feast[gcp, redis]'` - `pip install 'feast[snowflake, redis]'` @@ -60,22 +65,85 @@ online_store: ``` {% endcode %} -Additionally, the redis online store also supports automatically deleting data via a TTL mechanism. -The TTL is applied at the entity level, so feature values from any associated feature views for an entity are removed together. -This TTL can be set in the `feature_store.yaml`, using the `key_ttl_seconds` field in the online store. For example: +## TTL configuration + +The Redis online store supports two complementary TTL mechanisms: + +### Key-level TTL (`key_ttl_seconds`) + +Sets a Redis `EXPIRE` on the entire entity hash key. When the TTL elapses, Redis automatically deletes all feature values for that entity across **all** feature views that share the same key. Use this to bound memory usage and automatically evict stale entity data. -{% code title="feature_store.yaml" %} ```yaml -project: my_feature_repo -registry: data/registry.db -provider: local online_store: type: redis - key_ttl_seconds: 604800 + key_ttl_seconds: 604800 # 7 days connection_string: "localhost:6379" ``` -{% endcode %} +{% hint style="warning" %} +Because all feature views for the same entity share one Redis hash key, `key_ttl_seconds` uses the **entity** as the expiry unit, not the feature view. Writing any feature view for an entity resets the TTL for the whole hash. This means a frequently written feature view can keep a stale, infrequently written feature view alive beyond its intended TTL. +{% endhint %} + +{% hint style="info" %} +`FeatureView.ttl` defines the **offline retrieval window** (how far back in time point-in-time joins look in the offline store). It does **not** filter online store reads. To control online data expiry, use `key_ttl_seconds`. +{% endhint %} + +## Performance characteristics + +### Batched multi-feature-view reads + +Unlike most online stores, the Redis implementation overrides `get_online_features()` to issue a **single pipeline execution** for all feature views in the request. Because all feature views for the same entity live in the same Redis hash, all `HMGET` commands across every feature view are batched into one `pipeline.execute()` call. + +| Feature views in request | Redis round trips (before) | Redis round trips (after) | +| :---: | :---: | :---: | +| 1 | 1 | 1 | +| 5 | 5 | 1 | +| 10 | 10 | 1 | +| 20 | 20 | 1 | + +Benchmark results against Redis 8.6.2 (localhost, 50 entities, 3 features/FV, 300 rounds): + +| Feature views | Master (per-FV pipeline) | Improved (batched pipeline) | Speedup | +| :---: | ---: | ---: | :---: | +| 1 | 1.57 ms | 1.32 ms | 1.19× | +| 5 | 7.27 ms | 5.63 ms | 1.29× | +| 10 | 15.64 ms | 10.65 ms | 1.47× | +| 20 | 36.33 ms | 21.21 ms | **1.71×** | + +The speedup grows with the number of feature views and is most pronounced in production environments with non-trivial network RTT to Redis. + +### Write path: `skip_dedup` for bulk loads + +By default, `online_write_batch()` checks existing timestamps before writing (to avoid overwriting newer data with older data). This requires two pipeline round trips per batch: one to read existing timestamps, one to write new values. + +For initial bulk loads or append-only pipelines where out-of-order writes are not a concern, set `skip_dedup: true` to write in a **single pipeline round trip**: + +```yaml +online_store: + type: redis + connection_string: "localhost:6379" + skip_dedup: true +``` + +{% hint style="warning" %} +With `skip_dedup: true`, writes always overwrite existing data regardless of timestamp order. Under concurrent writers, an older record can overwrite a newer one. Use only for controlled bulk loads or pipelines that guarantee ordered delivery. +{% endhint %} + +### Async write support + +The Redis online store implements `online_write_batch_async()` using the async Redis client. This enables non-blocking batch writes in async serving frameworks. `skip_dedup` is also respected in the async path. + +## Configuration reference + +| Parameter | Default | Description | +| --- | --- | --- | +| `type` | `redis` | Online store type selector | +| `redis_type` | `redis` | Connection type: `redis`, `redis_cluster`, or `redis_sentinel` | +| `connection_string` | `localhost:6379` | Host:port and optional parameters. For cluster: `redis1:6379,redis2:6379,ssl=true,password=...` | +| `sentinel_master` | `mymaster` | Sentinel master name (only used when `redis_type: redis_sentinel`) | +| `key_ttl_seconds` | `null` | Redis `EXPIRE` TTL in seconds applied to the entity hash key after each write. Expires all feature views for that entity together. | +| `full_scan_for_deletion` | `true` | When `true`, deleting or renaming a feature view scans Redis to remove its hash fields. Set `false` to skip deletion scans (faster `feast apply`, but leaves orphaned data). | +| `skip_dedup` | `false` | When `true`, skips the existing-timestamp read before each write, halving write round trips. Suitable for initial bulk loads; may cause older values to overwrite newer ones under concurrent writers. | The full set of configuration options is available in [RedisOnlineStoreConfig](https://rtd.feast.dev/en/latest/#feast.infra.online_stores.redis.RedisOnlineStoreConfig). @@ -102,5 +170,7 @@ Below is a matrix indicating which functionality is supported by the Redis onlin | collocated by feature view | no | | collocated by feature service | no | | collocated by entity key | yes | +| async batch writes | yes | +| batched multi-feature-view reads (single pipeline) | yes | To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/online-stores/remote.md b/docs/reference/online-stores/remote.md index b6734ccc1ec..9630e1e4e62 100644 --- a/docs/reference/online-stores/remote.md +++ b/docs/reference/online-stores/remote.md @@ -2,7 +2,7 @@ ## Description -This remote online store will let you interact with remote feature server. At this moment this only supports the read operation. You can use this online store and able retrieve online features `store.get_online_features` from remote feature server. +This remote online store lets you interact with a remote feature server. You can use this online store to retrieve online features via `store.get_online_features()` from a remote feature server. ## Examples @@ -25,6 +25,128 @@ auth: `cert` is an optional configuration to the public certificate path when the online server starts in TLS(SSL) mode. This may be needed if the online server is started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`. +## Connection Pooling Configuration + +The remote online store uses HTTP connection pooling to improve performance by reusing TCP/TLS connections across multiple requests. This significantly reduces latency by avoiding the overhead of establishing new connections for each request. + +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `connection_pool_size` | int | 50 | Maximum number of connections to keep in the pool. Increase for high-concurrency workloads. | +| `connection_idle_timeout` | int | 300 | Maximum time in seconds a session can be idle before being closed. Set to `0` to disable idle timeout. | +| `connection_retries` | int | 3 | Number of retries for failed requests with exponential backoff. | + +### Example with Connection Pooling + +{% code title="feature_store.yaml" %} +```yaml +project: my-local-project +registry: /remote/data/registry.db +provider: local +online_store: + type: remote + path: http://feast-feature-server:80 + + # Connection pooling configuration (optional) + connection_pool_size: 50 # Max connections in pool + connection_idle_timeout: 300 # Idle timeout in seconds (0 to disable) + connection_retries: 3 # Retry count with exponential backoff + +entity_key_serialization_version: 3 +auth: + type: no_auth +``` +{% endcode %} + +### Use Cases + +**High-throughput workloads:** +```yaml +online_store: + type: remote + path: http://feast-server:80 + connection_pool_size: 100 # More connections for high concurrency + connection_idle_timeout: 600 # 10 minutes idle timeout + connection_retries: 5 # More retries for resilience +``` + +**Long-running services:** +```yaml +online_store: + type: remote + path: http://feast-server:80 + connection_idle_timeout: 0 # Never auto-close session +``` + +**Resource-constrained environments:** +```yaml +online_store: + type: remote + path: http://feast-server:80 + connection_pool_size: 10 # Fewer connections to reduce memory + connection_idle_timeout: 60 # 1 minute timeout +``` + +### Performance Benefits + +Connection pooling provides significant latency improvements: + +- **Without pooling**: Each request requires a new TCP connection (~10-50ms) and TLS handshake (~30-100ms) +- **With pooling**: Subsequent requests reuse existing connections, eliminating connection overhead + +This is especially beneficial for: +- High-frequency feature retrieval in real-time inference pipelines +- Batch processing with many sequential `get_online_features()` calls +- Services with authentication enabled (reduces token refresh overhead) + +### Session Cleanup + +The HTTP session is automatically managed with idle timeout, but you can also explicitly close it when your application is shutting down or when you want to release resources. + +#### Using FeatureStore context manager (recommended) + +The recommended way to ensure proper cleanup is to use the `FeatureStore` as a context manager: + +```python +from feast import FeatureStore + +# Session is automatically closed when exiting the context +with FeatureStore(repo_path=".") as store: + features = store.get_online_features( + features=["driver_hourly_stats:conv_rate"], + entity_rows=[{"driver_id": 1001}] + ) +``` + +#### Explicit cleanup + +You can also explicitly close the session by calling `close()` on the `FeatureStore`: + +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") +try: + features = store.get_online_features( + features=["driver_hourly_stats:conv_rate"], + entity_rows=[{"driver_id": 1001}] + ) +finally: + store.close() # Closes HTTP session and releases resources +``` + +#### Direct session management + +For advanced use cases, you can directly manage the HTTP session via `HttpSessionManager`: + +```python +from feast.permissions.client.http_auth_requests_wrapper import HttpSessionManager + +# Close the cached HTTP session +HttpSessionManager.close_session() +``` + ## How to configure Authentication and Authorization Please refer the [page](./../../../docs/getting-started/concepts/permission.md) for more details on how to configure authentication and authorization. diff --git a/docs/reference/online-stores/scylladb.md b/docs/reference/online-stores/scylladb.md index c8583ac101a..98dc03b24cb 100644 --- a/docs/reference/online-stores/scylladb.md +++ b/docs/reference/online-stores/scylladb.md @@ -2,20 +2,15 @@ ## Description -ScyllaDB is a low-latency and high-performance Cassandra-compatible (uses CQL) database. You can use the existing Cassandra connector to use ScyllaDB as an online store in Feast. - -The [ScyllaDB](https://www.scylladb.com/) online store provides support for materializing feature values into a ScyllaDB or [ScyllaDB Cloud](https://www.scylladb.com/product/scylla-cloud/) cluster for serving online features real-time. +[ScyllaDB](https://www.scylladb.com/) is a distributed real-time NoSQL database with vector search support. +This integration uses the native **`scylla-driver`** Python driver for optimised performance and supports materializing feature values into a [ScyllaDB Cloud](https://www.scylladb.com/product/scylla-cloud/) cluster for real-time online feature serving. ## Getting started -Install Feast with Cassandra support: -```bash -pip install "feast[cassandra]" -``` +Install Feast with the `scylladb` extra, which pulls in `scylla-driver` automatically: -Create a new Feast project: ```bash -feast init REPO_NAME -t cassandra +pip install feast[scylladb] ``` ### Example (ScyllaDB) @@ -26,7 +21,7 @@ project: scylla_feature_repo registry: data/registry.db provider: local online_store: - type: cassandra + type: scylladb hosts: - 172.17.0.2 keyspace: feast @@ -43,44 +38,106 @@ project: scylla_feature_repo registry: data/registry.db provider: local online_store: - type: cassandra + type: scylladb hosts: - node-0.aws_us_east_1.xxxxxxxx.clusters.scylla.cloud - node-1.aws_us_east_1.xxxxxxxx.clusters.scylla.cloud - node-2.aws_us_east_1.xxxxxxxx.clusters.scylla.cloud keyspace: feast username: scylla - password: password + password: xxxxxx + local_dc: AWS_US_EAST_1 ``` {% endcode %} - -The full set of configuration options is available in [CassandraOnlineStoreConfig](https://rtd.feast.dev/en/master/#feast.infra.online_stores.cassandra_online_store.cassandra_online_store.CassandraOnlineStoreConfig). -For a full explanation of configuration options please look at file -`sdk/python/feast/infra/online_stores/contrib/cassandra_online_store/README.md`. +## Configuration options + +| Parameter | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| `hosts` | list[str] | *(required)* | Contact-point host addresses. | +| `port` | int | `9042` | CQL port. | +| `keyspace` | str | `feast_keyspace` | Target ScyllaDB keyspace. | +| `username` | str | `None` | Auth username. | +| `password` | str | `None` | Auth password. | +| `local_dc` | str | `None` | Local datacenter name for DC-aware load balancing. | +| `request_timeout` | float | `None` | Driver request timeout in seconds. | +| `read_concurrency` | int | `100` | `concurrency` argument passed to the driver's `execute_concurrent_with_args` for reads. Controls how many CQL statements are in-flight at once. | +| `write_concurrency` | int | `100` | `concurrency` argument passed to the driver's `execute_concurrent_with_args` for writes. Controls how many CQL statements are in-flight at once. | +| `vector_similarity_function` | str | `COSINE` | Default similarity function for vector indexes. Supported: `COSINE`, `DOT_PRODUCT`, `EUCLIDEAN`. Can be overridden per-feature via the `similarity_function` Field tag. | Storage specifications can be found at `docs/specs/online_store_format.md`. +## Vector Search + +ScyllaDB Cloud supports approximate nearest-neighbour (ANN) vector search. +To enable it for a feature view, tag the embedding `Field` with `vector_index=true` and specify the number of dimensions: + +{% code title="feature_definitions.py" %} +```python +from feast import FeatureView, Field +from feast.types import Array, Float32, String + +documents_fv = FeatureView( + name="documents", + entities=[item], + schema=[ + Field(name="text", dtype=String), + Field( + name="embedding", + dtype=Array(Float32), + tags={ + "vector_index": "true", + "dimensions": "768", + "similarity_function": "COSINE", # COSINE | DOT_PRODUCT | EUCLIDEAN + }, + ), + ], + online=True, + source=push_source, +) +``` +{% endcode %} + +When `feast apply` runs, the store automatically creates the necessary tables and HNSW ANN index for any feature view with vector-tagged fields. + +To query the top-k most similar documents: + +```python +result = store.retrieve_online_documents_v2( + features=["documents:text", "documents:embedding"], + query=[0.1, 0.2, ...], # your query embedding + top_k=10, + distance_metric="COSINE", +) +``` + +### Metadata filtering (OpenAI-compatible) + +ScyllaDB supports vector similarity search, but OpenAI-style metadata filtering is **not supported yet**. +Passing `filters` to `retrieve_online_documents_v2` or the OpenAI-compatible search endpoint raises `NotImplementedError`. + +For filtered vector search today, use one of the backends that implement metadata filters (for example Milvus, Elasticsearch, Postgres, SQLite, or MongoDB). See [Alpha Vector Database](../alpha-vector-database.md#supported-online-stores). + ## Functionality Matrix The set of functionality supported by online stores is described in detail [here](overview.md#functionality). -Below is a matrix indicating which functionality is supported by the Cassandra plugin. +Below is a matrix indicating which functionality is supported by the ScyllaDB online store. -| | Cassandra | +| | ScyllaDB | | :-------------------------------------------------------- | :-------- | | write feature values to the online store | yes | | read feature values from the online store | yes | | update infrastructure (e.g. tables) in the online store | yes | | teardown infrastructure (e.g. tables) in the online store | yes | -| generate a plan of infrastructure changes | yes | +| generate a plan of infrastructure changes | no | | support for on-demand transforms | yes | | readable by Python SDK | yes | | readable by Java | no | | readable by Go | no | | support for entityless feature views | yes | | support for concurrent writing to the same key | no | -| support for ttl (time to live) at retrieval | no | -| support for deleting expired data | no | +| support for ttl (time to live) at retrieval | yes | +| support for deleting expired data | yes | | collocated by feature view | yes | | collocated by feature service | no | | collocated by entity key | no | @@ -89,6 +146,6 @@ To compare this set of functionality against other online stores, please see the ## Resources -* [Sample application with ScyllaDB](https://feature-store.scylladb.com/stable/) +* [ScyllaDB Vector Search documentation](https://cloud.docs.scylladb.com/stable/vector-search/) * [ScyllaDB website](https://www.scylladb.com/) * [ScyllaDB Cloud documentation](https://cloud.docs.scylladb.com/stable/) diff --git a/docs/reference/online-stores/valkey.md b/docs/reference/online-stores/valkey.md new file mode 100644 index 00000000000..4ede3f65b5d --- /dev/null +++ b/docs/reference/online-stores/valkey.md @@ -0,0 +1,94 @@ +# Valkey online store + +## Description + +[Valkey](https://valkey.io/) is an open source (BSD-3-Clause), high-performance key/value datastore hosted by the Linux Foundation, created as a community fork of Redis. It maintains compatibility with the Redis wire protocol, so it can act as a drop-in replacement for Redis. Valkey is also offered as a managed engine by major cloud providers (for example, Amazon ElastiCache for Valkey). + +Similar to Redis and [Dragonfly](dragonfly.md), Valkey can be used as an online feature store for Feast: Feast's Redis online store only issues core commands (hash reads/writes, scans, key expiry, pipelines), all of which Valkey implements. + +Feast's standard online store operations have been verified against Valkey 8.1: `feast apply`, `feast materialize`, online retrieval via `get_online_features`, `feast teardown`, and key expiry via the `key_ttl_seconds` option. Features that depend on Redis modules (such as vector search) are outside the scope of this page. + +## Using Valkey as a drop-in Feast online store instead of Redis + +Make sure you have Python and `pip` installed. + +Install the Feast SDK and CLI + +`pip install feast` + +In order to use Valkey as the online store, you'll need to install the redis extra: + +`pip install 'feast[redis]'` + +### 1. Create a feature repository + +Bootstrap a new feature repository: + +``` +feast init feast_valkey +cd feast_valkey/feature_repo +``` + +Update `feature_repo/feature_store.yaml` with the below contents: + +``` +project: feast_valkey +registry: data/registry.db +provider: local +online_store: + type: redis + connection_string: "localhost:6379" +``` + +Note that the online store `type` remains `redis`: Feast talks to Valkey over the Redis protocol, and all options of the [Redis online store](redis.md) (such as `key_ttl_seconds`) apply unchanged. + +### 2. Start Valkey + +There are several options available to get Valkey up and running quickly. We will be using Docker for this tutorial. + +`docker run -d -p 6379:6379 valkey/valkey:8.1` + +### 3. Register feature definitions and deploy your feature store + +`feast apply` + +The `apply` command scans python files in the current directory for feature view/entity definitions, registers the objects, and deploys infrastructure. +You should see the following output: + +``` +.... +Created entity driver +Created feature view driver_hourly_stats_fresh +Created feature view driver_hourly_stats +Created on demand feature view transformed_conv_rate +Created on demand feature view transformed_conv_rate_fresh +Created feature service driver_activity_v1 +Created feature service driver_activity_v3 +Created feature service driver_activity_v2 +``` + +## Functionality Matrix + +The set of functionality supported by online stores is described in detail [here](overview.md#functionality). +Below is a matrix indicating which functionality is supported by the Redis online store, which Feast uses to communicate with Valkey. + +| | Redis | +| :-------------------------------------------------------- | :---- | +| write feature values to the online store | yes | +| read feature values from the online store | yes | +| update infrastructure (e.g. tables) in the online store | yes | +| teardown infrastructure (e.g. tables) in the online store | yes | +| generate a plan of infrastructure changes | no | +| support for on-demand transforms | yes | +| readable by Python SDK | yes | +| readable by Java | yes | +| readable by Go | yes | +| support for entityless feature views | yes | +| support for concurrent writing to the same key | yes | +| support for ttl (time to live) at retrieval | yes | +| support for deleting expired data | yes | +| collocated by feature view | no | +| collocated by feature service | no | +| collocated by entity key | yes | + +To compare this set of functionality against other online stores, please see the full [functionality matrix](overview.md#functionality-matrix). diff --git a/docs/reference/openlineage.md b/docs/reference/openlineage.md new file mode 100644 index 00000000000..bf9e18750ed --- /dev/null +++ b/docs/reference/openlineage.md @@ -0,0 +1,478 @@ +# OpenLineage Integration + +This module provides **native integration** between Feast and [OpenLineage](https://openlineage.io/), enabling automatic data lineage tracking for ML feature engineering workflows. + +## Overview + +When enabled, the integration **automatically** emits OpenLineage events for: + +- **Registry changes** - Events when feature views, feature services, and entities are applied +- **Feature materialization** - START, COMPLETE, and FAIL events when features are materialized + +**No code changes required** - just enable OpenLineage in your `feature_store.yaml`! + +## Installation + +OpenLineage is an optional dependency. Install it with: + +```bash +pip install openlineage-python +``` + +Or install Feast with the OpenLineage extra: + +```bash +pip install feast[openlineage] +``` + +## Configuration + +Add the `openlineage` section to your `feature_store.yaml`: + +```yaml +project: my_project +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db + +openlineage: + enabled: true + transport_type: http + transport_url: http://localhost:5000 + transport_endpoint: api/v1/lineage + namespace: feast + emit_on_apply: true + emit_on_materialize: true +``` + +Once configured, all Feast operations will automatically emit lineage events. + +### Environment Variables + +You can also configure via environment variables: + +```bash +export FEAST_OPENLINEAGE_ENABLED=true +export FEAST_OPENLINEAGE_TRANSPORT_TYPE=http +export FEAST_OPENLINEAGE_URL=http://localhost:5000 +export FEAST_OPENLINEAGE_ENDPOINT=api/v1/lineage +export FEAST_OPENLINEAGE_NAMESPACE=feast +``` + +## Usage + +Once configured, lineage is tracked automatically: + +```python +from feast import FeatureStore +from datetime import datetime, timedelta + +# Create FeatureStore - OpenLineage is initialized automatically if configured +fs = FeatureStore(repo_path="feature_repo") + +# Apply operations emit lineage events automatically +fs.apply([driver_entity, driver_hourly_stats_view]) + +# Materialize emits START, COMPLETE/FAIL events automatically +fs.materialize( + start_date=datetime.now() - timedelta(days=1), + end_date=datetime.now() +) + +``` + +## Configuration Options + +| Option | Default | Description | +|--------|---------|-------------| +| `enabled` | `false` | Enable/disable OpenLineage integration | +| `transport_type` | `None` | Transport type: `http`, `console`, `file`, `kafka`. When unset, defers to OpenLineage SDK defaults. | +| `transport_url` | - | URL for HTTP transport (required) | +| `transport_endpoint` | `api/v1/lineage` | API endpoint for HTTP transport | +| `api_key` | - | Optional API key for authentication | +| `namespace` | `feast` | Namespace for lineage events (uses project name if set to "feast") | +| `producer` | `feast` | Producer identifier | +| `emit_on_apply` | `true` | Emit events on `feast apply` | +| `emit_on_materialize` | `true` | Emit events on materialization | + +## Lineage Graph Structure + +When you run `feast apply`, Feast creates a lineage graph that matches the Feast UI: + +``` +DataSources ──┐ + ├──→ feast_feature_views_{project} ──→ FeatureViews +Entities ─────┘ │ + │ + ▼ + feature_service_{name} ──→ FeatureService +``` + +**Jobs created:** +- `feast_feature_views_{project}`: Shows DataSources + Entities → FeatureViews +- `feature_service_{name}`: Shows specific FeatureViews → FeatureService (one per service) + +**Datasets include:** +- Schema with feature names, types, descriptions, and tags +- Feast-specific facets with metadata (TTL, entities, owner, etc.) +- Documentation facets with descriptions + +## Transport Types + +### HTTP Transport (Recommended for Production) + +```yaml +openlineage: + enabled: true + transport_type: http + transport_url: http://marquez:5000 + transport_endpoint: api/v1/lineage + api_key: your-api-key # Optional +``` + +### File Transport + +```yaml +openlineage: + enabled: true + transport_type: file + additional_config: + log_file_path: openlineage_events.json +``` + +### Kafka Transport + +```yaml +openlineage: + enabled: true + transport_type: kafka + additional_config: + bootstrap_servers: localhost:9092 + topic: openlineage.events +``` + +## Custom Feast Facets + +The integration includes custom Feast-specific facets in lineage events: + +### FeastFeatureViewFacet + +Captures metadata about feature views: +- `name`: Feature view name +- `ttl_seconds`: Time-to-live in seconds +- `entities`: List of entity names +- `features`: List of feature names +- `online_enabled` / `offline_enabled`: Store configuration +- `description`: Feature view description +- `tags`: Key-value tags + +### FeastFeatureServiceFacet + +Captures metadata about feature services: +- `name`: Feature service name +- `feature_views`: List of feature view names +- `feature_count`: Total number of features +- `description`: Feature service description +- `tags`: Key-value tags + +### FeastMaterializationFacet + +Captures materialization run metadata: +- `feature_views`: Feature views being materialized +- `start_date` / `end_date`: Materialization window +- `rows_written`: Number of rows written + +## Lineage Visualization + +### Option 1: Feast UI (Built-in) + +Feast includes a built-in OpenLineage consumer that can receive, store, and visualize lineage from **all** OpenLineage producers (Airflow, Spark, dbt, Feast itself, etc.) directly in the Feast UI. See the [OpenLineage Consumer](#openlineage-consumer) section below. + +### Option 2: Marquez + +Use [Marquez](https://marquezproject.ai/) to visualize your Feast lineage: + +```bash +# Start Marquez +docker run -p 5000:5000 -p 3000:3000 marquezproject/marquez + +# Configure Feast to emit to Marquez (in feature_store.yaml) +# openlineage: +# enabled: true +# transport_type: http +# transport_url: http://localhost:5000 +``` + +Then access the Marquez UI at http://localhost:3000 to see your feature lineage. + +## Namespace Behavior + +- If `namespace` is set to `"feast"` (default): Uses project name as namespace (e.g., `my_project`) +- If `namespace` is set to a custom value: Uses `{namespace}/{project}` (e.g., `custom/my_project`) + +## Feast to OpenLineage Mapping + +| Feast Concept | OpenLineage Concept | +|---------------|---------------------| +| DataSource | InputDataset | +| FeatureView | OutputDataset (of feature views job) / InputDataset (of feature service job) | +| Feature | Schema field | +| Entity | InputDataset | +| FeatureService | OutputDataset | +| Materialization | RunEvent (START/COMPLETE/FAIL) | + +--- + +## OpenLineage Consumer + +Feast can act as an **OpenLineage consumer**, receiving lineage events from any OpenLineage-compatible producer and displaying them in the Feast UI. This eliminates the need for a separate Marquez deployment when you want to visualize cross-system data lineage alongside your feature store. + +### Consumer Architecture + +``` +Producers (Airflow, Spark, dbt, Feast, Flink, …) + │ + ▼ + POST /api/v1/lineage ──→ Event Processor ──→ Lineage Store (SQL) + │ + ▼ + Feast UI + ┌──────────────────────────┐ + │ Lineage tab │ + │ ├─ OpenLineage Graph │ + │ │ (all producers) │ + │ └─ ☐ Feast Only Lineage │ + │ (registry view) │ + │ │ + │ Events tab │ + │ └─ Event browser │ + └──────────────────────────┘ +``` + +When the consumer is **not** enabled, the Feast UI shows only the original registry-based lineage view — no tabs are added. + +### Enabling the Consumer + +Add the `consumer` section under `openlineage` in your `feature_store.yaml`: + +```yaml +project: my_project +registry: + registry_type: sql + path: postgresql://user:****@host:5432/feast # pragma: allowlist secret + +openlineage: + enabled: true + namespace: my_project + consumer: + enabled: true + store_type: sql + # Optional: separate database for lineage storage. + # If omitted, the SQL registry database is reused. + # connection_string: postgresql://user:****@host:5432/feast_lineage + api_key: "change-me" # pragma: allowlist secret + namespace_mapping: + airflow_ns: my_project + spark_ns: my_project +``` + +Or via environment variables: + +```bash +export FEAST_OPENLINEAGE_CONSUMER_ENABLED=true +export FEAST_OPENLINEAGE_CONSUMER_STORE_TYPE=sql +export FEAST_OPENLINEAGE_CONSUMER_API_KEY=change-me # pragma: allowlist secret +# Optional separate DB: +# export FEAST_OPENLINEAGE_CONSUMER_CONNECTION_STRING=postgresql://... +``` + +### Consumer Configuration Options + +| Option | Default | Description | +|--------|---------|-------------| +| `consumer.enabled` | `false` | Enable the OpenLineage consumer | +| `consumer.store_type` | `sql` | Storage backend type. Currently only `sql` is supported | +| `consumer.connection_string` | - | Optional separate database connection string. If omitted, reuses the SQL registry database | +| `consumer.api_key` | - | API key that producers must provide when sending events | +| `consumer.namespace_mapping` | `{}` | Maps OpenLineage namespaces to Feast projects for RBAC scoping | + +### Consumer API Endpoints + +When the consumer is enabled, the following endpoints are available on the Feast REST registry server: + +#### Event Receiver (Producer-facing) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/lineage` | `POST` | Receive a single OpenLineage event (or array of events) | +| `/api/v1/lineage/batch` | `POST` | Receive a batch of OpenLineage events | + +Both endpoints require the `X-API-Key` header (or `Authorization: Bearer `) if `consumer.api_key` is configured. + +#### Admin Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/lineage/openlineage/reset` | `DELETE` | Purge all OpenLineage data. Accepts optional `?namespace=X` to delete only a specific namespace. Requires API key. | + +#### OpenLineage Query Endpoints (UI-facing) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/lineage/openlineage/graph` | `GET` | Full lineage graph with all nodes, edges, and symlinks | +| `/lineage/openlineage/graph/{node_type}/{namespace}/{name}` | `GET` | Lineage graph centered on a specific node | +| `/lineage/openlineage/events` | `GET` | Browse stored events with filtering | +| `/lineage/openlineage/jobs` | `GET` | List all known OpenLineage jobs | +| `/lineage/openlineage/datasets` | `GET` | List all known OpenLineage datasets | +| `/lineage/openlineage/runs` | `GET` | List runs with optional `?job_namespace=X&job_name=Y` filtering | +| `/lineage/openlineage/runs/{run_id}` | `GET` | Single run detail with input/output datasets | + +#### Registry Query Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/lineage/registry` | `GET` | Feast registry lineage (entities, feature views, services) | +| `/lineage/registry/all` | `GET` | All registry objects with full metadata | +| `/lineage/objects/{object_type}/{object_name}` | `GET` | Detail for a specific registry object | +| `/lineage/complete` | `GET` | Complete registry lineage with relationships | +| `/lineage/complete/all` | `GET` | Complete registry lineage for all objects | + +### Configuring Producers to Send Events to Feast + +Configure any OpenLineage producer to send events to your Feast instance: + +#### Airflow + +```python +# In airflow.cfg or environment +OPENLINEAGE_URL = "http://feast-registry:8080/api" +OPENLINEAGE_API_KEY = "change-me" # pragma: allowlist secret +``` + +#### Spark + +```properties +spark.openlineage.transport.type=http +spark.openlineage.transport.url=http://feast-registry:8080/api +spark.openlineage.transport.endpoint=/v1/lineage +spark.openlineage.transport.auth.type=api_key +spark.openlineage.transport.auth.apiKey=change-me +``` + +#### dbt + +```yaml +# In profiles.yml or environment +OPENLINEAGE_URL: "http://feast-registry:8080/api" +OPENLINEAGE_API_KEY: "change-me" # pragma: allowlist secret +``` + +#### Feast (Self-reporting) + +When both the OpenLineage producer and consumer are enabled, Feast's own events (from `feast apply`, materialization, etc.) are automatically ingested into the local consumer store — no HTTP transport is needed. + +```yaml +# In feature_store.yaml +openlineage: + enabled: true + namespace: my_project + consumer: + enabled: true + api_key: change-me # pragma: allowlist secret +``` + +### Feast UI Lineage Views + +When the consumer is enabled, the lineage page in the Feast UI shows two tabs: + +**Lineage tab** + +- **OpenLineage Graph** (default) — shows lineage from all OpenLineage producers with cross-producer connectivity. Nodes are color-coded by producer (colors generated dynamically). The graph supports filtering by type, producer, and object name. Clicking a node opens a **detail panel** showing description, schema, tags, features, entities, data quality metrics, data source info, other facets, and **run history** (for job nodes — see [Per-Run Lineage](#per-run-lineage-run-history)). +- **Feast Only Lineage** (checkbox) — switches to the original Feast registry view (DataSource → FeatureView → FeatureService) powered entirely by the Feast registry. + +**Events tab** + +- Browse individual OpenLineage events with filtering by event type, job name, and run ID. Expand any event to inspect the full JSON payload. + +### Cross-Producer Lineage Connectivity + +The consumer automatically links datasets across different producers when they refer to the same physical data. Linking mechanisms: + +1. **Shared namespace + name** — If Airflow writes to `s3://bucket/path` and Spark reads from the same `s3://bucket/path`, the graph connects them automatically. +2. **SymlinksDatasetFacet** — Producers can declare aliases. For example, Feast can declare that its internal `driver_hourly_stats` is a symlink to the Spark output at `s3://bucket/features/driver_hourly_stats/`. +3. **dataSource URI matching** — Datasets with matching `dataSource.uri` facets are linked even if their namespace or name differ. + +Compatible producers include Airflow, Spark, dbt, Flink, Feast, and Dagster. + +### RBAC for Lineage + +The OpenLineage consumer integrates with Feast's existing RBAC: + +- **Write access** (producers sending events): Authenticated via API key in the `X-API-Key` header +- **Read access** (UI viewing lineage): Namespace-based filtering maps OpenLineage namespaces to Feast projects. Users see only lineage data for namespaces they have access to via the `namespace_mapping` configuration + +### Lineage Cleanup / Reset + +Over time the OpenLineage store accumulates historical data. Two mechanisms are provided for cleanup: + +#### Admin Reset Endpoint + +Use the `DELETE /lineage/openlineage/reset` endpoint to purge lineage data. The endpoint requires the same API key used for event ingestion. + +```bash +# Purge ALL OpenLineage data +curl -X DELETE -H "X-API-Key: your-key" \ + http://localhost:8080/api/v1/lineage/openlineage/reset + +# Purge only a specific namespace +curl -X DELETE -H "X-API-Key: your-key" \ + "http://localhost:8080/api/v1/lineage/openlineage/reset?namespace=airflow://prod-cluster" +``` + +A full purge deletes data from all seven `openlineage_*` tables. A namespace-scoped purge deletes jobs, datasets, runs, events, edges, and symlinks associated with that namespace, leaving other namespaces intact. + +#### Feast Teardown Hook + +When you run `feast teardown`, Feast automatically cleans up OpenLineage data for the project's namespace (if the consumer is configured). This ensures that tearing down a Feast project doesn't leave orphaned lineage data behind. + +```bash +# Tears down the Feast project AND its OpenLineage lineage +feast teardown +``` + +### Per-Run Lineage (Run History) + +The consumer tracks individual pipeline runs in the `openlineage_runs` table. When you click on a **job node** in the OpenLineage Graph, the detail panel shows a **Run History** section with: + +- A table of past runs: truncated run ID, status badge (COMPLETE, FAIL, RUNNING, ABORT), start time, and duration +- Click any run to expand its **inputs and outputs** — the specific datasets that run consumed and produced + +#### Run History API + +```bash +# List runs for a specific job +curl "http://localhost:8080/api/v1/lineage/openlineage/runs?job_namespace=spark://emr-cluster&job_name=feature_engineering" + +# Get a single run with its I/O datasets +curl "http://localhost:8080/api/v1/lineage/openlineage/runs/{run_id}" +``` + +The run detail response includes `inputs` and `outputs` arrays, each containing the dataset namespace, name, and any I/O facets recorded by the producer. + +### Database Schema + +The consumer creates the following tables (automatically on first startup): + +| Table | Purpose | +|-------|---------| +| `openlineage_events` | Raw event storage with JSON payloads | +| `openlineage_jobs` | Deduplicated job records with producer, description, and facets | +| `openlineage_datasets` | Deduplicated dataset records with schema, facets, and Feast mapping | +| `openlineage_runs` | Run lifecycle tracking (START/COMPLETE/FAIL) | +| `openlineage_run_io` | Input/output relationships between runs and datasets | +| `openlineage_lineage_edges` | Materialized lineage graph edges for efficient traversal | +| `openlineage_dataset_symlinks` | Cross-producer dataset linking via `SymlinksDatasetFacet` and `dataSource` URI matching | + +By default these tables are created in the **same database** as the SQL registry (hybrid storage). Set `consumer.connection_string` to store them in a separate database instead. diff --git a/docs/reference/registries/metadata.md b/docs/reference/registries/metadata.md index 575f2a5c8b7..371be9b5289 100644 --- a/docs/reference/registries/metadata.md +++ b/docs/reference/registries/metadata.md @@ -20,6 +20,7 @@ The metadata info of Feast `feature_store.yaml` is: | registry.warehouse | N | string | snowflake warehouse name | | registry.database | N | string | snowflake db name | | registry.schema | N | string | snowflake schema name | +| registry.enable_online_feature_view_versioning | N | boolean | enable versioned online store tables and version-qualified reads (default: false). Version history tracking is always active. | | online_store | Y | | | | offline_store | Y | NA | | | | offline_store.type | Y | string | storage type | diff --git a/docs/reference/registries/remote.md b/docs/reference/registries/remote.md index a03e30ac85f..64055304283 100644 --- a/docs/reference/registries/remote.md +++ b/docs/reference/registries/remote.md @@ -18,7 +18,27 @@ registry: {% endcode %} The optional `cert` parameter can be configured as well, it should point to the public certificate path when the Registry Server starts in SSL mode. This may be needed if the Registry Server is started with a self-signed certificate, typically this file ends with *.crt, *.cer, or *.pem. -More info about the `cert` parameter can be found in [feast-client-connecting-to-remote-registry-sever-started-in-tls-mode](../../how-to-guides/starting-feast-servers-tls-mode.md#feast-client-connecting-to-remote-registry-sever-started-in-tls-mode) + +For **mutual TLS (mTLS)**, you can also configure: +* `client_cert` — Path to the client certificate presented to the server. Must be paired with `client_key`. Typically ends with `*.crt` or `*.pem`. +* `client_key` — Path to the client private key. Must be paired with `client_cert`. Typically ends with `*.key` or `*.pem`. + +When connecting through a tunnel or proxy where the connection address differs from the server hostname, set: +* `authority` — Overrides the gRPC `:authority` header so the server certificate is validated against the correct hostname. + +{% code title="feature_store.yaml" %} +```yaml +registry: + registry_type: remote + path: localhost:8443 + cert: /path/to/ca.crt + client_cert: /path/to/tls.crt + client_key: /path/to/tls.key + authority: feature-registry.example.com +``` +{% endcode %} + +More info about TLS configuration can be found in [feast-client-connecting-to-remote-registry-sever-started-in-tls-mode](../../how-to-guides/starting-feast-servers-tls-mode.md#feast-client-connecting-to-remote-registry-sever-started-in-tls-mode) ## How to configure the server diff --git a/docs/reference/registries/sql.md b/docs/reference/registries/sql.md index ef9993c8753..e8d1bcef17a 100644 --- a/docs/reference/registries/sql.md +++ b/docs/reference/registries/sql.md @@ -80,10 +80,114 @@ docker build \ If you are running Feast in Kubernetes, set the `image.repository` and `imagePullSecrets` Helm values accordingly to utilize your custom image. +## Schema management (`schema_mode`) + +By default, the SQL registry creates its tables on every startup (`schema_mode: auto`). In production environments where the application should not have DDL privileges, you can pre-create the schema and configure the registry to only verify it: + +```yaml +registry: + registry_type: sql + path: postgresql://db:5432/feast + schema_mode: verify # or "skip" +``` + +| Value | Behavior | +|---|---| +| `auto` (default) | Creates tables if they don't exist. Current behavior, no breaking change. | +| `verify` | Skips DDL. Checks that all expected tables exist on startup; raises an error listing missing tables if any are absent. When a separate `read_path` is configured, the read replica is also verified — a lagging replica (e.g. mid-migration) will block startup. Note: this is a table-level check only — it does not verify individual columns. A schema created by an older Feast version (missing newer columns) will pass verification but may fail at query time. | +| `skip` | Skips both creation and verification. Use when schema is managed entirely outside Feast (e.g. by a migration tool). | + +### Pre-creating the schema + +When using `verify` or `skip` mode, run the following CLI command with a user that has DDL privileges to create the schema before starting the application: + +```shell +feast registry create-schema +``` + +This reads `feature_store.yaml`, connects to the configured database, and creates all required tables. It is safe to run multiple times — existing tables are not modified. + There are some things to note about how the SQL registry works: -- Once instantiated, the Registry ensures the tables needed to store data exist, and creates them if they do not. -- Upon tearing down the feast project, the registry ensures that the tables are dropped from the database. -- The schema for how data is laid out in tables can be found . It is intentionally simple, storing the serialized protobuf versions of each Feast object keyed by its name. +- When `schema_mode` is `auto` (the default), the Registry ensures the tables needed to store data exist, and creates them if they do not. +- Upon tearing down the feast project, the registry deletes all rows from the registry tables (it does not drop the tables themselves). This runs regardless of `schema_mode` and requires only DML (`DELETE`) privileges, not DDL. +- The schema for how data is laid out in tables can be found in the table definitions in [`sdk/python/feast/infra/registry/sql.py`](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/infra/registry/sql.py). It is intentionally simple, storing the serialized protobuf versions of each Feast object keyed by its name. + +## MySQL: serialized-proto columns use `LONGBLOB` + +The registry stores each Feast object as a serialized protobuf in a binary +column. On MySQL these columns are created as `LONGBLOB` (up to 4 GB). Earlier +versions created them as `BLOB`, which caps at 64 KB — a single `FeatureView` +proto routinely exceeds that, so MySQL would silently truncate the write and the +registry would later fail to load with a protobuf `DecodeError` (for example, +`feast serve` failing to start). Other dialects (PostgreSQL, SQLite) were never +affected. + +New deployments get the correct schema automatically — the registry creates its +tables as `LONGBLOB` on first use. When an existing MySQL/MariaDB registry still +has `BLOB` columns, the registry logs an error at startup listing the affected +columns (it does not refuse to start — a registry whose protos all fit in 64 KB +is unaffected). **Existing deployments are not migrated automatically**: the +registry only creates tables that do not already exist, and it has no +schema-migration step, so previously created `BLOB` columns remain `BLOB`. To +upgrade an existing MySQL registry, alter each serialized-proto column to +`LONGBLOB`, for example: + +> ⚠️ **Run the migration carefully on a live registry.** A `BLOB`→`LONGBLOB` +> change is a column *data-type* change, which MySQL InnoDB performs with +> `ALGORITHM=COPY` — a full table rebuild under a metadata lock that blocks +> readers and writers for the duration (potentially minutes on a large table +> such as `feature_view_version_history`). `ALGORITHM=INPLACE` is **not** +> generally supported for this change and is rejected with +> `ER_ALTER_OPERATION_NOT_SUPPORTED_REASON` on most builds — do not rely on it. +> +> **Before running any `ALTER TABLE`:** +> +> 1. **Stop all `feast apply` and materialization jobs.** This is required, not +> optional — a write of a `>64 KB` proto to a not-yet-widened `BLOB` column +> truncates silently with no error, and concurrent writes also extend the +> `ALTER`'s lock duration. +> 2. Confirm there are no active writers (e.g. `SHOW PROCESSLIST`). +> 3. Verify you have a backup of the registry database. +> +> Then, to minimize the lock window: +> +> - On large tables, or on managed MySQL (AWS RDS, Aurora) without shell access, +> use an online schema-change tool — +> [`pt-online-schema-change`](https://docs.percona.com/percona-toolkit/pt-online-schema-change.html) +> (Percona Toolkit) or [`gh-ost`](https://github.com/github/gh-ost) — which +> rebuild the table without a long-held lock. For small tables a plain +> `ALTER TABLE` in the maintenance window is fine. +> - Apply one table at a time so a failure is easy to isolate and re-run. +> - Resume jobs only after all `ALTER TABLE` statements complete successfully. +> - Rollback is safe (revert `MODIFY ... BLOB`) **only** while no stored proto +> exceeds 64 KB; otherwise a revert re-introduces truncation. + +```sql +ALTER TABLE projects MODIFY project_proto LONGBLOB NOT NULL; +ALTER TABLE entities MODIFY entity_proto LONGBLOB NOT NULL; +ALTER TABLE data_sources MODIFY data_source_proto LONGBLOB NOT NULL; +ALTER TABLE feature_views MODIFY materialized_intervals LONGBLOB, + MODIFY feature_view_proto LONGBLOB NOT NULL, + MODIFY user_metadata LONGBLOB; +ALTER TABLE stream_feature_views MODIFY feature_view_proto LONGBLOB NOT NULL, + MODIFY user_metadata LONGBLOB; +ALTER TABLE on_demand_feature_views MODIFY feature_view_proto LONGBLOB NOT NULL, + MODIFY user_metadata LONGBLOB; +ALTER TABLE label_views MODIFY feature_view_proto LONGBLOB NOT NULL, + MODIFY user_metadata LONGBLOB; +ALTER TABLE feature_services MODIFY feature_service_proto LONGBLOB NOT NULL; +ALTER TABLE saved_datasets MODIFY saved_dataset_proto LONGBLOB NOT NULL; +ALTER TABLE validation_references MODIFY validation_reference_proto LONGBLOB NOT NULL; +ALTER TABLE managed_infra MODIFY infra_proto LONGBLOB NOT NULL; +ALTER TABLE permissions MODIFY permission_proto LONGBLOB NOT NULL; +-- LARGE TABLE: one row per versioned apply — likely the slowest ALTER. Use +-- pt-online-schema-change or gh-ost if this registry has significant history. +ALTER TABLE feature_view_version_history MODIFY feature_view_proto LONGBLOB NOT NULL; +``` + +Any object whose proto already exceeded 64 KB before the upgrade may have been +stored truncated; re-run `feast apply` for those objects after altering the +columns so the full proto is rewritten. ## Example Usage: Concurrent materialization The SQL Registry should be used when materializing feature views concurrently to ensure correctness of data in the registry. This can be achieved by simply running feast materialize or feature_store.materialize multiple times using a correctly configured feature_store.yaml. This will make each materialization process talk to the registry database concurrently, and ensure the metadata updates are serialized. diff --git a/docs/reference/type-system.md b/docs/reference/type-system.md index affe394f570..97cc6036dc8 100644 --- a/docs/reference/type-system.md +++ b/docs/reference/type-system.md @@ -3,12 +3,537 @@ ## Motivation Feast uses an internal type system to provide guarantees on training and serving data. -Feast currently supports eight primitive types - `INT32`, `INT64`, `FLOAT32`, `FLOAT64`, `STRING`, `BYTES`, `BOOL`, and `UNIX_TIMESTAMP` - and the corresponding array types. +Feast supports primitive types, array types, set types, map types, JSON, and struct types for feature values. Null types are not supported, although the `UNIX_TIMESTAMP` type is nullable. The type system is controlled by [`Value.proto`](https://github.com/feast-dev/feast/blob/master/protos/feast/types/Value.proto) in protobuf and by [`types.py`](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/types.py) in Python. Type conversion logic can be found in [`type_map.py`](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/type_map.py). -## Examples +## Supported Types + +Feast supports the following data types: + +### Primitive Types + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `Int32` | `int` | 32-bit signed integer | +| `Int64` | `int` | 64-bit signed integer | +| `Float32` | `float` | 32-bit floating point | +| `Float64` | `float` | 64-bit floating point | +| `String` | `str` | String/text value | +| `Bytes` | `bytes` | Binary data | +| `Bool` | `bool` | Boolean value | +| `UnixTimestamp` | `datetime` | Unix timestamp (nullable) | +| `ZonedTimestamp` | `datetime` | Timezone-aware datetime preserving its source zone (nullable) | +| `Uuid` | `uuid.UUID` | UUID (any version) | +| `TimeUuid` | `uuid.UUID` | Time-based UUID (version 1) | +| `Decimal` | `decimal.Decimal` | Arbitrary-precision decimal number | + +### Domain-Specific Primitive Types + +These types are semantic aliases over `Bytes` for domain-specific use cases (e.g., RAG pipelines, image processing). They are stored as `bytes` at the proto level. + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `PdfBytes` | `bytes` | PDF document binary data (used in RAG / document processing pipelines) | +| `ImageBytes` | `bytes` | Image binary data (used in image processing / multimodal pipelines) | + +{% hint style="warning" %} +`PdfBytes` and `ImageBytes` are not natively supported by any backend's type inference. You must explicitly declare them in your feature view schema. Backend storage treats them as raw `bytes`. +{% endhint %} + +### Array Types + +All primitive types have corresponding array (list) types: + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `Array(Int32)` | `List[int]` | List of 32-bit integers | +| `Array(Int64)` | `List[int]` | List of 64-bit integers | +| `Array(Float32)` | `List[float]` | List of 32-bit floats | +| `Array(Float64)` | `List[float]` | List of 64-bit floats | +| `Array(String)` | `List[str]` | List of strings | +| `Array(Bytes)` | `List[bytes]` | List of binary data | +| `Array(Bool)` | `List[bool]` | List of booleans | +| `Array(UnixTimestamp)` | `List[datetime]` | List of timestamps | +| `Array(Uuid)` | `List[uuid.UUID]` | List of UUIDs | +| `Array(TimeUuid)` | `List[uuid.UUID]` | List of time-based UUIDs | +| `Array(Decimal)` | `List[decimal.Decimal]` | List of arbitrary-precision decimals | + +### Set Types + +All primitive types (except `Map` and `Json`) have corresponding set types for storing unique values: + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `Set(Int32)` | `Set[int]` | Set of unique 32-bit integers | +| `Set(Int64)` | `Set[int]` | Set of unique 64-bit integers | +| `Set(Float32)` | `Set[float]` | Set of unique 32-bit floats | +| `Set(Float64)` | `Set[float]` | Set of unique 64-bit floats | +| `Set(String)` | `Set[str]` | Set of unique strings | +| `Set(Bytes)` | `Set[bytes]` | Set of unique binary data | +| `Set(Bool)` | `Set[bool]` | Set of unique booleans | +| `Set(UnixTimestamp)` | `Set[datetime]` | Set of unique timestamps | +| `Set(Uuid)` | `Set[uuid.UUID]` | Set of unique UUIDs | +| `Set(TimeUuid)` | `Set[uuid.UUID]` | Set of unique time-based UUIDs | +| `Set(Decimal)` | `Set[decimal.Decimal]` | Set of unique arbitrary-precision decimals | + +**Note:** Set types automatically remove duplicate values. When converting from lists or other iterables to sets, duplicates are eliminated. + +{% hint style="warning" %} +**Backend limitations for Set types:** + +- **No backend infers Set types from schema.** No offline store (BigQuery, Snowflake, Redshift, PostgreSQL, Spark, Athena, MSSQL) maps its native types to Feast Set types. You **must** explicitly declare Set types in your feature view schema. +- **No native PyArrow set type.** Feast converts Sets to `pyarrow.list_()` internally, but `feast_value_type_to_pa()` in `type_map.py` does not include Set mappings, which can cause errors in some code paths. +- **Online stores** that serialize proto bytes (e.g., SQLite, Redis, DynamoDB) handle Sets correctly. +- **Offline stores** may not handle Set types correctly during retrieval. For example, the Ray offline store only special-cases `_LIST` types, not `_SET`. +- Set types are best suited for **online serving** use cases where feature values are written as Python sets and retrieved via `get_online_features`. +{% endhint %} + +### Nested Collection Types + +Feast supports arbitrarily nested collections using a recursive `VALUE_LIST` / `VALUE_SET` design. The outer container determines the proto enum (`VALUE_LIST` for `Array(…)`, `VALUE_SET` for `Set(…)`), while the full inner type structure is persisted via a mandatory `feast:nested_inner_type` Field tag. + +| Feast Type | Python Type | ValueType | Description | +|------------|-------------|-----------|-------------| +| `Array(Array(T))` | `List[List[T]]` | `VALUE_LIST` | List of lists | +| `Array(Set(T))` | `List[List[T]]` | `VALUE_LIST` | List of sets | +| `Set(Array(T))` | `List[List[T]]` | `VALUE_SET` | Set of lists | +| `Set(Set(T))` | `List[List[T]]` | `VALUE_SET` | Set of sets | +| `Array(Array(Array(T)))` | `List[List[List[T]]]` | `VALUE_LIST` | 3-level nesting | + +Where `T` is any supported primitive type (Int32, Int64, Float32, Float64, String, Bytes, Bool, UnixTimestamp) or another nested collection type. + +**Notes:** +- Nesting depth is **unlimited**. `Array(Array(Array(T)))`, `Set(Array(Set(T)))`, etc. are all supported. +- Inner type information is preserved via Field tags (`feast:nested_inner_type`) and restored during deserialization. This tag is mandatory for nested collection types. +- Empty inner collections (`[]`) are stored as empty proto values and round-trip as `None`. For example, `[[1, 2], [], [3]]` becomes `[[1, 2], None, [3]]` after a write-read cycle. + +### Map Types + +Map types allow storing dictionary-like data structures: + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `Map` | `Dict[str, Any]` | Dictionary with string keys and values of any supported Feast type (including nested maps) | +| `Array(Map)` | `List[Dict[str, Any]]` | List of dictionaries | +| `ScalarMap` | `Dict[Any, Any]` | Dictionary with non-string scalar keys (int, float, bool, UUID, Decimal, bytes, datetime) and values of any supported Feast type | + +**Note:** `Map` keys must always be strings. `ScalarMap` supports non-string scalar keys — Feast infers `ScalarMap` automatically when the first key of a dict is not a string. Map values can be any supported Feast type, including primitives, arrays, or nested maps at the proto level. However, the PyArrow representation is `map`, which means backends that rely on PyArrow schemas (e.g., during materialization) treat Map as string-to-string. + +{% hint style="warning" %} +`ScalarMap` is **not** inferred from any backend schema. You must declare it explicitly in your feature view schema. It is best suited for online serving use cases where the online store serializes proto bytes directly (e.g., Redis, DynamoDB, SQLite). +{% endhint %} + +**Backend support for Map:** + +| Backend | Native Type | Notes | +|---------|-------------|-------| +| PostgreSQL | `jsonb`, `jsonb[]` | `jsonb` → `Map`, `jsonb[]` → `Array(Map)` | +| Snowflake | `VARIANT`, `OBJECT` | Inferred as `Map` | +| Redshift | `SUPER` | Inferred as `Map` | +| Spark | `map` | `map<>` → `Map`, `array>` → `Array(Map)` | +| Athena | `map` | Inferred as `Map` | +| MSSQL | `nvarchar(max)` | Serialized as string | +| DynamoDB / Redis | Proto bytes | Full proto Map and ScalarMap support | + +### JSON Type + +The `Json` type represents opaque JSON data. Unlike `Map`, which is schema-free key-value storage, `Json` is stored as a string at the proto level but backends use native JSON types where available. + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `Json` | `str` (JSON-encoded) | JSON data stored as a string at the proto level | +| `Array(Json)` | `List[str]` | List of JSON strings | + +**Backend support for Json:** + +| Backend | Native Type | +|---------|-------------| +| PostgreSQL | `jsonb` | +| Snowflake | `JSON` / `VARIANT` | +| Redshift | `json` | +| BigQuery | `JSON` | +| Spark | Not natively distinguished from `String` | +| MSSQL | `nvarchar(max)` | + +{% hint style="info" %} +When a backend's native type is ambiguous (e.g., PostgreSQL `jsonb` could be `Map` or `Json`), **the schema-declared Feast type takes precedence**. The backend-to-Feast mappings are only used during schema inference when no explicit type is provided. +{% endhint %} + +### Struct Type + +The `Struct` type represents a schema-aware structured type with named, typed fields. Unlike `Map` (which is schema-free), a `Struct` declares its field names and their types, enabling schema validation. + +| Feast Type | Python Type | Description | +|------------|-------------|-------------| +| `Struct({"field": Type, ...})` | `Dict[str, Any]` | Named fields with typed values | +| `Array(Struct({"field": Type, ...}))` | `List[Dict[str, Any]]` | List of structs | + +**Example:** +```python +from feast.types import Struct, String, Int32, Array + +# Struct with named, typed fields +address_type = Struct({"street": String, "city": String, "zip": Int32}) +Field(name="address", dtype=address_type) + +# Array of structs +items_type = Array(Struct({"name": String, "quantity": Int32})) +Field(name="order_items", dtype=items_type) +``` + +**Backend support for Struct:** + +| Backend | Native Type | +|---------|-------------| +| BigQuery | `STRUCT` / `RECORD` | +| Spark | `struct<...>` / `array>` | +| PostgreSQL | `jsonb` (serialized) | +| Snowflake | `VARIANT` (serialized) | +| MSSQL | `nvarchar(max)` (serialized) | +| DynamoDB / Redis | Proto bytes | + +## Complete Feature View Example + +Below is a complete example showing how to define a feature view with all supported types: + +```python +from datetime import timedelta +from feast import Entity, FeatureView, Field, FileSource +from feast.types import ( + Int32, Int64, Float32, Float64, String, Bytes, Bool, UnixTimestamp, + Uuid, TimeUuid, Decimal, Array, Set, Map, ScalarMap, Json, Struct, + ZonedTimestamp +) + +# Define a data source +user_features_source = FileSource( + path="data/user_features.parquet", + timestamp_field="event_timestamp", +) + +# Define an entity +user = Entity( + name="user_id", + description="User identifier", +) + +# Define a feature view with all supported types +user_features = FeatureView( + name="user_features", + entities=[user], + ttl=timedelta(days=1), + schema=[ + # Primitive types + Field(name="age", dtype=Int32), + Field(name="account_balance", dtype=Int64), + Field(name="transaction_amount", dtype=Float32), + Field(name="credit_score", dtype=Float64), + Field(name="username", dtype=String), + Field(name="profile_picture", dtype=Bytes), + Field(name="is_active", dtype=Bool), + Field(name="last_login", dtype=UnixTimestamp), + Field(name="event_time", dtype=ZonedTimestamp), + Field(name="session_id", dtype=Uuid), + Field(name="event_id", dtype=TimeUuid), + Field(name="price", dtype=Decimal), + + # Array types + Field(name="daily_steps", dtype=Array(Int32)), + Field(name="transaction_history", dtype=Array(Int64)), + Field(name="ratings", dtype=Array(Float32)), + Field(name="portfolio_values", dtype=Array(Float64)), + Field(name="favorite_items", dtype=Array(String)), + Field(name="document_hashes", dtype=Array(Bytes)), + Field(name="notification_settings", dtype=Array(Bool)), + Field(name="login_timestamps", dtype=Array(UnixTimestamp)), + Field(name="related_session_ids", dtype=Array(Uuid)), + Field(name="event_chain", dtype=Array(TimeUuid)), + Field(name="historical_prices", dtype=Array(Decimal)), + + # Set types (unique values only — see backend caveats above) + Field(name="visited_pages", dtype=Set(String)), + Field(name="unique_categories", dtype=Set(Int32)), + Field(name="tag_ids", dtype=Set(Int64)), + Field(name="preferred_languages", dtype=Set(String)), + Field(name="unique_device_ids", dtype=Set(Uuid)), + Field(name="unique_event_ids", dtype=Set(TimeUuid)), + Field(name="unique_prices", dtype=Set(Decimal)), + + # Map types + Field(name="user_preferences", dtype=Map), + Field(name="metadata", dtype=Map), + Field(name="activity_log", dtype=Array(Map)), + Field(name="event_counts", dtype=ScalarMap), # non-string keys, e.g. {1001: 5, 1002: 12} + + # Nested collection types + Field(name="weekly_scores", dtype=Array(Array(Float64))), + Field(name="unique_tags_per_category", dtype=Array(Set(String))), + + # JSON type + Field(name="raw_event", dtype=Json), + + # Struct type + Field(name="address", dtype=Struct({"street": String, "city": String, "zip": Int32})), + Field(name="order_items", dtype=Array(Struct({"name": String, "qty": Int32}))), + ], + source=user_features_source, +) +``` + +### Set Type Usage Examples + +Sets store unique values and automatically remove duplicates: + +```python +# Simple set +visited_pages = {"home", "products", "checkout", "products"} # "products" appears twice +# Feast will store this as: {"home", "products", "checkout"} + +# Integer set +unique_categories = {1, 2, 3, 2, 1} # duplicates will be removed +# Feast will store this as: {1, 2, 3} + +# Converting a list with duplicates to a set +tag_list = [100, 200, 300, 100, 200] +tag_ids = set(tag_list) # {100, 200, 300} +``` + +### UUID Type Usage Examples + +UUID types store universally unique identifiers natively, with support for both random UUIDs and time-based UUIDs: + +```python +import uuid + +# Random UUID (version 4) — use Uuid type +session_id = uuid.uuid4() # e.g., UUID('a8098c1a-f86e-11da-bd1a-00112444be1e') + +# Time-based UUID (version 1) — use TimeUuid type +event_id = uuid.uuid1() # e.g., UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') + +# UUID values are returned as uuid.UUID objects from get_online_features() +response = store.get_online_features( + features=["user_features:session_id"], + entity_rows=[{"user_id": 1}], +) +result = response.to_dict() +# result["session_id"][0] is a uuid.UUID object + +# UUID lists +related_sessions = [uuid.uuid4(), uuid.uuid4(), uuid.uuid4()] + +# UUID sets (unique values) +unique_devices = {uuid.uuid4(), uuid.uuid4()} +``` + +### Decimal Type Usage Examples + +The `Decimal` type stores arbitrary-precision decimal numbers using Python's `decimal.Decimal`. +Values are stored as strings in the proto to preserve full precision — no floating-point rounding occurs. + +```python +import decimal + +# Scalar decimal — e.g., a financial price +price = decimal.Decimal("19.99") + +# High-precision value — all digits preserved +tax_rate = decimal.Decimal("0.08750000000000000000") + +# Decimal values are returned as decimal.Decimal objects from get_online_features() +response = store.get_online_features( + features=["product_features:price"], + entity_rows=[{"product_id": 42}], +) +result = response.to_dict() +# result["price"][0] is a decimal.Decimal object + +# Decimal lists — e.g., a history of prices +historical_prices = [ + decimal.Decimal("18.50"), + decimal.Decimal("19.00"), + decimal.Decimal("19.99"), +] + +# Decimal sets — unique price points seen +unique_prices = {decimal.Decimal("9.99"), decimal.Decimal("19.99"), decimal.Decimal("29.99")} +``` + +{% hint style="warning" %} +`Decimal` is **not** inferred from any backend schema. You must declare it explicitly in your feature view schema. The pandas dtype for `Decimal` columns is `object` (holding `decimal.Decimal` instances), not a numeric dtype. +{% endhint %} + +### ZonedTimestamp Type Usage Examples + +The `ZonedTimestamp` type stores a timezone-aware `datetime` as both the UTC instant +and its originating zone, so the original wall-clock zone round-trips losslessly. +By contrast, `UnixTimestamp` always decodes to UTC and discards the source zone. + +```python +from datetime import datetime, timezone +from zoneinfo import ZoneInfo + +# A datetime in a specific zone — both the instant and "America/Los_Angeles" are kept +event_time = datetime(2026, 6, 17, 9, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles")) + +# ZonedTimestamp values are returned as tz-aware datetime objects, in their own zone +response = store.get_online_features( + features=["event_features:event_time"], + entity_rows=[{"user_id": 1001}], +) +result = response.to_dict() +# result["event_time"][0] == event_time (same instant AND same zone, e.g. 09:00-07:00) + +# Two values at the same instant but different zones stay distinct +la = datetime(2026, 6, 17, 9, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles")) +utc = datetime(2026, 6, 17, 16, 0, 0, tzinfo=timezone.utc) # same instant as `la` + +# A naive (tz-less) datetime is interpreted as UTC +naive = datetime(2026, 6, 17, 12, 0, 0) # stored zone is empty, decoded as UTC +``` + +{% hint style="warning" %} +`ZonedTimestamp` is **not** inferred from any backend schema — you must declare it +explicitly in your feature view schema. It is not supported as an entity key. The +zone is stored as an IANA name (e.g. `America/Los_Angeles`) when available, falling +back to a fixed-offset string; offline stores that cannot natively carry a zone may +normalize to UTC on that backend. +{% endhint %} + +### Nested Collection Type Usage Examples + +```python +# List of lists — e.g., weekly score history per user +weekly_scores = [[85.0, 90.5, 78.0], [92.0, 88.5], [95.0, 91.0, 87.5]] + +# List of sets — e.g., unique tags assigned per category +unique_tags_per_category = [["python", "ml"], ["rust", "systems"], ["python", "web"]] + +# 3-level nesting — e.g., multi-dimensional matrices +Field(name="tensor", dtype=Array(Array(Array(Float64)))) + +# Mixed nesting +Field(name="grouped_tags", dtype=Array(Set(Array(String)))) +``` + +**Limitation:** Empty inner collections round-trip as `None`: +```python +# Input: [[1, 2], [], [3]] +# Output: [[1, 2], None, [3]] (empty [] becomes None after write-read cycle) +``` + +### Map Type Usage Examples + +Maps can store complex nested data structures: + +```python +# Simple map (string keys) +user_preferences = { + "theme": "dark", + "language": "en", + "notifications_enabled": True, + "font_size": 14 +} + +# Nested map +metadata = { + "profile": { + "bio": "Software engineer", + "location": "San Francisco" + }, + "stats": { + "followers": 1000, + "posts": 250 + } +} + +# List of maps +activity_log = [ + {"action": "login", "timestamp": "2024-01-01T10:00:00", "ip": "192.168.1.1"}, + {"action": "purchase", "timestamp": "2024-01-01T11:30:00", "amount": 99.99}, + {"action": "logout", "timestamp": "2024-01-01T12:00:00"} +] +``` + +### ScalarMap Type Usage Examples + +`ScalarMap` supports non-string keys. Feast infers it automatically when the first dict key is not a string: + +```python +import uuid +import decimal + +# Integer keys — e.g., category ID → item count +event_counts = {1001: 5, 1002: 12, 1003: 0} + +# UUID keys — e.g., session ID → score +import uuid +session_scores = { + uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"): 0.95, + uuid.UUID("a8098c1a-f86e-11da-bd1a-00112444be1e"): 0.87, +} + +# Decimal keys — e.g., price bucket → product name +price_tier = { + decimal.Decimal("9.99"): "budget", + decimal.Decimal("49.99"): "standard", + decimal.Decimal("99.99"): "premium", +} + +# Type inference: Feast automatically picks SCALAR_MAP when the key is non-string +from feast.type_map import python_type_to_feast_value_type +from feast.value_type import ValueType + +python_type_to_feast_value_type({1: "a"}) # → ValueType.SCALAR_MAP +python_type_to_feast_value_type({"a": 1}) # → ValueType.MAP +python_type_to_feast_value_type({}) # → ValueType.MAP (empty dict defaults to MAP) +``` + +{% hint style="warning" %} +`ScalarMap` must be **explicitly declared** in your feature view schema — it is never inferred from backend type schemas. It is best suited for online serving via stores that use proto byte serialization (e.g., Redis, DynamoDB, SQLite). Materialization paths that use PyArrow (e.g., BigQuery, Snowflake, Redshift, Spark) do not have native `ScalarMap` support. +{% endhint %} + +### JSON Type Usage Examples + +Feast's `Json` type stores values as JSON strings at the proto level. You can pass either a +pre-serialized JSON string or a Python dict/list — Feast will call `json.dumps()` automatically +when the value is not already a string: + +```python +import json + +# Option 1: pass a Python dict — Feast calls json.dumps() internally during proto conversion +raw_event = {"type": "click", "target": "button_1", "metadata": {"page": "home"}} + +# Option 2: pass an already-serialized JSON string — Feast validates it via json.loads() +raw_event = '{"type": "click", "target": "button_1", "metadata": {"page": "home"}}' + +# When building a DataFrame for store.push(), values must be strings since +# Pandas/PyArrow columns expect uniform types: +import pandas as pd +event_df = pd.DataFrame({ + "user_id": ["user_1"], + "event_timestamp": [datetime.now()], + "raw_event": [json.dumps({"type": "click", "target": "button_1"})], +}) +store.push("event_push_source", event_df) +``` + +### Struct Type Usage Examples + +```python +# Struct — schema-aware, fields and types are declared +from feast.types import Struct, String, Int32 + +address = Struct({"street": String, "city": String, "zip": Int32}) +# Value: {"street": "123 Main St", "city": "Springfield", "zip": 62704} +``` + +## Type System in Practice + +The sections below explain how Feast uses its type system in different contexts. ### Feature inference @@ -17,7 +542,11 @@ For example, if the `schema` parameter is not specified for a feature view, Feas Each of these columns must be associated with a Feast type, which requires conversion from the data source type system to the Feast type system. * The feature inference logic calls `_infer_features_and_entities`. * `_infer_features_and_entities` calls `source_datatype_to_feast_value_type`. -* `source_datatype_to_feast_value_type` cals the appropriate method in `type_map.py`. For example, if a `SnowflakeSource` is being examined, `snowflake_python_type_to_feast_value_type` from `type_map.py` will be called. +* `source_datatype_to_feast_value_type` calls the appropriate method in `type_map.py`. For example, if a `SnowflakeSource` is being examined, `snowflake_python_type_to_feast_value_type` from `type_map.py` will be called. + +{% hint style="info" %} +**Types that cannot be inferred:** `Set`, `Json`, `Struct`, `Decimal`, `ScalarMap`, `PdfBytes`, and `ImageBytes` types are never inferred from backend schemas. If you use these types, you must declare them explicitly in your feature view schema. +{% endhint %} ### Materialization diff --git a/docs/roadmap.md b/docs/roadmap.md index b7bab598cca..d92ffa38f24 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -17,33 +17,57 @@ The list below contains the functionality that contributors are planning to deve * [x] [Postgres (contrib plugin)](https://docs.feast.dev/reference/data-sources/postgres) * [x] [Spark (contrib plugin)](https://docs.feast.dev/reference/data-sources/spark) * [x] [Couchbase (contrib plugin)](https://docs.feast.dev/reference/data-sources/couchbase) + * [x] [Athena (contrib plugin)](https://docs.feast.dev/reference/data-sources/athena) + * [x] [Clickhouse (contrib plugin)](https://docs.feast.dev/reference/data-sources/clickhouse) + * [x] [Oracle (contrib plugin)](https://docs.feast.dev/reference/data-sources/oracle) + * [x] [MongoDB (contrib plugin)](https://docs.feast.dev/reference/data-sources/mongodb) + * [x] [Ray source (contrib plugin)](https://docs.feast.dev/reference/data-sources/ray) * [x] Kafka / Kinesis sources (via [push support into the online store](https://docs.feast.dev/reference/data-sources/push)) * **Offline Stores** * [x] [Snowflake](https://docs.feast.dev/reference/offline-stores/snowflake) * [x] [Redshift](https://docs.feast.dev/reference/offline-stores/redshift) * [x] [BigQuery](https://docs.feast.dev/reference/offline-stores/bigquery) - * [x] [Azure Synapse + Azure SQL (contrib plugin)](https://docs.feast.dev/reference/offline-stores/mssql.md) + * [x] [DuckDB](https://docs.feast.dev/reference/offline-stores/duckdb) + * [x] [Dask](https://docs.feast.dev/reference/offline-stores/dask) + * [x] [Remote](https://docs.feast.dev/reference/offline-stores/remote-offline-store) + * [x] [Azure Synapse + Azure SQL (contrib plugin)](https://docs.feast.dev/reference/offline-stores/mssql) * [x] [Hive (community plugin)](https://github.com/baineng/feast-hive) * [x] [Postgres (contrib plugin)](https://docs.feast.dev/reference/offline-stores/postgres) - * [x] [Trino (contrib plugin)](https://github.com/Shopify/feast-trino) + * [x] [Trino (contrib plugin)](https://docs.feast.dev/reference/offline-stores/trino) * [x] [Spark (contrib plugin)](https://docs.feast.dev/reference/offline-stores/spark) * [x] [Couchbase (contrib plugin)](https://docs.feast.dev/reference/offline-stores/couchbase) - * [x] [In-memory / Pandas](https://docs.feast.dev/reference/offline-stores/file) + * [x] [Athena (contrib plugin)](https://docs.feast.dev/reference/offline-stores/athena) + * [x] [Clickhouse (contrib plugin)](https://docs.feast.dev/reference/offline-stores/clickhouse) + * [x] [Ray (contrib plugin)](https://docs.feast.dev/reference/offline-stores/ray) + * [x] [Oracle (contrib plugin)](https://docs.feast.dev/reference/offline-stores/oracle) + * [x] [MongoDB (contrib plugin)](https://docs.feast.dev/reference/offline-stores/mongodb) + * [x] [Hybrid](https://docs.feast.dev/reference/offline-stores/hybrid) * [x] [Custom offline store support](https://docs.feast.dev/how-to-guides/customizing-feast/adding-a-new-offline-store) * **Online Stores** * [x] [Snowflake](https://docs.feast.dev/reference/online-stores/snowflake) * [x] [DynamoDB](https://docs.feast.dev/reference/online-stores/dynamodb) * [x] [Redis](https://docs.feast.dev/reference/online-stores/redis) + * [x] [Dragonfly](https://docs.feast.dev/reference/online-stores/dragonfly) * [x] [Datastore](https://docs.feast.dev/reference/online-stores/datastore) * [x] [Bigtable](https://docs.feast.dev/reference/online-stores/bigtable) * [x] [SQLite](https://docs.feast.dev/reference/online-stores/sqlite) - * [x] [Dragonfly](https://docs.feast.dev/reference/online-stores/dragonfly) - * [x] [IKV - Inlined Key Value Store](https://docs.feast.dev/reference/online-stores/ikv) + * [x] [Remote](https://docs.feast.dev/reference/online-stores/remote) + * [x] [Postgres](https://docs.feast.dev/reference/online-stores/postgres) + * [x] [HBase](https://docs.feast.dev/reference/online-stores/hbase) + * [x] [Cassandra / AstraDB](https://docs.feast.dev/reference/online-stores/cassandra) + * [x] [ScyllaDB](https://docs.feast.dev/reference/online-stores/scylladb) + * [x] [MySQL](https://docs.feast.dev/reference/online-stores/mysql) + * [x] [Hazelcast](https://docs.feast.dev/reference/online-stores/hazelcast) + * [x] [Elasticsearch](https://docs.feast.dev/reference/online-stores/elasticsearch) + * [x] [SingleStore](https://docs.feast.dev/reference/online-stores/singlestore) + * [x] [Couchbase](https://docs.feast.dev/reference/online-stores/couchbase) + * [x] [MongoDB](https://docs.feast.dev/reference/online-stores/mongodb) + * [x] [Aerospike](https://docs.feast.dev/reference/online-stores/aerospike) + * [x] [Qdrant (vector store)](https://docs.feast.dev/reference/online-stores/qdrant) + * [x] [Milvus (vector store)](https://docs.feast.dev/reference/online-stores/milvus) + * [x] [Faiss (vector store)](https://docs.feast.dev/reference/online-stores/faiss) + * [x] [Hybrid](https://docs.feast.dev/reference/online-stores/hybrid) * [x] [Azure Cache for Redis (community plugin)](https://github.com/Azure/feast-azure) - * [x] [Postgres (contrib plugin)](https://docs.feast.dev/reference/online-stores/postgres) - * [x] [Cassandra / AstraDB (contrib plugin)](https://docs.feast.dev/reference/online-stores/cassandra) - * [x] [ScyllaDB (contrib plugin)](https://docs.feast.dev/reference/online-stores/scylladb) - * [x] [Couchbase (contrib plugin)](https://docs.feast.dev/reference/online-stores/couchbase) * [x] [Custom online store support](https://docs.feast.dev/how-to-guides/customizing-feast/adding-support-for-a-new-online-store) * **Feature Engineering** * [x] On-demand Transformations (On Read) (Beta release. See [RFC](https://docs.google.com/document/d/1lgfIw0Drc65LpaxbUu49RCeJgMew547meSJttnUqz7c/edit#)) @@ -66,7 +90,7 @@ The list below contains the functionality that contributors are planning to deve * [x] [Offline Feature Server (alpha)](https://docs.feast.dev/reference/feature-servers/offline-feature-server) * [x] [Registry server (alpha)](https://github.com/feast-dev/feast/blob/master/docs/reference/feature-servers/registry-server.md) * **Data Quality Management (See [RFC](https://docs.google.com/document/d/110F72d4NTv80p35wDSONxhhPBqWRwbZXG4f9mNEMd98/edit))** - * [x] Data profiling and validation (Great Expectations) + * [x] [Feature Quality Monitoring](https://docs.feast.dev/how-to-guides/feature-monitoring) — built-in metrics, drift detection, serving log monitoring, and UI dashboard * **Feature Discovery and Governance** * [x] Python SDK for browsing feature registry * [x] CLI for browsing feature registry diff --git a/docs/specs/offline_store_format.md b/docs/specs/offline_store_format.md index ac829dd52f1..1b440d34c27 100644 --- a/docs/specs/offline_store_format.md +++ b/docs/specs/offline_store_format.md @@ -49,6 +49,12 @@ Here's how Feast types map to Pandas types for Feast APIs that take in or return | DOUBLE\_LIST | `list[float]`| | FLOAT\_LIST | `list[float]`| | BOOL\_LIST | `list[bool]`| +| MAP | `dict` (`Dict[str, Any]`)| +| MAP\_LIST | `list[dict]` (`List[Dict[str, Any]]`)| +| JSON | `object` (parsed Python dict/list/str)| +| JSON\_LIST | `list[object]`| +| STRUCT | `dict` (`Dict[str, Any]`)| +| STRUCT\_LIST | `list[dict]` (`List[Dict[str, Any]]`)| Note that this mapping is non-injective, that is more than one Pandas type may corresponds to one Feast type (but not vice versa). In these cases, when converting Feast values to Pandas, the **first** Pandas type in the table above is used. @@ -78,6 +84,12 @@ Here's how Feast types map to BigQuery types when using BigQuery for offline sto | DOUBLE\_LIST | `ARRAY`| | FLOAT\_LIST | `ARRAY`| | BOOL\_LIST | `ARRAY`| +| MAP | `JSON` / `STRUCT` | +| MAP\_LIST | `ARRAY` / `ARRAY` | +| JSON | `JSON` | +| JSON\_LIST | `ARRAY` | +| STRUCT | `STRUCT` / `RECORD` | +| STRUCT\_LIST | `ARRAY` | Values that are not specified by the table above will cause an error on conversion. @@ -94,3 +106,23 @@ https://docs.snowflake.com/en/user-guide/python-connector-pandas.html#snowflake- | INT32 | `INT8 / UINT8 / INT16 / UINT16 / INT32 / UINT32` | | INT64 | `INT64 / UINT64` | | DOUBLE | `FLOAT64` | +| MAP | `VARIANT` / `OBJECT` | +| JSON | `JSON` / `VARIANT` | + +#### Redshift Types +Here's how Feast types map to Redshift types when using Redshift for offline storage: + +| Feast Type | Redshift Type | +|-------------|--| +| Event Timestamp | `TIMESTAMP` / `TIMESTAMPTZ` | +| BYTES | `VARBYTE` | +| STRING | `VARCHAR` | +| INT32 | `INT4` / `SMALLINT` | +| INT64 | `INT8` / `BIGINT` | +| DOUBLE | `FLOAT8` / `DOUBLE PRECISION` | +| FLOAT | `FLOAT4` / `REAL` | +| BOOL | `BOOL` | +| MAP | `SUPER` | +| JSON | `json` / `SUPER` | + +Note: Redshift's `SUPER` type stores semi-structured JSON data. During materialization, Feast automatically handles `SUPER` columns that are exported as JSON strings by parsing them back into Python dictionaries before converting to `MAP` proto values. diff --git a/docs/tutorials/azure/data/data_generator.py b/docs/tutorials/azure/data/data_generator.py index 77fec082963..20af682c9a3 100644 --- a/docs/tutorials/azure/data/data_generator.py +++ b/docs/tutorials/azure/data/data_generator.py @@ -7,8 +7,7 @@ from pytz import FixedOffset, timezone, utc from random import randint from enum import Enum -from sqlalchemy import create_engine, DateTime -from datetime import datetime +from sqlalchemy import DateTime DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL = "event_timestamp" @@ -65,7 +64,8 @@ def create_orders_df( ) ] df.sort_values( - by=["e_ts", "order_id", "driver_id", "customer_id"], inplace=True, + by=["e_ts", "order_id", "driver_id", "customer_id"], + inplace=True, ) else: df[DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL] = [ @@ -208,9 +208,7 @@ def create_customer_daily_profile_df(customers, start_date, end_date) -> pd.Data def generate_entities(date, n_customers, n_drivers, order_count): end_date = date - before_start_date = end_date - timedelta(days=365) start_date = end_date - timedelta(days=7) - after_end_date = end_date + timedelta(days=365) customer_entities = [20000 + c_id for c_id in range(n_customers)] driver_entities = [50000 + d_id for d_id in range(n_drivers)] orders_df = create_orders_df( @@ -225,7 +223,7 @@ def generate_entities(date, n_customers, n_drivers, order_count): def save_df_to_csv(df, table_name, dtype): - df.to_csv(table_name+".csv", index=False) + df.to_csv(table_name + ".csv", index=False) if __name__ == "__main__": @@ -247,7 +245,6 @@ def save_df_to_csv(df, table_name, dtype): print(drivers_df.head()) - orders_table = "orders" driver_hourly_table = "driver_hourly" customer_profile_table = "customer_profile" @@ -257,4 +254,4 @@ def save_df_to_csv(df, table_name, dtype): print("uploading drivers") save_df_to_csv(drivers_df, driver_hourly_table, dtype={"datetime": DateTime()}) print("uploading customers") - save_df_to_csv(customer_df, customer_profile_table, dtype={"datetime": DateTime()}) \ No newline at end of file + save_df_to_csv(customer_df, customer_profile_table, dtype={"datetime": DateTime()}) diff --git a/docs/tutorials/azure/notebooks/src/score.py b/docs/tutorials/azure/notebooks/src/score.py index 7def7d2d2ad..f6281c47157 100644 --- a/docs/tutorials/azure/notebooks/src/score.py +++ b/docs/tutorials/azure/notebooks/src/score.py @@ -11,7 +11,7 @@ from feast.infra.offline_stores.contrib.mssql_offline_store.mssql import ( MsSqlServerOfflineStoreConfig, ) -from feast.infra.online_stores.redis import RedisOnlineStoreConfig, RedisOnlineStore +from feast.infra.online_stores.redis import RedisOnlineStoreConfig def init(): diff --git a/docs/tutorials/demo-notebooks.md b/docs/tutorials/demo-notebooks.md new file mode 100644 index 00000000000..8c0ba059f81 --- /dev/null +++ b/docs/tutorials/demo-notebooks.md @@ -0,0 +1,114 @@ +# Demo Notebooks + +Feast can generate tailored Jupyter notebooks for any Feast project. The notebooks adapt to your `feature_store.yaml` configuration and provide a hands-on walkthrough of core Feast functionality. + +## What you get + +For each project discovered, Feast creates a directory with notebooks covering: + +| Notebook | Description | +|----------|-------------| +| **01 — Feature Store Overview** | Explore registered entities, feature views, feature services, and data sources. | +| **02 — Historical Feature Retrieval** | Build a training dataset with point-in-time correct joins using `get_historical_features`. | +| **03 — Online Feature Serving** | Materialize features to the online store and retrieve them at low latency with `get_online_features`. | + +The content adapts automatically based on: + +* **Online / offline store types** — descriptions reflect the actual backends configured. +* **Registry type** — local registries include `feast apply`; remote registries use `refresh_registry()`. +* **Authentication** — auth details from `feature_store.yaml` are surfaced when configured. +* **Vector search** — a vector/RAG retrieval section is included when embeddings are detected. + +## Prerequisites + +* Python 3.9+ +* Feast installed (`pip install feast`) +* A feature repository with a valid `feature_store.yaml` + +## Using the CLI + +Run the command from (or pointing to) a directory containing `feature_store.yaml`: + +```bash +feast demo-notebooks +``` + +This searches for `feature_store.yaml` in the current directory and every file inside the `feast-config/` directory. Each file in `feast-config/` is treated as a separate project config. For each project found, notebooks are written to `./feast-demo-notebooks//`. + +### Options + +| Option | Default | Description | +|--------|---------|-------------| +| `-o, --output-dir` | `./feast-demo-notebooks` | Root directory for generated notebooks | +| `--overwrite` | `false` | Overwrite if the output directory already exists | + +```bash +# Write to a custom directory +feast demo-notebooks -o ./my-notebooks + +# Overwrite existing notebooks +feast demo-notebooks --overwrite + +# Use --chdir to point at a different feature repo +feast -c /path/to/feature_repo demo-notebooks +``` + +## Using the Python SDK + +```python +from feast import copy_demo_notebooks + +copy_demo_notebooks() +``` + +### Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `output_dir` | `str` | `"./feast-demo-notebooks"` | Root directory for generated notebooks | +| `repo_path` | `str` | `"."` | Directory to search for `feature_store.yaml` files | +| `overwrite` | `bool` | `False` | Overwrite existing output directories | + +### Examples + +```python +from feast import copy_demo_notebooks + +# Default — searches current directory, writes to ./feast-demo-notebooks/ +copy_demo_notebooks() + +# Custom paths +copy_demo_notebooks( + output_dir="/home/user/notebooks", + repo_path="/home/user/feast-projects/my-repo/feature_repo", + overwrite=True, +) +``` + +## Multi-project repositories + +If your `feast-config/` directory contains multiple files, each is treated as a separate project and a dedicated notebook directory is created: + +``` +feast-demo-notebooks/ +├── project_alpha/ +│ ├── 01_feature_store_overview.ipynb +│ ├── 02_historical_features_training.ipynb +│ └── 03_online_features_serving.ipynb +└── project_beta/ + ├── 01_feature_store_overview.ipynb + ├── 02_historical_features_training.ipynb + └── 03_online_features_serving.ipynb +``` + +## Running the notebooks + +Open any generated notebook in Jupyter, JupyterLab, or VS Code and run cells from top to bottom. Each notebook: + +1. Configures the path to your `feature_store.yaml` automatically (no manual editing needed). +2. Connects to the feature store using the Feast Python SDK. +3. Walks through relevant operations with real data from your project. + +{% hint style="info" %} +The first notebook (**01 — Overview**) includes a prerequisites check and `feast apply` / registry sync step. Subsequent notebooks assume these have already been completed. +{% endhint %} diff --git a/docs/tutorials/rag-with-docling.md b/docs/tutorials/rag-with-docling.md index 88f2bd2aad7..6b85db4177c 100644 --- a/docs/tutorials/rag-with-docling.md +++ b/docs/tutorials/rag-with-docling.md @@ -409,6 +409,234 @@ response = client.chat.completions.create( print('\n'.join([c.message.content for c in response.choices])) ``` +## Alternative: Using DocEmbedder for Simplified Ingestion + +Instead of manually chunking, embedding, and writing documents as shown above, you can use Feast's `DocEmbedder` class to handle the entire pipeline in a single step. `DocEmbedder` automates chunking, embedding generation, FeatureView creation, and writing to the online store. + +### Install Dependencies + +```bash +pip install feast[milvus,rag] +``` + +### Set Up and Ingest with DocEmbedder + +```python +from feast import DocEmbedder +import pandas as pd + +# Prepare your documents as a DataFrame +df = pd.DataFrame({ + "id": ["doc1", "doc2", "doc3"], + "text": [ + "Aaron is a prophet, high priest, and the brother of Moses...", + "God at Sinai granted Aaron the priesthood for himself...", + "His rod turned into a snake. Then he stretched out...", + ], +}) + +# DocEmbedder handles everything: generates FeatureView, applies repo, +# chunks text, generates embeddings, and writes to the online store +embedder = DocEmbedder( + repo_path="feature_repo/", + feature_view_name="text_feature_view", +) + +result = embedder.embed_documents( + documents=df, + id_column="id", + source_column="text", + column_mapping=("text", "text_embedding"), +) +``` + +### Retrieve and Query + +Once documents are ingested, you can retrieve them the same way as shown in Step 5 above: + +```python +from feast import FeatureStore + +store = FeatureStore("feature_repo/") + +query_embedding = embed_text("Who are the authors of the paper?") +context_data = store.retrieve_online_documents_v2( + features=[ + "text_feature_view:embedding", + "text_feature_view:text", + "text_feature_view:source_id", + ], + query=query_embedding, + top_k=3, + distance_metric="COSINE", +).to_df() +``` + +### Customizing the Pipeline + +`DocEmbedder` is extensible at every stage. Below are examples of how to create custom components and wire them together. + +#### Custom Chunker + +Subclass `BaseChunker` to implement your own chunking strategy. The `load_parse_and_chunk` method receives each document and must return a list of chunk dictionaries. + +```python +from feast.chunker import BaseChunker, ChunkingConfig +from typing import Any, Optional + +class SentenceChunker(BaseChunker): + """Chunks text by sentences instead of word count.""" + + def load_parse_and_chunk( + self, + source: Any, + source_id: str, + source_column: str, + source_type: Optional[str] = None, + ) -> list[dict]: + import re + + text = str(source) + # Split on sentence boundaries + sentences = re.split(r'(?<=[.!?])\s+', text) + + chunks = [] + current_chunk = [] + chunk_index = 0 + + for sentence in sentences: + current_chunk.append(sentence) + combined = " ".join(current_chunk) + + if len(combined.split()) >= self.config.chunk_size: + chunks.append({ + "chunk_id": f"{source_id}_{chunk_index}", + "original_id": source_id, + source_column: combined, + "chunk_index": chunk_index, + }) + # Keep overlap by retaining the last sentence + current_chunk = [sentence] + chunk_index += 1 + + # Don't forget the last chunk + if current_chunk and len(" ".join(current_chunk).split()) >= self.config.min_chunk_size: + chunks.append({ + "chunk_id": f"{source_id}_{chunk_index}", + "original_id": source_id, + source_column: " ".join(current_chunk), + "chunk_index": chunk_index, + }) + + return chunks +``` + +Or simply configure the built-in `TextChunker`: + +```python +from feast import TextChunker, ChunkingConfig + +chunker = TextChunker(config=ChunkingConfig( + chunk_size=200, + chunk_overlap=50, + min_chunk_size=30, + max_chunk_chars=1000, +)) +``` + +#### Custom Embedder + +Subclass `BaseEmbedder` to use a different embedding model. Register modality handlers in `_register_default_modalities` and implement the `embed` method. + +```python +from feast.embedder import BaseEmbedder, EmbeddingConfig +from typing import Any, List, Optional +import numpy as np + +class OpenAIEmbedder(BaseEmbedder): + """Embedder that uses the OpenAI API for text embeddings.""" + + def __init__(self, model: str = "text-embedding-3-small", config: Optional[EmbeddingConfig] = None): + self.model = model + self._client = None + super().__init__(config) + + def _register_default_modalities(self) -> None: + self.register_modality("text", self._embed_text) + + @property + def client(self): + if self._client is None: + from openai import OpenAI + self._client = OpenAI() + return self._client + + def get_embedding_dim(self, modality: str) -> Optional[int]: + # text-embedding-3-small produces 1536-dim vectors + if modality == "text": + return 1536 + return None + + def embed(self, inputs: List[Any], modality: str) -> np.ndarray: + if modality not in self._modality_handlers: + raise ValueError(f"Unsupported modality: '{modality}'") + return self._modality_handlers[modality](inputs) + + def _embed_text(self, inputs: List[str]) -> np.ndarray: + response = self.client.embeddings.create(input=inputs, model=self.model) + return np.array([item.embedding for item in response.data]) +``` + +#### Custom Logical Layer Function + +The schema transform function transforms the chunked + embedded DataFrame into the exact schema your FeatureView expects. It must accept a `pd.DataFrame` and return a `pd.DataFrame`. + +```python +import pandas as pd +from datetime import datetime, timezone + +def my_schema_transform_fn(df: pd.DataFrame) -> pd.DataFrame: + """Map chunked + embedded columns to the FeatureView schema.""" + return pd.DataFrame({ + "passage_id": df["chunk_id"], + "text": df["text"], + "embedding": df["text_embedding"], + "event_timestamp": [datetime.now(timezone.utc)] * len(df), + "source_id": df["original_id"], + # Add any extra columns your FeatureView expects + "chunk_index": df["chunk_index"], + }) +``` + +#### Putting It All Together + +Pass your custom components to `DocEmbedder`: + +```python +from feast import DocEmbedder + +embedder = DocEmbedder( + repo_path="feature_repo/", + feature_view_name="text_feature_view", + chunker=SentenceChunker(config=ChunkingConfig(chunk_size=150, min_chunk_size=20)), + embedder=OpenAIEmbedder(model="text-embedding-3-small"), + schema_transform_fn=my_schema_transform_fn, + vector_length=1536, # Match the OpenAI embedding dimension +) + +# Embed and ingest +result = embedder.embed_documents( + documents=df, + id_column="id", + source_column="text", + column_mapping=("text", "text_embedding"), +) +``` + +> **Note:** When using a custom `schema_transform_fn`, ensure the returned DataFrame columns match your FeatureView schema. When using a custom embedder with a different output dimension, set `vector_length` accordingly (or let it auto-detect via `get_embedding_dim`). + +For a complete end-to-end example, see the [DocEmbedder notebook](https://github.com/feast-dev/feast/tree/master/examples/rag-retriever/rag_feast_docembedder.ipynb). + ## Why Feast for RAG? Feast makes it remarkably easy to set up and manage a RAG system by: diff --git a/docs/tutorials/validating-historical-features.md b/docs/tutorials/validating-historical-features.md deleted file mode 100644 index 1984adcdcf9..00000000000 --- a/docs/tutorials/validating-historical-features.md +++ /dev/null @@ -1,916 +0,0 @@ -# Validating historical features with Great Expectations - -In this tutorial, we will use the public dataset of Chicago taxi trips to present data validation capabilities of Feast. -- The original dataset is stored in BigQuery and consists of raw data for each taxi trip (one row per trip) since 2013. -- We will generate several training datasets (aka historical features in Feast) for different periods and evaluate expectations made on one dataset against another. - -Types of features we're ingesting and generating: -- Features that aggregate raw data with daily intervals (eg, trips per day, average fare or speed for a specific day, etc.). -- Features using SQL while pulling data from BigQuery (like total trips time or total miles travelled). -- Features calculated on the fly when requested using Feast's on-demand transformations - -Our plan: - -0. Prepare environment -1. Pull data from BigQuery (optional) -2. Declare & apply features and feature views in Feast -3. Generate reference dataset -4. Develop & test profiler function -5. Run validation on different dataset using reference dataset & profiler - - -> The original notebook and datasets for this tutorial can be found on [GitHub](https://github.com/feast-dev/dqm-tutorial). - -### 0. Setup - -Install Feast Python SDK and great expectations: - - -```python -!pip install 'feast[ge]' -``` - - -### 1. Dataset preparation (Optional) - -**You can skip this step if you don't have GCP account. Please use parquet files that are coming with this tutorial instead** - - -```python -!pip install google-cloud-bigquery -``` - - -```python -import pyarrow.parquet - -from google.cloud.bigquery import Client -``` - - -```python -bq_client = Client(project='kf-feast') -``` - -Running some basic aggregations while pulling data from BigQuery. Grouping by taxi_id and day: - - -```python -data_query = """SELECT - taxi_id, - TIMESTAMP_TRUNC(trip_start_timestamp, DAY) as day, - SUM(trip_miles) as total_miles_travelled, - SUM(trip_seconds) as total_trip_seconds, - SUM(fare) as total_earned, - COUNT(*) as trip_count -FROM `bigquery-public-data.chicago_taxi_trips.taxi_trips` -WHERE - trip_miles > 0 AND trip_seconds > 60 AND - trip_start_timestamp BETWEEN '2019-01-01' and '2020-12-31' AND - trip_total < 1000 -GROUP BY taxi_id, TIMESTAMP_TRUNC(trip_start_timestamp, DAY)""" -``` - - -```python -driver_stats_table = bq_client.query(data_query).to_arrow() - -# Storing resulting dataset into parquet file -pyarrow.parquet.write_table(driver_stats_table, "trips_stats.parquet") -``` - - -```python -def entities_query(year): - return f"""SELECT - distinct taxi_id -FROM `bigquery-public-data.chicago_taxi_trips.taxi_trips` -WHERE - trip_miles > 0 AND trip_seconds > 0 AND - trip_start_timestamp BETWEEN '{year}-01-01' and '{year}-12-31' -""" -``` - - -```python -entities_2019_table = bq_client.query(entities_query(2019)).to_arrow() - -# Storing entities (taxi ids) into parquet file -pyarrow.parquet.write_table(entities_2019_table, "entities.parquet") -``` - - -## 2. Declaring features - - -```python -import pyarrow.parquet -import pandas as pd - -from feast import FeatureView, Entity, FeatureStore, Field, BatchFeatureView -from feast.types import Float64, Int64 -from feast.value_type import ValueType -from feast.data_format import ParquetFormat -from feast.on_demand_feature_view import on_demand_feature_view -from feast.infra.offline_stores.file_source import FileSource -from feast.infra.offline_stores.file import SavedDatasetFileStorage -from datetime import timedelta - -``` - - -```python -batch_source = FileSource( - timestamp_field="day", - path="trips_stats.parquet", # using parquet file that we created on previous step - file_format=ParquetFormat() -) -``` - - -```python -taxi_entity = Entity(name='taxi', join_keys=['taxi_id']) -``` - - -```python -trips_stats_fv = BatchFeatureView( - name='trip_stats', - entities=[taxi_entity], - schema=[ - Field(name="total_miles_travelled", dtype=Float64), - Field(name="total_trip_seconds", dtype=Float64), - Field(name="total_earned", dtype=Float64), - Field(name="trip_count", dtype=Int64), - - ], - ttl=timedelta(seconds=86400), - source=batch_source, -) -``` - -*Read more about feature views in [Feast docs](https://docs.feast.dev/getting-started/concepts/feature-view)* - - -```python -@on_demand_feature_view( - sources=[ - trips_stats_fv, - ], - schema=[ - Field(name="avg_fare", dtype=Float64), - Field(name="avg_speed", dtype=Float64), - Field(name="avg_trip_seconds", dtype=Float64), - Field(name="earned_per_hour", dtype=Float64), - ] -) -def on_demand_stats(inp: pd.DataFrame) -> pd.DataFrame: - out = pd.DataFrame() - out["avg_fare"] = inp["total_earned"] / inp["trip_count"] - out["avg_speed"] = 3600 * inp["total_miles_travelled"] / inp["total_trip_seconds"] - out["avg_trip_seconds"] = inp["total_trip_seconds"] / inp["trip_count"] - out["earned_per_hour"] = 3600 * inp["total_earned"] / inp["total_trip_seconds"] - return out -``` - -*Read more about on demand feature views [here](../reference/beta-on-demand-feature-view.md)* - - -```python -store = FeatureStore(".") # using feature_store.yaml that stored in the same directory -``` - - -```python -store.apply([taxi_entity, trips_stats_fv, on_demand_stats]) # writing to the registry -``` - - -## 3. Generating training (reference) dataset - - -```python -taxi_ids = pyarrow.parquet.read_table("entities.parquet").to_pandas() -``` - -Generating range of timestamps with daily frequency: - - -```python -timestamps = pd.DataFrame() -timestamps["event_timestamp"] = pd.date_range("2019-06-01", "2019-07-01", freq='D') -``` - -Cross merge (aka relation multiplication) produces entity dataframe with each taxi_id repeated for each timestamp: - - -```python -entity_df = pd.merge(taxi_ids, timestamps, how='cross') -entity_df -``` - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
taxi_idevent_timestamp
091d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-01
191d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-02
291d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-03
391d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-04
491d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2019-06-05
.........
1569797ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-06-27
1569807ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-06-28
1569817ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-06-29
1569827ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-06-30
1569837ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2019-07-01
-

156984 rows × 2 columns

-
- - - -Retrieving historical features for resulting entity dataframe and persisting output as a saved dataset: - - -```python -job = store.get_historical_features( - entity_df=entity_df, - features=[ - "trip_stats:total_miles_travelled", - "trip_stats:total_trip_seconds", - "trip_stats:total_earned", - "trip_stats:trip_count", - "on_demand_stats:avg_fare", - "on_demand_stats:avg_trip_seconds", - "on_demand_stats:avg_speed", - "on_demand_stats:earned_per_hour", - ] -) - -store.create_saved_dataset( - from_=job, - name='my_training_ds', - storage=SavedDatasetFileStorage(path='my_training_ds.parquet') -) -``` - -```python -, full_feature_names = False, tags = {}, _retrieval_job = , min_event_timestamp = 2019-06-01 00:00:00, max_event_timestamp = 2019-07-01 00:00:00)> -``` - - -## 4. Developing dataset profiler - -Dataset profiler is a function that accepts dataset and generates set of its characteristics. This charasteristics will be then used to evaluate (validate) next datasets. - -**Important: datasets are not compared to each other! -Feast use a reference dataset and a profiler function to generate a reference profile. -This profile will be then used during validation of the tested dataset.** - - -```python -import numpy as np - -from feast.dqm.profilers.ge_profiler import ge_profiler - -from great_expectations.core.expectation_suite import ExpectationSuite -from great_expectations.dataset import PandasDataset -``` - - -Loading saved dataset first and exploring the data: - - -```python -ds = store.get_saved_dataset('my_training_ds') -ds.to_df() -``` - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
total_earnedavg_trip_secondstaxi_idtotal_miles_travelledtrip_countearned_per_hourevent_timestamptotal_trip_secondsavg_fareavg_speed
068.252270.00000091d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...24.702.054.1189432019-06-01 00:00:00+00:004540.034.12500019.585903
1221.00560.5000007a4a6162eaf27805aef407d25d5cb21fe779cd962922cb...54.1824.059.1436222019-06-01 00:00:00+00:0013452.09.20833314.499554
2160.501010.769231f4c9d05b215d7cbd08eca76252dae51cdb7aca9651d4ef...41.3013.043.9726032019-06-01 00:00:00+00:0013140.012.34615411.315068
3183.75697.550000c1f533318f8480a59173a9728ea0248c0d3eb187f4b897...37.3020.047.4159562019-06-01 00:00:00+00:0013951.09.1875009.625116
4217.751054.076923455b6b5cae6ca5a17cddd251485f2266d13d6a2c92f07c...69.6913.057.2064512019-06-01 00:00:00+00:0013703.016.75000018.308692
.................................
15697938.001980.0000000cccf0ec1f46d1e0beefcfdeaf5188d67e170cdff92618...14.901.069.0909092019-07-01 00:00:00+00:001980.038.00000027.090909
156980135.00551.250000beefd3462e3f5a8e854942a2796876f6db73ebbd25b435...28.4016.055.1020412019-07-01 00:00:00+00:008820.08.43750011.591837
156981NaNNaN9a3c52aa112f46cf0d129fafbd42051b0fb9b0ff8dcb0e...NaNNaNNaN2019-07-01 00:00:00+00:00NaNNaNNaN
15698263.00815.00000008308c31cd99f495dea73ca276d19a6258d7b4c9c88e43...19.964.069.5705522019-07-01 00:00:00+00:003260.015.75000022.041718
156983NaNNaN7ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...NaNNaNNaN2019-07-01 00:00:00+00:00NaNNaNNaN
-

156984 rows × 10 columns

-
- - - -Feast uses [Great Expectations](https://docs.greatexpectations.io/docs/) as a validation engine and [ExpectationSuite](https://legacy.docs.greatexpectations.io/en/latest/autoapi/great_expectations/core/expectation_suite/index.html#great_expectations.core.expectation_suite.ExpectationSuite) as a dataset's profile. Hence, we need to develop a function that will generate ExpectationSuite. This function will receive instance of [PandasDataset](https://legacy.docs.greatexpectations.io/en/latest/autoapi/great_expectations/dataset/index.html?highlight=pandasdataset#great_expectations.dataset.PandasDataset) (wrapper around pandas.DataFrame) so we can utilize both Pandas DataFrame API and some helper functions from PandasDataset during profiling. - - -```python -DELTA = 0.1 # controlling allowed window in fraction of the value on scale [0, 1] - -@ge_profiler -def stats_profiler(ds: PandasDataset) -> ExpectationSuite: - # simple checks on data consistency - ds.expect_column_values_to_be_between( - "avg_speed", - min_value=0, - max_value=60, - mostly=0.99 # allow some outliers - ) - - ds.expect_column_values_to_be_between( - "total_miles_travelled", - min_value=0, - max_value=500, - mostly=0.99 # allow some outliers - ) - - # expectation of means based on observed values - observed_mean = ds.trip_count.mean() - ds.expect_column_mean_to_be_between("trip_count", - min_value=observed_mean * (1 - DELTA), - max_value=observed_mean * (1 + DELTA)) - - observed_mean = ds.earned_per_hour.mean() - ds.expect_column_mean_to_be_between("earned_per_hour", - min_value=observed_mean * (1 - DELTA), - max_value=observed_mean * (1 + DELTA)) - - - # expectation of quantiles - qs = [0.5, 0.75, 0.9, 0.95] - observed_quantiles = ds.avg_fare.quantile(qs) - - ds.expect_column_quantile_values_to_be_between( - "avg_fare", - quantile_ranges={ - "quantiles": qs, - "value_ranges": [[None, max_value] for max_value in observed_quantiles] - }) - - return ds.get_expectation_suite() -``` - -Testing our profiler function: - - -```python -ds.get_profile(profiler=stats_profiler) -``` - 02/02/2022 02:43:47 PM INFO: 5 expectation(s) included in expectation_suite. result_format settings filtered. - - - - -**Verify that all expectations that we coded in our profiler are present here. Otherwise (if you can't find some expectations) it means that it failed to pass on the reference dataset (do it silently is default behavior of Great Expectations).** - -Now we can create validation reference from dataset and profiler function: - - -```python -validation_reference = ds.as_reference(name="validation_reference_dataset", profiler=stats_profiler) -``` - -and test it against our existing retrieval job - - -```python -_ = job.to_df(validation_reference=validation_reference) -``` - - 02/02/2022 02:43:52 PM INFO: 5 expectation(s) included in expectation_suite. result_format settings filtered. - 02/02/2022 02:43:53 PM INFO: Validating data_asset_name None with expectation_suite_name default - - -Validation successfully passed as no exception were raised. - - -### 5. Validating new historical retrieval - -Creating new timestamps for Dec 2020: - - -```python -from feast.dqm.errors import ValidationFailed -``` - - -```python -timestamps = pd.DataFrame() -timestamps["event_timestamp"] = pd.date_range("2020-12-01", "2020-12-07", freq='D') -``` - - -```python -entity_df = pd.merge(taxi_ids, timestamps, how='cross') -entity_df -``` - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
taxi_idevent_timestamp
091d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-01
191d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-02
291d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-03
391d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-04
491d5288487e87c5917b813ba6f75ab1c3a9749af906a2d...2020-12-05
.........
354437ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-03
354447ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-04
354457ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-05
354467ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-06
354477ebf27414a0c7b128e7925e1da56d51a8b81484f7630cf...2020-12-07
-

35448 rows × 2 columns

-
- - -```python -job = store.get_historical_features( - entity_df=entity_df, - features=[ - "trip_stats:total_miles_travelled", - "trip_stats:total_trip_seconds", - "trip_stats:total_earned", - "trip_stats:trip_count", - "on_demand_stats:avg_fare", - "on_demand_stats:avg_trip_seconds", - "on_demand_stats:avg_speed", - "on_demand_stats:earned_per_hour", - ] -) -``` - -Execute retrieval job with validation reference: - - -```python -try: - df = job.to_df(validation_reference=validation_reference) -except ValidationFailed as exc: - print(exc.validation_report) -``` - - 02/02/2022 02:43:58 PM INFO: 5 expectation(s) included in expectation_suite. result_format settings filtered. - 02/02/2022 02:43:59 PM INFO: Validating data_asset_name None with expectation_suite_name default - - [ - { - "expectation_config": { - "expectation_type": "expect_column_mean_to_be_between", - "kwargs": { - "column": "trip_count", - "min_value": 10.387244591346153, - "max_value": 12.695521167200855, - "result_format": "COMPLETE" - }, - "meta": {} - }, - "meta": {}, - "result": { - "observed_value": 6.692920555429092, - "element_count": 35448, - "missing_count": 31055, - "missing_percent": 87.6071992778154 - }, - "exception_info": { - "raised_exception": false, - "exception_message": null, - "exception_traceback": null - }, - "success": false - }, - { - "expectation_config": { - "expectation_type": "expect_column_mean_to_be_between", - "kwargs": { - "column": "earned_per_hour", - "min_value": 52.320624975640214, - "max_value": 63.94743052578249, - "result_format": "COMPLETE" - }, - "meta": {} - }, - "meta": {}, - "result": { - "observed_value": 68.99268345164135, - "element_count": 35448, - "missing_count": 31055, - "missing_percent": 87.6071992778154 - }, - "exception_info": { - "raised_exception": false, - "exception_message": null, - "exception_traceback": null - }, - "success": false - }, - { - "expectation_config": { - "expectation_type": "expect_column_quantile_values_to_be_between", - "kwargs": { - "column": "avg_fare", - "quantile_ranges": { - "quantiles": [ - 0.5, - 0.75, - 0.9, - 0.95 - ], - "value_ranges": [ - [ - null, - 16.4 - ], - [ - null, - 26.229166666666668 - ], - [ - null, - 36.4375 - ], - [ - null, - 42.0 - ] - ] - }, - "result_format": "COMPLETE" - }, - "meta": {} - }, - "meta": {}, - "result": { - "observed_value": { - "quantiles": [ - 0.5, - 0.75, - 0.9, - 0.95 - ], - "values": [ - 19.5, - 28.1, - 38.0, - 44.125 - ] - }, - "element_count": 35448, - "missing_count": 31055, - "missing_percent": 87.6071992778154, - "details": { - "success_details": [ - false, - false, - false, - false - ] - } - }, - "exception_info": { - "raised_exception": false, - "exception_message": null, - "exception_traceback": null - }, - "success": false - } - ] - - -Validation failed since several expectations didn't pass: -* Trip count (mean) decreased more than 10% (which is expected when comparing Dec 2020 vs June 2019) -* Average Fare increased - all quantiles are higher than expected -* Earn per hour (mean) increased more than 10% (most probably due to increased fare) - diff --git a/environment-setup.md b/environment-setup.md index 581dc35f778..9723fedf306 100644 --- a/environment-setup.md +++ b/environment-setup.md @@ -1,22 +1,15 @@ -1. install anaconda, install docker -2. create an environment for feast, selecting python 3.9. Activate the environment: +1. Install Docker and [uv](https://docs.astral.sh/uv/getting-started/installation/) +2. Create a virtual environment and activate it: ```bash -conda create --name feast python=3.9 -conda activate feast +uv venv --python 3.11 +source .venv/bin/activate ``` -3. install dependencies: +3. Install dependencies: ```bash -pip install pip-tools -brew install mysql -brew install xz protobuf openssl zlib -pip install cryptography -U -conda install protobuf -conda install pymssql -pip install -e ".[dev]" -make install-python-ci-dependencies PYTHON=3.9 +make install-python-dependencies-dev ``` -4. start the docker daemon -5. run unit tests: +4. Start the Docker daemon +5. Run unit tests: ```bash make test-python-unit ``` diff --git a/examples/agent_feature_store/README.md b/examples/agent_feature_store/README.md new file mode 100644 index 00000000000..039b580fd59 --- /dev/null +++ b/examples/agent_feature_store/README.md @@ -0,0 +1,420 @@ +# Feast-Powered AI Agent Example + +This example demonstrates an **AI agent with persistent memory** that uses **Feast as both a feature store and a context memory layer** through the **Model Context Protocol (MCP)**. This demo uses **Milvus** as the vector-capable online store, but Feast supports multiple vector backends -- including **Milvus, Elasticsearch, Qdrant, PGVector, and FAISS** -- swappable via configuration. + +## Why Feast for Agents? + +Agents need more than just access to data -- they need to **remember** what happened in prior interactions. Feast's online store is entity-keyed, low-latency, governed, and supports both reads and writes, making it a natural fit for agent context and memory. + +| Capability | How Feast Provides It | +|---|---| +| **Structured context** | Entity-keyed feature retrieval (customer profiles, account data) | +| **Document search** | Vector similarity search via pluggable backends (Milvus, Elasticsearch, Qdrant, PGVector, FAISS) | +| **Persistent memory** | Auto-checkpointed after each turn via `write_to_online_store` | +| **Governance** | RBAC, audit trails, and feature-level permissions | +| **TTL management** | Declarative expiration on feature views (memory auto-expires) | +| **Offline analysis** | Memory is queryable offline like any other feature | + +## Architecture + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#E3F2FD', 'primaryBorderColor': '#1565C0', 'primaryTextColor': '#0D47A1', 'lineColor': '#546E7A', 'secondaryColor': '#F3E5F5', 'tertiaryColor': '#E8F5E9'}}}%% +flowchart LR + User((("🧑 User"))):::userClass + + subgraph AgentLoop ["🤖 Agent Loop"] + LLM["LLM Engine\ntool-calling · reasoning"]:::agentClass + end + + subgraph Feast ["🏗️ Feast MCP Server"] + direction TB + ReadAPI["get_online_features\nretrieve_online_documents"]:::feastClass + WriteAPI["write_to_online_store"]:::feastClass + end + + subgraph VectorStore ["🗄️ Online Store"] + direction TB + Profiles[("👤 customer_profile\nplan · spend · tickets")]:::storageClass + Articles[("📚 knowledge_base\nvector embeddings")]:::storageClass + Memory[("🧠 agent_memory\ntopic · resolution · prefs")]:::memoryClass + end + + User -->|"query"| LLM + LLM -->|"MCP: recall_memory\nlookup_customer\nsearch_kb"| ReadAPI + LLM -->|"MCP: auto-checkpoint"| WriteAPI + ReadAPI --> Profiles + ReadAPI --> Articles + ReadAPI --> Memory + WriteAPI --> Memory + ReadAPI -.->|"results"| LLM + LLM -->|"answer"| User + + classDef userClass fill:#E8EAF6,stroke:#283593,color:#1A237E + classDef agentClass fill:#E3F2FD,stroke:#1565C0,color:#0D47A1 + classDef feastClass fill:#FFF3E0,stroke:#E65100,color:#BF360C + classDef storageClass fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20 + classDef memoryClass fill:#F3E5F5,stroke:#6A1B9A,color:#4A148C +``` + +## Tools (backed by Feast) + +The agent has four tools. Feast is both the **read path** (context) and the **write path** (memory): + +| Tool | Direction | What it does | When the LLM calls it | +|---|---|---|---| +| `lookup_customer` | READ | Fetches customer profile features (plan, spend, tickets) | Questions about the customer's account | +| `search_knowledge_base` | READ | Retrieves support articles from the vector store | Questions needing product docs | +| `recall_memory` | READ | Reads past interaction context (last topic, open issues, preferences) | Start of every conversation | + +Memory is **auto-saved after each agent turn** (not as an LLM tool call). This follows the same pattern used by production frameworks -- see [Memory as Infrastructure](#memory-as-infrastructure) below. + +### Feast as Context Memory + +The `agent_memory` feature view stores per-customer interaction state: + +```python +agent_memory = FeatureView( + name="agent_memory", + entities=[customer], + schema=[ + Field(name="last_topic", dtype=String), + Field(name="last_resolution", dtype=String), + Field(name="interaction_count", dtype=Int64), + Field(name="preferences", dtype=String), + Field(name="open_issue", dtype=String), + ], + ttl=timedelta(days=30), +) +``` + +This gives agents **persistent, governed, entity-keyed memory** that survives across sessions, is versioned, and lives under the same RBAC as every other feature -- unlike an ad-hoc Redis cache or an in-process dict. + +### Memory as Infrastructure + +Production agent frameworks treat memory as **infrastructure, not an LLM decision**. The framework auto-saves state after each step - the LLM never needs to "decide" to persist: + +| Framework | Memory mechanism | How it works | +|---|---|---| +| **LangGraph** | Checkpointers (`MemorySaver`, `PostgresSaver`) | Every graph step is checkpointed automatically by `thread_id` | +| **CrewAI** | Built-in memory (`memory=True`) | Short-term, long-term, and entity memory auto-persist after each task | +| **AutoGen** | Teachable agents | Post-conversation hooks extract and store learnings in a vector DB | +| **OpenAI Agents SDK** | Application-level | Serialize `RunResult` between turns; framework manages state | + +This demo follows the same pattern: the agent's three read tools (`recall_memory`, `lookup_customer`, `search_knowledge_base`) are exposed to the LLM for reasoning, while **memory persistence is handled by the framework after each turn** via `_auto_save_memory`. This ensures consistent, reliable memory regardless of LLM behaviour - no risk of the LLM forgetting to save, double-saving, or writing inconsistent state. + +Feast is a natural fit for this checkpoint layer because it already provides: +- **Entity-keyed storage**: memory is keyed by customer ID (or any entity) +- **TTL management**: memory auto-expires via declarative feature view TTL +- **Schema enforcement**: typed fields prevent corrupt memory writes +- **RBAC and audit trails**: memory reads/writes are governed like any other feature +- **Offline queryability**: agent memory can be analysed in batch pipelines + +## Prerequisites + +- Python 3.10+ +- Feast with MCP and Milvus support +- OpenAI API key (for live tool-calling; demo mode works without it) + +## Quickstart + +### One command + +```bash +cd examples/agent_feature_store +./run_demo.sh + +# Or with live LLM tool-calling: +OPENAI_API_KEY=sk-... ./run_demo.sh +``` + +The script installs dependencies, generates sample data, starts the Feast server, runs the agent, and cleans up on exit. + +### Step by step + +### 1. Install dependencies + +```bash +pip install "feast[mcp,milvus]" +``` + +### 2. Generate sample data and apply the registry + +```bash +cd examples/agent_feature_store +python setup_data.py +``` + +This creates: +- **3 customer profiles** with attributes like plan tier, spend, and satisfaction score +- **6 knowledge-base articles** with 384-dimensional vector embeddings +- **Empty agent memory scaffold** (populated as the agent runs) + +### 3. Start the Feast MCP Feature Server + +```bash +cd feature_repo +feast serve --host 0.0.0.0 --port 6566 --workers 1 +``` + +### 4. Run the agent + +In a new terminal: + +```bash +# Without API key: runs in demo mode (simulated tool selection) +python agent.py +``` + +To run with a real LLM, set the API key and (optionally) the base URL: + +```bash +# OpenAI +export OPENAI_API_KEY="sk-..." #pragma: allowlist secret +python agent.py + +# Ollama (free, local -- no API key needed) +ollama pull llama3.1:8b +export OPENAI_API_KEY="ollama" #pragma: allowlist secret +export OPENAI_BASE_URL="http://localhost:11434/v1" +export LLM_MODEL="llama3.1:8b" +python agent.py + +# Any OpenAI-compatible provider (Azure, vLLM, LiteLLM, etc.) +export OPENAI_API_KEY="your-key" #pragma: allowlist secret +export OPENAI_BASE_URL="https://your-endpoint/v1" +export LLM_MODEL="your-model" +python agent.py +``` + +### Demo mode output + +Without an API key, the agent simulates the decision-making process with memory: + +``` +================================================================= + Scene 1: Enterprise customer (C1001) asks about SSO + Customer: C1001 | Query: "How do I set up SSO for my team?" +================================================================= + [Demo mode] Simulating agent reasoning + + Round 1 | recall_memory(customer_id=C1001) + -> No prior interactions found + + Round 1 | lookup_customer(customer_id=C1001) + -> Alice Johnson | enterprise plan | $24,500 spend | 1 open tickets + + Round 1 | search_knowledge_base(query="How do I set up SSO for my team?...") + -> Best match: "Configuring single sign-on (SSO)" + + Round 2 | Generating personalised response... + + ───────────────────────────────────────────────────────────── + Agent Response: + ───────────────────────────────────────────────────────────── + Hi Alice! + Since you're on our Enterprise plan, SSO is available for your + team. Go to Settings > Security > SSO and enter your Identity + Provider metadata URL. We support SAML 2.0 and OIDC... + + [Checkpoint] Memory saved: topic="SSO setup" + +================================================================= + Scene 4: C1001 returns -- does the agent remember Scene 1? + Customer: C1001 | Query: "I'm back about my SSO question from earlier." +================================================================= + [Demo mode] Simulating agent reasoning + + Round 1 | recall_memory(customer_id=C1001) + -> Previous topic: SSO setup + -> Open issue: none + -> Interaction count: 1 + + Round 1 | lookup_customer(customer_id=C1001) + -> Alice Johnson | enterprise plan | $24,500 spend | 1 open tickets + + Round 2 | Generating personalised response... + + ───────────────────────────────────────────────────────────── + Agent Response: + ───────────────────────────────────────────────────────────── + Welcome back, Alice! I can see from our records that we last + discussed "SSO setup". How can I help you today? + + [Checkpoint] Memory saved: topic="SSO setup" +``` + +Scene 4 demonstrates memory continuity -- the agent recalls the SSO conversation from Scene 1 without the customer re-explaining. + +### Live mode output (with API key) + +With an API key, the LLM autonomously decides which tools to use: + +``` +================================================================= + Scene 1: Enterprise customer (C1001) asks about SSO + Customer: C1001 | Query: "How do I set up SSO for my team?" +================================================================= + [Round 1] Tool call: recall_memory({'customer_id': 'C1001'}) + [Round 1] Tool call: lookup_customer({'customer_id': 'C1001'}) + [Round 1] Tool call: search_knowledge_base({'query': 'SSO setup'}) + Agent finished after 2 round(s) + + ───────────────────────────────────────────────────────────── + Agent Response: + ───────────────────────────────────────────────────────────── + Hi Alice! Since you're on our Enterprise plan, SSO is available + for your team. Go to Settings > Security > SSO and enter your + Identity Provider metadata URL. We support SAML 2.0 and OIDC... + + [Checkpoint] Memory saved: topic="SSO setup" +``` + +## How It Works + +> **Why a raw loop?** This example builds the agent from scratch using the OpenAI tool-calling API and the MCP Python SDK to keep dependencies minimal and make every Feast call visible. All Feast interactions go through the MCP protocol -- the agent connects to Feast's MCP endpoint, discovers tools dynamically, and invokes them via `session.call_tool()`. In production, you would use a framework like LangChain/LangGraph, LlamaIndex, CrewAI, or AutoGen -- Feast's MCP endpoint lets any of them auto-discover the tools with zero custom wiring (see [MCP Integration](#mcp-integration) below). + +### The Agent Loop (`agent.py`) + +```python +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +async with streamablehttp_client("http://localhost:6566/mcp") as (r, w, _): + async with ClientSession(r, w) as session: + await session.initialize() + tools = await session.list_tools() # discover Feast tools + + for round in range(MAX_ROUNDS): + # 1. Send messages + read tools to LLM + response = call_llm(messages, tools=[...]) + + # 2. If LLM says "stop" -> return the answer + if response.finish_reason == "stop": + break + + # 3. Execute tool calls via MCP + for tool_call in response.tool_calls: + result = await session.call_tool(name, args) + messages.append(tool_result(result)) + + # 4. Framework-style checkpoint: auto-save via MCP + await session.call_tool("write_to_online_store", {...}) +``` + +The LLM sees the tool definitions (JSON Schema) and decides: +- **Which tools to call** (can call zero, one, or multiple per round) +- **What arguments to pass** (e.g., which customer ID to look up) +- **When to stop** (once it has enough information to answer) + +All Feast calls go through **MCP** (`session.call_tool()`), not direct REST. Memory is saved **automatically after each turn** by the framework, not by the LLM. This mirrors how production frameworks handle persistence (see [Memory as Infrastructure](#memory-as-infrastructure)). + +### Feature Definitions (`feature_repo/features.py`) + +- **`customer_profile`**: Structured data (name, plan, spend, tickets, satisfaction) +- **`knowledge_base`**: Support articles with 384-dim vector embeddings (Milvus in this demo; swappable to Elasticsearch, Qdrant, PGVector, or FAISS) +- **`agent_memory`**: Per-customer interaction history (last topic, resolution, preferences, open issues) + +### MCP Integration + +The Feast Feature Server exposes all endpoints as MCP tools at `http://localhost:6566/mcp`. +Any MCP-compatible framework can connect: + +```python +# LangChain / LangGraph +from langchain_mcp_adapters.client import MultiServerMCPClient +from langgraph.prebuilt import create_react_agent + +async with MultiServerMCPClient( + {"feast": {"url": "http://localhost:6566/mcp", "transport": "streamable_http"}} +) as client: + tools = client.get_tools() + agent = create_react_agent(llm, tools) + result = await agent.ainvoke({"messages": "How do I set up SSO?"}) +``` + +```python +# LlamaIndex +from llama_index.tools.mcp import aget_tools_from_mcp_url +from llama_index.core.agent.function_calling import FunctionCallingAgent +from llama_index.llms.openai import OpenAI + +tools = await aget_tools_from_mcp_url("http://localhost:6566/mcp") +agent = FunctionCallingAgent.from_tools(tools, llm=OpenAI(model="gpt-4o-mini")) +response = await agent.achat("How do I set up SSO?") +``` + +```json +// Claude Desktop / Cursor +{ + "mcpServers": { + "feast": { + "url": "http://localhost:6566/mcp", + "transport": "streamable_http" + } + } +} +``` + +> **Building the same agent with a framework:** The examples above show the Feast-specific part -- connecting to the MCP endpoint and getting the tools. Once you have the tools, building the agent follows each framework's standard patterns. The key difference from this demo's raw loop: frameworks handle the tool-calling loop, message threading, and (with LangGraph checkpointers or CrewAI `memory=True`) automatic state persistence natively. Feast's MCP endpoint means zero custom integration code -- the tools are discovered and callable immediately. + +**Adapting to your use case:** The demo's system prompt, tool wrappers (`lookup_customer`, `recall_memory`), and feature views are all specific to customer support. For your own agent, you define your feature views in Feast (e.g., `product_catalog`, `order_history`, `fraud_signals`), run `feast apply`, and start the server. The same three generic MCP tools -- `get_online_features`, `retrieve_online_documents`, and `write_to_online_store` -- serve any domain. With a framework like LangChain or LlamaIndex, you don't even need custom tool wrappers -- the LLM calls the generic Feast tools directly with your feature view names and entities. + +## Production Deployment + +For production, Feast fits into a layered platform architecture: + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#E3F2FD', 'primaryBorderColor': '#1565C0', 'lineColor': '#546E7A'}}}%% +flowchart TB + Agent(("🤖 AI Agent")):::agentClass + + subgraph Platform ["🛡️ Production Platform"] + direction TB + Gateway["🔐 MCP Gateway\nJWT auth · tool filtering"]:::platformClass + Sandbox["📦 Sandboxed Container\nkernel-level isolation"]:::platformClass + Guardrails["🛑 Guardrails Orchestrator\ninput/output screening"]:::platformClass + end + + subgraph Observability ["📊 Observability + Lifecycle"] + direction LR + OTel["OpenTelemetry + MLflow\ntraces · metrics · audit"]:::obsClass + Kagenti["Kagenti Operator\nAgentCard CRDs · discovery"]:::lifecycleClass + end + + subgraph FeastSvc ["🏗️ Feast MCP Server"] + FS["/mcp · /get-online-features · /write-to-online-store"]:::feastClass + end + + subgraph Store ["🗄️ Online Store"] + direction LR + P[("👤 Profiles")]:::storageClass + K[("📚 Knowledge Base")]:::storageClass + M[("🧠 Agent Memory")]:::memoryClass + end + + Agent --> Gateway + Gateway --> Sandbox + Sandbox --> Guardrails + Guardrails --> FS + FS --> P + FS --> K + FS <--> M + OTel -.->|"traces"| FS + Kagenti -.->|"discover"| FS + + classDef agentClass fill:#E3F2FD,stroke:#1565C0,color:#0D47A1 + classDef platformClass fill:#FFEBEE,stroke:#C62828,color:#B71C1C + classDef obsClass fill:#FFF8E1,stroke:#F57F17,color:#E65100 + classDef lifecycleClass fill:#E0F2F1,stroke:#00695C,color:#004D40 + classDef feastClass fill:#FFF3E0,stroke:#E65100,color:#BF360C + classDef storageClass fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20 + classDef memoryClass fill:#F3E5F5,stroke:#6A1B9A,color:#4A148C +``` + +This demo uses Milvus Lite (embedded). For production, swap to any supported vector-capable backend by updating `feature_store.yaml`: + +- **Milvus cluster**: Deploy via the [Milvus Operator](https://milvus.io/docs/install_cluster-milvusoperator.md) and set `host`/`port` instead of `path`. +- **Elasticsearch**: Set `online_store: type: elasticsearch` with your cluster URL. +- **Qdrant**: Set `online_store: type: qdrant` with your Qdrant endpoint. +- **PGVector**: Set `online_store: type: postgres` with `pgvector_enabled: true`. +- **FAISS**: Set `online_store: type: faiss` for in-process vector search. diff --git a/examples/agent_feature_store/agent.py b/examples/agent_feature_store/agent.py new file mode 100644 index 00000000000..3297cbe393e --- /dev/null +++ b/examples/agent_feature_store/agent.py @@ -0,0 +1,816 @@ +""" +Customer-support AI agent powered by Feast features and memory via MCP. + +The LLM decides which tools to call, when to call them, and what to do +with the results. Feast acts as both the **context provider** (read) and +the **memory store** (write). + +All Feast interactions use the **Model Context Protocol (MCP)**: the agent +connects to the Feast MCP server, discovers available tools dynamically, +and invokes them through the standard protocol -- exactly how production +frameworks like LangChain, LlamaIndex, and CrewAI integrate with MCP +tool servers. + +The agent has three Feast-backed read tools: + + - lookup_customer: Retrieve customer profile features. + - search_knowledge_base: Retrieve support articles. + - recall_memory: Read past interaction context for this customer. + +Memory is automatically saved after every agent turn (framework-style +checkpointing), not as an explicit LLM tool call. This mirrors how +production frameworks like LangGraph, CrewAI, and AutoGen handle +persistence -- as infrastructure, not an LLM decision. + +Memory is entity-keyed (per customer), TTL-managed, versioned, and governed +by the same RBAC as every other feature -- unlike an ad-hoc Redis cache or +an in-process dict. + +Prerequisites: + 1. Run `python setup_data.py` to populate sample data. + 2. Start the Feast server: `cd feature_repo && feast serve --host 0.0.0.0 --port 6566 --workers 1` + 3. Set OPENAI_API_KEY (required for tool-calling). + +Usage: + python agent.py +""" + +import asyncio +import json +import os +import sys +from typing import Any + +import requests + +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +FEAST_SERVER = os.getenv("FEAST_SERVER_URL", "http://localhost:6566") +FEAST_MCP_URL = os.getenv("FEAST_MCP_URL", f"{FEAST_SERVER}/mcp") +OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "") +OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") +LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini") +MAX_TOOL_ROUNDS = 5 + +# Module-level MCP session (initialised in main) +_mcp_session: ClientSession | None = None +# Maps Feast MCP tool names discovered at startup +_feast_tools: dict[str, str] = {} + + +async def _call_feast_tool(tool_name: str, arguments: dict) -> dict: + """Call a Feast MCP tool and parse the JSON response.""" + assert _mcp_session is not None, "MCP session not initialised" + mcp_tool = _feast_tools.get(tool_name) + if not mcp_tool: + raise ValueError( + f"Feast MCP tool '{tool_name}' not found. " + f"Available: {list(_feast_tools.keys())}" + ) + result = await _mcp_session.call_tool(mcp_tool, arguments) + text = result.content[0].text if result.content else "{}" + return json.loads(text) + + +async def _discover_feast_tools() -> dict[str, str]: + """List MCP tools and build a lookup mapping logical names to MCP names.""" + assert _mcp_session is not None + tools_result = await _mcp_session.list_tools() + tool_map: dict[str, str] = {} + for tool in tools_result.tools: + name = tool.name + if "get_online_features" in name: + tool_map["get_online_features"] = name + elif "retrieve_online_documents" in name: + tool_map["retrieve_online_documents"] = name + elif "write_to_online_store" in name: + tool_map["write_to_online_store"] = name + return tool_map + + +# --------------------------------------------------------------------------- +# Tools: each wraps a Feast MCP call +# --------------------------------------------------------------------------- +# These tool specs are domain-specific wrappers for the customer-support demo. +# With a framework (LangChain, LlamaIndex), you can skip these entirely -- +# the LLM calls Feast's generic MCP tools (get_online_features, etc.) directly. + +TOOLS_SPEC = [ + { + "type": "function", + "function": { + "name": "lookup_customer", + "description": ( + "Look up a customer's profile from the feature store. Returns " + "name, email, plan tier, account age, total spend, open support " + "tickets, and satisfaction score." + ), + "parameters": { + "type": "object", + "properties": { + "customer_id": { + "type": "string", + "description": "The customer ID, e.g. 'C1001'", + } + }, + "required": ["customer_id"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "search_knowledge_base", + "description": ( + "Search the support knowledge base for articles relevant to " + "the user's question. Returns article titles, content, and " + "categories. Use this when you need product documentation or " + "how-to information." + ), + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query describing what the user needs help with", + } + }, + "required": ["query"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "recall_memory", + "description": ( + "Recall past interaction context for a customer from the memory " + "store. Returns the last topic discussed, how it was resolved, " + "interaction count, stated preferences, and any open issue. " + "Call this at the start of a conversation to personalise your " + "response based on history." + ), + "parameters": { + "type": "object", + "properties": { + "customer_id": { + "type": "string", + "description": "The customer ID to recall memory for", + } + }, + "required": ["customer_id"], + }, + }, + }, +] + + +def _parse_online_features(data: dict) -> dict[str, Any]: + """Parse the response from get-online-features into a flat dict.""" + results = data.get("results", []) + feature_names = data.get("metadata", {}).get("feature_names", []) + features: dict[str, Any] = {} + for i, name in enumerate(feature_names): + values = results[i].get("values", []) + val = values[0] if values else None + if val is not None: + features[name] = val + return features + + +async def tool_lookup_customer(customer_id: str) -> dict[str, Any]: + """Fetch customer profile features from Feast via MCP.""" + data = await _call_feast_tool( + "get_online_features", + { + "features": [ + "customer_profile:name", + "customer_profile:email", + "customer_profile:plan_tier", + "customer_profile:account_age_days", + "customer_profile:total_spend", + "customer_profile:open_tickets", + "customer_profile:satisfaction_score", + ], + "entities": {"customer_id": [customer_id]}, + }, + ) + return _parse_online_features(data) + + +async def tool_search_knowledge_base(query: str) -> list[dict[str, Any]]: + """Search knowledge-base articles from Feast via MCP.""" + data = await _call_feast_tool( + "retrieve_online_documents", + { + "features": [ + "knowledge_base:title", + "knowledge_base:content", + "knowledge_base:category", + ], + "query_string": query, + "top_k": 3, + "api_version": 2, + }, + ) + results = data.get("results", []) + feature_names = data.get("metadata", {}).get("feature_names", []) + + num_docs = len(results[0]["values"]) if results else 0 + docs = [] + for doc_idx in range(num_docs): + doc = {} + for feat_idx, name in enumerate(feature_names): + doc[name] = results[feat_idx]["values"][doc_idx] + if doc.get("title"): + docs.append(doc) + return docs + + +async def tool_recall_memory(customer_id: str) -> dict[str, Any]: + """Read the agent's memory for a customer from Feast via MCP.""" + data = await _call_feast_tool( + "get_online_features", + { + "features": [ + "agent_memory:last_topic", + "agent_memory:last_resolution", + "agent_memory:interaction_count", + "agent_memory:preferences", + "agent_memory:open_issue", + ], + "entities": {"customer_id": [customer_id]}, + }, + ) + memory = _parse_online_features(data) + has_memory = any(v is not None for k, v in memory.items() if k != "customer_id") + if not has_memory: + return {"status": "no_previous_interactions", "customer_id": customer_id} + return memory + + +async def tool_save_memory( + customer_id: str, + topic: str, + resolution: str, + open_issue: str = "", + preferences: str = "", +) -> dict[str, str]: + """Write interaction memory back to Feast via MCP.""" + from datetime import datetime, timezone + + existing = await tool_recall_memory(customer_id) + prev_count = existing.get("interaction_count", 0) + prev_preferences = existing.get("preferences", "") + prev_open_issue = existing.get("open_issue", "") + + now = datetime.now(timezone.utc).isoformat() + await _call_feast_tool( + "write_to_online_store", + { + "feature_view_name": "agent_memory", + "df": { + "customer_id": [customer_id], + "last_topic": [topic], + "last_resolution": [resolution], + "interaction_count": [prev_count + 1], + "preferences": [preferences or prev_preferences], + "open_issue": [open_issue or prev_open_issue], + "event_timestamp": [now], + }, + "allow_registry_cache": True, + }, + ) + return {"status": "saved", "customer_id": customer_id, "topic": topic} + + +TOOL_REGISTRY: dict[str, Any] = { + "lookup_customer": lambda args: tool_lookup_customer( + args.get("customer_id") or next(iter(args.values())) + ), + "search_knowledge_base": lambda args: tool_search_knowledge_base( + args.get("query") or args.get("search_query") or next(iter(args.values())) + ), + "recall_memory": lambda args: tool_recall_memory( + args.get("customer_id") or next(iter(args.values())) + ), +} + + +# --------------------------------------------------------------------------- +# Agent loop +# --------------------------------------------------------------------------- + +# Demo-specific prompt: replace with your own domain instructions and tool +# names when adapting this example to a different use case. +SYSTEM_PROMPT = """\ +You are a customer-support agent. You MUST follow these steps in order: + +1. ALWAYS call BOTH recall_memory AND lookup_customer in your first round. + Call them together in the same round. You MUST call lookup_customer even if + recall_memory returns no history -- you need the customer's name and plan. +2. If the question is about a product feature (SSO, API, invoices, passwords, + upgrades, etc.), also call search_knowledge_base with a short keyword. +3. Once you have the tool results, write a helpful, personalised answer. + Use the customer's name and plan tier. Enterprise customers get full access; + starter/pro customers may need to upgrade for certain features. + +Memory is saved automatically -- do NOT try to save it yourself. + +Rules: +- Never call the same tool twice with the same arguments. +- After you have tool results, WRITE your answer immediately. Do not call more tools. +""" + + +def _call_llm(messages: list, use_tools: bool = True) -> dict: + """Make a single LLM API call, returning the parsed choice dict.""" + url = f"{OPENAI_BASE_URL}/chat/completions" + payload: dict[str, Any] = { + "model": LLM_MODEL, + "messages": messages, + "temperature": 0.3, + } + if use_tools: + payload["tools"] = TOOLS_SPEC + resp = requests.post( + url, + headers={ + "Authorization": f"Bearer {OPENAI_API_KEY}", + "Content-Type": "application/json", + }, + json=payload, + ) + if not resp.ok: + print(f"\n ERROR {resp.status_code} from {url}") + print(f" Response: {resp.text[:300]}") + print( + "\n Hint: set OPENAI_BASE_URL to an OpenAI-compatible endpoint.\n" + " Examples:\n" + " OpenAI: export OPENAI_BASE_URL=https://api.openai.com/v1\n" + " Ollama: export OPENAI_BASE_URL=http://localhost:11434/v1\n" + ) + resp.raise_for_status() + return resp.json()["choices"][0] + + +async def run_agent(customer_id: str, user_message: str) -> str: + """ + Agentic loop: the LLM decides which tools to call (if any), executes + them, feeds results back, and repeats until it produces a final answer. + Memory is auto-saved after every turn -- framework-style checkpointing, + not an LLM decision. + """ + if not OPENAI_API_KEY: + response = await _run_agent_demo_mode(customer_id, user_message) + else: + response = await _run_agent_llm(customer_id, user_message) + + await _auto_save_memory(customer_id, user_message, response) + return response + + +async def _auto_save_memory(customer_id: str, user_message: str, response: str) -> None: + """ + Framework-style memory checkpoint: automatically persist interaction + context after every agent turn. + + Production agent frameworks (LangGraph checkpointers, CrewAI memory, + AutoGen teachable agents) all treat memory as infrastructure -- the + framework saves state after each step, rather than relying on the LLM + to decide when to persist. This ensures consistent, reliable memory + regardless of LLM behaviour. + """ + topic = _extract_topic(user_message) + resolution = response[:120] if response else "Answered query" + try: + await tool_save_memory( + customer_id=customer_id, + topic=topic, + resolution=resolution, + ) + print(f' [Checkpoint] Memory saved: topic="{topic}"') + except Exception as e: + print(f" [Checkpoint] Failed to save memory: {e}") + + +async def _run_agent_llm(customer_id: str, user_message: str) -> str: + """Core LLM agent loop. Returns the response text.""" + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + { + "role": "user", + "content": f"[Customer ID: {customer_id}]\n\n{user_message}", + }, + ] + + seen_calls: set[str] = set() + collected_context: dict[str, Any] = {} + + for round_num in range(1, MAX_TOOL_ROUNDS + 1): + choice = _call_llm(messages) + assistant_msg = choice["message"] + messages.append(assistant_msg) + + content = assistant_msg.get("content") or "" + if choice["finish_reason"] == "stop": + if content: + print(f" ✓ Agent finished after {round_num} round(s)") + return content + break # empty stop -- fall through to forced response + + tool_calls = assistant_msg.get("tool_calls", []) + if not tool_calls: + if content: + return content + break + + tool_names = [tc["function"]["name"] for tc in tool_calls] + print(f" 🔧 Round {round_num}: LLM chose tool(s): {', '.join(tool_names)}") + + for tc in tool_calls: + fn_name = tc["function"]["name"] + fn_args = json.loads(tc["function"]["arguments"]) + call_key = f"{fn_name}:{json.dumps(fn_args, sort_keys=True)}" + + if call_key in seen_calls: + print(f" [Round {round_num}] Skipping duplicate: {fn_name}({fn_args})") + messages.append( + { + "role": "tool", + "tool_call_id": tc["id"], + "content": json.dumps( + { + "note": "Already called. Use the results you have and respond." + } + ), + } + ) + continue + + seen_calls.add(call_key) + print(f" [Round {round_num}] ➜ {fn_name}({fn_args})") + + handler = TOOL_REGISTRY.get(fn_name) + if handler: + result = await handler(fn_args) + result_str = json.dumps(result, default=str) + collected_context[fn_name] = result + else: + result_str = json.dumps({"error": f"Unknown tool: {fn_name}"}) + + messages.append( + { + "role": "tool", + "tool_call_id": tc["id"], + "content": result_str, + } + ) + + # If the LLM never produced a response, force one last call without tools + print(" ⏳ Forcing final response...") + messages.append( + { + "role": "user", + "content": "You have all the information. Write your answer to the customer now.", + } + ) + choice = _call_llm(messages, use_tools=False) + content = choice["message"].get("content") or "" + if content: + return content + + # Last resort: build a response from collected tool results + return _fallback_response(collected_context, customer_id, user_message) + + +def _fallback_response(context: dict, customer_id: str, user_message: str) -> str: + """Build a basic response from collected tool results when the LLM fails.""" + profile = context.get("lookup_customer", {}) + name = profile.get("name", customer_id) + plan = profile.get("plan_tier", "your") + memory = context.get("recall_memory", {}) + + parts = [] + if memory and memory.get("last_topic"): + parts.append( + f'Welcome back, {name}! I see we last discussed "{memory["last_topic"]}".' + ) + else: + parts.append(f"Hi {name}!") + + parts.append( + f"You're on the {plan} plan. I've noted your question about " + f'"{user_message[:50]}" and will follow up.' + ) + return " ".join(parts) + + +async def _run_agent_demo_mode(customer_id: str, user_message: str) -> str: + """ + Demo mode: simulates the agentic tool-calling flow and generates a + personalised response that shows how Feast context shapes the answer. + """ + print(" [Demo mode] Simulating agent reasoning\n") + + # ── Round 1: recall memory ────────────────────────────────────────── + print(f" Round 1 | recall_memory(customer_id={customer_id})") + memory = await tool_recall_memory(customer_id) + has_memory = memory.get("status") != "no_previous_interactions" + if has_memory: + print(f" -> Previous topic: {memory.get('last_topic')}") + print(f" -> Open issue: {memory.get('open_issue') or 'none'}") + print(f" -> Interaction count: {memory.get('interaction_count')}") + else: + print(" -> No prior interactions found") + + # ── Round 1: lookup customer ──────────────────────────────────────── + print(f"\n Round 1 | lookup_customer(customer_id={customer_id})") + profile = await tool_lookup_customer(customer_id) + name = profile.get("name", "Customer") + plan = profile.get("plan_tier", "unknown") + spend = profile.get("total_spend", 0) + tickets = profile.get("open_tickets", 0) + print( + f" -> {name} | {plan} plan | ${spend:,.0f} spend | {tickets} open tickets" + ) + + # ── Round 1: search knowledge base (if question needs docs) ───────── + needs_kb = any( + kw in user_message.lower() + for kw in [ + "how", + "what", + "set up", + "configure", + "reset", + "help", + "sso", + "api", + "invoice", + "upgrade", + "password", + ] + ) + kb_article = None + if needs_kb: + print(f'\n Round 1 | search_knowledge_base(query="{user_message[:50]}...")') + docs = await tool_search_knowledge_base(user_message) + if docs: + kb_article = _pick_best_article(user_message, docs) + print(f' -> Best match: "{kb_article.get("title")}"') + + # ── Round 2: generate response (simulated LLM reasoning) ──────────── + print("\n Round 2 | Generating personalised response...") + response = _build_demo_response( + name=name, + plan=plan, + profile=profile, + memory=memory if has_memory else None, + kb_article=kb_article, + user_message=user_message, + ) + + return response + + +def _pick_best_article(query: str, docs: list) -> dict: + """Simple keyword matching to select the most relevant article.""" + query_lower = query.lower() + keywords_to_category = { + "sso": "Configuring single sign-on", + "single sign": "Configuring single sign-on", + "password": "How to reset your password", # pragma: allowlist secret + "reset": "How to reset your password", + "invoice": "Understanding your invoice", + "billing": "Understanding your invoice", + "upgrade": "Upgrading your subscription", + "plan": "Upgrading your subscription", + "api": "Setting up API access", + "rate limit": "Setting up API access", + "support": "Contacting support", + "contact": "Contacting support", + } + for keyword, title_prefix in keywords_to_category.items(): + if keyword in query_lower: + for doc in docs: + if doc.get("title", "").startswith(title_prefix): + return doc + return docs[0] + + +def _extract_topic(message: str) -> str: + """Extract a short topic label from the user message.""" + topic_map = { + "sso": "SSO setup", + "invoice": "Invoice help", + "upgrade": "Plan upgrade", + "api": "API access", + "password": "Password reset", # pragma: allowlist secret + "reset": "Password reset", + } + lower = message.lower() + for keyword, topic in topic_map.items(): + if keyword in lower: + return topic + return message[:40] + + +def _build_demo_response( + name: str, + plan: str, + profile: dict, + memory: dict | None, + kb_article: dict | None, + user_message: str, +) -> str: + """Build a realistic personalised response based on Feast context.""" + parts = [] + + # Acknowledge returning customer if we have memory + if memory and memory.get("last_topic"): + parts.append( + f"Welcome back, {name}! I can see from our records that we last " + f'discussed "{memory["last_topic"]}".' + ) + if memory.get("open_issue"): + parts.append( + f"I also notice you have an open issue: {memory['open_issue']}. " + "Let me know if you'd like to follow up on that." + ) + else: + parts.append(f"Hi {name}!") + + # Role-based response logic + lower = user_message.lower() + + if "sso" in lower: + if plan == "enterprise": + parts.append( + "Since you're on our Enterprise plan, SSO is available for your " + "team. Go to Settings > Security > SSO and enter your Identity " + "Provider metadata URL. We support SAML 2.0 and OIDC. Once " + "configured, all team members will authenticate through your IdP." + ) + parts.append( + "As an Enterprise customer, you also have a dedicated Slack " + "channel and account manager if you need hands-on help." + ) + elif plan == "pro": + parts.append( + "SSO is only available on our Enterprise plan. You're currently " + "on the Pro plan. Would you like to learn about upgrading? The " + "Enterprise plan includes SSO, priority support, and a dedicated " + "account manager." + ) + else: + parts.append( + "SSO is an Enterprise-only feature. You're currently on the " + f"Starter plan (${profile.get('total_spend', 0):,.0f} total spend). " + "You'd need to upgrade to Enterprise to access SSO. I can walk " + "you through the upgrade options if you're interested." + ) + + elif "invoice" in lower: + parts.append( + "Invoices are generated on the first of each month and sent to " + f"{profile.get('email', 'your billing email')}." + ) + if plan == "enterprise": + parts.append( + "As an Enterprise customer, your invoice includes base plan " + "charges, any overage fees, and applied credits. You can also " + "reach your dedicated account manager for a detailed breakdown." + ) + else: + parts.append( + "You can download past invoices from Billing > Invoices. Each " + "invoice shows base charges and any overages." + ) + if profile.get("open_tickets", 0) > 0: + parts.append( + f"I also see you have {profile['open_tickets']} open support " + "ticket(s) -- let me know if any are billing-related." + ) + + elif "upgrade" in lower or "api" in lower: + if plan == "starter": + parts.append( + "Great question! You're on the Starter plan. Upgrading to Pro " + "gives you API access with 1,000 requests/minute. Enterprise " + "gets you 5,000 req/min plus priority support. The price " + "difference is prorated for your current billing cycle." + ) + elif plan == "pro": + parts.append( + "You're on the Pro plan with 1,000 API requests/minute. " + "Upgrading to Enterprise would give you 5,000 req/min, SSO, " + f"and a dedicated account manager. Given your ${profile.get('total_spend', 0):,.0f} " + "total spend, I can check if there are any loyalty discounts available." + ) + else: + parts.append( + "You're already on our Enterprise plan with the highest rate " + "limits (5,000 req/min). If you need even higher throughput, " + "I can connect you with your account manager to discuss custom limits." + ) + + elif memory and memory.get("last_topic"): + parts.append( + f"Yes, I have the full context from our previous conversation about " + f'"{memory["last_topic"]}". ' + f"We've now had {memory.get('interaction_count', 1)} interaction(s). " + "How can I help you today?" + ) + + else: + parts.append("How can I help you today?") + + return "\n".join(parts) + + +# --------------------------------------------------------------------------- +# Demo queries +# --------------------------------------------------------------------------- + +DEMO_QUERIES = [ + # Scene 1: Enterprise customer asks about SSO -- should get full access instructions + ("C1001", "How do I set up SSO for my team?"), + # Scene 2: Starter customer asks the SAME question -- should be told it's Enterprise-only + ("C1003", "How do I set up SSO for my team?"), + # Scene 3: Pro customer asks about invoices -- response uses their email/ticket context + ("C1002", "I need help understanding my last invoice."), + # Scene 4: C1001 returns -- agent should recall the SSO conversation from Scene 1 + ("C1001", "I'm back about my SSO question from earlier."), +] + + +async def main(): + global _mcp_session, _feast_tools + + print("=" * 65) + print(" Feast-Powered AI Agent Demo: Context + Memory via MCP") + print("=" * 65) + print() + print(" This demo shows two key capabilities:") + print(" 1. ROLE-BASED RESPONSES: Same question, different answer per plan tier") + print(" 2. PERSISTENT MEMORY: Agent recalls prior conversations via Feast") + print() + print(" Tools: recall_memory | lookup_customer | search_knowledge_base") + print(" Memory: auto-saved after each turn (framework-style checkpoint)") + print(f" Protocol: MCP ({FEAST_MCP_URL})") + print() + + try: + resp = requests.get(f"{FEAST_SERVER}/health") + resp.raise_for_status() + print(f"Feast server: healthy at {FEAST_SERVER}") + except (requests.ConnectionError, requests.HTTPError) as exc: + print(f"ERROR: Cannot reach Feast server at {FEAST_SERVER} ({exc})") + print( + "Start it with: cd feature_repo && feast serve --host 0.0.0.0 --port 6566 --workers 1" + ) + sys.exit(1) + + async with streamablehttp_client(FEAST_MCP_URL) as (read_stream, write_stream, _): + async with ClientSession(read_stream, write_stream) as session: + _mcp_session = session + await session.initialize() + + _feast_tools = await _discover_feast_tools() + print(f"MCP tools discovered: {', '.join(_feast_tools.values())}") + + if not OPENAI_API_KEY: + print( + "OPENAI_API_KEY not set -- running in demo mode (simulated reasoning)\n" + ) + else: + print(f"Using LLM: {LLM_MODEL} via {OPENAI_BASE_URL}\n") + + scene_labels = [ + "Scene 1: Enterprise customer (C1001) asks about SSO", + "Scene 2: Starter customer (C1003) asks the SAME SSO question", + "Scene 3: Pro customer (C1002) asks about invoices", + "Scene 4: C1001 returns -- does the agent remember Scene 1?", + ] + + for i, (customer_id, query) in enumerate(DEMO_QUERIES): + label = scene_labels[i] if i < len(scene_labels) else "" + print(f"\n{'=' * 65}") + print(f" {label}") + print(f' Customer: {customer_id} | Query: "{query}"') + print(f"{'=' * 65}") + + response = await run_agent(customer_id, query) + + print(f"\n {'─' * 61}") + print(" Agent Response:") + print(f" {'─' * 61}") + for line in response.split("\n"): + print(f" {line}") + print() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/agent_feature_store/feature_repo/feature_store.yaml b/examples/agent_feature_store/feature_repo/feature_store.yaml new file mode 100644 index 00000000000..7cc89d536c8 --- /dev/null +++ b/examples/agent_feature_store/feature_repo/feature_store.yaml @@ -0,0 +1,30 @@ +project: feast_agent +provider: local +registry: data/registry.db + +online_store: + type: milvus + path: data/online_store.db + vector_enabled: true + embedding_dim: 384 + index_type: "IVF_FLAT" + metric_type: "COSINE" + nlist: 128 + +offline_store: + type: file + +entity_key_serialization_version: 3 + +feature_server: + type: mcp + enabled: true + mcp_enabled: true + mcp_transport: http + mcp_server_name: "feast-agent-demo" + mcp_server_version: "1.0.0" + feature_logging: + enabled: false + +auth: + type: no_auth diff --git a/examples/agent_feature_store/feature_repo/features.py b/examples/agent_feature_store/feature_repo/features.py new file mode 100644 index 00000000000..935b9cb03c0 --- /dev/null +++ b/examples/agent_feature_store/feature_repo/features.py @@ -0,0 +1,83 @@ +from datetime import timedelta + +from feast import Entity, FeatureView, Field, FileSource +from feast.data_format import ParquetFormat +from feast.types import Array, Float32, Float64, Int64, String, ValueType + +customer = Entity( + name="customer_id", + description="Unique customer identifier", + value_type=ValueType.STRING, +) + +document = Entity( + name="doc_id", + description="Knowledge-base document chunk identifier", + value_type=ValueType.INT64, +) + +customer_profile_source = FileSource( + file_format=ParquetFormat(), + path="data/customer_profiles.parquet", + timestamp_field="event_timestamp", +) + +knowledge_base_source = FileSource( + file_format=ParquetFormat(), + path="data/knowledge_base.parquet", + timestamp_field="event_timestamp", +) + +agent_memory_source = FileSource( + file_format=ParquetFormat(), + path="data/agent_memory.parquet", + timestamp_field="event_timestamp", +) + +customer_profile = FeatureView( + name="customer_profile", + entities=[customer], + schema=[ + Field(name="name", dtype=String), + Field(name="email", dtype=String), + Field(name="plan_tier", dtype=String), + Field(name="account_age_days", dtype=Int64), + Field(name="total_spend", dtype=Float64), + Field(name="open_tickets", dtype=Int64), + Field(name="satisfaction_score", dtype=Float64), + ], + source=customer_profile_source, + ttl=timedelta(days=1), +) + +knowledge_base = FeatureView( + name="knowledge_base", + entities=[document], + schema=[ + Field( + name="vector", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="title", dtype=String), + Field(name="content", dtype=String), + Field(name="category", dtype=String), + ], + source=knowledge_base_source, + ttl=timedelta(days=7), +) + +agent_memory = FeatureView( + name="agent_memory", + entities=[customer], + schema=[ + Field(name="last_topic", dtype=String), + Field(name="last_resolution", dtype=String), + Field(name="interaction_count", dtype=Int64), + Field(name="preferences", dtype=String), + Field(name="open_issue", dtype=String), + ], + source=agent_memory_source, + ttl=timedelta(days=30), +) diff --git a/examples/agent_feature_store/run_demo.sh b/examples/agent_feature_store/run_demo.sh new file mode 100755 index 00000000000..487ed4a7c6e --- /dev/null +++ b/examples/agent_feature_store/run_demo.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# +# One-command setup and demo for the Feast-powered AI agent example. +# +# Usage: +# cd examples/agent_feature_store +# ./run_demo.sh # demo mode (no API key needed) +# OPENAI_API_KEY=sk-... ./run_demo.sh # live LLM tool-calling +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +PYTHON="${PYTHON:-$(command -v python3 || command -v python || true)}" +if [[ -z "$PYTHON" ]]; then + echo "ERROR: python3 or python not found on PATH." + exit 1 +fi +PIP="${PIP:-$(command -v pip3 || command -v pip || true)}" +if [[ -z "$PIP" ]]; then + echo "ERROR: pip3 or pip not found on PATH." + exit 1 +fi + +SERVER_PORT=6566 +SERVER_PID="" + +cleanup() { + if [[ -n "$SERVER_PID" ]]; then + echo "" + echo "Stopping Feast server (pid $SERVER_PID)..." + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# ── 1. Install dependencies ───────────────────────────────────────────────── +echo "==> Step 1/4: Installing dependencies..." +$PIP install -q "feast[mcp,milvus]" + +# ── 2. Generate data and apply registry ────────────────────────────────────── +echo "" +echo "==> Step 2/4: Generating sample data and applying Feast registry..." +$PYTHON setup_data.py + +# ── 3. Start the Feast MCP server in the background ───────────────────────── +echo "" +echo "==> Step 3/4: Starting Feast MCP feature server on port $SERVER_PORT..." +cd feature_repo +feast serve --host 0.0.0.0 --port "$SERVER_PORT" --workers 1 & +SERVER_PID=$! +cd "$SCRIPT_DIR" + +echo " Waiting for server to become healthy..." +for i in $(seq 1 30); do + if curl -sf "http://localhost:${SERVER_PORT}/health" > /dev/null 2>&1; then + echo " Server is ready." + break + fi + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + echo "ERROR: Server process exited unexpectedly." + exit 1 + fi + sleep 1 +done + +if ! curl -sf "http://localhost:${SERVER_PORT}/health" > /dev/null 2>&1; then + echo "ERROR: Server did not become healthy within 30 seconds." + exit 1 +fi + +# ── 4. Run the agent ──────────────────────────────────────────────────────── +echo "" +echo "==> Step 4/4: Running the agent..." +echo "" +$PYTHON agent.py + +echo "" +echo "Demo complete." diff --git a/examples/agent_feature_store/setup_data.py b/examples/agent_feature_store/setup_data.py new file mode 100644 index 00000000000..cd88e008af4 --- /dev/null +++ b/examples/agent_feature_store/setup_data.py @@ -0,0 +1,201 @@ +""" +Generates sample data, applies the Feast registry, and materializes features +into the online store so the agent demo is ready to run. + +Usage: + cd examples/agent_feature_store + python setup_data.py +""" + +import os +import sys + +import numpy as np +import pandas as pd + +REPO_DIR = os.path.join(os.path.dirname(__file__), "feature_repo") +DATA_DIR = os.path.join(REPO_DIR, "data") +os.makedirs(DATA_DIR, exist_ok=True) + +EMBEDDING_DIM = 384 +NOW = pd.Timestamp.now() + + +def generate_customer_profiles() -> pd.DataFrame: + customers = [ + { + "customer_id": "C1001", + "name": "Alice Johnson", + "email": "alice@example.com", + "plan_tier": "enterprise", + "account_age_days": 730, + "total_spend": 24500.00, + "open_tickets": 1, + "satisfaction_score": 4.5, + }, + { + "customer_id": "C1002", + "name": "Bob Smith", + "email": "bob@example.com", + "plan_tier": "pro", + "account_age_days": 365, + "total_spend": 8400.00, + "open_tickets": 3, + "satisfaction_score": 3.2, + }, + { + "customer_id": "C1003", + "name": "Carol Lee", + "email": "carol@example.com", + "plan_tier": "starter", + "account_age_days": 90, + "total_spend": 990.00, + "open_tickets": 0, + "satisfaction_score": 4.8, + }, + ] + df = pd.DataFrame(customers) + df["event_timestamp"] = NOW + return df + + +def generate_knowledge_base() -> pd.DataFrame: + articles = [ + { + "doc_id": 1, + "title": "How to reset your password", + "content": ( + "To reset your password, go to Settings > Security > Reset Password. " + "Enter your current password, then choose a new one that is at least " + "12 characters long. Click Save. If you forgot your current password, " + "click 'Forgot Password' on the login page to receive a reset link " + "via email." + ), + "category": "account", + }, + { + "doc_id": 2, + "title": "Upgrading your subscription plan", + "content": ( + "You can upgrade your plan from Starter to Pro or Enterprise at any " + "time. Navigate to Billing > Plans and select the plan you want. " + "The price difference is prorated for the current billing cycle. " + "Enterprise plans include priority support, custom integrations, " + "and a dedicated account manager." + ), + "category": "billing", + }, + { + "doc_id": 3, + "title": "Setting up API access", + "content": ( + "To generate an API key, go to Settings > Developer > API Keys and " + "click 'Create New Key'. Choose the appropriate scopes for your use " + "case. API keys are tied to your account and inherit your permissions. " + "Rate limits are 1000 requests/minute for Pro and 5000 for Enterprise." + ), + "category": "developer", + }, + { + "doc_id": 4, + "title": "Understanding your invoice", + "content": ( + "Invoices are generated on the first of each month and sent to the " + "billing email on file. Each invoice includes a breakdown of base plan " + "charges, overage fees, and any credits applied. You can download past " + "invoices from Billing > Invoices." + ), + "category": "billing", + }, + { + "doc_id": 5, + "title": "Configuring single sign-on (SSO)", + "content": ( + "SSO is available on Enterprise plans. To configure SSO, go to " + "Settings > Security > SSO and provide your Identity Provider (IdP) " + "metadata URL. We support SAML 2.0 and OIDC. Once configured, all " + "team members will authenticate through your IdP." + ), + "category": "account", + }, + { + "doc_id": 6, + "title": "Contacting support", + "content": ( + "You can reach our support team via the in-app chat widget, by " + "emailing support@example.com, or by opening a ticket at " + "https://support.example.com. Enterprise customers have access to " + "a dedicated Slack channel and a named account manager with a " + "guaranteed 1-hour response time." + ), + "category": "support", + }, + ] + + np.random.seed(42) + df = pd.DataFrame(articles) + df["vector"] = [ + np.random.randn(EMBEDDING_DIM).astype(np.float32).tolist() + for _ in range(len(df)) + ] + df["event_timestamp"] = NOW + return df + + +def main(): + print("Generating customer profile data...") + customers_df = generate_customer_profiles() + customers_path = os.path.join(DATA_DIR, "customer_profiles.parquet") + customers_df.to_parquet(customers_path, index=False) + print(f" Saved {len(customers_df)} customer profiles to {customers_path}") + + print("Generating knowledge-base data...") + kb_df = generate_knowledge_base() + kb_path = os.path.join(DATA_DIR, "knowledge_base.parquet") + kb_df.to_parquet(kb_path, index=False) + print(f" Saved {len(kb_df)} knowledge-base articles to {kb_path}") + + print("Generating empty agent memory scaffold...") + memory_df = pd.DataFrame( + { + "customer_id": pd.Series(dtype="str"), + "last_topic": pd.Series(dtype="str"), + "last_resolution": pd.Series(dtype="str"), + "interaction_count": pd.Series(dtype="int64"), + "preferences": pd.Series(dtype="str"), + "open_issue": pd.Series(dtype="str"), + "event_timestamp": pd.Series(dtype="datetime64[ns]"), + } + ) + memory_path = os.path.join(DATA_DIR, "agent_memory.parquet") + memory_df.to_parquet(memory_path, index=False) + print(f" Saved empty memory scaffold to {memory_path}") + + print("Applying Feast registry...") + sys.path.insert(0, REPO_DIR) + from feast import FeatureStore + from features import ( + agent_memory, + customer, + customer_profile, + document, + knowledge_base, + ) + + store = FeatureStore(repo_path=REPO_DIR) + store.apply([customer, document, customer_profile, knowledge_base, agent_memory]) + + print("Materializing customer profiles to the online store...") + store.write_to_online_store(feature_view_name="customer_profile", df=customers_df) + print(" Done.") + + print("Materializing knowledge-base to the online store...") + store.write_to_online_store(feature_view_name="knowledge_base", df=kb_df) + print(" Done.") + + print("\nSetup complete! Start the feature server with:") + print(" cd feature_repo && feast serve --host 0.0.0.0 --port 6566") + + +if __name__ == "__main__": + main() diff --git a/examples/java-demo/feature_repo/driver_repo.py b/examples/java-demo/feature_repo/driver_repo.py index f7dd05afff7..edb3e1e9d89 100644 --- a/examples/java-demo/feature_repo/driver_repo.py +++ b/examples/java-demo/feature_repo/driver_repo.py @@ -14,7 +14,10 @@ timestamp_field="event_timestamp", created_timestamp_column="created", ) -driver = Entity(name="driver_id", description="driver id",) +driver = Entity( + name="driver_id", + description="driver id", +) driver_hourly_stats_view = FeatureView( name="driver_hourly_stats", entities=[driver], @@ -58,4 +61,3 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] return df - diff --git a/examples/java-demo/feature_repo/test.py b/examples/java-demo/feature_repo/test.py index f73883019d6..9dd0c34a1f6 100644 --- a/examples/java-demo/feature_repo/test.py +++ b/examples/java-demo/feature_repo/test.py @@ -20,9 +20,13 @@ def fetch_java(): print( stub.GetOnlineFeatures( - GetOnlineFeaturesRequest(features=feature_refs, entities=entity_rows,) + GetOnlineFeaturesRequest( + features=feature_refs, + entities=entity_rows, + ) ) ) + if __name__ == "__main__": fetch_java() diff --git a/examples/java-demo/feature_repo/test_python_fetch.py b/examples/java-demo/feature_repo/test_python_fetch.py index 5e2781e1508..0edc05c6603 100644 --- a/examples/java-demo/feature_repo/test_python_fetch.py +++ b/examples/java-demo/feature_repo/test_python_fetch.py @@ -15,7 +15,7 @@ def run_demo(): }, { "driver_id": 1002, - } + }, ], ).to_dict() for key, value in sorted(features.items()): diff --git a/examples/kind-quickstart/src/utils.py b/examples/kind-quickstart/src/utils.py index ea549d7ed8c..34d674a088a 100644 --- a/examples/kind-quickstart/src/utils.py +++ b/examples/kind-quickstart/src/utils.py @@ -1,12 +1,18 @@ import subprocess -def port_forward(service, external_port, local_port=80) : - """ - Run a background process to forward port 80 of the given `service` service to the given `external_port` port. - Returns: the process instance - """ - command = ["kubectl", "port-forward", f"service/{service}", f"{external_port}:{local_port}"] - process = subprocess.Popen(command) - print(f"Port-forwarding {service} with process ID: {process.pid}") - return process +def port_forward(service, external_port, local_port=80): + """ + Run a background process to forward port 80 of the given `service` service to the given `external_port` port. + + Returns: the process instance + """ + command = [ + "kubectl", + "port-forward", + f"service/{service}", + f"{external_port}:{local_port}", + ] + process = subprocess.Popen(command) + print(f"Port-forwarding {service} with process ID: {process.pid}") + return process diff --git a/examples/mcp_feature_store/feature_store.yaml b/examples/mcp_feature_store/feature_store.yaml index 305be159956..82029eb111f 100644 --- a/examples/mcp_feature_store/feature_store.yaml +++ b/examples/mcp_feature_store/feature_store.yaml @@ -14,9 +14,10 @@ feature_server: type: mcp enabled: true mcp_enabled: true # Enable MCP support - defaults to false + mcp_transport: http mcp_server_name: "feast-feature-store" mcp_server_version: "1.0.0" feature_logging: enabled: false -entity_key_serialization_version: 3 \ No newline at end of file +entity_key_serialization_version: 3 diff --git a/examples/monitoring/monitoring-quickstart.ipynb b/examples/monitoring/monitoring-quickstart.ipynb new file mode 100644 index 00000000000..77101ffff51 --- /dev/null +++ b/examples/monitoring/monitoring-quickstart.ipynb @@ -0,0 +1,1256 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Feature Quality Monitoring — Quickstart\n", + "\n", + "This notebook walks you through Feast's data quality monitoring end-to-end:\n", + "\n", + "1. Set up a feature store with a PostgreSQL offline store\n", + "2. Register features and trigger baseline computation\n", + "3. Compute metrics across multiple granularities\n", + "4. Read metrics via the Python SDK and REST API\n", + "5. Set up serving log monitoring\n", + "6. Use on-demand exploration for custom date ranges\n", + "\n", + "**Prerequisites:** A running PostgreSQL instance and `feast[postgres]` installed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Install Feast" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "!uv pip install -q 'feast[postgres]'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Configure the Feature Store\n", + "\n", + "Create a minimal `feature_store.yaml` with a PostgreSQL offline store." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Working directory: /var/folders/cn/z7vz24yj25d8fjqdrs9jbsh00000gn/T/feast_monitoring_demo_kze7m3sk\n" + ] + } + ], + "source": [ + "import os\n", + "import tempfile\n", + "\n", + "REPO_DIR = tempfile.mkdtemp(prefix=\"feast_monitoring_demo_\")\n", + "os.makedirs(REPO_DIR, exist_ok=True)\n", + "print(f\"Working directory: {REPO_DIR}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "feature_store.yaml written.\n" + ] + } + ], + "source": [ + "# Adjust these to match your PostgreSQL instance\n", + "PG_HOST = os.environ.get(\"FEAST_PG_HOST\", \"localhost\")\n", + "PG_PORT = os.environ.get(\"FEAST_PG_PORT\", \"5432\")\n", + "PG_DB = os.environ.get(\"FEAST_PG_DB\", \"feast\")\n", + "PG_USER = os.environ.get(\"FEAST_PG_USER\", \"feast\")\n", + "PG_PASS = os.environ.get(\"FEAST_PG_PASS\", \"feast\")\n", + "\n", + "PG_SSLMODE = os.environ.get(\"FEAST_PG_SSLMODE\", \"disable\")\n", + "\n", + "feature_store_yaml = f\"\"\"\n", + "project: monitoring_demo\n", + "registry:\n", + " registry_type: sql\n", + " path: postgresql://{PG_USER}:{PG_PASS}@{PG_HOST}:{PG_PORT}/{PG_DB}?sslmode={PG_SSLMODE}\n", + "provider: local\n", + "offline_store:\n", + " type: postgres\n", + " host: {PG_HOST}\n", + " port: {PG_PORT}\n", + " database: {PG_DB}\n", + " user: {PG_USER}\n", + " password: {PG_PASS}\n", + " sslmode: {PG_SSLMODE}\n", + "online_store:\n", + " type: sqlite\n", + " path: {REPO_DIR}/online_store.db\n", + "entity_key_serialization_version: 3\n", + "\"\"\"\n", + "\n", + "with open(os.path.join(REPO_DIR, \"feature_store.yaml\"), \"w\") as f:\n", + " f.write(feature_store_yaml)\n", + "\n", + "print(\"feature_store.yaml written.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: Create Sample Data and Feature Definitions" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample data: 5000 rows, 60 days\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
driver_idevent_timestampconv_rateacc_rateavg_daily_tripsvehicle_typecreated
011482025-02-080.3483070.79439014compact2025-02-08
115392025-02-210.3059450.74904625van2025-02-21
214872025-01-290.7916410.78449217sedan2025-01-29
318212025-01-150.2673080.72622617sedan2025-01-15
414372025-02-120.5446180.72956811suv2025-02-12
\n", + "
" + ], + "text/plain": [ + " driver_id event_timestamp conv_rate acc_rate avg_daily_trips \\\n", + "0 1148 2025-02-08 0.348307 0.794390 14 \n", + "1 1539 2025-02-21 0.305945 0.749046 25 \n", + "2 1487 2025-01-29 0.791641 0.784492 17 \n", + "3 1821 2025-01-15 0.267308 0.726226 17 \n", + "4 1437 2025-02-12 0.544618 0.729568 11 \n", + "\n", + " vehicle_type created \n", + "0 compact 2025-02-08 \n", + "1 van 2025-02-21 \n", + "2 sedan 2025-01-29 \n", + "3 sedan 2025-01-15 \n", + "4 suv 2025-02-12 " + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "\n", + "np.random.seed(42)\n", + "\n", + "N_ROWS = 5000\n", + "N_DAYS = 60\n", + "\n", + "base_date = datetime(2025, 1, 1)\n", + "timestamps = [base_date + timedelta(days=int(d)) for d in np.random.randint(0, N_DAYS, N_ROWS)]\n", + "\n", + "df = pd.DataFrame({\n", + " \"driver_id\": np.random.randint(1000, 2000, N_ROWS),\n", + " \"event_timestamp\": timestamps,\n", + " \"conv_rate\": np.clip(np.random.normal(0.5, 0.2, N_ROWS), 0, 1),\n", + " \"acc_rate\": np.clip(np.random.normal(0.7, 0.15, N_ROWS), 0, 1),\n", + " \"avg_daily_trips\": np.random.poisson(20, N_ROWS).astype(\"int32\"),\n", + " \"vehicle_type\": np.random.choice([\"sedan\", \"suv\", \"truck\", \"van\", \"compact\"], N_ROWS),\n", + " \"created\": timestamps,\n", + "})\n", + "\n", + "print(f\"Sample data: {len(df)} rows, {N_DAYS} days\")\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "!uv pip install -q 'psycopg2'" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded sample data into PostgreSQL table 'driver_stats_source'.\n" + ] + } + ], + "source": [ + "# Load sample data into PostgreSQL'\n", + "from sqlalchemy import create_engine\n", + "\n", + "engine = create_engine(f\"postgresql://{PG_USER}:{PG_PASS}@{PG_HOST}:{PG_PORT}/{PG_DB}\")\n", + "df.to_sql(\"driver_stats_source\", engine, if_exists=\"replace\", index=False)\n", + "print(\"Loaded sample data into PostgreSQL table 'driver_stats_source'.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Feature definitions written.\n" + ] + } + ], + "source": [ + "# Write feature definitions\n", + "definitions = '''\n", + "from datetime import timedelta\n", + "from feast import Entity, FeatureView, FeatureService, Field\n", + "from feast.types import Float32, Int32, String\n", + "from feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source import (\n", + " PostgreSQLSource,\n", + ")\n", + "\n", + "driver = Entity(name=\"driver\", join_keys=[\"driver_id\"])\n", + "\n", + "driver_stats_source = PostgreSQLSource(\n", + " name=\"driver_stats_source\",\n", + " query=\"SELECT * FROM driver_stats_source\",\n", + " timestamp_field=\"event_timestamp\",\n", + " created_timestamp_column=\"created\",\n", + ")\n", + "\n", + "driver_stats_fv = FeatureView(\n", + " name=\"driver_stats\",\n", + " entities=[driver],\n", + " ttl=timedelta(days=365),\n", + " schema=[\n", + " Field(name=\"conv_rate\", dtype=Float32),\n", + " Field(name=\"acc_rate\", dtype=Float32),\n", + " Field(name=\"avg_daily_trips\", dtype=Int32),\n", + " Field(name=\"vehicle_type\", dtype=String),\n", + " ],\n", + " source=driver_stats_source,\n", + ")\n", + "\n", + "driver_service = FeatureService(\n", + " name=\"driver_service\",\n", + " features=[driver_stats_fv],\n", + ")\n", + "'''\n", + "\n", + "with open(os.path.join(REPO_DIR, \"definitions.py\"), \"w\") as f:\n", + " f.write(definitions)\n", + "\n", + "print(\"Feature definitions written.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: Apply — Registers Features & Triggers Baseline\n", + "\n", + "Running `feast apply` registers the feature definitions and automatically queues baseline metric computation." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/var/folders/cn/z7vz24yj25d8fjqdrs9jbsh00000gn/T/feast_monitoring_demo_kze7m3sk/definitions.py:9: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'driver'.\n", + " driver = Entity(name=\"driver\", join_keys=[\"driver_id\"])\n", + "The `path` of the `RegistryConfig` starts with a plain `postgresql` string. We are updating this to `postgresql+psycopg` to ensure that the `psycopg3` driver is used by `sqlalchemy`. If you want to use `psycopg2` pass `postgresql+psycopg2` explicitely to `path`. To silence this warning, pass `postgresql+psycopg` explicitely to `path`.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Features registered. Baseline computation queued.\n" + ] + } + ], + "source": [ + "import sys\n", + "from feast import FeatureStore\n", + "\n", + "sys.path.insert(0, REPO_DIR)\n", + "from definitions import driver, driver_stats_source, driver_stats_fv, driver_service\n", + "\n", + "store = FeatureStore(repo_path=REPO_DIR)\n", + "store.apply([driver, driver_stats_source, driver_stats_fv, driver_service])\n", + "print(\"Features registered. Baseline computation queued.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 5: Compute Batch Metrics\n", + "\n", + "### 5a. Auto-compute (recommended for production)\n", + "\n", + "Auto-compute detects the latest event timestamp and generates metrics for all 5 granularities: `daily`, `weekly`, `biweekly`, `monthly`, and `quarterly`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computed metrics for 20 features\n", + "Granularities: ['biweekly', 'daily', 'monthly', 'quarterly', 'weekly']\n" + ] + } + ], + "source": [ + "from feast.monitoring.monitoring_service import MonitoringService\n", + "\n", + "monitoring = MonitoringService(store)\n", + "\n", + "result = monitoring.auto_compute(\n", + " project=\"monitoring_demo\",\n", + ")\n", + "\n", + "print(f\"Computed metrics for {result.get('computed_features', 'N/A')} features\")\n", + "print(f\"Granularities: {result.get('granularities', [])}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5b. Targeted compute (specific date range)\n", + "\n", + "Compute `weekly` metrics for a specific window." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'status': 'completed', 'granularity': 'weekly', 'computed_features': 4, 'computed_feature_views': 1, 'computed_feature_services': 1, 'metric_dates': ['2025-01-01'], 'duration_ms': 43}\n" + ] + } + ], + "source": [ + "from datetime import date\n", + "\n", + "result = monitoring.compute_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " start_date=date(2025, 1, 1),\n", + " end_date=date(2025, 1, 7),\n", + " granularity=\"weekly\",\n", + ")\n", + "\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5c. Set a manual baseline\n", + "\n", + "Use `set_baseline=True` to mark the computed metrics as the reference distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Baseline set.\n" + ] + } + ], + "source": [ + "result = monitoring.compute_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " start_date=date(2025, 1, 1),\n", + " end_date=date(2025, 2, 28),\n", + " granularity=\"daily\",\n", + " set_baseline=True,\n", + ")\n", + "\n", + "print(\"Baseline set.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 6: Read Metrics\n", + "\n", + "### Per-feature metrics" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Date: 2025-01-01 Mean: 0.4989 Null rate: 0.0000 Rows: 4922\n", + "Date: 2025-02-28 Mean: 0.5201 Null rate: 0.0000 Rows: 104\n" + ] + } + ], + "source": [ + "metrics = monitoring.get_feature_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_name=\"conv_rate\",\n", + " data_source_type=\"batch\",\n", + " granularity=\"daily\",\n", + ")\n", + "\n", + "for m in metrics[:3]:\n", + " print(f\"Date: {m['metric_date']} Mean: {m['mean']:.4f} Null rate: {m['null_rate']:.4f} Rows: {m['row_count']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Categorical feature metrics\n", + "\n", + "Categorical features (like `vehicle_type`) produce value-count histograms instead of numeric statistics." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Date: 2025-01-01 Type: categorical Rows: 4922 Null rate: 0.0000\n", + " Unique values: 5 Other count: 0\n", + " van: 1051\n", + " suv: 1028\n", + " sedan: 970\n", + " truck: 954\n", + " compact: 919\n", + "Date: 2025-02-28 Type: categorical Rows: 104 Null rate: 0.0000\n", + " Unique values: 5 Other count: 0\n", + " compact: 26\n", + " truck: 24\n", + " sedan: 19\n", + " van: 18\n", + " suv: 17\n" + ] + } + ], + "source": [ + "cat_metrics = monitoring.get_feature_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_name=\"vehicle_type\",\n", + " data_source_type=\"batch\",\n", + " granularity=\"daily\",\n", + ")\n", + "\n", + "for m in cat_metrics[:3]:\n", + " print(f\"Date: {m['metric_date']} Type: {m['feature_type']} \"\n", + " f\"Rows: {m['row_count']} Null rate: {m['null_rate']:.4f}\")\n", + " if m.get(\"histogram\"):\n", + " hist = m[\"histogram\"]\n", + " print(f\" Unique values: {hist['unique_count']} Other count: {hist['other_count']}\")\n", + " for entry in hist[\"values\"]:\n", + " print(f\" {entry['value']}: {entry['count']}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAGGCAYAAADmRxfNAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAANlNJREFUeJzt3QmcjeX///HPjDFjmca+G7ufnYTKUrLvhUhSiEqRkqTUD1HiS5SQoujrG/mWolWSXWTJvmdrpiKyjK0sM/f/8bl+//t0zmxmXMOZmfN6Ph7HmPvc576v+77PzFzv+1pOkOM4jgAAAACAhWCbFwMAAACAIlgAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gA8IsPPvhAgoKCZOPGjVdd96677jKP1NLtv/zyy9dYwsxNz4ueH2+lSpWSnj17Xvd9Hz582Oxb3wMu3W94eLjcKLw3ACDtESwA4Bq8/fbbPhXjQPXNN9+k2wp6ei5bWvvss8+kS5cuUqZMGcmRI4dUqFBBnn32WTl9+nSi63/xxRdyyy23SLZs2aREiRIyfPhwuXLlis86S5YskV69esn//M//mG3qth955BE5cuRIgu1p8NewFv/RsmXLFB+DlvWxxx6TAgUKSM6cOaVRo0ayadOmBOv997//lQcffFDKly9v9nEtNx1u5L6AQBLi7wIAwNV89913kh6DRf78+W/IHf4bZe/evRIcHJzqyvuUKVNSVYEvWbKk/PXXX5I1a9ZrKGXalE33HxKSef4EaiW5aNGiphKsQWH79u0yefJkcw60wpw9e3bPugsXLpT27dubSvKkSZPMuq+++qocO3ZMpk6d6lnv+eefl5MnT0rnzp1NxfrgwYNmm1999ZVs2bJFChcu7FOG4sWLy+jRo32WaZlSIi4uTtq0aSNbt26V5557zvxs6c+YlvGnn34y+3dpGXVZnTp15MSJE6k+VzdyX0CgyTy/VQFkWqGhof4uQkAICwu7rtvXO+JaqdPrqXfK/cnf+09r8+bNS3A3vVatWtKjRw+ZPXu2aWlwDRo0SKpXr24CuxuuIiIi5LXXXpOnn35aKlasaJZNmDBBGjRo4BM2tQWiYcOGJmBoGPGWK1cuE2yutfxr1qyRTz75RDp16mSW3Xfffaa1RFtT5syZ41n3P//5jxQrVsyUq2rVqul6X0CgoSsUgBT9IdZuACtWrEjw3Lvvvmue27Fjh2fZnj17zB/svHnzmgpc7dq1TdeLxFy8eFEGDhzo6ZLQoUMHOX78+FXHWPz999/mTrRWBnQfRYoUkY4dO8qBAweSPZbffvvNdO8oVKiQqUhXqVJFZsyYkarzoWMRdu7cac6H2+VDy6d3dPX/b7zxRoLXaEVGn/voo498xjjoudJKjVbs8uXLZyp2emzxffjhh6aiqHee9bzef//9Eh0dnaLyrl692txx1fNUtmxZc82SOi7vFpjLly/LiBEjzB1cfa2WTyuaixcvNs/rutoioLy7v3iPo3j99dflzTffNPvV871r165Ex1i49By2aNHCvBf0bvfIkSPFcRzP88uXLzev1a/e4m8zubK5y+K3ZGzevFlatWplroWO92jSpIn8+OOPiY4N+uGHH676vr2REuuio2VSu3fv9izT868PbeHwbrHp27evOc/6s+668847E7Rg6TJ9/3lvM354PHfuXKrLr/vVn0n9GXbpudWfjc8//9z8nnBFRkamumXNX/sCAg0tFgCuSrsNaEXr448/Nncr4/dB1sq5ezdPK9z169c3d/leeOEFU+nS12nXi08//dRT2XH1799f8uTJY+4UauVQK6FPPvmk2W5SYmNjpW3btqYPuFawtTJ+9uxZU+HVgKOV2MT88ccfcvvtt5uKoe5DKxPaLaR3795y5swZGTBgQIrOh5ZRy63n5KWXXjLLtKKifdD12PUO8TPPPOPzGl120003yT333OOzXCszWqHXLiRaiX3rrbfk1KlTMmvWLM86o0aNkqFDh5p19c6zVmC1C4tW8rQynDt37iTLqt1cmjdvbo5VK9Ja8dNzreW9Gl1fy6X7vPXWW8050sH22rWmWbNm0qdPH/n999/Nedc7u4mZOXOmCUpakdVgoZVSbbVI6rrqHXG9RmPHjpVvv/3W0/dfA0ZqpKRs3vR9e8cdd5hQMXjwYNNNSwOYVtg1QN52223W79sb7ejRo+ardvVx6ftFadj3piFOuzK5zydFQ4M+vLfp2rdvn/l5v3Tpknl/PfroozJs2LAUdXnT/eqYj/iVeH3fTZs2zWy7WrVqkhZu5L6AgOMAQAp07drVKViwoHPlyhXPsiNHjjjBwcHOyJEjPcuaNGniVKtWzfn77789y+Li4px69eo55cuX9yybOXOm3oZ2mjZtap53PfPMM06WLFmc06dPe5Y1bNjQPFwzZswwr50wYUKCcnpvS9cZPny45/vevXs7RYoUcf7880+f19x///1Orly5nAsXLqT4fFSpUsWnTK53333X7Hf37t2eZZcuXXLy58/v9OjRw7NMy6Xr3X333T6v79u3r1m+detW8/3hw4fN+Rg1apTPetu3b3dCQkISLI+vffv2TrZs2ZxffvnFs2zXrl1mm/H/BJQsWdKnjDVq1HDatGmT7Pb79euXYDvq0KFDZnlERIRz7NixRJ/T94BL96vL+vfv73Mtdf+hoaHO8ePHzbJly5aZ9fTr1baZVNkSe2/oedL9HDhwwLPs999/d2666SbnzjvvvKb3rb/p+13LtG/fPs+ycePGmfJHRUUlWL9OnTrO7bffnuw2X3nlFfP6JUuW+Czv1auX8/LLLzuffvqpM2vWLPO+1vXuu+++FJU1Z86cZhvxff3112Y73377bap+DtPLvoBAQ/segBTRGWd0cKd3FxTtUqB3n/U5pQM9ly5dau6sawvCn3/+aR466FG7t/z888+mK5I3vZPt3UVF7xrrnetffvklybJoy4feMdW7xvHFn0LVpXVJfV27du3M/92y6UPLFhMTk+isMKmlx67dhrSFwrVo0SKzn8T6n/fr18/ne/eYdNCtO9uPnmPdrneZdeCsdlFatmxZkmXR86j71tYiHdDrqlSpkjnmq9GWEL2Tr9ftWt17772mtSSl9K6/y21Z0jvg33//vVwvep50vIGeJ211cmn3ugceeMB0JdPWGtv37Y2k4wTef/99MzOU92BkHbSe1Hgafd+6zydm5cqVpmucvhcbN27s85zuS1tvtHvRQw89ZLoUaYuFtlbG706WGN1vUmXyLndauJH7AgINXaEApIh2UdHBmdrVQ/ueK/3/zTffbMY5qP3795tKu3bb0UdiNJxoNymXd4VXafcSpd2BkqLjKHQ6zdTM6qPdh3SKSe3qoI+kymZLK+MaXrRi98orr5hlGjL0mONXxpR3pU9pNy7toqHda5RW6vWcxl/PlVw3Ez1mrSQl9lo9f254SYp2P9KuW3p9taubvge00qgDf1OqdOnSKV5Xj9u7Yq/c95Z7Pq4HPU8XLlww5yQ+DWEa7HQ8i3b5s3nf6rXQAHstdGyN/vylxKpVq0z3Pg2P2o0u/naU9zgCl3ZZ8549ypuOBdJujPo+eO+991JUDg0106dPN6FQu7dpQNSbD940dGbJksXsN6kyeZc7pW7kvgD8g2ABIEX0Dp/e0Z0/f76ZmlHHK+gAVp1JxuX2nddZZ5K6I16uXDmf7/UPfWK8B+ymBbds2mqgM+UkJjUV5uR0797dzDijA7a1r7YOXNfBsSkZBBq/xUXLrct0LEhi5+p6fqicjuHQEKd3n/WOvlYodWD6O++84zPLUHLSupKWVIuUthbcSNfyvtUg/vDDD1/T/vQ9m5LPTdEpVO+++24TALRFMX741lYYpZ9FoQOTvekyHWcQn4YqHaejwUbDqI4VSgl3+24FX38e9PMivB06dMiMMdJyJfb5GO6ylE5b67qR+wLwD4IFgBTTLk///ve/zaBpnRVGK1FuNyjl3m3Wu+hNmza9buXQu/rr1q0zsxal9LMQ9G6lVoi0ApoWZUuqgqv0zr7uT1sqdNCv3g3XO/2J0RYJ77v62uqjYUIrQO6x6nnWddy79ymlZdCKfWJdmfQzK1JCB1trZVgfOmhXw4YO6naDRXLnIbX0uHVWKO/j1IG0yj0fbstA/A9+S6wLUkrLpudJPwAusXOid+o1EMavhF8LDdvujFqplZLKroZAfe8VLFjQBIDEQqe2MCodhO8dInSg+6+//mq6eHnTbowaKvQOv/7cu8EkJfRaKrcrXI0aNRIcv/tZGFoubWnR94B3ANefc702qX3v38h9AfgHwQJAimmFXCuaeudVg4VWTLwrxVqh0Vl0dDYdHSsQvxKiXU5S098+uX77X3/9tZlLP/7sS1oJT6xCqXeY9XXaRUlnjoo/J31qy6az3yT1qcZ6l7hr165mX3qetNUiqdYQnRJVK24une1J6bSnSvusDxkyxPRt1ylnvY9Nj1XvBus0sInRY9bK7IIFCyQqKsrTfUfLpGMvrkYrld7b1oqqtjh5T3Or50HpuUhudqqU0muqM2O5x6ffa3h0u9/ph+vpcWl/f21Bc2krWnwpLZtuT6+Btsxolys3xGirnF5DnWJXZ4uypT8PqamYp3YGKD0GrSjrtU3qvazdufRzKrQ7oM6c5ba86AfB6XvL/VwHdf78eWndurUZF6VjeZLqjqfjT7RF03vcgl4793Mu3NZLDYVJhXrdr7aw6Jgitww6lkhb/rRrYWo/Y+VG7gvAPwgWAFJMK3ha0Z07d66pdOhnFCRWUdaKmFamdfCmtmJoBW3t2rXmjqh21UiLrkY6Hat+jsD69evNwFktj/bl1i5H8ad0dY0ZM8ZUkLQVQctWuXJlUzHXQdv62vh9spOjnymhlTGtPGllW0OV9xgKLaNWkHV///rXv5LcjnbP0K4reqdZz5GGBx0wrHdc3RYL3YeGC630amVaW170ddotTe8wa9ezpGgg0Wlb9RzpudGpWzW8aAVz27ZtyR6jnh8NinqsGij1LrdWyLwHWOtz6qmnnjIVSK2o6hTA10IHz2pZtduPXiPt/qUB8sUXX/RUlLU7jn4StB6DVoT1/OgnQSc2PiY1ZdNzrHe49b2r50nDoQZkvVOvU9+md/r+0RYCnSpXB5vrw6VTv+r0wK5x48aZ95wGET0fGrQ1wGkrlI4pcXXr1s38fOnnvmgY9f7sCg2ZbrDTnx8N0vrQnwUdS6LvTe0qqe9Pndr1arSCr+MwtGVMP2fD/TRsbWHU97A3DZX6cG8I6M++G2K0RU0f6WVfQMDx97RUADKWxYsXmykZg4KCnOjo6ETX0Sk7u3fv7hQuXNjJmjWrU6xYMadt27bOvHnzEkzbuWHDBp/XJjadaPzpZpVODfvSSy85pUuXNvvQfXXq1MlnutD4U4qqP/74w0xDGhkZ6XmdTpE7bdq0VJ2Ho0ePmqlQdTpS3U9i01Dq9JQ6He+vv/6a4Dl3ulmd+lXLrdvJkyeP8+STTzp//fVXgvV1Gs8GDRqYqTL1UbFiRXMce/fuvWpZV6xY4dSqVctMp1qmTBnnnXfe8ew/uelmX331VefWW291cufO7WTPnt3sU6e31elzXTr9sE4RW6BAAfOecLfpTv+q05vGl9R0s3pcev2aN2/u5MiRwylUqJApZ2xsrM/rderZe++916yj56xPnz7Ojh07EmwzqbIl9d7YtGmT06JFCyc8PNxsu1GjRs6aNWt81knN+/ZG0n0n9UjsvTl//nzn5ptvdsLCwpzixYs7//u//+tzXd33Q1Lb1OdcBw8edDp37uyUKlXKTG2s507fb/o+856S92pOnjxppsjNly+f2YaWO/55Vu57N7FH/GuaHvYFBJIg/cff4QYAMqOaNWuaO/3aNz0+Haegd0f1LmhiHzYGAEBGw+dYAMB1oN2GtmzZYrpEAQAQCBhjAQBetAUhualLQ0NDTStEUrS/+k8//STjx483A3W9Z80CACAzI1gAgJc6deok++nJDRs29Pn08fh0cLN+sJx+2NpHH33k+TRfAAAyO8ZYAIAXnclGZ7VJbhpLd7YhAADwD4IFAAAAAGsM3gYAAABgjTEWFuLi4uT33383H1aV2Cf9AgAAABmZdm46e/asFC1aVIKDk2+TIFhY0FARGRnp72IAAAAA11V0dLQUL1482XUIFha0pcI90REREf4uDgAAAJCmzpw5Y26ku/Xe5BAsLLjdnzRUECwAAACQWaWk2z+DtwEAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAIA0Exvn+LsI8JMQf+04M5nw5VY5fsHfpQAAAPCvyPzh8kKHmv4uBvyEYJEGfjtxXqJiYv1dDAAAAMBv6AoFAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWDx/wUFBcmCBQv8XQwAAAAgQyJYAAAAALjxwSIuLk7Gjh0r5cqVk7CwMClRooSMGjXKPLd9+3Zp3LixZM+eXfLlyyePPfaYnDt3zvPanj17Svv27eW1116TQoUKSe7cuWXkyJFy5coVee655yRv3rxSvHhxmTlzpuc1hw8fNq0Jc+fOlXr16km2bNmkatWqsmLFCs86sbGx0rt3byldurTZd4UKFWTixIkJyj5jxgypUqWKKXeRIkXkySefNMtLlSplvnbo0MHsy/0eAAAAwHUKFkOGDJExY8bI0KFDZdeuXTJnzhwTEs6fPy8tWrSQPHnyyIYNG+STTz6R77//3lN5dy1dulR+//13WblypUyYMEGGDx8ubdu2Na9bt26dPP7449KnTx/59ddffV6nwePZZ5+VzZs3S926daVdu3Zy4sQJT9jRQKL71DINGzZMXnzxRfn44489r586dar069fPhB0NQF988YUJR0rLqzTQHDlyxPN9fBcvXpQzZ874PAAAAACIBDmO46R05bNnz0qBAgVk8uTJ8sgjj/g8N336dHn++eclOjpacubMaZZ98803JgBokNDwoS0Wy5cvl4MHD0pw8P9lmooVK0rBggVN0HBbH3LlyiXvvfee3H///abFQlsiNMzo9pW2cOiy/v37y+DBgxMtqwaao0ePyrx588z3xYoVk4cfflheffXVxE9EUJDMnz/ftKgk5eWXX5YRI0YkWP7IxIUSFRObwrMIAACQOZUrHCFTHr3D38VAGtIb6Vo3j4mJkYiIiLRrsdi9e7e5a9+kSZNEn6tRo4YnVKj69eub1oS9e/d6lmlXJDdUKA0c1apV83yfJUsW043q2LFjPtvXVgpXSEiI1K5d2+zTNWXKFKlVq5YJPuHh4TJt2jSJiooyz+m2NNwkVu7UttboSXUfGqIAAAAAiISkZmUdv2Ara9asCVoKElumgSSldPzFoEGDZPz48SaA3HTTTTJu3DjTtSqtyq10bIY+AAAAAFi0WJQvX95U0pcsWZLguUqVKsnWrVvNWAvXDz/8YFondDC1rR9//NHzf+0K9dNPP5l9uvvRgd19+/aVmjVrmrETBw4c8KyvQUMHZCdWbpeGG+2GBQAAAOA6BwudkUnHOei4hlmzZpnKu1b433//fenWrZt5vkePHrJjxw5ZtmyZGQPx0EMPme5OtrSrk46B2LNnjxmEferUKenVq5cn8GzcuFEWLVok+/btMwPL4w/A1vER2qLx1ltvyc8//yybNm2SSZMmeZ53g4eOy9BtAwAAALiOs0JppV1nZ9KZl7TFoEuXLmYMQ44cOUzF/uTJk1KnTh3p1KmTGdOgA73Tgg7e1oeO41i9erWZ1Sl//vzmOZ1FqmPHjqYst912m5ktSlsvvGngefPNN+Xtt9824zx0JioNGC4NHYsXL5bIyEjT6gEAAADgOs0K5Q/urFA6zezNN98s6XGUPLNCAQAAMCtUZnTdZoUCAAAAgMQQLAAAAADc2Olm/UEHVafz3loAAABAwKPFAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACshdhvAsXy5ZTQ7P4uBQAAgH9F5g/3dxHgRwSLNDCwXQ2JiIjwdzEAAAD8LjbOkSzBQf4uBvyArlAAAABIM4SKwEWwAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAA6V5snOPvIuAqQq62Aq5uwpdb5fgFf5cCAAAgc4rMHy4vdKjp72LgKggWaeC3E+clKibW38UAAAAA/IauUAAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIC1gA0Whw8flqCgINmyZYu/iwIAAABkeOkuWNx1110yYMAAfxcDAAAAQEYOFlfjOI5cuXLF38UAAAAAkF6DRc+ePWXFihUyceJE001JHx988IH5unDhQqlVq5aEhYXJ6tWrzbrt27f3eb22dGiLhysuLk7Gjh0r5cqVM68rUaKEjBo1KtF9x8bGSq9evaRixYoSFRV13Y8VAAAAyExCJB3RQLFv3z6pWrWqjBw50izbuXOn+frCCy/I66+/LmXKlJE8efKkaHtDhgyR6dOnyxtvvCENGjSQI0eOyJ49exKsd/HiRenatasZd7Fq1SopUKBAGh8ZAAAAkLmlq2CRK1cuCQ0NlRw5ckjhwoXNMjcIaNBo1qxZird19uxZE1QmT54sPXr0MMvKli1rAoa3c+fOSZs2bUy4WLZsmSlDUnQdfbjOnDmT6mMEAAAAMqN01RUqObVr107V+rt37zYhoEmTJsmupy0V58+fl++++y7ZUKFGjx5t1nEfkZGRqSoTAAAAkFllmGCRM2dOn++Dg4PNQG5vly9f9vw/e/bsKdpu69atZdu2bbJ27doUda2KiYnxPKKjo1NcfgAAACAzS3fBQrtC6UDqq9FxEDpmwpv3Z1KUL1/ehIslS5Yku50nnnhCxowZI3fffbcZOJ4cHQAeERHh8wAAAACQzsZYqFKlSsm6devMQOrw8HAzs1NiGjduLOPGjZNZs2ZJ3bp15cMPP5QdO3ZIzZo1zfPZsmWT559/XgYPHmzCSv369eX48eNmMHjv3r19ttW/f38TZtq2bWtmn4o/DgMAAABABmuxGDRokGTJkkUqV65sWiWSmvq1RYsWMnToUBMc6tSpYwZrd+/e3Wcdff7ZZ5+VYcOGSaVKlaRLly5y7NixRLenU9WOGDHCdI1as2bNdTk2AAAAILMKcuIPVECK6axQOoj7kYkLJSrm6t23AAAAkHrlCkfIlEfv8HcxArq+GxMTc9VhAOmuxQIAAABAxkOwAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYC7HfBIrlyymh2f1dCgAAgMwpMn+4v4uAFCBYpIGB7WpIRESEv4sBAACQacXGOZIlOMjfxUAy6AoFAACAdI9Qkf4RLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAg04iNc/xdhIAV4u8CZAYTvtwqxy/4uxQAAACBLTJ/uLzQoaa/ixGwCBZp4LcT5yUqJtbfxQAAAAD8hq5QAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrmSJYHD58WIKCgmTLli3+LgoAAAAQkDJFsAAAAADgXwQLAAAAABk3WMybN0+qVasm2bNnl3z58knTpk3l/Pnz5rn33ntPKlWqJNmyZZOKFSvK22+/7fPa9evXS82aNc3ztWvXls2bN/s8HxsbK71795bSpUub7VeoUEEmTpzos07Pnj2lffv28vrrr0uRIkVMGfr16yeXL1++AUcPAAAAZC4h/tjpkSNHpGvXrjJ27Fjp0KGDnD17VlatWiWO48js2bNl2LBhMnnyZBMeNDQ8+uijkjNnTunRo4ecO3dO2rZtK82aNZMPP/xQDh06JE8//bTP9uPi4qR48eLyySefmMCwZs0aeeyxx0yAuO+++zzrLVu2zCzTr/v375cuXbrIzTffbPYHAAAAIAMEiytXrkjHjh2lZMmSZpm2Xqjhw4fL+PHjzXNKWx127dol7777rgkWc+bMMcHh/fffNy0WVapUkV9//VWeeOIJz/azZs0qI0aM8Hyv21i7dq18/PHHPsEiT548JsBkyZLFtIy0adNGlixZkmSwuHjxonm4zpw5cx3ODgAAAJDx+CVY1KhRQ5o0aWLCRIsWLaR58+bSqVMnCQ0NlQMHDphuTN6Vew0huXLlMv/fvXu3VK9e3YQKV926dRPsY8qUKTJjxgyJioqSv/76Sy5dumRaI7xpKNFQ4dLWi+3btydZ7tGjR/sEFgAAAAB+HGOhlfnFixfLwoULpXLlyjJp0iQzDmLHjh3m+enTp5upY92HLv/xxx9TvP25c+fKoEGDTED57rvvzDYefvhhEy68acuGN52yVltDkjJkyBCJiYnxPKKjo1N97AAAAEBm5JcWC7cSX79+ffPQMRXaJeqHH36QokWLysGDB6Vbt26Jvk4Hdf/nP/+Rv//+29NqET906Hbq1asnffv29SzTlhBbYWFh5gEAAAAgHQSLdevWmbEM2gWqYMGC5vvjx4+b0KBdjZ566inT9ally5ZmTMPGjRvl1KlTMnDgQHnggQfkpZdeMl2ltAVBPxxPZ3byVr58eZk1a5YsWrTIjK/QILJhwwbzfwAAAACZJFhERETIypUr5c033zQDoLW1Qgdst2rVyjyfI0cOGTdunDz33HNmNigdizFgwADzXHh4uHz55Zfy+OOPm1mjtCvVv/71L7n33ns92+/Tp4+ZTUpnedKWEZ2BSlsvtOsVAAAAgLQX5Ogcr7gmGoq0ZeWRiQslKibW38UBAAAIaOUKR8iUR+/wdzEyZX1Xxxdr40By+ORtAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAayH2m0CxfDklNLu/SwEAABDYIvOH+7sIAY1gkQYGtqshERER/i4GAABAwIuNcyRLcJC/ixGQ6AoFAACATINQ4T8ECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYI1gAQAAAMAawQIAAACANYIFAAAAAGsECwAAAADWCBYAAADAdRQb50ggCPF3ATKDCV9uleMX/F0KAAAApDeR+cPlhQ41JRAQLNLAbyfOS1RMrL+LAQAAAPgNXaEAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALBGsAAAAABgjWABAAAAwBrBAgAAAIA1ggUAAAAAawQLAAAAANYIFgAAAACsESwAAAAAWCNYAAAAALCW4YPFvHnzpFq1apI9e3bJly+fNG3aVM6fPy933XWXDBgwwGfd9u3bS8+ePc3/X3zxRbntttsSbK9GjRoycuTIG1Z+AAAAIDPI0MHiyJEj0rVrV+nVq5fs3r1bli9fLh07dhTHca762m7dusn69evlwIEDnmU7d+6Ubdu2yQMPPJDoay5evChnzpzxeQAAAADIBMHiypUrJkyUKlXKtFz07dtXwsPDr/raKlWqmNaJOXPmeJbNnj3btGKUK1cu0deMHj1acuXK5XlERkam6fEAAAAAGVWGDhYaDJo0aWICRefOnWX69Oly6tSpFL9eWy3cYKGtHB999JFZlpQhQ4ZITEyM5xEdHZ0mxwEAAABkdBk6WGTJkkUWL14sCxculMqVK8ukSZOkQoUKcujQIQkODk7QJery5cs+32s3qr1798qmTZtkzZo1Jih06dIlyf2FhYVJRESEzwMAAABABg8WKigoSOrXry8jRoyQzZs3S2hoqMyfP18KFChgukq5YmNjZceOHT6vLV68uDRs2NB0gdJHs2bNpGDBgn44CgAAACBjC5EMbN26dbJkyRJp3ry5CQT6/fHjx6VSpUqSM2dOGThwoHz99ddStmxZmTBhgpw+fTrBNrTr0/Dhw+XSpUvyxhtv+OU4AAAAgIwuQwcL7Yq0cuVKefPNN80MTSVLlpTx48dLq1atTLenrVu3Svfu3SUkJESeeeYZadSoUYJtdOrUSZ588knTrUqnowUAAACQekFOSuZmRaI0zOjsUI9MXChRMbH+Lg4AAADSmXKFI2TKo3dIRq/v6sRFVxtfnOHHWAAAAADwP4IFAAAAAGsECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYI1gAQAAAMAawQIAAACANYIFAAAAAGsECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYC3EfhMoli+nhGb3dykAAACQ3kTmD5dAQbBIAwPb1ZCIiAh/FwMAAADpUGycI1mCgySzoysUAAAAcB1lCYBQoQgWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAA6VRsnCMZRYi/C5AZTPhyqxy/4O9SAAAAIDOJzB8uL3SoKRkFwSIN/HbivETFxPq7GAAAAIDf0BUKAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAQGAGi2nTpknRokUlLi7OZ/k999wjvXr1kgMHDpj/FypUSMLDw6VOnTry/fff+6xbqlQpee2118z6N910k5QoUcJsFwAAAECABIvOnTvLiRMnZNmyZZ5lJ0+elG+//Va6desm586dk9atW8uSJUtk8+bN0rJlS2nXrp1ERUX5bGf8+PFSu3Zts07fvn3liSeekL179/rhiAAAAICMLUMGizx58kirVq1kzpw5nmXz5s2T/PnzS6NGjaRGjRrSp08fqVq1qpQvX15eeeUVKVu2rHzxxRc+29HwoYGiXLly8vzzz5vXe4eV+C5evChnzpzxeQAAAADIoMFCacvEp59+air7avbs2XL//fdLcHCwabEYNGiQVKpUSXLnzm26Q+3evTtBi0X16tU9/w8KCpLChQvLsWPHktzn6NGjJVeuXJ5HZGTkdTxCAAAAIOPIsMFCuzY5jiNff/21REdHy6pVq0zYUBoq5s+fb8ZQ6PItW7ZItWrV5NKlSz7byJo1q8/3Gi7ij9vwNmTIEImJifE8dL8AAAAAREIkg8qWLZt07NjRtFTs379fKlSoILfccot57ocffpCePXtKhw4dzPfagnH48GHrfYaFhZkHAAAAgEwSLJS2ULRt21Z27twpDz74oGe5jqv47LPPTKuGtkIMHTo02ZYIAAAAAAHaFUo1btxY8ubNa2ZyeuCBBzzLJ0yYYAZ416tXz4SLFi1aeFozAAAAAKS9DN1ioQO1f//99wTL9TMqli5d6rOsX79+Pt8n1jVKx2IAAAAACLAWCwAAAADpA8ECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGAtxH4TKJYvp4Rm93cpAAAAkJlE5g+XjIRgkQYGtqshERER/i4GAAAAMpnYOEeyBAdJRkBXKAAAACCdypJBQoUiWAAAAACwRrAAAAAAYI1gAQAAAMAawQIAAACANYIFAAAAAGsECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYI1gAQAAAMBaiP0mApfjOObrmTNn/F0UAAAAIM259Vy33pscgoWFEydOmK+RkZH+LgoAAABw3Zw9e1Zy5cqV7DoECwt58+Y1X6Oioq56opH50rsGyujoaImIiPB3cXADce0DF9c+MHHdAxfXXjwtFRoqihYtKldDsLAQHPx/Q1Q0VATyGy6Q6XXn2gcmrn3g4toHJq574OLaS4pvoDN4GwAAAIA1ggUAAAAAawQLC2FhYTJ8+HDzFYGFax+4uPaBi2sfmLjugYtrn3pBTkrmjgIAAACAZNBiAQAAAMAawQIAAACANYIFAAAAAGsEi2s0ZcoUKVWqlGTLlk1uu+02Wb9+vb+LBEujR4+WOnXqyE033SQFCxaU9u3by969e33W+fvvv6Vfv36SL18+CQ8Pl3vvvVf++OMPn3X0AxPbtGkjOXLkMNt57rnn5MqVKzf4aHCtxowZI0FBQTJgwADPMq575vXbb7/Jgw8+aK5t9uzZpVq1arJx40bP8zoMcdiwYVKkSBHzfNOmTeXnn3/22cbJkyelW7duZp773LlzS+/eveXcuXN+OBqkVGxsrAwdOlRKly5trmvZsmXllVdeMdfbxbXPHFauXCnt2rUzH+6mv9sXLFjg83xaXedt27bJHXfcYeqF+qF6Y8eOlYCkg7eROnPnznVCQ0OdGTNmODt37nQeffRRJ3fu3M4ff/zh76LBQosWLZyZM2c6O3bscLZs2eK0bt3aKVGihHPu3DnPOo8//rgTGRnpLFmyxNm4caNz++23O/Xq1fM8f+XKFadq1apO06ZNnc2bNzvffPONkz9/fmfIkCF+Oiqkxvr1651SpUo51atXd55++mnPcq575nTy5EmnZMmSTs+ePZ1169Y5Bw8edBYtWuTs37/fs86YMWOcXLlyOQsWLHC2bt3q3H333U7p0qWdv/76y7NOy5YtnRo1ajg//vijs2rVKqdcuXJO165d/XRUSIlRo0Y5+fLlc7766ivn0KFDzieffOKEh4c7EydO9KzDtc8c9PfxSy+95Hz22WeaGp358+f7PJ8W1zkmJsYpVKiQ061bN1OH+Oijj5zs2bM77777rhNoCBbX4NZbb3X69evn+T42NtYpWrSoM3r0aL+WC2nr2LFj5pfQihUrzPenT592smbNav4AuXbv3m3WWbt2recXWHBwsHP06FHPOlOnTnUiIiKcixcv+uEokFJnz551ypcv7yxevNhp2LChJ1hw3TOv559/3mnQoEGSz8fFxTmFCxd2xo0b51mm74ewsDBTcVC7du0y74UNGzZ41lm4cKETFBTk/Pbbb9f5CHCt2rRp4/Tq1ctnWceOHU3FUHHtM6f4wSKtrvPbb7/t5MmTx+f3vf5+qVChghNo6AqVSpcuXZKffvrJNJW5goODzfdr1671a9mQtmJiYszXvHnzmq963S9fvuxz7StWrCglSpTwXHv9ql0pChUq5FmnRYsWcubMGdm5c+cNPwaknHZ10q5M3tdXcd0zry+++EJq164tnTt3Nt3XatasKdOnT/c8f+jQITl69KjPtc+VK5fp/up97bVrhG7Hpevr34V169bd4CNCStWrV0+WLFki+/btM99v3bpVVq9eLa1atTLfc+0DQ1pdZ13nzjvvlNDQUJ+/Adqd+tSpUxJIQvxdgIzmzz//NH0zvSsQSr/fs2eP38qFtBUXF2f62NevX1+qVq1qlukvH/2lob9g4l97fc5dJ7H3hvsc0qe5c+fKpk2bZMOGDQme47pnXgcPHpSpU6fKwIED5cUXXzTX/6mnnjLXu0ePHp5rl9i19b72Gkq8hYSEmBsSXPv064UXXjDBX28SZMmSxfxdHzVqlOlHr7j2gSGtrrN+1fE6Sf0NyJMnjwQKggWQxN3rHTt2mDtYyNyio6Pl6aeflsWLF5tBdwisGwh6F/K1114z32uLhf7cv/POOyZYIPP6+OOPZfbs2TJnzhypUqWKbNmyxdxM0gG+XHvg2tEVKpXy589v7m7EnxFGvy9cuLDfyoW08+STT8pXX30ly5Ytk+LFi3uW6/XVrnCnT59O8trr18TeG+5zSH+0q9OxY8fklltuMXeh9LFixQp56623zP/1rhPXPXPSWWAqV67ss6xSpUpmhi/va5fc73v9qu8fbzobmM4iw7VPv3TWNm21uP/++003xoceekieeeYZMzug4toHhrS6zvwN+AfBIpW0ibxWrVqmb6b3XS/9vm7dun4tG+zouC4NFfPnz5elS5cmaNbU6541a1afa6/9J7US4l57/bp9+3afX0J6J1ynqItfgUH60KRJE3PN9I6l+9C72Nolwv0/1z1z0q6O8aeU1j73JUuWNP/X3wFaKfC+9tp9RvtVe197DZ0aUF36+0P/Lmg/baRPFy5cMH3kvelNQ71uimsfGNLqOus6Oq3t5cuXff4GVKhQIaC6QRn+Hj2eUaeb1RkDPvjgAzNbwGOPPWamm/WeEQYZzxNPPGGmnFu+fLlz5MgRz+PChQs+047qFLRLly41047WrVvXPOJPO9q8eXMzZe23337rFChQgGlHMxjvWaEU1z3zTi8cEhJiph79+eefndmzZzs5cuRwPvzwQ5+pKPX3++eff+5s27bNueeeexKdirJmzZpmytrVq1eb2cWYcjR969Gjh1OsWDHPdLM6FalOET148GDPOlz7zDPjn04Drg+t9k6YMMH8/5dffkmz66wzSel0sw899JCZblbrifq7hOlmkWKTJk0yFQ39PAudflbnNkbGpr9wEnvoZ1u49BdN3759zbRy+kujQ4cOJnx4O3z4sNOqVSszh7X+oXr22Wedy5cv++GIkFbBguueeX355ZcmFOrNoooVKzrTpk3zeV6noxw6dKipNOg6TZo0cfbu3euzzokTJ0wlQz8HQacYfvjhh01lBunXmTNnzM+4/h3Pli2bU6ZMGfNZB97ThXLtM4dly5Yl+rddw2VaXmf9DIwGDRqYbWho1cASiIL0H3+3mgAAAADI2BhjAQAAAMAawQIAAACANYIFAAAAAGsECwAAAADWCBYAAAAArBEsAAAAAFgjWAAAAACwRrAAAAAAYI1gAQAAAMAawQIAcEMcPXpU+vfvL2XKlJGwsDCJjIyUdu3ayZIlS25oOYKCgmTBggU3dJ8AEAhC/F0AAEDmd/jwYalfv77kzp1bxo0bJ9WqVZPLly/LokWLpF+/frJnzx5/FxEAYCnIcRzHdiMAACSndevWsm3bNtm7d6/kzJnT57nTp0+bwBEVFWVaNLQFIzg4WFq2bCmTJk2SQoUKmfV69uxp1vVubRgwYIBs2bJFli9fbr6/6667pHr16pItWzZ57733JDQ0VB5//HF5+eWXzfOlSpWSX375xfP6kiVLmtADALBHVygAwHV18uRJ+fbbb03LRPxQoTRUxMXFyT333GPWXbFihSxevFgOHjwoXbp0SfX+/v3vf5v9rFu3TsaOHSsjR44021MbNmwwX2fOnClHjhzxfA8AsEdXKADAdbV//37RxvGKFSsmuY62Umzfvl0OHTpkxl6oWbNmSZUqVUzlv06dOinen7ZYDB8+3Py/fPnyMnnyZLP9Zs2aSYECBTxhpnDhwtbHBgD4By0WAIDrKiU9bnfv3m0ChRsqVOXKlU0A0OdSQ4OFtyJFisixY8dStQ0AQOoRLAAA15W2GuhMTLYDtHXcRfyQogPA48uaNavP97pv7WoFALi+CBYAgOsqb9680qJFC5kyZYqcP38+wfM6ILtSpUoSHR1tHq5du3aZ57TlQmk3Jh0X4U0HbqeWBo/Y2NhrOhYAQNIIFgCA605DhVbmb731Vvn000/l559/Nl2c3nrrLalbt640bdrUTEHbrVs32bRpk6xfv166d+8uDRs2lNq1a5ttNG7cWDZu3GjGXujrdRzFjh07Ul0WnRlKx1zo52qcOnXqOhwtAAQmggUA4LrTD8XTwNCoUSN59tlnpWrVqmYwtVbwp06darorff7555InTx658847TdDQ1/z3v//1bENbPYYOHSqDBw82g7nPnj1rwkdqjR8/3swSpeM5atasmcZHCgCBi8+xAAAAAGCNFgsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAsEawAAAAAGCNYAEAAADAGsECAAAAgDWCBQAAAABrBAsAAAAA1ggWAAAAAKwRLAAAAABYI1gAAAAAEFv/D/X0w0X7J7aHAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " import matplotlib.pyplot as plt\n", + "\n", + " latest_cat = cat_metrics[0] if cat_metrics else None\n", + " if latest_cat and latest_cat.get(\"histogram\"):\n", + " hist = latest_cat[\"histogram\"]\n", + " labels = [e[\"value\"] for e in hist[\"values\"]]\n", + " counts = [e[\"count\"] for e in hist[\"values\"]]\n", + " if hist[\"other_count\"] > 0:\n", + " labels.append(\"(other)\")\n", + " counts.append(hist[\"other_count\"])\n", + "\n", + " fig, ax = plt.subplots(figsize=(8, 4))\n", + " ax.barh(labels, counts, color=\"steelblue\", edgecolor=\"white\")\n", + " ax.set_title(f\"vehicle_type distribution — {latest_cat['metric_date']}\")\n", + " ax.set_xlabel(\"Count\")\n", + " plt.tight_layout()\n", + " plt.show() # pragma: allowlist secret\n", + " else:\n", + " print(\"No categorical histogram data available.\")\n", + "except ImportError:\n", + " print(\"Install matplotlib to visualize: pip install matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Feature view aggregates" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Date: 2024-12-01 Total rows: 5000 Features w/ nulls: 0 Max null rate: 0.0\n", + "Date: 2025-01-01 Total rows: 4922 Features w/ nulls: 0 Max null rate: 0.0\n", + "Date: 2025-01-01 Total rows: 576 Features w/ nulls: 0 Max null rate: 0.0\n" + ] + } + ], + "source": [ + "view_metrics = monitoring.get_feature_view_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " data_source_type=\"batch\",\n", + ")\n", + "\n", + "for m in view_metrics[:3]:\n", + " print(f\"Date: {m['metric_date']} Total rows: {m['total_row_count']} \"\n", + " f\"Features w/ nulls: {m['features_with_nulls']} Max null rate: {m.get('max_null_rate', 'N/A')}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Feature service aggregates" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Date: 2025-01-01 Total features: 4 Avg null rate: 0.0\n", + "Date: 2025-01-01 Total features: 4 Avg null rate: 0.0\n", + "Date: 2025-02-28 Total features: 4 Avg null rate: 0.0\n" + ] + } + ], + "source": [ + "svc_metrics = monitoring.get_feature_service_metrics(\n", + " project=\"monitoring_demo\",\n", + " feature_service_name=\"driver_service\",\n", + " data_source_type=\"batch\",\n", + ")\n", + "\n", + "for m in svc_metrics[:3]:\n", + " print(f\"Date: {m['metric_date']} Total features: {m['total_features']} \"\n", + " f\"Avg null rate: {m.get('avg_null_rate', 'N/A')}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Baseline metrics" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Baseline mean: 0.4989\n", + "Baseline stddev: 0.1975\n", + "Baseline null_rate: 0.0000\n" + ] + } + ], + "source": [ + "baseline = monitoring.get_baseline(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_name=\"conv_rate\",\n", + " data_source_type=\"batch\",\n", + ")\n", + "\n", + "if baseline:\n", + " print(f\"Baseline mean: {baseline[0]['mean']:.4f}\")\n", + " print(f\"Baseline stddev: {baseline[0]['stddev']:.4f}\")\n", + " print(f\"Baseline null_rate: {baseline[0]['null_rate']:.4f}\")\n", + "else:\n", + " print(\"No baseline found.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 7: Visualize a Feature Distribution\n", + "\n", + "Use the histogram stored in the metrics to plot a distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/opt/homebrew/Cellar/python@3.12/3.12.11/Frameworks/Python.framework/Versions/3.12/lib/python3.12/pty.py:95: DeprecationWarning: This process (pid=12140) is multi-threaded, use of forkpty() may lead to deadlocks in the child.\n", + " pid, fd = os.forkpty()\n" + ] + } + ], + "source": [ + "!uv pip install -q 'matplotlib'" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAARL1JREFUeJzt3Qd4FOXa//E7Cb2F3kNRkd4EBDwoUgQRUYr1KKIiKgIqqJQjCIIIBxVQATk2sCHKK6hU6eARUGlKURSlRCEUEQIoLZn3up/3P/vfDQmEMJOZ3f1+rmuNmV12752dze5vnhZjWZYlAAAAAADAcbHO3yUAAAAAAFCEbgAAAAAAXELoBgAAAADAJYRuAAAAAABcQugGAAAAAMAlhG4AAAAAAFxC6AYAAAAAwCWEbgAAAAAAXELoBgAAAADAJYRuAACyYNiwYRITExOyrVKlSnLvvfe6/tg7d+40jz116tTANn3cAgUKSHbRx9d9AAAAzo3QDQCICKtWrTIh8PDhwxJO5s2b59vw6ufanDZz5ky5/fbb5ZJLLpF8+fJJ1apV5YknnsjwePr888/liiuukDx58kiFChVk6NChcubMmZDbLFmyRO6//365/PLLzX3qfT/wwAOyd+/es+7v2muvNScy0l6uv/76TD8HrfXBBx+UEiVKSP78+aVFixayfv36s2730Ucfyd133y1VqlQxj6GPfaGy87EAINzl8LoAAACcCt3PPvusafEtXLiwJzVs27ZNYmNjLzjYTpw48YLCbcWKFeXvv/+WnDlzZqFKZ2rTx8+RI3K+RmiALFu2rAmIGqI3bdokEyZMMPtAw2TevHkDt50/f7507NjRBMhXX33V3Pa5556T/fv3y2uvvRa43YABA+TQoUNy6623mtD566+/mvucM2eObNy4UUqXLh1SQ/ny5WXUqFEh27SmzEhNTZX27dvLd999J0899ZQUL15cJk2aZGpct26deXyb1qjbGjVqJH/88ccF76vsfCwAiASR82kJAIgox48fNy1o4SR37tyu3r+2pGrgyZUrl2lh9ZLXj++0//mf/zmrFbZBgwbSrVs3+eCDD0wLte3JJ5+UOnXqyMKFCwMnHgoVKiTPP/+8PPbYY1KtWjWzbezYsdKsWbOQEzHact28eXMTvjWoB4uPjzehP6v164mnGTNmyC233GK23XbbbaaVXVvhp02bFrjte++9J+XKlTN11apVy9ePBQCRgO7lABAhfv/9d+nevbtpGdPwV7lyZenZs6ecOnUqcBttadNWt6JFi5rurk2aNJG5c+eG3M/y5ctNN9CPP/5YRo4caVrfNGC1atVKtm/fHrhd7969zRjiv/7666xa7rzzTtOKl5KSckHjo7du3Sr//Oc/pUiRIiasqO+//960XmvXXK1D71e77Aa3mum/1xY3pc/b7pqrY59t77//vglR2mKpz/+OO+6QxMTETNX33//+17TU6eNfeuml8p///Cfd26Ud03369GnT+q4tf/pvixUrZp7XokWLzPV6W21JVsFdioPHbb/44osyfvx487j6uuo+Sm9Md/Br3LZtW3PCQo+F4cOHi2VZZ72++jNY2vs8V232trQt4Bs2bJB27dqZAKrHhh4za9asCbmN3r/+26+++kr69esX6J7cqVMnOXDggHglvW7PWpP64YcfAtt0/+tFW8aDW/ofeeQRs581kNquueaas3o+6DY9/oLvM+2JlWPHjl1w/fq4pUqVks6dOwe26b7VMPzZZ5/JyZMnA9sTEhIuuEeGV48FAJGAlm4AiAB79uyRK6+8MjDOUlvaNITrl2MNxdoyum/fPrnqqqvM748++qgJgO+8847cdNNN5nZ2wLCNHj3afFnWVr0jR47ImDFj5K677pKvv/7aXK/jXzWUaWjXIG/T+589e7YJbXFxcRf0POxuuNpiaAdFDagaJO+77z4TuLds2SKvv/66+amBTgOcfvn/6aef5MMPP5Rx48aZ7q52EFB68mDIkCEmFGiLpYY77RasAUiD4rm6o2vX4TZt2pj70pCpoUhb8zR0nI/eXrsL62Pq65OcnCxr16413ZWvu+46eeihh8xrp89RWwTTM2XKFDlx4oR5XTV0a2DT1u706EkObUnVkyn6ei1YsCAw1ljD94XITG3B9PW4+uqrTeDu37+/6fquJyc0zK5YsUIaN24ccvs+ffqYkytanwZ+PbGgJ3J0DLBfJCUlmZ/28aT0eFENGzYMua2e4NATVPb1GdFArZfg+7TpMawnIPREmR5fPXr0kGeeeSZTwwj0cXWMedqAq8edvl/0vmvXri1OyM7HAoCIYAEAwt4999xjxcbGWt9+++1Z16Wmppqfjz/+uKZY68svvwxcd/ToUaty5cpWpUqVrJSUFLNt2bJl5nbVq1e3Tp48Gbjtyy+/bLZv2rQpcL/lypWzunTpEvJ4H3/8sbndypUrM13/0KFDzb+58847z7rur7/+Omvbhx9+eNZjvPDCC2bbjh07Qm67c+dOKy4uzho5cmTIdn0eOXLkOGt7Wh07drTy5Mlj7dq1K7Bt69at5j7TfoxWrFjR6tatW+D3unXrWu3btz/n/ffq1eus+1H6PHR7oUKFrP3796d73ZQpUwLb9HF1W58+fQLb9DXSx8+VK5d14MCBkNdXf57vPjOqTel2fd2C95M+zi+//BLYtmfPHqtgwYLWNddcE9im96//tnXr1oFjU/Xt29fs08OHD1t+0b17d1PTTz/9dNZxtnv37rNu36hRI6tJkybnvM8RI0aYf79kyZKQ7ffff781bNgw65NPPrHeffdd66abbjK3u+222zJVa/78+c19pDV37lxzPwsWLEj339WsWdNq3rx5ph7Di8cCgEhAfx8ACHPa6vnpp59Khw4dzmp9U3aXYJ0QSlui7G7bSrsAawuqtjRql9lg2rKsLeQ2bcVU2ups36+2TOv9BneH1ZZKHcMZ/DiZ9fDDD5+1LXgCK23xPXjwoGnJVenNlpzerNS6j7SVW/+tfdFWc21VX7ZsWYb/VluOv/jiCzNplk6uZatevbrpwn0+2oKuLcA///yzZFWXLl0CLfaZoa3FNn2N9HdtOV28eLG4RfeTjm/W/aTDAGxlypQxwwW0e7628gfT4y64u7oeX3o/u3btEj/QcclvvfWWmcE8eGIwnUAuo/H7OoTAvj49K1euNMMN9Fhs2bJlyHX6WNrqr702unbtarppa0u3DvNI20U/Pfq4GdUUXLcTsvOxACASELoBIMxpV2kNNOebpEjDjC6DlJYGSPv6YMEhU2lXYPXnn38GtmkXc/2CrcsnKQ3fGsI1jKddwzozdDx2Wjr7s05Opd1tNYBrALVvp93ez0cDrzbManDSfxt80XG1OuP0ufatPr/g0GVLb1+mpV26tcu/TjCl3W113LmOUb/YfZIR7e4bHHqVPrYKHt/uNN1POqwgo+NLT3qkHT+fmeMrLX0ttMt3Vi6ZOVZsX375pZkfQU+s6NCE9E4CBY9bDj4pFHySKNiPP/5ohnDo+/TNN9/MVB0a+JV9wkRPnqR9Xva8Cfq4GdUUXHdmZedjAUCkY0w3ACBdGY3HDp6US1ucdfIwbY3TFk0dy63BSMN4VqT3ZV1bBXWmZA2s9erVM63zGuJ07HJGY5uD6W30BIAu85Tec9L7c4uOGf/ll19Mq6W2BGvY0jHnkydPDpkN+1ycDjAZnQzJ7KR32Xl8paW9KLQHRlboLOTpTTyXli6DpfMcaDjWuQ7SLoumrfdK19rWScKC6TbtTZKWnnDQeQF0dnI9KVWwYMFM1Wzfv554Uvo+0PWwg+3YscO8B7Wu9Nb/trdldukxW3Y+FgBEOkI3AIQ5bbHVyas2b9583rWddR3p9Frg7OuzQkPxyy+/bFrbNRTpl3K7+/fF0lbPJUuWmC65OqGULb3u2hmFSZ31W4Octhjbrb4Xsm819Kb3eOnty/ToxGcaFPWiPQE0iOsEa3bozkqPgHOdYNDu/8HPUye1Uvq6BLcoawt8sPS6dWe2Nt1POht+RseXtsCnDahZoS3P9szvFyozQVBPkOjJnJIlS5pwnN4JGT3xo3RCvOCArZPO/fbbb6bbfDCdZV8Dt7YM67Fsh/bMsIdy2MML6tate9bzt9f61rq0hV6PgeAJznTiQ31tLvTYz87HAoBIR+gGgDCnX3p1LK0uiaVBIO24bg2cGp5uuOEGM0P06tWrpWnTpoG1sHW2YQ1kNWrUyNLja6u2zpStM6HrbNnaFdzp1tC0rZ/6PNKy1/ROGyZ1jOygQYNMcNd9FBwk9X61FVFncs/o8TXo6Zj53bt3B7pEa7d0Het9Phq4gu9bQ9xll10W0tU6uO5zzaKeWbr+8yuvvBJ4fvq7zn6ty3fZJ1f0een4Yj1ubJMmTTrrvjJbm96fBktt0ddu7HbA1xnzdWy0ju/XE0MXSwPrhYTWC6Hdp/U56PtJX9uMxtHXrFnTrA6g7xud4d0+Rl977TVzbNnrVtvvL33f6UoCOndAesMUlJ6w0jHSweOk9bWz1/G25w/QEyatW7dO9z70cbVlXucwsGvQuQt0LW2d7+FC15DPzscCgEhH6AaACKBLbGn35ebNm5uWNh1Hq1099UuwTmKlgWngwIFmSS1dR1mXDNMWWA3K2mX0k08+yfJaurp0kAbJp59+2rTmZbVreXo0qGnLsIZ6XfNaJ2jT56k1p6VrcCutQ9fg1qCpAUBbujW8aPDWQKhBU7v36n3MmjXL7C9dFi0jGtb1ZIJO9KVrMevyW7rcmIav843P1hMZumSW1qb7W0+KaFgJnuzMrltfEw1XGuK0/qzQiay0Vu1KrUt0aZd6XdLtX//6VyBEahdnHXOvz0FDou6fOXPmpDu2/UJq032sLaMasHU/abdsXTJMjwl9/fxOW7i1ZVmXO9P3jF5sOp+ALvFme+GFF0wXdA3puj+0l4me3NDeC/YcCUqX2Pvmm2/MuvJ6oiZ4bW49AWOf9NAJAXVte73oe0mHaOixqWuZ6/Gp77Hz0fCrPUy0R4VOiqhLkumJFB02oMdwMD3hohd7PL6eHLADvr7f9OKXxwKAiOD19OkAAGfokla6dFiJEiWs3LlzW5dccolZ8il42S9dzumWW26xChcubJbBuvLKK605c+aE3I+9pNSMGTPOu6SU7emnnzbXXXbZZVmq3V4yzF7WKthvv/1mderUydQcHx9v3XrrrWYpqrRLVtnLMekyZrp8Wtrlw3QppmbNmpnljvRSrVo1s3+2bdt23vpWrFhhNWjQwCyJpft18uTJgZrPtWTYc889Z/ax1p43b17zmLpE2alTpwK3OXPmjFnmS1+3mJiYwH3a+1uXqEoroyXD9Hnpa9ymTRsrX758VqlSpUyd9nJwNt3PutSb3qZIkSLWQw89ZG3evPms+8yoNpXe/l+/fr3Vtm1bq0CBAua+W7RoYa1atSrkNvaSYWmXt8toKbPsoo+d0SW9Za5mzZpl1atXz7zXypcvbw0ePDjkdbWPh4zuU6+z/frrr+a41qX79H2p+06PNz3OgpdVO59Dhw6ZZc6KFStm7kPrTm8ZQfvYTe+S9jX1w2MBQLiL0f94HfwBAAAAAIhELBkGAAAAAIBLGNMNAHCNztatl3PRscYZLR8FAAAQ7gjdAADXvPjii2dNrJSWvfYvAABAJGJMNwDANTobtL3WcEZ0tmuddRsAACASEboBAAAAAHAJE6kBAAAAAOASxnSLSGpqquzZs0cKFiwoMTExXpcDAAAAAPA57TR+9OhRKVu2rMTGZtyeTegWMYE7ISHB6zIAAAAAAGEmMTFRypcvn+H1hG4R08Jt76xChQp5XQ4AAAAAwOeSk5NN462dJzNC6NbZ5P5fl3IN3IRuAAAAAEBmnW+IMhOpAQAAAADgEkI3AAAAAAAuIXQDAAAAAOASQjcAAAAAAC4hdAMAAAAA4BJCNwAAAAAALiF0AwAAAADgEkI3AAAAAAAuIXQDAAAAABCJoXvYsGESExMTcqlWrVrg+hMnTkivXr2kWLFiUqBAAenSpYvs27cv5D52794t7du3l3z58knJkiXlqaeekjNnznjwbAAAAAAACJVDPFazZk1ZvHhx4PccOf5/SX379pW5c+fKjBkzJD4+Xnr37i2dO3eWr776ylyfkpJiAnfp0qVl1apVsnfvXrnnnnskZ86c8vzzz3vyfAAAAAAA8E3o1pCtoTmtI0eOyFtvvSXTpk2Tli1bmm1TpkyR6tWry5o1a6RJkyaycOFC2bp1qwntpUqVknr16smIESNkwIABphU9V65cHjwjAAAiW0qqJXGxMV6X4Zs6AADwdej++eefpWzZspInTx5p2rSpjBo1SipUqCDr1q2T06dPS+vWrQO31a7net3q1atN6NaftWvXNoHb1rZtW+nZs6ds2bJF6tev79GzAgAgcmnQHT1rgyQePOZZDQnFC8jATnzOAwD8z9PQ3bhxY5k6dapUrVrVdA1/9tln5eqrr5bNmzdLUlKSaakuXLhwyL/RgK3XKf0ZHLjt6+3rMnLy5ElzsSUnJzv8zAAAiGwauLcn8fkJAICvQ3e7du0C/1+nTh0TwitWrCgff/yx5M2b17XH1dZ0DfgAAAAAAETNkmHaqn355ZfL9u3bzTjvU6dOyeHDh0Nuo7OX22PA9Wfa2czt39MbJ24bNGiQGTNuXxITE115PgAAAACA6Oar0H3s2DH55ZdfpEyZMtKgQQMzC/mSJUsC12/bts0sEaZjv5X+3LRpk+zfvz9wm0WLFkmhQoWkRo0aGT5O7ty5zW2CLwAAAAAARFT38ieffFI6dOhgupTv2bNHhg4dKnFxcXLnnXeaJcK6d+8u/fr1k6JFi5pg3KdPHxO0dRI11aZNGxOuu3btKmPGjDHjuAcPHmzW9tZgDQBAOPHLbNx+qQMAgEjgaej+7bffTMD+448/pESJEtKsWTOzHJj+vxo3bpzExsZKly5dzMRnOjP5pEmTAv9eA/qcOXPMbOUaxvPnzy/dunWT4cOHe/isAADIGmYFBwAg8ngauqdPn37O63UZsYkTJ5pLRrSVfN68eS5UBwBA9mNWcAAAIouvxnQDAAAAABBJCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAIh4KamW+IFf6gAAANknRzY+FgAAnoiLjZHRszZI4sFjntWQULyADOxU37PHBwAA3iB0AwCiggbu7UnJXpcBAACiDN3LAQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAQETyyxJtfqkDAOANZi8HAAARiaXiAAB+QOgGAAARi6XiAABeo3s5AAAAAAAuIXQDAAAAAOASQjcAAICH/DLRml/qAIBI45sx3aNHj5ZBgwbJY489JuPHjzfbTpw4IU888YRMnz5dTp48KW3btpVJkyZJqVKlAv9u9+7d0rNnT1m2bJkUKFBAunXrJqNGjZIcOXzz1AAAADLEhG8AENl8kUy//fZb+c9//iN16tQJ2d63b1+ZO3euzJgxQ+Lj46V3797SuXNn+eqrr8z1KSkp0r59eyldurSsWrVK9u7dK/fcc4/kzJlTnn/+eY+eDQAAwIVhwjcAiFyedy8/duyY3HXXXfLGG29IkSJFAtuPHDkib731lowdO1ZatmwpDRo0kClTpphwvWbNGnObhQsXytatW+X999+XevXqSbt27WTEiBEyceJEOXXqlIfPCgAAAAAAH4TuXr16mdbq1q1bh2xft26dnD59OmR7tWrVpEKFCrJ69Wrzu/6sXbt2SHdz7YKenJwsW7ZsycZnAQAAAACAz7qX61jt9evXm+7laSUlJUmuXLmkcOHCIds1YOt19m2CA7d9vX1dRnR8uF5sGtIBAAAAAIiYlu7ExEQzadoHH3wgefLkydbH1onWdIy4fUlISMjWxweASOGX2Y79UgcAAIBvWrq1+/j+/fvliiuuCGzTidFWrlwpEyZMkC+++MKMyz58+HBIa/e+ffvMxGlKf37zzTch96vX29dlRGdJ79evX0hLN8EbAC4csy4DAAD4NHS3atVKNm3aFLLtvvvuM+O2BwwYYEKwzkK+ZMkS6dKli7l+27ZtZomwpk2bmt/158iRI014L1mypNm2aNEiKVSokNSoUSPDx86dO7e5AAAuHrMuAwAA+DB0FyxYUGrVqhWyLX/+/FKsWLHA9u7du5sW6aJFi5og3adPHxO0mzRpYq5v06aNCdddu3aVMWPGmHHcgwcPNpOzEaoBAAAAAF7zxTrdGRk3bpzExsaalm6d+ExnJp80aVLg+ri4OJkzZ4707NnThHEN7d26dZPhw4d7WjcAAAAAAL4L3cuXLw/5XSdY0zW39ZKRihUryrx587KhOgAAAAAAwmydbgAAAAAAIhWhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugHAh1JSLfEDv9QBAAAQrnJ4XQAA4GxxsTEyetYGSTx4zLMaEooXkIGd6nv2+AAAAJGA0A0APqWBe3tSstdlAAAA4CLQvRwAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAABwXimplviBX+oAgMzKkelbAgAAIGrFxcbI6FkbJPHgMc9qSCheQAZ2qu/Z4wNAVhC6AQAAkCkauLcnJXtdBgCEFbqXAwAAAADgEkI3AAAAAAAuIXQDAAAAAOASQjcAAAAAAC4hdAMAAAAA4BJCNwAAAAAALiF0AwAAAADgEkI3AAAAAACRGLpfe+01qVOnjhQqVMhcmjZtKvPnzw9cf+LECenVq5cUK1ZMChQoIF26dJF9+/aF3Mfu3bulffv2ki9fPilZsqQ89dRTcubMGQ+eDQAAAAAAPgrd5cuXl9GjR8u6detk7dq10rJlS7n55ptly5Yt5vq+ffvK7NmzZcaMGbJixQrZs2ePdO7cOfDvU1JSTOA+deqUrFq1St555x2ZOnWqPPPMMx4+KwAAAAAA/k8O8VCHDh1Cfh85cqRp/V6zZo0J5G+99ZZMmzbNhHE1ZcoUqV69urm+SZMmsnDhQtm6dassXrxYSpUqJfXq1ZMRI0bIgAEDZNiwYZIrVy6PnhkAAAAAAD4a062t1tOnT5fjx4+bbuba+n369Glp3bp14DbVqlWTChUqyOrVq83v+rN27domcNvatm0rycnJgdZyAAAAAACisqVbbdq0yYRsHb+t47ZnzZolNWrUkI0bN5qW6sKFC4fcXgN2UlKS+X/9GRy47evt6zJy8uRJc7FpSAcAAAAAwBct3Zdccon88ccfZ20/fPiwue5CVK1a1QTsr7/+Wnr27CndunUzXcbdNGrUKImPjw9cEhISXH08AAAAAEB0ylLo3rlzp+kOnpa2Hv/+++8XdF/amn3ZZZdJgwYNTBiuW7euvPzyy1K6dGkzQZoG+WA6e7lep/Rn2tnM7d/t26Rn0KBBcuTIkcAlMTHxgmoGAAAAAMDx7uWff/554P+/+OIL00ps0xC+ZMkSqVSpklyM1NRUE941hOfMmdPcpy4VprZt22aWCNPu6Ep/6uRr+/fvN8uFqUWLFpnlx7SLekZy585tLgAAAAAA+CZ0d+zY0fyMiYkx3cCDaUDWwP3SSy9l+v60xbldu3ZmcrSjR4+amcqXL18eCPTdu3eXfv36SdGiRU2Q7tOnjwnaOnO5atOmjQnXXbt2lTFjxphx3IMHDzZrexOqAQAAAABhFbq1FVpVrlxZvv32WylevPhFPbi2UN9zzz2yd+9eE7Lr1KljAvd1111nrh83bpzExsaalm5t/daZySdNmhT493FxcTJnzhwzFlzDeP78+c3JgOHDh19UXQAAAAAAeDZ7+Y4dOxx5cF2H+1zy5MkjEydONJeMVKxYUebNm+dIPQAAAAAA+GLJMB1rrRdtrbZbwG1vv/22E7UBAAAAABB9ofvZZ581XbgbNmwoZcqUMWO8AQAAAACAA6F78uTJMnXqVDOBGQAAAAAAcHCdbl0/+6qrrsrKPwUAAABck5JqiR/4pQ4AYdrS/cADD5jlvYYMGeJ8RQAAAEAWxcXGyOhZGyTx4DHPakgoXkAGdqrv2eMDiIDQfeLECXn99ddl8eLFZpkvXaM72NixY52qDwAAALggGri3JyV7XQYAZD10f//991KvXj3z/5s3bw65jknVAAAAAAC4iNC9bNmyrPwzAAAAAACiSpYmUgMAAAAAAC61dLdo0eKc3ciXLl2albsFAAAAACCiZCl02+O5badPn5aNGzea8d3dunVzqjYAAAAAAKIvdI8bNy7d7cOGDZNjx7xbngEAAAAAgIgd03333XfL22+/7eRdAoCjUlIt8QO/1AEAAAAftnRnZPXq1ZInTx4n7xIAHBUXGyOjZ20wa7h6JaF4ARnYqb5njw8AAACfh+7OnTuH/G5Zluzdu1fWrl0rQ4YMcao2AHCFBu7tSclelwEAAIAokKXQHR8fH/J7bGysVK1aVYYPHy5t2rRxqjYAAAAAAKIvdE+ZMsX5SgAAAAAAiDAXNaZ73bp18sMPP5j/r1mzptSvzxhFAAAAAAAuKnTv379f7rjjDlm+fLkULlzYbDt8+LC0aNFCpk+fLiVKlMjK3QIAAAAAEFGytGRYnz595OjRo7JlyxY5dOiQuWzevFmSk5Pl0Ucfdb5KAAAAAACipaV7wYIFsnjxYqlevXpgW40aNWTixIlMpAYAAAAAwMW0dKempkrOnDnP2q7b9DoAAAAAAJDF0N2yZUt57LHHZM+ePYFtv//+u/Tt21datWrlZH0AAAAAAERX6J4wYYIZv12pUiW59NJLzaVy5cpm26uvvup8lQAAAAAARMuY7oSEBFm/fr0Z1/3jjz+abTq+u3Xr1k7XBwAAAABAdLR0L1261EyYpi3aMTExct1115mZzPXSqFEjs1b3l19+6V61AAAAAABEaugeP3689OjRQwoVKnTWdfHx8fLQQw/J2LFjnawPAAAAAIDoCN3fffedXH/99Rler8uFrVu3zom6AAAAAACIrtC9b9++dJcKs+XIkUMOHDjgRF0AAAAAAERX6C5Xrpxs3rw5w+u///57KVOmjBN1AQAAAAAQXaH7hhtukCFDhsiJEyfOuu7vv/+WoUOHyo033uhkfQAAAAAARMeSYYMHD5aZM2fK5ZdfLr1795aqVaua7bps2MSJEyUlJUWefvppt2oFAAAAACByQ3epUqVk1apV0rNnTxk0aJBYlmW26/Jhbdu2NcFbbwMAAAAAAC4wdKuKFSvKvHnz5M8//5Tt27eb4F2lShUpUqSIOxUCAAAAABAtodumIbtRo0bOVgMAAAAAQLROpAYAAAAAADKP0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAQCSG7lGjRkmjRo2kYMGCUrJkSenYsaNs27Yt5DYnTpyQXr16SbFixaRAgQLSpUsX2bdvX8htdu/eLe3bt5d8+fKZ+3nqqafkzJkz2fxsAAAAAADwUehesWKFCdRr1qyRRYsWyenTp6VNmzZy/PjxwG369u0rs2fPlhkzZpjb79mzRzp37hy4PiUlxQTuU6dOyapVq+Sdd96RqVOnyjPPPOPRswIAAAAA4P/kEA8tWLAg5HcNy9pSvW7dOrnmmmvkyJEj8tZbb8m0adOkZcuW5jZTpkyR6tWrm6DepEkTWbhwoWzdulUWL14spUqVknr16smIESNkwIABMmzYMMmVK5dHzw4AAAAAEO18NaZbQ7YqWrSo+anhW1u/W7duHbhNtWrVpEKFCrJ69Wrzu/6sXbu2Cdy2tm3bSnJysmzZsiXbnwMAAAAAAL5o6Q6Wmpoqjz/+uPzjH/+QWrVqmW1JSUmmpbpw4cIht9WArdfZtwkO3Pb19nXpOXnypLnYNKADAAAAABCxLd06tnvz5s0yffr0bJnALT4+PnBJSEhw/TEBAAAAANHHF6G7d+/eMmfOHFm2bJmUL18+sL106dJmgrTDhw+H3F5nL9fr7Nuknc3c/t2+TVqDBg0yXdntS2JiogvPCgAAAAAQ7TwN3ZZlmcA9a9YsWbp0qVSuXDnk+gYNGkjOnDllyZIlgW26pJguEda0aVPzu/7ctGmT7N+/P3AbnQm9UKFCUqNGjXQfN3fu3Ob64AsAAAAAABE1plu7lOvM5J999plZq9seg61dvvPmzWt+du/eXfr162cmV9Nw3KdPHxO0deZypUuMabju2rWrjBkzxtzH4MGDzX1ruAYAAAAAICpD92uvvWZ+XnvttSHbdVmwe++91/z/uHHjJDY2Vrp06WImP9OZySdNmhS4bVxcnOma3rNnTxPG8+fPL926dZPhw4dn87MBAAAAAMBHoVu7l59Pnjx5ZOLEieaSkYoVK8q8efMcrg4AAAAAgAiYSA0AAAAAgEhE6AYAAACyWUrq+Xt8RlMdQCTztHs5AAAAEI3iYmNk9KwNknjwmGc1JBQvIAM71ffs8YFoQegGAAAAPKCBe3tSstdlAHAZ3csBAAAAAHAJoRtARI0J80sdAAAAgKJ7OQBHMDYNAAAAOBuhG4BjGJsGAAAAhKJ7OQAAAAAALiF0AwAAAADgEkI3AAAAAAAuIXQDAAAAAOASQjcAAAAAAC4hdAMAAAAA4BJCNwAAAAAALiF0AwAAAADgEkI3AAAAAAAuIXQDAAAAAOASQjcAAAAAAC4hdAMAAAAA4BJCNwAAAAAALiF0AwAAAADgEkI3AAAAAAAuIXQDPpeSaokf+KUOAAAAIJzk8LoAAOcWFxsjo2dtkMSDxzyrIaF4ARnYqb5njw8AAACEK0I3EAY0cG9PSva6DAAAAAAXiO7lAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAgXSmpltcl+KIG4GLkuKh/DQAAACBixcXGyOhZGyTx4DFPHj+heAEZ2Km+J48NOIXQDQAAACBDGri3JyV7XQYQtuheDgAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAARGLoXrlypXTo0EHKli0rMTEx8umnn4Zcb1mWPPPMM1KmTBnJmzevtG7dWn7++eeQ2xw6dEjuuusuKVSokBQuXFi6d+8ux455s44gAAAAAAC+Cd3Hjx+XunXrysSJE9O9fsyYMfLKK6/I5MmT5euvv5b8+fNL27Zt5cSJE4HbaODesmWLLFq0SObMmWOC/IMPPpiNzwIAAAAAgPTlEA+1a9fOXNKjrdzjx4+XwYMHy80332y2vfvuu1KqVCnTIn7HHXfIDz/8IAsWLJBvv/1WGjZsaG7z6quvyg033CAvvviiaUEHAAAAAMArvh3TvWPHDklKSjJdym3x8fHSuHFjWb16tfldf2qXcjtwK719bGysaRnPyMmTJyU5OTnkAgAAAABA1IRuDdxKW7aD6e/2dfqzZMmSIdfnyJFDihYtGrhNekaNGmUCvH1JSEhw5TkAAAAAAKKbb0O3mwYNGiRHjhwJXBITE70uCQAAAAAQgXwbukuXLm1+7tu3L2S7/m5fpz/3798fcv2ZM2fMjOb2bdKTO3duM9t58AUAAAAAgKgJ3ZUrVzbBecmSJYFtOvZax2o3bdrU/K4/Dx8+LOvWrQvcZunSpZKammrGfgMAAAAAELWzl+t62tu3bw+ZPG3jxo1mTHaFChXk8ccfl+eee06qVKliQviQIUPMjOQdO3Y0t69evbpcf/310qNHD7Os2OnTp6V3795mZnNmLgcAAACiQ0qqJXGxMVFfA/zJ09C9du1aadGiReD3fv36mZ/dunWTqVOnSv/+/c1a3rrutrZoN2vWzCwRlidPnsC/+eCDD0zQbtWqlZm1vEuXLmZtbwAAAADRQcPu6FkbJPHgMU8eP6F4ARnYqb4njw3/8zR0X3vttWY97ozExMTI8OHDzSUj2io+bdo0lyoEAAAAEA40cG9PYilg+I9vx3QDAAAAABDuCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN2IWimpGa8RH411AAAAAHBeDhfuEwgLcbExMnrWBkk8eMyzGhKKF5CBnep79vgAAAAA3EXoRlTTwL09KdnrMgAAAABEKLqXAwAAAADgEkI3InaMsl/qAAAAABC96F4OxzFWGgAAAAD+D6EbrmCsNAAAAADQvRwAAAAAANcQugEAAAAAcAmhGwAAAADgqwmJU3xQg1MY0w0AAAAA8M3EyAkRNikyoTuM6NkefQN4zS91AAAAAHAHEyM7h9AdRrw+4xSJZ50AAAAAwE2E7jDDGScAAAAACB9MpAYAAAAAgEsI3QAAAAAQJTNy+6GGaEP3cgAAAACIgjmamJ/JG4RuAAAAAMgmzNEUfeheDgAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNANAAAAAIBLCN0AAAAAALiE0A0AAAAAgEsI3QAAAAAAuCRiQvfEiROlUqVKkidPHmncuLF88803XpcEAAAAAIhyERG6P/roI+nXr58MHTpU1q9fL3Xr1pW2bdvK/v37vS4NAAAAABDFIiJ0jx07Vnr06CH33Xef1KhRQyZPniz58uWTt99+2+vSAAAAAABRLIeEuVOnTsm6detk0KBBgW2xsbHSunVrWb16dbr/5uTJk+ZiO3LkiPmZnJwsflcin8ip+DhPHz8z+4k6M4c6nUWdzqJOZ1Gns6jTWdTpLOrM/hrt21Jn9NXpJbtGy7LOebsY63y38Lk9e/ZIuXLlZNWqVdK0adPA9v79+8uKFSvk66+/PuvfDBs2TJ599tlsrhQAAAAAEGkSExOlfPnykdvSnRXaKq5jwG2pqaly6NAhKVasmMTExEik0jMxCQkJ5qAoVKiQ+BV1Oos6nUWdzqJOZ1Gns6jTWdTpLOp0FnVGZ50XS9uvjx49KmXLlj3n7cI+dBcvXlzi4uJk3759Idv199KlS6f7b3Lnzm0uwQoXLizRQg/8cDj4qdNZ1Oks6nQWdTqLOp1Fnc6iTmdRp7OoMzrrvBjx8fGRP5Farly5pEGDBrJkyZKQlmv9Pbi7OQAAAAAA2S3sW7qVdhXv1q2bNGzYUK688koZP368HD9+3MxmDgAAAACAVyIidN9+++1y4MABeeaZZyQpKUnq1asnCxYskFKlSnldmq9ol3pdyzxt13q/oU5nUaezqNNZ1Oks6nQWdTqLOp1Fnc6izuisM7uE/ezlAAAAAAD4VdiP6QYAAAAAwK8I3QAAAAAAuITQDQAAAACASwjdAAAAAAC4hNCNsKJrsMM57E9nsT+dxf50FvvTWexPZ7E/ncX+dBb70zmpUbovCd0IC7t27ZLff/9dYmM5ZJ3w888/y6+//sr+dAjHp7PYn85ifzqL/eksPo+cxfHpLI5P5/wc5fsyOp81jO3bt8u4ceOkf//+Mn/+fNm3b5/40caNG6VBgwby5Zdfip/99NNPZq34e++9V959913ZtGmT+NF3330ntWrVki+++EL8jOPTWexPZ7E/ncX+dBafR87i+HQWx6dz2JdhRNfpRvTZtGmTVaRIEatZs2ZW48aNrdy5c1t33nmnNW/ePMtPNm7caOXNm9d64oknzrouNTXV8ostW7ZYhQsXtq6//npzKVWqlNWyZUtrypQplp9s2LDB7M8nn3zS8jOOT2exP53F/nQW+9NZfB45i+PTWRyfzmFfhhdCdxT666+/rBtvvNHq06ePdebMGbNt/vz5Vps2baxrr73WmjlzpuUHP/74o/lwGzZsmPlda/3vf/9r6vv+++8DtXvt1KlTVteuXa0HHngg8MH2zTffmN9r1Khhvfbaa5Yf/PTTT1aOHDms4cOHm99Pnz5tLViwwHr99det5cuXW/v27bP8gOPTWexPZ7E/ncX+dBafR87i+HQWx6dz2Jfhh9AdhfSPb/369a3nnnsuZPvq1autm266yZwtW7NmjeWlEydOWP/85z+tokWLWt9++63Z1qFDB6tmzZpW8eLFrbi4OOupp56yfv31V8tr+sfu6quvtnr16nXWh+AjjzxiNWjQwPrss88sr/849+/f38qTJ481d+5cs61du3Zmf5YtW9Zs1z/eegx4jePTWexPZ7E/ncX+dBafR9F3fP79998cn1F4fLIvww9juqNwxsCTJ09KmTJl5ODBg2ZbSkqK+dmkSRN58sknZffu3fLpp5+abXpixgu5c+eWBx98UFq1amVqqlKliql9ypQpZvyK/nzjjTfkvffe87RO+3Hr1KkjBw4ckD///DNwXdWqVeXhhx+WYsWKycyZMz2tM2fOnNK1a1d56KGHpG/fvlKxYkWz7cMPP5TExERT34YNG2Tq1Kme1snx6Syt6cSJE2GxP3v06OH7/an7juPTOWfOnGF/OkwfOxw+j+6++27ffx6pv/76y/fHZ548eaR79+6+Pz61pnA5Pu+66y5fH5989wxTXqd+eGPChAlWrly5rC+++ML8npKSErhu0qRJVsGCBa39+/dbXtOuJ/ZYlV9++SXkutGjR5uxLH/88YcntQWPkfr444/NeBXtLpN27NSMGTNM1xqvzjIHv7Zbt261HnzwQXOmUf8/2Ntvv23lzJnT2r17t+W1iRMnhsXxuWLFCt8en8F4v198S2cw7bbnx/2Zts4vv/zSl/vz0KFDYbE/09bp1/2p3TO3b9/u+88jrVO7mvr98+jnn3+2XnjhBd8fn1rnv//9b98fn2mPwWnTpvny+Exbi3bL9+PxGezDDz/05b5Mb16EB32+L7MDoTsKJCYmmvET+kEc/Obr1q2b+bDQcT/BFi5caNWuXTvb/zhnVKd2O5k9e7YZBxL8gacfhHXq1DHdV7LT0aNHz6pFDRkyxIypev/990O+/OrkJtqVJrv/8GVUp37pWbp0aWC/2dd98sknZhzQ4cOHs7XOpKQka+3atea405pt3bt399XxmVGd2p3PT8fnrl27zJcaPXGh47v8uj8zqtNv+1Mnqmnfvr21ePHikO06bs5P+zOjOvWY9dP+XL9+vRUbG2t+Bv9d8tv+zKhOv+3P7777zrr88svN+0j/RtkGDx7sq8+jjOrctm2brz6PtE7tpl2xYsWQIO234zO4zgMHDvj2+NTvGzp5Vs+ePa1Ro0b59vjMqE7tqu2X41NDqR5v+rn5ww8/BLYPHDjQV/syozq3bNnim33pFUJ3hNMzdTqbYaNGjcy4noYNG1q9e/cOjFW67bbbrHz58lnvvPOOtWPHDrNNZ76sW7eu9eeff3paZ/A4lbQtOOrRRx+1OnfubCY6ya6ZOfUMXdu2bc0fE/sPh/3hpnTclH5RGzFihAkSR44cMduqVKkS8sHoRZ3BXxzT21/6uuvkMMGBMjte9+rVq5vjLSYmxrrhhhvMNqVfeO666y7fHJ9p69QvPbbgfevl8al1JiQkWC1atLDi4+PNTw0NSo8/Hffnl/2ZUZ0qvUl/vNif+jh6clJr1MmUggOtfXxqK4PX+/Ncdfrp+NQvgRpc+vXrd9Z1Bw8eNDNC++H4TK/O4H2UXnDxYn9qUChWrJj12GOPpft3+/HHH/fF59H56jx58qQvPo/s2b/1vaStwq+88krI8emXz6O0db766qu+fL/r33kdU37rrbeaGbV1bLz2urLpvvPD8ZlendqDwRb8Hc+r41O/b+h3ZP1epydbmjRpYl5/v73X06vz3nvvPed3+Sc8eK97hdAdwfSskX4Y6JtR//+3334zb0g986VfzIIPeH1zVKhQwYRd/XAM/gLsVZ21atUy4Sa9s2h6llS/YG7evDnb6tQP2WrVqpluMFdddZU5O5deoB03bpw5a6f7VJ9X6dKls3V/ZrZOm3ZL/Ne//mU+vLULUHZ+EStTpox5LfVMrJ5RLl++vPliZtMvCHoW18vjM6M69XhNj1fHp9alx9rTTz9t9pvWoftNu5/Z9MuW1+/3jOqcPn26r/anTSek0WWCOnXqZLVu3TrQxdT+AjFgwABP92dGdWpLg5/2p/5t0aCgPYKCuxvrl7Tgkyza2uTl/syoTv1inl7Y9vL41Peynqiw39v6XtegqKHQNmbMGE8/jzJbp9efR/aSRvp5Y7+f9PNTv4/46fMoozp///33dG/v1fGpQU9b1nUSLaXf7bRr8dixY0Nup93jvTw+M1unfaLCi+NTe4boSf9BgwaZv0F6wnfo0KGmAUB7N/nlvZ7ZOr1+r3uJ0B3BtPumdudatWpVYJueSdLu27pdz+rZvvrqKzP+44MPPjChzS91Vq1aNaRO/QOiy3RUrlzZfPhkFz3TqWO8dLZSreG6664zM0MGB9rgL44a1JYtW2a+oAd/aPuhzuCz3NrdR2+j+zk796d+eXnooYdMl2dt5bD33eTJk81JobRn43WIgRfH5/nq1NAVXKe2QHhxfB4/ftx0f9QxU3oM2DXdcsst1siRI61nn302JNRqF0kv9uf56tQlRYLrXLdunSf7M5j2FtHxkF9//bU5e69n5LUu/bJoj0Hz6vg8X526z7RO/Rvr5fGpf8+bN29uvlzZtNVNW5T0C5nWFNyq6NXn0fnq1B4ZL7/8cuA63YdeHp/6vrHr0RYlncn40ksvNRftNWafZNWeT158HmWmTj1RZNepJza8+DzSk6kaSu0gq/Qzs1ChQqY7bNoT1l693y+0Tq/e70r/RurJ/+C5Bu677z7zftIeV/q5avPy+Dxfndrd3KYnCL04PleuXGnVq1fP2rNnT8g+095iun68niQIHq7h1b48V516IuDGoMY+PQHkxb70GqE7gukkMPrH9sUXXwzZrkFBzzDrOCQdX+X3OvUspIYcm477TjtJiNs0HOgfZz0RoDTABgdau3uc12thZrbO4KCofyjtL+XZ+eVWP9imTJkSsv3TTz81rcrJycmmxvRa5v1YZ7B58+Zl+/GpS8Z8/vnn5kuWTQOsBgX94qAtIfp+D+5F4IXM1hncrdeL93swHR+pdSnttq0tyeXKlTM1Z9S65Lc6g8fQerE/9eSVjje87LLLrI4dO5qTAvoFTIOLngDSbrsaEqdOnZqtdWWlziuvvNJ67733fHF8ao3690nH6+pJFu0CrRddwkpbnNLrKebHOnV5K5uefM/uzyMNzum1umtd11xzTaBLrNefR5mtM5hXx6f2aNLeALpuuJ5g1b/zOqmX9gzSXmIatuy/V17KTJ3NmjXz9PhctGiRGbuv4/VtGlT1b9H48eNNo5WedPVaZur86KOPPN2XXiN0RzD9A6xjPnQmS3ucbHCLk7aG3nHHHZbXwqXOtIFav6DZgXbmzJmBcT9er4uY2TpnzZpleSn4bKhds34J02EFwUE27UyX2S1c6gweF6ln5HXsoX0s6pdF/RKh3SG1q2w41BkcFL2kLQfaGmfTrttas7bY6WzBfuH3OvWEi4ZXPcHatGlTa+/evYHrdBKqf/zjHybUei0c6rTDnwYwfZ317/szzzwTchvtMaJdTb2cufhC6gxuafSixmD233U92aqt8fba116G7nCp06ZjirXLtp7409ddg6ye+Ldpy7x2f9ZW2XCo0+5J4AUNppUqVTLfk/X9og0l2uNBh2gpDbX6uem1cKnTS4TuCKfjJHRSA50wLe2H2ksvvWRdccUVJth6LVzqTBu+tCb9Q60hQVuXH374Yats2bIhQc3PdfqhpS74C4Ke+dSzzseOHTO/63gfbRnxw6yW4VKnzT4G7bp1SRE/zhDq9zq1Lm1F0q7kXbt2Ne8bnWRHW++0dVaXjfODcKhTTwDOmTPHmj9/fuBvk/1TJ87U+v0QGMKlTv2Sq93htTeDvubB9PXWVrqdO3daXguXOtNrELjkkkus+++/3/Izv9apPcH0pI++xnqSOu0s69qjRHvmec3PddonVnRyNO31qa+zzikTHF5vv/1201PMS+FSp9dyeL1OONyTmpoqtWrVks8++0xatWplfn/kkUekRYsW5voff/xRypcvLzlyeHsYhEudweLi4uTMmTOSL18++fzzz6Vjx45y9913S86cOWXlypVSpkwZCYc6y5Yt63WJEhsbG/j/U6dOydGjR81rPXToUBkzZoysXr1a4uPjxWvhUqetdOnSIXVv2rTJvM9y584tfuLnOvXEtL5/9GfTpk1NjXPnzpV69epJxYoV5d1335VKlSp5XWbY1Jk3b1657rrrTH36t0nZPw8ePGjqDX6feSUc6tTXukKFCvL666/LHXfcYV7vUaNGyaBBg+TkyZOyZMkSKVasmBQqVIg6syAlJcX8Derfv7+MHTtW1q1bJw0aNBC/8XOdBQsWNBf9Tqc1/vDDD3L11Veb6/T7XoECBaRcuXJel+nrOmNiYkxdjRo1kkWLFpn3zPHjx6VatWrmev27n5ycLM2aNfOkvnCr03Nep35cPD3jnrZLsX0W3t6uZ+t0ggNtMdZZDW+++WYz+Ubw2ErqPH+dadm305ZjnSgiO2cHjcQ6dYIabZXTGYx13cngsUFui8Q67V4O2hJfokQJ377ufq9Tx/lq1+20r7Pd0yE7RFKdaVuV9XXXORJ0fGV2iYQ67Z86tEAnK9MJi7Q+bYnXv/PZOUFRJNSZHh02lCtXrpAJ9LJDJNWpQ5q0l532ttPejNoirxOA+e1197rOc9WY3jJv2lNRu27rcmc6eW92CZc6/ShG/+N18EfWbd26VZ5//nlJSkqSKlWqyI033ijt27cPnAHVs/P2z927d5uzoEuXLpWEhAS56aabAmehqDPzdaY1YcIEefTRR03N9evXp86LqHPVqlXmTGiRIkXM2dIrrriCOi+iTu3dMHPmTPNe0jP2fn3d/V7n6dOnzVn7woULm9/1Y1PP7GeXSKvTNmvWLJkxY4YsX77ctID67XUPhzq1dUlb3f/44w/57bffZP78+aZluXHjxnLppZdS5wXWmZ5///vf5rY1a9akzgus0/4bpK3Hr7zyiuzcudP0vnnsscekevXq1HkBNQbbsWOHvPnmmzJlyhTf/U3yQ51+RegOY9u2bTMfWO3atTNdB/VDTLsNaxgYN25coBtsrly5sv3LV6TXGezAgQOm20x2fXGI5Dr1g+62226TqVOnSo0aNajzIuvctWuXCbN64srPr7tf69QucsHd3O3gkJ0iqc703kfvv/++3H777eYLHHVeXJ1eiOQ6zxVwqTPzddp/j/7++28zdENPDuptqfPCa7TpyVX9dyVKlDCNU9khXOr0Na+b2pE12oVDu7ppF5jgySCee+450z27R48eZy1x5MWsxZFap862vH//fup0sE57luD0lj2hzqzXmZ0TPkVyneHyPgqXOu3XPTuXWYzkOsPldafO6Ksz7fe69LogR2udWXnNw+U7stcrpfiR9zOWIEu0NXjPnj2mi4dNJ4LQ7sM6UdaGDRtk9OjRZrt26ejdu7fpMqNn8qjz4uvs1auXvPzyy9TpYJ2vvvqqOWOfnS0ikV6nvu7Z2XMkkusMl/dRuNRpv+7Z2SIfyXWGy+tOndFXZ9rvddn1tz4c6szKax4u35G9qNPvCN1hyB4RoONI9cu/dt0IfhPcf//9ZtzE7NmzTVcPHW+h2/SSnV8cqJM6L6TO7t27my5y2fWBHA116utOndH1PgqXOsPldQ+XOsPldadO6qTO8KkxnOoMC143tSPrdD1rnQ1QZ1g8evRoSHcYXatV18ScPXu2x1VSp9Oo01nU6SzqdBZ1Oos6nUWdzqLO6KszHGoMpzr9jNAd5pYuXWqWLOrVq5d14MCBwHYdh6ZLbq1atcryA+p0FnU6izqdRZ3Ook5nUaezqNNZ1Bl9dYZDjeFUp18RuiPA559/bt4EnTt3tqZPn27Waxw4cKBZBzMxMdHyC+p0FnU6izqdRZ3Ook5nUaezqNNZ1Bl9dYZDjeFUpx8RuiPEunXrrObNm1sVK1a0Lr30Uuvyyy+31q9fb/kNdTqLOp1Fnc6iTmdRp7Oo01nU6SzqjL46w6HGcKrTb1inO4LoGsyHDh2So0ePSpkyZaR48eLiR9TpLOp0FnU6izqdRZ3Ook5nUaezqDP66gyHGsOpTj8hdAMAAAAA4BLmcgcAAAAAwCWEbgAAAAAAXELoBgAAAADAJYRuAAAAAABcQugGAAAAAMAlhG4AAAAAAFxC6AYAAAAAwCWEbgAAAAAAXELoBgAA6dq5c6fExMTIxo0bvS4FAICwRegGACBK3XvvvSZU25dixYrJ9ddfL99//725PiEhQfbu3Su1atXyulQAAMIWoRsAgCimIVuDtV6WLFkiOXLkkBtvvNFcFxcXJ6VLlzbbAABA1hC6AQCIYrlz5zbBWi/16tWTgQMHSmJiohw4cOCs7uXLly83v2s4b9iwoeTLl0+uuuoq2bZtm9dPAwAA3yJ0AwAA49ixY/L+++/LZZddZrqaZ+Tpp5+Wl156SdauXWtawe+///5srRMAgHBCfzEAAKLYnDlzpECBAub/jx8/LmXKlDHbYmMzPi8/cuRIad68ufl/bRlv3769nDhxQvLkyZNtdQMAEC5o6QYAIIq1aNHCdB/XyzfffCNt27aVdu3aya5duzL8N3Xq1An8v4Z0tX///mypFwCAcEPoBgAgiuXPn990J9dLo0aN5M033zQt3m+88UaG/yZnzpyB/9cx3io1NTVb6gUAINwQugEAQEiI1q7lf//9t9elAAAQERjTDQBAFDt58qQkJSWZ///zzz9lwoQJZkK1Dh06eF0aAAARgdANAEAUW7BgQWBcdsGCBaVatWoyY8YMufbaa82SYQAA4OLEWJZlXeR9AAAAAACAdDCmGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAcAmhGwAAAAAAlxC6AQAAAABwCaEbAAAAAACXELoBAAAAAHAJoRsAAAAAAJcQugEAAAAAEHf8L6qfN8v/SQz9AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " import matplotlib.pyplot as plt\n", + "\n", + " # Get the latest daily metric for conv_rate\n", + " latest = metrics[0] if metrics else None\n", + " if latest and latest.get(\"histogram\"):\n", + " hist = latest[\"histogram\"]\n", + " bins = hist[\"bins\"]\n", + " counts = hist[\"counts\"]\n", + "\n", + " fig, ax = plt.subplots(figsize=(10, 4))\n", + " ax.bar(\n", + " [f\"{bins[i]:.2f}\" for i in range(len(counts))],\n", + " counts,\n", + " color=\"steelblue\",\n", + " edgecolor=\"white\",\n", + " )\n", + " ax.set_title(f\"conv_rate distribution — {latest['metric_date']}\")\n", + " ax.set_xlabel(\"Bin\")\n", + " ax.set_ylabel(\"Count\")\n", + " plt.xticks(rotation=45)\n", + " plt.tight_layout()\n", + " plt.show() # pragma: allowlist secret\n", + " else:\n", + " print(\"No histogram data available.\")\n", + "except ImportError:\n", + " print(\"Install matplotlib to visualize: pip install matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 8: Time-Series Trend\n", + "\n", + "Plot how a metric (e.g., `mean`) evolves over time." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 data points from 2025-01-01 to 2025-02-28\n", + " 2025-01-01: mean=0.4989, null_rate=0.0000\n", + " 2025-02-28: mean=0.5201, null_rate=0.0000\n", + " ...\n" + ] + } + ], + "source": [ + "timeseries = monitoring.get_timeseries(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_name=\"conv_rate\",\n", + " data_source_type=\"batch\",\n", + " granularity=\"daily\",\n", + " start_date=date(2025, 1, 1),\n", + " end_date=date(2025, 3, 1),\n", + ")\n", + "\n", + "if timeseries:\n", + " dates = [t[\"metric_date\"] for t in timeseries]\n", + " means = [t[\"mean\"] for t in timeseries]\n", + " null_rates = [t[\"null_rate\"] for t in timeseries]\n", + "\n", + " print(f\"{len(timeseries)} data points from {dates[0]} to {dates[-1]}\")\n", + " for t in timeseries[:5]:\n", + " print(f\" {t['metric_date']}: mean={t['mean']:.4f}, null_rate={t['null_rate']:.4f}\")\n", + " print(\" ...\")\n", + "else:\n", + " print(\"No time-series data.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAJOCAYAAABm7rQwAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAepVJREFUeJzt3Ql4lNX59/F7Jvs2gewsYacKiqyCoK0bLRSrolgBrSAiWlusihuogOBC3RFFsfXvVhdwodatvLVoqxZURK2IQmVfsxGSyb7NvNd9JjMkmQEDSSZ5Jt/Pdc01mWfOTM4kD8jz8z73sbndbrcAAAAAAAAAQWQP5jcDAAAAAAAAFKEUAAAAAAAAgo5QCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAJrNnXfeKTabrd6xHj16yOWXXy7txb/+9S/zM9B7AABweIRSAAAATbRmzRoTxhQUFIiVgiPvLTY2Vrp16ybnnnuuPPvss1JRUSFt0RlnnFFv3oe76ecDAABtX3hrTwAAACAUQqkFCxaYaqAOHTqIVTz55JMSHx9vQqi9e/fK//t//0+uuOIKWbx4sbzzzjuSmZl51O95xx13yOzZs1tkvrfffrtceeWVvsfr1q2TJUuWyG233Sb9+vXzHT/ppJNa5PsDAIDmRSgFAADQQElJicTFxUmou+iiiyQlJcX3eN68efLSSy/JlClT5Ne//rV8+umnR/2e4eHh5tYSfv7zn9d7HB0dbUIpPa5VVO399wkAgNWwfA8AADSZVtlMnz5dOnfuLFFRUdKzZ0+55pprpLKy0jdm27ZtJuhISkoyy8VOOeUUeffddwP24nn11Vflnnvuka5du5rg4eyzz5YtW7b4xs2cOdNU+JSWlvrNZfLkyZKRkSE1NTVHtZTtu+++k0suuUQ6duwop512mnnum2++MdVPvXr1MvPQ99VKogMHDtR7/c0332y+1s/tXUK2Y8cO35gXX3xRhg4dKjExMebzT5o0SXbv3i1t0aWXXmqqkT777DN5//33fcc//vhj8/vTZX76O9YqqhtuuEHKysp+tKdUXXoe6POPPPJIwIozfe6VV1455vkf6ffZ2N+FBlwnnniieY8zzzzTnK9dunSR+++/3+/77dmzR8aPH29Cr7S0NPMzaavLHwEAaGuolAIAAE2yb98+GT58uOmndNVVV8nxxx9vQqrXX3/dhEaRkZGSnZ0to0aNMo//8Ic/SHJysjz//PNy3nnnmXEXXHBBvff84x//KHa7XW666SYpLCw0YYCGJRqUqIkTJ8rSpUtNqKVBiZe+/9tvv22CpLCwsKP6HPo+ffv2lXvvvVfcbrc5pqGMhijTpk0zgdTGjRvlT3/6k7nXKiINPy688EL53//+Z4IUDVq8lUepqanmXsO1uXPnysUXX2zCntzcXHnsscfkZz/7mXz11VdtcrnfZZddZj7nP/7xD1910muvvWZ+vho26u/v888/N59DQxl9rrE04Dv11FNNRZYGOHXpsYSEBDn//POb/BkC/T6P5ndx8OBBGTt2rPn96ng9T2+99VYZMGCA/PKXvzRjNJDTwHTXrl3mvNZQ9i9/+Yt88MEHTZ4/AADtghsAAKAJpkyZ4rbb7e5169b5Pedyucz99ddfr6mA++OPP/Y9V1RU5O7Zs6e7R48e7pqaGnPsww8/NOP69evnrqio8I199NFHzfENGzb43rdLly7uCRMm1Pt+r776qhn30UcfNXr+8+fPN6+ZPHmy33OlpaV+x1555RW/7/HAAw+YY9u3b683dseOHe6wsDD3PffcU++4fo7w8HC/48Hi/cy5ubkBnz948KB5/oILLjjiz2LRokVum83m3rlzp99719W9e3f31KlTfY+feuopM+b777/3HausrHSnpKTUG/djXnvtNfM+et782O/zaH4Xp59+unmPF154wXdMz8eMjIx659zixYvNOD3vvEpKStx9+vTxmxcAAPDH8j0AAHDMXC6XvPnmm2bXtmHDhvk9713G9d5775lqqrrLqHT5nVZW6TI3XSZVl1YmaYWV109/+lNzr1VL3vfVShh93+LiYt+4FStWmGVWdb9PY/32t7/1O6ZLvLzKy8slLy/PLDtUX3755Y++58qVK83PSCtt9LXem1ZdaRXPhx9+KG2R/m5UUVFRwJ+F9mjSz6HVb1qFpFVGR0N/HrocUiujvLTJur7nb37zm2b5DA1/n0f7u9CfQd256Pmo57D3HFR6/nXq1Mn05vLSpX56XgMAgB/H8j0AAHDMdPmT0+k0/XeOZOfOnTJixAi/494d0/T5uu+hfYvq0r5A3iVVXrqET3eJe+utt0zvIA2nNCS4+uqrj9jT6HC0H1RD+fn5Zle95cuXS05OTr3ndFnhj/nhhx9MaKOhRyARERGHfa3249Lvfyw0QNF+ScfKG/TpUjovXaKmjdD1513399DYn0VdukxOg8yXX35Z7rrrLnNMAyoNFM866yxpDg1/n0f7u9B+Zg3PIz0Ptc+Yl563ffr08Rt33HHHNcMnAAAg9BFKAQCANudw/aC8vYGUViz16NHDNEXXUEp7SWmPHw2rjkXdSiAvrarR5tvayHzQoEGmekarbbTXkN7/GB2jgcXf//73gJ/JW5EUiH5fbbJ9LE4//XTTNP5Yffvtt+ZeAxelTeO1t5SGZNpXSfuGaWNv7R2m/bsa87NoSHf4015U+jm1T5OGXb/73e9ML7Hm0PD3ebS/i8acgwAAoGkIpQAAwDHTZt4Oh8MXYhxO9+7dZfPmzX7HN23a5Hv+WGho9Oijj5pqLV26pyGVd3ldU2k10OrVq02llFYI1a24aehwlVm9e/c2IYZW7fzkJz85qu8/cODAervfHQ1vZdmx0mbdasyYMeZ+w4YNppm7NqfXMMnrWOenNNjT80crpLSKTpuoa4P1ltKU38Xh6Hmr576+b91zINC5DgAA/BFKAQCAY6ZVLePHj5cXX3xRvvjiC7++Ut6L9XHjxpmldmvXrpWRI0f6+hLpDm8aJPXv3/+Yvr9WRenOfBqWrFq1Sq677jppLt5KmYaVMfo5GtKqIaU7ENalO7fNmTPHBFv6M6obXOj7auWR7mR3uGBp9OjREmy6pO7pp582vyfdWe5wPwv9WgPBYxUeHi6TJ0823+/777831VInnXSStJSm/C4OR89r3aFQd+bz7gKp4Zqe1wAA4McRSgEAgCa59957zYW5LhnTBs/aJ2r//v1madYnn3xi+gfNnj1bXnnlFfnlL38pf/jDH0y/Iw2Stm/fLm+88cYxL9kaMmSIWWJ2++23S0VFxTEv3QtEK8B+9rOfmdCrqqrK9DvSz6lzbmjo0KHmXucxadIk059IeyZpdc7dd99twhBt6K4BnvZp0vf461//an5eN910k7QWDVN02Zr2r9KleNps/D//+Y+p0tLfn5cu19PPonPVcfqz0d9bw95SR0urrpYsWWKajN93333SklridzFjxgx5/PHHzedYv369aXquVWba7BwAAPw4QikAANAkGtZ89tlnMnfuXLMUS5fS6TENoLwX5+np6aZ3kPYjeuyxx8xOdloVo32gzjnnnCZ9fw2i7rnnHhNOaUjVnLSK59prr5WlS5eaappf/OIXpidR586d6407+eSTTcPuZcuWmYot7V+kYYdWUGkgp8vFHnnkEVOlozIzM817nXfeedKarrnmGnOvO+GlpKSYvlnPPPOM6dEVFRXlG6chm/6uNFBctGiRGX/BBRfIzJkzTYB1rDTMO+GEE0yl1KWXXiotrbl/F3p+6xJPPUf0vNbH+jn03NfliQAA4Mhsbro1AgAAoJUMHjzYVM5puAMAANqX5tneBAAAADhK2ofs66+/rtc8HQAAtB9USgEAgJBUXFxsbkeiu795m3gjeHTHOu3B9NBDD0leXp5s27bNLAkEAADtC5VSAAAgJD344IOm8fSRbrt3727tabZL2mB92rRppoG8NsAnkAIAoH2iUgoAAIQkrb7R25GcdtppBCIAAACthFAKAAAAAAAAQcfyPQAAAAAAAARdePC/ZehxuVyyb98+SUhIEJvN1trTAQAAAAAAaDW6KK+oqEg6d+4sdvvh66EIpZqBBlKZmZmtPQ0AAAAAAIA2QzeV6dq162GfJ5RqBloh5f1hOxwOsWq1V25urtka+0gpJgAAAAAAaBmuELk2dzqdpnjHm5ccDqFUM/Au2dNAysqhVHl5uZm/lU98AAAAAACsyhVi1+Y/1uLI+p8QAAAAAAAAlkMoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAACgfYdSS5culR49ekh0dLSMGDFCPv/888OOfe6550zDrLo3fZ1XVVWV3HrrrTJgwACJi4uTzp07y5QpU2Tfvn313ic/P18uvfRS00SsQ4cOMn36dCkuLm7RzwkAAAAAANBQrrNMvt9XZO7bgzYTSq1YsUJmzZol8+fPly+//FIGDhwoY8aMkZycnMO+RoOk/fv3+247d+70PVdaWmreZ+7cueZ+5cqVsnnzZjnvvPPqvYcGUhs3bpT3339f3nnnHfnoo4/kqquuatHPCgAAAAAAUNeqr3bJ1Mf/JX9893/mXh+HOpvb7XZLG6CVUSeffLI8/vjjvm0QMzMz5dprr5XZs2cHrJS6/vrrpaCgoNHfY926dTJ8+HATXnXr1k2+//576d+/vzk+bNgwM2bVqlUybtw42bNnj6muagyn0ymJiYlSWFhogjIr0p+3BoBpaWkhse0kAAAAAABtXWV1jWzLLpKvt+fJsx9urvec3WaTF/5wpqQ6YsRqGpuThEsbUFlZKevXr5c5c+b4jmkwMnr0aFm7du1hX6fL7Lp3724ClSFDhsi9994rJ5xwwmHH6w9Dl/npMj2l761fewMppd9Tv/dnn30mF1xwQcD3qaioMLe6P2yl89CbFem8NZ+06vwBAAAAAGjLyiurZVtOkWzZ75QtWYWyJcspO3OLxXWYWiE9vvdAsSTHR4nVNDZbaBOhVF5entTU1Eh6enq94/p406ZNAV9z3HHHyTPPPCMnnXSSCZsefPBBGTVqlFmK17VrV7/x5eXlpsfU5MmTfSldVlaWqQyqKzw8XJKSksxzh7No0SJZsGCB3/Hc3FzzfaxITxj9OWowRaUUAAAAAADHrqyyRnYeKJUdeaWyM89zv7+wXALlTwnR4dKlQ7Rsyqrf39puE4msKTtiW6O2qqioyDqh1LEYOXKkuXlpINWvXz956qmn5K677qo3VpueX3zxxSZwefLJJ5v8vbWiS/tf1a2U0qWGqampll6+p1Vk+hkIpQAAAAAAaJyisipT+bQ1SyugPFVQe/NLA45Nio+SPhkO6ZOR6Lnv5JCUhGhzPf7/vt4tS977VlxuTyD1h3EnyvG9/IturKDuRnRtPpRKSUmRsLAwyc7OrndcH2dkZDTqPSIiImTw4MGyZcuWgIGU9pH64IMP6oVG+t4NE8fq6mqzI9+Rvm9UVJS5NaRhjpUDHf1DYPXPAAAAAABASykoqfAET/sL5Yf9ugSvULIKAu+Ul5YY4wug+nZKlN4ZDklOOHxY88sh3WVIrxT5bts+6d+rs6R3iBOramyu0CZCqcjISBk6dKisXr1axo8f76vc0cczZ85s1Hvo8r8NGzaYJuUNA6kffvhBPvzwQ0lOTq73Gq200kbp2s9Kv7/S4Eq/tzZeBwAAAAAA7dOBonJP76f9Tl8AlesM3LKnU8dYE0Bp+KQhlAZQHeKOvhdUqiNG+nVOsGRz82PRJkIppcvhpk6dapqO6w55ixcvlpKSEpk2bZp5fsqUKdKlSxfTz0ktXLhQTjnlFOnTp48Jlh544AFTDXXllVf6AqmLLrpIvvzyS3nnnXdMaOXtE6U9ozQI0+V+Y8eOlRkzZsiyZcvMazQEmzRpUqN33gMAAAAAANalrX40bNLQyRM+eSqh8osPbXBWV9ekOOmj4VMnh/Q1AVSiJMREBH3eoaDNhFITJ040jcLnzZtnwqNBgwbJqlWrfM3Pd+3aVa/86+DBgyZM0rEdO3Y0lU5r1qyR/v37m+f37t0rb731lvla36surZo644wzzNcvvfSSCaLOPvts8/4TJkyQJUuWBPGTAwAAAACAYAVQ2QVlJnz6IetQAFVYWuk3Vvs6dU2O91Q/dfIsweuVniBxUQRQzcXm1t8ImkQbnScmJprd66zc6Fz7a+luhPSUAgAAAABYncvtlv35pb6ld9774vJqv7Fhdpt0T02obT5eG0ClJUh0ZHBreVwhcm3e2JykzVRKAQAAAAAAHIsal1v2HCg2VU9a/aQBlO6GV1rpH0BFhNmlR5ongPJWQfVMS5DI8LBWmXt7RigFAAAAAAAso7rGJbvyig9VP+13ytZsp1RU1fiNjQy3S690bwNyz0543dMSTDCF1kcoBQAAAAAA2qTK6hrZmVvsW3qnAdS2bKdU1bj8xkZHhJld77w74Ol9ZkqchFl4GVyoI5QCAAAAAACtTiudtufo0jtnbQBVKDtyiqTa5d8KOzYq/FD/pwzPErwuSRpA2Vpl7jg2hFIAAAAAACCoyiqrTcWTBk/eEEororQ5eUMJMRGm8qluD6hOHWPFbiOAsjpCKQAAAAAA0GJKyqtMzydP/ydPI/LdecXiHz+JJMZGmuDJ1wOqU6KkJ8aIjQAqJBFKAQAAAACAZuEsqzS73nkDqB+yCmVffmnAsckJUb6ld94eUHqMAKr9IJQCAAAAAABHraCkorYBee0yvKxCyS4oCzhWq518PaA6JZqG5Enx0UGfM9oWQikAAAAAAHBEB4rKfQGUdye8PGd5wLHa78lb+dSnk8N8rcvygIYIpQAAAAAAgOF2uyXXWV7bgNwTPmkQlV9c4TdWF9l1SY7zC6DioyNaZe6wHkIpAAAAAADaaQCVVVBWpwG5J4AqLK30G2u3iWSmxNc2IPf0geqd7pDYKGIFHDvOHgAAAAAAQpzL7ZZ9+SX1ekBpCFVcXu03Nsxukx6pCb7KJw2ieqY7JDoirFXmjtBFKAUAAAAAQAipcblld16xr/JJg6itWYVSVlnjNzYizC490zSAql2Cl+GQHmkJEhlOAIWWRygFAAAAAIBFVde4ZGeuJ4Dy9oDaluWUimqX39iocLv0SnfUC6C6pSaYYApoDYRSAAAAAABYQGV1jQmgNHzyBlDbs4ukqsY/gNKldr0zHL4eUHqfmRInYXYCKLQdhFIAAAAAALQxFVU1si3b6VmCt9+zBG9HbpFZmtdQXFS4qX7SyidvAKW74tltuj8e0HYRSgEAAAAA0IrKKqtlqzYf9y7B2++UXXnFpjl5QwkxEfWqnzSIyugYSwAFSyKUAgAAAAAgSErKqzy73/kCqELZc6BE/OMnkQ5xkSZ46puR6KuESkuMERsBFEIEoRQAAAAAAC3AWVrp2/3OsxNeoezLLw04NiUh2tOAPONQI/Kk+CgCKIQ0QikAAAAAAJqooKSiTgNyp6mAyi4sCzg2PTHGV/nkXYrXMT4q6HMGWhuhFAAAAAAAjeR2uyW/2BNAbakTQuUVlQcc3zkp1oROdXtAOWIjgz5voC0ilAIAAAAA4DABVE5hma/y6YfanfAOllT4jdVFdl2T43xL7zSE6p3hkPjoiFaZO2AFhFIAAAAAgHZPA6j9B0vr94DaXyjOsiq/sXabSLeUBE/41MnhC6BiIrnEBo4Gf2IAAAAAAO2Ky+2WvQdK6jQg91RClVRU+40Ns9ukR2r9AKpnukOiI8JaZe5AKCGUAgAAAACErBqXS3bnHQqg9H5btlPKKmv8xkaE2aVneoKv/5PeuqfGS2Q4ARTQEgilAAAAAAAhoarGJbtyiw4twasNoCqqXX5jo8Lt0ivDU/nk7QGlAVR4mL1V5g60R4RSAAAAAADLqayukR05Rb7d7zSA2p5TZIKphmIiw6R3nd3vNIDKTIk3S/MAtB5CKQAAAABAm1ZeVSPbs+s2IHfKjtwiqXG5/cbGR4eb0KlPbQClQVTnpDix2wiggLaGUAoAAAAA0GaUVlTL1mxP5ZO3B9TuvGIJkD+JIybCt/TO04g8UTI6xIiNAAqwBEIpAAAAAECrKC6v8lU+eaugdFe8APmTdIyLkr61u995A6hURzQBFGBhhFIAAAAAgBbnLK2UH0wApdVPThNA7T9YGnBsiiO6TgNyzxK85ITooM8ZQMsilAIAAAAANKuDxRW+pXcmhMpySk5hWcCx6R1ipG+DHlAd4qKCPmcAwUcoBQAAAAA4Jm63Ww4UVdRpQK4BVKE5FkiXpDjP7nedPFVQvTMc4oiJDPq8AbQNhFIAAAAAgEYFUNmFZbUNyA/1gCooqfQbq12eMlPifZVPGkL1TndIXHREq8wdQNtEKAUAAAAAqMfldpt+T57+T54QSgOoorIqv7F2m026p2oApdVPniqoXukOiYnkchPAkbWpvyWWLl0qDzzwgGRlZcnAgQPlsccek+HDhwcc+9xzz8m0adPqHYuKipLy8nLf45UrV8qyZctk/fr1kp+fL1999ZUMGjSo3mvOOOMM+fe//13v2NVXX21eBwAAAAChrsbllr35Jb6ld3q/NcspJRXVfmPD7TbpkZZQ2//JE0L1THNIVERYq8wdgLW1mVBqxYoVMmvWLBMGjRgxQhYvXixjxoyRzZs3S1paWsDXOBwO87xXw61AS0pK5LTTTpOLL75YZsyYcdjvrc8tXLjQ9zg2NrZZPhMAAAAAtCU1Lpfsyi32VT79UBtAlVfV+I2NCLObiqc+Wv1UuxOeVkRFhhNAAQixUOrhhx824ZC3+knDqXfffVeeeeYZmT17dsDXaAiVkZFx2Pe87LLLzP2OHTuO+L01hDrS+wAAAACA1VTVuGRnTtGhXfCynLIt2ymV1S6/sVrp1Ls2gDI9oDISpVtKvISH2Vtl7gDahzYRSlVWVpoldnPmzPEds9vtMnr0aFm7du1hX1dcXCzdu3cXl8slQ4YMkXvvvVdOOOGEo/7+L730krz44osmmDr33HNl7ty5VEsBAAAAsIzK6hrZrgFUnR5QO3KKTDDVUGxkuNn1zhM+eXpAdU2OlzB7/ZUnANAuQqm8vDypqamR9PT0esf18aZNmwK+5rjjjjNVVCeddJIUFhbKgw8+KKNGjZKNGzdK165dG/29L7nkEhNsde7cWb755hu59dZbzZJA7Ud1OBUVFebm5XQ6zb2GY3qzIp237qZh1fkDAAAA7YUutduerbvfeZbg6fK7nXnFpjdUQ/HR4abqScMnbxDVqWOsaU5en14L+L8eQHC5QuTavLHzbxOh1LEYOXKkuXlpINWvXz956qmn5K677mr0+1x11VW+rwcMGCCdOnWSs88+W7Zu3Sq9e/cO+JpFixbJggUL/I7n5ubWa7RutRNGwz09+bVKDQAAAEDrK6uskZ0HSmVnXqnsqL3tLywXd4D8KCE6XHqkxEr3lFjpkRxrvk5JiKzfe7e6RPJyS4L6GQC0v2vzoqIi64RSKSkpEhYWJtnZ2fWO6+PG9nqKiIiQwYMHy5YtW5o0F22yrvR9DhdK6TJDbcpet1IqMzNTUlNTTfN1q574+h8r/QxWPvEBAAAAqyour/I1IN9iqqCcsi+/RALVLyXFRx2qftJKqE4OSUmI9tv8CYC1uELk2jw6Oto6oVRkZKQMHTpUVq9eLePHj/f9IvTxzJkzG/Ueuvxvw4YNMm7cuCbN5euvvzb3WjF1OFFRUebWkJ4wVj5p9MS3+mcAAAAArKCwtNL0f6rbhHz/wdKAY1Md0b7d77w74SUnNO6CD4D12ELg2ryxc28ToZTSyqOpU6fKsGHDZPjw4bJ48WIpKSnx7cY3ZcoU6dKli1k6pxYuXCinnHKK9OnTRwoKCuSBBx6QnTt3ypVXXul7z/z8fNm1a5fs27fPPNZeUUqrr/SmS/RefvllE2QlJyebnlI33HCD/OxnPzO9qgAAAACgqfKLy03lkyd88gRQOYVlAcdmdIjx7X6nDci1GqpDnP//EAeAUNBmQqmJEyeankzz5s2TrKwsGTRokKxatcrX/FzDpbpJ28GDB2XGjBlmbMeOHU2l1Zo1a6R///6+MW+99ZYv1FKTJk0y9/Pnz5c777zTVGj985//9AVgugRvwoQJcscddwT1swMAAACwPu0Bk1dU7gmfapuQ69f5xYc2SaqrS1JcvR3wNIhKiIkI+rwBoLXY3Po3J5pEe0olJiaaZmRW7imVk5MjaWlpli4RBAAAAIJBL6OyC8rkB9P/qVB+0F5Q+wvNsryG7DaRrsnxvgBK73tlOCQuigAKQGhemzc2J2kzlVIAAAAA0Ba53G7Zn19aJ4DyVEJpY/KG7DabdE+NN5VP3hCqd7pDoiO59AKAhvibEQAAAABq1bjcsvdAsa/5uLcHVGlFtd/YcLtNeqQl1DYg9yy/65WeIJHhYa0ydwCwGkIpAAAAAO1Sjcslu3KLfZVPGkBtzXJKeVWN39jIcLv0Snf4+j/1zUiU7mkJEhFm3eU1ANDaCKUAAAAAhLyqGpfsyCnyVD7pErz9Ttme45TKapff2KiIME/4lHFoCV5mSryEE0ABQLMilAIAAAAQUiqqamR7bQDl2Qmv0ARS1S7/PZ5iI8OlT6f6AVSX5HgJ0+7kAIAWRSgFAAAAwLLKK6tla7az3g54O3OLTXPyhuKjI0wApUvvvEvwOiXFmubkAIDgI5QCAAAAYAklFVWyLcvpa0Ku93sOaADlPzYxNtJX+eTdCS89MUZsBFAA0GYQSgEAAABoc4rKqnz9n7wB1N78koBjkxOizPI73xK8Tg5JSYgmgAKANo5QCgAAAECrKiipMMGTpwG5hlCFklVQFnBsWmKMqX7yVEF5Aqik+OigzxkA0HSEUgAAAACC5kBReW0Dck8IpV/nOssDju3UMbZeANU7wyEd4qKCPmcAQMsglAIAAADQ7NxutwmbPA3IPUvw9Ov84oqA47smxZneT95G5L0zEiUhJiLo8wYABA+hFAAAAIAmB1C63M4XQNX2gSosrfQba7eJZKbE1y698/SA6pWeIHFRBFAA0N4QSgEAAABoNJfbLfvyS2SLLr8zy/A8S/CKy6v9xobZbdI9NeHQEjwNoNISJDqSyxAAAKEUAAAAgMOocbllz4Hi2gooz/K7rVlOKa30D6AiwuzSIy2htv+TwwRQPdMSJDI8rFXmDgBo+wilAAAAAEh1jUt25RX7Kp+0EmprtlMqqmr8xkaG26V3uid48lZBdUtNMMEUAACNRSgFAAAAtDOV1TWyM/dQAKX327OLpKrG5Tc2OiLM7Hrn3QFP7zNT4iTMTgAFAGgaQikAAAAghGml0/Ycp/ygPaBqQ6gdOUVS7XL7jY2NCvctvdMd8PS+S5IGULZWmTsAILQRSgEAAAAhoqyyWrZle8InE0JlFZqKKG1O3lBCTISv8skbRHXqGCt2GwEUACA4CKUAAAAACyoprzI9n8wSPFMB5ZTdecXiHz+JdIiL9C2/8/aASkuMERsBFACgFRFKAQAAAG2cs6zSNB73NCDXnfAKZV9+acCxyQlRvqV33kooPUYABQBoawilAAAAgDakoKSitgF5bRVUVqFkF5QFHJueGHOoB1SnRNOQPCk+OuhzBgDgWBBKAQAAAK3kQFG5b/ndD1meSqg8Z3nAsdrvydcDqpPDfJ0YGxn0OQMA0FwIpQAAAIAW5na7JddZXtuA3FP9pJVQ+cUVfmN1kV2X5LhDPaBqA6j46IhWmTsAAC2FUAoAAABo5gAqq6CsTgNyTwBVWFrpN9ZuE+mWkuALnjSI6pXukNgo/pkOAAh9/NcOAAAAOEYut1v2HijxBU8aRG3NKpTi8mq/sWF2m/RI9QRQ3iqonukOiY4Ia5W5AwDQ2gilAAAAgEaocblkd54ngPI2ItcAqqyyxm9sRJhdeqZpAFXbAyrDIT3SEiQynAAKAAAvQikAAACggeoal+zMLa4TQBXKtiynVFS7/MZGhdvNkru6AVT31AQJD7O3ytwBALAKQikAAAC0a5XVNSaA0vDJ2wdqe06RVNX4B1AxkWHSW5uPZxzqAZWZEidhdgIoAACOFqEUAAAA2o2KqhrZlu309IDa7+kBtSO3SGpcbr+xcVHhpvpJAyhvDyjdFc9u0/3xAABAq4dSNTU18txzz8nq1aslJydHXK76/0fpgw8+aOq3AAAAAI5aWWW1bM3yBFCeCiin7MorNs3JG0qIiTDBU1+tgqoNojp1jBUbARQAAG03lLruuutMKHXOOefIiSeeyH+4AQAAEHQl5VW+3e88VVCFsudAifjHTyId4iL9Aqi0xBj+HQsAgNVCqeXLl8urr74q48aNa54ZAQAAAEfgLK2UH2qX33mroPYfLA04NiUh2tOAPONQI/Kk+CgCKAAAQiGUioyMlD59+jTPbAAAAIA6CkoqDjUg16V4+wslu7As4Nj0DjGm71PdHlAd46OCPmcAABCkUOrGG2+URx99VB5//HH+jxMAAACOidvtlgNFFb6ld94QKq+oPOD4zkmxvt3vvEGUIzYy6PMGAACtGEp98skn8uGHH8rf//53OeGEEyQiIqLe8ytXrmzqtwAAAECIBVA5hWUNekA55WBJhd9Y/V+eXZPjPOFTnQAqLrr+vzkBAEA7DKU6dOggF1xwQbNMZunSpfLAAw9IVlaWDBw4UB577DEZPnx4wLHaXH3atGn1jkVFRUl5eXm9QGzZsmWyfv16yc/Pl6+++koGDRpU7zU6Xqu9tDdWRUWFjBkzRp544glJT09vls8EAADQ3gMo7ffkW35XWwnlLKvyG2u32aR7arwneOrkWYLXK90hMZFN/icrAABog5r8X/hnn322WSayYsUKmTVrlgmRRowYIYsXLzYB0ebNmyUtLS3gaxwOh3neq+HywZKSEjnttNPk4osvlhkzZgR8jxtuuEHeffddee211yQxMVFmzpwpF154ofznP/9pls8FAADQXrjcbrPjnYZO3gbkW7OcUlJR7Tc23G6THmkJ9QKoHmkOiY4Ia5W5AwCA4Gsz/9vp4YcfNsGRt/pJwykNi5555hmZPXt2wNdoCJWRkXHY97zsssvM/Y4dOwI+X1hYKP/3f/8nL7/8spx11lm+kK1fv37y6aefyimnnNIMnwwAACD01LhcsjuvxLf8Tu+3ZTulrLLGb2xEmF16pnsCKA2f9KYVUZHhBFAAALRnzRJKvf766/Lqq6/Krl27pLKyst5zX3755Y++Xl+jS+zmzJnjO2a322X06NGydu3aw76uuLhYunfvLi6XS4YMGSL33nuv6WvVWPo9q6qqzPfxOv7446Vbt27m+xJKAQAAiFTVuGRXbtGhHlC1AVRFtctvbFS4XXrV2f1ObxpAhYfZW2XuAAAghEOpJUuWyO233y6XX365/O1vfzOVTlu3bpV169bJ73//+0a9R15entTU1Pj1cdLHmzZtCvia4447zlRRnXTSSabi6cEHH5RRo0bJxo0bpWvXro36vtq7KjIy0vTFavh99bnD0d5TevNyOp3mXsMxvVmRzlt7Plh1/gAAoHlUVtfIjpxiT+8n0wPKKdtznFJd4/YbGxMZJr3THbUNyB3m1jU5XsLs/jsy828MAADaz7W5q5Hzb3IopU3B//SnP8nkyZNN8/FbbrlFevXqJfPmzTPNxVvKyJEjzc1LAylddvfUU0/JXXfdJS1p0aJFsmDBAr/jubm59RqtW+2E0XBPT36tUgMAAKFPK512HyiVHXmlsjPPc7/3YJkEyJ8kNjJMeqTESne9JcdKz5RYSUuMMs3JfdxlciCvLKifAQCAUOIKkWvzoqKi4IRSumRPAyEVExPj+8baz0mXvz3++OM/+h4pKSkSFhYm2dnZ9Y7r4yP1jKorIiJCBg8eLFu2bGn03PW9delgQUFBvWqpH/u+usxQm7LXrZTKzMyU1NRU03zdqie+9ujSz2DlEx8AAARWWlFtltz5dsDLcsruvGJxBQigHDER9aqfdAleRocYv01lAABA83KFyLV5dHR0cEIpDW+0Ikp7O2kvJm0QPnDgQNm+fbtJ9hpDl9ANHTpUVq9eLePHj/f9IvSx7obXGLr8b8OGDTJu3LhGz12/p4ZZ+n0mTJhgjulufhq01a3CaigqKsrcGtITxsonjZ74Vv8MAABApLi8yhM87a/tAZVVKHsPlEigf5l1jIuSvp08S/D6mp3wEiXVEU0ABQBAK7GFwLV5Y+fe5FBKd6176623TJWS9pO64YYbTOPzL774Qi688MJGv49WHk2dOlWGDRsmw4cPl8WLF0tJSYlvN74pU6ZIly5dzNI5tXDhQlOJ1adPH1Pp9MADD8jOnTvlyiuv9L2nhmUaMO3bt88XOHmDNL0lJibK9OnTzfdOSkoyVU7XXnutCaRocg4AAKygsLSyNoDSHfA8VVD7D5YGHJviiPYFT31qm5EnJzTu/2QCAAA0tyaHUtpPytvAShubJycny5o1a+S8886Tq6++utHvM3HiRNOTSXtRaZPxQYMGyapVq3zNzzVcqpu0HTx4UGbMmGHGduzY0VQ96fft37+/b4yGZd5QS02aNMncz58/X+68807z9SOPPGLeVyultHn5mDFjTJ8sAACAtuZgcYWv8smEUFlOySkM3MNJl9uZ3e+0Aqo2hOoQ51/pDQAA0Fps7sauscNhaU8prbrSZmRW7imVk5MjaWlpli4RBAAgFOg/z/KKys3yOw2gvEHUgaJDu//W1SUpzlf5pCFU7wyHOGIigz5vAADQNK4QuTZvbE7S5Eop9fHHH5td77Zu3WqW7ukyu7/85S/Ss2dPOe2005rjWwAAAIRsAJVdWFa7/M7TgFwDqIKSSr+x2uUpMyXeL4CKi4polbkDAAA0RZNDqTfeeMPstHfppZfKV199ZZbAKU3D7r33Xnnvvfea+i0AAABCgsvtNv2eGgZQRWVVfmPtNpt0T42vbUDuaUTeK90hMZHN8v8UAQAAWl2T/1Vz9913y7Jly0wj8uXLl/uOn3rqqeY5AACA9qjG5Za9+SW1vZ88PaA0hCqtqPYbG263SY+0hNoG5J4eUD3TEiQqIqxV5g4AAGCJUEp3tPvZz37md1zXDuqueAAAAKGuxuWSXbnFvsonrYLamuWU8qoav7ERYXZT8dSnU+0SvIxEUxEVGU4ABQAA2pcmh1IZGRmyZcsW6dGjR73jn3zyifTq1aupbw8AANCmVNW4ZGdOUZ0G5E7Zlu2UymrPbsR1aaVT73Rv/yeHCaC6pcRLeJh1G5cCAAC0mVBqxowZct1118kzzzwjNptN9u3bJ2vXrpWbbrpJ5s6d2zyzBAAAaAWV1TWyPafIEz7VBlA7copMMNVQbGS4aTruqX7y9IDqmhwvYXZtTw4AAIBmD6Vmz55ttiw8++yzpbS01Czli4qKMqHUtdde29S3BwAACIryymrZ1iCA2plbZHpDNRQfHeFZfpdxqAdUp6RY05wcAAAAjWNz6z7EzaCystIs4ysuLpb+/ftLfHy8tBdOp9P00NIdBx0Oh1iRBos5OTmSlpYmdjtLCgAAoU2bjW/V5XfaA6p2J7w9B4olQP4kibGRtQ3IPVVQGkSld4gxFeIAAADNyRUi1+aNzUmOuVLqiiuuaNQ4XdYHAADQWorKqmoDKK2A8oRQe/JLAo5Nio8yAZSpgKrtAZXqiCaAAgAAaAHHHEo999xz0r17dxk8eLA0U7EVAABAkxSWVvoqn7QRuS7B23+wNOBYDZu8u995A6jkhOigzxkAAKC9OuZQ6pprrpFXXnlFtm/fLtOmTZPf/OY3kpSU1LyzAwAAOIz84vLa/k9O3054uc7ygGM7dYz1NB+v7f+kDck7xEUFfc4AAABopp5SFRUVsnLlSrNEb82aNXLOOefI9OnT5Re/+EW7KnOnpxQAAC1H/6miYZOpfNrvrF2GVyj5xRUBx3dNiqvXA6p3RqIkxEQEfd4AAADt9drc2dI9pZTusjd58mRz27lzp1nS97vf/U6qq6tl48aN7arZOQAAaJ4AKrugzARPniV4nh5QuiyvIbtNpGtyvGcJnukD5ZBeGQ6JiyKAAgAAsIImhVJ1aYKn1VH6j8mamprmelsAABCiXG637M8v9VU+eRuRF5dX+Y2122zSPfVQAKVVUL3THRId2Wz/lAEAAECQNelfcnWX733yySfyq1/9Sh5//HEZO3aspcvMAABA86pxuWXvgeJD1U+1TchLK6r9xobbbdIjLeFQBVSnROmZliCR4WGtMncAAAC0sVBKl+ktX75cMjMz5YorrjBNz1NSUpp3dgAAwHJqXC7ZmVt8qAfU/kLZmu2Uiir/SurIcLv0StcG5I7aJXiJ0j0tQSLC+J9bAAAAoe6YQ6lly5ZJt27dpFevXvLvf//b3ALRSioAABCaqmpcsiOnqDaA0j5QTtme45TKapff2OiIMLPrnXcHPA2iuqXGSxjV1QAAAO3SMYdSU6ZMaVc77AEA0N5ppdP22gDKLMPbX2gCqWqX/0a+sVHh9aqf9OsuyRpA8W8HAAAANDGU0p32AABAaCqvrDZL7jwNyD33uiRPm5M3FB8d4at88vaA6tQx1jQnBwAAAA6HLWsAAGjnSiqqZGtt8KTNx7UKandesfjHTyKJsZG+AMrbiDw9MYbqaQAAABw1QikAANoRZ1mlL4Dy7oS3N78k4NjkhKg6/Z80gHJISkI0ARQAAACaBaEUAAAhqqCkwlf55KmCKpSsgrKAY9MSYw5VP9UGUEnx0UGfMwAAANoPQikAAELAgaLy2gbk3j5QhZLnLA84Vvs9eSqgPDvh6RI8XZYHAAAABBOhFAAAFuJ2uyXXWe4LnrQSSr/OL67wG6uL7Lokx/kqn7QKqnd6oiTERLTK3AEAAIC6CKUAAGjDAZQut/MFULU9oApLK/3G2m0imSnxh3pAmQDKIbFR/KceAAAAbRP/UgUAoA1wud2yL79Etux31gmgCqW4vNpvbJjdJt1TE+otv+uV7pDoiLBWmTsAAABwLAilAAAIshqXW/YcKPbtfqcBlO6IV1rpH0BFhNmlR1pCbQNyhwmgeqYlSGQ4ARQAAACsjVAKAIAWVF3jkl153gBKK6CcsjXbKRVVNX5jI8PtZsmdBk/enfC6pSaYYAoAAAAINYRSAAA0k8rqGtmZeyiA0vvt2UVSVePyG6tL7XrXBk/ePlCZKXESZieAAgAAQPtAKAUAwDHQSqftOU75Yb9n+Z2GUDtyiqTa5fYbq83GvZVP3h5QXZI0gNL98QAAAID2iVAKAIAfUVZZLduyNYDyLL/TAEororQ5eUMJMRGHwqfaICqjY6zYbQRQAAAAQF2EUgAA1FFSXuVpPl67A54GUXsOlIh//CTSIS6y3vI7DaHSEmPERgAFAAAA/ChCKQBAu+Usq/RVPnn7QO3LLw04NiUh2rf7nTeISk6IIoACAAAAjhGhFACgXSgoqagNnmqX4WUVSnZBWcCx6YkxfgFUx/iooM8ZAAAACGWEUgCAkOJ2uyW/uDaA0uV3tUvx8pzlAcd36hhbpwG5Q/pmJIojNjLo8wYAAADaG0IpAIClA6hcZ7kvgPIsw3PKwZIKv7G6yK5Lcly9HlC9MxwSHx3RKnMHAAAA2rs2FUotXbpUHnjgAcnKypKBAwfKY489JsOHDw849rnnnpNp06bVOxYVFSXl5eX1Llbmz58vf/7zn6WgoEBOPfVUefLJJ6Vv376+MT169JCdO3fWe59FixbJ7Nmzm/3zAQCOnf6dvv9gqacJuamA8gRRzrIqv7F2m0i3lART+eQNoHqlOyQ2qk39Zw8AAABo19rMv85XrFghs2bNkmXLlsmIESNk8eLFMmbMGNm8ebOkpaUFfI3D4TDPezVsNnv//ffLkiVL5Pnnn5eePXvK3LlzzXt+9913Eh0d7Ru3cOFCmTFjhu9xQkJCi3xGAEDjuNxu2XugxLMDnrcH1P5CKamo9hsbZrdJj1RPAOWtguqZ7pDoiLBWmTsAAAAAi4VSDz/8sAmGvNVPGk69++678swzzxy2aklDqIyMjMP+H3UNtu644w45//zzzbEXXnhB0tPT5c0335RJkybVC6EO9z4AgJZV43LJ7jxPAOVtRL41q1DKKmv8xkaE2aVnWkKdBuQO6ZGWIJHhBFAAAACA1bSJUKqyslLWr18vc+bM8R2z2+0yevRoWbt27WFfV1xcLN27dxeXyyVDhgyRe++9V0444QTz3Pbt280yQH0Pr8TERFOFpe9ZN5T64x//KHfddZd069ZNLrnkErnhhhskPPzwP5qKigpz83I6neZe56E3K9J5a5Bn1fkDsIbqGpfsyis24dNW04DcKduynVJR7f93T1S43Sy50+BJez9pCNUtJV7Cw+x+Y/m7CwAAAKHAFSLX5o2df5sIpfLy8qSmpsZUMdWljzdt2hTwNccdd5ypojrppJOksLBQHnzwQRk1apRs3LhRunbtagIp73s0fE/vc+oPf/iDCbSSkpJkzZo1Jhjbv3+/qdw6HO05tWDBAr/jubm59XpaWe2E0Z+jnvwaCAJAU1XVuGRPfpnsyCuVnXml5n53fplUu9x+Y6Mj7NItOVZ6pHhu3ZNjpVOHaLM075ByyT9gzb9jAQAAgPZ0bV5UVGSdUOpYjBw50ty8NJDq16+fPPXUU6bqqbG0j5WXBlyRkZFy9dVXm+BJG6cHosFV3ddppVRmZqakpqaaPldWPfF1OaR+Biuf+ABaR3lVjWzPccrW/U75wVRAFcrO3GKpCRBAxUWFm+onXYJn7jMc0jkpTuwN+gICAAAA7Y0rRK7N6/bxbvOhVEpKioSFhUl2dna94/q4sb2eIiIiZPDgwbJlyxbz2Ps6fY9OnTrVe89BgwYd9n10eV91dbXs2LHDVGMFomFVoMBKTxgrnzR64lv9MwBoeWWV1Wbpnaf/kzYgd5oledqcvCFHTISn/1NGoi+E6tQx1m9jCgAAAAChc23e2Lm3iVBKq5OGDh0qq1evlvHjx/vSQX08c+bMRr2HLv/bsGGDjBs3zjzW3fY0mNL38IZQWtH02WefyTXXXHPY9/n666/ND+9wO/4BQHtSUl51aPe72kbkuiuef/wk0jEuSvp20sonTwClPaBSHdEEUAAAAADabiildDnc1KlTZdiwYTJ8+HCzc15JSYlvN74pU6ZIly5dzLI6tXDhQjnllFOkT58+UlBQIA888IDs3LlTrrzySvO8XgRdf/31cvfdd0vfvn1NSDV37lzp3LmzL/jShucaUp155plmBz59rE3Of/Ob30jHjh1b8acBAMHnLK2UH2orn7wB1P6DpQHHpjiiTfjUt3YZngZQSfFRBFAAAAAArBdKTZw40TQKnzdvnmlErtVNq1at8jUq37VrV73yr4MHD8qMGTPMWA2QtNJKG5X379/fN+aWW24xwdZVV11lgqvTTjvNvKd3baMuwVu+fLnceeedZjc9Da40lKrbLwoAQtHB4gpf8LTFVEE5JbuwLODY9A4xngDK1wMqUTrGB+65BwAAAACNZXNrS3c0iS4LTExMNB3yrdzoPCcnxyxbtPK6VQD16V/xB4r8A6i8osC72HVOiq0TQOkyPIc4YiKDPm8AAACgPXKFyLV5Y3OSNlMpBQBoegCVU1hW2//p0BK8gpJKv7G6yK5rcpwnfPIGUBkOiYuOaJW5AwAAAGh/CKUAwKIBlPZ7qhtAaRWUs6zKb6zdZpPuqfG+yicNonqlOyQmkv8EAAAAAGg9XJEAQBvncrtlz4GS2qV3nuqnrVlOKamo9hsbbrdJj7SEOjvgOaRnmkOiIsJaZe4AAAAAcDiEUgDQhtS4XLI7r6S2AsoTQG3LdkpZZY3f2Igwu/RMT/D1f9J7rYiKDCeAAgAAAND2EUoBQCupqnHJrtyiQ0vwagOoimqX31itdOqd7jDL77wBVLeUeAkPs27zQwAAAADtG6EUAARBZXWNbM8p8u1+p0HUjpwiE0w1FBsZLr0zNIDyNB/XAKprcryE2bU9OQAAAACEBkIpAGhm5VU1puJJAyhvFdTO3CKpcbn9xsZHhx/q/1TbiLxzUpxpTg4AAAAAoYxQCgCaoLSiWrbWC6AKZXdesQTIn8QRE+Hp/+QLoBIlo0OM2AigAAAAALRDhFIA0EjF5VW+5uNb9jvN13sPlEiA/EmS4qMOLb+rDaBSHdEEUAAAAABQi1AKAAIoLK2s7f+kIZQngNp/sDTg2BRHtC946lvbiDw5ITrocwYAAAAAKyGUAtDu5ReX+yqfvD2gcgrLAo7V5Xa+HlC1lVAd4qKCPmcAAAAAsDpCKQDthtvtlryihgFUoRwoqgg4vktSnG/3Ow2hdEc8R0xk0OcNAAAAAKGIUApAyAZQ2YVl9XbA0wCqoKTSb6x2ecpMifdVPnkDqLioiFaZOwAAAAC0B4RSACzP5Xabfk+eBuSHAqiisiq/sXabTbqnxtfugOcJoHqlOyQmkr8OAQAAACCYuAoDYCk1LrfsPVBsgqcfsg6FUKUV1X5jw+026ZGWUKf/U6L0TEuQqIiwVpk7AAAAAOAQQikAbVaNyyW7cmsDqNr+T1uznFJeVeM3NiLMbiqezO53tQGUBlJ6HAAAAADQ9hBKAWgTqmpcsjOnqF7107Zsp1RWu/zGaqVT73RvA3KHCaC6pcRLOAEUAAAAAFgGoRSAoKusrpHtGkDV9oDS+x05RVLtcvuNjY0M9wVP3p3wuiTHS5hd25MDAAAAAKyKUApAiyqvrJat2dp43OkLoHbmFpvm5A3FR0eYAKqvBlCmEXmidEqKNc3JAQAAAAChhVAKQLMpqaiSbaYB+aEAas8BDaD8xybGRtbbAU8DqPQOMWIjgAIAAACAdoFQCsAxKSqrkq1ZnuDJWwW1J78k4Nik+Chf8ORdipfqiCaAAgAAAIB2jFAKwI8qLK309X/SHfD066yCsoBjNWwyDcjrBFDJCdFBnzMAAAAAoG0jlAJQT35xeW0A5fQFULnO8oBjO3WMNc3HNXjSIKp3hkM6xEUFfc4AAAAAAOshlALaKbfbbcImDZ40gPrB3BdKfnFFwPFdk+LMEjxvI/LeGYmSEBMR9HkDAAAAAEIDoRTQTgKo7IIyEzzV7QGly/IasttEuibHe5bg1TYi75XhkLgoAigAAAAAQPMhlAJCjMvtlv35pbXhU2FtBZRTisur/MaG2W3SPTXBswRPA6hOidIrLUGiI/mrAQAAAADQsrjyBCysxuWWPQeKaxuQO00QtTXLKaWV1X5jI8Ls0iOtfgDVMy1BIsPDWmXuAAAAAID2jVAKsIjqGpfsyiv2NR/X6qet2U6pqKrxGxsZbpde6dqA3OHbCa97WoIJpgAAAAAAaAsIpYA2qLK6Rnbm1g+gtuc4pbLa5Tc2OiLM7HrnDZ80iOqWGi9hdgIoAAAAAEDbRSgFtDKtdNqeU+TrAaVL8XbkFEm1y+03NjYq/NDyOw2gOiVKl6Q40xsKAAAAAAArIZQCgqi8stosudPg6YfaHfC0IkqbkzcUHx1RW/1UWwXVKVE6dYwVu40ACgAAAABgfYRSQAspqagyTcdNAFXbiHx3XrH4x08iibGRfgFUemKM2AigAAAAAAAhilAKaAbOskq/AGpvfknAsckJUab3k68HVCeHpCREE0ABAAAAANoVQingKBWUVJjQydOA3NMHKqugLODYtMQY6VvbA8obQCXFRwd9zgAAAAAAtDWEUsARHCgq91U+efpAFUqeszzgWO335KmAOhRC6bI8AAAAAADQxkOppUuXygMPPCBZWVkycOBAeeyxx2T48OEBxz733HMybdq0eseioqKkvPxQYOB2u2X+/Pny5z//WQoKCuTUU0+VJ598Uvr27esbk5+fL9dee628/fbbYrfbZcKECfLoo49KfHx8C35StDV6ruQ6y33Bk6cCyin5xRV+Y3WRXZfkuENL8Do5pHd6oiTERLTK3AEAAAAAsKI2E0qtWLFCZs2aJcuWLZMRI0bI4sWLZcyYMbJ582ZJS0sL+BqHw2Ge92rYk+f++++XJUuWyPPPPy89e/aUuXPnmvf87rvvJDras4Tq0ksvlf3798v7778vVVVVJui66qqr5OWXX27hT4zWDKB0ud2h/k+eAKqwtNJvrN0mkpkSX6f/U6L0TndIbFSb+aMDAAAAAIAl2dx6hd4GaBB18skny+OPP24eu1wuyczMNFVMs2fPDlgpdf3115sKqED0Y3Xu3FluvPFGuemmm8yxwsJCSU9PN6+dNGmSfP/999K/f39Zt26dDBs2zIxZtWqVjBs3Tvbs2WNe3xhOp1MSExPN+2tQZkX6887JyTEBoFaMhQqX2y378kvqLcHTEKq4vNpvbJjdJt1TEzzL72oDqF7pDomOCGuVuQMAAAAA2hdXiFybNzYnaRPlHpWVlbJ+/XqZM2eO75j+8EePHi1r16497OuKi4ule/fu5pc2ZMgQuffee+WEE04wz23fvt0sA9T38NIfiIZf+p4aSul9hw4dfIGU0vH6vT/77DO54IILAn7fiooKc6v7w1Y6D71Zkc5bgzyrzl/VuNyy50CxJ3yqDaC2ZjulrLLGb2x4mE16pmn45L0lSo+0eIkM9w+grPwzAQAAAABYhysErs1VY+ffJkKpvLw8qampMVVMdenjTZs2BXzNcccdJ88884ycdNJJJnl78MEHZdSoUbJx40bp2rWrCaS879HwPb3P6X3DpYHh4eGSlJTkGxPIokWLZMGCBX7Hc3Nz6/W0stoJoz9HPfmtkMZWu9yy72CZ7MgrlZ15peZ+V36ZVFb7n/iRYTbJTI6VHimeW/eUWOnSIVrCw+p+zgopyPfvHwUAAAAAQLC4LHZtfjhFRUXWCaWOxciRI83NSwOpfv36yVNPPSV33XVXi35vrejS/ld1K6V0qWFqaqqll+9pTy79DG3txK+srpGdubUVULXL8LbnFElVjX8ApUvtetepftL7zJQ4CWtjnwkAAAAAACtdmx8Nbx9vS4RSKSkpEhYWJtnZ2fWO6+OMjIxGvUdERIQMHjxYtmzZYh57X6fv0alTp3rvOWjQIN8YXatZV3V1tdmR70jfV3f501tDesJY+aTRE7+1P0NFVY1sz3F6ekDt12V4hbIjp8hURjWkzcY1dKrbhLxLkgZQ9RveAwAAAABgFbY2cG3eVI2de5sIpSIjI2Xo0KGyevVqGT9+vC8d1MczZ85s1Hvo8r8NGzaYJuVKd9vTYEnfwxtCaUWT9oq65pprzGOttNJG6drPSr+/+uCDD8z31t5TaFllldWy1fR/8gRQGkTtyis2zckbSoiJOBQ+1QZRGR1jxd5gx0UAAAAAAGANbSKUUrocburUqabp+PDhw2Xx4sVSUlIi06ZNM89PmTJFunTpYvo5qYULF8opp5wiffr0McHSAw88IDt37pQrr7zSlyzq7nx333239O3b14RUc+fONTvqeYMvXe43duxYmTFjhixbtkyqqqpMCKZN0Bu78x4ap6S8qrYBuQZQhSaA2nOgRAJt/dghLtIXQHnuHZKWGGN+pwAAAAAAIDS0mVBq4sSJplH4vHnzTJNxrW5atWqVr1H5rl276pV/HTx40IRJOrZjx46m0mnNmjXSv39/35hbbrnFBFtXXXWVCa5OO+0085511za+9NJLJog6++yzzftPmDBBlixZEuRPH1qcpZW+AMosw8sqlH35pQHHpiREH1qCVxtEJSdEEUABAAAAABDibG5t6Y4m0WWBiYmJpkO+lRuda38t3Y3waNatFpRUmODJEz55gqjsgrKAY9MTY2qDp0N9oDrG+/fmAgAAAACgPXId47W5VXOSNlMphbZNs8v8Yk8AZZbf1e6El1dUHnB856TY2v5Ph5bgOWIjgz5vAAAAAADQNhFKwch1lsn3+4rEFp0gaYmxklNY5ql8MgGUpxH5wZIKv9fpIruuyXG+pXcaQPXOcEh8dESrfA4AAAAAAGANhFKQVV/tksXvbKhtOv4/iY4Ik/KqGr9xdptIt5QE6dPp0PK7XukOiY3iNAIAAAAAAEeHNKGd0wqpR9/1BlIeGkhpANUzzduA3GECqJ7pDhNYAQAAAAAANBWhVDu3N79EXAFa3d89+WQZ2jutNaYEAAAAAADaAeu2ckez6JIUZ6qi6rLbbNItNaG1pgQAAAAAANoBQql2LtURI9edM8AXTOn9deecaI4DAAAAAAC0FJbvQcYO7iaDeybLd9v2Sf9enSW9Q1xrTwkAAAAAAIQ4KqVgaGVUv84JVEgBAAAAAICgIJQCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0NDpvBm6329w7nU6xKpfLJUVFRRIdHS12O1klAAAAAADBFirX5t58xJuXHA6hVDPQE0ZlZma29lQAAAAAAADaTF6SmJh42Odt7h+LrdCoJHPfvn2SkJAgNptNrJpiaqi2e/ducTgcrT0dAAAAAADaHWeIXJtr1KSBVOfOnY9Y8UWlVDPQH3DXrl0lFOhJb+UTHwAAAAAAq3OEwLX5kSqkvKy7QBEAAAAAAACWRSgFAAAAAACAoCOUghEVFSXz58839wAAAAAAIPii2tm1OY3OAQAAAAAAEHRUSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAg6QikAAAAAAAAEXXjwv2Xocblcsm/fPklISBCbzdba0wEAAAAAAGg1brdbioqKpHPnzmK3H74eilCqGWgglZmZ2drTAAAAAAAAaDN2794tXbt2PezzhFLNQCukvD9sh8MhVq32ys3NldTU1COmmAAAAAAAoGW4QuTa3Ol0muIdb15yOIRSzcC7ZE8DKSuHUuXl5Wb+Vj7xAQAAAACwKleIXZv/WIsj639CAAAAAAAAWA6hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAg6QikAAAAAAAAEHaEUAAAAAAAAgo5QCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgsF0otXbpUevToIdHR0TJixAj5/PPPjzj+tddek+OPP96MHzBggLz33nuHHfvb3/5WbDabLF68uAVmDgAAAAAAAEuGUitWrJBZs2bJ/Pnz5csvv5SBAwfKmDFjJCcnJ+D4NWvWyOTJk2X69Ony1Vdfyfjx483t22+/9Rv717/+VT799FPp3LlzED4JAAAAAABA+2apUOrhhx+WGTNmyLRp06R///6ybNkyiY2NlWeeeSbg+EcffVTGjh0rN998s/Tr10/uuusuGTJkiDz++OP1xu3du1euvfZaeemllyQiIiJInwYAAAAAAKD9skwoVVlZKevXr5fRo0f7jtntdvN47dq1AV+jx+uOV1pZVXe8y+WSyy67zARXJ5xwQgt+AgAAAAAAAHiFi0Xk5eVJTU2NpKen1zuujzdt2hTwNVlZWQHH63Gv++67T8LDw+UPf/hDo+dSUVFhbl5Op9MXcOnNinTebrfbsvMHAAAAAMDqXCFybd7Y+VsmlGoJWnmlS/y0P5U2OG+sRYsWyYIFC/yO5+bmSnl5uVj1hCksLDQnv1agAQAAAACA4HKFyLV5UVFRaIVSKSkpEhYWJtnZ2fWO6+OMjIyAr9HjRxr/8ccfmybp3bp18z2v1Vg33nij2YFvx44dAd93zpw5puF63UqpzMxMSU1NFYfDIVY98TWY089g5RMfAAAAAACrcoXItXl0dHRohVKRkZEydOhQWb16tdlBz/vL0sczZ84M+JqRI0ea56+//nrfsffff98cV9pLKlDPKT2uzdQPJyoqytwa0hPGyieNnvhW/wwAAAAAAFiZLQSuzRs7d8uEUkqrk6ZOnSrDhg2T4cOHm2qmkpISX4A0ZcoU6dKli1lep6677jo5/fTT5aGHHpJzzjlHli9fLl988YX86U9/Ms8nJyebW126+55WUh133HGt8AkBAAAAAADaB0uFUhMnTjR9m+bNm2ealQ8aNEhWrVrla2a+a9euemncqFGj5OWXX5Y77rhDbrvtNunbt6+8+eabcuKJJ7bipwAAAAAAAIDNrd2z0CTaUyoxMdE0I7NyTyntr5WWlmbpEkEAAAAAAKzKFSLX5o3NSaz7CQEAAAAAAGBZhFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAg6QikAAAAAAAAEHaEUAAAAAAAAgo5QCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAILOcqHU0qVLpUePHhIdHS0jRoyQzz///IjjX3vtNTn++OPN+AEDBsh7773ne66qqkpuvfVWczwuLk46d+4sU6ZMkX379gXhkwAAAAAAALRflgqlVqxYIbNmzZL58+fLl19+KQMHDpQxY8ZITk5OwPFr1qyRyZMny/Tp0+Wrr76S8ePHm9u3335rni8tLTXvM3fuXHO/cuVK2bx5s5x33nlB/mQAAAAAAADti83tdrvFIrQy6uSTT5bHH3/cPHa5XJKZmSnXXnutzJ4922/8xIkTpaSkRN555x3fsVNOOUUGDRoky5YtC/g91q1bJ8OHD5edO3dKt27dGjUvp9MpiYmJUlhYKA6HQ6xIf5Ya7qWlpYndbqmsEgAAAACAkOAKkWvzxuYklvmElZWVsn79ehk9erTvmP6C9PHatWsDvkaP1x2vtLLqcOOV/sBsNpt06NChGWcPAAAAAACAusLFIvLy8qSmpkbS09PrHdfHmzZtCviarKysgOP1eCDl5eWmx5Qu+TtSkldRUWFudRNAb6KpNyvSeWvRnFXnDwAAAACA1blC5Nq8sfO3TCjV0rTp+cUXX2x++U8++eQRxy5atEgWLFjgdzw3N9cEW1Y9YbRKTD+/lUsEAQAAAACwKleIXJsXFRWFViiVkpIiYWFhkp2dXe+4Ps7IyAj4Gj3emPHeQEr7SH3wwQc/2hdqzpw5puF63Uop7W2Vmppq6Z5SumxRP4OVT3wAAAAAAKzKFSLX5tHR0aEVSkVGRsrQoUNl9erVZgc97y9LH8+cOTPga0aOHGmev/76633H3n//fXO8YSD1ww8/yIcffijJyck/OpeoqChza0hPGCufNHriW/0zAAAAAABgZbYQuDZv7NwtE0oprU6aOnWqDBs2zOyQt3jxYrO73rRp08zzU6ZMkS5dupjldeq6666T008/XR566CE555xzZPny5fLFF1/In/70J18gddFFF8mXX35pdujTnlXeflNJSUkmCAMAAAAAAEDzs1QoNXHiRNO3ad68eSY8GjRokKxatcrXzHzXrl310rhRo0bJyy+/LHfccYfcdttt0rdvX3nzzTflxBNPNM/v3btX3nrrLfO1vlddWjV1xhlnBPXzAQAAAAAAtBc2t3bPQpNoT6nExETTjMzKPaVycnIkLS3N0iWCAAAAAABYlStErs0bm5NY9xMCAAAAAADAsgilAAAAAAAAEHSEUgAAAAAAALBGKFVQUCBPP/20zJkzR/Lz880x3cFOG4cDAAAAAAAAzb773jfffCOjR482Dat27NghM2bMkKSkJFm5cqXZ/e6FF1442rcEAAAAAABAO3PUlVKzZs2Syy+/XH744QeJjo72HR83bpx89NFHzT0/AAAAAAAAhKCjDqXWrVsnV199td/xLl26SFZWVnPNCwAAAAAAACHsqEOpqKgocTqdfsf/97//SWpqanPNCwAAAAAAACHsqEOp8847TxYuXChVVVXmsc1mM72kbr31VpkwYUJLzBEAAAAAAADtPZR66KGHpLi4WNLS0qSsrExOP/106dOnjyQkJMg999zTMrMEAAAAAABA+959T3fde//99+U///mP/Pe//zUB1ZAhQ8yOfAAAAAAAAECLhFIvvPCCTJw4UU499VRz86qsrJTly5fLlClTjvYtAQAAAAAA0M4c9fK9adOmSWFhod/xoqIi8xwAAAAAAADQ7KGU2+02zc0b2rNnj1naBwAAAAAAADTb8r3BgwebMEpvZ599toSHH3ppTU2NbN++XcaOHdvYtwMAAAAAAEA71uhQavz48eb+66+/ljFjxkh8fLzvucjISOnRo4dMmDChZWYJAAAAAACA9hlKzZ8/39xr+KSNzqOjo1tyXgAAAAAAAAhhR7373tSpU1tmJgAAAAAAAGg3jjqU0v5RjzzyiLz66quya9cuqaysrPd8fn5+c84PAAAAAAAAIeiod99bsGCBPPzww2YJX2FhocyaNUsuvPBCsdvtcuedd7bMLAEAAAAAANC+Q6mXXnpJ/vznP8uNN95oduCbPHmyPP300zJv3jz59NNPW2aWAAAAAAAAaN+hVFZWlgwYMMB8rTvwabWU+tWvfiXvvvtu888QAAAAAAAAIeeoQ6muXbvK/v37zde9e/eWf/zjH+brdevWSVRUVPPPEAAAAAAAACHnqEOpCy64QFavXm2+vvbaa2Xu3LnSt29fmTJlilxxxRUtMUcAAAAAAAC09933/vjHP/q+1mbn3bt3lzVr1phg6txzz23u+QEAAAAAACAEHXUo1dApp5xibuqLL76QYcOGNce8AAAAAAAAEMKOevlecXGxlJWV1Tv29ddfmyqpESNGNOfcAAAAAAAA0N5Dqd27d8vIkSMlMTHR3GbNmiWlpaWml5SGUXFxcWYZHwAAAAAAANBsy/duvvlmKS8vl0cffVRWrlxp7j/++GMTSG3dutXsygcAAAAAAAA0a6XURx99JE8++aTMnDlTli9fLm63Wy699FJ5/PHHgxpILV26VHr06CHR0dEmEPv888+POP61116T448/3owfMGCAvPfee/We188xb9486dSpk8TExMjo0aPlhx9+aOFPAQAAAAAA0L41OpTKzs6Wnj17mq/T0tIkNjZWfvnLX0owrVixwiwbnD9/vnz55ZcycOBAGTNmjOTk5AQcr8sJJ0+eLNOnT5evvvpKxo8fb27ffvutb8z9998vS5YskWXLlslnn31mliHqe2pVWLviPCCRezebewAAAAAA0Aqc7eva3ObWUqFGCAsLk6ysLElNTTWPHQ6H/Pe///UFVcGglVEnn3yyqc5SLpdLMjMz5dprr5XZs2f7jZ84caKUlJTIO++84zumOwUOGjTIhFD60Tt37iw33nij3HTTTeb5wsJCSU9Pl+eee04mTZrUqHk5nU7TZ0tfqz8Xy1n/vrjfeVJPBnHbbGL75QyRQWe29qwAAAAAAGg/vv5Q3H//86Fr83N/JzJktFhRY3OSRveU0gDnJz/5idhsNt8ufIMHDxa7vX6xVX5+vrSEyspKWb9+vcyZM8d3TL+3Lrdbu3ZtwNfoca2sqkuroN58803z9fbt203Qpu/hpT80Db/0tYcLpSoqKsyt7g/bG5LpzVKcB8RWG0gpc//enzw3AAAAAAAQNDbvvQZTbz8p7l4DRRzJYjWNzUYaHUo9++yz0pry8vKkpqbGVDHVpY83bdoU8DUaOAUar8e9z3uPHW5MIIsWLZIFCxb4Hc/NzbXcsj8tC0xqXLEcAAAAAAAIEpvbJQe3fi+VXX4iVlNUVNS8odTUqVObMp+QotVadSuwtFJKlxHq0kbLLd+LDvOUBdYJptw2u7ivWWzJNBYAAAAAAMtxHhDbk9f5XZt36N3Pktfmutlcs4ZSrS0lJcX0tdKG63Xp44yMjICv0eNHGu+912O6+17dMdp36nCioqLMrSFdTthwOWOb1yFV5NzfmbJATWH1pLede43Y0jJbe2YAAAAAALQP0bGBr831mt2CGpuNWCZBiYyMlKFDh8rq1avrrVHUxyNHjgz4Gj1ed7x6//33feO1SbsGU3XHaNWT7sJ3uPcMSUNGi/u6ZZJ/7g3m3qqN1AAAAAAAsKwh7e/a3DKVUkqXzOkywmHDhsnw4cNl8eLFZne9adOmmeenTJkiXbp0MT2f1HXXXSenn366PPTQQ3LOOefI8uXL5YsvvpA//cnTxFubtl9//fVy9913S9++fU1INXfuXLMj3/jx46VdcSR71qlasCwQAAAAAICQ4Ghf1+aWCqUmTpxomonPmzfPNCLXJXarVq3yNSrftWtXvRKxUaNGycsvvyx33HGH3HbbbSZ40p33TjzxRN+YW265xQRbV111lRQUFMhpp51m3rOx6x8BAAAAAABw9GxuN1uvNZUu+UtMTJTCwkLrNTqvsxQyJydH0tLSrNcXCwAAAACAEOAKkWvzxuYkjaqUqrvT3I95+OGHGz0WAAAAAAAA7VOjQqmvvvqqUW+mPZoAAAAAAACAZgmlPvzww8YMAwAAAAAAABrFugsUAQAAAAAAENqVUhdeeGGj33DlypVNmQ8AAAAAAADagUaFUtoxHQAAAAAAAAhqKPXss8822zcEAAAAAAAA6CkFAAAAAACAtlkpVVfPnj3FZrMd9vlt27Y1dU4AAAAAAAAIcUcdSl1//fX1HldVVclXX30lq1atkptvvrk55wYAAAAAAIAQddSh1HXXXRfw+NKlS+WLL75ojjkBAAAAAAAgxDVbT6lf/vKX8sYbbzTX2wEAAAAAACCENVso9frrr0tSUlJzvR0AAAAAAABC2FEv3xs8eHC9Rudut1uysrIkNzdXnnjiieaeHwAAAAAAAELQUYdS48ePr/fYbrdLamqqnHHGGXL88cc359wAAAAAAAAQoo46lJo/f37LzAQAAAAAAADtRrP1lAIAAAAAAACavVJKl+nV7SUViD5fXV3d6G8OAAAAAACA9qnRodRf//rXwz63du1aWbJkibhcruaaFwAAAAAAAEJYo0Op888/3+/Y5s2bZfbs2fL222/LpZdeKgsXLmzu+QEAAAAAACAEHVNPqX379smMGTNkwIABZrne119/Lc8//7x07969+WcIAAAAAACA9h1KFRYWyq233ip9+vSRjRs3yurVq02V1IknnthyMwQAAAAAAED7Xb53//33y3333ScZGRnyyiuvBFzOBwAAAAAAADSGze12uxu7+15MTIyMHj1awsLCDjtu5cqV0t44nU5JTEw0lWQOh0OsSJvU5+TkSFpamvldAwAAAACA4HKFyLV5Y3OSRldKTZkyRWw2W3PNDwAAAAAAAO1Yo0Op5557rmVnAgAAAAAAgHbDurVgAAAAAAAAsCxCKQAAAAAAAASdZUKp/Px8ufTSS02DrA4dOsj06dOluLj4iK8pLy+X3//+95KcnCzx8fEyYcIEyc7O9j3/3//+VyZPniyZmZmmiXu/fv3k0UcfDcKnAQAAAAAAaN8sE0ppILVx40Z5//335Z133pGPPvpIrrrqqiO+5oYbbpC3335bXnvtNfn3v/8t+/btkwsvvND3/Pr1601H+xdffNG89+233y5z5syRxx9/PAifCAAAAAAAoP2yud1ut7Rx33//vfTv31/WrVsnw4YNM8dWrVol48aNkz179kjnzp39XqPbDqampsrLL78sF110kTm2adMmUw21du1aOeWUUwJ+L62s0u/3wQcfNPtWh21ZqGw7CQAAAACAVblC5Nq8sTmJJT6hhki6ZM8bSKnRo0ebX9Bnn30W8DVaBVVVVWXGeR1//PHSrVs3836Hoz+wpKSkZv4EAAAAAAAAqCtcLCArK8ukhHWFh4eb8EifO9xrIiMjTZhVV3p6+mFfs2bNGlmxYoW8++67R5xPRUWFudVNAL2Jpt6sSOetRXNWnT8AAAAAAFbnCpFr88bOv1VDqdmzZ8t99913xDG6lC4Yvv32Wzn//PNl/vz58otf/OKIYxctWiQLFizwO56bm2uaq1v1hNEqMT35rVwiCAAAAACAVblC5Nq8qKio7YdSN954o1x++eVHHNOrVy/JyMgwayrrqq6uNjvy6XOB6PHKykopKCioVy2lu+81fM13330nZ599tmmcfscdd/zovLUZ+qxZs+pVSukOftrDyso9pWw2m/kMVj7xAQAAAACwKleIXJtHR0e3/VBKf8h6+zEjR4404ZL2iRo6dKg5po3I9Zc1YsSIgK/RcREREbJ69WqZMGGCObZ582bZtWuXeT8v3XXvrLPOkqlTp8o999zTqHlHRUWZW0N6wlj5pNET3+qfAQAAAAAAK7OFwLV5Y+duiU+oO+aNHTtWZsyYIZ9//rn85z//kZkzZ8qkSZN8O+/t3bvXNDLX55V2eZ8+fbqpaPrwww9NoDVt2jQTSHl33tMle2eeeaZZrqfjtNeU3nQZHgAAAAAAANp5o3P10ksvmSBKl9lp4qbVT0uWLPE9rzvtaSVUaWmp79gjjzziG6uNyceMGSNPPPGE7/nXX3/dBFAvvviiuXl1795dduzYEcRPBwAAAAAA0L7Y3No9C02iPaW0MkubkVm5p5T27dJdDq1cIggAAAAAgFW5QuTavLE5iXU/IQAAAAAAACyLUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAg6QikAAAAAAAAEHaEUAAAAAAAAgo5QCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNBZJpTKz8+XSy+9VBwOh3To0EGmT58uxcXFR3xNeXm5/P73v5fk5GSJj4+XCRMmSHZ2dsCxBw4ckK5du4rNZpOCgoIW+hQAAAAAAACwVCilgdTGjRvl/fffl3feeUc++ugjueqqq474mhtuuEHefvttee211+Tf//637Nu3Ty688MKAYzXkOumkk1po9gAAAAAAALBcKPX999/LqlWr5Omnn5YRI0bIaaedJo899pgsX77cBE2BFBYWyv/93//Jww8/LGeddZYMHTpUnn32WVmzZo18+umn9cY++eSTpjrqpptuCtInAgAAAAAAaN8sEUqtXbvWLNkbNmyY79jo0aPFbrfLZ599FvA169evl6qqKjPO6/jjj5du3bqZ9/P67rvvZOHChfLCCy+Y9wMAAAAAAEDLCxcLyMrKkrS0tHrHwsPDJSkpyTx3uNdERkaaMKuu9PR032sqKipk8uTJ8sADD5iwatu2bY2aj75Ob15Op9Pcu1wuc7Minbfb7bbs/AEAAAAAsDpXiFybN3b+rRpKzZ49W+67774fXbrXUubMmSP9+vWT3/zmN0f1ukWLFsmCBQv8jufm5prm6lY9YXTJo578VIwBAAAAABB8rhC5Ni8qKmr7odSNN94ol19++RHH9OrVSzIyMiQnJ6fe8erqarMjnz4XiB6vrKw0vaLqVkvp7nve13zwwQeyYcMGef31181j/aWrlJQUuf322wMGT94wa9asWfUqpTIzMyU1NdXsDmjVE193HtTPYOUTHwAAAAAAq3KFyLV5dHR02w+l9Iestx8zcuRIEy5pnyhtWO4NlPSXpY3PA9FxERERsnr1apkwYYI5tnnzZtm1a5d5P/XGG29IWVmZ7zXr1q2TK664Qj7++GPp3bv3YecTFRVlbg3pCWPlk0ZPfKt/BgAAAAAArMwWAtfmjZ27JXpK6RK7sWPHyowZM2TZsmWmgfnMmTNl0qRJ0rlzZzNm7969cvbZZ5uG5cOHD5fExESZPn26qWjS3lNawXTttdeaQOqUU04xr2kYPOXl5fm+X8NeVAAAAAAAAGg+lgil1EsvvWSCKA2eNHHT6qclS5b4ntegSiuhSktLfcceeeQR31htTD5mzBh54oknWukTAAAAAAAAwMvm9jZSwjHTnlJamaXNyKzcU0r7dukuh1YuEQQAAAAAwKpcIXJt3ticxLqfEAAAAAAAAJZFKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAAAAACDpCKQAAAAAAAAQdoRQAAAAAAACCjlAKAAAAAAAAQUcoBQAAAAAAgKAjlAIAAAAAAEDQEUoBAAAAAAAg6AilAAAAAAAAEHSEUgAAAAAAAAi68OB/y9DjdrvNvdPpFKtyuVxSVFQk0dHRYreTVQIAAAAAEGyuELk29+Yj3rzkcAilmoGeMCozM7O1pwIAAAAAANBm8pLExMTDPm9z/1hshUYlmfv27ZOEhASx2Wxi1RRTQ7Xdu3eLw+Fo7ekAAAAAANDuOEPk2lyjJg2kOnfufMSKLyqlmoH+gLt27SqhQE96K5/4AAAAAABYnSMErs2PVCHlZd0FigAAAAAAALAsQikAAAAAAAAEHaEUjKioKJk/f765BwAAAAAAwRfVzq7NaXQOAAAAAACAoKNSCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIICjZ5BAAAAAAAdYXXewQ0s+LiYomKipKIiAgTTNlsttaeEgAAAAAA7crWrVvllVdekZKSEjnxxBPl0ksvlbaASim0mO+//14uuOACWbFihVRWVppAioopAAAAAACCZ8OGDTJq1Cj54osv5O2335bHH39c3nvvPWkLqJRCi9i5c6dMmDDBpLFaLRUdHS3nnXeeREZGUjEFAAAAAEAQZGdny8SJE2X69Oly7733Sl5enpx11lmyb98+aQuolEKzq6mpkTfeeEP69Okjn3/+uXTo0MGc/G+99RYVUwAAAAAABMnmzZvNNfjvf/978zglJUUGDhwo//3vf+V3v/uduVZvTYRSaHZhYWEmeZ0yZYo52d99911JT0/3BVMVFRUEUwAAAAAAtLDw8HApLS31LdfT6/KXXnpJ7Ha7qZpavny5XHzxxdJabG6SAbSAqqoq09zcSyukzj//fFM6eNttt5mv9fm//e1v5msAAAAAANC89Br8D3/4g1nF1LdvX/nggw9k5cqVpr2Oev755+Xuu++Wv/71r6YBerDRUwrNQhPW3bt3S2xsrKSlpUnHjh3F5XKZ9LW6utr0knrzzTdl/PjxJpnVJX4ffvihqZw6+eSTpXPnzq39EQAAAAAAsLTS0lJzi4mJMdfhumppyZIlpoeU9n7Ozc2V008/3Te+e/fu5tq9blFJMBFKocm++eYb+fWvf22CJl2apye9dvM/5ZRTfOWCGkxFRUWZyijdke+yyy4zf0A++ugjAikAAAAAAJpo48aNcv3110tWVpZ5fOWVV8rUqVPNNbre9Jpdr8vz8/MlMTHRjPnHP/4hqampptdUayCUQpPoyX7uuefKpEmTTDf/7777TlasWCE/+9nP5IUXXjDHvcGU/gHQIEqT2ISEBBNInXDCCa39EQAAAAAAsLTvv/9ezjzzTHMNrk3NtYfUU089JaNGjTKrk5QGU1u3bpXrrrtOevToYdru6PW7rmJKTk5ulXnTUwpN8vXXX5uqp7ffftuc1KqsrEzmzZtnSgR1reo555zjW8r3xBNPyMyZM2X9+vUyePDg1p4+AAAAAACWdvDgQRNG9enTR5YuXeo7PnToUBk+fLg8+eSTvmty3XVv9uzZ5rpdVy3dfvvtrVosQqUUmqSwsNCUCHqzTT3Rde3q/fffb07ySy65RL744gvTUE1NnDhRxo4dK7169WrlmQMAAAAAYH179+4Vh8Nhrre9G43pKqWzzz5bDhw4YI7ZbDazemngwIHy2muvSXx8vGm/o8v5WpO9Vb87LO+0006Tn/70pzJnzhyzLlWTVw2m9ITXY4MGDZJXXnnFhFZ6XEsCCaQAAAAAAGgeWumklVLaRsfbPkclJSVJcXGx+Vqv0cPCwsxjDaRUawdSilAKTaIntaaxO3bsMMv1nE6nCaZUly5dzMm+adMm8wfAexwAAAAAADSdtyhkwoQJ5rEWhHivvUtKSsxue166ounOO+80FVNtBcv3cMz0ZNeT/5prrjHN0nRnPV2yp2tStXRQaWVUx44dzUmvfzB0PAAAAAAAaDpvAOW9PtdbdXW1qZbSDca8u+zNnTtX7rnnHtMXWotL2goaneOYadCkJ7O3Ydpdd90l7777rhQUFMh5550nu3fvlnfeeUc+/fRTdtkDAAAAAKAFr82L6yzNU48++qh888030r17d1m0aJF88sknpvl5W8J6KjSKBk+BTvqdO3fKgAED5F//+pdJXu+77z75xS9+IRs2bDDrU9euXUsgBQAAAABAM3O73aYqynttPn78eBM8eenyvWeffdYs22uLgZRi+R5+dHc9LffzNjD3lgZ6T/pTTz1VfvWrX5mG5+r00083N/3DUXctKwAAAAAAODb79u2TdevWSXl5udndfsiQIWapni7T27Ztm5xxxhnyy1/+0ndtrjIyMkyV1HvvvSf9+vWTtojlezis7777TkaNGiW33HKL3HbbbeZY3WDqiiuukIiICFm2bJmvV5R3HSsAAAAAAGi6DRs2yAUXXGD6Nefk5JhjTzzxhJxzzjnmGnzs2LGSkpIiL774Yr3rcX0uKytLOnXqJG0VoRQC2rNnj+kLpeV+eXl5cvPNN8vs2bPrLd2rqqoyoRQAAAAAAGh+W7duNauRfvOb35hrcu3drIGU7qr3/PPPS1xcnFRWVppr87qBVN2CkraM5XvwoyfvG2+8IT179pSZM2fK559/Lvfee695Tv8QEEgBAAAAANCyKisrZenSpWYFk24sptfgHTp0kJNPPtn0dPb2fo6MjPR7rRUCKUUohYAn77hx4yQtLU3OPPNMGTRokCn702793mBK/zBYJXkFAAAAAMBq7Ha79OnTxxSM6DW4t13OWWedJQsXLjQ9oBMSEuq9xmotdQilEJA2TtOTX+m61SuvvNKc2HUrpvRkf/vtt2XkyJFm/SoAAAAAAGge4eHhppdUw55Q3sooba3jDaE2bdokxx9/vKUCKUUoBV8n/71798qBAwdk9OjRJpHVm24vqX8QNHTSxuZKgyk98XXso48+Krt27Wrt6QMAAAAAEDLX5nl5eTJmzBhJT083x73X5rpiyel0SmlpqQmnNISaM2eO3HfffXLw4EFxOByWCqYIpSDffPON/OpXvzJlf//73/9kwIABctVVV5lGavHx8b7G5qmpqTJ9+nQTSOlufLqWde3atW26kz8AAAAAAFa9Np8xY4Zcdtll5trc20JHwygNqGJiYmTBggWm79Snn34qiYmJYjU0BGrnNH2dNGmSXHLJJfLuu++aVFZL/p577jnTOK2oqMgEUt4Galox9d1335k/JJ988okMGzastT8CAAAAAAAheW3+/PPP+67NvT2dNaDSohEtJtGVTB9++KEMHz5crIhQqp3LysqSsrIyc+L36NHDnNgaSGmZ4Jo1a0wJYHl5uTn5tULqxRdflH/84x/mpO/fv39rTx8AAAAAgHZzba5yc3Nlw4YN8s4778jnn38uQ4cOFasilGrnvGtQvX2hdJ2qHtMk9vTTTzcJ7bp168xzOu7UU0+Vzz77TIYMGdLKMwcAAAAAoP1dm3fp0kVuvPFGWb9+vQwcOFCszObW8he0WxUVFXLaaadJRkaGvPnmm2apnreBmp4aeoIPHjzYlAxabWtJAAAAAABC7drcOz4qKkqsjkqpdkz7ROlJ/Oyzz8pHH30k11xzjTnuPek1gDrvvPMkJyfHHCeQAgAAAACg9a7N3bV1RaEQSClCqXZM+0TpznonnniiSVtfeeUVmTJlimRnZ/vGbN++XTp27GjGAQAAAACA1rs2d9VuQhYqWL7XjjRcfuctBSwuLjalf19//bVpqta9e3dJSkqS5ORk+dvf/iZr1641W1ECAAAAAICm4dr8ECql2oGtW7fKwYMH6530msLqSb9jxw75yU9+YhqmnX322bJx40YZN26caZyWlpZmOvmH2kkPAAAAAECwcW3uj0qpEPff//7XNEN7+umn5Yorrqj33O7du80ueueff778+c9/NmWA2kzNm9rqYy0jBAAAAAAAx45r88AIpUL8pD/11FNl5syZ8sc//tHv+ccee0y2bdsmDz/8cL2k1nvis9seAAAAAABNw7X54RFKhahNmzaZ0r558+bJ3LlzTbL6r3/9S7Zs2WKap/Xt21dSU1NDOnEFAAAAAKA1cW1+ZOE/8jwsSE/mV1991axNveiii8yxn//853LgwAGzTlWbpPXs2dOksCeddFJrTxcAAAAAgJDDtfmPa38xXDug6erVV18tM2bMMGtWNZXt0KGD2VoyNzdXHnzwQbM+9e677zbd/QEAAAAAQPPi2vzHUSkVotLT082JrV38tUu/ft2vXz/z3AUXXCA7d+6U++67TwoLCyU+Pr61pwsAAAAAQMjh2vzICKVCxL59++TLL7+UyspK6datmwwbNsysS73jjjvMSd67d28zTssGNYnt06ePdOzYUSIjI1t76gAAAAAAhASuzY8OoVQI2LBhg4wfP15SUlJMx/4ePXrILbfcIr/+9a+lU6dOkpGR4evUrye9+uc//yldu3aV2NjYVp49AAAAAADWx7X50aOnlMVt3bpVxo0bZ5qm/eMf/5BVq1bJCSecYO41eW24deSuXbvk5ptvlr/85S/y0EMPSVxcXKvOHwAAAAAAq+Pa/NjY3PqTgSVpOeCcOXNkz5495kT2lvs988wzJo3dvHmz6ebvpetXn3rqKVmzZo288sorMmjQoFacPQAAAAAA1se1+bFj+Z7Ft5fUMj9tkqYnvTd5HTVqlGmQVlVVVW/88OHDpaioSBYuXChdunRptXkDAAAAABAquDY/doRSFhYdHW3Wq/bs2bPecd1iMiIiot6Jv379ehk6dKicffbZrTBTAAAAAABCE9fmx46eUhazf/9+U+qn61I1jfWe9LpG1bs+VbeSPHjwoO818+bNk5///Ody4MABk9gCAAAAAIBjx7V586BSykK++eYbOe+88yQqKkqys7NN9349qceMGSNJSUm+EkG92e12UyZ49913y4MPPigff/xxvTWsAAAAAADg6HFt3nxodG4Rubm58rOf/UwuvPBCmT59uikPnDVrlvnDcPHFF8vvf/97SU1NNWNzcnJk7Nix8pOf/ET++te/muZpWh4IAAAAAACOHdfmzYtKKQud+OXl5ebE79Wrlzm2fPlymT17tqxcudJsH6knf2xsrCkF/Prrr2XTpk3y2WeftetO/gAAAAAANBeuzZsXPaUsQhujVVdXS2lpqXlcVlZm7v/4xz/KmWeeKU8++aRs2bLFHOvYsaP87ne/ky+//JKTHgAAAACAZsK1efNi+Z6F6LaRuhb1gw8+MI8rKirMGlZ18sknS58+feSVV14xjzW51TJCAAAAAADQfLg2bz5USrVRJSUlUlRUJE6n03fsqaeeko0bN8oll1xiHutJrwmt0jWt+hovTnoAAAAAAJqGa/OWRSjVBn333Xdmferpp58u/fr1k5deeskc168fffRRef/99+XXv/61KRvUTv7eBmq6dlX/IFD8BgAAAABA03Bt3vJodN4GT3pNVqdMmSLDhg2T9evXy7Rp06R///4yePBgs+2knuC6LvWkk06S448/XiIjI+Xdd9+VTz/9VMLD+ZUCAAAAANAUXJsHBz2l2pD8/HyZPHmyOZk1dfXSZmkDBgyQJUuW+I5p+eDdd99tXqPlgNdcc435wwEAAAAAAI4d1+bBQ3TXhmjJX0FBgVx00UXmscvlMiWAPXv2NCe40gxRbwkJCXLffffVGwcAAAAAAJqGa/Pg4afVhqSnp8uLL74oP/3pT83jmpoac9+lSxffiW2z2czXdZus6TEAAAAAANB0XJsHD6FUG9O3b19fwhoREWG+1vRVm6V5LVq0SJ5++mlfd39OfAAAAAAAmg/X5sHB8r02ShNXPeG9J7U3jZ03b55Zr/rVV1/ROA0AAAAAgBbEtXnLolKqDfP2oNcTPDMzUx588EG5//775YsvvpCBAwe29vQAAAAAAAh5XJu3HOK8NsybwGqp4J///GdxOBzyySefyJAhQ1p7agAAAAAAtAtcm7ccKqUsYMyYMeZ+zZo1MmzYsNaeDgAAAAAA7Q7X5s3P5vbWoaFNKykpkbi4uNaeBgAAAAAA7RbX5s2LUAoAAAAAAABBx/I9AAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAACDoCKUAAAAAAAAQdIRSAAAAbcjll18uNpvN3CIiIiQ9PV1+/vOfyzPPPCMul6vR7/Pcc89Jhw4dWnSuAAAATUEoBQAA0MaMHTtW9u/fLzt27JC///3vcuaZZ8p1110nv/rVr6S6urq1pwcAANAsCKUAAADamKioKMnIyJAuXbrIkCFD5LbbbpO//e1vJqDSCij18MMPy4ABAyQuLk4yMzPld7/7nRQXF5vn/vWvf8m0adOksLDQV3V15513mucqKirkpptuMu+trx0xYoQZDwAAEGyEUgAAABZw1llnycCBA2XlypXmsd1ulyVLlsjGjRvl+eeflw8++EBuueUW89yoUaNk8eLF4nA4TMWV3jSIUjNnzpS1a9fK8uXL5ZtvvpFf//rXpjLrhx9+aNXPBwAA2h+b2+12t/YkAAAAcKinVEFBgbz55pt+z02aNMkESd99953fc6+//rr89re/lby8PPNYK6quv/56815eu3btkl69epn7zp07+46PHj1ahg8fLvfee2+LfS4AAICGwv2OAAAAoE3S/5eoS/HUP//5T1m0aJFs2rRJnE6n6TVVXl4upaWlEhsbG/D1GzZskJqaGvnJT35S77gu6UtOTg7KZwAAAPAilAIAALCI77//Xnr27GkaoGvT82uuuUbuueceSUpKkk8++USmT58ulZWVhw2ltOdUWFiYrF+/3tzXFR8fH6RPAQAA4EEoBQAAYAHaM0ornW644QYTKrlcLnnooYdMbyn16quv1hsfGRlpqqLqGjx4sDmWk5MjP/3pT4M6fwAAgIYIpQAAANoYXU6XlZVlAqTs7GxZtWqVWaqn1VFTpkyRb7/9VqqqquSxxx6Tc889V/7zn//IsmXL6r1Hjx49TGXU6tWrTYN0rZ7SZXuXXnqpeQ8NtDSkys3NNWNOOukkOeecc1rtMwMAgPaH3fcAAADaGA2hOnXqZIIl3Rnvww8/NDvt/e1vfzPL7jRkevjhh+W+++6TE088UV566SUTWtWlO/Bp4/OJEydKamqq3H///eb4s88+a0KpG2+8UY477jgZP368rFu3Trp169ZKnxYAALRX7L4HAAAAAACAoKNSCgAAAAAAAEFHKAUAAAAAAICgI5QCAAAAAABA0BFKAQAAAAAAIOgIpQAAAAAAABB0hFIAAAAAAAAIOkIpAAAAAAAABB2hFAAAAAAAAIKOUAoAAAAAAABBRygFAAAAAACAoCOUAgAAAAAAQNARSgEAAAAAAECC7f8D11SrppRFFAIAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " import matplotlib.pyplot as plt\n", + "\n", + " if timeseries:\n", + " fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6), sharex=True)\n", + "\n", + " ax1.plot(dates, means, marker=\".\", color=\"steelblue\")\n", + " ax1.set_ylabel(\"Mean\")\n", + " ax1.set_title(\"conv_rate — Daily Trend\")\n", + " ax1.grid(True, alpha=0.3)\n", + "\n", + " ax2.plot(dates, null_rates, marker=\".\", color=\"coral\")\n", + " ax2.set_ylabel(\"Null Rate\")\n", + " ax2.set_xlabel(\"Date\")\n", + " ax2.grid(True, alpha=0.3)\n", + "\n", + " plt.xticks(rotation=45)\n", + " plt.tight_layout()\n", + " plt.show() # pragma: allowlist secret\n", + "except ImportError:\n", + " print(\"Install matplotlib to visualize: pip install matplotlib\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 9: On-Demand Exploration (Transient Compute)\n", + "\n", + "Compute metrics for an arbitrary date range without storing them. Useful for ad-hoc investigation." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "conv_rate (numeric):\n", + " rows=905 nulls=0 null_rate=0.0000\n", + " mean=0.5041 stddev=0.1929\n", + " p50=0.4964 p95=0.8221 p99=0.9757\n", + "\n", + "avg_daily_trips (numeric):\n", + " rows=905 nulls=0 null_rate=0.0000\n", + " mean=20.1525 stddev=4.4410\n", + " p50=20.0000 p95=27.0000 p99=31.9600\n", + "\n", + "vehicle_type (categorical):\n", + " rows=905 nulls=0 null_rate=0.0000\n", + " unique_values=5\n", + " van: 194\n", + " sedan: 193\n", + " suv: 186\n", + " truck: 171\n", + " compact: 161\n", + "\n" + ] + } + ], + "source": [ + "transient_result = monitoring.compute_transient(\n", + " project=\"monitoring_demo\",\n", + " feature_view_name=\"driver_stats\",\n", + " feature_names=[\"conv_rate\", \"avg_daily_trips\", \"vehicle_type\"],\n", + " start_date=date(2025, 1, 10),\n", + " end_date=date(2025, 1, 20),\n", + ")\n", + "\n", + "for fm in transient_result.get(\"metrics\", []):\n", + " print(f\"{fm['feature_name']} ({fm['feature_type']}):\")\n", + " print(f\" rows={fm['row_count']} nulls={fm['null_count']} null_rate={fm['null_rate']:.4f}\")\n", + " if fm[\"feature_type\"] == \"numeric\":\n", + " print(f\" mean={fm['mean']:.4f} stddev={fm['stddev']:.4f}\")\n", + " print(f\" p50={fm['p50']:.4f} p95={fm['p95']:.4f} p99={fm['p99']:.4f}\")\n", + " elif fm[\"feature_type\"] == \"categorical\" and fm.get(\"histogram\"):\n", + " hist = fm[\"histogram\"]\n", + " print(f\" unique_values={hist['unique_count']}\")\n", + " for entry in hist[\"values\"]:\n", + " print(f\" {entry['value']}: {entry['count']}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 10: REST API Usage\n", + "\n", + "Once the Feast registry server is running, all monitoring endpoints are available via HTTP.\n", + "\n", + "```bash\n", + "# Start the server\n", + "feast serve_registry\n", + "```\n", + "\n", + "### Compute metrics via REST" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'job_id': '077f59c5-c341-4fbb-9adc-b0111fc9228b', 'status': 'completed', 'computed_feature_views': 1, 'computed_features': 20, 'granularities': ['biweekly', 'daily', 'monthly', 'quarterly', 'weekly'], 'duration_ms': 98}\n", + "[{'project_id': 'monitoring_demo', 'feature_view_name': 'driver_stats', 'feature_name': 'conv_rate', 'metric_date': '2025-01-01', 'granularity': 'daily', 'data_source_type': 'batch', 'computed_at': '2026-04-21T13:41:42.687597+05:30', 'is_baseline': True, 'feature_type': 'numeric', 'row_count': 4922, 'null_count': 0, 'null_rate': 0.0, 'mean': 0.4988999058272324, 'stddev': 0.1975387054069251, 'min_val': 0.0, 'max_val': 1.0, 'p50': 0.4998365219303598, 'p75': 0.633892663793526, 'p90': 0.7521919750314627, 'p95': 0.825733080299169, 'p99': 0.9640086762359101, 'histogram': {'bins': [0.0, 0.05, 0.1, 0.15000000000000002, 0.2, 0.25, 0.30000000000000004, 0.35000000000000003, 0.4, 0.45, 0.5, 0.55, 0.6000000000000001, 0.65, 0.7000000000000001, 0.75, 0.8, 0.8500000000000001, 0.9, 0.9500000000000001, 1.0], 'counts': [53, 67, 75, 146, 180, 267, 355, 399, 432, 493, 505, 420, 411, 330, 283, 186, 124, 93, 46, 57], 'bin_width': 0.05}}, {'project_id': 'monitoring_demo', 'feature_view_name': 'driver_stats', 'feature_name': 'conv_rate', 'metric_date': '2025-02-28', 'granularity': 'daily', 'data_source_type': 'batch', 'computed_at': '2026-04-21T19:02:39.068597+05:30', 'is_baseline': False, 'feature_type': 'numeric', 'row_count': 104, 'null_count': 0, 'null_rate': 0.0, 'mean': 0.5201334885346333, 'stddev': 0.21216576270117404, 'min_val': 0.09993354474902831, 'max_val': 1.0, 'p50': 0.5065079886167952, 'p75': 0.6963620898617928, 'p90': 0.7809868206291576, 'p95': 0.8538056054296318, 'p99': 0.9187701931117264, 'histogram': {'bins': [0.09993354474902831, 0.1449368675115769, 0.18994019027412548, 0.23494351303667405, 0.27994683579922264, 0.32495015856177123, 0.3699534813243198, 0.4149568040868684, 0.459960126849417, 0.5049634496119656, 0.5499667723745142, 0.5949700951370628, 0.6399734178996113, 0.6849767406621599, 0.7299800634247084, 0.774983386187257, 0.8199867089498056, 0.8649900317123542, 0.9099933544749028, 0.9549966772374514, 1.0], 'counts': [4, 1, 6, 7, 5, 5, 7, 6, 11, 5, 4, 10, 5, 8, 9, 3, 4, 2, 1, 1], 'bin_width': 0.045003322762548585}}]\n", + "[{'project_id': 'monitoring_demo', 'feature_view_name': 'driver_stats', 'feature_name': 'conv_rate', 'metric_date': '2025-01-01', 'granularity': 'daily', 'data_source_type': 'batch', 'computed_at': '2026-04-21T13:41:42.687597+05:30', 'is_baseline': True, 'feature_type': 'numeric', 'row_count': 4922, 'null_count': 0, 'null_rate': 0.0, 'mean': 0.4988999058272324, 'stddev': 0.1975387054069251, 'min_val': 0.0, 'max_val': 1.0, 'p50': 0.4998365219303598, 'p75': 0.633892663793526, 'p90': 0.7521919750314627, 'p95': 0.825733080299169, 'p99': 0.9640086762359101, 'histogram': {'bins': [0.0, 0.05, 0.1, 0.15000000000000002, 0.2, 0.25, 0.30000000000000004, 0.35000000000000003, 0.4, 0.45, 0.5, 0.55, 0.6000000000000001, 0.65, 0.7000000000000001, 0.75, 0.8, 0.8500000000000001, 0.9, 0.9500000000000001, 1.0], 'counts': [53, 67, 75, 146, 180, 267, 355, 399, 432, 493, 505, 420, 411, 330, 283, 186, 124, 93, 46, 57], 'bin_width': 0.05}}]\n" + ] + } + ], + "source": [ + "# This cell is for reference — run it when the registry server is up.\n", + "\n", + "import requests\n", + "\n", + "BASE_URL = \"http://localhost:6572/api/v1\"\n", + "\n", + "# Auto-compute all metrics\n", + "resp = requests.post(f\"{BASE_URL}/monitoring/auto_compute\", json={\n", + " \"project\": \"monitoring_demo\",\n", + "})\n", + "print(resp.json())\n", + "\n", + "# Read per-feature metrics\n", + "resp = requests.get(f\"{BASE_URL}/monitoring/metrics/features\", params={\n", + " \"project\": \"monitoring_demo\",\n", + " \"feature_view_name\": \"driver_stats\",\n", + " \"feature_name\": \"conv_rate\",\n", + " \"granularity\": \"daily\",\n", + " \"data_source_type\": \"batch\",\n", + "})\n", + "print(resp.json())\n", + "\n", + "# Read baseline\n", + "resp = requests.get(f\"{BASE_URL}/monitoring/metrics/baseline\", params={\n", + " \"project\": \"monitoring_demo\",\n", + " \"feature_view_name\": \"driver_stats\",\n", + " \"feature_name\": \"conv_rate\",\n", + " \"data_source_type\": \"batch\",\n", + "})\n", + "print(resp.json())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 11: Monitoring Feature Serving Logs\n", + "\n", + "If your feature service has logging enabled, you can compute metrics from actual production traffic.\n", + "\n", + "### Define a feature service with logging" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "See the code cell above for the logging config pattern.\n", + "Once applied, log metrics can be computed with:\n", + " CLI: feast monitor run --source-type log\n", + " API: POST /monitoring/compute/log\n", + " SDK: monitoring.compute_log_metrics(project, feature_service_name)\n" + ] + } + ], + "source": [ + "# Example feature service definition with logging\n", + "#\n", + "# from feast import FeatureService, LoggingConfig\n", + "# from feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source import (\n", + "# PostgreSQLLoggingDestination,\n", + "# )\n", + "#\n", + "# driver_service = FeatureService(\n", + "# name=\"driver_service\",\n", + "# features=[driver_stats_fv],\n", + "# logging_config=LoggingConfig(\n", + "# destination=PostgreSQLLoggingDestination(table_name=\"feast_driver_logs\"),\n", + "# sample_rate=1.0,\n", + "# ),\n", + "# )\n", + "print(\"See the code cell above for the logging config pattern.\")\n", + "print(\"Once applied, log metrics can be computed with:\")\n", + "print(\" CLI: feast monitor run --source-type log\")\n", + "print(\" API: POST /monitoring/compute/log\")\n", + "print(\" SDK: monitoring.compute_log_metrics(project, feature_service_name)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Compute log metrics (SDK)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment when you have a feature service with logging enabled\n", + "#\n", + "# result = monitoring.compute_log_metrics(\n", + "# project=\"monitoring_demo\",\n", + "# feature_service_name=\"driver_service\",\n", + "# granularity=\"daily\",\n", + "# )\n", + "# print(result)\n", + "\n", + "# Or auto-compute all log metrics\n", + "# result = monitoring.auto_compute_log_metrics(project=\"monitoring_demo\")\n", + "# print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Read log vs. batch metrics side-by-side" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Uncomment the cell above once log metrics have been computed.\n" + ] + } + ], + "source": [ + "# Compare batch vs. log metrics for the same feature\n", + "#\n", + "# batch = monitoring.get_feature_metrics(\n", + "# project=\"monitoring_demo\",\n", + "# feature_view_name=\"driver_stats\",\n", + "# feature_name=\"conv_rate\",\n", + "# data_source_type=\"batch\",\n", + "# granularity=\"daily\",\n", + "# )\n", + "#\n", + "# log = monitoring.get_feature_metrics(\n", + "# project=\"monitoring_demo\",\n", + "# feature_view_name=\"driver_stats\",\n", + "# feature_name=\"conv_rate\",\n", + "# data_source_type=\"log\",\n", + "# granularity=\"daily\",\n", + "# )\n", + "#\n", + "# print(\"Batch metrics:\")\n", + "# for m in batch[:3]:\n", + "# print(f\" {m['metric_date']}: mean={m['mean']:.4f}\")\n", + "#\n", + "# print(\"\\nLog metrics:\")\n", + "# for m in log[:3]:\n", + "# print(f\" {m['metric_date']}: mean={m['mean']:.4f}\")\n", + "\n", + "print(\"Uncomment the cell above once log metrics have been computed.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 12: Scheduling in Production\n", + "\n", + "### Cron (simplest)\n", + "\n", + "```bash\n", + "# Compute all batch + log metrics daily at 2 AM\n", + "0 2 * * * cd /path/to/feast/repo && feast monitor run --source-type all >> /var/log/feast-monitor.log 2>&1\n", + "```\n", + "\n", + "### Airflow\n", + "\n", + "```python\n", + "from airflow.operators.bash import BashOperator\n", + "\n", + "monitor_task = BashOperator(\n", + " task_id=\"feast_monitor\",\n", + " bash_command=\"feast monitor run --source-type all\",\n", + " cwd=\"/path/to/feast/repo\",\n", + ")\n", + "```\n", + "\n", + "### Kubernetes CronJob\n", + "\n", + "```yaml\n", + "apiVersion: batch/v1\n", + "kind: CronJob\n", + "metadata:\n", + " name: feast-monitor\n", + "spec:\n", + " schedule: \"0 2 * * *\"\n", + " jobTemplate:\n", + " spec:\n", + " template:\n", + " spec:\n", + " containers:\n", + " - name: feast-monitor\n", + " image: feast-image:latest\n", + " command: [\"feast\", \"monitor\", \"run\", \"--source-type\", \"all\"]\n", + " volumeMounts:\n", + " - name: feast-repo\n", + " mountPath: /feast/repo\n", + " restartPolicy: OnFailure\n", + " volumes:\n", + " - name: feast-repo\n", + " configMap:\n", + " name: feast-repo-config\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Capability | CLI | REST API | SDK |\n", + "|-----------|-----|----------|-----|\n", + "| Auto-compute (all granularities) | `feast monitor run` | `POST /monitoring/auto_compute` | `monitoring.auto_compute_metrics()` |\n", + "| Targeted compute | `feast monitor run --feature-view X --granularity daily` | `POST /monitoring/compute` | `monitoring.compute_metrics()` |\n", + "| Set baseline | `feast monitor run --set-baseline` | `POST /monitoring/compute` (with `set_baseline: true`) | `monitoring.compute_metrics(set_baseline=True)` |\n", + "| Log metrics | `feast monitor run --source-type log` | `POST /monitoring/compute/log` | `monitoring.compute_log_metrics()` |\n", + "| On-demand exploration | — | `POST /monitoring/compute/transient` | `monitoring.compute_transient()` |\n", + "| Read metrics | — | `GET /monitoring/metrics/*` | `monitoring.get_feature_metrics()` etc. |\n", + "| Read baseline | — | `GET /monitoring/metrics/baseline` | `monitoring.get_baseline()` |\n", + "| Time-series | — | `GET /monitoring/metrics/timeseries` | `monitoring.get_timeseries()` |" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv312", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/online_store/aerospike_overrides_and_hooks/README.md b/examples/online_store/aerospike_overrides_and_hooks/README.md new file mode 100644 index 00000000000..7143b6a3dff --- /dev/null +++ b/examples/online_store/aerospike_overrides_and_hooks/README.md @@ -0,0 +1,65 @@ +# Aerospike: per-feature-view overrides + prewriting hooks + +A short companion to [`docs/reference/online-stores/aerospike.md`](../../../docs/reference/online-stores/aerospike.md) +demonstrating three deployment patterns the Aerospike online store supports +without needing any Feast extension code: + +1. **Per-feature-view namespace overrides** — pin one view to a RAM-only + namespace and another to an SSD-backed one without splitting the project. +2. **Per-feature-view set overrides** — isolate one view in its own set so + `feast apply` deletions or admin truncates only touch that view. +3. **Prewriting hooks** — apply a project-wide write-side transformation + (PII masking in this example) without sprinkling it through every + materialization job. + +Nothing here is Aerospike-specific *infrastructure* — it's all configured +in `feature_store.yaml`. This directory only adds the hook-target Python +module the YAML references. + +## Files + +| file | purpose | +|---|---| +| [`hooks.py`](hooks.py) | A pure-Python prewriting-hook module containing `hash_pii_string_features`, the same example used in the docs. Drop into any module on the writer's `PYTHONPATH`. | +| [`feature_store.yaml`](feature_store.yaml) | Reference `online_store` block showing all three features wired together. Copy the `online_store` section into your own `feature_store.yaml` — the rest is project-specific scaffolding. | + +## Prerequisites + +* Feast installed with the Aerospike extra (`pip install 'feast[aerospike]'`). +* An Aerospike cluster reachable from your writer process. The + [Aerospike online-store reference](../../../docs/reference/online-stores/aerospike.md) + shows a minimal local CE config (`127.0.0.1:3000`); run Aerospike however + you normally would (Docker, Kubernetes, bare metal). +* On every process that calls `online_write_batch` through this store + (materialization workers, the registry CLI host, the feature server if + you run one), the `FEAST_PII_SALT` environment variable must be set + before the first write — `hash_pii_string_features` raises rather than + silently writing plaintext if the salt isn't configured. +* The two namespaces referenced by `namespace_overrides` (`feast_ram` and + `feast_ssd` in the sample YAML) must already exist on the Aerospike + cluster — Aerospike cannot create namespaces at runtime. + +## Trying it out + +1. Drop `hooks.py` into a module on your `PYTHONPATH` that the writer + process can import (e.g. inside your existing feature-repo package). + The example uses the qualified path + `examples.online_store.aerospike_overrides_and_hooks.hooks.hash_pii_string_features`. +2. Copy the `online_store:` block from `feature_store.yaml` into your + own feature repo, adjusting hosts / namespaces / the hook import path + for your project. +3. `export FEAST_PII_SALT=...` (anything random and stable across + processes — rotate by re-running materialization with a new salt). +4. `feast apply` — the new config is registered. +5. Materialize as usual — the hook runs once per `online_write_batch`, + and any feature named `email`, `phone_number` or `ssn` lands in + Aerospike as a salted SHA-256 hex digest instead of plaintext. + +## Read-side note + +Prewriting hooks are **only** invoked on the write path. If your hook is +a one-way transform (hashing, encryption-without-decryption-key) you +have to apply the same transform to the candidate value at read time +yourself. Two-way transforms (deterministic encryption, Base64) need a +matching post-read step in your serving code; the Aerospike store does +not currently expose a symmetric "postreading hook". diff --git a/examples/online_store/aerospike_overrides_and_hooks/feature_store.yaml b/examples/online_store/aerospike_overrides_and_hooks/feature_store.yaml new file mode 100644 index 00000000000..bd1061f30aa --- /dev/null +++ b/examples/online_store/aerospike_overrides_and_hooks/feature_store.yaml @@ -0,0 +1,59 @@ +# Reference feature_store.yaml demonstrating all three Aerospike +# extension points wired together. Copy the `online_store:` block into +# your own feature repo and adjust hosts / namespaces / hook import +# path for your project. +# +# Prerequisites: +# - The `feast_ram` and `feast_ssd` namespaces must already exist on +# the Aerospike cluster. Aerospike cannot create namespaces at +# runtime; a missing namespace surfaces as AEROSPIKE_ERR_PARAM on +# the first read or write touching that view. +# - `FEAST_PII_SALT` must be set in every process that calls +# online_write_batch through this store. + +project: my_feature_repo +registry: data/registry.db +provider: local + +online_store: + type: aerospike + + hosts: + - ["aerospike.internal", 3000] + + # Store-level defaults. Anything not listed in *_overrides below + # falls back to these. + namespace: feast + set_name_template: "{project}_{collection_suffix}" + + # Pin individual feature views to different namespaces -- typically + # one in-memory namespace for hot, latency-sensitive views and one + # device-backed namespace for cold, wide views. + namespace_overrides: + driver_realtime_stats: feast_ram + driver_history_lookup: feast_ssd + + # Isolate one feature view in its own set so that admin operations on + # it (truncate, scan-based deletion via `feast apply`) do not touch + # the records of other views. + set_overrides: + isolated_view: my_feature_repo_isolated + + # Project-wide write-side hook. The store dynamically imports the + # callable on first use and caches it. Adjust the import path to + # whatever module is on your writers' PYTHONPATH; the value below + # assumes you have the example folder on PYTHONPATH from the + # repository root. + prewriting_hook: examples.online_store.aerospike_overrides_and_hooks.hooks.hash_pii_string_features + + # Standard timing knobs (optional -- shown for completeness). + ttl_seconds: 86400 + read_timeout_ms: 150 + write_timeout_ms: 300 + batch_total_timeout_ms: 500 + socket_timeout_ms: 50 + max_retries: 2 + +# Offline store / entity_key_serialization_version / etc. are +# project-specific and intentionally omitted; this file is a snippet, +# not a runnable repo. diff --git a/examples/online_store/aerospike_overrides_and_hooks/hooks.py b/examples/online_store/aerospike_overrides_and_hooks/hooks.py new file mode 100644 index 00000000000..15e6f8a10c7 --- /dev/null +++ b/examples/online_store/aerospike_overrides_and_hooks/hooks.py @@ -0,0 +1,109 @@ +"""Sample prewriting hooks for the Feast Aerospike online store. + +Reference the callable from ``feature_store.yaml`` via its import string, +e.g.:: + + online_store: + type: aerospike + ... + prewriting_hook: examples.online_store.aerospike_overrides_and_hooks.hooks.hash_pii_string_features + +The Aerospike online store invokes the configured callable once per +``online_write_batch`` call, passing the rows about to be written. The +callable must return a row list with the same schema. Returning ``[]`` +short-circuits the write — same path as an empty input, no wire call is +issued. +""" + +from __future__ import annotations + +import hashlib +import os +from datetime import datetime +from typing import List, Optional, Tuple + +from feast import FeatureView +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import RepoConfig + +# Names of features that must never reach the online store as plaintext. +# Match is by exact feature name; tweak to your project's conventions +# (regex, suffix-based, FV-tag-driven, etc.). +_SENSITIVE_FEATURES = frozenset({"email", "phone_number", "ssn"}) + +# Type alias for the per-row payload Feast hands to ``online_write_batch``. +WriteRow = Tuple[ + EntityKeyProto, + dict, + datetime, + Optional[datetime], +] + + +def hash_pii_string_features( + config: RepoConfig, + table: FeatureView, + data: List[WriteRow], +) -> List[WriteRow]: + """Replace any sensitive string feature with a salted SHA-256 hex digest. + + Determinism: same plaintext + same ``FEAST_PII_SALT`` → same digest. + Downstream lookups that hash the candidate value the same way still + hit; lookups against the raw plaintext silently miss. + + Safety: an unset salt raises rather than falling back to plaintext. + Set ``FEAST_PII_SALT`` on every process that materialises features + (workers, registry CLI host, feature server). + """ + salt = os.environ.get("FEAST_PII_SALT") + if salt is None: + raise RuntimeError( + "FEAST_PII_SALT is not set; refusing to write feature batches " + "without a configured PII salt." + ) + salt_bytes = salt.encode("utf-8") + + def _digest(plaintext: str) -> str: + h = hashlib.sha256() + h.update(salt_bytes) + h.update(plaintext.encode("utf-8")) + return h.hexdigest() + + transformed: List[WriteRow] = [] + for entity_key, values, event_ts, created_ts in data: + new_values = dict(values) + for feature_name in _SENSITIVE_FEATURES.intersection(new_values): + v: ValueProto = new_values[feature_name] + if v.HasField("string_val") and v.string_val: + new_values[feature_name] = ValueProto(string_val=_digest(v.string_val)) + transformed.append((entity_key, new_values, event_ts, created_ts)) + return transformed + + +def drop_rows_with_negative_amounts( + config: RepoConfig, + table: FeatureView, + data: List[WriteRow], +) -> List[WriteRow]: + """Defensive sample hook: filter rows whose ``amount`` feature is < 0. + + Demonstrates that hooks can also *remove* rows. Returning an empty + list short-circuits the wire call entirely — useful for emergency + feature-write quarantines without a code deploy. + """ + keep: List[WriteRow] = [] + for entity_key, values, event_ts, created_ts in data: + amount: Optional[ValueProto] = values.get("amount") + if ( + amount is not None + and amount.HasField("double_val") + and amount.double_val < 0 + ): + continue + if amount is not None and amount.HasField("float_val") and amount.float_val < 0: + continue + if amount is not None and amount.HasField("int64_val") and amount.int64_val < 0: + continue + keep.append((entity_key, values, event_ts, created_ts)) + return keep diff --git a/examples/online_store/milvus_tutorial/milvus_example.py b/examples/online_store/milvus_tutorial/milvus_example.py index dc5cb646c60..c8889efdf0f 100644 --- a/examples/online_store/milvus_tutorial/milvus_example.py +++ b/examples/online_store/milvus_tutorial/milvus_example.py @@ -32,39 +32,71 @@ def generate_sample_data(): # Sample product data products = [ - {"id": 1, "name": "Smartphone", - "description": "A high-end smartphone with advanced camera features and long battery life."}, - {"id": 2, "name": "Laptop", - "description": "Powerful laptop with fast processor and high-resolution display for professional use."}, - {"id": 3, "name": "Headphones", - "description": "Wireless noise-cancelling headphones with premium sound quality."}, - {"id": 4, "name": "Smartwatch", - "description": "Fitness tracking smartwatch with heart rate monitoring and sleep analysis."}, - {"id": 5, "name": "Tablet", - "description": "Lightweight tablet with vibrant display perfect for reading and browsing."}, - {"id": 6, "name": "Camera", - "description": "Professional digital camera with high-resolution sensor and interchangeable lenses."}, - {"id": 7, "name": "Speaker", - "description": "Bluetooth speaker with rich bass and long battery life for outdoor use."}, - {"id": 8, "name": "Gaming Console", - "description": "Next-generation gaming console with 4K graphics and fast loading times."}, - {"id": 9, "name": "E-reader", - "description": "E-ink display reader with backlight for comfortable reading in any lighting condition."}, - {"id": 10, "name": "Smart TV", - "description": "4K smart television with built-in streaming apps and voice control."} + { + "id": 1, + "name": "Smartphone", + "description": "A high-end smartphone with advanced camera features and long battery life.", + }, + { + "id": 2, + "name": "Laptop", + "description": "Powerful laptop with fast processor and high-resolution display for professional use.", + }, + { + "id": 3, + "name": "Headphones", + "description": "Wireless noise-cancelling headphones with premium sound quality.", + }, + { + "id": 4, + "name": "Smartwatch", + "description": "Fitness tracking smartwatch with heart rate monitoring and sleep analysis.", + }, + { + "id": 5, + "name": "Tablet", + "description": "Lightweight tablet with vibrant display perfect for reading and browsing.", + }, + { + "id": 6, + "name": "Camera", + "description": "Professional digital camera with high-resolution sensor and interchangeable lenses.", + }, + { + "id": 7, + "name": "Speaker", + "description": "Bluetooth speaker with rich bass and long battery life for outdoor use.", + }, + { + "id": 8, + "name": "Gaming Console", + "description": "Next-generation gaming console with 4K graphics and fast loading times.", + }, + { + "id": 9, + "name": "E-reader", + "description": "E-ink display reader with backlight for comfortable reading in any lighting condition.", + }, + { + "id": 10, + "name": "Smart TV", + "description": "4K smart television with built-in streaming apps and voice control.", + }, ] # Create DataFrame df = pd.DataFrame(products) # Generate embeddings using sentence-transformers - model = SentenceTransformer('all-MiniLM-L6-v2') # Small, fast model with 384-dim embeddings - embeddings = model.encode(df['description'].tolist()) + model = SentenceTransformer( + "all-MiniLM-L6-v2" + ) # Small, fast model with 384-dim embeddings + embeddings = model.encode(df["description"].tolist()) # Add embeddings and timestamp to DataFrame - df['embedding'] = embeddings.tolist() - df['event_timestamp'] = datetime.now() - timedelta(days=1) - df['created_timestamp'] = datetime.now() - timedelta(days=1) + df["embedding"] = embeddings.tolist() + df["event_timestamp"] = datetime.now() - timedelta(days=1) + df["created_timestamp"] = datetime.now() - timedelta(days=1) # Save to parquet file parquet_path = "data/sample_data.parquet" @@ -135,16 +167,20 @@ def perform_similarity_search(store, query_text: str, top_k: int = 3): print(f"\nPerforming similarity search for: '{query_text}'") # Generate embedding for query text - model = SentenceTransformer('all-MiniLM-L6-v2') + model = SentenceTransformer("all-MiniLM-L6-v2") query_embedding = model.encode(query_text).tolist() # Perform similarity search using vector embeddings with version 2 API try: results = store.retrieve_online_documents_v2( - features=["product_embeddings:embedding", "product_embeddings:name", "product_embeddings:description"], + features=[ + "product_embeddings:embedding", + "product_embeddings:name", + "product_embeddings:description", + ], query=query_embedding, top_k=top_k, - distance_metric="L2" + distance_metric="L2", ).to_df() # Print results @@ -184,7 +220,9 @@ def main(): perform_similarity_search(store, "portable computing device for work", top_k=3) print("\n=== Tutorial Complete ===") - print("You've successfully set up Milvus with Feast and performed vector similarity searches!") + print( + "You've successfully set up Milvus with Feast and performed vector similarity searches!" + ) if __name__ == "__main__": diff --git a/examples/online_store/pgvector_tutorial/pgvector_example.py b/examples/online_store/pgvector_tutorial/pgvector_example.py index c8236ea4a7b..94c6e079952 100644 --- a/examples/online_store/pgvector_tutorial/pgvector_example.py +++ b/examples/online_store/pgvector_tutorial/pgvector_example.py @@ -4,12 +4,9 @@ # as a vector database backend for Feast. import os -import numpy as np import pandas as pd from datetime import datetime, timedelta -from typing import List, Optional import subprocess -import time # For generating embeddings try: @@ -21,53 +18,97 @@ from feast import FeatureStore, Entity, FeatureView, Field, FileSource from feast.data_format import ParquetFormat -from feast.types import Float32, Array, String, Int64 +from feast.types import Float32, Array, String from feast.value_type import ValueType # Create data directory if it doesn't exist os.makedirs("data", exist_ok=True) + # Step 1: Generate sample data with embeddings def generate_sample_data(): print("Generating sample data with embeddings...") - + # Sample product data products = [ - {"id": 1, "name": "Smartphone", "description": "A high-end smartphone with advanced camera features and long battery life."}, - {"id": 2, "name": "Laptop", "description": "Powerful laptop with fast processor and high-resolution display for professional use."}, - {"id": 3, "name": "Headphones", "description": "Wireless noise-cancelling headphones with premium sound quality."}, - {"id": 4, "name": "Smartwatch", "description": "Fitness tracking smartwatch with heart rate monitoring and sleep analysis."}, - {"id": 5, "name": "Tablet", "description": "Lightweight tablet with vibrant display perfect for reading and browsing."}, - {"id": 6, "name": "Camera", "description": "Professional digital camera with high-resolution sensor and interchangeable lenses."}, - {"id": 7, "name": "Speaker", "description": "Bluetooth speaker with rich bass and long battery life for outdoor use."}, - {"id": 8, "name": "Gaming Console", "description": "Next-generation gaming console with 4K graphics and fast loading times."}, - {"id": 9, "name": "E-reader", "description": "E-ink display reader with backlight for comfortable reading in any lighting condition."}, - {"id": 10, "name": "Smart TV", "description": "4K smart television with built-in streaming apps and voice control."} + { + "id": 1, + "name": "Smartphone", + "description": "A high-end smartphone with advanced camera features and long battery life.", + }, + { + "id": 2, + "name": "Laptop", + "description": "Powerful laptop with fast processor and high-resolution display for professional use.", + }, + { + "id": 3, + "name": "Headphones", + "description": "Wireless noise-cancelling headphones with premium sound quality.", + }, + { + "id": 4, + "name": "Smartwatch", + "description": "Fitness tracking smartwatch with heart rate monitoring and sleep analysis.", + }, + { + "id": 5, + "name": "Tablet", + "description": "Lightweight tablet with vibrant display perfect for reading and browsing.", + }, + { + "id": 6, + "name": "Camera", + "description": "Professional digital camera with high-resolution sensor and interchangeable lenses.", + }, + { + "id": 7, + "name": "Speaker", + "description": "Bluetooth speaker with rich bass and long battery life for outdoor use.", + }, + { + "id": 8, + "name": "Gaming Console", + "description": "Next-generation gaming console with 4K graphics and fast loading times.", + }, + { + "id": 9, + "name": "E-reader", + "description": "E-ink display reader with backlight for comfortable reading in any lighting condition.", + }, + { + "id": 10, + "name": "Smart TV", + "description": "4K smart television with built-in streaming apps and voice control.", + }, ] - + # Create DataFrame df = pd.DataFrame(products) - + # Generate embeddings using sentence-transformers - model = SentenceTransformer('all-MiniLM-L6-v2') # Small, fast model with 384-dim embeddings - embeddings = model.encode(df['description'].tolist()) - + model = SentenceTransformer( + "all-MiniLM-L6-v2" + ) # Small, fast model with 384-dim embeddings + embeddings = model.encode(df["description"].tolist()) + # Add embeddings and timestamp to DataFrame - df['embedding'] = embeddings.tolist() - df['event_timestamp'] = datetime.now() - timedelta(days=1) - df['created_timestamp'] = datetime.now() - timedelta(days=1) - + df["embedding"] = embeddings.tolist() + df["event_timestamp"] = datetime.now() - timedelta(days=1) + df["created_timestamp"] = datetime.now() - timedelta(days=1) + # Save to parquet file parquet_path = "data/sample_data.parquet" df.to_parquet(parquet_path, index=False) - + print(f"Sample data saved to {parquet_path}") return parquet_path + # Step 2: Define feature repository def create_feature_definitions(data_path): print("Creating feature definitions...") - + # Define entity product = Entity( name="product_id", @@ -75,7 +116,7 @@ def create_feature_definitions(data_path): join_keys=["id"], value_type=ValueType.INT64, ) - + # Define data source source = FileSource( file_format=ParquetFormat(), @@ -83,7 +124,7 @@ def create_feature_definitions(data_path): timestamp_field="event_timestamp", created_timestamp_column="created_timestamp", ) - + # Define feature view with vector embeddings product_embeddings = FeatureView( name="product_embeddings", @@ -91,10 +132,10 @@ def create_feature_definitions(data_path): ttl=timedelta(days=30), schema=[ Field( - name="embedding", - dtype=Array(Float32), + name="embedding", + dtype=Array(Float32), vector_index=True, # Mark as vector field - vector_search_metric="L2" # Use L2 distance for similarity + vector_search_metric="L2", # Use L2 distance for similarity ), Field(name="name", dtype=String), Field(name="description", dtype=String), @@ -102,106 +143,152 @@ def create_feature_definitions(data_path): source=source, online=True, ) - + return product, product_embeddings + # Step 3: Initialize and apply feature store def setup_feature_store(product, product_embeddings): print("Setting up feature store...") - + # Initialize feature store store = FeatureStore(repo_path=".") - + # Apply feature definitions store.apply([product, product_embeddings]) - + # Materialize features to online store store.materialize( start_date=datetime.now() - timedelta(days=2), end_date=datetime.now(), ) - + print("Feature store setup complete") return store + # Step 4: Perform vector similarity search def perform_similarity_search(store, query_text: str, top_k: int = 3): print(f"\nPerforming similarity search for: '{query_text}'") - + # Generate embedding for query text - model = SentenceTransformer('all-MiniLM-L6-v2') + model = SentenceTransformer("all-MiniLM-L6-v2") query_embedding = model.encode(query_text).tolist() - + # Perform similarity search using vector embeddings results = store.retrieve_online_documents( query=query_embedding, features=["product_embeddings:embedding"], top_k=top_k, - distance_metric="L2" + distance_metric="L2", ) - + # Extract product IDs from the results by parsing entity keys # (The entities are encoded in a way that's not directly accessible) - + print(f"\nTop {top_k} similar products:") print("Available fields:", list(results.to_dict().keys())) - + # Since we can't access the entity keys directly, let's do a manual search # to show the top similar products based on our search query - + # Get top 5 products sorted by relevance to our query (manual approach) products = [ - {"id": 3, "name": "Headphones", "description": "Wireless noise-cancelling headphones with premium sound quality."}, - {"id": 7, "name": "Speaker", "description": "Bluetooth speaker with rich bass and long battery life for outdoor use."}, - {"id": 2, "name": "Laptop", "description": "Powerful laptop with fast processor and high-resolution display for professional use."}, - {"id": 5, "name": "Tablet", "description": "Lightweight tablet with vibrant display perfect for reading and browsing."}, - {"id": 1, "name": "Smartphone", "description": "A high-end smartphone with advanced camera features and long battery life."}, + { + "id": 3, + "name": "Headphones", + "description": "Wireless noise-cancelling headphones with premium sound quality.", + }, + { + "id": 7, + "name": "Speaker", + "description": "Bluetooth speaker with rich bass and long battery life for outdoor use.", + }, + { + "id": 2, + "name": "Laptop", + "description": "Powerful laptop with fast processor and high-resolution display for professional use.", + }, + { + "id": 5, + "name": "Tablet", + "description": "Lightweight tablet with vibrant display perfect for reading and browsing.", + }, + { + "id": 1, + "name": "Smartphone", + "description": "A high-end smartphone with advanced camera features and long battery life.", + }, ] - + # Filter based on the search query - if "wireless" in query_text.lower() or "audio" in query_text.lower() or "sound" in query_text.lower(): - relevant = [products[0], products[1], products[4]] # Headphones, Speaker, Smartphone - elif "portable" in query_text.lower() or "computing" in query_text.lower() or "work" in query_text.lower(): + if ( + "wireless" in query_text.lower() + or "audio" in query_text.lower() + or "sound" in query_text.lower() + ): + relevant = [ + products[0], + products[1], + products[4], + ] # Headphones, Speaker, Smartphone + elif ( + "portable" in query_text.lower() + or "computing" in query_text.lower() + or "work" in query_text.lower() + ): relevant = [products[2], products[4], products[3]] # Laptop, Smartphone, Tablet else: relevant = products[:3] # Just show first 3 - + # Display results for i, product in enumerate(relevant[:top_k], 1): print(f"\n{i}. Name: {product['name']}") print(f" Description: {product['description']}") - + print("\nNote: Using simulated results for display purposes.") - print("The vector search is working, but the result structure in this Feast version") - print("doesn't allow easy access to the entity keys to retrieve the product details.") + print( + "The vector search is working, but the result structure in this Feast version" + ) + print( + "doesn't allow easy access to the entity keys to retrieve the product details." + ) + # Main function to run the example def main(): print("=== PGVector Tutorial with Feast ===") - + # Check if PostgreSQL with pgvector is running print("\nEnsure PostgreSQL with pgvector is running:") - print("docker run -d \\\n --name postgres-pgvector \\\n -e POSTGRES_USER=feast \\\n -e POSTGRES_PASSWORD=feast \\\n -e POSTGRES_DB=feast \\\n -p 5432:5432 \\\n pgvector/pgvector:pg16") + print( + "docker run -d \\\n --name postgres-pgvector \\\n -e POSTGRES_USER=feast \\\n -e POSTGRES_PASSWORD=feast \\\n -e POSTGRES_DB=feast \\\n -p 5432:5432 \\\n pgvector/pgvector:pg16" + ) print("\nEnsure pgvector extension is created:") - print("docker exec -it postgres-pgvector psql -U feast -c \"CREATE EXTENSION IF NOT EXISTS vector;\"") - + print( + 'docker exec -it postgres-pgvector psql -U feast -c "CREATE EXTENSION IF NOT EXISTS vector;"' + ) + input("\nPress Enter to continue once PostgreSQL with pgvector is ready...") - + # Generate sample data data_path = generate_sample_data() - + # Create feature definitions product, product_embeddings = create_feature_definitions(data_path) - + # Setup feature store store = setup_feature_store(product, product_embeddings) - + # Perform similarity searches perform_similarity_search(store, "wireless audio device with good sound", top_k=3) perform_similarity_search(store, "portable computing device for work", top_k=3) - + print("\n=== Tutorial Complete ===") - print("You've successfully set up pgvector with Feast and performed vector similarity searches!") + print( + "You've successfully set up pgvector with Feast and performed vector similarity searches!" + ) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/openlineage-integration/README.md b/examples/openlineage-integration/README.md new file mode 100644 index 00000000000..43dc0d29da8 --- /dev/null +++ b/examples/openlineage-integration/README.md @@ -0,0 +1,58 @@ +# Feast OpenLineage Integration Example + +This example demonstrates Feast's **native OpenLineage integration** for automatic data lineage tracking. + +For full documentation, see the [OpenLineage Reference](../../docs/reference/openlineage.md). + +## Prerequisites + +```bash +pip install feast[openlineage] +``` + +## Running the Demo + +1. Start Marquez: +```bash +docker run -p 5000:5000 -p 3000:3000 marquezproject/marquez +``` + +2. Run the demo: +```bash +python openlineage_demo.py --url http://localhost:5000 +``` + +3. View lineage at http://localhost:3000 + +## What the Demo Shows + +The demo creates a sample feature repository and demonstrates: + +- **Entity**: `driver_id` +- **DataSource**: `driver_stats_source` (Parquet file) +- **FeatureView**: `driver_hourly_stats` with features like conversion rate, acceptance rate +- **FeatureService**: `driver_stats_service` aggregating features + +When you run the demo, it will: +1. Create the feature store with OpenLineage enabled +2. Apply the features (emits lineage events) +3. Materialize features (emits START/COMPLETE events) +4. Retrieve features (demonstrates online feature retrieval) + +## Lineage Graph + +After running the demo, you'll see this lineage in Marquez: + +``` +driver_stats_source ──┐ + ├──→ feast_feature_views_openlineage_demo ──→ driver_hourly_stats +driver_id ────────────┘ │ + ▼ + feature_service_driver_stats_service ──→ driver_stats_service +``` + +## Learn More + +- [Feast OpenLineage Reference](../../docs/reference/openlineage.md) +- [OpenLineage Documentation](https://openlineage.io/docs) +- [Marquez Project](https://marquezproject.ai) diff --git a/examples/openlineage-integration/openlineage_demo.py b/examples/openlineage-integration/openlineage_demo.py new file mode 100644 index 00000000000..fb02136eeb1 --- /dev/null +++ b/examples/openlineage-integration/openlineage_demo.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Feast OpenLineage Native Integration Demo + +This demo shows how Feast's native OpenLineage integration works. +When OpenLineage is enabled in feature_store.yaml, lineage events +are emitted automatically - no code changes required! + +Usage: + python openlineage_demo.py --url http://localhost:5000 +""" + +import argparse +import tempfile +from datetime import datetime, timedelta +from pathlib import Path +import pandas as pd +from feast import FeatureStore +from feast import Entity, FeatureService, FeatureView, FileSource, Field +from feast.types import Float32, Int64 + + +def create_feature_store_yaml(url: str) -> str: + """Create a feature_store.yaml with OpenLineage configuration.""" + return f"""project: openlineage_demo +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db + +openlineage: + enabled: true + transport_type: http + transport_url: {url} + transport_endpoint: api/v1/lineage + namespace: feast + emit_on_apply: true + emit_on_materialize: true +""" + + +def run_demo(url: str): + """Run the OpenLineage integration demo.""" + print("Feast OpenLineage Native Integration Demo") + + # Create temporary directory for the demo + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + data_dir = repo_path / "data" + data_dir.mkdir(exist_ok=True) + + # Create feature_store.yaml with OpenLineage configuration + feature_store_yaml = create_feature_store_yaml(url) + (repo_path / "feature_store.yaml").write_text(feature_store_yaml) + + print(f"Created demo repository at: {repo_path}") + print("feature_store.yaml:") + print("-" * 50) + print(feature_store_yaml) + print("-" * 50) + + try: + import openlineage.client # noqa: F401 + except ImportError: + print("OpenLineage client not installed.") + print("Install with: pip install openlineage-python") + raise ImportError("OpenLineage client not installed") + + fs = FeatureStore(repo_path=str(repo_path)) + driver_stats_df = pd.DataFrame( + { + "driver_id": [1001, 1002, 1003, 1001, 1002, 1003], + "conv_rate": [0.85, 0.72, 0.91, 0.87, 0.75, 0.89], + "acc_rate": [0.95, 0.88, 0.97, 0.94, 0.90, 0.96], + "avg_daily_trips": [12, 8, 15, 14, 9, 16], + "event_timestamp": pd.to_datetime( + [ + "2024-01-01 10:00:00", + "2024-01-01 10:00:00", + "2024-01-01 10:00:00", + "2024-01-02 10:00:00", + "2024-01-02 10:00:00", + "2024-01-02 10:00:00", + ], + utc=True, + ), + "created": pd.to_datetime(["2024-01-01"] * 6, utc=True), + } + ) + + parquet_path = data_dir / "driver_stats.parquet" + driver_stats_df.to_parquet(str(parquet_path)) + + driver = Entity(name="driver_id", description="Driver identifier") + + driver_stats_source = FileSource( + name="driver_stats_source", + path=str(parquet_path), + timestamp_field="event_timestamp", + created_timestamp_column="created", + description="Driver statistics from data warehouse", + ) + + driver_hourly_stats_view = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32, description="Conversion rate"), + Field(name="acc_rate", dtype=Float32, description="Acceptance rate"), + Field( + name="avg_daily_trips", + dtype=Int64, + description="Average daily trips", + ), + ], + source=driver_stats_source, + description="Hourly driver performance statistics", + tags={"team": "driver_performance", "priority": "high"}, + ) + + driver_stats_service = FeatureService( + name="driver_stats_service", + features=[driver_hourly_stats_view], + description="Driver statistics for real-time scoring", + tags={"use_case": "scoring"}, + ) + + try: + fs.apply( + [ + driver, + driver_stats_source, + driver_hourly_stats_view, + driver_stats_service, + ] + ) + print("Applied entities, feature views, and feature services") + print("OpenLineage events emitted automatically:") + print( + " - feast_feature_views_openlineage_demo (DataSources → FeatureViews)" + ) + print( + " - feature_service_driver_stats_service (FeatureViews → FeatureService)" + ) + except Exception as e: + print(f"Apply failed: {e}") + + start_date = datetime( + 2024, 1, 1, tzinfo=driver_stats_df["event_timestamp"].dt.tz + ) + end_date = datetime(2024, 1, 3, tzinfo=driver_stats_df["event_timestamp"].dt.tz) + fs.materialize(start_date=start_date, end_date=end_date) + + # Retrieve online features (no OpenLineage events emitted for retrieval) + online_features = fs.get_online_features( + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], + entity_rows=[{"driver_id": 1001}, {"driver_id": 1002}], + ) + print(f"Retrieved online features: {online_features.to_dict()}") + print( + """ +The native OpenLineage integration works automatically when configured. + +Lineage Graph Created: + DataSources + Entities → feast_feature_views_{project} → FeatureViews + FeatureViews → feature_service_{name} → FeatureServices + +Key benefits: + - No code changes required + - Just add 'openlineage' section to feature_store.yaml + - All operations emit lineage events automatically + - Feature metadata (tags, descriptions) included in lineage + - Non-blocking and fail-safe (won't break Feast operations) + +View your lineage at: http://localhost:3000 +""" + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Feast OpenLineage Native Integration Demo", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Example: + # Start Marquez first: + docker run -p 5000:5000 -p 3000:3000 marquezproject/marquez + + # Run the demo: + python openlineage_demo.py --url http://localhost:5000 + + # View lineage at http://localhost:3000 +""", + ) + parser.add_argument( + "--url", + required=True, + help="OpenLineage HTTP URL (e.g., http://localhost:5000)", + ) + + args = parser.parse_args() + + run_demo(args.url) + + +if __name__ == "__main__": + main() diff --git a/examples/operator-postgres-tls-demo/02-Install-feast.ipynb b/examples/operator-postgres-tls-demo/02-Install-feast.ipynb index 16948b3610c..ae1795213ab 100644 --- a/examples/operator-postgres-tls-demo/02-Install-feast.ipynb +++ b/examples/operator-postgres-tls-demo/02-Install-feast.ipynb @@ -106,11 +106,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**Option 1: Directly Setting the CA Certificate Path** \n", - "\n", - "In this approach, we specify the CA certificate path directly in the Feast PostgreSQL URL using the `sslrootcert` parameter. \n", - "\n", - "You can refer to the `v1alpha1_featurestore_postgres_db_volumes_tls.yaml` file for the complete configuration details. " + "**Option 1: Directly Setting the CA Certificate Path** \n\nIn this approach, we specify the CA certificate path directly in the Feast PostgreSQL URL using the `sslrootcert` parameter. \n\nYou can refer to the `v1_featurestore_postgres_db_volumes_tls.yaml` file for the complete configuration details. " ] }, { @@ -131,19 +127,14 @@ } ], "source": [ - "!kubectl apply -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_db_volumes_tls.yaml --namespace=feast" + "!kubectl apply -f ../../infra/feast-operator/config/samples/v1_featurestore_postgres_db_volumes_tls.yaml --namespace=feast" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Option 2: Using an Environment Variable for the CA Certificate** \n", - "\n", - "In this approach, you define the CA certificate path as an environment variable. You can refer to the `v1alpha1_featurestore_postgres_tls_volumes_ca_env.yaml` file for the complete configuration details. \n", - "\n", - "```bash\n", - "FEAST_CA_CERT_FILE_PATH=\n" + "**Option 2: Using an Environment Variable for the CA Certificate** \n\nIn this approach, you define the CA certificate path as an environment variable. You can refer to the `v1_featurestore_postgres_tls_volumes_ca_env.yaml` file for the complete configuration details. \n\n```bash\nFEAST_CA_CERT_FILE_PATH=\n" ] }, { @@ -162,7 +153,7 @@ } ], "source": [ - "!kubectl apply -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_tls_volumes_ca_env.yaml --namespace=feast" + "!kubectl apply -f ../../infra/feast-operator/config/samples/v1_featurestore_postgres_tls_volumes_ca_env.yaml --namespace=feast" ] }, { @@ -455,4 +446,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/examples/operator-postgres-tls-demo/03-Uninstall.ipynb b/examples/operator-postgres-tls-demo/03-Uninstall.ipynb index 007b8d7bc1a..1dd0ed831c4 100644 --- a/examples/operator-postgres-tls-demo/03-Uninstall.ipynb +++ b/examples/operator-postgres-tls-demo/03-Uninstall.ipynb @@ -26,13 +26,7 @@ } ], "source": [ - "# If you have choosen the option 1 example earlier.\n", - "!kubectl delete -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_db_volumes_tls.yaml\n", - "\n", - "# If you have choosen the option 2 example earlier.\n", - "!kubectl delete -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_tls_volumes_ca_env.yaml\n", - "\n", - "#!kubectl delete -f ../../infra/feast-operator/dist/install.yaml" + "# If you have choosen the option 1 example earlier.\n!kubectl delete -f ../../infra/feast-operator/config/samples/v1_featurestore_postgres_db_volumes_tls.yaml\n\n# If you have choosen the option 2 example earlier.\n!kubectl delete -f ../../infra/feast-operator/config/samples/v1_featurestore_postgres_tls_volumes_ca_env.yaml\n\n#!kubectl delete -f ../../infra/feast-operator/dist/install.yaml" ] }, { @@ -131,4 +125,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/examples/operator-rbac-openshift-tls/1-setup-operator-rbac.ipynb b/examples/operator-rbac-openshift-tls/1-setup-operator-rbac.ipynb index edfc9f263cf..885a0c49382 100644 --- a/examples/operator-rbac-openshift-tls/1-setup-operator-rbac.ipynb +++ b/examples/operator-rbac-openshift-tls/1-setup-operator-rbac.ipynb @@ -159,8 +159,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Install the Feast services via FeatureStore CR\n", - "Next, we'll use the running Feast Operator to install the feast services with Server components online, offline, registry with kubernetes Authorization set. Apply the included [reference deployment](../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml) to install and configure Feast with kubernetes Authorization ." + "## Install the Feast services via FeatureStore CR\nNext, we'll use the running Feast Operator to install the feast services with Server components online, offline, registry with kubernetes Authorization set. Apply the included [reference deployment](../../infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml) to install and configure Feast with kubernetes Authorization ." ] }, { @@ -202,8 +201,7 @@ } ], "source": [ - "!cat ../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml\n", - "!kubectl apply -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml -n feast" + "!cat ../../infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml\n!kubectl apply -f ../../infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml -n feast" ] }, { @@ -738,4 +736,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/examples/operator-rbac-openshift-tls/3-uninstall.ipynb b/examples/operator-rbac-openshift-tls/3-uninstall.ipynb index 5bfd13d2ef1..52752d32e2d 100644 --- a/examples/operator-rbac-openshift-tls/3-uninstall.ipynb +++ b/examples/operator-rbac-openshift-tls/3-uninstall.ipynb @@ -52,8 +52,7 @@ } ], "source": [ - "!kubectl delete -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml\n", - "!kubectl delete -f ../../infra/feast-operator/dist/install.yaml" + "!kubectl delete -f ../../infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml\n!kubectl delete -f ../../infra/feast-operator/dist/install.yaml" ] }, { @@ -218,4 +217,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/examples/operator-rbac-openshift-tls/client/feature_repo/test.py b/examples/operator-rbac-openshift-tls/client/feature_repo/test.py index 78732327a62..88f7a0e6484 100644 --- a/examples/operator-rbac-openshift-tls/client/feature_repo/test.py +++ b/examples/operator-rbac-openshift-tls/client/feature_repo/test.py @@ -9,6 +9,7 @@ repo_path = os.getenv("FEAST_REPO_PATH", ".") store = FeatureStore(repo_path=repo_path) + def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): """Fetch historical features for training or batch scoring.""" try: @@ -38,13 +39,17 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: "transformed_conv_rate:conv_rate_plus_val2", ], ).to_df() - print(f"Successfully fetched {'batch scoring' if for_batch_scoring else 'training'} historical features:\n", training_df.head()) + print( + f"Successfully fetched {'batch scoring' if for_batch_scoring else 'training'} historical features:\n", + training_df.head(), + ) except PermissionError: print("\n*** PERMISSION DENIED *** Cannot fetch historical features.") except Exception as e: print(f"Unexpected error while fetching historical features: {e}") + def fetch_online_features(store: FeatureStore, source: str = ""): """Fetch online features from the feature store.""" try: @@ -76,7 +81,9 @@ def fetch_online_features(store: FeatureStore, source: str = ""): entity_rows=entity_rows, ).to_dict() - print(f"Successfully fetched online features {'via feature service' if source else 'directly'}:\n") + print( + f"Successfully fetched online features {'via feature service' if source else 'directly'}:\n" + ) for key, value in sorted(returned_features.items()): print(f"{key} : {value}") @@ -85,6 +92,7 @@ def fetch_online_features(store: FeatureStore, source: str = ""): except Exception as e: print(f"Unexpected error while fetching online features: {e}") + def check_permissions(): """Check user role, test various Feast operations.""" feature_views = [] @@ -94,7 +102,9 @@ def check_permissions(): try: feature_views = store.list_feature_views() if not feature_views: - print("No feature views found. You might not have access or they haven't been created.") + print( + "No feature views found. You might not have access or they haven't been created." + ) else: print(f"Successfully listed {len(feature_views)} feature views:") for fv in feature_views: @@ -117,7 +127,9 @@ def check_permissions(): store.apply(feature_views) print("User has write access to the feature store.") except PermissionError: - print("\n*** PERMISSION DENIED *** User lacks permission to modify the feature store.") + print( + "\n*** PERMISSION DENIED *** User lacks permission to modify the feature store." + ) except Exception as e: print(f"Unexpected error testing write access: {e}") @@ -151,5 +163,6 @@ def check_permissions(): except Exception as e: print(f"Unexpected error while pushing event: {e}") + if __name__ == "__main__": check_permissions() diff --git a/examples/operator-rbac-openshift-tls/permissions_apply.py b/examples/operator-rbac-openshift-tls/permissions_apply.py index 0d46ad5260a..d628fa88eb8 100644 --- a/examples/operator-rbac-openshift-tls/permissions_apply.py +++ b/examples/operator-rbac-openshift-tls/permissions_apply.py @@ -5,8 +5,10 @@ from feast.permissions.policy import RoleBasedPolicy # Define K8s roles same as created with FeatureStore CR -admin_roles = ["feast-writer"] # Full access (can create, update, delete ) Feast Resources -user_roles = ["feast-reader"] # Read-only access on Feast Resources +admin_roles = [ + "feast-writer" +] # Full access (can create, update, delete ) Feast Resources +user_roles = ["feast-reader"] # Read-only access on Feast Resources # User permissions (feast_user_permission) # - Grants read and describing Feast objects access @@ -14,7 +16,8 @@ name="feast_user_permission", types=ALL_RESOURCE_TYPES, policy=RoleBasedPolicy(roles=user_roles), - actions=[AuthzedAction.DESCRIBE] + READ # Read access (READ_ONLINE, READ_OFFLINE) + describe other Feast Resources. + actions=[AuthzedAction.DESCRIBE] + + READ, # Read access (READ_ONLINE, READ_OFFLINE) + describe other Feast Resources. ) # Admin permissions (feast_admin_permission) @@ -23,5 +26,5 @@ name="feast_admin_permission", types=ALL_RESOURCE_TYPES, policy=RoleBasedPolicy(roles=admin_roles), - actions=ALL_ACTIONS # Full permissions: CREATE, UPDATE, DELETE, READ, WRITE + actions=ALL_ACTIONS, # Full permissions: CREATE, UPDATE, DELETE, READ, WRITE ) diff --git a/examples/operator-rbac-openshift-tls/permissions_with_groups_namespaces.py b/examples/operator-rbac-openshift-tls/permissions_with_groups_namespaces.py index 5565c612902..d5c17fe1c2b 100644 --- a/examples/operator-rbac-openshift-tls/permissions_with_groups_namespaces.py +++ b/examples/operator-rbac-openshift-tls/permissions_with_groups_namespaces.py @@ -5,28 +5,36 @@ from feast.feast_object import ALL_FEATURE_VIEW_TYPES, ALL_RESOURCE_TYPES from feast.project import Project from feast.entity import Entity -from feast.feature_view import FeatureView -from feast.on_demand_feature_view import OnDemandFeatureView -from feast.batch_feature_view import BatchFeatureView -from feast.stream_feature_view import StreamFeatureView from feast.feature_service import FeatureService from feast.data_source import DataSource from feast.saved_dataset import SavedDataset from feast.permissions.permission import Permission from feast.permissions.action import READ, AuthzedAction, ALL_ACTIONS -from feast.permissions.policy import RoleBasedPolicy, GroupBasedPolicy, NamespaceBasedPolicy, CombinedGroupNamespacePolicy +from feast.permissions.policy import ( + RoleBasedPolicy, + GroupBasedPolicy, + NamespaceBasedPolicy, + CombinedGroupNamespacePolicy, +) -# New Testing +# New Testing -WITHOUT_DATA_SOURCE = [Project, Entity, FeatureService, SavedDataset] + ALL_FEATURE_VIEW_TYPES +WITHOUT_DATA_SOURCE = [ + Project, + Entity, + FeatureService, + SavedDataset, +] + ALL_FEATURE_VIEW_TYPES ONLY_ENTITIES = [Entity] ONLY_DS = [DataSource] # Define K8s roles (existing functionality) -admin_roles = ["feast-writer"] # Full access (can create, update, delete) Feast Resources -user_roles = ["feast-reader"] # Read-only access on Feast Resources +admin_roles = [ + "feast-writer" +] # Full access (can create, update, delete) Feast Resources +user_roles = ["feast-reader"] # Read-only access on Feast Resources # Define groups for different teams data_team_groups = ["data-team", "ml-engineers"] @@ -41,9 +49,14 @@ name="pre_Changed", types=ONLY_ENTITIES, policy=NamespaceBasedPolicy(namespaces=prod_namespaces), - actions=[AuthzedAction.DESCRIBE] + READ + actions=[AuthzedAction.DESCRIBE] + READ, +) +only_ds = Permission( + name="entity_reader", + types=ONLY_DS, + policy=NamespaceBasedPolicy(namespaces=prod_namespaces), + actions=[AuthzedAction.DESCRIBE] + READ, ) -only_ds = Permission(name="entity_reader", types=ONLY_DS, policy=NamespaceBasedPolicy(namespaces=[prod_namespaces]), actions=[AuthzedAction.DESCRIBE] + READ) staging_namespaces = ["staging", "dev"] test_namespaces = ["test", "testing"] @@ -53,7 +66,8 @@ name="feast_user_permission", types=ALL_RESOURCE_TYPES, policy=RoleBasedPolicy(roles=user_roles), - actions=[AuthzedAction.DESCRIBE] + READ # Read access (READ_ONLINE, READ_OFFLINE) + describe other Feast Resources. + actions=[AuthzedAction.DESCRIBE] + + READ, # Read access (READ_ONLINE, READ_OFFLINE) + describe other Feast Resources. ) # Admin permissions (existing functionality) @@ -62,7 +76,7 @@ name="feast_admin_permission", types=ALL_RESOURCE_TYPES, policy=RoleBasedPolicy(roles=admin_roles), - actions=ALL_ACTIONS # Full permissions: CREATE, UPDATE, DELETE, READ, WRITE + actions=ALL_ACTIONS, # Full permissions: CREATE, UPDATE, DELETE, READ, WRITE ) # Group-based permissions (new functionality) @@ -71,7 +85,7 @@ name="data_team_read_permission", types=ALL_RESOURCE_TYPES, policy=GroupBasedPolicy(groups=data_team_groups), - actions=[AuthzedAction.DESCRIBE] + READ + actions=[AuthzedAction.DESCRIBE] + READ, ) # - Grants full access to admin groups @@ -79,7 +93,7 @@ name="admin_group_permission", types=ALL_RESOURCE_TYPES, policy=GroupBasedPolicy(groups=admin_groups), - actions=ALL_ACTIONS + actions=ALL_ACTIONS, ) # Namespace-based permissions (new functionality) @@ -88,7 +102,7 @@ name="production_read_permission", types=ALL_RESOURCE_TYPES, policy=NamespaceBasedPolicy(namespaces=prod_namespaces), - actions=[AuthzedAction.DESCRIBE] + READ + actions=[AuthzedAction.DESCRIBE] + READ, ) # # - Grants full access to staging namespace users @@ -96,7 +110,7 @@ name="staging_full_permission", types=ALL_RESOURCE_TYPES, policy=NamespaceBasedPolicy(namespaces=staging_namespaces), - actions=ALL_ACTIONS + actions=ALL_ACTIONS, ) # # Combined permissions (using combined policy type) @@ -104,14 +118,18 @@ dev_test_perm = Permission( name="dev_test_permission", types=ALL_RESOURCE_TYPES, - policy=CombinedGroupNamespacePolicy(groups=dev_team_groups, namespaces=test_namespaces), - actions=[AuthzedAction.DESCRIBE] + READ + policy=CombinedGroupNamespacePolicy( + groups=dev_team_groups, namespaces=test_namespaces + ), + actions=[AuthzedAction.DESCRIBE] + READ, ) # # - Grants full access to data team members in staging namespaces data_staging_perm = Permission( name="data_staging_permission", types=ALL_RESOURCE_TYPES, - policy=CombinedGroupNamespacePolicy(groups=data_team_groups, namespaces=staging_namespaces), - actions=ALL_ACTIONS + policy=CombinedGroupNamespacePolicy( + groups=data_team_groups, namespaces=staging_namespaces + ), + actions=ALL_ACTIONS, ) diff --git a/examples/operator-rbac/04-uninstall.ipynb b/examples/operator-rbac/04-uninstall.ipynb index 871e86fe3fd..faf62b3b9ba 100644 --- a/examples/operator-rbac/04-uninstall.ipynb +++ b/examples/operator-rbac/04-uninstall.ipynb @@ -52,8 +52,7 @@ } ], "source": [ - "!kubectl delete -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml\n", - "!kubectl delete -f ../../infra/feast-operator/dist/install.yaml" + "!kubectl delete -f ../../infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml\n!kubectl delete -f ../../infra/feast-operator/dist/install.yaml" ] }, { @@ -216,4 +215,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/examples/operator-rbac/1-setup-operator-rbac.ipynb b/examples/operator-rbac/1-setup-operator-rbac.ipynb index 0a4615a0ce6..6798e8d1b3b 100644 --- a/examples/operator-rbac/1-setup-operator-rbac.ipynb +++ b/examples/operator-rbac/1-setup-operator-rbac.ipynb @@ -176,8 +176,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Install the Feast services via FeatureStore CR\n", - "Next, we'll use the running Feast Operator to install the feast services with Server components online, offline, registry with kubernetes Authorization set. Apply the included [reference deployment](../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml) to install and configure Feast with kubernetes Authorization ." + "## Install the Feast services via FeatureStore CR\nNext, we'll use the running Feast Operator to install the feast services with Server components online, offline, registry with kubernetes Authorization set. Apply the included [reference deployment](../../infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml) to install and configure Feast with kubernetes Authorization ." ] }, { @@ -219,8 +218,7 @@ } ], "source": [ - "!cat ../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml\n", - "!kubectl apply -f ../../infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml -n feast" + "!cat ../../infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml\n!kubectl apply -f ../../infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml -n feast" ] }, { @@ -761,4 +759,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/examples/operator-rbac/client/feature_repo/test.py b/examples/operator-rbac/client/feature_repo/test.py index 78732327a62..88f7a0e6484 100644 --- a/examples/operator-rbac/client/feature_repo/test.py +++ b/examples/operator-rbac/client/feature_repo/test.py @@ -9,6 +9,7 @@ repo_path = os.getenv("FEAST_REPO_PATH", ".") store = FeatureStore(repo_path=repo_path) + def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): """Fetch historical features for training or batch scoring.""" try: @@ -38,13 +39,17 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: "transformed_conv_rate:conv_rate_plus_val2", ], ).to_df() - print(f"Successfully fetched {'batch scoring' if for_batch_scoring else 'training'} historical features:\n", training_df.head()) + print( + f"Successfully fetched {'batch scoring' if for_batch_scoring else 'training'} historical features:\n", + training_df.head(), + ) except PermissionError: print("\n*** PERMISSION DENIED *** Cannot fetch historical features.") except Exception as e: print(f"Unexpected error while fetching historical features: {e}") + def fetch_online_features(store: FeatureStore, source: str = ""): """Fetch online features from the feature store.""" try: @@ -76,7 +81,9 @@ def fetch_online_features(store: FeatureStore, source: str = ""): entity_rows=entity_rows, ).to_dict() - print(f"Successfully fetched online features {'via feature service' if source else 'directly'}:\n") + print( + f"Successfully fetched online features {'via feature service' if source else 'directly'}:\n" + ) for key, value in sorted(returned_features.items()): print(f"{key} : {value}") @@ -85,6 +92,7 @@ def fetch_online_features(store: FeatureStore, source: str = ""): except Exception as e: print(f"Unexpected error while fetching online features: {e}") + def check_permissions(): """Check user role, test various Feast operations.""" feature_views = [] @@ -94,7 +102,9 @@ def check_permissions(): try: feature_views = store.list_feature_views() if not feature_views: - print("No feature views found. You might not have access or they haven't been created.") + print( + "No feature views found. You might not have access or they haven't been created." + ) else: print(f"Successfully listed {len(feature_views)} feature views:") for fv in feature_views: @@ -117,7 +127,9 @@ def check_permissions(): store.apply(feature_views) print("User has write access to the feature store.") except PermissionError: - print("\n*** PERMISSION DENIED *** User lacks permission to modify the feature store.") + print( + "\n*** PERMISSION DENIED *** User lacks permission to modify the feature store." + ) except Exception as e: print(f"Unexpected error testing write access: {e}") @@ -151,5 +163,6 @@ def check_permissions(): except Exception as e: print(f"Unexpected error while pushing event: {e}") + if __name__ == "__main__": check_permissions() diff --git a/examples/operator-rbac/permissions_apply.py b/examples/operator-rbac/permissions_apply.py index 0d46ad5260a..d628fa88eb8 100644 --- a/examples/operator-rbac/permissions_apply.py +++ b/examples/operator-rbac/permissions_apply.py @@ -5,8 +5,10 @@ from feast.permissions.policy import RoleBasedPolicy # Define K8s roles same as created with FeatureStore CR -admin_roles = ["feast-writer"] # Full access (can create, update, delete ) Feast Resources -user_roles = ["feast-reader"] # Read-only access on Feast Resources +admin_roles = [ + "feast-writer" +] # Full access (can create, update, delete ) Feast Resources +user_roles = ["feast-reader"] # Read-only access on Feast Resources # User permissions (feast_user_permission) # - Grants read and describing Feast objects access @@ -14,7 +16,8 @@ name="feast_user_permission", types=ALL_RESOURCE_TYPES, policy=RoleBasedPolicy(roles=user_roles), - actions=[AuthzedAction.DESCRIBE] + READ # Read access (READ_ONLINE, READ_OFFLINE) + describe other Feast Resources. + actions=[AuthzedAction.DESCRIBE] + + READ, # Read access (READ_ONLINE, READ_OFFLINE) + describe other Feast Resources. ) # Admin permissions (feast_admin_permission) @@ -23,5 +26,5 @@ name="feast_admin_permission", types=ALL_RESOURCE_TYPES, policy=RoleBasedPolicy(roles=admin_roles), - actions=ALL_ACTIONS # Full permissions: CREATE, UPDATE, DELETE, READ, WRITE + actions=ALL_ACTIONS, # Full permissions: CREATE, UPDATE, DELETE, READ, WRITE ) diff --git a/examples/podman_local/client/feature_repo/test.py b/examples/podman_local/client/feature_repo/test.py index 13ab2444aab..1e3b741a1b3 100644 --- a/examples/podman_local/client/feature_repo/test.py +++ b/examples/podman_local/client/feature_repo/test.py @@ -1,9 +1,9 @@ -import subprocess from datetime import datetime import pandas as pd from feast import FeatureStore from feast.data_source import PushMode + def run_demo(): try: store = FeatureStore(repo_path=".") @@ -51,6 +51,7 @@ def run_demo(): except Exception as e: print(f"An error occurred in run_demo: {e}") + def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): try: entity_df = pd.DataFrame.from_dict( @@ -83,6 +84,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: except Exception as e: print(f"An error occurred in fetch_historical_features_entity_df: {e}") + def fetch_online_features(store, source: str = ""): try: entity_rows = [ @@ -116,6 +118,7 @@ def fetch_online_features(store, source: str = ""): except Exception as e: print(f"An error occurred in fetch_online_features: {e}") + if __name__ == "__main__": try: run_demo() diff --git a/examples/python-helm-demo/feature_repo/driver_repo.py b/examples/python-helm-demo/feature_repo/driver_repo.py index f7dd05afff7..edb3e1e9d89 100644 --- a/examples/python-helm-demo/feature_repo/driver_repo.py +++ b/examples/python-helm-demo/feature_repo/driver_repo.py @@ -14,7 +14,10 @@ timestamp_field="event_timestamp", created_timestamp_column="created", ) -driver = Entity(name="driver_id", description="driver id",) +driver = Entity( + name="driver_id", + description="driver id", +) driver_hourly_stats_view = FeatureView( name="driver_hourly_stats", entities=[driver], @@ -58,4 +61,3 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] return df - diff --git a/examples/python-helm-demo/test/test_python_fetch.py b/examples/python-helm-demo/test/test_python_fetch.py index 715912422f3..3412f7d4de0 100644 --- a/examples/python-helm-demo/test/test_python_fetch.py +++ b/examples/python-helm-demo/test/test_python_fetch.py @@ -18,8 +18,11 @@ def run_demo_http(): resp_data = json.loads(r.text) records = pd.DataFrame.from_records( - columns=resp_data["metadata"]["feature_names"], - data=[[r["values"][i] for r in resp_data["results"]] for i in range(len(resp_data["results"]))] + columns=resp_data["metadata"]["feature_names"], + data=[ + [r["values"][i] for r in resp_data["results"]] + for i in range(len(resp_data["results"])) + ], ) for col in sorted(records.columns): print(col, " : ", records[col].values) diff --git a/examples/rag-docling/feature_repo/example_repo.py b/examples/rag-docling/feature_repo/example_repo.py index 6ce2adab8a5..1169b1c830a 100644 --- a/examples/rag-docling/feature_repo/example_repo.py +++ b/examples/rag-docling/feature_repo/example_repo.py @@ -1,6 +1,5 @@ from datetime import timedelta -import pandas as pd from feast import ( FeatureView, Field, @@ -12,7 +11,7 @@ from feast.types import Float64, Array, String, ValueType, PdfBytes from feast.on_demand_feature_view import on_demand_feature_view from sentence_transformers import SentenceTransformer -from typing import Dict, Any, List +from typing import Any import hashlib from docling.datamodel.base_models import DocumentStream @@ -20,7 +19,6 @@ import io from docling.document_converter import DocumentConverter from transformers import AutoTokenizer -from sentence_transformers import SentenceTransformer from docling.chunking import HybridChunker # Load tokenizer and embedding model @@ -31,13 +29,19 @@ embedding_model = SentenceTransformer(EMBED_MODEL_ID) chunker = HybridChunker(tokenizer=tokenizer, max_tokens=MAX_TOKENS, merge_peers=True) + def embed_text(text: str) -> list[float]: """Generate an embedding for a given text.""" return embedding_model.encode([text], normalize_embeddings=True).tolist()[0] -def generate_chunk_id(file_name: str, raw_chunk_markdown: str="") -> str: + +def generate_chunk_id(file_name: str, raw_chunk_markdown: str = "") -> str: """Generate a unique chunk ID based on file_name and raw_chunk_markdown.""" - unique_string = f"{file_name}-{raw_chunk_markdown}" if raw_chunk_markdown != "" else f"{file_name}" + unique_string = ( + f"{file_name}-{raw_chunk_markdown}" + if raw_chunk_markdown != "" + else f"{file_name}" + ) return hashlib.sha256(unique_string.encode()).hexdigest() @@ -64,7 +68,7 @@ def generate_chunk_id(file_name: str, raw_chunk_markdown: str="") -> str: input_request_pdf = RequestSource( name="pdf_request_source", schema=[ - Field(name="document_id", dtype=String), + Field(name="document_id", dtype=String), Field(name="pdf_bytes", dtype=PdfBytes), Field(name="file_name", dtype=String), ], @@ -88,6 +92,7 @@ def generate_chunk_id(file_name: str, raw_chunk_markdown: str="") -> str: ttl=timedelta(hours=2), ) + @on_demand_feature_view( entities=[chunk, document], sources=[input_request_pdf], diff --git a/examples/rag-retriever/README.md b/examples/rag-retriever/README.md index 4c9eb9bf8c2..7df89957cfa 100644 --- a/examples/rag-retriever/README.md +++ b/examples/rag-retriever/README.md @@ -62,6 +62,59 @@ Navigate to the examples/rag-retriever directory. Here you will find the followi Open `rag_feast.ipynb` and follow the steps in the notebook to run the example. +## Using DocEmbedder for Simplified Ingestion + +As an alternative to the manual data preparation steps in the notebook above, Feast provides the `DocEmbedder` class that automates the entire document-to-embeddings pipeline: chunking, embedding generation, FeatureView creation, and writing to the online store. + +### Install Dependencies + +```bash +pip install feast[milvus,rag] +``` + +### Quick Start + +```python +from feast import DocEmbedder +from datasets import load_dataset + +# Load your dataset +dataset = load_dataset("facebook/wiki_dpr", "psgs_w100.nq.exact", split="train[:1%]", + with_index=False, trust_remote_code=True) +df = dataset.select(range(100)).to_pandas() + +# DocEmbedder handles everything in one step +embedder = DocEmbedder( + repo_path="feature_repo_docembedder/", + feature_view_name="text_feature_view", +) + +result = embedder.embed_documents( + documents=df, + id_column="id", + source_column="text", + column_mapping=("text", "text_embedding"), +) +``` + +### What DocEmbedder Does + +1. **Generates a FeatureView**: Automatically creates a Python file with Entity and FeatureView definitions compatible with `feast apply` +2. **Applies the repo**: Registers the FeatureView in the Feast registry and deploys infrastructure (e.g., Milvus collection) +3. **Chunks documents**: Splits text into smaller passages using `TextChunker` (configurable chunk size, overlap, etc.) +4. **Generates embeddings**: Produces vector embeddings using `MultiModalEmbedder` (defaults to `all-MiniLM-L6-v2`) +5. **Writes to online store**: Stores the processed data in your configured online store (e.g., Milvus) + +### Customization + +* **Custom Chunker**: Subclass `BaseChunker` for your own chunking strategy +* **Custom Embedder**: Subclass `BaseEmbedder` to use a different embedding model +* **Logical Layer Function**: Provide a `SchemaTransformFn` to control how the output maps to your FeatureView schema + +### Example Notebook + +See **`rag_feast_docembedder.ipynb`** for a complete end-to-end example that uses DocEmbedder with the Wiki DPR dataset and then queries the results using `FeastRAGRetriever`. + ## FeastRagRetriver Low Level Design Low level design for feast rag retriever diff --git a/examples/rag-retriever/rag_feast_docembedder.ipynb b/examples/rag-retriever/rag_feast_docembedder.ipynb new file mode 100644 index 00000000000..47728fef556 --- /dev/null +++ b/examples/rag-retriever/rag_feast_docembedder.ipynb @@ -0,0 +1,648 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "2855fd1a", + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install --quiet feast[milvus] sentence-transformers datasets\n", + "# %pip install bigtree==0.19.2\n", + "# %pip install marshmallow==3.10.0 " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3bb14cf1", + "metadata": {}, + "outputs": [], + "source": [ + "from datasets import load_dataset\n", + "# load wikipedia dataset - 1% of the training split\n", + "dataset = load_dataset(\n", + " \"facebook/wiki_dpr\",\n", + " \"psgs_w100.nq.exact\",\n", + " split=\"train[:1%]\",\n", + " with_index=False,\n", + " trust_remote_code=True,\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "92a5e18c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idtexttitleembeddings
01Aaron Aaron ( or ; \"Ahärôn\") is a prophet, hig...Aaron[0.013342111, 0.58217376, -0.31309745, -0.6991...
12God at Sinai granted Aaron the priesthood for ...Aaron[-0.19236332, 0.539003, -0.5652932, -0.5195250...
23his rod turn into a snake. Then he stretched o...Aaron[-0.23045847, 0.28877887, -0.3449004, -0.14077...
34however, Aaron and Hur remained below to look ...Aaron[0.107315615, 0.5992388, -0.37498242, -0.53419...
45Aaron and his sons to the priesthood, and arra...Aaron[0.32623303, 0.51600194, -0.5568064, -0.494033...
\n", + "
" + ], + "text/plain": [ + " id text title \\\n", + "0 1 Aaron Aaron ( or ; \"Ahärôn\") is a prophet, hig... Aaron \n", + "1 2 God at Sinai granted Aaron the priesthood for ... Aaron \n", + "2 3 his rod turn into a snake. Then he stretched o... Aaron \n", + "3 4 however, Aaron and Hur remained below to look ... Aaron \n", + "4 5 Aaron and his sons to the priesthood, and arra... Aaron \n", + "\n", + " embeddings \n", + "0 [0.013342111, 0.58217376, -0.31309745, -0.6991... \n", + "1 [-0.19236332, 0.539003, -0.5652932, -0.5195250... \n", + "2 [-0.23045847, 0.28877887, -0.3449004, -0.14077... \n", + "3 [0.107315615, 0.5992388, -0.37498242, -0.53419... \n", + "4 [0.32623303, 0.51600194, -0.5568064, -0.494033... " + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataset.column_names\n", + "df = dataset.select(range(100)).to_pandas()\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "088eaf84", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "import os\n", + "\n", + "\n", + "def write_feature_store_yaml(file_path: str, project_name: str) -> str:\n", + " \"\"\"\n", + " Write a feature_store.yaml file to the specified path.\n", + "\n", + " Args:\n", + " file_path: Full path where the YAML file should be written\n", + " (e.g. \"feature_repo/feature_store.yaml\").\n", + " project_name: The project name to use in the YAML.\n", + "\n", + " Returns:\n", + " The absolute path of the written file.\n", + " \"\"\"\n", + " config = {\n", + " \"project\": project_name,\n", + " \"provider\": \"local\",\n", + " \"registry\": \"data/registry.db\",\n", + " \"online_store\": {\n", + " \"type\": \"milvus\",\n", + " \"host\": \"http://localhost\",\n", + " \"port\": 19530,\n", + " \"vector_enabled\": True,\n", + " \"embedding_dim\": 384,\n", + " \"index_type\": \"FLAT\",\n", + " \"metric_type\": \"COSINE\",\n", + " },\n", + " \"offline_store\": {\n", + " \"type\": \"file\",\n", + " },\n", + " \"entity_key_serialization_version\": 3,\n", + " \"auth\": {\n", + " \"type\": \"no_auth\",\n", + " },\n", + " }\n", + "\n", + " os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True)\n", + "\n", + " with open(file_path, \"w\") as f:\n", + " yaml.dump(config, f, default_flow_style=False, sort_keys=False)\n", + "\n", + " return os.path.abspath(file_path)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f951c804", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mkdir: feature_repo_docebedder: File exists\n", + "/Users/chpatel/projects/feast/examples/rag-retriever\n" + ] + } + ], + "source": [ + "%mkdir feature_repo_docebedder\n", + "!pwd" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "27be0f7e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "YAML written to: /Users/chpatel/projects/feast/examples/rag-retriever/feature_repo_docebedder/feature_store.yaml\n" + ] + } + ], + "source": [ + "path = write_feature_store_yaml(\"feature_repo_docebedder/feature_store.yaml\", \"my_project\")\n", + "print(f\"YAML written to: {path}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a19428c3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "No project found in the repository. Using project name my_project defined in feature_store.yaml\n", + "Applying changes for project my_project\n", + "Connecting to Milvus remotely at http://localhost:19530\n", + "Deploying infrastructure for \u001b[1m\u001b[32mtext_feature_view\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/chpatel/projects/feast/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py:86: UserWarning: Field name \"vector_enabled\" in \"MilvusOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\n", + " class MilvusOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig):\n" + ] + } + ], + "source": [ + "from feast import DocEmbedder\n", + "\n", + "de = DocEmbedder(repo_path=\"feature_repo_docebedder\", feature_view_name=\"text_feature_view\",yaml_file=\"feature_store.yaml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ed217e95", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b19b54476308402eaa251a88fc098300", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Batches: 0%| | 0/4 [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
passage_idtextembeddingevent_timestampsource_id
01_0Aaron Aaron ( or ; \"Ahärôn\") is a prophet, hig...[0.002557202707976103, 0.12003513425588608, -0...2026-02-11 12:26:29.098091+00:001
11_1Israelites, Aaron served as his brother's spok...[-0.01853535883128643, 0.13290095329284668, -0...2026-02-11 12:26:29.098091+00:001
22_0God at Sinai granted Aaron the priesthood for ...[0.014343681745231152, 0.10290483385324478, -0...2026-02-11 12:26:29.098091+00:002
32_1could not speak well, God appointed Aaron as M...[0.0504433810710907, 0.1175316572189331, -0.00...2026-02-11 12:26:29.098091+00:002
43_0his rod turn into a snake. Then he stretched o...[-0.06228446215391159, 0.10652626305818558, 0....2026-02-11 12:26:29.098091+00:003
..................
19598_1State College before entering Columbia Univers...[0.03597380220890045, 0.04296444356441498, 0.0...2026-02-11 12:26:29.098091+00:0098
19699_0joined the Merchant Marine to earn money to co...[0.05798682942986488, -0.007653537206351757, -...2026-02-11 12:26:29.098091+00:0099
19799_1spent several months in a mental institution a...[0.05905637890100479, 0.030195411294698715, -0...2026-02-11 12:26:29.098091+00:0099
198100_0harboring stolen goods in his dorm room. It wa...[-0.005938616115599871, 0.02653227001428604, -...2026-02-11 12:26:29.098091+00:00100
199100_1Eugene to party meetings. Ginsberg later said ...[0.007752032019197941, 0.06832979619503021, 0....2026-02-11 12:26:29.098091+00:00100
\n", + "

200 rows × 5 columns

\n", + "" + ], + "text/plain": [ + " passage_id text \\\n", + "0 1_0 Aaron Aaron ( or ; \"Ahärôn\") is a prophet, hig... \n", + "1 1_1 Israelites, Aaron served as his brother's spok... \n", + "2 2_0 God at Sinai granted Aaron the priesthood for ... \n", + "3 2_1 could not speak well, God appointed Aaron as M... \n", + "4 3_0 his rod turn into a snake. Then he stretched o... \n", + ".. ... ... \n", + "195 98_1 State College before entering Columbia Univers... \n", + "196 99_0 joined the Merchant Marine to earn money to co... \n", + "197 99_1 spent several months in a mental institution a... \n", + "198 100_0 harboring stolen goods in his dorm room. It wa... \n", + "199 100_1 Eugene to party meetings. Ginsberg later said ... \n", + "\n", + " embedding \\\n", + "0 [0.002557202707976103, 0.12003513425588608, -0... \n", + "1 [-0.01853535883128643, 0.13290095329284668, -0... \n", + "2 [0.014343681745231152, 0.10290483385324478, -0... \n", + "3 [0.0504433810710907, 0.1175316572189331, -0.00... \n", + "4 [-0.06228446215391159, 0.10652626305818558, 0.... \n", + ".. ... \n", + "195 [0.03597380220890045, 0.04296444356441498, 0.0... \n", + "196 [0.05798682942986488, -0.007653537206351757, -... \n", + "197 [0.05905637890100479, 0.030195411294698715, -0... \n", + "198 [-0.005938616115599871, 0.02653227001428604, -... \n", + "199 [0.007752032019197941, 0.06832979619503021, 0.... \n", + "\n", + " event_timestamp source_id \n", + "0 2026-02-11 12:26:29.098091+00:00 1 \n", + "1 2026-02-11 12:26:29.098091+00:00 1 \n", + "2 2026-02-11 12:26:29.098091+00:00 2 \n", + "3 2026-02-11 12:26:29.098091+00:00 2 \n", + "4 2026-02-11 12:26:29.098091+00:00 3 \n", + ".. ... ... \n", + "195 2026-02-11 12:26:29.098091+00:00 98 \n", + "196 2026-02-11 12:26:29.098091+00:00 99 \n", + "197 2026-02-11 12:26:29.098091+00:00 99 \n", + "198 2026-02-11 12:26:29.098091+00:00 100 \n", + "199 2026-02-11 12:26:29.098091+00:00 100 \n", + "\n", + "[200 rows x 5 columns]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "de.embed_documents(documents=df, id_column=\"id\", source_column=\"text\", column_mapping= (\"text\", \"text_embedding\"))" + + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b25b69df", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/Users/chpatel/projects/feast/examples/rag-retriever/feature_repo_docebedder\n" + ] + } + ], + "source": [ + "%cd feature_repo_docebedder" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5bf57671", + "metadata": {}, + "outputs": [], + "source": [ + "from feast import FeatureStore\n", + "import pandas as pd\n", + "\n", + "store = FeatureStore(repo_path=\".\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2bd1f1da", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "92de6524b74c4a98ac1b56668926681a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading checkpoint shards: 0%| | 0/2 [00:00:241: DeprecationWarning: builtin type SwigPyPacked has no __module__ attribute\n", + ":241: DeprecationWarning: builtin type SwigPyObject has no __module__ attribute\n", + ":241: DeprecationWarning: builtin type swigvarlink has no __module__ attribute\n" + ] + } + ], + "source": [ + "import sys\n", + "sys.path.append(\"..\")\n", + "from text_feature_view import text_feature_view\n", + "from feast.vector_store import FeastVectorStore\n", + "from feast.rag_retriever import FeastIndex, FeastRAGRetriever\n", + "\n", + "generator_config=generator_model.config\n", + "question_encoder = AutoModel.from_pretrained(\"sentence-transformers/all-MiniLM-L6-v2\")\n", + "question_encoder_tokenizer = AutoTokenizer.from_pretrained(\"sentence-transformers/all-MiniLM-L6-v2\")\n", + "\n", + "\n", + "query_encoder_config = {\n", + " \"model_type\": \"bert\",\n", + " \"hidden_size\": 384\n", + "}\n", + "\n", + "vector_store = FeastVectorStore(\n", + " repo_path=\".\",\n", + " rag_view=text_feature_view,\n", + " features=[\"text_feature_view:text\", \"text_feature_view:embedding\", \"text_feature_view:passage_id\",\"text_feature_view:source_id\"]\n", + ")\n", + "\n", + "feast_index = FeastIndex()\n", + "\n", + "config = RagConfig(\n", + " question_encoder=query_encoder_config,\n", + " generator=generator_config.to_dict(),\n", + " index=feast_index\n", + ")\n", + "retriever = FeastRAGRetriever(\n", + " question_encoder=question_encoder,\n", + " question_encoder_tokenizer=question_encoder_tokenizer,\n", + " generator_tokenizer=generator_tokenizer,\n", + " feast_repo_path=\".\",\n", + " feature_view=vector_store.rag_view,\n", + " features=vector_store.features,\n", + " generator_model=generator_model, \n", + " search_type=\"vector\",\n", + " id_field=\"passage_id\",\n", + " text_field=\"text\",\n", + " config=config,\n", + " index=feast_index,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "09793dd4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connecting to Milvus remotely at http://localhost:19530\n", + "Generated Answer: Context: \n", + "\n", + "Question: What is the capital of Ireland?\n", + "\n", + "Answer: The capital of Ireland is Dublin.\n", + "\n", + "Context: \n", + "\n", + "Question: What is the capital of Ireland?\n", + "\n", + "Answer: The capital of Ireland is Dublin.\n", + "\n", + "Context: \n", + "\n", + "Question: What is the capital city of Australia?\n", + "\n", + "Answer: The capital city of Australia is Canberra.\n", + "\n", + "Context: \n", + "\n", + "Question: What is the capital of Ireland?\n", + "\n", + "Answer: The capital of I\n" + ] + } + ], + "source": [ + "query = \"What is the capital of Ireland?\"\n", + "answer = retriever.generate_answer(query, top_k=10)\n", + "print(\"Generated Answer:\", answer)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/rag/feature_repo/example_repo.py b/examples/rag/feature_repo/example_repo.py index e0a9be21452..7a37d99d495 100644 --- a/examples/rag/feature_repo/example_repo.py +++ b/examples/rag/feature_repo/example_repo.py @@ -39,4 +39,4 @@ ], source=source, ttl=timedelta(hours=2), -) \ No newline at end of file +) diff --git a/examples/rag/feature_repo/test_workflow.py b/examples/rag/feature_repo/test_workflow.py index 05cd554d823..8a8a8813bf9 100644 --- a/examples/rag/feature_repo/test_workflow.py +++ b/examples/rag/feature_repo/test_workflow.py @@ -33,21 +33,23 @@ def run_model(sentences, tokenizer, model): sentence_embeddings = F.normalize(sentence_embeddings, p=2, dim=1) return sentence_embeddings + def run_demo(): store = FeatureStore(repo_path=".") df = pd.read_parquet("./data/city_wikipedia_summaries_with_embeddings.parquet") - embedding_length = len(df['vector'][0]) - print(f'embedding length = {embedding_length}') + embedding_length = len(df["vector"][0]) + print(f"embedding length = {embedding_length}") store.apply([city_embeddings_feature_view, item]) - fields = [ - f.name for f in city_embeddings_feature_view.features - ] + city_embeddings_feature_view.entities + [city_embeddings_feature_view.batch_source.timestamp_field] - print('\ndata=') + fields = ( + [f.name for f in city_embeddings_feature_view.features] + + city_embeddings_feature_view.entities + + [city_embeddings_feature_view.batch_source.timestamp_field] + ) + print("\ndata=") print(df[fields].head().T) store.write_to_online_store("city_embeddings", df[fields][0:3]) - question = "the most populous city in the state of New York is New York" tokenizer = AutoTokenizer.from_pretrained(TOKENIZER) model = AutoModel.from_pretrained(MODEL) @@ -70,5 +72,6 @@ def run_demo(): print(features.to_df()) store.teardown() + if __name__ == "__main__": run_demo() diff --git a/examples/ray-llm-posttrain/.gitignore b/examples/ray-llm-posttrain/.gitignore new file mode 100644 index 00000000000..4bf1a46bf0e --- /dev/null +++ b/examples/ray-llm-posttrain/.gitignore @@ -0,0 +1,13 @@ +# Feast / Ray local artifacts +data/ +.feast/ +ray_storage/ +/tmp/ray/ +ray_results/ +.ray/ + +__pycache__/ +*.py[cod] +.pytest_cache/ +.venv/ +.env diff --git a/examples/ray-llm-posttrain/README.md b/examples/ray-llm-posttrain/README.md new file mode 100644 index 00000000000..64252a5a603 --- /dev/null +++ b/examples/ray-llm-posttrain/README.md @@ -0,0 +1,35 @@ +# How to Use Feast for SLM/LLM Post-Training (with Ray) + +| Name | Type | Fields | +|---|---|---| +| `web_documents` | FeatureView | `human`, `bot`, `human_repeat_ratio`, `bot_repeat_ratio` | +| `train_example` | OnDemandFeatureView | `cleaned_human`, `cleaned_bot`, `char_count`, `is_trainable`, `sft_text` | +| `llm_posttrain` | FeatureService | `web_documents` + `train_example` | + +Source data is **prepared parquet** (`document_id` + `event_timestamp` already present). No Feast core patches. + +## Paths + +| Flag | What happens | +|---|---| +| (default) | `to_ray_dataset()` + preprocess `sft_text` (ODFV does **not** run) | +| `--via-df` | `to_df()` so ODFV `train_example` runs | + +## Setup + +```bash +uv pip install -e "../../sdk/python[ray]" -r requirements.txt +PYTHONPATH=../../sdk/python python scripts/prepare_data.py +cd feature_repo && feast apply && cd .. +``` + +## Run (data load only) + +```bash +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df +``` + +## Blog + +[How to Use Feast for SLM/LLM Post-Training with Ray](/blog/feast-ray-llm-posttrain) diff --git a/examples/ray-llm-posttrain/feature_repo/feature_definitions.py b/examples/ray-llm-posttrain/feature_repo/feature_definitions.py new file mode 100644 index 00000000000..a85470c3729 --- /dev/null +++ b/examples/ray-llm-posttrain/feature_repo/feature_definitions.py @@ -0,0 +1,108 @@ +"""Feast feature definitions for Ray + ODFV LLM post-training. + +Pipeline (supported Feast APIs only): + scripts/prepare_data.py → parquet with document_id + event_timestamp + → RaySource (parquet) + → FeatureView web_documents + → OnDemandFeatureView train_example + → FeatureService llm_posttrain +""" + +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path + +from feast import Entity, FeatureService, FeatureView, Field, ValueType +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Bool, Float64, Int64, String + +_REPO_DIR = Path(__file__).resolve().parent +_PARQUET = str(_REPO_DIR / "data" / "tiny_webtext.parquet") + +document = Entity( + name="document", + join_keys=["document_id"], + value_type=ValueType.STRING, + description="Document id (added by scripts/prepare_data.py)", +) + +# Parquet already has document_id + event_timestamp (see prepare_data.py). +# Do not rely on BatchFeatureView UDFs to invent timestamps during entity-less +# retrieval — that path is not supported without Feast core changes. +tiny_web = RaySource( + name="tiny_webtext", + reader_type="parquet", + path=_PARQUET, + timestamp_field="event_timestamp", +) + +web_documents = FeatureView( + name="web_documents", + entities=[document], + ttl=timedelta(days=365), + schema=[ + Field(name="human", dtype=String), + Field(name="bot", dtype=String), + Field(name="human_repeat_ratio", dtype=Float64), + Field(name="bot_repeat_ratio", dtype=Float64), + ], + source=tiny_web, + online=False, + description="Conversation columns from prepared parquet", + tags={"use_case": "llm_posttrain", "source": "parquet"}, +) + + +@on_demand_feature_view( + sources=[web_documents], + schema=[ + Field(name="cleaned_human", dtype=String), + Field(name="cleaned_bot", dtype=String), + Field(name="char_count", dtype=Int64), + Field(name="is_trainable", dtype=Bool), + Field(name="sft_text", dtype=String), + ], + mode="pandas", +) +def train_example(inputs): + """Quality gate + human→bot SFT formatting.""" + import pandas as pd + + min_chars = 64 + max_repeat_ratio = 0.65 + + cleaned_human = inputs["human"].fillna("").astype(str).str.strip() + cleaned_bot = inputs["bot"].fillna("").astype(str).str.strip() + char_count = cleaned_bot.str.len().astype("int64") + + human_ratio = inputs["human_repeat_ratio"].fillna(1.0).astype(float) + bot_ratio = inputs["bot_repeat_ratio"].fillna(1.0).astype(float) + is_trainable = ( + (char_count >= min_chars) + & (human_ratio <= max_repeat_ratio) + & (bot_ratio <= max_repeat_ratio) + ) + + sft_text = ( + "<|im_start|>user\n" + cleaned_human + "<|im_end|>\n" + "<|im_start|>assistant\n" + cleaned_bot + "<|im_end|>" + ) + + return pd.DataFrame( + { + "cleaned_human": cleaned_human, + "cleaned_bot": cleaned_bot, + "char_count": char_count, + "is_trainable": is_trainable, + "sft_text": sft_text, + } + ) + + +llm_posttrain = FeatureService( + name="llm_posttrain", + features=[web_documents, train_example], + tags={"use_case": "llm_posttrain", "model": "gpt2"}, +) diff --git a/examples/ray-llm-posttrain/feature_repo/feature_store.yaml b/examples/ray-llm-posttrain/feature_repo/feature_store.yaml new file mode 100644 index 00000000000..226dbdfb45c --- /dev/null +++ b/examples/ray-llm-posttrain/feature_repo/feature_store.yaml @@ -0,0 +1,25 @@ +project: ray_llm_posttrain +registry: data/registry.db +provider: local + +# Laptop-friendly Ray offline store (no KubeRay) +offline_store: + type: ray + storage_path: data/ray_storage + enable_ray_logging: false + ray_conf: + num_cpus: 2 + object_store_memory: 104857600 + _memory: 524288000 + +batch_engine: + type: ray.engine + max_workers: 2 + +online_store: + type: sqlite + path: data/online_store.db + +entity_key_serialization_version: 3 +auth: + type: no_auth diff --git a/examples/ray-llm-posttrain/requirements.txt b/examples/ray-llm-posttrain/requirements.txt new file mode 100644 index 00000000000..4a82b391c07 --- /dev/null +++ b/examples/ray-llm-posttrain/requirements.txt @@ -0,0 +1,8 @@ +# Feast + Ray offline store / compute engine +feast[ray]>=0.50.0 +datasets>=2.19.0 + +# Short GPT-2 SFT +transformers>=4.40.0 +torch>=2.1.0 +accelerate>=0.30.0 diff --git a/examples/ray-llm-posttrain/scripts/prepare_data.py b/examples/ray-llm-posttrain/scripts/prepare_data.py new file mode 100644 index 00000000000..efd1a5e032b --- /dev/null +++ b/examples/ray-llm-posttrain/scripts/prepare_data.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Prepare a small local parquet seed for the example. + +Hugging Face tiny-webtext has no document_id / event_timestamp. Feast entity-less +retrieval needs those columns on the *source* data. We synthesize them here +(outside Feast) and write parquet — no Feast core changes required. + + PYTHONPATH=../../sdk/python python scripts/prepare_data.py +""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +import pandas as pd + +REPO_ROOT = Path(__file__).resolve().parents[1] +OUT_PATH = REPO_ROOT / "feature_repo" / "data" / "tiny_webtext.parquet" +SPLIT = "train[:2000]" +DATASET = "nampdn-ai/tiny-webtext" + + +def main() -> int: + from datasets import load_dataset + + print(f"Loading {DATASET} split={SPLIT!r}...") + ds = load_dataset(DATASET, split=SPLIT) + df = ds.to_pandas() + + demo_base_ts = pd.Timestamp("2024-06-01", tz="UTC") + demo_window_seconds = 30 * 24 * 3600 + + humans = df["human"].fillna("").astype(str) + bots = df["bot"].fillna("").astype(str) + doc_ids: list[str] = [] + timestamps: list[pd.Timestamp] = [] + for human, bot in zip(humans, bots, strict=True): + digest = hashlib.sha256(f"{human}\n{bot}".encode()).hexdigest() + doc_ids.append(digest[:16]) + offset = int(digest[:8], 16) % demo_window_seconds + timestamps.append(demo_base_ts + pd.Timedelta(seconds=offset)) + + df = df.copy() + df["document_id"] = doc_ids + df["event_timestamp"] = timestamps + + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) + df.to_parquet(OUT_PATH, index=False) + print(f"Wrote {len(df)} rows → {OUT_PATH}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/ray-llm-posttrain/scripts/train_sft.py b/examples/ray-llm-posttrain/scripts/train_sft.py new file mode 100644 index 00000000000..a3f4939f855 --- /dev/null +++ b/examples/ray-llm-posttrain/scripts/train_sft.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Feast conversation features → training rows (paths match the blog). + +Paths: + A) Default: get_historical_features → to_ray_dataset() → preprocess sft_text + (ODFVs do NOT run on to_ray_dataset) + B) --via-df: get_historical_features → to_df() (ODFV train_example runs) + + PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run + PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df +""" + +from __future__ import annotations + +import argparse +import sys +from datetime import datetime, timezone +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +FEATURE_REPO = REPO_ROOT / "feature_repo" +DATA_DIR = REPO_ROOT / "data" + +# Matches the blog Option A snippet (length gate) +_MIN_CHARS = 64 + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--max-steps", type=int, default=20) + parser.add_argument("--batch-size", type=int, default=2) + parser.add_argument("--max-length", type=int, default=256) + parser.add_argument( + "--output-dir", + type=Path, + default=DATA_DIR / "gpt2-sft", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print a few training rows; skip the optional GPT-2 smoke", + ) + parser.add_argument( + "--via-df", + action="store_true", + help="Use to_df() so OnDemandFeatureView train_example runs", + ) + return parser.parse_args() + + +def _date_window() -> tuple[datetime, datetime]: + return ( + datetime(2024, 6, 1, tzinfo=timezone.utc), + datetime(2024, 7, 1, tzinfo=timezone.utc), + ) + + +def _preprocess_sft_batch(batch): + """Build sft_text from FeatureView columns (blog Option A).""" + import pandas as pd + + if not isinstance(batch, pd.DataFrame): + batch = pd.DataFrame(batch) + + human = batch["human"].fillna("").astype(str).str.strip() + bot = batch["bot"].fillna("").astype(str).str.strip() + ok = bot.str.len() >= _MIN_CHARS + sft_text = ( + "<|im_start|>user\n" + human + "<|im_end|>\n" + "<|im_start|>assistant\n" + bot + "<|im_end|>" + ) + return pd.DataFrame({"sft_text": sft_text}).loc[ok].reset_index(drop=True) + + +def retrieve_via_ray_stream(): + """Option A: to_ray_dataset() + preprocess (ODFV does not run).""" + from feast import FeatureStore + + store = FeatureStore(repo_path=str(FEATURE_REPO)) + start_date, end_date = _date_window() + + print("get_historical_features → to_ray_dataset() (preprocess sft_text on Ray)") + job = store.get_historical_features( + features=[ + "web_documents:human", + "web_documents:bot", + "web_documents:human_repeat_ratio", + "web_documents:bot_repeat_ratio", + ], + start_date=start_date, + end_date=end_date, + ) + ds = job.to_ray_dataset() + return ds.map_batches(_preprocess_sft_batch, batch_format="pandas") + + +def retrieve_via_df(): + """Option B: to_df() so ODFV train_example runs, then Ray from pandas.""" + import ray + from feast import FeatureStore + + store = FeatureStore(repo_path=str(FEATURE_REPO)) + start_date, end_date = _date_window() + + print("get_historical_features → to_df() (ODFV train_example runs)") + df = store.get_historical_features( + features=store.get_feature_service("llm_posttrain"), + start_date=start_date, + end_date=end_date, + ).to_df() + + if "is_trainable" not in df.columns or "sft_text" not in df.columns: + raise RuntimeError("Expected ODFV columns is_trainable / sft_text from to_df()") + + mask = df["is_trainable"].fillna(False).astype(bool) + mask &= df["sft_text"].fillna("").astype(str).str.len() > 0 + slim = df.loc[mask, ["sft_text"]].reset_index(drop=True) + return ray.data.from_pandas(slim) + + +def train_gpt2_optional( + ray_ds, *, max_steps: int, batch_size: int, max_length: int, output_dir: Path +) -> None: + import torch + from transformers import ( + AutoModelForCausalLM, + AutoTokenizer, + DataCollatorForLanguageModeling, + Trainer, + TrainingArguments, + ) + + rows = ray_ds.take(min(500, max(50, max_steps * batch_size * 4))) + texts = [r["sft_text"] for r in rows if r.get("sft_text")] + if not texts: + raise RuntimeError("No trainable SFT rows") + + print(f"[optional] GPT-2 smoke on {len(texts)} rows, {max_steps} steps...") + tokenizer = AutoTokenizer.from_pretrained("gpt2") + if tokenizer.pad_token is None: + tokenizer.pad_token = tokenizer.eos_token + model = AutoModelForCausalLM.from_pretrained("gpt2") + encodings = tokenizer( + texts, + truncation=True, + max_length=max_length, + padding="max_length", + return_tensors="pt", + ) + + class _TextDataset(torch.utils.data.Dataset): + def __len__(self) -> int: + return encodings["input_ids"].shape[0] + + def __getitem__(self, idx: int) -> dict: + return { + "input_ids": encodings["input_ids"][idx], + "attention_mask": encodings["attention_mask"][idx], + "labels": encodings["input_ids"][idx].clone(), + } + + output_dir.mkdir(parents=True, exist_ok=True) + args = TrainingArguments( + output_dir=str(output_dir), + per_device_train_batch_size=batch_size, + max_steps=max_steps, + logging_steps=max(1, max_steps // 5), + save_steps=max_steps, + learning_rate=5e-5, + report_to=[], + remove_unused_columns=False, + ) + trainer = Trainer( + model=model, + args=args, + train_dataset=_TextDataset(), + data_collator=DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False), + ) + trainer.train() + trainer.save_model(str(output_dir)) + tokenizer.save_pretrained(str(output_dir)) + print(f"Saved optional checkpoint to {output_dir}") + + +def main() -> int: + args = _parse_args() + if not (FEATURE_REPO / "feature_store.yaml").exists(): + print(f"Missing feature repo at {FEATURE_REPO}", file=sys.stderr) + return 1 + + if args.via_df: + ds = retrieve_via_df() + else: + ds = retrieve_via_ray_stream() + + sample = ds.take(3) + print(f"Sample training rows: {len(sample)}") + for i, row in enumerate(sample): + preview = str(row.get("sft_text", row))[:160].replace("\n", "\\n") + print(f" [{i}] {preview}...") + + if args.dry_run: + print("Done (trainer skipped).") + return 0 + + train_gpt2_optional( + ds, + max_steps=args.max_steps, + batch_size=args.batch_size, + max_length=args.max_length, + output_dir=args.output_dir, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/rbac-remote/client/k8s/feature_repo/test.py b/examples/rbac-remote/client/k8s/feature_repo/test.py index 6e1480bc947..96fe678d5f8 100644 --- a/examples/rbac-remote/client/k8s/feature_repo/test.py +++ b/examples/rbac-remote/client/k8s/feature_repo/test.py @@ -20,9 +20,11 @@ def run_demo(): try: print("\n--- Load features into online store/materialize_incremental ---") - feature_views= store.list_feature_views() + feature_views = store.list_feature_views() if not feature_views: - raise PermissionError("No access to feature-views or no feature-views available.") + raise PermissionError( + "No access to feature-views or no feature-views available." + ) store.materialize_incremental(end_date=datetime.now()) except PermissionError as pe: print(f"Permission error: {pe}") @@ -74,9 +76,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: # values we're using for an on-demand transformation "val_to_add": [1, 2, 3], "val_to_add_2": [10, 20, 30], - } - ) if for_batch_scoring: entity_df["event_timestamp"] = pd.to_datetime("now", utc=True) diff --git a/examples/rbac-remote/client/oidc/feature_repo/test.py b/examples/rbac-remote/client/oidc/feature_repo/test.py index 6e1480bc947..96fe678d5f8 100644 --- a/examples/rbac-remote/client/oidc/feature_repo/test.py +++ b/examples/rbac-remote/client/oidc/feature_repo/test.py @@ -20,9 +20,11 @@ def run_demo(): try: print("\n--- Load features into online store/materialize_incremental ---") - feature_views= store.list_feature_views() + feature_views = store.list_feature_views() if not feature_views: - raise PermissionError("No access to feature-views or no feature-views available.") + raise PermissionError( + "No access to feature-views or no feature-views available." + ) store.materialize_incremental(end_date=datetime.now()) except PermissionError as pe: print(f"Permission error: {pe}") @@ -74,9 +76,7 @@ def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: # values we're using for an on-demand transformation "val_to_add": [1, 2, 3], "val_to_add_2": [10, 20, 30], - } - ) if for_batch_scoring: entity_df["event_timestamp"] = pd.to_datetime("now", utc=True) diff --git a/examples/rbac-remote/server/feature_repo/example_repo.py b/examples/rbac-remote/server/feature_repo/example_repo.py index 5b8105bb948..08914f33b73 100644 --- a/examples/rbac-remote/server/feature_repo/example_repo.py +++ b/examples/rbac-remote/server/feature_repo/example_repo.py @@ -5,7 +5,9 @@ import pandas as pd from feast import Entity, FeatureService, FeatureView, Field, PushSource, RequestSource -from feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source import PostgreSQLSource +from feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source import ( + PostgreSQLSource, +) from feast.on_demand_feature_view import on_demand_feature_view from feast.types import Float32, Float64, Int64 diff --git a/examples/rbac-remote/server/feature_repo/permissions_apply.py b/examples/rbac-remote/server/feature_repo/permissions_apply.py index 93bdf2ffc62..244d0bcc73f 100644 --- a/examples/rbac-remote/server/feature_repo/permissions_apply.py +++ b/examples/rbac-remote/server/feature_repo/permissions_apply.py @@ -10,12 +10,12 @@ name="feast_user_permission", types=ALL_RESOURCE_TYPES, policy=RoleBasedPolicy(roles=user_roles), - actions=[AuthzedAction.DESCRIBE] + READ + actions=[AuthzedAction.DESCRIBE] + READ, ) admin_perm = Permission( name="feast_admin_permission", types=ALL_RESOURCE_TYPES, policy=RoleBasedPolicy(roles=admin_roles), - actions=ALL_ACTIONS + actions=ALL_ACTIONS, ) diff --git a/examples/rhoai-quickstart/feast-demo-quickstart.ipynb b/examples/rhoai-quickstart/feast-demo-quickstart.ipynb index 8777798e49f..0e6eab794db 100644 --- a/examples/rhoai-quickstart/feast-demo-quickstart.ipynb +++ b/examples/rhoai-quickstart/feast-demo-quickstart.ipynb @@ -230,7 +230,7 @@ "id": "cd23ce19-03fa-4353-86d3-261c7f514fd8", "metadata": {}, "source": [ - "File `data/driver_stats.parquet` is generated by the `feast init` command and it acts a historical information source to this example. We have defined this source in the [my_feast_project/feature_repo/example_repo.py](./my_feast_project/feature_repo/example_repo.py) file.\n", + "File `data/driver_stats.parquet` is generated by the `feast init` command and it acts a historical information source to this example. We have defined this source in the [my_feast_project/feature_repo/feature_definitions.py](./my_feast_project/feature_repo/feature_definitions.py) file.\n", "\n", "```python\n", "driver_stats_source = FileSource(\n", diff --git a/go.mod b/go.mod index a097aa67719..4de94b829d6 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,52 @@ module github.com/feast-dev/feast -go 1.24.0 - -toolchain go1.24.4 +go 1.25.0 require ( + cloud.google.com/go/storage v1.58.0 github.com/apache/arrow/go/v17 v17.0.0 github.com/aws/aws-sdk-go-v2 v1.36.4 github.com/aws/aws-sdk-go-v2/config v1.29.14 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.3 github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 + github.com/cabify/gotoprom v1.1.0 github.com/ghodss/yaml v1.0.0 + github.com/go-sql-driver/mysql v1.8.1 github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 + github.com/jackc/pgx/v5 v5.8.0 github.com/mattn/go-sqlite3 v1.14.23 github.com/pkg/errors v0.9.1 - github.com/redis/go-redis/v9 v9.6.1 + github.com/prometheus/client_golang v1.23.2 + github.com/redis/go-redis/v9 v9.20.0 github.com/roberson-io/mmh3 v0.0.0-20190729202758-fdfce3ba6225 github.com/rs/zerolog v1.33.0 github.com/spaolacci/murmur3 v1.1.0 github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 - go.opentelemetry.io/otel/sdk v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 - golang.org/x/sync v0.17.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 - google.golang.org/grpc v1.75.0 - google.golang.org/protobuf v1.36.8 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 + golang.org/x/sync v0.20.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa + google.golang.org/grpc v1.81.1 + google.golang.org/protobuf v1.36.11 ) require ( + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.2 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/apache/thrift v0.21.0 // indirect @@ -51,38 +66,68 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect github.com/aws/smithy-go v1.22.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect - github.com/klauspost/compress v1.17.9 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.1 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/tools v0.35.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.44.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/api v0.256.0 // indirect + google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f2530758915..f936e04c8f5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,39 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo= +cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apache/arrow/go/v17 v17.0.0 h1:RRR2bdqKcdbss9Gxy2NS/hK8i4LDMh23L6BbkN5+F54= @@ -46,29 +80,57 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/Xv github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cabify/gotoprom v1.1.0 h1:IyM06IuVDPpEhBdXqSIfQK1KGrerkjGkDamrQqu8dWo= +github.com/cabify/gotoprom v1.1.0/go.mod h1:8H4gdB+iJqM8QrNneQxxbYsx4xA7m3h1BP8K7h16R4w= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -77,20 +139,46 @@ github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81A github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -99,89 +187,157 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0= +github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/roberson-io/mmh3 v0.0.0-20190729202758-fdfce3ba6225 h1:ZMsPCp7oYgjoIFt1c+sM2qojxZXotSYcMF8Ur9/LJlM= github.com/roberson-io/mmh3 v0.0.0-20190729202758-fdfce3ba6225/go.mod h1:XEESr+X1SY8ZSuc3jqsTlb3clCkqQJ4DcF3Qxv1N3PM= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/README.md b/go/README.md index a8e381519a4..308787cac9f 100644 --- a/go/README.md +++ b/go/README.md @@ -6,14 +6,27 @@ To build and run the Go Feature Server locally, create a feature_store.yaml file ```bash go build -o feast-go ./go/main.go - # start the http server - ./feast-go --type=http --port=8080 + # start the http server (metrics on port 9090 by default) + ./feast-go --type=http --port=8080 --metrics-port=9090 # or start the gRPC server - #./feast-go --type=grpc --port=[your-choice] + #./feast-go --type=grpc --port=[your-choice] --metrics-port=9091 ``` +## Prometheus Metrics +The server exposes Prometheus metrics at the `/metrics` endpoint on a dedicated port (default `:9090`). +- **HTTP Mode**: Metrics server runs on port `9090` (configurable via `-metrics-port`). +- **gRPC Mode**: Metrics server runs on port `9090` (configurable via `-metrics-port`). + +Key metrics include: +- `http_request_duration_seconds`: Histogram of response latency. +- `http_requests_total`: Counter of HTTP requests by status, method, and path. +- Standard Go and Process metrics. + +A `/health` endpoint is available on the main application port (default `:8080`) for readiness probes. + ## OTEL based observability The OS level env variable `ENABLE_OTEL_TRACING=="true"/"false"` (string type) is used to enable/disable this service (with Tracing only). +You can also configure the service name using `OTEL_SERVICE_NAME` env variable (defaults to "FeastGoFeatureServer"). The default exporter URL is "http://localhost:4318". The default schema of sending data to collector is **HTTP**. Please refer the following two docs about the configuration of the OTEL exporter: 1. https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ diff --git a/go/infra/docker/feature-server/Dockerfile b/go/infra/docker/feature-server/Dockerfile index a02a98332ab..3f7a2ab7b94 100644 --- a/go/infra/docker/feature-server/Dockerfile +++ b/go/infra/docker/feature-server/Dockerfile @@ -1,4 +1,5 @@ -FROM golang:1.23.12 +FROM golang:1.25 +ENV GOTOOLCHAIN=auto # Update the package list and install the ca-certificates package RUN apt-get update && apt-get install -y ca-certificates @@ -28,4 +29,4 @@ EXPOSE 8080 # Command to run the executable # Pass arguments to the executable (Ex: ./feast --type=grpc) -CMD ["./feast"] \ No newline at end of file +CMD ["./feast"] diff --git a/go/internal/feast/metrics/metrics.go b/go/internal/feast/metrics/metrics.go new file mode 100644 index 00000000000..d4783f257b7 --- /dev/null +++ b/go/internal/feast/metrics/metrics.go @@ -0,0 +1,65 @@ +package metrics + +import ( + "reflect" + "time" + + "github.com/cabify/gotoprom" + "github.com/cabify/gotoprom/prometheusvanilla" + "github.com/prometheus/client_golang/prometheus" +) + +var HttpMetrics struct { + Duration func(HttpLabels) TimeHistogram `name:"http_request_duration_seconds" help:"Time taken to serve HTTP requests" buckets:".005,.01,.025,.05,.1,.25,.5,1,2.5,5,10"` + + RequestsTotal func(HttpLabels) prometheus.Counter `name:"http_requests_total" help:"Total number of HTTP requests"` +} + +type HttpLabels struct { + Method string `label:"method"` + Status int `label:"status"` + Path string `label:"path"` +} + +func init() { + gotoprom.MustAddBuilder(TimeHistogramType, RegisterTimeHistogram) + gotoprom.MustInit(&HttpMetrics, "feast") +} + +var ( + TimeHistogramType = reflect.TypeOf((*TimeHistogram)(nil)).Elem() +) + +func RegisterTimeHistogram(name, help, namespace string, labelNames []string, tag reflect.StructTag) (func(prometheus.Labels) interface{}, prometheus.Collector, error) { + f, collector, err := prometheusvanilla.BuildHistogram(name, help, namespace, labelNames, tag) + if err != nil { + return nil, nil, err + } + + return func(labels prometheus.Labels) interface{} { + return timeHistogramAdapter{Histogram: f(labels).(prometheus.Histogram)} + }, collector, nil +} + +// TimeHistogram offers the basic prometheus.Histogram functionality +type TimeHistogram interface { + prometheus.Histogram + // Duration observes the duration in seconds + Duration(duration time.Duration) + // Since observes the duration in seconds since the time point provided + Since(time.Time) +} + +type timeHistogramAdapter struct { + prometheus.Histogram +} + +// Duration observes the duration in seconds +func (to timeHistogramAdapter) Duration(duration time.Duration) { + to.Observe(duration.Seconds()) +} + +// Since observes the duration in seconds since the time point provided +func (to timeHistogramAdapter) Since(duration time.Time) { + to.Duration(time.Since(duration)) +} diff --git a/go/internal/feast/onlineserving/serving.go b/go/internal/feast/onlineserving/serving.go index 2ae733b62bb..1ce5f6c555c 100644 --- a/go/internal/feast/onlineserving/serving.go +++ b/go/internal/feast/onlineserving/serving.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "errors" "fmt" + "regexp" "sort" "strings" @@ -20,6 +21,8 @@ import ( "github.com/feast-dev/feast/go/types" ) +var versionTagRegex = regexp.MustCompile(`^[vV]\d+$`) + /* FeatureVector type represent result of retrieving single feature for multiple rows. It can be imagined as a column in output dataframe / table. @@ -272,7 +275,9 @@ func ValidateEntityValues(joinKeyValues map[string]*prototypes.RepeatedValue, requestData map[string]*prototypes.RepeatedValue, expectedJoinKeysSet map[string]interface{}) (int, error) { numRows := -1 - + if err := validateJoinKeys(joinKeyValues, expectedJoinKeysSet); err != nil { + return -1, errors.New("valueError: " + err.Error()) + } for joinKey, values := range joinKeyValues { if _, ok := expectedJoinKeysSet[joinKey]; !ok { requestData[joinKey] = values @@ -490,6 +495,18 @@ func ParseFeatureReference(featureRef string) (featureViewName, featureName stri featureViewName = parsedFeatureName[0] featureName = parsedFeatureName[1] } + + // Handle @version qualifier on feature view name + if atIdx := strings.Index(featureViewName, "@"); atIdx >= 0 { + suffix := featureViewName[atIdx+1:] + if versionTagRegex.MatchString(suffix) { + e = fmt.Errorf("versioned feature refs (@%s) are not supported by the Go feature server", suffix) + return + } + if strings.EqualFold(suffix, "latest") { + featureViewName = featureViewName[:atIdx] + } + } return } @@ -647,6 +664,17 @@ func getQualifiedFeatureName(viewName string, featureName string, fullFeatureNam } } +func validateJoinKeys( + joinKeyValues map[string]*prototypes.RepeatedValue, + expectedJoinKeysSet map[string]interface{}) error { + for joinKey := range joinKeyValues { + if _, ok := expectedJoinKeysSet[joinKey]; !ok { + return fmt.Errorf("Invalid entity join key. key=%s", joinKey) + } + } + return nil +} + type featureNameCollisionError struct { featureRefCollisions []string fullFeatureNames bool diff --git a/go/internal/feast/onlinestore/dynamodbonlinestore.go b/go/internal/feast/onlinestore/dynamodbonlinestore.go index 0a8ccbc855d..235449bfdf2 100644 --- a/go/internal/feast/onlinestore/dynamodbonlinestore.go +++ b/go/internal/feast/onlinestore/dynamodbonlinestore.go @@ -50,11 +50,10 @@ func NewDynamodbOnlineStore(project string, config *registry.RepoConfig, onlineS } // aws configuration - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + ctx := context.Background() cfg, err := awsConfig.LoadDefaultConfig(ctx) if err != nil { - panic(err) + return nil, err } store.client = dynamodb.NewFromConfig(cfg) @@ -238,24 +237,64 @@ func (d *DynamodbOnlineStore) OnlineRead(ctx context.Context, entityKeys []*type // process response from dynamodb for j := 0; j < batchSize; j++ { - entityId := Responses[j]["entity_id"].(*dtypes.AttributeValueMemberS).Value - timestampString := Responses[j]["event_ts"].(*dtypes.AttributeValueMemberS).Value + entityIdAttr, ok := Responses[j]["entity_id"] + if !ok || entityIdAttr == nil { + continue + } + entityIdMember, ok := entityIdAttr.(*dtypes.AttributeValueMemberS) + if !ok { + return fmt.Errorf("unexpected DynamoDB attribute type for 'entity_id' in table %s", tableName) + } + entityId := entityIdMember.Value + + tsAttr, ok := Responses[j]["event_ts"] + if !ok || tsAttr == nil { + continue + } + tsMember, ok := tsAttr.(*dtypes.AttributeValueMemberS) + if !ok { + return fmt.Errorf("unexpected DynamoDB attribute type for 'event_ts' in table %s", tableName) + } + timestampString := tsMember.Value + t, err := time.Parse("2006-01-02 15:04:05-07:00", timestampString) if err != nil { return err } timeStamp := timestamppb.New(t) - featureValues := Responses[j]["values"].(*dtypes.AttributeValueMemberM).Value + rawValues, ok := Responses[j]["values"] + if !ok || rawValues == nil { + continue + } + valuesMap, ok := rawValues.(*dtypes.AttributeValueMemberM) + if !ok { + return fmt.Errorf("unexpected DynamoDB attribute type for 'values' in table %s", tableName) + } + featureValues := valuesMap.Value entityIndex := entityIndexMap[entityId] for _, featureName := range featureNames { - featureValue := featureValues[featureName].(*dtypes.AttributeValueMemberB).Value + featureIndex := featureNamesIndex[featureName] + rawVal, exists := featureValues[featureName] + if !exists || rawVal == nil { + mu.Lock() + results[entityIndex][featureIndex] = FeatureData{ + Reference: serving.FeatureReferenceV2{FeatureViewName: featureViewName, FeatureName: featureName}, + Timestamp: timestamppb.Timestamp{Seconds: timeStamp.Seconds, Nanos: timeStamp.Nanos}, + Value: types.Value{Val: &types.Value_NullVal{NullVal: types.Null_NULL}}, + } + mu.Unlock() + continue + } + memberB, ok := rawVal.(*dtypes.AttributeValueMemberB) + if !ok { + return fmt.Errorf("unexpected DynamoDB attribute type for feature %q in view %q", featureName, featureViewName) + } var value types.Value - if err := proto.Unmarshal(featureValue, &value); err != nil { + if err := proto.Unmarshal(memberB.Value, &value); err != nil { return err } - featureIndex := featureNamesIndex[featureName] mu.Lock() results[entityIndex][featureIndex] = FeatureData{Reference: serving.FeatureReferenceV2{FeatureViewName: featureViewName, FeatureName: featureName}, diff --git a/go/internal/feast/onlinestore/onlinestore.go b/go/internal/feast/onlinestore/onlinestore.go index a2652af2e1b..837a3f31d82 100644 --- a/go/internal/feast/onlinestore/onlinestore.go +++ b/go/internal/feast/onlinestore/onlinestore.go @@ -66,8 +66,11 @@ func NewOnlineStore(config *registry.RepoConfig) (OnlineStore, error) { } else if onlineStoreType == "dynamodb" { onlineStore, err := NewDynamodbOnlineStore(config.Project, config, config.OnlineStore) return onlineStore, err + } else if onlineStoreType == "postgres" { + onlineStore, err := NewPostgresOnlineStore(config.Project, config, config.OnlineStore) + return onlineStore, err } else { - return nil, fmt.Errorf("%s online store type is currently not supported; only redis and sqlite are supported", onlineStoreType) + return nil, fmt.Errorf("%s online store type is currently not supported; only redis, sqlite, dynamodb, and postgres are supported", onlineStoreType) } } diff --git a/go/internal/feast/onlinestore/postgresonlinestore.go b/go/internal/feast/onlinestore/postgresonlinestore.go new file mode 100644 index 00000000000..4077a9e06fa --- /dev/null +++ b/go/internal/feast/onlinestore/postgresonlinestore.go @@ -0,0 +1,197 @@ +package onlinestore + +import ( + "context" + "fmt" + "net/url" + "strings" + "time" + + "github.com/feast-dev/feast/go/internal/feast/registry" + "github.com/feast-dev/feast/go/protos/feast/serving" + "github.com/feast-dev/feast/go/protos/feast/types" + "github.com/jackc/pgx/v5/pgxpool" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type PostgresOnlineStore struct { + project string + pool *pgxpool.Pool + config *registry.RepoConfig +} + +func NewPostgresOnlineStore(project string, config *registry.RepoConfig, onlineStoreConfig map[string]interface{}) (*PostgresOnlineStore, error) { + connStr := buildPostgresConnString(onlineStoreConfig) + poolConfig, err := pgxpool.ParseConfig(connStr) + if err != nil { + return nil, fmt.Errorf("failed to parse postgres config: %w", err) + } + + if schema, ok := onlineStoreConfig["db_schema"].(string); ok && schema != "" { + poolConfig.ConnConfig.RuntimeParams["search_path"] = schema + } + + pool, err := pgxpool.NewWithConfig(context.Background(), poolConfig) + if err != nil { + return nil, fmt.Errorf("failed to create postgres pool: %w", err) + } + + return &PostgresOnlineStore{ + project: project, + pool: pool, + config: config, + }, nil +} + +func (p *PostgresOnlineStore) Destruct() { + if p.pool != nil { + p.pool.Close() + } +} + +func (p *PostgresOnlineStore) OnlineRead(ctx context.Context, entityKeys []*types.EntityKey, featureViewNames []string, featureNames []string) ([][]FeatureData, error) { + featureCount := len(featureNames) + results := make([][]FeatureData, len(entityKeys)) + + serializedKeys := make([][]byte, len(entityKeys)) + entityKeyMap := make(map[string]int, len(entityKeys)) + + for i, entityKey := range entityKeys { + serKey, err := serializeEntityKey(entityKey, p.config.EntityKeySerializationVersion) + if err != nil { + return nil, err + } + serializedKeys[i] = *serKey + entityKeyMap[string(*serKey)] = i + } + + type featureRef struct { + name string + index int + } + featuresByView := make(map[string][]featureRef) + for i, viewName := range featureViewNames { + featuresByView[viewName] = append(featuresByView[viewName], featureRef{ + name: featureNames[i], + index: i, + }) + } + + for viewName, features := range featuresByView { + featureNamesToIdx := make(map[string]int, len(features)) + for _, f := range features { + featureNamesToIdx[f.name] = f.index + } + + tableName := fmt.Sprintf(`"%s"`, strings.ReplaceAll(tableId(p.project, viewName), `"`, `""`)) + query := fmt.Sprintf( + `SELECT entity_key, feature_name, value, event_ts FROM %s WHERE entity_key = ANY($1)`, + tableName, + ) + + rows, err := p.pool.Query(ctx, query, serializedKeys) + if err != nil { + return nil, fmt.Errorf("failed to query postgres: %w", err) + } + + for rows.Next() { + var entityKeyBytes []byte + var featureName string + var valueBytes []byte + var eventTs time.Time + + if err := rows.Scan(&entityKeyBytes, &featureName, &valueBytes, &eventTs); err != nil { + rows.Close() + return nil, fmt.Errorf("failed to scan postgres row: %w", err) + } + + rowIdx, ok := entityKeyMap[string(entityKeyBytes)] + if !ok { + continue + } + + if results[rowIdx] == nil { + results[rowIdx] = make([]FeatureData, featureCount) + } + + if featureIdx, ok := featureNamesToIdx[featureName]; ok { + var value types.Value + if err := proto.Unmarshal(valueBytes, &value); err != nil { + rows.Close() + return nil, fmt.Errorf("failed to unmarshal feature value: %w", err) + } + + results[rowIdx][featureIdx] = FeatureData{ + Reference: serving.FeatureReferenceV2{FeatureViewName: viewName, FeatureName: featureName}, + Timestamp: *timestamppb.New(eventTs), + Value: types.Value{Val: value.Val}, + } + } + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating postgres rows: %w", err) + } + } + + return results, nil +} + +func buildPostgresConnString(config map[string]interface{}) string { + host, _ := config["host"].(string) + if host == "" { + host = "localhost" + } + + port := 5432 + if p, ok := config["port"].(float64); ok { + port = int(p) + } + + database, _ := config["database"].(string) + user, _ := config["user"].(string) + password, _ := config["password"].(string) + + var userInfo *url.Userinfo + if user != "" { + if password != "" { + userInfo = url.UserPassword(user, password) + } else { + userInfo = url.User(user) + } + } + + query := url.Values{} + if sslMode, ok := config["sslmode"].(string); ok && sslMode != "" { + query.Set("sslmode", sslMode) + } else { + query.Set("sslmode", "require") + } + + if v, ok := config["sslcert_path"].(string); ok && v != "" { + query.Set("sslcert", v) + } + if v, ok := config["sslkey_path"].(string); ok && v != "" { + query.Set("sslkey", v) + } + if v, ok := config["sslrootcert_path"].(string); ok && v != "" { + query.Set("sslrootcert", v) + } + if v, ok := config["min_conn"].(float64); ok { + query.Set("pool_min_conns", fmt.Sprintf("%d", int(v))) + } + if v, ok := config["max_conn"].(float64); ok { + query.Set("pool_max_conns", fmt.Sprintf("%d", int(v))) + } + + connURL := url.URL{ + Scheme: "postgres", + User: userInfo, + Host: fmt.Sprintf("%s:%d", host, port), + Path: database, + RawQuery: query.Encode(), + } + + return connURL.String() +} diff --git a/go/internal/feast/onlinestore/postgresonlinestore_test.go b/go/internal/feast/onlinestore/postgresonlinestore_test.go new file mode 100644 index 00000000000..b241e857e18 --- /dev/null +++ b/go/internal/feast/onlinestore/postgresonlinestore_test.go @@ -0,0 +1,145 @@ +package onlinestore + +import ( + "testing" + + "github.com/feast-dev/feast/go/internal/feast/registry" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/stretchr/testify/assert" +) + +func TestBuildPostgresConnString(t *testing.T) { + config := map[string]interface{}{ + "type": "postgres", + "host": "db.example.com", + "port": float64(5432), + "database": "feast", + "user": "feast_user", + "password": "feast_pass", + "sslmode": "require", + } + connStr := buildPostgresConnString(config) + assert.Contains(t, connStr, "db.example.com:5432") + assert.Contains(t, connStr, "feast_user:") + assert.Contains(t, connStr, "feast_pass@") + assert.Contains(t, connStr, "/feast") + assert.Contains(t, connStr, "sslmode=require") +} + +func TestBuildPostgresConnStringDefaults(t *testing.T) { + config := map[string]interface{}{ + "database": "feast", + "user": "feast_user", + "password": "feast_pass", + } + connStr := buildPostgresConnString(config) + assert.Contains(t, connStr, "localhost:5432") + assert.Contains(t, connStr, "sslmode=require") +} + +func TestBuildPostgresConnStringWithSSL(t *testing.T) { + config := map[string]interface{}{ + "host": "db.example.com", + "port": float64(5433), + "database": "feast", + "user": "feast_user", + "password": "feast_pass", + "sslmode": "verify-full", + "sslcert_path": "/path/to/cert", + "sslkey_path": "/path/to/key", + "sslrootcert_path": "/path/to/rootcert", + } + connStr := buildPostgresConnString(config) + assert.Contains(t, connStr, "db.example.com:5433") + assert.Contains(t, connStr, "sslmode=verify-full") + assert.Contains(t, connStr, "sslcert=%2Fpath%2Fto%2Fcert") + assert.Contains(t, connStr, "sslkey=%2Fpath%2Fto%2Fkey") + assert.Contains(t, connStr, "sslrootcert=%2Fpath%2Fto%2Frootcert") +} + +func TestBuildPostgresConnStringWithPooling(t *testing.T) { + config := map[string]interface{}{ + "host": "localhost", + "database": "feast", + "user": "feast_user", + "password": "feast_pass", + "min_conn": float64(2), + "max_conn": float64(10), + } + connStr := buildPostgresConnString(config) + assert.Contains(t, connStr, "pool_min_conns=2") + assert.Contains(t, connStr, "pool_max_conns=10") +} + +func TestBuildPostgresConnStringSpecialChars(t *testing.T) { + config := map[string]interface{}{ + "host": "localhost", + "database": "feast", + "user": "user@domain", + "password": "p@ss=word&special", + } + connStr := buildPostgresConnString(config) + poolConfig, err := pgxpool.ParseConfig(connStr) + assert.Nil(t, err) + assert.Equal(t, "user@domain", poolConfig.ConnConfig.User) + assert.Equal(t, "p@ss=word&special", poolConfig.ConnConfig.Password) +} + +func TestBuildPostgresConnStringParseable(t *testing.T) { + config := map[string]interface{}{ + "host": "localhost", + "port": float64(5432), + "database": "feast", + "user": "feast_user", + "password": "feast_pass", + } + connStr := buildPostgresConnString(config) + poolConfig, err := pgxpool.ParseConfig(connStr) + assert.Nil(t, err) + assert.Equal(t, "localhost", poolConfig.ConnConfig.Host) + assert.Equal(t, uint16(5432), poolConfig.ConnConfig.Port) + assert.Equal(t, "feast", poolConfig.ConnConfig.Database) + assert.Equal(t, "feast_user", poolConfig.ConnConfig.User) + assert.Equal(t, "feast_pass", poolConfig.ConnConfig.Password) +} + +func TestNewPostgresOnlineStore(t *testing.T) { + config := map[string]interface{}{ + "type": "postgres", + "host": "localhost", + "port": float64(5432), + "database": "feast", + "user": "feast_user", + "password": "feast_pass", + } + rc := ®istry.RepoConfig{ + OnlineStore: config, + EntityKeySerializationVersion: 2, + } + store, err := NewPostgresOnlineStore("test", rc, config) + assert.Nil(t, err) + assert.NotNil(t, store) + assert.Equal(t, "feast", store.pool.Config().ConnConfig.Database) + store.Destruct() +} + +func TestNewPostgresOnlineStoreWithSchema(t *testing.T) { + config := map[string]interface{}{ + "type": "postgres", + "host": "localhost", + "port": float64(5432), + "database": "feast", + "user": "feast_user", + "password": "feast_pass", + "db_schema": "custom_schema", + } + rc := ®istry.RepoConfig{ + OnlineStore: config, + EntityKeySerializationVersion: 2, + } + store, err := NewPostgresOnlineStore("test", rc, config) + assert.Nil(t, err) + assert.NotNil(t, store) + assert.Equal(t, "custom_schema", store.pool.Config().ConnConfig.RuntimeParams["search_path"]) + store.Destruct() +} diff --git a/go/internal/feast/registry/gcs.go b/go/internal/feast/registry/gcs.go new file mode 100644 index 00000000000..6d13a34e11d --- /dev/null +++ b/go/internal/feast/registry/gcs.go @@ -0,0 +1,113 @@ +package registry + +import ( + "context" + "errors" + "io" + "net/url" + "strings" + "time" + + "cloud.google.com/go/storage" + "google.golang.org/protobuf/proto" + + "github.com/feast-dev/feast/go/protos/feast/core" +) + +// GCSObjectReader defines the interface for reading GCS objects to allow mocking in tests. +type GCSObjectReader interface { + GetObject(ctx context.Context, bucket string, object string) (io.ReadCloser, error) + DeleteObject(ctx context.Context, bucket string, object string) error +} + +// GCSClient implements GCSObjectReader using the real GCS SDK. +type GCSClient struct { + client *storage.Client +} + +func (g *GCSClient) GetObject(ctx context.Context, bucket string, object string) (io.ReadCloser, error) { + return g.client.Bucket(bucket).Object(object).NewReader(ctx) +} + +func (g *GCSClient) DeleteObject(ctx context.Context, bucket string, object string) error { + return g.client.Bucket(bucket).Object(object).Delete(ctx) +} + +// GCSRegistryStore is a GCS bucket-based implementation of the RegistryStore interface. +type GCSRegistryStore struct { + registryPath string + client GCSObjectReader +} + +// NewGCSRegistryStore creates a GCSRegistryStore with the given configuration. +func NewGCSRegistryStore(config *RegistryConfig, repoPath string) *GCSRegistryStore { + var rs GCSRegistryStore + ctx := context.Background() + + client, err := storage.NewClient(ctx) + if err != nil { + rs = GCSRegistryStore{ + registryPath: config.Path, + } + } else { + rs = GCSRegistryStore{ + registryPath: config.Path, + client: &GCSClient{client: client}, + } + } + return &rs +} + +// GetRegistryProto reads and parses the registry proto from the GCS bucket object. +func (g *GCSRegistryStore) GetRegistryProto() (*core.Registry, error) { + bucket, object, err := g.parseGCSPath() + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + reader, err := g.client.GetObject(ctx, bucket, object) + if err != nil { + return nil, err + } + defer reader.Close() + + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + registry := &core.Registry{} + if err := proto.Unmarshal(data, registry); err != nil { + return nil, err + } + return registry, nil +} + +func (g *GCSRegistryStore) UpdateRegistryProto(rp *core.Registry) error { + return errors.New("not implemented in GCSRegistryStore") +} + +func (g *GCSRegistryStore) Teardown() error { + bucket, object, err := g.parseGCSPath() + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + return g.client.DeleteObject(ctx, bucket, object) +} + +func (g *GCSRegistryStore) parseGCSPath() (string, string, error) { + uri, err := url.Parse(g.registryPath) + if err != nil { + return "", "", errors.New("invalid GCS registry path format") + } + bucket := uri.Host + object := strings.TrimPrefix(uri.Path, "/") + return bucket, object, nil +} diff --git a/go/internal/feast/registry/mysql_registry_store.go b/go/internal/feast/registry/mysql_registry_store.go new file mode 100644 index 00000000000..f67e5a2eaf6 --- /dev/null +++ b/go/internal/feast/registry/mysql_registry_store.go @@ -0,0 +1,389 @@ +// Package registry implements Feast registry stores. +// +// MySQL Registry Store: +// The MySQL registry store provides read-only access to a Feast registry stored in MySQL. +// It queries a database schema matching the Python SQLAlchemy schema defined in +// sdk/python/feast/infra/registry/sql.py. When the Python schema evolves, the Go queries +// in this package must be updated accordingly. +package registry + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net" + "net/url" + "strings" + "sync" + "time" + + "github.com/feast-dev/feast/go/protos/feast/core" + "github.com/go-sql-driver/mysql" + "github.com/rs/zerolog/log" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// SQL queries for loading registry objects from the MySQL registry. +// These queries assume the schema defined in sdk/python/feast/infra/registry/sql.py. +const ( + queryProjects = "SELECT project_proto FROM projects WHERE project_id = ?" + queryEntities = "SELECT entity_proto FROM entities WHERE project_id = ?" + queryFeatureViews = "SELECT feature_view_proto FROM feature_views WHERE project_id = ?" + queryStreamFeatureViews = "SELECT feature_view_proto FROM stream_feature_views WHERE project_id = ?" + queryOnDemandFeatureViews = "SELECT feature_view_proto FROM on_demand_feature_views WHERE project_id = ?" + queryFeatureServices = "SELECT feature_service_proto FROM feature_services WHERE project_id = ?" + queryDataSources = "SELECT data_source_proto FROM data_sources WHERE project_id = ?" + querySavedDatasets = "SELECT saved_dataset_proto FROM saved_datasets WHERE project_id = ?" + queryValidationReferences = "SELECT validation_reference_proto FROM validation_references WHERE project_id = ?" + queryPermissions = "SELECT permission_proto FROM permissions WHERE project_id = ?" + queryManagedInfra = "SELECT infra_proto FROM managed_infra WHERE project_id = ?" + queryMaxLastUpdated = "SELECT MAX(last_updated_timestamp) FROM projects WHERE project_id = ?" +) + +type MySQLRegistryStore struct { + dsn string + dsnErr error + db *sql.DB + dbOnce sync.Once + dbErr error + project string + driverName string + registryConfig *RegistryConfig +} + +// NewMySQLRegistryStore creates a MySQLRegistryStore from a SQLAlchemy-style URL or a raw DSN. +func NewMySQLRegistryStore(config *RegistryConfig, repoPath string, project string) *MySQLRegistryStore { + dsn, err := mysqlURLToDSN(config.Path) + return &MySQLRegistryStore{ + dsn: dsn, + dsnErr: err, + project: project, + driverName: "mysql", + registryConfig: config, + } +} + +// newMySQLRegistryStoreWithDB is for tests to inject a pre-configured DB handle. +func newMySQLRegistryStoreWithDB(db *sql.DB, project string) *MySQLRegistryStore { + return &MySQLRegistryStore{ + db: db, + project: project, + driverName: "mysql", + } +} + +func (r *MySQLRegistryStore) GetRegistryProto() (*core.Registry, error) { + if r.project == "" { + return nil, errors.New("mysql registry store requires a project name") + } + db, err := r.getDB() + if err != nil { + return nil, err + } + + queryTimeout := r.getQueryTimeout() + ctx, cancel := context.WithTimeout(context.Background(), queryTimeout) + defer cancel() + + if err := db.PingContext(ctx); err != nil { + return nil, fmt.Errorf("failed to ping MySQL registry database: %w", err) + } + + registry := &core.Registry{ + RegistrySchemaVersion: REGISTRY_SCHEMA_VERSION, + } + + projects, err := readProtoRows(ctx, db, + queryProjects, + []any{r.project}, + func() *core.Project { return &core.Project{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load projects: %w", err) + } + registry.Projects = projects + + if lastUpdated, err := r.getMaxProjectUpdatedTimestamp(ctx, db); err == nil && lastUpdated != nil { + registry.LastUpdated = lastUpdated + } + + entities, err := readProtoRows(ctx, db, + queryEntities, + []any{r.project}, + func() *core.Entity { return &core.Entity{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load entities: %w", err) + } + registry.Entities = entities + + featureViews, err := readProtoRows(ctx, db, + queryFeatureViews, + []any{r.project}, + func() *core.FeatureView { return &core.FeatureView{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load feature_views: %w", err) + } + registry.FeatureViews = featureViews + + streamFeatureViews, err := readProtoRows(ctx, db, + queryStreamFeatureViews, + []any{r.project}, + func() *core.StreamFeatureView { return &core.StreamFeatureView{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load stream_feature_views: %w", err) + } + registry.StreamFeatureViews = streamFeatureViews + + onDemandFeatureViews, err := readProtoRows(ctx, db, + queryOnDemandFeatureViews, + []any{r.project}, + func() *core.OnDemandFeatureView { return &core.OnDemandFeatureView{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load on_demand_feature_views: %w", err) + } + registry.OnDemandFeatureViews = onDemandFeatureViews + + featureServices, err := readProtoRows(ctx, db, + queryFeatureServices, + []any{r.project}, + func() *core.FeatureService { return &core.FeatureService{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load feature_services: %w", err) + } + registry.FeatureServices = featureServices + + dataSources, err := readProtoRows(ctx, db, + queryDataSources, + []any{r.project}, + func() *core.DataSource { return &core.DataSource{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load data_sources: %w", err) + } + registry.DataSources = dataSources + + savedDatasets, err := readProtoRows(ctx, db, + querySavedDatasets, + []any{r.project}, + func() *core.SavedDataset { return &core.SavedDataset{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load saved_datasets: %w", err) + } + registry.SavedDatasets = savedDatasets + + validationReferences, err := readProtoRows(ctx, db, + queryValidationReferences, + []any{r.project}, + func() *core.ValidationReference { return &core.ValidationReference{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load validation_references: %w", err) + } + registry.ValidationReferences = validationReferences + + permissions, err := readProtoRows(ctx, db, + queryPermissions, + []any{r.project}, + func() *core.Permission { return &core.Permission{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load permissions: %w", err) + } + registry.Permissions = permissions + + infra, err := readProtoRows(ctx, db, + queryManagedInfra, + []any{r.project}, + func() *core.Infra { return &core.Infra{} }, + ) + if err != nil { + return nil, fmt.Errorf("failed to load managed_infra: %w", err) + } + if len(infra) > 0 { + registry.Infra = infra[0] + } + + log.Debug().Str("project", r.project).Msg("Loaded registry from MySQL") + return registry, nil +} + +func (r *MySQLRegistryStore) UpdateRegistryProto(rp *core.Registry) error { + return errors.New("not implemented in MySQLRegistryStore") +} + +func (r *MySQLRegistryStore) Teardown() error { + if r.db != nil { + return r.db.Close() + } + return nil +} + +func (r *MySQLRegistryStore) getDB() (*sql.DB, error) { + r.dbOnce.Do(func() { + if r.db != nil { + // Already initialized (e.g., via newMySQLRegistryStoreWithDB for tests) + return + } + if r.dsnErr != nil { + r.dbErr = fmt.Errorf("invalid MySQL registry DSN: %w", r.dsnErr) + return + } + if r.dsn == "" { + r.dbErr = errors.New("mysql registry store requires a non-empty DSN") + return + } + db, err := sql.Open(r.driverName, r.dsn) + if err != nil { + r.dbErr = fmt.Errorf("failed to open MySQL registry database: %w", err) + return + } + applyMySQLPoolSettings(db, r.registryConfig) + r.db = db + }) + if r.dbErr != nil { + return nil, r.dbErr + } + return r.db, nil +} + +func (r *MySQLRegistryStore) getMaxProjectUpdatedTimestamp(ctx context.Context, db *sql.DB) (*timestamppb.Timestamp, error) { + var maxUpdated sql.NullInt64 + err := db.QueryRowContext(ctx, + queryMaxLastUpdated, + r.project, + ).Scan(&maxUpdated) + if err != nil { + return nil, err + } + if !maxUpdated.Valid { + return nil, nil + } + return timestamppb.New(time.Unix(maxUpdated.Int64, 0)), nil +} + +func (r *MySQLRegistryStore) getQueryTimeout() time.Duration { + if r.registryConfig != nil && r.registryConfig.MySQLQueryTimeoutSeconds > 0 { + return time.Duration(r.registryConfig.MySQLQueryTimeoutSeconds) * time.Second + } + return time.Duration(defaultMySQLQueryTimeoutSeconds) * time.Second +} + +func readProtoRows[T proto.Message](ctx context.Context, db *sql.DB, query string, args []any, newProto func() T) ([]T, error) { + rows, err := db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + results := make([]T, 0) + for rows.Next() { + var data []byte + if err := rows.Scan(&data); err != nil { + return nil, err + } + msg := newProto() + if err := proto.Unmarshal(data, msg); err != nil { + return nil, err + } + results = append(results, msg) + } + if err := rows.Err(); err != nil { + return nil, err + } + return results, nil +} + +func mysqlURLToDSN(registryPath string) (string, error) { + if strings.TrimSpace(registryPath) == "" { + return "", errors.New("mysql registry path is empty") + } + + parsed, err := url.Parse(registryPath) + if err != nil { + return "", err + } + + if parsed.Scheme == "" { + // Assume raw DSN. + return registryPath, nil + } + if !isMySQLScheme(parsed.Scheme) { + return "", fmt.Errorf("unsupported mysql scheme %q", parsed.Scheme) + } + + cfg := mysql.NewConfig() + if parsed.User != nil { + cfg.User = parsed.User.Username() + if pwd, ok := parsed.User.Password(); ok { + cfg.Passwd = pwd + } + } + + cfg.Net = "tcp" + if host := parsed.Hostname(); host != "" { + if port := parsed.Port(); port != "" { + cfg.Addr = net.JoinHostPort(host, port) + } else { + cfg.Addr = host + } + } + + cfg.DBName = strings.TrimPrefix(parsed.Path, "/") + if cfg.DBName == "" { + return "", errors.New("mysql registry path missing database name") + } + + params := parsed.Query() + if socket := params.Get("unix_socket"); socket != "" { + cfg.Net = "unix" + cfg.Addr = socket + params.Del("unix_socket") + } + + if len(params) > 0 { + cfg.Params = map[string]string{} + for key, values := range params { + if len(values) > 0 { + cfg.Params[key] = values[len(values)-1] + } else { + cfg.Params[key] = "" + } + } + } + + if cfg.Params == nil { + cfg.Params = map[string]string{} + } + if _, ok := cfg.Params["parseTime"]; !ok { + cfg.Params["parseTime"] = "true" + } + + return cfg.FormatDSN(), nil +} + +func applyMySQLPoolSettings(db *sql.DB, config *RegistryConfig) { + if config == nil { + return + } + if config.MySQLMaxOpenConns > 0 { + db.SetMaxOpenConns(config.MySQLMaxOpenConns) + } + if config.MySQLMaxIdleConns > 0 { + db.SetMaxIdleConns(config.MySQLMaxIdleConns) + } + if config.MySQLConnMaxLifetimeSeconds > 0 { + db.SetConnMaxLifetime(time.Duration(config.MySQLConnMaxLifetimeSeconds) * time.Second) + } +} + +func isMySQLScheme(scheme string) bool { + return strings.ToLower(scheme) == "mysql" +} diff --git a/go/internal/feast/registry/mysql_registry_store_test.go b/go/internal/feast/registry/mysql_registry_store_test.go new file mode 100644 index 00000000000..ae661c51c70 --- /dev/null +++ b/go/internal/feast/registry/mysql_registry_store_test.go @@ -0,0 +1,127 @@ +package registry + +import ( + "database/sql" + "testing" + + "github.com/feast-dev/feast/go/protos/feast/core" + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// getRegistrySchemaDDL returns the DDL statements for creating the registry schema. +// Schema must match the Python SQLAlchemy schema defined in: +// sdk/python/feast/infra/registry/sql.py +// When the Python schema evolves, this function must be updated accordingly. +func getRegistrySchemaDDL() []string { + return []string{ + `CREATE TABLE projects (project_id TEXT PRIMARY KEY, project_name TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, project_proto BLOB NOT NULL);`, + `CREATE TABLE entities (entity_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, entity_proto BLOB NOT NULL, PRIMARY KEY (entity_name, project_id));`, + `CREATE TABLE feature_views (feature_view_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, materialized_intervals BLOB NULL, feature_view_proto BLOB NOT NULL, user_metadata BLOB NULL, PRIMARY KEY (feature_view_name, project_id));`, + `CREATE TABLE stream_feature_views (feature_view_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, feature_view_proto BLOB NOT NULL, user_metadata BLOB NULL, PRIMARY KEY (feature_view_name, project_id));`, + `CREATE TABLE on_demand_feature_views (feature_view_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, feature_view_proto BLOB NOT NULL, user_metadata BLOB NULL, PRIMARY KEY (feature_view_name, project_id));`, + `CREATE TABLE feature_services (feature_service_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, feature_service_proto BLOB NOT NULL, PRIMARY KEY (feature_service_name, project_id));`, + `CREATE TABLE data_sources (data_source_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, data_source_proto BLOB NOT NULL, PRIMARY KEY (data_source_name, project_id));`, + `CREATE TABLE saved_datasets (saved_dataset_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, saved_dataset_proto BLOB NOT NULL, PRIMARY KEY (saved_dataset_name, project_id));`, + `CREATE TABLE validation_references (validation_reference_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, validation_reference_proto BLOB NOT NULL, PRIMARY KEY (validation_reference_name, project_id));`, + `CREATE TABLE permissions (permission_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, permission_proto BLOB NOT NULL, PRIMARY KEY (permission_name, project_id));`, + `CREATE TABLE managed_infra (infra_name TEXT NOT NULL, project_id TEXT NOT NULL, last_updated_timestamp INTEGER NOT NULL, infra_proto BLOB NOT NULL, PRIMARY KEY (infra_name, project_id));`, + } +} + +func TestMySQLRegistryStore_GetRegistryProto_FromSQLRegistrySchema(t *testing.T) { + db, err := sql.Open("sqlite3", ":memory:") + require.NoError(t, err, "failed to open sqlite db") + defer db.Close() + + project := "feature_repo" + lastUpdated := int64(1710000000) + + for _, stmt := range getRegistrySchemaDDL() { + _, err := db.Exec(stmt) + require.NoError(t, err, "failed to create tables") + } + + projectProto := &core.Project{ + Spec: &core.ProjectSpec{Name: project}, + } + entityProto := &core.Entity{ + Spec: &core.EntitySpecV2{Name: "driver", Project: project}, + } + featureViewProto := &core.FeatureView{ + Spec: &core.FeatureViewSpec{Name: "driver_stats", Project: project}, + } + featureServiceProto := &core.FeatureService{ + Spec: &core.FeatureServiceSpec{Name: "driver_stats_service", Project: project}, + } + infraProto := &core.Infra{} + + projectBlob, err := proto.Marshal(projectProto) + require.NoError(t, err, "failed to marshal project proto") + entityBlob, err := proto.Marshal(entityProto) + require.NoError(t, err, "failed to marshal entity proto") + featureViewBlob, err := proto.Marshal(featureViewProto) + require.NoError(t, err, "failed to marshal feature view proto") + featureServiceBlob, err := proto.Marshal(featureServiceProto) + require.NoError(t, err, "failed to marshal feature service proto") + infraBlob, err := proto.Marshal(infraProto) + require.NoError(t, err, "failed to marshal infra proto") + + _, err = db.Exec( + "INSERT INTO projects (project_id, project_name, last_updated_timestamp, project_proto) VALUES (?, ?, ?, ?)", + project, project, lastUpdated, projectBlob, + ) + require.NoError(t, err, "failed to insert project row") + + _, err = db.Exec( + "INSERT INTO entities (entity_name, project_id, last_updated_timestamp, entity_proto) VALUES (?, ?, ?, ?)", + "driver", project, lastUpdated, entityBlob, + ) + require.NoError(t, err, "failed to insert entity row") + + _, err = db.Exec( + "INSERT INTO feature_views (feature_view_name, project_id, last_updated_timestamp, feature_view_proto) VALUES (?, ?, ?, ?)", + "driver_stats", project, lastUpdated, featureViewBlob, + ) + require.NoError(t, err, "failed to insert feature view row") + + _, err = db.Exec( + "INSERT INTO feature_services (feature_service_name, project_id, last_updated_timestamp, feature_service_proto) VALUES (?, ?, ?, ?)", + "driver_stats_service", project, lastUpdated, featureServiceBlob, + ) + require.NoError(t, err, "failed to insert feature service row") + + _, err = db.Exec( + "INSERT INTO managed_infra (infra_name, project_id, last_updated_timestamp, infra_proto) VALUES (?, ?, ?, ?)", + "infra_obj", project, lastUpdated, infraBlob, + ) + require.NoError(t, err, "failed to insert infra row") + + store := newMySQLRegistryStoreWithDB(db, project) + registryProto, err := store.GetRegistryProto() + require.NoError(t, err, "GetRegistryProto failed") + + assert.Equal(t, REGISTRY_SCHEMA_VERSION, registryProto.RegistrySchemaVersion) + require.Len(t, registryProto.Projects, 1) + assert.Equal(t, project, registryProto.Projects[0].Spec.GetName()) + require.Len(t, registryProto.Entities, 1) + assert.Equal(t, "driver", registryProto.Entities[0].Spec.GetName()) + require.Len(t, registryProto.FeatureViews, 1) + assert.Equal(t, "driver_stats", registryProto.FeatureViews[0].Spec.GetName()) + require.Len(t, registryProto.FeatureServices, 1) + assert.Equal(t, "driver_stats_service", registryProto.FeatureServices[0].Spec.GetName()) + require.NotNil(t, registryProto.LastUpdated) + assert.Equal(t, lastUpdated, registryProto.LastUpdated.GetSeconds()) + assert.NotNil(t, registryProto.Infra) +} + +func TestMySQLRegistryStore_SchemeRouting(t *testing.T) { + registryConfig := &RegistryConfig{ + Path: "mysql://user:pass@localhost:3306/feast", + } + store, err := getRegistryStoreFromScheme(registryConfig.Path, registryConfig, "", "feature_repo") + require.NoError(t, err, "getRegistryStoreFromScheme failed") + assert.IsType(t, &MySQLRegistryStore{}, store) +} diff --git a/go/internal/feast/registry/registry.go b/go/internal/feast/registry/registry.go index 160dda94fd6..3ff94807049 100644 --- a/go/internal/feast/registry/registry.go +++ b/go/internal/feast/registry/registry.go @@ -15,10 +15,11 @@ import ( var REGISTRY_SCHEMA_VERSION string = "1" var REGISTRY_STORE_CLASS_FOR_SCHEME map[string]string = map[string]string{ - "gs": "GCSRegistryStore", - "s3": "S3RegistryStore", - "file": "FileRegistryStore", - "": "FileRegistryStore", + "gs": "GCSRegistryStore", + "s3": "S3RegistryStore", + "file": "FileRegistryStore", + "mysql": "MySQLRegistryStore", + "": "FileRegistryStore", } /* @@ -80,6 +81,10 @@ func (r *Registry) InitializeRegistry() error { } func (r *Registry) RefreshRegistryOnInterval() { + if r.cachedRegistryProtoTtl <= 0 { + log.Info().Msg("Registry cache TTL is non-positive; skipping periodic refresh") + return + } ticker := time.NewTicker(r.cachedRegistryProtoTtl) for ; true; <-ticker.C { err := r.refresh() @@ -357,7 +362,7 @@ func getRegistryStoreFromScheme(registryPath string, registryConfig *RegistryCon if registryStoreType, ok := REGISTRY_STORE_CLASS_FOR_SCHEME[uri.Scheme]; ok { return getRegistryStoreFromType(registryStoreType, registryConfig, repoPath, project) } - return nil, fmt.Errorf("registry path %s has unsupported scheme %s. Supported schemes are file, s3 and gs", registryPath, uri.Scheme) + return nil, fmt.Errorf("registry path %s has unsupported scheme %s. Supported schemes are file, s3, gcs, and mysql", registryPath, uri.Scheme) } func getRegistryStoreFromType(registryStoreType string, registryConfig *RegistryConfig, repoPath string, project string) (RegistryStore, error) { @@ -366,6 +371,10 @@ func getRegistryStoreFromType(registryStoreType string, registryConfig *Registry return NewFileRegistryStore(registryConfig, repoPath), nil case "S3RegistryStore": return NewS3RegistryStore(registryConfig, repoPath), nil + case "GCSRegistryStore": + return NewGCSRegistryStore(registryConfig, repoPath), nil + case "MySQLRegistryStore": + return NewMySQLRegistryStore(registryConfig, repoPath, project), nil } - return nil, errors.New("only FileRegistryStore as a RegistryStore is supported at this moment") + return nil, errors.New("only FileRegistryStore, S3RegistryStore, GCSRegistryStore, and MySQLRegistryStore are supported at this moment") } diff --git a/go/internal/feast/registry/registry_test.go b/go/internal/feast/registry/registry_test.go index 3e544d486e5..0f5d1c20ea7 100644 --- a/go/internal/feast/registry/registry_test.go +++ b/go/internal/feast/registry/registry_test.go @@ -10,9 +10,10 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/assert" ) -func TestGetOnlineFeaturesS3Registry(t *testing.T) { +func TestCloudRegistryStores(t *testing.T) { mockS3Client := &MockS3Client{ GetObjectFn: func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) { return &s3.GetObjectOutput{ @@ -24,56 +25,100 @@ func TestGetOnlineFeaturesS3Registry(t *testing.T) { }, } + mockGCSClient := &MockGCSClient{ + GetObjectFn: func(ctx context.Context, bucket string, object string) (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader("mock data")), nil + }, + DeleteObjectFn: func(ctx context.Context, bucket string, object string) error { + return nil + }, + } + tests := []struct { name string config *RepoConfig }{ { - name: "redis with simple features", + name: "s3 registry store", config: &RepoConfig{ Project: "feature_repo", - Registry: map[string]interface{}{ + Registry: map[string]any{ "path": "s3://test-bucket/path/to/registry.db", }, Provider: "aws", }, }, + { + name: "gcs registry store", + config: &RepoConfig{ + Project: "feature_repo", + Registry: map[string]any{ + "path": "gs://test-bucket/path/to/registry.db", + }, + Provider: "gcp", + }, + }, } + for _, test := range tests { - registryConfig, err := test.config.GetRegistryConfig() - if err != nil { - t.Errorf("Error getting registry config. msg: %s", err.Error()) - } - r := &Registry{ - project: test.config.Project, - cachedRegistryProtoTtl: time.Duration(registryConfig.CacheTtlSeconds) * time.Second, - } - _ = registryConfig.RegistryStoreType - registryPath := registryConfig.Path - uri, err := url.Parse(registryPath) - if err != nil { - t.Errorf("Error parsing registry path. msg: %s", err.Error()) - } - if registryStoreType, ok := REGISTRY_STORE_CLASS_FOR_SCHEME[uri.Scheme]; ok { - switch registryStoreType { - case "S3RegistryStore": - registryStore := &S3RegistryStore{ - filePath: registryConfig.Path, - s3Client: mockS3Client, + t.Run(test.name, func(t *testing.T) { + registryConfig, err := test.config.GetRegistryConfig() + if err != nil { + t.Errorf("Error getting registry config. msg: %s", err.Error()) + } + r := &Registry{ + project: test.config.Project, + cachedRegistryProtoTtl: time.Duration(registryConfig.CacheTtlSeconds) * time.Second, + } + registryPath := registryConfig.Path + uri, err := url.Parse(registryPath) + if err != nil { + t.Errorf("Error parsing registry path. msg: %s", err.Error()) + } + if registryStoreType, ok := REGISTRY_STORE_CLASS_FOR_SCHEME[uri.Scheme]; ok { + switch registryStoreType { + case "S3RegistryStore": + r.registryStore = &S3RegistryStore{ + filePath: registryConfig.Path, + s3Client: mockS3Client, + } + case "GCSRegistryStore": + r.registryStore = &GCSRegistryStore{ + registryPath: registryConfig.Path, + client: mockGCSClient, + } + default: + t.Errorf("Unsupported registry store type: %s", registryStoreType) + return } - r.registryStore = registryStore err := r.InitializeRegistry() if err != nil { t.Errorf("Error initializing registry. msg: %s. registry path=%q", err.Error(), registryPath) } - default: - t.Errorf("Only S3RegistryStore is supported on this testing. got=%s", registryStoreType) } - } + }) + } +} + +func TestRefreshRegistryOnIntervalNonPositiveTTL(t *testing.T) { + tests := []struct { + name string + ttl time.Duration + }{ + {name: "zero ttl", ttl: 0}, + {name: "negative ttl", ttl: -1 * time.Second}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + r := &Registry{cachedRegistryProtoTtl: test.ttl} + assert.NotPanics(t, func() { + r.RefreshRegistryOnInterval() + }) + }) } } -// MockS3Client is mock client for testing s3 registry store +// MockS3Client is mock client for testing S3 registry store type MockS3Client struct { GetObjectFn func(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) DeleteObjectFn func(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) @@ -92,3 +137,23 @@ func (m *MockS3Client) DeleteObject(ctx context.Context, params *s3.DeleteObject } return nil, errors.New("not implemented") } + +// MockGCSClient is mock client for testing GCS registry store +type MockGCSClient struct { + GetObjectFn func(ctx context.Context, bucket string, object string) (io.ReadCloser, error) + DeleteObjectFn func(ctx context.Context, bucket string, object string) error +} + +func (m *MockGCSClient) GetObject(ctx context.Context, bucket string, object string) (io.ReadCloser, error) { + if m.GetObjectFn != nil { + return m.GetObjectFn(ctx, bucket, object) + } + return nil, errors.New("not implemented") +} + +func (m *MockGCSClient) DeleteObject(ctx context.Context, bucket string, object string) error { + if m.DeleteObjectFn != nil { + return m.DeleteObjectFn(ctx, bucket, object) + } + return errors.New("not implemented") +} diff --git a/go/internal/feast/registry/repoconfig.go b/go/internal/feast/registry/repoconfig.go index f70310f261c..c13537ca021 100644 --- a/go/internal/feast/registry/repoconfig.go +++ b/go/internal/feast/registry/repoconfig.go @@ -12,8 +12,12 @@ import ( ) const ( - defaultCacheTtlSeconds = int64(600) - defaultClientID = "Unknown" + defaultCacheTtlSeconds = int64(600) + defaultClientID = "Unknown" + defaultMySQLMaxOpenConns = 20 + defaultMySQLMaxIdleConns = 10 + defaultMySQLConnMaxLifetimeSeconds = int64(300) + defaultMySQLQueryTimeoutSeconds = int64(30) ) type RepoConfig struct { @@ -39,10 +43,14 @@ type RepoConfig struct { } type RegistryConfig struct { - RegistryStoreType string `json:"registry_store_type"` - Path string `json:"path"` - ClientId string `json:"client_id" default:"Unknown"` - CacheTtlSeconds int64 `json:"cache_ttl_seconds" default:"600"` + RegistryStoreType string `json:"registry_store_type"` + Path string `json:"path"` + ClientId string `json:"client_id" default:"Unknown"` + CacheTtlSeconds int64 `json:"cache_ttl_seconds" default:"600"` + MySQLMaxOpenConns int `json:"mysql_max_open_conns"` + MySQLMaxIdleConns int `json:"mysql_max_idle_conns"` + MySQLConnMaxLifetimeSeconds int64 `json:"mysql_conn_max_lifetime_seconds"` + MySQLQueryTimeoutSeconds int64 `json:"mysql_query_timeout_seconds"` } // NewRepoConfigFromJSON converts a JSON string into a RepoConfig struct and also sets the repo path. @@ -111,7 +119,14 @@ func (r *RepoConfig) GetLoggingOptions() (*logging.LoggingOptions, error) { func (r *RepoConfig) GetRegistryConfig() (*RegistryConfig, error) { if registryConfigMap, ok := r.Registry.(map[string]interface{}); ok { - registryConfig := RegistryConfig{CacheTtlSeconds: defaultCacheTtlSeconds, ClientId: defaultClientID} + registryConfig := RegistryConfig{ + CacheTtlSeconds: defaultCacheTtlSeconds, + ClientId: defaultClientID, + MySQLMaxOpenConns: defaultMySQLMaxOpenConns, + MySQLMaxIdleConns: defaultMySQLMaxIdleConns, + MySQLConnMaxLifetimeSeconds: defaultMySQLConnMaxLifetimeSeconds, + MySQLQueryTimeoutSeconds: defaultMySQLQueryTimeoutSeconds, + } for k, v := range registryConfigMap { switch k { case "path": @@ -127,23 +142,70 @@ func (r *RepoConfig) GetRegistryConfig() (*RegistryConfig, error) { registryConfig.ClientId = value } case "cache_ttl_seconds": - // cache_ttl_seconds defaulted to type float64. Ex: "cache_ttl_seconds": 60 in registryConfigMap - switch value := v.(type) { - case float64: - registryConfig.CacheTtlSeconds = int64(value) - case int: - registryConfig.CacheTtlSeconds = int64(value) - case int32: - registryConfig.CacheTtlSeconds = int64(value) - case int64: - registryConfig.CacheTtlSeconds = value - default: - return nil, fmt.Errorf("unexpected type %T for CacheTtlSeconds", v) + parsed, err := parseInt64Field("cache_ttl_seconds", v) + if err != nil { + return nil, err + } + registryConfig.CacheTtlSeconds = parsed + case "mysql_max_open_conns": + parsed, err := parseIntField("mysql_max_open_conns", v) + if err != nil { + return nil, err + } + registryConfig.MySQLMaxOpenConns = parsed + case "mysql_max_idle_conns": + parsed, err := parseIntField("mysql_max_idle_conns", v) + if err != nil { + return nil, err } + registryConfig.MySQLMaxIdleConns = parsed + case "mysql_conn_max_lifetime_seconds": + parsed, err := parseInt64Field("mysql_conn_max_lifetime_seconds", v) + if err != nil { + return nil, err + } + registryConfig.MySQLConnMaxLifetimeSeconds = parsed + case "mysql_query_timeout_seconds": + parsed, err := parseInt64Field("mysql_query_timeout_seconds", v) + if err != nil { + return nil, err + } + registryConfig.MySQLQueryTimeoutSeconds = parsed } } return ®istryConfig, nil } else { - return &RegistryConfig{Path: r.Registry.(string), ClientId: defaultClientID, CacheTtlSeconds: defaultCacheTtlSeconds}, nil + return &RegistryConfig{ + Path: r.Registry.(string), + ClientId: defaultClientID, + CacheTtlSeconds: defaultCacheTtlSeconds, + MySQLMaxOpenConns: defaultMySQLMaxOpenConns, + MySQLMaxIdleConns: defaultMySQLMaxIdleConns, + MySQLConnMaxLifetimeSeconds: defaultMySQLConnMaxLifetimeSeconds, + MySQLQueryTimeoutSeconds: defaultMySQLQueryTimeoutSeconds, + }, nil + } +} + +func parseInt64Field(field string, value interface{}) (int64, error) { + switch parsed := value.(type) { + case float64: + return int64(parsed), nil + case int: + return int64(parsed), nil + case int32: + return int64(parsed), nil + case int64: + return parsed, nil + default: + return 0, fmt.Errorf("unexpected type %T for %s", value, field) + } +} + +func parseIntField(field string, value interface{}) (int, error) { + parsed, err := parseInt64Field(field, value) + if err != nil { + return 0, err } + return int(parsed), nil } diff --git a/go/internal/feast/registry/repoconfig_test.go b/go/internal/feast/registry/repoconfig_test.go index 4d30bf7bca0..3b139e411e0 100644 --- a/go/internal/feast/registry/repoconfig_test.go +++ b/go/internal/feast/registry/repoconfig_test.go @@ -191,10 +191,14 @@ func TestGetRegistryConfig_Map(t *testing.T) { // Create a RepoConfig with a map Registry config := &RepoConfig{ Registry: map[string]interface{}{ - "path": "data/registry.db", - "registry_store_type": "local", - "client_id": "test_client_id", - "cache_ttl_seconds": 60, + "path": "data/registry.db", + "registry_store_type": "local", + "client_id": "test_client_id", + "cache_ttl_seconds": 60, + "mysql_max_open_conns": 25, + "mysql_max_idle_conns": 12, + "mysql_conn_max_lifetime_seconds": 180, + "mysql_query_timeout_seconds": 60, }, } @@ -206,6 +210,10 @@ func TestGetRegistryConfig_Map(t *testing.T) { assert.Equal(t, "local", registryConfig.RegistryStoreType) assert.Equal(t, int64(60), registryConfig.CacheTtlSeconds) assert.Equal(t, "test_client_id", registryConfig.ClientId) + assert.Equal(t, 25, registryConfig.MySQLMaxOpenConns) + assert.Equal(t, 12, registryConfig.MySQLMaxIdleConns) + assert.Equal(t, int64(180), registryConfig.MySQLConnMaxLifetimeSeconds) + assert.Equal(t, int64(60), registryConfig.MySQLQueryTimeoutSeconds) } func TestGetRegistryConfig_String(t *testing.T) { @@ -223,6 +231,10 @@ func TestGetRegistryConfig_String(t *testing.T) { println(registryConfig.CacheTtlSeconds) assert.Empty(t, registryConfig.RegistryStoreType) assert.Equal(t, defaultCacheTtlSeconds, registryConfig.CacheTtlSeconds) + assert.Equal(t, defaultMySQLMaxOpenConns, registryConfig.MySQLMaxOpenConns) + assert.Equal(t, defaultMySQLMaxIdleConns, registryConfig.MySQLMaxIdleConns) + assert.Equal(t, defaultMySQLConnMaxLifetimeSeconds, registryConfig.MySQLConnMaxLifetimeSeconds) + assert.Equal(t, defaultMySQLQueryTimeoutSeconds, registryConfig.MySQLQueryTimeoutSeconds) } func TestGetRegistryConfig_CacheTtlSecondsTypes(t *testing.T) { diff --git a/go/internal/feast/registry/s3.go b/go/internal/feast/registry/s3.go index 71ca8e2f135..c4e30f06077 100644 --- a/go/internal/feast/registry/s3.go +++ b/go/internal/feast/registry/s3.go @@ -29,22 +29,21 @@ type S3RegistryStore struct { // NewS3RegistryStore creates a S3RegistryStore with the given configuration func NewS3RegistryStore(config *RegistryConfig, repoPath string) *S3RegistryStore { - var lr S3RegistryStore - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + var rs S3RegistryStore + ctx := context.Background() cfg, err := awsConfig.LoadDefaultConfig(ctx) if err != nil { - lr = S3RegistryStore{ + rs = S3RegistryStore{ filePath: config.Path, } } else { - lr = S3RegistryStore{ + rs = S3RegistryStore{ filePath: config.Path, s3Client: s3.NewFromConfig(cfg), } } - return &lr + return &rs } func (r *S3RegistryStore) GetRegistryProto() (*core.Registry, error) { diff --git a/go/internal/feast/server/grpc_server.go b/go/internal/feast/server/grpc_server.go index ab76aa554ca..27aded75b04 100644 --- a/go/internal/feast/server/grpc_server.go +++ b/go/internal/feast/server/grpc_server.go @@ -9,7 +9,6 @@ import ( prototypes "github.com/feast-dev/feast/go/protos/feast/types" "github.com/feast-dev/feast/go/types" "github.com/google/uuid" - ) const feastServerVersion = "0.0.1" diff --git a/go/internal/feast/server/http_server.go b/go/internal/feast/server/http_server.go index 312a0a6352e..876f42f846b 100644 --- a/go/internal/feast/server/http_server.go +++ b/go/internal/feast/server/http_server.go @@ -2,6 +2,7 @@ package server import ( "context" + "crypto/tls" "encoding/json" "fmt" "net/http" @@ -17,6 +18,8 @@ import ( prototypes "github.com/feast-dev/feast/go/protos/feast/types" "github.com/feast-dev/feast/go/types" "github.com/rs/zerolog/log" + + "github.com/feast-dev/feast/go/internal/feast/metrics" ) type httpServer struct { @@ -335,10 +338,55 @@ func recoverMiddleware(next http.Handler) http.Handler { }) } +type statusWriter struct { + http.ResponseWriter + status int +} + +func (w *statusWriter) WriteHeader(status int) { + if w.status == 0 { + w.status = status + } + w.ResponseWriter.WriteHeader(status) +} + +func (w *statusWriter) Write(b []byte) (int, error) { + if w.status == 0 { + w.status = 200 + } + n, err := w.ResponseWriter.Write(b) + return n, err +} + +func metricsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t0 := time.Now() + sw := &statusWriter{ResponseWriter: w} + next.ServeHTTP(sw, r) + duration := time.Since(t0) + + if sw.status == 0 { + sw.status = 200 + } + + metrics.HttpMetrics.Duration(metrics.HttpLabels{ + Method: r.Method, + Status: sw.status, + Path: r.URL.Path, + }).Duration(duration) + + metrics.HttpMetrics.RequestsTotal(metrics.HttpLabels{ + Method: r.Method, + Status: sw.status, + Path: r.URL.Path, + }).Inc() + }) +} + func (s *httpServer) Serve(host string, port int) error { mux := http.NewServeMux() - mux.Handle("/get-online-features", recoverMiddleware(http.HandlerFunc(s.getOnlineFeatures))) - mux.HandleFunc("/health", healthCheckHandler) + mux.Handle("/get-online-features", metricsMiddleware(recoverMiddleware(http.HandlerFunc(s.getOnlineFeatures)))) + mux.Handle("/health", metricsMiddleware(http.HandlerFunc(healthCheckHandler))) s.server = &http.Server{Addr: fmt.Sprintf("%s:%d", host, port), Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 15 * time.Second} err := s.server.ListenAndServe() // Don't return the error if it's caused by graceful shutdown using Stop() @@ -349,6 +397,34 @@ func (s *httpServer) Serve(host string, port int) error { return err } +func (s *httpServer) ServeTLS(host string, port int, certFile string, keyFile string) error { + mux := http.NewServeMux() + mux.Handle("/get-online-features", metricsMiddleware(recoverMiddleware(http.HandlerFunc(s.getOnlineFeatures)))) + mux.Handle("/health", metricsMiddleware(http.HandlerFunc(healthCheckHandler))) + s.server = &http.Server{ + Addr: fmt.Sprintf("%s:%d", host, port), + Handler: mux, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 15 * time.Second, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{ + tls.CurveP256, + tls.X25519MLKEM768, + //tls.SecP256r1MLKEM768, // Only available in Go 1.26 + }, + }, + } + err := s.server.ListenAndServeTLS(certFile, keyFile) + // Don't return the error if it's caused by graceful shutdown using Stop() + if err == http.ErrServerClosed { + return nil + } + log.Fatal().Stack().Err(err).Msg("Failed to start HTTPS server") + return err +} + func healthCheckHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Healthy") diff --git a/go/internal/feast/server/server_commons.go b/go/internal/feast/server/server_commons.go index a6959076c4a..b433e7f4a85 100644 --- a/go/internal/feast/server/server_commons.go +++ b/go/internal/feast/server/server_commons.go @@ -4,8 +4,8 @@ import ( "os" "github.com/rs/zerolog" - "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" ) var tracer = otel.Tracer("github.com/feast-dev/feast/go/server") diff --git a/go/internal/test/feature_repo/example.py b/go/internal/test/feature_repo/example.py index a814b58913b..efd1966d03e 100644 --- a/go/internal/test/feature_repo/example.py +++ b/go/internal/test/feature_repo/example.py @@ -2,7 +2,7 @@ from datetime import timedelta -from feast import Entity, Feature, FeatureView, Field, FileSource, FeatureService, RequestSource +from feast import Entity, FeatureView, Field, FileSource, FeatureService, RequestSource from feast.feature_logging import LoggingConfig from feast.infra.offline_stores.file_source import FileLoggingDestination from feast.types import Float32, Float64, Int64, PrimitiveFeastType @@ -42,7 +42,7 @@ driver_stats_fs = FeatureService( name="test_service", features=[driver_hourly_stats_view], - logging_config=LoggingConfig(destination=FileLoggingDestination(path="")) + logging_config=LoggingConfig(destination=FileLoggingDestination(path="")), ) @@ -53,22 +53,20 @@ schema=[ Field(name="val_to_add", dtype=PrimitiveFeastType.INT64), Field(name="val_to_add_2", dtype=PrimitiveFeastType.INT64), - ] + ], ) + # Use the input data and feature view features to create new features @on_demand_feature_view( - sources=[ - driver_hourly_stats_view, - input_request - ], - schema=[ - Field(name='conv_rate_plus_val1', dtype=Float64), - Field(name='conv_rate_plus_val2', dtype=Float64) - ] + sources=[driver_hourly_stats_view, input_request], + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], ) def transformed_conv_rate(features_df: pd.DataFrame) -> pd.DataFrame: df = pd.DataFrame() - df['conv_rate_plus_val1'] = (features_df['conv_rate'] + features_df['val_to_add']) - df['conv_rate_plus_val2'] = (features_df['conv_rate'] + features_df['val_to_add_2']) + df["conv_rate_plus_val1"] = features_df["conv_rate"] + features_df["val_to_add"] + df["conv_rate_plus_val2"] = features_df["conv_rate"] + features_df["val_to_add_2"] return df diff --git a/go/main.go b/go/main.go index 77999671e07..7f89fe66c3b 100644 --- a/go/main.go +++ b/go/main.go @@ -5,10 +5,13 @@ import ( "flag" "fmt" "net" + "net/http" "os" "os/signal" "strings" + "sync" "syscall" + "time" "github.com/feast-dev/feast/go/internal/feast" "github.com/feast-dev/feast/go/internal/feast/registry" @@ -20,6 +23,9 @@ import ( "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" @@ -31,19 +37,32 @@ import ( var tracer trace.Tracer +var newSignalStopChannel = func() (chan os.Signal, func()) { + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + return stop, func() { + signal.Stop(stop) + } +} + type ServerStarter interface { - StartHttpServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error - StartGrpcServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error + StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error + StartGrpcServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error + StartHttpsServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions, certFile string, keyFile string) error } type RealServerStarter struct{} -func (s *RealServerStarter) StartHttpServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { - return StartHttpServer(fs, host, port, writeLoggedFeaturesCallback, loggingOpts) +func (s *RealServerStarter) StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { + return StartHttpServer(fs, host, port, metricsPort, writeLoggedFeaturesCallback, loggingOpts, false, "", "") +} + +func (s *RealServerStarter) StartHttpsServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions, certFile string, keyFile string) error { + return StartHttpServer(fs, host, port, metricsPort, writeLoggedFeaturesCallback, loggingOpts, true, certFile, keyFile) } -func (s *RealServerStarter) StartGrpcServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { - return StartGrpcServer(fs, host, port, writeLoggedFeaturesCallback, loggingOpts) +func (s *RealServerStarter) StartGrpcServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { + return StartGrpcServer(fs, host, port, metricsPort, writeLoggedFeaturesCallback, loggingOpts) } func main() { @@ -51,18 +70,24 @@ func main() { serverType := "http" host := "" port := 8080 + metricsPort := 9090 server := RealServerStarter{} + certFile := "" + keyFile := "" // Current Directory repoPath, err := os.Getwd() if err != nil { log.Error().Stack().Err(err).Msg("Failed to get current directory") } - flag.StringVar(&serverType, "type", serverType, "Specify the server type (http or grpc)") + flag.StringVar(&serverType, "type", serverType, "Specify the server type (http, https or grpc)") flag.StringVar(&repoPath, "chdir", repoPath, "Repository path where feature store yaml file is stored") flag.StringVar(&host, "host", host, "Specify a host for the server") flag.IntVar(&port, "port", port, "Specify a port for the server") + flag.IntVar(&metricsPort, "metrics-port", metricsPort, "Specify a port for the metrics server") + flag.StringVar(&certFile, "tls-cert-file", "", "Path to the TLS certificate file") + flag.StringVar(&keyFile, "tls-key-file", "", "Path to the TLS key file") flag.Parse() // Initialize tracer @@ -109,11 +134,13 @@ func main() { // TODO: writeLoggedFeaturesCallback is defaulted to nil. write_logged_features functionality needs to be // implemented in Golang specific to OfflineStoreSink. Python Feature Server doesn't support this. if serverType == "http" { - err = server.StartHttpServer(fs, host, port, nil, loggingOptions) + err = server.StartHttpServer(fs, host, port, metricsPort, nil, loggingOptions) } else if serverType == "grpc" { - err = server.StartGrpcServer(fs, host, port, nil, loggingOptions) + err = server.StartGrpcServer(fs, host, port, metricsPort, nil, loggingOptions) + } else if serverType == "https" { + err = server.StartHttpsServer(fs, host, port, metricsPort, nil, loggingOptions, certFile, keyFile) } else { - fmt.Println("Unknown server type. Please specify 'http' or 'grpc'.") + fmt.Println("Unknown server type. Please specify 'http' or 'grpc' or 'https'.") } if err != nil { @@ -144,7 +171,7 @@ func constructLoggingService(fs *feast.FeatureStore, writeLoggedFeaturesCallback } // StartGprcServerWithLogging starts gRPC server with enabled feature logging -func StartGrpcServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { +func StartGrpcServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { loggingService, err := constructLoggingService(fs, writeLoggedFeaturesCallback, loggingOpts) if err != nil { return err @@ -155,33 +182,76 @@ func StartGrpcServer(fs *feast.FeatureStore, host string, port int, writeLoggedF if err != nil { return err } - - grpcServer := grpc.NewServer() + srvMetrics := grpc_prometheus.NewServerMetrics( + grpc_prometheus.WithServerHandlingTimeHistogram( + grpc_prometheus.WithHistogramBuckets([]float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10}), + ), + ) + prometheus.MustRegister(srvMetrics) + grpcServer := grpc.NewServer( + grpc.UnaryInterceptor(srvMetrics.UnaryServerInterceptor()), + ) serving.RegisterServingServiceServer(grpcServer, ser) healthService := health.NewServer() grpc_health_v1.RegisterHealthServer(grpcServer, healthService) + srvMetrics.InitializeMetrics(grpcServer) + + // Start metrics server + metricsServer := &http.Server{Addr: fmt.Sprintf(":%d", metricsPort)} + go func() { + log.Info().Msgf("Starting metrics server on port %d", metricsPort) + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + metricsServer.Handler = mux + if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Error().Err(err).Msg("Failed to start metrics server") + } + }() stop := make(chan os.Signal, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + var wg sync.WaitGroup + wg.Add(1) + serverExited := make(chan struct{}) go func() { - // As soon as these signals are received from OS, try to gracefully stop the gRPC server - <-stop - log.Info().Msg("Stopping the gRPC server...") - grpcServer.GracefulStop() - if loggingService != nil { - loggingService.Stop() + defer wg.Done() + select { + case <-stop: + // Received SIGINT/SIGTERM. Perform graceful shutdown. + log.Info().Msg("Stopping the gRPC server...") + grpcServer.GracefulStop() + if loggingService != nil { + loggingService.Stop() + } + log.Info().Msg("Stopping metrics server...") + if err := metricsServer.Shutdown(context.Background()); err != nil { + log.Error().Err(err).Msg("Error stopping metrics server") + } + log.Info().Msg("gRPC server terminated") + case <-serverExited: + // Server exited (e.g. startup error), ensure metrics server is stopped + metricsServer.Shutdown(context.Background()) + if loggingService != nil { + loggingService.Stop() + } } - log.Info().Msg("gRPC server terminated") }() - return grpcServer.Serve(lis) + err = grpcServer.Serve(lis) + close(serverExited) + wg.Wait() + return err } // StartHttpServerWithLogging starts HTTP server with enabled feature logging // Go does not allow direct assignment to package-level functions as a way to // mock them for tests -func StartHttpServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { +func StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions, httpsEnable bool, certFile string, keyFile string) error { + if httpsEnable && (certFile == "" || keyFile == "") { + return fmt.Errorf("--tls-cert-file and --tls-key-file must be provided for HTTPS server.") + } + loggingService, err := constructLoggingService(fs, writeLoggedFeaturesCallback, loggingOpts) if err != nil { return err @@ -189,24 +259,63 @@ func StartHttpServer(fs *feast.FeatureStore, host string, port int, writeLoggedF ser := server.NewHttpServer(fs, loggingService) log.Info().Msgf("Starting a HTTP server on host %s, port %d", host, port) - stop := make(chan os.Signal, 1) - signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) - + // Start metrics server + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + metricsServer := &http.Server{ + Addr: fmt.Sprintf(":%d", metricsPort), + Handler: mux, + } go func() { - // As soon as these signals are received from OS, try to gracefully stop the gRPC server - <-stop - log.Info().Msg("Stopping the HTTP server...") - err := ser.Stop() - if err != nil { - log.Error().Err(err).Msg("Error when stopping the HTTP server") + log.Info().Msgf("Starting metrics server on port %d", metricsPort) + if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Error().Err(err).Msg("Failed to start metrics server") } - if loggingService != nil { - loggingService.Stop() + }() + + stop, stopCleanup := newSignalStopChannel() + defer stopCleanup() + + var wg sync.WaitGroup + wg.Add(1) + serverExited := make(chan struct{}) + go func() { + defer wg.Done() + select { + case <-stop: + // Received SIGINT/SIGTERM. Perform graceful shutdown. + log.Info().Msg("Stopping the HTTP server...") + err := ser.Stop() + if err != nil { + log.Error().Err(err).Msg("Error when stopping the HTTP server") + } + log.Info().Msg("Stopping metrics server...") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := metricsServer.Shutdown(ctx); err != nil { + log.Error().Err(err).Msg("Error stopping metrics server") + } + if loggingService != nil { + loggingService.Stop() + } + log.Info().Msg("HTTP server terminated") + case <-serverExited: + // Server exited (e.g. startup error), ensure metrics server is stopped + metricsServer.Shutdown(context.Background()) + if loggingService != nil { + loggingService.Stop() + } } - log.Info().Msg("HTTP server terminated") }() - return ser.Serve(host, port) + if httpsEnable { + err = ser.ServeTLS(host, port, certFile, keyFile) + } else { + err = ser.Serve(host, port) + } + close(serverExited) + wg.Wait() + return err } func OTELTracingEnabled() bool { @@ -223,11 +332,15 @@ func newExporter(ctx context.Context) (*otlptrace.Exporter, error) { } func newTracerProvider(exp sdktrace.SpanExporter) (*sdktrace.TracerProvider, error) { + serviceName := os.Getenv("OTEL_SERVICE_NAME") + if serviceName == "" { + serviceName = "FeastGoFeatureServer" + } r, err := resource.Merge( resource.Default(), resource.NewWithAttributes( semconv.SchemaURL, - semconv.ServiceName("FeastGoFeatureServer"), + semconv.ServiceName(serviceName), ), ) diff --git a/go/main_test.go b/go/main_test.go index 567a6cf5af4..7eb0e0a6676 100644 --- a/go/main_test.go +++ b/go/main_test.go @@ -1,12 +1,28 @@ package main import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io" + "math/big" + "net" + "net/http" + "os" + "strings" + "syscall" "testing" + "time" "github.com/feast-dev/feast/go/internal/feast" "github.com/feast-dev/feast/go/internal/feast/server/logging" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) // MockServerStarter is a mock of ServerStarter interface for testing @@ -14,13 +30,13 @@ type MockServerStarter struct { mock.Mock } -func (m *MockServerStarter) StartHttpServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { - args := m.Called(fs, host, port, writeLoggedFeaturesCallback, loggingOpts) +func (m *MockServerStarter) StartHttpServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { + args := m.Called(fs, host, port, metricsPort, writeLoggedFeaturesCallback, loggingOpts) return args.Error(0) } -func (m *MockServerStarter) StartGrpcServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { - args := m.Called(fs, host, port, writeLoggedFeaturesCallback, loggingOpts) +func (m *MockServerStarter) StartGrpcServer(fs *feast.FeatureStore, host string, port int, metricsPort int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error { + args := m.Called(fs, host, port, metricsPort, writeLoggedFeaturesCallback, loggingOpts) return args.Error(0) } @@ -34,9 +50,9 @@ func TestStartHttpServer(t *testing.T) { loggingOpts := &logging.LoggingOptions{} - mockServerStarter.On("StartHttpServer", fs, host, port, mock.AnythingOfType("logging.OfflineStoreWriteCallback"), loggingOpts).Return(nil) + mockServerStarter.On("StartHttpServer", fs, host, port, 9090, mock.AnythingOfType("logging.OfflineStoreWriteCallback"), loggingOpts).Return(nil) - err := mockServerStarter.StartHttpServer(fs, host, port, writeLoggedFeaturesCallback, loggingOpts) + err := mockServerStarter.StartHttpServer(fs, host, port, 9090, writeLoggedFeaturesCallback, loggingOpts) assert.NoError(t, err) mockServerStarter.AssertExpectations(t) } @@ -50,9 +66,9 @@ func TestStartGrpcServer(t *testing.T) { var writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback loggingOpts := &logging.LoggingOptions{} - mockServerStarter.On("StartGrpcServer", fs, host, port, mock.AnythingOfType("logging.OfflineStoreWriteCallback"), loggingOpts).Return(nil) + mockServerStarter.On("StartGrpcServer", fs, host, port, 9090, mock.AnythingOfType("logging.OfflineStoreWriteCallback"), loggingOpts).Return(nil) - err := mockServerStarter.StartGrpcServer(fs, host, port, writeLoggedFeaturesCallback, loggingOpts) + err := mockServerStarter.StartGrpcServer(fs, host, port, 9090, writeLoggedFeaturesCallback, loggingOpts) assert.NoError(t, err) mockServerStarter.AssertExpectations(t) } @@ -68,4 +84,129 @@ func TestConstructLoggingService(t *testing.T) { // Further assertions can be added here based on the expected behavior of constructLoggingService } -// Note: Additional tests can be written for other functions and error scenarios. +func TestStartHttpsServerHealthEndpoint(t *testing.T) { + certPath, keyPath := createSelfSignedTLSFiles(t) + host := "127.0.0.1" + port := getFreePort(t) + metricsPort := getFreePort(t) + + stop := make(chan os.Signal, 1) + prevNewSignalStopChannel := newSignalStopChannel + newSignalStopChannel = func() (chan os.Signal, func()) { + return stop, func() {} + } + t.Cleanup(func() { + newSignalStopChannel = prevNewSignalStopChannel + }) + + errCh := make(chan error, 1) + go func() { + errCh <- StartHttpServer(&feast.FeatureStore{}, host, port, metricsPort, nil, &logging.LoggingOptions{}, true, certPath, keyPath) + }() + + httpsClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec + }, + } + t.Cleanup(httpsClient.CloseIdleConnections) + + url := fmt.Sprintf("https://%s:%d/health", host, port) + + var ( + resp *http.Response + err error + ) + require.Eventually(t, func() bool { + resp, err = httpsClient.Get(url) + if err != nil { + return false + } + return true + }, 5*time.Second, 100*time.Millisecond) + require.NoError(t, err) + t.Cleanup(func() { + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + }) + + body, readErr := io.ReadAll(resp.Body) + require.NoError(t, readErr) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "Healthy", strings.TrimSpace(string(body))) + + stop <- syscall.SIGTERM + + select { + case startErr := <-errCh: + require.NoError(t, startErr) + case <-time.After(5 * time.Second): + t.Fatal("StartHttpsServer did not shutdown within timeout") + } +} + +func TestStartHttpsServerTLSFilesRequired(t *testing.T) { + err := StartHttpServer(&feast.FeatureStore{}, "127.0.0.1", 0, 0, nil, &logging.LoggingOptions{}, true, "", "") + require.Error(t, err) + assert.Contains(t, err.Error(), "--tls-cert-file and --tls-key-file must be provided") +} + +func getFreePort(t *testing.T) int { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer func() { + _ = listener.Close() + }() + + addr, ok := listener.Addr().(*net.TCPAddr) + require.True(t, ok) + return addr.Port +} + +func createSelfSignedTLSFiles(t *testing.T) (string, string) { + t.Helper() + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + tmpl := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "localhost", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + } + + der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) + require.NoError(t, err) + + certFile, err := os.CreateTemp(t.TempDir(), "feast-test-cert-*.pem") + require.NoError(t, err) + defer func() { + _ = certFile.Close() + }() + + keyFile, err := os.CreateTemp(t.TempDir(), "feast-test-key-*.pem") + require.NoError(t, err) + defer func() { + _ = keyFile.Close() + }() + + err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: der}) + require.NoError(t, err) + + err = pem.Encode(keyFile, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + require.NoError(t, err) + + return certFile.Name(), keyFile.Name() +} diff --git a/infra/charts/feast-feature-server/Chart.yaml b/infra/charts/feast-feature-server/Chart.yaml index eaf14f21fe6..f77f882a9d2 100644 --- a/infra/charts/feast-feature-server/Chart.yaml +++ b/infra/charts/feast-feature-server/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: feast-feature-server description: Feast Feature Server in Go or Python type: application -version: 0.56.0 +version: 0.65.0 keywords: - machine learning - big data diff --git a/infra/charts/feast-feature-server/README.md b/infra/charts/feast-feature-server/README.md index e73be95a909..cd8cc475031 100644 --- a/infra/charts/feast-feature-server/README.md +++ b/infra/charts/feast-feature-server/README.md @@ -1,6 +1,6 @@ # Feast Python / Go Feature Server Helm Charts -Current chart version is `0.56.0` +Current chart version is `0.65.0` ## Installation @@ -42,7 +42,7 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/python-helm-d | fullnameOverride | string | `""` | | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"quay.io/feastdev/feature-server"` | Docker image for Feature Server repository | -| image.tag | string | `"0.56.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | +| image.tag | string | `"0.65.0"` | The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) | | imagePullSecrets | list | `[]` | | | livenessProbe.initialDelaySeconds | int | `30` | | | livenessProbe.periodSeconds | int | `30` | | diff --git a/infra/charts/feast-feature-server/templates/deployment.yaml b/infra/charts/feast-feature-server/templates/deployment.yaml index 6cd17d4a4d1..ef0cc9671de 100644 --- a/infra/charts/feast-feature-server/templates/deployment.yaml +++ b/infra/charts/feast-feature-server/templates/deployment.yaml @@ -11,10 +11,12 @@ spec: {{- include "feast-feature-server.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} + {{- if or .Values.podAnnotations .Values.metrics.enabled }} annotations: + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} - {{- if .Values.metrics.enabled }} + {{- end }} + {{- if $.Values.metrics.enabled }} instrumentation.opentelemetry.io/inject-python: "true" {{- end }} {{- end }} diff --git a/infra/charts/feast-feature-server/values.yaml b/infra/charts/feast-feature-server/values.yaml index 0f94b5bc579..f2bc97d7a2b 100644 --- a/infra/charts/feast-feature-server/values.yaml +++ b/infra/charts/feast-feature-server/values.yaml @@ -9,7 +9,7 @@ image: repository: quay.io/feastdev/feature-server pullPolicy: IfNotPresent # image.tag -- The Docker image tag (can be overwritten if custom feature server deps are needed for on demand transforms) - tag: 0.56.0 + tag: 0.65.0 logLevel: "WARNING" # Set log level DEBUG, INFO, WARNING, ERROR, and CRITICAL (case-insensitive) diff --git a/infra/charts/feast/Chart.yaml b/infra/charts/feast/Chart.yaml index 307e01563cd..dc49ff3fb2f 100644 --- a/infra/charts/feast/Chart.yaml +++ b/infra/charts/feast/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v1 description: Feature store for machine learning name: feast -version: 0.56.0 +version: 0.65.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index dca35b67065..2a288bb48aa 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -8,7 +8,7 @@ This repo contains Helm charts for Feast Java components that are being installe ## Chart: Feast -Feature store for machine learning Current chart version is `0.56.0` +Feature store for machine learning Current chart version is `0.65.0` ## Installation @@ -65,8 +65,8 @@ See [here](https://github.com/feast-dev/feast/tree/master/examples/java-demo) fo | Repository | Name | Version | |------------|------|---------| | https://charts.helm.sh/stable | redis | 10.5.6 | -| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.56.0 | -| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.56.0 | +| https://feast-helm-charts.storage.googleapis.com | feature-server(feature-server) | 0.65.0 | +| https://feast-helm-charts.storage.googleapis.com | transformation-service(transformation-service) | 0.65.0 | ## Values diff --git a/infra/charts/feast/charts/feature-server/Chart.yaml b/infra/charts/feast/charts/feature-server/Chart.yaml index 43a7e41c3ab..b20c1778a18 100644 --- a/infra/charts/feast/charts/feature-server/Chart.yaml +++ b/infra/charts/feast/charts/feature-server/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Feast Feature Server: Online feature serving service for Feast" name: feature-server -version: 0.56.0 -appVersion: v0.56.0 +version: 0.65.0 +appVersion: v0.65.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/feature-server/README.md b/infra/charts/feast/charts/feature-server/README.md index 46159a543c5..571449d9009 100644 --- a/infra/charts/feast/charts/feature-server/README.md +++ b/infra/charts/feast/charts/feature-server/README.md @@ -1,6 +1,6 @@ # feature-server -![Version: 0.56.0](https://img.shields.io/badge/Version-0.56.0-informational?style=flat-square) ![AppVersion: v0.56.0](https://img.shields.io/badge/AppVersion-v0.56.0-informational?style=flat-square) +![Version: 0.65.0](https://img.shields.io/badge/Version-0.65.0-informational?style=flat-square) ![AppVersion: v0.65.0](https://img.shields.io/badge/AppVersion-v0.65.0-informational?style=flat-square) Feast Feature Server: Online feature serving service for Feast @@ -17,7 +17,7 @@ Feast Feature Server: Online feature serving service for Feast | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"quay.io/feastdev/feature-server-java"` | Docker image for Feature Server repository | -| image.tag | string | `"0.56.0"` | Image tag | +| image.tag | string | `"0.65.0"` | Image tag | | ingress.grpc.annotations | object | `{}` | Extra annotations for the ingress | | ingress.grpc.auth.enabled | bool | `false` | Flag to enable auth | | ingress.grpc.class | string | `"nginx"` | Which ingress controller to use | diff --git a/infra/charts/feast/charts/feature-server/values.yaml b/infra/charts/feast/charts/feature-server/values.yaml index c054158be62..3367dd665fa 100644 --- a/infra/charts/feast/charts/feature-server/values.yaml +++ b/infra/charts/feast/charts/feature-server/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Feature Server repository repository: quay.io/feastdev/feature-server-java # image.tag -- Image tag - tag: 0.56.0 + tag: 0.65.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/charts/transformation-service/Chart.yaml b/infra/charts/feast/charts/transformation-service/Chart.yaml index 10c15403d9d..9dbc3f73cb4 100644 --- a/infra/charts/feast/charts/transformation-service/Chart.yaml +++ b/infra/charts/feast/charts/transformation-service/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v1 description: "Transformation service: to compute on-demand features" name: transformation-service -version: 0.56.0 -appVersion: v0.56.0 +version: 0.65.0 +appVersion: v0.65.0 keywords: - machine learning - big data diff --git a/infra/charts/feast/charts/transformation-service/README.md b/infra/charts/feast/charts/transformation-service/README.md index 716d5f9332b..ad1dd75cd65 100644 --- a/infra/charts/feast/charts/transformation-service/README.md +++ b/infra/charts/feast/charts/transformation-service/README.md @@ -1,6 +1,6 @@ # transformation-service -![Version: 0.56.0](https://img.shields.io/badge/Version-0.56.0-informational?style=flat-square) ![AppVersion: v0.56.0](https://img.shields.io/badge/AppVersion-v0.56.0-informational?style=flat-square) +![Version: 0.65.0](https://img.shields.io/badge/Version-0.65.0-informational?style=flat-square) ![AppVersion: v0.65.0](https://img.shields.io/badge/AppVersion-v0.65.0-informational?style=flat-square) Transformation service: to compute on-demand features @@ -13,7 +13,7 @@ Transformation service: to compute on-demand features | envOverrides | object | `{}` | Extra environment variables to set | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | | image.repository | string | `"quay.io/feastdev/feature-transformation-server"` | Docker image for Transformation Server repository | -| image.tag | string | `"0.56.0"` | Image tag | +| image.tag | string | `"0.65.0"` | Image tag | | nodeSelector | object | `{}` | Node labels for pod assignment | | podLabels | object | `{}` | Labels to be added to Feast Serving pods | | replicaCount | int | `1` | Number of pods that will be created | diff --git a/infra/charts/feast/charts/transformation-service/values.yaml b/infra/charts/feast/charts/transformation-service/values.yaml index fe805f0bed1..266cd4b48aa 100644 --- a/infra/charts/feast/charts/transformation-service/values.yaml +++ b/infra/charts/feast/charts/transformation-service/values.yaml @@ -5,7 +5,7 @@ image: # image.repository -- Docker image for Transformation Server repository repository: quay.io/feastdev/feature-transformation-server # image.tag -- Image tag - tag: 0.56.0 + tag: 0.65.0 # image.pullPolicy -- Image pull policy pullPolicy: IfNotPresent diff --git a/infra/charts/feast/requirements.yaml b/infra/charts/feast/requirements.yaml index 2ef611c9a0e..3f29ad7dfdc 100644 --- a/infra/charts/feast/requirements.yaml +++ b/infra/charts/feast/requirements.yaml @@ -1,12 +1,12 @@ dependencies: - name: feature-server alias: feature-server - version: 0.56.0 + version: 0.65.0 condition: feature-server.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: transformation-service alias: transformation-service - version: 0.56.0 + version: 0.65.0 condition: transformation-service.enabled repository: https://feast-helm-charts.storage.googleapis.com - name: redis diff --git a/infra/feast-operator/.golangci.bck.yml b/infra/feast-operator/.golangci.bck.yml new file mode 100644 index 00000000000..6c104980d43 --- /dev/null +++ b/infra/feast-operator/.golangci.bck.yml @@ -0,0 +1,55 @@ +run: + timeout: 5m + allow-parallel-runners: true + +issues: + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) + exclude-rules: + - path: "api/*" + linters: + - lll + - path: "internal/*" + linters: + - dupl + - lll + - path: "test/*" + linters: + - lll + - path: "upgrade/*" + linters: + - lll + - path: "previous-version/*" + linters: + - lll +linters: + disable-all: true + enable: + - dupl + - errcheck + - goconst + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - ginkgolinter + - prealloc + - revive + - staticcheck + - typecheck + - unconvert + - unparam + - unused + +linters-settings: + revive: + rules: + - name: comment-spacings diff --git a/infra/feast-operator/.golangci.yml b/infra/feast-operator/.golangci.yml index 6c104980d43..95be6d14177 100644 --- a/infra/feast-operator/.golangci.yml +++ b/infra/feast-operator/.golangci.yml @@ -1,55 +1,67 @@ +version: "2" run: - timeout: 5m allow-parallel-runners: true - -issues: - # don't skip warning about doc comments - # don't exclude the default set of lint - exclude-use-default: false - # restore some of the defaults - # (fill in the rest as needed) - exclude-rules: - - path: "api/*" - linters: - - lll - - path: "internal/*" - linters: - - dupl - - lll - - path: "test/*" - linters: - - lll - - path: "upgrade/*" - linters: - - lll - - path: "previous-version/*" - linters: - - lll linters: - disable-all: true + default: none enable: - dupl - errcheck + - ginkgolinter - goconst - gocyclo - - gofmt - - goimports - - gosimple - govet - ineffassign - lll - misspell - nakedret - - ginkgolinter - prealloc - revive - staticcheck - - typecheck - unconvert - unparam - unused - -linters-settings: - revive: + settings: + revive: + rules: + - name: comment-spacings + staticcheck: + checks: + - "all" + - "-QF*" + - "-ST*" + exclusions: + generated: lax + presets: [] rules: - - name: comment-spacings + - linters: + - lll + path: api/* + - linters: + - dupl + - goconst + - lll + path: internal/* + - linters: + - goconst + - lll + path: test/* + - linters: + - lll + path: upgrade/* + - linters: + - lll + path: previous-version/* + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/infra/feast-operator/Dockerfile b/infra/feast-operator/Dockerfile index f0814b25576..a7ad3fa044d 100644 --- a/infra/feast-operator/Dockerfile +++ b/infra/feast-operator/Dockerfile @@ -1,28 +1,29 @@ # Build the manager binary -FROM registry.access.redhat.com/ubi9/go-toolset:1.22.9 AS builder +FROM registry.access.redhat.com/ubi9/go-toolset:1.25 AS builder ARG TARGETOS ARG TARGETARCH +ENV GOTOOLCHAIN=auto # Copy the Go Modules manifests -COPY go.mod go.mod -COPY go.sum go.sum +COPY --chown=1001:0 go.mod go.mod +COPY --chown=1001:0 go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download # Copy the go source -COPY cmd/main.go cmd/main.go -COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY --chown=1001:0 cmd/ cmd/ +COPY --chown=1001:0 api/ api/ +COPY --chown=1001:0 internal/controller/ internal/controller/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager ./cmd/ -FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5 +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.8 WORKDIR / COPY --from=builder /opt/app-root/src/manager . USER 65532:65532 diff --git a/infra/feast-operator/Makefile b/infra/feast-operator/Makefile index 9541ed99656..b70608a389e 100644 --- a/infra/feast-operator/Makefile +++ b/infra/feast-operator/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 0.56.0 +VERSION ?= 0.65.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") @@ -48,7 +48,7 @@ endif # Set the Operator SDK version to use. By default, what is installed on the system is used. # This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit. -OPERATOR_SDK_VERSION ?= v1.38.0 +OPERATOR_SDK_VERSION ?= v1.41.0 # Image URL to use all building/pushing image targets # During development and testing, and before make deploy we need to export FS_IMG to point to # the dev image generated using command `make build-feature-server-dev-docker` @@ -56,7 +56,7 @@ IMG ?= $(IMAGE_TAG_BASE):$(VERSION) FS_IMG ?= quay.io/feastdev/feature-server:$(VERSION) CJ_IMG ?= quay.io/openshift/origin-cli:4.17 # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION = 1.30.0 +ENVTEST_K8S_VERSION = 1.31.0 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -116,7 +116,7 @@ vet: ## Run go vet against code. .PHONY: test test: build-installer vet lint envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v test/e2e | grep -v test/data-source-types | grep -v test/upgrade | grep -v test/previous-version) -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" GOTOOLCHAIN=go$(shell go env GOVERSION | sed 's/^go//') go test $$(go list ./... | grep -v test/e2e | grep -v test/data-source-types | grep -v test/upgrade | grep -v test/previous-version) -coverprofile cover.out # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. .PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. @@ -131,10 +131,10 @@ test-upgrade: test-previous-version: go test -timeout 60m ./test/previous-version/ -v -ginkgo.v -# Requires python3 +# Requires python3 with feast installed (use uv run in CI) .PHONY: test-datasources test-datasources: - python3 test/data-source-types/data-source-types.py + cd ../.. && uv run python infra/feast-operator/test/data-source-types/data-source-types.py go test ./test/data-source-types/ .PHONY: lint @@ -206,7 +206,7 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply --server-side --force-conflicts -f - .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. @@ -215,7 +215,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified .PHONY: deploy deploy: manifests kustomize related-image-fs ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + $(KUSTOMIZE) build config/default | $(KUBECTL) apply --server-side --force-conflicts -f - .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. @@ -239,11 +239,11 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ENVSUBST = $(LOCALBIN)/envsubst ## Tool Versions -KUSTOMIZE_VERSION ?= v5.4.2 -CONTROLLER_TOOLS_VERSION ?= v0.15.0 -CRD_REF_DOCS_VERSION ?= v0.1.0 -ENVTEST_VERSION ?= release-0.18 -GOLANGCI_LINT_VERSION ?= v1.59.1 +KUSTOMIZE_VERSION ?= v5.4.3 +CONTROLLER_TOOLS_VERSION ?= v0.18.0 +CRD_REF_DOCS_VERSION ?= v0.2.0 +ENVTEST_VERSION ?= release-0.21 +GOLANGCI_LINT_VERSION ?= v2.12.2 ENVSUBST_VERSION ?= v1.4.2 .PHONY: kustomize @@ -264,7 +264,7 @@ $(ENVTEST): $(LOCALBIN) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) - $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) .PHONY: envsubst envsubst: $(ENVSUBST) ## Download envsubst locally if necessary. @@ -312,7 +312,7 @@ $(CRD_REF_DOCS): $(LOCALBIN) .PHONY: generate-ref generate-ref: generate fmt crd-ref-docs - $(CRD_REF_DOCS) --log-level=WARN --max-depth=30 --config=$(LOCALDIR)/docs/crd-ref-templates/config.yaml --source-path=$(LOCALDIR)/api/v1alpha1 --renderer=markdown --templates-dir=$(LOCALDIR)/docs/crd-ref-templates/markdown --output-path=$(LOCALDIR)/docs/api/markdown/ref.md + $(CRD_REF_DOCS) --log-level=WARN --max-depth=30 --config=$(LOCALDIR)/docs/crd-ref-templates/config.yaml --source-path=$(LOCALDIR)/api/v1 --renderer=markdown --templates-dir=$(LOCALDIR)/docs/crd-ref-templates/markdown --output-path=$(LOCALDIR)/docs/api/markdown/ref.md .PHONY: bundle bundle: manifests kustomize related-image-fs operator-sdk ## Generate bundle manifests and metadata, then validate generated files. @@ -338,7 +338,7 @@ ifeq (,$(shell which opm 2>/dev/null)) set -e ;\ mkdir -p $(dir $(OPM)) ;\ OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ - curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$${OS}-$${ARCH}-opm ;\ + curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.55.0/$${OS}-$${ARCH}-opm ;\ chmod +x $(OPM) ;\ } else diff --git a/infra/feast-operator/README.md b/infra/feast-operator/README.md index 9f3f1080932..f879dff1cc1 100644 --- a/infra/feast-operator/README.md +++ b/infra/feast-operator/README.md @@ -3,6 +3,18 @@ This is a K8s Operator that can be used to deploy and manage **Feast**, an open ### **[FeatureStore CR API Reference](docs/api/markdown/ref.md)** +### **[Operator Configuration Guides](https://docs.feast.dev/how-to-guides/feast-operator)** + +| Guide | Topic | +|-------|-------| +| [1 — Project Provisioning](https://docs.feast.dev/how-to-guides/feast-operator/01-project-provisioning) | `feastProjectDir`: git clone, `feast init`, or a repository packaged in an image | +| [2 — Persistence](https://docs.feast.dev/how-to-guides/feast-operator/02-persistence) | File (path + PVC) vs DB store for offline/online/registry; Secret format | +| [3 — Serving & Observability](https://docs.feast.dev/how-to-guides/feast-operator/03-serving-and-observability) | Workers, log level, Prometheus metrics, offline push batching, MCP | +| [4 — Registry Topology](https://docs.feast.dev/how-to-guides/feast-operator/04-registry-topology) | Local, remote, cross-namespace `feastRef` | +| [5 — Security](https://docs.feast.dev/how-to-guides/feast-operator/05-security) | Kubernetes RBAC roles vs OIDC auth; TLS for all servers | +| [6 — Batch & Jobs](https://docs.feast.dev/how-to-guides/feast-operator/06-batch-and-jobs) | `batchEngine` ConfigMap, `cronJob` for scheduled materialization | +| [7 — OpenLineage & Materialization](https://docs.feast.dev/how-to-guides/feast-operator/07-openlineage-and-materialization) | Lineage transports, API key Secret, materialization batch size | + ## Getting Started ### Prerequisites @@ -13,15 +25,18 @@ This is a K8s Operator that can be used to deploy and manage **Feast**, an open ## To deploy an Operator release on a cluster -Users can just run `kubectl apply -f ` to install the project, i.e.: +Users can deploy the operator using Server-Side Apply to avoid CRD annotation size limits: ```sh ## Install the latest release - -kubectl apply -f https://raw.githubusercontent.com/feast-dev/feast/refs/heads/stable/infra/feast-operator/dist/install.yaml +kubectl apply --server-side --force-conflicts -f https://raw.githubusercontent.com/feast-dev/feast/refs/heads/stable/infra/feast-operator/dist/install.yaml ## OR, install a specific version - -# kubectl apply -f https://raw.githubusercontent.com/feast-dev/feast/refs/tags//infra/feast-operator/dist/install.yaml +# kubectl apply --server-side --force-conflicts -f https://raw.githubusercontent.com/feast-dev/feast/refs/tags//infra/feast-operator/dist/install.yaml ``` + +> **NOTE**: Server-Side Apply (`--server-side`) is required because the CRD includes both v1alpha1 and v1 API versions, making it too large for the standard `kubectl apply` annotation limit. If you encounter annotation size errors, use `--server-side --force-conflicts` flags. + ##### Feast Operator Demo Videos [![](https://img.youtube.com/vi/48cb4AHxPR4/0.jpg)](https://www.youtube.com/playlist?list=PLPzVNzik7rsAN-amQLZckd0so3cIr7blX) diff --git a/infra/feast-operator/api/feastversion/version.go b/infra/feast-operator/api/feastversion/version.go index 9279475e53d..deabd34ac38 100644 --- a/infra/feast-operator/api/feastversion/version.go +++ b/infra/feast-operator/api/feastversion/version.go @@ -17,4 +17,4 @@ limitations under the License. package feastversion // Feast release version. Keep on line #20, this is critical to release CI -const FeastVersion = "0.56.0" +const FeastVersion = "0.65.0" diff --git a/infra/feast-operator/api/v1/featurestore_types.go b/infra/feast-operator/api/v1/featurestore_types.go new file mode 100644 index 00000000000..3372e74f63a --- /dev/null +++ b/infra/feast-operator/api/v1/featurestore_types.go @@ -0,0 +1,1047 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +const ( + // Feast phases: + ReadyPhase = "Ready" + PendingPhase = "Pending" + FailedPhase = "Failed" + + // Feast condition types: + ClientReadyType = "Client" + OfflineStoreReadyType = "OfflineStore" + OnlineStoreReadyType = "OnlineStore" + RegistryReadyType = "Registry" + UIReadyType = "UI" + ReadyType = "FeatureStore" + AuthorizationReadyType = "Authorization" + CronJobReadyType = "CronJob" + + // Feast condition reasons: + ReadyReason = "Ready" + FailedReason = "FeatureStoreFailed" + DeploymentNotAvailableReason = "DeploymentNotAvailable" + OfflineStoreFailedReason = "OfflineStoreDeploymentFailed" + OnlineStoreFailedReason = "OnlineStoreDeploymentFailed" + RegistryFailedReason = "RegistryDeploymentFailed" + UIFailedReason = "UIDeploymentFailed" + ClientFailedReason = "ClientDeploymentFailed" + CronJobFailedReason = "CronJobDeploymentFailed" + KubernetesAuthzFailedReason = "KubernetesAuthorizationDeploymentFailed" + OidcAuthzFailedReason = "OidcAuthorizationDeploymentFailed" + + // Feast condition messages: + ReadyMessage = "FeatureStore installation complete" + OfflineStoreReadyMessage = "Offline Store installation complete" + OnlineStoreReadyMessage = "Online Store installation complete" + RegistryReadyMessage = "Registry installation complete" + UIReadyMessage = "UI installation complete" + ClientReadyMessage = "Client installation complete" + CronJobReadyMessage = "CronJob installation complete" + KubernetesAuthzReadyMessage = "Kubernetes authorization installation complete" + OidcAuthzReadyMessage = "OIDC authorization installation complete" + DeploymentNotAvailableMessage = "Deployment is not available" + + // entity_key_serialization_version + SerializationVersion = 3 +) + +// MaterializationConfig controls feature materialization behavior written into feature_store.yaml. +type MaterializationConfig struct { + // Number of rows per batch when writing to the online store during materialization. + // Prevents OOM for large feature views. Supported engines: local, spark, ray. + // If unset, all rows are written in a single batch. + // +kubebuilder:validation:Minimum=1 + // +optional + OnlineWriteBatchSize *int32 `json:"onlineWriteBatchSize,omitempty"` + // ExtraConfig passes additional materialization key-value settings inline into + // feature_store.yaml. + // +optional + ExtraConfig map[string]string `json:"extraConfig,omitempty"` +} + +// OpenLineageConfig enables OpenLineage data lineage tracking for Feast operations. +// Lineage events are emitted during feast apply and materialization when enabled. +type OpenLineageConfig struct { + // Enable OpenLineage integration. + Enabled bool `json:"enabled"` + // Transport type for lineage events. + // +kubebuilder:validation:Enum=http;console;file;kafka + // +optional + TransportType *string `json:"transportType,omitempty"` + // URL for HTTP transport (e.g. http://marquez:5000). Required when transportType is "http". + // +optional + TransportUrl *string `json:"transportUrl,omitempty"` + // API endpoint path appended to transportUrl. Defaults to "api/v1/lineage". + // +optional + TransportEndpoint *string `json:"transportEndpoint,omitempty"` + // Reference to a Secret containing the key "api_key" for lineage server authentication. + // +optional + ApiKeySecretRef *corev1.LocalObjectReference `json:"apiKeySecretRef,omitempty"` + // ExtraConfig holds additional OpenLineage key-value settings written inline into + // the openlineage block of feature_store.yaml alongside the typed fields above. + // Use this for non-core settings (e.g. namespace, producer, emit_on_apply, + // emit_on_materialize) and transport-specific options (e.g. kafka + // bootstrap_servers, topic; file path). Boolean values ("true"/"false") and + // integer values are automatically coerced to their native YAML types. + // Keys must be valid Feast OpenLineageConfig YAML field names. + // +optional + ExtraConfig map[string]string `json:"extraConfig,omitempty"` + // Consumer configures the OpenLineage consumer (event receiver) that enables + // Feast to receive and display lineage from external producers (Airflow, Spark, dbt, etc.). + // +optional + Consumer *OpenLineageConsumerConfig `json:"consumer,omitempty"` +} + +// OpenLineageConsumerConfig configures the OpenLineage consumer (event receiver). +// When enabled, the Feast REST server exposes POST /api/v1/lineage to receive +// OpenLineage events from any producer, storing them for visualization in the Feast UI. +type OpenLineageConsumerConfig struct { + // Enable the OpenLineage consumer. + Enabled bool `json:"enabled"` + // StoreType is the storage backend for lineage events. Currently only "sql" is supported. + // +kubebuilder:default="sql" + // +kubebuilder:validation:Enum=sql + // +optional + StoreType *string `json:"storeType,omitempty"` + // Reference to a Secret containing the key "connection_string" for a separate + // lineage database. If omitted, the SQL registry database is reused. + // +optional + ConnectionStringSecretRef *corev1.LocalObjectReference `json:"connectionStringSecretRef,omitempty"` + // Reference to a Secret containing the key "api_key" that producers must + // provide in the X-API-Key header when sending events. + // +optional + ApiKeySecretRef *corev1.LocalObjectReference `json:"apiKeySecretRef,omitempty"` + // NamespaceMapping maps OpenLineage namespaces to Feast projects for + // RBAC-based filtering of lineage data in the UI. + // +optional + NamespaceMapping map[string]string `json:"namespaceMapping,omitempty"` +} + +// FeatureStoreSpec defines the desired state of FeatureStore +// +kubebuilder:validation:XValidation:rule="self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) || !has(self.services.scaling.autoscaling)",message="replicas > 1 and services.scaling.autoscaling are mutually exclusive." +// +kubebuilder:validation:XValidation:rule="self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) || !has(self.services.scaling.autoscaling)) || (has(self.services) && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) && has(self.services.onlineStore.persistence.store))",message="Scaling requires DB-backed persistence for the online store. Configure services.onlineStore.persistence.store when using replicas > 1 or autoscaling." +// +kubebuilder:validation:XValidation:rule="self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) || !has(self.services.scaling.autoscaling)) || (!has(self.services) || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) && has(self.services.offlineStore.persistence.store)))",message="Scaling requires DB-backed persistence for the offline store. Configure services.offlineStore.persistence.store when using replicas > 1 or autoscaling." +// +kubebuilder:validation:XValidation:rule="self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) || !has(self.services.scaling.autoscaling)) || (has(self.services) && has(self.services.registry) && (has(self.services.registry.remote) || (has(self.services.registry.local) && has(self.services.registry.local.persistence) && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) && has(self.services.registry.local.persistence.file.path) && (self.services.registry.local.persistence.file.path.startsWith('s3://') || self.services.registry.local.persistence.file.path.startsWith('gs://')))))))",message="Scaling requires DB-backed or remote registry. Configure registry.local.persistence.store or use a remote registry when using replicas > 1 or autoscaling. S3/GCS-backed registry is also allowed." +type FeatureStoreSpec struct { + // +kubebuilder:validation:Pattern="^[A-Za-z0-9][A-Za-z0-9_-]*$" + // FeastProject is the Feast project id. This can be any alphanumeric string with underscores and hyphens, but it cannot start with an underscore or hyphen. Required. + FeastProject string `json:"feastProject"` + FeastProjectDir *FeastProjectDir `json:"feastProjectDir,omitempty"` + Services *FeatureStoreServices `json:"services,omitempty"` + AuthzConfig *AuthzConfig `json:"authz,omitempty"` + CronJob *FeastCronJob `json:"cronJob,omitempty"` + BatchEngine *BatchEngineConfig `json:"batchEngine,omitempty"` + // DataQualityMonitoring configures Data Quality Monitoring behaviour. + // +optional + DataQualityMonitoring *DataQualityMonitoringConfig `json:"dataQualityMonitoring,omitempty"` + // Replicas is the desired number of pod replicas. Used by the scale sub-resource. + // Mutually exclusive with services.scaling.autoscaling. + // +kubebuilder:default=1 + // +kubebuilder:validation:Minimum=1 + Replicas *int32 `json:"replicas,omitempty"` + // Materialization controls feature materialization behavior (batch size, pull strategy). + // Written into feature_store.yaml for all service pods. + // +optional + Materialization *MaterializationConfig `json:"materialization,omitempty"` + // OpenLineage enables OpenLineage data lineage tracking for Feast operations. + // Written into feature_store.yaml for all service pods. + // +optional + OpenLineage *OpenLineageConfig `json:"openlineage,omitempty"` +} + +// FeastProjectDir defines how to create the feast project directory. +// +kubebuilder:validation:XValidation:rule="[has(self.git), has(self.init), has(self.packaged)].exists_one(c, c)",message="One selection required between init, git, or packaged." +type FeastProjectDir struct { + Git *GitCloneOptions `json:"git,omitempty"` + Init *FeastInitOptions `json:"init,omitempty"` + Packaged *FeastPackagedOptions `json:"packaged,omitempty"` +} + +// FeastPackagedOptions describes a feature repository packaged in a feature server image. +// +kubebuilder:validation:XValidation:rule="self.featureRepoPath.startsWith('/') && self.featureRepoPath != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..')",message="FeatureRepoPath must be a canonical absolute, non-root path without dot segments or repeated separators." +type FeastPackagedOptions struct { + // Image containing the packaged feature repository. When set, this image is used by the + // repository initialization and feast apply containers and as the default service image. + // When omitted, the operator's configured feature server image is used. + Image string `json:"image,omitempty"` + // FeatureRepoPath is the canonical absolute path to the feature repository in the image. + FeatureRepoPath string `json:"featureRepoPath"` +} + +// GitCloneOptions describes how a clone should be performed. +// +kubebuilder:validation:XValidation:rule="has(self.featureRepoPath) ? !self.featureRepoPath.startsWith('/') : true",message="RepoPath must be a file name only, with no slashes." +type GitCloneOptions struct { + // The repository URL to clone from. + URL string `json:"url"` + // Reference to a branch / tag / commit + Ref string `json:"ref,omitempty"` + // Configs passed to git via `-c` + // e.g. http.sslVerify: 'false' + // OR 'url."https://api:\${TOKEN}@github.com/".insteadOf': 'https://github.com/' + Configs map[string]string `json:"configs,omitempty"` + // FeatureRepoPath is the relative path to the feature repo subdirectory. Default is 'feature_repo'. + FeatureRepoPath string `json:"featureRepoPath,omitempty"` + Env *[]corev1.EnvVar `json:"env,omitempty"` + EnvFrom *[]corev1.EnvFromSource `json:"envFrom,omitempty"` +} + +// FeastInitOptions defines how to run a `feast init`. +type FeastInitOptions struct { + Minimal bool `json:"minimal,omitempty"` + // Template for the created project + // +kubebuilder:validation:Enum=local;gcp;aws;snowflake;spark;postgres;hbase;cassandra;hazelcast;couchbase;clickhouse;milvus;ray;ray_rag;pytorch_nlp + Template string `json:"template,omitempty"` +} + +// FeastCronJob defines a CronJob to execute against a Feature Store deployment. +type FeastCronJob struct { + // Annotations to be added to the CronJob metadata. + Annotations map[string]string `json:"annotations,omitempty"` + + // Specification of the desired behavior of a job. + JobSpec *JobSpec `json:"jobSpec,omitempty"` + ContainerConfigs *CronJobContainerConfigs `json:"containerConfigs,omitempty"` + + // The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + Schedule string `json:"schedule,omitempty"` + + // The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + // If not specified, this will default to the time zone of the kube-controller-manager process. + // The set of valid time zone names and the time zone offset is loaded from the system-wide time zone + // database by the API server during CronJob validation and the controller manager during execution. + // If no system-wide time zone database can be found a bundled version of the database is used instead. + // If the time zone name becomes invalid during the lifetime of a CronJob or due to a change in host + // configuration, the controller will stop creating new new Jobs and will create a system event with the + // reason UnknownTimeZone. + // More information can be found in https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#time-zones + TimeZone *string `json:"timeZone,omitempty"` + + // Optional deadline in seconds for starting the job if it misses scheduled + // time for any reason. Missed jobs executions will be counted as failed ones. + StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"` + + // Specifies how to treat concurrent executions of a Job. + // Valid values are: + // + // - "Allow" (default): allows CronJobs to run concurrently; + // - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; + // - "Replace": cancels currently running job and replaces it with a new one + ConcurrencyPolicy batchv1.ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"` + + // This flag tells the controller to suspend subsequent executions, it does + // not apply to already started executions. + Suspend *bool `json:"suspend,omitempty"` + + // The number of successful finished jobs to retain. Value must be non-negative integer. + SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"` + + // The number of failed finished jobs to retain. Value must be non-negative integer. + FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"` +} + +// BatchEngineConfig defines the batch compute engine configuration. +type BatchEngineConfig struct { + // Reference to a ConfigMap containing the batch engine configuration. + // The ConfigMap should contain YAML-formatted config with 'type' and engine-specific fields. + ConfigMapRef *corev1.LocalObjectReference `json:"configMapRef,omitempty"` + // Key name in the ConfigMap. Defaults to "config" if not specified. + ConfigMapKey string `json:"configMapKey,omitempty"` +} + +// DataQualityMonitoringConfig defines the Data Quality Monitoring configuration. +type DataQualityMonitoringConfig struct { + // AutoBaseline controls whether baseline distribution is computed automatically on feast apply. Defaults to true. + // +kubebuilder:default=true + AutoBaseline *bool `json:"autoBaseline,omitempty"` +} + +// JobSpec describes how the job execution will look like. +type JobSpec struct { + // PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + // metadata. This is separate from the CronJob-level annotations and must be + // set explicitly by users if they want annotations on the PodTemplate. + PodTemplateAnnotations map[string]string `json:"podTemplateAnnotations,omitempty"` + + // Specifies the maximum desired number of pods the job should + // run at any given time. The actual number of pods running in steady state will + // be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), + // i.e. when the work left to do is less than max parallelism. + // More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ + Parallelism *int32 `json:"parallelism,omitempty"` + + // Specifies the desired number of successfully finished pods the + // job should be run with. Setting to null means that the success of any + // pod signals the success of all pods, and allows parallelism to have any positive + // value. Setting to 1 means that parallelism is limited to 1 and the success of that + // pod signals the success of the job. + // More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ + Completions *int32 `json:"completions,omitempty"` + + // Specifies the duration in seconds relative to the startTime that the job + // may be continuously active before the system tries to terminate it; value + // must be positive integer. If a Job is suspended (at creation or through an + // update), this timer will effectively be stopped and reset when the Job is + // resumed again. + ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty"` + + // Specifies the policy of handling failed pods. In particular, it allows to + // specify the set of actions and conditions which need to be + // satisfied to take the associated action. + // If empty, the default behaviour applies - the counter of failed pods, + // represented by the jobs's .status.failed field, is incremented and it is + // checked against the backoffLimit. This field cannot be used in combination + // with restartPolicy=OnFailure. + // + // This field is beta-level. It can be used when the `JobPodFailurePolicy` + // feature gate is enabled (enabled by default). + PodFailurePolicy *batchv1.PodFailurePolicy `json:"podFailurePolicy,omitempty"` + + // Specifies the number of retries before marking this job failed. + BackoffLimit *int32 `json:"backoffLimit,omitempty"` + + // Specifies the limit for the number of retries within an + // index before marking this index as failed. When enabled the number of + // failures per index is kept in the pod's + // batch.kubernetes.io/job-index-failure-count annotation. It can only + // be set when Job's completionMode=Indexed, and the Pod's restart + // policy is Never. The field is immutable. + // This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` + // feature gate is enabled (enabled by default). + BackoffLimitPerIndex *int32 `json:"backoffLimitPerIndex,omitempty"` + + // Specifies the maximal number of failed indexes before marking the Job as + // failed, when backoffLimitPerIndex is set. Once the number of failed + // indexes exceeds this number the entire Job is marked as Failed and its + // execution is terminated. When left as null the job continues execution of + // all of its indexes and is marked with the `Complete` Job condition. + // It can only be specified when backoffLimitPerIndex is set. + // It can be null or up to completions. It is required and must be + // less than or equal to 10^4 when is completions greater than 10^5. + // This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` + // feature gate is enabled (enabled by default). + MaxFailedIndexes *int32 `json:"maxFailedIndexes,omitempty"` + + // ttlSecondsAfterFinished limits the lifetime of a Job that has finished + // execution (either Complete or Failed). If this field is set, + // ttlSecondsAfterFinished after the Job finishes, it is eligible to be + // automatically deleted. When the Job is being deleted, its lifecycle + // guarantees (e.g. finalizers) will be honored. If this field is unset, + // the Job won't be automatically deleted. If this field is set to zero, + // the Job becomes eligible to be deleted immediately after it finishes. + TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"` + + // completionMode specifies how Pod completions are tracked. It can be + // `NonIndexed` (default) or `Indexed`. + // + // `NonIndexed` means that the Job is considered complete when there have + // been .spec.completions successfully completed Pods. Each Pod completion is + // homologous to each other. + // + // `Indexed` means that the Pods of a + // Job get an associated completion index from 0 to (.spec.completions - 1), + // available in the annotation batch.kubernetes.io/job-completion-index. + // The Job is considered complete when there is one successfully completed Pod + // for each index. + // When value is `Indexed`, .spec.completions must be specified and + // `.spec.parallelism` must be less than or equal to 10^5. + // In addition, The Pod name takes the form + // `$(job-name)-$(index)-$(random-string)`, + // the Pod hostname takes the form `$(job-name)-$(index)`. + // + // More completion modes can be added in the future. + // If the Job controller observes a mode that it doesn't recognize, which + // is possible during upgrades due to version skew, the controller + // skips updates for the Job. + CompletionMode *batchv1.CompletionMode `json:"completionMode,omitempty"` + + // suspend specifies whether the Job controller should create Pods or not. If + // a Job is created with suspend set to true, no Pods are created by the Job + // controller. If a Job is suspended after creation (i.e. the flag goes from + // false to true), the Job controller will delete all active Pods associated + // with this Job. Users must design their workload to gracefully handle this. + // Suspending a Job will reset the StartTime field of the Job, effectively + // resetting the ActiveDeadlineSeconds timer too. + // + Suspend *bool `json:"suspend,omitempty"` + + // podReplacementPolicy specifies when to create replacement Pods. + // Possible values are: + // - TerminatingOrFailed means that we recreate pods + // when they are terminating (has a metadata.deletionTimestamp) or failed. + // - Failed means to wait until a previously created Pod is fully terminated (has phase + // Failed or Succeeded) before creating a replacement Pod. + // + // When using podFailurePolicy, Failed is the the only allowed value. + // TerminatingOrFailed and Failed are allowed values when podFailurePolicy is not in use. + // This is an beta field. To use this, enable the JobPodReplacementPolicy feature toggle. + // This is on by default. + PodReplacementPolicy *batchv1.PodReplacementPolicy `json:"podReplacementPolicy,omitempty"` +} + +// FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. +type FeatureStoreServices struct { + OfflineStore *OfflineStore `json:"offlineStore,omitempty"` + OnlineStore *OnlineStore `json:"onlineStore,omitempty"` + Registry *Registry `json:"registry,omitempty"` + // Creates a UI server container + UI *ServerConfigs `json:"ui,omitempty"` + DeploymentStrategy *appsv1.DeploymentStrategy `json:"deploymentStrategy,omitempty"` + SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` + // PodAnnotations are annotations to be applied to the Deployment's PodTemplate metadata. + // This enables annotation-driven integrations like OpenTelemetry auto-instrumentation, + // Istio sidecar injection, Vault agent injection, etc. + // +optional + PodAnnotations map[string]string `json:"podAnnotations,omitempty"` + // Disable the 'feast repo initialization' initContainer + DisableInitContainers bool `json:"disableInitContainers,omitempty"` + // InitImage overrides the image for init containers (feast-init, feast-apply). + // Resolution order: InitImage → FeastProjectDir.Packaged.Image → RELATED_IMAGE_FEATURE_SERVER → DefaultImage. + // +optional + InitImage *string `json:"initImage,omitempty"` + // Runs feast apply on pod start to populate the registry. Defaults to true. Ignored when DisableInitContainers is true. + RunFeastApplyOnInit *bool `json:"runFeastApplyOnInit,omitempty"` + // Volumes specifies the volumes to mount in the FeatureStore deployment. A corresponding `VolumeMount` should be added to whichever feast service(s) require access to said volume(s). + Volumes []corev1.Volume `json:"volumes,omitempty"` + // Scaling configures horizontal scaling for the FeatureStore deployment (e.g. HPA autoscaling). + // For static replicas, use spec.replicas instead. + Scaling *ScalingConfig `json:"scaling,omitempty"` + // PodDisruptionBudgets configures a PodDisruptionBudget for the FeatureStore deployment. + // Only created when scaling is enabled (replicas > 1 or autoscaling). + // +optional + PodDisruptionBudgets *PDBConfig `json:"podDisruptionBudgets,omitempty"` + // TopologySpreadConstraints defines how pods are spread across topology domains. + // When scaling is enabled and this is not set, the operator auto-injects a soft + // zone-spread constraint (whenUnsatisfiable: ScheduleAnyway). + // Set to an empty array to disable auto-injection. + // +optional + TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + // Affinity defines the pod scheduling constraints for the FeatureStore deployment. + // When scaling is enabled and this is not set, the operator auto-injects a soft + // pod anti-affinity rule to prefer spreading pods across nodes. + // +optional + Affinity *corev1.Affinity `json:"affinity,omitempty"` + // ResourceClaims defines which ResourceClaims must be allocated + // and reserved before the Pod is allowed to start. The resources + // will be made available to those containers which consume them + // by name. + // + // +patchMergeKey=name + // +patchStrategy=merge,retainKeys + // +listType=map + // +listMapKey=name + // +optional + ResourceClaims []corev1.PodResourceClaim `json:"resourceClaims,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name"` +} + +// ScalingConfig configures horizontal scaling for the FeatureStore deployment. +type ScalingConfig struct { + // Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + // Mutually exclusive with spec.replicas. + // +optional + Autoscaling *AutoscalingConfig `json:"autoscaling,omitempty"` +} + +// AutoscalingConfig defines HPA settings for the FeatureStore deployment. +type AutoscalingConfig struct { + // MinReplicas is the lower limit for the number of replicas. Defaults to 1. + // +kubebuilder:validation:Minimum=1 + // +optional + MinReplicas *int32 `json:"minReplicas,omitempty"` + // MaxReplicas is the upper limit for the number of replicas. Required. + // +kubebuilder:validation:Minimum=1 + MaxReplicas int32 `json:"maxReplicas"` + // Metrics contains the specifications for which to use to calculate the desired replica count. + // If not set, defaults to 80% CPU utilization. + // +optional + Metrics []autoscalingv2.MetricSpec `json:"metrics,omitempty"` + // Behavior configures the scaling behavior of the target. + // +optional + Behavior *autoscalingv2.HorizontalPodAutoscalerBehavior `json:"behavior,omitempty"` +} + +// PDBConfig configures a PodDisruptionBudget for the FeatureStore deployment. +// Exactly one of minAvailable or maxUnavailable must be set. +// +kubebuilder:validation:XValidation:rule="[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, c)",message="Exactly one of minAvailable or maxUnavailable must be set." +type PDBConfig struct { + // MinAvailable specifies the minimum number/percentage of pods that must remain available. + // Mutually exclusive with maxUnavailable. + // +optional + MinAvailable *intstr.IntOrString `json:"minAvailable,omitempty"` + // MaxUnavailable specifies the maximum number/percentage of pods that can be unavailable. + // Mutually exclusive with minAvailable. + // +optional + MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"` +} + +// OfflineStore configures the offline store service +type OfflineStore struct { + // Creates a remote offline server container + Server *ServerConfigs `json:"server,omitempty"` + Persistence *OfflineStorePersistence `json:"persistence,omitempty"` +} + +// OfflineStorePersistence configures the persistence settings for the offline store service +// +kubebuilder:validation:XValidation:rule="[has(self.file), has(self.store)].exists_one(c, c)",message="One selection required between file or store." +type OfflineStorePersistence struct { + FilePersistence *OfflineStoreFilePersistence `json:"file,omitempty"` + DBPersistence *OfflineStoreDBStorePersistence `json:"store,omitempty"` +} + +// OfflineStoreFilePersistence configures the file-based persistence for the offline store service +type OfflineStoreFilePersistence struct { + // +kubebuilder:validation:Enum=file;dask;duckdb + Type string `json:"type,omitempty"` + PvcConfig *PvcConfig `json:"pvc,omitempty"` +} + +var ValidOfflineStoreFilePersistenceTypes = []string{ + "dask", + "duckdb", + "file", +} + +// OfflineStoreDBStorePersistence configures the DB store persistence for the offline store service +type OfflineStoreDBStorePersistence struct { + // Type of the persistence type you want to use. + // +kubebuilder:validation:Enum=snowflake.offline;bigquery;redshift;spark;postgres;trino;athena;mssql;couchbase.offline;clickhouse;ray;oracle + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the selected store "type" is used as the SecretKeyName + SecretKeyName string `json:"secretKeyName,omitempty"` +} + +var ValidOfflineStoreDBStorePersistenceTypes = []string{ + "snowflake.offline", + "bigquery", + "redshift", + "spark", + "postgres", + "trino", + "athena", + "mssql", + "couchbase.offline", + "clickhouse", + "ray", + "oracle", +} + +// OnlineStore configures the online store service +type OnlineStore struct { + // Creates a feature server container + Server *ServerConfigs `json:"server,omitempty"` + Persistence *OnlineStorePersistence `json:"persistence,omitempty"` + // Serving configures the Feast feature_server section written into feature_store.yaml for the online serve pod. + // Controls metrics granularity, offline push batching, and MCP. + // +optional + Serving *ServingConfig `json:"serving,omitempty"` + // Disabled skips deploying the online store service entirely, including its + // serving pod and persistence. Omitting the online store block, or setting + // this to false, deploys the online store with defaults as before. + // +optional + Disabled bool `json:"disabled,omitempty"` +} + +// ServingConfig configures the feature_server section of the generated feature_store.yaml. +// When Mcp is set, the feature server type is switched to "mcp"; otherwise "local" is used. +type ServingConfig struct { + // Metrics configures per-category Prometheus metrics for the feature server. + // Coexists with the server.metrics bool flag — both can be set simultaneously. + // +optional + Metrics *ServingMetricsConfig `json:"metrics,omitempty"` + // OfflinePushBatching batches writes to the offline store via the /push endpoint. + // +optional + OfflinePushBatching *OfflinePushBatchingConfig `json:"offlinePushBatching,omitempty"` + // Mcp enables MCP (Model Context Protocol) server support. When set, feature server type is "mcp". + // +optional + Mcp *McpConfig `json:"mcp,omitempty"` +} + +// ServingMetricsConfig controls per-category Prometheus metrics for the feature server. +// Setting Enabled to true activates the metrics HTTP server on port 8000. +// All metric categories default to true when enabled; use Categories to selectively disable them. +type ServingMetricsConfig struct { + // Enable the Prometheus metrics endpoint on port 8000. + Enabled bool `json:"enabled"` + // Categories selectively enables or disables individual Feast metric categories. + // Keys are Feast MetricsConfig field names (e.g. "resource", "request", + // "online_features", "push", "materialization", "freshness"). Omitted keys + // default to true when metrics is enabled. + // +optional + Categories map[string]bool `json:"categories,omitempty"` +} + +// OfflinePushBatchingConfig controls batching of writes to the offline store via the /push endpoint. +// Recommended for high-throughput push workloads (streaming pipelines, IoT) to prevent OOM. +type OfflinePushBatchingConfig struct { + // Enable offline push batching. + Enabled bool `json:"enabled"` + // Maximum number of rows per offline write batch. + // +kubebuilder:validation:Minimum=1 + // +optional + BatchSize *int32 `json:"batchSize,omitempty"` + // Seconds between batch flushes to the offline store. + // +kubebuilder:validation:Minimum=1 + // +optional + BatchIntervalSeconds *int32 `json:"batchIntervalSeconds,omitempty"` +} + +// McpConfig enables MCP (Model Context Protocol) server support in the feature server. +// When this field is set on ServingConfig, the feature server type is switched to "mcp". +type McpConfig struct { + // Enable the MCP server. + Enabled bool `json:"enabled"` + // MCP server name for identification. Defaults to "feast-mcp-server". + // +optional + ServerName *string `json:"serverName,omitempty"` + // MCP server version string. Defaults to "1.0.0". + // +optional + ServerVersion *string `json:"serverVersion,omitempty"` + // MCP transport protocol. + // +kubebuilder:validation:Enum=sse;http + // +optional + Transport *string `json:"transport,omitempty"` +} + +// OnlineStorePersistence configures the persistence settings for the online store service +// +kubebuilder:validation:XValidation:rule="[has(self.file), has(self.store)].exists_one(c, c)",message="One selection required between file or store." +type OnlineStorePersistence struct { + FilePersistence *OnlineStoreFilePersistence `json:"file,omitempty"` + DBPersistence *OnlineStoreDBStorePersistence `json:"store,omitempty"` +} + +// OnlineStoreFilePersistence configures the file-based persistence for the online store service +// +kubebuilder:validation:XValidation:rule="(!has(self.pvc) && has(self.path)) ? self.path.startsWith('/') : true",message="Ephemeral stores must have absolute paths." +// +kubebuilder:validation:XValidation:rule="(has(self.pvc) && has(self.path)) ? !self.path.startsWith('/') : true",message="PVC path must be a file name only, with no slashes." +// +kubebuilder:validation:XValidation:rule="has(self.path) ? !(self.path.startsWith('s3://') || self.path.startsWith('gs://')) : true",message="Online store does not support S3 or GS buckets." +type OnlineStoreFilePersistence struct { + Path string `json:"path,omitempty"` + PvcConfig *PvcConfig `json:"pvc,omitempty"` +} + +// OnlineStoreDBStorePersistence configures the DB store persistence for the online store service +type OnlineStoreDBStorePersistence struct { + // Type of the persistence type you want to use. + // +kubebuilder:validation:Enum=snowflake.online;redis;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore;hbase;elasticsearch;qdrant;couchbase.online;milvus;hybrid;mongodb;aerospike;scylladb + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the selected store "type" is used as the SecretKeyName + SecretKeyName string `json:"secretKeyName,omitempty"` +} + +var ValidOnlineStoreDBStorePersistenceTypes = []string{ + "snowflake.online", + "redis", + "datastore", + "dynamodb", + "bigtable", + "postgres", + "cassandra", + "mysql", + "hazelcast", + "singlestore", + "hbase", + "elasticsearch", + "qdrant", + "couchbase.online", + "milvus", + "hybrid", + "mongodb", + "aerospike", + "scylladb", +} + +// LocalRegistryConfig configures the registry service +type LocalRegistryConfig struct { + // Creates a registry server container + Server *RegistryServerConfigs `json:"server,omitempty"` + Persistence *RegistryPersistence `json:"persistence,omitempty"` +} + +// RegistryPersistence configures the persistence settings for the registry service +// +kubebuilder:validation:XValidation:rule="[has(self.file), has(self.store)].exists_one(c, c)",message="One selection required between file or store." +type RegistryPersistence struct { + FilePersistence *RegistryFilePersistence `json:"file,omitempty"` + DBPersistence *RegistryDBStorePersistence `json:"store,omitempty"` +} + +// RegistryFilePersistence configures the file-based persistence for the registry service +// +kubebuilder:validation:XValidation:rule="(!has(self.pvc) && has(self.path)) ? (self.path.startsWith('/') || self.path.startsWith('s3://') || self.path.startsWith('gs://')) : true",message="Registry files must use absolute paths or be S3 ('s3://') or GS ('gs://') object store URIs." +// +kubebuilder:validation:XValidation:rule="(has(self.pvc) && has(self.path)) ? !self.path.startsWith('/') : true",message="PVC path must be a file name only, with no slashes." +// +kubebuilder:validation:XValidation:rule="(has(self.pvc) && has(self.path)) ? !(self.path.startsWith('s3://') || self.path.startsWith('gs://')) : true",message="PVC persistence does not support S3 or GS object store URIs." +// +kubebuilder:validation:XValidation:rule="(has(self.s3_additional_kwargs) && has(self.path)) ? self.path.startsWith('s3://') : true",message="Additional S3 settings are available only for S3 object store URIs." +type RegistryFilePersistence struct { + Path string `json:"path,omitempty"` + PvcConfig *PvcConfig `json:"pvc,omitempty"` + S3AdditionalKwargs *map[string]string `json:"s3_additional_kwargs,omitempty"` + + // CacheTTLSeconds defines the TTL (in seconds) for the registry cache. + // +kubebuilder:validation:Minimum=0 + // +optional + CacheTTLSeconds *int32 `json:"cache_ttl_seconds,omitempty"` + + // CacheMode defines the registry cache update strategy. + // Allowed values are "sync" and "thread". + // +kubebuilder:validation:Enum=none;sync;thread + // +optional + CacheMode *string `json:"cache_mode,omitempty"` +} + +// RegistryDBStorePersistence configures the DB store persistence for the registry service +type RegistryDBStorePersistence struct { + // Type of the persistence type you want to use. + // +kubebuilder:validation:Enum=sql;snowflake.registry + Type string `json:"type"` + // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. + SecretRef corev1.LocalObjectReference `json:"secretRef"` + // By default, the selected store "type" is used as the SecretKeyName + SecretKeyName string `json:"secretKeyName,omitempty"` +} + +var ValidRegistryDBStorePersistenceTypes = []string{ + "sql", + "snowflake.registry", +} + +// PvcConfig defines the settings for a persistent file store based on PVCs. +// We can refer to an existing PVC using the `Ref` field, or create a new one using the `Create` field. +// +kubebuilder:validation:XValidation:rule="[has(self.ref), has(self.create)].exists_one(c, c)",message="One selection is required between ref and create." +// +kubebuilder:validation:XValidation:rule="self.mountPath.matches('^/[^:]*$')",message="Mount path must start with '/' and must not contain ':'" +type PvcConfig struct { + // Reference to an existing field + Ref *corev1.LocalObjectReference `json:"ref,omitempty"` + // Settings for creating a new PVC + Create *PvcCreate `json:"create,omitempty"` + // MountPath within the container at which the volume should be mounted. + // Must start by "/" and cannot contain ':'. + MountPath string `json:"mountPath"` +} + +// PvcCreate defines the immutable settings to create a new PVC mounted at the given path. +// The PVC name is the same as the associated deployment & feast service name. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="PvcCreate is immutable" +type PvcCreate struct { + // AccessModes k8s persistent volume access modes. Defaults to ["ReadWriteOnce"]. + AccessModes []corev1.PersistentVolumeAccessMode `json:"accessModes,omitempty"` + // StorageClassName is the name of an existing StorageClass to which this persistent volume belongs. Empty value + // means that this volume does not belong to any StorageClass and the cluster default will be used. + StorageClassName *string `json:"storageClassName,omitempty"` + // Resources describes the storage resource requirements for a volume. + // Default requested storage size depends on the associated service: + // - 10Gi for offline store + // - 5Gi for online store + // - 5Gi for registry + Resources corev1.VolumeResourceRequirements `json:"resources,omitempty"` +} + +// Registry configures the registry service. One selection is required. Local is the default setting. +// +kubebuilder:validation:XValidation:rule="[has(self.local), has(self.remote)].exists_one(c, c)",message="One selection required." +type Registry struct { + Local *LocalRegistryConfig `json:"local,omitempty"` + Remote *RemoteRegistryConfig `json:"remote,omitempty"` +} + +// RemoteRegistryConfig points to a remote feast registry server. When set, the operator will not deploy a registry for this FeatureStore CR. +// Instead, this FeatureStore CR's online/offline services will use a remote registry. One selection is required. +// +kubebuilder:validation:XValidation:rule="[has(self.hostname), has(self.feastRef)].exists_one(c, c)",message="One selection required." +type RemoteRegistryConfig struct { + // Host address of the remote registry service - :, e.g. `registry..svc.cluster.local:80` + Hostname *string `json:"hostname,omitempty"` + // Reference to an existing `FeatureStore` CR in the same k8s cluster. + FeastRef *FeatureStoreRef `json:"feastRef,omitempty"` + TLS *TlsRemoteRegistryConfigs `json:"tls,omitempty"` +} + +// FeatureStoreRef defines which existing FeatureStore's registry should be used +type FeatureStoreRef struct { + // Name of the FeatureStore + Name string `json:"name"` + // Namespace of the FeatureStore + Namespace string `json:"namespace,omitempty"` +} + +// ServerConfigs creates a server for the feast service, with specified container configurations. +type ServerConfigs struct { + ContainerConfigs `json:",inline"` + TLS *TlsConfigs `json:"tls,omitempty"` + // LogLevel sets the logging level for the server + // Allowed values: "debug", "info", "warning", "error", "critical". + // +kubebuilder:validation:Enum=debug;info;warning;error;critical + LogLevel *string `json:"logLevel,omitempty"` + // Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. + Metrics *bool `json:"metrics,omitempty"` + // VolumeMounts defines the list of volumes that should be mounted into the feast container. + // This allows attaching persistent storage, config files, secrets, or other resources + // required by the Feast components. Ensure that each volume mount has a corresponding + // volume definition in the Volumes field. + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` + // WorkerConfigs defines the worker configuration for the Feast server. + // These options are primarily used for production deployments to optimize performance. + WorkerConfigs *WorkerConfigs `json:"workerConfigs,omitempty"` +} + +// WorkerConfigs defines the worker configuration for Feast servers. +// These settings control gunicorn worker processes for production deployments. +type WorkerConfigs struct { + // Workers is the number of worker processes. Use -1 to auto-calculate based on CPU cores (2 * CPU + 1). + // Defaults to 1 if not specified. + // +kubebuilder:validation:Minimum=-1 + // +optional + Workers *int32 `json:"workers,omitempty"` + // WorkerConnections is the maximum number of simultaneous clients per worker process. + // Defaults to 1000. + // +kubebuilder:validation:Minimum=1 + // +optional + WorkerConnections *int32 `json:"workerConnections,omitempty"` + // MaxRequests is the maximum number of requests a worker will process before restarting. + // This helps prevent memory leaks. Defaults to 1000. + // +kubebuilder:validation:Minimum=0 + // +optional + MaxRequests *int32 `json:"maxRequests,omitempty"` + // MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + // thundering herd effect on worker restart. Defaults to 50. + // +kubebuilder:validation:Minimum=0 + // +optional + MaxRequestsJitter *int32 `json:"maxRequestsJitter,omitempty"` + // KeepAliveTimeout is the timeout for keep-alive connections in seconds. + // Defaults to 30. + // +kubebuilder:validation:Minimum=1 + // +optional + KeepAliveTimeout *int32 `json:"keepAliveTimeout,omitempty"` + // RegistryTTLSeconds is the number of seconds after which the registry is refreshed. + // Higher values reduce refresh overhead but increase staleness. Defaults to 60. + // +kubebuilder:validation:Minimum=0 + // +optional + RegistryTTLSeconds *int32 `json:"registryTTLSeconds,omitempty"` +} + +// RegistryServerConfigs creates a registry server for the feast service, with specified container configurations. +// +kubebuilder:validation:XValidation:rule="self.restAPI == true || self.grpc == true || !has(self.grpc)", message="At least one of restAPI or grpc must be true" +// +kubebuilder:validation:XValidation:rule="!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) && self.restAPI == true)", message="MCP requires restAPI to be true" +type RegistryServerConfigs struct { + ServerConfigs `json:",inline"` + + // Enable REST API registry server. + RestAPI *bool `json:"restAPI,omitempty"` + + // Enable gRPC registry server. Defaults to true if unset. + GRPC *bool `json:"grpc,omitempty"` + + // Mcp enables MCP (Model Context Protocol) on the REST registry server. + // Requires restAPI to be true. Reuses the same McpConfig struct as the online store. + // +optional + Mcp *McpConfig `json:"mcp,omitempty"` +} + +// CronJobContainerConfigs k8s container settings for the CronJob +type CronJobContainerConfigs struct { + ContainerConfigs `json:",inline"` + + // Array of commands to be executed (in order) against a Feature Store deployment. + // Defaults to "feast apply" & "feast materialize-incremental $(date -u +'%Y-%m-%dT%H:%M:%S')" + Commands []string `json:"commands,omitempty"` +} + +// ContainerConfigs k8s container settings for the server +type ContainerConfigs struct { + DefaultCtrConfigs `json:",inline"` + OptionalCtrConfigs `json:",inline"` +} + +// DefaultCtrConfigs k8s container settings that are applied by default +type DefaultCtrConfigs struct { + Image *string `json:"image,omitempty"` +} + +// OptionalCtrConfigs k8s container settings that are optional +type OptionalCtrConfigs struct { + Env *[]corev1.EnvVar `json:"env,omitempty"` + EnvFrom *[]corev1.EnvFromSource `json:"envFrom,omitempty"` + ImagePullPolicy *corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + NodeSelector *map[string]string `json:"nodeSelector,omitempty"` +} + +// AuthzConfig defines the authorization settings for the deployed Feast services. +// +kubebuilder:validation:XValidation:rule="[has(self.kubernetes), has(self.oidc)].exists_one(c, c)",message="One selection required between kubernetes or oidc." +type AuthzConfig struct { + KubernetesAuthz *KubernetesAuthz `json:"kubernetes,omitempty"` + OidcAuthz *OidcAuthz `json:"oidc,omitempty"` +} + +// KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. +// https://kubernetes.io/docs/reference/access-authn-authz/rbac/ +type KubernetesAuthz struct { + // The Kubernetes RBAC roles to be deployed in the same namespace of the FeatureStore. + // Roles are managed by the operator and created with an empty list of rules. + // See the Feast permission model at https://docs.feast.dev/getting-started/concepts/permission + // The feature store admin is not obligated to manage roles using the Feast operator, roles can be managed independently. + // This configuration option is only providing a way to automate this procedure. + // Important note: the operator cannot ensure that these roles will match the ones used in the configured Feast permissions. + Roles []string `json:"roles,omitempty"` +} + +// OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. +// https://auth0.com/docs/authenticate/protocols/openid-connect-protocol +type OidcAuthz struct { + // OIDC issuer URL. The operator appends /.well-known/openid-configuration to derive the discovery endpoint. + // +optional + // +kubebuilder:validation:Pattern=`^https://\S+$` + IssuerUrl string `json:"issuerUrl,omitempty"` + // Secret with OIDC properties (auth_discovery_url, client_id, client_secret). issuerUrl takes precedence. + // +optional + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` + // Key in the Secret containing all OIDC properties as a YAML value. If unset, each key is a property. + // +optional + SecretKeyName string `json:"secretKeyName,omitempty"` + // Env var name for client pods to read an OIDC token from. Sets token_env_var in client config. + // +optional + TokenEnvVar *string `json:"tokenEnvVar,omitempty"` + // Verify SSL certificates for the OIDC provider. Defaults to true. + // +optional + VerifySSL *bool `json:"verifySSL,omitempty"` + // ConfigMap with the CA certificate for self-signed OIDC providers. Auto-detected on RHOAI/ODH. + // +optional + CACertConfigMap *OidcCACertConfigMap `json:"caCertConfigMap,omitempty"` +} + +// OidcCACertConfigMap references a ConfigMap containing a CA certificate for OIDC provider TLS. +type OidcCACertConfigMap struct { + // ConfigMap name. + Name string `json:"name"` + // Key in the ConfigMap holding the PEM certificate. Defaults to "ca-bundle.crt". + // +optional + Key string `json:"key,omitempty"` +} + +// TlsConfigs configures server TLS for a feast service. in an openshift cluster, this is configured by default using service serving certificates. +// +kubebuilder:validation:XValidation:rule="(!has(self.disable) || !self.disable) ? has(self.secretRef) : true",message="`secretRef` required if `disable` is false." +type TlsConfigs struct { + // references the local k8s secret where the TLS key and cert reside + SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` + SecretKeyNames SecretKeyNames `json:"secretKeyNames,omitempty"` + // will disable TLS for the feast service. useful in an openshift cluster, for example, where TLS is configured by default + Disable *bool `json:"disable,omitempty"` +} + +// `secretRef` required if `disable` is false. +func (tls *TlsConfigs) IsTLS() bool { + if tls != nil { + if tls.Disable != nil && *tls.Disable { + return false + } else if tls.SecretRef == nil { + return false + } + return true + } + return false +} + +// TlsRemoteRegistryConfigs configures client TLS for a remote feast registry. in an openshift cluster, this is configured by default when the remote feast registry is using service serving certificates. +type TlsRemoteRegistryConfigs struct { + // references the local k8s configmap where the TLS cert resides + ConfigMapRef corev1.LocalObjectReference `json:"configMapRef"` + // defines the configmap key name for the client TLS cert. + CertName string `json:"certName"` +} + +// SecretKeyNames defines the secret key names for the TLS key and cert. +type SecretKeyNames struct { + // defaults to "tls.crt" + TlsCrt string `json:"tlsCrt,omitempty"` + // defaults to "tls.key" + TlsKey string `json:"tlsKey,omitempty"` +} + +// FeatureStoreStatus defines the observed state of FeatureStore +type FeatureStoreStatus struct { + // Shows the currently applied feast configuration, including any pertinent defaults + Applied FeatureStoreSpec `json:"applied,omitempty"` + // ConfigMap in this namespace containing a client `feature_store.yaml` for this feast deployment + ClientConfigMap string `json:"clientConfigMap,omitempty"` + // CronJob in this namespace for this feast deployment + CronJob string `json:"cronJob,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + FeastVersion string `json:"feastVersion,omitempty"` + Phase string `json:"phase,omitempty"` + ServiceHostnames ServiceHostnames `json:"serviceHostnames,omitempty"` + // Replicas is the current number of ready pod replicas (used by the scale sub-resource). + Replicas int32 `json:"replicas,omitempty"` + // Selector is the label selector for pods managed by the FeatureStore deployment (used by the scale sub-resource). + Selector string `json:"selector,omitempty"` + // ScalingStatus reports the current scaling state of the FeatureStore deployment. + ScalingStatus *ScalingStatus `json:"scalingStatus,omitempty"` +} + +// ScalingStatus reports the observed scaling state. +type ScalingStatus struct { + // CurrentReplicas is the current number of pod replicas. + CurrentReplicas int32 `json:"currentReplicas,omitempty"` + // DesiredReplicas is the desired number of pod replicas. + DesiredReplicas int32 `json:"desiredReplicas,omitempty"` +} + +// ServiceHostnames defines the service hostnames in the format of :, e.g. example.svc.cluster.local:80 +type ServiceHostnames struct { + OfflineStore string `json:"offlineStore,omitempty"` + OnlineStore string `json:"onlineStore,omitempty"` + Registry string `json:"registry,omitempty"` + RegistryRest string `json:"registryRest,omitempty"` + UI string `json:"ui,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=feast +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector +// +kubebuilder:storageversion + +// FeatureStore is the Schema for the featurestores API +type FeatureStore struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FeatureStoreSpec `json:"spec,omitempty"` + Status FeatureStoreStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// FeatureStoreList contains a list of FeatureStore +type FeatureStoreList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FeatureStore `json:"items"` +} + +func init() { + SchemeBuilder.Register(&FeatureStore{}, &FeatureStoreList{}) +} diff --git a/infra/feast-operator/api/v1/groupversion_info.go b/infra/feast-operator/api/v1/groupversion_info.go new file mode 100644 index 00000000000..91add6e1e55 --- /dev/null +++ b/infra/feast-operator/api/v1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1 contains API Schema definitions for the v1 API group +// +kubebuilder:object:generate=true +// +groupName=feast.dev +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "feast.dev", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/infra/feast-operator/api/v1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..3035ed066cf --- /dev/null +++ b/infra/feast-operator/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,1672 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + appsv1 "k8s.io/api/apps/v1" + "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthzConfig) DeepCopyInto(out *AuthzConfig) { + *out = *in + if in.KubernetesAuthz != nil { + in, out := &in.KubernetesAuthz, &out.KubernetesAuthz + *out = new(KubernetesAuthz) + (*in).DeepCopyInto(*out) + } + if in.OidcAuthz != nil { + in, out := &in.OidcAuthz, &out.OidcAuthz + *out = new(OidcAuthz) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthzConfig. +func (in *AuthzConfig) DeepCopy() *AuthzConfig { + if in == nil { + return nil + } + out := new(AuthzConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AutoscalingConfig) DeepCopyInto(out *AutoscalingConfig) { + *out = *in + if in.MinReplicas != nil { + in, out := &in.MinReplicas, &out.MinReplicas + *out = new(int32) + **out = **in + } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make([]v2.MetricSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Behavior != nil { + in, out := &in.Behavior, &out.Behavior + *out = new(v2.HorizontalPodAutoscalerBehavior) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalingConfig. +func (in *AutoscalingConfig) DeepCopy() *AutoscalingConfig { + if in == nil { + return nil + } + out := new(AutoscalingConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BatchEngineConfig) DeepCopyInto(out *BatchEngineConfig) { + *out = *in + if in.ConfigMapRef != nil { + in, out := &in.ConfigMapRef, &out.ConfigMapRef + *out = new(corev1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BatchEngineConfig. +func (in *BatchEngineConfig) DeepCopy() *BatchEngineConfig { + if in == nil { + return nil + } + out := new(BatchEngineConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerConfigs) DeepCopyInto(out *ContainerConfigs) { + *out = *in + in.DefaultCtrConfigs.DeepCopyInto(&out.DefaultCtrConfigs) + in.OptionalCtrConfigs.DeepCopyInto(&out.OptionalCtrConfigs) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerConfigs. +func (in *ContainerConfigs) DeepCopy() *ContainerConfigs { + if in == nil { + return nil + } + out := new(ContainerConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CronJobContainerConfigs) DeepCopyInto(out *CronJobContainerConfigs) { + *out = *in + in.ContainerConfigs.DeepCopyInto(&out.ContainerConfigs) + if in.Commands != nil { + in, out := &in.Commands, &out.Commands + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobContainerConfigs. +func (in *CronJobContainerConfigs) DeepCopy() *CronJobContainerConfigs { + if in == nil { + return nil + } + out := new(CronJobContainerConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataQualityMonitoringConfig) DeepCopyInto(out *DataQualityMonitoringConfig) { + *out = *in + if in.AutoBaseline != nil { + in, out := &in.AutoBaseline, &out.AutoBaseline + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataQualityMonitoringConfig. +func (in *DataQualityMonitoringConfig) DeepCopy() *DataQualityMonitoringConfig { + if in == nil { + return nil + } + out := new(DataQualityMonitoringConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultCtrConfigs) DeepCopyInto(out *DefaultCtrConfigs) { + *out = *in + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultCtrConfigs. +func (in *DefaultCtrConfigs) DeepCopy() *DefaultCtrConfigs { + if in == nil { + return nil + } + out := new(DefaultCtrConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeastCronJob) DeepCopyInto(out *FeastCronJob) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.JobSpec != nil { + in, out := &in.JobSpec, &out.JobSpec + *out = new(JobSpec) + (*in).DeepCopyInto(*out) + } + if in.ContainerConfigs != nil { + in, out := &in.ContainerConfigs, &out.ContainerConfigs + *out = new(CronJobContainerConfigs) + (*in).DeepCopyInto(*out) + } + if in.TimeZone != nil { + in, out := &in.TimeZone, &out.TimeZone + *out = new(string) + **out = **in + } + if in.StartingDeadlineSeconds != nil { + in, out := &in.StartingDeadlineSeconds, &out.StartingDeadlineSeconds + *out = new(int64) + **out = **in + } + if in.Suspend != nil { + in, out := &in.Suspend, &out.Suspend + *out = new(bool) + **out = **in + } + if in.SuccessfulJobsHistoryLimit != nil { + in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit + *out = new(int32) + **out = **in + } + if in.FailedJobsHistoryLimit != nil { + in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastCronJob. +func (in *FeastCronJob) DeepCopy() *FeastCronJob { + if in == nil { + return nil + } + out := new(FeastCronJob) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeastInitOptions) DeepCopyInto(out *FeastInitOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastInitOptions. +func (in *FeastInitOptions) DeepCopy() *FeastInitOptions { + if in == nil { + return nil + } + out := new(FeastInitOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeastPackagedOptions) DeepCopyInto(out *FeastPackagedOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastPackagedOptions. +func (in *FeastPackagedOptions) DeepCopy() *FeastPackagedOptions { + if in == nil { + return nil + } + out := new(FeastPackagedOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeastProjectDir) DeepCopyInto(out *FeastProjectDir) { + *out = *in + if in.Git != nil { + in, out := &in.Git, &out.Git + *out = new(GitCloneOptions) + (*in).DeepCopyInto(*out) + } + if in.Init != nil { + in, out := &in.Init, &out.Init + *out = new(FeastInitOptions) + **out = **in + } + if in.Packaged != nil { + in, out := &in.Packaged, &out.Packaged + *out = new(FeastPackagedOptions) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastProjectDir. +func (in *FeastProjectDir) DeepCopy() *FeastProjectDir { + if in == nil { + return nil + } + out := new(FeastProjectDir) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStore) DeepCopyInto(out *FeatureStore) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStore. +func (in *FeatureStore) DeepCopy() *FeatureStore { + if in == nil { + return nil + } + out := new(FeatureStore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FeatureStore) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreList) DeepCopyInto(out *FeatureStoreList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FeatureStore, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreList. +func (in *FeatureStoreList) DeepCopy() *FeatureStoreList { + if in == nil { + return nil + } + out := new(FeatureStoreList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FeatureStoreList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreRef) DeepCopyInto(out *FeatureStoreRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreRef. +func (in *FeatureStoreRef) DeepCopy() *FeatureStoreRef { + if in == nil { + return nil + } + out := new(FeatureStoreRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreServices) DeepCopyInto(out *FeatureStoreServices) { + *out = *in + if in.OfflineStore != nil { + in, out := &in.OfflineStore, &out.OfflineStore + *out = new(OfflineStore) + (*in).DeepCopyInto(*out) + } + if in.OnlineStore != nil { + in, out := &in.OnlineStore, &out.OnlineStore + *out = new(OnlineStore) + (*in).DeepCopyInto(*out) + } + if in.Registry != nil { + in, out := &in.Registry, &out.Registry + *out = new(Registry) + (*in).DeepCopyInto(*out) + } + if in.UI != nil { + in, out := &in.UI, &out.UI + *out = new(ServerConfigs) + (*in).DeepCopyInto(*out) + } + if in.DeploymentStrategy != nil { + in, out := &in.DeploymentStrategy, &out.DeploymentStrategy + *out = new(appsv1.DeploymentStrategy) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(corev1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + if in.PodAnnotations != nil { + in, out := &in.PodAnnotations, &out.PodAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.InitImage != nil { + in, out := &in.InitImage, &out.InitImage + *out = new(string) + **out = **in + } + if in.RunFeastApplyOnInit != nil { + in, out := &in.RunFeastApplyOnInit, &out.RunFeastApplyOnInit + *out = new(bool) + **out = **in + } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]corev1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Scaling != nil { + in, out := &in.Scaling, &out.Scaling + *out = new(ScalingConfig) + (*in).DeepCopyInto(*out) + } + if in.PodDisruptionBudgets != nil { + in, out := &in.PodDisruptionBudgets, &out.PodDisruptionBudgets + *out = new(PDBConfig) + (*in).DeepCopyInto(*out) + } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]corev1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(corev1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.ResourceClaims != nil { + in, out := &in.ResourceClaims, &out.ResourceClaims + *out = make([]corev1.PodResourceClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreServices. +func (in *FeatureStoreServices) DeepCopy() *FeatureStoreServices { + if in == nil { + return nil + } + out := new(FeatureStoreServices) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreSpec) DeepCopyInto(out *FeatureStoreSpec) { + *out = *in + if in.FeastProjectDir != nil { + in, out := &in.FeastProjectDir, &out.FeastProjectDir + *out = new(FeastProjectDir) + (*in).DeepCopyInto(*out) + } + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = new(FeatureStoreServices) + (*in).DeepCopyInto(*out) + } + if in.AuthzConfig != nil { + in, out := &in.AuthzConfig, &out.AuthzConfig + *out = new(AuthzConfig) + (*in).DeepCopyInto(*out) + } + if in.CronJob != nil { + in, out := &in.CronJob, &out.CronJob + *out = new(FeastCronJob) + (*in).DeepCopyInto(*out) + } + if in.BatchEngine != nil { + in, out := &in.BatchEngine, &out.BatchEngine + *out = new(BatchEngineConfig) + (*in).DeepCopyInto(*out) + } + if in.DataQualityMonitoring != nil { + in, out := &in.DataQualityMonitoring, &out.DataQualityMonitoring + *out = new(DataQualityMonitoringConfig) + (*in).DeepCopyInto(*out) + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Materialization != nil { + in, out := &in.Materialization, &out.Materialization + *out = new(MaterializationConfig) + (*in).DeepCopyInto(*out) + } + if in.OpenLineage != nil { + in, out := &in.OpenLineage, &out.OpenLineage + *out = new(OpenLineageConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreSpec. +func (in *FeatureStoreSpec) DeepCopy() *FeatureStoreSpec { + if in == nil { + return nil + } + out := new(FeatureStoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureStoreStatus) DeepCopyInto(out *FeatureStoreStatus) { + *out = *in + in.Applied.DeepCopyInto(&out.Applied) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.ServiceHostnames = in.ServiceHostnames + if in.ScalingStatus != nil { + in, out := &in.ScalingStatus, &out.ScalingStatus + *out = new(ScalingStatus) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreStatus. +func (in *FeatureStoreStatus) DeepCopy() *FeatureStoreStatus { + if in == nil { + return nil + } + out := new(FeatureStoreStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitCloneOptions) DeepCopyInto(out *GitCloneOptions) { + *out = *in + if in.Configs != nil { + in, out := &in.Configs, &out.Configs + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = new([]corev1.EnvVar) + if **in != nil { + in, out := *in, *out + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.EnvFrom != nil { + in, out := &in.EnvFrom, &out.EnvFrom + *out = new([]corev1.EnvFromSource) + if **in != nil { + in, out := *in, *out + *out = make([]corev1.EnvFromSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitCloneOptions. +func (in *GitCloneOptions) DeepCopy() *GitCloneOptions { + if in == nil { + return nil + } + out := new(GitCloneOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobSpec) DeepCopyInto(out *JobSpec) { + *out = *in + if in.PodTemplateAnnotations != nil { + in, out := &in.PodTemplateAnnotations, &out.PodTemplateAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Parallelism != nil { + in, out := &in.Parallelism, &out.Parallelism + *out = new(int32) + **out = **in + } + if in.Completions != nil { + in, out := &in.Completions, &out.Completions + *out = new(int32) + **out = **in + } + if in.ActiveDeadlineSeconds != nil { + in, out := &in.ActiveDeadlineSeconds, &out.ActiveDeadlineSeconds + *out = new(int64) + **out = **in + } + if in.PodFailurePolicy != nil { + in, out := &in.PodFailurePolicy, &out.PodFailurePolicy + *out = new(batchv1.PodFailurePolicy) + (*in).DeepCopyInto(*out) + } + if in.BackoffLimit != nil { + in, out := &in.BackoffLimit, &out.BackoffLimit + *out = new(int32) + **out = **in + } + if in.BackoffLimitPerIndex != nil { + in, out := &in.BackoffLimitPerIndex, &out.BackoffLimitPerIndex + *out = new(int32) + **out = **in + } + if in.MaxFailedIndexes != nil { + in, out := &in.MaxFailedIndexes, &out.MaxFailedIndexes + *out = new(int32) + **out = **in + } + if in.TTLSecondsAfterFinished != nil { + in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished + *out = new(int32) + **out = **in + } + if in.CompletionMode != nil { + in, out := &in.CompletionMode, &out.CompletionMode + *out = new(batchv1.CompletionMode) + **out = **in + } + if in.Suspend != nil { + in, out := &in.Suspend, &out.Suspend + *out = new(bool) + **out = **in + } + if in.PodReplacementPolicy != nil { + in, out := &in.PodReplacementPolicy, &out.PodReplacementPolicy + *out = new(batchv1.PodReplacementPolicy) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobSpec. +func (in *JobSpec) DeepCopy() *JobSpec { + if in == nil { + return nil + } + out := new(JobSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesAuthz) DeepCopyInto(out *KubernetesAuthz) { + *out = *in + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesAuthz. +func (in *KubernetesAuthz) DeepCopy() *KubernetesAuthz { + if in == nil { + return nil + } + out := new(KubernetesAuthz) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalRegistryConfig) DeepCopyInto(out *LocalRegistryConfig) { + *out = *in + if in.Server != nil { + in, out := &in.Server, &out.Server + *out = new(RegistryServerConfigs) + (*in).DeepCopyInto(*out) + } + if in.Persistence != nil { + in, out := &in.Persistence, &out.Persistence + *out = new(RegistryPersistence) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalRegistryConfig. +func (in *LocalRegistryConfig) DeepCopy() *LocalRegistryConfig { + if in == nil { + return nil + } + out := new(LocalRegistryConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MaterializationConfig) DeepCopyInto(out *MaterializationConfig) { + *out = *in + if in.OnlineWriteBatchSize != nil { + in, out := &in.OnlineWriteBatchSize, &out.OnlineWriteBatchSize + *out = new(int32) + **out = **in + } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MaterializationConfig. +func (in *MaterializationConfig) DeepCopy() *MaterializationConfig { + if in == nil { + return nil + } + out := new(MaterializationConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *McpConfig) DeepCopyInto(out *McpConfig) { + *out = *in + if in.ServerName != nil { + in, out := &in.ServerName, &out.ServerName + *out = new(string) + **out = **in + } + if in.ServerVersion != nil { + in, out := &in.ServerVersion, &out.ServerVersion + *out = new(string) + **out = **in + } + if in.Transport != nil { + in, out := &in.Transport, &out.Transport + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new McpConfig. +func (in *McpConfig) DeepCopy() *McpConfig { + if in == nil { + return nil + } + out := new(McpConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OfflinePushBatchingConfig) DeepCopyInto(out *OfflinePushBatchingConfig) { + *out = *in + if in.BatchSize != nil { + in, out := &in.BatchSize, &out.BatchSize + *out = new(int32) + **out = **in + } + if in.BatchIntervalSeconds != nil { + in, out := &in.BatchIntervalSeconds, &out.BatchIntervalSeconds + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflinePushBatchingConfig. +func (in *OfflinePushBatchingConfig) DeepCopy() *OfflinePushBatchingConfig { + if in == nil { + return nil + } + out := new(OfflinePushBatchingConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OfflineStore) DeepCopyInto(out *OfflineStore) { + *out = *in + if in.Server != nil { + in, out := &in.Server, &out.Server + *out = new(ServerConfigs) + (*in).DeepCopyInto(*out) + } + if in.Persistence != nil { + in, out := &in.Persistence, &out.Persistence + *out = new(OfflineStorePersistence) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStore. +func (in *OfflineStore) DeepCopy() *OfflineStore { + if in == nil { + return nil + } + out := new(OfflineStore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OfflineStoreDBStorePersistence) DeepCopyInto(out *OfflineStoreDBStorePersistence) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStoreDBStorePersistence. +func (in *OfflineStoreDBStorePersistence) DeepCopy() *OfflineStoreDBStorePersistence { + if in == nil { + return nil + } + out := new(OfflineStoreDBStorePersistence) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OfflineStoreFilePersistence) DeepCopyInto(out *OfflineStoreFilePersistence) { + *out = *in + if in.PvcConfig != nil { + in, out := &in.PvcConfig, &out.PvcConfig + *out = new(PvcConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStoreFilePersistence. +func (in *OfflineStoreFilePersistence) DeepCopy() *OfflineStoreFilePersistence { + if in == nil { + return nil + } + out := new(OfflineStoreFilePersistence) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OfflineStorePersistence) DeepCopyInto(out *OfflineStorePersistence) { + *out = *in + if in.FilePersistence != nil { + in, out := &in.FilePersistence, &out.FilePersistence + *out = new(OfflineStoreFilePersistence) + (*in).DeepCopyInto(*out) + } + if in.DBPersistence != nil { + in, out := &in.DBPersistence, &out.DBPersistence + *out = new(OfflineStoreDBStorePersistence) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OfflineStorePersistence. +func (in *OfflineStorePersistence) DeepCopy() *OfflineStorePersistence { + if in == nil { + return nil + } + out := new(OfflineStorePersistence) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OidcAuthz) DeepCopyInto(out *OidcAuthz) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.TokenEnvVar != nil { + in, out := &in.TokenEnvVar, &out.TokenEnvVar + *out = new(string) + **out = **in + } + if in.VerifySSL != nil { + in, out := &in.VerifySSL, &out.VerifySSL + *out = new(bool) + **out = **in + } + if in.CACertConfigMap != nil { + in, out := &in.CACertConfigMap, &out.CACertConfigMap + *out = new(OidcCACertConfigMap) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OidcAuthz. +func (in *OidcAuthz) DeepCopy() *OidcAuthz { + if in == nil { + return nil + } + out := new(OidcAuthz) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OidcCACertConfigMap) DeepCopyInto(out *OidcCACertConfigMap) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OidcCACertConfigMap. +func (in *OidcCACertConfigMap) DeepCopy() *OidcCACertConfigMap { + if in == nil { + return nil + } + out := new(OidcCACertConfigMap) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OnlineStore) DeepCopyInto(out *OnlineStore) { + *out = *in + if in.Server != nil { + in, out := &in.Server, &out.Server + *out = new(ServerConfigs) + (*in).DeepCopyInto(*out) + } + if in.Persistence != nil { + in, out := &in.Persistence, &out.Persistence + *out = new(OnlineStorePersistence) + (*in).DeepCopyInto(*out) + } + if in.Serving != nil { + in, out := &in.Serving, &out.Serving + *out = new(ServingConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStore. +func (in *OnlineStore) DeepCopy() *OnlineStore { + if in == nil { + return nil + } + out := new(OnlineStore) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OnlineStoreDBStorePersistence) DeepCopyInto(out *OnlineStoreDBStorePersistence) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStoreDBStorePersistence. +func (in *OnlineStoreDBStorePersistence) DeepCopy() *OnlineStoreDBStorePersistence { + if in == nil { + return nil + } + out := new(OnlineStoreDBStorePersistence) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OnlineStoreFilePersistence) DeepCopyInto(out *OnlineStoreFilePersistence) { + *out = *in + if in.PvcConfig != nil { + in, out := &in.PvcConfig, &out.PvcConfig + *out = new(PvcConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStoreFilePersistence. +func (in *OnlineStoreFilePersistence) DeepCopy() *OnlineStoreFilePersistence { + if in == nil { + return nil + } + out := new(OnlineStoreFilePersistence) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OnlineStorePersistence) DeepCopyInto(out *OnlineStorePersistence) { + *out = *in + if in.FilePersistence != nil { + in, out := &in.FilePersistence, &out.FilePersistence + *out = new(OnlineStoreFilePersistence) + (*in).DeepCopyInto(*out) + } + if in.DBPersistence != nil { + in, out := &in.DBPersistence, &out.DBPersistence + *out = new(OnlineStoreDBStorePersistence) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnlineStorePersistence. +func (in *OnlineStorePersistence) DeepCopy() *OnlineStorePersistence { + if in == nil { + return nil + } + out := new(OnlineStorePersistence) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenLineageConfig) DeepCopyInto(out *OpenLineageConfig) { + *out = *in + if in.TransportType != nil { + in, out := &in.TransportType, &out.TransportType + *out = new(string) + **out = **in + } + if in.TransportUrl != nil { + in, out := &in.TransportUrl, &out.TransportUrl + *out = new(string) + **out = **in + } + if in.TransportEndpoint != nil { + in, out := &in.TransportEndpoint, &out.TransportEndpoint + *out = new(string) + **out = **in + } + if in.ApiKeySecretRef != nil { + in, out := &in.ApiKeySecretRef, &out.ApiKeySecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Consumer != nil { + in, out := &in.Consumer, &out.Consumer + *out = new(OpenLineageConsumerConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenLineageConfig. +func (in *OpenLineageConfig) DeepCopy() *OpenLineageConfig { + if in == nil { + return nil + } + out := new(OpenLineageConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenLineageConsumerConfig) DeepCopyInto(out *OpenLineageConsumerConfig) { + *out = *in + if in.StoreType != nil { + in, out := &in.StoreType, &out.StoreType + *out = new(string) + **out = **in + } + if in.ConnectionStringSecretRef != nil { + in, out := &in.ConnectionStringSecretRef, &out.ConnectionStringSecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.ApiKeySecretRef != nil { + in, out := &in.ApiKeySecretRef, &out.ApiKeySecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.NamespaceMapping != nil { + in, out := &in.NamespaceMapping, &out.NamespaceMapping + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenLineageConsumerConfig. +func (in *OpenLineageConsumerConfig) DeepCopy() *OpenLineageConsumerConfig { + if in == nil { + return nil + } + out := new(OpenLineageConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OptionalCtrConfigs) DeepCopyInto(out *OptionalCtrConfigs) { + *out = *in + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = new([]corev1.EnvVar) + if **in != nil { + in, out := *in, *out + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.EnvFrom != nil { + in, out := &in.EnvFrom, &out.EnvFrom + *out = new([]corev1.EnvFromSource) + if **in != nil { + in, out := *in, *out + *out = make([]corev1.EnvFromSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.ImagePullPolicy != nil { + in, out := &in.ImagePullPolicy, &out.ImagePullPolicy + *out = new(corev1.PullPolicy) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(corev1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = new(map[string]string) + if **in != nil { + in, out := *in, *out + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionalCtrConfigs. +func (in *OptionalCtrConfigs) DeepCopy() *OptionalCtrConfigs { + if in == nil { + return nil + } + out := new(OptionalCtrConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDBConfig) DeepCopyInto(out *PDBConfig) { + *out = *in + if in.MinAvailable != nil { + in, out := &in.MinAvailable, &out.MinAvailable + *out = new(intstr.IntOrString) + **out = **in + } + if in.MaxUnavailable != nil { + in, out := &in.MaxUnavailable, &out.MaxUnavailable + *out = new(intstr.IntOrString) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDBConfig. +func (in *PDBConfig) DeepCopy() *PDBConfig { + if in == nil { + return nil + } + out := new(PDBConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PvcConfig) DeepCopyInto(out *PvcConfig) { + *out = *in + if in.Ref != nil { + in, out := &in.Ref, &out.Ref + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.Create != nil { + in, out := &in.Create, &out.Create + *out = new(PvcCreate) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PvcConfig. +func (in *PvcConfig) DeepCopy() *PvcConfig { + if in == nil { + return nil + } + out := new(PvcConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PvcCreate) DeepCopyInto(out *PvcCreate) { + *out = *in + if in.AccessModes != nil { + in, out := &in.AccessModes, &out.AccessModes + *out = make([]corev1.PersistentVolumeAccessMode, len(*in)) + copy(*out, *in) + } + if in.StorageClassName != nil { + in, out := &in.StorageClassName, &out.StorageClassName + *out = new(string) + **out = **in + } + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PvcCreate. +func (in *PvcCreate) DeepCopy() *PvcCreate { + if in == nil { + return nil + } + out := new(PvcCreate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Registry) DeepCopyInto(out *Registry) { + *out = *in + if in.Local != nil { + in, out := &in.Local, &out.Local + *out = new(LocalRegistryConfig) + (*in).DeepCopyInto(*out) + } + if in.Remote != nil { + in, out := &in.Remote, &out.Remote + *out = new(RemoteRegistryConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Registry. +func (in *Registry) DeepCopy() *Registry { + if in == nil { + return nil + } + out := new(Registry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegistryDBStorePersistence) DeepCopyInto(out *RegistryDBStorePersistence) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryDBStorePersistence. +func (in *RegistryDBStorePersistence) DeepCopy() *RegistryDBStorePersistence { + if in == nil { + return nil + } + out := new(RegistryDBStorePersistence) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegistryFilePersistence) DeepCopyInto(out *RegistryFilePersistence) { + *out = *in + if in.PvcConfig != nil { + in, out := &in.PvcConfig, &out.PvcConfig + *out = new(PvcConfig) + (*in).DeepCopyInto(*out) + } + if in.S3AdditionalKwargs != nil { + in, out := &in.S3AdditionalKwargs, &out.S3AdditionalKwargs + *out = new(map[string]string) + if **in != nil { + in, out := *in, *out + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } + if in.CacheTTLSeconds != nil { + in, out := &in.CacheTTLSeconds, &out.CacheTTLSeconds + *out = new(int32) + **out = **in + } + if in.CacheMode != nil { + in, out := &in.CacheMode, &out.CacheMode + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryFilePersistence. +func (in *RegistryFilePersistence) DeepCopy() *RegistryFilePersistence { + if in == nil { + return nil + } + out := new(RegistryFilePersistence) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegistryPersistence) DeepCopyInto(out *RegistryPersistence) { + *out = *in + if in.FilePersistence != nil { + in, out := &in.FilePersistence, &out.FilePersistence + *out = new(RegistryFilePersistence) + (*in).DeepCopyInto(*out) + } + if in.DBPersistence != nil { + in, out := &in.DBPersistence, &out.DBPersistence + *out = new(RegistryDBStorePersistence) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryPersistence. +func (in *RegistryPersistence) DeepCopy() *RegistryPersistence { + if in == nil { + return nil + } + out := new(RegistryPersistence) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegistryServerConfigs) DeepCopyInto(out *RegistryServerConfigs) { + *out = *in + in.ServerConfigs.DeepCopyInto(&out.ServerConfigs) + if in.RestAPI != nil { + in, out := &in.RestAPI, &out.RestAPI + *out = new(bool) + **out = **in + } + if in.GRPC != nil { + in, out := &in.GRPC, &out.GRPC + *out = new(bool) + **out = **in + } + if in.Mcp != nil { + in, out := &in.Mcp, &out.Mcp + *out = new(McpConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryServerConfigs. +func (in *RegistryServerConfigs) DeepCopy() *RegistryServerConfigs { + if in == nil { + return nil + } + out := new(RegistryServerConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemoteRegistryConfig) DeepCopyInto(out *RemoteRegistryConfig) { + *out = *in + if in.Hostname != nil { + in, out := &in.Hostname, &out.Hostname + *out = new(string) + **out = **in + } + if in.FeastRef != nil { + in, out := &in.FeastRef, &out.FeastRef + *out = new(FeatureStoreRef) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TlsRemoteRegistryConfigs) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteRegistryConfig. +func (in *RemoteRegistryConfig) DeepCopy() *RemoteRegistryConfig { + if in == nil { + return nil + } + out := new(RemoteRegistryConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScalingConfig) DeepCopyInto(out *ScalingConfig) { + *out = *in + if in.Autoscaling != nil { + in, out := &in.Autoscaling, &out.Autoscaling + *out = new(AutoscalingConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalingConfig. +func (in *ScalingConfig) DeepCopy() *ScalingConfig { + if in == nil { + return nil + } + out := new(ScalingConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScalingStatus) DeepCopyInto(out *ScalingStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalingStatus. +func (in *ScalingStatus) DeepCopy() *ScalingStatus { + if in == nil { + return nil + } + out := new(ScalingStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretKeyNames) DeepCopyInto(out *SecretKeyNames) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyNames. +func (in *SecretKeyNames) DeepCopy() *SecretKeyNames { + if in == nil { + return nil + } + out := new(SecretKeyNames) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerConfigs) DeepCopyInto(out *ServerConfigs) { + *out = *in + in.ContainerConfigs.DeepCopyInto(&out.ContainerConfigs) + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TlsConfigs) + (*in).DeepCopyInto(*out) + } + if in.LogLevel != nil { + in, out := &in.LogLevel, &out.LogLevel + *out = new(string) + **out = **in + } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = new(bool) + **out = **in + } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.WorkerConfigs != nil { + in, out := &in.WorkerConfigs, &out.WorkerConfigs + *out = new(WorkerConfigs) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerConfigs. +func (in *ServerConfigs) DeepCopy() *ServerConfigs { + if in == nil { + return nil + } + out := new(ServerConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceHostnames) DeepCopyInto(out *ServiceHostnames) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceHostnames. +func (in *ServiceHostnames) DeepCopy() *ServiceHostnames { + if in == nil { + return nil + } + out := new(ServiceHostnames) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServingConfig) DeepCopyInto(out *ServingConfig) { + *out = *in + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = new(ServingMetricsConfig) + (*in).DeepCopyInto(*out) + } + if in.OfflinePushBatching != nil { + in, out := &in.OfflinePushBatching, &out.OfflinePushBatching + *out = new(OfflinePushBatchingConfig) + (*in).DeepCopyInto(*out) + } + if in.Mcp != nil { + in, out := &in.Mcp, &out.Mcp + *out = new(McpConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServingConfig. +func (in *ServingConfig) DeepCopy() *ServingConfig { + if in == nil { + return nil + } + out := new(ServingConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServingMetricsConfig) DeepCopyInto(out *ServingMetricsConfig) { + *out = *in + if in.Categories != nil { + in, out := &in.Categories, &out.Categories + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServingMetricsConfig. +func (in *ServingMetricsConfig) DeepCopy() *ServingMetricsConfig { + if in == nil { + return nil + } + out := new(ServingMetricsConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TlsConfigs) DeepCopyInto(out *TlsConfigs) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(corev1.LocalObjectReference) + **out = **in + } + out.SecretKeyNames = in.SecretKeyNames + if in.Disable != nil { + in, out := &in.Disable, &out.Disable + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TlsConfigs. +func (in *TlsConfigs) DeepCopy() *TlsConfigs { + if in == nil { + return nil + } + out := new(TlsConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TlsRemoteRegistryConfigs) DeepCopyInto(out *TlsRemoteRegistryConfigs) { + *out = *in + out.ConfigMapRef = in.ConfigMapRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TlsRemoteRegistryConfigs. +func (in *TlsRemoteRegistryConfigs) DeepCopy() *TlsRemoteRegistryConfigs { + if in == nil { + return nil + } + out := new(TlsRemoteRegistryConfigs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerConfigs) DeepCopyInto(out *WorkerConfigs) { + *out = *in + if in.Workers != nil { + in, out := &in.Workers, &out.Workers + *out = new(int32) + **out = **in + } + if in.WorkerConnections != nil { + in, out := &in.WorkerConnections, &out.WorkerConnections + *out = new(int32) + **out = **in + } + if in.MaxRequests != nil { + in, out := &in.MaxRequests, &out.MaxRequests + *out = new(int32) + **out = **in + } + if in.MaxRequestsJitter != nil { + in, out := &in.MaxRequestsJitter, &out.MaxRequestsJitter + *out = new(int32) + **out = **in + } + if in.KeepAliveTimeout != nil { + in, out := &in.KeepAliveTimeout, &out.KeepAliveTimeout + *out = new(int32) + **out = **in + } + if in.RegistryTTLSeconds != nil { + in, out := &in.RegistryTTLSeconds, &out.RegistryTTLSeconds + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerConfigs. +func (in *WorkerConfigs) DeepCopy() *WorkerConfigs { + if in == nil { + return nil + } + out := new(WorkerConfigs) + in.DeepCopyInto(out) + return out +} diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 243827a487c..8ccde377e77 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -78,10 +78,22 @@ type FeatureStoreSpec struct { } // FeastProjectDir defines how to create the feast project directory. -// +kubebuilder:validation:XValidation:rule="[has(self.git), has(self.init)].exists_one(c, c)",message="One selection required between init or git." +// +kubebuilder:validation:XValidation:rule="[has(self.git), has(self.init), has(self.packaged)].exists_one(c, c)",message="One selection required between init, git, or packaged." type FeastProjectDir struct { - Git *GitCloneOptions `json:"git,omitempty"` - Init *FeastInitOptions `json:"init,omitempty"` + Git *GitCloneOptions `json:"git,omitempty"` + Init *FeastInitOptions `json:"init,omitempty"` + Packaged *FeastPackagedOptions `json:"packaged,omitempty"` +} + +// FeastPackagedOptions describes a feature repository packaged in a feature server image. +// +kubebuilder:validation:XValidation:rule="self.featureRepoPath.startsWith('/') && self.featureRepoPath != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..')",message="FeatureRepoPath must be a canonical absolute, non-root path without dot segments or repeated separators." +type FeastPackagedOptions struct { + // Image containing the packaged feature repository. When set, this image is used by the + // repository initialization and feast apply containers and as the default service image. + // When omitted, the operator's configured feature server image is used. + Image string `json:"image,omitempty"` + // FeatureRepoPath is the canonical absolute path to the feature repository in the image. + FeatureRepoPath string `json:"featureRepoPath"` } // GitCloneOptions describes how a clone should be performed. @@ -105,7 +117,7 @@ type GitCloneOptions struct { type FeastInitOptions struct { Minimal bool `json:"minimal,omitempty"` // Template for the created project - // +kubebuilder:validation:Enum=local;gcp;aws;snowflake;spark;postgres;hbase;cassandra;hazelcast;ikv;couchbase;clickhouse + // +kubebuilder:validation:Enum=local;gcp;aws;snowflake;spark;postgres;hbase;cassandra;hazelcast;couchbase;clickhouse;milvus;ray;ray_rag;pytorch_nlp Template string `json:"template,omitempty"` } @@ -157,6 +169,11 @@ type FeastCronJob struct { // JobSpec describes how the job execution will look like. type JobSpec struct { + // PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + // metadata. This is separate from the CronJob-level annotations and must be + // set explicitly by users if they want annotations on the PodTemplate. + PodTemplateAnnotations map[string]string `json:"podTemplateAnnotations,omitempty"` + // Specifies the maximum desired number of pods the job should // run at any given time. The actual number of pods running in steady state will // be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), @@ -284,6 +301,8 @@ type FeatureStoreServices struct { SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` // Disable the 'feast repo initialization' initContainer DisableInitContainers bool `json:"disableInitContainers,omitempty"` + // Runs feast apply on pod start to populate the registry. Defaults to true. Ignored when DisableInitContainers is true. + RunFeastApplyOnInit *bool `json:"runFeastApplyOnInit,omitempty"` // Volumes specifies the volumes to mount in the FeatureStore deployment. A corresponding `VolumeMount` should be added to whichever feast service(s) require access to said volume(s). Volumes []corev1.Volume `json:"volumes,omitempty"` } @@ -366,7 +385,7 @@ type OnlineStoreFilePersistence struct { // OnlineStoreDBStorePersistence configures the DB store persistence for the online store service type OnlineStoreDBStorePersistence struct { // Type of the persistence type you want to use. - // +kubebuilder:validation:Enum=snowflake.online;redis;ikv;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore;hbase;elasticsearch;qdrant;couchbase.online;milvus + // +kubebuilder:validation:Enum=snowflake.online;redis;datastore;dynamodb;bigtable;postgres;cassandra;mysql;hazelcast;singlestore;hbase;elasticsearch;qdrant;couchbase.online;milvus;hybrid;mongodb;aerospike;scylladb Type string `json:"type"` // Data store parameters should be placed as-is from the "feature_store.yaml" under the secret key. "registry_type" & "type" fields should be removed. SecretRef corev1.LocalObjectReference `json:"secretRef"` @@ -377,7 +396,6 @@ type OnlineStoreDBStorePersistence struct { var ValidOnlineStoreDBStorePersistenceTypes = []string{ "snowflake.online", "redis", - "ikv", "datastore", "dynamodb", "bigtable", @@ -391,6 +409,10 @@ var ValidOnlineStoreDBStorePersistenceTypes = []string{ "qdrant", "couchbase.online", "milvus", + "hybrid", + "mongodb", + "aerospike", + "scylladb", } // LocalRegistryConfig configures the registry service @@ -416,6 +438,17 @@ type RegistryFilePersistence struct { Path string `json:"path,omitempty"` PvcConfig *PvcConfig `json:"pvc,omitempty"` S3AdditionalKwargs *map[string]string `json:"s3_additional_kwargs,omitempty"` + + // CacheTTLSeconds defines the TTL (in seconds) for the registry cache. + // +kubebuilder:validation:Minimum=0 + // +optional + CacheTTLSeconds *int32 `json:"cache_ttl_seconds,omitempty"` + + // CacheMode defines the registry cache update strategy. + // Allowed values are "sync" and "thread". + // +kubebuilder:validation:Enum=none;sync;thread + // +optional + CacheMode *string `json:"cache_mode,omitempty"` } // RegistryDBStorePersistence configures the DB store persistence for the registry service @@ -499,11 +532,51 @@ type ServerConfigs struct { // Allowed values: "debug", "info", "warning", "error", "critical". // +kubebuilder:validation:Enum=debug;info;warning;error;critical LogLevel *string `json:"logLevel,omitempty"` + // Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. + Metrics *bool `json:"metrics,omitempty"` // VolumeMounts defines the list of volumes that should be mounted into the feast container. // This allows attaching persistent storage, config files, secrets, or other resources // required by the Feast components. Ensure that each volume mount has a corresponding // volume definition in the Volumes field. VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` + // WorkerConfigs defines the worker configuration for the Feast server. + // These options are primarily used for production deployments to optimize performance. + WorkerConfigs *WorkerConfigs `json:"workerConfigs,omitempty"` +} + +// WorkerConfigs defines the worker configuration for Feast servers. +// These settings control gunicorn worker processes for production deployments. +type WorkerConfigs struct { + // Workers is the number of worker processes. Use -1 to auto-calculate based on CPU cores (2 * CPU + 1). + // Defaults to 1 if not specified. + // +kubebuilder:validation:Minimum=-1 + // +optional + Workers *int32 `json:"workers,omitempty"` + // WorkerConnections is the maximum number of simultaneous clients per worker process. + // Defaults to 1000. + // +kubebuilder:validation:Minimum=1 + // +optional + WorkerConnections *int32 `json:"workerConnections,omitempty"` + // MaxRequests is the maximum number of requests a worker will process before restarting. + // This helps prevent memory leaks. Defaults to 1000. + // +kubebuilder:validation:Minimum=0 + // +optional + MaxRequests *int32 `json:"maxRequests,omitempty"` + // MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + // thundering herd effect on worker restart. Defaults to 50. + // +kubebuilder:validation:Minimum=0 + // +optional + MaxRequestsJitter *int32 `json:"maxRequestsJitter,omitempty"` + // KeepAliveTimeout is the timeout for keep-alive connections in seconds. + // Defaults to 30. + // +kubebuilder:validation:Minimum=1 + // +optional + KeepAliveTimeout *int32 `json:"keepAliveTimeout,omitempty"` + // RegistryTTLSeconds is the number of seconds after which the registry is refreshed. + // Higher values reduce refresh overhead but increase staleness. Defaults to 60. + // +kubebuilder:validation:Minimum=0 + // +optional + RegistryTTLSeconds *int32 `json:"registryTTLSeconds,omitempty"` } // RegistryServerConfigs creates a registry server for the feast service, with specified container configurations. @@ -639,6 +712,7 @@ type ServiceHostnames struct { // +kubebuilder:resource:shortName=feast // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:deprecatedversion:warning="v1alpha1 is deprecated and will be removed in a future release. Please migrate to v1." // FeatureStore is the Schema for the featurestores API type FeatureStore struct { diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index 15c61cc86d6..17ae4841966 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -183,6 +183,21 @@ func (in *FeastInitOptions) DeepCopy() *FeastInitOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeastPackagedOptions) DeepCopyInto(out *FeastPackagedOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastPackagedOptions. +func (in *FeastPackagedOptions) DeepCopy() *FeastPackagedOptions { + if in == nil { + return nil + } + out := new(FeastPackagedOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FeastProjectDir) DeepCopyInto(out *FeastProjectDir) { *out = *in @@ -196,6 +211,11 @@ func (in *FeastProjectDir) DeepCopyInto(out *FeastProjectDir) { *out = new(FeastInitOptions) **out = **in } + if in.Packaged != nil { + in, out := &in.Packaged, &out.Packaged + *out = new(FeastPackagedOptions) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastProjectDir. @@ -315,6 +335,11 @@ func (in *FeatureStoreServices) DeepCopyInto(out *FeatureStoreServices) { *out = new(v1.PodSecurityContext) (*in).DeepCopyInto(*out) } + if in.RunFeastApplyOnInit != nil { + in, out := &in.RunFeastApplyOnInit, &out.RunFeastApplyOnInit + *out = new(bool) + **out = **in + } if in.Volumes != nil { in, out := &in.Volumes, &out.Volumes *out = make([]v1.Volume, len(*in)) @@ -440,6 +465,13 @@ func (in *GitCloneOptions) DeepCopy() *GitCloneOptions { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JobSpec) DeepCopyInto(out *JobSpec) { *out = *in + if in.PodTemplateAnnotations != nil { + in, out := &in.PodTemplateAnnotations, &out.PodTemplateAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.Parallelism != nil { in, out := &in.Parallelism, &out.Parallelism *out = new(int32) @@ -909,6 +941,16 @@ func (in *RegistryFilePersistence) DeepCopyInto(out *RegistryFilePersistence) { } } } + if in.CacheTTLSeconds != nil { + in, out := &in.CacheTTLSeconds, &out.CacheTTLSeconds + *out = new(int32) + **out = **in + } + if in.CacheMode != nil { + in, out := &in.CacheMode, &out.CacheMode + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryFilePersistence. @@ -1031,6 +1073,11 @@ func (in *ServerConfigs) DeepCopyInto(out *ServerConfigs) { *out = new(string) **out = **in } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = new(bool) + **out = **in + } if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]v1.VolumeMount, len(*in)) @@ -1038,6 +1085,11 @@ func (in *ServerConfigs) DeepCopyInto(out *ServerConfigs) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.WorkerConfigs != nil { + in, out := &in.WorkerConfigs, &out.WorkerConfigs + *out = new(WorkerConfigs) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerConfigs. @@ -1106,3 +1158,48 @@ func (in *TlsRemoteRegistryConfigs) DeepCopy() *TlsRemoteRegistryConfigs { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerConfigs) DeepCopyInto(out *WorkerConfigs) { + *out = *in + if in.Workers != nil { + in, out := &in.Workers, &out.Workers + *out = new(int32) + **out = **in + } + if in.WorkerConnections != nil { + in, out := &in.WorkerConnections, &out.WorkerConnections + *out = new(int32) + **out = **in + } + if in.MaxRequests != nil { + in, out := &in.MaxRequests, &out.MaxRequests + *out = new(int32) + **out = **in + } + if in.MaxRequestsJitter != nil { + in, out := &in.MaxRequestsJitter, &out.MaxRequestsJitter + *out = new(int32) + **out = **in + } + if in.KeepAliveTimeout != nil { + in, out := &in.KeepAliveTimeout, &out.KeepAliveTimeout + *out = new(int32) + **out = **in + } + if in.RegistryTTLSeconds != nil { + in, out := &in.RegistryTTLSeconds, &out.RegistryTTLSeconds + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerConfigs. +func (in *WorkerConfigs) DeepCopy() *WorkerConfigs { + if in == nil { + return nil + } + out := new(WorkerConfigs) + in.DeepCopyInto(out) + return out +} diff --git a/infra/feast-operator/bundle.Dockerfile b/infra/feast-operator/bundle.Dockerfile index 685b137b92a..eda73b6494e 100644 --- a/infra/feast-operator/bundle.Dockerfile +++ b/infra/feast-operator/bundle.Dockerfile @@ -6,7 +6,7 @@ LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ LABEL operators.operatorframework.io.bundle.package.v1=feast-operator LABEL operators.operatorframework.io.bundle.channels.v1=alpha -LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.38.0 +LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.41.0 LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 LABEL operators.operatorframework.io.metrics.project_layout=go.kubebuilder.io/v4 diff --git a/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml b/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml index 913517e198a..5749c2042b5 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator-controller-manager-metrics-service_v1_service.yaml @@ -14,6 +14,7 @@ spec: protocol: TCP targetPort: 8443 selector: + app.kubernetes.io/name: feast-operator control-plane: controller-manager status: loadBalancer: {} diff --git a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml index aff4d1f9840..b1f88ff62c3 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-editor-role_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -5,6 +5,8 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: feast-operator + rbac.authorization.k8s.io/aggregate-to-admin: "true" + rbac.authorization.k8s.io/aggregate-to-edit: "true" name: feast-operator-featurestore-editor-role rules: - apiGroups: diff --git a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml index bcf9699fc1a..c64d255eed7 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator-featurestore-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -5,6 +5,7 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: feast-operator + rbac.authorization.k8s.io/aggregate-to-view: "true" name: feast-operator-featurestore-viewer-role rules: - apiGroups: diff --git a/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml index 78205683183..19af99046e9 100644 --- a/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml +++ b/infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml @@ -5,7 +5,36 @@ metadata: alm-examples: |- [ { - "apiVersion": "feast.dev/v1alpha1", + "apiVersion": "feast.dev/v1", + "kind": "FeatureStore", + "metadata": { + "name": "sample-materialization-openlineage", + "namespace": "feast" + }, + "spec": { + "feastProject": "my_project", + "materialization": { + "onlineWriteBatchSize": 10000 + }, + "openlineage": { + "apiKeySecretRef": { + "name": "openlineage-secret" + }, + "enabled": true, + "extraConfig": { + "emit_on_apply": "true", + "emit_on_materialize": "true", + "namespace": "my-feast-project", + "producer": "feast-operator" + }, + "transportEndpoint": "api/v1/lineage", + "transportType": "http", + "transportUrl": "http://marquez.feast.svc.cluster.local:5000" + } + } + }, + { + "apiVersion": "feast.dev/v1", "kind": "FeatureStore", "metadata": { "name": "sample" @@ -15,7 +44,40 @@ metadata: } }, { - "apiVersion": "feast.dev/v1alpha1", + "apiVersion": "feast.dev/v1", + "kind": "FeatureStore", + "metadata": { + "name": "sample-mcp" + }, + "spec": { + "feastProject": "my_project", + "services": { + "onlineStore": { + "server": {}, + "serving": { + "mcp": { + "enabled": true, + "serverName": "feast-mcp-server", + "serverVersion": "1.0.0", + "transport": "sse" + } + } + }, + "registry": { + "local": { + "server": { + "mcp": { + "enabled": true + }, + "restAPI": true + } + } + } + } + } + }, + { + "apiVersion": "feast.dev/v1", "kind": "FeatureStore", "metadata": { "name": "sample-remote-servers" @@ -36,7 +98,42 @@ metadata: } }, { - "apiVersion": "feast.dev/v1alpha1", + "apiVersion": "feast.dev/v1", + "kind": "FeatureStore", + "metadata": { + "name": "sample-serving" + }, + "spec": { + "feastProject": "my_project", + "services": { + "onlineStore": { + "server": {}, + "serving": { + "metrics": { + "categories": { + "audit_logging": false, + "freshness": false, + "materialization": true, + "offline_features": true, + "online_features": true, + "push": true, + "request": true, + "resource": true + }, + "enabled": true + }, + "offlinePushBatching": { + "batchIntervalSeconds": 10, + "batchSize": 1000, + "enabled": true + } + } + } + } + } + }, + { + "apiVersion": "feast.dev/v1", "kind": "FeatureStore", "metadata": { "name": "sample-ui" @@ -50,15 +147,20 @@ metadata: } ] capabilities: Basic Install - createdAt: "2025-10-27T20:28:39Z" - operators.operatorframework.io/builder: operator-sdk-v1.38.0 + createdAt: "2026-07-20T13:27:58Z" + operators.operatorframework.io/builder: operator-sdk-v1.41.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 - name: feast-operator.v0.56.0 + name: feast-operator.v0.65.0 namespace: placeholder spec: apiservicedefinitions: {} customresourcedefinitions: owned: + - description: FeatureStore is the Schema for the featurestores API + displayName: Feature Store + kind: FeatureStore + name: featurestores.feast.dev + version: v1 - description: FeatureStore is the Schema for the featurestores API displayName: Feature Store kind: FeatureStore @@ -73,6 +175,63 @@ spec: spec: clusterPermissions: - rules: + - apiGroups: + - "" + resources: + - configmaps + - persistentvolumeclaims + - services + verbs: + - create + - delete + - deletecollection + - get + - list + - update + - watch + - apiGroups: + - "" + resources: + - namespaces + - secrets + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - deletecollection + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods/exec + verbs: + - create + - apiGroups: + - "" + resources: + - pods/log + verbs: + - get + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - update + - watch - apiGroups: - apps resources: @@ -91,9 +250,9 @@ spec: verbs: - create - apiGroups: - - batch + - autoscaling resources: - - cronjobs + - horizontalpodautoscalers verbs: - create - delete @@ -103,35 +262,25 @@ spec: - update - watch - apiGroups: - - "" + - batch resources: - - configmaps - - persistentvolumeclaims - - serviceaccounts - - services + - cronjobs verbs: - create - delete - get - list + - patch - update - watch - apiGroups: - - "" + - config.openshift.io resources: - - namespaces - - pods - - secrets + - apiservers verbs: - get - list - watch - - apiGroups: - - "" - resources: - - pods/exec - verbs: - - create - apiGroups: - feast.dev resources: @@ -158,6 +307,29 @@ spec: - get - patch - update + - apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - watch + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resources: @@ -184,6 +356,14 @@ spec: - list - update - watch + - apiGroups: + - sparkoperator.k8s.io + resources: + - sparkapplications + verbs: + - create + - delete + - get - apiGroups: - authentication.k8s.io resources: @@ -207,6 +387,7 @@ spec: replicas: 1 selector: matchLabels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager strategy: {} template: @@ -214,6 +395,7 @@ spec: annotations: kubectl.kubernetes.io/default-container: manager labels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager spec: containers: @@ -225,10 +407,13 @@ spec: - /manager env: - name: RELATED_IMAGE_FEATURE_SERVER - value: quay.io/feastdev/feature-server:0.56.0 + value: quay.io/feastdev/feature-server:0.65.0 - name: RELATED_IMAGE_CRON_JOB value: quay.io/openshift/origin-cli:4.17 - image: quay.io/feastdev/feast-operator:0.56.0 + - name: GOMEMLIMIT + value: 230MiB + - name: OIDC_ISSUER_URL + image: quay.io/feastdev/feast-operator:0.65.0 livenessProbe: httpGet: path: /healthz @@ -318,8 +503,8 @@ spec: name: Feast Community url: https://lf-aidata.atlassian.net/wiki/spaces/FEAST/ relatedImages: - - image: quay.io/feastdev/feature-server:0.56.0 + - image: quay.io/feastdev/feature-server:0.65.0 name: feature-server - image: quay.io/openshift/origin-cli:4.17 name: cron-job - version: 0.56.0 + version: 0.65.0 diff --git a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml index 9c0c09d141a..0ab08afef51 100644 --- a/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml +++ b/infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml @@ -2,7 +2,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 creationTimestamp: null name: featurestores.feast.dev spec: @@ -23,6 +23,12832 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date + name: v1 + schema: + openAPIV3Schema: + description: FeatureStore is the Schema for the featurestores API + properties: + apiVersion: + description: APIVersion defines the versioned schema of this representation + of an object. + type: string + kind: + description: Kind is a string value representing the REST resource this + object represents. + type: string + metadata: + type: object + spec: + description: FeatureStoreSpec defines the desired state of FeatureStore + properties: + authz: + description: AuthzConfig defines the authorization settings for the + deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed in the + same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM certificate. + Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string + secretRef: + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + batchEngine: + description: BatchEngineConfig defines the batch compute engine configuration. + properties: + configMapKey: + description: Key name in the ConfigMap. Defaults to "config" if + not specified. + type: string + configMapRef: + description: Reference to a ConfigMap containing the batch engine + configuration. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + cronJob: + description: FeastCronJob defines a CronJob to execute against a Feature + Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions of a + Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings for + the CronJob + properties: + commands: + description: Array of commands to be executed (in order) against + a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + type: object + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. Value + must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. + properties: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system... + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. + properties: + rules: + description: A list of pod failure policy rules. The rules + are evaluated in order. + items: + description: PodFailurePolicyRule describes how a pod + failure is handled when the requirements are met. + properties: + action: + description: Specifies the action taken on a pod + failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the container + exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod condition + status. + type: string + type: + description: Specifies the required Pod condition + type. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer + type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string + type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. + properties: + git: + description: GitCloneOptions describes how a clone should be performed. + properties: + configs: + additionalProperties: + type: string + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the feature + repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - couchbase + - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') + type: object + x-kubernetes-validations: + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints for + the FeatureStore deployment. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. + properties: + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be unavailable + during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or "RollingUpdate". + Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures the + file-based persistence for the offline store service + properties: + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string + type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + - oracle + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a remote offline server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the online store service + properties: + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service + properties: + file: + description: OnlineStoreFilePersistence configures the + file-based persistence for the online store service + properties: + path: + type: string + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: object + x-kubernetes-validations: + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : true' + - message: PVC path must be a file name only, with no + slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: Online store does not support S3 or GS buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.online + - redis + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + - mongodb + - aerospike + - scylladb + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + serving: + description: Serving configures the Feast feature_server section + written into feature_store.yaml for the online serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. Defaults + to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults to + "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or disables + individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to the + offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to the + offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline write + batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied to the + Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true + type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' + registry: + description: Registry configures the registry service. One selection + is required. Local is the default setting. + properties: + local: + description: LocalRegistryConfig configures the registry service + properties: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures the + file-based persistence for the registry service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-validations: + - message: Registry files must use absolute paths + or be S3 ('s3://') or GS ('gs://') object store + URIs. + rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') + || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) + : true' + - message: PVC path must be a file name only, with + no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: PVC persistence does not support S3 or + GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available only + for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be true + rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` CR + in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for the + client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the FeatureStore + deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: scaleUp is scaling policy for scaling + Up. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + description: MaxReplicas is the upper limit for the number + of replicas. Required. + format: int32 + minimum: 1 + type: integer + metrics: + description: Metrics contains the specifications for which + to use to calculate the desired replica count. + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at... + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes... + properties: + container: + description: container is the name of the container + in the pods of the scaling target + type: string + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the descriptions + of a object,such as kind,name apiVersion + properties: + apiVersion: + description: apiVersion is the API version + of the referent + type: string + kind: + description: 'kind is the kind of the referent; + More info: https://git.k8s.' + type: string + name: + description: 'name is the name of the referent; + More info: https://kubernetes.' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example,... + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing... + properties: + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: type is the type of metric source. + type: string + required: + - type + type: object + type: array + minReplicas: + description: MinReplicas is the lower limit for the number + of replicas. Defaults to 1. + format: int32 + minimum: 1 + type: integer + required: + - maxReplicas + type: object + type: object + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to use + by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile loaded + on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor profile + will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies to + all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as a non-root + user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string + seLinuxOptions: + description: The SELinux context to be applied to all containers. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile defined + in a file on the node should be used. + type: string + type: + description: type indicates which kind of seccomp profile + will be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string + sysctls: + description: Sysctls hold a list of namespaced sysctls used + for the pod. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + description: The Windows specific settings applied to all + containers. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container should + be run as a 'Host Process' container. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. + type: string + type: object + type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are spread + across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of eligible + domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + ui: + description: Creates a UI server container + properties: + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. useful + in an openshift cluster, for example, where TLS is configured + by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key names + for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where the + TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes that + should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from which + the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * CPU + + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + volumes: + description: Volumes specifies the volumes to mount in the FeatureStore + deployment. + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to... + properties: + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes. + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes. + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk mount + on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk in + the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in the + blob storage + type: string + fsType: + default: ext4 + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage...' + type: string + readOnly: + default: false + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the host + that shares a pod's lifetime. + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s. + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile is + the path to key ring for User, default is /etc/ceph/user.' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is reference + to the authentication secret for User, default is + empty.' + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s. + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + Deprecated: Cinder is deprecated. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits used + to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used + to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers. + properties: + driver: + description: driver is the name of the CSI driver that + handles this volume. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", "ntfs". + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about the + pod that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created + files by default.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the + pod: only annotations, labels, name, namespace + and uid are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must not + be absolute or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes. + properties: + medium: + description: medium represents what type of storage + medium should back this directory. + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: sizeLimit is the total amount of local + storage required for this EmptyDir volume. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: ephemeral represents a volume that is handled + by a cluster storage driver. + properties: + volumeClaimTemplate: + description: Will be used to create a stand-alone PVC + to provision the volume. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. + type: object + spec: + description: The specification for the PersistentVolumeClaim. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s. + properties: + apiGroup: + description: APIGroup is the group for the + resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. + properties: + apiGroup: + description: APIGroup is the group for the + resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking. + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum + resources the volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may be + used to set the VolumeAttributesClass used + by this claim. + type: string + volumeMode: + description: volumeMode defines what type of + volume is required by the claim. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource that + is attached to a kubelet's host machine and then exposed + to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to use + for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as... + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the... + properties: + fsType: + description: fsType is filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes. + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes. + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + Deprecated: GitRepo is deprecated. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. + properties: + endpoints: + description: endpoints is the endpoint name that details + Glusterfs topology. + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + default: default + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal List. + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: targetPortal is iSCSI Target Portal. + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes. + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes. + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes. + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + properties: + claimName: + description: claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + pdID: + description: pdID is the ID that identifies Photon Controller + persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources secrets, + configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used to set + permissions on created files by default. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: ClusterTrustBundle allows a pod to + access the `.spec. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. + type: string + required: + - path + type: object + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode + bits used to set permissions on this + file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name, namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file to + be created. Must not be absolute or + contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object + secret: + description: secret information about the secret + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode + bits used to set permissions on this + file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: audience is the intended audience + of the token. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + description: quobyte represents a Quobyte mount on the host + that shares a pod's lifetime. + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple... + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set... + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. + properties: + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + default: /etc/ceph/keyring + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s. + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s. + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + default: xfs + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + default: ThinProvisioned + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes. + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits used + to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used + to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes. + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope of + the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine. + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + x-kubernetes-validations: + - message: replicas > 1 and services.scaling.autoscaling are mutually + exclusive. + rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling) + - message: Scaling requires DB-backed persistence for the online store. + Configure services.onlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) + && has(self.services.onlineStore.persistence.store)) + - message: Scaling requires DB-backed persistence for the offline store. + Configure services.offlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (!has(self.services) + || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) + && has(self.services.offlineStore.persistence.store))) + - message: Scaling requires DB-backed or remote registry. Configure registry.local.persistence.store + or use a remote registry when using replicas > 1 or autoscaling. S3/GCS-backed + registry is also allowed. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.registry) && (has(self.services.registry.remote) + || (has(self.services.registry.local) && has(self.services.registry.local.persistence) + && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) + && has(self.services.registry.local.persistence.file.path) && (self.services.registry.local.persistence.file.path.startsWith('s3://') + || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + properties: + applied: + description: Shows the currently applied feast configuration, including + any pertinent defaults + properties: + authz: + description: AuthzConfig defines the authorization settings for + the deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed + in the same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM + certificate. Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string + secretRef: + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + batchEngine: + description: BatchEngineConfig defines the batch compute engine + configuration. + properties: + configMapKey: + description: Key name in the ConfigMap. Defaults to "config" + if not specified. + type: string + configMapRef: + description: Reference to a ConfigMap containing the batch + engine configuration. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + type: object + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. + properties: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system... + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. + properties: + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer + type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string + type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. + properties: + git: + description: GitCloneOptions describes how a clone should + be performed. + properties: + configs: + additionalProperties: + type: string + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast + init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - couchbase + - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') + type: object + x-kubernetes-validations: + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints + for the FeatureStore deployment. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in the + range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, + etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. + properties: + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be + unavailable during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or + "RollingUpdate". Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures + the file-based persistence for the offline store + service + properties: + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string + type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + - oracle + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a remote offline server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the online store service + properties: + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service + properties: + file: + description: OnlineStoreFilePersistence configures + the file-based persistence for the online store + service + properties: + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: object + x-kubernetes-validations: + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : true' + - message: PVC path must be a file name only, with + no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: Online store does not support S3 or GS + buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - snowflake.online + - redis + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + - mongodb + - aerospike + - scylladb + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + serving: + description: Serving configures the Feast feature_server + section written into feature_store.yaml for the online + serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is + "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or + disables individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to + the offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to + the offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline + write batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied + to the Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true + type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' + registry: + description: Registry configures the registry service. One + selection is required. Local is the default setting. + properties: + local: + description: LocalRegistryConfig configures the registry + service + properties: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures + the file-based persistence for the registry + service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL + (in seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings + for a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new + PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to + ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the + storage resource requirements for + a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes + the minimum amount of compute + resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the + name of an existing StorageClass + to which this persistent volume + belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing + field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between + ref and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' + and must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-validations: + - message: Registry files must use absolute paths + or be S3 ('s3://') or GS ('gs://') object + store URIs. + rule: '(!has(self.pvc) && has(self.path)) ? + (self.path.startsWith(''/'') || self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: PVC path must be a file name only, + with no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: PVC persistence does not support S3 + or GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available + only for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store + "type" is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type + you want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or + store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment + variable present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment + variable's value. Cannot be used if value + is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the + ConfigMap or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the + pod: supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret + in the pod's namespace + properties: + key: + description: The key of the secret + to select from. Must be a valid + secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the + Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to + the name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for + if/when to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the + compute resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one + entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS + for a feast service. + properties: + disable: + description: will disable TLS for the feast + service. useful in an openshift cluster, + for example, where TLS is configured by + default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret + where the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` + is false.' + rule: '(!has(self.disable) || !self.disable) + ? has(self.secretRef) : true' + volumeMounts: + description: VolumeMounts defines the list of + volumes that should be mounted into the feast + container. + items: + description: VolumeMount describes a mounting + of a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of + a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should + be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker + configuration for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker + processes. Use -1 to auto-calculate based + on CPU cores (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be + true + rule: self.restAPI == true || self.grpc == true + || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` + CR in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for + the client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap + where the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, + c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the + FeatureStore deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential + scaling polices which can be used during + scaling. + items: + description: HPAScalingPolicy is a single + policy which must hold true for a specified + past interval. + properties: + periodSeconds: + description: periodSeconds specifies + the window of time for which the policy + should hold true. + format: int32 + type: integer + type: + description: type is used to specify + the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: scaleUp is scaling policy for scaling + Up. + properties: + policies: + description: policies is a list of potential + scaling polices which can be used during + scaling. + items: + description: HPAScalingPolicy is a single + policy which must hold true for a specified + past interval. + properties: + periodSeconds: + description: periodSeconds specifies + the window of time for which the policy + should hold true. + format: int32 + type: integer + type: + description: type is used to specify + the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + description: MaxReplicas is the upper limit for the + number of replicas. Required. + format: int32 + minimum: 1 + type: integer + metrics: + description: Metrics contains the specifications for + which to use to calculate the desired replica count. + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at... + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes... + properties: + container: + description: container is the name of the + container in the pods of the scaling target + type: string + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. + properties: + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the + descriptions of a object,such as kind,name + apiVersion + properties: + apiVersion: + description: apiVersion is the API version + of the referent + type: string + kind: + description: 'kind is the kind of the + referent; More info: https://git.k8s.' + type: string + name: + description: 'name is the name of the + referent; More info: https://kubernetes.' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example,... + properties: + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing... + properties: + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: type is the type of metric source. + type: string + required: + - type + type: object + type: array + minReplicas: + description: MinReplicas is the lower limit for the + number of replicas. Defaults to 1. + format: int32 + minimum: 1 + type: integer + required: + - maxReplicas + type: object + type: object + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to + use by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile + loaded on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor + profile will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies + to all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as + a non-root user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string + seLinuxOptions: + description: The SELinux context to be applied to all + containers. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. + type: string + type: + description: type indicates which kind of seccomp + profile will be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string + sysctls: + description: Sysctls hold a list of namespaced sysctls + used for the pod. + items: + description: Sysctl defines a kernel parameter to be + set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + description: The Windows specific settings applied to + all containers. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of + the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. + type: string + type: object + type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are + spread across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + ui: + description: Creates a UI server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + volumes: + description: Volumes specifies the volumes to mount in the + FeatureStore deployment. + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to... + properties: + fsType: + description: fsType is the filesystem type of the + volume that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes. + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes. + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + default: ext4 + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage...' + type: string + readOnly: + default: false + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime. + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s. + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile + is the path to key ring for User, default is /etc/ceph/user.' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is + reference to the authentication secret for User, + default is empty.' + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s. + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + Deprecated: Cinder is deprecated. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits + used to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers. + properties: + driver: + description: driver is the name of the CSI driver + that handles this volume. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", + "ntfs". + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created + files by default.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name, + namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes. + properties: + medium: + description: medium represents what type of storage + medium should back this directory. + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: sizeLimit is the total amount of local + storage required for this EmptyDir volume. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: ephemeral represents a volume that is handled + by a cluster storage driver. + properties: + volumeClaimTemplate: + description: Will be used to create a stand-alone + PVC to provision the volume. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. + type: object + spec: + description: The specification for the PersistentVolumeClaim. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s. + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking. + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum + resources the volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may + be used to set the VolumeAttributesClass + used by this claim. + type: string + volumeMode: + description: volumeMode defines what type + of volume is required by the claim. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as... + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the... + properties: + fsType: + description: fsType is filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes. + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes. + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + Deprecated: GitRepo is deprecated. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. + properties: + endpoints: + description: endpoints is the endpoint name that + details Glusterfs topology. + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: fsType is the filesystem type of the + volume that you want to mount. + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + default: default + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal + List. + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: targetPortal is iSCSI Target Portal. + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes. + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes. + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes. + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + properties: + claimName: + description: claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used + to set permissions on created files by default. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: ClusterTrustBundle allows a pod + to access the `.spec. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name, namespace and uid + are supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: audience is the intended + audience of the token. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime. + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple... + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set... + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. + properties: + fsType: + description: fsType is the filesystem type of the + volume that you want to mount. + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + default: /etc/ceph/keyring + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s. + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s. + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + default: xfs + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + default: ThinProvisioned + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes. + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits + used to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes. + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope + of the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + x-kubernetes-validations: + - message: replicas > 1 and services.scaling.autoscaling are mutually + exclusive. + rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling) + - message: Scaling requires DB-backed persistence for the online store. + Configure services.onlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) + && has(self.services.onlineStore.persistence.store)) + - message: Scaling requires DB-backed persistence for the offline + store. Configure services.offlineStore.persistence.store when + using replicas > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (!has(self.services) + || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) + && has(self.services.offlineStore.persistence.store))) + - message: Scaling requires DB-backed or remote registry. Configure + registry.local.persistence.store or use a remote registry when + using replicas > 1 or autoscaling. S3/GCS-backed registry is also + allowed. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.registry) && (has(self.services.registry.remote) + || (has(self.services.registry.local) && has(self.services.registry.local.persistence) + && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) + && has(self.services.registry.local.persistence.file.path) && + (self.services.registry.local.persistence.file.path.startsWith('s3://') + || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) + clientConfigMap: + description: ConfigMap in this namespace containing a client `feature_store.yaml` + for this feast deployment + type: string + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if . + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + cronJob: + description: CronJob in this namespace for this feast deployment + type: string + feastVersion: + type: string + phase: + type: string + replicas: + description: Replicas is the current number of ready pod replicas + (used by the scale sub-resource). + format: int32 + type: integer + scalingStatus: + description: ScalingStatus reports the current scaling state of the + FeatureStore deployment. + properties: + currentReplicas: + description: CurrentReplicas is the current number of pod replicas. + format: int32 + type: integer + desiredReplicas: + description: DesiredReplicas is the desired number of pod replicas. + format: int32 + type: integer + type: object + selector: + description: Selector is the label selector for pods managed by the + FeatureStore deployment (used by the scale sub-resource). + type: string + serviceHostnames: + description: ServiceHostnames defines the service hostnames in the + format of :, e.g. example.svc.cluster.local:80 + properties: + offlineStore: + type: string + onlineStore: + type: string + registry: + type: string + registryRest: + type: string + ui: + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: v1alpha1 is deprecated and will be removed in a future release. + Please migrate to v1. name: v1alpha1 schema: openAPIV3Schema: @@ -87,6 +12913,11 @@ spec: description: FeastCronJob defines a CronJob to execute against a Feature Store deployment. properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object concurrencyPolicy: description: Specifies how to treat concurrent executions of a Job. @@ -107,14 +12938,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -158,6 +12989,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -213,7 +13073,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -232,8 +13092,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -279,6 +13139,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -320,7 +13184,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -414,7 +13278,6 @@ spec: type. type: string required: - - status - type type: object type: array @@ -431,6 +13294,13 @@ spec: description: podReplacementPolicy specifies when to create replacement Pods. type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object suspend: description: suspend specifies whether the Job controller should create Pods or not. @@ -490,14 +13360,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -541,6 +13411,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -596,7 +13495,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -615,8 +13514,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -670,15 +13569,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp type: string type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -865,14 +13789,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -916,6 +13840,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -972,7 +13926,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -991,8 +13945,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -1029,6 +13983,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -1050,6 +14008,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -1161,6 +14123,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object onlineStore: @@ -1292,7 +14300,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -1306,6 +14313,10 @@ spec: - qdrant - couchbase.online - milvus + - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -1324,14 +14335,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -1375,6 +14386,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1431,7 +14472,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1450,8 +14491,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -1488,6 +14529,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -1509,6 +14554,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -1620,6 +14669,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object registry: @@ -1637,6 +14732,21 @@ spec: description: RegistryFilePersistence configures the file-based persistence for the registry service properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer path: type: string pvc: @@ -1787,14 +14897,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -1839,6 +14949,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1897,7 +15037,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1916,8 +15056,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -1958,6 +15098,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -1979,6 +15123,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -2094,6 +15242,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object x-kubernetes-validations: - message: At least one of restAPI or grpc must be true @@ -2154,6 +15348,10 @@ spec: x-kubernetes-validations: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -2199,6 +15397,10 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. properties: @@ -2237,13 +15439,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -2294,14 +15501,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -2345,6 +15552,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -2400,7 +15636,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -2419,8 +15655,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -2457,6 +15693,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -2477,6 +15717,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -2588,6 +15832,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * CPU + + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object volumes: description: Volumes specifies the volumes to mount in the FeatureStore @@ -2599,7 +15889,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the volume @@ -2641,6 +15931,7 @@ spec: blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -2649,9 +15940,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -2682,7 +15974,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: monitors: description: |- @@ -2730,7 +16022,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -2776,7 +16068,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -2816,7 +16108,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver that @@ -2828,7 +16120,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -2890,7 +16182,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -3139,9 +16431,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide identifiers - (wwids)\nEither wwids or combination of targetWWNs - and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -3176,7 +16468,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -3197,7 +16489,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -3207,7 +16499,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -3236,7 +16528,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -3254,14 +16546,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that details + Glusterfs topology. type: string path: description: |- @@ -3296,6 +16586,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -3321,6 +16627,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -3411,7 +16718,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -3428,7 +16735,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -3458,10 +16765,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected along - with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod to @@ -3541,7 +16851,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -3612,7 +16922,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -3654,6 +16964,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -3661,7 +17017,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -3727,7 +17083,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: group: description: |- @@ -3742,12 +17098,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -3763,9 +17119,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the volume @@ -3777,6 +17132,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -3791,6 +17147,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -3818,6 +17175,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -3832,6 +17190,7 @@ spec: attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -3869,6 +17228,7 @@ spec: with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -3903,7 +17263,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -3978,7 +17338,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine + and mounted on kubelets host machine. properties: fsType: description: |- @@ -4063,6 +17423,11 @@ spec: description: FeastCronJob defines a CronJob to execute against a Feature Store deployment. properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object concurrencyPolicy: description: Specifies how to treat concurrent executions of a Job. @@ -4083,14 +17448,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4134,6 +17499,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4190,7 +17585,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4209,8 +17604,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -4257,6 +17652,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -4298,7 +17697,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -4393,7 +17792,6 @@ spec: condition type. type: string required: - - status - type type: object type: array @@ -4410,6 +17808,13 @@ spec: description: podReplacementPolicy specifies when to create replacement Pods. type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object suspend: description: suspend specifies whether the Job controller should create Pods or not. @@ -4471,14 +17876,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4522,6 +17927,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4578,7 +18013,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4597,8 +18032,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -4653,15 +18088,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -4851,14 +18311,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4903,6 +18363,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4961,7 +18451,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4980,8 +18470,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5018,6 +18508,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -5039,6 +18533,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -5151,6 +18649,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object onlineStore: @@ -5285,7 +18829,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -5299,6 +18842,10 @@ spec: - qdrant - couchbase.online - milvus + - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -5318,14 +18865,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -5370,6 +18917,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5428,7 +19005,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5447,8 +19024,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5485,6 +19062,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -5506,6 +19087,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -5618,6 +19203,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object registry: @@ -5637,6 +19268,21 @@ spec: the file-based persistence for the registry service properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL + (in seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer path: type: string pvc: @@ -5793,14 +19439,14 @@ spec: variable present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment @@ -5846,6 +19492,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5905,7 +19581,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5924,9 +19600,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be - a C_IDENTIFIER. + description: Optional text to prepend to + the name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5967,6 +19642,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -5988,6 +19667,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -6107,6 +19790,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker + configuration for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker + processes. Use -1 to auto-calculate based + on CPU cores (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object x-kubernetes-validations: - message: At least one of restAPI or grpc must be @@ -6170,6 +19899,11 @@ spec: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -6215,6 +19949,11 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. @@ -6254,13 +19993,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -6312,14 +20056,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -6363,6 +20107,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -6419,7 +20193,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -6438,8 +20212,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -6476,6 +20250,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -6497,6 +20275,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -6608,6 +20390,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object volumes: description: Volumes specifies the volumes to mount in the @@ -6619,7 +20447,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the @@ -6661,6 +20489,7 @@ spec: the blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -6669,9 +20498,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -6702,7 +20532,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: monitors: description: |- @@ -6751,7 +20581,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -6797,7 +20627,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -6837,7 +20667,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver @@ -6850,7 +20680,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -6914,7 +20744,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -7166,9 +20996,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide - identifiers (wwids)\nEither wwids or combination - of targetWWNs and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -7203,7 +21033,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -7224,7 +21054,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -7234,7 +21064,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -7263,7 +21093,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -7281,14 +21111,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that + details Glusterfs topology. type: string path: description: |- @@ -7323,6 +21151,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -7348,6 +21192,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -7439,7 +21284,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -7456,7 +21301,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -7486,10 +21331,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected - along with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod @@ -7570,7 +21418,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -7643,7 +21491,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -7685,6 +21533,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -7692,7 +21586,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -7758,7 +21652,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: group: description: |- @@ -7773,12 +21667,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -7794,9 +21688,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the @@ -7808,6 +21701,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -7822,6 +21716,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -7849,6 +21744,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -7863,6 +21759,7 @@ spec: volume attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -7900,6 +21797,7 @@ spec: communication with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -7934,7 +21832,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -8009,7 +21907,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -8083,10 +21981,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition. + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string @@ -8123,7 +22018,7 @@ spec: type: object type: object served: true - storage: true + storage: false subresources: status: {} status: diff --git a/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml b/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml new file mode 100644 index 00000000000..40483cc0c43 --- /dev/null +++ b/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: openlineage-secret +stringData: + api_key: your-marquez-api-key diff --git a/infra/feast-operator/bundle/metadata/annotations.yaml b/infra/feast-operator/bundle/metadata/annotations.yaml index 5e280a43e24..2c6d3383343 100644 --- a/infra/feast-operator/bundle/metadata/annotations.yaml +++ b/infra/feast-operator/bundle/metadata/annotations.yaml @@ -5,7 +5,7 @@ annotations: operators.operatorframework.io.bundle.metadata.v1: metadata/ operators.operatorframework.io.bundle.package.v1: feast-operator operators.operatorframework.io.bundle.channels.v1: alpha - operators.operatorframework.io.metrics.builder: operator-sdk-v1.38.0 + operators.operatorframework.io.metrics.builder: operator-sdk-v1.41.0 operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v4 diff --git a/infra/feast-operator/cmd/main.go b/infra/feast-operator/cmd/main.go index 82f0fd2eeca..5d2bbece7dc 100644 --- a/infra/feast-operator/cmd/main.go +++ b/infra/feast-operator/cmd/main.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "context" "crypto/tls" "flag" "os" @@ -25,11 +26,20 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + configv1 "github.com/openshift/api/config/v1" + tlspkg "github.com/openshift/controller-runtime-common/pkg/tls" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -37,10 +47,12 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" routev1 "github.com/openshift/api/route/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller" + feastmetrics "github.com/feast-dev/feast/infra/feast-operator/internal/controller/metrics" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" // +kubebuilder:scaffold:imports ) @@ -52,18 +64,43 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(configv1.Install(scheme)) utilruntime.Must(routev1.AddToScheme(scheme)) utilruntime.Must(feastdevv1alpha1.AddToScheme(scheme)) + utilruntime.Must(feastdevv1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } +func newCacheOptions() cache.Options { + managedBySelector := labels.SelectorFromSet(labels.Set{ + services.ManagedByLabelKey: services.ManagedByLabelValue, + }) + managedByFilter := cache.ByObject{Label: managedBySelector} + + return cache.Options{ + DefaultTransform: cache.TransformStripManagedFields(), + ByObject: map[client.Object]cache.ByObject{ + &corev1.ConfigMap{}: managedByFilter, + &appsv1.Deployment{}: managedByFilter, + &corev1.Service{}: managedByFilter, + &corev1.ServiceAccount{}: managedByFilter, + &corev1.PersistentVolumeClaim{}: managedByFilter, + &rbacv1.RoleBinding{}: managedByFilter, + &rbacv1.Role{}: managedByFilter, + &batchv1.CronJob{}: managedByFilter, + &autoscalingv2.HorizontalPodAutoscaler{}: managedByFilter, + &policyv1.PodDisruptionBudget{}: managedByFilter, + }, + } +} + func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string var secureMetrics bool - var enableHTTP2 bool - var tlsOpts []func(*tls.Config) + var featureStoreMetrics bool + tlsOpts := make([]func(*tls.Config), 0, 2) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -72,8 +109,9 @@ func main() { "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - flag.BoolVar(&enableHTTP2, "enable-http2", false, - "If set, HTTP/2 will be enabled for the metrics and webhook servers") + flag.BoolVar(&featureStoreMetrics, "feature-store-metrics", true, + "Enable Prometheus gauges exposing online/offline store and registry configuration per FeatureStore. "+ + "Disable with --feature-store-metrics=false.") opts := zap.Options{ Development: true, } @@ -82,20 +120,20 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - // if the enable-http2 flag is false (the default), http/2 should be disabled - // due to its vulnerabilities. More specifically, disabling http/2 will - // prevent from being vulnerable to the HTTP/2 Stream Cancellation and - // Rapid Reset CVEs. For more information see: - // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 - // - https://github.com/advisories/GHSA-4374-p667-p6c8 - disableHTTP2 := func(c *tls.Config) { - setupLog.Info("disabling http/2") - c.NextProtos = []string{"http/1.1"} + // Fetch cluster TLS profile from apiservers.config.openshift.io/cluster + cfg := ctrl.GetConfigOrDie() + bootstrapClient, err := client.New(cfg, client.Options{Scheme: scheme}) + if err != nil { + setupLog.Error(err, "unable to create bootstrap client for TLS profile fetch") + os.Exit(1) } - if !enableHTTP2 { - tlsOpts = append(tlsOpts, disableHTTP2) + tlsResult, err := bootstrapTLS(context.Background(), bootstrapClient) + if err != nil { + setupLog.Error(err, "TLS bootstrap failed") + os.Exit(1) } + tlsOpts = append(tlsOpts, tlsResult.TLSOpts...) webhookServer := webhook.NewServer(webhook.Options{ TLSOpts: tlsOpts, @@ -103,7 +141,7 @@ func main() { // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // More info: - // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/server + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server // - https://book.kubebuilder.io/reference/metrics.html metricsServerOptions := metricsserver.Options{ BindAddress: metricsAddr, @@ -121,11 +159,11 @@ func main() { // FilterProvider is used to protect the metrics endpoint with authn/authz. // These configurations ensure that only authorized users and service accounts // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: - // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/filters#WithAuthenticationAndAuthorization + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/filters#WithAuthenticationAndAuthorization metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, @@ -143,11 +181,26 @@ func main() { // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, + Cache: newCacheOptions(), Client: client.Options{ Cache: &client.CacheOptions{ + // Bypass the label-filtered informer cache for all reads so that + // pre-existing resources without the managed-by label are still + // visible to the reconciler. The ByObject cache filter above still + // restricts the watch to managed-by-labeled objects, limiting + // memory usage while avoiding upgrade deadlocks. DisableFor: []client.Object{ &corev1.ConfigMap{}, &corev1.Secret{}, + &appsv1.Deployment{}, + &corev1.Service{}, + &corev1.ServiceAccount{}, + &corev1.PersistentVolumeClaim{}, + &rbacv1.RoleBinding{}, + &rbacv1.Role{}, + &batchv1.CronJob{}, + &autoscalingv2.HorizontalPodAutoscaler{}, + &policyv1.PodDisruptionBudget{}, }, }, }, @@ -159,15 +212,51 @@ func main() { services.SetIsOpenShift(mgr.GetConfig()) + var fsMetrics *feastmetrics.FeatureStoreMetrics + if featureStoreMetrics { + fsMetrics = feastmetrics.NewFeatureStoreMetrics() + fsMetrics.Register() + setupLog.Info("FeatureStore installation metrics enabled") + } else { + setupLog.Info("FeatureStore installation metrics disabled (--feature-store-metrics=false)") + } + if err = (&controller.FeatureStoreReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Metrics: fsMetrics, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "FeatureStore") os.Exit(1) } // +kubebuilder:scaffold:builder + // Register SecurityProfileWatcher to restart on TLS profile changes + ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler()) + defer cancel() + + if tlsResult.ProfileFetched { + watcher := &tlspkg.SecurityProfileWatcher{ + Client: mgr.GetClient(), + InitialTLSProfileSpec: tlsResult.ProfileSpec, + OnProfileChange: func(_ context.Context, _, _ configv1.TLSProfileSpec) { + setupLog.Info("TLS profile changed, initiating shutdown to reload") + cancel() + }, + } + if tlsResult.AdherenceFetched { + watcher.InitialTLSAdherencePolicy = tlsResult.AdherencePolicy + watcher.OnAdherencePolicyChange = func(_ context.Context, _, _ configv1.TLSAdherencePolicy) { + setupLog.Info("TLS adherence policy changed, initiating shutdown to reload") + cancel() + } + } + if err := watcher.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to set up TLS profile watcher") + os.Exit(1) + } + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) @@ -178,7 +267,7 @@ func main() { } setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } diff --git a/infra/feast-operator/cmd/tls_bootstrap.go b/infra/feast-operator/cmd/tls_bootstrap.go new file mode 100644 index 00000000000..6fe33631e02 --- /dev/null +++ b/infra/feast-operator/cmd/tls_bootstrap.go @@ -0,0 +1,134 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "time" + + configv1 "github.com/openshift/api/config/v1" + tlspkg "github.com/openshift/controller-runtime-common/pkg/tls" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + tlsFetchTimeout = 10 * time.Second + alpnH2 = "h2" + alpnHTTP11 = "http/1.1" +) + +type tlsBootstrapResult struct { + TLSOpts []func(*tls.Config) + ProfileFetched bool + ProfileSpec configv1.TLSProfileSpec + AdherenceFetched bool + AdherencePolicy configv1.TLSAdherencePolicy + UnsupportedCiphers []string +} + +func fetchTLSProfile(ctx context.Context, k8sClient client.Client) (configv1.TLSProfileSpec, bool, error) { + fetchCtx, cancel := context.WithTimeout(ctx, tlsFetchTimeout) + defer cancel() + + profile, err := tlspkg.FetchAPIServerTLSProfile(fetchCtx, k8sClient) + if err != nil { + return classifyTLSProfileError(err) + } + return profile, true, nil +} + +func classifyTLSProfileError(err error) (configv1.TLSProfileSpec, bool, error) { + intermediate := *configv1.TLSProfiles[configv1.TLSProfileIntermediateType] + + switch { + case apimeta.IsNoMatchError(err): + return intermediate, false, nil + case apierrors.IsNotFound(err): + return intermediate, false, nil + case isTransientError(err): + return intermediate, true, nil + default: + return configv1.TLSProfileSpec{}, false, fmt.Errorf("unable to read APIServer TLS profile: %w", err) + } +} + +func fetchTLSAdherencePolicy(ctx context.Context, k8sClient client.Client) (configv1.TLSAdherencePolicy, bool, error) { + fetchCtx, cancel := context.WithTimeout(ctx, tlsFetchTimeout) + defer cancel() + + policy, err := tlspkg.FetchAPIServerTLSAdherencePolicy(fetchCtx, k8sClient) + if err == nil { + return policy, true, nil + } + + switch { + case apimeta.IsNoMatchError(err), + apierrors.IsNotFound(err), + isTransientError(err): + return "", false, nil + default: + return "", false, fmt.Errorf("unable to read APIServer TLS adherence policy: %w", err) + } +} + +func bootstrapTLS(ctx context.Context, k8sClient client.Client) (*tlsBootstrapResult, error) { + logger := log.FromContext(ctx) + result := &tlsBootstrapResult{ + TLSOpts: make([]func(*tls.Config), 0, 2), + } + + profile, profileFetched, err := fetchTLSProfile(ctx, k8sClient) + if err != nil { + return nil, err + } + result.ProfileFetched = profileFetched + result.ProfileSpec = profile + + tlsConfigFn, unsupported := tlspkg.NewTLSConfigFromProfile(profile) + result.UnsupportedCiphers = unsupported + if len(unsupported) > 0 { + logger.Info("TLS profile contains ciphers unsupported by Go", "unsupported", unsupported) + } + result.TLSOpts = append(result.TLSOpts, tlsConfigFn) + + adherence, adherenceFetched, err := fetchTLSAdherencePolicy(ctx, k8sClient) + if err != nil { + return nil, err + } + result.AdherenceFetched = adherenceFetched + result.AdherencePolicy = adherence + + result.TLSOpts = append(result.TLSOpts, func(c *tls.Config) { + c.NextProtos = []string{alpnH2, alpnHTTP11} + }) + + return result, nil +} + +func isTransientError(err error) bool { + return apierrors.IsServiceUnavailable(err) || + apierrors.IsTimeout(err) || + apierrors.IsServerTimeout(err) || + apierrors.IsTooManyRequests(err) || + errors.Is(err, context.DeadlineExceeded) +} diff --git a/infra/feast-operator/cmd/tls_bootstrap_test.go b/infra/feast-operator/cmd/tls_bootstrap_test.go new file mode 100644 index 00000000000..0bb31c14bf6 --- /dev/null +++ b/infra/feast-operator/cmd/tls_bootstrap_test.go @@ -0,0 +1,347 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "crypto/tls" + "errors" + "testing" + + configv1 "github.com/openshift/api/config/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func intermediateProfile() configv1.TLSProfileSpec { + return *configv1.TLSProfiles[configv1.TLSProfileIntermediateType] +} + +func TestClassifyTLSProfileError(t *testing.T) { + tests := []struct { + name string + err error + wantProfileFetched bool + wantError bool + wantIntermediate bool + }{ + { + name: "NoMatchError returns Intermediate defaults, profileFetched=false", + err: &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "config.openshift.io"}}, + wantProfileFetched: false, + wantError: false, + wantIntermediate: true, + }, + { + name: "NotFound returns Intermediate defaults, profileFetched=false", + err: apierrors.NewNotFound(schema.GroupResource{Group: "config.openshift.io", Resource: "apiservers"}, "cluster"), + wantProfileFetched: false, + wantError: false, + wantIntermediate: true, + }, + { + name: "ServiceUnavailable is transient, profileFetched=true", + err: apierrors.NewServiceUnavailable("api server down"), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "Timeout is transient, profileFetched=true", + err: apierrors.NewTimeoutError("timed out", 5), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "ServerTimeout is transient, profileFetched=true", + err: apierrors.NewServerTimeout(schema.GroupResource{Group: "config.openshift.io", Resource: "apiservers"}, "GET", 5), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "TooManyRequests is transient, profileFetched=true", + err: apierrors.NewTooManyRequests("throttled", 5), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "DeadlineExceeded is transient, profileFetched=true", + err: context.DeadlineExceeded, + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + { + name: "Forbidden is fatal, returns error", + err: apierrors.NewForbidden(schema.GroupResource{Group: "config.openshift.io", Resource: "apiservers"}, "cluster", errors.New("RBAC")), + wantProfileFetched: false, + wantError: true, + wantIntermediate: false, + }, + { + name: "Unauthorized is fatal, returns error", + err: apierrors.NewUnauthorized("no token"), + wantProfileFetched: false, + wantError: true, + wantIntermediate: false, + }, + { + name: "InternalServerError is fatal, returns error", + err: apierrors.NewInternalError(errors.New("crash")), + wantProfileFetched: false, + wantError: true, + wantIntermediate: false, + }, + { + name: "Generic error is fatal, returns error", + err: errors.New("something unexpected"), + wantProfileFetched: false, + wantError: true, + wantIntermediate: false, + }, + { + name: "Wrapped DeadlineExceeded is transient", + err: errors.Join(errors.New("fetch failed"), context.DeadlineExceeded), + wantProfileFetched: true, + wantError: false, + wantIntermediate: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + profile, fetched, err := classifyTLSProfileError(tt.err) + + if tt.wantError && err == nil { + t.Errorf("expected error, got nil") + } + if !tt.wantError && err != nil { + t.Errorf("unexpected error: %v", err) + } + if fetched != tt.wantProfileFetched { + t.Errorf("profileFetched = %v, want %v", fetched, tt.wantProfileFetched) + } + if tt.wantIntermediate { + intermediate := intermediateProfile() + if profile.MinTLSVersion != intermediate.MinTLSVersion { + t.Errorf("MinTLSVersion = %v, want %v (Intermediate)", profile.MinTLSVersion, intermediate.MinTLSVersion) + } + } + }) + } +} + +func TestIsTransientError(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + {"ServiceUnavailable", apierrors.NewServiceUnavailable("down"), true}, + {"Timeout", apierrors.NewTimeoutError("slow", 5), true}, + {"ServerTimeout", apierrors.NewServerTimeout(schema.GroupResource{}, "GET", 5), true}, + {"TooManyRequests", apierrors.NewTooManyRequests("throttled", 5), true}, + {"DeadlineExceeded", context.DeadlineExceeded, true}, + {"Wrapped DeadlineExceeded", errors.Join(errors.New("wrapper"), context.DeadlineExceeded), true}, + {"NotFound", apierrors.NewNotFound(schema.GroupResource{}, "x"), false}, + {"Forbidden", apierrors.NewForbidden(schema.GroupResource{}, "x", errors.New("RBAC")), false}, + {"Unauthorized", apierrors.NewUnauthorized("no token"), false}, + {"InternalError", apierrors.NewInternalError(errors.New("crash")), false}, + {"Generic error", errors.New("oops"), false}, + {"Nil", nil, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isTransientError(tt.err); got != tt.want { + t.Errorf("isTransientError() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIntermediateProfileHasExpectedDefaults(t *testing.T) { + profile := intermediateProfile() + + if profile.MinTLSVersion != configv1.VersionTLS12 { + t.Errorf("Intermediate MinTLSVersion = %v, want %v", profile.MinTLSVersion, configv1.VersionTLS12) + } + if len(profile.Ciphers) == 0 { + t.Error("Intermediate profile should have non-empty cipher list") + } +} + +func TestTLSConfigFromIntermediateProfile(t *testing.T) { + profile := intermediateProfile() + tlsConfigFn := configv1ToTLSConfig(profile) + + cfg := &tls.Config{} + tlsConfigFn(cfg) + + if cfg.MinVersion != tls.VersionTLS12 { + t.Errorf("MinVersion = %v, want %v (TLS 1.2)", cfg.MinVersion, tls.VersionTLS12) + } + if len(cfg.CipherSuites) == 0 { + t.Error("CipherSuites should not be empty for Intermediate profile") + } +} + +func configv1ToTLSConfig(profile configv1.TLSProfileSpec) func(*tls.Config) { + // Thin wrapper to test the actual conversion without importing tlspkg in tests. + // tlspkg.NewTLSConfigFromProfile is what main.go uses. + var minVersion uint16 + switch profile.MinTLSVersion { + case configv1.VersionTLS10: + minVersion = tls.VersionTLS10 + case configv1.VersionTLS11: + minVersion = tls.VersionTLS11 + case configv1.VersionTLS12: + minVersion = tls.VersionTLS12 + case configv1.VersionTLS13: + minVersion = tls.VersionTLS13 + } + + return func(c *tls.Config) { + c.MinVersion = minVersion + c.CipherSuites = mapCiphers(profile.Ciphers) + } +} + +func mapCiphers(names []string) []uint16 { + cipherMap := map[string]uint16{ + "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, + "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, + "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + } + var ids []uint16 + for _, name := range names { + if id, ok := cipherMap[name]; ok { + ids = append(ids, id) + } + } + return ids +} + +func TestClassifyTLSProfileError_AllTransientErrorsSetProfileFetched(t *testing.T) { + transientErrors := []error{ + apierrors.NewServiceUnavailable("down"), + apierrors.NewTimeoutError("slow", 5), + apierrors.NewServerTimeout(schema.GroupResource{Group: "config.openshift.io", Resource: "apiservers"}, "GET", 5), + apierrors.NewTooManyRequests("throttled", 5), + context.DeadlineExceeded, + } + + for _, err := range transientErrors { + _, fetched, classifyErr := classifyTLSProfileError(err) + if classifyErr != nil { + t.Errorf("transient error %T should not return error, got: %v", err, classifyErr) + } + if !fetched { + t.Errorf("transient error %T should set profileFetched=true", err) + } + } +} + +func TestClassifyTLSProfileError_NonTransientErrorsDoNotSetProfileFetched(t *testing.T) { + nonTransientErrors := []error{ + &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "config.openshift.io"}}, + apierrors.NewNotFound(schema.GroupResource{}, "cluster"), + } + + for _, err := range nonTransientErrors { + _, fetched, classifyErr := classifyTLSProfileError(err) + if classifyErr != nil { + t.Errorf("graceful error %T should not return error, got: %v", err, classifyErr) + } + if fetched { + t.Errorf("graceful error %T should set profileFetched=false", err) + } + } +} + +func TestClassifyTLSProfileError_FatalErrorsReturnError(t *testing.T) { + fatalErrors := []error{ + apierrors.NewForbidden(schema.GroupResource{}, "cluster", errors.New("RBAC")), + apierrors.NewUnauthorized("no token"), + apierrors.NewInternalError(errors.New("crash")), + errors.New("unexpected"), + } + + for _, err := range fatalErrors { + _, _, classifyErr := classifyTLSProfileError(err) + if classifyErr == nil { + t.Errorf("fatal error %T should return error", err) + } + } +} + +func TestClassifyTLSProfileError_IntermediateProfileAlwaysApplied(t *testing.T) { + allNonFatalErrors := []error{ + &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "config.openshift.io"}}, + apierrors.NewNotFound(schema.GroupResource{}, "cluster"), + apierrors.NewServiceUnavailable("down"), + apierrors.NewTimeoutError("slow", 5), + apierrors.NewServerTimeout(schema.GroupResource{}, "GET", 5), + apierrors.NewTooManyRequests("throttled", 5), + context.DeadlineExceeded, + } + + intermediate := intermediateProfile() + for _, err := range allNonFatalErrors { + profile, _, classifyErr := classifyTLSProfileError(err) + if classifyErr != nil { + t.Fatalf("unexpected error for %T: %v", err, classifyErr) + } + if profile.MinTLSVersion != intermediate.MinTLSVersion { + t.Errorf("for error %T: MinTLSVersion = %v, want Intermediate (%v)", err, profile.MinTLSVersion, intermediate.MinTLSVersion) + } + if len(profile.Ciphers) != len(intermediate.Ciphers) { + t.Errorf("for error %T: got %d ciphers, want %d (Intermediate)", err, len(profile.Ciphers), len(intermediate.Ciphers)) + } + } +} + +func TestTLSBootstrapResult_NextProtosAlwaysSet(t *testing.T) { + // Verify that the TLSOpts from bootstrapTLS always include ALPN with h2 and http/1.1. + // We can't call bootstrapTLS without a real client, but we can verify the function + // in tls_bootstrap.go sets NextProtos. + result := &tlsBootstrapResult{ + TLSOpts: make([]func(*tls.Config), 0, 2), + } + result.TLSOpts = append(result.TLSOpts, func(c *tls.Config) { + c.NextProtos = []string{"h2", alpnHTTP11} + }) + + cfg := &tls.Config{} + for _, opt := range result.TLSOpts { + opt(cfg) + } + + if len(cfg.NextProtos) != 2 || cfg.NextProtos[0] != "h2" || cfg.NextProtos[1] != alpnHTTP11 { + t.Errorf("NextProtos = %v, want [h2, %s]", cfg.NextProtos, alpnHTTP11) + } +} diff --git a/infra/feast-operator/config/component_metadata.yaml b/infra/feast-operator/config/component_metadata.yaml index cda672ead9a..7ee38fdb165 100644 --- a/infra/feast-operator/config/component_metadata.yaml +++ b/infra/feast-operator/config/component_metadata.yaml @@ -1,5 +1,5 @@ # This file is required to configure Feast release information for ODH/RHOAI Operator releases: - name: Feast - version: 0.56.0 + version: 0.65.0 repoUrl: https://github.com/feast-dev/feast diff --git a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml index 9264cfecf49..8184906e14d 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: featurestores.feast.dev spec: group: feast.dev @@ -23,6 +23,12832 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date + name: v1 + schema: + openAPIV3Schema: + description: FeatureStore is the Schema for the featurestores API + properties: + apiVersion: + description: APIVersion defines the versioned schema of this representation + of an object. + type: string + kind: + description: Kind is a string value representing the REST resource this + object represents. + type: string + metadata: + type: object + spec: + description: FeatureStoreSpec defines the desired state of FeatureStore + properties: + authz: + description: AuthzConfig defines the authorization settings for the + deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed in the + same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM certificate. + Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string + secretRef: + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + batchEngine: + description: BatchEngineConfig defines the batch compute engine configuration. + properties: + configMapKey: + description: Key name in the ConfigMap. Defaults to "config" if + not specified. + type: string + configMapRef: + description: Reference to a ConfigMap containing the batch engine + configuration. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + cronJob: + description: FeastCronJob defines a CronJob to execute against a Feature + Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions of a + Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings for + the CronJob + properties: + commands: + description: Array of commands to be executed (in order) against + a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + type: object + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. Value + must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. + properties: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system... + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. + properties: + rules: + description: A list of pod failure policy rules. The rules + are evaluated in order. + items: + description: PodFailurePolicyRule describes how a pod + failure is handled when the requirements are met. + properties: + action: + description: Specifies the action taken on a pod + failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the container + exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod condition + status. + type: string + type: + description: Specifies the required Pod condition + type. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer + type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string + type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. + properties: + git: + description: GitCloneOptions describes how a clone should be performed. + properties: + configs: + additionalProperties: + type: string + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the feature + repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - couchbase + - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') + type: object + x-kubernetes-validations: + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints for + the FeatureStore deployment. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. + properties: + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be unavailable + during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or "RollingUpdate". + Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures the + file-based persistence for the offline store service + properties: + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string + type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + - oracle + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a remote offline server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the online store service + properties: + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service + properties: + file: + description: OnlineStoreFilePersistence configures the + file-based persistence for the online store service + properties: + path: + type: string + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: object + x-kubernetes-validations: + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : true' + - message: PVC path must be a file name only, with no + slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: Online store does not support S3 or GS buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.online + - redis + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + - mongodb + - aerospike + - scylladb + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + serving: + description: Serving configures the Feast feature_server section + written into feature_store.yaml for the online serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. Defaults + to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults to + "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or disables + individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to the + offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to the + offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline write + batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied to the + Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true + type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' + registry: + description: Registry configures the registry service. One selection + is required. Local is the default setting. + properties: + local: + description: LocalRegistryConfig configures the registry service + properties: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures the + file-based persistence for the registry service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-validations: + - message: Registry files must use absolute paths + or be S3 ('s3://') or GS ('gs://') object store + URIs. + rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') + || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) + : true' + - message: PVC path must be a file name only, with + no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: PVC persistence does not support S3 or + GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available only + for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be true + rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` CR + in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for the + client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the FeatureStore + deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: scaleUp is scaling policy for scaling + Up. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + description: MaxReplicas is the upper limit for the number + of replicas. Required. + format: int32 + minimum: 1 + type: integer + metrics: + description: Metrics contains the specifications for which + to use to calculate the desired replica count. + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at... + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes... + properties: + container: + description: container is the name of the container + in the pods of the scaling target + type: string + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the descriptions + of a object,such as kind,name apiVersion + properties: + apiVersion: + description: apiVersion is the API version + of the referent + type: string + kind: + description: 'kind is the kind of the referent; + More info: https://git.k8s.' + type: string + name: + description: 'name is the name of the referent; + More info: https://kubernetes.' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example,... + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing... + properties: + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: type is the type of metric source. + type: string + required: + - type + type: object + type: array + minReplicas: + description: MinReplicas is the lower limit for the number + of replicas. Defaults to 1. + format: int32 + minimum: 1 + type: integer + required: + - maxReplicas + type: object + type: object + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to use + by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile loaded + on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor profile + will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies to + all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as a non-root + user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string + seLinuxOptions: + description: The SELinux context to be applied to all containers. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile defined + in a file on the node should be used. + type: string + type: + description: type indicates which kind of seccomp profile + will be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string + sysctls: + description: Sysctls hold a list of namespaced sysctls used + for the pod. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + description: The Windows specific settings applied to all + containers. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container should + be run as a 'Host Process' container. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. + type: string + type: object + type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are spread + across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of eligible + domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + ui: + description: Creates a UI server container + properties: + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. useful + in an openshift cluster, for example, where TLS is configured + by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key names + for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where the + TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes that + should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from which + the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * CPU + + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + volumes: + description: Volumes specifies the volumes to mount in the FeatureStore + deployment. + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to... + properties: + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes. + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes. + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk mount + on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk in + the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in the + blob storage + type: string + fsType: + default: ext4 + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage...' + type: string + readOnly: + default: false + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the host + that shares a pod's lifetime. + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s. + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile is + the path to key ring for User, default is /etc/ceph/user.' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is reference + to the authentication secret for User, default is + empty.' + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s. + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + Deprecated: Cinder is deprecated. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits used + to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used + to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers. + properties: + driver: + description: driver is the name of the CSI driver that + handles this volume. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", "ntfs". + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about the + pod that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created + files by default.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the + pod: only annotations, labels, name, namespace + and uid are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must not + be absolute or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes. + properties: + medium: + description: medium represents what type of storage + medium should back this directory. + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: sizeLimit is the total amount of local + storage required for this EmptyDir volume. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: ephemeral represents a volume that is handled + by a cluster storage driver. + properties: + volumeClaimTemplate: + description: Will be used to create a stand-alone PVC + to provision the volume. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. + type: object + spec: + description: The specification for the PersistentVolumeClaim. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s. + properties: + apiGroup: + description: APIGroup is the group for the + resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. + properties: + apiGroup: + description: APIGroup is the group for the + resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking. + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum + resources the volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may be + used to set the VolumeAttributesClass used + by this claim. + type: string + volumeMode: + description: volumeMode defines what type of + volume is required by the claim. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource that + is attached to a kubelet's host machine and then exposed + to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to use + for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as... + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the... + properties: + fsType: + description: fsType is filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes. + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes. + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + Deprecated: GitRepo is deprecated. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. + properties: + endpoints: + description: endpoints is the endpoint name that details + Glusterfs topology. + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + default: default + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal List. + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: targetPortal is iSCSI Target Portal. + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes. + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes. + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes. + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + properties: + claimName: + description: claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + pdID: + description: pdID is the ID that identifies Photon Controller + persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources secrets, + configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used to set + permissions on created files by default. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: ClusterTrustBundle allows a pod to + access the `.spec. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. + type: string + required: + - path + type: object + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode + bits used to set permissions on this + file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name, namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file to + be created. Must not be absolute or + contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object + secret: + description: secret information about the secret + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode + bits used to set permissions on this + file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: audience is the intended audience + of the token. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + description: quobyte represents a Quobyte mount on the host + that shares a pod's lifetime. + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple... + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set... + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. + properties: + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + default: /etc/ceph/keyring + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s. + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s. + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + default: xfs + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + default: ThinProvisioned + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes. + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits used + to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used + to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes. + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope of + the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine. + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + x-kubernetes-validations: + - message: replicas > 1 and services.scaling.autoscaling are mutually + exclusive. + rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling) + - message: Scaling requires DB-backed persistence for the online store. + Configure services.onlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) + && has(self.services.onlineStore.persistence.store)) + - message: Scaling requires DB-backed persistence for the offline store. + Configure services.offlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (!has(self.services) + || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) + && has(self.services.offlineStore.persistence.store))) + - message: Scaling requires DB-backed or remote registry. Configure registry.local.persistence.store + or use a remote registry when using replicas > 1 or autoscaling. S3/GCS-backed + registry is also allowed. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.registry) && (has(self.services.registry.remote) + || (has(self.services.registry.local) && has(self.services.registry.local.persistence) + && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) + && has(self.services.registry.local.persistence.file.path) && (self.services.registry.local.persistence.file.path.startsWith('s3://') + || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + properties: + applied: + description: Shows the currently applied feast configuration, including + any pertinent defaults + properties: + authz: + description: AuthzConfig defines the authorization settings for + the deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed + in the same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM + certificate. Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string + secretRef: + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + batchEngine: + description: BatchEngineConfig defines the batch compute engine + configuration. + properties: + configMapKey: + description: Key name in the ConfigMap. Defaults to "config" + if not specified. + type: string + configMapRef: + description: Reference to a ConfigMap containing the batch + engine configuration. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + type: object + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. + properties: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system... + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. + properties: + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer + type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string + type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. + properties: + git: + description: GitCloneOptions describes how a clone should + be performed. + properties: + configs: + additionalProperties: + type: string + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast + init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - couchbase + - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') + type: object + x-kubernetes-validations: + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints + for the FeatureStore deployment. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in the + range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, + etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. + properties: + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be + unavailable during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or + "RollingUpdate". Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures + the file-based persistence for the offline store + service + properties: + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string + type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + - oracle + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a remote offline server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the online store service + properties: + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service + properties: + file: + description: OnlineStoreFilePersistence configures + the file-based persistence for the online store + service + properties: + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: object + x-kubernetes-validations: + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : true' + - message: PVC path must be a file name only, with + no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: Online store does not support S3 or GS + buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - snowflake.online + - redis + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + - mongodb + - aerospike + - scylladb + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + serving: + description: Serving configures the Feast feature_server + section written into feature_store.yaml for the online + serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is + "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or + disables individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to + the offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to + the offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline + write batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied + to the Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true + type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' + registry: + description: Registry configures the registry service. One + selection is required. Local is the default setting. + properties: + local: + description: LocalRegistryConfig configures the registry + service + properties: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures + the file-based persistence for the registry + service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL + (in seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings + for a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new + PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to + ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the + storage resource requirements for + a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes + the minimum amount of compute + resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the + name of an existing StorageClass + to which this persistent volume + belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing + field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between + ref and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' + and must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-validations: + - message: Registry files must use absolute paths + or be S3 ('s3://') or GS ('gs://') object + store URIs. + rule: '(!has(self.pvc) && has(self.path)) ? + (self.path.startsWith(''/'') || self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: PVC path must be a file name only, + with no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: PVC persistence does not support S3 + or GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available + only for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store + "type" is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type + you want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or + store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment + variable present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment + variable's value. Cannot be used if value + is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the + ConfigMap or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the + pod: supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret + in the pod's namespace + properties: + key: + description: The key of the secret + to select from. Must be a valid + secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the + Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to + the name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for + if/when to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the + compute resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one + entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS + for a feast service. + properties: + disable: + description: will disable TLS for the feast + service. useful in an openshift cluster, + for example, where TLS is configured by + default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret + where the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` + is false.' + rule: '(!has(self.disable) || !self.disable) + ? has(self.secretRef) : true' + volumeMounts: + description: VolumeMounts defines the list of + volumes that should be mounted into the feast + container. + items: + description: VolumeMount describes a mounting + of a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of + a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should + be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker + configuration for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker + processes. Use -1 to auto-calculate based + on CPU cores (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be + true + rule: self.restAPI == true || self.grpc == true + || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` + CR in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for + the client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap + where the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, + c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the + FeatureStore deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential + scaling polices which can be used during + scaling. + items: + description: HPAScalingPolicy is a single + policy which must hold true for a specified + past interval. + properties: + periodSeconds: + description: periodSeconds specifies + the window of time for which the policy + should hold true. + format: int32 + type: integer + type: + description: type is used to specify + the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: scaleUp is scaling policy for scaling + Up. + properties: + policies: + description: policies is a list of potential + scaling polices which can be used during + scaling. + items: + description: HPAScalingPolicy is a single + policy which must hold true for a specified + past interval. + properties: + periodSeconds: + description: periodSeconds specifies + the window of time for which the policy + should hold true. + format: int32 + type: integer + type: + description: type is used to specify + the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + description: MaxReplicas is the upper limit for the + number of replicas. Required. + format: int32 + minimum: 1 + type: integer + metrics: + description: Metrics contains the specifications for + which to use to calculate the desired replica count. + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at... + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes... + properties: + container: + description: container is the name of the + container in the pods of the scaling target + type: string + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. + properties: + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the + descriptions of a object,such as kind,name + apiVersion + properties: + apiVersion: + description: apiVersion is the API version + of the referent + type: string + kind: + description: 'kind is the kind of the + referent; More info: https://git.k8s.' + type: string + name: + description: 'name is the name of the + referent; More info: https://kubernetes.' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example,... + properties: + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing... + properties: + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: type is the type of metric source. + type: string + required: + - type + type: object + type: array + minReplicas: + description: MinReplicas is the lower limit for the + number of replicas. Defaults to 1. + format: int32 + minimum: 1 + type: integer + required: + - maxReplicas + type: object + type: object + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to + use by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile + loaded on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor + profile will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies + to all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as + a non-root user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string + seLinuxOptions: + description: The SELinux context to be applied to all + containers. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. + type: string + type: + description: type indicates which kind of seccomp + profile will be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string + sysctls: + description: Sysctls hold a list of namespaced sysctls + used for the pod. + items: + description: Sysctl defines a kernel parameter to be + set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + description: The Windows specific settings applied to + all containers. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of + the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. + type: string + type: object + type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are + spread across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + ui: + description: Creates a UI server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + volumes: + description: Volumes specifies the volumes to mount in the + FeatureStore deployment. + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to... + properties: + fsType: + description: fsType is the filesystem type of the + volume that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes. + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes. + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + default: ext4 + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage...' + type: string + readOnly: + default: false + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime. + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s. + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile + is the path to key ring for User, default is /etc/ceph/user.' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is + reference to the authentication secret for User, + default is empty.' + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s. + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + Deprecated: Cinder is deprecated. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits + used to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers. + properties: + driver: + description: driver is the name of the CSI driver + that handles this volume. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", + "ntfs". + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created + files by default.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name, + namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes. + properties: + medium: + description: medium represents what type of storage + medium should back this directory. + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: sizeLimit is the total amount of local + storage required for this EmptyDir volume. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: ephemeral represents a volume that is handled + by a cluster storage driver. + properties: + volumeClaimTemplate: + description: Will be used to create a stand-alone + PVC to provision the volume. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. + type: object + spec: + description: The specification for the PersistentVolumeClaim. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s. + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking. + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum + resources the volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may + be used to set the VolumeAttributesClass + used by this claim. + type: string + volumeMode: + description: volumeMode defines what type + of volume is required by the claim. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as... + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the... + properties: + fsType: + description: fsType is filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes. + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes. + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + Deprecated: GitRepo is deprecated. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. + properties: + endpoints: + description: endpoints is the endpoint name that + details Glusterfs topology. + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: fsType is the filesystem type of the + volume that you want to mount. + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + default: default + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal + List. + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: targetPortal is iSCSI Target Portal. + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes. + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes. + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes. + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + properties: + claimName: + description: claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used + to set permissions on created files by default. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: ClusterTrustBundle allows a pod + to access the `.spec. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name, namespace and uid + are supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: audience is the intended + audience of the token. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime. + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple... + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set... + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. + properties: + fsType: + description: fsType is the filesystem type of the + volume that you want to mount. + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + default: /etc/ceph/keyring + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s. + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s. + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + default: xfs + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + default: ThinProvisioned + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes. + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits + used to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes. + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope + of the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + x-kubernetes-validations: + - message: replicas > 1 and services.scaling.autoscaling are mutually + exclusive. + rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling) + - message: Scaling requires DB-backed persistence for the online store. + Configure services.onlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) + && has(self.services.onlineStore.persistence.store)) + - message: Scaling requires DB-backed persistence for the offline + store. Configure services.offlineStore.persistence.store when + using replicas > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (!has(self.services) + || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) + && has(self.services.offlineStore.persistence.store))) + - message: Scaling requires DB-backed or remote registry. Configure + registry.local.persistence.store or use a remote registry when + using replicas > 1 or autoscaling. S3/GCS-backed registry is also + allowed. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.registry) && (has(self.services.registry.remote) + || (has(self.services.registry.local) && has(self.services.registry.local.persistence) + && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) + && has(self.services.registry.local.persistence.file.path) && + (self.services.registry.local.persistence.file.path.startsWith('s3://') + || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) + clientConfigMap: + description: ConfigMap in this namespace containing a client `feature_store.yaml` + for this feast deployment + type: string + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if . + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + cronJob: + description: CronJob in this namespace for this feast deployment + type: string + feastVersion: + type: string + phase: + type: string + replicas: + description: Replicas is the current number of ready pod replicas + (used by the scale sub-resource). + format: int32 + type: integer + scalingStatus: + description: ScalingStatus reports the current scaling state of the + FeatureStore deployment. + properties: + currentReplicas: + description: CurrentReplicas is the current number of pod replicas. + format: int32 + type: integer + desiredReplicas: + description: DesiredReplicas is the desired number of pod replicas. + format: int32 + type: integer + type: object + selector: + description: Selector is the label selector for pods managed by the + FeatureStore deployment (used by the scale sub-resource). + type: string + serviceHostnames: + description: ServiceHostnames defines the service hostnames in the + format of :, e.g. example.svc.cluster.local:80 + properties: + offlineStore: + type: string + onlineStore: + type: string + registry: + type: string + registryRest: + type: string + ui: + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: v1alpha1 is deprecated and will be removed in a future release. + Please migrate to v1. name: v1alpha1 schema: openAPIV3Schema: @@ -112,14 +12938,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -163,6 +12989,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -218,7 +13073,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -237,8 +13092,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -284,6 +13139,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -325,7 +13184,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -419,7 +13278,6 @@ spec: type. type: string required: - - status - type type: object type: array @@ -436,6 +13294,13 @@ spec: description: podReplacementPolicy specifies when to create replacement Pods. type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object suspend: description: suspend specifies whether the Job controller should create Pods or not. @@ -495,14 +13360,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -546,6 +13411,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -601,7 +13495,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -620,8 +13514,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -675,15 +13569,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -870,14 +13789,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -921,6 +13840,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -977,7 +13926,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -996,8 +13945,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -1034,6 +13983,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -1055,6 +14008,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -1166,6 +14123,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object onlineStore: @@ -1297,7 +14300,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -1311,6 +14313,10 @@ spec: - qdrant - couchbase.online - milvus + - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -1329,14 +14335,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -1380,6 +14386,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1436,7 +14472,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1455,8 +14491,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -1493,6 +14529,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -1514,6 +14554,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -1625,6 +14669,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object registry: @@ -1642,6 +14732,21 @@ spec: description: RegistryFilePersistence configures the file-based persistence for the registry service properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer path: type: string pvc: @@ -1792,14 +14897,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -1844,6 +14949,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1902,7 +15037,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1921,8 +15056,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -1963,6 +15098,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -1984,6 +15123,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -2099,6 +15242,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object x-kubernetes-validations: - message: At least one of restAPI or grpc must be true @@ -2159,6 +15348,10 @@ spec: x-kubernetes-validations: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -2204,6 +15397,10 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. properties: @@ -2242,13 +15439,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -2299,14 +15501,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -2350,6 +15552,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -2405,7 +15636,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -2424,8 +15655,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -2462,6 +15693,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -2482,6 +15717,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -2593,6 +15832,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * CPU + + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object volumes: description: Volumes specifies the volumes to mount in the FeatureStore @@ -2604,7 +15889,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the volume @@ -2646,6 +15931,7 @@ spec: blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -2654,9 +15940,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -2687,7 +15974,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: monitors: description: |- @@ -2735,7 +16022,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -2781,7 +16068,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -2821,7 +16108,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver that @@ -2833,7 +16120,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -2895,7 +16182,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -3144,9 +16431,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide identifiers - (wwids)\nEither wwids or combination of targetWWNs - and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -3181,7 +16468,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -3202,7 +16489,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -3212,7 +16499,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -3241,7 +16528,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -3259,14 +16546,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that details + Glusterfs topology. type: string path: description: |- @@ -3301,6 +16586,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -3326,6 +16627,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -3416,7 +16718,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -3433,7 +16735,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -3463,10 +16765,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected along - with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod to @@ -3546,7 +16851,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -3617,7 +16922,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -3659,6 +16964,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -3666,7 +17017,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -3732,7 +17083,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: group: description: |- @@ -3747,12 +17098,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -3768,9 +17119,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the volume @@ -3782,6 +17132,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -3796,6 +17147,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -3823,6 +17175,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -3837,6 +17190,7 @@ spec: attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -3874,6 +17228,7 @@ spec: with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -3908,7 +17263,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -3983,7 +17338,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine + and mounted on kubelets host machine. properties: fsType: description: |- @@ -4093,14 +17448,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4144,6 +17499,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4200,7 +17585,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4219,8 +17604,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -4267,6 +17652,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -4308,7 +17697,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -4403,7 +17792,6 @@ spec: condition type. type: string required: - - status - type type: object type: array @@ -4420,6 +17808,13 @@ spec: description: podReplacementPolicy specifies when to create replacement Pods. type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object suspend: description: suspend specifies whether the Job controller should create Pods or not. @@ -4481,14 +17876,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4532,6 +17927,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4588,7 +18013,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4607,8 +18032,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -4663,15 +18088,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -4861,14 +18311,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4913,6 +18363,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4971,7 +18451,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4990,8 +18470,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5028,6 +18508,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -5049,6 +18533,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -5161,6 +18649,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object onlineStore: @@ -5295,7 +18829,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -5309,6 +18842,10 @@ spec: - qdrant - couchbase.online - milvus + - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -5328,14 +18865,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -5380,6 +18917,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5438,7 +19005,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5457,8 +19024,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5495,6 +19062,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -5516,6 +19087,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -5628,6 +19203,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object registry: @@ -5647,6 +19268,21 @@ spec: the file-based persistence for the registry service properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL + (in seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer path: type: string pvc: @@ -5803,14 +19439,14 @@ spec: variable present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment @@ -5856,6 +19492,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5915,7 +19581,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5934,9 +19600,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be - a C_IDENTIFIER. + description: Optional text to prepend to + the name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5977,6 +19642,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -5998,6 +19667,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -6117,6 +19790,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker + configuration for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker + processes. Use -1 to auto-calculate based + on CPU cores (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object x-kubernetes-validations: - message: At least one of restAPI or grpc must be @@ -6180,6 +19899,11 @@ spec: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -6225,6 +19949,11 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. @@ -6264,13 +19993,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -6322,14 +20056,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -6373,6 +20107,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -6429,7 +20193,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -6448,8 +20212,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -6486,6 +20250,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -6507,6 +20275,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -6618,6 +20390,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object volumes: description: Volumes specifies the volumes to mount in the @@ -6629,7 +20447,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the @@ -6671,6 +20489,7 @@ spec: the blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -6679,9 +20498,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -6712,7 +20532,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: monitors: description: |- @@ -6761,7 +20581,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -6807,7 +20627,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -6847,7 +20667,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver @@ -6860,7 +20680,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -6924,7 +20744,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -7176,9 +20996,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide - identifiers (wwids)\nEither wwids or combination - of targetWWNs and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -7213,7 +21033,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -7234,7 +21054,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -7244,7 +21064,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -7273,7 +21093,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -7291,14 +21111,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that + details Glusterfs topology. type: string path: description: |- @@ -7333,6 +21151,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -7358,6 +21192,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -7449,7 +21284,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -7466,7 +21301,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -7496,10 +21331,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected - along with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod @@ -7580,7 +21418,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -7653,7 +21491,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -7695,6 +21533,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -7702,7 +21586,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -7768,7 +21652,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: group: description: |- @@ -7783,12 +21667,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -7804,9 +21688,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the @@ -7818,6 +21701,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -7832,6 +21716,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -7859,6 +21744,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -7873,6 +21759,7 @@ spec: volume attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -7910,6 +21797,7 @@ spec: communication with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -7944,7 +21832,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -8019,7 +21907,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -8093,10 +21981,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition. + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string @@ -8133,6 +22018,6 @@ spec: type: object type: object served: true - storage: true + storage: false subresources: status: {} diff --git a/infra/feast-operator/config/default/metrics_service.yaml b/infra/feast-operator/config/default/metrics_service.yaml index 0207c0469d4..fbf17dd96ec 100644 --- a/infra/feast-operator/config/default/metrics_service.yaml +++ b/infra/feast-operator/config/default/metrics_service.yaml @@ -14,4 +14,5 @@ spec: protocol: TCP targetPort: 8443 selector: + app.kubernetes.io/name: feast-operator control-plane: controller-manager diff --git a/infra/feast-operator/config/default/related_image_fs_patch.tmpl b/infra/feast-operator/config/default/related_image_fs_patch.tmpl index 23bf80c98ba..11e127dab39 100644 --- a/infra/feast-operator/config/default/related_image_fs_patch.tmpl +++ b/infra/feast-operator/config/default/related_image_fs_patch.tmpl @@ -1,10 +1,14 @@ -- op: replace - path: "/spec/template/spec/containers/0/env/0" - value: - name: RELATED_IMAGE_FEATURE_SERVER - value: ${FS_IMG} -- op: replace - path: "/spec/template/spec/containers/0/env/1" - value: - name: RELATED_IMAGE_CRON_JOB - value: ${CJ_IMG} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager +spec: + template: + spec: + containers: + - name: manager + env: + - name: RELATED_IMAGE_FEATURE_SERVER + value: ${FS_IMG} + - name: RELATED_IMAGE_CRON_JOB + value: ${CJ_IMG} diff --git a/infra/feast-operator/config/default/related_image_fs_patch.yaml b/infra/feast-operator/config/default/related_image_fs_patch.yaml index 30cd7d3616f..5ab07fd91e1 100644 --- a/infra/feast-operator/config/default/related_image_fs_patch.yaml +++ b/infra/feast-operator/config/default/related_image_fs_patch.yaml @@ -1,10 +1,14 @@ -- op: replace - path: "/spec/template/spec/containers/0/env/0" - value: - name: RELATED_IMAGE_FEATURE_SERVER - value: quay.io/feastdev/feature-server:0.56.0 -- op: replace - path: "/spec/template/spec/containers/0/env/1" - value: - name: RELATED_IMAGE_CRON_JOB - value: quay.io/openshift/origin-cli:4.17 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager +spec: + template: + spec: + containers: + - name: manager + env: + - name: RELATED_IMAGE_FEATURE_SERVER + value: quay.io/feastdev/feature-server:0.65.0 + - name: RELATED_IMAGE_CRON_JOB + value: quay.io/openshift/origin-cli:4.17 diff --git a/infra/feast-operator/config/manager/kustomization.yaml b/infra/feast-operator/config/manager/kustomization.yaml index 80aaa3faf28..5f3ce6cadda 100644 --- a/infra/feast-operator/config/manager/kustomization.yaml +++ b/infra/feast-operator/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: quay.io/feastdev/feast-operator - newTag: 0.56.0 + newTag: 0.65.0 diff --git a/infra/feast-operator/config/manager/manager.yaml b/infra/feast-operator/config/manager/manager.yaml index 242144e2b03..4213787e075 100644 --- a/infra/feast-operator/config/manager/manager.yaml +++ b/infra/feast-operator/config/manager/manager.yaml @@ -19,6 +19,7 @@ metadata: spec: selector: matchLabels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager replicas: 1 template: @@ -26,6 +27,7 @@ spec: annotations: kubectl.kubernetes.io/default-container: manager labels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager spec: # TODO(user): Uncomment the following code to configure the nodeAffinity expression @@ -71,10 +73,16 @@ spec: drop: - "ALL" env: + - name: GOMEMLIMIT + value: "230MiB" - name: RELATED_IMAGE_FEATURE_SERVER value: feast:latest - name: RELATED_IMAGE_CRON_JOB value: origin-cli:latest + # Injected from params.env via kustomize replacements (ODH/RHOAI overlays). + # Open Data Hub operator sets this from GatewayConfig when the cluster uses external OIDC. + - name: OIDC_ISSUER_URL + value: "" livenessProbe: httpGet: path: /healthz diff --git a/infra/feast-operator/config/manifests/bases/feast-operator.clusterserviceversion.yaml b/infra/feast-operator/config/manifests/bases/feast-operator.clusterserviceversion.yaml index b9915a296a4..c1ae77619ad 100644 --- a/infra/feast-operator/config/manifests/bases/feast-operator.clusterserviceversion.yaml +++ b/infra/feast-operator/config/manifests/bases/feast-operator.clusterserviceversion.yaml @@ -10,6 +10,11 @@ spec: apiservicedefinitions: {} customresourcedefinitions: owned: + - description: FeatureStore is the Schema for the featurestores API + displayName: Feature Store + kind: FeatureStore + name: featurestores.feast.dev + version: v1 - description: FeatureStore is the Schema for the featurestores API displayName: Feature Store kind: FeatureStore diff --git a/infra/feast-operator/config/overlays/odh/kustomization.yaml b/infra/feast-operator/config/overlays/odh/kustomization.yaml index cf751d178bd..044614f01fe 100644 --- a/infra/feast-operator/config/overlays/odh/kustomization.yaml +++ b/infra/feast-operator/config/overlays/odh/kustomization.yaml @@ -52,3 +52,13 @@ replacements: name: controller-manager fieldPaths: - spec.template.spec.containers.[name=manager].env.[name=RELATED_IMAGE_CRON_JOB].value + - source: + kind: ConfigMap + name: feast-operator-parameters + fieldPath: data.OIDC_ISSUER_URL + targets: + - select: + kind: Deployment + name: controller-manager + fieldPaths: + - spec.template.spec.containers.[name=manager].env.[name=OIDC_ISSUER_URL].value diff --git a/infra/feast-operator/config/overlays/odh/params.env b/infra/feast-operator/config/overlays/odh/params.env index b112bb2d854..b0d55d6bd70 100644 --- a/infra/feast-operator/config/overlays/odh/params.env +++ b/infra/feast-operator/config/overlays/odh/params.env @@ -1,3 +1,5 @@ -RELATED_IMAGE_FEAST_OPERATOR=quay.io/feastdev/feast-operator:0.56.0 -RELATED_IMAGE_FEATURE_SERVER=quay.io/feastdev/feature-server:0.56.0 +RELATED_IMAGE_FEAST_OPERATOR=quay.io/feastdev/feast-operator:0.65.0 +RELATED_IMAGE_FEATURE_SERVER=quay.io/feastdev/feature-server:0.65.0 RELATED_IMAGE_CRON_JOB=quay.io/openshift/origin-cli:4.17 +# Set at deploy time by the Open Data Hub operator from GatewayConfig (external OIDC). +OIDC_ISSUER_URL= diff --git a/infra/feast-operator/config/overlays/rhoai/kustomization.yaml b/infra/feast-operator/config/overlays/rhoai/kustomization.yaml index 4917579ef28..b9d075bdf39 100644 --- a/infra/feast-operator/config/overlays/rhoai/kustomization.yaml +++ b/infra/feast-operator/config/overlays/rhoai/kustomization.yaml @@ -52,3 +52,13 @@ replacements: name: controller-manager fieldPaths: - spec.template.spec.containers.[name=manager].env.[name=RELATED_IMAGE_CRON_JOB].value + - source: + kind: ConfigMap + name: feast-operator-parameters + fieldPath: data.OIDC_ISSUER_URL + targets: + - select: + kind: Deployment + name: controller-manager + fieldPaths: + - spec.template.spec.containers.[name=manager].env.[name=OIDC_ISSUER_URL].value diff --git a/infra/feast-operator/config/overlays/rhoai/params.env b/infra/feast-operator/config/overlays/rhoai/params.env index f548227235d..dabacfd458c 100644 --- a/infra/feast-operator/config/overlays/rhoai/params.env +++ b/infra/feast-operator/config/overlays/rhoai/params.env @@ -1,3 +1,5 @@ -RELATED_IMAGE_FEAST_OPERATOR=quay.io/feastdev/feast-operator:0.56.0 -RELATED_IMAGE_FEATURE_SERVER=quay.io/feastdev/feature-server:0.56.0 -RELATED_IMAGE_CRON_JOB=registry.redhat.io/openshift4/ose-cli@sha256:bc35a9fc663baf0d6493cc57e89e77a240a36c43cf38fb78d8e61d3b87cf5cc5 \ No newline at end of file +RELATED_IMAGE_FEAST_OPERATOR=quay.io/feastdev/feast-operator:0.65.0 +RELATED_IMAGE_FEATURE_SERVER=quay.io/feastdev/feature-server:0.65.0 +RELATED_IMAGE_CRON_JOB=registry.redhat.io/openshift4/ose-cli@sha256:bc35a9fc663baf0d6493cc57e89e77a240a36c43cf38fb78d8e61d3b87cf5cc5 +# Set at deploy time by the Open Data Hub operator from GatewayConfig (external OIDC). +OIDC_ISSUER_URL= \ No newline at end of file diff --git a/infra/feast-operator/config/prometheus/monitor.yaml b/infra/feast-operator/config/prometheus/monitor.yaml index e76479a1305..50f2ea8e448 100644 --- a/infra/feast-operator/config/prometheus/monitor.yaml +++ b/infra/feast-operator/config/prometheus/monitor.yaml @@ -27,4 +27,5 @@ spec: insecureSkipVerify: true selector: matchLabels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager diff --git a/infra/feast-operator/config/rbac/featurestore_editor_role.yaml b/infra/feast-operator/config/rbac/featurestore_editor_role.yaml index 37c38e6f618..e2e3acf1b60 100644 --- a/infra/feast-operator/config/rbac/featurestore_editor_role.yaml +++ b/infra/feast-operator/config/rbac/featurestore_editor_role.yaml @@ -5,6 +5,8 @@ metadata: labels: app.kubernetes.io/name: feast-operator app.kubernetes.io/managed-by: kustomize + rbac.authorization.k8s.io/aggregate-to-admin: "true" + rbac.authorization.k8s.io/aggregate-to-edit: "true" name: featurestore-editor-role rules: - apiGroups: diff --git a/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml b/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml index b4444cbe60a..bca67d9788d 100644 --- a/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml +++ b/infra/feast-operator/config/rbac/featurestore_viewer_role.yaml @@ -5,6 +5,7 @@ metadata: labels: app.kubernetes.io/name: feast-operator app.kubernetes.io/managed-by: kustomize + rbac.authorization.k8s.io/aggregate-to-view: "true" name: featurestore-viewer-role rules: - apiGroups: diff --git a/infra/feast-operator/config/rbac/role.yaml b/infra/feast-operator/config/rbac/role.yaml index 56a952dd95c..a79dca283ed 100644 --- a/infra/feast-operator/config/rbac/role.yaml +++ b/infra/feast-operator/config/rbac/role.yaml @@ -4,6 +4,63 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + - persistentvolumeclaims + - services + verbs: + - create + - delete + - deletecollection + - get + - list + - update + - watch +- apiGroups: + - "" + resources: + - namespaces + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - deletecollection + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods/exec + verbs: + - create +- apiGroups: + - "" + resources: + - pods/log + verbs: + - get +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - update + - watch - apiGroups: - apps resources: @@ -22,9 +79,9 @@ rules: verbs: - create - apiGroups: - - batch + - autoscaling resources: - - cronjobs + - horizontalpodautoscalers verbs: - create - delete @@ -34,35 +91,25 @@ rules: - update - watch - apiGroups: - - "" + - batch resources: - - configmaps - - persistentvolumeclaims - - serviceaccounts - - services + - cronjobs verbs: - create - delete - get - list + - patch - update - watch - apiGroups: - - "" + - config.openshift.io resources: - - namespaces - - pods - - secrets + - apiservers verbs: - get - list - watch -- apiGroups: - - "" - resources: - - pods/exec - verbs: - - create - apiGroups: - feast.dev resources: @@ -89,6 +136,29 @@ rules: - get - patch - update +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - watch +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resources: @@ -115,3 +185,11 @@ rules: - list - update - watch +- apiGroups: + - sparkoperator.k8s.io + resources: + - sparkapplications + verbs: + - create + - delete + - get diff --git a/infra/feast-operator/config/samples/kustomization.yaml b/infra/feast-operator/config/samples/kustomization.yaml index 12cfa7dad65..65061a2b265 100644 --- a/infra/feast-operator/config/samples/kustomization.yaml +++ b/infra/feast-operator/config/samples/kustomization.yaml @@ -1,6 +1,9 @@ ## Append samples of your project ## resources: -- v1alpha1_featurestore.yaml -- v1alpha1_featurestore_with_ui.yaml -- v1alpha1_featurestore_all_remote_servers.yaml +- v1_featurestore.yaml +- v1_featurestore_with_ui.yaml +- v1_featurestore_all_remote_servers.yaml +- v1_featurestore_serving.yaml +- v1_featurestore_mcp.yaml +- v1_featurestore_materialization_openlineage.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore.yaml b/infra/feast-operator/config/samples/v1_featurestore.yaml similarity index 71% rename from infra/feast-operator/config/samples/v1alpha1_featurestore.yaml rename to infra/feast-operator/config/samples/v1_featurestore.yaml index 3eb62850435..10c2a179e04 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_all_openshift_non_tls.yaml b/infra/feast-operator/config/samples/v1_featurestore_all_openshift_non_tls.yaml similarity index 91% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_all_openshift_non_tls.yaml rename to infra/feast-operator/config/samples/v1_featurestore_all_openshift_non_tls.yaml index 4e0ef943634..379671b4708 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_all_openshift_non_tls.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_all_openshift_non_tls.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-openshift-non-tls diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_all_remote_servers.yaml b/infra/feast-operator/config/samples/v1_featurestore_all_remote_servers.yaml similarity index 90% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_all_remote_servers.yaml rename to infra/feast-operator/config/samples/v1_featurestore_all_remote_servers.yaml index b30e93383cd..b7fc3a5cd7a 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_all_remote_servers.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_all_remote_servers.yaml @@ -1,5 +1,5 @@ # runs an optional remote server container & creates a k8s service for each supported feast component -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-remote-servers diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml b/infra/feast-operator/config/samples/v1_featurestore_db_persistence.yaml similarity index 97% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml rename to infra/feast-operator/config/samples/v1_featurestore_db_persistence.yaml index e66b7fc3283..3cbc374add0 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_db_persistence.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_db_persistence.yaml @@ -30,7 +30,7 @@ stringData: user: ${POSTGRES_USER} password: ${POSTGRES_PASSWORD} --- -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-db-persistence diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_gcs.yaml b/infra/feast-operator/config/samples/v1_featurestore_gcs.yaml similarity index 97% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_gcs.yaml rename to infra/feast-operator/config/samples/v1_featurestore_gcs.yaml index 7f2bf982657..7c15216a99a 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_gcs.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_gcs.yaml @@ -19,7 +19,7 @@ stringData: xxxx xxxxx --- -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: gcs diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_git.yaml b/infra/feast-operator/config/samples/v1_featurestore_git.yaml similarity index 87% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_git.yaml rename to infra/feast-operator/config/samples/v1_featurestore_git.yaml index 7730ef88518..1ea5bcabb1b 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_git.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_git.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-git diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_git_repopath.yaml b/infra/feast-operator/config/samples/v1_featurestore_git_repopath.yaml similarity index 88% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_git_repopath.yaml rename to infra/feast-operator/config/samples/v1_featurestore_git_repopath.yaml index 6519e1bf429..61c2ee2efd6 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_git_repopath.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_git_repopath.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-git-repopath diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_git_token.yaml b/infra/feast-operator/config/samples/v1_featurestore_git_token.yaml similarity index 92% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_git_token.yaml rename to infra/feast-operator/config/samples/v1_featurestore_git_token.yaml index f16f503c8fb..44339bca56c 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_git_token.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_git_token.yaml @@ -5,7 +5,7 @@ metadata: stringData: TOKEN: xxxxxxxxxxx --- -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-git-token diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_init.yaml b/infra/feast-operator/config/samples/v1_featurestore_init.yaml similarity index 81% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_init.yaml rename to infra/feast-operator/config/samples/v1_featurestore_init.yaml index f2324eeab2d..a5ab73de981 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_init.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_init.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-init diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml b/infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml similarity index 91% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml rename to infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml index 33225b2edfb..a14b55d873d 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_kubernetes_auth.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_kubernetes_auth.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-kubernetes-auth diff --git a/infra/feast-operator/config/samples/v1_featurestore_materialization_openlineage.yaml b/infra/feast-operator/config/samples/v1_featurestore_materialization_openlineage.yaml new file mode 100644 index 00000000000..f2fa585b1f8 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_materialization_openlineage.yaml @@ -0,0 +1,53 @@ +apiVersion: v1 +kind: Secret +metadata: + name: openlineage-secret + namespace: feast +stringData: + # api_key is read from this Secret and written into feature_store.yaml + api_key: "your-marquez-api-key" #pragma: allowlist secret +--- +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-materialization-openlineage + namespace: feast +spec: + feastProject: my_project + # materialization controls how features are written to the online store. + # Written into feature_store.yaml for all service pods. + materialization: + # onlineWriteBatchSize limits rows per batch to prevent OOM during materialization. + # Supported by local, Spark, and Ray engines. + onlineWriteBatchSize: 10000 + # extraConfig passes additional materialization settings inline into feature_store.yaml. + # Use for fields not typed above (e.g. pull_latest_features) or fields added + # in newer Feast SDK versions without waiting for an operator update. + # Boolean ("true"/"false") and integer strings are coerced to native YAML types. + # extraConfig: + # pull_latest_features: "false" + # openlineage emits data lineage events during feast apply and materialization. + openlineage: + enabled: true + transportType: http + # transportUrl is the base URL of your Marquez or OpenLineage-compatible server. + transportUrl: "http://marquez.feast.svc.cluster.local:5000" + transportEndpoint: "api/v1/lineage" + # apiKeySecretRef references a Secret with key "api_key" for bearer auth. + apiKeySecretRef: + name: openlineage-secret + # extraConfig passes any additional OpenLineage settings inline into feature_store.yaml. + # Use it for non-core options (namespace, producer, emit_on_apply, + # emit_on_materialize) and transport-specific settings (e.g. kafka + # bootstrap_servers, topic, sasl_mechanism; file path). + # Boolean values ("true"/"false") and integers are automatically coerced to + # their native YAML types so Feast Pydantic validators accept them. + extraConfig: + namespace: "my-feast-project" + producer: "feast-operator" + emit_on_apply: "true" + emit_on_materialize: "true" + # kafka transport example: + # bootstrap_servers: "kafka.svc:9092" + # topic: "openlineage" + # sasl_mechanism: "PLAIN" diff --git a/infra/feast-operator/config/samples/v1_featurestore_mcp.yaml b/infra/feast-operator/config/samples/v1_featurestore_mcp.yaml new file mode 100644 index 00000000000..67dd1c6947d --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_mcp.yaml @@ -0,0 +1,26 @@ +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-mcp +spec: + feastProject: my_project + services: + onlineStore: + server: {} + # serving.mcp switches the feature server to MCP (Model Context Protocol) mode. + # When mcp is set, the feature_server type in feature_store.yaml is "mcp". + # MCP enables LLM-friendly tool-based access to Feast feature views. + serving: + mcp: + enabled: true + serverName: feast-mcp-server + serverVersion: "1.0.0" + # transport can be "sse" (default) or "http" + transport: sse + registry: + local: + server: + # MCP on the registry requires REST API to be enabled. + restAPI: true + mcp: + enabled: true diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_objectstore_persistence.yaml b/infra/feast-operator/config/samples/v1_featurestore_objectstore_persistence.yaml similarity index 80% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_objectstore_persistence.yaml rename to infra/feast-operator/config/samples/v1_featurestore_objectstore_persistence.yaml index 2146dabe85c..b445598c9ac 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_objectstore_persistence.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_objectstore_persistence.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-s3-registry @@ -10,6 +10,8 @@ spec: persistence: file: path: s3://bucket/registry.db + cache_ttl_seconds: 60 + cache_mode: sync s3_additional_kwargs: ServerSideEncryption: AES256 ACL: bucket-owner-full-control diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_oidc_auth.yaml b/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml similarity index 58% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_oidc_auth.yaml rename to infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml index 54660a5c232..97f325bb0bd 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_oidc_auth.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_oidc_auth.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-oidc-auth @@ -19,3 +19,7 @@ stringData: client_secret: client_secret username: username password: password + # Optional: enable audience/issuer claim verification on the servers. + # Values must match the claims in the tokens your IdP issues. + # audience: api://feast-feature-server + # issuer: https://idp.example.com/realms/feast diff --git a/infra/feast-operator/config/samples/v1_featurestore_openlineage_consumer.yaml b/infra/feast-operator/config/samples/v1_featurestore_openlineage_consumer.yaml new file mode 100644 index 00000000000..9e242896135 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_openlineage_consumer.yaml @@ -0,0 +1,63 @@ +apiVersion: v1 +kind: Secret +metadata: + name: openlineage-producer-secret + namespace: feast +stringData: + api_key: "your-marquez-api-key" #pragma: allowlist secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: openlineage-consumer-secret + namespace: feast +stringData: + api_key: "consumer-api-key-for-producers" #pragma: allowlist secret +--- +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-openlineage-consumer + namespace: feast +spec: + feastProject: my_project + services: + registry: + local: + persistence: + store: + type: sql + secretRef: + name: registry-db-secret + openlineage: + enabled: true + transportType: http + transportUrl: "http://localhost:8080/api" + transportEndpoint: "v1/lineage" + apiKeySecretRef: + name: openlineage-producer-secret + extraConfig: + namespace: "my_project" + producer: "feast-operator" + emit_on_apply: "true" + emit_on_materialize: "true" + # consumer enables Feast as an OpenLineage event receiver. + # External producers (Airflow, Spark, dbt) can POST events to + # the Feast REST server at POST /api/v1/lineage. + # The Feast UI then displays lineage from all producers in + # Registry, OpenLineage, and Merged views. + consumer: + enabled: true + storeType: sql + # Optional: use a separate database for lineage storage. + # If omitted, the SQL registry database is reused. + # connectionStringSecretRef: + # name: lineage-db-secret + apiKeySecretRef: + name: openlineage-consumer-secret + # namespaceMapping maps OL namespaces to Feast projects + # for RBAC-scoped visibility in the UI. + namespaceMapping: + airflow_production: my_project + spark_etl: my_project + dbt_analytics: my_project diff --git a/infra/feast-operator/config/samples/v1_featurestore_packaged.yaml b/infra/feast-operator/config/samples/v1_featurestore_packaged.yaml new file mode 100644 index 00000000000..4b334b9d3d8 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_packaged.yaml @@ -0,0 +1,10 @@ +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-packaged +spec: + feastProject: sample_packaged + feastProjectDir: + packaged: + image: registry.example.com/feature-server@sha256:0123456789abcdef + featureRepoPath: /opt/feast/feature_repo diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_db_volumes_tls.yaml b/infra/feast-operator/config/samples/v1_featurestore_postgres_db_volumes_tls.yaml similarity index 98% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_db_volumes_tls.yaml rename to infra/feast-operator/config/samples/v1_featurestore_postgres_db_volumes_tls.yaml index 61add153716..307a4a1e3aa 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_db_volumes_tls.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_postgres_db_volumes_tls.yaml @@ -33,7 +33,7 @@ stringData: sslcert_path: /var/lib/postgresql/certs/tls.crt sslrootcert_path: /var/lib/postgresql/certs/ca.crt --- -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-db-ssl diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_tls_volumes_ca_env.yaml b/infra/feast-operator/config/samples/v1_featurestore_postgres_tls_volumes_ca_env.yaml similarity index 98% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_tls_volumes_ca_env.yaml rename to infra/feast-operator/config/samples/v1_featurestore_postgres_tls_volumes_ca_env.yaml index 42e1ae4b4a6..07bd98f2662 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_postgres_tls_volumes_ca_env.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_postgres_tls_volumes_ca_env.yaml @@ -34,7 +34,7 @@ stringData: sslcert_path: /var/lib/postgresql/certs/tls.crt sslrootcert_path: system --- -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-db-ssl diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_pvc_persistence.yaml b/infra/feast-operator/config/samples/v1_featurestore_pvc_persistence.yaml similarity index 97% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_pvc_persistence.yaml rename to infra/feast-operator/config/samples/v1_featurestore_pvc_persistence.yaml index 15aa46c456c..679eadacde7 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_pvc_persistence.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_pvc_persistence.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-pvc-persistence diff --git a/infra/feast-operator/config/samples/v1_featurestore_scaling_hpa.yaml b/infra/feast-operator/config/samples/v1_featurestore_scaling_hpa.yaml new file mode 100644 index 00000000000..af4a9fd1d02 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_scaling_hpa.yaml @@ -0,0 +1,78 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret + namespace: test + labels: + app: postgres +stringData: + POSTGRES_DB: feast + POSTGRES_USER: feast + POSTGRES_PASSWORD: feast # pragma: allowlist secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: feast-data-stores + namespace: test +stringData: + sql: | + path: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres.test.svc.cluster.local:5432/${POSTGRES_DB} + cache_ttl_seconds: 60 + sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true + postgres: | + host: postgres.test.svc.cluster.local + port: 5432 + database: ${POSTGRES_DB} + db_schema: public + user: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} +--- +# HPA autoscaling: 2-10 replicas with DB-backed persistence and HA +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-scaling-hpa + namespace: test +spec: + feastProject: my_project + services: + scaling: + autoscaling: + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + podDisruptionBudgets: + maxUnavailable: 1 + onlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-data-stores + server: + envFrom: + - secretRef: + name: postgres-secret + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-data-stores diff --git a/infra/feast-operator/config/samples/v1_featurestore_scaling_static.yaml b/infra/feast-operator/config/samples/v1_featurestore_scaling_static.yaml new file mode 100644 index 00000000000..e4df5a6245a --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_scaling_static.yaml @@ -0,0 +1,75 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret + namespace: test + labels: + app: postgres +stringData: + POSTGRES_DB: feast + POSTGRES_USER: feast + POSTGRES_PASSWORD: feast # pragma: allowlist secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: feast-data-stores + namespace: test +stringData: + sql: | + path: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres.test.svc.cluster.local:5432/${POSTGRES_DB} + cache_ttl_seconds: 60 + sqlalchemy_config_kwargs: + echo: false + pool_pre_ping: true + postgres: | + host: postgres.test.svc.cluster.local + port: 5432 + database: ${POSTGRES_DB} + db_schema: public + user: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} +--- +# Static scaling: 3 replicas with DB-backed persistence, PDB, and HA +# +# By default the operator auto-injects: +# - Soft pod anti-affinity (prefer different nodes) +# - Soft zone topology spread (prefer different zones, e.g. us-east-1a, us-east-1b, us-east-1c) +# +# To enforce strict zone spreading on AWS (DoNotSchedule), uncomment topologySpreadConstraints below. +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-scaling-static + namespace: test +spec: + feastProject: my_project + replicas: 3 + services: + podDisruptionBudgets: + maxUnavailable: 1 + # Uncomment to enforce strict spreading across AWS availability zones: + # topologySpreadConstraints: + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # whenUnsatisfiable: DoNotSchedule + # labelSelector: + # matchLabels: + # feast.dev/name: sample-scaling-static + onlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-data-stores + server: + envFrom: + - secretRef: + name: postgres-secret + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-data-stores diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_services_loglevel.yaml b/infra/feast-operator/config/samples/v1_featurestore_services_loglevel.yaml similarity index 91% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_services_loglevel.yaml rename to infra/feast-operator/config/samples/v1_featurestore_services_loglevel.yaml index e738e6352be..473beea200b 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_services_loglevel.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_services_loglevel.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-services-loglevel diff --git a/infra/feast-operator/config/samples/v1_featurestore_serving.yaml b/infra/feast-operator/config/samples/v1_featurestore_serving.yaml new file mode 100644 index 00000000000..412499412e6 --- /dev/null +++ b/infra/feast-operator/config/samples/v1_featurestore_serving.yaml @@ -0,0 +1,34 @@ +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: sample-serving +spec: + feastProject: my_project + services: + onlineStore: + server: {} + # serving configures the Feast feature_server section in feature_store.yaml. + # It controls fine-grained metrics and offline push batching. + # This section only applies to the online feature server (feast serve). + serving: + metrics: + enabled: true + # categories selectively enables or disables Feast metric categories. + # All categories default to true when metrics is enabled. + # Keys must match Feast MetricsConfig field names for your SDK version. + # When a newer Feast SDK adds a metric category you can toggle it here + # immediately — no operator update needed, the key passes through to + # feature_store.yaml and is validated by the running Feast SDK. + categories: + resource: true # CPU / memory gauges + request: true # per-endpoint latency and request counters + online_features: true # online feature retrieval metrics + push: true # push/write request counters + materialization: true # materialization counters and duration histograms + freshness: false # feature freshness gauges (can be expensive at scale) + offline_features: true # offline store retrieval counters, latency, row count + audit_logging: false # structured JSON audit logs via the feast.audit logger + offlinePushBatching: + enabled: true + batchSize: 1000 # max rows per offline write batch + batchIntervalSeconds: 10 # flush batch every 10 s diff --git a/infra/feast-operator/config/samples/v1alpha1_featurestore_with_ui.yaml b/infra/feast-operator/config/samples/v1_featurestore_with_ui.yaml similarity index 76% rename from infra/feast-operator/config/samples/v1alpha1_featurestore_with_ui.yaml rename to infra/feast-operator/config/samples/v1_featurestore_with_ui.yaml index 78c25e677ff..65a88508629 100644 --- a/infra/feast-operator/config/samples/v1alpha1_featurestore_with_ui.yaml +++ b/infra/feast-operator/config/samples/v1_featurestore_with_ui.yaml @@ -1,4 +1,4 @@ -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: sample-ui diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 102abd70e3b..be85a29a7b2 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -11,7 +11,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.18.0 name: featurestores.feast.dev spec: group: feast.dev @@ -31,6 +31,12832 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date + name: v1 + schema: + openAPIV3Schema: + description: FeatureStore is the Schema for the featurestores API + properties: + apiVersion: + description: APIVersion defines the versioned schema of this representation + of an object. + type: string + kind: + description: Kind is a string value representing the REST resource this + object represents. + type: string + metadata: + type: object + spec: + description: FeatureStoreSpec defines the desired state of FeatureStore + properties: + authz: + description: AuthzConfig defines the authorization settings for the + deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed in the + same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM certificate. + Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string + secretRef: + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + batchEngine: + description: BatchEngineConfig defines the batch compute engine configuration. + properties: + configMapKey: + description: Key name in the ConfigMap. Defaults to "config" if + not specified. + type: string + configMapRef: + description: Reference to a ConfigMap containing the batch engine + configuration. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + cronJob: + description: FeastCronJob defines a CronJob to execute against a Feature + Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions of a + Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings for + the CronJob + properties: + commands: + description: Array of commands to be executed (in order) against + a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + type: object + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. Value + must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. + properties: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system... + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. + properties: + rules: + description: A list of pod failure policy rules. The rules + are evaluated in order. + items: + description: PodFailurePolicyRule describes how a pod + failure is handled when the requirements are met. + properties: + action: + description: Specifies the action taken on a pod + failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the container + exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod condition + status. + type: string + type: + description: Specifies the required Pod condition + type. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer + type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string + type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. + properties: + git: + description: GitCloneOptions describes how a clone should be performed. + properties: + configs: + additionalProperties: + type: string + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the feature + repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - couchbase + - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') + type: object + x-kubernetes-validations: + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints for + the FeatureStore deployment. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. + properties: + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be unavailable + during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or "RollingUpdate". + Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures the + file-based persistence for the offline store service + properties: + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string + type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + - oracle + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a remote offline server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the online store service + properties: + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service + properties: + file: + description: OnlineStoreFilePersistence configures the + file-based persistence for the online store service + properties: + path: + type: string + pvc: + description: PvcConfig defines the settings for a + persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent volume + access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which this + persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref and + create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and must + not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: object + x-kubernetes-validations: + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : true' + - message: PVC path must be a file name only, with no + slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: Online store does not support S3 or GS buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the secret + key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you want + to use. + enum: + - snowflake.online + - redis + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + - mongodb + - aerospike + - scylladb + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + serving: + description: Serving configures the Feast feature_server section + written into feature_store.yaml for the online serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. Defaults + to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults to + "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or disables + individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to the + offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to the + offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline write + batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied to the + Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true + type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' + registry: + description: Registry configures the registry service. One selection + is required. Local is the default setting. + properties: + local: + description: LocalRegistryConfig configures the registry service + properties: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures the + file-based persistence for the registry service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-validations: + - message: Registry files must use absolute paths + or be S3 ('s3://') or GS ('gs://') object store + URIs. + rule: '(!has(self.pvc) && has(self.path)) ? (self.path.startsWith(''/'') + || self.path.startsWith(''s3://'') || self.path.startsWith(''gs://'')) + : true' + - message: PVC path must be a file name only, with + no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: PVC persistence does not support S3 or + GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available only + for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be true + rule: self.restAPI == true || self.grpc == true || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` CR + in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for the + client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap where + the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the FeatureStore + deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: scaleUp is scaling policy for scaling + Up. + properties: + policies: + description: policies is a list of potential scaling + polices which can be used during scaling. + items: + description: HPAScalingPolicy is a single policy + which must hold true for a specified past + interval. + properties: + periodSeconds: + description: periodSeconds specifies the + window of time for which the policy should + hold true. + format: int32 + type: integer + type: + description: type is used to specify the + scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + description: MaxReplicas is the upper limit for the number + of replicas. Required. + format: int32 + minimum: 1 + type: integer + metrics: + description: Metrics contains the specifications for which + to use to calculate the desired replica count. + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at... + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes... + properties: + container: + description: container is the name of the container + in the pods of the scaling target + type: string + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the descriptions + of a object,such as kind,name apiVersion + properties: + apiVersion: + description: apiVersion is the API version + of the referent + type: string + kind: + description: 'kind is the kind of the referent; + More info: https://git.k8s.' + type: string + name: + description: 'name is the name of the referent; + More info: https://kubernetes.' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example,... + properties: + metric: + description: metric identifies the target metric + by name and selector + properties: + name: + description: name is the name of the given + metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing... + properties: + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target value + for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the + metric type is Utilization, Value, or + AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of + the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: type is the type of metric source. + type: string + required: + - type + type: object + type: array + minReplicas: + description: MinReplicas is the lower limit for the number + of replicas. Defaults to 1. + format: int32 + minimum: 1 + type: integer + required: + - maxReplicas + type: object + type: object + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to use + by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile loaded + on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor profile + will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies to + all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as a non-root + user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string + seLinuxOptions: + description: The SELinux context to be applied to all containers. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile defined + in a file on the node should be used. + type: string + type: + description: type indicates which kind of seccomp profile + will be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string + sysctls: + description: Sysctls hold a list of namespaced sysctls used + for the pod. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + description: The Windows specific settings applied to all + containers. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container should + be run as a 'Host Process' container. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. + type: string + type: object + type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are spread + across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of eligible + domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + ui: + description: Creates a UI server container + properties: + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name of + each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. useful + in an openshift cluster, for example, where TLS is configured + by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key names + for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where the + TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes that + should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from which + the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * CPU + + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + volumes: + description: Volumes specifies the volumes to mount in the FeatureStore + deployment. + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to... + properties: + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes. + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes. + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk mount + on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk in + the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in the + blob storage + type: string + fsType: + default: ext4 + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage...' + type: string + readOnly: + default: false + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the host + that shares a pod's lifetime. + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s. + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile is + the path to key ring for User, default is /etc/ceph/user.' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is reference + to the authentication secret for User, default is + empty.' + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s. + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + Deprecated: Cinder is deprecated. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits used + to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used + to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers. + properties: + driver: + description: driver is the name of the CSI driver that + handles this volume. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", "ntfs". + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about the + pod that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created + files by default.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the + pod: only annotations, labels, name, namespace + and uid are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must not + be absolute or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes. + properties: + medium: + description: medium represents what type of storage + medium should back this directory. + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: sizeLimit is the total amount of local + storage required for this EmptyDir volume. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: ephemeral represents a volume that is handled + by a cluster storage driver. + properties: + volumeClaimTemplate: + description: Will be used to create a stand-alone PVC + to provision the volume. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. + type: object + spec: + description: The specification for the PersistentVolumeClaim. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s. + properties: + apiGroup: + description: APIGroup is the group for the + resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. + properties: + apiGroup: + description: APIGroup is the group for the + resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking. + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum + resources the volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may be + used to set the VolumeAttributesClass used + by this claim. + type: string + volumeMode: + description: volumeMode defines what type of + volume is required by the claim. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource that + is attached to a kubelet's host machine and then exposed + to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to use + for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as... + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the... + properties: + fsType: + description: fsType is filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes. + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes. + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + Deprecated: GitRepo is deprecated. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. + properties: + endpoints: + description: endpoints is the endpoint name that details + Glusterfs topology. + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + default: default + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal List. + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: targetPortal is iSCSI Target Portal. + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes. + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes. + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes. + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + properties: + claimName: + description: claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + pdID: + description: pdID is the ID that identifies Photon Controller + persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources secrets, + configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used to set + permissions on created files by default. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: ClusterTrustBundle allows a pod to + access the `.spec. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. + type: string + required: + - path + type: object + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode + bits used to set permissions on this + file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name, namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file to + be created. Must not be absolute or + contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object + secret: + description: secret information about the secret + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode + bits used to set permissions on this + file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: audience is the intended audience + of the token. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + description: quobyte represents a Quobyte mount on the host + that shares a pod's lifetime. + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple... + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set... + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. + properties: + fsType: + description: fsType is the filesystem type of the volume + that you want to mount. + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + default: /etc/ceph/keyring + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s. + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s. + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + default: xfs + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + default: ThinProvisioned + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes. + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits used + to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits used + to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes. + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope of + the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine. + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + x-kubernetes-validations: + - message: replicas > 1 and services.scaling.autoscaling are mutually + exclusive. + rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling) + - message: Scaling requires DB-backed persistence for the online store. + Configure services.onlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) + && has(self.services.onlineStore.persistence.store)) + - message: Scaling requires DB-backed persistence for the offline store. + Configure services.offlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (!has(self.services) + || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) + && has(self.services.offlineStore.persistence.store))) + - message: Scaling requires DB-backed or remote registry. Configure registry.local.persistence.store + or use a remote registry when using replicas > 1 or autoscaling. S3/GCS-backed + registry is also allowed. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.registry) && (has(self.services.registry.remote) + || (has(self.services.registry.local) && has(self.services.registry.local.persistence) + && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) + && has(self.services.registry.local.persistence.file.path) && (self.services.registry.local.persistence.file.path.startsWith('s3://') + || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + properties: + applied: + description: Shows the currently applied feast configuration, including + any pertinent defaults + properties: + authz: + description: AuthzConfig defines the authorization settings for + the deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed + in the same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + caCertConfigMap: + description: ConfigMap with the CA certificate for self-signed + OIDC providers. Auto-detected on RHOAI/ODH. + properties: + key: + description: Key in the ConfigMap holding the PEM + certificate. Defaults to "ca-bundle.crt". + type: string + name: + description: ConfigMap name. + type: string + required: + - name + type: object + issuerUrl: + description: OIDC issuer URL. The operator appends /.well-known/openid-configuration + to derive the discovery endpoint. + pattern: ^https://\S+$ + type: string + secretKeyName: + description: Key in the Secret containing all OIDC properties + as a YAML value. If unset, each key is a property. + type: string + secretRef: + description: Secret with OIDC properties (auth_discovery_url, + client_id, client_secret). issuerUrl takes precedence. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + tokenEnvVar: + description: Env var name for client pods to read an OIDC + token from. Sets token_env_var in client config. + type: string + verifySSL: + description: Verify SSL certificates for the OIDC provider. + Defaults to true. + type: boolean + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + batchEngine: + description: BatchEngineConfig defines the batch compute engine + configuration. + properties: + configMapKey: + description: Key name in the ConfigMap. Defaults to "config" + if not specified. + type: string + configMapRef: + description: Reference to a ConfigMap containing the batch + engine configuration. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + annotations: + additionalProperties: + type: string + description: Annotations to be added to the CronJob metadata. + type: object + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + type: object + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. + properties: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system... + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. + properties: + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer + type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string + type: object + dataQualityMonitoring: + description: DataQualityMonitoring configures Data Quality Monitoring + behaviour. + properties: + autoBaseline: + default: true + description: AutoBaseline controls whether baseline distribution + is computed automatically on feast apply. Defaults to true. + type: boolean + type: object + feastProject: + description: FeastProject is the Feast project id. + pattern: ^[A-Za-z0-9][A-Za-z0-9_-]*$ + type: string + feastProjectDir: + description: FeastProjectDir defines how to create the feast project + directory. + properties: + git: + description: GitCloneOptions describes how a clone should + be performed. + properties: + configs: + additionalProperties: + type: string + description: |- + Configs passed to git via `-c` + e.g. http.sslVerify: 'false' + OR 'url."https://api:\${TOKEN}@github.com/". + type: object + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + featureRepoPath: + description: FeatureRepoPath is the relative path to the + feature repo subdirectory. Default is 'feature_repo'. + type: string + ref: + description: Reference to a branch / tag / commit + type: string + url: + description: The repository URL to clone from. + type: string + required: + - url + type: object + x-kubernetes-validations: + - message: RepoPath must be a file name only, with no slashes. + rule: 'has(self.featureRepoPath) ? !self.featureRepoPath.startsWith(''/'') + : true' + init: + description: FeastInitOptions defines how to run a `feast + init`. + properties: + minimal: + type: boolean + template: + description: Template for the created project + enum: + - local + - gcp + - aws + - snowflake + - spark + - postgres + - hbase + - cassandra + - hazelcast + - couchbase + - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') + type: object + x-kubernetes-validations: + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' + materialization: + description: |- + Materialization controls feature materialization behavior (batch size, pull strategy). + Written into feature_store. + properties: + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig passes additional materialization key-value settings inline into + feature_store.yaml. + type: object + onlineWriteBatchSize: + description: |- + Number of rows per batch when writing to the online store during materialization. + Prevents OOM for large feature views. + format: int32 + minimum: 1 + type: integer + type: object + openlineage: + description: |- + OpenLineage enables OpenLineage data lineage tracking for Feast operations. + Written into feature_store. + properties: + apiKeySecretRef: + description: Reference to a Secret containing the key "api_key" + for lineage server authentication. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + consumer: + description: |- + Consumer configures the OpenLineage consumer (event receiver) that enables + Feast to receive and display lineage from... + properties: + apiKeySecretRef: + description: |- + Reference to a Secret containing the key "api_key" that producers must + provide in the X-API-Key header when sending... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + connectionStringSecretRef: + description: |- + Reference to a Secret containing the key "connection_string" for a separate + lineage database. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + enabled: + description: Enable the OpenLineage consumer. + type: boolean + namespaceMapping: + additionalProperties: + type: string + description: |- + NamespaceMapping maps OpenLineage namespaces to Feast projects for + RBAC-based filtering of lineage data in the UI. + type: object + storeType: + default: sql + description: StoreType is the storage backend for lineage + events. Currently only "sql" is supported. + enum: + - sql + type: string + required: + - enabled + type: object + enabled: + description: Enable OpenLineage integration. + type: boolean + extraConfig: + additionalProperties: + type: string + description: |- + ExtraConfig holds additional OpenLineage key-value settings written inline into + the openlineage block of feature_store. + type: object + transportEndpoint: + description: API endpoint path appended to transportUrl. Defaults + to "api/v1/lineage". + type: string + transportType: + description: Transport type for lineage events. + enum: + - http + - console + - file + - kafka + type: string + transportUrl: + description: URL for HTTP transport (e.g. http://marquez:5000). + Required when transportType is "http". + type: string + required: + - enabled + type: object + replicas: + default: 1 + description: |- + Replicas is the desired number of pod replicas. Used by the scale sub-resource. + Mutually exclusive with services. + format: int32 + minimum: 1 + type: integer + services: + description: FeatureStoreServices defines the desired feast services. + An ephemeral onlineStore feature server is deployed by default. + properties: + affinity: + description: Affinity defines the pod scheduling constraints + for the FeatureStore deployment. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). + properties: + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in the + range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, + etc. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field,... + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled... + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should... + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in... + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + deploymentStrategy: + description: DeploymentStrategy describes how to replace existing + pods with new ones. + properties: + rollingUpdate: + description: |- + Rolling update config params. Present only if DeploymentStrategyType = + RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: The maximum number of pods that can be + unavailable during the update. + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or + "RollingUpdate". Default is RollingUpdate. + type: string + type: object + disableInitContainers: + description: Disable the 'feast repo initialization' initContainer + type: boolean + initImage: + description: InitImage overrides the image for init containers + (feast-init, feast-apply). + type: string + offlineStore: + description: OfflineStore configures the offline store service + properties: + persistence: + description: OfflineStorePersistence configures the persistence + settings for the offline store service + properties: + file: + description: OfflineStoreFilePersistence configures + the file-based persistence for the offline store + service + properties: + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: + enum: + - file + - dask + - duckdb + type: string + type: object + store: + description: OfflineStoreDBStorePersistence configures + the DB store persistence for the offline store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - snowflake.offline + - bigquery + - redshift + - spark + - postgres + - trino + - athena + - mssql + - couchbase.offline + - clickhouse + - ray + - oracle + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a remote offline server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + type: object + onlineStore: + description: OnlineStore configures the online store service + properties: + disabled: + description: |- + Disabled skips deploying the online store service entirely, including its + serving pod and persistence. + type: boolean + persistence: + description: OnlineStorePersistence configures the persistence + settings for the online store service + properties: + file: + description: OnlineStoreFilePersistence configures + the file-based persistence for the online store + service + properties: + path: + type: string + pvc: + description: PvcConfig defines the settings for + a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the storage + resource requirements for a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + storageClassName: + description: StorageClassName is the name + of an existing StorageClass to which + this persistent volume belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between ref + and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' and + must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + type: object + x-kubernetes-validations: + - message: Ephemeral stores must have absolute paths. + rule: '(!has(self.pvc) && has(self.path)) ? self.path.startsWith(''/'') + : true' + - message: PVC path must be a file name only, with + no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: Online store does not support S3 or GS + buckets. + rule: 'has(self.path) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + store: + description: OnlineStoreDBStorePersistence configures + the DB store persistence for the online store service + properties: + secretKeyName: + description: By default, the selected store "type" + is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should be placed + as-is from the "feature_store.yaml" under the + secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type you + want to use. + enum: + - snowflake.online + - redis + - datastore + - dynamodb + - bigtable + - postgres + - cassandra + - mysql + - hazelcast + - singlestore + - hbase + - elasticsearch + - qdrant + - couchbase.online + - milvus + - hybrid + - mongodb + - aerospike + - scylladb + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a feature server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, + defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults + to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to + select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the + name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for + a feast service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, + where TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of + a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should be + mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + serving: + description: Serving configures the Feast feature_server + section written into feature_store.yaml for the online + serve pod. + properties: + mcp: + description: Mcp enables MCP (Model Context Protocol) + server support. When set, feature server type is + "mcp". + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: |- + Metrics configures per-category Prometheus metrics for the feature server. + Coexists with the server. + properties: + categories: + additionalProperties: + type: boolean + description: Categories selectively enables or + disables individual Feast metric categories. + type: object + enabled: + description: Enable the Prometheus metrics endpoint + on port 8000. + type: boolean + required: + - enabled + type: object + offlinePushBatching: + description: OfflinePushBatching batches writes to + the offline store via the /push endpoint. + properties: + batchIntervalSeconds: + description: Seconds between batch flushes to + the offline store. + format: int32 + minimum: 1 + type: integer + batchSize: + description: Maximum number of rows per offline + write batch. + format: int32 + minimum: 1 + type: integer + enabled: + description: Enable offline push batching. + type: boolean + required: + - enabled + type: object + type: object + type: object + podAnnotations: + additionalProperties: + type: string + description: PodAnnotations are annotations to be applied + to the Deployment's PodTemplate metadata. + type: object + podDisruptionBudgets: + description: PodDisruptionBudgets configures a PodDisruptionBudget + for the FeatureStore deployment. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: MaxUnavailable specifies the maximum number/percentage + of pods that can be unavailable. + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: MinAvailable specifies the minimum number/percentage + of pods that must remain available. + x-kubernetes-int-or-string: true + type: object + x-kubernetes-validations: + - message: Exactly one of minAvailable or maxUnavailable must + be set. + rule: '[has(self.minAvailable), has(self.maxUnavailable)].exists_one(c, + c)' + registry: + description: Registry configures the registry service. One + selection is required. Local is the default setting. + properties: + local: + description: LocalRegistryConfig configures the registry + service + properties: + persistence: + description: RegistryPersistence configures the persistence + settings for the registry service + properties: + file: + description: RegistryFilePersistence configures + the file-based persistence for the registry + service + properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL + (in seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer + path: + type: string + pvc: + description: PvcConfig defines the settings + for a persistent file store based on PVCs. + properties: + create: + description: Settings for creating a new + PVC + properties: + accessModes: + description: AccessModes k8s persistent + volume access modes. Defaults to + ["ReadWriteOnce"]. + items: + type: string + type: array + resources: + description: Resources describes the + storage resource requirements for + a volume. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes + the minimum amount of compute + resources required. + type: object + type: object + storageClassName: + description: StorageClassName is the + name of an existing StorageClass + to which this persistent volume + belongs. + type: string + type: object + x-kubernetes-validations: + - message: PvcCreate is immutable + rule: self == oldSelf + mountPath: + description: |- + MountPath within the container at which the volume should be mounted. + Must start by "/" and cannot contain ':'. + type: string + ref: + description: Reference to an existing + field + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - mountPath + type: object + x-kubernetes-validations: + - message: One selection is required between + ref and create. + rule: '[has(self.ref), has(self.create)].exists_one(c, + c)' + - message: Mount path must start with '/' + and must not contain ':' + rule: self.mountPath.matches('^/[^:]*$') + s3_additional_kwargs: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-validations: + - message: Registry files must use absolute paths + or be S3 ('s3://') or GS ('gs://') object + store URIs. + rule: '(!has(self.pvc) && has(self.path)) ? + (self.path.startsWith(''/'') || self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: PVC path must be a file name only, + with no slashes. + rule: '(has(self.pvc) && has(self.path)) ? !self.path.startsWith(''/'') + : true' + - message: PVC persistence does not support S3 + or GS object store URIs. + rule: '(has(self.pvc) && has(self.path)) ? !(self.path.startsWith(''s3://'') + || self.path.startsWith(''gs://'')) : true' + - message: Additional S3 settings are available + only for S3 object store URIs. + rule: '(has(self.s3_additional_kwargs) && has(self.path)) + ? self.path.startsWith(''s3://'') : true' + store: + description: RegistryDBStorePersistence configures + the DB store persistence for the registry service + properties: + secretKeyName: + description: By default, the selected store + "type" is used as the SecretKeyName + type: string + secretRef: + description: Data store parameters should + be placed as-is from the "feature_store.yaml" + under the secret key. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type of the persistence type + you want to use. + enum: + - sql + - snowflake.registry + type: string + required: + - secretRef + - type + type: object + type: object + x-kubernetes-validations: + - message: One selection required between file or + store. + rule: '[has(self.file), has(self.store)].exists_one(c, + c)' + server: + description: Creates a registry server container + properties: + env: + items: + description: EnvVar represents an environment + variable present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment + variable's value. Cannot be used if value + is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the + ConfigMap or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the + pod: supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret + in the pod's namespace + properties: + key: + description: The key of the secret + to select from. Must be a valid + secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the + Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source + of a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to + the name of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + grpc: + description: Enable gRPC registry server. Defaults + to true if unset. + type: boolean + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for + if/when to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + mcp: + description: |- + Mcp enables MCP (Model Context Protocol) on the REST registry server. + Requires restAPI to be true. + properties: + enabled: + description: Enable the MCP server. + type: boolean + serverName: + description: MCP server name for identification. + Defaults to "feast-mcp-server". + type: string + serverVersion: + description: MCP server version string. Defaults + to "1.0.0". + type: string + transport: + description: MCP transport protocol. + enum: + - sse + - http + type: string + required: + - enabled + type: object + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the + compute resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one + entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum + amount of compute resources required. + type: object + type: object + restAPI: + description: Enable REST API registry server. + type: boolean + tls: + description: TlsConfigs configures server TLS + for a feast service. + properties: + disable: + description: will disable TLS for the feast + service. useful in an openshift cluster, + for example, where TLS is configured by + default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret + key names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret + where the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` + is false.' + rule: '(!has(self.disable) || !self.disable) + ? has(self.secretRef) : true' + volumeMounts: + description: VolumeMounts defines the list of + volumes that should be mounted into the feast + container. + items: + description: VolumeMount describes a mounting + of a Volume within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of + a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume + from which the container's volume should + be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker + configuration for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker + processes. Use -1 to auto-calculate based + on CPU cores (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + x-kubernetes-validations: + - message: At least one of restAPI or grpc must be + true + rule: self.restAPI == true || self.grpc == true + || !has(self.grpc) + - message: MCP requires restAPI to be true + rule: '!has(self.mcp) || !self.mcp.enabled || (has(self.restAPI) + && self.restAPI == true)' + type: object + remote: + description: RemoteRegistryConfig points to a remote feast + registry server. + properties: + feastRef: + description: Reference to an existing `FeatureStore` + CR in the same k8s cluster. + properties: + name: + description: Name of the FeatureStore + type: string + namespace: + description: Namespace of the FeatureStore + type: string + required: + - name + type: object + hostname: + description: Host address of the remote registry service + - :, e.g. `registry..svc.cluster.local:80` + type: string + tls: + description: TlsRemoteRegistryConfigs configures client + TLS for a remote feast registry. + properties: + certName: + description: defines the configmap key name for + the client TLS cert. + type: string + configMapRef: + description: references the local k8s configmap + where the TLS cert resides + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - certName + - configMapRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.hostname), has(self.feastRef)].exists_one(c, + c)' + type: object + x-kubernetes-validations: + - message: One selection required. + rule: '[has(self.local), has(self.remote)].exists_one(c, + c)' + resourceClaims: + description: |- + ResourceClaims defines which ResourceClaims must be allocated + and reserved before the Pod is allowed to start. + items: + description: |- + PodResourceClaim references exactly one ResourceClaim, either directly + or by naming a ResourceClaimTemplate which is... + properties: + name: + description: |- + Name uniquely identifies this resource claim inside the pod. + This must be a DNS_LABEL. + type: string + resourceClaimName: + description: |- + ResourceClaimName is the name of a ResourceClaim object in the same + namespace as this pod. + type: string + resourceClaimTemplateName: + description: |- + ResourceClaimTemplateName is the name of a ResourceClaimTemplate + object in the same namespace as this pod. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean + scaling: + description: Scaling configures horizontal scaling for the + FeatureStore deployment (e.g. HPA autoscaling). + properties: + autoscaling: + description: |- + Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. + Mutually exclusive with spec.replicas. + properties: + behavior: + description: Behavior configures the scaling behavior + of the target. + properties: + scaleDown: + description: scaleDown is scaling policy for scaling + Down. + properties: + policies: + description: policies is a list of potential + scaling polices which can be used during + scaling. + items: + description: HPAScalingPolicy is a single + policy which must hold true for a specified + past interval. + properties: + periodSeconds: + description: periodSeconds specifies + the window of time for which the policy + should hold true. + format: int32 + type: integer + type: + description: type is used to specify + the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: scaleUp is scaling policy for scaling + Up. + properties: + policies: + description: policies is a list of potential + scaling polices which can be used during + scaling. + items: + description: HPAScalingPolicy is a single + policy which must hold true for a specified + past interval. + properties: + periodSeconds: + description: periodSeconds specifies + the window of time for which the policy + should hold true. + format: int32 + type: integer + type: + description: type is used to specify + the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling... + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to... + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + description: MaxReplicas is the upper limit for the + number of replicas. Required. + format: int32 + minimum: 1 + type: integer + metrics: + description: Metrics contains the specifications for + which to use to calculate the desired replica count. + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at... + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes... + properties: + container: + description: container is the name of the + container in the pods of the scaling target + type: string + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. + properties: + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the + descriptions of a object,such as kind,name + apiVersion + properties: + apiVersion: + description: apiVersion is the API version + of the referent + type: string + kind: + description: 'kind is the kind of the + referent; More info: https://git.k8s.' + type: string + name: + description: 'name is the name of the + referent; More info: https://kubernetes.' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example,... + properties: + metric: + description: metric identifies the target + metric by name and selector + properties: + name: + description: name is the name of the + given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed... + properties: + matchExpressions: + description: matchExpressions is + a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing... + properties: + name: + description: name is the name of the resource + in question. + type: string + target: + description: target specifies the target + value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a... + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether + the metric type is Utilization, Value, + or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value + of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: type is the type of metric source. + type: string + required: + - type + type: object + type: array + minReplicas: + description: MinReplicas is the lower limit for the + number of replicas. Defaults to 1. + format: int32 + minimum: 1 + type: integer + required: + - maxReplicas + type: object + type: object + securityContext: + description: PodSecurityContext holds pod-level security attributes + and common container settings. + properties: + appArmorProfile: + description: appArmorProfile is the AppArmor options to + use by the containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates a profile + loaded on the node that should be used. + type: string + type: + description: type indicates which kind of AppArmor + profile will be applied. + type: string + required: + - type + type: object + fsGroup: + description: A special supplemental group that applies + to all containers in a pod. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as + a non-root user. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + format: int64 + type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string + seLinuxOptions: + description: The SELinux context to be applied to all + containers. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. + type: string + type: + description: type indicates which kind of seccomp + profile will be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string + sysctls: + description: Sysctls hold a list of namespaced sysctls + used for the pod. + items: + description: Sysctl defines a kernel parameter to be + set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + description: The Windows specific settings applied to + all containers. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of + the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. + type: string + type: object + type: object + topologySpreadConstraints: + description: TopologySpreadConstraints defines how pods are + spread across topology domains. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: MaxSkew describes the degree to which pods + may be unevenly distributed. + format: int32 + type: integer + minDomains: + description: MinDomains indicates a minimum number of + eligible domains. + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread... + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. + type: string + topologyKey: + description: TopologyKey is the key of node labels. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + ui: + description: Creates a UI server container + properties: + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and... + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: Optional text to prepend to the name + of each environment variable. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + logLevel: + description: |- + LogLevel sets the logging level for the server + Allowed values: "debug", "info", "warning", "error", "critical". + enum: + - debug + - info + - warning + - error + - critical + type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean + nodeSelector: + additionalProperties: + type: string + type: object + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object + tls: + description: TlsConfigs configures server TLS for a feast + service. + properties: + disable: + description: will disable TLS for the feast service. + useful in an openshift cluster, for example, where + TLS is configured by default + type: boolean + secretKeyNames: + description: SecretKeyNames defines the secret key + names for the TLS key and cert. + properties: + tlsCrt: + description: defaults to "tls.crt" + type: string + tlsKey: + description: defaults to "tls.key" + type: string + type: object + secretRef: + description: references the local k8s secret where + the TLS key and cert reside + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + type: object + x-kubernetes-validations: + - message: '`secretRef` required if `disable` is false.' + rule: '(!has(self.disable) || !self.disable) ? has(self.secretRef) + : true' + volumeMounts: + description: VolumeMounts defines the list of volumes + that should be mounted into the feast container. + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + type: string + required: + - mountPath + - name + type: object + type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object + type: object + volumes: + description: Volumes specifies the volumes to mount in the + FeatureStore deployment. + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to... + properties: + fsType: + description: fsType is the filesystem type of the + volume that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes. + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes. + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + default: ext4 + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage...' + type: string + readOnly: + default: false + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime. + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s. + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile + is the path to key ring for User, default is /etc/ceph/user.' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is + reference to the authentication secret for User, + default is empty.' + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s. + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + Deprecated: Cinder is deprecated. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits + used to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers. + properties: + driver: + description: driver is the name of the CSI driver + that handles this volume. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", + "ntfs". + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created + files by default.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name, + namespace and uid are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes. + properties: + medium: + description: medium represents what type of storage + medium should back this directory. + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: sizeLimit is the total amount of local + storage required for this EmptyDir volume. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: ephemeral represents a volume that is handled + by a cluster storage driver. + properties: + volumeClaimTemplate: + description: Will be used to create a stand-alone + PVC to provision the volume. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. + type: object + spec: + description: The specification for the PersistentVolumeClaim. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes. + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s. + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking. + type: string + required: + - kind + - name + type: object + resources: + description: resources represents the minimum + resources the volume should have. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the + minimum amount of compute resources + required. + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes. + type: string + volumeAttributesClassName: + description: volumeAttributesClassName may + be used to set the VolumeAttributesClass + used by this claim. + type: string + volumeMode: + description: volumeMode defines what type + of volume is required by the claim. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the... + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as... + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the... + properties: + fsType: + description: fsType is filesystem type of the volume + that you want to mount. + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes. + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes. + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + Deprecated: GitRepo is deprecated. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. + properties: + endpoints: + description: endpoints is the endpoint name that + details Glusterfs topology. + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: fsType is the filesystem type of the + volume that you want to mount. + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + default: default + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal + List. + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: targetPortal is iSCSI Target Portal. + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes. + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes. + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes. + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + properties: + claimName: + description: claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used + to set permissions on created files by default. + format: int32 + type: integer + sources: + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. + items: + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. + properties: + clusterTrustBundle: + description: ClusterTrustBundle allows a pod + to access the `.spec. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map + of {key,value} pairs. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the... + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name, namespace and uid + are supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal... + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path.' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: audience is the intended + audience of the token. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime. + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple... + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set... + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. + properties: + fsType: + description: fsType is the filesystem type of the + volume that you want to mount. + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + default: /etc/ceph/keyring + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s. + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s. + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + default: xfs + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + default: ThinProvisioned + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes. + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits + used to set permissions on created files by default.' + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume... + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file.' + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes. + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope + of the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine. + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + x-kubernetes-validations: + - message: replicas > 1 and services.scaling.autoscaling are mutually + exclusive. + rule: self.replicas <= 1 || !has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling) + - message: Scaling requires DB-backed persistence for the online store. + Configure services.onlineStore.persistence.store when using replicas + > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.onlineStore) && has(self.services.onlineStore.persistence) + && has(self.services.onlineStore.persistence.store)) + - message: Scaling requires DB-backed persistence for the offline + store. Configure services.offlineStore.persistence.store when + using replicas > 1 or autoscaling. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (!has(self.services) + || !has(self.services.offlineStore) || (has(self.services.offlineStore.persistence) + && has(self.services.offlineStore.persistence.store))) + - message: Scaling requires DB-backed or remote registry. Configure + registry.local.persistence.store or use a remote registry when + using replicas > 1 or autoscaling. S3/GCS-backed registry is also + allowed. + rule: self.replicas <= 1 && (!has(self.services) || !has(self.services.scaling) + || !has(self.services.scaling.autoscaling)) || (has(self.services) + && has(self.services.registry) && (has(self.services.registry.remote) + || (has(self.services.registry.local) && has(self.services.registry.local.persistence) + && (has(self.services.registry.local.persistence.store) || (has(self.services.registry.local.persistence.file) + && has(self.services.registry.local.persistence.file.path) && + (self.services.registry.local.persistence.file.path.startsWith('s3://') + || self.services.registry.local.persistence.file.path.startsWith('gs://'))))))) + clientConfigMap: + description: ConfigMap in this namespace containing a client `feature_store.yaml` + for this feast deployment + type: string + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if . + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + cronJob: + description: CronJob in this namespace for this feast deployment + type: string + feastVersion: + type: string + phase: + type: string + replicas: + description: Replicas is the current number of ready pod replicas + (used by the scale sub-resource). + format: int32 + type: integer + scalingStatus: + description: ScalingStatus reports the current scaling state of the + FeatureStore deployment. + properties: + currentReplicas: + description: CurrentReplicas is the current number of pod replicas. + format: int32 + type: integer + desiredReplicas: + description: DesiredReplicas is the desired number of pod replicas. + format: int32 + type: integer + type: object + selector: + description: Selector is the label selector for pods managed by the + FeatureStore deployment (used by the scale sub-resource). + type: string + serviceHostnames: + description: ServiceHostnames defines the service hostnames in the + format of :, e.g. example.svc.cluster.local:80 + properties: + offlineStore: + type: string + onlineStore: + type: string + registry: + type: string + registryRest: + type: string + ui: + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: v1alpha1 is deprecated and will be removed in a future release. + Please migrate to v1. name: v1alpha1 schema: openAPIV3Schema: @@ -120,14 +12946,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -168,7 +12994,36 @@ spec: the specified API version. type: string required: - - fieldPath + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: @@ -226,7 +13081,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -245,8 +13100,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -292,6 +13147,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -333,7 +13192,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -427,7 +13286,6 @@ spec: type. type: string required: - - status - type type: object type: array @@ -444,6 +13302,13 @@ spec: description: podReplacementPolicy specifies when to create replacement Pods. type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object suspend: description: suspend specifies whether the Job controller should create Pods or not. @@ -503,14 +13368,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -554,6 +13419,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -609,7 +13503,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -628,8 +13522,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -683,15 +13577,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp + type: string + type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute path + to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. type: string + required: + - featureRepoPath type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -878,14 +13797,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -929,6 +13848,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -985,7 +13934,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1004,8 +13953,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -1042,6 +13991,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -1063,6 +14016,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -1174,6 +14131,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object onlineStore: @@ -1305,7 +14308,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -1319,6 +14321,10 @@ spec: - qdrant - couchbase.online - milvus + - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -1337,14 +14343,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -1388,6 +14394,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1444,7 +14480,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1463,8 +14499,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -1501,6 +14537,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -1522,6 +14562,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -1633,6 +14677,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object registry: @@ -1650,6 +14740,21 @@ spec: description: RegistryFilePersistence configures the file-based persistence for the registry service properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL (in + seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer path: type: string pvc: @@ -1800,14 +14905,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -1852,6 +14957,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -1910,7 +15045,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -1929,8 +15064,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -1971,6 +15106,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -1992,6 +15131,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -2107,6 +15250,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object x-kubernetes-validations: - message: At least one of restAPI or grpc must be true @@ -2167,6 +15356,10 @@ spec: x-kubernetes-validations: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the registry. + Defaults to true. Ignored when DisableInitContainers is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -2212,6 +15405,10 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. properties: @@ -2250,13 +15447,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -2307,14 +15509,14 @@ spec: in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's value. @@ -2358,6 +15560,35 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. An + invalid key will prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the file or its + key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -2413,7 +15644,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of a set - of ConfigMaps + of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -2432,8 +15663,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to each - key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name of + each environment variable. type: string secretRef: description: The Secret to select from @@ -2470,6 +15701,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -2490,6 +15725,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for a request + in the referenced claim. + type: string required: - name type: object @@ -2601,6 +15840,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * CPU + + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object volumes: description: Volumes specifies the volumes to mount in the FeatureStore @@ -2612,7 +15897,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the volume @@ -2654,6 +15939,7 @@ spec: blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -2662,9 +15948,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -2695,7 +15982,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: monitors: description: |- @@ -2743,7 +16030,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -2789,7 +16076,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -2829,7 +16116,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver that @@ -2841,7 +16128,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -2903,7 +16190,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -3152,9 +16439,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide identifiers - (wwids)\nEither wwids or combination of targetWWNs - and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -3189,7 +16476,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -3210,7 +16497,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -3220,7 +16507,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -3249,7 +16536,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -3267,14 +16554,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount on the + host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that details + Glusterfs topology. type: string path: description: |- @@ -3309,6 +16594,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -3334,6 +16635,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -3424,7 +16726,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -3441,7 +16743,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -3471,10 +16773,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected along - with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod to @@ -3554,7 +16859,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -3625,7 +16930,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -3667,6 +16972,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle at + this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet will + generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs will + be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -3674,7 +17025,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -3740,7 +17091,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the host - that shares a pod's lifetime + that shares a pod's lifetime. properties: group: description: |- @@ -3755,12 +17106,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -3776,9 +17127,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount on + the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the volume @@ -3790,6 +17140,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -3804,6 +17155,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -3831,6 +17183,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -3845,6 +17198,7 @@ spec: attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -3882,6 +17236,7 @@ spec: with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -3916,7 +17271,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -3991,7 +17346,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine + and mounted on kubelets host machine. properties: fsType: description: |- @@ -4101,14 +17456,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4152,6 +17507,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4208,7 +17593,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4227,8 +17612,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -4275,6 +17660,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -4316,7 +17705,7 @@ spec: activeDeadlineSeconds: description: |- Specifies the duration in seconds relative to the startTime that the job - may be continuously active before the system tr + may be continuously active before the system... format: int64 type: integer backoffLimit: @@ -4411,7 +17800,6 @@ spec: condition type. type: string required: - - status - type type: object type: array @@ -4428,6 +17816,13 @@ spec: description: podReplacementPolicy specifies when to create replacement Pods. type: string + podTemplateAnnotations: + additionalProperties: + type: string + description: |- + PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate + metadata. + type: object suspend: description: suspend specifies whether the Job controller should create Pods or not. @@ -4489,14 +17884,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4540,6 +17935,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4596,7 +18021,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4615,8 +18040,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -4671,15 +18096,40 @@ spec: - hbase - cassandra - hazelcast - - ikv - couchbase - clickhouse + - milvus + - ray + - ray_rag + - pytorch_nlp type: string type: object + packaged: + description: FeastPackagedOptions describes a feature repository + packaged in a feature server image. + properties: + featureRepoPath: + description: FeatureRepoPath is the canonical absolute + path to the feature repository in the image. + type: string + image: + description: Image containing the packaged feature repository. + type: string + required: + - featureRepoPath + type: object + x-kubernetes-validations: + - message: FeatureRepoPath must be a canonical absolute, non-root + path without dot segments or repeated separators. + rule: self.featureRepoPath.startsWith('/') && self.featureRepoPath + != '/' && !self.featureRepoPath.contains('//') && !self.featureRepoPath.endsWith('/') + && !self.featureRepoPath.contains('/./') && !self.featureRepoPath.endsWith('/.') + && !self.featureRepoPath.contains('/../') && !self.featureRepoPath.endsWith('/..') type: object x-kubernetes-validations: - - message: One selection required between init or git. - rule: '[has(self.git), has(self.init)].exists_one(c, c)' + - message: One selection required between init, git, or packaged. + rule: '[has(self.git), has(self.init), has(self.packaged)].exists_one(c, + c)' services: description: FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. @@ -4869,14 +18319,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -4921,6 +18371,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -4979,7 +18459,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -4998,8 +18478,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5036,6 +18516,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -5057,6 +18541,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -5169,6 +18657,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object onlineStore: @@ -5303,7 +18837,6 @@ spec: enum: - snowflake.online - redis - - ikv - datastore - dynamodb - bigtable @@ -5317,6 +18850,10 @@ spec: - qdrant - couchbase.online - milvus + - hybrid + - mongodb + - aerospike + - scylladb type: string required: - secretRef @@ -5336,14 +18873,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -5388,6 +18925,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env + file. An invalid key will prevent + the pod from starting. + type: string + optional: + default: false + description: Specify whether the file + or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5446,7 +19013,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5465,8 +19032,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the + name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5503,6 +19070,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -5524,6 +19095,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -5636,6 +19211,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores + (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object type: object registry: @@ -5655,6 +19276,21 @@ spec: the file-based persistence for the registry service properties: + cache_mode: + description: |- + CacheMode defines the registry cache update strategy. + Allowed values are "sync" and "thread". + enum: + - none + - sync + - thread + type: string + cache_ttl_seconds: + description: CacheTTLSeconds defines the TTL + (in seconds) for the registry cache. + format: int32 + minimum: 0 + type: integer path: type: string pvc: @@ -5811,14 +19447,14 @@ spec: variable present in a Container. properties: name: - description: Name of the environment variable. - Must be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment @@ -5864,6 +19500,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the + env file. An invalid key will + prevent the pod from starting. + type: string + optional: + default: false + description: Specify whether the + file or its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume + mount containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -5923,7 +19589,7 @@ spec: envFrom: items: description: EnvFromSource represents the source - of a set of ConfigMaps + of a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -5942,9 +19608,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend - to each key in the ConfigMap. Must be - a C_IDENTIFIER. + description: Optional text to prepend to + the name of each environment variable. type: string secretRef: description: The Secret to select from @@ -5985,6 +19650,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible + metrics for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -6006,6 +19675,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen + for a request in the referenced claim. + type: string required: - name type: object @@ -6125,6 +19798,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker + configuration for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number + of seconds after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker + processes. Use -1 to auto-calculate based + on CPU cores (2 * CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object x-kubernetes-validations: - message: At least one of restAPI or grpc must be @@ -6188,6 +19907,11 @@ spec: - message: One selection required. rule: '[has(self.local), has(self.remote)].exists_one(c, c)' + runFeastApplyOnInit: + description: Runs feast apply on pod start to populate the + registry. Defaults to true. Ignored when DisableInitContainers + is true. + type: boolean securityContext: description: PodSecurityContext holds pod-level security attributes and common container settings. @@ -6233,6 +19957,11 @@ spec: Defaults to user specified in image metadata if unspecified. format: int64 type: integer + seLinuxChangePolicy: + description: seLinuxChangePolicy defines how the container's + SELinux label is applied to all volumes used by the + Pod. + type: string seLinuxOptions: description: The SELinux context to be applied to all containers. @@ -6272,13 +20001,18 @@ spec: type: object supplementalGroups: description: |- - A list of groups applied to the first process run in each container, in addition - to the container's primary GID, the fsG + A list of groups applied to the first process run in each container, in + addition to the container's primary GID and... items: format: int64 type: integer type: array x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + description: |- + Defines how supplemental groups of the first container processes are calculated. + Valid values are "Merge" and "Strict". + type: string sysctls: description: Sysctls hold a list of namespaced sysctls used for the pod. @@ -6330,14 +20064,14 @@ spec: present in a Container. properties: name: - description: Name of the environment variable. Must - be a C_IDENTIFIER. + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. type: string value: description: |- Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any + using the previously defined environment variables in the container and... type: string valueFrom: description: Source for the environment variable's @@ -6381,6 +20115,36 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: The key within the env file. + An invalid key will prevent the pod from + starting. + type: string + optional: + default: false + description: Specify whether the file or + its key must be defined. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '.. + type: string + volumeName: + description: The name of the volume mount + containing the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: description: |- Selects a resource of the container: only resources limits and requests @@ -6437,7 +20201,7 @@ spec: envFrom: items: description: EnvFromSource represents the source of - a set of ConfigMaps + a set of ConfigMaps or Secrets properties: configMapRef: description: The ConfigMap to select from @@ -6456,8 +20220,8 @@ spec: type: object x-kubernetes-map-type: atomic prefix: - description: An optional identifier to prepend to - each key in the ConfigMap. Must be a C_IDENTIFIER. + description: Optional text to prepend to the name + of each environment variable. type: string secretRef: description: The Secret to select from @@ -6494,6 +20258,10 @@ spec: - error - critical type: string + metrics: + description: Metrics exposes Prometheus-compatible metrics + for the Feast server when enabled. + type: boolean nodeSelector: additionalProperties: type: string @@ -6515,6 +20283,10 @@ spec: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. type: string + request: + description: Request is the name chosen for + a request in the referenced claim. + type: string required: - name type: object @@ -6626,6 +20398,52 @@ spec: - name type: object type: array + workerConfigs: + description: WorkerConfigs defines the worker configuration + for the Feast server. + properties: + keepAliveTimeout: + description: |- + KeepAliveTimeout is the timeout for keep-alive connections in seconds. + Defaults to 30. + format: int32 + minimum: 1 + type: integer + maxRequests: + description: |- + MaxRequests is the maximum number of requests a worker will process before restarting. + This helps prevent memory leaks. + format: int32 + minimum: 0 + type: integer + maxRequestsJitter: + description: |- + MaxRequestsJitter is the maximum jitter to add to max-requests to prevent + thundering herd effect on worker restart. + format: int32 + minimum: 0 + type: integer + registryTTLSeconds: + description: RegistryTTLSeconds is the number of seconds + after which the registry is refreshed. + format: int32 + minimum: 0 + type: integer + workerConnections: + description: |- + WorkerConnections is the maximum number of simultaneous clients per worker process. + Defaults to 1000. + format: int32 + minimum: 1 + type: integer + workers: + description: Workers is the number of worker processes. + Use -1 to auto-calculate based on CPU cores (2 * + CPU + 1). + format: int32 + minimum: -1 + type: integer + type: object type: object volumes: description: Volumes specifies the volumes to mount in the @@ -6637,7 +20455,7 @@ spec: awsElasticBlockStore: description: |- awsElasticBlockStore represents an AWS Disk resource that is attached to a - kubelet's host machine and then exposed to th + kubelet's host machine and then exposed to... properties: fsType: description: fsType is the filesystem type of the @@ -6679,6 +20497,7 @@ spec: the blob storage type: string fsType: + default: ext4 description: |- fsType is Filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -6687,9 +20506,10 @@ spec: kind: description: 'kind expected values are Shared: multiple blob disks per storage account Dedicated: single - blob disk per storage accoun' + blob disk per storage...' type: string readOnly: + default: false description: |- readOnly Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts. @@ -6720,7 +20540,7 @@ spec: type: object cephfs: description: cephFS represents a Ceph FS mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: monitors: description: |- @@ -6769,7 +20589,7 @@ spec: cinder: description: |- cinder represents a cinder volume attached and mounted on kubelets host machine. - More info: https://examples.k8s. + Deprecated: Cinder is deprecated. properties: fsType: description: |- @@ -6815,7 +20635,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -6855,7 +20675,7 @@ spec: csi: description: csi (Container Storage Interface) represents ephemeral storage that is handled by certain external - CSI drivers (Beta fea + CSI drivers. properties: driver: description: driver is the name of the CSI driver @@ -6868,7 +20688,7 @@ spec: nodePublishSecretRef: description: |- nodePublishSecretRef is a reference to the secret object containing - sensitive information to pass to the CSI driver to c + sensitive information to pass to the CSI driver to... properties: name: default: "" @@ -6932,7 +20752,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -7184,9 +21004,9 @@ spec: type: array x-kubernetes-list-type: atomic wwids: - description: "wwids Optional: FC volume world wide - identifiers (wwids)\nEither wwids or combination - of targetWWNs and lun must be set, " + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set,... items: type: string type: array @@ -7221,7 +21041,7 @@ spec: secretRef: description: |- secretRef is Optional: secretRef is reference to the secret object containing - sensitive information to pass to the plugi + sensitive information to pass to the... properties: name: default: "" @@ -7242,7 +21062,7 @@ spec: datasetName: description: |- datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker - should be considered as depreca + should be considered as... type: string datasetUUID: description: datasetUUID is the UUID of the dataset. @@ -7252,7 +21072,7 @@ spec: gcePersistentDisk: description: |- gcePersistentDisk represents a GCE Disk resource that is attached to a - kubelet's host machine and then exposed to the po + kubelet's host machine and then exposed to the... properties: fsType: description: fsType is filesystem type of the volume @@ -7281,7 +21101,7 @@ spec: gitRepo: description: |- gitRepo represents a git repository at a particular revision. - DEPRECATED: GitRepo is deprecated. + Deprecated: GitRepo is deprecated. properties: directory: description: |- @@ -7299,14 +21119,12 @@ spec: - repository type: object glusterfs: - description: |- - glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: glusterfs represents a Glusterfs mount + on the host that shares a pod's lifetime. properties: endpoints: - description: |- - endpoints is the endpoint name that details Glusterfs topology. - More info: https://examples.k8s. + description: endpoints is the endpoint name that + details Glusterfs topology. type: string path: description: |- @@ -7341,6 +21159,22 @@ spec: required: - path type: object + image: + description: image represents an OCI object (a container + image or artifact) pulled and mounted on the kubelet's + host machine. + properties: + pullPolicy: + description: |- + Policy for pulling OCI objects. Possible values are: + Always: the kubelet always attempts to pull the reference. + type: string + reference: + description: |- + Required: Image or artifact reference to be used. + Behaves in the same way as pod.spec.containers[*].image. + type: string + type: object iscsi: description: |- iscsi represents an ISCSI Disk resource that is attached to a @@ -7366,6 +21200,7 @@ spec: description: iqn is the target iSCSI Qualified Name. type: string iscsiInterface: + default: default description: |- iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp). @@ -7457,7 +21292,7 @@ spec: photonPersistentDisk: description: photonPersistentDisk represents a PhotonController persistent disk attached and mounted on kubelets host - machine + machine. properties: fsType: description: |- @@ -7474,7 +21309,7 @@ spec: type: object portworxVolume: description: portworxVolume represents a portworx volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -7504,10 +21339,13 @@ spec: format: int32 type: integer sources: - description: sources is the list of volume projections + description: |- + sources is the list of volume projections. Each entry in this list + handles one source. items: - description: Projection that may be projected - along with other supported volume types + description: |- + Projection that may be projected along with other supported volume types. + Exactly one of these fields must be set. properties: clusterTrustBundle: description: ClusterTrustBundle allows a pod @@ -7588,7 +21426,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - ConfigMap will be projected into the volum + ConfigMap will be projected into the... items: description: Maps a string key to a path within a volume. @@ -7661,7 +21499,7 @@ spec: mode: description: |- Optional: mode bits used to set permissions on this file, must be an octal value - between 0000 and 0777 or a decimal valu + between 0000 and 0777 or a decimal... format: int32 type: integer path: @@ -7703,6 +21541,52 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + description: |- + Projects an auto-rotating credential bundle (private key and certificate + chain) that the pod can use either as a TLS... + properties: + certificateChainPath: + description: |- + Write the certificate chain at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + credentialBundlePath: + description: Write the credential bundle + at this path in the projected volume. + type: string + keyPath: + description: |- + Write the key at this path in the projected volume. + + Most applications should use credentialBundlePath. + type: string + keyType: + description: The type of keypair Kubelet + will generate for the pod. + type: string + maxExpirationSeconds: + description: |- + maxExpirationSeconds is the maximum lifetime permitted for the + certificate. + format: int32 + type: integer + signerName: + description: Kubelet's generated CSRs + will be addressed to this signer. + type: string + userAnnotations: + additionalProperties: + type: string + description: |- + userAnnotations allow pod authors to pass additional information to + the signer implementation. + type: object + required: + - keyType + - signerName + type: object secret: description: secret information about the secret data to project @@ -7710,7 +21594,7 @@ spec: items: description: |- items if unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -7776,7 +21660,7 @@ spec: type: object quobyte: description: quobyte represents a Quobyte mount on the - host that shares a pod's lifetime + host that shares a pod's lifetime. properties: group: description: |- @@ -7791,12 +21675,12 @@ spec: registry: description: |- registry represents a single or multiple Quobyte Registry services - specified as a string as host:port pair (multiple ent + specified as a string as host:port pair (multiple... type: string tenant: description: |- tenant owning the given Quobyte volume in the Backend - Used with dynamically provisioned Quobyte volumes, value is set by + Used with dynamically provisioned Quobyte volumes, value is set... type: string user: description: |- @@ -7812,9 +21696,8 @@ spec: - volume type: object rbd: - description: |- - rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. - More info: https://examples.k8s. + description: rbd represents a Rados Block Device mount + on the host that shares a pod's lifetime. properties: fsType: description: fsType is the filesystem type of the @@ -7826,6 +21709,7 @@ spec: More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it type: string keyring: + default: /etc/ceph/keyring description: |- keyring is the path to key ring for RBDUser. Default is /etc/ceph/keyring. @@ -7840,6 +21724,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd description: |- pool is the rados pool name. Default is rbd. @@ -7867,6 +21752,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin description: |- user is the rados user name. Default is admin. @@ -7881,6 +21767,7 @@ spec: volume attached and mounted on Kubernetes nodes. properties: fsType: + default: xfs description: |- fsType is the filesystem type to mount. Must be a filesystem type supported by the host operating system. @@ -7918,6 +21805,7 @@ spec: communication with Gateway, default false type: boolean storageMode: + default: ThinProvisioned description: storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. type: string @@ -7952,7 +21840,7 @@ spec: items: description: |- items If unspecified, each key-value pair in the Data field of the referenced - Secret will be projected into the volume a + Secret will be projected into the volume... items: description: Maps a string key to a path within a volume. @@ -8027,7 +21915,7 @@ spec: type: object vsphereVolume: description: vsphereVolume represents a vSphere volume - attached and mounted on kubelets host machine + attached and mounted on kubelets host machine. properties: fsType: description: |- @@ -8101,10 +21989,7 @@ spec: - Unknown type: string type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition. + description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string @@ -8141,7 +22026,7 @@ spec: type: object type: object served: true - storage: true + storage: false subresources: status: {} --- @@ -8201,6 +22086,8 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: feast-operator + rbac.authorization.k8s.io/aggregate-to-admin: "true" + rbac.authorization.k8s.io/aggregate-to-edit: "true" name: feast-operator-featurestore-editor-role rules: - apiGroups: @@ -8228,6 +22115,7 @@ metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: feast-operator + rbac.authorization.k8s.io/aggregate-to-view: "true" name: feast-operator-featurestore-viewer-role rules: - apiGroups: @@ -8250,6 +22138,63 @@ kind: ClusterRole metadata: name: feast-operator-manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + - persistentvolumeclaims + - services + verbs: + - create + - delete + - deletecollection + - get + - list + - update + - watch +- apiGroups: + - "" + resources: + - namespaces + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - deletecollection + - get + - list + - watch +- apiGroups: + - "" + resources: + - pods/exec + verbs: + - create +- apiGroups: + - "" + resources: + - pods/log + verbs: + - get +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - update + - watch - apiGroups: - apps resources: @@ -8268,9 +22213,9 @@ rules: verbs: - create - apiGroups: - - batch + - autoscaling resources: - - cronjobs + - horizontalpodautoscalers verbs: - create - delete @@ -8280,35 +22225,25 @@ rules: - update - watch - apiGroups: - - "" + - batch resources: - - configmaps - - persistentvolumeclaims - - serviceaccounts - - services + - cronjobs verbs: - create - delete - get - list + - patch - update - watch - apiGroups: - - "" + - config.openshift.io resources: - - namespaces - - pods - - secrets + - apiservers verbs: - get - list - watch -- apiGroups: - - "" - resources: - - pods/exec - verbs: - - create - apiGroups: - feast.dev resources: @@ -8335,6 +22270,29 @@ rules: - get - patch - update +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - watch +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resources: @@ -8361,6 +22319,14 @@ rules: - list - update - watch +- apiGroups: + - sparkoperator.k8s.io + resources: + - sparkapplications + verbs: + - create + - delete + - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -8461,6 +22427,7 @@ spec: protocol: TCP targetPort: 8443 selector: + app.kubernetes.io/name: feast-operator control-plane: controller-manager --- apiVersion: apps/v1 @@ -8476,12 +22443,14 @@ spec: replicas: 1 selector: matchLabels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: + app.kubernetes.io/name: feast-operator control-plane: controller-manager spec: containers: @@ -8493,10 +22462,14 @@ spec: - /manager env: - name: RELATED_IMAGE_FEATURE_SERVER - value: quay.io/feastdev/feature-server:0.56.0 + value: quay.io/feastdev/feature-server:0.65.0 - name: RELATED_IMAGE_CRON_JOB value: quay.io/openshift/origin-cli:4.17 - image: quay.io/feastdev/feast-operator:0.56.0 + - name: GOMEMLIMIT + value: 230MiB + - name: OIDC_ISSUER_URL + value: "" + image: quay.io/feastdev/feast-operator:0.65.0 livenessProbe: httpGet: path: /healthz diff --git a/infra/feast-operator/dist/operator-e2e-tests b/infra/feast-operator/dist/operator-e2e-tests index 2a8e2a02352..0d5ff42aef8 100755 Binary files a/infra/feast-operator/dist/operator-e2e-tests and b/infra/feast-operator/dist/operator-e2e-tests differ diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index 6016a70a1b8..cb911ffae22 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -1,12 +1,12 @@ # API Reference ## Packages -- [feast.dev/v1alpha1](#feastdevv1alpha1) +- [feast.dev/v1](#feastdevv1) -## feast.dev/v1alpha1 +## feast.dev/v1 -Package v1alpha1 contains API Schema definitions for the v1alpha1 API group +Package v1 contains API Schema definitions for the v1 API group ### Resource Types - [FeatureStore](#featurestore) @@ -28,6 +28,40 @@ _Appears in:_ | `oidc` _[OidcAuthz](#oidcauthz)_ | | +#### AutoscalingConfig + + + +AutoscalingConfig defines HPA settings for the FeatureStore deployment. + +_Appears in:_ +- [ScalingConfig](#scalingconfig) + +| Field | Description | +| --- | --- | +| `minReplicas` _integer_ | MinReplicas is the lower limit for the number of replicas. Defaults to 1. | +| `maxReplicas` _integer_ | MaxReplicas is the upper limit for the number of replicas. Required. | +| `metrics` _[MetricSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#metricspec-v2-autoscaling) array_ | Metrics contains the specifications for which to use to calculate the desired replica count. +If not set, defaults to 80% CPU utilization. | +| `behavior` _[HorizontalPodAutoscalerBehavior](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#horizontalpodautoscalerbehavior-v2-autoscaling)_ | Behavior configures the scaling behavior of the target. | + + +#### BatchEngineConfig + + + +BatchEngineConfig defines the batch compute engine configuration. + +_Appears in:_ +- [FeatureStoreSpec](#featurestorespec) + +| Field | Description | +| --- | --- | +| `configMapRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | Reference to a ConfigMap containing the batch engine configuration. +The ConfigMap should contain YAML-formatted config with 'type' and engine-specific fields. | +| `configMapKey` _string_ | Key name in the ConfigMap. Defaults to "config" if not specified. | + + #### ContainerConfigs @@ -70,6 +104,20 @@ _Appears in:_ Defaults to "feast apply" & "feast materialize-incremental $(date -u +'%Y-%m-%dT%H:%M:%S')" | +#### DataQualityMonitoringConfig + + + +DataQualityMonitoringConfig defines the Data Quality Monitoring configuration. + +_Appears in:_ +- [FeatureStoreSpec](#featurestorespec) + +| Field | Description | +| --- | --- | +| `autoBaseline` _boolean_ | AutoBaseline controls whether baseline distribution is computed automatically on feast apply. Defaults to true. | + + #### DefaultCtrConfigs @@ -116,7 +164,6 @@ time for any reason. Missed jobs executions will be counted as failed ones. | | `concurrencyPolicy` _[ConcurrencyPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#concurrencypolicy-v1-batch)_ | Specifies how to treat concurrent executions of a Job. Valid values are: - - "Allow" (default): allows CronJobs to run concurrently; - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; - "Replace": cancels currently running job and replaces it with a new one | @@ -141,6 +188,23 @@ _Appears in:_ | `template` _string_ | Template for the created project | +#### FeastPackagedOptions + + + +FeastPackagedOptions describes a feature repository packaged in a feature server image. + +_Appears in:_ +- [FeastProjectDir](#feastprojectdir) + +| Field | Description | +| --- | --- | +| `image` _string_ | Image containing the packaged feature repository. When set, this image is used by the +repository initialization and feast apply containers and as the default service image. +When omitted, the operator's configured feature server image is used. | +| `featureRepoPath` _string_ | FeatureRepoPath is the canonical absolute path to the feature repository in the image. | + + #### FeastProjectDir @@ -154,6 +218,7 @@ _Appears in:_ | --- | --- | | `git` _[GitCloneOptions](#gitcloneoptions)_ | | | `init` _[FeastInitOptions](#feastinitoptions)_ | | +| `packaged` _[FeastPackagedOptions](#feastpackagedoptions)_ | | #### FeatureStore @@ -166,7 +231,7 @@ FeatureStore is the Schema for the featurestores API | Field | Description | | --- | --- | -| `apiVersion` _string_ | `feast.dev/v1alpha1` +| `apiVersion` _string_ | `feast.dev/v1` | `kind` _string_ | `FeatureStore` | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | `spec` _[FeatureStoreSpec](#featurestorespec)_ | | @@ -205,8 +270,29 @@ _Appears in:_ | `ui` _[ServerConfigs](#serverconfigs)_ | Creates a UI server container | | `deploymentStrategy` _[DeploymentStrategy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#deploymentstrategy-v1-apps)_ | | | `securityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#podsecuritycontext-v1-core)_ | | +| `podAnnotations` _object (keys:string, values:string)_ | PodAnnotations are annotations to be applied to the Deployment's PodTemplate metadata. +This enables annotation-driven integrations like OpenTelemetry auto-instrumentation, +Istio sidecar injection, Vault agent injection, etc. | | `disableInitContainers` _boolean_ | Disable the 'feast repo initialization' initContainer | +| `initImage` _string_ | InitImage overrides the image for init containers (feast-init, feast-apply). +Resolution order: InitImage → FeastProjectDir.Packaged.Image → RELATED_IMAGE_FEATURE_SERVER → DefaultImage. | +| `runFeastApplyOnInit` _boolean_ | Runs feast apply on pod start to populate the registry. Defaults to true. Ignored when DisableInitContainers is true. | | `volumes` _[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volume-v1-core) array_ | Volumes specifies the volumes to mount in the FeatureStore deployment. A corresponding `VolumeMount` should be added to whichever feast service(s) require access to said volume(s). | +| `scaling` _[ScalingConfig](#scalingconfig)_ | Scaling configures horizontal scaling for the FeatureStore deployment (e.g. HPA autoscaling). +For static replicas, use spec.replicas instead. | +| `podDisruptionBudgets` _[PDBConfig](#pdbconfig)_ | PodDisruptionBudgets configures a PodDisruptionBudget for the FeatureStore deployment. +Only created when scaling is enabled (replicas > 1 or autoscaling). | +| `topologySpreadConstraints` _[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#topologyspreadconstraint-v1-core) array_ | TopologySpreadConstraints defines how pods are spread across topology domains. +When scaling is enabled and this is not set, the operator auto-injects a soft +zone-spread constraint (whenUnsatisfiable: ScheduleAnyway). +Set to an empty array to disable auto-injection. | +| `affinity` _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#affinity-v1-core)_ | Affinity defines the pod scheduling constraints for the FeatureStore deployment. +When scaling is enabled and this is not set, the operator auto-injects a soft +pod anti-affinity rule to prefer spreading pods across nodes. | +| `resourceClaims` _[PodResourceClaim](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#podresourceclaim-v1-core) array_ | ResourceClaims defines which ResourceClaims must be allocated +and reserved before the Pod is allowed to start. The resources +will be made available to those containers which consume them +by name. | #### FeatureStoreSpec @@ -226,6 +312,14 @@ _Appears in:_ | `services` _[FeatureStoreServices](#featurestoreservices)_ | | | `authz` _[AuthzConfig](#authzconfig)_ | | | `cronJob` _[FeastCronJob](#feastcronjob)_ | | +| `batchEngine` _[BatchEngineConfig](#batchengineconfig)_ | | +| `dataQualityMonitoring` _[DataQualityMonitoringConfig](#dataqualitymonitoringconfig)_ | DataQualityMonitoring configures Data Quality Monitoring behaviour. | +| `replicas` _integer_ | Replicas is the desired number of pod replicas. Used by the scale sub-resource. +Mutually exclusive with services.scaling.autoscaling. | +| `materialization` _[MaterializationConfig](#materializationconfig)_ | Materialization controls feature materialization behavior (batch size, pull strategy). +Written into feature_store.yaml for all service pods. | +| `openlineage` _[OpenLineageConfig](#openlineageconfig)_ | OpenLineage enables OpenLineage data lineage tracking for Feast operations. +Written into feature_store.yaml for all service pods. | #### FeatureStoreStatus @@ -246,6 +340,9 @@ _Appears in:_ | `feastVersion` _string_ | | | `phase` _string_ | | | `serviceHostnames` _[ServiceHostnames](#servicehostnames)_ | | +| `replicas` _integer_ | Replicas is the current number of ready pod replicas (used by the scale sub-resource). | +| `selector` _string_ | Selector is the label selector for pods managed by the FeatureStore deployment (used by the scale sub-resource). | +| `scalingStatus` _[ScalingStatus](#scalingstatus)_ | ScalingStatus reports the current scaling state of the FeatureStore deployment. | #### GitCloneOptions @@ -280,6 +377,9 @@ _Appears in:_ | Field | Description | | --- | --- | +| `podTemplateAnnotations` _object (keys:string, values:string)_ | PodTemplateAnnotations are annotations to be applied to the CronJob's PodTemplate +metadata. This is separate from the CronJob-level annotations and must be +set explicitly by users if they want annotations on the PodTemplate. | | `parallelism` _integer_ | Specifies the maximum desired number of pods the job should run at any given time. The actual number of pods running in steady state will be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), @@ -304,7 +404,6 @@ represented by the jobs's .status.failed field, is incremented and it is checked against the backoffLimit. This field cannot be used in combination with restartPolicy=OnFailure. - This field is beta-level. It can be used when the `JobPodFailurePolicy` feature gate is enabled (enabled by default). | | `backoffLimit` _integer_ | Specifies the number of retries before marking this job failed. | @@ -336,12 +435,10 @@ the Job becomes eligible to be deleted immediately after it finishes. | | `completionMode` _[CompletionMode](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#completionmode-v1-batch)_ | completionMode specifies how Pod completions are tracked. It can be `NonIndexed` (default) or `Indexed`. - `NonIndexed` means that the Job is considered complete when there have been .spec.completions successfully completed Pods. Each Pod completion is homologous to each other. - `Indexed` means that the Pods of a Job get an associated completion index from 0 to (.spec.completions - 1), available in the annotation batch.kubernetes.io/job-completion-index. @@ -353,7 +450,6 @@ In addition, The Pod name takes the form `$(job-name)-$(index)-$(random-string)`, the Pod hostname takes the form `$(job-name)-$(index)`. - More completion modes can be added in the future. If the Job controller observes a mode that it doesn't recognize, which is possible during upgrades due to version skew, the controller @@ -372,7 +468,6 @@ Possible values are: - Failed means to wait until a previously created Pod is fully terminated (has phase Failed or Succeeded) before creating a replacement Pod. - When using podFailurePolicy, Failed is the the only allowed value. TerminatingOrFailed and Failed are allowed values when podFailurePolicy is not in use. This is an beta field. To use this, enable the JobPodReplacementPolicy feature toggle. @@ -414,6 +509,60 @@ _Appears in:_ | `persistence` _[RegistryPersistence](#registrypersistence)_ | | +#### MaterializationConfig + + + +MaterializationConfig controls feature materialization behavior written into feature_store.yaml. + +_Appears in:_ +- [FeatureStoreSpec](#featurestorespec) + +| Field | Description | +| --- | --- | +| `onlineWriteBatchSize` _integer_ | Number of rows per batch when writing to the online store during materialization. +Prevents OOM for large feature views. Supported engines: local, spark, ray. +If unset, all rows are written in a single batch. | +| `extraConfig` _object (keys:string, values:string)_ | ExtraConfig passes additional materialization key-value settings inline into +feature_store.yaml. | + + +#### McpConfig + + + +McpConfig enables MCP (Model Context Protocol) server support in the feature server. +When this field is set on ServingConfig, the feature server type is switched to "mcp". + +_Appears in:_ +- [RegistryServerConfigs](#registryserverconfigs) +- [ServingConfig](#servingconfig) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable the MCP server. | +| `serverName` _string_ | MCP server name for identification. Defaults to "feast-mcp-server". | +| `serverVersion` _string_ | MCP server version string. Defaults to "1.0.0". | +| `transport` _string_ | MCP transport protocol. | + + +#### OfflinePushBatchingConfig + + + +OfflinePushBatchingConfig controls batching of writes to the offline store via the /push endpoint. +Recommended for high-throughput push workloads (streaming pipelines, IoT) to prevent OOM. + +_Appears in:_ +- [ServingConfig](#servingconfig) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable offline push batching. | +| `batchSize` _integer_ | Maximum number of rows per offline write batch. | +| `batchIntervalSeconds` _integer_ | Seconds between batch flushes to the offline store. | + + #### OfflineStore @@ -487,7 +636,27 @@ _Appears in:_ | Field | Description | | --- | --- | -| `secretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | | +| `issuerUrl` _string_ | OIDC issuer URL. The operator appends /.well-known/openid-configuration to derive the discovery endpoint. | +| `secretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | Secret with OIDC properties (auth_discovery_url, client_id, client_secret). issuerUrl takes precedence. | +| `secretKeyName` _string_ | Key in the Secret containing all OIDC properties as a YAML value. If unset, each key is a property. | +| `tokenEnvVar` _string_ | Env var name for client pods to read an OIDC token from. Sets token_env_var in client config. | +| `verifySSL` _boolean_ | Verify SSL certificates for the OIDC provider. Defaults to true. | +| `caCertConfigMap` _[OidcCACertConfigMap](#oidccacertconfigmap)_ | ConfigMap with the CA certificate for self-signed OIDC providers. Auto-detected on RHOAI/ODH. | + + +#### OidcCACertConfigMap + + + +OidcCACertConfigMap references a ConfigMap containing a CA certificate for OIDC provider TLS. + +_Appears in:_ +- [OidcAuthz](#oidcauthz) + +| Field | Description | +| --- | --- | +| `name` _string_ | ConfigMap name. | +| `key` _string_ | Key in the ConfigMap holding the PEM certificate. Defaults to "ca-bundle.crt". | #### OnlineStore @@ -503,6 +672,11 @@ _Appears in:_ | --- | --- | | `server` _[ServerConfigs](#serverconfigs)_ | Creates a feature server container | | `persistence` _[OnlineStorePersistence](#onlinestorepersistence)_ | | +| `serving` _[ServingConfig](#servingconfig)_ | Serving configures the Feast feature_server section written into feature_store.yaml for the online serve pod. +Controls metrics granularity, offline push batching, and MCP. | +| `disabled` _boolean_ | Disabled skips deploying the online store service entirely, including its +serving pod and persistence. Omitting the online store block, or setting +this to false, deploys the online store with defaults as before. | #### OnlineStoreDBStorePersistence @@ -551,6 +725,57 @@ _Appears in:_ | `store` _[OnlineStoreDBStorePersistence](#onlinestoredbstorepersistence)_ | | +#### OpenLineageConfig + + + +OpenLineageConfig enables OpenLineage data lineage tracking for Feast operations. +Lineage events are emitted during feast apply and materialization when enabled. + +_Appears in:_ +- [FeatureStoreSpec](#featurestorespec) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable OpenLineage integration. | +| `transportType` _string_ | Transport type for lineage events. | +| `transportUrl` _string_ | URL for HTTP transport (e.g. http://marquez:5000). Required when transportType is "http". | +| `transportEndpoint` _string_ | API endpoint path appended to transportUrl. Defaults to "api/v1/lineage". | +| `apiKeySecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | Reference to a Secret containing the key "api_key" for lineage server authentication. | +| `extraConfig` _object (keys:string, values:string)_ | ExtraConfig holds additional OpenLineage key-value settings written inline into +the openlineage block of feature_store.yaml alongside the typed fields above. +Use this for non-core settings (e.g. namespace, producer, emit_on_apply, +emit_on_materialize) and transport-specific options (e.g. kafka +bootstrap_servers, topic; file path). Boolean values ("true"/"false") and +integer values are automatically coerced to their native YAML types. +Keys must be valid Feast OpenLineageConfig YAML field names. | +| `consumer` _[OpenLineageConsumerConfig](#openlineageconsumerconfig)_ | Consumer configures the OpenLineage consumer (event receiver) that enables +Feast to receive and display lineage from external producers (Airflow, Spark, dbt, etc.). | + + +#### OpenLineageConsumerConfig + + + +OpenLineageConsumerConfig configures the OpenLineage consumer (event receiver). +When enabled, the Feast REST server exposes POST /api/v1/lineage to receive +OpenLineage events from any producer, storing them for visualization in the Feast UI. + +_Appears in:_ +- [OpenLineageConfig](#openlineageconfig) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable the OpenLineage consumer. | +| `storeType` _string_ | StoreType is the storage backend for lineage events. Currently only "sql" is supported. | +| `connectionStringSecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | Reference to a Secret containing the key "connection_string" for a separate +lineage database. If omitted, the SQL registry database is reused. | +| `apiKeySecretRef` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_ | Reference to a Secret containing the key "api_key" that producers must +provide in the X-API-Key header when sending events. | +| `namespaceMapping` _object (keys:string, values:string)_ | NamespaceMapping maps OpenLineage namespaces to Feast projects for +RBAC-based filtering of lineage data in the UI. | + + #### OptionalCtrConfigs @@ -572,6 +797,24 @@ _Appears in:_ | `nodeSelector` _map[string]string_ | | +#### PDBConfig + + + +PDBConfig configures a PodDisruptionBudget for the FeatureStore deployment. +Exactly one of minAvailable or maxUnavailable must be set. + +_Appears in:_ +- [FeatureStoreServices](#featurestoreservices) + +| Field | Description | +| --- | --- | +| `minAvailable` _[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#intorstring-intstr-util)_ | MinAvailable specifies the minimum number/percentage of pods that must remain available. +Mutually exclusive with maxUnavailable. | +| `maxUnavailable` _[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#intorstring-intstr-util)_ | MaxUnavailable specifies the maximum number/percentage of pods that can be unavailable. +Mutually exclusive with minAvailable. | + + #### PvcConfig @@ -659,6 +902,9 @@ _Appears in:_ | `path` _string_ | | | `pvc` _[PvcConfig](#pvcconfig)_ | | | `s3_additional_kwargs` _map[string]string_ | | +| `cache_ttl_seconds` _integer_ | CacheTTLSeconds defines the TTL (in seconds) for the registry cache. | +| `cache_mode` _string_ | CacheMode defines the registry cache update strategy. +Allowed values are "sync" and "thread". | #### RegistryPersistence @@ -696,12 +942,17 @@ _Appears in:_ | `tls` _[TlsConfigs](#tlsconfigs)_ | | | `logLevel` _string_ | LogLevel sets the logging level for the server Allowed values: "debug", "info", "warning", "error", "critical". | +| `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the feast container. This allows attaching persistent storage, config files, secrets, or other resources required by the Feast components. Ensure that each volume mount has a corresponding volume definition in the Volumes field. | +| `workerConfigs` _[WorkerConfigs](#workerconfigs)_ | WorkerConfigs defines the worker configuration for the Feast server. +These options are primarily used for production deployments to optimize performance. | | `restAPI` _boolean_ | Enable REST API registry server. | | `grpc` _boolean_ | Enable gRPC registry server. Defaults to true if unset. | +| `mcp` _[McpConfig](#mcpconfig)_ | Mcp enables MCP (Model Context Protocol) on the REST registry server. +Requires restAPI to be true. Reuses the same McpConfig struct as the online store. | #### RemoteRegistryConfig @@ -721,6 +972,36 @@ _Appears in:_ | `tls` _[TlsRemoteRegistryConfigs](#tlsremoteregistryconfigs)_ | | +#### ScalingConfig + + + +ScalingConfig configures horizontal scaling for the FeatureStore deployment. + +_Appears in:_ +- [FeatureStoreServices](#featurestoreservices) + +| Field | Description | +| --- | --- | +| `autoscaling` _[AutoscalingConfig](#autoscalingconfig)_ | Autoscaling configures a HorizontalPodAutoscaler for the FeatureStore deployment. +Mutually exclusive with spec.replicas. | + + +#### ScalingStatus + + + +ScalingStatus reports the observed scaling state. + +_Appears in:_ +- [FeatureStoreStatus](#featurestorestatus) + +| Field | Description | +| --- | --- | +| `currentReplicas` _integer_ | CurrentReplicas is the current number of pod replicas. | +| `desiredReplicas` _integer_ | DesiredReplicas is the desired number of pod replicas. | + + #### SecretKeyNames @@ -759,10 +1040,13 @@ _Appears in:_ | `tls` _[TlsConfigs](#tlsconfigs)_ | | | `logLevel` _string_ | LogLevel sets the logging level for the server Allowed values: "debug", "info", "warning", "error", "critical". | +| `metrics` _boolean_ | Metrics exposes Prometheus-compatible metrics for the Feast server when enabled. | | `volumeMounts` _[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volumemount-v1-core) array_ | VolumeMounts defines the list of volumes that should be mounted into the feast container. This allows attaching persistent storage, config files, secrets, or other resources required by the Feast components. Ensure that each volume mount has a corresponding volume definition in the Volumes field. | +| `workerConfigs` _[WorkerConfigs](#workerconfigs)_ | WorkerConfigs defines the worker configuration for the Feast server. +These options are primarily used for production deployments to optimize performance. | #### ServiceHostnames @@ -783,6 +1067,44 @@ _Appears in:_ | `ui` _string_ | | +#### ServingConfig + + + +ServingConfig configures the feature_server section of the generated feature_store.yaml. +When Mcp is set, the feature server type is switched to "mcp"; otherwise "local" is used. + +_Appears in:_ +- [OnlineStore](#onlinestore) + +| Field | Description | +| --- | --- | +| `metrics` _[ServingMetricsConfig](#servingmetricsconfig)_ | Metrics configures per-category Prometheus metrics for the feature server. +Coexists with the server.metrics bool flag — both can be set simultaneously. | +| `offlinePushBatching` _[OfflinePushBatchingConfig](#offlinepushbatchingconfig)_ | OfflinePushBatching batches writes to the offline store via the /push endpoint. | +| `mcp` _[McpConfig](#mcpconfig)_ | Mcp enables MCP (Model Context Protocol) server support. When set, feature server type is "mcp". | + + +#### ServingMetricsConfig + + + +ServingMetricsConfig controls per-category Prometheus metrics for the feature server. +Setting Enabled to true activates the metrics HTTP server on port 8000. +All metric categories default to true when enabled; use Categories to selectively disable them. + +_Appears in:_ +- [ServingConfig](#servingconfig) + +| Field | Description | +| --- | --- | +| `enabled` _boolean_ | Enable the Prometheus metrics endpoint on port 8000. | +| `categories` _object (keys:string, values:boolean)_ | Categories selectively enables or disables individual Feast metric categories. +Keys are Feast MetricsConfig field names (e.g. "resource", "request", +"online_features", "push", "materialization", "freshness"). Omitted keys +default to true when metrics is enabled. | + + #### TlsConfigs @@ -815,3 +1137,30 @@ _Appears in:_ | `certName` _string_ | defines the configmap key name for the client TLS cert. | +#### WorkerConfigs + + + +WorkerConfigs defines the worker configuration for Feast servers. +These settings control gunicorn worker processes for production deployments. + +_Appears in:_ +- [RegistryServerConfigs](#registryserverconfigs) +- [ServerConfigs](#serverconfigs) + +| Field | Description | +| --- | --- | +| `workers` _integer_ | Workers is the number of worker processes. Use -1 to auto-calculate based on CPU cores (2 * CPU + 1). +Defaults to 1 if not specified. | +| `workerConnections` _integer_ | WorkerConnections is the maximum number of simultaneous clients per worker process. +Defaults to 1000. | +| `maxRequests` _integer_ | MaxRequests is the maximum number of requests a worker will process before restarting. +This helps prevent memory leaks. Defaults to 1000. | +| `maxRequestsJitter` _integer_ | MaxRequestsJitter is the maximum jitter to add to max-requests to prevent +thundering herd effect on worker restart. Defaults to 50. | +| `keepAliveTimeout` _integer_ | KeepAliveTimeout is the timeout for keep-alive connections in seconds. +Defaults to 30. | +| `registryTTLSeconds` _integer_ | RegistryTTLSeconds is the number of seconds after which the registry is refreshed. +Higher values reduce refresh overhead but increase staleness. Defaults to 60. | + + diff --git a/infra/feast-operator/docs/odh-operator-parameters.md b/infra/feast-operator/docs/odh-operator-parameters.md new file mode 100644 index 00000000000..8b29d4d1ca8 --- /dev/null +++ b/infra/feast-operator/docs/odh-operator-parameters.md @@ -0,0 +1,15 @@ +# Open Data Hub / RHOAI operator parameters + +These values are supplied through the Feast operator **`params.env`** files in the **ODH** and **RHOAI** overlays (`config/overlays/odh/params.env`, `config/overlays/rhoai/params.env`). The Open Data Hub operator updates keys in `params.env` before rendering; Kustomize **`replacements`** copy them into the controller Deployment. + +## `OIDC_ISSUER_URL` + +**Purpose:** OIDC issuer URL when the OpenShift cluster uses external OIDC (for example Keycloak). The Feast operator process receives it as the **`OIDC_ISSUER_URL`** environment variable. An empty value means the cluster is not using external OIDC in this integration path (OpenShift OAuth / default behavior). + +**Manifest parameter:** `OIDC_ISSUER_URL` in `params.env`. + +**Injected into:** `controller-manager` Deployment, `manager` container. + +**Set by:** Open Data Hub operator (Feast component reconcile), from `GatewayConfig.spec.oidc.issuerURL` when cluster authentication is OIDC. + +**Consumption:** Operator code should read `os.Getenv("OIDC_ISSUER_URL")` (or equivalent) where JWKS / OIDC discovery is required for managed workloads. diff --git a/infra/feast-operator/go.mod b/infra/feast-operator/go.mod index 3e41f468d68..ab19a1de20a 100644 --- a/infra/feast-operator/go.mod +++ b/infra/feast-operator/go.mod @@ -1,97 +1,108 @@ module github.com/feast-dev/feast/infra/feast-operator -go 1.22.9 +go 1.25.0 require ( - github.com/onsi/ginkgo/v2 v2.17.1 - github.com/onsi/gomega v1.32.0 - github.com/openshift/api v0.0.0-20240912201240-0a8800162826 // release-4.17 + github.com/onsi/ginkgo/v2 v2.28.1 + github.com/onsi/gomega v1.39.1 + github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb // release-4.17 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.30.1 - k8s.io/apimachinery v0.30.1 - k8s.io/client-go v0.30.1 - sigs.k8s.io/controller-runtime v0.18.4 + k8s.io/api v0.35.2 + k8s.io/apimachinery v0.35.2 + k8s.io/client-go v0.35.2 + sigs.k8s.io/controller-runtime v0.23.3 ) require ( - github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect + github.com/openshift/controller-runtime-common v0.0.0-20260428152732-64ee174f5e2e + github.com/prometheus-operator/prometheus-operator/pkg/client v0.83.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_model v0.6.2 + k8s.io/apiextensions-apiserver v0.35.1 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 +) + +require ( + cel.dev/expr v0.25.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.17.8 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.18.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.45.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/stoewer/go-strcase v1.2.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect - go.opentelemetry.io/otel v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/sdk v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 // indirect - go.opentelemetry.io/proto/otlp v1.0.0 // indirect + github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.83.0 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/spf13/cobra v1.10.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/oauth2 v0.12.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.44.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect - google.golang.org/grpc v1.58.3 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.30.1 // indirect - k8s.io/apiserver v0.30.1 // indirect - k8s.io/component-base v0.30.1 // indirect - k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + k8s.io/apiserver v0.35.1 // indirect + k8s.io/component-base v0.35.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/infra/feast-operator/go.sum b/infra/feast-operator/go.sum index ef5d6204916..b642252f7d3 100644 --- a/infra/feast-operator/go.sum +++ b/infra/feast-operator/go.sum @@ -1,253 +1,263 @@ -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= -github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= -github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.17.8 h1:j9m730pMZt1Fc4oKhCLUHfjj6527LuhYcYw0Rl8gqto= -github.com/google/cel-go v0.17.8/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= -github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= -github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= -github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= -github.com/openshift/api v0.0.0-20240912201240-0a8800162826 h1:A8D9SN/hJUwAbdO0rPCVTqmuBOctdgurr53gK701SYo= -github.com/openshift/api v0.0.0-20240912201240-0a8800162826/go.mod h1:OOh6Qopf21pSzqNVCB5gomomBXb8o5sGKZxG2KNpaXM= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb h1:iwBR3mzmyE3EMFx7R3CQ9lOccTS0dNht8TW82aGITg0= +github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= +github.com/openshift/controller-runtime-common v0.0.0-20260428152732-64ee174f5e2e h1:k89oIo2EjX0PRSdi1kesktCyWp50SC9WwKurvupvRGs= +github.com/openshift/controller-runtime-common v0.0.0-20260428152732-64ee174f5e2e/go.mod h1:XGabTMnNbz0M5Oa7IbscZp/jmcc7aHobvOCUWwkzKvM= +github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5 h1:9Pe6iVOMjt9CdA/vaKBNUSoEIjIe1po5Ha3ABRYXLJI= +github.com/openshift/library-go v0.0.0-20260213153706-03f1709971c5/go.mod h1:K3FoNLgNBFYbFuG+Kr8usAnQxj1w84XogyUp2M8rK8k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.83.0 h1:j9Ce3W6X6Tzi0QnSap+YzGwpqJLJGP/7xV6P9f86jjM= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.83.0/go.mod h1:sSxwdmprUfmRfTknPc4KIjUd2ZIc/kirw4UdXNhOauM= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.83.0 h1:odshP0+Jo6iUNGpK8MOFA6p5Yj0QOV4yLgiqFU5MVuI= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.83.0/go.mod h1:6Ndhfow0psSp7dV1qp9zK5h++CDKz4eSFWPbrHd5Iic= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0= +github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= +github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= -google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e h1:z3vDksarJxsAKM5dmEGv0GHwE2hKJ096wZra71Vs4sw= -google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= -k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= -k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= -k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= -k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= -k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/apiserver v0.30.1 h1:BEWEe8bzS12nMtDKXzCF5Q5ovp6LjjYkSp8qOPk8LZ8= -k8s.io/apiserver v0.30.1/go.mod h1:i87ZnQ+/PGAmSbD/iEKM68bm1D5reX8fO4Ito4B01mo= -k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= -k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= -k8s.io/component-base v0.30.1 h1:bvAtlPh1UrdaZL20D9+sWxsJljMi0QZ3Lmw+kmZAaxQ= -k8s.io/component-base v0.30.1/go.mod h1:e/X9kDiOebwlI41AvBHuWdqFriSRrX50CdwA9TFaHLI= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 h1:/U5vjBbQn3RChhv7P11uhYvCSm5G2GaIi5AIGBS6r4c= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0/go.mod h1:z7+wmGM2dfIiLRfrC6jb5kV2Mq/sK1ZP303cxzkV5Y4= -sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= -sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= +k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= +k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= +k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= +k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= +k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= +k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= +k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/infra/feast-operator/internal/controller/authz/authz.go b/infra/feast-operator/internal/controller/authz/authz.go index 33a3594607f..9ab8d10c55c 100644 --- a/infra/feast-operator/internal/controller/authz/authz.go +++ b/infra/feast-operator/internal/controller/authz/authz.go @@ -4,7 +4,7 @@ import ( "context" "slices" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" rbacv1 "k8s.io/api/rbac/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -15,16 +15,41 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) +const ( + authenticationAPIGroup = "authentication.k8s.io" + verbCreate = "create" +) + // Deploy the feast authorization func (authz *FeastAuthorization) Deploy() error { if authz.isKubernetesAuth() { + authz.cleanupOidcRbac() return authz.deployKubernetesAuth() } + // Clean up namespace-scoped Kubernetes auth resources authz.removeOrphanedRoles() _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRole()) _ = authz.Handler.DeleteOwnedFeastObj(authz.initFeastRoleBinding()) apimeta.RemoveStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue].Type) + + // Clean up cluster-scoped Kubernetes auth CRB (handles Kubernetes→OIDC or Kubernetes→no-auth transitions) + authz.cleanupKubernetesClusterRbac() + + if authz.isOidcAuth() { + if err := authz.createOidcClusterRole(); err != nil { + return authz.setFeastOidcAuthCondition(err) + } + if err := authz.createOidcClusterRoleBinding(); err != nil { + return authz.setFeastOidcAuthCondition(err) + } + return authz.setFeastOidcAuthCondition(nil) + } + + // No auth - clean up OIDC RBAC and remove condition + authz.cleanupOidcRbac() + apimeta.RemoveStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastOidcAuthConditions[metav1.ConditionTrue].Type) + return nil } @@ -33,6 +58,11 @@ func (authz *FeastAuthorization) isKubernetesAuth() bool { return authzConfig != nil && authzConfig.KubernetesAuthz != nil } +func (authz *FeastAuthorization) isOidcAuth() bool { + authzConfig := authz.Handler.FeatureStore.Status.Applied.AuthzConfig + return authzConfig != nil && authzConfig.OidcAuthz != nil +} + func (authz *FeastAuthorization) deployKubernetesAuth() error { if authz.isKubernetesAuth() { authz.removeOrphanedRoles() @@ -127,32 +157,32 @@ func (authz *FeastAuthorization) setFeastClusterRole(clusterRole *rbacv1.Cluster { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"rolebindings"}, - Verbs: []string{"list"}, + Verbs: []string{verbList}, }, { - APIGroups: []string{"authentication.k8s.io"}, - Resources: []string{"tokenreviews"}, - Verbs: []string{"create"}, + APIGroups: []string{authenticationAPIGroup}, + Resources: []string{resourceTokenReviews}, + Verbs: []string{verbCreate}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"subjectaccessreviews"}, - Verbs: []string{"create"}, + Verbs: []string{verbCreate}, }, { APIGroups: []string{""}, Resources: []string{"namespaces"}, - Verbs: []string{"get", "list", "watch"}, + Verbs: []string{verbGet, verbList, verbWatch}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"clusterroles"}, - Verbs: []string{"get", "list"}, + Verbs: []string{verbGet, verbList}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"clusterrolebindings"}, - Verbs: []string{"get", "list"}, + Verbs: []string{verbGet, verbList}, }, } // Don't set controller reference for shared ClusterRole @@ -213,32 +243,32 @@ func (authz *FeastAuthorization) setFeastRole(role *rbacv1.Role) error { { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"roles", "rolebindings"}, - Verbs: []string{"get", "list", "watch"}, + Verbs: []string{verbGet, verbList, verbWatch}, }, { - APIGroups: []string{"authentication.k8s.io"}, - Resources: []string{"tokenreviews"}, - Verbs: []string{"create"}, + APIGroups: []string{authenticationAPIGroup}, + Resources: []string{resourceTokenReviews}, + Verbs: []string{verbCreate}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"subjectaccessreviews"}, - Verbs: []string{"create"}, + Verbs: []string{verbCreate}, }, { APIGroups: []string{""}, Resources: []string{"namespaces"}, - Verbs: []string{"get", "list", "watch"}, + Verbs: []string{verbGet, verbList, verbWatch}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"clusterroles"}, - Verbs: []string{"get", "list"}, + Verbs: []string{verbGet, verbList}, }, { APIGroups: []string{rbacv1.GroupName}, Resources: []string{"clusterrolebindings"}, - Verbs: []string{"get", "list"}, + Verbs: []string{verbGet, verbList}, }, } @@ -312,24 +342,120 @@ func (authz *FeastAuthorization) setAuthRole(role *rbacv1.Role) error { return controllerutil.SetControllerReference(authz.Handler.FeatureStore, role, authz.Handler.Scheme) } +func (authz *FeastAuthorization) createOidcClusterRole() error { + logger := log.FromContext(authz.Handler.Context) + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Name: authz.getOidcClusterRoleName()}, + } + clusterRole.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRole")) + if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, clusterRole, controllerutil.MutateFn(func() error { + clusterRole.Labels = authz.getSharedOidcClusterRoleLabels() + clusterRole.Rules = []rbacv1.PolicyRule{ + { + APIGroups: []string{authenticationAPIGroup}, + Resources: []string{resourceTokenReviews}, + Verbs: []string{verbCreate}, + }, + } + return nil + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "ClusterRole", clusterRole.Name, "operation", op) + } + return nil +} + +func (authz *FeastAuthorization) createOidcClusterRoleBinding() error { + logger := log.FromContext(authz.Handler.Context) + crb := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: authz.getOidcClusterRoleBindingName()}, + } + crb.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding")) + if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, crb, controllerutil.MutateFn(func() error { + crb.Labels = authz.getLabels() + crb.Subjects = []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: authz.getFeastServiceAccountName(), + Namespace: authz.Handler.FeatureStore.Namespace, + }, + } + crb.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: authz.getOidcClusterRoleName(), + } + return nil + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "ClusterRoleBinding", crb.Name, "operation", op) + } + return nil +} + +func (authz *FeastAuthorization) cleanupOidcRbac() { + crb := &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: authz.getOidcClusterRoleBindingName()}} + crb.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding")) + _ = authz.Handler.Client.Delete(authz.Handler.Context, crb) +} + +func (authz *FeastAuthorization) cleanupKubernetesClusterRbac() { + crb := &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: authz.getFeastClusterRoleBindingName()}} + crb.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding")) + if err := authz.Handler.Client.Get(authz.Handler.Context, client.ObjectKeyFromObject(crb), crb); err != nil { + return + } + _ = authz.Handler.Client.Delete(authz.Handler.Context, crb) +} + +func (authz *FeastAuthorization) getOidcClusterRoleName() string { + return "feast-oidc-token-review" +} + +func (authz *FeastAuthorization) getOidcClusterRoleBindingName() string { + return services.GetFeastName(authz.Handler.FeatureStore) + "-oidc-token-review" +} + func (authz *FeastAuthorization) getLabels() map[string]string { return map[string]string{ services.NameLabelKey: authz.Handler.FeatureStore.Name, services.ServiceTypeLabelKey: string(services.AuthzFeastType), + services.ManagedByLabelKey: services.ManagedByLabelValue, + } +} + +func (authz *FeastAuthorization) getSharedOidcClusterRoleLabels() map[string]string { + return map[string]string{ + services.ServiceTypeLabelKey: string(services.AuthzFeastType), + services.ManagedByLabelKey: services.ManagedByLabelValue, } } +func (authz *FeastAuthorization) setFeastOidcAuthCondition(err error) error { + if err != nil { + logger := log.FromContext(authz.Handler.Context) + cond := feastOidcAuthConditions[metav1.ConditionFalse] + cond.Message = services.ErrorMessagePrefix + err.Error() + apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, cond) + logger.Error(err, "Error deploying the OIDC authorization") + return err + } + apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastOidcAuthConditions[metav1.ConditionTrue]) + return nil +} + func (authz *FeastAuthorization) setFeastKubernetesAuthCondition(err error) error { if err != nil { logger := log.FromContext(authz.Handler.Context) cond := feastKubernetesAuthConditions[metav1.ConditionFalse] - cond.Message = "Error: " + err.Error() + cond.Message = services.ErrorMessagePrefix + err.Error() apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, cond) logger.Error(err, "Error deploying the Kubernetes authorization") return err - } else { - apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue]) } + apimeta.SetStatusCondition(&authz.Handler.FeatureStore.Status.Conditions, feastKubernetesAuthConditions[metav1.ConditionTrue]) return nil } @@ -337,7 +463,7 @@ func (authz *FeastAuthorization) getFeastRoleName() string { return GetFeastRoleName(authz.Handler.FeatureStore) } -func GetFeastRoleName(featureStore *feastdevv1alpha1.FeatureStore) string { +func GetFeastRoleName(featureStore *feastdevv1.FeatureStore) string { return services.GetFeastName(featureStore) } @@ -345,7 +471,7 @@ func (authz *FeastAuthorization) getFeastClusterRoleName() string { return GetFeastClusterRoleName(authz.Handler.FeatureStore) } -func GetFeastClusterRoleName(featureStore *feastdevv1alpha1.FeatureStore) string { +func GetFeastClusterRoleName(featureStore *feastdevv1.FeatureStore) string { // Use a shared ClusterRole name for all Feast instances // This allows multiple FeatureStores to share the same Token Access Review permissions return "feast-token-review-cluster-role" @@ -355,7 +481,7 @@ func (authz *FeastAuthorization) getFeastClusterRoleBindingName() string { return GetFeastClusterRoleBindingName(authz.Handler.FeatureStore) } -func GetFeastClusterRoleBindingName(featureStore *feastdevv1alpha1.FeatureStore) string { +func GetFeastClusterRoleBindingName(featureStore *feastdevv1.FeatureStore) string { return services.GetFeastName(featureStore) + "-cluster-binding" } diff --git a/infra/feast-operator/internal/controller/authz/authz_types.go b/infra/feast-operator/internal/controller/authz/authz_types.go index f955f5b40f1..388ab179e7e 100644 --- a/infra/feast-operator/internal/controller/authz/authz_types.go +++ b/infra/feast-operator/internal/controller/authz/authz_types.go @@ -1,7 +1,7 @@ package authz import ( - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -11,18 +11,41 @@ type FeastAuthorization struct { Handler handler.FeastHandler } +const ( + // RBAC verbs + verbGet = "get" + verbList = "list" + verbWatch = "watch" + + // RBAC resources + resourceTokenReviews = "tokenreviews" +) + var ( feastKubernetesAuthConditions = map[metav1.ConditionStatus]metav1.Condition{ metav1.ConditionTrue: { - Type: feastdevv1alpha1.AuthorizationReadyType, + Type: feastdevv1.AuthorizationReadyType, + Status: metav1.ConditionTrue, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.KubernetesAuthzReadyMessage, + }, + metav1.ConditionFalse: { + Type: feastdevv1.AuthorizationReadyType, + Status: metav1.ConditionFalse, + Reason: feastdevv1.KubernetesAuthzFailedReason, + }, + } + feastOidcAuthConditions = map[metav1.ConditionStatus]metav1.Condition{ + metav1.ConditionTrue: { + Type: feastdevv1.AuthorizationReadyType, Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.KubernetesAuthzReadyMessage, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.OidcAuthzReadyMessage, }, metav1.ConditionFalse: { - Type: feastdevv1alpha1.AuthorizationReadyType, + Type: feastdevv1.AuthorizationReadyType, Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.KubernetesAuthzFailedReason, + Reason: feastdevv1.OidcAuthzFailedReason, }, } ) diff --git a/infra/feast-operator/internal/controller/featurestore_controller.go b/infra/feast-operator/internal/controller/featurestore_controller.go index 74d8c0af9ab..b94808c0df5 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller.go +++ b/infra/feast-operator/internal/controller/featurestore_controller.go @@ -22,13 +22,17 @@ import ( "time" appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -36,9 +40,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/authz" feasthandler "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + feastmetrics "github.com/feast-dev/feast/infra/feast-operator/internal/controller/metrics" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" routev1 "github.com/openshift/api/route/v1" ) @@ -51,20 +56,29 @@ const ( // FeatureStoreReconciler reconciles a FeatureStore object type FeatureStoreReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + Metrics *feastmetrics.FeatureStoreMetrics } +// +kubebuilder:rbac:groups=config.openshift.io,resources=apiservers,verbs=get;list;watch // +kubebuilder:rbac:groups=feast.dev,resources=featurestores,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=feast.dev,resources=featurestores/status,verbs=get;update;patch // +kubebuilder:rbac:groups=feast.dev,resources=featurestores/finalizers,verbs=update // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;create;update;watch;delete -// +kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims;serviceaccounts,verbs=get;list;create;update;watch;delete +// +kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims,verbs=get;list;create;update;watch;delete;deletecollection +// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;create;update;watch;delete // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings;clusterroles;clusterrolebindings;subjectaccessreviews,verbs=get;list;create;update;watch;delete -// +kubebuilder:rbac:groups=core,resources=secrets;pods;namespaces,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=secrets;namespaces,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;delete;deletecollection // +kubebuilder:rbac:groups=core,resources=pods/exec,verbs=create +// +kubebuilder:rbac:groups=core,resources=pods/log,verbs=get +// +kubebuilder:rbac:groups=sparkoperator.k8s.io,resources=sparkapplications,verbs=create;get;delete // +kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create // +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;create;update;watch;delete // +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;list;watch;create;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -73,14 +87,18 @@ type FeatureStoreReconciler struct { func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, recErr error) { logger := log.FromContext(ctx) - cr := &feastdevv1alpha1.FeatureStore{} + // Get the FeatureStore using v1 (storage version) + cr := &feastdevv1.FeatureStore{} err := r.Get(ctx, req.NamespacedName, cr) if err != nil { if apierrors.IsNotFound(err) { // CR deleted since request queued, child objects getting GC'd, no requeue logger.V(1).Info("FeatureStore CR not found, has been deleted") + if r.Metrics != nil { + r.Metrics.DeleteFeatureStore(req.NamespacedName.Namespace, req.NamespacedName.Name) + } // Clean up namespace registry entry even if the CR is not found - if err := r.cleanupNamespaceRegistry(ctx, &feastdevv1alpha1.FeatureStore{ + if err := r.cleanupNamespaceRegistry(ctx, &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: req.NamespacedName.Name, Namespace: req.NamespacedName.Namespace, @@ -91,7 +109,6 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request } return ctrl.Result{}, nil } - // error fetching FeatureStore instance, requeue and try again logger.Error(err, "Unable to get FeatureStore CR") return ctrl.Result{}, err } @@ -100,6 +117,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request // Handle deletion - clean up namespace registry entry if cr.DeletionTimestamp != nil { logger.Info("FeatureStore is being deleted, cleaning up namespace registry entry") + if r.Metrics != nil { + r.Metrics.DeleteFeatureStore(cr.Namespace, cr.Name) + } if err := r.cleanupNamespaceRegistry(ctx, cr); err != nil { logger.Error(err, "Failed to clean up namespace registry entry") return ctrl.Result{}, err @@ -108,6 +128,9 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request } result, recErr = r.deployFeast(ctx, cr) + if recErr == nil && r.Metrics != nil { + r.Metrics.RecordFeatureStore(cr) + } if cr.DeletionTimestamp == nil && !reflect.DeepEqual(currentStatus, cr.Status) { if err = r.Client.Status().Update(ctx, cr); err != nil { if apierrors.IsConflict(err) { @@ -142,13 +165,13 @@ func (r *FeatureStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request return result, recErr } -func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1alpha1.FeatureStore) (result ctrl.Result, err error) { +func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1.FeatureStore) (result ctrl.Result, err error) { logger := log.FromContext(ctx) condition := metav1.Condition{ - Type: feastdevv1alpha1.ReadyType, + Type: feastdevv1.ReadyType, Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.ReadyMessage, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.ReadyMessage, } feast := services.FeastServices{ Handler: feasthandler.FeastHandler{ @@ -173,30 +196,34 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 } if err != nil { condition = metav1.Condition{ - Type: feastdevv1alpha1.ReadyType, + Type: feastdevv1.ReadyType, Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.FailedReason, + Reason: feastdevv1.FailedReason, Message: "Error: " + err.Error(), } } else { deployment, deploymentErr := feast.GetDeployment() if deploymentErr != nil { condition = metav1.Condition{ - Type: feastdevv1alpha1.ReadyType, + Type: feastdevv1.ReadyType, Status: metav1.ConditionUnknown, - Reason: feastdevv1alpha1.DeploymentNotAvailableReason, - Message: feastdevv1alpha1.DeploymentNotAvailableMessage, + Reason: feastdevv1.DeploymentNotAvailableReason, + Message: feastdevv1.DeploymentNotAvailableMessage, } result = errResult } else { isDeployAvailable := services.IsDeploymentAvailable(deployment.Status.Conditions) if !isDeployAvailable { + msg := feastdevv1.DeploymentNotAvailableMessage + if podMsg := feast.GetPodContainerFailureMessage(deployment); podMsg != "" { + msg = msg + ": " + podMsg + } condition = metav1.Condition{ - Type: feastdevv1alpha1.ReadyType, + Type: feastdevv1.ReadyType, Status: metav1.ConditionUnknown, - Reason: feastdevv1alpha1.DeploymentNotAvailableReason, - Message: feastdevv1alpha1.DeploymentNotAvailableMessage, + Reason: feastdevv1.DeploymentNotAvailableReason, + Message: msg, } result = errResult @@ -206,12 +233,12 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 logger.Info(condition.Message) apimeta.SetStatusCondition(&cr.Status.Conditions, condition) - if apimeta.IsStatusConditionTrue(cr.Status.Conditions, feastdevv1alpha1.ReadyType) { - cr.Status.Phase = feastdevv1alpha1.ReadyPhase - } else if apimeta.IsStatusConditionFalse(cr.Status.Conditions, feastdevv1alpha1.ReadyType) { - cr.Status.Phase = feastdevv1alpha1.FailedPhase + if apimeta.IsStatusConditionTrue(cr.Status.Conditions, feastdevv1.ReadyType) { + cr.Status.Phase = feastdevv1.ReadyPhase + } else if apimeta.IsStatusConditionFalse(cr.Status.Conditions, feastdevv1.ReadyType) { + cr.Status.Phase = feastdevv1.FailedPhase } else { - cr.Status.Phase = feastdevv1alpha1.PendingPhase + cr.Status.Phase = feastdevv1.PendingPhase } return result, err @@ -220,7 +247,7 @@ func (r *FeatureStoreReconciler) deployFeast(ctx context.Context, cr *feastdevv1 // SetupWithManager sets up the controller with the Manager. func (r *FeatureStoreReconciler) SetupWithManager(mgr ctrl.Manager) error { bldr := ctrl.NewControllerManagedBy(mgr). - For(&feastdevv1alpha1.FeatureStore{}). + For(&feastdevv1.FeatureStore{}). Owns(&corev1.ConfigMap{}). Owns(&appsv1.Deployment{}). Owns(&corev1.Service{}). @@ -229,17 +256,29 @@ func (r *FeatureStoreReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&rbacv1.RoleBinding{}). Owns(&rbacv1.Role{}). Owns(&batchv1.CronJob{}). - Watches(&feastdevv1alpha1.FeatureStore{}, handler.EnqueueRequestsFromMapFunc(r.mapFeastRefsToFeastRequests)) + Owns(&autoscalingv2.HorizontalPodAutoscaler{}). + Owns(&policyv1.PodDisruptionBudget{}). + Watches(&feastdevv1.FeatureStore{}, handler.EnqueueRequestsFromMapFunc(r.mapFeastRefsToFeastRequests)) + if services.IsOpenShift() { bldr = bldr.Owns(&routev1.Route{}) } + if services.HasServiceMonitorCRD() { + sm := &unstructured.Unstructured{} + sm.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "monitoring.coreos.com", + Version: "v1", + Kind: "ServiceMonitor", + }) + bldr = bldr.Owns(sm) + } return bldr.Complete(r) } // cleanupNamespaceRegistry removes the feature store instance from the namespace registry -func (r *FeatureStoreReconciler) cleanupNamespaceRegistry(ctx context.Context, cr *feastdevv1alpha1.FeatureStore) error { +func (r *FeatureStoreReconciler) cleanupNamespaceRegistry(ctx context.Context, cr *feastdevv1.FeatureStore) error { feast := services.FeastServices{ Handler: feasthandler.FeastHandler{ Client: r.Client, @@ -255,10 +294,15 @@ func (r *FeatureStoreReconciler) cleanupNamespaceRegistry(ctx context.Context, c // if a remotely referenced FeatureStore is changed, reconcile any FeatureStores that reference it. func (r *FeatureStoreReconciler) mapFeastRefsToFeastRequests(ctx context.Context, object client.Object) []reconcile.Request { logger := log.FromContext(ctx) - feastRef := object.(*feastdevv1alpha1.FeatureStore) + + feastRef, ok := object.(*feastdevv1.FeatureStore) + if !ok { + logger.Error(nil, "Unexpected object type in mapFeastRefsToFeastRequests") + return nil + } // list all FeatureStores in the cluster - var feastList feastdevv1alpha1.FeatureStoreList + var feastList feastdevv1.FeatureStoreList if err := r.List(ctx, &feastList, client.InNamespace("")); err != nil { logger.Error(err, "could not list FeatureStores. "+ "FeatureStores affected by changes to the referenced FeatureStore object will not be reconciled.") diff --git a/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go b/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go index b72e191ae34..11ae2af7777 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go @@ -30,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -45,24 +45,24 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} BeforeEach(func() { By("creating the custom resource for the Kind FeatureStore") err := k8sClient.Get(ctx, typeNamespacedName, featurestore) if err != nil && errors.IsNotFound(err) { - resource := &feastdevv1alpha1.FeatureStore{ + resource := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, - Spec: feastdevv1alpha1.FeatureStoreSpec{FeastProject: feastProject}, + Spec: feastdevv1.FeatureStoreSpec{FeastProject: feastProject}, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -82,7 +82,7 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ @@ -98,14 +98,16 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { Expect(resource.Status).NotTo(BeNil()) Expect(resource.Status.CronJob).To(Equal(objMeta.Name)) Expect(resource.Status.Applied.CronJob.Schedule).NotTo(BeEmpty()) + Expect(resource.Status.Applied.Services.RunFeastApplyOnInit).NotTo(BeNil()) + Expect(*resource.Status.Applied.Services.RunFeastApplyOnInit).To(BeTrue()) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.CronJobReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.CronJobReadyType) Expect(cond).ToNot(BeNil()) - Expect(cond.Type).To(Equal(feastdevv1alpha1.CronJobReadyType)) + Expect(cond.Type).To(Equal(feastdevv1.CronJobReadyType)) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.CronJobReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Message).To(Equal(feastdevv1.CronJobReadyMessage)) // check CronJob cronJob := &batchv1.CronJob{} @@ -137,7 +139,7 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -151,7 +153,7 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { replacementPolicy := batchv1.TerminatingOrFailed always := corev1.PullAlways startingDeadlineSeconds := int64(6) - resource.Spec.CronJob = &feastdevv1alpha1.FeastCronJob{ + resource.Spec.CronJob = &feastdevv1.FeastCronJob{ Schedule: schedule, StartingDeadlineSeconds: &startingDeadlineSeconds, TimeZone: &timeZone, @@ -159,7 +161,7 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { Suspend: &suspend, SuccessfulJobsHistoryLimit: &int32Var, FailedJobsHistoryLimit: &int32Var, - ContainerConfigs: &feastdevv1alpha1.CronJobContainerConfigs{ + ContainerConfigs: &feastdevv1.CronJobContainerConfigs{ Commands: []string{ "feast apply", "feast feature-views list", @@ -167,11 +169,11 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { "feast on-demand-feature-views list", "feast projects list", }, - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ Image: &image, }, - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ Env: &[]corev1.EnvVar{ { Name: "test", @@ -182,7 +184,7 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { }, }, }, - JobSpec: &feastdevv1alpha1.JobSpec{ + JobSpec: &feastdevv1.JobSpec{ Parallelism: &int32Var, Completions: &int32Var, ActiveDeadlineSeconds: &int64Var, @@ -223,12 +225,12 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { Expect(resource.Status.Applied.CronJob.FailedJobsHistoryLimit).To(Equal(&int32Var)) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.CronJobReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.CronJobReadyType) Expect(cond).ToNot(BeNil()) - Expect(cond.Type).To(Equal(feastdevv1alpha1.CronJobReadyType)) + Expect(cond.Type).To(Equal(feastdevv1.CronJobReadyType)) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.CronJobReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Message).To(Equal(feastdevv1.CronJobReadyMessage)) // check CronJob cronJob := &batchv1.CronJob{} @@ -247,7 +249,7 @@ var _ = Describe("FeatureStore Controller - Feast CronJob", func() { }) }) -func checkCronJob(appliedFeastCronJob *feastdevv1alpha1.FeastCronJob, cronSpec batchv1.CronJobSpec) { +func checkCronJob(appliedFeastCronJob *feastdevv1.FeastCronJob, cronSpec batchv1.CronJobSpec) { Expect(appliedFeastCronJob).NotTo(BeNil()) Expect(appliedFeastCronJob.Schedule).To(Equal(cronSpec.Schedule)) if len(appliedFeastCronJob.ConcurrencyPolicy) > 0 { @@ -272,7 +274,7 @@ func checkCronJob(appliedFeastCronJob *feastdevv1alpha1.FeastCronJob, cronSpec b checkCronJobContainers(appliedFeastCronJob.ContainerConfigs, cronSpec.JobTemplate.Spec.Template.Spec) } -func checkJobSpec(appliedFeastJobSpec *feastdevv1alpha1.JobSpec, jobSpec batchv1.JobSpec) { +func checkJobSpec(appliedFeastJobSpec *feastdevv1.JobSpec, jobSpec batchv1.JobSpec) { if appliedFeastJobSpec != nil { if appliedFeastJobSpec.ActiveDeadlineSeconds != nil { Expect(appliedFeastJobSpec.ActiveDeadlineSeconds).To(Equal(jobSpec.ActiveDeadlineSeconds)) @@ -310,7 +312,7 @@ func checkJobSpec(appliedFeastJobSpec *feastdevv1alpha1.JobSpec, jobSpec batchv1 } } -func checkCronJobContainers(cronJobContainerConfigs *feastdevv1alpha1.CronJobContainerConfigs, podSpec corev1.PodSpec) { +func checkCronJobContainers(cronJobContainerConfigs *feastdevv1.CronJobContainerConfigs, podSpec corev1.PodSpec) { Expect(cronJobContainerConfigs).NotTo(BeNil()) Expect(cronJobContainerConfigs.Image).NotTo(BeNil()) Expect(cronJobContainerConfigs.Commands).NotTo(BeEmpty()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go index 879774b2cd5..08276293032 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_db_store_test.go @@ -39,7 +39,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -132,25 +132,25 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { typeNamespacedName := types.NamespacedName{ Name: resourceName, - Namespace: "default", + Namespace: services.DefaultNs, } offlineSecretNamespacedName := types.NamespacedName{ - Name: "offline-store-secret", - Namespace: "default", + Name: services.OfflineStoreSecretName, + Namespace: services.DefaultNs, } onlineSecretNamespacedName := types.NamespacedName{ - Name: "online-store-secret", - Namespace: "default", + Name: services.OnlineStoreSecretName, + Namespace: services.DefaultNs, } registrySecretNamespacedName := types.NamespacedName{ - Name: "registry-store-secret", - Namespace: "default", + Name: services.RegistryStoreSecretName, + Namespace: services.DefaultNs, } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} offlineType := services.OfflineDBPersistenceSnowflakeConfigType onlineType := services.OnlineDBPersistenceCassandraConfigType registryType := services.RegistryDBPersistenceSQLConfigType @@ -208,27 +208,27 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { err = k8sClient.Get(ctx, typeNamespacedName, featurestore) if err != nil && errors.IsNotFound(err) { resource := createFeatureStoreResource(resourceName, image, pullPolicy, &[]corev1.EnvVar{}, withEnvFrom()) - resource.Spec.Services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{ - DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + resource.Spec.Services.OfflineStore.Persistence = &feastdevv1.OfflineStorePersistence{ + DBPersistence: &feastdevv1.OfflineStoreDBStorePersistence{ Type: string(offlineType), SecretRef: corev1.LocalObjectReference{ - Name: "offline-store-secret", + Name: services.OfflineStoreSecretName, }, }, } - resource.Spec.Services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{ - DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + resource.Spec.Services.OnlineStore.Persistence = &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ Type: string(onlineType), SecretRef: corev1.LocalObjectReference{ - Name: "online-store-secret", + Name: services.OnlineStoreSecretName, }, }, } - resource.Spec.Services.Registry.Local.Persistence = &feastdevv1alpha1.RegistryPersistence{ - DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + resource.Spec.Services.Registry.Local.Persistence = &feastdevv1.RegistryPersistence{ + DBPersistence: &feastdevv1.RegistryDBStorePersistence{ Type: string(registryType), SecretRef: corev1.LocalObjectReference{ - Name: "registry-store-secret", + Name: services.RegistryStoreSecretName, }, SecretKeyName: "sql_custom_registry_key", }, @@ -250,7 +250,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { err = k8sClient.Get(ctx, registrySecretNamespacedName, registrySecret) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -267,7 +267,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { It("should fail reconciling the resource", func() { By("Referring to a secret that doesn't exist") - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -287,14 +287,14 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(err.Error()).To(Equal("secrets \"invalid_secret\" not found")) By("Referring to a secret with a key that doesn't exist") - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: services.OnlineStoreSecretName} // pragma: allowlist secret resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "invalid.secret.key" Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -306,7 +306,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(err.Error()).To(Equal("secret key invalid.secret.key doesn't exist in secret online-store-secret")) By("Referring to a secret that contains parameter named type with invalid value") - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -316,10 +316,10 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(invalidSecretTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) - resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: services.OnlineStoreSecretName} // pragma: allowlist secret resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -343,7 +343,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -364,7 +364,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(resource.Status.Applied.Services.OfflineStore.Persistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.Type).To(Equal(string(offlineType))) - Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "offline-store-secret"})) + Expect(resource.Status.Applied.Services.OfflineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: services.OfflineStoreSecretName})) Expect(resource.Status.Applied.Services.OfflineStore.Server.ImagePullPolicy).To(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Server.Resources).To(BeNil()) Expect(resource.Status.Applied.Services.OfflineStore.Server.Image).To(Equal(&services.DefaultImage)) @@ -372,7 +372,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(resource.Status.Applied.Services.OnlineStore.Persistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.Type).To(Equal(string(onlineType))) - Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "online-store-secret"})) + Expect(resource.Status.Applied.Services.OnlineStore.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: services.OnlineStoreSecretName})) Expect(resource.Status.Applied.Services.OnlineStore.Server.ImagePullPolicy).To(Equal(&pullPolicy)) Expect(resource.Status.Applied.Services.OnlineStore.Server.Resources).NotTo(BeNil()) Expect(resource.Status.Applied.Services.OnlineStore.Server.Image).To(Equal(&image)) @@ -381,7 +381,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(resource.Status.Applied.Services.Registry.Local.Persistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence).NotTo(BeNil()) Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.Type).To(Equal(string(registryType))) - Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: "registry-store-secret"})) + Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretRef).To(Equal(corev1.LocalObjectReference{Name: services.RegistryStoreSecretName})) Expect(resource.Status.Applied.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName).To(Equal("sql_custom_registry_key")) Expect(resource.Status.Applied.Services.Registry.Local.Server.ImagePullPolicy).To(BeNil()) Expect(resource.Status.Applied.Services.Registry.Local.Server.Resources).To(BeNil()) @@ -392,42 +392,42 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + domain)) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.DeploymentNotAvailableReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.DeploymentNotAvailableMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.DeploymentNotAvailableReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OfflineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.PendingPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) // check deployment deploy := &appsv1.Deployment{} @@ -451,7 +451,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(svc.Spec.Ports[0].TargetPort).To(Equal(intstr.FromInt(int(services.FeastServiceConstants[services.RegistryFeastType].TargetHttpPort)))) By("Referring to a secret that contains parameter named type") - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -461,10 +461,10 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret.Data[string(services.OnlineDBPersistenceCassandraConfigType)] = []byte(secretContainingValidTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) - resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "online-store-secret"} + resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: services.OnlineStoreSecretName} // pragma: allowlist secret resource.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretKeyName = "" Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -475,7 +475,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { Expect(err).To(Not(HaveOccurred())) By("Referring to a secret that contains parameter named registry_type") - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -492,10 +492,10 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { secret.Data[string(services.RegistryDBPersistenceSQLConfigType)] = []byte(invalidSecretRegistryTypeYamlString) Expect(k8sClient.Update(ctx, secret)).To(Succeed()) - resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: "registry-store-secret"} + resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: services.RegistryStoreSecretName} // pragma: allowlist secret resource.Spec.Services.Registry.Local.Persistence.DBPersistence.SecretKeyName = "" Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -517,7 +517,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -574,18 +574,24 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { dbParametersMap := unmarshallYamlString(sqlTypeYamlString) copyMap := services.CopyMap(dbParametersMap) delete(dbParametersMap, "path") + // Expect cache_ttl_seconds to be mapped into Registry.CacheTTLSeconds + ttlVal, ok := copyMap["cache_ttl_seconds"].(int) + Expect(ok).To(BeTrue()) + ttl := int32(ttlVal) + delete(dbParametersMap, "cache_ttl_seconds") testConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: services.OfflineStoreConfig{ Type: services.OfflineDBPersistenceSnowflakeConfigType, DBParameters: unmarshallYamlString(snowflakeYamlString), }, Registry: services.RegistryConfig{ - Path: copyMap["path"].(string), - RegistryType: services.RegistryDBPersistenceSQLConfigType, - DBParameters: dbParametersMap, + Path: copyMap["path"].(string), + RegistryType: services.RegistryDBPersistenceSQLConfigType, + CacheTTLSeconds: &ttl, + DBParameters: dbParametersMap, }, OnlineStore: services.OnlineStoreConfig{ Type: onlineType, @@ -657,7 +663,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { clientConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: offlineRemote, OnlineStore: services.OnlineStoreConfig{ Path: fmt.Sprintf("http://feast-%s-online.default.svc.cluster.local:80", resourceName), @@ -670,7 +676,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { // change paths and reconcile resourceNew := resource.DeepCopy() - newOnlineSecretName := "offline-store-secret" + newOnlineSecretName := services.OfflineStoreSecretName // pragma: allowlist secret newOnlineDBPersistenceType := services.OnlineDBPersistenceSnowflakeConfigType resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.Type = string(newOnlineDBPersistenceType) resourceNew.Spec.Services.OnlineStore.Persistence.DBPersistence.SecretRef = corev1.LocalObjectReference{Name: newOnlineSecretName} @@ -682,7 +688,7 @@ var _ = Describe("FeatureStore Controller - db storage services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource diff --git a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go index 5c2270bcaad..212fa80228b 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_ephemeral_test.go @@ -38,7 +38,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -57,7 +57,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} onlineStorePath := "/data/online.db" registryPath := "/data/registry.db" @@ -68,18 +68,18 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { if err != nil && errors.IsNotFound(err) { resource := createFeatureStoreResource(resourceName, image, pullPolicy, &[]corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.namespace"}}}}, withEnvFrom()) - resource.Spec.Services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ + resource.Spec.Services.OfflineStore.Persistence = &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ Type: offlineType, }, } - resource.Spec.Services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ + resource.Spec.Services.OnlineStore.Persistence = &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ Path: onlineStorePath, }, } - resource.Spec.Services.Registry.Local.Persistence = &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + resource.Spec.Services.Registry.Local.Persistence = &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: registryPath, }, } @@ -88,7 +88,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -110,7 +110,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -158,45 +158,45 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + domain)) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.DeploymentNotAvailableReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.DeploymentNotAvailableMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.DeploymentNotAvailableReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) Expect(cond).To(BeNil()) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OfflineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.PendingPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) // check deployment deploy := &appsv1.Deployment{} @@ -232,7 +232,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -290,7 +290,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { testConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: services.OfflineStoreConfig{ Type: services.OfflineFilePersistenceDuckDbConfigType, }, @@ -358,7 +358,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { clientConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: services.OfflineStoreConfig{ Host: fmt.Sprintf("feast-%s-offline.default.svc.cluster.local", resourceName), Type: services.OfflineRemoteConfigType, @@ -389,7 +389,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource diff --git a/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go index 386b7c59902..3bfab485e85 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_kubernetes_auth_test.go @@ -38,7 +38,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/authz" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" @@ -55,7 +55,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} roles := []string{"reader", "writer"} BeforeEach(func() { @@ -65,7 +65,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { err := k8sClient.Get(ctx, typeNamespacedName, featurestore) if err != nil && errors.IsNotFound(err) { resource := createFeatureStoreResource(resourceName, image, pullPolicy, &[]corev1.EnvVar{}, withEnvFrom()) - resource.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{KubernetesAuthz: &feastdevv1alpha1.KubernetesAuthz{ + resource.Spec.AuthzConfig = &feastdevv1.AuthzConfig{KubernetesAuthz: &feastdevv1.KubernetesAuthz{ Roles: roles, }} @@ -73,7 +73,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -95,7 +95,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -111,8 +111,8 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Expect(resource.Status.FeastVersion).To(Equal(feastversion.FeastVersion)) Expect(resource.Status.ClientConfigMap).To(Equal(feast.GetFeastServiceName(services.ClientFeastType))) Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) - expectedAuthzConfig := &feastdevv1alpha1.AuthzConfig{ - KubernetesAuthz: &feastdevv1alpha1.KubernetesAuthz{ + expectedAuthzConfig := &feastdevv1.AuthzConfig{ + KubernetesAuthz: &feastdevv1.KubernetesAuthz{ Roles: roles, }, } @@ -148,49 +148,49 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + domain)) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.DeploymentNotAvailableReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.DeploymentNotAvailableMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.DeploymentNotAvailableReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.AuthorizationReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.KubernetesAuthzReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.AuthorizationReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.KubernetesAuthzReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OfflineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.PendingPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) // check deployment deploy := &appsv1.Deployment{} @@ -280,7 +280,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource @@ -318,7 +318,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource @@ -357,7 +357,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -482,7 +482,7 @@ var _ = Describe("FeatureStore Controller-Kubernetes authorization", func() { clientConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: offlineRemote, OnlineStore: services.OnlineStoreConfig{ Path: fmt.Sprintf("http://feast-%s-online.default.svc.cluster.local:80", resourceName), diff --git a/infra/feast-operator/internal/controller/featurestore_controller_loglevel_test.go b/infra/feast-operator/internal/controller/featurestore_controller_loglevel_test.go index a02d0894c8c..948ddec210d 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_loglevel_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_loglevel_test.go @@ -30,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -45,40 +45,40 @@ var _ = Describe("FeatureStore Controller - Feast service LogLevel", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} BeforeEach(func() { By("creating the custom resource for the Kind FeatureStore") err := k8sClient.Get(ctx, typeNamespacedName, featurestore) if err != nil && errors.IsNotFound(err) { - resource := &feastdevv1alpha1.FeatureStore{ + resource := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: feastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{ + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ LogLevel: strPtr("error"), }, }, }, }, - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ LogLevel: strPtr("debug"), }, }, - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{ LogLevel: strPtr("info"), }, }, - UI: &feastdevv1alpha1.ServerConfigs{ + UI: &feastdevv1.ServerConfigs{ LogLevel: strPtr("info"), }, }, @@ -88,7 +88,7 @@ var _ = Describe("FeatureStore Controller - Feast service LogLevel", func() { } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -108,7 +108,7 @@ var _ = Describe("FeatureStore Controller - Feast service LogLevel", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ @@ -129,41 +129,41 @@ var _ = Describe("FeatureStore Controller - Feast service LogLevel", func() { Expect(resource.Status.Applied.Services.Registry).NotTo(BeNil()) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.DeploymentNotAvailableReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.DeploymentNotAvailableMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.DeploymentNotAvailableReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OfflineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.PendingPhase)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) // check deployment deploy := &appsv1.Deployment{} @@ -191,20 +191,20 @@ var _ = Describe("FeatureStore Controller - Feast service LogLevel", func() { It("should not include --log-level parameter when logLevel is not specified for any service", func() { By("Updating the FeatureStore resource without specifying logLevel for any service") - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - resource.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{}, + resource.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{}, }, }, }, - OfflineStore: &feastdevv1alpha1.OfflineStore{}, - UI: &feastdevv1alpha1.ServerConfigs{}, + OfflineStore: &feastdevv1.OfflineStore{}, + UI: &feastdevv1.ServerConfigs{}, } Expect(k8sClient.Update(ctx, resource)).To(Succeed()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_namespace_registry_test.go b/infra/feast-operator/internal/controller/featurestore_controller_namespace_registry_test.go index 67fd4dcb632..b92d5cec50e 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_namespace_registry_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_namespace_registry_test.go @@ -31,7 +31,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -51,7 +51,7 @@ var _ = Describe("FeatureStore Controller - Namespace Registry", func() { Name: resourceName, Namespace: DefaultNamespace, } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} BeforeEach(func() { By("Ensuring manager namespace exists") diff --git a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go index 7ad4d04e9eb..a326752a78f 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_objectstore_test.go @@ -37,7 +37,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -55,7 +55,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} registryPath := "s3://bucket/registry.db" s3AdditionalKwargs := map[string]string{ @@ -73,8 +73,8 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.namespace"}}}}, withEnvFrom()) resource.Spec.Services.UI = nil resource.Spec.Services.OfflineStore = nil - resource.Spec.Services.Registry.Local.Persistence = &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + resource.Spec.Services.Registry.Local.Persistence = &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: registryPath, S3AdditionalKwargs: &s3AdditionalKwargs, }, @@ -83,7 +83,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -105,7 +105,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -143,37 +143,37 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + domain)) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.DeploymentNotAvailableReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.DeploymentNotAvailableMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.DeploymentNotAvailableReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) Expect(cond).To(BeNil()) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).To(BeNil()) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).NotTo(BeNil()) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.PendingPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) // check deployment deploy := &appsv1.Deployment{} @@ -185,7 +185,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(2)) Expect(services.GetRegistryContainer(*deploy)).NotTo(BeNil()) Expect(services.GetOnlineContainer(*deploy)).NotTo(BeNil()) @@ -208,7 +208,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource @@ -239,7 +239,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -342,7 +342,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource @@ -367,5 +367,67 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { testConfig.Registry.S3AdditionalKwargs = nil Expect(repoConfig).To(Equal(&testConfig)) }) + + It("should propagate registry file cache settings into repo config", func() { + By("Reconciling the created resource with registry cache settings") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // update the FeatureStore to set cache settings on registry file persistence + resource := &feastdevv1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + ttl := int32(300) + mode := "thread" + resource.Spec.Services.Registry.Local.Persistence.FilePersistence.CacheTTLSeconds = &ttl + resource.Spec.Services.Registry.Local.Persistence.FilePersistence.CacheMode = &mode + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + // fetch updated resource and deployment + resource = &feastdevv1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + deploy := &appsv1.Deployment{} + objMeta := feast.GetObjectMeta() + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: objMeta.Name, + Namespace: objMeta.Namespace, + }, deploy) + Expect(err).NotTo(HaveOccurred()) + + env := getFeatureStoreYamlEnvVar(services.GetRegistryContainer(*deploy).Env) + Expect(env).NotTo(BeNil()) + + // decode feature_store.yaml and verify registry cache settings + envByte, err := base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfig := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + + Expect(repoConfig.Registry.CacheTTLSeconds).NotTo(BeNil()) + Expect(*repoConfig.Registry.CacheTTLSeconds).To(Equal(ttl)) + Expect(repoConfig.Registry.CacheMode).NotTo(BeNil()) + Expect(*repoConfig.Registry.CacheMode).To(Equal(mode)) + }) }) }) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go index 82980d5091f..0f99f6d0479 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_oidc_auth_test.go @@ -38,7 +38,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/authz" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" @@ -60,7 +60,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} BeforeEach(func() { By("creating the OIDC secret") @@ -76,8 +76,8 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { err = k8sClient.Get(ctx, typeNamespacedName, featurestore) if err != nil && errors.IsNotFound(err) { resource := createFeatureStoreResource(resourceName, image, pullPolicy, &[]corev1.EnvVar{}, withEnvFrom()) - resource.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{OidcAuthz: &feastdevv1alpha1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ + resource.Spec.AuthzConfig = &feastdevv1.AuthzConfig{OidcAuthz: &feastdevv1.OidcAuthz{ + SecretRef: &corev1.LocalObjectReference{ Name: oidcSecretName, }, }} @@ -87,7 +87,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -116,7 +116,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -132,9 +132,9 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(resource.Status.FeastVersion).To(Equal(feastversion.FeastVersion)) Expect(resource.Status.ClientConfigMap).To(Equal(feast.GetFeastServiceName(services.ClientFeastType))) Expect(resource.Status.Applied.FeastProject).To(Equal(resource.Spec.FeastProject)) - expectedAuthzConfig := &feastdevv1alpha1.AuthzConfig{ - OidcAuthz: &feastdevv1alpha1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ + expectedAuthzConfig := &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{ + SecretRef: &corev1.LocalObjectReference{ Name: oidcSecretName, }, }, @@ -171,45 +171,48 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + domain)) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.DeploymentNotAvailableReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.DeploymentNotAvailableMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.DeploymentNotAvailableReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) - Expect(cond).To(BeNil()) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Message).To(Equal(feastdevv1.OidcAuthzReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OfflineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.PendingPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) // check deployment deploy := &appsv1.Deployment{} @@ -221,7 +224,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) Expect(deploy.Spec.Template.Spec.Volumes).To(HaveLen(1)) Expect(services.GetOfflineContainer(*deploy).VolumeMounts).To(HaveLen(1)) @@ -270,7 +273,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource @@ -298,7 +301,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -354,7 +357,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { testConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: services.OfflineStoreConfig{ Type: services.OfflineFilePersistenceDaskConfigType, }, @@ -422,7 +425,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { clientConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: offlineRemote, OnlineStore: services.OnlineStoreConfig{ Path: fmt.Sprintf("http://feast-%s-online.default.svc.cluster.local:80", resourceName), @@ -455,7 +458,7 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { Expect(k8sClient.Create(ctx, newOidcSecret)).To(Succeed()) } - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -467,16 +470,16 @@ var _ = Describe("FeatureStore Controller-OIDC authorization", func() { }) Expect(err).To(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionFalse)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.FailedReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(ContainSubstring("missing OIDC")) + Expect(cond.Reason).To(Equal(feastdevv1.FailedReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(ContainSubstring("OIDC discovery URL")) }) }) }) @@ -487,16 +490,17 @@ func expectedServerOidcAuthorizConfig() services.AuthzConfig { OidcParameters: map[string]interface{}{ string(services.OidcAuthDiscoveryUrl): "auth-discovery-url", string(services.OidcClientId): "client-id", + string(services.OidcClientSecret): "client-secret", + string(services.OidcUsername): "username", + string(services.OidcPassword): "password", + string(services.OidcAudience): "api://feast-feature-server", + string(services.OidcIssuer): "https://keycloak.example.com/realms/test", }, } } func expectedClientOidcAuthorizConfig() services.AuthzConfig { return services.AuthzConfig{ Type: services.OidcAuthType, - OidcParameters: map[string]interface{}{ - string(services.OidcClientSecret): "client-secret", - string(services.OidcUsername): "username", - string(services.OidcPassword): "password"}, } } @@ -507,6 +511,8 @@ func validOidcSecretMap() map[string]string { string(services.OidcClientSecret): "client-secret", string(services.OidcUsername): "username", string(services.OidcPassword): "password", + string(services.OidcAudience): "api://feast-feature-server", + string(services.OidcIssuer): "https://keycloak.example.com/realms/test", } } @@ -524,7 +530,7 @@ func createValidOidcSecret(secretName string) *corev1.Secret { func createInvalidOidcSecret(secretName string) *corev1.Secret { oidcProperties := validOidcSecretMap() - delete(oidcProperties, string(services.OidcClientId)) + delete(oidcProperties, string(services.OidcAuthDiscoveryUrl)) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, diff --git a/infra/feast-operator/internal/controller/featurestore_controller_packaged_test.go b/infra/feast-operator/internal/controller/featurestore_controller_packaged_test.go new file mode 100644 index 00000000000..9c534a624bd --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_packaged_test.go @@ -0,0 +1,237 @@ +/* +Copyright 2026 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("Packaged feature repositories", func() { + const ( + resourceName = "packaged-feature-repo" + packagedImage = "registry.example.com/feature-server@sha256:0123456789abcdef" + packagedRepoDir = "/opt/feast/feature_repo" + ) + + ctx := context.Background() + key := types.NamespacedName{Name: resourceName, Namespace: "default"} + + newFeatureStore := func() *feastdevv1.FeatureStore { + return &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: key.Name, Namespace: key.Namespace}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: feastProject, + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{ + Image: packagedImage, + FeatureRepoPath: packagedRepoDir, + }, + }, + }, + } + } + + reconcileFeatureStore := func() (*feastdevv1.FeatureStore, *appsv1.Deployment) { + reconciler := &FeatureStoreReconciler{Client: k8sClient, Scheme: k8sClient.Scheme()} + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) + Expect(err).NotTo(HaveOccurred()) + + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + feastServices := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: k8sClient, + Context: ctx, + Scheme: k8sClient.Scheme(), + FeatureStore: featureStore, + }, + } + deployment := &appsv1.Deployment{} + meta := feastServices.GetObjectMeta() + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: meta.Name, Namespace: meta.Namespace}, deployment)).To(Succeed()) + return featureStore, deployment + } + + BeforeEach(func() { + Expect(k8sClient.Create(ctx, newFeatureStore())).To(Succeed()) + }) + + AfterEach(func() { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + Expect(k8sClient.Delete(ctx, featureStore)).To(Succeed()) + }) + + It("stages the packaged repository and applies it from the shared directory", func() { + featureStore, deployment := reconcileFeatureStore() + + canonicalRepoDir := services.EphemeralPath + "/" + feastProject + "/" + services.FeatureRepoDir + Expect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(2)) + initContainer := deployment.Spec.Template.Spec.InitContainers[0] + Expect(initContainer.Name).To(Equal("feast-init")) + Expect(initContainer.Image).To(Equal(packagedImage)) + Expect(initContainer.WorkingDir).To(Equal(services.EphemeralPath)) + Expect(initContainer.Env).To(ContainElements( + corev1.EnvVar{Name: "FEAST_PACKAGED_FEATURE_REPO_PATH", Value: packagedRepoDir}, + corev1.EnvVar{Name: "FEAST_STAGED_FEATURE_REPO_PATH", Value: canonicalRepoDir}, + )) + Expect(initContainer.Args).To(HaveLen(1)) + Expect(initContainer.Args[0]).To(ContainSubstring(`rm -rf -- "${FEAST_STAGED_FEATURE_REPO_PATH}"`)) + Expect(initContainer.Args[0]).To(ContainSubstring(`cp -a -- "${FEAST_PACKAGED_FEATURE_REPO_PATH}/." "${FEAST_STAGED_FEATURE_REPO_PATH}/"`)) + Expect(initContainer.Args[0]).To(ContainSubstring(`printf '%s' "${TMP_FEATURE_STORE_YAML_BASE64}" | base64 -d`)) + Expect(initContainer.Args[0]).To(ContainSubstring(`"${FEAST_STAGED_FEATURE_REPO_PATH}/feature_store.yaml"`)) + + applyContainer := deployment.Spec.Template.Spec.InitContainers[1] + Expect(applyContainer.Name).To(Equal("feast-apply")) + Expect(applyContainer.Image).To(Equal(packagedImage)) + Expect(applyContainer.Command).To(Equal([]string{"feast", "apply"})) + Expect(applyContainer.WorkingDir).To(Equal(canonicalRepoDir)) + + online := services.GetOnlineContainer(*deployment) + Expect(online.Image).To(Equal(packagedImage)) + Expect(online.WorkingDir).To(Equal(canonicalRepoDir)) + Expect(*featureStore.Status.Applied.Services.OnlineStore.Server.Image).To(Equal(packagedImage)) + }) + + It("supports staging without applying and direct use of the baked repository", func() { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{RunFeastApplyOnInit: ptr(false)} + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + featureStore, deployment := reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.InitContainers[0].Name).To(Equal("feast-init")) + + featureStore.Spec.Services.DisableInitContainers = true + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + _, deployment = reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers).To(BeEmpty()) + online := services.GetOnlineContainer(*deployment) + Expect(online.Image).To(Equal(packagedImage)) + Expect(online.WorkingDir).To(Equal(packagedRepoDir)) + }) + + It("keeps explicit service images ahead of the packaged image", func() { + const serviceImage = "registry.example.com/online-server:custom" + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{Image: ptr(serviceImage)}, + }, + }, + }, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + _, deployment := reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers[0].Image).To(Equal(packagedImage)) + Expect(services.GetOnlineContainer(*deployment).Image).To(Equal(serviceImage)) + }) + + It("keeps an explicit init image ahead of the packaged image", func() { + const initImage = "registry.example.com/feast-init:custom" + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + InitImage: ptr(initImage), + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + _, deployment := reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(2)) + Expect(deployment.Spec.Template.Spec.InitContainers[0].Image).To(Equal(initImage)) + Expect(deployment.Spec.Template.Spec.InitContainers[1].Image).To(Equal(initImage)) + Expect(services.GetOnlineContainer(*deployment).Image).To(Equal(packagedImage)) + }) + + It("supports path-only direct mode with an explicit service image", func() { + const serviceImage = "registry.example.com/online-server:air-gapped" + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.FeastProjectDir.Packaged.Image = "" + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + DisableInitContainers: true, + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{Image: ptr(serviceImage)}, + }, + }, + }, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + _, deployment := reconcileFeatureStore() + Expect(deployment.Spec.Template.Spec.InitContainers).To(BeEmpty()) + online := services.GetOnlineContainer(*deployment) + Expect(online.Image).To(Equal(serviceImage)) + Expect(online.WorkingDir).To(Equal(packagedRepoDir)) + }) + + It("retains the operator image fallback when the packaged image is omitted", func() { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.FeastProjectDir.Packaged.Image = "" + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + _, deployment := reconcileFeatureStore() + initImage := deployment.Spec.Template.Spec.InitContainers[0].Image + Expect(initImage).NotTo(BeEmpty()) + Expect(services.GetOnlineContainer(*deployment).Image).To(Equal(initImage)) + }) + + DescribeTable("rejects packaged and staged repository path overlap", + func(featureRepoPath string) { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.FeastProjectDir.Packaged.FeatureRepoPath = featureRepoPath + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + reconciler := &FeatureStoreReconciler{Client: k8sClient, Scheme: k8sClient.Scheme()} + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key}) + Expect(err).To(MatchError(ContainSubstring("overlaps staged repository path"))) + }, + Entry("equal paths", services.EphemeralPath+"/"+feastProject+"/"+services.FeatureRepoDir), + Entry("packaged path is an ancestor", services.EphemeralPath+"/"+feastProject), + Entry("packaged path is a descendant", services.EphemeralPath+"/"+feastProject+"/"+services.FeatureRepoDir+"/baked"), + ) + + It("allows similar path prefixes that do not overlap", func() { + featureStore := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, key, featureStore)).To(Succeed()) + featureStore.Spec.FeastProjectDir.Packaged.FeatureRepoPath = + services.EphemeralPath + "/" + feastProject + "/" + services.FeatureRepoDir + "-image" + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + + reconcileFeatureStore() + }) +}) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go index ff273c78648..8e7303cee34 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_pvc_test.go @@ -41,7 +41,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -59,7 +59,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} onlineStorePath := "online.db" registryPath := "registry.db" offlineType := "duckdb" @@ -87,11 +87,11 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { resource := createFeatureStoreResource(resourceName, image, pullPolicy, &[]corev1.EnvVar{{Name: testEnvVarName, Value: testEnvVarValue}, {Name: "fieldRefName", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.namespace"}}}}, withEnvFrom()) resource.Spec.Services.UI = nil - resource.Spec.Services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ + resource.Spec.Services.OfflineStore.Persistence = &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ Type: offlineType, - PvcConfig: &feastdevv1alpha1.PvcConfig{ - Create: &feastdevv1alpha1.PvcCreate{ + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{ AccessModes: accessModes, StorageClassName: &storageClassName, }, @@ -99,20 +99,20 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }, }, } - resource.Spec.Services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ + resource.Spec.Services.OnlineStore.Persistence = &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ Path: onlineStorePath, - PvcConfig: &feastdevv1alpha1.PvcConfig{ - Create: &feastdevv1alpha1.PvcCreate{}, + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{}, MountPath: onlineStoreMountPath, }, }, } - resource.Spec.Services.Registry.Local.Persistence = &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + resource.Spec.Services.Registry.Local.Persistence = &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: registryPath, - PvcConfig: &feastdevv1alpha1.PvcConfig{ - Create: &feastdevv1alpha1.PvcCreate{}, + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{}, MountPath: registryMountPath, }, }, @@ -122,7 +122,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -144,7 +144,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -225,45 +225,45 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + domain)) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.DeploymentNotAvailableReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.DeploymentNotAvailableMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.DeploymentNotAvailableReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) Expect(cond).To(BeNil()) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OfflineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.PendingPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) ephemeralName := "feast-data" ephemeralVolume := corev1.Volume{ @@ -383,7 +383,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource @@ -425,7 +425,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { Scheme: k8sClient.Scheme(), } - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -498,7 +498,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { testConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, Registry: services.RegistryConfig{ RegistryType: services.RegistryFileConfigType, Path: registryMountedPath, @@ -573,7 +573,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { clientConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: offlineRemote, OnlineStore: services.OnlineStoreConfig{ Path: fmt.Sprintf("http://feast-%s-online.default.svc.cluster.local:80", resourceName), @@ -601,7 +601,7 @@ var _ = Describe("FeatureStore Controller-Ephemeral services", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index cb45d85e766..712644f7a0c 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -19,6 +19,7 @@ package controller import ( "context" "encoding/base64" + "encoding/json" "fmt" "reflect" "strings" @@ -40,7 +41,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -65,24 +66,24 @@ var _ = Describe("FeatureStore Controller", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} BeforeEach(func() { By("creating the custom resource for the Kind FeatureStore") err := k8sClient.Get(ctx, typeNamespacedName, featurestore) if err != nil && errors.IsNotFound(err) { - resource := &feastdevv1alpha1.FeatureStore{ + resource := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, - Spec: feastdevv1alpha1.FeatureStoreSpec{FeastProject: feastProject}, + Spec: feastdevv1.FeatureStoreSpec{FeastProject: feastProject}, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -102,7 +103,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -154,7 +155,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -175,30 +176,30 @@ var _ = Describe("FeatureStore Controller", func() { Expect(resource.Status.Applied.Services.OnlineStore).NotTo(BeNil()) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ReadyMessage)) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) Expect(cond).To(BeNil()) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.ReadyPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.ReadyPhase)) deploy := &appsv1.Deployment{} objMeta := feast.GetObjectMeta() @@ -210,8 +211,10 @@ var _ = Describe("FeatureStore Controller", func() { Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("feast init")) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Name).To(Equal("feast-apply")) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Command).To(Equal([]string{"feast", "apply"})) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(1)) deploy.Spec.Replicas = int32Ptr(3) @@ -237,8 +240,8 @@ var _ = Describe("FeatureStore Controller", func() { Value: "value", }, } - resource.Spec.FeastProjectDir = &feastdevv1alpha1.FeastProjectDir{ - Git: &feastdevv1alpha1.GitCloneOptions{ + resource.Spec.FeastProjectDir = &feastdevv1.FeastProjectDir{ + Git: &feastdevv1.GitCloneOptions{ URL: "test", Ref: ref, FeatureRepoPath: featureRepoPath, @@ -263,8 +266,8 @@ var _ = Describe("FeatureStore Controller", func() { Namespace: objMeta.Namespace, }, deploy) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(3))) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("git -c http.sslVerify=false clone")) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("git checkout " + ref)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring(featureRepoPath)) @@ -274,8 +277,8 @@ var _ = Describe("FeatureStore Controller", func() { Expect(online.WorkingDir).To(Equal(services.EphemeralPath + "/" + resource.Spec.FeastProject + "/" + featureRepoPath)) // change projectDir to use an init template - resource.Spec.FeastProjectDir = &feastdevv1alpha1.FeastProjectDir{ - Init: &feastdevv1alpha1.FeastInitOptions{ + resource.Spec.FeastProjectDir = &feastdevv1.FeastProjectDir{ + Init: &feastdevv1.FeastInitOptions{ Template: "spark", }, } @@ -294,8 +297,43 @@ var _ = Describe("FeatureStore Controller", func() { Namespace: objMeta.Namespace, }, deploy) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(deploy.Spec.Template.Spec.InitContainers[0].Args[0]).To(ContainSubstring("feast init -t spark")) + + // initImage is independent of server images: init containers use initImage, + // main containers keep their own server.image. + initImage := "quay.io/org/feast-init:custom" + serverImage := "quay.io/org/feast-online:server" + if resource.Spec.Services == nil { + resource.Spec.Services = &feastdevv1.FeatureStoreServices{} + } + resource.Spec.Services.InitImage = &initImage + if resource.Spec.Services.OnlineStore == nil { + resource.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{} + } + if resource.Spec.Services.OnlineStore.Server == nil { + resource.Spec.Services.OnlineStore.Server = &feastdevv1.ServerConfigs{} + } + resource.Spec.Services.OnlineStore.Server.Image = &serverImage + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: objMeta.Name, + Namespace: objMeta.Namespace, + }, deploy) + Expect(err).NotTo(HaveOccurred()) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) + Expect(deploy.Spec.Template.Spec.InitContainers[0].Image).To(Equal(initImage)) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Image).To(Equal(initImage)) + online = services.GetOnlineContainer(*deploy) + Expect(online).NotTo(BeNil()) + Expect(online.Image).To(Equal(serverImage)) + Expect(online.Image).NotTo(Equal(initImage)) }) It("should properly encode a feature_store.yaml config", func() { @@ -310,7 +348,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -371,7 +409,7 @@ var _ = Describe("FeatureStore Controller", func() { // change feast project and reconcile resourceNew := resource.DeepCopy() resourceNew.Spec.FeastProject = "changed" - resourceNew.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ + resourceNew.Spec.Services = &feastdevv1.FeatureStoreServices{ DeploymentStrategy: &appsv1.DeploymentStrategy{ Type: appsv1.RollingUpdateDeploymentStrategyType, }, @@ -393,6 +431,9 @@ var _ = Describe("FeatureStore Controller", func() { deploy) Expect(err).NotTo(HaveOccurred()) + // Update feast object with the refreshed resource + feast.Handler.FeatureStore = resource + testConfig.Project = resourceNew.Spec.FeastProject Expect(deploy.Spec.Strategy.Type).To(Equal(appsv1.RollingUpdateDeploymentStrategyType)) Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) @@ -422,7 +463,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -470,37 +511,37 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).NotTo(HaveOccurred()) Expect(resource.Status.Conditions).To(HaveLen(4)) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) Expect(cond.Status).To(Equal(metav1.ConditionFalse)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.FailedReason)) + Expect(cond.Reason).To(Equal(feastdevv1.FailedReason)) Expect(cond.Message).To(Equal("Error: Object " + resource.Namespace + "/" + deploy.Name + " is already owned by another Service controller " + name)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) Expect(cond).To(BeNil()) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.CronJobReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.CronJobReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.CronJobReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.CronJobReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.CronJobReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.CronJobReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.FailedPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.FailedPhase)) }) }) @@ -516,7 +557,7 @@ var _ = Describe("FeatureStore Controller", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} BeforeEach(func() { createEnvFromSecretAndConfigMap() @@ -530,7 +571,7 @@ var _ = Describe("FeatureStore Controller", func() { } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -553,7 +594,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -607,52 +648,52 @@ var _ = Describe("FeatureStore Controller", func() { Expect(resource.Status.ServiceHostnames.UI).To(Equal(feast.GetFeastServiceName(services.UIFeastType) + "." + resource.Namespace + domain)) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.DeploymentNotAvailableReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.DeploymentNotAvailableMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.DeploymentNotAvailableReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) Expect(cond).To(BeNil()) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OfflineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.UIReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.UIReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.UIReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.UIReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.UIReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.UIReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.PendingPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) deploy := &appsv1.Deployment{} objMeta := feast.GetObjectMeta() @@ -688,7 +729,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -734,6 +775,12 @@ var _ = Describe("FeatureStore Controller", func() { }, deploy) Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Template.Spec.ServiceAccountName).To(Equal(deploy.Name)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Name).To(Equal("feast-apply")) + Expect(deploy.Spec.Template.Spec.InitContainers[1].Env).To(ContainElements( + corev1.EnvVar{Name: testEnvVarName, Value: testEnvVarValue}, + )) + Expect(deploy.Spec.Template.Spec.InitContainers[1].EnvFrom).NotTo(BeEmpty()) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) registryContainer := services.GetRegistryContainer(*deploy) Expect(registryContainer.Env).To(HaveLen(1)) @@ -819,7 +866,7 @@ var _ = Describe("FeatureStore Controller", func() { clientConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: offlineRemote, OnlineStore: services.OnlineStoreConfig{ Path: "http://feast-services-online.default.svc.cluster.local:80", @@ -850,6 +897,9 @@ var _ = Describe("FeatureStore Controller", func() { deploy) Expect(err).NotTo(HaveOccurred()) + // Update feast object with the refreshed resource + feast.Handler.FeatureStore = resource + testConfig.Project = resourceNew.Spec.FeastProject Expect(deploy.Spec.Template.Spec.Containers[0].Env).To(HaveLen(1)) env = getFeatureStoreYamlEnvVar(deploy.Spec.Template.Spec.Containers[0].Env) @@ -878,7 +928,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -965,7 +1015,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -1035,26 +1085,26 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - referencedRegistry := &feastdevv1alpha1.FeatureStore{} + referencedRegistry := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, referencedRegistry) Expect(err).NotTo(HaveOccurred()) name := "remote-registry-reference" - resource := &feastdevv1alpha1.FeatureStore{ + resource := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: referencedRegistry.Namespace, }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: referencedRegistry.Spec.FeastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{}, + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{}, }, - OfflineStore: &feastdevv1alpha1.OfflineStore{}, - Registry: &feastdevv1alpha1.Registry{ - Remote: &feastdevv1alpha1.RemoteRegistryConfig{ - FeastRef: &feastdevv1alpha1.FeatureStoreRef{ + OfflineStore: &feastdevv1.OfflineStore{}, + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + FeastRef: &feastdevv1.FeatureStoreRef{ Name: name, }, }, @@ -1062,7 +1112,7 @@ var _ = Describe("FeatureStore Controller", func() { }, }, } - resource.SetGroupVersionKind(feastdevv1alpha1.GroupVersion.WithKind("FeatureStore")) + resource.SetGroupVersionKind(feastdevv1.GroupVersion.WithKind("FeatureStore")) nsName := client.ObjectKeyFromObject(resource) err = k8sClient.Create(ctx, resource) Expect(err).NotTo(HaveOccurred()) @@ -1073,10 +1123,10 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) Expect(resource.Status.Applied.Services.Registry.Remote.FeastRef.Namespace).NotTo(BeEmpty()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType)).To(BeNil()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType)).To(BeNil()) - Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.ReadyType)).To(BeFalse()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType)).To(BeNil()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.ReadyType)).To(BeFalse()) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).NotTo(BeNil()) Expect(cond.Message).To(Equal("Error: FeatureStore '" + name + "' can't reference itself in `spec.services.registry.remote.feastRef`")) @@ -1089,10 +1139,10 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).To(HaveOccurred()) err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType)).To(BeNil()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType)).To(BeNil()) - Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.ReadyType)).To(BeFalse()) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType)).To(BeNil()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.ReadyType)).To(BeFalse()) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).NotTo(BeNil()) Expect(cond.Message).To(Equal("Error: Referenced FeatureStore '" + resource.Spec.Services.Registry.Remote.FeastRef.Name + "' was not found")) @@ -1107,10 +1157,10 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType)).To(BeNil()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType)).To(BeNil()) - Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType)).To(BeTrue()) - Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType)).To(BeTrue()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType)).To(BeNil()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType)).To(BeTrue()) Expect(resource.Status.ServiceHostnames.Registry).ToNot(BeEmpty()) Expect(resource.Status.ServiceHostnames.Registry).To(Equal(referencedRegistry.Status.ServiceHostnames.Registry)) feast := services.FeastServices{ @@ -1139,7 +1189,7 @@ var _ = Describe("FeatureStore Controller", func() { Namespace: objMeta.Namespace, }, deploy) Expect(err).NotTo(HaveOccurred()) - Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(deploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) // check client config cm := &corev1.ConfigMap{} @@ -1154,7 +1204,7 @@ var _ = Describe("FeatureStore Controller", func() { clientConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OnlineStore: services.OnlineStoreConfig{ Path: "http://feast-" + resource.Name + "-online.default.svc.cluster.local:80", Type: services.OnlineRemoteConfigType, @@ -1188,8 +1238,8 @@ var _ = Describe("FeatureStore Controller", func() { // break remote reference hostname := "test:80" - referencedRegistry.Spec.Services.Registry = &feastdevv1alpha1.Registry{ - Remote: &feastdevv1alpha1.RemoteRegistryConfig{ + referencedRegistry.Spec.Services.Registry = &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ Hostname: &hostname, }, } @@ -1208,17 +1258,138 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) Expect(resource.Status.ServiceHostnames.Registry).To(BeEmpty()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType)).To(BeNil()) - Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType)).To(BeNil()) - Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.ReadyType)).To(BeFalse()) - Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType)).To(BeTrue()) - Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType)).To(BeTrue()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType)).To(BeNil()) + Expect(apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType)).To(BeNil()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.ReadyType)).To(BeFalse()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) + Expect(apimeta.IsStatusConditionTrue(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType)).To(BeTrue()) Expect(resource.Status.Applied.Services.Registry.Remote.FeastRef.Name).To(Equal(referencedRegistry.Name)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).NotTo(BeNil()) Expect(cond.Message).To(Equal("Error: Remote feast registry of referenced FeatureStore '" + referencedRegistry.Name + "' is not ready")) }) + It("should allow cross-project registry references with different feastProject names", func() { + By("Reconciling the primary local registry FeatureStore") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + primaryStore := &feastdevv1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, primaryStore) + Expect(err).NotTo(HaveOccurred()) + Expect(primaryStore.Status.Applied.FeastProject).To(Equal(feastProject)) + + By("Creating a second FeatureStore with a DIFFERENT feastProject name referencing the first") + crossProjectName := "cross-project-ref" + crossProjectFeastName := "different_project" + crossProjectResource := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: crossProjectName, + Namespace: primaryStore.Namespace, + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: crossProjectFeastName, + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{}, + }, + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + FeastRef: &feastdevv1.FeatureStoreRef{ + Name: primaryStore.Name, + }, + }, + }, + }, + }, + } + crossProjectResource.SetGroupVersionKind(feastdevv1.GroupVersion.WithKind("FeatureStore")) + crossProjectNsName := client.ObjectKeyFromObject(crossProjectResource) + err = k8sClient.Create(ctx, crossProjectResource) + Expect(err).NotTo(HaveOccurred()) + + By("Reconciling the cross-project FeatureStore — should succeed without error") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: crossProjectNsName, + }) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, crossProjectNsName, crossProjectResource) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying the cross-project FeatureStore is ready and uses its own project name") + Expect(crossProjectResource.Status.Applied.FeastProject).To(Equal(crossProjectFeastName)) + Expect(crossProjectResource.Status.ServiceHostnames.Registry).To(Equal(primaryStore.Status.ServiceHostnames.Registry)) + Expect(apimeta.IsStatusConditionTrue(crossProjectResource.Status.Conditions, feastdevv1.OnlineStoreReadyType)).To(BeTrue()) + + By("Verifying the cross-project client ConfigMap uses the correct project name and shared registry") + crossFeast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: crossProjectResource, + }, + } + crossCm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: crossFeast.GetFeastServiceName(services.ClientFeastType), + Namespace: crossProjectResource.Namespace, + }, crossCm) + Expect(err).NotTo(HaveOccurred()) + crossRepoConfig := &services.RepoConfig{} + err = yaml.Unmarshal([]byte(crossCm.Data[services.FeatureStoreYamlCmKey]), crossRepoConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(crossRepoConfig.Project).To(Equal(crossProjectFeastName)) + Expect(crossRepoConfig.Registry.Path).To(ContainSubstring(primaryStore.Name)) + + By("Verifying the primary store client ConfigMap still uses its own project name") + primaryFeast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: primaryStore, + }, + } + primaryCm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: primaryFeast.GetFeastServiceName(services.ClientFeastType), + Namespace: primaryStore.Namespace, + }, primaryCm) + Expect(err).NotTo(HaveOccurred()) + primaryRepoConfig := &services.RepoConfig{} + err = yaml.Unmarshal([]byte(primaryCm.Data[services.FeatureStoreYamlCmKey]), primaryRepoConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(primaryRepoConfig.Project).To(Equal(feastProject)) + + By("Verifying both stores share the same registry path") + Expect(crossRepoConfig.Registry.Path).To(Equal(primaryRepoConfig.Registry.Path)) + + By("Verifying the namespace registry ConfigMap lists both client configs") + registryCm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: services.NamespaceRegistryConfigMapName, + Namespace: services.DefaultKubernetesNamespace, + }, registryCm) + Expect(err).NotTo(HaveOccurred()) + var registryData services.NamespaceRegistryData + err = json.Unmarshal([]byte(registryCm.Data[services.NamespaceRegistryDataKey]), ®istryData) + Expect(err).NotTo(HaveOccurred()) + ns := primaryStore.Namespace + Expect(registryData.Namespaces[ns]).To(ContainElement(primaryFeast.GetFeastServiceName(services.ClientFeastType))) + Expect(registryData.Namespaces[ns]).To(ContainElement(crossFeast.GetFeastServiceName(services.ClientFeastType))) + + By("Cleaning up the cross-project FeatureStore") + Expect(k8sClient.Delete(ctx, crossProjectResource)).To(Succeed()) + }) + It("should correctly set container command args for grpc/rest modes", func() { controllerReconciler := &FeatureStoreReconciler{ Client: k8sClient, @@ -1265,17 +1436,17 @@ var _ = Describe("FeatureStore Controller", func() { Name: name, Namespace: "default", } - resource := &feastdevv1alpha1.FeatureStore{ + resource := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "default", }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: feastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ GRPC: tc.grpc, RestAPI: tc.restAPI, }, @@ -1284,7 +1455,7 @@ var _ = Describe("FeatureStore Controller", func() { }, }, } - resource.SetGroupVersionKind(feastdevv1alpha1.GroupVersion.WithKind("FeatureStore")) + resource.SetGroupVersionKind(feastdevv1.GroupVersion.WithKind("FeatureStore")) err := k8sClient.Create(ctx, resource) Expect(err).NotTo(HaveOccurred()) @@ -1320,26 +1491,26 @@ var _ = Describe("FeatureStore Controller", func() { "expected %s to be present in container command: %v", expectedArg, registryContainer.Command) } Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) } By("Verifying that creation fails when both REST API and gRPC are disabled") - disabledResource := &feastdevv1alpha1.FeatureStore{ + disabledResource := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: "disabled-both", Namespace: "default", }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: feastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ RestAPI: ptr(false), GRPC: ptr(false), }, @@ -1348,13 +1519,116 @@ var _ = Describe("FeatureStore Controller", func() { }, }, } - disabledResource.SetGroupVersionKind(feastdevv1alpha1.GroupVersion.WithKind("FeatureStore")) + disabledResource.SetGroupVersionKind(feastdevv1.GroupVersion.WithKind("FeatureStore")) err := k8sClient.Create(ctx, disabledResource) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("At least one of restAPI or grpc must be true")) }) + It("should generate correct feature_store.yaml when registry MCP is enabled", func() { + const mcpName = "mcp-registry" + mcpNsName := types.NamespacedName{ + Name: mcpName, + Namespace: "default", + } + + resource := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: mcpName, + Namespace: "default", + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: feastProject, + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + RestAPI: ptr(true), + Mcp: &feastdevv1.McpConfig{ + Enabled: true, + }, + }, + }, + }, + }, + }, + } + resource.SetGroupVersionKind(feastdevv1.GroupVersion.WithKind("FeatureStore")) + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: mcpNsName}) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Get(ctx, mcpNsName, resource) + Expect(err).NotTo(HaveOccurred()) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + deploy := &appsv1.Deployment{} + objMeta := feast.GetObjectMeta() + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: objMeta.Name, + Namespace: objMeta.Namespace, + }, deploy) + Expect(err).NotTo(HaveOccurred()) + + registryContainer := services.GetRegistryContainer(*deploy) + Expect(registryContainer).NotTo(BeNil()) + + env := getFeatureStoreYamlEnvVar(registryContainer.Env) + Expect(env).NotTo(BeNil()) + + envByte, err := base64.StdEncoding.DecodeString(env.Value) + Expect(err).NotTo(HaveOccurred()) + repoConfig := &services.RepoConfig{} + err = yaml.Unmarshal(envByte, repoConfig) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Registry.Mcp).NotTo(BeNil()) + Expect(repoConfig.Registry.Mcp.Enabled).To(BeTrue()) + }) + + It("should reject registry MCP without restAPI enabled", func() { + mcpNoRestResource := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mcp-no-rest", + Namespace: "default", + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: feastProject, + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + RestAPI: ptr(false), + GRPC: ptr(true), + Mcp: &feastdevv1.McpConfig{ + Enabled: true, + }, + }, + }, + }, + }, + }, + } + mcpNoRestResource.SetGroupVersionKind(feastdevv1.GroupVersion.WithKind("FeatureStore")) + + err := k8sClient.Create(ctx, mcpNoRestResource) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("MCP requires restAPI to be true")) + }) + It("should error on reconcile", func() { By("Trying to set the controller OwnerRef of a Deployment that already has a controller") controllerReconciler := &FeatureStoreReconciler{ @@ -1367,7 +1641,7 @@ var _ = Describe("FeatureStore Controller", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -1416,75 +1690,75 @@ var _ = Describe("FeatureStore Controller", func() { Expect(err).NotTo(HaveOccurred()) Expect(resource.Status.Conditions).To(HaveLen(7)) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) Expect(cond.Status).To(Equal(metav1.ConditionFalse)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.FailedReason)) + Expect(cond.Reason).To(Equal(feastdevv1.FailedReason)) Expect(cond.Message).To(Equal("Error: Object " + resource.Namespace + "/" + deploy.Name + " is already owned by another Service controller " + name)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.AuthorizationReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.AuthorizationReadyType) Expect(cond).To(BeNil()) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OfflineStoreReadyType)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.FailedPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.FailedPhase)) }) It("should error on reconcile", func() { By("By failing to pass CRD schema validation") - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{} + resource.Spec.Services.Registry = &feastdevv1.Registry{} err = k8sClient.Update(ctx, resource) Expect(err).To(HaveOccurred()) - resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{}, - Remote: &feastdevv1alpha1.RemoteRegistryConfig{}, + resource.Spec.Services.Registry = &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{}, + Remote: &feastdevv1.RemoteRegistryConfig{}, } err = k8sClient.Update(ctx, resource) Expect(err).To(HaveOccurred()) - resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ - Remote: &feastdevv1alpha1.RemoteRegistryConfig{}, + resource.Spec.Services.Registry = &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{}, } err = k8sClient.Update(ctx, resource) Expect(err).To(HaveOccurred()) hostname := "test:80" - resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ - Remote: &feastdevv1alpha1.RemoteRegistryConfig{ + resource.Spec.Services.Registry = &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ Hostname: &hostname, - FeastRef: &feastdevv1alpha1.FeatureStoreRef{ + FeastRef: &feastdevv1.FeatureStoreRef{ Name: "test", }, }, @@ -1492,9 +1766,9 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Update(ctx, resource) Expect(err).To(HaveOccurred()) - resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ - Remote: &feastdevv1alpha1.RemoteRegistryConfig{ - FeastRef: &feastdevv1alpha1.FeatureStoreRef{ + resource.Spec.Services.Registry = &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + FeastRef: &feastdevv1.FeatureStoreRef{ Name: "test", }, }, @@ -1511,17 +1785,17 @@ var _ = Describe("FeatureStore Controller", func() { } // Create remote FeatureStore with gRPC disabled - remote := &feastdevv1alpha1.FeatureStore{ + remote := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: remoteStoreName, Namespace: "default", }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: feastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ GRPC: ptr(false), RestAPI: ptr(true), }, @@ -1543,9 +1817,9 @@ var _ = Describe("FeatureStore Controller", func() { // Update main FeatureStore to reference the remote registry Expect(k8sClient.Get(ctx, typeNamespacedName, featurestore)).To(Succeed()) featurestore.Spec.FeastProject = feastProject - featurestore.Spec.Services.Registry = &feastdevv1alpha1.Registry{ - Remote: &feastdevv1alpha1.RemoteRegistryConfig{ - FeastRef: &feastdevv1alpha1.FeatureStoreRef{Name: remoteStoreName}, + featurestore.Spec.Services.Registry = &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + FeastRef: &feastdevv1.FeatureStoreRef{Name: remoteStoreName}, }, } Expect(k8sClient.Update(ctx, featurestore)).To(Succeed()) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test_utils_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test_utils_test.go index 63df6d46f46..cadb1ac13e6 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test_utils_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test_utils_test.go @@ -5,7 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" . "github.com/onsi/gomega" @@ -95,31 +95,31 @@ func deleteEnvFromSecretAndConfigMap() { Expect(err).ToNot(HaveOccurred()) } -func createFeatureStoreResource(resourceName string, image string, pullPolicy corev1.PullPolicy, envVars *[]corev1.EnvVar, envFromVar *[]corev1.EnvFromSource) *feastdevv1alpha1.FeatureStore { - return &feastdevv1alpha1.FeatureStore{ +func createFeatureStoreResource(resourceName string, image string, pullPolicy corev1.PullPolicy, envVars *[]corev1.EnvVar, envFromVar *[]corev1.EnvFromSource) *feastdevv1.FeatureStore { + return &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: feastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + Services: &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ EnvFrom: envFromVar, }, }, }, }, - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ Image: &image, }, - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ Env: envVars, EnvFrom: envFromVar, ImagePullPolicy: &pullPolicy, @@ -128,19 +128,19 @@ func createFeatureStoreResource(resourceName string, image string, pullPolicy co }, }, }, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{}, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{}, }, }, }, - UI: &feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ + UI: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ Image: &image, }, - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ Env: envVars, EnvFrom: envFromVar, ImagePullPolicy: &pullPolicy, diff --git a/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go index b7aca319f05..bc60aed4374 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_tls_test.go @@ -38,7 +38,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -53,43 +53,43 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} localRef := corev1.LocalObjectReference{Name: "test"} - tlsConfigs := &feastdevv1alpha1.TlsConfigs{ + tlsConfigs := &feastdevv1.TlsConfigs{ SecretRef: &localRef, } BeforeEach(func() { By("creating the custom resource for the Kind FeatureStore") err := k8sClient.Get(ctx, typeNamespacedName, featurestore) if err != nil && errors.IsNotFound(err) { - resource := &feastdevv1alpha1.FeatureStore{ + resource := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: feastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ TLS: tlsConfigs, }, }, - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{ TLS: tlsConfigs, }, }, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ TLS: tlsConfigs, }, }, }, }, - UI: &feastdevv1alpha1.ServerConfigs{ + UI: &feastdevv1.ServerConfigs{ TLS: tlsConfigs, }, }, @@ -99,7 +99,7 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -119,7 +119,7 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ @@ -145,42 +145,42 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(resource.Status.ServiceHostnames.Registry).To(Equal(feast.GetFeastServiceName(services.RegistryFeastType) + "." + resource.Namespace + domainTls)) Expect(resource.Status.Conditions).NotTo(BeEmpty()) - cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.DeploymentNotAvailableReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.DeploymentNotAvailableMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.DeploymentNotAvailableReason)) + Expect(cond.Type).To(Equal(feastdevv1.ReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.DeploymentNotAvailableMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.RegistryReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.RegistryReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.RegistryReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.RegistryReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.RegistryReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.RegistryReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ClientReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.ClientReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.ClientReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.ClientReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OfflineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OfflineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OfflineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OfflineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OfflineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OfflineStoreReadyMessage)) - cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.OnlineStoreReadyType) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1.OnlineStoreReadyType) Expect(cond).ToNot(BeNil()) Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) - Expect(cond.Type).To(Equal(feastdevv1alpha1.OnlineStoreReadyType)) - Expect(cond.Message).To(Equal(feastdevv1alpha1.OnlineStoreReadyMessage)) + Expect(cond.Reason).To(Equal(feastdevv1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1.OnlineStoreReadyType)) + Expect(cond.Message).To(Equal(feastdevv1.OnlineStoreReadyMessage)) - Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.PendingPhase)) + Expect(resource.Status.Phase).To(Equal(feastdevv1.PendingPhase)) // check deployment deploy := &appsv1.Deployment{} @@ -193,6 +193,16 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(deploy.Spec.Replicas).To(Equal(int32Ptr(1))) Expect(controllerutil.HasControllerReference(deploy)).To(BeTrue()) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(4)) + + // verify init containers have TLS volume mounts after reconciliation + Expect(deploy.Spec.Template.Spec.InitContainers).NotTo(BeEmpty()) + for _, initContainer := range deploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement(SatisfyAll( + HaveField("MountPath", services.GetTlsPath(services.RegistryFeastType)), + HaveField("ReadOnly", true), + )), "init container %s should have registry TLS volume mount", initContainer.Name) + } + svc := &corev1.Service{} err = k8sClient.Get(ctx, types.NamespacedName{ Name: feast.GetFeastServiceName(services.RegistryFeastType), @@ -216,7 +226,7 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast := services.FeastServices{ @@ -338,7 +348,7 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { clientConfig := &services.RepoConfig{ Project: feastProject, Provider: services.LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, OfflineStore: offlineRemote, OnlineStore: services.OnlineStoreConfig{ Path: fmt.Sprintf("https://feast-%s-online.default.svc.cluster.local:443", resourceName), @@ -354,25 +364,25 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { resourceNew := resource.DeepCopy() disable := true remoteRegHost := "test.other-ns:443" - resourceNew.Spec = feastdevv1alpha1.FeatureStoreSpec{ + resourceNew.Spec = feastdevv1.FeatureStoreSpec{ FeastProject: feastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ Disable: &disable, }, }, }, - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{ TLS: tlsConfigs, }, }, - Registry: &feastdevv1alpha1.Registry{ - Remote: &feastdevv1alpha1.RemoteRegistryConfig{ + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ Hostname: &remoteRegHost, - TLS: &feastdevv1alpha1.TlsRemoteRegistryConfigs{ + TLS: &feastdevv1.TlsRemoteRegistryConfigs{ ConfigMapRef: localRef, CertName: "remote.crt", }, @@ -387,7 +397,7 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { }) Expect(err).NotTo(HaveOccurred()) - resource = &feastdevv1alpha1.FeatureStore{} + resource = &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) feast.Handler.FeatureStore = resource @@ -401,6 +411,14 @@ var _ = Describe("FeatureStore Controller - Feast service TLS", func() { Expect(err).NotTo(HaveOccurred()) Expect(deploy.Spec.Template.Spec.Containers).To(HaveLen(2)) + // verify init containers have remote registry TLS volume mounts + for _, initContainer := range deploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement(SatisfyAll( + HaveField("MountPath", services.GetTlsPath(services.RegistryFeastType)), + HaveField("ReadOnly", true), + )), "init container %s should have remote registry TLS volume mount", initContainer.Name) + } + // check offline config offlineContainer = services.GetOfflineContainer(*deploy) env = getFeatureStoreYamlEnvVar(offlineContainer.Env) @@ -457,25 +475,25 @@ var _ = Describe("Test mountCustomCABundle functionality", func() { Namespace: "default", } - fs := &feastdevv1alpha1.FeatureStore{ + fs := &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: nsName.Namespace, }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: feastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{Local: &feastdevv1alpha1.LocalRegistryConfig{Server: &feastdevv1alpha1.RegistryServerConfigs{ServerConfigs: feastdevv1alpha1.ServerConfigs{}}}}, - OnlineStore: &feastdevv1alpha1.OnlineStore{Server: &feastdevv1alpha1.ServerConfigs{}}, - OfflineStore: &feastdevv1alpha1.OfflineStore{Server: &feastdevv1alpha1.ServerConfigs{}}, - UI: &feastdevv1alpha1.ServerConfigs{}, + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{Local: &feastdevv1.LocalRegistryConfig{Server: &feastdevv1.RegistryServerConfigs{ServerConfigs: feastdevv1.ServerConfigs{}}}}, + OnlineStore: &feastdevv1.OnlineStore{Server: &feastdevv1.ServerConfigs{}}, + OfflineStore: &feastdevv1.OfflineStore{Server: &feastdevv1.ServerConfigs{}}, + UI: &feastdevv1.ServerConfigs{}, }, }, } AfterEach(func() { By("cleaning up FeatureStore and ConfigMap") - _ = k8sClient.Delete(ctx, &feastdevv1alpha1.FeatureStore{ObjectMeta: metav1.ObjectMeta{Name: resourceName, Namespace: nsName.Namespace}}) + _ = k8sClient.Delete(ctx, &feastdevv1.FeatureStore{ObjectMeta: metav1.ObjectMeta{Name: resourceName, Namespace: nsName.Namespace}}) _ = k8sClient.Delete(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: configMapName, Namespace: nsName.Namespace}}) }) @@ -502,7 +520,7 @@ var _ = Describe("Test mountCustomCABundle functionality", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) @@ -530,6 +548,12 @@ var _ = Describe("Test mountCustomCABundle functionality", func() { HaveField("MountPath", tlsPathCustomCABundle), ))) } + for _, initContainer := range deploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement(SatisfyAll( + HaveField("Name", configMapName), + HaveField("MountPath", tlsPathCustomCABundle), + )), "init container %s should have CA bundle volume mount", initContainer.Name) + } }) It("should not mount CA bundle volume or container mounts when ConfigMap is absent", func() { @@ -545,7 +569,7 @@ var _ = Describe("Test mountCustomCABundle functionality", func() { }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, nsName, resource) Expect(err).NotTo(HaveOccurred()) @@ -570,5 +594,10 @@ var _ = Describe("Test mountCustomCABundle functionality", func() { for _, container := range deploy.Spec.Template.Spec.Containers { Expect(container.VolumeMounts).NotTo(ContainElement(HaveField("Name", configMapName))) } + for _, initContainer := range deploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).NotTo(ContainElement( + HaveField("Name", configMapName), + ), "init container %s should not have CA bundle mount when ConfigMap is absent", initContainer.Name) + } }) }) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_volume_volumemount_test.go b/infra/feast-operator/internal/controller/featurestore_controller_volume_volumemount_test.go index 521f18cdc36..5751227e6ad 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_volume_volumemount_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_volume_volumemount_test.go @@ -25,7 +25,7 @@ import ( "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" "sigs.k8s.io/controller-runtime/pkg/reconcile" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" @@ -46,7 +46,7 @@ var _ = Describe("FeatureStore Controller - Deployment Volumes and VolumeMounts" Name: resourceName, Namespace: "default", } - featurestore := &feastdevv1alpha1.FeatureStore{} + featurestore := &feastdevv1.FeatureStore{} onlineStorePath := "/data/online.db" registryPath := "/data/registry.db" @@ -55,20 +55,20 @@ var _ = Describe("FeatureStore Controller - Deployment Volumes and VolumeMounts" err := k8sClient.Get(ctx, typeNamespacedName, featurestore) if err != nil && errors.IsNotFound(err) { resource := createFeatureStoreVolumeResource(resourceName, image, pullPolicy) - resource.Spec.Services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ + resource.Spec.Services.OfflineStore.Persistence = &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ Type: offlineType, }, } - resource.Spec.Services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ + resource.Spec.Services.OnlineStore.Persistence = &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ Path: onlineStorePath, }, } - resource.Spec.Services.Registry = &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + resource.Spec.Services.Registry = &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: registryPath, }, }, @@ -78,7 +78,7 @@ var _ = Describe("FeatureStore Controller - Deployment Volumes and VolumeMounts" } }) AfterEach(func() { - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -99,7 +99,7 @@ var _ = Describe("FeatureStore Controller - Deployment Volumes and VolumeMounts" }) Expect(err).NotTo(HaveOccurred()) - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -158,7 +158,7 @@ var _ = Describe("FeatureStore Controller - Deployment Volumes and VolumeMounts" }) }) -func createFeatureStoreVolumeResource(resourceName string, image string, pullPolicy corev1.PullPolicy) *feastdevv1alpha1.FeatureStore { +func createFeatureStoreVolumeResource(resourceName string, image string, pullPolicy corev1.PullPolicy) *feastdevv1.FeatureStore { volume := corev1.Volume{ Name: "test-volume", VolumeSource: corev1.VolumeSource{ @@ -170,40 +170,40 @@ func createFeatureStoreVolumeResource(resourceName string, image string, pullPol MountPath: "/data", } - return &feastdevv1alpha1.FeatureStore{ + return &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: feastProject, - Services: &feastdevv1alpha1.FeatureStoreServices{ + Services: &feastdevv1.FeatureStoreServices{ Volumes: []corev1.Volume{volume}, - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{}, + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{}, }, }, - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ VolumeMounts: []corev1.VolumeMount{volumeMount}, - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ Image: &image, }, - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ ImagePullPolicy: &pullPolicy, Resources: &corev1.ResourceRequirements{}, }, }, }, }, - UI: &feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ + UI: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ Image: &image, }, - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ ImagePullPolicy: &pullPolicy, Resources: &corev1.ResourceRequirements{}, }, diff --git a/infra/feast-operator/internal/controller/handler/handler_types.go b/infra/feast-operator/internal/controller/handler/handler_types.go index 5a26776f569..d6a66c55ffd 100644 --- a/infra/feast-operator/internal/controller/handler/handler_types.go +++ b/infra/feast-operator/internal/controller/handler/handler_types.go @@ -3,7 +3,7 @@ package handler import ( "context" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -16,5 +16,5 @@ type FeastHandler struct { client.Client Context context.Context Scheme *runtime.Scheme - FeatureStore *feastdevv1alpha1.FeatureStore + FeatureStore *feastdevv1.FeatureStore } diff --git a/infra/feast-operator/internal/controller/metrics/metrics.go b/infra/feast-operator/internal/controller/metrics/metrics.go new file mode 100644 index 00000000000..c29342aab2a --- /dev/null +++ b/infra/feast-operator/internal/controller/metrics/metrics.go @@ -0,0 +1,137 @@ +/* +Copyright 2026 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package metrics provides a Prometheus info gauge that records the store +// types configured for each FeatureStore CR (online store, offline store, +// registry). These operator-level metrics are distinct from the Feast +// feature-server application metrics (feast_feature_server_*) and are useful +// for usage telemetry and assessing the impact of removing store type support. +package metrics + +import ( + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + "github.com/prometheus/client_golang/prometheus" + ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +const ( + typeNone = "none" + labelName = "name" + labelNamespace = "namespace" +) + +// FeatureStoreMetrics holds the Prometheus GaugeVec for feast-operator +// installation telemetry. +type FeatureStoreMetrics struct { + FeatureStoreInfo *prometheus.GaugeVec +} + +// NewFeatureStoreMetrics creates a new FeatureStoreMetrics with the GaugeVec +// initialised but not yet registered. Call Register() before starting the manager. +func NewFeatureStoreMetrics() *FeatureStoreMetrics { + return &FeatureStoreMetrics{ + FeatureStoreInfo: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "feast_operator_feature_store_info", + Help: "Information about a deployed FeatureStore. " + + "Value is always 1. Labels carry the configured store types: " + + "'online_store_type', 'offline_store_type', and 'registry_type' " + + "are set to the persistence type (e.g. redis, snowflake.offline, local) " + + "or 'none' when that component is not configured.", + }, + []string{labelNamespace, labelName, "online_store_type", "offline_store_type", "registry_type"}, + ), + } +} + +// Register registers the metric with the controller-runtime metrics registry +// so it is exposed on the manager's /metrics endpoint. +func (m *FeatureStoreMetrics) Register() { + ctrlmetrics.Registry.MustRegister(m.FeatureStoreInfo) +} + +// RecordFeatureStore updates the gauge for the given FeatureStore using the +// applied configuration stored in status.Applied (which has operator defaults +// applied). The previous label set for this FeatureStore is deleted first so +// that store type changes are reflected cleanly on the next scrape. +func (m *FeatureStoreMetrics) RecordFeatureStore(fs *feastdevv1.FeatureStore) { + svcs := fs.Status.Applied.Services + m.FeatureStoreInfo.DeletePartialMatch(prometheus.Labels{ + labelNamespace: fs.Namespace, + labelName: fs.Name, + }) + m.FeatureStoreInfo.WithLabelValues( + fs.Namespace, + fs.Name, + onlineStoreType(svcs), + offlineStoreType(svcs), + registryType(svcs), + ).Set(1) +} + +// DeleteFeatureStore removes the metric label set for the given FeatureStore. +// Safe to call when the CR has already been deleted from the API server. +func (m *FeatureStoreMetrics) DeleteFeatureStore(namespace, name string) { + m.FeatureStoreInfo.DeletePartialMatch(prometheus.Labels{ + "namespace": namespace, + "name": name, + }) +} + +// onlineStoreType returns the online store persistence type or "none". +func onlineStoreType(svcs *feastdevv1.FeatureStoreServices) string { + if svcs == nil || svcs.OnlineStore == nil { + return typeNone + } + if p := svcs.OnlineStore.Persistence; p != nil && p.DBPersistence != nil { + return p.DBPersistence.Type + } + return "file" +} + +// offlineStoreType returns the offline store persistence type or "none". +func offlineStoreType(svcs *feastdevv1.FeatureStoreServices) string { + if svcs == nil || svcs.OfflineStore == nil { + return typeNone + } + if p := svcs.OfflineStore.Persistence; p != nil { + if p.DBPersistence != nil { + return p.DBPersistence.Type + } + if p.FilePersistence != nil && p.FilePersistence.Type != "" { + return p.FilePersistence.Type + } + } + return "file" +} + +// registryType returns "local", "remote", "remote_feastref", or "none". +func registryType(svcs *feastdevv1.FeatureStoreServices) string { + if svcs == nil || svcs.Registry == nil { + return typeNone + } + switch { + case svcs.Registry.Local != nil: + return "local" + case svcs.Registry.Remote != nil: + if svcs.Registry.Remote.FeastRef != nil { + return "remote_feastref" + } + return "remote" + default: + return typeNone + } +} diff --git a/infra/feast-operator/internal/controller/metrics/metrics_test.go b/infra/feast-operator/internal/controller/metrics/metrics_test.go new file mode 100644 index 00000000000..480a861560d --- /dev/null +++ b/infra/feast-operator/internal/controller/metrics/metrics_test.go @@ -0,0 +1,275 @@ +/* +Copyright 2026 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics_test + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + . "github.com/feast-dev/feast/infra/feast-operator/internal/controller/metrics" +) + +const testNamespace = "test-ns" + +// gaugeValue reads the float64 value for the given label values. +// Returns -1 if the metric is not found. +func gaugeValue(gv *prometheus.GaugeVec, labels ...string) float64 { + g, err := gv.GetMetricWithLabelValues(labels...) + if err != nil { + return -1 + } + m := &dto.Metric{} + if err := g.Write(m); err != nil { + return -1 + } + return m.GetGauge().GetValue() +} + +func featureStore(name string, svcs *feastdevv1.FeatureStoreServices) *feastdevv1.FeatureStore { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: name}, + } + fs.Status.Applied.Services = svcs + return fs +} + +func TestRecordFeatureStore_NoServices(t *testing.T) { + m := NewFeatureStoreMetrics() + m.RecordFeatureStore(featureStore("fs", nil)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "none", "none"); v != 1 { + t.Errorf("expected 1 for all-absent store, got %v", v) + } +} + +func TestRecordFeatureStore_OnlineStore_File(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{}, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "file", "none", "none"); v != 1 { + t.Errorf("expected 1 for file online store, got %v", v) + } +} + +func TestRecordFeatureStore_OnlineStore_Redis(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "none", "none"); v != 1 { + t.Errorf("expected 1 for redis online store, got %v", v) + } +} + +func TestRecordFeatureStore_OfflineStore_DB(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + DBPersistence: &feastdevv1.OfflineStoreDBStorePersistence{Type: "snowflake.offline"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "snowflake.offline", "none"); v != 1 { + t.Errorf("expected 1 for snowflake offline store, got %v", v) + } +} + +func TestRecordFeatureStore_OfflineStore_FilePersistenceType(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{Type: "duckdb"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "duckdb", "none"); v != 1 { + t.Errorf("expected 1 for duckdb offline store, got %v", v) + } +} + +func TestRecordFeatureStore_Registry_Local(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{}, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "none", "local"); v != 1 { + t.Errorf("expected 1 for local registry, got %v", v) + } +} + +func TestRecordFeatureStore_Registry_RemoteHostname(t *testing.T) { + hostname := "registry.example.com:443" + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{Hostname: &hostname}, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "none", "remote"); v != 1 { + t.Errorf("expected 1 for remote registry, got %v", v) + } +} + +func TestRecordFeatureStore_Registry_RemoteFeastRef(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + FeastRef: &feastdevv1.FeatureStoreRef{Name: "other-fs", Namespace: "other-ns"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "none", "none", "remote_feastref"); v != 1 { + t.Errorf("expected 1 for remote_feastref registry, got %v", v) + } +} + +func TestRecordFeatureStore_AllComponents(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + DBPersistence: &feastdevv1.OfflineStoreDBStorePersistence{Type: "snowflake.offline"}, + }, + }, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{}, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "snowflake.offline", "local"); v != 1 { + t.Errorf("expected 1 for full store config, got %v", v) + } +} + +func TestRecordFeatureStore_TypeChange(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs1 := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + } + svcs2 := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "postgres"}, + }, + }, + } + + m.RecordFeatureStore(featureStore("fs", svcs1)) + m.RecordFeatureStore(featureStore("fs", svcs2)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "none", "none"); v != 0 { + t.Errorf("old label set (redis) should be removed after type change, got %v", v) + } + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "postgres", "none", "none"); v != 1 { + t.Errorf("new label set (postgres) should be 1 after type change, got %v", v) + } +} + +func TestDeleteFeatureStore_RemovesMetric(t *testing.T) { + m := NewFeatureStoreMetrics() + svcs := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + } + m.RecordFeatureStore(featureStore("fs", svcs)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "none", "none"); v != 1 { + t.Fatalf("setup: expected 1 before delete, got %v", v) + } + + m.DeleteFeatureStore(testNamespace, "fs") + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs", "redis", "none", "none"); v != 0 { + t.Errorf("expected 0 after DeleteFeatureStore, got %v", v) + } +} + +func TestMultipleFeatureStores_IndependentLabelSets(t *testing.T) { + m := NewFeatureStoreMetrics() + + svcs1 := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "redis"}, + }, + }, + } + svcs2 := &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{Type: "postgres"}, + }, + }, + } + + m.RecordFeatureStore(featureStore("fs-1", svcs1)) + m.RecordFeatureStore(featureStore("fs-2", svcs2)) + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs-1", "redis", "none", "none"); v != 1 { + t.Errorf("fs-1: expected redis=1, got %v", v) + } + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs-2", "postgres", "none", "none"); v != 1 { + t.Errorf("fs-2: expected postgres=1, got %v", v) + } + + m.DeleteFeatureStore(testNamespace, "fs-1") + + if v := gaugeValue(m.FeatureStoreInfo, testNamespace, "fs-2", "postgres", "none", "none"); v != 1 { + t.Errorf("fs-2 should be unaffected after fs-1 deletion, got %v", v) + } +} diff --git a/infra/feast-operator/internal/controller/services/batch_engine_rbac.go b/infra/feast-operator/internal/controller/services/batch_engine_rbac.go new file mode 100644 index 00000000000..97afcd3b48d --- /dev/null +++ b/infra/feast-operator/internal/controller/services/batch_engine_rbac.go @@ -0,0 +1,285 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "embed" + "fmt" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/yaml" +) + +const ( + BatchEngineFeastType FeastServiceType = "batch-engine" + BatchDriverFeastType FeastServiceType = "batch-driver" +) + +//go:embed rbac_templates/*.yaml +var batchEngineRBACTemplates embed.FS + +// BatchEngineRBACTemplate declares RBAC requirements for a batch compute engine. +type BatchEngineRBACTemplate struct { + EngineType string `json:"engine_type" yaml:"engine_type"` + Server *RBACRoleSpec `json:"server,omitempty" yaml:"server,omitempty"` + Driver *DriverRBACSpec `json:"driver,omitempty" yaml:"driver,omitempty"` +} + +// RBACRoleSpec defines policy rules for a Role. +type RBACRoleSpec struct { + Rules []rbacv1.PolicyRule `json:"rules" yaml:"rules"` +} + +// DriverRBACSpec defines policy rules and optional SA creation for a driver Role. +type DriverRBACSpec struct { + CreateServiceAccount bool `json:"create_service_account" yaml:"create_service_account"` + Rules []rbacv1.PolicyRule `json:"rules" yaml:"rules"` +} + +func loadBatchEngineTemplate(engineType string) (*BatchEngineRBACTemplate, error) { + data, err := batchEngineRBACTemplates.ReadFile( + "rbac_templates/" + engineType + ".yaml", + ) + if err != nil { + return nil, nil + } + var tmpl BatchEngineRBACTemplate + if err := yaml.Unmarshal(data, &tmpl); err != nil { + return nil, fmt.Errorf("failed to parse RBAC template for engine %q: %w", engineType, err) + } + return &tmpl, nil +} + +func (feast *FeastServices) reconcileBatchEngineRBAC() error { + config, ok := feast.getBatchEngineConfig() + if !ok { + return feast.deleteBatchEngineRBAC() + } + + engineType, _ := config["type"].(string) + if engineType == "" { + return feast.deleteBatchEngineRBAC() + } + + tmpl, err := loadBatchEngineTemplate(engineType) + if err != nil { + return err + } + if tmpl == nil { + return feast.deleteBatchEngineRBAC() + } + + if tmpl.Server != nil { + if err := feast.ensureBatchEngineRole(BatchEngineFeastType, tmpl.Server.Rules); err != nil { + return err + } + if err := feast.ensureBatchEngineRoleBinding(BatchEngineFeastType, feast.initFeastSA().Name); err != nil { + return err + } + } + + if tmpl.Driver != nil { + driverSAName := resolveBatchDriverSAName(feast.Handler.FeatureStore, config) + if tmpl.Driver.CreateServiceAccount { + if err := feast.ensureBatchDriverServiceAccount(driverSAName); err != nil { + return err + } + } + if err := feast.ensureBatchEngineRole(BatchDriverFeastType, tmpl.Driver.Rules); err != nil { + return err + } + if err := feast.ensureBatchEngineRoleBinding(BatchDriverFeastType, driverSAName); err != nil { + return err + } + } + + return nil +} + +// getBatchEngineConfig returns the parsed batch-engine ConfigMap data. +// ok=false means no batch engine is configured or the ConfigMap is unreadable. +func (feast *FeastServices) getBatchEngineConfig() (map[string]interface{}, bool) { + appliedSpec := feast.Handler.FeatureStore.Status.Applied + if appliedSpec.BatchEngine == nil || appliedSpec.BatchEngine.ConfigMapRef == nil { + return nil, false + } + + configMapKey := appliedSpec.BatchEngine.ConfigMapKey + if configMapKey == "" { + configMapKey = "config" + } + + cm, err := feast.getConfigMap(appliedSpec.BatchEngine.ConfigMapRef.Name) + if err != nil { + return nil, false + } + + data, found := cm.Data[configMapKey] + if !found { + return nil, false + } + + var config map[string]interface{} + if err := yaml.Unmarshal([]byte(data), &config); err != nil { + return nil, false + } + return config, true +} + +// resolveBatchDriverSAName returns the ServiceAccount name for the Spark driver. +// If batch engine config sets a non-empty service_account, that value wins. +// Otherwise defaults to feast--batch-driver (same name used for RBAC). +func resolveBatchDriverSAName(featureStore *feastdevv1.FeatureStore, config map[string]interface{}) string { + if sa, ok := config["service_account"].(string); ok && sa != "" { + return sa + } + return GetFeastServiceName(featureStore, BatchDriverFeastType) +} + +func (feast *FeastServices) ensureBatchEngineRole(feastType FeastServiceType, rules []rbacv1.PolicyRule) error { + logger := log.FromContext(feast.Handler.Context) + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: feast.GetFeastServiceName(feastType), + Namespace: feast.Handler.FeatureStore.Namespace, + }, + } + role.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + + op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, role, func() error { + role.Labels = feast.getFeastTypeLabels(feastType) + role.Rules = rules + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, role, feast.Handler.Scheme) + }) + if err != nil { + return err + } + if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "Role", role.Name, "operation", op) + } + return nil +} + +func (feast *FeastServices) ensureBatchEngineRoleBinding(feastType FeastServiceType, saName string) error { + logger := log.FromContext(feast.Handler.Context) + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: feast.GetFeastServiceName(feastType), + Namespace: feast.Handler.FeatureStore.Namespace, + }, + } + roleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) + + op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, roleBinding, func() error { + roleBinding.Labels = feast.getFeastTypeLabels(feastType) + roleBinding.Subjects = []rbacv1.Subject{{ + Kind: rbacv1.ServiceAccountKind, + Name: saName, + Namespace: feast.Handler.FeatureStore.Namespace, + }} + roleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: feast.GetFeastServiceName(feastType), + } + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, roleBinding, feast.Handler.Scheme) + }) + if err != nil { + return err + } + if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "RoleBinding", roleBinding.Name, "operation", op) + } + return nil +} + +func (feast *FeastServices) ensureBatchDriverServiceAccount(saName string) error { + logger := log.FromContext(feast.Handler.Context) + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: saName, + Namespace: feast.Handler.FeatureStore.Namespace, + }, + } + sa.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount")) + + op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, sa, func() error { + if sa.Labels == nil { + sa.Labels = map[string]string{} + } + for k, v := range feast.getFeastTypeLabels(BatchDriverFeastType) { + sa.Labels[k] = v + } + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, sa, feast.Handler.Scheme) + }) + if err != nil { + return err + } + if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "ServiceAccount", sa.Name, "operation", op) + } + return nil +} + +func (feast *FeastServices) deleteBatchEngineRBAC() error { + serverRoleName := feast.GetFeastServiceName(BatchEngineFeastType) + driverRoleName := feast.GetFeastServiceName(BatchDriverFeastType) + ns := feast.Handler.FeatureStore.Namespace + + serverRoleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: serverRoleName, Namespace: ns}, + } + serverRoleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) + if err := feast.Handler.DeleteOwnedFeastObj(serverRoleBinding); err != nil { + return err + } + + serverRole := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: serverRoleName, Namespace: ns}, + } + serverRole.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + if err := feast.Handler.DeleteOwnedFeastObj(serverRole); err != nil { + return err + } + + driverRoleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: driverRoleName, Namespace: ns}, + } + driverRoleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) + if err := feast.Handler.DeleteOwnedFeastObj(driverRoleBinding); err != nil { + return err + } + + driverRole := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: driverRoleName, Namespace: ns}, + } + driverRole.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + if err := feast.Handler.DeleteOwnedFeastObj(driverRole); err != nil { + return err + } + + driverSA := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: driverRoleName, Namespace: ns}, + } + driverSA.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ServiceAccount")) + return feast.Handler.DeleteOwnedFeastObj(driverSA) +} diff --git a/infra/feast-operator/internal/controller/services/batch_engine_rbac_test.go b/infra/feast-operator/internal/controller/services/batch_engine_rbac_test.go new file mode 100644 index 00000000000..9d792c6f923 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/batch_engine_rbac_test.go @@ -0,0 +1,195 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" +) + +var _ = Describe("Batch Engine RBAC", func() { + + Describe("loadBatchEngineTemplate", func() { + It("should load spark_application template", func() { + tmpl, err := loadBatchEngineTemplate("spark_application") + Expect(err).NotTo(HaveOccurred()) + Expect(tmpl).NotTo(BeNil()) + Expect(tmpl.EngineType).To(Equal("spark_application")) + Expect(tmpl.Server).NotTo(BeNil()) + Expect(tmpl.Server.Rules).NotTo(BeEmpty()) + Expect(tmpl.Driver).NotTo(BeNil()) + Expect(tmpl.Driver.CreateServiceAccount).To(BeTrue()) + Expect(tmpl.Driver.Rules).NotTo(BeEmpty()) + }) + + It("should return nil for unknown engine type", func() { + tmpl, err := loadBatchEngineTemplate("nonexistent_engine") + Expect(err).NotTo(HaveOccurred()) + Expect(tmpl).To(BeNil()) + }) + + It("should contain correct server rules for spark_application", func() { + tmpl, err := loadBatchEngineTemplate("spark_application") + Expect(err).NotTo(HaveOccurred()) + + serverRules := tmpl.Server.Rules + Expect(serverRules).To(HaveLen(4)) + + hasConfigMapRule := false + hasSparkAppRule := false + hasPodListRule := false + hasPodLogRule := false + + for _, rule := range serverRules { + if containsResource(rule, "configmaps") && containsVerb(rule, "create") && containsVerb(rule, "delete") { + hasConfigMapRule = true + } + if containsResource(rule, "sparkapplications") && containsVerb(rule, "create") && containsVerb(rule, "get") && containsVerb(rule, "delete") { + hasSparkAppRule = true + } + if containsResource(rule, "pods") && containsVerb(rule, "list") { + hasPodListRule = true + } + if containsResource(rule, "pods/log") && containsVerb(rule, "get") { + hasPodLogRule = true + } + } + + Expect(hasConfigMapRule).To(BeTrue(), "should have configmaps create/delete rule") + Expect(hasSparkAppRule).To(BeTrue(), "should have sparkapplications create/get/delete rule") + Expect(hasPodListRule).To(BeTrue(), "should have pods list rule") + Expect(hasPodLogRule).To(BeTrue(), "should have pods/log get rule") + }) + + It("should contain correct driver rules for spark_application", func() { + tmpl, err := loadBatchEngineTemplate("spark_application") + Expect(err).NotTo(HaveOccurred()) + + driverRules := tmpl.Driver.Rules + Expect(driverRules).To(HaveLen(2)) + + hasPodRule := false + hasResourceRule := false + for _, rule := range driverRules { + if containsResource(rule, "pods") && + containsVerb(rule, "create") && + containsVerb(rule, "deletecollection") { + hasPodRule = true + } + if containsResource(rule, "services") && + containsResource(rule, "configmaps") && + containsResource(rule, "persistentvolumeclaims") && + containsVerb(rule, "deletecollection") { + hasResourceRule = true + } + } + Expect(hasPodRule).To(BeTrue(), "should have pods CRUD + deletecollection rule") + Expect(hasResourceRule).To(BeTrue(), "should have services/configmaps/PVCs CRUD + deletecollection rule") + }) + }) + + Describe("BatchEngineRBACTemplate YAML parsing", func() { + It("should correctly unmarshal a template", func() { + yamlData := ` +engine_type: test_engine +server: + rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] +driver: + create_service_account: true + rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get"] +` + var tmpl BatchEngineRBACTemplate + err := yaml.Unmarshal([]byte(yamlData), &tmpl) + Expect(err).NotTo(HaveOccurred()) + Expect(tmpl.EngineType).To(Equal("test_engine")) + Expect(tmpl.Server).NotTo(BeNil()) + Expect(tmpl.Server.Rules).To(HaveLen(1)) + Expect(tmpl.Driver).NotTo(BeNil()) + Expect(tmpl.Driver.CreateServiceAccount).To(BeTrue()) + Expect(tmpl.Driver.Rules).To(HaveLen(1)) + }) + + It("should handle server-only template (no driver)", func() { + yamlData := ` +engine_type: server_only +server: + rules: + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["create", "delete"] +` + var tmpl BatchEngineRBACTemplate + err := yaml.Unmarshal([]byte(yamlData), &tmpl) + Expect(err).NotTo(HaveOccurred()) + Expect(tmpl.Server).NotTo(BeNil()) + Expect(tmpl.Driver).To(BeNil()) + }) + }) +}) + +var _ = Describe("resolveBatchDriverSAName", func() { + It("defaults to feast--batch-driver when service_account is omitted", func() { + fs := &feastdevv1.FeatureStore{ObjectMeta: metav1.ObjectMeta{Name: "spark-pg-e2e", Namespace: "feast-spark"}} + Expect(resolveBatchDriverSAName(fs, map[string]interface{}{ + "type": "spark_application", + "image": "quay.io/example/driver:v1", + })).To(Equal("feast-spark-pg-e2e-batch-driver")) + }) + + It("defaults when service_account is empty string", func() { + fs := &feastdevv1.FeatureStore{ObjectMeta: metav1.ObjectMeta{Name: "spark-pg-e2e"}} + Expect(resolveBatchDriverSAName(fs, map[string]interface{}{ + "service_account": "", + })).To(Equal("feast-spark-pg-e2e-batch-driver")) + }) + + It("keeps an explicit service_account override", func() { + fs := &feastdevv1.FeatureStore{ObjectMeta: metav1.ObjectMeta{Name: "spark-pg-e2e"}} + Expect(resolveBatchDriverSAName(fs, map[string]interface{}{ + "service_account": "my-custom-driver", + })).To(Equal("my-custom-driver")) + }) +}) + +func containsResource(rule rbacv1.PolicyRule, resource string) bool { + for _, r := range rule.Resources { + if r == resource { + return true + } + } + return false +} + +func containsVerb(rule rbacv1.PolicyRule, verb string) bool { + for _, v := range rule.Verbs { + if v == verb { + return true + } + } + return false +} diff --git a/infra/feast-operator/internal/controller/services/client.go b/infra/feast-operator/internal/controller/services/client.go index fbd972368fb..4fcd9b894e7 100644 --- a/infra/feast-operator/internal/controller/services/client.go +++ b/infra/feast-operator/internal/controller/services/client.go @@ -47,7 +47,7 @@ func (feast *FeastServices) createClientConfigMap() error { func (feast *FeastServices) setClientConfigMap(cm *corev1.ConfigMap) error { cm.Labels = feast.getFeastTypeLabels(ClientFeastType) - clientYaml, err := feast.getClientFeatureStoreYaml(feast.extractConfigFromSecret) + clientYaml, err := feast.getClientFeatureStoreYaml() if err != nil { return err } @@ -74,7 +74,7 @@ func (feast *FeastServices) setCaConfigMap(cm *corev1.ConfigMap) error { if len(cm.Annotations) == 0 { cm.Annotations = map[string]string{} } - cm.Annotations["service.beta.openshift.io/inject-cabundle"] = "true" + cm.Annotations[openshiftInjectCaBundleAnnotation] = stringTrue return controllerutil.SetControllerReference(feast.Handler.FeatureStore, cm, feast.Handler.Scheme) } diff --git a/infra/feast-operator/internal/controller/services/cronjob.go b/infra/feast-operator/internal/controller/services/cronjob.go index b368133b113..f3b978928f7 100644 --- a/infra/feast-operator/internal/controller/services/cronjob.go +++ b/infra/feast-operator/internal/controller/services/cronjob.go @@ -4,7 +4,7 @@ import ( "os" "strconv" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -54,6 +54,7 @@ func (feast *FeastServices) initCronJob() *batchv1.CronJob { func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error { appliedCronJob := feast.Handler.FeatureStore.Status.Applied.CronJob cronJob.Labels = feast.getFeastTypeLabels(CronJobFeastType) + if appliedCronJob.Annotations != nil { cronJob.Annotations = make(map[string]string, len(appliedCronJob.Annotations)) for k, v := range appliedCronJob.Annotations { @@ -65,7 +66,8 @@ func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error { JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ - Spec: feast.getCronJobPodSpec(), + ObjectMeta: metav1.ObjectMeta{}, + Spec: feast.getCronJobPodSpec(), }, }, }, @@ -93,6 +95,16 @@ func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error { if appliedJobSpec != nil { jobSpec := &cronJob.Spec.JobTemplate.Spec + // apply PodTemplateAnnotations into the PodTemplate metadata if provided + if appliedJobSpec.PodTemplateAnnotations != nil { + if jobSpec.Template.Annotations == nil { + jobSpec.Template.Annotations = make(map[string]string, len(appliedJobSpec.PodTemplateAnnotations)) + } + for k, v := range appliedJobSpec.PodTemplateAnnotations { + jobSpec.Template.Annotations[k] = v + } + } + if appliedJobSpec.ActiveDeadlineSeconds != nil { jobSpec.ActiveDeadlineSeconds = appliedJobSpec.ActiveDeadlineSeconds } @@ -127,6 +139,7 @@ func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error { jobSpec.TTLSecondsAfterFinished = appliedJobSpec.TTLSecondsAfterFinished } } + feast.Handler.FeatureStore.Status.CronJob = cronJob.Name return controllerutil.SetControllerReference(feast.Handler.FeatureStore, cronJob, feast.Handler.Scheme) } @@ -259,7 +272,7 @@ func (feast *FeastServices) getCronJobRoleName() string { // defaults to a CronJob configuration that will never run. this default Job can be executed manually, however. // e.g. kubectl create job --from=cronjob/feast-sample feast-sample-job -func setDefaultCronJobConfigs(feastCronJob *feastdevv1alpha1.FeastCronJob) { +func setDefaultCronJobConfigs(feastCronJob *feastdevv1.FeastCronJob) { if len(feastCronJob.Schedule) == 0 { feastCronJob.Schedule = "@yearly" if feastCronJob.Suspend == nil { @@ -273,7 +286,7 @@ func setDefaultCronJobConfigs(feastCronJob *feastdevv1alpha1.FeastCronJob) { } } if feastCronJob.ContainerConfigs == nil { - feastCronJob.ContainerConfigs = &feastdevv1alpha1.CronJobContainerConfigs{} + feastCronJob.ContainerConfigs = &feastdevv1.CronJobContainerConfigs{} } if feastCronJob.ContainerConfigs.Image == nil { feastCronJob.ContainerConfigs.Image = getCronJobImage() diff --git a/infra/feast-operator/internal/controller/services/namespace_registry.go b/infra/feast-operator/internal/controller/services/namespace_registry.go index 64cdaebd6f0..122e7ba9e98 100644 --- a/infra/feast-operator/internal/controller/services/namespace_registry.go +++ b/infra/feast-operator/internal/controller/services/namespace_registry.go @@ -36,8 +36,22 @@ type NamespaceRegistryData struct { Namespaces map[string][]string `json:"namespaces"` } +// isProtectedProject checks if this CR is annotated as a protected project +func (feast *FeastServices) isProtectedProject() bool { + annotations := feast.Handler.FeatureStore.GetAnnotations() + return annotations[ProtectedProjectAnnotation] == "true" +} + // deployNamespaceRegistry creates and manages the namespace registry ConfigMap func (feast *FeastServices) deployNamespaceRegistry() error { + // Skip namespace registry for protected projects. + // Protected projects are managed externally and should not be visible to other instances. + if feast.isProtectedProject() { + logger := log.FromContext(feast.Handler.Context) + logger.V(1).Info("Skipping namespace registry for protected project", "project", feast.Handler.FeatureStore.Spec.FeastProject) + return nil + } + // Check if we can determine the target namespace before creating any resources targetNamespace, err := feast.getNamespaceRegistryNamespace() if err != nil { @@ -163,42 +177,38 @@ func (feast *FeastServices) createNamespaceRegistryRoleBinding(targetNamespace s // setNamespaceRegistryRoleBinding sets the RoleBinding for namespace registry access func (feast *FeastServices) setNamespaceRegistryRoleBinding(rb *rbacv1.RoleBinding) error { - // Create a Role that allows reading the ConfigMap + roleName := NamespaceRegistryConfigMapName + "-reader" + + desiredRules := []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + ResourceNames: []string{NamespaceRegistryConfigMapName}, + Verbs: []string{"get", "list"}, + }, + } + role := &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ - Name: NamespaceRegistryConfigMapName + "-reader", + Name: roleName, Namespace: rb.Namespace, }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"configmaps"}, - ResourceNames: []string{NamespaceRegistryConfigMapName}, - Verbs: []string{"get", "list"}, - }, - }, } + role.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) - // Create or update the Role - if _, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, role, controllerutil.MutateFn(func() error { - role.Rules = []rbacv1.PolicyRule{ - { - APIGroups: []string{""}, - Resources: []string{"configmaps"}, - ResourceNames: []string{NamespaceRegistryConfigMapName}, - Verbs: []string{"get", "list"}, - }, - } + if _, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, role, func() error { + role.Labels = feast.getLabels() + role.Rules = desiredRules return nil - })); err != nil { - return err + }); err != nil { + return fmt.Errorf("failed to reconcile namespace registry Role: %w", err) } - // Set the RoleBinding + rb.Labels = feast.getLabels() rb.RoleRef = rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "Role", - Name: role.Name, + Name: roleName, } rb.Subjects = []rbacv1.Subject{ @@ -234,6 +244,11 @@ func (feast *FeastServices) getNamespaceRegistryNamespace() (string, error) { // AddToNamespaceRegistry adds a feature store instance to the namespace registry func (feast *FeastServices) AddToNamespaceRegistry() error { + // Skip for protected projects — they should not appear in the namespace registry. + if feast.isProtectedProject() { + return nil + } + logger := log.FromContext(feast.Handler.Context) targetNamespace, err := feast.getNamespaceRegistryNamespace() if err != nil { diff --git a/infra/feast-operator/internal/controller/services/rbac_templates/spark_application.yaml b/infra/feast-operator/internal/controller/services/rbac_templates/spark_application.yaml new file mode 100644 index 00000000000..c03e0f3db57 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/rbac_templates/spark_application.yaml @@ -0,0 +1,26 @@ +engine_type: spark_application + +server: + rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["create", "delete"] + - apiGroups: ["sparkoperator.k8s.io"] + resources: ["sparkapplications"] + verbs: ["create", "get", "delete"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["list"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] + +driver: + create_service_account: true + rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["create", "get", "list", "watch", "delete", "deletecollection"] + - apiGroups: [""] + resources: ["services", "configmaps", "persistentvolumeclaims"] + verbs: ["create", "get", "list", "watch", "delete", "deletecollection"] diff --git a/infra/feast-operator/internal/controller/services/repo_config.go b/infra/feast-operator/internal/controller/services/repo_config.go index 2df0d5cb189..b4a01b312b1 100644 --- a/infra/feast-operator/internal/controller/services/repo_config.go +++ b/infra/feast-operator/internal/controller/services/repo_config.go @@ -19,13 +19,16 @@ package services import ( "encoding/base64" "fmt" + "os" "path" "strings" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "gopkg.in/yaml.v3" ) +const oidcIssuerUrlEnvVar = "OIDC_ISSUER_URL" + // GetServiceFeatureStoreYamlBase64 returns a base64 encoded feature_store.yaml config for the feast service func (feast *FeastServices) GetServiceFeatureStoreYamlBase64() (string, error) { fsYaml, err := feast.getServiceFeatureStoreYaml() @@ -44,13 +47,16 @@ func (feast *FeastServices) getServiceFeatureStoreYaml() ([]byte, error) { } func (feast *FeastServices) getServiceRepoConfig() (RepoConfig, error) { - return getServiceRepoConfig(feast.Handler.FeatureStore, feast.extractConfigFromSecret) + odhCaBundleExists := feast.GetCustomCertificatesBundle().IsDefined + return getServiceRepoConfig(feast.Handler.FeatureStore, feast.extractConfigFromSecret, feast.extractConfigFromConfigMap, odhCaBundleExists) } func getServiceRepoConfig( - featureStore *feastdevv1alpha1.FeatureStore, - secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { - repoConfig, err := getBaseServiceRepoConfig(featureStore, secretExtractionFunc) + featureStore *feastdevv1.FeatureStore, + secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), + configMapExtractionFunc func(configMapRef string, configMapKey string) (map[string]interface{}, error), + odhCaBundleExists bool) (RepoConfig, error) { + repoConfig, err := getBaseServiceRepoConfig(featureStore, secretExtractionFunc, odhCaBundleExists) if err != nil { return repoConfig, err } @@ -78,45 +84,134 @@ func getServiceRepoConfig( } } + if appliedSpec.BatchEngine != nil { + err := setRepoConfigBatchEngine(featureStore, appliedSpec.BatchEngine, configMapExtractionFunc, &repoConfig) + if err != nil { + return repoConfig, err + } + } + + if appliedSpec.Services != nil && appliedSpec.Services.OnlineStore != nil && + appliedSpec.Services.OnlineStore.Serving != nil { + setRepoConfigFeatureServer(appliedSpec.Services.OnlineStore.Serving, &repoConfig) + } + + if appliedSpec.Materialization != nil { + setRepoConfigMaterialization(appliedSpec.Materialization, &repoConfig) + } + + if appliedSpec.OpenLineage != nil { + if err := setRepoConfigOpenLineage(appliedSpec.OpenLineage, secretExtractionFunc, &repoConfig); err != nil { + return repoConfig, err + } + } + + if appliedSpec.DataQualityMonitoring != nil { + setRepoConfigDataQualityMonitoring(appliedSpec.DataQualityMonitoring, &repoConfig) + } + return repoConfig, nil } func getBaseServiceRepoConfig( - featureStore *feastdevv1alpha1.FeatureStore, - secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { + featureStore *feastdevv1.FeatureStore, + secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), + odhCaBundleExists bool) (RepoConfig, error) { repoConfig := defaultRepoConfig(featureStore) - clientRepoConfig, err := getClientRepoConfig(featureStore, secretExtractionFunc, nil) - if err != nil { - return repoConfig, err - } + clientRepoConfig := getClientRepoConfig(featureStore, nil) if isRemoteRegistry(featureStore) { repoConfig.Registry = clientRepoConfig.Registry } - repoConfig.AuthzConfig = clientRepoConfig.AuthzConfig - appliedSpec := featureStore.Status.Applied if appliedSpec.AuthzConfig != nil && appliedSpec.AuthzConfig.OidcAuthz != nil { - propertiesMap, authSecretErr := secretExtractionFunc("", appliedSpec.AuthzConfig.OidcAuthz.SecretRef.Name, "") - if authSecretErr != nil { - return repoConfig, authSecretErr + repoConfig.AuthzConfig = AuthzConfig{Type: OidcAuthType} + oidcAuthz := appliedSpec.AuthzConfig.OidcAuthz + oidcParameters := map[string]interface{}{} + + var secretProperties map[string]interface{} + if oidcAuthz.SecretRef != nil { + var err error + secretProperties, err = secretExtractionFunc("", oidcAuthz.SecretRef.Name, oidcAuthz.SecretKeyName) + if err != nil { + return repoConfig, err + } + for _, prop := range OidcOptionalSecretProperties { + if val, exists := secretProperties[string(prop)]; exists { + // Secret values are YAML-parsed on extraction, so an + // all-digits audience or issuer arrives as an int and + // would render unquoted, which the SDK's OidcAuthConfig + // rejects (Optional[str]). Coerce the claim keys back to + // strings; the five original keys keep their historical + // typing. + if prop == OidcAudience || prop == OidcIssuer { + if _, isString := val.(string); !isString { + val = fmt.Sprintf("%v", val) + } + } + oidcParameters[string(prop)] = val + } + } } - oidcServerProperties := map[string]interface{}{} - for _, oidcServerProperty := range OidcServerProperties { - if val, exists := propertiesMap[string(oidcServerProperty)]; exists { - oidcServerProperties[string(oidcServerProperty)] = val - } else { - return repoConfig, missingOidcSecretProperty(oidcServerProperty) - } + discoveryUrl, err := resolveAuthDiscoveryUrl(oidcAuthz, secretProperties) + if err != nil { + return repoConfig, err + } + oidcParameters[string(OidcAuthDiscoveryUrl)] = discoveryUrl + + if oidcAuthz.VerifySSL != nil { + oidcParameters[string(OidcVerifySsl)] = *oidcAuthz.VerifySSL + } + if caCertPath := resolveOidcCACertPath(oidcAuthz, odhCaBundleExists); caCertPath != "" { + oidcParameters[string(OidcCaCertPath)] = caCertPath } - repoConfig.AuthzConfig.OidcParameters = oidcServerProperties + repoConfig.AuthzConfig.OidcParameters = oidcParameters + } else { + repoConfig.AuthzConfig = clientRepoConfig.AuthzConfig } return repoConfig, nil } -func setRepoConfigRegistry(services *feastdevv1alpha1.FeatureStoreServices, secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { +// resolveAuthDiscoveryUrl determines the OIDC discovery URL from the first available source. +// Priority: CR issuerUrl > Secret auth_discovery_url > OIDC_ISSUER_URL env var. +func resolveAuthDiscoveryUrl(oidcAuthz *feastdevv1.OidcAuthz, secretProperties map[string]interface{}) (string, error) { + if oidcAuthz.IssuerUrl != "" { + return issuerToDiscoveryUrl(oidcAuthz.IssuerUrl), nil + } + + if val, ok := secretProperties[string(OidcAuthDiscoveryUrl)]; ok { + if s, ok := val.(string); ok && s != "" { + return s, nil + } + } + + if envIssuer := os.Getenv(oidcIssuerUrlEnvVar); envIssuer != "" { + return issuerToDiscoveryUrl(envIssuer), nil + } + + return "", fmt.Errorf("no OIDC discovery URL configured: set issuerUrl on the OidcAuthz CR, "+ + "include auth_discovery_url in the referenced Secret, or ensure the %s environment variable is set on the operator pod", oidcIssuerUrlEnvVar) +} + +func issuerToDiscoveryUrl(issuerUrl string) string { + return strings.TrimRight(issuerUrl, "/") + "/.well-known/openid-configuration" +} + +// resolveOidcCACertPath determines the CA cert file path for OIDC provider TLS verification. +// Priority: explicit CRD caCertConfigMap > ODH auto-detected bundle > empty (system CA fallback). +func resolveOidcCACertPath(oidcAuthz *feastdevv1.OidcAuthz, odhCaBundleExists bool) string { + if oidcAuthz.CACertConfigMap != nil { + return tlsPathOidcCA + } + if odhCaBundleExists { + return tlsPathOdhCABundle + } + return "" +} + +func setRepoConfigRegistry(services *feastdevv1.FeatureStoreServices, secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { registryPersistence := services.Registry.Local.Persistence if registryPersistence != nil { @@ -127,6 +222,13 @@ func setRepoConfigRegistry(services *feastdevv1alpha1.FeatureStoreServices, secr repoConfig.Registry.RegistryType = RegistryFileConfigType repoConfig.Registry.Path = getActualPath(filePersistence.Path, filePersistence.PvcConfig) repoConfig.Registry.S3AdditionalKwargs = filePersistence.S3AdditionalKwargs + if filePersistence.CacheTTLSeconds != nil { + repoConfig.Registry.CacheTTLSeconds = filePersistence.CacheTTLSeconds + } + if filePersistence.CacheMode != nil { + repoConfig.Registry.CacheMode = filePersistence.CacheMode + } + } else if dbPersistence != nil && len(dbPersistence.Type) > 0 { repoConfig.Registry.Path = "" repoConfig.Registry.RegistryType = RegistryConfigType(dbPersistence.Type) @@ -139,6 +241,31 @@ func setRepoConfigRegistry(services *feastdevv1alpha1.FeatureStoreServices, secr return err } + // Extract typed cache settings from DB parameters to avoid inline map conflicts + if ttlVal, ok := parametersMap["cache_ttl_seconds"]; ok { + switch v := ttlVal.(type) { + case int: + ttl := int32(v) + repoConfig.Registry.CacheTTLSeconds = &ttl + case int32: + ttl := v + repoConfig.Registry.CacheTTLSeconds = &ttl + case int64: + ttl := int32(v) + repoConfig.Registry.CacheTTLSeconds = &ttl + case float64: + ttl := int32(v) + repoConfig.Registry.CacheTTLSeconds = &ttl + } + delete(parametersMap, "cache_ttl_seconds") + } + if modeVal, ok := parametersMap["cache_mode"]; ok { + if modeStr, ok := modeVal.(string); ok { + repoConfig.Registry.CacheMode = &modeStr + } + delete(parametersMap, "cache_mode") + } + err = mergeStructWithDBParametersMap(¶metersMap, &repoConfig.Registry) if err != nil { return err @@ -147,10 +274,19 @@ func setRepoConfigRegistry(services *feastdevv1alpha1.FeatureStoreServices, secr repoConfig.Registry.DBParameters = parametersMap } } + + if services.Registry.Local.Server != nil && + services.Registry.Local.Server.Mcp != nil && + services.Registry.Local.Server.Mcp.Enabled { + repoConfig.Registry.Mcp = &RegistryMcpYamlConfig{ + Enabled: true, + } + } + return nil } -func setRepoConfigOnline(services *feastdevv1alpha1.FeatureStoreServices, secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { +func setRepoConfigOnline(services *feastdevv1.FeatureStoreServices, secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { onlineStorePersistence := services.OnlineStore.Persistence if onlineStorePersistence != nil { @@ -183,7 +319,7 @@ func setRepoConfigOnline(services *feastdevv1alpha1.FeatureStoreServices, secret return nil } -func setRepoConfigOffline(services *feastdevv1alpha1.FeatureStoreServices, secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { +func setRepoConfigOffline(services *feastdevv1.FeatureStoreServices, secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), repoConfig *RepoConfig) error { repoConfig.OfflineStore = defaultOfflineStoreConfig offlineStorePersistence := services.OfflineStore.Persistence @@ -216,24 +352,230 @@ func setRepoConfigOffline(services *feastdevv1alpha1.FeatureStoreServices, secre return nil } -func (feast *FeastServices) getClientFeatureStoreYaml(secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error)) ([]byte, error) { - clientRepo, err := getClientRepoConfig(feast.Handler.FeatureStore, secretExtractionFunc, feast) +func setRepoConfigBatchEngine( + featureStore *feastdevv1.FeatureStore, + batchEngineConfig *feastdevv1.BatchEngineConfig, + configMapExtractionFunc func(configMapRef string, configMapKey string) (map[string]interface{}, error), + repoConfig *RepoConfig) error { + if batchEngineConfig.ConfigMapRef == nil { + return nil + } + configMapKey := batchEngineConfig.ConfigMapKey + if configMapKey == "" { + configMapKey = "config" + } + config, err := configMapExtractionFunc(batchEngineConfig.ConfigMapRef.Name, configMapKey) if err != nil { - return []byte{}, err + return err + } + // Extract type from config + engineType, ok := config["type"].(string) + if !ok { + return fmt.Errorf("batch engine config must contain 'type' field") + } + delete(config, "type") + // Inject service_account only for spark_application so baked feature_store.yaml + // matches the SA/RoleBinding created by reconcileBatchEngineRBAC. + // Other batch engines are left unchanged. + if engineType == "spark_application" { + config["service_account"] = resolveBatchDriverSAName(featureStore, config) + } + repoConfig.BatchEngine = &ComputeEngineConfig{ + Type: engineType, + Parameters: config, + } + return nil +} + +// setRepoConfigFeatureServer maps the CRD ServingConfig into the feature_server YAML block. +// Type is set to "mcp" only when fs.Mcp is non-nil and fs.Mcp.Enabled is true; otherwise "local". +func setRepoConfigFeatureServer(fs *feastdevv1.ServingConfig, repoConfig *RepoConfig) { + serverType := "local" + if fs.Mcp != nil && fs.Mcp.Enabled { + serverType = "mcp" + } + + yamlCfg := &FeatureServerYamlConfig{ + Type: serverType, + } + + if fs.Metrics != nil { + m := &MetricsYamlConfig{ + Enabled: fs.Metrics.Enabled, + } + if len(fs.Metrics.Categories) > 0 { + m.Categories = make(map[string]interface{}, len(fs.Metrics.Categories)) + for k, v := range fs.Metrics.Categories { + m.Categories[k] = v + } + } + yamlCfg.Metrics = m + } + + if fs.OfflinePushBatching != nil { + enabled := fs.OfflinePushBatching.Enabled + yamlCfg.OfflinePushBatchingEnabled = &enabled + yamlCfg.OfflinePushBatchingBatchSize = fs.OfflinePushBatching.BatchSize + yamlCfg.OfflinePushBatchingBatchIntervalSeconds = fs.OfflinePushBatching.BatchIntervalSeconds + } + + if fs.Mcp != nil && fs.Mcp.Enabled { + enabled := fs.Mcp.Enabled + yamlCfg.McpEnabled = &enabled + yamlCfg.McpServerName = fs.Mcp.ServerName + yamlCfg.McpServerVersion = fs.Mcp.ServerVersion + yamlCfg.McpTransport = fs.Mcp.Transport } + + repoConfig.FeatureServer = yamlCfg +} + +// setRepoConfigMaterialization maps the CRD MaterializationConfig into the materialization YAML block. +func setRepoConfigMaterialization(mat *feastdevv1.MaterializationConfig, repoConfig *RepoConfig) { + yamlCfg := &MaterializationYamlConfig{ + OnlineWriteBatchSize: mat.OnlineWriteBatchSize, + } + if len(mat.ExtraConfig) > 0 { + ec := make(map[string]interface{}, len(mat.ExtraConfig)) + for k, v := range mat.ExtraConfig { + ec[k] = coerceStringToYamlType(v) + } + yamlCfg.ExtraConfig = ec + } + repoConfig.Materialization = yamlCfg +} + +// setRepoConfigOpenLineage maps the CRD OpenLineageConfig into the openlineage YAML block. +// When ApiKeySecretRef is set, the api_key value is resolved from the referenced Secret. +// ExtraConfig string values are coerced to native YAML types (bool/int) so that Feast's +// StrictBool/StrictInt Pydantic validators accept them correctly. +func setRepoConfigOpenLineage( + ol *feastdevv1.OpenLineageConfig, + secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), + repoConfig *RepoConfig) error { + + yamlCfg := &OpenLineageYamlConfig{ + Enabled: ol.Enabled, + TransportType: ol.TransportType, + TransportUrl: ol.TransportUrl, + TransportEndpoint: ol.TransportEndpoint, + } + if len(ol.ExtraConfig) > 0 { + ec := make(map[string]interface{}, len(ol.ExtraConfig)) + for k, v := range ol.ExtraConfig { + ec[k] = coerceStringToYamlType(v) + } + yamlCfg.ExtraConfig = ec + } + + if ol.ApiKeySecretRef != nil { + params, err := secretExtractionFunc("", ol.ApiKeySecretRef.Name, "") + if err != nil { + return fmt.Errorf("failed to read OpenLineage API key from secret %s: %w", ol.ApiKeySecretRef.Name, err) + } + apiKey, exists := params["api_key"] + if !exists { + return fmt.Errorf("secret %q does not contain the required key \"api_key\"", ol.ApiKeySecretRef.Name) + } + apiKeyStr, ok := apiKey.(string) + if !ok { + return fmt.Errorf("key \"api_key\" in secret %q must be a string, got %T", ol.ApiKeySecretRef.Name, apiKey) + } + yamlCfg.ApiKey = &apiKeyStr + } + + if ol.Consumer != nil { + consumerCfg := &OpenLineageConsumerYamlConfig{ + Enabled: ol.Consumer.Enabled, + StoreType: ol.Consumer.StoreType, + NamespaceMapping: ol.Consumer.NamespaceMapping, + } + + if ol.Consumer.ConnectionStringSecretRef != nil { + params, err := secretExtractionFunc("", ol.Consumer.ConnectionStringSecretRef.Name, "") + if err != nil { + return fmt.Errorf("failed to read consumer connection string from secret %s: %w", + ol.Consumer.ConnectionStringSecretRef.Name, err) + } + connStr, exists := params["connection_string"] + if !exists { + return fmt.Errorf("secret %q does not contain the required key \"connection_string\"", + ol.Consumer.ConnectionStringSecretRef.Name) + } + connStrStr, ok := connStr.(string) + if !ok { + return fmt.Errorf("key \"connection_string\" in secret %q must be a string, got %T", + ol.Consumer.ConnectionStringSecretRef.Name, connStr) + } + consumerCfg.ConnectionString = &connStrStr + } + + if ol.Consumer.ApiKeySecretRef != nil { + params, err := secretExtractionFunc("", ol.Consumer.ApiKeySecretRef.Name, "") + if err != nil { + return fmt.Errorf("failed to read consumer API key from secret %s: %w", + ol.Consumer.ApiKeySecretRef.Name, err) + } + apiKey, exists := params["api_key"] + if !exists { + return fmt.Errorf("secret %q does not contain the required key \"api_key\"", + ol.Consumer.ApiKeySecretRef.Name) + } + apiKeyStr, ok := apiKey.(string) + if !ok { + return fmt.Errorf("key \"api_key\" in secret %q must be a string, got %T", + ol.Consumer.ApiKeySecretRef.Name, apiKey) + } + consumerCfg.ApiKey = &apiKeyStr + } + + yamlCfg.Consumer = consumerCfg + } + + repoConfig.OpenLineage = yamlCfg + return nil +} + +// coerceStringToYamlType converts "true"/"false" strings to native Go booleans +// so the YAML marshaler emits an unquoted boolean rather than a quoted string. +// This is required because CRD map[string]string fields can only hold strings, +// but Feast Pydantic StrictBool fields reject string inputs. +// +// Integer coercion is intentionally omitted: some ExtraConfig target fields are +// typed as StrictStr (e.g. OpenLineageConfig.namespace, .producer), and coercing +// a numeric string like "123" to int64 would cause a Pydantic validation failure +// at runtime. Fields that genuinely require integers should be exposed as typed +// CRD fields rather than going through ExtraConfig. +func coerceStringToYamlType(v string) interface{} { + switch v { + case stringTrue: + return true + case stringFalse: + return false + } + return v +} + +func setRepoConfigDataQualityMonitoring(dqmConfig *feastdevv1.DataQualityMonitoringConfig, repoConfig *RepoConfig) { + if dqmConfig.AutoBaseline == nil { + return + } + repoConfig.DataQualityMonitoring = &DataQualityMonitoringYamlConfig{ + AutoBaseline: *dqmConfig.AutoBaseline, + } +} + +func (feast *FeastServices) getClientFeatureStoreYaml() ([]byte, error) { + clientRepo := getClientRepoConfig(feast.Handler.FeatureStore, feast) return yaml.Marshal(clientRepo) } func getClientRepoConfig( - featureStore *feastdevv1alpha1.FeatureStore, - secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error), - feast *FeastServices) (RepoConfig, error) { + featureStore *feastdevv1.FeatureStore, + feast *FeastServices) RepoConfig { status := featureStore.Status appliedServices := status.Applied.Services - clientRepoConfig, err := getRepoConfig(featureStore, secretExtractionFunc) - if err != nil { - return clientRepoConfig, err - } + clientRepoConfig := getRepoConfig(featureStore) if len(status.ServiceHostnames.OfflineStore) > 0 { clientRepoConfig.OfflineStore = OfflineStoreConfig{ Type: OfflineRemoteConfigType, @@ -271,12 +613,10 @@ func getClientRepoConfig( } } - return clientRepoConfig, nil + return clientRepoConfig } -func getRepoConfig( - featureStore *feastdevv1alpha1.FeatureStore, - secretExtractionFunc func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error)) (RepoConfig, error) { +func getRepoConfig(featureStore *feastdevv1.FeatureStore) RepoConfig { status := featureStore.Status repoConfig := initRepoConfig(status.Applied.FeastProject) if status.Applied.AuthzConfig != nil { @@ -288,27 +628,19 @@ func getRepoConfig( repoConfig.AuthzConfig = AuthzConfig{ Type: OidcAuthType, } - - propertiesMap, err := secretExtractionFunc("", status.Applied.AuthzConfig.OidcAuthz.SecretRef.Name, "") - if err != nil { - return repoConfig, err - } - oidcClientProperties := map[string]interface{}{} - for _, oidcClientProperty := range OidcClientProperties { - if val, exists := propertiesMap[string(oidcClientProperty)]; exists { - oidcClientProperties[string(oidcClientProperty)] = val - } else { - return repoConfig, missingOidcSecretProperty(oidcClientProperty) - } + if status.Applied.AuthzConfig.OidcAuthz.TokenEnvVar != nil { + oidcClientProperties[string(OidcTokenEnvVar)] = *status.Applied.AuthzConfig.OidcAuthz.TokenEnvVar + } + if len(oidcClientProperties) > 0 { + repoConfig.AuthzConfig.OidcParameters = oidcClientProperties } - repoConfig.AuthzConfig.OidcParameters = oidcClientProperties } } - return repoConfig, nil + return repoConfig } -func getActualPath(filePath string, pvcConfig *feastdevv1alpha1.PvcConfig) string { +func getActualPath(filePath string, pvcConfig *feastdevv1.PvcConfig) string { if pvcConfig == nil { return filePath } @@ -356,6 +688,26 @@ func (feast *FeastServices) extractConfigFromSecret(storeType string, secretRef return parameters, nil } +func (feast *FeastServices) extractConfigFromConfigMap(configMapRef string, configMapKey string) (map[string]interface{}, error) { + configMap, err := feast.getConfigMap(configMapRef) + if err != nil { + return nil, err + } + if configMapKey == "" { + configMapKey = "config" + } + val, exists := configMap.Data[configMapKey] + if !exists { + return nil, fmt.Errorf("configmap key %s doesn't exist in configmap %s", configMapKey, configMapRef) + } + var config map[string]interface{} + err = yaml.Unmarshal([]byte(val), &config) + if err != nil { + return nil, fmt.Errorf("configmap %s contains invalid YAML in key %s", configMapRef, configMapKey) + } + return config, nil +} + func mergeStructWithDBParametersMap(parametersMap *map[string]interface{}, s interface{}) error { for key, val := range *parametersMap { hasAttribute, err := hasAttrib(s, key, val) @@ -375,7 +727,7 @@ func (feast *FeastServices) GetDefaultRepoConfig() RepoConfig { return defaultRepoConfig(feast.Handler.FeatureStore) } -func defaultRepoConfig(featureStore *feastdevv1alpha1.FeatureStore) RepoConfig { +func defaultRepoConfig(featureStore *feastdevv1.FeatureStore) RepoConfig { repoConfig := initRepoConfig(featureStore.Status.Applied.FeastProject) repoConfig.OnlineStore = defaultOnlineStoreConfig(featureStore) repoConfig.Registry = defaultRegistryConfig(featureStore) @@ -390,19 +742,19 @@ func initRepoConfig(feastProject string) RepoConfig { return RepoConfig{ Project: feastProject, Provider: LocalProviderType, - EntityKeySerializationVersion: feastdevv1alpha1.SerializationVersion, + EntityKeySerializationVersion: feastdevv1.SerializationVersion, AuthzConfig: defaultAuthzConfig, } } -func defaultOnlineStoreConfig(featureStore *feastdevv1alpha1.FeatureStore) OnlineStoreConfig { +func defaultOnlineStoreConfig(featureStore *feastdevv1.FeatureStore) OnlineStoreConfig { return OnlineStoreConfig{ Type: OnlineSqliteConfigType, Path: defaultOnlineStorePath(featureStore), } } -func defaultRegistryConfig(featureStore *feastdevv1alpha1.FeatureStore) RegistryConfig { +func defaultRegistryConfig(featureStore *feastdevv1.FeatureStore) RegistryConfig { return RegistryConfig{ RegistryType: RegistryFileConfigType, Path: defaultRegistryPath(featureStore), diff --git a/infra/feast-operator/internal/controller/services/repo_config_test.go b/infra/feast-operator/internal/controller/services/repo_config_test.go index 72ba289900d..e87efdf7dec 100644 --- a/infra/feast-operator/internal/controller/services/repo_config_test.go +++ b/infra/feast-operator/internal/controller/services/repo_config_test.go @@ -25,11 +25,13 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" ) var projectName = "test-project" +const marquezUrl = "http://marquez:5000" + var _ = Describe("Repo Config", func() { Context("When creating the RepoConfig of a FeatureStore", func() { It("should successfully create the repo configs", func() { @@ -46,7 +48,7 @@ var _ = Describe("Repo Config", func() { Path: EphemeralPath + "/" + DefaultOnlineStorePath, } - repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret) + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig)) @@ -56,11 +58,11 @@ var _ = Describe("Repo Config", func() { By("Having the local registry resource") featureStore = minimalFeatureStore() testPath := "/test/file.db" - featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: testPath, }, }, @@ -74,7 +76,7 @@ var _ = Describe("Repo Config", func() { Path: testPath, } - repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig)) @@ -82,10 +84,10 @@ var _ = Describe("Repo Config", func() { Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) By("Adding an offlineStore with PVC") - featureStore.Spec.Services.OfflineStore = &feastdevv1alpha1.OfflineStore{ - Persistence: &feastdevv1alpha1.OfflineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ - PvcConfig: &feastdevv1alpha1.PvcConfig{ + featureStore.Spec.Services.OfflineStore = &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ + PvcConfig: &feastdevv1.PvcConfig{ MountPath: "/testing", }, }, @@ -96,7 +98,7 @@ var _ = Describe("Repo Config", func() { Expect(appliedServices.OnlineStore).NotTo(BeNil()) Expect(appliedServices.Registry.Local).NotTo(BeNil()) - repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.OfflineStore).To(Equal(defaultOfflineStoreConfig)) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) @@ -105,17 +107,17 @@ var _ = Describe("Repo Config", func() { By("Having the remote registry resource") featureStore = minimalFeatureStore() - featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Remote: &feastdevv1alpha1.RemoteRegistryConfig{ - FeastRef: &feastdevv1alpha1.FeatureStoreRef{ + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + FeastRef: &feastdevv1.FeatureStoreRef{ Name: "registry", }, }, }, } ApplyDefaultsToStatus(featureStore) - repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(emptyOfflineStoreConfig)) @@ -124,26 +126,26 @@ var _ = Describe("Repo Config", func() { By("Having the all the file services") featureStore = minimalFeatureStore() - featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Persistence: &feastdevv1alpha1.OfflineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ Type: "duckdb", }, }, }, - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Persistence: &feastdevv1alpha1.OnlineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ - Path: "/data/online.db", + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ + Path: dataOnlineDbPath, }, }, }, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ - Path: "/data/registry.db", + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ + Path: dataRegistryDbPath, }, }, }, @@ -156,14 +158,14 @@ var _ = Describe("Repo Config", func() { } expectedRegistryConfig = RegistryConfig{ RegistryType: "file", - Path: "/data/registry.db", + Path: dataRegistryDbPath, } expectedOnlineConfig = OnlineStoreConfig{ Type: "sqlite", - Path: "/data/online.db", + Path: dataOnlineDbPath, } - repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(NoAuthAuthType)) Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) @@ -172,14 +174,14 @@ var _ = Describe("Repo Config", func() { By("Having kubernetes authorization") featureStore = minimalFeatureStore() - featureStore.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{ - KubernetesAuthz: &feastdevv1alpha1.KubernetesAuthz{}, - } - featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{}, - OnlineStore: &feastdevv1alpha1.OnlineStore{}, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{}, + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + KubernetesAuthz: &feastdevv1.KubernetesAuthz{}, + } + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{}, + OnlineStore: &feastdevv1.OnlineStore{}, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{}, }, } ApplyDefaultsToStatus(featureStore) @@ -188,18 +190,18 @@ var _ = Describe("Repo Config", func() { Type: "dask", } - repoConfig, err = getServiceRepoConfig(featureStore, mockExtractConfigFromSecret) + repoConfig, err = getServiceRepoConfig(featureStore, mockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(KubernetesAuthType)) Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) Expect(repoConfig.OnlineStore).To(Equal(defaultOnlineStoreConfig(featureStore))) Expect(repoConfig.Registry).To(Equal(defaultRegistryConfig(featureStore))) - By("Having oidc authorization") - featureStore.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{ - OidcAuthz: &feastdevv1alpha1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ - Name: "oidc-secret", + By("Having oidc authorization with Secret") + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{ + SecretRef: &corev1.LocalObjectReference{ + Name: oidcSecretName, }, }, } @@ -207,34 +209,76 @@ var _ = Describe("Repo Config", func() { secretExtractionFunc := mockOidcConfigFromSecret(map[string]interface{}{ string(OidcAuthDiscoveryUrl): "discovery-url", - string(OidcClientId): "client-id", + string(OidcClientId): clientIDValue, string(OidcClientSecret): "client-secret", string(OidcUsername): "username", - string(OidcPassword): "password"}) - repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc) + string(OidcPassword): "password", + string(OidcAudience): "api://feast-feature-server", + string(OidcIssuer): "https://login.example.com/realms/master"}) + repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(2)) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(7)) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientId))) Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcAuthDiscoveryUrl))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientSecret))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcUsername))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcPassword))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcAudience), "api://feast-feature-server")) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcIssuer), "https://login.example.com/realms/master")) Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) Expect(repoConfig.OnlineStore).To(Equal(defaultOnlineStoreConfig(featureStore))) Expect(repoConfig.Registry).To(Equal(defaultRegistryConfig(featureStore))) - repoConfig, err = getClientRepoConfig(featureStore, secretExtractionFunc, nil) + repoConfig = getClientRepoConfig(featureStore, nil) + Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) + + By("Coercing numeric audience and issuer Secret values to strings") + secretExtractionFunc = mockOidcConfigFromSecret(map[string]interface{}{ + string(OidcAuthDiscoveryUrl): "discovery-url", + string(OidcClientId): clientIDValue, + // Secret extraction YAML-parses values, so an all-digits + // audience/issuer reaches this code as an int. + string(OidcAudience): 1234567890, + string(OidcIssuer): 9876543210}) + repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcAudience), "1234567890")) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKeyWithValue(string(OidcIssuer), "9876543210")) + + By("Having oidc authorization with issuerUrl only (no Secret)") + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{ + IssuerUrl: "https://keycloak.example.com/realms/test", + }, + } + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) Expect(repoConfig.AuthzConfig.Type).To(Equal(OidcAuthType)) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(3)) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcClientSecret))) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcUsername))) - Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveKey(string(OidcPassword))) + Expect(repoConfig.AuthzConfig.OidcParameters).To(HaveLen(1)) + Expect(repoConfig.AuthzConfig.OidcParameters[string(OidcAuthDiscoveryUrl)]).To(Equal("https://keycloak.example.com/realms/test/.well-known/openid-configuration")) + + By("Having oidc with issuerUrl on CR and auth_discovery_url in Secret — CR wins") + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{ + IssuerUrl: "https://keycloak.example.com/realms/cr-wins", + SecretRef: &corev1.LocalObjectReference{ + Name: oidcSecretName, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.AuthzConfig.OidcParameters[string(OidcAuthDiscoveryUrl)]).To(Equal("https://keycloak.example.com/realms/cr-wins/.well-known/openid-configuration")) By("Having the all the db services") featureStore = minimalFeatureStore() - featureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Persistence: &feastdevv1alpha1.OfflineStorePersistence{ - DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + DBPersistence: &feastdevv1.OfflineStoreDBStorePersistence{ Type: string(OfflineDBPersistenceSnowflakeConfigType), SecretRef: corev1.LocalObjectReference{ Name: "offline-test-secret", @@ -242,9 +286,9 @@ var _ = Describe("Repo Config", func() { }, }, }, - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Persistence: &feastdevv1alpha1.OnlineStorePersistence{ - DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ Type: string(OnlineDBPersistenceSnowflakeConfigType), SecretRef: corev1.LocalObjectReference{ Name: "online-test-secret", @@ -252,10 +296,10 @@ var _ = Describe("Repo Config", func() { }, }, }, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + DBPersistence: &feastdevv1.RegistryDBStorePersistence{ Type: string(RegistryDBPersistenceSnowflakeConfigType), SecretRef: corev1.LocalObjectReference{ Name: "registry-test-secret", @@ -270,7 +314,7 @@ var _ = Describe("Repo Config", func() { featureStore.Spec.Services.OfflineStore.Persistence.FilePersistence = nil featureStore.Spec.Services.OnlineStore.Persistence.FilePersistence = nil featureStore.Spec.Services.Registry.Local.Persistence.FilePersistence = nil - repoConfig, err = getServiceRepoConfig(featureStore, mockExtractConfigFromSecret) + repoConfig, err = getServiceRepoConfig(featureStore, mockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) Expect(err).NotTo(HaveOccurred()) newMap := CopyMap(parameterMap) port := parameterMap["port"].(int) @@ -291,43 +335,465 @@ var _ = Describe("Repo Config", func() { Expect(repoConfig.OfflineStore).To(Equal(expectedOfflineConfig)) Expect(repoConfig.OnlineStore).To(Equal(expectedOnlineConfig)) Expect(repoConfig.Registry).To(Equal(expectedRegistryConfig)) + + By("Having DQM config with auto_baseline disabled") + featureStore = minimalFeatureStore() + dqmAutoBaseline := false + featureStore.Spec.DataQualityMonitoring = &feastdevv1.DataQualityMonitoringConfig{ + AutoBaseline: &dqmAutoBaseline, + } + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.DataQualityMonitoring).NotTo(BeNil()) + Expect(repoConfig.DataQualityMonitoring.AutoBaseline).To(BeFalse()) + + fsYaml, marshalErr := yaml.Marshal(repoConfig) + Expect(marshalErr).NotTo(HaveOccurred()) + Expect(string(fsYaml)).To(ContainSubstring("data_quality_monitoring:")) + Expect(string(fsYaml)).To(ContainSubstring("auto_baseline: false")) + + By("Having no DataQualityMonitoring config — should be nil") + featureStore = minimalFeatureStore() + ApplyDefaultsToStatus(featureStore) + repoConfig, err = getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.DataQualityMonitoring).To(BeNil()) + }) + + It("should set feature_server block with type local and all options", func() { + featureStore := minimalFeatureStore() + batchSize := int32(500) + batchInterval := int32(15) + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Serving: &feastdevv1.ServingConfig{ + Metrics: &feastdevv1.ServingMetricsConfig{ + Enabled: true, + Categories: map[string]bool{ + "resource": true, + "freshness": false, + "registry_sync": false, + }, + }, + OfflinePushBatching: &feastdevv1.OfflinePushBatchingConfig{ + Enabled: true, + BatchSize: &batchSize, + BatchIntervalSeconds: &batchInterval, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.FeatureServer).NotTo(BeNil()) + Expect(repoConfig.FeatureServer.Type).To(Equal("local")) + + Expect(repoConfig.FeatureServer.Metrics).NotTo(BeNil()) + Expect(repoConfig.FeatureServer.Metrics.Enabled).To(BeTrue()) + Expect(repoConfig.FeatureServer.Metrics.Categories).To(HaveKeyWithValue("resource", true)) + Expect(repoConfig.FeatureServer.Metrics.Categories).To(HaveKeyWithValue("freshness", false)) + Expect(repoConfig.FeatureServer.Metrics.Categories).To(HaveKeyWithValue("registry_sync", false)) + + Expect(repoConfig.FeatureServer.OfflinePushBatchingEnabled).NotTo(BeNil()) + Expect(*repoConfig.FeatureServer.OfflinePushBatchingEnabled).To(BeTrue()) + Expect(repoConfig.FeatureServer.OfflinePushBatchingBatchSize).To(Equal(&batchSize)) + Expect(repoConfig.FeatureServer.OfflinePushBatchingBatchIntervalSeconds).To(Equal(&batchInterval)) + + Expect(repoConfig.FeatureServer.McpEnabled).To(BeNil()) + }) + + It("should set feature_server block with type mcp", func() { + featureStore := minimalFeatureStore() + serverName := "my-mcp-server" + serverVersion := "2.0.0" + transport := HttpScheme + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Serving: &feastdevv1.ServingConfig{ + Mcp: &feastdevv1.McpConfig{ + Enabled: true, + ServerName: &serverName, + ServerVersion: &serverVersion, + Transport: &transport, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.FeatureServer).NotTo(BeNil()) + Expect(repoConfig.FeatureServer.Type).To(Equal("mcp")) + Expect(repoConfig.FeatureServer.McpEnabled).NotTo(BeNil()) + Expect(*repoConfig.FeatureServer.McpEnabled).To(BeTrue()) + Expect(repoConfig.FeatureServer.McpServerName).To(Equal(&serverName)) + Expect(repoConfig.FeatureServer.McpServerVersion).To(Equal(&serverVersion)) + Expect(repoConfig.FeatureServer.McpTransport).To(Equal(&transport)) + }) + + It("should use type local when Mcp is present but Enabled is false", func() { + featureStore := minimalFeatureStore() + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Serving: &feastdevv1.ServingConfig{ + Mcp: &feastdevv1.McpConfig{ + Enabled: false, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.FeatureServer).NotTo(BeNil()) + Expect(repoConfig.FeatureServer.Type).To(Equal("local")) + Expect(repoConfig.FeatureServer.McpEnabled).To(BeNil()) + }) + + It("should set registry mcp when enabled", func() { + featureStore := minimalFeatureStore() + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + Mcp: &feastdevv1.McpConfig{ + Enabled: true, + }, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Registry.Mcp).NotTo(BeNil()) + Expect(repoConfig.Registry.Mcp.Enabled).To(BeTrue()) + }) + + It("should not set registry mcp when disabled", func() { + featureStore := minimalFeatureStore() + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + Mcp: &feastdevv1.McpConfig{ + Enabled: false, + }, + }, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Registry.Mcp).To(BeNil()) + }) + + It("should not set registry mcp when server has no mcp config", func() { + featureStore := minimalFeatureStore() + + featureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{}, + }, + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Registry.Mcp).To(BeNil()) + }) + + It("should set materialization block", func() { + featureStore := minimalFeatureStore() + batchSize := int32(10000) + + featureStore.Spec.Materialization = &feastdevv1.MaterializationConfig{ + OnlineWriteBatchSize: &batchSize, + ExtraConfig: map[string]string{ + "pull_latest_features": stringFalse, + "max_workers": "4", + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.Materialization).NotTo(BeNil()) + Expect(repoConfig.Materialization.OnlineWriteBatchSize).To(Equal(&batchSize)) + // "true"/"false" strings are coerced to native booleans; other strings pass through unchanged. + Expect(repoConfig.Materialization.ExtraConfig).To(HaveKeyWithValue("pull_latest_features", false)) + Expect(repoConfig.Materialization.ExtraConfig).To(HaveKeyWithValue("max_workers", "4")) + }) + + It("should set openlineage block without api_key secret", func() { + featureStore := minimalFeatureStore() + transportType := HttpScheme + transportUrl := marquezUrl + endpoint := "api/v1/lineage" + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + TransportUrl: &transportUrl, + TransportEndpoint: &endpoint, + ExtraConfig: map[string]string{ + "namespace": "my-feast", + "producer": "feast-operator", + "emit_on_apply": stringTrue, + "emit_on_materialize": "false", + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.OpenLineage).NotTo(BeNil()) + Expect(repoConfig.OpenLineage.Enabled).To(BeTrue()) + Expect(repoConfig.OpenLineage.TransportType).To(Equal(&transportType)) + Expect(repoConfig.OpenLineage.TransportUrl).To(Equal(&transportUrl)) + Expect(repoConfig.OpenLineage.TransportEndpoint).To(Equal(&endpoint)) + Expect(repoConfig.OpenLineage.ApiKey).To(BeNil()) + // ExtraConfig: "true"/"false" strings coerced to booleans; other strings unchanged. + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("namespace", "my-feast")) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("producer", "feast-operator")) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("emit_on_apply", true)) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("emit_on_materialize", false)) + }) + + It("should set openlineage block with kafka extraConfig", func() { + featureStore := minimalFeatureStore() + transportType := "kafka" + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + ExtraConfig: map[string]string{ + "bootstrap_servers": "kafka.svc:9092", + "topic": "openlineage", + "sasl_mechanism": "PLAIN", + }, + } + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.OpenLineage).NotTo(BeNil()) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("bootstrap_servers", "kafka.svc:9092")) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("topic", "openlineage")) + Expect(repoConfig.OpenLineage.ExtraConfig).To(HaveKeyWithValue("sasl_mechanism", "PLAIN")) + }) + + It("should resolve api_key from secret for openlineage", func() { + featureStore := minimalFeatureStore() + transportType := HttpScheme + transportUrl := marquezUrl + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + TransportUrl: &transportUrl, + ApiKeySecretRef: &corev1.LocalObjectReference{ + Name: lineageSecretName, + }, + } + ApplyDefaultsToStatus(featureStore) + + apiKeyMockExtract := func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error) { + return map[string]interface{}{ + "api_key": "my-secret-key", + }, nil + } + + repoConfig, err := getServiceRepoConfig(featureStore, apiKeyMockExtract, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.OpenLineage).NotTo(BeNil()) + Expect(repoConfig.OpenLineage.ApiKey).NotTo(BeNil()) + Expect(*repoConfig.OpenLineage.ApiKey).To(Equal("my-secret-key")) + }) + + It("should return error when apiKeySecretRef Secret is missing the api_key key", func() { + featureStore := minimalFeatureStore() + transportType := HttpScheme + transportUrl := marquezUrl + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + TransportUrl: &transportUrl, + ApiKeySecretRef: &corev1.LocalObjectReference{ + Name: lineageSecretName, + }, + } + ApplyDefaultsToStatus(featureStore) + + missingKeyMock := func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error) { + return map[string]interface{}{ + "wrong_key": "some-value", + }, nil + } + + _, err := getServiceRepoConfig(featureStore, missingKeyMock, emptyMockExtractConfigFromConfigMap, false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("api_key")) + Expect(err.Error()).To(ContainSubstring(lineageSecretName)) + }) + + It("should return error when apiKeySecretRef api_key value is not a string", func() { + featureStore := minimalFeatureStore() + transportType := HttpScheme + transportUrl := marquezUrl + + featureStore.Spec.OpenLineage = &feastdevv1.OpenLineageConfig{ + Enabled: true, + TransportType: &transportType, + TransportUrl: &transportUrl, + ApiKeySecretRef: &corev1.LocalObjectReference{ + Name: lineageSecretName, + }, + } + ApplyDefaultsToStatus(featureStore) + + nonStringMock := func(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error) { + return map[string]interface{}{ + "api_key": 12345, // integer, not a string + }, nil + } + + _, err := getServiceRepoConfig(featureStore, nonStringMock, emptyMockExtractConfigFromConfigMap, false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("api_key")) + Expect(err.Error()).To(ContainSubstring(lineageSecretName)) + }) + + It("should not set feature_server block when serving is nil", func() { + featureStore := minimalFeatureStore() + ApplyDefaultsToStatus(featureStore) + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.FeatureServer).To(BeNil()) + Expect(repoConfig.Materialization).To(BeNil()) + Expect(repoConfig.OpenLineage).To(BeNil()) + }) + + It("should inject default batch_engine.service_account when ConfigMap omits it", func() { + featureStore := minimalFeatureStore() + featureStore.Name = "spark-pg-e2e" + featureStore.Spec.BatchEngine = &feastdevv1.BatchEngineConfig{ + ConfigMapRef: &corev1.LocalObjectReference{Name: "spark-pg-batch-engine"}, + } + ApplyDefaultsToStatus(featureStore) + + extractCM := func(configMapRef string, configMapKey string) (map[string]interface{}, error) { + return map[string]interface{}{ + "type": "spark_application", + "image": "quay.io/example/feast-spark-driver:v6", + // service_account intentionally omitted + }, nil + } + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, extractCM, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.BatchEngine).NotTo(BeNil()) + Expect(repoConfig.BatchEngine.Type).To(Equal("spark_application")) + Expect(repoConfig.BatchEngine.Parameters["service_account"]).To(Equal("feast-spark-pg-e2e-batch-driver")) + }) + + It("should preserve explicit batch_engine.service_account from ConfigMap", func() { + featureStore := minimalFeatureStore() + featureStore.Name = "spark-pg-e2e" + featureStore.Spec.BatchEngine = &feastdevv1.BatchEngineConfig{ + ConfigMapRef: &corev1.LocalObjectReference{Name: "spark-pg-batch-engine"}, + } + ApplyDefaultsToStatus(featureStore) + + extractCM := func(configMapRef string, configMapKey string) (map[string]interface{}, error) { + return map[string]interface{}{ + "type": "spark_application", + "service_account": "my-custom-driver", + }, nil + } + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, extractCM, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.BatchEngine.Parameters["service_account"]).To(Equal("my-custom-driver")) + }) + + It("should not inject service_account for non-spark_application batch engines", func() { + featureStore := minimalFeatureStore() + featureStore.Name = "spark-pg-e2e" + featureStore.Spec.BatchEngine = &feastdevv1.BatchEngineConfig{ + ConfigMapRef: &corev1.LocalObjectReference{Name: "other-batch-engine"}, + } + ApplyDefaultsToStatus(featureStore) + + extractCM := func(configMapRef string, configMapKey string) (map[string]interface{}, error) { + return map[string]interface{}{ + "type": "spark", + // no service_account — must stay omitted for non-spark_application + }, nil + } + + repoConfig, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, extractCM, false) + Expect(err).NotTo(HaveOccurred()) + Expect(repoConfig.BatchEngine).NotTo(BeNil()) + Expect(repoConfig.BatchEngine.Type).To(Equal("spark")) + _, hasSA := repoConfig.BatchEngine.Parameters["service_account"] + Expect(hasSA).To(BeFalse()) }) }) It("should fail to create the repo configs", func() { featureStore := minimalFeatureStore() - By("Having invalid server oidc authorization") - featureStore.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{ - OidcAuthz: &feastdevv1alpha1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ - Name: "oidc-secret", + By("Having oidc with no issuerUrl, no Secret, no env var — should fail") + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{}, + } + ApplyDefaultsToStatus(featureStore) + + _, err := getServiceRepoConfig(featureStore, emptyMockExtractConfigFromSecret, emptyMockExtractConfigFromConfigMap, false) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no OIDC discovery URL configured")) + + By("Having oidc with Secret missing auth_discovery_url and no issuerUrl — should fail") + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{ + SecretRef: &corev1.LocalObjectReference{ + Name: oidcSecretName, }, }, } ApplyDefaultsToStatus(featureStore) secretExtractionFunc := mockOidcConfigFromSecret(map[string]interface{}{ - string(OidcClientId): "client-id", + string(OidcClientId): clientIDValue, string(OidcClientSecret): "client-secret", string(OidcUsername): "username", string(OidcPassword): "password"}) - _, err := getServiceRepoConfig(featureStore, secretExtractionFunc) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) - _, err = getServiceRepoConfig(featureStore, secretExtractionFunc) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) - _, err = getServiceRepoConfig(featureStore, secretExtractionFunc) + _, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) - _, err = getClientRepoConfig(featureStore, secretExtractionFunc, nil) - Expect(err).ToNot(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no OIDC discovery URL configured")) By("Having invalid client oidc authorization") - featureStore.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{ - OidcAuthz: &feastdevv1alpha1.OidcAuthz{ - SecretRef: corev1.LocalObjectReference{ - Name: "oidc-secret", + featureStore.Spec.AuthzConfig = &feastdevv1.AuthzConfig{ + OidcAuthz: &feastdevv1.OidcAuthz{ + SecretRef: &corev1.LocalObjectReference{ + Name: oidcSecretName, }, }, } @@ -335,49 +801,40 @@ var _ = Describe("Repo Config", func() { secretExtractionFunc = mockOidcConfigFromSecret(map[string]interface{}{ string(OidcAuthDiscoveryUrl): "discovery-url", - string(OidcClientId): "client-id", + string(OidcClientId): clientIDValue, string(OidcUsername): "username", string(OidcPassword): "password"}) - _, err = getServiceRepoConfig(featureStore, secretExtractionFunc) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) - _, err = getServiceRepoConfig(featureStore, secretExtractionFunc) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) - _, err = getServiceRepoConfig(featureStore, secretExtractionFunc) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) - _, err = getClientRepoConfig(featureStore, secretExtractionFunc, nil) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("missing OIDC secret")) + _, err = getServiceRepoConfig(featureStore, secretExtractionFunc, emptyMockExtractConfigFromConfigMap, false) + Expect(err).NotTo(HaveOccurred()) + getClientRepoConfig(featureStore, nil) }) }) var emptyOfflineStoreConfig = OfflineStoreConfig{} var emptyRegistryConfig = RegistryConfig{} -func minimalFeatureStore() *feastdevv1alpha1.FeatureStore { - return &feastdevv1alpha1.FeatureStore{ +func minimalFeatureStore() *feastdevv1.FeatureStore { + return &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: projectName, }, } } -func minimalFeatureStoreWithAllServers() *feastdevv1alpha1.FeatureStore { +func minimalFeatureStoreWithAllServers() *feastdevv1.FeatureStore { feast := minimalFeatureStore() // onlineStore configured by default - feast.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Server: &feastdevv1alpha1.ServerConfigs{}, + feast.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{}, }, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{}, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{}, }, }, - UI: &feastdevv1alpha1.ServerConfigs{}, + UI: &feastdevv1.ServerConfigs{}, } return feast } @@ -386,6 +843,10 @@ func emptyMockExtractConfigFromSecret(storeType string, secretRef string, secret return map[string]interface{}{}, nil } +func emptyMockExtractConfigFromConfigMap(configMapRef string, configMapKey string) (map[string]interface{}, error) { + return map[string]interface{}{}, nil +} + func mockExtractConfigFromSecret(storeType string, secretRef string, secretKeyName string) (map[string]interface{}, error) { return createParameterMap(), nil } @@ -464,51 +925,51 @@ var _ = Describe("TLS Certificate Path Configuration", func() { Context("in getClientRepoConfig", func() { It("should use individual service certificate paths when no custom CA bundle", func() { // Create a feature store with TLS enabled - featureStore := &feastdevv1alpha1.FeatureStore{ - Status: feastdevv1alpha1.FeatureStoreStatus{ - ServiceHostnames: feastdevv1alpha1.ServiceHostnames{ + featureStore := &feastdevv1.FeatureStore{ + Status: feastdevv1.FeatureStoreStatus{ + ServiceHostnames: feastdevv1.ServiceHostnames{ OfflineStore: "offline.example.com:443", OnlineStore: "online.example.com:443", Registry: "registry.example.com:443", }, - Applied: feastdevv1alpha1.FeatureStoreSpec{ - Services: &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + Applied: feastdevv1.FeatureStoreSpec{ + Services: &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "offline-tls"}, - SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ - TlsCrt: "tls.crt", + SecretKeyNames: feastdevv1.SecretKeyNames{ + TlsCrt: tlsCertKey, }, }, }, }, - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "online-tls"}, - SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ - TlsCrt: "tls.crt", + SecretKeyNames: feastdevv1.SecretKeyNames{ + TlsCrt: tlsCertKey, }, }, }, }, - UI: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + UI: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "ui-tls"}, - SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ + SecretKeyNames: feastdevv1.SecretKeyNames{ TlsCrt: "tls.crt", }, }, }, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "registry-tls"}, - SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ - TlsCrt: "tls.crt", + SecretKeyNames: feastdevv1.SecretKeyNames{ + TlsCrt: tlsCertKey, }, }, }, @@ -521,8 +982,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { } // Test with nil feast parameter (no custom CA bundle) - repoConfig, err := getClientRepoConfig(featureStore, emptyMockExtractConfigFromSecret, nil) - Expect(err).NotTo(HaveOccurred()) + repoConfig := getClientRepoConfig(featureStore, nil) // Verify individual service certificate paths are used Expect(repoConfig.OfflineStore.Cert).To(Equal("/tls/offline/tls.crt")) @@ -533,52 +993,52 @@ var _ = Describe("TLS Certificate Path Configuration", func() { It("should use custom CA bundle path when available", func() { // This test would require a full FeastServices setup with custom CA bundle // For now, we verify the function signature and basic behavior - featureStore := &feastdevv1alpha1.FeatureStore{ - Status: feastdevv1alpha1.FeatureStoreStatus{ - ServiceHostnames: feastdevv1alpha1.ServiceHostnames{ + featureStore := &feastdevv1.FeatureStore{ + Status: feastdevv1.FeatureStoreStatus{ + ServiceHostnames: feastdevv1.ServiceHostnames{ OfflineStore: "offline.example.com:443", OnlineStore: "online.example.com:443", Registry: "registry.example.com:443", UI: "ui.example.com:443", }, - Applied: feastdevv1alpha1.FeatureStoreSpec{ - Services: &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + Applied: feastdevv1.FeatureStoreSpec{ + Services: &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "offline-tls"}, - SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ - TlsCrt: "tls.crt", + SecretKeyNames: feastdevv1.SecretKeyNames{ + TlsCrt: tlsCertKey, }, }, }, }, - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "online-tls"}, - SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ - TlsCrt: "tls.crt", + SecretKeyNames: feastdevv1.SecretKeyNames{ + TlsCrt: tlsCertKey, }, }, }, }, - UI: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + UI: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "ui-tls"}, - SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ + SecretKeyNames: feastdevv1.SecretKeyNames{ TlsCrt: "tls.crt", }, }, }, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{Name: "registry-tls"}, - SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ - TlsCrt: "tls.crt", + SecretKeyNames: feastdevv1.SecretKeyNames{ + TlsCrt: tlsCertKey, }, }, }, @@ -591,8 +1051,7 @@ var _ = Describe("TLS Certificate Path Configuration", func() { } // Test with nil feast parameter (no custom CA bundle available) - repoConfig, err := getClientRepoConfig(featureStore, emptyMockExtractConfigFromSecret, nil) - Expect(err).NotTo(HaveOccurred()) + repoConfig := getClientRepoConfig(featureStore, nil) Expect(repoConfig.OfflineStore.Cert).To(Equal("/tls/offline/tls.crt")) }) }) diff --git a/infra/feast-operator/internal/controller/services/scaling.go b/infra/feast-operator/internal/controller/services/scaling.go new file mode 100644 index 00000000000..b02dc1eee07 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/scaling.go @@ -0,0 +1,268 @@ +/* +Copyright 2026 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "encoding/json" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + hpaac "k8s.io/client-go/applyconfigurations/autoscaling/v2" + metaac "k8s.io/client-go/applyconfigurations/meta/v1" + pdbac "k8s.io/client-go/applyconfigurations/policy/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + defaultHPACPUUtilization int32 = 80 + defaultHPAMinReplicas int32 = 1 + fieldManager = "feast-operator" +) + +// getDesiredReplicas returns the replica count the operator should set on the +// Deployment. When autoscaling is configured the Deployment replicas field is +// left to the HPA (nil is returned). Otherwise the static replica count from +// spec.replicas is returned. +func (feast *FeastServices) getDesiredReplicas() *int32 { + cr := feast.Handler.FeatureStore + services := cr.Status.Applied.Services + if services != nil && services.Scaling != nil && services.Scaling.Autoscaling != nil { + return nil + } + if cr.Status.Applied.Replicas != nil { + r := *cr.Status.Applied.Replicas + return &r + } + return nil +} + +// createOrDeleteHPA reconciles the HorizontalPodAutoscaler for the FeatureStore +// deployment using Server-Side Apply with typed apply configurations. If +// autoscaling is not configured, any existing HPA is deleted. +func (feast *FeastServices) createOrDeleteHPA() error { + cr := feast.Handler.FeatureStore + + scaling := cr.Status.Applied.Services.Scaling + if scaling == nil || scaling.Autoscaling == nil { + hpa := &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: feast.GetObjectMeta(), + } + hpa.SetGroupVersionKind(autoscalingv2.SchemeGroupVersion.WithKind("HorizontalPodAutoscaler")) + return feast.Handler.DeleteOwnedFeastObj(hpa) + } + + hpaAC := feast.buildHPAApplyConfig() + data, err := json.Marshal(hpaAC) + if err != nil { + return err + } + + hpa := &autoscalingv2.HorizontalPodAutoscaler{ObjectMeta: feast.GetObjectMeta()} + logger := log.FromContext(feast.Handler.Context) + if err := feast.Handler.Client.Patch(feast.Handler.Context, hpa, + client.RawPatch(types.ApplyPatchType, data), + client.FieldOwner(fieldManager), client.ForceOwnership); err != nil { + return err + } + logger.Info("Successfully applied", "HorizontalPodAutoscaler", hpa.Name) + + return nil +} + +// buildHPAApplyConfig constructs the fully desired HPA state as a typed apply +// configuration for Server-Side Apply. +func (feast *FeastServices) buildHPAApplyConfig() *hpaac.HorizontalPodAutoscalerApplyConfiguration { + cr := feast.Handler.FeatureStore + autoscaling := cr.Status.Applied.Services.Scaling.Autoscaling + objMeta := feast.GetObjectMeta() + deploy := feast.initFeastDeploy() + + minReplicas := defaultHPAMinReplicas + if autoscaling.MinReplicas != nil { + minReplicas = *autoscaling.MinReplicas + } + + hpa := hpaac.HorizontalPodAutoscaler(objMeta.Name, objMeta.Namespace). + WithLabels(feast.getLabels()). + WithOwnerReferences( + metaac.OwnerReference(). + WithAPIVersion(feastdevv1.GroupVersion.String()). + WithKind("FeatureStore"). + WithName(cr.Name). + WithUID(cr.UID). + WithController(true). + WithBlockOwnerDeletion(true), + ). + WithSpec(hpaac.HorizontalPodAutoscalerSpec(). + WithScaleTargetRef( + hpaac.CrossVersionObjectReference(). + WithAPIVersion(appsv1.SchemeGroupVersion.String()). + WithKind("Deployment"). + WithName(deploy.Name), + ). + WithMinReplicas(minReplicas). + WithMaxReplicas(autoscaling.MaxReplicas), + ) + + if len(autoscaling.Metrics) > 0 { + hpa.Spec.Metrics = convertMetrics(autoscaling.Metrics) + } else { + hpa.Spec.Metrics = defaultHPAMetrics() + } + + if autoscaling.Behavior != nil { + hpa.Spec.Behavior = convertBehavior(autoscaling.Behavior) + } + + return hpa +} + +func defaultHPAMetrics() []hpaac.MetricSpecApplyConfiguration { + return []hpaac.MetricSpecApplyConfiguration{ + *hpaac.MetricSpec(). + WithType(autoscalingv2.ResourceMetricSourceType). + WithResource( + hpaac.ResourceMetricSource(). + WithName(corev1.ResourceCPU). + WithTarget( + hpaac.MetricTarget(). + WithType(autoscalingv2.UtilizationMetricType). + WithAverageUtilization(defaultHPACPUUtilization), + ), + ), + } +} + +// convertMetrics converts standard API metric specs to their apply configuration +// equivalents via JSON round-trip (the types share identical JSON schemas). +func convertMetrics(metrics []autoscalingv2.MetricSpec) []hpaac.MetricSpecApplyConfiguration { + data, err := json.Marshal(metrics) + if err != nil { + return nil + } + var result []hpaac.MetricSpecApplyConfiguration + if err := json.Unmarshal(data, &result); err != nil { + return nil + } + return result +} + +// convertBehavior converts a standard API behavior spec to its apply configuration +// equivalent via JSON round-trip. +func convertBehavior(behavior *autoscalingv2.HorizontalPodAutoscalerBehavior) *hpaac.HorizontalPodAutoscalerBehaviorApplyConfiguration { + data, err := json.Marshal(behavior) + if err != nil { + return nil + } + result := &hpaac.HorizontalPodAutoscalerBehaviorApplyConfiguration{} + if err := json.Unmarshal(data, result); err != nil { + return nil + } + return result +} + +// applyOrDeletePDB reconciles the PodDisruptionBudget for the FeatureStore +// deployment using Server-Side Apply. If PodDisruptionBudgets is not configured +// or scaling is not enabled, any existing PDB is deleted. +func (feast *FeastServices) applyOrDeletePDB() error { + cr := feast.Handler.FeatureStore + services := cr.Status.Applied.Services + + if services == nil || services.PodDisruptionBudgets == nil || !isScalingEnabled(cr) { + pdb := &policyv1.PodDisruptionBudget{ObjectMeta: feast.GetObjectMeta()} + pdb.SetGroupVersionKind(policyv1.SchemeGroupVersion.WithKind("PodDisruptionBudget")) + return feast.Handler.DeleteOwnedFeastObj(pdb) + } + + pdbAC := feast.buildPDBApplyConfig() + data, err := json.Marshal(pdbAC) + if err != nil { + return err + } + + pdb := &policyv1.PodDisruptionBudget{ObjectMeta: feast.GetObjectMeta()} + logger := log.FromContext(feast.Handler.Context) + if err := feast.Handler.Client.Patch(feast.Handler.Context, pdb, + client.RawPatch(types.ApplyPatchType, data), + client.FieldOwner(fieldManager), client.ForceOwnership); err != nil { + return err + } + logger.Info("Successfully applied", "PodDisruptionBudget", pdb.Name) + + return nil +} + +// buildPDBApplyConfig constructs the fully desired PDB state as a typed apply +// configuration for Server-Side Apply. +func (feast *FeastServices) buildPDBApplyConfig() *pdbac.PodDisruptionBudgetApplyConfiguration { + cr := feast.Handler.FeatureStore + pdbConfig := cr.Status.Applied.Services.PodDisruptionBudgets + objMeta := feast.GetObjectMeta() + + pdb := pdbac.PodDisruptionBudget(objMeta.Name, objMeta.Namespace). + WithLabels(feast.getLabels()). + WithOwnerReferences( + metaac.OwnerReference(). + WithAPIVersion(feastdevv1.GroupVersion.String()). + WithKind("FeatureStore"). + WithName(cr.Name). + WithUID(cr.UID). + WithController(true). + WithBlockOwnerDeletion(true), + ). + WithSpec(pdbac.PodDisruptionBudgetSpec(). + WithSelector(metaac.LabelSelector().WithMatchLabels(feast.getSelectorLabels())), + ) + + if pdbConfig.MinAvailable != nil { + pdb.Spec.WithMinAvailable(*pdbConfig.MinAvailable) + } + if pdbConfig.MaxUnavailable != nil { + pdb.Spec.WithMaxUnavailable(*pdbConfig.MaxUnavailable) + } + + return pdb +} + +// updateScalingStatus updates the scaling status fields using the deployment +func (feast *FeastServices) updateScalingStatus(deploy *appsv1.Deployment) { + cr := feast.Handler.FeatureStore + + cr.Status.Replicas = deploy.Status.ReadyReplicas + cr.Status.Selector = metav1.FormatLabelSelector(metav1.SetAsLabelSelector(feast.getSelectorLabels())) + + if !isScalingEnabled(cr) { + cr.Status.ScalingStatus = nil + return + } + + var desired int32 + if deploy.Spec.Replicas != nil { + desired = *deploy.Spec.Replicas + } + + cr.Status.ScalingStatus = &feastdevv1.ScalingStatus{ + CurrentReplicas: deploy.Status.ReadyReplicas, + DesiredReplicas: desired, + } +} diff --git a/infra/feast-operator/internal/controller/services/scaling_test.go b/infra/feast-operator/internal/controller/services/scaling_test.go new file mode 100644 index 00000000000..1f65a4835ce --- /dev/null +++ b/infra/feast-operator/internal/controller/services/scaling_test.go @@ -0,0 +1,1009 @@ +/* +Copyright 2026 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + autoscalingv1 "k8s.io/api/autoscaling/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("Horizontal Scaling", func() { + var ( + featureStore *feastdevv1.FeatureStore + feast *FeastServices + typeNamespacedName types.NamespacedName + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + typeNamespacedName = types.NamespacedName{ + Name: "scaling-test-fs", + Namespace: DefaultNs, + } + + featureStore = &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: typeNamespacedName.Name, + Namespace: typeNamespacedName.Namespace, + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "scalingproject", + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ + Type: redisType, + SecretRef: corev1.LocalObjectReference{ + Name: redisSecretName, + }, + }, + }, + }, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + GRPC: ptr.To(true), + }, + Persistence: &feastdevv1.RegistryPersistence{ + DBPersistence: &feastdevv1.RegistryDBStorePersistence{ + Type: "sql", + SecretRef: corev1.LocalObjectReference{ + Name: registrySecretName, + }, + }, + }, + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, featureStore)).To(Succeed()) + applySpecToStatus(featureStore) + + feast = &FeastServices{ + Handler: handler.FeastHandler{ + Client: k8sClient, + Context: ctx, + Scheme: k8sClient.Scheme(), + FeatureStore: featureStore, + }, + } + }) + + AfterEach(func() { + Expect(k8sClient.Delete(ctx, featureStore)).To(Succeed()) + }) + + Describe("isScalingEnabled", func() { + It("should return false when no scaling config is present", func() { + Expect(isScalingEnabled(featureStore)).To(BeFalse()) + }) + + It("should return false when replicas=1", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(1)) + Expect(isScalingEnabled(featureStore)).To(BeFalse()) + }) + + It("should return true when replicas > 1", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + Expect(isScalingEnabled(featureStore)).To(BeTrue()) + }) + + It("should return true when autoscaling is configured", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{ + MaxReplicas: 5, + }, + } + Expect(isScalingEnabled(featureStore)).To(BeTrue()) + }) + }) + + Describe("CEL admission validation rejects invalid scaling configurations", func() { + dbOnlineStore := &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ + Type: redisType, + SecretRef: corev1.LocalObjectReference{Name: redisSecretName}, + }, + }, + } + + dbRegistry := &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + DBPersistence: &feastdevv1.RegistryDBStorePersistence{ + Type: "sql", + SecretRef: corev1.LocalObjectReference{Name: registrySecretName}, + }, + }, + }, + } + + It("should accept scaling with full DB persistence", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-valid-db", Namespace: DefaultNs}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + }, + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + + It("should reject scaling when online store is missing (implicit file default)", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-no-online", Namespace: DefaultNs}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + Registry: dbRegistry, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("online store")) + }) + + It("should reject scaling when online store uses file persistence", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-file-online", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ + Path: dataOnlineDbPath, + }, + }, + }, + Registry: dbRegistry, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("online store")) + }) + + It("should reject scaling when offline store uses file persistence", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-file-offline", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ + Type: "duckdb", + }, + }, + }, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("offline store")) + }) + + It("should reject scaling when no registry is configured (implicit file default)", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-no-registry", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("registry")) + }) + + It("should reject scaling when registry uses file persistence", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-file-registry", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ + Path: dataRegistryDbPath, + }, + }, + }, + }, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("registry")) + }) + + It("should accept scaling with S3-backed registry", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-s3-registry", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ + Path: "s3://my-bucket/registry.db", + }, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + + It("should accept scaling with GS-backed registry", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-gs-registry", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ + Path: "gs://my-bucket/registry.db", + }, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + + It("should accept scaling with remote registry", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-remote-reg", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: &feastdevv1.Registry{ + Remote: &feastdevv1.RemoteRegistryConfig{ + Hostname: ptr.To("registry.example.com:80"), + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + + It("should accept file persistence when replicas is 1", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-rep1-file", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(1)), + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + + It("should accept file persistence when no scaling is configured", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-no-scaling", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + + It("should reject autoscaling without DB online store", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-hpa-no-db", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Services: &feastdevv1.FeatureStoreServices{ + Scaling: &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{MaxReplicas: 5}, + }, + Registry: dbRegistry, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("online store")) + }) + + It("should reject scaling when online store has no persistence configured", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-online-nop", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{}, + Registry: dbRegistry, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("online store")) + }) + + It("should reject replicas and autoscaling set simultaneously", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-mutual-excl", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + Scaling: &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{MaxReplicas: 5}, + }, + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("mutually exclusive")) + }) + }) + + Describe("getDesiredReplicas", func() { + It("should return 1 when no explicit replicas are configured (default)", func() { + replicas := feast.getDesiredReplicas() + Expect(replicas).NotTo(BeNil()) + Expect(*replicas).To(Equal(int32(1))) + }) + + It("should return static replicas when configured", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + replicas := feast.getDesiredReplicas() + Expect(replicas).NotTo(BeNil()) + Expect(*replicas).To(Equal(int32(3))) + }) + + It("should return nil when autoscaling is configured (HPA manages replicas)", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{ + MaxReplicas: 5, + }, + } + Expect(feast.getDesiredReplicas()).To(BeNil()) + }) + }) + + Describe("Deployment Strategy", func() { + It("should default to Recreate when no scaling is configured", func() { + Expect(feast.ApplyDefaults()).To(Succeed()) + strategy := feast.getDeploymentStrategy() + Expect(strategy.Type).To(Equal(appsv1.RecreateDeploymentStrategyType)) + }) + + It("should default to RollingUpdate when scaling is enabled via replicas", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + strategy := feast.getDeploymentStrategy() + Expect(strategy.Type).To(Equal(appsv1.RollingUpdateDeploymentStrategyType)) + }) + + It("should respect user-defined strategy even with scaling", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + featureStore.Status.Applied.Services.DeploymentStrategy = &appsv1.DeploymentStrategy{ + Type: appsv1.RecreateDeploymentStrategyType, + } + strategy := feast.getDeploymentStrategy() + Expect(strategy.Type).To(Equal(appsv1.RecreateDeploymentStrategyType)) + }) + }) + + Describe("setDeployment with scaling", func() { + setFilePersistence := func() { + featureStore.Status.Applied.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + Persistence: &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ + Path: "/feast-data/online.db", + }, + }, + } + featureStore.Status.Applied.Services.Registry = &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + GRPC: ptr.To(true), + }, + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ + Path: "/feast-data/registry.db", + }, + }, + }, + } + } + + It("should set static replicas on the deployment", func() { + setFilePersistence() + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + deployment := feast.initFeastDeploy() + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Replicas).NotTo(BeNil()) + Expect(*deployment.Spec.Replicas).To(Equal(int32(3))) + }) + + It("should preserve existing replicas when autoscaling is configured", func() { + setFilePersistence() + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{ + MaxReplicas: 5, + }, + } + + deployment := feast.initFeastDeploy() + existing := int32(4) + deployment.Spec.Replicas = &existing + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Replicas).NotTo(BeNil()) + Expect(*deployment.Spec.Replicas).To(Equal(int32(4))) + }) + + It("should set default replicas=1 when no explicit scaling is configured", func() { + setFilePersistence() + Expect(k8sClient.Status().Update(ctx, featureStore)).To(Succeed()) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Replicas).NotTo(BeNil()) + Expect(*deployment.Spec.Replicas).To(Equal(int32(1))) + }) + }) + + Describe("HPA Configuration", func() { + It("should build an HPA apply config with default CPU metrics", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{ + MaxReplicas: 10, + }, + } + + hpa := feast.buildHPAApplyConfig() + Expect(*hpa.Spec.MaxReplicas).To(Equal(int32(10))) + Expect(*hpa.Spec.MinReplicas).To(Equal(int32(1))) + Expect(hpa.Spec.Metrics).To(HaveLen(1)) + Expect(*hpa.Spec.Metrics[0].Resource.Name).To(Equal(corev1.ResourceCPU)) + }) + + It("should build an HPA apply config with custom min replicas", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{ + MinReplicas: ptr.To(int32(2)), + MaxReplicas: 10, + }, + } + + hpa := feast.buildHPAApplyConfig() + Expect(*hpa.Spec.MinReplicas).To(Equal(int32(2))) + Expect(*hpa.Spec.MaxReplicas).To(Equal(int32(10))) + }) + + It("should set correct scale target reference", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{ + MaxReplicas: 5, + }, + } + + hpa := feast.buildHPAApplyConfig() + Expect(*hpa.Spec.ScaleTargetRef.APIVersion).To(Equal("apps/v1")) + Expect(*hpa.Spec.ScaleTargetRef.Kind).To(Equal("Deployment")) + Expect(*hpa.Spec.ScaleTargetRef.Name).To(Equal(GetFeastName(featureStore))) + }) + + It("should set TypeMeta and owner reference for SSA", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{ + MaxReplicas: 5, + }, + } + + hpa := feast.buildHPAApplyConfig() + Expect(*hpa.Kind).To(Equal("HorizontalPodAutoscaler")) + Expect(*hpa.APIVersion).To(Equal("autoscaling/v2")) + Expect(hpa.OwnerReferences).To(HaveLen(1)) + Expect(*hpa.OwnerReferences[0].Name).To(Equal(featureStore.Name)) + Expect(*hpa.OwnerReferences[0].Controller).To(BeTrue()) + }) + + It("should convert custom metrics via JSON round-trip", func() { + customMetrics := []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceMemory, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: ptr.To(int32(75)), + }, + }, + }, + } + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{ + MaxReplicas: 10, + Metrics: customMetrics, + }, + } + + hpa := feast.buildHPAApplyConfig() + Expect(hpa.Spec.Metrics).To(HaveLen(1)) + Expect(*hpa.Spec.Metrics[0].Resource.Name).To(Equal(corev1.ResourceMemory)) + Expect(*hpa.Spec.Metrics[0].Resource.Target.AverageUtilization).To(Equal(int32(75))) + }) + }) + + Describe("PDB Configuration", func() { + It("should build a PDB apply config with maxUnavailable", func() { + maxUnavail := intstr.FromInt(1) + featureStore.Status.Applied.Services.PodDisruptionBudgets = &feastdevv1.PDBConfig{ + MaxUnavailable: &maxUnavail, + } + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + pdb := feast.buildPDBApplyConfig() + Expect(*pdb.Kind).To(Equal("PodDisruptionBudget")) + Expect(*pdb.APIVersion).To(Equal("policy/v1")) + Expect(pdb.Spec.MaxUnavailable).NotTo(BeNil()) + Expect(pdb.Spec.MaxUnavailable.IntValue()).To(Equal(1)) + Expect(pdb.Spec.MinAvailable).To(BeNil()) + Expect(pdb.Spec.Selector.MatchLabels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + }) + + It("should build a PDB apply config with minAvailable", func() { + minAvail := intstr.FromString("50%") + featureStore.Status.Applied.Services.PodDisruptionBudgets = &feastdevv1.PDBConfig{ + MinAvailable: &minAvail, + } + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + pdb := feast.buildPDBApplyConfig() + Expect(pdb.Spec.MinAvailable).NotTo(BeNil()) + Expect(pdb.Spec.MinAvailable.String()).To(Equal("50%")) + Expect(pdb.Spec.MaxUnavailable).To(BeNil()) + }) + + It("should set owner reference on PDB for SSA", func() { + maxUnavail := intstr.FromInt(1) + featureStore.Status.Applied.Services.PodDisruptionBudgets = &feastdevv1.PDBConfig{ + MaxUnavailable: &maxUnavail, + } + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + pdb := feast.buildPDBApplyConfig() + Expect(pdb.OwnerReferences).To(HaveLen(1)) + Expect(*pdb.OwnerReferences[0].Name).To(Equal(featureStore.Name)) + Expect(*pdb.OwnerReferences[0].Controller).To(BeTrue()) + }) + }) + + Describe("CEL admission validation rejects invalid PDB configurations", func() { + dbOnlineStore := &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ + Type: redisType, + SecretRef: corev1.LocalObjectReference{Name: redisSecretName}, + }, + }, + } + dbRegistry := &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + DBPersistence: &feastdevv1.RegistryDBStorePersistence{ + Type: "sql", + SecretRef: corev1.LocalObjectReference{Name: registrySecretName}, + }, + }, + }, + } + + It("should reject PDB with both minAvailable and maxUnavailable set", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-pdb-both", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + PodDisruptionBudgets: &feastdevv1.PDBConfig{ + MinAvailable: ptr.To(intstr.FromInt(1)), + MaxUnavailable: ptr.To(intstr.FromInt(1)), + }, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Exactly one of minAvailable or maxUnavailable")) + }) + + It("should reject PDB with neither minAvailable nor maxUnavailable set", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-pdb-none", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + PodDisruptionBudgets: &feastdevv1.PDBConfig{}, + }, + }, + } + err := k8sClient.Create(ctx, fs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Exactly one of minAvailable or maxUnavailable")) + }) + + It("should accept PDB with only maxUnavailable", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-pdb-maxu", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + PodDisruptionBudgets: &feastdevv1.PDBConfig{ + MaxUnavailable: ptr.To(intstr.FromInt(1)), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + + It("should accept PDB with only minAvailable", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: "cel-pdb-mina", Namespace: "default"}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: celTestProject, + Replicas: ptr.To(int32(3)), + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: dbOnlineStore, + Registry: dbRegistry, + PodDisruptionBudgets: &feastdevv1.PDBConfig{ + MinAvailable: ptr.To(intstr.FromString("50%")), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) + }) + }) + + Describe("Topology Spread", func() { + It("should auto-inject soft zone constraint when replicas > 1 and no explicit constraints", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(HaveLen(1)) + Expect(podSpec.TopologySpreadConstraints[0].TopologyKey).To(Equal("topology.kubernetes.io/zone")) + Expect(podSpec.TopologySpreadConstraints[0].WhenUnsatisfiable).To(Equal(corev1.ScheduleAnyway)) + Expect(podSpec.TopologySpreadConstraints[0].MaxSkew).To(Equal(int32(1))) + Expect(podSpec.TopologySpreadConstraints[0].LabelSelector.MatchLabels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + }) + + It("should auto-inject when autoscaling is configured", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{MaxReplicas: 5}, + } + + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(HaveLen(1)) + Expect(podSpec.TopologySpreadConstraints[0].WhenUnsatisfiable).To(Equal(corev1.ScheduleAnyway)) + }) + + It("should not inject when replicas is 1 and no autoscaling", func() { + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(BeEmpty()) + }) + + It("should use user-provided constraints instead of defaults", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + featureStore.Status.Applied.Services.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{{ + MaxSkew: 2, + TopologyKey: kubernetesHostnameTopologyKey, + WhenUnsatisfiable: corev1.DoNotSchedule, + LabelSelector: metav1.SetAsLabelSelector(map[string]string{"custom": "label"}), + }} + + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(HaveLen(1)) + Expect(podSpec.TopologySpreadConstraints[0].TopologyKey).To(Equal("kubernetes.io/hostname")) + Expect(podSpec.TopologySpreadConstraints[0].WhenUnsatisfiable).To(Equal(corev1.DoNotSchedule)) + Expect(podSpec.TopologySpreadConstraints[0].MaxSkew).To(Equal(int32(2))) + }) + + It("should disable auto-injection when empty array is set", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + featureStore.Status.Applied.Services.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{} + + podSpec := &corev1.PodSpec{} + feast.applyTopologySpread(podSpec) + + Expect(podSpec.TopologySpreadConstraints).To(BeEmpty()) + }) + }) + + Describe("Pod Anti-Affinity", func() { + It("should auto-inject soft node anti-affinity when replicas > 1", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity).NotTo(BeNil()) + Expect(podSpec.Affinity.PodAntiAffinity).NotTo(BeNil()) + terms := podSpec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution + Expect(terms).To(HaveLen(1)) + Expect(terms[0].Weight).To(Equal(int32(100))) + Expect(terms[0].PodAffinityTerm.TopologyKey).To(Equal("kubernetes.io/hostname")) + Expect(terms[0].PodAffinityTerm.LabelSelector.MatchLabels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + }) + + It("should auto-inject when autoscaling is configured", func() { + featureStore.Status.Applied.Services.Scaling = &feastdevv1.ScalingConfig{ + Autoscaling: &feastdevv1.AutoscalingConfig{MaxReplicas: 5}, + } + + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity).NotTo(BeNil()) + Expect(podSpec.Affinity.PodAntiAffinity).NotTo(BeNil()) + }) + + It("should not inject when replicas is 1 and no autoscaling", func() { + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity).To(BeNil()) + }) + + It("should use user-provided affinity instead of defaults", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + featureStore.Status.Applied.Services.Affinity = &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{{ + TopologyKey: "kubernetes.io/hostname", + LabelSelector: metav1.SetAsLabelSelector(map[string]string{"custom": "label"}), + }}, + }, + } + + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution).To(HaveLen(1)) + Expect(podSpec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution).To(BeEmpty()) + Expect(podSpec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution[0].TopologyKey).To(Equal("kubernetes.io/hostname")) + }) + + It("should allow user to set node affinity alongside anti-affinity", func() { + featureStore.Status.Applied.Replicas = ptr.To(int32(3)) + featureStore.Status.Applied.Services.Affinity = &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{{ + MatchExpressions: []corev1.NodeSelectorRequirement{{ + Key: "gpu", + Operator: corev1.NodeSelectorOpIn, + Values: []string{stringTrue}, + }}, + }}, + }, + }, + } + + podSpec := &corev1.PodSpec{} + feast.applyAffinity(podSpec) + + Expect(podSpec.Affinity.NodeAffinity).NotTo(BeNil()) + Expect(podSpec.Affinity.PodAntiAffinity).To(BeNil()) + }) + }) + + Describe("Scale sub-resource", func() { + newDBFeatureStore := func(name string) *feastdevv1.FeatureStore { + return &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: DefaultNs, + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "scaletest", + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ + Type: "redis", + SecretRef: corev1.LocalObjectReference{Name: "redis-secret"}, + }, + }, + }, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + DBPersistence: &feastdevv1.RegistryDBStorePersistence{ + Type: "sql", + SecretRef: corev1.LocalObjectReference{Name: registrySecretName}, + }, + }, + }, + }, + }, + }, + } + } + + It("should allow scaling up via the scale sub-resource with DB persistence", func() { + fs := newDBFeatureStore("scale-sub-valid") + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + defer func() { Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) }() + + scale := &autoscalingv1.Scale{} + Expect(k8sClient.SubResource("scale").Get(ctx, fs, scale)).To(Succeed()) + Expect(scale.Spec.Replicas).To(Equal(int32(1))) + + scale.Spec.Replicas = 3 + Expect(k8sClient.SubResource("scale").Update(ctx, fs, client.WithSubResourceBody(scale))).To(Succeed()) + + updated := &feastdevv1.FeatureStore{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: fs.Name, Namespace: fs.Namespace}, updated)).To(Succeed()) + Expect(updated.Spec.Replicas).NotTo(BeNil()) + Expect(*updated.Spec.Replicas).To(Equal(int32(3))) + }) + + It("should reject scaling up via the scale sub-resource without DB persistence", func() { + fs := &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scale-sub-reject", + Namespace: DefaultNs, + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "scaletest", + }, + } + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + defer func() { Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) }() + + scale := &autoscalingv1.Scale{} + Expect(k8sClient.SubResource("scale").Get(ctx, fs, scale)).To(Succeed()) + + scale.Spec.Replicas = 3 + err := k8sClient.SubResource("scale").Update(ctx, fs, client.WithSubResourceBody(scale)) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("online store")) + }) + + It("should read the status replicas from the scale sub-resource", func() { + fs := newDBFeatureStore("scale-sub-status") + fs.Spec.Replicas = ptr.To(int32(2)) + Expect(k8sClient.Create(ctx, fs)).To(Succeed()) + defer func() { Expect(k8sClient.Delete(ctx, fs)).To(Succeed()) }() + + fs.Status.Replicas = 2 + fs.Status.Applied.FeastProject = fs.Spec.FeastProject + Expect(k8sClient.Status().Update(ctx, fs)).To(Succeed()) + + scale := &autoscalingv1.Scale{} + Expect(k8sClient.SubResource("scale").Get(ctx, fs, scale)).To(Succeed()) + Expect(scale.Status.Replicas).To(Equal(int32(2))) + Expect(scale.Spec.Replicas).To(Equal(int32(2))) + }) + }) +}) diff --git a/infra/feast-operator/internal/controller/services/service_monitor.go b/infra/feast-operator/internal/controller/services/service_monitor.go new file mode 100644 index 00000000000..8de4d131289 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/service_monitor.go @@ -0,0 +1,117 @@ +/* +Copyright 2026 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "encoding/json" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + monitoringv1apply "github.com/prometheus-operator/prometheus-operator/pkg/client/applyconfiguration/monitoring/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + metav1apply "k8s.io/client-go/applyconfigurations/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var serviceMonitorGVK = schema.GroupVersionKind{ + Group: "monitoring.coreos.com", + Version: "v1", + Kind: "ServiceMonitor", +} + +// createOrDeleteServiceMonitor reconciles the ServiceMonitor for the +// FeatureStore's online store metrics endpoint using Server-Side Apply. +// When the Prometheus Operator CRD is not present in the cluster, this is +// a no-op. When metrics are enabled on the online store, a ServiceMonitor +// is applied; otherwise any existing ServiceMonitor is deleted. +func (feast *FeastServices) createOrDeleteServiceMonitor() error { + if !hasServiceMonitorCRD { + return nil + } + + if feast.isOnlineStore() && feast.isMetricsEnabled(OnlineFeastType) { + return feast.applyServiceMonitor() + } + + return feast.deleteServiceMonitor() +} + +func (feast *FeastServices) applyServiceMonitor() error { + smApply := feast.buildServiceMonitorApplyConfig() + data, err := json.Marshal(smApply) + if err != nil { + return err + } + + sm := feast.initServiceMonitor() + logger := log.FromContext(feast.Handler.Context) + if err := feast.Handler.Client.Patch(feast.Handler.Context, sm, + client.RawPatch(types.ApplyPatchType, data), + client.FieldOwner(fieldManager), client.ForceOwnership); err != nil { + return err + } + logger.Info("Successfully applied", "ServiceMonitor", sm.GetName()) + + return nil +} + +func (feast *FeastServices) deleteServiceMonitor() error { + sm := feast.initServiceMonitor() + return feast.Handler.DeleteOwnedFeastObj(sm) +} + +func (feast *FeastServices) initServiceMonitor() *unstructured.Unstructured { + sm := &unstructured.Unstructured{} + sm.SetGroupVersionKind(serviceMonitorGVK) + sm.SetName(feast.GetFeastServiceName(OnlineFeastType)) + sm.SetNamespace(feast.Handler.FeatureStore.Namespace) + return sm +} + +// buildServiceMonitorApplyConfig constructs the fully desired ServiceMonitor +// state for Server-Side Apply. +func (feast *FeastServices) buildServiceMonitorApplyConfig() *monitoringv1apply.ServiceMonitorApplyConfiguration { + cr := feast.Handler.FeatureStore + objMeta := feast.GetObjectMetaType(OnlineFeastType) + + return monitoringv1apply.ServiceMonitor(objMeta.Name, objMeta.Namespace). + WithLabels(feast.getFeastTypeLabels(OnlineFeastType)). + WithOwnerReferences( + metav1apply.OwnerReference(). + WithAPIVersion(feastdevv1.GroupVersion.String()). + WithKind("FeatureStore"). + WithName(cr.Name). + WithUID(cr.UID). + WithController(true). + WithBlockOwnerDeletion(true), + ). + WithSpec(monitoringv1apply.ServiceMonitorSpec(). + WithEndpoints( + monitoringv1apply.Endpoint(). + WithPort("metrics"). + WithPath("/metrics"), + ). + WithSelector(metav1apply.LabelSelector(). + WithMatchLabels(map[string]string{ + NameLabelKey: cr.Name, + ServiceTypeLabelKey: string(OnlineFeastType), + }), + ), + ) +} diff --git a/infra/feast-operator/internal/controller/services/service_monitor_test.go b/infra/feast-operator/internal/controller/services/service_monitor_test.go new file mode 100644 index 00000000000..8d982e6953e --- /dev/null +++ b/infra/feast-operator/internal/controller/services/service_monitor_test.go @@ -0,0 +1,168 @@ +/* +Copyright 2026 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "context" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" +) + +var _ = Describe("ServiceMonitor", func() { + var ( + featureStore *feastdevv1.FeatureStore + feast *FeastServices + typeNamespacedName types.NamespacedName + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + typeNamespacedName = types.NamespacedName{ + Name: "sm-test-fs", + Namespace: DefaultNs, + } + + featureStore = &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: typeNamespacedName.Name, + Namespace: typeNamespacedName.Namespace, + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "smtestproject", + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + }, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + GRPC: ptr.To(true), + }, + }, + }, + }, + }, + } + + Expect(k8sClient.Create(ctx, featureStore)).To(Succeed()) + + feast = &FeastServices{ + Handler: handler.FeastHandler{ + Client: k8sClient, + Context: ctx, + Scheme: k8sClient.Scheme(), + FeatureStore: featureStore, + }, + } + + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + }) + + AfterEach(func() { + testSetHasServiceMonitorCRD(false) + Expect(k8sClient.Delete(ctx, featureStore)).To(Succeed()) + }) + + Describe("initServiceMonitor", func() { + It("should create an unstructured ServiceMonitor with correct GVK and name", func() { + sm := feast.initServiceMonitor() + Expect(sm).NotTo(BeNil()) + Expect(sm.GetKind()).To(Equal("ServiceMonitor")) + Expect(sm.GetAPIVersion()).To(Equal("monitoring.coreos.com/v1")) + Expect(sm.GetName()).To(Equal(feast.GetFeastServiceName(OnlineFeastType))) + Expect(sm.GetNamespace()).To(Equal(featureStore.Namespace)) + }) + }) + + Describe("buildServiceMonitorApplyConfig", func() { + It("should build the correct SSA payload with labels, endpoints, selector, and owner reference", func() { + sm := feast.buildServiceMonitorApplyConfig() + + Expect(*sm.APIVersion).To(Equal("monitoring.coreos.com/v1")) + Expect(*sm.Kind).To(Equal("ServiceMonitor")) + Expect(*sm.Name).To(Equal(feast.GetFeastServiceName(OnlineFeastType))) + Expect(*sm.Namespace).To(Equal(featureStore.Namespace)) + + Expect(sm.Labels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + Expect(sm.Labels).To(HaveKeyWithValue(ServiceTypeLabelKey, string(OnlineFeastType))) + + Expect(sm.OwnerReferences).To(HaveLen(1)) + ownerRef := sm.OwnerReferences[0] + Expect(*ownerRef.APIVersion).To(Equal(feastdevv1.GroupVersion.String())) + Expect(*ownerRef.Kind).To(Equal("FeatureStore")) + Expect(*ownerRef.Name).To(Equal(featureStore.Name)) + Expect(*ownerRef.Controller).To(BeTrue()) + Expect(*ownerRef.BlockOwnerDeletion).To(BeTrue()) + + Expect(sm.Spec).NotTo(BeNil()) + Expect(sm.Spec.Endpoints).To(HaveLen(1)) + Expect(*sm.Spec.Endpoints[0].Port).To(Equal("metrics")) + Expect(*sm.Spec.Endpoints[0].Path).To(Equal("/metrics")) + + Expect(sm.Spec.Selector).NotTo(BeNil()) + Expect(sm.Spec.Selector.MatchLabels).To(HaveKeyWithValue(NameLabelKey, featureStore.Name)) + Expect(sm.Spec.Selector.MatchLabels).To(HaveKeyWithValue(ServiceTypeLabelKey, string(OnlineFeastType))) + }) + }) + + Describe("createOrDeleteServiceMonitor", func() { + It("should be a no-op when ServiceMonitor CRD is not available", func() { + testSetHasServiceMonitorCRD(false) + Expect(feast.createOrDeleteServiceMonitor()).To(Succeed()) + }) + + It("should not error when metrics is not enabled and CRD is unavailable", func() { + testSetHasServiceMonitorCRD(false) + featureStore.Status.Applied.Services.OnlineStore.Server.Metrics = ptr.To(false) + Expect(feast.createOrDeleteServiceMonitor()).To(Succeed()) + }) + }) + + Describe("HasServiceMonitorCRD", func() { + It("should return false by default", func() { + testSetHasServiceMonitorCRD(false) + Expect(HasServiceMonitorCRD()).To(BeFalse()) + }) + + It("should return true when set", func() { + testSetHasServiceMonitorCRD(true) + Expect(HasServiceMonitorCRD()).To(BeTrue()) + }) + }) +}) diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index 3b14f9c49db..6964938c093 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -18,10 +18,11 @@ package services import ( "errors" + "path" "strconv" "strings" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" routev1 "github.com/openshift/api/route/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" @@ -32,6 +33,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" @@ -71,14 +73,51 @@ func (feast *FeastServices) Deploy() error { _ = feast.Handler.DeleteOwnedFeastObj(feast.initCaConfigMap()) } + if err := feast.reconcileServices(); err != nil { + return err + } + + if err := feast.createServiceAccount(); err != nil { + return err + } + if err := feast.reconcileBatchEngineRBAC(); err != nil { + return err + } + if err := feast.createDeployment(); err != nil { + return err + } + if err := feast.createOrDeleteHPA(); err != nil { + return err + } + if err := feast.applyOrDeletePDB(); err != nil { + return err + } + if err := feast.deployClient(); err != nil { + return err + } + if err := feast.deployNamespaceRegistry(); err != nil { + return err + } + if err := feast.deployCronJob(); err != nil { + return err + } + if err := feast.createOrDeleteServiceMonitor(); err != nil { + return err + } + + return nil +} + +// reconcileServices validates persistence and deploys or removes each feast +// service type based on the applied spec. +func (feast *FeastServices) reconcileServices() error { services := feast.Handler.FeatureStore.Status.Applied.Services + if feast.isOfflineStore() { - err := feast.validateOfflineStorePersistence(services.OfflineStore.Persistence) - if err != nil { + if err := feast.validateOfflineStorePersistence(services.OfflineStore.Persistence); err != nil { return err } - - if err = feast.deployFeastServiceByType(OfflineFeastType); err != nil { + if err := feast.deployFeastServiceByType(OfflineFeastType); err != nil { return err } } else { @@ -88,12 +127,10 @@ func (feast *FeastServices) Deploy() error { } if feast.isOnlineStore() { - err := feast.validateOnlineStorePersistence(services.OnlineStore.Persistence) - if err != nil { + if err := feast.validateOnlineStorePersistence(services.OnlineStore.Persistence); err != nil { return err } - - if err = feast.deployFeastServiceByType(OnlineFeastType); err != nil { + if err := feast.deployFeastServiceByType(OnlineFeastType); err != nil { return err } } else { @@ -103,12 +140,10 @@ func (feast *FeastServices) Deploy() error { } if feast.isLocalRegistry() { - err := feast.validateRegistryPersistence(services.Registry.Local.Persistence) - if err != nil { + if err := feast.validateRegistryPersistence(services.Registry.Local.Persistence); err != nil { return err } - - if err = feast.deployFeastServiceByType(RegistryFeastType); err != nil { + if err := feast.deployFeastServiceByType(RegistryFeastType); err != nil { return err } } else { @@ -116,11 +151,12 @@ func (feast *FeastServices) Deploy() error { return err } } + if feast.isUiServer() { - if err = feast.deployFeastServiceByType(UIFeastType); err != nil { + if err := feast.deployFeastServiceByType(UIFeastType); err != nil { return err } - if err = feast.createRoute(UIFeastType); err != nil { + if err := feast.createRoute(UIFeastType); err != nil { return err } } else { @@ -132,26 +168,10 @@ func (feast *FeastServices) Deploy() error { } } - if err := feast.createServiceAccount(); err != nil { - return err - } - if err := feast.createDeployment(); err != nil { - return err - } - if err := feast.deployClient(); err != nil { - return err - } - if err := feast.deployNamespaceRegistry(); err != nil { - return err - } - if err := feast.deployCronJob(); err != nil { - return err - } - return nil } -func (feast *FeastServices) validateRegistryPersistence(registryPersistence *feastdevv1alpha1.RegistryPersistence) error { +func (feast *FeastServices) validateRegistryPersistence(registryPersistence *feastdevv1.RegistryPersistence) error { if registryPersistence != nil { dbPersistence := registryPersistence.DBPersistence @@ -172,7 +192,7 @@ func (feast *FeastServices) validateRegistryPersistence(registryPersistence *fea return nil } -func (feast *FeastServices) validateOnlineStorePersistence(onlinePersistence *feastdevv1alpha1.OnlineStorePersistence) error { +func (feast *FeastServices) validateOnlineStorePersistence(onlinePersistence *feastdevv1.OnlineStorePersistence) error { if onlinePersistence != nil { dbPersistence := onlinePersistence.DBPersistence @@ -193,7 +213,7 @@ func (feast *FeastServices) validateOnlineStorePersistence(onlinePersistence *fe return nil } -func (feast *FeastServices) validateOfflineStorePersistence(offlinePersistence *feastdevv1alpha1.OfflineStorePersistence) error { +func (feast *FeastServices) validateOfflineStorePersistence(offlinePersistence *feastdevv1.OfflineStorePersistence) error { if offlinePersistence != nil { filePersistence := offlinePersistence.FilePersistence dbPersistence := offlinePersistence.DBPersistence @@ -338,6 +358,8 @@ func (feast *FeastServices) createDeployment() error { logger.Info("Successfully reconciled", "Deployment", deploy.Name, "operation", op) } + feast.updateScalingStatus(deploy) + return nil } @@ -359,7 +381,7 @@ func (feast *FeastServices) createRoute(feastType FeastServiceType) error { return nil } -func (feast *FeastServices) createPVC(pvcCreate *feastdevv1alpha1.PvcCreate, feastType FeastServiceType) error { +func (feast *FeastServices) createPVC(pvcCreate *feastdevv1.PvcCreate, feastType FeastServiceType) error { logger := log.FromContext(feast.Handler.Context) pvc, err := feast.createNewPVC(pvcCreate, feastType) if err != nil { @@ -367,10 +389,13 @@ func (feast *FeastServices) createPVC(pvcCreate *feastdevv1alpha1.PvcCreate, fea } // PVCs are immutable, so we only create... we don't update an existing one. + // Treat AlreadyExists as success: a pre-existing PVC without the managed-by label + // won't appear in the filtered cache (Client.Get returns NotFound), but Create + // will hit AlreadyExists on the API server — both cases mean the PVC is present. err = feast.Handler.Client.Get(feast.Handler.Context, client.ObjectKeyFromObject(pvc), pvc) if err != nil && apierrors.IsNotFound(err) { err = feast.Handler.Client.Create(feast.Handler.Context, pvc) - if err != nil { + if err != nil && !apierrors.IsAlreadyExists(err) { return err } logger.Info("Successfully created", "PersistentVolumeClaim", pvc.Name) @@ -381,16 +406,25 @@ func (feast *FeastServices) createPVC(pvcCreate *feastdevv1alpha1.PvcCreate, fea func (feast *FeastServices) setDeployment(deploy *appsv1.Deployment) error { cr := feast.Handler.FeatureStore + + // Determine replica count: + // - spec.replicas is set on the Deployment (defaults to 1) + // - When HPA is configured, replicas is left unset so the HPA controller manages it replicas := deploy.Spec.Replicas + if desired := feast.getDesiredReplicas(); desired != nil { + replicas = desired + } deploy.Labels = feast.getLabels() + selectorLabels := feast.getSelectorLabels() deploy.Spec = appsv1.DeploymentSpec{ Replicas: replicas, - Selector: metav1.SetAsLabelSelector(deploy.GetLabels()), + Selector: metav1.SetAsLabelSelector(selectorLabels), Strategy: feast.getDeploymentStrategy(), Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: deploy.GetLabels(), + Labels: deploy.GetLabels(), + Annotations: cr.Status.Applied.Services.PodAnnotations, }, Spec: corev1.PodSpec{ ServiceAccountName: feast.initFeastSA().Name, @@ -413,11 +447,18 @@ func (feast *FeastServices) setPod(podSpec *corev1.PodSpec) error { feast.mountEmptyDirVolumes(podSpec) feast.mountUserDefinedVolumes(podSpec) feast.applyNodeSelector(podSpec) + feast.applyTopologySpread(podSpec) + feast.applyAffinity(podSpec) + feast.applyResourceClaims(podSpec) return nil } func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec) error { + if err := feast.validatePackagedFeatureRepoPath(); err != nil { + return err + } + fsYamlB64, err := feast.GetServiceFeatureStoreYamlBase64() if err != nil { return err @@ -436,6 +477,20 @@ func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec) error { if feast.isUiServer() { feast.setContainer(&podSpec.Containers, UIFeastType, fsYamlB64) } + + // When the CR is annotated as a protected project, set FEAST_PROTECTED_PROJECT=true + // so the registry server tags its own project in the shared registry. + // Other FeatureStore instances then exclude this project automatically. + if feast.isProtectedProject() { + protectedEnv := corev1.EnvVar{ + Name: "FEAST_PROTECTED_PROJECT", + Value: "true", + } + for i := range podSpec.Containers { + podSpec.Containers[i].Env = append(podSpec.Containers[i].Env, protectedEnv) + } + } + return nil } @@ -470,6 +525,13 @@ func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastTy ContainerPort: getTargetPort(feastType, tls), Protocol: corev1.ProtocolTCP, }) + if feastType == OnlineFeastType && feast.isMetricsEnabled(feastType) { + container.Ports = append(container.Ports, corev1.ContainerPort{ + Name: metricsPortName, + ContainerPort: MetricsPort, + Protocol: corev1.ProtocolTCP, + }) + } } container.StartupProbe = &corev1.Probe{ @@ -494,7 +556,7 @@ func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastTy } } -func getContainer(name, workingDir string, cmd []string, containerConfigs feastdevv1alpha1.ContainerConfigs, fsYamlB64 string) *corev1.Container { +func getContainer(name, workingDir string, cmd []string, containerConfigs feastdevv1.ContainerConfigs, fsYamlB64 string) *corev1.Container { container := &corev1.Container{ Name: name, Command: cmd, @@ -557,7 +619,7 @@ func (feast *FeastServices) setRoute(route *routev1.Route, feastType FeastServic } func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []string { - baseCommand := "feast" + baseCommand := feastCommand options := []string{} logLevel := feast.getLogLevelForType(feastType) if logLevel != nil { @@ -565,6 +627,13 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st } deploySettings := FeastServiceConstants[feastType] + deploySettings.Args = append([]string{}, deploySettings.Args...) + // Only inject --metrics CLI flag for the server.metrics bool path. + // When serving.metrics.enabled is used, Python reads it from feature_store.yaml + // and starts the metrics server itself — no CLI flag needed. + if feastType == OnlineFeastType && feast.isMetricsEnabledViaCLI(feastType) { + deploySettings.Args = append([]string{deploySettings.Args[0], "--metrics"}, deploySettings.Args[1:]...) + } targetPort := deploySettings.TargetHttpPort tls := feast.getTlsConfigs(feastType) @@ -579,6 +648,32 @@ func (feast *FeastServices) getContainerCommand(feastType FeastServiceType) []st deploySettings.Args = append(deploySettings.Args, "--rest-port", strconv.Itoa(int(getTargetRestPort(feastType, tls)))) } } + + // Add worker configuration options for online store (feast serve) + if feastType == OnlineFeastType { + workerConfigs := feast.getWorkerConfigs(feastType) + if workerConfigs != nil { + if workerConfigs.Workers != nil { + deploySettings.Args = append(deploySettings.Args, "--workers", strconv.Itoa(int(*workerConfigs.Workers))) + } + if workerConfigs.WorkerConnections != nil { + deploySettings.Args = append(deploySettings.Args, "--worker-connections", strconv.Itoa(int(*workerConfigs.WorkerConnections))) + } + if workerConfigs.MaxRequests != nil { + deploySettings.Args = append(deploySettings.Args, "--max-requests", strconv.Itoa(int(*workerConfigs.MaxRequests))) + } + if workerConfigs.MaxRequestsJitter != nil { + deploySettings.Args = append(deploySettings.Args, "--max-requests-jitter", strconv.Itoa(int(*workerConfigs.MaxRequestsJitter))) + } + if workerConfigs.KeepAliveTimeout != nil { + deploySettings.Args = append(deploySettings.Args, "--keep-alive-timeout", strconv.Itoa(int(*workerConfigs.KeepAliveTimeout))) + } + if workerConfigs.RegistryTTLSeconds != nil { + deploySettings.Args = append(deploySettings.Args, "--registry_ttl_sec", strconv.Itoa(int(*workerConfigs.RegistryTTLSeconds))) + } + } + } + if tls.IsTLS() { targetPort = deploySettings.TargetHttpsPort feastTlsPath := GetTlsPath(feastType) @@ -598,6 +693,11 @@ func (feast *FeastServices) getDeploymentStrategy() appsv1.DeploymentStrategy { if feast.Handler.FeatureStore.Status.Applied.Services.DeploymentStrategy != nil { return *feast.Handler.FeatureStore.Status.Applied.Services.DeploymentStrategy } + if isScalingEnabled(feast.Handler.FeatureStore) { + return appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + } + } return appsv1.DeploymentStrategy{ Type: appsv1.RecreateDeploymentStrategyType, } @@ -609,9 +709,10 @@ func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, fsYamlB64 feastProjectDir := applied.FeastProjectDir workingDir := getOfflineMountPath(feast.Handler.FeatureStore) projectPath := workingDir + "/" + applied.FeastProject + initImage := getInitContainerImage(&applied) container := corev1.Container{ - Name: "feast-init", - Image: getFeatureServerImage(), + Name: feastInitContainerName, + Image: initImage, Env: []corev1.EnvVar{ { Name: TmpFeatureStoreYamlEnvVar, @@ -622,6 +723,7 @@ func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, fsYamlB64 WorkingDir: workingDir, } + featureRepoDir := feast.getFeatureRepoDir() var createCommand string if feastProjectDir.Init != nil { initSlice := []string{"feast", "init"} @@ -651,16 +753,79 @@ func (feast *FeastServices) setInitContainer(podSpec *corev1.PodSpec, fsYamlB64 if feastProjectDir.Git.EnvFrom != nil { container.EnvFrom = *feastProjectDir.Git.EnvFrom } + } else if feastProjectDir.Packaged != nil { + container.Env = append(container.Env, + corev1.EnvVar{ + Name: packagedFeatureRepoEnvVar, + Value: path.Clean(feastProjectDir.Packaged.FeatureRepoPath), + }, + corev1.EnvVar{ + Name: stagedFeatureRepoEnvVar, + Value: featureRepoDir, + }, + ) + container.Args = []string{ + "set -euo pipefail\n" + + "echo \"Staging packaged feast repository...\"\n" + + "if [[ ! -d \"${" + packagedFeatureRepoEnvVar + "}\" ]]; then " + + "echo \"Packaged feature repository not found: ${" + packagedFeatureRepoEnvVar + "}\" >&2; exit 1; fi\n" + + "rm -rf -- \"${" + stagedFeatureRepoEnvVar + "}\"\n" + + "mkdir -p -- \"${" + stagedFeatureRepoEnvVar + "}\"\n" + + "cp -a -- \"${" + packagedFeatureRepoEnvVar + "}/.\" \"${" + stagedFeatureRepoEnvVar + "}/\"\n" + + "printf '%s' \"${" + TmpFeatureStoreYamlEnvVar + "}\" | base64 -d > \"${" + stagedFeatureRepoEnvVar + "}/feature_store.yaml\"\n" + + "echo \"Packaged feast repository staging complete\"\n", + } } - featureRepoDir := feast.getFeatureRepoDir() - container.Args = []string{ - "echo \"Creating feast repository...\"\necho '" + createCommand + "'\n" + - "if [[ ! -d " + featureRepoDir + " ]]; then " + createCommand + "; fi;\n" + - "echo $" + TmpFeatureStoreYamlEnvVar + " | base64 -d \u003e " + featureRepoDir + "/feature_store.yaml;\necho \"Feast repo creation complete\";\n", + if feastProjectDir.Packaged == nil { + container.Args = []string{ + "echo \"Creating feast repository...\"\necho '" + createCommand + "'\n" + + "if [[ ! -d " + featureRepoDir + " ]]; then " + createCommand + "; fi;\n" + + "echo $" + TmpFeatureStoreYamlEnvVar + " | base64 -d \u003e " + featureRepoDir + "/feature_store.yaml;\necho \"Feast repo creation complete\";\n", + } } podSpec.InitContainers = append(podSpec.InitContainers, container) + + if applied.Services.RunFeastApplyOnInit != nil && *applied.Services.RunFeastApplyOnInit { + applyContainer := corev1.Container{ + Name: feastApplyContainerName, + Image: initImage, + Command: []string{feastCommand, "apply"}, + WorkingDir: featureRepoDir, + } + // feast apply needs DB/store connectivity, so inherit env, envFrom + // and volume mounts from all server container configs. + seen := map[string]bool{} + for _, feastType := range []FeastServiceType{RegistryFeastType, OnlineFeastType, OfflineFeastType} { + if serverConfigs := feast.getServerConfigs(feastType); serverConfigs != nil { + if serverConfigs.OptionalCtrConfigs.Env != nil { + applyContainer.Env = envOverride(applyContainer.Env, *serverConfigs.OptionalCtrConfigs.Env) + } + if serverConfigs.OptionalCtrConfigs.EnvFrom != nil { + applyContainer.EnvFrom = append(applyContainer.EnvFrom, *serverConfigs.OptionalCtrConfigs.EnvFrom...) + } + for _, vm := range feast.getVolumeMounts(feastType) { + if !seen[vm.MountPath] { + applyContainer.VolumeMounts = append(applyContainer.VolumeMounts, vm) + seen[vm.MountPath] = true + } + } + } + } + podSpec.InitContainers = append(podSpec.InitContainers, applyContainer) + } + } +} + +// getServiceAppProtocol returns the appProtocol for a Service port. +// The registry gRPC service uses the gRPC protocol, which requires HTTP/2. +// Setting appProtocol allows service meshes (e.g. Istio) and load balancers +// to correctly classify the traffic and avoid downgrading to HTTP/1.1. +func (feast *FeastServices) getServiceAppProtocol(feastType FeastServiceType, isRestService bool) *string { + if feastType == RegistryFeastType && !isRestService && feast.isRegistryGrpcEnabled() { + return ptr.To("grpc") } + return nil } func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServiceType, isRestService bool) error { @@ -676,25 +841,31 @@ func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServi restEnabled := feast.isRegistryRestEnabled() if grpcEnabled && restEnabled { - // Both services enabled: Use gRPC service name as primary, add REST as SAN - grpcSvcName := feast.initFeastSvc(RegistryFeastType).Name - svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = grpcSvcName + tlsNameSuffix - - // Add Subject Alternative Names (SANs) for both services - grpcHostname := grpcSvcName + "." + svc.Namespace + ".svc.cluster.local" - restHostname := feast.GetFeastRestServiceName(RegistryFeastType) + "." + svc.Namespace + ".svc.cluster.local" - svc.Annotations["service.beta.openshift.io/serving-cert-sans"] = grpcHostname + "," + restHostname + // Both services enabled: Only set TLS annotation on gRPC service to ensure + // OpenShift creates certificate with gRPC service name as CN (not REST service name) + // The certificate will include both hostnames as SANs + if !isRestService { + grpcSvcName := feast.initFeastSvc(RegistryFeastType).Name + svc.Annotations[openshiftServingCertSecretAnnotation] = grpcSvcName + tlsNameSuffix // pragma: allowlist secret + + // Add Subject Alternative Names (SANs) for both services + grpcHostname := grpcSvcName + "." + svc.Namespace + ".svc.cluster.local" + restHostname := feast.GetFeastRestServiceName(RegistryFeastType) + "." + svc.Namespace + ".svc.cluster.local" + svc.Annotations[openshiftServingCertSansAnnotation] = grpcHostname + "," + restHostname + } + // REST service should not have the annotation - it will use the same certificate + // from the gRPC service secret (mounted in the pod) } else if grpcEnabled && !restEnabled { // Only gRPC enabled: Use gRPC service name grpcSvcName := feast.initFeastSvc(RegistryFeastType).Name - svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = grpcSvcName + tlsNameSuffix + svc.Annotations[openshiftServingCertSecretAnnotation] = grpcSvcName + tlsNameSuffix // pragma: allowlist secret } else if !grpcEnabled && restEnabled { // Only REST enabled: Use REST service name - svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = svc.Name + tlsNameSuffix + svc.Annotations[openshiftServingCertSecretAnnotation] = svc.Name + tlsNameSuffix // pragma: allowlist secret } } else { // Standard behavior for non-registry services - svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = svc.Name + tlsNameSuffix + svc.Annotations[openshiftServingCertSecretAnnotation] = svc.Name + tlsNameSuffix // pragma: allowlist secret } } @@ -714,18 +885,28 @@ func (feast *FeastServices) setService(svc *corev1.Service, feastType FeastServi } svc.Spec = corev1.ServiceSpec{ - Selector: feast.getLabels(), + Selector: feast.getSelectorLabels(), Type: corev1.ServiceTypeClusterIP, Ports: []corev1.ServicePort{ { - Name: scheme, - Port: port, - Protocol: corev1.ProtocolTCP, - TargetPort: intstr.FromInt(int(targetPort)), + Name: scheme, + Port: port, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(int(targetPort)), + AppProtocol: feast.getServiceAppProtocol(feastType, isRestService), }, }, } + if feastType == OnlineFeastType && feast.isMetricsEnabled(feastType) { + svc.Spec.Ports = append(svc.Spec.Ports, corev1.ServicePort{ + Name: metricsPortName, + Port: MetricsPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(int(MetricsPort)), + }) + } + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, svc, feast.Handler.Scheme) } @@ -753,8 +934,9 @@ func (feast *FeastServices) setServiceAccount(sa *corev1.ServiceAccount) error { return controllerutil.SetControllerReference(feast.Handler.FeatureStore, sa, feast.Handler.Scheme) } -func (feast *FeastServices) createNewPVC(pvcCreate *feastdevv1alpha1.PvcCreate, feastType FeastServiceType) (*corev1.PersistentVolumeClaim, error) { +func (feast *FeastServices) createNewPVC(pvcCreate *feastdevv1.PvcCreate, feastType FeastServiceType) (*corev1.PersistentVolumeClaim, error) { pvc := feast.initPVC(feastType) + pvc.Labels = feast.getFeastTypeLabels(feastType) pvc.Spec = corev1.PersistentVolumeClaimSpec{ AccessModes: pvcCreate.AccessModes, @@ -766,7 +948,7 @@ func (feast *FeastServices) createNewPVC(pvcCreate *feastdevv1alpha1.PvcCreate, return pvc, controllerutil.SetControllerReference(feast.Handler.FeatureStore, pvc, feast.Handler.Scheme) } -func (feast *FeastServices) getServerConfigs(feastType FeastServiceType) *feastdevv1alpha1.ServerConfigs { +func (feast *FeastServices) getServerConfigs(feastType FeastServiceType) *feastdevv1.ServerConfigs { appliedServices := feast.Handler.FeatureStore.Status.Applied.Services switch feastType { case OfflineFeastType: @@ -794,6 +976,53 @@ func (feast *FeastServices) getLogLevelForType(feastType FeastServiceType) *stri return nil } +func (feast *FeastServices) getWorkerConfigs(feastType FeastServiceType) *feastdevv1.WorkerConfigs { + if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil { + return serviceConfigs.WorkerConfigs + } + return nil +} + +// isMetricsEnabledViaCLI returns true only when metrics are enabled via the +// server.metrics bool flag, which requires the --metrics CLI argument to be +// injected into the feast serve command. +func (feast *FeastServices) isMetricsEnabledViaCLI(feastType FeastServiceType) bool { + if feastType != OnlineFeastType { + return false + } + if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil && serviceConfigs.Metrics != nil { + return *serviceConfigs.Metrics + } + return false +} + +func (feast *FeastServices) isMetricsEnabled(feastType FeastServiceType) bool { + if feastType != OnlineFeastType { + return false + } + + // CLI flag path: server.metrics: true → adds --metrics arg + exposes port 8000. + // Only return true immediately; an explicit false must not suppress the YAML path. + if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil && + serviceConfigs.Metrics != nil && *serviceConfigs.Metrics { + return true + } + + // YAML config path: serving.metrics.enabled: true → written into feature_store.yaml; + // Python reads it and starts the metrics server on port 8000 automatically. + // We still need to expose the port and Service so Prometheus can scrape it. + appliedSpec := feast.Handler.FeatureStore.Status.Applied + if appliedSpec.Services != nil && + appliedSpec.Services.OnlineStore != nil && + appliedSpec.Services.OnlineStore.Serving != nil && + appliedSpec.Services.OnlineStore.Serving.Metrics != nil && + appliedSpec.Services.OnlineStore.Serving.Metrics.Enabled { + return true + } + + return false +} + func (feast *FeastServices) getNodeSelectorForType(feastType FeastServiceType) *map[string]string { if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil { return serviceConfigs.ContainerConfigs.OptionalCtrConfigs.NodeSelector @@ -826,6 +1055,61 @@ func (feast *FeastServices) applyNodeSelector(podSpec *corev1.PodSpec) { podSpec.NodeSelector = finalNodeSelector } +func (feast *FeastServices) applyTopologySpread(podSpec *corev1.PodSpec) { + cr := feast.Handler.FeatureStore + services := cr.Status.Applied.Services + + // User-provided explicit constraints take precedence (including empty array to disable) + if services != nil && services.TopologySpreadConstraints != nil { + podSpec.TopologySpreadConstraints = services.TopologySpreadConstraints + return + } + + if !isScalingEnabled(cr) { + return + } + + podSpec.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{{ + MaxSkew: 1, + TopologyKey: "topology.kubernetes.io/zone", + WhenUnsatisfiable: corev1.ScheduleAnyway, + LabelSelector: metav1.SetAsLabelSelector(feast.getSelectorLabels()), + }} +} + +func (feast *FeastServices) applyAffinity(podSpec *corev1.PodSpec) { + cr := feast.Handler.FeatureStore + services := cr.Status.Applied.Services + + if services != nil && services.Affinity != nil { + podSpec.Affinity = services.Affinity + return + } + + if !isScalingEnabled(cr) { + return + } + + podSpec.Affinity = &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{{ + Weight: 100, + PodAffinityTerm: corev1.PodAffinityTerm{ + TopologyKey: "kubernetes.io/hostname", + LabelSelector: metav1.SetAsLabelSelector(feast.getSelectorLabels()), + }, + }}, + }, + } +} + +func (feast *FeastServices) applyResourceClaims(podSpec *corev1.PodSpec) { + services := feast.Handler.FeatureStore.Status.Applied.Services + if services != nil && len(services.ResourceClaims) > 0 { + podSpec.ResourceClaims = services.ResourceClaims + } +} + // mergeNodeSelectors merges existing and operator node selectors // Existing selectors are preserved, operator selectors can override existing keys func (feast *FeastServices) mergeNodeSelectors(existing, operator map[string]string) map[string]string { @@ -866,11 +1150,11 @@ func (feast *FeastServices) GetDeployment() (appsv1.Deployment, error) { } // GetFeastServiceName returns the feast service object name based on service type -func GetFeastServiceName(featureStore *feastdevv1alpha1.FeatureStore, feastType FeastServiceType) string { +func GetFeastServiceName(featureStore *feastdevv1.FeatureStore, feastType FeastServiceType) string { return GetFeastName(featureStore) + "-" + string(feastType) } -func GetFeastName(featureStore *feastdevv1alpha1.FeatureStore) string { +func GetFeastName(featureStore *feastdevv1.FeatureStore) string { return handler.FeastPrefix + featureStore.Name } @@ -880,14 +1164,26 @@ func (feast *FeastServices) getFeastTypeLabels(feastType FeastServiceType) map[s return labels } -func (feast *FeastServices) getLabels() map[string]string { +// getSelectorLabels returns the minimal label set used for immutable selectors +// (Deployment spec.selector, Service spec.selector, TopologySpreadConstraints, PodAffinity). +// This must NOT change after initial resource creation. +func (feast *FeastServices) getSelectorLabels() map[string]string { return map[string]string{ NameLabelKey: feast.Handler.FeatureStore.Name, } } +// getLabels returns the full label set for mutable metadata (ObjectMeta.Labels). +// Includes the managed-by label used by the informer cache filter. +func (feast *FeastServices) getLabels() map[string]string { + return map[string]string{ + NameLabelKey: feast.Handler.FeatureStore.Name, + ManagedByLabelKey: ManagedByLabelValue, + } +} + func (feast *FeastServices) setServiceHostnames() error { - feast.Handler.FeatureStore.Status.ServiceHostnames = feastdevv1alpha1.ServiceHostnames{} + feast.Handler.FeatureStore.Status.ServiceHostnames = feastdevv1.ServiceHostnames{} domain := svcDomain + ":" if feast.isOfflineServer() { objMeta := feast.initFeastSvc(OfflineFeastType) @@ -925,13 +1221,12 @@ func (feast *FeastServices) setFeastServiceCondition(err error, feastType FeastS if err != nil { logger := log.FromContext(feast.Handler.Context) cond := conditionMap[metav1.ConditionFalse] - cond.Message = "Error: " + err.Error() + cond.Message = ErrorMessagePrefix + err.Error() apimeta.SetStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, cond) logger.Error(err, "Error deploying the FeatureStore "+string(ClientFeastType)+" service") return err - } else { - apimeta.SetStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, conditionMap[metav1.ConditionTrue]) } + apimeta.SetStatusCondition(&feast.Handler.FeatureStore.Status.Conditions, conditionMap[metav1.ConditionTrue]) return nil } @@ -946,7 +1241,7 @@ func (feast *FeastServices) setRemoteRegistryURL() error { // referenced/remote registry must use the local registry server option and be in a 'Ready' state. if remoteFeast != nil && remoteFeast.isRegistryServer() && - apimeta.IsStatusConditionTrue(remoteFeast.Handler.FeatureStore.Status.Conditions, feastdevv1alpha1.RegistryReadyType) && + apimeta.IsStatusConditionTrue(remoteFeast.Handler.FeatureStore.Status.Conditions, feastdevv1.RegistryReadyType) && len(remoteFeast.Handler.FeatureStore.Status.ServiceHostnames.Registry) > 0 { // Check if gRPC server is enabled if !remoteFeast.isRegistryGrpcEnabled() { @@ -968,16 +1263,13 @@ func (feast *FeastServices) getRemoteRegistryFeastHandler() (*FeastServices, err if nsName == crNsName { return nil, errors.New("FeatureStore '" + crNsName.Name + "' can't reference itself in `spec.services.registry.remote.feastRef`") } - remoteFeastObj := &feastdevv1alpha1.FeatureStore{} + remoteFeastObj := &feastdevv1.FeatureStore{} if err := feast.Handler.Client.Get(feast.Handler.Context, nsName, remoteFeastObj); err != nil { if apierrors.IsNotFound(err) { return nil, errors.New("Referenced FeatureStore '" + feastRemoteRef.Name + "' was not found") } return nil, err } - if feast.Handler.FeatureStore.Status.Applied.FeastProject != remoteFeastObj.Status.Applied.FeastProject { - return nil, errors.New("FeatureStore '" + remoteFeastObj.Name + "' is using a different feast project than '" + feast.Handler.FeatureStore.Status.Applied.FeastProject + "'. Project names must match.") - } return &FeastServices{ Handler: handler.FeastHandler{ Client: feast.Handler.Client, @@ -1029,7 +1321,7 @@ func (feast *FeastServices) isOnlineServer() bool { func (feast *FeastServices) isOnlineStore() bool { appliedServices := feast.Handler.FeatureStore.Status.Applied.Services - return appliedServices != nil && appliedServices.OnlineStore != nil + return appliedServices != nil && appliedServices.OnlineStore != nil && !appliedServices.OnlineStore.Disabled } func (feast *FeastServices) noLocalCoreServerConfigured() bool { @@ -1093,8 +1385,10 @@ func (feast *FeastServices) initRoute(feastType FeastServiceType) *routev1.Route return route } -func applyCtrConfigs(container *corev1.Container, containerConfigs feastdevv1alpha1.ContainerConfigs) { - container.Image = *containerConfigs.DefaultCtrConfigs.Image +func applyCtrConfigs(container *corev1.Container, containerConfigs feastdevv1.ContainerConfigs) { + if containerConfigs.DefaultCtrConfigs.Image != nil { + container.Image = *containerConfigs.DefaultCtrConfigs.Image + } // apply optional container configs if containerConfigs.OptionalCtrConfigs.Env != nil { container.Env = envOverride(container.Env, *containerConfigs.OptionalCtrConfigs.Env) @@ -1118,7 +1412,7 @@ func (feast *FeastServices) mountPvcConfigs(podSpec *corev1.PodSpec) { } } -func (feast *FeastServices) mountPvcConfig(podSpec *corev1.PodSpec, pvcConfig *feastdevv1alpha1.PvcConfig, feastType FeastServiceType) { +func (feast *FeastServices) mountPvcConfig(podSpec *corev1.PodSpec, pvcConfig *feastdevv1.PvcConfig, feastType FeastServiceType) { if podSpec != nil && pvcConfig != nil { volName := feast.initPVC(feastType).Name pvcName := volName @@ -1133,13 +1427,11 @@ func (feast *FeastServices) mountPvcConfig(podSpec *corev1.PodSpec, pvcConfig *f }, }, }) - if feastType == OfflineFeastType { - for i := range podSpec.InitContainers { - podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, corev1.VolumeMount{ - Name: volName, - MountPath: pvcConfig.MountPath, - }) - } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: pvcConfig.MountPath, + }) } for i := range podSpec.Containers { podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ @@ -1158,6 +1450,9 @@ func (feast *FeastServices) mountEmptyDirVolumes(podSpec *corev1.PodSpec) { func (feast *FeastServices) getFeatureRepoDir() string { applied := feast.Handler.FeatureStore.Status.Applied + if applied.FeastProjectDir != nil && applied.FeastProjectDir.Packaged != nil && applied.Services.DisableInitContainers { + return path.Clean(applied.FeastProjectDir.Packaged.FeatureRepoPath) + } feastProjectDir := getOfflineMountPath(feast.Handler.FeatureStore) + "/" + applied.FeastProject if applied.FeastProjectDir != nil && applied.FeastProjectDir.Git != nil && len(applied.FeastProjectDir.Git.FeatureRepoPath) > 0 { return feastProjectDir + "/" + applied.FeastProjectDir.Git.FeatureRepoPath @@ -1165,6 +1460,39 @@ func (feast *FeastServices) getFeatureRepoDir() string { return feastProjectDir + "/" + FeatureRepoDir } +func (feast *FeastServices) validatePackagedFeatureRepoPath() error { + applied := feast.Handler.FeatureStore.Status.Applied + if applied.FeastProjectDir == nil || applied.FeastProjectDir.Packaged == nil { + return nil + } + + featureRepoPath := applied.FeastProjectDir.Packaged.FeatureRepoPath + cleanFeatureRepoPath := path.Clean(featureRepoPath) + if !path.IsAbs(featureRepoPath) || cleanFeatureRepoPath == "/" || cleanFeatureRepoPath != featureRepoPath { + return errors.New("packaged feature repository path " + strconv.Quote(featureRepoPath) + " must be a canonical absolute, non-root path") + } + + if !applied.Services.DisableInitContainers { + stagedFeatureRepoPath := path.Clean(feast.getFeatureRepoDir()) + if pathsOverlap(cleanFeatureRepoPath, stagedFeatureRepoPath) { + return errors.New( + "packaged feature repository path " + strconv.Quote(cleanFeatureRepoPath) + + " overlaps staged repository path " + strconv.Quote(stagedFeatureRepoPath), + ) + } + } + + return nil +} + +func pathsOverlap(firstPath, secondPath string) bool { + firstPath = path.Clean(firstPath) + secondPath = path.Clean(secondPath) + return firstPath == secondPath || + strings.HasPrefix(firstPath, secondPath+"/") || + strings.HasPrefix(secondPath, firstPath+"/") +} + func mountEmptyDirVolume(podSpec *corev1.PodSpec) { if podSpec != nil { volName := strings.TrimPrefix(EphemeralPath, "/") @@ -1189,21 +1517,21 @@ func mountEmptyDirVolume(podSpec *corev1.PodSpec) { } } -func getTargetPort(feastType FeastServiceType, tls *feastdevv1alpha1.TlsConfigs) int32 { +func getTargetPort(feastType FeastServiceType, tls *feastdevv1.TlsConfigs) int32 { if tls.IsTLS() { return FeastServiceConstants[feastType].TargetHttpsPort } return FeastServiceConstants[feastType].TargetHttpPort } -func getTargetRestPort(feastType FeastServiceType, tls *feastdevv1alpha1.TlsConfigs) int32 { +func getTargetRestPort(feastType FeastServiceType, tls *feastdevv1.TlsConfigs) int32 { if tls.IsTLS() { return FeastServiceConstants[feastType].TargetRestHttpsPort } return FeastServiceConstants[feastType].TargetRestHttpPort } -func (feast *FeastServices) getProbeHandler(feastType FeastServiceType, tls *feastdevv1alpha1.TlsConfigs) corev1.ProbeHandler { +func (feast *FeastServices) getProbeHandler(feastType FeastServiceType, tls *feastdevv1.TlsConfigs) corev1.ProbeHandler { targetPort := getTargetPort(feastType, tls) if feastType == RegistryFeastType { @@ -1256,6 +1584,67 @@ func IsDeploymentAvailable(conditions []appsv1.DeploymentCondition) bool { return false } +// GetPodContainerFailureMessage inspects pods belonging to the given deployment +// and returns a human-readable message describing the first init or regular +// container that is in a failing state. Returns empty string if no failure found. +func (feast *FeastServices) GetPodContainerFailureMessage(deploy appsv1.Deployment) string { + podList := corev1.PodList{} + selectorLabels := feast.getSelectorLabels() + if err := feast.Handler.Client.List(feast.Handler.Context, &podList, + client.InNamespace(deploy.Namespace), + client.MatchingLabels(selectorLabels), + ); err != nil { + return "" + } + for i := range podList.Items { + pod := &podList.Items[i] + if msg := initContainerFailureMessage(pod); msg != "" { + return msg + } + if msg := containerFailureMessage(pod); msg != "" { + return msg + } + } + return "" +} + +func initContainerFailureMessage(pod *corev1.Pod) string { + for _, cs := range pod.Status.InitContainerStatuses { + if cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "PodInitializing" { + return "Init container '" + cs.Name + "' waiting: " + cs.State.Waiting.Reason + + messageIfPresent(cs.State.Waiting.Message) + } + if cs.State.Terminated != nil && cs.State.Terminated.ExitCode != 0 { + return "Init container '" + cs.Name + "' failed with exit code " + + strconv.Itoa(int(cs.State.Terminated.ExitCode)) + + messageIfPresent(cs.State.Terminated.Message) + } + } + return "" +} + +func containerFailureMessage(pod *corev1.Pod) string { + for _, cs := range pod.Status.ContainerStatuses { + if cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "ContainerCreating" { + return "Container '" + cs.Name + "' waiting: " + cs.State.Waiting.Reason + + messageIfPresent(cs.State.Waiting.Message) + } + if cs.State.Terminated != nil && cs.State.Terminated.ExitCode != 0 { + return "Container '" + cs.Name + "' failed with exit code " + + strconv.Itoa(int(cs.State.Terminated.ExitCode)) + + messageIfPresent(cs.State.Terminated.Message) + } + } + return "" +} + +func messageIfPresent(msg string) string { + if msg != "" { + return " - " + msg + } + return "" +} + // GetFeastRestServiceName returns the feast REST service object name based on service type func (feast *FeastServices) GetFeastRestServiceName(feastType FeastServiceType) string { return feast.GetFeastServiceName(feastType) + "-rest" diff --git a/infra/feast-operator/internal/controller/services/services_test.go b/infra/feast-operator/internal/controller/services/services_test.go index 14509fe9933..da3590674f1 100644 --- a/infra/feast-operator/internal/controller/services/services_test.go +++ b/infra/feast-operator/internal/controller/services/services_test.go @@ -19,26 +19,24 @@ package services import ( "context" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" ) -func ptr[T any](v T) *T { - return &v -} - func (feast *FeastServices) refreshFeatureStore(ctx context.Context, key types.NamespacedName) { - fs := &feastdevv1alpha1.FeatureStore{} + fs := &feastdevv1.FeatureStore{} Expect(k8sClient.Get(ctx, key, fs)).To(Succeed()) feast.Handler.FeatureStore = fs } -func applySpecToStatus(fs *feastdevv1alpha1.FeatureStore) { +func applySpecToStatus(fs *feastdevv1.FeatureStore) { fs.Status.Applied.Services = fs.Spec.Services.DeepCopy() fs.Status.Applied.FeastProject = fs.Spec.FeastProject Expect(k8sClient.Status().Update(context.Background(), fs)).To(Succeed()) @@ -46,15 +44,15 @@ func applySpecToStatus(fs *feastdevv1alpha1.FeatureStore) { var _ = Describe("Registry Service", func() { var ( - featureStore *feastdevv1alpha1.FeatureStore + featureStore *feastdevv1.FeatureStore feast *FeastServices typeNamespacedName types.NamespacedName ctx context.Context ) var setFeatureStoreServerConfig = func(grpcEnabled, restEnabled bool) { - featureStore.Spec.Services.Registry.Local.Server.GRPC = ptr(grpcEnabled) - featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr(restEnabled) + featureStore.Spec.Services.Registry.Local.Server.GRPC = ptr.To(grpcEnabled) + featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr.To(restEnabled) Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) Expect(feast.ApplyDefaults()).To(Succeed()) applySpecToStatus(featureStore) @@ -65,29 +63,29 @@ var _ = Describe("Registry Service", func() { ctx = context.Background() typeNamespacedName = types.NamespacedName{ Name: "testfeaturestore", - Namespace: "default", + Namespace: DefaultNs, } - featureStore = &feastdevv1alpha1.FeatureStore{ + featureStore = &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: typeNamespacedName.Name, Namespace: typeNamespacedName.Namespace, }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: "testproject", - Services: &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ - Image: ptr("test-image"), + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), }, }, }, - GRPC: ptr(true), - RestAPI: ptr(false), + GRPC: ptr.To(true), + RestAPI: ptr.To(false), }, }, }, @@ -204,12 +202,135 @@ var _ = Describe("Registry Service", func() { }) }) + Describe("PodAnnotations Configuration", func() { + It("should apply podAnnotations to deployment pod template", func() { + featureStore.Spec.Services.PodAnnotations = map[string]string{ + otelInjectPythonAnnotation: stringTrue, + "sidecar.istio.io/inject": stringTrue, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + Expect(deployment.Spec.Template.Annotations).To(Equal(map[string]string{ + otelInjectPythonAnnotation: stringTrue, + "sidecar.istio.io/inject": stringTrue, + })) + }) + + It("should have no pod template annotations when podAnnotations is not set", func() { + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + Expect(deployment.Spec.Template.Annotations).To(BeNil()) + }) + + It("should remove pod template annotations when podAnnotations is removed", func() { + featureStore.Spec.Services.PodAnnotations = map[string]string{ + otelInjectPythonAnnotation: stringTrue, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Template.Annotations).To(HaveKey("instrumentation.opentelemetry.io/inject-python")) + + featureStore.Spec.Services.PodAnnotations = nil + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Template.Annotations).To(BeNil()) + }) + }) + + Describe("ResourceClaims Configuration", func() { + It("should apply resourceClaims to deployment pod template", func() { + featureStore.Spec.Services.ResourceClaims = []corev1.PodResourceClaim{ + { + Name: "gpu-claim", + ResourceClaimName: ptr.To("my-gpu-claim"), + }, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + Expect(deployment.Spec.Template.Spec.ResourceClaims).To(Equal([]corev1.PodResourceClaim{ + { + Name: "gpu-claim", + ResourceClaimName: ptr.To("my-gpu-claim"), + }, + })) + }) + + It("should have no resourceClaims when field is not set", func() { + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + Expect(deployment.Spec.Template.Spec.ResourceClaims).To(BeNil()) + }) + + It("should remove resourceClaims when field is removed", func() { + featureStore.Spec.Services.ResourceClaims = []corev1.PodResourceClaim{ + { + Name: "gpu-claim", + ResourceClaimName: ptr.To("my-gpu-claim"), + }, + } + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Template.Spec.ResourceClaims).To(HaveLen(1)) + + featureStore.Spec.Services.ResourceClaims = nil + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.setDeployment(deployment)).To(Succeed()) + Expect(deployment.Spec.Template.Spec.ResourceClaims).To(BeNil()) + }) + }) + Describe("NodeSelector Configuration", func() { It("should apply NodeSelector to pod spec when configured", func() { // Set NodeSelector for registry service nodeSelector := map[string]string{ - "kubernetes.io/os": "linux", - "node-type": "compute", + kubernetesOsLabel: linuxOS, + nodeTypeLabel: computeNodeType, } featureStore.Spec.Services.Registry.Local.Server.ContainerConfigs.OptionalCtrConfigs.NodeSelector = &nodeSelector Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) @@ -224,8 +345,8 @@ var _ = Describe("Registry Service", func() { // Verify NodeSelector is applied to pod spec expectedNodeSelector := map[string]string{ - "kubernetes.io/os": "linux", - "node-type": "compute", + kubernetesOsLabel: linuxOS, + nodeTypeLabel: computeNodeType, } Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(expectedNodeSelector)) }) @@ -233,23 +354,23 @@ var _ = Describe("Registry Service", func() { It("should merge NodeSelectors from multiple services", func() { // Set NodeSelector for registry service registryNodeSelector := map[string]string{ - "kubernetes.io/os": "linux", - "node-type": "compute", + kubernetesOsLabel: linuxOS, + nodeTypeLabel: computeNodeType, } featureStore.Spec.Services.Registry.Local.Server.ContainerConfigs.OptionalCtrConfigs.NodeSelector = ®istryNodeSelector // Set NodeSelector for online store service onlineNodeSelector := map[string]string{ - "node-type": "online", - "zone": "us-west-1a", + nodeTypeLabel: "online", + zoneLabel: "us-west-1a", } - featureStore.Spec.Services.OnlineStore = &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ - Image: ptr("test-image"), + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), }, - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ NodeSelector: &onlineNodeSelector, }, }, @@ -268,9 +389,9 @@ var _ = Describe("Registry Service", func() { // Verify NodeSelector merges all service selectors (online overrides registry for node-type) expectedNodeSelector := map[string]string{ - "kubernetes.io/os": "linux", - "node-type": "online", - "zone": "us-west-1a", + kubernetesOsLabel: linuxOS, + "node-type": "online", + zoneLabel: "us-west-1a", } Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(expectedNodeSelector)) }) @@ -280,12 +401,12 @@ var _ = Describe("Registry Service", func() { uiNodeSelector := map[string]string{ "node-type": "ui", } - featureStore.Spec.Services.UI = &feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ - Image: ptr("test-image"), + featureStore.Spec.Services.UI = &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), }, - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ NodeSelector: &uiNodeSelector, }, }, @@ -325,15 +446,15 @@ var _ = Describe("Registry Service", func() { It("should apply UI service NodeSelector when UI has highest precedence", func() { // Set NodeSelector for online service onlineNodeSelector := map[string]string{ - "node-type": "online", + nodeTypeLabel: "online", } - featureStore.Spec.Services.OnlineStore = &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ - Image: ptr("test-image"), + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), }, - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ NodeSelector: &onlineNodeSelector, }, }, @@ -345,12 +466,12 @@ var _ = Describe("Registry Service", func() { "node-type": "ui", "zone": "us-east-1", } - featureStore.Spec.Services.UI = &feastdevv1alpha1.ServerConfigs{ - ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ - DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ - Image: ptr("test-image"), + featureStore.Spec.Services.UI = &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), }, - OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + OptionalCtrConfigs: feastdevv1.OptionalCtrConfigs{ NodeSelector: &uiNodeSelector, }, }, @@ -374,6 +495,49 @@ var _ = Describe("Registry Service", func() { Expect(deployment.Spec.Template.Spec.NodeSelector).To(Equal(expectedNodeSelector)) }) + It("should enable metrics on the online service when configured", func() { + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{Metrics: ptr.To(true)}, + } + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + onlineContainer := GetOnlineContainer(*deployment) + Expect(onlineContainer).NotTo(BeNil()) + Expect(onlineContainer.Command).To(Equal([]string{feastCommand, "serve", "--metrics", "-h", hostAllIPv4, "-p", "6566"})) + Expect(onlineContainer.Ports).To(ContainElement(corev1.ContainerPort{ + Name: metricsPortName, + ContainerPort: MetricsPort, + Protocol: corev1.ProtocolTCP, + })) + metricsPortCount := 0 + for _, port := range onlineContainer.Ports { + if port.Name == metricsPortName { + metricsPortCount++ + } + } + Expect(metricsPortCount).To(Equal(1)) + + svc := feast.initFeastSvc(OnlineFeastType) + Expect(svc).NotTo(BeNil()) + Expect(feast.setService(svc, OnlineFeastType, false)).To(Succeed()) + Expect(svc.Spec.Ports).To(ContainElement(corev1.ServicePort{ + Name: "metrics", + Port: MetricsPort, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(int(MetricsPort)), + })) + }) + It("should handle empty NodeSelector gracefully", func() { // Set empty NodeSelector emptyNodeSelector := map[string]string{} @@ -392,4 +556,371 @@ var _ = Describe("Registry Service", func() { Expect(deployment.Spec.Template.Spec.NodeSelector).To(BeEmpty()) }) }) + + Describe("WorkerConfigs Configuration", func() { + It("should apply WorkerConfigs to the online store command", func() { + // Set WorkerConfigs for online store + workers := int32(4) + workerConnections := int32(2000) + maxRequests := int32(500) + maxRequestsJitter := int32(100) + keepAliveTimeout := int32(60) + registryTTLSeconds := int32(120) + + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + WorkerConfigs: &feastdevv1.WorkerConfigs{ + Workers: &workers, + WorkerConnections: &workerConnections, + MaxRequests: &maxRequests, + MaxRequestsJitter: &maxRequestsJitter, + KeepAliveTimeout: &keepAliveTimeout, + RegistryTTLSeconds: ®istryTTLSeconds, + }, + }, + } + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + onlineContainer := GetOnlineContainer(*deployment) + Expect(onlineContainer).NotTo(BeNil()) + + // Verify all worker config options are present in the command + Expect(onlineContainer.Command).To(ContainElement("--workers")) + Expect(onlineContainer.Command).To(ContainElement("4")) + Expect(onlineContainer.Command).To(ContainElement("--worker-connections")) + Expect(onlineContainer.Command).To(ContainElement("2000")) + Expect(onlineContainer.Command).To(ContainElement("--max-requests")) + Expect(onlineContainer.Command).To(ContainElement("500")) + Expect(onlineContainer.Command).To(ContainElement("--max-requests-jitter")) + Expect(onlineContainer.Command).To(ContainElement("100")) + Expect(onlineContainer.Command).To(ContainElement("--keep-alive-timeout")) + Expect(onlineContainer.Command).To(ContainElement("60")) + Expect(onlineContainer.Command).To(ContainElement("--registry_ttl_sec")) + Expect(onlineContainer.Command).To(ContainElement("120")) + }) + + It("should apply partial WorkerConfigs to the online store command", func() { + // Set only some WorkerConfigs options + workers := int32(-1) // Auto-calculate + registryTTLSeconds := int32(300) + + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + WorkerConfigs: &feastdevv1.WorkerConfigs{ + Workers: &workers, + RegistryTTLSeconds: ®istryTTLSeconds, + }, + }, + } + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + onlineContainer := GetOnlineContainer(*deployment) + Expect(onlineContainer).NotTo(BeNil()) + + // Verify specified options are present + Expect(onlineContainer.Command).To(ContainElement("--workers")) + Expect(onlineContainer.Command).To(ContainElement("-1")) + Expect(onlineContainer.Command).To(ContainElement("--registry_ttl_sec")) + Expect(onlineContainer.Command).To(ContainElement("300")) + + // Verify unspecified options are not present + Expect(onlineContainer.Command).NotTo(ContainElement("--worker-connections")) + Expect(onlineContainer.Command).NotTo(ContainElement("--max-requests")) + Expect(onlineContainer.Command).NotTo(ContainElement("--max-requests-jitter")) + Expect(onlineContainer.Command).NotTo(ContainElement("--keep-alive-timeout")) + }) + + It("should not add worker config options when WorkerConfigs is nil", func() { + featureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + // WorkerConfigs is not set (nil) + }, + } + + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + feast.refreshFeatureStore(ctx, typeNamespacedName) + + Expect(feast.deployFeastServiceByType(OnlineFeastType)).To(Succeed()) + + deployment := feast.initFeastDeploy() + Expect(deployment).NotTo(BeNil()) + Expect(feast.setDeployment(deployment)).To(Succeed()) + + onlineContainer := GetOnlineContainer(*deployment) + Expect(onlineContainer).NotTo(BeNil()) + + // Verify no worker config options are present (defaults are used by the CLI) + Expect(onlineContainer.Command).NotTo(ContainElement("--workers")) + Expect(onlineContainer.Command).NotTo(ContainElement("--worker-connections")) + Expect(onlineContainer.Command).NotTo(ContainElement("--max-requests")) + Expect(onlineContainer.Command).NotTo(ContainElement("--max-requests-jitter")) + Expect(onlineContainer.Command).NotTo(ContainElement("--keep-alive-timeout")) + Expect(onlineContainer.Command).NotTo(ContainElement("--registry_ttl_sec")) + }) + }) +}) + +var _ = Describe("Service AppProtocol Configuration", func() { + var ( + featureStore *feastdevv1.FeatureStore + feast *FeastServices + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + featureStore = &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testfeaturestore-approtocol", + Namespace: DefaultNs, + }, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "testproject", + Services: &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("test-image"), + }, + }, + }, + GRPC: ptr.To(true), + RestAPI: ptr.To(false), + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, featureStore)).To(Succeed()) + applySpecToStatus(featureStore) + feast = &FeastServices{ + Handler: handler.FeastHandler{ + Client: k8sClient, + Context: ctx, + Scheme: k8sClient.Scheme(), + FeatureStore: featureStore, + }, + } + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + }) + + AfterEach(func() { + Expect(k8sClient.Delete(ctx, featureStore)).To(Succeed()) + }) + + It("should return grpc appProtocol for the registry gRPC service", func() { + Expect(feast.isRegistryGrpcEnabled()).To(BeTrue()) + Expect(feast.getServiceAppProtocol(RegistryFeastType, false)).To(Equal(ptr.To("grpc"))) + }) + + It("should return nil appProtocol for the registry REST service", func() { + featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr.To(true) + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + + Expect(feast.getServiceAppProtocol(RegistryFeastType, true)).To(BeNil()) + }) + + It("should return nil appProtocol for the online store service", func() { + Expect(feast.getServiceAppProtocol(OnlineFeastType, false)).To(BeNil()) + }) + + It("should return nil appProtocol for the offline store service", func() { + Expect(feast.getServiceAppProtocol(OfflineFeastType, false)).To(BeNil()) + }) + + It("should return nil appProtocol when registry gRPC is disabled", func() { + featureStore.Spec.Services.Registry.Local.Server.GRPC = ptr.To(false) + featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr.To(true) + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + + Expect(feast.isRegistryGrpcEnabled()).To(BeFalse()) + Expect(feast.getServiceAppProtocol(RegistryFeastType, false)).To(BeNil()) + }) + + It("should set grpc appProtocol on the registry gRPC Service port", func() { + Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) + svc := feast.initFeastSvc(RegistryFeastType) + Expect(svc).NotTo(BeNil()) + Expect(feast.setService(svc, RegistryFeastType, false)).To(Succeed()) + + Expect(svc.Spec.Ports).To(HaveLen(1)) + Expect(svc.Spec.Ports[0].AppProtocol).To(Equal(ptr.To("grpc"))) + }) + + It("should not set appProtocol on the registry REST Service port", func() { + featureStore.Spec.Services.Registry.Local.Server.RestAPI = ptr.To(true) + Expect(k8sClient.Update(ctx, featureStore)).To(Succeed()) + Expect(feast.ApplyDefaults()).To(Succeed()) + applySpecToStatus(featureStore) + + Expect(feast.deployFeastServiceByType(RegistryFeastType)).To(Succeed()) + restSvc := feast.initFeastRestSvc(RegistryFeastType) + Expect(restSvc).NotTo(BeNil()) + Expect(feast.setService(restSvc, RegistryFeastType, true)).To(Succeed()) + + Expect(restSvc.Spec.Ports).To(HaveLen(1)) + Expect(restSvc.Spec.Ports[0].AppProtocol).To(BeNil()) + }) +}) + +var _ = Describe("Pod Container Failure Messages", func() { + It("should detect init container in CrashLoopBackOff", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: feastInitContainerName, + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}, + }, + }, + { + Name: feastApplyContainerName, + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "CrashLoopBackOff", + Message: "back-off 5m0s restarting failed container", + }, + }, + }, + }, + }, + } + msg := initContainerFailureMessage(pod) + Expect(msg).To(ContainSubstring("feast-apply")) + Expect(msg).To(ContainSubstring("CrashLoopBackOff")) + Expect(msg).To(ContainSubstring("back-off 5m0s")) + }) + + It("should detect init container terminated with non-zero exit code", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: feastApplyContainerName, + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 1, + Message: "feast apply failed", + }, + }, + }, + }, + }, + } + msg := initContainerFailureMessage(pod) + Expect(msg).To(ContainSubstring("feast-apply")) + Expect(msg).To(ContainSubstring("exit code 1")) + Expect(msg).To(ContainSubstring("feast apply failed")) + }) + + It("should return empty for init containers still initializing", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: feastInitContainerName, + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "PodInitializing", + }, + }, + }, + }, + }, + } + Expect(initContainerFailureMessage(pod)).To(BeEmpty()) + }) + + It("should detect regular container failure", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: registryName, + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "ImagePullBackOff", + Message: "image not found", + }, + }, + }, + }, + }, + } + msg := containerFailureMessage(pod) + Expect(msg).To(ContainSubstring("registry")) + Expect(msg).To(ContainSubstring("ImagePullBackOff")) + }) + + It("should return empty for healthy pods", func() { + pod := &corev1.Pod{ + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + Name: feastInitContainerName, + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}, + }, + }, + }, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: registryName, + State: corev1.ContainerState{ + Running: &corev1.ContainerStateRunning{}, + }, + }, + }, + }, + } + Expect(initContainerFailureMessage(pod)).To(BeEmpty()) + Expect(containerFailureMessage(pod)).To(BeEmpty()) + }) }) diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index 68ee053ace5..090696eccaf 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -18,7 +18,7 @@ package services import ( "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" handler "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,6 +26,8 @@ import ( const ( TmpFeatureStoreYamlEnvVar = "TMP_FEATURE_STORE_YAML_BASE64" + packagedFeatureRepoEnvVar = "FEAST_PACKAGED_FEATURE_REPO_PATH" + stagedFeatureRepoEnvVar = "FEAST_STAGED_FEATURE_REPO_PATH" feastServerImageVar = "RELATED_IMAGE_FEATURE_SERVER" cronJobImageVar = "RELATED_IMAGE_CRON_JOB" FeatureStoreYamlCmKey = "feature_store.yaml" @@ -40,6 +42,14 @@ const ( NamespaceRegistryDataKey = "namespaces" DefaultKubernetesNamespace = "feast-operator-system" + // ProtectedProjectAnnotation is the annotation key on a FeatureStore CR + // that marks its project as protected. Protected projects are excluded + // from project listings and shielded from teardown by other instances. + // When this annotation is "true", the operator sets FEAST_PROTECTED_PROJECT=true + // on the server pods, which causes the server to tag the project in the + // shared registry on startup. + ProtectedProjectAnnotation = "feast.dev/protected-project" + HttpPort = 80 HttpsPort = 443 HttpScheme = "http" @@ -48,12 +58,22 @@ const ( tlsPathCustomCABundle = "/etc/pki/tls/custom-certs/ca-bundle.crt" tlsNameSuffix = "-tls" - caBundleAnnotation = "config.openshift.io/inject-trusted-cabundle" - caBundleName = "odh-trusted-ca-bundle" - - DefaultOfflineStorageRequest = "20Gi" - DefaultOnlineStorageRequest = "5Gi" - DefaultRegistryStorageRequest = "5Gi" + caBundleAnnotation = "config.openshift.io/inject-trusted-cabundle" + caBundleName = "odh-trusted-ca-bundle" + odhCaBundleKey = "odh-ca-bundle.crt" + tlsPathOdhCABundle = "/etc/pki/tls/custom-certs/odh-ca-bundle.crt" + tlsPathOidcCA = "/etc/pki/tls/oidc-ca/ca.crt" + oidcCaVolumeName = "oidc-ca-cert" + defaultCACertKey = "ca-bundle.crt" + openshiftServingCertSecretAnnotation = "service.beta.openshift.io/serving-cert-secret-name" // pragma: allowlist secret + openshiftServingCertSansAnnotation = "service.beta.openshift.io/serving-cert-sans" + openshiftInjectCaBundleAnnotation = "service.beta.openshift.io/inject-cabundle" + ErrorMessagePrefix = "Error: " + + DefaultOfflineStorageRequest = "20Gi" + DefaultOnlineStorageRequest = "5Gi" + DefaultRegistryStorageRequest = "5Gi" + MetricsPort int32 = 8000 AuthzFeastType FeastServiceType = "authorization" OfflineFeastType FeastServiceType = "offline" @@ -90,25 +110,76 @@ const ( OidcClientSecret OidcPropertyType = "client_secret" OidcUsername OidcPropertyType = "username" OidcPassword OidcPropertyType = "password" + OidcTokenEnvVar OidcPropertyType = "token_env_var" + OidcVerifySsl OidcPropertyType = "verify_ssl" + OidcCaCertPath OidcPropertyType = "ca_cert_path" + OidcAudience OidcPropertyType = "audience" + OidcIssuer OidcPropertyType = "issuer" OidcMissingSecretError string = "missing OIDC secret: %s" + + // Common string constants + stringTrue = "true" + stringFalse = "false" + hostAllIPv4 = "0.0.0.0" + tlsCertKey = "tls.crt" + DefaultNs = "default" + feastCommand = "feast" + metricsPortName = "metrics" + registryName = "registry" + feastInitContainerName = "feast-init" + feastApplyContainerName = "feast-apply" + + // Test-specific constants + dataOnlineDbPath = "/data/online.db" + dataRegistryDbPath = "/data/registry.db" + oidcSecretName = "oidc-secret" // pragma: allowlist secret + clientIDValue = "client-id" + lineageSecretName = "lineage-secret" // pragma: allowlist secret + redisType = "redis" + redisSecretName = "redis-secret" // pragma: allowlist secret + registrySecretName = "registry-secret" // pragma: allowlist secret + celTestProject = "celtest" + kubernetesHostnameTopologyKey = "kubernetes.io/hostname" + dailyMidnightCron = "0 0 * * *" + otelInjectPythonAnnotation = "instrumentation.opentelemetry.io/inject-python" + kubernetesOsLabel = "kubernetes.io/os" + computeNodeType = "compute" + nodeTypeLabel = "node-type" + zoneLabel = "zone" + linuxOS = "linux" + TestValue = "test" + OfflineStoreSecretName = "offline-store-secret" // pragma: allowlist secret + OnlineStoreSecretName = "online-store-secret" // pragma: allowlist secret + RegistryStoreSecretName = "registry-store-secret" // pragma: allowlist secret + FieldRefName = "fieldRefName" + ConfigOne = "config-1" + MetadataNameField = "metadata.name" + GrpcFlag = "--grpc" + ExampleConfigMapName = "example-configmap" + ExampleSecretName = "example-secret" // pragma: allowlist secret +) + +const ( + ManagedByLabelKey = "app.kubernetes.io/managed-by" + ManagedByLabelValue = "feast-operator" ) var ( DefaultImage = "quay.io/feastdev/feature-server:" + feastversion.FeastVersion DefaultCronJobImage = "quay.io/openshift/origin-cli:4.17" DefaultPVCAccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce} - NameLabelKey = feastdevv1alpha1.GroupVersion.Group + "/name" - ServiceTypeLabelKey = feastdevv1alpha1.GroupVersion.Group + "/service-type" + NameLabelKey = feastdevv1.GroupVersion.Group + "/name" + ServiceTypeLabelKey = feastdevv1.GroupVersion.Group + "/service-type" FeastServiceConstants = map[FeastServiceType]deploymentSettings{ OfflineFeastType: { - Args: []string{"serve_offline", "-h", "0.0.0.0"}, + Args: []string{"serve_offline", "-h", hostAllIPv4}, TargetHttpPort: 8815, TargetHttpsPort: 8816, }, OnlineFeastType: { - Args: []string{"serve", "-h", "0.0.0.0"}, + Args: []string{"serve", "-h", hostAllIPv4}, TargetHttpPort: 6566, TargetHttpsPort: 6567, }, @@ -129,86 +200,85 @@ var ( FeastServiceConditions = map[FeastServiceType]map[metav1.ConditionStatus]metav1.Condition{ OfflineFeastType: { metav1.ConditionTrue: { - Type: feastdevv1alpha1.OfflineStoreReadyType, + Type: feastdevv1.OfflineStoreReadyType, Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.OfflineStoreReadyMessage, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.OfflineStoreReadyMessage, }, metav1.ConditionFalse: { - Type: feastdevv1alpha1.OfflineStoreReadyType, + Type: feastdevv1.OfflineStoreReadyType, Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.OfflineStoreFailedReason, + Reason: feastdevv1.OfflineStoreFailedReason, }, }, OnlineFeastType: { metav1.ConditionTrue: { - Type: feastdevv1alpha1.OnlineStoreReadyType, + Type: feastdevv1.OnlineStoreReadyType, Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.OnlineStoreReadyMessage, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.OnlineStoreReadyMessage, }, metav1.ConditionFalse: { - Type: feastdevv1alpha1.OnlineStoreReadyType, + Type: feastdevv1.OnlineStoreReadyType, Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.OnlineStoreFailedReason, + Reason: feastdevv1.OnlineStoreFailedReason, }, }, RegistryFeastType: { metav1.ConditionTrue: { - Type: feastdevv1alpha1.RegistryReadyType, + Type: feastdevv1.RegistryReadyType, Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.RegistryReadyMessage, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.RegistryReadyMessage, }, metav1.ConditionFalse: { - Type: feastdevv1alpha1.RegistryReadyType, + Type: feastdevv1.RegistryReadyType, Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.RegistryFailedReason, + Reason: feastdevv1.RegistryFailedReason, }, }, UIFeastType: { metav1.ConditionTrue: { - Type: feastdevv1alpha1.UIReadyType, + Type: feastdevv1.UIReadyType, Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.UIReadyMessage, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.UIReadyMessage, }, metav1.ConditionFalse: { - Type: feastdevv1alpha1.UIReadyType, + Type: feastdevv1.UIReadyType, Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.UIFailedReason, + Reason: feastdevv1.UIFailedReason, }, }, ClientFeastType: { metav1.ConditionTrue: { - Type: feastdevv1alpha1.ClientReadyType, + Type: feastdevv1.ClientReadyType, Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.ClientReadyMessage, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.ClientReadyMessage, }, metav1.ConditionFalse: { - Type: feastdevv1alpha1.ClientReadyType, + Type: feastdevv1.ClientReadyType, Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.ClientFailedReason, + Reason: feastdevv1.ClientFailedReason, }, }, CronJobFeastType: { metav1.ConditionTrue: { - Type: feastdevv1alpha1.CronJobReadyType, + Type: feastdevv1.CronJobReadyType, Status: metav1.ConditionTrue, - Reason: feastdevv1alpha1.ReadyReason, - Message: feastdevv1alpha1.CronJobReadyMessage, + Reason: feastdevv1.ReadyReason, + Message: feastdevv1.CronJobReadyMessage, }, metav1.ConditionFalse: { - Type: feastdevv1alpha1.CronJobReadyType, + Type: feastdevv1.CronJobReadyType, Status: metav1.ConditionFalse, - Reason: feastdevv1alpha1.CronJobFailedReason, + Reason: feastdevv1.CronJobFailedReason, }, }, } - OidcServerProperties = []OidcPropertyType{OidcClientId, OidcAuthDiscoveryUrl} - OidcClientProperties = []OidcPropertyType{OidcClientSecret, OidcUsername, OidcPassword} + OidcOptionalSecretProperties = []OidcPropertyType{OidcAuthDiscoveryUrl, OidcClientId, OidcClientSecret, OidcUsername, OidcPassword, OidcAudience, OidcIssuer} ) // Feast server types: Reserved only for server types like Online, Offline, and Registry servers. Should not be used for client types like the UI, etc. @@ -247,13 +317,79 @@ type FeastServices struct { // RepoConfig is the Repo config. Typically loaded from feature_store.yaml. // https://rtd.feast.dev/en/stable/#feast.repo_config.RepoConfig type RepoConfig struct { - Project string `yaml:"project,omitempty"` - Provider FeastProviderType `yaml:"provider,omitempty"` - OfflineStore OfflineStoreConfig `yaml:"offline_store,omitempty"` - OnlineStore OnlineStoreConfig `yaml:"online_store,omitempty"` - Registry RegistryConfig `yaml:"registry,omitempty"` - AuthzConfig AuthzConfig `yaml:"auth,omitempty"` - EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,omitempty"` + Project string `yaml:"project,omitempty"` + Provider FeastProviderType `yaml:"provider,omitempty"` + OfflineStore OfflineStoreConfig `yaml:"offline_store,omitempty"` + OnlineStore OnlineStoreConfig `yaml:"online_store,omitempty"` + Registry RegistryConfig `yaml:"registry,omitempty"` + AuthzConfig AuthzConfig `yaml:"auth,omitempty"` + EntityKeySerializationVersion int `yaml:"entity_key_serialization_version,omitempty"` + BatchEngine *ComputeEngineConfig `yaml:"batch_engine,omitempty"` + FeatureServer *FeatureServerYamlConfig `yaml:"feature_server,omitempty"` + Materialization *MaterializationYamlConfig `yaml:"materialization,omitempty"` + OpenLineage *OpenLineageYamlConfig `yaml:"openlineage,omitempty"` + DataQualityMonitoring *DataQualityMonitoringYamlConfig `yaml:"data_quality_monitoring,omitempty"` +} + +// FeatureServerYamlConfig maps to the feature_server section of feature_store.yaml. +// Field names match Feast's Python SDK YAML keys exactly. +type FeatureServerYamlConfig struct { + Type string `yaml:"type"` + Metrics *MetricsYamlConfig `yaml:"metrics,omitempty"` + OfflinePushBatchingEnabled *bool `yaml:"offline_push_batching_enabled,omitempty"` + OfflinePushBatchingBatchSize *int32 `yaml:"offline_push_batching_batch_size,omitempty"` + OfflinePushBatchingBatchIntervalSeconds *int32 `yaml:"offline_push_batching_batch_interval_seconds,omitempty"` + McpEnabled *bool `yaml:"mcp_enabled,omitempty"` + McpServerName *string `yaml:"mcp_server_name,omitempty"` + McpServerVersion *string `yaml:"mcp_server_version,omitempty"` + McpTransport *string `yaml:"mcp_transport,omitempty"` +} + +// MetricsYamlConfig maps to the feature_server.metrics section of feature_store.yaml. +// Category booleans are merged inline so they sit at the same YAML level as +// "enabled". Keys must be valid Feast MetricsConfig field names for the SDK +// version in use (e.g. resource, request, online_features, push, +// materialization, freshness). Note: Feast's MetricsConfig uses +// extra="forbid", so unknown keys will be rejected by SDK validation. +type MetricsYamlConfig struct { + Enabled bool `yaml:"enabled"` + Categories map[string]interface{} `yaml:",inline,omitempty"` +} + +// DataQualityMonitoringYamlConfig mirrors the Python DqmConfig in feature_store.yaml. +type DataQualityMonitoringYamlConfig struct { + AutoBaseline bool `yaml:"auto_baseline"` +} + +// MaterializationYamlConfig maps to the materialization section of feature_store.yaml. +// ExtraConfig is merged inline so future Feast MaterializationConfig fields appear +// at the same YAML level as the typed fields above. +type MaterializationYamlConfig struct { + OnlineWriteBatchSize *int32 `yaml:"online_write_batch_size,omitempty"` + ExtraConfig map[string]interface{} `yaml:",inline,omitempty"` +} + +// OpenLineageYamlConfig maps to the openlineage section of feature_store.yaml. +// ExtraConfig is merged inline so all extra key-value pairs (namespace, producer, +// emit_on_apply, emit_on_materialize, transport-specific options, etc.) appear at +// the same YAML level as the typed connection fields. +type OpenLineageYamlConfig struct { + Enabled bool `yaml:"enabled"` + TransportType *string `yaml:"transport_type,omitempty"` + TransportUrl *string `yaml:"transport_url,omitempty"` + TransportEndpoint *string `yaml:"transport_endpoint,omitempty"` + ApiKey *string `yaml:"api_key,omitempty"` + ExtraConfig map[string]interface{} `yaml:",inline,omitempty"` + Consumer *OpenLineageConsumerYamlConfig `yaml:"consumer,omitempty"` +} + +// OpenLineageConsumerYamlConfig maps to the openlineage.consumer section of feature_store.yaml. +type OpenLineageConsumerYamlConfig struct { + Enabled bool `yaml:"enabled"` + StoreType *string `yaml:"store_type,omitempty"` + ConnectionString *string `yaml:"connection_string,omitempty"` + ApiKey *string `yaml:"api_key,omitempty"` + NamespaceMapping map[string]string `yaml:"namespace_mapping,omitempty"` } // OfflineStoreConfig is the configuration that relates to reading from and writing to the Feast offline store. @@ -280,15 +416,29 @@ type RegistryConfig struct { RegistryType RegistryConfigType `yaml:"registry_type,omitempty"` Cert string `yaml:"cert,omitempty"` S3AdditionalKwargs *map[string]string `yaml:"s3_additional_kwargs,omitempty"` + CacheTTLSeconds *int32 `yaml:"cache_ttl_seconds,omitempty"` + CacheMode *string `yaml:"cache_mode,omitempty"` + Mcp *RegistryMcpYamlConfig `yaml:"mcp,omitempty"` DBParameters map[string]interface{} `yaml:",inline,omitempty"` } +// RegistryMcpYamlConfig maps to the registry.mcp section of feature_store.yaml. +type RegistryMcpYamlConfig struct { + Enabled bool `yaml:"enabled"` +} + // AuthzConfig is the RBAC authorization configuration. type AuthzConfig struct { Type AuthzType `yaml:"type,omitempty"` OidcParameters map[string]interface{} `yaml:",inline,omitempty"` } +// ComputeEngineConfig is the configuration for batch compute engine. +type ComputeEngineConfig struct { + Type string `yaml:"type,omitempty"` + Parameters map[string]interface{} `yaml:",inline,omitempty"` +} + type deploymentSettings struct { Args []string TargetHttpPort int32 diff --git a/infra/feast-operator/internal/controller/services/suite_test.go b/infra/feast-operator/internal/controller/services/suite_test.go index 5e922bc7e4a..de1b75817ef 100644 --- a/infra/feast-operator/internal/controller/services/suite_test.go +++ b/infra/feast-operator/internal/controller/services/suite_test.go @@ -31,7 +31,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" // +kubebuilder:scaffold:imports ) @@ -68,7 +68,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - err = feastdevv1alpha1.AddToScheme(scheme.Scheme) + err = feastdevv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme @@ -88,3 +88,7 @@ var _ = AfterSuite(func() { func testSetIsOpenShift() { isOpenShift = true } + +func testSetHasServiceMonitorCRD(val bool) { + hasServiceMonitorCRD = val +} diff --git a/infra/feast-operator/internal/controller/services/tls.go b/infra/feast-operator/internal/controller/services/tls.go index 4b209e64e8c..a3a5493ba5b 100644 --- a/infra/feast-operator/internal/controller/services/tls.go +++ b/infra/feast-operator/internal/controller/services/tls.go @@ -23,7 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" corev1 "k8s.io/api/core/v1" ) @@ -50,21 +50,21 @@ func (feast *FeastServices) setTlsDefaults() error { func (feast *FeastServices) setOpenshiftTls() error { appliedServices := feast.Handler.FeatureStore.Status.Applied.Services if feast.offlineOpenshiftTls() { - appliedServices.OfflineStore.Server.TLS = &feastdevv1alpha1.TlsConfigs{ + appliedServices.OfflineStore.Server.TLS = &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{ Name: feast.initFeastSvc(OfflineFeastType).Name + tlsNameSuffix, }, } } if feast.onlineOpenshiftTls() { - appliedServices.OnlineStore.Server.TLS = &feastdevv1alpha1.TlsConfigs{ + appliedServices.OnlineStore.Server.TLS = &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{ Name: feast.initFeastSvc(OnlineFeastType).Name + tlsNameSuffix, }, } } if feast.uiOpenshiftTls() { - appliedServices.UI.TLS = &feastdevv1alpha1.TlsConfigs{ + appliedServices.UI.TLS = &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{ Name: feast.initFeastSvc(UIFeastType).Name + tlsNameSuffix, }, @@ -77,21 +77,21 @@ func (feast *FeastServices) setOpenshiftTls() error { if grpcEnabled && restEnabled { // Both services enabled: Use gRPC service name as primary certificate // The certificate will include both hostnames as SANs via service annotations - appliedServices.Registry.Local.Server.TLS = &feastdevv1alpha1.TlsConfigs{ + appliedServices.Registry.Local.Server.TLS = &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{ Name: feast.initFeastSvc(RegistryFeastType).Name + tlsNameSuffix, }, } } else if grpcEnabled && !restEnabled { // Only gRPC enabled: Use gRPC service name - appliedServices.Registry.Local.Server.TLS = &feastdevv1alpha1.TlsConfigs{ + appliedServices.Registry.Local.Server.TLS = &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{ Name: feast.initFeastSvc(RegistryFeastType).Name + tlsNameSuffix, }, } } else if !grpcEnabled && restEnabled { // Only REST enabled: Use REST service name - appliedServices.Registry.Local.Server.TLS = &feastdevv1alpha1.TlsConfigs{ + appliedServices.Registry.Local.Server.TLS = &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{ Name: feast.initFeastRestSvc(RegistryFeastType).Name + tlsNameSuffix, }, @@ -100,7 +100,7 @@ func (feast *FeastServices) setOpenshiftTls() error { } else if remote, err := feast.remoteRegistryOpenshiftTls(); remote { // if the remote registry reference is using openshift's service serving certificates, we can use the injected service CA bundle configMap if appliedServices.Registry.Remote.TLS == nil { - appliedServices.Registry.Remote.TLS = &feastdevv1alpha1.TlsRemoteRegistryConfigs{ + appliedServices.Registry.Remote.TLS = &feastdevv1.TlsRemoteRegistryConfigs{ ConfigMapRef: corev1.LocalObjectReference{ Name: feast.initCaConfigMap().Name, }, @@ -135,7 +135,7 @@ func (feast *FeastServices) isOpenShiftTls(feastType FeastServiceType) (isOpenSh return } -func (feast *FeastServices) getTlsConfigs(feastType FeastServiceType) *feastdevv1alpha1.TlsConfigs { +func (feast *FeastServices) getTlsConfigs(feastType FeastServiceType) *feastdevv1.TlsConfigs { if serviceConfigs := feast.getServerConfigs(feastType); serviceConfigs != nil { return serviceConfigs.TLS } @@ -210,6 +210,10 @@ func (feast *FeastServices) mountTlsConfigs(podSpec *corev1.PodSpec) { feast.mountTlsConfig(OnlineFeastType, podSpec) feast.mountTlsConfig(UIFeastType, podSpec) feast.mountCustomCABundle(podSpec) + appliedSpec := feast.Handler.FeatureStore.Status.Applied + if appliedSpec.AuthzConfig != nil && appliedSpec.AuthzConfig.OidcAuthz != nil { + feast.mountOidcCACert(podSpec, appliedSpec.AuthzConfig.OidcAuthz) + } } func (feast *FeastServices) mountTlsConfig(feastType FeastServiceType, podSpec *corev1.PodSpec) { @@ -224,17 +228,21 @@ func (feast *FeastServices) mountTlsConfig(feastType FeastServiceType, podSpec * }, }, }) + tlsMount := corev1.VolumeMount{ + Name: volName, + MountPath: GetTlsPath(feastType), + ReadOnly: true, + } if i, container := getContainerByType(feastType, *podSpec); container != nil { - podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ - Name: volName, - MountPath: GetTlsPath(feastType), - ReadOnly: true, - }) + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, tlsMount) + } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, tlsMount) } } } -func mountTlsRemoteRegistryConfig(podSpec *corev1.PodSpec, tls *feastdevv1alpha1.TlsRemoteRegistryConfigs) { +func mountTlsRemoteRegistryConfig(podSpec *corev1.PodSpec, tls *feastdevv1.TlsRemoteRegistryConfigs) { if tls != nil { volName := string(RegistryFeastType) + tlsNameSuffix podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ @@ -245,12 +253,16 @@ func mountTlsRemoteRegistryConfig(podSpec *corev1.PodSpec, tls *feastdevv1alpha1 }, }, }) + tlsMount := corev1.VolumeMount{ + Name: volName, + MountPath: GetTlsPath(RegistryFeastType), + ReadOnly: true, + } for i := range podSpec.Containers { - podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ - Name: volName, - MountPath: GetTlsPath(RegistryFeastType), - ReadOnly: true, - }) + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, tlsMount) + } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, tlsMount) } } } @@ -267,24 +279,69 @@ func (feast *FeastServices) mountCustomCABundle(podSpec *corev1.PodSpec) { }, }) + caMount := corev1.VolumeMount{ + Name: customCaBundle.VolumeName, + MountPath: tlsPathCustomCABundle, + ReadOnly: true, + SubPath: "ca-bundle.crt", + } + odhCaMount := corev1.VolumeMount{ + Name: customCaBundle.VolumeName, + MountPath: tlsPathOdhCABundle, + ReadOnly: true, + SubPath: odhCaBundleKey, + } for i := range podSpec.Containers { - podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, corev1.VolumeMount{ - Name: customCaBundle.VolumeName, - MountPath: tlsPathCustomCABundle, - ReadOnly: true, - SubPath: "ca-bundle.crt", - }) + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, caMount, odhCaMount) + } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, caMount, odhCaMount) } log.FromContext(feast.Handler.Context).Info("Mounted custom CA bundle ConfigMap to Feast pods.") } } +func (feast *FeastServices) mountOidcCACert(podSpec *corev1.PodSpec, oidcAuthz *feastdevv1.OidcAuthz) { + if oidcAuthz.CACertConfigMap == nil { + return + } + cmName := oidcAuthz.CACertConfigMap.Name + cmKey := oidcAuthz.CACertConfigMap.Key + if cmKey == "" { + cmKey = defaultCACertKey + } + + podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ + Name: oidcCaVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, + }, + }, + }) + + mount := corev1.VolumeMount{ + Name: oidcCaVolumeName, + MountPath: tlsPathOidcCA, + ReadOnly: true, + SubPath: cmKey, + } + for i := range podSpec.Containers { + podSpec.Containers[i].VolumeMounts = append(podSpec.Containers[i].VolumeMounts, mount) + } + for i := range podSpec.InitContainers { + podSpec.InitContainers[i].VolumeMounts = append(podSpec.InitContainers[i].VolumeMounts, mount) + } + + log.FromContext(feast.Handler.Context).Info("Mounted OIDC CA certificate ConfigMap to Feast pods.", "configMap", cmName, "key", cmKey) +} + // GetCustomCertificatesBundle retrieves the custom CA bundle ConfigMap if it exists when deployed with RHOAI or ODH func (feast *FeastServices) GetCustomCertificatesBundle() CustomCertificatesBundle { var customCertificatesBundle CustomCertificatesBundle configMapList := &corev1.ConfigMapList{} - labelSelector := client.MatchingLabels{caBundleAnnotation: "true"} + labelSelector := client.MatchingLabels{caBundleAnnotation: stringTrue} err := feast.Handler.Client.List( feast.Handler.Context, @@ -312,17 +369,17 @@ func (feast *FeastServices) GetCustomCertificatesBundle() CustomCertificatesBund return customCertificatesBundle } -func getPortStr(tls *feastdevv1alpha1.TlsConfigs) string { +func getPortStr(tls *feastdevv1.TlsConfigs) string { if tls.IsTLS() { return strconv.Itoa(HttpsPort) } return strconv.Itoa(HttpPort) } -func tlsDefaults(tls *feastdevv1alpha1.TlsConfigs) { +func tlsDefaults(tls *feastdevv1.TlsConfigs) { if tls.IsTLS() { if len(tls.SecretKeyNames.TlsCrt) == 0 { - tls.SecretKeyNames.TlsCrt = "tls.crt" + tls.SecretKeyNames.TlsCrt = tlsCertKey } if len(tls.SecretKeyNames.TlsKey) == 0 { tls.SecretKeyNames.TlsKey = "tls.key" @@ -330,11 +387,11 @@ func tlsDefaults(tls *feastdevv1alpha1.TlsConfigs) { } } -func localRegistryTls(featureStore *feastdevv1alpha1.FeatureStore) bool { +func localRegistryTls(featureStore *feastdevv1.FeatureStore) bool { return IsRegistryServer(featureStore) && featureStore.Status.Applied.Services.Registry.Local.Server.TLS.IsTLS() } -func remoteRegistryTls(featureStore *feastdevv1alpha1.FeatureStore) bool { +func remoteRegistryTls(featureStore *feastdevv1.FeatureStore) bool { return isRemoteRegistry(featureStore) && featureStore.Status.Applied.Services.Registry.Remote.TLS != nil } diff --git a/infra/feast-operator/internal/controller/services/tls_test.go b/infra/feast-operator/internal/controller/services/tls_test.go index aa29fe97548..7f1c94789bc 100644 --- a/infra/feast-operator/internal/controller/services/tls_test.go +++ b/infra/feast-operator/internal/controller/services/tls_test.go @@ -19,7 +19,7 @@ package services import ( "context" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -34,10 +34,10 @@ var _ = Describe("TLS Config", func() { Context("When reconciling a FeatureStore", func() { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(feastdevv1alpha1.AddToScheme(scheme)) + utilruntime.Must(feastdevv1.AddToScheme(scheme)) - secretKeyNames := feastdevv1alpha1.SecretKeyNames{ - TlsCrt: "tls.crt", + secretKeyNames := feastdevv1.SecretKeyNames{ + TlsCrt: tlsCertKey, TlsKey: "tls.key", } @@ -53,11 +53,11 @@ var _ = Describe("TLS Config", func() { FeatureStore: minimalFeatureStore(), }, } - feast.Handler.FeatureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{}, + feast.Handler.FeatureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{}, }, }, }, @@ -84,11 +84,11 @@ var _ = Describe("TLS Config", func() { // registry service w/ openshift tls testSetIsOpenShift() feast.Handler.FeatureStore = minimalFeatureStore() - feast.Handler.FeatureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{}, + feast.Handler.FeatureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{}, }, }, }, @@ -128,8 +128,7 @@ var _ = Describe("TLS Config", func() { err = feast.ApplyDefaults() Expect(err).ToNot(HaveOccurred()) - repoConfig, err := getClientRepoConfig(feast.Handler.FeatureStore, emptyMockExtractConfigFromSecret, &feast) - Expect(err).NotTo(HaveOccurred()) + repoConfig := getClientRepoConfig(feast.Handler.FeatureStore, &feast) Expect(repoConfig.OfflineStore.Port).To(Equal(HttpsPort)) Expect(repoConfig.OfflineStore.Scheme).To(Equal(HttpsScheme)) Expect(repoConfig.OfflineStore.Cert).To(ContainSubstring(string(OfflineFeastType))) @@ -173,7 +172,7 @@ var _ = Describe("TLS Config", func() { feastDeploy := feast.initFeastDeploy() err = feast.setDeployment(feastDeploy) Expect(err).ToNot(HaveOccurred()) - Expect(feastDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(1)) + Expect(feastDeploy.Spec.Template.Spec.InitContainers).To(HaveLen(2)) Expect(feastDeploy.Spec.Template.Spec.Containers).To(HaveLen(4)) Expect(feastDeploy.Spec.Template.Spec.Containers[0].Command).To(ContainElements(ContainSubstring("--key"))) Expect(feastDeploy.Spec.Template.Spec.Containers[1].Command).To(ContainElements(ContainSubstring("--key"))) @@ -181,24 +180,37 @@ var _ = Describe("TLS Config", func() { Expect(feastDeploy.Spec.Template.Spec.Containers[3].Command).To(ContainElements(ContainSubstring("--key"))) Expect(feastDeploy.Spec.Template.Spec.Volumes).To(HaveLen(5)) + // verify init containers receive TLS volume mounts when all services have TLS + for _, initContainer := range feastDeploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement( + HaveField("MountPath", GetTlsPath(RegistryFeastType)), + ), "init container %s should have registry TLS mount", initContainer.Name) + Expect(initContainer.VolumeMounts).To(ContainElement( + HaveField("MountPath", GetTlsPath(OnlineFeastType)), + ), "init container %s should have online TLS mount", initContainer.Name) + Expect(initContainer.VolumeMounts).To(ContainElement( + HaveField("MountPath", GetTlsPath(OfflineFeastType)), + ), "init container %s should have offline TLS mount", initContainer.Name) + } + // registry service w/ tls and in an openshift cluster feast.Handler.FeatureStore = minimalFeatureStore() - feast.Handler.FeatureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{}, + feast.Handler.FeatureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{}, }, }, - UI: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{}, + UI: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{}, }, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ SecretRef: &corev1.LocalObjectReference{}, - SecretKeyNames: feastdevv1alpha1.SecretKeyNames{ + SecretKeyNames: feastdevv1.SecretKeyNames{ TlsCrt: "test.crt", }, }, @@ -238,21 +250,21 @@ var _ = Describe("TLS Config", func() { // all services w/ tls and in an openshift cluster feast.Handler.FeatureStore = minimalFeatureStoreWithAllServers() disable := true - feast.Handler.FeatureStore.Spec.Services.OnlineStore = &feastdevv1alpha1.OnlineStore{ - Server: &feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + feast.Handler.FeatureStore.Spec.Services.OnlineStore = &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ Disable: &disable, }, }, } - feast.Handler.FeatureStore.Spec.Services.UI.TLS = &feastdevv1alpha1.TlsConfigs{ + feast.Handler.FeatureStore.Spec.Services.UI.TLS = &feastdevv1.TlsConfigs{ Disable: &disable, } - feast.Handler.FeatureStore.Spec.Services.Registry = &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{ - TLS: &feastdevv1alpha1.TlsConfigs{ + feast.Handler.FeatureStore.Spec.Services.Registry = &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{ + TLS: &feastdevv1.TlsConfigs{ Disable: &disable, }, }, @@ -262,8 +274,7 @@ var _ = Describe("TLS Config", func() { err = feast.ApplyDefaults() Expect(err).ToNot(HaveOccurred()) - repoConfig, err = getClientRepoConfig(feast.Handler.FeatureStore, emptyMockExtractConfigFromSecret, &feast) - Expect(err).NotTo(HaveOccurred()) + repoConfig = getClientRepoConfig(feast.Handler.FeatureStore, &feast) Expect(repoConfig.OfflineStore.Port).To(Equal(HttpsPort)) Expect(repoConfig.OfflineStore.Scheme).To(Equal(HttpsScheme)) Expect(repoConfig.OfflineStore.Cert).To(ContainSubstring(string(OfflineFeastType))) @@ -336,15 +347,28 @@ var _ = Describe("TLS Config", func() { Expect(GetUIContainer(*feastDeploy).Command).NotTo(ContainElements(ContainSubstring("--key"))) Expect(GetUIContainer(*feastDeploy).VolumeMounts).To(HaveLen(1)) + // verify init containers receive only the offline TLS mount when only offline has TLS + for _, initContainer := range feastDeploy.Spec.Template.Spec.InitContainers { + Expect(initContainer.VolumeMounts).To(ContainElement( + HaveField("MountPath", GetTlsPath(OfflineFeastType)), + ), "init container %s should have offline TLS mount", initContainer.Name) + Expect(initContainer.VolumeMounts).NotTo(ContainElement( + HaveField("MountPath", GetTlsPath(RegistryFeastType)), + ), "init container %s should not have registry TLS mount when registry TLS is disabled", initContainer.Name) + Expect(initContainer.VolumeMounts).NotTo(ContainElement( + HaveField("MountPath", GetTlsPath(OnlineFeastType)), + ), "init container %s should not have online TLS mount when online TLS is disabled", initContainer.Name) + } + // Test REST registry server TLS configuration feast.Handler.FeatureStore = minimalFeatureStore() restEnabled := true grpcEnabled := false - feast.Handler.FeatureStore.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ - ServerConfigs: feastdevv1alpha1.ServerConfigs{}, + feast.Handler.FeatureStore.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ + ServerConfigs: feastdevv1.ServerConfigs{}, RestAPI: &restEnabled, GRPC: &grpcEnabled, }, diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 662308056e2..84951f2077b 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -21,23 +21,24 @@ import ( ) var isOpenShift = false +var hasServiceMonitorCRD = false -func IsRegistryServer(featureStore *feastdevv1alpha1.FeatureStore) bool { +func IsRegistryServer(featureStore *feastdevv1.FeatureStore) bool { return IsLocalRegistry(featureStore) && featureStore.Status.Applied.Services.Registry.Local.Server != nil } -func IsLocalRegistry(featureStore *feastdevv1alpha1.FeatureStore) bool { +func IsLocalRegistry(featureStore *feastdevv1.FeatureStore) bool { appliedServices := featureStore.Status.Applied.Services return appliedServices != nil && appliedServices.Registry != nil && appliedServices.Registry.Local != nil } -func isRemoteRegistry(featureStore *feastdevv1alpha1.FeatureStore) bool { +func isRemoteRegistry(featureStore *feastdevv1.FeatureStore) bool { appliedServices := featureStore.Status.Applied.Services return appliedServices != nil && appliedServices.Registry != nil && appliedServices.Registry.Remote != nil } -func hasPvcConfig(featureStore *feastdevv1alpha1.FeatureStore, feastType FeastServiceType) (*feastdevv1alpha1.PvcConfig, bool) { - var pvcConfig *feastdevv1alpha1.PvcConfig +func hasPvcConfig(featureStore *feastdevv1.FeatureStore, feastType FeastServiceType) (*feastdevv1.PvcConfig, bool) { + var pvcConfig *feastdevv1.PvcConfig services := featureStore.Status.Applied.Services if services != nil { switch feastType { @@ -61,14 +62,14 @@ func hasPvcConfig(featureStore *feastdevv1alpha1.FeatureStore, feastType FeastSe return pvcConfig, pvcConfig != nil } -func shouldCreatePvc(featureStore *feastdevv1alpha1.FeatureStore, feastType FeastServiceType) (*feastdevv1alpha1.PvcCreate, bool) { +func shouldCreatePvc(featureStore *feastdevv1.FeatureStore, feastType FeastServiceType) (*feastdevv1.PvcCreate, bool) { if pvcConfig, ok := hasPvcConfig(featureStore, feastType); ok { return pvcConfig.Create, pvcConfig.Create != nil } return nil, false } -func shouldMountEmptyDir(featureStore *feastdevv1alpha1.FeatureStore) bool { +func shouldMountEmptyDir(featureStore *feastdevv1.FeatureStore) bool { for _, feastType := range feastServerTypes { if _, ok := hasPvcConfig(featureStore, feastType); !ok { return true @@ -77,43 +78,47 @@ func shouldMountEmptyDir(featureStore *feastdevv1alpha1.FeatureStore) bool { return false } -func getOfflineMountPath(featureStore *feastdevv1alpha1.FeatureStore) string { +func getOfflineMountPath(featureStore *feastdevv1.FeatureStore) string { if pvcConfig, ok := hasPvcConfig(featureStore, OfflineFeastType); ok { return pvcConfig.MountPath } return EphemeralPath } -func ApplyDefaultsToStatus(cr *feastdevv1alpha1.FeatureStore) { +func ApplyDefaultsToStatus(cr *feastdevv1.FeatureStore) { // overwrite status.applied with every reconcile cr.Spec.DeepCopyInto(&cr.Status.Applied) cr.Status.FeastVersion = feastversion.FeastVersion applied := &cr.Status.Applied if applied.FeastProjectDir == nil { - applied.FeastProjectDir = &feastdevv1alpha1.FeastProjectDir{ - Init: &feastdevv1alpha1.FeastInitOptions{}, + applied.FeastProjectDir = &feastdevv1.FeastProjectDir{ + Init: &feastdevv1.FeastInitOptions{}, } } if applied.Services == nil { - applied.Services = &feastdevv1alpha1.FeatureStoreServices{} + applied.Services = &feastdevv1.FeatureStoreServices{} } + defaultFeatureServerImage := getFeatureServerImageForSpec(applied) services := applied.Services + if services.RunFeastApplyOnInit == nil { + services.RunFeastApplyOnInit = boolPtr(true) + } if services.Registry != nil { // if remote registry not set, proceed w/ local registry defaults if services.Registry.Remote == nil { // if local registry not set, apply an empty pointer struct if services.Registry.Local == nil { - services.Registry.Local = &feastdevv1alpha1.LocalRegistryConfig{} + services.Registry.Local = &feastdevv1.LocalRegistryConfig{} } if services.Registry.Local.Persistence == nil { - services.Registry.Local.Persistence = &feastdevv1alpha1.RegistryPersistence{} + services.Registry.Local.Persistence = &feastdevv1.RegistryPersistence{} } if services.Registry.Local.Persistence.DBPersistence == nil { if services.Registry.Local.Persistence.FilePersistence == nil { - services.Registry.Local.Persistence.FilePersistence = &feastdevv1alpha1.RegistryFilePersistence{} + services.Registry.Local.Persistence.FilePersistence = &feastdevv1.RegistryFilePersistence{} } if len(services.Registry.Local.Persistence.FilePersistence.Path) == 0 { @@ -124,7 +129,7 @@ func ApplyDefaultsToStatus(cr *feastdevv1alpha1.FeatureStore) { } if services.Registry.Local.Server != nil { - setDefaultCtrConfigs(&services.Registry.Local.Server.ContainerConfigs.DefaultCtrConfigs) + setDefaultCtrConfigs(&services.Registry.Local.Server.ContainerConfigs.DefaultCtrConfigs, defaultFeatureServerImage) // Set default for GRPC: true if nil if services.Registry.Local.Server.GRPC == nil { defaultGRPC := true @@ -139,12 +144,12 @@ func ApplyDefaultsToStatus(cr *feastdevv1alpha1.FeatureStore) { if services.OfflineStore != nil { if services.OfflineStore.Persistence == nil { - services.OfflineStore.Persistence = &feastdevv1alpha1.OfflineStorePersistence{} + services.OfflineStore.Persistence = &feastdevv1.OfflineStorePersistence{} } if services.OfflineStore.Persistence.DBPersistence == nil { if services.OfflineStore.Persistence.FilePersistence == nil { - services.OfflineStore.Persistence.FilePersistence = &feastdevv1alpha1.OfflineStoreFilePersistence{} + services.OfflineStore.Persistence.FilePersistence = &feastdevv1.OfflineStoreFilePersistence{} } if len(services.OfflineStore.Persistence.FilePersistence.Type) == 0 { @@ -155,52 +160,61 @@ func ApplyDefaultsToStatus(cr *feastdevv1alpha1.FeatureStore) { } if services.OfflineStore.Server != nil { - setDefaultCtrConfigs(&services.OfflineStore.Server.ContainerConfigs.DefaultCtrConfigs) + setDefaultCtrConfigs(&services.OfflineStore.Server.ContainerConfigs.DefaultCtrConfigs, defaultFeatureServerImage) } } - // default to onlineStore service deployment + // default to onlineStore service deployment unless it is explicitly disabled if services.OnlineStore == nil { - services.OnlineStore = &feastdevv1alpha1.OnlineStore{} - } - if services.OnlineStore.Persistence == nil { - services.OnlineStore.Persistence = &feastdevv1alpha1.OnlineStorePersistence{} + services.OnlineStore = &feastdevv1.OnlineStore{} } - - if services.OnlineStore.Persistence.DBPersistence == nil { - if services.OnlineStore.Persistence.FilePersistence == nil { - services.OnlineStore.Persistence.FilePersistence = &feastdevv1alpha1.OnlineStoreFilePersistence{} + if !services.OnlineStore.Disabled { + if services.OnlineStore.Persistence == nil { + services.OnlineStore.Persistence = &feastdevv1.OnlineStorePersistence{} } - if len(services.OnlineStore.Persistence.FilePersistence.Path) == 0 { - services.OnlineStore.Persistence.FilePersistence.Path = defaultOnlineStorePath(cr) - } + if services.OnlineStore.Persistence.DBPersistence == nil { + if services.OnlineStore.Persistence.FilePersistence == nil { + services.OnlineStore.Persistence.FilePersistence = &feastdevv1.OnlineStoreFilePersistence{} + } - ensurePVCDefaults(services.OnlineStore.Persistence.FilePersistence.PvcConfig, OnlineFeastType) - } + if len(services.OnlineStore.Persistence.FilePersistence.Path) == 0 { + services.OnlineStore.Persistence.FilePersistence.Path = defaultOnlineStorePath(cr) + } + + ensurePVCDefaults(services.OnlineStore.Persistence.FilePersistence.PvcConfig, OnlineFeastType) + } - if services.OnlineStore.Server == nil { - services.OnlineStore.Server = &feastdevv1alpha1.ServerConfigs{} + if services.OnlineStore.Server == nil { + services.OnlineStore.Server = &feastdevv1.ServerConfigs{} + } + setDefaultCtrConfigs(&services.OnlineStore.Server.ContainerConfigs.DefaultCtrConfigs, defaultFeatureServerImage) } - setDefaultCtrConfigs(&services.OnlineStore.Server.ContainerConfigs.DefaultCtrConfigs) if services.UI != nil { - setDefaultCtrConfigs(&services.UI.ContainerConfigs.DefaultCtrConfigs) + setDefaultCtrConfigs(&services.UI.ContainerConfigs.DefaultCtrConfigs, defaultFeatureServerImage) } if applied.CronJob == nil { - applied.CronJob = &feastdevv1alpha1.FeastCronJob{} + applied.CronJob = &feastdevv1.FeastCronJob{} } setDefaultCronJobConfigs(applied.CronJob) } -func setDefaultCtrConfigs(defaultConfigs *feastdevv1alpha1.DefaultCtrConfigs) { +func setDefaultCtrConfigs(defaultConfigs *feastdevv1.DefaultCtrConfigs, defaultImage string) { if defaultConfigs.Image == nil { - img := getFeatureServerImage() + img := defaultImage defaultConfigs.Image = &img } } +func getFeatureServerImageForSpec(spec *feastdevv1.FeatureStoreSpec) string { + if spec != nil && spec.FeastProjectDir != nil && spec.FeastProjectDir.Packaged != nil && spec.FeastProjectDir.Packaged.Image != "" { + return spec.FeastProjectDir.Packaged.Image + } + return getFeatureServerImage() +} + func getFeatureServerImage() string { if img, exists := os.LookupEnv(feastServerImageVar); exists { return img @@ -208,8 +222,18 @@ func getFeatureServerImage() string { return DefaultImage } +// getInitContainerImage resolves the image for feast-init / feast-apply. +// Order: spec.services.initImage → spec.feastProjectDir.packaged.image → +// RELATED_IMAGE_FEATURE_SERVER → DefaultImage. +func getInitContainerImage(spec *feastdevv1.FeatureStoreSpec) string { + if spec != nil && spec.Services != nil && spec.Services.InitImage != nil && len(*spec.Services.InitImage) > 0 { + return *spec.Services.InitImage + } + return getFeatureServerImageForSpec(spec) +} + func checkOfflineStoreFilePersistenceType(value string) error { - if slices.Contains(feastdevv1alpha1.ValidOfflineStoreFilePersistenceTypes, value) { + if slices.Contains(feastdevv1.ValidOfflineStoreFilePersistenceTypes, value) { return nil } return fmt.Errorf("invalid file type %s for offline store", value) @@ -224,7 +248,7 @@ func ensureRequestedStorage(resources *corev1.VolumeResourceRequirements, reques } } -func ensurePVCDefaults(pvc *feastdevv1alpha1.PvcConfig, feastType FeastServiceType) { +func ensurePVCDefaults(pvc *feastdevv1.PvcConfig, feastType FeastServiceType) { if pvc != nil { var storageRequest string switch feastType { @@ -244,7 +268,7 @@ func ensurePVCDefaults(pvc *feastdevv1alpha1.PvcConfig, feastType FeastServiceTy } } -func defaultOnlineStorePath(featureStore *feastdevv1alpha1.FeatureStore) string { +func defaultOnlineStorePath(featureStore *feastdevv1.FeatureStore) string { if _, ok := hasPvcConfig(featureStore, OnlineFeastType); ok { return DefaultOnlineStorePath } @@ -252,7 +276,7 @@ func defaultOnlineStorePath(featureStore *feastdevv1alpha1.FeatureStore) string return EphemeralPath + "/" + DefaultOnlineStorePath } -func defaultRegistryPath(featureStore *feastdevv1alpha1.FeatureStore) string { +func defaultRegistryPath(featureStore *feastdevv1.FeatureStore) string { if _, ok := hasPvcConfig(featureStore, RegistryFeastType); ok { return DefaultRegistryPath } @@ -261,21 +285,21 @@ func defaultRegistryPath(featureStore *feastdevv1alpha1.FeatureStore) string { } func checkOfflineStoreDBStorePersistenceType(value string) error { - if slices.Contains(feastdevv1alpha1.ValidOfflineStoreDBStorePersistenceTypes, value) { + if slices.Contains(feastdevv1.ValidOfflineStoreDBStorePersistenceTypes, value) { return nil } return fmt.Errorf("invalid DB store type %s for offline store", value) } func checkOnlineStoreDBStorePersistenceType(value string) error { - if slices.Contains(feastdevv1alpha1.ValidOnlineStoreDBStorePersistenceTypes, value) { + if slices.Contains(feastdevv1.ValidOnlineStoreDBStorePersistenceTypes, value) { return nil } return fmt.Errorf("invalid DB store type %s for online store", value) } func checkRegistryDBStorePersistenceType(value string) error { - if slices.Contains(feastdevv1alpha1.ValidRegistryDBStorePersistenceTypes, value) { + if slices.Contains(feastdevv1.ValidRegistryDBStorePersistenceTypes, value) { return nil } return fmt.Errorf("invalid DB store type %s for registry", value) @@ -295,12 +319,25 @@ func (feast *FeastServices) getSecret(secretRef string) (*corev1.Secret, error) return secret, nil } +func (feast *FeastServices) getConfigMap(configMapRef string) (*corev1.ConfigMap, error) { + logger := log.FromContext(feast.Handler.Context) + configMap := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: configMapRef, Namespace: feast.Handler.FeatureStore.Namespace}} + objectKey := client.ObjectKeyFromObject(configMap) + if err := feast.Handler.Client.Get(feast.Handler.Context, objectKey, configMap); err != nil { + if apierrors.IsNotFound(err) { + logger.Error(err, "invalid configmap "+configMapRef+" for batch engine") + } + return nil, err + } + return configMap, nil +} + // Function to check if a struct has a specific field or field tag and sets the value in the field if empty func hasAttrib(s interface{}, fieldName string, value interface{}) (bool, error) { val := reflect.ValueOf(s) // Check that the object is a pointer so we can modify it - if val.Kind() != reflect.Ptr || val.IsNil() { + if val.Kind() != reflect.Pointer || val.IsNil() { return false, fmt.Errorf("expected a pointer to struct, got %v", val.Kind()) } @@ -358,6 +395,12 @@ func IsOpenShift() bool { return isOpenShift } +// HasServiceMonitorCRD returns whether the monitoring.coreos.com API group +// (Prometheus Operator) is available in the cluster. +func HasServiceMonitorCRD() bool { + return hasServiceMonitorCRD +} + // SetIsOpenShift sets the global flag isOpenShift by the controller manager. // We don't need to keep fetching the API every reconciliation cycle that we need to know about the platform. func SetIsOpenShift(cfg *rest.Config) { @@ -377,15 +420,13 @@ func SetIsOpenShift(cfg *rest.Config) { for _, v := range apiList.Groups { if v.Name == "route.openshift.io" { isOpenShift = true - break + } + if v.Name == "monitoring.coreos.com" { + hasServiceMonitorCRD = true } } } -func missingOidcSecretProperty(property OidcPropertyType) error { - return fmt.Errorf(OidcMissingSecretError, property) -} - // getEnvVar returns the position of the EnvVar found by name func getEnvVar(envName string, env []corev1.EnvVar) int { for pos, v := range env { @@ -438,19 +479,19 @@ func getContainerByType(feastType FeastServiceType, podSpec corev1.PodSpec) (int return -1, nil } -func GetRegistryVolume(featureStore *feastdevv1alpha1.FeatureStore, volumes []corev1.Volume) *corev1.Volume { +func GetRegistryVolume(featureStore *feastdevv1.FeatureStore, volumes []corev1.Volume) *corev1.Volume { return getVolumeByType(RegistryFeastType, featureStore, volumes) } -func GetOnlineVolume(featureStore *feastdevv1alpha1.FeatureStore, volumes []corev1.Volume) *corev1.Volume { +func GetOnlineVolume(featureStore *feastdevv1.FeatureStore, volumes []corev1.Volume) *corev1.Volume { return getVolumeByType(OnlineFeastType, featureStore, volumes) } -func GetOfflineVolume(featureStore *feastdevv1alpha1.FeatureStore, volumes []corev1.Volume) *corev1.Volume { +func GetOfflineVolume(featureStore *feastdevv1.FeatureStore, volumes []corev1.Volume) *corev1.Volume { return getVolumeByType(OfflineFeastType, featureStore, volumes) } -func getVolumeByType(feastType FeastServiceType, featureStore *feastdevv1alpha1.FeatureStore, volumes []corev1.Volume) *corev1.Volume { +func getVolumeByType(feastType FeastServiceType, featureStore *feastdevv1.FeatureStore, volumes []corev1.Volume) *corev1.Volume { for _, v := range volumes { if v.Name == GetFeastServiceName(featureStore, feastType) { return &v @@ -459,19 +500,19 @@ func getVolumeByType(feastType FeastServiceType, featureStore *feastdevv1alpha1. return nil } -func GetRegistryVolumeMount(featureStore *feastdevv1alpha1.FeatureStore, volumeMounts []corev1.VolumeMount) *corev1.VolumeMount { +func GetRegistryVolumeMount(featureStore *feastdevv1.FeatureStore, volumeMounts []corev1.VolumeMount) *corev1.VolumeMount { return getVolumeMountByType(RegistryFeastType, featureStore, volumeMounts) } -func GetOnlineVolumeMount(featureStore *feastdevv1alpha1.FeatureStore, volumeMounts []corev1.VolumeMount) *corev1.VolumeMount { +func GetOnlineVolumeMount(featureStore *feastdevv1.FeatureStore, volumeMounts []corev1.VolumeMount) *corev1.VolumeMount { return getVolumeMountByType(OnlineFeastType, featureStore, volumeMounts) } -func GetOfflineVolumeMount(featureStore *feastdevv1alpha1.FeatureStore, volumeMounts []corev1.VolumeMount) *corev1.VolumeMount { +func GetOfflineVolumeMount(featureStore *feastdevv1.FeatureStore, volumeMounts []corev1.VolumeMount) *corev1.VolumeMount { return getVolumeMountByType(OfflineFeastType, featureStore, volumeMounts) } -func getVolumeMountByType(feastType FeastServiceType, featureStore *feastdevv1alpha1.FeatureStore, volumeMounts []corev1.VolumeMount) *corev1.VolumeMount { +func getVolumeMountByType(feastType FeastServiceType, featureStore *feastdevv1.FeatureStore, volumeMounts []corev1.VolumeMount) *corev1.VolumeMount { for _, vm := range volumeMounts { if vm.Name == GetFeastServiceName(featureStore, feastType) { return &vm @@ -480,6 +521,16 @@ func getVolumeMountByType(feastType FeastServiceType, featureStore *feastdevv1al return nil } +// isScalingEnabled returns true when the user has configured horizontal scaling +// with either static replicas > 1 or HPA autoscaling. +func isScalingEnabled(featureStore *feastdevv1.FeatureStore) bool { + if featureStore.Status.Applied.Replicas != nil && *featureStore.Status.Applied.Replicas > 1 { + return true + } + services := featureStore.Status.Applied.Services + return services != nil && services.Scaling != nil && services.Scaling.Autoscaling != nil +} + func boolPtr(value bool) *bool { return &value } diff --git a/infra/feast-operator/internal/controller/services/util_test.go b/infra/feast-operator/internal/controller/services/util_test.go new file mode 100644 index 00000000000..5a868d2d101 --- /dev/null +++ b/infra/feast-operator/internal/controller/services/util_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package services + +import ( + "os" + "testing" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/utils/ptr" +) + +var _ = Describe("ApplyDefaultsToStatus", func() { + It("deploys the online store with defaults when it is not declared", func() { + cr := &feastdevv1.FeatureStore{ + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "test_project", + Services: &feastdevv1.FeatureStoreServices{}, + }, + } + + ApplyDefaultsToStatus(cr) + + online := cr.Status.Applied.Services.OnlineStore + Expect(online).ToNot(BeNil()) + Expect(online.Disabled).To(BeFalse()) + Expect(online.Persistence).ToNot(BeNil()) + Expect(online.Server).ToNot(BeNil()) + }) + + It("applies online store defaults when it is declared", func() { + cr := &feastdevv1.FeatureStore{ + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "test_project", + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{}, + }, + }, + } + + ApplyDefaultsToStatus(cr) + + online := cr.Status.Applied.Services.OnlineStore + Expect(online).ToNot(BeNil()) + Expect(online.Persistence).ToNot(BeNil()) + Expect(online.Server).ToNot(BeNil()) + }) + + // #6586: disabling the online store opts out of its persistence and serving + // pod, letting a registry-only or offline-only ViewerStore skip it while + // leaving the default-on behavior unchanged for everyone else. + It("does not apply persistence or server defaults when the online store is disabled", func() { + cr := &feastdevv1.FeatureStore{ + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "test_project", + Services: &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{Disabled: true}, + }, + }, + } + + ApplyDefaultsToStatus(cr) + + online := cr.Status.Applied.Services.OnlineStore + Expect(online).ToNot(BeNil()) + Expect(online.Disabled).To(BeTrue()) + Expect(online.Persistence).To(BeNil()) + Expect(online.Server).To(BeNil()) + }) +}) + +func TestGetInitContainerImage(t *testing.T) { + customInit := "quay.io/org/feast-init:custom" + packagedImage := "quay.io/org/feast-packaged:test" + envImage := "quay.io/org/feast-env:test" + + t.Run("uses initImage ahead of packaged and server images", func(t *testing.T) { + t.Setenv(feastServerImageVar, envImage) + got := getInitContainerImage(&feastdevv1.FeatureStoreSpec{ + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{Image: packagedImage}, + }, + Services: &feastdevv1.FeatureStoreServices{ + InitImage: ptr.To(customInit), + OfflineStore: &feastdevv1.OfflineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("quay.io/org/offline:v1"), + }, + }, + }, + }, + OnlineStore: &feastdevv1.OnlineStore{ + Server: &feastdevv1.ServerConfigs{ + ContainerConfigs: feastdevv1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1.DefaultCtrConfigs{ + Image: ptr.To("quay.io/org/online:v1"), + }, + }, + }, + }, + }, + }) + if got != customInit { + t.Fatalf("got %q, want %q (must not inherit server images)", got, customInit) + } + }) + + t.Run("uses packaged image ahead of RELATED_IMAGE_FEATURE_SERVER", func(t *testing.T) { + t.Setenv(feastServerImageVar, envImage) + got := getInitContainerImage(&feastdevv1.FeatureStoreSpec{ + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{Image: packagedImage}, + }, + Services: &feastdevv1.FeatureStoreServices{}, + }) + if got != packagedImage { + t.Fatalf("got %q, want %q", got, packagedImage) + } + }) + + t.Run("falls back to RELATED_IMAGE_FEATURE_SERVER", func(t *testing.T) { + t.Setenv(feastServerImageVar, envImage) + got := getInitContainerImage(&feastdevv1.FeatureStoreSpec{ + Services: &feastdevv1.FeatureStoreServices{}, + }) + if got != envImage { + t.Fatalf("got %q, want %q", got, envImage) + } + }) + + t.Run("falls back to DefaultImage", func(t *testing.T) { + _ = os.Unsetenv(feastServerImageVar) + got := getInitContainerImage(nil) + if got != DefaultImage { + t.Fatalf("got %q, want %q", got, DefaultImage) + } + }) + + t.Run("ignores empty initImage", func(t *testing.T) { + t.Setenv(feastServerImageVar, envImage) + got := getInitContainerImage(&feastdevv1.FeatureStoreSpec{ + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{Image: packagedImage}, + }, + Services: &feastdevv1.FeatureStoreServices{ + InitImage: ptr.To(""), + }, + }) + if got != packagedImage { + t.Fatalf("got %q, want %q", got, packagedImage) + } + }) +} diff --git a/infra/feast-operator/internal/controller/suite_test.go b/infra/feast-operator/internal/controller/suite_test.go index 127bb7fe3b3..5a266abc8ba 100644 --- a/infra/feast-operator/internal/controller/suite_test.go +++ b/infra/feast-operator/internal/controller/suite_test.go @@ -35,7 +35,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" // +kubebuilder:scaffold:imports ) @@ -73,7 +73,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - err = feastdevv1alpha1.AddToScheme(scheme.Scheme) + err = feastdevv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme diff --git a/infra/feast-operator/test/api/featurestore_packaged_types_test.go b/infra/feast-operator/test/api/featurestore_packaged_types_test.go new file mode 100644 index 00000000000..97525ec1abe --- /dev/null +++ b/infra/feast-operator/test/api/featurestore_packaged_types_test.go @@ -0,0 +1,162 @@ +/* +Copyright 2026 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "context" + "strings" + + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type packagedFeatureStoreFactory func(name, featureRepoPath string) client.Object +type conflictingPackagedFeatureStoreFactory func(name, conflictingMode string) client.Object + +func newV1PackagedFeatureStore(name, featureRepoPath string) client.Object { + return &feastdevv1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespaceName}, + Spec: feastdevv1.FeatureStoreSpec{ + FeastProject: "test_project", + FeastProjectDir: &feastdevv1.FeastProjectDir{ + Packaged: &feastdevv1.FeastPackagedOptions{FeatureRepoPath: featureRepoPath}, + }, + }, + } +} + +func newV1Alpha1PackagedFeatureStore(name, featureRepoPath string) client.Object { + return &feastdevv1alpha1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespaceName}, + Spec: feastdevv1alpha1.FeatureStoreSpec{ + FeastProject: "test_project", + FeastProjectDir: &feastdevv1alpha1.FeastProjectDir{ + Packaged: &feastdevv1alpha1.FeastPackagedOptions{FeatureRepoPath: featureRepoPath}, + }, + }, + } +} + +func newV1ConflictingPackagedFeatureStore(name, conflictingMode string) client.Object { + featureStore := newV1PackagedFeatureStore(name, "/opt/feast/feature_repo").(*feastdevv1.FeatureStore) + switch conflictingMode { + case "init": + featureStore.Spec.FeastProjectDir.Init = &feastdevv1.FeastInitOptions{} + case "git": + featureStore.Spec.FeastProjectDir.Git = &feastdevv1.GitCloneOptions{ + URL: "https://example.com/feature-repo.git", + } + } + return featureStore +} + +func newV1Alpha1ConflictingPackagedFeatureStore(name, conflictingMode string) client.Object { + featureStore := newV1Alpha1PackagedFeatureStore(name, "/opt/feast/feature_repo").(*feastdevv1alpha1.FeatureStore) + switch conflictingMode { + case "init": + featureStore.Spec.FeastProjectDir.Init = &feastdevv1alpha1.FeastInitOptions{} + case "git": + featureStore.Spec.FeastProjectDir.Git = &feastdevv1alpha1.GitCloneOptions{ + URL: "https://example.com/feature-repo.git", + } + } + return featureStore +} + +var _ = Describe("Packaged feature repository path validation", func() { + ctx := context.Background() + apiVersions := []struct { + name string + id string + factory packagedFeatureStoreFactory + conflictingFactory conflictingPackagedFeatureStoreFactory + }{ + { + name: "feast.dev/v1", + id: "v1", + factory: newV1PackagedFeatureStore, + conflictingFactory: newV1ConflictingPackagedFeatureStore, + }, + { + name: "feast.dev/v1alpha1", + id: "v1alpha1", + factory: newV1Alpha1PackagedFeatureStore, + conflictingFactory: newV1Alpha1ConflictingPackagedFeatureStore, + }, + } + + for _, apiVersion := range apiVersions { + apiVersion := apiVersion + Context(apiVersion.name, func() { + DescribeTable("accepts canonical absolute non-root paths", + func(nameSuffix, featureRepoPath string) { + featureStore := apiVersion.factory( + "packaged-"+apiVersion.id+"-"+nameSuffix, + featureRepoPath, + ) + Expect(k8sClient.Create(ctx, featureStore)).To(Succeed()) + Expect(k8sClient.Delete(ctx, featureStore)).To(Succeed()) + }, + Entry("standard", "standard", "/opt/feast/feature_repo"), + Entry("hidden component", "hidden", "/opt/.feast/feature_repo"), + Entry("dot in component", "dot-name", "/opt/feature_repo.v2"), + ) + + DescribeTable("rejects non-canonical, relative, or root paths", + func(nameSuffix, featureRepoPath string) { + featureStore := apiVersion.factory( + "packaged-"+apiVersion.id+"-"+nameSuffix, + featureRepoPath, + ) + err := k8sClient.Create(ctx, featureStore) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsInvalid(err)).To(BeTrue(), "expected invalid error, got %v", err) + Expect(strings.ToLower(err.Error())).To(ContainSubstring("canonical absolute, non-root path")) + }, + Entry("relative", "relative", "opt/feast/feature_repo"), + Entry("root", "root", "/"), + Entry("parent collapses to root", "parent-root", "/opt/.."), + Entry("leading parent traversal", "leading-parent", "/../x"), + Entry("repeated separator", "repeated-separator", "/opt//feature_repo"), + Entry("current-directory component", "current-dir", "/opt/./feature_repo"), + Entry("trailing separator", "trailing-separator", "/opt/feature_repo/"), + Entry("nested traversal", "nested-traversal", "/a/../../etc"), + Entry("repeated root separator", "repeated-root", "//"), + ) + + DescribeTable("rejects packaged together with another project directory mode", + func(nameSuffix, conflictingMode string) { + featureStore := apiVersion.conflictingFactory( + "packaged-"+apiVersion.id+"-"+nameSuffix, + conflictingMode, + ) + err := k8sClient.Create(ctx, featureStore) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsInvalid(err)).To(BeTrue(), "expected invalid error, got %v", err) + Expect(err.Error()).To(ContainSubstring("One selection required between init, git, or packaged")) + }, + Entry("init", "with-init", "init"), + Entry("git", "with-git", "git"), + ) + }) + } +}) diff --git a/infra/feast-operator/test/api/featurestore_types_test.go b/infra/feast-operator/test/api/featurestore_types_test.go index e8b08b549d0..00312e0fabb 100644 --- a/infra/feast-operator/test/api/featurestore_types_test.go +++ b/infra/feast-operator/test/api/featurestore_types_test.go @@ -12,7 +12,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/log" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,19 +22,19 @@ func boolPtr(b bool) *bool { return &b } -func createFeatureStore() *feastdevv1alpha1.FeatureStore { - return &feastdevv1alpha1.FeatureStore{ +func createFeatureStore() *feastdevv1.FeatureStore { + return &feastdevv1.FeatureStore{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: namespaceName, }, - Spec: feastdevv1alpha1.FeatureStoreSpec{ + Spec: feastdevv1.FeatureStoreSpec{ FeastProject: "test_project", }, } } -func attemptInvalidCreationAndAsserts(ctx context.Context, featurestore *feastdevv1alpha1.FeatureStore, matcher string) { +func attemptInvalidCreationAndAsserts(ctx context.Context, featurestore *feastdevv1.FeatureStore, matcher string) { By("Creating the resource") logger := log.FromContext(ctx) logger.Info("Creating", "FeatureStore", featurestore) @@ -44,26 +44,26 @@ func attemptInvalidCreationAndAsserts(ctx context.Context, featurestore *feastde Expect(err.Error()).Should(ContainSubstring(matcher)) } -func onlineStoreWithAbsolutePathForPvc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func onlineStoreWithAbsolutePathForPvc(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Persistence: &feastdevv1alpha1.OnlineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ Path: "/data/online_store.db", - PvcConfig: &feastdevv1alpha1.PvcConfig{}, + PvcConfig: &feastdevv1.PvcConfig{}, }, }, }, } return fsCopy } -func onlineStoreWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func onlineStoreWithRelativePathForEphemeral(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Persistence: &feastdevv1alpha1.OnlineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ Path: "data/online_store.db", }, }, @@ -72,15 +72,15 @@ func onlineStoreWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.Feat return fsCopy } -func onlineStoreWithObjectStoreBucketForPvc(path string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func onlineStoreWithObjectStoreBucketForPvc(path string, featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Persistence: &feastdevv1alpha1.OnlineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ Path: path, - PvcConfig: &feastdevv1alpha1.PvcConfig{ - Create: &feastdevv1alpha1.PvcCreate{}, + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{}, MountPath: "/data/online", }, }, @@ -90,12 +90,12 @@ func onlineStoreWithObjectStoreBucketForPvc(path string, featureStore *feastdevv return fsCopy } -func offlineStoreWithUnmanagedFileType(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func offlineStoreWithUnmanagedFileType(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Persistence: &feastdevv1alpha1.OfflineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ Type: "unmanaged", }, }, @@ -104,28 +104,28 @@ func offlineStoreWithUnmanagedFileType(featureStore *feastdevv1alpha1.FeatureSto return fsCopy } -func registryWithAbsolutePathForPvc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithAbsolutePathForPvc(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: "/data/registry.db", - PvcConfig: &feastdevv1alpha1.PvcConfig{}, + PvcConfig: &feastdevv1.PvcConfig{}, }}, }, }, } return fsCopy } -func registryWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithRelativePathForEphemeral(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: "data/online_store.db", }, }, @@ -134,16 +134,16 @@ func registryWithRelativePathForEphemeral(featureStore *feastdevv1alpha1.Feature } return fsCopy } -func registryWithObjectStoreBucketForPvc(path string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithObjectStoreBucketForPvc(path string, featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: path, - PvcConfig: &feastdevv1alpha1.PvcConfig{ - Create: &feastdevv1alpha1.PvcCreate{}, + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{}, MountPath: "/data/registry", }, }, @@ -153,13 +153,13 @@ func registryWithObjectStoreBucketForPvc(path string, featureStore *feastdevv1al } return fsCopy } -func registryWithS3AdditionalKeywordsForFile(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithS3AdditionalKeywordsForFile(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: "/data/online_store.db", S3AdditionalKwargs: &map[string]string{}, }, @@ -169,13 +169,13 @@ func registryWithS3AdditionalKeywordsForFile(featureStore *feastdevv1alpha1.Feat } return fsCopy } -func registryWithS3AdditionalKeywordsForGsBucket(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithS3AdditionalKeywordsForGsBucket(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ Path: "gs://online_store.db", S3AdditionalKwargs: &map[string]string{}, }, @@ -186,30 +186,30 @@ func registryWithS3AdditionalKeywordsForGsBucket(featureStore *feastdevv1alpha1. return fsCopy } -func pvcConfigWithNeitherRefNorCreate(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func pvcConfigWithNeitherRefNorCreate(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Persistence: &feastdevv1alpha1.OfflineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ - PvcConfig: &feastdevv1alpha1.PvcConfig{}, + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ + PvcConfig: &feastdevv1.PvcConfig{}, }, }, }, } return fsCopy } -func pvcConfigWithBothRefAndCreate(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func pvcConfigWithBothRefAndCreate(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Persistence: &feastdevv1alpha1.OfflineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ - PvcConfig: &feastdevv1alpha1.PvcConfig{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ + PvcConfig: &feastdevv1.PvcConfig{ Ref: &corev1.LocalObjectReference{ Name: "pvc", }, - Create: &feastdevv1alpha1.PvcCreate{}, + Create: &feastdevv1.PvcCreate{}, }, }, }, @@ -218,35 +218,35 @@ func pvcConfigWithBothRefAndCreate(featureStore *feastdevv1alpha1.FeatureStore) return fsCopy } -func pvcConfigWithNoResources(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func pvcConfigWithNoResources(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Persistence: &feastdevv1alpha1.OfflineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OfflineStoreFilePersistence{ - PvcConfig: &feastdevv1alpha1.PvcConfig{ - Create: &feastdevv1alpha1.PvcCreate{}, + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + FilePersistence: &feastdevv1.OfflineStoreFilePersistence{ + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{}, MountPath: "/data/offline", }, }, }, }, - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Persistence: &feastdevv1alpha1.OnlineStorePersistence{ - FilePersistence: &feastdevv1alpha1.OnlineStoreFilePersistence{ - PvcConfig: &feastdevv1alpha1.PvcConfig{ - Create: &feastdevv1alpha1.PvcCreate{}, + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + FilePersistence: &feastdevv1.OnlineStoreFilePersistence{ + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{}, MountPath: "/data/online", }, }, }, }, - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - FilePersistence: &feastdevv1alpha1.RegistryFilePersistence{ - PvcConfig: &feastdevv1alpha1.PvcConfig{ - Create: &feastdevv1alpha1.PvcCreate{}, + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + FilePersistence: &feastdevv1.RegistryFilePersistence{ + PvcConfig: &feastdevv1.PvcConfig{ + Create: &feastdevv1.PvcCreate{}, MountPath: "/data/registry", }, }, @@ -257,7 +257,7 @@ func pvcConfigWithNoResources(featureStore *feastdevv1alpha1.FeatureStore) *feas return fsCopy } -func pvcConfigWithResources(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func pvcConfigWithResources(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := pvcConfigWithNoResources(featureStore) fsCopy.Spec.Services.OfflineStore.Persistence.FilePersistence.PvcConfig.Create.Resources = corev1.VolumeResourceRequirements{ Requests: corev1.ResourceList{ @@ -277,31 +277,31 @@ func pvcConfigWithResources(featureStore *feastdevv1alpha1.FeatureStore) *feastd return fsCopy } -func authzConfigWithKubernetes(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func authzConfigWithKubernetes(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() if fsCopy.Spec.AuthzConfig == nil { - fsCopy.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{} + fsCopy.Spec.AuthzConfig = &feastdevv1.AuthzConfig{} } - fsCopy.Spec.AuthzConfig.KubernetesAuthz = &feastdevv1alpha1.KubernetesAuthz{ + fsCopy.Spec.AuthzConfig.KubernetesAuthz = &feastdevv1.KubernetesAuthz{ Roles: []string{}, } return fsCopy } -func authzConfigWithOidc(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func authzConfigWithOidc(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() if fsCopy.Spec.AuthzConfig == nil { - fsCopy.Spec.AuthzConfig = &feastdevv1alpha1.AuthzConfig{} + fsCopy.Spec.AuthzConfig = &feastdevv1.AuthzConfig{} } - fsCopy.Spec.AuthzConfig.OidcAuthz = &feastdevv1alpha1.OidcAuthz{} + fsCopy.Spec.AuthzConfig.OidcAuthz = &feastdevv1.OidcAuthz{} return fsCopy } -func onlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func onlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OnlineStore: &feastdevv1alpha1.OnlineStore{ - Persistence: &feastdevv1alpha1.OnlineStorePersistence{ - DBPersistence: &feastdevv1alpha1.OnlineStoreDBStorePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + OnlineStore: &feastdevv1.OnlineStore{ + Persistence: &feastdevv1.OnlineStorePersistence{ + DBPersistence: &feastdevv1.OnlineStoreDBStorePersistence{ Type: dbPersistenceType, }, }, @@ -310,12 +310,12 @@ func onlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *fe return fsCopy } -func offlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func offlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - OfflineStore: &feastdevv1alpha1.OfflineStore{ - Persistence: &feastdevv1alpha1.OfflineStorePersistence{ - DBPersistence: &feastdevv1alpha1.OfflineStoreDBStorePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + OfflineStore: &feastdevv1.OfflineStore{ + Persistence: &feastdevv1.OfflineStorePersistence{ + DBPersistence: &feastdevv1.OfflineStoreDBStorePersistence{ Type: dbPersistenceType, }, }, @@ -324,13 +324,13 @@ func offlineStoreWithDBPersistenceType(dbPersistenceType string, featureStore *f return fsCopy } -func registryStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryStoreWithDBPersistenceType(dbPersistenceType string, featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Persistence: &feastdevv1alpha1.RegistryPersistence{ - DBPersistence: &feastdevv1alpha1.RegistryDBStorePersistence{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Persistence: &feastdevv1.RegistryPersistence{ + DBPersistence: &feastdevv1.RegistryDBStorePersistence{ Type: dbPersistenceType, }, }, @@ -340,12 +340,12 @@ func registryStoreWithDBPersistenceType(dbPersistenceType string, featureStore * return fsCopy } -func registryWithRestAPIFalse(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithRestAPIFalse(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ RestAPI: boolPtr(false), }, }, @@ -354,12 +354,12 @@ func registryWithRestAPIFalse(featureStore *feastdevv1alpha1.FeatureStore) *feas return fsCopy } -func registryWithOnlyRestAPI(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithOnlyRestAPI(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ RestAPI: boolPtr(true), }, }, @@ -368,12 +368,12 @@ func registryWithOnlyRestAPI(featureStore *feastdevv1alpha1.FeatureStore) *feast return fsCopy } -func registryWithOnlyGRPC(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithOnlyGRPC(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ GRPC: boolPtr(true), }, }, @@ -382,12 +382,12 @@ func registryWithOnlyGRPC(featureStore *feastdevv1alpha1.FeatureStore) *feastdev return fsCopy } -func registryWithBothAPIs(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithBothAPIs(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ RestAPI: boolPtr(true), GRPC: boolPtr(true), }, @@ -397,24 +397,24 @@ func registryWithBothAPIs(featureStore *feastdevv1alpha1.FeatureStore) *feastdev return fsCopy } -func registryWithNoAPIs(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithNoAPIs(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{}, + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{}, }, }, } return fsCopy } -func registryWithBothFalse(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithBothFalse(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ RestAPI: boolPtr(false), GRPC: boolPtr(false), }, @@ -424,12 +424,12 @@ func registryWithBothFalse(featureStore *feastdevv1alpha1.FeatureStore) *feastde return fsCopy } -func registryWithGRPCFalse(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func registryWithGRPCFalse(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.Services = &feastdevv1alpha1.FeatureStoreServices{ - Registry: &feastdevv1alpha1.Registry{ - Local: &feastdevv1alpha1.LocalRegistryConfig{ - Server: &feastdevv1alpha1.RegistryServerConfigs{ + fsCopy.Spec.Services = &feastdevv1.FeatureStoreServices{ + Registry: &feastdevv1.Registry{ + Local: &feastdevv1.LocalRegistryConfig{ + Server: &feastdevv1.RegistryServerConfigs{ GRPC: boolPtr(false), }, }, @@ -438,31 +438,31 @@ func registryWithGRPCFalse(featureStore *feastdevv1alpha1.FeatureStore) *feastde return fsCopy } -func cronJobWithAnnotations(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func cronJobWithAnnotations(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.CronJob = &feastdevv1alpha1.FeastCronJob{ + fsCopy.Spec.CronJob = &feastdevv1.FeastCronJob{ Annotations: map[string]string{ "test-annotation": "test-value", "another-annotation": "another-value", }, - Schedule: "0 0 * * *", + Schedule: dailyMidnightCron, } return fsCopy } -func cronJobWithEmptyAnnotations(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func cronJobWithEmptyAnnotations(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.CronJob = &feastdevv1alpha1.FeastCronJob{ + fsCopy.Spec.CronJob = &feastdevv1.FeastCronJob{ Annotations: map[string]string{}, Schedule: "0 0 * * *", } return fsCopy } -func cronJobWithoutAnnotations(featureStore *feastdevv1alpha1.FeatureStore) *feastdevv1alpha1.FeatureStore { +func cronJobWithoutAnnotations(featureStore *feastdevv1.FeatureStore) *feastdevv1.FeatureStore { fsCopy := featureStore.DeepCopy() - fsCopy.Spec.CronJob = &feastdevv1alpha1.FeastCronJob{ - Schedule: "0 0 * * *", + fsCopy.Spec.CronJob = &feastdevv1.FeastCronJob{ + Schedule: dailyMidnightCron, } return fsCopy } @@ -477,15 +477,19 @@ func quotedSlice(stringSlice []string) string { return strings.Join(quotedSlice, ", ") } -const resourceName = "test-resource" -const namespaceName = "default" +const ( + resourceName = "test-resource" + namespaceName = "default" + defaultNs = "default" + dailyMidnightCron = "0 0 * * *" +) var typeNamespacedName = types.NamespacedName{ Name: resourceName, - Namespace: "default", + Namespace: defaultNs, } -func initContext() (context.Context, *feastdevv1alpha1.FeatureStore) { +func initContext() (context.Context, *feastdevv1.FeatureStore) { ctx := context.Background() featurestore := createFeatureStore() @@ -520,7 +524,7 @@ var _ = Describe("FeatureStore API", func() { }) It("should fail when db persistence type is invalid", func() { - attemptInvalidCreationAndAsserts(ctx, onlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: "+quotedSlice(feastdevv1alpha1.ValidOnlineStoreDBStorePersistenceTypes)) + attemptInvalidCreationAndAsserts(ctx, onlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: "+quotedSlice(feastdevv1.ValidOnlineStoreDBStorePersistenceTypes)) }) }) @@ -531,7 +535,7 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, offlineStoreWithUnmanagedFileType(featurestore), "Unsupported value") }) It("should fail when db persistence type is invalid", func() { - attemptInvalidCreationAndAsserts(ctx, offlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: "+quotedSlice(feastdevv1alpha1.ValidOfflineStoreDBStorePersistenceTypes)) + attemptInvalidCreationAndAsserts(ctx, offlineStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: "+quotedSlice(feastdevv1.ValidOfflineStoreDBStorePersistenceTypes)) }) }) @@ -553,7 +557,7 @@ var _ = Describe("FeatureStore API", func() { attemptInvalidCreationAndAsserts(ctx, registryWithS3AdditionalKeywordsForGsBucket(featurestore), "Additional S3 settings are available only for S3 object store URIs") }) It("should fail when db persistence type is invalid", func() { - attemptInvalidCreationAndAsserts(ctx, registryStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: "+quotedSlice(feastdevv1alpha1.ValidRegistryDBStorePersistenceTypes)) + attemptInvalidCreationAndAsserts(ctx, registryStoreWithDBPersistenceType("invalid", featurestore), "Unsupported value: \"invalid\": supported values: "+quotedSlice(feastdevv1.ValidRegistryDBStorePersistenceTypes)) }) }) @@ -613,13 +617,13 @@ var _ = Describe("FeatureStore API", func() { BeforeEach(func() { By("verifying the custom resource FeatureStore is not there") - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err != nil && errors.IsNotFound(err)).To(BeTrue()) }) AfterEach(func() { By("Cleaning up the test resource") - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) if err == nil { Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) @@ -680,13 +684,13 @@ var _ = Describe("FeatureStore API", func() { BeforeEach(func() { By("verifying the custom resource FeatureStore is not there") - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err != nil && errors.IsNotFound(err)).To(BeTrue()) }) AfterEach(func() { By("Cleaning up the test resource") - resource := &feastdevv1alpha1.FeatureStore{} + resource := &feastdevv1.FeatureStore{} err := k8sClient.Get(ctx, typeNamespacedName, resource) if err == nil { Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) diff --git a/infra/feast-operator/test/api/suite_test.go b/infra/feast-operator/test/api/suite_test.go index e8c46a240c1..eef4718cf58 100644 --- a/infra/feast-operator/test/api/suite_test.go +++ b/infra/feast-operator/test/api/suite_test.go @@ -25,6 +25,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -71,6 +72,8 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) + err = feastdevv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) err = feastdevv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) diff --git a/infra/feast-operator/test/data-source-types/data-source-types.py b/infra/feast-operator/test/data-source-types/data-source-types.py index be7d70e5ede..ccda6665286 100644 --- a/infra/feast-operator/test/data-source-types/data-source-types.py +++ b/infra/feast-operator/test/data-source-types/data-source-types.py @@ -1,13 +1,20 @@ import os -from feast.repo_config import REGISTRY_CLASS_FOR_TYPE, OFFLINE_STORE_CLASS_FOR_TYPE, ONLINE_STORE_CLASS_FOR_TYPE, LEGACY_ONLINE_STORE_CLASS_FOR_TYPE +from feast.repo_config import ( + REGISTRY_CLASS_FOR_TYPE, + OFFLINE_STORE_CLASS_FOR_TYPE, + ONLINE_STORE_CLASS_FOR_TYPE, + LEGACY_ONLINE_STORE_CLASS_FOR_TYPE, +) + def save_in_script_directory(filename: str, typedict: dict[str, str]): script_dir = os.path.dirname(os.path.abspath(__file__)) file_path = os.path.join(script_dir, filename) - - with open(file_path, 'w') as file: + + with open(file_path, "w") as file: for k in typedict.keys(): - file.write(k+"\n") + file.write(k + "\n") + for legacyType in LEGACY_ONLINE_STORE_CLASS_FOR_TYPE.keys(): if legacyType in ONLINE_STORE_CLASS_FOR_TYPE: diff --git a/infra/feast-operator/test/data-source-types/data_source_types_test.go b/infra/feast-operator/test/data-source-types/data_source_types_test.go index 8448b2c4212..09d71239349 100644 --- a/infra/feast-operator/test/data-source-types/data_source_types_test.go +++ b/infra/feast-operator/test/data-source-types/data_source_types_test.go @@ -9,7 +9,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" ) @@ -23,16 +23,16 @@ var _ = Describe("FeatureStore Data Source Types", func() { Context("When checking against the python code in feast.repo_config", func() { It("should match defined registry persistence types in the operator", func() { registryFilePersistenceTypes := []string{string(services.RegistryFileConfigType)} - registryPersistenceTypes := append(feastdevv1alpha1.ValidRegistryDBStorePersistenceTypes, registryFilePersistenceTypes...) + registryPersistenceTypes := append(feastdevv1.ValidRegistryDBStorePersistenceTypes, registryFilePersistenceTypes...) checkPythonPersistenceTypes("registry.out", registryPersistenceTypes) }) It("should match defined onlineStore persistence types in the operator", func() { onlineFilePersistenceTypes := []string{string(services.OnlineSqliteConfigType)} - onlinePersistenceTypes := append(feastdevv1alpha1.ValidOnlineStoreDBStorePersistenceTypes, onlineFilePersistenceTypes...) + onlinePersistenceTypes := append(feastdevv1.ValidOnlineStoreDBStorePersistenceTypes, onlineFilePersistenceTypes...) checkPythonPersistenceTypes("online-store.out", onlinePersistenceTypes) }) It("should match defined offlineStore persistence types in the operator", func() { - offlinePersistenceTypes := append(feastdevv1alpha1.ValidOfflineStoreDBStorePersistenceTypes, feastdevv1alpha1.ValidOfflineStoreFilePersistenceTypes...) + offlinePersistenceTypes := append(feastdevv1.ValidOfflineStoreDBStorePersistenceTypes, feastdevv1.ValidOfflineStoreFilePersistenceTypes...) checkPythonPersistenceTypes("offline-store.out", offlinePersistenceTypes) }) }) diff --git a/infra/feast-operator/test/e2e/e2e_test.go b/infra/feast-operator/test/e2e/e2e_test.go index fb2ce69992a..4515dcccc9f 100644 --- a/infra/feast-operator/test/e2e/e2e_test.go +++ b/infra/feast-operator/test/e2e/e2e_test.go @@ -40,12 +40,12 @@ var _ = Describe("controller", Ordered, func() { applyAndMaterializeTest := "TestApplyAndMaterializeFeastDefinitions" runTestDeploySimpleCRFunc := utils.GetTestDeploySimpleCRFunc("/test/e2e", - "test/testdata/feast_integration_test_crs/v1alpha1_default_featurestore.yaml", + "test/testdata/feast_integration_test_crs/v1_default_featurestore.yaml", featureStoreName, feastResourceName, feastK8sResourceNames, namespace) runTestWithRemoteRegistryFunction := utils.GetTestWithRemoteRegistryFunc("/test/e2e", - "test/testdata/feast_integration_test_crs/v1alpha1_default_featurestore.yaml", - "test/testdata/feast_integration_test_crs/v1alpha1_remote_registry_featurestore.yaml", + "test/testdata/feast_integration_test_crs/v1_default_featurestore.yaml", + "test/testdata/feast_integration_test_crs/v1_remote_registry_featurestore.yaml", featureStoreName, feastResourceName, feastK8sResourceNames, namespace) runTestApplyAndMaterializeFunc := utils.RunTestApplyAndMaterializeFunc("/test/e2e", namespace, "credit-scoring", utils.FeastPrefix+"credit-scoring") diff --git a/infra/feast-operator/test/e2e_rhoai/e2e_suite_test.go b/infra/feast-operator/test/e2e_rhoai/e2e_suite_test.go deleted file mode 100644 index 86750f36e4f..00000000000 --- a/infra/feast-operator/test/e2e_rhoai/e2e_suite_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2025 Feast Community. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package e2erhoai - -import ( - "fmt" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -// Run e2e feast Notebook tests using the Ginkgo runner. -func TestNotebookRunE2E(t *testing.T) { - RegisterFailHandler(Fail) - _, _ = fmt.Fprintf(GinkgoWriter, "Feast Jupyter Notebook Test suite\n") - RunSpecs(t, "e2erhoai Feast Notebook test suite") -} diff --git a/infra/feast-operator/test/e2e_rhoai/feast_postupgrade_test.go b/infra/feast-operator/test/e2e_rhoai/feast_postupgrade_test.go deleted file mode 100644 index d8d091a44b8..00000000000 --- a/infra/feast-operator/test/e2e_rhoai/feast_postupgrade_test.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2025 Feast Community. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package e2erhoai - -import ( - "fmt" - - . "github.com/feast-dev/feast/infra/feast-operator/test/utils" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Feast PostUpgrade scenario Testing", Ordered, func() { - const ( - namespace = "test-ns-feast-upgrade" - testDir = "/test/e2e_rhoai" - feastDeploymentName = FeastPrefix + "credit-scoring" - feastCRName = "credit-scoring" - ) - - AfterAll(func() { - By(fmt.Sprintf("Deleting test namespace: %s", namespace)) - Expect(DeleteNamespace(namespace, testDir)).To(Succeed()) - fmt.Printf("Namespace %s deleted successfully\n", namespace) - }) - runPostUpgradeTest := func() { - By("Verify Feature Store CR is in Ready state") - ValidateFeatureStoreCRStatus(namespace, feastCRName) - - By("Running `feast apply` and `feast materialize-incremental` to validate registry definitions") - VerifyApplyFeatureStoreDefinitions(namespace, feastCRName, feastDeploymentName) - - By("Validating Feast entity, feature, and feature view presence") - VerifyFeastMethods(namespace, feastDeploymentName, testDir) - } - - // This context verifies that a pre-created Feast FeatureStore CR continues to function as expected - // after an upgrade. It validates `feast apply`, registry sync, feature retrieval, and model execution. - Context("Feast post Upgrade Test", func() { - It("Should create and run a feastPostUpgrade test scenario feast apply and materialize functionality successfully", runPostUpgradeTest) - }) -}) diff --git a/infra/feast-operator/test/e2e_rhoai/feast_preupgrade_test.go b/infra/feast-operator/test/e2e_rhoai/feast_preupgrade_test.go deleted file mode 100644 index 925f276c92d..00000000000 --- a/infra/feast-operator/test/e2e_rhoai/feast_preupgrade_test.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright 2025 Feast Community. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package e2erhoai - -import ( - "fmt" - - . "github.com/feast-dev/feast/infra/feast-operator/test/utils" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Feast PreUpgrade scenario Testing", Ordered, func() { - const ( - namespace = "test-ns-feast-upgrade" - replaceNamespace = "test-ns-feast" - testDir = "/test/e2e_rhoai" - feastDeploymentName = FeastPrefix + "credit-scoring" - feastCRName = "credit-scoring" - ) - - filesToUpdateNamespace := []string{ - "test/testdata/feast_integration_test_crs/postgres.yaml", - "test/testdata/feast_integration_test_crs/redis.yaml", - "test/testdata/feast_integration_test_crs/feast.yaml", - } - - BeforeAll(func() { - By(fmt.Sprintf("Creating test namespace: %s", namespace)) - Expect(CreateNamespace(namespace, testDir)).To(Succeed()) - fmt.Printf("Namespace %s created successfully\n", namespace) - - By("Replacing placeholder namespace in CR YAMLs for test setup") - Expect(ReplaceNamespaceInYamlFilesInPlace(filesToUpdateNamespace, replaceNamespace, namespace)).To(Succeed()) - }) - - AfterAll(func() { - By("Restoring original namespace in CR YAMLs") - Expect(ReplaceNamespaceInYamlFilesInPlace(filesToUpdateNamespace, namespace, replaceNamespace)).To(Succeed()) - - if CurrentSpecReport().Failed() { - By(fmt.Sprintf("Deleting test namespace: %s", namespace)) - Expect(DeleteNamespace(namespace, testDir)).To(Succeed()) - fmt.Printf("Namespace %s deleted successfully\n", namespace) - } - }) - - runPreUpgradeTest := func() { - By("Applying Feast infra manifests and verifying setup") - ApplyFeastInfraManifestsAndVerify(namespace, testDir) - - By("Applying and validating the credit-scoring FeatureStore CR") - ApplyFeastYamlAndVerify(namespace, testDir, feastDeploymentName, feastCRName) - } - - // This context ensures the Feast CR setup is functional prior to any upgrade - Context("Feast Pre Upgrade Test", func() { - It("Should create and run a feastPreUpgrade test scenario feast credit-scoring CR setup successfully", runPreUpgradeTest) - }) -}) diff --git a/infra/feast-operator/test/e2e_rhoai/feast_wb_test.go b/infra/feast-operator/test/e2e_rhoai/feast_wb_test.go deleted file mode 100644 index 64bb1f2dea4..00000000000 --- a/infra/feast-operator/test/e2e_rhoai/feast_wb_test.go +++ /dev/null @@ -1,151 +0,0 @@ -/* -Copyright 2025 Feast Community. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package e2erhoai provides end-to-end (E2E) test coverage for Feast integration with -// Red Hat OpenShift AI (RHOAI) environments. This specific test validates the functionality -// of executing a Feast Jupyter notebook within a fully configured OpenShift namespace -package e2erhoai - -import ( - "fmt" - "os" - "os/exec" - "strings" - - utils "github.com/feast-dev/feast/infra/feast-operator/test/utils" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Feast Jupyter Notebook Testing", Ordered, func() { - const ( - namespace = "test-ns-feast-wb" - configMapName = "feast-wb-cm" - rolebindingName = "rb-feast-test" - notebookFile = "test/e2e_rhoai/resources/feast-test.ipynb" - pvcFile = "test/e2e_rhoai/resources/pvc.yaml" - notebookPVC = "jupyterhub-nb-kube-3aadmin-pvc" - testDir = "/test/e2e_rhoai" - notebookName = "feast-test.ipynb" - feastMilvusTest = "TestFeastMilvusNotebook" - ) - - BeforeAll(func() { - By(fmt.Sprintf("Creating test namespace: %s", namespace)) - Expect(utils.CreateNamespace(namespace, testDir)).To(Succeed()) - fmt.Printf("Namespace %s created successfully\n", namespace) - }) - - AfterAll(func() { - By(fmt.Sprintf("Deleting test namespace: %s", namespace)) - Expect(utils.DeleteNamespace(namespace, testDir)).To(Succeed()) - fmt.Printf("Namespace %s deleted successfully\n", namespace) - }) - - runNotebookTest := func() { - env := func(key string) string { - val, _ := os.LookupEnv(key) - return val - } - - username := utils.GetOCUser(testDir) - - // set namespace context - By(fmt.Sprintf("Setting namespace context to : %s", namespace)) - cmd := exec.Command("kubectl", "config", "set-context", "--current", "--namespace", namespace) - output, err := utils.Run(cmd, "/test/e2e_rhoai") - Expect(err).ToNot(HaveOccurred(), fmt.Sprintf( - "Failed to set namespace context to %s.\nError: %v\nOutput: %s\n", - namespace, err, output, - )) - fmt.Printf("Successfully set namespace context to: %s\n", namespace) - - // create config map - By(fmt.Sprintf("Creating Config map: %s", configMapName)) - cmd = exec.Command("kubectl", "create", "configmap", configMapName, "--from-file="+notebookFile, "--from-file=test/e2e_rhoai/resources/feature_repo") - output, err = utils.Run(cmd, "/test/e2e_rhoai") - Expect(err).ToNot(HaveOccurred(), fmt.Sprintf( - "Failed to create ConfigMap %s.\nError: %v\nOutput: %s\n", - configMapName, err, output, - )) - fmt.Printf("ConfigMap %s created successfully\n", configMapName) - - // create pvc - By(fmt.Sprintf("Creating Persistent volume claim: %s", notebookPVC)) - cmd = exec.Command("kubectl", "apply", "-f", "test/e2e_rhoai/resources/pvc.yaml") - _, err = utils.Run(cmd, "/test/e2e_rhoai") - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - fmt.Printf("Persistent Volume Claim %s created successfully", notebookPVC) - - // create rolebinding - By(fmt.Sprintf("Creating rolebinding %s for the user", rolebindingName)) - cmd = exec.Command("kubectl", "create", "rolebinding", rolebindingName, "-n", namespace, "--role=admin", "--user="+username) - _, err = utils.Run(cmd, "/test/e2e_rhoai") - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - fmt.Printf("Created rolebinding %s successfully\n", rolebindingName) - - // configure papermill notebook command execution - command := []string{ - "/bin/sh", - "-c", - fmt.Sprintf( - "pip install papermill && "+ - "mkdir -p /opt/app-root/src/feature_repo && "+ - "cp -rL /opt/app-root/notebooks/* /opt/app-root/src/feature_repo/ && "+ - "oc login --token=%s --server=%s --insecure-skip-tls-verify=true && "+ - "(papermill /opt/app-root/notebooks/%s /opt/app-root/src/output.ipynb --kernel python3 && "+ - "echo '✅ Notebook executed successfully' || "+ - "(echo '❌ Notebook execution failed' && "+ - "cp /opt/app-root/src/output.ipynb /opt/app-root/src/failed_output.ipynb && "+ - "echo '📄 Copied failed notebook to failed_output.ipynb')) && "+ - "jupyter nbconvert --to notebook --stdout /opt/app-root/src/output.ipynb || echo '⚠️ nbconvert failed' && "+ - "sleep 100; exit 0", - utils.GetOCToken("test/e2e_rhoai"), - utils.GetOCServer("test/e2e_rhoai"), - "feast-test.ipynb", - ), - } - - // Defining notebook parameters - nbParams := utils.NotebookTemplateParams{ - Namespace: namespace, - IngressDomain: utils.GetIngressDomain(testDir), - OpenDataHubNamespace: env("APPLICATIONS_NAMESPACE"), - NotebookImage: env("NOTEBOOK_IMAGE"), - NotebookConfigMapName: configMapName, - NotebookPVC: notebookPVC, - Username: username, - OC_TOKEN: utils.GetOCToken(testDir), - OC_SERVER: utils.GetOCServer(testDir), - NotebookFile: notebookName, - Command: "[\"" + strings.Join(command, "\",\"") + "\"]", - PipIndexUrl: env("PIP_INDEX_URL"), - PipTrustedHost: env("PIP_TRUSTED_HOST"), - FeastVerison: env("FEAST_VERSION"), - OpenAIAPIKey: env("OPENAI_API_KEY"), - } - - By("Creating Jupyter Notebook") - Expect(utils.CreateNotebook(nbParams)).To(Succeed(), "Failed to create notebook") - - By("Monitoring notebook logs") - Expect(utils.MonitorNotebookPod(namespace, "jupyter-nb-", notebookName)).To(Succeed(), "Notebook execution failed") - } - - Context("Feast Jupyter Notebook Test", func() { - It("Should create and run a "+feastMilvusTest+" successfully", runNotebookTest) - }) -}) diff --git a/infra/feast-operator/test/e2e_rhoai/resources/custom-nb.yaml b/infra/feast-operator/test/e2e_rhoai/resources/custom-nb.yaml deleted file mode 100644 index 8c91cdc5f34..00000000000 --- a/infra/feast-operator/test/e2e_rhoai/resources/custom-nb.yaml +++ /dev/null @@ -1,157 +0,0 @@ -# This template maybe used to spin up a custom notebook image -# i.e.: sed s/{{.IngressDomain}}/$(oc get ingresses.config/cluster -o jsonpath={.spec.domain})/g tests/resources/custom-nb.template | oc apply -f - -# resources generated: -# pod/jupyter-nb-kube-3aadmin-0 -# service/jupyter-nb-kube-3aadmin -# route.route.openshift.io/jupyter-nb-kube-3aadmin (jupyter-nb-kube-3aadmin-opendatahub.apps.tedbig412.cp.fyre.ibm.com) -# service/jupyter-nb-kube-3aadmin-tls -apiVersion: kubeflow.org/v1 -kind: Notebook -metadata: - annotations: - notebooks.opendatahub.io/inject-oauth: "true" - notebooks.opendatahub.io/last-size-selection: Small - notebooks.opendatahub.io/oauth-logout-url: https://odh-dashboard-{{.OpenDataHubNamespace}}.{{.IngressDomain}}/notebookController/kube-3aadmin/home - opendatahub.io/link: https://jupyter-nb-kube-3aadmin-{{.Namespace}}.{{.IngressDomain}}/notebook/{{.Namespace}}/jupyter-nb-kube-3aadmin - opendatahub.io/username: {{.Username}} - generation: 1 - labels: - app: jupyter-nb-kube-3aadmin - opendatahub.io/dashboard: "true" - opendatahub.io/odh-managed: "true" - opendatahub.io/user: {{.Username}} - name: jupyter-nb-kube-3aadmin - namespace: {{.Namespace}} -spec: - template: - spec: - affinity: - nodeAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - preference: - matchExpressions: - - key: nvidia.com/gpu.present - operator: NotIn - values: - - "true" - weight: 1 - containers: - - env: - - name: NOTEBOOK_ARGS - value: |- - --ServerApp.port=8888 - --ServerApp.token='' - --ServerApp.password='' - --ServerApp.base_url=/notebook/test-feast-wb/jupyter-nb-kube-3aadmin - --ServerApp.quit_button=False - --ServerApp.tornado_settings={"user":"{{.Username}}","hub_host":"https://odh-dashboard-{{.OpenDataHubNamespace}}.{{.IngressDomain}}","hub_prefix":"/notebookController/{{.Username}}"} - - name: JUPYTER_IMAGE - value: {{.NotebookImage}} - - name: JUPYTER_NOTEBOOK_PORT - value: "8888" - - name: PIP_INDEX_URL - value: {{.PipIndexUrl}} - - name: PIP_TRUSTED_HOST - value: {{.PipTrustedHost}} - - name: FEAST_VERSION - value: {{.FeastVerison}} - - name: OPENAI_API_KEY - value: {{.OpenAIAPIKey}} - image: {{.NotebookImage}} - command: {{.Command}} - imagePullPolicy: Always - name: jupyter-nb-kube-3aadmin - ports: - - containerPort: 8888 - name: notebook-port - protocol: TCP - resources: - limits: - cpu: "2" - memory: 3Gi - requests: - cpu: "1" - memory: 3Gi - volumeMounts: - - mountPath: /opt/app-root/src - name: jupyterhub-nb-kube-3aadmin-pvc - - mountPath: /opt/app-root/notebooks - name: {{.NotebookConfigMapName}} - workingDir: /opt/app-root/src - - args: - - --provider=openshift - - --https-address=:8443 - - --http-address= - - --openshift-service-account=jupyter-nb-kube-3aadmin - - --cookie-secret-file=/etc/oauth/config/cookie_secret - - --cookie-expire=24h0m0s - - --tls-cert=/etc/tls/private/tls.crt - - --tls-key=/etc/tls/private/tls.key - - --upstream=http://localhost:8888 - - --upstream-ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt - - --skip-auth-regex=^(?:/notebook/test-feast-wb/jupyter-nb-kube-3aadmin)?/api$ - - --email-domain=* - - --skip-provider-button - - --openshift-sar={"verb":"get","resource":"notebooks","resourceAPIGroup":"kubeflow.org","resourceName":"jupyter-nb-kube-3aadmin","namespace":$(NAMESPACE)} - - --logout-url=https://odh-dashboard-{{.OpenDataHubNamespace}}.{{.IngressDomain}}/notebookController/kube-3aadmin/home - env: - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - image: registry.redhat.io/openshift4/ose-oauth-proxy:v4.10 - imagePullPolicy: Always - livenessProbe: - failureThreshold: 3 - httpGet: - path: /oauth/healthz - port: oauth-proxy - scheme: HTTPS - initialDelaySeconds: 30 - periodSeconds: 5 - successThreshold: 1 - timeoutSeconds: 1 - name: oauth-proxy - ports: - - containerPort: 8443 - name: oauth-proxy - protocol: TCP - readinessProbe: - failureThreshold: 3 - httpGet: - path: /oauth/healthz - port: oauth-proxy - scheme: HTTPS - initialDelaySeconds: 5 - periodSeconds: 5 - successThreshold: 1 - timeoutSeconds: 1 - resources: - limits: - cpu: 100m - memory: 64Mi - requests: - cpu: 100m - memory: 64Mi - volumeMounts: - - mountPath: /etc/oauth/config - name: oauth-config - - mountPath: /etc/tls/private - name: tls-certificates - enableServiceLinks: false - serviceAccountName: jupyter-nb-kube-3aadmin - volumes: - - name: jupyterhub-nb-kube-3aadmin-pvc - persistentVolumeClaim: - claimName: {{.NotebookPVC}} - - name: oauth-config - secret: - defaultMode: 420 - secretName: jupyter-nb-kube-3aadmin-oauth-config - - name: tls-certificates - secret: - defaultMode: 420 - secretName: jupyter-nb-kube-3aadmin-tls - - name: {{.NotebookConfigMapName}} - configMap: - name: {{.NotebookConfigMapName}} diff --git a/infra/feast-operator/test/e2e_rhoai/resources/feast-test.ipynb b/infra/feast-operator/test/e2e_rhoai/resources/feast-test.ipynb deleted file mode 100755 index e2838a4f33e..00000000000 --- a/infra/feast-operator/test/e2e_rhoai/resources/feast-test.ipynb +++ /dev/null @@ -1,481 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import feast\n", - "\n", - "actual_version = feast.__version__\n", - "assert actual_version == os.environ.get(\"FEAST_VERSION\"), (\n", - " f\"❌ Feast version mismatch. Expected: {os.environ.get('FEAST_VERSION')}, Found: {actual_version}\"\n", - ")\n", - "print(f\"✅ Successfully installed Feast version: {actual_version}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%cd /opt/app-root/src/feature_repo\n", - "!ls -l" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!cat /opt/app-root/src/feature_repo/feature_store.yaml" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!mkdir -p data\n", - "!wget -O data/city_wikipedia_summaries_with_embeddings.parquet https://raw.githubusercontent.com/opendatahub-io/feast/master/examples/rag/feature_repo/data/city_wikipedia_summaries_with_embeddings.parquet" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd \n", - "\n", - "df = pd.read_parquet(\"./data/city_wikipedia_summaries_with_embeddings.parquet\")\n", - "df['vector'] = df['vector'].apply(lambda x: x.tolist())\n", - "embedding_length = len(df['vector'][0])\n", - "assert embedding_length == 384, f\"❌ Expected vector length 384, but got {embedding_length}\"\n", - "print(f'embedding length = {embedding_length}')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import display\n", - "\n", - "display(df.head())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install -q pymilvus[milvus_lite] transformers torch" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import subprocess\n", - "\n", - "# Run `feast apply` and capture output\n", - "result = subprocess.run([\"feast\", \"apply\"], capture_output=True, text=True)\n", - "\n", - "# Combine stdout and stderr in case important info is in either\n", - "output = result.stdout + result.stderr\n", - "\n", - "# Print full output for debugging (optional)\n", - "print(output)\n", - "\n", - "# Expected substrings to validate\n", - "expected_messages = [\n", - " \"Applying changes for project rag\",\n", - " \"Connecting to Milvus in local mode\",\n", - " \"Deploying infrastructure for city_embeddings\"\n", - "]\n", - "\n", - "# Validate all expected messages are in output\n", - "for msg in expected_messages:\n", - " assert msg in output, f\"❌ Expected message not found: '{msg}'\"\n", - "\n", - "print(\"✅ All expected messages were found in the output.\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from datetime import datetime\n", - "from feast import FeatureStore\n", - "\n", - "store = FeatureStore(repo_path=\".\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import io\n", - "import sys\n", - "\n", - "# Capture stdout\n", - "captured_output = io.StringIO()\n", - "sys_stdout_backup = sys.stdout\n", - "sys.stdout = captured_output\n", - "\n", - "# Call the function\n", - "store.write_to_online_store(feature_view_name='city_embeddings', df=df)\n", - "\n", - "# Restore stdout\n", - "sys.stdout = sys_stdout_backup\n", - "\n", - "# Get the output\n", - "output_str = captured_output.getvalue()\n", - "\n", - "# Expected message\n", - "expected_msg = \"Connecting to Milvus in local mode using data/online_store.db\"\n", - "\n", - "# Validate\n", - "assert expected_msg in output_str, f\"❌ Expected message not found.\\nExpected: {expected_msg}\\nActual Output:\\n{output_str}\"\n", - "\n", - "print(\"✅ Output message validated successfully.\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# List batch feature views\n", - "batch_fvs = store.list_batch_feature_views()\n", - "\n", - "# Print the number of batch feature views\n", - "print(\"Number of batch feature views:\", len(batch_fvs))\n", - "\n", - "# Assert that the result is an integer and non-negative\n", - "assert isinstance(len(batch_fvs), int), \"Result is not an integer\"\n", - "assert len(batch_fvs) >= 0, \"Feature view count is negative\"\n", - "\n", - "print(\"Feature views listed correctly ✅\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from feast import FeatureStore\n", - "\n", - "# Initialize store (if not already)\n", - "store = FeatureStore(repo_path=\".\") # Adjust path if necessary\n", - "\n", - "# Retrieve the feature view\n", - "fv = store.get_feature_view(\"city_embeddings\")\n", - "\n", - "# Assert name\n", - "assert fv.name == \"city_embeddings\", \"Feature view name mismatch\"\n", - "\n", - "# Assert entities\n", - "assert fv.entities == [\"item_id\"], f\"Expected entities ['item_id'], got {fv.entities}\"\n", - "\n", - "# Assert feature names and vector index settings\n", - "feature_names = [f.name for f in fv.features]\n", - "assert \"vector\" in feature_names, \"Missing 'vector' feature\"\n", - "assert \"state\" in feature_names, \"Missing 'state' feature\"\n", - "assert \"sentence_chunks\" in feature_names, \"Missing 'sentence_chunks' feature\"\n", - "assert \"wiki_summary\" in feature_names, \"Missing 'wiki_summary' feature\"\n", - "\n", - "# Assert 'vector' feature is a vector index with COSINE metric\n", - "vector_feature = next(f for f in fv.features if f.name == \"vector\")\n", - "assert vector_feature.vector_index, \"'vector' feature is not indexed\"\n", - "assert vector_feature.vector_search_metric == \"COSINE\", \"Expected COSINE search metric for 'vector'\"\n", - "\n", - "print(\"All assertions passed ✅\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from feast.entity import Entity\n", - "from feast.types import ValueType\n", - "entity = Entity(\n", - " name=\"item_id1\",\n", - " value_type=ValueType.INT64,\n", - " description=\"test id\",\n", - " tags={\"team\": \"feast\"},\n", - ")\n", - "store.apply(entity)\n", - "assert any(e.name == \"item_id1\" for e in store.list_entities())\n", - "print(\"Entity added ✅\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "entity_to_delete = store.get_entity(\"item_id1\")\n", - "\n", - "store.apply(\n", - " objects=[],\n", - " objects_to_delete=[entity_to_delete],\n", - " partial=False\n", - ")\n", - "\n", - "# Validation after deletion\n", - "assert not any(e.name == \"item_id1\" for e in store.list_entities())\n", - "print(\"Entity deleted ✅\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# List batch feature views\n", - "batch_fvs = store.list_batch_feature_views()\n", - "assert len(batch_fvs) == 1\n", - "\n", - "# Print count\n", - "print(f\"Found {len(batch_fvs)} batch feature view(s) ✅\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pymilvus_client = store._provider._online_store._connect(store.config)\n", - "COLLECTION_NAME = pymilvus_client.list_collections()[0]\n", - "\n", - "milvus_query_result = pymilvus_client.query(\n", - " collection_name=COLLECTION_NAME,\n", - " filter=\"item_id == '0'\",\n", - ")\n", - "pd.DataFrame(milvus_query_result[0]).head()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import torch\n", - "import torch.nn.functional as F\n", - "from feast import FeatureStore\n", - "from pymilvus import MilvusClient, DataType, FieldSchema\n", - "from transformers import AutoTokenizer, AutoModel\n", - "from example_repo import city_embeddings_feature_view, item\n", - "\n", - "TOKENIZER = \"sentence-transformers/all-MiniLM-L6-v2\"\n", - "MODEL = \"sentence-transformers/all-MiniLM-L6-v2\"\n", - "\n", - "def mean_pooling(model_output, attention_mask):\n", - " token_embeddings = model_output[\n", - " 0\n", - " ] # First element of model_output contains all token embeddings\n", - " input_mask_expanded = (\n", - " attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()\n", - " )\n", - " return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(\n", - " input_mask_expanded.sum(1), min=1e-9\n", - " )\n", - "\n", - "def run_model(sentences, tokenizer, model):\n", - " encoded_input = tokenizer(\n", - " sentences, padding=True, truncation=True, return_tensors=\"pt\"\n", - " )\n", - " # Compute token embeddings\n", - " with torch.no_grad():\n", - " model_output = model(**encoded_input)\n", - "\n", - " sentence_embeddings = mean_pooling(model_output, encoded_input[\"attention_mask\"])\n", - " sentence_embeddings = F.normalize(sentence_embeddings, p=2, dim=1)\n", - " return sentence_embeddings" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "question = \"Which city has the largest population in New York?\"\n", - "\n", - "tokenizer = AutoTokenizer.from_pretrained(TOKENIZER)\n", - "model = AutoModel.from_pretrained(MODEL)\n", - "query_embedding = run_model(question, tokenizer, model)\n", - "query = query_embedding.detach().cpu().numpy().tolist()[0]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import display\n", - "\n", - "# Retrieve top k documents\n", - "context_data = store.retrieve_online_documents_v2(\n", - " features=[\n", - " \"city_embeddings:vector\",\n", - " \"city_embeddings:item_id\",\n", - " \"city_embeddings:state\",\n", - " \"city_embeddings:sentence_chunks\",\n", - " \"city_embeddings:wiki_summary\",\n", - " ],\n", - " query=query,\n", - " top_k=3,\n", - " distance_metric='COSINE',\n", - ").to_df()\n", - "display(context_data)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def format_documents(context_df):\n", - " output_context = \"\"\n", - " unique_documents = context_df.drop_duplicates().apply(\n", - " lambda x: \"City & State = {\" + x['state'] +\"}\\nSummary = {\" + x['wiki_summary'].strip()+\"}\",\n", - " axis=1,\n", - " )\n", - " for i, document_text in enumerate(unique_documents):\n", - " output_context+= f\"****START DOCUMENT {i}****\\n{document_text.strip()}\\n****END DOCUMENT {i}****\"\n", - " return output_context" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "RAG_CONTEXT = format_documents(context_data[['state', 'wiki_summary']])\n", - "print(RAG_CONTEXT)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "FULL_PROMPT = f\"\"\"\n", - "You are an assistant for answering questions about states. You will be provided documentation from Wikipedia. Provide a conversational answer.\n", - "If you don't know the answer, just say \"I do not know.\" Don't make up an answer.\n", - "\n", - "Here are document(s) you should use when answer the users question:\n", - "{RAG_CONTEXT}\n", - "\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install openai" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from openai import OpenAI\n", - "\n", - "client = OpenAI(\n", - " api_key=os.environ.get(\"OPENAI_API_KEY\"),\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "response = client.chat.completions.create(\n", - " model=\"gpt-4o-mini\",\n", - " messages=[\n", - " {\"role\": \"system\", \"content\": FULL_PROMPT},\n", - " {\"role\": \"user\", \"content\": question}\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# The expected output\n", - "expected_output = (\n", - " \"New York City\"\n", - ")\n", - "\n", - "# Actual output from response\n", - "actual_output = '\\n'.join([c.message.content.strip() for c in response.choices])\n", - "\n", - "# Validate\n", - "assert expected_output in actual_output, f\"❌ Output mismatch:\\nExpected: {expected_output}\\nActual: {actual_output}\"\n", - "\n", - "print(\"✅ Output matches expected response.\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/infra/feast-operator/test/e2e_rhoai/resources/feature_repo/example_repo.py b/infra/feast-operator/test/e2e_rhoai/resources/feature_repo/example_repo.py deleted file mode 100755 index 7a37d99d495..00000000000 --- a/infra/feast-operator/test/e2e_rhoai/resources/feature_repo/example_repo.py +++ /dev/null @@ -1,42 +0,0 @@ -from datetime import timedelta - -from feast import ( - FeatureView, - Field, - FileSource, -) -from feast.data_format import ParquetFormat -from feast.types import Float32, Array, String, ValueType -from feast import Entity - -item = Entity( - name="item_id", - description="Item ID", - value_type=ValueType.INT64, -) - -parquet_file_path = "./data/city_wikipedia_summaries_with_embeddings.parquet" - -source = FileSource( - file_format=ParquetFormat(), - path=parquet_file_path, - timestamp_field="event_timestamp", -) - -city_embeddings_feature_view = FeatureView( - name="city_embeddings", - entities=[item], - schema=[ - Field( - name="vector", - dtype=Array(Float32), - vector_index=True, - vector_search_metric="COSINE", - ), - Field(name="state", dtype=String), - Field(name="sentence_chunks", dtype=String), - Field(name="wiki_summary", dtype=String), - ], - source=source, - ttl=timedelta(hours=2), -) diff --git a/infra/feast-operator/test/e2e_rhoai/resources/pvc.yaml b/infra/feast-operator/test/e2e_rhoai/resources/pvc.yaml deleted file mode 100644 index a9e8c1be299..00000000000 --- a/infra/feast-operator/test/e2e_rhoai/resources/pvc.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: jupyterhub-nb-kube-3aadmin-pvc -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi diff --git a/infra/feast-operator/test/testdata/feast_integration_test_crs/feast.yaml b/infra/feast-operator/test/testdata/feast_integration_test_crs/feast.yaml index 8d311ab1de1..56ab0ffbd4b 100644 --- a/infra/feast-operator/test/testdata/feast_integration_test_crs/feast.yaml +++ b/infra/feast-operator/test/testdata/feast_integration_test_crs/feast.yaml @@ -13,7 +13,7 @@ stringData: echo: false pool_pre_ping: true --- -apiVersion: feast.dev/v1alpha1 +apiVersion: feast.dev/v1 kind: FeatureStore metadata: name: credit-scoring @@ -29,6 +29,17 @@ spec: persistence: file: type: duckdb + server: + envFrom: + - secretRef: + name: postgres-secret + env: + - name: MPLCONFIGDIR + value: /tmp + resources: + requests: + cpu: 150m + memory: 128Mi onlineStore: persistence: store: @@ -53,3 +64,8 @@ spec: type: sql secretRef: name: feast-data-stores + server: + envFrom: + - secretRef: + name: postgres-secret + restAPI: true diff --git a/infra/feast-operator/test/testdata/feast_integration_test_crs/postgres.yaml b/infra/feast-operator/test/testdata/feast_integration_test_crs/postgres.yaml index f6799e1c700..ba4599cea25 100644 --- a/infra/feast-operator/test/testdata/feast_integration_test_crs/postgres.yaml +++ b/infra/feast-operator/test/testdata/feast_integration_test_crs/postgres.yaml @@ -25,7 +25,7 @@ spec: spec: containers: - name: postgres - image: 'postgres:16-alpine' + image: 'quay.io/feastdev-ci/feast-test-images:postgres-17-alpine' ports: - containerPort: 5432 envFrom: diff --git a/infra/feast-operator/test/testdata/feast_integration_test_crs/redis.yaml b/infra/feast-operator/test/testdata/feast_integration_test_crs/redis.yaml index 57f2765e97e..c81fdb46b67 100644 --- a/infra/feast-operator/test/testdata/feast_integration_test_crs/redis.yaml +++ b/infra/feast-operator/test/testdata/feast_integration_test_crs/redis.yaml @@ -15,7 +15,8 @@ spec: spec: containers: - name: redis - image: 'quay.io/sclorg/redis-7-c9s' + image: 'quay.io/feastdev-ci/feast-test-images:redis-7-alpine' + command: ["redis-server", "--save", ""] ports: - containerPort: 6379 env: diff --git a/infra/feast-operator/test/testdata/feast_integration_test_crs/v1_default_featurestore.yaml b/infra/feast-operator/test/testdata/feast_integration_test_crs/v1_default_featurestore.yaml new file mode 100644 index 00000000000..7c28a46145e --- /dev/null +++ b/infra/feast-operator/test/testdata/feast_integration_test_crs/v1_default_featurestore.yaml @@ -0,0 +1,14 @@ +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: simple-feast-setup +spec: + feastProject: my_project + services: + offlineStore: + server: {} + registry: + local: + server: {} + ui: {} + diff --git a/infra/feast-operator/test/testdata/feast_integration_test_crs/v1_remote_registry_featurestore.yaml b/infra/feast-operator/test/testdata/feast_integration_test_crs/v1_remote_registry_featurestore.yaml new file mode 100644 index 00000000000..83b4e327829 --- /dev/null +++ b/infra/feast-operator/test/testdata/feast_integration_test_crs/v1_remote_registry_featurestore.yaml @@ -0,0 +1,16 @@ +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: simple-feast-remote-setup +spec: + feastProject: my_project + services: + offlineStore: + server: {} + ui: {} + registry: + remote: + feastRef: + name: simple-feast-setup + namespace: test-ns-feast + diff --git a/infra/feast-operator/test/utils/notebook_util.go b/infra/feast-operator/test/utils/notebook_util.go deleted file mode 100644 index 28bf64a67eb..00000000000 --- a/infra/feast-operator/test/utils/notebook_util.go +++ /dev/null @@ -1,218 +0,0 @@ -package utils - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "strings" - "text/template" - "time" - - . "github.com/onsi/gomega" -) - -type NotebookTemplateParams struct { - Namespace string - IngressDomain string - OpenDataHubNamespace string - NotebookImage string - NotebookConfigMapName string - NotebookPVC string - Username string - OC_TOKEN string - OC_SERVER string - NotebookFile string - Command string - PipIndexUrl string - PipTrustedHost string - FeastVerison string - OpenAIAPIKey string -} - -// CreateNotebook renders a notebook manifest from a template and applies it using kubectl. -func CreateNotebook(params NotebookTemplateParams) error { - content, err := os.ReadFile("test/e2e_rhoai/resources/custom-nb.yaml") - if err != nil { - return fmt.Errorf("failed to read template file: %w", err) - } - - tmpl, err := template.New("notebook").Parse(string(content)) - if err != nil { - return fmt.Errorf("failed to parse template: %w", err) - } - - var rendered bytes.Buffer - if err := tmpl.Execute(&rendered, params); err != nil { - return fmt.Errorf("failed to substitute template: %w", err) - } - - tmpFile, err := os.CreateTemp("", "notebook-*.yaml") - if err != nil { - return fmt.Errorf("failed to create temp file: %w", err) - } - - // Defer cleanup of temp file - defer func() { - if err := os.Remove(tmpFile.Name()); err != nil { - fmt.Printf("warning: failed to remove temp file %s: %v", tmpFile.Name(), err) - } - }() - - if _, err := tmpFile.Write(rendered.Bytes()); err != nil { - return fmt.Errorf("failed to write to temp file: %w", err) - } - - if err := tmpFile.Close(); err != nil { - return fmt.Errorf("failed to close temp file: %w", err) - } - - // fmt.Println("Notebook manifest applied successfully") - cmd := exec.Command("kubectl", "apply", "-f", tmpFile.Name(), "-n", params.Namespace) - output, err := Run(cmd, "/test/e2e_rhoai") - Expect(err).ToNot(HaveOccurred(), fmt.Sprintf( - "Failed to create Notebook %s.\nError: %v\nOutput: %s\n", - tmpFile.Name(), err, output, - )) - fmt.Printf("Notebook %s created successfully\n", tmpFile.Name()) - return nil -} - -// MonitorNotebookPod waits for a notebook pod to reach Running state and verifies execution logs. -func MonitorNotebookPod(namespace, podPrefix string, notebookName string) error { - const successMarker = "Notebook executed successfully" - const failureMarker = "Notebook execution failed" - const pollInterval = 5 * time.Second - var pod *PodInfo - - fmt.Println("🔄 Waiting for notebook pod to reach Running & Ready state...") - - foundRunningReady := false - for i := 0; i < 36; i++ { - var err error - pod, err = getPodByPrefix(namespace, podPrefix) - if err != nil { - fmt.Printf("⏳ Pod not created yet: %v\n", err) - time.Sleep(pollInterval) - continue - } - if pod.Status == "Running" { - fmt.Printf("✅ Pod %s is Running and Ready.\n", pod.Name) - foundRunningReady = true - break - } - fmt.Printf("⏳ Pod %s not ready yet. Phase: %s\n", pod.Name, pod.Status) - time.Sleep(pollInterval) - } - - if !foundRunningReady { - return fmt.Errorf("❌ Pod %s did not reach Running & Ready state within 3 minutes", podPrefix) - } - - // Start monitoring notebook logs - fmt.Printf("⏳ Monitoring Notebook pod %s Logs for Jupyter Notebook %s execution status\n", pod.Name, notebookName) - - for i := 0; i < 60; i++ { - logs, err := getPodLogs(namespace, pod.Name) - if err != nil { - fmt.Printf("⏳ Failed to get logs for pod %s: %v\n", pod.Name, err) - time.Sleep(pollInterval) - continue - } - - if strings.Contains(logs, successMarker) { - Expect(logs).To(ContainSubstring(successMarker)) - fmt.Printf("✅ Jupyter Notebook pod %s executed successfully.\n", pod.Name) - return nil - } - - if strings.Contains(logs, failureMarker) { - fmt.Printf("❌ Notebook pod %s failed: failure marker found.\n", pod.Name) - return fmt.Errorf("Notebook failed in execution. Logs:\n%s", logs) - } - - time.Sleep(pollInterval) - } - - return fmt.Errorf("❌ Timed out waiting for notebook pod %s to complete", podPrefix) -} - -type PodInfo struct { - Name string - Status string -} - -// returns the first pod matching a name prefix in the given namespace. -func getPodByPrefix(namespace, prefix string) (*PodInfo, error) { - cmd := exec.Command( - "kubectl", "get", "pods", "-n", namespace, - "-o", "jsonpath={range .items[*]}{.metadata.name} {.status.phase}{\"\\n\"}{end}", - ) - output, err := Run(cmd, "/test/e2e_rhoai") - if err != nil { - return nil, fmt.Errorf("failed to get pods: %w", err) - } - - lines := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, line := range lines { - parts := strings.Fields(line) - if len(parts) < 2 { - continue - } - name := parts[0] - status := parts[1] - - if strings.HasPrefix(name, prefix) { - return &PodInfo{ - Name: name, - Status: status, - }, nil - } - } - - return nil, fmt.Errorf("no pod found with prefix %q in namespace %q", prefix, namespace) -} - -// retrieves the logs of a specified pod in the given namespace. -func getPodLogs(namespace, podName string) (string, error) { - cmd := exec.Command("kubectl", "logs", "-n", namespace, podName) - var out bytes.Buffer - var stderr bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &stderr - - err := cmd.Run() - if err != nil { - return "", fmt.Errorf("error getting pod logs: %v - %s", err, stderr.String()) - } - - return out.String(), nil -} - -// returns the OpenShift cluster ingress domain. -func GetIngressDomain(testDir string) string { - cmd := exec.Command("oc", "get", "ingresses.config.openshift.io", "cluster", "-o", "jsonpath={.spec.domain}") - output, _ := Run(cmd, testDir) - return string(output) -} - -// returns the current OpenShift user authentication token. -func GetOCToken(testDir string) string { - cmd := exec.Command("oc", "whoami", "--show-token") - output, _ := Run(cmd, testDir) - return string(output) -} - -// returns the OpenShift API server URL for the current user. -func GetOCServer(testDir string) string { - cmd := exec.Command("oc", "whoami", "--show-server") - output, _ := Run(cmd, testDir) - return string(output) -} - -// returns the OpenShift cluster logged in Username -func GetOCUser(testDir string) string { - cmd := exec.Command("oc", "whoami") - output, _ := Run(cmd, testDir) - return strings.TrimSpace(string(output)) -} diff --git a/infra/feast-operator/test/utils/test_util.go b/infra/feast-operator/test/utils/test_util.go index 868ce9066f4..dfb5a9f31fd 100644 --- a/infra/feast-operator/test/utils/test_util.go +++ b/infra/feast-operator/test/utils/test_util.go @@ -16,7 +16,7 @@ import ( appsv1 "k8s.io/api/apps/v1" "github.com/feast-dev/feast/infra/feast-operator/api/feastversion" - "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1" ) const ( @@ -26,6 +26,9 @@ const ( FeastPrefix = "feast-" FeatureStoreName = "simple-feast-setup" FeastResourceName = FeastPrefix + FeatureStoreName + FeatureStoreResourceName = "featurestores.feast.dev" + feastCommand = "feast" + listCommand = "list" ) // dynamically checks if all conditions of custom resource featurestore are in "Ready" state. @@ -33,7 +36,7 @@ func checkIfFeatureStoreCustomResourceConditionsInReady(featureStoreName, namesp // Wait 10 seconds to lets the feature store status update time.Sleep(1 * time.Minute) - cmd := exec.Command("kubectl", "get", "featurestore", featureStoreName, "-n", namespace, "-o", "json") + cmd := exec.Command("kubectl", "get", FeatureStoreResourceName, featureStoreName, "-n", namespace, "-o", "json") var out bytes.Buffer var stderr bytes.Buffer @@ -46,7 +49,7 @@ func checkIfFeatureStoreCustomResourceConditionsInReady(featureStoreName, namesp } // Parse the JSON into FeatureStore - var resource v1alpha1.FeatureStore + var resource feastdevv1.FeatureStore if err := json.Unmarshal(out.Bytes(), &resource); err != nil { return fmt.Errorf("failed to parse the resource JSON. Error: %v", err) } @@ -174,12 +177,21 @@ func checkIfKubernetesServiceExists(namespace, serviceName string) error { } func isFeatureStoreHavingRemoteRegistry(namespace, featureStoreName string) (bool, error) { - timeout := time.Second * 30 + timeout := 5 * time.Minute interval := time.Second * 2 // Poll every 2 seconds startTime := time.Now() for time.Since(startTime) < timeout { - cmd := exec.Command("kubectl", "get", "featurestore", featureStoreName, "-n", namespace, + // First check if the resource exists + checkCmd := exec.Command("kubectl", "get", FeatureStoreResourceName, featureStoreName, "-n", namespace) + if err := checkCmd.Run(); err != nil { + // Resource doesn't exist yet, retry + fmt.Printf("FeatureStore %s/%s does not exist yet, waiting...\n", namespace, featureStoreName) + time.Sleep(interval) + continue + } + + cmd := exec.Command("kubectl", "get", FeatureStoreResourceName, featureStoreName, "-n", namespace, "-o=jsonpath='{.status.applied.services.registry}'") output, err := cmd.Output() @@ -206,7 +218,7 @@ func isFeatureStoreHavingRemoteRegistry(namespace, featureStoreName string) (boo } // Parse the JSON into a map - var registryConfig v1alpha1.Registry + var registryConfig feastdevv1.Registry if err := json.Unmarshal([]byte(result), ®istryConfig); err != nil { return false, err // Return false on JSON parsing failure } @@ -399,6 +411,10 @@ func DeployOperatorFromCode(testDir string, skipBuilds bool) { _, err = Run(cmd, testDir) ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("deleting existing controller-manager deployment to allow selector changes on upgrade") + cmd = exec.Command("kubectl", "delete", "deployment", ControllerDeploymentName, "-n", FeastControllerNamespace, "--ignore-not-found=true") + _, _ = Run(cmd, testDir) + By("deploying the controller-manager") cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage), fmt.Sprintf("FS_IMG=%s", feastLocalImage)) _, err = Run(cmd, testDir) @@ -429,7 +445,17 @@ func DeleteOperatorDeployment(testDir string) { func DeployPreviousVersionOperator() { var err error - cmd := exec.Command("kubectl", "apply", "-f", fmt.Sprintf("https://raw.githubusercontent.com/feast-dev/feast/refs/tags/v%s/infra/feast-operator/dist/install.yaml", feastversion.FeastVersion)) + // Delete existing CRD first to avoid version conflicts when downgrading + // The old operator version may not have v1, but the cluster might have v1 in status.storedVersions + By("Deleting existing CRD to allow downgrade to previous version") + cmd := exec.Command("kubectl", "delete", "crd", "featurestores.feast.dev", "--ignore-not-found=true") + _, err = Run(cmd, "/test/upgrade") + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // Wait a bit for CRD deletion to complete + time.Sleep(2 * time.Second) + + cmd = exec.Command("kubectl", "apply", "--server-side", "--force-conflicts", "-f", fmt.Sprintf("https://raw.githubusercontent.com/feast-dev/feast/refs/tags/v%s/infra/feast-operator/dist/install.yaml", feastversion.FeastVersion)) _, err = Run(cmd, "/test/upgrade") ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -475,7 +501,7 @@ func DeleteNamespace(namespace string, testDir string) error { func RunTestApplyAndMaterializeFunc(testDir string, namespace string, feastCRName string, feastDeploymentName string) func() { return func() { ApplyFeastInfraManifestsAndVerify(namespace, testDir) - ApplyFeastYamlAndVerify(namespace, testDir, feastDeploymentName, feastCRName) + ApplyFeastYamlAndVerify(namespace, testDir, feastDeploymentName, feastCRName, "test/testdata/feast_integration_test_crs/feast.yaml") VerifyApplyFeatureStoreDefinitions(namespace, feastCRName, feastDeploymentName) VerifyFeastMethods(namespace, feastDeploymentName, testDir) } @@ -524,27 +550,27 @@ func VerifyFeastMethods(namespace string, feastDeploymentName string, testDir st } checks := []feastCheck{ { - command: []string{"feast", "projects", "list"}, + command: []string{feastCommand, "projects", listCommand}, expected: []string{"credit_scoring_local"}, logPrefix: "Projects List", }, { - command: []string{"feast", "feature-views", "list"}, + command: []string{feastCommand, "feature-views", listCommand}, expected: []string{"credit_history", "zipcode_features", "total_debt_calc"}, logPrefix: "Feature Views List", }, { - command: []string{"feast", "entities", "list"}, + command: []string{feastCommand, "entities", listCommand}, expected: []string{"zipcode", "dob_ssn"}, logPrefix: "Entities List", }, { - command: []string{"feast", "data-sources", "list"}, + command: []string{feastCommand, "data-sources", listCommand}, expected: []string{"Zipcode source", "Credit history", "application_data"}, logPrefix: "Data Sources List", }, { - command: []string{"feast", "features", "list"}, + command: []string{feastCommand, "features", listCommand}, expected: []string{ "credit_card_due", "mortgage_due", "student_loan_due", "vehicle_loan_due", "hard_pulls", "missed_payments_2y", "missed_payments_1y", "missed_payments_6m", @@ -637,10 +663,10 @@ func validateFeatureStoreYaml(namespace, deployment string) { } // apply and verifies the Feast deployment becomes available, the CR status is "Ready -func ApplyFeastYamlAndVerify(namespace string, testDir string, feastDeploymentName string, feastCRName string) { +func ApplyFeastYamlAndVerify(namespace string, testDir string, feastDeploymentName string, feastCRName string, feastYAMLFilePath string) { By("Applying Feast yaml for secrets and Feature store CR") cmd := exec.Command("kubectl", "apply", "-n", namespace, - "-f", "test/testdata/feast_integration_test_crs/feast.yaml") + "-f", feastYAMLFilePath) _, err := Run(cmd, testDir) ExpectWithOffset(1, err).NotTo(HaveOccurred()) checkDeployment(namespace, feastDeploymentName) @@ -676,20 +702,3 @@ func ApplyFeastYamlAndVerify(namespace string, testDir string, feastDeploymentNa By("Verifying client feature_store.yaml for expected store types") validateFeatureStoreYaml(namespace, feastDeploymentName) } - -// ReplaceNamespaceInYaml reads a YAML file, replaces all existingNamespace with the actual namespace -func ReplaceNamespaceInYamlFilesInPlace(filePaths []string, existingNamespace string, actualNamespace string) error { - for _, filePath := range filePaths { - data, err := os.ReadFile(filePath) - if err != nil { - return fmt.Errorf("failed to read YAML file %s: %w", filePath, err) - } - updated := strings.ReplaceAll(string(data), existingNamespace, actualNamespace) - - err = os.WriteFile(filePath, []byte(updated), 0644) - if err != nil { - return fmt.Errorf("failed to write updated YAML file %s: %w", filePath, err) - } - } - return nil -} diff --git a/infra/scripts/cleanup_ci.py b/infra/scripts/cleanup_ci.py index 262adf1e3eb..347e8ce1d39 100644 --- a/infra/scripts/cleanup_ci.py +++ b/infra/scripts/cleanup_ci.py @@ -1,4 +1,3 @@ -from time import sleep import boto3 from tqdm import tqdm from google.cloud import bigtable @@ -25,7 +24,7 @@ def cleanup_bigtable_ci(): client = bigtable.Client(project="kf-feast", admin=True) instance = client.instance("feast-integration-tests") if instance.exists(): - print(f"Deleted Bigtable CI instance") + print("Deleted Bigtable CI instance") instance.delete() location_id = "us-central1-f" @@ -38,7 +37,7 @@ def cleanup_bigtable_ci(): default_storage_type=storage_type, ) instance.create(clusters=[cluster]) - print(f"Created new Bigtable CI tables") + print("Created new Bigtable CI tables") def main() -> None: diff --git a/infra/scripts/feature_server_docker_smoke.py b/infra/scripts/feature_server_docker_smoke.py new file mode 100644 index 00000000000..801decac90c --- /dev/null +++ b/infra/scripts/feature_server_docker_smoke.py @@ -0,0 +1,45 @@ +from types import SimpleNamespace + +import uvicorn + +from feast.feature_server import get_app + + +class _FakeRegistry: + def proto(self): + return object() + + def list_projects(self, allow_cache=True, tags=None): + return [] + + +class _FakeStore: + def __init__(self): + self.config = SimpleNamespace() + self.project = "smoke_test" + self.registry = _FakeRegistry() + self._provider = SimpleNamespace( + async_supported=SimpleNamespace( + online=SimpleNamespace(read=False, write=False) + ) + ) + + def _get_provider(self): + return self._provider + + async def initialize(self): + return None + + def refresh_registry(self): + return None + + def list_feature_views(self): + return [] + + async def close(self): + return None + + +if __name__ == "__main__": + app = get_app(_FakeStore()) + uvicorn.run(app, host="0.0.0.0", port=6566, log_level="error") diff --git a/infra/scripts/feature_store_client_configs_gen.py b/infra/scripts/feature_store_client_configs_gen.py index 124877a18ef..71a2d5d4d53 100644 --- a/infra/scripts/feature_store_client_configs_gen.py +++ b/infra/scripts/feature_store_client_configs_gen.py @@ -13,19 +13,18 @@ import os import yaml from pathlib import Path -from typing import Dict, List, Any, Optional +from typing import Dict, Any from feast import FeatureStore -from feast.repo_config import RepoConfig def create_feature_store_yaml(config_content: str, config_name: str) -> str: """ Create a feature_store.yaml file from config content. - + Args: config_content: YAML content as string config_name: Name identifier for the config (used for filename) - + Returns: Path to the created YAML file """ @@ -34,31 +33,33 @@ def create_feature_store_yaml(config_content: str, config_name: str) -> str: config_dict = yaml.safe_load(config_content) except yaml.YAMLError as e: raise ValueError(f"Failed to parse YAML content for {config_name}: {e}") - + # Ensure required fields are present - required_fields = ['project', 'registry', 'provider'] + required_fields = ["project", "registry", "provider"] for field in required_fields: if field not in config_dict: - raise ValueError(f"Failed to create config {config_name}: missing required field '{field}'") - + raise ValueError( + f"Failed to create config {config_name}: missing required field '{field}'" + ) + # Create filename filename = f"feature_store_{config_name}.yaml" filepath = Path(filename) - + # Write the YAML file - with open(filepath, 'w') as f: + with open(filepath, "w") as f: yaml.dump(config_dict, f, default_flow_style=False, sort_keys=False) - + return str(filepath) def create_feature_store_object(yaml_file_path: str) -> FeatureStore: """ Create a FeatureStore object from a YAML file. - + Args: yaml_file_path: Path to the feature_store.yaml file - + Returns: FeatureStore object """ @@ -67,26 +68,28 @@ def create_feature_store_object(yaml_file_path: str) -> FeatureStore: fs = FeatureStore(fs_yaml_file=Path(yaml_file_path)) return fs except Exception as e: - raise RuntimeError(f"Failed to create FeatureStore object from {yaml_file_path}: {e}") + raise RuntimeError( + f"Failed to create FeatureStore object from {yaml_file_path}: {e}" + ) def process_client_configs(client_configs: Dict[str, str]) -> Dict[str, Dict[str, Any]]: """ Process multiple client config YAML contents and create feature stores. - + Args: client_configs: Dictionary mapping config names to YAML content strings - + Returns: Dictionary with results for each config """ results = {} created_yamls = [] feature_stores = {} - + print("Creating feature store YAMLs and objects...") print("=" * 50) - + for config_name, config_content in client_configs.items(): try: print(f"\nProcessing config: {config_name}") @@ -95,7 +98,7 @@ def process_client_configs(client_configs: Dict[str, str]) -> Dict[str, Dict[str yaml_path = create_feature_store_yaml(config_content, config_name) created_yamls.append(yaml_path) print(f"✓ Created YAML file: {yaml_path}") - + # Create FeatureStore object fs = create_feature_store_object(yaml_path) fs_var_name = f"fs_{fs.project}" @@ -104,30 +107,30 @@ def process_client_configs(client_configs: Dict[str, str]) -> Dict[str, Dict[str print(f"✓ Created FeatureStore object: {fs_var_name}") results[config_name] = { - 'yaml_path': yaml_path, - 'feature_store': fs_var_name, - 'project_name': fs.project, - 'success': True, - 'error': None + "yaml_path": yaml_path, + "feature_store": fs_var_name, + "project_name": fs.project, + "success": True, + "error": None, } - + except Exception as e: print(f"✗ Failed to process config {config_name}: {e}") results[config_name] = { - 'yaml_path': None, - 'feature_store': None, - 'project_name': None, - 'success': False, - 'error': str(e) + "yaml_path": None, + "feature_store": None, + "project_name": None, + "success": False, + "error": str(e), } - + return results def print_summary(results: Dict[str, Dict[str, Any]]) -> None: """ Print summary of all operations. - + Args: results: Results dictionary from process_client_configs """ @@ -135,13 +138,15 @@ def print_summary(results: Dict[str, Dict[str, Any]]) -> None: print("SUMMARY") print("=" * 50) - successful_configs = [name for name, result in results.items() if result['success']] - failed_configs = [name for name, result in results.items() if not result['success']] + successful_configs = [name for name, result in results.items() if result["success"]] + failed_configs = [name for name, result in results.items() if not result["success"]] print(f"\n\n✓✓Feature Store YAML files have been created in: {os.getcwd()}") print(f"\n✓ Successfully processed {len(successful_configs)} config(s):") for config_name in successful_configs: result = results[config_name] - print(f" - {config_name}: {result['yaml_path']} (Project: {result['project_name']})") + print( + f" - {config_name}: {result['yaml_path']} (Project: {result['project_name']})" + ) if failed_configs: print(f"\n✗ Failed to process {len(failed_configs)} config(s):") @@ -149,15 +154,21 @@ def print_summary(results: Dict[str, Dict[str, Any]]) -> None: result = results[config_name] print(f" - {config_name}: {result['error']}") - print(f"\n\n✓✓ Feature Store Object(s) details:") + print("\n\n✓✓ Feature Store Object(s) details:") for config_name in successful_configs: result = results[config_name] - print(f"> Object Name - {result['feature_store']} ; project name - {result['project_name']} ; yaml path - {result['yaml_path']}") + print( + f"> Object Name - {result['feature_store']} ; project name - {result['project_name']} ; yaml path - {result['yaml_path']}" + ) print("\n") print("=" * 25, "Usage:", "=" * 25) - print("You can now use feature store object(s) to access the feature store resources and functions!") - print("\n// Note: Replace object_name with the actual object name from the list above.") + print( + "You can now use feature store object(s) to access the feature store resources and functions!" + ) + print( + "\n// Note: Replace object_name with the actual object name from the list above." + ) print("object_name.list_features()\nobject_name.get_historical_features()") print("=" * 58) @@ -179,7 +190,6 @@ def main(): type: file entity_key_serialization_version: 3 """, - "aws_redshift": """ project: aws_feature_store registry: data/registry.db @@ -197,7 +207,6 @@ def main(): iam_role: arn:aws:iam::123456789012:role/RedshiftRole entity_key_serialization_version: 3 """, - "gcp_bigquery": """ project: gcp_feature_store registry: data/registry.db @@ -210,18 +219,20 @@ def main(): project_id: my-gcp-project dataset_id: my_dataset entity_key_serialization_version: 3 -""" +""", } print("=" * 50) - print("This script will create feature store YAMLs and objects from client configs.") + print( + "This script will create feature store YAMLs and objects from client configs." + ) print(f"Processing {len(example_configs)} example configurations...") - + # Process the configs results = process_client_configs(example_configs) - + # Print summary print_summary(results) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/infra/scripts/generate_protos.py b/infra/scripts/generate_protos.py index 2ce7e29e12e..c030a9347df 100644 --- a/infra/scripts/generate_protos.py +++ b/infra/scripts/generate_protos.py @@ -9,6 +9,7 @@ PROTO_SUBDIRS = ["core", "registry", "serving", "types", "storage"] PYTHON_CODE_PREFIX = "sdk/python" + class BuildPythonProtosCommand: description = "Builds the proto files into Python files." user_options = [ @@ -76,5 +77,6 @@ def run(self): with open(path, "w") as file: file.write(filedata) + if __name__ == "__main__": - BuildPythonProtosCommand().run() \ No newline at end of file + BuildPythonProtosCommand().run() diff --git a/infra/scripts/pixi/pixi.toml b/infra/scripts/pixi/pixi.toml index afb6407042d..b5ce74b1791 100644 --- a/infra/scripts/pixi/pixi.toml +++ b/infra/scripts/pixi/pixi.toml @@ -1,4 +1,4 @@ -[project] +[workspace] name = "pixi-feast" channels = ["conda-forge"] platforms = ["linux-64", "osx-arm64", "osx-64"] diff --git a/infra/scripts/release/bump_file_versions.py b/infra/scripts/release/bump_file_versions.py index c913e9f43f7..f13b9e257df 100644 --- a/infra/scripts/release/bump_file_versions.py +++ b/infra/scripts/release/bump_file_versions.py @@ -4,7 +4,9 @@ import pathlib import sys -USAGE = f"Usage: python {sys.argv[0]} [--help] | current_semver_version new_semver_version]" +USAGE = ( + f"Usage: python {sys.argv[0]} [--help] | current_semver_version new_semver_version]" +) VERSIONS_TO_BUMP = 27 @@ -17,18 +19,24 @@ def main() -> None: new_version = args[1].strip() if current_version == new_version: - raise SystemExit(f"Current and new versions are the same: {current_version} == {new_version}") + raise SystemExit( + f"Current and new versions are the same: {current_version} == {new_version}" + ) # Validate that the input arguments are semver versions if not is_semantic_version(current_version): - raise SystemExit(f"Current version is not a valid semantic version: {current_version}") + raise SystemExit( + f"Current version is not a valid semantic version: {current_version}" + ) if not is_semantic_version(new_version): raise SystemExit(f"New version is not a valid semantic version: {new_version}") # Get git repo root directory repo_root = pathlib.Path(__file__).resolve().parent.parent.parent.parent - path_to_file_list = repo_root.joinpath("infra", "scripts", "release", "files_to_bump.txt") + path_to_file_list = repo_root.joinpath( + "infra", "scripts", "release", "files_to_bump.txt" + ) # Get files to bump versions within with open(path_to_file_list, "r") as f: @@ -47,11 +55,15 @@ def main() -> None: file_contents = f.readlines() for line in lines: # note we validate the version above already - current_parsed_version = _get_semantic_version(file_contents[int(line) - 1]) - file_contents[int(line) - 1] = file_contents[int(line) - 1].replace(current_parsed_version, new_version) + current_parsed_version = _get_semantic_version( + file_contents[int(line) - 1] + ) + file_contents[int(line) - 1] = file_contents[int(line) - 1].replace( + current_parsed_version, new_version + ) with open(repo_root.joinpath(file_path), "w") as f: - f.write(''.join(file_contents)) + f.write("".join(file_contents)) updated_count += 1 print(f"Updated {updated_count} files with new version {new_version}") @@ -70,22 +82,27 @@ def is_semantic_version(version: str) -> bool: def validate_files_to_bump(current_version, files_to_bump, repo_root): for file in files_to_bump: components = file.split(" ") - assert len(components) > 1, f"Entry {file} should have a file name, and a list of line numbers with versions" + assert len(components) > 1, ( + f"Entry {file} should have a file name, and a list of line numbers with versions" + ) file_path = components[0] lines = components[1:] with open(repo_root.joinpath(file_path), "r") as f: file_contents = f.readlines() for line in lines: new_version = _get_semantic_version(file_contents[int(line) - 1]) - current_major_minor_version = '.'.join(current_version.split(".")[0:1]) - assert current_version in new_version or current_major_minor_version in new_version, ( + current_major_minor_version = ".".join(current_version.split(".")[0:1]) + assert ( + current_version in new_version + or current_major_minor_version in new_version + ), ( f"File `{file_path}` line `{line}` didn't contain version {current_version}. " f"Contents: {file_contents[int(line) - 1]}" ) def _get_semantic_version(input_string: str) -> str: - semver_pattern = r'\bv?(\d+\.\d+\.\d+)\b' + semver_pattern = r"\bv?(\d+\.\d+\.\d+)\b" match = re.search(semver_pattern, input_string) return match.group(1) diff --git a/infra/scripts/release/unset_prerelease.py b/infra/scripts/release/unset_prerelease.py index 4a2ba131970..c474c0908de 100644 --- a/infra/scripts/release/unset_prerelease.py +++ b/infra/scripts/release/unset_prerelease.py @@ -4,7 +4,9 @@ import sys import requests -USAGE = f"Usage: python {sys.argv[0]} [--help] | version_being_released (e.g., v0.19.1)]" +USAGE = ( + f"Usage: python {sys.argv[0]} [--help] | version_being_released (e.g., v0.19.1)]" +) def get_prerelease_status(version_being_released, token): @@ -13,12 +15,12 @@ def get_prerelease_status(version_being_released, token): headers = { "Content-Type": "application/json", "Accept": "application/vnd.github.v3+json", - "Authorization": f"Bearer {token}" + "Authorization": f"Bearer {token}", } response = requests.request("GET", url, headers=headers) response_json = response.json() - return bool(response_json['prerelease']), response_json['id'] + return bool(response_json["prerelease"]), response_json["id"] def set_prerelease_status(release_id, status, token): @@ -29,7 +31,7 @@ def set_prerelease_status(release_id, status, token): headers = { "Content-Type": "application/json", "Accept": "application/vnd.github.v3+json", - "Authorization": f"Bearer {token}" + "Authorization": f"Bearer {token}", } requests.request("PATCH", url, json=payload, headers=headers) @@ -44,7 +46,7 @@ def main() -> None: print(f"Disabling prerelease status for {version_being_released}") - token = os.getenv('GITHUB_TOKEN', default=None) + token = os.getenv("GITHUB_TOKEN", default=None) if token is None: raise OSError("GITHUB_TOKEN environmental variable is not set") @@ -61,9 +63,14 @@ def main() -> None: if is_prerelease: import warnings - warnings.warn(f"Failed to unset prerelease status for {version_being_released} release id {release_id}") + + warnings.warn( + f"Failed to unset prerelease status for {version_being_released} release id {release_id}" + ) else: - print(f"Successfully unset prerelease status for {version_being_released} release id {release_id}") + print( + f"Successfully unset prerelease status for {version_being_released} release id {release_id}" + ) if __name__ == "__main__": diff --git a/infra/scripts/test-end-to-end.sh b/infra/scripts/test-end-to-end.sh index 4cfc2307f94..44353af4ce6 100755 --- a/infra/scripts/test-end-to-end.sh +++ b/infra/scripts/test-end-to-end.sh @@ -7,8 +7,8 @@ infra/scripts/download-maven-cache.sh --archive-uri ${MAVEN_CACHE} --output-dir apt-get update && apt-get install -y redis-server postgresql libpq-dev make build-java-no-tests REVISION=develop -python -m pip install --upgrade pip setuptools wheel pip-tools -make install-python +pip install uv +make install-python-dependencies-dev python -m pip install -qr tests/requirements.txt su -p postgres -c "PATH=$PATH HOME=/tmp pytest -v tests/e2e/ --feast-version develop" diff --git a/infra/templates/README.md.jinja2 b/infra/templates/README.md.jinja2 index ccaadc29ff0..2c92401f83d 100644 --- a/infra/templates/README.md.jinja2 +++ b/infra/templates/README.md.jinja2 @@ -17,7 +17,7 @@ ## Join us on Slack! -👋👋👋 [Come say hi on Slack!](https://communityinviter.com/apps/feastopensource/feast-the-open-source-feature-store) +👋👋👋 [Come say hi on Slack!](https://slack.feast.dev/) [Check out our DeepWiki!](https://deepwiki.com/feast-dev/feast) diff --git a/infra/website/docs/blog/entity-less-historical-features-retrieval.md b/infra/website/docs/blog/entity-less-historical-features-retrieval.md new file mode 100644 index 00000000000..ec907fe26de --- /dev/null +++ b/infra/website/docs/blog/entity-less-historical-features-retrieval.md @@ -0,0 +1,142 @@ +--- +title: Historical Features Without Entity IDs +description: Feast now supports entity-less historical feature retrieval by datetime range—making it easier to train models when you don't have or need entity IDs. +date: 2026-02-19 +authors: ["Jitendra Yejare", "Aniket Paluskar"] +--- + +# Historical Features Without Entity IDs + +For years, Historical Feature Retrieval in Feast required an **entity dataframe**; you had to supply the exact entity keys (e.g. `driver_id`, `user_id`) and timestamps you wanted to join features for. That works well when you have a fixed set of entities—for example, a list of users you want to score or a training set already keyed by IDs. But in many AI and ML projects, you **don’t have** entity IDs upfront, or the problem **doesn’t naturally have** entities at all. In those cases, being forced to create and pass an entity dataframe was a real friction. + +We’re excited to share that Feast now supports **entity-less historical feature retrieval** based on a **datetime range**. You can pull all historical feature data for a time window without specifying any entity dataframe—addressing the long-standing [GitHub issue #1611](https://github.com/feast-dev/feast/issues/1611) and simplifying training and tuning workflows where entity IDs are optional or irrelevant. + +# The Problem: Entity IDs Aren’t Always There + +Classic use of a feature store looks like this: + +```python +entity_df = pd.DataFrame({ + "driver_id": [1001, 1002, 1003], + "event_timestamp": [datetime(2025, 1, 1), datetime(2025, 1, 2), datetime(2025, 1, 3)] +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], +).to_df() +``` + +You already have a set of entities and timestamps; Feast joins features onto them. But in many real-world setups: + +- **Time-series and sequence models** – You care about a time range and all data in it, not a pre-defined list of entity IDs. Building an entity dataframe means first querying “who existed in this period?” and then passing those IDs in, which is extra plumbing and can be expensive. +- **Global or population-level models** – You’re modeling aggregates, trends, or system-wide behavior. There may be no natural “entity” to key on, or you want “all entities” in a window. +- **Exploratory analysis and research** – You want “all features in the last 7 days” to experiment with models or features. Requiring entity IDs forces you to materialize a full entity list before you can even call the feature store. +- **Cold start and new users** – When training models that will later serve new or rarely-seen entities (e.g. recommendation cold start, fraud detection for new accounts), you often don’t have a fixed, known entity set at training time. You want to train on “all entities that had activity in this window” so the model generalizes from the full population. +- **Batch training on full history** – You want to train on all available history in a date range. Generating and passing a huge entity dataframe is cumbersome and sometimes not even possible if the entity set is large or dynamic. + +In all these cases, **passing entity IDs is either not possible, not required, or unnecessarily complex**. Making the entity dataframe optional and supporting retrieval by datetime range makes the feature store much easier to use in production and in research. + +# What’s New: Optional Entity DataFrame and Date Range + +Feast now supports entity-less historical feature retrieval by datetime range for several offline stores; you can pull historical feature data for a time window without specifying any entity dataframe. You specify a time window (and optionally rely on TTL for defaults), and the offline store returns all feature data in that range. + +- **Entity dataframe is optional** – You can omit `entity_df` and use `start_date` and/or `end_date` instead. +- **Point-in-time correctness** – Retrieval still uses point-in-time semantics (e.g. LATERAL joins in the offline stores) so you get correct historical values. +- **Smart defaults** – If you don’t pass `start_date`, the range can be derived from the feature view TTL; if you don’t pass `end_date`, it defaults to “now”. +- **Backward compatible** – The existing entity-based API is unchanged. When you have an entity dataframe (e.g. for ODFV or targeted batch scoring), you keep using it with entity dataframe as before. + +Entity-less retrieval is supported across multiple offline stores: **Postgres** (where it was first introduced), **Dask**, **Spark**, and **Ray**—with Spark and Ray being especially important for large-scale and distributed training workloads. More offline stores will be supported in the future based on user demand and priority. + +# How to Use It + +You can use any of these patterns depending on how much you want to specify. + +**1. Explicit date range (data between start and end):** + +```python +training_df = store.get_historical_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + start_date=datetime(2025, 7, 1, 1, 0, 0), + end_date=datetime(2025, 7, 2, 3, 30, 0), +).to_df() +``` + +**2. Only end date (Start date is end date minus TTL):** + +```python +training_df = store.get_historical_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + end_date=datetime(2025, 7, 2, 3, 30, 0), +).to_df() +``` + +**3. Only start date (data from start date to now):** + +```python +training_df = store.get_historical_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + start_date=datetime(2025, 7, 1, 1, 0, 0), +).to_df() +``` + +**4. No dates (data from TTL window to now):** + +```python +training_df = store.get_historical_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], +).to_df() +``` + +**5. Entity-based retrieval still works (e.g. for ODFV or when you need data for specific entities):** + +```python +entity_df = pd.DataFrame.from_dict({ + "driver_id": [1005], + "event_timestamp": [datetime(2025, 6, 29, 23, 0, 0)], +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "transformed_conv_rate:conv_rate_plus_val1", + ], +).to_df() +``` + +Feast does not support mixing entity-based and range-based retrieval in one call; either pass `entity_df` or pass `start_date`/`end_date`, not both. + +# Try It Out + +To experiment with entity-less retrieval: + +1. Use a feature store backed by an offline store that supports it: **Postgres**, **Dask**, **Spark**, or **Ray** (see [Feast docs](https://docs.feast.dev/) for setup). Spark and Ray are a great fit for distributed and large-scale training. +2. Call `get_historical_features()` with only `features` and, as needed, `start_date` and `end_date` (or rely on TTL and default end time). +3. For full details, tests, and behavior, see [PR #5527](https://github.com/feast-dev/feast/pull/5527) and the updated [FAQ on historical retrieval without an entity dataframe](https://docs.feast.dev/getting-started/faq#how-do-i-run-get_historical_features-without-providing-an-entity-dataframe). + +# Why This Makes Production Easier + +- **Simpler training pipelines** – No need to pre-query “all entity IDs in range” or maintain a separate entity table just to call the feature store. You specify a time window and get features. +- **Fewer moving parts** – Less code, fewer joins, and fewer failure modes when you don’t need entity-based slicing. +- **Better fit for time-range-centric workflows** – Time-series, global models, and exploratory jobs can all use the same API without artificial entity construction. +- **Same point-in-time guarantees** – Entity-less retrieval still respects feature view TTL and temporal correctness, so your training data remains valid. + +We’re excited to see how the community uses entity-less historical retrieval. If you have feedback or want to help bring this to more offline stores, join the discussion on [GitHub issue #1611](https://github.com/feast-dev/feast/issues/1611) or [Feast Slack](https://slack.feast.dev). diff --git a/infra/website/docs/blog/feast-agents-mcp.md b/infra/website/docs/blog/feast-agents-mcp.md new file mode 100644 index 00000000000..bfa46ee8b02 --- /dev/null +++ b/infra/website/docs/blog/feast-agents-mcp.md @@ -0,0 +1,363 @@ +--- +title: "Building AI Agents with Feast: Feature Stores as Context and Memory" +description: "How Feast's MCP integration turns your feature store into a governed context and memory layer for AI agents, bridging the gap between experimental agents and production-ready systems." +date: 2026-04-11 +authors: ["Nikhil Kathole"] +--- + +
+ AI Agents powered by Feast Feature Store +
+ +AI agents are moving from demos to production. They handle customer support, orchestrate complex workflows, and make real-time decisions that affect business outcomes. But there is a gap between a working prototype and a production system: agents need reliable, low-latency access to structured data, they need to remember what happened in prior interactions, and all of this access needs to be governed. + +This is where feature stores enter the picture. In this post, we show how **Feast** -- an open-source feature store -- can serve as both the **context provider** and the **persistent memory layer** for AI agents, using the **Model Context Protocol (MCP)**. + +## The Problem: Agents Need Context, Memory, and Governance + +A standalone LLM knows nothing about your customers, your products, or your internal processes. To make good decisions, agents need **tools** that give them access to real data: + +- **Who is this user?** Their plan tier, account age, purchase history, satisfaction score. +- **What do we know about this topic?** Relevant documentation, knowledge-base articles, FAQs. +- **What happened before?** What did this agent discuss with this customer last time? What was left unresolved? + +That last point is critical and often overlooked. Most agent demos are stateless -- every conversation starts from scratch. But real support interactions build on prior context: *"I called about this yesterday"*, *"you said you'd escalate"*, *"I prefer email over chat."* An agent without memory cannot handle these. + +Without a proper data layer, teams end up writing ad-hoc database queries, hardcoding API calls, stuffing memory into Redis with no governance, or giving agents raw database access. This creates fragile, ungoverned, and hard-to-audit agent systems. + +## Feature Stores Solve This -- Including Memory + +Feature stores were built to solve exactly this class of problem -- albeit originally for traditional ML. They provide: + +1. **Low-latency online serving** of pre-computed features. +2. **Versioned, governed access** to data with RBAC and audit trails. +3. **Consistency** between training/offline and serving/online environments. +4. **A single abstraction** over diverse data sources (databases, data warehouses, streaming systems, and vector stores -- including Milvus, Elasticsearch, Qdrant, PGVector, and FAISS). +5. **Entity-keyed read/write** -- the same mechanism that serves features can also store and retrieve agent memory, keyed by customer ID, session ID, or any entity. + +With Feast's MCP support, these capabilities are exposed as **tools that AI agents can discover and call dynamically** -- and critically, agents can **write back** to the feature store, turning it into a governed memory layer. + +## Feast + MCP: Turning a Feature Store into an Agent Tool + +The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that lets AI applications discover and interact with external tools through a unified interface. Feast's feature server can now expose its endpoints as MCP tools with a simple configuration change: + +```yaml +feature_server: + type: mcp + enabled: true + mcp_enabled: true + mcp_transport: http + mcp_server_name: "feast-feature-store" + mcp_server_version: "1.0.0" +``` + +Once enabled, any MCP-compatible agent -- whether built with LangChain, LlamaIndex, CrewAI, AutoGen, or a custom framework -- can connect to `http://your-feast-server/mcp` and discover available tools like `get-online-features` for entity-based retrieval, `search` for vector similarity search, `vector_store_search` for OpenAI-compatible text search, and `write-to-online-store` for persisting agent state. + +## A Concrete Example: Customer-Support Agent with Memory + +To make this tangible, let's walk through a customer-support agent that uses Feast for structured feature retrieval, document search, and persistent memory. + +> **Note on the implementation:** This example builds the agent loop from scratch using the OpenAI tool-calling API and the MCP Python SDK -- no framework required. All Feast interactions use the MCP protocol: the agent connects to Feast's MCP endpoint, discovers available tools via `session.list_tools()`, and invokes them via `session.call_tool()`. We chose this approach to keep dependencies minimal and make every Feast interaction visible. In production, you would typically use a framework like LangChain/LangGraph, LlamaIndex, CrewAI, or AutoGen. Because Feast exposes a standard MCP endpoint, any of these frameworks can auto-discover the tools with zero custom code (see [Connecting Your Agent Framework](#connecting-your-agent-framework) below). + +### The Setup + +We define three feature views in Feast: + +**Customer profiles** -- structured data served from the online store: + +```python +customer_profile = FeatureView( + name="customer_profile", + entities=[customer], + schema=[ + Field(name="name", dtype=String), + Field(name="email", dtype=String), + Field(name="plan_tier", dtype=String), + Field(name="account_age_days", dtype=Int64), + Field(name="total_spend", dtype=Float64), + Field(name="open_tickets", dtype=Int64), + Field(name="satisfaction_score", dtype=Float64), + ], + source=customer_profile_source, + ttl=timedelta(days=1), +) +``` + +**Knowledge base** -- support articles stored as vector embeddings (Feast supports multiple vector backends including Milvus, Elasticsearch, Qdrant, PGVector, and FAISS -- this example uses Milvus): + +```python +knowledge_base = FeatureView( + name="knowledge_base", + entities=[document], + schema=[ + Field( + name="vector", dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="title", dtype=String), + Field(name="content", dtype=String), + Field(name="category", dtype=String), + ], + source=knowledge_base_source, + ttl=timedelta(days=7), +) +``` + +**Agent memory** -- per-customer interaction state written back by the agent: + +```python +agent_memory = FeatureView( + name="agent_memory", + entities=[customer], + schema=[ + Field(name="last_topic", dtype=String), + Field(name="last_resolution", dtype=String), + Field(name="interaction_count", dtype=Int64), + Field(name="preferences", dtype=String), + Field(name="open_issue", dtype=String), + ], + ttl=timedelta(days=30), +) +``` + +This is the key insight: Feast is not just providing context to the agent -- it is also **storing the agent's memory**. The `agent_memory` feature view is entity-keyed by customer ID, TTL-managed (30-day expiration), schema-typed, and governed by the same RBAC as every other feature. The agent reads prior interactions via `recall_memory`, and memory is automatically checkpointed after each turn using the same online store infrastructure. + +### The Agent Loop + +The agent connects to Feast's MCP endpoint, discovers tools dynamically, and uses the MCP protocol for all Feast interactions. Each round, the LLM sees the conversation history plus the available read tools, and decides what to do. Memory is saved automatically after the turn -- framework-style, not as an LLM decision: + +```python +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +async with streamablehttp_client("http://localhost:6566/mcp") as (r, w, _): + async with ClientSession(r, w) as session: + await session.initialize() + tools = await session.list_tools() # discover Feast tools + + for round in range(MAX_ROUNDS): + response = call_llm(messages, tools=[...]) + + if response.finish_reason == "stop": + break + + for tool_call in response.tool_calls: + result = await session.call_tool(name, args) # MCP + messages.append(tool_result(result)) + + # Framework-style checkpoint: auto-save via MCP + await session.call_tool("write_to_online_store", {...}) +``` + +A typical multi-turn flow looks like: + +1. **Round 1**: Agent calls `recall_memory("C1001")` -- checks for prior interactions. Calls `lookup_customer("C1001")` -- gets profile data. Calls `search_knowledge_base("SSO setup")` -- finds the relevant article. +2. **Round 2**: Has enough context. Generates a personalised response and returns it. +3. **Checkpoint**: The framework auto-saves `topic="SSO setup"` and a resolution summary to Feast. + +When C1001 comes back later and says *"I'm following up on my SSO question"*, the agent calls `recall_memory` and immediately knows what was discussed -- no re-explanation needed. + +For a simpler question like *"What's my current plan?"*, the agent only calls `lookup_customer` -- skipping the knowledge base entirely. The LLM makes these routing decisions based on the question, not hardcoded logic. + +### Memory as Infrastructure + +Production agent frameworks treat persistence as infrastructure, not an LLM decision: + +- **LangGraph** uses checkpointers (`MemorySaver`, `PostgresSaver`) that auto-save state after every graph step, keyed by `thread_id`. +- **CrewAI** enables `memory=True` for automatic short-term, long-term, and entity memory. +- **AutoGen** uses post-conversation hooks to extract and store learnings. + +This demo follows the same pattern. The LLM has three read tools for reasoning; memory is checkpointed by the framework after each turn via Feast's `write-to-online-store` endpoint. This ensures consistent, reliable memory regardless of LLM behaviour -- no risk of the model forgetting to save or writing inconsistent state. Feast is a natural fit for this checkpoint layer because it provides entity-keyed storage, TTL-managed expiration, schema enforcement, RBAC governance, and offline queryability -- all out of the box. + +
+ Feast MCP Agent Workflow — agent loop with context retrieval, vector search, and memory persistence through Feast +
+ +The agent doesn't need to know *where* the data lives or *which* vector database is behind the scenes. It calls Feast through a standard protocol, and Feast handles the routing to the right store -- whether that's Milvus, Elasticsearch, Qdrant, PGVector, or FAISS. Swapping the vector backend is a configuration change, not a code change. The same protocol handles both reads and writes -- context retrieval and memory persistence use the same governed infrastructure. + +### The Response + +Instead of a generic answer, the agent produces something like: + +> *"Hi Alice! Since you're on our Enterprise plan, SSO is available for your team. Go to Settings > Security > SSO and enter your Identity Provider metadata URL. We support SAML 2.0 and OIDC. Once configured, all team members will authenticate through your IdP. As an Enterprise customer, you also have a dedicated Slack channel and account manager if you need hands-on help."* + +The personalisation (mentioning the Enterprise plan, dedicated Slack channel) comes directly from the Feast features. And if Alice calls back next week, the agent already knows what was discussed. + +## Why This Matters for Production + +### Unified Data Access: Structured Features + Vector Search + Memory + +Real-world agents need more than document retrieval. They need access to multiple data types through a single governed interface: + +| Data Type | Example | Feast Capability | +|---|---|---| +| Structured features | Account tier, spend history | `get_online_features` | +| Vector embeddings | Support article search | `retrieve_online_documents_v2` | +| Agent memory | Last interaction topic, open issues | `get_online_features` + `write_to_online_store` | +| Streaming features | Real-time click counts | Push sources with `write_to_online_store` | +| Pre-computed predictions | Churn probability | Served alongside other features | + +Feast unifies all of these behind a single API that agents can both read from and write to. + +### Context Memory: Why Feast Beats Ad-Hoc Solutions + +Many teams use Redis, in-memory dicts, or custom databases for agent memory. Feast provides a better foundation: + +| Concern | Ad-hoc Memory | Feast Memory | +|---|---|---| +| **Governance** | No RBAC, no audit trail | Same RBAC and permissions as all features | +| **TTL management** | Manual expiration logic | Declarative TTL on the feature view | +| **Entity-keying** | Custom key design | Native entity model (customer_id, session_id, etc.) | +| **Observability** | Custom logging | Integrated with OpenTelemetry and MLflow traces | +| **Offline analysis** | Separate export pipeline | Memory is just another feature -- queryable offline | +| **Schema evolution** | Unstructured blobs | Typed schema with versioning | + +Because agent memory is stored as a standard Feast feature view, it inherits all the infrastructure that already exists for serving ML features: monitoring, access control, TTL management, and offline queryability. There is no separate system to operate. + +### Governance: Who Can Access What + +In production, you do not want every agent to access every feature. Feast provides: + +- **RBAC**: Role-based access control with OIDC integration. +- **Feature-level permissions**: Control which feature views each service account can read or write. +- **Audit trails**: Track which agent accessed which features and when. + +This is especially important for memory: you want governance over what agents remember and who can read those memories. + +### Production Platform Architecture + +Deploying agents in production requires more than just the agent code. A well-architected platform wraps agents in enterprise infrastructure without requiring changes to the agent itself. Feast fits naturally into this layered approach: + +
+ Production platform architecture — Feast MCP Server behind MCP Gateway with observability, guardrails, and lifecycle management +
+ +- **MCP Gateway**: Feast sits behind an Envoy-based MCP Gateway as one of many tool servers. The gateway provides identity-based tool filtering -- an agent's JWT claims determine whether it can call Feast at all, and which features it can access. +- **Sandboxed Execution**: Feast runs as a standard Kubernetes service, benefiting from sandboxed container isolation that keeps agent workloads separated. +- **Observability**: Feast feature-retrieval and memory-write calls flow through the platform's OpenTelemetry pipeline, appearing in MLflow traces alongside LLM calls and tool invocations. +- **Agent Lifecycle Management (Kagenti)**: An operator like Kagenti can discover Feast as a tool server via AgentCard CRDs and inject tracing and governance without code changes. + +The principle is straightforward: the agent is yours, the platform provides the guardrails, and Feast provides the data and memory. + +## Connecting Your Agent Framework + +Since Feast exposes a standard MCP endpoint, integration is framework-agnostic: + +**LangChain / LangGraph:** +```python +from langchain_mcp_adapters.client import MultiServerMCPClient +from langgraph.prebuilt import create_react_agent + +async with MultiServerMCPClient( + {"feast": {"url": "http://feast-server:6566/mcp", "transport": "streamable_http"}} +) as client: + tools = client.get_tools() + agent = create_react_agent(llm, tools) + result = await agent.ainvoke({"messages": "How do I set up SSO?"}) +``` + +**LlamaIndex:** +```python +from llama_index.tools.mcp import aget_tools_from_mcp_url +from llama_index.core.agent.function_calling import FunctionCallingAgent +from llama_index.llms.openai import OpenAI + +tools = await aget_tools_from_mcp_url("http://feast-server:6566/mcp") +agent = FunctionCallingAgent.from_tools(tools, llm=OpenAI(model="gpt-4o-mini")) +response = await agent.achat("How do I set up SSO?") +``` + +**Claude Desktop / Cursor:** +```json +{ + "mcpServers": { + "feast": { + "url": "http://feast-server:6566/mcp", + "transport": "streamable_http" + } + } +} +``` + +**Direct REST API:** +```python +import requests + +features = requests.post("http://feast-server:6566/get-online-features", json={ + "features": ["customer_profile:plan_tier", "customer_profile:satisfaction_score"], + "entities": {"customer_id": ["C1001"]}, +}).json() +``` + +The Feast-specific integration is just connecting to the MCP endpoint and getting the tools. Once you have them, building the agent follows each framework's standard patterns -- the tool-calling loop, message threading, and state persistence are handled natively. Feast's MCP endpoint means zero custom wiring. + +### Customizing for Your Use Case + +This demo's system prompt, tool names, and feature views are all specific to the customer-support scenario. Here's what changes when you build your own agent: + +| What | This demo | Your agent | +|---|---|---| +| **Feature views** | `customer_profile`, `knowledge_base`, `agent_memory` | Define your own in `features.py` (e.g., `product_catalog`, `order_history`, `fraud_signals`) | +| **System prompt** | "Call recall_memory and lookup_customer first..." | Write instructions specific to your domain and workflow | +| **Tool wrappers** | `lookup_customer`, `search_knowledge_base`, `recall_memory` | Optional -- see below | + +**Do you need custom tool wrappers?** It depends on how you build your agent: + +- **With a framework (LangChain, LlamaIndex, etc.):** No. The framework discovers Feast's generic MCP tools (`get_online_features`, `retrieve_online_documents`, `write_to_online_store`) automatically. The LLM calls them directly with your feature view names and entities. No wrapper code needed. + +- **With a raw loop (like this demo):** Optional but recommended. This demo wraps `get_online_features` into `lookup_customer` and `recall_memory` to give the LLM friendlier, domain-specific tool names. You'd create similar wrappers for your use case (e.g., `check_inventory`, `get_order_status`). The wrappers are thin -- they just call the Feast MCP tool with the right feature names and entities. + +**What stays the same** regardless of use case: Feast's MCP server, the online/offline store infrastructure, RBAC, TTL management, and the auto-save memory pattern. You define feature views, `feast apply`, start the server, and connect -- the same three generic MCP tools serve any domain. + +## Try It Yourself + +We have published a complete working example in the Feast repository. A single script handles setup, server startup, and the demo: + +```bash +git clone https://github.com/feast-dev/feast.git +cd feast/examples/agent_feature_store + +./run_demo.sh # demo mode (no API key needed) +OPENAI_API_KEY=sk-... ./run_demo.sh # live LLM tool-calling +``` + +The script installs dependencies, generates sample data (customer profiles, knowledge-base articles, and the agent memory scaffold), starts the Feast MCP server, runs the agent, and tears everything down on exit. + +To run with a real LLM, set the API key and (optionally) the base URL and model: + +```bash +# OpenAI +export OPENAI_API_KEY="sk-..." # pragma: allowlist secret +./run_demo.sh + +# Ollama (free, local -- no API key needed) +ollama pull llama3.1:8b +export OPENAI_API_KEY="ollama" # pragma: allowlist secret +export OPENAI_BASE_URL="http://localhost:11434/v1" +export LLM_MODEL="llama3.1:8b" +./run_demo.sh + +# Any OpenAI-compatible provider (Azure, vLLM, etc.) +export OPENAI_API_KEY="your-key" # pragma: allowlist secret +export OPENAI_BASE_URL="https://your-endpoint/v1" +export LLM_MODEL="your-model" +./run_demo.sh +``` + +The agent demonstrates memory continuity: when the same customer returns, the agent recalls what was discussed previously. + +See the full example on [GitHub](https://github.com/feast-dev/feast/tree/master/examples/agent_feature_store). + +## What's Next + +The intersection of feature stores and agentic AI is just getting started. Here is what we are working on: + +- **Richer MCP tools**: Exposing `retrieve_online_documents_v2` as a first-class MCP tool for native vector search. +- **Memory patterns**: Expanding the memory model to support session-scoped memory, hierarchical summarisation, and cross-agent shared memory. +- **Platform integration**: First-class support in MCP Gateway tool catalogs and agent lifecycle operators. +- **Streaming features for agents**: Real-time feature updates from Kafka/Flink that agents can subscribe to. + +## Join the Conversation + +We would love to hear how you are using (or plan to use) Feast in your agent workflows. Reach out on [Slack](https://slack.feast.dev/) or [GitHub](https://github.com/feast-dev/feast) -- and give the example a try! diff --git a/infra/website/docs/blog/feast-data-quality-monitoring.md b/infra/website/docs/blog/feast-data-quality-monitoring.md new file mode 100644 index 00000000000..2c918c83a63 --- /dev/null +++ b/infra/website/docs/blog/feast-data-quality-monitoring.md @@ -0,0 +1,224 @@ +--- +title: Data Quality Monitoring in Feast 0.64 +description: Feast 0.64 adds native data quality monitoring with baseline metrics, batch and serving-log analysis, REST APIs, CLI workflows, and a built-in monitoring UI. +date: 2026-06-26 +authors: ["Jitendra Yejare", "Nikhil Kathole", "Francisco Javier Arceo"] +--- + +
+ Feast Data Quality Monitoring +
+ +# Data Quality Monitoring in Feast 0.64 + +Serving ML models in production is extremely hard. + +The reason is simple: production models depend on data from many different places. Every source system has some probability of operational error: a delayed pipeline, a schema change, a column that starts producing nulls, a categorical value that changes meaning, a late partition, a silent backfill, or a service that behaves differently under production traffic. + +The more data sources a model depends on, the more chances there are for one of those systems to drift, fail, or change underneath you. That creates a basic tension in ML systems. Models are data hungry and often benefit from orthogonal features from many upstream systems, but every additional upstream dependency increases operational risk. What ML wants for predictive power can conflict with what engineering wants for reliability. + +The only way to manage that tension is to monitor what is actually happening in production. Feature quality problems rarely arrive as neat exceptions. A model may keep serving predictions while one upstream table starts producing nulls, a batch pipeline shifts a numeric distribution, or production requests drift away from the training baseline. By the time these issues show up in model metrics, the debugging path usually crosses feature definitions, data sources, materialization jobs, and serving logs. + +Feast 0.64 adds a native data quality monitoring system that brings those signals directly into the feature store. Instead of relying on a separate validation framework, Feast can now compute, store, serve, and visualize feature-level statistics across batch data and logged serving data. + +The biggest change is that monitoring is now a first-class Feast workflow: + +- `feast apply` can compute baseline metrics for registered feature views +- `feast monitor run` can compute scheduled daily, weekly, biweekly, monthly, and quarterly metrics +- REST endpoints expose monitoring jobs, per-feature metrics, aggregate feature-view and feature-service metrics, baselines, and time series +- the Feast UI includes a Monitoring page with filters, summary tabs, feature drilldowns, histograms, and time-series charts +- compute is pushed into supported offline stores where possible, with a Python fallback for other backends + +## From validation to monitoring + +Feast previously supported an external-library-based validation path for historical retrievals. That integration was useful, but it lived outside the normal feature store workflow: users had to install extra dependencies, write profiler code, and run validation against saved datasets. + +That original integration proved the need for data quality inside Feast. It helped answer an important question: after generating a training dataset, does this dataset satisfy the expectations we care about? + +But production feature quality problems usually happen after the training dataset is generated. A pipeline may keep running while an upstream producer changes a column, shifts a distribution, starts sending nulls, or changes the meaning of a categorical value. In those cases, the feature code may be perfectly correct while the data feeding it has changed. + +Feast needed monitoring that was closer to the system that actually computes and serves features. By coupling DQM to Feast's compute engines and offline stores, Feast can compute quality metrics where the data already lives, reuse feature metadata, compare batch and serving-log distributions, and expose the results through the same CLI, REST API, and UI used to operate the feature store. + +This also helps when teams maintain multiple feature execution paths. For example, a feature may be generated one way for training and another way for low-latency serving or streaming. DQM is not a formal proof that two implementations are equivalent, but distribution metrics, baselines, and serving-log comparisons provide an early warning when those paths start producing meaningfully different values. + +The new system is broader and more operational. It automatically computes statistical profiles for registered features, stores them in monitoring tables, and makes them available to the CLI, REST API, and UI. This gives teams the kind of feature health view they need after features are already in production, not only during one historical retrieval. + +For each feature, Feast can track: + +| Metric family | Examples | +|---|---| +| completeness | row count, null count, null rate | +| numeric profile | mean, standard deviation, min, max | +| percentiles | p50, p75, p90, p95, p99 | +| distributions | numeric histograms or categorical top values | +| aggregate health | feature-view and feature-service summaries | + +## Baselines start at registration + +The simplest way to turn on monitoring is to enable DQM in `feature_store.yaml`: + +```yaml +data_quality_monitoring: + auto_baseline: true +``` + +When `auto_baseline` is enabled, `feast apply` computes baseline metrics for feature views that do not already have one. The baseline is marked as the reference distribution and can be compared with later scheduled metrics. + +That matters because the baseline lives next to the feature definitions. When a feature view is registered, Feast can also capture what "normal" looked like at registration time. Later monitoring runs can answer whether the current data still resembles that baseline. + +For Feast Operator deployments, the same setting is available on the `FeatureStore` custom resource: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +spec: + feastProject: my_project + dataQualityMonitoring: + autoBaseline: true +``` + +## Scheduled monitoring with the CLI + +For ongoing monitoring, schedule: + +```bash +feast monitor run +``` + +In auto mode, Feast detects the latest event timestamp in the source data and computes metrics across the supported granularities: daily, weekly, biweekly, monthly, and quarterly. + +You can also scope monitoring to a specific feature view: + +```bash +feast monitor run --feature-view driver_stats +``` + +Or compute a specific window and mark it as a baseline: + +```bash +feast monitor run \ + --feature-view driver_stats \ + --start-date 2025-01-01 \ + --end-date 2025-03-31 \ + --granularity daily \ + --set-baseline +``` + +This makes the CLI easy to wire into Airflow, Kubeflow Pipelines, cron, or any scheduler that already runs Feast materialization jobs. + +## Monitoring serving logs + +Batch data tells you whether source features look healthy. Serving logs tell you what your models actually received. + +If a `FeatureService` has logging configured, Feast can compute monitoring metrics from the logged online features: + +```bash +feast monitor run --source-type log +``` + +You can also run batch and log monitoring together: + +```bash +feast monitor run --source-type all +``` + +Log metrics are stored with `data_source_type="log"` alongside batch metrics. Feast normalizes logged feature names back to the feature view and feature name, which lets the UI and API compare batch and serving distributions without forcing users to maintain a separate mapping. + +## The new Monitoring UI + +The most visible 0.64 improvement is the Monitoring page in the Feast UI. It turns DQM from a background job into something feature owners can inspect without leaving Feast. + +The page includes three main tabs: + +| Tab | What it shows | +|---|---| +| Features | per-feature metrics such as null rate, row count, freshness, and health | +| Feature Views | aggregate quality summaries per feature view | +| Feature Services | aggregate quality summaries for model-facing feature services | + +At the top of the page, users can filter by feature view, granularity, source type, and date range. Baseline is treated as its own view because it represents all baseline data rather than a normal date window. The page also includes a Compute Metrics action that triggers DQM computation from the UI, plus Refresh for reloading already computed results. + +
+ Feast DQM Monitoring dashboard showing feature metrics, filters, histograms, and health status +
+ +Clicking a feature opens a detail page with: + +- a distribution chart for numeric histograms or categorical values +- a statistics panel with null rate, mean, standard deviation, min, max, and percentiles +- a granularity selector that can switch between computed windows and baseline +- time-series charts for metric drift, including aggregate statistics and null-rate trends + +
+ Feast DQM numeric feature detail page with distribution chart, statistics, and time-series analysis +
+ +
+ Feast DQM categorical feature detail page with category distribution and statistics +
+ +This is the workflow we wanted: feature owners can start from a table of health signals, filter down to the part of the feature store they care about, and then drill into the exact feature whose distribution changed. + +## How compute engines fit in + +DQM is intentionally tied to Feast's compute and offline-store architecture. The goal is to compute metrics where the data already lives whenever possible, then store the results in backend-specific monitoring tables. + +Supported backends push computation into the underlying system: + +| Backend | Compute path | Storage path | +|---|---|---| +| PostgreSQL | SQL push-down | `INSERT ON CONFLICT` | +| Snowflake | SQL push-down | `MERGE` with JSON metrics | +| BigQuery | SQL push-down | BigQuery `MERGE` | +| Redshift | SQL push-down | Data API-backed writes | +| Spark | SparkSQL push-down | Parquet-backed tables | +| Oracle | SQL through Ibis | `MERGE` | +| DuckDB | in-memory SQL | Parquet files | +| Dask | PyArrow compute | Parquet files | + +For backends without native monitoring support, Feast falls back to pulling data through the offline store and computing metrics with PyArrow and NumPy. That fallback keeps the API consistent while still allowing mature warehouse and distributed engines to do the heavy lifting. + +This design is especially important for larger feature stores. A null-rate or histogram job should not require exporting a warehouse table into a separate monitoring system. If the feature data already lives in Snowflake, BigQuery, Spark, Redshift, or another supported backend, Feast can push the computation closer to that data. + +Feast 0.64 also adds the Apache Flink compute engine, continuing the broader move toward a unified compute-engine model. DQM follows the same direction: feature quality checks should be part of the feature platform's execution model, not a sidecar that every team wires up differently. + +## REST APIs for automation + +The UI and CLI are built on top of monitoring APIs that can also be used by external systems: + +| Method | Endpoint | Use | +|---|---|---| +| `POST` | `/monitoring/compute` | submit a batch DQM job | +| `POST` | `/monitoring/auto_compute` | auto-detect dates and compute all granularities | +| `POST` | `/monitoring/compute/transient` | compute ad hoc metrics without storing them | +| `POST` | `/monitoring/compute/log` | compute metrics from serving logs | +| `POST` | `/monitoring/auto_compute/log` | auto-compute log metrics | +| `GET` | `/monitoring/jobs/{job_id}` | read DQM job status | +| `GET` | `/monitoring/metrics/features` | read per-feature metrics | +| `GET` | `/monitoring/metrics/feature_views` | read feature-view summaries | +| `GET` | `/monitoring/metrics/feature_services` | read feature-service summaries | +| `GET` | `/monitoring/metrics/baseline` | read baseline metrics | +| `GET` | `/monitoring/metrics/timeseries` | read trend data for charts and alerts | + +The transient compute endpoint is useful for exploration. If someone wants to inspect a very specific date range, Feast can compute fresh metrics and return them directly without storing them as part of the scheduled monitoring history. + +## Production shape + +A typical production setup now looks like this: + +1. Add `data_quality_monitoring.auto_baseline: true` to `feature_store.yaml` +2. Run `feast apply` to register features and compute baseline metrics +3. Schedule `feast monitor run` for batch metrics +4. Enable feature-service logging and schedule `feast monitor run --source-type log` for production serving metrics +5. Use the UI to investigate feature health and distribution changes +6. Use REST APIs to connect monitoring results to alerting, orchestration, or custom dashboards + +Monitoring also respects Feast's existing authorization model. Compute operations require update permissions, while reads and transient exploration require describe permissions. That keeps the new DQM surface aligned with the rest of the registry and feature-store API. + +## What's next + +Feast 0.64 makes DQM part of the feature store instead of an integration around it. The release adds the backend compute path, the CLI, the REST API, and the UI surface in one coherent workflow. + +The next step for users is simple: enable baselines, run monitoring jobs on the same cadence as your data pipelines, and use the UI to make feature quality visible to the teams that own production models. + +For setup details, see the [Feature Quality Monitoring guide](/docs/how-to-guides/feature-monitoring) and the [0.64.0 changelog](https://github.com/feast-dev/feast/blob/master/CHANGELOG.md#0640-2026-06-13). diff --git a/infra/website/docs/blog/feast-dbt-integration.md b/infra/website/docs/blog/feast-dbt-integration.md new file mode 100644 index 00000000000..5a8ce082af1 --- /dev/null +++ b/infra/website/docs/blog/feast-dbt-integration.md @@ -0,0 +1,457 @@ +--- +title: Streamlining AI Feature Engineering with Feast and dbt +description: Learn how to leverage your dbt transformations as Feast features to eliminate duplicate work and accelerate AI development. +date: 2026-01-27 +authors: ["Francisco Javier Arceo", "Yassin Nouh"] +--- + +
+ Feast and dbt Integration +
+ +# Streamlining AI Feature Engineering with Feast and dbt + +If you're a dbt user, you know the power of well-crafted data models. You've invested time building clean, tested, and documented transformations that your team relies on. Your dbt models represent the single source of truth for analytics, reporting, and increasingly—AI features. + +But here's the challenge: when your AI team wants to use these models for production predictions, they often need to rebuild the same transformations in their feature store. Your beautiful dbt models, with all their logic and documentation, end up getting reimplemented elsewhere. This feels like wasted effort, and it is. + +What if you could take your existing dbt models and put them directly into production for AI without rewriting anything? That's exactly what Feast's dbt integration enables. + +## Your dbt Models Are Already AI-Ready + +You've already done the hard work with dbt: + +- **Transformed raw data** into clean, aggregated tables +- **Documented your models** with column descriptions and metadata +- **Tested your logic** to ensure data quality +- **Organized your transformations** into a maintainable codebase + +These models are perfect for AI features. The aggregations you've built for your daily reports? Those are features. The customer attributes you've enriched? Features. The time-based calculations you've perfected? You guessed it—features. + +The problem isn't your models—they're great. The problem is getting them into a system that can serve them for real-time AI predictions with low latency and point-in-time correctness. + +## How Feast Brings Your dbt Models to Production AI + +Feast's dbt integration is designed with one principle in mind: **your dbt models should be the single source of truth**. Instead of asking you to rewrite your transformations, Feast reads your dbt project and automatically generates everything needed to serve those models for AI predictions. + +Here's how it works: + +1. **Tag your dbt models** that you want to use as features (just add `tags: ['feast']` to your config) +2. **Run `feast dbt import`** to automatically generate Feast definitions from your dbt metadata +3. **Deploy to production** using Feast's feature serving infrastructure + +Feast reads your `manifest.json` (the compiled output from `dbt compile`) and extracts: + +- Column names, types, and descriptions from your schema files +- Table locations from your dbt models +- All the metadata you've already documented + +Then it generates Python code defining Feast entities, data sources, and feature views—all matching your dbt models exactly. Your documentation becomes feature documentation. Your data types become feature types. Your models become production-ready features. + +The best part? **You don't change your dbt workflow at all.** Keep building models the way you always have. The integration simply creates a bridge from your dbt project to production AI serving. + +## See It In Action: From dbt Model to Production Features + +Let's walk through a real example. Imagine you're a data engineer at a ride-sharing company, and you've already built dbt models to track driver performance. Your analytics team loves these models, and now your AI team wants to use them to predict which drivers are likely to accept rides. + +Perfect use case. Let's take your existing dbt models to production AI in just a few steps. + +### Step 1: Install Feast with dbt Support + +First, ensure you have Feast installed with dbt support: + +```bash +pip install 'feast[dbt]' +``` + +### Step 2: Tag Your Existing dbt Model + +You already have a dbt model that computes driver metrics. All you need to do is add one tag to mark it for Feast: + +{% code title="models/features/driver_features.sql" %} +```sql +{{ config( + materialized='table', + tags=['feast'] -- ← Just add this tag! +) }} + +WITH driver_stats AS ( + SELECT + driver_id, + DATE(completed_at) as date, + AVG(rating) as avg_rating, + COUNT(*) as total_trips, + SUM(fare_amount) as total_earnings, + AVG(trip_duration_minutes) as avg_trip_duration + FROM {{ ref('trips') }} + WHERE status = 'completed' + GROUP BY driver_id, DATE(completed_at) +) + +SELECT + driver_id, + TIMESTAMP(date) as event_timestamp, + avg_rating, + total_trips, + total_earnings, + avg_trip_duration, + CASE WHEN total_trips >= 5 THEN true ELSE false END as is_active +FROM driver_stats +``` +{% endcode %} + +That's it. One tag. Your model doesn't change—it keeps working exactly as before for your analytics workloads. + +### Step 3: Use Your Existing Documentation + +You're probably already documenting your dbt models (and if you're not, you should be!). Feast uses this exact same documentation—no duplication needed: + +{% code title="models/features/schema.yml" %} +```yaml +version: 2 +models: + - name: driver_features + description: "Daily aggregated features for drivers including ratings and activity metrics" + columns: + - name: driver_id + description: "Unique identifier for the driver" + data_type: STRING + - name: event_timestamp + description: "Date of the feature computation" + data_type: TIMESTAMP + - name: avg_rating + description: "Average rating received from riders" + data_type: FLOAT64 + - name: total_trips + description: "Total number of completed trips" + data_type: INT64 + - name: total_earnings + description: "Total earnings in dollars" + data_type: FLOAT64 + - name: avg_trip_duration + description: "Average trip duration in minutes" + data_type: FLOAT64 + - name: is_active + description: "Whether driver completed 5+ trips (active status)" + data_type: BOOLEAN +``` +{% endcode %} + +Your column descriptions and data types become the feature documentation in Feast automatically. Write it once, use it everywhere. + +### Step 4: Compile Your dbt Project (As Usual) + +This is your normal dbt workflow—nothing special here: + +```bash +cd your_dbt_project +dbt compile +``` + +This creates `target/manifest.json` with all your model metadata—the same artifact you're already generating. + +### Step 5: See What Feast Found + +Use the Feast CLI to discover your tagged models: + +```bash +feast dbt list -m target/manifest.json --tag feast +``` + +You'll see output like: + +``` +Found 1 model(s): + +driver_features [tags: feast] + Table: my_project.my_dataset.driver_features + Description: Daily aggregated features for drivers including ratings and activity metrics +``` + +### Step 6: Import Your dbt Model to Feast + +Now for the magic—automatically generate production-ready feature definitions from your dbt model: + +```bash +feast dbt import -m target/manifest.json \ + --entity-column driver_id \ + --data-source-type bigquery \ + --tag feast \ + --output feature_repo/driver_features.py +``` + +In seconds, Feast generates a complete Python file with everything needed for production AI serving—all from your existing dbt model: + +{% code title="feature_repo/driver_features.py" %} +```python +""" +Feast feature definitions generated from dbt models. + +Source: target/manifest.json +Generated by: feast dbt import +""" + +from datetime import timedelta +from feast import Entity, FeatureView, Field +from feast.types import Bool, Float64, Int64, String +from feast.infra.offline_stores.bigquery_source import BigQuerySource + + +# Entities +driver_id = Entity( + name="driver_id", + join_keys=["driver_id"], + description="Entity key for dbt models", + tags={'source': 'dbt'}, +) + + +# Data Sources +driver_features_source = BigQuerySource( + name="driver_features_source", + table="my_project.my_dataset.driver_features", + timestamp_field="event_timestamp", + description="Daily aggregated features for drivers including ratings and activity metrics", + tags={'dbt.model': 'driver_features', 'dbt.tag.feast': 'true'}, +) + + +# Feature Views +driver_features_fv = FeatureView( + name="driver_features", + entities=[driver_id], + ttl=timedelta(days=1), + schema=[ + Field(name="avg_rating", dtype=Float64, description="Average rating received from riders"), + Field(name="total_trips", dtype=Int64, description="Total number of completed trips"), + Field(name="total_earnings", dtype=Float64, description="Total earnings in dollars"), + Field(name="avg_trip_duration", dtype=Float64, description="Average trip duration in minutes"), + Field(name="is_active", dtype=Bool, description="Whether driver completed 5+ trips (active status)"), + ], + online=True, + source=driver_features_source, + description="Daily aggregated features for drivers including ratings and activity metrics", + tags={'dbt.model': 'driver_features', 'dbt.tag.feast': 'true'}, +) +``` +{% endcode %} + +### Step 7: Apply to Your Feature Store + +Now you can use standard Feast commands to materialize these features: + +```bash +cd feature_repo +feast apply +feast materialize-incremental $(date -u +%Y-%m-%dT%H:%M:%S) +``` + +## What Just Happened? + +You just went from dbt model to production AI features without rewriting a single line of transformation logic. Your dbt model—with all its carefully crafted SQL, documentation, and testing—is now: + +- **Serving features in milliseconds** for real-time predictions +- **Maintaining point-in-time correctness** to prevent data leakage during training +- **Syncing with your data warehouse** automatically as your dbt models update +- **Self-documenting** using the descriptions you already wrote + +And here's the best part: when you update your dbt model (maybe you add a new column or refine your logic), just re-run `feast dbt import` and `feast apply`. Your production features stay in sync with your dbt source of truth. + +## Advanced Use Cases for dbt Users + +### Multiple Entity Support + +For features involving multiple entities (like user-merchant transactions), specify multiple entity columns: + +```bash +feast dbt import -m target/manifest.json \ + -e user_id \ + -e merchant_id \ + --tag feast \ + -o feature_repo/transaction_features.py +``` + +This creates a FeatureView with composite keys, useful for: +- Transaction features keyed by both user and merchant +- Interaction features for recommendation systems +- Many-to-many relationship features + +### Snowflake and Other Data Sources + +Feast's dbt integration supports multiple data warehouse backends: + +**Snowflake:** +```bash +feast dbt import -m manifest.json \ + -e user_id \ + -d snowflake \ + -o features.py +``` + +**File-based sources (Parquet, etc.):** +```bash +feast dbt import -m manifest.json \ + -e user_id \ + -d file \ + -o features.py +``` + +### Customizing Generated Code + +You can fine-tune the import with additional options: + +```bash +feast dbt import -m target/manifest.json \ + -e driver_id \ + -d bigquery \ + --timestamp-field created_at \ + --ttl-days 7 \ + --exclude-columns internal_id,temp_field \ + -o features.py +``` + +## Best Practices + +### 1. Establish a Tagging Convention + +Use dbt's configuration hierarchy to automatically tag entire directories: + +```yaml +# dbt_project.yml +models: + my_project: + features: + +tags: ['feast'] # All models in features/ get tagged +``` + +### 2. Maintain Rich Documentation + +Column descriptions from your dbt schema files become feature descriptions in Feast, creating a self-documenting feature catalog. Invest time in documenting your dbt models—it pays dividends in feature discoverability. + +### 3. Integrate with CI/CD + +Automate feature definition updates in your deployment pipeline: + +```yaml +# .github/workflows/features.yml +name: Update Features + +on: + push: + paths: + - 'dbt_project/**' + +jobs: + update-features: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install 'feast[dbt]' + pip install dbt-bigquery + + - name: Compile dbt + run: | + cd dbt_project + dbt compile + + - name: Generate Feast definitions + run: | + feast dbt import -m dbt_project/target/manifest.json \ + -e user_id \ + -d bigquery \ + --tag feast \ + -o feature_repo/features.py + + - name: Apply to feature store + run: | + cd feature_repo + feast apply +``` + +### 4. Use Dry Run for Validation + +Before generating code, preview what will be created: + +```bash +feast dbt import -m manifest.json -e driver_id --dry-run +``` + +This helps catch issues like missing columns or incorrect types before committing. + +### 5. Version Control Generated Code + +Commit the generated Python files to your repository. This provides: +- Change tracking for feature definitions +- Code review visibility for dbt-to-Feast mappings +- Rollback capability if needed + +## Why dbt Users Love This + +Data teams using Feast with dbt are seeing real impact: + +- **"We stopped rewriting features twice"**: Data engineers build once in dbt, AI teams use directly +- **50-70% faster AI deployment**: From dbt model to production features in minutes, not weeks +- **Single source of truth**: When dbt models update, AI features stay in sync +- **Analytics expertise becomes AI expertise**: Your dbt knowledge directly translates to AI feature engineering +- **Better collaboration**: No more need to rewrite SQL in Python + +## Current Limitations and Future Roadmap + +The dbt integration is currently in alpha with some limitations: + +- **Data source support**: Currently supports BigQuery, Snowflake, and file-based sources +- **Manual entity specification**: You must explicitly specify entity columns +- **No incremental updates**: Each import generates a complete file + +We're actively working on enhancements including: +- Automatic entity inference from foreign key relationships +- Support for additional data sources (Redshift, Postgres) +- Incremental updates to preserve custom modifications +- Enhanced type mapping for complex nested structures + +## Getting Help + +If you encounter issues or have questions: + +- **Documentation**: Check our [dbt integration guide](https://docs.feast.dev/how-to-guides/dbt-integration) +- **Community**: Join our [Slack community](https://slack.feast.dev/) +- **Issues**: Report bugs or request features on [GitHub](https://github.com/feast-dev/feast/issues) + +## Conclusion: Your dbt Models Deserve Production AI + +You've invested time and care into your dbt models. They're clean, documented, tested, and trusted by your organization. They shouldn't have to be rewritten to power AI—they should work as-is. + +Feast's dbt integration makes that possible. Your dbt models become production AI features with: + +- ✅ No rewriting or duplication +- ✅ No changes to your dbt workflow +- ✅ All your documentation preserved +- ✅ Real-time serving for predictions +- ✅ Point-in-time correctness for training + +If you're a dbt user who's been asked to "make those models work for AI," this is your answer. + +Ready to see your dbt models in production? Install Feast and try it out: + +```bash +pip install 'feast[dbt]' +cd your_dbt_project +dbt compile +feast dbt import -m target/manifest.json -e your_entity_column -d bigquery +``` + +Your models are already great. Now make them do more. + +Join us on [Slack](https://slack.feast.dev/) to share your dbt + Feast success stories, or check out the [full documentation](https://docs.feast.dev/how-to-guides/dbt-integration) to dive deeper. + +--- + +*Want to contribute to Feast's dbt integration? Check out our [contributing guide](https://docs.feast.dev/project/contributing) and join us on [GitHub](https://github.com/feast-dev/feast).* diff --git a/infra/website/docs/blog/feast-feature-server-monitoring.md b/infra/website/docs/blog/feast-feature-server-monitoring.md new file mode 100644 index 00000000000..c416e84e82b --- /dev/null +++ b/infra/website/docs/blog/feast-feature-server-monitoring.md @@ -0,0 +1,443 @@ +--- +title: "Monitoring Your Feast Feature Server with Prometheus and Grafana" +description: "Feast now ships built-in Prometheus metrics for the feature server — request latency, feature freshness, materialization health, ODFV transformation duration, and more. Enable with a single flag and get production-grade observability for your ML infrastructure." +date: 2026-03-26 +authors: ["Nikhil Kathole"] +--- + +
+ Feast Feature Server Monitoring — Feast exports metrics to Prometheus for monitoring and alerting, visualized in Grafana dashboards +
+ +# Monitoring Your Feast Feature Server with Prometheus and Grafana + +As feature stores become a critical part of production ML systems, the question shifts from *"Can I serve features?"* to *"Can I trust what I'm serving?"*. Are my features fresh? Is latency within SLA? Are materialization pipelines succeeding? How long are my on-demand transformations taking? + +Until now, answering these questions for Feast required ad-hoc monitoring — parsing logs, writing custom health checks, or bolting on external instrumentation. That changes today. + +**Feast now ships built-in Prometheus metrics for the feature server**, covering the full request lifecycle — from HTTP request handling through online store reads and on-demand feature transformations to materialization pipelines and feature freshness tracking. Enable it with a single flag, point Prometheus at the metrics endpoint, and get production-grade observability for your feature serving infrastructure. + +This post walks through the metrics available, what each one tells you, how to enable and configure them, and how to build a Grafana dashboard that gives you a complete operational picture of your feature server. + +## What's New + +Feast's feature server now exposes a comprehensive set of Prometheus metrics across seven categories, designed to give ML platform teams full visibility into their feature serving infrastructure: + +- **Request metrics** — Per-endpoint request counters and latency histograms with `feature_count` and `feature_view_count` labels, so you can correlate latency with request complexity. +- **Online store read duration** — A histogram capturing total time spent reading from the online store (Redis, DynamoDB, PostgreSQL, etc.), covering both synchronous and async paths across all backends. +- **ODFV transformation duration** — Per-ODFV histograms for both read-path (during `/get-online-features`) and write-path (during push/materialize) transformations, with `odfv_name` and `mode` labels to compare Pandas vs Python vs Substrait performance. +- **Online feature retrieval counters** — Request counts and entity-row-per-request histograms, revealing the shape of your traffic. +- **Push counters** — Tracked by push source and mode (online/offline/both), giving early warning when ingestion pipelines stop sending data. +- **Materialization tracking** — Success/failure counters and duration histograms per feature view, so you know immediately when a pipeline breaks. +- **Feature freshness gauges** — Per-feature-view staleness (seconds since last materialization), updated by a background thread every 30 seconds. The single most important metric for ML model quality. +- **CPU and memory gauges** — Per-worker resource usage for capacity planning and leak detection. +- **Kubernetes-native discovery** — The Feast Operator auto-generates a `ServiceMonitor` when metrics are enabled, so Prometheus Operator discovers the scrape target automatically. + +All metrics are fully opt-in with zero overhead when disabled. Per-category toggles let you enable exactly the metrics you need. + +## Enabling Metrics + +### CLI: One Flag + +The simplest way — one flag, everything enabled: + +```bash +feast serve --metrics +``` + +This starts the feature server on its default port (6566) and a Prometheus metrics endpoint on port 8000. + +### YAML: Fine-Grained Control + +For production deployments, configure metrics in `feature_store.yaml` with per-category toggles: + +```yaml +feature_server: + metrics: + enabled: true + resource: true # CPU and memory gauges + request: true # HTTP request counters and latency histograms + online_features: true # Entity count and retrieval tracking + push: true # Push/ingestion request counters + materialization: true # Pipeline success/failure and duration + freshness: true # Per-feature-view data staleness +``` + +### Kubernetes: Feast Operator + +If you're running Feast on Kubernetes with the [Feast Operator](/blog/scaling-feast-feature-server), set `metrics: true` on the online store server: + +```yaml +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +metadata: + name: production-feast +spec: + feastProject: my_project + services: + onlineStore: + server: + metrics: true +``` + +The operator automatically appends `--metrics` to the serve command and exposes port 8000 as a `metrics` port on the Service. It also auto-generates a `ServiceMonitor` resource for Prometheus Operator discovery. The operator detects the `monitoring.coreos.com` API group at startup; if the Prometheus Operator CRD is absent, ServiceMonitor creation is silently skipped, so vanilla Kubernetes clusters are unaffected. + +## The Metrics + +Feast exposes metrics across seven categories. Here's the full reference, organized by what each category helps you answer. + +### Request Metrics — "How is my API performing?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_server_request_total` | Counter | `endpoint`, `status` | +| `feast_feature_server_request_latency_seconds` | Histogram | `endpoint`, `feature_count`, `feature_view_count` | + +These are the core RED metrics (Rate, Errors, Duration) for your feature server. The latency histogram includes `feature_count` and `feature_view_count` labels so you can correlate latency with request complexity — a request fetching 200 features from 15 feature views will naturally be slower than one fetching 5 features from 2 views. + +The histogram uses bucket boundaries tuned for feature serving workloads: `5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s`. Most online feature requests should complete in the lower buckets. + +### Online Feature Retrieval — "What does my traffic look like?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_online_features_request_total` | Counter | — | +| `feast_online_features_entity_count` | Histogram | — | + +The entity count histogram (buckets: `1, 5, 10, 25, 50, 100, 250, 500, 1000`) tells you the shape of your traffic. Are callers sending single-entity lookups (real-time inference) or batch requests of hundreds (batch scoring)? A sudden spike in entity count per request means an upstream service changed its batching strategy — this directly impacts latency and memory. + +### Online Store Read Duration — "Where is my latency coming from?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_server_online_store_read_duration_seconds` | Histogram | — | + +This metric captures the total time spent reading from the online store (Redis, DynamoDB, PostgreSQL, etc.) during a `/get-online-features` request. It covers both the synchronous for-loop path and the async `asyncio.gather` path across all backends. + +By comparing this with the overall request latency, you can determine whether latency is dominated by the store read or by other processing (serialization, transformation, network overhead). + +### ODFV Transformation Duration — "How expensive are my transforms?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_server_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | +| `feast_feature_server_write_transformation_duration_seconds` | Histogram | `odfv_name`, `mode` | + +These metrics capture per-ODFV transformation time for both read-path (during `/get-online-features`) and write-path (during push/materialize with `write_to_online_store=True`) operations. The `mode` label distinguishes `pandas`, `python`, and `substrait` transformation modes, making it easy to compare their performance characteristics side by side. + +ODFV transformation metrics are **opt-in at the definition level** via `track_metrics=True`: + +```python +@on_demand_feature_view( + sources=[driver_stats_fv, input_request], + schema=[Field(name="conv_rate_plus_val1", dtype=Float64)], + mode="python", + track_metrics=True, # Enable Prometheus metrics for this ODFV +) +def transformed_conv_rate_python(inputs: Dict[str, Any]) -> Dict[str, Any]: + return {"conv_rate_plus_val1": inputs["conv_rate"] + inputs["val_to_add"]} +``` + +When `track_metrics=False` (the default), zero metrics code runs for that ODFV — no timing, no Prometheus recording. This lets you selectively instrument the transforms you care about without adding overhead to others. + +### Push Metrics — "Is my ingestion pipeline healthy?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_push_request_total` | Counter | `push_source`, `mode` | + +The `push_source` label identifies which source is pushing data. The `mode` label is one of `online`, `offline`, or `online_and_offline`. A push source that stops sending data is an early signal that an upstream pipeline is broken — long before feature staleness becomes visible. + +### Materialization Metrics — "Are my pipelines succeeding?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_materialization_total` | Counter | `feature_view`, `status` | +| `feast_materialization_duration_seconds` | Histogram | `feature_view` | + +The `status` label is `success` or `failure`. The duration histogram uses wide buckets (`1s, 5s, 10s, 30s, 60s, 2min, 5min, 10min, 30min, 1hr`) because materialization jobs can range from seconds to tens of minutes depending on the feature view size and offline store. + +### Feature Freshness — "How stale is my data?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_freshness_seconds` | Gauge | `feature_view`, `project` | + +**This is the single most important metric for ML teams.** It measures data staleness — the gap between "now" and the last successful materialization end time — per feature view. A background thread computes this every 30 seconds. + +If your model was trained on hourly features and the freshness gauge crosses 2 hours, your model is receiving data it has never seen patterns for. Before this metric existed, this was a silent failure. Now you can set an alert and catch it in minutes. + +The dashboard below shows these ML-specific metrics in action — latency correlated with feature count, online feature request rate, average entities per request, feature freshness per feature view, and materialization success counts with duration: + +
+ Grafana dashboard showing latency by feature count, feature freshness, average entities per request, and materialization metrics +
+ +### Resource Metrics — "Is my server healthy?" + +| Metric | Type | Labels | +|---|---|---| +| `feast_feature_server_cpu_usage` | Gauge | (per worker PID) | +| `feast_feature_server_memory_usage` | Gauge | (per worker PID) | + +Per-worker CPU and memory gauges, updated every 5 seconds by a background thread. In Gunicorn deployments, each worker reports independently, so you can spot an individual worker consuming excessive resources. + +## Latency Breakdown: Understanding Where Time Is Spent + +One of the most powerful uses of these metrics is **latency decomposition**. By overlaying the overall request latency with the online store read duration and ODFV transformation duration, you can pinpoint exactly where time is spent: + +``` +Total request latency = Store read + ODFV transforms + Serialization/overhead +``` + +The Grafana dashboard below shows this decomposition in action — online store read latency (p50/p95/p99), per-ODFV read-path and write-path transform latency, and a side-by-side Pandas vs Python ODFV comparison: + +
+ Grafana dashboard showing online store read latency, ODFV transformation latency by name, and Pandas vs Python ODFV comparison +
+ +If store reads dominate, the bottleneck is your online store (consider Redis instead of PostgreSQL, or tune your connection pool). If ODFV transforms dominate, consider switching from Pandas mode to Python mode — or re-evaluate whether the transformation should be precomputed during materialization instead of computed on the fly. + +### Pandas vs Python ODFV Comparison + +The `mode` label on transformation metrics makes it straightforward to compare Pandas and Python ODFV performance. The bottom-left panel in the dashboard above shows p50/p95 latencies for Pandas-mode and Python-mode ODFVs overlaid, making the comparison immediate. You can also query these directly: + +```promql +# Pandas p95 read-path latency +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket{mode="pandas"}[1m])) by (le)) + +# Python p95 read-path latency +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket{mode="python"}[1m])) by (le)) +``` + +## Building Alerts + +Here are the recommended alert rules, ordered by impact: + +### Feature Freshness SLO Breach + +```yaml +- alert: FeastFeatureViewStale + expr: feast_feature_freshness_seconds > 3600 + for: 5m + labels: + severity: critical + annotations: + summary: > + Feature view {{ $labels.feature_view }} in project {{ $labels.project }} + has not been materialized in {{ $value | humanizeDuration }}. + impact: Models consuming this feature view are receiving stale data. +``` + +### Materialization Failures + +```yaml +- alert: FeastMaterializationFailing + expr: rate(feast_materialization_total{status="failure"}[15m]) > 0 + for: 5m + labels: + severity: critical + annotations: + summary: > + Materialization is failing for feature view {{ $labels.feature_view }}. +``` + +### High p99 Latency + +```yaml +- alert: FeastHighLatency + expr: | + histogram_quantile(0.99, + rate(feast_feature_server_request_latency_seconds_bucket{ + endpoint="/get-online-features" + }[5m]) + ) > 1.0 + for: 5m + labels: + severity: warning + annotations: + summary: > + Feast p99 latency for online features is {{ $value }}s. +``` + +### High Error Rate + +```yaml +- alert: FeastHighErrorRate + expr: | + sum(rate(feast_feature_server_request_total{status="error"}[5m])) + / sum(rate(feast_feature_server_request_total[5m])) + > 0.01 + for: 5m + labels: + severity: warning + annotations: + summary: > + Feast feature server error rate is {{ $value | humanizePercentage }}. +``` + +## Building a Grafana Dashboard + +With these metrics exposed, you can build a Grafana dashboard that gives you a complete operational picture of your feature server. We've published a [ready-to-import Grafana dashboard JSON](https://github.com/ntkathole/feast-automated-setups/blob/main/feast-prometheus-metrics/grafana_dashboard.json) that covers all the panels described below — import it into your Grafana instance and point it at your Prometheus datasource to get started immediately. + +### Connecting Prometheus to Feast + +Add the Feast metrics endpoint to your Prometheus scrape configuration: + +```yaml +scrape_configs: + - job_name: feast + static_configs: + - targets: [":8000"] + scrape_interval: 15s +``` + +Once Prometheus is scraping, verify the raw metrics output: + +```bash +curl -s http://localhost:8000 | grep feast_ +``` + +### Key PromQL Queries + +Here are the most useful queries for building your own panels or running ad-hoc investigations in the Prometheus UI: + +**Throughput and errors:** + +```promql +# Request rate by endpoint +rate(feast_feature_server_request_total[5m]) + +# Error rate +sum(rate(feast_feature_server_request_total{status="error"}[5m])) + / sum(rate(feast_feature_server_request_total[5m])) +``` + +**Latency percentiles:** + +```promql +# p99 latency for online features +histogram_quantile(0.99, + rate(feast_feature_server_request_latency_seconds_bucket{endpoint="/get-online-features"}[5m])) + +# Online store read p95 +histogram_quantile(0.95, + sum(rate(feast_feature_server_online_store_read_duration_seconds_bucket[1m])) by (le)) + +# ODFV transform p95 by name and mode +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket[1m])) by (le, odfv_name)) + +# ODFV write-path transform p95 by name +histogram_quantile(0.95, + sum(rate(feast_feature_server_write_transformation_duration_seconds_bucket[1m])) by (le, odfv_name)) +``` + +**Latency decomposition:** + +```promql +# Average total request latency +rate(feast_feature_server_request_latency_seconds_sum{endpoint="/get-online-features"}[5m]) + / rate(feast_feature_server_request_latency_seconds_count{endpoint="/get-online-features"}[5m]) + +# Average store read time +rate(feast_feature_server_online_store_read_duration_seconds_sum[5m]) + / rate(feast_feature_server_online_store_read_duration_seconds_count[5m]) + +# Average ODFV transform time +rate(feast_feature_server_transformation_duration_seconds_sum[5m]) + / rate(feast_feature_server_transformation_duration_seconds_count[5m]) +``` + +**Pandas vs Python comparison:** + +```promql +# Pandas p95 +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket{mode="pandas"}[1m])) by (le)) + +# Python p95 +histogram_quantile(0.95, + sum(rate(feast_feature_server_transformation_duration_seconds_bucket{mode="python"}[1m])) by (le)) +``` + +**ML-specific signals:** + +```promql +# Feature freshness — views stale beyond 1 hour +feast_feature_freshness_seconds > 3600 + +# Materialization failure rate +rate(feast_materialization_total{status="failure"}[1h]) + +# Average entities per request +rate(feast_online_features_entity_count_sum[5m]) + / rate(feast_online_features_entity_count_count[5m]) + +# Push rate by source +rate(feast_push_request_total[5m]) +``` + +## Try the Automated Demo + +Want to see all of this in action without manual setup? We've published an [automated demo](https://github.com/ntkathole/feast-automated-setups/tree/main/feast-prometheus-metrics) that deploys a Feast feature server with metrics, a Prometheus instance, and the pre-built Grafana dashboard — all with a single `./setup.sh` command. It includes a traffic generator that exercises every metric category (plain online features, Pandas and Python ODFVs, push, materialize, and write-path transforms), so the dashboard populates immediately. + +## Kubernetes: Automatic Prometheus Discovery + +For teams running Feast on Kubernetes, the Feast Operator now auto-generates a `ServiceMonitor` when `metrics: true` is set on the online store. The operator: + +1. Detects the `monitoring.coreos.com` API group at startup +2. If present, creates a `ServiceMonitor` owned by the `FeatureStore` CR, targeting the `metrics` port (8000) +3. If absent (vanilla Kubernetes without Prometheus Operator), skips silently — no errors, no CRD dependency + +This means on an OpenShift or Prometheus-Operator-enabled cluster, metrics discovery is fully automatic — no manual `ServiceMonitor` creation required. The `ServiceMonitor` is cleaned up automatically when the `FeatureStore` CR is deleted or `metrics` is set back to `false`. + +For teams using [KEDA for autoscaling](/blog/scaling-feast-feature-server), these Prometheus metrics also serve as scaling signals. For example, you can scale the feature server based on request rate: + +```yaml +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: feast-scaledobject +spec: + scaleTargetRef: + apiVersion: feast.dev/v1 + kind: FeatureStore + name: my-feast + triggers: + - type: prometheus + metadata: + serverAddress: http://prometheus.monitoring.svc:9090 + query: sum(rate(feast_feature_server_request_total[2m])) + threshold: "100" +``` + +## Metrics Summary + +| Category | Metric | What It Answers | +|---|---|---| +| Request | `feast_feature_server_request_total` | What is my throughput and error rate? | +| Request | `feast_feature_server_request_latency_seconds` | What are my p50/p99 latencies? | +| Online Features | `feast_online_features_entity_count` | What is my traffic shape? | +| Store Read | `feast_feature_server_online_store_read_duration_seconds` | Is my online store the bottleneck? | +| ODFV Transform | `feast_feature_server_transformation_duration_seconds` | How expensive are my read-path transforms? | +| ODFV Transform | `feast_feature_server_write_transformation_duration_seconds` | How expensive are my write-path transforms? | +| Push | `feast_push_request_total` | Is my ingestion pipeline sending data? | +| Materialization | `feast_materialization_total` | Are my pipelines succeeding? | +| Materialization | `feast_materialization_duration_seconds` | How long do my pipelines take? | +| Freshness | `feast_feature_freshness_seconds` | How stale is the data my models are using? | +| Resource | `feast_feature_server_cpu_usage` / `memory_usage` | Is my server healthy? | + +## How Can I Get Started? + +1. **Enable metrics**: `feast serve --metrics` or set `metrics.enabled: true` in your `feature_store.yaml` +2. **Verify the endpoint**: `curl http://localhost:8000` +3. **Import the dashboard**: Grab the [Grafana dashboard JSON](https://github.com/ntkathole/feast-automated-setups/blob/main/feast-prometheus-metrics/grafana_dashboard.json) and import it into your Grafana instance +4. **Set up alerts**: Start with freshness and materialization failures — those catch the problems that affect ML model quality first +5. **Try the automated demo**: Run the [feast-prometheus-metrics setup](https://github.com/ntkathole/feast-automated-setups/tree/main/feast-prometheus-metrics) for a one-command local experience +6. Check out the [Feast documentation](https://docs.feast.dev/) for the full configuration reference +7. Join the [Feast Slack](https://slack.feast.dev) to share feedback and ask questions + +We're excited to bring production-grade observability to Feast and welcome feedback from the community! diff --git a/infra/website/docs/blog/feast-joins-pytorch-ecosystem.md b/infra/website/docs/blog/feast-joins-pytorch-ecosystem.md new file mode 100644 index 00000000000..ab52a4db3d3 --- /dev/null +++ b/infra/website/docs/blog/feast-joins-pytorch-ecosystem.md @@ -0,0 +1,189 @@ +--- +title: Feast Joins the PyTorch Ecosystem +description: We're excited to announce that Feast has officially joined the PyTorch ecosystem, bringing feature stores to the broader ML community. +date: 2026-01-26 +authors: ["Francisco Javier Arceo", "Hao Xu", "Shuchu Han"] +--- + +
+ Feast Joins PyTorch Ecosystem +
+ +# Feast Joins the PyTorch Ecosystem 🎉 + +We're thrilled to announce that Feast has officially joined the [PyTorch ecosystem](https://pytorch.org/blog/feast-joins-the-pytorch-ecosystem/)! This is a significant milestone for the Feast community and represents our commitment to making feature stores accessible to the broader machine learning community. + +## What This Means for the ML Community + +By joining the PyTorch ecosystem, Feast becomes part of a vibrant community of tools and libraries that power modern machine learning workflows. PyTorch has become the de facto standard for ML research and production, and we're excited to bring feature store capabilities to PyTorch users. + +### Why Feature Stores Matter for PyTorch Users + +Feature stores solve critical challenges that ML engineers face when moving from model development to production: + +1. **Consistent Feature Engineering**: Ensure that features computed during training match those used in production +2. **Low-Latency Feature Serving**: Serve features with millisecond latency for real-time inference +3. **Feature Reusability**: Share features across teams and models to accelerate development +4. **Point-in-Time Correctness**: Prevent data leakage by ensuring training data reflects what would have been available at prediction time +5. **Data Infrastructure Abstraction**: Decouple your ML code from underlying data infrastructure + +## How Feast Works with PyTorch + +Feast integrates seamlessly with PyTorch workflows, whether you're training models locally or deploying them at scale. Here's a typical workflow: + +### Training with Feast and PyTorch + +```python +from feast import FeatureStore +import torch +from torch.utils.data import Dataset, DataLoader +import pandas as pd +from datetime import datetime + +# Initialize Feast +store = FeatureStore(repo_path=".") + +# Retrieve historical features for training +entity_df = pd.DataFrame({ + "user_id": [1001, 1002, 1003], + "event_timestamp": [datetime(2025, 1, 1), datetime(2025, 1, 2), datetime(2025, 1, 3)] +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "user_features:age", + "user_features:activity_score", + "item_features:popularity" + ] +).to_df() + +# Add your labels (from your training data source) +# training_df['label'] = ... + +# Create PyTorch Dataset +class FeatureDataset(Dataset): + def __init__(self, df, feature_cols): + self.features = torch.tensor(df[feature_cols].values, dtype=torch.float32) + self.labels = torch.tensor(df['label'].values, dtype=torch.float32) + + def __len__(self): + return len(self.features) + + def __getitem__(self, idx): + return self.features[idx], self.labels[idx] + +# Train your PyTorch model +feature_cols = ['age', 'activity_score', 'popularity'] +dataset = FeatureDataset(training_df, feature_cols) +dataloader = DataLoader(dataset, batch_size=32, shuffle=True) + +# Define and train your model +model = torch.nn.Sequential( + torch.nn.Linear(len(feature_cols), 64), + torch.nn.ReLU(), + torch.nn.Linear(64, 1) +) +# Training loop... +``` + +### Serving Features for Real-Time Inference + +Once your PyTorch model is trained, Feast makes it easy to serve features in production: + +```python +# Get online features for real-time prediction +online_features = store.get_online_features( + entity_rows=[{"user_id": 1001}], + features=[ + "user_features:age", + "user_features:activity_score", + "item_features:popularity" + ] +).to_dict() + +# Convert to tensor and run inference +# Note: Feature keys in the dict will use the format "feature_view__feature_name" +feature_values = [ + online_features['age'][0], + online_features['activity_score'][0], + online_features['popularity'][0] +] +feature_tensor = torch.tensor(feature_values, dtype=torch.float32) + +with torch.no_grad(): + prediction = model(feature_tensor) +``` + +## Key Benefits for PyTorch Users + +### 1. Production-Ready Feature Engineering + +Feast helps you move from notebook experiments to production-ready feature pipelines. Define your features once and use them consistently across training and serving. + +### 2. Flexible Data Sources + +Feast supports a wide range of data sources including: +- BigQuery, Snowflake, Redshift for batch features +- Redis, DynamoDB, PostgreSQL for online features +- Parquet files for local development + +### 3. Scalability + +Whether you're training on a laptop or serving millions of predictions per second, Feast scales with your needs. + +### 4. Open Source and Extensible + +Feast is fully open source with a pluggable architecture, making it easy to extend and customize for your specific use cases. + +## Getting Started + +Ready to use Feast with PyTorch? Here's how to get started: + +```bash +# Install Feast +pip install feast + +# Initialize a new feature repository +feast init my_feature_repo +cd my_feature_repo + +# Apply feature definitions +feast apply + +# Start serving features +feast serve +``` + +Check out our [documentation](https://docs.feast.dev) for comprehensive guides and examples. + +## Join the Community + +We're excited to be part of the PyTorch ecosystem and look forward to collaborating with the community. Here's how you can get involved: + +- **GitHub**: Star us at [github.com/feast-dev/feast](https://github.com/feast-dev/feast) +- **Slack**: Join our [community Slack](https://slack.feast.dev) +- **Documentation**: Explore our [docs](https://docs.feast.dev) +- **Examples**: Check out [example projects](https://github.com/feast-dev/feast/tree/master/examples) + +## What's Next? + +We're committed to making Feast the best feature store for PyTorch users. Some of our upcoming priorities include: + +- Enhanced PyTorch integration examples +- Performance optimizations for large-scale deployments +- Additional connectors for popular data sources +- Improved developer experience + +Thank you to the PyTorch team for welcoming us to the ecosystem, and to the Feast community for your continued support! + +## Learn More + +- [PyTorch Blog Announcement](https://pytorch.org/blog/feast-joins-the-pytorch-ecosystem/) +- [Feast Documentation](https://docs.feast.dev) +- [Feast GitHub Repository](https://github.com/feast-dev/feast) +- [What is a Feature Store?](./what-is-a-feature-store) + +--- + +Have questions or feedback? Reach out to us on [Slack](https://slack.feast.dev) or [GitHub Discussions](https://github.com/feast-dev/feast/discussions). diff --git a/infra/website/docs/blog/feast-launches-rbac.md b/infra/website/docs/blog/feast-launches-rbac.md new file mode 100644 index 00000000000..044b5e883a8 --- /dev/null +++ b/infra/website/docs/blog/feast-launches-rbac.md @@ -0,0 +1,179 @@ +--- +title: Feast Launches RBAC! +description: Feast now supports Role Based Access Controls (RBAC) so you can secure and govern your data with granular authorization policies. +date: 2024-11-21 +authors: ["Danielle Martinoli", "Francisco Javier Arceo"] +--- + +
+ Feast RBAC Architecture +
+ +# Feast Launches RBAC! 🚀 + +# What is the Feast Permission Model? + +Feast now supports Role Based Access Controls (RBAC) so you can secure and govern your data. + +If you ever wanted to securely partition your feature store across different teams, the new Feast permissions model is here to make that possible! + +This powerful feature allows administrators to configure granular authorization policies, letting them decide which users and groups can access specific resources and what operations they can perform. + +The default implementation is based on Role-Based Access Control (RBAC): user roles determine whether a user has permission to perform specific functions on registered resources. + +# Why is RBAC important for Feast? + +Feature stores often operate on sensitive, proprietary data and we want to make sure teams are able to govern the access and control of that data thoughtfully, while benefiting from transparent code and an open source community like Feast. + +That's why we built RBAC using [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac) and [OpenID Connect protocol (OIDC)](https://auth0.com/docs/authenticate/protocols/openid-connect-protocol), ensuring secure, fine-grained access control in Feast. + +# What are the Benefits of using Feast Permissions? + +Using the Feast Permissions Model offers two key benefits: + +1. Securely share and partition your feature store: grant each team only the minimum privileges necessary to access and manage the relevant resources. +2. Adopt a Service-Oriented Architecture and leverage the benefits of a distributed system. + +# How Feast Uses RBAC + +## Permissions as Feast resources + +The RBAC configuration is defined using a new Feast object type called "Permission". Permissions are registered in the Feast registry and are defined and applied like all the other registry objects, using Python code. + +A permission is defined by these three components: + +* A **resource**: a Feast object that we want to secure against unauthorized access. It's identified by the matching type(s), a possibly empty list of name patterns and a dictionary of required tags. +* An **action**: a logical operation performed on the secured resource, such as managing the resource state with CREATE, DESCRIBE, UPDATE or DELETE, or accessing the resource data with READ and WRITE (differentiated by ONLINE and OFFLINE store types) +* A **policy**: the rule to enforce authorization decisions based on the current user. The default implementation uses role-based policies. + +The resource types supported by the permission framework are those defining the customer feature store: + +* Project +* Entity +* FeatureView +* OnDemandFeatureView +* BatchFeatureView +* StreamFeatureView +* FeatureService +* DataSource +* ValidationReference +* SavedDataset +* Permission + + +**TIP**: Check out the Permission APIs in the [Feast Python API Documentation](https://docs.feast.dev/getting-started/concepts/permission) to learn more! + +```python +# This configuration grants users with the 'owner' role permissions +# to fetch resource status and read data from all the feature views +from feast.permissions.action import AuthzedAction, READ +# Note: READ is a global list including both READ_OFFLINE and +# READ_ONLINE values from AuthzedAction enum + +# You do not have to specify `name_patterns` +Permission( + name="fv-owner", + types=[FeatureView], + policy=RoleBasedPolicy(roles=["owner"]), + actions=[AuthzedAction.DESCRIBE, READ], +) + +# This configuration grants users with the 'lab' role permissions +# to fetch resource status and read data from all feature views +# named 'lab_stream_feature_view' or 'lab_feature_view' +from feast.permissions.action import AuthzedAction, READ + +Permission( + name="lab-reader", + types=[FeatureView], + name_patterns=["lab_stream_feature_view", "lab_feature_view"], + policy=RoleBasedPolicy(roles=["lab"]), + actions=[AuthzedAction.DESCRIBE, READ], +) + +# As an alternative, we can use Python regular expression patterns +# to grant the same permission to all feature views whose name +# starts by 'lab' +from feast.permissions.action import AuthzedAction, READ + +Permission( + name="lab-reader", + types=[FeatureView], + name_patterns="lab.*", # Accepts both 'str' and 'list[str]' types + policy=RoleBasedPolicy(roles=["lab"]), + actions=[AuthzedAction.DESCRIBE, READ], +) + +# This configuration grants users with the 'prod' role permissions +# to fetch resource status and read data from all feature views +# whose names include the '_prod_' word +from feast.permissions.action import AuthzedAction, READ + +Permission( + name="prod-reader", + types=[FeatureView, FeatureService], + name_patterns=".*_prod_.*", + policy=RoleBasedPolicy(roles=["prod"]), + actions=[AuthzedAction.DESCRIBE, READ], +) + +# This configuration grants permissions to write on all data sources +# tagged with 'risk_level' set to 'high', exclusively to users +# with the 'admin' or 'data_team' roles +from feast.permissions.action import WRITE +# Note: WRITE is a global list including both WRITE_OFFLINE and +# WRITE_ONLINE values from AuthzedAction enum + +Permission( + name="data-writer", + types=[DataSource], + required_tags={"risk_level": "high"}, + policy=RoleBasedPolicy(roles=["admin", "data_team"]), + actions=[WRITE], +) +``` + +## Why Now is the Time for Distributed Feature Stores + +But wait a moment—does that mean every time I access the FeatureStore API, I have to go through an authorization check? + +Well, yes and no-but mostly no if you work in a development environment. +If your environment doesn't use any remote Feast service, RBAC enforcement won't take place. + +Indeed, the reference architecture for the permission model feature represents a fully distributed environment: + +
+ RBAC Architecture Diagram +
+ +* Feast functions are deployed as interconnected services. +* Service endpoints enforce authorization, processing only authorized requests. +* Clients use the feature store transparently, with authorization headers automatically injected in every request. +* Service-to-service communications are permitted automatically. + +Currently, only the following Python servers are supported in an authorized environment: +- Online REST feature server +- Offline Arrow Flight feature server +- gRPC Registry server + +## Configuring Feast Authorization + +For backward compatibility, by default no authorizations are enforced. The authorization functionality must be explicitly enabled using the auth configuration section in feature\_store.yaml. +Of course, all server and client applications must have a consistent configuration. + +Currently, feast supports [OIDC](https://auth0.com/docs/authenticate/protocols/openid-connect-protocol) and [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac) authentication/authorization. + +* With OIDC authorization, the client uses an OIDC server to fetch a JSON Web Token (JWT), which is then included in every request. On the server side, the token is parsed to extract user roles and validate them against the configured permissions. +* With Kubernetes authorization, the client injects its service account JWT token into the request. The server then extracts the service account name from the token and uses it to look up the associated role in the Kubernetes RBAC resources. + +## Inspecting and Troubleshooting the Permissions Model + +The feast CLI includes a new `permissions` command to list the registered permissions, with options to identify the matching resources for each configured permission and the existing resources that are not covered by any permission. + +For troubleshooting purposes, it also provides a command to list all the resources and operations allowed to any managed role. + +# How Can I Get Started? + +This new feature includes working examples for both supported authorization protocols. You can start by experimenting with these examples to see how they fit your own feature store and assess their benefits. + +As this is a completely new functionality, your feedback will be extremely valuable. It will help us adapt the feature to meet real-world requirements and better serve our customers. diff --git a/infra/website/docs/blog/feast-mlflow-kubeflow.md b/infra/website/docs/blog/feast-mlflow-kubeflow.md new file mode 100644 index 00000000000..d0b89ac7138 --- /dev/null +++ b/infra/website/docs/blog/feast-mlflow-kubeflow.md @@ -0,0 +1,516 @@ +--- +title: "Feast + MLflow + Kubeflow: A Unified AI/ML Lifecycle" +description: Learn how to use Feast, MLflow, and Kubeflow to power your AI/ML Lifecycle +date: 2026-03-09 +authors: ["Francisco Javier Arceo", "Nikhil Kathole"] +--- + +
+ Feast, MLflow, and Kubeflow +
+ +# Feast + MLflow + Kubeflow: A Unified AI/ML Lifecycle + +## Overview + +Building production-ready machine learning systems requires more than a great model. It demands a clear separation of concerns between feature management, experiment tracking, and workflow orchestration. This post explores how [Feast](https://feast.dev/), [MLflow](https://mlflow.org/), and [Kubeflow](https://www.kubeflow.org/) work together as complementary open-source tools to cover the full AI/ML lifecycle — from raw data to serving predictions at scale. + +These tools are not competitors. Each one occupies a distinct role: + +* **Feast** manages feature data: defining, transforming, storing, and serving features consistently for both training and inference. It also tracks feature lineage and supports data quality monitoring. +* **MLflow** tracks experiments: logging runs, metrics, parameters, artifacts, and candidate models. +* **Kubeflow** orchestrates ML workflows: running distributed training, hyperparameter sweeps, and end-to-end pipelines on Kubernetes. + +Together they form a complete, open-source foundation for operationalizing ML. + +### How are Feast, MLflow, and Kubeflow different? + +If you are new to these tools, it is natural to wonder whether they overlap. The short answer is: they solve fundamentally different problems in the ML lifecycle. The table below makes this concrete. + +| Capability | Feast | MLflow | Kubeflow | +|---|---|---|---| +| Define and version feature schemas | Yes | No | No | +| Store and serve features (online + offline) | Yes | No | No | +| Point-in-time-correct feature retrieval | Yes | No | No | +| Feature transformations (training = serving) | Yes | No | No | +| Feature lineage and registry | Yes | No | No | +| Data quality validation on features | Yes | No | No | +| Log experiments, metrics, and parameters | No | Yes | No | +| Track and compare model versions | No | Yes | No | +| Model registry (promote / alias models) | No | Yes | No | +| Orchestrate multi-step ML pipelines | No | No | Yes (Pipelines) | +| Distributed training on Kubernetes | No | No | Yes (Training Operator) | +| Hyperparameter tuning | No | Yes (with Optuna, etc.) | Yes (Katib) | + +A few common misconceptions: + +* **"Can't MLflow track my features?"** — MLflow can *log* feature names as parameters, but it does not *define*, *store*, *transform*, or *serve* features. It has no concept of an offline store, an online store, or point-in-time joins. Feast fills that gap. +* **"Doesn't Kubeflow handle everything end-to-end?"** — Kubeflow orchestrates *workflows* — it tells your pipeline steps when to run and where. But it does not provide feature storage, experiment tracking, or model versioning. You still need Feast for the data layer and MLflow for the experiment layer. +* **"Why do I need Feast if I just read from a database?"** — Without Feast, teams typically duplicate feature logic between training scripts and serving endpoints, which leads to training–serving skew. Feast guarantees the same transformation and retrieval logic is used in both contexts. + +With that context, the rest of this post walks through each tool in detail and shows how they hand off to one another in practice. + +This topic has been explored by the community before — the post ["Feast with AI: Feed Your MLflow Models with Feature Store"](https://blog.qooba.net/2021/05/22/feast-with-ai-feed-your-mlflow-models-with-feature-store/) by [@qooba](https://github.com/qooba) is an excellent early look at combining Feast and MLflow. For a hands-on, end-to-end example of Feast and Kubeflow working together, see ["From Raw Data to Model Serving: A Blueprint for the AI/ML Lifecycle with Kubeflow and Feast"](/blog/kubeflow-fraud-detection-e2e) by Helber Belmiro. This post builds on that prior work and brings all three tools — Feast, MLflow, and Kubeflow — into a single narrative. + +--- + +## The AI/ML Lifecycle + +A typical production ML project passes through several stages: + +1. **Feature development** — raw data is transformed into meaningful signals. +2. **Model development** — data scientists experiment with algorithms, features, and hyperparameters. +3. **Model evaluation & selection** — the best experiment is chosen for promotion. +4. **Production deployment** — the selected model is deployed and features are served in real time. +5. **Monitoring & iteration** — model and feature health is observed; the cycle repeats. + +The diagram below maps each stage to its primary tool: + +``` +Raw Data ──► Feast (Feature Engineering & Storage) + │ + ▼ + MLflow + Kubeflow Pipelines (Experiment Tracking & Orchestration) + │ + ▼ + Kubeflow Training Operator (Distributed Training) + │ + ▼ + MLflow Model Registry (Candidate Models) + │ + ▼ + Feast Online Store + Feature Server (Production Serving) +``` + +--- + +## Feast: Feature Development, Iteration, and Serving + +Feast is the data layer of the ML stack. Its core job is to make the same feature logic available both at training time (via the offline store) and at inference time (via the online store), eliminating training–serving skew. Beyond storage and serving, Feast also handles **feature transformations**, **feature lineage tracking**, and **data quality monitoring** — capabilities that are essential when moving features from experimentation to production. + +### Defining features + +A Feast `FeatureView` declares how a feature is computed and where it is stored: + +```python +from datetime import timedelta +from feast import FeatureView, Field, FileSource +from feast.types import Float64, Int64 + +driver_stats = FeatureView( + name="driver_hourly_stats", + entities=["driver_id"], + ttl=timedelta(days=7), + schema=[ + Field(name="conv_rate", dtype=Float64), + Field(name="acc_rate", dtype=Float64), + Field(name="avg_daily_trips", dtype=Int64), + ], + source=FileSource(path="data/driver_stats.parquet", timestamp_field="event_timestamp"), +) +``` + +After running `feast apply`, these features are registered in the Feast registry and visible in the Feast UI: + +
+ Feast UI showing the Feature List for the Driver Ranking project with conv_rate, acc_rate, and avg_daily_trips features +

The Feast UI showing three registered features in the driver_hourly_stats feature view — conv_rate, acc_rate, and avg_daily_trips — each linked to the Driver Ranking project.

+
+ +### Retrieving historical features for training + +Point-in-time-correct historical features are retrieved from the offline store. This prevents future data from leaking into training examples: + +```python +from feast import FeatureStore +import pandas as pd + +store = FeatureStore(repo_path=".") + +entity_df = pd.DataFrame({ + "driver_id": [1001, 1002, 1003], + "event_timestamp": pd.to_datetime(["2025-01-01", "2025-01-02", "2025-01-03"]), +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], +).to_df() +``` + +### Materializing features for real-time serving + +When a model is promoted to production, features are materialized to the online store so they can be retrieved with single-digit millisecond latency: + +```python +from datetime import datetime + +store.materialize_incremental(end_date=datetime.utcnow()) +``` + +Serving then becomes a single call: + +```python +features = store.get_online_features( + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], + entity_rows=[{"driver_id": 1001}], +).to_dict() +``` + +### Feature transformations + +Feast supports on-demand feature transformations, allowing you to define transformation logic that runs at retrieval time — both offline (for training) and online (for serving) — using the same Python function. This eliminates the need to duplicate transformation code across training and inference pipelines: + +```python +from feast.on_demand_feature_view import on_demand_feature_view +from feast import Field +from feast.types import Float64 + +@on_demand_feature_view( + sources=[driver_stats], + schema=[Field(name="conv_acc_ratio", dtype=Float64)], +) +def driver_ratios(inputs): + df = inputs.copy() + df["conv_acc_ratio"] = df["conv_rate"] / (df["acc_rate"] + 1e-6) + return df[["conv_acc_ratio"]] +``` + +Here `driver_stats` is the `FeatureView` object defined earlier. The `sources` parameter accepts `FeatureView`, `RequestSource`, or `FeatureViewProjection` objects. + +Using `on_demand_feature_view` ensures that the same transformation logic is applied whether features are retrieved from the offline store for training or from the online store at inference time, preventing transformation skew. + +### Feature lineage + +The Feast feature registry acts as the single source of truth for feature definitions. Every `FeatureView`, data source, entity, and transformation is registered and versioned in the registry. This gives you full lineage from raw data source through transformation logic to the feature values consumed by a model — a critical requirement for debugging, auditing, and regulatory compliance. + +You can inspect the lineage of any feature programmatically: + +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") +feature_view = store.get_feature_view("driver_hourly_stats") +print(feature_view.source) # upstream data source +print(feature_view.schema) # feature schema +``` + +For cross-system lineage that extends beyond Feast into upstream data pipelines and downstream model training, Feast also supports native [OpenLineage integration](/blog/feast-openlineage-integration). Enabling it in your `feature_store.yaml` automatically emits lineage events on `feast apply` and `feast materialize`, letting you visualize the full data flow in tools like [Marquez](https://marquezproject.ai/). + +### Data quality monitoring + +Feast's native data quality monitoring system automatically computes statistical metrics — null rates, distributions, percentiles, histograms — for every registered feature across both batch data and serving logs. It detects drift by comparing current metrics against baselines computed during `feast apply`. + +```yaml +# feature_store.yaml +data_quality_monitoring: + auto_baseline: true +``` + +```bash +# Compute metrics across all granularities (daily, weekly, monthly, quarterly) +feast monitor run + +# Monitor serving logs +feast monitor run --source-type log +``` + +The monitoring UI dashboard (accessible from the sidebar) provides per-feature health status, distribution histograms, time-series drift charts, and configurable filters. Metrics are also available via REST API endpoints for integration with external alerting systems. + +For details, see the [Feature Quality Monitoring guide](/docs/how-to-guides/feature-monitoring). + +### Feast Feature Registry vs. MLflow Model Registry + +A common question is how the **Feast feature registry** relates to the **MLflow model registry**. They are different things that serve complementary roles. + +| | Feast Feature Registry | MLflow Model Registry | +|---|---|---| +| **What it tracks** | Feature definitions, schemas, data sources, entity relationships | Model artifacts, versions, model aliases (e.g., "production", "staging") | +| **Primary users** | Feature engineers, data scientists, ML platform teams | Data scientists, ML engineers | +| **Relationship to production** | Defines what data is available for training *and* serving | Tracks which model version is promoted to production | +| **Scope** | All features ever defined — a superset of what any one model uses | All model versions, including candidates that never ship | + +This distinction is important: the **Feast registry is a superset of the MLflow model registry** from a feature perspective. During experimentation, a data scientist may train models using dozens of features. Once a model is selected for production, only a *subset* of those features will be needed for online serving. Feast's registry records all available features; the specific features required by the production model are a narrower slice that corresponds to what MLflow logged as model inputs. + +--- + +## MLflow: Experiment Tracking, Hyperparameter Optimization, and Feature Selection + +MLflow is the experimentation layer. It answers the question: *"Which combination of features, model architecture, and hyperparameters produced the best result?"* + +### Logging a training run with Feast features + +Because Feast provides a consistent `get_historical_features` API, it is straightforward to combine it with MLflow tracking: + +```python +import mlflow +import mlflow.sklearn +from feast import FeatureStore +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import train_test_split +from sklearn.metrics import roc_auc_score +import pandas as pd + +store = FeatureStore(repo_path=".") + +entity_df = pd.read_parquet("data/driver_labels.parquet") +feature_df = store.get_historical_features( + entity_df=entity_df, + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate", "driver_hourly_stats:avg_daily_trips"], +).to_df() + +X = feature_df[["conv_rate", "acc_rate", "avg_daily_trips"]] +y = feature_df["label"] +X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) + +with mlflow.start_run(): + mlflow.log_param("features", ["conv_rate", "acc_rate", "avg_daily_trips"]) + mlflow.log_param("model_type", "LogisticRegression") + + model = LogisticRegression() + model.fit(X_train, y_train) + + auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) + mlflow.log_metric("auc", auc) + + # Log the feature store snapshot alongside the model + mlflow.sklearn.log_model(model, artifact_path="model") + mlflow.log_artifact("feature_store.yaml", artifact_path="feast_config") +``` + +Logging `feature_store.yaml` together with the model artifact ensures that, at any future point, the exact set of Feast feature definitions used for that run can be reproduced. + +### Feature selection with MLflow + +One of the most powerful uses of Feast + MLflow together is systematic **feature selection**: training models with different subsets of Feast features and using MLflow's comparison UI to identify which combination produces the best results. This is far more rigorous than manually trying feature sets in a notebook, and the results are often counterintuitive. + +The pattern is to loop over candidate feature subsets, retrieve each one from Feast, train a model, and log the metrics and feature names as a separate MLflow run: + +```python +import mlflow +import mlflow.sklearn +from feast import FeatureStore +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import train_test_split +from sklearn.metrics import roc_auc_score +import pandas as pd + +store = FeatureStore(repo_path=".") + +entity_df = pd.read_parquet("data/driver_labels.parquet") + +# Define candidate feature subsets to compare +feature_subsets = { + "acc_rate_only": ["driver_hourly_stats:acc_rate"], + "acc_rate_trips": ["driver_hourly_stats:acc_rate", "driver_hourly_stats:avg_daily_trips"], + "all_features": ["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips"], +} + +with mlflow.start_run(run_name="feast_feature_selection"): + for subset_name, feature_refs in feature_subsets.items(): + feature_df = store.get_historical_features( + entity_df=entity_df, + features=feature_refs, + ).to_df() + + feature_cols = [ref.split(":")[1] for ref in feature_refs] + X = feature_df[feature_cols] + y = feature_df["label"] + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) + + with mlflow.start_run(run_name=subset_name, nested=True): + mlflow.log_param("features", feature_cols) + model = LogisticRegression() + model.fit(X_train, y_train) + auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) + mlflow.log_metric("auc", auc) + mlflow.sklearn.log_model(model, artifact_path="model") +``` + +After running, the MLflow UI lets you sort all nested runs by AUC and immediately see which feature subset wins. The results can be surprising — for example, with synthetic driver data a single feature may outperform the full feature set: + +| Features | Model | AUC | +|---|---|---| +| `acc_rate` only | LogisticRegression | 0.645 | +| `acc_rate` + `avg_daily_trips` | LogisticRegression | 0.613 | +| All 3 features | LogisticRegression | 0.570 | + +
+ MLflow UI showing a LogisticRegression run with all three Feast features, metrics, parameters, and feature tags +

The MLflow UI showing a LogisticRegression run trained with all three Feast features (conv_rate, acc_rate, avg_daily_trips). The run logs five metrics (accuracy, AUC, precision, recall, F1), the feature list as a parameter, and the demo tags each included feature (e.g., feature_conv_rate: included) for easy filtering.

+
+ +This is exactly the kind of insight MLflow's comparison interface is built for. You can sort runs by AUC, filter by which features were included, and visualize performance across experiments. Note that with synthetic data these numbers won't carry real meaning — the point is that the tooling makes it trivial to *observe* these differences systematically and let data drive the feature selection decision. + +
+ MLflow comparison view showing three experiment runs side by side with different feature combinations +

MLflow's comparison view showing three runs side by side with different feature subsets. The "Show diff only" toggle highlights how the features parameter varies across runs, making it easy to identify which combination of Feast features produces the best results.

+
+ +
+ MLflow metric charts showing accuracy, AUC, F1, precision, and recall grouped by num_features across three feature subsets +

MLflow's metric charts view visualizing accuracy, AUC, F1, precision, and recall across all feature selection runs, grouped by num_features. This chart makes it easy to spot how model performance changes as more Feast features are included.

+
+ +Once you have identified the winning subset, the Feast registry ensures that only those features need to be materialized into the online store for production serving. + +### Hyperparameter sweeps + +MLflow integrates natively with hyperparameter optimization libraries. For example, using MLflow with [Optuna](https://optuna.org/): + +```python +import optuna +import mlflow + +def objective(trial): + C = trial.suggest_float("C", 1e-3, 10.0, log=True) + max_iter = trial.suggest_int("max_iter", 100, 1000) + + with mlflow.start_run(nested=True): + mlflow.log_params({"C": C, "max_iter": max_iter}) + model = LogisticRegression(C=C, max_iter=max_iter) + model.fit(X_train, y_train) + auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) + mlflow.log_metric("auc", auc) + return auc + +with mlflow.start_run(run_name="optuna_sweep"): + study = optuna.create_study(direction="maximize") + study.optimize(objective, n_trials=20) +``` + +All trials, their parameters, and their metrics are automatically captured in the MLflow tracking server, making it easy to compare runs and select the best candidate for promotion. + +--- + +## Kubeflow: Orchestrating the ML Workflow + +[Kubeflow](https://www.kubeflow.org/) brings Kubernetes-native orchestration to the ML lifecycle. Its two most relevant components here are: + +* **Kubeflow Pipelines** — a platform for building and deploying repeatable ML workflows as DAGs. +* **Kubeflow Training Operator** — manages distributed training jobs (PyTorchJob, TFJob, etc.) on Kubernetes. + +### Kubeflow Pipelines integrating Feast and MLflow + +Kubeflow Pipelines lets you compose the entire workflow — feature retrieval, training, evaluation, and registration — as a single, reproducible pipeline: + +```python +from kfp import dsl + +@dsl.component(base_image="python:3.10-slim", packages_to_install=["feast", "mlflow", "scikit-learn", "pandas", "pyarrow"]) +def retrieve_features(entity_df_path: str, feature_store_repo: str, output_path: dsl.Output[dsl.Dataset]): + from feast import FeatureStore + import pandas as pd + + store = FeatureStore(repo_path=feature_store_repo) + entity_df = pd.read_parquet(entity_df_path) + df = store.get_historical_features( + entity_df=entity_df, + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], + ).to_df() + df.to_parquet(output_path.path) + + +@dsl.component(base_image="python:3.10-slim", packages_to_install=["feast", "mlflow", "scikit-learn", "pandas"]) +def train_and_log(features_path: dsl.Input[dsl.Dataset], mlflow_tracking_uri: str, model_name: str): + import mlflow, mlflow.sklearn + import pandas as pd + from sklearn.linear_model import LogisticRegression + from sklearn.model_selection import train_test_split + from sklearn.metrics import roc_auc_score + + mlflow.set_tracking_uri(mlflow_tracking_uri) + df = pd.read_parquet(features_path.path) + X = df[["conv_rate", "acc_rate"]] + y = df["label"] + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) + + with mlflow.start_run(): + model = LogisticRegression() + model.fit(X_train, y_train) + auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) + mlflow.log_metric("auc", auc) + mlflow.sklearn.log_model(model, artifact_path="model", registered_model_name=model_name) + + +@dsl.pipeline(name="feast-mlflow-training-pipeline") +def training_pipeline(entity_df_path: str, feature_store_repo: str, mlflow_tracking_uri: str, model_name: str): + fetch_step = retrieve_features(entity_df_path=entity_df_path, feature_store_repo=feature_store_repo) + train_and_log( + features_path=fetch_step.outputs["output_path"], + mlflow_tracking_uri=mlflow_tracking_uri, + model_name=model_name, + ) +``` + +Each step runs in its own container, making the pipeline portable and reproducible across environments. + +### Distributed training with the Kubeflow Training Operator + +For large-scale models, the [Kubeflow Training Operator](https://www.kubeflow.org/docs/components/training/) schedules distributed training jobs. Feast integrates naturally because it provides a consistent Python API for retrieving feature data — whether training is running on a single machine or across a cluster of workers. Each worker calls `get_historical_features` for its shard of the entity dataframe, and the resulting features are passed directly into the training loop. + +--- + +## Bringing It All Together: Feast → MLflow → Production + +The following end-to-end workflow shows how the three tools hand off to one another: + +### Step 1: Register and materialize features with Feast + +```bash +feast apply # Register feature definitions in the registry +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +### Step 2: Run experiments and select the best model with MLflow + +Feature engineers iterate on feature definitions in Feast while data scientists run experiments in MLflow, logging which features were used for each run. The best run is registered in the MLflow Model Registry: + +```python +mlflow.register_model(f"runs:/{best_run_id}/model", "driver_conversion_model") +``` + +### Step 3: Promote to production + +Promoting the model in MLflow signals that it is ready for deployment. At this point, you also know the exact subset of Feast features required by that model — these are the features to materialize and serve. + +```python +client = mlflow.tracking.MlflowClient() +client.set_registered_model_alias( + name="driver_conversion_model", alias="production", version="3" +) +``` + +### Step 4: Serve features and predictions + +The deployed model reads its inputs from the Feast online store at inference time: + +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +def predict(driver_id: int) -> float: + features = store.get_online_features( + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], + entity_rows=[{"driver_id": driver_id}], + ).to_dict() + return model.predict_proba([[features["conv_rate"][0], features["acc_rate"][0]]])[0][1] +``` + +--- + +## Summary + +| Concern | Tool | +|---|---| +| Feature definition, storage, and serving | **Feast** | +| Experiment tracking, metric logging, and model versioning | **MLflow** | +| Workflow orchestration and distributed training | **Kubeflow Pipelines + Training Operator** | +| Hyperparameter optimization | **MLflow + Katib (Kubeflow)** | +| Production feature serving | **Feast Online Store / Feature Server** | + +Feast, MLflow, and Kubeflow are each best-in-class at what they do, and they are designed to work alongside one another rather than replace each other. By combining them you get a fully open-source, end-to-end ML platform that handles everything from raw data to live predictions — without lock-in. + +If you are new to Feast, check out the [Feast documentation](https://docs.feast.dev/) and [GitHub](https://github.com/feast-dev/feast) to get started. Join the community on [Slack](http://slack.feastsite.wpenginepowered.com/) and let us know how you are using Feast in your ML stack! diff --git a/infra/website/docs/blog/feast-mlflow-native-integration.md b/infra/website/docs/blog/feast-mlflow-native-integration.md new file mode 100644 index 00000000000..882fd7ef6f6 --- /dev/null +++ b/infra/website/docs/blog/feast-mlflow-native-integration.md @@ -0,0 +1,270 @@ +--- +title: "Native MLflow Integration for Feast: Automatic Feature Lineage for Every Experiment" +description: "Feast now ships native MLflow integration : enable it in feature_store.yaml and every feature retrieval is automatically linked to the MLflow run that consumed it. No glue code, no manual tagging, full model-to-feature traceability." +date: 2026-06-01 +authors: ["Vanshika"] +--- + +
+ Feast Native MLflow Integration +
+ +# Native MLflow Integration for Feast + +## The Problem: Features and Experiments Live in Separate Worlds + +Feast manages your features. MLflow tracks your experiments. But between the two, there has always been a manual gap. + +When a data scientist trains a model, the features that shaped it are retrieved from Feast, but MLflow has no idea which features were used, which feature service they belong to, or what entity DataFrame produced the training set. The result is a familiar set of problems: + +- **"Which features did model v3 use?"** — dig through notebooks and hope the comments are accurate. +- **"Can I reproduce the training data for last month's experiment?"** — re-derive the entity DataFrame from memory. +- **"Which models break if I change `driver_hourly_stats`?"** — grep through repos and ask around. +- **"I promoted a model — which features do I need to serve?"** — read the training script, cross-reference with the feature registry. + +Teams have tried to close this gap with manual `mlflow.log_param("features", ...)` calls, custom wrappers, or convention-based tagging. These approaches are fragile, inconsistent, and the first thing to break when someone new joins the team. + +## The Solution: One Config Line, Automatic Lineage + +Starting with Feast v0.62, the Feast–MLflow integration is **native and zero-code**. Add an `mlflow:` block to your `feature_store.yaml`, and every feature retrieval inside an active MLflow run is automatically tagged with the features, feature views, feature service, entity count, and retrieval duration. + +```yaml +project: driver_ranking +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db +mlflow: + enabled: true + tracking_uri: http://127.0.0.1:5000 +``` + +That's it. No decorators, no wrappers, no `import mlflow` scattered through your training code. + +## How It Works + +### Auto-Logging: Zero Code, Full Lineage + +When `mlflow.enabled: true` and an active MLflow run exists, Feast hooks into `get_historical_features()` and `get_online_features()` at the end of each call and writes structured metadata to the run: + +| Tag | Example | +|-----|---------| +| `feast.project` | `driver_ranking` | +| `feast.retrieval_type` | `historical` | +| `feast.feature_service` | `driver_activity_v1` | +| `feast.feature_views` | `driver_hourly_stats` | +| `feast.feature_refs` | `driver_hourly_stats:conv_rate, driver_hourly_stats:acc_rate` | +| `feast.entity_count` | `200` | +| `feast.feature_count` | `5` | +| `feast.job_submission_sec` | `0.43` (metric) | + +Even if features are passed as a list of refs rather than a `FeatureService` object, Feast auto resolves the matching feature service from the registry. The resolution is cached with a 5-minute TTL, so there is no registry overhead on every call. + +
+ Model metadata with Feast tags in MLflow + Feature lineage from data source to model +
+ +### The `store.mlflow` API + +The integration surfaces through a single property on `FeatureStore`: + +```python +from feast import FeatureStore +store = FeatureStore(".") + +with store.mlflow.start_run(run_name="v1_training"): + # Auto-logged: feature refs, feature views, entity count, duration + training_df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + model = train(training_df) + + # Saves feast_features.json alongside the model artifact + store.mlflow.log_model(model, "model") + + train_run_id = store.mlflow.active_run_id + +# Propagates feast.feature_service to the model version +store.mlflow.register_model(f"runs:/{train_run_id}/model", "driver_model") + +# Prediction: links back to the training run +with store.mlflow.start_run(run_name="batch_prediction"): + model = store.mlflow.load_model("models:/driver_model/1") + features = store.get_online_features( + features=store.get_feature_service("driver_activity_v1"), + entity_rows=[{"driver_id": 1001}], + ) + predictions = model.predict(...) +``` + +`store.mlflow` is lazy-initialized on first access. When MLflow is not installed or `enabled` is `false`, it returns `None` — so existing code that doesn't use MLflow is unaffected. + +### Model-to-Feature Resolution + +This is the capability that closes the loop between experiment tracking and production serving. Given any registered model URI, Feast can tell you exactly which feature service it needs: + +```python +fs_name = store.mlflow.resolve_features("models:/driver_model/1") +# Returns: "driver_activity_v1" +``` + +Resolution follows a precise chain: + +1. Check the model version tag `feast.feature_service` (set by `register_model`) +2. Fall back to the training run tag `feast.feature_service` (set by auto-logging) +3. Validate against the `feast_features.json` artifact to ensure the feature service projections match the features the model was actually trained on + +If there is a mismatch : say someone renamed a feature in the service after training — `resolve_features()` raises `FeastMlflowModelResolutionError` with a clear diff. No silent serving skew. + +This enables a powerful production pattern: your serving pipeline doesn't hardcode feature names. It resolves them from the model: + +```python +fs_name = store.mlflow.resolve_features(f"models:/driver_model/production") +features = store.get_online_features( + features=store.get_feature_service(fs_name), + entity_rows=request_entities, +) +``` + +Promote a new model version that uses different features, and the serving pipeline auto-adapts. + +
+ Registered model with feast.feature_service tag +
+ +### Training Reproducibility + +When `auto_log_entity_df: true`, the integration saves the entity DataFrame as a Parquet artifact on every historical retrieval. Later, you can reconstruct the exact training inputs: + +```python +entity_df = store.mlflow.get_training_entity_df(run_id="abc123") + +with store.mlflow.start_run(run_name="retrain_v2"): + new_df = store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() +``` + +Even without entity DataFrame archival, Feast always logs metadata : row count, column names, date range, or the SQL query — so you have an audit trail of what went into the model. + +
+ Entity DataFrame saved as artifact in MLflow +
+ +### Operations Audit Trail + +When `log_operations: true`, `feast apply` and `feast materialize` are logged to a dedicated MLflow experiment (`{project}-feast-ops`). These are self-contained runs : they don't require a user-initiated active run: + +```yaml +mlflow: + enabled: true + log_operations: true + ops_experiment_suffix: "-feast-ops" +``` + +Apply runs record which feature views, feature services, and entities were created, updated, or deleted. Materialize runs record the feature views, date range, and duration. This gives platform teams a time-series audit trail of every registry and materialization change. + +
+ Operations audit trail in MLflow +
+ +### Dataset Tracking + +For teams that use MLflow's dataset tracking, the integration provides an explicit API: + +```python +store.mlflow.log_training_dataset( + df=training_df, + dataset_name="driver_training_v1", + source="feast.get_historical_features", +) +``` + +This uses `mlflow.data.from_pandas` and `mlflow.log_input` to register the DataFrame as a dataset input on the active run. + +## Two Access Patterns + +The integration provides two ways to access MLflow, depending on your preference: + +### 1. `store.mlflow` — explicit, multi-store safe + +```python +store = FeatureStore(".") +store.mlflow.start_run(run_name="training") +store.mlflow.log_model(model, "model") +``` + +`store.mlflow` only exposes Feast-enhanced methods. For raw MLflow access, use the escape hatches: + +```python +store.mlflow.client # MlflowClient instance +store.mlflow.mlflow # raw mlflow module +``` + +### 2. `feast.mlflow` — drop-in module replacement + +```python +import feast.mlflow + +feast.mlflow.start_run(run_name="training") # Feast-enhanced +feast.mlflow.log_params({"lr": "0.01"}) # passthrough to mlflow +feast.mlflow.log_model(model, "model") # Feast-enhanced +``` + +`feast.mlflow` auto-discovers the most recently created `FeatureStore`. For Feast-specific methods (like `log_model`, `register_model`, `resolve_features`), it uses the enhanced version. For everything else (`log_params`, `set_tag`, `MlflowClient`, ...), it delegates to raw `mlflow`. One import, both worlds. + +## Feast UI Integration + +The Feast UI automatically surfaces MLflow data when the integration is enabled. Three new API endpoints power the UI: + +| Endpoint | What it shows | +|----------|---------------| +| `/api/mlflow-runs` | All Feast-tagged runs with linked registered models | +| `/api/mlflow-feature-usage` | Per-feature-view: run count, last used, associated models | +| `/api/mlflow-feature-models` | Reverse index: feature ref to registered models | + +The feature view detail page shows MLflow training run count, last-used date, and a table of registered models that depend on the view. The registry graph visualization draws edges from feature services through MLflow runs to registered models. + +When MLflow is not enabled, these endpoints return empty responses and the UI components are hidden — no visual noise for users who don't use MLflow. + +
+ Feast UI Feature List with MLflow model associations + Feast UI Feature View detail with MLflow usage +
+ +## Configuration Reference + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | `bool` | `false` | Master switch | +| `tracking_uri` | `string` | (env/default) | MLflow tracking URI | +| `auto_log` | `bool` | `true` | Auto-tag runs on retrieval | +| `auto_log_entity_df` | `bool` | `false` | Save entity DataFrame as artifact | +| `entity_df_max_rows` | `int` | `100000` | Skip artifact for large DataFrames | +| `log_operations` | `bool` | `false` | Log apply/materialize to ops experiment | +| `ops_experiment_suffix` | `string` | `"-feast-ops"` | Ops experiment name suffix | + +## Getting Started + +Install Feast with MLflow support: + +```bash +pip install feast[mlflow] +``` + +Add the `mlflow:` block to your `feature_store.yaml`, start an MLflow tracking server, and run your training code. Features are automatically linked to experiments from the first retrieval. + +
+ End-to-end lineage from data source to registered model +
+ +## Join the Conversation + +We'd love to hear how you're using (or plan to use) the Feast–MLflow integration. Reach out on [Slack](https://slack.feast.dev/) or [GitHub](https://github.com/feast-dev/feast) — issues and PRs welcome! + + diff --git a/infra/website/docs/blog/feast-offline-store-sox-metrics.md b/infra/website/docs/blog/feast-offline-store-sox-metrics.md new file mode 100644 index 00000000000..a41f951f604 --- /dev/null +++ b/infra/website/docs/blog/feast-offline-store-sox-metrics.md @@ -0,0 +1,386 @@ +--- +title: "Extending Feast Observability: Offline Store Metrics and SOX Audit Logging" +description: "Feast now captures RED metrics for offline store retrievals and emits structured SOX audit logs for both online and offline feature access — closing the observability gap between serving and training paths." +date: 2026-06-09 +authors: ["Jitendra Yejare"] +--- + +
+ Feast Offline Store Metrics and SOX Audit Logging — Prometheus metrics for offline retrievals and structured audit logs for compliance +
+ +# Extending Feast Observability: Offline Store Metrics and SOX Audit Logging + +In [our previous post](/blog/feast-feature-server-monitoring), we introduced built-in Prometheus metrics for the Feast feature server — covering the full online serving lifecycle from HTTP request handling through online store reads, on-demand feature transformations, materialization pipelines, and feature freshness tracking. + +That covered the **online** path. But production ML systems don't just serve features in real time — they also build training datasets through offline store retrievals. And for teams operating in regulated environments (financial services, healthcare, government), observability isn't enough. You need an **auditable record** of who accessed what data, when, and how much. + +This post covers two new capabilities added to Feast: + +1. **Offline Store RED Metrics** — Prometheus counters and histograms for offline store retrieval operations (request rate, error rate, latency, row counts) +2. **SOX Audit Logging** — Structured JSON audit log entries for both online and offline feature retrieval paths, routed to a dedicated `feast.audit` logger + +Together, these close the observability gap between online and offline operations and give compliance teams the structured audit trail they need. + +## Offline Store Metrics: Closing the Observability Gap + +The online feature server already had comprehensive metrics, but the offline store — where `get_historical_features` queries execute against your data warehouse to build training datasets — had zero instrumentation. This matters because training-serving skew, stalled pipelines, and data volume anomalies all originate in the offline path. + +### The Problem + +Without offline store metrics, teams faced three blind spots: + +- **Silent training failures** — An offline retrieval that returns incomplete data (or errors out) produces a corrupted training dataset. Models trained on bad data degrade in production, and without metrics, there's no signal until prediction quality drops. +- **Invisible pipeline stalls** — A `get_historical_features` call that normally takes 30 seconds but suddenly takes 10 minutes looks like a "hang" from the orchestrator's perspective. No latency metrics means no alerting until the pipeline times out. +- **Data volume anomalies** — If a typical training query returns 500K rows but suddenly returns 50K, something changed upstream. Without row count tracking, this silently propagates into model training. + +### How Feast Solves It + +Feast now automatically captures RED metrics (Rate, Errors, Duration) for every offline store retrieval — regardless of the backend. Whether you're running against BigQuery, Redshift, Snowflake, DuckDB, or local files, you get the same three Prometheus metrics out of the box: + +- **`feast_offline_store_request_total`** — Counts every retrieval, labeled by success/error. Set an alert and know immediately when training pipelines start failing. +- **`feast_offline_store_request_latency_seconds`** — Latency histogram with buckets tuned for offline workloads (`0.1s` to `10min`). Set SLOs and catch slow queries before pipelines time out. +- **`feast_offline_store_row_count`** — Row count histogram covering `100` to `5M` rows. Detect data volume anomalies before they reach model training. + +Metrics collection never interferes with your queries — if the metrics path fails for any reason, your offline retrieval completes normally. + +``` +# Alert when offline retrievals start failing +- alert: FeastOfflineStoreErrors + expr: rate(feast_offline_store_request_total{status="error"}[15m]) > 0 + for: 5m + labels: + severity: critical + annotations: + summary: > + Offline store retrievals are failing ({{ $value }} errors/sec). + Training pipelines may be producing incomplete datasets. +``` + +## Why SOX Audit Logging Matters + +For organizations subject to SOX (Sarbanes-Oxley), GDPR, HIPAA, or other regulatory frameworks, you need to answer questions like: + +- *Who accessed customer features at 3:47 PM on March 15th?* +- *Which feature views were involved in the training dataset built yesterday?* +- *How many rows of PII-adjacent data were retrieved by the batch scoring pipeline?* + +Before this change, answering these questions required parsing unstructured application logs and correlating timestamps across services. Feature stores sit at the intersection of data access and ML model behavior — yet most have no structured audit trail. + +Feast now emits **structured JSON audit entries** for both online and offline retrieval paths, routed to a dedicated `feast.audit` logger that can be independently sent to your SIEM, log aggregator, or compliance sink — without touching your operational log pipeline. + +What makes this production-ready: + +- **PII-minimized by design.** Entity key *names* are logged, not *values*. A compliance auditor sees "the ML pipeline accessed `user_id` features from `transaction_features` at 3:47 PM" without the log itself containing PII. +- **Dedicated logger.** Audit entries go to `feast.audit`, separate from the application logger. Route them to a SOX-compliant sink (Splunk, ELK with retention policies, S3 with WORM locks) independently. +- **Never breaks your serving path.** Audit logging is best-effort — a broken audit sink never affects feature serving latency or availability. +- **Zero overhead when disabled.** `audit_logging` defaults to `false`. Enable it only when you need it. + +## The New Metrics + +### Offline Store RED Metrics + +| Metric | Type | Labels | What It Answers | +|--------|------|--------|-----------------| +| `feast_offline_store_request_total` | Counter | `method`, `status` | What is my offline retrieval throughput and error rate? | +| `feast_offline_store_request_latency_seconds` | Histogram | `method` | How long are my training data queries taking? | +| `feast_offline_store_row_count` | Histogram | `method` | How much data are my offline retrievals returning? | + +The `method` label captures the retrieval type (`to_arrow`), and `status` is `success` or `error`. The latency histogram uses wide buckets tuned for offline workloads: `0.1s, 0.5s, 1s, 5s, 10s, 30s, 60s, 2min, 5min, 10min` — because offline queries can range from sub-second (small entity sets against local files) to minutes (large point-in-time joins against BigQuery or Redshift). + +The row count histogram uses exponential buckets: `100, 1K, 10K, 100K, 500K, 1M, 5M` — covering the range from small test retrievals to production training datasets. + +### SOX Audit Log Entries + +**Online feature request audit entry:** + +```json +{ + "event": "online_feature_request", + "timestamp": "2026-06-07T14:42:29.739Z", + "requestor_id": "service-account:ml-pipeline", + "entity_keys": ["driver_id"], + "entity_count": 5, + "feature_views": ["driver_hourly_stats"], + "feature_count": 3, + "status": "success", + "latency_ms": 12.45 +} +``` + +**Offline feature retrieval audit entry:** + +```json +{ + "event": "offline_feature_retrieval", + "timestamp": "2026-06-07T14:42:29.739Z", + "method": "to_arrow", + "start_time": "2026-06-07T14:42:29.697Z", + "end_time": "2026-06-07T14:42:29.739Z", + "feature_views": ["driver_hourly_stats"], + "feature_count": 3, + "row_count": 150000, + "status": "success", + "duration_ms": 42.39 +} +``` + +Each entry is a single JSON line, making it trivial to parse with `jq`, ingest into Elasticsearch, or stream to a Kafka topic for compliance processing. + +**Note on accessor identity:** Online audit entries include `requestor_id`, extracted from the Feast authentication layer (SecurityManager). Offline retrievals run as direct SDK calls in the user's own process (a notebook, Airflow task, or training script) — there is no server in the middle to extract auth context. In production SOX environments, offline accessor identity is typically established at the infrastructure level: the Kubernetes service account running the job, the IAM role accessing the data warehouse, or the CI/CD pipeline identity. A future enhancement could optionally capture identity from `os.getenv("USER")` or an explicit SDK parameter. + +## Enabling the New Metrics + +### YAML Configuration + +Add `offline_features` and `audit_logging` to your `feature_store.yaml`: + +```yaml +feature_server: + metrics: + enabled: true + resource: true + request: true + online_features: true + push: true + materialization: true + freshness: true + offline_features: true # NEW: Offline store RED metrics + audit_logging: true # NEW: SOX audit log entries +``` + +`offline_features` defaults to `true` when metrics are enabled (consistent with other categories). `audit_logging` defaults to `false` — it's opt-in because audit entries have a non-trivial cost (JSON serialization + I/O per request) and are only needed in regulated environments. + +### CLI + +When using `feast serve --metrics`, offline store metrics are enabled by default. Audit logging still requires the YAML toggle since it's opt-in. + +### Routing Audit Logs + +The `feast.audit` logger is a standard Python logger. Configure it like any other: + +```python +import logging + +audit_logger = logging.getLogger("feast.audit") +audit_logger.setLevel(logging.INFO) +audit_logger.propagate = False + +handler = logging.FileHandler("/var/log/feast/audit.log") +handler.setFormatter(logging.Formatter("%(message)s")) +audit_logger.addHandler(handler) +``` + +Or route to a JSON-aware sink in production: + +```yaml +# logging.yaml for production +loggers: + feast.audit: + level: INFO + propagate: false + handlers: [audit_file, splunk_forwarder] +``` + +## Key PromQL Queries for Offline Store + +**Throughput and errors:** + +```promql +# Offline retrieval rate +rate(feast_offline_store_request_total[5m]) + +# Offline error rate +sum(rate(feast_offline_store_request_total{status="error"}[5m])) + / sum(rate(feast_offline_store_request_total[5m])) +``` + +**Latency percentiles:** + +```promql +# Offline retrieval p95 latency +histogram_quantile(0.95, + sum(rate(feast_offline_store_request_latency_seconds_bucket[5m])) by (le)) + +# Average offline retrieval duration +rate(feast_offline_store_request_latency_seconds_sum[5m]) + / rate(feast_offline_store_request_latency_seconds_count[5m]) +``` + +**Row count analysis:** + +```promql +# Average rows per retrieval +feast_offline_store_row_count_sum / feast_offline_store_row_count_count + +# p95 row count (detect large retrievals) +histogram_quantile(0.95, + sum(rate(feast_offline_store_row_count_bucket[5m])) by (le)) +``` + +## Building Alerts for Offline Store + +### Offline Retrieval Failures + +```yaml +- alert: FeastOfflineStoreErrors + expr: rate(feast_offline_store_request_total{status="error"}[15m]) > 0 + for: 5m + labels: + severity: critical + annotations: + summary: > + Offline store retrievals are failing. + Training pipelines may be producing incomplete datasets. +``` + +### Slow Offline Queries + +```yaml +- alert: FeastOfflineStoreSlowQuery + expr: | + histogram_quantile(0.95, + sum(rate(feast_offline_store_request_latency_seconds_bucket[5m])) by (le) + ) > 300 + for: 5m + labels: + severity: warning + annotations: + summary: > + Offline store p95 latency is {{ $value | humanizeDuration }}. + Training pipelines may be stalling. +``` + +### Row Count Anomaly + +```yaml +- alert: FeastOfflineStoreRowCountDrop + expr: | + feast_offline_store_row_count_sum / feast_offline_store_row_count_count + < 0.5 * avg_over_time( + (feast_offline_store_row_count_sum / feast_offline_store_row_count_count)[1d:1h]) + for: 10m + labels: + severity: warning + annotations: + summary: > + Average rows per offline retrieval dropped by >50%. + Possible upstream data issue. +``` + +## The Extended Grafana Dashboard + +We've extended the existing Feast Grafana dashboard with a dedicated **Offline Store** section containing six new panels: + +- **Offline Store Request Rate** — Rate of offline retrievals by method and status +- **Offline Store Total Requests** — Cumulative request counts (stat panel) +- **Offline Store Retrieval Latency (p50/p95/p99)** — Latency percentile time series +- **Offline Store Row Count Distribution** — Row count percentiles over time +- **Avg Offline Retrieval Duration** — Average duration per method +- **Offline Store Error Rate** — Gauge showing current error percentage with threshold coloring + +
+ Grafana dashboard showing dedicated offline store containing six new panels +
+ +These panels sit alongside the existing online store panels, giving you a single dashboard that covers both serving paths. + +For SOX compliance, a separate **Audit Trail** dashboard powered by Loki visualizes: + +- **Total Audited Events** — Count of all audited access events +- **Online vs Offline Access Timeline** — Stacked time series showing access patterns +- **Offline Data Volume** — Total rows retrieved over time, flagging bulk data exports +- **Anomaly Detection** — Large row counts and slow queries that may need compliance review + +
+ Grafana dashboard showing SOX compliance and access containing five new panels +
+ +- **Live Audit Log Stream** — Raw structured audit entries, expandable for investigation + +
+ Grafana dashboard showing audit logs for offline store +
+ + +## Updated Metrics Summary + +| Category | Metric | What It Answers | +|----------|--------|-----------------| +| **Online** Request | `feast_feature_server_request_total` | What is my online throughput and error rate? | +| **Online** Request | `feast_feature_server_request_latency_seconds` | What are my online p50/p99 latencies? | +| **Online** Features | `feast_online_features_entity_count` | What is my online traffic shape? | +| **Online** Store Read | `feast_feature_server_online_store_read_duration_seconds` | Is my online store the bottleneck? | +| ODFV Transform | `feast_feature_server_transformation_duration_seconds` | How expensive are my read-path transforms? | +| ODFV Transform | `feast_feature_server_write_transformation_duration_seconds` | How expensive are my write-path transforms? | +| Push | `feast_push_request_total` | Is my ingestion pipeline sending data? | +| Materialization | `feast_materialization_total` | Are my pipelines succeeding? | +| Materialization | `feast_materialization_duration_seconds` | How long do my pipelines take? | +| Freshness | `feast_feature_freshness_seconds` | How stale is the data my models are using? | +| Resource | `feast_feature_server_cpu_usage / memory_usage` | Is my server healthy? | +| **Offline** Request | `feast_offline_store_request_total` | What is my offline retrieval throughput? | +| **Offline** Latency | `feast_offline_store_request_latency_seconds` | How long are my training queries taking? | +| **Offline** Row Count | `feast_offline_store_row_count` | How much data are retrievals returning? | +| **Audit** | `feast.audit` logger (online) | Who requested which features, when? | +| **Audit** | `feast.audit` logger (offline) | Which training datasets were built, with how much data? | + +## How to Try It + +### Automated Demo + +We've extended the [feast-prometheus-metrics](https://github.com/ntkathole/feast-automated-setups/tree/main/feast-prometheus-metrics) automated demo to include offline store metrics and SOX audit logging. The extended traffic generator exercises both online and offline paths: + +```bash +# Clone and run +git clone https://github.com/ntkathole/feast-automated-setups.git +cd feast-automated-setups/feast-prometheus-metrics + +# Run setup (uses feast from your environment) +./setup.sh + +# Generate extended traffic including offline retrievals +python3 generate_traffic_extended.py \ + --url http://localhost:6566 \ + --duration 120 \ + --repo-path workspace/feast_demo/feature_repo \ + --log-dir workspace/logs +``` + +After traffic generation, check the audit log: + +```bash +# View structured audit entries +cat workspace/logs/feast_audit.log | python3 -m json.tool + +# Count by event type +cat workspace/logs/feast_audit.log | \ + python3 -c "import sys,json; events=[json.loads(l)['event'] for l in sys.stdin]; print({e:events.count(e) for e in set(events)})" +``` + +### Manual Verification + +Verify offline store metrics are being emitted: + +```bash +# Check the Prometheus metrics endpoint for offline store metrics +curl -s http://localhost:8000 | grep feast_offline + +# Query Prometheus directly +curl -s 'http://localhost:9090/api/v1/query?query=feast_offline_store_request_total' +``` + +### Enable in Your Deployment + +1. **Update `feature_store.yaml`** — Add `offline_features: true` and `audit_logging: true` to the metrics block +2. **Configure audit log routing** — Set up a handler for the `feast.audit` logger in your logging config +3. **Import the updated Grafana dashboard** — Add the offline store panels to your existing dashboard +4. **Set up alerts** — Start with offline retrieval failures and row count anomalies + + +We're excited to bring full-lifecycle observability to Feast — covering both the real-time serving path and the batch training path — and welcome feedback from the community! + +--- + +*References:* +- *[Existing blog: Monitoring Your Feast Feature Server with Prometheus and Grafana](https://feast.dev/blog/feast-feature-server-monitoring/)* +- *[Feast Prometheus Metrics Demo](https://github.com/ntkathole/feast-automated-setups/tree/main/feast-prometheus-metrics)* diff --git a/infra/website/docs/blog/feast-online-server-performance-tuning.md b/infra/website/docs/blog/feast-online-server-performance-tuning.md new file mode 100644 index 00000000000..ffe29910b9e --- /dev/null +++ b/infra/website/docs/blog/feast-online-server-performance-tuning.md @@ -0,0 +1,338 @@ +--- +title: "Tuning the Feast Feature Server for Sub-2ms Online Serving" +description: "A practical guide to achieving low-latency, high-throughput feature serving with Feast on Kubernetes — from default configuration to production-grade performance with pre-computed feature vectors and benchmarks at every step." +date: 2026-06-02 +authors: ["Nikhil Kathole"] +--- + +**Feast supports production-grade worker configuration, connection pooling, async reads, batched pipelines, serialization optimizations, and pre-computed feature vectors for the Python feature server.** This post walks through a real-world performance tuning exercise in two stages: first, server and client tuning that brings p99 latency down to **sub-5ms** for single-row requests; then, **pre-computed feature vectors** that push it further to **sub-2ms p99** — regardless of how many feature views your FeatureService spans. We share the benchmarking methodology, the exact configuration changes, and the measured impact of each step so you can apply the same approach to your own deployments. + +--- + +## The Problem + +When you deploy Feast on Kubernetes using the [Feast Operator](https://docs.feast.dev/how-to-guides/production-deployment-topologies), the default configuration is designed for simplicity — a single Gunicorn worker, short keep-alive timeouts, and frequent registry refreshes. This is fine for development but leaves significant performance on the table for production workloads where every millisecond matters. + +We set out to answer a practical question: **how low can we push the Feast online server's p99 latency, and what does it take to get there?** + +--- + +## Test Environment + +| Component | Configuration | +|-----------|--------------| +| **Online Store** | Redis 7.0.12 (standalone, in-cluster) | +| **Registry** | PostgreSQL 16 (SQL registry) | +| **Platform** | Kubernetes | +| **Deployment** | Feast Operator with `FeatureStore` CR | + +We used a banking feature store project with multiple feature views spanning customer demographics, transactions, and behavioral profiles. + +All benchmarks run 200 iterations (after 30–50 warmup) for each scenario, measuring p50, p95, p99, and mean latency. Throughput is measured with 10 concurrent workers over 15 seconds. + +--- + +## Three Access Modes + +Feast supports three ways to retrieve online features. Understanding how each one works is key to knowing where latency comes from — and where to optimize. + +### REST API + +``` +Client → HTTP POST (JSON) → Gunicorn/FastAPI Server → Redis mget() → JSON response +``` + +The simplest and most common pattern. Your application sends a JSON request to the feature server's `/get-online-features` endpoint. The server holds persistent Redis connections and a pre-loaded registry, so each request is just a Redis read plus JSON serialization. HTTP keep-alive reuses TCP/TLS connections across requests. + +### Direct SDK + +``` +Client (Python) → FeatureStore SDK → Redis mget() directly +``` + +The Python SDK connects to Redis directly — no HTTP hop, no JSON overhead. However, it pays for in-process registry lookups and entity key serialization on every call, and reads each FeatureView sequentially. + +### Remote SDK + +``` +Client (Python SDK) → HTTP POST → Feature Server → Redis → JSON → Client +``` + +The SDK delegates feature retrieval to a remote feature server over HTTP. This combines the worst of both worlds: SDK-side overhead *plus* an HTTP round-trip. Without connection pooling, each call creates a new TCP connection and TLS handshake. + +--- + +## Baseline: Default Configuration + +With no tuning applied — a single Gunicorn worker, default timeouts, and no connection pooling: + +| Mode | p99 (1 row) | p99 (5 rows) | Throughput | +|------|----------------|-------------------|------------| +| **REST API** | 6.92 ms | 4.94 ms | 480 RPS | +| **Direct SDK** | 5.83 ms | 5.59 ms | — | +| **Remote SDK** | 11.71 ms | **74.31 ms** | ~2 RPS | + +The REST API and Direct SDK are already in the 5–7ms range out of the box, but the Remote SDK fails badly — p99 spiking to **74ms** at just 5 rows due to per-request TCP/TLS setup overhead. This is our starting point. + +--- + +## Server-Side Configuration + +These are changes you apply to the **feature server deployment** — no code changes needed, just configuration via the `FeatureStore` CR and Redis runtime settings. + +### Worker Tuning via the Feast Operator + +The Feast Operator exposes `workerConfigs` in the `FeatureStore` CR, letting you tune the Gunicorn server without rebuilding images: + +```yaml +apiVersion: feast.dev/v1alpha1 +kind: FeatureStore +spec: + services: + onlineStore: + server: + workerConfigs: + workers: -1 # Auto: 2 × CPU cores + 1 + keepAliveTimeout: 120 # Reuse connections longer + maxRequests: 5000 # Recycle workers to prevent memory leaks + maxRequestsJitter: 200 # Stagger recycling + registryTTLSeconds: 300 # Reduce registry refresh overhead + workerConnections: 2000 # High-concurrency support +``` + +Setting `workers: -1` on a 4-core pod gives 9 Gunicorn workers, each with its own event loop and Redis connection. This is the **single most impactful change** — it transforms the server from single-threaded to multi-process, dropping 5-row p99 from ~10ms to ~8ms and putting us on the path to sub-5ms. + +### Redis Runtime Tuning + +Three Redis settings made a measurable difference: + +- **`hz 100`** (default 10) — Redis processes expired keys and timeouts 10x faster, reducing tail latency spikes. +- **`tcp-keepalive 60`** (default 300) — Detects dead connections 5x faster, freeing resources sooner. +- **`save ""`** (disable RDB persistence) — Eliminates periodic snapshot I/O that causes 10–50ms p99 spikes. Since features are materialized from the offline store and reconstructible at any time, persistence is unnecessary. + +### High Availability and Auto-Scaling + +For production, we added horizontal scaling and availability guarantees using the Feast Operator's [built-in HA support](https://docs.feast.dev/how-to-guides/feast-snowflake-gcp-aws/scaling-feast): + +```yaml +spec: + replicas: 2 + services: + onlineStore: + server: + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + scaling: + autoscaling: + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + pdb: + minAvailable: 1 +``` + +When scaling is enabled, the operator auto-injects pod anti-affinity and zone topology spread constraints, ensuring replicas land on different nodes for resilience. With HPA, the cluster auto-scales based on CPU utilization — we observed it scaling from 2 to 3 pods in response to load during benchmarks. At 10 pods with 9 workers each, theoretical throughput reaches ~7,180 RPS (~25.8M RPH). + +### Server-side quick wins summary + +1. **Set `workers: -1`** — single most impactful change +2. **Disable Redis persistence** — `CONFIG SET save ""` +3. **Set `registryTTLSeconds: 300`** — reduce registry refresh overhead +4. **Use `replicas: 2`** minimum with HPA for burst capacity +5. **Set resource limits** — defaults are far too low for production + +--- + +## Client-Side Configuration + +These are changes you apply on the **client** — how the SDK connects to the feature server and which access mode you choose. + +### Connection Pooling for the Remote SDK + +The biggest problem with the Remote SDK was that every call created a brand-new `requests.Session`, established a fresh TCP connection, negotiated TLS, and then threw it all away — adding 2–4ms per call for HTTPS endpoints. + +Feast now includes `HttpSessionManager` — a thread-safe, singleton session manager that reuses HTTP connections across requests with configurable pooling and retry: + +```yaml +online_store: + type: remote + path: https://feast-server:443 + connection_pool_size: 50 + connection_idle_timeout: 300 + connection_retries: 3 +``` + +This dropped Remote SDK 5-row p99 from **74ms to 21ms** — a 72% reduction — by eliminating the per-request TLS handshake. + +### Choosing the right access mode + +| Use Case | Recommended Mode | Why | +|----------|-----------------|-----| +| **Application serving** | REST API | Sub-5ms single-row p99, simplest integration, 718 RPS per pod | +| **Python ML pipeline** | Direct SDK | No HTTP hop, sub-5ms p99, native protobuf | +| **Async Python applications** | Async Direct SDK | Non-blocking, batched pipeline, sub-5ms p99 | +| **Cross-cluster serving** | Remote SDK + pooling | When the client can't reach Redis directly; 760 RPS with pooling | + +--- + +## Code Enhancements in Feast + +Beyond configuration, several code-level improvements in Feast itself contributed to reaching sub-5ms p99. These require no user configuration — just upgrading to the latest Feast version. + +### Serialization Optimization + +The feature server used `google.protobuf.json_format.MessageToDict` to convert protobuf responses to JSON — a generic, reflection-based serializer that was a meaningful fraction of server-side latency. Replacing it with an optimized custom dict builder delivered a **66% throughput increase** (432 to 718 RPS) and **72% reduction in tail latency under load** (132ms to 37ms p99). + +### Async Redis Reads with Batched Pipeline + +The `RedisOnlineStore` had async support (`online_read_async` with `redis_asyncio`), but the `async_supported` property was not overridden, so the feature server never used it. Enabling it unlocks non-blocking I/O on the server side — the FastAPI handler calls `get_online_features_async` directly instead of wrapping the sync path in `run_in_threadpool`. + +Additionally, the base class async path issued O(N_feature_views) separate round trips to Redis via `asyncio.gather`. We added a `get_online_features_async` override to `RedisOnlineStore` that batches all HMGET commands across all feature views into a **single async pipeline execution** (O(1) round trips), matching the existing sync batched pipeline. This cut async 5-row p99 from ~11ms to **5.6ms** — a 49% improvement. + +### Cached Per-Request Checks + +`_check_versioned_read_support()` performed up to 7 lazy module imports on **every request** to determine if the current online store supports versioned reads. We cache the result per store instance, resolving imports once and eliminating ~0.5–1ms of overhead per request. + +### Skip Duplicate Feature Resolution + +When auth is `no_auth` (the common case), the feature server was resolving feature views solely to check permissions (which are no-ops), then resolving them again inside `get_online_features`. We skip the first resolution entirely, avoiding a redundant registry lookup. + +### Session Wrapping Fix + +The `rest_error_handling_decorator` re-wrapped cached `requests.Session` HTTP methods on every call. After ~1000 requests, this caused progressive performance degradation and eventually a `RecursionError`. We now wrap each method exactly once per session lifetime, fixing Remote SDK stability and enabling it to sustain **760 RPS**. + +--- + +## Final Results + +After applying all server-side configuration, client-side configuration, and code enhancements: + +### Stage 1: Tuning only (sub-5ms target) + +| Mode | p50 (1 row) | p99 (1 row) | p50 (5 rows) | p99 (5 rows) | Throughput | +|------|----------|----------|----------|----------|------------| +| **REST API** | 3.34 ms | **4.61 ms** | 7.88 ms | 11.32 ms | 718 RPS | +| **REST API (FeatureService)** | 4.21 ms | **6.15 ms** | 9.15 ms | 17.43 ms | — | +| **Direct SDK** | 3.12 ms | **4.21 ms** | 3.29 ms | **4.60 ms** | 402 RPS | +| **Direct SDK (FeatureService)** | 3.48 ms | **5.70 ms** | 3.44 ms | **5.10 ms** | — | +| **Async Direct SDK** | 3.25 ms | **6.25 ms** | 3.47 ms | **8.72 ms** | — | +| **Async Direct SDK (FeatureService)** | 3.60 ms | **4.84 ms** | 3.76 ms | **5.13 ms** | — | +| **Remote SDK** | 3.34 ms | **5.30 ms** | 8.15 ms | 11.63 ms | 760 RPS | +| **Remote SDK (FeatureService)** | 3.78 ms | **5.17 ms** | 9.86 ms | 16.06 ms | — | + +### Stage 2: Pre-computed vectors (sub-2ms target) + +| Batch Size | p50 Regular | p99 Regular | p50 Precomputed | p99 Precomputed | Speedup (p50) | +|---|---|---|---|---|---| +| 1 | 5.95 ms | 10.74 ms | **0.98 ms** | **1.70 ms** | 6.1x | +| 5 | 9.66 ms | 44.91 ms | **1.37 ms** | **3.00 ms** | 7.1x | +| 10 | 16.60 ms | 60.37 ms | **1.81 ms** | **2.07 ms** | 9.2x | +| 50 | 60.07 ms | 120.12 ms | **5.27 ms** | **7.49 ms** | 11.4x | +| 100 | 85.58 ms | 208.18 ms | **9.48 ms** | **114.38 ms** | 9.0x | +| 500 | 218.79 ms | 424.91 ms | **40.25 ms** | **198.13 ms** | 5.4x | + +**Key takeaways:** + +- **Stage 1 (tuning)** gets all SDK modes to **sub-5ms p99** for single-row requests — REST API at 4.61ms, Direct SDK at 4.21ms, Async SDK at 4.84ms. +- **Stage 2 (pre-computed vectors)** pushes latency to **sub-2ms p99** for single-row requests — a 6x improvement over the tuned regular path. +- **REST API** delivers the best throughput at **718 RPS** (2.6M RPH); **Remote SDK** sustains **760 RPS** after the session wrapping fix. +- For FeatureServices spanning multiple feature views, **`precompute_online=True` is the single most impactful optimization** — it changes the read complexity from O(N feature views) to O(1). +- At large batch sizes, the bottleneck shifts from store I/O to Python CPU overhead (protobuf deserialization). For these workloads, split large requests into smaller batches on the client side. + +--- + +## A Note on Online Store Selection + +All benchmarks in this post used a **standalone Redis pod** running in the same Kubernetes cluster as the feature server. Production deployments often use managed services — here's how that changes the picture. + +**Managed Redis** (ElastiCache, Memorystore, Azure Cache for Redis) provides dedicated compute, optimized networking, cluster mode for sharding, and automatic failover. In our benchmarks, Redis RTT was ~0.5ms (in-cluster). A managed instance in the **same availability zone** would deliver comparable latency with more consistent tail behavior. Cross-AZ hops add 1–2ms per request. + +**DynamoDB** offers zero operational overhead and automatic scaling. When the feature server runs in the **same AWS region and VPC**, single-digit millisecond reads are typical (1–5ms for eventually consistent reads). With [DAX](https://aws.amazon.com/dynamodb/dax/), read latency drops to microseconds for cached items. A same-region setup could deliver comparable sub-5ms p99 for single-row reads. + +Feast also supports PostgreSQL, SQLite, Snowflake, Bigtable, and more. The general rule is: **the online store is the single largest factor in `get_online_features()` latency** — choose based on your latency budget, throughput needs, and operational requirements. The tuning steps in this post (worker configuration, registry caching, connection pooling, serialization optimization) apply equally to all stores — they optimize the layers above. + +--- + +## Pre-computed Feature Vectors + +The tuning steps above achieve our first target: **sub-5ms p99** for single-row requests. But for FeatureServices spanning multiple feature views, per-FV read fan-out becomes the dominant bottleneck — each request issues N separate store reads, N protobuf deserializations, and N response assemblies. To reach our final target of **sub-2ms**, we need to eliminate this fan-out entirely. + +**Pre-computed feature vectors** do exactly that: at materialize time, all features for a FeatureService are assembled into a single serialized blob per entity. At read time, one key lookup replaces N feature-view reads — reducing the operation from O(N feature views) to O(1) and delivering **sub-2ms p99 latency**. + +### How it works + +1. **Define** a FeatureService with `precompute_online=True`: + +```python +scoring_service = FeatureService( + name="realtime_scoring", + features=[user_profile_fv, transaction_fv, risk_fv], + precompute_online=True, +) +``` + +2. **Apply** and **materialize** as usual — vectors are built automatically: + +```bash +feast apply +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +Feast detects which FeatureServices have `precompute_online=True` and rebuilds their pre-computed vectors after the per-feature-view writes complete. Vectors are also refreshed automatically on `feast push`. + +3. **Read** features as usual — the server automatically uses the pre-computed path: + +```python +features = store.get_online_features( + features=store.get_feature_service("realtime_scoring"), + entity_rows=[{"user_id": "U12345"}], + full_feature_names=True, +) +``` + +### Design decisions + +- **Store-agnostic**: The pre-computed logic lives in the base `OnlineStore` class and works with all backends (Redis, DynamoDB, PostgreSQL, etc.). No store-specific code is needed. +- **Opt-in**: `precompute_online` defaults to `False`. Existing deployments are completely unaffected. +- **Strict error handling**: When `precompute_online=True`, there is no silent fallback to per-FV reads. If vectors are missing or stale, the server raises a `RuntimeError`, making problems visible immediately. +- **Schema-aware**: A fingerprint of feature names detects schema changes and rejects stale vectors, with column-order-independent comparison. +- **Per-FV TTL enforcement**: Individual feature view TTLs are checked within the pre-computed blob. +- **Materialized view pattern**: Conceptually similar to a database materialized view — trades storage for read speed with explicit refresh. + +### Benchmark: precomputed vs regular path + +We benchmarked a FeatureService spanning multiple feature views against the same features read via the regular per-feature-view path. All numbers from the same pod, same run, 200 iterations with 30 warmup. + +| Batch Size (rows/request) | p50 Regular | p50 Precomputed | p99 Regular | p99 Precomputed | Speedup (p50) | +|---|---|---|---|---|---| +| 1 | 5.95 ms | **0.98 ms** | 10.74 ms | **1.70 ms** | 6.1x | +| 5 | 9.66 ms | **1.37 ms** | 44.91 ms | **3.00 ms** | 7.1x | +| 10 | 16.60 ms | **1.81 ms** | 60.37 ms | **2.07 ms** | 9.2x | +| 50 | 60.07 ms | **5.27 ms** | 120.12 ms | **7.49 ms** | 11.4x | + +For the typical production use case of 1–10 rows per inference request, pre-computed vectors deliver **sub-2ms p99** — well under any reasonable SLA target. The speedup ranges from **6x to 9x** depending on batch size, with p50 consistently under 2ms for up to 10 rows. + +--- + +## Try It Yourself + +To deploy the same setup: + +1. Deploy Feast with the Feast Operator using a `FeatureStore` CR with `workerConfigs` +2. Use Redis as the online store and PostgreSQL for the registry +3. Apply the [production tuning guide](https://docs.feast.dev/how-to-guides/online-server-performance-tuning) for worker configuration, registry caching, and scaling +4. For FeatureServices spanning multiple feature views, enable `precompute_online=True` and materialize — see the [feature service docs](https://docs.feast.dev/getting-started/concepts/feature-retrieval#pre-computed-feature-vectors-precompute_online) +5. Monitor with [built-in Prometheus metrics](https://docs.feast.dev/reference/feature-servers/python-feature-server) — `feast_feature_server_request_latency_seconds` is your primary SLI + +We'd love to hear about your production performance results. Join the conversation on [Feast Slack](https://slack.feast.dev) or open an issue on [GitHub](https://github.com/feast-dev/feast). diff --git a/infra/website/docs/blog/feast-openai-compatible-api.md b/infra/website/docs/blog/feast-openai-compatible-api.md new file mode 100644 index 00000000000..f228836afd6 --- /dev/null +++ b/infra/website/docs/blog/feast-openai-compatible-api.md @@ -0,0 +1,364 @@ +--- +title: "Using Feast's OpenAI Compatible Search API" +description: "Feast now exposes an OpenAI-compatible vector store search endpoint. Send a plain text query, get results back in the standard OpenAI format. No client-side embeddings required." +date: 2026-07-07 +authors: ["Chaitanya Patel", "Nikhil Kathole"] +--- + +
+ Sequence diagram showing a client sending a text query to Feast, which embeds and searches server-side +
+ +If you've tried to connect an AI agent to Feast's vector search, you've probably hit this wall: the agent needs to search your feature store, but Feast expects a raw embedding vector. The agent doesn't have one. It has a question in English. + +Until now, the workaround was ugly. You'd call an embedding provider (OpenAI, Ollama, whatever) to turn the text into a float array, then pass that array to Feast's vector search endpoint (`POST /search`, formerly `retrieve-online-documents`). Every client had to know both APIs, carry both sets of credentials, and run glue code whose only job was bridging the gap. + +Feast now has a new endpoint: `POST /v1/vector_stores/{vector_store_id}/search`. It follows the [OpenAI Vector Store Search API](https://platform.openai.com/docs/api-reference/vector-stores-search) format, including proper `vs_{hash}` identifiers for vector stores. You send text, Feast handles the embedding internally, and you get results back in the same JSON shape that OpenAI returns. No float arrays, no extra SDK. + +Each feature view with vector search enabled gets a deterministic `vs_` identifier (e.g. `vs_a1b2c3d4e5f6...`). Discover them via `GET /v1/vector_stores`. + +## The two-API tax + +Here's what searching Feast looked like before: + +```python +import openai +import requests + +# Step 1: Call the embedding provider yourself +embed_response = openai.embeddings.create( + model="text-embedding-3-small", + input="wireless noise-cancelling headphones" +) +query_vector = embed_response.data[0].embedding # 1536 floats + +# Step 2: Call Feast's proprietary API with the raw vector +result = requests.post("http://feast-server:6566/search", json={ + "features": [ + "product_catalog:vector", + "product_catalog:name", + "product_catalog:description", + "product_catalog:price", + ], + "query": query_vector, + "top_k": 5, + "api_version": 2, +}) +``` + +This works fine. But it has costs that add up: + +- Every service calling Feast needs an embedding SDK, an API key, and logic to handle the embedding call. Five microservices means five places managing embedding credentials. +- LLM agents can't use it. They discover tools through MCP or function calling, and they know how to call OpenAI-shaped endpoints. They don't know how to compute embeddings and pass raw float arrays to a custom API. +- The embedding model becomes a client-side decision. Different clients might use different models or versions, which means inconsistent search results against the same vector store. +- Feast's filter syntax is its own format. Not something an agent framework knows out of the box. + +## One endpoint, standard format + +With the new endpoint, that same search looks like this: + +```python +import requests + +# First, discover your vector store IDs +stores = requests.get("http://feast-server:6566/v1/vector_stores").json() +vs_id = stores["data"][0]["id"] # e.g. "vs_a1b2c3d4e5f6..." + +# Then search using the vs_ identifier +result = requests.post( + f"http://feast-server:6566/v1/vector_stores/{vs_id}/search", + json={ + "query": "wireless noise-cancelling headphones", + "max_num_results": 5, + }, +) +``` + +No embedding SDK. No raw vectors. The request and response match OpenAI's format, so anything that already talks to OpenAI can talk to Feast. + +### What happens under the hood + +When Feast receives this request, it: + +1. Embeds the query server-side using the model configured in `feature_store.yaml` (via [Sentence Transformers](https://www.sbert.net/) for local inference — no external API key required). +2. Runs vector similarity search against the feature view's online store (Postgres/pgvector, Milvus, Elasticsearch, SQLite, or whatever backend you've configured). +3. Applies filters if you provided any, using string equality, numeric comparisons, or compound AND/OR conditions in the OpenAI filter format. +4. Returns results in OpenAI's `vector_store.search_results.page` format. + +Because the embedding model is a server-side configuration, every client gets consistent results. No more worrying about whether service A is using `text-embedding-3-small` while service B accidentally stuck with `ada-002`. + +## Setting it up + +### Step 1: Configure the embedding model + +Add an `embedding_model` section to your `feature_store.yaml`: + +```yaml +project: my_project +registry: data/registry.db +provider: local + +online_store: + type: postgres + host: localhost + port: 5432 + database: feast + user: feast + password: ${DB_PASSWORD} + pgvector_enabled: true + vector_len: 384 + enable_openai_compatible_store: true + +embedding_model: + provider: sentence_transformers # default; can be omitted + model: all-MiniLM-L6-v2 +``` + +Feast uses [Sentence Transformers](https://www.sbert.net/) for embedding, so everything runs locally — no external API key required. You can use any HuggingFace model compatible with `SentenceTransformer`: + +```yaml +# Default — lightweight, fast +embedding_model: + model: all-MiniLM-L6-v2 + +# Higher quality, larger model +embedding_model: + model: BAAI/bge-small-en-v1.5 +``` + +### Step 2: Define a feature view with vector search + +```python +from feast import Entity, FeatureView, Field +from feast.types import Array, Float32, String, Float64, Int64 +from datetime import timedelta + +product = Entity(name="product_id", join_keys=["product_id"]) + +product_catalog = FeatureView( + name="product_catalog", + entities=[product], + schema=[ + Field( + name="vector", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="name", dtype=String), + Field(name="description", dtype=String), + Field(name="category", dtype=String), + Field(name="price", dtype=Float64), + Field(name="rating", dtype=Float64), + ], + source=product_source, + ttl=timedelta(days=7), +) +``` + +### Step 3: Apply, load data, and serve + +```bash +feast apply +feast serve +``` + +### Step 4: Discover your vector store ID + +```bash +curl http://localhost:6566/v1/vector_stores +``` + +```json +{ + "object": "list", + "data": [ + { + "id": "vs_a1b2c3d4e5f6a1b2c3d4e5f6", + "object": "vector_store", + "name": "product_catalog", + "status": "completed", + "created_at": 1717200000 + } + ] +} +``` + +### Step 5: Search + +```bash +curl -X POST http://localhost:6566/v1/vector_stores/vs_a1b2c3d4e5f6a1b2c3d4e5f6/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "wireless noise-cancelling headphones", + "max_num_results": 3 + }' +``` + +Response: + +```json +{ + "object": "vector_store.search_results.page", + "search_query": ["wireless noise-cancelling headphones"], + "data": [ + { + "file_id": "vs_a1b2c3d4e5f6a1b2c3d4e5f6_42", + "filename": "vs_a1b2c3d4e5f6a1b2c3d4e5f6", + "score": 0.92, + "attributes": { + "name": "Sony WH-1000XM5", + "description": "Premium wireless noise-cancelling headphones", + "category": "Electronics", + "price": 349.99, + "rating": 4.8 + }, + "content": [ + {"type": "text", "text": "Sony WH-1000XM5"}, + {"type": "text", "text": "Premium wireless noise-cancelling headphones"}, + {"type": "text", "text": "Electronics"} + ] + } + ], + "has_more": false, + "next_page": null +} +``` + +The response follows OpenAI's `vector_store.search_results.page` schema. Any client that already parses OpenAI search results can parse this without changes. + +## Filtering + +The endpoint supports OpenAI-style filters for narrowing results beyond vector similarity. Filters work on the metadata stored alongside your vectors. + +### String filters + +```json +{ + "query": "running shoes", + "max_num_results": 5, + "filters": { + "type": "eq", + "key": "category", + "value": "Footwear" + } +} +``` + +### Numeric filters + +```json +{ + "query": "budget laptop", + "max_num_results": 5, + "filters": { + "type": "lt", + "key": "price", + "value": 500.0 + } +} +``` + +### Compound filters (AND / OR) + +```json +{ + "query": "wireless earbuds", + "max_num_results": 5, + "filters": { + "type": "and", + "filters": [ + {"type": "eq", "key": "category", "value": "Electronics"}, + {"type": "gte", "key": "rating", "value": 4.5}, + {"type": "lt", "key": "price", "value": 200.0} + ] + } +} +``` + +Comparison operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`. Compound operators: `and`, `or`. These nest to arbitrary depth. + +Numeric and boolean filters require the `enable_openai_compatible_store` flag in your online store config, plus a `feast apply` to add the `value_num` column to existing tables. String filters work on all existing schemas without migration. + +## What this means for AI agents + +We built this with agents in mind. When Feast added [MCP support](./feast-agents-mcp) earlier this year, agents could discover and call Feast tools dynamically. But vector search still had this gap where the agent needed to produce a float array. LLMs can't do that. + +Now the search tool is just text in, structured results out. An agent calls it the same way it calls any other OpenAI-compatible service. The feature server currently exposes these tools: + +| Capability | Endpoint | What it does | +|---|---|---| +| Structured feature lookup | `get-online-features` | Get customer profiles, account data, etc. | +| Vector search | `search` | Search with a pre-computed embedding vector (or text via `api_version: 2`) | +| List vector stores | `GET /v1/vector_stores` | Discover available vector stores and their `vs_` IDs | +| Get vector store | `GET /v1/vector_stores/{id}` | Get metadata for a specific vector store | +| Vector search (OpenAI format) | `POST /v1/vector_stores/{id}/search` | Search with plain text, embedding handled server-side | +| Write features / memory | `write-to-online-store` | Persist agent state, update features | + +`POST /retrieve-online-documents` remains available as a deprecated alias for `POST /search`. + +That last row is what this post is about. Before it existed, agents could read structured features and write state back, but they couldn't search vectors without help from glue code. + +## What this is, and what it isn't + +This makes Feast's vector search speak OpenAI's protocol. It doesn't turn Feast into a general purpose OpenAI-compatible vector database. + +| Works today | Not yet | +|---|---| +| `GET /v1/vector_stores` (list) | Creating vector stores via the API | +| `GET /v1/vector_stores/{id}` (get) | | +| `POST /v1/vector_stores/{id}/search` | | +| Plain text queries with server-side embedding | Client-provided embedding vectors on this endpoint | +| OpenAI-format filters (string, numeric, compound) | `ranking_options.score_threshold`, `ranking_options.ranker`, `rewrite_query: true` (rejected with 422) | +| All Feast online store backends | Standalone `/v1/embeddings` endpoint | + +Feature views are still defined in Python and managed through `feast apply`. Data is still ingested through Feast's existing write paths. The OpenAI-compatible layer is a read API that gives standard access to what's already in your feature store. + +## Deploying on Kubernetes + +Below is an example Kubernetes setup that deploys the feature server with Sentence Transformers for local embedding: + +```yaml +# configmap.yaml (embedding model section) +embedding_model: + provider: sentence_transformers + model: all-MiniLM-L6-v2 +``` + +```yaml +# deployment.yaml +containers: + - name: feast-server + command: ["feast", "serve", "-h", "0.0.0.0", "-p", "6566"] + ports: + - containerPort: 6566 +``` + +With this setup, embedding happens in-cluster. Nothing leaves your network. + +## Try it yourself + +```bash +# Install Feast with Sentence Transformers support +pip install feast sentence-transformers +``` + +Configure your `feature_store.yaml` with an `embedding_model` section, define a feature view with vector search enabled, run `feast apply`, load your data, start the server with `feast serve`, and search: + +```bash +# Discover your vector store IDs +curl -s http://localhost:6566/v1/vector_stores | python -m json.tool + +# Search using the vs_ identifier from the list response +curl -s http://localhost:6566/v1/vector_stores/YOUR_VS_ID/search \ + -H "Content-Type: application/json" \ + -d '{"query": "your search query", "max_num_results": 5}' | python -m json.tool +``` + +## What's next + +Next on the list: wiring up `ranking_options` and `rewrite_query` so they actually do something (right now they're accepted but ignored). We also want a standalone `/v1/embeddings` endpoint for clients that just need embeddings, and eventually the ability to create feature views through the OpenAI vector store API instead of requiring Python + `feast apply`. + +## Join the conversation + +If you're using this or have thoughts on what the OpenAI-compatible layer should support next, come find us on [Slack](https://slack.feast.dev/) or [GitHub](https://github.com/feast-dev/feast). diff --git a/infra/website/docs/blog/feast-openlineage-integration.md b/infra/website/docs/blog/feast-openlineage-integration.md new file mode 100644 index 00000000000..29bed9833a6 --- /dev/null +++ b/infra/website/docs/blog/feast-openlineage-integration.md @@ -0,0 +1,215 @@ +--- +title: Tracking Feature Lineage with OpenLineage +description: Feast now supports native OpenLineage integration for automatic data lineage tracking of your ML features - no code changes required. +date: 2026-01-29 +authors: ["Nikhil Kathole", "Francisco Javier Arceo"] +--- + +
+ Feast OpenLineage Integration - Marquez UI +
+ +# Tracking Feature Lineage with OpenLineage 🔗 + +# Feast's Native Lineage Support + +Feast already provides **built-in lineage tracking** through its native UI. When you explore your feature store in the Feast UI, you can visualize relationships between data sources, entities, feature views, and feature services—all without any additional configuration. + +
+ Feast Native Lineage UI +
+ +This native lineage view shows: +- How **Data Sources** feed into **Feature Views** +- Which **Entities** are associated with each **Feature View** +- How **Feature Views** compose **Feature Services** + +# Why OpenLineage? Lineage Across Your ML Ecosystem + +While Feast's native lineage is powerful for understanding your feature store, modern ML systems span many tools—data pipelines, training jobs, model registries, and serving infrastructure. **OpenLineage** is the open standard that connects lineage across all these systems. + +We are excited to announce that Feast now supports native integration with [OpenLineage](https://openlineage.io/), enabling you to: + +- **Track end-to-end ML lineage**: Connect Feast feature lineage with upstream data pipelines (Airflow, Spark, dbt) and downstream model training +- **Unified visibility**: See your entire ML data flow in a single lineage graph +- **Interoperability**: Use any OpenLineage-compatible tool + +With this integration, Feast automatically tracks and emits lineage events whenever you apply feature definitions or materialize features—**no code changes required**. Simply enable OpenLineage in your `feature_store.yaml`, and Feast handles the rest. + +# Why Data Lineage Matters for Feature Stores + +Feature stores manage the lifecycle of ML features, from raw data sources to model inference. As ML systems grow in complexity, teams often struggle to answer fundamental questions: + +- *Where does this feature's data come from?* +- *Which models depend on this feature view?* +- *What downstream impact will changing this data source have?* +- *How do I audit the data flow for compliance?* + +OpenLineage solves these challenges by providing a standardized way to capture and visualize data lineage. By integrating OpenLineage into Feast, ML teams gain automatic visibility into their feature engineering pipelines without manual instrumentation. + +# How It Works + +The integration automatically emits OpenLineage events for two key operations: + +## Registry Changes (`feast apply`) + +When you run `feast apply`, Feast creates a lineage graph that mirrors what you see in the Feast UI: + +``` +DataSources ──┐ + ├──→ feast_feature_views_{project} ──→ FeatureViews +Entities ─────┘ │ + │ + ▼ + feature_service_{name} ──→ FeatureService +``` + +This creates two types of jobs: +- **`feast_feature_views_{project}`**: Shows how DataSources and Entities flow into FeatureViews +- **`feature_service_{name}`**: Shows which FeatureViews compose each FeatureService + +## Feature Materialization (`feast materialize`) + +When materializing features, Feast emits START, COMPLETE, and FAIL events, allowing you to track: +- Which feature views were materialized +- The time window of materialization +- Success or failure status +- Duration and row counts + +# Getting Started + +## Step 1: Install OpenLineage + +```bash +pip install feast[openlineage] +``` + +## Step 2: Configure Your Feature Store + +Add the `openlineage` section to your `feature_store.yaml`: + +```yaml +project: my_fraud_detection +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db + +openlineage: + enabled: true + transport_type: http + transport_url: http://localhost:5000 + namespace: feast +``` + +## Step 3: Start Marquez (Optional) + +[Marquez](https://marquezproject.ai/) is the reference implementation for OpenLineage and provides a beautiful UI for exploring lineage: + +```bash +docker run -p 5000:5000 -p 3000:3000 marquezproject/marquez +``` + +## Step 4: Apply Your Features + +```python +from feast import FeatureStore + +fs = FeatureStore(repo_path="feature_repo") + +# This automatically emits lineage events! +fs.apply([ + driver_entity, + driver_stats_source, + driver_hourly_stats_view, + driver_stats_service +]) +``` + +Visit http://localhost:3000 to see your lineage graph in Marquez! + +# Rich Metadata Tracking + +The integration doesn't just track relationships—it captures comprehensive metadata about your Feast objects: + +**Feature Views** +- Feature names, types, and descriptions +- TTL (time-to-live) configuration +- Associated entities +- Custom tags +- Online/offline store enablement + +**Feature Services** +- Constituent feature views +- Total feature count +- Service-level descriptions and tags + +**Data Sources** +- Source type (File, BigQuery, Snowflake, etc.) +- Connection URIs +- Timestamp fields +- Field mappings + +All this metadata is attached as OpenLineage facets, making it queryable and explorable in any OpenLineage-compatible tool. + +# Try It Out: Complete Working Example + +We've included a complete working example in the Feast repository that demonstrates the OpenLineage integration end-to-end. The example creates a driver statistics feature store and shows how lineage events are automatically emitted. + +**Run the example:** + +```bash +# Start Marquez first +docker run -p 5000:5000 -p 3000:3000 marquezproject/marquez + +# Clone and run the example +cd feast/examples/openlineage-integration +python openlineage_demo.py --url http://localhost:5000 + +# View lineage at http://localhost:3000 +``` + +The example demonstrates: +- Creating entities, data sources, feature views, and feature services +- Automatic lineage emission on `feast apply` +- Materialization tracking with START/COMPLETE events +- Feature retrieval (no lineage events for retrieval operations) + +In Marquez, you'll see the complete lineage graph: +- `driver_stats_source` (DataSource) → `driver_hourly_stats` (FeatureView) +- `driver_id` (Entity) → `driver_hourly_stats` (FeatureView) +- `driver_hourly_stats` (FeatureView) → `driver_stats_service` (FeatureService) + +
+ Feast Lineage Graph in Marquez UI +
+ +Check out the [full example code](https://github.com/feast-dev/feast/tree/master/examples/openlineage-integration) for complete details including feature definitions with descriptions and tags. + +# Benefits for ML Teams + +**Debugging Made Easy** +When a model's predictions degrade, trace back through the lineage to identify which data source or feature transformation changed. + +**Impact Analysis** +Before modifying a data source, understand all downstream feature views and services that will be affected. + +**Compliance & Audit** +Maintain a complete audit trail of data flow for regulatory requirements like GDPR, CCPA, or SOC2. + +**Documentation** +Auto-generated lineage serves as living documentation that stays in sync with your actual feature store configuration. + +**Cross-Team Collaboration** +Data engineers, ML engineers, and data scientists can all view the same lineage graph to understand the feature store structure. + +# How Can I Get Started? + +This integration is available now in the latest version of Feast. To get started: + +1. Check out the [OpenLineage Integration documentation](https://docs.feast.dev/reference/openlineage) +2. Try the [example in the Feast repository](https://github.com/feast-dev/feast/tree/master/examples/openlineage-integration) +3. Join the [Feast Slack](https://slack.feast.dev) to share feedback and ask questions + +We're excited to see how teams use OpenLineage integration to improve their ML operations and welcome feedback from the community! diff --git a/infra/website/docs/blog/feast-oracle-offline-store.md b/infra/website/docs/blog/feast-oracle-offline-store.md new file mode 100644 index 00000000000..e8ce13d81ae --- /dev/null +++ b/infra/website/docs/blog/feast-oracle-offline-store.md @@ -0,0 +1,274 @@ +--- +title: "Feast Meets Oracle: Unlocking Feature Store for Oracle Database Users" +description: Oracle Database is now a fully featured Feast offline store, integrated with Kubernetes-native operators. This enables teams to leverage their existing Oracle infrastructure for scalable, production ML feature engineering. +date: 2026-03-16 +authors: ["Aniket Paluskar", "Srihari Venkataramaiah"] +--- + +
+ Feast and Oracle Database +
+ +# Feast Meets Oracle: Unlocking Feature Store for Oracle Database Users + +## The Problem: Your Data Is Already in Oracle — Why Move It? + +If you work in a Fortune 500 company, chances are your most valuable data lives in Oracle Database. It is the one of the most widely used enterprise database for a reason — decades of battle-tested reliability, performance, and governance have made it the backbone of mission-critical systems across finance, healthcare, telecommunications, government, and retail. + +But here's the friction: when ML teams want to build features for their models, they typically export data *out* of Oracle into some other system — a data lake, a warehouse, a CSV on someone's laptop. That data movement introduces latency, staleness bugs, security blind spots, and an entire class of silent failures that only surface when a model starts degrading in production. + +**What if you didn't have to move your data at all?** + +With Feast's new Oracle offline store support — now fully integrated into the Feast Kubernetes operator — you can define, compute, and serve ML features directly from your existing Oracle infrastructure. No data migration. No pipeline duct tape. No compromises. + +--- + +## What's New + +Oracle Database is now a first-class offline store in Feast, supported across the full stack: + +| Layer | What Changed | +|---|---| +| **Python SDK** | `OracleOfflineStore` and `OracleSource` — a complete offline store implementation built on `ibis-framework[oracle]` | +| **Feast Operator (v1 API)** | `oracle` is a validated persistence type in the `FeatureStore` CRD, with Secret-backed credential management | +| **CRD & Validation** | Kubernetes validates `oracle` at admission time — bad configs are rejected before they ever reach the operator | +| **Type System** | Full Oracle-to-Feast type mapping covering `NUMBER`, `VARCHAR2`, `CLOB`, `BLOB`, `BINARY_FLOAT`, `TIMESTAMP`, and more | +| **Documentation** | Reference docs for the [Oracle offline store](https://docs.feast.dev/reference/offline-stores/oracle) and [Oracle data source](https://docs.feast.dev/reference/data-sources/oracle) | + +This isn't a thin wrapper or a partial integration. The Oracle offline store supports the complete Feast offline store interface: + +- **`get_historical_features`** — point-in-time correct feature retrieval for training datasets, preventing future data leakage +- **`pull_latest_from_table_or_query`** — fetch the most recent feature values +- **`pull_all_from_table_or_query`** — full table scans for batch processing +- **`offline_write_batch`** — write feature data back to Oracle +- **`write_logged_features`** — persist logged features for monitoring and debugging + +--- + +## Why This Matters: Oracle Is Where the Enterprise Lives + +Oracle Database isn't just another backend option. It is the database that runs the world's banks, hospitals, supply chains, and telecom networks. When we say "number one enterprise database," we mean it in terms of: + +- **Installed base** — More Fortune 100 companies run Oracle than any other database +- **Data gravity** — Petabytes of the world's most regulated, most valuable data already sits in Oracle +- **Operational maturity** — Decades of enterprise features: partitioning, RAC, Data Guard, Advanced Security, Audit Vault + +For ML teams in these organizations, the path to production has always involved a painful detour: extract data from Oracle, load it somewhere else, build features there, then figure out how to serve them. Every step in that chain is a potential point of failure, a security review, and a compliance headache. + +Feast's Oracle integration eliminates the detour entirely. Your features are computed where your data already has governance, backup, encryption, and access controls in place. + +--- + +## How It Works: From Oracle Table to Production Features + +### Step 1: Configure your feature store + +Point Feast at your Oracle database in `feature_store.yaml`: + +```yaml +project: my_project +registry: data/registry.db +provider: local +offline_store: + type: oracle + host: oracle-db.example.com + port: 1521 + user: feast_user + password: ${DB_PASSWORD} + service_name: ORCL +online_store: + path: data/online_store.db +``` + +Feast supports three Oracle connection modes — `service_name`, `sid`, or `dsn` — so it fits however your DBA has set things up: + +```yaml +# Using SID +offline_store: + type: oracle + host: oracle-db.example.com + port: 1521 + user: feast_user + password: ${DB_PASSWORD} + sid: ORCL + +# Using full DSN +offline_store: + type: oracle + host: oracle-db.example.com + port: 1521 + user: feast_user + password: ${DB_PASSWORD} + dsn: "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=oracle-db.example.com)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL)))" +``` + +### Step 2: Define features backed by Oracle tables + +```python +from feast import FeatureView, Field, Entity +from feast.types import Float64, Int64 +from feast.infra.offline_stores.contrib.oracle_offline_store.oracle_source import OracleSource +from datetime import timedelta + +customer = Entity(name="customer_id", join_keys=["customer_id"]) + +customer_transactions = OracleSource( + name="customer_txn_source", + table_ref="ANALYTICS.CUSTOMER_TRANSACTIONS", + event_timestamp_column="TXN_TIMESTAMP", +) + +customer_features = FeatureView( + name="customer_transaction_features", + entities=[customer], + ttl=timedelta(days=30), + schema=[ + Field(name="avg_txn_amount_30d", dtype=Float64), + Field(name="txn_count_7d", dtype=Int64), + Field(name="max_txn_amount_90d", dtype=Float64), + ], + source=customer_transactions, +) +``` + +### Step 3: Retrieve features for training + +```python +from feast import FeatureStore +import pandas as pd + +store = FeatureStore(repo_path=".") + +entity_df = pd.DataFrame({ + "customer_id": [101, 102, 103, 104], + "event_timestamp": pd.to_datetime(["2026-01-15", "2026-01-16", "2026-02-01", "2026-02-15"]), +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "customer_transaction_features:avg_txn_amount_30d", + "customer_transaction_features:txn_count_7d", + "customer_transaction_features:max_txn_amount_90d", + ], +).to_df() +``` + +Feast performs point-in-time correct joins against Oracle — no future data leaks into your training set, and the query runs *inside* Oracle, not in some external compute engine. + +### Step 4: Serve features in production + +```python +store.materialize_incremental(end_date=datetime.utcnow()) + +features = store.get_online_features( + features=[ + "customer_transaction_features:avg_txn_amount_30d", + "customer_transaction_features:txn_count_7d", + ], + entity_rows=[{"customer_id": 101}], +).to_dict() +``` + +--- + +## Kubernetes-Native: The Feast Operator and Oracle + +For teams running Feast on Kubernetes, the Feast operator now natively manages Oracle-backed feature stores through the `FeatureStore` custom resource. + +### Create a Secret with your Oracle credentials + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: oracle-offline-store +type: Opaque +stringData: + oracle: | + host: oracle-db.example.com + port: "1521" + user: feast_user + password: changeme + service_name: ORCL +``` + +### Define a FeatureStore custom resource + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: production-feature-store +spec: + services: + offlineStore: + persistence: + store: + type: oracle + secretRef: + name: oracle-offline-store +``` + +### Apply and let the operator do the rest + +```bash +kubectl apply -f feature-store.yaml +``` + +The operator validates the configuration against the CRD schema (rejecting invalid types at admission), reads the Secret, merges the Oracle connection parameters into the generated Feast config, and deploys the offline store service. Credential rotation, version upgrades, and config changes are all handled through Kubernetes-native reconciliation — the same operational model your platform team already knows. + +Because credentials live in Kubernetes Secrets, they integrate naturally with external secret managers like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault through standard Kubernetes mechanisms. Oracle credentials never appear in plain text in your manifests or CI/CD logs. + +--- + +## Real-World Use Cases + +### Financial Services: Fraud Detection Without Data Movement + +A global bank running Oracle for core banking can now build fraud detection features — transaction velocity, merchant category patterns, geographic anomaly scores — directly from their existing Oracle tables. The features stay within the same security perimeter, audit trail, and encryption boundary as the source data. No ETL pipeline to a secondary warehouse means no replication lag and no additional attack surface. + +### Healthcare: Predictive Models on Regulated Data + +Hospitals and insurers with patient data in Oracle can compute ML features (readmission risk scores, treatment outcome signals, resource utilization patterns) without copying PHI into a less governed system. Feast's feature definitions become the documented lineage trail that compliance teams need. + +### Telecommunications: Network Optimization at Scale + +Telcos managing billions of CDRs and network metrics in Oracle can build churn prediction, capacity forecasting, and service quality features on top of the data they already have — avoiding the cost and latency of replicating to a separate analytical platform. + +### Retail: Demand Forecasting from Point-of-Sale Data + +Retailers with Oracle-backed inventory and transaction systems can build demand forecasting and recommendation features without standing up a parallel data infrastructure. Features computed in Oracle can be materialized to the online store for real-time serving at the edge. + +--- + +## Under the Hood: Built on ibis + +The Oracle offline store is built on the [ibis framework](https://ibis-project.org/), a portable Python dataframe API that compiles to native SQL for each backend. This means: + +- **Queries execute inside Oracle** — ibis translates Feast's retrieval operations into Oracle SQL, pushing computation to where the data lives +- **No intermediate data movement** — results are streamed back as Arrow tables without staging in a temporary system +- **Full Oracle type fidelity** — the type mapping covers the complete spectrum of Oracle data types, including `NUMBER`, `VARCHAR2`, `NVARCHAR2`, `CHAR`, `CLOB`, `NCLOB`, `BLOB`, `RAW`, `BINARY_FLOAT`, `BINARY_DOUBLE`, `DATE`, `TIMESTAMP`, `INTEGER`, `SMALLINT`, and `FLOAT` +- **Automatic DATE-to-TIMESTAMP casting** — Oracle's `DATE` type (which includes time components, unlike SQL standard) is properly handled + +--- + +## Getting Started + +Install Feast with Oracle support: + +```bash +pip install 'feast[oracle]' +``` + +For Kubernetes deployments, ensure you're running Feast operator v0.61.0+ with the v1 API. + +The full configuration reference, functionality matrix, and data source documentation are available in the Feast docs: + +- [Oracle Offline Store Reference](https://docs.feast.dev/reference/offline-stores/oracle) +- [Oracle Data Source Reference](https://docs.feast.dev/reference/data-sources/oracle) +- [Feast Operator Documentation](https://docs.feast.dev/) + +--- + +*Get started with the [Feast documentation](https://docs.feast.dev/) and join the community on [GitHub](https://github.com/feast-dev/feast) and [Slack](https://feastopensource.slack.com/). We'd love to hear how you're using Feast with Oracle.* diff --git a/infra/website/docs/blog/feast-ray-llm-posttrain.md b/infra/website/docs/blog/feast-ray-llm-posttrain.md new file mode 100644 index 00000000000..4b5eac30214 --- /dev/null +++ b/infra/website/docs/blog/feast-ray-llm-posttrain.md @@ -0,0 +1,304 @@ +--- +title: "How to Use Feast for SLM/LLM Post-Training with Ray" +description: "Keep conversation features in Feast, retrieve them for training, then stream into your trainer with Ray." +date: 2026-07-14 +authors: ["Chaitanya Patel"] +--- + +# How to Use Feast for SLM/LLM Post-Training with Ray + +Your support bot answers a lot of tickets. It’s fine—but it sounds generic. The team wants a smaller model that talks more like *your* agents: your refund wording, your product names, your tone. + +So someone says: **fine-tune on our real chats.** + +That part sounds easy. The messy part is the data—exports, notebook cleaning, and prompt formatting scattered across training scripts. + +This post walks through the [ray-llm-posttrain example](https://github.com/feast-dev/feast/tree/master/examples/ray-llm-posttrain): + +1. Put conversation features in Feast +2. Retrieve them with `get_historical_features` (entity-less date range) +3. Get rows into your trainer — stream with Ray **or** materialize with `.to_df()` + +You bring your own trainer. GPT-2 in the script is optional smoke only. + +## What’s in the example + +| Name | Type | What it holds | +|---|---|---| +| `web_documents` | [FeatureView](https://docs.feast.dev/getting-started/concepts/feature-view) | `human`, `bot`, `human_repeat_ratio`, `bot_repeat_ratio` | +| `train_example` | [OnDemandFeatureView](https://docs.feast.dev/reference/beta-on-demand-feature-view) | `cleaned_human`, `cleaned_bot`, `char_count`, `is_trainable`, `sft_text` | +| `llm_posttrain` | FeatureService | Bundles `web_documents` + `train_example` | + +Full definitions live in [feature_definitions.py](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/feature_repo/feature_definitions.py). Ray is the [offline store](https://docs.feast.dev/reference/offline-stores/ray) and one way to stream rows out—not a separate feature catalog. + +This example stays on **supported Feast APIs only** (no core patches). Conversation rows already include `document_id` and `event_timestamp` before Feast reads them. + +## Step 1: Point Feast at conversation data + +### Ray offline store (local) + +From the example [feature_store.yaml](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/feature_repo/feature_store.yaml). Cap Ray resources on a laptop—see [Ray offline store: resource management](https://docs.feast.dev/reference/offline-stores/ray#important-resource-management): + +```yaml +project: ray_llm_posttrain +registry: data/registry.db +provider: local + +offline_store: + type: ray + storage_path: data/ray_storage + enable_ray_logging: false + ray_conf: + num_cpus: 2 + object_store_memory: 104857600 + _memory: 524288000 + +batch_engine: + type: ray.engine + max_workers: 2 + +online_store: + type: sqlite + path: data/online_store.db + +entity_key_serialization_version: 3 +auth: + type: no_auth +``` + +You can also start from the built-in template: + +```bash +feast init -t ray my_ray_project +``` + +See the [Ray template / offline store docs](https://docs.feast.dev/reference/offline-stores/ray#quick-start-with-ray-template) and the related blog [Scaling ML with Feast and Ray](/blog/feast-ray-distributed-processing). + +### Demo seed: prepare parquet, then `RaySource` + +[RaySource](https://docs.feast.dev/reference/data-sources/ray) tells Feast how to load data through Ray. Hugging Face is only used in a **prepare script**—not as a live Feast source that invents timestamps at retrieval time. + +`nampdn-ai/tiny-webtext` has no `document_id` / `event_timestamp`. Entity-less retrieval needs those columns on the source. We add them **outside Feast**, write parquet, then point Feast at that file (supported path): + +```bash +PYTHONPATH=../../sdk/python python scripts/prepare_data.py +# → feature_repo/data/tiny_webtext.parquet +``` + +```python +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource + +tiny_web = RaySource( + name="tiny_webtext", + reader_type="parquet", + path="data/tiny_webtext.parquet", + timestamp_field="event_timestamp", +) +``` + +In production you’d skip the HF prepare step and register your real conversation store (warehouse / lake / parquet) that already has join keys and timestamps. + +More reader types are in the [Ray data source reference](https://docs.feast.dev/reference/data-sources/ray#supported-reader_type-values). + +### Feature view + +```python +web_documents = FeatureView( + name="web_documents", + entities=[document], + ttl=timedelta(days=365), + schema=[ + Field(name="human", dtype=String), + Field(name="bot", dtype=String), + Field(name="human_repeat_ratio", dtype=Float64), + Field(name="bot_repeat_ratio", dtype=Float64), + ], + source=tiny_web, + online=False, +) +``` + +### Optional: OnDemandFeatureView for derived training features + +If you want Feast to own `sft_text` / quality gates (same idea as in the [ODFV docs](https://docs.feast.dev/reference/beta-on-demand-feature-view)): + +```python +@on_demand_feature_view( + sources=[web_documents], + schema=[ + Field(name="cleaned_human", dtype=String), + Field(name="cleaned_bot", dtype=String), + Field(name="char_count", dtype=Int64), + Field(name="is_trainable", dtype=Bool), + Field(name="sft_text", dtype=String), + ], + mode="pandas", +) +def train_example(inputs): + cleaned_human = inputs["human"].fillna("").astype(str).str.strip() + cleaned_bot = inputs["bot"].fillna("").astype(str).str.strip() + # ... length + repeat-ratio gate ... + sft_text = ( + "<|im_start|>user\n" + cleaned_human + "<|im_end|>\n" + "<|im_start|>assistant\n" + cleaned_bot + "<|im_end|>" + ) + return pd.DataFrame({...}) +``` + +```python +llm_posttrain = FeatureService( + name="llm_posttrain", + features=[web_documents, train_example], +) +``` + +Apply: + +```bash +cd examples/ray-llm-posttrain/feature_repo +feast apply +``` + +## Step 2: Retrieve for training (entity-less) + +No `entity_df`—just a date window. That pattern is covered in [Historical Features Without Entity IDs](/blog/entity-less-historical-features-retrieval) and the [FAQ](https://docs.feast.dev/getting-started/faq#how-do-i-run-get_historical_features-without-providing-an-entity-dataframe): + +```python +from datetime import datetime, timezone +from feast import FeatureStore + +store = FeatureStore(repo_path="feature_repo") + +job = store.get_historical_features( + features=[ + "web_documents:human", + "web_documents:bot", + "web_documents:human_repeat_ratio", + "web_documents:bot_repeat_ratio", + ], + start_date=datetime(2024, 6, 1, tzinfo=timezone.utc), + end_date=datetime(2024, 7, 1, tzinfo=timezone.utc), +) +``` + +Then choose how you turn that job into training rows. + +## Step 3: Two ways into the trainer + +| Path | ODFV runs? | When to use | +|---|---|---| +| `job.to_ray_dataset()` then preprocess | **No** | Stream FeatureView columns; shape `sft_text` yourself | +| `job.to_df()` / `to_arrow()` | **Yes** | Want `train_example` outputs from Feast | + +Pick **Option A** when you want full control over text formatting or need custom preprocessing (e.g., multi-turn chat templates, tokenization-aware truncation). Pick **Option B** when you want Feast to enforce quality gates consistently across training and serving. + +### Option A — Stream with Ray, preprocess yourself + +`to_ray_dataset()` returns a Ray Dataset of retrieved FeatureView columns. It does **not** apply OnDemandFeatureViews. Build training text with Ray `map_batches` (as in [train_sft.py](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/scripts/train_sft.py)): + +```python +ds = job.to_ray_dataset() + +def preprocess_sft(batch): + import pandas as pd + + if not isinstance(batch, pd.DataFrame): + batch = pd.DataFrame(batch) + human = batch["human"].fillna("").astype(str).str.strip() + bot = batch["bot"].fillna("").astype(str).str.strip() + ok = bot.str.len() >= 64 + sft_text = ( + "<|im_start|>user\n" + human + "<|im_end|>\n" + "<|im_start|>assistant\n" + bot + "<|im_end|>" + ) + return pd.DataFrame({"sft_text": sft_text}).loc[ok].reset_index(drop=True) + +train_ds = ds.map_batches(preprocess_sft, batch_format="pandas") +# → hand train_ds to your SLM/LLM trainer +``` + +Run the example default path: + +```bash +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run +``` + +### Option B — Use the ODFV, then train + +Materialize with `.to_df()` so `train_example` runs (same retrieval/serving idea as in the [ODFV overview](https://docs.feast.dev/reference/beta-on-demand-feature-view#why-use-on-demand-feature-views)): + +```python +df = store.get_historical_features( + features=store.get_feature_service("llm_posttrain"), + start_date=datetime(2024, 6, 1, tzinfo=timezone.utc), + end_date=datetime(2024, 7, 1, tzinfo=timezone.utc), +).to_df() + +trainable = df[df["is_trainable"] & df["sft_text"].astype(str).str.len().gt(0)] +# trainable["sft_text"] → your trainer +# or: import ray; ray.data.from_pandas(trainable[["sft_text"]]) +``` + +```bash +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df +``` + +### The same ODFV at serving time + +The `train_example` ODFV runs identically during online serving — the quality gate and formatting logic stay in one place: + +```python +# At inference time, the same ODFV runs on the fly +features = store.get_online_features( + features=["train_example:sft_text", "train_example:is_trainable"], + entity_rows=[{"document_id": "doc_42"}], +).to_dict() +# features["sft_text"], features["is_trainable"] — same logic as training +``` + +## Try the full example + +```bash +cd examples/ray-llm-posttrain +uv pip install -e "../../sdk/python[ray]" -r requirements.txt +PYTHONPATH=../../sdk/python python scripts/prepare_data.py +cd feature_repo && feast apply && cd .. + +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run +PYTHONPATH=../../sdk/python python scripts/train_sft.py --dry-run --via-df + +# optional GPT-2 smoke +PYTHONPATH=../../sdk/python python scripts/train_sft.py --max-steps 20 +``` + +Details: [ray-llm-posttrain README](https://github.com/feast-dev/feast/tree/master/examples/ray-llm-posttrain). + +## Takeaways + +1. **Keep conversation features in Feast** — this example’s `web_documents`. +2. **Stream with Ray** — `to_ray_dataset()`, then preprocess training text yourself. +3. **Want ODFVs** — `.to_df()` / `.to_arrow()` to materialize, then train. +4. **Bring your own trainer** — GPT-2 in the example is optional. + +## References + +**This example** + +- [ray-llm-posttrain example](https://github.com/feast-dev/feast/tree/master/examples/ray-llm-posttrain) +- [feature_definitions.py](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/feature_repo/feature_definitions.py) +- [train_sft.py](https://github.com/feast-dev/feast/blob/master/examples/ray-llm-posttrain/scripts/train_sft.py) + +**Docs** + +- [Ray offline store](https://docs.feast.dev/reference/offline-stores/ray) +- [Ray data source](https://docs.feast.dev/reference/data-sources/ray) +- [Ray compute engine](https://docs.feast.dev/reference/compute-engine/ray) +- [On demand feature views](https://docs.feast.dev/reference/beta-on-demand-feature-view) +- [Feature retrieval](https://docs.feast.dev/getting-started/concepts/feature-retrieval) +- [FAQ: historical features without entity dataframe](https://docs.feast.dev/getting-started/faq#how-do-i-run-get_historical_features-without-providing-an-entity-dataframe) + +**Related blogs & tutorials** + +- [Historical Features Without Entity IDs](/blog/entity-less-historical-features-retrieval) +- [Scaling ML with Feast and Ray](/blog/feast-ray-distributed-processing) +- [Validating historical features](https://docs.feast.dev/tutorials/validating-historical-features) diff --git a/infra/website/docs/blog/feast-unity-catalog-integration.md b/infra/website/docs/blog/feast-unity-catalog-integration.md new file mode 100644 index 00000000000..1ee977a71eb --- /dev/null +++ b/infra/website/docs/blog/feast-unity-catalog-integration.md @@ -0,0 +1,342 @@ +--- +title: "Feast Gets Native Apache Iceberg Support" +description: "Feast now reads features from any Iceberg catalog — REST, SQL, Hive, Glue, DynamoDB. Connect to Unity Catalog, Apache Polaris, Nessie, or your own PyIceberg catalog. Full support for get_historical_features, materialize, and online serving." +date: 2026-07-18 +authors: ["Nikhil Kathole"] +--- + +# Feast Gets Native Apache Iceberg Support + +Apache Iceberg has become the open table format. Your data lake is probably already on it — whether through Databricks, Snowflake, AWS, or self-managed infrastructure. But until now, connecting Feast to Iceberg tables meant either going through Spark (heavyweight, slow to start) or copying data into Feast-managed Parquet files (data duplication, governance gap). + +Feast now ships a native `IcebergSource` that reads directly from any Iceberg catalog. No data copies. Your feature tables live where they already live — in your Iceberg catalog — and Feast reads from them via PyIceberg. With the DuckDB offline store, you don't even need a Spark cluster — reads happen entirely in-process. + +## Why This Matters + +Before this, the path from "data in Iceberg" to "features in Feast" looked like this: + +1. Data engineers build Iceberg tables in their catalog (UC, Glue, Hive) +2. ML engineers copy data to Feast-managed Parquet files, or configure a SparkSource that couples them to a specific compute engine +3. Two copies of the data. Two metadata systems. No connection between them. + +Now the path is: + +1. Data engineers build Iceberg tables in their catalog +2. ML engineers point `IcebergSource` at the table +3. Done. Feast reads directly from the catalog via PyIceberg. One copy. One source of truth. Choose DuckDB for lightweight local reads or Spark when you need distributed compute — the data source definition stays the same either way. + +## What You Get + +### Any Iceberg Catalog + +`IcebergSource` supports every catalog backend that PyIceberg supports: + +| `catalog_type` | Backend | Example Use Case | +|---|---|---| +| `"rest"` | Iceberg REST Catalog | Databricks Unity Catalog, Apache Polaris, Project Nessie, Snowflake Open Catalog | +| `"sql"` | SQL-backed catalog | Local dev with SQLite, CI/CD, PostgreSQL-backed catalogs | +| `"hive"` | Hive Metastore | On-premise Hadoop, EMR | +| `"glue"` | AWS Glue Data Catalog | AWS-native lakehouse | +| `"dynamodb"` | DynamoDB catalog | Serverless AWS | + +### Both Offline Stores + +| Operation | DuckDB | Spark | +|---|---|---| +| `feast apply` | Yes | Yes | +| `get_historical_features` | Yes | Yes | +| `materialize` / `materialize-incremental` | Yes | Yes | +| `get_online_features` | Yes | Yes | + +Both offline stores use PyIceberg for the actual Iceberg table scan — the difference is what happens after. DuckDB processes the Arrow table in-process (no JVM, no cluster), making it ideal for local development and moderate-scale workloads. Spark is there when you need distributed compute over large datasets. The same `IcebergSource` definition works with either offline store — just change `offline_store.type` in your YAML. + +### Full Iceberg Semantics + +Every read goes through PyIceberg's `table.scan().to_arrow()`. This means you get proper Iceberg semantics: schema evolution, partition pruning, and snapshot isolation — not just raw Parquet file reads. + +## Quick Start + +### Install + +```bash +pip install "feast[iceberg]" +``` + +### Define a Source + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import IcebergSource + +driver_stats = IcebergSource( + warehouse="my_catalog", + namespace="ml_features", + table="driver_hourly_stats", + catalog_type="rest", + endpoint="https://my-iceberg-catalog.example.com", + token_env_var="CATALOG_TOKEN", + timestamp_field="event_timestamp", +) +``` + +### Use It + +```python +from datetime import timedelta +from feast import Entity, FeatureView, Field +from feast.types import Float64, Int64 + +driver = Entity(name="driver", join_keys=["driver_id"]) + +driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=365), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float64), + Field(name="acc_rate", dtype=Float64), + Field(name="avg_daily_trips", dtype=Int64), + ], + source=driver_stats, + online=True, +) +``` + +```yaml +# feature_store.yaml +project: my_project +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db +offline_store: + type: duckdb +``` + +```bash +feast apply +``` + +Then use it like any other Feast source: + +```python +from feast import FeatureStore +import pandas as pd +from datetime import datetime, timezone + +store = FeatureStore(repo_path=".") + +# Training data +training_df = store.get_historical_features( + entity_df=pd.DataFrame({ + "driver_id": [1001, 1002, 1003], + "event_timestamp": [datetime(2026, 7, 1, tzinfo=timezone.utc)] * 3, + }), + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], +).to_df() + +# Materialize to online store +store.materialize_incremental(end_date=datetime.now(tz=timezone.utc)) + +# Online serving +online = store.get_online_features( + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], + entity_rows=[{"driver_id": 1001}], +).to_dict() +``` + +## Catalog Examples + +### AWS Glue + +```python +glue_source = IcebergSource( + warehouse="my_glue_database", + namespace="ml_features", + table="driver_stats", + catalog_type="glue", + catalog_properties={"region_name": "us-east-1"}, + timestamp_field="event_timestamp", +) +``` + +### Hive Metastore + +```python +hive_source = IcebergSource( + endpoint="thrift://hive-metastore:9083", + warehouse="warehouse", + namespace="features", + table="driver_stats", + catalog_type="hive", + timestamp_field="event_timestamp", +) +``` + +### Apache Polaris / Nessie + +```python +polaris_source = IcebergSource( + endpoint="https://polaris.example.com", + warehouse="my_catalog", + namespace="ml", + table="features", + catalog_type="rest", + token_env_var="POLARIS_TOKEN", + timestamp_field="event_timestamp", +) +``` + +### Local Development (SQLite-backed) + +For development and CI/CD, use a local PyIceberg SQL catalog — no external service required: + +```python +local_source = IcebergSource( + warehouse="dev_warehouse", + namespace="default", + table="driver_stats", + catalog_type="sql", + catalog_name="dev_catalog", + catalog_properties={ + "uri": "sqlite:////tmp/iceberg_catalog.db", + "warehouse": "file:///tmp/iceberg_warehouse", + }, + timestamp_field="event_timestamp", +) +``` + +## Use Case: Unity Catalog Integration + +Unity Catalog users get everything above with simpler configuration. `UnityCatalogSource` extends `IcebergSource` with UC-specific defaults: + +- Default connection via `DATABRICKS_HOST` and `DATABRICKS_TOKEN` environment variables — no manual endpoint or token setup +- Three-level naming (`warehouse.namespace.table`) maps directly to UC's catalog/schema/table hierarchy + +### Databricks Setup + +```bash +export DATABRICKS_HOST="https://your-workspace.cloud.databricks.com" +export DATABRICKS_TOKEN="dapi_your_token_here" +``` + +```python +from feast.infra.data_sources.contrib.iceberg_catalog import UnityCatalogSource + +driver_stats_source = UnityCatalogSource( + warehouse="ml_catalog", + namespace="driver_features", + table="driver_hourly_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created", + description="Hourly aggregated driver statistics", +) +``` + +That's it. No endpoint or token parameters needed — they come from the environment variables. + +### Optional: Governance Metadata Sync + +If you want `feast apply` to annotate your UC tables with `feast.*` properties (project name, feature view, primary keys, owner), you can use the `UnityCatalogProvider`: + +```yaml +# feature_store.yaml +provider: unity_catalog +``` + +This is optional. For most users, `provider: local` is sufficient. Reads, materialization, and online serving work the same regardless of which provider you use. + +### OSS Unity Catalog + +The integration also works with the open-source Unity Catalog, with some differences: + +| Capability | Databricks UC | OSS UC | +|---|---|---| +| Read via Iceberg REST | Built-in | Requires `uniform_iceberg_metadata_location` in H2 DB | +| Read via SQL catalog | Yes | Yes | +| Credential vending | Yes | Not available | + +For OSS UC, set `credential_vending=False` and `token_env_var=None`: + +```python +source = UnityCatalogSource( + warehouse="unity", + namespace="default", + table="driver_hourly_stats", + endpoint="http://localhost:8080/api/2.1/unity-catalog/iceberg", + token_env_var=None, + credential_vending=False, + catalog_type="sql", # recommended for OSS UC + catalog_name="my_catalog", + catalog_properties={ + "uri": "sqlite:////tmp/pyiceberg_catalog.db", + "warehouse": "file:///tmp/warehouse", + }, + timestamp_field="event_timestamp", + register_as_feature_table=False, +) +``` + +## How It Works + +### Read Path + +All data reads go through PyIceberg, regardless of catalog type: + +``` +IcebergSource.get_catalog_client() + → PyIceberg catalog (REST / SQL / Hive / Glue / DynamoDB) + → table.scan().to_arrow() + → Arrow table + → DuckDB ibis memtable or Spark DataFrame +``` + +If the catalog is misconfigured, you get a clear error — consistent with how every other Feast data source works. + +### Catalog Name Isolation + +Each source has a `catalog_name` parameter (default: `"feast_iceberg"`). This is the instance name PyIceberg uses when loading the catalog. If you have multiple sources pointing at different catalogs, use different names to avoid collisions: + +```python +production = IcebergSource(catalog_name="prod_catalog", ...) +staging = IcebergSource(catalog_name="staging_catalog", ...) +``` + +## What's Not Supported + +- **Write-back to Iceberg/UC tables.** Feast reads from existing tables; it doesn't write feature data back. The `write_to_offline_store` API only supports `FileSource` (DuckDB) and `SparkSource` (Spark). Your data engineering pipelines create and populate the tables. +- **Table creation.** Tables must exist before Feast can read from them. `feast apply` registers the feature view in Feast's registry, not the table in the catalog. + +## Configuration Reference + +### IcebergSource + +| Parameter | Default | Description | +|---|---|---| +| `warehouse` | *required* | Catalog or warehouse name | +| `namespace` | *required* | Schema or namespace | +| `table` | *required* | Table name | +| `catalog_type` | `"rest"` | Backend: `"rest"`, `"sql"`, `"hive"`, `"glue"`, `"dynamodb"` | +| `catalog_name` | `"feast_iceberg"` | PyIceberg instance name (unique per catalog to avoid collisions) | +| `endpoint` | `None` | Catalog endpoint URL | +| `catalog_properties` | `{}` | Additional catalog config (e.g., `{"uri": "sqlite:///..."}`) | +| `token_env_var` | `None` | Env var containing auth token | +| `credential_vending` | `True` | Request scoped storage credentials | +| `timestamp_field` | `None` | Event timestamp column | +| `created_timestamp_column` | `None` | Creation timestamp for deduplication | + +### UnityCatalogSource (extends IcebergSource) + +All `IcebergSource` parameters plus: + +| Parameter | Default | Description | +|---|---|---| +| `endpoint` | From `DATABRICKS_HOST` | Defaults to `{DATABRICKS_HOST}/api/2.1/unity-catalog/iceberg` | +| `token_env_var` | `"DATABRICKS_TOKEN"` | Defaults to Databricks token env var | +| `register_as_feature_table` | `True` | Sync `feast.*` properties to UC on `feast apply` | +| `sync_lineage` | `True` | Record lineage in UC (Databricks only) | + +--- + +*Native Iceberg support is available in Feast 0.64+. Install with `pip install "feast[iceberg]"` and check the [Iceberg data source documentation](/reference/data-sources/iceberg) for the full API reference.* diff --git a/infra/website/docs/blog/feature-transformation-latency.md b/infra/website/docs/blog/feature-transformation-latency.md new file mode 100644 index 00000000000..5fdce45f72a --- /dev/null +++ b/infra/website/docs/blog/feature-transformation-latency.md @@ -0,0 +1,79 @@ +--- +title: "Faster On Demand Transformations in Feast 🏎️💨" +description: "Discover how Feast's new Native Python Mode and transformations on writes reduce feature transformation latency by up to 10x, enabling faster and more efficient feature engineering workflows." +date: 2025-01-14 +authors: ["Francisco Javier Arceo", "Shuchu Han"] +--- + +
+ Pandas vs Native Python Latency Decomposition +
+ +# Faster On Demand Transformations in Feast 🏎️💨 + +*Thank you to Shuchu Han and Francisco Javier Arceo for their contributions [evaluating the latency](https://github.com/feast-dev/feast/issues/4207) of transformation for On Demand Feature Views and adding [transformations on writes to On Demand Feature Views](https://github.com/feast-dev/feast/issues/4376). Thank you also to Maks Stachowiak, Ross Briden, Ankit Nadig, and the folks at Affirm for inspiring this work and creating an initial proof of concept.* + +Feature engineering is at the core of building high-performance machine learning models. The Feast team has introduced two major enhancements to [On Demand Feature Views](https://docs.feast.dev/reference/beta-on-demand-feature-view) (ODFVs), pushing the boundaries of efficiency and flexibility for data scientists and engineers. Here's a closer look at these exciting updates: + +## 1. Turbocharging Transformations with Native Python Mode + +Traditionally, transformations in ODFVs were limited to Pandas-based operations. While powerful, Pandas transformations can be computationally expensive for certain use cases. Feast now introduces **Native Python Mode**, a feature that allows users to write transformations using pure Python. + +Key benefits of Native Python Mode include: + +* **Blazing Speed:** Transformations using Native Python are nearly **10x faster** compared to Pandas for many operations. +* **Intuitive Design:** This mode supports list-based and singleton (row-level) transformations, making it easier for data scientists to think in terms of individual rows rather than entire datasets. +* **Versatility:** Users can now switch between batch and singleton transformations effortlessly, catering to both historical and online retrieval scenarios. + +Here's an example of using Native Python Mode for singleton transformations: + +```python +@on_demand_feature_view( + sources=[driver_hourly_stats_view, input_request], + schema=[Field(name="conv_rate_plus_acc_singleton", dtype=Float64)], + mode="python", + singleton=True, +) +def transformed_conv_rate_singleton(inputs: Dict[str, Any]) -> Dict[str, Any]: + return {"conv_rate_plus_acc_singleton": inputs["conv_rate"] + inputs["acc_rate"]} +``` + +This approach aligns with how many data scientists naturally process data, simplifying the implementation of feature engineering workflows. + +## 2. Transformations on Writes: A New Dimension of Latency Optimization + +Until now, ODFVs operated solely as **transformations on reads**, applying logic during online feature retrieval. While this ensured flexibility, it sometimes came at the cost of increased latency during retrieval. Feast now supports **transformations on writes**, enabling users to apply transformations during data ingestion and store the transformed features in the online store. + +Why does this matter? + +* **Reduced Online Latency:** With transformations pre-applied at ingestion, online retrieval becomes a straightforward lookup, significantly improving performance for latency-sensitive applications. +* **Operational Flexibility:** By toggling the `write_to_online_store` parameter, users can choose whether transformations should occur at write time (to optimize reads) or at read time (to preserve data freshness). + +Here's an example of applying transformations during ingestion: + +```python +@on_demand_feature_view( + sources=[driver_hourly_stats_view], + schema=[Field(name="conv_rate_adjusted", dtype=Float64)], + mode="pandas", + write_to_online_store=True, # Apply transformation during write time +) +def transformed_conv_rate(features_df: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_adjusted"] = features_df["conv_rate"] * 1.1 + return df +``` + +With this new capability, data engineers can optimize online retrieval performance without sacrificing the flexibility of on-demand transformations. + +## The Future of ODFVs and Feature Transformations + +These enhancements bring ODFVs closer to the goal of seamless feature engineering at scale. By combining high-speed Python-based transformations with the ability to optimize retrieval latency, Feast empowers teams to build more efficient, responsive, and production-ready feature pipelines. + +For more detailed examples and use cases, check out the [documentation for On Demand Feature Views](https://docs.feast.dev/reference/beta-on-demand-feature-view). Whether you're a data scientist prototyping features or an engineer optimizing a production system, the new ODFV capabilities offer the tools you need to succeed. + +The future of Feature Transformations in Feast will be to unify feature transformations and feature views to allow for a simpler API. If you have thoughts or interest in giving feedback to the maintainers, feel free to comment directly on [the GitHub Issue](https://github.com/feast-dev/feast/issues/4584) or in [the RFC](https://docs.google.com/document/d/1KXCXcsXq1bUvbSpfhnUjDSsu4HpuUZ5XiZoQyltCkvo/edit?tab=t.0#heading=h.g6j9jyjkf5sm). + +Our goal is to make Feast blazing fast for feature transformation and serving. + +**Are you ready to unlock the full potential of On Demand Feature Views? Update your Feast repository and start building today!** diff --git a/infra/website/docs/blog/feature-view-versioning.md b/infra/website/docs/blog/feature-view-versioning.md new file mode 100644 index 00000000000..574dac54cc1 --- /dev/null +++ b/infra/website/docs/blog/feature-view-versioning.md @@ -0,0 +1,244 @@ +--- +title: Feast Introduces Experimental Feature View Versioning +description: Feast now supports experimental feature view versioning — bringing automatic version tracking, safe rollback, and multi-version online serving to your feature store. Only supported for SQLite today; we're inviting the community to test and give feedback. +date: 2026-03-31 +authors: ["Francisco Javier Arceo"] +--- + +
+ Feast Feature Versioning +
+ +# Feast Introduces Experimental Feature View Versioning 🚀 + +We are excited to announce the experimental release of **Feature View Versioning** in Feast — a [long-requested capability](https://github.com/feast-dev/feast/issues/2728) that brings automatic version tracking, safe rollback, and multi-version online serving to your feature store. + +This feature is still **experimental** and we would love to hear your feedback. Try it out and let us know what works, what doesn't, and which online stores you'd like to see supported next. + +## Why Feature Versioning Matters + +Serving data in production AI applications is one of the hardest problems in ML engineering. The Feast community is built on practitioners who run these high-stakes pipelines every day — where **point-in-time correctness** is not just a nice-to-have but a hard requirement for model integrity. A feature value seen by the wrong model at the wrong time can silently corrupt predictions and, in high stakes scenarios, impact critical business applications. This is the most important motivating factor behind our versioning work. + +Feature versioning solves two distinct but equally real problems: + +### Case 1: Fixing Forward in Production (the critical path) + +A live feature view powering a production model needs to be updated — perhaps a critical bug in a transformation logic, a renamed column, or a changed data source. In an ideal world you'd cut over to a brand-new feature view and update every downstream consumer atomically. In practice that's rarely viable: models are already deployed, consumers are already reading that name, and a rename causes an immediate outage. + +With feature versioning you can overwrite the existing feature view definition while retaining the complete history as a recoverable snapshot. If the change turns out to break something, you can roll back to the previous version. You always have an audit trail: who changed what, when, and what the feature looked like at every prior point in time. + +This case is the hardest to get right. When your feature is live in production, correctness is non-negotiable. The materialized data that feeds real-time predictions must stay consistent with the schema that was active when it was written — otherwise you risk serving stale rows with the wrong columns to models that expect the new schema, or vice versa. Our per-version table design (see below) exists precisely to prevent that class of failure. + +### Case 2: Offline Experimentation Before Production + +During active feature development, multiple data scientists may be building and evaluating competing versions of the same feature simultaneously — different transformations, different data sources, different schemas. Today there is no first-class way to manage this in Feast without creating separate feature views with different names and hoping teams don't collide. + +With versioning, teams can stage a new feature version using `--no-promote`, test it in isolation, and only promote it to the active (default) definition once it has been validated. The rest of the system sees no change until the promotion happens. + +### The Persistent Pain Points + +Both cases expose the same underlying gaps that exist today: + +1. **No audit trail.** Teams struggle to answer "what did this feature view look like last week?" or "when was this schema changed and by whom?" +2. **No safe rollback.** If a schema change breaks a downstream model, the only recourse is to manually reconstruct the old definition — often from memory or version control history. +3. **No multi-version serving.** During a migration, Model A might rely on the old feature schema while Model B needs the new one. Without versioning, serving both simultaneously requires duplicating the entire feature view under a different name. + +## How It Works + +Version tracking is fully automatic. You don't need to change anything about how you write or apply feature views: + +```bash +feast apply # First apply → saved as v0 +# ... edit schema ... +feast apply # Detects change → saved as v1 +feast apply # No change detected → still v1 (idempotent) +# ... edit source or UDF ... +feast apply # Detects change → saved as v2 +``` + +Every time `feast apply` detects a real change to a feature view's schema or transformation logic, it automatically saves a versioned snapshot to the registry. Metadata-only changes (description, tags, TTL) are updated in place without creating a new version. + +### What Gets Versioned + +- Changes to the feature schema (adding, removing, or renaming fields) +- Changes to batch or stream data sources +- Changes to on-demand feature view UDFs +- Any other structural change that affects data layout or derivation + +### What Does Not Trigger a New Version + +- Tag or description updates +- TTL changes +- Re-applying an identical definition (idempotent behavior) + +## Exploring Version History + +You can list all recorded versions of a feature view from the CLI: + +```bash +feast feature-views list-versions driver_stats +``` + +``` +VERSION TYPE CREATED VERSION_ID +v0 feature_view 2026-01-15 10:30:00 a1b2c3d4-... +v1 feature_view 2026-01-16 14:22:00 e5f6g7h8-... +v2 feature_view 2026-01-20 09:15:00 i9j0k1l2-... +``` + +Or programmatically: + +```python +store = FeatureStore(repo_path=".") +versions = store.list_feature_view_versions("driver_stats") +for v in versions: + print(f"{v['version']} created at {v['created_timestamp']}") +``` + +## Multi-Version Online Serving + +When `enable_online_feature_view_versioning: true` is set in your `feature_store.yaml`, you can read features from a specific version using the `@v` syntax: + +```yaml +registry: + path: data/registry.db + enable_online_feature_view_versioning: true +``` + +```python +online_features = store.get_online_features( + features=[ + "driver_stats:trips_today", # latest version (default) + "driver_stats@v1:trips_today", # read from v1 + "driver_stats@v2:avg_rating", # read from v2 + ], + entity_rows=[{"driver_id": 1001}], +) +``` + +Multiple versions can be queried in a single call, making gradual migrations straightforward: keep serving the old version to existing consumers while routing new consumers to the latest. **By default, unversioned requests always resolve to the latest promoted version** — opting into a specific version requires the explicit `@v` syntax. + +### Online Store Table Naming + +Each version owns its own isolated online table (see [The Challenges of Correctness](#the-challenges-of-correctness-in-feature-versioning) below for why this design is necessary for point-in-time correctness): + +- v0 continues to use the existing, unversioned table (e.g., `project_driver_stats`) — fully backward compatible +- v1 and later use suffixed tables (e.g., `project_driver_stats_v1`, `project_driver_stats_v2`) + +Each version requires its own materialization: + +```bash +feast materialize --views driver_stats --version v2 +``` + +## Safe Rollback + +You can pin a feature view to a specific historical version by setting the `version` parameter. `feast apply` will replace the active definition with the stored snapshot: + +```python +# Revert to v1 — restores schema, source, and transformations from the v1 snapshot +driver_stats = FeatureView( + name="driver_stats", + entities=[driver], + schema=[...], + source=my_source, + version="v1", +) +``` + +After running `feast apply`, the active feature view will match the v1 snapshot exactly. Remove the `version` parameter (or set it to `"latest"`) to resume auto-incrementing behavior. + +## Staged Publishing with `--no-promote` + +For breaking schema changes, you may want to publish a new version without immediately making it the default for unversioned consumers. Use the `--no-promote` flag: + +```bash +feast apply --no-promote +``` + +This saves the version snapshot without updating the active definition. Unversioned consumers (`driver_stats:trips_today`) continue reading from the previous version, while opted-in consumers can start using `driver_stats@v2:trips_today` right away. When you're ready to make the new schema the default, run `feast apply` without the flag. + +## The Challenges of Correctness in Feature Versioning + +Feature versioning might sound simple in principle, but **getting it right is surprisingly hard**, especially for materialization. This is particularly acute for Case 1 above — production systems where the consistency of data matters and the cost of a mistake is high. + +### Why We Can't Share a Single Online Table + +The naive approach to versioning would be to keep a single online table and tag rows with a version column. We explicitly rejected this design. + +Each version of a feature view may have a completely different schema — different columns, different types, different derivation logic. A single shared table would require the union of all column schemas across all versions, leading to sparse rows, broken type contracts, and materialization jobs that cannot safely run in parallel. More fundamentally, it makes **point-in-time correctness impossible**: a model trained against v1 of a feature must retrieve v1 rows, not v2 rows that happen to occupy the same table. + +Instead, we give each version its own online table: + +- v0 continues to use the existing, unversioned table (e.g., `project_driver_stats`) — fully backward compatible +- v1 and later use suffixed tables (e.g., `project_driver_stats_v1`, `project_driver_stats_v2`) + +This is more operationally complex — more tables to manage, more materialization jobs to schedule — but it is the **only design that guarantees correctness**. Each version's materialized data is isolated, independently freshed, and independently queryable. A buggy v2 materialization cannot corrupt v1 data. + +### The Core Tension + +Each version of a feature view may have a completely different schema, source, or transformation. This means: + +- Each version needs its own online store table with the right columns +- Materializing one version must not corrupt data in another version's table +- Version-qualified online reads must resolve the snapshot at the right point in time before looking up the online table +- The active (promoted) version and historical snapshots must stay consistent + +### Materialization Complexity + +Today, running `feast materialize` without specifying a version fills the **active** version's table. To populate a historical version's table, you must explicitly pass `--version v` so Feast can reconstruct the schema from the saved snapshot and target the correct online table. + +This matters for correctness: if you apply v2 of a feature view (which drops a column) and then run an unversioned `feast materialize`, the v1 online table is not automatically backfilled or maintained. Teams need to think carefully about which versions they want to keep materialized and for how long. + +### What This Means for Clients + +The multi-table design does introduce a responsibility shift toward the client. By default, unversioned feature requests (`driver_stats:trips_today`) resolve to the **latest promoted version** — no change to existing consumers. But clients that need to pin to a specific version must opt in explicitly using the `@v` syntax (`driver_stats@v1:trips_today`). + +This is an intentional design choice. Automatic version following would hide schema changes from consumers that may not be ready for them. Explicit version pinning keeps the contract between producers and consumers clear and auditable — each consumer controls exactly which version of a feature it reads. + +### Tradeoffs + +| Concern | Tradeoff | +|---|---| +| Storage cost | Each active version requires its own online table — storage scales with the number of versions kept live | +| Operational complexity | Teams must manage materialization schedules per version | +| Consistency windows | Because each version has its own materialization job, two versions of the same feature for the same entity may have different freshness | +| Concurrency | Two simultaneous `feast apply` calls can race on version number assignment — the registry backends use optimistic locking to handle this, but teams should be aware | + +These tradeoffs are real and we're still refining the model based on community experience. We've tried to make the defaults safe (versioning is opt-in for online reads, backward compatible for unversioned access) while giving teams the controls they need. + +## Current Limitations + +This is an experimental feature and there are known gaps: + +- **Online store support** — Version-qualified reads (`@v`) are **SQLite-only** today. We plan to add Redis, DynamoDB, Bigtable, Postgres, and others based on community demand. If you need a specific store, [comment or upvote on the appropriate the child GitHub issue of the main GitHub issue](https://github.com/feast-dev/feast/issues/2728) and let us know. +- **Offline store versioning** — Versioned historical retrieval is not yet supported. +- **Version deletion** — There is no mechanism today to prune old versions from the registry. +- **Feature services** — Feature services always resolve to the active (promoted) version. `--no-promote` versions are not accessible through feature services until promoted. + +## Supported Feature View Types + +Versioning works across all three feature view types: + +- `FeatureView` (and `BatchFeatureView`) +- `StreamFeatureView` +- `OnDemandFeatureView` + +## Getting Started + +1. Upgrade to the latest version of Feast. +2. Add `enable_online_feature_view_versioning: true` to your registry config in `feature_store.yaml` (only needed for versioned online reads). +3. Run `feast apply` as usual — version history tracking starts automatically. +4. Explore your version history with `feast feature-views list-versions `. + +For full details, see the [Feature View Versioning documentation](https://docs.feast.dev/reference/alpha-feature-view-versioning). + +## Share Your Feedback + +We want to hear from you! Try out feature view versioning and tell us: + +- Which online stores you need supported next +- Which workflows feel awkward or incomplete +- How the materialization model fits your real-world pipelines + +Join the conversation on [GitHub](https://github.com/feast-dev/feast/issues) or in the [Feast Slack community](https://slack.feast.dev/). Your feedback directly shapes what we build next. diff --git a/infra/website/docs/blog/mongodb-feast-integration.md b/infra/website/docs/blog/mongodb-feast-integration.md new file mode 100644 index 00000000000..8a9ea4c255b --- /dev/null +++ b/infra/website/docs/blog/mongodb-feast-integration.md @@ -0,0 +1,210 @@ +--- +title: "Native MongoDB Support in Feast: One Database for Operational Data, Features, and Vectors" +description: Feast now ships first-class support for MongoDB as both an online and an offline store, plus native Vector Search for embedding-based retrieval. Machine Learning teams running on MongoDB can serve features at low latency, generate point-in-time-correct training datasets, and power RAG or recommender workloads, all from a single MongoDB Atlas cluster, with no separate cache, no separate warehouse, and no parallel vector database to keep in sync. +date: 2026-05-07 +authors: ["Rishabh Bisht"] +--- + + +
+MongoDB Feast Stores +
+ + +## The three-database problem in production ML + +A typical Feast deployment runs three different databases: + +1. The **application's primary database** where the operational data that features are derived from actually lives. +2. A dedicated **online store** used to serve features at low latency to live models. +3. A **separate warehouse** used as the offline store for training-set generation and historical retrieval. + +That's three sets of credentials, three security postures, three monitoring stacks, and a constant feedback loop of "the feature is in the warehouse but stale in the online store" or "we materialized last night but the model is reading yesterday's values." + +For teams whose operational data already lives in MongoDB, this was especially painful. Until now, Feast had no native MongoDB option so teams either stood up parallel infrastructure they didn't want, or settled for community plugins of varying maturity. + +With this release, both types of the feature store run on MongoDB - same connection string, same auth, same backups, same observability. The features sit next to the operational data they were derived from. + +## What's in the integration + +Three components ship together as generally available: + +### 1. MongoDBOnlineStore - low-latency feature serving + +Available in Feast `v0.61.0` and above. Built on the official PyMongo driver, with both sync and native async paths (the async implementation uses PyMongo's `AsyncMongoClient`). It supports `online_write_batch`, `online_read`, and their async equivalents. + +Features from multiple feature views for the same entity are colocated in a single MongoDB collection keyed by the serialized entity key, so a read for an entity is a single primary-key lookup, not a fan-out across collections. + +### 2. MongoDBOfflineStore - historical retrieval and training-set generation + +Available in `v0.63.0` and above. Uses the MongoDB aggregation framework for retrieval, with `pandas.merge_asof` for the point-in-time join when entities repeat across timestamps. Ships with `MongoDBSource` (the `DataSource` class), `offline_write_batch` for ingest, and `persist` to write joined results to Parquet for downstream training pipelines. + +### 3. MongoDB Vector Search - embeddings as first-class features + +When you set `vector_enabled: true` on the online store, Feast automatically creates and manages MongoDB vector search indexes on any `FeatureView` field marked with `vector_index=True`. The `retrieve_online_documents_v2()` method runs a `$vectorSearch` aggregation under the hood and returns nearest-neighbor results as `(event_ts, entity_key, feature_dict)` tuples with a similarity score - with `top_k` limiting and configurable distance metrics (`cosine`, `dot product`, `euclidean`). + +The result: a team running RAG, recommenders, or agent workloads can store, serve, and similarity-search feature embeddings in the same Atlas cluster as their other features — with no separate vector database to bolt on. + +## **Quick start** + +### Install + +```shell +pip install 'feast[mongodb]' +``` + +### Configure your `feature_store.yaml` + +Point both the online and offline store at the same Atlas cluster. No separate Atlas feature flag or opt-in required. + +```yaml +project: my_feature_repo +registry: data/registry.db +provider: local + +online_store: + type: mongodb + connection_string: "mongodb+srv://:@.mongodb.net" + database: "feast_online" + +offline_store: + type: mongodb + connection_string: "mongodb+srv://:@.mongodb.net" + database: "feast_offline" + +entity_key_serialization_version: 3 +``` + +### Define a feature view backed by `MongoDBSource` + +```py +from datetime import timedelta +from feast import Entity, FeatureView, Field +from feast.types import Float32, Int64 +from feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb_source import ( + MongoDBSource, +) + +driver = Entity(name="driver", join_keys=["driver_id"]) + +driver_stats_source = MongoDBSource( + name="driver_stats_source", + database="feast_offline", + collection="driver_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=7), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, +) +``` + +### Apply, materialize, and serve + +```shell +feast apply +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +```py +from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +features = store.get_online_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + entity_rows=[{"driver_id": 1001}, {"driver_id": 1002}], +).to_dict() +``` + +That's it. Same connection string, same auth model, same cluster - features in, features out. + +## RAG and embeddings: vector search in the same cluster + +If you're building a RAG pipeline, a recommender, or an agent that needs nearest-neighbor lookup over feature embeddings, the online store doubles as a vector store when `vector_enabled` is set: + +```yaml +online_store: + type: mongodb + connection_string: "mongodb+srv://:@.mongodb.net" + database: "feast_online" + vector_enabled: true + vector_index_wait_timeout: 60 + vector_index_wait_poll_interval: 2 +``` + +Mark the embedding field on your `FeatureView`: + +```py +from feast import FeatureView, Field +from feast.types import Array, Float32, Int64, String, UnixTimestamp + +document_embeddings = FeatureView( + name="embedded_documents", + entities=[item], + schema=[ + Field( + name="vector", + dtype=Array(Float32), + vector_index=True, # ← enable vector index + vector_search_metric="COSINE", # cosine | dot product | euclidean + ), + Field(name="item_id", dtype=Int64), + Field(name="sentence_chunks", dtype=String), + Field(name="event_timestamp", dtype=UnixTimestamp), + ], + source=rag_documents_source, +) +``` + +When you run `feast apply`, Feast creates the corresponding Atlas vector search index. When the feature view is removed, the index is dropped. The `vector_index_wait_timeout` and `vector_index_wait_poll_interval` settings control how long Feast waits for newly created Atlas Search indexes to become queryable before returning. + +Querying nearest neighbors is then one call: + +```py +results = store.retrieve_online_documents_v2( + features=[ + "embedded_documents:vector", + "embedded_documents:item_id", + "embedded_documents:sentence_chunks", + ], + query=query_embedding, # list[float] of the same dim + top_k=5, + distance_metric="COSINE", +).to_df() +``` + +Under the hood, this becomes a `$vectorSearch` aggregation against your Atlas cluster - no second system to provision, no vector data to keep in sync with the rest of your features. + +## Why this matters + +A few reasons we think this lands in the right place for ML teams already on MongoDB: + +* **One database for training and inference.** The same Atlas cluster powers historical retrieval, materialization, and online serving. No ETL pipelines pushing features from a warehouse. Update a feature once, see it everywhere. +* **One security and compliance posture.** Atlas networking, IAM, encryption, and audit logging cover both halves of the feature store. Architects don't have to add a new database vendor and a new threat model to say yes to ML. +* **Vector and operational data colocated.** For RAG, recommenders, and agents, the embeddings live next to the entity data they describe. Filter your vector search on operational fields with the same query language you already use. +* **Flexible schema where it helps.** Feature engineering is iterative. MongoDB's document model means adding a field to a feature view doesn't require a schema migration on day one. +* **Async serving when you need it.** The online store ships a native async path on `AsyncMongoClient`, so feature lookups don't block the rest of your serving stack. + +## Where to next + +* **Online store reference:** [Feast docs - MongoDB online store](https://docs.feast.dev/master/reference/online-stores/mongodb) +* **Offline store reference:** [Feast docs - MongoDB offline store](https://docs.feast.dev/master/reference/offline-stores/mongodb) +* **Vector search:** [Feast docs - Vector Search](https://docs.feast.dev/master/reference/data-sources/mongodb#vector-search) +* **Tutorial:** [Integrate MongoDB with Feast](https://www.mongodb.com/docs/atlas/ai-integrations/feast/) + +If you're already on MongoDB and want to standardize your ML stack on a single backend, this is the time to try it. Spin up a feature repo, point both stores at your cluster, and let us know how it goes - issues and PRs welcome on GitHub. \ No newline at end of file diff --git a/infra/website/docs/blog/scaling-feast-feature-server.md b/infra/website/docs/blog/scaling-feast-feature-server.md new file mode 100644 index 00000000000..994811ea0ac --- /dev/null +++ b/infra/website/docs/blog/scaling-feast-feature-server.md @@ -0,0 +1,317 @@ +--- +title: Feature Server High-Availability and Auto-Scaling on Kubernetes +description: The Feast Operator now supports horizontal scaling with static replicas, HPA autoscaling, KEDA, and high-availability features including PodDisruptionBudgets and topology spread constraints. +date: 2026-03-02 +authors: ["Nikhil Kathole", "Antonin Stefanutti"] +--- + +# Feature Server High-Availability and Auto-Scaling on Kubernetes + +As ML systems move from experimentation to production, the feature server often becomes a critical bottleneck. A single-replica deployment might handle development traffic, but production workloads — real-time inference, batch scoring, multiple consuming services — demand the ability to scale horizontally. + +We're excited to announce that the Feast Operator now supports **horizontal scaling** for the FeatureStore deployment, giving teams the tools to run Feast at production scale on Kubernetes. + +# The Problem: Single-Replica Limitations + +By default, the Feast Operator deploys a single-replica Deployment. This works well for getting started, but presents challenges as traffic grows: + +- **Single point of failure** — one pod crash means downtime for all feature consumers +- **Throughput ceiling** — a single pod can only handle so many concurrent requests +- **No elasticity** — traffic spikes (model retraining, batch inference) can overwhelm the server +- **Rolling updates cause downtime** — the default `Recreate` strategy tears down the old pod before starting a new one + +Teams have been manually patching Deployments or creating external HPAs, but this bypasses the operator's reconciliation loop and can lead to configuration drift. + +# The Solution: Native Scaling Support + +The Feast Operator now supports three scaling modes. The FeatureStore CRD implements the Kubernetes **scale sub-resource**, which means you can also scale with `kubectl scale featurestore/my-feast --replicas=3`. + +## 1. Static Replicas + +The simplest approach — set a fixed number of replicas via `spec.replicas`: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: production-feast +spec: + feastProject: my_project + replicas: 3 + services: + onlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-data-stores + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-data-stores +``` + +This gives you high availability and load distribution with a predictable resource footprint. The operator automatically switches the Deployment strategy to `RollingUpdate`, ensuring zero-downtime deployments. + +## 2. HPA Autoscaling + +For workloads with variable traffic patterns, the operator can create and manage a `HorizontalPodAutoscaler` directly. HPA autoscaling is configured under `services.scaling.autoscaling` and is mutually exclusive with `spec.replicas > 1`: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: autoscaled-feast +spec: + feastProject: my_project + services: + scaling: + autoscaling: + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + podDisruptionBudgets: + maxUnavailable: 1 + onlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-data-stores + server: + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-data-stores +``` + +The operator creates the HPA as an owned resource — it's automatically cleaned up if you remove the autoscaling configuration or delete the FeatureStore CR. If no custom metrics are specified, the operator defaults to **80% CPU utilization**. The operator also auto-injects soft pod anti-affinity (node-level) and topology spread constraints (zone-level) to improve resilience — see the [High Availability](#high-availability) section for details. + +## 3. External Autoscalers (KEDA, Custom HPAs) + +For teams using [KEDA](https://keda.sh) or other external autoscalers, KEDA should target the FeatureStore's scale sub-resource directly (since it implements the Kubernetes scale API). This is the recommended approach because the operator manages the Deployment's replica count from `spec.replicas` — targeting the Deployment directly would conflict with the operator's reconciliation. + +When using KEDA, do **not** set `spec.replicas > 1` or `services.scaling.autoscaling` — KEDA manages the replica count through the scale sub-resource. Configure the FeatureStore with DB-backed persistence, then create a KEDA `ScaledObject` targeting the FeatureStore resource: + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: keda-feast +spec: + feastProject: my_project + services: + onlineStore: + persistence: + store: + type: postgres + secretRef: + name: feast-data-stores + registry: + local: + persistence: + store: + type: sql + secretRef: + name: feast-data-stores +--- +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: feast-scaledobject +spec: + scaleTargetRef: + apiVersion: feast.dev/v1 + kind: FeatureStore + name: keda-feast + minReplicaCount: 1 + maxReplicaCount: 10 + triggers: + - type: prometheus + metadata: + serverAddress: http://prometheus.monitoring.svc:9090 + metricName: http_requests_total + query: sum(rate(http_requests_total{service="feast"}[2m])) + threshold: "100" +``` + +When KEDA scales up `spec.replicas` via the scale sub-resource, the CRD's CEL validation rules automatically ensure DB-backed persistence is configured. The operator also automatically switches the deployment strategy to `RollingUpdate` when `replicas > 1`. This gives you the full power of KEDA's 50+ event-driven triggers with built-in safety checks. + +# High Availability + +Scaling to multiple replicas is only half the story — you also need to ensure pods are spread across failure domains and protected during disruptions. The operator includes two HA features that activate when scaling is enabled: + +## Pod Anti-Affinity + +When scaling is enabled, the operator **automatically injects** a soft pod anti-affinity rule that prefers spreading pods across different nodes: + +```yaml +affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + feast.dev/name: my-feast +``` + +This means the scheduler will *try* to place each replica on a separate node, but won't prevent scheduling if nodes are constrained. You can override this with your own `affinity` configuration in the CR, or set it to an explicit value to customize the behavior (e.g. `requiredDuringSchedulingIgnoredDuringExecution` for strict anti-affinity). + +## Topology Spread Constraints + +When `replicas > 1` or autoscaling is configured, the operator **automatically injects** a soft zone-spread constraint: + +```yaml +topologySpreadConstraints: +- maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + feast.dev/name: my-feast +``` + +This distributes pods across availability zones on a best-effort basis. If your cluster has 3 zones and 3 replicas, each zone gets one pod. If zones are unavailable, pods still get scheduled rather than staying pending. + +You can override this with explicit constraints (e.g. strict `DoNotSchedule`) or disable it entirely by setting `topologySpreadConstraints: []`. + +## PodDisruptionBudgets + +For protection during voluntary disruptions (node drains, cluster upgrades), you can configure a PDB: + +```yaml +spec: + replicas: 3 + services: + podDisruptionBudgets: + maxUnavailable: 1 + onlineStore: + # ... +``` + +The PDB requires explicit configuration — it's not auto-injected because a misconfigured PDB can block node drains. The operator enforces that exactly one of `minAvailable` or `maxUnavailable` is set via CEL validation. The PDB is only created when scaling is enabled and is automatically cleaned up when scaling is disabled. + +# Safety First: Persistence Validation + +Not all persistence backends are safe for multi-replica deployments. File-based stores like SQLite, DuckDB, and local `registry.db` use single-writer file locks that don't work across pods. + +The operator enforces this at admission time via CEL validation rules on the CRD — if you try to create or update a FeatureStore with scaling and file-based persistence, the API server rejects the request immediately: + +``` +Scaling requires DB-backed persistence for the online store. +Configure services.onlineStore.persistence.store when using replicas > 1 or autoscaling. +``` + +This validation applies to all enabled services (online store, offline store, and registry) and is enforced for both direct CR updates and `kubectl scale` commands via the scale sub-resource. Object-store-backed registry paths (`s3://` and `gs://`) are treated as safe since they support concurrent readers. + +| Persistence Type | Compatible with Scaling? | +|---|---| +| PostgreSQL / MySQL | Yes | +| Redis | Yes | +| Cassandra | Yes | +| SQL-based Registry | Yes | +| S3/GCS Registry | Yes | +| SQLite | No | +| DuckDB | No | +| Local `registry.db` | No | + +# How It Works Under the Hood + +The implementation adds three key behaviors to the operator's reconciliation loop: + +**1. Replica management** — The operator sets the Deployment's replica count from `spec.replicas` (which defaults to 1). When HPA is configured, the operator leaves the `replicas` field unset so the HPA controller can manage it. External autoscalers like KEDA can update the replica count through the FeatureStore's scale sub-resource, which updates `spec.replicas` and triggers the operator to reconcile. + +**2. Deployment strategy** — The operator automatically switches from `Recreate` (the default for single-replica) to `RollingUpdate` when scaling is enabled. This prevents the "kill-all-pods-then-start-new-ones" behavior that would cause downtime during scaling events. Users can always override this with an explicit `deploymentStrategy` in the CR. + +**3. HPA lifecycle** — The operator creates, updates, and deletes the HPA as an owned resource tied to the FeatureStore CR. Removing the `autoscaling` configuration automatically cleans up the HPA. + +**4. HA features** — The operator auto-injects soft topology spread constraints across zones when scaling is enabled, and manages PodDisruptionBudgets as owned resources when explicitly configured. + +The scaling status is reported back on the FeatureStore status: + +```yaml +status: + scalingStatus: + currentReplicas: 3 + desiredReplicas: 3 +``` + +# What About TLS, CronJobs, and Services? + +Scaling is designed to work seamlessly with existing operator features: + +- **TLS** — Each pod mounts the same TLS secret. OpenShift service-serving certificates work automatically since they're bound to the Service, not individual pods. +- **Kubernetes Services** — The Service's label selector already matches all pods in the Deployment, so load balancing across replicas works out of the box. +- **CronJobs** — The `feast apply` and `feast materialize-incremental` CronJobs use `kubectl exec` into a single pod. Since DB-backed persistence is required for scaling, all pods share the same state — it doesn't matter which pod the CronJob runs against. + +# Getting Started + +**1. Ensure DB-backed persistence** for all enabled services (online store, offline store, registry). + +**2. Configure scaling** in your FeatureStore CR — use either static replicas or HPA (mutually exclusive). Optionally add a PDB for disruption protection: + +```yaml +spec: + replicas: 3 # static replicas (top-level) + services: + podDisruptionBudgets: # optional: protect against disruptions + maxUnavailable: 1 + # -- OR -- + # services: + # scaling: + # autoscaling: # HPA + # minReplicas: 2 + # maxReplicas: 10 + # podDisruptionBudgets: + # maxUnavailable: 1 +``` + +**3. Apply** the updated CR: + +```bash +kubectl apply -f my-featurestore.yaml +``` + +**4. Verify** the scaling: + +```bash +# Check pods +kubectl get pods -l app.kubernetes.io/managed-by=feast + +# Check HPA (if using autoscaling) +kubectl get hpa + +# Check FeatureStore status +kubectl get feast -o yaml +``` + +# Learn More + +- [Scaling Feast documentation](https://docs.feast.dev/how-to-guides/scaling-feast) +- [Feast on Kubernetes guide](https://docs.feast.dev/how-to-guides/feast-on-kubernetes) +- [FeatureStore CRD API reference](https://github.com/feast-dev/feast/blob/master/infra/feast-operator/docs/api/markdown/ref.md) +- [Sample CRs for static scaling and HPA](https://github.com/feast-dev/feast/tree/master/infra/feast-operator/config/samples) +- Join the [Feast Slack](https://slack.feast.dev) to share feedback and ask questions + +We're excited to see teams scale their feature serving infrastructure with confidence. Try it out and let us know how it works for your use case! diff --git a/infra/website/package-lock.json b/infra/website/package-lock.json index 419bd36c346..6879bc48b40 100644 --- a/infra/website/package-lock.json +++ b/infra/website/package-lock.json @@ -8,293 +8,213 @@ "name": "feast-website", "version": "0.0.1", "dependencies": { - "astro": "^4.5.5", + "astro": "^5.16.10", "d3": "^7.9.0", "shiki": "^1.29.2" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@astrojs/compiler": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.11.0.tgz", - "integrity": "sha512-zZOO7i+JhojO8qmlyR/URui6LyfHJY6m+L9nwyX5GiKD78YoRaZ5tzz6X0fkl+5bD3uwlDHayf6Oe8Fu36RKNg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.0.tgz", + "integrity": "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==", "license": "MIT" }, "node_modules/@astrojs/internal-helpers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.4.1.tgz", - "integrity": "sha512-bMf9jFihO8YP940uD70SI/RDzIhUHJAolWVcO1v5PUivxGKvfLZTLTVVxEYzGYyPsA3ivdLNqMnL5VgmQySa+g==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.7.5.tgz", + "integrity": "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA==", "license": "MIT" }, "node_modules/@astrojs/markdown-remark": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-5.3.0.tgz", - "integrity": "sha512-r0Ikqr0e6ozPb5bvhup1qdWnSPUvQu6tub4ZLYaKyG50BXZ0ej6FhGz3GpChKpH7kglRFPObJd/bDyf2VM9pkg==", + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.3.10.tgz", + "integrity": "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A==", "license": "MIT", "dependencies": { - "@astrojs/prism": "3.1.0", + "@astrojs/internal-helpers": "0.7.5", + "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", - "import-meta-resolve": "^4.1.0", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.0", + "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.1", + "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", - "shiki": "^1.22.0", + "shiki": "^3.19.0", + "smol-toml": "^1.5.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", - "unist-util-visit-parents": "^6.0.1", + "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, - "node_modules/@astrojs/prism": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.1.0.tgz", - "integrity": "sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==", + "node_modules/@astrojs/markdown-remark/node_modules/@shikijs/core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz", + "integrity": "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==", "license": "MIT", "dependencies": { - "prismjs": "^1.29.0" - }, - "engines": { - "node": "^18.17.1 || ^20.3.0 || >=21.0.0" + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" } }, - "node_modules/@astrojs/telemetry": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.1.0.tgz", - "integrity": "sha512-/ca/+D8MIKEC8/A9cSaPUqQNZm+Es/ZinRv0ZAzvu2ios7POQSsVD+VOj7/hypWNsNM3T7RpfgNq7H2TU1KEHA==", + "node_modules/@astrojs/markdown-remark/node_modules/@shikijs/engine-javascript": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz", + "integrity": "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==", "license": "MIT", "dependencies": { - "ci-info": "^4.0.0", - "debug": "^4.3.4", - "dlv": "^1.1.3", - "dset": "^3.1.3", - "is-docker": "^3.0.0", - "is-wsl": "^3.0.0", - "which-pm-runs": "^1.1.0" - }, - "engines": { - "node": "^18.17.1 || ^20.3.0 || >=21.0.0" + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" } }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "node_modules/@astrojs/markdown-remark/node_modules/@shikijs/engine-oniguruma": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz", + "integrity": "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2" } }, - "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "node_modules/@astrojs/markdown-remark/node_modules/@shikijs/langs": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.21.0.tgz", + "integrity": "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==", "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "@shikijs/types": "3.21.0" } }, - "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "node_modules/@astrojs/markdown-remark/node_modules/@shikijs/themes": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.21.0.tgz", + "integrity": "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==", "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "@shikijs/types": "3.21.0" } }, - "node_modules/@babel/generator": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", - "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "node_modules/@astrojs/markdown-remark/node_modules/@shikijs/types": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.21.0.tgz", + "integrity": "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.10", - "@babel/types": "^7.26.10", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "node_modules/@astrojs/markdown-remark/node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "node_modules/@astrojs/markdown-remark/node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" + "regex-utilities": "^2.3.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@astrojs/markdown-remark/node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "node_modules/@astrojs/markdown-remark/node_modules/shiki": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz", + "integrity": "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" + "@shikijs/core": "3.21.0", + "@shikijs/engine-javascript": "3.21.0", + "@shikijs/engine-oniguruma": "3.21.0", + "@shikijs/langs": "3.21.0", + "@shikijs/themes": "3.21.0", + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "node_modules/@astrojs/prism": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.3.0.tgz", + "integrity": "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "prismjs": "^1.30.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "node_modules/@astrojs/telemetry": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", + "integrity": "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==", "license": "MIT", + "dependencies": { + "ci-info": "^4.2.0", + "debug": "^4.4.0", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "is-docker": "^3.0.0", + "is-wsl": "^3.1.0", + "which-pm-runs": "^1.1.0" + }, "engines": { - "node": ">=6.9.0" + "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", - "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.10" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -303,89 +223,35 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz", - "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-jsx": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", - "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "node_modules/@capsizecss/unpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", + "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "fontkitten": "^1.0.0" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "license": "MIT", "optional": true, "dependencies": { @@ -393,9 +259,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", - "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -404,15 +270,14 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", - "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -421,15 +286,14 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", - "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -438,15 +302,14 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", - "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -455,15 +318,14 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", - "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -472,15 +334,14 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", - "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -489,15 +350,14 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", - "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -506,15 +366,14 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", - "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -523,15 +382,14 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", - "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -540,15 +398,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", - "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -557,15 +414,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", - "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -574,15 +430,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", - "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -591,15 +446,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", - "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -608,15 +462,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", - "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -625,15 +478,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", - "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -642,15 +494,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", - "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -659,15 +510,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", - "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -676,15 +526,14 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", - "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -693,15 +542,14 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", - "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -710,15 +558,14 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", - "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -727,15 +574,14 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", - "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -744,15 +590,30 @@ "os": [ "openbsd" ], - "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", - "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -761,15 +622,14 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", - "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -778,15 +638,14 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", - "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -795,15 +654,14 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", - "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -812,15 +670,24 @@ "os": [ "win32" ], - "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, "engines": { "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -836,13 +703,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -858,13 +725,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -878,9 +745,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -894,9 +761,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -910,9 +777,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -925,10 +792,42 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -942,9 +841,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -958,9 +857,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -974,9 +873,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -990,9 +889,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -1008,13 +907,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -1030,13 +929,57 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -1052,13 +995,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -1074,13 +1017,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -1096,13 +1039,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -1118,21 +1061,40 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -1141,9 +1103,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -1160,9 +1122,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -1178,104 +1140,27 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@oslojs/encoding": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", - "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", - "license": "MIT" - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" @@ -1296,9 +1181,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", - "integrity": "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", "cpu": [ "arm" ], @@ -1309,9 +1194,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz", - "integrity": "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", "cpu": [ "arm64" ], @@ -1322,9 +1207,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz", - "integrity": "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", "cpu": [ "arm64" ], @@ -1335,9 +1220,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz", - "integrity": "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", "cpu": [ "x64" ], @@ -1348,9 +1233,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz", - "integrity": "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", "cpu": [ "arm64" ], @@ -1361,9 +1246,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz", - "integrity": "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", "cpu": [ "x64" ], @@ -1374,9 +1259,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz", - "integrity": "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", "cpu": [ "arm" ], @@ -1387,9 +1272,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz", - "integrity": "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", "cpu": [ "arm" ], @@ -1400,9 +1285,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz", - "integrity": "sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", "cpu": [ "arm64" ], @@ -1413,9 +1298,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz", - "integrity": "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", "cpu": [ "arm64" ], @@ -1425,10 +1310,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz", - "integrity": "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", "cpu": [ "loong64" ], @@ -1438,10 +1323,36 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz", - "integrity": "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", "cpu": [ "ppc64" ], @@ -1452,9 +1363,22 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz", - "integrity": "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", "cpu": [ "riscv64" ], @@ -1465,9 +1389,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz", - "integrity": "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", "cpu": [ "s390x" ], @@ -1478,9 +1402,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz", - "integrity": "sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", "cpu": [ "x64" ], @@ -1491,9 +1415,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.35.0.tgz", - "integrity": "sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", "cpu": [ "x64" ], @@ -1503,10 +1427,36 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz", - "integrity": "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", "cpu": [ "arm64" ], @@ -1517,9 +1467,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz", - "integrity": "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", "cpu": [ "ia32" ], @@ -1529,10 +1479,23 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz", - "integrity": "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", "cpu": [ "x64" ], @@ -1611,53 +1574,6 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT" - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1668,9 +1584,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/hast": { @@ -1719,9 +1635,9 @@ "license": "ISC" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1804,6 +1720,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1830,549 +1771,195 @@ } }, "node_modules/astro": { - "version": "4.16.18", - "resolved": "https://registry.npmjs.org/astro/-/astro-4.16.18.tgz", - "integrity": "sha512-G7zfwJt9BDHEZwlaLNvjbInIw2hPryyD654314KV/XT34pJU6SfN1S+mWa8RAkALcZNJnJXCJmT3JXLQStD3Lw==", - "license": "MIT", - "dependencies": { - "@astrojs/compiler": "^2.10.3", - "@astrojs/internal-helpers": "0.4.1", - "@astrojs/markdown-remark": "5.3.0", - "@astrojs/telemetry": "3.1.0", - "@babel/core": "^7.26.0", - "@babel/plugin-transform-react-jsx": "^7.25.9", - "@babel/types": "^7.26.0", + "version": "5.16.10", + "resolved": "https://registry.npmjs.org/astro/-/astro-5.16.10.tgz", + "integrity": "sha512-mBaRwFrqwCHiKDvvfNW2rRRkLAqnkj3lbkte6Vg4OzeUiDyEsdU4oOqTZxHJf/mxzZvBiU37BxG1oeh+tq1IUA==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.13.0", + "@astrojs/internal-helpers": "0.7.5", + "@astrojs/markdown-remark": "6.3.10", + "@astrojs/telemetry": "3.3.0", + "@capsizecss/unpack": "^4.0.0", "@oslojs/encoding": "^1.1.0", - "@rollup/pluginutils": "^5.1.3", - "@types/babel__core": "^7.20.5", - "@types/cookie": "^0.6.0", - "acorn": "^8.14.0", + "@rollup/pluginutils": "^5.3.0", + "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", - "ci-info": "^4.1.0", + "ci-info": "^4.3.1", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", - "cookie": "^0.7.2", + "cookie": "^1.1.1", "cssesc": "^3.0.0", - "debug": "^4.3.7", + "debug": "^4.4.3", "deterministic-object-hash": "^2.0.2", - "devalue": "^5.1.1", - "diff": "^5.2.0", + "devalue": "^5.6.1", + "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", - "es-module-lexer": "^1.5.4", - "esbuild": "^0.21.5", + "es-module-lexer": "^1.7.0", + "esbuild": "^0.25.0", "estree-walker": "^3.0.3", - "fast-glob": "^3.3.2", "flattie": "^1.1.1", + "fontace": "~0.4.0", "github-slugger": "^2.0.0", - "gray-matter": "^4.0.3", - "html-escaper": "^3.0.3", - "http-cache-semantics": "^4.1.1", - "js-yaml": "^4.1.0", - "kleur": "^4.1.5", - "magic-string": "^0.30.14", - "magicast": "^0.3.5", - "micromatch": "^4.0.8", - "mrmime": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.1", + "mrmime": "^2.0.1", "neotraverse": "^0.6.18", - "ora": "^8.1.1", - "p-limit": "^6.1.0", - "p-queue": "^8.0.1", - "preferred-pm": "^4.0.0", + "p-limit": "^6.2.0", + "p-queue": "^8.1.1", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", - "semver": "^7.6.3", - "shiki": "^1.23.1", - "tinyexec": "^0.3.1", - "tsconfck": "^3.1.4", + "semver": "^7.7.3", + "shiki": "^3.20.0", + "smol-toml": "^1.6.0", + "svgo": "^4.0.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tsconfck": "^3.1.6", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.1", "unist-util-visit": "^5.0.0", + "unstorage": "^1.17.3", "vfile": "^6.0.3", - "vite": "^5.4.11", - "vitefu": "^1.0.4", - "which-pm": "^3.0.0", + "vite": "^6.4.1", + "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.23.5", + "yocto-spinner": "^0.2.3", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1", "zod-to-ts": "^1.2.0" }, "bin": { "astro": "astro.js" }, "engines": { - "node": "^18.17.1 || ^20.3.0 || >=21.0.0", + "node": "18.20.8 || ^20.3.0 || >=22.0.0", "npm": ">=9.6.5", "pnpm": ">=7.1.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, "optionalDependencies": { - "sharp": "^0.33.3" - } - }, - "node_modules/astro/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "sharp": "^0.34.0" } }, - "node_modules/astro/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "node_modules/astro/node_modules/@shikijs/core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz", + "integrity": "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" } }, - "node_modules/astro/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/astro/node_modules/@shikijs/engine-javascript": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz", + "integrity": "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/astro/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" } }, - "node_modules/astro/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "node_modules/astro/node_modules/@shikijs/engine-oniguruma": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz", + "integrity": "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2" } }, - "node_modules/astro/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "node_modules/astro/node_modules/@shikijs/langs": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.21.0.tgz", + "integrity": "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@shikijs/types": "3.21.0" } }, - "node_modules/astro/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/astro/node_modules/@shikijs/themes": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.21.0.tgz", + "integrity": "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@shikijs/types": "3.21.0" } }, - "node_modules/astro/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/astro/node_modules/@shikijs/types": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.21.0.tgz", + "integrity": "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, - "node_modules/astro/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "node_modules/astro/node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" } }, - "node_modules/astro/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/astro/node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "regex-utilities": "^2.3.0" } }, - "node_modules/astro/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "node_modules/astro/node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "regex-utilities": "^2.3.0" } }, - "node_modules/astro/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "hasInstallScript": true, + "node_modules/astro/node_modules/shiki": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz", + "integrity": "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/astro/node_modules/vite": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", - "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "dependencies": { + "@shikijs/core": "3.21.0", + "@shikijs/engine-javascript": "3.21.0", + "@shikijs/engine-oniguruma": "3.21.0", + "@shikijs/langs": "3.21.0", + "@shikijs/themes": "3.21.0", + "@shikijs/types": "3.21.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, "node_modules/axobject-query": { @@ -2400,6 +1987,12 @@ "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/boxen": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", @@ -2418,52 +2011,8 @@ "engines": { "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/camelcase": { @@ -2478,26 +2027,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001705", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz", - "integrity": "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -2550,10 +2079,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ci-info": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", - "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "funding": [ { "type": "github", @@ -2577,33 +2121,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2613,51 +2130,6 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "optional": true - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2683,19 +2155,73 @@ "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", "license": "ISC" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.6" + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, "node_modules/cssesc": { @@ -2710,6 +2236,39 @@ "node": ">=4" } }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, "node_modules/d3": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", @@ -3027,6 +2586,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -3112,9 +2672,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3129,9 +2689,9 @@ } }, "node_modules/decode-named-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", - "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -3141,6 +2701,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -3159,10 +2725,16 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "optional": true, "engines": { @@ -3182,9 +2754,9 @@ } }, "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", + "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", "license": "MIT" }, "node_modules/devlop": { @@ -3201,9 +2773,9 @@ } }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", + "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -3215,6 +2787,61 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", @@ -3224,12 +2851,6 @@ "node": ">=4" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.119", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.119.tgz", - "integrity": "sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==", - "license": "ISC" - }, "node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -3255,19 +2876,17 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", - "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", - "optional": true, - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3275,40 +2894,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.1", - "@esbuild/android-arm": "0.25.1", - "@esbuild/android-arm64": "0.25.1", - "@esbuild/android-x64": "0.25.1", - "@esbuild/darwin-arm64": "0.25.1", - "@esbuild/darwin-x64": "0.25.1", - "@esbuild/freebsd-arm64": "0.25.1", - "@esbuild/freebsd-x64": "0.25.1", - "@esbuild/linux-arm": "0.25.1", - "@esbuild/linux-arm64": "0.25.1", - "@esbuild/linux-ia32": "0.25.1", - "@esbuild/linux-loong64": "0.25.1", - "@esbuild/linux-mips64el": "0.25.1", - "@esbuild/linux-ppc64": "0.25.1", - "@esbuild/linux-riscv64": "0.25.1", - "@esbuild/linux-s390x": "0.25.1", - "@esbuild/linux-x64": "0.25.1", - "@esbuild/netbsd-arm64": "0.25.1", - "@esbuild/netbsd-x64": "0.25.1", - "@esbuild/openbsd-arm64": "0.25.1", - "@esbuild/openbsd-x64": "0.25.1", - "@esbuild/sunos-x64": "0.25.1", - "@esbuild/win32-arm64": "0.25.1", - "@esbuild/win32-ia32": "0.25.1", - "@esbuild/win32-x64": "0.25.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escape-string-regexp": { @@ -3323,19 +2934,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -3357,97 +2955,51 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "node": ">=12.0.0" }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, "engines": { "node": ">=8" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/fontace": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.0.tgz", + "integrity": "sha512-moThBCItUe2bjZip5PF/iZClpKHGLwMvR79Kp8XpGRBrvoRSnySN4VcILdv3/MJzbhvUA5WeiUXF5o538m5fvg==", "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" + "fontkitten": "^1.0.0" } }, - "node_modules/find-up-simple": { + "node_modules/fontkitten": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.1.tgz", + "integrity": "sha512-m+/cO+/kAU9farlejecXLgQH20+UXyH0K6oosGtogAz7BWco+KTYE60epKwMt8eVxqlOE2Fs+GoHVlGDUbKOoA==", "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-yarn-workspace-root2": { - "version": "1.2.16", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", - "integrity": "sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==", - "license": "Apache-2.0", "dependencies": { - "micromatch": "^4.0.2", - "pkg-dir": "^4.2.0" - } - }, - "node_modules/flattie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", - "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", - "license": "MIT", + "tiny-inflate": "^1.0.3" + }, "engines": { - "node": ">=8" + "node": ">=24.12.0" } }, "node_modules/fsevents": { @@ -3464,15 +3016,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -3481,78 +3024,31 @@ "engines": { "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/github-slugger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", - "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", - "license": "ISC" - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gray-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" }, - "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/h3": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", + "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "cookie-es": "^1.2.2", + "crossws": "^0.3.5", + "defu": "^6.1.4", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" } }, "node_modules/hast-util-from-html": { @@ -3668,15 +3164,15 @@ } }, "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", - "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", - "property-information": "^6.0.0", + "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" @@ -3686,16 +3182,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hast-util-to-parse5/node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/hast-util-to-text": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", @@ -3759,9 +3245,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, "node_modules/iconv-lite": { @@ -3777,9 +3263,9 @@ } }, "node_modules/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", "license": "MIT", "funding": { "type": "github", @@ -3795,12 +3281,14 @@ "node": ">=12" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", "license": "MIT", - "optional": true + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } }, "node_modules/is-docker": { "version": "3.0.0", @@ -3817,24 +3305,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3844,18 +3314,6 @@ "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -3874,27 +3332,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3907,18 +3344,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-wsl": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", @@ -3934,27 +3359,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3963,134 +3371,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/load-yaml-file": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", - "integrity": "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.5", - "js-yaml": "^3.13.0", - "pify": "^4.0.1", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/load-yaml-file/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/load-yaml-file/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/load-yaml-file/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -4102,32 +3382,32 @@ } }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" } }, "node_modules/markdown-table": { @@ -4365,14 +3645,11 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" }, "node_modules/micromark": { "version": "4.0.2", @@ -4937,43 +4214,6 @@ ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -4990,9 +4230,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz", - "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -5029,107 +4269,92 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "license": "MIT" }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" }, - "node_modules/oniguruma-to-es": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", - "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", - "dependencies": { - "emoji-regex-xs": "^1.0.0", - "regex": "^5.1.1", - "regex-recursion": "^5.1.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "license": "MIT", + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" + "boolbase": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/p-limit": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", - "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", "license": "MIT", "dependencies": { - "yocto-queue": "^1.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", + "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" + "emoji-regex-xs": "^1.0.0", + "regex": "^5.1.1", + "regex-recursion": "^5.1.1" } }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^1.1.1" }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-queue": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.0.tgz", - "integrity": "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", "license": "MIT", "dependencies": { "eventemitter3": "^5.0.1", @@ -5154,14 +4379,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" }, "node_modules/parse-latin": { "version": "7.0.0", @@ -5193,14 +4415,11 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" }, "node_modules/picocolors": { "version": "1.1.1", @@ -5209,9 +4428,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -5220,22 +4439,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5252,7 +4459,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5260,20 +4467,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/preferred-pm": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-4.1.1.tgz", - "integrity": "sha512-rU+ZAv1Ur9jAUZtGPebQVQPzdGhNzaEiQ7VL9+cjsAWPHFYOccNXPNiev1CCDSOg/2j7UujM7ojNhpkuILEVNQ==", - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "find-yarn-workspace-root2": "1.2.16", - "which-pm": "^3.0.1" - }, - "engines": { - "node": ">=18.12" - } - }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -5315,26 +4508,25 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/regex": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", @@ -5456,9 +4648,9 @@ } }, "node_modules/remark-rehype": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", - "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -5502,22 +4694,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/retext": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", @@ -5579,16 +4755,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", @@ -5596,12 +4762,13 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz", - "integrity": "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "license": "MIT", + "peer": true, "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5611,51 +4778,34 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.35.0", - "@rollup/rollup-android-arm64": "4.35.0", - "@rollup/rollup-darwin-arm64": "4.35.0", - "@rollup/rollup-darwin-x64": "4.35.0", - "@rollup/rollup-freebsd-arm64": "4.35.0", - "@rollup/rollup-freebsd-x64": "4.35.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", - "@rollup/rollup-linux-arm-musleabihf": "4.35.0", - "@rollup/rollup-linux-arm64-gnu": "4.35.0", - "@rollup/rollup-linux-arm64-musl": "4.35.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", - "@rollup/rollup-linux-riscv64-gnu": "4.35.0", - "@rollup/rollup-linux-s390x-gnu": "4.35.0", - "@rollup/rollup-linux-x64-gnu": "4.35.0", - "@rollup/rollup-linux-x64-musl": "4.35.0", - "@rollup/rollup-win32-arm64-msvc": "4.35.0", - "@rollup/rollup-win32-ia32-msvc": "4.35.0", - "@rollup/rollup-win32-x64-msvc": "4.35.0", + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", @@ -5668,23 +4818,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=4" + "node": ">=11.0.0" } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5694,16 +4840,16 @@ } }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -5712,25 +4858,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shiki": { @@ -5749,34 +4900,24 @@ "@types/hast": "^3.0.4" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/smol-toml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", + "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5796,24 +4937,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -5860,40 +4983,69 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "node_modules/svgo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.4.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, "engines": { - "node": ">=4" + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" } }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "node_modules/svgo/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=16" } }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "license": "MIT" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/trim-lines": { @@ -5917,9 +5069,9 @@ } }, "node_modules/tsconfck": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", - "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", "license": "MIT", "bin": { "tsconfck": "bin/tsconfck.js" @@ -5969,6 +5121,24 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -5988,6 +5158,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unifont": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.3.tgz", + "integrity": "sha512-b0GtQzKCyuSHGsfj5vyN8st7muZ6VCI4XD4vFlr7Uy1rlWVYxC3npnfk8MyreHxJYrz1ooLDqDzFe9XqQTlAhA==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, "node_modules/unist-util-find-after": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", @@ -6098,9 +5279,9 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -6111,34 +5292,100 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/unstorage": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.4.tgz", + "integrity": "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==", "license": "MIT", "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.5", + "lru-cache": "^11.2.0", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" }, "peerDependencies": { - "browserslist": ">= 4.21.0" + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } } }, "node_modules/vfile": { @@ -6184,16 +5431,18 @@ } }, "node_modules/vite": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz", - "integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "optional": true, "peer": true, "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -6257,16 +5506,17 @@ } }, "node_modules/vitefu": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", - "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "license": "MIT", "workspaces": [ "tests/deps/*", - "tests/projects/*" + "tests/projects/*", + "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "peerDependenciesMeta": { "vite": { @@ -6284,18 +5534,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/which-pm": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-3.0.1.tgz", - "integrity": "sha512-v2JrMq0waAI4ju1xU5x3blsxBBMgdgZve580iYMN5frDaLGjbA24fok7wKCsya8KLVO19Ju4XDc5+zTZCJkQfg==", - "license": "MIT", - "dependencies": { - "load-yaml-file": "^0.2.0" - }, - "engines": { - "node": ">=18.12" - } - }, "node_modules/which-pm-runs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", @@ -6343,26 +5581,6 @@ "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "license": "MIT" }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -6384,22 +5602,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yocto-spinner": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.2.3.tgz", + "integrity": "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==", + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.24.4", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.4.tgz", - "integrity": "sha512-0uNlcvgabyrni9Ag8Vghj21drk7+7tp7VTwwR7KxxXXc/3pbXz2PHlDgj3cICahgF1kHm4dExBFj7BXrZJXzig==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", "peerDependencies": { - "zod": "^3.24.1" + "zod": "^3.25 || ^4" } }, "node_modules/zod-to-ts": { diff --git a/infra/website/package.json b/infra/website/package.json index 2cebf3797e7..3871bf9db8f 100644 --- a/infra/website/package.json +++ b/infra/website/package.json @@ -10,7 +10,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^4.5.5", + "astro": "^5.16.10", "d3": "^7.9.0", "shiki": "^1.29.2" } diff --git a/infra/website/public/images/blog/dbt+feast.png b/infra/website/public/images/blog/dbt+feast.png new file mode 100644 index 00000000000..119d6e4ad83 Binary files /dev/null and b/infra/website/public/images/blog/dbt+feast.png differ diff --git a/infra/website/public/images/blog/end_to_end_lineage.png b/infra/website/public/images/blog/end_to_end_lineage.png new file mode 100644 index 00000000000..8d49cd2ec74 Binary files /dev/null and b/infra/website/public/images/blog/end_to_end_lineage.png differ diff --git a/infra/website/public/images/blog/entity_dataframe.png b/infra/website/public/images/blog/entity_dataframe.png new file mode 100644 index 00000000000..5588c9260d8 Binary files /dev/null and b/infra/website/public/images/blog/entity_dataframe.png differ diff --git a/infra/website/public/images/blog/feast-dqm-monitoring-hero.png b/infra/website/public/images/blog/feast-dqm-monitoring-hero.png new file mode 100644 index 00000000000..86d7125a3c3 Binary files /dev/null and b/infra/website/public/images/blog/feast-dqm-monitoring-hero.png differ diff --git a/infra/website/public/images/blog/feast-dqm-ui-all-features.png b/infra/website/public/images/blog/feast-dqm-ui-all-features.png new file mode 100644 index 00000000000..4e728a2e86e Binary files /dev/null and b/infra/website/public/images/blog/feast-dqm-ui-all-features.png differ diff --git a/infra/website/public/images/blog/feast-dqm-ui-categorical-feature.png b/infra/website/public/images/blog/feast-dqm-ui-categorical-feature.png new file mode 100644 index 00000000000..a3e6f22b74c Binary files /dev/null and b/infra/website/public/images/blog/feast-dqm-ui-categorical-feature.png differ diff --git a/infra/website/public/images/blog/feast-dqm-ui-numeric-feature.png b/infra/website/public/images/blog/feast-dqm-ui-numeric-feature.png new file mode 100644 index 00000000000..0b5e7f0d23d Binary files /dev/null and b/infra/website/public/images/blog/feast-dqm-ui-numeric-feature.png differ diff --git a/infra/website/public/images/blog/feast-feature-versioning-hero.png b/infra/website/public/images/blog/feast-feature-versioning-hero.png new file mode 100644 index 00000000000..d40d71a16dd Binary files /dev/null and b/infra/website/public/images/blog/feast-feature-versioning-hero.png differ diff --git a/infra/website/public/images/blog/feast-features-ui.png b/infra/website/public/images/blog/feast-features-ui.png new file mode 100644 index 00000000000..2b728e4c43a Binary files /dev/null and b/infra/website/public/images/blog/feast-features-ui.png differ diff --git a/infra/website/public/images/blog/feast-mcp-agent-workflow-prod.png b/infra/website/public/images/blog/feast-mcp-agent-workflow-prod.png new file mode 100644 index 00000000000..5cf94df580b Binary files /dev/null and b/infra/website/public/images/blog/feast-mcp-agent-workflow-prod.png differ diff --git a/infra/website/public/images/blog/feast-mcp-agent-workflow.png b/infra/website/public/images/blog/feast-mcp-agent-workflow.png new file mode 100644 index 00000000000..87504642e39 Binary files /dev/null and b/infra/website/public/images/blog/feast-mcp-agent-workflow.png differ diff --git a/infra/website/public/images/blog/feast-mcp-agent.png b/infra/website/public/images/blog/feast-mcp-agent.png new file mode 100644 index 00000000000..191e97517fe Binary files /dev/null and b/infra/website/public/images/blog/feast-mcp-agent.png differ diff --git a/infra/website/public/images/blog/feast-metrics-dashboard-overview.png b/infra/website/public/images/blog/feast-metrics-dashboard-overview.png new file mode 100644 index 00000000000..21293e8499e Binary files /dev/null and b/infra/website/public/images/blog/feast-metrics-dashboard-overview.png differ diff --git a/infra/website/public/images/blog/feast-metrics-hero.png b/infra/website/public/images/blog/feast-metrics-hero.png new file mode 100644 index 00000000000..b7c11e13e8d Binary files /dev/null and b/infra/website/public/images/blog/feast-metrics-hero.png differ diff --git a/infra/website/public/images/blog/feast-metrics-odfv-latency.png b/infra/website/public/images/blog/feast-metrics-odfv-latency.png new file mode 100644 index 00000000000..d6d33c0e5db Binary files /dev/null and b/infra/website/public/images/blog/feast-metrics-odfv-latency.png differ diff --git a/infra/website/public/images/blog/feast-mlflow-kubeflow.png b/infra/website/public/images/blog/feast-mlflow-kubeflow.png new file mode 100644 index 00000000000..c4e92228189 Binary files /dev/null and b/infra/website/public/images/blog/feast-mlflow-kubeflow.png differ diff --git a/infra/website/public/images/blog/feast-mlflow-native-integration.png b/infra/website/public/images/blog/feast-mlflow-native-integration.png new file mode 100644 index 00000000000..83df6a2c990 Binary files /dev/null and b/infra/website/public/images/blog/feast-mlflow-native-integration.png differ diff --git a/infra/website/public/images/blog/feast-openai-compat-flow.png b/infra/website/public/images/blog/feast-openai-compat-flow.png new file mode 100644 index 00000000000..c7dcac8a5ed Binary files /dev/null and b/infra/website/public/images/blog/feast-openai-compat-flow.png differ diff --git a/infra/website/public/images/blog/feast-oracle-offline-store.png b/infra/website/public/images/blog/feast-oracle-offline-store.png new file mode 100644 index 00000000000..425c7fda0cc Binary files /dev/null and b/infra/website/public/images/blog/feast-oracle-offline-store.png differ diff --git a/infra/website/public/images/blog/feast_native_lineage.png b/infra/website/public/images/blog/feast_native_lineage.png new file mode 100644 index 00000000000..534c17e87ed Binary files /dev/null and b/infra/website/public/images/blog/feast_native_lineage.png differ diff --git a/infra/website/public/images/blog/latency.png b/infra/website/public/images/blog/latency.png new file mode 100644 index 00000000000..c0271a3f4fe Binary files /dev/null and b/infra/website/public/images/blog/latency.png differ diff --git a/infra/website/public/images/blog/lineage_till_training.png b/infra/website/public/images/blog/lineage_till_training.png new file mode 100644 index 00000000000..a91adf8bf0e Binary files /dev/null and b/infra/website/public/images/blog/lineage_till_training.png differ diff --git a/infra/website/public/images/blog/mlflow-feast-feature-selection-metrics.png b/infra/website/public/images/blog/mlflow-feast-feature-selection-metrics.png new file mode 100644 index 00000000000..b6ad16c3346 Binary files /dev/null and b/infra/website/public/images/blog/mlflow-feast-feature-selection-metrics.png differ diff --git a/infra/website/public/images/blog/mlflow-feature-selection-comparison.png b/infra/website/public/images/blog/mlflow-feature-selection-comparison.png new file mode 100644 index 00000000000..4cad26065c6 Binary files /dev/null and b/infra/website/public/images/blog/mlflow-feature-selection-comparison.png differ diff --git a/infra/website/public/images/blog/mlflow-feature-selection-run.png b/infra/website/public/images/blog/mlflow-feature-selection-run.png new file mode 100644 index 00000000000..aabc6055480 Binary files /dev/null and b/infra/website/public/images/blog/mlflow-feature-selection-run.png differ diff --git a/infra/website/public/images/blog/mlflow-feature-selection-ui.png b/infra/website/public/images/blog/mlflow-feature-selection-ui.png new file mode 100644 index 00000000000..a598bbf4151 Binary files /dev/null and b/infra/website/public/images/blog/mlflow-feature-selection-ui.png differ diff --git a/infra/website/public/images/blog/mlflow_dashboard.png b/infra/website/public/images/blog/mlflow_dashboard.png new file mode 100644 index 00000000000..1d9896a0880 Binary files /dev/null and b/infra/website/public/images/blog/mlflow_dashboard.png differ diff --git a/infra/website/public/images/blog/mlflow_featurelist.png b/infra/website/public/images/blog/mlflow_featurelist.png new file mode 100644 index 00000000000..23920c179ff Binary files /dev/null and b/infra/website/public/images/blog/mlflow_featurelist.png differ diff --git a/infra/website/public/images/blog/model_metadata.png b/infra/website/public/images/blog/model_metadata.png new file mode 100644 index 00000000000..7ef300d9b2b Binary files /dev/null and b/infra/website/public/images/blog/model_metadata.png differ diff --git a/infra/website/public/images/blog/mongodb-feature-stores.png b/infra/website/public/images/blog/mongodb-feature-stores.png new file mode 100644 index 00000000000..c0705834dc9 Binary files /dev/null and b/infra/website/public/images/blog/mongodb-feature-stores.png differ diff --git a/infra/website/public/images/blog/offline_store_operational_metrics.png b/infra/website/public/images/blog/offline_store_operational_metrics.png new file mode 100644 index 00000000000..12006128d81 Binary files /dev/null and b/infra/website/public/images/blog/offline_store_operational_metrics.png differ diff --git a/infra/website/public/images/blog/openlineage1.png b/infra/website/public/images/blog/openlineage1.png new file mode 100644 index 00000000000..50119ab14c3 Binary files /dev/null and b/infra/website/public/images/blog/openlineage1.png differ diff --git a/infra/website/public/images/blog/openlineage2.png b/infra/website/public/images/blog/openlineage2.png new file mode 100644 index 00000000000..f5a43c0f411 Binary files /dev/null and b/infra/website/public/images/blog/openlineage2.png differ diff --git a/infra/website/public/images/blog/operation.png b/infra/website/public/images/blog/operation.png new file mode 100644 index 00000000000..43b6500faee Binary files /dev/null and b/infra/website/public/images/blog/operation.png differ diff --git a/infra/website/public/images/blog/rbac-architecture.jpg b/infra/website/public/images/blog/rbac-architecture.jpg new file mode 100644 index 00000000000..0de87d1718c Binary files /dev/null and b/infra/website/public/images/blog/rbac-architecture.jpg differ diff --git a/infra/website/public/images/blog/registered_model_with_feature_service.png b/infra/website/public/images/blog/registered_model_with_feature_service.png new file mode 100644 index 00000000000..1195d4c3cbb Binary files /dev/null and b/infra/website/public/images/blog/registered_model_with_feature_service.png differ diff --git a/infra/website/public/images/blog/sox_compliance_and_access.png b/infra/website/public/images/blog/sox_compliance_and_access.png new file mode 100644 index 00000000000..0236e5b5cdc Binary files /dev/null and b/infra/website/public/images/blog/sox_compliance_and_access.png differ diff --git a/infra/website/public/images/blog/sox_offline_store_audit_logs.png b/infra/website/public/images/blog/sox_offline_store_audit_logs.png new file mode 100644 index 00000000000..fa7be01ebcc Binary files /dev/null and b/infra/website/public/images/blog/sox_offline_store_audit_logs.png differ diff --git a/infra/website/src/components/Navigation.astro b/infra/website/src/components/Navigation.astro index a5987bf6348..143fcbc047f 100644 --- a/infra/website/src/components/Navigation.astro +++ b/infra/website/src/components/Navigation.astro @@ -14,11 +14,29 @@ COMMUNITY - +
@@ -38,7 +56,7 @@ top: 0; left: 0; right: 0; - background-color: white; + background-color: var(--color-nav-bg); z-index: 1000; } @@ -80,6 +98,35 @@ margin-left: 32px; } + .nav-right { + display: flex; + align-items: center; + gap: 4px; + padding-right: var(--content-padding); + } + + .theme-toggle { + background: none; + border: none; + padding: 8px; + cursor: pointer; + color: var(--color-text); + display: flex; + align-items: center; + justify-content: center; + opacity: 0.7; + transition: opacity 0.2s ease; + } + + .theme-toggle:hover { + opacity: 1; + } + + .icon-sun { display: none; } + .icon-moon { display: block; } + :global([data-theme="dark"]) .icon-sun { display: block; } + :global([data-theme="dark"]) .icon-moon { display: none; } + .mobile-menu-button { display: block; background: none; @@ -87,7 +134,6 @@ padding: 8px; cursor: pointer; color: var(--color-text); - margin-right: var(--content-padding); } @media (min-width: 1024px) { @@ -95,7 +141,7 @@ display: flex; align-items: center; } - + .mobile-menu-button { display: none; } @@ -104,9 +150,9 @@ .mobile-menu { display: none; width: 100%; - background: white; + background: var(--color-nav-bg); padding: 16px 0; - border-top: 1px solid #eee; + border-top: 1px solid var(--color-border); position: absolute; top: 52px; left: 0; @@ -122,17 +168,27 @@ } .mobile-menu a:hover { - background-color: #f5f5f5; + background-color: var(--color-nav-hover); } \ No newline at end of file diff --git a/infra/website/src/layouts/BaseLayout.astro b/infra/website/src/layouts/BaseLayout.astro index d73c7b0ebde..96e30a6aee2 100644 --- a/infra/website/src/layouts/BaseLayout.astro +++ b/infra/website/src/layouts/BaseLayout.astro @@ -4,9 +4,17 @@ import '../styles/global.css'; interface Props { title: string; description?: string; + socialImage?: string; + ogUrl?: string; } -const { title, description = "Feast is an end-to-end open source feature store for machine learning. It allows teams to define, manage, discover, and serve features." } = Astro.props; +const defaultSocialImage = "https://feast.dev/wp-content/uploads/2023/01/feast-og@2x.png"; +const { + title, + description = "Feast is an end-to-end open source feature store for machine learning. It allows teams to define, manage, discover, and serve features.", + socialImage = defaultSocialImage, + ogUrl = "https://feast.dev/", +} = Astro.props; --- @@ -25,16 +33,16 @@ const { title, description = "Feast is an end-to-end open source feature store f - + - + - + @@ -44,11 +52,21 @@ const { title, description = "Feast is an end-to-end open source feature store f - + +
- \ No newline at end of file + diff --git a/infra/website/src/layouts/BlogLayout.astro b/infra/website/src/layouts/BlogLayout.astro index c5f8379fc80..9fe68a9daa7 100644 --- a/infra/website/src/layouts/BlogLayout.astro +++ b/infra/website/src/layouts/BlogLayout.astro @@ -14,7 +14,7 @@ const { frontmatter } = Astro.props;

{frontmatter.title}

{frontmatter.date && ( -
diff --git a/infra/website/src/styles/global.css b/infra/website/src/styles/global.css index 95163f38941..d9916f6b66e 100644 --- a/infra/website/src/styles/global.css +++ b/infra/website/src/styles/global.css @@ -4,7 +4,9 @@ --font-mono: "IBM Plex Mono", monospace; --font-sans: "IBM Plex Sans", sans-serif; --color-text: rgb(0, 0, 0); + --color-text-secondary: #666; --color-background: #ffffff; + --color-surface: #f5f5f5; --color-primary: #0068c9; --color-border: #e5e5e5; --spacing-sm: 16px; @@ -14,6 +16,12 @@ --border-color: rgb(214, 214, 214); --content-max-width: 1200px; --content-padding: 20px; + --color-nav-bg: #ffffff; + --color-nav-hover: #f5f5f5; + --color-button-hover: #333; + --color-code-bg: #000; + --color-code-text: #e6edf3; + --color-logo-filter: none; --shiki-color-text: #e6edf3; --shiki-color-background: #000000; --shiki-token-constant: #79c0ff; @@ -27,6 +35,22 @@ --shiki-token-link: #a5d6ff; } +[data-theme="dark"] { + --color-text: rgb(230, 230, 230); + --color-text-secondary: #999; + --color-background: #0d0d0d; + --color-surface: #1a1a1a; + --color-primary: #4da3ff; + --color-border: #2a2a2a; + --border-color: #2a2a2a; + --color-nav-bg: #111111; + --color-nav-hover: #1a1a1a; + --color-button-hover: #ccc; + --color-code-bg: #111; + --color-code-text: #e6edf3; + --color-logo-filter: invert(1); +} + /* Common text rendering properties */ body, .text-smooth { -webkit-font-smoothing: antialiased; @@ -83,7 +107,7 @@ main::before { top: 0; left: 0; right: 0; - background-color: white; + background-color: var(--color-nav-bg); z-index: 1000; } @@ -160,9 +184,9 @@ main::before { .mobile-menu { display: none; width: 100%; - background: white; + background: var(--color-nav-bg); padding: 16px 0; - border-top: 1px solid #eee; + border-top: 1px solid var(--color-border); position: absolute; top: 52px; left: 0; @@ -178,7 +202,7 @@ main::before { } .mobile-menu a:hover { - background-color: #f5f5f5; + background-color: var(--color-nav-hover); } /* Hero section */ @@ -234,11 +258,11 @@ main::before { .button-primary { background-color: var(--color-text); - color: white; + color: var(--color-background); } .button-primary:hover { - background-color: #333; + background-color: var(--color-button-hover); } .hero-button { @@ -324,12 +348,12 @@ main::before { font-family: var(--font-sans); font-size: 16px; line-height: 24px; - color: #333; + color: var(--color-text-secondary); } /* Code Example */ .code-example { - background-color: #f5f5f5; + background-color: var(--color-surface); border-radius: 4px; padding: var(--spacing-md); margin-top: var(--spacing-lg); @@ -357,7 +381,7 @@ main::before { .features-section { padding-top: 80px; margin-top: 40px; - border-top: 1px solid #f0f0f0; + border-top: 1px solid var(--color-border); } /* Add a visual indicator to encourage scrolling */ @@ -392,13 +416,13 @@ main::before { } .metric-area { - fill: #ccc; - opacity: 0.6; + fill: var(--color-text-secondary); + opacity: 0.4; } .metric-line { fill: none; - stroke: black; + stroke: var(--color-text); stroke-width: 1; opacity: 0.8; } @@ -407,6 +431,7 @@ main::before { font-family: var(--font-mono); font-size: 12px; font-weight: 500; + fill: var(--color-text); } /* Metrics section */ @@ -452,7 +477,7 @@ main::before { .metric-label { font-family: var(--font-mono); font-size: 14px; - color: #666; + color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.02em; } @@ -516,7 +541,7 @@ main::before { font-family: var(--font-sans); font-size: 14px; line-height: 20px; - color: #666; + color: var(--color-text-secondary); margin: 0; } @@ -599,6 +624,22 @@ main::before { margin: 0 auto; } +[data-theme="dark"] .integration-logo { + filter: grayscale(100%) contrast(1.2) invert(1); +} + +[data-theme="dark"] .integration-logo:hover { + filter: grayscale(0%) contrast(1) brightness(1.2); +} + +[data-theme="dark"] .company-logo { + filter: var(--color-logo-filter); +} + +[data-theme="dark"] .logo img { + filter: var(--color-logo-filter); +} + /* Specific logo size adjustments */ .integration-item img[alt="SingleStore"] { max-width: 130px; @@ -619,7 +660,7 @@ main::before { font-size: 14px; font-weight: 600; text-align: center; - color: #666; + color: var(--color-text-secondary); opacity: 0.9; transition: opacity 0.2s ease; padding: 8px 0; @@ -655,7 +696,7 @@ main::before { } .code-block { - background-color: #000; + background-color: var(--color-code-bg); overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); margin: 0 auto; @@ -752,7 +793,7 @@ main::before { font-size: 12px; font-weight: 400; text-transform: uppercase; - color: #666; + color: var(--color-text-secondary); margin-bottom: 12px; } @@ -769,7 +810,7 @@ main::before { font-family: var(--font-sans); font-size: 14px; line-height: 20px; - color: #666; + color: var(--color-text-secondary); } /* CTA section */ @@ -815,7 +856,7 @@ main::before { font-family: var(--font-sans); font-size: 14px; line-height: 20px; - color: #666; + color: var(--color-text-secondary); margin: 0 auto 24px; max-width: 280px; } @@ -840,7 +881,7 @@ main::before { .cta-button:hover { background: var(--color-text); - color: white; + color: var(--color-background); } /* Bordered container */ diff --git a/java/datatypes/pom.xml b/java/datatypes/pom.xml index 967262d0e01..6579c707351 100644 --- a/java/datatypes/pom.xml +++ b/java/datatypes/pom.xml @@ -76,11 +76,8 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - - false - + org.sonatype.central + central-publishing-maven-plugin diff --git a/java/pom.xml b/java/pom.xml index d9625cecf49..bdb8978974f 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -35,7 +35,7 @@ - 0.56.0 + 0.65.0 https://github.com/feast-dev/feast UTF-8 @@ -46,9 +46,9 @@ 3.25.5 1.111.1 0.8.0 - 1.9.10 + 1.14.11 1.3 - 2.28.2 + 5.8.0 0.26.0 @@ -439,18 +439,15 @@ - + - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 + org.sonatype.central + central-publishing-maven-plugin + 0.6.0 true - ossrh - https://oss.sonatype.org/ - - true - true + central + true 5.5.2 - 2.28.2 + 5.8.0 0.33.0 @@ -125,12 +125,6 @@ ${mockito.version} test - - org.mockito - mockito-inline - ${mockito.version} - test - @@ -148,11 +142,8 @@ jacoco-maven-plugin - org.sonatype.plugins - nexus-staging-maven-plugin - - false - + org.sonatype.central + central-publishing-maven-plugin diff --git a/java/serving/pom.xml b/java/serving/pom.xml index 8a70d133c76..18183ab7c3a 100644 --- a/java/serving/pom.xml +++ b/java/serving/pom.xml @@ -414,10 +414,10 @@ - org.sonatype.plugins - nexus-staging-maven-plugin + org.sonatype.central + central-publishing-maven-plugin - true + true diff --git a/java/serving/src/test/resources/docker-compose/feast10/Dockerfile b/java/serving/src/test/resources/docker-compose/feast10/Dockerfile index 09a8d23faef..0a5342061f7 100644 --- a/java/serving/src/test/resources/docker-compose/feast10/Dockerfile +++ b/java/serving/src/test/resources/docker-compose/feast10/Dockerfile @@ -1,14 +1,14 @@ FROM python:3.11 +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + WORKDIR /app COPY sdk/python /mnt/feast/sdk/python COPY protos /mnt/feast/protos -COPY setup.py /mnt/feast/setup.py COPY pyproject.toml /mnt/feast/pyproject.toml COPY README.md /mnt/feast/README.md COPY Makefile /mnt/feast/Makefile ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 -RUN pip install uv RUN cd /mnt/feast && uv pip install --system .[grpcio,redis] COPY java/serving/src/test/resources/docker-compose/feast10/ . EXPOSE 8080 diff --git a/java/serving/src/test/resources/docker-compose/feast10/definitions.py b/java/serving/src/test/resources/docker-compose/feast10/definitions.py index 769ac155452..580605fe149 100644 --- a/java/serving/src/test/resources/docker-compose/feast10/definitions.py +++ b/java/serving/src/test/resources/docker-compose/feast10/definitions.py @@ -9,7 +9,6 @@ from feast.field import Field from feast.on_demand_feature_view import on_demand_feature_view from feast.types import Float32, Float64, Int64 -from feast.value_type import ValueType from feast import FileSource file_path = "driver_stats.parquet" @@ -70,7 +69,8 @@ def transformed_conv_rate(features_df: pd.DataFrame) -> pd.DataFrame: generated_data_source = FileSource( - path="benchmark_data.parquet", timestamp_field="event_timestamp", + path="benchmark_data.parquet", + timestamp_field="event_timestamp", ) entity = Entity(name="entity") @@ -88,5 +88,6 @@ def transformed_conv_rate(features_df: pd.DataFrame) -> pd.DataFrame: benchmark_feature_views.append(fv) benchmark_feature_service = FeatureService( - name=f"benchmark_feature_service", features=benchmark_feature_views, + name="benchmark_feature_service", + features=benchmark_feature_views, ) diff --git a/java/serving/src/test/resources/docker-compose/feast10/materialize.py b/java/serving/src/test/resources/docker-compose/feast10/materialize.py index 404fec27e12..3307805f2a5 100644 --- a/java/serving/src/test/resources/docker-compose/feast10/materialize.py +++ b/java/serving/src/test/resources/docker-compose/feast10/materialize.py @@ -55,7 +55,7 @@ def generate_data(num_rows: int, num_features: int, destination: str) -> pd.Data df.to_parquet(destination) -generate_data(10 ** 3, 250, "benchmark_data.parquet") +generate_data(10**3, 250, "benchmark_data.parquet") fs = FeatureStore(".") diff --git a/java/serving/src/test/resources/docker-compose/feast10/setup_it.py b/java/serving/src/test/resources/docker-compose/feast10/setup_it.py index 61aaa6fec8e..ad1cab07da4 100644 --- a/java/serving/src/test/resources/docker-compose/feast10/setup_it.py +++ b/java/serving/src/test/resources/docker-compose/feast10/setup_it.py @@ -41,9 +41,7 @@ def setup_data(): # Please read more in Feast RFC-031 # (link https://docs.google.com/document/d/12UuvTQnTTCJhdRgy6h10zSbInNGSyEJkIxpOcgOen1I/edit) # about this benchmark setup - def generate_data( - num_rows, num_features, destination - ): + def generate_data(num_rows, num_features, destination): features = [f"feature_{i}" for i in range(num_features)] columns = ["entity", "event_timestamp"] + features df = pd.DataFrame(0, index=np.arange(num_rows), columns=columns) diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 00000000000..ec4c79ff7c3 --- /dev/null +++ b/pixi.lock @@ -0,0 +1,7660 @@ +version: 7 +platforms: +- name: linux-64 + virtual-packages: + - __unix=0=0 + - __linux=4.18 + - __glibc=2.28 + - __archspec=0=x86_64 +- name: osx-64 + virtual-packages: + - __unix=0=0 + - __osx=13.0 + - __archspec=0=x86_64 +- name: osx-arm64 + virtual-packages: + - __unix=0=0 + - __osx=13.0 + - __archspec=0=m1 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.6-habeac84_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: 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 + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: 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 + - pypi: https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/5e/2a902317d7fc4aa93236e80c932662dadfc459b323d758329e01775125e1/numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.6-h7c6738f_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/ad/abc44aaceaf7b17ee1edde2bbb4458da591bc79574cffff50c4bb35f00d1/numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.3-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.6-h156bc91_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/39/b72e168daf9c00fb20c9fc996d00437ccecdef3102387775d29d7a62576d/numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + duckdb-tests: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.20-h267e890_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/23/1b/545eee1c18f3af4cf140bb5822b6ef81ebe569df0a63ac109973103a30a5/wrapt-2.2.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/89/dda79527bb7573ba91828b2fb91b3105d87378d6a2749ca0c0924ce0addd/coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/c3/94ade4906a2f88bc935772f59c934013b4205e773bcb4239db114a6da136/pyarrow_hotfix-0.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/7d/5c0cc66fb90a1b14474eebb5ab535eacc51cb20b0e45358348b51c07abc9/duckdb-1.5.4-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6e/24/e6b7a8fe8b9e336d684779a88027b261374417f2be7c5a0fcdb40f0c8cc5/deltalake-0.25.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/fc/8cb9073bb1bee54eb49a1ae501a36402d01763812962ac811cdc1c81a9d7/parsy-2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: 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 + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/c0/271f3e1e3502a8decb8ee5c680dbed2d8dc2cd504f5e20f7ed491d5f37e1/atpublic-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/b3/11d406849715b47c9d69bb22f50874f80caee96bd1cbe7b61abbebbf5a05/ibis_framework-12.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: 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 + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c6/b1/e20d5f0d19c4c0f3df213fa7dcfa0942c4fb127d38e11f398ae8ddf6cccc/mmh3-5.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/9e/82a390ecc85f066ff80affa01d195f744e3de60ad4d695b8de31c9a66da3/sqlglot-30.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/e4/77487e14fc7be47180fd0eb4267c7486d0cc59b74031839a3daf8650136b/httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/11/402295b388dd35861007f8a26a37c2e2f284212d57bdf407c31f36043746/grpcio-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.20-hea035f4_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/03/060ce69008ac97bbc01b1411b3e55b61f6f015659400b46749b662107831/coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/c3/94ade4906a2f88bc935772f59c934013b4205e773bcb4239db114a6da136/pyarrow_hotfix-0.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/b9/be66eb0decd730d89b9c94f930e4b8d87787b05724bb84af98bfd825f72c/httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/73/18/9da267ade389d4e7e533ac0c77b3a7041513a66efab93beb84f27627b0b8/duckdb-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/fc/8cb9073bb1bee54eb49a1ae501a36402d01763812962ac811cdc1c81a9d7/parsy-2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/41/9c95232b94b219ed8b14029d9cd000e0381cafba869c451dda60af84f4ba/grpcio-1.81.1-cp310-cp310-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/96/c0/271f3e1e3502a8decb8ee5c680dbed2d8dc2cd504f5e20f7ed491d5f37e1/atpublic-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/b3/11d406849715b47c9d69bb22f50874f80caee96bd1cbe7b61abbebbf5a05/ibis_framework-12.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cc/bf/5404c2fd6ac84819e8ff1b7e34437b37cf55a2b11318894909e7bb88de3f/mmh3-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/9e/82a390ecc85f066ff80affa01d195f744e3de60ad4d695b8de31c9a66da3/sqlglot-30.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/0e/f372bb290cef68c67331cd649b94d62220183ddc1b5bf3a9351ea6e9c8ec/deltalake-0.25.5-cp39-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/8b/59781d0fe7b0adfbea37f600857de4be68921e454aeecf1a11bda35cdccc/wrapt-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.3-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.20-hac0b6dc_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/c3/94ade4906a2f88bc935772f59c934013b4205e773bcb4239db114a6da136/pyarrow_hotfix-0.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/7a/ec22ff9d5c891b4f9ae834ef70524c92bd59d1408e9944e2652c87bc3f02/deltalake-0.25.5-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/b4/ad73c1a396288e443b18af50819448060b318c1e933305167c1d7f98a507/duckdb-1.5.4-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/77/fc/8cb9073bb1bee54eb49a1ae501a36402d01763812962ac811cdc1c81a9d7/parsy-2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/41/9c95232b94b219ed8b14029d9cd000e0381cafba869c451dda60af84f4ba/grpcio-1.81.1-cp310-cp310-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/dc/66c61aca927230c9cf97a3cb005c803971a1076ff9f7d61085d035c20085/wrapt-2.2.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/96/c0/271f3e1e3502a8decb8ee5c680dbed2d8dc2cd504f5e20f7ed491d5f37e1/atpublic-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/b3/11d406849715b47c9d69bb22f50874f80caee96bd1cbe7b61abbebbf5a05/ibis_framework-12.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/f7/b4d41eaae2869d31356bc4bbf546f44fae83ff298af0a043ca0625b06773/httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/db/9e/82a390ecc85f066ff80affa01d195f744e3de60ad4d695b8de31c9a66da3/sqlglot-30.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/0b/52bffad0b52ae4ea53e222b594bd38c08ecac1fc410323220a7202e43da5/mmh3-5.2.1-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/a3/d936e8b53edd9684100a6aefaf3fcabaa54728fe33324436c8d279c047aa/coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + ray-tests: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.20-h267e890_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/ad/0b9cc9f38a7324a7eb1d80f834eaa5283d17e9271bbda3186e598dddaeac/yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/66/73034ad30b59f13439b75e620989dacba4c047256e358ba7c2e9ec98ea22/datasets-5.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/23/1b/545eee1c18f3af4cf140bb5822b6ef81ebe569df0a63ac109973103a30a5/wrapt-2.2.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/89/dda79527bb7573ba91828b2fb91b3105d87378d6a2749ca0c0924ce0addd/coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2b/9d/93294c3045775c708ac8310eb3d3622a11d2951345ad590d532d62a1faa4/aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: 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 + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/e6/d80a2fcbd80f024d8e74a579aad538c5a24c6b672e6ce8180a9a8bfc2231/xxhash-3.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: 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 + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b4/64/640f5525bac171282c6f76f3ecc9c4cfef60149ac0d00231afb22018ebe5/ray-2.55.1-cp310-cp310-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c6/b1/e20d5f0d19c4c0f3df213fa7dcfa0942c4fb127d38e11f398ae8ddf6cccc/mmh3-5.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/e4/77487e14fc7be47180fd0eb4267c7486d0cc59b74031839a3daf8650136b/httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/a9/39cf856d03690af6fd570cf40331f1f79acdbb3132a9c35d2c5002f7f30b/multiprocess-0.70.17-py310-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/11/402295b388dd35861007f8a26a37c2e2f284212d57bdf407c31f36043746/grpcio-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.20-hea035f4_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/66/73034ad30b59f13439b75e620989dacba4c047256e358ba7c2e9ec98ea22/datasets-5.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/03/060ce69008ac97bbc01b1411b3e55b61f6f015659400b46749b662107831/coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/68/4ae5b4e08943f316594bb68da89957d3baf5760588fa09509594bd777e4b/aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/b9/be66eb0decd730d89b9c94f930e4b8d87787b05724bb84af98bfd825f72c/httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/41/7daafb32dd7562bf45b1ce56562e7e1a9146f6479b6456873eb8a3413c40/yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d1/36cdfc7d9a5cdcebb2ff3eeeaebae2c51a7aca50de27a44520af4d6923fa/xxhash-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/c5/c2ceba832fe3f47cfd7e11cd7cc7a1bbc2c028424c5bca70435aa4ca1dec/ray-2.49.2-cp310-cp310-macosx_12_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/41/9c95232b94b219ed8b14029d9cd000e0381cafba869c451dda60af84f4ba/grpcio-1.81.1-cp310-cp310-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cc/bf/5404c2fd6ac84819e8ff1b7e34437b37cf55a2b11318894909e7bb88de3f/mmh3-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/8b/59781d0fe7b0adfbea37f600857de4be68921e454aeecf1a11bda35cdccc/wrapt-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/a9/39cf856d03690af6fd570cf40331f1f79acdbb3132a9c35d2c5002f7f30b/multiprocess-0.70.17-py310-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.3-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.20-hac0b6dc_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/05/66/73034ad30b59f13439b75e620989dacba4c047256e358ba7c2e9ec98ea22/datasets-5.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/d0/a85097dd53aaca1a44acc4dd0b3d2c0e9233179433e2ee326e4018ab3cf7/ray-2.55.1-cp310-cp310-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/41/9c95232b94b219ed8b14029d9cd000e0381cafba869c451dda60af84f4ba/grpcio-1.81.1-cp310-cp310-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/f3439475537ca4c59e9b8cbc2b934672d1965b13b6e5fb32b1796c76e517/xxhash-3.8.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/94/dc/66c61aca927230c9cf97a3cb005c803971a1076ff9f7d61085d035c20085/wrapt-2.2.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/f7/b4d41eaae2869d31356bc4bbf546f44fae83ff298af0a043ca0625b06773/httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a8/8f/7b3ec212f1ea0683f55f978e3246bc313c38818664edfc97a9f349a4901e/yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/c1/316c8f3549dbe5245f92bfd523ec6f32dd4d98cafe21df3f6a19b1184c75/aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/0b/52bffad0b52ae4ea53e222b594bd38c08ecac1fc410323220a7202e43da5/mmh3-5.2.1-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/a9/39cf856d03690af6fd570cf40331f1f79acdbb3132a9c35d2c5002f7f30b/multiprocess-0.70.17-py310-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/a3/d936e8b53edd9684100a6aefaf3fcabaa54728fe33324436c8d279c047aa/coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl + registration-tests: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.20-h267e890_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/ad/0b9cc9f38a7324a7eb1d80f834eaa5283d17e9271bbda3186e598dddaeac/yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/06/3d/2f0e9c5cbc456d34f48215645d876f7885a15e09a72a07d1de3ddb181c38/pandas_gbq-0.35.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/23/1b/545eee1c18f3af4cf140bb5822b6ef81ebe569df0a63ac109973103a30a5/wrapt-2.2.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/c9/4e9cd249afc101ac283943295fe3359bdd711a0bb8c667752eb0da80609d/hiredis-3.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/89/dda79527bb7573ba91828b2fb91b3105d87378d6a2749ca0c0924ce0addd/coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2b/9d/93294c3045775c708ac8310eb3d3622a11d2951345ad590d532d62a1faa4/aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/d3/d7dff0d58a9e9244b48044bfb6a898bfcc8ecc42e0031d1bebc695344725/google_auth_oauthlib-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/2e/2677f3f93dae0497e7e33b6637302e7f3744efc553f34231183e32584885/redis-7.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/39/679e7b08ad313ad600b98b06395274cbef5ab51a2b0b7f68c7bb001c9717/grpcio_testing-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5e/71/4dd20c69332a2a4bf7ece8a655c9da98e4bd9b6bcea235349c1a00399d57/pyspark-4.1.2.tar.gz + - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/19/996b45846fcc5f2015bd70ed98420b11c847c1b79b57b82b250e0a409f49/snowflake_connector_python-4.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: 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 + - pypi: https://files.pythonhosted.org/packages/7a/1e/8b098aeb69ef87de5195d076ce6d82002c3ebf1920373d386ca7b6e63cfa/google_cloud_bigquery-3.42.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/9a/f088207b4cd6772f9e0d8a91807e79fa2458d4eb9eb1ae406c68415f2bec/psycopg_binary-3.3.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/95/b2/fd5a3e55a5f698fd527f783ce27368e8e9c3d872bd1e1c39dd377b2b9f14/google_cloud_datastore-2.25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/0e/c21d79625c155acc31a4a55a9b2c04e25f3b45225bc2691612084da0b7d2/google_cloud_bigtable-2.40.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: 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 + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/5a/db02b5e6633fbe49eaf4e3194bc64ec031e6436a0cfcc610cbda4f1b6a24/types_pymysql-1.1.0.20260518-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b3/a0/3e6a0b1c1ea6bec76f71473727ef27abf3cd40e9709b3ebcbfbcfaae6f79/boto3-1.43.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bd/db/ea0203e495be491c85af87b66e37acfd3bf756fd985f87e46fc5e3bf022c/py4j-0.10.9.9-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/7a/797ed371bde520223e49dbbe0f8762fa4cc724d0e21fc6af944bb8f82150/db_dtypes-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/b1/e20d5f0d19c4c0f3df213fa7dcfa0942c4fb127d38e11f398ae8ddf6cccc/mmh3-5.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/cb/cdeaba62aa3c48f0d8834afb82b4a21463cd83df34fe01f9daa89a08ec6c/pydata_google_auth-1.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/94/6db383d8ee1adf45dc6c73477152b82731fa4c4a46d9c1932cc8757e0fd4/google_cloud_storage-2.19.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/5e/5abfec5f7e89d3b7993d57cfb025ca5f968a2c18656d7fcda2b6919440b9/grpcio_status-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/e4/77487e14fc7be47180fd0eb4267c7486d0cc59b74031839a3daf8650136b/httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/f6/4157466c10181907d07786fb41df5d0a9ff339c1770b9e2a15cfe483e845/google_cloud_bigquery_storage-2.39.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/11/402295b388dd35861007f8a26a37c2e2f284212d57bdf407c31f36043746/grpcio-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h8f8c405_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.20-hea035f4_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/06/3d/2f0e9c5cbc456d34f48215645d876f7885a15e09a72a07d1de3ddb181c38/pandas_gbq-0.35.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/03/060ce69008ac97bbc01b1411b3e55b61f6f015659400b46749b662107831/coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/68/4ae5b4e08943f316594bb68da89957d3baf5760588fa09509594bd777e4b/aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/d3/d7dff0d58a9e9244b48044bfb6a898bfcc8ecc42e0031d1bebc695344725/google_auth_oauthlib-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/b9/be66eb0decd730d89b9c94f930e4b8d87787b05724bb84af98bfd825f72c/httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/41/7daafb32dd7562bf45b1ce56562e7e1a9146f6479b6456873eb8a3413c40/yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/49/4d/7e6a9088381386b4cfae4c5d1d23ea0c3618ca694bc2290118737af59f36/snowflake_connector_python-4.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/4a/2e/2677f3f93dae0497e7e33b6637302e7f3744efc553f34231183e32584885/redis-7.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/39/679e7b08ad313ad600b98b06395274cbef5ab51a2b0b7f68c7bb001c9717/grpcio_testing-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/71/4dd20c69332a2a4bf7ece8a655c9da98e4bd9b6bcea235349c1a00399d57/pyspark-4.1.2.tar.gz + - pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6f/75/2a08a6062228720747570a07ab42e6f5826725f09c9a95d75b7b5b938022/hiredis-3.4.0-cp310-cp310-macosx_10_15_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/1e/8b098aeb69ef87de5195d076ce6d82002c3ebf1920373d386ca7b6e63cfa/google_cloud_bigquery-3.42.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/41/9c95232b94b219ed8b14029d9cd000e0381cafba869c451dda60af84f4ba/grpcio-1.81.1-cp310-cp310-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/95/b2/fd5a3e55a5f698fd527f783ce27368e8e9c3d872bd1e1c39dd377b2b9f14/google_cloud_datastore-2.25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/0e/c21d79625c155acc31a4a55a9b2c04e25f3b45225bc2691612084da0b7d2/google_cloud_bigtable-2.40.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/5a/db02b5e6633fbe49eaf4e3194bc64ec031e6436a0cfcc610cbda4f1b6a24/types_pymysql-1.1.0.20260518-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/a0/3e6a0b1c1ea6bec76f71473727ef27abf3cd40e9709b3ebcbfbcfaae6f79/boto3-1.43.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/bf/70d8a60488f9955cbbcd538beae44d56bb2f1d19e673b72788f2d343ff55/psycopg_binary-3.3.4-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/bd/db/ea0203e495be491c85af87b66e37acfd3bf756fd985f87e46fc5e3bf022c/py4j-0.10.9.9-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/7a/797ed371bde520223e49dbbe0f8762fa4cc724d0e21fc6af944bb8f82150/db_dtypes-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/cb/cdeaba62aa3c48f0d8834afb82b4a21463cd83df34fe01f9daa89a08ec6c/pydata_google_auth-1.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cc/bf/5404c2fd6ac84819e8ff1b7e34437b37cf55a2b11318894909e7bb88de3f/mmh3-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/94/6db383d8ee1adf45dc6c73477152b82731fa4c4a46d9c1932cc8757e0fd4/google_cloud_storage-2.19.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e1/8b/59781d0fe7b0adfbea37f600857de4be68921e454aeecf1a11bda35cdccc/wrapt-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/5e/5abfec5f7e89d3b7993d57cfb025ca5f968a2c18656d7fcda2b6919440b9/grpcio_status-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/eb/f6/4157466c10181907d07786fb41df5d0a9ff339c1770b9e2a15cfe483e845/google_cloud_bigquery_storage-2.39.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.3-h1b79a29_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.20-hac0b6dc_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - pypi: ./ + - pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/06/3d/2f0e9c5cbc456d34f48215645d876f7885a15e09a72a07d1de3ddb181c38/pandas_gbq-0.35.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/d3/d7dff0d58a9e9244b48044bfb6a898bfcc8ecc42e0031d1bebc695344725/google_auth_oauthlib-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/49/4d/7e6a9088381386b4cfae4c5d1d23ea0c3618ca694bc2290118737af59f36/snowflake_connector_python-4.6.0.tar.gz + - pypi: https://files.pythonhosted.org/packages/4a/2e/2677f3f93dae0497e7e33b6637302e7f3744efc553f34231183e32584885/redis-7.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4d/39/679e7b08ad313ad600b98b06395274cbef5ab51a2b0b7f68c7bb001c9717/grpcio_testing-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/71/4dd20c69332a2a4bf7ece8a655c9da98e4bd9b6bcea235349c1a00399d57/pyspark-4.1.2.tar.gz + - pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7a/1e/8b098aeb69ef87de5195d076ce6d82002c3ebf1920373d386ca7b6e63cfa/google_cloud_bigquery-3.42.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/80/41/9c95232b94b219ed8b14029d9cd000e0381cafba869c451dda60af84f4ba/grpcio-1.81.1-cp310-cp310-macosx_11_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/dc/66c61aca927230c9cf97a3cb005c803971a1076ff9f7d61085d035c20085/wrapt-2.2.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/95/b2/fd5a3e55a5f698fd527f783ce27368e8e9c3d872bd1e1c39dd377b2b9f14/google_cloud_datastore-2.25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/0e/c21d79625c155acc31a4a55a9b2c04e25f3b45225bc2691612084da0b7d2/google_cloud_bigtable-2.40.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9d/f7/b4d41eaae2869d31356bc4bbf546f44fae83ff298af0a043ca0625b06773/httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a8/8f/7b3ec212f1ea0683f55f978e3246bc313c38818664edfc97a9f349a4901e/yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/5a/db02b5e6633fbe49eaf4e3194bc64ec031e6436a0cfcc610cbda4f1b6a24/types_pymysql-1.1.0.20260518-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/a0/3e6a0b1c1ea6bec76f71473727ef27abf3cd40e9709b3ebcbfbcfaae6f79/boto3-1.43.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/db/ea0203e495be491c85af87b66e37acfd3bf756fd985f87e46fc5e3bf022c/py4j-0.10.9.9-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/7a/797ed371bde520223e49dbbe0f8762fa4cc724d0e21fc6af944bb8f82150/db_dtypes-1.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/cb/cdeaba62aa3c48f0d8834afb82b4a21463cd83df34fe01f9daa89a08ec6c/pydata_google_auth-1.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/c1/316c8f3549dbe5245f92bfd523ec6f32dd4d98cafe21df3f6a19b1184c75/aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/94/6db383d8ee1adf45dc6c73477152b82731fa4c4a46d9c1932cc8757e0fd4/google_cloud_storage-2.19.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/db/b0/29e98ba210c9dbc75a6dc91e3f99b9e06ea901a62ca95804e02a1ae13e6b/psycopg_binary-3.3.4-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/0b/52bffad0b52ae4ea53e222b594bd38c08ecac1fc410323220a7202e43da5/mmh3-5.2.1-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/3d/28c61f9c628dfaf1f96bb8b8592cb006eaad3747248c95f9ae7f694abb47/hiredis-3.4.0-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/5e/5abfec5f7e89d3b7993d57cfb025ca5f968a2c18656d7fcda2b6919440b9/grpcio_status-1.81.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/eb/f6/4157466c10181907d07786fb41df5d0a9ff339c1770b9e2a15cfe483e845/google_cloud_bigquery_storage-2.39.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fc/a3/d936e8b53edd9684100a6aefaf3fcabaa54728fe33324436c8d279c047aa/coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c + md5: 18335a698559cdbcd86150a48bf54ba6 + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 728002 + timestamp: 1774197446916 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + sha256: 16feffd9ddbbe5b718515d38ee376c685ba95491cd901244e24671d20b952a77 + md5: b24d3c612f71e7aa74158d92106318b2 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + purls: [] + size: 77856 + timestamp: 1781203599810 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + sha256: 8e0a3b5e41272e5678499b5dfc4cddb673f9e935de01eb0767ce857001229f46 + md5: 57736f29cc2b0ec0b6c2952d3f101b6a + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_19 + - libgomp 15.2.0 he0feb66_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1041084 + timestamp: 1778269013026 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + sha256: 9dcf54adfaa5e861123c2da4f2f0451a685464ea7e5a41ad91cf67b31d658d98 + md5: 331ee9b72b9dff570d56b1302c5ab37d + depends: + - libgcc 15.2.0 he0feb66_19 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 27694 + timestamp: 1778269016987 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + sha256: 5abe4ab9d93f6c9757d654f1969ae2267d4505315c1f2f8fe705fd60af084f1b + md5: faac990cb7aedc7f3a2224f2c9b0c26c + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 603817 + timestamp: 1778268942614 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d + md5: b88d90cad08e6bc8ad540cb310a761fb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 113478 + timestamp: 1775825492909 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 + md5: 2c21e66f50753a083cbe6b80f38268fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL + purls: [] + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.3-h0c1763c_0.conda + sha256: 365376f4815e5e80def2b3462a2419708b7c292da0da85278386c2618621fff4 + md5: 4aed8e657e9ff156bdbe849b4df44389 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 962119 + timestamp: 1782519076616 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda + sha256: 9b1bdce27a7e31f7d241aeecff67a1f3101d52a2b1e33ccc2cdf2613072bf81f + md5: 01bb81d12c957de066ea7362007df642 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 40017 + timestamp: 1781625522462 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + purls: [] + size: 100393 + timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 63629 + timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 + md5: fc21868a1a5aacc937e7a18747acb8a5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: X11 AND BSD-3-Clause + purls: [] + size: 918956 + timestamp: 1777422145199 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + sha256: d48f5c22b9897c01e4dff3680f1f57ceb02711ab9c62f74339b080419dfad34b + md5: 79dd2074b5cd5c5c6b2930514a11e22d + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3159683 + timestamp: 1781069855778 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.20-h267e890_1_cpython.conda + build_number: 1 + sha256: c15d8585b7a52fdb734bd16dbdcae4b81ed59268862d3a2588eb8ed69c8cbc52 + md5: c5eace1c2d8dae0bb08c094617ea8cc7 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.4,<4.0a0 + - libgcc >=14 + - liblzma >=5.8.3,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libuuid >=2.42.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.10.* *_cp310 + license: Python-2.0 + purls: [] + size: 25403213 + timestamp: 1781149348162 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.6-habeac84_100_cp314.conda + build_number: 100 + sha256: 6d28ac2b061179deb434d3d57afa98ffd20ec3c5d44ab8048a1ca33424b22d38 + md5: 0b9b2f83b5b600e1ac38becde8d0dd44 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libuuid >=2.42.1,<3.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 36717183 + timestamp: 1781255094700 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda + sha256: f8e3c730fa14ee3f170493779f06522c4acf89169f43db4f039727709b6419cf + md5: a9965dd99f683c5f444428f896635716 + depends: + - __unix + license: ISC + purls: [] + size: 128866 + timestamp: 1781708962055 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + build_number: 8 + sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 + md5: 0539938c55b6b1a59b560e843ad864a4 + constrains: + - python 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6989 + timestamp: 1752805904792 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda + sha256: 9f242f13537ef1ce195f93f0cc162965d6cc79da578568d6d8e50f70dd025c42 + md5: 4173ac3b19ec0a4f400b4f782910368b + depends: + - __osx >=10.13 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 133427 + timestamp: 1771350680709 +- conda: https://conda.anaconda.org/conda-forge/osx-64/icu-78.3-h25d91c4_0.conda + sha256: 1294117122d55246bb83ad5b589e2a031aacdf2d0b1f99fd338aa4394f881735 + md5: 627eca44e62e2b665eeec57a984a7f00 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 12273764 + timestamp: 1773822733780 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.8.1-hcc62823_1.conda + sha256: 9c96cc05e056e1bba5b545cbbd57b6e01db622dc2c82934caaaa25cfb22fe666 + md5: dcfdea7b7013beef0a4d744d776ea38f + depends: + - __osx >=11.0 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + purls: [] + size: 76020 + timestamp: 1781204303305 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.5.2-hd1f9c09_0.conda + sha256: 951958d1792238006fdc6fce7f71f1b559534743b26cc1333497d46e5903a2d6 + md5: 66a0dc7464927d0853b590b6f53ba3ea + depends: + - __osx >=10.13 + license: MIT + license_family: MIT + purls: [] + size: 53583 + timestamp: 1769456300951 +- conda: https://conda.anaconda.org/conda-forge/osx-64/liblzma-5.8.3-hbb4bfdb_0.conda + sha256: d9e2006051529aec5578c6efeb13bb6a7200a014b2d5a77a579e83a8049d5f3c + md5: becdfbfe7049fa248e52aa37a9df09e2 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 105724 + timestamp: 1775826029494 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libmpdec-4.0.0-hf3981d6_1.conda + sha256: 1096c740109386607938ab9f09a7e9bca06d86770a284777586d6c378b8fb3fd + md5: ec88ba8a245855935b871a7324373105 + depends: + - __osx >=10.13 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 79899 + timestamp: 1769482558610 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.53.3-h8f8c405_0.conda + sha256: 84d4af77021ca28e02ff60f396575b921ed1747e459838daa0e73b4724b47953 + md5: 72d9eaef59342a3614c83d57a32f578b + depends: + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 1012648 + timestamp: 1782519619230 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.2-hbb4bfdb_2.conda + sha256: 4c6da089952b2d70150c74234679d6f7ac04f4a98f9432dec724968f912691e7 + md5: 30439ff30578e504ee5e0b390afc8c65 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 59000 + timestamp: 1774073052242 +- conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.6-hcc0dc9a_0.conda + sha256: f5f7e006ff4271305ab4cc08eedd855c67a571793c3d18aff73f645f088a8cae + md5: 31b8740cf1b2588d4e61c81191004061 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 831711 + timestamp: 1777423052277 +- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.6.3-hc881268_0.conda + sha256: 819d4368d6b5b298fa40d4bc836c1250842489002cacf3fb918a13ee2033b7c6 + md5: 46be42ab403712fd349d007d763bf767 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 2775300 + timestamp: 1781071391999 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.20-hea035f4_1_cpython.conda + build_number: 1 + sha256: 9bc83a907d13a532f3a38ddc666a58d612cf548347d5e8eec2ce1ad1dacbe420 + md5: b0564ca60a54a4087fcd11326e1169e2 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.4,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.10.* *_cp310 + license: Python-2.0 + purls: [] + size: 13071051 + timestamp: 1781151393975 +- conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.14.6-h7c6738f_100_cp314.conda + build_number: 100 + sha256: f8261699d80fb6e653fc56c9b89ca4c3dd1aa374a10d11af64a089cf4b2b0d4a + md5: ecfbc87d80647d5076839d8d1006ac5f + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 14368118 + timestamp: 1781256031540 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + sha256: 4614af680aa0920e82b953fece85a03007e0719c3399f13d7de64176874b80d5 + md5: eefd65452dfe7cce476a519bece46704 + depends: + - __osx >=10.13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 317819 + timestamp: 1765813692798 +- conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h7142dee_3.conda + sha256: 7f0d9c320288532873e2d8486c331ec6d87919c9028208d3f6ac91dc8f99a67b + md5: 6e6efb7463f8cef69dbcb4c2205bf60e + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3282953 + timestamp: 1769460532442 +- conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h3eecb57_6.conda + sha256: 47101a4055a70a4876ffc87b750ab2287b67eca793f21c8224be5e1ee6394d3f + md5: 727109b184d680772e3122f40136d5ca + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 528148 + timestamp: 1764777156963 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df + md5: 620b85a3f45526a8bc4d23fd78fc22f0 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 124834 + timestamp: 1771350416561 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.1-hf6b4638_1.conda + sha256: 5af74261101e3c777399c6294b2b5d290e508153268eb2e9ff99c4d69834612f + md5: a915151d5d3c5bf039f5ccc8402a436f + depends: + - __osx >=11.0 + constrains: + - expat 2.8.1.* + license: MIT + license_family: MIT + purls: [] + size: 69362 + timestamp: 1781203631990 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 + md5: 43c04d9cb46ef176bb2a4c77e324d599 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40979 + timestamp: 1769456747661 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + sha256: 34878d87275c298f1a732c6806349125cebbf340d24c6c23727268184bba051e + md5: b1fd823b5ae54fbec272cea0811bd8a9 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.3.* + license: 0BSD + purls: [] + size: 92472 + timestamp: 1775825802659 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + sha256: 1089c7f15d5b62c622625ec6700732ece83be8b705da8c6607f4dabb0c4bd6d2 + md5: 57c4be259f5e0b99a5983799a228ae55 + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 73690 + timestamp: 1769482560514 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.3-h1b79a29_0.conda + sha256: a73a8acd97a6599fd6e561514db9f101ca7fd984cdc0cfd91ba74c8aa9dbe067 + md5: 7184d95871a58b8258a8ea124ed5aabc + depends: + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: blessing + purls: [] + size: 924912 + timestamp: 1782519136322 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05 + md5: bc5a5721b6439f2f62a84f2548136082 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + purls: [] + size: 47759 + timestamp: 1774072956767 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + sha256: 4ea6c620b87bd1d42bb2ccc2c87cd2483fa2d7f9e905b14c223f11ff3f4c455d + md5: 343d10ed5b44030a2f67193905aea159 + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 805509 + timestamp: 1777423252320 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + sha256: b3e3ca895c336d4eb91c5d2f244a312bdb59a0de8cfa0cc4c179225ab2f6bbfb + md5: 8187a86242741725bfa74785fe812979 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3102584 + timestamp: 1781069820667 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.20-hac0b6dc_1_cpython.conda + build_number: 1 + sha256: 3c9e084162759c4029212b96147a179b0ad8076abfca85f00984d2aaa10c70f9 + md5: 7f498ade7b9aa9e327ad23931e6c6d4a + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.4,<4.0a0 + - liblzma >=5.8.3,<6.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.10.* *_cp310 + license: Python-2.0 + purls: [] + size: 12888297 + timestamp: 1781148720732 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.14.6-h156bc91_100_cp314.conda + build_number: 100 + sha256: 984081c9fae3a3944c6f2707bbbbc70e8b961f02cdb7c640d9745e2636235632 + md5: 4841be3d0cf616a860efc6e60af66f8b + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.8.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.3,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.53.2,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - ncurses >=6.6,<7.0a0 + - openssl >=3.5.7,<4.0a0 + - python_abi 3.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + purls: [] + size: 14059371 + timestamp: 1781254578985 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 313930 + timestamp: 1765813902568 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + sha256: 799cab4b6cde62f91f750149995d149bc9db525ec12595e8a1d91b9317f038b3 + md5: a9d86bc62f39b94c4661716624eb21b0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3127137 + timestamp: 1769460817696 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + sha256: 9485ba49e8f47d2b597dd399e88f4802e100851b27c21d7525625b0b4025a5d9 + md5: ab136e4c34e97f34fb621d2592a393d8 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 433413 + timestamp: 1764777166076 +- pypi: ./ + name: feast + requires_dist: + - attrs + - click>=7.0.0,<9.0.0 + - colorama>=0.3.9,<1 + - dill~=0.3.0 + - protobuf>=4.24.0 + - jinja2>=2,<4 + - mmh3 + - numpy>=2.0.0,<3 + - pandas>=1.4.3,<3 + - pyarrow>=16.1.0 + - pydantic>=2.10.6 + - pygments>=2.12.0,<3 + - pyyaml>=5.4.0,<7 + - requests + - sqlalchemy[mypy]>1 + - tabulate>=0.8.0,<1 + - tenacity>=7,<9 + - toml>=0.10.0,<1 + - tqdm>=4,<5 + - typeguard>=4.0.0 + - fastapi>=0.68.0 + - starlette>=1.0.1 + - uvicorn[standard]>=0.30.6 + - uvicorn-worker + - gunicorn ; sys_platform != 'win32' + - dask[dataframe]>=2024.2.1 + - prometheus-client>=0.20.0,<0.25.0 + - psutil + - bigtree>=0.19.2 + - pyjwt>=2.13.0 + - aerospike>=19.0.0,<20.0.0 ; extra == 'aerospike' + - boto3>=1.38.27 ; extra == 'aws' + - fsspec>=2024.1.0 ; extra == 'aws' + - aiobotocore>=2 ; extra == 'aws' + - azure-storage-blob>=0.37.0 ; extra == 'azure' + - azure-identity>=1.6.1 ; extra == 'azure' + - sqlalchemy>=1.4.19 ; extra == 'azure' + - pyodbc>=4.0.30 ; extra == 'azure' + - pymssql>=2.3.6 ; extra == 'azure' + - cassandra-driver>=3.26.0,<4 ; extra == 'cassandra' + - clickhouse-connect>=0.7.19 ; extra == 'clickhouse' + - couchbase==4.3.2 ; extra == 'couchbase' + - couchbase-columnar==1.0.0 ; extra == 'couchbase' + - deltalake<1.0.0 ; extra == 'delta' + - docling==2.27.0 ; extra == 'docling' + - ibis-framework[duckdb]>=10.0.0 ; extra == 'duckdb' + - elasticsearch>=8.13.0 ; extra == 'elasticsearch' + - faiss-cpu>=1.7.0,<=1.10.0 ; extra == 'faiss' + - apache-flink>=2.2.1,<3 ; extra == 'flink' + - pyarrow<21.0.0 ; extra == 'flink' + - google-api-core>=1.23.0,<3 ; extra == 'gcp' + - googleapis-common-protos>=1.52.0,<2 ; extra == 'gcp' + - google-cloud-bigquery[pandas]>=2,<4 ; extra == 'gcp' + - google-cloud-bigquery-storage>=2.0.0,<3 ; extra == 'gcp' + - google-cloud-datastore>=2.16.0,<3 ; extra == 'gcp' + - google-cloud-storage>=1.34.0,<3 ; extra == 'gcp' + - google-cloud-bigtable>=2.11.0,<3 ; extra == 'gcp' + - fsspec>=2024.1.0 ; extra == 'gcp' + - great-expectations>=0.15.41,<1 ; extra == 'ge' + - cffi>=1.15.0 ; extra == 'go' + - grpcio>=1.56.2 ; extra == 'grpcio' + - grpcio-reflection>=1.56.2 ; extra == 'grpcio' + - grpcio-health-checking>=1.56.2 ; extra == 'grpcio' + - hazelcast-python-client>=5.1 ; extra == 'hazelcast' + - happybase>=1.2.0,<3 ; extra == 'hbase' + - ibis-framework>=10.0.0 ; extra == 'ibis' + - kubernetes ; extra == 'k8s' + - feast[pytorch] ; extra == 'image' + - timm>=0.6.0 ; extra == 'image' + - pillow>=8.0.0 ; extra == 'image' + - scikit-learn>=1.0.0 ; extra == 'image' + - pymilvus>=2.5 ; extra == 'milvus' + - milvus-lite>=2.4.12 ; extra == 'milvus' + - feast[setuptools] ; extra == 'milvus' + - pymongo>=4.13.0,<5.0.0 ; extra == 'mongodb' + - dnspython>=2.0.0 ; extra == 'mongodb' + - ibis-framework[mssql]>=10.0.0 ; extra == 'mssql' + - ibis-framework[oracle]>=10.0.0 ; extra == 'oracle' + - pymysql ; extra == 'mysql' + - types-pymysql ; extra == 'mysql' + - openlineage-python>=1.40.0 ; extra == 'openlineage' + - prometheus-client ; extra == 'opentelemetry' + - psutil ; extra == 'opentelemetry' + - pyspark>=4.0.0 ; extra == 'spark' + - pyiceberg>=0.7.0 ; extra == 'iceberg' + - trino>=0.305.0,<0.400.0 ; extra == 'trino' + - regex ; extra == 'trino' + - psycopg[binary,pool]>=3.2.5 ; extra == 'postgres' + - psycopg[c,pool]>=3.2.5 ; extra == 'postgres-c' + - torch>=2.7.0 ; extra == 'pytorch' + - torchvision>=0.22.1 ; extra == 'pytorch' + - qdrant-client>=1.12.0 ; extra == 'qdrant' + - transformers>=4.36.0 ; extra == 'rag' + - datasets>=3.6.0 ; extra == 'rag' + - sentence-transformers>=3.0.0 ; extra == 'rag' + - ray>=2.47.0 ; python_full_version == '3.10.*' and extra == 'ray' + - codeflare-sdk>=0.31.1 ; python_full_version >= '3.11' and extra == 'ray' + - datasets>=3.6.0 ; extra == 'ray' + - redis>=4.2.2,<8 ; extra == 'redis' + - hiredis>=2.0.0,<4 ; extra == 'redis' + - scylla-driver>=3.28.0,<4 ; extra == 'scylladb' + - singlestoredb<1.8.0 ; extra == 'singlestore' + - snowflake-connector-python[pandas]>=3.7,<5 ; extra == 'snowflake' + - sqlite-vec==0.1.6 ; extra == 'sqlite-vec' + - fastapi-mcp ; extra == 'mcp' + - mcp>=1.0,<2 ; extra == 'mcp' + - mlflow>=2.10.0 ; extra == 'mlflow' + - dbt-artifacts-parser ; extra == 'dbt' + - pytest>=6.0.0,<8 ; extra == 'test' + - pytest-xdist>=3.8.0 ; extra == 'test' + - pytest-cov>=5.0.0 ; extra == 'test' + - pytest-timeout==1.4.2 ; extra == 'test' + - pytest-lazy-fixture==0.6.3 ; extra == 'test' + - pytest-ordering~=0.6.0 ; extra == 'test' + - pytest-mock==1.10.4 ; extra == 'test' + - pytest-env ; extra == 'test' + - pytest-benchmark>=3.4.1,<4 ; extra == 'test' + - pytest-asyncio<=0.24.0 ; extra == 'test' + - py>=1.11.0 ; extra == 'test' + - testcontainers>=4.15.0rc2 ; extra == 'test' + - minio==7.2.11 ; extra == 'test' + - python-keycloak==4.2.2 ; extra == 'test' + - cryptography>=43.0 ; extra == 'test' + - feast[aws,azure,cassandra,clickhouse,couchbase,delta,docling,duckdb,elasticsearch,faiss,gcp,ge,go,grpcio,hazelcast,hbase,ibis,iceberg,image,k8s,mcp,milvus,mlflow,mongodb,mssql,mysql,openlineage,opentelemetry,oracle,postgres,pytorch,qdrant,rag,ray,redis,scylladb,singlestore,snowflake,spark,sqlite-vec,test,trino] ; extra == 'ci' + - build ; extra == 'ci' + - virtualenv==20.23.0 ; extra == 'ci' + - dbt-artifacts-parser ; extra == 'ci' + - ruff>=0.8.0 ; extra == 'ci' + - mypy-protobuf>=3.1 ; extra == 'ci' + - grpcio-tools>=1.56.2 ; extra == 'ci' + - grpcio-testing>=1.56.2 ; extra == 'ci' + - httpx==0.27.2 ; extra == 'ci' + - mock==2.0.0 ; extra == 'ci' + - moto<5 ; extra == 'ci' + - mypy>=1.4.1,<1.11.3 ; extra == 'ci' + - urllib3>=2.6.3,<3 ; extra == 'ci' + - psutil==5.9.0 ; extra == 'ci' + - pytest-cov ; extra == 'ci' + - sphinx>4.0.0,<7 ; extra == 'ci' + - sqlglot[rs]>=23.4 ; extra == 'ci' + - pre-commit<3.3.2 ; extra == 'ci' + - assertpy==1.1 ; extra == 'ci' + - pip-tools ; extra == 'ci' + - pybindgen==0.22.0 ; extra == 'ci' + - types-protobuf~=3.19.22 ; extra == 'ci' + - python-dateutil==2.9.0 ; extra == 'ci' + - types-python-dateutil ; extra == 'ci' + - types-pytz ; extra == 'ci' + - types-pyyaml ; extra == 'ci' + - types-redis ; extra == 'ci' + - types-requests<2.31.0 ; extra == 'ci' + - types-setuptools ; extra == 'ci' + - types-tabulate ; extra == 'ci' + - feast[docling,image,milvus,pytorch,rag] ; extra == 'nlp' + - feast[ci] ; extra == 'dev' + - feast[ci] ; extra == 'docs' + - feast[aws,duckdb,gcp,go,grpcio,k8s,mcp,milvus,mysql,opentelemetry,postgres-c,redis,snowflake] ; extra == 'minimal' + - feast[minimal] ; extra == 'minimal-sdist-build' + - feast[ibis] ; extra == 'minimal-sdist-build' + - meson<1.7.2 ; extra == 'minimal-sdist-build' + - pybindgen==0.22.0 ; extra == 'minimal-sdist-build' + - sphinx!=4.0.0 ; extra == 'minimal-sdist-build' + - types-psutil<7.0.0.20250401 ; extra == 'minimal-sdist-build' + - greenlet!=0.4.17 ; extra == 'minimal-sdist-build' + - meson-python>=0.15.0,<0.16.0 ; extra == 'minimal-sdist-build' + - cython>=0.29.34,<3.1 ; extra == 'minimal-sdist-build' + - flit-core>=3.8,<4 ; extra == 'minimal-sdist-build' + - patchelf>=0.11.0 ; extra == 'minimal-sdist-build' + - scikit-build-core>=0.10 ; extra == 'minimal-sdist-build' + - hatch-fancy-pypi-readme>=23.2.0 ; extra == 'minimal-sdist-build' + - hatch-vcs==0.4.0 ; extra == 'minimal-sdist-build' + - hatchling>=1.6.0,<2 ; extra == 'minimal-sdist-build' + - calver<2025.4.1 ; extra == 'minimal-sdist-build' + - setuptools>=60,<81 ; extra == 'setuptools' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/02/17/b82b537a30be67ba178fc0b0fbda8fcbaeda188fb039d6ad8a84e89353a2/dask-2026.6.0-py3-none-any.whl + name: dask + version: 2026.6.0 + sha256: 1539859071065dca379ca592ff76e911cd7965dc466da0040354ab466179189b + requires_dist: + - click>=8.1 + - cloudpickle>=3.0.0 + - fsspec>=2021.9.0 + - packaging>=20.0 + - partd>=1.4.0 + - pyyaml>=5.4.1 + - toolz>=0.12.0 + - importlib-metadata>=4.13.0 ; python_full_version < '3.12' + - numpy>=1.24 ; extra == 'array' + - dask[array] ; extra == 'dataframe' + - pandas>=2.0 ; extra == 'dataframe' + - pyarrow>=16.0 ; extra == 'dataframe' + - distributed>=2026.6.0,<2026.6.1 ; extra == 'distributed' + - bokeh>=3.1.0 ; extra == 'diagnostics' + - jinja2>=2.10.3 ; extra == 'diagnostics' + - dask[array,dataframe,diagnostics,distributed] ; extra == 'complete' + - lz4>=4.3.2 ; extra == 'complete' + - pandas[test] ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - pre-commit ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/02/ad/0b9cc9f38a7324a7eb1d80f834eaa5283d17e9271bbda3186e598dddaeac/yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: yarl + version: 1.24.2 + sha256: f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl + name: deprecation + version: 2.1.0 + sha256: a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a + requires_dist: + - packaging +- pypi: https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: watchfiles + version: 1.2.0 + sha256: b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 + requires_dist: + - anyio>=3.0.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + name: h11 + version: 0.16.0 + sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl + name: pandas + version: 2.3.3 + sha256: ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0 + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl + name: pyyaml + version: 6.0.3 + sha256: 02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/05/66/73034ad30b59f13439b75e620989dacba4c047256e358ba7c2e9ec98ea22/datasets-5.0.0-py3-none-any.whl + name: datasets + version: 5.0.0 + sha256: 7dd34927a0fd7046e98aad5cb9430e699c373238a15befa7b9bf22b991a7fee6 + requires_dist: + - filelock + - numpy>=1.17 + - pyarrow>=21.0.0 + - dill>=0.3.0,<0.4.2 + - pandas + - requests>=2.32.2 + - httpx<1.0.0 + - tqdm>=4.66.3 + - xxhash + - multiprocess<0.70.20 + - fsspec[http]>=2023.1.0,<=2026.4.0 + - huggingface-hub>=0.25.0,<2.0 + - packaging + - pyyaml>=5.1 + - torchcodec>=0.6.0 ; extra == 'audio' + - torch>=2.8.0 ; extra == 'audio' + - pillow>=9.4.0 ; extra == 'vision' + - trimesh>=4.10.0 ; extra == 'mesh' + - tensorflow>=2.6.0 ; extra == 'tensorflow' + - tensorflow>=2.6.0 ; extra == 'tensorflow-gpu' + - torch ; extra == 'torch' + - jax>=0.3.14 ; extra == 'jax' + - jaxlib>=0.3.14 ; extra == 'jax' + - numba>=0.56.4 ; python_full_version < '3.14' and extra == 'dev' + - absl-py ; extra == 'dev' + - decorator ; extra == 'dev' + - joblib<1.3.0 ; extra == 'dev' + - joblibspark ; python_full_version < '3.14' and extra == 'dev' + - pytest ; extra == 'dev' + - pytest-datadir ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - aiohttp ; extra == 'dev' + - elasticsearch>=7.17.12,<8.0.0 ; extra == 'dev' + - faiss-cpu>=1.8.0.post1 ; extra == 'dev' + - h5py ; extra == 'dev' + - pylance ; extra == 'dev' + - pyiceberg[pyarrow,sql-sqlite] ; extra == 'dev' + - jax>=0.3.14 ; sys_platform != 'win32' and extra == 'dev' + - jaxlib>=0.3.14 ; sys_platform != 'win32' and extra == 'dev' + - lz4 ; python_full_version < '3.14' and extra == 'dev' + - moto[server] ; extra == 'dev' + - pyspark>=3.4 ; extra == 'dev' + - py7zr ; extra == 'dev' + - rarfile>=4.0 ; extra == 'dev' + - sqlalchemy ; extra == 'dev' + - protobuf<4.0.0 ; extra == 'dev' + - tensorflow>=2.6.0 ; python_full_version < '3.10' and sys_platform != 'win32' and extra == 'dev' + - tensorflow>=2.16.0 ; python_full_version >= '3.10' and python_full_version < '3.14' and sys_platform != 'win32' and extra == 'dev' + - tiktoken ; extra == 'dev' + - torch>=2.8.0 ; extra == 'dev' + - torchdata ; extra == 'dev' + - transformers>=4.42.0 ; extra == 'dev' + - zstandard ; extra == 'dev' + - polars[timezone]>=0.20.0 ; extra == 'dev' + - pillow>=9.4.0 ; extra == 'dev' + - torchcodec>=0.7.0 ; python_full_version < '3.14' and extra == 'dev' + - nibabel>=5.3.1 ; extra == 'dev' + - trimesh>=4.10.0 ; extra == 'dev' + - teich==0.1.1a76 ; extra == 'dev' + - ruff>=0.3.0 ; extra == 'dev' + - transformers ; extra == 'dev' + - torch ; extra == 'dev' + - tensorflow>=2.6.0 ; extra == 'dev' + - numba>=0.56.4 ; python_full_version < '3.14' and extra == 'tests' + - absl-py ; extra == 'tests' + - decorator ; extra == 'tests' + - joblib<1.3.0 ; extra == 'tests' + - joblibspark ; python_full_version < '3.14' and extra == 'tests' + - pytest ; extra == 'tests' + - pytest-datadir ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - aiohttp ; extra == 'tests' + - elasticsearch>=7.17.12,<8.0.0 ; extra == 'tests' + - faiss-cpu>=1.8.0.post1 ; extra == 'tests' + - h5py ; extra == 'tests' + - pylance ; extra == 'tests' + - pyiceberg[pyarrow,sql-sqlite] ; extra == 'tests' + - jax>=0.3.14 ; sys_platform != 'win32' and extra == 'tests' + - jaxlib>=0.3.14 ; sys_platform != 'win32' and extra == 'tests' + - lz4 ; python_full_version < '3.14' and extra == 'tests' + - moto[server] ; extra == 'tests' + - pyspark>=3.4 ; extra == 'tests' + - py7zr ; extra == 'tests' + - rarfile>=4.0 ; extra == 'tests' + - sqlalchemy ; extra == 'tests' + - protobuf<4.0.0 ; extra == 'tests' + - tensorflow>=2.6.0 ; python_full_version < '3.10' and sys_platform != 'win32' and extra == 'tests' + - tensorflow>=2.16.0 ; python_full_version >= '3.10' and python_full_version < '3.14' and sys_platform != 'win32' and extra == 'tests' + - tiktoken ; extra == 'tests' + - torch>=2.8.0 ; extra == 'tests' + - torchdata ; extra == 'tests' + - transformers>=4.42.0 ; extra == 'tests' + - zstandard ; extra == 'tests' + - polars[timezone]>=0.20.0 ; extra == 'tests' + - pillow>=9.4.0 ; extra == 'tests' + - torchcodec>=0.7.0 ; python_full_version < '3.14' and extra == 'tests' + - nibabel>=5.3.1 ; extra == 'tests' + - trimesh>=4.10.0 ; extra == 'tests' + - teich==0.1.1a76 ; extra == 'tests' + - numba>=0.56.4 ; python_full_version < '3.14' and extra == 'tests-numpy2' + - absl-py ; extra == 'tests-numpy2' + - decorator ; extra == 'tests-numpy2' + - joblib<1.3.0 ; extra == 'tests-numpy2' + - joblibspark ; python_full_version < '3.14' and extra == 'tests-numpy2' + - pytest ; extra == 'tests-numpy2' + - pytest-datadir ; extra == 'tests-numpy2' + - pytest-xdist ; extra == 'tests-numpy2' + - aiohttp ; extra == 'tests-numpy2' + - elasticsearch>=7.17.12,<8.0.0 ; extra == 'tests-numpy2' + - h5py ; extra == 'tests-numpy2' + - pylance ; extra == 'tests-numpy2' + - pyiceberg[pyarrow,sql-sqlite] ; extra == 'tests-numpy2' + - jax>=0.3.14 ; sys_platform != 'win32' and extra == 'tests-numpy2' + - jaxlib>=0.3.14 ; sys_platform != 'win32' and extra == 'tests-numpy2' + - lz4 ; python_full_version < '3.14' and extra == 'tests-numpy2' + - moto[server] ; extra == 'tests-numpy2' + - pyspark>=3.4 ; extra == 'tests-numpy2' + - py7zr ; extra == 'tests-numpy2' + - rarfile>=4.0 ; extra == 'tests-numpy2' + - sqlalchemy ; extra == 'tests-numpy2' + - protobuf<4.0.0 ; extra == 'tests-numpy2' + - tiktoken ; extra == 'tests-numpy2' + - torch>=2.8.0 ; extra == 'tests-numpy2' + - torchdata ; extra == 'tests-numpy2' + - transformers>=4.42.0 ; extra == 'tests-numpy2' + - zstandard ; extra == 'tests-numpy2' + - polars[timezone]>=0.20.0 ; extra == 'tests-numpy2' + - pillow>=9.4.0 ; extra == 'tests-numpy2' + - torchcodec>=0.7.0 ; python_full_version < '3.14' and extra == 'tests-numpy2' + - nibabel>=5.3.1 ; extra == 'tests-numpy2' + - trimesh>=4.10.0 ; extra == 'tests-numpy2' + - teich==0.1.1a76 ; extra == 'tests-numpy2' + - ruff>=0.3.0 ; extra == 'quality' + - tensorflow==2.12.0 ; extra == 'benchmarks' + - torch==2.0.1 ; extra == 'benchmarks' + - transformers==4.30.1 ; extra == 'benchmarks' + - transformers ; extra == 'docs' + - torch ; extra == 'docs' + - tensorflow>=2.6.0 ; extra == 'docs' + - pdfplumber>=0.11.4 ; extra == 'pdfs' + - nibabel>=5.3.2 ; extra == 'nibabel' + - ipyniivue==2.4.2 ; extra == 'nibabel' + - pyiceberg>=0.7.0 ; extra == 'iceberg' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl + name: rpds-py + version: 0.30.0 + sha256: 679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/06/3d/2f0e9c5cbc456d34f48215645d876f7885a15e09a72a07d1de3ddb181c38/pandas_gbq-0.35.0-py3-none-any.whl + name: pandas-gbq + version: 0.35.0 + sha256: 258de481019566611031919997bf9c1ece4ca30a4dd02d3fc3664b251d446182 + requires_dist: + - setuptools + - db-dtypes>=1.1.1,<2.0.0 + - numpy>=1.26.4 + - pandas>=1.5.3 + - pyarrow>=12.0.0 + - pyarrow>=22.0.0 ; python_full_version >= '3.14' + - pydata-google-auth>=1.5.0 + - psutil>=5.9.8 + - google-api-core>=2.15.0,<3.0.0 + - google-auth>=2.14.1 + - google-auth-oauthlib>=0.7.0 + - google-cloud-bigquery>=3.20.0,<4.0.0 + - packaging>=22.0.0 + - google-cloud-bigquery-storage>=2.16.2,<3.0.0 ; extra == 'bqstorage' + - tqdm>=4.23.0 ; extra == 'tqdm' + - geopandas>=0.14.4 ; extra == 'geopandas' + - shapely>=1.8.5 ; extra == 'geopandas' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: argon2-cffi-bindings + version: 25.1.0 + sha256: d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a + requires_dist: + - cffi>=1.0.1 ; python_full_version < '3.14' + - cffi>=2.0.0b1 ; python_full_version >= '3.14' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl + name: argon2-cffi-bindings + version: 25.1.0 + sha256: 2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44 + requires_dist: + - cffi>=1.0.1 ; python_full_version < '3.14' + - cffi>=2.0.0b1 ; python_full_version >= '3.14' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: librt + version: 0.11.0 + sha256: 05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + name: python-dotenv + version: 1.2.2 + sha256: 1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a + requires_dist: + - click>=5.0 ; extra == 'cli' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + name: pycparser + version: '3.0' + sha256: b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl + name: watchfiles + version: 1.2.0 + sha256: bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 + requires_dist: + - anyio>=3.0.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl + name: uvloop + version: 0.22.1 + sha256: 4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 + requires_dist: + - aiohttp>=3.10.5 ; extra == 'test' + - flake8~=6.1 ; extra == 'test' + - psutil ; extra == 'test' + - pycodestyle~=2.11.0 ; extra == 'test' + - pyopenssl~=25.3.0 ; extra == 'test' + - mypy>=0.800 ; extra == 'test' + - setuptools>=60 ; extra == 'dev' + - cython~=3.0 ; extra == 'dev' + - sphinx~=4.1.2 ; extra == 'docs' + - sphinxcontrib-asyncio~=0.3.0 ; extra == 'docs' + - sphinx-rtd-theme~=0.5.2 ; extra == 'docs' + requires_python: '>=3.8.1' +- pypi: https://files.pythonhosted.org/packages/10/03/060ce69008ac97bbc01b1411b3e55b61f6f015659400b46749b662107831/coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl + name: coverage + version: 7.15.2 + sha256: 9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl + name: protobuf + version: 7.35.1 + sha256: 24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl + name: websockets + version: '16.0' + sha256: 583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl + name: aioitertools + version: 0.13.0 + sha256: 0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be + requires_dist: + - typing-extensions>=4.0 ; python_full_version < '3.10' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl + name: filelock + version: 3.29.4 + sha256: dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl + name: pandas + version: 2.3.3 + sha256: e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl + name: jmespath + version: 1.1.0 + sha256: a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: pandas + version: 2.3.3 + sha256: ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl + name: protobuf + version: 6.33.6 + sha256: e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + name: typing-extensions + version: 4.15.0 + sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/18/68/4ae5b4e08943f316594bb68da89957d3baf5760588fa09509594bd777e4b/aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl + name: aiohttp + version: 3.14.1 + sha256: 4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491 + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - typing-extensions>=4.4 ; python_full_version < '3.13' + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl + name: websockets + version: '16.0' + sha256: 8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl + name: cryptography + version: 49.0.0 + sha256: ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a + requires_dist: + - cffi>=2.0.0 ; platform_python_implementation != 'PyPy' + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - bcrypt>=3.1.5 ; extra == 'ssh' + requires_python: '!=3.9.0,>=3.9,!=3.9.1' +- pypi: https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl + name: rpds-py + version: 0.30.0 + sha256: 4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl + name: httptools + version: 0.8.0 + sha256: de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl + name: propcache + version: 0.5.2 + sha256: 29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl + name: pyarrow + version: 24.0.0 + sha256: e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl + name: pyarrow + version: 24.0.0 + sha256: 644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl + name: idna + version: '3.18' + sha256: 7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 + requires_dist: + - ruff>=0.6.2 ; extra == 'all' + - mypy>=1.11.2 ; extra == 'all' + - pytest>=8.3.2 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + name: annotated-doc + version: 0.0.4 + sha256: 571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz + name: cryptography + version: 49.0.0 + sha256: f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 + requires_dist: + - cffi>=2.0.0 ; platform_python_implementation != 'PyPy' + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - bcrypt>=3.1.5 ; extra == 'ssh' + requires_python: '!=3.9.0,>=3.9,!=3.9.1' +- pypi: https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl + name: propcache + version: 0.5.2 + sha256: 6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: charset-normalizer + version: 3.4.7 + sha256: cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl + name: pandas + version: 2.3.3 + sha256: 1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593 + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl + name: numpy + version: 2.2.6 + sha256: 8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/23/1b/545eee1c18f3af4cf140bb5822b6ef81ebe569df0a63ac109973103a30a5/wrapt-2.2.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: wrapt + version: 2.2.2 + sha256: 656593bb3f5529f03d27af4136c4d7b11990e470bcbc6fefa5ef218695bece55 + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/25/4b/0dcff723965361b5f816a0fdbd6f89771a1cab8faff23f37436dea95c90e/bigtree-1.5.0-py3-none-any.whl + name: bigtree + version: 1.5.0 + sha256: 1917d90a8bd464289a56e34842cb04d3a8131e6d532352afd33fa822fa397fcb + requires_dist: + - lark ; extra == 'all' + - matplotlib ; extra == 'all' + - pandas ; extra == 'all' + - pillow ; extra == 'all' + - polars ; extra == 'all' + - pydot ; extra == 'all' + - pyvis ; extra == 'all' + - rich ; extra == 'all' + - textual>=0.6.0 ; extra == 'all' + - pillow ; extra == 'image' + - pydot ; extra == 'image' + - matplotlib ; extra == 'matplotlib' + - pandas ; extra == 'pandas' + - polars ; extra == 'polars' + - lark ; extra == 'query' + - rich ; extra == 'rich' + - textual>=0.6.0 ; extra == 'studio' + - pyvis ; extra == 'vis' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/25/b2/bdc663a5647ce2034f7e8420122af340df87c01ba97745fc753b8c917acb/pytest_env-1.1.3-py3-none-any.whl + name: pytest-env + version: 1.1.3 + sha256: aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc + requires_dist: + - pytest>=7.4.3 + - tomli>=2.0.1 ; python_full_version < '3.11' + - covdefaults>=2.3 ; extra == 'test' + - coverage>=7.3.2 ; extra == 'test' + - pytest-mock>=3.12 ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl + name: charset-normalizer + version: 3.4.7 + sha256: cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl + name: ast-serialize + version: 0.5.0 + sha256: bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/26/c9/4e9cd249afc101ac283943295fe3359bdd711a0bb8c667752eb0da80609d/hiredis-3.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: hiredis + version: 3.4.0 + sha256: 4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl + name: mypy + version: 2.1.0 + sha256: 49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f + requires_dist: + - typing-extensions>=4.6.0 ; python_full_version < '3.15' + - typing-extensions>=4.14.0 ; python_full_version >= '3.15' + - mypy-extensions>=1.0.0 + - pathspec>=1.0.0 + - tomli>=1.1.0 ; python_full_version < '3.11' + - librt>=0.11.0 ; platform_python_implementation != 'PyPy' + - ast-serialize>=0.3.0,<1.0.0 + - psutil>=4.0 ; extra == 'dmypy' + - setuptools>=50 ; extra == 'mypyc' + - lxml ; extra == 'reports' + - pip ; extra == 'install-types' + - orjson ; extra == 'faster-cache' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl + name: librt + version: 0.11.0 + sha256: 4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + name: httpx + version: 0.28.1 + sha256: d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + requires_dist: + - anyio + - certifi + - httpcore==1.* + - idna + - brotli ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi ; platform_python_implementation != 'CPython' and extra == 'brotli' + - click==8.* ; extra == 'cli' + - pygments==2.* ; extra == 'cli' + - rich>=10,<14 ; extra == 'cli' + - h2>=3,<5 ; extra == 'http2' + - socksio==1.* ; extra == 'socks' + - zstandard>=0.18.0 ; extra == 'zstd' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/2b/89/dda79527bb7573ba91828b2fb91b3105d87378d6a2749ca0c0924ce0addd/coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: coverage + version: 7.15.2 + sha256: 1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/2b/9d/93294c3045775c708ac8310eb3d3622a11d2951345ad590d532d62a1faa4/aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: aiohttp + version: 3.14.1 + sha256: 23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - typing-extensions>=4.4 ; python_full_version < '3.13' + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + name: referencing + version: 0.37.0 + sha256: 381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 + requires_dist: + - attrs>=22.2.0 + - rpds-py>=0.7.0 + - typing-extensions>=4.4.0 ; python_full_version < '3.13' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/2c/60/423a63fb190a0483d049786a121bd3dfd7d93bb5ff1bb5b5cd13e5df99a7/pytest_benchmark-3.4.1-py2.py3-none-any.whl + name: pytest-benchmark + version: 3.4.1 + sha256: 36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809 + requires_dist: + - pytest>=3.8 + - py-cpuinfo + - statistics ; python_full_version < '3.4' + - pathlib2 ; python_full_version < '3.4' + - aspectlib ; extra == 'aspect' + - elasticsearch ; extra == 'elasticsearch' + - pygal ; extra == 'histogram' + - pygaljs ; extra == 'histogram' + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' +- pypi: https://files.pythonhosted.org/packages/2d/a1/2f2c1c2353350d66c4d110d283e422e4943eb5ad10effa9357ba66f7b5b9/pytest_lazy_fixture-0.6.3-py3-none-any.whl + name: pytest-lazy-fixture + version: 0.6.3 + sha256: e0b379f38299ff27a653f03eaa69b08a6fd4484e46fd1c9907d984b9f9daeda6 + requires_dist: + - pytest>=3.2.5 +- pypi: https://files.pythonhosted.org/packages/2e/2e/dfbd2c9b3edf6a5a8cd9e66090221046839b488ea27824970426bf06b242/python_keycloak-4.2.2-py3-none-any.whl + name: python-keycloak + version: 4.2.2 + sha256: 5137fd87c69031a372a578df96bae96b9aead2c9dad976613bc978e9e0246a1e + requires_dist: + - async-property>=0.2.2 + - deprecation>=2.1.0 + - httpx>=0.23.2 + - jwcrypto>=1.5.4 + - requests>=2.20.0 + - requests-toolbelt>=0.6.0 + requires_python: '>=3.8,<4.0' +- pypi: https://files.pythonhosted.org/packages/2e/c3/94ade4906a2f88bc935772f59c934013b4205e773bcb4239db114a6da136/pyarrow_hotfix-0.7-py3-none-any.whl + name: pyarrow-hotfix + version: '0.7' + sha256: 3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 + requires_python: '>=3.5' +- pypi: https://files.pythonhosted.org/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl + name: mypy + version: 2.1.0 + sha256: 8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849 + requires_dist: + - typing-extensions>=4.6.0 ; python_full_version < '3.15' + - typing-extensions>=4.14.0 ; python_full_version >= '3.15' + - mypy-extensions>=1.0.0 + - pathspec>=1.0.0 + - tomli>=1.1.0 ; python_full_version < '3.11' + - librt>=0.11.0 ; platform_python_implementation != 'PyPy' + - ast-serialize>=0.3.0,<1.0.0 + - psutil>=4.0 ; extra == 'dmypy' + - setuptools>=50 ; extra == 'mypyc' + - lxml ; extra == 'reports' + - pip ; extra == 'install-types' + - orjson ; extra == 'faster-cache' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/30/43/8deecb4c123bbc16d25666f1a6d241109c97aeb2e50806b952661c8e4b95/pytest_mock-1.10.4-py2.py3-none-any.whl + name: pytest-mock + version: 1.10.4 + sha256: 43ce4e9dd5074993e7c021bb1c22cbb5363e612a2b5a76bc6d956775b10758b7 + requires_dist: + - pytest>=2.7 + - mock ; python_full_version < '3' + - pre-commit ; extra == 'dev' + - tox ; extra == 'dev' + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl + name: httptools + version: 0.8.0 + sha256: 159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl + name: multidict + version: 6.7.1 + sha256: cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl + name: sortedcontainers + version: 2.4.0 + sha256: a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 +- pypi: https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl + name: markupsafe + version: 3.0.3 + sha256: eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl + name: hf-xet + version: 1.5.1 + sha256: 94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e + requires_dist: + - pytest ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/37/d3/d7dff0d58a9e9244b48044bfb6a898bfcc8ecc42e0031d1bebc695344725/google_auth_oauthlib-1.4.0-py3-none-any.whl + name: google-auth-oauthlib + version: 1.4.0 + sha256: 251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 + requires_dist: + - google-auth>=2.15.0,!=2.43.0,!=2.44.0,!=2.45.0,<3.0.0 + - requests-oauthlib>=0.7.0 + - click>=6.0.0 ; extra == 'tool' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl + name: psycopg-pool + version: 3.3.1 + sha256: 2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 + requires_dist: + - typing-extensions>=4.6 + - anyio>=4.0 ; extra == 'test' + - mypy>=1.14 ; extra == 'test' + - pproxy>=2.7 ; extra == 'test' + - pytest>=6.2.5 ; extra == 'test' + - pytest-cov>=3.0 ; extra == 'test' + - pytest-randomly>=3.5 ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl + name: importlib-metadata + version: 9.0.0 + sha256: 2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 + requires_dist: + - zipp>=3.20 + - pytest>=6,!=8.1.* ; extra == 'test' + - packaging ; extra == 'test' + - pyfakefs ; extra == 'test' + - pytest-perf>=0.9.2 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - ipython ; extra == 'perf' + - pytest-checkdocs>=2.14 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl + name: fastapi + version: 0.138.1 + sha256: b994cae7ba8b82c976a728b544244de31333fa5f7d261f9a1dffe526444cae23 + requires_dist: + - starlette>=0.46.0 + - pydantic>=2.9.0 + - typing-extensions>=4.8.0 + - typing-inspection>=0.4.2 + - annotated-doc>=0.0.2 + - fastapi-cli[standard]>=0.0.8 ; extra == 'standard' + - fastar>=0.9.0 ; extra == 'standard' + - httpx>=0.23.0,<1.0.0 ; extra == 'standard' + - jinja2>=3.1.5 ; extra == 'standard' + - python-multipart>=0.0.18 ; extra == 'standard' + - email-validator>=2.0.0 ; extra == 'standard' + - uvicorn[standard]>=0.12.0 ; extra == 'standard' + - pydantic-settings>=2.0.0 ; extra == 'standard' + - pydantic-extra-types>=2.0.0 ; extra == 'standard' + - fastapi-cli[standard-no-fastapi-cloud-cli]>=0.0.8 ; extra == 'standard-no-fastapi-cloud-cli' + - httpx>=0.23.0,<1.0.0 ; extra == 'standard-no-fastapi-cloud-cli' + - jinja2>=3.1.5 ; extra == 'standard-no-fastapi-cloud-cli' + - python-multipart>=0.0.18 ; extra == 'standard-no-fastapi-cloud-cli' + - email-validator>=2.0.0 ; extra == 'standard-no-fastapi-cloud-cli' + - uvicorn[standard]>=0.12.0 ; extra == 'standard-no-fastapi-cloud-cli' + - pydantic-settings>=2.0.0 ; extra == 'standard-no-fastapi-cloud-cli' + - pydantic-extra-types>=2.0.0 ; extra == 'standard-no-fastapi-cloud-cli' + - fastapi-cli[standard]>=0.0.8 ; extra == 'all' + - httpx>=0.23.0,<1.0.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - python-multipart>=0.0.18 ; extra == 'all' + - itsdangerous>=1.1.0 ; extra == 'all' + - pyyaml>=5.3.1 ; extra == 'all' + - email-validator>=2.0.0 ; extra == 'all' + - uvicorn[standard]>=0.12.0 ; extra == 'all' + - pydantic-settings>=2.0.0 ; extra == 'all' + - pydantic-extra-types>=2.0.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: propcache + version: 0.5.2 + sha256: b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl + name: zipp + version: 4.1.0 + sha256: 25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-itertools ; extra == 'test' + - jaraco-functools ; extra == 'test' + - more-itertools ; extra == 'test' + - big-o ; extra == 'test' + - pytest-ignore-flaky ; extra == 'test' + - jaraco-test ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.14 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3b/10/5152fbc98f6b5b4f54cdf97c585d19fe962c265774692e446ad6b4f4950c/grpcio_reflection-1.81.1-py3-none-any.whl + name: grpcio-reflection + version: 1.81.1 + sha256: d82fc4ac39dd5dcfd63e577075f6a68eac4a765e66a453914bbf57caf35665d0 + requires_dist: + - protobuf>=6.33.5,<7.0.0 + - grpcio>=1.81.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl + name: requests-oauthlib + version: 2.0.0 + sha256: 7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 + requires_dist: + - oauthlib>=3.0.0 + - requests>=2.0.0 + - oauthlib[signedtoken]>=3.0.0 ; extra == 'rsa' + requires_python: '>=3.4' +- pypi: https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + name: google-crc32c + version: 1.8.0 + sha256: 6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl + name: pandas + version: 2.3.3 + sha256: 376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + name: requests-toolbelt + version: 1.0.0 + sha256: cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 + requires_dist: + - requests>=2.0.1,<3.0.0 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + name: typer + version: 0.25.1 + sha256: 75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89 + requires_dist: + - click>=8.2.1 + - shellingham>=1.3.0 + - rich>=13.8.0 + - annotated-doc>=0.0.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: pandas + version: 2.3.3 + sha256: dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838 + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/40/b9/be66eb0decd730d89b9c94f930e4b8d87787b05724bb84af98bfd825f72c/httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl + name: httptools + version: 0.8.0 + sha256: bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 + requires_python: '>=3.9' +- pypi: 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 + name: markupsafe + version: 3.0.3 + sha256: 457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + name: jsonschema-specifications + version: 2025.9.1 + sha256: 98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe + requires_dist: + - referencing>=0.31.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: mypy + version: 2.1.0 + sha256: 47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166 + requires_dist: + - typing-extensions>=4.6.0 ; python_full_version < '3.15' + - typing-extensions>=4.14.0 ; python_full_version >= '3.15' + - mypy-extensions>=1.0.0 + - pathspec>=1.0.0 + - tomli>=1.1.0 ; python_full_version < '3.11' + - librt>=0.11.0 ; platform_python_implementation != 'PyPy' + - ast-serialize>=0.3.0,<1.0.0 + - psutil>=4.0 ; extra == 'dmypy' + - setuptools>=50 ; extra == 'mypyc' + - lxml ; extra == 'reports' + - pip ; extra == 'install-types' + - orjson ; extra == 'faster-cache' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl + name: toml + version: 0.10.2 + sha256: 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + requires_python: '>=2.6,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl + name: dill + version: 0.3.9 + sha256: 468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a + requires_dist: + - objgraph>=1.7.2 ; extra == 'graph' + - gprof2dot>=2022.7.29 ; extra == 'profile' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/46/df/97cc0b5b8b53da0e265acd0aeecfc0c279e950a029acd2d7b4e54b00b25f/pytest_timeout-1.4.2-py2.py3-none-any.whl + name: pytest-timeout + version: 1.4.2 + sha256: 541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063 + requires_dist: + - pytest>=3.6.0 +- pypi: https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: charset-normalizer + version: 3.4.7 + sha256: bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl + name: pyasn1-modules + version: 0.4.2 + sha256: 29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a + requires_dist: + - pyasn1>=0.6.1,<0.7.0 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/48/41/7daafb32dd7562bf45b1ce56562e7e1a9146f6479b6456873eb8a3413c40/yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl + name: yarl + version: 1.24.2 + sha256: 7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/49/4d/7e6a9088381386b4cfae4c5d1d23ea0c3618ca694bc2290118737af59f36/snowflake_connector_python-4.6.0.tar.gz + name: snowflake-connector-python + version: 4.6.0 + sha256: 06e2dba02703da6fd60e07bb0574506f810a85e5831d3461247753ecce4b8335 + requires_dist: + - asn1crypto>0.24.0,<2.0.0 + - cryptography>=46.0.5 + - pyopenssl>=24.0.0 + - pyjwt>=2.10.1,<3.0.0 + - pytz + - requests>=2.32.4,<3.0.0 + - packaging + - charset-normalizer>=2,<4 + - idna>=3.7,<4 + - certifi>=2024.7.4 + - typing-extensions>=4.3,<5 + - filelock>=3.5,<4 + - sortedcontainers>=2.4.0 + - platformdirs>=2.6.0,<5.0.0 + - tomlkit + - boto3>=1.24 + - botocore>=1.24 + - boto3>=1.24 ; extra == 'boto' + - botocore>=1.24 ; extra == 'boto' + - cython ; extra == 'development' + - coverage ; extra == 'development' + - mitmproxy>=12.0.0 ; python_full_version >= '3.12' and extra == 'development' + - more-itertools ; extra == 'development' + - numpy<=2.4.3 ; extra == 'development' + - pendulum!=2.1.1 ; extra == 'development' + - pexpect ; extra == 'development' + - pytest<7.5.0 ; extra == 'development' + - pytest-asyncio ; extra == 'development' + - pytest-cov ; extra == 'development' + - pytest-rerunfailures<16.0 ; extra == 'development' + - pytest-timeout ; extra == 'development' + - pytest-xdist ; extra == 'development' + - pytzdata ; extra == 'development' + - responses ; extra == 'development' + - pandas>=1.0.0,<3.0.0 ; python_full_version < '3.13' and extra == 'pandas' + - pandas>=2.1.2,<3.0.0 ; python_full_version >= '3.13' and extra == 'pandas' + - pyarrow>=14.0.1 ; extra == 'pandas' + - keyring>=23.1.0,<26.0.0 ; extra == 'secure-local-storage' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4a/2e/2677f3f93dae0497e7e33b6637302e7f3744efc553f34231183e32584885/redis-7.4.1-py3-none-any.whl + name: redis + version: 7.4.1 + sha256: 1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + requires_dist: + - async-timeout>=4.0.3 ; python_full_version < '3.11.3' + - pybreaker>=1.4.0 ; extra == 'circuit-breaker' + - hiredis>=3.2.0 ; extra == 'hiredis' + - pyjwt>=2.13.0 ; extra == 'jwt' + - cryptography>=36.0.1 ; extra == 'ocsp' + - pyopenssl>=20.0.1 ; extra == 'ocsp' + - requests>=2.31.0 ; extra == 'ocsp' + - opentelemetry-api>=1.39.1 ; extra == 'otel' + - opentelemetry-exporter-otlp-proto-http>=1.39.1 ; extra == 'otel' + - opentelemetry-sdk>=1.39.1 ; extra == 'otel' + - xxhash~=3.6.0 ; extra == 'xxhash' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: multidict + version: 6.7.1 + sha256: 9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/4c/5a/cbf9ac6fb58bfbdbd331f98be56ffd02e5c51c015086cf6aa8b08ad1fe2e/testcontainers-4.15.0rc4-py3-none-any.whl + name: testcontainers + version: 4.15.0rc4 + sha256: bf1be1f33b7e20d1d515c10d0df03ddb23fe35a208cb25cef131d80f1799da2b + requires_dist: + - docker + - python-dotenv + - typing-extensions + - urllib3 + - wrapt + - python-arango>=8 ; extra == 'arangodb' + - boto3>=1 ; extra == 'aws' + - httpx ; extra == 'aws' + - azure-storage-blob>=12 ; extra == 'azurite' + - chromadb-client>=1 ; extra == 'chroma' + - clickhouse-driver ; extra == 'clickhouse' + - azure-cosmos>=4 ; extra == 'cosmosdb' + - ibm-db-sa ; platform_machine != 'aarch64' and platform_machine != 'arm64' and extra == 'db2' + - sqlalchemy>=2 ; extra == 'db2' + - httpx ; extra == 'generic' + - redis>=7 ; extra == 'generic' + - sqlalchemy ; extra == 'generic' + - google-cloud-datastore>=2 ; extra == 'google' + - google-cloud-pubsub>=2 ; extra == 'google' + - influxdb-client>=1 ; extra == 'influxdb' + - influxdb>=5 ; extra == 'influxdb' + - kubernetes ; extra == 'k3s' + - pyyaml>=6.0.3 ; extra == 'k3s' + - python-keycloak>=6 ; python_full_version < '4' and extra == 'keycloak' + - boto3>=1 ; extra == 'localstack' + - cryptography ; extra == 'mailpit' + - minio>=7 ; extra == 'minio' + - pymongo>=4 ; extra == 'mongodb' + - pymssql>=2 ; extra == 'mssql' + - sqlalchemy>=2 ; extra == 'mssql' + - pymysql[rsa]>=1 ; extra == 'mysql' + - sqlalchemy>=2 ; extra == 'mysql' + - nats-py>=2 ; extra == 'nats' + - neo4j>=6 ; extra == 'neo4j' + - openfga-sdk ; extra == 'openfga' + - opensearch-py>=3 ; python_full_version < '4' and extra == 'opensearch' + - oracledb>=3 ; extra == 'oracle' + - sqlalchemy>=2 ; extra == 'oracle' + - oracledb>=3 ; extra == 'oracle-free' + - sqlalchemy>=2 ; extra == 'oracle-free' + - qdrant-client>=1 ; extra == 'qdrant' + - pika>=1 ; extra == 'rabbitmq' + - redis>=7 ; extra == 'redis' + - bcrypt>=5 ; extra == 'registry' + - cassandra-driver>=3 ; python_full_version < '3.14' and extra == 'scylla' + - selenium>=4 ; extra == 'selenium' + - cryptography ; extra == 'sftp' + - httpx ; extra == 'test-module-import' + - trino ; extra == 'trino' + - weaviate-client>=4 ; extra == 'weaviate' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4d/39/679e7b08ad313ad600b98b06395274cbef5ab51a2b0b7f68c7bb001c9717/grpcio_testing-1.81.1-py3-none-any.whl + name: grpcio-testing + version: 1.81.1 + sha256: 69792db25781cefd2479d12285ca6bebe589e2aa1d64b4b8d18cf35f58c6b810 + requires_dist: + - protobuf>=6.33.5,<7.0.0 + - grpcio>=1.81.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl + name: huggingface-hub + version: 1.21.0 + sha256: eadaa3678c512c82aea69e8675d90a184861e68de32f1105668628b4dce0e7cd + requires_dist: + - click>=8.4.0 + - filelock>=3.10.0 + - fsspec>=2023.5.0 + - hf-xet>=1.5.1,<2.0.0 ; platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' + - httpx>=0.23.0,<1 + - packaging>=20.9 + - pyyaml>=5.1 + - tqdm>=4.42.1 + - typer>=0.20.0,<0.26.0 + - typing-extensions>=4.1.0 + - authlib>=1.3.2 ; extra == 'oauth' + - fastapi ; extra == 'oauth' + - httpx ; extra == 'oauth' + - itsdangerous ; extra == 'oauth' + - torch ; extra == 'torch' + - safetensors[torch] ; extra == 'torch' + - toml ; extra == 'fastai' + - fastai>=2.4 ; extra == 'fastai' + - fastcore>=1.3.27 ; extra == 'fastai' + - hf-xet>=1.5.1,<2.0.0 ; extra == 'hf-xet' + - mcp>=1.8.0 ; extra == 'mcp' + - authlib>=1.3.2 ; extra == 'testing' + - fastapi ; extra == 'testing' + - httpx ; extra == 'testing' + - itsdangerous ; extra == 'testing' + - jedi ; extra == 'testing' + - jinja2 ; extra == 'testing' + - pytest>=8.4.2 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-env ; extra == 'testing' + - pytest-xdist ; extra == 'testing' + - pytest-vcr ; extra == 'testing' + - pytest-asyncio ; extra == 'testing' + - pytest-rerunfailures<16.0 ; extra == 'testing' + - pytest-mock ; extra == 'testing' + - urllib3<2.0 ; extra == 'testing' + - soundfile ; extra == 'testing' + - pillow ; extra == 'testing' + - numpy ; extra == 'testing' + - duckdb ; extra == 'testing' + - fastapi ; extra == 'testing' + - gradio>=5.0.0 ; extra == 'gradio' + - requests ; extra == 'gradio' + - typing-extensions>=4.8.0 ; extra == 'typing' + - types-pyyaml ; extra == 'typing' + - types-simplejson ; extra == 'typing' + - types-toml ; extra == 'typing' + - types-tqdm ; extra == 'typing' + - types-urllib3 ; extra == 'typing' + - ruff>=0.9.0 ; extra == 'quality' + - mypy==1.15.0 ; extra == 'quality' + - libcst>=1.4.0 ; extra == 'quality' + - ty ; extra == 'quality' + - authlib>=1.3.2 ; extra == 'all' + - fastapi ; extra == 'all' + - httpx ; extra == 'all' + - itsdangerous ; extra == 'all' + - jedi ; extra == 'all' + - jinja2 ; extra == 'all' + - pytest>=8.4.2 ; extra == 'all' + - pytest-cov ; extra == 'all' + - pytest-env ; extra == 'all' + - pytest-xdist ; extra == 'all' + - pytest-vcr ; extra == 'all' + - pytest-asyncio ; extra == 'all' + - pytest-rerunfailures<16.0 ; extra == 'all' + - pytest-mock ; extra == 'all' + - urllib3<2.0 ; extra == 'all' + - soundfile ; extra == 'all' + - pillow ; extra == 'all' + - numpy ; extra == 'all' + - duckdb ; extra == 'all' + - fastapi ; extra == 'all' + - ruff>=0.9.0 ; extra == 'all' + - mypy==1.15.0 ; extra == 'all' + - libcst>=1.4.0 ; extra == 'all' + - ty ; extra == 'all' + - typing-extensions>=4.8.0 ; extra == 'all' + - types-pyyaml ; extra == 'all' + - types-simplejson ; extra == 'all' + - types-toml ; extra == 'all' + - types-tqdm ; extra == 'all' + - types-urllib3 ; extra == 'all' + - authlib>=1.3.2 ; extra == 'dev' + - fastapi ; extra == 'dev' + - httpx ; extra == 'dev' + - itsdangerous ; extra == 'dev' + - jedi ; extra == 'dev' + - jinja2 ; extra == 'dev' + - pytest>=8.4.2 ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-env ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pytest-vcr ; extra == 'dev' + - pytest-asyncio ; extra == 'dev' + - pytest-rerunfailures<16.0 ; extra == 'dev' + - pytest-mock ; extra == 'dev' + - urllib3<2.0 ; extra == 'dev' + - soundfile ; extra == 'dev' + - pillow ; extra == 'dev' + - numpy ; extra == 'dev' + - duckdb ; extra == 'dev' + - fastapi ; extra == 'dev' + - ruff>=0.9.0 ; extra == 'dev' + - mypy==1.15.0 ; extra == 'dev' + - libcst>=1.4.0 ; extra == 'dev' + - ty ; extra == 'dev' + - typing-extensions>=4.8.0 ; extra == 'dev' + - types-pyyaml ; extra == 'dev' + - types-simplejson ; extra == 'dev' + - types-toml ; extra == 'dev' + - types-tqdm ; extra == 'dev' + - types-urllib3 ; extra == 'dev' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/4f/d1/36cdfc7d9a5cdcebb2ff3eeeaebae2c51a7aca50de27a44520af4d6923fa/xxhash-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl + name: xxhash + version: 3.8.0 + sha256: a2289857ab90ebb2408d4ac2b7cf7e9ff29bba9d2cb21020c9d11fbbaef78eea + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + name: argon2-cffi + version: 25.1.0 + sha256: fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 + requires_dist: + - argon2-cffi-bindings + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: mypy + version: 2.1.0 + sha256: c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef + requires_dist: + - typing-extensions>=4.6.0 ; python_full_version < '3.15' + - typing-extensions>=4.14.0 ; python_full_version >= '3.15' + - mypy-extensions>=1.0.0 + - pathspec>=1.0.0 + - tomli>=1.1.0 ; python_full_version < '3.11' + - librt>=0.11.0 ; platform_python_implementation != 'PyPy' + - ast-serialize>=0.3.0,<1.0.0 + - psutil>=4.0 ; extra == 'dmypy' + - setuptools>=50 ; extra == 'mypyc' + - lxml ; extra == 'reports' + - pip ; extra == 'install-types' + - orjson ; extra == 'faster-cache' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl + name: pytest + version: 7.4.4 + sha256: b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 + requires_dist: + - iniconfig + - packaging + - pluggy>=0.12,<2.0 + - exceptiongroup>=1.0.0rc8 ; python_full_version < '3.11' + - tomli>=1.0.0 ; python_full_version < '3.11' + - importlib-metadata>=0.12 ; python_full_version < '3.8' + - colorama ; sys_platform == 'win32' + - argcomplete ; extra == 'testing' + - attrs>=19.2.0 ; extra == 'testing' + - hypothesis>=3.56 ; extra == 'testing' + - mock ; extra == 'testing' + - nose ; extra == 'testing' + - pygments>=2.7.2 ; extra == 'testing' + - requests ; extra == 'testing' + - setuptools ; extra == 'testing' + - xmlschema ; extra == 'testing' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl + name: pyopenssl + version: 26.3.0 + sha256: 46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 + requires_dist: + - cryptography>=49.0.0,<50 + - typing-extensions>=4.9 ; python_full_version < '3.13' + - pytest-rerunfailures ; extra == 'test' + - pretend ; extra == 'test' + - pytest>=3.0.1 ; extra == 'test' + - sphinx!=5.2.0,!=5.2.0.post0,!=7.2.5 ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + name: pluggy + version: 1.6.0 + sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + requires_dist: + - pre-commit ; extra == 'dev' + - tox ; extra == 'dev' + - pytest ; extra == 'testing' + - pytest-benchmark ; extra == 'testing' + - coverage ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl + name: watchfiles + version: 1.2.0 + sha256: c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 + requires_dist: + - anyio>=3.0.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl + name: pydantic-core + version: 2.46.4 + sha256: da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5a/7d/5c0cc66fb90a1b14474eebb5ab535eacc51cb20b0e45358348b51c07abc9/duckdb-1.5.4-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: duckdb + version: 1.5.4 + sha256: a9a10dc40469b9c0e458625d2a8359461a982c6151bb53ff259fea00c4695ad4 + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl + name: msgpack + version: 1.2.1 + sha256: 8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl + name: typeguard + version: 4.5.2 + sha256: fcf9de18bd945cdb4c7b996e12b4c51ce83f92f191314a6d7cf1739586ec98cf + requires_dist: + - importlib-metadata>=3.6 ; python_full_version < '3.10' + - typing-extensions>=4.14.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl + name: protobuf + version: 6.33.6 + sha256: 9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl + name: psycopg + version: 3.3.4 + sha256: b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a + requires_dist: + - typing-extensions>=4.6 ; python_full_version < '3.13' + - tzdata ; sys_platform == 'win32' + - psycopg-c==3.3.4 ; implementation_name != 'pypy' and extra == 'c' + - psycopg-binary==3.3.4 ; implementation_name != 'pypy' and extra == 'binary' + - psycopg-pool ; extra == 'pool' + - anyio>=4.0 ; extra == 'test' + - mypy>=1.19.0 ; implementation_name != 'pypy' and extra == 'test' + - pproxy>=2.7 ; extra == 'test' + - pytest>=6.2.5 ; extra == 'test' + - pytest-cov>=3.0 ; extra == 'test' + - pytest-randomly>=3.5 ; extra == 'test' + - ast-comments>=1.1.2 ; extra == 'dev' + - black>=26.1.0 ; extra == 'dev' + - codespell>=2.2 ; extra == 'dev' + - cython-lint>=0.16 ; extra == 'dev' + - dnspython>=2.1 ; extra == 'dev' + - flake8>=4.0 ; extra == 'dev' + - isort[colors]>=6.0 ; extra == 'dev' + - isort-psycopg ; extra == 'dev' + - mypy>=1.19.0 ; extra == 'dev' + - pre-commit>=4.0.1 ; extra == 'dev' + - types-setuptools>=57.4 ; extra == 'dev' + - types-shapely>=2.0 ; extra == 'dev' + - wheel>=0.37 ; extra == 'dev' + - sphinx>=9.1 ; extra == 'docs' + - furo==2025.12.19 ; extra == 'docs' + - sphinx-autobuild>=2025.8.25 ; extra == 'docs' + - sphinx-autodoc-typehints>=3.10.2 ; extra == 'docs' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5d/39/b72e168daf9c00fb20c9fc996d00437ccecdef3102387775d29d7a62576d/numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl + name: numpy + version: 2.5.0 + sha256: 28e7137057d551e4a83c4ae414e3451f50568409db7569aacc7f9811ee06a446 + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl + name: pyasn1 + version: 0.6.3 + sha256: a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: frozenlist + version: 1.8.0 + sha256: f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5e/71/4dd20c69332a2a4bf7ece8a655c9da98e4bd9b6bcea235349c1a00399d57/pyspark-4.1.2.tar.gz + name: pyspark + version: 4.1.2 + sha256: fa5d6159f700d0990a07f4f62df1b7449401dccee9cd7d5d6df8957530841602 + requires_dist: + - py4j>=0.10.9.7,<0.10.9.10 + - numpy>=1.21 ; extra == 'ml' + - numpy>=1.21 ; extra == 'mllib' + - pandas>=2.2.0 ; extra == 'sql' + - pyarrow>=15.0.0 ; extra == 'sql' + - numpy>=1.21 ; extra == 'sql' + - pandas>=2.2.0 ; extra == 'pandas-on-spark' + - pyarrow>=15.0.0 ; extra == 'pandas-on-spark' + - numpy>=1.21 ; extra == 'pandas-on-spark' + - pandas>=2.2.0 ; extra == 'connect' + - pyarrow>=15.0.0 ; extra == 'connect' + - grpcio>=1.76.0 ; extra == 'connect' + - grpcio-status>=1.76.0 ; extra == 'connect' + - googleapis-common-protos>=1.71.0 ; extra == 'connect' + - zstandard>=0.25.0 ; extra == 'connect' + - numpy>=1.21 ; extra == 'connect' + - pandas>=2.2.0 ; extra == 'pipelines' + - pyarrow>=15.0.0 ; extra == 'pipelines' + - numpy>=1.21 ; extra == 'pipelines' + - grpcio>=1.76.0 ; extra == 'pipelines' + - grpcio-status>=1.76.0 ; extra == 'pipelines' + - googleapis-common-protos>=1.71.0 ; extra == 'pipelines' + - zstandard>=0.25.0 ; extra == 'pipelines' + - pyyaml>=3.11 ; extra == 'pipelines' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pycryptodome + version: 3.23.0 + sha256: c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl + name: aiohappyeyeballs + version: 2.6.2 + sha256: 4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl + name: uvicorn + version: 0.34.0 + sha256: 023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 + requires_dist: + - click>=7.0 + - h11>=0.8 + - typing-extensions>=4.0 ; python_full_version < '3.11' + - colorama>=0.4 ; sys_platform == 'win32' and extra == 'standard' + - httptools>=0.6.3 ; extra == 'standard' + - python-dotenv>=0.13 ; extra == 'standard' + - pyyaml>=5.1 ; extra == 'standard' + - uvloop>=0.14.0,!=0.15.0,!=0.15.1 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' and extra == 'standard' + - watchfiles>=0.13 ; extra == 'standard' + - websockets>=10.4 ; extra == 'standard' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: rpds-py + version: 0.30.0 + sha256: 0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/61/c5/c2ceba832fe3f47cfd7e11cd7cc7a1bbc2c028424c5bca70435aa4ca1dec/ray-2.49.2-cp310-cp310-macosx_12_0_x86_64.whl + name: ray + version: 2.49.2 + sha256: 3e441bf2acd7f368cf45132752066c5c3b83d88cd5f85762e703774bba4f2b6d + requires_dist: + - click>=7.0 + - filelock + - jsonschema + - msgpack>=1.0.0,<2.0.0 + - packaging + - protobuf>=3.20.3 + - pyyaml + - requests + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'cgraph' + - grpcio!=1.56.0 ; sys_platform == 'darwin' and extra == 'client' + - grpcio ; extra == 'client' + - numpy>=1.20 ; extra == 'data' + - pandas>=1.3 ; extra == 'data' + - pyarrow>=9.0.0 ; extra == 'data' + - fsspec ; extra == 'data' + - aiohttp>=3.7 ; extra == 'default' + - aiohttp-cors ; extra == 'default' + - colorful ; extra == 'default' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'default' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'default' + - requests ; extra == 'default' + - grpcio>=1.32.0 ; python_full_version < '3.10' and extra == 'default' + - grpcio>=1.42.0 ; python_full_version >= '3.10' and extra == 'default' + - opencensus ; extra == 'default' + - opentelemetry-sdk>=1.30.0 ; extra == 'default' + - opentelemetry-exporter-prometheus ; extra == 'default' + - opentelemetry-proto ; extra == 'default' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3 ; extra == 'default' + - prometheus-client>=0.7.1 ; extra == 'default' + - smart-open ; extra == 'default' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'default' + - memray ; sys_platform != 'win32' and extra == 'observability' + - smart-open ; extra == 'serve' + - aiohttp>=3.7 ; extra == 'serve' + - grpcio>=1.42.0 ; python_full_version >= '3.10' and extra == 'serve' + - prometheus-client>=0.7.1 ; extra == 'serve' + - fastapi ; extra == 'serve' + - opencensus ; extra == 'serve' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'serve' + - grpcio>=1.32.0 ; python_full_version < '3.10' and extra == 'serve' + - opentelemetry-sdk>=1.30.0 ; extra == 'serve' + - aiohttp-cors ; extra == 'serve' + - requests ; extra == 'serve' + - starlette ; extra == 'serve' + - opentelemetry-exporter-prometheus ; extra == 'serve' + - watchfiles ; extra == 'serve' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'serve' + - uvicorn[standard] ; extra == 'serve' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'serve' + - colorful ; extra == 'serve' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3 ; extra == 'serve' + - opentelemetry-proto ; extra == 'serve' + - pandas ; extra == 'tune' + - tensorboardx>=1.9 ; extra == 'tune' + - requests ; extra == 'tune' + - pyarrow>=9.0.0 ; extra == 'tune' + - fsspec ; extra == 'tune' + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'adag' + - smart-open ; extra == 'serve-grpc' + - aiohttp>=3.7 ; extra == 'serve-grpc' + - grpcio>=1.42.0 ; python_full_version >= '3.10' and extra == 'serve-grpc' + - prometheus-client>=0.7.1 ; extra == 'serve-grpc' + - fastapi ; extra == 'serve-grpc' + - opencensus ; extra == 'serve-grpc' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'serve-grpc' + - grpcio>=1.32.0 ; python_full_version < '3.10' and extra == 'serve-grpc' + - opentelemetry-sdk>=1.30.0 ; extra == 'serve-grpc' + - aiohttp-cors ; extra == 'serve-grpc' + - requests ; extra == 'serve-grpc' + - starlette ; extra == 'serve-grpc' + - opentelemetry-exporter-prometheus ; extra == 'serve-grpc' + - watchfiles ; extra == 'serve-grpc' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'serve-grpc' + - uvicorn[standard] ; extra == 'serve-grpc' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'serve-grpc' + - colorful ; extra == 'serve-grpc' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3 ; extra == 'serve-grpc' + - opentelemetry-proto ; extra == 'serve-grpc' + - pyopenssl ; extra == 'serve-grpc' + - smart-open ; extra == 'serve-async-inference' + - aiohttp>=3.7 ; extra == 'serve-async-inference' + - celery ; extra == 'serve-async-inference' + - grpcio>=1.42.0 ; python_full_version >= '3.10' and extra == 'serve-async-inference' + - prometheus-client>=0.7.1 ; extra == 'serve-async-inference' + - fastapi ; extra == 'serve-async-inference' + - opencensus ; extra == 'serve-async-inference' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'serve-async-inference' + - grpcio>=1.32.0 ; python_full_version < '3.10' and extra == 'serve-async-inference' + - opentelemetry-sdk>=1.30.0 ; extra == 'serve-async-inference' + - aiohttp-cors ; extra == 'serve-async-inference' + - requests ; extra == 'serve-async-inference' + - starlette ; extra == 'serve-async-inference' + - opentelemetry-exporter-prometheus ; extra == 'serve-async-inference' + - watchfiles ; extra == 'serve-async-inference' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'serve-async-inference' + - uvicorn[standard] ; extra == 'serve-async-inference' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'serve-async-inference' + - colorful ; extra == 'serve-async-inference' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3 ; extra == 'serve-async-inference' + - opentelemetry-proto ; extra == 'serve-async-inference' + - ray-cpp==2.49.2 ; extra == 'cpp' + - pandas ; extra == 'rllib' + - tensorboardx>=1.9 ; extra == 'rllib' + - requests ; extra == 'rllib' + - pyarrow>=9.0.0 ; extra == 'rllib' + - fsspec ; extra == 'rllib' + - dm-tree ; extra == 'rllib' + - gymnasium==1.1.1 ; extra == 'rllib' + - lz4 ; extra == 'rllib' + - ormsgpack==1.7.0 ; extra == 'rllib' + - pyyaml ; extra == 'rllib' + - scipy ; extra == 'rllib' + - pandas ; extra == 'train' + - tensorboardx>=1.9 ; extra == 'train' + - requests ; extra == 'train' + - pyarrow>=9.0.0 ; extra == 'train' + - fsspec ; extra == 'train' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3 ; extra == 'train' + - smart-open ; extra == 'air' + - aiohttp>=3.7 ; extra == 'air' + - grpcio>=1.42.0 ; python_full_version >= '3.10' and extra == 'air' + - fsspec ; extra == 'air' + - prometheus-client>=0.7.1 ; extra == 'air' + - fastapi ; extra == 'air' + - pandas ; extra == 'air' + - opencensus ; extra == 'air' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'air' + - grpcio>=1.32.0 ; python_full_version < '3.10' and extra == 'air' + - opentelemetry-sdk>=1.30.0 ; extra == 'air' + - numpy>=1.20 ; extra == 'air' + - aiohttp-cors ; extra == 'air' + - requests ; extra == 'air' + - starlette ; extra == 'air' + - opentelemetry-exporter-prometheus ; extra == 'air' + - watchfiles ; extra == 'air' + - pyarrow>=9.0.0 ; extra == 'air' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'air' + - uvicorn[standard] ; extra == 'air' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'air' + - colorful ; extra == 'air' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3 ; extra == 'air' + - tensorboardx>=1.9 ; extra == 'air' + - pandas>=1.3 ; extra == 'air' + - opentelemetry-proto ; extra == 'air' + - smart-open ; extra == 'all' + - aiohttp>=3.7 ; extra == 'all' + - celery ; extra == 'all' + - grpcio>=1.42.0 ; python_full_version >= '3.10' and extra == 'all' + - fsspec ; extra == 'all' + - grpcio!=1.56.0 ; sys_platform == 'darwin' and extra == 'all' + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'all' + - prometheus-client>=0.7.1 ; extra == 'all' + - gymnasium==1.1.1 ; extra == 'all' + - ormsgpack==1.7.0 ; extra == 'all' + - scipy ; extra == 'all' + - fastapi ; extra == 'all' + - pandas ; extra == 'all' + - opencensus ; extra == 'all' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'all' + - grpcio>=1.32.0 ; python_full_version < '3.10' and extra == 'all' + - opentelemetry-sdk>=1.30.0 ; extra == 'all' + - dm-tree ; extra == 'all' + - numpy>=1.20 ; extra == 'all' + - lz4 ; extra == 'all' + - aiohttp-cors ; extra == 'all' + - requests ; extra == 'all' + - starlette ; extra == 'all' + - memray ; sys_platform != 'win32' and extra == 'all' + - opentelemetry-exporter-prometheus ; extra == 'all' + - watchfiles ; extra == 'all' + - pyyaml ; extra == 'all' + - pyarrow>=9.0.0 ; extra == 'all' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'all' + - grpcio ; extra == 'all' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'all' + - uvicorn[standard] ; extra == 'all' + - colorful ; extra == 'all' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3 ; extra == 'all' + - tensorboardx>=1.9 ; extra == 'all' + - pandas>=1.3 ; extra == 'all' + - opentelemetry-proto ; extra == 'all' + - pyopenssl ; extra == 'all' + - smart-open ; extra == 'all-cpp' + - aiohttp>=3.7 ; extra == 'all-cpp' + - celery ; extra == 'all-cpp' + - grpcio>=1.42.0 ; python_full_version >= '3.10' and extra == 'all-cpp' + - fsspec ; extra == 'all-cpp' + - grpcio!=1.56.0 ; sys_platform == 'darwin' and extra == 'all-cpp' + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'all-cpp' + - prometheus-client>=0.7.1 ; extra == 'all-cpp' + - gymnasium==1.1.1 ; extra == 'all-cpp' + - ormsgpack==1.7.0 ; extra == 'all-cpp' + - scipy ; extra == 'all-cpp' + - fastapi ; extra == 'all-cpp' + - pandas ; extra == 'all-cpp' + - opencensus ; extra == 'all-cpp' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'all-cpp' + - grpcio>=1.32.0 ; python_full_version < '3.10' and extra == 'all-cpp' + - dm-tree ; extra == 'all-cpp' + - opentelemetry-sdk>=1.30.0 ; extra == 'all-cpp' + - numpy>=1.20 ; extra == 'all-cpp' + - lz4 ; extra == 'all-cpp' + - aiohttp-cors ; extra == 'all-cpp' + - requests ; extra == 'all-cpp' + - starlette ; extra == 'all-cpp' + - memray ; sys_platform != 'win32' and extra == 'all-cpp' + - opentelemetry-exporter-prometheus ; extra == 'all-cpp' + - watchfiles ; extra == 'all-cpp' + - pyyaml ; extra == 'all-cpp' + - pyarrow>=9.0.0 ; extra == 'all-cpp' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'all-cpp' + - grpcio ; extra == 'all-cpp' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'all-cpp' + - uvicorn[standard] ; extra == 'all-cpp' + - ray-cpp==2.49.2 ; extra == 'all-cpp' + - colorful ; extra == 'all-cpp' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3 ; extra == 'all-cpp' + - tensorboardx>=1.9 ; extra == 'all-cpp' + - pandas>=1.3 ; extra == 'all-cpp' + - opentelemetry-proto ; extra == 'all-cpp' + - pyopenssl ; extra == 'all-cpp' + - smart-open ; extra == 'llm' + - typer ; extra == 'llm' + - aiohttp>=3.7 ; extra == 'llm' + - grpcio>=1.42.0 ; python_full_version >= '3.10' and extra == 'llm' + - fsspec ; extra == 'llm' + - vllm>=0.10.0 ; extra == 'llm' + - async-timeout ; python_full_version < '3.11' and extra == 'llm' + - prometheus-client>=0.7.1 ; extra == 'llm' + - fastapi ; extra == 'llm' + - hf-transfer ; extra == 'llm' + - jsonschema ; extra == 'llm' + - opencensus ; extra == 'llm' + - jsonref>=1.1.0 ; extra == 'llm' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'llm' + - grpcio>=1.32.0 ; python_full_version < '3.10' and extra == 'llm' + - opentelemetry-sdk>=1.30.0 ; extra == 'llm' + - numpy>=1.20 ; extra == 'llm' + - ninja ; extra == 'llm' + - aiohttp-cors ; extra == 'llm' + - requests ; extra == 'llm' + - starlette ; extra == 'llm' + - opentelemetry-exporter-prometheus ; extra == 'llm' + - watchfiles ; extra == 'llm' + - pyarrow>=9.0.0 ; extra == 'llm' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'llm' + - uvicorn[standard] ; extra == 'llm' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'llm' + - colorful ; extra == 'llm' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,<3 ; extra == 'llm' + - pandas>=1.3 ; extra == 'llm' + - opentelemetry-proto ; extra == 'llm' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + name: jinja2 + version: 3.1.6 + sha256: 85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + requires_dist: + - markupsafe>=2.0 + - babel>=2.7 ; extra == 'i18n' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl + name: frozenlist + version: 1.8.0 + sha256: a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: websockets + version: '16.0' + sha256: 781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl + name: attrs + version: 26.1.0 + sha256: c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl + name: pyarrow + version: 24.0.0 + sha256: e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + name: jsonschema + version: 4.26.0 + sha256: d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + requires_dist: + - attrs>=22.2.0 + - jsonschema-specifications>=2023.3.6 + - referencing>=0.28.4 + - rpds-py>=0.25.0 + - fqdn ; extra == 'format' + - idna ; extra == 'format' + - isoduration ; extra == 'format' + - jsonpointer>1.13 ; extra == 'format' + - rfc3339-validator ; extra == 'format' + - rfc3987 ; extra == 'format' + - uri-template ; extra == 'format' + - webcolors>=1.11 ; extra == 'format' + - fqdn ; extra == 'format-nongpl' + - idna ; extra == 'format-nongpl' + - isoduration ; extra == 'format-nongpl' + - jsonpointer>1.13 ; extra == 'format-nongpl' + - rfc3339-validator ; extra == 'format-nongpl' + - rfc3986-validator>0.1.0 ; extra == 'format-nongpl' + - rfc3987-syntax>=1.1.0 ; extra == 'format-nongpl' + - uri-template ; extra == 'format-nongpl' + - webcolors>=24.6.0 ; extra == 'format-nongpl' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl + name: tomlkit + version: 0.15.0 + sha256: 4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/6e/24/e6b7a8fe8b9e336d684779a88027b261374417f2be7c5a0fcdb40f0c8cc5/deltalake-0.25.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: deltalake + version: 0.25.5 + sha256: 0b36afba5936f74c42920c06d140535e6efc8361f659770014944d8e69fbca09 + requires_dist: + - pyarrow>=16,!=19.0.0 + - pandas ; extra == 'pandas' + - azure-storage-blob==12.20.0 ; extra == 'devel' + - packaging>=20 ; extra == 'devel' + - pytest ; extra == 'devel' + - pytest-mock ; extra == 'devel' + - pytest-cov ; extra == 'devel' + - pytest-timeout ; extra == 'devel' + - sphinx<=4.5 ; extra == 'devel' + - sphinx-rtd-theme ; extra == 'devel' + - toml ; extra == 'devel' + - wheel ; extra == 'devel' + - pip>=24.0 ; extra == 'devel' + - pytest-benchmark ; extra == 'devel' + - mypy==1.10.1 ; extra == 'devel' + - ruff==0.5.2 ; extra == 'devel' + - polars==1.17.1 ; extra == 'polars' + - lakefs==0.8.0 ; extra == 'lakefs' + - pyspark ; extra == 'pyspark' + - delta-spark ; extra == 'pyspark' + - numpy==1.26.4 ; extra == 'pyspark' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl + name: pycryptodome + version: 3.23.0 + sha256: cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl + name: jwcrypto + version: 1.5.8 + sha256: 85aeb475f808d56bbc2f2ed1f6f73e6a317c4011a4321505f02f0aed695a3742 + requires_dist: + - cryptography>=39.0.0 + - typing-extensions>=4.5.0 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/6e/87/174bbfb5613794862c45b5cdf567fcfb478ad5a3a7b594e11d998656b2f9/grpcio_health_checking-1.81.1-py3-none-any.whl + name: grpcio-health-checking + version: 1.81.1 + sha256: cbc6a4171825ec64389de2f062d296ba129a5c27eefd0dd55fa837909184bdf9 + requires_dist: + - protobuf>=6.33.5,<7.0.0 + - grpcio>=1.81.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: librt + version: 0.11.0 + sha256: 88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/6f/75/2a08a6062228720747570a07ab42e6f5826725f09c9a95d75b7b5b938022/hiredis-3.4.0-cp310-cp310-macosx_10_15_x86_64.whl + name: hiredis + version: 3.4.0 + sha256: 4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl + name: mmh3 + version: 5.2.1 + sha256: 72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 + requires_dist: + - pytest==9.0.2 ; extra == 'test' + - pytest-sugar==1.1.1 ; extra == 'test' + - actionlint-py==1.7.11.24 ; extra == 'lint' + - clang-format==22.1.0 ; extra == 'lint' + - codespell==2.4.1 ; extra == 'lint' + - pylint==4.0.5 ; extra == 'lint' + - ruff==0.15.4 ; extra == 'lint' + - mypy==1.19.1 ; extra == 'type' + - myst-parser==5.0.0 ; extra == 'docs' + - shibuya==2026.1.9 ; extra == 'docs' + - sphinx==8.2.3 ; extra == 'docs' + - sphinx-copybutton==0.5.2 ; extra == 'docs' + - pymmh3==0.0.5 ; extra == 'benchmark' + - pyperf==2.10.0 ; extra == 'benchmark' + - xxhash==3.6.0 ; extra == 'benchmark' + - matplotlib==3.10.8 ; extra == 'plot' + - pandas==3.0.1 ; extra == 'plot' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl + name: sqlalchemy + version: 2.0.51 + sha256: 0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl + name: partd + version: 1.4.2 + sha256: 978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f + requires_dist: + - locket + - toolz + - numpy>=1.20.0 ; extra == 'complete' + - pandas>=1.3 ; extra == 'complete' + - pyzmq ; extra == 'complete' + - blosc ; extra == 'complete' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/73/18/9da267ade389d4e7e533ac0c77b3a7041513a66efab93beb84f27627b0b8/duckdb-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl + name: duckdb + version: 1.5.4 + sha256: 360f2542d09759c3739400f8b787e29b43ba0da665c21756216291458bf6fc59 + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/74/19/996b45846fcc5f2015bd70ed98420b11c847c1b79b57b82b250e0a409f49/snowflake_connector_python-4.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: snowflake-connector-python + version: 4.6.0 + sha256: fe9005d226b234bf190409e5d7e8db9f7daba271880de9105f5173a6858b8e6b + requires_dist: + - asn1crypto>0.24.0,<2.0.0 + - cryptography>=46.0.5 + - pyopenssl>=24.0.0 + - pyjwt>=2.10.1,<3.0.0 + - pytz + - requests>=2.32.4,<3.0.0 + - packaging + - charset-normalizer>=2,<4 + - idna>=3.7,<4 + - certifi>=2024.7.4 + - typing-extensions>=4.3,<5 + - filelock>=3.5,<4 + - sortedcontainers>=2.4.0 + - platformdirs>=2.6.0,<5.0.0 + - tomlkit + - boto3>=1.24 + - botocore>=1.24 + - boto3>=1.24 ; extra == 'boto' + - botocore>=1.24 ; extra == 'boto' + - cython ; extra == 'development' + - coverage ; extra == 'development' + - mitmproxy>=12.0.0 ; python_full_version >= '3.12' and extra == 'development' + - more-itertools ; extra == 'development' + - numpy<=2.4.3 ; extra == 'development' + - pendulum!=2.1.1 ; extra == 'development' + - pexpect ; extra == 'development' + - pytest<7.5.0 ; extra == 'development' + - pytest-asyncio ; extra == 'development' + - pytest-cov ; extra == 'development' + - pytest-rerunfailures<16.0 ; extra == 'development' + - pytest-timeout ; extra == 'development' + - pytest-xdist ; extra == 'development' + - pytzdata ; extra == 'development' + - responses ; extra == 'development' + - pandas>=1.0.0,<3.0.0 ; python_full_version < '3.13' and extra == 'pandas' + - pandas>=2.1.2,<3.0.0 ; python_full_version >= '3.13' and extra == 'pandas' + - pyarrow>=14.0.1 ; extra == 'pandas' + - keyring>=23.1.0,<26.0.0 ; extra == 'secure-local-storage' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/74/7a/ec22ff9d5c891b4f9ae834ef70524c92bd59d1408e9944e2652c87bc3f02/deltalake-0.25.5-cp39-abi3-macosx_11_0_arm64.whl + name: deltalake + version: 0.25.5 + sha256: e8f0d24bf64455f702da8402307b22e01f91e0f76694f7c5e33c9513011e8d29 + requires_dist: + - pyarrow>=16,!=19.0.0 + - pandas ; extra == 'pandas' + - azure-storage-blob==12.20.0 ; extra == 'devel' + - packaging>=20 ; extra == 'devel' + - pytest ; extra == 'devel' + - pytest-mock ; extra == 'devel' + - pytest-cov ; extra == 'devel' + - pytest-timeout ; extra == 'devel' + - sphinx<=4.5 ; extra == 'devel' + - sphinx-rtd-theme ; extra == 'devel' + - toml ; extra == 'devel' + - wheel ; extra == 'devel' + - pip>=24.0 ; extra == 'devel' + - pytest-benchmark ; extra == 'devel' + - mypy==1.10.1 ; extra == 'devel' + - ruff==0.5.2 ; extra == 'devel' + - polars==1.17.1 ; extra == 'polars' + - lakefs==0.8.0 ; extra == 'lakefs' + - pyspark ; extra == 'pyspark' + - delta-spark ; extra == 'pyspark' + - numpy==1.26.4 ; extra == 'pyspark' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl + name: prometheus-client + version: 0.24.1 + sha256: 150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 + requires_dist: + - twisted ; extra == 'twisted' + - aiohttp ; extra == 'aiohttp' + - django ; extra == 'django' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl + name: librt + version: 0.11.0 + sha256: b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/76/b4/ad73c1a396288e443b18af50819448060b318c1e933305167c1d7f98a507/duckdb-1.5.4-cp310-cp310-macosx_11_0_arm64.whl + name: duckdb + version: 1.5.4 + sha256: 0cf932055061e544d3fa27cc6c147da25f3f681ee5980157fb55e77d6c2d9c63 + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/77/fc/8cb9073bb1bee54eb49a1ae501a36402d01763812962ac811cdc1c81a9d7/parsy-2.2-py3-none-any.whl + name: parsy + version: '2.2' + sha256: 5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + name: annotated-types + version: 0.7.0 + sha256: 1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 + requires_dist: + - typing-extensions>=4.0.0 ; python_full_version < '3.9' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + name: mypy-extensions + version: 1.1.0 + sha256: 1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 + requires_python: '>=3.8' +- pypi: 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 + name: pyyaml + version: 6.0.3 + sha256: 9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/7a/1e/8b098aeb69ef87de5195d076ce6d82002c3ebf1920373d386ca7b6e63cfa/google_cloud_bigquery-3.42.1-py3-none-any.whl + name: google-cloud-bigquery + version: 3.42.1 + sha256: 159143c1b7248858f35549c09efea5d031c2883145623419ea7c505a50c0899a + requires_dist: + - google-api-core[grpc]>=2.11.1,<3.0.0 + - google-auth[pyopenssl]>=2.14.1,<3.0.0 + - google-cloud-core>=2.4.1,<3.0.0 + - google-resumable-media>=2.0.0,<3.0.0 + - packaging>=24.2.0 + - python-dateutil>=2.8.2,<3.0.0 + - requests>=2.21.0,<3.0.0 + - google-cloud-bigquery-storage>=2.18.0,<3.0.0 ; extra == 'bqstorage' + - grpcio>=1.47.0,<2.0.0 ; extra == 'bqstorage' + - grpcio>=1.49.1,<2.0.0 ; python_full_version >= '3.11' and extra == 'bqstorage' + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'bqstorage' + - pyarrow>=4.0.0 ; extra == 'bqstorage' + - pandas>=1.3.0 ; extra == 'pandas' + - pandas-gbq>=0.26.1 ; extra == 'pandas' + - grpcio>=1.47.0,<2.0.0 ; extra == 'pandas' + - grpcio>=1.49.1,<2.0.0 ; python_full_version >= '3.11' and extra == 'pandas' + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'pandas' + - pyarrow>=3.0.0 ; extra == 'pandas' + - db-dtypes>=1.0.4,<2.0.0 ; extra == 'pandas' + - ipywidgets>=7.7.1 ; extra == 'ipywidgets' + - ipykernel>=6.2.0 ; extra == 'ipywidgets' + - geopandas>=0.9.0,<2.0.0 ; extra == 'geopandas' + - shapely>=1.8.4,<3.0.0 ; extra == 'geopandas' + - ipython>=7.23.1 ; extra == 'ipython' + - bigquery-magics>=0.6.0 ; extra == 'ipython' + - matplotlib>=3.10.3 ; extra == 'matplotlib' + - tqdm>=4.23.4,<5.0.0 ; extra == 'tqdm' + - opentelemetry-api>=1.1.0 ; extra == 'opentelemetry' + - opentelemetry-sdk>=1.1.0 ; extra == 'opentelemetry' + - opentelemetry-instrumentation>=0.20b0 ; extra == 'opentelemetry' + - proto-plus>=1.22.3,<2.0.0 ; extra == 'bigquery-v2' + - protobuf>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0 ; extra == 'bigquery-v2' + - google-cloud-bigquery[bigquery-v2,bqstorage,geopandas,ipython,ipywidgets,matplotlib,opentelemetry,pandas,tqdm] ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl + name: hf-xet + version: 1.5.1 + sha256: 6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e + requires_dist: + - pytest ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + name: tomli + version: 2.4.1 + sha256: 0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl + name: proto-plus + version: 1.28.0 + sha256: a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8 + requires_dist: + - protobuf>=4.25.8,<8.0.0 + - google-api-core>=1.31.5 ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: watchfiles + version: 1.2.0 + sha256: 054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 + requires_dist: + - anyio>=3.0.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7e/d0/a85097dd53aaca1a44acc4dd0b3d2c0e9233179433e2ee326e4018ab3cf7/ray-2.55.1-cp310-cp310-macosx_12_0_arm64.whl + name: ray + version: 2.55.1 + sha256: 2d5786661e192148719accc959def6cdcabd7a24cd9008005bf3d0e3c8cfd529 + requires_dist: + - click>=7.0 + - filelock + - jsonschema + - msgpack>=1.0.0,<2.0.0 + - packaging>=24.2 + - protobuf>=3.20.3 + - pyyaml + - requests + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'cgraph' + - grpcio!=1.56.0 ; sys_platform == 'darwin' and extra == 'client' + - grpcio ; extra == 'client' + - numpy>=1.20 ; extra == 'data' + - pandas>=1.3 ; extra == 'data' + - pyarrow>=9.0.0 ; extra == 'data' + - fsspec ; extra == 'data' + - aiohttp>=3.13.3 ; extra == 'default' + - aiohttp-cors ; extra == 'default' + - colorful ; extra == 'default' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'default' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'default' + - requests ; extra == 'default' + - grpcio>=1.42.0 ; extra == 'default' + - opencensus ; extra == 'default' + - opentelemetry-sdk>=1.30.0 ; extra == 'default' + - opentelemetry-exporter-prometheus ; extra == 'default' + - opentelemetry-proto ; extra == 'default' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'default' + - prometheus-client>=0.7.1 ; extra == 'default' + - smart-open ; extra == 'default' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'default' + - memray ; sys_platform != 'win32' and extra == 'observability' + - watchfiles ; extra == 'serve' + - opentelemetry-proto ; extra == 'serve' + - opencensus ; extra == 'serve' + - grpcio>=1.42.0 ; extra == 'serve' + - requests ; extra == 'serve' + - aiohttp>=3.13.3 ; extra == 'serve' + - colorful ; extra == 'serve' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'serve' + - opentelemetry-exporter-prometheus ; extra == 'serve' + - prometheus-client>=0.7.1 ; extra == 'serve' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'serve' + - aiohttp-cors ; extra == 'serve' + - smart-open ; extra == 'serve' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'serve' + - fastapi ; extra == 'serve' + - opentelemetry-sdk>=1.30.0 ; extra == 'serve' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'serve' + - starlette ; extra == 'serve' + - uvicorn[standard] ; extra == 'serve' + - pandas ; extra == 'tune' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'tune' + - tensorboardx>=1.9 ; extra == 'tune' + - requests ; extra == 'tune' + - pyarrow>=9.0.0 ; extra == 'tune' + - fsspec ; extra == 'tune' + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'adag' + - watchfiles ; extra == 'serve-grpc' + - opentelemetry-proto ; extra == 'serve-grpc' + - opencensus ; extra == 'serve-grpc' + - grpcio>=1.42.0 ; extra == 'serve-grpc' + - pyopenssl ; extra == 'serve-grpc' + - requests ; extra == 'serve-grpc' + - aiohttp>=3.13.3 ; extra == 'serve-grpc' + - colorful ; extra == 'serve-grpc' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'serve-grpc' + - opentelemetry-exporter-prometheus ; extra == 'serve-grpc' + - prometheus-client>=0.7.1 ; extra == 'serve-grpc' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'serve-grpc' + - aiohttp-cors ; extra == 'serve-grpc' + - smart-open ; extra == 'serve-grpc' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'serve-grpc' + - fastapi ; extra == 'serve-grpc' + - opentelemetry-sdk>=1.30.0 ; extra == 'serve-grpc' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'serve-grpc' + - starlette ; extra == 'serve-grpc' + - uvicorn[standard] ; extra == 'serve-grpc' + - watchfiles ; extra == 'serve-async-inference' + - opentelemetry-proto ; extra == 'serve-async-inference' + - taskiq ; extra == 'serve-async-inference' + - opencensus ; extra == 'serve-async-inference' + - grpcio>=1.42.0 ; extra == 'serve-async-inference' + - requests ; extra == 'serve-async-inference' + - aiohttp>=3.13.3 ; extra == 'serve-async-inference' + - colorful ; extra == 'serve-async-inference' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'serve-async-inference' + - opentelemetry-exporter-prometheus ; extra == 'serve-async-inference' + - prometheus-client>=0.7.1 ; extra == 'serve-async-inference' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'serve-async-inference' + - aiohttp-cors ; extra == 'serve-async-inference' + - smart-open ; extra == 'serve-async-inference' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'serve-async-inference' + - fastapi ; extra == 'serve-async-inference' + - opentelemetry-sdk>=1.30.0 ; extra == 'serve-async-inference' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'serve-async-inference' + - starlette ; extra == 'serve-async-inference' + - uvicorn[standard] ; extra == 'serve-async-inference' + - celery ; extra == 'serve-async-inference' + - ray-cpp==2.55.1 ; extra == 'cpp' + - pandas ; extra == 'rllib' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'rllib' + - tensorboardx>=1.9 ; extra == 'rllib' + - requests ; extra == 'rllib' + - pyarrow>=9.0.0 ; extra == 'rllib' + - fsspec ; extra == 'rllib' + - dm-tree ; extra == 'rllib' + - gymnasium==1.2.2 ; extra == 'rllib' + - lz4 ; extra == 'rllib' + - ormsgpack>=1.7.0 ; extra == 'rllib' + - pyyaml ; extra == 'rllib' + - scipy ; extra == 'rllib' + - pandas ; extra == 'train' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'train' + - tensorboardx>=1.9 ; extra == 'train' + - requests ; extra == 'train' + - pyarrow>=9.0.0 ; extra == 'train' + - fsspec ; extra == 'train' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'train' + - watchfiles ; extra == 'air' + - fsspec ; extra == 'air' + - tensorboardx>=1.9 ; extra == 'air' + - opentelemetry-proto ; extra == 'air' + - pandas>=1.3 ; extra == 'air' + - opencensus ; extra == 'air' + - grpcio>=1.42.0 ; extra == 'air' + - requests ; extra == 'air' + - aiohttp>=3.13.3 ; extra == 'air' + - colorful ; extra == 'air' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'air' + - opentelemetry-exporter-prometheus ; extra == 'air' + - prometheus-client>=0.7.1 ; extra == 'air' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'air' + - aiohttp-cors ; extra == 'air' + - pyarrow>=9.0.0 ; extra == 'air' + - smart-open ; extra == 'air' + - pandas ; extra == 'air' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'air' + - numpy>=1.20 ; extra == 'air' + - fastapi ; extra == 'air' + - opentelemetry-sdk>=1.30.0 ; extra == 'air' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'air' + - starlette ; extra == 'air' + - uvicorn[standard] ; extra == 'air' + - watchfiles ; extra == 'all' + - grpcio ; extra == 'all' + - gymnasium==1.2.2 ; extra == 'all' + - fsspec ; extra == 'all' + - tensorboardx>=1.9 ; extra == 'all' + - opentelemetry-proto ; extra == 'all' + - pandas>=1.3 ; extra == 'all' + - taskiq ; extra == 'all' + - opencensus ; extra == 'all' + - grpcio>=1.42.0 ; extra == 'all' + - ormsgpack>=1.7.0 ; extra == 'all' + - pyopenssl ; extra == 'all' + - grpcio!=1.56.0 ; sys_platform == 'darwin' and extra == 'all' + - requests ; extra == 'all' + - aiohttp>=3.13.3 ; extra == 'all' + - colorful ; extra == 'all' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'all' + - opentelemetry-exporter-prometheus ; extra == 'all' + - scipy ; extra == 'all' + - prometheus-client>=0.7.1 ; extra == 'all' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'all' + - aiohttp-cors ; extra == 'all' + - pyarrow>=9.0.0 ; extra == 'all' + - smart-open ; extra == 'all' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'all' + - memray ; sys_platform != 'win32' and extra == 'all' + - pandas ; extra == 'all' + - pyyaml ; extra == 'all' + - numpy>=1.20 ; extra == 'all' + - fastapi ; extra == 'all' + - opentelemetry-sdk>=1.30.0 ; extra == 'all' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'all' + - starlette ; extra == 'all' + - uvicorn[standard] ; extra == 'all' + - celery ; extra == 'all' + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'all' + - lz4 ; extra == 'all' + - dm-tree ; extra == 'all' + - watchfiles ; extra == 'all-cpp' + - grpcio ; extra == 'all-cpp' + - gymnasium==1.2.2 ; extra == 'all-cpp' + - fsspec ; extra == 'all-cpp' + - tensorboardx>=1.9 ; extra == 'all-cpp' + - opentelemetry-proto ; extra == 'all-cpp' + - pandas>=1.3 ; extra == 'all-cpp' + - taskiq ; extra == 'all-cpp' + - opencensus ; extra == 'all-cpp' + - grpcio>=1.42.0 ; extra == 'all-cpp' + - ormsgpack>=1.7.0 ; extra == 'all-cpp' + - pyopenssl ; extra == 'all-cpp' + - grpcio!=1.56.0 ; sys_platform == 'darwin' and extra == 'all-cpp' + - requests ; extra == 'all-cpp' + - aiohttp>=3.13.3 ; extra == 'all-cpp' + - colorful ; extra == 'all-cpp' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'all-cpp' + - opentelemetry-exporter-prometheus ; extra == 'all-cpp' + - scipy ; extra == 'all-cpp' + - prometheus-client>=0.7.1 ; extra == 'all-cpp' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'all-cpp' + - aiohttp-cors ; extra == 'all-cpp' + - pyarrow>=9.0.0 ; extra == 'all-cpp' + - smart-open ; extra == 'all-cpp' + - ray-cpp==2.55.1 ; extra == 'all-cpp' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'all-cpp' + - memray ; sys_platform != 'win32' and extra == 'all-cpp' + - pandas ; extra == 'all-cpp' + - pyyaml ; extra == 'all-cpp' + - numpy>=1.20 ; extra == 'all-cpp' + - fastapi ; extra == 'all-cpp' + - opentelemetry-sdk>=1.30.0 ; extra == 'all-cpp' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'all-cpp' + - starlette ; extra == 'all-cpp' + - uvicorn[standard] ; extra == 'all-cpp' + - celery ; extra == 'all-cpp' + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'all-cpp' + - lz4 ; extra == 'all-cpp' + - dm-tree ; extra == 'all-cpp' + - watchfiles ; extra == 'llm' + - fsspec ; extra == 'llm' + - opentelemetry-proto ; extra == 'llm' + - pandas>=1.3 ; extra == 'llm' + - jsonref>=1.1.0 ; extra == 'llm' + - ninja ; extra == 'llm' + - nixl>=1.0.0 ; extra == 'llm' + - meson ; extra == 'llm' + - opencensus ; extra == 'llm' + - async-timeout ; python_full_version < '3.11' and extra == 'llm' + - pybind11 ; extra == 'llm' + - grpcio>=1.42.0 ; extra == 'llm' + - vllm[audio]>=0.18.0 ; extra == 'llm' + - requests ; extra == 'llm' + - typer ; extra == 'llm' + - aiohttp>=3.13.3 ; extra == 'llm' + - colorful ; extra == 'llm' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'llm' + - opentelemetry-exporter-prometheus ; extra == 'llm' + - prometheus-client>=0.7.1 ; extra == 'llm' + - jsonschema ; extra == 'llm' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'llm' + - aiohttp-cors ; extra == 'llm' + - hf-transfer ; extra == 'llm' + - pyarrow>=9.0.0 ; extra == 'llm' + - smart-open ; extra == 'llm' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'llm' + - numpy>=1.20 ; extra == 'llm' + - fastapi ; extra == 'llm' + - opentelemetry-sdk>=1.30.0 ; extra == 'llm' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'llm' + - starlette ; extra == 'llm' + - uvicorn[standard] ; extra == 'llm' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + name: httpcore + version: 1.0.9 + sha256: 2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 + requires_dist: + - certifi + - h11>=0.16 + - anyio>=4.0,<5.0 ; extra == 'asyncio' + - h2>=3,<5 ; extra == 'http2' + - socksio==1.* ; extra == 'socks' + - trio>=0.22.0,<1.0 ; extra == 'trio' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl + name: urllib3 + version: 2.7.0 + sha256: 9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + requires_dist: + - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' + - h2>=4,<5 ; extra == 'h2' + - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' + - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/80/41/9c95232b94b219ed8b14029d9cd000e0381cafba869c451dda60af84f4ba/grpcio-1.81.1-cp310-cp310-macosx_11_0_universal2.whl + name: grpcio + version: 1.81.1 + sha256: 69ef28e54fc85397f91b8c19592b8ef3d81952080366914823bd8572a2958120 + requires_dist: + - typing-extensions~=4.12 + - grpcio-tools>=1.81.1 ; extra == 'protobuf' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl + name: psutil + version: 7.2.2 + sha256: 1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 + requires_dist: + - psleak ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-instafail ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - setuptools ; extra == 'dev' + - abi3audit ; extra == 'dev' + - black ; extra == 'dev' + - check-manifest ; extra == 'dev' + - coverage ; extra == 'dev' + - packaging ; extra == 'dev' + - pylint ; extra == 'dev' + - pyperf ; extra == 'dev' + - pypinfo ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - requests ; extra == 'dev' + - rstcheck ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - toml-sort ; extra == 'dev' + - twine ; extra == 'dev' + - validate-pyproject[all] ; extra == 'dev' + - virtualenv ; extra == 'dev' + - vulture ; extra == 'dev' + - wheel ; extra == 'dev' + - colorama ; os_name == 'nt' and extra == 'dev' + - pyreadline3 ; os_name == 'nt' and extra == 'dev' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - psleak ; extra == 'test' + - pytest ; extra == 'test' + - pytest-instafail ; extra == 'test' + - pytest-xdist ; extra == 'test' + - setuptools ; extra == 'test' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl + name: platformdirs + version: 4.10.0 + sha256: fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + name: rich + version: 15.0.0 + sha256: 33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb + requires_dist: + - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' + - markdown-it-py>=2.2.0 + - pygments>=2.13.0,<3.0.0 + requires_python: '>=3.9.0' +- pypi: https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl + name: librt + version: 0.11.0 + sha256: 6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl + name: google-cloud-core + version: 2.6.0 + sha256: 6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e + requires_dist: + - google-api-core>=2.11.0,<3.0.0 + - google-auth>=2.14.1,!=2.24.0,!=2.25.0,<3.0.0 + - grpcio>=1.47.0,<2.0.0 ; python_full_version < '3.14' and extra == 'grpc' + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' + - grpcio-status>=1.47.0,<2.0.0 ; extra == 'grpc' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/85/dd/904873250a6554fbae40cddbf9198e3cc37a2f1319d5e1a5ce82fe269c17/s3transfer-0.17.1-py3-none-any.whl + name: s3transfer + version: 0.17.1 + sha256: 5b9827d1044159bbb01b86ef8902760ea39281927f5de31de75e1d657177bf4c + requires_dist: + - botocore>=1.37.4,<2.0a0 + - botocore[crt]>=1.37.4,<2.0a0 ; extra == 'crt' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl + name: google-api-core + version: 2.31.0 + sha256: ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab + requires_dist: + - googleapis-common-protos>=1.63.2,<2.0.0 + - protobuf>=5.29.6,<8.0.0 + - proto-plus>=1.24.0,<2.0.0 + - proto-plus>=1.25.0,<2.0.0 ; python_full_version >= '3.13' + - google-auth>=2.14.1,<3.0.0 + - requests>=2.33.0,<3.0.0 + - google-auth[aiohttp]>=2.14.1,<3.0.0 ; extra == 'async-rest' + - aiohttp>=3.13.4 ; extra == 'async-rest' + - grpcio>=1.41.0,<2.0.0 ; extra == 'grpc' + - grpcio>=1.49.1,<2.0.0 ; python_full_version >= '3.11' and extra == 'grpc' + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' + - grpcio-status>=1.41.0,<2.0.0 ; extra == 'grpc' + - grpcio-status>=1.49.1,<2.0.0 ; python_full_version >= '3.11' and extra == 'grpc' + - grpcio-status>=1.75.1,<2.0.0 ; python_full_version >= '3.14' and extra == 'grpc' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/86/ad/abc44aaceaf7b17ee1edde2bbb4458da591bc79574cffff50c4bb35f00d1/numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl + name: numpy + version: 2.5.0 + sha256: f27582c55ba4c750b7c58c8faf021d2cd9324a662b466229db8a417b41368af9 + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/86/e6/d80a2fcbd80f024d8e74a579aad538c5a24c6b672e6ce8180a9a8bfc2231/xxhash-3.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: xxhash + version: 3.8.0 + sha256: d5a7fdfde5022f5000c8e6565db954580d19a8aa497ef80875f461e4546ed182 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/87/9a/f088207b4cd6772f9e0d8a91807e79fa2458d4eb9eb1ae406c68415f2bec/psycopg_binary-3.3.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: psycopg-binary + version: 3.3.4 + sha256: fa1cbc10768a796c96d3243656016bf4e337c81c71097270bb7b0ad6210d9765 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + name: cloudpickle + version: 3.1.2 + sha256: 9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + requires_python: '>=3.8' +- pypi: 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 + name: pyyaml + version: 6.0.3 + sha256: c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.46.4 + sha256: 7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl + name: grpc-google-iam-v1 + version: 0.14.4 + sha256: 412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 + requires_dist: + - grpcio>=1.44.0,<2.0.0 + - googleapis-common-protos[grpc]>=1.63.2,<2.0.0 + - protobuf>=4.25.8,<8.0.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + name: exceptiongroup + version: 1.3.1 + sha256: a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 + requires_dist: + - typing-extensions>=4.6.0 ; python_full_version < '3.13' + - pytest>=6 ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl + name: pydantic-core + version: 2.46.4 + sha256: 428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/8e/37/f3439475537ca4c59e9b8cbc2b934672d1965b13b6e5fb32b1796c76e517/xxhash-3.8.0-cp310-cp310-macosx_11_0_arm64.whl + name: xxhash + version: 3.8.0 + sha256: d211cfa927a107df09359d1f31070883a11121ddc88fd6dd27eda3a497a88f3d + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl + name: pyarrow + version: 24.0.0 + sha256: ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/90/5f/85535dfb3cfd6442d66d1df1694062c5d6df02f895329e7e120b2a3d2b8b/aiobotocore-3.7.0-py3-none-any.whl + name: aiobotocore + version: 3.7.0 + sha256: 680bde7c64679a821a9312641b759d9497f790ba8b2e88c6959e6273ee765b8e + requires_dist: + - aiohttp>=3.12.0,<4.0.0 + - aioitertools>=0.5.1,<1.0.0 + - botocore>=1.42.90,<1.43.1 + - python-dateutil>=2.1,<3.0.0 + - jmespath>=0.7.1,<2.0.0 + - multidict>=6.0.0,<7.0.0 + - typing-extensions>=4.14.0,<5.0.0 ; python_full_version < '3.11' + - wrapt>=1.10.10,<3.0.0 + - httpx>=0.25.1,<0.29 ; extra == 'httpx' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl + name: uvloop + version: 0.22.1 + sha256: 3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 + requires_dist: + - aiohttp>=3.10.5 ; extra == 'test' + - flake8~=6.1 ; extra == 'test' + - psutil ; extra == 'test' + - pycodestyle~=2.11.0 ; extra == 'test' + - pyopenssl~=25.3.0 ; extra == 'test' + - mypy>=0.800 ; extra == 'test' + - setuptools>=60 ; extra == 'dev' + - cython~=3.0 ; extra == 'dev' + - sphinx~=4.1.2 ; extra == 'docs' + - sphinxcontrib-asyncio~=0.3.0 ; extra == 'docs' + - sphinx-rtd-theme~=0.5.2 ; extra == 'docs' + requires_python: '>=3.8.1' +- pypi: https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl + name: cffi + version: 2.0.0 + sha256: 0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/94/dc/66c61aca927230c9cf97a3cb005c803971a1076ff9f7d61085d035c20085/wrapt-2.2.2-cp310-cp310-macosx_11_0_arm64.whl + name: wrapt + version: 2.2.2 + sha256: 8374eb6b1a58809211e84ff835a182bb17ab2807a5bfef23204c8cff38178a00 + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl + name: google-crc32c + version: 1.8.0 + sha256: 0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/95/b2/fd5a3e55a5f698fd527f783ce27368e8e9c3d872bd1e1c39dd377b2b9f14/google_cloud_datastore-2.25.0-py3-none-any.whl + name: google-cloud-datastore + version: 2.25.0 + sha256: e47e25933bcfad7118335015b0de2f2b2b5444e81f6f029a62040f568f4f52eb + requires_dist: + - google-api-core[grpc]>=2.17.1,<3.0.0 + - google-auth>=2.14.1,!=2.24.0,!=2.25.0,<3.0.0 + - google-cloud-core>=2.0.0,<3.0.0 + - grpcio>=1.59.0,<2.0.0 + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' + - proto-plus>=1.22.3,<2.0.0 + - proto-plus>=1.25.0,<2.0.0 ; python_full_version >= '3.13' + - protobuf>=4.25.8,<8.0.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/96/c0/271f3e1e3502a8decb8ee5c680dbed2d8dc2cd504f5e20f7ed491d5f37e1/atpublic-7.0.0-py3-none-any.whl + name: atpublic + version: 7.0.0 + sha256: 6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl + name: charset-normalizer + version: 3.4.7 + sha256: c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl + name: markupsafe + version: 3.0.3 + sha256: e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: cffi + version: 2.0.0 + sha256: fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + name: tabulate + version: 0.10.0 + sha256: f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + requires_dist: + - wcwidth ; extra == 'widechars' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9a/0e/c21d79625c155acc31a4a55a9b2c04e25f3b45225bc2691612084da0b7d2/google_cloud_bigtable-2.40.0-py3-none-any.whl + name: google-cloud-bigtable + version: 2.40.0 + sha256: 7571529a4bb4251585d959b0d1a268d4409b67e0cf08cae65e920d12764bc7ba + requires_dist: + - google-api-core[grpc]>=2.24.2,<3.0.0 + - google-auth>=2.14.1,!=2.24.0,!=2.25.0,<3.0.0 + - grpcio>=1.59.0,<2.0.0 + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' + - proto-plus>=1.26.1,<2.0.0 + - protobuf>=6.33.5,<8.0.0 + - google-cloud-core>=2.0.0,<3.0.0 + - grpc-google-iam-v1>=0.14.2,<1.0.0 + - google-crc32c>=1.6.0,<2.0.0.dev0 + - libcst>=0.2.5 ; extra == 'libcst' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl + name: numpy + version: 2.2.6 + sha256: b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: websockets + version: '16.0' + sha256: 7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl + name: setuptools + version: 82.0.1 + sha256: a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - virtualenv>=13.0.0 ; extra == 'test' + - wheel>=0.44.0 ; extra == 'test' + - pip>=19.1 ; extra == 'test' + - packaging>=24.2 ; extra == 'test' + - jaraco-envs>=2.2 ; extra == 'test' + - pytest-xdist>=3 ; extra == 'test' + - jaraco-path>=3.7.2 ; extra == 'test' + - build[virtualenv]>=1.0.3 ; extra == 'test' + - filelock>=3.4.0 ; extra == 'test' + - ini2toml[lite]>=0.14 ; extra == 'test' + - tomli-w>=1.0.0 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-perf ; sys_platform != 'cygwin' and extra == 'test' + - jaraco-develop>=7.21 ; python_full_version >= '3.9' and sys_platform != 'cygwin' and extra == 'test' + - pytest-home>=0.5 ; extra == 'test' + - pytest-subprocess ; extra == 'test' + - pyproject-hooks!=1.1 ; extra == 'test' + - jaraco-test>=5.5 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pygments-github-lexers==0.0.5 ; extra == 'doc' + - sphinx-favicon ; extra == 'doc' + - sphinx-inline-tabs ; extra == 'doc' + - sphinx-reredirects ; extra == 'doc' + - sphinxcontrib-towncrier ; extra == 'doc' + - sphinx-notfound-page>=1,<2 ; extra == 'doc' + - pyproject-hooks!=1.1 ; extra == 'doc' + - towncrier<24.7 ; extra == 'doc' + - packaging>=24.2 ; extra == 'core' + - more-itertools>=8.8 ; extra == 'core' + - jaraco-text>=3.7 ; extra == 'core' + - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' + - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' + - wheel>=0.43.0 ; extra == 'core' + - jaraco-functools>=4 ; extra == 'core' + - more-itertools ; extra == 'core' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - ruff>=0.13.0 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + - mypy==1.18.* ; extra == 'type' + - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' + - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl + name: pytest-cov + version: 7.1.0 + sha256: a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 + requires_dist: + - coverage[toml]>=7.10.6 + - pluggy>=1.2 + - pytest>=7 + - process-tests ; extra == 'testing' + - pytest-xdist ; extra == 'testing' + - virtualenv ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl + name: pyyaml + version: 6.0.3 + sha256: 8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/9d/b3/11d406849715b47c9d69bb22f50874f80caee96bd1cbe7b61abbebbf5a05/ibis_framework-12.0.0-py3-none-any.whl + name: ibis-framework + version: 12.0.0 + sha256: 0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 + requires_dist: + - atpublic>=2.3 + - parsy>=2 + - python-dateutil>=2.8.2 + - sqlglot>=23.4,!=26.32.0 + - toolz>=0.11 + - typing-extensions>=4.3.0 + - tzdata>=2022.7 + - fsspec[s3] ; extra == 'athena' + - numpy>=1.23.2,<3 ; extra == 'athena' + - pandas>=1.5.3,<4 ; extra == 'athena' + - pyarrow-hotfix>=0.4 ; extra == 'athena' + - pyarrow>=10.0.1 ; extra == 'athena' + - pyathena[arrow,pandas]>=3.11.0 ; extra == 'athena' + - rich>=12.4.4 ; extra == 'athena' + - db-dtypes>=0.3 ; extra == 'bigquery' + - google-cloud-bigquery-storage>=2 ; extra == 'bigquery' + - google-cloud-bigquery>=3 ; extra == 'bigquery' + - numpy>=1.23.2,<3 ; extra == 'bigquery' + - pandas-gbq>=0.26.1 ; extra == 'bigquery' + - pandas>=1.5.3,<4 ; extra == 'bigquery' + - pyarrow-hotfix>=0.4 ; extra == 'bigquery' + - pyarrow>=10.0.1 ; extra == 'bigquery' + - pydata-google-auth>=1.4.0 ; extra == 'bigquery' + - rich>=12.4.4 ; extra == 'bigquery' + - clickhouse-connect[arrow,numpy,pandas]>=0.5.23 ; extra == 'clickhouse' + - numpy>=1.23.2,<3 ; extra == 'clickhouse' + - pandas>=1.5.3,<4 ; extra == 'clickhouse' + - pyarrow-hotfix>=0.4 ; extra == 'clickhouse' + - pyarrow>=10.0.1 ; extra == 'clickhouse' + - rich>=12.4.4 ; extra == 'clickhouse' + - databricks-sql-connector>=4 ; extra == 'databricks' + - numpy>=1.23.2,<3 ; extra == 'databricks' + - pandas>=1.5.3,<4 ; extra == 'databricks' + - pyarrow-hotfix>=0.4 ; extra == 'databricks' + - pyarrow>=10.0.1 ; extra == 'databricks' + - rich>=12.4.4 ; extra == 'databricks' + - datafusion>=0.6 ; extra == 'datafusion' + - numpy>=1.23.2,<3 ; extra == 'datafusion' + - packaging>=21.3 ; extra == 'datafusion' + - pandas>=1.5.3,<4 ; extra == 'datafusion' + - pyarrow-hotfix>=0.4 ; extra == 'datafusion' + - pyarrow>=10.0.1 ; extra == 'datafusion' + - rich>=12.4.4 ; extra == 'datafusion' + - black>=22.1.0 ; extra == 'decompiler' + - deltalake>=0.9.0 ; extra == 'deltalake' + - numpy>=1.23.2,<3 ; extra == 'druid' + - pandas>=1.5.3,<4 ; extra == 'druid' + - pyarrow-hotfix>=0.4 ; extra == 'druid' + - pyarrow>=10.0.1 ; extra == 'druid' + - pydruid>=0.6.7 ; extra == 'druid' + - rich>=12.4.4 ; extra == 'druid' + - duckdb>=0.10.3,!=1.3.0 ; extra == 'duckdb' + - numpy>=1.23.2,<3 ; extra == 'duckdb' + - packaging>=21.3 ; extra == 'duckdb' + - pandas>=1.5.3,<4 ; extra == 'duckdb' + - pyarrow-hotfix>=0.4 ; extra == 'duckdb' + - pyarrow>=10.0.1 ; extra == 'duckdb' + - rich>=12.4.4 ; extra == 'duckdb' + - pins[gcs]>=0.8.3 ; extra == 'examples' + - numpy>=1.23.2,<3 ; extra == 'exasol' + - pandas>=1.5.3,<4 ; extra == 'exasol' + - pyarrow-hotfix>=0.4 ; extra == 'exasol' + - pyarrow>=10.0.1 ; extra == 'exasol' + - pyexasol>=0.25.2 ; extra == 'exasol' + - rich>=12.4.4 ; extra == 'exasol' + - numpy>=1.23.2,<3 ; extra == 'flink' + - pandas>=1.5.3,<4 ; extra == 'flink' + - pyarrow-hotfix>=0.4 ; extra == 'flink' + - pyarrow>=10.0.1 ; extra == 'flink' + - rich>=12.4.4 ; extra == 'flink' + - geoarrow-types>=0.2 ; extra == 'geospatial' + - geopandas>=0.6 ; extra == 'geospatial' + - pyproj>=3.3.0 ; extra == 'geospatial' + - shapely>=2 ; extra == 'geospatial' + - impyla>=0.17 ; extra == 'impala' + - numpy>=1.23.2,<3 ; extra == 'impala' + - pandas>=1.5.3,<4 ; extra == 'impala' + - pyarrow-hotfix>=0.4 ; extra == 'impala' + - pyarrow>=10.0.1 ; extra == 'impala' + - rich>=12.4.4 ; extra == 'impala' + - numpy>=1.23.2,<3 ; extra == 'materialize' + - pandas>=1.5.3,<4 ; extra == 'materialize' + - psycopg>=3.2.0 ; extra == 'materialize' + - pyarrow-hotfix>=0.4 ; extra == 'materialize' + - pyarrow>=10.0.1 ; extra == 'materialize' + - rich>=12.4.4 ; extra == 'materialize' + - numpy>=1.23.2,<3 ; extra == 'mssql' + - pandas>=1.5.3,<4 ; extra == 'mssql' + - pyarrow-hotfix>=0.4 ; extra == 'mssql' + - pyarrow>=10.0.1 ; extra == 'mssql' + - pyodbc>=4.0.39 ; extra == 'mssql' + - rich>=12.4.4 ; extra == 'mssql' + - mysqlclient>=2.2.4 ; extra == 'mysql' + - numpy>=1.23.2,<3 ; extra == 'mysql' + - pandas>=1.5.3,<4 ; extra == 'mysql' + - pyarrow-hotfix>=0.4 ; extra == 'mysql' + - pyarrow>=10.0.1 ; extra == 'mysql' + - rich>=12.4.4 ; extra == 'mysql' + - numpy>=1.23.2,<3 ; extra == 'oracle' + - oracledb>=1.3.1 ; extra == 'oracle' + - pandas>=1.5.3,<4 ; extra == 'oracle' + - pyarrow-hotfix>=0.4 ; extra == 'oracle' + - pyarrow>=10.0.1 ; extra == 'oracle' + - rich>=12.4.4 ; extra == 'oracle' + - numpy>=1.23.2,<3 ; extra == 'polars' + - pandas>=1.5.3,<4 ; extra == 'polars' + - polars>=1 ; extra == 'polars' + - pyarrow-hotfix>=0.4 ; extra == 'polars' + - pyarrow>=10.0.1 ; extra == 'polars' + - rich>=12.4.4 ; extra == 'polars' + - numpy>=1.23.2,<3 ; extra == 'postgres' + - pandas>=1.5.3,<4 ; extra == 'postgres' + - psycopg>=3.2.0 ; extra == 'postgres' + - pyarrow-hotfix>=0.4 ; extra == 'postgres' + - pyarrow>=10.0.1 ; extra == 'postgres' + - rich>=12.4.4 ; extra == 'postgres' + - numpy>=1.23.2,<3 ; extra == 'pyspark' + - packaging>=21.3 ; extra == 'pyspark' + - pandas>=1.5.3,<4 ; extra == 'pyspark' + - pyarrow-hotfix>=0.4 ; extra == 'pyspark' + - pyarrow>=10.0.1 ; extra == 'pyspark' + - pyspark>=3.5,<4.1 ; extra == 'pyspark' + - rich>=12.4.4 ; extra == 'pyspark' + - numpy>=1.23.2,<3 ; extra == 'risingwave' + - pandas>=1.5.3,<4 ; extra == 'risingwave' + - psycopg2>=2.8.4 ; extra == 'risingwave' + - pyarrow-hotfix>=0.4 ; extra == 'risingwave' + - pyarrow>=10.0.1 ; extra == 'risingwave' + - rich>=12.4.4 ; extra == 'risingwave' + - numpy>=1.23.2,<3 ; extra == 'singlestoredb' + - pandas>=1.5.3,<4 ; extra == 'singlestoredb' + - parsimonious>=0.11.0 ; extra == 'singlestoredb' + - pyarrow-hotfix>=0.4 ; extra == 'singlestoredb' + - pyarrow>=10.0.1 ; extra == 'singlestoredb' + - rich>=12.4.4 ; extra == 'singlestoredb' + - singlestoredb>=1.0 ; extra == 'singlestoredb' + - numpy>=1.23.2,<3 ; extra == 'snowflake' + - pandas>=1.5.3,<4 ; extra == 'snowflake' + - pyarrow-hotfix>=0.4 ; extra == 'snowflake' + - pyarrow>=10.0.1 ; extra == 'snowflake' + - rich>=12.4.4 ; extra == 'snowflake' + - snowflake-connector-python>=3.0.2,!=3.3.0b1 ; extra == 'snowflake' + - numpy>=1.23.2,<3 ; extra == 'sqlite' + - packaging>=21.3 ; extra == 'sqlite' + - pandas>=1.5.3,<4 ; extra == 'sqlite' + - pyarrow-hotfix>=0.4 ; extra == 'sqlite' + - pyarrow>=10.0.1 ; extra == 'sqlite' + - regex>=2021.7.6 ; extra == 'sqlite' + - rich>=12.4.4 ; extra == 'sqlite' + - numpy>=1.23.2,<3 ; extra == 'trino' + - pandas>=1.5.3,<4 ; extra == 'trino' + - pyarrow-hotfix>=0.4 ; extra == 'trino' + - pyarrow>=10.0.1 ; extra == 'trino' + - rich>=12.4.4 ; extra == 'trino' + - trino>=0.321 ; extra == 'trino' + - graphviz>=0.16 ; extra == 'visualization' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/9d/f7/b4d41eaae2869d31356bc4bbf546f44fae83ff298af0a043ca0625b06773/httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl + name: httptools + version: 0.8.0 + sha256: da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl + name: cffi + version: 2.0.0 + sha256: f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a0/5e/2a902317d7fc4aa93236e80c932662dadfc459b323d758329e01775125e1/numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: numpy + version: 2.5.0 + sha256: 39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03 + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl + name: requests + version: 2.34.2 + sha256: 2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 + requires_dist: + - charset-normalizer>=2,<4 + - idna>=2.5,<4 + - urllib3>=1.26,<3 + - certifi>=2023.5.7 + - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' + - chardet>=3.0.2,<8 ; extra == 'use-chardet-on-py3' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl + name: frozenlist + version: 1.8.0 + sha256: ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl + name: pyjwt + version: 2.13.0 + sha256: 66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + requires_dist: + - typing-extensions>=4.0 ; python_full_version < '3.11' + - cryptography>=3.4.0 ; extra == 'crypto' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl + name: mypy + version: 2.1.0 + sha256: 11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc + requires_dist: + - typing-extensions>=4.6.0 ; python_full_version < '3.15' + - typing-extensions>=4.14.0 ; python_full_version >= '3.15' + - mypy-extensions>=1.0.0 + - pathspec>=1.0.0 + - tomli>=1.1.0 ; python_full_version < '3.11' + - librt>=0.11.0 ; platform_python_implementation != 'PyPy' + - ast-serialize>=0.3.0,<1.0.0 + - psutil>=4.0 ; extra == 'dmypy' + - setuptools>=50 ; extra == 'mypyc' + - lxml ; extra == 'reports' + - pip ; extra == 'install-types' + - orjson ; extra == 'faster-cache' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl + name: pyarrow + version: 24.0.0 + sha256: 7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a8/8f/7b3ec212f1ea0683f55f978e3246bc313c38818664edfc97a9f349a4901e/yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl + name: yarl + version: 1.24.2 + sha256: 15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75 + requires_dist: + - idna>=2.0 + - multidict>=4.0 + - propcache>=0.2.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl + name: watchfiles + version: 1.2.0 + sha256: 9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 + requires_dist: + - anyio>=3.0.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl + name: execnet + version: 2.1.2 + sha256: 67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec + requires_dist: + - hatch ; extra == 'testing' + - pre-commit ; extra == 'testing' + - pytest ; extra == 'testing' + - tox ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl + name: pydantic-core + version: 2.46.4 + sha256: 23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl + name: pyarrow + version: 24.0.0 + sha256: 1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838 + requires_python: '>=3.10' +- pypi: 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 + name: markupsafe + version: 3.0.3 + sha256: f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl + name: anyio + version: 4.14.1 + sha256: 4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 + requires_dist: + - exceptiongroup>=1.0.2 ; python_full_version < '3.11' + - idna>=2.8 + - typing-extensions>=4.5 ; python_full_version < '3.13' + - trio>=0.32.0 ; extra == 'trio' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl + name: mypy + version: 2.1.0 + sha256: 7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2 + requires_dist: + - typing-extensions>=4.6.0 ; python_full_version < '3.15' + - typing-extensions>=4.14.0 ; python_full_version >= '3.15' + - mypy-extensions>=1.0.0 + - pathspec>=1.0.0 + - tomli>=1.1.0 ; python_full_version < '3.11' + - librt>=0.11.0 ; platform_python_implementation != 'PyPy' + - ast-serialize>=0.3.0,<1.0.0 + - psutil>=4.0 ; extra == 'dmypy' + - setuptools>=50 ; extra == 'mypyc' + - lxml ; extra == 'reports' + - pip ; extra == 'install-types' + - orjson ; extra == 'faster-cache' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl + name: google-resumable-media + version: 2.10.0 + sha256: 88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c + requires_dist: + - google-crc32c>=1.0.0,<2.0.0 + - requests>=2.18.0,<3.0.0 ; extra == 'requests' + - aiohttp>=3.6.2,<4.0.0 ; extra == 'aiohttp' + - google-auth>=1.22.0,<2.0.0 ; extra == 'aiohttp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl + name: sqlalchemy + version: 2.0.51 + sha256: 6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/b1/5a/db02b5e6633fbe49eaf4e3194bc64ec031e6436a0cfcc610cbda4f1b6a24/types_pymysql-1.1.0.20260518-py3-none-any.whl + name: types-pymysql + version: 1.1.0.20260518 + sha256: cf697ce4e44124fc859e8e8a7f047c1dc864745c3c628b85a51b3ee01502ef98 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: ast-serialize + version: 0.5.0 + sha256: b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + name: mdurl + version: 0.1.2 + sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + name: markdown-it-py + version: 4.2.0 + sha256: 9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + requires_dist: + - mdurl~=0.1 + - psutil ; extra == 'benchmarking' + - pytest ; extra == 'benchmarking' + - pytest-benchmark ; extra == 'benchmarking' + - commonmark~=0.9 ; extra == 'compare' + - markdown~=3.4 ; extra == 'compare' + - mistletoe~=1.0 ; extra == 'compare' + - mistune~=3.0 ; extra == 'compare' + - panflute~=2.3 ; extra == 'compare' + - markdown-it-pyrs ; extra == 'compare' + - linkify-it-py>=1,<3 ; extra == 'linkify' + - mdit-py-plugins>=0.5.0 ; extra == 'plugins' + - gprof2dot ; extra == 'profiling' + - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - myst-parser ; extra == 'rtd' + - pyyaml ; extra == 'rtd' + - sphinx ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-book-theme~=1.0 ; extra == 'rtd' + - jupyter-sphinx ; extra == 'rtd' + - ipykernel ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' + - requests ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/a0/3e6a0b1c1ea6bec76f71473727ef27abf3cd40e9709b3ebcbfbcfaae6f79/boto3-1.43.0-py3-none-any.whl + name: boto3 + version: 1.43.0 + sha256: 8ebe03754a4b73a5cb6ec2f14cca03ac33bd4760d0adea53da4724845130258b + requires_dist: + - botocore>=1.43.0,<1.44.0 + - jmespath>=0.7.1,<2.0.0 + - s3transfer>=0.17.0,<0.18.0 + - botocore[crt]>=1.21.0,<2.0a0 ; extra == 'crt' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: uvloop + version: 0.22.1 + sha256: 481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd + requires_dist: + - aiohttp>=3.10.5 ; extra == 'test' + - flake8~=6.1 ; extra == 'test' + - psutil ; extra == 'test' + - pycodestyle~=2.11.0 ; extra == 'test' + - pyopenssl~=25.3.0 ; extra == 'test' + - mypy>=0.800 ; extra == 'test' + - setuptools>=60 ; extra == 'dev' + - cython~=3.0 ; extra == 'dev' + - sphinx~=4.1.2 ; extra == 'docs' + - sphinxcontrib-asyncio~=0.3.0 ; extra == 'docs' + - sphinx-rtd-theme~=0.5.2 ; extra == 'docs' + requires_python: '>=3.8.1' +- pypi: https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: numpy + version: 2.2.6 + sha256: fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b4/64/640f5525bac171282c6f76f3ecc9c4cfef60149ac0d00231afb22018ebe5/ray-2.55.1-cp310-cp310-manylinux2014_x86_64.whl + name: ray + version: 2.55.1 + sha256: bb49fbbe53a1d931e1f92d17f9271338f0b738885f8f70b7f531aa33f019d8af + requires_dist: + - click>=7.0 + - filelock + - jsonschema + - msgpack>=1.0.0,<2.0.0 + - packaging>=24.2 + - protobuf>=3.20.3 + - pyyaml + - requests + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'cgraph' + - grpcio!=1.56.0 ; sys_platform == 'darwin' and extra == 'client' + - grpcio ; extra == 'client' + - numpy>=1.20 ; extra == 'data' + - pandas>=1.3 ; extra == 'data' + - pyarrow>=9.0.0 ; extra == 'data' + - fsspec ; extra == 'data' + - aiohttp>=3.13.3 ; extra == 'default' + - aiohttp-cors ; extra == 'default' + - colorful ; extra == 'default' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'default' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'default' + - requests ; extra == 'default' + - grpcio>=1.42.0 ; extra == 'default' + - opencensus ; extra == 'default' + - opentelemetry-sdk>=1.30.0 ; extra == 'default' + - opentelemetry-exporter-prometheus ; extra == 'default' + - opentelemetry-proto ; extra == 'default' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'default' + - prometheus-client>=0.7.1 ; extra == 'default' + - smart-open ; extra == 'default' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'default' + - memray ; sys_platform != 'win32' and extra == 'observability' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'serve' + - colorful ; extra == 'serve' + - starlette ; extra == 'serve' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'serve' + - grpcio>=1.42.0 ; extra == 'serve' + - aiohttp-cors ; extra == 'serve' + - opentelemetry-exporter-prometheus ; extra == 'serve' + - prometheus-client>=0.7.1 ; extra == 'serve' + - fastapi ; extra == 'serve' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'serve' + - opentelemetry-sdk>=1.30.0 ; extra == 'serve' + - requests ; extra == 'serve' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'serve' + - watchfiles ; extra == 'serve' + - uvicorn[standard] ; extra == 'serve' + - smart-open ; extra == 'serve' + - opentelemetry-proto ; extra == 'serve' + - aiohttp>=3.13.3 ; extra == 'serve' + - opencensus ; extra == 'serve' + - pandas ; extra == 'tune' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'tune' + - tensorboardx>=1.9 ; extra == 'tune' + - requests ; extra == 'tune' + - pyarrow>=9.0.0 ; extra == 'tune' + - fsspec ; extra == 'tune' + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'adag' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'serve-grpc' + - colorful ; extra == 'serve-grpc' + - starlette ; extra == 'serve-grpc' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'serve-grpc' + - grpcio>=1.42.0 ; extra == 'serve-grpc' + - aiohttp-cors ; extra == 'serve-grpc' + - opentelemetry-exporter-prometheus ; extra == 'serve-grpc' + - prometheus-client>=0.7.1 ; extra == 'serve-grpc' + - pyopenssl ; extra == 'serve-grpc' + - fastapi ; extra == 'serve-grpc' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'serve-grpc' + - opentelemetry-sdk>=1.30.0 ; extra == 'serve-grpc' + - requests ; extra == 'serve-grpc' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'serve-grpc' + - watchfiles ; extra == 'serve-grpc' + - uvicorn[standard] ; extra == 'serve-grpc' + - smart-open ; extra == 'serve-grpc' + - opentelemetry-proto ; extra == 'serve-grpc' + - aiohttp>=3.13.3 ; extra == 'serve-grpc' + - opencensus ; extra == 'serve-grpc' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'serve-async-inference' + - colorful ; extra == 'serve-async-inference' + - starlette ; extra == 'serve-async-inference' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'serve-async-inference' + - grpcio>=1.42.0 ; extra == 'serve-async-inference' + - aiohttp-cors ; extra == 'serve-async-inference' + - opentelemetry-exporter-prometheus ; extra == 'serve-async-inference' + - celery ; extra == 'serve-async-inference' + - prometheus-client>=0.7.1 ; extra == 'serve-async-inference' + - fastapi ; extra == 'serve-async-inference' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'serve-async-inference' + - taskiq ; extra == 'serve-async-inference' + - opentelemetry-sdk>=1.30.0 ; extra == 'serve-async-inference' + - requests ; extra == 'serve-async-inference' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'serve-async-inference' + - watchfiles ; extra == 'serve-async-inference' + - uvicorn[standard] ; extra == 'serve-async-inference' + - smart-open ; extra == 'serve-async-inference' + - opentelemetry-proto ; extra == 'serve-async-inference' + - aiohttp>=3.13.3 ; extra == 'serve-async-inference' + - opencensus ; extra == 'serve-async-inference' + - ray-cpp==2.55.1 ; extra == 'cpp' + - pandas ; extra == 'rllib' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'rllib' + - tensorboardx>=1.9 ; extra == 'rllib' + - requests ; extra == 'rllib' + - pyarrow>=9.0.0 ; extra == 'rllib' + - fsspec ; extra == 'rllib' + - dm-tree ; extra == 'rllib' + - gymnasium==1.2.2 ; extra == 'rllib' + - lz4 ; extra == 'rllib' + - ormsgpack>=1.7.0 ; extra == 'rllib' + - pyyaml ; extra == 'rllib' + - scipy ; extra == 'rllib' + - pandas ; extra == 'train' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'train' + - tensorboardx>=1.9 ; extra == 'train' + - requests ; extra == 'train' + - pyarrow>=9.0.0 ; extra == 'train' + - fsspec ; extra == 'train' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'train' + - pyarrow>=9.0.0 ; extra == 'air' + - fsspec ; extra == 'air' + - tensorboardx>=1.9 ; extra == 'air' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'air' + - colorful ; extra == 'air' + - starlette ; extra == 'air' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'air' + - grpcio>=1.42.0 ; extra == 'air' + - numpy>=1.20 ; extra == 'air' + - aiohttp-cors ; extra == 'air' + - opentelemetry-exporter-prometheus ; extra == 'air' + - pandas>=1.3 ; extra == 'air' + - prometheus-client>=0.7.1 ; extra == 'air' + - pandas ; extra == 'air' + - fastapi ; extra == 'air' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'air' + - opentelemetry-sdk>=1.30.0 ; extra == 'air' + - requests ; extra == 'air' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'air' + - watchfiles ; extra == 'air' + - uvicorn[standard] ; extra == 'air' + - smart-open ; extra == 'air' + - opentelemetry-proto ; extra == 'air' + - aiohttp>=3.13.3 ; extra == 'air' + - opencensus ; extra == 'air' + - pyarrow>=9.0.0 ; extra == 'all' + - fsspec ; extra == 'all' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'all' + - tensorboardx>=1.9 ; extra == 'all' + - ormsgpack>=1.7.0 ; extra == 'all' + - colorful ; extra == 'all' + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'all' + - pyyaml ; extra == 'all' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'all' + - starlette ; extra == 'all' + - grpcio>=1.42.0 ; extra == 'all' + - numpy>=1.20 ; extra == 'all' + - aiohttp-cors ; extra == 'all' + - memray ; sys_platform != 'win32' and extra == 'all' + - opentelemetry-exporter-prometheus ; extra == 'all' + - pandas>=1.3 ; extra == 'all' + - scipy ; extra == 'all' + - celery ; extra == 'all' + - lz4 ; extra == 'all' + - prometheus-client>=0.7.1 ; extra == 'all' + - pyopenssl ; extra == 'all' + - grpcio ; extra == 'all' + - pandas ; extra == 'all' + - fastapi ; extra == 'all' + - gymnasium==1.2.2 ; extra == 'all' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'all' + - taskiq ; extra == 'all' + - opentelemetry-sdk>=1.30.0 ; extra == 'all' + - requests ; extra == 'all' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'all' + - dm-tree ; extra == 'all' + - watchfiles ; extra == 'all' + - smart-open ; extra == 'all' + - uvicorn[standard] ; extra == 'all' + - opentelemetry-proto ; extra == 'all' + - aiohttp>=3.13.3 ; extra == 'all' + - grpcio!=1.56.0 ; sys_platform == 'darwin' and extra == 'all' + - opencensus ; extra == 'all' + - pyarrow>=9.0.0 ; extra == 'all-cpp' + - ray-cpp==2.55.1 ; extra == 'all-cpp' + - fsspec ; extra == 'all-cpp' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'all-cpp' + - tensorboardx>=1.9 ; extra == 'all-cpp' + - ormsgpack>=1.7.0 ; extra == 'all-cpp' + - colorful ; extra == 'all-cpp' + - cupy-cuda12x ; sys_platform != 'darwin' and extra == 'all-cpp' + - pyyaml ; extra == 'all-cpp' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'all-cpp' + - starlette ; extra == 'all-cpp' + - grpcio>=1.42.0 ; extra == 'all-cpp' + - numpy>=1.20 ; extra == 'all-cpp' + - aiohttp-cors ; extra == 'all-cpp' + - memray ; sys_platform != 'win32' and extra == 'all-cpp' + - opentelemetry-exporter-prometheus ; extra == 'all-cpp' + - pandas>=1.3 ; extra == 'all-cpp' + - scipy ; extra == 'all-cpp' + - celery ; extra == 'all-cpp' + - lz4 ; extra == 'all-cpp' + - prometheus-client>=0.7.1 ; extra == 'all-cpp' + - pyopenssl ; extra == 'all-cpp' + - grpcio ; extra == 'all-cpp' + - pandas ; extra == 'all-cpp' + - fastapi ; extra == 'all-cpp' + - gymnasium==1.2.2 ; extra == 'all-cpp' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'all-cpp' + - taskiq ; extra == 'all-cpp' + - opentelemetry-sdk>=1.30.0 ; extra == 'all-cpp' + - requests ; extra == 'all-cpp' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'all-cpp' + - dm-tree ; extra == 'all-cpp' + - watchfiles ; extra == 'all-cpp' + - smart-open ; extra == 'all-cpp' + - uvicorn[standard] ; extra == 'all-cpp' + - opentelemetry-proto ; extra == 'all-cpp' + - aiohttp>=3.13.3 ; extra == 'all-cpp' + - grpcio!=1.56.0 ; sys_platform == 'darwin' and extra == 'all-cpp' + - opencensus ; extra == 'all-cpp' + - pyarrow>=9.0.0 ; extra == 'llm' + - fsspec ; extra == 'llm' + - py-spy>=0.2.0 ; python_full_version < '3.12' and extra == 'llm' + - jsonref>=1.1.0 ; extra == 'llm' + - colorful ; extra == 'llm' + - starlette ; extra == 'llm' + - py-spy>=0.4.0 ; python_full_version >= '3.12' and extra == 'llm' + - grpcio>=1.42.0 ; extra == 'llm' + - vllm[audio]>=0.18.0 ; extra == 'llm' + - numpy>=1.20 ; extra == 'llm' + - aiohttp-cors ; extra == 'llm' + - opentelemetry-exporter-prometheus ; extra == 'llm' + - pandas>=1.3 ; extra == 'llm' + - typer ; extra == 'llm' + - async-timeout ; python_full_version < '3.11' and extra == 'llm' + - prometheus-client>=0.7.1 ; extra == 'llm' + - hf-transfer ; extra == 'llm' + - fastapi ; extra == 'llm' + - nixl>=1.0.0 ; extra == 'llm' + - virtualenv>=20.0.24,!=20.21.1 ; extra == 'llm' + - jsonschema ; extra == 'llm' + - meson ; extra == 'llm' + - opentelemetry-sdk>=1.30.0 ; extra == 'llm' + - requests ; extra == 'llm' + - pydantic!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,<3 ; extra == 'llm' + - watchfiles ; extra == 'llm' + - uvicorn[standard] ; extra == 'llm' + - smart-open ; extra == 'llm' + - ninja ; extra == 'llm' + - opentelemetry-proto ; extra == 'llm' + - pybind11 ; extra == 'llm' + - aiohttp>=3.13.3 ; extra == 'llm' + - opencensus ; extra == 'llm' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: uvloop + version: 0.22.1 + sha256: c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 + requires_dist: + - aiohttp>=3.10.5 ; extra == 'test' + - flake8~=6.1 ; extra == 'test' + - psutil ; extra == 'test' + - pycodestyle~=2.11.0 ; extra == 'test' + - pyopenssl~=25.3.0 ; extra == 'test' + - mypy>=0.800 ; extra == 'test' + - setuptools>=60 ; extra == 'dev' + - cython~=3.0 ; extra == 'dev' + - sphinx~=4.1.2 ; extra == 'docs' + - sphinxcontrib-asyncio~=0.3.0 ; extra == 'docs' + - sphinx-rtd-theme~=0.5.2 ; extra == 'docs' + requires_python: '>=3.8.1' +- pypi: https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl + name: markupsafe + version: 3.0.3 + sha256: c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl + name: psutil + version: 7.2.2 + sha256: 076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 + requires_dist: + - psleak ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-instafail ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - setuptools ; extra == 'dev' + - abi3audit ; extra == 'dev' + - black ; extra == 'dev' + - check-manifest ; extra == 'dev' + - coverage ; extra == 'dev' + - packaging ; extra == 'dev' + - pylint ; extra == 'dev' + - pyperf ; extra == 'dev' + - pypinfo ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - requests ; extra == 'dev' + - rstcheck ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - toml-sort ; extra == 'dev' + - twine ; extra == 'dev' + - validate-pyproject[all] ; extra == 'dev' + - virtualenv ; extra == 'dev' + - vulture ; extra == 'dev' + - wheel ; extra == 'dev' + - colorama ; os_name == 'nt' and extra == 'dev' + - pyreadline3 ; os_name == 'nt' and extra == 'dev' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - psleak ; extra == 'test' + - pytest ; extra == 'test' + - pytest-instafail ; extra == 'test' + - pytest-xdist ; extra == 'test' + - setuptools ; extra == 'test' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl + name: argon2-cffi-bindings + version: 25.1.0 + sha256: 7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 + requires_dist: + - cffi>=1.0.1 ; python_full_version < '3.14' + - cffi>=2.0.0b1 ; python_full_version >= '3.14' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: mmh3 + version: 5.2.1 + sha256: 8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc + requires_dist: + - pytest==9.0.2 ; extra == 'test' + - pytest-sugar==1.1.1 ; extra == 'test' + - actionlint-py==1.7.11.24 ; extra == 'lint' + - clang-format==22.1.0 ; extra == 'lint' + - codespell==2.4.1 ; extra == 'lint' + - pylint==4.0.5 ; extra == 'lint' + - ruff==0.15.4 ; extra == 'lint' + - mypy==1.19.1 ; extra == 'type' + - myst-parser==5.0.0 ; extra == 'docs' + - shibuya==2026.1.9 ; extra == 'docs' + - sphinx==8.2.3 ; extra == 'docs' + - sphinx-copybutton==0.5.2 ; extra == 'docs' + - pymmh3==0.0.5 ; extra == 'benchmark' + - pyperf==2.10.0 ; extra == 'benchmark' + - xxhash==3.6.0 ; extra == 'benchmark' + - matplotlib==3.10.8 ; extra == 'plot' + - pandas==3.0.1 ; extra == 'plot' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b6/9e/8d9f6b9746f8ede78b0a4e4b8908e4d80bd609fca0b3e3195a07dda29534/minio-7.2.11-py3-none-any.whl + name: minio + version: 7.2.11 + sha256: 153582ed52ff3b5005ba558e1f25bfe1e9e834f7f0745e594777f28e3e81e1a0 + requires_dist: + - certifi + - urllib3 + - argon2-cffi + - pycryptodome + - typing-extensions + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b7/bf/70d8a60488f9955cbbcd538beae44d56bb2f1d19e673b72788f2d343ff55/psycopg_binary-3.3.4-cp310-cp310-macosx_10_9_x86_64.whl + name: psycopg-binary + version: 3.3.4 + sha256: b7bfff1ca23732b488cbca3076fc11bc98d520ee122514fdb17a8e20d3338f5a + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + name: six + version: 1.17.0 + sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl + name: cryptography + version: 49.0.0 + sha256: 6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 + requires_dist: + - cffi>=2.0.0 ; platform_python_implementation != 'PyPy' + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - bcrypt>=3.1.5 ; extra == 'ssh' + requires_python: '!=3.9.0,>=3.9,!=3.9.1' +- pypi: https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.46.4 + sha256: 395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: httptools + version: 0.8.0 + sha256: c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl + name: uvloop + version: 0.22.1 + sha256: 7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 + requires_dist: + - aiohttp>=3.10.5 ; extra == 'test' + - flake8~=6.1 ; extra == 'test' + - psutil ; extra == 'test' + - pycodestyle~=2.11.0 ; extra == 'test' + - pyopenssl~=25.3.0 ; extra == 'test' + - mypy>=0.800 ; extra == 'test' + - setuptools>=60 ; extra == 'dev' + - cython~=3.0 ; extra == 'dev' + - sphinx~=4.1.2 ; extra == 'docs' + - sphinxcontrib-asyncio~=0.3.0 ; extra == 'docs' + - sphinx-rtd-theme~=0.5.2 ; extra == 'docs' + requires_python: '>=3.8.1' +- pypi: https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl + name: websockets + version: '16.0' + sha256: fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl + name: pyyaml + version: 6.0.3 + sha256: 34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/bd/db/ea0203e495be491c85af87b66e37acfd3bf756fd985f87e46fc5e3bf022c/py4j-0.10.9.9-py2.py3-none-any.whl + name: py4j + version: 0.10.9.9 + sha256: c7c26e4158defb37b0bb124933163641a2ff6e3a3913f7811b0ddbe07ed61533 +- pypi: https://files.pythonhosted.org/packages/be/7a/797ed371bde520223e49dbbe0f8762fa4cc724d0e21fc6af944bb8f82150/db_dtypes-1.7.0-py3-none-any.whl + name: db-dtypes + version: 1.7.0 + sha256: c80a1d9bc7dda3cc63638a3f2cf4ec27af50b809ec2d92b8a94cb9d8438cdc76 + requires_dist: + - numpy>=1.24.0,<=2.2.6 ; python_full_version == '3.10.*' + - numpy>=1.24.0 ; python_full_version != '3.10.*' + - packaging>=24.2.0 + - pandas>=1.5.3,<4.0.0 + - pyarrow>=13.0.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl + name: oauthlib + version: 3.3.1 + sha256: 88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 + requires_dist: + - cryptography>=3.0.0 ; extra == 'rsa' + - cryptography>=3.0.0 ; extra == 'signedtoken' + - pyjwt>=2.0.0,<3 ; extra == 'signedtoken' + - blinker>=1.4.0 ; extra == 'signals' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl + name: botocore + version: 1.43.0 + sha256: cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6 + requires_dist: + - jmespath>=0.7.1,<2.0.0 + - python-dateutil>=2.1,<3.0.0 + - urllib3>=1.25.4,!=2.2.0,<3 + - awscrt==0.32.2 ; extra == 'crt' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl + name: mmh3 + version: 5.2.1 + sha256: e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e + requires_dist: + - pytest==9.0.2 ; extra == 'test' + - pytest-sugar==1.1.1 ; extra == 'test' + - actionlint-py==1.7.11.24 ; extra == 'lint' + - clang-format==22.1.0 ; extra == 'lint' + - codespell==2.4.1 ; extra == 'lint' + - pylint==4.0.5 ; extra == 'lint' + - ruff==0.15.4 ; extra == 'lint' + - mypy==1.19.1 ; extra == 'type' + - myst-parser==5.0.0 ; extra == 'docs' + - shibuya==2026.1.9 ; extra == 'docs' + - sphinx==8.2.3 ; extra == 'docs' + - sphinx-copybutton==0.5.2 ; extra == 'docs' + - pymmh3==0.0.5 ; extra == 'benchmark' + - pyperf==2.10.0 ; extra == 'benchmark' + - xxhash==3.6.0 ; extra == 'benchmark' + - matplotlib==3.10.8 ; extra == 'plot' + - pandas==3.0.1 ; extra == 'plot' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl + name: greenlet + version: 3.5.3 + sha256: fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117 + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl + name: pymysql + version: 1.2.0 + sha256: 62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 + requires_dist: + - cryptography>=46.0.7 ; extra == 'rsa' + - pynacl>=1.6.2 ; extra == 'ed25519' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c6/b1/e20d5f0d19c4c0f3df213fa7dcfa0942c4fb127d38e11f398ae8ddf6cccc/mmh3-5.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: mmh3 + version: 5.2.1 + sha256: fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f + requires_dist: + - pytest==9.0.2 ; extra == 'test' + - pytest-sugar==1.1.1 ; extra == 'test' + - actionlint-py==1.7.11.24 ; extra == 'lint' + - clang-format==22.1.0 ; extra == 'lint' + - codespell==2.4.1 ; extra == 'lint' + - pylint==4.0.5 ; extra == 'lint' + - ruff==0.15.4 ; extra == 'lint' + - mypy==1.19.1 ; extra == 'type' + - myst-parser==5.0.0 ; extra == 'docs' + - shibuya==2026.1.9 ; extra == 'docs' + - sphinx==8.2.3 ; extra == 'docs' + - sphinx-copybutton==0.5.2 ; extra == 'docs' + - pymmh3==0.0.5 ; extra == 'benchmark' + - pyperf==2.10.0 ; extra == 'benchmark' + - xxhash==3.6.0 ; extra == 'benchmark' + - matplotlib==3.10.8 ; extra == 'plot' + - pandas==3.0.1 ; extra == 'plot' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c7/80/9f608d13b4b3afcebd1dd13baf9551c95fc424d6390e4b1cfd7b1810cd06/async_property-0.2.2-py2.py3-none-any.whl + name: async-property + version: 0.2.2 + sha256: 8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7 +- pypi: https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl + name: asn1crypto + version: 1.5.1 + sha256: db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 +- pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl + name: pytest-xdist + version: 3.8.0 + sha256: 202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88 + requires_dist: + - execnet>=2.1 + - pytest>=7.0.0 + - filelock ; extra == 'testing' + - psutil>=3.0 ; extra == 'psutil' + - setproctitle ; extra == 'setproctitle' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ca/cb/cdeaba62aa3c48f0d8834afb82b4a21463cd83df34fe01f9daa89a08ec6c/pydata_google_auth-1.9.1-py2.py3-none-any.whl + name: pydata-google-auth + version: 1.9.1 + sha256: 75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 + requires_dist: + - setuptools + - google-auth>=1.25.0,<3.0.dev0 + - google-auth-oauthlib>=0.4.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + name: iniconfig + version: 2.3.0 + sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/cb/c1/316c8f3549dbe5245f92bfd523ec6f32dd4d98cafe21df3f6a19b1184c75/aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl + name: aiohttp + version: 3.14.1 + sha256: d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce + requires_dist: + - aiohappyeyeballs>=2.5.0 + - aiosignal>=1.4.0 + - async-timeout>=4.0,<6.0 ; python_full_version < '3.11' + - attrs>=17.3.0 + - frozenlist>=1.1.1 + - multidict>=4.5,<7.0 + - propcache>=0.2.0 + - typing-extensions>=4.4 ; python_full_version < '3.13' + - yarl>=1.17.0,<2.0 + - aiodns>=3.3.0 ; sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' + - brotli>=1.2 ; platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' + - brotlicffi>=1.2 ; platform_python_implementation != 'CPython' and extra == 'speedups' + - backports-zstd ; python_full_version < '3.14' and platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios' and extra == 'speedups' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/cc/bf/5404c2fd6ac84819e8ff1b7e34437b37cf55a2b11318894909e7bb88de3f/mmh3-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl + name: mmh3 + version: 5.2.1 + sha256: 30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb + requires_dist: + - pytest==9.0.2 ; extra == 'test' + - pytest-sugar==1.1.1 ; extra == 'test' + - actionlint-py==1.7.11.24 ; extra == 'lint' + - clang-format==22.1.0 ; extra == 'lint' + - codespell==2.4.1 ; extra == 'lint' + - pylint==4.0.5 ; extra == 'lint' + - ruff==0.15.4 ; extra == 'lint' + - mypy==1.19.1 ; extra == 'type' + - myst-parser==5.0.0 ; extra == 'docs' + - shibuya==2026.1.9 ; extra == 'docs' + - sphinx==8.2.3 ; extra == 'docs' + - sphinx-copybutton==0.5.2 ; extra == 'docs' + - pymmh3==0.0.5 ; extra == 'benchmark' + - pyperf==2.10.0 ; extra == 'benchmark' + - xxhash==3.6.0 ; extra == 'benchmark' + - matplotlib==3.10.8 ; extra == 'plot' + - pandas==3.0.1 ; extra == 'plot' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: greenlet + version: 3.5.3 + sha256: 4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + name: tzdata + version: '2026.2' + sha256: bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7 + requires_python: '>=2' +- pypi: https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl + name: librt + version: 0.11.0 + sha256: ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + name: colorama + version: 0.4.6 + sha256: 4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl + name: tenacity + version: 8.5.0 + sha256: b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 + requires_dist: + - reno ; extra == 'doc' + - sphinx ; extra == 'doc' + - pytest ; extra == 'test' + - tornado>=4.5 ; extra == 'test' + - typeguard ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + name: fsspec + version: 2026.4.0 + sha256: 11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2 + requires_dist: + - adlfs ; extra == 'abfs' + - adlfs ; extra == 'adl' + - pyarrow>=1 ; extra == 'arrow' + - dask ; extra == 'dask' + - distributed ; extra == 'dask' + - pre-commit ; extra == 'dev' + - ruff>=0.5 ; extra == 'dev' + - numpydoc ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - yarl ; extra == 'doc' + - dropbox ; extra == 'dropbox' + - dropboxdrivefs ; extra == 'dropbox' + - requests ; extra == 'dropbox' + - adlfs ; extra == 'full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' + - dask ; extra == 'full' + - distributed ; extra == 'full' + - dropbox ; extra == 'full' + - dropboxdrivefs ; extra == 'full' + - fusepy ; extra == 'full' + - gcsfs>2024.2.0 ; extra == 'full' + - libarchive-c ; extra == 'full' + - ocifs ; extra == 'full' + - panel ; extra == 'full' + - paramiko ; extra == 'full' + - pyarrow>=1 ; extra == 'full' + - pygit2 ; extra == 'full' + - requests ; extra == 'full' + - s3fs>2024.2.0 ; extra == 'full' + - smbprotocol ; extra == 'full' + - tqdm ; extra == 'full' + - fusepy ; extra == 'fuse' + - gcsfs>2024.2.0 ; extra == 'gcs' + - pygit2 ; extra == 'git' + - requests ; extra == 'github' + - gcsfs ; extra == 'gs' + - panel ; extra == 'gui' + - pyarrow>=1 ; extra == 'hdfs' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' + - libarchive-c ; extra == 'libarchive' + - ocifs ; extra == 'oci' + - s3fs>2024.2.0 ; extra == 's3' + - paramiko ; extra == 'sftp' + - smbprotocol ; extra == 'smb' + - paramiko ; extra == 'ssh' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' + - numpy ; extra == 'test' + - pytest ; extra == 'test' + - pytest-asyncio!=0.22.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-recording ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - requests ; extra == 'test' + - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' + - dask[dataframe,test] ; extra == 'test-downstream' + - moto[server]>4,<5 ; extra == 'test-downstream' + - pytest-timeout ; extra == 'test-downstream' + - xarray ; extra == 'test-downstream' + - adlfs ; extra == 'test-full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' + - backports-zstd ; python_full_version < '3.14' and extra == 'test-full' + - cloudpickle ; extra == 'test-full' + - dask ; extra == 'test-full' + - distributed ; extra == 'test-full' + - dropbox ; extra == 'test-full' + - dropboxdrivefs ; extra == 'test-full' + - fastparquet ; extra == 'test-full' + - fusepy ; extra == 'test-full' + - gcsfs ; extra == 'test-full' + - jinja2 ; extra == 'test-full' + - kerchunk ; extra == 'test-full' + - libarchive-c ; extra == 'test-full' + - lz4 ; extra == 'test-full' + - notebook ; extra == 'test-full' + - numpy ; extra == 'test-full' + - ocifs ; extra == 'test-full' + - pandas<3.0.0 ; extra == 'test-full' + - panel ; extra == 'test-full' + - paramiko ; extra == 'test-full' + - pyarrow ; extra == 'test-full' + - pyarrow>=1 ; extra == 'test-full' + - pyftpdlib ; extra == 'test-full' + - pygit2 ; extra == 'test-full' + - pytest ; extra == 'test-full' + - pytest-asyncio!=0.22.0 ; extra == 'test-full' + - pytest-benchmark ; extra == 'test-full' + - pytest-cov ; extra == 'test-full' + - pytest-mock ; extra == 'test-full' + - pytest-recording ; extra == 'test-full' + - pytest-rerunfailures ; extra == 'test-full' + - python-snappy ; extra == 'test-full' + - requests ; extra == 'test-full' + - smbprotocol ; extra == 'test-full' + - tqdm ; extra == 'test-full' + - urllib3 ; extra == 'test-full' + - zarr ; extra == 'test-full' + - zstandard ; python_full_version < '3.14' and extra == 'test-full' + - tqdm ; extra == 'tqdm' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d5/94/6db383d8ee1adf45dc6c73477152b82731fa4c4a46d9c1932cc8757e0fd4/google_cloud_storage-2.19.0-py2.py3-none-any.whl + name: google-cloud-storage + version: 2.19.0 + sha256: aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba + requires_dist: + - google-auth>=2.26.1,<3.0.dev0 + - google-api-core>=2.15.0,<3.0.0.dev0 + - google-cloud-core>=2.3.0,<3.0.dev0 + - google-resumable-media>=2.7.2 + - requests>=2.18.0,<3.0.0.dev0 + - google-crc32c>=1.0,<2.0.dev0 + - protobuf<6.0.0.dev0 ; extra == 'protobuf' + - opentelemetry-api>=1.1.0 ; extra == 'tracing' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl + name: tqdm + version: 4.68.3 + sha256: 39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03 + requires_dist: + - colorama ; sys_platform == 'win32' + - requests ; extra == 'discord' + - envwrap ; extra == 'discord' + - slack-sdk ; extra == 'slack' + - envwrap ; extra == 'slack' + - requests ; extra == 'telegram' + - envwrap ; extra == 'telegram' + - ipywidgets>=6 ; extra == 'notebook' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + name: pycryptodome + version: 3.23.0 + sha256: 187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/db/9e/82a390ecc85f066ff80affa01d195f744e3de60ad4d695b8de31c9a66da3/sqlglot-30.12.0-py3-none-any.whl + name: sqlglot + version: 30.12.0 + sha256: 86cccc610073c645c03e72b55b60ae0518aa3253a7fc3bd56551370d003c6554 + requires_dist: + - duckdb>=0.6 ; extra == 'dev' + - sqlglot-mypy>=2.1.0.post3 ; python_full_version >= '3.10' and extra == 'dev' + - mypy ; python_full_version < '3.10' and extra == 'dev' + - setuptools-scm ; extra == 'dev' + - pandas ; extra == 'dev' + - pandas-stubs ; extra == 'dev' + - python-dateutil ; extra == 'dev' + - pytz ; extra == 'dev' + - pdoc ; extra == 'dev' + - pre-commit ; extra == 'dev' + - ruff==0.15.6 ; extra == 'dev' + - types-python-dateutil ; extra == 'dev' + - types-pytz ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - pyperf ; extra == 'dev' + - sqlglotc==30.12.0 ; python_full_version >= '3.10' and extra == 'c' + - sqlglotrs==0.13.0 ; extra == 'rs' + - sqlglotc==30.12.0 ; python_full_version >= '3.10' and extra == 'rs' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/db/b0/29e98ba210c9dbc75a6dc91e3f99b9e06ea901a62ca95804e02a1ae13e6b/psycopg_binary-3.3.4-cp310-cp310-macosx_11_0_arm64.whl + name: psycopg-binary + version: 3.3.4 + sha256: 32a6fbf8481e3a370d0d72b860d35948a693cb01281da217f7b2f307636e591a + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl + name: locket + version: 1.0.0 + sha256: b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/dc/0e/f372bb290cef68c67331cd649b94d62220183ddc1b5bf3a9351ea6e9c8ec/deltalake-0.25.5-cp39-abi3-macosx_10_12_x86_64.whl + name: deltalake + version: 0.25.5 + sha256: 76be7e1ed8d13f2dc933361057a44a47a89e6112d4f5ea0a73fb510bedd96efc + requires_dist: + - pyarrow>=16,!=19.0.0 + - pandas ; extra == 'pandas' + - azure-storage-blob==12.20.0 ; extra == 'devel' + - packaging>=20 ; extra == 'devel' + - pytest ; extra == 'devel' + - pytest-mock ; extra == 'devel' + - pytest-cov ; extra == 'devel' + - pytest-timeout ; extra == 'devel' + - sphinx<=4.5 ; extra == 'devel' + - sphinx-rtd-theme ; extra == 'devel' + - toml ; extra == 'devel' + - wheel ; extra == 'devel' + - pip>=24.0 ; extra == 'devel' + - pytest-benchmark ; extra == 'devel' + - mypy==1.10.1 ; extra == 'devel' + - ruff==0.5.2 ; extra == 'devel' + - polars==1.17.1 ; extra == 'polars' + - lakefs==0.8.0 ; extra == 'lakefs' + - pyspark ; extra == 'pyspark' + - delta-spark ; extra == 'pyspark' + - numpy==1.26.4 ; extra == 'pyspark' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + name: typing-inspection + version: 0.4.2 + sha256: 4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 + requires_dist: + - typing-extensions>=4.12.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: sqlalchemy + version: 2.0.51 + sha256: 4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/de/0b/52bffad0b52ae4ea53e222b594bd38c08ecac1fc410323220a7202e43da5/mmh3-5.2.1-cp310-cp310-macosx_11_0_arm64.whl + name: mmh3 + version: 5.2.1 + sha256: 0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c + requires_dist: + - pytest==9.0.2 ; extra == 'test' + - pytest-sugar==1.1.1 ; extra == 'test' + - actionlint-py==1.7.11.24 ; extra == 'lint' + - clang-format==22.1.0 ; extra == 'lint' + - codespell==2.4.1 ; extra == 'lint' + - pylint==4.0.5 ; extra == 'lint' + - ruff==0.15.4 ; extra == 'lint' + - mypy==1.19.1 ; extra == 'type' + - myst-parser==5.0.0 ; extra == 'docs' + - shibuya==2026.1.9 ; extra == 'docs' + - sphinx==8.2.3 ; extra == 'docs' + - sphinx-copybutton==0.5.2 ; extra == 'docs' + - pymmh3==0.0.5 ; extra == 'benchmark' + - pyperf==2.10.0 ; extra == 'benchmark' + - xxhash==3.6.0 ; extra == 'benchmark' + - matplotlib==3.10.8 ; extra == 'plot' + - pandas==3.0.1 ; extra == 'plot' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: hf-xet + version: 1.5.1 + sha256: 892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350 + requires_dist: + - pytest ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl + name: packaging + version: '26.2' + sha256: 5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e0/3d/28c61f9c628dfaf1f96bb8b8592cb006eaad3747248c95f9ae7f694abb47/hiredis-3.4.0-cp310-cp310-macosx_11_0_arm64.whl + name: hiredis + version: 3.4.0 + sha256: 98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl + name: ast-serialize + version: 0.5.0 + sha256: 0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl + name: py-cpuinfo + version: 9.0.0 + sha256: 859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5 +- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + name: shellingham + version: 1.5.4 + sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e1/8b/59781d0fe7b0adfbea37f600857de4be68921e454aeecf1a11bda35cdccc/wrapt-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl + name: wrapt + version: 2.2.2 + sha256: 055e6fcfaa28e58c6a8c247d48b92be9d56f818b7068aa4f22b15b3343a09931 + requires_dist: + - pytest ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl + name: sqlalchemy + version: 2.0.51 + sha256: bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + name: docker + version: 7.1.0 + sha256: c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0 + requires_dist: + - pywin32>=304 ; sys_platform == 'win32' + - requests>=2.26.0 + - urllib3>=1.26.0 + - coverage==7.2.7 ; extra == 'dev' + - pytest-cov==4.1.0 ; extra == 'dev' + - pytest-timeout==2.1.0 ; extra == 'dev' + - pytest==7.4.2 ; extra == 'dev' + - ruff==0.1.8 ; extra == 'dev' + - myst-parser==0.18.0 ; extra == 'docs' + - sphinx==5.1.1 ; extra == 'docs' + - paramiko>=2.4.3 ; extra == 'ssh' + - websocket-client>=1.3.0 ; extra == 'websockets' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl + name: msgpack + version: 1.2.1 + sha256: 1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl + name: protobuf + version: 7.35.1 + sha256: 74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl + name: fsspec + version: 2026.6.0 + sha256: 02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1 + requires_dist: + - adlfs ; extra == 'abfs' + - adlfs ; extra == 'adl' + - pyarrow>=1 ; extra == 'arrow' + - dask ; extra == 'dask' + - distributed ; extra == 'dask' + - pre-commit ; extra == 'dev' + - ruff>=0.5 ; extra == 'dev' + - numpydoc ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - yarl ; extra == 'doc' + - dropbox ; extra == 'dropbox' + - dropboxdrivefs ; extra == 'dropbox' + - requests ; extra == 'dropbox' + - adlfs ; extra == 'full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' + - dask ; extra == 'full' + - distributed ; extra == 'full' + - dropbox ; extra == 'full' + - dropboxdrivefs ; extra == 'full' + - fusepy ; extra == 'full' + - gcsfs>2024.2.0 ; extra == 'full' + - libarchive-c ; extra == 'full' + - ocifs ; extra == 'full' + - panel ; extra == 'full' + - paramiko ; extra == 'full' + - pyarrow>=1 ; extra == 'full' + - pygit2 ; extra == 'full' + - requests ; extra == 'full' + - s3fs>2024.2.0 ; extra == 'full' + - smbprotocol ; extra == 'full' + - tqdm ; extra == 'full' + - fusepy ; extra == 'fuse' + - gcsfs>2024.2.0 ; extra == 'gcs' + - pygit2 ; extra == 'git' + - requests ; extra == 'github' + - gcsfs ; extra == 'gs' + - panel ; extra == 'gui' + - pyarrow>=1 ; extra == 'hdfs' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' + - libarchive-c ; extra == 'libarchive' + - ocifs ; extra == 'oci' + - s3fs>2024.2.0 ; extra == 's3' + - paramiko ; extra == 'sftp' + - smbprotocol ; extra == 'smb' + - paramiko ; extra == 'ssh' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' + - numpy ; extra == 'test' + - pytest ; extra == 'test' + - pytest-asyncio!=0.22.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-recording ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - requests ; extra == 'test' + - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' + - dask[dataframe,test] ; extra == 'test-downstream' + - moto[server]>4,<5 ; extra == 'test-downstream' + - pytest-timeout ; extra == 'test-downstream' + - xarray ; extra == 'test-downstream' + - adlfs ; extra == 'test-full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' + - backports-zstd ; python_full_version < '3.14' and extra == 'test-full' + - cloudpickle ; extra == 'test-full' + - dask ; extra == 'test-full' + - distributed ; extra == 'test-full' + - dropbox ; extra == 'test-full' + - dropboxdrivefs ; extra == 'test-full' + - fastparquet ; extra == 'test-full' + - fusepy ; extra == 'test-full' + - gcsfs ; extra == 'test-full' + - jinja2 ; extra == 'test-full' + - kerchunk ; extra == 'test-full' + - libarchive-c ; extra == 'test-full' + - lz4 ; extra == 'test-full' + - notebook ; extra == 'test-full' + - numpy ; extra == 'test-full' + - ocifs ; extra == 'test-full' + - pandas<3.0.0 ; extra == 'test-full' + - panel ; extra == 'test-full' + - paramiko ; extra == 'test-full' + - pyarrow ; extra == 'test-full' + - pyarrow>=1 ; extra == 'test-full' + - pyftpdlib ; extra == 'test-full' + - pygit2 ; extra == 'test-full' + - pytest ; extra == 'test-full' + - pytest-asyncio!=0.22.0 ; extra == 'test-full' + - pytest-benchmark ; extra == 'test-full' + - pytest-cov ; extra == 'test-full' + - pytest-mock ; extra == 'test-full' + - pytest-recording ; extra == 'test-full' + - pytest-rerunfailures ; extra == 'test-full' + - python-snappy ; extra == 'test-full' + - requests ; extra == 'test-full' + - smbprotocol ; extra == 'test-full' + - tqdm ; extra == 'test-full' + - urllib3 ; extra == 'test-full' + - zarr<3.2.0 ; extra == 'test-full' + - zstandard ; python_full_version < '3.14' and extra == 'test-full' + - tqdm ; extra == 'tqdm' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e5/5e/5abfec5f7e89d3b7993d57cfb025ca5f968a2c18656d7fcda2b6919440b9/grpcio_status-1.81.1-py3-none-any.whl + name: grpcio-status + version: 1.81.1 + sha256: 08072fa9995f4a95c647fc6f4f85e2411573d00087bcabdf30f260114338f232 + requires_dist: + - protobuf>=6.33.5,<8.0.0 + - grpcio>=1.81.1 + - googleapis-common-protos>=1.5.5 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl + name: gunicorn + version: 26.0.0 + sha256: 40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc + requires_dist: + - packaging + - gevent>=24.10.1 ; extra == 'gevent' + - tornado>=6.5.0 ; extra == 'tornado' + - setproctitle ; extra == 'setproctitle' + - h2>=4.1.0 ; extra == 'http2' + - gunicorn-h1c>=0.6.5 ; extra == 'fast' + - gevent>=24.10.1 ; extra == 'testing' + - h2>=4.1.0 ; extra == 'testing' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-asyncio ; extra == 'testing' + - uvloop>=0.19.0 ; extra == 'testing' + - httpx[http2] ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e6/e4/77487e14fc7be47180fd0eb4267c7486d0cc59b74031839a3daf8650136b/httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: httptools + version: 0.8.0 + sha256: a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl + name: pydantic-core + version: 2.46.4 + sha256: a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl + name: psutil + version: 7.2.2 + sha256: ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 + requires_dist: + - psleak ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-instafail ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - setuptools ; extra == 'dev' + - abi3audit ; extra == 'dev' + - black ; extra == 'dev' + - check-manifest ; extra == 'dev' + - coverage ; extra == 'dev' + - packaging ; extra == 'dev' + - pylint ; extra == 'dev' + - pyperf ; extra == 'dev' + - pypinfo ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - requests ; extra == 'dev' + - rstcheck ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - toml-sort ; extra == 'dev' + - twine ; extra == 'dev' + - validate-pyproject[all] ; extra == 'dev' + - virtualenv ; extra == 'dev' + - vulture ; extra == 'dev' + - wheel ; extra == 'dev' + - colorama ; os_name == 'nt' and extra == 'dev' + - pyreadline3 ; os_name == 'nt' and extra == 'dev' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'dev' + - psleak ; extra == 'test' + - pytest ; extra == 'test' + - pytest-instafail ; extra == 'test' + - pytest-xdist ; extra == 'test' + - setuptools ; extra == 'test' + - pywin32 ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wheel ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + - wmi ; implementation_name != 'pypy' and os_name == 'nt' and extra == 'test' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl + name: watchfiles + version: 1.2.0 + sha256: b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 + requires_dist: + - anyio>=3.0.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e7/a9/39cf856d03690af6fd570cf40331f1f79acdbb3132a9c35d2c5002f7f30b/multiprocess-0.70.17-py310-none-any.whl + name: multiprocess + version: 0.70.17 + sha256: 38357ca266b51a2e22841b755d9a91e4bb7b937979a54d411677111716c32744 + requires_dist: + - dill>=0.3.9 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl + name: googleapis-common-protos + version: 1.75.0 + sha256: 961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed + requires_dist: + - protobuf>=4.25.8,<8.0.0 + - grpcio>=1.44.0,<2.0.0 ; extra == 'grpc' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e8/1d/f6d3ca1ad0725f2e08a1c6915640748a52de2e66596160a4d53b010cccf0/google_auth-2.55.1-py3-none-any.whl + name: google-auth + version: 2.55.1 + sha256: eada68dfd52b3b81191827601e2a0c3fa12540c818534b630ddc5355769c3995 + requires_dist: + - pyasn1-modules>=0.2.1 + - cryptography>=38.0.3 + - cryptography>=38.0.3 ; extra == 'cryptography' + - aiohttp>=3.8.0,<4.0.0 ; extra == 'aiohttp' + - requests>=2.20.0,<3.0.0 ; extra == 'aiohttp' + - pyopenssl ; extra == 'enterprise-cert' + - pyopenssl>=20.0.0 ; extra == 'pyopenssl' + - pyjwt>=2.0 ; extra == 'pyjwt' + - pyu2f>=0.1.5 ; extra == 'reauth' + - requests>=2.20.0,<3.0.0 ; extra == 'requests' + - grpcio ; extra == 'testing' + - flask ; extra == 'testing' + - freezegun ; extra == 'testing' + - pyjwt>=2.0 ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-localserver ; extra == 'testing' + - pyopenssl>=20.0.0 ; extra == 'testing' + - pyu2f>=0.1.5 ; extra == 'testing' + - responses ; extra == 'testing' + - urllib3 ; extra == 'testing' + - packaging ; extra == 'testing' + - aiohttp>=3.8.0,<4.0.0 ; extra == 'testing' + - requests>=2.20.0,<3.0.0 ; extra == 'testing' + - aioresponses ; extra == 'testing' + - pytest-asyncio ; extra == 'testing' + - pyopenssl<24.3.0 ; extra == 'testing' + - aiohttp<3.10.0 ; extra == 'testing' + - urllib3 ; extra == 'urllib3' + - packaging ; extra == 'urllib3' + - rsa>=3.1.4,<5 ; extra == 'rsa' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl + name: markupsafe + version: 3.0.3 + sha256: 2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl + name: uvloop + version: 0.22.1 + sha256: ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c + requires_dist: + - aiohttp>=3.10.5 ; extra == 'test' + - flake8~=6.1 ; extra == 'test' + - psutil ; extra == 'test' + - pycodestyle~=2.11.0 ; extra == 'test' + - pyopenssl~=25.3.0 ; extra == 'test' + - mypy>=0.800 ; extra == 'test' + - setuptools>=60 ; extra == 'dev' + - cython~=3.0 ; extra == 'dev' + - sphinx~=4.1.2 ; extra == 'docs' + - sphinxcontrib-asyncio~=0.3.0 ; extra == 'docs' + - sphinx-rtd-theme~=0.5.2 ; extra == 'docs' + requires_python: '>=3.8.1' +- pypi: https://files.pythonhosted.org/packages/eb/f6/4157466c10181907d07786fb41df5d0a9ff339c1770b9e2a15cfe483e845/google_cloud_bigquery_storage-2.39.0-py3-none-any.whl + name: google-cloud-bigquery-storage + version: 2.39.0 + sha256: 8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 + requires_dist: + - google-api-core[grpc]>=2.17.1,<3.0.0 + - google-auth>=2.14.1,!=2.24.0,!=2.25.0,<3.0.0 + - grpcio>=1.59.0,<2.0.0 + - grpcio>=1.75.1,<2.0.0 ; python_full_version >= '3.14' + - proto-plus>=1.22.3,<2.0.0 + - proto-plus>=1.25.0,<2.0.0 ; python_full_version >= '3.13' + - protobuf>=4.25.8,<8.0.0 + - pandas>=1.1.3 ; extra == 'pandas' + - fastavro>=1.1.0 ; extra == 'fastavro' + - pyarrow>=3.0.0 ; extra == 'pyarrow' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + name: python-dateutil + version: 2.9.0.post0 + sha256: a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + requires_dist: + - six>=1.5 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/ec/98/adc368fe369465f291ab24e18b9900473786ed1afdf861ba90467eb0767e/pytest_ordering-0.6-py3-none-any.whl + name: pytest-ordering + version: '0.6' + sha256: 3f314a178dbeb6777509548727dc69edf22d6d9a2867bf2d310ab85c403380b6 + requires_dist: + - pytest +- pypi: https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl + name: starlette + version: 1.3.1 + sha256: c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 + requires_dist: + - anyio>=3.6.2,<5 + - typing-extensions>=4.10.0 ; python_full_version < '3.13' + - httpx2>=2.0.0 ; extra == 'full' + - httpx>=0.27.0,<0.29.0 ; extra == 'full' + - itsdangerous ; extra == 'full' + - jinja2 ; extra == 'full' + - python-multipart>=0.0.18 ; extra == 'full' + - pyyaml ; extra == 'full' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + name: pytz + version: '2026.2' + sha256: 04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126 +- pypi: https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl + name: pytest-asyncio + version: 0.23.8 + sha256: 50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2 + requires_dist: + - pytest>=7.0.0,<9 + - sphinx>=5.3 ; extra == 'docs' + - sphinx-rtd-theme>=1.0 ; extra == 'docs' + - coverage>=6.2 ; extra == 'testing' + - hypothesis>=5.7.1 ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl + name: multidict + version: 6.7.1 + sha256: 974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 + requires_dist: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl + name: certifi + version: 2026.6.17 + sha256: 2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + name: pathspec + version: 1.1.1 + sha256: a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + requires_dist: + - hyperscan>=0.7 ; extra == 'hyperscan' + - typing-extensions>=4 ; extra == 'optional' + - google-re2>=1.1 ; extra == 're2' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl + name: websockets + version: '16.0' + sha256: daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + name: pygments + version: 2.20.0 + sha256: 81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + requires_dist: + - colorama>=0.4.6 ; extra == 'windows-terminal' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl + name: pyyaml + version: 6.0.3 + sha256: 214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl + name: greenlet + version: 3.5.3 + sha256: c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: greenlet + version: 3.5.3 + sha256: 9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec + requires_dist: + - sphinx ; extra == 'docs' + - furo ; extra == 'docs' + - objgraph ; extra == 'test' + - psutil ; extra == 'test' + - setuptools ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl + name: py + version: 1.11.0 + sha256: 607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' +- pypi: https://files.pythonhosted.org/packages/f7/11/402295b388dd35861007f8a26a37c2e2f284212d57bdf407c31f36043746/grpcio-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: grpcio + version: 1.81.1 + sha256: ca1cc11d82677b9662082e5478b7528e2b7db7beaa6bdff42bd62789d81be399 + requires_dist: + - typing-extensions~=4.12 + - grpcio-tools>=1.81.1 ; extra == 'protobuf' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl + name: uvicorn-worker + version: 0.3.0 + sha256: ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 + requires_dist: + - gunicorn>=20.1.0 + - uvicorn>=0.15.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl + name: google-crc32c + version: 1.8.0 + sha256: 119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: msgpack + version: 1.2.1 + sha256: 83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl + name: toolz + version: 1.1.0 + sha256: 15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl + name: aiosignal + version: 1.4.0 + sha256: 053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e + requires_dist: + - frozenlist>=1.1.0 + - typing-extensions>=4.2 ; python_full_version < '3.13' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl + name: click + version: 8.4.2 + sha256: e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: sqlalchemy + version: 2.0.51 + sha256: 2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d + requires_dist: + - importlib-metadata ; python_full_version < '3.8' + - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' + - typing-extensions>=4.6.0 + - greenlet>=1 ; extra == 'asyncio' + - mypy>=0.910 ; extra == 'mypy' + - pyodbc ; extra == 'mssql' + - pymssql ; extra == 'mssql-pymssql' + - pyodbc ; extra == 'mssql-pyodbc' + - mysqlclient>=1.4.0 ; extra == 'mysql' + - mysql-connector-python ; extra == 'mysql-connector' + - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' + - cx-oracle>=8 ; extra == 'oracle' + - oracledb>=1.0.1 ; extra == 'oracle-oracledb' + - psycopg2>=2.7 ; extra == 'postgresql' + - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' + - greenlet>=1 ; extra == 'postgresql-asyncpg' + - asyncpg ; extra == 'postgresql-asyncpg' + - psycopg2-binary ; extra == 'postgresql-psycopg2binary' + - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' + - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' + - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' + - pymysql ; extra == 'pymysql' + - greenlet>=1 ; extra == 'aiomysql' + - aiomysql>=0.2.0 ; extra == 'aiomysql' + - greenlet>=1 ; extra == 'aioodbc' + - aioodbc ; extra == 'aioodbc' + - greenlet>=1 ; extra == 'asyncmy' + - asyncmy>=0.2.3,!=0.2.4,!=0.2.6 ; extra == 'asyncmy' + - greenlet>=1 ; extra == 'aiosqlite' + - aiosqlite ; extra == 'aiosqlite' + - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' + - sqlcipher3-binary ; extra == 'sqlcipher' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/fc/a3/d936e8b53edd9684100a6aefaf3fcabaa54728fe33324436c8d279c047aa/coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl + name: coverage + version: 7.15.2 + sha256: 44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl + name: pydantic + version: 2.13.4 + sha256: 45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba + requires_dist: + - annotated-types>=0.6.0 + - pydantic-core==2.46.4 + - typing-extensions>=4.14.1 + - typing-inspection>=0.4.2 + - email-validator>=2.0.0 ; extra == 'email' + - tzdata ; python_full_version >= '3.9' and sys_platform == 'win32' and extra == 'timezone' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl + name: async-timeout + version: 5.0.1 + sha256: 39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c + requires_python: '>=3.8' diff --git a/protos/feast/core/Aggregation.proto b/protos/feast/core/Aggregation.proto index d2d6cab7021..c1103b78769 100644 --- a/protos/feast/core/Aggregation.proto +++ b/protos/feast/core/Aggregation.proto @@ -12,4 +12,5 @@ message Aggregation { string function = 2; google.protobuf.Duration time_window = 3; google.protobuf.Duration slide_interval = 4; + string name = 5; } \ No newline at end of file diff --git a/protos/feast/core/DataFormat.proto b/protos/feast/core/DataFormat.proto index 0a32089b0f3..98464fa3c2b 100644 --- a/protos/feast/core/DataFormat.proto +++ b/protos/feast/core/DataFormat.proto @@ -27,12 +27,59 @@ message FileFormat { // Defines options for the Parquet data format message ParquetFormat {} - // Defines options for delta data format - message DeltaFormat {} - oneof format { ParquetFormat parquet_format = 1; + // Deprecated: Delta Lake is a table format, not a file format. + // Use TableFormat.DeltaFormat instead for Delta Lake support. + TableFormat.DeltaFormat delta_format = 2 [deprecated = true]; + } +} + +message TableFormat { + // Defines options for Apache Iceberg table format + message IcebergFormat { + // Optional catalog name for the Iceberg table + string catalog = 1; + + // Optional namespace (schema/database) within the catalog + string namespace = 2; + + // Additional properties for Iceberg configuration + // Examples: warehouse location, snapshot-id, as-of-timestamp, etc. + map properties = 3; + } + + // Defines options for Delta Lake table format + message DeltaFormat { + // Optional checkpoint location for Delta transaction logs + string checkpoint_location = 1; + + // Additional properties for Delta configuration + // Examples: auto-optimize settings, vacuum settings, etc. + map properties = 2; + } + + // Defines options for Apache Hudi table format + message HudiFormat { + // Type of Hudi table (COPY_ON_WRITE or MERGE_ON_READ) + string table_type = 1; + + // Field(s) that uniquely identify a record + string record_key = 2; + + // Field used to determine the latest version of a record + string precombine_field = 3; + + // Additional properties for Hudi configuration + // Examples: compaction strategy, indexing options, etc. + map properties = 4; + } + + // Specifies the table format and format-specific options + oneof format { + IcebergFormat iceberg_format = 1; DeltaFormat delta_format = 2; + HudiFormat hudi_format = 3; } } diff --git a/protos/feast/core/DataSource.proto b/protos/feast/core/DataSource.proto index b27767f527c..fe04e9ffc3e 100644 --- a/protos/feast/core/DataSource.proto +++ b/protos/feast/core/DataSource.proto @@ -29,14 +29,14 @@ import "feast/types/Value.proto"; import "feast/core/Feature.proto"; // Defines a Data Source that can be used source Feature data -// Next available id: 28 +// Next available id: 29 message DataSource { // Field indexes should *not* be reused. Not sure if fields 6-10 were used previously or not, // but they are going to be reserved for backwards compatibility. reserved 6 to 10; // Type of Data Source. - // Next available id: 12 + // Next available id: 14 enum SourceType { INVALID = 0; BATCH_FILE = 1; @@ -51,6 +51,7 @@ message DataSource { BATCH_TRINO = 10; BATCH_SPARK = 11; BATCH_ATHENA = 12; + BATCH_ICEBERG = 13; } // Unique name of data source within the project @@ -81,6 +82,10 @@ message DataSource { // Must specify creation timestamp column name string created_timestamp_column = 5; + // (Optional) Type of the timestamp_field column ("TIMESTAMP" or "DATE"). + // When set to "DATE", SQL generation uses date-only comparisons. + string timestamp_field_type = 28; + // This is an internal field that is represents the python class for the data source object a proto object represents. // This should be set by feast, and not by users. // The field is used primarily by custom data sources and is mandatory for them to set. Feast may set it for @@ -231,6 +236,9 @@ message DataSource { // Date Format of date partition column (e.g. %Y-%m-%d) string date_partition_column_format = 5; + + // Table Format (e.g. iceberg, delta, hudi) + TableFormat table_format = 6; } // Defines configuration for custom third-party data sources. diff --git a/protos/feast/core/FeatureService.proto b/protos/feast/core/FeatureService.proto index 380b2dc3718..557782fe552 100644 --- a/protos/feast/core/FeatureService.proto +++ b/protos/feast/core/FeatureService.proto @@ -38,6 +38,10 @@ message FeatureServiceSpec { // (optional) if provided logging will be enabled for this feature service. LoggingConfig logging_config = 7; + + // When true, a pre-computed feature vector is maintained per entity for this + // service, enabling single-read online retrieval instead of per-feature-view reads. + bool precompute_online = 8; } diff --git a/protos/feast/core/FeatureView.proto b/protos/feast/core/FeatureView.proto index 6306d425be3..e9fab625bb4 100644 --- a/protos/feast/core/FeatureView.proto +++ b/protos/feast/core/FeatureView.proto @@ -28,6 +28,26 @@ import "feast/core/DataSource.proto"; import "feast/core/Feature.proto"; import "feast/core/Transformation.proto"; +// Lifecycle state of a feature view. +enum FeatureViewState { + // Default value for backward compatibility. Treated as AVAILABLE_ONLINE + // for existing feature views that predate the state machine. + STATE_UNSPECIFIED = 0; + + // Feature view has been registered via feast apply but no data is available. + CREATED = 1; + + // Feature engineering / offline data generation is complete. + // The feature view is ready to be materialized. + GENERATED = 2; + + // Materialization is currently in progress. + MATERIALIZING = 3; + + // Materialization completed. Features are available in the online store. + AVAILABLE_ONLINE = 4; +} + message FeatureView { // User-specified specifications of this feature view. FeatureViewSpec spec = 1; @@ -36,7 +56,7 @@ message FeatureView { FeatureViewMeta meta = 2; } -// Next available id: 17 +// Next available id: 20 // TODO(adchia): refactor common fields from this and ODFV into separate metadata proto message FeatureViewSpec { // Name of the feature view. Must be unique. Not updated. @@ -61,6 +81,7 @@ message FeatureViewSpec { google.protobuf.Duration ttl = 6; // Batch/Offline DataSource where this view can retrieve offline feature data. + // Optional: if not set, the feature view has no associated batch data source (e.g. purely derived views). DataSource batch_source = 7; // Whether these features should be served online or not @@ -68,6 +89,7 @@ message FeatureViewSpec { bool online = 8; // Streaming DataSource from where this view can consume "online" feature data. + // Optional: only required for streaming feature views. DataSource stream_source = 9; // Description of the feature view. @@ -89,6 +111,20 @@ message FeatureViewSpec { // The transformation mode (e.g., "python", "pandas", "spark", "sql", "ray") string mode = 16; + + // Whether schema validation is enabled during materialization + bool enable_validation = 17; + + // User-specified version pin (e.g. "latest", "v2", "version2") + string version = 18; + + // Organizational unit that owns this feature view (e.g. "ads", "search"). + string org = 19; + + // Whether this feature view is disabled for serving and materialization. + // When true, the feature view will not serve online features or be materialized. + // Defaults to false (enabled) for backward compatibility. + bool disabled = 20; } message FeatureViewMeta { @@ -100,6 +136,15 @@ message FeatureViewMeta { // List of pairs (start_time, end_time) for which this feature view has been materialized. repeated MaterializationInterval materialization_intervals = 3; + + // The current version number of this feature view in the version history. + int32 current_version_number = 4; + + // Auto-generated UUID identifying this specific version. + string version_id = 5; + + // Lifecycle state of this feature view. + FeatureViewState state = 6; } message MaterializationInterval { diff --git a/protos/feast/core/FeatureViewProjection.proto b/protos/feast/core/FeatureViewProjection.proto index b0e697b656f..a130eb96ad2 100644 --- a/protos/feast/core/FeatureViewProjection.proto +++ b/protos/feast/core/FeatureViewProjection.proto @@ -32,4 +32,11 @@ message FeatureViewProjection { // Streaming DataSource from where this view can consume "online" feature data. DataSource stream_source = 9; + // Optional version tag for version-qualified feature references (e.g., @v2). + optional int32 version_tag = 10; + + // Distinguishes the source view type (e.g., "featureView", "labelView"). + // Empty string or unset means "featureView" for backward compatibility. + string view_type = 11; + } diff --git a/protos/feast/core/FeatureViewVersion.proto b/protos/feast/core/FeatureViewVersion.proto new file mode 100644 index 00000000000..c88a43eea80 --- /dev/null +++ b/protos/feast/core/FeatureViewVersion.proto @@ -0,0 +1,42 @@ +// +// Copyright 2024 The Feast Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +syntax = "proto3"; +package feast.core; + +option go_package = "github.com/feast-dev/feast/go/protos/feast/core"; +option java_outer_classname = "FeatureViewVersionProto"; +option java_package = "feast.proto.core"; + +import "google/protobuf/timestamp.proto"; + +message FeatureViewVersionRecord { + string feature_view_name = 1; + string project_id = 2; + int32 version_number = 3; + // "feature_view" | "stream_feature_view" | "on_demand_feature_view" + string feature_view_type = 4; + // serialized FV proto snapshot + bytes feature_view_proto = 5; + google.protobuf.Timestamp created_timestamp = 6; + string description = 7; + // auto-generated UUID for unique identification + string version_id = 8; +} + +message FeatureViewVersionHistory { + repeated FeatureViewVersionRecord records = 1; +} diff --git a/protos/feast/core/LabelView.proto b/protos/feast/core/LabelView.proto new file mode 100644 index 00000000000..8d2ff2abcf5 --- /dev/null +++ b/protos/feast/core/LabelView.proto @@ -0,0 +1,96 @@ +// +// Copyright 2026 The Feast Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +syntax = "proto3"; +package feast.core; + +option go_package = "github.com/feast-dev/feast/go/protos/feast/core"; +option java_outer_classname = "LabelViewProto"; +option java_package = "feast.proto.core"; + +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "feast/core/DataSource.proto"; +import "feast/core/Feature.proto"; + +message LabelView { + LabelViewSpec spec = 1; + LabelViewMeta meta = 2; +} + +enum ConflictResolutionPolicy { + LAST_WRITE_WINS = 0; + LABELER_PRIORITY = 1; + MAJORITY_VOTE = 2; +} + +// Next available id: 16 +message LabelViewSpec { + // Name of the label view. Must be unique. Not updated. + string name = 1; + + // Name of Feast project that this label view belongs to. + string project = 2; + + // List of names of entities associated with this label view. + repeated string entities = 3; + + // List of specifications for each label field defined as part of this label view. + repeated FeatureSpecV2 features = 4; + + // User defined metadata. + map tags = 5; + + // Labels older than ttl will be excluded from online serving. + google.protobuf.Duration ttl = 6; + + // DataSource (typically a PushSource) that feeds label data into this view. + DataSource source = 7; + + // Whether labels should be served from the online store. + bool online = 8; + + // Description of the label view. + string description = 9; + + // Owner of the label view. + string owner = 10; + + // List of specifications for each entity column. + repeated FeatureSpecV2 entity_columns = 11; + + // The schema field that identifies the labeler (e.g. "labeler"). + string labeler_field = 12; + + // How conflicting labels from different labelers are resolved. + // Enforced for offline store reads; online store uses LAST_WRITE_WINS. + ConflictResolutionPolicy conflict_policy = 13; + + // DEPRECATED: retain_history is always true (offline store always appends). + // Field kept for backwards wire compatibility; ignored by the SDK. + bool retain_history = 14 [deprecated = true]; + + // Optional name of the FeatureView whose entities this label view annotates. + string reference_feature_view = 15; +} + +message LabelViewMeta { + // Time when this Label View was created. + google.protobuf.Timestamp created_timestamp = 1; + + // Time when this Label View was last updated. + google.protobuf.Timestamp last_updated_timestamp = 2; +} diff --git a/protos/feast/core/OnDemandFeatureView.proto b/protos/feast/core/OnDemandFeatureView.proto index 4b8dabb4f39..47372af15b2 100644 --- a/protos/feast/core/OnDemandFeatureView.proto +++ b/protos/feast/core/OnDemandFeatureView.proto @@ -36,7 +36,7 @@ message OnDemandFeatureView { OnDemandFeatureViewMeta meta = 2; } -// Next available id: 9 +// Next available id: 19 message OnDemandFeatureViewSpec { // Name of the feature view. Must be unique. Not updated. string name = 1; @@ -75,6 +75,16 @@ message OnDemandFeatureViewSpec { // Aggregation definitions repeated Aggregation aggregations = 16; + // User-specified version pin (e.g. "latest", "v2", "version2") + string version = 17; + + // Organizational unit that owns this feature view (e.g. "ads", "search"). + string org = 18; + + // Whether this feature view is disabled for serving. + // When true, the feature view will not serve features. + // Defaults to false (enabled) for backward compatibility. + bool disabled = 19; } message OnDemandFeatureViewMeta { @@ -83,6 +93,15 @@ message OnDemandFeatureViewMeta { // Time where this Feature View is last updated google.protobuf.Timestamp last_updated_timestamp = 2; + + // The current version number of this feature view in the version history. + int32 current_version_number = 3; + + // Auto-generated UUID identifying this specific version. + string version_id = 4; + + // Lifecycle state of this feature view. + FeatureViewState state = 5; } message OnDemandSource { diff --git a/protos/feast/core/Permission.proto b/protos/feast/core/Permission.proto index 8a876a0dc78..e0c1d4efb98 100644 --- a/protos/feast/core/Permission.proto +++ b/protos/feast/core/Permission.proto @@ -46,6 +46,7 @@ message PermissionSpec { SAVED_DATASET = 8; PERMISSION = 9; PROJECT = 10; + LABEL_VIEW = 11; } repeated Type types = 3; diff --git a/protos/feast/core/PrecomputedFeatureVector.proto b/protos/feast/core/PrecomputedFeatureVector.proto new file mode 100644 index 00000000000..798df0b8d1e --- /dev/null +++ b/protos/feast/core/PrecomputedFeatureVector.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; +package feast.core; + +option go_package = "github.com/feast-dev/feast/go/protos/feast/core"; +option java_outer_classname = "PrecomputedFeatureVectorProto"; +option java_package = "feast.proto.core"; + +import "google/protobuf/timestamp.proto"; +import "feast/types/Value.proto"; + +// A pre-computed feature vector stores all features for a FeatureService +// as a single serialized blob per entity, enabling O(1) online retrieval. +message PrecomputedFeatureVector { + // Fully-qualified feature names in deterministic order, e.g. "fv1__feat1". + repeated string feature_names = 1; + + // Feature values in the same order as feature_names. + repeated feast.types.Value values = 2; + + // Per-feature-view event timestamps for TTL enforcement at read time. + repeated FeatureViewTimestamp fv_timestamps = 3; + + // Wall-clock time when this vector was assembled. + google.protobuf.Timestamp precomputed_at = 4; +} + +// Event timestamp associated with a specific feature view within the vector. +message FeatureViewTimestamp { + string feature_view_name = 1; + google.protobuf.Timestamp event_timestamp = 2; +} diff --git a/protos/feast/core/Registry.proto b/protos/feast/core/Registry.proto index 45ecd2c173e..1224f6d26ff 100644 --- a/protos/feast/core/Registry.proto +++ b/protos/feast/core/Registry.proto @@ -1,63 +1,67 @@ -// -// * Copyright 2020 The Feast Authors -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * https://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// - -syntax = "proto3"; - -package feast.core; -option java_package = "feast.proto.core"; -option java_outer_classname = "RegistryProto"; -option go_package = "github.com/feast-dev/feast/go/protos/feast/core"; - -import "feast/core/Entity.proto"; -import "feast/core/FeatureService.proto"; -import "feast/core/FeatureTable.proto"; -import "feast/core/FeatureView.proto"; -import "feast/core/InfraObject.proto"; -import "feast/core/OnDemandFeatureView.proto"; -import "feast/core/StreamFeatureView.proto"; -import "feast/core/DataSource.proto"; -import "feast/core/SavedDataset.proto"; -import "feast/core/ValidationProfile.proto"; -import "google/protobuf/timestamp.proto"; -import "feast/core/Permission.proto"; -import "feast/core/Project.proto"; - -// Next id: 18 -message Registry { - repeated Entity entities = 1; - repeated FeatureTable feature_tables = 2; - repeated FeatureView feature_views = 6; - repeated DataSource data_sources = 12; - repeated OnDemandFeatureView on_demand_feature_views = 8; - repeated StreamFeatureView stream_feature_views = 14; - repeated FeatureService feature_services = 7; - repeated SavedDataset saved_datasets = 11; - repeated ValidationReference validation_references = 13; - Infra infra = 10; - // Tracking metadata of Feast by project - repeated ProjectMetadata project_metadata = 15 [deprecated = true]; - - string registry_schema_version = 3; // to support migrations; incremented when schema is changed - string version_id = 4; // version id, random string generated on each update of the data; now used only for debugging purposes - google.protobuf.Timestamp last_updated = 5; - repeated Permission permissions = 16; - repeated Project projects = 17; -} - -message ProjectMetadata { - string project = 1; - string project_uuid = 2; -} +// +// * Copyright 2020 The Feast Authors +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// + +syntax = "proto3"; + +package feast.core; +option java_package = "feast.proto.core"; +option java_outer_classname = "RegistryProto"; +option go_package = "github.com/feast-dev/feast/go/protos/feast/core"; + +import "feast/core/Entity.proto"; +import "feast/core/FeatureService.proto"; +import "feast/core/FeatureTable.proto"; +import "feast/core/FeatureView.proto"; +import "feast/core/InfraObject.proto"; +import "feast/core/OnDemandFeatureView.proto"; +import "feast/core/StreamFeatureView.proto"; +import "feast/core/DataSource.proto"; +import "feast/core/SavedDataset.proto"; +import "feast/core/ValidationProfile.proto"; +import "google/protobuf/timestamp.proto"; +import "feast/core/Permission.proto"; +import "feast/core/Project.proto"; +import "feast/core/FeatureViewVersion.proto"; +import "feast/core/LabelView.proto"; + +// Next id: 20 +message Registry { + repeated Entity entities = 1; + repeated FeatureTable feature_tables = 2; + repeated FeatureView feature_views = 6; + repeated DataSource data_sources = 12; + repeated OnDemandFeatureView on_demand_feature_views = 8; + repeated StreamFeatureView stream_feature_views = 14; + repeated FeatureService feature_services = 7; + repeated SavedDataset saved_datasets = 11; + repeated ValidationReference validation_references = 13; + Infra infra = 10; + // Tracking metadata of Feast by project + repeated ProjectMetadata project_metadata = 15 [deprecated = true]; + + string registry_schema_version = 3; // to support migrations; incremented when schema is changed + string version_id = 4; // version id, random string generated on each update of the data; now used only for debugging purposes + google.protobuf.Timestamp last_updated = 5; + repeated Permission permissions = 16; + repeated Project projects = 17; + FeatureViewVersionHistory feature_view_version_history = 18; + repeated LabelView label_views = 19; +} + +message ProjectMetadata { + string project = 1; + string project_uuid = 2; +} diff --git a/protos/feast/core/SavedDataset.proto b/protos/feast/core/SavedDataset.proto index 111548aa480..c4a65accd36 100644 --- a/protos/feast/core/SavedDataset.proto +++ b/protos/feast/core/SavedDataset.proto @@ -48,6 +48,19 @@ message SavedDatasetSpec { // User defined metadata map tags = 7; + + // Optional logical namespace for hierarchical grouping. + // Maps to the top-level prefix in Iceberg REST Catalog API. + // Empty string means not set (no namespace scoping). + string namespace = 9; + + // Optional sub-grouping within a namespace. + // Maps to the namespace level in Iceberg REST Catalog API. + // Empty string means not set (dataset sits directly under namespace). + string collection = 10; + + // Description of the saved dataset. + string description = 11; } message SavedDatasetStorage { diff --git a/protos/feast/core/StreamFeatureView.proto b/protos/feast/core/StreamFeatureView.proto index cb7da0faf34..8539e74bb6d 100644 --- a/protos/feast/core/StreamFeatureView.proto +++ b/protos/feast/core/StreamFeatureView.proto @@ -37,7 +37,7 @@ message StreamFeatureView { FeatureViewMeta meta = 2; } -// Next available id: 17 +// Next available id: 23 message StreamFeatureViewSpec { // Name of the feature view. Must be unique. Not updated. string name = 1; @@ -92,5 +92,26 @@ message StreamFeatureViewSpec { // Oneof with {user_defined_function, on_demand_substrait_transformation} FeatureTransformationV2 feature_transformation = 17; + + // Enable tiling for efficient window aggregation + bool enable_tiling = 18; + + // Hop size for tiling (e.g., 5 minutes). Determines the granularity of pre-aggregated tiles. + // If not specified, defaults to 5 minutes. Only used when enable_tiling is true. + google.protobuf.Duration tiling_hop_size = 19; + + // Whether schema validation is enabled during materialization + bool enable_validation = 20; + + // User-specified version pin (e.g. "latest", "v2", "version2") + string version = 21; + + // Organizational unit that owns this stream feature view (e.g. "ads", "search"). + string org = 22; + + // Whether this feature view is disabled for serving and materialization. + // When true, the feature view will not serve online features or be materialized. + // Defaults to false (enabled) for backward compatibility. + bool disabled = 23; } diff --git a/protos/feast/registry/RegistryServer.proto b/protos/feast/registry/RegistryServer.proto index 1e305abe728..2906e8a80b5 100644 --- a/protos/feast/registry/RegistryServer.proto +++ b/protos/feast/registry/RegistryServer.proto @@ -14,6 +14,7 @@ import "feast/core/FeatureService.proto"; import "feast/core/SavedDataset.proto"; import "feast/core/ValidationProfile.proto"; import "feast/core/InfraObject.proto"; +import "feast/core/LabelView.proto"; import "feast/core/Permission.proto"; import "feast/core/Project.proto"; @@ -50,6 +51,10 @@ service RegistryServer{ rpc GetOnDemandFeatureView (GetOnDemandFeatureViewRequest) returns (feast.core.OnDemandFeatureView) {} rpc ListOnDemandFeatureViews (ListOnDemandFeatureViewsRequest) returns (ListOnDemandFeatureViewsResponse) {} + // LabelView RPCs + rpc GetLabelView (GetLabelViewRequest) returns (feast.core.LabelView) {} + rpc ListLabelViews (ListLabelViewsRequest) returns (ListLabelViewsResponse) {} + // FeatureService RPCs rpc ApplyFeatureService (ApplyFeatureServiceRequest) returns (google.protobuf.Empty) {} rpc GetFeatureService (GetFeatureServiceRequest) returns (feast.core.FeatureService) {} @@ -62,6 +67,12 @@ service RegistryServer{ rpc ListSavedDatasets (ListSavedDatasetsRequest) returns (ListSavedDatasetsResponse) {} rpc DeleteSavedDataset (DeleteSavedDatasetRequest) returns (google.protobuf.Empty) {} + // SavedDataset Operation RPCs + rpc CreateDatasetFromRetrieval (CreateDatasetFromRetrievalRequest) returns (CreateDatasetFromRetrievalResponse) {} + rpc GetDatasetData (GetDatasetDataRequest) returns (GetDatasetDataResponse) {} + rpc GetDatasetJobStatus (GetDatasetJobStatusRequest) returns (GetDatasetJobStatusResponse) {} + rpc ListDatasetJobs (ListDatasetJobsRequest) returns (ListDatasetJobsResponse) {} + // ValidationReference RPCs rpc ApplyValidationReference (ApplyValidationReferenceRequest) returns (google.protobuf.Empty) {} rpc GetValidationReference (GetValidationReferenceRequest) returns (feast.core.ValidationReference) {} @@ -220,6 +231,7 @@ message ApplyFeatureViewRequest { feast.core.FeatureView feature_view = 1; feast.core.OnDemandFeatureView on_demand_feature_view = 2; feast.core.StreamFeatureView stream_feature_view = 3; + feast.core.LabelView label_view = 6; } string project = 4; bool commit = 5; @@ -255,6 +267,7 @@ message AnyFeatureView { feast.core.FeatureView feature_view = 1; feast.core.OnDemandFeatureView on_demand_feature_view = 2; feast.core.StreamFeatureView stream_feature_view = 3; + feast.core.LabelView label_view = 4; } } @@ -278,6 +291,7 @@ message ListAllFeatureViewsRequest { string data_source = 7; PaginationParams pagination = 8; SortingParams sorting = 9; + google.protobuf.Timestamp updated_since = 10; } message ListAllFeatureViewsResponse { @@ -328,6 +342,27 @@ message ListOnDemandFeatureViewsResponse { PaginationMetadata pagination = 2; } +// LabelViews + +message GetLabelViewRequest { + string name = 1; + string project = 2; + bool allow_cache = 3; +} + +message ListLabelViewsRequest { + string project = 1; + bool allow_cache = 2; + map tags = 3; + PaginationParams pagination = 4; + SortingParams sorting = 5; +} + +message ListLabelViewsResponse { + repeated feast.core.LabelView label_views = 1; + PaginationMetadata pagination = 2; +} + // FeatureServices message ApplyFeatureServiceRequest { @@ -382,6 +417,10 @@ message ListSavedDatasetsRequest { map tags = 3; PaginationParams pagination = 4; SortingParams sorting = 5; + // Optional logical namespace filter. Empty string means no filter. + string namespace = 6; + // Optional collection filter. Empty string means no filter. + string collection = 7; } message ListSavedDatasetsResponse { @@ -395,6 +434,71 @@ message DeleteSavedDatasetRequest { bool commit = 3; } +// SavedDataset Operations + +message CreateDatasetFromRetrievalRequest { + string name = 1; + string project = 2; + string feature_service_name = 3; + repeated string features = 4; + string entity_source_type = 5; + string entity_source_path = 6; + repeated string entity_keys = 8; + string entity_values = 9; + string start_date = 10; + string end_date = 11; + string extra_columns = 12; + string storage_type = 13; + string storage_path = 14; + map tags = 15; + bool allow_overwrite = 16; +} + +message CreateDatasetFromRetrievalResponse { + string job_id = 1; + string status = 2; +} + +message GetDatasetDataRequest { + string name = 1; + string project = 2; + int32 limit = 3; +} + +message TabularRow { + repeated string values = 1; +} + +message GetDatasetDataResponse { + repeated string columns = 1; + repeated TabularRow rows = 2; + int32 total_rows = 3; + int32 sample_size = 4; +} + +message GetDatasetJobStatusRequest { + string job_id = 1; +} + +message GetDatasetJobStatusResponse { + string job_id = 1; + string dataset_name = 2; + string project = 3; + string status = 4; + string created_at = 5; + string completed_at = 6; + string error = 7; +} + +message ListDatasetJobsRequest { + string project = 1; + string status_filter = 2; +} + +message ListDatasetJobsResponse { + repeated GetDatasetJobStatusResponse jobs = 1; +} + // ValidationReference message ApplyValidationReferenceRequest { @@ -543,6 +647,8 @@ message Feature { google.protobuf.Timestamp created_timestamp = 6; google.protobuf.Timestamp last_updated_timestamp = 7; map tags = 8; + // Semantic kind: "feature" (input/predictor) or "label" (target/annotation). + string kind = 9; } message ListFeaturesRequest { @@ -552,6 +658,8 @@ message ListFeaturesRequest { bool allow_cache = 6; PaginationParams pagination = 4; SortingParams sorting = 5; + // Filter by kind: "feature" or "label". Empty means no filter. + string kind = 7; } message ListFeaturesResponse { diff --git a/protos/feast/serving/GrpcServer.proto b/protos/feast/serving/GrpcServer.proto index b30e1e9d74d..44d7c393b7c 100644 --- a/protos/feast/serving/GrpcServer.proto +++ b/protos/feast/serving/GrpcServer.proto @@ -1,7 +1,10 @@ syntax = "proto3"; import "feast/serving/ServingService.proto"; +import "feast/types/Value.proto"; +option java_package = "feast.proto.serving"; +option java_outer_classname = "GrpcServerAPIProto"; option go_package = "github.com/feast-dev/feast/go/protos/feast/serving"; message PushRequest { @@ -9,6 +12,7 @@ message PushRequest { string stream_feature_view = 2; bool allow_registry_cache = 3; string to = 4; + map typed_features = 5; } message PushResponse { @@ -19,6 +23,7 @@ message WriteToOnlineStoreRequest { map features = 1; string feature_view_name = 2; bool allow_registry_cache = 3; + map typed_features = 4; } message WriteToOnlineStoreResponse { @@ -29,4 +34,4 @@ service GrpcFeatureServer { rpc Push (PushRequest) returns (PushResponse) {}; rpc WriteToOnlineStore (WriteToOnlineStoreRequest) returns (WriteToOnlineStoreResponse); rpc GetOnlineFeatures (feast.serving.GetOnlineFeaturesRequest) returns (feast.serving.GetOnlineFeaturesResponse); -} \ No newline at end of file +} diff --git a/protos/feast/serving/ServingService.proto b/protos/feast/serving/ServingService.proto index 154d850099f..87e35ac4edf 100644 --- a/protos/feast/serving/ServingService.proto +++ b/protos/feast/serving/ServingService.proto @@ -91,6 +91,9 @@ message GetOnlineFeaturesRequest { // (was moved to dedicated parameter to avoid unnecessary separation logic on serving side) // A map of variable name -> list of values map request_context = 5; + + // Whether to include feature view version metadata in the response + bool include_feature_view_version_metadata = 6; } message GetOnlineFeaturesResponse { @@ -109,8 +112,14 @@ message GetOnlineFeaturesResponse { bool status = 3; } +message FeatureViewMetadata { + string name = 1; // Feature view name (e.g., "driver_stats") + int32 version = 2; // Version number (e.g., 2) +} + message GetOnlineFeaturesResponseMetadata { - FeatureList feature_names = 1; + FeatureList feature_names = 1; // Clean feature names without @v2 syntax + repeated FeatureViewMetadata feature_view_metadata = 2; // Only populated when requested } enum FieldStatus { diff --git a/protos/feast/types/Value.proto b/protos/feast/types/Value.proto index b273fecfeae..648045cb1e0 100644 --- a/protos/feast/types/Value.proto +++ b/protos/feast/types/Value.proto @@ -1,3 +1,4 @@ + /* * Copyright 2018 The Feast Authors * @@ -42,6 +43,33 @@ message ValueType { BOOL_LIST = 17; UNIX_TIMESTAMP_LIST = 18; NULL = 19; + MAP = 20; + MAP_LIST = 21; + BYTES_SET = 22; + STRING_SET = 23; + INT32_SET = 24; + INT64_SET = 25; + DOUBLE_SET = 26; + FLOAT_SET = 27; + BOOL_SET = 28; + UNIX_TIMESTAMP_SET = 29; + JSON = 32; + JSON_LIST = 33; + STRUCT = 34; + STRUCT_LIST = 35; + UUID = 36; + TIME_UUID = 37; + UUID_LIST = 38; + TIME_UUID_LIST = 39; + UUID_SET = 40; + TIME_UUID_SET = 41; + VALUE_LIST = 42; + VALUE_SET = 43; + DECIMAL = 44; + DECIMAL_LIST = 45; + DECIMAL_SET = 46; + SCALAR_MAP = 47; + ZONED_TIMESTAMP = 48; } } @@ -67,6 +95,33 @@ message Value { BoolList bool_list_val = 17; Int64List unix_timestamp_list_val = 18; Null null_val = 19; + Map map_val = 20; + MapList map_list_val = 21; + BytesSet bytes_set_val = 22; + StringSet string_set_val = 23; + Int32Set int32_set_val = 24; + Int64Set int64_set_val = 25; + DoubleSet double_set_val = 26; + FloatSet float_set_val = 27; + BoolSet bool_set_val = 28; + Int64Set unix_timestamp_set_val = 29; + string json_val = 32; + StringList json_list_val = 33; + Map struct_val = 34; + MapList struct_list_val = 35; + string uuid_val = 36; + string time_uuid_val = 37; + StringList uuid_list_val = 38; + StringList time_uuid_list_val = 39; + StringSet uuid_set_val = 40; + StringSet time_uuid_set_val = 41; + RepeatedValue list_val = 42; + RepeatedValue set_val = 43; + string decimal_val = 44; + StringList decimal_list_val = 45; + StringSet decimal_set_val = 46; + ScalarMap scalar_map_val = 47; + ZonedTimestamp zoned_timestamp_val = 48; } } @@ -74,6 +129,17 @@ enum Null { NULL = 0; } +// A timezone-aware datetime: the UTC instant plus its originating zone, so a +// zoned datetime round-trips losslessly (unlike UNIX_TIMESTAMP, which is decoded +// as UTC and discards the original zone). +message ZonedTimestamp { + // Epoch seconds (UTC instant), same convention as unix_timestamp_val. + int64 unix_timestamp = 1; + // IANA zone name (e.g. "America/Los_Angeles") or a fixed-offset string + // (e.g. "-07:00", "UTC"). Empty string is treated as UTC on read. + string zone = 2; +} + message BytesList { repeated bytes val = 1; } @@ -102,8 +168,71 @@ message BoolList { repeated bool val = 1; } +message BytesSet { + repeated bytes val = 1; +} + +message StringSet { + repeated string val = 1; +} + +message Int32Set { + repeated int32 val = 1; +} + +message Int64Set { + repeated int64 val = 1; +} + +message DoubleSet { + repeated double val = 1; +} + +message FloatSet { + repeated float val = 1; +} + +message BoolSet { + repeated bool val = 1; +} + +message Map { + map val = 1; +} + +message MapList { + repeated Map val = 1; +} + // This is to avoid an issue of being unable to specify `repeated value` in oneofs or maps // In JSON "val" field can be omitted message RepeatedValue { repeated Value val = 1; -} \ No newline at end of file +} + +// Map key for maps with non-string keys. +// Excludes string (handled by Map) and all collection types (not valid as keys). +message MapKey { + oneof key { + int32 int32_key = 1; + int64 int64_key = 2; + float float_key = 3; + double double_key = 4; + bool bool_key = 5; + int64 unix_timestamp_key = 6; + bytes bytes_key = 7; + string uuid_key = 8; + string time_uuid_key = 9; + string decimal_key = 10; + } +} + +message ScalarMapEntry { + MapKey key = 1; + Value value = 2; +} + +// Map with non-string keys. For string-keyed maps use Map. +message ScalarMap { + repeated ScalarMapEntry val = 1; +} diff --git a/pyproject.toml b/pyproject.toml index 548b8d75234..e2441730106 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ name = "feast" description = "Python SDK for Feast" readme = "README.md" requires-python = ">=3.10.0" -license = {file = "LICENSE"} +license = "Apache-2.0" +license-files = ["LICENSE"] classifiers = [ - "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10" @@ -14,17 +14,17 @@ dynamic = [ "version", ] dependencies = [ + "attrs", "click>=7.0.0,<9.0.0", "colorama>=0.3.9,<1", "dill~=0.3.0", "protobuf>=4.24.0", "Jinja2>=2,<4", - "jsonschema", "mmh3", "numpy>=2.0.0,<3", "pandas>=1.4.3,<3", - "pyarrow<=21.0.0", - "pydantic==2.10.6", + "pyarrow>=16.1.0", + "pydantic>=2.10.6", "pygments>=2.12.0,<3", "PyYAML>=5.4.0,<7", "requests", @@ -35,33 +35,40 @@ dependencies = [ "tqdm>=4,<5", "typeguard>=4.0.0", "fastapi>=0.68.0", - "uvicorn[standard]>=0.30.6,<=0.34.0", + "starlette>=1.0.1", + "uvicorn[standard]>=0.30.6", "uvicorn-worker", "gunicorn; platform_system != 'Windows'", "dask[dataframe]>=2024.2.1", - "prometheus_client", + "prometheus_client>=0.20.0,<0.25.0", "psutil", "bigtree>=0.19.2", - "pyjwt", + # >=2.13 for PyJWKClient's JWK-set cache surviving transient fetch + # failures; the OIDC token parser reuses one client and relies on it. + "pyjwt>=2.13.0", ] [project.optional-dependencies] -aws = ["boto3==1.38.27", "fsspec<=2024.9.0", "aiobotocore>2,<3"] +aerospike = [ + "aerospike>=19.0.0,<20.0.0", +] +aws = ["boto3>=1.38.27", "fsspec>=2024.1.0", "aiobotocore>=2"] azure = [ "azure-storage-blob>=0.37.0", "azure-identity>=1.6.1", "SQLAlchemy>=1.4.19", "pyodbc>=4.0.30", - "pymssql<2.3.3" + "pymssql>=2.3.6" ] -cassandra = ["cassandra-driver>=3.24.0,<4"] +cassandra = ["cassandra-driver>=3.26.0,<4"] clickhouse = ["clickhouse-connect>=0.7.19"] couchbase = ["couchbase==4.3.2", "couchbase-columnar==1.0.0"] delta = ["deltalake<1.0.0"] docling = ["docling==2.27.0"] -duckdb = ["ibis-framework[duckdb]>=9.0.0,<=9.5.0"] +duckdb = ["ibis-framework[duckdb]>=10.0.0"] elasticsearch = ["elasticsearch>=8.13.0"] faiss = ["faiss-cpu>=1.7.0,<=1.10.0"] +flink = ["apache-flink>=2.2.1,<3", "pyarrow<21.0.0"] gcp = [ "google-api-core>=1.23.0,<3", "googleapis-common-protos>=1.52.0,<2", @@ -70,25 +77,19 @@ gcp = [ "google-cloud-datastore>=2.16.0,<3", "google-cloud-storage>=1.34.0,<3", "google-cloud-bigtable>=2.11.0,<3", - "fsspec<=2024.9.0", + "fsspec>=2024.1.0", ] ge = ["great_expectations>=0.15.41,<1"] go = ["cffi>=1.15.0"] grpcio = [ - "grpcio>=1.56.2,<=1.62.3", - "grpcio-reflection>=1.56.2,<=1.62.3", - "grpcio-health-checking>=1.56.2,<=1.62.3", + "grpcio>=1.56.2", + "grpcio-reflection>=1.56.2", + "grpcio-health-checking>=1.56.2", ] hazelcast = ["hazelcast-python-client>=5.1"] hbase = ["happybase>=1.2.0,<3"] ibis = [ - "ibis-framework>=9.0.0,<=9.5.0", - "ibis-substrait>=4.0.0", - "poetry-core<2", - "poetry-dynamic-versioning", -] -ikv = [ - "ikvpy>=0.0.36", + "ibis-framework>=10.0.0", ] k8s = ["kubernetes"] image = [ @@ -98,71 +99,90 @@ image = [ "scikit-learn>=1.0.0", ] milvus = [ - "pymilvus==2.4.9", - "milvus-lite==2.4.12", + "pymilvus>=2.5", + "milvus-lite>=2.4.12", "feast[setuptools]" ] -mssql = ["ibis-framework[mssql]>=9.0.0,<=9.5.0"] +mongodb = [ + "pymongo>=4.13.0,<5.0.0", + "dnspython>=2.0.0", +] +mssql = ["ibis-framework[mssql]>=10.0.0"] +oracle = ["ibis-framework[oracle]>=10.0.0"] mysql = ["pymysql", "types-PyMySQL"] +openlineage = ["openlineage-python>=1.40.0"] opentelemetry = ["prometheus_client", "psutil"] spark = ["pyspark>=4.0.0"] +iceberg = ["pyiceberg>=0.7.0"] trino = ["trino>=0.305.0,<0.400.0", "regex"] -postgres = ["psycopg[binary,pool]==3.2.5"] +postgres = ["psycopg[binary,pool]>=3.2.5"] # psycopg[c] install requires a system with a C compiler, python dev headers, & postgresql client dev headers # https://www.psycopg.org/psycopg3/docs/basic/install.html#local-installation -postgres-c = ["psycopg[c,pool]==3.2.5"] +postgres-c = ["psycopg[c,pool]>=3.2.5"] pytorch = ["torch>=2.7.0", "torchvision>=0.22.1"] qdrant = ["qdrant-client>=1.12.0"] rag = [ "transformers>=4.36.0", "datasets>=3.6.0", + "sentence-transformers>=3.0.0", ] ray = [ 'ray>=2.47.0; python_version == "3.10"', -'codeflare-sdk>=0.31.1; python_version > "3.10"' +'codeflare-sdk>=0.31.1; python_version > "3.10"', +"datasets>=3.6.0", ] redis = [ - "redis>=4.2.2,<5", - "hiredis>=2.0.0,<3", + "redis>=4.2.2,<8", + "hiredis>=2.0.0,<4", ] +scylladb = ["scylla-driver>=3.28.0,<4"] singlestore = ["singlestoredb<1.8.0"] snowflake = [ - "snowflake-connector-python[pandas]>=3.7,<4", + "snowflake-connector-python[pandas]>=3.7,<5", ] sqlite_vec = ["sqlite-vec==v0.1.6"] -mcp = ["fastapi_mcp"] +mcp = ["fastapi_mcp", "mcp>=1.0,<2"] +mlflow = ["mlflow>=2.10.0"] + +dbt = ["dbt-artifacts-parser"] + +test = [ + "pytest>=6.0.0,<8", + "pytest-xdist>=3.8.0", + "pytest-cov>=5.0.0", + "pytest-timeout==1.4.2", + "pytest-lazy-fixture==0.6.3", + "pytest-ordering~=0.6.0", + "pytest-mock==1.10.4", + "pytest-env", + "pytest-benchmark>=3.4.1,<4", + "pytest-asyncio<=0.24.0", + "py>=1.11.0", + "testcontainers>=4.15.0rc2", + "minio==7.2.11", + "python-keycloak==4.2.2", + "cryptography>=43.0", +] ci = [ + "feast[test, aws, azure, cassandra, clickhouse, couchbase, delta, docling, duckdb, elasticsearch, faiss, gcp, ge, go, grpcio, hazelcast, hbase, ibis, iceberg, image, k8s, mcp, milvus, mlflow, mongodb, mssql, mysql, openlineage, opentelemetry, oracle, scylladb, spark, trino, postgres, pytorch, qdrant, rag, ray, redis, singlestore, snowflake, sqlite_vec]", "build", "virtualenv==20.23.0", - "cryptography>=43.0,<44", + "dbt-artifacts-parser", "ruff>=0.8.0", "mypy-protobuf>=3.1", - "grpcio-tools>=1.56.2,<=1.62.3", - "grpcio-testing>=1.56.2,<=1.62.3", + "grpcio-tools>=1.56.2", + "grpcio-testing>=1.56.2", # FastAPI does not correctly pull starlette dependency on httpx see thread(https://github.com/tiangolo/fastapi/issues/5656). "httpx==0.27.2", - "minio==7.2.11", "mock==2.0.0", "moto<5", "mypy>=1.4.1,<1.11.3", - "urllib3>=1.25.4,<3", + "urllib3>=2.6.3,<3", "psutil==5.9.0", - "py>=1.11.0", # https://github.com/pytest-dev/pytest/issues/10420 - "pytest>=6.0.0,<8", - "pytest-asyncio<=0.24.0", "pytest-cov", - "pytest-xdist", - "pytest-benchmark>=3.4.1,<4", - "pytest-lazy-fixture==0.6.3", - "pytest-timeout==1.4.2", - "pytest-ordering~=0.6.0", - "pytest-mock==1.10.4", - "pytest-env", "Sphinx>4.0.0,<7", "sqlglot[rs]>=23.4", - "testcontainers==4.9.0", - "python-keycloak==4.2.2", "pre-commit<3.3.2", "assertpy==1.1", "pip-tools", @@ -176,8 +196,6 @@ ci = [ "types-requests<2.31.0", "types-setuptools", "types-tabulate", - "virtualenv<20.24.2", - "feast[aws, azure, cassandra, clickhouse, couchbase, delta, docling, duckdb, elasticsearch, faiss, gcp, ge, go, grpcio, hazelcast, hbase, ibis, ikv, image, k8s, mcp, milvus, mssql, mysql, opentelemetry, spark, trino, postgres, pytorch, qdrant, rag, ray, redis, singlestore, snowflake, sqlite_vec]" ] nlp = ["feast[docling, image, milvus, pytorch, rag]"] dev = ["feast[ci]"] @@ -227,8 +245,9 @@ requires = [ "setuptools>=60,<81", "setuptools_scm>=6.2", "sphinx!=4.0.0", - "wheel", + "wheel>=0.46.2", ] +build-backend = "setuptools.build_meta" [tool.setuptools] packages = {find = {where = ["sdk/python"], exclude = ["java", "infra", "sdk/python/tests", "ui"]}} @@ -245,16 +264,20 @@ line-length = 88 target-version = "py310" include = ["*.py", "*.pyi"] +[tool.ruff.lint] +# Keep on using old default rules: https://astral.sh/blog/ruff-v0.16.0 +select = ["E4", "E7", "E9", "F"] + [tool.ruff.format] # exclude a few common directories in the root of the project -exclude = [ - ".eggs", - ".git", - ".hg", +exclude = [ + ".eggs", + ".git", + ".hg", ".mypy_cache", - ".tox", - ".venv", - "_build", + ".tox", + ".venv", + "_build", "buck-out", "build", "dist", @@ -262,3 +285,89 @@ exclude = [ ".pyi", "protos", "sdk/python/feast/embedded_go/lib"] + +[dependency-groups] +dev = [ + "pytest-xdist>=3.8.0", +] + +[tool.uv] +conflicts = [ + [ + { extra = "flink" }, + { extra = "ge" }, + ], + [ + { extra = "flink" }, + { extra = "ci" }, + ], + [ + { extra = "flink" }, + { extra = "dev" }, + ], + [ + { extra = "flink" }, + { extra = "docs" }, + ], +] + +# Pixi configuration +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["linux-64", "osx-arm64", "osx-64"] + +[tool.pixi.feature.py310.dependencies] +python = "~=3.10.0" + +[tool.pixi.feature.duckdb-tests.pypi-dependencies] +feast = { path = ".", editable = true, extras = ["duckdb", "delta", "grpcio", "test"] } + +[tool.pixi.feature.duckdb-tests.tasks] +test = { cmd = "python -m pytest -n 8 --integration -m 'not ray_offline_stores_only' --ignore=sdk/python/tests/integration/offline_store/test_dqm_validation.py sdk/python/tests/integration/offline_store", env = { PYTHONPATH = ".", FULL_REPO_CONFIGS_MODULE = "sdk.python.tests.universal.feature_repos.duckdb_repo_configuration", FEAST_IS_LOCAL_TEST = "True" } } + +[tool.pixi.feature.ray-tests.pypi-dependencies] +feast = { path = ".", editable = true, extras = ["ray", "grpcio", "test"] } + +[tool.pixi.feature.ray-tests.tasks] +test-offline = { cmd = "python -m pytest -v --integration --ignore=sdk/python/tests/integration/offline_store/test_dqm_validation.py sdk/python/tests/integration/offline_store", env = { PYTHONPATH = ".", FULL_REPO_CONFIGS_MODULE = "sdk.python.tests.universal.feature_repos.ray_repo_configuration", FEAST_IS_LOCAL_TEST = "True" } } +test-compute = { cmd = "python -m pytest -v --integration sdk/python/tests/component/ray", env = { PYTHONPATH = ".", FEAST_IS_LOCAL_TEST = "True" } } +test-unit-compute-ray = { cmd = "python -m pytest -v sdk/python/tests/component/ray", env = { PYTHONPATH = ".", FEAST_IS_LOCAL_TEST = "True" } } +test = { depends-on = ["test-offline", "test-unit-compute-ray", "test-compute"] } + +[tool.pixi.feature.registration-tests.pypi-dependencies] +feast = { path = ".", editable = true, extras = ["aws", "gcp", "grpcio", "postgres", "mysql", "redis", "snowflake", "spark", "test"] } +grpcio-testing = ">=1.56.2" + +[tool.pixi.feature.registration-tests.tasks] +test = { cmd = "python -m pytest -n auto --dist loadgroup --integration sdk/python/tests/integration/registration", env = { PYTHONPATH = ".", FEAST_IS_LOCAL_TEST = "True" } } +test-ci = { cmd = "python -m pytest -n auto --dist loadgroup --integration sdk/python/tests/integration/registration", env = { PYTHONPATH = "." } } + +[tool.pixi.environments] +duckdb-tests = ["py310", "duckdb-tests"] +ray-tests = ["py310", "ray-tests"] +registration-tests = ["py310", "registration-tests"] + +[tool.coverage.run] +branch = true +source = ["feast"] +omit = [ + "*/tests/*", + "*/protos/*", + "*_pb2.py", + "*_pb2_grpc.py", + "feast/embedded_go/*", +] + +[tool.coverage.report] +show_missing = true +skip_covered = false +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "pass$", +] + +[tool.coverage.xml] +output = "coverage.xml" diff --git a/scripts/check-init-py.sh b/scripts/check-init-py.sh new file mode 100755 index 00000000000..726e210a9d5 --- /dev/null +++ b/scripts/check-init-py.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Check for missing __init__.py files in Python packages + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Find Python package directories missing __init__.py +missing_init_files=() + +while IFS= read -r -d '' dir; do + # Skip .ipynb_checkpoints directories and other unwanted directories + if [[ "${dir}" == *".ipynb_checkpoints"* ]] || [[ "${dir}" == *"__pycache__"* ]]; then + continue + fi + + if [[ ! -f "${dir}/__init__.py" ]] && [[ -n "$(find "${dir}" -maxdepth 1 -name "*.py" -print -quit)" ]]; then + missing_init_files+=("${dir}") + fi +done < <(find "${ROOT_DIR}/sdk/python/feast" -type d -print0) + +if [[ ${#missing_init_files[@]} -gt 0 ]]; then + echo "❌ Missing __init__.py files in:" + printf " %s\n" "${missing_init_files[@]}" + echo "" + echo "Run: touch ${missing_init_files[*]/%//__init__.py}" + exit 1 +fi + +echo "✅ All Python packages have __init__.py files" diff --git a/scripts/mypy-daemon.sh b/scripts/mypy-daemon.sh new file mode 100755 index 00000000000..0b99854f3e0 --- /dev/null +++ b/scripts/mypy-daemon.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# MyPy daemon for sub-second type checking + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +MYPY_CACHE_DIR="${ROOT_DIR}/sdk/python/.mypy_cache" +PID_FILE="$MYPY_CACHE_DIR/dmypy.pid" + +case "${1:-}" in + start) + echo "🚀 Starting MyPy daemon..." + cd ${ROOT_DIR}/sdk/python + uv run dmypy start -- --config-file=pyproject.toml + echo "✅ MyPy daemon started" + ;; + check) + echo "🔍 Running MyPy daemon check..." + cd ${ROOT_DIR}/sdk/python + time uv run dmypy check feast tests + ;; + stop) + echo "🛑 Stopping MyPy daemon..." + cd ${ROOT_DIR}/sdk/python + uv run dmypy stop + echo "✅ MyPy daemon stopped" + ;; + restart) + echo "🔄 Restarting MyPy daemon..." + $0 stop + $0 start + ;; + status) + echo "📊 MyPy daemon status:" + cd ${ROOT_DIR}/sdk/python + if uv run dmypy status; then + echo "✅ MyPy daemon is running" + else + echo "❌ MyPy daemon is not running" + fi + ;; + *) + echo "Usage: $0 {start|check|stop|restart|status}" + echo "" + echo "Commands:" + echo " start - Start the MyPy daemon" + echo " check - Run type checking with the daemon" + echo " stop - Stop the MyPy daemon" + echo " restart - Restart the daemon" + echo " status - Check daemon status" + exit 1 + ;; +esac diff --git a/scripts/uv-run.sh b/scripts/uv-run.sh new file mode 100755 index 00000000000..fe0f3cdc6df --- /dev/null +++ b/scripts/uv-run.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# UV runner script for consistent environment handling + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# Change to SDK directory for Python operations +cd "${ROOT_DIR}/sdk/python" + +# Run uv with provided arguments +exec uv "$@" diff --git a/sdk/python/docs/conf.py b/sdk/python/docs/conf.py index 5e8fd11d161..1594a7096a5 100644 --- a/sdk/python/docs/conf.py +++ b/sdk/python/docs/conf.py @@ -19,8 +19,6 @@ import os import sys -import sphinx_rtd_theme - sys.path.insert(0, os.path.abspath("../../feast")) sys.path.insert(0, os.path.abspath("../..")) @@ -32,7 +30,6 @@ # example where the Python protos did not build, which subsequently broke # the RTD build. In order to fix this, we manually compile the protos. import subprocess - from pathlib import Path # cwd will be feast/sdk/python/docs/source @@ -42,7 +39,9 @@ os.chdir(cwd.parent.parent.parent.parent) # Compile Python protos -result = subprocess.run(["python", "setup.py", "build_python_protos", "--inplace"], capture_output=True) +result = subprocess.run( + ["python", "infra/scripts/generate_protos.py"], capture_output=True +) stdout = result.stdout.decode("utf-8") stderr = result.stderr.decode("utf-8") print(f"Apply stdout:\n{stdout}") diff --git a/sdk/python/docs/index.rst b/sdk/python/docs/index.rst index 14af6a6d9ef..ad21025eb2e 100644 --- a/sdk/python/docs/index.rst +++ b/sdk/python/docs/index.rst @@ -128,6 +128,18 @@ Stream Feature View .. autoclass:: feast.stream_feature_view.StreamFeatureView :members: +Label View +---------------------- + +.. autoclass:: feast.labeling.label_view.LabelView + :members: + +Conflict Policy +---------------------- + +.. autoclass:: feast.labeling.conflict_policy.ConflictPolicy + :members: + Field ================== diff --git a/sdk/python/docs/source/conf.py b/sdk/python/docs/source/conf.py index 5e8fd11d161..1594a7096a5 100644 --- a/sdk/python/docs/source/conf.py +++ b/sdk/python/docs/source/conf.py @@ -19,8 +19,6 @@ import os import sys -import sphinx_rtd_theme - sys.path.insert(0, os.path.abspath("../../feast")) sys.path.insert(0, os.path.abspath("../..")) @@ -32,7 +30,6 @@ # example where the Python protos did not build, which subsequently broke # the RTD build. In order to fix this, we manually compile the protos. import subprocess - from pathlib import Path # cwd will be feast/sdk/python/docs/source @@ -42,7 +39,9 @@ os.chdir(cwd.parent.parent.parent.parent) # Compile Python protos -result = subprocess.run(["python", "setup.py", "build_python_protos", "--inplace"], capture_output=True) +result = subprocess.run( + ["python", "infra/scripts/generate_protos.py"], capture_output=True +) stdout = result.stdout.decode("utf-8") stderr = result.stderr.decode("utf-8") print(f"Apply stdout:\n{stdout}") diff --git a/sdk/python/docs/source/feast.aggregation.rst b/sdk/python/docs/source/feast.aggregation.rst new file mode 100644 index 00000000000..757f0976bac --- /dev/null +++ b/sdk/python/docs/source/feast.aggregation.rst @@ -0,0 +1,17 @@ +feast.aggregation package +========================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + feast.aggregation.tiling + +Module contents +--------------- + +.. automodule:: feast.aggregation + :members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.aggregation.tiling.rst b/sdk/python/docs/source/feast.aggregation.tiling.rst new file mode 100644 index 00000000000..d51ef22ed05 --- /dev/null +++ b/sdk/python/docs/source/feast.aggregation.tiling.rst @@ -0,0 +1,9 @@ +feast.aggregation.tiling package +================================= + +Module contents +--------------- + +.. automodule:: feast.aggregation.tiling + :members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.dbt.rst b/sdk/python/docs/source/feast.dbt.rst new file mode 100644 index 00000000000..3a7c28d620b --- /dev/null +++ b/sdk/python/docs/source/feast.dbt.rst @@ -0,0 +1,37 @@ +feast.dbt package +================= + +Submodules +---------- + +feast.dbt.codegen module +------------------------ + +.. automodule:: feast.dbt.codegen + :members: + :undoc-members: + :show-inheritance: + +feast.dbt.mapper module +----------------------- + +.. automodule:: feast.dbt.mapper + :members: + :undoc-members: + :show-inheritance: + +feast.dbt.parser module +----------------------- + +.. automodule:: feast.dbt.parser + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.dbt + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.diff.rst b/sdk/python/docs/source/feast.diff.rst index e4142171711..f03ab5d8409 100644 --- a/sdk/python/docs/source/feast.diff.rst +++ b/sdk/python/docs/source/feast.diff.rst @@ -4,6 +4,14 @@ feast.diff package Submodules ---------- +feast.diff.apply\_progress module +---------------------------------- + +.. automodule:: feast.diff.apply_progress + :members: + :undoc-members: + :show-inheritance: + feast.diff.infra\_diff module ----------------------------- diff --git a/sdk/python/docs/source/feast.infra.online_stores.contrib.ikv_online_store.rst b/sdk/python/docs/source/feast.infra.online_stores.contrib.ikv_online_store.rst deleted file mode 100644 index 812e30932d5..00000000000 --- a/sdk/python/docs/source/feast.infra.online_stores.contrib.ikv_online_store.rst +++ /dev/null @@ -1,21 +0,0 @@ -feast.infra.online\_stores.contrib.ikv\_online\_store package -============================================================= - -Submodules ----------- - -feast.infra.online\_stores.contrib.ikv\_online\_store.ikv module ----------------------------------------------------------------- - -.. automodule:: feast.infra.online_stores.ikv_online_store.ikv - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.infra.online_stores.ikv_online_store - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.online_stores.contrib.rst b/sdk/python/docs/source/feast.infra.online_stores.contrib.rst index d77e7d175d5..92b4b182d32 100644 --- a/sdk/python/docs/source/feast.infra.online_stores.contrib.rst +++ b/sdk/python/docs/source/feast.infra.online_stores.contrib.rst @@ -11,7 +11,6 @@ Subpackages feast.infra.online_stores.couchbase_online_store feast.infra.online_stores.hazelcast_online_store feast.infra.online_stores.hbase_online_store - feast.infra.online_stores.ikv_online_store feast.infra.online_stores.mysql_online_store Submodules diff --git a/sdk/python/docs/source/feast.infra.online_stores.ikv_online_store.rst b/sdk/python/docs/source/feast.infra.online_stores.ikv_online_store.rst deleted file mode 100644 index 391af17024f..00000000000 --- a/sdk/python/docs/source/feast.infra.online_stores.ikv_online_store.rst +++ /dev/null @@ -1,21 +0,0 @@ -feast.infra.online\_stores.ikv\_online\_store package -===================================================== - -Submodules ----------- - -feast.infra.online\_stores.ikv\_online\_store.ikv module --------------------------------------------------------- - -.. automodule:: feast.infra.online_stores.ikv_online_store.ikv - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.infra.online_stores.ikv_online_store - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.online_stores.mongodb_online_store.rst b/sdk/python/docs/source/feast.infra.online_stores.mongodb_online_store.rst new file mode 100644 index 00000000000..b221a370d31 --- /dev/null +++ b/sdk/python/docs/source/feast.infra.online_stores.mongodb_online_store.rst @@ -0,0 +1,29 @@ +feast.infra.online\_stores.mongodb\_online\_store package +================================================================= + +Submodules +---------- + +feast.infra.online\_stores.mongodb\_online\_store.mongodb module +------------------------------------------------------------------------ + +.. automodule:: feast.infra.online_stores.mongodb_online_store.mongodb + :members: + :undoc-members: + :show-inheritance: + +feast.infra.online\_stores.mongodb\_online\_store.mongodb\_repo\_configuration module +--------------------------------------------------------------------------------------------- + +.. automodule:: feast.infra.online_stores.mongodb_online_store.mongodb_repo_configuration + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: feast.infra.online_stores.mongodb_online_store + :members: + :undoc-members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.infra.online_stores.rst b/sdk/python/docs/source/feast.infra.online_stores.rst index c07c7e0c279..91c3c6c90fa 100644 --- a/sdk/python/docs/source/feast.infra.online_stores.rst +++ b/sdk/python/docs/source/feast.infra.online_stores.rst @@ -12,8 +12,8 @@ Subpackages feast.infra.online_stores.elasticsearch_online_store feast.infra.online_stores.hazelcast_online_store feast.infra.online_stores.hbase_online_store - feast.infra.online_stores.ikv_online_store feast.infra.online_stores.milvus_online_store + feast.infra.online_stores.mongodb_online_store feast.infra.online_stores.mysql_online_store feast.infra.online_stores.postgres_online_store feast.infra.online_stores.qdrant_online_store diff --git a/sdk/python/docs/source/feast.loaders.rst b/sdk/python/docs/source/feast.loaders.rst deleted file mode 100644 index d4968a29999..00000000000 --- a/sdk/python/docs/source/feast.loaders.rst +++ /dev/null @@ -1,21 +0,0 @@ -feast.loaders package -===================== - -Submodules ----------- - -feast.loaders.yaml module -------------------------- - -.. automodule:: feast.loaders.yaml - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: feast.loaders - :members: - :undoc-members: - :show-inheritance: diff --git a/sdk/python/docs/source/feast.openlineage.rst b/sdk/python/docs/source/feast.openlineage.rst new file mode 100644 index 00000000000..a861bcccf56 --- /dev/null +++ b/sdk/python/docs/source/feast.openlineage.rst @@ -0,0 +1,9 @@ +feast.openlineage package +========================= + +Module contents +--------------- + +.. automodule:: feast.openlineage + :members: + :show-inheritance: diff --git a/sdk/python/docs/source/feast.protos.feast.core.rst b/sdk/python/docs/source/feast.protos.feast.core.rst index b2fa998509c..e5bdced9f22 100644 --- a/sdk/python/docs/source/feast.protos.feast.core.rst +++ b/sdk/python/docs/source/feast.protos.feast.core.rst @@ -180,6 +180,14 @@ feast.protos.feast.core.InfraObject\_pb2\_grpc module :undoc-members: :show-inheritance: +feast.protos.feast.core.LabelView\_pb2 module +---------------------------------------------- + +.. automodule:: feast.protos.feast.core.LabelView_pb2 + :members: + :undoc-members: + :show-inheritance: + feast.protos.feast.core.OnDemandFeatureView\_pb2 module ------------------------------------------------------- diff --git a/sdk/python/docs/source/feast.rst b/sdk/python/docs/source/feast.rst index ffb8a2eb580..304083138d2 100644 --- a/sdk/python/docs/source/feast.rst +++ b/sdk/python/docs/source/feast.rst @@ -7,14 +7,16 @@ Subpackages .. toctree:: :maxdepth: 4 + feast.aggregation feast.api feast.cli + feast.dbt feast.diff feast.dqm feast.embedded_go feast.infra feast.lineage - feast.loaders + feast.openlineage feast.permissions feast.protos feast.transformation @@ -23,14 +25,6 @@ Subpackages Submodules ---------- -feast.aggregation module ------------------------- - -.. automodule:: feast.aggregation - :members: - :undoc-members: - :show-inheritance: - feast.arrow\_error\_handler module ---------------------------------- @@ -71,6 +65,14 @@ feast.data\_format module :undoc-members: :show-inheritance: +feast.demos module +------------------ + +.. automodule:: feast.demos + :members: + :undoc-members: + :show-inheritance: + feast.data\_source module ------------------------- diff --git a/sdk/python/feast/__init__.py b/sdk/python/feast/__init__.py index 6b6154ed270..7be72ea6c86 100644 --- a/sdk/python/feast/__init__.py +++ b/sdk/python/feast/__init__.py @@ -1,23 +1,40 @@ from importlib.metadata import PackageNotFoundError from importlib.metadata import version as _version +from feast.demos import copy_demo_notebooks from feast.infra.offline_stores.bigquery_source import BigQuerySource from feast.infra.offline_stores.contrib.athena_offline_store.athena_source import ( AthenaSource, ) +from feast.infra.offline_stores.contrib.oracle_offline_store.oracle_source import ( + OracleSource, +) from feast.infra.offline_stores.file_source import FileSource from feast.infra.offline_stores.redshift_source import RedshiftSource from feast.infra.offline_stores.snowflake_source import SnowflakeSource +from .aggregation import Aggregation from .batch_feature_view import BatchFeatureView +from .chunker import BaseChunker, ChunkingConfig, TextChunker from .data_source import KafkaSource, KinesisSource, PushSource, RequestSource from .dataframe import DataFrameEngine, FeastDataFrame +from .doc_embedder import DocEmbedder, SchemaTransformFn +from .embedder import ( + BaseEmbedder, + EmbeddingConfig, + EmbeddingProvider, + MultiModalEmbedder, + SentenceTransformersEmbeddingProvider, + get_embedding_provider, +) from .entity import Entity from .feature import Feature from .feature_service import FeatureService from .feature_store import FeatureStore -from .feature_view import FeatureView +from .feature_view import FeatureView, FeatureViewState from .field import Field +from .filter_models import FilterTranslator +from .labeling import ConflictPolicy, LabelView from .on_demand_feature_view import OnDemandFeatureView from .project import Project from .repo_config import RepoConfig @@ -32,7 +49,9 @@ pass __all__ = [ + "Aggregation", "BatchFeatureView", + "copy_demo_notebooks", "DataFrameEngine", "Entity", "KafkaSource", @@ -43,6 +62,9 @@ "FeatureService", "FeatureStore", "FeatureView", + "FeatureViewState", + "LabelView", + "ConflictPolicy", "OnDemandFeatureView", "RepoConfig", "StreamFeatureView", @@ -54,6 +76,19 @@ "PushSource", "RequestSource", "AthenaSource", + "OracleSource", "Project", "FeastVectorStore", + "DocEmbedder", + "SchemaTransformFn", + "BaseChunker", + "TextChunker", + "ChunkingConfig", + "BaseEmbedder", + "MultiModalEmbedder", + "EmbeddingConfig", + "EmbeddingProvider", + "SentenceTransformersEmbeddingProvider", + "get_embedding_provider", + "FilterTranslator", ] diff --git a/sdk/python/feast/aggregation.py b/sdk/python/feast/aggregation/__init__.py similarity index 62% rename from sdk/python/feast/aggregation.py rename to sdk/python/feast/aggregation/__init__.py index cfb2e7de94c..d0865c0dabd 100644 --- a/sdk/python/feast/aggregation.py +++ b/sdk/python/feast/aggregation/__init__.py @@ -1,5 +1,9 @@ +""" +Aggregation module for Feast. +""" + from datetime import timedelta -from typing import Optional +from typing import Any, Dict, Iterable, Optional, Tuple from google.protobuf.duration_pb2 import Duration from typeguard import typechecked @@ -14,15 +18,17 @@ class Aggregation: Attributes: column: str # Column name of the feature we are aggregating. - function: str # Provided built in aggregations sum, max, min, count mean + function: str # Provided built in aggregations sum, max, min, count, mean, count_distinct time_window: timedelta # The time window for this aggregation. slide_interval: timedelta # The sliding window for these aggregations + name: str # Optional override for the output feature name (defaults to {function}_{column}) """ column: str function: str time_window: Optional[timedelta] slide_interval: Optional[timedelta] + name: str def __init__( self, @@ -30,6 +36,7 @@ def __init__( function: Optional[str] = "", time_window: Optional[timedelta] = None, slide_interval: Optional[timedelta] = None, + name: Optional[str] = None, ): self.column = column or "" self.function = function or "" @@ -38,6 +45,7 @@ def __init__( self.slide_interval = self.time_window else: self.slide_interval = slide_interval + self.name = name or "" def to_proto(self) -> AggregationProto: window_duration = None @@ -55,6 +63,7 @@ def to_proto(self) -> AggregationProto: function=self.function, time_window=window_duration, slide_interval=slide_interval_duration, + name=self.name, ) @classmethod @@ -75,19 +84,58 @@ def from_proto(cls, agg_proto: AggregationProto): function=agg_proto.function, time_window=time_window, slide_interval=slide_interval, + name=agg_proto.name or None, ) return aggregation def __eq__(self, other): if not isinstance(other, Aggregation): - raise TypeError("Comparisons should only involve Aggregations.") + return False if ( self.column != other.column or self.function != other.function or self.time_window != other.time_window or self.slide_interval != other.slide_interval + or self.name != other.name ): return False return True + + def resolved_name(self, time_window: Optional[timedelta] = None) -> str: + """Return the output feature name for this aggregation. + + If ``name`` is set it is returned as-is. Otherwise the name is + derived as ``{function}_{column}``, with ``_{seconds}s`` appended + when *time_window* is provided. + """ + if self.name: + return self.name + base = f"{self.function}_{self.column}" + if time_window is not None and time_window.total_seconds() > 0: + return f"{base}_{int(time_window.total_seconds())}s" + return base + + +_FUNCTION_ALIASES: Dict[str, str] = { + "count_distinct": "nunique", +} + + +def aggregation_specs_to_agg_ops( + agg_specs: Iterable[Any], + *, + time_window_unsupported_error_message: str, +) -> Dict[str, Tuple[str, str]]: + agg_ops: Dict[str, Tuple[str, str]] = {} + for agg in agg_specs: + if getattr(agg, "time_window", None) is not None: + raise ValueError(time_window_unsupported_error_message) + alias = getattr(agg, "name", None) or f"{agg.function}_{agg.column}" + func_name = _FUNCTION_ALIASES.get(agg.function, agg.function) + agg_ops[alias] = (func_name, agg.column) + return agg_ops + + +__all__ = ["Aggregation", "aggregation_specs_to_agg_ops"] diff --git a/sdk/python/feast/aggregation/tiling/__init__.py b/sdk/python/feast/aggregation/tiling/__init__.py new file mode 100644 index 00000000000..08017db0a02 --- /dev/null +++ b/sdk/python/feast/aggregation/tiling/__init__.py @@ -0,0 +1,27 @@ +""" +Tiling for efficient time-windowed aggregations. + +This module provides tiling algorithms and interfaces +that can be implemented by any compute engine (Spark, Ray, etc.). + +Architecture: +1. Engine nodes: Convert to pandas (e.g., dataset.to_pandas(), toPandas()) +2. orchestrator.py: Generate cumulative tiles +3. tile_subtraction.py: Convert cumulative tiles to windowed aggregations +4. Engine nodes: Convert back to engine format (e.g., from_pandas(), createDataFrame()) +""" + +from feast.aggregation.tiling.base import IRMetadata, get_ir_metadata_for_aggregation +from feast.aggregation.tiling.orchestrator import apply_sawtooth_window_tiling +from feast.aggregation.tiling.tile_subtraction import ( + convert_cumulative_to_windowed, + deduplicate_keep_latest, +) + +__all__ = [ + "IRMetadata", + "get_ir_metadata_for_aggregation", + "apply_sawtooth_window_tiling", + "convert_cumulative_to_windowed", + "deduplicate_keep_latest", +] diff --git a/sdk/python/feast/aggregation/tiling/base.py b/sdk/python/feast/aggregation/tiling/base.py new file mode 100644 index 00000000000..8a4bbacf3ca --- /dev/null +++ b/sdk/python/feast/aggregation/tiling/base.py @@ -0,0 +1,97 @@ +""" +Base utilities for tiling. + +- Orchestrator works in pure pandas +- Engine-specific nodes do direct DataFrame conversions. +""" + +from dataclasses import dataclass +from typing import Any, List, Optional, Tuple + +from feast.aggregation import Aggregation + + +@dataclass +class IRMetadata: + """ + Metadata about intermediate representations for an aggregation. + + Attributes: + type: "algebraic" (sum, count, max, min) or "holistic" (avg, std, var) + ir_columns: List of IR column names (e.g., ["sum", "count"] for avg) + computation: String describing how to compute final value from IRs + """ + + type: str # "algebraic" or "holistic" + ir_columns: Optional[List[str]] = None + computation: Optional[str] = None + + +def get_ir_metadata_for_aggregation( + agg: Aggregation, feature_name: str +) -> Tuple[List[Tuple[str, Any]], IRMetadata]: + """ + Get intermediate representation metadata for an aggregation. + + This function determines what intermediate values need to be stored + to correctly compute holistic aggregations when merging tiles. + + For example: + - avg: store sum and count (not just final average) + - std: store sum, count, and sum_of_squares + - sum: just store sum (algebraic, no IRs needed) + + Args: + agg: Aggregation specification + feature_name: Name of the feature being aggregated + + Returns: + Tuple of (expression_list, metadata) + """ + agg_type = agg.function.lower() + + # Algebraic aggregations + if agg_type in ["sum", "count", "max", "min"]: + return ([], IRMetadata(type="algebraic")) + + # Holistic aggregations (need intermediate representations) + elif agg_type in ["avg", "mean"]: + # Average needs sum and count + ir_cols = [ + f"_tail_{feature_name}_sum", + f"_tail_{feature_name}_count", + ] + return ( + [], + IRMetadata( + type="avg", # Mark as avg for special handling + ir_columns=ir_cols, + computation="sum / count", + ), + ) + + elif agg_type in ["std", "stddev", "var", "variance"]: + # Std/Var need sum, count, and sum of squares + ir_cols = [ + f"_tail_{feature_name}_sum", + f"_tail_{feature_name}_count", + f"_tail_{feature_name}_sum_sq", + ] + return ( + [], + IRMetadata( + type="holistic", + ir_columns=ir_cols, + computation="sqrt((sum_sq - sum^2/count) / (count-1))", + ), + ) + + elif agg_type == "count_distinct": + raise ValueError( + "count_distinct does not support tiling. " + "Use enable_tiling=False or choose an algebraic aggregation (sum, count, min, max)." + ) + + else: + # Unknown aggregation: treat as algebraic + return ([], IRMetadata(type="algebraic")) diff --git a/sdk/python/feast/aggregation/tiling/orchestrator.py b/sdk/python/feast/aggregation/tiling/orchestrator.py new file mode 100644 index 00000000000..fbec5568799 --- /dev/null +++ b/sdk/python/feast/aggregation/tiling/orchestrator.py @@ -0,0 +1,189 @@ +""" +Tiling orchestrator. + +This module provides the core tiling logic using pure pandas operations. +Engines (Spark, Ray, etc.) just need to convert to/from pandas. +""" + +from datetime import timedelta +from typing import Any, Callable, Dict, List, Tuple, Union + +import numpy as np +import pandas as pd + +from feast.aggregation import Aggregation +from feast.aggregation.tiling.base import get_ir_metadata_for_aggregation + + +def apply_sawtooth_window_tiling( + df: pd.DataFrame, + aggregations: List[Aggregation], + group_by_keys: List[str], + timestamp_col: str, + window_size: timedelta, + hop_size: timedelta, +) -> pd.DataFrame: + """ + Generate cumulative tiles. + + This function creates cumulative "tail" aggregations that can be efficiently + merged to compute sliding window aggregations. For sawtooth windows, tiles + at time T contain the aggregation from the start of the window to T. + + Args: + df: Pandas DataFrame with input data + aggregations: List of aggregation specifications + group_by_keys: Entity column names for grouping + timestamp_col: Timestamp column name + window_size: Size of the time window + hop_size: Size of hop intervals for tiling + + Returns: + Pandas DataFrame with cumulative tiles + """ + if df.empty: + return df + + # Step 1: Add hop interval column + hop_size_ms = int(hop_size.total_seconds() * 1000) + + # Convert timestamp to milliseconds + if pd.api.types.is_datetime64_any_dtype(df[timestamp_col]): + timestamp_ms = df[timestamp_col].astype("int64") // 10**6 + else: + timestamp_ms = pd.to_datetime(df[timestamp_col]).astype("int64") // 10**6 + + # Compute hop interval (inclusive lower boundaries) + df["_hop_interval"] = (timestamp_ms // hop_size_ms) * hop_size_ms + + # Step 2: Group by entity keys + hop interval and aggregate + agg_dict: Dict[str, Tuple[str, Union[str, Callable[[Any], Any]]]] = {} + ir_metadata_dict = {} + + for agg in aggregations: + feature_name = agg.resolved_name(window_size) + _, metadata = get_ir_metadata_for_aggregation(agg, feature_name) + ir_metadata_dict[feature_name] = metadata + + # Build pandas aggregation dict based on IR metadata + if metadata.type == "algebraic": + # Algebraic aggregations: just aggregate directly + if agg.function == "sum": + agg_dict[f"_tail_{feature_name}"] = (agg.column, "sum") + elif agg.function == "count": + agg_dict[f"_tail_{feature_name}"] = (agg.column, "count") + elif agg.function == "max": + agg_dict[f"_tail_{feature_name}"] = (agg.column, "max") + elif agg.function == "min": + agg_dict[f"_tail_{feature_name}"] = (agg.column, "min") + + elif metadata.type in ("holistic", "avg", "std", "var"): + # Holistic aggregations: compute IRs + if metadata.ir_columns: + ir_column_names = metadata.ir_columns + + # For avg: compute sum and count + if len(ir_column_names) >= 2: + agg_dict[ir_column_names[0]] = (agg.column, "sum") # sum + agg_dict[ir_column_names[1]] = (agg.column, "count") # count + + # For std/var: also compute sum of squares + if len(ir_column_names) >= 3: + agg_dict[ir_column_names[2]] = ( + agg.column, + lambda x: (x**2).sum(), + ) # sum_sq + + # Perform aggregation within each hop + grouped = df.groupby(group_by_keys + ["_hop_interval"], as_index=False) + hop_aggregated = grouped.agg(**agg_dict) if agg_dict else grouped.first() + + # Step 3: Compute cumulative sums (convert hop aggregations to cumulative tiles) + result = hop_aggregated.copy() + + # Sort by entity keys and hop interval + result = result.sort_values(by=group_by_keys + ["_hop_interval"]).reset_index( + drop=True + ) + + # For each IR column, compute cumulative sum within each entity group + ir_columns = [ + col for col in result.columns if col not in group_by_keys + ["_hop_interval"] + ] + + if ir_columns: + # Generate a complete grid of hop intervals for forward-filling + # This ensures we have tiles even where no data exists + min_hop = result["_hop_interval"].min() + max_hop = result["_hop_interval"].max() + all_hops = range(int(min_hop), int(max_hop) + hop_size_ms, hop_size_ms) + + # Create cumulative tiles for each entity + cumulative_results = [] + for entity_values, group_df in result.groupby(group_by_keys): + # Create complete hop grid for this entity + entity_dict = dict( + zip( + group_by_keys, + entity_values + if isinstance(entity_values, tuple) + else (entity_values,), + ) + ) + complete_grid = pd.DataFrame( + {**entity_dict, "_hop_interval": list(all_hops)} + ) + + # Left join actual data + merged = complete_grid.merge( + group_df, on=group_by_keys + ["_hop_interval"], how="left" + ) + + # Fill NaN with 0 for IR columns + for col in ir_columns: + if col in merged.columns: + merged[col] = merged[col].fillna(0) + + # Compute cumulative sum for IR columns + for col in ir_columns: + if col in merged.columns: + merged[col] = merged[col].cumsum() + + cumulative_results.append(merged) + + result = pd.concat(cumulative_results, ignore_index=True) + + # Step 4: Add tile metadata + result["_tile_start"] = result["_hop_interval"] + result["_tile_end"] = result["_hop_interval"] + hop_size_ms + + # Step 5: Compute final feature values from IRs (for algebraic aggs, just rename) + for agg in aggregations: + feature_name = agg.resolved_name(window_size) + metadata = ir_metadata_dict[feature_name] + + if metadata.type == "algebraic": + # For algebraic, final value = IR value + ir_col = f"_tail_{feature_name}" + if ir_col in result.columns: + result[feature_name] = result[ir_col] + + elif metadata.type in ("holistic", "avg", "std", "var"): + # For holistic, compute final value from IRs + if metadata.ir_columns: + # Use IR column names directly (they have _tail_ prefix) + ir_column_names = metadata.ir_columns + + if len(ir_column_names) >= 2: + sum_col = ir_column_names[0] + count_col = ir_column_names[1] + + if sum_col in result.columns and count_col in result.columns: + # Compute avg = sum / count + result[feature_name] = np.where( + result[count_col] > 0, + result[sum_col] / result[count_col], + 0, + ) + + return result diff --git a/sdk/python/feast/aggregation/tiling/tile_subtraction.py b/sdk/python/feast/aggregation/tiling/tile_subtraction.py new file mode 100644 index 00000000000..778e05ecfb2 --- /dev/null +++ b/sdk/python/feast/aggregation/tiling/tile_subtraction.py @@ -0,0 +1,177 @@ +""" +Tile subtraction logic. + +This module provides the core algorithm for converting cumulative tiles +to windowed aggregations. All compute engines use this shared logic. +""" + +from datetime import timedelta +from typing import List + +import pandas as pd + +from feast.aggregation import Aggregation +from feast.aggregation.tiling.base import get_ir_metadata_for_aggregation + + +def convert_cumulative_to_windowed( + tiles_df: pd.DataFrame, + entity_keys: List[str], + timestamp_col: str, + window_size: timedelta, + aggregations: List[Aggregation], +) -> pd.DataFrame: + """ + Convert cumulative tiles to windowed aggregations. + + This function performs the core mathematical operation: + windowed_agg_at_T = cumulative_tile_at_T - cumulative_tile_at_(T-window_size) + + For holistic aggregations (avg, std, var), it: + 1. Subtracts intermediate representation (IR) components + 2. Recomputes the final value from windowed IRs + + Args: + tiles_df: DataFrame with cumulative tiles from orchestrator. + Must contain: entity_keys, _tile_end, IR columns, feature columns + entity_keys: Entity column names for grouping + timestamp_col: Name of the timestamp column to create + window_size: Size of the time window for aggregations + aggregations: List of aggregation specifications + + Returns: + DataFrame with windowed aggregations ready for online store. + Contains: entity_keys, timestamp_col, feature columns (no IR columns or tile metadata) + """ + if tiles_df.empty: + return tiles_df + + window_size_ms = int(window_size.total_seconds() * 1000) + + windowed_results = [] + + # Group by entity keys to process each entity separately + for entity_group, group_df in tiles_df.groupby(entity_keys): + # Sort by _tile_end to ensure correct temporal ordering + group_df = group_df.sort_values("_tile_end").reset_index(drop=True) + + # For each tile, compute windowed aggregation by subtracting previous tile + for idx, row in group_df.iterrows(): + tile_end = int(row["_tile_end"]) + window_start = tile_end - window_size_ms + + exact_match = group_df[group_df["_tile_end"] == window_start] + if len(exact_match) > 0: + prev_tile = exact_match.iloc[0] + has_prev_tile = True + else: + # Only use exact matches to ensure correct window boundaries + # If no exact match exists, any previous tile would end before + # window_start, which would compute an incorrect window that's + # larger than requested + has_prev_tile = False + + # Create windowed row (will contain windowed aggregations) + windowed_row = row.to_dict() + + # Subtract previous tile values from current tile for each aggregation + for agg in aggregations: + feature_name = agg.resolved_name(window_size) + _, metadata = get_ir_metadata_for_aggregation(agg, feature_name) + + if metadata.type == "algebraic": + # For algebraic aggregations: sum and count can be subtracted + if feature_name in group_df.columns: + current_val = float(row[feature_name]) + + if agg.function.lower() in ("max", "min"): + if has_prev_tile and feature_name in prev_tile.index: + prev_val = float(prev_tile[feature_name]) + windowed_row[feature_name] = current_val + else: + windowed_row[feature_name] = current_val + else: + # For sum and count: subtract previous from current + if has_prev_tile and feature_name in prev_tile.index: + prev_val = float(prev_tile[feature_name]) + else: + prev_val = 0.0 + windowed_row[feature_name] = current_val - prev_val + + elif metadata.type in ("holistic", "avg", "std", "var"): + # For holistic aggregations: + # 1. Subtract each IR component: windowed_IR = current_IR - previous_IR + # 2. Recompute final value from windowed IRs + if metadata.ir_columns: + ir_column_names = metadata.ir_columns + + # Subtract each IR component + for ir_col in ir_column_names: + if ir_col in group_df.columns: + current_ir = row[ir_col] + if has_prev_tile and ir_col in prev_tile.index: + prev_ir = prev_tile[ir_col] + else: + prev_ir = 0 + windowed_row[ir_col] = current_ir - prev_ir + + # Recompute final value from windowed IRs + if len(ir_column_names) >= 2: + sum_col = ir_column_names[ + 0 + ] # e.g., "_tail_avg_amount_3600s_sum" + count_col = ir_column_names[ + 1 + ] # e.g., "_tail_avg_amount_3600s_count" + + if sum_col in windowed_row and count_col in windowed_row: + count_val = windowed_row[count_col] + if count_val <= 0: + windowed_row[feature_name] = 0 + else: + # avg = windowed_sum / windowed_count + windowed_row[feature_name] = ( + windowed_row[sum_col] / count_val + ) + + # Set event_timestamp to the tile end time + windowed_row[timestamp_col] = pd.to_datetime(tile_end, unit="ms") + + windowed_results.append(windowed_row) + + if not windowed_results: + return pd.DataFrame() + + # Create DataFrame from windowed results + result_df = pd.DataFrame(windowed_results) + + # Drop internal tile metadata columns + cols_to_drop = ["_tile_start", "_tile_end", "_hop_interval"] + result_df = result_df.drop( + columns=[c for c in cols_to_drop if c in result_df.columns] + ) + return result_df + + +def deduplicate_keep_latest( + df: pd.DataFrame, entity_keys: List[str], timestamp_col: str +) -> pd.DataFrame: + """ + Keep only the latest timestamp per entity. + + Args: + df: DataFrame with entity_keys and timestamp_col + entity_keys: List of entity key column names + timestamp_col: Name of timestamp column + + Returns: + DataFrame with one row per entity (latest timestamp) + """ + if df.empty or timestamp_col not in df.columns: + return df + + return ( + df.sort_values(by=timestamp_col, ascending=False) + .groupby(entity_keys, as_index=False) + .first() + ) diff --git a/sdk/python/feast/api/registry/rest/__init__.py b/sdk/python/feast/api/registry/rest/__init__.py index 14db40d7af6..4f5712e1493 100644 --- a/sdk/python/feast/api/registry/rest/__init__.py +++ b/sdk/python/feast/api/registry/rest/__init__.py @@ -1,27 +1,160 @@ +import logging +from typing import Any, Optional + from fastapi import FastAPI +from feast.api.registry.rest.compute_engines import get_compute_engine_router from feast.api.registry.rest.data_sources import get_data_source_router from feast.api.registry.rest.entities import get_entity_router from feast.api.registry.rest.feature_services import get_feature_service_router from feast.api.registry.rest.feature_views import get_feature_view_router from feast.api.registry.rest.features import get_feature_router +from feast.api.registry.rest.label_views import get_label_view_router from feast.api.registry.rest.lineage import get_lineage_router from feast.api.registry.rest.metrics import get_metrics_router +from feast.api.registry.rest.monitoring import get_monitoring_router from feast.api.registry.rest.permissions import get_permission_router -from feast.api.registry.rest.projects import get_project_router +from feast.api.registry.rest.projects import get_project_router, get_registry_router from feast.api.registry.rest.saved_datasets import get_saved_dataset_router from feast.api.registry.rest.search import get_search_router +logger = logging.getLogger(__name__) + +_ol_store_instance: Optional[Any] = None +_ol_config: Optional[Any] = None +_ol_processor: Optional[Any] = None -def register_all_routes(app: FastAPI, grpc_handler, server=None): + +def register_all_routes(app: FastAPI, grpc_handler, server=None, store=None): app.include_router(get_entity_router(grpc_handler)) app.include_router(get_data_source_router(grpc_handler)) app.include_router(get_feature_service_router(grpc_handler)) app.include_router(get_feature_view_router(grpc_handler)) app.include_router(get_feature_router(grpc_handler)) + app.include_router(get_label_view_router(grpc_handler)) app.include_router(get_lineage_router(grpc_handler)) app.include_router(get_permission_router(grpc_handler)) app.include_router(get_project_router(grpc_handler)) - app.include_router(get_saved_dataset_router(grpc_handler)) app.include_router(get_search_router(grpc_handler)) app.include_router(get_metrics_router(grpc_handler, server)) + resolved_store = store or ( + server.store if server and hasattr(server, "store") else None + ) + app.include_router(get_saved_dataset_router(grpc_handler)) + app.include_router(get_monitoring_router(grpc_handler, store=resolved_store)) + app.include_router(get_compute_engine_router(grpc_handler, store=resolved_store)) + + if resolved_store: + app.include_router(get_registry_router(resolved_store)) + + _register_openlineage_consumer(app, resolved_store) + + +def _register_openlineage_consumer(app: FastAPI, feast_store): + """Register the OpenLineage consumer router if consumer is enabled.""" + if feast_store is None: + return + + try: + ol_pydantic = getattr(feast_store.config, "openlineage", None) + if ol_pydantic is None: + return + + from feast.openlineage.config import ( + OpenLineageConfig, + OpenLineageConsumerConfig, + ) + + if hasattr(ol_pydantic, "to_openlineage_config"): + ol_config = ol_pydantic.to_openlineage_config() + elif isinstance(ol_pydantic, dict): + ol_config = OpenLineageConfig.from_dict(ol_pydantic) + elif isinstance(ol_pydantic, OpenLineageConfig): + ol_config = ol_pydantic + else: + consumer_dict = getattr(ol_pydantic, "consumer", None) + if consumer_dict is None: + return + if isinstance(consumer_dict, dict): + consumer = OpenLineageConsumerConfig.from_dict(consumer_dict) + else: + consumer = OpenLineageConsumerConfig( + enabled=getattr(consumer_dict, "enabled", False), + store_type=getattr(consumer_dict, "store_type", "sql"), + connection_string=getattr(consumer_dict, "connection_string", None), + api_key=getattr(consumer_dict, "api_key", None), + namespace_mapping=getattr(consumer_dict, "namespace_mapping", None) + or {}, + ) + ol_config = OpenLineageConfig( + enabled=getattr(ol_pydantic, "enabled", False), + consumer=consumer, + ) + + consumer_config = getattr(ol_config, "consumer", None) + if consumer_config is None or not consumer_config.enabled: + return + + from feast.openlineage.consumer import get_consumer_router + from feast.openlineage.processor import OpenLineageProcessor + from feast.openlineage.store import OpenLineageStore + + if consumer_config.connection_string: + ol_store = OpenLineageStore( + connection_string=consumer_config.connection_string + ) + else: + registry_config = feast_store.config.registry + registry_path = getattr(registry_config, "path", None) + if isinstance(registry_config, dict): + registry_path = registry_config.get("path") + if not registry_path: + logger.warning("No registry path found for OpenLineage consumer store") + return + ol_store = OpenLineageStore(connection_string=registry_path) + + ol_store.initialize() + + import feast.api.registry.rest as _self_module + + _self_module._ol_store_instance = ol_store + _self_module._ol_config = ol_config + + ns_mapping = consumer_config.namespace_mapping + if isinstance(ns_mapping, type(None)): + ns_mapping = {} + + processor = OpenLineageProcessor( + store=ol_store, + namespace_mapping=ns_mapping, + ) + + _self_module._ol_processor = processor + + # Wire the local processor into Feast's own OL emitter so Feast events + # are also stored in the consumer DB automatically. + try: + if feast_store and hasattr(feast_store, "_openlineage_emitter"): + emitter = feast_store._openlineage_emitter + if emitter and hasattr(emitter, "_client") and emitter._client: + emitter._client.set_local_processor(processor) + logger.info("Feast OL emitter wired to local consumer processor") + except Exception as wire_err: + logger.debug(f"Could not wire emitter to local processor: {wire_err}") + + consumer_router = get_consumer_router( + config=ol_config, + store=ol_store, + processor=processor, + ) + + app.include_router(consumer_router) + logger.info("OpenLineage consumer endpoints registered") + + except ImportError as e: + logger.debug(f"OpenLineage consumer not available: {e}") + except Exception as e: + logger.warning(f"Failed to register OpenLineage consumer: {e}") + import traceback + + traceback.print_exc() diff --git a/sdk/python/feast/api/registry/rest/compute_engines.py b/sdk/python/feast/api/registry/rest/compute_engines.py new file mode 100644 index 00000000000..31da2edc234 --- /dev/null +++ b/sdk/python/feast/api/registry/rest/compute_engines.py @@ -0,0 +1,256 @@ +import logging +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, Depends, Query + +from feast.api.registry.rest.rest_utils import ( + get_pagination_params, + get_sorting_params, + grpc_call, +) +from feast.protos.feast.registry import RegistryServer_pb2 +from feast.repo_config import BATCH_ENGINE_CLASS_FOR_TYPE + +logger = logging.getLogger(__name__) + + +def _extract_engine_info(store) -> Dict[str, Any]: + """Extract compute engine configuration from the FeatureStore.""" + config = getattr(store, "config", None) + if config is None: + return { + "engineType": "local", + "engineClass": "LocalComputeEngine", + "config": {"type": "local"}, + } + + batch_engine = getattr(config, "batch_engine", None) + if batch_engine is None: + return { + "engineType": "local", + "engineClass": "LocalComputeEngine", + "config": {"type": "local"}, + } + + if isinstance(batch_engine, dict): + engine_type = batch_engine.get("type", "local") + engine_config = {k: v for k, v in batch_engine.items() if v is not None} + elif hasattr(batch_engine, "type"): + engine_type = str(batch_engine.type) + dump = getattr(batch_engine, "model_dump", None) or getattr( + batch_engine, "dict", None + ) + if dump: + engine_config = {k: v for k, v in dump().items() if v is not None} + else: + engine_config = {"type": engine_type} + else: + engine_type = str(batch_engine) + engine_config = {"type": engine_type} + + engine_class_path = BATCH_ENGINE_CLASS_FOR_TYPE.get(engine_type, engine_type) + engine_class = ( + engine_class_path.rsplit(".", 1)[-1] + if "." in engine_class_path + else engine_class_path + ) + + return { + "engineType": engine_type, + "engineClass": engine_class, + "config": engine_config, + } + + +def _extract_materialization_jobs( + grpc_handler, project: str, allow_cache: bool = True +) -> List[Dict[str, Any]]: + """Extract materialization job info from feature view metadata.""" + req = RegistryServer_pb2.ListFeatureViewsRequest( + project=project, + allow_cache=allow_cache, + ) + response = grpc_call(grpc_handler.ListFeatureViews, req) + feature_views = response.get("featureViews", []) + + jobs: List[Dict[str, Any]] = [] + for fv in feature_views: + spec = fv.get("spec", fv) + meta = fv.get("meta", {}) + fv_name = spec.get("name", "unknown") + intervals = meta.get("materializationIntervals", []) + + for idx, interval in enumerate(intervals): + start_time = interval.get("startTime", interval.get("start_time")) + end_time = interval.get("endTime", interval.get("end_time")) + + jobs.append( + { + "id": f"{fv_name}-{idx}", + "featureView": fv_name, + "status": "SUCCEEDED", + "startTime": start_time, + "endTime": end_time, + } + ) + + jobs.sort(key=lambda j: j.get("startTime", ""), reverse=True) + return jobs + + +def get_compute_engine_router(grpc_handler, store=None) -> APIRouter: + router = APIRouter() + + def _fv_to_info(fv: dict, fv_type: str) -> Dict[str, Any]: + spec = fv.get("spec", fv) + meta = fv.get("meta", {}) + intervals = meta.get("materializationIntervals", []) + batch_engine = spec.get("batchEngine") + + last_materialized = None + if intervals: + last_interval = intervals[-1] + last_materialized = last_interval.get( + "endTime", last_interval.get("end_time") + ) + + return { + "name": spec.get("name"), + "type": fv_type, + "online": spec.get("online", True), + "lastMaterialized": last_materialized, + "hasOverride": batch_engine is not None, + "overrides": batch_engine, + "materializationIntervals": intervals, + } + + @router.get("/compute_engines") + def get_compute_engine( + project: str = Query(...), + allow_cache: bool = Query(default=True), + ): + engine_info = ( + _extract_engine_info(store) + if store + else { + "engineType": "local", + "engineClass": "LocalComputeEngine", + "config": {"type": "local"}, + } + ) + + fv_infos: List[Dict[str, Any]] = [] + + batch_resp = grpc_call( + grpc_handler.ListFeatureViews, + RegistryServer_pb2.ListFeatureViewsRequest( + project=project, allow_cache=allow_cache + ), + ) + for fv in batch_resp.get("featureViews", []): + fv_infos.append(_fv_to_info(fv, "Batch")) + + stream_resp = grpc_call( + grpc_handler.ListStreamFeatureViews, + RegistryServer_pb2.ListStreamFeatureViewsRequest( + project=project, allow_cache=allow_cache + ), + ) + for sfv in stream_resp.get("streamFeatureViews", []): + fv_infos.append(_fv_to_info(sfv, "Stream")) + + engine_info["featureViewCount"] = len(fv_infos) + engine_info["project"] = project + + return { + "engine": engine_info, + "featureViews": fv_infos, + } + + @router.get("/compute_engines/all") + def list_all_compute_engines( + allow_cache: bool = Query(default=True), + page: int = Query(1, ge=1), + limit: int = Query(50, ge=1, le=100), + ): + req = RegistryServer_pb2.ListProjectsRequest(allow_cache=allow_cache) + projects_resp = grpc_call(grpc_handler.ListProjects, req) + projects = projects_resp.get("projects", []) + + engines = [] + for proj in projects: + proj_name = proj.get("spec", {}).get("name") or proj.get("name", "") + if not proj_name: + continue + + engine_info = ( + _extract_engine_info(store) + if store + else { + "engineType": "local", + "engineClass": "LocalComputeEngine", + "config": {"type": "local"}, + "mode": "standalone", + "configSource": "feature_store.yaml", + } + ) + + fv_req = RegistryServer_pb2.ListFeatureViewsRequest( + project=proj_name, + allow_cache=allow_cache, + ) + fv_resp = grpc_call(grpc_handler.ListFeatureViews, fv_req) + fv_count = len(fv_resp.get("featureViews", [])) + + engine_info["project"] = proj_name + engine_info["featureViewCount"] = fv_count + engines.append(engine_info) + + start = (page - 1) * limit + end = start + limit + paginated = engines[start:end] + + return { + "engines": paginated, + "pagination": { + "page": page, + "limit": limit, + "total": len(engines), + }, + } + + @router.get("/materialization_jobs") + def list_materialization_jobs( + project: str = Query(...), + allow_cache: bool = Query(default=True), + status: Optional[str] = Query(None, description="Filter by status"), + feature_view: Optional[str] = Query( + None, alias="feature_view", description="Filter by feature view name" + ), + pagination_params: dict = Depends(get_pagination_params), + sorting_params: dict = Depends(get_sorting_params), + ): + jobs = _extract_materialization_jobs(grpc_handler, project, allow_cache) + + if status: + jobs = [j for j in jobs if j.get("status") == status.upper()] + if feature_view: + jobs = [j for j in jobs if j.get("featureView") == feature_view] + + page = pagination_params.get("page", 1) + limit = pagination_params.get("limit", 50) + start = (page - 1) * limit + end = start + limit + total = len(jobs) + paginated = jobs[start:end] + + return { + "jobs": paginated, + "pagination": { + "page": page, + "limit": limit, + "total": total, + }, + } + + return router diff --git a/sdk/python/feast/api/registry/rest/data_sources.py b/sdk/python/feast/api/registry/rest/data_sources.py index bbc120707fc..ed0f6f983fa 100644 --- a/sdk/python/feast/api/registry/rest/data_sources.py +++ b/sdk/python/feast/api/registry/rest/data_sources.py @@ -1,7 +1,9 @@ import logging -from typing import Dict +from typing import Dict, Optional from fastapi import APIRouter, Depends, Query +from fastapi.responses import JSONResponse +from pydantic import BaseModel from feast.api.registry.rest.codegen_utils import ( render_data_source_code, @@ -19,6 +21,7 @@ grpc_call, parse_tags, ) +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.protos.feast.registry import RegistryServer_pb2 from feast.type_map import _convert_value_type_str_to_value_type from feast.types import from_value_type @@ -26,6 +29,99 @@ logger = logging.getLogger(__name__) +class FileOptionsModel(BaseModel): + uri: str = "" + file_format: Optional[str] = "parquet" + s3_endpoint_override: Optional[str] = "" + + +class BigQueryOptionsModel(BaseModel): + table: str = "" + query: str = "" + + +class SnowflakeOptionsModel(BaseModel): + table: str = "" + database: str = "" + schema_: str = "" + query: Optional[str] = "" + warehouse: Optional[str] = "" + + +class RedshiftOptionsModel(BaseModel): + table: str = "" + database: str = "" + schema_: str = "" + query: Optional[str] = "" + + +class KafkaOptionsModel(BaseModel): + kafka_bootstrap_servers: str = "" + topic: str = "" + message_format: Optional[str] = "json" + watermark_delay_threshold: Optional[str] = "" + + +class SparkOptionsModel(BaseModel): + table: str = "" + path: str = "" + query: Optional[str] = "" + file_format: Optional[str] = "" + table_format: Optional[str] = "" + table_format_catalog: Optional[str] = "" + table_format_namespace: Optional[str] = "" + table_format_properties: Optional[str] = "" + date_partition_column: Optional[str] = "" + date_partition_column_format: Optional[str] = "" + + +class KinesisOptionsModel(BaseModel): + region: str = "" + stream_name: str = "" + record_format: Optional[str] = "json" + + +class TrinoOptionsModel(BaseModel): + table: str = "" + query: str = "" + + +class AthenaOptionsModel(BaseModel): + table: str = "" + query: str = "" + database: str = "" + data_source: Optional[str] = "" + + +class CustomOptionsModel(BaseModel): + configuration: Optional[str] = "" + class_name: Optional[str] = "" + config: Optional[str] = "" + + +class ApplyDataSourceRequestBody(BaseModel): + name: str + project: str + type: Optional[int] = None + timestamp_field: Optional[str] = "" + created_timestamp_column: Optional[str] = "" + date_partition_column: Optional[str] = "" + description: Optional[str] = "" + tags: Optional[Dict[str, str]] = {} + owner: Optional[str] = "" + file_options: Optional[FileOptionsModel] = None + bigquery_options: Optional[BigQueryOptionsModel] = None + snowflake_options: Optional[SnowflakeOptionsModel] = None + redshift_options: Optional[RedshiftOptionsModel] = None + kafka_options: Optional[KafkaOptionsModel] = None + spark_options: Optional[SparkOptionsModel] = None + trino_options: Optional[TrinoOptionsModel] = None + athena_options: Optional[AthenaOptionsModel] = None + kinesis_options: Optional[KinesisOptionsModel] = None + custom_options: Optional[CustomOptionsModel] = None + data_source_class_type: Optional[str] = None + + def get_data_source_router(grpc_handler) -> APIRouter: router = APIRouter() @@ -157,4 +253,108 @@ def get_data_source( result["featureDefinition"] = render_data_source_code(context) return result + @router.post("/data_sources", status_code=201) + def apply_data_source(body: ApplyDataSourceRequestBody): + ds_proto = DataSourceProto( + name=body.name, + timestamp_field=body.timestamp_field or "", + created_timestamp_column=body.created_timestamp_column or "", + description=body.description or "", + tags=body.tags or {}, + owner=body.owner or "", + ) + if body.type is not None: + ds_proto.type = body.type # type: ignore[assignment] + + if body.date_partition_column: + ds_proto.date_partition_column = body.date_partition_column + + if body.file_options: + ds_proto.file_options.uri = body.file_options.uri + if body.file_options.s3_endpoint_override: + ds_proto.file_options.s3_endpoint_override = ( + body.file_options.s3_endpoint_override + ) + elif body.bigquery_options: + ds_proto.bigquery_options.table = body.bigquery_options.table + ds_proto.bigquery_options.query = body.bigquery_options.query + elif body.snowflake_options: + ds_proto.snowflake_options.table = body.snowflake_options.table + ds_proto.snowflake_options.database = body.snowflake_options.database + ds_proto.snowflake_options.schema = body.snowflake_options.schema_ + if body.snowflake_options.query: + ds_proto.snowflake_options.query = body.snowflake_options.query + elif body.redshift_options: + ds_proto.redshift_options.table = body.redshift_options.table + ds_proto.redshift_options.database = body.redshift_options.database + ds_proto.redshift_options.schema = body.redshift_options.schema_ + if body.redshift_options.query: + ds_proto.redshift_options.query = body.redshift_options.query + elif body.kafka_options: + ds_proto.kafka_options.kafka_bootstrap_servers = ( + body.kafka_options.kafka_bootstrap_servers + ) + ds_proto.kafka_options.topic = body.kafka_options.topic + elif body.spark_options: + ds_proto.spark_options.table = body.spark_options.table + ds_proto.spark_options.path = body.spark_options.path + if body.spark_options.query: + ds_proto.spark_options.query = body.spark_options.query + if body.spark_options.file_format: + ds_proto.spark_options.file_format = body.spark_options.file_format + if body.spark_options.date_partition_column_format: + ds_proto.spark_options.date_partition_column_format = ( + body.spark_options.date_partition_column_format + ) + elif body.trino_options: + ds_proto.trino_options.table = body.trino_options.table + ds_proto.trino_options.query = body.trino_options.query + elif body.athena_options: + ds_proto.athena_options.table = body.athena_options.table + ds_proto.athena_options.query = body.athena_options.query + ds_proto.athena_options.database = body.athena_options.database + if body.athena_options.data_source: + ds_proto.athena_options.data_source = body.athena_options.data_source + elif body.kinesis_options: + ds_proto.kinesis_options.region = body.kinesis_options.region + ds_proto.kinesis_options.stream_name = body.kinesis_options.stream_name + elif body.custom_options: + if body.custom_options.configuration: + ds_proto.custom_options.configuration = ( + body.custom_options.configuration.encode("utf-8") + ) + + if body.data_source_class_type: + ds_proto.data_source_class_type = body.data_source_class_type + + req = RegistryServer_pb2.ApplyDataSourceRequest( + data_source=ds_proto, + project=body.project, + commit=True, + ) + grpc_call(grpc_handler.ApplyDataSource, req) + + return JSONResponse( + status_code=201, + content={ + "name": body.name, + "project": body.project, + "status": "applied", + }, + ) + + @router.delete("/data_sources/{name}") + def delete_data_source( + name: str, + project: str = Query(...), + ): + req = RegistryServer_pb2.DeleteDataSourceRequest( + name=name, + project=project, + commit=True, + ) + grpc_call(grpc_handler.DeleteDataSource, req) + + return {"name": name, "project": project, "status": "deleted"} + return router diff --git a/sdk/python/feast/api/registry/rest/entities.py b/sdk/python/feast/api/registry/rest/entities.py index d2943ea74c4..5326433c0aa 100644 --- a/sdk/python/feast/api/registry/rest/entities.py +++ b/sdk/python/feast/api/registry/rest/entities.py @@ -1,6 +1,9 @@ import logging +from typing import Dict, Optional from fastapi import APIRouter, Depends, Query +from fastapi.responses import JSONResponse +from pydantic import BaseModel from feast.api.registry.rest.codegen_utils import render_entity_code from feast.api.registry.rest.rest_utils import ( @@ -13,11 +16,23 @@ get_sorting_params, grpc_call, ) +from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto +from feast.protos.feast.core.Entity_pb2 import EntitySpecV2 as EntitySpecProto from feast.protos.feast.registry import RegistryServer_pb2 logger = logging.getLogger(__name__) +class ApplyEntityRequestBody(BaseModel): + name: str + project: str + join_key: Optional[str] = None + value_type: Optional[int] = 2 + description: Optional[str] = "" + tags: Optional[Dict[str, str]] = {} + owner: Optional[str] = "" + + def get_entity_router(grpc_handler) -> APIRouter: router = APIRouter() @@ -136,4 +151,44 @@ def get_entity( result["featureDefinition"] = render_entity_code(context) return result + @router.post("/entities", status_code=201) + def apply_entity(body: ApplyEntityRequestBody): + join_key = body.join_key if body.join_key else body.name + + spec = EntitySpecProto( + name=body.name, + value_type=body.value_type, + join_key=join_key, + description=body.description or "", + tags=body.tags or {}, + owner=body.owner or "", + ) + entity_proto = EntityProto(spec=spec) + + req = RegistryServer_pb2.ApplyEntityRequest( + entity=entity_proto, + project=body.project, + commit=True, + ) + grpc_call(grpc_handler.ApplyEntity, req) + + return JSONResponse( + status_code=201, + content={"name": body.name, "project": body.project, "status": "applied"}, + ) + + @router.delete("/entities/{name}") + def delete_entity( + name: str, + project: str = Query(...), + ): + req = RegistryServer_pb2.DeleteEntityRequest( + name=name, + project=project, + commit=True, + ) + grpc_call(grpc_handler.DeleteEntity, req) + + return {"name": name, "project": project, "status": "deleted"} + return router diff --git a/sdk/python/feast/api/registry/rest/feature_services.py b/sdk/python/feast/api/registry/rest/feature_services.py index a3e634f3ce3..2486d28ea8d 100644 --- a/sdk/python/feast/api/registry/rest/feature_services.py +++ b/sdk/python/feast/api/registry/rest/feature_services.py @@ -1,6 +1,8 @@ -from typing import Dict +from typing import Dict, List, Optional from fastapi import APIRouter, Depends, Query +from fastapi.responses import JSONResponse +from pydantic import BaseModel from feast.api.registry.rest.codegen_utils import render_feature_service_code from feast.api.registry.rest.rest_utils import ( @@ -14,9 +16,39 @@ grpc_call, parse_tags, ) +from feast.feature_service import FeatureService from feast.protos.feast.registry import RegistryServer_pb2 +class FeatureViewRefModel(BaseModel): + feature_view_name: str + feature_names: Optional[List[str]] = [] + + +class ApplyFeatureServiceRequestBody(BaseModel): + name: str + project: str + features: List[FeatureViewRefModel] + description: Optional[str] = "" + tags: Optional[Dict[str, str]] = {} + owner: Optional[str] = "" + + +def _projection_view_name(projection: dict) -> Optional[str]: + return ( + projection.get("featureViewName") + or projection.get("feature_view_name") + or projection.get("name") + ) + + +def _projection_feature_columns(projection: dict) -> list: + columns = projection.get("featureColumns") + if columns is None: + columns = projection.get("feature_columns") + return columns if isinstance(columns, list) else [] + + def get_feature_service_router(grpc_handler) -> APIRouter: router = APIRouter() @@ -97,9 +129,7 @@ def get_feature_service( project=project, allow_cache=allow_cache, ) - feature_service = grpc_call(grpc_handler.GetFeatureService, req) - - result = feature_service + result = grpc_call(grpc_handler.GetFeatureService, req) if include_relationships: relationships = get_object_relationships( @@ -109,25 +139,33 @@ def get_feature_service( if result: spec = result.get("spec", result) - name = spec.get("name") or result.get("name") or "default_feature_service" + service_name = ( + spec.get("name") or result.get("name") or "default_feature_service" + ) projections = spec.get("features", []) if not isinstance(projections, list): projections = [] features_exprs = [] for proj in projections: - if isinstance(proj, dict): - view_name = proj.get("name") - feature_names = proj.get("features", []) - else: - view_name = str(proj) - feature_names = [] + if not isinstance(proj, dict): + continue + view_name = _projection_view_name(proj) if not view_name: continue + feature_columns = _projection_feature_columns(proj) + feature_names = [ + column.get("name") + for column in feature_columns + if isinstance(column, dict) and column.get("name") + ] + if feature_names: - feature_list = ", ".join([repr(f) for f in feature_names]) + feature_list = ", ".join( + [repr(feature_name) for feature_name in feature_names] + ) features_exprs.append(f"{view_name}[[{feature_list}]]") else: features_exprs.append(view_name) @@ -135,7 +173,7 @@ def get_feature_service( features_str = ", ".join(features_exprs) context = { - "name": name, + "name": service_name, "features": features_str, "tags": spec.get("tags", {}), "description": spec.get("description", ""), @@ -145,4 +183,39 @@ def get_feature_service( result["featureDefinition"] = render_feature_service_code(context) return result + @router.post("/feature_services", status_code=201) + def apply_feature_service(body: ApplyFeatureServiceRequestBody): + req = FeatureService.build_apply_request( + name=body.name, + project=body.project, + feature_view_refs=[ + (feature.feature_view_name, feature.feature_names or None) + for feature in body.features + ], + description=body.description or "", + tags=body.tags or {}, + owner=body.owner or "", + commit=True, + ) + grpc_call(grpc_handler.ApplyFeatureService, req) + + return JSONResponse( + status_code=201, + content={"name": body.name, "project": body.project, "status": "applied"}, + ) + + @router.delete("/feature_services/{name}") + def delete_feature_service( + name: str, + project: str = Query(...), + ): + req = RegistryServer_pb2.DeleteFeatureServiceRequest( + name=name, + project=project, + commit=True, + ) + grpc_call(grpc_handler.DeleteFeatureService, req) + + return {"name": name, "project": project, "status": "deleted"} + return router diff --git a/sdk/python/feast/api/registry/rest/feature_views.py b/sdk/python/feast/api/registry/rest/feature_views.py index 0c921a20870..0e46e921c48 100644 --- a/sdk/python/feast/api/registry/rest/feature_views.py +++ b/sdk/python/feast/api/registry/rest/feature_views.py @@ -1,6 +1,12 @@ -from typing import Dict +import logging +from datetime import timezone +from typing import Dict, List, Optional -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi.responses import JSONResponse +from google.protobuf import timestamp_pb2 +from google.protobuf.duration_pb2 import Duration +from pydantic import BaseModel from feast.api.registry.rest.codegen_utils import render_feature_view_code from feast.api.registry.rest.rest_utils import ( @@ -14,10 +20,35 @@ paginate_and_sort, parse_tags, ) +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto +from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 +from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto +from feast.protos.feast.core.FeatureView_pb2 import FeatureViewSpec from feast.registry_server import RegistryServer_pb2 from feast.type_map import _convert_value_type_str_to_value_type from feast.types import from_value_type +logger = logging.getLogger(__name__) + + +class FeatureModel(BaseModel): + name: str + value_type: int = 2 + description: Optional[str] = "" + + +class ApplyFeatureViewRequestBody(BaseModel): + name: str + project: str + entities: Optional[List[str]] = [] + features: Optional[List[FeatureModel]] = [] + batch_source: Optional[str] = "" + ttl_seconds: Optional[int] = None + online: Optional[bool] = True + description: Optional[str] = "" + tags: Optional[Dict[str, str]] = {} + owner: Optional[str] = "" + def _extract_feature_view_from_any(any_feature_view: dict) -> dict: """Extract the specific feature view type and data from an AnyFeatureView object. @@ -237,10 +268,34 @@ def list_all_feature_views( data_source: str = Query( None, description="Filter feature views by data source name" ), + updated_since: Optional[str] = Query( + None, + description="Only return feature views updated at or after this ISO-8601 UTC timestamp (e.g. 2024-01-01T00:00:00Z)", + ), tags: Dict[str, str] = Depends(parse_tags), pagination_params: dict = Depends(get_pagination_params), sorting_params: dict = Depends(get_sorting_params), ): + updated_since_proto = None + if updated_since is not None: + from datetime import datetime + + try: + dt = datetime.fromisoformat(updated_since.replace("Z", "+00:00")) + except ValueError: + raise HTTPException( + status_code=400, + detail=( + f"Invalid 'updated_since' value '{updated_since}'; expected an " + "ISO-8601 timestamp (e.g. 2024-01-01T00:00:00Z)." + ), + ) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + ts = timestamp_pb2.Timestamp() + ts.FromDatetime(dt.astimezone(timezone.utc)) + updated_since_proto = ts + req = RegistryServer_pb2.ListAllFeatureViewsRequest( project=project, allow_cache=allow_cache, @@ -251,6 +306,7 @@ def list_all_feature_views( data_source=data_source, pagination=create_grpc_pagination_params(pagination_params), sorting=create_grpc_sorting_params(sorting_params), + updated_since=updated_since_proto, ) response = grpc_call(grpc_handler.ListAllFeatureViews, req) any_feature_views = response.get("featureViews", []) @@ -275,4 +331,70 @@ def list_all_feature_views( return result + @router.post("/feature_views", status_code=201) + def apply_feature_view(body: ApplyFeatureViewRequestBody): + feature_specs = [] + for f in body.features or []: + feature_specs.append( + FeatureSpecV2( + name=f.name, + value_type=f.value_type, + description=f.description or "", + ) + ) + + batch_source_proto = ( + DataSourceProto(name=body.batch_source) if body.batch_source else None + ) + + ttl = ( + Duration(seconds=body.ttl_seconds) if body.ttl_seconds is not None else None + ) + + spec = FeatureViewSpec( + name=body.name, + entities=body.entities or [], + features=feature_specs, + tags=body.tags or {}, + online=body.online if body.online is not None else True, + description=body.description or "", + owner=body.owner or "", + ) + if ttl is not None: + spec.ttl.CopyFrom(ttl) + if batch_source_proto: + spec.batch_source.CopyFrom(batch_source_proto) + + fv_proto = FeatureViewProto(spec=spec) + + req = RegistryServer_pb2.ApplyFeatureViewRequest( + feature_view=fv_proto, + project=body.project, + commit=True, + ) + grpc_call(grpc_handler.ApplyFeatureView, req) + + return JSONResponse( + status_code=201, + content={ + "name": body.name, + "project": body.project, + "status": "applied", + }, + ) + + @router.delete("/feature_views/{name}") + def delete_feature_view( + name: str, + project: str = Query(...), + ): + req = RegistryServer_pb2.DeleteFeatureViewRequest( + name=name, + project=project, + commit=True, + ) + grpc_call(grpc_handler.DeleteFeatureView, req) + + return {"name": name, "project": project, "status": "deleted"} + return router diff --git a/sdk/python/feast/api/registry/rest/features.py b/sdk/python/feast/api/registry/rest/features.py index 122b02577b9..f2834e32285 100644 --- a/sdk/python/feast/api/registry/rest/features.py +++ b/sdk/python/feast/api/registry/rest/features.py @@ -38,6 +38,7 @@ def list_features( feature_view=feature_view or "", name=name or "", allow_cache=allow_cache, + kind="feature", pagination=create_grpc_pagination_params(pagination_params), sorting=create_grpc_sorting_params(sorting_params), ) @@ -127,6 +128,76 @@ def list_features_all( limit=limit, sort_by=sort_by, sort_order=sort_order, + extra_request_params={"kind": "feature"}, ) + # --- Labels endpoints (items from LabelViews only) --- + + @router.get("/labels") + def list_labels( + project: str = Query(...), + feature_view: str = Query(None, description="Filter by label view name"), + name: str = Query(None, description="Filter by label name"), + include_relationships: bool = Query( + False, description="Include relationships for each label" + ), + allow_cache: bool = Query(True), + pagination_params: dict = Depends(get_pagination_params), + sorting_params: dict = Depends(get_sorting_params), + ): + req = RegistryServer_pb2.ListFeaturesRequest( + project=project, + feature_view=feature_view or "", + name=name or "", + allow_cache=allow_cache, + kind="label", + pagination=create_grpc_pagination_params(pagination_params), + sorting=create_grpc_sorting_params(sorting_params), + ) + response = grpc_call(grpc_handler.ListFeatures, req) + if "features" not in response: + response["features"] = [] + if "pagination" not in response: + response["pagination"] = {} + + # Rename key for clarity in the response + response["labels"] = response.pop("features") + + if include_relationships: + labels = response.get("labels", []) + relationships = get_relationships_for_objects( + grpc_handler, labels, "feature", project, allow_cache + ) + response["relationships"] = relationships + return response + + @router.get("/labels/all") + def list_labels_all( + page: int = Query(1, ge=1), + limit: int = Query(50, ge=1, le=100), + sort_by: str = Query(None), + sort_order: str = Query("asc"), + include_relationships: bool = Query( + False, description="Include relationships for each label" + ), + allow_cache: bool = Query(True), + ): + result = aggregate_across_projects( + grpc_handler=grpc_handler, + list_method=grpc_handler.ListFeatures, + request_cls=RegistryServer_pb2.ListFeaturesRequest, + response_key="features", + object_type="feature", + include_relationships=include_relationships, + allow_cache=allow_cache, + page=page, + limit=limit, + sort_by=sort_by, + sort_order=sort_order, + extra_request_params={"kind": "label"}, + ) + if "features" in result: + result["labels"] = result.pop("features") + return result + return router diff --git a/sdk/python/feast/api/registry/rest/label_views.py b/sdk/python/feast/api/registry/rest/label_views.py new file mode 100644 index 00000000000..c6aa199e68c --- /dev/null +++ b/sdk/python/feast/api/registry/rest/label_views.py @@ -0,0 +1,137 @@ +from fastapi import APIRouter, Depends, Query + +from feast.api.registry.rest.rest_utils import ( + create_grpc_pagination_params, + create_grpc_sorting_params, + get_object_relationships, + get_pagination_params, + get_relationships_for_objects, + get_sorting_params, + grpc_call, + paginate_and_sort, + parse_tags, +) +from feast.registry_server import RegistryServer_pb2 + + +def get_label_view_router(grpc_handler) -> APIRouter: + router = APIRouter() + + @router.get("/label_views") + def list_label_views( + project: str = Query(...), + allow_cache: bool = Query(default=True), + include_relationships: bool = Query( + False, description="Include relationships for each label view" + ), + tags: dict = Depends(parse_tags), + pagination_params: dict = Depends(get_pagination_params), + sorting_params: dict = Depends(get_sorting_params), + ): + req = RegistryServer_pb2.ListLabelViewsRequest( + project=project, + allow_cache=allow_cache, + tags=tags, + pagination=create_grpc_pagination_params(pagination_params), + sorting=create_grpc_sorting_params(sorting_params), + ) + response = grpc_call(grpc_handler.ListLabelViews, req) + label_views = response.get("labelViews", []) + + for lv in label_views: + lv["type"] = "labelView" + + result = { + "featureViews": label_views, + "pagination": response.get("pagination", {}), + } + + if include_relationships: + relationships = get_relationships_for_objects( + grpc_handler, label_views, "featureView", project, allow_cache + ) + result["relationships"] = relationships + + return result + + @router.get("/label_views/all") + def list_label_views_all( + allow_cache: bool = Query(default=True), + page: int = Query(1, ge=1), + limit: int = Query(50, ge=1, le=100), + sort_by: str = Query(None), + sort_order: str = Query("asc"), + include_relationships: bool = Query( + False, description="Include relationships for each label view" + ), + ): + projects_resp = grpc_call( + grpc_handler.ListProjects, + RegistryServer_pb2.ListProjectsRequest(allow_cache=allow_cache), + ) + projects = projects_resp.get("projects", []) + all_label_views = [] + for project in projects: + project_name = project["spec"]["name"] + req = RegistryServer_pb2.ListLabelViewsRequest( + project=project_name, + allow_cache=allow_cache, + ) + response = grpc_call(grpc_handler.ListLabelViews, req) + label_views = response.get("labelViews", []) + for lv in label_views: + lv["type"] = "labelView" + lv["project"] = project_name + all_label_views.extend(label_views) + + paged_label_views, pagination = paginate_and_sort( + all_label_views, page, limit, sort_by, sort_order + ) + result = { + "featureViews": paged_label_views, + "pagination": pagination, + } + if include_relationships: + relationships_map = {} + for project in projects: + project_name = project["spec"]["name"] + project_lvs = [ + lv for lv in all_label_views if lv.get("project") == project_name + ] + rels = get_relationships_for_objects( + grpc_handler, + project_lvs, + "featureView", + project_name, + allow_cache, + ) + relationships_map.update(rels) + result["relationships"] = relationships_map + return result + + @router.get("/label_views/{name}") + def get_label_view( + name: str, + project: str = Query(...), + include_relationships: bool = Query( + False, description="Include relationships for this label view" + ), + allow_cache: bool = Query(True), + ): + req = RegistryServer_pb2.GetLabelViewRequest( + name=name, + project=project, + allow_cache=allow_cache, + ) + response = grpc_call(grpc_handler.GetLabelView, req) + response["type"] = "labelView" + + if include_relationships: + relationships = get_object_relationships( + grpc_handler, "featureView", name, project, allow_cache + ) + response["relationships"] = relationships + + return response + + return router diff --git a/sdk/python/feast/api/registry/rest/lineage.py b/sdk/python/feast/api/registry/rest/lineage.py index 250498fcf16..360d6210aa6 100644 --- a/sdk/python/feast/api/registry/rest/lineage.py +++ b/sdk/python/feast/api/registry/rest/lineage.py @@ -171,18 +171,21 @@ def get_complete_registry_data( "entities": project_resources.get("entities", []), "dataSources": project_resources.get("dataSources", []), "featureViews": project_resources.get("featureViews", []), + "labelViews": project_resources.get("labelViews", []), "featureServices": project_resources.get("featureServices", []), "features": project_resources.get("features", []), + "labels": project_resources.get("labels", []), }, "relationships": lineage_response.get("relationships", []), "indirectRelationships": lineage_response.get("indirectRelationships", []), "pagination": { - # Get pagination metadata from project_resources if available, otherwise use empty dicts "entities": pagination.get("entities", {}), "dataSources": pagination.get("dataSources", {}), "featureViews": pagination.get("featureViews", {}), + "labelViews": pagination.get("labelViews", {}), "featureServices": pagination.get("featureServices", {}), "features": pagination.get("features", {}), + "labels": pagination.get("labels", {}), "relationships": lineage_response.get("relationshipsPagination", {}), "indirectRelationships": lineage_response.get( "indirectRelationshipsPagination", {} @@ -263,10 +266,14 @@ def get_complete_registry_data_all( ds["project"] = project_name for fv in project_resources.get("featureViews", []): fv["project"] = project_name + for lv in project_resources.get("labelViews", []): + lv["project"] = project_name for fs in project_resources.get("featureServices", []): fs["project"] = project_name for feat in project_resources.get("features", []): feat["project"] = project_name + for lbl in project_resources.get("labels", []): + lbl["project"] = project_name all_data.append( { "project": project_name, @@ -274,8 +281,10 @@ def get_complete_registry_data_all( "entities": project_resources.get("entities", []), "dataSources": project_resources.get("dataSources", []), "featureViews": project_resources.get("featureViews", []), + "labelViews": project_resources.get("labelViews", []), "featureServices": project_resources.get("featureServices", []), "features": project_resources.get("features", []), + "labels": project_resources.get("labels", []), }, "relationships": lineage_response.get("relationships", []), "indirectRelationships": lineage_response.get( diff --git a/sdk/python/feast/api/registry/rest/metrics.py b/sdk/python/feast/api/registry/rest/metrics.py index 58dc7156144..e2249c7a420 100644 --- a/sdk/python/feast/api/registry/rest/metrics.py +++ b/sdk/python/feast/api/registry/rest/metrics.py @@ -11,6 +11,9 @@ grpc_call, paginate_and_sort, ) +from feast.errors import FeastObjectNotFoundException +from feast.permissions.action import AuthzedAction +from feast.permissions.security_manager import assert_permissions from feast.protos.feast.registry import RegistryServer_pb2 @@ -65,77 +68,163 @@ async def resource_counts( ), allow_cache: bool = Query(True), ): - def count_resources_for_project(project_name: str): - entities = grpc_call( - grpc_handler.ListEntities, - RegistryServer_pb2.ListEntitiesRequest( - project=project_name, allow_cache=allow_cache - ), - ) - data_sources = grpc_call( - grpc_handler.ListDataSources, - RegistryServer_pb2.ListDataSourcesRequest( - project=project_name, allow_cache=allow_cache - ), - ) + """ + Resource counts and feature store inventory metadata. + + Returns counts per resource type, plus enriched summaries: + feature services (names), feature views (with per-view feature + count and materialization info), project list, and the registry + last-updated timestamp. + """ + + def get_registry_last_updated() -> Optional[str]: + """Read registry last_updated from in-process registry (not the Proto gRPC RPC).""" try: - saved_datasets = grpc_call( + from google.protobuf.json_format import MessageToDict + + registry = getattr(grpc_handler, "proxied_registry", None) + if registry is None: + return None + registry_proto = registry.proto() + if registry_proto is None or not registry_proto.HasField( + "last_updated" + ): + return None + return MessageToDict(registry_proto).get("lastUpdated") + except Exception: + return None + + def _extract_fv_summary(any_fv: dict, project_name: str) -> Optional[Dict]: + fv = _extract_feature_view_from_any(any_fv) + if not fv: + return None + spec = fv.get("spec", {}) + features = spec.get("features", []) + return { + "name": spec.get("name", ""), + "project": project_name, + "type": fv.get("type", "featureView"), + "featureCount": len(features) if isinstance(features, list) else 0, + } + + def collect_resources_for_project(project_name: str) -> dict: + entities_list: list = [] + try: + entities_resp = grpc_call( + grpc_handler.ListEntities, + RegistryServer_pb2.ListEntitiesRequest( + project=project_name, allow_cache=allow_cache + ), + ) + entities_list = entities_resp.get("entities", []) + except Exception: + pass + + data_sources_list: list = [] + try: + ds_resp = grpc_call( + grpc_handler.ListDataSources, + RegistryServer_pb2.ListDataSourcesRequest( + project=project_name, allow_cache=allow_cache + ), + ) + data_sources_list = ds_resp.get("dataSources", []) + except Exception: + pass + + saved_datasets_list: list = [] + try: + sd_resp = grpc_call( grpc_handler.ListSavedDatasets, RegistryServer_pb2.ListSavedDatasetsRequest( project=project_name, allow_cache=allow_cache ), ) + saved_datasets_list = sd_resp.get("savedDatasets", []) except Exception: - saved_datasets = {"savedDatasets": []} + pass + + features_list: list = [] try: - features = grpc_call( + feat_resp = grpc_call( grpc_handler.ListFeatures, RegistryServer_pb2.ListFeaturesRequest( project=project_name, allow_cache=allow_cache ), ) + features_list = feat_resp.get("features", []) except Exception: - features = {"features": []} + pass + + raw_fv_list: list = [] + fv_summaries: List[Dict] = [] try: - feature_views = grpc_call( + fv_resp = grpc_call( grpc_handler.ListAllFeatureViews, RegistryServer_pb2.ListAllFeatureViewsRequest( project=project_name, allow_cache=allow_cache ), ) + raw_fv_list = fv_resp.get("featureViews", []) + for any_fv in raw_fv_list: + summary = _extract_fv_summary(any_fv, project_name) + if summary: + fv_summaries.append(summary) except Exception: - feature_views = {"featureViews": []} + pass + + fs_summaries: List[Dict] = [] + raw_fs_list: list = [] try: - feature_services = grpc_call( + fs_resp = grpc_call( grpc_handler.ListFeatureServices, RegistryServer_pb2.ListFeatureServicesRequest( project=project_name, allow_cache=allow_cache ), ) + raw_fs_list = fs_resp.get("featureServices", []) + for fs in raw_fs_list: + spec = fs.get("spec", {}) + fs_summaries.append( + {"name": spec.get("name", ""), "project": project_name} + ) except Exception: - feature_services = {"featureServices": []} + pass + return { - "entities": len(entities.get("entities", [])), - "dataSources": len(data_sources.get("dataSources", [])), - "savedDatasets": len(saved_datasets.get("savedDatasets", [])), - "features": len(features.get("features", [])), - "featureViews": len(feature_views.get("featureViews", [])), - "featureServices": len(feature_services.get("featureServices", [])), + "counts": { + "entities": len(entities_list), + "dataSources": len(data_sources_list), + "savedDatasets": len(saved_datasets_list), + "features": len(features_list), + "featureViews": len(fv_summaries), + "featureServices": len(fs_summaries), + }, + "featureServices": fs_summaries, + "featureViews": fv_summaries, } + registry_last_updated = get_registry_last_updated() + if project: - counts = count_resources_for_project(project) - return {"project": project, "counts": counts} + resources = collect_resources_for_project(project) + return { + "project": project, + "counts": resources["counts"], + "featureServices": resources["featureServices"], + "featureViews": resources["featureViews"], + "projects": [{"name": project}], + "registryLastUpdated": registry_last_updated, + } else: - # List all projects via gRPC projects_resp = grpc_call( grpc_handler.ListProjects, RegistryServer_pb2.ListProjectsRequest(allow_cache=allow_cache), ) - all_projects = [ - p["spec"]["name"] for p in projects_resp.get("projects", []) - ] - all_counts = {} + all_projects = projects_resp.get("projects", []) + project_names = [p["spec"]["name"] for p in all_projects if "spec" in p] + + all_counts: Dict[str, dict] = {} total_counts = { "entities": 0, "dataSources": 0, @@ -144,12 +233,37 @@ def count_resources_for_project(project_name: str): "featureViews": 0, "featureServices": 0, } - for project_name in all_projects: - counts = count_resources_for_project(project_name) - all_counts[project_name] = counts + all_fs: List[Dict] = [] + all_fv: List[Dict] = [] + project_summaries: List[Dict] = [] + + for pname in project_names: + resources = collect_resources_for_project(pname) + counts = resources["counts"] + all_counts[pname] = counts for k in total_counts: total_counts[k] += counts[k] - return {"total": total_counts, "perProject": all_counts} + all_fs.extend(resources["featureServices"]) + all_fv.extend(resources["featureViews"]) + proj_info: Dict = next( + (p for p in all_projects if p.get("spec", {}).get("name") == pname), + {}, + ) + project_summaries.append( + { + "name": pname, + "description": proj_info.get("spec", {}).get("description", ""), + } + ) + + return { + "total": total_counts, + "perProject": all_counts, + "featureServices": all_fs, + "featureViews": all_fv, + "projects": project_summaries, + "registryLastUpdated": registry_last_updated, + } @router.get( "/metrics/popular_tags", tags=["Metrics"], response_model=PopularTagsResponse @@ -322,15 +436,21 @@ async def recently_visited( key = f"recently_visited_{user}" visits = [] if project: - try: - visits_json = ( - server.registry.get_project_metadata(project, key) - if server - else None - ) - visits = json.loads(visits_json) if visits_json else [] - except Exception: - visits = [] + if server: + try: + project_obj = server.registry.get_project( + name=project, allow_cache=True + ) + assert_permissions( + resource=project_obj, actions=[AuthzedAction.DESCRIBE] + ) + except FeastObjectNotFoundException: + pass + try: + visits_json = server.registry.get_project_metadata(project, key) + visits = json.loads(visits_json) if visits_json else [] + except Exception: + visits = [] else: try: if server: diff --git a/sdk/python/feast/api/registry/rest/monitoring.py b/sdk/python/feast/api/registry/rest/monitoring.py new file mode 100644 index 00000000000..a10f3ab47f6 --- /dev/null +++ b/sdk/python/feast/api/registry/rest/monitoring.py @@ -0,0 +1,405 @@ +import logging +from datetime import date +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel, Field + +from feast.infra.offline_stores.offline_store import OfflineStore +from feast.permissions.action import AuthzedAction +from feast.permissions.security_manager import assert_permissions + +logger = logging.getLogger(__name__) + +VALID_GRANULARITIES = OfflineStore.MONITORING_VALID_GRANULARITIES + + +class ComputeMetricsRequest(BaseModel): + project: str + feature_view_name: Optional[str] = None + feature_names: Optional[List[str]] = None + start_date: Optional[str] = None + end_date: Optional[str] = None + granularity: str = Field("daily") + set_baseline: bool = False + + +class AutoComputeRequest(BaseModel): + project: str + feature_view_name: Optional[str] = None + + +class ComputeLogMetricsRequest(BaseModel): + project: str + feature_service_name: str + start_date: Optional[str] = None + end_date: Optional[str] = None + granularity: str = Field("daily") + set_baseline: bool = False + + +class AutoComputeLogRequest(BaseModel): + project: str + feature_service_name: Optional[str] = None + + +class ComputeTransientRequest(BaseModel): + project: str + feature_view_name: str + feature_names: Optional[List[str]] = None + start_date: Optional[str] = None + end_date: Optional[str] = None + + +def get_monitoring_router(grpc_handler, store=None): + router = APIRouter() + + _monitoring_service = None + + def _get_monitoring_service(): + nonlocal _monitoring_service + if _monitoring_service is None: + if store is None: + raise HTTPException( + status_code=503, + detail="Monitoring service is not available: no FeatureStore configured", + ) + dqm_config = getattr(store.config, "data_quality_monitoring_config", None) + if dqm_config is None: + raise HTTPException( + status_code=503, + detail="Monitoring is not enabled: add data_quality_monitoring section to feature_store.yaml", + ) + from feast.monitoring.monitoring_service import MonitoringService + + _monitoring_service = MonitoringService(store) + return _monitoring_service + + def _get_store(): + if store is None: + raise HTTPException( + status_code=503, + detail="Monitoring service is not available: no FeatureStore configured", + ) + return store + + # ------------------------------------------------------------------ # + # DQM Job: submit and track + # ------------------------------------------------------------------ # + + @router.post("/monitoring/compute", tags=["Monitoring"]) + async def compute_metrics(request: ComputeMetricsRequest): + """Submit a DQM job to compute and store metrics. Returns job_id. + + When set_baseline is True and no date range is provided, computes + baseline from all available source data. + """ + store = _get_store() + if request.feature_view_name: + fv = store.registry.get_feature_view( + name=request.feature_view_name, project=request.project + ) + assert_permissions(fv, actions=[AuthzedAction.UPDATE]) + + svc = _get_monitoring_service() + + if request.set_baseline and not request.start_date and not request.end_date: + try: + result = svc.compute_baseline( + project=request.project, + feature_view_name=request.feature_view_name, + feature_names=request.feature_names, + ) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + if request.granularity not in VALID_GRANULARITIES: + raise HTTPException( + status_code=400, + detail=f"Invalid granularity '{request.granularity}'. " + f"Must be one of {VALID_GRANULARITIES}", + ) + + params: Dict[str, Any] = {} + if request.start_date: + params["start_date"] = request.start_date + if request.end_date: + params["end_date"] = request.end_date + if request.feature_names: + params["feature_names"] = request.feature_names + params["granularity"] = request.granularity + params["set_baseline"] = request.set_baseline + + job_id = svc.submit_job( + project=request.project, + job_type="compute", + feature_view_name=request.feature_view_name, + parameters=params, + ) + + try: + result = svc.execute_job(job_id) + return {"job_id": job_id, **result} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @router.post("/monitoring/auto_compute", tags=["Monitoring"]) + async def auto_compute(request: AutoComputeRequest): + """Auto-detect date ranges and compute all granularities.""" + store = _get_store() + if request.feature_view_name: + fv = store.registry.get_feature_view( + name=request.feature_view_name, project=request.project + ) + assert_permissions(fv, actions=[AuthzedAction.UPDATE]) + + svc = _get_monitoring_service() + + job_id = svc.submit_job( + project=request.project, + job_type="auto_compute", + feature_view_name=request.feature_view_name, + ) + + try: + result = svc.execute_job(job_id) + return {"job_id": job_id, **result} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + # ------------------------------------------------------------------ # + # Log source: compute from feature serving logs + # ------------------------------------------------------------------ # + + @router.post("/monitoring/compute/log", tags=["Monitoring"]) + async def compute_log_metrics(request: ComputeLogMetricsRequest): + """Compute metrics from feature serving logs for a feature service.""" + if request.granularity not in VALID_GRANULARITIES: + raise HTTPException( + status_code=400, + detail=f"Invalid granularity '{request.granularity}'. " + f"Must be one of {VALID_GRANULARITIES}", + ) + + store = _get_store() + fs = store.registry.get_feature_service( + name=request.feature_service_name, project=request.project + ) + assert_permissions(fs, actions=[AuthzedAction.UPDATE]) + + svc = _get_monitoring_service() + + start_d = date.fromisoformat(request.start_date) if request.start_date else None + end_d = date.fromisoformat(request.end_date) if request.end_date else None + + try: + result = svc.compute_log_metrics( + project=request.project, + feature_service_name=request.feature_service_name, + start_date=start_d, + end_date=end_d, + granularity=request.granularity, + set_baseline=request.set_baseline, + ) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @router.post("/monitoring/auto_compute/log", tags=["Monitoring"]) + async def auto_compute_log(request: AutoComputeLogRequest): + """Auto-detect date ranges from log data and compute all granularities.""" + store = _get_store() + if request.feature_service_name: + fs = store.registry.get_feature_service( + name=request.feature_service_name, project=request.project + ) + assert_permissions(fs, actions=[AuthzedAction.UPDATE]) + + svc = _get_monitoring_service() + try: + result = svc.auto_compute_log_metrics( + project=request.project, + feature_service_name=request.feature_service_name, + ) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @router.get("/monitoring/jobs/{job_id}", tags=["Monitoring"]) + async def get_job_status(job_id: str): + svc = _get_monitoring_service() + job = svc.get_job(job_id) + if job is None: + raise HTTPException(status_code=404, detail=f"Job '{job_id}' not found") + return job + + # ------------------------------------------------------------------ # + # Transient compute (not stored) + # ------------------------------------------------------------------ # + + @router.post("/monitoring/compute/transient", tags=["Monitoring"]) + async def compute_transient(request: ComputeTransientRequest): + """Compute metrics on-the-fly for an arbitrary date range. Results are + returned directly and NOT persisted to the monitoring tables.""" + store = _get_store() + fv = store.registry.get_feature_view( + name=request.feature_view_name, project=request.project + ) + assert_permissions(fv, actions=[AuthzedAction.DESCRIBE]) + + svc = _get_monitoring_service() + + start_d = date.fromisoformat(request.start_date) if request.start_date else None + end_d = date.fromisoformat(request.end_date) if request.end_date else None + + result = svc.compute_transient( + project=request.project, + feature_view_name=request.feature_view_name, + feature_names=request.feature_names, + start_date=start_d, + end_date=end_d, + ) + return result + + # ------------------------------------------------------------------ # + # Read endpoints + # ------------------------------------------------------------------ # + + @router.get("/monitoring/metrics/features", tags=["Monitoring"]) + async def get_feature_metrics( + project: str = Query(...), + feature_view_name: Optional[str] = Query(None), + feature_name: Optional[str] = Query(None), + feature_service_name: Optional[str] = Query(None), + granularity: Optional[str] = Query(None), + data_source_type: Optional[str] = Query(None), + start_date: Optional[str] = Query(None), + end_date: Optional[str] = Query(None), + ): + store = _get_store() + if feature_view_name: + fv = store.registry.get_feature_view( + name=feature_view_name, project=project + ) + assert_permissions(fv, actions=[AuthzedAction.DESCRIBE]) + + svc = _get_monitoring_service() + return svc.get_feature_metrics( + project=project, + feature_service_name=feature_service_name, + feature_view_name=feature_view_name, + feature_name=feature_name, + granularity=granularity, + data_source_type=data_source_type, + start_date=date.fromisoformat(start_date) if start_date else None, + end_date=date.fromisoformat(end_date) if end_date else None, + ) + + @router.get("/monitoring/metrics/feature_views", tags=["Monitoring"]) + async def get_feature_view_metrics( + project: str = Query(...), + feature_view_name: Optional[str] = Query(None), + feature_service_name: Optional[str] = Query(None), + granularity: Optional[str] = Query(None), + data_source_type: Optional[str] = Query(None), + start_date: Optional[str] = Query(None), + end_date: Optional[str] = Query(None), + ): + store = _get_store() + if feature_view_name: + fv = store.registry.get_feature_view( + name=feature_view_name, project=project + ) + assert_permissions(fv, actions=[AuthzedAction.DESCRIBE]) + + svc = _get_monitoring_service() + return svc.get_feature_view_metrics( + project=project, + feature_service_name=feature_service_name, + feature_view_name=feature_view_name, + granularity=granularity, + data_source_type=data_source_type, + start_date=date.fromisoformat(start_date) if start_date else None, + end_date=date.fromisoformat(end_date) if end_date else None, + ) + + @router.get("/monitoring/metrics/feature_services", tags=["Monitoring"]) + async def get_feature_service_metrics( + project: str = Query(...), + feature_service_name: Optional[str] = Query(None), + granularity: Optional[str] = Query(None), + data_source_type: Optional[str] = Query(None), + start_date: Optional[str] = Query(None), + end_date: Optional[str] = Query(None), + ): + store = _get_store() + if feature_service_name: + fs = store.registry.get_feature_service( + name=feature_service_name, project=project + ) + assert_permissions(fs, actions=[AuthzedAction.DESCRIBE]) + + svc = _get_monitoring_service() + return svc.get_feature_service_metrics( + project=project, + feature_service_name=feature_service_name, + granularity=granularity, + data_source_type=data_source_type, + start_date=date.fromisoformat(start_date) if start_date else None, + end_date=date.fromisoformat(end_date) if end_date else None, + ) + + @router.get("/monitoring/metrics/baseline", tags=["Monitoring"]) + async def get_baseline( + project: str = Query(...), + feature_view_name: Optional[str] = Query(None), + feature_name: Optional[str] = Query(None), + data_source_type: Optional[str] = Query(None), + ): + store = _get_store() + if feature_view_name: + fv = store.registry.get_feature_view( + name=feature_view_name, project=project + ) + assert_permissions(fv, actions=[AuthzedAction.DESCRIBE]) + + svc = _get_monitoring_service() + return svc.get_baseline( + project=project, + feature_view_name=feature_view_name, + feature_name=feature_name, + data_source_type=data_source_type, + ) + + @router.get("/monitoring/metrics/timeseries", tags=["Monitoring"]) + async def get_timeseries( + project: str = Query(...), + feature_view_name: Optional[str] = Query(None), + feature_name: Optional[str] = Query(None), + feature_service_name: Optional[str] = Query(None), + granularity: Optional[str] = Query(None), + data_source_type: Optional[str] = Query(None), + start_date: Optional[str] = Query(None), + end_date: Optional[str] = Query(None), + ): + store = _get_store() + if feature_view_name: + fv = store.registry.get_feature_view( + name=feature_view_name, project=project + ) + assert_permissions(fv, actions=[AuthzedAction.DESCRIBE]) + + svc = _get_monitoring_service() + return svc.get_timeseries( + project=project, + feature_view_name=feature_view_name, + feature_name=feature_name, + feature_service_name=feature_service_name, + granularity=granularity, + data_source_type=data_source_type, + start_date=date.fromisoformat(start_date) if start_date else None, + end_date=date.fromisoformat(end_date) if end_date else None, + ) + + return router diff --git a/sdk/python/feast/api/registry/rest/permissions.py b/sdk/python/feast/api/registry/rest/permissions.py index b7735700833..15599a6ec9a 100644 --- a/sdk/python/feast/api/registry/rest/permissions.py +++ b/sdk/python/feast/api/registry/rest/permissions.py @@ -1,4 +1,8 @@ +from typing import Dict, List, Optional + from fastapi import APIRouter, Depends, Query +from fastapi.responses import JSONResponse +from pydantic import BaseModel from feast.api.registry.rest.rest_utils import ( create_grpc_pagination_params, @@ -9,7 +13,107 @@ get_sorting_params, grpc_call, ) -from feast.registry_server import RegistryServer_pb2 +from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto +from feast.protos.feast.core.Permission_pb2 import PermissionSpec as PermissionSpecProto +from feast.protos.feast.core.Policy_pb2 import ( + CombinedGroupNamespacePolicy as CombinedGroupNamespacePolicyProto, +) +from feast.protos.feast.core.Policy_pb2 import GroupBasedPolicy as GroupBasedPolicyProto +from feast.protos.feast.core.Policy_pb2 import ( + NamespaceBasedPolicy as NamespaceBasedPolicyProto, +) +from feast.protos.feast.core.Policy_pb2 import Policy as PolicyProto +from feast.protos.feast.core.Policy_pb2 import RoleBasedPolicy as RoleBasedPolicyProto +from feast.protos.feast.registry import RegistryServer_pb2 + + +class RoleBasedPolicyModel(BaseModel): + roles: List[str] + + +class GroupBasedPolicyModel(BaseModel): + groups: List[str] + + +class NamespaceBasedPolicyModel(BaseModel): + namespaces: List[str] + + +class CombinedGroupNamespacePolicyModel(BaseModel): + groups: List[str] + namespaces: List[str] + + +class PolicyModel(BaseModel): + role_based_policy: Optional[RoleBasedPolicyModel] = None + group_based_policy: Optional[GroupBasedPolicyModel] = None + namespace_based_policy: Optional[NamespaceBasedPolicyModel] = None + combined_group_namespace_policy: Optional[CombinedGroupNamespacePolicyModel] = None + + +class ApplyPermissionRequestBody(BaseModel): + name: str + project: str + types: List[str] = [] + name_patterns: List[str] = [] + actions: List[str] = [] + policy: PolicyModel + tags: Optional[Dict[str, str]] = {} + required_tags: Optional[Dict[str, str]] = {} + + +def _build_policy_proto(policy: PolicyModel) -> PolicyProto: + if policy.role_based_policy: + return PolicyProto( + role_based_policy=RoleBasedPolicyProto(roles=policy.role_based_policy.roles) + ) + elif policy.group_based_policy: + return PolicyProto( + group_based_policy=GroupBasedPolicyProto( + groups=policy.group_based_policy.groups + ) + ) + elif policy.namespace_based_policy: + return PolicyProto( + namespace_based_policy=NamespaceBasedPolicyProto( + namespaces=policy.namespace_based_policy.namespaces + ) + ) + elif policy.combined_group_namespace_policy: + return PolicyProto( + combined_group_namespace_policy=CombinedGroupNamespacePolicyProto( + groups=policy.combined_group_namespace_policy.groups, + namespaces=policy.combined_group_namespace_policy.namespaces, + ) + ) + return PolicyProto() + + +_TYPE_NAME_TO_ENUM = { + "FEATURE_VIEW": PermissionSpecProto.Type.FEATURE_VIEW, + "ON_DEMAND_FEATURE_VIEW": PermissionSpecProto.Type.ON_DEMAND_FEATURE_VIEW, + "BATCH_FEATURE_VIEW": PermissionSpecProto.Type.BATCH_FEATURE_VIEW, + "STREAM_FEATURE_VIEW": PermissionSpecProto.Type.STREAM_FEATURE_VIEW, + "ENTITY": PermissionSpecProto.Type.ENTITY, + "FEATURE_SERVICE": PermissionSpecProto.Type.FEATURE_SERVICE, + "DATA_SOURCE": PermissionSpecProto.Type.DATA_SOURCE, + "VALIDATION_REFERENCE": PermissionSpecProto.Type.VALIDATION_REFERENCE, + "SAVED_DATASET": PermissionSpecProto.Type.SAVED_DATASET, + "PERMISSION": PermissionSpecProto.Type.PERMISSION, + "PROJECT": PermissionSpecProto.Type.PROJECT, + "LABEL_VIEW": PermissionSpecProto.Type.LABEL_VIEW, +} + +_ACTION_NAME_TO_ENUM = { + "CREATE": PermissionSpecProto.AuthzedAction.CREATE, + "DESCRIBE": PermissionSpecProto.AuthzedAction.DESCRIBE, + "UPDATE": PermissionSpecProto.AuthzedAction.UPDATE, + "DELETE": PermissionSpecProto.AuthzedAction.DELETE, + "READ_ONLINE": PermissionSpecProto.AuthzedAction.READ_ONLINE, + "READ_OFFLINE": PermissionSpecProto.AuthzedAction.READ_OFFLINE, + "WRITE_ONLINE": PermissionSpecProto.AuthzedAction.WRITE_ONLINE, + "WRITE_OFFLINE": PermissionSpecProto.AuthzedAction.WRITE_OFFLINE, +} def get_permission_router(grpc_handler) -> APIRouter: @@ -33,8 +137,6 @@ def get_permission( result = permission - # Note: permissions may not have relationships in the traditional sense - # but we include the functionality for consistency if include_relationships: relationships = get_object_relationships( grpc_handler, "permission", name, project, allow_cache @@ -75,4 +177,53 @@ def list_permissions( return result + @router.post("/permissions", status_code=201) + def apply_permission(body: ApplyPermissionRequestBody): + types = [_TYPE_NAME_TO_ENUM[t] for t in body.types if t in _TYPE_NAME_TO_ENUM] + actions = [ + _ACTION_NAME_TO_ENUM[a] for a in body.actions if a in _ACTION_NAME_TO_ENUM + ] + policy_proto = _build_policy_proto(body.policy) + + permission_spec = PermissionSpecProto( + name=body.name, + types=types, + name_patterns=body.name_patterns, + actions=actions, + policy=policy_proto, + tags=body.tags or {}, + required_tags=body.required_tags or {}, + ) + permission_proto = PermissionProto(spec=permission_spec) + + req = RegistryServer_pb2.ApplyPermissionRequest( + permission=permission_proto, + project=body.project, + commit=True, + ) + grpc_call(grpc_handler.ApplyPermission, req) + + return JSONResponse( + status_code=201, + content={ + "name": body.name, + "project": body.project, + "status": "applied", + }, + ) + + @router.delete("/permissions/{name}") + def delete_permission( + name: str, + project: str = Query(...), + ): + req = RegistryServer_pb2.DeletePermissionRequest( + name=name, + project=project, + commit=True, + ) + grpc_call(grpc_handler.DeletePermission, req) + + return {"name": name, "project": project, "status": "deleted"} + return router diff --git a/sdk/python/feast/api/registry/rest/projects.py b/sdk/python/feast/api/registry/rest/projects.py index 659fb22dc8f..2047c8717c9 100644 --- a/sdk/python/feast/api/registry/rest/projects.py +++ b/sdk/python/feast/api/registry/rest/projects.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Query, Response, status from feast.api.registry.rest.rest_utils import ( get_pagination_params, @@ -50,3 +50,19 @@ def list_projects( } return router + + +def get_registry_router(store) -> APIRouter: + router = APIRouter() + + @router.post("/registry/refresh") + def refresh_registry(): + from feast.permissions.action import AuthzedAction + from feast.permissions.security_manager import assert_permissions + + project = store.registry.get_project(name=store.project, allow_cache=True) + assert_permissions(resource=project, actions=[AuthzedAction.UPDATE]) + store.refresh_registry() + return Response(status_code=status.HTTP_200_OK) + + return router diff --git a/sdk/python/feast/api/registry/rest/rest_registry_server.py b/sdk/python/feast/api/registry/rest/rest_registry_server.py index d67321abc48..8dd3415cd70 100644 --- a/sdk/python/feast/api/registry/rest/rest_registry_server.py +++ b/sdk/python/feast/api/registry/rest/rest_registry_server.py @@ -33,7 +33,7 @@ class RestRegistryServer: def __init__(self, store: FeatureStore): self.store = store self.registry = store.registry - self.grpc_handler = RegistryServer(self.registry) + self.grpc_handler = RegistryServer(self.registry, store=self.store) recent_visit_logging_cfg = {} feature_server_cfg = getattr( getattr(store, "config", None), "feature_server", None @@ -76,6 +76,30 @@ def __init__(self, store: FeatureStore): self._init_auth() self._register_routes() + registry_cfg = getattr(store.config, "registry", None) + mcp_cfg = getattr(registry_cfg, "mcp", None) + if mcp_cfg and getattr(mcp_cfg, "enabled", False): + try: + from fastapi_mcp import FastApiMCP + + from feast.infra.mcp_servers.mcp_server import ( + _patch_fastapi_mcp_schema_resolver, + ) + + _patch_fastapi_mcp_schema_resolver() + mcp = FastApiMCP(self.app, name="feast-registry-mcp") + mount_sse = getattr(mcp, "mount_sse", None) + if mount_sse is not None: + mount_sse() + else: + mcp.mount() + logger.info("MCP support enabled on REST registry server") + except ImportError: + logger.warning( + "MCP support requested but fastapi_mcp is not installed. " + "Install it with: pip install feast[mcp]" + ) + def _add_exception_handlers(self): """Add custom exception handlers to include HTTP status codes in JSON responses.""" @@ -220,28 +244,35 @@ async def dispatch(self, request: Request, call_next): else: object_type = None object_name = None - visit = { - "path": path, - "timestamp": _utc_now().isoformat(), - "project": project, - "user": user, - "object": object_type, - "object_name": object_name, - "method": method, - } - try: - visits_json = self.registry.get_project_metadata(project, key) - visits = json.loads(visits_json) if visits_json else [] - except Exception: - visits = [] - visits.append(visit) - visits = visits[-self.recent_visits_limit :] - try: - self.registry.set_project_metadata( - project, key, json.dumps(visits) - ) - except Exception as e: - logger.warning(f"Failed to persist recent visits: {e}") + + response = await call_next(request) + + if response.status_code < 400: + visit = { + "path": path, + "timestamp": _utc_now().isoformat(), + "project": project, + "user": user, + "object": object_type, + "object_name": object_name, + "method": method, + } + try: + visits_json = self.registry.get_project_metadata( + project, key + ) + visits = json.loads(visits_json) if visits_json else [] + except Exception: + visits = [] + visits.append(visit) + visits = visits[-self.recent_visits_limit :] + try: + self.registry.set_project_metadata( + project, key, json.dumps(visits) + ) + except Exception as e: + logger.warning(f"Failed to persist recent visits: {e}") + return response response = await call_next(request) return response @@ -296,6 +327,10 @@ def start_server( ): import uvicorn + from feast.registry_server import _sync_protected_project_tag + + _sync_protected_project_tag(self.store) + if tls_key_path and tls_cert_path: logger.info("Starting REST registry server in TLS(SSL) mode") logger.info(f"REST registry server listening on https://localhost:{port}") diff --git a/sdk/python/feast/api/registry/rest/rest_utils.py b/sdk/python/feast/api/registry/rest/rest_utils.py index 765e1bbeb8b..233ec07ac38 100644 --- a/sdk/python/feast/api/registry/rest/rest_utils.py +++ b/sdk/python/feast/api/registry/rest/rest_utils.py @@ -9,6 +9,7 @@ FeastPermissionError, PushSourceNotFoundException, ) +from feast.permissions.security_manager import get_security_manager from feast.protos.feast.registry import RegistryServer_pb2 logger = logging.getLogger(__name__) @@ -18,13 +19,22 @@ MATCH_SCORE_NAME = 100 MATCH_SCORE_DESCRIPTION = 80 MATCH_SCORE_TAGS = 60 -MATCH_SCORE_PARTIAL = 40 def grpc_call(handler_fn, request): """ Wrapper to invoke gRPC method with context=None and handle common errors. + + Sets SecurityManager's current project from ``request.project`` so REST + handlers (which call RegistryServer methods in-process, not through the + gRPC AuthInterceptor) authorize against the target project's permissions. """ + sm = get_security_manager() + project_token = None + if sm is not None: + project_token = sm.set_current_project( + getattr(request, "project", None) or None + ) try: response = handler_fn(request, context=None) return MessageToDict(response) @@ -36,6 +46,9 @@ def grpc_call(handler_fn, request): raise except Exception as e: raise e + finally: + if sm is not None and project_token is not None: + sm.reset_current_project(project_token) def get_object_relationships( @@ -131,6 +144,7 @@ def aggregate_across_projects( sort_by: Optional[str] = None, sort_order: str = "asc", include_relationships: bool = False, + extra_request_params: Optional[Dict] = None, ) -> Dict: """ Fetches and aggregates objects across all projects, adds project field, handles relationships, and paginates/sorts. @@ -148,6 +162,7 @@ def aggregate_across_projects( req = request_cls( project=project_name, allow_cache=allow_cache, + **(extra_request_params or {}), ) response = grpc_call(list_method, req) objects = response.get(response_key, []) @@ -435,9 +450,11 @@ def get_all_project_resources( "entities": [], "dataSources": [], "featureViews": [], + "labelViews": [], "featureServices": [], "savedDatasets": [], "features": [], + "labels": [], "pagination": {}, "errors": [], } @@ -485,6 +502,18 @@ def get_all_project_resources( if err_msg: errors.append(err_msg) + # Get label views + resources["labelViews"], pagination["labelViews"], err_msg = list_label_views( + grpc_handler=grpc_handler, + project=project, + allow_cache=allow_cache, + tags=tags, + pagination_params=pagination_params, + sorting_params=sorting_params, + ) + if err_msg: + errors.append(err_msg) + # Get feature services resources["featureServices"], pagination["featureServices"], err_msg = ( list_feature_services( @@ -524,6 +553,17 @@ def get_all_project_resources( if err_msg: errors.append(err_msg) + # Get labels + resources["labels"], pagination["labels"], err_msg = list_labels( + grpc_handler=grpc_handler, + project=project, + allow_cache=allow_cache, + pagination_params=pagination_params, + sorting_params=sorting_params, + ) + if err_msg: + errors.append(err_msg) + except Exception as e: err_msg = f"Error getting resources for project '{project}'" errors.append(err_msg) @@ -537,12 +577,42 @@ def filter_search_results_and_match_score( ) -> List[Dict]: """Filter search results based on query string""" if not query: + # Add all tags as matched_tags when no query (all tags match) + for result in results: + result["matched_tags"] = result.get("tags", {}) return results query_lower = query.lower() filtered_results = [] for result in results: + matched_tags = {} + best_fuzzy_tag_score = 0.0 + + # Collect all matching tags (exact and fuzzy) upfront + tags = result.get("tags", {}) + has_exact_tag_match = False + + for key, value in tags.items(): + key_lower = str(key).lower() + value_str = str(value).lower() + tag_combined = f"{key_lower}={value_str}" + + # Exact match in key or value + if query_lower in tag_combined: + has_exact_tag_match = True + matched_tags[key] = value + else: + # Fuzzy match for tags (on combined "key:value" string) + tag_fuzzy_score = fuzzy_match(query_lower, tag_combined) + + if tag_fuzzy_score >= MATCH_SCORE_DEFAULT_THRESHOLD: + matched_tags[key] = value + if tag_fuzzy_score > best_fuzzy_tag_score: + best_fuzzy_tag_score = tag_fuzzy_score + + result["matched_tags"] = matched_tags + # Search in name if query_lower in result.get("name", "").lower(): result["match_score"] = MATCH_SCORE_NAME @@ -555,19 +625,18 @@ def filter_search_results_and_match_score( filtered_results.append(result) continue - # Search in tags - tags = result.get("tags", {}) - tag_match = False - for key, value in tags.items(): - if query_lower in key.lower() or query_lower in str(value).lower(): - tag_match = True - break - - if tag_match: + # Exact tag match + if has_exact_tag_match: result["match_score"] = MATCH_SCORE_TAGS filtered_results.append(result) continue + # Fuzzy tag match + if best_fuzzy_tag_score >= MATCH_SCORE_DEFAULT_THRESHOLD: + result["match_score"] = best_fuzzy_tag_score * 100 + filtered_results.append(result) + continue + # Partial name match (fuzzy search) fuzzy_match_score = fuzzy_match(query_lower, result.get("name", "").lower()) if fuzzy_match_score >= MATCH_SCORE_DEFAULT_THRESHOLD: @@ -856,6 +925,87 @@ def list_features( return features, pagination, err_msg +def list_label_views( + grpc_handler, + project: str, + allow_cache: bool, + tags: Optional[Dict[str, str]] = None, + pagination_params: Optional[dict] = None, + sorting_params: Optional[dict] = None, +) -> tuple[List[Dict[str, Any]], Dict[str, Any], str]: + """List label views in a project with optional sorting and pagination""" + label_views: List[Dict[str, Any]] = [] + pagination: Dict[str, Any] = {} + err_msg = "" + + grpc_pagination = None + grpc_sorting = None + + if pagination_params: + grpc_pagination = create_grpc_pagination_params(pagination_params) + if sorting_params: + grpc_sorting = create_grpc_sorting_params(sorting_params) + + try: + req = RegistryServer_pb2.ListLabelViewsRequest( + project=project, + allow_cache=allow_cache, + pagination=grpc_pagination, + sorting=grpc_sorting, + tags=tags, + ) + response = grpc_call(grpc_handler.ListLabelViews, req) + label_views, pagination = ( + response.get("labelViews", []), + response.get("pagination", {}), + ) + except Exception as e: + err_msg = f"Error searching label views in project '{project}'" + logger.error(f"{err_msg}: {e}") + finally: + return label_views, pagination, err_msg + + +def list_labels( + grpc_handler, + project: str, + allow_cache: bool, + pagination_params: Optional[dict] = None, + sorting_params: Optional[dict] = None, +) -> tuple[List[Dict[str, Any]], Dict[str, Any], str]: + """List labels in a project with optional sorting and pagination""" + labels: List[Dict[str, Any]] = [] + pagination: Dict[str, Any] = {} + err_msg = "" + + grpc_pagination = None + grpc_sorting = None + + if pagination_params: + grpc_pagination = create_grpc_pagination_params(pagination_params) + if sorting_params: + grpc_sorting = create_grpc_sorting_params(sorting_params) + + try: + req = RegistryServer_pb2.ListFeaturesRequest( + project=project, + allow_cache=allow_cache, + pagination=grpc_pagination, + sorting=grpc_sorting, + kind="label", + ) + response = grpc_call(grpc_handler.ListFeatures, req) + labels, pagination = ( + response.get("features", []), + response.get("pagination", {}), + ) + except Exception as e: + err_msg = f"Error searching labels in project '{project}'" + logger.error(f"{err_msg}: {e}") + finally: + return labels, pagination, err_msg + + def list_all_projects( grpc_handler, allow_cache: bool, diff --git a/sdk/python/feast/api/registry/rest/saved_datasets.py b/sdk/python/feast/api/registry/rest/saved_datasets.py index 972988d7877..feb3e999d30 100644 --- a/sdk/python/feast/api/registry/rest/saved_datasets.py +++ b/sdk/python/feast/api/registry/rest/saved_datasets.py @@ -1,6 +1,9 @@ -from typing import Dict +import json +import logging +from typing import Dict, List, Optional -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Body, Depends, HTTPException, Query +from pydantic import BaseModel from feast.api.registry.rest.codegen_utils import render_saved_dataset_code from feast.api.registry.rest.rest_utils import ( @@ -14,8 +17,103 @@ grpc_call, parse_tags, ) +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto +from feast.protos.feast.core.SavedDataset_pb2 import ( + SavedDataset as SavedDatasetProto, +) +from feast.protos.feast.core.SavedDataset_pb2 import ( + SavedDatasetMeta, + SavedDatasetSpec, +) +from feast.protos.feast.core.SavedDataset_pb2 import ( + SavedDatasetStorage as SavedDatasetStorageProto, +) from feast.protos.feast.registry import RegistryServer_pb2 +logger = logging.getLogger(__name__) + + +def _build_feature_view_to_datasource_map( + grpc_handler, project: str, allow_cache: bool = True +) -> Dict[str, str]: + """Build a map of feature_view_name → data_source_name for a project.""" + try: + fv_req = RegistryServer_pb2.ListAllFeatureViewsRequest( + project=project, allow_cache=allow_cache + ) + fv_response = grpc_call(grpc_handler.ListAllFeatureViews, fv_req) + feature_views = fv_response.get("featureViews", []) + except Exception: + return {} + + fv_ds_map: Dict[str, str] = {} + for any_fv in feature_views: + # AnyFeatureView wraps under "featureView", "onDemandFeatureView", etc. + fv_data = any_fv.get("featureView") or any_fv.get("streamFeatureView") or {} + spec = fv_data.get("spec", {}) + fv_name = spec.get("name", "") + if not fv_name: + continue + batch_source = spec.get("batchSource", {}) + ds_name = batch_source.get("name", "") if batch_source else "" + if ds_name: + fv_ds_map[fv_name] = ds_name + return fv_ds_map + + +def _get_dataset_data_sources(dataset: dict, fv_ds_map: Dict[str, str]) -> List[str]: + """Get data source names for a dataset via its features' feature view lineage.""" + spec = dataset.get("spec", dataset) + features = spec.get("features", []) + data_sources: List[str] = [] + seen: set = set() + for feature_ref in features: + if isinstance(feature_ref, str) and ":" in feature_ref: + fv_name = feature_ref.split(":")[0] + ds_name = fv_ds_map.get(fv_name) + if ds_name and ds_name not in seen: + seen.add(ds_name) + data_sources.append(ds_name) + return data_sources + + +class RegisterDatasetRequest(BaseModel): + name: str + project: str + features: List[str] = [] + join_keys: List[str] = [] + storage_path: Optional[str] = None + storage_type: str = "file" + storage_file_format: Optional[str] = "parquet" + tags: Dict[str, str] = {} + full_feature_names: bool = False + feature_service_name: Optional[str] = None + allow_override: bool = False + namespace: str = "" + collection: str = "" + description: str = "" + + +class CreateDatasetRequest(BaseModel): + name: str + project: str + feature_service_name: Optional[str] = None + features: Optional[List[str]] = None + entity_source_type: str = "inline" + entity_source_path: Optional[str] = None + # Inline entity definition + entity_keys: Optional[List[str]] = None + entity_values: Optional[str] = None + start_date: Optional[str] = None + end_date: Optional[str] = None + extra_columns: Optional[str] = None + # Storage + storage_type: str = "file" + storage_path: str = "" + storage_file_format: Optional[str] = "parquet" + tags: Dict[str, str] = {} + allow_overwrite: bool = False + def get_saved_dataset_router(grpc_handler) -> APIRouter: router = APIRouter() @@ -27,11 +125,23 @@ def list_saved_datasets_all( limit: int = Query(50, ge=1, le=100), sort_by: str = Query(None), sort_order: str = Query("asc"), + namespace: Optional[str] = Query( + None, description="Filter by namespace (logical grouping)" + ), + collection: Optional[str] = Query( + None, description="Filter by collection (sub-grouping within namespace)" + ), include_relationships: bool = Query( False, description="Include relationships for each saved dataset" ), ): - return aggregate_across_projects( + extra_params = {} + if namespace is not None: + extra_params["namespace"] = namespace + if collection is not None: + extra_params["collection"] = collection + + result = aggregate_across_projects( grpc_handler=grpc_handler, list_method=grpc_handler.ListSavedDatasets, request_cls=RegistryServer_pb2.ListSavedDatasetsRequest, @@ -43,8 +153,106 @@ def list_saved_datasets_all( sort_by=sort_by, sort_order=sort_order, include_relationships=include_relationships, + extra_request_params=extra_params or None, ) + saved_datasets = result.get("savedDatasets", []) + projects_seen: Dict[str, Dict[str, str]] = {} + for sd in saved_datasets: + proj = sd.get("project", "") + if proj not in projects_seen: + projects_seen[proj] = _build_feature_view_to_datasource_map( + grpc_handler, proj, allow_cache + ) + ds_names = _get_dataset_data_sources(sd, projects_seen[proj]) + if ds_names: + spec = sd.get("spec", sd) + spec["dataSources"] = ds_names + + return result + + @router.get("/saved_datasets/data/{name}") + def get_dataset_data( + name: str, + project: str = Query(...), + limit: int = Query(10, ge=1, le=100), + ): + import json + + req = RegistryServer_pb2.GetDatasetDataRequest( + name=name, project=project, limit=limit + ) + try: + result = grpc_call(grpc_handler.GetDatasetData, req) + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to read dataset data: {str(e)}", + ) + + columns = result.get("columns", []) + raw_rows = result.get("rows", []) + rows = [] + for raw_row in raw_rows: + values = raw_row.get("values", []) + row_dict = {} + for i, col in enumerate(columns): + if i < len(values): + try: + row_dict[col] = json.loads(values[i]) + except (json.JSONDecodeError, TypeError): + row_dict[col] = values[i] + else: + row_dict[col] = "" + rows.append(row_dict) + + return { + "columns": columns, + "rows": rows, + "total_rows": result.get("totalRows", 0), + "sample_size": result.get("sampleSize", 0), + } + + @router.get("/saved_datasets/jobs/{job_id}") + def get_dataset_job_status(job_id: str): + req = RegistryServer_pb2.GetDatasetJobStatusRequest(job_id=job_id) + result = grpc_call(grpc_handler.GetDatasetJobStatus, req) + return { + "job_id": result.get("jobId", ""), + "dataset_name": result.get("datasetName", ""), + "project": result.get("project", ""), + "status": result.get("status", ""), + "created_at": result.get("createdAt", ""), + "completed_at": result.get("completedAt", ""), + "error": result.get("error", ""), + } + + @router.get("/saved_datasets/jobs") + def list_dataset_jobs( + project: str = Query(""), + status: Optional[str] = Query(None), + ): + req = RegistryServer_pb2.ListDatasetJobsRequest( + project=project or "", + status_filter=status or "", + ) + result = grpc_call(grpc_handler.ListDatasetJobs, req) + jobs = result.get("jobs", []) + return { + "jobs": [ + { + "job_id": j.get("jobId", ""), + "dataset_name": j.get("datasetName", ""), + "project": j.get("project", ""), + "status": j.get("status", ""), + "created_at": j.get("createdAt", ""), + "completed_at": j.get("completedAt", ""), + "error": j.get("error", ""), + } + for j in jobs + ] + } + @router.get("/saved_datasets/{name}") def get_saved_dataset( name: str, @@ -63,8 +271,14 @@ def get_saved_dataset( result = saved_dataset - # Note: saved datasets may not have relationships in the traditional sense - # but we include the functionality for consistency + fv_ds_map = _build_feature_view_to_datasource_map( + grpc_handler, project, allow_cache + ) + ds_names = _get_dataset_data_sources(result, fv_ds_map) + if ds_names: + spec = result.get("spec", result) + spec["dataSources"] = ds_names + if include_relationships: relationships = get_object_relationships( grpc_handler, "savedDataset", name, project, allow_cache @@ -102,6 +316,12 @@ def list_saved_datasets( project: str = Query(...), allow_cache: bool = Query(default=True), tags: Dict[str, str] = Depends(parse_tags), + namespace: Optional[str] = Query( + None, description="Filter by namespace (logical grouping)" + ), + collection: Optional[str] = Query( + None, description="Filter by collection (sub-grouping within namespace)" + ), include_relationships: bool = Query( False, description="Include relationships for each saved dataset" ), @@ -112,12 +332,23 @@ def list_saved_datasets( project=project, allow_cache=allow_cache, tags=tags, + namespace=namespace or "", + collection=collection or "", pagination=create_grpc_pagination_params(pagination_params), sorting=create_grpc_sorting_params(sorting_params), ) response = grpc_call(grpc_handler.ListSavedDatasets, req) saved_datasets = response.get("savedDatasets", []) + fv_ds_map = _build_feature_view_to_datasource_map( + grpc_handler, project, allow_cache + ) + for sd in saved_datasets: + ds_names = _get_dataset_data_sources(sd, fv_ds_map) + if ds_names: + spec = sd.get("spec", sd) + spec["dataSources"] = ds_names + result = { "savedDatasets": saved_datasets, "pagination": response.get("pagination", {}), @@ -131,4 +362,225 @@ def list_saved_datasets( return result + @router.post("/saved_datasets") + def register_saved_dataset(payload: RegisterDatasetRequest = Body(...)): + if not payload.name or not payload.name.strip(): + raise HTTPException(status_code=400, detail="Dataset name is required.") + if not payload.project or not payload.project.strip(): + raise HTTPException(status_code=400, detail="Project is required.") + if not payload.storage_path or not payload.storage_path.strip(): + raise HTTPException( + status_code=400, + detail="Storage path is required. Provide a file path or table reference.", + ) + + if not payload.allow_override: + try: + req = RegistryServer_pb2.GetSavedDatasetRequest( + name=payload.name.strip(), + project=payload.project.strip(), + allow_cache=False, + ) + grpc_call(grpc_handler.GetSavedDataset, req) + raise HTTPException( + status_code=409, + detail=f'A dataset named "{payload.name.strip()}" already exists in project "{payload.project.strip()}". Use edit to update it.', + ) + except HTTPException: + raise + except Exception: + pass + + storage_proto = SavedDatasetStorageProto() + path = payload.storage_path.strip() + if payload.storage_type == "bigquery": + storage_proto.bigquery_storage.CopyFrom( + DataSourceProto.BigQueryOptions(table=path) + ) + elif payload.storage_type == "redshift": + storage_proto.redshift_storage.CopyFrom( + DataSourceProto.RedshiftOptions(table=path) + ) + elif payload.storage_type == "snowflake": + storage_proto.snowflake_storage.CopyFrom( + DataSourceProto.SnowflakeOptions(table=path) + ) + elif payload.storage_type == "trino": + storage_proto.trino_storage.CopyFrom( + DataSourceProto.TrinoOptions(table=path) + ) + elif payload.storage_type == "spark": + storage_proto.spark_storage.CopyFrom( + DataSourceProto.SparkOptions( + path=path, + file_format=payload.storage_file_format or "parquet", + ) + ) + elif payload.storage_type == "athena": + storage_proto.athena_storage.CopyFrom( + DataSourceProto.AthenaOptions(table=path) + ) + elif payload.storage_type == "postgres": + config = json.dumps({"name": None, "query": None, "table": path}) + storage_proto.custom_storage.CopyFrom( + DataSourceProto.CustomSourceOptions( + configuration=config.encode("utf-8") + ) + ) + elif payload.storage_type == "clickhouse": + config = json.dumps({"name": None, "query": None, "table": path}) + storage_proto.custom_storage.CopyFrom( + DataSourceProto.CustomSourceOptions( + configuration=config.encode("utf-8") + ) + ) + elif payload.storage_type == "couchbase": + parts = path.split(".", 2) + database = parts[0] if len(parts) > 0 else "" + scope = parts[1] if len(parts) > 1 else "" + collection = parts[2] if len(parts) > 2 else "" + config = json.dumps( + { + "name": None, + "query": None, + "database": database, + "scope": scope, + "collection": collection, + } + ) + storage_proto.custom_storage.CopyFrom( + DataSourceProto.CustomSourceOptions( + configuration=config.encode("utf-8") + ) + ) + elif payload.storage_type == "custom": + storage_proto.custom_storage.CopyFrom( + DataSourceProto.CustomSourceOptions(configuration=path.encode("utf-8")) + ) + else: + storage_proto.file_storage.CopyFrom(DataSourceProto.FileOptions(uri=path)) + + # Auto-populate features and join_keys from feature service if not provided + features = payload.features + join_keys = payload.join_keys + if payload.feature_service_name and (not features or not join_keys): + try: + derived_features, derived_keys = ( + grpc_handler.resolve_feature_service_metadata( + payload.feature_service_name.strip(), + payload.project.strip(), + ) + ) + if not features and derived_features: + features = derived_features + if not join_keys and derived_keys: + join_keys = derived_keys + except Exception: + pass + + spec = SavedDatasetSpec( + name=payload.name, + project=payload.project, + features=features or [], + join_keys=join_keys or [], + full_feature_names=payload.full_feature_names, + storage=storage_proto, + tags=payload.tags, + namespace=payload.namespace, + collection=payload.collection, + description=payload.description, + ) + if payload.feature_service_name: + spec.feature_service_name = payload.feature_service_name + + saved_dataset_proto = SavedDatasetProto( + spec=spec, + meta=SavedDatasetMeta(), + ) + + apply_req = RegistryServer_pb2.ApplySavedDatasetRequest( + saved_dataset=saved_dataset_proto, + project=payload.project, + commit=True, + ) + grpc_call(grpc_handler.ApplySavedDataset, apply_req) + return {"status": "ok", "name": payload.name, "project": payload.project} + + @router.delete("/saved_datasets/{name}") + def delete_saved_dataset( + name: str, + project: str = Query(...), + ): + req = RegistryServer_pb2.DeleteSavedDatasetRequest( + name=name, + project=project, + commit=True, + ) + grpc_call(grpc_handler.DeleteSavedDataset, req) + return {"status": "ok", "name": name, "project": project} + + @router.post("/saved_datasets/create") + def create_dataset(payload: CreateDatasetRequest = Body(...)): + if not payload.name or not payload.name.strip(): + raise HTTPException(status_code=400, detail="Dataset name is required.") + if not payload.project or not payload.project.strip(): + raise HTTPException(status_code=400, detail="Project is required.") + if not payload.storage_path or not payload.storage_path.strip(): + raise HTTPException( + status_code=400, detail="Storage path (output destination) is required." + ) + if not payload.feature_service_name and not payload.features: + raise HTTPException( + status_code=400, + detail="Either feature_service_name or features list is required.", + ) + + if payload.entity_source_type == "inline": + if not payload.entity_keys: + raise HTTPException( + status_code=400, + detail="entity_keys is required when entity_source_type is 'inline'.", + ) + if not payload.entity_values or not payload.entity_values.strip(): + raise HTTPException( + status_code=400, + detail="entity_values is required when entity_source_type is 'inline'.", + ) + elif payload.entity_source_type == "reference": + if not payload.entity_source_path: + raise HTTPException( + status_code=400, + detail="entity_source_path is required when entity_source_type is 'reference'.", + ) + + req = RegistryServer_pb2.CreateDatasetFromRetrievalRequest( + name=payload.name.strip(), + project=payload.project.strip(), + feature_service_name=payload.feature_service_name or "", + features=payload.features or [], + entity_source_type=payload.entity_source_type, + entity_source_path=payload.entity_source_path or "", + entity_keys=payload.entity_keys or [], + entity_values=payload.entity_values or "", + start_date=payload.start_date or "", + end_date=payload.end_date or "", + extra_columns=payload.extra_columns or "", + storage_type=payload.storage_type, + storage_path=payload.storage_path.strip(), + tags=payload.tags or {}, + allow_overwrite=payload.allow_overwrite, + ) + try: + result = grpc_call(grpc_handler.CreateDatasetFromRetrieval, req) + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Failed to create dataset: {str(e)}", + ) + + return { + "job_id": result.get("jobId", ""), + "status": result.get("status", ""), + } + return router diff --git a/sdk/python/feast/api/registry/rest/search.py b/sdk/python/feast/api/registry/rest/search.py index e670a8981ac..572e34c12d8 100644 --- a/sdk/python/feast/api/registry/rest/search.py +++ b/sdk/python/feast/api/registry/rest/search.py @@ -158,6 +158,19 @@ def search_resources( } ) + # Extract and convert label views + label_views = project_resources.get("labelViews", []) + for lv in label_views: + results.append( + { + "type": "labelView", + "name": lv.get("spec", {}).get("name", ""), + "description": lv.get("spec", {}).get("description", ""), + "project": current_project, + "tags": lv.get("spec", {}).get("tags", {}), + } + ) + # Extract and convert features features = project_resources.get("features", []) for feature in features: @@ -172,6 +185,20 @@ def search_resources( } ) + # Extract and convert labels + labels = project_resources.get("labels", []) + for label in labels: + results.append( + { + "type": "label", + "name": label.get("name", ""), + "description": label.get("description", ""), + "project": current_project, + "labelView": label.get("featureView", ""), + "tags": label.get("tags", {}), + } + ) + # Extract and convert feature services feature_services = project_resources.get("featureServices", []) for fs in feature_services: diff --git a/sdk/python/feast/arrow_error_handler.py b/sdk/python/feast/arrow_error_handler.py index e873592bd5d..59f889a0a34 100644 --- a/sdk/python/feast/arrow_error_handler.py +++ b/sdk/python/feast/arrow_error_handler.py @@ -1,4 +1,6 @@ +import json import logging +import time from functools import wraps import pyarrow.flight as fl @@ -7,17 +9,45 @@ logger = logging.getLogger(__name__) +BACKOFF_FACTOR = 0.5 + def arrow_client_error_handling_decorator(func): @wraps(func) def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - except Exception as e: - mapped_error = FeastError.from_error_detail(_get_exception_data(e.args[0])) - if mapped_error is not None: - raise mapped_error - raise e + # Retry only applies to FeastFlightClient methods where args[0] (self) + # carries _connection_retries from RemoteOfflineStoreConfig. + # Standalone stream functions (write_table, read_all) get 0 retries: + # broken streams can't be reused and retrying risks duplicate writes. + max_retries = max(0, getattr(args[0], "_connection_retries", 0)) if args else 0 + + for attempt in range(max_retries + 1): + try: + return func(*args, **kwargs) + except fl.FlightUnavailableError as e: + if attempt < max_retries: + wait_time = BACKOFF_FACTOR * (2**attempt) + logger.warning( + "Transient Arrow Flight error on attempt %d/%d, " + "retrying in %.1fs: %s", + attempt + 1, + max_retries + 1, + wait_time, + e, + ) + time.sleep(wait_time) + continue + mapped_error = FeastError.from_error_detail(_get_exception_data(str(e))) + if mapped_error is not None: + raise mapped_error + raise e + except Exception as e: + mapped_error = FeastError.from_error_detail( + _get_exception_data(e.args[0]) + ) + if mapped_error is not None: + raise mapped_error + raise e return wrapper @@ -30,20 +60,35 @@ def wrapper(*args, **kwargs): except Exception as e: if isinstance(e, FeastError): raise fl.FlightError(e.to_error_detail()) + # Re-raise non-Feast exceptions so Arrow Flight returns a proper error + # instead of allowing the server method to return None. + raise e return wrapper def _get_exception_data(except_str) -> str: - substring = "Flight error: " + if not isinstance(except_str, str): + return "" - # Find the starting index of the substring + substring = "Flight error: " position = except_str.find(substring) - end_json_index = except_str.find("}") + if position == -1: + return "" - if position != -1 and end_json_index != -1: - # Extract the part of the string after the substring - result = except_str[position + len(substring) : end_json_index + 1] - return result + search_start = position + len(substring) + json_start = except_str.find("{", search_start) + if json_start == -1: + return "" - return "" + search_pos = json_start + while True: + json_end = except_str.find("}", search_pos + 1) + if json_end == -1: + return "" + candidate = except_str[json_start : json_end + 1] + try: + json.loads(candidate) + return candidate + except (json.JSONDecodeError, ValueError): + search_pos = json_end diff --git a/sdk/python/feast/base_feature_view.py b/sdk/python/feast/base_feature_view.py index d7dc2237bd3..b657f505f1e 100644 --- a/sdk/python/feast/base_feature_view.py +++ b/sdk/python/feast/base_feature_view.py @@ -22,6 +22,7 @@ from feast.feature_view_projection import FeatureViewProjection from feast.field import Field from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto +from feast.protos.feast.core.LabelView_pb2 import LabelView as LabelViewProto from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( OnDemandFeatureView as OnDemandFeatureViewProto, ) @@ -56,6 +57,8 @@ class BaseFeatureView(ABC): projection: FeatureViewProjection created_timestamp: Optional[datetime] last_updated_timestamp: Optional[datetime] + version: str + current_version_number: Optional[int] @abstractmethod def __init__( @@ -92,9 +95,12 @@ def __init__( self.projection = FeatureViewProjection.from_definition(self) self.created_timestamp = None self.last_updated_timestamp = None + if not hasattr(self, "version"): + self.version = "latest" + if not hasattr(self, "current_version_number"): + self.current_version_number = None - if source: - self.source = source + self.source = source @property @abstractmethod @@ -104,7 +110,12 @@ def proto_class(self) -> Type[Message]: @abstractmethod def to_proto( self, - ) -> Union[FeatureViewProto, OnDemandFeatureViewProto, StreamFeatureViewProto]: + ) -> Union[ + FeatureViewProto, + OnDemandFeatureViewProto, + StreamFeatureViewProto, + LabelViewProto, + ]: pass @classmethod @@ -148,11 +159,20 @@ def __getitem__(self, item): return cp + def _schema_or_udf_changed(self, other: "BaseFeatureView") -> bool: + """Check if schema or UDF-related fields have changed (version-worthy changes). + + Callers always match by name first, so name comparison is omitted here. + """ + if sorted(self.features) != sorted(other.features): + return True + # Skip metadata: description, tags, owner, projection + # Skip source changes: treat as deployment/location details, not schema changes + return False + def __eq__(self, other): if not isinstance(other, BaseFeatureView): - raise TypeError( - "Comparisons should only involve BaseFeatureView class objects." - ) + return False if ( self.name != other.name @@ -179,6 +199,18 @@ def ensure_valid(self): """ if not self.name: raise ValueError("Feature view needs a name.") + if "@" in self.name: + raise ValueError( + f"Feature view name '{self.name}' must not contain '@'. " + f"The '@' character is reserved for version-qualified references " + f"(e.g., 'fv@v2:feature')." + ) + if ":" in self.name: + raise ValueError( + f"Feature view name '{self.name}' must not contain ':'. " + f"The ':' character is reserved as the separator in fully qualified " + f"feature references (e.g., 'feature_view:feature_name')." + ) def with_name(self, name: str): """ diff --git a/sdk/python/feast/batch_feature_view.py b/sdk/python/feast/batch_feature_view.py index 3f3e1bf20ec..0bfbdf9d936 100644 --- a/sdk/python/feast/batch_feature_view.py +++ b/sdk/python/feast/batch_feature_view.py @@ -82,7 +82,9 @@ def __init__( *, name: str, mode: Union[TransformationMode, str] = TransformationMode.PYTHON, - source: Union[DataSource, "BatchFeatureView", List["BatchFeatureView"]], + source: Optional[ + Union[DataSource, "BatchFeatureView", List["BatchFeatureView"]] + ] = None, sink_source: Optional[DataSource] = None, entities: Optional[List[Entity]] = None, ttl: Optional[timedelta] = None, @@ -91,12 +93,15 @@ def __init__( offline: bool = False, description: str = "", owner: str = "", + org: str = "", schema: Optional[List[Field]] = None, udf: Optional[Callable[[Any], Any]] = None, udf_string: Optional[str] = "", feature_transformation: Optional[Transformation] = None, batch_engine: Optional[Dict[str, Any]] = None, aggregations: Optional[List[Aggregation]] = None, + enable_validation: bool = False, + version: str = "latest", ): if not flags_helper.is_test(): warnings.warn( @@ -114,6 +119,21 @@ def __init__( f"or CUSTOM_SOURCE, got {type(source).__name__}: {source.name} instead " ) + if source is None and aggregations: + raise ValueError( + "BatchFeatureView with aggregations requires a source to aggregate from." + ) + + if ( + source is None + and not udf + and not feature_transformation + and not aggregations + ): + raise ValueError( + "BatchFeatureView requires at least one of: source, udf, feature_transformation, or aggregations." + ) + self.mode = mode self.udf = udf self.udf_string = udf_string @@ -132,10 +152,13 @@ def __init__( offline=offline, description=description, owner=owner, + org=org, schema=schema, source=source, # type: ignore[arg-type] sink_source=sink_source, mode=mode, + enable_validation=enable_validation, + version=version, ) def get_feature_transformation(self) -> Optional[Transformation]: @@ -146,13 +169,14 @@ def get_feature_transformation(self) -> Optional[Transformation]: TransformationMode.PYTHON, TransformationMode.SQL, TransformationMode.RAY, - ) or self.mode in ("pandas", "python", "sql", "ray"): + TransformationMode.FLINK, + ) or self.mode in ("pandas", "python", "sql", "ray", "flink"): return Transformation( mode=self.mode, udf=self.udf, udf_string=self.udf_string or "" ) else: raise ValueError( - f"Unsupported transformation mode: {self.mode} for StreamFeatureView" + f"Unsupported transformation mode: {self.mode} for BatchFeatureView" ) @@ -168,7 +192,10 @@ def batch_feature_view( offline: bool = True, description: str = "", owner: str = "", + org: str = "", schema: Optional[List[Field]] = None, + enable_validation: bool = False, + version: str = "latest", ): """ Creates a BatchFeatureView object with the given user-defined function (UDF) as the transformation. @@ -196,9 +223,12 @@ def decorator(user_function): offline=offline, description=description, owner=owner, + org=org, schema=schema, udf=user_function, udf_string=udf_string, + enable_validation=enable_validation, + version=version, ) functools.update_wrapper(wrapper=batch_feature_view_obj, wrapped=user_function) return batch_feature_view_obj diff --git a/sdk/python/feast/chunker.py b/sdk/python/feast/chunker.py new file mode 100644 index 00000000000..484f2b398a8 --- /dev/null +++ b/sdk/python/feast/chunker.py @@ -0,0 +1,150 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any, Optional + +import pandas as pd + + +@dataclass +class ChunkingConfig: + chunk_size: int = 100 + chunk_overlap: int = 20 + min_chunk_size: int = 20 + max_chunk_chars: Optional[int] = 500 + + +class BaseChunker(ABC): + """ + Abstract base class for document chunking. + + Subclasses implement load_parse_and_chunk() with their own: + - Loading logic + - Parsing logic + - Chunking strategy + """ + + def __init__(self, config: Optional[ChunkingConfig] = None): + self.config = config or ChunkingConfig() + + @abstractmethod + def load_parse_and_chunk( + self, + source: Any, + source_id: str, + source_column: str, + source_type: Optional[str] = None, + ) -> list[dict]: + """ + Load, parse, and chunk a document. + + Args: + source: File path, raw text, bytes, etc. + source_id: Document identifier. + source_type: Optional type hint. + source_column: The column containing the document sources. + + Returns: + List of chunk dicts with keys: + - chunk_id: str + - original_id: str + - text: str + - chunk_index: int + - (any additional metadata) + """ + pass + + def chunk_dataframe( + self, + df: pd.DataFrame, + id_column: str, + source_column: str, + type_column: Optional[str] = None, + ) -> pd.DataFrame: + """ + Chunk all documents in a DataFrame. + + Args: + df: The DataFrame containing the documents to chunk. + id_column: The column containing the document IDs. + source_column: The column containing the document sources. + type_column: The column containing the document types. + """ + + all_chunks = [] + for row in df.itertuples(index=False): + chunks = self.load_parse_and_chunk( + getattr(row, source_column), + str(getattr(row, id_column)), + source_column, + getattr(row, type_column) if type_column else None, + ) + all_chunks.extend(chunks) + + if not all_chunks: + return pd.DataFrame( + columns=["chunk_id", "original_id", source_column, "chunk_index"] + ) + return pd.DataFrame(all_chunks) + + +class TextChunker(BaseChunker): + """Default chunker for plain text. Chunks by word count.""" + + def load_parse_and_chunk( + self, + source: Any, + source_id: str, + source_column: str, + source_type: Optional[str] = None, + ) -> list[dict]: + # Load + text = self._load(source) + + # Chunk by words + return self._chunk_by_words(text, source_id, source_column) + + def _load(self, source: Any) -> str: + from pathlib import Path + + if isinstance(source, Path) and source.exists(): + return Path(source).read_text() + if isinstance(source, str): + if source.endswith(".txt") and Path(source).exists(): + return Path(source).read_text() + return str(source) + + def _chunk_by_words( + self, text: str, source_id: str, source_column: str + ) -> list[dict]: + words = text.split() + chunks = [] + + step = self.config.chunk_size - self.config.chunk_overlap + if step <= 0: + raise ValueError( + f"chunk_overlap ({self.config.chunk_overlap}) must be less than " + f"chunk_size ({self.config.chunk_size})" + ) + chunk_index = 0 + + for i in range(0, len(words), step): + chunk_words = words[i : i + self.config.chunk_size] + + if len(chunk_words) < self.config.min_chunk_size: + continue + + chunk_text = " ".join(chunk_words) + if self.config.max_chunk_chars: + chunk_text = chunk_text[: self.config.max_chunk_chars] + + chunks.append( + { + "chunk_id": f"{source_id}_{chunk_index}", + "original_id": source_id, + source_column: chunk_text, + "chunk_index": chunk_index, + } + ) + chunk_index += 1 + + return chunks diff --git a/sdk/python/feast/cli/cli.py b/sdk/python/feast/cli/cli.py index 91fa2a92606..7826dfb650a 100644 --- a/sdk/python/feast/cli/cli.py +++ b/sdk/python/feast/cli/cli.py @@ -1,553 +1,662 @@ -# Copyright 2019 The Feast Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import json -import logging -from datetime import datetime -from importlib.metadata import version as importlib_version -from pathlib import Path -from typing import List, Optional - -import click -import yaml -from colorama import Fore, Style -from dateutil import parser -from pygments import formatters, highlight, lexers - -from feast import utils -from feast.cli.data_sources import data_sources_cmd -from feast.cli.entities import entities_cmd -from feast.cli.feature_services import feature_services_cmd -from feast.cli.feature_views import feature_views_cmd -from feast.cli.features import ( - features_cmd, - get_historical_features, - get_online_features, -) -from feast.cli.on_demand_feature_views import on_demand_feature_views_cmd -from feast.cli.permissions import feast_permissions_cmd -from feast.cli.projects import projects_cmd -from feast.cli.saved_datasets import saved_datasets_cmd -from feast.cli.serve import ( - serve_command, - serve_offline_command, - serve_registry_command, - serve_transformations_command, -) -from feast.cli.stream_feature_views import stream_feature_views_cmd -from feast.cli.ui import ui -from feast.cli.validation_references import validation_references_cmd -from feast.constants import FEAST_FS_YAML_FILE_PATH_ENV_NAME -from feast.errors import FeastProviderLoginError -from feast.repo_config import load_repo_config -from feast.repo_operations import ( - apply_total, - cli_check_repo, - create_feature_store, - generate_project_name, - init_repo, - plan, - registry_dump, - teardown, -) -from feast.utils import maybe_local_tz - -_logger = logging.getLogger(__name__) - - -class NoOptionDefaultFormat(click.Command): - def format_options(self, ctx: click.Context, formatter: click.HelpFormatter): - """Writes all the options into the formatter if they exist.""" - opts = [] - for param in self.get_params(ctx): - rv = param.get_help_record(ctx) - if rv is not None: - opts.append(rv) - if opts: - with formatter.section("Options(No current command options)"): - formatter.write_dl(opts) - - -@click.group() -@click.option( - "--chdir", - "-c", - envvar="FEATURE_REPO_DIR_ENV_VAR", - help="Switch to a different feature repository directory before executing the given subcommand. Can also be set via the FEATURE_REPO_DIR_ENV_VAR environment variable.", -) -@click.option( - "--log-level", - default="warning", - help="The logging level. One of DEBUG, INFO, WARNING, ERROR, and CRITICAL (case-insensitive).", -) -@click.option( - "--feature-store-yaml", - "-f", - help=f"Override the directory where the CLI should look for the feature_store.yaml file. Can also be set via the {FEAST_FS_YAML_FILE_PATH_ENV_NAME} environment variable.", -) -@click.pass_context -def cli( - ctx: click.Context, - chdir: Optional[str], - log_level: str, - feature_store_yaml: Optional[str], -): - """ - Feast CLI - - For more information, see our public docs at https://docs.feast.dev/ - """ - ctx.ensure_object(dict) - ctx.obj["CHDIR"] = Path.cwd() if chdir is None else Path(chdir).absolute() - ctx.obj["FS_YAML_FILE"] = ( - Path(feature_store_yaml).absolute() - if feature_store_yaml - else utils.get_default_yaml_file_path(ctx.obj["CHDIR"]) - ) - try: - level = getattr(logging, log_level.upper()) - logging.basicConfig( - format="%(asctime)s %(name)s %(levelname)s: %(message)s", - datefmt="%m/%d/%Y %I:%M:%S %p", - level=level, - ) - # Override the logging level for already created loggers (due to loggers being created at the import time) - # Note, that format & datefmt does not need to be set, because by default child loggers don't override them - - # Also note, that mypy complains that logging.root doesn't have "manager" because of the way it's written. - # So we have to put a type ignore hint for mypy. - for logger_name in logging.root.manager.loggerDict: # type: ignore - if "feast" in logger_name: - logger = logging.getLogger(logger_name) - logger.setLevel(level) - except Exception as e: - raise e - pass - - -@cli.command() -def version(): - """ - Display Feast SDK version - """ - print( - f'{Style.BRIGHT + Fore.BLUE}Feast SDK Version: {Style.BRIGHT + Fore.GREEN}"{importlib_version("feast")}"' - ) - - -@cli.command() -@click.argument("object_id") -@click.pass_context -def delete(ctx: click.Context, object_id: str): - """ - Delete Feast Object - """ - repo = ctx.obj["CHDIR"] - fs_yaml_file = ctx.obj["FS_YAML_FILE"] - cli_check_repo(repo, fs_yaml_file) - store = create_feature_store(ctx) - - e = None - object_type = None - - # Order matters if names can overlap between types, - # though typically they shouldn't in a well-structured feature store. - object_getters_and_types = [ - (store.get_entity, "Entity"), - (store.get_feature_view, "FeatureView"), - (store.get_feature_service, "FeatureService"), - (store.get_data_source, "DataSource"), - (store.get_saved_dataset, "SavedDataset"), - (store.get_validation_reference, "ValidationReference"), - (store.get_stream_feature_view, "StreamFeatureView"), - (store.get_on_demand_feature_view, "OnDemandFeatureView"), - # Add other get_* methods here if needed - ] - - for getter, obj_type_str in object_getters_and_types: - try: - potential_e = getter(object_id) # type: ignore[operator] - if potential_e: - e = potential_e - object_type = obj_type_str - break - except Exception: - pass - - if isinstance(e, list): - e = e[0] - if e: - store.apply([e], objects_to_delete=[e], partial=False) - print( - f"{Style.BRIGHT + Fore.RED}Deleted {Style.BRIGHT + Fore.GREEN}{object_type} {Fore.YELLOW}{object_id} from {Fore.GREEN}{store.project}.{Style.RESET_ALL}" - ) - else: - print( - f"{Style.BRIGHT + Fore.GREEN}Object not found. Deletion skipped.{Style.RESET_ALL}" - ) - - -@cli.command() -@click.pass_context -def configuration(ctx: click.Context): - """ - Display Feast configuration - """ - repo = ctx.obj["CHDIR"] - fs_yaml_file = ctx.obj["FS_YAML_FILE"] - cli_check_repo(repo, fs_yaml_file) - repo_config = load_repo_config(repo, fs_yaml_file) - if repo_config: - config_dict = repo_config.model_dump(by_alias=True, exclude_unset=True) - config_dict.pop("repo_path", None) - print(yaml.dump(config_dict, default_flow_style=False, sort_keys=False)) - else: - print("No configuration found.") - - -@cli.command() -@click.pass_context -def endpoint(ctx: click.Context): - """ - Display feature server endpoints - """ - store = create_feature_store(ctx) - endpoint = store.get_feature_server_endpoint() - if endpoint is not None: - _logger.info( - f"Feature server endpoint: {Style.BRIGHT + Fore.GREEN}{endpoint}{Style.RESET_ALL}" - ) - else: - _logger.info("There is no active feature server.") - - -@cli.command("plan", cls=NoOptionDefaultFormat) -@click.option( - "--skip-source-validation", - is_flag=True, - help="Don't validate the data sources by checking for that the tables exist.", -) -@click.pass_context -def plan_command(ctx: click.Context, skip_source_validation: bool): - """ - Create or update a feature store deployment - """ - repo = ctx.obj["CHDIR"] - fs_yaml_file = ctx.obj["FS_YAML_FILE"] - cli_check_repo(repo, fs_yaml_file) - repo_config = load_repo_config(repo, fs_yaml_file) - try: - plan(repo_config, repo, skip_source_validation) - except FeastProviderLoginError as e: - print(str(e)) - - -@cli.command("apply", cls=NoOptionDefaultFormat) -@click.option( - "--skip-source-validation", - is_flag=True, - help="Don't validate the data sources by checking for that the tables exist.", -) -@click.pass_context -def apply_total_command(ctx: click.Context, skip_source_validation: bool): - """ - Create or update a feature store deployment - """ - repo = ctx.obj["CHDIR"] - fs_yaml_file = ctx.obj["FS_YAML_FILE"] - cli_check_repo(repo, fs_yaml_file) - - repo_config = load_repo_config(repo, fs_yaml_file) - try: - apply_total(repo_config, repo, skip_source_validation) - except FeastProviderLoginError as e: - print(str(e)) - - -@cli.command("teardown", cls=NoOptionDefaultFormat) -@click.pass_context -def teardown_command(ctx: click.Context): - """ - Tear down deployed feature store infrastructure - """ - repo = ctx.obj["CHDIR"] - fs_yaml_file = ctx.obj["FS_YAML_FILE"] - cli_check_repo(repo, fs_yaml_file) - repo_config = load_repo_config(repo, fs_yaml_file) - - teardown(repo_config, repo) - - -@cli.command("registry-dump") -@click.pass_context -def registry_dump_command(ctx: click.Context): - """ - Print contents of the metadata registry - """ - repo = ctx.obj["CHDIR"] - fs_yaml_file = ctx.obj["FS_YAML_FILE"] - cli_check_repo(repo, fs_yaml_file) - repo_config = load_repo_config(repo, fs_yaml_file) - - click.echo(registry_dump(repo_config, repo_path=repo)) - - -@cli.command("materialize") -@click.argument("start_ts", required=False) -@click.argument("end_ts", required=False) -@click.option( - "--views", - "-v", - help="Feature views to materialize", - multiple=True, -) -@click.option( - "--disable-event-timestamp", - is_flag=True, - help="Materialize all available data using current datetime as event timestamp (useful when source data lacks event timestamps)", -) -@click.pass_context -def materialize_command( - ctx: click.Context, - start_ts: Optional[str], - end_ts: Optional[str], - views: List[str], - disable_event_timestamp: bool, -): - """ - Run a (non-incremental) materialization job to ingest data into the online store. Feast - will read all data between START_TS and END_TS from the offline store and write it to the - online store. If you don't specify feature view names using --views, all registered Feature - Views will be materialized. - - START_TS and END_TS should be in ISO 8601 format, e.g. '2021-07-16T19:20:01' - - If --disable-event-timestamp is used, timestamps are not required and all available data will be materialized using the current datetime as the event timestamp. - """ - store = create_feature_store(ctx) - - if disable_event_timestamp: - if start_ts or end_ts: - raise click.UsageError( - "Cannot specify START_TS or END_TS when --disable-event-timestamp is used" - ) - now = datetime.now() - # Query all available data and use current datetime as event timestamp - start_date = datetime( - 1970, 1, 1 - ) # Beginning of time to capture all historical data - end_date = now - else: - if not start_ts or not end_ts: - raise click.UsageError( - "START_TS and END_TS are required unless --disable-event-timestamp is used" - ) - start_date = utils.make_tzaware(parser.parse(start_ts)) - end_date = utils.make_tzaware(parser.parse(end_ts)) - - store.materialize( - feature_views=None if not views else views, - start_date=start_date, - end_date=end_date, - disable_event_timestamp=disable_event_timestamp, - ) - - -@cli.command("materialize-incremental") -@click.argument("end_ts") -@click.option( - "--views", - "-v", - help="Feature views to incrementally materialize", - multiple=True, -) -@click.pass_context -def materialize_incremental_command(ctx: click.Context, end_ts: str, views: List[str]): - """ - Run an incremental materialization job to ingest new data into the online store. Feast will read - all data from the previously ingested point to END_TS from the offline store and write it to the - online store. If you don't specify feature view names using --views, all registered Feature - Views will be incrementally materialized. - - END_TS should be in ISO 8601 format, e.g. '2021-07-16T19:20:01' - """ - store = create_feature_store(ctx) - store.materialize_incremental( - feature_views=None if not views else views, - end_date=utils.make_tzaware(datetime.fromisoformat(end_ts)), - ) - - -@cli.command("init") -@click.argument("PROJECT_DIRECTORY", required=False) -@click.option( - "--minimal", "-m", is_flag=True, help="Create an empty project repository" -) -@click.option( - "--template", - "-t", - type=click.Choice( - [ - "local", - "gcp", - "aws", - "snowflake", - "spark", - "postgres", - "hbase", - "cassandra", - "hazelcast", - "ikv", - "couchbase", - "milvus", - "ray", - "ray_rag", - ], - case_sensitive=False, - ), - help="Specify a template for the created project", - default="local", -) -def init_command(project_directory, minimal: bool, template: str): - """Create a new Feast repository""" - if not project_directory: - project_directory = generate_project_name() - - if minimal: - template = "minimal" - - init_repo(project_directory, template) - - -@cli.command("listen") -@click.option( - "--address", - "-a", - type=click.STRING, - default="localhost:50051", - show_default=True, - help="Address of the gRPC server", -) -@click.option( - "--max_workers", - "-w", - type=click.INT, - default=10, - show_default=False, - help="The maximum number of threads that can be used to execute the gRPC calls", -) -@click.option( - "--registry_ttl_sec", - "-r", - help="Number of seconds after which the registry is refreshed", - type=click.INT, - default=5, - show_default=True, -) -@click.pass_context -def listen_command( - ctx: click.Context, - address: str, - max_workers: int, - registry_ttl_sec: int, -): - """Start a gRPC feature server to ingest streaming features on given address""" - from feast.infra.contrib.grpc_server import get_grpc_server - - store = create_feature_store(ctx) - server = get_grpc_server(address, store, max_workers, registry_ttl_sec) - server.start() - server.wait_for_termination() - - -@cli.command("validate") -@click.option( - "--feature-service", - "-f", - help="Specify a feature service name", -) -@click.option( - "--reference", - "-r", - help="Specify a validation reference name", -) -@click.option( - "--no-profile-cache", - is_flag=True, - help="Do not store cached profile in registry", -) -@click.argument("start_ts") -@click.argument("end_ts") -@click.pass_context -def validate( - ctx: click.Context, - feature_service: str, - reference: str, - start_ts: str, - end_ts: str, - no_profile_cache, -): - """ - Perform validation of logged features (produced by a given feature service) against provided reference. - - START_TS and END_TS should be in ISO 8601 format, e.g. '2021-07-16T19:20:01' - """ - store = create_feature_store(ctx) - - _feature_service = store.get_feature_service(name=feature_service) - _reference = store.get_validation_reference(reference) - - result = store.validate_logged_features( - source=_feature_service, - reference=_reference, - start=maybe_local_tz(datetime.fromisoformat(start_ts)), - end=maybe_local_tz(datetime.fromisoformat(end_ts)), - throw_exception=False, - cache_profile=not no_profile_cache, - ) - - if not result: - print(f"{Style.BRIGHT + Fore.GREEN}Validation successful!{Style.RESET_ALL}") - return - - errors = [e.to_dict() for e in result.report.errors] - formatted_json = json.dumps(errors, indent=4) - colorful_json = highlight( - formatted_json, lexers.JsonLexer(), formatters.TerminalFormatter() - ) - print(f"{Style.BRIGHT + Fore.RED}Validation failed!{Style.RESET_ALL}") - print(colorful_json) - exit(1) - - -cli.add_command(data_sources_cmd) -cli.add_command(entities_cmd) -cli.add_command(feature_services_cmd) -cli.add_command(feature_views_cmd) -cli.add_command(features_cmd) -cli.add_command(get_historical_features) -cli.add_command(get_online_features) -cli.add_command(on_demand_feature_views_cmd) -cli.add_command(feast_permissions_cmd) -cli.add_command(projects_cmd) -cli.add_command(saved_datasets_cmd) -cli.add_command(stream_feature_views_cmd) -cli.add_command(validation_references_cmd) -cli.add_command(ui) -cli.add_command(serve_command) -cli.add_command(serve_offline_command) -cli.add_command(serve_registry_command) -cli.add_command(serve_transformations_command) - -if __name__ == "__main__": - cli() +# Copyright 2019 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +import logging +from datetime import datetime +from importlib.metadata import version as importlib_version +from pathlib import Path +from typing import List, Optional + +import click +import yaml +from colorama import Fore, Style +from dateutil import parser +from pygments import formatters, highlight, lexers + +from feast import utils +from feast.cli.data_sources import data_sources_cmd +from feast.cli.dbt_import import dbt_cmd +from feast.cli.entities import entities_cmd +from feast.cli.feature_services import feature_services_cmd +from feast.cli.feature_views import feature_views_cmd +from feast.cli.features import ( + features_cmd, + get_historical_features, + get_online_features, +) +from feast.cli.label_views import label_views_cmd +from feast.cli.monitor import monitor_cmd +from feast.cli.on_demand_feature_views import on_demand_feature_views_cmd +from feast.cli.permissions import feast_permissions_cmd +from feast.cli.projects import projects_cmd +from feast.cli.registry import registry_cmd +from feast.cli.saved_datasets import saved_datasets_cmd +from feast.cli.serve import ( + serve_command, + serve_offline_command, + serve_registry_command, + serve_transformations_command, +) +from feast.cli.stream_feature_views import stream_feature_views_cmd +from feast.cli.ui import ui +from feast.cli.validation_references import validation_references_cmd +from feast.constants import FEAST_FS_YAML_FILE_PATH_ENV_NAME +from feast.errors import FeastProviderLoginError +from feast.repo_config import load_repo_config +from feast.repo_operations import ( + apply_total, + cli_check_repo, + create_feature_store, + generate_project_name, + init_repo, + plan, + registry_dump, + teardown, +) +from feast.utils import maybe_local_tz + +_logger = logging.getLogger(__name__) + + +class NoOptionDefaultFormat(click.Command): + def format_options(self, ctx: click.Context, formatter: click.HelpFormatter): + """Writes all the options into the formatter if they exist.""" + opts = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None: + opts.append(rv) + if opts: + with formatter.section("Options(No current command options)"): + formatter.write_dl(opts) + + +@click.group() +@click.option( + "--chdir", + "-c", + envvar="FEATURE_REPO_DIR_ENV_VAR", + help="Switch to a different feature repository directory before executing the given subcommand. Can also be set via the FEATURE_REPO_DIR_ENV_VAR environment variable.", +) +@click.option( + "--log-level", + default="warning", + help="The logging level. One of DEBUG, INFO, WARNING, ERROR, and CRITICAL (case-insensitive).", +) +@click.option( + "--feature-store-yaml", + "-f", + help=f"Override the directory where the CLI should look for the feature_store.yaml file. Can also be set via the {FEAST_FS_YAML_FILE_PATH_ENV_NAME} environment variable.", +) +@click.pass_context +def cli( + ctx: click.Context, + chdir: Optional[str], + log_level: str, + feature_store_yaml: Optional[str], +): + """ + Feast CLI + + For more information, see our public docs at https://docs.feast.dev/ + """ + ctx.ensure_object(dict) + ctx.obj["CHDIR"] = Path.cwd() if chdir is None else Path(chdir).absolute() + ctx.obj["FS_YAML_FILE"] = ( + Path(feature_store_yaml).absolute() + if feature_store_yaml + else utils.get_default_yaml_file_path(ctx.obj["CHDIR"]) + ) + try: + level = getattr(logging, log_level.upper()) + logging.basicConfig( + format="%(asctime)s %(name)s %(levelname)s: %(message)s", + datefmt="%m/%d/%Y %I:%M:%S %p", + level=level, + ) + # Override the logging level for already created loggers (due to loggers being created at the import time) + # Note, that format & datefmt does not need to be set, because by default child loggers don't override them + + # Also note, that mypy complains that logging.root doesn't have "manager" because of the way it's written. + # So we have to put a type ignore hint for mypy. + for logger_name in logging.root.manager.loggerDict: # type: ignore + if "feast" in logger_name: + logger = logging.getLogger(logger_name) + logger.setLevel(level) + except Exception as e: + raise e + pass + + +@cli.command() +def version(): + """ + Display Feast SDK version + """ + print( + f'{Style.BRIGHT + Fore.BLUE}Feast SDK Version: {Style.BRIGHT + Fore.GREEN}"{importlib_version("feast")}"' + ) + + +@cli.command() +@click.argument("object_id") +@click.pass_context +def delete(ctx: click.Context, object_id: str): + """ + Delete Feast Object + """ + repo = ctx.obj["CHDIR"] + fs_yaml_file = ctx.obj["FS_YAML_FILE"] + cli_check_repo(repo, fs_yaml_file) + store = create_feature_store(ctx) + + e = None + object_type = None + + # Order matters if names can overlap between types, + # though typically they shouldn't in a well-structured feature store. + object_getters_and_types = [ + (store.get_entity, "Entity"), + (store.get_feature_view, "FeatureView"), + (store.get_feature_service, "FeatureService"), + (store.get_data_source, "DataSource"), + (store.get_saved_dataset, "SavedDataset"), + (store.get_validation_reference, "ValidationReference"), + (store.get_stream_feature_view, "StreamFeatureView"), + (store.get_on_demand_feature_view, "OnDemandFeatureView"), + # Add other get_* methods here if needed + ] + + for getter, obj_type_str in object_getters_and_types: + try: + potential_e = getter(object_id) # type: ignore[operator] + if potential_e: + e = potential_e + object_type = obj_type_str + break + except Exception: + pass + + if isinstance(e, list): + e = e[0] + if e: + store.apply([e], objects_to_delete=[e], partial=False) + print( + f"{Style.BRIGHT + Fore.RED}Deleted {Style.BRIGHT + Fore.GREEN}{object_type} {Fore.YELLOW}{object_id} from {Fore.GREEN}{store.project}.{Style.RESET_ALL}" + ) + else: + print( + f"{Style.BRIGHT + Fore.GREEN}Object not found. Deletion skipped.{Style.RESET_ALL}" + ) + + +@cli.command() +@click.pass_context +def configuration(ctx: click.Context): + """ + Display Feast configuration + """ + repo = ctx.obj["CHDIR"] + fs_yaml_file = ctx.obj["FS_YAML_FILE"] + cli_check_repo(repo, fs_yaml_file) + repo_config = load_repo_config(repo, fs_yaml_file) + if repo_config: + config_dict = repo_config.model_dump(by_alias=True, exclude_unset=True) + config_dict.pop("repo_path", None) + print(yaml.dump(config_dict, default_flow_style=False, sort_keys=False)) + else: + print("No configuration found.") + + +@cli.command() +@click.pass_context +def endpoint(ctx: click.Context): + """ + Display feature server endpoints + """ + store = create_feature_store(ctx) + endpoint = store.get_feature_server_endpoint() + if endpoint is not None: + _logger.info( + f"Feature server endpoint: {Style.BRIGHT + Fore.GREEN}{endpoint}{Style.RESET_ALL}" + ) + else: + _logger.info("There is no active feature server.") + + +@cli.command("plan", cls=NoOptionDefaultFormat) +@click.option( + "--skip-source-validation", + is_flag=True, + help="Don't validate the data sources by checking for that the tables exist.", +) +@click.option( + "--skip-feature-view-validation", + is_flag=True, + help="Don't validate feature views. Use with caution as this skips important checks.", +) +@click.pass_context +def plan_command( + ctx: click.Context, skip_source_validation: bool, skip_feature_view_validation: bool +): + """ + Create or update a feature store deployment + """ + repo = ctx.obj["CHDIR"] + fs_yaml_file = ctx.obj["FS_YAML_FILE"] + cli_check_repo(repo, fs_yaml_file) + repo_config = load_repo_config(repo, fs_yaml_file) + try: + plan(repo_config, repo, skip_source_validation, skip_feature_view_validation) + except FeastProviderLoginError as e: + print(str(e)) + + +@cli.command("apply", cls=NoOptionDefaultFormat) +@click.option( + "--skip-source-validation", + is_flag=True, + help="Don't validate the data sources by checking for that the tables exist.", +) +@click.option( + "--skip-feature-view-validation", + is_flag=True, + help="Don't validate feature views. Use with caution as this skips important checks.", +) +@click.option( + "--no-progress", + is_flag=True, + help="Disable progress bars during apply operation.", +) +@click.option( + "--no-promote", + is_flag=True, + default=False, + help="Save new versions without promoting them to active. " + "New versions are accessible via @v reads and --version materialization.", +) +@click.pass_context +def apply_total_command( + ctx: click.Context, + skip_source_validation: bool, + skip_feature_view_validation: bool, + no_progress: bool, + no_promote: bool, +): + """ + Create or update a feature store deployment + """ + repo = ctx.obj["CHDIR"] + fs_yaml_file = ctx.obj["FS_YAML_FILE"] + cli_check_repo(repo, fs_yaml_file) + + repo_config = load_repo_config(repo, fs_yaml_file) + + # Set environment variable to disable progress if requested + if no_progress: + import os + + os.environ["FEAST_NO_PROGRESS"] = "1" + + try: + apply_total( + repo_config, + repo, + skip_source_validation, + skip_feature_view_validation, + no_promote=no_promote, + ) + except FeastProviderLoginError as e: + print(str(e)) + + +@cli.command("teardown", cls=NoOptionDefaultFormat) +@click.pass_context +def teardown_command(ctx: click.Context): + """ + Tear down deployed feature store infrastructure + """ + repo = ctx.obj["CHDIR"] + fs_yaml_file = ctx.obj["FS_YAML_FILE"] + cli_check_repo(repo, fs_yaml_file) + repo_config = load_repo_config(repo, fs_yaml_file) + + teardown(repo_config, repo) + + +@cli.command("registry-dump") +@click.pass_context +def registry_dump_command(ctx: click.Context): + """ + Print contents of the metadata registry + """ + repo = ctx.obj["CHDIR"] + fs_yaml_file = ctx.obj["FS_YAML_FILE"] + cli_check_repo(repo, fs_yaml_file) + repo_config = load_repo_config(repo, fs_yaml_file) + + click.echo(registry_dump(repo_config, repo_path=repo)) + + +@cli.command("materialize") +@click.argument("start_ts", required=False) +@click.argument("end_ts", required=False) +@click.option( + "--views", + "-v", + help="Feature views to materialize", + multiple=True, +) +@click.option( + "--disable-event-timestamp", + is_flag=True, + help="Materialize all available data using current datetime as event timestamp (useful when source data lacks event timestamps)", +) +@click.option( + "--version", + "feature_view_version", + default=None, + help="Version to materialize (e.g., 'v2'). Requires --views with exactly one feature view.", +) +@click.pass_context +def materialize_command( + ctx: click.Context, + start_ts: Optional[str], + end_ts: Optional[str], + views: List[str], + disable_event_timestamp: bool, + feature_view_version: Optional[str], +): + """ + Run a (non-incremental) materialization job to ingest data into the online store. Feast + will read all data between START_TS and END_TS from the offline store and write it to the + online store. If you don't specify feature view names using --views, all registered Feature + Views will be materialized. + + START_TS and END_TS should be in ISO 8601 format, e.g. '2021-07-16T19:20:01' + + If --disable-event-timestamp is used, timestamps are not required and all available data will be materialized using the current datetime as the event timestamp. + """ + store = create_feature_store(ctx) + + if disable_event_timestamp: + if start_ts or end_ts: + raise click.UsageError( + "Cannot specify START_TS or END_TS when --disable-event-timestamp is used" + ) + now = datetime.now() + # Query all available data and use current datetime as event timestamp + start_date = datetime( + 1970, 1, 1 + ) # Beginning of time to capture all historical data + end_date = now + else: + if not start_ts or not end_ts: + raise click.UsageError( + "START_TS and END_TS are required unless --disable-event-timestamp is used" + ) + start_date = utils.make_tzaware(parser.parse(start_ts)) + end_date = utils.make_tzaware(parser.parse(end_ts)) + + store.materialize( + feature_views=None if not views else views, + start_date=start_date, + end_date=end_date, + disable_event_timestamp=disable_event_timestamp, + version=feature_view_version, + ) + + +@cli.command("materialize-incremental") +@click.argument("end_ts") +@click.option( + "--views", + "-v", + help="Feature views to incrementally materialize", + multiple=True, +) +@click.option( + "--version", + "feature_view_version", + default=None, + help="Version to materialize (e.g., 'v2'). Requires --views with exactly one feature view.", +) +@click.pass_context +def materialize_incremental_command( + ctx: click.Context, + end_ts: str, + views: List[str], + feature_view_version: Optional[str], +): + """ + Run an incremental materialization job to ingest new data into the online store. Feast will read + all data from the previously ingested point to END_TS from the offline store and write it to the + online store. If you don't specify feature view names using --views, all registered Feature + Views will be incrementally materialized. + + END_TS should be in ISO 8601 format, e.g. '2021-07-16T19:20:01' + """ + store = create_feature_store(ctx) + store.materialize_incremental( + feature_views=None if not views else views, + end_date=utils.make_tzaware(datetime.fromisoformat(end_ts)), + version=feature_view_version, + ) + + +@cli.command("init") +@click.argument("PROJECT_DIRECTORY", required=False) +@click.option( + "--minimal", "-m", is_flag=True, help="Create an empty project repository" +) +@click.option( + "--template", + "-t", + type=click.Choice( + [ + "local", + "gcp", + "aws", + "snowflake", + "spark", + "postgres", + "hbase", + "cassandra", + "hazelcast", + "couchbase", + "milvus", + "ray", + "ray_rag", + "rag", + "pytorch_nlp", + ], + case_sensitive=False, + ), + help="Specify a template for the created project", + default="local", +) +@click.option( + "--repo-path", + help="Directory path where the repository will be created (default: create subdirectory with project name)", +) +def init_command(project_directory, minimal: bool, template: str, repo_path: str): + """Create a new Feast repository""" + if not project_directory: + project_directory = generate_project_name() + + if minimal: + template = "minimal" + + init_repo(project_directory, template, repo_path) + + +@cli.command("listen") +@click.option( + "--address", + "-a", + type=click.STRING, + default="localhost:50051", + show_default=True, + help="Address of the gRPC server", +) +@click.option( + "--max_workers", + "-w", + type=click.INT, + default=10, + show_default=False, + help="The maximum number of threads that can be used to execute the gRPC calls", +) +@click.option( + "--registry_ttl_sec", + "-r", + help="Number of seconds after which the registry is refreshed", + type=click.INT, + default=5, + show_default=True, +) +@click.pass_context +def listen_command( + ctx: click.Context, + address: str, + max_workers: int, + registry_ttl_sec: int, +): + """Start a gRPC feature server to ingest streaming features on given address""" + from feast.infra.contrib.grpc_server import get_grpc_server + + store = create_feature_store(ctx) + server = get_grpc_server(address, store, max_workers, registry_ttl_sec) + server.start() + server.wait_for_termination() + + +@cli.command("validate") +@click.option( + "--feature-service", + "-f", + help="Specify a feature service name", +) +@click.option( + "--reference", + "-r", + help="Specify a validation reference name", +) +@click.option( + "--no-profile-cache", + is_flag=True, + help="Do not store cached profile in registry", +) +@click.argument("start_ts") +@click.argument("end_ts") +@click.pass_context +def validate( + ctx: click.Context, + feature_service: str, + reference: str, + start_ts: str, + end_ts: str, + no_profile_cache, +): + """ + Perform validation of logged features (produced by a given feature service) against provided reference. + + START_TS and END_TS should be in ISO 8601 format, e.g. '2021-07-16T19:20:01' + """ + store = create_feature_store(ctx) + + _feature_service = store.get_feature_service(name=feature_service) + _reference = store.get_validation_reference(reference) + + result = store.validate_logged_features( + source=_feature_service, + reference=_reference, + start=maybe_local_tz(datetime.fromisoformat(start_ts)), + end=maybe_local_tz(datetime.fromisoformat(end_ts)), + throw_exception=False, + cache_profile=not no_profile_cache, + ) + + if not result: + print(f"{Style.BRIGHT + Fore.GREEN}Validation successful!{Style.RESET_ALL}") + return + + errors = [e.to_dict() for e in result.report.errors] + formatted_json = json.dumps(errors, indent=4) + colorful_json = highlight( + formatted_json, lexers.JsonLexer(), formatters.TerminalFormatter() + ) + print(f"{Style.BRIGHT + Fore.RED}Validation failed!{Style.RESET_ALL}") + print(colorful_json) + exit(1) + + +@cli.command("demo-notebooks") +@click.option( + "--output-dir", + "-o", + default="./feast-demo-notebooks", + show_default=True, + help="Directory where the demo notebooks are written.", +) +@click.option( + "--overwrite", + is_flag=True, + default=False, + help="Overwrite existing notebooks if the output directory already exists.", +) +@click.pass_context +def demo_notebooks_command(ctx: click.Context, output_dir: str, overwrite: bool): + """ + Generate demo Jupyter notebooks tailored to the feature store configuration. + + Searches for feature_store.yaml in the current directory and every file + inside feast-config/. Each file is treated as a separate project config. + For each project found, a sub-directory is created under OUTPUT_DIR. + """ + from feast.demos import copy_demo_notebooks + + repo = ctx.obj["CHDIR"] + copy_demo_notebooks( + output_dir=output_dir, + repo_path=str(repo), + overwrite=overwrite, + ) + + +cli.add_command(data_sources_cmd) +cli.add_command(entities_cmd) +cli.add_command(feature_services_cmd) +cli.add_command(feature_views_cmd) +cli.add_command(features_cmd) +cli.add_command(get_historical_features) +cli.add_command(get_online_features) +cli.add_command(on_demand_feature_views_cmd) +cli.add_command(feast_permissions_cmd) +cli.add_command(projects_cmd) +cli.add_command(registry_cmd) +cli.add_command(saved_datasets_cmd) +cli.add_command(stream_feature_views_cmd) +cli.add_command(label_views_cmd) +cli.add_command(validation_references_cmd) +cli.add_command(ui) +cli.add_command(serve_command) +cli.add_command(serve_offline_command) +cli.add_command(serve_registry_command) +cli.add_command(serve_transformations_command) +cli.add_command(dbt_cmd) +cli.add_command(monitor_cmd) + +if __name__ == "__main__": + cli() diff --git a/sdk/python/feast/cli/dbt_import.py b/sdk/python/feast/cli/dbt_import.py new file mode 100644 index 00000000000..16bd7812101 --- /dev/null +++ b/sdk/python/feast/cli/dbt_import.py @@ -0,0 +1,407 @@ +""" +CLI commands for importing dbt models as Feast features. + +This module provides the `feast dbt` command group for integrating +dbt models with Feast feature stores. +""" + +from typing import Any, Dict, List, Optional + +import click +from colorama import Fore, Style + +from feast.repo_operations import cli_check_repo, create_feature_store + + +@click.group(name="dbt") +def dbt_cmd(): + """Import dbt models as Feast features.""" + pass + + +@dbt_cmd.command("import") +@click.option( + "--manifest-path", + "-m", + required=True, + type=click.Path(exists=True), + help="Path to dbt manifest.json file (typically target/manifest.json)", +) +@click.option( + "--entity-column", + "-e", + "entity_columns", + multiple=True, + required=True, + help="Entity column name (can be specified multiple times, e.g., -e user_id -e merchant_id)", +) +@click.option( + "--data-source-type", + "-d", + type=click.Choice(["bigquery", "snowflake", "file"]), + default="bigquery", + show_default=True, + help="Type of data source to create", +) +@click.option( + "--timestamp-field", + "-t", + default="event_timestamp", + show_default=True, + help="Timestamp field name for point-in-time joins", +) +@click.option( + "--tag", + "tag_filter", + default=None, + help="Only import models with this dbt tag (e.g., --tag feast)", +) +@click.option( + "--model", + "model_names", + multiple=True, + help="Specific model names to import (can be specified multiple times)", +) +@click.option( + "--ttl-days", + type=int, + default=1, + show_default=True, + help="TTL (time-to-live) in days for feature views", +) +@click.option( + "--dry-run", + is_flag=True, + default=False, + help="Preview what would be created without applying changes", +) +@click.option( + "--exclude-columns", + default=None, + help="Comma-separated list of columns to exclude from features", +) +@click.option( + "--output", + "-o", + type=click.Path(), + default=None, + help="Output Python file path (e.g., features.py). Generates code instead of applying to registry.", +) +@click.pass_context +def import_command( + ctx: click.Context, + manifest_path: str, + entity_columns: tuple, + data_source_type: str, + timestamp_field: str, + tag_filter: Optional[str], + model_names: tuple, + ttl_days: int, + dry_run: bool, + exclude_columns: Optional[str], + output: Optional[str], +): + """ + Import dbt models as Feast FeatureViews. + + This command parses a dbt manifest.json file and creates corresponding + Feast DataSource and FeatureView objects. + + Examples: + + # Import all models with 'feast' tag + feast dbt import -m target/manifest.json -e driver_id --tag feast + + # Import specific models + feast dbt import -m target/manifest.json -e customer_id --model orders --model customers + + # Dry run to preview changes + feast dbt import -m target/manifest.json -e driver_id --tag feast --dry-run + + # Generate Python file instead of applying to registry + feast dbt import -m target/manifest.json -e driver_id --tag feast --output features.py + """ + from feast.dbt.mapper import DbtToFeastMapper + from feast.dbt.parser import DbtManifestParser + + # Parse manifest + click.echo(f"{Fore.CYAN}Parsing dbt manifest: {manifest_path}{Style.RESET_ALL}") + + try: + parser = DbtManifestParser(manifest_path) + parser.parse() + except (FileNotFoundError, ValueError, ImportError) as e: + click.echo(f"{Fore.RED}Error: {e}{Style.RESET_ALL}", err=True) + raise SystemExit(1) + + # Display manifest info + if parser.dbt_version: + click.echo(f" dbt version: {parser.dbt_version}") + if parser.project_name: + click.echo(f" Project: {parser.project_name}") + + # Convert tuple to list and validate + entity_cols: List[str] = list(entity_columns) if entity_columns else [] + + # Validation: At least one entity required (redundant with required=True but explicit) + if not entity_cols: + click.echo( + f"{Fore.RED}Error: At least one entity column required{Style.RESET_ALL}", + err=True, + ) + raise SystemExit(1) + + # Validation: No duplicate entity columns + if len(entity_cols) != len(set(entity_cols)): + duplicates = [col for col in entity_cols if entity_cols.count(col) > 1] + click.echo( + f"{Fore.RED}Error: Duplicate entity columns: {', '.join(set(duplicates))}{Style.RESET_ALL}", + err=True, + ) + raise SystemExit(1) + + click.echo(f"Entity columns: {', '.join(entity_cols)}") + + # Get models with filters + model_list: Optional[List[str]] = list(model_names) if model_names else None + models = parser.get_models(model_names=model_list, tag_filter=tag_filter) + + if not models: + click.echo( + f"{Fore.YELLOW}No models found matching the criteria.{Style.RESET_ALL}" + ) + if tag_filter: + click.echo(f" Tag filter: {tag_filter}") + if model_names: + click.echo(f" Model names: {', '.join(model_names)}") + raise SystemExit(0) + + click.echo(f"{Fore.GREEN}Found {len(models)} model(s) to import:{Style.RESET_ALL}") + for model in models: + tags_str = f" [tags: {', '.join(model.tags)}]" if model.tags else "" + click.echo(f" - {model.name} ({len(model.columns)} columns){tags_str}") + + # Parse exclude columns + excluded: Optional[List[str]] = None + if exclude_columns: + excluded = [c.strip() for c in exclude_columns.split(",")] + + # Create mapper + mapper = DbtToFeastMapper( + data_source_type=data_source_type, + timestamp_field=timestamp_field, + ttl_days=ttl_days, + ) + + # Generate Feast objects + click.echo(f"\n{Fore.CYAN}Generating Feast objects...{Style.RESET_ALL}") + + all_objects: List[Any] = [] + entities_created: Dict[str, Any] = {} + + for model in models: + # Validate timestamp field exists + column_names = [c.name for c in model.columns] + if timestamp_field not in column_names: + click.echo( + f"{Fore.YELLOW}Warning: Model '{model.name}' missing timestamp " + f"field '{timestamp_field}'. Skipping.{Style.RESET_ALL}" + ) + continue + + # Validate ALL entity columns exist + missing_entities = [e for e in entity_cols if e not in column_names] + if missing_entities: + click.echo( + f"{Fore.YELLOW}Warning: Model '{model.name}' missing entity " + f"column(s): {', '.join(missing_entities)}. Skipping.{Style.RESET_ALL}" + ) + continue + + # Create or reuse entities (one per entity column) + model_entities: List[Any] = [] + for entity_col in entity_cols: + if entity_col not in entities_created: + # Use mapper's internal method for value type inference + entity_value_type = mapper._infer_entity_value_type(model, entity_col) + entity = mapper.create_entity( + name=entity_col, + description="Entity key for dbt models", + value_type=entity_value_type, + ) + entities_created[entity_col] = entity + all_objects.append(entity) + else: + entity = entities_created[entity_col] + model_entities.append(entity) + + # Create data source + data_source = mapper.create_data_source( + model=model, + timestamp_field=timestamp_field, + ) + all_objects.append(data_source) + + # Create feature view + feature_view = mapper.create_feature_view( + model=model, + source=data_source, + entity_columns=entity_cols, + entities=model_entities, + timestamp_field=timestamp_field, + ttl_days=ttl_days, + exclude_columns=excluded, + ) + all_objects.append(feature_view) + + click.echo( + f" {Fore.GREEN}✓{Style.RESET_ALL} {model.name}: " + f"DataSource + FeatureView ({len(feature_view.features)} features)" + ) + + if not all_objects: + click.echo( + f"{Fore.YELLOW}No valid models to import (check warnings above).{Style.RESET_ALL}" + ) + raise SystemExit(0) + + # Filter models that were actually processed (have valid columns) + valid_models = [ + m + for m in models + if timestamp_field in [c.name for c in m.columns] + and all(e in [c.name for c in m.columns] for e in entity_cols) + ] + + # Summary + click.echo(f"\n{Fore.CYAN}Summary:{Style.RESET_ALL}") + click.echo(f" Entities: {len(entities_created)}") + click.echo(f" DataSources: {len(valid_models)}") + click.echo(f" FeatureViews: {len(valid_models)}") + + # Generate Python file if --output specified + if output: + from feast.dbt.codegen import generate_feast_code + + code = generate_feast_code( + models=valid_models, + entity_columns=entity_cols, + data_source_type=data_source_type, + timestamp_field=timestamp_field, + ttl_days=ttl_days, + manifest_path=manifest_path, + project_name=parser.project_name or "", + exclude_columns=excluded, + online=True, + ) + + with open(output, "w") as f: + f.write(code) + + click.echo( + f"\n{Fore.GREEN}✓ Generated Feast definitions: {output}{Style.RESET_ALL}" + ) + click.echo(" You can now import this file in your feature_store.yaml repo.") + return + + if dry_run: + click.echo(f"\n{Fore.YELLOW}Dry run - no changes applied.{Style.RESET_ALL}") + click.echo("Remove --dry-run flag to apply changes.") + return + + # Apply to Feast + click.echo(f"\n{Fore.CYAN}Applying to Feast registry...{Style.RESET_ALL}") + + repo = ctx.obj["CHDIR"] + fs_yaml_file = ctx.obj["FS_YAML_FILE"] + cli_check_repo(repo, fs_yaml_file) + store = create_feature_store(ctx) + + store.apply(all_objects) + + click.echo( + f"{Fore.GREEN}✓ Successfully imported {len(valid_models)} dbt model(s) " + f"to Feast project '{store.project}'{Style.RESET_ALL}" + ) + + +@dbt_cmd.command("list") +@click.option( + "--manifest-path", + "-m", + required=True, + type=click.Path(exists=True), + help="Path to dbt manifest.json file", +) +@click.option( + "--tag", + "tag_filter", + default=None, + help="Filter models by dbt tag", +) +@click.option( + "--show-columns", + is_flag=True, + default=False, + help="Show column details for each model", +) +def list_command( + manifest_path: str, + tag_filter: Optional[str], + show_columns: bool, +): + """ + List dbt models available for import. + + Examples: + + # List all models + feast dbt list -m target/manifest.json + + # List models with specific tag + feast dbt list -m target/manifest.json --tag feast + + # Show column details + feast dbt list -m target/manifest.json --show-columns + """ + from feast.dbt.parser import DbtManifestParser + + click.echo(f"{Fore.CYAN}Parsing dbt manifest: {manifest_path}{Style.RESET_ALL}") + + try: + parser = DbtManifestParser(manifest_path) + parser.parse() + except (FileNotFoundError, ValueError, ImportError) as e: + click.echo(f"{Fore.RED}Error: {e}{Style.RESET_ALL}", err=True) + raise SystemExit(1) + + if parser.dbt_version: + click.echo(f" dbt version: {parser.dbt_version}") + if parser.project_name: + click.echo(f" Project: {parser.project_name}") + + models = parser.get_models(tag_filter=tag_filter) + + if not models: + click.echo(f"{Fore.YELLOW}No models found.{Style.RESET_ALL}") + return + + click.echo(f"\n{Fore.GREEN}Found {len(models)} model(s):{Style.RESET_ALL}\n") + + for model in models: + tags_str = f" [tags: {', '.join(model.tags)}]" if model.tags else "" + click.echo(f"{Fore.CYAN}{model.name}{Style.RESET_ALL}{tags_str}") + click.echo(f" Table: {model.full_table_name}") + if model.description: + desc = model.description[:80] + ( + "..." if len(model.description) > 80 else "" + ) + click.echo(f" Description: {desc}") + + if show_columns and model.columns: + click.echo(f" Columns ({len(model.columns)}):") + for col in model.columns: + type_str = col.data_type or "unknown" + click.echo(f" - {col.name}: {type_str}") + + click.echo() diff --git a/sdk/python/feast/cli/feature_views.py b/sdk/python/feast/cli/feature_views.py index a1a29ac9f27..44357b0c1c1 100644 --- a/sdk/python/feast/cli/feature_views.py +++ b/sdk/python/feast/cli/feature_views.py @@ -1,10 +1,16 @@ +import sys + import click import yaml from feast import utils from feast.cli.cli_options import tagsOption from feast.errors import FeastObjectNotFoundException -from feast.feature_view import FeatureView +from feast.feature_view import ( + _VALID_STATE_TRANSITIONS, + FeatureView, + FeatureViewState, +) from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_operations import create_feature_store @@ -32,11 +38,13 @@ def feature_view_describe(ctx: click.Context, name: str): print(e) exit(1) - print( - yaml.dump( - yaml.safe_load(str(feature_view)), default_flow_style=False, sort_keys=False - ) - ) + data = yaml.safe_load(str(feature_view)) + # Always show enabled and state even when they are at default values. + if hasattr(feature_view, "enabled"): + data["enabled"] = feature_view.enabled + if hasattr(feature_view, "state"): + data["state"] = feature_view.state.name + print(yaml.dump(data, default_flow_style=False, sort_keys=False)) @feature_views_cmd.command(name="list") @@ -59,14 +67,170 @@ def feature_view_list(ctx: click.Context, tags: list[str]): elif isinstance(feature_view, OnDemandFeatureView): for backing_fv in feature_view.source_feature_view_projections.values(): entities.update(store.get_feature_view(backing_fv.name).entities) + enabled = getattr(feature_view, "enabled", True) + state = getattr(feature_view, "state", FeatureViewState.STATE_UNSPECIFIED) + state_display = ( + state.name if isinstance(state, FeatureViewState) else str(state) + ) table.append( [ feature_view.name, entities if len(entities) > 0 else "n/a", type(feature_view).__name__, + "Yes" if enabled else "No", + state_display, + ] + ) + + from tabulate import tabulate + + print( + tabulate( + table, + headers=["NAME", "ENTITIES", "TYPE", "ENABLED", "STATE"], + tablefmt="plain", + ) + ) + + +@feature_views_cmd.command("enable") +@click.argument("name", type=click.STRING) +@click.pass_context +def feature_view_enable(ctx: click.Context, name: str): + """ + Enable a feature view for serving and materialization. + """ + store = create_feature_store(ctx) + try: + fv = store.registry.get_any_feature_view(name, store.project) + except FeastObjectNotFoundException as e: + print(e) + sys.exit(1) + + if not isinstance(fv, (FeatureView, OnDemandFeatureView)): + print(f"Feature view '{name}' does not support enable/disable.") + return + + if fv.enabled: + print(f"Feature view '{name}' is already enabled.") + return + + fv.enabled = True + store.registry.apply_feature_view(fv, store.project) + print(f"Feature view '{name}' has been enabled.") + + +@feature_views_cmd.command("disable") +@click.argument("name", type=click.STRING) +@click.pass_context +def feature_view_disable(ctx: click.Context, name: str): + """ + Disable a feature view to prevent serving and materialization. + """ + store = create_feature_store(ctx) + try: + fv = store.registry.get_any_feature_view(name, store.project) + except FeastObjectNotFoundException as e: + print(e) + sys.exit(1) + + if not isinstance(fv, (FeatureView, OnDemandFeatureView)): + print(f"Feature view '{name}' does not support enable/disable.") + return + + if not fv.enabled: + print(f"Feature view '{name}' is already disabled.") + return + + fv.enabled = False + store.registry.apply_feature_view(fv, store.project) + print(f"Feature view '{name}' has been disabled.") + + +@feature_views_cmd.command("set-state") +@click.argument("name", type=click.STRING) +@click.argument( + "state", + type=click.Choice( + ["CREATED", "GENERATED", "MATERIALIZING", "AVAILABLE_ONLINE"], + case_sensitive=False, + ), +) +@click.pass_context +def feature_view_set_state(ctx: click.Context, name: str, state: str): + """ + Set the lifecycle state of a feature view. + """ + store = create_feature_store(ctx) + try: + fv = store.registry.get_any_feature_view(name, store.project) + except FeastObjectNotFoundException as e: + print(e) + sys.exit(1) + + if not isinstance(fv, (FeatureView, OnDemandFeatureView)): + print(f"Feature view '{name}' does not support state management.") + return + + new_state = FeatureViewState[state.upper()] + if fv.state == new_state: + print(f"Feature view '{name}' is already in state {new_state.name}.") + return + + if not fv.state.can_transition_to(new_state): + current = fv.state.name + allowed = _VALID_STATE_TRANSITIONS.get(fv.state, set()) + allowed_names = ", ".join(sorted(s.name for s in allowed)) or "none" + print( + f"Invalid state transition: {current} -> {new_state.name}. " + f"Allowed transitions from {current}: {allowed_names}." + ) + return + + fv.state = new_state + store.registry.apply_feature_view(fv, store.project) + print(f"Feature view '{name}' state set to {new_state.name}.") + + +@feature_views_cmd.command("list-versions") +@click.argument("name", type=click.STRING) +@click.pass_context +def feature_view_versions(ctx: click.Context, name: str): + """ + List version history for a feature view + """ + store = create_feature_store(ctx) + + try: + versions = store.list_feature_view_versions(name) + except NotImplementedError: + print("Version history is not supported by this registry backend.") + exit(1) + except Exception as e: + print(e) + exit(1) + + if not versions: + print(f"No version history found for feature view '{name}'.") + return + + table = [] + for v in versions: + table.append( + [ + v["version"], + v["feature_view_type"], + str(v["created_timestamp"]), + v["version_id"], ] ) from tabulate import tabulate - print(tabulate(table, headers=["NAME", "ENTITIES", "TYPE"], tablefmt="plain")) + print( + tabulate( + table, + headers=["VERSION", "TYPE", "CREATED", "VERSION_ID"], + tablefmt="plain", + ) + ) diff --git a/sdk/python/feast/cli/label_views.py b/sdk/python/feast/cli/label_views.py new file mode 100644 index 00000000000..232a2fbce21 --- /dev/null +++ b/sdk/python/feast/cli/label_views.py @@ -0,0 +1,69 @@ +import click +import yaml + +from feast import utils +from feast.cli.cli_options import tagsOption +from feast.errors import FeastObjectNotFoundException +from feast.repo_operations import create_feature_store + + +@click.group(name="label-views") +def label_views_cmd(): + """ + Access label views + """ + pass + + +@label_views_cmd.command("describe") +@click.argument("name", type=click.STRING) +@click.pass_context +def label_view_describe(ctx: click.Context, name: str): + """ + Describe a label view + """ + store = create_feature_store(ctx) + + try: + label_view = store.get_label_view(name) + except FeastObjectNotFoundException as e: + print(e) + exit(1) + + print( + yaml.dump( + yaml.safe_load(str(label_view)), + default_flow_style=False, + sort_keys=False, + ) + ) + + +@label_views_cmd.command(name="list") +@tagsOption +@click.pass_context +def label_view_list(ctx: click.Context, tags: list[str]): + """ + List all label views + """ + store = create_feature_store(ctx) + table = [] + tags_filter = utils.tags_list_to_dict(tags) + for label_view in store.list_label_views(tags=tags_filter): + table.append( + [ + label_view.name, + ", ".join(label_view.entities) if label_view.entities else "n/a", + str(label_view.conflict_policy.value), + ] + ) + + from tabulate import tabulate + + print( + tabulate( + table, + headers=["NAME", "ENTITIES", "CONFLICT_POLICY"], + tablefmt="plain", + ) + ) diff --git a/sdk/python/feast/cli/monitor.py b/sdk/python/feast/cli/monitor.py new file mode 100644 index 00000000000..f2f645ad1be --- /dev/null +++ b/sdk/python/feast/cli/monitor.py @@ -0,0 +1,225 @@ +from datetime import date +from typing import List, Optional + +import click + +from feast.infra.offline_stores.offline_store import OfflineStore +from feast.repo_operations import create_feature_store + +VALID_GRANULARITIES = OfflineStore.MONITORING_VALID_GRANULARITIES + + +@click.group(name="monitor") +def monitor_cmd(): + """Feature monitoring commands.""" + pass + + +@monitor_cmd.command("run") +@click.option( + "--project", + "-p", + default=None, + help="Feast project name. Defaults to the project in feature_store.yaml.", +) +@click.option( + "--feature-view", + "-v", + default=None, + help="Feature view name. If omitted, all feature views are computed.", +) +@click.option( + "--feature-name", + "-f", + multiple=True, + help="Feature name(s) to compute. Can be specified multiple times.", +) +@click.option( + "--start-date", + default=None, + help="Start date (YYYY-MM-DD). If omitted, auto-detected from source data.", +) +@click.option( + "--end-date", + default=None, + help="End date (YYYY-MM-DD). If omitted, auto-detected from source data.", +) +@click.option( + "--granularity", + "-g", + default=None, + type=click.Choice(list(VALID_GRANULARITIES)), + help="Metric granularity. If omitted, all granularities are computed (auto mode).", +) +@click.option( + "--set-baseline", + is_flag=True, + default=False, + help="Mark this computation as the baseline for drift detection.", +) +@click.option( + "--feature-service", + "-s", + default=None, + help="Feature service name (required for --source-type log with explicit dates).", +) +@click.option( + "--source-type", + type=click.Choice(["batch", "log", "all"]), + default="batch", + help="Data source type: 'batch' (offline store), 'log' (serving logs), or 'all'.", +) +@click.pass_context +def monitor_run( + ctx: click.Context, + project: Optional[str], + feature_view: Optional[str], + feature_name: tuple, + start_date: Optional[str], + end_date: Optional[str], + granularity: Optional[str], + set_baseline: bool, + feature_service: Optional[str], + source_type: str, +): + """Compute feature quality metrics. + + Without --start-date/--end-date/--granularity, runs in auto mode: + detects date ranges from source data and computes all granularities. + + Use --source-type log to compute metrics from feature serving logs + (requires feature services with logging configured). + """ + store = create_feature_store(ctx) + + if project is None: + project = store.project + + from feast.monitoring.monitoring_service import MonitoringService + + svc = MonitoringService(store) + + auto_mode = start_date is None and end_date is None and granularity is None + feat_names: Optional[List[str]] = list(feature_name) if feature_name else None + + if source_type in ("batch", "all"): + _run_batch_monitoring( + svc, + project, + feature_view, + feat_names, + start_date, + end_date, + granularity, + set_baseline, + auto_mode, + ) + + if source_type in ("log", "all"): + _run_log_monitoring( + svc, + project, + feature_service, + start_date, + end_date, + granularity, + auto_mode, + ) + + +def _run_batch_monitoring( + svc, + project, + feature_view, + feat_names, + start_date, + end_date, + granularity, + set_baseline, + auto_mode, +): + if auto_mode and set_baseline and not start_date and not end_date: + click.echo("Computing baseline from all available source data...") + result = svc.compute_baseline( + project=project, + feature_view_name=feature_view, + feature_names=feat_names, + ) + click.echo(f"Status: {result['status']}") + click.echo(f"Features computed: {result['computed_features']}") + click.echo(f"Feature views computed: {result['computed_feature_views']}") + click.echo(f"Duration: {result['duration_ms']}ms") + click.echo("Baseline: SET") + elif auto_mode and not set_baseline: + click.echo("Auto-computing batch metrics for all granularities...") + result = svc.auto_compute( + project=project, + feature_view_name=feature_view, + ) + click.echo(f"Status: {result['status']}") + click.echo(f"Feature views computed: {result['computed_feature_views']}") + click.echo(f"Features computed: {result['computed_features']}") + click.echo(f"Granularities: {', '.join(result['granularities'])}") + click.echo(f"Duration: {result['duration_ms']}ms") + else: + start_d = date.fromisoformat(start_date) if start_date else None + end_d = date.fromisoformat(end_date) if end_date else None + + result = svc.compute_metrics( + project=project, + feature_view_name=feature_view, + feature_names=feat_names, + start_date=start_d, + end_date=end_d, + granularity=granularity or "daily", + set_baseline=set_baseline, + ) + + click.echo(f"Status: {result['status']}") + click.echo(f"Granularity: {result['granularity']}") + click.echo(f"Features computed: {result['computed_features']}") + click.echo(f"Feature views computed: {result['computed_feature_views']}") + click.echo(f"Feature services computed: {result['computed_feature_services']}") + click.echo(f"Metric dates: {', '.join(result['metric_dates'])}") + click.echo(f"Duration: {result['duration_ms']}ms") + + if set_baseline: + click.echo("Baseline: SET") + + +def _run_log_monitoring( + svc, project, feature_service_name, start_date, end_date, granularity, auto_mode +): + if auto_mode: + click.echo("Auto-computing log metrics for all granularities...") + result = svc.auto_compute_log_metrics( + project=project, + feature_service_name=feature_service_name, + ) + click.echo(f"Status: {result['status']}") + click.echo(f"Feature services computed: {result['computed_feature_services']}") + click.echo(f"Features computed: {result['computed_features']}") + click.echo(f"Granularities: {', '.join(result['granularities'])}") + click.echo(f"Duration: {result['duration_ms']}ms") + else: + if not feature_service_name: + click.echo( + "Error: --feature-service is required for log source with explicit dates." + ) + return + + start_d = date.fromisoformat(start_date) if start_date else None + end_d = date.fromisoformat(end_date) if end_date else None + + result = svc.compute_log_metrics( + project=project, + feature_service_name=feature_service_name, + start_date=start_d, + end_date=end_d, + granularity=granularity or "daily", + ) + + click.echo(f"Status: {result['status']}") + click.echo("Source: log") + click.echo(f"Features computed: {result.get('computed_features', 0)}") + click.echo(f"Duration: {result['duration_ms']}ms") diff --git a/sdk/python/feast/cli/projects.py b/sdk/python/feast/cli/projects.py index ea8d5b573a6..c552a058635 100644 --- a/sdk/python/feast/cli/projects.py +++ b/sdk/python/feast/cli/projects.py @@ -3,7 +3,7 @@ from feast import utils from feast.cli.cli_options import tagsOption -from feast.errors import FeastObjectNotFoundException +from feast.errors import FeastObjectNotFoundException, ProjectNotFoundException from feast.repo_operations import create_feature_store @@ -23,16 +23,16 @@ def project_describe(ctx: click.Context, name: str): Describe a project """ store = create_feature_store(ctx) - try: project = store.get_project(name) except FeastObjectNotFoundException as e: print(e) exit(1) - print( yaml.dump( - yaml.safe_load(str(project)), default_flow_style=False, sort_keys=False + yaml.safe_load(str(project)), + default_flow_style=False, + sort_keys=False, ) ) @@ -44,20 +44,52 @@ def project_current(ctx: click.Context): Returns the current project configured with FeatureStore object """ store = create_feature_store(ctx) - try: project = store.get_project(name=None) except FeastObjectNotFoundException as e: print(e) exit(1) - print( yaml.dump( - yaml.safe_load(str(project)), default_flow_style=False, sort_keys=False + yaml.safe_load(str(project)), + default_flow_style=False, + sort_keys=False, ) ) +@projects_cmd.command("delete") +@click.argument("name", type=click.STRING) +@click.option( + "-y", + "--yes", + is_flag=True, + default=False, + help="Skip confirmation prompt and delete immediately.", +) +@click.pass_context +def project_delete(ctx: click.Context, name: str, yes: bool): + """ + Delete a project and all its resources from the registry. + """ + store = create_feature_store(ctx) + + if not yes: + click.confirm( + f"Are you sure you want to delete project '{name}'? " + "This will remove all associated resources from the registry.", + abort=True, + ) + + try: + store.delete_project(name) + except (FeastObjectNotFoundException, ProjectNotFoundException) as e: + print(str(e)) + raise SystemExit(1) + + print(f"Project '{name}' deleted successfully.") + + @projects_cmd.command(name="list") @tagsOption @click.pass_context @@ -70,11 +102,12 @@ def project_list(ctx: click.Context, tags: list[str]): tags_filter = utils.tags_list_to_dict(tags) for project in store.list_projects(tags=tags_filter): table.append([project.name, project.description, project.tags, project.owner]) - from tabulate import tabulate print( tabulate( - table, headers=["NAME", "DESCRIPTION", "TAGS", "OWNER"], tablefmt="plain" + table, + headers=["NAME", "DESCRIPTION", "TAGS", "OWNER"], + tablefmt="plain", ) ) diff --git a/sdk/python/feast/cli/registry.py b/sdk/python/feast/cli/registry.py new file mode 100644 index 00000000000..063e841f5d2 --- /dev/null +++ b/sdk/python/feast/cli/registry.py @@ -0,0 +1,46 @@ +import click +from sqlalchemy import create_engine + +from feast.infra.registry.sql import SqlRegistryConfig, metadata +from feast.repo_config import load_repo_config +from feast.repo_operations import cli_check_repo + + +@click.group(name="registry") +def registry_cmd() -> None: + """ + Manage the feature registry + """ + pass + + +@registry_cmd.command("create-schema") +@click.pass_context +def registry_create_schema(ctx: click.Context) -> None: + """ + Pre-create the SQL registry schema. + + Use this when schema_mode is set to 'verify' or 'skip' so the application + does not need DDL privileges at runtime. + """ + repo = ctx.obj["CHDIR"] + fs_yaml_file = ctx.obj["FS_YAML_FILE"] + cli_check_repo(repo, fs_yaml_file) + repo_config = load_repo_config(repo, fs_yaml_file) + + if repo_config is None: + raise click.ClickException("Could not load feature_store.yaml") + + registry_config = repo_config.registry + if not isinstance(registry_config, SqlRegistryConfig): + raise click.ClickException( + "This command only applies to SQL-based registries " + f"(registry_type='sql'). Current type: '{registry_config.registry_type}'" + ) + + engine = create_engine( + registry_config.path, **registry_config.sqlalchemy_config_kwargs + ) + metadata.create_all(engine) + engine.dispose() + click.echo("SQL registry schema created successfully.") diff --git a/sdk/python/feast/cli/serve.py b/sdk/python/feast/cli/serve.py index b5ff950a042..b3b981e9dbf 100644 --- a/sdk/python/feast/cli/serve.py +++ b/sdk/python/feast/cli/serve.py @@ -52,21 +52,42 @@ type=click.INT, default=1, show_default=True, - help="Number of worker", + help="Number of worker processes. Use -1 to auto-calculate based on CPU cores", +) +@click.option( + "--worker-connections", + type=click.INT, + default=1000, + show_default=True, + help="Maximum number of simultaneous clients per worker process", +) +@click.option( + "--max-requests", + type=click.INT, + default=1000, + show_default=True, + help="Maximum number of requests a worker will process before restarting (prevents memory leaks)", +) +@click.option( + "--max-requests-jitter", + type=click.INT, + default=50, + show_default=True, + help="Maximum jitter to add to max-requests to prevent thundering herd on worker restart", ) @click.option( "--keep-alive-timeout", type=click.INT, - default=5, + default=30, show_default=True, - help="Timeout for keep alive", + help="Timeout for keep alive connections (seconds)", ) @click.option( "--registry_ttl_sec", "-r", - help="Number of seconds after which the registry is refreshed", + help="Number of seconds after which the registry is refreshed. Higher values reduce refresh overhead but increase staleness", type=click.INT, - default=5, + default=60, show_default=True, ) @click.option( @@ -102,11 +123,14 @@ def serve_command( type_: str, no_access_log: bool, workers: int, - metrics: bool, + worker_connections: int, + max_requests: int, + max_requests_jitter: int, keep_alive_timeout: int, + registry_ttl_sec: int, tls_key_path: str, tls_cert_path: str, - registry_ttl_sec: int = 5, + metrics: bool, ): """Start a feature server locally on a given port.""" if (tls_key_path and not tls_cert_path) or (not tls_key_path and tls_cert_path): @@ -115,12 +139,19 @@ def serve_command( ) store = create_feature_store(ctx) + # Auto-calculate workers if -1 is specified + if workers == -1: + workers = max(1, (multiprocessing.cpu_count() * 2) + 1) + store.serve( host=host, port=port, type_=type_, no_access_log=no_access_log, workers=workers, + worker_connections=worker_connections, + max_requests=max_requests, + max_requests_jitter=max_requests_jitter, metrics=metrics, keep_alive_timeout=keep_alive_timeout, tls_key_path=tls_key_path, diff --git a/sdk/python/feast/cli/ui.py b/sdk/python/feast/cli/ui.py index 9fd7b24b7cd..6962eea1e51 100644 --- a/sdk/python/feast/cli/ui.py +++ b/sdk/python/feast/cli/ui.py @@ -1,6 +1,6 @@ import click -from feast.repo_operations import create_feature_store, registry_dump +from feast.repo_operations import create_feature_store @click.command() @@ -20,14 +20,6 @@ show_default=True, help="Specify a port for the server", ) -@click.option( - "--registry_ttl_sec", - "-r", - help="Number of seconds after which the registry is refreshed", - type=click.INT, - default=5, - show_default=True, -) @click.option( "--root_path", help="Provide root path to make the UI working behind proxy", @@ -57,7 +49,6 @@ def ui( ctx: click.Context, host: str, port: int, - registry_ttl_sec: int, root_path: str = "", tls_key_path: str = "", tls_cert_path: str = "", @@ -70,12 +61,9 @@ def ui( "Please configure --key and --cert args to start the feature server in SSL mode." ) store = create_feature_store(ctx) - # Pass in the registry_dump method to get around a circular dependency store.serve_ui( host=host, port=port, - get_registry_dump=registry_dump, - registry_ttl_sec=registry_ttl_sec, root_path=root_path, tls_key_path=tls_key_path, tls_cert_path=tls_cert_path, diff --git a/sdk/python/feast/constants.py b/sdk/python/feast/constants.py index 240dda3311b..a687b91cf19 100644 --- a/sdk/python/feast/constants.py +++ b/sdk/python/feast/constants.py @@ -46,3 +46,12 @@ # Default feature server registry ttl (seconds) DEFAULT_FEATURE_SERVER_REGISTRY_TTL = 5 + +# Tag key set on Feast projects that are protected. +# Protected projects are excluded from project listings, +# shielded from teardown, and hidden from delete operations. +PROTECTED_PROJECT_TAG = "feast.dev/protected-project" + +# Environment variable set by the operator on protected project pods. +# When "true", the Feast server tags its own project as protected. +FEAST_PROTECTED_PROJECT_ENV = "FEAST_PROTECTED_PROJECT" diff --git a/sdk/python/feast/data_format.py b/sdk/python/feast/data_format.py index 301dfb81302..b58af65ca99 100644 --- a/sdk/python/feast/data_format.py +++ b/sdk/python/feast/data_format.py @@ -17,6 +17,7 @@ from feast.protos.feast.core.DataFormat_pb2 import FileFormat as FileFormatProto from feast.protos.feast.core.DataFormat_pb2 import StreamFormat as StreamFormatProto +from feast.protos.feast.core.DataFormat_pb2 import TableFormat as TableFormatProto class FileFormat(ABC): @@ -32,6 +33,8 @@ def to_proto(self): pass def __eq__(self, other): + if not isinstance(other, FileFormat): + return False return self.to_proto() == other.to_proto() @classmethod @@ -70,11 +73,12 @@ def __str__(self): class DeltaFormat(FileFormat): """ - Defines delta data format + Defines delta data format (deprecated - use TableFormat.DeltaFormat instead) """ def to_proto(self): - return FileFormatProto(delta_format=FileFormatProto.DeltaFormat()) + # Reference TableFormat.DeltaFormat since DeltaFormat is now nested there + return FileFormatProto(delta_format=TableFormatProto.DeltaFormat()) def __str__(self): return "delta" @@ -93,6 +97,8 @@ def to_proto(self): pass def __eq__(self, other): + if not isinstance(other, StreamFormat): + return False return self.to_proto() == other.to_proto() @classmethod diff --git a/sdk/python/feast/data_source.py b/sdk/python/feast/data_source.py index 2d4997ae786..6981ec03abd 100644 --- a/sdk/python/feast/data_source.py +++ b/sdk/python/feast/data_source.py @@ -157,6 +157,7 @@ def to_proto(self) -> DataSourceProto.KinesisOptions: DataSourceProto.SourceType.BATCH_TRINO: "feast.infra.offline_stores.contrib.trino_offline_store.trino_source.TrinoSource", DataSourceProto.SourceType.BATCH_SPARK: "feast.infra.offline_stores.contrib.spark_offline_store.spark_source.SparkSource", DataSourceProto.SourceType.BATCH_ATHENA: "feast.infra.offline_stores.contrib.athena_offline_store.athena_source.AthenaSource", + DataSourceProto.SourceType.BATCH_ICEBERG: "feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source.IcebergSource", DataSourceProto.SourceType.STREAM_KAFKA: "feast.data_source.KafkaSource", DataSourceProto.SourceType.STREAM_KINESIS: "feast.data_source.KinesisSource", DataSourceProto.SourceType.REQUEST_SOURCE: "feast.data_source.RequestSource", @@ -205,6 +206,7 @@ class DataSource(ABC): tags: Dict[str, str] owner: str date_partition_column: str + timestamp_field_type: str created_timestamp: Optional[datetime] last_updated_timestamp: Optional[datetime] @@ -219,6 +221,7 @@ def __init__( tags: Optional[Dict[str, str]] = None, owner: Optional[str] = "", date_partition_column: Optional[str] = None, + timestamp_field_type: Optional[str] = None, ): """ Creates a DataSource object. @@ -237,6 +240,9 @@ def __init__( owner (optional): The owner of the data source, typically the email of the primary maintainer. date_partition_column (optional): Timestamp column used for partitioning. Not supported by all stores + timestamp_field_type (optional): Type of the timestamp_field column. + Defaults to "TIMESTAMP". Set to "DATE" when the event timestamp column + is a DATE type, so SQL generation uses date-only comparisons. """ self.name = name self.timestamp_field = timestamp_field or "" @@ -257,6 +263,7 @@ def __init__( self.date_partition_column = ( date_partition_column if date_partition_column else "" ) + self.timestamp_field_type = timestamp_field_type if timestamp_field_type else "" now = _utc_now() self.created_timestamp = now self.last_updated_timestamp = now @@ -272,7 +279,7 @@ def __eq__(self, other): return False if not isinstance(other, DataSource): - raise TypeError("Comparisons should only involve DataSource class objects.") + return False if ( self.name != other.name @@ -280,6 +287,7 @@ def __eq__(self, other): or self.created_timestamp_column != other.created_timestamp_column or self.field_mapping != other.field_mapping or self.date_partition_column != other.date_partition_column + or self.timestamp_field_type != other.timestamp_field_type or self.description != other.description or self.tags != other.tags or self.owner != other.owner @@ -491,9 +499,7 @@ def __init__( def __eq__(self, other): if not isinstance(other, KafkaSource): - raise TypeError( - "Comparisons should only involve KafkaSource class objects." - ) + return False if not super().__eq__(other): return False @@ -631,9 +637,7 @@ def get_table_column_names_and_types( def __eq__(self, other): if not isinstance(other, RequestSource): - raise TypeError( - "Comparisons should only involve RequestSource class objects." - ) + return False if not super().__eq__(other): return False @@ -793,9 +797,7 @@ def __init__( def __eq__(self, other): if not isinstance(other, KinesisSource): - raise TypeError( - "Comparisons should only involve KinesisSource class objects." - ) + return False if not super().__eq__(other): return False diff --git a/sdk/python/feast/dataset_job_manager.py b/sdk/python/feast/dataset_job_manager.py new file mode 100644 index 00000000000..4fd97a5a7e1 --- /dev/null +++ b/sdk/python/feast/dataset_job_manager.py @@ -0,0 +1,136 @@ +import threading +import uuid +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field +from datetime import datetime, timezone +from enum import Enum +from typing import Any, Callable, Dict, List, Optional + + +class JobStatus(str, Enum): + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + + +@dataclass +class DatasetJob: + job_id: str + name: str + project: str + status: JobStatus = JobStatus.PENDING + created_at: str = "" + completed_at: Optional[str] = None + error: Optional[str] = None + metadata: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + return { + "job_id": self.job_id, + "dataset_name": self.name, + "project": self.project, + "status": self.status.value, + "created_at": self.created_at, + "completed_at": self.completed_at, + "error": self.error, + } + + +class DatasetJobManager: + """Thread-safe in-memory job manager for dataset creation jobs.""" + + def __init__(self, max_workers: int = 2): + self._jobs: Dict[str, DatasetJob] = {} + self._lock = threading.Lock() + self._executor = ThreadPoolExecutor( + max_workers=max_workers, thread_name_prefix="dataset-job" + ) + + def submit_job( + self, + name: str, + project: str, + task_fn: Callable[[DatasetJob], None], + metadata: Optional[Dict[str, Any]] = None, + ) -> DatasetJob: + job_id = str(uuid.uuid4()) + job = DatasetJob( + job_id=job_id, + name=name, + project=project, + status=JobStatus.PENDING, + created_at=datetime.now(timezone.utc).isoformat(), + metadata=metadata or {}, + ) + + with self._lock: + self._jobs[job_id] = job + + self._executor.submit(self._run_job, job, task_fn) + return job + + def _run_job(self, job: DatasetJob, task_fn: Callable[[DatasetJob], None]): + with self._lock: + job.status = JobStatus.RUNNING + + try: + task_fn(job) + with self._lock: + job.status = JobStatus.COMPLETED + job.completed_at = datetime.now(timezone.utc).isoformat() + except Exception as e: + with self._lock: + job.status = JobStatus.FAILED + job.completed_at = datetime.now(timezone.utc).isoformat() + job.error = str(e) + + def get_job(self, job_id: str) -> Optional[DatasetJob]: + with self._lock: + return self._jobs.get(job_id) + + def list_jobs( + self, + project: Optional[str] = None, + status: Optional[JobStatus] = None, + ) -> List[DatasetJob]: + with self._lock: + jobs = list(self._jobs.values()) + + if project: + jobs = [j for j in jobs if j.project == project] + if status: + jobs = [j for j in jobs if j.status == status] + + return sorted(jobs, key=lambda j: j.created_at, reverse=True) + + def cleanup_completed(self, max_age_seconds: int = 3600): + """Remove completed/failed jobs older than max_age_seconds.""" + now = datetime.now(timezone.utc) + with self._lock: + to_remove = [] + for job_id, job in self._jobs.items(): + if job.status in (JobStatus.COMPLETED, JobStatus.FAILED): + if job.completed_at: + completed = datetime.fromisoformat(job.completed_at) + if (now - completed).total_seconds() > max_age_seconds: + to_remove.append(job_id) + for job_id in to_remove: + del self._jobs[job_id] + + def shutdown(self): + self._executor.shutdown(wait=False) + + +# Singleton instance for use across the REST API +_dataset_job_manager: Optional[DatasetJobManager] = None +_manager_lock = threading.Lock() + + +def get_dataset_job_manager() -> DatasetJobManager: + global _dataset_job_manager + if _dataset_job_manager is None: + with _manager_lock: + if _dataset_job_manager is None: + _dataset_job_manager = DatasetJobManager() + return _dataset_job_manager diff --git a/sdk/python/feast/dataset_utils.py b/sdk/python/feast/dataset_utils.py new file mode 100644 index 00000000000..cea6c9330a2 --- /dev/null +++ b/sdk/python/feast/dataset_utils.py @@ -0,0 +1,191 @@ +""" +Utility functions for dataset creation and entity DataFrame construction. + +These helpers are used by the FeatureStore SDK and gRPC handlers to build +entity DataFrames from inline input and to resolve storage backend classes. +""" + +import importlib +from typing import List, Optional, Union + + +def coerce_value(val: str) -> Union[int, float, str]: + """Try to coerce a string value to int or float, otherwise keep as string.""" + try: + return int(val) + except ValueError: + pass + try: + return float(val) + except ValueError: + pass + return val + + +def build_saved_dataset_storage( + storage_type: str, path: str, file_format: Optional[str] = None +): + """Build a SavedDatasetStorage object from type string and path/table reference. + + Supports: file (default), bigquery, snowflake, redshift, spark, trino, athena. + Unknown types fall back to file storage. + """ + from feast.infra.offline_stores.file_source import SavedDatasetFileStorage + + storage_classes = {} + for mod, cls_name, key in [ + ( + "feast.infra.offline_stores.bigquery_source", + "SavedDatasetBigQueryStorage", + "bigquery", + ), + ( + "feast.infra.offline_stores.snowflake_source", + "SavedDatasetSnowflakeStorage", + "snowflake", + ), + ( + "feast.infra.offline_stores.redshift_source", + "SavedDatasetRedshiftStorage", + "redshift", + ), + ( + "feast.infra.offline_stores.contrib.spark_offline_store.spark_source", + "SavedDatasetSparkStorage", + "spark", + ), + ( + "feast.infra.offline_stores.contrib.trino_offline_store.trino_source", + "SavedDatasetTrinoStorage", + "trino", + ), + ( + "feast.infra.offline_stores.contrib.athena_offline_store.athena_source", + "SavedDatasetAthenaStorage", + "athena", + ), + ( + "feast.infra.offline_stores.contrib.postgres_offline_store.postgres_source", + "SavedDatasetPostgreSQLStorage", + "postgres", + ), + ( + "feast.infra.offline_stores.contrib.clickhouse_offline_store.clickhouse_source", + "SavedDatasetClickhouseStorage", + "clickhouse", + ), + ( + "feast.infra.offline_stores.contrib.couchbase_offline_store.couchbase_source", + "SavedDatasetCouchbaseColumnarStorage", + "couchbase", + ), + ]: + try: + m = importlib.import_module(mod) + storage_classes[key] = getattr(m, cls_name) + except (ImportError, AttributeError): + pass + + if storage_type == "bigquery" and "bigquery" in storage_classes: + return storage_classes["bigquery"](table=path) + elif storage_type == "snowflake" and "snowflake" in storage_classes: + return storage_classes["snowflake"](table_ref=path) + elif storage_type == "redshift" and "redshift" in storage_classes: + return storage_classes["redshift"](table_ref=path) + elif storage_type == "spark" and "spark" in storage_classes: + return storage_classes["spark"](path=path, file_format=file_format or "parquet") + elif storage_type == "trino" and "trino" in storage_classes: + return storage_classes["trino"](table=path) + elif storage_type == "athena" and "athena" in storage_classes: + return storage_classes["athena"](table_ref=path) + elif storage_type == "postgres" and "postgres" in storage_classes: + return storage_classes["postgres"](table_ref=path) + elif storage_type == "clickhouse" and "clickhouse" in storage_classes: + return storage_classes["clickhouse"](table_ref=path) + elif storage_type == "couchbase" and "couchbase" in storage_classes: + parts = path.split(".", 2) + database_ref = parts[0] if len(parts) > 0 else "" + scope_ref = parts[1] if len(parts) > 1 else "" + collection_ref = parts[2] if len(parts) > 2 else "" + return storage_classes["couchbase"]( + database_ref=database_ref, + scope_ref=scope_ref, + collection_ref=collection_ref, + ) + else: + return SavedDatasetFileStorage(path=path) + + +def build_entity_df_from_inline( + entity_keys: List[str], + entity_values: str, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + extra_columns: Optional[str] = None, +): + """Build a pandas DataFrame from inline entity key values and optional time range. + + Args: + entity_keys: Column names for the entity keys (e.g., ["driver_id"]). + entity_values: Comma or newline separated values string. + start_date: Optional ISO start date for event_timestamp range. + end_date: Optional ISO end date for event_timestamp range. + extra_columns: Optional newline-separated "col=value" pairs to add as + constant columns (used for ODFV request-data inputs). + + Returns: + pandas DataFrame with entity columns and event_timestamp. + + Raises: + ValueError: If no entity values could be parsed. + """ + import pandas as pd + + values_str = entity_values.strip() + rows: list = [] + + if len(entity_keys) == 1: + raw_vals = [ + v.strip() for v in values_str.replace("\n", ",").split(",") if v.strip() + ] + for val in raw_vals: + rows.append({entity_keys[0]: coerce_value(val)}) + else: + lines = [line.strip() for line in values_str.split("\n") if line.strip()] + for line in lines: + parts = [v.strip() for v in line.split(",")] + row = {} + for i, key in enumerate(entity_keys): + row[key] = coerce_value(parts[i]) if i < len(parts) else None + rows.append(row) + + if not rows: + raise ValueError("No entity values could be parsed from the input.") + + entity_df = pd.DataFrame(rows) + + if start_date and end_date: + start = pd.to_datetime(start_date) + end = pd.to_datetime(end_date) + n = len(entity_df) + if n == 1: + entity_df["event_timestamp"] = [end] + else: + timestamps = pd.date_range(start=start, end=end, periods=n) + entity_df["event_timestamp"] = timestamps + elif end_date: + entity_df["event_timestamp"] = pd.to_datetime(end_date) + else: + entity_df["event_timestamp"] = pd.Timestamp.now() + + if extra_columns: + for col_line in extra_columns.strip().split("\n"): + col_line = col_line.strip() + if "=" in col_line: + col_name, col_value = col_line.split("=", 1) + col_name = col_name.strip() + col_value = col_value.strip() + if col_name: + entity_df[col_name] = coerce_value(col_value) + + return entity_df diff --git a/sdk/python/feast/dbt/__init__.py b/sdk/python/feast/dbt/__init__.py new file mode 100644 index 00000000000..7d1312d5a1a --- /dev/null +++ b/sdk/python/feast/dbt/__init__.py @@ -0,0 +1,30 @@ +""" +dbt integration for Feast. + +This module provides functionality to import dbt models as Feast FeatureViews, +enabling automatic generation of Feast objects from dbt manifest.json files. + +Example usage:: + + from feast.dbt import DbtManifestParser, DbtToFeastMapper + parser = DbtManifestParser("target/manifest.json") + parser.parse() + models = parser.get_models(tag_filter="feast") + mapper = DbtToFeastMapper(data_source_type="bigquery") + for model in models: + data_source = mapper.create_data_source(model) + feature_view = mapper.create_feature_view(model, data_source, "driver_id") +""" + +from feast.dbt.codegen import DbtCodeGenerator, generate_feast_code +from feast.dbt.mapper import DbtToFeastMapper +from feast.dbt.parser import DbtColumn, DbtManifestParser, DbtModel + +__all__ = [ + "DbtManifestParser", + "DbtModel", + "DbtColumn", + "DbtToFeastMapper", + "DbtCodeGenerator", + "generate_feast_code", +] diff --git a/sdk/python/feast/dbt/codegen.py b/sdk/python/feast/dbt/codegen.py new file mode 100644 index 00000000000..5d28a263c99 --- /dev/null +++ b/sdk/python/feast/dbt/codegen.py @@ -0,0 +1,423 @@ +""" +Code generator for dbt to Feast imports. + +This module generates Python code files containing Feast object definitions +(Entity, DataSource, FeatureView) from dbt model metadata. +""" + +import logging +from typing import Any, List, Optional, Set, Union + +from jinja2 import BaseLoader, Environment + +from feast.dbt.mapper import map_dbt_type_to_feast_type +from feast.dbt.parser import DbtModel +from feast.types import ( + Array, + Bool, + Bytes, + Float32, + Float64, + Int32, + Int64, + String, + UnixTimestamp, +) + +logger = logging.getLogger(__name__) + +# Template for generating a complete Feast definitions file +FEAST_FILE_TEMPLATE = '''""" +Feast feature definitions generated from dbt models. + +Source: {{ manifest_path }} +Project: {{ project_name }} +Generated by: feast dbt import +""" + +from datetime import timedelta + +from feast import Entity, FeatureView, Field +{% if type_imports %} +from feast.types import {{ type_imports | join(', ') }} +{% endif %} +{% if data_source_type == 'bigquery' %} +from feast.infra.offline_stores.bigquery_source import BigQuerySource +{% elif data_source_type == 'snowflake' %} +from feast.infra.offline_stores.snowflake_source import SnowflakeSource +{% elif data_source_type == 'file' %} +from feast.infra.offline_stores.file_source import FileSource +{% endif %} + + +# ============================================================================= +# Entities +# ============================================================================= + +{% for entity in entities %} +{{ entity.var_name }} = Entity( + name="{{ entity.name }}", + join_keys=["{{ entity.join_key }}"], + description="{{ entity.description }}", + tags={{ entity.tags }}, +) + +{% endfor %} + +# ============================================================================= +# Data Sources +# ============================================================================= + +{% for source in data_sources %} +{% if data_source_type == 'bigquery' %} +{{ source.var_name }} = BigQuerySource( + name="{{ source.name }}", + table="{{ source.table }}", + timestamp_field="{{ source.timestamp_field }}", + description="{{ source.description }}", + tags={{ source.tags }}, +) +{% elif data_source_type == 'snowflake' %} +{{ source.var_name }} = SnowflakeSource( + name="{{ source.name }}", + database="{{ source.database }}", + schema="{{ source.schema }}", + table="{{ source.table }}", + timestamp_field="{{ source.timestamp_field }}", + description="{{ source.description }}", + tags={{ source.tags }}, +) +{% elif data_source_type == 'file' %} +{{ source.var_name }} = FileSource( + name="{{ source.name }}", + path="{{ source.path }}", + timestamp_field="{{ source.timestamp_field }}", + description="{{ source.description }}", + tags={{ source.tags }}, +) +{% endif %} + +{% endfor %} + +# ============================================================================= +# Feature Views +# ============================================================================= + +{% for fv in feature_views %} +{{ fv.var_name }} = FeatureView( + name="{{ fv.name }}", + entities=[{{ fv.entity_vars | join(', ') }}], + ttl=timedelta(days={{ fv.ttl_days }}), + schema=[ +{% for field in fv.fields %} + Field(name="{{ field.name }}", dtype={{ field.dtype }}{% if field.description %}, description="{{ field.description }}"{% endif %}), +{% endfor %} + ], + online={{ fv.online }}, + source={{ fv.source_var }}, + description="{{ fv.description }}", + tags={{ fv.tags }}, +) + +{% endfor %} +''' + + +def _get_feast_type_name(feast_type: Any) -> str: + """Get the string name of a Feast type for code generation.""" + if isinstance(feast_type, Array): + # Safely get base_type. Should always exist since Array.__init__ sets it. + # Example: Array(String) -> base_type = String + base_type = getattr(feast_type, "base_type", None) + + if base_type is None: + logger.warning( + "Array type missing 'base_type' attribute. " + "This indicates a bug in Array initialization. Falling back to String." + ) + base_type = String + + base_type_name = _get_feast_type_name(base_type) + return f"Array({base_type_name})" + + # Map type objects to their names. + # Note: ImageBytes and PdfBytes are excluded since dbt manifests only expose + # generic BYTES type without semantic information about binary content. + type_map = { + String: "String", + Int32: "Int32", + Int64: "Int64", + Float32: "Float32", + Float64: "Float64", + Bool: "Bool", + UnixTimestamp: "UnixTimestamp", + Bytes: "Bytes", + } + + return type_map.get(feast_type, "String") + + +def _make_var_name(name: str) -> str: + """Convert a name to a valid Python variable name.""" + # Replace hyphens and spaces with underscores + var_name = name.replace("-", "_").replace(" ", "_") + # Ensure it starts with a letter or underscore + if var_name and var_name[0].isdigit(): + var_name = f"_{var_name}" + return var_name + + +def _escape_description(desc: Optional[str]) -> str: + """Escape a description string for use in Python code.""" + if not desc: + return "" + # Escape quotes and newlines + return desc.replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ") + + +class DbtCodeGenerator: + """ + Generates Python code for Feast objects from dbt models. + + This class creates complete, importable Python files containing + Entity, DataSource, and FeatureView definitions. + + Example:: + + generator = DbtCodeGenerator( + data_source_type="bigquery", + timestamp_field="event_timestamp", + ttl_days=7 + ) + code = generator.generate( + models=models, + entity_column="user_id", + manifest_path="target/manifest.json", + project_name="my_project" + ) + with open("features.py", "w") as f: + f.write(code) + """ + + def __init__( + self, + data_source_type: str = "bigquery", + timestamp_field: str = "event_timestamp", + ttl_days: int = 1, + ): + self.data_source_type = data_source_type.lower() + self.timestamp_field = timestamp_field + self.ttl_days = ttl_days + + # Set up Jinja2 environment + self.env = Environment( + loader=BaseLoader(), + trim_blocks=True, + lstrip_blocks=True, + ) + self.template = self.env.from_string(FEAST_FILE_TEMPLATE) + + def generate( + self, + models: List[DbtModel], + entity_columns: Union[str, List[str]], + manifest_path: str = "", + project_name: str = "", + exclude_columns: Optional[List[str]] = None, + online: bool = True, + ) -> str: + """ + Generate Python code for Feast objects from dbt models. + + Args: + models: List of DbtModel objects to generate code for + entity_columns: Entity column name(s) - single string or list of strings + manifest_path: Path to the dbt manifest (for documentation) + project_name: dbt project name (for documentation) + exclude_columns: Columns to exclude from features + online: Whether to enable online serving + + Returns: + Generated Python code as a string + """ + # Normalize entity_columns to list + entity_cols: List[str] = ( + [entity_columns] if isinstance(entity_columns, str) else entity_columns + ) + + if not entity_cols: + raise ValueError("At least one entity column must be specified") + + # Note: entity columns should NOT be excluded - FeatureView.__init__ + # expects entity columns to be in the schema and will extract them + excluded = {self.timestamp_field} + if exclude_columns: + excluded.update(exclude_columns) + + # Collect all Feast types used for imports + type_imports: Set[str] = set() + + # Prepare entity data - create one entity per entity column + entities = [] + entity_vars = [] # Track variable names for feature views + for entity_col in entity_cols: + entity_var = _make_var_name(entity_col) + entity_vars.append(entity_var) + entities.append( + { + "var_name": entity_var, + "name": entity_col, + "join_key": entity_col, + "description": "Entity key for dbt models", + "tags": {"source": "dbt"}, + } + ) + + # Prepare data sources and feature views + data_sources = [] + feature_views = [] + + for model in models: + # Check required columns exist + column_names = [c.name for c in model.columns] + if self.timestamp_field not in column_names: + continue + + # Skip if ANY entity column is missing + if not all(e in column_names for e in entity_cols): + continue + + # Build tags + tags = {"dbt.model": model.name} + for tag in model.tags: + tags[f"dbt.tag.{tag}"] = "true" + + # Data source + source_var = _make_var_name(f"{model.name}_source") + source_data = { + "var_name": source_var, + "name": f"{model.name}_source", + "timestamp_field": self.timestamp_field, + "description": _escape_description(model.description), + "tags": tags, + } + + if self.data_source_type == "bigquery": + source_data["table"] = model.full_table_name + elif self.data_source_type == "snowflake": + source_data["database"] = model.database + source_data["schema"] = model.schema + source_data["table"] = model.alias + elif self.data_source_type == "file": + source_data["path"] = f"/data/{model.name}.parquet" + + data_sources.append(source_data) + + # Feature view fields + fields = [] + for column in model.columns: + if column.name in excluded: + continue + + feast_type = map_dbt_type_to_feast_type(column.data_type) + type_name = _get_feast_type_name(feast_type) + + # Track base type for imports. For Array types, import both Array and base type. + # Example: Array(Int64) requires imports: Array, Int64 + if isinstance(feast_type, Array): + type_imports.add("Array") + + base_type = getattr(feast_type, "base_type", None) + if base_type is None: + logger.warning( + "Array type missing 'base_type' attribute while generating imports. " + "This indicates a bug in Array initialization. Falling back to String." + ) + base_type = String + + base_type_name = _get_feast_type_name(base_type) + type_imports.add(base_type_name) + else: + type_imports.add(type_name) + + fields.append( + { + "name": column.name, + "dtype": type_name, + "description": _escape_description(column.description), + } + ) + + # Feature view + fv_var = _make_var_name(f"{model.name}_fv") + feature_views.append( + { + "var_name": fv_var, + "name": model.name, + "entity_vars": entity_vars, + "source_var": source_var, + "ttl_days": self.ttl_days, + "fields": fields, + "online": online, + "description": _escape_description(model.description), + "tags": tags, + } + ) + + # Sort type imports for consistent output + sorted_types = sorted(type_imports) + + # Render template + return self.template.render( + manifest_path=manifest_path, + project_name=project_name, + data_source_type=self.data_source_type, + type_imports=sorted_types, + entities=entities, + data_sources=data_sources, + feature_views=feature_views, + ) + + +def generate_feast_code( + models: List[DbtModel], + entity_columns: Union[str, List[str]], + data_source_type: str = "bigquery", + timestamp_field: str = "event_timestamp", + ttl_days: int = 1, + manifest_path: str = "", + project_name: str = "", + exclude_columns: Optional[List[str]] = None, + online: bool = True, +) -> str: + """ + Convenience function to generate Feast code from dbt models. + + Args: + models: List of DbtModel objects + entity_columns: Entity column name(s) - single string or list of strings + data_source_type: Type of data source (bigquery, snowflake, file) + timestamp_field: Timestamp column name + ttl_days: TTL in days for feature views + manifest_path: Path to manifest for documentation + project_name: Project name for documentation + exclude_columns: Columns to exclude from features + online: Whether to enable online serving + + Returns: + Generated Python code as a string + """ + generator = DbtCodeGenerator( + data_source_type=data_source_type, + timestamp_field=timestamp_field, + ttl_days=ttl_days, + ) + + return generator.generate( + models=models, + entity_columns=entity_columns, + manifest_path=manifest_path, + project_name=project_name, + exclude_columns=exclude_columns, + online=online, + ) diff --git a/sdk/python/feast/dbt/mapper.py b/sdk/python/feast/dbt/mapper.py new file mode 100644 index 00000000000..49a954856ff --- /dev/null +++ b/sdk/python/feast/dbt/mapper.py @@ -0,0 +1,480 @@ +""" +dbt to Feast type and object mapper. + +This module provides functionality to map dbt model metadata to Feast objects +including DataSource, Entity, and FeatureView. +""" + +from datetime import timedelta +from typing import Any, Dict, List, Optional, Union + +from feast.dbt.parser import DbtModel +from feast.entity import Entity +from feast.feature_view import FeatureView +from feast.field import Field +from feast.types import ( + Array, + Bool, + Bytes, + FeastType, + Float32, + Float64, + Int32, + Int64, + String, + UnixTimestamp, +) +from feast.value_type import ValueType + +# Mapping from FeastType to ValueType for entity value inference +FEAST_TYPE_TO_VALUE_TYPE: Dict[FeastType, ValueType] = { + String: ValueType.STRING, + Int32: ValueType.INT32, + Int64: ValueType.INT64, + Float32: ValueType.FLOAT, + Float64: ValueType.DOUBLE, + Bool: ValueType.BOOL, + Bytes: ValueType.BYTES, + UnixTimestamp: ValueType.UNIX_TIMESTAMP, +} + + +def feast_type_to_value_type(feast_type: FeastType) -> ValueType: + """Convert a FeastType to its corresponding ValueType for entities.""" + return FEAST_TYPE_TO_VALUE_TYPE.get(feast_type, ValueType.STRING) + + +# Comprehensive mapping from dbt/warehouse types to Feast types +# Covers BigQuery, Snowflake, Redshift, PostgreSQL, and common SQL types +DBT_TO_FEAST_TYPE_MAP: Dict[str, FeastType] = { + # String types + "STRING": String, + "TEXT": String, + "VARCHAR": String, + "CHAR": String, + "CHARACTER": String, + "NVARCHAR": String, + "NCHAR": String, + "CHARACTER VARYING": String, + # Integer types + "INT": Int64, + "INT32": Int32, + "INT64": Int64, + "INTEGER": Int64, + "BIGINT": Int64, + "SMALLINT": Int32, + "TINYINT": Int32, + "BYTEINT": Int32, + "NUMBER": Int64, # Snowflake - default to Int64, precision handling below + "NUMERIC": Int64, + "DECIMAL": Int64, + # Float types + "FLOAT": Float32, + "FLOAT32": Float32, + "FLOAT64": Float64, + "DOUBLE": Float64, + "DOUBLE PRECISION": Float64, + "REAL": Float32, + # Boolean types + "BOOL": Bool, + "BOOLEAN": Bool, + # Timestamp types + "TIMESTAMP": UnixTimestamp, + "TIMESTAMP_NTZ": UnixTimestamp, + "TIMESTAMP_LTZ": UnixTimestamp, + "TIMESTAMP_TZ": UnixTimestamp, + "DATETIME": UnixTimestamp, + "DATE": UnixTimestamp, + "TIME": UnixTimestamp, + # Binary types + "BYTES": Bytes, + "BINARY": Bytes, + "VARBINARY": Bytes, + "BLOB": Bytes, +} + + +def map_dbt_type_to_feast_type(dbt_type: str) -> FeastType: + """ + Map a dbt data type to a Feast type. + + Handles various database type formats including: + - Simple types: STRING, INT64, FLOAT + - Parameterized types: VARCHAR(255), NUMBER(10,2), DECIMAL(18,0) + - Array types: ARRAY, ARRAY + + Args: + dbt_type: The dbt/database data type string + + Returns: + The corresponding Feast type + """ + if not dbt_type: + return String + + # Normalize the type string + normalized = dbt_type.upper().strip() + + # Handle ARRAY types: ARRAY + if normalized.startswith("ARRAY<") and normalized.endswith(">"): + element_type_str = normalized[6:-1].strip() + element_type = map_dbt_type_to_feast_type(element_type_str) + # Array only supports primitive types + valid_array_types = { + String, + Int32, + Int64, + Float32, + Float64, + Bool, + Bytes, + UnixTimestamp, + } + if element_type in valid_array_types: + return Array(element_type) + return Array(String) # Fallback for complex nested types + + # Handle parameterized types: VARCHAR(255), NUMBER(10,2), etc. + # Extract base type by removing parentheses and parameters + base_type = normalized.split("(")[0].strip() + + # Handle Snowflake NUMBER with precision + if base_type == "NUMBER" and "(" in normalized: + try: + # Parse precision and scale: NUMBER(precision, scale) + params = normalized.split("(")[1].rstrip(")").split(",") + precision = int(params[0].strip()) + scale = int(params[1].strip()) if len(params) > 1 else 0 + + if scale > 0: + # Has decimal places, use Float64 + return Float64 + elif precision <= 9: + return Int32 + elif precision <= 18: + return Int64 + else: + # Precision > 18, may exceed Int64 range + return Float64 + except (ValueError, IndexError): + return Int64 + + # Look up in mapping table + if base_type in DBT_TO_FEAST_TYPE_MAP: + return DBT_TO_FEAST_TYPE_MAP[base_type] + + # Default to String for unknown types + return String + + +class DbtToFeastMapper: + """ + Maps dbt models to Feast objects. + + Supports creating DataSource, Entity, and FeatureView objects from + dbt model metadata. + + Example:: + + mapper = DbtToFeastMapper(data_source_type="bigquery") + data_source = mapper.create_data_source(model) + feature_view = mapper.create_feature_view( + model, data_source, entity_column="driver_id" + ) + + Args: + data_source_type: Type of data source ('bigquery', 'snowflake', 'file') + timestamp_field: Default timestamp field name + ttl_days: Default TTL in days for feature views + """ + + def __init__( + self, + data_source_type: str = "bigquery", + timestamp_field: str = "event_timestamp", + ttl_days: int = 1, + ): + self.data_source_type = data_source_type.lower() + self.timestamp_field = timestamp_field + self.ttl_days = ttl_days + + def _infer_entity_value_type(self, model: DbtModel, entity_col: str) -> ValueType: + """Infer entity ValueType from dbt model column type.""" + for column in model.columns: + if column.name == entity_col: + feast_type = map_dbt_type_to_feast_type(column.data_type) + return feast_type_to_value_type(feast_type) + return ValueType.UNKNOWN + + def create_data_source( + self, + model: DbtModel, + timestamp_field: Optional[str] = None, + created_timestamp_column: Optional[str] = None, + ) -> Any: + """ + Create a Feast DataSource from a dbt model. + + Args: + model: The DbtModel to create a DataSource from + timestamp_field: Override the default timestamp field + created_timestamp_column: Column for created timestamp (dedup) + + Returns: + A Feast DataSource (BigQuerySource, SnowflakeSource, or FileSource) + + Raises: + ValueError: If data_source_type is not supported + """ + ts_field = timestamp_field or self.timestamp_field + + # Build tags from dbt metadata + tags = {"dbt.model": model.name} + for tag in model.tags: + tags[f"dbt.tag.{tag}"] = "true" + + if self.data_source_type == "bigquery": + from feast.infra.offline_stores.bigquery_source import BigQuerySource + + return BigQuerySource( + name=f"{model.name}_source", + table=model.full_table_name, + timestamp_field=ts_field, + created_timestamp_column=created_timestamp_column or "", + description=model.description, + tags=tags, + ) + + elif self.data_source_type == "snowflake": + from feast.infra.offline_stores.snowflake_source import SnowflakeSource + + return SnowflakeSource( + name=f"{model.name}_source", + database=model.database, + schema=model.schema, + table=model.alias, + timestamp_field=ts_field, + created_timestamp_column=created_timestamp_column or "", + description=model.description, + tags=tags, + ) + + elif self.data_source_type == "file": + from feast.infra.offline_stores.file_source import FileSource + + # For file sources, use the model name as a placeholder path + return FileSource( + name=f"{model.name}_source", + path=f"/data/{model.name}.parquet", + timestamp_field=ts_field, + created_timestamp_column=created_timestamp_column or "", + description=model.description, + tags=tags, + ) + + else: + raise ValueError( + f"Unsupported data_source_type: {self.data_source_type}. " + f"Supported types: bigquery, snowflake, file" + ) + + def create_entity( + self, + name: str, + join_keys: Optional[List[str]] = None, + description: str = "", + tags: Optional[Dict[str, str]] = None, + value_type: ValueType = ValueType.STRING, + ) -> Entity: + """ + Create a Feast Entity. + + Args: + name: Entity name + join_keys: List of join key column names (defaults to [name]) + description: Entity description + tags: Optional tags + value_type: Value type for the entity (default: STRING) + + Returns: + A Feast Entity + """ + return Entity( + name=name, + join_keys=join_keys or [name], + value_type=value_type, + description=description, + tags=tags or {}, + ) + + def create_feature_view( + self, + model: DbtModel, + source: Any, + entity_columns: Union[str, List[str]], + entities: Optional[Union[Entity, List[Entity]]] = None, + timestamp_field: Optional[str] = None, + ttl_days: Optional[int] = None, + exclude_columns: Optional[List[str]] = None, + online: bool = True, + ) -> FeatureView: + """ + Create a Feast FeatureView from a dbt model. + + Args: + model: The DbtModel to create a FeatureView from + source: The DataSource for this FeatureView + entity_columns: Entity column name(s) - single string or list of strings + entities: Optional pre-created Entity or list of Entities + timestamp_field: Override the default timestamp field + ttl_days: Override the default TTL in days + exclude_columns: Additional columns to exclude from features + online: Whether to enable online serving + + Returns: + A Feast FeatureView + """ + # Normalize to lists + entity_cols: List[str] = ( + [entity_columns] + if isinstance(entity_columns, str) + else list(entity_columns) + ) + + entity_objs: List[Entity] = [] + if entities is not None: + entity_objs = [entities] if isinstance(entities, Entity) else list(entities) + + # Validate + if not entity_cols: + raise ValueError("At least one entity column must be specified") + + if entity_objs and len(entity_cols) != len(entity_objs): + raise ValueError( + f"Number of entity_columns ({len(entity_cols)}) must match " + f"number of entities ({len(entity_objs)})" + ) + + ts_field = timestamp_field or self.timestamp_field + ttl = timedelta(days=ttl_days if ttl_days is not None else self.ttl_days) + + # Columns to exclude from schema (timestamp + any explicitly excluded) + # Note: entity columns should NOT be excluded - FeatureView.__init__ + # expects entity columns to be in the schema and will extract them + excluded = {ts_field} + if exclude_columns: + excluded.update(exclude_columns) + + # Create schema from model columns (includes entity columns) + schema: List[Field] = [] + for column in model.columns: + if column.name not in excluded: + feast_type = map_dbt_type_to_feast_type(column.data_type) + schema.append( + Field( + name=column.name, + dtype=feast_type, + description=column.description, + ) + ) + + # Create entities if not provided + if not entity_objs: + entity_objs = [] + for entity_col in entity_cols: + # Infer entity value type from model column + entity_value_type = self._infer_entity_value_type(model, entity_col) + ent = self.create_entity( + name=entity_col, + description=f"Entity for {model.name}", + value_type=entity_value_type, + ) + entity_objs.append(ent) + + # Build tags from dbt metadata + tags = { + "dbt.model": model.name, + "dbt.unique_id": model.unique_id, + } + for tag in model.tags: + tags[f"dbt.tag.{tag}"] = "true" + + return FeatureView( + name=model.name, + source=source, + schema=schema, + entities=entity_objs, + ttl=ttl, + online=online, + description=model.description, + tags=tags, + ) + + def create_all_from_model( + self, + model: DbtModel, + entity_columns: Union[str, List[str]], + timestamp_field: Optional[str] = None, + ttl_days: Optional[int] = None, + exclude_columns: Optional[List[str]] = None, + online: bool = True, + ) -> Dict[str, Union[List[Entity], Any, FeatureView]]: + """ + Create all Feast objects (DataSource, Entity, FeatureView) from a dbt model. + + This is a convenience method that creates all necessary Feast objects + in one call. + + Args: + model: The DbtModel to create objects from + entity_columns: Entity column name(s) - single string or list of strings + timestamp_field: Override the default timestamp field + ttl_days: Override the default TTL in days + exclude_columns: Additional columns to exclude from features + online: Whether to enable online serving + + Returns: + Dict with keys 'entities', 'data_source', 'feature_view' + """ + # Normalize to list + entity_cols: List[str] = ( + [entity_columns] + if isinstance(entity_columns, str) + else list(entity_columns) + ) + + # Create entities (plural) + entities_list = [] + for entity_col in entity_cols: + entity_value_type = self._infer_entity_value_type(model, entity_col) + entity = self.create_entity( + name=entity_col, + description=f"Entity for {model.name}", + tags={"dbt.model": model.name}, + value_type=entity_value_type, + ) + entities_list.append(entity) + + # Create data source + data_source = self.create_data_source( + model=model, + timestamp_field=timestamp_field, + ) + + # Create feature view + feature_view = self.create_feature_view( + model=model, + source=data_source, + entity_columns=entity_cols, + entities=entities_list, + timestamp_field=timestamp_field, + ttl_days=ttl_days, + exclude_columns=exclude_columns, + online=online, + ) + + return { + "entities": entities_list, + "data_source": data_source, + "feature_view": feature_view, + } diff --git a/sdk/python/feast/dbt/parser.py b/sdk/python/feast/dbt/parser.py new file mode 100644 index 00000000000..9314104c9c1 --- /dev/null +++ b/sdk/python/feast/dbt/parser.py @@ -0,0 +1,297 @@ +""" +dbt manifest parser for Feast integration. + +This module provides functionality to parse dbt manifest.json files and extract +model metadata for generating Feast FeatureViews. + +Uses dbt-artifacts-parser for typed parsing of manifest versions v1-v12 (dbt 0.19 through 1.11+). +""" + +import json +from copy import deepcopy +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional + + +@dataclass +class DbtColumn: + """Represents a column in a dbt model.""" + + name: str + description: str = "" + data_type: str = "STRING" + tags: List[str] = field(default_factory=list) + meta: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class DbtModel: + """Represents a dbt model.""" + + name: str + unique_id: str + database: str + schema: str + alias: str + description: str = "" + columns: List[DbtColumn] = field(default_factory=list) + tags: List[str] = field(default_factory=list) + meta: Dict[str, Any] = field(default_factory=dict) + depends_on: List[str] = field(default_factory=list) + + @property + def full_table_name(self) -> str: + """Returns fully qualified table name (database.schema.table).""" + return f"{self.database}.{self.schema}.{self.alias}" + + +class DbtManifestParser: + """ + Parser for dbt manifest.json files using dbt-artifacts-parser. + + Uses dbt-artifacts-parser for typed parsing of manifest versions v1-v12 + (dbt versions 0.19 through 1.11+). + + Example:: + + parser = DbtManifestParser("target/manifest.json") + parser.parse() + models = parser.get_models(tag_filter="feast") + for model in models: + print(f"Model: {model.name}, Columns: {len(model.columns)}") + + Args: + manifest_path: Path to manifest.json file (typically target/manifest.json) + + Raises: + FileNotFoundError: If manifest.json doesn't exist + ValueError: If manifest.json is invalid JSON + """ + + def __init__(self, manifest_path: str): + """ + Initialize parser. + + Args: + manifest_path: Path to manifest.json file + """ + self.manifest_path = Path(manifest_path) + self._raw_manifest: Optional[Dict[str, Any]] = None + self._parsed_manifest: Optional[Any] = None + + @staticmethod + def _sanitize_supported_languages(manifest: Dict[str, Any]) -> Dict[str, Any]: + """ + Remove unsupported macro language values before typed parsing. + + dbt may emit values like "javascript" in `macros.*.supported_languages`, + while dbt-artifacts-parser currently accepts only "python" and "sql". + Since Feast only needs model metadata, dropping unknown values is safe. + """ + manifest_copy = deepcopy(manifest) + macros = manifest_copy.get("macros", {}) + if not isinstance(macros, dict): + return manifest_copy + + for macro in macros.values(): + if not isinstance(macro, dict): + continue + + supported_languages = macro.get("supported_languages") + if isinstance(supported_languages, list): + macro["supported_languages"] = [ + language + for language in supported_languages + if language in {"python", "sql"} + ] + + return manifest_copy + + def parse(self) -> None: + """ + Load and parse the manifest.json file using dbt-artifacts-parser. + + Raises: + FileNotFoundError: If manifest.json doesn't exist + ValueError: If manifest.json is invalid JSON + ImportError: If dbt-artifacts-parser is not installed + """ + if not self.manifest_path.exists(): + raise FileNotFoundError( + f"dbt manifest not found at {self.manifest_path}.\n" + f"Run 'dbt compile' or 'dbt run' first.\n" + f"Expected path: /target/manifest.json" + ) + + try: + with open(self.manifest_path, "r") as f: + self._raw_manifest = json.load(f) + except json.JSONDecodeError as e: + raise ValueError( + f"Invalid JSON in manifest: {e}\nTry: dbt clean && dbt compile" + ) + + # Parse using dbt-artifacts-parser for typed access + try: + from dbt_artifacts_parser.parser import parse_manifest + + assert self._raw_manifest is not None + try: + self._parsed_manifest = parse_manifest(manifest=self._raw_manifest) + except Exception as e: + if "supported_languages" not in str(e): + raise + sanitized_manifest = self._sanitize_supported_languages( + self._raw_manifest + ) + self._parsed_manifest = parse_manifest(manifest=sanitized_manifest) + except ImportError: + raise ImportError( + "dbt-artifacts-parser is required for dbt integration.\n" + "Install with: pip install 'feast[dbt]' or pip install dbt-artifacts-parser" + ) + + def _extract_column_from_node(self, col_name: str, col_data: Any) -> DbtColumn: + """Extract column info from a parsed node column.""" + return DbtColumn( + name=col_name, + description=getattr(col_data, "description", "") or "", + data_type=getattr(col_data, "data_type", "STRING") or "STRING", + tags=list(getattr(col_data, "tags", []) or []), + meta=dict(getattr(col_data, "meta", {}) or {}), + ) + + def _extract_model_from_node(self, node_id: str, node: Any) -> Optional[DbtModel]: + """Extract DbtModel from a parsed manifest node.""" + # Check resource type + resource_type = getattr(node, "resource_type", None) + if resource_type is None: + if not node_id.startswith("model."): + return None + else: + resource_type_str = ( + resource_type.value + if hasattr(resource_type, "value") + else str(resource_type) + ) + if resource_type_str != "model": + return None + + model_name = getattr(node, "name", "") + node_tags = list(getattr(node, "tags", []) or []) + node_columns = getattr(node, "columns", {}) or {} + depends_on = getattr(node, "depends_on", None) + + if depends_on: + depends_on_nodes = list(getattr(depends_on, "nodes", []) or []) + else: + depends_on_nodes = [] + + # Extract columns + columns = [ + self._extract_column_from_node(col_name, col_data) + for col_name, col_data in node_columns.items() + ] + + # Get schema - dbt-artifacts-parser uses schema_ to avoid Python keyword + schema = getattr(node, "schema_", "") or getattr(node, "schema", "") or "" + + return DbtModel( + name=model_name, + unique_id=node_id, + database=getattr(node, "database", "") or "", + schema=schema, + alias=getattr(node, "alias", model_name) or model_name, + description=getattr(node, "description", "") or "", + columns=columns, + tags=node_tags, + meta=dict(getattr(node, "meta", {}) or {}), + depends_on=depends_on_nodes, + ) + + def get_models( + self, + model_names: Optional[List[str]] = None, + tag_filter: Optional[str] = None, + ) -> List[DbtModel]: + """ + Extract dbt models from manifest. + + Args: + model_names: Optional list of specific model names to extract + tag_filter: Optional tag to filter models by + + Returns: + List of DbtModel objects + + Example:: + + models = parser.get_models(model_names=["driver_stats"]) + models = parser.get_models(tag_filter="feast") + """ + if self._parsed_manifest is None: + self.parse() + + if self._parsed_manifest is None: + return [] + + models = [] + nodes = getattr(self._parsed_manifest, "nodes", {}) or {} + + for node_id, node in nodes.items(): + # Only process models (not tests, seeds, snapshots, etc.) + if not node_id.startswith("model."): + continue + + model = self._extract_model_from_node(node_id, node) + if model is None: + continue + + # Filter by model names if specified + if model_names and model.name not in model_names: + continue + + # Filter by tag if specified + if tag_filter and tag_filter not in model.tags: + continue + + models.append(model) + + return models + + def get_model_by_name(self, model_name: str) -> Optional[DbtModel]: + """ + Get a specific model by name. + + Args: + model_name: Name of the model to retrieve + + Returns: + DbtModel if found, None otherwise + """ + models = self.get_models(model_names=[model_name]) + return models[0] if models else None + + @property + def dbt_version(self) -> Optional[str]: + """Get dbt version from manifest metadata.""" + if self._parsed_manifest is None: + return None + metadata = getattr(self._parsed_manifest, "metadata", None) + if metadata is None: + return None + return getattr(metadata, "dbt_version", None) + + @property + def project_name(self) -> Optional[str]: + """Get project name from manifest metadata.""" + if self._parsed_manifest is None: + return None + metadata = getattr(self._parsed_manifest, "metadata", None) + if metadata is None: + return None + # project_name may not exist in all manifest versions + return getattr(metadata, "project_name", None) or getattr( + metadata, "project_id", None + ) diff --git a/sdk/python/feast/demos.py b/sdk/python/feast/demos.py new file mode 100644 index 00000000000..9970f90316b --- /dev/null +++ b/sdk/python/feast/demos.py @@ -0,0 +1,851 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Demo notebook generation for Feast projects. + +Usage:: + + from feast import copy_demo_notebooks + copy_demo_notebooks() + +This will search for ``feature_store.yaml`` in the current directory and every +file inside the ``feast-config/`` directory, then write tailored Jupyter +notebooks into a ``./feast-demo-notebooks//`` directory for each +project found. +""" + +import json +import logging +import os +import pathlib +from typing import Any, Optional + +import yaml + +_logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Discovery helpers +# --------------------------------------------------------------------------- + + +def _find_feature_store_yamls(repo_path: pathlib.Path) -> list[pathlib.Path]: + """Return all feature-store config paths found under *repo_path*. + + Searches: + 1. ``repo_path/feature_store.yaml`` + 2. Every file directly inside ``repo_path/feast-config/`` + — each file is treated as a separate project config. + """ + found: list[pathlib.Path] = [] + + direct = repo_path / "feature_store.yaml" + if direct.exists(): + found.append(direct) + + feast_config_dir = repo_path / "feast-config" + if feast_config_dir.is_dir(): + for entry in sorted(feast_config_dir.iterdir()): + if entry.is_file(): + found.append(entry) + + return found + + +def _parse_yaml(yaml_path: pathlib.Path) -> dict[str, Any]: + with open(yaml_path) as fh: + return yaml.safe_load(os.path.expandvars(fh.read())) or {} + + +def _extract_store_info(config: dict[str, Any]) -> dict[str, Any]: + """Summarise the key fields from a raw ``feature_store.yaml`` dict.""" + info: dict[str, Any] = { + "project": config.get("project", "my_feast_project"), + "provider": config.get("provider", "local"), + "online_store_type": "sqlite", + "offline_store_type": "file", + "registry_type": "file", + "auth_type": "no_auth", + "vector_enabled": False, + "embedding_dim": None, + } + + online = config.get("online_store", {}) + if isinstance(online, dict): + info["online_store_type"] = online.get("type", "sqlite").lower() + info["vector_enabled"] = bool(online.get("vector_enabled", False)) + if online.get("embedding_dim"): + info["embedding_dim"] = online["embedding_dim"] + elif isinstance(online, str): + info["online_store_type"] = online.lower() + + offline = config.get("offline_store", {}) + if isinstance(offline, dict): + info["offline_store_type"] = offline.get("type", "file").lower() + elif isinstance(offline, str): + info["offline_store_type"] = offline.lower() + + registry = config.get("registry", {}) + if isinstance(registry, dict): + # Operator client YAML uses "registry_type" key; standard Feast uses "type" + info["registry_type"] = ( + registry.get("registry_type") or registry.get("type", "file") + ).lower() + # string registry value is a plain file path — keep default "file" + + auth = config.get("auth", {}) + if isinstance(auth, dict): + info["auth_type"] = auth.get("type", "no_auth").lower() + + return info + + +# --------------------------------------------------------------------------- +# Notebook cell builders +# --------------------------------------------------------------------------- + + +def _md(source: str) -> dict[str, Any]: + return { + "cell_type": "markdown", + "metadata": {}, + "source": source, + } + + +def _code(source: str, tags: Optional[list[str]] = None) -> dict[str, Any]: + meta: dict[str, Any] = {} + if tags: + meta["tags"] = tags + return { + "cell_type": "code", + "execution_count": None, + "metadata": meta, + "outputs": [], + "source": source, + } + + +def _notebook(cells: list[dict[str, Any]]) -> dict[str, Any]: + return { + "nbformat": 4, + "nbformat_minor": 5, + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3", + }, + }, + "cells": cells, + } + + +# --------------------------------------------------------------------------- +# Per-store setup snippets +# --------------------------------------------------------------------------- + + +def _is_operator_client(info: dict[str, Any]) -> bool: + """Return True when the feature_store.yaml was generated by the Feast operator. + + The operator sets provider=local with registry_type=remote, online_store.type=remote, + and offline_store.type=remote. + """ + return ( + info["registry_type"] == "remote" + and info["online_store_type"] == "remote" + and info["offline_store_type"] == "remote" + ) + + +# --------------------------------------------------------------------------- +# Notebook generators +# --------------------------------------------------------------------------- + + +def _apply_md(info: dict[str, Any]) -> dict[str, Any]: + """Return the markdown cell that introduces the apply / registry-sync section.""" + if info["registry_type"] == "remote": + return _md( + "## 4. Registry Sync\n\nRefresh the registry cache to load the latest feature definitions." + ) + return _md( + "## 4. Apply Feature Definitions\n\n" + "Register entities, feature views, and services into the registry. " + "Skip if already applied." + ) + + +def _apply_code(info: dict[str, Any]) -> dict[str, Any]: + """Return the code cell that applies (local) or refreshes (remote) the registry.""" + if info["registry_type"] == "remote": + return _code( + "store.refresh_registry()\n" + "fvs = store.list_feature_views()\n" + "print(f'Registry synced — {len(fvs)} feature view(s) available.')" + ) + # Local file registry — auto-apply if empty, then refresh. + return _code( + "fvs = store.list_feature_views()\n" + "entities = store.list_entities()\n" + "\n" + "if fvs or entities:\n" + " print(f'Registry ready: {len(entities)} entity/entities, {len(fvs)} feature view(s)')\n" + "else:\n" + " print('Registry is empty — running feast apply ...')\n" + " !feast -f {FEAST_FS_YAML} apply\n" + " store.refresh_registry()\n" + " print('Apply complete.')" + ) + + +def _path_setup_cell(yaml_abs: str) -> dict[str, Any]: + """Return a code cell that sets ``FEAST_FS_YAML`` to the absolute path of + the feature-store config resolved at generation time.""" + return _code( + "import os\n" + "\n" + f"FEAST_FS_YAML = r{repr(yaml_abs)}\n" + "\n" + "assert os.path.exists(FEAST_FS_YAML), (\n" + " f'Config not found at {FEAST_FS_YAML!r}. '\n" + " 'Update FEAST_FS_YAML to the correct path.'\n" + ")\n" + "print(f'Using feature_store.yaml: {FEAST_FS_YAML}')", + tags=["parameters"], + ) + + +def _nb_overview(info: dict[str, Any], yaml_abs: str) -> dict[str, Any]: + project = info["project"] + ost = info["online_store_type"] + offst = info["offline_store_type"] + auth = info["auth_type"] + provider = info["provider"] + vector_enabled = info["vector_enabled"] + + cells: list[dict[str, Any]] = [ + _md( + f"# Feature Store Overview — `{project}`\n\n" + "Explore the entities, feature views, feature services, and data sources " + "registered in this project." + ), + _md("## 1. Prerequisites"), + _code( + "# Verify feast installation\nimport feast\nprint(f'Feast version: {feast.__version__}')" + ), + _md("## 2. Feature Store Path"), + _path_setup_cell(yaml_abs), + _md( + f"## 3. Connect to the Feature Store\n" + f"The feature store for project **`{project}`** is configured with:\n\n" + f"| Setting | Value |\n" + f"|---------|-------|\n" + f"| Provider | `{provider}` |\n" + f"| Online store | `{ost}` |\n" + f"| Offline store | `{offst}` |\n" + f"| Auth | `{auth}` |\n" + + ( + f"| Vector search | enabled (embedding dim: {info['embedding_dim']}) |\n" + if vector_enabled + else "" + ) + ), + _code( + "from feast import FeatureStore\n" + "\n" + "store = FeatureStore(fs_yaml_file=FEAST_FS_YAML)\n" + "print(f'Connected to project: {store.project}')" + ), + _apply_md(info), + _apply_code(info), + _md("## 5. List Entities"), + _code( + "entities = store.list_entities()\n" + "print(f'Found {len(entities)} entity/entities\\n')\n" + "for e in entities:\n" + " print(f' • {e.name} (join_key={e.join_key}, type={e.value_type})')" + ), + _md("## 6. List Feature Views"), + _code( + "feature_views = store.list_feature_views()\n" + "print(f'Found {len(feature_views)} batch feature view(s)\\n')\n" + "for fv in feature_views:\n" + " feature_names = [f.name for f in fv.features]\n" + " print(f' • {fv.name}')\n" + " print(f' Features : {feature_names}')\n" + " print(f' Entities : {fv.entities}')\n" + " print(f' TTL : {fv.ttl}')\n" + ), + _md("## 7. List On-Demand Feature Views"), + _code( + "odfvs = store.list_on_demand_feature_views()\n" + "if odfvs:\n" + " print(f'Found {len(odfvs)} on-demand feature view(s)\\n')\n" + " for odfv in odfvs:\n" + " print(f' • {odfv.name}')\n" + "else:\n" + " print('No on-demand feature views defined.')" + ), + _md("## 8. List Feature Services"), + _code( + "services = store.list_feature_services()\n" + "if services:\n" + " print(f'Found {len(services)} feature service(s)\\n')\n" + " for svc in services:\n" + " views = [p.name for p in svc.feature_view_projections]\n" + " print(f' • {svc.name} -> views: {views}')\n" + "else:\n" + " print('No feature services defined.')" + ), + _md("## 9. List Data Sources"), + _code( + "sources = store.list_data_sources()\n" + "print(f'Found {len(sources)} data source(s)\\n')\n" + "for src in sources:\n" + " print(f' • {src.name} ({type(src).__name__})')" + ), + _md( + "## Next Steps\n\n" + "- **`02_historical_features_training.ipynb`** — retrieve historical features for training.\n" + "- **`03_online_features_serving.ipynb`** — materialize and serve online features." + ), + ] + return _notebook(cells) + + +def _nb_historical(info: dict[str, Any], yaml_abs: str) -> dict[str, Any]: + project = info["project"] + + cells: list[dict[str, Any]] = [ + _md( + f"# Historical Features & Training Datasets — `{project}`\n\n" + "Retrieve point-in-time correct feature values to build ML training datasets." + ), + _md("## 1. Feature Store Path"), + _path_setup_cell(yaml_abs), + _md("## 2. Connect to the Feature Store"), + _code( + "from feast import FeatureStore\n" + "\n" + "store = FeatureStore(fs_yaml_file=FEAST_FS_YAML)\n" + "print(f'Project : {store.project}')\n" + "print('Feature views:', [fv.name for fv in store.list_feature_views()])" + ), + _md( + "## 3. Discover Available Features\n\nList feature views and read a sample of entity data." + ), + _code( + "import pandas as pd\n" + "from datetime import datetime, timedelta, timezone\n" + "\n" + "fvs = store.list_feature_views()\n" + "entities = store.list_entities()\n" + "\n" + "if not fvs:\n" + " print('No feature views found — run `feast apply` first.')\n" + "else:\n" + " first_fv = fvs[0]\n" + "\n" + " # Identify the entity join key.\n" + " entity_name = entities[0].join_key if entities else 'entity_id'\n" + " if first_fv.entities:\n" + " fv_entity = next(\n" + " (e for e in entities if e.name in set(first_fv.entities)),\n" + " entities[0] if entities else None,\n" + " )\n" + " if fv_entity:\n" + " entity_name = fv_entity.join_key\n" + "\n" + " # Read latest entity values from the offline store.\n" + " # This uses the same mechanism Feast uses for materialization.\n" + " source = first_fv.batch_source\n" + " provider = store._get_provider()\n" + " sample_df = provider.offline_store.pull_latest_from_table_or_query(\n" + " config=store.config,\n" + " data_source=source,\n" + " join_key_columns=[entity_name],\n" + " feature_name_columns=[f.name for f in first_fv.features],\n" + " timestamp_field=source.timestamp_field,\n" + " created_timestamp_column=source.created_timestamp_column or '',\n" + " start_date=datetime(2000, 1, 1, tzinfo=timezone.utc),\n" + " end_date=datetime.now(tz=timezone.utc),\n" + " ).to_df()\n" + "\n" + " print(f'Feature view : {first_fv.name}')\n" + " print(f'Entity join key : {entity_name!r}')\n" + " print(f'Rows in source : {len(sample_df):,}')\n" + " print(f'Columns : {list(sample_df.columns)}')\n" + " if len(sample_df) > 0:\n" + " display(sample_df.head())\n" + " else:\n" + " print('No data found — check that your data source has been populated.')" + ), + _md( + "## 4. Build an Entity DataFrame\n\n" + "Specify which entity IDs and at what timestamps you want features for." + ), + _code( + "if not fvs:\n" + " raise SystemExit('No feature views — run feast apply first.')\n" + "\n" + "# Use real entity IDs and timestamps from the sample.\n" + "if entity_name in sample_df.columns and len(sample_df) > 0:\n" + " entity_ids = sample_df[entity_name].dropna().unique()[:5].tolist()\n" + " # Detect the timestamp column from the source's configuration.\n" + " ts_col = source.timestamp_field if source.timestamp_field in sample_df.columns else None\n" + " if not ts_col:\n" + " ts_col = next((c for c in sample_df.columns if 'timestamp' in c.lower()), None)\n" + " if ts_col:\n" + " timestamps = (\n" + " sample_df[sample_df[entity_name].isin(entity_ids)]\n" + " .sort_values(ts_col, ascending=False)\n" + " .drop_duplicates(subset=[entity_name])[ts_col]\n" + " .tolist()\n" + " )\n" + " else:\n" + " timestamps = [datetime.now() - timedelta(hours=i) for i in range(len(entity_ids))]\n" + "else:\n" + " entity_ids = [1001, 1002, 1003]\n" + " timestamps = [datetime.now() - timedelta(hours=i) for i in range(len(entity_ids))]\n" + " print('Using placeholder entity IDs — replace with real values from your data.')\n" + "\n" + "entity_df = pd.DataFrame(\n" + " {\n" + " entity_name: entity_ids[:len(timestamps)],\n" + " 'event_timestamp': timestamps[:len(entity_ids)],\n" + " }\n" + ")\n" + "print(f'Entity IDs : {entity_ids}')\n" + "print(f'Rows : {len(entity_df)}')\n" + "entity_df" + ), + _md("## 5. Choose Features to Retrieve"), + _code( + "# List all available feature views and their features.\n" + "print('Available feature views:')\n" + "for fv in fvs:\n" + " features = [f.name for f in fv.features]\n" + " print(f' {fv.name}: {features}')\n" + "\n" + "# Select features from the first feature view.\n" + "# Using a single view avoids name collisions across views with identical column names.\n" + "feature_refs = [f'{first_fv.name}:{f.name}' for f in first_fv.features]\n" + "print('\\nWill retrieve:', feature_refs)" + ), + _md("## 6. Retrieve Historical Features"), + _code( + "if feature_refs:\n" + " training_df = store.get_historical_features(\n" + " entity_df=entity_df,\n" + " features=feature_refs,\n" + " ).to_df()\n" + " print(f'Training dataset shape: {training_df.shape}')\n" + " training_df.head()\n" + "else:\n" + " print('No feature views found — run `feast apply` first.')" + ), + _md( + "## 7. (Optional) Retrieve via FeatureService\n\nRetrieve features using a versioned FeatureService instead of individual feature references." + ), + _code( + "services = store.list_feature_services()\n" + "if not services:\n" + " print('No feature services found — define one in your feature repo.')\n" + "else:\n" + " svc = services[0]\n" + "\n" + " # Detect extra request-data columns required by ODFVs in this service.\n" + " odfv_map = {v.name: v for v in store.list_on_demand_feature_views()}\n" + " missing_cols = {\n" + " field.name: field.dtype\n" + " for proj in svc.feature_view_projections\n" + " if proj.name in odfv_map\n" + " for rs in odfv_map[proj.name].source_request_sources.values()\n" + " for field in rs.schema\n" + " if field.name not in entity_df.columns\n" + " }\n" + "\n" + " if missing_cols:\n" + " print('This service requires the following extra columns in entity_df:')\n" + " for col, dtype in missing_cols.items():\n" + " print(f' entity_df[{col!r}] = ')\n" + " print('Add them to entity_df above and re-run this cell.')\n" + " else:\n" + " # Check if service needs entity keys not already in entity_df.\n" + " svc_entities = set()\n" + " for proj in svc.feature_view_projections:\n" + " fv_match = next((fv for fv in fvs if fv.name == proj.name), None)\n" + " if fv_match:\n" + " for ent_name in fv_match.entities:\n" + " ent_obj = next((e for e in entities if e.name == ent_name), None)\n" + " if ent_obj:\n" + " svc_entities.add(ent_obj.join_key)\n" + " missing_keys = svc_entities - set(entity_df.columns)\n" + " if missing_keys:\n" + " print(f'This service requires additional entity columns: {missing_keys}')\n" + " print('Add them to entity_df above and re-run this cell.')\n" + " else:\n" + " print(f'Using feature service: {svc.name}')\n" + " training_df_svc = store.get_historical_features(\n" + " entity_df=entity_df,\n" + " features=svc,\n" + " full_feature_names=True,\n" + " ).to_df()\n" + " print(f'Dataset shape: {training_df_svc.shape}')\n" + " training_df_svc.head()" + ), + _md("## 8. Use the Training Dataset"), + _code( + "# Example: split into features (X) and labels (y)\n" + "# Adjust column names to match your actual feature names and label.\n" + "if feature_refs and 'training_df' in dir():\n" + " label_col = 'label' # TODO: replace with your label column\n" + " feature_cols = [c for c in training_df.columns\n" + " if c not in ('event_timestamp', entity_name, label_col)]\n" + " X = training_df[feature_cols]\n" + " print('Feature matrix shape:', X.shape)\n" + " print('Feature columns:', feature_cols)" + ), + _md( + "## Next Steps\n\n" + "- **`03_online_features_serving.ipynb`** — materialize and serve online features." + ), + ] + return _notebook(cells) + + +def _nb_online(info: dict[str, Any], yaml_abs: str) -> dict[str, Any]: + project = info["project"] + auth = info["auth_type"] + vector_enabled = info["vector_enabled"] + + cells: list[dict[str, Any]] = [ + _md( + f"# Online Feature Serving — `{project}`\n\n" + "Materialize features and retrieve them at low latency for inference." + ), + _md("## 1. Feature Store Path"), + _path_setup_cell(yaml_abs), + _md("## 2. Connect to the Feature Store"), + _code( + "from feast import FeatureStore\n" + "\n" + "store = FeatureStore(fs_yaml_file=FEAST_FS_YAML)\n" + "print(f'Project : {store.project}')" + ), + ] + + # Materialization section. + materialize_md = ( + "## 3. Materialize Features\n\n" + + ( + "> **Optional** — materialization is typically handled server-side.\n\n" + if _is_operator_client(info) + else "" + ) + + "Load feature values into the online store for low-latency serving.\n\n" + "| Method | When to use |\n" + "|--------|-------------|\n" + "| `materialize_incremental` | Regular runs — only new data since last run |\n" + "| `materialize` | First run or full refresh of a time window |" + ) + cells += [ + _md(materialize_md), + _code( + "from datetime import datetime, timedelta, timezone\n" + "\n" + "fvs = store.list_feature_views()\n" + "\n" + "if not fvs:\n" + " print('No feature views found — run feast apply first (see section 3).')\n" + "else:\n" + " # Check last materialization watermarks across all feature views.\n" + " last_written = [\n" + " fv.materialization_intervals[-1][1]\n" + " for fv in fvs\n" + " if fv.materialization_intervals\n" + " ]\n" + "\n" + " if not last_written:\n" + " # No materialization history — do a full initial load.\n" + " end_date = datetime.now(tz=timezone.utc)\n" + " start_date = end_date - timedelta(days=30)\n" + " print(f'First materialization: loading {start_date.date()} → {end_date.date()} ...')\n" + " store.materialize(start_date=start_date, end_date=end_date)\n" + " else:\n" + " # Incremental: only pick up data since the last run.\n" + " end_date = datetime.now(tz=timezone.utc)\n" + " print(f'Incremental materialization up to {end_date} ...')\n" + " store.materialize_incremental(end_date=end_date)\n" + "\n" + " print('Materialization complete.')" + ), + _md("### 3b. Force a Full Refresh"), + _code( + "# from datetime import datetime, timedelta, timezone\n" + "# store.materialize(\n" + "# start_date=datetime.now(tz=timezone.utc) - timedelta(days=7),\n" + "# end_date=datetime.now(tz=timezone.utc),\n" + "# )" + ), + ] + + cells += [ + _md("## 4. Retrieve Online Features"), + _code( + "entities = store.list_entities()\n" + "fvs = store.list_feature_views()\n" + "\n" + "if not entities or not fvs:\n" + " print('No entities or feature views — run `feast apply` first.')\n" + "else:\n" + " first_fv = fvs[0]\n" + " feature_refs = [f'{first_fv.name}:{f.name}' for f in first_fv.features[:3]]\n" + "\n" + " # Resolve the correct entity join key for the first feature view.\n" + " entity_name = entities[0].join_key\n" + " if first_fv.entities:\n" + " fv_entity = next(\n" + " (e for e in entities if e.name in set(first_fv.entities)),\n" + " entities[0],\n" + " )\n" + " entity_name = fv_entity.join_key\n" + "\n" + " # Discover real entity IDs from the offline source.\n" + " from datetime import timezone\n" + " source = first_fv.batch_source\n" + " provider = store._get_provider()\n" + " sample_df = provider.offline_store.pull_latest_from_table_or_query(\n" + " config=store.config,\n" + " data_source=source,\n" + " join_key_columns=[entity_name],\n" + " feature_name_columns=[f.name for f in first_fv.features],\n" + " timestamp_field=source.timestamp_field,\n" + " created_timestamp_column=source.created_timestamp_column or '',\n" + " start_date=datetime(2000, 1, 1, tzinfo=timezone.utc),\n" + " end_date=datetime.now(tz=timezone.utc),\n" + " ).to_df()\n" + "\n" + " if len(sample_df) > 0 and entity_name in sample_df.columns:\n" + " entity_ids = sample_df[entity_name].dropna().unique()[:5].tolist()\n" + " else:\n" + " entity_ids = [1001, 1002]\n" + " print('Using placeholder IDs — replace with real values.')\n" + "\n" + " entity_rows = [{entity_name: eid} for eid in entity_ids]\n" + "\n" + " response = store.get_online_features(\n" + " features=feature_refs,\n" + " entity_rows=entity_rows,\n" + " )\n" + " import pandas as pd\n" + " print(pd.DataFrame(response.to_dict()))" + ), + _md( + "## 5. Online Features via FeatureService\n\nRetrieve features using a versioned FeatureService." + ), + _code( + "services = store.list_feature_services()\n" + "if not services:\n" + " print('No feature services defined.')\n" + "else:\n" + " svc = services[0]\n" + "\n" + " # Detect extra request-data fields required by ODFVs in this service.\n" + " odfv_map = {v.name: v for v in store.list_on_demand_feature_views()}\n" + " current_keys = set(entity_rows[0].keys()) if entity_rows else set()\n" + " missing_fields = {\n" + " field.name: field.dtype\n" + " for proj in svc.feature_view_projections\n" + " if proj.name in odfv_map\n" + " for rs in odfv_map[proj.name].source_request_sources.values()\n" + " for field in rs.schema\n" + " if field.name not in current_keys\n" + " }\n" + "\n" + " if missing_fields:\n" + " print('This service requires the following extra fields in each entity row:')\n" + " for col, dtype in missing_fields.items():\n" + " print(f' {col!r}: ')\n" + " print('Add them to entity_rows above and re-run this cell.')\n" + " else:\n" + " # Check if service needs extra entity keys beyond what we have.\n" + " svc_entities = set()\n" + " for proj in svc.feature_view_projections:\n" + " fv_match = next((fv for fv in fvs if fv.name == proj.name), None)\n" + " if fv_match:\n" + " for ent_name in fv_match.entities:\n" + " ent_obj = next((e for e in entities if e.name == ent_name), None)\n" + " if ent_obj:\n" + " svc_entities.add(ent_obj.join_key)\n" + " missing_keys = svc_entities - current_keys\n" + " if missing_keys:\n" + " print(f'This service requires additional entity keys: {missing_keys}')\n" + " print('Add them to entity_rows above and re-run this cell.')\n" + " else:\n" + " print(f'Using feature service: {svc.name}')\n" + " response = store.get_online_features(\n" + " features=svc,\n" + " entity_rows=entity_rows,\n" + " full_feature_names=True,\n" + " )\n" + " import pandas as pd\n" + " print(pd.DataFrame(response.to_dict()))" + ), + ] + + if auth in ("kubernetes", "oidc"): + cells.append(_md(f"## 6. Authentication (`{auth}`)")) + cells.append(_code("print(store.config.auth)")) + + if vector_enabled: + dim = info.get("embedding_dim") or 384 + section = 7 if auth in ("kubernetes", "oidc") else 6 + cells.append( + _md( + f"## {section}. Vector / RAG Feature Retrieval\n\nSearch stored embeddings (dim: {dim})." + ) + ) + cells.append( + _code( + "import numpy as np\n" + "\n" + "# TODO: replace with a real query embedding from your encoder model\n" + f"query_embedding = np.random.rand({dim}).tolist()\n" + "\n" + "# List feature views with vector features\n" + "fvs = store.list_feature_views()\n" + "vec_fvs = [\n" + " fv for fv in fvs\n" + " if any(getattr(f, 'vector_index', False) for f in fv.features)\n" + "]\n" + "\n" + "if vec_fvs:\n" + " fv = vec_fvs[0]\n" + " results = store.retrieve_online_documents(\n" + " feature=f'{fv.name}:{fv.features[0].name}',\n" + " query=query_embedding,\n" + " top_k=5,\n" + " )\n" + " import pandas as pd\n" + " print(pd.DataFrame(results.to_dict()))\n" + "else:\n" + " print('No vector feature views found.')" + ) + ) + + cells.append( + _md( + "## Next Steps\n\n" + "- Schedule `materialize_incremental` to keep the online store fresh.\n" + ) + ) + + return _notebook(cells) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def copy_demo_notebooks( + output_dir: str = "./feast-demo-notebooks", + repo_path: str = ".", + overwrite: bool = False, +) -> None: + """Generate tailored demo notebooks for each Feast project found nearby. + + The function searches *repo_path* (default: current working directory) for + feature-store YAML files in: + + * ``/feature_store.yaml`` + * Every file inside ``/feast-config/`` + + For each project discovered a sub-directory is created under *output_dir* + and one or more notebooks are written (the exact set depends on the project + configuration and may grow in future releases). + + Parameters + ---------- + output_dir: + Root directory where notebooks are written. + Defaults to ``./feast-demo-notebooks``. + repo_path: + Directory to search for ``feature_store.yaml`` files. + Defaults to the current working directory. + overwrite: + When *False* (default) raise :class:`FileExistsError` if *output_dir* + already exists. Set to *True* to update notebooks in place. + """ + out = pathlib.Path(output_dir).resolve() + + if not overwrite and out.exists(): + raise FileExistsError( + f"Directory '{out}' already exists. " + "Remove it or pass overwrite=True to update notebooks in place." + ) + + root = pathlib.Path(repo_path).absolute() + yaml_paths = _find_feature_store_yamls(root) + + if not yaml_paths: + _logger.warning( + "No feature_store.yaml found under '%s'. " + "Make sure you run this from a directory that contains feature_store.yaml " + "or a feast-config/ subdirectory.", + root, + ) + return + + out.mkdir(parents=True, exist_ok=True) + print(f"Writing demo notebooks to: {out}\n") + + for yaml_path in yaml_paths: + raw = _parse_yaml(yaml_path) + info = _extract_store_info(raw) + project = info["project"] + + project_dir = out / project + project_dir.mkdir(parents=True, exist_ok=True) + + # Absolute path — use absolute() instead of resolve() to preserve + # Kubernetes ConfigMap/Secret symlinks. + yaml_abs_str = str(yaml_path.absolute()) + + notebooks = { + "01_feature_store_overview.ipynb": _nb_overview(info, yaml_abs_str), + "02_historical_features_training.ipynb": _nb_historical(info, yaml_abs_str), + "03_online_features_serving.ipynb": _nb_online(info, yaml_abs_str), + } + + for nb_name, nb_content in notebooks.items(): + nb_path = project_dir / nb_name + with open(nb_path, "w") as fh: + json.dump(nb_content, fh, indent=1) + + print( + f" [{project}]\n" + f" feature_store.yaml : {yaml_abs_str}\n" + f" online_store : {info['online_store_type']}\n" + f" offline_store : {info['offline_store_type']}\n" + f" auth : {info['auth_type']}\n" + + (" vector search : enabled\n" if info["vector_enabled"] else "") + + f" → {project_dir}/" + ) + for nb_name in notebooks: + print(f" ✓ {nb_name}") + print() diff --git a/sdk/python/feast/diff/apply_progress.py b/sdk/python/feast/diff/apply_progress.py new file mode 100644 index 00000000000..7d12ecef018 --- /dev/null +++ b/sdk/python/feast/diff/apply_progress.py @@ -0,0 +1,185 @@ +""" +Enhanced progress tracking infrastructure for feast apply operations. + +This module provides the ApplyProgressContext class that manages positioned, +color-coded progress bars during apply operations with fixed-width formatting +for perfect alignment. +""" + +from dataclasses import dataclass +from typing import Optional + +from tqdm import tqdm + +try: + from feast.diff.progress_utils import ( + create_positioned_tqdm, + get_color_for_phase, + is_tty_available, + ) + + _PROGRESS_UTILS_AVAILABLE = True +except ImportError: + # Graceful fallback when progress_utils is not available (e.g., in tests) + _PROGRESS_UTILS_AVAILABLE = False + + def create_positioned_tqdm( + position: int, + description: str, + total: int, + color: str = "blue", + postfix: Optional[str] = None, + ) -> Optional[tqdm]: + return None + + def get_color_for_phase(phase: str) -> str: + return "blue" + + def is_tty_available() -> bool: + return False + + +@dataclass +class ApplyProgressContext: + """ + Enhanced context object for tracking progress during feast apply operations. + + This class manages multiple positioned progress bars with fixed-width formatting: + 1. Overall progress (position 0) - tracks main phases + 2. Phase progress (position 1) - tracks operations within current phase + + Features: + - Fixed-width alignment for perfect visual consistency + - Color-coded progress bars by phase + - Position coordination to prevent overlap + - TTY detection for CI/CD compatibility + """ + + # Core tracking state + current_phase: str = "" + overall_progress: Optional[tqdm] = None + phase_progress: Optional[tqdm] = None + + # Progress tracking + total_phases: int = 3 + completed_phases: int = 0 + tty_available: bool = True + + # Position allocation + OVERALL_POSITION = 0 + PHASE_POSITION = 1 + + def __post_init__(self): + """Initialize TTY detection after dataclass creation.""" + self.tty_available = _PROGRESS_UTILS_AVAILABLE and is_tty_available() + + def start_overall_progress(self): + """Initialize the overall progress bar for apply phases.""" + if not self.tty_available: + return + + if self.overall_progress is None: + try: + self.overall_progress = create_positioned_tqdm( + position=self.OVERALL_POSITION, + description="Applying changes", + total=self.total_phases, + color=get_color_for_phase("overall"), + ) + except (TypeError, AttributeError): + # Handle case where fallback functions don't work as expected + self.overall_progress = None + + def start_phase(self, phase_name: str, operations_count: int = 0): + """ + Start tracking a new phase. + + Args: + phase_name: Human-readable name of the phase + operations_count: Number of operations in this phase (0 for unknown) + """ + if not self.tty_available: + return + + self.current_phase = phase_name + + # Close previous phase progress if exists + if self.phase_progress: + try: + self.phase_progress.close() + except (AttributeError, TypeError): + pass + self.phase_progress = None + + # Create new phase progress bar if operations are known + if operations_count > 0: + try: + self.phase_progress = create_positioned_tqdm( + position=self.PHASE_POSITION, + description=phase_name, + total=operations_count, + color=get_color_for_phase(phase_name.lower()), + ) + except (TypeError, AttributeError): + # Handle case where fallback functions don't work as expected + self.phase_progress = None + + def update_phase_progress(self, description: Optional[str] = None): + """ + Update progress within the current phase. + + Args: + description: Optional description of current operation + """ + if not self.tty_available or not self.phase_progress: + return + + try: + if description: + # Update postfix with current operation + self.phase_progress.set_postfix_str(description) + + self.phase_progress.update(1) + except (AttributeError, TypeError): + # Handle case where phase_progress is None or fallback function returned None + pass + + def complete_phase(self): + """Mark current phase as complete and advance overall progress.""" + if not self.tty_available: + return + + # Close phase progress + if self.phase_progress: + try: + self.phase_progress.close() + except (AttributeError, TypeError): + pass + self.phase_progress = None + + # Update overall progress + if self.overall_progress: + try: + self.overall_progress.update(1) + # Update postfix with phase completion + phase_text = f"({self.completed_phases + 1}/{self.total_phases} phases)" + self.overall_progress.set_postfix_str(phase_text) + except (AttributeError, TypeError): + pass + + self.completed_phases += 1 + + def cleanup(self): + """Clean up all progress bars. Should be called in finally blocks.""" + if self.phase_progress: + try: + self.phase_progress.close() + except (AttributeError, TypeError): + pass + self.phase_progress = None + if self.overall_progress: + try: + self.overall_progress.close() + except (AttributeError, TypeError): + pass + self.overall_progress = None diff --git a/sdk/python/feast/diff/infra_diff.py b/sdk/python/feast/diff/infra_diff.py index bffd655d2f5..2fa9e8e882e 100644 --- a/sdk/python/feast/diff/infra_diff.py +++ b/sdk/python/feast/diff/infra_diff.py @@ -1,5 +1,8 @@ from dataclasses import dataclass -from typing import Generic, Iterable, List, Tuple, TypeVar +from typing import TYPE_CHECKING, Generic, Iterable, List, Optional, Tuple, TypeVar + +if TYPE_CHECKING: + from feast.diff.apply_progress import ApplyProgressContext from feast.diff.property_diff import PropertyDiff, TransitionType from feast.infra.infra_object import ( @@ -33,8 +36,9 @@ class InfraDiff: def __init__(self): self.infra_object_diffs = [] - def update(self): + def update(self, progress_ctx: Optional["ApplyProgressContext"] = None): """Apply the infrastructure changes specified in this object.""" + for infra_object_diff in self.infra_object_diffs: if infra_object_diff.transition_type in [ TransitionType.DELETE, @@ -43,6 +47,10 @@ def update(self): infra_object = InfraObject.from_proto( infra_object_diff.current_infra_object ) + if progress_ctx: + progress_ctx.update_phase_progress( + f"Tearing down {infra_object_diff.name}" + ) infra_object.teardown() elif infra_object_diff.transition_type in [ TransitionType.CREATE, @@ -51,6 +59,10 @@ def update(self): infra_object = InfraObject.from_proto( infra_object_diff.new_infra_object ) + if progress_ctx: + progress_ctx.update_phase_progress( + f"Creating/updating {infra_object_diff.name}" + ) infra_object.update() def to_string(self): @@ -98,7 +110,9 @@ def tag_infra_proto_objects_for_keep_delete_add( def diff_infra_protos( - current_infra_proto: InfraProto, new_infra_proto: InfraProto + current_infra_proto: InfraProto, + new_infra_proto: InfraProto, + project: Optional[str] = None, ) -> InfraDiff: infra_diff = InfraDiff() @@ -114,6 +128,19 @@ def diff_infra_protos( new_infra_objects = get_infra_object_protos_by_type( new_infra_proto, infra_object_class_type ) + + # Filter infra objects by project prefix when using shared online stores + # Table names include project prefix: {project}_{table_name} + if project: + project_prefix = f"{project}_" + current_infra_objects = [ + obj + for obj in current_infra_objects + if obj.name.startswith(project_prefix) + ] + new_infra_objects = [ + obj for obj in new_infra_objects if obj.name.startswith(project_prefix) + ] ( infra_objects_to_keep, infra_objects_to_delete, diff --git a/sdk/python/feast/diff/registry_diff.py b/sdk/python/feast/diff/registry_diff.py index 272c4590d88..9c201cf46ad 100644 --- a/sdk/python/feast/diff/registry_diff.py +++ b/sdk/python/feast/diff/registry_diff.py @@ -1,416 +1,425 @@ -from dataclasses import dataclass -from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, TypeVar, cast - -from feast.base_feature_view import BaseFeatureView -from feast.data_source import DataSource -from feast.diff.property_diff import PropertyDiff, TransitionType -from feast.entity import Entity -from feast.feast_object import FeastObject, FeastObjectSpecProto -from feast.feature_service import FeatureService -from feast.feature_view import DUMMY_ENTITY_NAME -from feast.infra.registry.base_registry import BaseRegistry -from feast.infra.registry.registry import FEAST_OBJECT_TYPES, FeastObjectType -from feast.permissions.permission import Permission -from feast.project import Project -from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto -from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto -from feast.protos.feast.core.FeatureService_pb2 import ( - FeatureService as FeatureServiceProto, -) -from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto -from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( - OnDemandFeatureView as OnDemandFeatureViewProto, -) -from feast.protos.feast.core.OnDemandFeatureView_pb2 import OnDemandFeatureViewSpec -from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto -from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto -from feast.protos.feast.core.StreamFeatureView_pb2 import ( - StreamFeatureView as StreamFeatureViewProto, -) -from feast.protos.feast.core.ValidationProfile_pb2 import ( - ValidationReference as ValidationReferenceProto, -) -from feast.repo_contents import RepoContents - - -@dataclass -class FeastObjectDiff: - name: str - feast_object_type: FeastObjectType - current_feast_object: Optional[FeastObject] - new_feast_object: Optional[FeastObject] - feast_object_property_diffs: List[PropertyDiff] - transition_type: TransitionType - - -@dataclass -class RegistryDiff: - feast_object_diffs: List[FeastObjectDiff] - - def __init__(self): - self.feast_object_diffs = [] - - def add_feast_object_diff(self, feast_object_diff: FeastObjectDiff): - self.feast_object_diffs.append(feast_object_diff) - - def to_string(self): - from colorama import Fore, Style - - log_string = "" - - message_action_map = { - TransitionType.CREATE: ("Created", Fore.GREEN), - TransitionType.DELETE: ("Deleted", Fore.RED), - TransitionType.UNCHANGED: ("Unchanged", Fore.LIGHTBLUE_EX), - TransitionType.UPDATE: ("Updated", Fore.YELLOW), - } - for feast_object_diff in self.feast_object_diffs: - if feast_object_diff.name == DUMMY_ENTITY_NAME: - continue - if feast_object_diff.transition_type == TransitionType.UNCHANGED: - continue - if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: - # TODO(adchia): Print statements out starting in Feast 0.24 - continue - action, color = message_action_map[feast_object_diff.transition_type] - log_string += f"{action} {feast_object_diff.feast_object_type.value} {Style.BRIGHT + color}{feast_object_diff.name}{Style.RESET_ALL}\n" - if feast_object_diff.transition_type == TransitionType.UPDATE: - for _p in feast_object_diff.feast_object_property_diffs: - log_string += f"\t{_p.property_name}: {Style.BRIGHT + color}{_p.val_existing}{Style.RESET_ALL} -> {Style.BRIGHT + Fore.LIGHTGREEN_EX}{_p.val_declared}{Style.RESET_ALL}\n" - - log_string = ( - f"{Style.BRIGHT + Fore.LIGHTBLUE_EX}No changes to registry" - if not log_string - else log_string - ) - - return log_string - - -def tag_objects_for_keep_delete_update_add( - existing_objs: Iterable[FeastObject], desired_objs: Iterable[FeastObject] -) -> Tuple[Set[FeastObject], Set[FeastObject], Set[FeastObject], Set[FeastObject]]: - # TODO(adchia): Remove the "if X.name" condition when data sources are forced to have names - existing_obj_names = {e.name for e in existing_objs if e.name} - desired_objs = [obj for obj in desired_objs if obj.name] - existing_objs = [obj for obj in existing_objs if obj.name] - desired_obj_names = {e.name for e in desired_objs if e.name} - - objs_to_add = {e for e in desired_objs if e.name not in existing_obj_names} - objs_to_update = {e for e in desired_objs if e.name in existing_obj_names} - objs_to_keep = {e for e in existing_objs if e.name in desired_obj_names} - objs_to_delete = {e for e in existing_objs if e.name not in desired_obj_names} - - return objs_to_keep, objs_to_delete, objs_to_update, objs_to_add - - -FeastObjectProto = TypeVar( - "FeastObjectProto", - DataSourceProto, - EntityProto, - FeatureViewProto, - FeatureServiceProto, - OnDemandFeatureViewProto, - StreamFeatureViewProto, - ValidationReferenceProto, - SavedDatasetProto, - PermissionProto, -) - - -FIELDS_TO_IGNORE = {"project"} - - -def diff_registry_objects( - current: FeastObject, new: FeastObject, object_type: FeastObjectType -) -> FeastObjectDiff: - current_proto = current.to_proto() - new_proto = new.to_proto() - assert current_proto.DESCRIPTOR.full_name == new_proto.DESCRIPTOR.full_name - property_diffs = [] - transition: TransitionType = TransitionType.UNCHANGED - - current_spec: FeastObjectSpecProto - new_spec: FeastObjectSpecProto - if isinstance( - current_proto, (DataSourceProto, ValidationReferenceProto) - ) or isinstance(new_proto, (DataSourceProto, ValidationReferenceProto)): - assert type(current_proto) == type(new_proto) - current_spec = cast(DataSourceProto, current_proto) - new_spec = cast(DataSourceProto, new_proto) - else: - current_spec = current_proto.spec - new_spec = new_proto.spec - if current != new: - for _field in current_spec.DESCRIPTOR.fields: - if _field.name in FIELDS_TO_IGNORE: - continue - elif getattr(current_spec, _field.name) != getattr(new_spec, _field.name): - if _field.name == "feature_transformation": - current_spec = cast(OnDemandFeatureViewSpec, current_spec) - new_spec = cast(OnDemandFeatureViewSpec, new_spec) - # Check if the old proto is populated and use that if it is - feature_transformation_udf = ( - current_spec.feature_transformation.user_defined_function - ) - if ( - current_spec.HasField("user_defined_function") - and not feature_transformation_udf - ): - deprecated_udf = current_spec.user_defined_function - else: - deprecated_udf = None - current_udf = ( - deprecated_udf - if deprecated_udf is not None - else feature_transformation_udf - ) - new_udf = new_spec.feature_transformation.user_defined_function - for _udf_field in current_udf.DESCRIPTOR.fields: - if _udf_field.name == "body": - continue - if getattr(current_udf, _udf_field.name) != getattr( - new_udf, _udf_field.name - ): - transition = TransitionType.UPDATE - property_diffs.append( - PropertyDiff( - _field.name + "." + _udf_field.name, - getattr(current_udf, _udf_field.name), - getattr(new_udf, _udf_field.name), - ) - ) - else: - transition = TransitionType.UPDATE - property_diffs.append( - PropertyDiff( - _field.name, - getattr(current_spec, _field.name), - getattr(new_spec, _field.name), - ) - ) - return FeastObjectDiff( - name=new_spec.name, - feast_object_type=object_type, - current_feast_object=current, - new_feast_object=new, - feast_object_property_diffs=property_diffs, - transition_type=transition, - ) - - -def extract_objects_for_keep_delete_update_add( - registry: BaseRegistry, - current_project: str, - desired_repo_contents: RepoContents, -) -> Tuple[ - Dict[FeastObjectType, Set[FeastObject]], - Dict[FeastObjectType, Set[FeastObject]], - Dict[FeastObjectType, Set[FeastObject]], - Dict[FeastObjectType, Set[FeastObject]], -]: - """ - Returns the objects in the registry that must be modified to achieve the desired repo state. - - Args: - registry: The registry storing the current repo state. - current_project: The Feast project whose objects should be compared. - desired_repo_contents: The desired repo state. - """ - objs_to_keep = {} - objs_to_delete = {} - objs_to_update = {} - objs_to_add = {} - - registry_object_type_to_objects: Dict[FeastObjectType, List[Any]] = ( - FeastObjectType.get_objects_from_registry(registry, current_project) - ) - registry_object_type_to_repo_contents: Dict[FeastObjectType, List[Any]] = ( - FeastObjectType.get_objects_from_repo_contents(desired_repo_contents) - ) - - for object_type in FEAST_OBJECT_TYPES: - ( - to_keep, - to_delete, - to_update, - to_add, - ) = tag_objects_for_keep_delete_update_add( - registry_object_type_to_objects[object_type], - registry_object_type_to_repo_contents[object_type], - ) - - objs_to_keep[object_type] = to_keep - objs_to_delete[object_type] = to_delete - objs_to_update[object_type] = to_update - objs_to_add[object_type] = to_add - - return objs_to_keep, objs_to_delete, objs_to_update, objs_to_add - - -def diff_between( - registry: BaseRegistry, - current_project: str, - desired_repo_contents: RepoContents, -) -> RegistryDiff: - """ - Returns the difference between the current and desired repo states. - - Args: - registry: The registry storing the current repo state. - current_project: The Feast project for which the diff is being computed. - desired_repo_contents: The desired repo state. - """ - diff = RegistryDiff() - - ( - objs_to_keep, - objs_to_delete, - objs_to_update, - objs_to_add, - ) = extract_objects_for_keep_delete_update_add( - registry, current_project, desired_repo_contents - ) - - for object_type in FEAST_OBJECT_TYPES: - objects_to_keep = objs_to_keep[object_type] - objects_to_delete = objs_to_delete[object_type] - objects_to_update = objs_to_update[object_type] - objects_to_add = objs_to_add[object_type] - - for e in objects_to_add: - diff.add_feast_object_diff( - FeastObjectDiff( - name=e.name, - feast_object_type=object_type, - current_feast_object=None, - new_feast_object=e, - feast_object_property_diffs=[], - transition_type=TransitionType.CREATE, - ) - ) - for e in objects_to_delete: - diff.add_feast_object_diff( - FeastObjectDiff( - name=e.name, - feast_object_type=object_type, - current_feast_object=e, - new_feast_object=None, - feast_object_property_diffs=[], - transition_type=TransitionType.DELETE, - ) - ) - for e in objects_to_update: - current_obj = [_e for _e in objects_to_keep if _e.name == e.name][0] - diff.add_feast_object_diff( - diff_registry_objects(current_obj, e, object_type) - ) - - return diff - - -def apply_diff_to_registry( - registry: BaseRegistry, - registry_diff: RegistryDiff, - project: str, - commit: bool = True, -): - """ - Applies the given diff to the given Feast project in the registry. - - Args: - registry: The registry to be updated. - registry_diff: The diff to apply. - project: Feast project to be updated. - commit: Whether the change should be persisted immediately - """ - for feast_object_diff in registry_diff.feast_object_diffs: - # There is no need to delete the object on an update, since applying the new object - # will automatically delete the existing object. - if feast_object_diff.transition_type == TransitionType.DELETE: - if feast_object_diff.feast_object_type == FeastObjectType.ENTITY: - entity_obj = cast(Entity, feast_object_diff.current_feast_object) - registry.delete_entity(entity_obj.name, project, commit=False) - elif feast_object_diff.feast_object_type == FeastObjectType.FEATURE_SERVICE: - feature_service_obj = cast( - FeatureService, feast_object_diff.current_feast_object - ) - registry.delete_feature_service( - feature_service_obj.name, project, commit=False - ) - elif feast_object_diff.feast_object_type in [ - FeastObjectType.FEATURE_VIEW, - FeastObjectType.ON_DEMAND_FEATURE_VIEW, - FeastObjectType.STREAM_FEATURE_VIEW, - ]: - feature_view_obj = cast( - BaseFeatureView, feast_object_diff.current_feast_object - ) - registry.delete_feature_view( - feature_view_obj.name, - project, - commit=False, - ) - elif feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: - ds_obj = cast(DataSource, feast_object_diff.current_feast_object) - registry.delete_data_source( - ds_obj.name, - project, - commit=False, - ) - elif feast_object_diff.feast_object_type == FeastObjectType.PERMISSION: - permission_obj = cast( - Permission, feast_object_diff.current_feast_object - ) - registry.delete_permission( - permission_obj.name, - project, - commit=False, - ) - - if feast_object_diff.transition_type in [ - TransitionType.CREATE, - TransitionType.UPDATE, - ]: - if feast_object_diff.feast_object_type == FeastObjectType.PROJECT: - registry.apply_project( - cast(Project, feast_object_diff.new_feast_object), - commit=False, - ) - if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: - registry.apply_data_source( - cast(DataSource, feast_object_diff.new_feast_object), - project, - commit=False, - ) - if feast_object_diff.feast_object_type == FeastObjectType.ENTITY: - registry.apply_entity( - cast(Entity, feast_object_diff.new_feast_object), - project, - commit=False, - ) - elif feast_object_diff.feast_object_type == FeastObjectType.FEATURE_SERVICE: - registry.apply_feature_service( - cast(FeatureService, feast_object_diff.new_feast_object), - project, - commit=False, - ) - elif feast_object_diff.feast_object_type in [ - FeastObjectType.FEATURE_VIEW, - FeastObjectType.ON_DEMAND_FEATURE_VIEW, - FeastObjectType.STREAM_FEATURE_VIEW, - ]: - registry.apply_feature_view( - cast(BaseFeatureView, feast_object_diff.new_feast_object), - project, - commit=False, - ) - elif feast_object_diff.feast_object_type == FeastObjectType.PERMISSION: - registry.apply_permission( - cast(Permission, feast_object_diff.new_feast_object), - project, - commit=False, - ) - - if commit: - registry.commit() +from dataclasses import dataclass +from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, TypeVar, cast + +from feast.base_feature_view import BaseFeatureView +from feast.data_source import DataSource +from feast.diff.property_diff import PropertyDiff, TransitionType +from feast.entity import Entity +from feast.feast_object import FeastObject, FeastObjectSpecProto +from feast.feature_service import FeatureService +from feast.feature_view import DUMMY_ENTITY_NAME +from feast.infra.registry.base_registry import BaseRegistry +from feast.infra.registry.registry import FEAST_OBJECT_TYPES, FeastObjectType +from feast.permissions.permission import Permission +from feast.project import Project +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto +from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto +from feast.protos.feast.core.FeatureService_pb2 import ( + FeatureService as FeatureServiceProto, +) +from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto +from feast.protos.feast.core.LabelView_pb2 import LabelView as LabelViewProto +from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( + OnDemandFeatureView as OnDemandFeatureViewProto, +) +from feast.protos.feast.core.OnDemandFeatureView_pb2 import OnDemandFeatureViewSpec +from feast.protos.feast.core.Permission_pb2 import Permission as PermissionProto +from feast.protos.feast.core.SavedDataset_pb2 import SavedDataset as SavedDatasetProto +from feast.protos.feast.core.StreamFeatureView_pb2 import ( + StreamFeatureView as StreamFeatureViewProto, +) +from feast.protos.feast.core.ValidationProfile_pb2 import ( + ValidationReference as ValidationReferenceProto, +) +from feast.repo_contents import RepoContents + + +@dataclass +class FeastObjectDiff: + name: str + feast_object_type: FeastObjectType + current_feast_object: Optional[FeastObject] + new_feast_object: Optional[FeastObject] + feast_object_property_diffs: List[PropertyDiff] + transition_type: TransitionType + + +@dataclass +class RegistryDiff: + feast_object_diffs: List[FeastObjectDiff] + + def __init__(self): + self.feast_object_diffs = [] + + def add_feast_object_diff(self, feast_object_diff: FeastObjectDiff): + self.feast_object_diffs.append(feast_object_diff) + + def to_string(self): + from colorama import Fore, Style + + log_string = "" + + message_action_map = { + TransitionType.CREATE: ("Created", Fore.GREEN), + TransitionType.DELETE: ("Deleted", Fore.RED), + TransitionType.UNCHANGED: ("Unchanged", Fore.LIGHTBLUE_EX), + TransitionType.UPDATE: ("Updated", Fore.YELLOW), + } + for feast_object_diff in self.feast_object_diffs: + if feast_object_diff.name == DUMMY_ENTITY_NAME: + continue + if feast_object_diff.transition_type == TransitionType.UNCHANGED: + continue + if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: + # TODO(adchia): Print statements out starting in Feast 0.24 + continue + action, color = message_action_map[feast_object_diff.transition_type] + log_string += f"{action} {feast_object_diff.feast_object_type.value} {Style.BRIGHT + color}{feast_object_diff.name}{Style.RESET_ALL}\n" + if feast_object_diff.transition_type == TransitionType.UPDATE: + for _p in feast_object_diff.feast_object_property_diffs: + log_string += f"\t{_p.property_name}: {Style.BRIGHT + color}{_p.val_existing}{Style.RESET_ALL} -> {Style.BRIGHT + Fore.LIGHTGREEN_EX}{_p.val_declared}{Style.RESET_ALL}\n" + + log_string = ( + f"{Style.BRIGHT + Fore.LIGHTBLUE_EX}No changes to registry" + if not log_string + else log_string + ) + + return log_string + + +def tag_objects_for_keep_delete_update_add( + existing_objs: Iterable[FeastObject], desired_objs: Iterable[FeastObject] +) -> Tuple[Set[FeastObject], Set[FeastObject], Set[FeastObject], Set[FeastObject]]: + # TODO(adchia): Remove the "if X.name" condition when data sources are forced to have names + existing_obj_names = {e.name for e in existing_objs if e.name} + desired_objs = [obj for obj in desired_objs if obj.name] + existing_objs = [obj for obj in existing_objs if obj.name] + desired_obj_names = {e.name for e in desired_objs if e.name} + + objs_to_add = {e for e in desired_objs if e.name not in existing_obj_names} + objs_to_update = {e for e in desired_objs if e.name in existing_obj_names} + objs_to_keep = {e for e in existing_objs if e.name in desired_obj_names} + objs_to_delete = {e for e in existing_objs if e.name not in desired_obj_names} + + return objs_to_keep, objs_to_delete, objs_to_update, objs_to_add + + +FeastObjectProto = TypeVar( + "FeastObjectProto", + DataSourceProto, + EntityProto, + FeatureViewProto, + FeatureServiceProto, + OnDemandFeatureViewProto, + StreamFeatureViewProto, + LabelViewProto, + ValidationReferenceProto, + SavedDatasetProto, + PermissionProto, +) + + +FIELDS_TO_IGNORE = {"project"} + + +def diff_registry_objects( + current: FeastObject, new: FeastObject, object_type: FeastObjectType +) -> FeastObjectDiff: + current_proto = current.to_proto() + new_proto = new.to_proto() + assert current_proto.DESCRIPTOR.full_name == new_proto.DESCRIPTOR.full_name + property_diffs = [] + transition: TransitionType = TransitionType.UNCHANGED + + current_spec: FeastObjectSpecProto + new_spec: FeastObjectSpecProto + if isinstance( + current_proto, (DataSourceProto, ValidationReferenceProto) + ) or isinstance(new_proto, (DataSourceProto, ValidationReferenceProto)): + assert type(current_proto) == type(new_proto) + current_spec = cast(DataSourceProto, current_proto) + new_spec = cast(DataSourceProto, new_proto) + else: + current_spec = current_proto.spec + new_spec = new_proto.spec + if current != new: + for _field in current_spec.DESCRIPTOR.fields: + if _field.name in FIELDS_TO_IGNORE: + continue + elif getattr(current_spec, _field.name) != getattr(new_spec, _field.name): + if _field.name == "feature_transformation": + current_spec = cast(OnDemandFeatureViewSpec, current_spec) + new_spec = cast(OnDemandFeatureViewSpec, new_spec) + # Check if the old proto is populated and use that if it is + feature_transformation_udf = ( + current_spec.feature_transformation.user_defined_function + ) + if ( + current_spec.HasField("user_defined_function") + and not feature_transformation_udf + ): + deprecated_udf = current_spec.user_defined_function + else: + deprecated_udf = None + current_udf = ( + deprecated_udf + if deprecated_udf is not None + else feature_transformation_udf + ) + new_udf = new_spec.feature_transformation.user_defined_function + for _udf_field in current_udf.DESCRIPTOR.fields: + if _udf_field.name == "body": + continue + if getattr(current_udf, _udf_field.name) != getattr( + new_udf, _udf_field.name + ): + transition = TransitionType.UPDATE + property_diffs.append( + PropertyDiff( + _field.name + "." + _udf_field.name, + getattr(current_udf, _udf_field.name), + getattr(new_udf, _udf_field.name), + ) + ) + else: + transition = TransitionType.UPDATE + property_diffs.append( + PropertyDiff( + _field.name, + getattr(current_spec, _field.name), + getattr(new_spec, _field.name), + ) + ) + return FeastObjectDiff( + name=new_spec.name, + feast_object_type=object_type, + current_feast_object=current, + new_feast_object=new, + feast_object_property_diffs=property_diffs, + transition_type=transition, + ) + + +def extract_objects_for_keep_delete_update_add( + registry: BaseRegistry, + current_project: str, + desired_repo_contents: RepoContents, +) -> Tuple[ + Dict[FeastObjectType, Set[FeastObject]], + Dict[FeastObjectType, Set[FeastObject]], + Dict[FeastObjectType, Set[FeastObject]], + Dict[FeastObjectType, Set[FeastObject]], +]: + """ + Returns the objects in the registry that must be modified to achieve the desired repo state. + + Args: + registry: The registry storing the current repo state. + current_project: The Feast project whose objects should be compared. + desired_repo_contents: The desired repo state. + """ + objs_to_keep = {} + objs_to_delete = {} + objs_to_update = {} + objs_to_add = {} + + registry_object_type_to_objects: Dict[FeastObjectType, List[Any]] = ( + FeastObjectType.get_objects_from_registry(registry, current_project) + ) + registry_object_type_to_repo_contents: Dict[FeastObjectType, List[Any]] = ( + FeastObjectType.get_objects_from_repo_contents(desired_repo_contents) + ) + + for object_type in FEAST_OBJECT_TYPES: + ( + to_keep, + to_delete, + to_update, + to_add, + ) = tag_objects_for_keep_delete_update_add( + registry_object_type_to_objects[object_type], + registry_object_type_to_repo_contents[object_type], + ) + + objs_to_keep[object_type] = to_keep + objs_to_delete[object_type] = to_delete + objs_to_update[object_type] = to_update + objs_to_add[object_type] = to_add + + return objs_to_keep, objs_to_delete, objs_to_update, objs_to_add + + +def diff_between( + registry: BaseRegistry, + current_project: str, + desired_repo_contents: RepoContents, +) -> RegistryDiff: + """ + Returns the difference between the current and desired repo states. + + Args: + registry: The registry storing the current repo state. + current_project: The Feast project for which the diff is being computed. + desired_repo_contents: The desired repo state. + """ + diff = RegistryDiff() + + ( + objs_to_keep, + objs_to_delete, + objs_to_update, + objs_to_add, + ) = extract_objects_for_keep_delete_update_add( + registry, current_project, desired_repo_contents + ) + + for object_type in FEAST_OBJECT_TYPES: + objects_to_keep = objs_to_keep[object_type] + objects_to_delete = objs_to_delete[object_type] + objects_to_update = objs_to_update[object_type] + objects_to_add = objs_to_add[object_type] + + for e in objects_to_add: + diff.add_feast_object_diff( + FeastObjectDiff( + name=e.name, + feast_object_type=object_type, + current_feast_object=None, + new_feast_object=e, + feast_object_property_diffs=[], + transition_type=TransitionType.CREATE, + ) + ) + for e in objects_to_delete: + diff.add_feast_object_diff( + FeastObjectDiff( + name=e.name, + feast_object_type=object_type, + current_feast_object=e, + new_feast_object=None, + feast_object_property_diffs=[], + transition_type=TransitionType.DELETE, + ) + ) + for e in objects_to_update: + current_obj = [_e for _e in objects_to_keep if _e.name == e.name][0] + diff.add_feast_object_diff( + diff_registry_objects(current_obj, e, object_type) + ) + + return diff + + +def apply_diff_to_registry( + registry: BaseRegistry, + registry_diff: RegistryDiff, + project: str, + commit: bool = True, + no_promote: bool = False, +): + """ + Applies the given diff to the given Feast project in the registry. + + Args: + registry: The registry to be updated. + registry_diff: The diff to apply. + project: Feast project to be updated. + commit: Whether the change should be persisted immediately + no_promote: If True, save new feature view version snapshots without + promoting them to the active definition. New versions are accessible + only via explicit @v reads. + """ + for feast_object_diff in registry_diff.feast_object_diffs: + # There is no need to delete the object on an update, since applying the new object + # will automatically delete the existing object. + if feast_object_diff.transition_type == TransitionType.DELETE: + if feast_object_diff.feast_object_type == FeastObjectType.ENTITY: + entity_obj = cast(Entity, feast_object_diff.current_feast_object) + registry.delete_entity(entity_obj.name, project, commit=False) + elif feast_object_diff.feast_object_type == FeastObjectType.FEATURE_SERVICE: + feature_service_obj = cast( + FeatureService, feast_object_diff.current_feast_object + ) + registry.delete_feature_service( + feature_service_obj.name, project, commit=False + ) + elif feast_object_diff.feast_object_type in [ + FeastObjectType.FEATURE_VIEW, + FeastObjectType.ON_DEMAND_FEATURE_VIEW, + FeastObjectType.STREAM_FEATURE_VIEW, + FeastObjectType.LABEL_VIEW, + ]: + feature_view_obj = cast( + BaseFeatureView, feast_object_diff.current_feast_object + ) + registry.delete_feature_view( + feature_view_obj.name, + project, + commit=False, + ) + elif feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: + ds_obj = cast(DataSource, feast_object_diff.current_feast_object) + registry.delete_data_source( + ds_obj.name, + project, + commit=False, + ) + elif feast_object_diff.feast_object_type == FeastObjectType.PERMISSION: + permission_obj = cast( + Permission, feast_object_diff.current_feast_object + ) + registry.delete_permission( + permission_obj.name, + project, + commit=False, + ) + + if feast_object_diff.transition_type in [ + TransitionType.CREATE, + TransitionType.UPDATE, + ]: + if feast_object_diff.feast_object_type == FeastObjectType.PROJECT: + registry.apply_project( + cast(Project, feast_object_diff.new_feast_object), + commit=False, + ) + if feast_object_diff.feast_object_type == FeastObjectType.DATA_SOURCE: + registry.apply_data_source( + cast(DataSource, feast_object_diff.new_feast_object), + project, + commit=False, + ) + if feast_object_diff.feast_object_type == FeastObjectType.ENTITY: + registry.apply_entity( + cast(Entity, feast_object_diff.new_feast_object), + project, + commit=False, + ) + elif feast_object_diff.feast_object_type == FeastObjectType.FEATURE_SERVICE: + registry.apply_feature_service( + cast(FeatureService, feast_object_diff.new_feast_object), + project, + commit=False, + ) + elif feast_object_diff.feast_object_type in [ + FeastObjectType.FEATURE_VIEW, + FeastObjectType.ON_DEMAND_FEATURE_VIEW, + FeastObjectType.STREAM_FEATURE_VIEW, + FeastObjectType.LABEL_VIEW, + ]: + registry.apply_feature_view( + cast(BaseFeatureView, feast_object_diff.new_feast_object), + project, + commit=False, + no_promote=no_promote, + ) + elif feast_object_diff.feast_object_type == FeastObjectType.PERMISSION: + registry.apply_permission( + cast(Permission, feast_object_diff.new_feast_object), + project, + commit=False, + ) + + if commit: + registry.commit() diff --git a/sdk/python/feast/doc_embedder.py b/sdk/python/feast/doc_embedder.py new file mode 100644 index 00000000000..9ccd8ec461b --- /dev/null +++ b/sdk/python/feast/doc_embedder.py @@ -0,0 +1,380 @@ +import inspect +import os +import warnings +from pathlib import Path +from typing import TYPE_CHECKING, Callable, Optional, Protocol, runtime_checkable + +import pandas as pd + +from feast.chunker import BaseChunker, TextChunker +from feast.embedder import BaseEmbedder, MultiModalEmbedder + +if TYPE_CHECKING: + from feast.feature_store import FeatureStore + + +@runtime_checkable +class SchemaTransformFn(Protocol): + """ + Protocol defining the structure for schema transform functions. + + The schema transform converts the output of Chunker + Embedder + into the format expected by the FeatureView schema. + """ + + def __call__(self, df: pd.DataFrame) -> pd.DataFrame: + """ + Transform chunked + embedded DataFrame to FeatureView schema. + + Args: + df: Input DataFrame with chunks and embeddings. + + Returns: + DataFrame with columns matching FeatureView schema. + """ + ... + + +def default_schema_transform_fn(df: pd.DataFrame) -> pd.DataFrame: + """ + Default schema transform function that transforms the output of Chunker + Embedder + into the format expected by the FeatureView schema. + """ + from datetime import datetime, timezone + + return pd.DataFrame( + { + "passage_id": df["chunk_id"], + "text": df["text"], + "embedding": df["text_embedding"], + "event_timestamp": [datetime.now(timezone.utc)] * len(df), + "source_id": df["original_id"], + } + ) + + +def generate_repo_file( + repo_path: str, + feature_view_name: str = "text_feature_view", + vector_length: int = 384, +) -> str: + """ + Generate a Python file with Entity and FeatureView definitions. + + This file is compatible with `feast apply` CLI. + + Args: + repo_path: Path to the feature repo directory. + feature_view_name: Name of the feature view to create. + vector_length: Dimension of the embedding vectors. Should match the + output dimension of the embedding model being used. Defaults to + 384 (matching the default all-MiniLM-L6-v2 model). + + Returns: + Path to generated file. + """ + from feast.repo_operations import is_valid_name + + if not is_valid_name(feature_view_name) or not feature_view_name.isidentifier(): + raise ValueError( + f"feature_view_name '{feature_view_name}' is invalid. " + "It should only contain alphanumeric characters, underscores, " + "and must not start with an underscore." + ) + code = f'''""" +Auto-generated by DocEmbedder. +Compatible with `feast apply` CLI. +""" +from datetime import timedelta + +from feast import Entity, FeatureView, Field, FileSource +from feast.types import Array, Float32, String, ValueType + + +# Entity +text_entity = Entity( + name="passage_id", + join_keys=["passage_id"], + description="Passage identifier", + value_type=ValueType.STRING, +) + +# Source +{feature_view_name.replace(" ", "_").replace("-", "_")}_source = FileSource( + name="{feature_view_name}_source", + path="data/{feature_view_name}.parquet", + timestamp_field="event_timestamp", +) + +# FeatureView +{feature_view_name.replace(" ", "_").replace("-", "_")} = FeatureView( + name="{feature_view_name}", + entities=[text_entity], + ttl=timedelta(days=1), + schema=[ + Field( + name="text", + dtype=String, + description="Document text content", + ), + Field( + name="embedding", + dtype=Array(Float32), + description="Vector embedding", + vector_index=True, + vector_length={vector_length}, + vector_search_metric="COSINE", + ), + Field( + name="source_id", + dtype=String, + description="Source ID", + ), + ], + source={feature_view_name.replace(" ", "_").replace("-", "_")}_source, + online=True, +) +''' + + filepath = os.path.join( + repo_path, feature_view_name.replace(" ", "_").replace("-", "_") + ".py" + ) + with open(filepath, "w") as f: + f.write(code) + + return filepath + + +class DocEmbedder: + """ + DocEmbedder is a class that embeds documents and chunks them into a format expected by the FeatureView schema using a Logic Implementation By the user. + + Args: + repo_path: Path to the feature repo (can be "." for current directory). + yaml_file: Name of the feature_store.yaml file inside repo_path. + Defaults to "feature_store.yaml". + feature_view_name: Name of the feature view to create. + chunker: Chunker to use for chunking the documents. + embedder: Embedder to use for embedding the documents. + schema_transform_fn: Schema transform function to use for transforming the output of the chunker and embedder into the format expected by the FeatureView schema. + create_feature_view: Whether to create a feature view in the feature repo. By default it will generate a Python file with the FeatureView definition. + vector_length: Explicit embedding dimension for the generated FeatureView schema. + If None (default), the dimension is auto-detected from the embedder + via ``get_embedding_dim("text")``. Falls back to 384 if detection + is not supported by the embedder. + auto_apply_repo: Whether to apply the repository automatically. By default it will apply the repository after creating the feature view. + """ + + def __init__( + self, + repo_path: str, + yaml_file: str = "feature_store.yaml", + feature_view_name: str = "text_feature_view", + chunker: Optional[BaseChunker] = None, + embedder: Optional[BaseEmbedder] = None, + schema_transform_fn: SchemaTransformFn = default_schema_transform_fn, + create_feature_view: bool = True, + vector_length: Optional[int] = None, + auto_apply_repo: bool = True, + ): + self.repo_path = repo_path + self.yaml_path = os.path.join(Path(repo_path).resolve(), yaml_file) + self.feature_view_name = feature_view_name + self.chunker = chunker or TextChunker() + self.embedder = embedder or MultiModalEmbedder() + self.store: Optional[FeatureStore] = None + + sig = inspect.signature(schema_transform_fn) + params = list(sig.parameters.values()) + if ( + len(params) != 1 + or params[0].annotation != pd.DataFrame + or sig.return_annotation != pd.DataFrame + ): + raise ValueError( + "schema_transform_fn must be a function that takes a DataFrame and returns a DataFrame" + ) + self.schema_transform_fn = schema_transform_fn + if create_feature_view: + resolved_vector_length = self._resolve_vector_length(vector_length, "text") + generate_repo_file( + repo_path=repo_path, + feature_view_name=feature_view_name, + vector_length=resolved_vector_length, + ) + if auto_apply_repo: + self.apply_repo() + + def _resolve_vector_length( + self, explicit_length: Optional[int], modality: str + ) -> int: + """ + Determine the vector length to use for the generated FeatureView. + + Priority: + 1. Explicitly provided vector_length + 2. Auto-detected from embedder via get_embedding_dim("text") + 3. Default of 384 (matching all-MiniLM-L6-v2) + + Args: + explicit_length: User-provided vector length, or None. + + Returns: + The resolved vector length as an integer. + """ + _DEFAULT_VECTOR_LENGTH = 384 + + if explicit_length is not None: + return explicit_length + + try: + dim = self.embedder.get_embedding_dim(modality) + if dim is not None: + return dim + except Exception: + pass + + return _DEFAULT_VECTOR_LENGTH + + def save_to_online_store(self, df: pd.DataFrame, feature_view_name: str) -> None: + """ + Save the embedded documents to the online store. + """ + from feast.feature_store import FeatureStore + + if self.store is None: + self.store = FeatureStore(repo_path=self.repo_path) + self.store.write_to_online_store( + feature_view_name=feature_view_name, + df=df, + ) + + # TODO (Future scope): Implement save_to_offline_store to write embedded + # documents to the offline store. Currently blocked by DaskOfflineStore + # .offline_write_batch not creating the parquet file if it does not exist. + # Once that is fixed, add a method that calls: + # store = FeatureStore(repo_path=self.repo_path) + # store.write_to_offline_store(feature_view_name=feature_view_name, df=df) + + def apply_repo(self) -> None: + """ + Apply the repository to register feature views in the registry. + """ + from feast.repo_config import load_repo_config + from feast.repo_operations import apply_total + + original_cwd = None + try: + original_cwd = os.getcwd() + repo_path = Path(self.repo_path).resolve() + config = load_repo_config( + repo_path=repo_path, + fs_yaml_file=Path(self.yaml_path), + ) + apply_total( + repo_config=config, + repo_path=repo_path, + skip_source_validation=True, + ) + finally: + if original_cwd is not None: + os.chdir(original_cwd) + + def embed_documents( + self, + documents: pd.DataFrame, + id_column: str, + source_column: str, + type_column: Optional[str] = None, + column_mapping: Optional[tuple[str, str]] = None, + custom_schema_transform_fn: Optional[ + Callable[[pd.DataFrame], pd.DataFrame] + ] = None, + ) -> pd.DataFrame: + """ + Embed a list of documents and chunk them into a format expected by the FeatureView schema using a Logic Implementation By the user and save the DataFrame to the online store. + + Args: + documents: DataFrame containing the documents to embed. + id_column: Column name containing the document IDs. + source_column: Column name containing the document sources. + type_column: Column name containing the document types. + column_mapping: Tuple mapping source columns to (modality, output column). + custom_schema_transform_fn: Custom schema transform function to use for transforming the output of the chunker and embedder into the format expected by the FeatureView schema. + Returns: + DataFrame with the embedded documents. + + Example: + documents = pd.DataFrame({ + "id": [1, 2, 3], + "source": ["source1", "source2", "source3"], + "type": ["type1", "type2", "type3"], + "text": ["text1", "text2", "text3"], + }) + column_mapping = ("text", "text_embedding") + df = embed_documents(documents=documents, id_column="id", source_column="source", type_column="type", column_mapping=column_mapping) + + """ + if custom_schema_transform_fn is not None: + sig = inspect.signature(custom_schema_transform_fn) + params = list(sig.parameters.values()) + if ( + len(params) != 1 + or params[0].annotation != pd.DataFrame + or sig.return_annotation != pd.DataFrame + ): + raise ValueError( + "custom_schema_transform_fn must be a function that takes a DataFrame and returns a DataFrame" + ) + current_schema_transform_fn = ( + custom_schema_transform_fn + if custom_schema_transform_fn is not None + else self.schema_transform_fn + ) + + if column_mapping is None: + column_mapping = ("text", "text_embedding") + + if ( + current_schema_transform_fn is default_schema_transform_fn + and column_mapping[0] == "text" + and (source_column != "text" or column_mapping[1] != "text_embedding") + ): + raise ValueError( + f"source_column='{source_column}' with output column='{column_mapping[1]}' " + f"is not compatible with default_schema_transform_fn, which expects " + f"source_column='text' and column_mapping=('text', 'text_embedding'). " + f"Provide a custom schema_transform_fn." + ) + + if column_mapping[0] == "text": + df = self.chunker.chunk_dataframe( + df=documents, + id_column=id_column, + source_column=source_column, + type_column=type_column, + ) + else: + df = documents + + df = self.embedder.embed_dataframe( + df, column_mapping={source_column: column_mapping} + ) + + if ( + column_mapping[0] == "text" + or current_schema_transform_fn is not default_schema_transform_fn + ): + df = current_schema_transform_fn(df) + else: + warnings.warn( + f"Modality '{column_mapping[0]}' is not supported by the default schema transform function. " + f"The output DataFrame will be passed directly to the online store. " + f"Ensure your FeatureView schema matches the output columns. " + f"You can provide a custom schema transform function to handle this.", + UserWarning, + stacklevel=2, + ) + + self.save_to_online_store(df=df, feature_view_name=self.feature_view_name) + return df diff --git a/sdk/python/feast/driver_test_data.py b/sdk/python/feast/driver_test_data.py index d96c9c6d387..39b7faf22c2 100644 --- a/sdk/python/feast/driver_test_data.py +++ b/sdk/python/feast/driver_test_data.py @@ -136,10 +136,38 @@ def create_driver_hourly_stats_df(drivers, start_date, end_date) -> pd.DataFrame df_all_drivers["conv_rate"] = np.random.random(size=rows).astype(np.float32) df_all_drivers["acc_rate"] = np.random.random(size=rows).astype(np.float32) df_all_drivers["avg_daily_trips"] = np.random.randint(0, 1000, size=rows).astype( - np.int32 + np.int64 ) df_all_drivers["created"] = pd.to_datetime(pd.Timestamp.now(tz=None).round("ms")) + # Complex type columns for Map, Json, and Struct examples + import json as _json + + df_all_drivers["driver_metadata"] = [ + { + "vehicle_type": np.random.choice(["sedan", "suv", "truck"]), + "rating": str(round(np.random.uniform(3.0, 5.0), 1)), + } + for _ in range(len(df_all_drivers)) + ] + df_all_drivers["driver_config"] = [ + _json.dumps( + { + "max_distance_km": int(np.random.randint(10, 200)), + "preferred_zones": list( + np.random.choice( + ["north", "south", "east", "west"], size=2, replace=False + ) + ), + } + ) + for _ in range(len(df_all_drivers)) + ] + df_all_drivers["driver_profile"] = [ + {"name": f"driver_{driver_id}", "age": str(int(np.random.randint(25, 60)))} + for driver_id in df_all_drivers["driver_id"] + ] + # Create duplicate rows that should be filtered by created timestamp # TODO: These duplicate rows area indirectly being filtered out by the point in time join already. We need to # inject a bad row at a timestamp where we know it will get joined to the entity dataframe, and then test that diff --git a/sdk/python/feast/embedded_go/online_features_service.py b/sdk/python/feast/embedded_go/online_features_service.py index 8dd7b5ba0a1..cc54808d4e2 100644 --- a/sdk/python/feast/embedded_go/online_features_service.py +++ b/sdk/python/feast/embedded_go/online_features_service.py @@ -1,345 +1,346 @@ -import logging -from functools import partial -from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union - -import pyarrow as pa -from google.protobuf.timestamp_pb2 import Timestamp -from pyarrow.cffi import ffi - -from feast.errors import ( - FeatureNameCollisionError, - RequestDataNotFoundInEntityRowsException, -) -from feast.feature_service import FeatureService -from feast.infra.feature_servers.base_config import FeatureLoggingConfig -from feast.online_response import OnlineResponse -from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse -from feast.protos.feast.types import Value_pb2 -from feast.repo_config import RepoConfig -from feast.types import from_value_type -from feast.value_type import ValueType - -from .lib.embedded import ( - DataTable, - LoggingOptions, - NewOnlineFeatureService, - OnlineFeatureServiceConfig, -) -from .lib.go import Slice_string -from .type_map import FEAST_TYPE_TO_ARROW_TYPE, arrow_array_to_array_of_proto - -if TYPE_CHECKING: - from feast.feature_store import FeatureStore - -NANO_SECOND = 1 -MICRO_SECOND = 1000 * NANO_SECOND -MILLI_SECOND = 1000 * MICRO_SECOND -SECOND = 1000 * MILLI_SECOND - -logger = logging.getLogger(__name__) - - -class EmbeddedOnlineFeatureServer: - def __init__( - self, repo_path: str, repo_config: RepoConfig, feature_store: "FeatureStore" - ): - # keep callback in self to prevent it from GC - self._transformation_callback = partial(transformation_callback, feature_store) - self._logging_callback = partial(logging_callback, feature_store) - - self._config = OnlineFeatureServiceConfig( - RepoPath=repo_path, RepoConfig=repo_config.json() - ) - - self._service = NewOnlineFeatureService( - self._config, - self._transformation_callback, - ) - - # This should raise an exception if there were any errors in NewOnlineFeatureService. - self._service.CheckForInstantiationError() - - def get_online_features( - self, - features_refs: List[str], - feature_service: Optional[FeatureService], - entities: Dict[str, Union[List[Any], Value_pb2.RepeatedValue]], - request_data: Dict[str, Union[List[Any], Value_pb2.RepeatedValue]], - full_feature_names: bool = False, - ): - if feature_service: - join_keys_types = self._service.GetEntityTypesMapByFeatureService( - feature_service.name - ) - else: - join_keys_types = self._service.GetEntityTypesMap( - Slice_string(features_refs) - ) - - join_keys_types = { - join_key: ValueType(enum_value) for join_key, enum_value in join_keys_types - } - - # Here we create C structures that will be shared between Python and Go. - # We will pass entities as arrow Record Batch to Go part (in_c_array & in_c_schema) - # and receive features as Record Batch from Go (out_c_array & out_c_schema) - # This objects needs to be initialized here in order to correctly - # free them later using Python GC. - ( - entities_c_schema, - entities_ptr_schema, - entities_c_array, - entities_ptr_array, - ) = allocate_schema_and_array() - ( - req_data_c_schema, - req_data_ptr_schema, - req_data_c_array, - req_data_ptr_array, - ) = allocate_schema_and_array() - - ( - features_c_schema, - features_ptr_schema, - features_c_array, - features_ptr_array, - ) = allocate_schema_and_array() - - batch, schema = map_to_record_batch(entities, join_keys_types) - schema._export_to_c(entities_ptr_schema) - batch._export_to_c(entities_ptr_array) - - batch, schema = map_to_record_batch(request_data) - schema._export_to_c(req_data_ptr_schema) - batch._export_to_c(req_data_ptr_array) - - try: - self._service.GetOnlineFeatures( - featureRefs=Slice_string(features_refs), - featureServiceName=feature_service and feature_service.name or "", - entities=DataTable( - SchemaPtr=entities_ptr_schema, DataPtr=entities_ptr_array - ), - requestData=DataTable( - SchemaPtr=req_data_ptr_schema, DataPtr=req_data_ptr_array - ), - fullFeatureNames=full_feature_names, - output=DataTable( - SchemaPtr=features_ptr_schema, DataPtr=features_ptr_array - ), - ) - except RuntimeError as exc: - (msg,) = exc.args - if msg.startswith("featureNameCollisionError"): - feature_refs = msg[len("featureNameCollisionError: ") : msg.find(";")] - feature_refs = feature_refs.split(",") - raise FeatureNameCollisionError( - feature_refs_collisions=feature_refs, - full_feature_names=full_feature_names, - ) - - if msg.startswith("requestDataNotFoundInEntityRowsException"): - feature_refs = msg[len("requestDataNotFoundInEntityRowsException: ") :] - feature_refs = feature_refs.split(",") - raise RequestDataNotFoundInEntityRowsException(feature_refs) - - raise - - record_batch = pa.RecordBatch._import_from_c( - features_ptr_array, features_ptr_schema - ) - resp = record_batch_to_online_response(record_batch) - del record_batch - return OnlineResponse(resp) - - def start_grpc_server( - self, - host: str, - port: int, - enable_logging: bool = True, - logging_options: Optional[FeatureLoggingConfig] = None, - ): - if enable_logging: - if logging_options: - self._service.StartGprcServerWithLogging( - host, - port, - self._logging_callback, - LoggingOptions( - FlushInterval=logging_options.flush_interval_secs * SECOND, - WriteInterval=logging_options.write_to_disk_interval_secs - * SECOND, - EmitTimeout=logging_options.emit_timeout_micro_secs - * MICRO_SECOND, - ChannelCapacity=logging_options.queue_capacity, - ), - ) - else: - self._service.StartGprcServerWithLoggingDefaultOpts( - host, port, self._logging_callback - ) - else: - self._service.StartGprcServer(host, port) - - def start_http_server( - self, - host: str, - port: int, - enable_logging: bool = True, - logging_options: Optional[FeatureLoggingConfig] = None, - ): - if enable_logging: - if logging_options: - self._service.StartHttpServerWithLogging( - host, - port, - self._logging_callback, - LoggingOptions( - FlushInterval=logging_options.flush_interval_secs * SECOND, - WriteInterval=logging_options.write_to_disk_interval_secs - * SECOND, - EmitTimeout=logging_options.emit_timeout_micro_secs - * MICRO_SECOND, - ChannelCapacity=logging_options.queue_capacity, - ), - ) - else: - self._service.StartHttpServerWithLoggingDefaultOpts( - host, port, self._logging_callback - ) - else: - self._service.StartHttpServer(host, port) - - def stop_grpc_server(self): - self._service.StopGrpcServer() - - def stop_http_server(self): - self._service.StopHttpServer() - - -def _to_arrow(value, type_hint: Optional[ValueType]) -> pa.Array: - if isinstance(value, Value_pb2.RepeatedValue): - _proto_to_arrow(value) - - if type_hint: - feast_type = from_value_type(type_hint) - if feast_type in FEAST_TYPE_TO_ARROW_TYPE: - return pa.array(value, FEAST_TYPE_TO_ARROW_TYPE[feast_type]) - - return pa.array(value) - - -def _proto_to_arrow(value: Value_pb2.RepeatedValue) -> pa.Array: - """ - ToDo: support entity rows already packed in protos - """ - raise NotImplementedError - - -def transformation_callback( - fs: "FeatureStore", - on_demand_feature_view_name: str, - input_arr_ptr: int, - input_schema_ptr: int, - output_arr_ptr: int, - output_schema_ptr: int, - full_feature_names: bool, -) -> int: - try: - odfv = fs.get_on_demand_feature_view(on_demand_feature_view_name) - - input_record = pa.RecordBatch._import_from_c(input_arr_ptr, input_schema_ptr) - - # For some reason, the callback is called with `full_feature_names` as a 1 if True or 0 if false. This handles - # the typeguard requirement. - full_feature_names = bool(full_feature_names) - - if odfv.mode != "pandas": - raise Exception( - f"OnDemandFeatureView mode '{odfv.mode} not supported by EmbeddedOnlineFeatureServer." - ) - - output = odfv.get_transformed_features_df( # type: ignore - input_record.to_pandas(), full_feature_names=full_feature_names - ) - output_record = pa.RecordBatch.from_pandas(output) - - output_record.schema._export_to_c(output_schema_ptr) - output_record._export_to_c(output_arr_ptr) - - return output_record.num_rows - except Exception as e: - logger.exception(f"transformation callback failed with exception: {e}", e) - return 0 - - -def logging_callback( - fs: "FeatureStore", - feature_service_name: str, - dataset_dir: str, -) -> bytes: - feature_service = fs.get_feature_service(feature_service_name, allow_cache=True) - try: - fs.write_logged_features(logs=Path(dataset_dir), source=feature_service) - except Exception as exc: - return repr(exc).encode() - - return "".encode() # no error - - -def allocate_schema_and_array(): - c_schema = ffi.new("struct ArrowSchema*") - ptr_schema = int(ffi.cast("uintptr_t", c_schema)) - - c_array = ffi.new("struct ArrowArray*") - ptr_array = int(ffi.cast("uintptr_t", c_array)) - return c_schema, ptr_schema, c_array, ptr_array - - -def map_to_record_batch( - map: Dict[str, Union[List[Any], Value_pb2.RepeatedValue]], - type_hint: Optional[Dict[str, ValueType]] = None, -) -> Tuple[pa.RecordBatch, pa.Schema]: - fields = [] - columns = [] - type_hint = type_hint or {} - - for name, values in map.items(): - arr = _to_arrow(values, type_hint.get(name)) - fields.append((name, arr.type)) - columns.append(arr) - - schema = pa.schema(fields) - batch = pa.RecordBatch.from_arrays(columns, schema=schema) - return batch, schema - - -def record_batch_to_online_response(record_batch): - resp = GetOnlineFeaturesResponse() - - for idx, field in enumerate(record_batch.schema): - if field.name.endswith("__timestamp") or field.name.endswith("__status"): - continue - - feature_vector = GetOnlineFeaturesResponse.FeatureVector( - statuses=record_batch.columns[idx + 1].to_pylist(), - event_timestamps=[ - Timestamp(seconds=seconds) - for seconds in record_batch.columns[idx + 2].to_pylist() - ], - ) - - if field.type == pa.null(): - feature_vector.values.extend( - [Value_pb2.Value()] * len(record_batch.columns[idx]) - ) - else: - feature_vector.values.extend( - arrow_array_to_array_of_proto(field.type, record_batch.columns[idx]) - ) - - resp.results.append(feature_vector) - resp.metadata.feature_names.val.append(field.name) - - return resp +import logging +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union + +import pyarrow as pa +from google.protobuf.timestamp_pb2 import Timestamp +from pyarrow.cffi import ffi + +from feast.errors import ( + FeatureNameCollisionError, + RequestDataNotFoundInEntityRowsException, +) +from feast.feature_service import FeatureService +from feast.infra.feature_servers.base_config import FeatureLoggingConfig +from feast.online_response import OnlineResponse +from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse +from feast.protos.feast.types import Value_pb2 +from feast.repo_config import RepoConfig +from feast.types import from_value_type +from feast.value_type import ValueType + +from .lib.embedded import ( + DataTable, + LoggingOptions, + NewOnlineFeatureService, + OnlineFeatureServiceConfig, +) +from .lib.go import Slice_string +from .type_map import FEAST_TYPE_TO_ARROW_TYPE, arrow_array_to_array_of_proto + +if TYPE_CHECKING: + from feast.feature_store import FeatureStore + +NANO_SECOND = 1 +MICRO_SECOND = 1000 * NANO_SECOND +MILLI_SECOND = 1000 * MICRO_SECOND +SECOND = 1000 * MILLI_SECOND + +logger = logging.getLogger(__name__) + + +class EmbeddedOnlineFeatureServer: + def __init__( + self, repo_path: str, repo_config: RepoConfig, feature_store: "FeatureStore" + ): + # keep callback in self to prevent it from GC + self._transformation_callback = partial(transformation_callback, feature_store) + self._logging_callback = partial(logging_callback, feature_store) + + self._config = OnlineFeatureServiceConfig( + RepoPath=repo_path, RepoConfig=repo_config.json() + ) + + self._service = NewOnlineFeatureService( + self._config, + self._transformation_callback, + ) + + # This should raise an exception if there were any errors in NewOnlineFeatureService. + self._service.CheckForInstantiationError() + + def get_online_features( + self, + features_refs: List[str], + feature_service: Optional[FeatureService], + entities: Dict[str, Union[List[Any], Value_pb2.RepeatedValue]], + request_data: Dict[str, Union[List[Any], Value_pb2.RepeatedValue]], + full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, + ): + if feature_service: + join_keys_types = self._service.GetEntityTypesMapByFeatureService( + feature_service.name + ) + else: + join_keys_types = self._service.GetEntityTypesMap( + Slice_string(features_refs) + ) + + join_keys_types = { + join_key: ValueType(enum_value) for join_key, enum_value in join_keys_types + } + + # Here we create C structures that will be shared between Python and Go. + # We will pass entities as arrow Record Batch to Go part (in_c_array & in_c_schema) + # and receive features as Record Batch from Go (out_c_array & out_c_schema) + # This objects needs to be initialized here in order to correctly + # free them later using Python GC. + ( + entities_c_schema, + entities_ptr_schema, + entities_c_array, + entities_ptr_array, + ) = allocate_schema_and_array() + ( + req_data_c_schema, + req_data_ptr_schema, + req_data_c_array, + req_data_ptr_array, + ) = allocate_schema_and_array() + + ( + features_c_schema, + features_ptr_schema, + features_c_array, + features_ptr_array, + ) = allocate_schema_and_array() + + batch, schema = map_to_record_batch(entities, join_keys_types) + schema._export_to_c(entities_ptr_schema) + batch._export_to_c(entities_ptr_array) + + batch, schema = map_to_record_batch(request_data) + schema._export_to_c(req_data_ptr_schema) + batch._export_to_c(req_data_ptr_array) + + try: + self._service.GetOnlineFeatures( + featureRefs=Slice_string(features_refs), + featureServiceName=feature_service and feature_service.name or "", + entities=DataTable( + SchemaPtr=entities_ptr_schema, DataPtr=entities_ptr_array + ), + requestData=DataTable( + SchemaPtr=req_data_ptr_schema, DataPtr=req_data_ptr_array + ), + fullFeatureNames=full_feature_names, + output=DataTable( + SchemaPtr=features_ptr_schema, DataPtr=features_ptr_array + ), + ) + except RuntimeError as exc: + (msg,) = exc.args + if msg.startswith("featureNameCollisionError"): + feature_refs = msg[len("featureNameCollisionError: ") : msg.find(";")] + feature_refs = feature_refs.split(",") + raise FeatureNameCollisionError( + feature_refs_collisions=feature_refs, + full_feature_names=full_feature_names, + ) + + if msg.startswith("requestDataNotFoundInEntityRowsException"): + feature_refs = msg[len("requestDataNotFoundInEntityRowsException: ") :] + feature_refs = feature_refs.split(",") + raise RequestDataNotFoundInEntityRowsException(feature_refs) + + raise + + record_batch = pa.RecordBatch._import_from_c( + features_ptr_array, features_ptr_schema + ) + resp = record_batch_to_online_response(record_batch) + del record_batch + return OnlineResponse(resp) + + def start_grpc_server( + self, + host: str, + port: int, + enable_logging: bool = True, + logging_options: Optional[FeatureLoggingConfig] = None, + ): + if enable_logging: + if logging_options: + self._service.StartGprcServerWithLogging( + host, + port, + self._logging_callback, + LoggingOptions( + FlushInterval=logging_options.flush_interval_secs * SECOND, + WriteInterval=logging_options.write_to_disk_interval_secs + * SECOND, + EmitTimeout=logging_options.emit_timeout_micro_secs + * MICRO_SECOND, + ChannelCapacity=logging_options.queue_capacity, + ), + ) + else: + self._service.StartGprcServerWithLoggingDefaultOpts( + host, port, self._logging_callback + ) + else: + self._service.StartGprcServer(host, port) + + def start_http_server( + self, + host: str, + port: int, + enable_logging: bool = True, + logging_options: Optional[FeatureLoggingConfig] = None, + ): + if enable_logging: + if logging_options: + self._service.StartHttpServerWithLogging( + host, + port, + self._logging_callback, + LoggingOptions( + FlushInterval=logging_options.flush_interval_secs * SECOND, + WriteInterval=logging_options.write_to_disk_interval_secs + * SECOND, + EmitTimeout=logging_options.emit_timeout_micro_secs + * MICRO_SECOND, + ChannelCapacity=logging_options.queue_capacity, + ), + ) + else: + self._service.StartHttpServerWithLoggingDefaultOpts( + host, port, self._logging_callback + ) + else: + self._service.StartHttpServer(host, port) + + def stop_grpc_server(self): + self._service.StopGrpcServer() + + def stop_http_server(self): + self._service.StopHttpServer() + + +def _to_arrow(value, type_hint: Optional[ValueType]) -> pa.Array: + if isinstance(value, Value_pb2.RepeatedValue): + _proto_to_arrow(value) + + if type_hint: + feast_type = from_value_type(type_hint) + if feast_type in FEAST_TYPE_TO_ARROW_TYPE: + return pa.array(value, FEAST_TYPE_TO_ARROW_TYPE[feast_type]) + + return pa.array(value) + + +def _proto_to_arrow(value: Value_pb2.RepeatedValue) -> pa.Array: + """ + ToDo: support entity rows already packed in protos + """ + raise NotImplementedError + + +def transformation_callback( + fs: "FeatureStore", + on_demand_feature_view_name: str, + input_arr_ptr: int, + input_schema_ptr: int, + output_arr_ptr: int, + output_schema_ptr: int, + full_feature_names: bool, +) -> int: + try: + odfv = fs.get_on_demand_feature_view(on_demand_feature_view_name) + + input_record = pa.RecordBatch._import_from_c(input_arr_ptr, input_schema_ptr) + + # For some reason, the callback is called with `full_feature_names` as a 1 if True or 0 if false. This handles + # the typeguard requirement. + full_feature_names = bool(full_feature_names) + + if odfv.mode != "pandas": + raise Exception( + f"OnDemandFeatureView mode '{odfv.mode} not supported by EmbeddedOnlineFeatureServer." + ) + + output = odfv.get_transformed_features_df( # type: ignore + input_record.to_pandas(), full_feature_names=full_feature_names + ) + output_record = pa.RecordBatch.from_pandas(output) + + output_record.schema._export_to_c(output_schema_ptr) + output_record._export_to_c(output_arr_ptr) + + return output_record.num_rows + except Exception as e: + logger.exception(f"transformation callback failed with exception: {e}", e) + return 0 + + +def logging_callback( + fs: "FeatureStore", + feature_service_name: str, + dataset_dir: str, +) -> bytes: + feature_service = fs.get_feature_service(feature_service_name, allow_cache=True) + try: + fs.write_logged_features(logs=Path(dataset_dir), source=feature_service) + except Exception as exc: + return repr(exc).encode() + + return "".encode() # no error + + +def allocate_schema_and_array(): + c_schema = ffi.new("struct ArrowSchema*") + ptr_schema = int(ffi.cast("uintptr_t", c_schema)) + + c_array = ffi.new("struct ArrowArray*") + ptr_array = int(ffi.cast("uintptr_t", c_array)) + return c_schema, ptr_schema, c_array, ptr_array + + +def map_to_record_batch( + map: Dict[str, Union[List[Any], Value_pb2.RepeatedValue]], + type_hint: Optional[Dict[str, ValueType]] = None, +) -> Tuple[pa.RecordBatch, pa.Schema]: + fields = [] + columns = [] + type_hint = type_hint or {} + + for name, values in map.items(): + arr = _to_arrow(values, type_hint.get(name)) + fields.append((name, arr.type)) + columns.append(arr) + + schema = pa.schema(fields) + batch = pa.RecordBatch.from_arrays(columns, schema=schema) + return batch, schema + + +def record_batch_to_online_response(record_batch): + resp = GetOnlineFeaturesResponse() + + for idx, field in enumerate(record_batch.schema): + if field.name.endswith("__timestamp") or field.name.endswith("__status"): + continue + + feature_vector = GetOnlineFeaturesResponse.FeatureVector( + statuses=record_batch.columns[idx + 1].to_pylist(), + event_timestamps=[ + Timestamp(seconds=seconds) + for seconds in record_batch.columns[idx + 2].to_pylist() + ], + ) + + if field.type == pa.null(): + feature_vector.values.extend( + [Value_pb2.Value()] * len(record_batch.columns[idx]) + ) + else: + feature_vector.values.extend( + arrow_array_to_array_of_proto(field.type, record_batch.columns[idx]) + ) + + resp.results.append(feature_vector) + resp.metadata.feature_names.val.append(field.name) + + return resp diff --git a/sdk/python/feast/embedder.py b/sdk/python/feast/embedder.py new file mode 100644 index 00000000000..a0bccdd75ed --- /dev/null +++ b/sdk/python/feast/embedder.py @@ -0,0 +1,359 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import ( + TYPE_CHECKING, + Any, + Callable, + List, + Optional, + Protocol, + runtime_checkable, +) + +import pandas as pd + +if TYPE_CHECKING: + import numpy as np + + from feast.repo_config import EmbeddingModelConfig + + +@runtime_checkable +class EmbeddingProvider(Protocol): + """Protocol for query-time embedding providers. + + Implement this to plug in any embedding backend (OpenAI, Cohere, + SentenceTransformers, a local model, etc.) for use in + ``openai_search`` and the OpenAI-compatible + feature server endpoints. + + Example:: + + class MyEmbeddingProvider: + def embed(self, texts: List[str]) -> List[List[float]]: + return my_model.encode(texts) + + async def aembed(self, texts: List[str]) -> List[List[float]]: + return await my_model.aencode(texts) + """ + + def embed(self, texts: List[str]) -> List[List[float]]: + """Return one embedding vector per input text.""" + ... + + async def aembed(self, texts: List[str]) -> List[List[float]]: + """Async variant of :meth:`embed`.""" + ... + + +class SentenceTransformersEmbeddingProvider: + """EmbeddingProvider backed by Sentence Transformers (local inference). + + Runs entirely on-device — no external API key required. Ideal for + air-gapped deployments, cost-sensitive workloads, or environments where + outbound API calls are restricted. + + Requires the ``sentence-transformers`` package:: + + pip install sentence-transformers + + Configuration in ``feature_store.yaml``:: + + embedding_model: + provider: sentence_transformers + model: all-MiniLM-L6-v2 # any HuggingFace sentence-transformers model + + The model is loaded lazily on the first call to :meth:`embed` or + :meth:`aembed` so that import cost is paid only when the provider is + actually used. + """ + + def __init__(self, model: str = "all-MiniLM-L6-v2"): + self.model_name = model + self._model = None + + @classmethod + def from_config( + cls, config: "EmbeddingModelConfig" + ) -> "SentenceTransformersEmbeddingProvider": + """Build a provider from a :class:`~feast.repo_config.EmbeddingModelConfig`.""" + return cls(model=config.model) + + def _load_model(self): + if self._model is None: + try: + from sentence_transformers import SentenceTransformer + except ImportError: + raise ImportError( + "sentence-transformers is required for the " + "'sentence_transformers' embedding provider. " + "Install with: pip install sentence-transformers" + ) + self._model = SentenceTransformer(self.model_name) + return self._model + + def embed(self, texts: List[str]) -> List[List[float]]: + model = self._load_model() + embeddings = model.encode(texts, convert_to_numpy=True) + return [emb.tolist() for emb in embeddings] + + async def aembed(self, texts: List[str]) -> List[List[float]]: + import asyncio + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, self.embed, texts) + + +def get_embedding_provider( + config: "EmbeddingModelConfig", +) -> "EmbeddingProvider": + """Factory that returns the appropriate :class:`EmbeddingProvider` for *config*. + + Dispatches on ``config.provider``: + + * ``"sentence_transformers"`` (default) → :class:`SentenceTransformersEmbeddingProvider` + + Raises: + ValueError: If ``config.provider`` is not a recognised value. + """ + provider = (config.provider or "sentence_transformers").lower() + if provider == "sentence_transformers": + return SentenceTransformersEmbeddingProvider.from_config(config) + raise ValueError( + f"Unknown embedding provider: '{config.provider}'. " + "Supported values: 'sentence_transformers'." + ) + + +@dataclass +class EmbeddingConfig: + batch_size: int = 64 + show_progress: bool = True + + +class BaseEmbedder(ABC): + """ + Abstract base class for embedding generation. + + Supports multiple modalities via routing. + Users can register custom modality handlers. + """ + + def __init__(self, config: Optional[EmbeddingConfig] = None): + self.config = config or EmbeddingConfig() + + # Registry: modality -> embedding function + self._modality_handlers: dict[str, Callable[[List[Any]], "np.ndarray"]] = {} + + # Register default modalities (subclass can override) + self._register_default_modalities() + + def _register_default_modalities(self) -> None: + """Override in subclass to register default modality handlers.""" + pass + + def register_modality( + self, + modality: str, + handler: Callable[[List[Any]], "np.ndarray"], + ) -> None: + """ + Register a handler for a modality. + + Args: + modality: Name of modality ("text", "image", "video", etc.) + handler: Function that takes list of inputs and returns embeddings. + """ + self._modality_handlers[modality] = handler + + @property + def supported_modalities(self) -> List[str]: + """Return list of supported modalities.""" + return list(self._modality_handlers.keys()) + + def get_embedding_dim(self, modality: str) -> Optional[int]: + """ + Return the embedding dimension for a given modality. + + Subclasses should override this to return the actual dimension + so that auto-generated FeatureView schemas use the correct vector_length. + + Args: + modality: The modality to query (e.g. "text", "image"). + + Returns: + The embedding dimension, or None if unknown. + """ + return None + + @abstractmethod + def embed(self, inputs: List[Any], modality: str) -> "np.ndarray": + """ + Generate embeddings for inputs of a given modality. + + Args: + inputs: List of inputs. + modality: Type of content ("text", "image", "video", etc.) + + Returns: + numpy array of shape (len(inputs), embedding_dim) + """ + pass + + def embed_dataframe( + self, + df: pd.DataFrame, + column_mapping: dict[str, tuple[str, str]], + ) -> pd.DataFrame: + """ + Add embeddings for multiple columns with modality routing. + + Args: + df: Input DataFrame. + column_mapping: Dict mapping source_column -> (modality, output_column). + Example: { + "text": ("text", "text_embedding"), + "image_path": ("image", "image_embedding"), + "video_path": ("video", "video_embedding"), + } + """ + df = df.copy() + + for source_column, (modality, output_column) in column_mapping.items(): + inputs = df[source_column].tolist() + embeddings = self.embed(inputs, modality) + df[output_column] = pd.Series( + [emb.tolist() for emb in embeddings], dtype=object, index=df.index + ) + + return df + + +class MultiModalEmbedder(BaseEmbedder): + """ + Multi-modal embedder with built-in support for common modalities. + + Supports: text, image, video (extensible) + """ + + def __init__( + self, + text_model: str = "all-MiniLM-L6-v2", + image_model: str = "openai/clip-vit-base-patch32", + config: Optional[EmbeddingConfig] = None, + ): + self.text_model_name = text_model + self.image_model_name = image_model + + # Lazy-loaded models + self._text_model = None + self._image_model = None + self._image_processor = None + + super().__init__(config) + + def _register_default_modalities(self) -> None: + """Register built-in modality handlers.""" + self.register_modality("text", self._embed_text) + self.register_modality("image", self._embed_image) + # Future: add more as needed + # self.register_modality("video", self._embed_video) + # self.register_modality("audio", self._embed_audio) + + def embed(self, inputs: List[Any], modality: str) -> "np.ndarray": + """Route to appropriate handler based on modality.""" + if modality not in self._modality_handlers: + raise ValueError( + f"Unsupported modality: '{modality}'. " + f"Supported: {self.supported_modalities}" + ) + + handler = self._modality_handlers[modality] + return handler(inputs) + + def get_embedding_dim(self, modality: str) -> Optional[int]: + """ + Return the embedding dimension for a given modality. + + For "text", this queries the SentenceTransformer model's dimension + (which triggers lazy model loading). + + Args: + modality: The modality to query (e.g. "text", "image"). + + Returns: + The embedding dimension, or None if unknown. + """ + if modality == "text": + return self.text_model.get_sentence_embedding_dimension() + elif modality == "image": + return self.image_model.config.vision_config.hidden_size + return None + + # Text Embedding + @property + def text_model(self): + if self._text_model is None: + from sentence_transformers import SentenceTransformer + + self._text_model = SentenceTransformer(self.text_model_name) + return self._text_model + + def _embed_text(self, inputs: List[str]) -> "np.ndarray": + return self.text_model.encode( + inputs, + batch_size=self.config.batch_size, + show_progress_bar=self.config.show_progress, + ) + + # Image Embedding + @property + def image_model(self): + if self._image_model is None: + from transformers import CLIPModel + + self._image_model = CLIPModel.from_pretrained(self.image_model_name) + return self._image_model + + @property + def image_processor(self): + if self._image_processor is None: + from transformers import CLIPProcessor + + self._image_processor = CLIPProcessor.from_pretrained(self.image_model_name) + return self._image_processor + + def _embed_image(self, inputs: List[Any]) -> "np.ndarray": + from pathlib import Path + + import numpy as np + from PIL import Image + + all_embeddings: List["np.ndarray"] = [] + batch_size = self.config.batch_size + + for start in range(0, len(inputs), batch_size): + batch = inputs[start : start + batch_size] + images = [] + opened: List[Image.Image] = [] + try: + for inp in batch: + if isinstance( + inp, (str, Path) + ): # If the input string path is too large that It gives error and we could not open the image. + img = Image.open(inp) + opened.append(img) + images.append(img) + else: + images.append(inp) + + processed = self.image_processor(images=images, return_tensors="pt") + finally: + for opened_img in opened: + opened_img.close() + + embeddings = self.image_model.get_image_features(**processed) + embeddings = embeddings / embeddings.norm(p=2, dim=-1, keepdim=True) + all_embeddings.append(embeddings.detach().numpy()) + + return np.concatenate(all_embeddings, axis=0) diff --git a/sdk/python/feast/entity.py b/sdk/python/feast/entity.py index 7f4eadc6352..bbd13c83e48 100644 --- a/sdk/python/feast/entity.py +++ b/sdk/python/feast/entity.py @@ -126,7 +126,7 @@ def __hash__(self) -> int: def __eq__(self, other): if not isinstance(other, Entity): - raise TypeError("Comparisons should only involve Entity class objects.") + return False if ( self.name != other.name @@ -203,7 +203,7 @@ def to_proto(self) -> EntityProto: spec = EntitySpecProto( name=self.name, - value_type=self.value_type.value, + value_type=self.value_type.value, # type: ignore[arg-type] join_key=self.join_key, description=self.description, tags=self.tags, diff --git a/sdk/python/feast/errors.py b/sdk/python/feast/errors.py index 8c72422f44e..515a6c39b11 100644 --- a/sdk/python/feast/errors.py +++ b/sdk/python/feast/errors.py @@ -128,6 +128,38 @@ def __init__(self, name, project=None): super().__init__(f"Feature view {name} does not exist") +class FeatureViewVersionNotFound(FeastObjectNotFoundException): + def __init__(self, name, version, project=None): + if project: + super().__init__( + f"Version {version} of feature view {name} does not exist in project {project}" + ) + else: + super().__init__(f"Version {version} of feature view {name} does not exist") + + +class VersionedOnlineReadNotSupported(FeastError): + def __init__(self, store_name: str, version: int): + super().__init__( + f"Versioned feature reads (@v{version}) are not yet supported by {store_name}. " + f"Currently only SQLite, PostgreSQL, MySQL, FAISS, Redis, and DynamoDB support version-qualified feature references. " + ) + + +class FeatureViewPinConflict(FeastError): + def __init__(self, name, version): + super().__init__( + f"Cannot pin feature view '{name}' to {version} because the definition has also been modified. " + f"To pin to an older version, only change the 'version' parameter — do not modify other fields. " + f"To apply a new definition, use version='latest' or omit the version parameter." + ) + + +class ConcurrentVersionConflict(FeastError): + def __init__(self, msg: str): + super().__init__(msg) + + class OnDemandFeatureViewNotFoundException(FeastObjectNotFoundException): def __init__(self, name, project=None): if project: @@ -411,10 +443,32 @@ def __init__(self, entity_type: type): class ConflictingFeatureViewNames(FeastError): # TODO: print file location of conflicting feature views - def __init__(self, feature_view_name: str): - super().__init__( - f"The feature view name: {feature_view_name} refers to feature views of different types." - ) + def __init__( + self, + feature_view_name: str, + existing_type: Optional[str] = None, + new_type: Optional[str] = None, + ): + if existing_type and new_type: + if existing_type == new_type: + # Same-type duplicate + super().__init__( + f"Multiple {existing_type}s with name '{feature_view_name}' found. " + f"Feature view names must be case-insensitively unique. " + f"It may be necessary to ignore certain files in your feature " + f"repository by using a .feastignore file." + ) + else: + # Cross-type conflict + super().__init__( + f"Feature view name '{feature_view_name}' is already used by a {existing_type}. " + f"Cannot register a {new_type} with the same name. " + f"Feature view names must be unique across FeatureView, StreamFeatureView, and OnDemandFeatureView." + ) + else: + super().__init__( + f"The feature view name: {feature_view_name} refers to feature views of different types." + ) class FeastInvalidInfraObjectType(FeastError): diff --git a/sdk/python/feast/feast_object.py b/sdk/python/feast/feast_object.py index 63fa1e913b4..c092b25d7be 100644 --- a/sdk/python/feast/feast_object.py +++ b/sdk/python/feast/feast_object.py @@ -8,12 +8,14 @@ from .entity import Entity from .feature_service import FeatureService from .feature_view import FeatureView +from .labeling.label_view import LabelView from .on_demand_feature_view import OnDemandFeatureView from .permissions.permission import Permission from .protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from .protos.feast.core.Entity_pb2 import EntitySpecV2 from .protos.feast.core.FeatureService_pb2 import FeatureServiceSpec from .protos.feast.core.FeatureView_pb2 import FeatureViewSpec +from .protos.feast.core.LabelView_pb2 import LabelViewSpec from .protos.feast.core.OnDemandFeatureView_pb2 import OnDemandFeatureViewSpec from .protos.feast.core.Permission_pb2 import PermissionSpec as PermissionSpec from .protos.feast.core.SavedDataset_pb2 import SavedDatasetSpec @@ -31,6 +33,7 @@ OnDemandFeatureView, BatchFeatureView, StreamFeatureView, + LabelView, Entity, FeatureService, DataSource, @@ -44,6 +47,7 @@ FeatureViewSpec, OnDemandFeatureViewSpec, StreamFeatureViewSpec, + LabelViewSpec, EntitySpecV2, FeatureServiceSpec, DataSourceProto, @@ -58,4 +62,5 @@ OnDemandFeatureView, BatchFeatureView, StreamFeatureView, + LabelView, ] diff --git a/sdk/python/feast/feature.py b/sdk/python/feast/feature.py index db629d677a8..df8f860acd8 100644 --- a/sdk/python/feast/feature.py +++ b/sdk/python/feast/feature.py @@ -50,6 +50,8 @@ def __init__( self._labels = labels def __eq__(self, other): + if not isinstance(other, Feature): + return False if self.name != other.name or self.dtype != other.dtype: return False return True diff --git a/sdk/python/feast/feature_logging.py b/sdk/python/feast/feature_logging.py index 9bd5d8a91c0..d1086215e99 100644 --- a/sdk/python/feast/feature_logging.py +++ b/sdk/python/feast/feature_logging.py @@ -8,10 +8,9 @@ from feast.embedded_go.type_map import FEAST_TYPE_TO_ARROW_TYPE, PA_TIMESTAMP_TYPE from feast.errors import ( FeastObjectNotFoundException, - FeatureViewNotFoundException, - OnDemandFeatureViewNotFoundException, ) from feast.feature_view import DUMMY_ENTITY_ID +from feast.feature_view_utils import get_feature_view_from_registry from feast.protos.feast.core.FeatureService_pb2 import ( LoggingConfig as LoggingConfigProto, ) @@ -58,25 +57,22 @@ def get_schema(self, registry: "BaseRegistry") -> pa.Schema: # Go code can be found here: # https://github.com/feast-dev/feast/blob/master/go/internal/feast/server/logging/memorybuffer.go#L51 try: - feature_view = registry.get_feature_view(projection.name, self._project) - except FeatureViewNotFoundException: - try: - on_demand_feature_view = registry.get_on_demand_feature_view( - projection.name, self._project - ) - except OnDemandFeatureViewNotFoundException: - raise FeastObjectNotFoundException( - f"Can't recognize feature view with a name {projection.name}" - ) + feast_object = get_feature_view_from_registry( + registry, + projection.name, + self._project, + ) + except FeastObjectNotFoundException: + raise FeastObjectNotFoundException( + f"Can't recognize feature view with a name {projection.name}" + ) - for ( - request_source - ) in on_demand_feature_view.source_request_sources.values(): + if hasattr(feast_object, "source_request_sources"): + for request_source in feast_object.source_request_sources.values(): for field in request_source.schema: fields[field.name] = FEAST_TYPE_TO_ARROW_TYPE[field.dtype] - else: - for entity_column in feature_view.entity_columns: + for entity_column in feast_object.entity_columns: if entity_column.name == DUMMY_ENTITY_ID: continue diff --git a/sdk/python/feast/feature_server.py b/sdk/python/feast/feature_server.py index fee7e56e9c1..e42e28d6db9 100644 --- a/sdk/python/feast/feature_server.py +++ b/sdk/python/feast/feature_server.py @@ -1,20 +1,38 @@ +# Copyright 2025 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import asyncio +import functools import os import sys import threading import time import traceback +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor from contextlib import asynccontextmanager from datetime import datetime from importlib import resources as importlib_resources -from typing import Any, Dict, List, Optional, Union +from types import SimpleNamespace +from typing import Any, DefaultDict, Dict, List, NamedTuple, Optional, Set, Union import pandas as pd -import psutil from dateutil import parser from fastapi import ( Depends, FastAPI, + Query, Request, Response, WebSocket, @@ -25,11 +43,10 @@ from fastapi.logger import logger from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles -from google.protobuf.json_format import MessageToDict -from prometheus_client import Gauge, start_http_server -from pydantic import BaseModel +from pydantic import BaseModel, Field, field_validator import feast +from feast import metrics as feast_metrics from feast import proto_json, utils from feast.constants import DEFAULT_FEATURE_SERVER_REGISTRY_TTL from feast.data_source import PushMode @@ -38,8 +55,16 @@ FeatureViewNotFoundException, ) from feast.feast_object import FeastObject +from feast.feature_server_utils import convert_response_to_dict +from feast.feature_view import FeatureViewState +from feast.feature_view_utils import get_feature_view_from_feature_store +from feast.filter_models import ComparisonFilter, CompoundFilter from feast.permissions.action import WRITE, AuthzedAction -from feast.permissions.security_manager import assert_permissions +from feast.permissions.security_manager import ( + assert_permissions, + get_security_manager, + is_auth_necessary, +) from feast.permissions.server.rest import inject_user_details from feast.permissions.server.utils import ( ServerType, @@ -47,14 +72,7 @@ init_security_manager, str_to_auth_manager_type, ) - -# Define prometheus metrics -cpu_usage_gauge = Gauge( - "feast_feature_server_cpu_usage", "CPU usage of the Feast feature server" -) -memory_usage_gauge = Gauge( - "feast_feature_server_memory_usage", "Memory usage of the Feast feature server" -) +from feast.vector_store_utils import VectorStoreRegistry, build_vector_store_object # TODO: deprecate this in favor of push features @@ -78,11 +96,27 @@ class MaterializeRequest(BaseModel): end_ts: Optional[str] = None feature_views: Optional[List[str]] = None disable_event_timestamp: bool = False + full_feature_names: bool = False + version: Optional[str] = Field( + None, + description=( + "Optional version to materialize (e.g. 'v2'). Requires feature_views " + "with exactly one entry and registry.enable_online_feature_view_versioning." + ), + ) class MaterializeIncrementalRequest(BaseModel): end_ts: str feature_views: Optional[List[str]] = None + full_feature_names: bool = False + version: Optional[str] = Field( + None, + description=( + "Optional version to materialize (e.g. 'v2'). Requires feature_views " + "with exactly one entry and registry.enable_online_feature_view_versioning." + ), + ) class GetOnlineFeaturesRequest(BaseModel): @@ -90,16 +124,73 @@ class GetOnlineFeaturesRequest(BaseModel): feature_service: Optional[str] = None features: List[str] = [] full_feature_names: bool = False + include_feature_view_version_metadata: bool = False class GetOnlineDocumentsRequest(BaseModel): feature_service: Optional[str] = None features: List[str] = [] full_feature_names: bool = False + include_feature_view_version_metadata: bool = False top_k: Optional[int] = None query: Optional[List[float]] = None query_string: Optional[str] = None + distance_metric: Optional[str] = None api_version: Optional[int] = 1 + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None + + +class OpenAISearchMetadata(BaseModel): + features_to_retrieve: Optional[List[str]] = None + content_field: Optional[str] = None + + +class OpenAIRankingOptions(BaseModel): + ranker: Optional[str] = None + score_threshold: Optional[float] = None + + +class OpenAISearchRequest(BaseModel): + query: Union[str, List[str]] + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None + max_num_results: Optional[int] = 10 + ranking_options: Optional[OpenAIRankingOptions] = None + rewrite_query: Optional[bool] = None + metadata: Optional[OpenAISearchMetadata] = None + + +class OpenAIVectorStoreObject(BaseModel): + id: str + object: str = "vector_store" + name: str + status: str = "completed" + created_at: int = 0 + + +class FeatureVectorResponse(BaseModel): + values: List[Any] = [] + statuses: List[str] = [] + event_timestamps: List[str] = [] + + +class OnlineFeaturesMetadataResponse(BaseModel): + feature_names: List[str] = [] + + @field_validator("feature_names", mode="before") + @classmethod + def _unwrap_feature_list(cls, v: Any) -> Any: + """Accept both the proto_json-patched flat list and the raw + protobuf ``{"val": [...]}`` dict produced by ``MessageToDict`` + when the monkey-patch is absent or ineffective.""" + if isinstance(v, dict) and "val" in v: + return v["val"] + return v + + +class OnlineFeaturesResponse(BaseModel): + metadata: Optional[OnlineFeaturesMetadataResponse] = None + results: List[FeatureVectorResponse] = [] + status: Optional[bool] = None class ChatMessage(BaseModel): @@ -111,13 +202,71 @@ class ChatRequest(BaseModel): messages: List[ChatMessage] -class ReadDocumentRequest(BaseModel): - file_path: str +def _parse_feature_info( + features: Union[List[str], "feast.FeatureService"], +) -> tuple: + """Return ``(feature_view_names, feature_count)`` from resolved features. + + ``features`` is either a list of ``"feature_view:feature"`` strings or + a ``FeatureService`` with ``feature_view_projections``. + + Returns: + (fv_names, feat_count) where fv_names is a list of unique feature + view name strings and feat_count is the total number of features. + """ + from feast.feature_service import FeatureService + from feast.utils import _parse_feature_ref + + if isinstance(features, FeatureService): + projections = features.feature_view_projections + fv_names = [p.name for p in projections] + feat_count = sum(len(p.features) for p in projections) + elif isinstance(features, list): + feat_count = len(features) + fv_names = list({_parse_feature_ref(ref)[0] for ref in features if ":" in ref}) + else: + fv_names = [] + feat_count = 0 + return fv_names, feat_count + + +def _resolve_feature_counts( + features: Union[List[str], "feast.FeatureService"], +) -> tuple: + """Return ``(feature_count_str, feature_view_count_str)`` for Prometheus labels.""" + fv_names, feat_count = _parse_feature_info(features) + return str(feat_count), str(len(fv_names)) -class SaveDocumentRequest(BaseModel): - file_path: str - data: dict +def _emit_online_audit( + request: GetOnlineFeaturesRequest, + features: Union[List[str], "feast.FeatureService"], + entity_count: int, + status: str, + latency_ms: float, +): + """Best-effort audit log emission for online feature requests.""" + try: + from feast.permissions.security_manager import get_security_manager + + requestor_id = "anonymous" + sm = get_security_manager() + if sm and sm.current_user: + requestor_id = sm.current_user.username or "anonymous" + + fv_names, feat_count = _parse_feature_info(features) + + feast_metrics.emit_online_audit_log( + requestor_id=requestor_id, + entity_keys=list(request.entities.keys()), + entity_count=entity_count, + feature_views=fv_names, + feature_count=feat_count, + status=status, + latency_ms=latency_ms, + ) + except Exception: + logger.warning("Failed to emit online audit log", exc_info=True) async def _get_features( @@ -132,7 +281,7 @@ async def _get_features( resource=feature_service, actions=[AuthzedAction.READ_ONLINE] ) features = feature_service # type: ignore - else: + elif is_auth_necessary(get_security_manager()): all_feature_views, all_on_demand_feature_views = await run_in_threadpool( utils._get_feature_views_to_use, store.registry, @@ -150,9 +299,179 @@ async def _get_features( resource=od_feature_view, actions=[AuthzedAction.READ_ONLINE] ) features = request.features # type: ignore + else: + features = request.features # type: ignore return features +async def load_static_artifacts(app: FastAPI, store): + """ + Load static artifacts (models, lookup tables, etc.) into app.state. + + This function can be extended to load various types of static artifacts: + - Small ML models (scikit-learn, small neural networks) + - Lookup tables and reference data + - Configuration parameters + - Pre-computed embeddings + + Note: Not recommended for large language models - use dedicated + model serving solutions (vLLM, TGI, etc.) for those. + """ + try: + # Import here to avoid loading heavy dependencies unless needed + import importlib.util + import inspect + from pathlib import Path + + # Look for static artifacts loading in the feature repository + # This allows templates and users to define their own artifact loading + repo_path = Path(store.repo_path) if store.repo_path else Path.cwd() + artifacts_file = repo_path / "static_artifacts.py" + + if artifacts_file.exists(): + # Load and execute custom static artifacts loading + spec = importlib.util.spec_from_file_location( + "static_artifacts", artifacts_file + ) + if spec and spec.loader: + artifacts_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(artifacts_module) + + # Look for load_artifacts function + if hasattr(artifacts_module, "load_artifacts"): + load_func = artifacts_module.load_artifacts + if inspect.iscoroutinefunction(load_func): + await load_func(app) + else: + load_func(app) + logger.info("Loaded static artifacts from static_artifacts.py") + except Exception as e: + # Non-fatal error - feature server should still start + logger.warning(f"Failed to load static artifacts: {e}") + + +def _authorize_materialize_views( + store: "feast.FeatureStore", + feature_view_names: Optional[List[str]], + version: Optional[str] = None, +) -> List[str]: + """Resolve + authorize feature views for materialization. + + Returns the resolved list of FV names (all eligible FVs when + feature_view_names is None). + """ + parsed_version = store._validate_materialize_version(version, feature_view_names) + feature_views_to_materialize = store._get_feature_views_to_materialize( + feature_view_names, version=parsed_version + ) + for fv in feature_views_to_materialize: + assert_permissions( + resource=fv, + actions=[AuthzedAction.WRITE_ONLINE], + ) + return [fv.name for fv in feature_views_to_materialize] + + +def _check_already_materializing( + store: "feast.FeatureStore", + fv_names: List[str], +) -> Optional[JSONResponse]: + """Return a 409 JSONResponse if any requested FV is already MATERIALIZING.""" + conflicting: List[str] = [] + for fv_name in fv_names: + try: + fv = store.registry.get_feature_view( + fv_name, store.project, allow_cache=False + ) + if getattr(fv, "state", None) == FeatureViewState.MATERIALIZING: + conflicting.append(fv_name) + except (FeatureViewNotFoundException, KeyError): + pass + except Exception as e: + logger.warning( + f"Unexpected error checking MATERIALIZING state for {fv_name}: {e}" + ) + if conflicting: + return JSONResponse( + status_code=409, + content={ + "error": ( + f"Cannot start async materialization — the following feature " + f"views are already in MATERIALIZING state: {conflicting}. " + f"Use ?force=true to override." + ), + "feature_views": conflicting, + }, + ) + return None + + +def _update_fv_state( + store: "feast.FeatureStore", + fv_names: List[str], + state: FeatureViewState, +) -> None: + """Set FV state in the registry for each named feature view.""" + for fv_name in fv_names: + try: + fv = store.registry.get_feature_view( + fv_name, store.project, allow_cache=False + ) + fv.state = state + store.registry.apply_feature_view(fv, store.project) + except (FeatureViewNotFoundException, KeyError): + logger.warning(f"Feature view {fv_name} not found; skip state={state}") + except Exception as e: + logger.warning(f"Failed to set state={state} for {fv_name}: {e}") + + +def _reset_stuck_materializing_to_generated( + store: "feast.FeatureStore", + fv_names: List[str], +) -> None: + """Reset FVs currently in MATERIALIZING to GENERATED (force override).""" + stuck: List[str] = [] + for fv_name in fv_names: + try: + fv = store.registry.get_feature_view( + fv_name, store.project, allow_cache=False + ) + if getattr(fv, "state", None) == FeatureViewState.MATERIALIZING: + stuck.append(fv_name) + except (FeatureViewNotFoundException, KeyError): + pass + except Exception as e: + logger.warning(f"Unexpected error while force-resetting {fv_name}: {e}") + if stuck: + _update_fv_state(store, stuck, FeatureViewState.GENERATED) + logger.info( + "Force reset MATERIALIZING → GENERATED for feature views: %s", stuck + ) + + +def _parse_materialize_timestamps( + request: "MaterializeRequest", +) -> tuple: + """Parse and validate start/end timestamps from a MaterializeRequest.""" + if request.disable_event_timestamp: + now = datetime.now() + return datetime(1970, 1, 1), now + + if not request.start_ts or not request.end_ts: + raise ValueError( + "start_ts and end_ts are required when disable_event_timestamp is False" + ) + try: + start_date = utils.make_tzaware(parser.parse(request.start_ts)) + end_date = utils.make_tzaware(parser.parse(request.end_ts)) + except (ValueError, TypeError) as e: + raise ValueError(f"Invalid timestamp format: {e}") from e + + if start_date >= end_date: + raise ValueError(f"start_ts ({start_date}) must be before end_ts ({end_date})") + return start_date, end_date + + def get_app( store: "feast.FeatureStore", registry_ttl_sec: int = DEFAULT_FEATURE_SERVER_REGISTRY_TTL, @@ -177,7 +496,11 @@ def get_app( The app provides the following endpoints: - `/get-online-features`: Get online features - - `/retrieve-online-documents`: Retrieve online documents + - `/search`: Vector similarity search (RAG) + - `/retrieve-online-documents`: Deprecated alias for `/search` + - `/v1/vector_stores`: List vector stores (GET) + - `/v1/vector_stores/{vector_store_id}`: Get a vector store (GET) + - `/v1/vector_stores/{vector_store_id}/search`: OpenAI-compatible vector search - `/push`: Push features to the feature store - `/write-to-online-store`: Write to the online store - `/health`: Health check @@ -190,9 +513,59 @@ def get_app( """ proto_json.patch() # Asynchronously refresh registry, notifying shutdown and canceling the active timer if the app is shutting down - registry_proto = None shutting_down = False active_timer: Optional[threading.Timer] = None + # --- Offline write batching config and batcher --- + fs_cfg = getattr(store.config, "feature_server", None) + batching_cfg = None + if fs_cfg is not None: + enabled = getattr(fs_cfg, "offline_push_batching_enabled", False) + batch_size = getattr(fs_cfg, "offline_push_batching_batch_size", None) + batch_interval_seconds = getattr( + fs_cfg, "offline_push_batching_batch_interval_seconds", None + ) + + if enabled is True: + size_ok = isinstance(batch_size, int) and not isinstance(batch_size, bool) + interval_ok = isinstance(batch_interval_seconds, int) and not isinstance( + batch_interval_seconds, bool + ) + if size_ok and interval_ok: + batching_cfg = SimpleNamespace( + enabled=True, + batch_size=batch_size, + batch_interval_seconds=batch_interval_seconds, + ) + else: + logger.warning( + "Offline write batching enabled but missing or invalid numeric values; " + "disabling batching (batch_size=%r, batch_interval_seconds=%r)", + batch_size, + batch_interval_seconds, + ) + + offline_batcher: Optional[OfflineWriteBatcher] = None + if batching_cfg is not None and batching_cfg.enabled is True: + offline_batcher = OfflineWriteBatcher(store=store, cfg=batching_cfg) + logger.debug("Offline write batching is ENABLED") + else: + logger.debug("Offline write batching is DISABLED") + + # Dedicated pool for async materialize so long Spark/offline waits do not + # starve the default executor used by online serving and run_in_threadpool. + _mat_workers_raw = os.environ.get("FEAST_MATERIALIZE_MAX_WORKERS", "2") + try: + materialize_max_workers = max(1, int(_mat_workers_raw)) + except ValueError: + logger.warning( + "Invalid FEAST_MATERIALIZE_MAX_WORKERS=%r; using default 2", + _mat_workers_raw, + ) + materialize_max_workers = 2 + materialize_executor = ThreadPoolExecutor( + max_workers=materialize_max_workers, + thread_name_prefix="feast-materialize", + ) def stop_refresh(): nonlocal shutting_down @@ -200,13 +573,14 @@ def stop_refresh(): if active_timer: active_timer.cancel() + vs_registry = VectorStoreRegistry(store) + def async_refresh(): if shutting_down: return store.refresh_registry() - nonlocal registry_proto - registry_proto = store.registry.proto() + vs_registry.refresh() if registry_ttl_sec: nonlocal active_timer @@ -215,147 +589,333 @@ def async_refresh(): @asynccontextmanager async def lifespan(app: FastAPI): + # Load static artifacts before initializing store + await load_static_artifacts(app, store) + await store.initialize() async_refresh() - yield - stop_refresh() - await store.close() + try: + yield + finally: + stop_refresh() + if offline_batcher is not None: + offline_batcher.shutdown() + # wait=False: do not block process exit on in-flight materialize + # (same fire-and-forget contract as returning 202 mid-job). + materialize_executor.shutdown(wait=False) + await store.close() app = FastAPI(lifespan=lifespan) @app.post( "/get-online-features", dependencies=[Depends(inject_user_details)], + response_model=OnlineFeaturesResponse, ) - async def get_online_features(request: GetOnlineFeaturesRequest) -> Dict[str, Any]: - # Initialize parameters for FeatureStore.get_online_features(...) call - features = await _get_features(request, store) - - read_params = dict( - features=features, - entity_rows=request.entities, - full_feature_names=request.full_feature_names, - ) + async def get_online_features(request: GetOnlineFeaturesRequest) -> Any: + with feast_metrics.track_request_latency( + "/get-online-features", + ) as metrics_ctx: + features = await _get_features(request, store) + feat_count, fv_count = _resolve_feature_counts(features) + metrics_ctx.feature_count = feat_count + metrics_ctx.feature_view_count = fv_count + + entity_count = len(next(iter(request.entities.values()), [])) + feast_metrics.track_online_features_entities(entity_count) + + read_params = dict( + features=features, + entity_rows=request.entities, + full_feature_names=request.full_feature_names, + include_feature_view_version_metadata=request.include_feature_view_version_metadata, + ) - if store._get_provider().async_supported.online.read: - response = await store.get_online_features_async(**read_params) # type: ignore - else: - response = await run_in_threadpool( - lambda: store.get_online_features(**read_params) # type: ignore + audit_start_ms = time.monotonic() * 1000 + audit_status = "success" + try: + if store._get_provider().async_supported.online.read: + response = await store.get_online_features_async(**read_params) # type: ignore + else: + response = await run_in_threadpool( + lambda: store.get_online_features(**read_params) # type: ignore + ) + except Exception: + audit_status = "error" + raise + finally: + audit_latency_ms = time.monotonic() * 1000 - audit_start_ms + _emit_online_audit( + request, features, entity_count, audit_status, audit_latency_ms + ) + + response_dict = await run_in_threadpool( + convert_response_to_dict, response.proto ) + return JSONResponse(content=response_dict) - # Convert the Protobuf object to JSON and return it - response_dict = await run_in_threadpool( - MessageToDict, - response.proto, - preserving_proto_field_name=True, - float_precision=18, - ) - return response_dict + async def _search_online_documents( + request: GetOnlineDocumentsRequest, + *, + metrics_path: str, + ) -> JSONResponse: + with feast_metrics.track_request_latency(metrics_path): + features = await _get_features(request, store) + + read_params = dict( + features=features, + query=request.query, + top_k=request.top_k, + ) + if request.api_version == 2 and request.query_string is not None: + read_params["query_string"] = request.query_string + if request.api_version == 2 and request.distance_metric is not None: + read_params["distance_metric"] = request.distance_metric + if request.api_version == 2 and request.filters is not None: + read_params["filters"] = request.filters + + if request.api_version == 2: + read_params["include_feature_view_version_metadata"] = ( + request.include_feature_view_version_metadata + ) + response = await run_in_threadpool( + lambda: store.retrieve_online_documents_v2(**read_params) # type: ignore + ) + else: + response = await run_in_threadpool( + lambda: store.retrieve_online_documents(**read_params) # type: ignore + ) + + response_dict = await run_in_threadpool( + convert_response_to_dict, response.proto + ) + return JSONResponse(content=response_dict) + + @app.post( + "/search", + dependencies=[Depends(inject_user_details)], + response_model=OnlineFeaturesResponse, + ) + async def search(request: GetOnlineDocumentsRequest) -> JSONResponse: + """Vector similarity search against online document embeddings.""" + return await _search_online_documents(request, metrics_path="/search") @app.post( "/retrieve-online-documents", dependencies=[Depends(inject_user_details)], + response_model=OnlineFeaturesResponse, + include_in_schema=False, ) async def retrieve_online_documents( request: GetOnlineDocumentsRequest, - ) -> Dict[str, Any]: + ) -> JSONResponse: logger.warning( - "This endpoint is in alpha and will be moved to /get-online-features when stable." + "POST /retrieve-online-documents is deprecated; use POST /search instead." + ) + return await _search_online_documents( + request, metrics_path="/retrieve-online-documents" ) - # Initialize parameters for FeatureStore.retrieve_online_documents_v2(...) call - features = await _get_features(request, store) - - read_params = dict(features=features, query=request.query, top_k=request.top_k) - if request.api_version == 2 and request.query_string is not None: - read_params["query_string"] = request.query_string - if request.api_version == 2: - response = await run_in_threadpool( - lambda: store.retrieve_online_documents_v2(**read_params) # type: ignore - ) - else: - response = await run_in_threadpool( - lambda: store.retrieve_online_documents(**read_params) # type: ignore + @app.get( + "/v1/vector_stores", + dependencies=[Depends(inject_user_details)], + ) + async def list_vector_stores() -> JSONResponse: + permitted: list = [] + for obj in vs_registry.list_vector_stores(): + fv = vs_registry.resolve(obj["id"]) + try: + assert_permissions(resource=fv, actions=[AuthzedAction.DESCRIBE]) + permitted.append(obj) + except Exception: + pass + return JSONResponse(content={"object": "list", "data": permitted}) + + @app.get( + "/v1/vector_stores/{vector_store_id}", + dependencies=[Depends(inject_user_details)], + ) + async def get_vector_store(vector_store_id: str) -> JSONResponse: + try: + fv = vs_registry.resolve(vector_store_id) + assert_permissions(resource=fv, actions=[AuthzedAction.DESCRIBE]) + except FeatureViewNotFoundException: + return JSONResponse( + status_code=404, + content={ + "error": { + "message": f"No vector store found with id '{vector_store_id}'", + "type": "not_found_error", + } + }, ) + return JSONResponse(content=build_vector_store_object(store.project, fv)) - # Convert the Protobuf object to JSON and return it - response_dict = await run_in_threadpool( - MessageToDict, - response.proto, - preserving_proto_field_name=True, - float_precision=18, - ) - return response_dict + @app.post( + "/v1/vector_stores/{vector_store_id}/search", + dependencies=[Depends(inject_user_details)], + ) + async def vector_store_search( + vector_store_id: str, + request: OpenAISearchRequest, + ) -> JSONResponse: + with feast_metrics.track_request_latency( + "/v1/vector_stores/{vector_store_id}/search" + ): + try: + feature_view = vs_registry.resolve(vector_store_id) + assert_permissions( + resource=feature_view, + actions=[AuthzedAction.READ_ONLINE], + ) - @app.post("/push", dependencies=[Depends(inject_user_details)]) - async def push(request: PushFeaturesRequest) -> None: - df = pd.DataFrame(request.df) - actions = [] - if request.to == "offline": - to = PushMode.OFFLINE - actions = [AuthzedAction.WRITE_OFFLINE] - elif request.to == "online": - to = PushMode.ONLINE - actions = [AuthzedAction.WRITE_ONLINE] - elif request.to == "online_and_offline": - to = PushMode.ONLINE_AND_OFFLINE - actions = WRITE - else: - raise ValueError( - f"{request.to} is not a supported push format. Please specify one of these ['online', 'offline', 'online_and_offline']." - ) + result = await store.openai_search( + vector_store_id=feature_view.name, + query=request.query, + vs_id=vector_store_id, + max_num_results=request.max_num_results or 10, + filters=request.filters, + ranking_options=( + request.ranking_options.model_dump() + if request.ranking_options + else None + ), + rewrite_query=request.rewrite_query, + features_to_retrieve=( + request.metadata.features_to_retrieve + if request.metadata + else None + ), + ) + except FeatureViewNotFoundException: + return JSONResponse( + status_code=404, + content={ + "error": { + "message": f"No vector store found with id '{vector_store_id}'", + "type": "not_found_error", + } + }, + ) + except ValueError as e: + return JSONResponse( + status_code=422, + content={ + "error": { + "message": str(e), + "type": "invalid_request_error", + } + }, + ) + return JSONResponse(content=result) - from feast.data_source import PushSource - - all_fvs = store.list_feature_views( - allow_cache=request.allow_registry_cache - ) + store.list_stream_feature_views(allow_cache=request.allow_registry_cache) - fvs_with_push_sources = { - fv - for fv in all_fvs - if ( - fv.stream_source is not None - and isinstance(fv.stream_source, PushSource) - and fv.stream_source.name == request.push_source_name - ) - } + @app.post("/push", dependencies=[Depends(inject_user_details)]) + async def push(request: PushFeaturesRequest) -> Response: + with feast_metrics.track_request_latency("/push"): + df = pd.DataFrame(request.df) + actions = [] + if request.to == "offline": + to = PushMode.OFFLINE + actions = [AuthzedAction.WRITE_OFFLINE] + elif request.to == "online": + to = PushMode.ONLINE + actions = [AuthzedAction.WRITE_ONLINE] + elif request.to == "online_and_offline": + to = PushMode.ONLINE_AND_OFFLINE + actions = WRITE + else: + raise ValueError( + f"{request.to} is not a supported push format. Please specify one of these ['online', 'offline', 'online_and_offline']." + ) - for feature_view in fvs_with_push_sources: - assert_permissions(resource=feature_view, actions=actions) + from feast.data_source import PushSource - push_params = dict( - push_source_name=request.push_source_name, - df=df, - allow_registry_cache=request.allow_registry_cache, - to=to, - transform_on_write=request.transform_on_write, - ) + all_fvs = store.list_feature_views( + allow_cache=request.allow_registry_cache + ) + store.list_stream_feature_views( + allow_cache=request.allow_registry_cache + ) + fvs_with_push_sources = { + fv + for fv in all_fvs + if ( + fv.stream_source is not None + and isinstance(fv.stream_source, PushSource) + and fv.stream_source.name == request.push_source_name + ) + } + + for feature_view in fvs_with_push_sources: + assert_permissions(resource=feature_view, actions=actions) + + async def _push_with_to(push_to: PushMode) -> None: + """ + Helper for performing a single push operation. + + NOTE: + - Feast providers **do not currently support async offline writes**. + - Therefore: + * ONLINE and ONLINE_AND_OFFLINE → may be async, depending on provider.async_supported.online.write + * OFFLINE → always synchronous, but executed via run_in_threadpool when called from HTTP handlers. + - The OfflineWriteBatcher handles offline writes directly in its own background thread, but the offline store writes are currently synchronous only. + """ + push_source_name = request.push_source_name + allow_registry_cache = request.allow_registry_cache + transform_on_write = request.transform_on_write + + # Async currently only applies to online store writes (ONLINE / ONLINE_AND_OFFLINE paths) as theres no async for offline store + if push_to in (PushMode.ONLINE, PushMode.ONLINE_AND_OFFLINE) and ( + store._get_provider().async_supported.online.write + ): + await store.push_async( + push_source_name=push_source_name, + df=df, + allow_registry_cache=allow_registry_cache, + to=push_to, + transform_on_write=transform_on_write, + ) + else: + await run_in_threadpool( + lambda: store.push( + push_source_name=push_source_name, + df=df, + allow_registry_cache=allow_registry_cache, + to=push_to, + transform_on_write=transform_on_write, + ) + ) + + needs_online = to in (PushMode.ONLINE, PushMode.ONLINE_AND_OFFLINE) + needs_offline = to in (PushMode.OFFLINE, PushMode.ONLINE_AND_OFFLINE) + + status_code = status.HTTP_200_OK + + if offline_batcher is None or not needs_offline: + await _push_with_to(to) + else: + if needs_online: + await _push_with_to(PushMode.ONLINE) + + offline_batcher.enqueue( + push_source_name=request.push_source_name, + df=df, + allow_registry_cache=request.allow_registry_cache, + transform_on_write=request.transform_on_write, + ) + status_code = status.HTTP_202_ACCEPTED - should_push_async = ( - store._get_provider().async_supported.online.write - and to in [PushMode.ONLINE, PushMode.ONLINE_AND_OFFLINE] - ) - if should_push_async: - await store.push_async(**push_params) - else: - store.push(**push_params) + feast_metrics.track_push(request.push_source_name, request.to) + return Response(status_code=status_code) async def _get_feast_object( feature_view_name: str, allow_registry_cache: bool ) -> FeastObject: - try: - return await run_in_threadpool( - store.get_stream_feature_view, - feature_view_name, - allow_registry_cache=allow_registry_cache, - ) - except FeatureViewNotFoundException: - return await run_in_threadpool( - store.get_feature_view, - feature_view_name, - allow_registry_cache=allow_registry_cache, - ) + return await run_in_threadpool( + get_feature_view_from_feature_store, + store, + feature_view_name, + allow_registry_cache, + ) @app.post("/write-to-online-store", dependencies=[Depends(inject_user_details)]) async def write_to_online_store(request: WriteToFeatureStoreRequest) -> None: @@ -374,11 +934,11 @@ async def write_to_online_store(request: WriteToFeatureStoreRequest) -> None: @app.get("/health") async def health(): - return ( - Response(status_code=status.HTTP_200_OK) - if registry_proto - else Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE) - ) + try: + store.registry.list_projects(allow_cache=True) + return Response(status_code=status.HTTP_200_OK) + except Exception: + return Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE) @app.post("/chat") async def chat(request: ChatRequest): @@ -386,42 +946,6 @@ async def chat(request: ChatRequest): # For now, just return dummy text return {"response": "This is a dummy response from the Feast feature server."} - @app.post("/read-document") - async def read_document_endpoint(request: ReadDocumentRequest): - try: - import os - - if not os.path.exists(request.file_path): - return {"error": f"File not found: {request.file_path}"} - - with open(request.file_path, "r", encoding="utf-8") as file: - content = file.read() - - return {"content": content, "file_path": request.file_path} - except Exception as e: - return {"error": str(e)} - - @app.post("/save-document") - async def save_document_endpoint(request: SaveDocumentRequest): - try: - import json - import os - from pathlib import Path - - file_path = Path(request.file_path).resolve() - if not str(file_path).startswith(os.getcwd()): - return {"error": "Invalid file path"} - - base_name = file_path.stem - labels_file = file_path.parent / f"{base_name}-labels.json" - - with open(labels_file, "w", encoding="utf-8") as file: - json.dump(request.data, file, indent=2, ensure_ascii=False) - - return {"success": True, "saved_to": str(labels_file)} - except Exception as e: - return {"error": str(e)} - @app.get("/chat") async def chat_ui(): # Serve the chat UI @@ -432,50 +956,116 @@ async def chat_ui(): return Response(content=content, media_type="text/html") @app.post("/materialize", dependencies=[Depends(inject_user_details)]) - async def materialize(request: MaterializeRequest) -> None: - for feature_view in request.feature_views or []: - resource = await _get_feast_object(feature_view, True) - assert_permissions( - resource=resource, - actions=[AuthzedAction.WRITE_ONLINE], + async def materialize( + request: MaterializeRequest, + async_mode: bool = Query(False, alias="async"), + force: bool = Query(False), + ): + with feast_metrics.track_request_latency("/materialize"): + fv_names = _authorize_materialize_views( + store, request.feature_views, version=request.version ) - - if request.disable_event_timestamp: - # Query all available data and use current datetime as event timestamp - now = datetime.now() - start_date = datetime( - 1970, 1, 1 - ) # Beginning of time to capture all historical data - end_date = now - else: - if not request.start_ts or not request.end_ts: - raise ValueError( - "start_ts and end_ts are required when disable_event_timestamp is False" + start_date, end_date = _parse_materialize_timestamps(request) + + if async_mode: + if force: + _reset_stuck_materializing_to_generated(store, fv_names) + else: + conflict = _check_already_materializing(store, fv_names) + if conflict: + return conflict + + # Reserve MATERIALIZING before 202 so concurrent requests hit 409. + # store.materialize() treats already-MATERIALIZING as a no-op. + _update_fv_state(store, fv_names, FeatureViewState.MATERIALIZING) + + def _run_materialize(): + try: + store.materialize( + start_date, + end_date, + fv_names, + disable_event_timestamp=request.disable_event_timestamp, + full_feature_names=request.full_feature_names, + version=request.version, + ) + except Exception as e: + logger.error( + f"Async materialization failed for {fv_names}: {e}", + exc_info=True, + ) + _reset_stuck_materializing_to_generated(store, fv_names) + + loop = asyncio.get_running_loop() + loop.run_in_executor(materialize_executor, _run_materialize) + + return JSONResponse( + status_code=202, + content={"status": "accepted", "feature_views": fv_names}, ) - start_date = utils.make_tzaware(parser.parse(request.start_ts)) - end_date = utils.make_tzaware(parser.parse(request.end_ts)) - await run_in_threadpool( - store.materialize, - start_date, - end_date, - request.feature_views, - disable_event_timestamp=request.disable_event_timestamp, - ) + await run_in_threadpool( + store.materialize, + start_date, + end_date, + fv_names, + disable_event_timestamp=request.disable_event_timestamp, + full_feature_names=request.full_feature_names, + version=request.version, + ) @app.post("/materialize-incremental", dependencies=[Depends(inject_user_details)]) - async def materialize_incremental(request: MaterializeIncrementalRequest) -> None: - for feature_view in request.feature_views or []: - resource = await _get_feast_object(feature_view, True) - assert_permissions( - resource=resource, - actions=[AuthzedAction.WRITE_ONLINE], + async def materialize_incremental( + request: MaterializeIncrementalRequest, + async_mode: bool = Query(False, alias="async"), + force: bool = Query(False), + ): + with feast_metrics.track_request_latency("/materialize-incremental"): + fv_names = _authorize_materialize_views( + store, request.feature_views, version=request.version + ) + end_date = utils.make_tzaware(parser.parse(request.end_ts)) + + if async_mode: + if force: + _reset_stuck_materializing_to_generated(store, fv_names) + else: + conflict = _check_already_materializing(store, fv_names) + if conflict: + return conflict + + _update_fv_state(store, fv_names, FeatureViewState.MATERIALIZING) + + def _run_materialize_incremental(): + try: + store.materialize_incremental( + end_date, + fv_names, + full_feature_names=request.full_feature_names, + version=request.version, + ) + except Exception as e: + logger.error( + f"Async materialize-incremental failed for {fv_names}: {e}", + exc_info=True, + ) + _reset_stuck_materializing_to_generated(store, fv_names) + + loop = asyncio.get_running_loop() + loop.run_in_executor(materialize_executor, _run_materialize_incremental) + + return JSONResponse( + status_code=202, + content={"status": "accepted", "feature_views": fv_names}, + ) + + await run_in_threadpool( + store.materialize_incremental, + end_date, + fv_names, + full_feature_names=request.full_feature_names, + version=request.version, ) - await run_in_threadpool( - store.materialize_incremental, - utils.make_tzaware(parser.parse(request.end_ts)), - request.feature_views, - ) @app.exception_handler(Exception) async def rest_exception_handler(request: Request, exc: Exception): @@ -510,12 +1100,41 @@ async def send_message(self, message: str, websocket: WebSocket): manager = ConnectionManager() + MAX_WS_CONNECTIONS = 5 + MAX_MESSAGE_SIZE = 4096 + MAX_MESSAGES_PER_MINUTE = 60 + WS_READ_TIMEOUT_SEC = 60 + @app.websocket("/ws/chat") async def websocket_endpoint(websocket: WebSocket): + if len(manager.active_connections) >= MAX_WS_CONNECTIONS: + await websocket.close(code=status.WS_1008_POLICY_VIOLATION) + return + await manager.connect(websocket) + message_timestamps: List[float] = [] try: while True: - message = await websocket.receive_text() + try: + message = await asyncio.wait_for( + websocket.receive_text(), timeout=WS_READ_TIMEOUT_SEC + ) + except asyncio.TimeoutError: + await websocket.close(code=status.WS_1001_GOING_AWAY) + return + + if len(message) > MAX_MESSAGE_SIZE: + await websocket.close(code=status.WS_1009_MESSAGE_TOO_BIG) + return + + now = time.time() + cutoff = now - 60 + message_timestamps = [ts for ts in message_timestamps if ts >= cutoff] + if len(message_timestamps) >= MAX_MESSAGES_PER_MINUTE: + await websocket.close(code=status.WS_1008_POLICY_VIOLATION) + return + message_timestamps.append(now) + # Process the received message (currently unused but kept for future implementation) # For now, just return dummy text response = f"You sent: '{message}'. This is a dummy response from the Feast feature server." @@ -541,6 +1160,7 @@ async def websocket_endpoint(websocket: WebSocket): def _add_mcp_support_if_enabled(app, store: "feast.FeatureStore"): """Add MCP support to the FastAPI app if enabled in configuration.""" + mcp_transport_not_supported_error = None try: # Check if MCP is enabled in feature server config if ( @@ -549,7 +1169,16 @@ def _add_mcp_support_if_enabled(app, store: "feast.FeatureStore"): and store.config.feature_server.type == "mcp" and getattr(store.config.feature_server, "mcp_enabled", False) ): - from feast.infra.mcp_servers.mcp_server import add_mcp_support_to_app + try: + from feast.infra.mcp_servers.mcp_server import ( + McpTransportNotSupportedError, + add_mcp_support_to_app, + ) + + mcp_transport_not_supported_error = McpTransportNotSupportedError + except ImportError as e: + logger.error(f"Error checking/adding MCP support: {e}") + return mcp_server = add_mcp_support_to_app(app, store, store.config.feature_server) @@ -560,6 +1189,10 @@ def _add_mcp_support_if_enabled(app, store: "feast.FeatureStore"): else: logger.debug("MCP support is not enabled in feature server configuration") except Exception as e: + if mcp_transport_not_supported_error and isinstance( + e, mcp_transport_not_supported_error + ): + raise logger.error(f"Error checking/adding MCP support: {e}") # Don't fail the entire server if MCP fails to initialize @@ -568,12 +1201,16 @@ def _add_mcp_support_if_enabled(app, store: "feast.FeatureStore"): import gunicorn.app.base class FeastServeApplication(gunicorn.app.base.BaseApplication): - def __init__(self, store: "feast.FeatureStore", **options): + def __init__( + self, store: "feast.FeatureStore", metrics_enabled: bool = False, **options + ): self._app = get_app( store=store, registry_ttl_sec=options["registry_ttl_sec"], ) + self._store = store self._options = options + self._metrics_enabled = metrics_enabled super().__init__() def load_config(self): @@ -582,25 +1219,24 @@ def load_config(self): self.cfg.set(key.lower(), value) self.cfg.set("worker_class", "uvicorn_worker.UvicornWorker") + if self._metrics_enabled: + self.cfg.set( + "post_worker_init", + functools.partial(_gunicorn_post_worker_init, self._store), + ) + self.cfg.set("child_exit", _gunicorn_child_exit) def load(self): return self._app + def _gunicorn_post_worker_init(store: "feast.FeatureStore", worker): + """Start per-worker resource and freshness monitoring after Gunicorn forks.""" + feast_metrics.init_worker_monitoring() + feast_metrics.init_worker_freshness_monitoring(store) -def monitor_resources(self, interval: int = 5): - """Function to monitor and update CPU and memory usage metrics.""" - logger.debug(f"Starting resource monitoring with interval {interval} seconds") - p = psutil.Process() - logger.debug(f"PID is {p.pid}") - while True: - with p.oneshot(): - cpu_usage = p.cpu_percent() - memory_usage = p.memory_percent() - logger.debug(f"CPU usage: {cpu_usage}%, Memory usage: {memory_usage}%") - logger.debug(f"CPU usage: {cpu_usage}%, Memory usage: {memory_usage}%") - cpu_usage_gauge.set(cpu_usage) - memory_usage_gauge.set(memory_usage) - time.sleep(interval) + def _gunicorn_child_exit(server, worker): + """Clean up Prometheus metric files for a dead worker.""" + feast_metrics.mark_process_dead(worker.pid) def start_server( @@ -609,6 +1245,9 @@ def start_server( port: int, no_access_log: bool, workers: int, + worker_connections: int, + max_requests: int, + max_requests_jitter: int, keep_alive_timeout: int, registry_ttl_sec: int, tls_key_path: str, @@ -619,15 +1258,20 @@ def start_server( raise ValueError( "Both key and cert file paths are required to start server in TLS mode." ) - if metrics: - logger.info("Starting Prometheus Server") - start_http_server(8000) - logger.debug("Starting background thread to monitor CPU and memory usage") - monitoring_thread = threading.Thread( - target=monitor_resources, args=(5,), daemon=True + fs_cfg = getattr(store.config, "feature_server", None) + metrics_cfg = getattr(fs_cfg, "metrics", None) + metrics_from_config = getattr(metrics_cfg, "enabled", False) + metrics_active = metrics or metrics_from_config + uses_gunicorn = sys.platform != "win32" + if metrics_active: + flags = feast_metrics.build_metrics_flags(metrics_cfg) + feast_metrics.start_metrics_server( + store, + metrics_config=flags, + start_resource_monitoring=not uses_gunicorn, + start_freshness_monitoring=not uses_gunicorn, ) - monitoring_thread.start() logger.debug("start_server called") auth_type = str_to_auth_manager_type(store.config.auth_config.type) @@ -641,11 +1285,14 @@ def start_server( ) logger.debug("Auth manager initialized successfully") - if sys.platform != "win32": + if uses_gunicorn: options = { "bind": f"{host}:{port}", "accesslog": None if no_access_log else "-", "workers": workers, + "worker_connections": worker_connections, + "max_requests": max_requests, + "max_requests_jitter": max_requests_jitter, "keepalive": keep_alive_timeout, "registry_ttl_sec": registry_ttl_sec, } @@ -654,7 +1301,9 @@ def start_server( if tls_key_path and tls_cert_path: options["keyfile"] = tls_key_path options["certfile"] = tls_cert_path - FeastServeApplication(store=store, **options).run() + FeastServeApplication( + store=store, metrics_enabled=metrics_active, **options + ).run() else: import uvicorn @@ -670,3 +1319,210 @@ def start_server( ) else: uvicorn.run(app, host=host, port=port, access_log=(not no_access_log)) + + +class _OfflineBatchKey(NamedTuple): + push_source_name: str + allow_registry_cache: bool + transform_on_write: bool + + +class OfflineWriteBatcher: + """ + In-process offline write batcher for /push requests. + + - Buffers DataFrames per (push_source_name, allow_registry_cache, transform_on_write) + - Flushes when either: + * total rows in a buffer >= batch_size, or + * time since last flush >= batch_interval_seconds + - Flush runs in a dedicated background thread so the HTTP event loop stays unblocked. + """ + + def __init__(self, store: "feast.FeatureStore", cfg: Any): + self._store = store + self._cfg = cfg + + # Buffers and timestamps keyed by batch key + self._buffers: DefaultDict[_OfflineBatchKey, List[pd.DataFrame]] = defaultdict( + list + ) + self._last_flush: DefaultDict[_OfflineBatchKey, float] = defaultdict(time.time) + self._inflight: Set[_OfflineBatchKey] = set() + + self._lock = threading.Lock() + self._stop_event = threading.Event() + + # Start background flusher thread + self._thread = threading.Thread( + target=self._run, name="offline_write_batcher", daemon=True + ) + self._thread.start() + + logger.debug( + "OfflineWriteBatcher initialized: batch_size=%s, batch_interval_seconds=%s", + getattr(cfg, "batch_size", None), + getattr(cfg, "batch_interval_seconds", None), + ) + + # ---------- Public API ---------- + + def enqueue( + self, + push_source_name: str, + df: pd.DataFrame, + allow_registry_cache: bool, + transform_on_write: bool, + ) -> None: + """ + Enqueue a dataframe for offline write, grouped by push source + flags. + Cheap and non-blocking; heavy I/O happens in background thread. + """ + key = _OfflineBatchKey( + push_source_name=push_source_name, + allow_registry_cache=allow_registry_cache, + transform_on_write=transform_on_write, + ) + + with self._lock: + self._buffers[key].append(df) + total_rows = sum(len(d) for d in self._buffers[key]) + should_flush = total_rows >= self._cfg.batch_size + + if should_flush: + # Size-based flush + logger.debug( + "OfflineWriteBatcher size threshold reached for %s: %s rows", + key, + total_rows, + ) + self._flush(key) + + def flush_all(self) -> None: + """ + Flush all buffers synchronously. Intended for graceful shutdown. + """ + with self._lock: + keys = list(self._buffers.keys()) + for key in keys: + self._flush(key) + + def shutdown(self, timeout: float = 5.0) -> None: + """ + Stop the background thread and perform a best-effort flush. + """ + logger.debug("Shutting down OfflineWriteBatcher") + self._stop_event.set() + try: + self._thread.join(timeout=timeout) + except Exception: + logger.exception("Error joining OfflineWriteBatcher thread") + + # Best-effort final flush + try: + self.flush_all() + except Exception: + logger.exception("Error during final OfflineWriteBatcher flush") + + # ---------- Internal helpers ---------- + + def _run(self) -> None: + """ + Background loop: periodically checks for buffers that should be flushed + based on time since last flush. + """ + interval = max(1, int(getattr(self._cfg, "batch_interval_seconds", 30))) + logger.debug( + "OfflineWriteBatcher background loop started with check interval=%s", + interval, + ) + + while not self._stop_event.wait(timeout=interval): + now = time.time() + try: + with self._lock: + keys_to_flush: List[_OfflineBatchKey] = [] + for key, dfs in list(self._buffers.items()): + if not dfs: + continue + last = self._last_flush[ + key + ] # this will also init the default timestamp + age = now - last + if age >= self._cfg.batch_interval_seconds: + logger.debug( + "OfflineWriteBatcher time threshold reached for %s: age=%s", + key, + age, + ) + keys_to_flush.append(key) + for key in keys_to_flush: + self._flush(key) + except Exception: + logger.exception("Error in OfflineWriteBatcher background loop") + + logger.debug("OfflineWriteBatcher background loop exiting") + + def _drain_locked(self, key: _OfflineBatchKey) -> Optional[List[pd.DataFrame]]: + """ + Drain a single buffer; caller must hold self._lock. + """ + if key in self._inflight: + return None + + dfs = self._buffers.get(key) + if not dfs: + return None + + self._buffers[key] = [] + self._inflight.add(key) + return dfs + + def _flush(self, key: _OfflineBatchKey) -> None: + """ + Flush a single buffer. Extracts data under lock, then does I/O without lock. + """ + while True: + with self._lock: + dfs = self._drain_locked(key) + + if not dfs: + return + + batch_df = pd.concat(dfs, ignore_index=True) + + # NOTE: offline writes are currently synchronous only, so we call directly + try: + self._store.push( + push_source_name=key.push_source_name, + df=batch_df, + allow_registry_cache=key.allow_registry_cache, + to=PushMode.OFFLINE, + transform_on_write=key.transform_on_write, + ) + except Exception: + logger.exception("Error flushing offline batch for %s", key) + with self._lock: + self._buffers[key] = dfs + self._buffers[key] + self._inflight.discard(key) + return + + logger.debug( + "Flushing offline batch for push_source=%s with %s rows", + key.push_source_name, + len(batch_df), + ) + + with self._lock: + self._last_flush[key] = time.time() + self._inflight.discard(key) + pending_rows = sum(len(d) for d in self._buffers.get(key, [])) + should_flush = pending_rows >= self._cfg.batch_size + + if not should_flush: + return + + logger.debug( + "OfflineWriteBatcher size threshold reached for %s: %s rows", + key, + pending_rows, + ) diff --git a/sdk/python/feast/feature_server_utils.py b/sdk/python/feast/feature_server_utils.py new file mode 100644 index 00000000000..946fae8a9b4 --- /dev/null +++ b/sdk/python/feast/feature_server_utils.py @@ -0,0 +1,160 @@ +"""Fast serialization utilities for Feature Server responses. + +Matches the output format of MessageToDict with proto_json.patch() applied. +Values are serialized as native Python types (not wrapped dicts). +""" + +import base64 +import logging +from datetime import datetime, timezone +from typing import Any, Dict, Optional + +from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse +from feast.protos.feast.types.Value_pb2 import Value + +logger = logging.getLogger(__name__) + +# FieldStatus enum mapping (protos/feast/serving/ServingService.proto) +_STATUS_NAMES: Dict[int, str] = { + 0: "INVALID", + 1: "PRESENT", + 2: "NULL_VALUE", + 3: "NOT_FOUND", + 4: "OUTSIDE_MAX_AGE", +} + + +def convert_response_to_dict(response: GetOnlineFeaturesResponse) -> Dict[str, Any]: + """Convert GetOnlineFeaturesResponse to a JSON-serializable dict. + + Matches the structure produced by MessageToDict(proto, preserving_proto_field_name=True) + with proto_json.patch() applied, with one intentional difference: + + - double_val fields are returned as Python float objects (json.dumps uses Python 3.1+ + shortest round-trip form, ~15-17 sig digits) rather than 18 fixed significant digits + (float_precision=18). Values are numerically identical; only the JSON string length + may differ. This is safe for all ML feature types and avoids unnecessary precision + overhead. + """ + result: Dict[str, Any] = { + "results": [ + { + "values": [_value_to_native(v) for v in feature_vector.values], + "statuses": [ + _STATUS_NAMES.get(s, "INVALID") for s in feature_vector.statuses + ], + **( + { + "event_timestamps": [ + _timestamp_to_str(ts) + for ts in feature_vector.event_timestamps + ] + } + if feature_vector.event_timestamps + else {} + ), + } + for feature_vector in response.results + ] + } + + if response.HasField("metadata"): + result["metadata"] = _metadata_to_dict(response.metadata) + + if response.status: + result["status"] = response.status + + return result + + +def _value_to_native(v: Value) -> Optional[Any]: + """Convert a Value proto to a JSON-serializable Python type. + + bytes_val and bytes_list_val are base64-encoded (RFC 4648) so that + JSONResponse can serialize them without TypeError. This matches standard + protobuf JSON encoding for bytes fields and is safe for all HTTP clients. + """ + which = v.WhichOneof("val") + if which is None or which == "null_val": + return None + # bytes must be base64-encoded for JSON serialization + elif which == "bytes_val": + return base64.b64encode(v.bytes_val).decode("ascii") + # RepeatedValue — nested Values that must be recursively converted + elif which in ("list_val", "set_val"): + return [_value_to_native(nested) for nested in getattr(v, which).val] + # Map — recursively convert nested Values + elif which in ("map_val", "struct_val"): + return {k: _value_to_native(vv) for k, vv in getattr(v, which).val.items()} + # MapList — list of Map + elif which in ("map_list_val", "struct_list_val"): + return [ + {k: _value_to_native(vv) for k, vv in m.val.items()} + for m in getattr(v, which).val + ] + # scalar_map_val has non-string keys; full conversion requires extra work and + # this type is not returned by standard get_online_features paths today. + elif which == "scalar_map_val": + logger.warning( + "scalar_map_val is not yet supported by convert_response_to_dict; value will be None" + ) + return None + # zoned_timestamp_val is a ZonedTimestamp message; serialize as an ISO 8601 + # string in its stored zone so JSONResponse can encode it (the raw message + # is not JSON-serializable). + elif which == "zoned_timestamp_val": + return _zoned_timestamp_to_str(v.zoned_timestamp_val) + # bytes_list_val / bytes_set_val — base64-encode each element + elif which in ("bytes_list_val", "bytes_set_val"): + return [base64.b64encode(b).decode("ascii") for b in getattr(v, which).val] + # All other list/set types have scalar .val fields + elif "_list_" in which or "_set_" in which: + return list(getattr(v, which).val) + else: + return getattr(v, which) + + +def _zoned_timestamp_to_str(zoned) -> Optional[str]: + """Convert a ZonedTimestamp proto to an ISO 8601 string in its stored zone. + + A null instant (NaT sentinel) returns None. The zone is resolved via the + same logic used on the read path so IANA names and fixed offsets round-trip. + """ + from feast.type_map import NULL_TIMESTAMP_INT_VALUE, _zone_from_name + + if zoned.unix_timestamp == NULL_TIMESTAMP_INT_VALUE: + return None + tz = _zone_from_name(zoned.zone) + return datetime.fromtimestamp(zoned.unix_timestamp, tz=tz).isoformat() + + +def _timestamp_to_str(ts) -> str: + """Convert protobuf Timestamp to RFC 3339 format with Z suffix. + + Uses adaptive precision to match MessageToDict output: + - No fractional seconds when nanos == 0 + - 3 digits (milliseconds) when nanos % 1_000_000 == 0 + - 6 digits (microseconds) when nanos % 1_000 == 0 + - 9 digits (nanoseconds) otherwise + """ + if ts.seconds == 0 and ts.nanos == 0: + return "1970-01-01T00:00:00Z" + dt = datetime.fromtimestamp(ts.seconds, tz=timezone.utc) + base = dt.strftime("%Y-%m-%dT%H:%M:%S") + nanos = ts.nanos + if nanos == 0: + return base + "Z" + elif nanos % 1_000_000 == 0: + return base + ".%03dZ" % (nanos // 1_000_000) + elif nanos % 1_000 == 0: + return base + ".%06dZ" % (nanos // 1_000) + else: + return base + ".%09dZ" % nanos + + +def _metadata_to_dict(metadata) -> Dict[str, Any]: + """Convert FeatureResponseMeta to dict (matches proto_json.patch() format).""" + result: Dict[str, Any] = {} + if metadata.HasField("feature_names"): + result["feature_names"] = list(metadata.feature_names.val) + return result diff --git a/sdk/python/feast/feature_service.py b/sdk/python/feast/feature_service.py index 5c062f15c67..5cc61372e0f 100644 --- a/sdk/python/feast/feature_service.py +++ b/sdk/python/feast/feature_service.py @@ -1,14 +1,19 @@ from datetime import datetime -from typing import Dict, List, Optional, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Union from google.protobuf.json_format import MessageToJson from typeguard import typechecked from feast.base_feature_view import BaseFeatureView -from feast.errors import FeatureViewMissingDuringFeatureServiceInference +from feast.errors import ( + FeastObjectNotFoundException, + FeatureViewMissingDuringFeatureServiceInference, +) from feast.feature_logging import LoggingConfig from feast.feature_view import FeatureView from feast.feature_view_projection import FeatureViewProjection +from feast.field import Field +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.protos.feast.core.FeatureService_pb2 import ( FeatureService as FeatureServiceProto, @@ -20,6 +25,9 @@ FeatureServiceSpec as FeatureServiceSpecProto, ) +if TYPE_CHECKING: + from feast.infra.registry.base_registry import BaseRegistry + @typechecked class FeatureService: @@ -40,7 +48,7 @@ class FeatureService: """ name: str - _features: List[Union[FeatureView, OnDemandFeatureView]] + _features: List[Union[FeatureView, OnDemandFeatureView, LabelView]] feature_view_projections: List[FeatureViewProjection] description: str tags: Dict[str, str] @@ -49,27 +57,32 @@ class FeatureService: last_updated_timestamp: Optional[datetime] = None logging_config: Optional[LoggingConfig] = None + precompute_online: bool = False + def __init__( self, *, name: str, - features: List[Union[FeatureView, OnDemandFeatureView]], + features: List[Union[FeatureView, OnDemandFeatureView, LabelView]], tags: Optional[Dict[str, str]] = None, description: str = "", owner: str = "", logging_config: Optional[LoggingConfig] = None, + precompute_online: bool = False, ): """ Creates a FeatureService object. Args: name: The unique name of the feature service. - feature_view_projections: A list containing feature views and feature view + features: A list containing feature views and feature view projections, representing the features in the feature service. description (optional): A human-readable description. tags (optional): A dictionary of key-value pairs to store arbitrary metadata. owner (optional): The owner of the feature view, typically the email of the primary maintainer. + precompute_online (optional): When True, a pre-computed feature vector is + maintained per entity for single-read online retrieval. """ self.name = name self._features = features @@ -80,6 +93,7 @@ def __init__( self.created_timestamp = None self.last_updated_timestamp = None self.logging_config = logging_config + self.precompute_online = precompute_online for feature_grouping in self._features: if isinstance(feature_grouping, BaseFeatureView): self.feature_view_projections.append(feature_grouping.projection) @@ -154,6 +168,119 @@ def infer_features( f'{type(feature_grouping)} as part of the "features" argument.)' ) + def prepare_for_apply( + self, + registry: "BaseRegistry", + project: str, + allow_cache: bool = False, + ) -> "FeatureService": + """ + Materialize feature view projections before registry apply. + + Uses the same FeatureService construction and ``infer_features`` path as + ``FeatureStore.apply`` for SDK-defined services. + + When the service is already fully resolved (SDK path where _features is + set and projections already have features populated via infer_features, + OR the proto deserialization path where projections carry full dtype + info), this is a no-op. + """ + from feast.types import Invalid + + if self._features and all(p.features for p in self.feature_view_projections): + return self + + if ( + not self._features + and self.feature_view_projections + and all( + p.features and all(f.dtype != Invalid for f in p.features) + for p in self.feature_view_projections + ) + ): + return self + + fvs_to_update: Dict[str, Union[FeatureView, BaseFeatureView]] = {} + + if self._features: + for feature_grouping in self._features: + if isinstance(feature_grouping, BaseFeatureView): + fvs_to_update[feature_grouping.name] = ( + registry.get_any_feature_view( + feature_grouping.name, project, allow_cache=allow_cache + ) + ) + self.infer_features(fvs_to_update=fvs_to_update) + return self + + resolved_features: List[Union[FeatureView, OnDemandFeatureView, LabelView]] = [] + for projection in self.feature_view_projections: + try: + feature_view = registry.get_any_feature_view( + projection.name, project, allow_cache=allow_cache + ) + except FeastObjectNotFoundException as exc: + raise FeastObjectNotFoundException( + f"Feature view '{projection.name}' not found in project '{project}'" + ) from exc + + if not isinstance( + feature_view, (FeatureView, OnDemandFeatureView, LabelView) + ): + raise ValueError( + f"Cannot resolve projection for feature view '{projection.name}'" + ) + + fvs_to_update[feature_view.name] = feature_view + features_by_name = { + feature.name: feature for feature in feature_view.features + } + + if self._projection_matches_registry_features(projection, features_by_name): + resolved_features.append(feature_view.with_projection(projection)) + elif projection.desired_features: + resolved_features.append( + feature_view[list(projection.desired_features)] + ) + elif not projection.features: + resolved_features.append(feature_view) + else: + resolved_features.append( + feature_view[[feature.name for feature in projection.features]] + ) + + prepared = FeatureService( + name=self.name, + features=resolved_features, + tags=self.tags, + description=self.description, + owner=self.owner, + logging_config=self.logging_config, + precompute_online=self.precompute_online, + ) + prepared.created_timestamp = self.created_timestamp + prepared.last_updated_timestamp = self.last_updated_timestamp + prepared.infer_features(fvs_to_update=fvs_to_update) + + self._features = prepared._features + self.feature_view_projections = prepared.feature_view_projections + return self + + @staticmethod + def _projection_matches_registry_features( + projection: FeatureViewProjection, + features_by_name: Dict[str, Field], + ) -> bool: + if not projection.features: + return False + + for feature in projection.features: + if feature.name not in features_by_name: + return False + if feature != features_by_name[feature.name]: + return False + return True + def __repr__(self): items = (f"{k} = {v}" for k, v in self.__dict__.items()) return f"<{self.__class__.__name__}({', '.join(items)})>" @@ -166,15 +293,14 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, FeatureService): - raise TypeError( - "Comparisons should only involve FeatureService class objects." - ) + return False if ( self.name != other.name or self.description != other.description or self.tags != other.tags or self.owner != other.owner + or self.precompute_online != other.precompute_online ): return False @@ -202,6 +328,7 @@ def from_proto(cls, feature_service_proto: FeatureServiceProto): logging_config=LoggingConfig.from_proto( feature_service_proto.spec.logging_config ), + precompute_online=feature_service_proto.spec.precompute_online, ) fs.feature_view_projections.extend( [ @@ -245,9 +372,62 @@ def to_proto(self) -> FeatureServiceProto: logging_config=self.logging_config.to_proto() if self.logging_config else None, + precompute_online=self.precompute_online, ) return FeatureServiceProto(spec=spec, meta=meta) + @classmethod + def build_apply_request( + cls, + *, + name: str, + project: str, + feature_view_refs: List[tuple[str, Optional[List[str]]]], + description: str = "", + tags: Optional[Dict[str, str]] = None, + owner: str = "", + commit: bool = True, + ): + """Build an unresolved ApplyFeatureServiceRequest from feature view refs.""" + from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 + from feast.protos.feast.core.FeatureViewProjection_pb2 import ( + FeatureViewProjection as FeatureViewProjectionProto, + ) + from feast.protos.feast.registry import RegistryServer_pb2 + + projections = [] + for feature_view_name, feature_names in feature_view_refs: + projection = FeatureViewProjectionProto( + feature_view_name=feature_view_name, + ) + if feature_names: + for feature_name in feature_names: + projection.feature_columns.append(FeatureSpecV2(name=feature_name)) + projections.append(projection) + + spec = FeatureServiceSpecProto( + name=name, + features=projections, + tags=tags or {}, + description=description, + owner=owner, + ) + return RegistryServer_pb2.ApplyFeatureServiceRequest( + feature_service=FeatureServiceProto(spec=spec), + project=project, + commit=commit, + ) + def validate(self): - pass + if not self.precompute_online: + return + + for fv in self._features: + if isinstance(fv, OnDemandFeatureView) and not fv.write_to_online_store: + raise ValueError( + f"FeatureService '{self.name}' has precompute_online=True but " + f"contains OnDemandFeatureView '{fv.name}' with " + f"write_to_online_store=False. On-demand transforms computed at " + f"serve time cannot be pre-computed." + ) diff --git a/sdk/python/feast/feature_store.py b/sdk/python/feast/feature_store.py index 7a0f362c8c7..546f6f7680d 100644 --- a/sdk/python/feast/feature_store.py +++ b/sdk/python/feast/feature_store.py @@ -12,14 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio +import copy import itertools +import logging import os +import time import warnings +from contextvars import ContextVar +from dataclasses import dataclass, field from datetime import datetime, timedelta from pathlib import Path from typing import ( + TYPE_CHECKING, Any, - Callable, Dict, Iterable, List, @@ -31,6 +36,10 @@ cast, ) +if TYPE_CHECKING: + from feast.diff.apply_progress import ApplyProgressContext + from feast.embedder import EmbeddingProvider + import pandas as pd import pyarrow as pa from colorama import Fore, Style @@ -53,6 +62,7 @@ from feast.dqm.errors import ValidationFailed from feast.entity import Entity from feast.errors import ( + ConflictingFeatureViewNames, DataFrameSerializationError, DataSourceRepeatNamesException, FeatureViewNotFoundException, @@ -61,7 +71,14 @@ ) from feast.feast_object import FeastObject from feast.feature_service import FeatureService -from feast.feature_view import DUMMY_ENTITY, DUMMY_ENTITY_NAME, FeatureView +from feast.feature_view import ( + DUMMY_ENTITY, + DUMMY_ENTITY_ID, + DUMMY_ENTITY_NAME, + FeatureView, + FeatureViewState, +) +from feast.filter_models import ComparisonFilter, CompoundFilter, convert_dict_to_filter from feast.inference import ( update_data_sources_with_inferred_event_timestamp_col, update_feature_views_with_inferred_features_and_entities, @@ -74,11 +91,11 @@ from feast.infra.registry.base_registry import BaseRegistry from feast.infra.registry.registry import Registry from feast.infra.registry.sql import SqlRegistry +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.online_response import OnlineResponse from feast.permissions.permission import Permission from feast.project import Project -from feast.protos.feast.core.InfraObject_pb2 import Infra as InfraProto from feast.protos.feast.serving.ServingService_pb2 import ( FieldStatus, GetOnlineFeaturesResponse, @@ -93,10 +110,56 @@ from feast.stream_feature_view import StreamFeatureView from feast.transformation.pandas_transformation import PandasTransformation from feast.transformation.python_transformation import PythonTransformation -from feast.utils import _get_feature_view_vector_field_metadata, _utc_now +from feast.utils import ( + _distance_to_score, + _get_feature_view_vector_field_metadata, + _utc_now, +) +from feast.vector_store_utils import feature_view_to_vs_id +from feast.version_utils import parse_version + +try: + from datetime import timezone as _timezone +except ImportError: + _timezone = None # type: ignore[assignment,misc] + +_track_materialization = None # Lazy-loaded on first materialization call +_track_materialization_loaded = False + +_logger = logging.getLogger(__name__) + + +def _get_track_materialization(): + """Lazy-import feast.metrics only when materialization tracking is needed. + + Avoids importing the metrics module (and its prometheus_client / + psutil dependencies plus temp-dir creation) for every FeatureStore + usage such as ``feast apply`` or simple SDK reads. + """ + global _track_materialization, _track_materialization_loaded + if not _track_materialization_loaded: + _track_materialization_loaded = True + try: + from feast.metrics import track_materialization + + _track_materialization = track_materialization + except Exception: # pragma: no cover + _track_materialization = None + return _track_materialization + warnings.simplefilter("once", DeprecationWarning) +_UNSET = object() + + +@dataclass +class _MaterializationDateRange: + """Per-batch start dates plus shared end date for materialization watermarks.""" + + end_date: datetime + fv_start_dates: dict = field(default_factory=dict) + class FeatureStore: """ @@ -107,18 +170,23 @@ class FeatureStore: repo_path: The path to the feature repo. _registry: The registry for the feature store. _provider: The provider for the feature store. + _openlineage_emitter: Optional OpenLineage emitter for lineage tracking. """ config: RepoConfig repo_path: Path - _registry: BaseRegistry - _provider: Provider + _registry: Optional[BaseRegistry] + _provider: Optional[Provider] + _openlineage_emitter: Optional[Any] = None + _embedding_provider: Optional["EmbeddingProvider"] + _feature_service_cache: Dict[str, List[str]] def __init__( self, repo_path: Optional[str] = None, config: Optional[RepoConfig] = None, fs_yaml_file: Optional[Path] = None, + embedding_provider: Optional["EmbeddingProvider"] = None, ): """ Creates a FeatureStore object. @@ -128,6 +196,10 @@ def __init__( config (optional): Configuration object used to configure the feature store. fs_yaml_file (optional): Path to the `feature_store.yaml` file used to configure the feature store. At most one of 'fs_yaml_file' and 'config' can be set. + embedding_provider (optional): Custom embedding provider implementing + the :class:`~feast.embedder.EmbeddingProvider` protocol. When not + supplied, a :class:`~feast.embedder.SentenceTransformersEmbeddingProvider` is + created from ``embedding_model`` in ``feature_store.yaml``. Raises: ValueError: If both or neither of repo_path and config are specified. @@ -153,54 +225,442 @@ def __init__( self.repo_path, utils.get_default_yaml_file_path(self.repo_path) ) + # Initialize lazy-loaded components as None + self._registry = None + self._provider = None + self._openlineage_emitter = None + self._current_project: ContextVar[Optional[str]] = ContextVar( + "current_project", default=None + ) + self._embedding_provider = embedding_provider + + # Initialize feature service cache for performance optimization + self._feature_service_cache = {} + + # Cache for _resolve_feature_service_name lookups + self._fs_name_cache: Dict[frozenset, Optional[str]] = {} + self._fs_name_index: Dict[frozenset, str] = {} + self._fs_name_index_ts: float = -self._FS_NAME_INDEX_TTL_SECONDS + + self._mlflow_client: Any = _UNSET + + def _init_mlflow(self) -> Optional[Any]: + """Bootstrap MLflow integration on first access. + + Checks the config, imports the module, and creates the integration + client. Returns the client or ``None`` if MLflow is disabled or + unavailable. + """ + try: + mlflow_cfg = getattr(self.config, "mlflow", None) + if mlflow_cfg is None or not mlflow_cfg.enabled: + return None + from feast.mlflow import _register_store + + _register_store(self) + + from feast.mlflow_integration.client import FeastMlflowClient + + return FeastMlflowClient(self) + except ImportError: + return None + except Exception as e: + warnings.warn(f"Failed to configure MLflow tracking: {e}") + return None + + @property + def mlflow(self) -> Any: + """Access the Feast–MLflow integration client. + + Lazily initializes on first access. Returns ``None`` when MLflow + integration is not enabled, allowing callers to guard with + ``if store.mlflow:``. + """ + if self._mlflow_client is _UNSET: + self._mlflow_client = self._init_mlflow() + return self._mlflow_client + + @staticmethod + def _count_entities(entity_rows: Any) -> int: + """Count entities from either a list or columnar mapping.""" + if isinstance(entity_rows, list): + return len(entity_rows) + if isinstance(entity_rows, Mapping): + try: + _first_col = next(iter(entity_rows.values())) + if isinstance(_first_col, RepeatedValue): + return len(_first_col.val) + return len(_first_col) + except Exception: + return 0 + return 0 + + _FS_NAME_INDEX_TTL_SECONDS = 300 + + def _rebuild_fs_name_index(self) -> None: + """Rebuild the {frozenset(refs) → service_name} index from the registry.""" + index: Dict[frozenset, str] = {} + for fs in self.registry.list_feature_services(self.project, allow_cache=True): + fs_refs = frozenset( + f"{p.name_to_use()}:{f.name}" + for p in fs.feature_view_projections + for f in p.features + ) + index[fs_refs] = fs.name + self._fs_name_index = index + self._fs_name_cache = {} + self._fs_name_index_ts = time.monotonic() + + def _resolve_feature_service_name(self, feature_refs: List[str]) -> Optional[str]: + """Find the best-matching feature service for the given feature refs. + + Resolution: exact match wins immediately; otherwise the smallest + superset (fewest extra features) is returned. The full index is + rebuilt from the registry every _FS_NAME_INDEX_TTL_SECONDS and + per-query results are cached for O(1) repeated lookups. + """ + try: + now = time.monotonic() + if (now - self._fs_name_index_ts) >= self._FS_NAME_INDEX_TTL_SECONDS: + self._rebuild_fs_name_index() + + ref_key = frozenset(feature_refs) + if ref_key in self._fs_name_cache: + return self._fs_name_cache[ref_key] + + if ref_key in self._fs_name_index: + self._fs_name_cache[ref_key] = self._fs_name_index[ref_key] + return self._fs_name_index[ref_key] + + best_match = None + best_extra = float("inf") + for fs_refs, fs_name in self._fs_name_index.items(): + if ref_key.issubset(fs_refs): + extra = len(fs_refs) - len(ref_key) + if extra < best_extra: + best_match = fs_name + best_extra = extra + + self._fs_name_cache[ref_key] = best_match + return best_match + except Exception as e: + _logger.debug("Failed to resolve feature service name: %s", e) + return None + + def _log_entity_df_metadata(self, entity_df, start_date=None, end_date=None): + """Log lightweight entity_df metadata to MLflow.""" + try: + if self.mlflow is not None: + self.mlflow.log_entity_df_metadata(entity_df, start_date, end_date) + except Exception as e: + _logger.debug("Failed to log entity_df metadata to MLflow: %s", e) + + def _log_entity_df_artifact(self, entity_df): + """Upload entity DataFrame as a parquet artifact to MLflow.""" + try: + if self.mlflow is not None: + self.mlflow.log_entity_df_artifact(entity_df) + except Exception as e: + _logger.debug("Failed to log entity_df artifact to MLflow: %s", e) + + def _init_openlineage_emitter(self) -> Optional[Any]: + """Initialize OpenLineage emitter if configured and enabled.""" + try: + if ( + hasattr(self.config, "openlineage") + and self.config.openlineage is not None + and self.config.openlineage.enabled + ): + from feast.openlineage import FeastOpenLineageEmitter + + ol_config = self.config.openlineage.to_openlineage_config() + emitter = FeastOpenLineageEmitter(ol_config) + if emitter.is_enabled: + return emitter + except ImportError: + # OpenLineage not installed, silently skip + pass + except Exception as e: + warnings.warn(f"Failed to initialize OpenLineage emitter: {e}") + return None + + def __repr__(self) -> str: + # Show lazy loading status without triggering initialization + registry_status = "not loaded" if self._registry is None else "loaded" + provider_status = "not loaded" if self._provider is None else "loaded" + return ( + f"FeatureStore(\n" + f" repo_path={self.repo_path!r},\n" + f" config={self.config!r},\n" + f" registry={registry_status},\n" + f" provider={provider_status}\n" + f")" + ) + + @property + def embedding_provider(self) -> "EmbeddingProvider": + """Return the active embedding provider, creating one from config if needed.""" + if self._embedding_provider is None: + from feast.embedder import get_embedding_provider + + embed_cfg = self.config.embedding_model + if embed_cfg is None: + raise ValueError( + "No embedding provider set and embedding_model is not " + "configured in feature_store.yaml. Either pass an " + "embedding_provider to FeatureStore() or add an " + "'embedding_model' section to feature_store.yaml.\n" + "Example:\n" + " embedding_model:\n" + " provider: sentence_transformers\n" + " model: all-MiniLM-L6-v2" + ) + self._embedding_provider = get_embedding_provider(embed_cfg) + return self._embedding_provider + + @embedding_provider.setter + def embedding_provider(self, provider: "EmbeddingProvider") -> None: + self._embedding_provider = provider + + @property + def registry(self) -> BaseRegistry: + """Gets the registry of this feature store.""" + if self._registry is None: + self._registry = self._create_registry() + # Add feature service cache to registry for performance optimization + if self._registry and not hasattr(self._registry, "_feature_service_cache"): + setattr( + self._registry, + "_feature_service_cache", + self._feature_service_cache, + ) + if self._registry is None: + raise RuntimeError("Registry failed to initialize properly") + return self._registry + + def _create_registry(self) -> BaseRegistry: + """Create and initialize the registry.""" registry_config = self.config.registry if registry_config.registry_type == "sql": - self._registry = SqlRegistry(registry_config, self.config.project, None) + return SqlRegistry(registry_config, self.config.project, None) elif registry_config.registry_type == "snowflake.registry": from feast.infra.registry.snowflake import SnowflakeRegistry - self._registry = SnowflakeRegistry( - registry_config, self.config.project, None - ) + return SnowflakeRegistry(registry_config, self.config.project, None) elif registry_config and registry_config.registry_type == "remote": from feast.infra.registry.remote import RemoteRegistry - self._registry = RemoteRegistry( + return RemoteRegistry( registry_config, self.config.project, None, self.config.auth_config ) else: - self._registry = Registry( + return Registry( self.config.project, registry_config, repo_path=self.repo_path, auth_config=self.config.auth_config, ) - self._provider = get_provider(self.config) + @property + def project(self) -> str: + """Gets the project for the current request context, falling back to the configured project.""" + return self._current_project.get() or self.config.project - def __repr__(self) -> str: - return ( - f"FeatureStore(\n" - f" repo_path={self.repo_path!r},\n" - f" config={self.config!r},\n" - f" registry={self._registry!r},\n" - f" provider={self._provider!r}\n" - f")" - ) + def set_current_project(self, project: Optional[str]): + return self._current_project.set(project) - @property - def registry(self) -> BaseRegistry: - """Gets the registry of this feature store.""" - return self._registry + def reset_current_project(self, token): + self._current_project.reset(token) @property - def project(self) -> str: - """Gets the project of this feature store.""" - return self.config.project + def provider(self) -> Provider: + """Gets the provider of this feature store.""" + if self._provider is None: + self._provider = get_provider(self.config) + return self._provider def _get_provider(self) -> Provider: # TODO: Bake self.repo_path into self.config so that we dont only have one interface to paths - return self._provider + return self.provider + + def _rollback_fv_states( + self, + feature_views: list, + previous_states: dict, + ) -> None: + """Restore feature views to their pre-materialization states.""" + for fv in feature_views: + prev = previous_states.get(fv.name) + if ( + hasattr(fv, "state") + and prev is not None + and prev != FeatureViewState.STATE_UNSPECIFIED + ): + fv.state = prev + self.registry.apply_feature_view(fv, self.project, commit=True) + + def _transition_fv_to_materializing( + self, + feature_view, + already_transitioned: list, + previous_states: dict, + ) -> None: + """ + Transition a feature view to MATERIALIZING state. + + Rolls back all already-transitioned FVs if this one can't transition. + Already MATERIALIZING is a no-op (async server may have reserved the state + before returning 202); rollback target is GENERATED in that case. + """ + current = getattr(feature_view, "state", None) + if current == FeatureViewState.MATERIALIZING: + previous_states[feature_view.name] = FeatureViewState.GENERATED + return + + previous_states[feature_view.name] = current + if ( + hasattr(feature_view, "state") + and feature_view.state != FeatureViewState.STATE_UNSPECIFIED + ): + if not feature_view.state.can_transition_to(FeatureViewState.MATERIALIZING): + self._rollback_fv_states(already_transitioned, previous_states) + raise ValueError( + f"FeatureView {feature_view.name} cannot transition " + f"from {feature_view.state.name} to MATERIALIZING." + ) + feature_view.state = FeatureViewState.MATERIALIZING + self.registry.apply_feature_view(feature_view, self.project, commit=True) + + def _submit_and_process_materialization_jobs( + self, + provider, + tasks: list, + regular_fvs: list, + previous_states: dict, + date_range: "_MaterializationDateRange", + ) -> None: + """ + Submit all tasks to the engine in one call and process the results. + + For each returned job: record watermark on success, roll back state on + error. If the engine itself raises, all states are rolled back. + """ + from feast.infra.common.materialization_job import ( + MaterializationJobStatus, + ) + + batch_start = time.monotonic() + try: + jobs = provider.batch_engine.materialize(self.registry, tasks) + except Exception: + self._rollback_fv_states(regular_fvs, previous_states) + raise + + if len(jobs) != len(regular_fvs): + self._rollback_fv_states(regular_fvs, previous_states) + raise RuntimeError( + f"Engine returned {len(jobs)} jobs for {len(regular_fvs)} tasks" + ) + + first_error = None + succeeded_fvs = [] + failed_fvs = [] + + for fv, job in zip(regular_fvs, jobs): + fv_status = job.status() + + if fv_status == MaterializationJobStatus.ERROR: + failed_fvs.append(fv) + if first_error is None and job.error(): + first_error = job.error() + else: + succeeded_fvs.append(fv) + + if failed_fvs: + self._rollback_fv_states(failed_fvs, previous_states) + + # Engines that apply watermarks themselves (e.g. SparkApplication pod) + # must not get a second apply_materialization — that duplicates intervals. + if not getattr(provider.batch_engine, "applies_materialization", False): + for fv in succeeded_fvs: + self.registry.apply_materialization( + fv, + self.project, + date_range.fv_start_dates[fv.name], + date_range.end_date, + ) + + _tracker = _get_track_materialization() + if _tracker is not None: + elapsed = time.monotonic() - batch_start + for fv in succeeded_fvs: + _tracker(fv.name, True, elapsed) + for fv in failed_fvs: + _tracker(fv.name, False, elapsed) + + if first_error: + raise first_error + + def _materialize_fvs_batch( + self, + provider, + fv_with_dates: list, + end_date: datetime, + tqdm_builder, + disable_event_timestamp: bool = False, + ) -> None: + """Batch path: collect all FVs, submit to engine in one call. + + Only used when ``provider.batch_engine.supports_batch`` is True. + """ + from feast.infra.common.materialization_job import MaterializationTask + + tasks: list = [] + regular_fvs: list = [] + previous_states: dict = {} + date_range = _MaterializationDateRange(end_date=end_date) + + for feature_view, fv_start in fv_with_dates: + self._transition_fv_to_materializing( + feature_view, regular_fvs, previous_states + ) + regular_fvs.append(feature_view) + date_range.fv_start_dates[feature_view.name] = fv_start + tasks.append( + MaterializationTask( + project=self.project, + feature_view=feature_view, + start_time=fv_start, + end_time=end_date, + tqdm_builder=tqdm_builder, + disable_event_timestamp=disable_event_timestamp, + ) + ) + + if tasks: + self._submit_and_process_materialization_jobs( + provider, + tasks, + regular_fvs, + previous_states, + date_range, + ) + + @property + def openlineage_emitter(self) -> Optional[Any]: + """Gets the OpenLineage emitter of this feature store.""" + if self._openlineage_emitter is None: + self._openlineage_emitter = self._init_openlineage_emitter() + return self._openlineage_emitter + + def _clear_feature_service_cache(self): + """Clear feature service cache to avoid stale data after registry refresh.""" + self._feature_service_cache.clear() + if hasattr(self.registry, "_feature_service_cache"): + getattr(self.registry, "_feature_service_cache").clear() + self._fs_name_cache.clear() + self._fs_name_index.clear() + self._fs_name_index_ts = -self._FS_NAME_INDEX_TTL_SECONDS def refresh_registry(self): """Fetches and caches a copy of the feature registry in memory. @@ -217,7 +677,8 @@ def refresh_registry(self): downloaded synchronously, which may increase latencies if the triggering method is get_online_features(). """ - self._registry.refresh(self.project) + self.registry.refresh(self.project) + self._clear_feature_service_cache() def list_entities( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None @@ -240,7 +701,7 @@ def _list_entities( hide_dummy_entity: bool = True, tags: Optional[dict[str, str]] = None, ) -> List[Entity]: - all_entities = self._registry.list_entities( + all_entities = self.registry.list_entities( self.project, allow_cache=allow_cache, tags=tags ) return [ @@ -261,7 +722,7 @@ def list_feature_services( Returns: A list of feature services. """ - return self._registry.list_feature_services(self.project, tags=tags) + return self.registry.list_feature_services(self.project, tags=tags) def _list_all_feature_views( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None @@ -308,7 +769,7 @@ def list_feature_views( A list of feature views. """ return utils._list_feature_views( - self._registry, self.project, allow_cache, tags=tags + self.registry, self.project, allow_cache, tags=tags ) def list_batch_feature_views( @@ -333,7 +794,7 @@ def _list_batch_feature_views( tags: Optional[dict[str, str]] = None, ) -> List[FeatureView]: feature_views = [] - for fv in self._registry.list_feature_views( + for fv in self.registry.list_feature_views( self.project, allow_cache=allow_cache, tags=tags ): if ( @@ -353,7 +814,7 @@ def _list_stream_feature_views( tags: Optional[dict[str, str]] = None, ) -> List[StreamFeatureView]: stream_feature_views = [] - for sfv in self._registry.list_stream_feature_views( + for sfv in self.registry.list_stream_feature_views( self.project, allow_cache=allow_cache, tags=tags ): if hide_dummy_entity and sfv.entities[0] == DUMMY_ENTITY_NAME: @@ -375,7 +836,7 @@ def list_on_demand_feature_views( Returns: A list of on demand feature views. """ - return self._registry.list_on_demand_feature_views( + return self.registry.list_on_demand_feature_views( self.project, allow_cache=allow_cache, tags=tags ) @@ -390,6 +851,43 @@ def list_stream_feature_views( """ return self._list_stream_feature_views(allow_cache, tags=tags) + def list_label_views( + self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + ) -> List[LabelView]: + """ + Retrieves the list of label views from the registry. + + Args: + allow_cache: Whether to allow returning label views from a cached registry. + tags: Filter by tags. + + Returns: + A list of label views. + """ + return self.registry.list_label_views( + self.project, allow_cache=allow_cache, tags=tags + ) + + def get_label_view( + self, name: str, allow_registry_cache: bool = False + ) -> LabelView: + """ + Retrieves a label view by name. + + Args: + name: Name of the label view. + allow_registry_cache: Whether to allow returning the label view from a cached registry. + + Returns: + The specified label view. + + Raises: + FeatureViewNotFoundException: The label view could not be found. + """ + return self.registry.get_label_view( + name, self.project, allow_cache=allow_registry_cache + ) + def list_data_sources( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None ) -> List[DataSource]: @@ -403,7 +901,7 @@ def list_data_sources( Returns: A list of data sources. """ - return self._registry.list_data_sources( + return self.registry.list_data_sources( self.project, allow_cache=allow_cache, tags=tags ) @@ -421,7 +919,7 @@ def get_entity(self, name: str, allow_registry_cache: bool = False) -> Entity: Raises: EntityNotFoundException: The entity could not be found. """ - return self._registry.get_entity( + return self.registry.get_entity( name, self.project, allow_cache=allow_registry_cache ) @@ -441,7 +939,7 @@ def get_feature_service( Raises: FeatureServiceNotFoundException: The feature service could not be found. """ - return self._registry.get_feature_service(name, self.project, allow_cache) + return self.registry.get_feature_service(name, self.project, allow_cache) def get_feature_view( self, name: str, allow_registry_cache: bool = False @@ -467,13 +965,25 @@ def _get_feature_view( hide_dummy_entity: bool = True, allow_registry_cache: bool = False, ) -> FeatureView: - feature_view = self._registry.get_feature_view( + feature_view = self.registry.get_feature_view( name, self.project, allow_cache=allow_registry_cache ) if hide_dummy_entity and feature_view.entities[0] == DUMMY_ENTITY_NAME: feature_view.entities = [] return feature_view + def list_feature_view_versions(self, name: str) -> List[Dict[str, Any]]: + """ + List version history for a feature view. + + Args: + name: Name of feature view. + + Returns: + List of version records. + """ + return self.registry.list_feature_view_versions(name, self.project) + def get_stream_feature_view( self, name: str, allow_registry_cache: bool = False ) -> StreamFeatureView: @@ -500,7 +1010,7 @@ def _get_stream_feature_view( hide_dummy_entity: bool = True, allow_registry_cache: bool = False, ) -> StreamFeatureView: - stream_feature_view = self._registry.get_stream_feature_view( + stream_feature_view = self.registry.get_stream_feature_view( name, self.project, allow_cache=allow_registry_cache ) if hide_dummy_entity and stream_feature_view.entities[0] == DUMMY_ENTITY_NAME: @@ -522,7 +1032,7 @@ def get_on_demand_feature_view( Raises: FeatureViewNotFoundException: The feature view could not be found. """ - return self._registry.get_on_demand_feature_view( + return self.registry.get_on_demand_feature_view( name, self.project, allow_cache=allow_registry_cache ) @@ -539,11 +1049,11 @@ def get_data_source(self, name: str) -> DataSource: Raises: DataSourceObjectNotFoundException: The data source could not be found. """ - return self._registry.get_data_source(name, self.project) + return self.registry.get_data_source(name, self.project) def delete_feature_view(self, name: str): """ - Deletes a feature view. + Deletes a feature view of any kind (FeatureView, OnDemandFeatureView, StreamFeatureView). Args: name: Name of feature view. @@ -551,7 +1061,45 @@ def delete_feature_view(self, name: str): Raises: FeatureViewNotFoundException: The feature view could not be found. """ - return self._registry.delete_feature_view(name, self.project) + return self.registry.delete_feature_view(name, self.project) + + def enable_feature_view(self, name: str): + """ + Enable a feature view for serving and materialization. + + Args: + name: Name of feature view. + """ + fv = self.registry.get_any_feature_view(name, self.project) + fv.enabled = True # type: ignore[attr-defined] + self.registry.apply_feature_view(fv, self.project) + + def disable_feature_view(self, name: str): + """ + Disable a feature view to prevent serving and materialization. + + Args: + name: Name of feature view. + """ + fv = self.registry.get_any_feature_view(name, self.project) + fv.enabled = False # type: ignore[attr-defined] + self.registry.apply_feature_view(fv, self.project) + + def set_feature_view_state(self, name: str, state: FeatureViewState): + """ + Set the lifecycle state of a feature view. + + Args: + name: Name of feature view. + state: Target state. + """ + fv = self.registry.get_any_feature_view(name, self.project) + if not fv.state.can_transition_to(state): # type: ignore[attr-defined] + raise ValueError( + f"Invalid state transition: {fv.state.name} -> {state.name}." # type: ignore[attr-defined] + ) + fv.state = state # type: ignore[attr-defined] + self.registry.apply_feature_view(fv, self.project) def delete_feature_service(self, name: str): """ @@ -563,7 +1111,7 @@ def delete_feature_service(self, name: str): Raises: FeatureServiceNotFoundException: The feature view could not be found. """ - return self._registry.delete_feature_service(name, self.project) + return self.registry.delete_feature_service(name, self.project) def _should_use_plan(self): """Returns True if plan and _apply_diffs should be used, False otherwise.""" @@ -577,6 +1125,7 @@ def _validate_all_feature_views( views_to_update: List[FeatureView], odfvs_to_update: List[OnDemandFeatureView], sfvs_to_update: List[StreamFeatureView], + lvs_to_update: Optional[List[LabelView]] = None, ): """Validates all feature views.""" if len(odfvs_to_update) > 0 and not flags_helper.is_test(): @@ -590,6 +1139,7 @@ def _validate_all_feature_views( *views_to_update, *odfvs_to_update, *sfvs_to_update, + *(lvs_to_update or []), ] ) @@ -601,18 +1151,36 @@ def _make_inferences( odfvs_to_update: List[OnDemandFeatureView], sfvs_to_update: List[StreamFeatureView], feature_services_to_update: List[FeatureService], + lvs_to_update: Optional[List[LabelView]] = None, ): """Makes inferences for entities, feature views, odfvs, and feature services.""" + lvs_to_update = lvs_to_update or [] + update_data_sources_with_inferred_event_timestamp_col( data_sources_to_update, self.config ) update_data_sources_with_inferred_event_timestamp_col( - [view.batch_source for view in views_to_update], self.config + [ + view.batch_source + for view in views_to_update + if view.batch_source is not None + ], + self.config, ) update_data_sources_with_inferred_event_timestamp_col( - [view.batch_source for view in sfvs_to_update], self.config + [ + view.batch_source + for view in sfvs_to_update + if view.batch_source is not None + ], + self.config, + ) + + update_data_sources_with_inferred_event_timestamp_col( + [lv.batch_source for lv in lvs_to_update if lv.batch_source is not None], + self.config, ) # New feature views may reference previously applied entities. @@ -648,25 +1216,63 @@ def _make_inferences( odfvs_to_write = [ odfv for odfv in odfvs_to_update if odfv.write_to_online_store ] - # Update to include ODFVs with write to online store fvs_to_update_map = { view.name: view - for view in [*views_to_update, *sfvs_to_update, *odfvs_to_write] + for view in [ + *views_to_update, + *sfvs_to_update, + *odfvs_to_write, + *lvs_to_update, + ] } for feature_service in feature_services_to_update: feature_service.infer_features(fvs_to_update=fvs_to_update_map) + def _validate_materialize_version( + self, + version: Optional[str], + feature_views: Optional[List[str]], + ) -> Optional[int]: + """Validate and parse the version parameter for materialize calls. + + Returns the parsed version number, or None if no version was specified. + """ + if version is None: + return None + + if not feature_views or len(feature_views) != 1: + raise ValueError( + "--version requires --views with exactly one feature view." + ) + + if not self.config.registry.enable_online_feature_view_versioning: + raise ValueError( + "Version-aware materialization requires " + "'enable_online_feature_view_versioning: true' under 'registry' " + "in feature_store.yaml." + ) + + is_latest, version_number = parse_version(version) + if is_latest: + return None + return version_number + def _get_feature_views_to_materialize( self, feature_views: Optional[List[str]], + version: Optional[int] = None, ) -> List[Union[FeatureView, OnDemandFeatureView]]: """ Returns the list of feature views that should be materialized. If no feature views are specified, all feature views will be returned. + LabelViews are excluded because they receive data via ``push()`` and + are not supported by the batch materialization providers. Args: feature_views: List of names of feature views to materialize. + version: If set, load this specific version number from the registry + instead of the active definition. Requires exactly one feature view name. Raises: FeatureViewNotFoundException: One of the specified feature views could not be found. @@ -676,42 +1282,80 @@ def _get_feature_views_to_materialize( if feature_views is None: regular_feature_views = utils._list_feature_views( - self._registry, self.project, hide_dummy_entity=False + self.registry, self.project, hide_dummy_entity=False ) feature_views_to_materialize.extend( - [fv for fv in regular_feature_views if fv.online] + [fv for fv in regular_feature_views if fv.online and fv.enabled] ) stream_feature_views_to_materialize = self._list_stream_feature_views( hide_dummy_entity=False ) feature_views_to_materialize.extend( - [sfv for sfv in stream_feature_views_to_materialize if sfv.online] + [ + sfv + for sfv in stream_feature_views_to_materialize + if sfv.online and sfv.enabled + ] ) on_demand_feature_views_to_materialize = self.list_on_demand_feature_views() feature_views_to_materialize.extend( [ odfv for odfv in on_demand_feature_views_to_materialize - if odfv.write_to_online_store + if odfv.write_to_online_store and odfv.enabled ] ) else: for name in feature_views: feature_view: Union[FeatureView, OnDemandFeatureView] - try: - feature_view = self._get_feature_view(name, hide_dummy_entity=False) - except FeatureViewNotFoundException: + if version is not None: + feature_view = cast( + Union[FeatureView, OnDemandFeatureView], + self.registry.get_feature_view_by_version( + name, self.project, version + ), + ) + else: try: - feature_view = self._get_stream_feature_view( + feature_view = self._get_feature_view( name, hide_dummy_entity=False ) except FeatureViewNotFoundException: - feature_view = self.get_on_demand_feature_view(name) + try: + feature_view = self._get_stream_feature_view( + name, hide_dummy_entity=False + ) + except FeatureViewNotFoundException: + try: + feature_view = self.get_on_demand_feature_view(name) + except FeatureViewNotFoundException: + try: + label_view = self.registry.get_label_view( + name, self.project + ) + raise ValueError( + f"LabelView {label_view.name} cannot be materialized via " + f"materialize(). Use FeatureStore.push() to write labels." + ) + except FeatureViewNotFoundException: + raise + + if isinstance(feature_view, LabelView): + raise ValueError( + f"LabelView {feature_view.name} cannot be materialized via " + f"materialize(). Use FeatureStore.push() to write labels." + ) - if hasattr(feature_view, "online") and not feature_view.online: + if hasattr(feature_view, "enabled") and not feature_view.enabled: raise ValueError( - f"FeatureView {feature_view.name} is not configured to be served online." + f"FeatureView {feature_view.name} is disabled. " + f"Enable it before materializing." ) + if hasattr(feature_view, "online") and not feature_view.online: + if not getattr(feature_view, "offline", False): + raise ValueError( + f"FeatureView {feature_view.name} is not configured to be served online." + ) elif ( hasattr(feature_view, "write_to_online_store") and not feature_view.write_to_online_store @@ -724,7 +1368,10 @@ def _get_feature_views_to_materialize( return feature_views_to_materialize def plan( - self, desired_repo_contents: RepoContents + self, + desired_repo_contents: RepoContents, + skip_feature_view_validation: bool = False, + progress_ctx: Optional["ApplyProgressContext"] = None, ) -> Tuple[RegistryDiff, InfraDiff, Infra]: """Dry-run registering objects to metadata store. @@ -734,6 +1381,8 @@ def plan( Args: desired_repo_contents: The desired repo state. + skip_feature_view_validation: If True, skip validation of feature views. This can be useful when the validation + system is being overly strict. Use with caution and report any issues on GitHub. Default is False. Raises: ValueError: The 'objects' parameter could not be parsed properly. @@ -763,16 +1412,19 @@ def plan( ... feature_views=[driver_hourly_stats_view], ... on_demand_feature_views=list(), ... stream_feature_views=list(), + ... label_views=list(), ... entities=[driver], ... feature_services=list(), ... permissions=list())) # register entity and feature view """ # Validate and run inference on all the objects to be registered. - self._validate_all_feature_views( - desired_repo_contents.feature_views, - desired_repo_contents.on_demand_feature_views, - desired_repo_contents.stream_feature_views, - ) + if not skip_feature_view_validation: + self._validate_all_feature_views( + desired_repo_contents.feature_views, + desired_repo_contents.on_demand_feature_views, + desired_repo_contents.stream_feature_views, + desired_repo_contents.label_views, + ) _validate_data_sources(desired_repo_contents.data_sources) self._make_inferences( desired_repo_contents.data_sources, @@ -781,28 +1433,37 @@ def plan( desired_repo_contents.on_demand_feature_views, desired_repo_contents.stream_feature_views, desired_repo_contents.feature_services, + desired_repo_contents.label_views, ) # Compute the desired difference between the current objects in the registry and # the desired repo state. - registry_diff = diff_between( - self._registry, self.project, desired_repo_contents - ) + registry_diff = diff_between(self.registry, self.project, desired_repo_contents) + + if progress_ctx: + progress_ctx.update_phase_progress("Computing infrastructure diff") # Compute the desired difference between the current infra, as stored in the registry, # and the desired infra. - self._registry.refresh(project=self.project) - current_infra_proto = InfraProto() - current_infra_proto.CopyFrom(self._registry.proto().infra) + self.registry.refresh(project=self.project) + self._clear_feature_service_cache() + current_infra_proto = self.registry.get_infra(self.project).to_proto() desired_registry_proto = desired_repo_contents.to_registry_proto() - new_infra = self._provider.plan_infra(self.config, desired_registry_proto) + new_infra = self.provider.plan_infra(self.config, desired_registry_proto) new_infra_proto = new_infra.to_proto() - infra_diff = diff_infra_protos(current_infra_proto, new_infra_proto) + infra_diff = diff_infra_protos( + current_infra_proto, new_infra_proto, project=self.project + ) return registry_diff, infra_diff, new_infra def _apply_diffs( - self, registry_diff: RegistryDiff, infra_diff: InfraDiff, new_infra: Infra + self, + registry_diff: RegistryDiff, + infra_diff: InfraDiff, + new_infra: Infra, + progress_ctx: Optional["ApplyProgressContext"] = None, + no_promote: bool = False, ): """Applies the given diffs to the metadata store and infrastructure. @@ -810,13 +1471,87 @@ def _apply_diffs( registry_diff: The diff between the current registry and the desired registry. infra_diff: The diff between the current infra and the desired infra. new_infra: The desired infra. + progress_ctx: Optional progress context for tracking apply progress. """ - infra_diff.update() - apply_diff_to_registry( - self._registry, registry_diff, self.project, commit=False - ) + try: + # Infrastructure phase + if progress_ctx: + infra_ops_count = len(infra_diff.infra_object_diffs) + progress_ctx.start_phase("Updating infrastructure", infra_ops_count) + + infra_diff.update(progress_ctx=progress_ctx) + + if progress_ctx: + progress_ctx.complete_phase() + progress_ctx.start_phase("Updating registry", 2) + + # Registry phase + apply_diff_to_registry( + self.registry, + registry_diff, + self.project, + commit=False, + no_promote=no_promote, + ) + + if progress_ctx: + progress_ctx.update_phase_progress("Committing registry changes") + + self.registry.update_infra(new_infra, self.project, commit=True) + + if progress_ctx: + progress_ctx.update_phase_progress("Registry update complete") + progress_ctx.complete_phase() + finally: + # Always cleanup progress bars + if progress_ctx: + progress_ctx.cleanup() + + # Emit OpenLineage events for applied objects + self._emit_openlineage_apply_diffs(registry_diff) + + # Emit MLflow events for applied objects (Phase 7) + self._mlflow_log_apply_diffs(registry_diff) + + def _mlflow_log_apply_diffs(self, registry_diff: RegistryDiff): + """Log apply operation to MLflow ops experiment.""" + try: + if self.mlflow is None or not self.config.mlflow.log_operations: + return + from feast.diff.property_diff import TransitionType + + objects: List[Any] = [] + transition_types: Dict[str, str] = {} + for feast_object_diff in registry_diff.feast_object_diffs: + obj = ( + feast_object_diff.new_feast_object + or feast_object_diff.current_feast_object + ) + if obj is None: + continue + tt = feast_object_diff.transition_type + if tt == TransitionType.UNCHANGED: + continue + objects.append(obj) + transition_types[feast_object_diff.name] = tt.name + if objects: + self._mlflow_log_apply(objects, transition_types=transition_types) + except Exception as e: + _logger.debug("MLflow apply logging failed: %s", e) + + def _emit_openlineage_apply_diffs(self, registry_diff: RegistryDiff): + """Emit OpenLineage events for objects applied via diffs.""" + if self.openlineage_emitter is None: + return + + # Collect all objects that were added or updated + objects: List[Any] = [] + for feast_object_diff in registry_diff.feast_object_diffs: + if feast_object_diff.new_feast_object is not None: + objects.append(feast_object_diff.new_feast_object) - self._registry.update_infra(new_infra, self.project, commit=True) + if objects: + self._emit_openlineage_apply(objects) def apply( self, @@ -828,6 +1563,7 @@ def apply( OnDemandFeatureView, BatchFeatureView, StreamFeatureView, + LabelView, FeatureService, ValidationReference, Permission, @@ -835,6 +1571,8 @@ def apply( ], objects_to_delete: Optional[List[FeastObject]] = None, partial: bool = True, + skip_feature_view_validation: bool = False, + no_promote: bool = False, ): """Register objects to metadata store and update related infrastructure. @@ -843,12 +1581,18 @@ def apply( an online store), it will commit the updated registry. All operations are idempotent, meaning they can safely be rerun. + Note: The apply method does NOT delete objects that are removed from the provided list. To delete objects + from the registry, use explicit delete methods like delete_feature_view(), delete_feature_service(), or + pass objects to the objects_to_delete parameter with partial=False. + Args: objects: A single object, or a list of objects that should be registered with the Feature Store. objects_to_delete: A list of objects to be deleted from the registry and removed from the provider's infrastructure. This deletion will only be performed if partial is set to False. partial: If True, apply will only handle the specified objects; if False, apply will also delete all the objects in objects_to_delete, and tear down any associated cloud resources. + skip_feature_view_validation: If True, skip validation of feature views. This can be useful when the validation + system is being overly strict. Use with caution and report any issues on GitHub. Default is False. Raises: ValueError: The 'objects' parameter could not be parsed properly. @@ -895,6 +1639,7 @@ def apply( ) ] sfvs_to_update = [ob for ob in objects if isinstance(ob, StreamFeatureView)] + lvs_to_update = [ob for ob in objects if isinstance(ob, LabelView)] odfvs_to_update = [ob for ob in objects if isinstance(ob, OnDemandFeatureView)] odfvs_with_writes_to_update = [ ob @@ -940,6 +1685,12 @@ def apply( else: pass + for lv in lvs_to_update: + if lv.source is not None: + data_sources_set_to_update.add(lv.source) + if isinstance(lv.source, PushSource) and lv.source.batch_source: + data_sources_set_to_update.add(lv.source.batch_source) + for odfv in odfvs_to_update: for v in odfv.source_request_sources.values(): data_sources_set_to_update.add(v) @@ -950,11 +1701,13 @@ def apply( entities_to_update.append(DUMMY_ENTITY) # Validate all feature views and make inferences. - self._validate_all_feature_views( - views_to_update, - odfvs_to_update, - sfvs_to_update, - ) + if not skip_feature_view_validation: + self._validate_all_feature_views( + views_to_update, + odfvs_to_update, + sfvs_to_update, + lvs_to_update, + ) self._make_inferences( data_sources_to_update, entities_to_update, @@ -962,27 +1715,33 @@ def apply( odfvs_to_update, sfvs_to_update, services_to_update, + lvs_to_update, ) # Add all objects to the registry and update the provider's infrastructure. for project in projects_to_update: - self._registry.apply_project(project, commit=False) + self.registry.apply_project(project, commit=False) for ds in data_sources_to_update: - self._registry.apply_data_source(ds, project=self.project, commit=False) - for view in itertools.chain(views_to_update, odfvs_to_update, sfvs_to_update): - self._registry.apply_feature_view(view, project=self.project, commit=False) + self.registry.apply_data_source(ds, project=self.project, commit=False) + for view in itertools.chain( + views_to_update, odfvs_to_update, sfvs_to_update, lvs_to_update + ): + self.registry.apply_feature_view( + view, project=self.project, commit=False, no_promote=no_promote + ) for ent in entities_to_update: - self._registry.apply_entity(ent, project=self.project, commit=False) + self.registry.apply_entity(ent, project=self.project, commit=False) + for feature_service in services_to_update: - self._registry.apply_feature_service( + self.registry.apply_feature_service( feature_service, project=self.project, commit=False ) for validation_references in validation_references_to_update: - self._registry.apply_validation_reference( + self.registry.apply_validation_reference( validation_references, project=self.project, commit=False ) for permission in permissions_to_update: - self._registry.apply_permission( + self.registry.apply_permission( permission, project=self.project, commit=False ) @@ -1021,46 +1780,60 @@ def apply( permissions_to_delete = [ ob for ob in objects_to_delete if isinstance(ob, Permission) ] + lvs_to_delete = [ + ob for ob in objects_to_delete if isinstance(ob, LabelView) + ] for data_source in data_sources_to_delete: - self._registry.delete_data_source( + self.registry.delete_data_source( data_source.name, project=self.project, commit=False ) for entity in entities_to_delete: - self._registry.delete_entity( + self.registry.delete_entity( entity.name, project=self.project, commit=False ) for view in views_to_delete: - self._registry.delete_feature_view( + self.registry.delete_feature_view( view.name, project=self.project, commit=False ) for odfv in odfvs_to_delete: - self._registry.delete_feature_view( + self.registry.delete_feature_view( odfv.name, project=self.project, commit=False ) for sfv in sfvs_to_delete: - self._registry.delete_feature_view( + self.registry.delete_feature_view( sfv.name, project=self.project, commit=False ) + for lv in lvs_to_delete: + self.registry.delete_feature_view( + lv.name, project=self.project, commit=False + ) for service in services_to_delete: - self._registry.delete_feature_service( + self.registry.delete_feature_service( service.name, project=self.project, commit=False ) for validation_references in validation_references_to_delete: - self._registry.delete_validation_reference( + self.registry.delete_validation_reference( validation_references.name, project=self.project, commit=False ) for permission in permissions_to_delete: - self._registry.delete_permission( + self.registry.delete_permission( permission.name, project=self.project, commit=False ) tables_to_delete: List[FeatureView] = ( - views_to_delete + sfvs_to_delete if not partial else [] # type: ignore + views_to_delete + sfvs_to_delete + lvs_to_delete # type: ignore + if not partial + else [] ) tables_to_keep: List[ - Union[FeatureView, StreamFeatureView, OnDemandFeatureView] - ] = views_to_update + sfvs_to_update + odfvs_with_writes_to_update # type: ignore + Union[FeatureView, StreamFeatureView, OnDemandFeatureView, LabelView] + ] = ( + views_to_update + + sfvs_to_update + + odfvs_with_writes_to_update + + lvs_to_update + ) # type: ignore self._get_provider().update_infra( project=self.project, @@ -1071,7 +1844,7 @@ def apply( partial=partial, ) - self._registry.commit() + self.registry.commit() # Refresh the registry cache to ensure that changes are immediately visible # This is especially important for UI and other clients that may be reading @@ -1084,17 +1857,87 @@ def apply( if self.config.registry.cache_mode == "sync": self.refresh_registry() + # Emit OpenLineage events for applied objects + self._emit_openlineage_apply(objects) + + # Emit MLflow events for applied objects (Phase 7) + self._mlflow_log_apply(objects) + + def _mlflow_log_apply( + self, + objects: List[Any], + transition_types: Optional[Dict[str, str]] = None, + ): + """Log applied objects to MLflow ops experiment.""" + try: + if self.mlflow is None or not self.config.mlflow.log_operations: + return + self.mlflow.log_apply( + changed_objects=objects, + transition_types=transition_types, + ) + except Exception as e: + _logger.debug("MLflow apply logging failed: %s", e) + + def _emit_openlineage_apply(self, objects: List[Any]): + """Emit OpenLineage events for applied objects.""" + if self.openlineage_emitter is None: + return + try: + self.openlineage_emitter.emit_apply(objects, self.project) + except Exception as e: + warnings.warn(f"Failed to emit OpenLineage apply events: {e}") + def teardown(self): """Tears down all local and cloud resources for the feature store.""" - tables: List[FeatureView] = [] - feature_views = self.list_feature_views() + from feast.constants import PROTECTED_PROJECT_TAG + + # Prevent teardown of protected projects + try: + current = self.registry.get_project(name=self.project, allow_cache=False) + if current and current.tags.get(PROTECTED_PROJECT_TAG) == "true": + raise ValueError( + f'Teardown is not allowed on protected project "{self.project}". ' + "Protected projects are managed externally and cannot be torn down via Feast." + ) + except ValueError: + raise + except Exception: + pass - tables.extend(feature_views) + tables: List[BaseFeatureView] = [] + tables.extend(self.list_feature_views()) + tables.extend(self.list_label_views()) entities = self.list_entities() - self._get_provider().teardown_infra(self.project, tables, entities) - self._registry.teardown() + self._get_provider().teardown_infra(self.project, tables, entities) # type: ignore[arg-type] + + for project in self.list_projects(): + self.registry.delete_project(project.name) + + self._teardown_openlineage() + + def _teardown_openlineage(self): + """Clean up OpenLineage data for this project's namespace during teardown.""" + try: + if ( + hasattr(self.config, "openlineage") + and self.config.openlineage is not None + and self.config.openlineage.enabled + ): + ol_config = self.config.openlineage.to_openlineage_config() + consumer_cfg = getattr(ol_config, "consumer", None) + if consumer_cfg and getattr(consumer_cfg, "enabled", False): + conn_str = getattr(consumer_cfg, "connection_string", None) + if conn_str: + from feast.openlineage.store import OpenLineageStore + + ol_store = OpenLineageStore(connection_string=conn_str) + namespace = f"{self.project}/{self.project}" + ol_store.purge_namespace(namespace) + except Exception as e: + warnings.warn(f"Failed to clean up OpenLineage data during teardown: {e}") def get_historical_features( self, @@ -1103,6 +1946,7 @@ def get_historical_features( full_feature_names: bool = False, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, + filter_by_created_timestamp: bool = False, ) -> RetrievalJob: """Enrich an entity dataframe with historical feature values for either training or batch scoring. @@ -1134,6 +1978,11 @@ def get_historical_features( Required when entity_df is not provided. end_date (Optional[datetime]): End date for the timestamp range when retrieving features without entity_df. Required when entity_df is not provided. By default, the current time is used. + filter_by_created_timestamp (bool): If True, exclude feature values whose created timestamp + (the batch source's ``created_timestamp_column``) is later than the entity row's event + timestamp, so retrieval only reflects what was known at the event time and backfilled + values cannot leak into training data. Feature views without a + ``created_timestamp_column`` are unaffected. Defaults to False. Returns: RetrievalJob which can be used to materialize the results. @@ -1175,15 +2024,28 @@ def get_historical_features( if entity_df is None and end_date is None: end_date = datetime.now() - _feature_refs = utils._get_features(self._registry, self.project, features) + _feature_refs = utils._get_features( + self.registry, self.project, features, allow_cache=True + ) ( all_feature_views, all_on_demand_feature_views, - ) = utils._get_feature_views_to_use(self._registry, self.project, features) + ) = utils._get_feature_views_to_use(self.registry, self.project, features) # TODO(achal): _group_feature_refs returns the on demand feature views, but it's not passed into the provider. # This is a weird interface quirk - we should revisit the `get_historical_features` to # pass in the on demand feature views as well. + + # Deliberately disable writing to online store for ODFVs during historical retrieval + # since it's not applicable in this context. + # This does not change the output, since it forces to recompute ODFVs on historical retrieval + # but that is fine, since ODFVs precompute does not to work for historical retrieval (as per docs), only for online retrieval + # Copy to avoid side effects outside of this method + all_on_demand_feature_views = copy.deepcopy(all_on_demand_feature_views) + + for odfv in all_on_demand_feature_views: + odfv.write_to_online_store = False + fvs, odfvs = utils._group_feature_refs( _feature_refs, all_feature_views, @@ -1192,6 +2054,19 @@ def get_historical_features( feature_views = list(view for view, _ in fvs) on_demand_feature_views = list(view for view, _ in odfvs) + # ODFV source FV dependencies (e.g. driver_stats:conv_rate) are resolved + # by _group_feature_refs and included in `fvs`, but not in _feature_refs. + # Offline stores use feature_refs to map which features to fetch from each + # FV, so we must include these implicit dependency refs. + _feature_refs_for_provider = list(_feature_refs) + existing_refs = set(_feature_refs) + for view, feats in fvs: + for feat in feats: + ref = f"{view.projection.name_to_use()}:{feat}" + if ref not in existing_refs: + _feature_refs_for_provider.append(ref) + existing_refs.add(ref) + # Check that the right request data is present in the entity_df if type(entity_df) == pd.DataFrame: if self.config.coerce_tz_aware: @@ -1209,23 +2084,61 @@ def get_historical_features( provider = self._get_provider() # Optional kwargs - kwargs = {} + kwargs: Dict[str, Any] = {} if start_date is not None: kwargs["start_date"] = start_date if end_date is not None: kwargs["end_date"] = end_date + if filter_by_created_timestamp: + kwargs["filter_by_created_timestamp"] = filter_by_created_timestamp + + _retrieval_start = time.monotonic() job = provider.get_historical_features( self.config, feature_views, - _feature_refs, + _feature_refs_for_provider, entity_df, - self._registry, + self.registry, self.project, full_feature_names, **kwargs, ) + # Auto-log to MLflow if configured + try: + if self.mlflow is not None and self.config.mlflow.auto_log: + _duration = time.monotonic() - _retrieval_start + if isinstance(entity_df, pd.DataFrame): + _entity_count = len(entity_df) + elif isinstance(entity_df, str): + _entity_count = -1 + else: + _entity_count = 0 + _fs = features if isinstance(features, FeatureService) else None + _fs_name = ( + features.name + if isinstance(features, FeatureService) + else self._resolve_feature_service_name(_feature_refs) + ) + self.mlflow.log_feature_retrieval( + feature_refs=_feature_refs, + entity_count=_entity_count, + duration_seconds=_duration, + retrieval_type="historical", + feature_service=_fs, + feature_service_name=_fs_name, + ) + + self._log_entity_df_metadata( + entity_df, start_date=start_date, end_date=end_date + ) + + if self.config.mlflow.auto_log_entity_df: + self._log_entity_df_artifact(entity_df) + except Exception as e: + _logger.debug("MLflow auto-log failed for historical retrieval: %s", e) + return job def create_saved_dataset( @@ -1271,10 +2184,32 @@ def create_saved_dataset( f"The RetrievalJob {type(from_)} must implement the metadata property." ) + # Derive actual entity join keys from feature views rather than using + # all metadata keys (which include ODFV request-data inputs). + entity_join_keys: List[str] = [] + try: + all_fv_join_keys: set = set() + for feat_ref in from_.metadata.features: + fv_name = feat_ref.split(":")[0] + try: + fv = self.get_feature_view(fv_name) + for jk in fv.join_keys: + all_fv_join_keys.add(jk) + except Exception: + pass + if all_fv_join_keys: + entity_join_keys = [ + k for k in from_.metadata.keys if k in all_fv_join_keys + ] + else: + entity_join_keys = list(from_.metadata.keys) + except Exception: + entity_join_keys = list(from_.metadata.keys) + dataset = SavedDataset( name=name, features=from_.metadata.features, - join_keys=from_.metadata.keys, + join_keys=entity_join_keys, full_feature_names=from_.full_feature_names, storage=storage, tags=tags, @@ -1292,7 +2227,7 @@ def create_saved_dataset( ) ) - self._registry.apply_saved_dataset(dataset, self.project, commit=True) + self.registry.apply_saved_dataset(dataset, self.project, commit=True) return dataset def get_saved_dataset(self, name: str) -> SavedDataset: @@ -1318,7 +2253,7 @@ def get_saved_dataset(self, name: str) -> SavedDataset: RuntimeWarning, ) - dataset = self._registry.get_saved_dataset(name, self.project) + dataset = self.registry.get_saved_dataset(name, self.project) provider = self._get_provider() retrieval_job = provider.retrieve_saved_dataset( @@ -1326,11 +2261,81 @@ def get_saved_dataset(self, name: str) -> SavedDataset: ) return dataset.with_retrieval_job(retrieval_job) + def create_dataset_from_retrieval( + self, + name: str, + entity_df: "pd.DataFrame", + features: Union[List[str], "FeatureService"], + storage: "SavedDatasetStorage", + tags: Optional[Dict[str, str]] = None, + allow_overwrite: bool = False, + ) -> "SavedDataset": + """Run historical retrieval and persist the result as a saved dataset. + + This is a convenience method that combines get_historical_features and + create_saved_dataset into a single call. + + Args: + name: Name for the saved dataset (must be unique within project). + entity_df: DataFrame with entity columns and event_timestamp. + features: Feature references or a FeatureService. + storage: Storage backend to persist the dataset to. + tags: Optional key-value metadata. + allow_overwrite: Whether to overwrite existing data at storage path. + + Returns: + The created SavedDataset with retrieval job attached. + """ + retrieval_job = self.get_historical_features( + entity_df=entity_df, features=features + ) + return self.create_saved_dataset( + from_=retrieval_job, + name=name, + storage=storage, + tags=tags, + allow_overwrite=allow_overwrite, + ) + + def retrieve_dataset_data( + self, + name: str, + limit: int = 10, + ) -> "pd.DataFrame": + """Retrieve preview data from a saved dataset's storage. + + Args: + name: Name of the saved dataset in the registry. + limit: Maximum number of rows to return. + + Returns: + pandas DataFrame with up to `limit` rows from the dataset. + + Raises: + SavedDatasetNotFound: If the dataset doesn't exist. + ValueError: If data cannot be retrieved from storage. + """ + dataset = self.registry.get_saved_dataset(name, self.project) + provider = self._get_provider() + + try: + retrieval_job = provider.retrieve_saved_dataset( + config=self.config, dataset=dataset + ) + df = retrieval_job.to_df() + except Exception as e: + raise ValueError(f"Unable to load preview for dataset '{name}': {e}") from e + + if df.empty: + return df + return df.head(limit) + def _materialize_odfv( self, feature_view: OnDemandFeatureView, start_date: datetime, end_date: datetime, + full_feature_names: bool, ): """Helper to materialize a single OnDemandFeatureView.""" if not feature_view.source_feature_view_projections: @@ -1350,7 +2355,7 @@ def _materialize_odfv( } for source_fv in source_fvs: - all_join_keys.update(source_fv.entities) + all_join_keys.update(source_fv.join_keys) if source_fv.batch_source: entity_timestamp_col_names.add(source_fv.batch_source.timestamp_field) @@ -1390,7 +2395,7 @@ def _materialize_odfv( job = provider.offline_store.pull_latest_from_table_or_query( config=self.config, data_source=source_fv.batch_source, - join_key_columns=source_fv.entities, + join_key_columns=source_fv.join_keys, feature_name_columns=[f.name for f in source_fv.features], timestamp_field=source_fv.batch_source.timestamp_field, created_timestamp_column=getattr( @@ -1428,6 +2433,7 @@ def _materialize_odfv( retrieval_job = self.get_historical_features( entity_df=entity_df, features=source_features_from_projections, + full_feature_names=full_feature_names, ) input_df = retrieval_job.to_df() transformed_df = self._transform_on_demand_feature_view_df( @@ -1435,10 +2441,116 @@ def _materialize_odfv( ) self.write_to_online_store(feature_view.name, df=transformed_df) + def _get_remote_materialize_url(self) -> str: + """Get the feature server URL from online_store.path for remote materialization.""" + online_cfg = self.config.online_store + url = getattr(online_cfg, "path", None) + if not url: + raise ValueError( + "online_store.path must be set to use remote materialization. " + "Configure online_store with type: remote and a valid path." + ) + return url.rstrip("/") + + def _get_remote_http_session(self): + """Get an HTTP session with auth configured for the feature server.""" + import requests + + auth_config = getattr(self.config, "auth_config", None) + if auth_config and getattr(auth_config, "type", "no_auth") != "no_auth": + from feast.permissions.client.http_auth_requests_wrapper import ( + get_http_auth_requests_session, + ) + + return get_http_auth_requests_session(auth_config) + + return requests.Session() + + def _is_remote_topology(self) -> bool: + """True when this client talks to a remote feature server for online ops.""" + return getattr(self.config.online_store, "type", None) == "remote" + + def _post_to_feature_server( + self, + endpoint: str, + payload: Dict[str, Any], + query_params: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """POST JSON to the feature server; raise on 409 / 4xx / 5xx.""" + url = f"{self._get_remote_materialize_url()}{endpoint}" + session = self._get_remote_http_session() + cert = getattr(self.config.online_store, "cert", "") or "" + verify: Any = cert if cert else True + try: + response = session.post( + url, + json=payload, + params=query_params or {}, + verify=verify, + ) + if response.status_code == 409: + try: + detail = response.json() + message = detail.get("error", response.text) + except Exception: + message = response.text + raise RuntimeError( + f"Remote materialization conflict (409): {message}" + ) from None + if response.status_code >= 400: + raise RuntimeError( + f"Remote materialization failed " + f"({response.status_code}): {response.text}" + ) + if not response.content: + return {} + try: + return response.json() + except Exception: + return {"status": "accepted", "raw": response.text} + finally: + session.close() + + def _delegate_remote_materialize( + self, + endpoint: str, + payload: Dict[str, Any], + force: bool = False, + run_async: bool = False, + ) -> None: + """POST materialize to the feature server. + + When run_async=False (default), omits async and blocks until the server + finishes synchronous materialization (HTTP response). + When run_async=True, sends ?async=true and returns after 202. + force=True is only valid with run_async=True (server force applies to async). + """ + if force and not run_async: + raise ValueError( + "force=True requires run_async=True. " + "force only overrides stuck MATERIALIZING on the async path." + ) + + query_params: Dict[str, str] = {} + if run_async: + query_params["async"] = "true" + if force: + query_params["force"] = "true" + + result = self._post_to_feature_server(endpoint, payload, query_params or None) + if run_async: + _logger.info("Remote materialization accepted (%s): %s", endpoint, result) + else: + _logger.info("Remote materialization completed (%s): %s", endpoint, result) + def materialize_incremental( self, end_date: datetime, feature_views: Optional[List[str]] = None, + full_feature_names: bool = False, + version: Optional[str] = None, + force: bool = False, + run_async: bool = False, ) -> None: """ Materialize incremental new data from the offline store into the online store. @@ -1453,6 +2565,15 @@ def materialize_incremental( end_date (datetime): End date for time range of data to materialize into the online store feature_views (List[str]): Optional list of feature view names. If selected, will only run materialization for the specified feature views. + full_feature_names (bool): If True, feature names will be prefixed with the corresponding + feature view name. + version (str): Optional version to materialize (e.g., 'v2'). Requires feature_views + with exactly one entry and enable_online_feature_view_versioning to be enabled. + force (bool): When using remote topology with run_async=True, pass force=true to + override stuck MATERIALIZING state on the feature server. Ignored for local topology. + run_async (bool): When using remote topology, if False (default) POST without async and + block until the server finishes sync materialization. If True, POST with ?async=true + and return after 202. Ignored for local topology. Raises: Exception: A feature view being materialized does not have a TTL set. @@ -1468,8 +2589,25 @@ def materialize_incremental( ... """ + if self._is_remote_topology(): + payload: Dict[str, Any] = { + "end_ts": end_date.isoformat(), + "feature_views": feature_views, + "full_feature_names": full_feature_names, + } + if version is not None: + payload["version"] = version + self._delegate_remote_materialize( + "/materialize-incremental", + payload, + force=force, + run_async=run_async, + ) + return + + parsed_version = self._validate_materialize_version(version, feature_views) feature_views_to_materialize = self._get_feature_views_to_materialize( - feature_views + feature_views, version=parsed_version ) _print_materialization_log( None, @@ -1477,83 +2615,186 @@ def materialize_incremental( len(feature_views_to_materialize), self.config.online_store.type, ) - # TODO paging large loads - for feature_view in feature_views_to_materialize: - if isinstance(feature_view, OnDemandFeatureView): - if feature_view.write_to_online_store: - source_fvs = { - self._get_feature_view(p.name) - for p in feature_view.source_feature_view_projections.values() - } - max_ttl = timedelta(0) - for fv in source_fvs: - if fv.ttl and fv.ttl > max_ttl: - max_ttl = fv.ttl - - if max_ttl.total_seconds() > 0: - odfv_start_date = end_date - max_ttl - else: - odfv_start_date = end_date - timedelta(weeks=52) - print( - f"{Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}:" - ) - self._materialize_odfv(feature_view, odfv_start_date, end_date) - continue + # Emit OpenLineage START event for incremental materialization + ol_run_id = self._emit_openlineage_materialize_start( + feature_views_to_materialize, None, end_date + ) - start_date = feature_view.most_recent_end_time - if start_date is None: - if feature_view.ttl is None: - raise Exception( - f"No start time found for feature view {feature_view.name}. materialize_incremental() requires" - f" either a ttl to be set or for materialize() to have been run at least once." - ) - elif feature_view.ttl.total_seconds() > 0: - start_date = _utc_now() - feature_view.ttl - else: - # TODO(felixwang9817): Find the earliest timestamp for this specific feature - # view from the offline store, and set the start date to that timestamp. - print( - f"Since the ttl is 0 for feature view {Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}, " - "the start date will be set to 1 year before the current time." - ) - start_date = _utc_now() - timedelta(weeks=52) + _mat_start = time.monotonic() + try: provider = self._get_provider() - print( - f"{Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}" - f" from {Style.BRIGHT + Fore.GREEN}{utils.make_tzaware(start_date.replace(microsecond=0))}{Style.RESET_ALL}" - f" to {Style.BRIGHT + Fore.GREEN}{utils.make_tzaware(end_date.replace(microsecond=0))}{Style.RESET_ALL}:" - ) + end_date_tz = utils.make_tzaware(end_date) or _utc_now() def tqdm_builder(length): return tqdm(total=length, ncols=100) - start_date = utils.make_tzaware(start_date) - end_date = utils.make_tzaware(end_date) or _utc_now() + # (feature_view, start_date) — start_date is always set before append + regular_fvs_with_dates: list[tuple[Any, datetime]] = [] + + for feature_view in feature_views_to_materialize: + if isinstance(feature_view, OnDemandFeatureView): + if feature_view.write_to_online_store: + source_fvs = { + self._get_feature_view(p.name) + for p in feature_view.source_feature_view_projections.values() + } + max_ttl = timedelta(0) + for fv in source_fvs: + if fv.ttl and fv.ttl > max_ttl: + max_ttl = fv.ttl + + if max_ttl.total_seconds() > 0: + odfv_start_date = end_date - max_ttl + else: + odfv_start_date = end_date - timedelta(weeks=52) + + print( + f"{Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}:" + ) + self._materialize_odfv( + feature_view, + odfv_start_date, + end_date, + full_feature_names=full_feature_names, + ) + continue + + start_date = feature_view.most_recent_end_time + if start_date is None: + if feature_view.ttl is None: + raise Exception( + f"No start time found for feature view {feature_view.name}. materialize_incremental() requires" + f" either a ttl to be set or for materialize() to have been run at least once." + ) + elif feature_view.ttl.total_seconds() > 0: + start_date = _utc_now() - feature_view.ttl + else: + # TODO(felixwang9817): Find the earliest timestamp for this specific feature + # view from the offline store, and set the start date to that timestamp. + print( + f"Since the ttl is 0 for feature view {Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}, " + "the start date will be set to 1 year before the current time." + ) + start_date = _utc_now() - timedelta(weeks=52) - provider.materialize_single_feature_view( - config=self.config, - feature_view=feature_view, - start_date=start_date, - end_date=end_date, - registry=self._registry, - project=self.project, - tqdm_builder=tqdm_builder, - ) - if not isinstance(feature_view, OnDemandFeatureView): - self._registry.apply_materialization( - feature_view, - self.project, - start_date, - end_date, + start_date = utils.make_tzaware(start_date) + print( + f"{Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}" + f" from {Style.BRIGHT + Fore.GREEN}{utils.make_tzaware(start_date.replace(microsecond=0))}{Style.RESET_ALL}" + f" to {Style.BRIGHT + Fore.GREEN}{utils.make_tzaware(end_date.replace(microsecond=0))}{Style.RESET_ALL}:" ) + regular_fvs_with_dates.append((feature_view, start_date)) + + # batch_engine is on PassthroughProvider (concrete); same access as + # _submit_and_process_materialization_jobs via untyped provider. + batch_engine = getattr(provider, "batch_engine", None) + if batch_engine and getattr(batch_engine, "supports_batch", False): + self._materialize_fvs_batch( + provider, + regular_fvs_with_dates, + end_date_tz, + tqdm_builder, + ) + else: + for feature_view, start_date in regular_fvs_with_dates: + previous_state = getattr(feature_view, "state", None) + if previous_state == FeatureViewState.MATERIALIZING: + previous_state = FeatureViewState.GENERATED + elif ( + hasattr(feature_view, "state") + and feature_view.state != FeatureViewState.STATE_UNSPECIFIED + ): + if not feature_view.state.can_transition_to( + FeatureViewState.MATERIALIZING + ): + raise ValueError( + f"FeatureView {feature_view.name} cannot transition " + f"from {feature_view.state.name} to MATERIALIZING." + ) + feature_view.state = FeatureViewState.MATERIALIZING + self.registry.apply_feature_view( + feature_view, self.project, commit=True + ) + + fv_start = time.monotonic() + fv_success = True + try: + provider.materialize_single_feature_view( + config=self.config, + feature_view=feature_view, + start_date=start_date, + end_date=end_date_tz, + registry=self.registry, + project=self.project, + tqdm_builder=tqdm_builder, + ) + except Exception: + fv_success = False + if ( + hasattr(feature_view, "state") + and previous_state is not None + and previous_state != FeatureViewState.STATE_UNSPECIFIED + ): + feature_view.state = previous_state + self.registry.apply_feature_view( + feature_view, self.project, commit=True + ) + raise + finally: + _tracker = _get_track_materialization() + if _tracker is not None: + _tracker( + feature_view.name, + fv_success, + time.monotonic() - fv_start, + ) + + self.registry.apply_materialization( + feature_view, + self.project, + start_date, + end_date_tz, + ) + + materialized_fv_names = [ + fv.name + for fv in feature_views_to_materialize + if not isinstance(fv, OnDemandFeatureView) + ] + if materialized_fv_names: + self._precompute_affected_services(materialized_fv_names) + + # Emit OpenLineage COMPLETE event + self._emit_openlineage_materialize_complete( + ol_run_id, feature_views_to_materialize + ) + + # Emit MLflow event for materialization (Phase 7) + _mat_duration = time.monotonic() - _mat_start + self._mlflow_log_materialize( + feature_views_to_materialize, + None, + end_date, + _mat_duration, + incremental=True, + ) + except Exception as e: + # Emit OpenLineage FAIL event + self._emit_openlineage_materialize_fail(ol_run_id, str(e)) + raise + def materialize( self, start_date: datetime, end_date: datetime, feature_views: Optional[List[str]] = None, disable_event_timestamp: bool = False, + full_feature_names: bool = False, + version: Optional[str] = None, + force: bool = False, + run_async: bool = False, ) -> None: """ Materialize data from the offline store into the online store. @@ -1568,6 +2809,15 @@ def materialize( feature_views (List[str]): Optional list of feature view names. If selected, will only run materialization for the specified feature views. disable_event_timestamp (bool): If True, materializes all available data using current datetime as event timestamp instead of source event timestamps + full_feature_names (bool): If True, feature names will be prefixed with the corresponding + feature view name. + version (str): Optional version to materialize (e.g., 'v2'). Requires feature_views + with exactly one entry and enable_online_feature_view_versioning to be enabled. + force (bool): When using remote topology with run_async=True, pass force=true to + override stuck MATERIALIZING state on the feature server. Ignored for local topology. + run_async (bool): When using remote topology, if False (default) POST without async and + block until the server finishes sync materialization. If True, POST with ?async=true + and return after 202. Ignored for local topology. Examples: Materialize all features into the online store over the interval @@ -1582,13 +2832,29 @@ def materialize( ... """ + if self._is_remote_topology(): + payload: Dict[str, Any] = { + "start_ts": start_date.isoformat(), + "end_ts": end_date.isoformat(), + "feature_views": feature_views, + "disable_event_timestamp": disable_event_timestamp, + "full_feature_names": full_feature_names, + } + if version is not None: + payload["version"] = version + self._delegate_remote_materialize( + "/materialize", payload, force=force, run_async=run_async + ) + return + if utils.make_tzaware(start_date) > utils.make_tzaware(end_date): raise ValueError( f"The given start_date {start_date} is greater than the given end_date {end_date}." ) + parsed_version = self._validate_materialize_version(version, feature_views) feature_views_to_materialize = self._get_feature_views_to_materialize( - feature_views + feature_views, version=parsed_version ) _print_materialization_log( start_date, @@ -1596,51 +2862,227 @@ def materialize( len(feature_views_to_materialize), self.config.online_store.type, ) - # TODO paging large loads - for feature_view in feature_views_to_materialize: - if isinstance(feature_view, OnDemandFeatureView): - if feature_view.write_to_online_store: - print( - f"{Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}:" - ) - self._materialize_odfv(feature_view, start_date, end_date) - continue + + # Emit OpenLineage START event + ol_run_id = self._emit_openlineage_materialize_start( + feature_views_to_materialize, start_date, end_date + ) + + _mat_start = time.monotonic() + try: provider = self._get_provider() - print(f"{Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}:") + start_date = utils.make_tzaware(start_date) + end_date = utils.make_tzaware(end_date) def tqdm_builder(length): return tqdm(total=length, ncols=100) - start_date = utils.make_tzaware(start_date) - end_date = utils.make_tzaware(end_date) + regular_fvs_with_dates: list[tuple[Any, datetime]] = [] + + for feature_view in feature_views_to_materialize: + if isinstance(feature_view, OnDemandFeatureView): + if feature_view.write_to_online_store: + print( + f"{Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}:" + ) + self._materialize_odfv( + feature_view, + start_date, + end_date, + full_feature_names=full_feature_names, + ) + continue + + print( + f"{Style.BRIGHT + Fore.GREEN}{feature_view.name}{Style.RESET_ALL}:" + ) + + regular_fvs_with_dates.append((feature_view, start_date)) + + # batch_engine is on PassthroughProvider (concrete); same access as + # _submit_and_process_materialization_jobs via untyped provider. + batch_engine = getattr(provider, "batch_engine", None) + if batch_engine and getattr(batch_engine, "supports_batch", False): + self._materialize_fvs_batch( + provider, + regular_fvs_with_dates, + end_date, + tqdm_builder, + disable_event_timestamp=disable_event_timestamp, + ) + else: + for feature_view, fv_start in regular_fvs_with_dates: + previous_state = getattr(feature_view, "state", None) + if previous_state == FeatureViewState.MATERIALIZING: + previous_state = FeatureViewState.GENERATED + elif ( + hasattr(feature_view, "state") + and feature_view.state != FeatureViewState.STATE_UNSPECIFIED + ): + if not feature_view.state.can_transition_to( + FeatureViewState.MATERIALIZING + ): + raise ValueError( + f"FeatureView {feature_view.name} cannot transition " + f"from {feature_view.state.name} to MATERIALIZING." + ) + feature_view.state = FeatureViewState.MATERIALIZING + self.registry.apply_feature_view( + feature_view, self.project, commit=True + ) + + fv_start_time = time.monotonic() + fv_success = True + try: + provider.materialize_single_feature_view( + config=self.config, + feature_view=feature_view, + start_date=fv_start, + end_date=end_date, + registry=self.registry, + project=self.project, + tqdm_builder=tqdm_builder, + disable_event_timestamp=disable_event_timestamp, + ) + except Exception: + fv_success = False + if ( + hasattr(feature_view, "state") + and previous_state is not None + and previous_state != FeatureViewState.STATE_UNSPECIFIED + ): + feature_view.state = previous_state + self.registry.apply_feature_view( + feature_view, self.project, commit=True + ) + raise + finally: + _tracker = _get_track_materialization() + if _tracker is not None: + _tracker( + feature_view.name, + fv_success, + time.monotonic() - fv_start_time, + ) + + self.registry.apply_materialization( + feature_view, + self.project, + fv_start, + end_date, + ) + + materialized_fv_names = [ + fv.name + for fv in feature_views_to_materialize + if not isinstance(fv, OnDemandFeatureView) + ] + if materialized_fv_names: + self._precompute_affected_services(materialized_fv_names) + + # Emit OpenLineage COMPLETE event + self._emit_openlineage_materialize_complete( + ol_run_id, feature_views_to_materialize + ) + + # Emit MLflow event for materialization (Phase 7) + _mat_duration = time.monotonic() - _mat_start + self._mlflow_log_materialize( + feature_views_to_materialize, + start_date, + end_date, + _mat_duration, + incremental=False, + ) + except Exception as e: + # Emit OpenLineage FAIL event + self._emit_openlineage_materialize_fail(ol_run_id, str(e)) + raise - provider.materialize_single_feature_view( - config=self.config, - feature_view=feature_view, + def _mlflow_log_materialize( + self, + feature_views: List[Any], + start_date: Optional[datetime], + end_date: datetime, + duration_seconds: float, + incremental: bool = False, + ): + """Log materialization to MLflow ops experiment.""" + try: + if self.mlflow is None or not self.config.mlflow.log_operations: + return + fv_names = [getattr(fv, "name", str(fv)) for fv in feature_views] + self.mlflow.log_materialize( + feature_view_names=fv_names, start_date=start_date, end_date=end_date, - registry=self._registry, - project=self.project, - tqdm_builder=tqdm_builder, - disable_event_timestamp=disable_event_timestamp, + duration_seconds=duration_seconds, + incremental=incremental, ) + except Exception as e: + _logger.debug("MLflow materialize logging failed: %s", e) - self._registry.apply_materialization( - feature_view, - self.project, - start_date, - end_date, + def _emit_openlineage_materialize_start( + self, + feature_views: List[Any], + start_date: Optional[datetime], + end_date: datetime, + ) -> Optional[str]: + """Emit OpenLineage START event for materialization.""" + if self.openlineage_emitter is None: + return None + try: + run_id, success = self.openlineage_emitter.emit_materialize_start( + feature_views, start_date, end_date, self.project + ) + # Return run_id only if START was successfully emitted + # This prevents orphaned COMPLETE/FAIL events + return run_id if run_id and success else None + except Exception as e: + warnings.warn(f"Failed to emit OpenLineage materialize start event: {e}") + return None + + def _emit_openlineage_materialize_complete( + self, + run_id: Optional[str], + feature_views: List[Any], + ): + """Emit OpenLineage COMPLETE event for materialization.""" + if self.openlineage_emitter is None or not run_id: + return + try: + self.openlineage_emitter.emit_materialize_complete( + run_id, feature_views, self.project + ) + except Exception as e: + warnings.warn(f"Failed to emit OpenLineage materialize complete event: {e}") + + def _emit_openlineage_materialize_fail( + self, + run_id: Optional[str], + error_message: str, + ): + """Emit OpenLineage FAIL event for materialization.""" + if self.openlineage_emitter is None or not run_id: + return + try: + self.openlineage_emitter.emit_materialize_fail( + run_id, self.project, error_message ) + except Exception as e: + warnings.warn(f"Failed to emit OpenLineage materialize fail event: {e}") def _fvs_for_push_source_or_raise( self, push_source_name: str, allow_cache: bool - ) -> set[FeatureView]: + ) -> set[BaseFeatureView]: from feast.data_source import PushSource - all_fvs = self.list_feature_views(allow_cache=allow_cache) + all_fvs: list[Union[FeatureView, StreamFeatureView]] = list( + self.list_feature_views(allow_cache=allow_cache) + ) all_fvs += self.list_stream_feature_views(allow_cache=allow_cache) - fvs_with_push_sources = { + fvs_with_push_sources: set[BaseFeatureView] = { fv for fv in all_fvs if ( @@ -1650,11 +3092,249 @@ def _fvs_for_push_source_or_raise( ) } + for lv in self.list_label_views(allow_cache=allow_cache): + if ( + lv.source is not None + and isinstance(lv.source, PushSource) + and lv.source.name == push_source_name + ): + fvs_with_push_sources.add(lv) + if not fvs_with_push_sources: raise PushSourceNotFoundException(push_source_name) return fvs_with_push_sources + def precompute_feature_service( + self, + feature_service_name: Optional[str] = None, + batch_size: int = 1000, + ) -> int: + """Pre-compute feature vectors for one or all FeatureServices. + + For each FeatureService with ``precompute_online=True`` (or matching + *feature_service_name*), reads every entity's features from the online + store via :meth:`OnlineStore.online_read` and writes a single serialized + blob per entity via :meth:`OnlineStore.write_precomputed_vector`. + + Works with **all** online store backends (Redis, DynamoDB, PostgreSQL, etc.). + + Returns the total number of entity vectors written. + """ + from feast.protos.feast.core.PrecomputedFeatureVector_pb2 import ( + FeatureViewTimestamp, + PrecomputedFeatureVector, + ) + + provider = self._get_provider() + online_store = provider.online_store + + services = self.registry.list_feature_services(self.project) + if feature_service_name: + services = [s for s in services if s.name == feature_service_name] + + total_written = 0 + for svc in services: + if not svc.precompute_online and not feature_service_name: + continue + + fv_projections = svc.feature_view_projections + feature_views = [] + for proj in fv_projections: + fv = self.registry.get_any_feature_view( + proj.name, self.project, allow_cache=True + ) + feature_views.append((fv, proj)) + + if not feature_views: + continue + + feature_names: List[str] = [] + for _fv, proj in feature_views: + fv_name = proj.name_to_use() + for f in proj.features: + feature_names.append(f"{fv_name}__{f.name}") + + # Collect all unique entity key protos from the primary feature view. + # Use online_read to discover entities that exist in the store. + primary_fv = feature_views[0][0] + + # Read entity keys by scanning the primary FV's online data. + # We use get_online_features with the full FeatureService to read + # all features for each entity in a single call, then build vectors. + # + # For stores that support native scanning (like Redis), we try the + # native scan; for others, we use the materialized entity list. + entity_key_protos: List[EntityKey] = [] + try: + from feast.infra.online_stores.redis import RedisOnlineStore + + if isinstance(online_store, RedisOnlineStore): + from feast.infra.key_encoding_utils import ( + deserialize_entity_key, + ) + from feast.infra.online_stores.helpers import _redis_key_prefix + + join_keys = ( + list(primary_fv.join_keys) + if hasattr(primary_fv, "join_keys") + else [] + ) # type: ignore[union-attr] + client = online_store._get_client( # type: ignore[attr-defined] + self.config.online_store + ) + scan_prefix = _redis_key_prefix(join_keys) + project_bytes = self.config.project.encode("utf-8") + seen_keys: set = set() + raw_keys: List[bytes] = [] + for key in client.scan_iter( + b"".join([scan_prefix, b"*", project_bytes]) + ): + if key not in seen_keys: + seen_keys.add(key) + raw_keys.append(key) + proj_len = len(project_bytes) + for rk in raw_keys: + try: + ek = deserialize_entity_key( + rk[:-proj_len], + self.config.entity_key_serialization_version, + ) + if set(ek.join_keys) == set(join_keys): + entity_key_protos.append(ek) + except (ValueError, Exception): + continue + else: + raise NotImplementedError + except (ImportError, NotImplementedError): + _logger.warning( + "Entity scanning not supported for '%s' — " + "precompute requires a store that supports entity scanning", + type(online_store).__name__, + ) + continue + + if not entity_key_protos: + continue + + for batch_start in range(0, len(entity_key_protos), batch_size): + batch_keys = entity_key_protos[batch_start : batch_start + batch_size] + + # Read features for each FV via base class online_read. + fv_data: Dict[ + str, + List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]], + ] = {} + for fv_obj, proj in feature_views: + req_features = [f.name for f in proj.features] + assert isinstance(fv_obj, FeatureView) + rows = online_store.online_read( + config=self.config, + table=fv_obj, + entity_keys=batch_keys, + requested_features=req_features, + ) + fv_data[proj.name_to_use()] = rows + + for entity_idx, entity_key in enumerate(batch_keys): + values: List[ValueProto] = [] + fv_timestamps: list = [] + + for _fv, proj in feature_views: + fv_name = proj.name_to_use() + fv_row_ts, feat_dict = fv_data[fv_name][entity_idx] + + if fv_row_ts: + fv_ts = Timestamp() + fv_ts.FromDatetime(utils.make_tzaware(fv_row_ts)) + fv_timestamps.append( + FeatureViewTimestamp( + feature_view_name=fv_name, + event_timestamp=fv_ts, + ) + ) + + for f in proj.features: + if feat_dict and f.name in feat_dict: + values.append(feat_dict[f.name]) + else: + values.append(ValueProto()) + + now_ts = Timestamp() + now_ts.FromDatetime(datetime.now(tz=_timezone.utc)) + + vector = PrecomputedFeatureVector( + feature_names=feature_names, + values=values, + fv_timestamps=fv_timestamps, + precomputed_at=now_ts, + ) + + online_store.write_precomputed_vector( + config=self.config, + feature_service_name=svc.name, + project=self.config.project, + entity_key=entity_key, + vector_bytes=vector.SerializeToString(), + ) + total_written += 1 + + _logger.info( + "Pre-computed %d entity vectors for FeatureService '%s'", + total_written, + svc.name, + ) + + return total_written + + def _precompute_affected_services(self, materialized_fv_names: List[str]) -> None: + """Trigger precomputation for services affected by materialized FVs.""" + try: + services = self.registry.list_feature_services(self.project) + except Exception: + return + + for svc in services: + if not svc.precompute_online: + continue + svc_fv_names = {p.name for p in svc.feature_view_projections} + if svc_fv_names & set(materialized_fv_names): + try: + self.precompute_feature_service(svc.name) + except Exception: + _logger.warning( + "Failed to precompute vectors for service '%s'", + svc.name, + exc_info=True, + ) + + def _precompute_for_push(self, feature_view_name: str, df: "pd.DataFrame") -> None: + """Re-compute pre-computed vectors for entities affected by a push.""" + try: + services = self.registry.list_feature_services(self.project) + except Exception: + return + + affected = [ + svc + for svc in services + if svc.precompute_online + and any(p.name == feature_view_name for p in svc.feature_view_projections) + ] + + if not affected: + return + + for svc in affected: + try: + self.precompute_feature_service(svc.name) + except Exception: + _logger.warning( + "Failed to precompute vectors for service '%s' after push", + svc.name, + exc_info=True, + ) + def push( self, push_source_name: str, @@ -1673,6 +3353,7 @@ def push( to: Whether to push to online or offline store. Defaults to online store only. transform_on_write: Whether to transform the data before pushing. """ + pushed_fv_names = [] for fv in self._fvs_for_push_source_or_raise( push_source_name, allow_registry_cache ): @@ -1683,11 +3364,15 @@ def push( allow_registry_cache=allow_registry_cache, transform_on_write=transform_on_write, ) + pushed_fv_names.append(fv.name) if to == PushMode.OFFLINE or to == PushMode.ONLINE_AND_OFFLINE: self.write_to_offline_store( fv.name, df, allow_registry_cache=allow_registry_cache ) + if pushed_fv_names: + self._precompute_for_push(pushed_fv_names[0], df) + async def push_async( self, push_source_name: str, @@ -1776,76 +3461,104 @@ def _transform_on_demand_feature_view_df( Raises: Exception: For unsupported OnDemandFeatureView modes """ - if feature_view.mode == "python" and isinstance( - feature_view.feature_transformation, PythonTransformation - ): - input_dict = ( - df.to_dict(orient="records")[0] - if feature_view.singleton - else df.to_dict(orient="list") - ) + _should_track = False + try: + from feast.metrics import _config as _metrics_config - if feature_view.singleton: - transformed_rows = [] + _should_track = _metrics_config.online_features and getattr( + feature_view, "track_metrics", False + ) + except Exception: + pass - for i, row in df.iterrows(): - output = feature_view.feature_transformation.udf(row.to_dict()) - if i == 0: - transformed_rows = output - else: - for k in output: - if isinstance(output[k], list): - transformed_rows[k].extend(output[k]) - else: - transformed_rows[k].append(output[k]) + if _should_track: + import time as _time - transformed_data = pd.DataFrame(transformed_rows) - else: - transformed_data = feature_view.feature_transformation.udf(input_dict) + _t0 = _time.monotonic() - if feature_view.write_to_online_store: - entities = [ - self.get_entity(entity) for entity in (feature_view.entities or []) - ] - join_keys = [entity.join_key for entity in entities if entity] - join_keys = [k for k in join_keys if k in input_dict.keys()] - transformed_df = ( - pd.DataFrame(transformed_data) - if not isinstance(transformed_data, pd.DataFrame) - else transformed_data - ) - input_df = pd.DataFrame( - [input_dict] if feature_view.singleton else input_dict + try: + if feature_view.mode == "python" and isinstance( + feature_view.feature_transformation, PythonTransformation + ): + input_dict = ( + df.to_dict(orient="records")[0] + if feature_view.singleton + else df.to_dict(orient="list") ) - if input_df.shape[0] == transformed_df.shape[0]: + + if feature_view.singleton: + transformed_rows = [] + + for i, row in df.iterrows(): + output = feature_view.feature_transformation.udf(row.to_dict()) + if i == 0: + transformed_rows = output + else: + for k in output: + if isinstance(output[k], list): + transformed_rows[k].extend(output[k]) + else: + transformed_rows[k].append(output[k]) + + transformed_data = pd.DataFrame(transformed_rows) + else: + transformed_data = feature_view.feature_transformation.udf( + input_dict + ) + + if feature_view.write_to_online_store: + entities = [ + self.get_entity(entity) + for entity in (feature_view.entities or []) + ] + join_keys = [entity.join_key for entity in entities if entity] + join_keys = [k for k in join_keys if k in input_dict.keys()] + transformed_df = ( + pd.DataFrame(transformed_data) + if not isinstance(transformed_data, pd.DataFrame) + else transformed_data + ) + input_df = pd.DataFrame( + [input_dict] if feature_view.singleton else input_dict + ) + if input_df.shape[0] == transformed_df.shape[0]: + for k in input_dict: + if k not in transformed_data: + transformed_data[k] = input_dict[k] + transformed_df = pd.DataFrame(transformed_data) + else: + transformed_df = pd.merge( + transformed_df, + input_df, + how="left", + on=join_keys, + ) + else: + # overwrite any transformed features and update the dictionary for k in input_dict: if k not in transformed_data: transformed_data[k] = input_dict[k] - transformed_df = pd.DataFrame(transformed_data) - else: - transformed_df = pd.merge( - transformed_df, - input_df, - how="left", - on=join_keys, - ) - else: - # overwrite any transformed features and update the dictionary - for k in input_dict: - if k not in transformed_data: - transformed_data[k] = input_dict[k] - return pd.DataFrame(transformed_data) + return pd.DataFrame(transformed_data) - elif feature_view.mode == "pandas" and isinstance( - feature_view.feature_transformation, PandasTransformation - ): - transformed_df = feature_view.feature_transformation.udf(df) - for col in df.columns: - transformed_df[col] = df[col] - return transformed_df - else: - raise Exception("Unsupported OnDemandFeatureView mode") + elif feature_view.mode == "pandas" and isinstance( + feature_view.feature_transformation, PandasTransformation + ): + transformed_df = feature_view.feature_transformation.udf(df) + for col in df.columns: + transformed_df[col] = df[col] + return transformed_df + else: + raise Exception("Unsupported OnDemandFeatureView mode") + finally: + if _should_track: + from feast.metrics import track_write_transformation + + track_write_transformation( + feature_view.name, + feature_view.mode, + _time.monotonic() - _t0, + ) def _validate_vector_features(self, feature_view, df: pd.DataFrame) -> None: """ @@ -1995,6 +3708,91 @@ async def write_to_online_store_async( provider = self._get_provider() await provider.ingest_df_async(feature_view, df) + async def update_online_store( + self, + feature_view_name: str, + df: pd.DataFrame, + update_expressions: Dict[str, str], + allow_registry_cache: bool = True, + ) -> None: + """ + Update features using DynamoDB-specific list operations. + + This method provides efficient in-place list updates using DynamoDB's native + UpdateItem operations with list_append and other expressions. This is more + efficient than the read-modify-write pattern for array-based features. + + Args: + feature_view_name: The feature view to update. + df: DataFrame with new values to append/prepend to existing lists. + update_expressions: Dict mapping feature names to DynamoDB update expressions. + Examples: + - {"transactions": "list_append(transactions, :new_val)"} # append + - {"recent_items": "list_append(:new_val, recent_items)"} # prepend + allow_registry_cache: Whether to allow cached registry. + + Raises: + NotImplementedError: If online store doesn't support update expressions. + ValueError: If the feature view or update expressions are invalid. + + Example: + # Append new transactions to existing transaction history + await store.update_online_store( + feature_view_name="user_transactions", + df=new_transactions_df, + update_expressions={ + "transaction_history": "list_append(transaction_history, :new_val)", + "recent_amounts": "list_append(:new_val, recent_amounts)" # prepend + } + ) + """ + # Check if online store supports update expressions + provider = self._get_provider() + if not hasattr(provider.online_store, "update_online_store_async"): + raise NotImplementedError( + f"Online store {type(provider.online_store).__name__} " + "does not support async update expressions. This feature is only available " + "with DynamoDB online store." + ) + + feature_view, df = self._get_feature_view_and_df_for_online_write( + feature_view_name=feature_view_name, + df=df, + allow_registry_cache=allow_registry_cache, + transform_on_write=False, # Don't transform for updates + ) + + # Validate that the dataframe has meaningful feature data + if df is not None: + if df.empty: + warnings.warn("Cannot update with empty dataframe") + return + + # Check if feature columns are empty + feature_column_names = [f.name for f in feature_view.features] + if feature_column_names: + feature_df = df[feature_column_names] + if feature_df.empty or feature_df.isnull().all().all(): + warnings.warn("Cannot update with empty feature columns") + return + + # Prepare data for online store + from feast.infra.passthrough_provider import PassthroughProvider + + rows_to_write = PassthroughProvider._prep_rows_to_write_for_ingestion( + feature_view=feature_view, + df=df, + ) + + # Call DynamoDB-specific async method + await provider.online_store.update_online_store_async( + config=self.config, + table=feature_view, + data=rows_to_write, + update_expressions=update_expressions, + progress=None, + ) + def write_to_offline_store( self, feature_view_name: str, @@ -2014,12 +3812,19 @@ def write_to_offline_store( feature_view_name, allow_registry_cache=allow_registry_cache ) except FeatureViewNotFoundException: - feature_view = self.get_feature_view( - feature_view_name, allow_registry_cache=allow_registry_cache - ) + try: + feature_view = self.get_feature_view( + feature_view_name, allow_registry_cache=allow_registry_cache + ) + except FeatureViewNotFoundException: + feature_view = self.get_label_view( # type: ignore[assignment] + feature_view_name, allow_registry_cache=allow_registry_cache + ) provider = self._get_provider() # Get columns of the batch source and the input dataframe. + if feature_view.batch_source is None: + raise ValueError(f"Feature view '{feature_view.name}' has no batch_source.") column_names_and_types = ( provider.get_table_column_names_and_types_from_data_source( self.config, feature_view.batch_source @@ -2028,9 +3833,17 @@ def write_to_offline_store( source_columns = [column for column, _ in column_names_and_types] input_columns = df.columns.values.tolist() - if set(input_columns) != set(source_columns): + input_columns_set = set(input_columns) + source_columns_set = set(source_columns) + + if input_columns_set != source_columns_set: + missing_expected_columns = sorted(source_columns_set - input_columns_set) + extra_unexpected_columns = sorted(input_columns_set - source_columns_set) + raise ValueError( - f"The input dataframe has columns {set(input_columns)} but the batch source has columns {set(source_columns)}." + "The input dataframe columns do not match the batch source columns. " + f"missing_expected_columns: {missing_expected_columns}, " + f"extra_unexpected_columns: {extra_unexpected_columns}." ) if reorder_columns: @@ -2047,6 +3860,7 @@ def get_online_features( Mapping[str, Union[Sequence[Any], Sequence[Value], RepeatedValue]], ], full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: """ Retrieves the latest online feature data. @@ -2091,15 +3905,45 @@ def get_online_features( """ provider = self._get_provider() - return provider.get_online_features( + _retrieval_start = time.monotonic() + + response = provider.get_online_features( config=self.config, features=features, entity_rows=entity_rows, - registry=self._registry, + registry=self.registry, project=self.project, full_feature_names=full_feature_names, + include_feature_view_version_metadata=include_feature_view_version_metadata, ) + # Auto-log to MLflow if configured + try: + if self.mlflow is not None and self.config.mlflow.auto_log: + _duration = time.monotonic() - _retrieval_start + _feature_refs = utils._get_features( + self.registry, self.project, features, allow_cache=True + ) + _entity_count = self._count_entities(entity_rows) + _fs = features if isinstance(features, FeatureService) else None + _fs_name = ( + features.name + if isinstance(features, FeatureService) + else self._resolve_feature_service_name(_feature_refs) + ) + self.mlflow.log_feature_retrieval( + feature_refs=_feature_refs, + entity_count=_entity_count, + duration_seconds=_duration, + retrieval_type="online", + feature_service=_fs, + feature_service_name=_fs_name, + ) + except Exception as e: + _logger.debug("MLflow auto-log failed for online retrieval: %s", e) + + return response + async def get_online_features_async( self, features: Union[List[str], FeatureService], @@ -2108,6 +3952,7 @@ async def get_online_features_async( Mapping[str, Union[Sequence[Any], Sequence[Value], RepeatedValue]], ], full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: """ [Alpha] Retrieves the latest online feature data asynchronously. @@ -2137,15 +3982,65 @@ async def get_online_features_async( """ provider = self._get_provider() - return await provider.get_online_features_async( + _retrieval_start = time.monotonic() + + response = await provider.get_online_features_async( config=self.config, features=features, entity_rows=entity_rows, - registry=self._registry, + registry=self.registry, project=self.project, full_feature_names=full_feature_names, + include_feature_view_version_metadata=include_feature_view_version_metadata, ) + try: + if self.mlflow is not None and self.config.mlflow.auto_log: + _duration = time.monotonic() - _retrieval_start + _mlflow_client = self.mlflow + _registry = self.registry + _project = self.project + + def _log_sync(): + try: + _feature_refs = utils._get_features( + _registry, _project, features, allow_cache=True + ) + _entity_count = self._count_entities(entity_rows) + _fs = features if isinstance(features, FeatureService) else None + _fs_name = ( + features.name + if isinstance(features, FeatureService) + else self._resolve_feature_service_name(_feature_refs) + ) + _mlflow_client.log_feature_retrieval( + feature_refs=_feature_refs, + entity_count=_entity_count, + duration_seconds=_duration, + retrieval_type="online", + feature_service=_fs, + feature_service_name=_fs_name, + ) + except Exception as exc: + _logger.debug( + "MLflow auto-log failed for async online retrieval: %s", + exc, + ) + + def _on_done(fut): + if not fut.cancelled() and fut.exception() is not None: + _logger.debug( + "MLflow auto-log executor failed: %s", fut.exception() + ) + + loop = asyncio.get_running_loop() + fut = loop.run_in_executor(None, _log_sync) + fut.add_done_callback(_on_done) + except Exception as e: + _logger.debug("MLflow auto-log failed for online retrieval: %s", e) + + return response + def retrieve_online_documents( self, query: Union[str, List[float]], @@ -2171,7 +4066,7 @@ def retrieve_online_documents( available_feature_views, _, ) = utils._get_feature_views_to_use( - registry=self._registry, + registry=self.registry, project=self.project, features=features, allow_cache=True, @@ -2179,13 +4074,15 @@ def retrieve_online_documents( ) feature_view_set = set() for _feature in features: - feature_view_name = _feature.split(":")[0] - feature_view = self.get_feature_view(feature_view_name) + fv_name, _, _ = utils._parse_feature_ref(_feature) + feature_view = self.get_feature_view(fv_name) feature_view_set.add(feature_view.name) if len(feature_view_set) > 1: raise ValueError("Document retrieval only supports a single feature view.") requested_features = [ - f.split(":")[1] for f in features if isinstance(f, str) and ":" in f + utils._parse_feature_ref(f)[2] + for f in features + if isinstance(f, str) and ":" in f ] requested_feature_view_name = list(feature_view_set)[0] for feature_view in available_feature_views: @@ -2244,7 +4141,10 @@ def _doc_feature(x): online_features_response=online_features_response, data=requested_features_data, ) - return OnlineResponse(online_features_response) + feature_types = { + f.name: f.dtype.to_value_type() for f in requested_feature_view.features + } + return OnlineResponse(online_features_response, feature_types=feature_types) def retrieve_online_documents_v2( self, @@ -2259,6 +4159,8 @@ def retrieve_online_documents_v2( text_weight: float = 0.5, image_weight: float = 0.5, combine_strategy: str = "weighted_sum", + include_feature_view_version_metadata: bool = False, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, ) -> OnlineResponse: """ Retrieves the top k closest document features. Note, embeddings are a subset of features. @@ -2372,7 +4274,7 @@ def retrieve_online_documents_v2( available_feature_views, available_odfv_views, ) = utils._get_feature_views_to_use( - registry=self._registry, + registry=self.registry, project=self.project, features=features, allow_cache=True, @@ -2380,18 +4282,20 @@ def retrieve_online_documents_v2( ) feature_view_set = set() for feature in features: - feature_view_name = feature.split(":")[0] - if feature_view_name in [fv.name for fv in available_odfv_views]: + fv_name, _, _ = utils._parse_feature_ref(feature) + if fv_name in [fv.name for fv in available_odfv_views]: feature_view: Union[OnDemandFeatureView, FeatureView] = ( - self.get_on_demand_feature_view(feature_view_name) + self.get_on_demand_feature_view(fv_name) ) else: - feature_view = self.get_feature_view(feature_view_name) + feature_view = self.get_feature_view(fv_name) feature_view_set.add(feature_view.name) if len(feature_view_set) > 1: raise ValueError("Document retrieval only supports a single feature view.") requested_features = [ - f.split(":")[1] for f in features if isinstance(f, str) and ":" in f + utils._parse_feature_ref(f)[2] + for f in features + if isinstance(f, str) and ":" in f ] if len(available_feature_views) == 0: available_feature_views.extend(available_odfv_views) # type: ignore[arg-type] @@ -2411,8 +4315,182 @@ def retrieve_online_documents_v2( top_k, distance_metric, query_string, + filters, + include_feature_view_version_metadata, + ) + + async def openai_search( + self, + vector_store_id: str, + query: Union[str, List[str]], + *, + vs_id: Optional[str] = None, + max_num_results: int = 10, + filters: Optional[ + Union[ComparisonFilter, CompoundFilter, Dict[str, Any]] + ] = None, + ranking_options: Optional[Dict[str, Any]] = None, + rewrite_query: Optional[bool] = None, + features_to_retrieve: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """ + OpenAI-compatible vector store search. + + Accepts a raw query string, embeds it via the configured embedding + provider (when ``embedding_model`` is configured in feature_store.yaml), + and returns results in OpenAI's ``vector_store.search_results.page`` + format. + + Args: + vector_store_id: Feature view name (maps to the OpenAI + ``vector_store_id`` path parameter). + query: Natural language query string, or list of strings. + max_num_results: Maximum number of results to return. + filters: OpenAI-compatible filters applied to the search. + ranking_options: OpenAI-compatible ranking options. Currently + unsupported; a ``ValueError`` is raised if + ``score_threshold`` or ``ranker`` are provided. + rewrite_query: Whether to rewrite the query. Currently + unsupported; ``False`` (the default/no-op) is accepted, + but ``True`` raises a ``ValueError``. + features_to_retrieve: Specific feature names to return. + If None, all features from the feature view are used. + + Returns: + Dict matching the OpenAI ``vector_store.search_results.page`` + schema. + + Examples: + Keyword search (no embedding model configured):: + + result = await store.openai_search( + vector_store_id="city_embeddings", + query="cities in California", + max_num_results=5, + ) + + Vector search (embedding model configured in YAML):: + + # feature_store.yaml has: + # embedding_model: + # model: text-embedding-3-small + result = await store.openai_search( + vector_store_id="product_embeddings", + query="wireless audio device", + max_num_results=3, + features_to_retrieve=["name", "description"], + ) + """ + unsupported: List[str] = [] + if ranking_options: + if ranking_options.get("score_threshold") is not None: + unsupported.append("ranking_options.score_threshold") + if ranking_options.get("ranker") is not None: + unsupported.append("ranking_options.ranker") + if rewrite_query is True: + unsupported.append("rewrite_query") + if unsupported: + raise ValueError( + f"The following parameters are not yet supported: " + f"{', '.join(unsupported)}. Remove them from the request or " + f"wait for a future release that implements them." + ) + + feature_view = self.get_feature_view(vector_store_id) + display_id = vs_id or feature_view_to_vs_id(self.project, vector_store_id) + + vector_field_metadata = _get_feature_view_vector_field_metadata(feature_view) + distance_metric: Optional[str] = None + if vector_field_metadata and vector_field_metadata.vector_search_metric: + distance_metric = vector_field_metadata.vector_search_metric + + if features_to_retrieve: + feature_names = features_to_retrieve + else: + feature_names = [ + f.name for f in feature_view.features if not f.vector_index + ] + + features = [f"{feature_view.name}:{name}" for name in feature_names] + query_text = query if isinstance(query, str) else " ".join(query) + + embeddings = await self.embedding_provider.aembed([query_text]) + query_embedding = embeddings[0] + + typed_filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None + if filters is not None: + if isinstance(filters, dict): + typed_filters = convert_dict_to_filter(filters) + else: + typed_filters = filters + + response = await run_in_threadpool( + lambda: self.retrieve_online_documents_v2( + features=features, + query=query_embedding, + top_k=max_num_results, + filters=typed_filters, + ) ) + response_dict = response.to_dict() + + entity_key_names = { + col.name + for col in feature_view.entity_columns + if col.name != DUMMY_ENTITY_ID + } + + result_data = [] + if response_dict: + first_key = next(iter(response_dict)) + num_rows = len(response_dict.get(first_key, [])) + for i in range(num_rows): + score = 0.0 + attributes: Dict[str, Any] = {} + content_parts: List[Dict[str, str]] = [] + + for key, values in response_dict.items(): + val = values[i] if i < len(values) else None + if key == "distance": + raw = float(val) if val is not None else 0.0 + score = _distance_to_score(raw, distance_metric) + elif key not in entity_key_names: + attributes[key] = val + if isinstance(val, str): + content_parts.append({"type": "text", "text": val}) + + if entity_key_names: + key_parts = [ + str(response_dict[k][i]) + for k in sorted(entity_key_names) + if k in response_dict and i < len(response_dict[k]) + ] + file_id = f"{display_id}_{'_'.join(key_parts)}" + else: + file_id = f"{display_id}_{i}" + + result_data.append( + { + "file_id": file_id, + "filename": display_id, + "score": score, + "attributes": attributes, + "content": content_parts + if content_parts + else [{"type": "text", "text": str(attributes)}], + } + ) + + search_query = query if isinstance(query, list) else [query] + return { + "object": "vector_store.search_results.page", + "search_query": search_query, + "data": result_data, + "has_more": False, + "next_page": None, + } + def _retrieve_from_online_store( self, provider: Provider, @@ -2475,22 +4553,26 @@ def _retrieve_from_online_store_v2( top_k: int, distance_metric: Optional[str], query_string: Optional[str], + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: """ Search and return document features from the online document store. """ vector_field_metadata = _get_feature_view_vector_field_metadata(table) - if vector_field_metadata: + if vector_field_metadata and vector_field_metadata.vector_search_metric: distance_metric = vector_field_metadata.vector_search_metric documents = provider.retrieve_online_documents_v2( config=self.config, table=table, requested_features=requested_features, - query=query, + embedding=query, top_k=top_k, distance_metric=distance_metric, query_string=query_string, + filters=filters, + include_feature_view_version_metadata=include_feature_view_version_metadata, ) entity_key_dict: Dict[str, List[ValueProto]] = {} @@ -2528,26 +4610,23 @@ def _retrieve_from_online_store_v2( online_features_response.metadata.feature_names.val.extend( features_to_request ) - return OnlineResponse(online_features_response) + feature_types = {f.name: f.dtype.to_value_type() for f in table.features} + return OnlineResponse(online_features_response, feature_types=feature_types) table_entity_values, idxs, output_len = utils._get_unique_entities_from_values( entity_key_dict, ) - feature_data = utils._convert_rows_to_protobuf( - requested_features=features_to_request, - read_rows=list(zip(datevals, list_of_feature_dicts)), - ) - online_features_response = GetOnlineFeaturesResponse(results=[]) utils._populate_response_from_feature_data( - feature_data=feature_data, + requested_features=features_to_request, + read_rows=list(zip(datevals, list_of_feature_dicts)), indexes=idxs, online_features_response=online_features_response, full_feature_names=False, - requested_features=features_to_request, table=table, output_len=output_len, + include_feature_view_version_metadata=include_feature_view_version_metadata, ) utils._populate_result_rows_from_columnar( @@ -2555,7 +4634,8 @@ def _retrieve_from_online_store_v2( data=entity_key_dict, ) - return OnlineResponse(online_features_response) + feature_types = {f.name: f.dtype.to_value_type() for f in table.features} + return OnlineResponse(online_features_response, feature_types=feature_types) def serve( self, @@ -2564,11 +4644,14 @@ def serve( type_: str = "http", no_access_log: bool = True, workers: int = 1, + worker_connections: int = 1000, + max_requests: int = 1000, + max_requests_jitter: int = 50, metrics: bool = False, keep_alive_timeout: int = 30, tls_key_path: str = "", tls_cert_path: str = "", - registry_ttl_sec: int = 2, + registry_ttl_sec: int = 60, ) -> None: """Start the feature consumption server locally on a given port.""" type_ = type_.lower() @@ -2583,6 +4666,9 @@ def serve( port=port, no_access_log=no_access_log, workers=workers, + worker_connections=worker_connections, + max_requests=max_requests, + max_requests_jitter=max_requests_jitter, metrics=metrics, keep_alive_timeout=keep_alive_timeout, tls_key_path=tls_key_path, @@ -2592,14 +4678,12 @@ def serve( def get_feature_server_endpoint(self) -> Optional[str]: """Returns endpoint for the feature server, if it exists.""" - return self._provider.get_feature_server_endpoint() + return self.provider.get_feature_server_endpoint() def serve_ui( self, host: str, port: int, - get_registry_dump: Callable, - registry_ttl_sec: int, root_path: str = "", tls_key_path: str = "", tls_cert_path: str = "", @@ -2615,9 +4699,7 @@ def serve_ui( self, host=host, port=port, - get_registry_dump=get_registry_dump, project_id=self.config.project, - registry_ttl_sec=registry_ttl_sec, root_path=root_path, tls_key_path=tls_key_path, tls_cert_path=tls_cert_path, @@ -2694,7 +4776,7 @@ def write_logged_features( feature_service=source, logs=logs, config=self.config, - registry=self._registry, + registry=self.registry, ) def validate_logged_features( @@ -2766,7 +4848,7 @@ def get_validation_reference( Raises: ValidationReferenceNotFoundException: The validation reference could not be found. """ - ref = self._registry.get_validation_reference( + ref = self.registry.get_validation_reference( name, project=self.project, allow_cache=allow_cache ) ref._dataset = self.get_saved_dataset(ref.dataset_name) @@ -2785,7 +4867,7 @@ def list_validation_references( Returns: A list of validation references. """ - return self._registry.list_validation_references( + return self.registry.list_validation_references( self.project, allow_cache=allow_cache, tags=tags ) @@ -2802,7 +4884,7 @@ def list_permissions( Returns: A list of permissions. """ - return self._registry.list_permissions( + return self.registry.list_permissions( self.project, allow_cache=allow_cache, tags=tags ) @@ -2819,7 +4901,7 @@ def get_permission(self, name: str) -> Permission: Raises: PermissionObjectNotFoundException: The permission could not be found. """ - return self._registry.get_permission(name, self.project) + return self.registry.get_permission(name, self.project) def list_projects( self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None @@ -2827,6 +4909,9 @@ def list_projects( """ Retrieves the list of projects from the registry. + Protected projects (feast.dev/protected-project=true) are automatically + excluded from the results. + Args: allow_cache: Whether to allow returning projects from a cached registry. tags: Filter by tags. @@ -2834,7 +4919,10 @@ def list_projects( Returns: A list of projects. """ - return self._registry.list_projects(allow_cache=allow_cache, tags=tags) + from feast.constants import PROTECTED_PROJECT_TAG + + projects = self.registry.list_projects(allow_cache=allow_cache, tags=tags) + return [p for p in projects if p.tags.get(PROTECTED_PROJECT_TAG) != "true"] def get_project(self, name: Optional[str]) -> Project: """ @@ -2849,10 +4937,41 @@ def get_project(self, name: Optional[str]) -> Project: Raises: ProjectObjectNotFoundException: The project could not be found. """ - return self._registry.get_project(name or self.project) + return self.registry.get_project(name or self.project) + + def delete_project(self, name: str, commit: bool = True) -> None: + """ + Deletes a project from the registry. + + Args: + name: Name of the project to delete. + commit: Whether the change should be persisted immediately. + + Raises: + ProjectNotFoundException: The project could not be found. + ValueError: If the project is protected. + """ + from feast.constants import PROTECTED_PROJECT_TAG + + try: + project = self.registry.get_project(name=name, allow_cache=False) + if project and project.tags.get(PROTECTED_PROJECT_TAG) == "true": + raise ValueError( + f'Cannot delete protected project "{name}". ' + "Protected projects are managed externally." + ) + except ValueError: + raise + except Exception: + pass + return self.registry.delete_project(name, commit=commit) def list_saved_datasets( - self, allow_cache: bool = False, tags: Optional[dict[str, str]] = None + self, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + namespace: Optional[str] = None, + collection: Optional[str] = None, ) -> List[SavedDataset]: """ Retrieves the list of saved datasets from the registry. @@ -2860,12 +4979,18 @@ def list_saved_datasets( Args: allow_cache: Whether to allow returning saved datasets from a cached registry. tags: Filter by tags. + namespace: Filter by logical namespace grouping. + collection: Filter by collection sub-grouping within namespace. Returns: A list of saved datasets. """ - return self._registry.list_saved_datasets( - self.project, allow_cache=allow_cache, tags=tags + return self.registry.list_saved_datasets( + self.project, + allow_cache=allow_cache, + tags=tags, + namespace=namespace, + collection=collection, ) async def initialize(self) -> None: @@ -2896,18 +5021,25 @@ def _print_materialization_log( def _validate_feature_views(feature_views: List[BaseFeatureView]): - """Verify feature views have case-insensitively unique names""" - fv_names = set() + """Verify feature views have case-insensitively unique names across all types. + + This validates that no two feature views (of any type: FeatureView, + StreamFeatureView, OnDemandFeatureView) share the same case-insensitive name. + This is critical because get_online_features uses get_any_feature_view which + resolves names in a fixed order, potentially returning the wrong feature view. + """ + fv_by_name: Dict[str, BaseFeatureView] = {} for fv in feature_views: case_insensitive_fv_name = fv.name.lower() - if case_insensitive_fv_name in fv_names: - raise ValueError( - f"More than one feature view with name {case_insensitive_fv_name} found. " - f"Please ensure that all feature view names are case-insensitively unique. " - f"It may be necessary to ignore certain files in your feature repository by using a .feastignore file." + if case_insensitive_fv_name in fv_by_name: + existing_fv = fv_by_name[case_insensitive_fv_name] + raise ConflictingFeatureViewNames( + fv.name, + existing_type=type(existing_fv).__name__, + new_type=type(fv).__name__, ) else: - fv_names.add(case_insensitive_fv_name) + fv_by_name[case_insensitive_fv_name] = fv def _validate_data_sources(data_sources: List[DataSource]): diff --git a/sdk/python/feast/feature_view.py b/sdk/python/feast/feature_view.py index a9406657a51..6d1a77b1114 100644 --- a/sdk/python/feast/feature_view.py +++ b/sdk/python/feast/feature_view.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import copy +import enum import warnings from datetime import datetime, timedelta from typing import Dict, List, Optional, Tuple, Type, Union @@ -26,6 +27,11 @@ from feast.entity import Entity from feast.feature_view_projection import FeatureViewProjection from feast.field import Field +from feast.proto_utils import ( + mode_to_string, + serialize_data_source, + transformation_to_proto, +) from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto from feast.protos.feast.core.FeatureView_pb2 import ( FeatureViewMeta as FeatureViewMetaProto, @@ -36,15 +42,56 @@ from feast.protos.feast.core.FeatureView_pb2 import ( MaterializationInterval as MaterializationIntervalProto, ) -from feast.protos.feast.core.Transformation_pb2 import ( - FeatureTransformationV2 as FeatureTransformationProto, -) from feast.transformation.mode import TransformationMode from feast.types import from_value_type from feast.value_type import ValueType +from feast.version_utils import normalize_version_string warnings.simplefilter("once", DeprecationWarning) + +class FeatureViewState(enum.IntEnum): + """Lifecycle state of a feature view. + + Maps to the ``FeatureViewState`` proto enum defined in + ``protos/feast/core/FeatureView.proto``. + """ + + STATE_UNSPECIFIED = 0 + CREATED = 1 + GENERATED = 2 + MATERIALIZING = 3 + AVAILABLE_ONLINE = 4 + + def can_transition_to(self, target: "FeatureViewState") -> bool: + """Return True if transitioning from this state to *target* is allowed.""" + allowed = _VALID_STATE_TRANSITIONS.get(self, set()) + return target in allowed + + @classmethod + def from_proto(cls, proto_val: int) -> "FeatureViewState": + try: + return cls(proto_val) + except ValueError: + return cls.STATE_UNSPECIFIED + + def to_proto(self) -> int: + return self.value + + +# Valid state transitions: maps each state to the set of states it can move to. +_VALID_STATE_TRANSITIONS: dict[FeatureViewState, set[FeatureViewState]] = { + FeatureViewState.STATE_UNSPECIFIED: {FeatureViewState.CREATED}, + FeatureViewState.CREATED: {FeatureViewState.GENERATED}, + FeatureViewState.GENERATED: {FeatureViewState.MATERIALIZING}, + FeatureViewState.MATERIALIZING: { + FeatureViewState.AVAILABLE_ONLINE, + FeatureViewState.GENERATED, + }, + FeatureViewState.AVAILABLE_ONLINE: {FeatureViewState.MATERIALIZING}, +} + + # DUMMY_ENTITY is a placeholder entity used in entityless FeatureViews DUMMY_ENTITY_ID = "__dummy_id" DUMMY_ENTITY_NAME = "__dummy" @@ -71,9 +118,8 @@ class FeatureView(BaseFeatureView): ttl: The amount of time this group of features lives. A ttl of 0 indicates that this group of features lives forever. Note that large ttl's or a ttl of 0 can result in extremely computationally intensive queries. - batch_source: The batch source of data where this group of features - is stored. This is optional ONLY if a push source is specified as the - stream_source, since push sources contain their own batch sources. + batch_source: Optional batch source of data where this group of features + is stored. If no source is provided, this will be None. stream_source: The stream source of data where this group of features is stored. schema: The schema of the feature view, including feature, timestamp, and entity columns. If not specified, can be inferred from the underlying data source. @@ -87,6 +133,8 @@ class FeatureView(BaseFeatureView): tags: A dictionary of key-value pairs to store arbitrary metadata. owner: The owner of the feature view, typically the email of the primary maintainer. + org: The organizational unit that owns this feature view (e.g. "ads", "search"). + Defaults to empty string. mode: The transformation mode for feature transformations. Only meaningful when transformations are applied. Choose from TransformationMode enum values (e.g., PYTHON, PANDAS, RAY, SQL, SPARK, SUBSTRAIT). @@ -95,7 +143,7 @@ class FeatureView(BaseFeatureView): name: str entities: List[str] ttl: Optional[timedelta] - batch_source: DataSource + batch_source: Optional[DataSource] stream_source: Optional[DataSource] source_views: Optional[List["FeatureView"]] entity_columns: List[Field] @@ -105,14 +153,19 @@ class FeatureView(BaseFeatureView): description: str tags: Dict[str, str] owner: str + org: str materialization_intervals: List[Tuple[datetime, datetime]] mode: Optional[Union["TransformationMode", str]] + enable_validation: bool + enabled: bool + state: FeatureViewState + _raw_feature_transformation_proto: Optional[Message] = None def __init__( self, *, name: str, - source: Union[DataSource, "FeatureView", List["FeatureView"]], + source: Optional[Union[DataSource, "FeatureView", List["FeatureView"]]] = None, sink_source: Optional[DataSource] = None, schema: Optional[List[Field]] = None, entities: Optional[List[Entity]] = None, @@ -122,15 +175,20 @@ def __init__( description: str = "", tags: Optional[Dict[str, str]] = None, owner: str = "", + org: str = "", mode: Optional[Union["TransformationMode", str]] = None, + enable_validation: bool = False, + version: str = "latest", + enabled: bool = True, ): """ Creates a FeatureView object. Args: name: The unique name of the feature view. - source: The source of data for this group of features. May be a stream source, or a batch source. - If a stream source, the source should contain a batch_source for backfills & batch materialization. + source (optional): The source of data for this group of features. May be a stream source, + a batch source, a FeatureView, or a list of FeatureViews. If None, the feature view + has no associated data source. schema (optional): The schema of the feature view, including feature, timestamp, and entity columns. # TODO: clarify that schema is only useful here... @@ -146,13 +204,23 @@ def __init__( tags (optional): A dictionary of key-value pairs to store arbitrary metadata. owner (optional): The owner of the feature view, typically the email of the primary maintainer. + org (optional): The organizational unit that owns this feature view + (e.g. "ads", "search"). mode (optional): The transformation mode for feature transformations. Only meaningful when transformations are applied. Choose from TransformationMode enum values. + enable_validation (optional): If True, enables schema validation during materialization + to check that data conforms to the declared feature types. Default is False. + version (optional): Version string for definition management. Controls which historical + snapshot is active after ``feast apply``. Only one version can be active per feature + view name per project. For concurrent multi-version testing, use separate projects + or distinct feature view names. Default is "latest". Raises: ValueError: A field mapping conflicts with an Entity or a Feature. """ self.name = name + self.version = version + self.enable_validation = enable_validation self.entities = [e.name for e in entities] if entities else [DUMMY_ENTITY_NAME] self.ttl = ttl schema = schema or [] @@ -163,7 +231,9 @@ def __init__( self.data_source: Optional[DataSource] = None self.source_views: List[FeatureView] = [] - if isinstance(source, DataSource): + if source is None: + pass # data_source remains None, source_views remains [] + elif isinstance(source, DataSource): self.data_source = source elif isinstance(source, FeatureView): self.source_views = [source] @@ -192,11 +262,14 @@ def __init__( elif self.data_source: # Batch source definition self.batch_source = self.data_source - else: + elif self.source_views: # Derived view source definition if not sink_source: raise ValueError("Derived FeatureView must specify `sink_source`.") self.batch_source = sink_source + else: + # source=None - no batch source + self.batch_source = None # Initialize features and entity columns. features: List[Field] = [] @@ -259,10 +332,13 @@ def __init__( owner=owner, source=self.batch_source, ) + self.org = org self.online = online self.offline = offline self.mode = mode self.materialization_intervals = [] + self.enabled = enabled + self.state = FeatureViewState.STATE_UNSPECIFIED def __hash__(self): return super().__hash__() @@ -279,6 +355,12 @@ def __copy__(self): online=self.online, offline=self.offline, sink_source=self.batch_source if self.source_views else None, + enable_validation=self.enable_validation, + version=self.version, + description=self.description, + owner=self.owner, + org=self.org, + enabled=self.enabled, ) # This is deliberately set outside of the FV initialization as we do not have the Entity objects. @@ -286,13 +368,34 @@ def __copy__(self): fv.features = copy.copy(self.features) fv.entity_columns = copy.copy(self.entity_columns) fv.projection = copy.copy(self.projection) + fv.state = self.state return fv + def _schema_or_udf_changed(self, other: "BaseFeatureView") -> bool: + """Check for FeatureView schema/UDF changes.""" + if super()._schema_or_udf_changed(other): + return True + + if not isinstance(other, FeatureView): + return True + + # Schema-related fields + if sorted(self.entities) != sorted(other.entities): + return True + if sorted(self.entity_columns) != sorted(other.entity_columns): + return True + if self.source_views != other.source_views: + return True + + # Skip UDF-related data source fields: batch_source, stream_source + # (treat as deployment configuration, not schema changes) + # Skip configuration: ttl, online, offline, enable_validation + # Skip metadata: materialization_intervals (excluded in current equality) + return False + def __eq__(self, other): if not isinstance(other, FeatureView): - raise TypeError( - "Comparisons should only involve FeatureView class objects." - ) + return False if not super().__eq__(other): return False @@ -307,6 +410,10 @@ def __eq__(self, other): or sorted(self.entity_columns) != sorted(other.entity_columns) or self.source_views != other.source_views or self.materialization_intervals != other.materialization_intervals + or self.enable_validation != other.enable_validation + or normalize_version_string(self.version) + != normalize_version_string(other.version) + or self.org != other.org ): return False @@ -414,15 +521,9 @@ def to_proto_spec( ) -> FeatureViewSpecProto: ttl_duration = self.get_ttl_duration() - batch_source_proto = None - if self.batch_source: - batch_source_proto = self.batch_source.to_proto() - batch_source_proto.data_source_class_type = f"{self.batch_source.__class__.__module__}.{self.batch_source.__class__.__name__}" + batch_source_proto = serialize_data_source(self.batch_source) + stream_source_proto = serialize_data_source(self.stream_source) - stream_source_proto = None - if self.stream_source: - stream_source_proto = self.stream_source.to_proto() - stream_source_proto.data_source_class_type = f"{self.stream_source.__class__.__module__}.{self.stream_source.__class__.__name__}" source_view_protos = None if self.source_views: source_view_protos = [ @@ -430,31 +531,11 @@ def to_proto_spec( ] feature_transformation_proto = None - if hasattr(self, "feature_transformation") and self.feature_transformation: - from feast.protos.feast.core.Transformation_pb2 import ( - SubstraitTransformationV2 as SubstraitTransformationProto, - ) - from feast.protos.feast.core.Transformation_pb2 import ( - UserDefinedFunctionV2 as UserDefinedFunctionProto, - ) - - transformation_proto = self.feature_transformation.to_proto() - - if isinstance(transformation_proto, UserDefinedFunctionProto): - feature_transformation_proto = FeatureTransformationProto( - user_defined_function=transformation_proto, - ) - elif isinstance(transformation_proto, SubstraitTransformationProto): - feature_transformation_proto = FeatureTransformationProto( - substrait_transformation=transformation_proto, - ) - - mode_str = "" - if self.mode: - mode_str = ( - self.mode.value - if isinstance(self.mode, TransformationMode) - else self.mode + if getattr(self, "_raw_feature_transformation_proto", None) is not None: + feature_transformation_proto = self._raw_feature_transformation_proto + elif hasattr(self, "feature_transformation") and self.feature_transformation: + feature_transformation_proto = transformation_to_proto( + self.feature_transformation ) return FeatureViewSpecProto( @@ -465,6 +546,7 @@ def to_proto_spec( description=self.description, tags=self.tags, owner=self.owner, + org=self.org, ttl=(ttl_duration if ttl_duration is not None else None), online=self.online, offline=self.offline, @@ -472,7 +554,10 @@ def to_proto_spec( stream_source=stream_source_proto, source_views=source_view_protos, feature_transformation=feature_transformation_proto, - mode=mode_str, + mode=mode_to_string(self.mode), + enable_validation=self.enable_validation, + version=self.version, + disabled=not self.enabled, ) def to_proto_meta(self): @@ -486,6 +571,10 @@ def to_proto_meta(self): interval_proto.start_time.FromDatetime(interval[0]) interval_proto.end_time.FromDatetime(interval[1]) meta.materialization_intervals.append(interval_proto) + if self.current_version_number is not None: + meta.current_version_number = self.current_version_number + if self.state != FeatureViewState.STATE_UNSPECIFIED: + meta.state = self.state.to_proto() return meta def get_ttl_duration(self): @@ -496,14 +585,17 @@ def get_ttl_duration(self): return ttl_duration @classmethod - def from_proto(cls, feature_view_proto: FeatureViewProto) -> "FeatureView": - return cls._from_proto_internal(feature_view_proto, seen={}) + def from_proto( + cls, feature_view_proto: FeatureViewProto, skip_udf: bool = False + ) -> "FeatureView": + return cls._from_proto_internal(feature_view_proto, seen={}, skip_udf=skip_udf) @classmethod def _from_proto_internal( cls, feature_view_proto: FeatureViewProto, seen: Dict[str, Union[None, "FeatureView"]], + skip_udf: bool = False, ) -> "FeatureView": """ Creates a feature view from a protobuf representation of a feature view. @@ -537,7 +629,7 @@ def _from_proto_internal( ) source_views = [ FeatureView._from_proto_internal( - FeatureViewProto(spec=view_spec, meta=None), seen + FeatureViewProto(spec=view_spec, meta=None), seen, skip_udf=skip_udf ) for view_spec in feature_view_proto.spec.source_views ] @@ -557,22 +649,23 @@ def _from_proto_internal( ) transformation = None - if feature_transformation_proto.HasField("user_defined_function"): - udf_proto = feature_transformation_proto.user_defined_function - if udf_proto.mode: - try: - transformation_class = get_transformation_class_from_type( - udf_proto.mode - ) - transformation = transformation_class.from_proto(udf_proto) - except (ValueError, KeyError): + if not skip_udf: + if feature_transformation_proto.HasField("user_defined_function"): + udf_proto = feature_transformation_proto.user_defined_function + if udf_proto.mode: + try: + transformation_class = get_transformation_class_from_type( + udf_proto.mode + ) + transformation = transformation_class.from_proto(udf_proto) + except (ValueError, KeyError): + transformation = PythonTransformation.from_proto(udf_proto) + else: transformation = PythonTransformation.from_proto(udf_proto) - else: - transformation = PythonTransformation.from_proto(udf_proto) - elif feature_transformation_proto.HasField("substrait_transformation"): - transformation = SubstraitTransformation.from_proto( - feature_transformation_proto.substrait_transformation - ) + elif feature_transformation_proto.HasField("substrait_transformation"): + transformation = SubstraitTransformation.from_proto( + feature_transformation_proto.substrait_transformation + ) mode: Union[TransformationMode, str] if feature_view_proto.spec.mode: @@ -587,6 +680,7 @@ def _from_proto_internal( description=feature_view_proto.spec.description, tags=dict(feature_view_proto.spec.tags), owner=feature_view_proto.spec.owner, + org=feature_view_proto.spec.org, online=feature_view_proto.spec.online, offline=feature_view_proto.spec.offline, ttl=( @@ -597,8 +691,14 @@ def _from_proto_internal( source=source_views if source_views else batch_source, # type: ignore[arg-type] sink_source=batch_source if source_views else None, mode=mode, - feature_transformation=transformation, + feature_transformation=transformation + if not skip_udf + else feature_transformation_proto, # type: ignore[arg-type] ) + if skip_udf: + feature_view._raw_feature_transformation_proto = ( + feature_transformation_proto + ) else: mode_from_spec = ( feature_view_proto.spec.mode if feature_view_proto.spec.mode else None @@ -609,6 +709,7 @@ def _from_proto_internal( description=feature_view_proto.spec.description, tags=dict(feature_view_proto.spec.tags), owner=feature_view_proto.spec.owner, + org=feature_view_proto.spec.org, online=feature_view_proto.spec.online, offline=feature_view_proto.spec.offline, ttl=( @@ -642,6 +743,28 @@ def _from_proto_internal( f"Entities: {feature_view.entities} vs Entity Columns: {feature_view.entity_columns}" ) + # Restore enable_validation from proto field. + feature_view.enable_validation = feature_view_proto.spec.enable_validation + + # Restore enabled from proto's inverted 'disabled' field. + # Proto bool defaults to False, so old protos without this field + # will correctly default to enabled=True. + feature_view.enabled = not feature_view_proto.spec.disabled + + # Restore lifecycle state from meta. + feature_view.state = FeatureViewState.from_proto(feature_view_proto.meta.state) + + # Restore version fields. + spec_version = feature_view_proto.spec.version + feature_view.version = spec_version or "latest" + cvn = feature_view_proto.meta.current_version_number + if cvn > 0: + feature_view.current_version_number = cvn + elif cvn == 0 and spec_version and spec_version.lower() != "latest": + feature_view.current_version_number = 0 + else: + feature_view.current_version_number = None + # FeatureViewProjections are not saved in the FeatureView proto. # Create the default projection. feature_view.projection = FeatureViewProjection.from_feature_view_definition( diff --git a/sdk/python/feast/feature_view_projection.py b/sdk/python/feast/feature_view_projection.py index 70415e9ed3a..a91c6498604 100644 --- a/sdk/python/feast/feature_view_projection.py +++ b/sdk/python/feast/feature_view_projection.py @@ -47,9 +47,14 @@ class FeatureViewProjection: date_partition_column: Optional[str] = None created_timestamp_column: Optional[str] = None batch_source: Optional[DataSource] = None + version_tag: Optional[int] = None + view_type: str = "featureView" def name_to_use(self): - return self.name_alias or self.name + base = self.name_alias or self.name + if self.version_tag is not None: + return f"{base}@v{self.version_tag}" + return base def to_proto(self) -> FeatureViewProjectionProto: batch_source = None @@ -66,10 +71,14 @@ def to_proto(self) -> FeatureViewProjectionProto: date_partition_column=self.date_partition_column or "", created_timestamp_column=self.created_timestamp_column or "", batch_source=batch_source, + view_type=getattr(self, "view_type", "") or "", ) for feature in self.features: feature_reference_proto.feature_columns.append(feature.to_proto()) + if self.version_tag is not None: + feature_reference_proto.version_tag = self.version_tag + return feature_reference_proto @staticmethod @@ -93,24 +102,27 @@ def from_proto(proto: FeatureViewProjectionProto) -> "FeatureViewProjection": for feature_column in proto.feature_columns: feature_view_projection.features.append(Field.from_proto(feature_column)) + if proto.HasField("version_tag"): + feature_view_projection.version_tag = proto.version_tag + + feature_view_projection.view_type = proto.view_type or "featureView" + return feature_view_projection @staticmethod def from_feature_view_definition(feature_view: "FeatureView"): # TODO need to implement this for StreamFeatureViews - if getattr(feature_view, "batch_source", None): + batch_source = getattr(feature_view, "batch_source", None) + if batch_source: return FeatureViewProjection( name=feature_view.name, name_alias=None, features=feature_view.features, desired_features=[], - timestamp_field=feature_view.batch_source.created_timestamp_column - or None, - created_timestamp_column=feature_view.batch_source.created_timestamp_column - or None, - date_partition_column=feature_view.batch_source.date_partition_column - or None, - batch_source=feature_view.batch_source or None, + timestamp_field=batch_source.created_timestamp_column or None, + created_timestamp_column=batch_source.created_timestamp_column or None, + date_partition_column=batch_source.date_partition_column or None, + batch_source=batch_source or None, ) else: return FeatureViewProjection( diff --git a/sdk/python/feast/feature_view_utils.py b/sdk/python/feast/feature_view_utils.py index daf28e09dec..0b599f4777c 100644 --- a/sdk/python/feast/feature_view_utils.py +++ b/sdk/python/feast/feature_view_utils.py @@ -5,16 +5,29 @@ import logging import typing from dataclasses import dataclass -from typing import Callable, Optional +from typing import Callable, Optional, Union + +from feast.errors import ( + FeastObjectNotFoundException, + FeatureViewNotFoundException, + OnDemandFeatureViewNotFoundException, +) if typing.TYPE_CHECKING: from feast.data_source import DataSource + from feast.feature_store import FeatureStore from feast.feature_view import FeatureView + from feast.infra.registry.base_registry import BaseRegistry + from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import RepoConfig + from feast.stream_feature_view import StreamFeatureView logger = logging.getLogger(__name__) +FeatureViewLike = Union["FeatureView", "OnDemandFeatureView", "StreamFeatureView"] + + @dataclass class FeatureViewSourceInfo: """Information about a feature view's data source resolution.""" @@ -123,6 +136,8 @@ def resolve_feature_view_source( if not is_derived_view: # Regular feature view - use its batch_source directly + if feature_view.batch_source is None: + raise ValueError(f"Feature view '{feature_view.name}' has no batch_source.") return FeatureViewSourceInfo( data_source=feature_view.batch_source, source_type="batch_source", @@ -165,8 +180,13 @@ def resolve_feature_view_source( if hasattr(parent_view, "source_views") and parent_view.source_views: # Parent is also a derived view - recursively find original source original_source_view = find_original_source_view(parent_view) + original_batch_source = original_source_view.batch_source + if original_batch_source is None: + raise ValueError( + f"Original source view '{original_source_view.name}' has no batch_source." + ) return FeatureViewSourceInfo( - data_source=original_source_view.batch_source, + data_source=original_batch_source, source_type="original_source", has_transformation=view_has_transformation, transformation_func=transformation_func, @@ -216,8 +236,13 @@ def resolve_feature_view_source_with_fallback( elif hasattr(feature_view, "source_views") and feature_view.source_views: # Try the original source view as last resort original_view = find_original_source_view(feature_view) + original_view_batch_source = original_view.batch_source + if original_view_batch_source is None: + raise ValueError( + f"Original source view '{original_view.name}' has no batch_source." + ) return FeatureViewSourceInfo( - data_source=original_view.batch_source, + data_source=original_view_batch_source, source_type="fallback_original_source", has_transformation=has_transformation(feature_view), transformation_func=get_transformation_function(feature_view), @@ -227,3 +252,50 @@ def resolve_feature_view_source_with_fallback( raise ValueError( f"Unable to resolve any data source for feature view {feature_view.name}" ) + + +def get_feature_view_from_feature_store( + store: "FeatureStore", + name: str, + allow_registry_cache: bool = False, +) -> FeatureViewLike: + try: + return store.get_feature_view(name, allow_registry_cache=allow_registry_cache) + except FeatureViewNotFoundException: + try: + return store.get_on_demand_feature_view( + name, allow_registry_cache=allow_registry_cache + ) + except (FeatureViewNotFoundException, OnDemandFeatureViewNotFoundException): + try: + return store.get_stream_feature_view( + name, allow_registry_cache=allow_registry_cache + ) + except FeatureViewNotFoundException as e: + raise FeastObjectNotFoundException( + f"Can't recognize feast object with a name {name}" + ) from e + + +def get_feature_view_from_registry( + registry: "BaseRegistry", + name: str, + project: str, + allow_cache: bool = False, +) -> FeatureViewLike: + try: + return registry.get_feature_view(name, project, allow_cache=allow_cache) + except FeatureViewNotFoundException: + try: + return registry.get_on_demand_feature_view( + name, project, allow_cache=allow_cache + ) + except (FeatureViewNotFoundException, OnDemandFeatureViewNotFoundException): + try: + return registry.get_stream_feature_view( + name, project, allow_cache=allow_cache + ) + except FeatureViewNotFoundException as e: + raise FeastObjectNotFoundException( + f"Can't recognize feast object with a name {name}" + ) from e diff --git a/sdk/python/feast/field.py b/sdk/python/feast/field.py index 27552878afc..3055bd70830 100644 --- a/sdk/python/feast/field.py +++ b/sdk/python/feast/field.py @@ -12,15 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json from typing import Dict, Optional from typeguard import typechecked from feast.feature import Feature from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 as FieldProto -from feast.types import FeastType, from_value_type +from feast.types import FeastType, Struct, from_value_type from feast.value_type import ValueType +STRUCT_SCHEMA_TAG = "feast:struct_schema" +NESTED_COLLECTION_INNER_TYPE_TAG = "feast:nested_inner_type" + @typechecked class Field: @@ -115,13 +119,26 @@ def __str__(self): def to_proto(self) -> FieldProto: """Converts a Field object to its protobuf representation.""" + from feast.types import Array, Set + value_type = self.dtype.to_value_type() vector_search_metric = self.vector_search_metric or "" + tags = dict(self.tags) + # Persist Struct field schema in tags + if isinstance(self.dtype, Struct): + tags[STRUCT_SCHEMA_TAG] = _serialize_struct_schema(self.dtype) + elif isinstance(self.dtype, Array) and isinstance(self.dtype.base_type, Struct): + tags[STRUCT_SCHEMA_TAG] = _serialize_struct_schema(self.dtype.base_type) + # Persist nested collection type info in tags + if isinstance(self.dtype, (Array, Set)) and isinstance( + self.dtype.base_type, (Array, Set) + ): + tags[NESTED_COLLECTION_INNER_TYPE_TAG] = _feast_type_to_str(self.dtype) return FieldProto( name=self.name, - value_type=value_type.value, + value_type=value_type.value, # type: ignore[arg-type] description=self.description, - tags=self.tags, + tags=tags, vector_index=self.vector_index, vector_length=self.vector_length, vector_search_metric=vector_search_metric, @@ -136,13 +153,37 @@ def from_proto(cls, field_proto: FieldProto): field_proto: FieldProto protobuf object """ value_type = ValueType(field_proto.value_type) + tags = dict(field_proto.tags) vector_search_metric = getattr(field_proto, "vector_search_metric", "") vector_index = getattr(field_proto, "vector_index", False) vector_length = getattr(field_proto, "vector_length", 0) + + # Reconstruct Struct type from persisted schema in tags + from feast.types import Array + + internal_tags = {STRUCT_SCHEMA_TAG, NESTED_COLLECTION_INNER_TYPE_TAG} + dtype: FeastType + if value_type == ValueType.STRUCT and STRUCT_SCHEMA_TAG in tags: + dtype = _deserialize_struct_schema(tags[STRUCT_SCHEMA_TAG]) + user_tags = {k: v for k, v in tags.items() if k not in internal_tags} + elif value_type == ValueType.STRUCT_LIST and STRUCT_SCHEMA_TAG in tags: + inner_struct = _deserialize_struct_schema(tags[STRUCT_SCHEMA_TAG]) + dtype = Array(inner_struct) + user_tags = {k: v for k, v in tags.items() if k not in internal_tags} + elif ( + value_type in (ValueType.VALUE_LIST, ValueType.VALUE_SET) + and NESTED_COLLECTION_INNER_TYPE_TAG in tags + ): + dtype = _str_to_feast_type(tags[NESTED_COLLECTION_INNER_TYPE_TAG]) + user_tags = {k: v for k, v in tags.items() if k not in internal_tags} + else: + dtype = from_value_type(value_type=value_type) + user_tags = {k: v for k, v in tags.items() if k not in internal_tags} + return cls( name=field_proto.name, - dtype=from_value_type(value_type=value_type), - tags=dict(field_proto.tags), + dtype=dtype, + tags=user_tags, description=field_proto.description, vector_index=vector_index, vector_length=vector_length, @@ -163,3 +204,86 @@ def from_feature(cls, feature: Feature): description=feature.description, tags=feature.labels, ) + + +def _feast_type_to_str(feast_type: FeastType) -> str: + """Convert a FeastType to a string representation for serialization.""" + from feast.types import ( + Array, + PrimitiveFeastType, + Set, + ) + + if isinstance(feast_type, PrimitiveFeastType): + return feast_type.name + elif isinstance(feast_type, Struct): + nested = { + name: _feast_type_to_str(ft) for name, ft in feast_type.fields.items() + } + return json.dumps({"__struct__": nested}) + elif isinstance(feast_type, Array): + return f"Array({_feast_type_to_str(feast_type.base_type)})" + elif isinstance(feast_type, Set): + return f"Set({_feast_type_to_str(feast_type.base_type)})" + else: + return str(feast_type) + + +def _str_to_feast_type(type_str: str) -> FeastType: + """Convert a string representation back to a FeastType.""" + from feast.types import ( + Array, + PrimitiveFeastType, + Set, + ) + + # Check if it's an Array type + if type_str.startswith("Array(") and type_str.endswith(")"): + inner = type_str[6:-1] + base_type = _str_to_feast_type(inner) + return Array(base_type) + + # Check if it's a Set type + if type_str.startswith("Set(") and type_str.endswith(")"): + inner = type_str[4:-1] + base_type = _str_to_feast_type(inner) + return Set(base_type) + + # Check if it's a nested Struct (JSON encoded) + if type_str.startswith("{"): + try: + parsed = json.loads(type_str) + if "__struct__" in parsed: + fields = { + name: _str_to_feast_type(ft_str) + for name, ft_str in parsed["__struct__"].items() + } + return Struct(fields) + except (json.JSONDecodeError, TypeError): + pass + + # Must be a PrimitiveFeastType name + try: + return PrimitiveFeastType[type_str] + except KeyError: + raise ValueError( + f"Unknown FeastType: {type_str!r}. " + f"Valid primitive types: {[t.name for t in PrimitiveFeastType]}" + ) + + +def _serialize_struct_schema(struct_type: Struct) -> str: + """Serialize a Struct's field schema to a JSON string for tag storage.""" + schema_dict = {} + for name, feast_type in struct_type.fields.items(): + schema_dict[name] = _feast_type_to_str(feast_type) + return json.dumps(schema_dict) + + +def _deserialize_struct_schema(schema_str: str) -> Struct: + """Deserialize a JSON string from tags back to a Struct type.""" + schema_dict = json.loads(schema_str) + fields = {} + for name, type_str in schema_dict.items(): + fields[name] = _str_to_feast_type(type_str) + return Struct(fields) diff --git a/sdk/python/feast/filter_models.py b/sdk/python/feast/filter_models.py new file mode 100644 index 00000000000..4b98d0034f9 --- /dev/null +++ b/sdk/python/feast/filter_models.py @@ -0,0 +1,101 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Literal, Optional, Union + +from pydantic import BaseModel + + +class ComparisonFilter(BaseModel): + """A filter that compares a metadata field against a value. + + :param type: The comparison operator to apply + :param key: The metadata field name to filter on + :param value: The value to compare against + """ + + type: Literal["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"] + key: str + value: Any + + +class CompoundFilter(BaseModel): + """A filter that combines multiple filters with a logical operator. + + :param type: The logical operator ("and" requires all filters match, + "or" requires any filter matches) + :param filters: The list of filters to combine + """ + + type: Literal["and", "or"] + filters: List[Union[ComparisonFilter, "CompoundFilter"]] + + +CompoundFilter.model_rebuild() + +FilterType = Optional[Union[ComparisonFilter, CompoundFilter]] + + +class FilterTranslator(ABC): + """Abstract base for translating Feast filters into backend-native expressions. + + Each online store backend provides a concrete subclass that knows how to + turn :class:`ComparisonFilter` / :class:`CompoundFilter` trees into the + query language understood by the underlying database or search engine. + + The :meth:`translate` entry-point handles ``None`` via :meth:`_empty` and + delegates to :meth:`translate_comparison` / :meth:`translate_compound` + through the shared :meth:`_dispatch` recursion, so subclasses only need to + implement the three abstract leaf methods. + """ + + @abstractmethod + def translate(self, filters: FilterType) -> Any: + """Return the backend-native filter expression.""" + ... + + def _dispatch(self, filter_obj: Union[ComparisonFilter, CompoundFilter]) -> Any: + """Route to comparison or compound handler (used for recursion).""" + if isinstance(filter_obj, ComparisonFilter): + return self.translate_comparison(filter_obj) + elif isinstance(filter_obj, CompoundFilter): + return self.translate_compound(filter_obj) + raise ValueError(f"Unknown filter type: {type(filter_obj)}") + + @abstractmethod + def translate_comparison(self, f: ComparisonFilter) -> Any: + """Translate a single comparison into a backend-native expression.""" + ... + + @abstractmethod + def translate_compound(self, f: CompoundFilter) -> Any: + """Translate a compound (AND/OR) filter into a backend-native expression.""" + ... + + +def filters_contain_numeric_comparison( + filter_obj: Union[ComparisonFilter, CompoundFilter], +) -> bool: + """Return True if any leaf comparison uses a numeric value.""" + if isinstance(filter_obj, ComparisonFilter): + return isinstance(filter_obj.value, (int, float)) and not isinstance( + filter_obj.value, bool + ) + if isinstance(filter_obj, CompoundFilter): + return any(filters_contain_numeric_comparison(f) for f in filter_obj.filters) + return False + + +def convert_dict_to_filter( + filter_dict: Dict[str, Any], +) -> Union[ComparisonFilter, CompoundFilter]: + """Convert a raw dict (e.g. from OpenAI-compatible JSON) into a typed filter object.""" + filter_type = filter_dict.get("type") + if filter_type in ("and", "or"): + return CompoundFilter( + type=filter_type, + filters=[convert_dict_to_filter(f) for f in filter_dict["filters"]], + ) + return ComparisonFilter( + type=filter_dict["type"], + key=filter_dict["key"], + value=filter_dict["value"], + ) diff --git a/sdk/python/feast/inference.py b/sdk/python/feast/inference.py index f5f234b7301..16023e3dac6 100644 --- a/sdk/python/feast/inference.py +++ b/sdk/python/feast/inference.py @@ -221,6 +221,9 @@ def _infer_features_and_entities( fv, join_keys, run_inference_for_features, config ) + if fv.batch_source is None: + return + entity_columns: List[Field] = fv.entity_columns if fv.entity_columns else [] columns_to_exclude = { fv.batch_source.timestamp_field, diff --git a/sdk/python/feast/infra/common/serde.py b/sdk/python/feast/infra/common/serde.py index 90e1be9234e..572b3db0bec 100644 --- a/sdk/python/feast/infra/common/serde.py +++ b/sdk/python/feast/infra/common/serde.py @@ -30,7 +30,8 @@ def unserialize(self): # unserialize proto = FeatureViewProto() proto.ParseFromString(self.feature_view_proto) - feature_view = FeatureView.from_proto(proto) + # skip_udf=True: the write node only needs schema / entity metadata. + feature_view = FeatureView.from_proto(proto, skip_udf=True) # load repo_config = dill.loads(self.repo_config_byte) diff --git a/sdk/python/feast/infra/compute_engines/aws_lambda/Dockerfile b/sdk/python/feast/infra/compute_engines/aws_lambda/Dockerfile index bbdb74bdfe1..b10325be9de 100644 --- a/sdk/python/feast/infra/compute_engines/aws_lambda/Dockerfile +++ b/sdk/python/feast/infra/compute_engines/aws_lambda/Dockerfile @@ -2,6 +2,7 @@ FROM public.ecr.aws/lambda/python:3.9 RUN yum install -y git +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv # Copy app handler code COPY sdk/python/feast/infra/materialization/lambda/app.py ${LAMBDA_TASK_ROOT} @@ -10,7 +11,6 @@ COPY sdk/python/feast/infra/materialization/lambda/app.py ${LAMBDA_TASK_ROOT} COPY sdk/python sdk/python COPY protos protos COPY go go -COPY setup.py setup.py COPY pyproject.toml pyproject.toml COPY README.md README.md @@ -19,7 +19,7 @@ COPY README.md README.md # git dir to infer the version of feast we're installing. # https://github.com/pypa/setuptools_scm#usage-from-docker # I think it also assumes that this dockerfile is being built from the root of the directory. -RUN --mount=source=.git,target=.git,type=bind pip3 install --no-cache-dir -e '.[aws,redis]' +RUN --mount=source=.git,target=.git,type=bind uv pip install --system --no-cache-dir -e '.[aws,redis]' # Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile) CMD [ "app.handler" ] diff --git a/infra/feast-operator/test/e2e_rhoai/resources/feature_repo/__init__.py b/sdk/python/feast/infra/compute_engines/aws_lambda/__init__.py similarity index 100% rename from infra/feast-operator/test/e2e_rhoai/resources/feature_repo/__init__.py rename to sdk/python/feast/infra/compute_engines/aws_lambda/__init__.py diff --git a/sdk/python/feast/infra/compute_engines/aws_lambda/app.py b/sdk/python/feast/infra/compute_engines/aws_lambda/app.py index 2bf65542e55..98760651dc0 100644 --- a/sdk/python/feast/infra/compute_engines/aws_lambda/app.py +++ b/sdk/python/feast/infra/compute_engines/aws_lambda/app.py @@ -1,26 +1,31 @@ import base64 -import json -import sys +import logging import tempfile -import traceback from pathlib import Path import pyarrow.parquet as pq from feast import FeatureStore from feast.constants import FEATURE_STORE_YAML_ENV_NAME -from feast.infra.materialization.local_engine import DEFAULT_BATCH_SIZE +from feast.infra.compute_engines.aws_lambda.lambda_engine import DEFAULT_BATCH_SIZE from feast.utils import _convert_arrow_to_proto, _run_pyarrow_field_mapping +logger = logging.getLogger() +logger.setLevel("INFO") -def handler(event, context): - """Provide an event that contains the following keys: - - operation: one of the operations in the operations dict below - - tableName: required for operations that interact with DynamoDB - - payload: a parameter to pass to the operation being performed +def handler(event, context): + """Load a parquet file and write the feature values to the online store. + + Args: + event (dict): payload containing the following keys: + FEATURE_STORE_YAML_ENV_NAME: Base64 encoded feature store config + view_name: Name of FeatureView to be materialized + view_type: Type of FeatureView + path: Path to parquet batch file on S3 bucket + context (dict): Lambda runtime context, not used. """ - print("Received event: " + json.dumps(event, indent=2), flush=True) + logger.info(f"Received event: {event}") try: config_base64 = event[FEATURE_STORE_YAML_ENV_NAME] @@ -28,57 +33,61 @@ def handler(event, context): config_bytes = base64.b64decode(config_base64) # Create a new unique directory for writing feature_store.yaml - repo_path = Path(tempfile.mkdtemp()) - - with open(repo_path / "feature_store.yaml", "wb") as f: - f.write(config_bytes) + with tempfile.TemporaryDirectory() as repo_posix_path: + repo_path = Path(repo_posix_path) - # Initialize the feature store - store = FeatureStore(repo_path=str(repo_path.resolve())) + with open(repo_path / "feature_store.yaml", "wb") as f: + f.write(config_bytes) - view_name = event["view_name"] - view_type = event["view_type"] - path = event["path"] + # Initialize the feature store + store = FeatureStore(repo_path=str(repo_path.resolve())) - bucket = path[len("s3://") :].split("/", 1)[0] - key = path[len("s3://") :].split("/", 1)[1] - print(f"Inferred Bucket: `{bucket}` Key: `{key}`", flush=True) + view_name = event["view_name"] + view_type = event["view_type"] + path = event["path"] - if view_type == "batch": - # TODO: This probably needs to be become `store.get_batch_feature_view` at some point. - feature_view = store.get_feature_view(view_name) - else: - feature_view = store.get_stream_feature_view(view_name) + bucket, key = path[len("s3://") :].split("/", 1) + logger.info(f"Inferred Bucket: `{bucket}` Key: `{key}`") - print(f"Got Feature View: `{feature_view}`", flush=True) + if view_type == "batch": + # TODO: This probably needs to be become `store.get_batch_feature_view` at some point. # noqa: E501,W505 + feature_view = store.get_feature_view(view_name) + else: + feature_view = store.get_stream_feature_view(view_name) - table = pq.read_table(path) - if feature_view.batch_source.field_mapping is not None: - table = _run_pyarrow_field_mapping( - table, feature_view.batch_source.field_mapping + logger.info( + f"Got Feature View: `{feature_view.name}`, \ + last updated: {feature_view.last_updated_timestamp}" ) - join_key_to_value_type = { - entity.name: entity.dtype.to_value_type() - for entity in feature_view.entity_columns - } - - written_rows = 0 - - for batch in table.to_batches(DEFAULT_BATCH_SIZE): - rows_to_write = _convert_arrow_to_proto( - batch, feature_view, join_key_to_value_type - ) - store._provider.online_write_batch( - store.config, - feature_view, - rows_to_write, - lambda x: None, + table = pq.read_table(path) + if feature_view.batch_source.field_mapping is not None: + table = _run_pyarrow_field_mapping( + table, feature_view.batch_source.field_mapping + ) + + join_key_to_value_type = { + entity.name: entity.dtype.to_value_type() + for entity in feature_view.entity_columns + } + + written_rows = 0 + + for batch in table.to_batches(DEFAULT_BATCH_SIZE): + rows_to_write = _convert_arrow_to_proto( + batch, feature_view, join_key_to_value_type + ) + store._provider.online_write_batch( + store.config, + feature_view, + rows_to_write, + lambda x: None, + ) + written_rows += len(rows_to_write) + logger.info( + f"Successfully updated {written_rows} rows.", + extra={"num_updated_rows": written_rows, "feature_view": view_name}, ) - written_rows += len(rows_to_write) - return {"written_rows": written_rows} - except Exception as e: - print(f"Exception: {e}", flush=True) - print("Traceback:", flush=True) - print(traceback.format_exc(), flush=True) - sys.exit(1) + except Exception: + logger.exception("Error in processing materialization.") + raise diff --git a/sdk/python/feast/infra/compute_engines/aws_lambda/lambda_engine.py b/sdk/python/feast/infra/compute_engines/aws_lambda/lambda_engine.py index cc32e5b74b3..b223328893d 100644 --- a/sdk/python/feast/infra/compute_engines/aws_lambda/lambda_engine.py +++ b/sdk/python/feast/infra/compute_engines/aws_lambda/lambda_engine.py @@ -108,9 +108,14 @@ def update( r = self.lambda_client.create_function( FunctionName=self.lambda_name, PackageType="Image", - Role=self.repo_config.batch_engine.lambda_role, - Code={"ImageUri": self.repo_config.batch_engine.materialization_image}, + Role=self.repo_config.batch_engine_config.lambda_role, + Code={ + "ImageUri": self.repo_config.batch_engine_config.materialization_image + }, Timeout=DEFAULT_TIMEOUT, + LoggingConfig={ + "LogFormat": "JSON", + }, Tags={ "feast-owned": "True", "project": project, @@ -188,7 +193,7 @@ def _materialize_one( offline_job = self.offline_store.pull_latest_from_table_or_query( config=self.repo_config, - data_source=feature_view.batch_source, + data_source=feature_view.batch_source, # type: ignore[arg-type] join_key_columns=join_key_columns, feature_name_columns=feature_name_columns, timestamp_field=timestamp_field, diff --git a/sdk/python/feast/infra/compute_engines/backends/factory.py b/sdk/python/feast/infra/compute_engines/backends/factory.py index ffe969f3003..91ece19b329 100644 --- a/sdk/python/feast/infra/compute_engines/backends/factory.py +++ b/sdk/python/feast/infra/compute_engines/backends/factory.py @@ -24,9 +24,10 @@ def from_name(name: str) -> DataFrameBackend: @staticmethod def infer_from_entity_df(entity_df) -> Optional[DataFrameBackend]: if ( - not entity_df - or isinstance(entity_df, pyarrow.Table) + entity_df is None or isinstance(entity_df, pd.DataFrame) + or isinstance(entity_df, pyarrow.Table) + or (isinstance(entity_df, str) and not entity_df) ): return PandasBackend() diff --git a/sdk/python/feast/infra/compute_engines/backends/polars_backend.py b/sdk/python/feast/infra/compute_engines/backends/polars_backend.py index 92e348c3652..118b0b8eab7 100644 --- a/sdk/python/feast/infra/compute_engines/backends/polars_backend.py +++ b/sdk/python/feast/infra/compute_engines/backends/polars_backend.py @@ -7,6 +7,8 @@ class PolarsBackend(DataFrameBackend): + _POLARS_FUNC_MAP = {"nunique": "n_unique"} + def columns(self, df: pl.DataFrame) -> list[str]: return df.columns @@ -21,7 +23,7 @@ def join(self, left: pl.DataFrame, right: pl.DataFrame, on, how) -> pl.DataFrame def groupby_agg(self, df: pl.DataFrame, group_keys, agg_ops) -> pl.DataFrame: agg_exprs = [ - getattr(pl.col(col), func)().alias(alias) + getattr(pl.col(col), self._POLARS_FUNC_MAP.get(func, func))().alias(alias) for alias, (func, col) in agg_ops.items() ] return df.groupby(group_keys).agg(agg_exprs) diff --git a/sdk/python/feast/infra/compute_engines/base.py b/sdk/python/feast/infra/compute_engines/base.py index 360cf95a044..a99907a82b2 100644 --- a/sdk/python/feast/infra/compute_engines/base.py +++ b/sdk/python/feast/infra/compute_engines/base.py @@ -85,6 +85,21 @@ def teardown_infra( """ pass + @property + def supports_batch(self) -> bool: + """Whether this engine can accept all tasks in a single materialize() call. + + When True, feature_store.py collects all FV tasks upfront and submits + them in one batch. When False (default), the standard per-FV loop + through ``provider.materialize_single_feature_view()`` is used. + """ + return False + + @property + def applies_materialization(self) -> bool: + """If True, the engine already wrote watermarks/state (e.g. driver pod).""" + return False + def materialize( self, registry: BaseRegistry, diff --git a/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/tests/__init__.py b/sdk/python/feast/infra/compute_engines/dag/__init__.py similarity index 100% rename from sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/tests/__init__.py rename to sdk/python/feast/infra/compute_engines/dag/__init__.py diff --git a/sdk/python/feast/infra/compute_engines/dag/context.py b/sdk/python/feast/infra/compute_engines/dag/context.py index 46eda356223..38b9a788872 100644 --- a/sdk/python/feast/infra/compute_engines/dag/context.py +++ b/sdk/python/feast/infra/compute_engines/dag/context.py @@ -42,6 +42,18 @@ def created_timestamp_column(self) -> Optional[str]: """ return self._get_mapped_column(self.created_ts_col) + @property + def join_keys_columns(self) -> List[str]: + """ + Get the join keys, mapped through field_mapping to their post-rename + column names. Use this when looking up columns on a DataFrame that has + already had its source columns renamed (e.g. inside DAG nodes that + consume the output of a source-read node). + """ + if not self.field_mapping: + return list(self.join_keys) + return [self.field_mapping.get(key, key) for key in self.join_keys] + def _get_mapped_column(self, column: Optional[str]) -> Optional[str]: """ Helper method to get the mapped column name if it exists in field_mapping. diff --git a/sdk/python/feast/infra/compute_engines/dag/model.py b/sdk/python/feast/infra/compute_engines/dag/model.py index 5990eea6141..263c5029f4f 100644 --- a/sdk/python/feast/infra/compute_engines/dag/model.py +++ b/sdk/python/feast/infra/compute_engines/dag/model.py @@ -6,3 +6,4 @@ class DAGFormat(str, Enum): PANDAS = "pandas" ARROW = "arrow" RAY = "ray" + FLINK = "flink" diff --git a/sdk/python/feast/infra/compute_engines/feature_builder.py b/sdk/python/feast/infra/compute_engines/feature_builder.py index 8510b676c07..2a102bf9f2f 100644 --- a/sdk/python/feast/infra/compute_engines/feature_builder.py +++ b/sdk/python/feast/infra/compute_engines/feature_builder.py @@ -158,9 +158,17 @@ def get_column_info( # we need to read ALL source columns, not just the output feature columns. # This is specifically for transformations that create new columns or need raw data. mode = getattr(getattr(view, "feature_transformation", None), "mode", None) - if mode == "ray" or getattr(mode, "value", None) == "ray": - # Signal to read all columns by passing empty list for feature_cols - # The transformation will produce the output columns defined in the schema + if mode in ("ray", "pandas", "python", "flink") or getattr( + mode, "value", None + ) in ( + "ray", + "pandas", + "python", + "flink", + ): + # Signal to read all columns by passing empty list for feature_cols. + # "python" (BatchFeatureView) transformations need all raw source columns — the + # UDF computes output features from raw input, not from pre-existing feature cols. feature_cols = [] return ColumnInfo( diff --git a/sdk/python/feast/infra/compute_engines/flink/__init__.py b/sdk/python/feast/infra/compute_engines/flink/__init__.py new file mode 100644 index 00000000000..678bae5afc6 --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/flink/__init__.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from feast.infra.compute_engines.flink.compute import ( + FlinkComputeEngine, + FlinkComputeEngineConfig, +) + +__all__ = [ + "FlinkComputeEngine", + "FlinkComputeEngineConfig", +] diff --git a/sdk/python/feast/infra/compute_engines/flink/compute.py b/sdk/python/feast/infra/compute_engines/flink/compute.py new file mode 100644 index 00000000000..b0cfc645a7a --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/flink/compute.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +import logging +from typing import Any, Dict, Literal, Optional, Sequence, Union + +from feast import ( + BatchFeatureView, + Entity, + FeatureView, + OnDemandFeatureView, + StreamFeatureView, +) +from feast.infra.common.materialization_job import ( + MaterializationJob, + MaterializationJobStatus, + MaterializationTask, +) +from feast.infra.common.retrieval_task import HistoricalRetrievalTask +from feast.infra.compute_engines.base import ComputeEngine +from feast.infra.compute_engines.flink.feature_builder import FlinkFeatureBuilder +from feast.infra.compute_engines.flink.job import ( + FlinkDAGRetrievalJob, + FlinkMaterializationJob, +) +from feast.infra.compute_engines.flink.utils import ( + cleanup_flink_temporary_views, + create_flink_table_environment, +) +from feast.infra.offline_stores.offline_store import OfflineStore, RetrievalJob +from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.registry.base_registry import BaseRegistry +from feast.repo_config import FeastConfigBaseModel, RepoConfig + +logger = logging.getLogger(__name__) + + +class FlinkComputeEngineConfig(FeastConfigBaseModel): + """Configuration for the Apache Flink compute engine.""" + + type: Literal["flink.engine"] = "flink.engine" + """Flink compute engine type selector.""" + + execution_mode: Literal["batch", "streaming"] = "batch" + """PyFlink TableEnvironment execution mode.""" + + parallelism: Optional[int] = None + """Default Flink parallelism for jobs created by this engine.""" + + table_config: Optional[Dict[str, str]] = None + """Additional PyFlink table configuration entries.""" + + pandas_split_num: int = 1 + """Number of PyFlink Arrow source splits for pandas entity DataFrames.""" + + +class FlinkComputeEngine(ComputeEngine): + def __init__( + self, + *, + repo_config: RepoConfig, + offline_store: OfflineStore, + online_store: OnlineStore, + table_environment: Optional[Any] = None, + **kwargs, + ) -> None: + super().__init__( + repo_config=repo_config, + offline_store=offline_store, + online_store=online_store, + **kwargs, + ) + self.config = repo_config.batch_engine + assert isinstance(self.config, FlinkComputeEngineConfig) + self.table_env = table_environment or create_flink_table_environment( + self.config + ) + + def update( + self, + project: str, + views_to_delete: Sequence[ + Union[BatchFeatureView, StreamFeatureView, FeatureView] + ], + views_to_keep: Sequence[ + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] + ], + entities_to_delete: Sequence[Entity], + entities_to_keep: Sequence[Entity], + ) -> None: + """Flink compute engine does not provision Feast-managed infrastructure.""" + pass + + def teardown_infra( + self, + project: str, + fvs: Sequence[Union[BatchFeatureView, StreamFeatureView, FeatureView]], + entities: Sequence[Entity], + ) -> None: + """Flink compute engine does not tear down Feast-managed infrastructure.""" + pass + + def _materialize_one( + self, registry: BaseRegistry, task: MaterializationTask, **kwargs + ) -> MaterializationJob: + job_id = f"{task.feature_view.name}-{task.start_time}-{task.end_time}" + context = self.get_execution_context(registry, task) + + try: + builder = FlinkFeatureBuilder( + registry=registry, + table_env=self.table_env, + task=task, + split_num=self.config.pandas_split_num, + ) + plan = builder.build() + plan.execute(context) + return FlinkMaterializationJob( + job_id=job_id, + status=MaterializationJobStatus.SUCCEEDED, + ) + except Exception as exc: + logger.error("Flink materialization failed for %s: %s", job_id, exc) + return FlinkMaterializationJob( + job_id=job_id, + status=MaterializationJobStatus.ERROR, + error=exc, + ) + finally: + cleanup_flink_temporary_views(self.table_env) + + def get_historical_features( + self, registry: BaseRegistry, task: HistoricalRetrievalTask + ) -> RetrievalJob: + context = self.get_execution_context(registry, task) + try: + builder = FlinkFeatureBuilder( + registry=registry, + table_env=self.table_env, + task=task, + split_num=self.config.pandas_split_num, + ) + plan = builder.build() + return FlinkDAGRetrievalJob( + plan=plan, + context=context, + table_env=self.table_env, + full_feature_names=task.full_feature_name, + ) + except Exception as exc: + logger.error( + "Flink historical retrieval setup failed for %s: %s", + task.feature_view.name, + exc, + ) + return FlinkDAGRetrievalJob( + plan=None, + context=context, + table_env=self.table_env, + full_feature_names=task.full_feature_name, + error=exc, + ) diff --git a/sdk/python/feast/infra/compute_engines/flink/feature_builder.py b/sdk/python/feast/infra/compute_engines/flink/feature_builder.py new file mode 100644 index 00000000000..3fd276c92fe --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/flink/feature_builder.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +import logging +from typing import Any, Union + +import pandas as pd + +from feast.infra.common.materialization_job import MaterializationTask +from feast.infra.common.retrieval_task import HistoricalRetrievalTask +from feast.infra.compute_engines.dag.node import DAGNode +from feast.infra.compute_engines.feature_builder import FeatureBuilder +from feast.infra.compute_engines.flink.nodes import ( + FlinkAggregationNode, + FlinkDedupNode, + FlinkFilterNode, + FlinkJoinNode, + FlinkOutputNode, + FlinkSourceReadNode, + FlinkTransformationNode, + FlinkValidationNode, +) +from feast.infra.registry.base_registry import BaseRegistry +from feast.types import PrimitiveFeastType, from_feast_to_pyarrow_type + +logger = logging.getLogger(__name__) + + +class FlinkFeatureBuilder(FeatureBuilder): + def __init__( + self, + registry: BaseRegistry, + table_env: Any, + task: Union[MaterializationTask, HistoricalRetrievalTask], + split_num: int, + ) -> None: + super().__init__(registry, task.feature_view, task) + self.table_env = table_env + self.split_num = split_num + + def _should_join_entity_df(self) -> bool: + return isinstance(self.task, HistoricalRetrievalTask) and ( + isinstance(self.task.entity_df, pd.DataFrame) + or ( + isinstance(self.task.entity_df, str) + and bool(self.task.entity_df.strip()) + ) + ) + + def _build(self, view: Any, input_nodes: list[DAGNode] | None) -> DAGNode: + if view.data_source: + last_node: DAGNode = self.build_source_node(view) + + if self._should_transform(view): + last_node = self.build_transformation_node(view, [last_node]) + + if self._should_join_entity_df(): + last_node = self.build_join_node(view, [last_node]) + + elif input_nodes: + if self._should_transform(view): + last_node = self.build_transformation_node(view, input_nodes) + else: + last_node = self.build_join_node(view, input_nodes) + else: + raise ValueError(f"FeatureView {view.name} has no valid source or inputs") + + last_node = self.build_filter_node(view, last_node) + + if self._should_aggregate(view): + last_node = self.build_aggregation_node(view, last_node) + elif self._should_dedupe(view): + last_node = self.build_dedup_node(view, last_node) + + if self._should_validate(view): + last_node = self.build_validation_node(view, last_node) + + return last_node + + def build_source_node(self, view: Any) -> FlinkSourceReadNode: + source = view.batch_source + column_info = self.get_column_info(view) + node = FlinkSourceReadNode( + f"{view.name}:source", + source, + column_info, + self.table_env, + self.split_num, + self.task.start_time, + self.task.end_time, + ) + self.nodes.append(node) + return node + + def build_aggregation_node( + self, view: Any, input_node: DAGNode + ) -> FlinkAggregationNode: + column_info = self.get_column_info(view) + node = FlinkAggregationNode( + f"{view.name}:agg", + column_info.join_keys_columns, + view.aggregations, + self.table_env, + self.split_num, + inputs=[input_node], + ) + self.nodes.append(node) + return node + + def build_join_node(self, view: Any, input_nodes: list[DAGNode]) -> FlinkJoinNode: + column_info = self.get_column_info(view) + node = FlinkJoinNode( + f"{view.name}:join", + column_info, + self.table_env, + self.split_num, + inputs=input_nodes, + ) + self.nodes.append(node) + return node + + def build_filter_node(self, view: Any, input_node: DAGNode) -> FlinkFilterNode: + filter_expr = getattr(view, "filter", None) + ttl = getattr(view, "ttl", None) + column_info = self.get_column_info(view) + node = FlinkFilterNode( + f"{view.name}:filter", + column_info, + self.table_env, + self.split_num, + filter_expr, + ttl, + inputs=[input_node], + ) + self.nodes.append(node) + return node + + def build_dedup_node(self, view: Any, input_node: DAGNode) -> FlinkDedupNode: + column_info = self.get_column_info(view) + node = FlinkDedupNode( + f"{view.name}:dedup", + column_info, + self.table_env, + self.split_num, + inputs=[input_node], + ) + self.nodes.append(node) + return node + + def build_transformation_node( + self, view: Any, input_nodes: list[DAGNode] + ) -> FlinkTransformationNode: + transform_config = view.feature_transformation + transformation_fn = ( + transform_config.udf + if hasattr(transform_config, "udf") + else transform_config + ) + node = FlinkTransformationNode( + f"{view.name}:transform", + transformation_fn, + self.table_env, + self.split_num, + inputs=input_nodes, + ) + self.nodes.append(node) + return node + + def build_output_nodes(self, view: Any, input_node: DAGNode) -> FlinkOutputNode: + node = FlinkOutputNode( + f"{view.name}:output", + self.dag_root.view, + self.table_env, + self.split_num, + isinstance(self.task, MaterializationTask), + [input_node], + ) + self.nodes.append(node) + return node + + def build_validation_node( + self, view: Any, input_node: DAGNode + ) -> FlinkValidationNode: + expected_columns = {} + json_columns: set[str] = set() + if hasattr(view, "features"): + for feature in view.features: + try: + expected_columns[feature.name] = from_feast_to_pyarrow_type( + feature.dtype + ) + except (ValueError, KeyError): + logger.debug( + "Could not resolve PyArrow type for feature '%s' " + "(dtype=%s), skipping type check for this column.", + feature.name, + feature.dtype, + ) + expected_columns[feature.name] = None + if ( + isinstance(feature.dtype, PrimitiveFeastType) + and feature.dtype.name == "JSON" + ): + json_columns.add(feature.name) + + node = FlinkValidationNode( + f"{view.name}:validate", + expected_columns, + json_columns, + self.table_env, + self.split_num, + inputs=[input_node], + ) + self.nodes.append(node) + return node diff --git a/sdk/python/feast/infra/compute_engines/flink/job.py b/sdk/python/feast/infra/compute_engines/flink/job.py new file mode 100644 index 00000000000..45e80df5506 --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/flink/job.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Optional + +import pandas as pd +import pyarrow as pa + +from feast.infra.common.materialization_job import ( + MaterializationJob, + MaterializationJobStatus, +) +from feast.infra.compute_engines.dag.context import ExecutionContext +from feast.infra.compute_engines.dag.plan import ExecutionPlan +from feast.infra.compute_engines.flink.utils import ( + cleanup_flink_temporary_views, + flink_table_to_arrow, +) +from feast.infra.offline_stores.offline_store import RetrievalJob, RetrievalMetadata +from feast.on_demand_feature_view import OnDemandFeatureView +from feast.saved_dataset import SavedDatasetStorage + + +class FlinkDAGRetrievalJob(RetrievalJob): + def __init__( + self, + plan: Optional[ExecutionPlan], + context: ExecutionContext, + table_env: object, + full_feature_names: bool, + on_demand_feature_views: Optional[List[OnDemandFeatureView]] = None, + metadata: Optional[RetrievalMetadata] = None, + error: Optional[BaseException] = None, + ) -> None: + self._plan = plan + self._context = context + self._table_env = table_env + self._full_feature_names = full_feature_names + self._on_demand_feature_views = on_demand_feature_views or [] + self._metadata = metadata + self._error = error + self._arrow_table: Optional[pa.Table] = None + + def error(self) -> Optional[BaseException]: + return self._error + + def _ensure_executed(self) -> None: + if self._arrow_table is None: + if self._error is not None: + raise self._error + if self._plan is None: + raise RuntimeError("Execution plan is not set") + try: + result = self._plan.execute(self._context) + self._arrow_table = flink_table_to_arrow(result.data) + finally: + cleanup_flink_temporary_views(self._table_env) + + def _to_df_internal(self, timeout: Optional[int] = None) -> pd.DataFrame: + self._ensure_executed() + assert self._arrow_table is not None + return self._arrow_table.to_pandas() + + def _to_arrow_internal(self, timeout: Optional[int] = None) -> pa.Table: + self._ensure_executed() + assert self._arrow_table is not None + return self._arrow_table + + @property + def full_feature_names(self) -> bool: + return self._full_feature_names + + @property + def on_demand_feature_views(self) -> List[OnDemandFeatureView]: + return self._on_demand_feature_views + + @property + def metadata(self) -> Optional[RetrievalMetadata]: + return self._metadata + + def persist( + self, + storage: SavedDatasetStorage, + allow_overwrite: bool = False, + timeout: Optional[int] = None, + ) -> None: + raise NotImplementedError("Persisting Flink retrieval jobs is not supported.") + + def to_remote_storage(self) -> List[str]: + raise NotImplementedError( + "Remote storage is not supported in FlinkDAGRetrievalJob." + ) + + def to_sql(self) -> str: + raise NotImplementedError("SQL generation is not supported for Flink DAGs.") + + +@dataclass +class FlinkMaterializationJob(MaterializationJob): + def __init__( + self, + job_id: str, + status: MaterializationJobStatus, + error: Optional[BaseException] = None, + ) -> None: + super().__init__() + self._job_id = job_id + self._status = status + self._error = error + + def status(self) -> MaterializationJobStatus: + return self._status + + def error(self) -> Optional[BaseException]: + return self._error + + def should_be_retried(self) -> bool: + return False + + def job_id(self) -> str: + return self._job_id + + def url(self) -> Optional[str]: + return None diff --git a/sdk/python/feast/infra/compute_engines/flink/nodes.py b/sdk/python/feast/infra/compute_engines/flink/nodes.py new file mode 100644 index 00000000000..2bdb29cd8c4 --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/flink/nodes.py @@ -0,0 +1,816 @@ +from __future__ import annotations + +import logging +import uuid +from datetime import datetime, timedelta +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union + +import pandas as pd +import pyarrow as pa + +from feast import BatchFeatureView, FeatureView, StreamFeatureView +from feast.aggregation import Aggregation, aggregation_specs_to_agg_ops +from feast.data_source import DataSource +from feast.infra.compute_engines.dag.context import ColumnInfo, ExecutionContext +from feast.infra.compute_engines.dag.model import DAGFormat +from feast.infra.compute_engines.dag.node import DAGNode +from feast.infra.compute_engines.dag.value import DAGValue +from feast.infra.compute_engines.flink.utils import ( + flink_table_to_arrow_batches, + pandas_to_flink_table, + register_flink_temporary_view, +) +from feast.infra.compute_engines.utils import ( + ENTITY_ROW_ID, + ENTITY_TS_ALIAS, + create_offline_store_retrieval_job, + find_entity_timestamp_column, + infer_entity_timestamp_column, +) +from feast.utils import _convert_arrow_to_proto + +logger = logging.getLogger(__name__) + +DEDUP_ROW_NUMBER = "__feast_row_number" + + +def _quote_identifier(identifier: str) -> str: + return f"`{identifier.replace('`', '``')}`" + + +def _qualified_column(alias: str, column: str) -> str: + return f"{alias}.{_quote_identifier(column)}" + + +def _select_column(alias: str, column: str, output_name: Optional[str] = None) -> str: + expr = _qualified_column(alias, column) + if output_name and output_name != column: + return f"{expr} AS {_quote_identifier(output_name)}" + return expr + + +def _flink_interval_literals(value: timedelta) -> List[str]: + total_seconds = int(value.total_seconds()) + if total_seconds <= 0: + return ["INTERVAL '0' SECOND"] + + days, remainder = divmod(total_seconds, 24 * 60 * 60) + hours, remainder = divmod(remainder, 60 * 60) + minutes, seconds = divmod(remainder, 60) + parts: List[str] = [] + if days: + parts.append(f"INTERVAL '{days}' DAY") + if hours: + parts.append(f"INTERVAL '{hours}' HOUR") + if minutes: + parts.append(f"INTERVAL '{minutes}' MINUTE") + if seconds: + parts.append(f"INTERVAL '{seconds}' SECOND") + return parts + + +def _subtract_flink_intervals(timestamp_expr: str, value: timedelta) -> str: + result = timestamp_expr + for interval in _flink_interval_literals(value): + result = f"{result} - {interval}" + return result + + +def _get_columns_from_schema(table: Any) -> Optional[List[str]]: + if not hasattr(table, "get_schema"): + return None + schema = table.get_schema() + if hasattr(schema, "get_field_names"): + return list(schema.get_field_names()) + if hasattr(schema, "get_field_count") and hasattr(schema, "get_field_name"): + return [schema.get_field_name(i) for i in range(schema.get_field_count())] + return None + + +def _get_columns(value: DAGValue) -> List[str]: + metadata_columns = value.metadata.get("columns") if value.metadata else None + if metadata_columns: + return list(metadata_columns) + schema_columns = _get_columns_from_schema(value.data) + if schema_columns: + return schema_columns + raise ValueError( + "Could not infer columns for Flink DAG value from metadata or PyFlink schema." + ) + + +def _can_use_sql(table_env: Any) -> bool: + return hasattr(table_env, "create_temporary_view") and hasattr( + table_env, "sql_query" + ) + + +def _require_sql(table_env: Any, node_name: str) -> None: + if not _can_use_sql(table_env): + raise RuntimeError( + f"Flink node '{node_name}' requires a PyFlink TableEnvironment with " + "create_temporary_view() and sql_query()." + ) + + +def _register_table(table_env: Any, table: Any, prefix: str) -> str: + view_name = f"__feast_{prefix}_{uuid.uuid4().hex}" + table_env.create_temporary_view(view_name, table) + register_flink_temporary_view(table_env, view_name) + return view_name + + +def _sql_value( + table_env: Any, + query: str, + columns: Iterable[str], + metadata: Optional[dict] = None, +) -> DAGValue: + return DAGValue( + data=table_env.sql_query(query), + format=DAGFormat.FLINK, + metadata={**(metadata or {}), "columns": list(columns), "native_sql": query}, + ) + + +def _entity_timestamp_column_from_columns(columns: List[str]) -> str: + entity_ts_col = find_entity_timestamp_column(columns) + if entity_ts_col: + return entity_ts_col + raise ValueError( + "SQL-based entity_df for FlinkComputeEngine must select an " + "`event_timestamp` column." + ) + + +def _entity_value_from_dataframe( + table_env: Any, + entity_df: pd.DataFrame, + split_num: int, + join_keys: List[str], +) -> tuple[Any, List[str], str]: + entity_df = entity_df.copy() + if entity_df.empty: + for join_key in join_keys: + if join_key not in entity_df.columns: + entity_df[join_key] = pd.Series(dtype="object") + entity_ts_col = find_entity_timestamp_column(list(entity_df.columns)) + if entity_ts_col is None: + entity_ts_col = ENTITY_TS_ALIAS + entity_df[entity_ts_col] = pd.Series(dtype="datetime64[ns]") + else: + entity_schema = dict(zip(entity_df.columns, entity_df.dtypes)) + entity_ts_col = infer_entity_timestamp_column(entity_schema) + + entity_df[ENTITY_ROW_ID] = range(len(entity_df)) + if entity_ts_col != ENTITY_TS_ALIAS: + entity_df = entity_df.rename(columns={entity_ts_col: ENTITY_TS_ALIAS}) + return ( + pandas_to_flink_table(table_env, entity_df, split_num), + list(entity_df.columns), + entity_ts_col, + ) + + +def _entity_value_from_sql( + table_env: Any, + entity_sql: str, + join_keys: List[str], +) -> tuple[Any, List[str], str]: + _require_sql(table_env, "entity_df") + entity_table = table_env.sql_query(entity_sql) + entity_columns = _get_columns_from_schema(entity_table) + if entity_columns is None: + raise ValueError("Could not infer columns for SQL-based entity_df.") + + entity_ts_col = _entity_timestamp_column_from_columns(entity_columns) + entity_view = _register_table(table_env, entity_table, "entity_sql") + output_columns = [ + ENTITY_TS_ALIAS if column == entity_ts_col else column + for column in entity_columns + ] + select_exprs = [ + _select_column( + "entity_src", + column, + ENTITY_TS_ALIAS if column == entity_ts_col else column, + ) + for column in entity_columns + ] + order_columns = [ + column for column in [entity_ts_col, *join_keys] if column in entity_columns + ] + order_expr = ", ".join( + _qualified_column("entity_src", col) for col in order_columns + ) + if not order_expr: + order_expr = _qualified_column("entity_src", entity_columns[0]) + select_exprs.append( + f"ROW_NUMBER() OVER (ORDER BY {order_expr}) - 1 AS " + f"{_quote_identifier(ENTITY_ROW_ID)}" + ) + query = ( + f"SELECT {', '.join(select_exprs)} " + f"FROM {_quote_identifier(entity_view)} AS entity_src" + ) + output_columns.append(ENTITY_ROW_ID) + value = _sql_value( + table_env, + query, + output_columns, + metadata={"entity_timestamp_column": entity_ts_col}, + ) + return value.data, output_columns, entity_ts_col + + +def _entity_value_from_context( + table_env: Any, + context: ExecutionContext, + split_num: int, + join_keys: List[str], +) -> tuple[Any, List[str], str]: + if isinstance(context.entity_df, pd.DataFrame): + return _entity_value_from_dataframe( + table_env, context.entity_df, split_num, join_keys + ) + if isinstance(context.entity_df, str): + return _entity_value_from_sql(table_env, context.entity_df, join_keys) + raise TypeError( + "FlinkComputeEngine entity_df must be a pandas DataFrame, SQL string, or None." + ) + + +def _retrieval_job_to_flink_table( + retrieval_job: Any, + table_env: Any, + split_num: int, +) -> tuple[Any, List[str]]: + to_flink_table = getattr(retrieval_job, "to_flink_table", None) + if callable(to_flink_table): + flink_table = to_flink_table(table_env) + columns = _get_columns_from_schema(flink_table) + if columns is None: + raise ValueError( + "Could not infer columns for source Flink table returned by " + "RetrievalJob.to_flink_table(table_env)." + ) + return flink_table, columns + + if not hasattr(retrieval_job, "to_arrow"): + raise TypeError( + "FlinkComputeEngine source reads require a RetrievalJob with either " + "to_flink_table(table_env) or to_arrow()." + ) + + arrow_table = retrieval_job.to_arrow() + if not isinstance(arrow_table, pa.Table): + raise TypeError( + "RetrievalJob.to_arrow() must return a pyarrow.Table for " + "FlinkComputeEngine source reads." + ) + columns = list(arrow_table.column_names) + return pandas_to_flink_table(table_env, arrow_table.to_pandas(), split_num), columns + + +class FlinkSourceReadNode(DAGNode): + def __init__( + self, + name: str, + source: DataSource, + column_info: ColumnInfo, + table_env: Any, + split_num: int, + start_time: Optional[datetime] = None, + end_time: Optional[datetime] = None, + ) -> None: + super().__init__(name) + self.source = source + self.column_info = column_info + self.table_env = table_env + self.split_num = split_num + self.start_time = start_time + self.end_time = end_time + + def execute(self, context: ExecutionContext) -> DAGValue: + retrieval_job = create_offline_store_retrieval_job( + data_source=self.source, + column_info=self.column_info, + context=context, + start_time=self.start_time, + end_time=self.end_time, + ) + flink_table, columns = _retrieval_job_to_flink_table( + retrieval_job, self.table_env, self.split_num + ) + + if self.column_info.field_mapping: + view_name = _register_table(self.table_env, flink_table, "source_read") + select_exprs = [ + _select_column( + "src", + col, + self.column_info.field_mapping.get(col, col), + ) + for col in columns + ] + renamed_columns = [ + self.column_info.field_mapping.get(col, col) for col in columns + ] + query = ( + f"SELECT {', '.join(select_exprs)} " + f"FROM {_quote_identifier(view_name)} AS src" + ) + return _sql_value( + self.table_env, + query, + renamed_columns, + metadata={ + "source": "feature_view_batch_source", + "timestamp_field": self.column_info.timestamp_column, + "created_timestamp_column": ( + self.column_info.created_timestamp_column + ), + "start_date": self.start_time, + "end_date": self.end_time, + }, + ) + + return DAGValue( + data=flink_table, + format=DAGFormat.FLINK, + metadata={ + "source": "feature_view_batch_source", + "timestamp_field": self.column_info.timestamp_column, + "created_timestamp_column": (self.column_info.created_timestamp_column), + "start_date": self.start_time, + "end_date": self.end_time, + "columns": columns, + }, + ) + + +class FlinkJoinNode(DAGNode): + def __init__( + self, + name: str, + column_info: ColumnInfo, + table_env: Any, + split_num: int, + inputs: Optional[List[DAGNode]] = None, + how: str = "left", + ) -> None: + super().__init__(name, inputs=inputs or []) + self.column_info = column_info + self.table_env = table_env + self.split_num = split_num + self.how = how + + def execute(self, context: ExecutionContext) -> DAGValue: + input_values = self.get_input_values(context) + for value in input_values: + value.assert_format(DAGFormat.FLINK) + if not input_values: + raise RuntimeError(f"FlinkJoinNode '{self.name}' requires inputs") + + _require_sql(self.table_env, self.name) + return self._execute_sql_join(input_values, context) + + def _execute_sql_join( + self, input_values: List[DAGValue], context: ExecutionContext + ) -> DAGValue: + join_keys = self.column_info.join_keys_columns + view_names = [ + _register_table(self.table_env, value.data, f"join_{index}") + for index, value in enumerate(input_values) + ] + columns_by_input = [_get_columns(value) for value in input_values] + output_columns = list(columns_by_input[0]) + seen_columns = set(output_columns) + select_exprs = [_select_column("t0", column) for column in columns_by_input[0]] + + joins = [] + for index, view_name in enumerate(view_names[1:], start=1): + alias = f"t{index}" + on_clause = " AND ".join( + f"{_qualified_column('t0', key)} = {_qualified_column(alias, key)}" + for key in join_keys + ) + joins.append( + f"{self.how.upper()} JOIN {_quote_identifier(view_name)} AS {alias} " + f"ON {on_clause}" + ) + for column in columns_by_input[index]: + if column in join_keys or column in seen_columns: + continue + output_columns.append(column) + seen_columns.add(column) + select_exprs.append(_select_column(alias, column)) + + query = ( + f"SELECT {', '.join(select_exprs)} " + f"FROM {_quote_identifier(view_names[0])} AS t0 " + f"{' '.join(joins)}" + ) + joined_value = _sql_value( + self.table_env, + query, + output_columns, + metadata={"joined_on": join_keys, "join_type": self.how}, + ) + + if context.entity_df is None: + return joined_value + + entity_table, entity_columns, entity_ts_col = _entity_value_from_context( + self.table_env, context, self.split_num, join_keys + ) + entity_view = _register_table(self.table_env, entity_table, "entity") + feature_view = _register_table(self.table_env, joined_value.data, "features") + feature_columns = [ + column + for column in output_columns + if column not in join_keys and column not in entity_columns + ] + select_entity = [_select_column("e", column) for column in entity_columns] + select_features = [_select_column("f", column) for column in feature_columns] + on_clause = " AND ".join( + f"{_qualified_column('e', key)} = {_qualified_column('f', key)}" + for key in join_keys + ) + entity_join_query = ( + f"SELECT {', '.join(select_entity + select_features)} " + f"FROM {_quote_identifier(entity_view)} AS e " + f"LEFT JOIN {_quote_identifier(feature_view)} AS f ON {on_clause}" + ) + return _sql_value( + self.table_env, + entity_join_query, + entity_columns + feature_columns, + metadata={ + "joined_on": join_keys, + "join_type": "left", + "entity_timestamp_column": entity_ts_col, + }, + ) + + +class FlinkFilterNode(DAGNode): + def __init__( + self, + name: str, + column_info: ColumnInfo, + table_env: Any, + split_num: int, + filter_expr: Optional[str] = None, + ttl: Optional[timedelta] = None, + inputs: Optional[List[DAGNode]] = None, + ) -> None: + super().__init__(name, inputs=inputs) + self.column_info = column_info + self.table_env = table_env + self.split_num = split_num + self.filter_expr = filter_expr + self.ttl = ttl + + def execute(self, context: ExecutionContext) -> DAGValue: + input_value = self.get_single_input_value(context) + input_value.assert_format(DAGFormat.FLINK) + + _require_sql(self.table_env, self.name) + return self._execute_sql_filter(input_value) + + def _execute_sql_filter(self, input_value: DAGValue) -> DAGValue: + columns = _get_columns(input_value) + timestamp_column = self.column_info.timestamp_column + conditions = [] + + if ENTITY_TS_ALIAS in columns and timestamp_column in columns: + conditions.append( + f"{_quote_identifier(timestamp_column)} <= " + f"{_quote_identifier(ENTITY_TS_ALIAS)}" + ) + if self.ttl: + lower_bound = _subtract_flink_intervals( + _quote_identifier(ENTITY_TS_ALIAS), self.ttl + ) + conditions.append( + f"{_quote_identifier(timestamp_column)} >= {lower_bound}" + ) + + if self.filter_expr: + conditions.append(f"({self.filter_expr})") + + if not conditions: + return input_value + + view_name = _register_table(self.table_env, input_value.data, "filter") + query = ( + f"SELECT * FROM {_quote_identifier(view_name)} " + f"WHERE {' AND '.join(conditions)}" + ) + return _sql_value( + self.table_env, + query, + columns, + metadata={**(input_value.metadata or {}), "filter_applied": True}, + ) + + +class FlinkAggregationNode(DAGNode): + def __init__( + self, + name: str, + group_keys: List[str], + aggregations: List[Aggregation], + table_env: Any, + split_num: int, + inputs: Optional[List[DAGNode]] = None, + ) -> None: + super().__init__(name, inputs=inputs) + self.group_keys = group_keys + self.aggregations = aggregations + self.table_env = table_env + self.split_num = split_num + + def execute(self, context: ExecutionContext) -> DAGValue: + agg_ops = aggregation_specs_to_agg_ops( + self.aggregations, + time_window_unsupported_error_message=( + "Time window aggregation is not yet supported in the Flink compute " + "engine. Use non-windowed aggregations or pre-window upstream in Flink." + ), + ) + input_value = self.get_single_input_value(context) + input_value.assert_format(DAGFormat.FLINK) + + _require_sql(self.table_env, self.name) + return self._execute_sql_aggregation(input_value, agg_ops) + + def _execute_sql_aggregation( + self, input_value: DAGValue, agg_ops: Dict[str, tuple[str, str]] + ) -> DAGValue: + view_name = _register_table(self.table_env, input_value.data, "aggregate") + select_exprs = [_quote_identifier(key) for key in self.group_keys] + for alias, (function, column) in agg_ops.items(): + sql_function = { + "mean": "AVG", + "avg": "AVG", + "sum": "SUM", + "min": "MIN", + "max": "MAX", + "count": "COUNT", + "nunique": "COUNT_DISTINCT", + "std": "STDDEV_SAMP", + "var": "VAR_SAMP", + }.get(function, function.upper()) + if sql_function == "COUNT_DISTINCT": + expr = ( + f"COUNT(DISTINCT {_quote_identifier(column)}) " + f"AS {_quote_identifier(alias)}" + ) + else: + expr = ( + f"{sql_function}({_quote_identifier(column)}) " + f"AS {_quote_identifier(alias)}" + ) + select_exprs.append(expr) + + query = ( + f"SELECT {', '.join(select_exprs)} " + f"FROM {_quote_identifier(view_name)} " + f"GROUP BY {', '.join(_quote_identifier(key) for key in self.group_keys)}" + ) + return _sql_value( + self.table_env, + query, + [*self.group_keys, *agg_ops.keys()], + metadata={"aggregated": True}, + ) + + +class FlinkDedupNode(DAGNode): + def __init__( + self, + name: str, + column_info: ColumnInfo, + table_env: Any, + split_num: int, + inputs: Optional[List[DAGNode]] = None, + ) -> None: + super().__init__(name, inputs=inputs) + self.column_info = column_info + self.table_env = table_env + self.split_num = split_num + + def execute(self, context: ExecutionContext) -> DAGValue: + input_value = self.get_single_input_value(context) + input_value.assert_format(DAGFormat.FLINK) + + _require_sql(self.table_env, self.name) + return self._execute_sql_dedup(input_value) + + def _execute_sql_dedup(self, input_value: DAGValue) -> DAGValue: + columns = _get_columns(input_value) + dedup_keys = ( + [ENTITY_ROW_ID] + if ENTITY_ROW_ID in columns + else self.column_info.join_keys_columns + ) + dedup_keys = [key for key in dedup_keys if key in columns] + if not dedup_keys: + return input_value + + order_columns = [ + self.column_info.timestamp_column, + self.column_info.created_timestamp_column, + ] + order_exprs = [ + f"{_quote_identifier(column)} DESC" + for column in order_columns + if column and column in columns + ] + if not order_exprs: + order_exprs = [f"{_quote_identifier(dedup_keys[0])} ASC"] + + view_name = _register_table(self.table_env, input_value.data, "dedup") + select_columns = ", ".join(_quote_identifier(column) for column in columns) + query = ( + f"SELECT {select_columns} FROM (" + f"SELECT *, ROW_NUMBER() OVER (" + f"PARTITION BY {', '.join(_quote_identifier(key) for key in dedup_keys)} " + f"ORDER BY {', '.join(order_exprs)}" + f") AS {_quote_identifier(DEDUP_ROW_NUMBER)} " + f"FROM {_quote_identifier(view_name)}" + f") WHERE {_quote_identifier(DEDUP_ROW_NUMBER)} = 1" + ) + return _sql_value( + self.table_env, + query, + columns, + metadata={**(input_value.metadata or {}), "deduped": True}, + ) + + +class FlinkTransformationNode(DAGNode): + def __init__( + self, + name: str, + transformation_fn: Callable[..., Any], + table_env: Any, + split_num: int, + inputs: Optional[List[DAGNode]] = None, + ) -> None: + super().__init__(name, inputs=inputs) + self.transformation_fn = transformation_fn + self.table_env = table_env + self.split_num = split_num + + def execute(self, context: ExecutionContext) -> DAGValue: + input_values = self.get_input_values(context) + for value in input_values: + value.assert_format(DAGFormat.FLINK) + + input_tables = [value.data for value in input_values] + transformed = self.transformation_fn(*input_tables) + + columns = _get_columns_from_schema(transformed) + if columns is None: + raise TypeError( + "Flink transformations must return a PyFlink Table with a schema." + ) + + return DAGValue( + data=transformed, + format=DAGFormat.FLINK, + metadata={"transformed": True, "columns": columns or []}, + ) + + +class FlinkValidationNode(DAGNode): + def __init__( + self, + name: str, + expected_columns: dict[str, Optional[pa.DataType]], + json_columns: Optional[Set[str]], + table_env: Any, + split_num: int, + inputs: Optional[List[DAGNode]] = None, + ) -> None: + super().__init__(name, inputs=inputs) + self.expected_columns = expected_columns + self.json_columns = json_columns or set() + self.table_env = table_env + self.split_num = split_num + + def execute(self, context: ExecutionContext) -> DAGValue: + input_value = self.get_single_input_value(context) + input_value.assert_format(DAGFormat.FLINK) + + columns = _get_columns(input_value) + missing = set(self.expected_columns.keys()) - set(columns) + if missing: + raise ValueError( + f"[Validation: {self.name}] Missing expected columns: {missing}. " + f"Actual columns: {sorted(columns)}" + ) + if not self.json_columns: + return DAGValue( + data=input_value.data, + format=DAGFormat.FLINK, + metadata={**(input_value.metadata or {}), "validated": True}, + ) + + raise NotImplementedError( + "JSON value validation is not supported by FlinkComputeEngine without " + "collecting data out of Flink. Validate JSON upstream in Flink SQL or " + "disable JSON validation for this FeatureView." + ) + + +class FlinkOutputNode(DAGNode): + def __init__( + self, + name: str, + feature_view: Union[BatchFeatureView, FeatureView, StreamFeatureView], + table_env: Any, + split_num: int, + write_output: bool, + inputs: Optional[List[DAGNode]] = None, + ) -> None: + super().__init__(name, inputs=inputs) + self.feature_view = feature_view + self.table_env = table_env + self.split_num = split_num + self.write_output = write_output + + def execute(self, context: ExecutionContext) -> DAGValue: + input_value = self.get_single_input_value(context) + input_value.assert_format(DAGFormat.FLINK) + output_value = self._drop_internal_columns(input_value) + output_table = output_value.data + if not self.write_output: + return output_value + + columns = _get_columns(output_value) + batch_size = context.repo_config.materialization_config.online_write_batch_size + if self.feature_view.online: + join_key_to_value_type = { + entity.name: entity.dtype.to_value_type() + for entity in self.feature_view.entity_columns + } + else: + join_key_to_value_type = {} + + for output_arrow in flink_table_to_arrow_batches( + output_table, + columns, + batch_size, + ): + if output_arrow.num_rows == 0: + continue + + if self.feature_view.online: + arrow_batches = ( + [output_arrow] + if batch_size is None + else output_arrow.to_batches(max_chunksize=batch_size) + ) + for batch in arrow_batches: + rows_to_write = _convert_arrow_to_proto( + batch, self.feature_view, join_key_to_value_type + ) + context.online_store.online_write_batch( + config=context.repo_config, + table=self.feature_view, + data=rows_to_write, + progress=lambda x: None, + ) + + if self.feature_view.offline: + context.offline_store.offline_write_batch( + config=context.repo_config, + feature_view=self.feature_view, + table=output_arrow, + progress=lambda x: None, + ) + + return output_value + + def _drop_internal_columns(self, input_value: DAGValue) -> DAGValue: + columns = _get_columns(input_value) + output_columns = [column for column in columns if column != ENTITY_ROW_ID] + if output_columns == columns: + return input_value + + _require_sql(self.table_env, self.name) + view_name = _register_table(self.table_env, input_value.data, "output") + query = ( + f"SELECT {', '.join(_quote_identifier(column) for column in output_columns)} " + f"FROM {_quote_identifier(view_name)}" + ) + return _sql_value( + self.table_env, + query, + output_columns, + metadata={**(input_value.metadata or {}), "output_cleaned": True}, + ) diff --git a/sdk/python/feast/infra/compute_engines/flink/utils.py b/sdk/python/feast/infra/compute_engines/flink/utils.py new file mode 100644 index 00000000000..f271e8bb113 --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/flink/utils.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +import logging +from datetime import date, datetime +from decimal import Decimal +from typing import TYPE_CHECKING, Any, Iterator, Optional + +import pandas as pd +import pyarrow as pa + +if TYPE_CHECKING: + from feast.infra.compute_engines.flink.compute import FlinkComputeEngineConfig + +logger = logging.getLogger(__name__) + +DEFAULT_FLINK_RESULT_BATCH_SIZE = 10_000 +_TEMP_VIEW_REGISTRY: dict[int, set[str]] = {} + + +def create_flink_table_environment(config: FlinkComputeEngineConfig) -> Any: + """Create a PyFlink TableEnvironment from Feast engine config.""" + try: + from pyflink.common import Configuration + from pyflink.table import EnvironmentSettings, TableEnvironment + except ImportError as exc: + raise ImportError( + "FlinkComputeEngine requires PyFlink. Install the `flink` extra with " + "uv from a Feast source checkout, or otherwise make the `pyflink` " + "package available to Feast." + ) from exc + + flink_conf = Configuration() + for key, value in (config.table_config or {}).items(): + flink_conf.set_string(key, value) + if config.parallelism is not None: + flink_conf.set_string("parallelism.default", str(config.parallelism)) + + builder = EnvironmentSettings.new_instance().with_configuration(flink_conf) + if config.execution_mode == "streaming": + builder = builder.in_streaming_mode() + else: + builder = builder.in_batch_mode() + return TableEnvironment.create(builder.build()) + + +def pandas_to_flink_table(table_env: Any, df: pd.DataFrame, split_num: int = 1) -> Any: + """Convert a pandas DataFrame to a PyFlink table.""" + schema = _build_pandas_flink_schema(df) + kwargs: dict[str, Any] = {"splits_num": split_num} + if schema is not None: + kwargs["schema"] = schema + return table_env.from_pandas(df, **kwargs) + + +def flink_table_to_pandas(table: Any) -> pd.DataFrame: + """Collect a PyFlink table into pandas.""" + if hasattr(table, "to_pandas"): + return table.to_pandas() + raise TypeError(f"Expected a PyFlink table, got {type(table)}") + + +def flink_table_to_arrow(table: Any) -> pa.Table: + """Collect a PyFlink table into Arrow.""" + value = flink_table_to_pandas(table) + return pa.Table.from_pandas(value) + + +def flink_table_to_arrow_batches( + table: Any, + columns: list[str], + batch_size: Optional[int], +) -> Iterator[pa.Table]: + """Stream a PyFlink table into Arrow tables without collecting it all first.""" + effective_batch_size = batch_size or DEFAULT_FLINK_RESULT_BATCH_SIZE + if effective_batch_size <= 0: + raise ValueError("Flink result batch size must be positive.") + + if hasattr(table, "execute"): + rows: list[dict[str, Any]] = [] + row_iterator = table.execute().collect() + try: + for row in row_iterator: + rows.append(_row_to_dict(row, columns)) + if len(rows) >= effective_batch_size: + yield _rows_to_arrow(rows, columns) + rows = [] + if rows: + yield _rows_to_arrow(rows, columns) + finally: + close = getattr(row_iterator, "close", None) + if callable(close): + close() + return + + if hasattr(table, "to_pandas"): + # Used by unit-test fakes and other table-like objects. Real PyFlink tables use + # the execute().collect() path above. + df = table.to_pandas() + for start in range(0, len(df), effective_batch_size): + yield pa.Table.from_pandas( + df.iloc[start : start + effective_batch_size], + preserve_index=False, + ) + return + + raise TypeError(f"Expected a PyFlink table, got {type(table)}") + + +def register_flink_temporary_view(table_env: Any, view_name: str) -> None: + """Track Feast-created temporary views so long-lived table envs can clean up.""" + views = _temporary_views_for_env(table_env) + views.add(view_name) + + +def cleanup_flink_temporary_views(table_env: Any) -> None: + """Drop Feast-created temporary views from a PyFlink TableEnvironment.""" + views = _temporary_views_for_env(table_env) + if not views: + return + + drop_view = getattr(table_env, "drop_temporary_view", None) + if not callable(drop_view): + return + + for view_name in list(views): + try: + drop_view(view_name) + except Exception as exc: + logger.debug("Failed to drop Flink temporary view %s: %s", view_name, exc) + finally: + views.discard(view_name) + if not views: + _TEMP_VIEW_REGISTRY.pop(id(table_env), None) + + +def _build_pandas_flink_schema(df: pd.DataFrame) -> Any: + try: + from pyflink.table import Schema + except ImportError: + return None + + builder = Schema.new_builder() + for column in df.columns: + builder.column(str(column), _pandas_dtype_to_flink_type(df[column])) + return builder.build() + + +def _pandas_dtype_to_flink_type(series: pd.Series) -> Any: + from pyflink.table import DataTypes + + dtype = series.dtype + if pd.api.types.is_bool_dtype(dtype): + return DataTypes.BOOLEAN() + if pd.api.types.is_integer_dtype(dtype): + dtype_name = str(dtype).lower() + if dtype_name.endswith("int8"): + return DataTypes.TINYINT() + if dtype_name.endswith("int16"): + return DataTypes.SMALLINT() + if dtype_name.endswith("int32"): + return DataTypes.INT() + return DataTypes.BIGINT() + if pd.api.types.is_float_dtype(dtype): + return DataTypes.FLOAT() if str(dtype) == "float32" else DataTypes.DOUBLE() + if pd.api.types.is_datetime64_any_dtype(dtype): + if getattr(dtype, "tz", None) is not None: + return DataTypes.TIMESTAMP_LTZ(3) + return DataTypes.TIMESTAMP(3) + if pd.api.types.is_timedelta64_dtype(dtype): + return DataTypes.BIGINT() + + sample = _first_non_null_value(series) + if isinstance(sample, (bytes, bytearray)): + return DataTypes.BYTES() + if isinstance(sample, date) and not isinstance(sample, datetime): + return DataTypes.DATE() + if isinstance(sample, Decimal): + return DataTypes.DECIMAL(38, 18) + return DataTypes.STRING() + + +def _first_non_null_value(series: pd.Series) -> Any: + for value in series: + if pd.notna(value): + return value + return None + + +def _row_to_dict(row: Any, columns: list[str]) -> dict[str, Any]: + if isinstance(row, dict): + return {column: row.get(column) for column in columns} + + as_dict = getattr(row, "as_dict", None) + if callable(as_dict): + row_dict = as_dict() + if row_dict: + return {column: row_dict.get(column) for column in columns} + + values = list(row) + return dict(zip(columns, values)) + + +def _rows_to_arrow(rows: list[dict[str, Any]], columns: list[str]) -> pa.Table: + df = pd.DataFrame.from_records(rows, columns=columns) + return pa.Table.from_pandas(df, preserve_index=False) + + +def _temporary_views_for_env(table_env: Any) -> set[str]: + views = getattr(table_env, "_feast_temporary_views", None) + if isinstance(views, set): + return views + + views = _TEMP_VIEW_REGISTRY.setdefault(id(table_env), set()) + try: + setattr(table_env, "_feast_temporary_views", views) + except Exception: + pass + return views diff --git a/sdk/python/feast/infra/compute_engines/kubernetes/Dockerfile b/sdk/python/feast/infra/compute_engines/kubernetes/Dockerfile index b6ecb490afd..eed53f2416c 100644 --- a/sdk/python/feast/infra/compute_engines/kubernetes/Dockerfile +++ b/sdk/python/feast/infra/compute_engines/kubernetes/Dockerfile @@ -3,6 +3,8 @@ FROM debian:11-slim AS build RUN apt-get update && \ apt-get install --no-install-suggests --no-install-recommends --yes git python3 python3-pip +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + WORKDIR /app COPY sdk/python/feast/infra/materialization/kubernetes/main.py /app @@ -11,7 +13,6 @@ COPY sdk/python/feast/infra/materialization/kubernetes/main.py /app COPY sdk/python sdk/python COPY protos protos COPY go go -COPY setup.py setup.py COPY pyproject.toml pyproject.toml COPY README.md README.md @@ -19,4 +20,4 @@ COPY README.md README.md # git dir to infer the version of feast we're installing. # https://github.com/pypa/setuptools_scm#usage-from-docker # I think it also assumes that this dockerfile is being built from the root of the directory. -RUN --mount=source=.git,target=.git,type=bind pip3 install --no-cache-dir '.[aws,gcp,k8s,snowflake,postgres]' +RUN --mount=source=.git,target=.git,type=bind uv pip install --system --no-cache-dir '.[aws,gcp,k8s,snowflake,postgres]' diff --git a/sdk/python/feast/infra/compute_engines/kubernetes/k8s_engine.py b/sdk/python/feast/infra/compute_engines/kubernetes/k8s_engine.py index 0dcff09f027..2f041301be7 100644 --- a/sdk/python/feast/infra/compute_engines/kubernetes/k8s_engine.py +++ b/sdk/python/feast/infra/compute_engines/kubernetes/k8s_engine.py @@ -145,7 +145,7 @@ def _materialize_one( offline_job = self.offline_store.pull_latest_from_table_or_query( config=self.repo_config, - data_source=feature_view.batch_source, + data_source=feature_view.batch_source, # type: ignore[arg-type] join_key_columns=join_key_columns, feature_name_columns=feature_name_columns, timestamp_field=timestamp_field, diff --git a/sdk/python/feast/infra/compute_engines/local/feature_builder.py b/sdk/python/feast/infra/compute_engines/local/feature_builder.py index a98573621fb..754a00db76f 100644 --- a/sdk/python/feast/infra/compute_engines/local/feature_builder.py +++ b/sdk/python/feast/infra/compute_engines/local/feature_builder.py @@ -1,5 +1,7 @@ +import logging from typing import Union +from feast.aggregation import aggregation_specs_to_agg_ops from feast.infra.common.materialization_job import MaterializationTask from feast.infra.common.retrieval_task import HistoricalRetrievalTask from feast.infra.compute_engines.backends.base import DataFrameBackend @@ -15,6 +17,9 @@ LocalValidationNode, ) from feast.infra.registry.base_registry import BaseRegistry +from feast.types import PrimitiveFeastType, from_feast_to_pyarrow_type + +logger = logging.getLogger(__name__) class LocalFeatureBuilder(FeatureBuilder): @@ -54,7 +59,12 @@ def build_filter_node(self, view, input_node): def build_aggregation_node(self, view, input_node): agg_specs = view.aggregations - agg_ops = self._get_aggregate_operations(agg_specs) + agg_ops = aggregation_specs_to_agg_ops( + agg_specs, + time_window_unsupported_error_message=( + "Time window aggregation is not supported in the local compute engine." + ), + ) group_by_keys = view.entities node = LocalAggregationNode( "agg", self.backend, group_by_keys, agg_ops, inputs=[input_node] @@ -82,7 +92,36 @@ def build_transformation_node(self, view, input_nodes): return node def build_validation_node(self, view, input_node): - validation_config = view.validation_config + validation_config = getattr(view, "validation_config", None) or {} + + if not validation_config.get("columns") and hasattr(view, "features"): + columns = {} + json_columns = set() + for feature in view.features: + try: + columns[feature.name] = from_feast_to_pyarrow_type(feature.dtype) + except (ValueError, KeyError): + logger.debug( + "Could not resolve PyArrow type for feature '%s' " + "(dtype=%s), skipping type check for this column.", + feature.name, + feature.dtype, + ) + columns[feature.name] = None + # Track which columns are Json type for content validation + if ( + isinstance(feature.dtype, PrimitiveFeastType) + and feature.dtype.name == "JSON" + ): + json_columns.add(feature.name) + if columns: + validation_config = {**validation_config, "columns": columns} + if json_columns: + validation_config = { + **validation_config, + "json_columns": json_columns, + } + node = LocalValidationNode( "validate", validation_config, self.backend, inputs=[input_node] ) @@ -93,15 +132,3 @@ def build_output_nodes(self, view, input_node): node = LocalOutputNode("output", self.dag_root.view, inputs=[input_node]) self.nodes.append(node) return node - - @staticmethod - def _get_aggregate_operations(agg_specs): - agg_ops = {} - for agg in agg_specs: - if agg.time_window is not None: - raise ValueError( - "Time window aggregation is not supported in the local compute engine." - ) - alias = f"{agg.function}_{agg.column}" - agg_ops[alias] = (agg.function, agg.column) - return agg_ops diff --git a/sdk/python/feast/infra/compute_engines/local/nodes.py b/sdk/python/feast/infra/compute_engines/local/nodes.py index 985a089daae..9d3e1a48881 100644 --- a/sdk/python/feast/infra/compute_engines/local/nodes.py +++ b/sdk/python/feast/infra/compute_engines/local/nodes.py @@ -1,5 +1,7 @@ +import json +import logging from datetime import datetime, timedelta -from typing import List, Optional, Union +from typing import List, Optional, Set, Union import pyarrow as pa @@ -12,14 +14,13 @@ from feast.infra.compute_engines.local.arrow_table_value import ArrowTableValue from feast.infra.compute_engines.local.local_node import LocalNode from feast.infra.compute_engines.utils import ( + ENTITY_TS_ALIAS, create_offline_store_retrieval_job, -) -from feast.infra.offline_stores.offline_utils import ( - infer_event_timestamp_from_entity_df, + infer_entity_timestamp_column, ) from feast.utils import _convert_arrow_to_proto -ENTITY_TS_ALIAS = "__entity_event_timestamp" +logger = logging.getLogger(__name__) class LocalSourceReadNode(LocalNode): @@ -75,6 +76,10 @@ def execute(self, context: ExecutionContext) -> ArrowTableValue: for val in input_values: val.assert_format(DAGFormat.ARROW) + # The upstream source-read node has already renamed columns via + # field_mapping, so use the mapped join keys for joining (see #5942). + join_keys = self.column_info.join_keys_columns + # Convert all upstream ArrowTables to backend DataFrames joined_df = self.backend.from_arrow(input_values[0].data) for val in input_values[1:]: @@ -82,7 +87,7 @@ def execute(self, context: ExecutionContext) -> ArrowTableValue: joined_df = self.backend.join( joined_df, next_df, - on=self.column_info.join_keys, + on=join_keys, how=self.how, ) @@ -91,7 +96,7 @@ def execute(self, context: ExecutionContext) -> ArrowTableValue: entity_df = self.backend.from_arrow(pa.Table.from_pandas(context.entity_df)) entity_schema = dict(zip(entity_df.columns, entity_df.dtypes)) - entity_ts_col = infer_event_timestamp_from_entity_df(entity_schema) + entity_ts_col = infer_entity_timestamp_column(entity_schema) if entity_ts_col != ENTITY_TS_ALIAS: entity_df = self.backend.rename_columns( @@ -101,7 +106,7 @@ def execute(self, context: ExecutionContext) -> ArrowTableValue: joined_df = self.backend.join( entity_df, joined_df, - on=self.column_info.join_keys, + on=join_keys, how="left", ) @@ -189,8 +194,10 @@ def execute(self, context: ExecutionContext) -> ArrowTableValue: # Extract join_keys, timestamp, and created_ts from context - # Dedup strategy: sort and drop_duplicates - dedup_keys = self.column_info.join_keys + # Dedup strategy: sort and drop_duplicates. Use the mapped join key + # names so we look up the columns that the source-read node has + # already renamed (see issue #5942). + dedup_keys = self.column_info.join_keys_columns if dedup_keys: sort_keys = [self.column_info.timestamp_column] if ( @@ -236,15 +243,114 @@ def __init__( def execute(self, context: ExecutionContext) -> ArrowTableValue: input_table = self.get_single_table(context).data - df = self.backend.from_arrow(input_table) - # Placeholder for actual validation logic + if self.validation_config: - print(f"[Validation: {self.name}] Passed.") - result = self.backend.to_arrow(df) - output = ArrowTableValue(result) + self._validate_schema(input_table) + + output = ArrowTableValue(input_table) context.node_outputs[self.name] = output return output + def _validate_schema(self, table: pa.Table): + """Validate that the input table conforms to the expected schema. + + Checks that all expected columns are present, that their types + are compatible with the declared Feast types, and that Json columns + contain well-formed JSON. Logs warnings for type mismatches but + raises on missing columns or invalid JSON content. + """ + expected_columns = self.validation_config.get("columns", {}) + if not expected_columns: + logger.debug( + "[Validation: %s] No column schema to validate against.", + self.name, + ) + return + + actual_columns = set(table.column_names) + expected_names = set(expected_columns.keys()) + + missing = expected_names - actual_columns + if missing: + raise ValueError( + f"[Validation: {self.name}] Missing expected columns: {missing}. " + f"Actual columns: {sorted(actual_columns)}" + ) + + for col_name, expected_type in expected_columns.items(): + actual_type = table.schema.field(col_name).type + if expected_type is not None and actual_type != expected_type: + # PyArrow map columns and struct columns are compatible + # with the Feast Map type — skip warning for these cases + if pa.types.is_map(expected_type) and ( + pa.types.is_map(actual_type) + or pa.types.is_struct(actual_type) + or pa.types.is_large_list(actual_type) + or pa.types.is_list(actual_type) + ): + continue + + # JSON type (large_string) is compatible with string types + if pa.types.is_large_string(expected_type) and ( + pa.types.is_string(actual_type) + or pa.types.is_large_string(actual_type) + ): + continue + + # Struct type — expected struct is compatible with actual + # struct or map representations + if pa.types.is_struct(expected_type) and ( + pa.types.is_struct(actual_type) + or pa.types.is_map(actual_type) + or pa.types.is_list(actual_type) + ): + continue + + logger.warning( + "[Validation: %s] Column '%s' type mismatch: expected %s, got %s", + self.name, + col_name, + expected_type, + actual_type, + ) + + # Validate JSON well-formedness for declared Json columns + json_columns: Set[str] = self.validation_config.get("json_columns", set()) + for col_name in json_columns: + if col_name not in actual_columns: + continue + + column = table.column(col_name) + invalid_count = 0 + first_error = None + first_error_row = None + + for i in range(len(column)): + value = column[i] + if not value.is_valid: + continue + + str_value = value.as_py() + if not isinstance(str_value, str): + continue + + try: + json.loads(str_value) + except (json.JSONDecodeError, TypeError) as e: + invalid_count += 1 + if first_error is None: + first_error = str(e) + first_error_row = i + + if invalid_count > 0: + raise ValueError( + f"[Validation: {self.name}] Column '{col_name}' declared as Json " + f"contains {invalid_count} invalid JSON value(s). " + f"First error at row {first_error_row}: {first_error}" + ) + + logger.debug("[Validation: %s] Schema validation passed.", self.name) + class LocalOutputNode(LocalNode): def __init__( @@ -258,10 +364,11 @@ def __init__( def execute(self, context: ExecutionContext) -> ArrowTableValue: input_table = self.get_single_table(context).data - context.node_outputs[self.name] = input_table + output = ArrowTableValue(data=input_table) + context.node_outputs[self.name] = output if input_table.num_rows == 0: - return input_table + return output if self.feature_view.online: online_store = context.online_store @@ -271,16 +378,25 @@ def execute(self, context: ExecutionContext) -> ArrowTableValue: for entity in self.feature_view.entity_columns } - rows_to_write = _convert_arrow_to_proto( - input_table, self.feature_view, join_key_to_value_type + batch_size = ( + context.repo_config.materialization_config.online_write_batch_size ) - - online_store.online_write_batch( - config=context.repo_config, - table=self.feature_view, - data=rows_to_write, - progress=lambda x: None, + # Single batch if None (backward compatible), otherwise use configured batch_size + batches = ( + [input_table] + if batch_size is None + else input_table.to_batches(max_chunksize=batch_size) ) + for batch in batches: + rows_to_write = _convert_arrow_to_proto( + batch, self.feature_view, join_key_to_value_type + ) + online_store.online_write_batch( + config=context.repo_config, + table=self.feature_view, + data=rows_to_write, + progress=lambda x: None, + ) if self.feature_view.offline: offline_store = context.offline_store @@ -291,4 +407,4 @@ def execute(self, context: ExecutionContext) -> ArrowTableValue: progress=lambda x: None, ) - return input_table + return output diff --git a/sdk/python/feast/infra/compute_engines/ray/compute.py b/sdk/python/feast/infra/compute_engines/ray/compute.py index a5c1b3caab5..fb870f9c308 100644 --- a/sdk/python/feast/infra/compute_engines/ray/compute.py +++ b/sdk/python/feast/infra/compute_engines/ray/compute.py @@ -22,7 +22,10 @@ RayDAGRetrievalJob, RayMaterializationJob, ) -from feast.infra.compute_engines.ray.utils import write_to_online_store +from feast.infra.compute_engines.ray.utils import ( + write_to_online_store, + write_to_online_store_from_ray_ds, +) from feast.infra.offline_stores.offline_store import RetrievalJob from feast.infra.ray_initializer import ( ensure_ray_initialized, @@ -163,7 +166,7 @@ def _materialize_from_offline_store( # Pull data from offline store retrieval_job = self.offline_store.pull_latest_from_table_or_query( config=self.repo_config, - data_source=feature_view.batch_source, + data_source=feature_view.batch_source, # type: ignore[arg-type] join_key_columns=join_key_columns, feature_name_columns=feature_name_columns, timestamp_field=timestamp_field, @@ -172,43 +175,93 @@ def _materialize_from_offline_store( end_date=end_date, ) - # Convert to Arrow Table and write to online/offline stores - arrow_table = retrieval_job.to_arrow() - - # Write to online store if enabled - write_to_online_store( - arrow_table=arrow_table, - feature_view=feature_view, - online_store=self.online_store, - repo_config=self.repo_config, + # Prefer the distributed Ray path: keep data on the cluster and + # write each partition in parallel. Fall back to the Arrow driver + # path for non-Ray retrieval jobs (e.g. Dask, DuckDB offline stores). + from feast.infra.offline_stores.contrib.ray_offline_store.ray import ( + RayRetrievalJob, ) - # Write to offline store if enabled (this handles sink_source automatically for derived views) - if getattr(feature_view, "offline", False): - self.offline_store.offline_write_batch( - config=self.repo_config, + if isinstance(retrieval_job, RayRetrievalJob): + ray_ds = retrieval_job.to_ray_dataset() + + needs_offline = getattr(feature_view, "offline", False) + sink_source = getattr(feature_view, "sink_source", None) + + # Materialise the lazy pipeline ONCE into Ray object-store memory + # when more than one write target will consume it. + # Without this, each use of ray_ds (online write via map_batches, + # to_arrow_refs for offline_write_batch, write_parquet for + # sink_source) independently re-executes the full source-read + # pipeline, producing up to three separate data snapshots that can + # diverge when the source changes between executions. + if needs_offline or sink_source is not None: + ray_ds = ray_ds.materialize() + + # Distributed online store write — each Ray worker writes its shard + write_to_online_store_from_ray_ds( + ray_ds=ray_ds, feature_view=feature_view, - table=arrow_table, - progress=lambda x: None, + online_store=self.online_store, + repo_config=self.repo_config, ) - # For derived views, also ensure data is written to sink_source if it exists - # This is critical for feature view chaining to work properly - sink_source = getattr(feature_view, "sink_source", None) - if sink_source is not None: - logger.debug( - f"Writing derived view {feature_view.name} to sink_source: {sink_source.path}" + # offline_write_batch and sink_source are independent — both can + # apply when a feature view has offline=True AND a sink_source. + if needs_offline: + import pyarrow as pa + import ray as _ray + + arrow_table = pa.concat_tables(_ray.get(ray_ds.to_arrow_refs())) + self.offline_store.offline_write_batch( + config=self.repo_config, + feature_view=feature_view, + table=arrow_table, + progress=lambda x: None, + ) + + if sink_source is not None: + logger.debug( + f"Writing derived view {feature_view.name} to sink_source: {sink_source.path}" + ) + try: + ray_ds.write_parquet(sink_source.path) + except Exception as e: + logger.error( + f"Failed to write to sink_source {sink_source.path}: {e}" + ) + else: + # Non-Ray offline store: collect on driver and write sequentially + arrow_table = retrieval_job.to_arrow() + + write_to_online_store( + arrow_table=arrow_table, + feature_view=feature_view, + online_store=self.online_store, + repo_config=self.repo_config, ) - # Write to sink_source using Ray data - try: - ray_wrapper = get_ray_wrapper() - ray_dataset = ray_wrapper.from_arrow(arrow_table) - ray_dataset.write_parquet(sink_source.path) - except Exception as e: - logger.error( - f"Failed to write to sink_source {sink_source.path}: {e}" + if getattr(feature_view, "offline", False): + self.offline_store.offline_write_batch( + config=self.repo_config, + feature_view=feature_view, + table=arrow_table, + progress=lambda x: None, + ) + + sink_source = getattr(feature_view, "sink_source", None) + if sink_source is not None: + logger.debug( + f"Writing derived view {feature_view.name} to sink_source: {sink_source.path}" ) + try: + ray_wrapper = get_ray_wrapper() + ray_dataset = ray_wrapper.from_arrow(arrow_table) + ray_dataset.write_parquet(sink_source.path) + except Exception as e: + logger.error( + f"Failed to write to sink_source {sink_source.path}: {e}" + ) return RayMaterializationJob( job_id=job_id, status=MaterializationJobStatus.SUCCEEDED, diff --git a/sdk/python/feast/infra/compute_engines/ray/config.py b/sdk/python/feast/infra/compute_engines/ray/config.py index bb6b63a05c5..3e231f576ad 100644 --- a/sdk/python/feast/infra/compute_engines/ray/config.py +++ b/sdk/python/feast/infra/compute_engines/ray/config.py @@ -41,11 +41,69 @@ class RayComputeEngineConfig(FeastConfigBaseModel): # Additional configuration options max_workers: Optional[int] = None - """Maximum number of Ray workers. If None, uses all available cores.""" + """Maximum number of Ray workers for transformation and join nodes. + If None, Ray uses all available cores.""" + + write_concurrency: Optional[int] = None + """Concurrency for the RayWriteNode's map_batches call (online-store writes). + If None, falls back to max_workers, then 1 (safe default + for single-file stores). + + Example - SQLite online store (default for local deployments): + write_concurrency: 1 + + Example - Redis / DynamoDB online store (supports parallel writes): + write_concurrency: 8 + """ enable_optimization: bool = True """Enable automatic performance optimizations.""" + # Worker task resource configuration + num_gpus: Optional[float] = None + """Number of GPUs to request per worker task. Requires GPU nodes in the + Ray cluster. Fractional values (e.g. 0.5) are supported by Ray for GPU + sharing. Supported in all modes: local, remote, and KubeRay.""" + + gpu_batch_format: str = "pandas" + """Batch format for map_batches when num_gpus is set. Use 'numpy' or + 'pyarrow' for GPU-native libraries (e.g. cuDF, PyTorch). Defaults to + 'pandas'.""" + + worker_task_options: Optional[Dict[str, Any]] = None + """Arbitrary Ray task options passed verbatim to @ray.remote .options() + and map_batches for every worker task Feast dispatches. This is the + escape hatch for any Ray or CodeFlare SDK scheduling parameter not + covered by the dedicated fields above. + + Pairs with ray_conf (which configures ray.init) — worker_task_options + targets the individual worker tasks rather than the cluster connection. + + Common keys (see https://docs.ray.io/en/latest/ray-core/api/doc/ray.remote_function.RemoteFunction.options.html): + num_cpus (float) – CPUs per task (default: 1) + memory (int) – Heap memory in bytes (e.g. 8 * 1024**3 for 8 GB) + accelerator_type (str) – Specific GPU model, e.g. 'A100', 'T4', 'V100'. + Pins tasks to nodes advertising that type. Useful + on KubeRay clusters with mixed GPU pools. + resources (dict) – Custom/extended resource labels, e.g. + {'intel.com/gpu': 1} for Kubernetes extended resources. + runtime_env (dict) – Per-task runtime environment (pip, conda, env_vars, + working_dir, …). For KubeRay use this to install + extra packages on workers without rebuilding images. + max_retries (int) – Task retry count on worker failure (default: 3). + scheduling_strategy (str) – 'DEFAULT', 'SPREAD', or a placement group strategy. + + Example: + worker_task_options: + num_cpus: 4 + memory: 8589934592 # 8 GB + accelerator_type: "A100" + max_retries: 5 + runtime_env: + pip: ["cudf-cu12==24.10.0"] + env_vars: {CUDA_VISIBLE_DEVICES: "0"} + """ + @property def window_size_timedelta(self) -> timedelta: """Convert window size string to timedelta.""" diff --git a/sdk/python/feast/infra/compute_engines/ray/feature_builder.py b/sdk/python/feast/infra/compute_engines/ray/feature_builder.py index 49a957da183..f0a26df263b 100644 --- a/sdk/python/feast/infra/compute_engines/ray/feature_builder.py +++ b/sdk/python/feast/infra/compute_engines/ray/feature_builder.py @@ -17,8 +17,10 @@ RayJoinNode, RayReadNode, RayTransformationNode, + RayValidationNode, RayWriteNode, ) +from feast.types import PrimitiveFeastType, from_feast_to_pyarrow_type if TYPE_CHECKING: from feast.infra.compute_engines.ray.config import RayComputeEngineConfig @@ -75,12 +77,17 @@ def build_aggregation_node(self, view, input_node: DAGNode) -> DAGNode: group_by_keys = view.entities timestamp_col = getattr(view.batch_source, "timestamp_field", "event_timestamp") + enable_tiling = getattr(view, "enable_tiling", False) + tiling_hop_size = getattr(view, "tiling_hop_size", None) + node = RayAggregationNode( name="aggregation", aggregations=agg_specs, group_by_keys=group_by_keys, timestamp_col=timestamp_col, config=self.config, + enable_tiling=enable_tiling, + hop_size=tiling_hop_size, ) node.add_input(input_node) @@ -129,6 +136,7 @@ def build_dedup_node(self, view, input_node): name="dedup", column_info=column_info, config=self.config, + is_materialization=self.is_materialization, ) node.add_input(input_node) @@ -169,11 +177,36 @@ def build_output_nodes(self, view, final_node): def build_validation_node(self, view, input_node): """Build the validation node for feature validation.""" - # TODO: Implement validation logic - logger.warning( - "Feature validation is not yet implemented for Ray compute engine." + expected_columns = {} + json_columns: set = set() + if hasattr(view, "features"): + for feature in view.features: + try: + expected_columns[feature.name] = from_feast_to_pyarrow_type( + feature.dtype + ) + except (ValueError, KeyError): + logger.debug( + "Could not resolve PyArrow type for feature '%s' " + "(dtype=%s), skipping type check for this column.", + feature.name, + feature.dtype, + ) + expected_columns[feature.name] = None + if ( + isinstance(feature.dtype, PrimitiveFeastType) + and feature.dtype.name == "JSON" + ): + json_columns.add(feature.name) + + node = RayValidationNode( + f"{view.name}:validate", + expected_columns=expected_columns, + json_columns=json_columns, + inputs=[input_node], ) - return input_node + self.nodes.append(node) + return node def _build(self, view, input_nodes: Optional[List[DAGNode]]) -> DAGNode: has_physical_source = (hasattr(view, "batch_source") and view.batch_source) or ( diff --git a/sdk/python/feast/infra/compute_engines/ray/job.py b/sdk/python/feast/infra/compute_engines/ray/job.py index 06eea4e5d88..e818220baff 100644 --- a/sdk/python/feast/infra/compute_engines/ray/job.py +++ b/sdk/python/feast/infra/compute_engines/ray/job.py @@ -18,6 +18,9 @@ from feast.infra.compute_engines.dag.model import DAGFormat from feast.infra.compute_engines.dag.plan import ExecutionPlan from feast.infra.compute_engines.dag.value import DAGValue +from feast.infra.offline_stores.contrib.ray_offline_store.ray import ( + REMOTE_STORAGE_SCHEMES, +) from feast.infra.offline_stores.file_source import SavedDatasetFileStorage from feast.infra.offline_stores.offline_store import RetrievalJob, RetrievalMetadata from feast.infra.ray_initializer import get_ray_wrapper @@ -205,7 +208,7 @@ def persist( destination_path = storage.file_options.uri # Check if destination already exists - if not destination_path.startswith(("s3://", "gs://", "hdfs://")): + if not destination_path.startswith(REMOTE_STORAGE_SCHEMES): import os if not allow_overwrite and os.path.exists(destination_path): diff --git a/sdk/python/feast/infra/compute_engines/ray/nodes.py b/sdk/python/feast/infra/compute_engines/ray/nodes.py index 32126a9e42f..edc6dafd5cd 100644 --- a/sdk/python/feast/infra/compute_engines/ray/nodes.py +++ b/sdk/python/feast/infra/compute_engines/ray/nodes.py @@ -1,6 +1,7 @@ +import json import logging from datetime import datetime, timedelta, timezone -from typing import List, Optional, Union +from typing import Any, Dict, List, Optional, Set, Union import dill import pandas as pd @@ -22,7 +23,10 @@ safe_batch_processor, write_to_online_store, ) -from feast.infra.compute_engines.utils import create_offline_store_retrieval_job +from feast.infra.compute_engines.utils import ( + ENTITY_TS_ALIAS, + create_offline_store_retrieval_job, +) from feast.infra.ray_initializer import get_ray_wrapper from feast.infra.ray_shared_utils import ( apply_field_mapping, @@ -32,9 +36,6 @@ logger = logging.getLogger(__name__) -# Entity timestamp alias for historical feature retrieval -ENTITY_TS_ALIAS = "__entity_event_timestamp" - class RayReadNode(DAGNode): """ @@ -326,12 +327,16 @@ def __init__( group_by_keys: List[str], timestamp_col: str, config: RayComputeEngineConfig, + enable_tiling: bool = False, + hop_size: Optional[timedelta] = None, ): super().__init__(name) self.aggregations = aggregations self.group_by_keys = group_by_keys self.timestamp_col = timestamp_col self.config = config + self.enable_tiling = enable_tiling + self.hop_size = hop_size def execute(self, context: ExecutionContext) -> DAGValue: """Execute the aggregation operation.""" @@ -339,12 +344,124 @@ def execute(self, context: ExecutionContext) -> DAGValue: input_value.assert_format(DAGFormat.RAY) dataset: Dataset = input_value.data + # Check if tiling should be used + has_time_windows = any(agg.time_window for agg in self.aggregations) + if self.enable_tiling and has_time_windows: + return self._execute_tiled_aggregation(dataset) + else: + return self._execute_standard_aggregation(dataset) + + def _execute_tiled_aggregation(self, dataset: Dataset) -> DAGValue: + """ + Execute tiled aggregation. + + Flow: + 1. Convert Ray Dataset → pandas + 2. Generate cumulative tiles + 3. Convert to windowed aggregations + 4. Convert pandas → Ray Dataset + """ + from feast.aggregation.tiling.orchestrator import apply_sawtooth_window_tiling + from feast.aggregation.tiling.tile_subtraction import ( + convert_cumulative_to_windowed, + deduplicate_keep_latest, + ) + + ray_wrapper = get_ray_wrapper() + + input_pdf = dataset.to_pandas() + + for agg in self.aggregations: + if agg.time_window is None: + raise ValueError( + f"Tiling is enabled but aggregation on column '{agg.column}' has no time_window set. " + f"Either set time_window for all aggregations or disable tiling by setting enable_tiling=False." + ) + + # Group aggregations by time window + window_to_aggs: Dict[timedelta, List[Aggregation]] = {} + for agg in self.aggregations: + if agg.time_window: + if agg.time_window not in window_to_aggs: + window_to_aggs[agg.time_window] = [] + window_to_aggs[agg.time_window].append(agg) + + # Process each time window in pandas + windowed_pdfs = [] + for window_size, window_aggs in window_to_aggs.items(): + # Step 1: Generate cumulative tiles + tiles_pdf = apply_sawtooth_window_tiling( + df=input_pdf, + aggregations=window_aggs, + group_by_keys=self.group_by_keys, + timestamp_col=self.timestamp_col, + window_size=window_size, + hop_size=self.hop_size or timedelta(minutes=5), + ) + + if tiles_pdf.empty: + continue + + # Step 2: Convert to windowed aggregations + windowed_pdf = convert_cumulative_to_windowed( + tiles_df=tiles_pdf, + entity_keys=self.group_by_keys, + timestamp_col=self.timestamp_col, + window_size=window_size, + aggregations=window_aggs, + ) + + if not windowed_pdf.empty: + windowed_pdfs.append(windowed_pdf) + + if not windowed_pdfs: + # No results, return empty Ray Dataset + aggregated_dataset = ray_wrapper.from_pandas(pd.DataFrame()) + else: + # Step 3: Join all windows in pandas (outer merge on entity keys + timestamp) + if len(windowed_pdfs) == 1: + final_pdf = windowed_pdfs[0] + else: + final_pdf = windowed_pdfs[0] + join_keys = self.group_by_keys + [self.timestamp_col] + for pdf in windowed_pdfs[1:]: + final_pdf = pd.merge( + final_pdf, + pdf, + on=join_keys, + how="outer", + suffixes=("", "_dup"), + ) + # Drop duplicate columns from merge + final_pdf = final_pdf.loc[ + :, ~final_pdf.columns.str.endswith("_dup") + ] + + # Step 4: Deduplicate in pandas (keep latest timestamp per entity) + if self.timestamp_col in final_pdf.columns and not final_pdf.empty: + final_pdf = deduplicate_keep_latest( + final_pdf, self.group_by_keys, self.timestamp_col + ) + + aggregated_dataset = ray_wrapper.from_pandas(final_pdf) + + return DAGValue( + data=aggregated_dataset, + format=DAGFormat.RAY, + metadata={ + "aggregated": True, + "aggregations": len(self.aggregations), + "group_by_keys": self.group_by_keys, + "tiled": True, + }, + ) + + def _execute_standard_aggregation(self, dataset: Dataset) -> DAGValue: + """Execute standard aggregation without tiling.""" # Convert aggregations to Ray's groupby format agg_dict = {} for agg in self.aggregations: - feature_name = f"{agg.function}_{agg.column}" - if agg.time_window: - feature_name += f"_{int(agg.time_window.total_seconds())}s" + feature_name = agg.resolved_name(agg.time_window) if agg.function == "count": agg_dict[feature_name] = (agg.column, "count") @@ -360,6 +477,8 @@ def execute(self, context: ExecutionContext) -> DAGValue: agg_dict[feature_name] = (agg.column, "std") elif agg.function == "var": agg_dict[feature_name] = (agg.column, "var") + elif agg.function == "count_distinct": + agg_dict[feature_name] = (agg.column, "nunique") else: raise ValueError(f"Unknown aggregation function: {agg.function}.") @@ -414,6 +533,8 @@ def _fallback_pandas_aggregation(self, dataset: Dataset, agg_dict: dict) -> Data result = grouped[column].std() elif function == "var": result = grouped[column].var() + elif function == "nunique": + result = grouped[column].nunique() else: raise ValueError(f"Unknown aggregation function: {function}.") @@ -438,6 +559,20 @@ def _fallback_pandas_aggregation(self, dataset: Dataset, agg_dict: dict) -> Data class RayDedupNode(DAGNode): """ Ray node for deduplicating records. + + Two dedup strategies are provided: + + * **Materialization** (``is_materialization=True``): per-block + ``drop_duplicates``. This is streaming-friendly because it never needs + to see all blocks at once. Any cross-block duplicates are resolved by + the online store, which does an UPSERT and therefore naturally keeps the + last-written value. This avoids the ``groupby().map_groups()`` full + shuffle that would otherwise block until every single block was produced. + + * **Historical retrieval** (``is_materialization=False``): global + ``groupby().map_groups()``. Correctness is required here because the + entity-timestamp join must return exactly one feature row per + (entity, query-timestamp) pair. """ def __init__( @@ -445,10 +580,12 @@ def __init__( name: str, column_info, config: RayComputeEngineConfig, + is_materialization: bool = False, ): super().__init__(name) self.column_info = column_info self.config = config + self.is_materialization = is_materialization def execute(self, context: ExecutionContext) -> DAGValue: """Execute the deduplication operation.""" @@ -456,39 +593,59 @@ def execute(self, context: ExecutionContext) -> DAGValue: input_value.assert_format(DAGFormat.RAY) dataset: Dataset = input_value.data - @safe_batch_processor - def deduplicate_batch(batch: pd.DataFrame) -> pd.DataFrame: - """Remove duplicates from the batch.""" - # Get deduplication keys - join_keys = self.column_info.join_keys - timestamp_col = self.column_info.timestamp_column - - if join_keys: - # Sort by join keys and timestamp (most recent first) - sort_columns = join_keys + [timestamp_col] - available_columns = [ - col for col in sort_columns if col in batch.columns - ] + join_keys = self.column_info.join_keys + timestamp_col = self.column_info.timestamp_column - if available_columns: - # Sort and deduplicate - sorted_batch = batch.sort_values( - available_columns, - ascending=[True] * len(join_keys) - + [False], # Recent timestamps first - ) + if join_keys: + if self.is_materialization: + # Per-block dedup: streaming-safe, no full shuffle required. + # Cross-block duplicates are handled by the online-store UPSERT. + # + # IMPORTANT: do NOT call dataset.schema() here. For streaming + # datasets backed by slow map_batches actors, .schema() triggers + # eager block execution to + # infer the output type. Those blocks are consumed and LOST — + # they never reach the write stage. We therefore defer the + # column-existence check to inside _dedup_block, which runs in + # a worker per block without interfering with streaming. + _join_keys = list(join_keys) + _ts_col = timestamp_col + + def _dedup_block(block: pd.DataFrame) -> pd.DataFrame: + available = [k for k in _join_keys if k in block.columns] + if not available: + return block + if _ts_col and _ts_col in block.columns: + block = block.sort_values(_ts_col, ascending=False) + return block.drop_duplicates(subset=available) + + dataset = dataset.map_batches(_dedup_block, batch_format="pandas") + else: + # Global dedup via groupby: required for historical retrieval + # where the entity–timestamp join must return exactly one row + # per (entity, query-timestamp) pair. + # NOTE: groupby().map_groups() is a full shuffle and blocks + # until ALL upstream blocks are produced. Use only when + # correctness across partition boundaries is mandatory. + available_join_keys = [ + k for k in join_keys if k in dataset.schema().names + ] + available_ts_col = ( + timestamp_col if timestamp_col in dataset.schema().names else None + ) - # Keep first occurrence (most recent) for each join key combination - deduped_batch = sorted_batch.drop_duplicates( - subset=join_keys, - keep="first", - ) + if available_join_keys: - return deduped_batch + def _keep_latest_in_group(group: pd.DataFrame) -> pd.DataFrame: + if available_ts_col and available_ts_col in group.columns: + group = group.sort_values(available_ts_col, ascending=False) + return group.head(1) - return batch + dataset = dataset.groupby(available_join_keys).map_groups( + _keep_latest_in_group, batch_format="pandas" + ) - deduped_dataset = dataset.map_batches(deduplicate_batch, batch_format="pandas") + deduped_dataset = dataset return DAGValue( data=deduped_dataset, @@ -567,10 +724,36 @@ def apply_transformation_with_serialized_udf( return transformed_batch + num_gpus = getattr(self.config, "num_gpus", None) or None + task_options: Dict[str, Any] = dict( + getattr(self.config, "worker_task_options", None) or {} + ) + if num_gpus: + task_options["num_gpus"] = num_gpus + batch_format = ( + getattr(self.config, "gpu_batch_format", "pandas") + if num_gpus + else "pandas" + ) + # Only the scheduling-relevant subset flows into map_batches + _MAP_BATCHES_RESOURCE_KEYS = { + "num_gpus", + "num_cpus", + "accelerator_type", + "resources", + } + map_kwargs: Dict[str, Any] = { + "batch_format": batch_format, + "concurrency": self.config.max_workers or 12, + **{ + k: v + for k, v in task_options.items() + if k in _MAP_BATCHES_RESOURCE_KEYS + }, + } transformed_dataset = dataset.map_batches( apply_transformation_with_serialized_udf, - batch_format="pandas", - concurrency=self.config.max_workers or 12, + **map_kwargs, ) return DAGValue( @@ -709,10 +892,19 @@ def write_batch_with_serialized_artifacts(batch: pd.DataFrame) -> pd.DataFrame: return batch + # Resolve write concurrency from config. + # write_concurrency takes precedence; falls back to max_workers, then 1. + if self.config is not None and self.config.write_concurrency is not None: + _write_concurrency = self.config.write_concurrency + elif self.config is not None and self.config.max_workers is not None: + _write_concurrency = self.config.max_workers + else: + _write_concurrency = 1 + written_dataset = dataset.map_batches( write_batch_with_serialized_artifacts, batch_format="pandas", - concurrency=self.config.max_workers if self.config else 12, + concurrency=_write_concurrency, ) written_dataset = written_dataset.materialize() @@ -729,3 +921,127 @@ def write_batch_with_serialized_artifacts(batch: pd.DataFrame) -> pd.DataFrame: ), }, ) + + +class RayValidationNode(DAGNode): + """ + Ray node for validating feature data against the declared schema. + + Checks that all expected columns are present and logs warnings for + type mismatches. Validation runs once on the first batch to avoid + per-batch overhead; the full dataset is passed through unchanged. + """ + + def __init__( + self, + name: str, + expected_columns: Dict[str, Optional[pa.DataType]], + json_columns: Optional[Set[str]] = None, + inputs: Optional[List[DAGNode]] = None, + ): + super().__init__(name, inputs=inputs) + self.expected_columns = expected_columns + self.json_columns = json_columns or set() + + def execute(self, context: ExecutionContext) -> DAGValue: + input_value = self.get_single_input_value(context) + dataset = input_value.data + + if not self.expected_columns: + context.node_outputs[self.name] = input_value + return input_value + + expected_names = set(self.expected_columns.keys()) + + schema = dataset.schema() + actual_columns = set(schema.names) + + missing = expected_names - actual_columns + if missing: + raise ValueError( + f"[Validation: {self.name}] Missing expected columns: {missing}. " + f"Actual columns: {sorted(actual_columns)}" + ) + + for col_name, expected_type in self.expected_columns.items(): + if expected_type is None: + continue + actual_field = schema.field(col_name) + actual_type = actual_field.type + if actual_type != expected_type: + # Map type compatibility + if pa.types.is_map(expected_type) and ( + pa.types.is_map(actual_type) + or pa.types.is_struct(actual_type) + or pa.types.is_list(actual_type) + ): + continue + + # JSON type compatibility (large_string / string) + if pa.types.is_large_string(expected_type) and ( + pa.types.is_string(actual_type) + or pa.types.is_large_string(actual_type) + ): + continue + + # Struct type compatibility + if pa.types.is_struct(expected_type) and ( + pa.types.is_struct(actual_type) + or pa.types.is_map(actual_type) + or pa.types.is_list(actual_type) + ): + continue + + logger.warning( + "[Validation: %s] Column '%s' type mismatch: expected %s, got %s", + self.name, + col_name, + expected_type, + actual_type, + ) + + # Validate JSON well-formedness for declared Json columns + if self.json_columns: + try: + first_batch = dataset.take_batch(1000) + except Exception: + logger.debug( + "[Validation: %s] Could not sample batch for JSON validation.", + self.name, + ) + first_batch = None + + if first_batch is not None: + for col_name in self.json_columns: + if col_name not in first_batch: + continue + + values = first_batch[col_name] + invalid_count = 0 + first_error = None + first_error_row = None + + for i, value in enumerate(values): + if value is None: + continue + if not isinstance(value, str): + continue + try: + json.loads(value) + except (json.JSONDecodeError, TypeError) as e: + invalid_count += 1 + if first_error is None: + first_error = str(e) + first_error_row = i + + if invalid_count > 0: + raise ValueError( + f"[Validation: {self.name}] Column '{col_name}' declared " + f"as Json contains {invalid_count} invalid JSON value(s) " + f"in sampled batch. First error at row {first_error_row}: " + f"{first_error}" + ) + + logger.debug("[Validation: %s] Schema validation passed.", self.name) + context.node_outputs[self.name] = input_value + return input_value diff --git a/sdk/python/feast/infra/compute_engines/ray/utils.py b/sdk/python/feast/infra/compute_engines/ray/utils.py index 94ebbe2c643..9727f010ca4 100644 --- a/sdk/python/feast/infra/compute_engines/ray/utils.py +++ b/sdk/python/feast/infra/compute_engines/ray/utils.py @@ -3,8 +3,9 @@ """ import logging -from typing import Callable, Dict, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Union +import numpy as np import pandas as pd import pyarrow as pa @@ -16,6 +17,9 @@ from feast.utils import _convert_arrow_to_proto from feast.value_type import ValueType +if TYPE_CHECKING: + import ray.data + logger = logging.getLogger(__name__) @@ -45,10 +49,107 @@ def write_to_online_store( for entity in feature_view.entity_columns } - rows_to_write = _convert_arrow_to_proto( - arrow_table, feature_view, join_key_to_value_type + batch_size = repo_config.materialization_config.online_write_batch_size + # Single batch if None (backward compatible), otherwise use configured batch_size + batches = ( + [arrow_table] + if batch_size is None + else arrow_table.to_batches(max_chunksize=batch_size) ) + total_rows = 0 + for batch in batches: + rows_to_write = _convert_arrow_to_proto( + batch, feature_view, join_key_to_value_type + ) + + if rows_to_write: + online_store.online_write_batch( + config=repo_config, + table=feature_view, + data=rows_to_write, + progress=lambda x: None, + ) + total_rows += len(rows_to_write) + + if total_rows > 0: + logger.debug( + f"Successfully wrote {total_rows} rows to online store for {feature_view.name}" + ) + else: + logger.warning(f"No rows to write for {feature_view.name}") + + except Exception as e: + logger.error(f"Failed to write to online store for {feature_view.name}: {e}") + + +# Ray Data batch type: pandas DataFrame, numpy dict, or pyarrow Table +BatchType = Union[pd.DataFrame, Dict[str, np.ndarray], pa.Table] + + +def _is_empty_batch(batch: BatchType) -> bool: + """Return True if the batch contains no rows, regardless of Ray Data batch format. + + Ray Data delivers batches in three formats depending on the batch_format + argument passed to map_batches: + - "pandas" → pd.DataFrame (.empty attribute) + - "numpy" → Dict[str, np.ndarray] (check length of first array) + - "pyarrow" → pa.Table (.num_rows attribute) + """ + if isinstance(batch, pd.DataFrame): + return batch.empty + if isinstance(batch, dict): + if not batch: + return True + first = next(iter(batch.values())) + return len(first) == 0 + if isinstance(batch, pa.Table): + return batch.num_rows == 0 + return False + + +def write_to_online_store_from_ray_ds( + ray_ds: "ray.data.Dataset", + feature_view: Union[BatchFeatureView, StreamFeatureView, FeatureView], + online_store: OnlineStore, + repo_config: RepoConfig, +) -> None: + """Write a Ray Dataset to the online store in a distributed fashion. + + Instead of collecting the entire dataset onto the driver (as + :func:`write_to_online_store` does via ``to_arrow()``), this function uses + ``ray_ds.map_batches`` so that each Ray worker writes its own partition + independently. This avoids driver memory pressure for large datasets and + fully exploits the parallelism of the Ray cluster. + + The ``online_store`` and ``repo_config`` objects must be serialisable + (picklable) because Ray ships them to remote workers. All built-in Feast + online stores satisfy this requirement. + + Args: + ray_ds: The Ray Dataset produced by + :meth:`RayRetrievalJob.to_ray_dataset`. + feature_view: Feature view being materialised. + online_store: Online store instance to write to. + repo_config: Repository configuration forwarded to the online store. + """ + if not getattr(feature_view, "online", False): + return + + join_key_to_value_type: Dict[str, ValueType] = {} + if hasattr(feature_view, "entity_columns") and feature_view.entity_columns: + join_key_to_value_type = { + entity.name: entity.dtype.to_value_type() + for entity in feature_view.entity_columns + } + + batch_size = repo_config.materialization_config.online_write_batch_size + + def _write_batch(batch: pa.Table) -> pa.Table: + """Write a single Arrow batch to the online store and pass it through.""" + rows_to_write = _convert_arrow_to_proto( + batch, feature_view, join_key_to_value_type + ) if rows_to_write: online_store.online_write_batch( config=repo_config, @@ -56,32 +157,52 @@ def write_to_online_store( data=rows_to_write, progress=lambda x: None, ) - logger.debug( - f"Successfully wrote {len(rows_to_write)} rows to online store for {feature_view.name}" - ) - else: - logger.warning(f"No rows to write for {feature_view.name}") + return batch + + # Ray's map_batches requires a positive integer or "default" for batch_size; + # None is not accepted. When no explicit batch size is configured, omit the + # argument entirely so Ray uses its own default partitioning heuristic. + map_batches_kwargs: dict[str, Any] = { + "batch_format": "pyarrow", + "zero_copy_batch": True, + } + if batch_size is not None: + map_batches_kwargs["batch_size"] = batch_size + try: + ray_ds.map_batches(_write_batch, **map_batches_kwargs).materialize() + logger.debug( + f"Distributed online store write completed for {feature_view.name}" + ) except Exception as e: - logger.error(f"Failed to write to online store for {feature_view.name}: {e}") + logger.error( + f"Distributed write to online store failed for {feature_view.name}: {e}" + ) def safe_batch_processor( - func: Callable[[pd.DataFrame], pd.DataFrame], -) -> Callable[[pd.DataFrame], pd.DataFrame]: + func: Callable[[BatchType], BatchType], +) -> Callable[[BatchType], BatchType]: """ - Decorator for batch processing functions that handles empty batches and errors gracefully. + Decorator for batch processing functions that handles empty batches and + exceptions gracefully across all Ray Data batch formats. + + Ray Data can deliver batches as a pandas DataFrame (batch_format="pandas"), + a Dict[str, np.ndarray] (batch_format="numpy"), or a pa.Table + (batch_format="pyarrow"). The decorator handles all three so that callers + using gpu_batch_format="numpy" or "pyarrow" do not crash on the empty-batch + check. Args: - func: Function that processes a pandas DataFrame batch + func: Batch processing function. Receives and returns the same batch + type that Ray Data passes (pandas, numpy dict, or pyarrow Table). Returns: - Wrapped function that handles empty batches and exceptions + Wrapped function that skips empty batches and swallows exceptions. """ - def wrapper(batch: pd.DataFrame) -> pd.DataFrame: - # Handle empty batches - if batch.empty: + def wrapper(batch: BatchType) -> BatchType: + if _is_empty_batch(batch): return batch try: diff --git a/sdk/python/feast/infra/compute_engines/snowflake/snowflake_engine.py b/sdk/python/feast/infra/compute_engines/snowflake/snowflake_engine.py index 7441cb0f18c..ddcdad9578a 100644 --- a/sdk/python/feast/infra/compute_engines/snowflake/snowflake_engine.py +++ b/sdk/python/feast/infra/compute_engines/snowflake/snowflake_engine.py @@ -1,4 +1,5 @@ import os +import re import shutil from datetime import timezone from typing import Literal, Optional, Sequence, Union @@ -7,7 +8,7 @@ import pandas as pd import pyarrow as pa from colorama import Fore, Style -from pydantic import ConfigDict, Field, StrictStr +from pydantic import ConfigDict, Field, StrictStr, field_validator from tqdm import tqdm import feast @@ -87,8 +88,41 @@ class SnowflakeComputeEngineConfig(FeastConfigBaseModel): schema_: Optional[str] = Field("PUBLIC", alias="schema") """ Snowflake schema name """ + + python_udf_runtime_version: StrictStr = "3.10" + """ + Snowflake Python UDF RUNTIME_VERSION used when Feast deploys its materialization + UDFs (see `snowflake_python_udfs_creation.sql`). + + Snowflake periodically decommissions old Python UDF runtimes -- e.g. the 3.9 + runtime was decommissioned, which required Feast to bump its default runtime + from 3.9 to 3.10 (see https://github.com/feast-dev/feast/issues/6606). Rather + than hardcoding a version that will eventually go stale again, this field is + user-configurable so you are not blocked on a new Feast release the next time + Snowflake deprecates a runtime. + + Defaults to "3.10", matching Feast's own minimum supported Python version + (`requires-python` in `pyproject.toml`). If Snowflake decommissions the 3.10 + runtime in the future, override it in your `feature_store.yaml`, e.g.: + + batch_engine: + type: snowflake.engine + ... + python_udf_runtime_version: "3.11" + """ + model_config = ConfigDict(populate_by_name=True, extra="allow") + @field_validator("python_udf_runtime_version") + @classmethod + def validate_python_udf_runtime_version(cls, v: str) -> str: + if not re.fullmatch(r"\d+\.\d+(\.\d+)?", v): + raise ValueError( + "python_udf_runtime_version must be a valid Python version string " + f"such as '3.10' or '3.11.2', got {v!r}" + ) + return v + class SnowflakeComputeEngine(ComputeEngine): def get_historical_features( @@ -110,25 +144,18 @@ def update( entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], ): - stage_context = f'"{self.repo_config.batch_engine.database}"."{self.repo_config.batch_engine.schema_}"' - stage_path = f'{stage_context}."feast_{project}"' + stage_path = f'"{self.repo_config.batch_engine.database}"."{self.repo_config.batch_engine.schema_}"."feast_{project}"' with GetSnowflakeConnection(self.repo_config.batch_engine) as conn: - query = f"SHOW USER FUNCTIONS LIKE 'FEAST_{project.upper()}%' IN SCHEMA {stage_context}" - cursor = execute_snowflake_statement(conn, query) - function_list = pd.DataFrame( - cursor.fetchall(), - columns=[column.name for column in cursor.description], - ) - - # if the SHOW FUNCTIONS query returns results, - # assumes that the materialization functions have been deployed - if len(function_list.index) > 0: - click.echo( - f"Materialization functions for {Style.BRIGHT + Fore.GREEN}{project}{Style.RESET_ALL} already detected." - ) - click.echo() - return None - + # Always (re)deploy the materialization functions, using + # `CREATE OR REPLACE FUNCTION` below, instead of skipping deployment + # when functions already exist. Previously, an early return here + # (triggered by a `SHOW USER FUNCTIONS` check) combined with + # `CREATE FUNCTION IF NOT EXISTS` in the SQL template meant that + # once UDFs were deployed for a project, they were never + # redeployed -- so a config change to `python_udf_runtime_version` + # (e.g. after Snowflake decommissions a runtime) would silently + # keep using the stale, already-deployed runtime version. See + # https://github.com/feast-dev/feast/pull/6608#discussion_r3602461959. click.echo( f"Deploying materialization functions for {Style.BRIGHT + Fore.GREEN}{project}{Style.RESET_ALL}" ) @@ -151,7 +178,11 @@ def update( sqlCommands = sqlFile.split(";") for command in sqlCommands: command = command.replace("STAGE_HOLDER", f"{stage_path}") - query = command.replace("PROJECT_NAME", f"{project}") + command = command.replace("PROJECT_NAME", f"{project}") + query = command.replace( + "RUNTIME_VERSION_HOLDER", + self.repo_config.batch_engine.python_udf_runtime_version, + ) execute_snowflake_statement(conn, query) return None @@ -226,13 +257,14 @@ def _materialize_one( timestamp_field, created_timestamp_column, ) = _get_column_names(feature_view, entities) + assert feature_view.batch_source is not None # guaranteed by _get_column_names job_id = f"{feature_view.name}-{start_date}-{end_date}" try: offline_job = self.offline_store.pull_latest_from_table_or_query( config=self.repo_config, - data_source=feature_view.batch_source, + data_source=feature_view.batch_source, # type: ignore[arg-type] join_key_columns=join_key_columns, feature_name_columns=feature_name_columns, timestamp_field=timestamp_field, @@ -341,6 +373,7 @@ def generate_snowflake_materialization_query( feature_batch: list, project: str, ) -> str: + assert feature_view.batch_source is not None if feature_view.batch_source.created_timestamp_column: fv_created_str = f',"{feature_view.batch_source.created_timestamp_column}"' else: @@ -406,6 +439,7 @@ def materialize_to_snowflake_online_store( project: str, ) -> None: assert_snowflake_feature_names(feature_view) + assert feature_view.batch_source is not None feature_names_str = '", "'.join( [feature.name for feature in feature_view.features] @@ -467,6 +501,7 @@ def materialize_to_external_online_store( feature_view: Union[StreamFeatureView, FeatureView], pbar: tqdm, ) -> None: + assert feature_view.batch_source is not None feature_names = [feature.name for feature in feature_view.features] with GetSnowflakeConnection(repo_config.batch_engine) as conn: diff --git a/sdk/python/feast/infra/compute_engines/spark/compute.py b/sdk/python/feast/infra/compute_engines/spark/compute.py index e6ec58dd74d..b6c7dc30d55 100644 --- a/sdk/python/feast/infra/compute_engines/spark/compute.py +++ b/sdk/python/feast/infra/compute_engines/spark/compute.py @@ -162,7 +162,7 @@ def _materialize_from_offline_store( SparkRetrievalJob, self.offline_store.pull_latest_from_table_or_query( config=self.repo_config, - data_source=feature_view.batch_source, + data_source=feature_view.batch_source, # type: ignore[arg-type] join_key_columns=join_key_columns, feature_name_columns=feature_name_columns, timestamp_field=timestamp_field, diff --git a/sdk/python/feast/infra/compute_engines/spark/feature_builder.py b/sdk/python/feast/infra/compute_engines/spark/feature_builder.py index e042bb000dc..94f29220513 100644 --- a/sdk/python/feast/infra/compute_engines/spark/feature_builder.py +++ b/sdk/python/feast/infra/compute_engines/spark/feature_builder.py @@ -1,3 +1,4 @@ +import logging from typing import Union from pyspark.sql import SparkSession @@ -12,9 +13,14 @@ SparkJoinNode, SparkReadNode, SparkTransformationNode, + SparkValidationNode, SparkWriteNode, + from_feast_to_spark_type, ) from feast.infra.registry.base_registry import BaseRegistry +from feast.types import PrimitiveFeastType + +logger = logging.getLogger(__name__) class SparkFeatureBuilder(FeatureBuilder): @@ -47,12 +53,20 @@ def build_aggregation_node(self, view, input_node): agg_specs = view.aggregations group_by_keys = view.entities timestamp_col = view.batch_source.timestamp_field + + # Check if tiling is enabled for this view + enable_tiling = getattr(view, "enable_tiling", False) + hop_size = getattr(view, "tiling_hop_size", None) + node = SparkAggregationNode( f"{view.name}:agg", agg_specs, group_by_keys, timestamp_col, + self.spark_session, inputs=[input_node], + enable_tiling=enable_tiling, + hop_size=hop_size, ) self.nodes.append(node) return node @@ -107,4 +121,30 @@ def build_output_nodes(self, view, input_node): return node def build_validation_node(self, view, input_node): - pass + expected_columns = {} + json_columns: set = set() + if hasattr(view, "features"): + for feature in view.features: + spark_type = from_feast_to_spark_type(feature.dtype) + if spark_type is None: + logger.debug( + "Could not resolve Spark type for feature '%s' " + "(dtype=%s), skipping type check for this column.", + feature.name, + feature.dtype, + ) + expected_columns[feature.name] = spark_type + if ( + isinstance(feature.dtype, PrimitiveFeastType) + and feature.dtype.name == "JSON" + ): + json_columns.add(feature.name) + + node = SparkValidationNode( + f"{view.name}:validate", + expected_columns=expected_columns, + json_columns=json_columns, + inputs=[input_node], + ) + self.nodes.append(node) + return node diff --git a/sdk/python/feast/infra/compute_engines/spark/nodes.py b/sdk/python/feast/infra/compute_engines/spark/nodes.py index 7c2b0bd7916..92964b72bc9 100644 --- a/sdk/python/feast/infra/compute_engines/spark/nodes.py +++ b/sdk/python/feast/infra/compute_engines/spark/nodes.py @@ -1,8 +1,28 @@ +import json +import logging from datetime import datetime, timedelta -from typing import Callable, List, Optional, Union, cast +from typing import Callable, Dict, List, Optional, Set, Union, cast +import pandas as pd from pyspark.sql import DataFrame, SparkSession, Window from pyspark.sql import functions as F +from pyspark.sql.pandas.types import from_arrow_schema +from pyspark.sql.types import ( + ArrayType, + BinaryType, + BooleanType, + DoubleType, + FloatType, + IntegerType, + LongType, + MapType, + StringType, + StructType, + TimestampType, +) +from pyspark.sql.types import ( + DataType as SparkDataType, +) from feast import BatchFeatureView, StreamFeatureView from feast.aggregation import Aggregation @@ -14,7 +34,9 @@ from feast.infra.compute_engines.dag.value import DAGValue from feast.infra.compute_engines.spark.utils import map_in_arrow from feast.infra.compute_engines.utils import ( + ENTITY_TS_ALIAS, create_offline_store_retrieval_job, + infer_entity_timestamp_column, ) from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( SparkRetrievalJob, @@ -23,11 +45,102 @@ from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import ( SparkSource, ) -from feast.infra.offline_stores.offline_utils import ( - infer_event_timestamp_from_entity_df, -) -ENTITY_TS_ALIAS = "__entity_event_timestamp" +logger = logging.getLogger(__name__) + + +def from_feast_to_spark_type(feast_type) -> Optional[SparkDataType]: + """Convert a Feast type to a PySpark DataType. + + Returns None if the Feast type cannot be mapped. + """ + from feast.types import ( + Array, + PrimitiveFeastType, + Set, + Struct, + ) + + if isinstance(feast_type, Struct): + from pyspark.sql.types import StructField + + spark_fields = [] + for name, ftype in feast_type.fields.items(): + spark_type = from_feast_to_spark_type(ftype) + if spark_type is None: + return None + spark_fields.append(StructField(name, spark_type, nullable=True)) + return StructType(spark_fields) + + if isinstance(feast_type, PrimitiveFeastType): + mapping = { + PrimitiveFeastType.BYTES: BinaryType(), + PrimitiveFeastType.STRING: StringType(), + PrimitiveFeastType.INT32: IntegerType(), + PrimitiveFeastType.INT64: LongType(), + PrimitiveFeastType.FLOAT64: DoubleType(), + PrimitiveFeastType.FLOAT32: FloatType(), + PrimitiveFeastType.BOOL: BooleanType(), + PrimitiveFeastType.UNIX_TIMESTAMP: TimestampType(), + PrimitiveFeastType.MAP: MapType(StringType(), StringType()), + PrimitiveFeastType.JSON: StringType(), + } + return mapping.get(feast_type) + + if isinstance(feast_type, Array): + base_type = feast_type.base_type + if isinstance(base_type, Struct): + inner = from_feast_to_spark_type(base_type) + return ArrayType(inner) if inner else None + if isinstance(base_type, PrimitiveFeastType): + if base_type == PrimitiveFeastType.MAP: + return ArrayType(MapType(StringType(), StringType())) + inner = from_feast_to_spark_type(base_type) + return ArrayType(inner) if inner else None + + if isinstance(feast_type, Set): + inner = from_feast_to_spark_type(feast_type.base_type) + return ArrayType(inner) if inner else None + + return None + + +def _spark_types_compatible(expected: SparkDataType, actual: SparkDataType) -> bool: + """Check if two Spark types are compatible for validation purposes. + + Exact match is always compatible. Beyond that, we allow common + representations that arise from different data source encodings. + """ + if expected == actual: + return True + + # Map ↔ Struct: data sources may encode maps as structs or vice versa + if isinstance(expected, MapType) and isinstance(actual, (MapType, StructType)): + return True + if isinstance(expected, StructType) and isinstance(actual, (StructType, MapType)): + return True + + # Json (StringType) is always compatible with StringType + if isinstance(expected, StringType) and isinstance(actual, StringType): + return True + + # Integer widening: IntegerType ↔ LongType + if isinstance(expected, (IntegerType, LongType)) and isinstance( + actual, (IntegerType, LongType) + ): + return True + + # Float widening: FloatType ↔ DoubleType + if isinstance(expected, (FloatType, DoubleType)) and isinstance( + actual, (FloatType, DoubleType) + ): + return True + + # Array compatibility: compare element types + if isinstance(expected, ArrayType) and isinstance(actual, ArrayType): + return _spark_types_compatible(expected.elementType, actual.elementType) + + return False # Rename entity_df event_timestamp_col to match feature_df @@ -42,9 +155,7 @@ def rename_entity_ts_column( spark_session=spark_session, entity_df=entity_df, ) - event_timestamp_col = infer_event_timestamp_from_entity_df( - entity_schema=entity_schema, - ) + event_timestamp_col = infer_entity_timestamp_column(entity_schema) if not isinstance(entity_df, DataFrame): entity_df = spark_session.createDataFrame(entity_df) entity_df = entity_df.withColumnRenamed(event_timestamp_col, ENTITY_TS_ALIAS) @@ -79,7 +190,14 @@ def execute(self, context: ExecutionContext) -> DAGValue: if isinstance(retrieval_job, SparkRetrievalJob): spark_df = cast(SparkRetrievalJob, retrieval_job).to_spark_df() else: - spark_df = self.spark_session.createDataFrame(retrieval_job.to_arrow()) + arrow_table = retrieval_job.to_arrow() + if arrow_table.num_rows == 0: + spark_schema = from_arrow_schema(arrow_table.schema) + spark_df = self.spark_session.createDataFrame( + self.spark_session.sparkContext.emptyRDD(), schema=spark_schema + ) + else: + spark_df = self.spark_session.createDataFrame(arrow_table.to_pandas()) return DAGValue( data=spark_df, @@ -101,26 +219,157 @@ def __init__( aggregations: List[Aggregation], group_by_keys: List[str], timestamp_col: str, + spark_session: SparkSession, inputs=None, + enable_tiling: bool = False, + hop_size: Optional[timedelta] = None, ): super().__init__(name, inputs=inputs) self.aggregations = aggregations self.group_by_keys = group_by_keys self.timestamp_col = timestamp_col + self.spark_session = spark_session + self.enable_tiling = enable_tiling + self.hop_size = hop_size def execute(self, context: ExecutionContext) -> DAGValue: input_value = self.get_single_input_value(context) input_value.assert_format(DAGFormat.SPARK) input_df: DataFrame = input_value.data - agg_exprs = [] + # Check if tiling is enabled and we have time-windowed aggregations + has_time_windows = any(agg.time_window for agg in self.aggregations) + + if self.enable_tiling and has_time_windows: + return self._execute_tiled_aggregation(input_df) + else: + return self._execute_standard_aggregation(input_df) + + def _execute_tiled_aggregation(self, input_df: DataFrame) -> DAGValue: + """ + Execute aggregation using tiling. + """ + entity_keys = self.group_by_keys + + # Group aggregations by time window to process separately + from collections import defaultdict + + aggs_by_window = defaultdict(list) for agg in self.aggregations: - func = getattr(F, agg.function) - expr = func(agg.column).alias( - f"{agg.function}_{agg.column}_{int(agg.time_window.total_seconds())}s" - if agg.time_window - else f"{agg.function}_{agg.column}" + if agg.time_window is None: + raise ValueError( + f"Tiling is enabled but aggregation on column '{agg.column}' has no time_window set. " + f"Either set time_window for all aggregations or disable tiling by setting enable_tiling=False." + ) + aggs_by_window[agg.time_window].append(agg) + + from feast.aggregation.tiling.orchestrator import apply_sawtooth_window_tiling + from feast.aggregation.tiling.tile_subtraction import ( + convert_cumulative_to_windowed, + deduplicate_keep_latest, + ) + + input_pdf = input_df.toPandas() + + # Process each time window in pandas + windowed_pdfs = [] + for time_window, window_aggs in aggs_by_window.items(): + # Step 1: Generate cumulative tiles + tiles_pdf = apply_sawtooth_window_tiling( + df=input_pdf, + aggregations=window_aggs, + group_by_keys=entity_keys, + timestamp_col=self.timestamp_col, + window_size=time_window, + hop_size=self.hop_size or timedelta(minutes=5), ) + + if tiles_pdf.empty: + continue + + # Step 2: Convert to windowed aggregations + windowed_pdf = convert_cumulative_to_windowed( + tiles_df=tiles_pdf, + entity_keys=entity_keys, + timestamp_col=self.timestamp_col, + window_size=time_window, + aggregations=window_aggs, + ) + + if not windowed_pdf.empty: + windowed_pdfs.append(windowed_pdf) + + if not windowed_pdfs: + # No results, return empty Spark DataFrame with correct schema + # Build expected columns: entity_keys + timestamp_col + feature columns + expected_columns = entity_keys + [self.timestamp_col] + for time_window, window_aggs in aggs_by_window.items(): + for agg in window_aggs: + feature_name = agg.resolved_name(time_window) + if feature_name not in expected_columns: + expected_columns.append(feature_name) + + empty_data = {} + for col in entity_keys: + empty_data[col] = pd.Series(dtype="string") + if self.timestamp_col in expected_columns: + empty_data[self.timestamp_col] = pd.Series(dtype="datetime64[ns]") + for col in expected_columns: + if col not in empty_data: + empty_data[col] = pd.Series(dtype="float64") + + empty_pdf = pd.DataFrame(empty_data) + final_df = self.spark_session.createDataFrame(empty_pdf) + else: + # Step 3: Join all windows in pandas (outer merge on entity keys + timestamp) + if len(windowed_pdfs) == 1: + final_pdf = windowed_pdfs[0] + else: + final_pdf = windowed_pdfs[0] + join_keys = entity_keys + [self.timestamp_col] + for pdf in windowed_pdfs[1:]: + final_pdf = pd.merge( + final_pdf, + pdf, + on=join_keys, + how="outer", + suffixes=("", "_dup"), + ) + # Drop duplicate columns from merge + final_pdf = final_pdf.loc[ + :, ~final_pdf.columns.str.endswith("_dup") + ] + + # Step 4: Deduplicate in pandas (keep latest timestamp per entity) + if self.timestamp_col in final_pdf.columns and not final_pdf.empty: + final_pdf = deduplicate_keep_latest( + final_pdf, entity_keys, self.timestamp_col + ) + + # Step 5: Convert to Spark once at the end + final_df = self.spark_session.createDataFrame(final_pdf) + + return DAGValue( + data=final_df, + format=DAGFormat.SPARK, + metadata={ + "aggregated": True, + "tiled": True, + "window_sizes": [ + int(tw.total_seconds()) for tw in aggs_by_window.keys() + ], + }, + ) + + def _execute_standard_aggregation(self, input_df: DataFrame) -> DAGValue: + """Execute standard Spark aggregation (existing logic).""" + agg_exprs = [] + for agg in self.aggregations: + if agg.function == "count_distinct": + func_expr = F.countDistinct(agg.column) + else: + func_expr = getattr(F, agg.function)(agg.column) + expr = func_expr.alias(agg.resolved_name(agg.time_window)) agg_exprs.append(expr) if any(agg.time_window for agg in self.aggregations): @@ -369,3 +618,107 @@ def execute(self, context: ExecutionContext) -> DAGValue: return DAGValue( data=transformed_df, format=DAGFormat.SPARK, metadata={"transformed": True} ) + + +class SparkValidationNode(DAGNode): + """ + Spark node for validating feature data against the declared schema. + + Checks that all expected columns are present in the Spark DataFrame, + validates column types using native Spark types, and checks JSON + well-formedness for Json columns. + """ + + def __init__( + self, + name: str, + expected_columns: Dict[str, Optional[SparkDataType]], + json_columns: Optional[Set[str]] = None, + inputs: Optional[List[DAGNode]] = None, + ): + super().__init__(name, inputs=inputs) + self.expected_columns = expected_columns + self.json_columns = json_columns or set() + + def execute(self, context: ExecutionContext) -> DAGValue: + input_value = self.get_single_input_value(context) + input_value.assert_format(DAGFormat.SPARK) + spark_df: DataFrame = input_value.data + + if not self.expected_columns: + context.node_outputs[self.name] = input_value + return input_value + + self._validate_schema(spark_df) + + logger.debug("[Validation: %s] Schema validation passed.", self.name) + context.node_outputs[self.name] = input_value + return input_value + + def _validate_schema(self, spark_df: DataFrame): + """Validate the Spark DataFrame against the expected schema. + + Checks for missing columns, type mismatches using native Spark types, + and JSON well-formedness for declared Json columns. + """ + actual_columns = set(spark_df.columns) + expected_names = set(self.expected_columns.keys()) + + missing = expected_names - actual_columns + if missing: + raise ValueError( + f"[Validation: {self.name}] Missing expected columns: {missing}. " + f"Actual columns: {sorted(actual_columns)}" + ) + + # Type validation using native Spark types + schema = spark_df.schema + for col_name, expected_type in self.expected_columns.items(): + if expected_type is None: + continue + try: + actual_field = schema[col_name] + except (KeyError, IndexError): + continue + actual_type = actual_field.dataType + if not _spark_types_compatible(expected_type, actual_type): + logger.warning( + "[Validation: %s] Column '%s' type mismatch: expected %s, got %s", + self.name, + col_name, + expected_type.simpleString(), + actual_type.simpleString(), + ) + + # Validate JSON well-formedness for declared Json columns + if self.json_columns: + sample_rows = spark_df.limit(1000).collect() + for col_name in self.json_columns: + if col_name not in actual_columns: + continue + + invalid_count = 0 + first_error = None + first_error_row = None + + for i, row in enumerate(sample_rows): + value = row[col_name] + if value is None: + continue + if not isinstance(value, str): + continue + try: + json.loads(value) + except (json.JSONDecodeError, TypeError) as e: + invalid_count += 1 + if first_error is None: + first_error = str(e) + first_error_row = i + + if invalid_count > 0: + raise ValueError( + f"[Validation: {self.name}] Column '{col_name}' declared as " + f"Json contains {invalid_count} invalid JSON value(s) in " + f"sampled rows. First error at row {first_error_row}: " + f"{first_error}" + ) diff --git a/sdk/python/feast/infra/compute_engines/spark/utils.py b/sdk/python/feast/infra/compute_engines/spark/utils.py index 4e429f8e075..8c84c9f17a6 100644 --- a/sdk/python/feast/infra/compute_engines/spark/utils.py +++ b/sdk/python/feast/infra/compute_engines/spark/utils.py @@ -1,3 +1,5 @@ +import logging +import os from typing import Dict, Iterable, Literal, Optional import pandas as pd @@ -9,6 +11,102 @@ from feast.infra.common.serde import SerializedArtifacts from feast.utils import _convert_arrow_to_proto, _run_pyarrow_field_mapping +try: + import boto3 + from botocore.client import Config as BotoConfig +except ImportError: + boto3 = None # type: ignore[assignment] + BotoConfig = None # type: ignore[assignment,misc] + +logger = logging.getLogger(__name__) + + +def _ensure_s3a_event_log_dir(spark_config: Dict[str, str]) -> None: + """Pre-create the S3A event log prefix before SparkContext initialisation. + + Spark's EventLogFileWriter.requireLogBaseDirAsDirectory() is called inside + SparkContext.__init__ and crashes if the S3A path doesn't exist yet (S3 has no + real directories, so an empty prefix returns a 404). This function writes a + zero-byte placeholder so the prefix exists before SparkContext is built. + + This is only attempted when: + - spark.eventLog.enabled == "true" + - spark.eventLog.dir starts with "s3a://" + Failures are non-fatal: Spark will surface its own error if the dir is still missing. + """ + if spark_config.get("spark.eventLog.enabled", "false").lower() != "true": + return + event_dir = spark_config.get("spark.eventLog.dir", "") + if not event_dir.startswith("s3a://"): + return + + path = event_dir[len("s3a://") :] + bucket, _, prefix = path.partition("/") + prefix = prefix.rstrip("/") + prefix = (prefix + "/") if prefix else prefix + placeholder_key = prefix + ".keep" + + endpoint = spark_config.get( + "spark.hadoop.fs.s3a.endpoint", + os.environ.get("AWS_ENDPOINT_URL", ""), + ) + access_key = spark_config.get( + "spark.hadoop.fs.s3a.access.key", + os.environ.get("AWS_ACCESS_KEY_ID", ""), + ) + secret_key = spark_config.get( + "spark.hadoop.fs.s3a.secret.key", + os.environ.get("AWS_SECRET_ACCESS_KEY", ""), + ) + session_token = ( + spark_config.get( + "spark.hadoop.fs.s3a.session.token", + os.environ.get("AWS_SESSION_TOKEN", ""), + ) + or None + ) + + try: + if boto3 is None: + raise ImportError("boto3 is not installed") + + addressing_style = ( + "path" + if spark_config.get( + "spark.hadoop.fs.s3a.path.style.access", "false" + ).lower() + == "true" + else "auto" + ) + + s3 = boto3.client( + "s3", + endpoint_url=endpoint if endpoint else None, + aws_access_key_id=access_key or None, + aws_secret_access_key=secret_key or None, + aws_session_token=session_token, + config=BotoConfig( + signature_version="s3v4", + s3={"addressing_style": addressing_style}, + ), + ) + resp = s3.list_objects_v2(Bucket=bucket, Prefix=prefix, MaxKeys=1) + if resp.get("KeyCount", 0) == 0: + s3.put_object(Bucket=bucket, Key=placeholder_key, Body=b"") + logger.debug( + "Created S3A event log dir placeholder: s3a://%s/%s", + bucket, + placeholder_key, + ) + except Exception as exc: + logger.warning( + "Could not pre-create S3A event log dir s3a://%s/%s — " + "SparkContext may fail if the path still doesn't exist: %s", + bucket, + prefix, + exc, + ) + def get_or_create_new_spark_session( spark_config: Optional[Dict[str, str]] = None, @@ -17,6 +115,7 @@ def get_or_create_new_spark_session( if not spark_session: spark_builder = SparkSession.builder if spark_config: + _ensure_s3a_event_log_dir(spark_config) spark_builder = spark_builder.config( conf=SparkConf().setAll([(k, v) for k, v in spark_config.items()]) ) @@ -47,16 +146,24 @@ def map_in_arrow( for entity in feature_view.entity_columns } - rows_to_write = _convert_arrow_to_proto( - table, feature_view, join_key_to_value_type - ) - - online_store.online_write_batch( - config=repo_config, - table=feature_view, - data=rows_to_write, - progress=lambda x: None, + batch_size = repo_config.materialization_config.online_write_batch_size + # Single batch if None (backward compatible), otherwise use configured batch_size + sub_batches = ( + [table] + if batch_size is None + else table.to_batches(max_chunksize=batch_size) ) + for sub_batch in sub_batches: + rows_to_write = _convert_arrow_to_proto( + sub_batch, feature_view, join_key_to_value_type + ) + + online_store.online_write_batch( + config=repo_config, + table=feature_view, + data=rows_to_write, + progress=lambda x: None, + ) if mode == "offline": offline_store.offline_write_batch( config=repo_config, @@ -95,15 +202,23 @@ def map_in_pandas(iterator, serialized_artifacts: SerializedArtifacts): for entity in feature_view.entity_columns } - rows_to_write = _convert_arrow_to_proto( - table, feature_view, join_key_to_value_type - ) - online_store.online_write_batch( - repo_config, - feature_view, - rows_to_write, - lambda x: None, + batch_size = repo_config.materialization_config.online_write_batch_size + # Single batch if None (backward compatible), otherwise use configured batch_size + sub_batches = ( + [table] + if batch_size is None + else table.to_batches(max_chunksize=batch_size) ) + for sub_batch in sub_batches: + rows_to_write = _convert_arrow_to_proto( + sub_batch, feature_view, join_key_to_value_type + ) + online_store.online_write_batch( + repo_config, + feature_view, + rows_to_write, + lambda x: None, + ) yield pd.DataFrame( [pd.Series(range(1, 2))] diff --git a/sdk/python/feast/infra/compute_engines/spark_application/.dockerignore b/sdk/python/feast/infra/compute_engines/spark_application/.dockerignore new file mode 100644 index 00000000000..0a4119f63bc --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/spark_application/.dockerignore @@ -0,0 +1,8 @@ +feast/.git +feast/__pycache__ +feast/**/__pycache__ +feast/.mypy_cache +feast/tests +feast/.pixi +**/*.pyc +**/.pytest_cache diff --git a/sdk/python/feast/infra/compute_engines/spark_application/Dockerfile b/sdk/python/feast/infra/compute_engines/spark_application/Dockerfile new file mode 100644 index 00000000000..066aac3ad86 --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/spark_application/Dockerfile @@ -0,0 +1,34 @@ +FROM apache/spark:4.0.1 + +USER root + +RUN apt-get update && apt-get install --no-install-suggests --no-install-recommends --yes git && rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +ENV PYTHONPATH="/opt/spark/python:/opt/spark/python/lib/py4j-0.10.9.9-src.zip" +ENV SPARK_HOME="/opt/spark" + +WORKDIR /app + +COPY sdk/python/feast/infra/compute_engines/spark_application/main.py /opt/feast/main.py + +# Copy necessary parts of the Feast codebase +COPY sdk/python sdk/python +COPY protos protos +COPY pyproject.toml pyproject.toml +COPY README.md README.md + +# setuptools_scm needs .git to infer the version. +# https://github.com/pypa/setuptools_scm#usage-from-docker +RUN --mount=source=.git,target=.git,type=bind uv pip install --system --no-cache-dir '.[redis,grpcio,k8s]' pymysql + +# Hadoop AWS JARs for S3A filesystem support + AWS SDK v2 bundle +RUN curl -sL https://repo1.maven.org/maven2/org/apache/hadoop/hadoop-aws/3.4.1/hadoop-aws-3.4.1.jar \ + -o /opt/spark/jars/hadoop-aws-3.4.1.jar && \ + curl -sL https://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-bundle/1.12.782/aws-java-sdk-bundle-1.12.782.jar \ + -o /opt/spark/jars/aws-java-sdk-bundle-1.12.782.jar && \ + curl -sL https://repo1.maven.org/maven2/software/amazon/awssdk/bundle/2.28.4/bundle-2.28.4.jar \ + -o /opt/spark/jars/bundle-2.28.4.jar + +USER spark diff --git a/sdk/python/feast/infra/online_stores/ikv_online_store/__init__.py b/sdk/python/feast/infra/compute_engines/spark_application/__init__.py similarity index 100% rename from sdk/python/feast/infra/online_stores/ikv_online_store/__init__.py rename to sdk/python/feast/infra/compute_engines/spark_application/__init__.py diff --git a/sdk/python/feast/infra/compute_engines/spark_application/compute.py b/sdk/python/feast/infra/compute_engines/spark_application/compute.py new file mode 100644 index 00000000000..d5fbf7493ce --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/spark_application/compute.py @@ -0,0 +1,516 @@ +import logging +import time +import uuid +from typing import List, Optional, Sequence, Union + +import pyarrow as pa +import yaml +from kubernetes import client +from kubernetes import config as k8s_config +from kubernetes.client.exceptions import ApiException + +from feast import RepoConfig +from feast.batch_feature_view import BatchFeatureView +from feast.entity import Entity +from feast.feature_view import FeatureView, FeatureViewState +from feast.infra.common.materialization_job import ( + MaterializationJob, + MaterializationJobStatus, + MaterializationTask, +) +from feast.infra.common.retrieval_task import HistoricalRetrievalTask +from feast.infra.compute_engines.base import ComputeEngine +from feast.infra.offline_stores.offline_store import OfflineStore +from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.registry.base_registry import BaseRegistry +from feast.on_demand_feature_view import OnDemandFeatureView +from feast.stream_feature_view import StreamFeatureView + +from .config import ( + SparkApplicationComputeEngineConfig, # noqa: F401 — required for Feast config resolution +) +from .job import ( + _MAX_RETRIES, + _RETRY_BACKOFF_BASE, + CompletedMaterializationJob, + SparkApplicationMaterializationJob, + _is_retryable, + _rbac_hint, +) + +logger = logging.getLogger(__name__) + + +class SparkApplicationComputeEngine(ComputeEngine): + def __init__( + self, + *, + repo_config: RepoConfig, + offline_store: OfflineStore, + online_store: OnlineStore, + **kwargs, + ): + super().__init__( + repo_config=repo_config, + offline_store=offline_store, + online_store=online_store, + **kwargs, + ) + self.config = repo_config.batch_engine + + _FILE_BASED_ONLINE = {"sqlite", "faiss"} + _FILE_BASED_OFFLINE = {"dask", "file", "duckdb"} + _FILE_BASED_REGISTRY = {"file"} + + online_type = getattr(repo_config.online_store, "type", "") + if online_type in _FILE_BASED_ONLINE: + raise ValueError( + f"spark_application engine cannot use '{online_type}' online store. " + f"File-based stores ({', '.join(sorted(_FILE_BASED_ONLINE))}) write " + "data inside the SparkApplication pod, which is lost when the pod " + "terminates. Use a network-accessible store: redis, postgres, etc." + ) + + offline_type = getattr(repo_config.offline_store, "type", "") + if offline_type in _FILE_BASED_OFFLINE: + raise ValueError( + f"spark_application engine cannot use '{offline_type}' offline store. " + f"File-based stores ({', '.join(sorted(_FILE_BASED_OFFLINE))}) read " + "from the local filesystem, which is inaccessible from the " + "SparkApplication pod. Use a network-accessible store: spark, " + "bigquery, snowflake, redshift, etc." + ) + + registry_type = getattr(repo_config.registry, "registry_type", "") + if registry_type in _FILE_BASED_REGISTRY: + raise ValueError( + f"spark_application engine cannot use '{registry_type}' registry. " + f"File-based registries ({', '.join(sorted(_FILE_BASED_REGISTRY))}) " + "store data on the local filesystem, which is inaccessible from the " + "SparkApplication pod. Use a network-accessible registry: sql, " + "snowflake.registry, etc." + ) + + # Defer kubeconfig load until materialize/cleanup — feast apply only + # constructs the engine and calls update() (a no-op), so it must not + # require a cluster. + self._k8s_client = None + self._core_v1 = None + self._custom_api = None + self._server_id = uuid.uuid4().hex[:8] + + def _ensure_k8s(self) -> None: + """Load kubeconfig and create API clients on first K8s use.""" + if self._custom_api is not None: + return + k8s_config.load_config() + self._k8s_client = client.ApiClient() + self._core_v1 = client.CoreV1Api(self._k8s_client) + self._custom_api = client.CustomObjectsApi(self._k8s_client) + + @property + def core_v1(self): + self._ensure_k8s() + return self._core_v1 + + @property + def custom_api(self): + self._ensure_k8s() + return self._custom_api + + @property + def supports_batch(self) -> bool: + return True + + @property + def applies_materialization(self) -> bool: + return True + + def update( + self, + project: str, + views_to_delete: Sequence[ + Union[BatchFeatureView, StreamFeatureView, FeatureView] + ], + views_to_keep: Sequence[ + Union[BatchFeatureView, StreamFeatureView, FeatureView, OnDemandFeatureView] + ], + entities_to_delete: Sequence[Entity], + entities_to_keep: Sequence[Entity], + ): + pass + + def teardown_infra( + self, + project: str, + fvs: Sequence[Union[BatchFeatureView, StreamFeatureView, FeatureView]], + entities: Sequence[Entity], + ): + pass + + def get_historical_features( + self, registry: BaseRegistry, task: HistoricalRetrievalTask + ) -> pa.Table: + raise NotImplementedError( + "SparkApplicationComputeEngine does not yet support get_historical_features(). " + "This is planned for Phase 2." + ) + + def materialize( + self, + registry: BaseRegistry, + tasks: Union[MaterializationTask, List[MaterializationTask]], + **kwargs, + ) -> List[MaterializationJob]: + """Batch all materialization tasks into a single SparkApplication. + + The pod calls apply_materialization (via gRPC → Feast server → registry) + for each FV it successfully materializes. After the pod finishes, we read + each FV's state from the registry: AVAILABLE_ONLINE = succeeded, + still MATERIALIZING = failed. + """ + if isinstance(tasks, MaterializationTask): + tasks = [tasks] + + job_id = uuid.uuid4().hex[:8] + + try: + self._create_with_retry( + lambda: self._create_configmap(job_id, tasks), + "ConfigMap", + job_id, + ) + except ApiException as e: + job = SparkApplicationMaterializationJob( + job_id, + self.config.namespace, + self.custom_api, + error=Exception( + f"ConfigMap creation failed: HTTP {e.status} {e.reason}." + f"{_rbac_hint(e.status)}" + ), + ) + return [job for _ in tasks] + + try: + cr = self._build_spark_application_cr(job_id) + self._create_with_retry( + lambda: self.custom_api.create_namespaced_custom_object( + group="sparkoperator.k8s.io", + version="v1beta2", + namespace=self.config.namespace, + plural="sparkapplications", + body=cr, + ), + "SparkApplication", + job_id, + ) + except ApiException as e: + self._cleanup(job_id) + job = SparkApplicationMaterializationJob( + job_id, + self.config.namespace, + self.custom_api, + error=Exception( + f"SparkApplication creation failed: HTTP {e.status} {e.reason}." + f"{_rbac_hint(e.status)}" + ), + ) + return [job for _ in tasks] + + job = SparkApplicationMaterializationJob( + job_id, self.config.namespace, self.custom_api + ) + try: + self._wait_for_completion(job) + return self._build_per_fv_jobs(registry, tasks, job_id, job) + finally: + self._cleanup(job_id) + + def _build_driver_repo_config(self) -> dict: + """Build feature_store.yaml for the SparkApplication driver pod. + + One rewrite: batch_engine → spark.engine. Pod uses SparkComputeEngine + with the active SparkSession (from spark-submit). Enables distributed + reads via SparkReadNode and distributed writes via mapInArrow across + executors. This is NOT recursive — SparkComputeEngine uses the local + session, it does not create CRDs. + + offline_store and registry are NOT rewritten — the pod inherits the + server's config. File-based registries are rejected at __init__(), so + the registry is always network-accessible (SQL, Snowflake, etc.) and + the pod can write apply_materialization() directly. + """ + config_dict = self.repo_config.model_dump(by_alias=True, mode="json") + + config_dict["batch_engine"] = {"type": "spark.engine"} + + return config_dict + + def _build_per_fv_jobs( + self, + registry: BaseRegistry, + tasks: List[MaterializationTask], + job_id: str, + job: SparkApplicationMaterializationJob, + ) -> List[MaterializationJob]: + """Build one independent job object per FV from registry state. + + The driver pod calls ``apply_materialization`` for each FV it + successfully materializes, setting state to ``AVAILABLE_ONLINE``. + FVs still in ``MATERIALIZING`` were not processed. + + Each returned job is an independent object so that a failed + SparkApplication does not pollute the status of succeeded FVs. + """ + if len(tasks) <= 1: + return [job for _ in tasks] + + jobs: List[MaterializationJob] = [] + for task in tasks: + fv = registry.get_feature_view(task.feature_view.name, task.project) + if getattr(fv, "state", None) == FeatureViewState.AVAILABLE_ONLINE: + jobs.append(CompletedMaterializationJob(job_id)) + else: + jobs.append( + SparkApplicationMaterializationJob( + job_id, + self.config.namespace, + self.custom_api, + error=Exception( + f"Feature view '{task.feature_view.name}' was not " + f"materialized by SparkApplication feast-sa-{job_id}" + ), + ) + ) + return jobs + + def _create_configmap(self, job_id: str, tasks: List[MaterializationTask]): + feast_config_yaml = yaml.dump( + self._build_driver_repo_config(), default_flow_style=False + ) + mat_config = { + "operation": "materialize", + "tasks": [ + { + "feature_view": task.feature_view.name, + "start_time": task.start_time.isoformat(), + "end_time": task.end_time.isoformat(), + } + for task in tasks + ], + } + if self.config.concurrency > 1: + mat_config["concurrency"] = self.config.concurrency + mat_config_yaml = yaml.dump(mat_config) + manifest = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": f"feast-sa-{job_id}", + "namespace": self.config.namespace, + "labels": {"feast-materializer": "configmap", **self.config.labels}, + }, + "data": { + "feature_store.yaml": feast_config_yaml, + "materialization_config.yaml": mat_config_yaml, + }, + } + self.core_v1.create_namespaced_config_map( + namespace=self.config.namespace, body=manifest + ) + + def _build_spark_application_cr(self, job_id: str) -> dict: + driver_env_conf = { + "spark.kubernetes.driverEnv.FEAST_CONFIGMAP_NAME": f"feast-sa-{job_id}", + "spark.kubernetes.driverEnv.FEAST_CONFIGMAP_NAMESPACE": self.config.namespace, + } + for entry in self.config.env: + name = entry.get("name", "") + if name and "value" in entry and entry["value"] is not None: + driver_env_conf[f"spark.kubernetes.driverEnv.{name}"] = str( + entry["value"] + ) + + spec = { + "type": "Python", + "mode": "cluster", + "pythonVersion": "3", + "image": self.config.image, + "imagePullPolicy": "IfNotPresent", + "mainApplicationFile": "local:///opt/feast/main.py", + "sparkVersion": self.config.spark_version, + "sparkConf": { + "spark.scheduler.mode": "FAIR", + **(self.config.spark_conf or {}), + **driver_env_conf, + }, + "restartPolicy": { + "type": self.config.restart_policy, + "onFailureRetries": self.config.max_retries, + "onFailureRetryInterval": 30, + }, + "timeToLiveSeconds": self.config.ttl_seconds_after_finished, + "volumes": [ + { + "name": "feast-config", + "configMap": {"name": f"feast-sa-{job_id}"}, + }, + *self.config.volumes, + ], + "driver": { + "cores": self.config.driver_cores, + "memory": self.config.driver_memory, + "serviceAccount": self.config.service_account, + "volumeMounts": [ + {"name": "feast-config", "mountPath": "/var/feast/"}, + *self.config.volume_mounts, + ], + }, + "executor": { + "instances": max(self.config.executor_instances, 1), + "cores": self.config.executor_cores, + "memory": self.config.executor_memory, + }, + } + + if self.config.image_pull_secrets: + spec["imagePullSecrets"] = self.config.image_pull_secrets + if self.config.hadoop_conf: + spec["hadoopConf"] = self.config.hadoop_conf + if self.config.py_files: + spec["deps"] = {"pyFiles": self.config.py_files} + if self.config.env: + spec["driver"]["env"] = self.config.env + spec["executor"]["env"] = self.config.env + if self.config.env_from: + spec["driver"]["envFrom"] = self.config.env_from + spec["executor"]["envFrom"] = self.config.env_from + if self.config.node_selector: + spec["driver"]["nodeSelector"] = self.config.node_selector + spec["executor"]["nodeSelector"] = self.config.node_selector + if self.config.tolerations: + spec["driver"]["tolerations"] = self.config.tolerations + spec["executor"]["tolerations"] = self.config.tolerations + if self.config.volume_mounts: + spec["executor"]["volumeMounts"] = self.config.volume_mounts + + return { + "apiVersion": "sparkoperator.k8s.io/v1beta2", + "kind": "SparkApplication", + "metadata": { + "name": f"feast-sa-{job_id}", + "namespace": self.config.namespace, + "labels": { + "feast-materializer": "sparkapplication", + "feast-job-id": job_id, + "feast-server-id": self._server_id, + **self._kueue_labels(), + **self.config.labels, + }, + }, + "spec": spec, + } + + def _kueue_labels(self) -> dict: + if self.config.queue_name: + return {"kueue.x-k8s.io/queue-name": self.config.queue_name} + return {} + + def _wait_for_completion(self, job: SparkApplicationMaterializationJob): + start = time.monotonic() + deadline = start + self.config.job_timeout_seconds + while time.monotonic() < deadline: + status = job.status() + elapsed = time.monotonic() - start + logger.info( + f"SparkApplication {job.job_id()} status={status.name} elapsed={elapsed:.0f}s" + ) + if status == MaterializationJobStatus.ERROR: + logs = self._get_driver_logs(job._job_id) + if logs: + logger.error(f"Driver logs (last 50 lines):\n{logs}") + return + if status == MaterializationJobStatus.SUCCEEDED: + return + time.sleep(self.config.poll_interval_seconds) + job._error = Exception( + f"SparkApplication {job.job_id()} did not complete " + f"within {self.config.job_timeout_seconds}s" + ) + + def _get_driver_logs(self, job_id: str, tail_lines: int = 50) -> Optional[str]: + """Fetch last N lines of driver pod logs for error diagnostics.""" + try: + pods = self.core_v1.list_namespaced_pod( + namespace=self.config.namespace, + label_selector=f"spark-role=driver,sparkoperator.k8s.io/app-name=feast-sa-{job_id}", + ) + if pods.items: + return self.core_v1.read_namespaced_pod_log( + name=pods.items[0].metadata.name, + namespace=self.config.namespace, + tail_lines=tail_lines, + ) + except ApiException: + logger.warning(f"Could not retrieve driver logs for feast-sa-{job_id}") + return None + + @staticmethod + def _create_with_retry(fn, resource_kind: str, job_id: str): + """Call *fn* with exponential backoff on transient K8s API errors.""" + last_exc = None + for attempt in range(_MAX_RETRIES): + try: + return fn() + except ApiException as e: + if not _is_retryable(e): + raise + last_exc = e + wait = _RETRY_BACKOFF_BASE**attempt + logger.warning( + f"{resource_kind} feast-sa-{job_id}: create attempt " + f"{attempt + 1}/{_MAX_RETRIES} failed (HTTP {e.status}), " + f"retrying in {wait}s" + ) + time.sleep(wait) + raise last_exc # type: ignore[misc] + + def _cleanup(self, job_id: str): + """Best-effort delete of SparkApplication + ConfigMap. + + The SparkApplication CR is also garbage-collected by the Spark Operator + after ``spec.timeToLiveSeconds`` (default 1h), so a failed delete here + is not a resource leak — just delayed cleanup. The ConfigMap is ours + and not covered by operator TTL. + """ + resources = [ + ( + "SparkApplication", + lambda: self.custom_api.delete_namespaced_custom_object( + "sparkoperator.k8s.io", + "v1beta2", + self.config.namespace, + "sparkapplications", + f"feast-sa-{job_id}", + ), + ), + ( + "ConfigMap", + lambda: self.core_v1.delete_namespaced_config_map( + f"feast-sa-{job_id}", + self.config.namespace, + ), + ), + ] + for kind, fn in resources: + try: + fn() + except ApiException as e: + if e.status != 404: + logger.warning( + f"Cleanup of {kind} feast-sa-{job_id} failed " + f"(HTTP {e.status}): {e.reason}. " + f"Manual cleanup: kubectl delete {kind.lower()} " + f"feast-sa-{job_id} -n {self.config.namespace}" + ) diff --git a/sdk/python/feast/infra/compute_engines/spark_application/config.py b/sdk/python/feast/infra/compute_engines/spark_application/config.py new file mode 100644 index 00000000000..b09aedeadc3 --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/spark_application/config.py @@ -0,0 +1,81 @@ +import warnings +from typing import Dict, List, Literal, Optional + +from pydantic import StrictStr, model_validator + +from feast.repo_config import FeastConfigBaseModel + + +class SparkApplicationComputeEngineConfig(FeastConfigBaseModel): + """Batch Compute Engine config for SparkApplication CRDs via Kubeflow Spark Operator.""" + + type: Literal["spark_application"] = "spark_application" + + image: StrictStr + image_pull_secrets: List[str] = [] + + namespace: StrictStr = "default" + service_account: StrictStr = "" + + driver_cores: int = 1 + driver_memory: StrictStr = "1g" + executor_instances: int = 1 + executor_cores: int = 1 + executor_memory: StrictStr = "1g" + + spark_conf: Optional[Dict[str, str]] = None + hadoop_conf: Optional[Dict[str, str]] = None + spark_version: StrictStr = "4.0.1" + staging_location: Optional[str] = None + + env: List[dict] = [] + env_from: List[dict] = [] + + queue_name: Optional[str] = None + + job_timeout_seconds: int = 3600 + poll_interval_seconds: int = 10 + ttl_seconds_after_finished: int = 3600 + restart_policy: StrictStr = "Never" + max_retries: int = 3 + + concurrency: int = 1 + + labels: Dict[str, str] = {} + + volumes: List[dict] = [] + volume_mounts: List[dict] = [] + py_files: List[str] = [] + node_selector: Optional[Dict[str, str]] = None + tolerations: List[dict] = [] + + @staticmethod + def _validate_env_entry(index: int, entry: object) -> None: + """Validate a single K8s EnvVar dict (must have name + value or valueFrom).""" + if not isinstance(entry, dict): + raise ValueError( + f"env[{index}] must be a dict, got {type(entry).__name__}: {entry}" + ) + if "name" not in entry: + raise ValueError( + f"env[{index}] is missing required 'name'. " + f"Each env entry must be a K8s EnvVar dict. Got: {entry}" + ) + if "value" not in entry and "valueFrom" not in entry: + raise ValueError( + f"env[{index}] must set 'value' or 'valueFrom' " + f"(K8s EnvVar spec). Got: {entry}" + ) + + @model_validator(mode="after") + def _validate_config(self) -> "SparkApplicationComputeEngineConfig": + if self.staging_location: + warnings.warn( + "staging_location is configured but only used for " + "get_historical_features (not yet supported by this engine). " + "It will be ignored for materialize operations.", + stacklevel=2, + ) + for i, entry in enumerate(self.env): + self._validate_env_entry(i, entry) + return self diff --git a/sdk/python/feast/infra/compute_engines/spark_application/job.py b/sdk/python/feast/infra/compute_engines/spark_application/job.py new file mode 100644 index 00000000000..ef05585b061 --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/spark_application/job.py @@ -0,0 +1,164 @@ +import logging +import time +from typing import Optional + +from kubernetes import client +from kubernetes.client.exceptions import ApiException + +from feast.infra.common.materialization_job import ( + MaterializationJob, + MaterializationJobStatus, +) + +logger = logging.getLogger(__name__) + +_MAX_RETRIES = 3 +_RETRY_BACKOFF_BASE = 2 +_RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504} + + +def _is_retryable(exc: ApiException) -> bool: + return exc.status in _RETRYABLE_STATUS_CODES + + +def _rbac_hint(status: int) -> str: + if status in (401, 403): + return ( + " Check that the Feast server ServiceAccount has the required" + " Role/RoleBinding for sparkapplications and configmaps in this namespace." + ) + return "" + + +_STATE_MAP = { + "": MaterializationJobStatus.WAITING, + "SUBMITTED": MaterializationJobStatus.WAITING, + "RUNNING": MaterializationJobStatus.RUNNING, + "COMPLETED": MaterializationJobStatus.SUCCEEDED, + "FAILED": MaterializationJobStatus.ERROR, + "SUBMISSION_FAILED": MaterializationJobStatus.ERROR, + "PENDING_RERUN": MaterializationJobStatus.WAITING, + "INVALIDATING": MaterializationJobStatus.WAITING, + "SUCCEEDING": MaterializationJobStatus.RUNNING, + "FAILING": MaterializationJobStatus.RUNNING, + "SUSPENDING": MaterializationJobStatus.WAITING, + "SUSPENDED": MaterializationJobStatus.WAITING, + "RESUMING": MaterializationJobStatus.WAITING, + "UNKNOWN": MaterializationJobStatus.WAITING, +} +assert len(_STATE_MAP) == 14 + + +class CompletedMaterializationJob(MaterializationJob): + """Lightweight stub for a FV whose materialization already succeeded. + + Used by ``_build_per_fv_jobs`` so that each FV gets an independent job + object. Unlike ``SparkApplicationMaterializationJob``, this never polls + the K8s API — the outcome is already known from the registry state. + """ + + def __init__(self, job_id: str): + super().__init__() + self._job_id = job_id + + def status(self) -> MaterializationJobStatus: + return MaterializationJobStatus.SUCCEEDED + + def error(self) -> Optional[BaseException]: + return None + + def should_be_retried(self) -> bool: + return False + + def job_id(self) -> str: + return f"feast-sa-{self._job_id}" + + def url(self) -> Optional[str]: + return None + + +class SparkApplicationMaterializationJob(MaterializationJob): + def __init__( + self, + job_id: str, + namespace: str, + custom_api: client.CustomObjectsApi, + error: Optional[BaseException] = None, + ): + super().__init__() + self._job_id = job_id + self.namespace = namespace + self.custom_api = custom_api + self._error: Optional[BaseException] = error + + def status(self) -> MaterializationJobStatus: + if self._error is not None: + return MaterializationJobStatus.ERROR + + obj = self._get_cr_with_retry() + if obj is None: + return ( + MaterializationJobStatus.ERROR + if self._error + else MaterializationJobStatus.RUNNING + ) + + state = obj.get("status", {}).get("applicationState", {}).get("state", "") + result = _STATE_MAP.get(state, MaterializationJobStatus.WAITING) + if result == MaterializationJobStatus.ERROR: + msg = ( + obj.get("status", {}) + .get("applicationState", {}) + .get("errorMessage", f"SparkApplication failed: {state}") + ) + self._error = Exception(msg) + return result + + def _get_cr_with_retry(self) -> Optional[dict]: + """Fetch SparkApplication CR with exponential backoff on transient errors.""" + last_exc = None + for attempt in range(_MAX_RETRIES): + try: + return self.custom_api.get_namespaced_custom_object( + group="sparkoperator.k8s.io", + version="v1beta2", + namespace=self.namespace, + plural="sparkapplications", + name=f"feast-sa-{self._job_id}", + ) + except ApiException as e: + if e.status == 404: + self._error = Exception( + f"SparkApplication feast-sa-{self._job_id} not found" + ) + return None + if not _is_retryable(e): + self._error = Exception( + f"Kubernetes API error polling feast-sa-{self._job_id}: " + f"HTTP {e.status} {e.reason}.{_rbac_hint(e.status)}" + ) + return None + last_exc = e + wait = _RETRY_BACKOFF_BASE**attempt + logger.warning( + f"API poll attempt {attempt + 1}/{_MAX_RETRIES} failed " + f"(HTTP {e.status}), retrying in {wait}s" + ) + time.sleep(wait) + self._error = Exception( + f"Failed to poll SparkApplication after {_MAX_RETRIES} attempts: " + f"{last_exc.reason if last_exc else 'unknown'}" + ) + return None + + def error(self) -> Optional[BaseException]: + return self._error + + def should_be_retried(self) -> bool: + return False + + def job_id(self) -> str: + return f"feast-sa-{self._job_id}" + + def url(self) -> Optional[str]: + return None diff --git a/sdk/python/feast/infra/compute_engines/spark_application/main.py b/sdk/python/feast/infra/compute_engines/spark_application/main.py new file mode 100644 index 00000000000..cabe9d8b2cd --- /dev/null +++ b/sdk/python/feast/infra/compute_engines/spark_application/main.py @@ -0,0 +1,195 @@ +import logging +import os +import sys +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +import yaml + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("feast.spark_application.driver") + + +def _load_config_from_configmap(): + """Load feast and materialization config from a Kubernetes ConfigMap.""" + from kubernetes import client + from kubernetes import config as k8s_config + + k8s_config.load_incluster_config() + v1 = client.CoreV1Api() + cm = v1.read_namespaced_config_map( + name=os.environ["FEAST_CONFIGMAP_NAME"], + namespace=os.environ["FEAST_CONFIGMAP_NAMESPACE"], + ) + feast_config = yaml.safe_load(cm.data["feature_store.yaml"]) + mat_config = yaml.safe_load(cm.data["materialization_config.yaml"]) + return feast_config, mat_config + + +def _load_config_from_files(): + """Load feast and materialization config from mounted files.""" + with open("/var/feast/feature_store.yaml") as f: + feast_config = yaml.safe_load(f) + with open("/var/feast/materialization_config.yaml") as f: + mat_config = yaml.safe_load(f) + return feast_config, mat_config + + +def _bind_spark_session_to_thread(spark): + """Register *spark* as the active session for this OS thread. + + spark-submit creates the SparkSession on the main thread. PySpark's + ``getActiveSession()`` is thread-local, so worker threads see ``None``. + Constructing a ``SparkSession`` from the same SparkContext + JVM session + calls Java ``setActiveSession`` for *this* thread — no classmethod + monkey-patching required. + """ + from pyspark.sql import SparkSession + + SparkSession(spark.sparkContext, spark._jsparkSession) + + +def _materialize_one_fv(spark_session, config, task_info): + """Materialize a single feature view in a worker thread. + + Each thread gets its own FeatureStore instance to avoid race conditions + in Feast's usage.py call_stack (not thread-safe). + """ + from datetime import datetime, timezone + + from tqdm import tqdm + + from feast import FeatureStore, RepoConfig + + _bind_spark_session_to_thread(spark_session) + + fv_name = task_info["feature_view"] + logger.info(f"Thread started: {fv_name}") + + # Per-thread FeatureStore avoids race in feast/usage.py call_stack + thread_config = RepoConfig(**config) + thread_store = FeatureStore(config=thread_config) + fv = thread_store.get_feature_view(fv_name) + provider = thread_store.provider + + start = datetime.fromisoformat(task_info["start_time"]) + end = datetime.fromisoformat(task_info["end_time"]) + if start.tzinfo is None: + start = start.replace(tzinfo=timezone.utc) + if end.tzinfo is None: + end = end.replace(tzinfo=timezone.utc) + + t0 = time.time() + provider.materialize_single_feature_view( + config=thread_config, + feature_view=fv, + start_date=start, + end_date=end, + registry=thread_store.registry, + project=thread_store.project, + tqdm_builder=lambda length: tqdm(total=length, ncols=100), + ) + + thread_store.registry.apply_materialization(fv, thread_store.project, start, end) + logger.info(f"Applied materialization metadata for {fv_name}") + + elapsed = time.time() - t0 + return fv_name, elapsed + + +def main(): + if os.environ.get("FEAST_CONFIGMAP_NAME"): + logger.info("Loading config from ConfigMap via K8s API") + feast_config, mat_config = _load_config_from_configmap() + elif os.path.exists("/var/feast/feature_store.yaml"): + logger.info("Loading config from mounted files") + feast_config, mat_config = _load_config_from_files() + else: + raise RuntimeError( + "No config source found. Set FEAST_CONFIGMAP_NAME env var " + "or mount config at /var/feast/" + ) + + from pyspark.sql import SparkSession + + from feast import RepoConfig + + RepoConfig(**feast_config) # validate config eagerly before any Spark work + operation = mat_config["operation"] + + if operation == "materialize": + tasks = mat_config.get("tasks", []) + if not tasks: + tasks = [ + { + "feature_view": mat_config["feature_view"], + "start_time": mat_config["start_time"], + "end_time": mat_config["end_time"], + } + ] + + concurrency = mat_config.get("concurrency", 1) + total = len(tasks) + total_start = time.time() + + logger.info( + f"Starting materialization: {total} feature views, " + f"concurrency={concurrency}" + ) + + # Get the active SparkSession (created by spark-submit) + spark = SparkSession.getActiveSession() + if spark is None: + spark = SparkSession.builder.getOrCreate() + logger.info(f"SparkSession: {spark.sparkContext.applicationId}") + + succeeded, failed = 0, 0 + if concurrency <= 1: + for i, task in enumerate(tasks, 1): + fv_name = task["feature_view"] + logger.info(f"[{i}/{total}] Materializing: {fv_name}") + try: + name, elapsed = _materialize_one_fv(spark, feast_config, task) + succeeded += 1 + logger.info(f"[{i}/{total}] Completed: {name} ({elapsed:.1f}s)") + except Exception: + failed += 1 + logger.exception(f"[{i}/{total}] Failed: {fv_name}") + else: + with ThreadPoolExecutor(max_workers=concurrency) as executor: + future_to_fv = { + executor.submit( + _materialize_one_fv, spark, feast_config, task + ): task["feature_view"] + for task in tasks + } + for future in as_completed(future_to_fv): + fv_name = future_to_fv[future] + try: + name, elapsed = future.result() + succeeded += 1 + logger.info(f"Completed: {name} ({elapsed:.1f}s)") + except Exception: + failed += 1 + logger.exception(f"Failed: {fv_name}") + + total_elapsed = time.time() - total_start + level = logging.ERROR if failed > 0 else logging.INFO + logger.log( + level, + f"Materialization batch complete: " + f"{succeeded} succeeded, {failed} failed, {total} total, " + f"elapsed={total_elapsed:.1f}s", + ) + if failed > 0: + sys.exit(1) + else: + raise ValueError(f"Unknown operation: {operation}") + + +if __name__ == "__main__": + try: + main() + except Exception: + logger.exception("Driver failed") + sys.exit(1) diff --git a/sdk/python/feast/infra/compute_engines/utils.py b/sdk/python/feast/infra/compute_engines/utils.py index 20a3dae981d..c36f4d19f07 100644 --- a/sdk/python/feast/infra/compute_engines/utils.py +++ b/sdk/python/feast/infra/compute_engines/utils.py @@ -1,9 +1,32 @@ from datetime import datetime -from typing import Optional +from typing import Any, Mapping, Optional, Sequence from feast.data_source import DataSource from feast.infra.compute_engines.dag.context import ColumnInfo, ExecutionContext from feast.infra.offline_stores.offline_store import RetrievalJob +from feast.infra.offline_stores.offline_utils import ( + DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL, + infer_event_timestamp_from_entity_df, +) + +ENTITY_TS_ALIAS = "__entity_event_timestamp" +ENTITY_ROW_ID = "__feast_entity_row_id" + + +def infer_entity_timestamp_column(entity_schema: Mapping[str, Any]) -> str: + """Resolve the entity timestamp column used for point-in-time joins.""" + if ENTITY_TS_ALIAS in entity_schema: + return ENTITY_TS_ALIAS + return infer_event_timestamp_from_entity_df(dict(entity_schema)) + + +def find_entity_timestamp_column(columns: Sequence[str]) -> Optional[str]: + """Find the timestamp column in an entity DataFrame schema, if present.""" + if ENTITY_TS_ALIAS in columns: + return ENTITY_TS_ALIAS + if DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL in columns: + return DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL + return None def create_offline_store_retrieval_job( @@ -21,20 +44,40 @@ def create_offline_store_retrieval_job( context: start_time: end_time: - Returns: """ offline_store = context.offline_store - # 📥 Reuse Feast's robust query resolver - retrieval_job = offline_store.pull_all_from_table_or_query( - config=context.repo_config, - data_source=data_source, - join_key_columns=column_info.join_keys, - feature_name_columns=column_info.feature_cols, - timestamp_field=column_info.ts_col, - created_timestamp_column=column_info.created_ts_col, - start_date=start_time, - end_date=end_time, - ) + + pull_latest = context.repo_config.materialization_config.pull_latest_features + + if pull_latest: + if not start_time or not end_time: + raise ValueError( + "start_time and end_time must be provided when pull_latest_features is True" + ) + + retrieval_job = offline_store.pull_latest_from_table_or_query( + config=context.repo_config, + data_source=data_source, + join_key_columns=column_info.join_keys, + feature_name_columns=column_info.feature_cols, + timestamp_field=column_info.ts_col, + created_timestamp_column=column_info.created_ts_col, + start_date=start_time, + end_date=end_time, + ) + else: + # 📥 Reuse Feast's robust query resolver + retrieval_job = offline_store.pull_all_from_table_or_query( + config=context.repo_config, + data_source=data_source, + join_key_columns=column_info.join_keys, + feature_name_columns=column_info.feature_cols, + timestamp_field=column_info.ts_col, + created_timestamp_column=column_info.created_ts_col, + start_date=start_time, + end_date=end_time, + ) + return retrieval_job diff --git a/sdk/python/feast/infra/contrib/grpc_server.py b/sdk/python/feast/infra/contrib/grpc_server.py index b6ed6cb25d4..4c042051225 100644 --- a/sdk/python/feast/infra/contrib/grpc_server.py +++ b/sdk/python/feast/infra/contrib/grpc_server.py @@ -1,11 +1,11 @@ import logging import threading +from collections.abc import Mapping from concurrent import futures from typing import Optional, Union import grpc import pandas as pd -from grpc_health.v1 import health, health_pb2_grpc from feast.data_source import PushMode from feast.errors import FeatureServiceNotFoundException, PushSourceNotFoundException @@ -34,6 +34,20 @@ def parse(features): return pd.DataFrame.from_dict(df) +def parse_typed(typed_features): + df = {} + for key, value in typed_features.items(): + val_case = value.WhichOneof("val") + if val_case is None or val_case == "null_val": + df[key] = [None] + else: + raw = getattr(value, val_case) + if hasattr(raw, "val"): + raw = dict(raw.val) if isinstance(raw.val, Mapping) else list(raw.val) + df[key] = [raw] + return pd.DataFrame.from_dict(df) + + class GrpcFeatureServer(GrpcFeatureServerServicer): fs: FeatureStore @@ -49,7 +63,17 @@ def __init__(self, fs: FeatureStore, registry_ttl_sec: int = 5): def Push(self, request, context): try: - df = parse(request.features) + if request.features and request.typed_features: + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + context.set_details( + "Only one of features or typed_features may be set, not both" + ) + return PushResponse(status=False) + df = ( + parse_typed(request.typed_features) + if request.typed_features + else parse(request.features) + ) if request.to == "offline": to = PushMode.OFFLINE elif request.to == "online": @@ -62,7 +86,7 @@ def Push(self, request, context): f"'online_and_offline']." ) self.fs.push( - push_source_name=request.push_source_name, + push_source_name=request.stream_feature_view, df=df, allow_registry_cache=request.allow_registry_cache, to=to, @@ -84,7 +108,17 @@ def WriteToOnlineStore(self, request, context): "write_to_online_store is deprecated. Please consider using Push instead" ) try: - df = parse(request.features) + if request.features and request.typed_features: + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + context.set_details( + "Only one of features or typed_features may be set, not both" + ) + return WriteToOnlineStoreResponse(status=False) + df = ( + parse_typed(request.typed_features) + if request.typed_features + else parse(request.features) + ) self.fs.write_to_online_store( feature_view_name=request.feature_view_name, df=df, @@ -94,7 +128,7 @@ def WriteToOnlineStore(self, request, context): logger.exception(str(e)) context.set_code(grpc.StatusCode.INTERNAL) context.set_details(str(e)) - return PushResponse(status=False) + return WriteToOnlineStoreResponse(status=False) return WriteToOnlineStoreResponse(status=True) def GetOnlineFeatures(self, request: GetOnlineFeaturesRequest, context): @@ -136,6 +170,8 @@ def get_grpc_server( max_workers: int, registry_ttl_sec: int, ): + from grpc_health.v1 import health, health_pb2_grpc + logger.info(f"Initializing gRPC server on {address}") server = grpc.server(futures.ThreadPoolExecutor(max_workers=max_workers)) add_GrpcFeatureServerServicer_to_server( diff --git a/sdk/python/feast/infra/contrib/spark_kafka_processor.py b/sdk/python/feast/infra/contrib/spark_kafka_processor.py index e148000bc96..94860a4be68 100644 --- a/sdk/python/feast/infra/contrib/spark_kafka_processor.py +++ b/sdk/python/feast/infra/contrib/spark_kafka_processor.py @@ -1,5 +1,5 @@ from types import MethodType -from typing import List, Optional, no_type_check +from typing import Dict, List, Optional, no_type_check import pandas as pd from pyspark.sql import DataFrame, SparkSession @@ -29,6 +29,7 @@ class SparkKafkaProcessor(StreamProcessor): format: str preprocess_fn: Optional[MethodType] join_keys: List[str] + stream_source_options: Optional[Dict[str, str]] def __init__( self, @@ -37,6 +38,7 @@ def __init__( sfv: StreamFeatureView, config: ProcessorConfig, preprocess_fn: Optional[MethodType] = None, + stream_source_options: Optional[Dict[str, str]], ): if not isinstance(sfv.stream_source, KafkaSource): raise ValueError("data source is not kafka source") @@ -59,6 +61,7 @@ def __init__( raise ValueError("config is not spark processor config") self.spark = config.spark_session self.preprocess_fn = preprocess_fn + self.stream_source_options = stream_source_options self.processing_time = config.processing_time self.query_timeout = config.query_timeout self.join_keys = [fs.get_entity(entity).join_key for entity in sfv.entities] @@ -80,6 +83,15 @@ def ingest_stream_feature_view( @no_type_check def _ingest_stream_data(self) -> StreamTable: """Only supports json and avro formats currently.""" + kafka_options: Dict[str, str] = { + "kafka.bootstrap.servers": self.data_source.kafka_options.kafka_bootstrap_servers, + "subscribe": self.data_source.kafka_options.topic, + "startingOffsets": "latest", + } + if self.stream_source_options: + # Update user-provided options to override defaults + kafka_options.update(self.stream_source_options) + if self.format == "json": if not isinstance( self.data_source.kafka_options.message_format, JsonFormat @@ -87,12 +99,7 @@ def _ingest_stream_data(self) -> StreamTable: raise ValueError("kafka source message format is not jsonformat") stream_df = ( self.spark.readStream.format("kafka") - .option( - "kafka.bootstrap.servers", - self.data_source.kafka_options.kafka_bootstrap_servers, - ) - .option("subscribe", self.data_source.kafka_options.topic) - .option("startingOffsets", "latest") # Query start + .options(**kafka_options) .load() .selectExpr("CAST(value AS STRING)") .select( @@ -110,12 +117,7 @@ def _ingest_stream_data(self) -> StreamTable: raise ValueError("kafka source message format is not avro format") stream_df = ( self.spark.readStream.format("kafka") - .option( - "kafka.bootstrap.servers", - self.data_source.kafka_options.kafka_bootstrap_servers, - ) - .option("subscribe", self.data_source.kafka_options.topic) - .option("startingOffsets", "latest") # Query start + .options(**kafka_options) .load() .selectExpr("CAST(value AS STRING)") .select( diff --git a/sdk/python/feast/loaders/__init__.py b/sdk/python/feast/infra/data_sources/__init__.py similarity index 100% rename from sdk/python/feast/loaders/__init__.py rename to sdk/python/feast/infra/data_sources/__init__.py diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/__init__.py b/sdk/python/feast/infra/data_sources/contrib/__init__.py similarity index 100% rename from sdk/python/tests/integration/feature_repos/universal/data_sources/__init__.py rename to sdk/python/feast/infra/data_sources/contrib/__init__.py diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/__init__.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/__init__.py new file mode 100644 index 00000000000..2a6d36f87e2 --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/__init__.py @@ -0,0 +1,27 @@ +from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_rest_client import ( + CatalogAuthError, + IcebergCatalogError, + IcebergRestClient, + TableMetadata, + TableNotFoundError, + TempCredential, +) +from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, +) +from feast.infra.data_sources.contrib.iceberg_catalog.uc_client import UCClient +from feast.infra.data_sources.contrib.iceberg_catalog.unity_catalog_source import ( + UnityCatalogSource, +) + +__all__ = [ + "CatalogAuthError", + "IcebergCatalogError", + "IcebergRestClient", + "IcebergSource", + "TableMetadata", + "TableNotFoundError", + "TempCredential", + "UCClient", + "UnityCatalogSource", +] diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/iceberg_rest_client.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/iceberg_rest_client.py new file mode 100644 index 00000000000..6a44a2fe71d --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/iceberg_rest_client.py @@ -0,0 +1,345 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + +import requests + +logger = logging.getLogger(__name__) + + +class IcebergCatalogError(Exception): + """Base exception for Iceberg REST Catalog errors.""" + + def __init__(self, message: str, status_code: Optional[int] = None): + self.status_code = status_code + super().__init__(message) + + +class TableNotFoundError(IcebergCatalogError): + """Raised when a table or namespace is not found (HTTP 404).""" + + pass + + +class CatalogAuthError(IcebergCatalogError): + """Raised on authentication/authorization failures (HTTP 401/403).""" + + pass + + +@dataclass +class TableMetadata: + """Metadata returned by the Iceberg REST Catalog for a table.""" + + table_id: str + location: str + schema_fields: List[Dict[str, str]] + properties: Dict[str, str] = field(default_factory=dict) + format_version: int = 2 + current_snapshot_id: Optional[int] = None + + +@dataclass +class TempCredential: + """Temporary, scoped credential vended by the catalog.""" + + access_key: Optional[str] = None + secret_key: Optional[str] = None + session_token: Optional[str] = None + expiration_ms: Optional[int] = None + uri: Optional[str] = None + credential_type: str = "s3" + + +class IcebergRestClient: + """Client for the standard Apache Iceberg REST Catalog protocol. + + Implements table metadata loading and credential vending as defined + in the Iceberg REST Catalog specification. Works with any compliant + catalog: Unity Catalog, Apache Polaris, Nessie, Snowflake Open Catalog, etc. + + URL construction follows the spec: + - With warehouse and url_style="unity": /v1/catalogs/{warehouse}/... + (Unity Catalog flavor) + - With warehouse and url_style="prefix": /v1/{warehouse}/... + (Polaris, Nessie, standard Iceberg REST spec) + - Without warehouse: /v1/... (vanilla spec) + """ + + def __init__( + self, + endpoint: str, + token: Optional[str] = None, + warehouse: Optional[str] = None, + credential_vending: bool = True, + timeout: int = 30, + url_style: str = "auto", + ): + """ + Args: + endpoint: Base URL of the catalog server. + token: Bearer token for authentication. + warehouse: Warehouse/catalog identifier. + credential_vending: Whether to request vended credentials. + timeout: HTTP request timeout in seconds. + url_style: URL path construction style: + - "unity": /v1/catalogs/{warehouse} (Databricks Unity Catalog) + - "prefix": /v1/{warehouse} (Polaris, Nessie, standard spec) + - "auto" (default): uses "unity" if endpoint contains + "unity-catalog", otherwise "prefix" + """ + self.endpoint = endpoint.rstrip("/") + self.warehouse = warehouse + self.credential_vending = credential_vending + self.timeout = timeout + + if url_style == "auto": + self.url_style = "unity" if "unity-catalog" in self.endpoint else "prefix" + else: + self.url_style = url_style + + self._session = requests.Session() + self._session.headers.update( + { + "Content-Type": "application/json", + "Accept": "application/json", + } + ) + if token: + self._session.headers["Authorization"] = f"Bearer {token}" + if credential_vending: + self._session.headers["X-Iceberg-Access-Delegation"] = "vended-credentials" + self.headers = dict(self._session.headers) + + @property + def _base_url(self) -> str: + """Build base URL following the Iceberg REST spec. + + Unity Catalog uses /v1/catalogs/{warehouse} while the standard + spec uses /v1/{prefix} (where prefix = warehouse). + """ + if self.warehouse: + if self.url_style == "unity": + return f"{self.endpoint}/v1/catalogs/{self.warehouse}" + return f"{self.endpoint}/v1/{self.warehouse}" + return f"{self.endpoint}/v1" + + def _raise_for_status(self, response: requests.Response, context: str): + """Raise typed exceptions based on HTTP status codes.""" + if response.ok: + return + status = response.status_code + try: + body = response.json() + detail = body.get("message", body.get("error", response.text)) + except Exception: + detail = response.text + + msg = f"{context}: HTTP {status} — {detail}" + if status == 404: + raise TableNotFoundError(msg, status_code=status) + if status in (401, 403): + raise CatalogAuthError(msg, status_code=status) + raise IcebergCatalogError(msg, status_code=status) + + def load_table(self, namespace: str, table: str) -> TableMetadata: + """Load table metadata from the catalog. + + Raises: + TableNotFoundError: If the table does not exist. + CatalogAuthError: On authentication/authorization failures. + IcebergCatalogError: On other HTTP errors. + """ + url = f"{self._base_url}/namespaces/{namespace}/tables/{table}" + try: + response = self._session.get(url, timeout=self.timeout) + except requests.exceptions.ConnectionError as e: + raise IcebergCatalogError( + f"Connection failed for {namespace}.{table}: {e}" + ) from e + except requests.exceptions.Timeout as e: + raise IcebergCatalogError( + f"Request timed out for {namespace}.{table}: {e}" + ) from e + + self._raise_for_status(response, f"Failed to load table {namespace}.{table}") + data = response.json() + return self._parse_table_metadata(data) + + def get_table_location(self, namespace: str, table: str) -> Optional[str]: + """Returns the storage location for a table.""" + metadata = self.load_table(namespace, table) + return metadata.location + + def vend_credentials(self, namespace: str, table: str) -> Optional[TempCredential]: + """Request temporary credentials for table access. + + Uses the standard Iceberg REST credential vending protocol via the + X-Iceberg-Access-Delegation header. Credentials are scoped and short-lived. + """ + if not self.credential_vending: + return None + + url = f"{self._base_url}/namespaces/{namespace}/tables/{table}" + try: + response = self._session.get(url, timeout=self.timeout) + except requests.exceptions.RequestException as e: + logger.warning( + "Credential vending failed for %s.%s: %s", namespace, table, e + ) + return None + + self._raise_for_status( + response, f"Credential vending failed for {namespace}.{table}" + ) + data = response.json() + return self._parse_credentials(data) + + def list_tables(self, namespace: str) -> List[str]: + """List all tables in a namespace, handling pagination. + + Raises: + CatalogAuthError: On authentication/authorization failures. + IcebergCatalogError: On other HTTP errors. + """ + tables: List[str] = [] + url = f"{self._base_url}/namespaces/{namespace}/tables" + params: Dict[str, str] = {} + + while True: + try: + response = self._session.get(url, params=params, timeout=self.timeout) + except requests.exceptions.RequestException as e: + raise IcebergCatalogError( + f"Failed to list tables in namespace {namespace}: {e}" + ) from e + + self._raise_for_status( + response, f"Failed to list tables in namespace {namespace}" + ) + data = response.json() + identifiers = data.get("identifiers", []) + tables.extend(ident["name"] for ident in identifiers if "name" in ident) + + next_token = data.get("next-page-token") + if not next_token: + break + params["pageToken"] = next_token + + return tables + + def list_namespaces(self) -> List[str]: + """List all namespaces in the catalog, handling pagination. + + Raises: + CatalogAuthError: On authentication/authorization failures. + IcebergCatalogError: On other HTTP errors. + """ + result: List[str] = [] + url = f"{self._base_url}/namespaces" + params: Dict[str, str] = {} + + while True: + try: + response = self._session.get(url, params=params, timeout=self.timeout) + except requests.exceptions.RequestException as e: + raise IcebergCatalogError(f"Failed to list namespaces: {e}") from e + + self._raise_for_status(response, "Failed to list namespaces") + data = response.json() + namespaces = data.get("namespaces", []) + result.extend(".".join(ns) for ns in namespaces) + + next_token = data.get("next-page-token") + if not next_token: + break + params["pageToken"] = next_token + + return result + + def get_table_schema(self, namespace: str, table: str) -> List[Dict[str, str]]: + """Returns the schema fields for a table.""" + metadata = self.load_table(namespace, table) + return metadata.schema_fields + + def _parse_table_metadata(self, data: Dict[str, Any]) -> TableMetadata: + """Parse the Iceberg REST API response into TableMetadata.""" + metadata = data.get("metadata", data) + location = metadata.get("location", "") + properties = metadata.get("properties", {}) + format_version = metadata.get("format-version", 2) + + schema_fields = [] + schemas = metadata.get("schemas", []) + if schemas: + current_schema_id = metadata.get("current-schema-id") + current_schema = None + if current_schema_id is not None: + for s in schemas: + if s.get("schema-id") == current_schema_id: + current_schema = s + break + if current_schema is None: + current_schema = schemas[-1] + + for col in current_schema.get("fields", []): + field_type = col.get("type", "") + if isinstance(field_type, dict): + field_type = field_type.get("type", "unknown") + schema_fields.append( + {"name": col.get("name", ""), "type": str(field_type)} + ) + + current_snapshot_id = metadata.get("current-snapshot-id") + table_uuid = metadata.get("table-uuid", "") + + return TableMetadata( + table_id=table_uuid, + location=location, + schema_fields=schema_fields, + properties=properties, + format_version=format_version, + current_snapshot_id=current_snapshot_id, + ) + + def _parse_credentials(self, data: Dict[str, Any]) -> Optional[TempCredential]: + """Parse vended credentials from the load-table response config.""" + config = data.get("config", {}) + if not config: + return None + + if any(k.startswith("s3.") for k in config): + return TempCredential( + access_key=config.get("s3.access-key-id"), + secret_key=config.get("s3.secret-access-key"), + session_token=config.get("s3.session-token"), + expiration_ms=config.get("s3.session-token-expiration-ms"), + uri=config.get("s3.endpoint"), + credential_type="s3", + ) + if any(k.startswith("gcs.") for k in config): + return TempCredential( + access_key=config.get("gcs.oauth2.token"), + expiration_ms=config.get("gcs.oauth2.token-expires-at"), + credential_type="gcs", + ) + if any(k.startswith("adls.") for k in config): + return TempCredential( + access_key=config.get("adls.sas-token"), + credential_type="adls", + ) + return None diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/iceberg_source.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/iceberg_source.py new file mode 100644 index 00000000000..757033865f9 --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/iceberg_source.py @@ -0,0 +1,306 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from typing import Any, Callable, Dict, Iterable, Optional, Tuple + +from feast.data_source import DataSource +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto +from feast.repo_config import RepoConfig +from feast.value_type import ValueType + + +class IcebergSource(DataSource): + """Data source backed by Apache Iceberg tables via any supported catalog. + + Supports multiple catalog backends through the ``catalog_type`` parameter: + - ``"rest"`` (default): Iceberg REST Catalog protocol (Unity Catalog, Polaris, Nessie, etc.) + - ``"hive"``: Hive Metastore catalog + - ``"glue"``: AWS Glue catalog + - ``"sql"``: SQL-based catalog (JDBC) + - ``"dynamodb"``: DynamoDB-based catalog + + Additional catalog-specific configuration can be passed via ``catalog_properties``. + """ + + def __init__( + self, + *, + warehouse: str, + namespace: str, + table: str, + catalog_type: str = "rest", + catalog_name: str = "feast_iceberg", + endpoint: Optional[str] = None, + catalog_properties: Optional[Dict[str, str]] = None, + name: Optional[str] = None, + timestamp_field: Optional[str] = None, + created_timestamp_column: Optional[str] = None, + field_mapping: Optional[Dict[str, str]] = None, + description: Optional[str] = "", + tags: Optional[Dict[str, str]] = None, + owner: Optional[str] = "", + token_env_var: Optional[str] = None, + credential_vending: bool = True, + ): + _name = name or f"{warehouse}.{namespace}.{table}" + super().__init__( + name=_name, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + field_mapping=field_mapping, + description=description, + tags=tags, + owner=owner, + ) + self.catalog_type = catalog_type + self.catalog_name = catalog_name + self.endpoint = endpoint or "" + self.warehouse = warehouse + self.namespace = namespace + self.iceberg_table = table + self.token_env_var = token_env_var + self.credential_vending = credential_vending + self.catalog_properties = catalog_properties or {} + + @property + def fqn(self) -> str: + """Fully qualified table name: warehouse.namespace.table.""" + return f"{self.warehouse}.{self.namespace}.{self.iceberg_table}" + + def get_catalog_client(self): + """Create a catalog client based on catalog_type. + + For "rest": returns an IcebergRestClient (built-in lightweight client). + For other types: delegates to PyIceberg's load_catalog(). + """ + if self.catalog_type == "rest": + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_rest_client import ( + IcebergRestClient, + ) + + token = None + if self.token_env_var: + token = os.environ.get(self.token_env_var) + if not token: + import logging + + logging.getLogger(__name__).warning( + "token_env_var '%s' is set but the environment " + "variable is empty or missing", + self.token_env_var, + ) + return IcebergRestClient( + endpoint=self.endpoint, + token=token, + warehouse=self.warehouse, + credential_vending=self.credential_vending, + ) + + from pyiceberg.catalog import load_catalog + + config = { + "type": self.catalog_type, + **self.catalog_properties, + } + if self.endpoint: + config.setdefault("uri", self.endpoint) + if self.warehouse: + config.setdefault("warehouse", self.warehouse) + if self.token_env_var: + token = os.environ.get(self.token_env_var, "") + if token: + config.setdefault("token", token) + return load_catalog(self.catalog_name, **config) + + def source_type(self) -> DataSourceProto.SourceType.ValueType: + return DataSourceProto.BATCH_ICEBERG + + @staticmethod + def from_proto(data_source: DataSourceProto) -> Any: + cls_type = data_source.data_source_class_type + if cls_type and "UnityCatalogSource" in cls_type: + from feast.infra.data_sources.contrib.iceberg_catalog.unity_catalog_source import ( + UnityCatalogSource, + ) + + return UnityCatalogSource.from_proto(data_source) + + custom_options = json.loads( + str(data_source.custom_options.configuration, encoding="utf8") + ) + return IcebergSource( + name=data_source.name, + catalog_type=custom_options.get("catalog_type", "rest"), + catalog_name=custom_options.get("catalog_name", "feast_iceberg"), + endpoint=custom_options.get("endpoint", ""), + warehouse=custom_options["warehouse"], + namespace=custom_options["namespace"], + table=custom_options["table"], + token_env_var=custom_options.get("token_env_var"), + credential_vending=custom_options.get("credential_vending", True), + catalog_properties=custom_options.get("catalog_properties", {}), + timestamp_field=data_source.timestamp_field, + created_timestamp_column=data_source.created_timestamp_column, + field_mapping=dict(data_source.field_mapping), + description=data_source.description, + tags=dict(data_source.tags), + owner=data_source.owner, + ) + + def _to_proto_impl(self) -> DataSourceProto: + config_json = json.dumps( + { + "catalog_type": self.catalog_type, + "catalog_name": self.catalog_name, + "endpoint": self.endpoint, + "warehouse": self.warehouse, + "namespace": self.namespace, + "table": self.iceberg_table, + "token_env_var": self.token_env_var, + "credential_vending": self.credential_vending, + "catalog_properties": self.catalog_properties, + } + ) + data_source_proto = DataSourceProto( + name=self.name, + type=DataSourceProto.BATCH_ICEBERG, + data_source_class_type=( + "feast.infra.data_sources.contrib.iceberg_catalog" + ".iceberg_source.IcebergSource" + ), + custom_options=DataSourceProto.CustomSourceOptions( + configuration=bytes(config_json, encoding="utf8") + ), + description=self.description, + tags=self.tags, + owner=self.owner, + timestamp_field=self.timestamp_field, + created_timestamp_column=self.created_timestamp_column, + field_mapping=self.field_mapping, + ) + return data_source_proto + + @staticmethod + def source_datatype_to_feast_value_type() -> Callable[[str], ValueType]: + return _iceberg_type_to_feast_value_type + + def get_table_column_names_and_types( + self, config: RepoConfig + ) -> Iterable[Tuple[str, str]]: + """Retrieve column names and types from the Iceberg catalog.""" + client = self.get_catalog_client() + if self.catalog_type == "rest": + metadata = client.load_table(self.namespace, self.iceberg_table) + if metadata.schema_fields: + return [(f["name"], f["type"]) for f in metadata.schema_fields] + return [] + else: + table = client.load_table(f"{self.namespace}.{self.iceberg_table}") + return [ + (field.name, str(field.field_type)) for field in table.schema().fields + ] + + def get_table_query_string(self) -> str: + """Return a SQL-friendly reference for this source (temp view name).""" + return f"iceberg_tmp_{self.iceberg_table}" + + def validate(self, config: RepoConfig): + """Validates connectivity to the catalog. + + Raises: + ValueError: If the table cannot be loaded. + """ + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_rest_client import ( + CatalogAuthError, + IcebergCatalogError, + TableNotFoundError, + ) + + try: + client = self.get_catalog_client() + if self.catalog_type == "rest": + client.load_table(self.namespace, self.iceberg_table) + else: + client.load_table(f"{self.namespace}.{self.iceberg_table}") + except TableNotFoundError: + raise ValueError( + f"Table '{self.fqn}' not found in catalog " + f"(type={self.catalog_type}) at '{self.endpoint}'" + ) + except CatalogAuthError as e: + raise ValueError( + f"Authentication failed for catalog at '{self.endpoint}': {e}" + ) + except IcebergCatalogError as e: + raise ValueError( + f"Could not load table '{self.fqn}' from catalog " + f"(type={self.catalog_type}) at '{self.endpoint}': {e}" + ) + except Exception as e: + raise ValueError( + f"Could not load table '{self.fqn}' from catalog " + f"(type={self.catalog_type}) at '{self.endpoint}': {e}" + ) + + def __eq__(self, other): + if not isinstance(other, IcebergSource): + return False + if not super().__eq__(other): + return False + return ( + self.catalog_type == other.catalog_type + and self.catalog_name == other.catalog_name + and self.endpoint == other.endpoint + and self.warehouse == other.warehouse + and self.namespace == other.namespace + and self.iceberg_table == other.iceberg_table + and self.token_env_var == other.token_env_var + and self.credential_vending == other.credential_vending + and self.catalog_properties == other.catalog_properties + ) + + def __hash__(self): + return hash( + ( + self.name, + self.catalog_type, + self.endpoint, + self.warehouse, + self.namespace, + self.iceberg_table, + ) + ) + + +def _iceberg_type_to_feast_value_type(iceberg_type: str) -> ValueType: + """Maps Iceberg data types to Feast ValueTypes.""" + type_map = { + "boolean": ValueType.BOOL, + "int": ValueType.INT32, + "long": ValueType.INT64, + "float": ValueType.FLOAT, + "double": ValueType.DOUBLE, + "string": ValueType.STRING, + "binary": ValueType.BYTES, + "date": ValueType.INT32, + "timestamp": ValueType.INT64, + "timestamptz": ValueType.INT64, + "decimal": ValueType.DOUBLE, + "uuid": ValueType.STRING, + "fixed": ValueType.BYTES, + } + normalized = iceberg_type.lower().split("(")[0].strip() + return type_map.get(normalized, ValueType.UNKNOWN) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/__init__.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/__init__.py similarity index 100% rename from sdk/python/tests/integration/feature_repos/universal/online_store/__init__.py rename to sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/__init__.py diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/data_source.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/data_source.py new file mode 100644 index 00000000000..7610e2bb447 --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/data_source.py @@ -0,0 +1,92 @@ +"""DataSourceCreator for IcebergSource with DuckDB offline store. + +Used by the universal integration test suite to parametrize tests against +IcebergSource backed by the DuckDB offline store. +""" + +import os +import tempfile +from typing import Dict, Optional + +import pandas as pd + +from feast.data_source import DataSource +from feast.feature_logging import LoggingDestination +from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, +) +from feast.infra.offline_stores.duckdb import DuckDBOfflineStoreConfig +from feast.repo_config import FeastConfigBaseModel +from tests.universal.feature_repos.universal.data_source_creator import ( + DataSourceCreator, +) + + +class IcebergDataSourceCreator(DataSourceCreator): + """Creates IcebergSource instances backed by local Parquet files. + + For testing without a real Iceberg REST Catalog server, this creator writes + DataFrames to local Parquet files. The DuckDB offline store's fallback path + reads Parquet directly from the resolved location via file:// endpoint. + """ + + tables: Dict[str, str] + + def __init__(self, project_name: str, *args, **kwargs): + super().__init__(project_name) + self.tables = {} + self._temp_dir = tempfile.mkdtemp(prefix="feast_iceberg_test_") + + def create_data_source( + self, + df: pd.DataFrame, + destination_name: str, + created_timestamp_column: Optional[str] = "created_ts", + field_mapping: Optional[Dict[str, str]] = None, + timestamp_field: Optional[str] = "ts", + ) -> DataSource: + """Write df to a local Parquet file, return an IcebergSource.""" + table_dir = os.path.join(self._temp_dir, destination_name) + os.makedirs(table_dir, exist_ok=True) + parquet_path = os.path.join(table_dir, "data.parquet") + df.to_parquet(parquet_path, index=False) + self.tables[destination_name] = table_dir + + return IcebergSource( + endpoint="file://local", + warehouse="test", + namespace="default", + table=destination_name, + name=destination_name, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + field_mapping=field_mapping or {}, + token_env_var=None, + credential_vending=False, + ) + + def create_offline_store_config(self) -> FeastConfigBaseModel: + return DuckDBOfflineStoreConfig() + + def create_saved_dataset_destination(self): + from feast.infra.offline_stores.file_source import SavedDatasetFileStorage + + return SavedDatasetFileStorage( + path=os.path.join(self._temp_dir, "saved_datasets"), + file_format="parquet", + ) + + def create_logged_features_destination(self) -> LoggingDestination: + from feast.infra.offline_stores.file_source import FileLoggingDestination + + return FileLoggingDestination( + path=os.path.join(self._temp_dir, "logged_features"), + ) + + def get_prefixed_table_name(self, suffix: str) -> str: + return f"{self.project_name}_{suffix}" + + def teardown(self): + import shutil + + shutil.rmtree(self._temp_dir, ignore_errors=True) diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/test_iceberg_duckdb.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/test_iceberg_duckdb.py new file mode 100644 index 00000000000..689607f5085 --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/test_iceberg_duckdb.py @@ -0,0 +1,89 @@ +"""Unit tests for DuckDB offline store reading from IcebergSource.""" + +from unittest.mock import MagicMock, patch + +import pyarrow as pa +import pytest + +from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, +) +from feast.infra.offline_stores.duckdb import ( + _read_iceberg_catalog_source, +) + + +class TestReadIcebergCatalogSource: + @patch( + "feast.infra.data_sources.contrib.iceberg_catalog" + ".iceberg_source.IcebergSource.get_catalog_client" + ) + def test_pyiceberg_scan(self, mock_get_client): + """Verify data is loaded via PyIceberg scan -> Arrow -> ibis.""" + arrow_tbl = pa.table({"id": [1, 2], "val": [10.0, 20.0]}) + + mock_iceberg_table = MagicMock() + mock_iceberg_table.scan.return_value.to_arrow.return_value = arrow_tbl + + mock_client = MagicMock() + mock_client.load_table.return_value = mock_iceberg_table + mock_get_client.return_value = mock_client + + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="my_table", + credential_vending=False, + ) + result = _read_iceberg_catalog_source(source, repo_path=".") + result_df = result.execute() + assert len(result_df) == 2 + assert list(result_df["val"]) == [10.0, 20.0] + mock_client.load_table.assert_called_once_with("ns.my_table") + + @patch( + "feast.infra.data_sources.contrib.iceberg_catalog" + ".iceberg_source.IcebergSource.get_catalog_client" + ) + def test_catalog_load_failure_propagates(self, mock_get_client): + """Verify errors propagate instead of being silently swallowed.""" + mock_client = MagicMock() + mock_client.load_table.side_effect = Exception("table not found") + mock_get_client.return_value = mock_client + + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + with pytest.raises(Exception, match="table not found"): + _read_iceberg_catalog_source(source, repo_path=".") + + @patch("pyiceberg.catalog.load_catalog") + def test_rest_catalog_path(self, mock_load_catalog): + """Verify REST catalog type uses load_catalog with correct config.""" + arrow_tbl = pa.table({"id": [1], "val": [42.0]}) + + mock_iceberg_table = MagicMock() + mock_iceberg_table.scan.return_value.to_arrow.return_value = arrow_tbl + + mock_catalog = MagicMock() + mock_catalog.load_table.return_value = mock_iceberg_table + mock_load_catalog.return_value = mock_catalog + + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + catalog_type="rest", + credential_vending=False, + ) + result = _read_iceberg_catalog_source(source, repo_path=".") + result_df = result.execute() + assert len(result_df) == 1 + assert result_df["val"].iloc[0] == 42.0 + mock_load_catalog.assert_called_once() + mock_catalog.load_table.assert_called_once_with("ns.t") diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/test_iceberg_source.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/test_iceberg_source.py new file mode 100644 index 00000000000..d728bda4545 --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/test_iceberg_source.py @@ -0,0 +1,655 @@ +"""Unit tests for IcebergSource, UnityCatalogSource, and IcebergRestClient.""" + +import json +from unittest.mock import MagicMock, Mock, patch + +import pytest +import requests + +from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_rest_client import ( + CatalogAuthError, + IcebergCatalogError, + IcebergRestClient, + TableNotFoundError, +) +from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, + _iceberg_type_to_feast_value_type, +) +from feast.infra.data_sources.contrib.iceberg_catalog.unity_catalog_source import ( + UnityCatalogSource, +) +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto +from feast.value_type import ValueType + + +class TestIcebergSource: + def test_basic_creation(self): + source = IcebergSource( + endpoint="http://localhost:8080/api/2.1/unity-catalog/iceberg", + warehouse="unity", + namespace="default", + table="my_table", + timestamp_field="event_ts", + token_env_var="MY_TOKEN", + ) + assert source.fqn == "unity.default.my_table" + assert source.endpoint == "http://localhost:8080/api/2.1/unity-catalog/iceberg" + assert source.warehouse == "unity" + assert source.namespace == "default" + assert source.iceberg_table == "my_table" + assert source.token_env_var == "MY_TOKEN" + assert source.credential_vending is True + assert source.name == "unity.default.my_table" + + def test_custom_name(self): + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + name="custom_name", + ) + assert source.name == "custom_name" + assert source.fqn == "w.ns.t" + + def test_source_type(self): + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + assert source.source_type() == DataSourceProto.BATCH_ICEBERG + + def test_catalog_type_default(self): + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + assert source.catalog_type == "rest" + + def test_catalog_type_explicit(self): + source = IcebergSource( + catalog_type="glue", + warehouse="my_account", + namespace="my_db", + table="t", + catalog_properties={"region_name": "us-east-1"}, + ) + assert source.catalog_type == "glue" + assert source.catalog_properties == {"region_name": "us-east-1"} + + def test_proto_roundtrip(self): + source = IcebergSource( + endpoint="http://localhost:8080/iceberg", + warehouse="prod", + namespace="ml", + table="features", + timestamp_field="ts", + created_timestamp_column="created", + field_mapping={"src_col": "dst_col"}, + description="Test source", + tags={"team": "ml"}, + owner="owner@example.com", + token_env_var="TOKEN", + credential_vending=False, + ) + proto = source.to_proto() + assert proto.type == DataSourceProto.BATCH_ICEBERG + assert ( + "iceberg_catalog.iceberg_source.IcebergSource" + in proto.data_source_class_type + ) + + config = json.loads(str(proto.custom_options.configuration, encoding="utf8")) + assert config["endpoint"] == "http://localhost:8080/iceberg" + assert config["warehouse"] == "prod" + assert config["namespace"] == "ml" + assert config["table"] == "features" + assert config["token_env_var"] == "TOKEN" + assert config["credential_vending"] is False + assert config["catalog_type"] == "rest" + + restored = IcebergSource.from_proto(proto) + assert restored == source + assert restored.fqn == source.fqn + assert restored.credential_vending == source.credential_vending + + def test_proto_roundtrip_with_catalog_properties(self): + source = IcebergSource( + catalog_type="hive", + warehouse="wh", + namespace="ns", + table="t", + catalog_properties={"uri": "thrift://metastore:9083"}, + ) + proto = source.to_proto() + config = json.loads(str(proto.custom_options.configuration, encoding="utf8")) + assert config["catalog_type"] == "hive" + assert config["catalog_properties"] == {"uri": "thrift://metastore:9083"} + + restored = IcebergSource.from_proto(proto) + assert restored == source + assert restored.catalog_type == "hive" + assert restored.catalog_properties == {"uri": "thrift://metastore:9083"} + + def test_get_table_query_string(self): + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="catalog", + namespace="schema", + table="tbl", + ) + assert source.get_table_query_string() == "iceberg_tmp_tbl" + + @patch( + "feast.infra.data_sources.contrib.iceberg_catalog" + ".iceberg_source.IcebergSource.get_catalog_client" + ) + def test_get_table_column_names_and_types(self, mock_client): + mock_instance = MagicMock() + mock_instance.load_table.return_value = MagicMock( + schema_fields=[ + {"name": "id", "type": "long"}, + {"name": "name", "type": "string"}, + {"name": "score", "type": "double"}, + ] + ) + mock_client.return_value = mock_instance + + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + columns = list(source.get_table_column_names_and_types(config=MagicMock())) + assert columns == [ + ("id", "long"), + ("name", "string"), + ("score", "double"), + ] + + @patch( + "feast.infra.data_sources.contrib.iceberg_catalog" + ".iceberg_source.IcebergSource.get_catalog_client" + ) + def test_validate_success(self, mock_client): + mock_instance = MagicMock() + mock_instance.load_table.return_value = MagicMock(location="s3://bucket/path") + mock_client.return_value = mock_instance + + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + source.validate(config=MagicMock()) + + @patch( + "feast.infra.data_sources.contrib.iceberg_catalog" + ".iceberg_source.IcebergSource.get_catalog_client" + ) + def test_validate_failure_not_found(self, mock_client): + mock_instance = MagicMock() + mock_instance.load_table.side_effect = TableNotFoundError( + "Table not found", status_code=404 + ) + mock_client.return_value = mock_instance + + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + with pytest.raises(ValueError, match="not found"): + source.validate(config=MagicMock()) + + @patch( + "feast.infra.data_sources.contrib.iceberg_catalog" + ".iceberg_source.IcebergSource.get_catalog_client" + ) + def test_validate_failure_auth(self, mock_client): + mock_instance = MagicMock() + mock_instance.load_table.side_effect = CatalogAuthError( + "Unauthorized", status_code=401 + ) + mock_client.return_value = mock_instance + + source = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + with pytest.raises(ValueError, match="Authentication failed"): + source.validate(config=MagicMock()) + + def test_equality(self): + s1 = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + s2 = IcebergSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + assert s1 == s2 + + s3 = IcebergSource( + endpoint="http://other/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + assert s1 != s3 + + def test_equality_different_catalog_type(self): + s1 = IcebergSource( + catalog_type="rest", + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + s2 = IcebergSource( + catalog_type="hive", + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + assert s1 != s2 + + +class TestUnityCatalogSource: + @patch.dict("os.environ", {"DATABRICKS_HOST": "http://my-workspace"}) + def test_default_endpoint(self): + source = UnityCatalogSource( + warehouse="prod", + namespace="ml", + table="features", + ) + assert source.endpoint == "http://my-workspace/api/2.1/unity-catalog/iceberg" + assert source.token_env_var == "DATABRICKS_TOKEN" + + def test_explicit_endpoint(self): + source = UnityCatalogSource( + endpoint="http://custom:8080/iceberg", + warehouse="prod", + namespace="ml", + table="features", + token_env_var="CUSTOM_TOKEN", + ) + assert source.endpoint == "http://custom:8080/iceberg" + assert source.token_env_var == "CUSTOM_TOKEN" + + def test_governance_flags(self): + source = UnityCatalogSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + register_as_feature_table=False, + sync_lineage=False, + ) + assert source.register_as_feature_table is False + assert source.sync_lineage is False + + def test_is_subclass_of_iceberg_source(self): + source = UnityCatalogSource( + endpoint="http://host/iceberg", + warehouse="w", + namespace="ns", + table="t", + ) + assert isinstance(source, IcebergSource) + + def test_proto_roundtrip(self): + source = UnityCatalogSource( + endpoint="http://host:8080/iceberg", + warehouse="catalog", + namespace="schema", + table="my_table", + timestamp_field="ts", + register_as_feature_table=True, + sync_lineage=False, + ) + proto = source.to_proto() + assert ( + "iceberg_catalog.unity_catalog_source.UnityCatalogSource" + in proto.data_source_class_type + ) + + config = json.loads(str(proto.custom_options.configuration, encoding="utf8")) + assert config["register_as_feature_table"] is True + assert config["sync_lineage"] is False + + restored = UnityCatalogSource.from_proto(proto) + assert restored == source + assert restored.register_as_feature_table is True + assert restored.sync_lineage is False + + +class TestIcebergTypeMapping: + @pytest.mark.parametrize( + "iceberg_type,expected", + [ + ("boolean", ValueType.BOOL), + ("int", ValueType.INT32), + ("long", ValueType.INT64), + ("float", ValueType.FLOAT), + ("double", ValueType.DOUBLE), + ("string", ValueType.STRING), + ("binary", ValueType.BYTES), + ("timestamp", ValueType.INT64), + ("timestamptz", ValueType.INT64), + ("decimal(10,2)", ValueType.DOUBLE), + ("uuid", ValueType.STRING), + ("unknown_type", ValueType.UNKNOWN), + ], + ) + def test_type_mapping(self, iceberg_type, expected): + assert _iceberg_type_to_feast_value_type(iceberg_type) == expected + + +class TestIcebergRestClient: + """Tests for IcebergRestClient: URL construction, error handling, pagination, schema resolution.""" + + def test_url_style_auto_unity(self): + client = IcebergRestClient( + endpoint="https://host/api/2.1/unity-catalog/iceberg", + warehouse="my_catalog", + ) + assert client.url_style == "unity" + assert client._base_url == ( + "https://host/api/2.1/unity-catalog/iceberg/v1/catalogs/my_catalog" + ) + + def test_url_style_auto_standard(self): + client = IcebergRestClient( + endpoint="https://polaris.example.com", + warehouse="my_catalog", + ) + assert client.url_style == "prefix" + assert client._base_url == ("https://polaris.example.com/v1/my_catalog") + + def test_url_style_no_warehouse(self): + client = IcebergRestClient(endpoint="https://nessie.example.com") + assert client._base_url == "https://nessie.example.com/v1" + + def test_url_style_explicit_unity(self): + client = IcebergRestClient( + endpoint="https://custom.host", + warehouse="w", + url_style="unity", + ) + assert client._base_url == "https://custom.host/v1/catalogs/w" + + def test_url_style_explicit_prefix(self): + client = IcebergRestClient( + endpoint="https://custom.host", + warehouse="w", + url_style="prefix", + ) + assert client._base_url == "https://custom.host/v1/w" + + def test_session_headers_with_token(self): + client = IcebergRestClient( + endpoint="https://host", + token="my-token", + ) + assert client._session.headers["Authorization"] == "Bearer my-token" + assert "Content-Type" in client._session.headers + + def test_session_headers_without_token(self): + client = IcebergRestClient( + endpoint="https://host", + credential_vending=False, + ) + assert "Authorization" not in client._session.headers + assert "X-Iceberg-Access-Delegation" not in client._session.headers + + @patch("requests.Session.get") + def test_load_table_404_raises_not_found(self, mock_get): + resp = Mock() + resp.ok = False + resp.status_code = 404 + resp.json.return_value = {"message": "Table not found"} + resp.text = "Table not found" + mock_get.return_value = resp + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + with pytest.raises(TableNotFoundError) as exc_info: + client.load_table("ns", "missing_table") + assert exc_info.value.status_code == 404 + + @patch("requests.Session.get") + def test_load_table_401_raises_auth_error(self, mock_get): + resp = Mock() + resp.ok = False + resp.status_code = 401 + resp.json.return_value = {"error": "Invalid token"} + resp.text = "Unauthorized" + mock_get.return_value = resp + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + with pytest.raises(CatalogAuthError) as exc_info: + client.load_table("ns", "t") + assert exc_info.value.status_code == 401 + + @patch("requests.Session.get") + def test_load_table_403_raises_auth_error(self, mock_get): + resp = Mock() + resp.ok = False + resp.status_code = 403 + resp.json.return_value = {"message": "Forbidden"} + resp.text = "Forbidden" + mock_get.return_value = resp + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + with pytest.raises(CatalogAuthError) as exc_info: + client.load_table("ns", "t") + assert exc_info.value.status_code == 403 + + @patch("requests.Session.get") + def test_load_table_500_raises_catalog_error(self, mock_get): + resp = Mock() + resp.ok = False + resp.status_code = 500 + resp.json.return_value = {"message": "Internal error"} + resp.text = "Internal Server Error" + mock_get.return_value = resp + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + with pytest.raises(IcebergCatalogError) as exc_info: + client.load_table("ns", "t") + assert exc_info.value.status_code == 500 + + @patch("requests.Session.get") + def test_load_table_connection_error(self, mock_get): + mock_get.side_effect = requests.exceptions.ConnectionError("refused") + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + with pytest.raises(IcebergCatalogError, match="Connection failed"): + client.load_table("ns", "t") + + @patch("requests.Session.get") + def test_load_table_timeout(self, mock_get): + mock_get.side_effect = requests.exceptions.Timeout("timed out") + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + with pytest.raises(IcebergCatalogError, match="timed out"): + client.load_table("ns", "t") + + @patch("requests.Session.get") + def test_load_table_success(self, mock_get): + resp = Mock() + resp.ok = True + resp.json.return_value = { + "metadata": { + "table-uuid": "abc-123", + "location": "s3://bucket/path", + "format-version": 2, + "schemas": [ + { + "schema-id": 0, + "fields": [ + {"name": "id", "type": "long"}, + {"name": "name", "type": "string"}, + ], + } + ], + "current-schema-id": 0, + "properties": {"owner": "test"}, + } + } + mock_get.return_value = resp + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + metadata = client.load_table("ns", "t") + assert metadata.table_id == "abc-123" + assert metadata.location == "s3://bucket/path" + assert len(metadata.schema_fields) == 2 + assert metadata.schema_fields[0] == {"name": "id", "type": "long"} + + @patch("requests.Session.get") + def test_schema_resolution_uses_current_schema_id(self, mock_get): + """Verifies that current-schema-id is used, not schemas[-1].""" + resp = Mock() + resp.ok = True + resp.json.return_value = { + "metadata": { + "table-uuid": "abc", + "location": "s3://b/p", + "schemas": [ + { + "schema-id": 0, + "fields": [{"name": "old_col", "type": "string"}], + }, + { + "schema-id": 1, + "fields": [{"name": "new_col", "type": "long"}], + }, + { + "schema-id": 2, + "fields": [{"name": "newest_col", "type": "double"}], + }, + ], + "current-schema-id": 1, + } + } + mock_get.return_value = resp + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + metadata = client.load_table("ns", "t") + assert len(metadata.schema_fields) == 1 + assert metadata.schema_fields[0]["name"] == "new_col" + + @patch("requests.Session.get") + def test_list_tables_with_pagination(self, mock_get): + resp1 = Mock() + resp1.ok = True + resp1.json.return_value = { + "identifiers": [{"namespace": ["ns"], "name": "t1"}], + "next-page-token": "page2", + } + resp2 = Mock() + resp2.ok = True + resp2.json.return_value = { + "identifiers": [{"namespace": ["ns"], "name": "t2"}], + } + mock_get.side_effect = [resp1, resp2] + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + tables = client.list_tables("ns") + assert tables == ["t1", "t2"] + assert mock_get.call_count == 2 + + @patch("requests.Session.get") + def test_list_tables_auth_error(self, mock_get): + resp = Mock() + resp.ok = False + resp.status_code = 403 + resp.json.return_value = {"message": "Forbidden"} + resp.text = "Forbidden" + mock_get.return_value = resp + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + with pytest.raises(CatalogAuthError): + client.list_tables("ns") + + @patch("requests.Session.get") + def test_list_namespaces_with_pagination(self, mock_get): + resp1 = Mock() + resp1.ok = True + resp1.json.return_value = { + "namespaces": [["ns1"]], + "next-page-token": "page2", + } + resp2 = Mock() + resp2.ok = True + resp2.json.return_value = { + "namespaces": [["ns2"], ["ns3"]], + } + mock_get.side_effect = [resp1, resp2] + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + namespaces = client.list_namespaces() + assert namespaces == ["ns1", "ns2", "ns3"] + + @patch("requests.Session.get") + def test_credential_vending_s3(self, mock_get): + resp = Mock() + resp.ok = True + resp.json.return_value = { + "config": { + "s3.access-key-id": "AKID", + "s3.secret-access-key": "secret", + "s3.session-token": "tok", + "s3.endpoint": "https://s3.amazonaws.com", + } + } + mock_get.return_value = resp + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + cred = client.vend_credentials("ns", "t") + assert cred is not None + assert cred.access_key == "AKID" + assert cred.credential_type == "s3" + + @patch("requests.Session.get") + def test_credential_vending_gcs(self, mock_get): + resp = Mock() + resp.ok = True + resp.json.return_value = { + "config": { + "gcs.oauth2.token": "gcs-token", + "gcs.oauth2.token-expires-at": 1700000000, + } + } + mock_get.return_value = resp + + client = IcebergRestClient(endpoint="https://host", warehouse="w") + cred = client.vend_credentials("ns", "t") + assert cred is not None + assert cred.access_key == "gcs-token" + assert cred.credential_type == "gcs" + + def test_credential_vending_disabled(self): + client = IcebergRestClient( + endpoint="https://host", + warehouse="w", + credential_vending=False, + ) + result = client.vend_credentials("ns", "t") + assert result is None diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/test_iceberg_spark.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/test_iceberg_spark.py new file mode 100644 index 00000000000..6c4c186830d --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/tests/test_iceberg_spark.py @@ -0,0 +1,122 @@ +"""Unit tests for Spark offline store reading from IcebergSource.""" + +from datetime import datetime, timezone +from unittest.mock import MagicMock, patch + +import pyarrow as pa +import pytest + +from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, +) + +pyspark = pytest.importorskip("pyspark") + +from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( # noqa: E402 + _pull_from_iceberg_source, + _register_iceberg_source_as_temp_view, +) + + +@pytest.fixture(scope="module") +def spark_session(): + from pyspark.sql import SparkSession + + spark = ( + SparkSession.builder.master("local[1]") + .appName("feast_iceberg_test") + .config("spark.ui.enabled", "false") + .config("spark.driver.bindAddress", "127.0.0.1") + .getOrCreate() + ) + yield spark + spark.stop() + + +class TestRegisterIcebergSourceAsTempView: + @patch( + "feast.infra.offline_stores.contrib.spark_offline_store" + ".spark._load_pyiceberg_table" + ) + def test_pyiceberg_scan(self, mock_load, spark_session): + """Verify data is loaded via PyIceberg scan -> Arrow -> Spark DataFrame.""" + arrow_tbl = pa.table({"id": [1, 2, 3], "value": [10.0, 20.0, 30.0]}) + + mock_iceberg_table = MagicMock() + mock_iceberg_table.scan.return_value.to_arrow.return_value = arrow_tbl + mock_load.return_value = mock_iceberg_table + + source = IcebergSource( + endpoint="http://host:8080/iceberg", + warehouse="w", + namespace="ns", + table="my_table", + ) + config = MagicMock() + + _register_iceberg_source_as_temp_view(spark_session, source, config) + + result = spark_session.sql("SELECT * FROM iceberg_tmp_my_table") + assert result.count() == 3 + mock_load.assert_called_once_with(source) + + @patch( + "feast.infra.offline_stores.contrib.spark_offline_store" + ".spark._load_pyiceberg_table" + ) + def test_catalog_load_failure_propagates(self, mock_load, spark_session): + """Verify errors propagate instead of being silently swallowed.""" + mock_load.side_effect = Exception("table not found") + + source = IcebergSource( + endpoint="http://host:8080/iceberg", + warehouse="w", + namespace="ns", + table="bad_table", + ) + config = MagicMock() + + with pytest.raises(Exception, match="table not found"): + _register_iceberg_source_as_temp_view(spark_session, source, config) + + +class TestPullFromIcebergSource: + @patch( + "feast.infra.offline_stores.contrib.spark_offline_store.spark" + "._register_iceberg_source_as_temp_view" + ) + def test_pull_with_time_filter(self, mock_register, spark_session): + """Verify _pull_from_iceberg_source creates proper time-filtered query.""" + test_df = spark_session.createDataFrame( + [ + (1, 0.9, datetime(2023, 1, 1, tzinfo=timezone.utc)), + (2, 0.8, datetime(2023, 6, 15, tzinfo=timezone.utc)), + (3, 0.7, datetime(2024, 1, 1, tzinfo=timezone.utc)), + ], + ["driver_id", "conv_rate", "event_timestamp"], + ) + test_df.createOrReplaceTempView("iceberg_tmp_driver_stats") + + source = IcebergSource( + endpoint="http://host:8080/iceberg", + warehouse="w", + namespace="ns", + table="driver_stats", + ) + config = MagicMock() + + job = _pull_from_iceberg_source( + spark_session=spark_session, + data_source=source, + join_key_columns=["driver_id"], + feature_name_columns=["conv_rate"], + timestamp_field="event_timestamp", + created_timestamp_column=None, + start_date=datetime(2023, 1, 1, tzinfo=timezone.utc), + end_date=datetime(2023, 12, 31, tzinfo=timezone.utc), + config=config, + ) + + result_df = job.to_df() + assert len(result_df) == 2 + assert set(result_df["driver_id"].tolist()) == {1, 2} diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/uc_client.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/uc_client.py new file mode 100644 index 00000000000..14441058305 --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/uc_client.py @@ -0,0 +1,230 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import Any, Dict, List, Optional + +import requests + +from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_rest_client import ( + IcebergRestClient, +) + +logger = logging.getLogger(__name__) + + +class UCClient(IcebergRestClient): + """Unity Catalog client extending IcebergRestClient with governance APIs. + + Provides UC-specific operations that go beyond the standard Iceberg REST + Catalog specification: + - Feature table registration (primary keys, schema, metadata) + - Lineage recording + - Table properties management + + Standard Iceberg REST operations (load_table, vend_credentials, etc.) + are inherited from IcebergRestClient. + """ + + @property + def _uc_base_url(self) -> str: + """Base URL for UC-specific REST APIs (non-Iceberg standard).""" + return self.endpoint + + def register_feature_table( + self, + fqn: str, + primary_keys: List[str], + columns: List[Dict[str, str]], + properties: Optional[Dict[str, str]] = None, + ) -> bool: + """Register or update a feature table in Unity Catalog. + + Maps Feast FeatureView metadata to UC table properties including + primary key constraints and feature-store-specific annotations. + + Args: + fqn: Fully qualified table name (catalog.schema.table). + primary_keys: Column names that form the primary key (entity join keys). + columns: List of column definitions [{"name": ..., "type": ...}]. + properties: Additional table properties to set. + + Returns: + True if registration succeeded, False otherwise. + """ + url = f"{self._uc_base_url}/tables/{fqn}" + payload: Dict[str, Any] = { + "properties": { + **(properties or {}), + "feast.primary_keys": ",".join(primary_keys), + "feast.is_feature_table": "true", + }, + } + + try: + response = requests.patch( + url, json=payload, headers=self.headers, timeout=self.timeout + ) + if response.status_code == 404: + logger.info(f"Table {fqn} not found for update, attempting creation") + return self._create_feature_table( + fqn, primary_keys, columns, properties + ) + response.raise_for_status() + logger.info(f"Updated UC feature table: {fqn}") + return True + except requests.exceptions.RequestException as e: + logger.error(f"Failed to register feature table {fqn}: {e}") + return False + + def _create_feature_table( + self, + fqn: str, + primary_keys: List[str], + columns: List[Dict[str, str]], + properties: Optional[Dict[str, str]] = None, + ) -> bool: + """Create a new feature table in UC if it doesn't exist.""" + parts = fqn.split(".") + if len(parts) != 3: + logger.error(f"Invalid FQN format: {fqn}. Expected catalog.schema.table") + return False + + catalog_name, schema_name, table_name = parts + url = f"{self._uc_base_url}/tables" + payload: Dict[str, Any] = { + "name": table_name, + "catalog_name": catalog_name, + "schema_name": schema_name, + "table_type": "MANAGED", + "data_source_format": "ICEBERG", + "columns": [ + { + "name": col["name"], + "type_name": col.get("type", "STRING"), + "position": idx, + "nullable": col["name"] not in primary_keys, + } + for idx, col in enumerate(columns) + ], + "properties": { + **(properties or {}), + "feast.primary_keys": ",".join(primary_keys), + "feast.is_feature_table": "true", + }, + } + + try: + response = requests.post( + url, json=payload, headers=self.headers, timeout=self.timeout + ) + response.raise_for_status() + logger.info(f"Created UC feature table: {fqn}") + return True + except requests.exceptions.RequestException as e: + logger.error(f"Failed to create feature table {fqn}: {e}") + return False + + def record_lineage( + self, + target_fqn: str, + source_fqns: List[str], + ) -> bool: + """Record lineage relationships in Unity Catalog. + + Links source tables to the target feature table for governance tracking. + + Args: + target_fqn: The downstream feature table FQN. + source_fqns: List of upstream source table FQNs. + + Returns: + True if lineage was recorded, False otherwise. + """ + url = f"{self._uc_base_url}/lineage/table-lineage" + payload = { + "table_name": target_fqn, + "upstream_tables": [{"table_name": src} for src in source_fqns], + } + + try: + response = requests.post( + url, json=payload, headers=self.headers, timeout=self.timeout + ) + response.raise_for_status() + logger.info(f"Recorded lineage for {target_fqn}: {source_fqns}") + return True + except requests.exceptions.RequestException as e: + logger.warning(f"Failed to record lineage for {target_fqn}: {e}") + return False + + def set_table_properties( + self, + fqn: str, + properties: Dict[str, str], + ) -> bool: + """Set arbitrary properties on a UC table. + + Used to sync Feast metadata (project, feature_view name, owner, tags) + into UC table properties for discoverability. + + Args: + fqn: Fully qualified table name. + properties: Key-value properties to set. + + Returns: + True if properties were set, False otherwise. + """ + url = f"{self._uc_base_url}/tables/{fqn}" + payload = {"properties": properties} + + try: + response = requests.patch( + url, json=payload, headers=self.headers, timeout=self.timeout + ) + response.raise_for_status() + logger.info(f"Set properties on {fqn}: {list(properties.keys())}") + return True + except requests.exceptions.RequestException as e: + logger.error(f"Failed to set properties on {fqn}: {e}") + return False + + def get_feature_tables(self, namespace: str) -> List[Dict[str, Any]]: + """List tables in a namespace that are marked as feature tables. + + Useful for the import-uc-table CLI to discover existing feature tables. + + Args: + namespace: The UC schema to scan. + + Returns: + List of table metadata dicts for tables with feast.is_feature_table=true. + """ + tables = self.list_tables(namespace) + feature_tables = [] + + for table_name in tables: + fqn = f"{self.warehouse}.{namespace}.{table_name}" + url = f"{self._uc_base_url}/tables/{fqn}" + try: + response = requests.get(url, headers=self.headers, timeout=self.timeout) + response.raise_for_status() + data = response.json() + props = data.get("properties", {}) + if props.get("feast.is_feature_table") == "true": + feature_tables.append(data) + except requests.exceptions.RequestException: + continue + + return feature_tables diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/uc_provider.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/uc_provider.py new file mode 100644 index 00000000000..95a9a5fb58c --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/uc_provider.py @@ -0,0 +1,165 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unity Catalog Provider — Feast lifecycle governance hooks. + +Detects UnityCatalogSource in feature views during ``feast apply`` and: +1. Registers/updates feature tables in Unity Catalog +2. Syncs lineage from source tables to feature tables +3. Sets Feast metadata as UC table properties + +Usage in feature_store.yaml: + provider: feast.infra.data_sources.contrib.iceberg_catalog.uc_provider.UnityCatalogProvider +""" + +import logging +from datetime import datetime, timezone +from typing import Any, Dict, List, Sequence + +from feast.base_feature_view import BaseFeatureView +from feast.entity import Entity +from feast.feature_view import FeatureView +from feast.infra.data_sources.contrib.iceberg_catalog.unity_catalog_source import ( + UnityCatalogSource, +) +from feast.infra.passthrough_provider import PassthroughProvider + +logger = logging.getLogger(__name__) + + +class UnityCatalogProvider(PassthroughProvider): + """Provider that hooks into Feast lifecycle to sync with Unity Catalog. + + Extends PassthroughProvider to detect UnityCatalogSource in feature views + and trigger UC-specific governance operations during feast apply/teardown. + Standard offline/online store operations are delegated to the configured stores. + """ + + def update_infra( + self, + project: str, + tables_to_delete: Sequence[BaseFeatureView], + tables_to_keep: Sequence[BaseFeatureView], + entities_to_delete: Sequence[Entity], + entities_to_keep: Sequence[Entity], + partial: bool, + ): + super().update_infra( + project, + tables_to_delete, + tables_to_keep, + entities_to_delete, + entities_to_keep, + partial, + ) + + self._register_uc_feature_tables(project, tables_to_keep) + self._cleanup_uc_feature_tables(project, tables_to_delete) + + def _register_uc_feature_tables( + self, project: str, feature_views: Sequence[BaseFeatureView] + ): + for fv in feature_views: + if not isinstance(fv, FeatureView): + continue + source = fv.batch_source + if not isinstance(source, UnityCatalogSource): + continue + if not source.register_as_feature_table: + continue + + logger.info(f"Registering UC feature table for: {fv.name} -> {source.fqn}") + + uc = source.get_uc_client() + + entity_columns = [] + if hasattr(fv, "entity_columns"): + entity_columns = [col.name for col in fv.entity_columns] + elif hasattr(fv, "entities"): + entity_columns = list(fv.entities) + + columns = [] + if hasattr(fv, "schema") and fv.schema: + columns = [ + {"name": field.name, "type": str(field.dtype)} + for field in fv.schema + ] + + properties: Dict[str, str] = { + "feast.project": project, + "feast.feature_view": fv.name, + } + if fv.owner: + properties["feast.owner"] = fv.owner + if fv.description: + properties["feast.description"] = fv.description + if fv.tags: + for k, v in fv.tags.items(): + properties[f"feast.tag.{k}"] = v + + success = uc.register_feature_table( + fqn=source.fqn, + primary_keys=entity_columns, + columns=columns, + properties=properties, + ) + + if success and source.sync_lineage: + self._sync_lineage(uc, source, fv) + + def _sync_lineage(self, uc: Any, source: UnityCatalogSource, fv: FeatureView): + source_fqns: List[str] = [] + + if hasattr(fv, "stream_source") and fv.stream_source: + if isinstance(fv.stream_source, UnityCatalogSource): + stream_fqn = fv.stream_source.fqn + if stream_fqn != source.fqn: + source_fqns.append(stream_fqn) + + for input_fv in getattr(fv, "source_feature_view_projections", {}).values(): + input_source = getattr(input_fv, "batch_source", None) + if isinstance(input_source, UnityCatalogSource): + if input_source.fqn != source.fqn: + source_fqns.append(input_source.fqn) + + if source_fqns: + uc.record_lineage( + target_fqn=source.fqn, + source_fqns=source_fqns, + ) + + def _cleanup_uc_feature_tables( + self, project: str, feature_views: Sequence[BaseFeatureView] + ): + for fv in feature_views: + if not isinstance(fv, FeatureView): + continue + source = fv.batch_source + if not isinstance(source, UnityCatalogSource): + continue + if not source.register_as_feature_table: + continue + + logger.info( + f"Feature view {fv.name} deleted. UC table {source.fqn} metadata " + f"will be preserved (table not dropped)." + ) + uc = source.get_uc_client() + uc.set_table_properties( + fqn=source.fqn, + properties={ + "feast.is_feature_table": "false", + "feast.deleted_at": datetime.now(timezone.utc).isoformat(), + }, + ) diff --git a/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/unity_catalog_source.py b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/unity_catalog_source.py new file mode 100644 index 00000000000..f286c6b8d81 --- /dev/null +++ b/sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/unity_catalog_source.py @@ -0,0 +1,188 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from typing import Any, Dict, Optional + +from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, +) +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto + + +class UnityCatalogSource(IcebergSource): + """Data source backed by Databricks Unity Catalog. + + Extends IcebergSource with UC-specific governance capabilities: + - Feature table registration on ``feast apply`` + - Lineage sync between Feast feature views and UC lineage graph + - UC table properties sync (feast.project, feast.feature_view, etc.) + + Connection defaults to DATABRICKS_HOST and DATABRICKS_TOKEN env vars. + """ + + def __init__( + self, + *, + warehouse: str, + namespace: str, + table: str, + name: Optional[str] = None, + timestamp_field: Optional[str] = None, + created_timestamp_column: Optional[str] = None, + field_mapping: Optional[Dict[str, str]] = None, + description: Optional[str] = "", + tags: Optional[Dict[str, str]] = None, + owner: Optional[str] = "", + endpoint: Optional[str] = None, + token_env_var: Optional[str] = None, + credential_vending: bool = True, + catalog_type: str = "rest", + catalog_name: str = "feast_iceberg", + catalog_properties: Optional[Dict[str, str]] = None, + register_as_feature_table: bool = True, + sync_lineage: bool = True, + ): + if endpoint is None: + host = os.environ.get("DATABRICKS_HOST", "") + endpoint = f"{host}/api/2.1/unity-catalog/iceberg" + + if token_env_var is None: + token_env_var = "DATABRICKS_TOKEN" + + super().__init__( + endpoint=endpoint, + warehouse=warehouse, + namespace=namespace, + table=table, + catalog_type=catalog_type, + catalog_name=catalog_name, + catalog_properties=catalog_properties, + name=name, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + field_mapping=field_mapping, + description=description, + tags=tags, + owner=owner, + token_env_var=token_env_var, + credential_vending=credential_vending, + ) + self.register_as_feature_table = register_as_feature_table + self.sync_lineage = sync_lineage + + def get_uc_client(self): + """Create a UCClient for governance operations beyond standard Iceberg REST.""" + from feast.infra.data_sources.contrib.iceberg_catalog.uc_client import ( + UCClient, + ) + + uc_endpoint = self.endpoint + iceberg_suffix = "/api/2.1/unity-catalog/iceberg" + if uc_endpoint.endswith(iceberg_suffix): + uc_endpoint = uc_endpoint[: -len("/iceberg")] + elif uc_endpoint.endswith("/iceberg"): + uc_endpoint = uc_endpoint[: -len("/iceberg")] + + token = os.environ.get(self.token_env_var) if self.token_env_var else None + return UCClient( + endpoint=uc_endpoint, + token=token, + warehouse=self.warehouse, + ) + + @staticmethod + def from_proto(data_source: DataSourceProto) -> Any: + custom_options = json.loads( + str(data_source.custom_options.configuration, encoding="utf8") + ) + return UnityCatalogSource( + name=data_source.name, + endpoint=custom_options["endpoint"], + warehouse=custom_options["warehouse"], + namespace=custom_options["namespace"], + table=custom_options["table"], + token_env_var=custom_options.get("token_env_var"), + credential_vending=custom_options.get("credential_vending", True), + catalog_type=custom_options.get("catalog_type", "rest"), + catalog_name=custom_options.get("catalog_name", "feast_iceberg"), + catalog_properties=custom_options.get("catalog_properties"), + register_as_feature_table=custom_options.get( + "register_as_feature_table", True + ), + sync_lineage=custom_options.get("sync_lineage", True), + timestamp_field=data_source.timestamp_field, + created_timestamp_column=data_source.created_timestamp_column, + field_mapping=dict(data_source.field_mapping), + description=data_source.description, + tags=dict(data_source.tags), + owner=data_source.owner, + ) + + def _to_proto_impl(self) -> DataSourceProto: + config_json = json.dumps( + { + "endpoint": self.endpoint, + "warehouse": self.warehouse, + "namespace": self.namespace, + "table": self.iceberg_table, + "token_env_var": self.token_env_var, + "credential_vending": self.credential_vending, + "catalog_type": self.catalog_type, + "catalog_name": self.catalog_name, + "catalog_properties": self.catalog_properties, + "register_as_feature_table": self.register_as_feature_table, + "sync_lineage": self.sync_lineage, + } + ) + data_source_proto = DataSourceProto( + name=self.name, + type=DataSourceProto.BATCH_ICEBERG, + data_source_class_type=( + "feast.infra.data_sources.contrib.iceberg_catalog" + ".unity_catalog_source.UnityCatalogSource" + ), + custom_options=DataSourceProto.CustomSourceOptions( + configuration=bytes(config_json, encoding="utf8") + ), + description=self.description, + tags=self.tags, + owner=self.owner, + timestamp_field=self.timestamp_field, + created_timestamp_column=self.created_timestamp_column, + field_mapping=self.field_mapping, + ) + return data_source_proto + + def __eq__(self, other): + if not isinstance(other, UnityCatalogSource): + return False + if not super().__eq__(other): + return False + return ( + self.register_as_feature_table == other.register_as_feature_table + and self.sync_lineage == other.sync_lineage + ) + + def __hash__(self): + return hash( + ( + self.name, + self.endpoint, + self.warehouse, + self.namespace, + self.iceberg_table, + ) + ) diff --git a/sdk/python/feast/infra/feature_servers/base_config.py b/sdk/python/feast/infra/feature_servers/base_config.py index 1a348032e17..14ad2fe505e 100644 --- a/sdk/python/feast/infra/feature_servers/base_config.py +++ b/sdk/python/feast/infra/feature_servers/base_config.py @@ -1,3 +1,16 @@ +# Copyright 2025 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from typing import Optional from pydantic import StrictBool, StrictInt @@ -24,11 +37,81 @@ class FeatureLoggingConfig(FeastConfigBaseModel): """Timeout for adding new log item to the queue.""" +class MetricsConfig(FeastConfigBaseModel): + """Prometheus metrics configuration. + + Follows the same pattern as ``FeatureLoggingConfig``: a single + ``enabled`` flag controls global on/off, and per-category booleans + allow fine-grained suppression. Can also be enabled at runtime via + the ``feast serve --metrics`` CLI flag — either option is sufficient. + """ + + enabled: StrictBool = False + """Whether Prometheus metrics collection and the metrics HTTP server + (default port 8000) should be enabled.""" + + resource: StrictBool = True + """Emit CPU and memory usage gauges (feast_feature_server_cpu_usage, + feast_feature_server_memory_usage).""" + + request: StrictBool = True + """Emit per-endpoint request counters and latency histograms + (feast_feature_server_request_total, + feast_feature_server_request_latency_seconds).""" + + online_features: StrictBool = True + """Emit online feature retrieval metrics + (feast_online_features_request_total, + feast_online_features_entity_count, + feast_feature_server_online_store_read_duration_seconds, + feast_feature_server_transformation_duration_seconds, + feast_feature_server_write_transformation_duration_seconds). + ODFV transformation metrics additionally require track_metrics=True + on the OnDemandFeatureView definition.""" + + push: StrictBool = True + """Emit push/write request counters + (feast_push_request_total).""" + + materialization: StrictBool = True + """Emit materialization success/failure counters and duration histograms + (feast_materialization_result_total, + feast_materialization_duration_seconds).""" + + freshness: StrictBool = True + """Emit per-feature-view freshness gauges + (feast_feature_freshness_seconds).""" + + offline_features: StrictBool = True + """Emit offline store retrieval metrics + (feast_offline_store_request_total, + feast_offline_store_request_latency_seconds, + feast_offline_store_row_count).""" + + audit_logging: StrictBool = False + """Emit structured JSON audit log entries for online and offline + feature requests via the ``feast.audit`` logger. Captures requestor + identity, entity keys, feature views, row counts, and latency.""" + + class BaseFeatureServerConfig(FeastConfigBaseModel): """Base Feature Server config that should be extended""" enabled: StrictBool = False """Whether the feature server should be launched.""" + metrics: Optional[MetricsConfig] = None + """Prometheus metrics configuration. Set ``metrics.enabled: true`` or + pass the ``feast serve --metrics`` CLI flag to activate.""" + feature_logging: Optional[FeatureLoggingConfig] = None """ Feature logging configuration """ + + offline_push_batching_enabled: Optional[StrictBool] = None + """Whether to batch writes to the offline store via the `/push` endpoint.""" + + offline_push_batching_batch_size: Optional[StrictInt] = None + """The maximum batch size for offline writes via `/push`.""" + + offline_push_batching_batch_interval_seconds: Optional[StrictInt] = None + """The batch interval between offline writes via `/push`.""" diff --git a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile index c98b358f079..6eb4cc96418 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile +++ b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile @@ -1,7 +1,13 @@ -FROM registry.access.redhat.com/ubi9/python-311:1 +FROM registry.access.redhat.com/ubi9/python-312-minimal:latest +USER 0 +RUN microdnf install -y git gcc libpq-devel python3.12-devel && microdnf clean all +USER 1001 + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +ENV UV_CACHE_DIR=/tmp/uv-cache COPY requirements.txt requirements.txt -RUN pip install -r requirements.txt +RUN uv pip install -r requirements.txt # modify permissions to support running with a random uid RUN chmod g+w $(python -c "import feast.ui as ui; print(ui.__path__)" | tr -d "[']")/build/projects-list.json diff --git a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev index 9aff4b52b16..faf49c20903 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev +++ b/sdk/python/feast/infra/feature_servers/multicloud/Dockerfile.dev @@ -1,14 +1,15 @@ -FROM registry.access.redhat.com/ubi9/python-311:1 +FROM registry.access.redhat.com/ubi9/python-312-minimal:latest USER 0 +RUN microdnf install -y npm git gcc libpq-devel python3.12-devel && microdnf clean all RUN npm install -g yarn yalc && rm -rf .npm -USER default +USER 1001 -COPY --chown=default .git ${APP_ROOT}/src/.git -COPY --chown=default setup.py pyproject.toml README.md Makefile ${APP_ROOT}/src/ -COPY --chown=default protos ${APP_ROOT}/src/protos -COPY --chown=default ui ${APP_ROOT}/src/ui -COPY --chown=default sdk/python ${APP_ROOT}/src/sdk/python +COPY --chown=1001:0 .git ${APP_ROOT}/src/.git +COPY --chown=1001:0 pyproject.toml README.md Makefile ${APP_ROOT}/src/ +COPY --chown=1001:0 protos ${APP_ROOT}/src/protos +COPY --chown=1001:0 ui ${APP_ROOT}/src/ui +COPY --chown=1001:0 sdk/python ${APP_ROOT}/src/sdk/python WORKDIR ${APP_ROOT}/src/ui RUN npm install && \ @@ -26,8 +27,10 @@ RUN yalc add @feast-dev/feast-ui && \ yarn cache clean --all WORKDIR ${APP_ROOT}/src -RUN pip install --require-hashes --no-deps -r sdk/python/requirements/py3.11-minimal-requirements.txt -RUN pip install --no-deps -e .[minimal] +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +ENV UV_CACHE_DIR=/tmp/uv-cache +RUN uv pip install --require-hashes --no-deps -r sdk/python/requirements/py3.12-minimal-requirements.txt +RUN uv pip install --no-deps -e .[minimal] # modify permissions to support running with a random uid RUN chmod g+w $(python -c "import feast.ui as ui; print(ui.__path__)" | tr -d "[']")/build/projects-list.json diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.binary b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.binary index f5da258fbd2..55e3f50c17c 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.binary +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.binary @@ -2,11 +2,11 @@ FROM yarn-builder:latest # This section only necessary when building from local feast source ... e.g. ".[minimal]" ######################## -COPY --chown=default .git ${APP_ROOT}/src/.git -COPY --chown=default setup.py pyproject.toml README.md Makefile ${APP_ROOT}/src/ -COPY --chown=default protos ${APP_ROOT}/src/protos -COPY --chown=default ui ${APP_ROOT}/src/ui -COPY --chown=default sdk/python ${APP_ROOT}/src/sdk/python +COPY --chown=1001:0 .git ${APP_ROOT}/src/.git +COPY --chown=1001:0 pyproject.toml README.md Makefile ${APP_ROOT}/src/ +COPY --chown=1001:0 protos ${APP_ROOT}/src/protos +COPY --chown=1001:0 ui ${APP_ROOT}/src/ui +COPY --chown=1001:0 sdk/python ${APP_ROOT}/src/sdk/python WORKDIR ${APP_ROOT}/src/ui ENV NPM_TOKEN= diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.binary.release b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.binary.release index d490a4ae5bc..546188a0bff 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.binary.release +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.binary.release @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi9/python-311:1 +FROM registry.access.redhat.com/ubi9/python-312-minimal:latest COPY requirements.txt requirements.txt RUN source /tmp/hermeto.env && \ diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.builder.yarn b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.builder.yarn index 1cac1bc4d90..124134a4f0d 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.builder.yarn +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.builder.yarn @@ -1,5 +1,6 @@ -FROM registry.access.redhat.com/ubi9/python-311:1 +FROM registry.access.redhat.com/ubi9/python-312-minimal:latest USER 0 +RUN microdnf install -y npm git && microdnf clean all RUN npm install -g yarn yalc && rm -rf .npm USER 1001 diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.builder.yum b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.builder.yum index 0a19017f1d4..198d42ab5c2 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.builder.yum +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.builder.yum @@ -1,10 +1,10 @@ -FROM registry.access.redhat.com/ubi9/python-311:1 +FROM registry.access.redhat.com/ubi9/python-312-minimal:latest ARG RELEASE ENV IBIS_VERSION="9.5.0" USER 0 -RUN yum install -y ninja-build llvm-devel cmake llvm-toolset ncurses-devel rust cargo +RUN microdnf install -y ninja-build llvm-devel cmake llvm-toolset ncurses-devel rust cargo npm git && microdnf clean all RUN if [[ -z "$RELEASE" ]] ; then npm install -g yarn yalc && rm -rf .npm ; fi USER 1001 diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.sdist b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.sdist index 3c2c344dbf6..e6e32718667 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.sdist +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.sdist @@ -13,6 +13,7 @@ ENV THIRD_PARTY_PATH=/tmp/hermeto-generic-output/deps/generic # unecessary for konflux build ENV RPM_PATH=/tmp/hermeto-rpm-output/deps/rpm/x86_64 USER 0 +RUN microdnf install -y shadow-utils && microdnf clean all RUN useradd mockbuild RUN groupadd mock RUN usermod -G mock mockbuild @@ -71,7 +72,7 @@ RUN mkdir ${APP_ROOT}/src/arrow ${ARROW_HOME} ${APP_ROOT}/src/arrow-build && \ ARROW_ZLIB_URL="${THIRD_PARTY_PATH}/zlib-1.3.1.tar.gz" \ ARROW_ZSTD_URL="${THIRD_PARTY_PATH}/zstd-1.5.6.tar.gz" \ && \ - cmake \ + cmake -G Ninja \ -DCMAKE_INSTALL_PREFIX=$ARROW_HOME \ -DARROW_COMPUTE=ON \ -DARROW_ACERO=ON \ @@ -130,11 +131,11 @@ RUN source /tmp/hermeto.env && \ # This section only necessary when building from local feast source ... e.g. ".[minimal]" ######################## -COPY --chown=default .git ${APP_ROOT}/src/.git -COPY --chown=default setup.py pyproject.toml README.md Makefile ${APP_ROOT}/src/ -COPY --chown=default protos ${APP_ROOT}/src/protos -COPY --chown=default ui ${APP_ROOT}/src/ui -COPY --chown=default sdk/python ${APP_ROOT}/src/sdk/python +COPY --chown=1001:0 .git ${APP_ROOT}/src/.git +COPY --chown=1001:0 pyproject.toml README.md Makefile ${APP_ROOT}/src/ +COPY --chown=1001:0 protos ${APP_ROOT}/src/protos +COPY --chown=1001:0 ui ${APP_ROOT}/src/ui +COPY --chown=1001:0 sdk/python ${APP_ROOT}/src/sdk/python WORKDIR ${APP_ROOT}/src/ui ENV NPM_TOKEN= diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.sdist.release b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.sdist.release index 946b6407626..02977b5c0bd 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.sdist.release +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/Dockerfile.sdist.release @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi9/python-311:1 +FROM registry.access.redhat.com/ubi9/python-312-minimal:latest ENV APACHE_ARROW_VERSION="17.0.0" ENV MILVUS_LITE_VERSION="2.4.12" @@ -13,6 +13,7 @@ ENV THIRD_PARTY_PATH=/tmp/hermeto-generic-output/deps/generic # unecessary for konflux build ENV RPM_PATH=/tmp/hermeto-rpm-output/deps/rpm/x86_64 USER 0 +RUN microdnf install -y shadow-utils && microdnf clean all RUN useradd mockbuild RUN groupadd mock RUN usermod -G mock mockbuild @@ -71,7 +72,7 @@ RUN mkdir ${APP_ROOT}/src/arrow ${ARROW_HOME} ${APP_ROOT}/src/arrow-build && \ ARROW_ZLIB_URL="${THIRD_PARTY_PATH}/zlib-1.3.1.tar.gz" \ ARROW_ZSTD_URL="${THIRD_PARTY_PATH}/zstd-1.5.6.tar.gz" \ && \ - cmake \ + cmake -G Ninja \ -DCMAKE_INSTALL_PREFIX=$ARROW_HOME \ -DARROW_COMPUTE=ON \ -DARROW_ACERO=ON \ diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-binary-build.sh b/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-binary-build.sh index 8d8ca4ba5f1..7f2820057b0 100755 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-binary-build.sh +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-binary-build.sh @@ -44,9 +44,9 @@ hermeto fetch-deps \ "sdk/python/feast/infra/feature_servers/multicloud/requirements.txt" ], "requirements_build_files": [ -"sdk/python/requirements/py3.11-minimal-requirements.txt", -"sdk/python/requirements/py3.11-minimal-sdist-requirements.txt", -"sdk/python/requirements/py3.11-minimal-sdist-requirements-build.txt" +"sdk/python/requirements/py3.12-minimal-requirements.txt", +"sdk/python/requirements/py3.12-minimal-sdist-requirements.txt", +"sdk/python/requirements/py3.12-minimal-sdist-requirements-build.txt" ], "allow_binary": "true" }' diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-binary-release-build.sh b/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-binary-release-build.sh index 8462cef705b..4571324972a 100755 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-binary-release-build.sh +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-binary-release-build.sh @@ -19,9 +19,9 @@ hermeto fetch-deps \ "sdk/python/feast/infra/feature_servers/multicloud/requirements.txt" ], "requirements_build_files": [ -"sdk/python/requirements/py3.11-minimal-requirements.txt", -"sdk/python/requirements/py3.11-minimal-sdist-requirements.txt", -"sdk/python/requirements/py3.11-minimal-sdist-requirements-build.txt" +"sdk/python/requirements/py3.12-minimal-requirements.txt", +"sdk/python/requirements/py3.12-minimal-sdist-requirements.txt", +"sdk/python/requirements/py3.12-minimal-sdist-requirements-build.txt" ], "allow_binary": "true" }' diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-build.sh b/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-build.sh index c84774f497c..f66035e75c1 100755 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-build.sh +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-build.sh @@ -55,8 +55,8 @@ hermeto fetch-deps \ "sdk/python/feast/infra/feature_servers/multicloud/requirements.txt" ], "requirements_build_files": [ -"sdk/python/requirements/py3.11-minimal-sdist-requirements.txt", -"sdk/python/requirements/py3.11-minimal-sdist-requirements-build.txt" +"sdk/python/requirements/py3.12-minimal-sdist-requirements.txt", +"sdk/python/requirements/py3.12-minimal-sdist-requirements-build.txt" ], "allow_binary": "false" }' diff --git a/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-release-build.sh b/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-release-build.sh index 49df0eae600..0ea10d2f24e 100755 --- a/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-release-build.sh +++ b/sdk/python/feast/infra/feature_servers/multicloud/offline/offline-release-build.sh @@ -30,8 +30,8 @@ hermeto fetch-deps \ "sdk/python/feast/infra/feature_servers/multicloud/requirements.txt" ], "requirements_build_files": [ -"sdk/python/requirements/py3.11-minimal-sdist-requirements.txt", -"sdk/python/requirements/py3.11-minimal-sdist-requirements-build.txt" +"sdk/python/requirements/py3.12-minimal-sdist-requirements.txt", +"sdk/python/requirements/py3.12-minimal-sdist-requirements-build.txt" ], "allow_binary": "false" }' diff --git a/sdk/python/feast/infra/feature_servers/multicloud/requirements.txt b/sdk/python/feast/infra/feature_servers/multicloud/requirements.txt index 4b3c6f959f0..7b5ab254e03 100644 --- a/sdk/python/feast/infra/feature_servers/multicloud/requirements.txt +++ b/sdk/python/feast/infra/feature_servers/multicloud/requirements.txt @@ -1,2 +1,2 @@ # keep VERSION on line #2, this is critical to release CI -feast[minimal] == 0.56.0 +feast[minimal] == 0.65.0 diff --git a/sdk/python/feast/infra/key_encoding_utils.py b/sdk/python/feast/infra/key_encoding_utils.py index 4a3df1fc183..10a9934ad6a 100644 --- a/sdk/python/feast/infra/key_encoding_utils.py +++ b/sdk/python/feast/infra/key_encoding_utils.py @@ -57,15 +57,20 @@ def serialize_entity_key_prefix( This encoding is a partial implementation of serialize_entity_key, only operating on the keys of entities, and not the values. """ - sorted_keys = sorted(entity_keys) + # Fast path optimization for single entity + if len(entity_keys) == 1: + sorted_keys = [entity_keys[0]] + else: + sorted_keys = sorted(entity_keys) output: List[bytes] = [] if entity_key_serialization_version > 2: output.append(struct.pack(" 2: - output.append(struct.pack(" 2: output.append(struct.pack(" 2: - output.append(struct.pack(" 2: + output.append(struct.pack(" Optional["FastApiMCP"]: +class McpTransportNotSupportedError(RuntimeError): + pass + + +def _resolve_schema_references_safe( + schema_part: Dict[str, Any], + reference_schema: Dict[str, Any], + _seen_refs: Optional[Set[str]] = None, +) -> Dict[str, Any]: + """Resolve ``$ref`` pointers in an OpenAPI schema **without** infinite recursion. + + ``fastapi_mcp`` <=0.4.0 ships a ``resolve_schema_references`` helper that + inlines every ``$ref`` it encounters, but never tracks which refs have + already been visited. Feast's OpenAPI schema contains self-referential + types (protobuf ``Value`` -> ``Struct`` -> ``Value``), which causes a + ``RecursionError``. + + This replacement keeps a *seen-refs* set and replaces any circular + ``$ref`` with an empty object schema instead of recursing forever. + + Reference: ``fastapi_mcp/openapi/utils.py`` lines 19-55 + https://github.com/tadata-org/fastapi_mcp/blob/main/fastapi_mcp/openapi/utils.py#L19-L55 + """ + if _seen_refs is None: + _seen_refs = set() + + schema_part = schema_part.copy() + + if "$ref" in schema_part: + ref_path = schema_part["$ref"] + if ref_path in _seen_refs: + schema_part.pop("$ref") + schema_part.setdefault("type", "object") + return schema_part + if ref_path.startswith("#/components/schemas/"): + model_name = ref_path.split("/")[-1] + schemas = reference_schema.get("components", {}).get("schemas", {}) + if model_name in schemas: + _seen_refs = _seen_refs | {ref_path} + ref_schema = schemas[model_name].copy() + schema_part.pop("$ref") + schema_part.update(ref_schema) + + for key, value in schema_part.items(): + if isinstance(value, dict): + schema_part[key] = _resolve_schema_references_safe( + value, reference_schema, _seen_refs + ) + elif isinstance(value, list): + schema_part[key] = [ + _resolve_schema_references_safe(item, reference_schema, _seen_refs) + if isinstance(item, dict) + else item + for item in value + ] + + return schema_part + + +def _patch_fastapi_mcp_schema_resolver() -> None: + """Monkey-patch ``fastapi_mcp.openapi.utils.resolve_schema_references`` + with our circular-ref-safe version so that ``FastApiMCP`` can process + Feast's OpenAPI schema without hitting a ``RecursionError``.""" + try: + import fastapi_mcp.openapi.utils as _mcp_utils + + _mcp_utils.resolve_schema_references = _resolve_schema_references_safe # type: ignore[assignment] + except (ImportError, AttributeError): + pass + + +def add_mcp_support_to_app( + app, store: "FeatureStore", config +) -> Optional["FastApiMCP"]: """Add MCP support to the FastAPI app if enabled in configuration.""" if not MCP_AVAILABLE: logger.warning("MCP support requested but fastapi_mcp is not available") return None try: + _patch_fastapi_mcp_schema_resolver() + # Create MCP server from the FastAPI app mcp = FastApiMCP( app, @@ -40,8 +116,29 @@ def add_mcp_support_to_app(app, store: FeatureStore, config) -> Optional["FastAp description="Feast Feature Store MCP Server - Access feature store data and operations through MCP", ) - # Mount the MCP server to the FastAPI app - mcp.mount() + transport = getattr(config, "mcp_transport", "sse") + if transport == "http": + mount_http = getattr(mcp, "mount_http", None) + if mount_http is None: + raise McpTransportNotSupportedError( + "mcp_transport=http requires fastapi_mcp with FastApiMCP.mount_http(). " + "Upgrade fastapi_mcp (or install feast[mcp]) to a newer version." + ) + mount_http() + elif transport == "sse": + mount_sse = getattr(mcp, "mount_sse", None) + if mount_sse is not None: + mount_sse() + else: + logger.warning( + "transport sse not supported, fallback to the deprecated mount()." + ) + mcp.mount() + else: + # Defensive guard for programmatic callers. + raise McpTransportNotSupportedError( + f"Unsupported mcp_transport={transport!r}. Expected 'sse' or 'http'." + ) logger.info( "MCP support has been enabled for the Feast feature server at /mcp endpoint" @@ -53,6 +150,8 @@ def add_mcp_support_to_app(app, store: FeatureStore, config) -> Optional["FastAp return mcp + except McpTransportNotSupportedError: + raise except Exception as e: - logger.error(f"Failed to initialize MCP integration: {e}") + logger.error(f"Failed to initialize MCP integration: {e}", exc_info=True) return None diff --git a/sdk/python/feast/infra/offline_stores/bigquery.py b/sdk/python/feast/infra/offline_stores/bigquery.py index 2c4bc8cdbc3..a7c8c41094d 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery.py +++ b/sdk/python/feast/infra/offline_stores/bigquery.py @@ -1,7 +1,8 @@ import contextlib +import json import tempfile import uuid -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone from pathlib import Path from typing import ( Any, @@ -9,11 +10,13 @@ ContextManager, Dict, Iterator, + KeysView, List, Literal, Optional, Tuple, Union, + cast, ) import numpy as np @@ -42,10 +45,21 @@ RetrievalMetadata, ) from feast.infra.registry.base_registry import BaseRegistry +from feast.monitoring.monitoring_utils import ( + MON_TABLE_FEATURE, + MON_TABLE_FEATURE_SERVICE, + MON_TABLE_FEATURE_VIEW, + MON_TABLE_JOB, + empty_categorical_metric, + empty_numeric_metric, + monitoring_table_meta, + normalize_monitoring_row, + opt_float, +) from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage -from feast.utils import _utc_now, get_user_agent +from feast.utils import _utc_now, compute_non_entity_date_range, get_user_agent from .bigquery_source import ( BigQueryLoggingDestination, @@ -123,6 +137,8 @@ def project_id_exists(cls, v, values, **kwargs): class BigQueryOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -161,6 +177,19 @@ def pull_latest_from_table_or_query( project=project_id, location=config.offline_store.location, ) + cast_style: Literal["date_func", "timestamp_func"] = ( + "date_func" + if data_source.timestamp_field_type == "DATE" + else "timestamp_func" + ) + timestamp_filter = get_timestamp_filter_sql( + start_date, + end_date, + timestamp_field, + date_partition_column=data_source.date_partition_column, + quote_fields=False, + cast_style=cast_style, + ) query = f""" SELECT {field_string} @@ -169,7 +198,7 @@ def pull_latest_from_table_or_query( SELECT {field_string}, ROW_NUMBER() OVER({partition_by_join_key_string} ORDER BY {timestamp_desc_string}) AS _feast_row FROM {from_expression} - WHERE {timestamp_field} BETWEEN TIMESTAMP('{start_date}') AND TIMESTAMP('{end_date}') + WHERE {timestamp_filter} ) WHERE _feast_row = 1 """ @@ -212,12 +241,18 @@ def pull_all_from_table_or_query( + BigQueryOfflineStore._escape_query_columns(feature_name_columns) + timestamp_fields ) + cast_style: Literal["date_func", "timestamp_func"] = ( + "date_func" + if data_source.timestamp_field_type == "DATE" + else "timestamp_func" + ) timestamp_filter = get_timestamp_filter_sql( start_date, end_date, timestamp_field, + date_partition_column=data_source.date_partition_column, quote_fields=False, - cast_style="timestamp_func", + cast_style=cast_style, ) query = f""" SELECT {field_string} @@ -236,10 +271,12 @@ def get_historical_features( config: RepoConfig, feature_views: List[FeatureView], feature_refs: List[str], - entity_df: Union[pd.DataFrame, str], + entity_df: Optional[Union[pd.DataFrame, str]], registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, + **kwargs: Any, ) -> RetrievalJob: # TODO: Add entity_df validation in order to fail before interacting with BigQuery assert isinstance(config.offline_store, BigQueryOfflineStoreConfig) @@ -266,39 +303,52 @@ def get_historical_features( config.offline_store.table_create_disposition, ) - entity_schema = _get_entity_schema( - client=client, - entity_df=entity_df, - ) + # Non-entity mode: create a left temporary table from entity keys - any entity key having an event in the time window - entity_df_event_timestamp_col = ( - offline_utils.infer_event_timestamp_from_entity_df(entity_schema) - ) + non_entity_mode = entity_df is None - entity_df_event_timestamp_range = _get_entity_df_event_timestamp_range( - entity_df, - entity_df_event_timestamp_col, - client, - ) + if non_entity_mode: + start_date, end_date = compute_non_entity_date_range( + feature_views, + start_date=kwargs.get("start_date"), + end_date=kwargs.get("end_date"), + ) + entity_df_event_timestamp_range = (start_date, end_date) - @contextlib.contextmanager - def query_generator() -> Iterator[str]: - _upload_entity_df( + # Pre-compute query contexts to collect entity column names per feature view. + fv_query_contexts_pre = offline_utils.get_feature_view_query_context( + feature_refs, + feature_views, + registry, + project, + entity_df_event_timestamp_range, + ) + all_entities = offline_utils.gather_all_entities(fv_query_contexts_pre) + event_timestamp_col = "entity_ts" + entity_schema_keys: KeysView[str] = cast( + KeysView[str], + {k: None for k in (all_entities + [event_timestamp_col])}.keys(), + ) + + entity_schema = None + else: + entity_schema = _get_entity_schema( client=client, - table_name=table_reference, entity_df=entity_df, ) - - expected_join_keys = offline_utils.get_expected_join_keys( - project, feature_views, registry + event_timestamp_col = offline_utils.infer_event_timestamp_from_entity_df( + entity_schema ) - - offline_utils.assert_expected_columns_in_entity_df( - entity_schema, expected_join_keys, entity_df_event_timestamp_col + entity_df_event_timestamp_range = _get_entity_df_event_timestamp_range( + entity_df, + event_timestamp_col, + client, ) - - # Build a query context containing all information required to template the BigQuery SQL query - query_context = offline_utils.get_feature_view_query_context( + entity_schema_keys = entity_schema.keys() + all_entities = [] + start_date = entity_df_event_timestamp_range[0] + end_date = entity_df_event_timestamp_range[1] + fv_query_contexts_pre = offline_utils.get_feature_view_query_context( feature_refs, feature_views, registry, @@ -306,20 +356,48 @@ def query_generator() -> Iterator[str]: entity_df_event_timestamp_range, ) + @contextlib.contextmanager + def query_generator() -> Iterator[str]: + if non_entity_mode: + _bq_create_entity_union_table( + client=client, + table_name=table_reference, + feature_views=feature_views, + fv_query_contexts=fv_query_contexts_pre, + start_date=start_date, + end_date=end_date, + all_entities=all_entities, + event_timestamp_col=event_timestamp_col, + ) + else: + _upload_entity_df( + client=client, + table_name=table_reference, + entity_df=entity_df, + ) + expected_join_keys = offline_utils.get_expected_join_keys( + project, feature_views, registry + ) + assert entity_schema is not None + offline_utils.assert_expected_columns_in_entity_df( + entity_schema, expected_join_keys, event_timestamp_col + ) + # Generate the BigQuery SQL query from the query context query = offline_utils.build_point_in_time_query( - query_context, + feature_view_query_contexts=fv_query_contexts_pre, left_table_query_string=table_reference, - entity_df_event_timestamp_col=entity_df_event_timestamp_col, - entity_df_columns=entity_schema.keys(), + entity_df_event_timestamp_col=event_timestamp_col, + entity_df_columns=entity_schema_keys, query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, full_feature_names=full_feature_names, + filter_by_created_timestamp=filter_by_created_timestamp, ) try: yield query finally: - # Asynchronously clean up the uploaded Bigquery table, which will expire + # Asynchronously clean up the uploaded BigQuery table, which will expire # if cleanup fails client.delete_table(table=table_reference, not_found_ok=True) @@ -333,7 +411,7 @@ def query_generator() -> Iterator[str]: ), metadata=RetrievalMetadata( features=feature_refs, - keys=list(entity_schema.keys() - {entity_df_event_timestamp_col}), + keys=list(set(entity_schema_keys) - {event_timestamp_col}), min_event_timestamp=entity_df_event_timestamp_range[0], max_event_timestamp=entity_df_event_timestamp_range[1], ), @@ -416,7 +494,7 @@ def offline_write_batch( ) if table.schema != pa_schema: - table = table.cast(pa_schema) + table = offline_utils.cast_arrow_table_to_schema(table, pa_schema) project_id = ( config.offline_store.billing_project_id or config.offline_store.project_id ) @@ -425,11 +503,15 @@ def offline_write_batch( location=config.offline_store.location, ) + parquet_options = bigquery.ParquetOptions() + parquet_options.enable_list_inference = True + job_config = bigquery.LoadJobConfig( source_format=bigquery.SourceFormat.PARQUET, schema=arrow_schema_to_bq_schema(pa_schema), create_disposition=config.offline_store.table_create_disposition, write_disposition="WRITE_APPEND", # Default but included for clarity + parquet_options=parquet_options, ) with tempfile.TemporaryFile() as parquet_temp_file: @@ -454,6 +536,704 @@ def offline_write_batch( def _escape_query_columns(columns: List[str]) -> List[str]: return [f"`{x}`" for x in columns] + @staticmethod + def compute_monitoring_metrics( + config: RepoConfig, + data_source: DataSource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, BigQueryOfflineStoreConfig) + assert isinstance(data_source, BigQuerySource) + return _bq_compute_monitoring_metrics( + config, + data_source, + feature_columns, + timestamp_field, + start_date=start_date, + end_date=end_date, + histogram_bins=histogram_bins, + top_n=top_n, + ) + + @staticmethod + def get_monitoring_max_timestamp( + config: RepoConfig, + data_source: DataSource, + timestamp_field: str, + ) -> Optional[datetime]: + assert isinstance(config.offline_store, BigQueryOfflineStoreConfig) + assert isinstance(data_source, BigQuerySource) + return _bq_get_monitoring_max_timestamp(config, data_source, timestamp_field) + + @staticmethod + def ensure_monitoring_tables(config: RepoConfig) -> None: + assert isinstance(config.offline_store, BigQueryOfflineStoreConfig) + _bq_ensure_monitoring_tables(config) + + @staticmethod + def save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], + ) -> None: + if not metrics: + return + assert isinstance(config.offline_store, BigQueryOfflineStoreConfig) + _bq_save_monitoring_metrics(config, metric_type, metrics) + + @staticmethod + def query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, BigQueryOfflineStoreConfig) + return _bq_query_monitoring_metrics( + config, project, metric_type, filters, start_date, end_date + ) + + @staticmethod + def clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> None: + assert isinstance(config.offline_store, BigQueryOfflineStoreConfig) + _bq_clear_monitoring_baseline( + config, project, feature_view_name, feature_name, data_source_type + ) + + +def _bq_create_entity_union_table( + client: "Client", + table_name: str, + feature_views: List[FeatureView], + fv_query_contexts: List[offline_utils.FeatureViewQueryContext], + start_date: datetime, + end_date: datetime, + all_entities: List[str], + event_timestamp_col: str, +) -> None: + """ + Creates a BigQuery temp table containing the UNION DISTINCT of entity keys observed + across all feature views in [start_date, end_date], plus a stable as-of timestamp + column set to end_date. Used as the left table for PIT joins in non-entity mode. + """ + start_str = start_date.strftime("%Y-%m-%dT%H:%M:%S") + end_str = end_date.strftime("%Y-%m-%dT%H:%M:%S") + + per_view_selects: List[str] = [] + for fv, ctx in zip(feature_views, fv_query_contexts): + assert isinstance(fv.batch_source, BigQuerySource) + from_expression = fv.batch_source.get_table_query_string() + timestamp_field = ctx.timestamp_field + + ctx_entities_set = set(ctx.entities) + select_entities: List[str] = [] + for col in all_entities: + if col in ctx_entities_set: + select_entities.append(f"`{col}`") + else: + select_entities.append(f"CAST(NULL AS STRING) AS `{col}`") + + per_view_selects.append( + f"SELECT DISTINCT {', '.join(select_entities)} " + f"FROM {from_expression} " + f"WHERE `{timestamp_field}` BETWEEN TIMESTAMP('{start_str}') AND TIMESTAMP('{end_str}')" + ) + + union_query = "\nUNION DISTINCT\n".join(per_view_selects) + entity_cols = ( + ", ".join(f"`{e}`" for e in all_entities) if all_entities else "TRUE AS _dummy" + ) + + create_sql = ( + f"CREATE TABLE `{table_name}` AS " + f"SELECT {entity_cols}, TIMESTAMP('{end_str}') AS `{event_timestamp_col}` " + f"FROM ({union_query}) AS _entity_union" + ) + + block_until_done(client, client.query(create_sql)) + + table = client.get_table(table_name) + table.expires = _utc_now() + timedelta(minutes=30) + client.update_table(table, ["expires"]) + + +# ------------------------------------------------------------------ # +# BigQuery monitoring metrics (native) +# ------------------------------------------------------------------ # + + +def _bq_monitoring_table_fqn(config: RepoConfig, table_name: str) -> str: + assert isinstance(config.offline_store, BigQueryOfflineStoreConfig) + project_id = config.offline_store.project_id + if not project_id: + client = _get_bigquery_client( + project=config.offline_store.billing_project_id, + location=config.offline_store.location, + ) + project_id = client.project + return f"`{project_id}.{config.offline_store.dataset}.{table_name}`" + + +def _bq_scalar_param_type(column: str) -> str: + if column == "is_baseline": + return "BOOL" + if column == "metric_date": + return "DATE" + if column == "computed_at": + return "TIMESTAMP" + if column in { + "row_count", + "null_count", + "total_row_count", + "total_features", + "features_with_nulls", + "total_feature_views", + }: + return "INT64" + if column in { + "null_rate", + "mean", + "stddev", + "min_val", + "max_val", + "p50", + "p75", + "p90", + "p95", + "p99", + "avg_null_rate", + "max_null_rate", + }: + return "FLOAT64" + return "STRING" + + +def _bq_merge_row( + config: RepoConfig, + table_fqn: str, + columns: List[str], + pk_columns: List[str], + row: Dict[str, Any], +) -> None: + project_id = ( + config.offline_store.billing_project_id or config.offline_store.project_id + ) + client = _get_bigquery_client( + project=project_id, + location=config.offline_store.location, + ) + non_pk = [c for c in columns if c not in pk_columns] + params: List[Any] = [] + using_parts: List[str] = [] + on_parts: List[str] = [] + merge_idx = 0 + for col in columns: + p = f"p{merge_idx}" + merge_idx += 1 + val = row.get(col) + if col == "histogram" and val is not None and not isinstance(val, str): + val = json.dumps(val) + param_type = _bq_scalar_param_type(col) + params.append(bigquery.ScalarQueryParameter(p, param_type, val)) + using_parts.append(f"@{p} AS {col}") + on_parts = [f"T.{col} = S.{col}" for col in pk_columns] + update_set = ", ".join(f"{c} = S.{c}" for c in non_pk) + merge_sql = f""" +MERGE {table_fqn} T +USING (SELECT {", ".join(using_parts)}) S +ON {" AND ".join(on_parts)} +WHEN MATCHED THEN UPDATE SET {update_set} +WHEN NOT MATCHED THEN INSERT ({", ".join(columns)}) VALUES ({", ".join(f"S.{c}" for c in columns)}) +""" + job_config = bigquery.QueryJobConfig(query_parameters=params) + client.query(merge_sql, job_config=job_config).result() + + +def _bq_save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], +) -> None: + table_short, columns, pk_columns = monitoring_table_meta(metric_type) + table_fqn = _bq_monitoring_table_fqn(config, table_short) + for row in metrics: + _bq_merge_row(config, table_fqn, columns, pk_columns, row) + + +def _bq_query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, +) -> List[Dict[str, Any]]: + table_short, columns, _ = monitoring_table_meta(metric_type) + table_fqn = _bq_monitoring_table_fqn(config, table_short) + project_id = ( + config.offline_store.billing_project_id or config.offline_store.project_id + ) + client = _get_bigquery_client( + project=project_id, + location=config.offline_store.location, + ) + params: List[Any] = [] + conditions: List[str] = [] + if project: + params.append( + bigquery.ScalarQueryParameter("project", "STRING", project), + ) + conditions.append("project_id = @project") + if filters: + for key, value in filters.items(): + if value is not None: + conditions.append(f"`{key}` = @{key}") + params.append( + bigquery.ScalarQueryParameter( + key, _bq_scalar_param_type(key), value + ) + ) + if start_date: + conditions.append("metric_date >= @start_date") + params.append(bigquery.ScalarQueryParameter("start_date", "DATE", start_date)) + if end_date: + conditions.append("metric_date <= @end_date") + params.append(bigquery.ScalarQueryParameter("end_date", "DATE", end_date)) + col_list = ", ".join(f"`{c}`" for c in columns) + where_sql = " AND ".join(conditions) if conditions else "TRUE" + order_col = "metric_date" if "metric_date" in columns else "job_id" + sql = f"SELECT {col_list} FROM {table_fqn} WHERE {where_sql} ORDER BY `{order_col}` ASC" + job_config = bigquery.QueryJobConfig(query_parameters=params) + job = client.query(sql, job_config=job_config) + job.result() + results: List[Dict[str, Any]] = [] + for r in job: + record = {columns[i]: r[i] for i in range(len(columns))} + results.append(normalize_monitoring_row(record)) + return results + + +def _bq_clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, +) -> None: + table_fqn = _bq_monitoring_table_fqn(config, MON_TABLE_FEATURE) + project_id = ( + config.offline_store.billing_project_id or config.offline_store.project_id + ) + client = _get_bigquery_client( + project=project_id, + location=config.offline_store.location, + ) + params: List[Any] = [ + bigquery.ScalarQueryParameter("project", "STRING", project), + ] + conditions = ["project_id = @project", "is_baseline = TRUE"] + if feature_view_name: + conditions.append("feature_view_name = @feature_view_name") + params.append( + bigquery.ScalarQueryParameter( + "feature_view_name", "STRING", feature_view_name + ) + ) + if feature_name: + conditions.append("feature_name = @feature_name") + params.append( + bigquery.ScalarQueryParameter("feature_name", "STRING", feature_name) + ) + if data_source_type: + conditions.append("data_source_type = @data_source_type") + params.append( + bigquery.ScalarQueryParameter( + "data_source_type", "STRING", data_source_type + ) + ) + sql = f"UPDATE {table_fqn} SET is_baseline = FALSE WHERE {' AND '.join(conditions)}" + job_config = bigquery.QueryJobConfig(query_parameters=params) + client.query(sql, job_config=job_config).result() + + +def _bq_ensure_monitoring_tables(config: RepoConfig) -> None: + project_id = ( + config.offline_store.billing_project_id or config.offline_store.project_id + ) + client = _get_bigquery_client( + project=project_id, + location=config.offline_store.location, + ) + ds = config.offline_store.dataset + proj = config.offline_store.project_id or client.project + feature_ddl = f""" +CREATE TABLE IF NOT EXISTS `{proj}.{ds}.{MON_TABLE_FEATURE}` ( + project_id STRING NOT NULL, + feature_view_name STRING NOT NULL, + feature_name STRING NOT NULL, + metric_date DATE NOT NULL, + granularity STRING NOT NULL, + data_source_type STRING NOT NULL, + computed_at TIMESTAMP NOT NULL, + is_baseline BOOL NOT NULL, + feature_type STRING NOT NULL, + row_count INT64, + null_count INT64, + null_rate FLOAT64, + mean FLOAT64, + stddev FLOAT64, + min_val FLOAT64, + max_val FLOAT64, + p50 FLOAT64, + p75 FLOAT64, + p90 FLOAT64, + p95 FLOAT64, + p99 FLOAT64, + histogram STRING +) +PRIMARY KEY (project_id, feature_view_name, feature_name, metric_date, granularity, data_source_type) NOT ENFORCED +""" + view_ddl = f""" +CREATE TABLE IF NOT EXISTS `{proj}.{ds}.{MON_TABLE_FEATURE_VIEW}` ( + project_id STRING NOT NULL, + feature_view_name STRING NOT NULL, + metric_date DATE NOT NULL, + granularity STRING NOT NULL, + data_source_type STRING NOT NULL, + computed_at TIMESTAMP NOT NULL, + is_baseline BOOL NOT NULL, + total_row_count INT64, + total_features INT64, + features_with_nulls INT64, + avg_null_rate FLOAT64, + max_null_rate FLOAT64 +) +PRIMARY KEY (project_id, feature_view_name, metric_date, granularity, data_source_type) NOT ENFORCED +""" + service_ddl = f""" +CREATE TABLE IF NOT EXISTS `{proj}.{ds}.{MON_TABLE_FEATURE_SERVICE}` ( + project_id STRING NOT NULL, + feature_service_name STRING NOT NULL, + metric_date DATE NOT NULL, + granularity STRING NOT NULL, + data_source_type STRING NOT NULL, + computed_at TIMESTAMP NOT NULL, + is_baseline BOOL NOT NULL, + total_feature_views INT64, + total_features INT64, + avg_null_rate FLOAT64, + max_null_rate FLOAT64 +) +PRIMARY KEY (project_id, feature_service_name, metric_date, granularity, data_source_type) NOT ENFORCED +""" + job_ddl = f""" +CREATE TABLE IF NOT EXISTS `{proj}.{ds}.{MON_TABLE_JOB}` ( + job_id STRING NOT NULL, + project_id STRING NOT NULL, + feature_view_name STRING, + job_type STRING NOT NULL, + status STRING NOT NULL, + parameters STRING, + metric_date DATE NOT NULL, + started_at TIMESTAMP, + completed_at TIMESTAMP, + error_message STRING, + result_summary STRING +) +PRIMARY KEY (job_id) NOT ENFORCED +""" + for ddl in (feature_ddl, view_ddl, service_ddl, job_ddl): + client.query(ddl).result() + + +def _bq_get_monitoring_max_timestamp( + config: RepoConfig, + data_source: BigQuerySource, + timestamp_field: str, +) -> Optional[datetime]: + from_expression = data_source.get_table_query_string() + ts_col = f"`{timestamp_field}`" + sql = f"SELECT MAX({ts_col}) AS _max_ts FROM {from_expression}" + project_id = ( + config.offline_store.billing_project_id or config.offline_store.project_id + ) + client = _get_bigquery_client( + project=project_id, + location=config.offline_store.location, + ) + job = client.query(sql) + job.result() + rows = list(job) + if not rows or rows[0][0] is None: + return None + val = rows[0][0] + if isinstance(val, datetime): + return val if val.tzinfo else val.replace(tzinfo=timezone.utc) + if isinstance(val, date): + return datetime.combine(val, datetime.min.time(), tzinfo=timezone.utc) + return val # type: ignore[no-any-return] + + +def _bq_numeric_histogram( + config: RepoConfig, + from_expression: str, + col_name: str, + ts_filter: str, + bins: int, + min_val: float, + max_val: float, +) -> Dict[str, Any]: + q_col = f"`{col_name}`" + project_id = ( + config.offline_store.billing_project_id or config.offline_store.project_id + ) + client = _get_bigquery_client( + project=project_id, + location=config.offline_store.location, + ) + if min_val == max_val: + sql = ( + f"SELECT COUNT(*) AS cnt FROM {from_expression} AS _src " + f"WHERE {q_col} IS NOT NULL AND {ts_filter}" + ) + job = client.query(sql) + job.result() + hrows = list(job) + cnt = int(hrows[0][0]) if hrows else 0 + return {"bins": [min_val, max_val], "counts": [cnt], "bin_width": 0.0} + + bin_width = (max_val - min_val) / bins + sql = f""" +SELECT + LEAST( + GREATEST( + CAST(FLOOR((CAST({q_col} AS FLOAT64) - {min_val}) / {bin_width}) AS INT64) + 1, + 1 + ), + {bins} + ) AS bucket, + COUNT(*) AS cnt +FROM {from_expression} AS _src +WHERE {q_col} IS NOT NULL AND {ts_filter} +GROUP BY bucket +ORDER BY bucket +""" + job = client.query(sql) + job.result() + rows = list(job) + counts = [0] * bins + for bucket, cnt in rows: + b = int(bucket) + if 1 <= b <= bins: + counts[b - 1] += int(cnt) + bin_edges = [min_val + i * bin_width for i in range(bins + 1)] + return { + "bins": [float(b) for b in bin_edges], + "counts": counts, + "bin_width": float(bin_width), + } + + +def _bq_numeric_stats( + config: RepoConfig, + from_expression: str, + feature_names: List[str], + ts_filter: str, + histogram_bins: int, +) -> List[Dict[str, Any]]: + project_id = ( + config.offline_store.billing_project_id or config.offline_store.project_id + ) + client = _get_bigquery_client( + project=project_id, + location=config.offline_store.location, + ) + select_parts: List[str] = ["COUNT(*) AS _row_count"] + for i, col in enumerate(feature_names): + q = f"`{col}`" + c = f"CAST({q} AS FLOAT64)" + select_parts.extend( + [ + f"COUNT({q}) AS c{i}_nn", + f"AVG({c}) AS c{i}_avg", + f"STDDEV_SAMP({c}) AS c{i}_stddev", + f"MIN({c}) AS c{i}_min", + f"MAX({c}) AS c{i}_max", + f"APPROX_QUANTILES({c}, 100)[OFFSET(50)] AS c{i}_p50", + f"APPROX_QUANTILES({c}, 100)[OFFSET(75)] AS c{i}_p75", + f"APPROX_QUANTILES({c}, 100)[OFFSET(90)] AS c{i}_p90", + f"APPROX_QUANTILES({c}, 100)[OFFSET(95)] AS c{i}_p95", + f"APPROX_QUANTILES({c}, 100)[OFFSET(99)] AS c{i}_p99", + ] + ) + query = ( + f"SELECT {', '.join(select_parts)} " + f"FROM {from_expression} AS _src WHERE {ts_filter}" + ) + job = client.query(query) + job.result() + rows = list(job) + if not rows: + return [empty_numeric_metric(n) for n in feature_names] + row = rows[0] + row_count = row["_row_count"] or 0 + results: List[Dict[str, Any]] = [] + for i, col in enumerate(feature_names): + base = f"c{i}_" + non_null = row[f"{base}nn"] or 0 + null_count = int(row_count) - int(non_null) + min_v = opt_float(row[f"{base}min"]) + max_v = opt_float(row[f"{base}max"]) + result: Dict[str, Any] = { + "feature_name": col, + "feature_type": "numeric", + "row_count": int(row_count), + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": opt_float(row[f"{base}avg"]), + "stddev": opt_float(row[f"{base}stddev"]), + "min_val": min_v, + "max_val": max_v, + "p50": opt_float(row[f"{base}p50"]), + "p75": opt_float(row[f"{base}p75"]), + "p90": opt_float(row[f"{base}p90"]), + "p95": opt_float(row[f"{base}p95"]), + "p99": opt_float(row[f"{base}p99"]), + "histogram": None, + } + if min_v is not None and max_v is not None and non_null and int(non_null) > 0: + result["histogram"] = _bq_numeric_histogram( + config, + from_expression, + col, + ts_filter, + histogram_bins, + min_v, + max_v, + ) + results.append(result) + return results + + +def _bq_categorical_stats( + config: RepoConfig, + from_expression: str, + col_name: str, + ts_filter: str, + top_n: int, +) -> Dict[str, Any]: + q_col = f"`{col_name}`" + project_id = ( + config.offline_store.billing_project_id or config.offline_store.project_id + ) + client = _get_bigquery_client( + project=project_id, + location=config.offline_store.location, + ) + query = f""" +WITH filtered AS ( + SELECT * FROM {from_expression} AS _src WHERE {ts_filter} +) +SELECT + (SELECT COUNT(*) FROM filtered) AS row_count, + (SELECT COUNT(*) - COUNT({q_col}) FROM filtered) AS null_count, + (SELECT COUNT(DISTINCT {q_col}) FROM filtered WHERE {q_col} IS NOT NULL) AS unique_count, + CAST({q_col} AS STRING) AS value, + COUNT(*) AS cnt +FROM filtered +WHERE {q_col} IS NOT NULL +GROUP BY CAST({q_col} AS STRING) +ORDER BY cnt DESC +LIMIT {int(top_n)} +""" + job = client.query(query) + job.result() + rows = list(job) + if not rows: + return empty_categorical_metric(col_name) + row_count = rows[0]["row_count"] + null_count = rows[0]["null_count"] + unique_count = rows[0]["unique_count"] + top_entries = [{"value": r["value"], "count": r["cnt"]} for r in rows] + top_total = sum(e["count"] for e in top_entries) + other_count = (row_count - null_count) - top_total + return { + "feature_name": col_name, + "feature_type": "categorical", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": { + "values": top_entries, + "other_count": max(other_count, 0), + "unique_count": unique_count, + }, + } + + +def _bq_compute_monitoring_metrics( + config: RepoConfig, + data_source: BigQuerySource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, +) -> List[Dict[str, Any]]: + from_expression = data_source.get_table_query_string() + ts_filter = ( + get_timestamp_filter_sql( + start_date, + end_date, + timestamp_field, + date_partition_column=data_source.date_partition_column, + quote_fields=False, + cast_style="timestamp_func", + ) + or "1=1" + ) + numeric_features = [n for n, t in feature_columns if t == "numeric"] + categorical_features = [n for n, t in feature_columns if t == "categorical"] + results: List[Dict[str, Any]] = [] + if numeric_features: + results.extend( + _bq_numeric_stats( + config, + from_expression, + numeric_features, + ts_filter, + histogram_bins, + ) + ) + for col_name in categorical_features: + results.append( + _bq_categorical_stats(config, from_expression, col_name, ts_filter, top_n) + ) + return results + class BigQueryRetrievalJob(RetrievalJob): def __init__( @@ -833,12 +1613,17 @@ def arrow_schema_to_bq_schema(arrow_schema: pyarrow.Schema) -> List[SchemaField] bq_schema = [] for field in arrow_schema: - if pyarrow.types.is_list(field.type): + if pyarrow.types.is_struct(field.type) or pyarrow.types.is_map(field.type): + detected_mode = "NULLABLE" + detected_type = "STRING" + elif pyarrow.types.is_list(field.type): detected_mode = "REPEATED" - detected_type = _ARROW_SCALAR_IDS_TO_BQ[field.type.value_type.id] + detected_type = _ARROW_SCALAR_IDS_TO_BQ.get( + field.type.value_type.id, "STRING" + ) else: detected_mode = "NULLABLE" - detected_type = _ARROW_SCALAR_IDS_TO_BQ[field.type.id] + detected_type = _ARROW_SCALAR_IDS_TO_BQ.get(field.type.id, "STRING") bq_schema.append( SchemaField(name=field.name, field_type=detected_type, mode=detected_mode) @@ -920,10 +1705,23 @@ def arrow_schema_to_bq_schema(arrow_schema: pyarrow.Schema) -> List[SchemaField] {% if loop.last %}{% else %}, {% endif %} {% endfor %} FROM {{ featureview.table_subquery }} + {% if featureview.timestamp_field_type == "DATE" %} + WHERE {{ featureview.timestamp_field }} <= DATE('{{ featureview.max_event_timestamp[:10] }}') + {% if featureview.ttl == 0 %}{% else %} + AND {{ featureview.timestamp_field }} >= DATE('{{ featureview.min_event_timestamp[:10] }}') + {% endif %} + {% else %} WHERE {{ featureview.timestamp_field }} <= '{{ featureview.max_event_timestamp }}' {% if featureview.ttl == 0 %}{% else %} AND {{ featureview.timestamp_field }} >= '{{ featureview.min_event_timestamp }}' {% endif %} + {% endif %} + {% if featureview.date_partition_column %} + AND {{ featureview.date_partition_column | backticks }} <= '{{ featureview.max_event_timestamp[:10] }}' + {% if featureview.min_event_timestamp %} + AND {{ featureview.date_partition_column | backticks }} >= '{{ featureview.min_event_timestamp[:10] }}' + {% endif %} + {% endif %} ), {{ featureview.name }}__base AS ( @@ -940,6 +1738,10 @@ def arrow_schema_to_bq_schema(arrow_schema: pyarrow.Schema) -> List[SchemaField] AND subquery.event_timestamp >= Timestamp_sub(entity_dataframe.entity_timestamp, interval {{ featureview.ttl }} second) {% endif %} + {% if filter_by_created_timestamp and featureview.created_timestamp_column %} + AND subquery.created_timestamp <= entity_dataframe.entity_timestamp + {% endif %} + {% for entity in featureview.entities %} AND subquery.{{ entity }} = entity_dataframe.{{ entity }} {% endfor %} diff --git a/sdk/python/feast/infra/offline_stores/bigquery_source.py b/sdk/python/feast/infra/offline_stores/bigquery_source.py index 1dda7af928f..d1ad9e8417a 100644 --- a/sdk/python/feast/infra/offline_stores/bigquery_source.py +++ b/sdk/python/feast/infra/offline_stores/bigquery_source.py @@ -34,6 +34,8 @@ def __init__( table: Optional[str] = None, created_timestamp_column: Optional[str] = "", field_mapping: Optional[Dict[str, str]] = None, + date_partition_column: Optional[str] = None, + timestamp_field_type: Optional[str] = None, query: Optional[str] = None, description: Optional[str] = "", tags: Optional[Dict[str, str]] = None, @@ -46,14 +48,18 @@ def __init__( case the table must be specified. timestamp_field (optional): Event timestamp field used for point in time joins of feature values. - table (optional): BigQuery table where the features are stored. Exactly one of 'table' - and 'query' must be specified. - table (optional): The BigQuery table where features can be found. + table (optional): BigQuery table where the features are stored. At least one of 'table' + and 'query' must be specified. When both are set, 'query' is used for reads and + 'table' is used as the write destination. created_timestamp_column (optional): Timestamp column when row was created, used for deduplicating rows. field_mapping (optional): A dictionary mapping of column names in this data source to feature names in a feature table or view. Only used for feature columns, not entities or timestamp columns. - query (optional): The query to be executed to obtain the features. Exactly one of 'table' - and 'query' must be specified. + date_partition_column (optional): Timestamp column used for partitioning. + timestamp_field_type (optional): Type of the timestamp_field column. + Set to "DATE" when the event timestamp column is a DATE type, + so SQL generation uses date-only comparisons instead of TIMESTAMP(). + query (optional): The query to be executed to obtain the features. When both 'table' + and 'query' are provided, 'query' takes priority for reads. description (optional): A human-readable description. tags (optional): A dictionary of key-value pairs to store arbitrary metadata. owner (optional): The owner of the bigquery source, typically the email of the primary @@ -78,6 +84,8 @@ def __init__( timestamp_field=timestamp_field, created_timestamp_column=created_timestamp_column, field_mapping=field_mapping, + date_partition_column=date_partition_column, + timestamp_field_type=timestamp_field_type, description=description, tags=tags, owner=owner, @@ -89,9 +97,7 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, BigQuerySource): - raise TypeError( - "Comparisons should only involve BigQuerySource class objects." - ) + return False return ( super().__eq__(other) @@ -117,6 +123,8 @@ def from_proto(data_source: DataSourceProto): table=data_source.bigquery_options.table, timestamp_field=data_source.timestamp_field, created_timestamp_column=data_source.created_timestamp_column, + date_partition_column=data_source.date_partition_column, + timestamp_field_type=data_source.timestamp_field_type or None, query=data_source.bigquery_options.query, description=data_source.description, tags=dict(data_source.tags), @@ -134,6 +142,8 @@ def _to_proto_impl(self) -> DataSourceProto: owner=self.owner, timestamp_field=self.timestamp_field, created_timestamp_column=self.created_timestamp_column, + date_partition_column=self.date_partition_column, + timestamp_field_type=self.timestamp_field_type, ) return data_source_proto @@ -151,10 +161,10 @@ def validate(self, config: RepoConfig): def get_table_query_string(self) -> str: """Returns a string that can directly be used to reference this table in SQL""" - if self.table: - return f"`{self.table}`" - else: + if self.query: return f"({self.query})" + else: + return f"`{self.table}`" @staticmethod def source_datatype_to_feast_value_type() -> Callable[[str], ValueType]: @@ -180,14 +190,14 @@ def get_table_column_names_and_types( location=config.offline_store.location, client_info=http_client_info.ClientInfo(user_agent=get_user_agent()), ) - if self.table: - schema = client.get_table(self.table).schema - if not isinstance(schema[0], bigquery.schema.SchemaField): - raise TypeError("Could not parse BigQuery table schema.") - else: + if self.query: bq_columns_query = f"SELECT * FROM ({self.query}) LIMIT 0" query_res = client.query(bq_columns_query).result() schema = query_res.schema + else: + schema = client.get_table(self.table).schema + if not isinstance(schema[0], bigquery.schema.SchemaField): + raise TypeError("Could not parse BigQuery table schema.") name_type_pairs: List[Tuple[str, str]] = [] for field in schema: diff --git a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py index 6f92f45793b..d8fae6bf19b 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py +++ b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena.py @@ -66,6 +66,8 @@ class AthenaOfflineStoreConfig(FeastConfigBaseModel): class AthenaOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -168,7 +170,7 @@ def pull_all_from_table_or_query( end_date_str, timestamp_field, date_partition_column, - cast_style="raw", + cast_style="timestamp", quote_fields=False, ) @@ -195,6 +197,7 @@ def get_historical_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, ) -> RetrievalJob: assert isinstance(config.offline_store, AthenaOfflineStoreConfig) for fv in feature_views: @@ -252,6 +255,7 @@ def query_generator() -> Iterator[str]: entity_df_columns=entity_schema.keys(), query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, full_feature_names=full_feature_names, + filter_by_created_timestamp=filter_by_created_timestamp, ) try: @@ -651,6 +655,10 @@ def _get_entity_df_event_timestamp_range( AND subquery.event_timestamp >= entity_dataframe.entity_timestamp - {{ featureview.ttl }} * interval '1' second {% endif %} + {% if filter_by_created_timestamp and featureview.created_timestamp_column %} + AND subquery.created_timestamp <= entity_dataframe.entity_timestamp + {% endif %} + {% for entity in featureview.entities %} AND subquery.{{ entity }} = entity_dataframe.{{ entity }} {% endfor %} diff --git a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena_source.py b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena_source.py index 157ca35933f..94d635b2ad8 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/athena_source.py @@ -115,9 +115,7 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, AthenaSource): - raise TypeError( - "Comparisons should only involve AthenaSource class objects." - ) + return False return ( super().__eq__(other) diff --git a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/tests/data_source.py index b43c874ddc3..08dbbbf978d 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/athena_offline_store/tests/data_source.py @@ -16,7 +16,7 @@ ) from feast.infra.utils import aws_utils from feast.repo_config import FeastConfigBaseModel -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/athena_repo_configuration.py b/sdk/python/feast/infra/offline_stores/contrib/athena_repo_configuration.py index 09bc6ce961c..9fa7472af62 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/athena_repo_configuration.py +++ b/sdk/python/feast/infra/offline_stores/contrib/athena_repo_configuration.py @@ -1,7 +1,7 @@ from feast.infra.offline_stores.contrib.athena_offline_store.tests.data_source import ( AthenaDataSourceCreator, ) -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/clickhouse_offline_store/clickhouse.py b/sdk/python/feast/infra/offline_stores/contrib/clickhouse_offline_store/clickhouse.py index 5e8cf3d9053..9e353151746 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/clickhouse_offline_store/clickhouse.py +++ b/sdk/python/feast/infra/offline_stores/contrib/clickhouse_offline_store/clickhouse.py @@ -31,6 +31,7 @@ from feast.infra.utils.clickhouse.clickhouse_config import ClickhouseConfig from feast.infra.utils.clickhouse.connection_utils import get_client from feast.saved_dataset import SavedDatasetStorage +from feast.utils import compute_non_entity_date_range class ClickhouseOfflineStoreConfig(ClickhouseConfig): @@ -38,20 +39,33 @@ class ClickhouseOfflineStoreConfig(ClickhouseConfig): class ClickhouseOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def get_historical_features( config: RepoConfig, feature_views: List[FeatureView], feature_refs: List[str], - entity_df: Union[pd.DataFrame, str], + entity_df: Optional[Union[pd.DataFrame, str]], registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, + **kwargs, ) -> RetrievalJob: assert isinstance(config.offline_store, ClickhouseOfflineStoreConfig) for fv in feature_views: assert isinstance(fv.batch_source, ClickhouseSource) + # Handle non-entity retrieval mode + if entity_df is None: + start_date, end_date = compute_non_entity_date_range( + feature_views, + start_date=kwargs.get("start_date"), + end_date=kwargs.get("end_date"), + ) + entity_df = pd.DataFrame({"event_timestamp": [end_date]}) + entity_schema = _get_entity_schema(entity_df, config) entity_df_event_timestamp_col = ( @@ -112,6 +126,7 @@ def query_generator() -> Iterator[str]: entity_df_columns=entity_schema.keys(), query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, full_feature_names=full_feature_names, + filter_by_created_timestamp=filter_by_created_timestamp, ) yield query finally: @@ -528,6 +543,10 @@ def _append_alias(field_names: List[str], alias: str) -> List[str]: {% if featureview.ttl == 0 %}{% else %} AND subquery.event_timestamp >= entity_dataframe.entity_timestamp - interval {{ featureview.ttl }} second {% endif %} + + {% if filter_by_created_timestamp and featureview.created_timestamp_column %} + AND subquery.created_timestamp <= entity_dataframe.entity_timestamp + {% endif %} ), /* diff --git a/sdk/python/feast/infra/offline_stores/contrib/clickhouse_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/clickhouse_offline_store/tests/data_source.py index 80fd1751dc5..4c6068fd6bd 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/clickhouse_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/clickhouse_offline_store/tests/data_source.py @@ -15,7 +15,9 @@ from feast.infra.offline_stores.contrib.clickhouse_offline_store.clickhouse_source import ( ClickhouseSource, ) -from tests.integration.feature_repos.universal.data_source_creator import ( +from feast.infra.utils.clickhouse.clickhouse_config import ClickhouseConfig +from feast.infra.utils.clickhouse.connection_utils import get_client +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) @@ -114,3 +116,109 @@ def create_saved_dataset_destination(self): def teardown(self): pass + + +def _make_offline_store_config(clickhouse_container): + """Build a ClickhouseOfflineStoreConfig pointing at the test container.""" + return ClickhouseOfflineStoreConfig( + type="clickhouse", + host=clickhouse_container.get_container_host_ip(), + port=clickhouse_container.get_exposed_port(8123), + database=CLICKHOUSE_OFFLINE_DB, + user=CLICKHOUSE_USER, + password=CLICKHOUSE_PASSWORD, + ) + + +def test_get_client_with_additional_params(clickhouse_container): + """ + Test that get_client works with a real ClickHouse container and properly passes + additional settings like send_receive_timeout. + """ + # Create config with custom send_receive_timeout + config = ClickhouseConfig( + host=clickhouse_container.get_container_host_ip(), + port=clickhouse_container.get_exposed_port(8123), + user=CLICKHOUSE_USER, + password=CLICKHOUSE_PASSWORD, + database=CLICKHOUSE_OFFLINE_DB, + additional_client_args={"send_receive_timeout": 60}, + ) + + # Get client and verify it works + client = get_client(config) + + # Verify client is connected and functional by running a simple query + result = client.query("SELECT 1 AS test_value") + assert result.result_rows == [(1,)] + + # Verify the send_receive_timeout was applied + assert client.timeout._read == 60 + + +def test_non_entity_retrieval(clickhouse_container): + """Integration test: get_historical_features with entity_df=None returns real data.""" + from datetime import datetime, timedelta, timezone + from unittest.mock import MagicMock + + from feast.feature_view import FeatureView, Field + from feast.infra.offline_stores.contrib.clickhouse_offline_store.clickhouse import ( + ClickhouseOfflineStore, + df_to_clickhouse_table, + ) + from feast.repo_config import RepoConfig + from feast.types import Float32 + + offline_config = _make_offline_store_config(clickhouse_container) + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=offline_config, + ) + + # Seed a feature table with real data + now = datetime.now(tz=timezone.utc) + feature_df = pd.DataFrame( + { + "event_timestamp": [now - timedelta(hours=2), now - timedelta(hours=1)], + "feature_value": [1.0, 2.0], + } + ) + table_name = "test_non_entity_features" + client = get_client(offline_config) + client.command(f"DROP TABLE IF EXISTS {table_name}") + df_to_clickhouse_table(offline_config, feature_df, table_name, "event_timestamp") + + source = ClickhouseSource( + name=table_name, + table=table_name, + timestamp_field="event_timestamp", + ) + fv = FeatureView( + name="test_fv", + entities=[], + ttl=timedelta(days=1), + source=source, + schema=[Field(name="feature_value", dtype=Float32)], + ) + + registry = MagicMock() + registry.list_on_demand_feature_views.return_value = [] + + job = ClickhouseOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["test_fv:feature_value"], + entity_df=None, + registry=registry, + project="test_project", + end_date=now, + ) + + result_df = job.to_df() + assert len(result_df) > 0 + assert "feature_value" in result_df.columns + + # Cleanup + client.command(f"DROP TABLE IF EXISTS {table_name}") diff --git a/sdk/python/feast/infra/offline_stores/contrib/clickhouse_repo_configuration.py b/sdk/python/feast/infra/offline_stores/contrib/clickhouse_repo_configuration.py index 5c9d4461b16..6874bc9a3fc 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/clickhouse_repo_configuration.py +++ b/sdk/python/feast/infra/offline_stores/contrib/clickhouse_repo_configuration.py @@ -1,8 +1,8 @@ from feast.infra.offline_stores.contrib.clickhouse_offline_store.tests.data_source import ( ClickhouseDataSourceCreator, ) -from tests.integration.feature_repos.repo_configuration import REDIS_CONFIG -from tests.integration.feature_repos.universal.online_store.redis import ( +from tests.universal.feature_repos.repo_configuration import REDIS_CONFIG +from tests.universal.feature_repos.universal.online_store.redis import ( RedisOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/couchbase_columnar_repo_configuration.py b/sdk/python/feast/infra/offline_stores/contrib/couchbase_columnar_repo_configuration.py index 745a074a757..3ce308a00cf 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/couchbase_columnar_repo_configuration.py +++ b/sdk/python/feast/infra/offline_stores/contrib/couchbase_columnar_repo_configuration.py @@ -1,11 +1,11 @@ from feast.infra.offline_stores.contrib.couchbase_offline_store.tests.data_source import ( CouchbaseColumnarDataSourceCreator, ) -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.repo_configuration import REDIS_CONFIG -from tests.integration.feature_repos.universal.online_store.redis import ( +from tests.universal.feature_repos.repo_configuration import REDIS_CONFIG +from tests.universal.feature_repos.universal.online_store.redis import ( RedisOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/couchbase.py b/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/couchbase.py index 54921e9515e..7ff7ed7225f 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/couchbase.py +++ b/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/couchbase.py @@ -64,6 +64,8 @@ class CouchbaseColumnarOfflineStoreConfig(FeastConfigBaseModel): class CouchbaseColumnarOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -136,6 +138,7 @@ def get_historical_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, ) -> RetrievalJob: """ Retrieve historical features using point-in-time joins. @@ -197,6 +200,7 @@ def query_generator() -> Iterator[str]: entity_df_columns=entity_schema.keys(), query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, full_feature_names=full_feature_names, + filter_by_created_timestamp=filter_by_created_timestamp, ) yield query finally: @@ -481,6 +485,7 @@ def build_point_in_time_query( entity_df_columns: KeysView[str], query_template: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, ) -> str: """Build point-in-time query between each feature view table and the entity dataframe for Couchbase Columnar""" template = Environment(loader=BaseLoader()).from_string(source=query_template) @@ -507,6 +512,7 @@ def build_point_in_time_query( "featureviews": feature_view_query_contexts, "full_feature_names": full_feature_names, "final_output_feature_names": final_output_feature_names, + "filter_by_created_timestamp": filter_by_created_timestamp, } query = template.render(template_context) @@ -620,6 +626,9 @@ def _get_entity_schema( {% if featureview.ttl == 0 %}{% else %} AND date_diff_str(entity_dataframe.entity_timestamp, subquery.event_timestamp, "second") <= {{ featureview.ttl }} {% endif %} + {% if filter_by_created_timestamp and featureview.created_timestamp_column %} + AND subquery.created_timestamp <= entity_dataframe.entity_timestamp + {% endif %} {% for entity in featureview.entities %} AND subquery.`{{ entity }}` = entity_dataframe.`{{ entity }}` {% endfor %} diff --git a/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/couchbase_source.py b/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/couchbase_source.py index 10b9863daed..3702e566759 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/couchbase_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/couchbase_source.py @@ -93,9 +93,7 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, CouchbaseColumnarSource): - raise TypeError( - "Comparisons should only involve CouchbaseColumnarSource class objects." - ) + return False return ( super().__eq__(other) diff --git a/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/tests/data_source.py index c23a8301a76..c9491accd59 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/couchbase_offline_store/tests/data_source.py @@ -23,7 +23,7 @@ ) from feast.infra.utils.couchbase.couchbase_utils import normalize_timestamp from feast.repo_config import FeastConfigBaseModel -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/mongodb_offline_store/README.md b/sdk/python/feast/infra/offline_stores/contrib/mongodb_offline_store/README.md new file mode 100644 index 00000000000..24446f8c003 --- /dev/null +++ b/sdk/python/feast/infra/offline_stores/contrib/mongodb_offline_store/README.md @@ -0,0 +1,71 @@ +# MongoDB Offline Store + +This offline store lets you train models and run batch scoring directly from it. +All feature views share a single collection. Reads use +MongoDB aggregation pipelines with a compound index, so per-entity cost is +O(log n_observations) regardless of collection size. + +## Schema + +All feature views share one collection (default: `feature_history`), discriminated by the `feature_view` field. + +```javascript +// Collection: feature_history +{ + "entity_id": Binary("..."), // Serialized entity key (bytes) + "feature_view": "driver_stats", // Discriminator + "features": { // Nested subdocument + "trips_today": 5, + "rating": 4.8 + }, + "event_timestamp": ISODate("2024-01-15T10:00:00Z"), + "created_at": ISODate("2024-01-15T10:00:01Z") +} +``` +## Index + +The store creates one compound index lazily on first use. This index supports every query issued.. + +```javascript +db.feature_history.createIndex({ + "entity_id": 1, + "feature_view": 1, + "event_timestamp": -1, + "created_at": -1 +}) + +``` +## Configuration + +```yaml +offline_store: + type: feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb.MongoDBOfflineStore + connection_string: mongodb://localhost:27017 + database: feast + collection: feature_history # optional, default: feature_history +``` + +## Key Features + +**Query-collapse** — Feature views that share the same join key set are grouped into a single MongoDB aggregation round-trip instead of one per feature view. Reduces round-trips from K to the number of unique join key signatures, often one. + +**Scoring path** — When `entity_df` contains unique entity IDs, a `$match + $sort + $group` pipeline performs server-side deduplication returning at most one document per `(entity_id, feature_view)`. The compound index makes per-entity cost O(log n_obs). + +**Training path** — When `entity_df` contains repeated entity IDs at different timestamps, the `$group` stage is omitted and `pandas.merge_asof` performs per-row point-in-time joins optimized in C. + +**`strict_pit`** — `get_historical_features` accepts a `strict_pit` keyword argument (default `True`). With `strict_pit=True` (default, safe for training), documents whose timestamp is strictly after the entity request timestamp are returned as `NULL`. Set `strict_pit=False` for real-time inference where you always want the most recent observation. + + +## Writing Data + +Use `offline_write_batch` (called automatically by `feast materialize`) to write feature observations: + +```python +store.write_to_offline_store(feature_view_name, df) +``` + +Documents are appended; `pull_latest` and the scoring path select the highest `created_at` at read time. + +## Memory Behaviour + +The store filters by entity key in `$match` rather than loading the entire collection. Memory usage is bounded by the number of unique entity IDs × documents per entity, not the total collection size. diff --git a/sdk/python/feast/infra/offline_stores/contrib/mongodb_offline_store/__init__.py b/sdk/python/feast/infra/offline_stores/contrib/mongodb_offline_store/__init__.py new file mode 100644 index 00000000000..535583bc38d --- /dev/null +++ b/sdk/python/feast/infra/offline_stores/contrib/mongodb_offline_store/__init__.py @@ -0,0 +1,8 @@ +import feast.version + +try: + from pymongo.driver_info import DriverInfo + + DRIVER_METADATA = DriverInfo(name="Feast", version=feast.version.get_version()) +except ImportError: + DRIVER_METADATA = None # type: ignore[assignment] diff --git a/sdk/python/feast/infra/offline_stores/contrib/mongodb_offline_store/mongodb.py b/sdk/python/feast/infra/offline_stores/contrib/mongodb_offline_store/mongodb.py new file mode 100644 index 00000000000..76aa173cb95 --- /dev/null +++ b/sdk/python/feast/infra/offline_stores/contrib/mongodb_offline_store/mongodb.py @@ -0,0 +1,1067 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +MongoDB Offline Store. + +Single-collection schema. Key optimizations: + +1. Server-side deduplication (scoring path): when entity_df has unique + entity IDs the aggregation adds a ``$group`` stage that returns at most + one document per (entity_id, feature_view) pair — O(N×K) transfer + instead of O(N×P×K). The compound index backs the entire pipeline, + making per-entity cost O(log P) rather than O(P). + + For training data (repeated entity IDs at different timestamps) the + ``$group`` optimisation is skipped and ``merge_asof`` is used instead, + + +Index (created lazily on first use):: + + (entity_id ASC, feature_view ASC, event_timestamp DESC, created_at DESC) +""" + +from collections import defaultdict +from datetime import datetime, timezone +from typing import ( + Any, + Callable, + Dict, + Generator, + List, + Optional, + Set, + Tuple, + Union, +) + +import pandas as pd +import pyarrow + +try: + from pymongo import ASCENDING, DESCENDING, MongoClient +except ImportError: + MongoClient = None # type: ignore[assignment,misc] + +from pydantic import StrictStr + +from feast.data_source import DataSource +from feast.errors import ( + DataSourceNoNameException, + FeastExtrasDependencyImportError, + SavedDatasetLocationAlreadyExists, +) +from feast.feature_view import FeatureView +from feast.infra.key_encoding_utils import deserialize_entity_key, serialize_entity_key +from feast.infra.offline_stores.offline_store import ( + OfflineStore, + RetrievalJob, + RetrievalMetadata, +) +from feast.infra.offline_stores.offline_utils import ( + get_expected_join_keys, + infer_event_timestamp_from_entity_df, +) +from feast.infra.registry.base_registry import BaseRegistry +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import FeastConfigBaseModel, RepoConfig +from feast.saved_dataset import SavedDatasetStorage +from feast.type_map import mongodb_to_feast_value_type +from feast.value_type import ValueType + +from . import DRIVER_METADATA + +# Cache: avoid re-creating the compound index on every call +_indexes_ensured: Set[str] = set() + +# Chunk sizes — exposed at module level so tests can patch them. +_CHUNK_SIZE = 50_000 +_MONGO_BATCH_SIZE = 10_000 + + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- + + +class MongoDBOfflineStoreConfig(FeastConfigBaseModel): + """Configuration for the MongoDB offline store (single shared collection).""" + + type: StrictStr = "feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb.MongoDBOfflineStore" + + connection_string: StrictStr = "mongodb://localhost:27017" + """MongoDB connection URI""" + + database: StrictStr = "feast" + """MongoDB database name""" + + collection: StrictStr = "feature_history" + """Single collection shared by all feature views""" + + +# --------------------------------------------------------------------------- +# Data source +# --------------------------------------------------------------------------- + + +class MongoDBSource(DataSource): + """Data source for the MongoDB offline store. + + The ``name`` field is used as the ``feature_view`` discriminator inside + the single shared collection. + """ + + def __init__( + self, + name: Optional[str] = None, + timestamp_field: str = "event_timestamp", + created_timestamp_column: str = "created_at", + field_mapping: Optional[Dict[str, str]] = None, + description: Optional[str] = "", + tags: Optional[Dict[str, str]] = None, + owner: Optional[str] = "", + ): + if name is None: + raise DataSourceNoNameException() + super().__init__( + name=name, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + field_mapping=field_mapping or {}, + description=description, + tags=tags or {}, + owner=owner, + ) + + @property + def feature_view_name(self) -> str: + return self.name + + def source_type(self) -> DataSourceProto.SourceType.ValueType: + return DataSourceProto.CUSTOM_SOURCE + + def validate(self, config: RepoConfig) -> None: + pass + + @staticmethod + def from_proto(data_source: DataSourceProto) -> "MongoDBSource": + assert data_source.HasField("custom_options") + return MongoDBSource( + name=data_source.name, + timestamp_field=data_source.timestamp_field, + created_timestamp_column=data_source.created_timestamp_column, + field_mapping=dict(data_source.field_mapping), + description=data_source.description, + tags=dict(data_source.tags), + owner=data_source.owner, + ) + + def _to_proto_impl(self) -> DataSourceProto: + import json + + return DataSourceProto( + name=self.name, + type=DataSourceProto.CUSTOM_SOURCE, + data_source_class_type="feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb.MongoDBSource", + field_mapping=self.field_mapping, + custom_options=DataSourceProto.CustomSourceOptions( + configuration=json.dumps({"feature_view": self.name}).encode() + ), + description=self.description, + tags=self.tags, + owner=self.owner, + timestamp_field=self.timestamp_field, + created_timestamp_column=self.created_timestamp_column, + ) + + def get_table_query_string(self) -> str: + return self.name + + def get_table_column_names_and_types( + self, config: RepoConfig + ) -> List[Tuple[str, str]]: + """Infer column names and types by reading a sample document from MongoDB.""" + if MongoClient is None: + raise FeastExtrasDependencyImportError("pymongo", "mongodb") + client: Any = MongoClient( + config.offline_store.connection_string, driver=DRIVER_METADATA + ) + try: + coll = client[config.offline_store.database][ + config.offline_store.collection + ] + doc = coll.find_one({"feature_view": self.feature_view_name}) + if doc is None: + return [] + result: List[Tuple[str, str]] = [] + # Entity key is binary — join keys are inferred from the FeatureView, + # not from the document. Expose event_timestamp and created_at. + if "event_timestamp" in doc: + result.append(("event_timestamp", "datetime")) + if "created_at" in doc: + result.append(("created_at", "datetime")) + features = doc.get("features", {}) + if isinstance(features, dict): + for k, v in features.items(): + if isinstance(v, bool): + result.append((k, "bool")) + elif isinstance(v, int): + result.append((k, "int64")) + elif isinstance(v, float): + result.append((k, "float64")) + elif isinstance(v, str): + result.append((k, "string")) + elif isinstance(v, list): + result.append((k, "list")) + elif isinstance(v, dict): + result.append((k, "dict")) + else: + result.append((k, "object")) + return result + finally: + client.close() + + @staticmethod + def source_datatype_to_feast_value_type() -> Callable[[str], ValueType]: + return mongodb_to_feast_value_type + + +# --------------------------------------------------------------------------- +# Retrieval job +# --------------------------------------------------------------------------- + + +class MongoDBRetrievalJob(RetrievalJob): + def __init__( + self, + query_fn: Callable[[], pyarrow.Table], + full_feature_names: bool, + config: RepoConfig, + metadata: Optional[RetrievalMetadata] = None, + ): + self._query_fn = query_fn + self._full_feature_names = full_feature_names + self._config = config + self._metadata = metadata + + @property + def full_feature_names(self) -> bool: + return self._full_feature_names + + @property + def on_demand_feature_views(self) -> List[Any]: + return [] + + @property + def metadata(self) -> Optional[RetrievalMetadata]: + return self._metadata + + def _to_arrow_internal(self, timeout: Optional[int] = None) -> pyarrow.Table: + return self._query_fn() + + def persist( + self, + storage: SavedDatasetStorage, + allow_overwrite: bool = False, + timeout: Optional[int] = None, + ) -> None: + import os + + from feast.infra.offline_stores.file_source import SavedDatasetFileStorage + + if isinstance(storage, SavedDatasetFileStorage): + path = storage.file_options.uri + elif hasattr(storage, "path"): + path = storage.path # type: ignore[union-attr] + else: + raise ValueError( + f"MongoDBRetrievalJob.persist does not support " + f"{type(storage).__name__!r}. Use SavedDatasetFileStorage." + ) + + if not allow_overwrite and os.path.exists(path): + raise SavedDatasetLocationAlreadyExists(location=path) + self.to_df().to_parquet(path) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _fetch_documents( + client: Any, db_name: str, collection_name: str, pipeline: List[Dict] +) -> List[Dict]: + db = client[db_name] + return list(db[collection_name].aggregate(pipeline)) + + +def _serialize_entity_key_from_row( + row: pd.Series, + join_keys: List[str], + entity_key_version: int, + join_key_types: Dict[str, ValueType], +) -> bytes: + entity_key = EntityKeyProto() + for jk in sorted(join_keys): + val = row[jk] + entity_key.join_keys.append(jk) + proto_val = ValueProto() + vtype = join_key_types.get(jk, ValueType.UNKNOWN) + if vtype == ValueType.INT32: + proto_val.int32_val = int(val) + elif vtype == ValueType.INT64 or isinstance(val, int): + proto_val.int64_val = int(val) + elif vtype == ValueType.STRING or isinstance(val, str): + proto_val.string_val = str(val) + elif isinstance(val, float): + proto_val.double_val = float(val) + else: + proto_val.int64_val = int(val) + entity_key.entity_values.append(proto_val) + return serialize_entity_key(entity_key, entity_key_version) + + +def _expand_entity_id_column( + df: pd.DataFrame, + join_key_columns: List[str], + entity_key_version: int, +) -> pd.DataFrame: + """Deserialize ``entity_id`` bytes into individual join key columns. + + Each ``entity_id`` value is deserialized via ``deserialize_entity_key`` + and the resulting join key names/values are added as new columns. + The ``entity_id`` column is then dropped. + """ + if "entity_id" not in df.columns or df.empty: + return df + + def _extract(eid_bytes: bytes) -> Dict[str, Any]: + ek = deserialize_entity_key(eid_bytes, entity_key_version) + row: Dict[str, Any] = {} + for key, val in zip(ek.join_keys, ek.entity_values): + which = val.WhichOneof("val") + row[key] = getattr(val, which) if which else None + return row + + expanded = pd.DataFrame([_extract(eid) for eid in df["entity_id"]]) + # Only keep the columns the caller asked for (in case the serialized + # key contains more keys than expected). + for col in join_key_columns: + if col in expanded.columns: + df[col] = expanded[col].values + df = df.drop(columns=["entity_id"], errors="ignore") + return df + + +# --------------------------------------------------------------------------- +# Offline store +# --------------------------------------------------------------------------- + + +class MongoDBOfflineStore(OfflineStore): + """MongoDB offline store using a single collection and grouped aggregation. + + Key optimizations: + - Collapsing K feature-view queries into one aggregation per join-key group + - Using server-side ``$group`` (O(log P) with index) for the scoring path + """ + + @staticmethod + def _ensure_indexes(client: Any, db_name: str, collection_name: str) -> None: + """Create the compound index that enables O(log P) per-entity lookups.""" + collection = client[db_name][collection_name] + target_key = [ + ("entity_id", ASCENDING), + ("feature_view", ASCENDING), + ("event_timestamp", DESCENDING), + ("created_at", DESCENDING), + ] + existing = collection.index_information() + for idx_info in existing.values(): + if idx_info.get("key") == target_key: + return + collection.create_index(target_key, name="entity_fv_ts_idx", background=True) + + @staticmethod + def _get_client_and_ensure_indexes(config: RepoConfig) -> Any: + if MongoClient is None: + raise FeastExtrasDependencyImportError("pymongo", "mongodb") + conn_str = config.offline_store.connection_string + db_name = config.offline_store.database + collection = config.offline_store.collection + cache_key = f"{conn_str}/{db_name}/{collection}" + client: Any = MongoClient(conn_str, driver=DRIVER_METADATA) + if cache_key not in _indexes_ensured: + MongoDBOfflineStore._ensure_indexes(client, db_name, collection) + _indexes_ensured.add(cache_key) + return client + + @staticmethod + def pull_latest_from_table_or_query( + config: RepoConfig, + data_source: DataSource, + join_key_columns: List[str], + feature_name_columns: List[str], + timestamp_field: str, + created_timestamp_column: Optional[str], + start_date: datetime, + end_date: datetime, + ) -> RetrievalJob: + if not isinstance(data_source, MongoDBSource): + raise ValueError( + f"MongoDBOfflineStore expected MongoDBSource, " + f"got {type(data_source).__name__!r}." + ) + db_name = config.offline_store.database + collection = config.offline_store.collection + feature_view_name = data_source.feature_view_name + start_utc = start_date.astimezone(tz=timezone.utc) + end_utc = end_date.astimezone(tz=timezone.utc) + + project_stage: Dict[str, Any] = { + "_id": 0, + "entity_id": "$doc.entity_id", + "event_timestamp": "$doc.event_timestamp", + } + if created_timestamp_column: + project_stage["created_at"] = "$doc.created_at" + for feat in feature_name_columns: + project_stage[feat] = f"$doc.features.{feat}" + + pipeline: List[Dict[str, Any]] = [ + { + "$match": { + "feature_view": feature_view_name, + "event_timestamp": {"$gte": start_utc, "$lte": end_utc}, + } + }, + {"$sort": {"entity_id": 1, "event_timestamp": -1, "created_at": -1}}, + {"$group": {"_id": "$entity_id", "doc": {"$first": "$$ROOT"}}}, + {"$project": project_stage}, + ] + + entity_key_version = config.entity_key_serialization_version + + def _run() -> pyarrow.Table: + client = MongoDBOfflineStore._get_client_and_ensure_indexes(config) + try: + docs = _fetch_documents(client, db_name, collection, pipeline) + if not docs: + return pyarrow.Table.from_pydict({}) + df = pd.DataFrame(docs) + df = _expand_entity_id_column(df, join_key_columns, entity_key_version) + if not df.empty and "event_timestamp" in df.columns: + if df["event_timestamp"].dt.tz is None: + df["event_timestamp"] = pd.to_datetime( + df["event_timestamp"], utc=True + ) + return pyarrow.Table.from_pandas(df, preserve_index=False) + finally: + client.close() + + return MongoDBRetrievalJob( + query_fn=_run, full_feature_names=False, config=config + ) + + @staticmethod + def pull_all_from_table_or_query( + config: RepoConfig, + data_source: DataSource, + join_key_columns: List[str], + feature_name_columns: List[str], + timestamp_field: str, + created_timestamp_column: Optional[str] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + ) -> RetrievalJob: + if not isinstance(data_source, MongoDBSource): + raise ValueError( + f"MongoDBOfflineStore expected MongoDBSource, " + f"got {type(data_source).__name__!r}." + ) + db_name = config.offline_store.database + collection = config.offline_store.collection + feature_view_name = data_source.feature_view_name + match_filter: Dict[str, Any] = {"feature_view": feature_view_name} + if start_date or end_date: + ts_filter: Dict[str, Any] = {} + if start_date: + ts_filter["$gte"] = start_date.astimezone(tz=timezone.utc) + if end_date: + ts_filter["$lte"] = end_date.astimezone(tz=timezone.utc) + match_filter["event_timestamp"] = ts_filter + project_stage: Dict[str, Any] = {"_id": 0, "entity_id": 1, "event_timestamp": 1} + if created_timestamp_column: + project_stage["created_at"] = 1 + for feat in feature_name_columns: + project_stage[feat] = f"$features.{feat}" + pipeline = [{"$match": match_filter}, {"$project": project_stage}] + entity_key_version = config.entity_key_serialization_version + + def _run() -> pyarrow.Table: + client = MongoDBOfflineStore._get_client_and_ensure_indexes(config) + try: + docs = _fetch_documents(client, db_name, collection, pipeline) + if not docs: + return pyarrow.Table.from_pydict({}) + df = pd.DataFrame(docs) + df = _expand_entity_id_column(df, join_key_columns, entity_key_version) + if not df.empty and "event_timestamp" in df.columns: + if df["event_timestamp"].dt.tz is None: + df["event_timestamp"] = pd.to_datetime( + df["event_timestamp"], utc=True + ) + return pyarrow.Table.from_pandas(df, preserve_index=False) + finally: + client.close() + + return MongoDBRetrievalJob( + query_fn=_run, full_feature_names=False, config=config + ) + + @staticmethod + def get_historical_features( + config: RepoConfig, + feature_views: List[FeatureView], + feature_refs: List[str], + entity_df: Union[pd.DataFrame, str], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + strict_pit: bool = True, + ) -> RetrievalJob: + """Fetch historical features using grouped aggregation. + + Groups feature views by join key signature so that FVs sharing the + same entity key are handled in a single MongoDB aggregation instead + of K separate queries. + + Scoring path (unique entity IDs in entity_df): + Uses ``$match + $sort + $group`` — server returns at most one + document per (entity_id, feature_view). The compound index + makes per-entity cost O(log P). Python post-filters the result. + + Training path (repeated entity IDs at different timestamps): + Omits ``$group`` and uses ``merge_asof`` in Python, matching + standard PIT behaviour. + + Args: + strict_pit: When True (default) features whose document timestamp + is strictly after the entity request timestamp are returned as + NULL — this is the safe training/evaluation default. Set to + False for real-time scoring where you want the most recent + observation even if it post-dates the nominal request time. + """ + if isinstance(entity_df, str): + raise ValueError( + "MongoDBOfflineStore does not support SQL entity_df strings." + ) + + db_name = config.offline_store.database + feature_collection = config.offline_store.collection + entity_key_version = config.entity_key_serialization_version + + entity_schema = dict(zip(entity_df.columns, entity_df.dtypes)) + event_timestamp_col = infer_event_timestamp_from_entity_df(entity_schema) + + # Feature refs use projection names (e.g. "origin:temperature") + fv_to_features: Dict[str, List[str]] = defaultdict(list) + for ref in feature_refs: + fv_name, feat_name = ref.split(":", 1) + fv_to_features[fv_name].append(feat_name) + + # All dicts keyed by projection name (name_to_use), not fv.name, + # because entity mapping creates multiple projections of the same FV. + fv_by_proj: Dict[str, FeatureView] = { + fv.projection.name_to_use(): fv for fv in feature_views + } + + # projection_name → MongoDB feature_view discriminator value + # (the data source name, NOT the FeatureView name) + fv_mongo_name: Dict[str, str] = {} + for fv in feature_views: + proj = fv.projection.name_to_use() + src = fv.batch_source + fv_mongo_name[proj] = ( + src.feature_view_name + if isinstance(src, MongoDBSource) + else getattr(src, "name", fv.name) + ) + + # projection_name → mapped join keys (as in entity_df columns) + fv_mapped_join_keys: Dict[str, List[str]] = { + fv.projection.name_to_use(): list( + get_expected_join_keys(project, [fv], registry) + ) + for fv in feature_views + } + + # projection_name → {mapped_key → original_key} + fv_reverse_jk: Dict[str, Dict[str, str]] = { + fv.projection.name_to_use(): { + v: k for k, v in fv.projection.join_key_map.items() + } + for fv in feature_views + } + + # projection_name → original join key types (keyed by original name) + fv_jk_types_original: Dict[str, Dict[str, ValueType]] = { + fv.projection.name_to_use(): { + ec.name: ec.dtype.to_value_type() for ec in fv.entity_columns + } + for fv in feature_views + } + + # projection_name → reverse field_mapping (feast_name → source_col_name) + fv_reverse_fm: Dict[str, Dict[str, str]] = {} + for fv in feature_views: + proj = fv.projection.name_to_use() + fm = fv.batch_source.field_mapping if fv.batch_source else {} + fv_reverse_fm[proj] = {v: k for k, v in fm.items()} if fm else {} + + CHUNK_SIZE = _CHUNK_SIZE + MONGO_BATCH_SIZE = _MONGO_BATCH_SIZE + + def _chunk_dataframe( + df: pd.DataFrame, size: int + ) -> Generator[pd.DataFrame, None, None]: + for i in range(0, len(df), size): + yield df.iloc[i : i + size] + + def _run_single(entity_subset_df: pd.DataFrame, coll: Any) -> pd.DataFrame: + result = entity_subset_df.copy() + if not pd.api.types.is_datetime64_any_dtype(result[event_timestamp_col]): + result[event_timestamp_col] = pd.to_datetime( + result[event_timestamp_col], utc=True + ) + elif result[event_timestamp_col].dt.tz is None: + result[event_timestamp_col] = pd.to_datetime( + result[event_timestamp_col], utc=True + ) + + max_ts = result[event_timestamp_col].max() + min_ts = result[event_timestamp_col].min() + + # Process each feature view projection independently. + # (Different projections of the same FV have different + # entity key mappings and must be handled separately.) + for proj_name, features in fv_to_features.items(): + fv = fv_by_proj.get(proj_name) + if fv is None: + for feat in features: + col = f"{proj_name}__{feat}" if full_feature_names else feat + result[col] = None + continue + + mongo_fv_name = fv_mongo_name[proj_name] + mapped_keys = fv_mapped_join_keys[proj_name] + reverse_jk = fv_reverse_jk[proj_name] + orig_key_types = fv_jk_types_original[proj_name] + reverse_fm = fv_reverse_fm[proj_name] + + # Serialize entity keys: read values from MAPPED columns in + # entity_df, but serialize with ORIGINAL join key names to + # match the bytes stored in MongoDB. + _mk = mapped_keys + _rjk = reverse_jk + _okt = orig_key_types + + def _ser(row, __mk=_mk, __rjk=_rjk, __okt=_okt): + ek = EntityKeyProto() + orig_keys = sorted([__rjk.get(m, m) for m in __mk]) + o2m = {__rjk.get(m, m): m for m in __mk} + for ok in orig_keys: + mk = o2m[ok] + val = row[mk] + ek.join_keys.append(ok) + pv = ValueProto() + vt = __okt.get(ok, ValueType.UNKNOWN) + if vt == ValueType.INT32: + pv.int32_val = int(val) + elif vt == ValueType.INT64 or isinstance(val, int): + pv.int64_val = int(val) + elif vt == ValueType.STRING or isinstance(val, str): + pv.string_val = str(val) + elif isinstance(val, float): + pv.double_val = float(val) + else: + pv.int64_val = int(val) + ek.entity_values.append(pv) + return serialize_entity_key(ek, entity_key_version) + + eid_col = f"_eid_{proj_name}" + result[eid_col] = result.apply(_ser, axis=1) + unique_eids = result[eid_col].unique().tolist() + + # Detect scoring vs training path per-FV. + # + # The scoring path ($group $first) requires unique entity + # IDs for THIS FV — otherwise $group discards rows that + # share an entity_id. + # + # When strict_pit=True, there is an additional constraint: + # all entity request timestamps must be identical. The + # $match uses $lte: max_ts which is correct only when + # every entity shares the same request time. When + # timestamps differ, $group may pick a doc that is after + # a specific entity's request time; the Python future_mask + # would null it, but the valid older doc was already + # discarded by $group. + # + # When strict_pit=False, there is no $lte filter and no + # future_mask — we want the globally latest doc per entity + # regardless of request time, so $group $first is always + # correct. + unique_entities = result[eid_col].nunique() == len(result) + scoring_path = unique_entities and ( + not strict_pit or result[event_timestamp_col].nunique() == 1 + ) + + # TTL filter — use min_ts for the lower bound so that + # documents needed for early entity rows are included. + # Per-row TTL enforcement happens in the merge_asof path. + fv_ttl = fv.ttl if fv else None + ts_filter: Dict[str, Any] = {"$lte": max_ts} if strict_pit else {} + if fv_ttl: + lower_ref = min_ts if strict_pit else datetime.now(tz=timezone.utc) + ts_filter["$gte"] = lower_ref - fv_ttl + + # Query MongoDB + all_docs: List[Dict] = [] + for i in range(0, len(unique_eids), MONGO_BATCH_SIZE): + batch_ids = unique_eids[i : i + MONGO_BATCH_SIZE] + match_q: Dict[str, Any] = { + "entity_id": {"$in": batch_ids}, + "feature_view": mongo_fv_name, + } + if ts_filter: + match_q["event_timestamp"] = ts_filter + + if scoring_path: + pipeline: List[Dict] = [ + {"$match": match_q}, + { + "$sort": { + "entity_id": 1, + "event_timestamp": -1, + "created_at": -1, + } + }, + { + "$group": { + "_id": "$entity_id", + "event_timestamp": {"$first": "$event_timestamp"}, + "features": {"$first": "$features"}, + "created_at": {"$first": "$created_at"}, + } + }, + { + "$project": { + "_id": 0, + "entity_id": "$_id", + "event_timestamp": 1, + "features": 1, + "created_at": 1, + } + }, + ] + else: + pipeline = [{"$match": match_q}] + + all_docs.extend(list(coll.aggregate(pipeline))) + + if not all_docs: + for feat in features: + col = f"{proj_name}__{feat}" if full_feature_names else feat + result[col] = None + result = result.drop(columns=[eid_col], errors="ignore") + continue + + fv_df = pd.DataFrame(all_docs) + fv_df = fv_df.rename(columns={"entity_id": eid_col}) + + # Extract features from nested dict, applying reverse field_mapping. + # Using .apply() instead of json_normalize preserves complex types + # (dicts for Map/Struct, lists for Array). + if "features" in fv_df.columns: + for feat in features: + src_col = reverse_fm.get(feat, feat) + fv_df[feat] = fv_df["features"].apply( + lambda d, _s=src_col: ( + d.get(_s) if isinstance(d, dict) else None + ) + ) + fv_df = fv_df.drop(columns=["features"]) + + if fv_df["event_timestamp"].dt.tz is None: + fv_df["event_timestamp"] = pd.to_datetime( + fv_df["event_timestamp"], utc=True + ) + + if scoring_path: + fv_join_cols = [eid_col, "event_timestamp"] + [ + f for f in features if f in fv_df.columns + ] + fv_join = fv_df[fv_join_cols].rename( + columns={"event_timestamp": "_fv_ts"} + ) + merged = result[[eid_col, event_timestamp_col]].merge( + fv_join, on=eid_col, how="left" + ) + if strict_pit: + future_mask = merged["_fv_ts"] > merged[event_timestamp_col] + else: + future_mask = pd.Series( + [False] * len(merged), index=merged.index + ) + if fv_ttl: + ttl_mask = merged["_fv_ts"] < ( + merged[event_timestamp_col] - fv_ttl + ) + bad_mask = future_mask | ttl_mask + else: + bad_mask = future_mask + for feat in features: + col = f"{proj_name}__{feat}" if full_feature_names else feat + vals = ( + merged[feat].copy() + if feat in merged.columns + else pd.Series([None] * len(merged), dtype=object) + ) + vals[bad_mask | merged["_fv_ts"].isna()] = None + result[col] = vals.values + else: + # merge_asof path (training data) + result = result.sort_values(event_timestamp_col).reset_index( + drop=True + ) + fv_df = fv_df.sort_values( + ["event_timestamp", "created_at"] + ).reset_index(drop=True) + merge_cols = [eid_col, "event_timestamp"] + [ + f for f in features if f in fv_df.columns + ] + fv_df_subset = fv_df[ + [c for c in merge_cols if c in fv_df.columns] + ].copy() + fv_df_subset = fv_df_subset.rename( + columns={"event_timestamp": "_fv_ts"} + ) + fv_prefix = f"__fv_{proj_name}__" + fv_df_subset = fv_df_subset.rename( + columns={ + f: f"{fv_prefix}{f}" + for f in features + if f in fv_df_subset.columns + } + ) + result = pd.merge_asof( + result, + fv_df_subset, + left_on=event_timestamp_col, + right_on="_fv_ts", + by=eid_col, + direction="backward", + ) + if fv_ttl: + cutoff = result[event_timestamp_col] - fv_ttl + stale = result["_fv_ts"] < cutoff + for feat in features: + tc = f"{fv_prefix}{feat}" + if tc in result.columns: + result.loc[stale, tc] = None + for feat in features: + tc = f"{fv_prefix}{feat}" + col = f"{proj_name}__{feat}" if full_feature_names else feat + if tc in result.columns: + if col in result.columns: + result = result.drop(columns=[col]) + result = result.rename(columns={tc: col}) + elif col not in result.columns: + result[col] = None + result = result.drop(columns=["_fv_ts"], errors="ignore") + + result = result.drop(columns=[eid_col], errors="ignore") + + return result + + def _run() -> pyarrow.Table: + working_df = entity_df.copy() + working_df["_row_idx"] = range(len(working_df)) + + client = MongoDBOfflineStore._get_client_and_ensure_indexes(config) + try: + coll = client[db_name][feature_collection] + if len(working_df) <= CHUNK_SIZE: + result_df = _run_single(working_df, coll) + else: + chunks = [ + _run_single(chunk, coll) + for chunk in _chunk_dataframe(working_df, CHUNK_SIZE) + ] + result_df = pd.concat(chunks, ignore_index=True) + finally: + client.close() + + result_df = result_df.sort_values("_row_idx").reset_index(drop=True) + result_df = result_df.drop(columns=["_row_idx"], errors="ignore") + + if not result_df.empty and event_timestamp_col in result_df.columns: + if result_df[event_timestamp_col].dt.tz is None: + result_df[event_timestamp_col] = pd.to_datetime( + result_df[event_timestamp_col], utc=True + ) + + return pyarrow.Table.from_pandas(result_df, preserve_index=False) + + return MongoDBRetrievalJob( + query_fn=_run, + full_feature_names=full_feature_names, + config=config, + ) + + @staticmethod + def offline_write_batch( + config: RepoConfig, + feature_view: FeatureView, + table: pyarrow.Table, + progress: Optional[Callable[[int], Any]], + ) -> None: + """Write a batch of feature observations into the feature_history collection. + + Each row in *table* is stored as one document:: + + { + "entity_id": , + "feature_view": , + "features": {: , ...}, + "event_timestamp": , + "created_at": , + } + + Writes are append-only (no upsert). Conflict resolution at read time: + pull_latest picks the highest ``created_at``; the scoring path + ``$sort created_at DESC`` → ``$group $first`` also picks the highest. + + Args: + config: Feast repo configuration. + feature_view: The feature view being written; must have a + MongoDBSource batch source. + table: Arrow table with join key columns, feature columns, + ``event_timestamp``, and optionally ``created_at``. + progress: Optional callback invoked with the row count after each + batch insert. + """ + if not isinstance(feature_view.batch_source, MongoDBSource): + raise ValueError( + f"MongoDBOfflineStore.offline_write_batch expected a MongoDBSource " + f"batch source, got {type(feature_view.batch_source).__name__!r}." + ) + + entity_key_version = config.entity_key_serialization_version + db_name = config.offline_store.database + collection_name = config.offline_store.collection + + # Use original (unmapped) join key names so that the serialized + # entity_id bytes match those produced by get_historical_features, + # which also serializes with original names (see _ser at line ~662). + join_key_types: Dict[str, ValueType] = { + ec.name: ec.dtype.to_value_type() for ec in feature_view.entity_columns + } + join_keys = list(join_key_types.keys()) + + timestamp_field = feature_view.batch_source.timestamp_field + created_ts_col: Optional[str] = ( + feature_view.batch_source.created_timestamp_column or None + ) + + reserved = set(join_keys) | {timestamp_field} + if created_ts_col: + reserved.add(created_ts_col) + feature_cols = [c for c in table.column_names if c not in reserved] + + df = table.to_pandas() + + for ts_col in [timestamp_field] + ([created_ts_col] if created_ts_col else []): + if ts_col in df.columns: + if not pd.api.types.is_datetime64_any_dtype(df[ts_col]): + df[ts_col] = pd.to_datetime(df[ts_col], utc=True) + elif df[ts_col].dt.tz is None: + df[ts_col] = df[ts_col].dt.tz_localize("UTC") + + df["_entity_id"] = df.apply( + lambda row: _serialize_entity_key_from_row( + row, join_keys, entity_key_version, join_key_types + ), + axis=1, + ) + + now = datetime.now(tz=timezone.utc) + + # Use the batch source name as the MongoDB discriminator so that + # data written via push/write_to_offline_store lands in the same + # collection partition as the initial ingest from create_data_source. + mongo_fv_name = feature_view.batch_source.feature_view_name + + docs = [] + for _, row in df.iterrows(): + features: Dict[str, Any] = {} + for col in feature_cols: + val = row[col] + try: + is_na = pd.isna(val) + if isinstance(is_na, bool) and is_na: + continue + except (ValueError, TypeError): + pass # non-scalar (list/array) — not NA + if hasattr(val, "item"): + val = val.item() + features[col] = val + + created_at = now + if created_ts_col and created_ts_col in df.columns: + ct = row[created_ts_col] + if not pd.isna(ct): + created_at = ( + ct.to_pydatetime() if hasattr(ct, "to_pydatetime") else ct + ) + + docs.append( + { + "entity_id": row["_entity_id"], + "feature_view": mongo_fv_name, + "features": features, + "event_timestamp": ( + row[timestamp_field].to_pydatetime() + if hasattr(row[timestamp_field], "to_pydatetime") + else row[timestamp_field] + ), + "created_at": created_at, + } + ) + + client = MongoDBOfflineStore._get_client_and_ensure_indexes(config) + try: + coll = client[db_name][collection_name] + BATCH_SIZE = 10_000 + for i in range(0, len(docs), BATCH_SIZE): + batch = docs[i : i + BATCH_SIZE] + coll.insert_many(batch, ordered=False) + if progress: + progress(len(batch)) + finally: + client.close() diff --git a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssql.py b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssql.py index 4821aa8dcb6..fd3e2cb4a34 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssql.py +++ b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssql.py @@ -118,6 +118,8 @@ class MsSqlServerOfflineStoreConfig(FeastConfigBaseModel): class MsSqlServerOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -151,6 +153,7 @@ def get_historical_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, ) -> RetrievalJob: # TODO avoid this conversion if type(entity_df) == str: @@ -168,6 +171,7 @@ def get_historical_features( data_source_reader=_build_data_source_reader(config), data_source_writer=_build_data_source_writer(config), event_expire_timestamp_fn=mssql_event_expire_timestamp_fn, + filter_by_created_timestamp=filter_by_created_timestamp, ) @staticmethod diff --git a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssqlserver_source.py b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssqlserver_source.py index ce1107730eb..7590f987ba7 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssqlserver_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/mssqlserver_source.py @@ -170,9 +170,7 @@ def __init__( def __eq__(self, other): if not isinstance(other, MsSqlServerSource): - raise TypeError( - "Comparisons should only involve SqlServerSource class objects." - ) + return False return ( self.name == other.name diff --git a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/tests/data_source.py index 9c87b8d7520..a2c89c11056 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/mssql_offline_store/tests/data_source.py @@ -15,7 +15,7 @@ MsSqlServerSource, ) from feast.saved_dataset import SavedDatasetStorage -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/mssql_repo_configuration.py b/sdk/python/feast/infra/offline_stores/contrib/mssql_repo_configuration.py index 50d636ba909..e64693b7494 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/mssql_repo_configuration.py +++ b/sdk/python/feast/infra/offline_stores/contrib/mssql_repo_configuration.py @@ -1,8 +1,8 @@ from feast.infra.offline_stores.contrib.mssql_offline_store.tests.data_source import ( MsSqlDataSourceCreator, ) -from tests.integration.feature_repos.repo_configuration import REDIS_CONFIG -from tests.integration.feature_repos.universal.online_store.redis import ( +from tests.universal.feature_repos.repo_configuration import REDIS_CONFIG +from tests.universal.feature_repos.universal.online_store.redis import ( RedisOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/oracle_offline_store/__init__.py b/sdk/python/feast/infra/offline_stores/contrib/oracle_offline_store/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/sdk/python/feast/infra/offline_stores/contrib/oracle_offline_store/__init__.py @@ -0,0 +1 @@ + diff --git a/sdk/python/feast/infra/offline_stores/contrib/oracle_offline_store/oracle.py b/sdk/python/feast/infra/offline_stores/contrib/oracle_offline_store/oracle.py new file mode 100644 index 00000000000..0aa657c69c9 --- /dev/null +++ b/sdk/python/feast/infra/offline_stores/contrib/oracle_offline_store/oracle.py @@ -0,0 +1,904 @@ +import json +from datetime import date, datetime, timezone +from pathlib import Path +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union + +import ibis +import pandas as pd +import pyarrow +from ibis.expr.types import Table +from pydantic import StrictInt, StrictStr, model_validator + +from feast.data_source import DataSource +from feast.feature_logging import LoggingConfig, LoggingSource +from feast.feature_view import FeatureView +from feast.infra.offline_stores.contrib.oracle_offline_store.oracle_source import ( + OracleSource, +) +from feast.infra.offline_stores.ibis import ( + get_historical_features_ibis, + offline_write_batch_ibis, + pull_all_from_table_or_query_ibis, + pull_latest_from_table_or_query_ibis, + write_logged_features_ibis, +) +from feast.infra.offline_stores.offline_store import OfflineStore, RetrievalJob +from feast.infra.offline_stores.offline_utils import get_timestamp_filter_sql +from feast.infra.registry.base_registry import BaseRegistry +from feast.monitoring.monitoring_utils import ( + MON_TABLE_FEATURE, + MON_TABLE_FEATURE_SERVICE, + MON_TABLE_FEATURE_VIEW, + MON_TABLE_JOB, + empty_categorical_metric, + empty_numeric_metric, + monitoring_table_meta, + normalize_monitoring_row, + opt_float, +) +from feast.repo_config import FeastConfigBaseModel, RepoConfig +from feast.utils import compute_non_entity_date_range + + +def get_ibis_connection(config: RepoConfig): + """Create an ibis Oracle connection from the offline store config.""" + offline_config = config.offline_store + assert isinstance(offline_config, OracleOfflineStoreConfig) + + kwargs = {} + if offline_config.service_name: + kwargs["service_name"] = offline_config.service_name + if offline_config.sid: + kwargs["sid"] = offline_config.sid + if offline_config.database: + kwargs["database"] = offline_config.database + if offline_config.dsn: + kwargs["dsn"] = offline_config.dsn + + return ibis.oracle.connect( + user=offline_config.user, + password=offline_config.password, + host=offline_config.host, + port=offline_config.port, + **kwargs, + ) + + +def _read_oracle_table(con, data_source: DataSource) -> Table: + """Read an Oracle table via ibis. + + Column names are returned exactly as Oracle stores them. The user is + expected to reference columns using the same casing shown by Oracle + (e.g. ``USER_ID`` for unquoted identifiers, ``CamelCase`` for quoted). + """ + assert isinstance(data_source, OracleSource) + table = con.table(data_source.table_ref) + + # Cast Oracle DATE columns (ibis date → timestamp) to preserve time. + casts = {} + for col_name in table.columns: + if table[col_name].type().is_date(): + casts[col_name] = table[col_name].cast("timestamp") + if casts: + table = table.mutate(**casts) + + return table + + +def _build_data_source_reader(config: RepoConfig, con=None): + """Build a reader that returns Oracle-backend ibis tables.""" + if con is None: + con = get_ibis_connection(config) + + def _read_data_source(data_source: DataSource, repo_path: str = "") -> Table: + return _read_oracle_table(con, data_source) + + return _read_data_source + + +def _build_data_source_writer(config: RepoConfig, con=None): + """Build a function that writes data to an Oracle table via ibis.""" + if con is None: + con = get_ibis_connection(config) + + def _write_data_source( + table: Table, + data_source: DataSource, + repo_path: str = "", + mode: str = "append", + allow_overwrite: bool = False, + ): + assert isinstance(data_source, OracleSource) + table_ref = data_source.table_ref + + if mode == "overwrite": + if not allow_overwrite: + raise ValueError( + f"Table '{table_ref}' already exists. " + f"Set allow_overwrite=True to truncate and replace data." + ) + con.truncate_table(table_ref) + + con.insert(table_name=table_ref, obj=table.to_pandas()) + + return _write_data_source + + +class OracleOfflineStoreConfig(FeastConfigBaseModel): + """Offline store config for Oracle Database""" + + type: Literal["oracle"] = "oracle" + """Offline store type selector""" + + user: StrictStr + """Oracle database user""" + + password: StrictStr + """Oracle database password""" + + host: StrictStr = "localhost" + """Oracle database host""" + + port: StrictInt = 1521 + """Oracle database port""" + + service_name: Optional[StrictStr] = None + """Oracle service name (mutually exclusive with sid and dsn)""" + + sid: Optional[StrictStr] = None + """Oracle SID (mutually exclusive with service_name and dsn)""" + + database: Optional[StrictStr] = None + """Oracle database name""" + + dsn: Optional[StrictStr] = None + """Oracle DSN string (mutually exclusive with service_name and sid)""" + + @model_validator(mode="after") + def _validate_connection_params(self): + exclusive = [ + f for f in ("service_name", "sid", "dsn") if getattr(self, f) is not None + ] + if len(exclusive) > 1: + raise ValueError( + f"Only one of 'service_name', 'sid', or 'dsn' may be set, " + f"but got: {', '.join(exclusive)}" + ) + return self + + +def _build_entity_df_from_feature_sources( + con, + feature_views: List[FeatureView], + start_date: datetime, + end_date: datetime, +) -> pd.DataFrame: + """Build a synthetic entity DataFrame by scanning each feature source within a date range.""" + entity_dfs = [] + for fv in feature_views: + source = fv.batch_source + assert source is not None, f"Feature view '{fv.name}' has no batch_source" + table = _read_oracle_table(con, source) + ts_col = source.timestamp_field + join_keys = [e.name for e in fv.entity_columns] + cols = join_keys + [ts_col] + sub = table.filter( + (table[ts_col] >= ibis.literal(start_date)) + & (table[ts_col] <= ibis.literal(end_date)) + ).select(cols) + sub = sub.rename({"event_timestamp": ts_col}) + entity_dfs.append(sub.execute()) + + return pd.concat(entity_dfs, ignore_index=True).drop_duplicates() + + +# ------------------------------------------------------------------ # +# Oracle monitoring helpers +# ------------------------------------------------------------------ # + + +def _oracle_quote_ident(name: str) -> str: + return f'"{name}"' + + +def _oracle_ts_where(ts_filter: str) -> str: + return f"({ts_filter})" if (ts_filter and ts_filter.strip()) else "1=1" + + +def _oracle_fetchall(con, sql: str): + cur = con.raw_sql(sql) + try: + return cur.fetchall() + finally: + if hasattr(cur, "close"): + cur.close() + + +def _oracle_exec(con, sql: str) -> None: + cur = con.raw_sql(sql) + try: + pass + finally: + if hasattr(cur, "close"): + cur.close() + + +def _oracle_numeric_histogram( + con, + from_expression: str, + col_name: str, + ts_filter: str, + bins: int, + min_val: float, + max_val: float, +) -> Dict[str, Any]: + q_col = _oracle_quote_ident(col_name) + + if min_val == max_val: + tw = _oracle_ts_where(ts_filter) + cnt_row = _oracle_fetchall( + con, + f"SELECT COUNT(*) FROM {from_expression} _src " + f"WHERE {q_col} IS NOT NULL AND {tw}", + ) + cnt = (cnt_row[0][0] if cnt_row else 0) or 0 + return {"bins": [min_val, max_val], "counts": [cnt], "bin_width": 0.0} + + upper = max_val + (max_val - min_val) * 1e-10 + bin_width = (max_val - min_val) / bins + bw = bin_width if bin_width != 0 else 1e-300 + + tw = _oracle_ts_where(ts_filter) + query = ( + f"SELECT bucket, COUNT(*) AS cnt FROM (" + f" SELECT " + f" CASE WHEN {q_col} IS NULL THEN NULL " + f" WHEN {min_val} = {upper} THEN 1 " + f" ELSE LEAST(GREATEST(" + f" FLOOR((CAST({q_col} AS NUMBER) - {min_val}) / {bw}) + 1, " + f" 1), {bins}) " + f" END AS bucket " + f" FROM {from_expression} _src " + f" WHERE {q_col} IS NOT NULL AND {tw}" + f") sub " + f"WHERE bucket IS NOT NULL " + f"GROUP BY bucket ORDER BY bucket" + ) + + rows = _oracle_fetchall(con, query) + counts = [0] * bins + for bucket, cnt in rows: + b = int(bucket) + if 1 <= b <= bins: + counts[b - 1] = cnt + + bin_edges = [min_val + i * bin_width for i in range(bins + 1)] + return { + "bins": [float(b) for b in bin_edges], + "counts": counts, + "bin_width": float(bin_width), + } + + +def _oracle_numeric_stats( + con, + from_expression: str, + feature_names: List[str], + ts_filter: str, + histogram_bins: int, +) -> List[Dict[str, Any]]: + select_parts = ["COUNT(*)"] + for col in feature_names: + q = _oracle_quote_ident(col) + c = f"CAST({q} AS NUMBER)" + select_parts.extend( + [ + f"COUNT({q})", + f"AVG({c})", + f"STDDEV_SAMP({c})", + f"MIN({c})", + f"MAX({c})", + f"PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY {c})", + ] + ) + + tw = _oracle_ts_where(ts_filter) + query = f"SELECT {', '.join(select_parts)} FROM {from_expression} _src WHERE {tw}" + + row = (_oracle_fetchall(con, query) or [None])[0] + + if row is None: + return [empty_numeric_metric(n) for n in feature_names] + + row_count = row[0] + results: List[Dict[str, Any]] = [] + + for i, col in enumerate(feature_names): + base = 1 + i * 10 + non_null = row[base] or 0 + null_count = row_count - non_null + + min_val = opt_float(row[base + 3]) + max_val = opt_float(row[base + 4]) + + result: Dict[str, Any] = { + "feature_name": col, + "feature_type": "numeric", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": opt_float(row[base + 1]), + "stddev": opt_float(row[base + 2]), + "min_val": min_val, + "max_val": max_val, + "p50": opt_float(row[base + 5]), + "p75": opt_float(row[base + 6]), + "p90": opt_float(row[base + 7]), + "p95": opt_float(row[base + 8]), + "p99": opt_float(row[base + 9]), + "histogram": None, + } + + if min_val is not None and max_val is not None and non_null > 0: + result["histogram"] = _oracle_numeric_histogram( + con, + from_expression, + col, + ts_filter, + histogram_bins, + min_val, + max_val, + ) + + results.append(result) + + return results + + +def _oracle_categorical_stats( + con, + from_expression: str, + col_name: str, + ts_filter: str, + top_n: int, +) -> Dict[str, Any]: + q_col = _oracle_quote_ident(col_name) + + tw = _oracle_ts_where(ts_filter) + query = ( + f"WITH filtered AS (" + f" SELECT * FROM {from_expression} _src WHERE {tw}" + f") " + f"SELECT " + f" (SELECT COUNT(*) FROM filtered) AS row_count, " + f" (SELECT COUNT(*) - COUNT({q_col}) FROM filtered) AS null_count, " + f" (SELECT COUNT(DISTINCT {q_col}) FROM filtered " + f" WHERE {q_col} IS NOT NULL) AS unique_count, " + f" TO_CHAR({q_col}) AS value, COUNT(*) AS cnt " + f"FROM filtered WHERE {q_col} IS NOT NULL " + f"GROUP BY {q_col} " + f"ORDER BY cnt DESC " + f"FETCH FIRST {int(top_n)} ROWS ONLY" + ) + + rows = _oracle_fetchall(con, query) + + if not rows: + return empty_categorical_metric(col_name) + + row_count = rows[0][0] + null_count = rows[0][1] + unique_count = rows[0][2] + + top_entries = [{"value": r[3], "count": r[4]} for r in rows] + top_total = sum(e["count"] for e in top_entries) + other_count = (row_count - null_count) - top_total + + return { + "feature_name": col_name, + "feature_type": "categorical", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": { + "values": top_entries, + "other_count": max(other_count, 0), + "unique_count": unique_count, + }, + } + + +def _oracle_escape_literal(val: Any) -> str: + if val is None: + return "NULL" + if isinstance(val, bool): + return "1" if val else "0" + if isinstance(val, (int, float)) and not isinstance(val, bool): + return str(val) + if isinstance(val, datetime): + s = val.isoformat(sep=" ", timespec="seconds") + return f"TIMESTAMP '{s}'" + if isinstance(val, date): + return f"DATE '{val.isoformat()}'" + s = str(val).replace("'", "''") + return f"'{s}'" + + +def _oracle_merge_metric_row( + con, table: str, columns: List[str], pk_cols: List[str], row: Dict[str, Any] +) -> None: + def qid(c: str) -> str: + return f'"{c}"' + + non_pk = [c for c in columns if c not in pk_cols] + vals = [] + for c in columns: + v = row.get(c) + if c == "histogram" and v is not None and not isinstance(v, str): + v = json.dumps(v) + vals.append(_oracle_escape_literal(v)) + + join_cond = " AND ".join(f"t.{qid(c)} = s.{qid(c)}" for c in pk_cols) + insert_cols = ", ".join(qid(c) for c in columns) + insert_vals = ", ".join(f"s.{qid(c)}" for c in columns) + update_set = ", ".join(f"t.{qid(c)} = s.{qid(c)}" for c in non_pk) + + src_select = ", ".join( + f"{vals[i]} AS {qid(columns[i])}" for i in range(len(columns)) + ) + + sql = ( + f"MERGE INTO {table} t " + f"USING (SELECT {src_select} FROM dual) s " + f"ON ({join_cond}) " + f"WHEN MATCHED THEN UPDATE SET {update_set} " + f"WHEN NOT MATCHED THEN INSERT ({insert_cols}) VALUES ({insert_vals})" + ) + _oracle_exec(con, sql) + + +def _oracle_try_execute_ddl(con, ddl: str) -> None: + """Run DDL; ignore ORA-00955 (name already used).""" + escaped = ddl.strip().replace("'", "''") + plsql = ( + "BEGIN\n" + f" EXECUTE IMMEDIATE '{escaped}';\n" + "EXCEPTION\n" + " WHEN OTHERS THEN\n" + " IF SQLCODE != -955 THEN RAISE;\n" + " END IF;\n" + "END;" + ) + _oracle_exec(con, plsql) + + +class OracleOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + + @staticmethod + def pull_latest_from_table_or_query( + config: RepoConfig, + data_source: DataSource, + join_key_columns: List[str], + feature_name_columns: List[str], + timestamp_field: str, + created_timestamp_column: Optional[str], + start_date: datetime, + end_date: datetime, + ) -> RetrievalJob: + con = get_ibis_connection(config) + + return pull_latest_from_table_or_query_ibis( + config=config, + data_source=data_source, + join_key_columns=join_key_columns, + feature_name_columns=feature_name_columns, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + start_date=start_date, + end_date=end_date, + data_source_reader=_build_data_source_reader(config, con=con), + data_source_writer=_build_data_source_writer(config, con=con), + ) + + @staticmethod + def get_historical_features( + config: RepoConfig, + feature_views: List[FeatureView], + feature_refs: List[str], + entity_df: Optional[Union[pd.DataFrame, str]], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, + **kwargs, + ) -> RetrievalJob: + if not feature_views: + raise ValueError("feature_views must not be empty") + + # Single connection reused across the entire call. + con = get_ibis_connection(config) + + # Handle non-entity retrieval mode (start_date/end_date only) + if entity_df is None: + start_date, end_date = compute_non_entity_date_range( + feature_views, + start_date=kwargs.get("start_date"), + end_date=kwargs.get("end_date"), + ) + entity_df = _build_entity_df_from_feature_sources( + con, feature_views, start_date, end_date + ) + + # If entity_df is a SQL string, execute it to get a DataFrame + if isinstance(entity_df, str): + entity_df = con.sql(entity_df).execute() + + return get_historical_features_ibis( + config=config, + feature_views=feature_views, + feature_refs=feature_refs, + entity_df=entity_df, + registry=registry, + project=project, + full_feature_names=full_feature_names, + data_source_reader=_build_data_source_reader(config, con=con), + data_source_writer=_build_data_source_writer(config, con=con), + filter_by_created_timestamp=filter_by_created_timestamp, + ) + + @staticmethod + def pull_all_from_table_or_query( + config: RepoConfig, + data_source: DataSource, + join_key_columns: List[str], + feature_name_columns: List[str], + timestamp_field: str, + created_timestamp_column: Optional[str] = None, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + ) -> RetrievalJob: + con = get_ibis_connection(config) + + return pull_all_from_table_or_query_ibis( + config=config, + data_source=data_source, + join_key_columns=join_key_columns, + feature_name_columns=feature_name_columns, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + start_date=start_date, + end_date=end_date, + data_source_reader=_build_data_source_reader(config, con=con), + data_source_writer=_build_data_source_writer(config, con=con), + ) + + @staticmethod + def offline_write_batch( + config: RepoConfig, + feature_view: FeatureView, + table: pyarrow.Table, + progress: Optional[Callable[[int], Any]], + ): + offline_write_batch_ibis( + config=config, + feature_view=feature_view, + table=table, + progress=progress, + data_source_writer=_build_data_source_writer(config), + ) + + @staticmethod + def write_logged_features( + config: RepoConfig, + data: Union[pyarrow.Table, Path], + source: LoggingSource, + logging_config: LoggingConfig, + registry: BaseRegistry, + ): + write_logged_features_ibis( + config=config, + data=data, + source=source, + logging_config=logging_config, + registry=registry, + ) + + @staticmethod + def compute_monitoring_metrics( + config: RepoConfig, + data_source: DataSource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, OracleOfflineStoreConfig) + assert isinstance(data_source, OracleSource) + + from_expression = data_source.get_table_query_string() + ts_filter = get_timestamp_filter_sql( + start_date, + end_date, + timestamp_field, + tz=timezone.utc, + cast_style="timestamp", + date_time_separator=" ", + ) + + numeric_features = [n for n, t in feature_columns if t == "numeric"] + categorical_features = [n for n, t in feature_columns if t == "categorical"] + results: List[Dict[str, Any]] = [] + + con = get_ibis_connection(config) + + if numeric_features: + results.extend( + _oracle_numeric_stats( + con, + from_expression, + numeric_features, + ts_filter, + histogram_bins, + ) + ) + + for col_name in categorical_features: + results.append( + _oracle_categorical_stats( + con, + from_expression, + col_name, + ts_filter, + top_n, + ) + ) + + return results + + @staticmethod + def get_monitoring_max_timestamp( + config: RepoConfig, + data_source: DataSource, + timestamp_field: str, + ) -> Optional[datetime]: + assert isinstance(config.offline_store, OracleOfflineStoreConfig) + assert isinstance(data_source, OracleSource) + + from_expression = data_source.get_table_query_string() + ts_col = _oracle_quote_ident(timestamp_field) + + con = get_ibis_connection(config) + rows = _oracle_fetchall( + con, + f"SELECT MAX({ts_col}) FROM {from_expression} _src", + ) + row = rows[0] if rows else None + + if row is None or row[0] is None: + return None + val = row[0] + if isinstance(val, datetime): + return val if val.tzinfo else val.replace(tzinfo=timezone.utc) + if isinstance(val, date): + return datetime.combine(val, datetime.min.time(), tzinfo=timezone.utc) + return None + + @staticmethod + def ensure_monitoring_tables(config: RepoConfig) -> None: + assert isinstance(config.offline_store, OracleOfflineStoreConfig) + con = get_ibis_connection(config) + + _oracle_try_execute_ddl( + con, + f""" + CREATE TABLE {MON_TABLE_FEATURE} ( + project_id VARCHAR2(255) NOT NULL, + feature_view_name VARCHAR2(255) NOT NULL, + feature_name VARCHAR2(255) NOT NULL, + metric_date DATE NOT NULL, + granularity VARCHAR2(20) DEFAULT 'daily' NOT NULL, + data_source_type VARCHAR2(50) DEFAULT 'batch' NOT NULL, + computed_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL, + is_baseline NUMBER(1) DEFAULT 0 NOT NULL, + feature_type VARCHAR2(50) NOT NULL, + row_count NUMBER, + null_count NUMBER, + null_rate NUMBER, + mean NUMBER, + stddev NUMBER, + min_val NUMBER, + max_val NUMBER, + p50 NUMBER, + p75 NUMBER, + p90 NUMBER, + p95 NUMBER, + p99 NUMBER, + histogram VARCHAR2(4000), + CONSTRAINT pk_fm PRIMARY KEY (project_id, feature_view_name, + feature_name, metric_date, granularity, data_source_type) + ) + """, + ) + + _oracle_try_execute_ddl( + con, + f""" + CREATE TABLE {MON_TABLE_FEATURE_VIEW} ( + project_id VARCHAR2(255) NOT NULL, + feature_view_name VARCHAR2(255) NOT NULL, + metric_date DATE NOT NULL, + granularity VARCHAR2(20) DEFAULT 'daily' NOT NULL, + data_source_type VARCHAR2(50) DEFAULT 'batch' NOT NULL, + computed_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL, + is_baseline NUMBER(1) DEFAULT 0 NOT NULL, + total_row_count NUMBER, + total_features NUMBER, + features_with_nulls NUMBER, + avg_null_rate NUMBER, + max_null_rate NUMBER, + CONSTRAINT pk_fvm PRIMARY KEY (project_id, feature_view_name, + metric_date, granularity, data_source_type) + ) + """, + ) + + _oracle_try_execute_ddl( + con, + f""" + CREATE TABLE {MON_TABLE_FEATURE_SERVICE} ( + project_id VARCHAR2(255) NOT NULL, + feature_service_name VARCHAR2(255) NOT NULL, + metric_date DATE NOT NULL, + granularity VARCHAR2(20) DEFAULT 'daily' NOT NULL, + data_source_type VARCHAR2(50) DEFAULT 'batch' NOT NULL, + computed_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL, + is_baseline NUMBER(1) DEFAULT 0 NOT NULL, + total_feature_views NUMBER, + total_features NUMBER, + avg_null_rate NUMBER, + max_null_rate NUMBER, + CONSTRAINT pk_fsm PRIMARY KEY (project_id, feature_service_name, + metric_date, granularity, data_source_type) + ) + """, + ) + + _oracle_try_execute_ddl( + con, + f""" + CREATE TABLE {MON_TABLE_JOB} ( + job_id VARCHAR2(36) NOT NULL, + project_id VARCHAR2(255) NOT NULL, + feature_view_name VARCHAR2(255), + job_type VARCHAR2(50) NOT NULL, + status VARCHAR2(20) DEFAULT 'pending' NOT NULL, + parameters VARCHAR2(4000), + metric_date DATE NOT NULL, + started_at TIMESTAMP WITH TIME ZONE, + completed_at TIMESTAMP WITH TIME ZONE, + error_message VARCHAR2(4000), + result_summary VARCHAR2(4000), + CONSTRAINT pk_fmj PRIMARY KEY (job_id) + ) + """, + ) + + @staticmethod + def save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], + ) -> None: + if not metrics: + return + assert isinstance(config.offline_store, OracleOfflineStoreConfig) + + table, columns, pk_columns = monitoring_table_meta(metric_type) + con = get_ibis_connection(config) + for row in metrics: + _oracle_merge_metric_row(con, table, columns, pk_columns, row) + + @staticmethod + def query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, OracleOfflineStoreConfig) + + table, columns, _ = monitoring_table_meta(metric_type) + + conditions: list = [] + if project: + conditions.append( + f"{_oracle_quote_ident('project_id')} = {_oracle_escape_literal(project)}" + ) + if filters: + for key, value in filters.items(): + if value is not None: + conditions.append( + f"{_oracle_quote_ident(key)} = {_oracle_escape_literal(value)}" + ) + if start_date: + conditions.append( + f"{_oracle_quote_ident('metric_date')} >= {_oracle_escape_literal(start_date)}" + ) + if end_date: + conditions.append( + f"{_oracle_quote_ident('metric_date')} <= {_oracle_escape_literal(end_date)}" + ) + + where_sql = " AND ".join(conditions) if conditions else "1=1" + col_list = ", ".join(_oracle_quote_ident(c) for c in columns) + order_col = "metric_date" if "metric_date" in columns else "job_id" + + con = get_ibis_connection(config) + rows = _oracle_fetchall( + con, + f"SELECT {col_list} FROM {table} WHERE {where_sql} " + f"ORDER BY {_oracle_quote_ident(order_col)} ASC", + ) + + results = [] + for row in rows: + record = dict(zip(columns, row)) + results.append(normalize_monitoring_row(record)) + + return results + + @staticmethod + def clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> None: + assert isinstance(config.offline_store, OracleOfflineStoreConfig) + + conditions = [ + f"{_oracle_quote_ident('project_id')} = {_oracle_escape_literal(project)}" + ] + if feature_view_name: + conditions.append( + f"{_oracle_quote_ident('feature_view_name')} = " + f"{_oracle_escape_literal(feature_view_name)}" + ) + if feature_name: + conditions.append( + f"{_oracle_quote_ident('feature_name')} = " + f"{_oracle_escape_literal(feature_name)}" + ) + if data_source_type: + conditions.append( + f"{_oracle_quote_ident('data_source_type')} = " + f"{_oracle_escape_literal(data_source_type)}" + ) + conditions.append(f"{_oracle_quote_ident('is_baseline')} = 1") + + where_sql = " AND ".join(conditions) + con = get_ibis_connection(config) + _oracle_exec( + con, + f"UPDATE {MON_TABLE_FEATURE} SET {_oracle_quote_ident('is_baseline')} = 0 " + f"WHERE {where_sql}", + ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/oracle_offline_store/oracle_source.py b/sdk/python/feast/infra/offline_stores/contrib/oracle_offline_store/oracle_source.py new file mode 100644 index 00000000000..cf45ec5454b --- /dev/null +++ b/sdk/python/feast/infra/offline_stores/contrib/oracle_offline_store/oracle_source.py @@ -0,0 +1,191 @@ +import json +from typing import Callable, Dict, Iterable, Optional, Tuple + +from feast import type_map +from feast.data_source import DataSource +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto +from feast.repo_config import RepoConfig +from feast.value_type import ValueType + + +class OracleOptions: + """ + DataSource Oracle options used to source features from an Oracle database. + """ + + def __init__( + self, + table_ref: Optional[str], + ): + self._table_ref = table_ref + + @property + def table_ref(self): + """Returns the table ref of this Oracle source""" + return self._table_ref + + @table_ref.setter + def table_ref(self, table_ref): + """Sets the table ref of this Oracle source""" + self._table_ref = table_ref + + @classmethod + def from_proto( + cls, oracle_options_proto: DataSourceProto.CustomSourceOptions + ) -> "OracleOptions": + """ + Creates an OracleOptions from a protobuf representation. + + Args: + oracle_options_proto: A protobuf representation of a DataSource + + Returns: + An OracleOptions object based on the protobuf + """ + options = json.loads(oracle_options_proto.configuration) + return cls(table_ref=options.get("table_ref")) + + def to_proto(self) -> DataSourceProto.CustomSourceOptions: + """ + Converts an OracleOptions object to its protobuf representation. + + Returns: + CustomSourceOptions protobuf + """ + return DataSourceProto.CustomSourceOptions( + configuration=json.dumps({"table_ref": self._table_ref}).encode("utf-8") + ) + + +class OracleSource(DataSource): + """An OracleSource defines a data source backed by an Oracle database table.""" + + def source_type(self) -> DataSourceProto.SourceType.ValueType: + return DataSourceProto.CUSTOM_SOURCE + + def __init__( + self, + name: str, + table_ref: Optional[str] = None, + event_timestamp_column: Optional[str] = None, + created_timestamp_column: Optional[str] = "", + field_mapping: Optional[Dict[str, str]] = None, + date_partition_column: Optional[str] = "", + description: Optional[str] = None, + tags: Optional[Dict[str, str]] = None, + owner: Optional[str] = None, + ): + """Creates an OracleSource object. + + Args: + name: Name of the source, which should be unique within a project. + table_ref: The table reference (e.g., "TRANSACTION_FEATURES" or "SCHEMA.TABLE"). + event_timestamp_column: Event timestamp column for point-in-time joins. + created_timestamp_column: Timestamp column indicating when the row was created + (used for deduplicating rows). + field_mapping: A dictionary mapping column names in this data source to feature + names in a feature table or view. + date_partition_column: The date partition column. + description: A human-readable description. + tags: A dictionary of key-value pairs to store arbitrary metadata. + owner: The owner of the data source, typically the email of the primary maintainer. + """ + self._oracle_options = OracleOptions(table_ref=table_ref) + + super().__init__( + created_timestamp_column=created_timestamp_column, + field_mapping=field_mapping, + date_partition_column=date_partition_column, + description=description, + tags=tags, + owner=owner, + name=name, + timestamp_field=event_timestamp_column, + ) + + def __eq__(self, other): + if not isinstance(other, OracleSource): + return False + + return ( + self.name == other.name + and self._oracle_options.table_ref == other._oracle_options.table_ref + and self.timestamp_field == other.timestamp_field + and self.created_timestamp_column == other.created_timestamp_column + and self.field_mapping == other.field_mapping + ) + + def __hash__(self): + return hash( + ( + self.name, + self._oracle_options.table_ref, + self.timestamp_field, + self.created_timestamp_column, + ) + ) + + @property + def table_ref(self): + return self._oracle_options.table_ref + + @property + def oracle_options(self): + """Returns the Oracle options of this data source""" + return self._oracle_options + + @oracle_options.setter + def oracle_options(self, oracle_options): + """Sets the Oracle options of this data source""" + self._oracle_options = oracle_options + + @staticmethod + def from_proto(data_source: DataSourceProto): + options = json.loads(data_source.custom_options.configuration) + return OracleSource( + name=data_source.name, + field_mapping=dict(data_source.field_mapping), + table_ref=options.get("table_ref"), + event_timestamp_column=data_source.timestamp_field, + created_timestamp_column=data_source.created_timestamp_column, + date_partition_column=data_source.date_partition_column, + ) + + def _to_proto_impl(self) -> DataSourceProto: + data_source_proto = DataSourceProto( + type=DataSourceProto.CUSTOM_SOURCE, + data_source_class_type="feast.infra.offline_stores.contrib.oracle_offline_store.oracle_source.OracleSource", + field_mapping=self.field_mapping, + custom_options=self._oracle_options.to_proto(), + ) + + data_source_proto.timestamp_field = self.timestamp_field + data_source_proto.created_timestamp_column = self.created_timestamp_column + data_source_proto.date_partition_column = self.date_partition_column + data_source_proto.name = self.name + return data_source_proto + + def get_table_query_string(self) -> str: + """Returns a string that can directly be used to reference this table in SQL""" + return f"{self.table_ref}" + + def validate(self, config: RepoConfig): + """Validates the Oracle data source by checking the table exists.""" + self.get_table_column_names_and_types(config) + + @staticmethod + def source_datatype_to_feast_value_type() -> Callable[[str], ValueType]: + return type_map.oracle_to_feast_value_type + + def get_table_column_names_and_types( + self, config: RepoConfig + ) -> Iterable[Tuple[str, str]]: + from feast.infra.offline_stores.contrib.oracle_offline_store.oracle import ( + get_ibis_connection, + ) + + con = get_ibis_connection(config) + + schema = con.get_schema(self.table_ref) + + return [(col_name, str(col_type)) for col_name, col_type in schema.items()] diff --git a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres.py b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres.py index 7c65b649046..44270356b2d 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres.py +++ b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres.py @@ -1,6 +1,6 @@ import contextlib from dataclasses import asdict -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timezone from enum import Enum from typing import ( Any, @@ -42,11 +42,22 @@ get_query_schema, ) from feast.infra.utils.postgres.postgres_config import PostgreSQLConfig +from feast.monitoring.monitoring_utils import ( + MON_TABLE_FEATURE, + MON_TABLE_FEATURE_SERVICE, + MON_TABLE_FEATURE_VIEW, + MON_TABLE_JOB, + empty_categorical_metric, + empty_numeric_metric, + monitoring_table_meta, + normalize_monitoring_row, + opt_float, +) from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import RepoConfig from feast.saved_dataset import SavedDatasetStorage from feast.type_map import pg_type_code_to_arrow -from feast.utils import _utc_now, make_tzaware +from feast.utils import compute_non_entity_date_range from .postgres_source import PostgreSQLSource @@ -64,6 +75,8 @@ class PostgreSQLOfflineStoreConfig(PostgreSQLConfig): class PostgreSQLOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -124,48 +137,23 @@ def get_historical_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, **kwargs, ) -> RetrievalJob: assert isinstance(config.offline_store, PostgreSQLOfflineStoreConfig) for fv in feature_views: assert isinstance(fv.batch_source, PostgreSQLSource) - start_date: Optional[datetime] = kwargs.get("start_date", None) - end_date: Optional[datetime] = kwargs.get("end_date", None) + start_date: Optional[datetime] = kwargs.get("start_date") + end_date: Optional[datetime] = kwargs.get("end_date") # Handle non-entity retrieval mode if entity_df is None: - # Default to current time if end_date not provided - if end_date is None: - end_date = _utc_now() - else: - end_date = make_tzaware(end_date) - - # Calculate start_date from TTL if not provided - - if start_date is None: - # Find the maximum TTL across all feature views to ensure we capture enough data - max_ttl_seconds = 0 - for fv in feature_views: - if fv.ttl and isinstance(fv.ttl, timedelta): - ttl_seconds = int(fv.ttl.total_seconds()) - max_ttl_seconds = max(max_ttl_seconds, ttl_seconds) - - if max_ttl_seconds > 0: - # Start from (end_date - max_ttl) to ensure we capture all relevant features - start_date = end_date - timedelta(seconds=max_ttl_seconds) - else: - # If no TTL is set, default to 30 days before end_date - start_date = end_date - timedelta(days=30) - else: - start_date = make_tzaware(start_date) - - entity_df = pd.DataFrame( - { - "event_timestamp": pd.date_range( - start=start_date, end=end_date, freq="1s", tz=timezone.utc - )[:1] # Just one row - } + start_date, end_date = compute_non_entity_date_range( + feature_views, + start_date=start_date, + end_date=end_date, ) + entity_df = pd.DataFrame({"event_timestamp": [end_date]}) entity_schema = _get_entity_schema(entity_df, config) @@ -173,11 +161,18 @@ def get_historical_features( offline_utils.infer_event_timestamp_from_entity_df(entity_schema) ) - entity_df_event_timestamp_range = _get_entity_df_event_timestamp_range( - entity_df, - entity_df_event_timestamp_col, - config, - ) + # In non-entity mode, use the actual requested range so that + # min_event_timestamp (= range[0] - TTL) doesn't clip the window. + # The synthetic entity_df only has end_date, which would wrongly + # set min_event_timestamp to end_date - TTL instead of start_date - TTL. + if start_date is not None and end_date is not None: + entity_df_event_timestamp_range = (start_date, end_date) + else: + entity_df_event_timestamp_range = _get_entity_df_event_timestamp_range( + entity_df, + entity_df_event_timestamp_col, + config, + ) @contextlib.contextmanager def query_generator() -> Iterator[str]: @@ -230,6 +225,7 @@ def query_generator() -> Iterator[str]: use_cte=use_cte, start_date=start_date, end_date=end_date, + filter_by_created_timestamp=filter_by_created_timestamp, ) finally: # Only cleanup if we created a table @@ -308,6 +304,263 @@ def pull_all_from_table_or_query( on_demand_feature_views=None, ) + @staticmethod + def compute_monitoring_metrics( + config: RepoConfig, + data_source: DataSource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, PostgreSQLOfflineStoreConfig) + assert isinstance(data_source, PostgreSQLSource) + + from_expression = data_source.get_table_query_string() + ts_filter = ( + get_timestamp_filter_sql( + start_date, + end_date, + timestamp_field, + tz=timezone.utc, + cast_style="timestamptz", + date_time_separator=" ", + ) + or "1=1" + ) + + numeric_features = [n for n, t in feature_columns if t == "numeric"] + categorical_features = [n for n, t in feature_columns if t == "categorical"] + results: List[Dict[str, Any]] = [] + + with _get_conn(config.offline_store) as conn: + conn.read_only = True + + if numeric_features: + results.extend( + _sql_numeric_stats( + conn, + from_expression, + numeric_features, + ts_filter, + histogram_bins, + ) + ) + + for col_name in categorical_features: + results.append( + _sql_categorical_stats( + conn, + from_expression, + col_name, + ts_filter, + top_n, + ) + ) + + return results + + @staticmethod + def get_monitoring_max_timestamp( + config: RepoConfig, + data_source: DataSource, + timestamp_field: str, + ) -> Optional[datetime]: + assert isinstance(config.offline_store, PostgreSQLOfflineStoreConfig) + assert isinstance(data_source, PostgreSQLSource) + + from_expression = data_source.get_table_query_string_with_alias("max_ts_alias") + + with _get_conn(config.offline_store) as conn: + conn.read_only = True + with conn.cursor() as cur: + cur.execute(f'SELECT MAX("{timestamp_field}") FROM {from_expression}') + row = cur.fetchone() + + if row is None or row[0] is None: + return None + val = row[0] + if isinstance(val, datetime): + return val if val.tzinfo else val.replace(tzinfo=timezone.utc) + return datetime.combine(val, datetime.min.time(), tzinfo=timezone.utc) + + # ------------------------------------------------------------------ # + # Monitoring metrics storage (native PostgreSQL) + # ------------------------------------------------------------------ # + + @staticmethod + def ensure_monitoring_tables(config: RepoConfig) -> None: + assert isinstance(config.offline_store, PostgreSQLOfflineStoreConfig) + with _get_conn(config.offline_store) as conn, conn.cursor() as cur: + cur.execute(f""" + CREATE TABLE IF NOT EXISTS {MON_TABLE_FEATURE} ( + project_id VARCHAR(255) NOT NULL, + feature_view_name VARCHAR(255) NOT NULL, + feature_name VARCHAR(255) NOT NULL, + metric_date DATE NOT NULL, + granularity VARCHAR(20) NOT NULL DEFAULT 'daily', + data_source_type VARCHAR(50) NOT NULL DEFAULT 'batch', + computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_baseline BOOLEAN NOT NULL DEFAULT FALSE, + feature_type VARCHAR(50) NOT NULL, + row_count BIGINT, + null_count BIGINT, + null_rate DOUBLE PRECISION, + mean DOUBLE PRECISION, + stddev DOUBLE PRECISION, + min_val DOUBLE PRECISION, + max_val DOUBLE PRECISION, + p50 DOUBLE PRECISION, + p75 DOUBLE PRECISION, + p90 DOUBLE PRECISION, + p95 DOUBLE PRECISION, + p99 DOUBLE PRECISION, + histogram JSONB, + PRIMARY KEY (project_id, feature_view_name, feature_name, + metric_date, granularity, data_source_type) + ); + CREATE INDEX IF NOT EXISTS idx_fm_feature_metrics_project + ON {MON_TABLE_FEATURE} (project_id); + CREATE INDEX IF NOT EXISTS idx_fm_feature_metrics_view + ON {MON_TABLE_FEATURE} (project_id, feature_view_name); + CREATE INDEX IF NOT EXISTS idx_fm_feature_metrics_date + ON {MON_TABLE_FEATURE} (metric_date); + CREATE INDEX IF NOT EXISTS idx_fm_feature_metrics_granularity + ON {MON_TABLE_FEATURE} (granularity); + CREATE INDEX IF NOT EXISTS idx_fm_feature_metrics_baseline + ON {MON_TABLE_FEATURE} (project_id, feature_view_name, feature_name) + WHERE is_baseline = TRUE; + """) + + cur.execute(f""" + CREATE TABLE IF NOT EXISTS {MON_TABLE_FEATURE_VIEW} ( + project_id VARCHAR(255) NOT NULL, + feature_view_name VARCHAR(255) NOT NULL, + metric_date DATE NOT NULL, + granularity VARCHAR(20) NOT NULL DEFAULT 'daily', + data_source_type VARCHAR(50) NOT NULL DEFAULT 'batch', + computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_baseline BOOLEAN NOT NULL DEFAULT FALSE, + total_row_count BIGINT, + total_features INTEGER, + features_with_nulls INTEGER, + avg_null_rate DOUBLE PRECISION, + max_null_rate DOUBLE PRECISION, + PRIMARY KEY (project_id, feature_view_name, metric_date, + granularity, data_source_type) + ); + """) + + cur.execute(f""" + CREATE TABLE IF NOT EXISTS {MON_TABLE_FEATURE_SERVICE} ( + project_id VARCHAR(255) NOT NULL, + feature_service_name VARCHAR(255) NOT NULL, + metric_date DATE NOT NULL, + granularity VARCHAR(20) NOT NULL DEFAULT 'daily', + data_source_type VARCHAR(50) NOT NULL DEFAULT 'batch', + computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + is_baseline BOOLEAN NOT NULL DEFAULT FALSE, + total_feature_views INTEGER, + total_features INTEGER, + avg_null_rate DOUBLE PRECISION, + max_null_rate DOUBLE PRECISION, + PRIMARY KEY (project_id, feature_service_name, metric_date, + granularity, data_source_type) + ); + """) + + cur.execute(f""" + CREATE TABLE IF NOT EXISTS {MON_TABLE_JOB} ( + job_id VARCHAR(36) PRIMARY KEY, + project_id VARCHAR(255) NOT NULL, + feature_view_name VARCHAR(255), + job_type VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + parameters TEXT, + metric_date DATE NOT NULL, + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + error_message TEXT, + result_summary TEXT + ); + CREATE INDEX IF NOT EXISTS idx_fm_jobs_status + ON {MON_TABLE_JOB} (status); + CREATE INDEX IF NOT EXISTS idx_fm_jobs_project + ON {MON_TABLE_JOB} (project_id); + """) + conn.commit() + + @staticmethod + def save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], + ) -> None: + if not metrics: + return + assert isinstance(config.offline_store, PostgreSQLOfflineStoreConfig) + + table, columns, pk_columns = monitoring_table_meta(metric_type) + _mon_upsert(config.offline_store, table, columns, pk_columns, metrics) + + @staticmethod + def query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional["date"] = None, + end_date: Optional["date"] = None, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, PostgreSQLOfflineStoreConfig) + + _, columns, _ = monitoring_table_meta(metric_type) + return _mon_query( + config.offline_store, + metric_type, + columns, + project, + filters, + start_date, + end_date, + ) + + @staticmethod + def clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> None: + assert isinstance(config.offline_store, PostgreSQLOfflineStoreConfig) + + conditions = [sql.SQL("project_id = %s")] + params: list = [project] + + if feature_view_name: + conditions.append(sql.SQL("feature_view_name = %s")) + params.append(feature_view_name) + if feature_name: + conditions.append(sql.SQL("feature_name = %s")) + params.append(feature_name) + if data_source_type: + conditions.append(sql.SQL("data_source_type = %s")) + params.append(data_source_type) + + conditions.append(sql.SQL("is_baseline = TRUE")) + + query = sql.SQL("UPDATE {} SET is_baseline = FALSE WHERE {}").format( + sql.Identifier(MON_TABLE_FEATURE), + sql.SQL(" AND ").join(conditions), + ) + + with _get_conn(config.offline_store) as conn, conn.cursor() as cur: + cur.execute(query, params) + conn.commit() + class PostgreSQLRetrievalJob(RetrievalJob): def __init__( @@ -444,6 +697,7 @@ def build_point_in_time_query( use_cte: bool = False, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, + filter_by_created_timestamp: bool = False, ) -> str: """Build point-in-time query between each feature view table and the entity dataframe for PostgreSQL""" template = Environment(loader=BaseLoader()).from_string(source=query_template) @@ -474,6 +728,7 @@ def build_point_in_time_query( "use_cte": use_cte, "start_date": start_date, "end_date": end_date, + "filter_by_created_timestamp": filter_by_created_timestamp, } query = template.render(template_context) @@ -716,6 +971,10 @@ def _get_entity_schema( AND subquery.event_timestamp >= entity_dataframe.entity_timestamp - {{ featureview.ttl }} * interval '1' second {% endif %} + {% if filter_by_created_timestamp and featureview.created_timestamp_column %} + AND subquery.created_timestamp <= entity_dataframe.entity_timestamp + {% endif %} + {% for entity in featureview.entities %} AND subquery."{{ entity }}" = entity_dataframe."{{ entity }}" {% endfor %} @@ -801,3 +1060,295 @@ def _get_entity_schema( {% endfor %} {% endif %} """ + + +# ------------------------------------------------------------------ # +# Monitoring SQL push-down helpers +# ------------------------------------------------------------------ # + + +def _sql_numeric_stats( + conn, + from_expression: str, + feature_names: List[str], + ts_filter: str, + histogram_bins: int, +) -> List[Dict[str, Any]]: + """Batch-compute numeric statistics via one SQL query, then histograms.""" + # 11 aggregate columns per feature (non_null, mean..p99) + 1 row_count + select_parts = ["COUNT(*)"] + for col in feature_names: + q = f'"{col}"' + c = f"{q}::float8" + select_parts.extend( + [ + f"COUNT({q})", + f"AVG({c})", + f"STDDEV_SAMP({c})", + f"MIN({c})", + f"MAX({c})", + f"PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY {c})", + ] + ) + + query = ( + f"SELECT {', '.join(select_parts)} " + f"FROM {from_expression} AS _src WHERE {ts_filter}" + ) + + with conn.cursor() as cur: + cur.execute(query) + row = cur.fetchone() + + if row is None: + return [empty_numeric_metric(n) for n in feature_names] + + row_count = row[0] + results: List[Dict[str, Any]] = [] + + for i, col in enumerate(feature_names): + base = 1 + i * 10 + non_null = row[base] or 0 + null_count = row_count - non_null + + min_val = opt_float(row[base + 3]) + max_val = opt_float(row[base + 4]) + + result: Dict[str, Any] = { + "feature_name": col, + "feature_type": "numeric", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": opt_float(row[base + 1]), + "stddev": opt_float(row[base + 2]), + "min_val": min_val, + "max_val": max_val, + "p50": opt_float(row[base + 5]), + "p75": opt_float(row[base + 6]), + "p90": opt_float(row[base + 7]), + "p95": opt_float(row[base + 8]), + "p99": opt_float(row[base + 9]), + "histogram": None, + } + + if min_val is not None and max_val is not None and non_null > 0: + result["histogram"] = _sql_numeric_histogram( + conn, + from_expression, + col, + ts_filter, + histogram_bins, + min_val, + max_val, + ) + + results.append(result) + + return results + + +def _sql_numeric_histogram( + conn, + from_expression: str, + col_name: str, + ts_filter: str, + bins: int, + min_val: float, + max_val: float, +) -> Dict[str, Any]: + q_col = f'"{col_name}"' + + if min_val == max_val: + with conn.cursor() as cur: + cur.execute( + f"SELECT COUNT(*) FROM {from_expression} AS _src " + f"WHERE {q_col} IS NOT NULL AND {ts_filter}" + ) + cnt = (cur.fetchone() or (0,))[0] + return {"bins": [min_val, max_val], "counts": [cnt], "bin_width": 0.0} + + upper = max_val + (max_val - min_val) * 1e-10 + bin_width = (max_val - min_val) / bins + + query = ( + f"SELECT width_bucket({q_col}::float8, {min_val}, {upper}, {bins}) AS bucket, " + f"COUNT(*) AS cnt " + f"FROM {from_expression} AS _src " + f"WHERE {q_col} IS NOT NULL AND {ts_filter} " + f"GROUP BY bucket ORDER BY bucket" + ) + + with conn.cursor() as cur: + cur.execute(query) + rows = cur.fetchall() + + counts = [0] * bins + for bucket, cnt in rows: + if 1 <= bucket <= bins: + counts[bucket - 1] = cnt + + bin_edges = [min_val + i * bin_width for i in range(bins + 1)] + return { + "bins": [float(b) for b in bin_edges], + "counts": counts, + "bin_width": float(bin_width), + } + + +def _sql_categorical_stats( + conn, + from_expression: str, + col_name: str, + ts_filter: str, + top_n: int, +) -> Dict[str, Any]: + q_col = f'"{col_name}"' + + query = ( + f"WITH filtered AS (" + f" SELECT * FROM {from_expression} AS _src WHERE {ts_filter}" + f") " + f"SELECT " + f" (SELECT COUNT(*) FROM filtered) AS row_count, " + f" (SELECT COUNT(*) - COUNT({q_col}) FROM filtered) AS null_count, " + f" (SELECT COUNT(DISTINCT {q_col}) FROM filtered " + f" WHERE {q_col} IS NOT NULL) AS unique_count, " + f" {q_col}::text AS value, COUNT(*) AS cnt " + f"FROM filtered WHERE {q_col} IS NOT NULL " + f"GROUP BY {q_col} ORDER BY cnt DESC LIMIT {int(top_n)}" + ) + + with conn.cursor() as cur: + cur.execute(query) + rows = cur.fetchall() + + if not rows: + return empty_categorical_metric(col_name) + + row_count = rows[0][0] + null_count = rows[0][1] + unique_count = rows[0][2] + + top_entries = [{"value": r[3], "count": r[4]} for r in rows] + top_total = sum(e["count"] for e in top_entries) + other_count = (row_count - null_count) - top_total + + return { + "feature_name": col_name, + "feature_type": "categorical", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": { + "values": top_entries, + "other_count": max(other_count, 0), + "unique_count": unique_count, + }, + } + + +# ------------------------------------------------------------------ # +# Monitoring metrics storage helpers +# ------------------------------------------------------------------ # + + +def _mon_upsert( + pg_config: PostgreSQLConfig, + table: str, + columns: List[str], + pk_columns: List[str], + rows: List[Dict[str, Any]], +) -> None: + import json as _json + + non_pk = [c for c in columns if c not in pk_columns] + col_ids = sql.SQL(", ").join(sql.Identifier(c) for c in columns) + placeholders = sql.SQL(", ").join(sql.Placeholder() for _ in columns) + update_clause = sql.SQL(", ").join( + sql.SQL("{} = EXCLUDED.{}").format(sql.Identifier(c), sql.Identifier(c)) + for c in non_pk + ) + pk_ids = sql.SQL(", ").join(sql.Identifier(c) for c in pk_columns) + + query = sql.SQL( + "INSERT INTO {} ({}) VALUES ({}) ON CONFLICT ({}) DO UPDATE SET {}" + ).format(sql.Identifier(table), col_ids, placeholders, pk_ids, update_clause) + + with _get_conn(pg_config) as conn, conn.cursor() as cur: + for row in rows: + values = [] + for col in columns: + val = row.get(col) + if col == "histogram" and val is not None: + val = _json.dumps(val) + values.append(val) + cur.execute(query, values) + conn.commit() + + +def _mon_query( + pg_config: PostgreSQLConfig, + metric_type: str, + columns: List[str], + project: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional["date"] = None, + end_date: Optional["date"] = None, +) -> List[Dict[str, Any]]: + table, _, _ = monitoring_table_meta(metric_type) + + conditions: list = [] + params: list = [] + + if project: + conditions.append(sql.SQL("project_id = %s")) + params.append(project) + + if filters: + for key, value in filters.items(): + if value is not None: + conditions.append(sql.SQL("{} = %s").format(sql.Identifier(key))) + params.append(value) + + if start_date: + conditions.append(sql.SQL("metric_date >= %s")) + params.append(start_date) + if end_date: + conditions.append(sql.SQL("metric_date <= %s")) + params.append(end_date) + + col_ids = sql.SQL(", ").join(sql.Identifier(c) for c in columns) + where = sql.SQL(" AND ").join(conditions) if conditions else sql.SQL("TRUE") + order_col = "metric_date" if "metric_date" in columns else "job_id" + query = sql.SQL("SELECT {} FROM {} WHERE {} ORDER BY {} ASC").format( + col_ids, + sql.Identifier(table), + where, + sql.Identifier(order_col), + ) + + with _get_conn(pg_config) as conn, conn.cursor() as cur: + conn.read_only = True + cur.execute(query, params) + rows = cur.fetchall() + + results = [] + for row in rows: + record = dict(zip(columns, row)) + results.append(normalize_monitoring_row(record)) + + return results diff --git a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres_source.py b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres_source.py index 0d3045e4aa7..ca693fc578a 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/postgres_source.py @@ -77,9 +77,7 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, PostgreSQLSource): - raise TypeError( - "Comparisons should only involve PostgreSQLSource class objects." - ) + return False return ( super().__eq__(other) @@ -145,11 +143,45 @@ def get_table_column_names_and_types( ) def get_table_query_string(self) -> str: + """Returns a string that can be used to reference this table in SQL. + + For query-based sources, returns the query wrapped in parentheses. + + Note: + When using the returned string directly in a FROM clause with PostgreSQL, + you may need to add an alias if this is a query-based source. PostgreSQL + requires all subqueries in FROM clauses to have aliases. Consider using + get_table_query_string_with_alias() for automatic aliasing. + """ if self._postgres_options._table: return f"{self._postgres_options._table}" else: return f"({self._postgres_options._query})" + def get_table_query_string_with_alias(self, alias: str = "subquery") -> str: + """Returns a string for use in FROM clause with alias for PostgreSQL compatibility. + + PostgreSQL requires all subqueries in FROM clauses to have aliases. This method + automatically adds an alias when the source is query-based. + + Args: + alias: The alias to use for query-based sources. Defaults to "subquery". + + Returns: + For table-based sources: the table name (no alias needed). + For query-based sources: "(query) AS alias". + + Example:: + + source = PostgreSQLSource(query="SELECT * FROM my_table", ...) + entity_sql = f"SELECT id, ts FROM {source.get_table_query_string_with_alias()}" + # Results in: "SELECT id, ts FROM (SELECT * FROM my_table) AS subquery" + """ + if self._postgres_options._table: + return f"{self._postgres_options._table}" + else: + return f"({self._postgres_options._query}) AS {alias}" + class PostgreSQLOptions: def __init__( diff --git a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/tests/data_source.py index c94b04329e0..273ae22cf54 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/postgres_offline_store/tests/data_source.py @@ -14,10 +14,10 @@ ) from feast.infra.utils.postgres.connection_utils import df_to_postgres_table from feast.infra.utils.postgres.postgres_config import PostgreSQLConfig -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) @@ -85,6 +85,7 @@ def __init__( db_schema="public", user=self.container.env["POSTGRES_USER"], password=self.container.env["POSTGRES_PASSWORD"], + sslmode="disable", ) def create_data_source( @@ -124,6 +125,7 @@ def create_online_store(self) -> PostgreSQLOnlineStoreConfig: # type: ignore db_schema="feature_store", user=POSTGRES_USER, password=POSTGRES_PASSWORD, + sslmode="disable", ) def create_saved_dataset_destination(self): diff --git a/sdk/python/feast/infra/offline_stores/contrib/postgres_repo_configuration.py b/sdk/python/feast/infra/offline_stores/contrib/postgres_repo_configuration.py index 2fa08bf47ad..4595fb89836 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/postgres_repo_configuration.py +++ b/sdk/python/feast/infra/offline_stores/contrib/postgres_repo_configuration.py @@ -1,8 +1,8 @@ from feast.infra.offline_stores.contrib.postgres_offline_store.tests.data_source import ( PostgreSQLDataSourceCreator, ) -from tests.integration.feature_repos.repo_configuration import REDIS_CONFIG -from tests.integration.feature_repos.universal.online_store.redis import ( +from tests.universal.feature_repos.repo_configuration import REDIS_CONFIG +from tests.universal.feature_repos.universal.online_store.redis import ( RedisOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray.py b/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray.py index 98247c6c0e0..5230797d94b 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray.py +++ b/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray.py @@ -59,9 +59,20 @@ feast_value_type_to_pandas_type, pa_to_feast_value_type, ) -from feast.utils import _get_column_names, make_df_tzaware, make_tzaware +from feast.utils import ( + _get_column_names, + compute_non_entity_date_range, + make_df_tzaware, + make_tzaware, +) logger = logging.getLogger(__name__) +# Remote storage URI schemes supported by the Ray offline store +# S3: Amazon S3 +# GCS: Google Cloud Storage +# HDFS: Hadoop Distributed File System +# Azure: Azure Storage Gen2 +REMOTE_STORAGE_SCHEMES = ("s3://", "gs://", "hdfs://", "abfs://", "abfss://") def _get_data_schema_info( @@ -354,6 +365,51 @@ class RayOfflineStoreConfig(FeastConfigBaseModel): kuberay_conf: Optional[Dict[str, Any]] = None """KubeRay/CodeFlare configuration parameters (passed to CodeFlare SDK)""" + # Worker task resource configuration + num_gpus: Optional[float] = None + """Number of GPUs to request per worker task. Requires GPU nodes in the + Ray cluster. Fractional values (e.g. 0.5) are supported by Ray for GPU + sharing. Supported in all modes: local, remote, and KubeRay.""" + + gpu_batch_format: str = "pandas" + """Batch format for map_batches when num_gpus is set. Use 'numpy' or + 'pyarrow' for GPU-native libraries (e.g. cuDF, PyTorch). Defaults to + 'pandas'.""" + + worker_task_options: Optional[Dict[str, Any]] = None + """Arbitrary Ray task options passed verbatim to @ray.remote .options() + and map_batches for every worker task Feast dispatches. This is the + escape hatch for any Ray or CodeFlare SDK scheduling parameter not + covered by the dedicated fields above. + + Pairs with ray_conf (which configures ray.init) — worker_task_options + targets the individual worker tasks rather than the cluster connection. + + Common keys (see https://docs.ray.io/en/latest/ray-core/api/doc/ray.remote_function.RemoteFunction.options.html): + num_cpus (float) – CPUs per task (default: 1) + memory (int) – Heap memory in bytes (e.g. 8 * 1024**3 for 8 GB) + accelerator_type (str) – Specific GPU model, e.g. 'A100', 'T4', 'V100'. + Pins tasks to nodes advertising that type. Useful + on KubeRay clusters with mixed GPU pools. + resources (dict) – Custom/extended resource labels, e.g. + {'intel.com/gpu': 1} for Kubernetes extended resources. + runtime_env (dict) – Per-task runtime environment (pip, conda, env_vars, + working_dir, …). For KubeRay use this to install + extra packages on workers without rebuilding images. + max_retries (int) – Task retry count on worker failure (default: 3). + scheduling_strategy (str) – 'DEFAULT', 'SPREAD', or a placement group strategy. + + Example: + worker_task_options: + num_cpus: 4 + memory: 8589934592 # 8 GB + accelerator_type: "A100" + max_retries: 5 + runtime_env: + pip: ["cudf-cu12==24.10.0"] + env_vars: {CUDA_VISIBLE_DEVICES: "0"} + """ + class RayResourceManager: """ @@ -371,12 +427,14 @@ def __init__(self, config: Optional[RayOfflineStoreConfig] = None) -> None: self.cluster_resources = {"CPU": 4, "memory": 8 * 1024**3} self.available_memory = 8 * 1024**3 self.available_cpus = 4 + self.available_gpus = 0 self.num_nodes = 1 return self.cluster_resources = ray.cluster_resources() self.available_memory = self.cluster_resources.get("memory", 8 * 1024**3) self.available_cpus = int(self.cluster_resources.get("CPU", 4)) + self.available_gpus = int(self.cluster_resources.get("GPU", 0)) self.num_nodes = len(ray.nodes()) def configure_ray_context(self) -> None: @@ -410,6 +468,7 @@ def configure_ray_context(self) -> None: if getattr(self.config, "enable_ray_logging", False): logger.info( f"Configured Ray context: {self.available_cpus} CPUs, " + f"{self.available_gpus} GPUs, " f"{self.available_memory // 1024**3}GB memory, {self.num_nodes} nodes" ) @@ -813,7 +872,7 @@ def windowed_temporal_join( ) combined_ds = entity_windowed.union(feature_windowed) result_ds = combined_ds.map_batches( - self._apply_windowed_point_in_time_logic, + self._apply_windowed_point_in_time_logic, # type: ignore[arg-type] batch_format="pandas", fn_kwargs={ "timestamp_field": timestamp_field, @@ -999,6 +1058,17 @@ def _get_ray_dataset(self) -> Dataset: else: raise ValueError(f"Unsupported result type: {type(result)}") + def to_ray_dataset(self) -> Dataset: + """Return the underlying Ray Dataset directly. + + Preferred by RayReadNode over to_arrow() / to_df() because it + avoids materialising the full dataset to pandas on the driver and + keeps the computation on the cluster. Works for both local Ray + (StandardRayWrapper) and KubeRay / ray:// client mode + (RemoteDatasetProxy via CodeFlareRayWrapper). + """ + return self._get_ray_dataset() + def to_df( self, validation_reference: Optional[ValidationReference] = None, @@ -1049,12 +1119,10 @@ def to_arrow( if self._prefer_ray_datasets: try: + import ray as _ray + ray_ds = self._get_ray_dataset() - if hasattr(ray_ds, "to_arrow"): - return ray_ds.to_arrow() - else: - df = ray_ds.to_pandas() - return pa.Table.from_pandas(df) + return pa.concat_tables(_ray.get(ray_ds.to_arrow_refs())) except Exception: df = self.to_df( validation_reference=validation_reference, timeout=timeout @@ -1129,16 +1197,14 @@ def _to_df_internal(self, timeout: Optional[int] = None) -> pd.DataFrame: def _to_arrow_internal(self, timeout: Optional[int] = None) -> pa.Table: if self._prefer_ray_datasets: - ray_ds = self._get_ray_dataset() try: - if hasattr(ray_ds, "to_arrow"): - return ray_ds.to_arrow() - else: - df = ray_ds.to_pandas() - return pa.Table.from_pandas(df) + import ray as _ray + + ray_ds = self._get_ray_dataset() + return pa.concat_tables(_ray.get(ray_ds.to_arrow_refs())) except Exception: - df = ray_ds.to_pandas() - return pa.Table.from_pandas(df) + ray_ds = self._get_ray_dataset() + return pa.Table.from_pandas(ray_ds.to_pandas()) else: result = self._resolve() if isinstance(result, pd.DataFrame): @@ -1160,13 +1226,13 @@ def persist( f"Ray offline store only supports SavedDatasetFileStorage, got {type(storage)}" ) destination_path = storage.file_options.uri - if not destination_path.startswith(("s3://", "gs://", "hdfs://")): + if not destination_path.startswith(REMOTE_STORAGE_SCHEMES): if not allow_overwrite and os.path.exists(destination_path): raise SavedDatasetLocationAlreadyExists(location=destination_path) try: ray_ds = self._get_ray_dataset() - if not destination_path.startswith(("s3://", "gs://", "hdfs://")): + if not destination_path.startswith(REMOTE_STORAGE_SCHEMES): os.makedirs(os.path.dirname(destination_path), exist_ok=True) ray_ds.write_parquet(destination_path) @@ -1197,6 +1263,140 @@ def schema(self) -> pa.Schema: return pa.Table.from_pandas(df).schema +def _make_filter_range(timestamp_field: str, start_date: datetime, end_date: datetime): + # Why: factory function for time-range filtering in Ray map_batches + def _filter_range(batch: pd.DataFrame) -> pd.Series: + ts = pd.to_datetime(batch[timestamp_field], utc=True) + return (ts >= start_date) & (ts <= end_date) + + return _filter_range + + +def _make_select_distinct_entity_timestamps(join_keys: List[str], timestamp_field: str): + # Why: factory function for distinct (entity_keys, event_timestamp) projection in Ray map_batches + # This preserves multiple transactions per entity ID with different timestamps for proper PIT joins + def _select_distinct_entity_timestamps(batch: pd.DataFrame) -> pd.DataFrame: + cols = [c for c in join_keys if c in batch.columns] + if timestamp_field in batch.columns: + # Rename timestamp to standardized event_timestamp + batch = batch.copy() + if timestamp_field != "event_timestamp": + batch["event_timestamp"] = batch[timestamp_field] + cols = cols + ["event_timestamp"] + if not cols: + return pd.DataFrame(columns=join_keys + ["event_timestamp"]) + return batch[cols].drop_duplicates().reset_index(drop=True) + + return _select_distinct_entity_timestamps + + +def _distinct_entities_for_feature_view_ray( + store: "RayOfflineStore", + config: RepoConfig, + fv: FeatureView, + registry: BaseRegistry, + project: str, + start_date: datetime, + end_date: datetime, +) -> Tuple[Dataset, List[str]]: + # Why: read minimal columns, filter by time, and project distinct (join_keys, event_timestamp) per FeatureView + # This preserves multiple transactions per entity ID for proper point-in-time joins + entities = fv.entities or [] + entity_objs = [registry.get_entity(e, project) for e in entities] + original_join_keys, _rev_feats, timestamp_field, _created_col = _get_column_names( + fv, entity_objs + ) + + source_info = resolve_feature_view_source_with_fallback( + fv, config, is_materialization=False + ) + required_columns = list(set(original_join_keys + [timestamp_field])) + ds = store._resolve_source_dataset( + source_info.data_source, config, columns=required_columns + ) + + field_mapping = getattr(fv.batch_source, "field_mapping", None) + if field_mapping: + ds = apply_field_mapping(ds, field_mapping) + original_join_keys = [field_mapping.get(k, k) for k in original_join_keys] + timestamp_field = field_mapping.get(timestamp_field, timestamp_field) + + if fv.projection.join_key_map: + join_keys = [ + fv.projection.join_key_map.get(key, key) for key in original_join_keys + ] + else: + join_keys = original_join_keys + + ds = ensure_timestamp_compatibility(ds, [timestamp_field]) + ds = ds.filter(_make_filter_range(timestamp_field, start_date, end_date)) + # Extract distinct (entity_keys, event_timestamp) combinations - not just entity_keys + ds = ds.map_batches( + _make_select_distinct_entity_timestamps(join_keys, timestamp_field), + batch_format="pandas", + ) + return ds, join_keys + + +def _make_align_columns(all_join_keys: List[str], include_timestamp: bool = False): + # Why: factory function for schema alignment in Ray map_batches + # When include_timestamp=True, also aligns event_timestamp column for proper PIT joins + def _align_columns(batch: pd.DataFrame) -> pd.DataFrame: + batch = batch.copy() + output_cols = list(all_join_keys) + if include_timestamp: + output_cols = output_cols + ["event_timestamp"] + for k in output_cols: + if k not in batch.columns: + batch[k] = pd.NA + return batch[output_cols] + + return _align_columns + + +def _make_distinct_by_keys(keys: List[str], include_timestamp: bool = False): + # Why: factory function for deduplication in Ray map_batches + # When include_timestamp=True, deduplicates on (keys + event_timestamp) for proper PIT joins + def _distinct(batch: pd.DataFrame) -> pd.DataFrame: + subset = list(keys) + if include_timestamp and "event_timestamp" in batch.columns: + subset = subset + ["event_timestamp"] + return batch.drop_duplicates(subset=subset).reset_index(drop=True) + + return _distinct + + +def _align_and_union_entities_ray( + datasets: List[Dataset], + all_join_keys: List[str], + include_timestamp: bool = False, +) -> Dataset: + # Why: align schemas across FeatureViews and union to a unified entity set + # When include_timestamp=True, preserves distinct (entity_keys, event_timestamp) combinations + # for proper point-in-time joins with multiple transactions per entity + ray_wrapper = get_ray_wrapper() + output_cols = list(all_join_keys) + if include_timestamp: + output_cols = output_cols + ["event_timestamp"] + if not datasets: + return ray_wrapper.from_pandas(pd.DataFrame(columns=output_cols)) + + aligned = [ + ds.map_batches( + _make_align_columns(all_join_keys, include_timestamp=include_timestamp), + batch_format="pandas", + ) + for ds in datasets + ] + entity_ds = aligned[0] + for ds in aligned[1:]: + entity_ds = entity_ds.union(ds) + return entity_ds.map_batches( + _make_distinct_by_keys(all_join_keys, include_timestamp=include_timestamp), + batch_format="pandas", + ) + + class RayOfflineStore(OfflineStore): def __init__(self) -> None: self._staging_location: Optional[str] = None @@ -1272,6 +1472,60 @@ def _get_source_path(self, source: DataSource, config: RepoConfig) -> str: uri = FileSource.get_uri_for_file_path(repo_path, source.path) return uri + def _resolve_source_dataset( + self, + source: DataSource, + config: RepoConfig, + columns: Optional[List[str]] = None, + ): + """Returns a ray.data.Dataset for a FileSource or RaySource. + + Args: + source: A FileSource or RaySource descriptor. + config: The Feast repo configuration. + columns: Optional list of columns to project. For FileSource the + list is pushed down to ``read_parquet`` as a read-time + optimisation. For RaySource the full dataset is loaded first + and then ``select_columns`` is applied, because most Ray Data + readers do not support column pushdown. + + Returns: + A ray.data.Dataset ready for transformation or point-in-time joins. + + Raises: + ValueError: If source is not a supported DataSource type. + """ + from feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader import ( + load_ray_dataset_from_source, + ) + from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import ( + RaySource, + ) + + if isinstance(source, RaySource): + ds = load_ray_dataset_from_source(source) + if columns: + # Post-load column selection — only keep columns that actually + # exist in the dataset to avoid KeyErrors on sparse sources. + available = set(ds.schema().names) if ds.schema() else set() + safe_cols = [c for c in columns if c in available] + if safe_cols: + ds = ds.select_columns(safe_cols) + return ds + + if isinstance(source, FileSource): + # Legacy path: Parquet via ray_wrapper — push columns down at read + # time so that only the required columns are deserialised. + ray_wrapper = get_ray_wrapper() + source_path = self._get_source_path(source, config) + return ray_wrapper.read_parquet(source_path, columns=columns) + + raise ValueError( + f"Unsupported source type '{type(source).__name__}'. " + f"Use FileSource (Parquet) or RaySource " + f"(images, HuggingFace, CSV, JSON, binary files, MongoDB, and more)." + ) + def _optimize_dataset_for_operation(self, ds: Dataset, operation: str) -> Dataset: """Optimize dataset for specific operations.""" if self._resource_manager is None: @@ -1317,10 +1571,36 @@ def offline_write_batch( if not ray_config.enable_ray_logging: RayOfflineStore._suppress_ray_logging() - assert isinstance(feature_view.batch_source, FileSource) + + from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import ( + RaySource, + ) + + batch_source = feature_view.batch_source + if isinstance(batch_source, RaySource): + if not batch_source.path: + raise NotImplementedError( + f"offline_write_batch is not supported for RaySource '{batch_source.name}' " + f"with reader_type='{batch_source.reader_type}' because it has no writable " + f"file path (e.g. HuggingFace, MongoDB, SQL sources are read-only). " + f"Use a RaySource with reader_type='parquet' and a local or remote path to " + f"enable offline writes." + ) + batch_source_path = batch_source.path + feature_path = batch_source_path + elif isinstance(batch_source, FileSource): + batch_source_path = batch_source.file_options.uri + feature_path = FileSource.get_uri_for_file_path( + repo_path, batch_source_path + ) + else: + raise ValueError( + f"offline_write_batch does not support source type " + f"'{type(batch_source).__name__}'." + ) validation_result = _safe_validate_schema( - config, feature_view.batch_source, table.column_names, "offline_write_batch" + config, batch_source, table.column_names, "offline_write_batch" ) if validation_result: @@ -1332,9 +1612,6 @@ def offline_write_batch( logger.info("Reordering table columns to match expected schema") table = table.select(expected_columns) - batch_source_path = feature_view.batch_source.file_options.uri - feature_path = FileSource.get_uri_for_file_path(repo_path, batch_source_path) - ray_wrapper = get_ray_wrapper() ds = ray_wrapper.from_arrow(table) @@ -1490,7 +1767,7 @@ def _load_and_filter_dataset( @staticmethod def _load_and_filter_dataset_ray( - source_path: str, + source_path: Optional[str], data_source: DataSource, join_key_columns: List[str], feature_name_columns: List[str], @@ -1498,26 +1775,90 @@ def _load_and_filter_dataset_ray( created_timestamp_column: Optional[str], start_date: Optional[datetime], end_date: Optional[datetime], + *, + pre_loaded_ds: Optional[Dataset] = None, ) -> Dataset: + """Load (or accept a pre-loaded) Ray Dataset then apply the shared + filter / transform pipeline. + + When ``pre_loaded_ds`` is supplied the parquet-read and column-projection + steps are skipped; time-range filtering is applied inline so that the + same pipeline covers both ``FileSource`` (path-based) and exotic + ``RaySource`` types (HuggingFace, SQL, MongoDB) whose data is already + loaded by ``load_ray_dataset_from_source``. + """ try: field_mapping = getattr(data_source, "field_mapping", None) - if not feature_name_columns: - columns_to_read = None + if pre_loaded_ds is not None: + ds = pre_loaded_ds + + # Normalise timestamps and apply time-range filter inside + # map_batches so that ds.schema() is NEVER called eagerly. + # Column-existence checks are deferred to each batch so that + # exotic sources whose timestamp column is synthesised inside a + # downstream UDF (e.g. HuggingFace image datasets) are handled + # gracefully: normalization and filtering are simply skipped for + # batches that do not yet contain the column. + _ts_field = timestamp_field + _created_ts = created_timestamp_column + _s_date = ( + make_tzaware(start_date) + if start_date and start_date.tzinfo is None + else start_date + ) + _e_date = ( + make_tzaware(end_date) + if end_date and end_date.tzinfo is None + else end_date + ) + + def _norm_and_filter(batch: pd.DataFrame) -> pd.DataFrame: + batch = make_df_tzaware(batch) + for col in [ + c for c in [_ts_field, _created_ts] if c and c in batch.columns + ]: + batch[col] = ( + pd.to_datetime(batch[col], utc=True, errors="coerce") + .dt.floor("s") + .astype("datetime64[ns, UTC]") + ) + if _ts_field and _ts_field in batch.columns: + if _s_date and _e_date: + batch = batch[ + (batch[_ts_field] >= _s_date) + & (batch[_ts_field] <= _e_date) + ] + elif _s_date: + batch = batch[batch[_ts_field] >= _s_date] + elif _e_date: + batch = batch[batch[_ts_field] <= _e_date] + return batch + + ds = ds.map_batches(_norm_and_filter, batch_format="pandas") else: - columns_to_read = list( - set(join_key_columns + feature_name_columns + [timestamp_field]) + if not feature_name_columns: + columns_to_read = None + else: + columns_to_read = list( + set(join_key_columns + feature_name_columns + [timestamp_field]) + ) + if created_timestamp_column: + columns_to_read.append(created_timestamp_column) + + if source_path is None: + raise ValueError( + "_load_and_filter_dataset_ray requires source_path when " + "pre_loaded_ds is not provided" + ) + ds = RayOfflineStore._create_filtered_dataset( + source_path, + timestamp_field, + start_date, + end_date, + columns=columns_to_read, ) - if created_timestamp_column: - columns_to_read.append(created_timestamp_column) - ds = RayOfflineStore._create_filtered_dataset( - source_path, - timestamp_field, - start_date, - end_date, - columns=columns_to_read, - ) if field_mapping: ds = apply_field_mapping(ds, field_mapping) timestamp_field_mapped = ( @@ -1558,7 +1899,7 @@ def _load_and_filter_dataset_ray( return ds except Exception as e: - raise RuntimeError(f"Failed to load data from {source_path}: {e}") + raise RuntimeError(f"Failed to load/filter dataset: {e}") @staticmethod def _pull_latest_processing_ray( @@ -1597,26 +1938,29 @@ def _pull_latest_processing_ray( if created_timestamp_column_mapped: timestamp_columns.append(created_timestamp_column_mapped) - def deduplicate_batch(batch: pd.DataFrame) -> pd.DataFrame: - if batch.empty: - return batch - - existing_timestamp_columns = [ - col for col in timestamp_columns if col in batch.columns - ] + available_join_keys = [k for k in join_key_columns if k in ds.schema().names] + if not available_join_keys: + return ds - sort_columns = join_key_columns + existing_timestamp_columns - if sort_columns: - batch = batch.sort_values( - sort_columns, - ascending=[True] * len(join_key_columns) - + [False] * len(existing_timestamp_columns), + # groupby().map_groups() co-locates ALL rows for the same entity in a + # single UDF call, guaranteeing correct deduplication regardless of how + # Ray partitions the dataset. sort + map_batches is NOT safe because Ray + # can place the same entity's rows in different partitions after a sort, + # causing duplicates to survive. + def _keep_latest_in_group(group: pd.DataFrame) -> pd.DataFrame: + if group.empty: + return group + existing_ts_cols = [c for c in timestamp_columns if c in group.columns] + if existing_ts_cols: + group = group.sort_values( + existing_ts_cols, + ascending=[False] * len(existing_ts_cols), ) - batch = batch.drop_duplicates(subset=join_key_columns, keep="first") - - return batch + return group.drop_duplicates(subset=available_join_keys, keep="first") - return ds.map_batches(deduplicate_batch, batch_format="pandas") + return ds.groupby(available_join_keys).map_groups( + _keep_latest_in_group, batch_format="pandas" + ) @staticmethod def pull_latest_from_table_or_query( @@ -1632,6 +1976,54 @@ def pull_latest_from_table_or_query( store = RayOfflineStore() store._init_ray(config) + from feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader import ( + load_ray_dataset_from_source, + ) + from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import ( + RaySource, + ) + + if isinstance(data_source, RaySource): + # Filtering only requires a timestamp_field; deduplication additionally + # requires join_key_columns. These are intentionally separate so that + # a feature view with a timestamp but no entities (join_key_columns=[]) + # still gets time-range filtering applied — matching the FileSource path. + # Sources with neither (e.g. HuggingFace image datasets where a + # RayTransformation UDF synthesises all Feast columns) are returned raw. + has_timestamp = bool(timestamp_field) + + def _load_ray_source_dataset(): + raw_ds = load_ray_dataset_from_source(data_source) + if not has_timestamp: + return raw_ds + filtered_ds = store._load_and_filter_dataset_ray( + None, + data_source, + join_key_columns, + feature_name_columns, + timestamp_field, + created_timestamp_column, + start_date, + end_date, + pre_loaded_ds=raw_ds, + ) + field_mapping = getattr(data_source, "field_mapping", None) + # _pull_latest_processing_ray already handles empty join_key_columns + # by returning the dataset unchanged (no dedup without entity keys). + return store._pull_latest_processing_ray( + filtered_ds, + join_key_columns, + timestamp_field, + created_timestamp_column, + field_mapping, + ) + + return RayRetrievalJob( + _load_ray_source_dataset, + staging_location=config.offline_store.storage_path, + config=config.offline_store, + ) + source_path = store._get_source_path(data_source, config) def _load_ray_dataset(): @@ -1696,6 +2088,40 @@ def pull_all_from_table_or_query( store = RayOfflineStore() store._init_ray(config) + from feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader import ( + load_ray_dataset_from_source, + ) + from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import ( + RaySource, + ) + + if isinstance(data_source, RaySource): + # pull_all only needs a timestamp_field to filter by time — join keys + # are not required (no deduplication step, unlike pull_latest). + has_timestamp = bool(timestamp_field) + + def _load_ray_source_all(): + raw_ds = load_ray_dataset_from_source(data_source) + if not has_timestamp: + return raw_ds + return store._load_and_filter_dataset_ray( + None, + data_source, + join_key_columns, + feature_name_columns, + timestamp_field, + created_timestamp_column, + start_date, + end_date, + pre_loaded_ds=raw_ds, + ) + + return RayRetrievalJob( + _load_ray_source_all, + staging_location=config.offline_store.storage_path, + config=config.offline_store, + ) + source_path = store._get_source_path(data_source, config) fs, path_in_fs = fsspec.core.url_to_fs(source_path) @@ -1791,12 +2217,19 @@ def normalize_timestamps(batch: pd.DataFrame) -> pd.DataFrame: filesystem, resolved_path = FileSource.create_filesystem_and_path( absolute_path, destination.s3_endpoint_override ) - path_obj = Path(resolved_path) - if path_obj.suffix == ".parquet": - path_obj = path_obj.with_suffix("") - if not absolute_path.startswith(("s3://", "gs://")): + if absolute_path.startswith(REMOTE_STORAGE_SCHEMES): + write_path = ( + absolute_path[:-8] + if absolute_path.endswith(".parquet") + else absolute_path + ) + else: + path_obj = Path(resolved_path) + if path_obj.suffix == ".parquet": + path_obj = path_obj.with_suffix("") path_obj.mkdir(parents=True, exist_ok=True) - ds.write_parquet(str(path_obj)) + write_path = str(path_obj) + ds.write_parquet(write_path) except Exception as e: raise RuntimeError(f"Failed to write logged features: {e}") @@ -1874,17 +2307,43 @@ def get_historical_features( config: RepoConfig, feature_views: List[FeatureView], feature_refs: List[str], - entity_df: Union[pd.DataFrame, str], + entity_df: Optional[Union[pd.DataFrame, str]], registry: BaseRegistry, project: str, full_feature_names: bool = False, + **kwargs: Any, ) -> RetrievalJob: store = RayOfflineStore() store._init_ray(config) - # Load entity_df as Ray dataset for distributed processing + # Load or derive entity dataset for distributed processing ray_wrapper = get_ray_wrapper() - if isinstance(entity_df, str): + if entity_df is None: + # Non-entity mode: derive entity set from feature sources within a bounded time window + # Preserves distinct (entity_keys, event_timestamp) combinations for proper PIT joins + # This handles cases where multiple transactions per entity ID exist + start_date, end_date = compute_non_entity_date_range( + feature_views, + start_date=kwargs.get("start_date"), + end_date=kwargs.get("end_date"), + ) + per_view_entity_ds: List[Dataset] = [] + all_join_keys: List[str] = [] + for fv in feature_views: + ds, join_keys = _distinct_entities_for_feature_view_ray( + store, config, fv, registry, project, start_date, end_date + ) + per_view_entity_ds.append(ds) + for k in join_keys: + if k not in all_join_keys: + all_join_keys.append(k) + # Use include_timestamp=True to preserve actual event_timestamp from data + # instead of assigning a fixed end_date to all entities + entity_ds = _align_and_union_entities_ray( + per_view_entity_ds, all_join_keys, include_timestamp=True + ) + entity_df_sample = entity_ds.limit(1000).to_pandas() + elif isinstance(entity_df, str): entity_ds = ray_wrapper.read_csv(entity_df) entity_df_sample = entity_ds.limit(1000).to_pandas() else: @@ -1966,7 +2425,7 @@ def get_historical_features( # Build reverse field mapping to get actual source column names reverse_field_mapping = {} - if fv.batch_source.field_mapping: + if fv.batch_source is not None and fv.batch_source.field_mapping: reverse_field_mapping = { v: k for k, v in fv.batch_source.field_mapping.items() } @@ -1981,20 +2440,25 @@ def get_historical_features( fv, config, is_materialization=False ) - # Read from the resolved data source - source_path = store._get_source_path(source_info.data_source, config) - + # Read from the resolved data source — works for both FileSource + # and RaySource. Column pushdown is applied for FileSource; for + # RaySource select_columns is applied post-load inside + # _resolve_source_dataset. if not source_info.has_transformation: required_feature_columns = set( original_join_keys + requested_feats + [timestamp_field] ) if created_col: required_feature_columns.add(created_col) - feature_ds = ray_wrapper.read_parquet( - source_path, columns=list(required_feature_columns) + feature_ds = store._resolve_source_dataset( + source_info.data_source, + config, + columns=list(required_feature_columns), ) else: - feature_ds = ray_wrapper.read_parquet(source_path) + feature_ds = store._resolve_source_dataset( + source_info.data_source, config + ) # Apply transformation if available if source_info.has_transformation and source_info.transformation_func: diff --git a/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray_offline_store_reader.py b/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray_offline_store_reader.py new file mode 100644 index 00000000000..ecea76842a2 --- /dev/null +++ b/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray_offline_store_reader.py @@ -0,0 +1,126 @@ +import logging +from typing import Any + +from feast.infra.ray_initializer import get_ray_wrapper + +logger = logging.getLogger(__name__) + + +def load_ray_dataset_from_source(source: Any) -> Any: + """Loads a ray.data.Dataset from a RaySource descriptor. + + All ray.data loading logic for RaySource lives here, in the offline store + layer. The RaySource descriptor carries only metadata (reader_type, path, + reader_options); it contains no loading logic itself. + + This follows the same pattern as SparkOfflineStore, which reads + SparkSource.file_format and calls spark.read.format(fmt).load(path) — the + source describes what to read, the store decides how. + + Every reader type is dispatched through get_ray_wrapper() so that: + - Local / GCS-connected mode → StandardRayWrapper → direct ray.data call + - KubeRay / Ray Client mode → CodeFlareRayWrapper → @ray.remote dispatch + + This ensures ray.data operations always run on the cluster, never on a thin + Ray Client driver where they are not supported. + + Path semantics for file-backed reader types (``parquet``, ``csv``, …): + - Remote URIs (``s3://``, ``gs://``, ``hdfs://``) are passed through unchanged; + Ray Data resolves them natively. + - Absolute local paths are passed through unchanged. + - Relative paths are passed through unchanged and resolved by each Ray worker + relative to its own working directory. This is intentional for shared + storage setups (e.g. a PVC mounted at the same path on every node) where + the path is meaningful to the workers, not to the driver. Unlike + ``FileSource``, ``RaySource`` data is always read on workers, never on the + driver, so driver-side ``repo_path`` resolution would produce incorrect + absolute paths on remote workers. + + Args: + source: A RaySource instance. + + Returns: + A ray.data.Dataset (or RemoteDatasetProxy for KubeRay mode) produced by + the reader indicated by source.reader_type. + + Raises: + ValueError: If source.reader_type is not a recognised reader type. + ImportError: If a reader-specific dependency (e.g. datasets) is missing. + """ + reader_type = source.reader_type + path = source.path or "" + opts = source.reader_options or {} + + logger.debug( + "RayOfflineStore: reading RaySource '%s' (reader_type=%s, path=%r)", + source.name, + reader_type, + path, + ) + + ray_wrapper = get_ray_wrapper() + + if reader_type == "parquet": + return ray_wrapper.read_parquet(path, **opts) + + if reader_type == "csv": + return ray_wrapper.read_csv(path, **opts) + + if reader_type == "json": + return ray_wrapper.read_json(path, **opts) + + if reader_type == "text": + return ray_wrapper.read_text(path, **opts) + + if reader_type == "images": + return ray_wrapper.read_images(path, **opts) + + if reader_type == "binary_files": + return ray_wrapper.read_binary_files(path, **opts) + + if reader_type == "tfrecords": + return ray_wrapper.read_tfrecords(path, **opts) + + if reader_type == "webdataset": + return ray_wrapper.read_webdataset(path, **opts) + + if reader_type == "huggingface": + dataset_name = opts.get("dataset_name") or path + split = opts.get("split", "train") + extra = { + k: v + for k, v in opts.items() + if k not in ("dataset_name", "split", "trust_remote_code") + } + return ray_wrapper.from_huggingface(dataset_name, split=split, **extra) + + if reader_type == "mongo": + return ray_wrapper.read_mongo( + uri=opts["uri"], + database=opts["database"], + collection=opts["collection"], + **{ + k: v + for k, v in opts.items() + if k not in ("uri", "database", "collection") + }, + ) + + if reader_type == "sql": + # Pass connection_url as a plain string so it is serialisable across + # the Ray object store boundary (CodeFlareRayWrapper rebuilds the + # connection_factory on the remote worker). + return ray_wrapper.read_sql( + sql=opts["sql"], + connection_url=opts["connection_url"], + **{k: v for k, v in opts.items() if k not in ("sql", "connection_url")}, + ) + + from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import ( + SUPPORTED_READER_TYPES, + ) + + raise ValueError( + f"Unknown reader_type '{reader_type}' on RaySource '{source.name}'. " + f"Supported types: {sorted(SUPPORTED_READER_TYPES)}" + ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray_source.py b/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray_source.py new file mode 100644 index 00000000000..0da9fe4f9dd --- /dev/null +++ b/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/ray_source.py @@ -0,0 +1,283 @@ +import json +import logging +from typing import Any, Callable, Dict, Iterable, Optional, Tuple + +from feast.data_source import DataSource +from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto +from feast.repo_config import RepoConfig +from feast.type_map import pa_to_feast_value_type +from feast.value_type import ValueType + +logger = logging.getLogger(__name__) + +SUPPORTED_READER_TYPES = ( + "parquet", + "csv", + "json", + "text", + "images", + "binary_files", + "tfrecords", + "webdataset", + "huggingface", + "mongo", + "sql", +) + + +class RaySource(DataSource): + """A RaySource object defines a data source that a RayOfflineStore can use. + + RaySource supports any format that ray.data can read: image directories, HuggingFace datasets, + binary files, JSON, CSV, MongoDB, WebDataset shards, and more. + + The source stores only metadata (reader_type, path, reader_options). All + data-loading logic lives in the RayOfflineStore, which reads reader_type and + dispatches to the appropriate ray.data.read_*() or ray.data.from_*() call. + + Supported reader types: + parquet ray.data.read_parquet(path) + csv ray.data.read_csv(path) + json ray.data.read_json(path) + text ray.data.read_text(path) + images ray.data.read_images(path) + binary_files ray.data.read_binary_files(path) + tfrecords ray.data.read_tfrecords(path) + webdataset ray.data.read_webdataset(path) + huggingface ray.data.from_huggingface(dataset_name, split=...) + mongo ray.data.read_mongo(uri, database, collection) + sql ray.data.read_sql(sql, connection_factory) + + Examples: + >>> # Directory of PNG images + >>> source = RaySource( + ... name="cheque_images", + ... reader_type="images", + ... path="s3://bucket/cheques/", + ... timestamp_field="event_timestamp", + ... ) + + >>> # HuggingFace dataset + >>> source = RaySource( + ... name="cheque_hf", + ... reader_type="huggingface", + ... reader_options={ + ... "dataset_name": "cheques_sample_data", + ... "split": "test", + ... }, + ... timestamp_field="event_timestamp", + ... ) + """ + + def source_type(self) -> DataSourceProto.SourceType.ValueType: + return DataSourceProto.CUSTOM_SOURCE + + def __init__( + self, + *, + name: str, + reader_type: str, + path: Optional[str] = None, + reader_options: Optional[Dict[str, Any]] = None, + created_timestamp_column: Optional[str] = None, + field_mapping: Optional[Dict[str, str]] = None, + description: Optional[str] = "", + tags: Optional[Dict[str, str]] = None, + owner: Optional[str] = "", + timestamp_field: Optional[str] = None, + ): + """Creates a RaySource object. + + Args: + name: The name of the data source, which should be unique within a project. + reader_type: The ray.data reader to use. One of: parquet, csv, json, text, + images, binary_files, tfrecords, webdataset, huggingface, mongo, sql. + RayOfflineStore dispatches on this value to call the matching + ray.data.read_*() or ray.data.from_*() function. + path: File path or directory for file-based reader types (parquet, csv, + json, text, images, binary_files, tfrecords, webdataset). Not required + for huggingface, mongo, or sql. + reader_options: Reader-type-specific connection parameters as a + JSON-serializable dict. Examples: + huggingface: {"dataset_name": "org/name", "split": "train"} + mongo: {"uri": "mongodb://host", "database": "db", + "collection": "col"} + sql: {"sql": "SELECT …", "connection_url": "sqlite:///…"} + created_timestamp_column: Timestamp column indicating when the row + was created, used for deduplicating rows. + field_mapping: A dictionary mapping of column names in this data + source to feature names in a feature table or view. + description: A human-readable description. + tags: A dictionary of key-value pairs to store arbitrary metadata. + owner: The owner of the DataSource, typically the email of the primary + maintainer. + timestamp_field: Event timestamp field used for point-in-time joins of + feature values. + """ + if reader_type not in SUPPORTED_READER_TYPES: + raise ValueError( + f"'reader_type' must be one of {SUPPORTED_READER_TYPES}, " + f"got '{reader_type}'." + ) + + super().__init__( + name=name, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + field_mapping=field_mapping, + description=description, + tags=tags, + owner=owner, + ) + + self.ray_source_options = RaySourceOptions( + reader_type=reader_type, + path=path or "", + reader_options=reader_options or {}, + ) + + @property + def reader_type(self) -> str: + """Returns the ray.data reader type of this data source.""" + return self.ray_source_options.reader_type + + @property + def path(self) -> str: + """Returns the path of this data source.""" + return self.ray_source_options.path + + @property + def reader_options(self) -> Dict[str, Any]: + """Returns the reader-specific options of this data source.""" + return self.ray_source_options.reader_options + + @staticmethod + def from_proto(data_source: DataSourceProto) -> Any: + """Creates a RaySource from a protobuf representation. + + Args: + data_source: A protobuf representation of a DataSource. + + Returns: + A RaySource object. + """ + assert data_source.type == DataSourceProto.CUSTOM_SOURCE + config = json.loads(data_source.custom_options.configuration) + return RaySource( + name=data_source.name, + reader_type=config["reader_type"], + path=config.get("path") or None, + reader_options=config.get("reader_options") or None, + field_mapping=dict(data_source.field_mapping), + timestamp_field=data_source.timestamp_field or None, + created_timestamp_column=data_source.created_timestamp_column or None, + description=data_source.description, + tags=dict(data_source.tags), + owner=data_source.owner, + ) + + def _to_proto_impl(self) -> DataSourceProto: + data_source_proto = DataSourceProto( + name=self.name, + type=DataSourceProto.CUSTOM_SOURCE, + data_source_class_type=( + "feast.infra.offline_stores.contrib.ray_offline_store" + ".ray_source.RaySource" + ), + field_mapping=self.field_mapping, + description=self.description, + tags=self.tags, + owner=self.owner, + custom_options=DataSourceProto.CustomSourceOptions( + configuration=json.dumps( + { + "reader_type": self.reader_type, + "path": self.path, + "reader_options": self.reader_options, + } + ).encode() + ), + ) + data_source_proto.timestamp_field = self.timestamp_field + data_source_proto.created_timestamp_column = self.created_timestamp_column + return data_source_proto + + def validate(self, config: RepoConfig) -> None: + pass + + @staticmethod + def source_datatype_to_feast_value_type() -> Callable[[str], ValueType]: + return pa_to_feast_value_type + + def get_table_column_names_and_types( + self, config: RepoConfig + ) -> Iterable[Tuple[str, str]]: + # Return an empty list so that inference.py skips schema/entity inference + # for RaySource during feast apply. Schema is resolved at materialisation + # time by RayOfflineStore when it reads the actual Ray Dataset. + return [] + + def get_table_query_string(self) -> str: + """Returns a string that can be used to reference this source.""" + parts = [f"reader_type={self.reader_type!r}"] + if self.path: + parts.append(f"path={self.path!r}") + if self.reader_options: + parts.append(f"reader_options={self.reader_options!r}") + return f"RaySource({', '.join(parts)})" + + def __eq__(self, other): + if not isinstance(other, RaySource): + return False + base_eq = super().__eq__(other) + if not base_eq: + return False + return ( + self.reader_type == other.reader_type + and self.path == other.path + and self.reader_options == other.reader_options + ) + + def __hash__(self): + return super().__hash__() + + +class RaySourceOptions: + """Options specific to a RaySource.""" + + def __init__( + self, + reader_type: str, + path: str, + reader_options: Dict[str, Any], + ): + self._reader_type = reader_type + self._path = path + self._reader_options = reader_options + + @property + def reader_type(self) -> str: + """Returns the ray.data reader type.""" + return self._reader_type + + @reader_type.setter + def reader_type(self, reader_type: str) -> None: + self._reader_type = reader_type + + @property + def path(self) -> str: + """Returns the file path or directory.""" + return self._path + + @path.setter + def path(self, path: str) -> None: + self._path = path + + @property + def reader_options(self) -> Dict[str, Any]: + """Returns the reader-specific options dict.""" + return self._reader_options + + @reader_options.setter + def reader_options(self, reader_options: Dict[str, Any]) -> None: + self._reader_options = reader_options diff --git a/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/tests/test_ray_integration.py b/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/tests/test_ray_integration.py deleted file mode 100644 index 5ab82f8ef47..00000000000 --- a/sdk/python/feast/infra/offline_stores/contrib/ray_offline_store/tests/test_ray_integration.py +++ /dev/null @@ -1,146 +0,0 @@ -import pandas as pd -import pytest - -from feast.utils import _utc_now -from tests.integration.feature_repos.repo_configuration import ( - construct_universal_feature_views, -) -from tests.integration.feature_repos.universal.entities import driver - - -@pytest.mark.integration -@pytest.mark.universal_offline_stores -def test_ray_offline_store_basic_write_and_read(environment, universal_data_sources): - """Test basic write and read functionality with Ray offline store.""" - store = environment.feature_store - _, _, data_sources = universal_data_sources - feature_views = construct_universal_feature_views(data_sources) - driver_fv = feature_views.driver - store.apply([driver(), driver_fv]) - - now = _utc_now() - ts = pd.Timestamp(now).round("ms") - - # Write data to offline store - df_to_write = pd.DataFrame.from_dict( - { - "event_timestamp": [ts, ts], - "driver_id": [1001, 1002], - "conv_rate": [0.1, 0.2], - "acc_rate": [0.9, 0.8], - "avg_daily_trips": [10, 20], - "created": [ts, ts], - }, - ) - - store.write_to_offline_store( - driver_fv.name, df_to_write, allow_registry_cache=False - ) - - # Read data back - entity_df = pd.DataFrame({"driver_id": [1001, 1002], "event_timestamp": [ts, ts]}) - - result_df = store.get_historical_features( - entity_df=entity_df, - features=[ - "driver_stats:conv_rate", - "driver_stats:acc_rate", - "driver_stats:avg_daily_trips", - ], - full_feature_names=False, - ).to_df() - - assert len(result_df) == 2 - assert "conv_rate" in result_df.columns - assert "acc_rate" in result_df.columns - assert "avg_daily_trips" in result_df.columns - - -@pytest.mark.integration -@pytest.mark.universal_offline_stores -@pytest.mark.parametrize("full_feature_names", [True, False], ids=lambda v: f"full:{v}") -def test_ray_offline_store_historical_features( - environment, universal_data_sources, full_feature_names -): - """Test historical features retrieval with Ray offline store.""" - store = environment.feature_store - - (entities, datasets, data_sources) = universal_data_sources - feature_views = construct_universal_feature_views(data_sources) - - entity_df_with_request_data = datasets.entity_df.copy(deep=True) - entity_df_with_request_data["val_to_add"] = [ - i for i in range(len(entity_df_with_request_data)) - ] - - store.apply( - [ - driver(), - *feature_views.values(), - ] - ) - - job = store.get_historical_features( - entity_df=entity_df_with_request_data, - features=[ - "driver_stats:conv_rate", - "driver_stats:avg_daily_trips", - "customer_profile:current_balance", - "conv_rate_plus_100:conv_rate_plus_100", - ], - full_feature_names=full_feature_names, - ) - - # Test DataFrame conversion - result_df = job.to_df() - assert len(result_df) > 0 - assert "event_timestamp" in result_df.columns - - # Test Arrow conversion - result_table = job.to_arrow().to_pandas() - assert len(result_table) > 0 - assert "event_timestamp" in result_table.columns - - -@pytest.mark.integration -@pytest.mark.universal_offline_stores -def test_ray_offline_store_persist(environment, universal_data_sources): - """Test dataset persistence with Ray offline store.""" - store = environment.feature_store - - (entities, datasets, data_sources) = universal_data_sources - feature_views = construct_universal_feature_views(data_sources) - - entity_df_with_request_data = datasets.entity_df.copy(deep=True) - entity_df_with_request_data["val_to_add"] = [ - i for i in range(len(entity_df_with_request_data)) - ] - - store.apply( - [ - driver(), - *feature_views.values(), - ] - ) - - job = store.get_historical_features( - entity_df=entity_df_with_request_data, - features=[ - "driver_stats:conv_rate", - "customer_profile:current_balance", - ], - full_feature_names=False, - ) - - # Test persisting the dataset - from feast.saved_dataset import SavedDatasetFileStorage - - storage = SavedDatasetFileStorage(path="data/test_saved_dataset.parquet") - saved_path = job.persist(storage, allow_overwrite=True) - - assert saved_path == "data/test_saved_dataset.parquet" - - # Verify the saved dataset exists - import os - - assert os.path.exists(saved_path) diff --git a/sdk/python/feast/infra/offline_stores/contrib/ray_repo_configuration.py b/sdk/python/feast/infra/offline_stores/contrib/ray_repo_configuration.py index 6e1fa66b102..fcab38a0eb9 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/ray_repo_configuration.py +++ b/sdk/python/feast/infra/offline_stores/contrib/ray_repo_configuration.py @@ -15,10 +15,10 @@ ) from feast.repo_config import FeastConfigBaseModel from feast.saved_dataset import SavedDatasetStorage -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py index f1ba4baa939..7e0a03e69bb 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark.py @@ -1,20 +1,24 @@ +import json import os import tempfile import uuid import warnings from dataclasses import asdict, dataclass -from datetime import datetime, timezone +from datetime import date, datetime, timezone +from datetime import time as dt_time from typing import ( TYPE_CHECKING, Any, Callable, Dict, + KeysView, List, Optional, Tuple, Union, cast, ) +from urllib.parse import urlparse if TYPE_CHECKING: from feast.saved_dataset import ValidationReference @@ -23,6 +27,7 @@ import pandas import pandas as pd import pyarrow +import pyarrow.dataset as ds import pyarrow.parquet as pq import pyspark from pydantic import StrictStr @@ -30,6 +35,7 @@ from pyspark.sql import SparkSession from feast import FeatureView, OnDemandFeatureView +from feast.batch_feature_view import BatchFeatureView from feast.data_source import DataSource from feast.dataframe import DataFrameEngine, FeastDataFrame from feast.errors import EntitySQLEmptyResults, InvalidEntityType @@ -46,10 +52,21 @@ ) from feast.infra.offline_stores.offline_utils import get_timestamp_filter_sql from feast.infra.registry.base_registry import BaseRegistry +from feast.monitoring.monitoring_utils import ( + MON_TABLE_FEATURE, + MON_TABLE_FEATURE_SERVICE, + MON_TABLE_FEATURE_VIEW, + MON_TABLE_JOB, + empty_categorical_metric, + empty_numeric_metric, + monitoring_table_meta, + normalize_monitoring_row, + opt_float, +) from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage from feast.type_map import spark_schema_to_np_dtypes -from feast.utils import _get_fields_with_aliases +from feast.utils import _get_fields_with_aliases, compute_non_entity_date_range # Make sure spark warning doesn't raise more than once. warnings.simplefilter("once", RuntimeWarning) @@ -72,10 +89,12 @@ class SparkOfflineStoreConfig(FeastConfigBaseModel): @dataclass(frozen=True) class SparkFeatureViewQueryContext(offline_utils.FeatureViewQueryContext): min_date_partition: Optional[str] - max_date_partition: str + max_date_partition: Optional[str] class SparkOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -151,18 +170,32 @@ def get_historical_features( config: RepoConfig, feature_views: List[FeatureView], feature_refs: List[str], - entity_df: Union[pandas.DataFrame, str, pyspark.sql.DataFrame], + entity_df: Optional[Union[pandas.DataFrame, str, pyspark.sql.DataFrame]], registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, + **kwargs, ) -> RetrievalJob: assert isinstance(config.offline_store, SparkOfflineStoreConfig) date_partition_column_formats = [] for fv in feature_views: - assert isinstance(fv.batch_source, SparkSource) - date_partition_column_formats.append( - fv.batch_source.date_partition_column_format - ) + if isinstance(fv.batch_source, SparkSource): + date_partition_column_formats.append( + fv.batch_source.date_partition_column_format + ) + else: + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, + ) + + if isinstance(fv.batch_source, IcebergSource): + date_partition_column_formats.append(None) + else: + raise ValueError( + f"SparkOfflineStore requires SparkSource or IcebergSource, " + f"got {type(fv.batch_source)}" + ) warnings.warn( "The spark offline store is an experimental feature in alpha development. " @@ -173,35 +206,92 @@ def get_historical_features( spark_session = get_spark_session_or_start_new_with_repoconfig( store_config=config.offline_store ) - tmp_entity_df_table_name = offline_utils.get_temp_entity_table_name() - entity_schema = _get_entity_schema( - spark_session=spark_session, - entity_df=entity_df, - ) - event_timestamp_col = offline_utils.infer_event_timestamp_from_entity_df( - entity_schema=entity_schema, - ) - entity_df_event_timestamp_range = _get_entity_df_event_timestamp_range( - entity_df, - event_timestamp_col, - spark_session, - ) - _upload_entity_df( - spark_session=spark_session, - table_name=tmp_entity_df_table_name, - entity_df=entity_df, - event_timestamp_col=event_timestamp_col, + # Pre-register IcebergSource feature views as temp views + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, ) - expected_join_keys = offline_utils.get_expected_join_keys( - project=project, feature_views=feature_views, registry=registry - ) - offline_utils.assert_expected_columns_in_entity_df( - entity_schema=entity_schema, - join_keys=expected_join_keys, - entity_df_event_timestamp_col=event_timestamp_col, - ) + for fv in feature_views: + if isinstance(fv.batch_source, IcebergSource): + _register_iceberg_source_as_temp_view( + spark_session, fv.batch_source, config + ) + + tmp_entity_df_table_name = offline_utils.get_temp_entity_table_name() + + # Non-entity mode: synthesize a left table and timestamp range from start/end dates to avoid requiring entity_df. + # This makes date-range retrievals possible without enumerating entities upfront; sources remain bounded by time. + non_entity_mode = entity_df is None + if non_entity_mode: + start_date, end_date = compute_non_entity_date_range( + feature_views, + start_date=kwargs.get("start_date"), + end_date=kwargs.get("end_date"), + ) + entity_df_event_timestamp_range = (start_date, end_date) + + # Build query contexts so we can reuse entity names and per-view table info consistently. + fv_query_contexts = offline_utils.get_feature_view_query_context( + feature_refs, + feature_views, + registry, + project, + entity_df_event_timestamp_range, + ) + + # Collect the union of entity columns required across all feature views. + all_entities = _gather_all_entities(fv_query_contexts) + + # Build a UNION DISTINCT of per-feature-view entity projections, time-bounded and partition-pruned. + _create_temp_entity_union_view( + spark_session=spark_session, + tmp_view_name=tmp_entity_df_table_name, + feature_views=feature_views, + fv_query_contexts=fv_query_contexts, + start_date=start_date, + end_date=end_date, + date_partition_column_formats=date_partition_column_formats, + ) + + # Add a stable as-of timestamp column for PIT joins. + left_table_query_string, event_timestamp_col = _make_left_table_query( + end_date=end_date, tmp_view_name=tmp_entity_df_table_name + ) + entity_schema_keys = _entity_schema_keys_from( + all_entities=all_entities, event_timestamp_col=event_timestamp_col + ) + else: + entity_schema = _get_entity_schema( + spark_session=spark_session, + entity_df=entity_df, + ) + event_timestamp_col = offline_utils.infer_event_timestamp_from_entity_df( + entity_schema=entity_schema, + ) + entity_df_event_timestamp_range = _get_entity_df_event_timestamp_range( + entity_df, + event_timestamp_col, + spark_session, + ) + _upload_entity_df( + spark_session=spark_session, + table_name=tmp_entity_df_table_name, + entity_df=entity_df, + event_timestamp_col=event_timestamp_col, + ) + left_table_query_string = tmp_entity_df_table_name + entity_schema_keys = cast(KeysView[str], entity_schema.keys()) + + if not non_entity_mode: + expected_join_keys = offline_utils.get_expected_join_keys( + project=project, feature_views=feature_views, registry=registry + ) + offline_utils.assert_expected_columns_in_entity_df( + entity_schema=entity_schema, + join_keys=expected_join_keys, + entity_df_event_timestamp_col=event_timestamp_col, + ) query_context = offline_utils.get_feature_view_query_context( feature_refs, @@ -211,17 +301,25 @@ def get_historical_features( entity_df_event_timestamp_range, ) + query_context = _apply_bfv_transformations( + spark_session=spark_session, + feature_views=feature_views, + query_contexts=query_context, + ) + spark_query_context = [ SparkFeatureViewQueryContext( **asdict(context), min_date_partition=datetime.fromisoformat( context.min_event_timestamp ).strftime(date_format) - if context.min_event_timestamp is not None + if context.min_event_timestamp is not None and date_format is not None else None, max_date_partition=datetime.fromisoformat( context.max_event_timestamp - ).strftime(date_format), + ).strftime(date_format) + if date_format is not None + else None, ) for date_format, context in zip( date_partition_column_formats, query_context @@ -232,11 +330,12 @@ def get_historical_features( feature_view_query_contexts=cast( List[offline_utils.FeatureViewQueryContext], spark_query_context ), - left_table_query_string=tmp_entity_df_table_name, + left_table_query_string=left_table_query_string, entity_df_event_timestamp_col=event_timestamp_col, - entity_df_columns=entity_schema.keys(), + entity_df_columns=entity_schema_keys, query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, full_feature_names=full_feature_names, + filter_by_created_timestamp=filter_by_created_timestamp, ) return SparkRetrievalJob( @@ -248,7 +347,7 @@ def get_historical_features( ), metadata=RetrievalMetadata( features=feature_refs, - keys=list(set(entity_schema.keys()) - {event_timestamp_col}), + keys=list(set(entity_schema_keys) - {event_timestamp_col}), min_event_timestamp=entity_df_event_timestamp_range[0], max_event_timestamp=entity_df_event_timestamp_range[1], ), @@ -324,7 +423,6 @@ def pull_all_from_table_or_query( source table and those column names are the values passed into this function. """ assert isinstance(config.offline_store, SparkOfflineStoreConfig) - assert isinstance(data_source, SparkSource) warnings.warn( "The spark offline store is an experimental feature in alpha development. " "This API is unstable and it could and most probably will be changed in the future.", @@ -335,15 +433,40 @@ def pull_all_from_table_or_query( store_config=config.offline_store ) + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, + ) + + if isinstance(data_source, IcebergSource): + return _pull_from_iceberg_source( + spark_session=spark_session, + data_source=data_source, + join_key_columns=join_key_columns, + feature_name_columns=feature_name_columns, + timestamp_field=timestamp_field, + created_timestamp_column=created_timestamp_column, + start_date=start_date, + end_date=end_date, + config=config, + ) + + assert isinstance(data_source, SparkSource) + timestamp_fields = [timestamp_field] if created_timestamp_column: timestamp_fields.append(created_timestamp_column) - (fields_with_aliases, aliases) = _get_fields_with_aliases( - fields=join_key_columns + feature_name_columns + timestamp_fields, - field_mappings=data_source.field_mapping, - ) - fields_with_alias_string = ", ".join(fields_with_aliases) + if feature_name_columns: + (fields_with_aliases, _) = _get_fields_with_aliases( + fields=join_key_columns + feature_name_columns + timestamp_fields, + field_mappings=data_source.field_mapping, + ) + fields_with_alias_string = ", ".join(fields_with_aliases) + else: + # Empty feature_name_columns signals "read all source columns". + # Used by BatchFeatureView with TransformationMode.PYTHON/ray/pandas where + # the UDF computes output features from raw input — don't project upfront. + fields_with_alias_string = "*" from_expression = data_source.get_table_query_string() timestamp_filter = get_timestamp_filter_sql( @@ -363,6 +486,494 @@ def pull_all_from_table_or_query( config=config, ) + @staticmethod + def compute_monitoring_metrics( + config: RepoConfig, + data_source: DataSource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, SparkOfflineStoreConfig) + assert isinstance(data_source, SparkSource) + + spark_session = get_spark_session_or_start_new_with_repoconfig( + store_config=config.offline_store + ) + from_expression = data_source.get_table_query_string() + ts_filter = get_timestamp_filter_sql( + start_date, + end_date, + timestamp_field, + tz=timezone.utc, + quote_fields=False, + ) + ts_clause = ts_filter if ts_filter else "1=1" + + numeric_features = [n for n, t in feature_columns if t == "numeric"] + categorical_features = [n for n, t in feature_columns if t == "categorical"] + results: List[Dict[str, Any]] = [] + + if numeric_features: + results.extend( + _spark_sql_numeric_stats( + spark_session, + from_expression, + numeric_features, + ts_clause, + histogram_bins, + ) + ) + + for col_name in categorical_features: + results.append( + _spark_sql_categorical_stats( + spark_session, + from_expression, + col_name, + ts_clause, + top_n, + ) + ) + + return results + + @staticmethod + def get_monitoring_max_timestamp( + config: RepoConfig, + data_source: DataSource, + timestamp_field: str, + ) -> Optional[datetime]: + assert isinstance(config.offline_store, SparkOfflineStoreConfig) + assert isinstance(data_source, SparkSource) + + spark_session = get_spark_session_or_start_new_with_repoconfig( + store_config=config.offline_store + ) + from_expression = data_source.get_table_query_string() + q_ts = f"`{timestamp_field}`" + sql = f"SELECT MAX({q_ts}) AS max_ts FROM {from_expression} AS _src" + row = spark_session.sql(sql).collect() + if not row or row[0][0] is None: + return None + val = row[0][0] + if isinstance(val, datetime): + return val if val.tzinfo else val.replace(tzinfo=timezone.utc) + if isinstance(val, date): + return datetime.combine(val, dt_time.min, tzinfo=timezone.utc) + return pandas.to_datetime(val, utc=True).to_pydatetime() + + @staticmethod + def ensure_monitoring_tables(config: RepoConfig) -> None: + assert isinstance(config.offline_store, SparkOfflineStoreConfig) + spark_session = get_spark_session_or_start_new_with_repoconfig( + store_config=config.offline_store + ) + for stmt in _SPARK_MONITORING_DDL_STATEMENTS: + spark_session.sql(stmt) + + @staticmethod + def save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], + ) -> None: + if not metrics: + return + assert isinstance(config.offline_store, SparkOfflineStoreConfig) + table, columns, pk_columns = monitoring_table_meta(metric_type) + pdf_new = pd.DataFrame([{c: m.get(c) for c in columns} for m in metrics]) + pdf_new = _spark_normalize_histogram_column(pdf_new) + + spark_session = get_spark_session_or_start_new_with_repoconfig( + store_config=config.offline_store + ) + if spark_session.catalog.tableExists(table): + pdf_old = spark_session.table(table).toPandas() + pdf_merged = _spark_pandas_upsert(pdf_old, pdf_new, pk_columns) + else: + pdf_merged = pdf_new + + spark_session.createDataFrame(pdf_merged).write.mode("overwrite").saveAsTable( + table + ) + + @staticmethod + def query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, SparkOfflineStoreConfig) + table, columns, _ = monitoring_table_meta(metric_type) + spark_session = get_spark_session_or_start_new_with_repoconfig( + store_config=config.offline_store + ) + if not spark_session.catalog.tableExists(table): + return [] + + from pyspark.sql import functions as F + + df = spark_session.table(table) + if project: + df = df.filter(F.col("project_id") == project) + if filters: + for key, value in filters.items(): + if value is not None: + df = df.filter(F.col(key) == value) + if start_date is not None and "metric_date" in df.columns: + df = df.filter(F.col("metric_date") >= F.lit(start_date)) + if end_date is not None and "metric_date" in df.columns: + df = df.filter(F.col("metric_date") <= F.lit(end_date)) + + order_col = "metric_date" if "metric_date" in df.columns else "job_id" + rows = df.orderBy(order_col).collect() + return _spark_rows_to_metric_dicts(rows, columns) + + @staticmethod + def clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> None: + assert isinstance(config.offline_store, SparkOfflineStoreConfig) + spark_session = get_spark_session_or_start_new_with_repoconfig( + store_config=config.offline_store + ) + if not spark_session.catalog.tableExists(MON_TABLE_FEATURE): + return + + pdf = spark_session.table(MON_TABLE_FEATURE).toPandas() + if pdf is None: + return + mask = (pdf["project_id"] == project) & (pdf["is_baseline"] == True) # noqa: E712 + if feature_view_name is not None: + mask &= pdf["feature_view_name"] == feature_view_name + if feature_name is not None: + mask &= pdf["feature_name"] == feature_name + if data_source_type is not None: + mask &= pdf["data_source_type"] == data_source_type + + pdf.loc[mask, "is_baseline"] = False + spark_session.createDataFrame(pdf).write.mode("overwrite").saveAsTable( + MON_TABLE_FEATURE + ) + + +_SPARK_MONITORING_DDL_STATEMENTS = [ + f""" +CREATE TABLE IF NOT EXISTS {MON_TABLE_FEATURE} ( + project_id STRING NOT NULL, + feature_view_name STRING NOT NULL, + feature_name STRING NOT NULL, + metric_date DATE NOT NULL, + granularity STRING NOT NULL, + data_source_type STRING NOT NULL, + computed_at TIMESTAMP NOT NULL, + is_baseline BOOLEAN NOT NULL, + feature_type STRING NOT NULL, + row_count BIGINT, + null_count BIGINT, + null_rate DOUBLE, + mean DOUBLE, + stddev DOUBLE, + min_val DOUBLE, + max_val DOUBLE, + p50 DOUBLE, + p75 DOUBLE, + p90 DOUBLE, + p95 DOUBLE, + p99 DOUBLE, + histogram STRING +) USING PARQUET +""", + f""" +CREATE TABLE IF NOT EXISTS {MON_TABLE_FEATURE_VIEW} ( + project_id STRING NOT NULL, + feature_view_name STRING NOT NULL, + metric_date DATE NOT NULL, + granularity STRING NOT NULL, + data_source_type STRING NOT NULL, + computed_at TIMESTAMP NOT NULL, + is_baseline BOOLEAN NOT NULL, + total_row_count BIGINT, + total_features INT, + features_with_nulls INT, + avg_null_rate DOUBLE, + max_null_rate DOUBLE +) USING PARQUET +""", + f""" +CREATE TABLE IF NOT EXISTS {MON_TABLE_FEATURE_SERVICE} ( + project_id STRING NOT NULL, + feature_service_name STRING NOT NULL, + metric_date DATE NOT NULL, + granularity STRING NOT NULL, + data_source_type STRING NOT NULL, + computed_at TIMESTAMP NOT NULL, + is_baseline BOOLEAN NOT NULL, + total_feature_views INT, + total_features INT, + avg_null_rate DOUBLE, + max_null_rate DOUBLE +) USING PARQUET +""", + f""" +CREATE TABLE IF NOT EXISTS {MON_TABLE_JOB} ( + job_id STRING NOT NULL, + project_id STRING NOT NULL, + feature_view_name STRING, + job_type STRING NOT NULL, + status STRING NOT NULL, + parameters STRING, + metric_date DATE NOT NULL, + started_at TIMESTAMP, + completed_at TIMESTAMP, + error_message STRING, + result_summary STRING +) USING PARQUET +""", +] + + +def _spark_normalize_histogram_column(pdf: pd.DataFrame) -> pd.DataFrame: + if "histogram" not in pdf.columns: + return pdf + out = pdf.copy() + + def _ser(x: Any) -> Any: + if x is None: + return None + if isinstance(x, str): + return x + return json.dumps(x) + + out["histogram"] = out["histogram"].map(_ser) + return out + + +def _spark_pandas_upsert( + pdf_old: pd.DataFrame, + pdf_new: pd.DataFrame, + pk_columns: List[str], +) -> pd.DataFrame: + if pdf_old.empty: + return pdf_new + old_idx = pdf_old.set_index(pk_columns) + new_idx = pdf_new.set_index(pk_columns) + kept = old_idx.loc[~old_idx.index.isin(new_idx.index)] + kept_df = kept.reset_index() + return pd.concat([kept_df, pdf_new], ignore_index=True) + + +def _spark_sql_numeric_stats( + spark_session: SparkSession, + from_expression: str, + feature_names: List[str], + ts_clause: str, + histogram_bins: int, +) -> List[Dict[str, Any]]: + select_parts = ["COUNT(*)"] + for col in feature_names: + q = f"`{col}`" + c = f"CAST({q} AS DOUBLE)" + select_parts.extend( + [ + f"COUNT({q})", + f"AVG({c})", + f"STDDEV_SAMP({c})", + f"MIN({c})", + f"MAX({c})", + f"PERCENTILE_APPROX({c}, 0.50)", + f"PERCENTILE_APPROX({c}, 0.75)", + f"PERCENTILE_APPROX({c}, 0.90)", + f"PERCENTILE_APPROX({c}, 0.95)", + f"PERCENTILE_APPROX({c}, 0.99)", + ] + ) + + query = ( + f"SELECT {', '.join(select_parts)} " + f"FROM {from_expression} AS _src WHERE {ts_clause}" + ) + rows = spark_session.sql(query).collect() + if not rows or rows[0] is None: + return [empty_numeric_metric(n) for n in feature_names] + + row = rows[0] + row_count = int(row[0] or 0) + results: List[Dict[str, Any]] = [] + + for i, col in enumerate(feature_names): + base = 1 + i * 10 + non_null = int(row[base] or 0) + null_count = row_count - non_null + + min_val = opt_float(row[base + 3]) + max_val = opt_float(row[base + 4]) + + result: Dict[str, Any] = { + "feature_name": col, + "feature_type": "numeric", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": opt_float(row[base + 1]), + "stddev": opt_float(row[base + 2]), + "min_val": min_val, + "max_val": max_val, + "p50": opt_float(row[base + 5]), + "p75": opt_float(row[base + 6]), + "p90": opt_float(row[base + 7]), + "p95": opt_float(row[base + 8]), + "p99": opt_float(row[base + 9]), + "histogram": None, + } + + if min_val is not None and max_val is not None and non_null > 0: + result["histogram"] = _spark_sql_numeric_histogram( + spark_session, + from_expression, + col, + ts_clause, + histogram_bins, + min_val, + max_val, + ) + + results.append(result) + + return results + + +def _spark_sql_numeric_histogram( + spark_session: SparkSession, + from_expression: str, + col_name: str, + ts_clause: str, + bins: int, + min_val: float, + max_val: float, +) -> Dict[str, Any]: + q_col = f"`{col_name}`" + + if min_val == max_val: + sql = ( + f"SELECT COUNT(*) FROM {from_expression} AS _src " + f"WHERE {q_col} IS NOT NULL AND {ts_clause}" + ) + cnt = int(spark_session.sql(sql).collect()[0][0] or 0) + return {"bins": [min_val, max_val], "counts": [cnt], "bin_width": 0.0} + + bin_width = (max_val - min_val) / bins + cast_col = f"CAST({q_col} AS DOUBLE)" + inner = ( + f"CASE WHEN {min_val} = {max_val} THEN 1 " + f"ELSE LEAST(GREATEST(FLOOR(({cast_col} - {min_val}) / {bin_width}) + 1, 1), {bins}) " + f"END AS bucket" + ) + + query = ( + f"SELECT bucket, COUNT(*) AS cnt FROM (" + f" SELECT {inner} " + f" FROM {from_expression} AS _src " + f" WHERE {q_col} IS NOT NULL AND {ts_clause}" + f") AS _b WHERE bucket IS NOT NULL " + f"GROUP BY bucket ORDER BY bucket" + ) + hrows = spark_session.sql(query).collect() + counts = [0] * bins + for hr in hrows: + bucket = int(hr[0] or 0) + cnt = int(hr[1] or 0) + if 1 <= bucket <= bins: + counts[bucket - 1] = cnt + + bin_edges = [min_val + i * bin_width for i in range(bins + 1)] + return { + "bins": [float(b) for b in bin_edges], + "counts": counts, + "bin_width": float(bin_width), + } + + +def _spark_sql_categorical_stats( + spark_session: SparkSession, + from_expression: str, + col_name: str, + ts_clause: str, + top_n: int, +) -> Dict[str, Any]: + q_col = f"`{col_name}`" + + query = ( + f"WITH filtered AS (" + f" SELECT * FROM {from_expression} AS _src WHERE {ts_clause}" + f") " + f"SELECT " + f" (SELECT COUNT(*) FROM filtered) AS row_count, " + f" (SELECT COUNT(*) - COUNT({q_col}) FROM filtered) AS null_count, " + f" (SELECT COUNT(DISTINCT {q_col}) FROM filtered " + f" WHERE {q_col} IS NOT NULL) AS unique_count, " + f" CAST({q_col} AS STRING) AS value, COUNT(*) AS cnt " + f"FROM filtered WHERE {q_col} IS NOT NULL " + f"GROUP BY {q_col} ORDER BY cnt DESC LIMIT {int(top_n)}" + ) + + rows = spark_session.sql(query).collect() + if not rows: + return empty_categorical_metric(col_name) + + row_count = int(rows[0][0] or 0) + null_count = int(rows[0][1] or 0) + unique_count = int(rows[0][2] or 0) + + top_entries = [{"value": r[3], "count": int(r[4] or 0)} for r in rows] + top_total = sum(e["count"] for e in top_entries) + other_count = (row_count - null_count) - top_total + + return { + "feature_name": col_name, + "feature_type": "categorical", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": { + "values": top_entries, + "other_count": max(other_count, 0), + "unique_count": unique_count, + }, + } + + +def _spark_rows_to_metric_dicts( + rows: List[Any], + columns: List[str], +) -> List[Dict[str, Any]]: + out: List[Dict[str, Any]] = [] + for r in rows: + d = r.asDict() + rec = {c: d.get(c) for c in columns} + out.append(normalize_monitoring_row(rec)) + return out + class SparkRetrievalJob(RetrievalJob): def __init__( @@ -401,8 +1012,72 @@ def _to_df_internal(self, timeout: Optional[int] = None) -> pd.DataFrame: def _to_arrow_internal(self, timeout: Optional[int] = None) -> pyarrow.Table: """Return dataset as pyarrow Table synchronously""" + if self._should_use_staging_for_arrow(): + return self._to_arrow_via_staging() + return pyarrow.Table.from_pandas(self._to_df_internal(timeout=timeout)) + def _should_use_staging_for_arrow(self) -> bool: + offline_store = getattr(self._config, "offline_store", None) + return bool( + isinstance(offline_store, SparkOfflineStoreConfig) + and getattr(offline_store, "staging_location", None) + ) + + def _to_arrow_via_staging(self) -> pyarrow.Table: + paths = self.to_remote_storage() + if not paths: + return pyarrow.table({}) + + parquet_paths = _filter_parquet_files(paths) + if not parquet_paths: + return pyarrow.table({}) + + pa_fs, stripped_paths = self._resolve_staging_filesystem(parquet_paths) + dataset = ds.dataset(stripped_paths, format="parquet", filesystem=pa_fs) + return dataset.to_table() + + def _resolve_staging_filesystem( + self, paths: List[str] + ) -> Tuple[Optional[pyarrow.fs.FileSystem], List[str]]: + """Return (pyarrow filesystem, prefix-stripped paths) for staging URIs.""" + sample = paths[0] + + import pyarrow.fs as pafs + + if sample.startswith("s3://") or sample.startswith("s3a://"): + endpoint = os.environ.get("AWS_ENDPOINT_URL_S3") or os.environ.get( + "AWS_S3_ENDPOINT", "" + ) + region = getattr( + self._config.offline_store, "region", None + ) or os.environ.get("AWS_DEFAULT_REGION", "us-east-1") + kwargs: Dict[str, Any] = {"region": region} + if endpoint: + kwargs["endpoint_override"] = ( + endpoint.rstrip("/") + .removeprefix("https://") + .removeprefix("http://") + ) + kwargs["scheme"] = "https" if endpoint.startswith("https") else "http" + fs = pafs.S3FileSystem(**kwargs) + stripped = [p.removeprefix("s3a://").removeprefix("s3://") for p in paths] + return fs, stripped + + if sample.startswith("gs://"): + fs = pafs.GcsFileSystem() + stripped = [p[len("gs://") :] for p in paths] + return fs, stripped + + # Local paths + normalized = [] + for p in paths: + if p.startswith("file://"): + normalized.append(p[len("file://") :]) + else: + normalized.append(p) + return None, normalized + def to_feast_df( self, validation_reference: Optional["ValidationReference"] = None, @@ -428,20 +1103,33 @@ def persist( ): """ Run the retrieval and persist the results in the same offline store used for read. - Please note the persisting is done only within the scope of the spark session for local warehouse directory. + Supports both table-based and path-based SparkSource configurations. + For table-based: persists via saveAsTable (remote warehouse) or createOrReplaceTempView (local). + For path-based: writes directly to the specified path in the given file format. """ assert isinstance(storage, SavedDatasetSparkStorage) + table_name = storage.spark_options.table - if not table_name: - raise ValueError("Cannot persist, table_name is not defined") - if self._has_remote_warehouse_in_config(): - file_format = storage.spark_options.file_format + path = storage.spark_options.path + file_format = storage.spark_options.file_format + + if path: if not file_format: - self.to_spark_df().write.saveAsTable(table_name) + file_format = "parquet" + write_mode = "overwrite" if allow_overwrite else "error" + self.to_spark_df().write.format(file_format).mode(write_mode).save(path) + elif table_name: + if self._has_remote_warehouse_in_config(): + if not file_format: + self.to_spark_df().write.saveAsTable(table_name) + else: + self.to_spark_df().write.format(file_format).saveAsTable(table_name) else: - self.to_spark_df().write.format(file_format).saveAsTable(table_name) + self.to_spark_df().createOrReplaceTempView(table_name) else: - self.to_spark_df().createOrReplaceTempView(table_name) + raise ValueError( + "Cannot persist: either 'table' or 'path' must be specified in SavedDatasetSparkStorage" + ) def _has_remote_warehouse_in_config(self) -> bool: """ @@ -464,55 +1152,53 @@ def supports_remote_storage_export(self) -> bool: def to_remote_storage(self) -> List[str]: """Currently only works for local and s3-based staging locations""" - if self.supports_remote_storage_export(): - sdf: pyspark.sql.DataFrame = self.to_spark_df() - - if self._config.offline_store.staging_location.startswith("/"): - local_file_staging_location = os.path.abspath( - self._config.offline_store.staging_location - ) - - # write to staging location - output_uri = os.path.join( - str(local_file_staging_location), str(uuid.uuid4()) - ) - sdf.write.parquet(output_uri) - - return _list_files_in_folder(output_uri) - elif self._config.offline_store.staging_location.startswith("s3://"): - from feast.infra.utils import aws_utils + if not self.supports_remote_storage_export(): + raise NotImplementedError() - spark_compatible_s3_staging_location = ( - self._config.offline_store.staging_location.replace( - "s3://", "s3a://" - ) - ) + sdf: pyspark.sql.DataFrame = self.to_spark_df() + staging_location = self._config.offline_store.staging_location - # write to staging location - output_uri = os.path.join( - str(spark_compatible_s3_staging_location), str(uuid.uuid4()) - ) - sdf.write.parquet(output_uri) - - return aws_utils.list_s3_files( - self._config.offline_store.region, output_uri - ) - elif self._config.offline_store.staging_location.startswith("hdfs://"): - output_uri = os.path.join( - self._config.offline_store.staging_location, str(uuid.uuid4()) - ) - sdf.write.parquet(output_uri) - spark_session = get_spark_session_or_start_new_with_repoconfig( - store_config=self._config.offline_store - ) - return _list_hdfs_files(spark_session, output_uri) - else: - raise NotImplementedError( - "to_remote_storage is only implemented for file://, s3:// and hdfs:// uri schemes" - ) + if staging_location.startswith("/"): + local_file_staging_location = os.path.abspath(staging_location) + output_uri = os.path.join(local_file_staging_location, str(uuid.uuid4())) + sdf.write.parquet(output_uri) + return _list_files_in_folder(output_uri) + elif staging_location.startswith("s3://"): + from feast.infra.utils import aws_utils + spark_compatible_s3_staging_location = staging_location.replace( + "s3://", "s3a://" + ) + output_uri = os.path.join( + spark_compatible_s3_staging_location, str(uuid.uuid4()) + ) + sdf.write.parquet(output_uri) + s3_uri_for_listing = output_uri.replace("s3a://", "s3://", 1) + return aws_utils.list_s3_files( + self._config.offline_store.region, s3_uri_for_listing + ) + elif staging_location.startswith("gs://"): + output_uri = os.path.join(staging_location, str(uuid.uuid4())) + sdf.write.parquet(output_uri) + return _list_gcs_files(output_uri) + elif staging_location.startswith(("wasbs://", "abfs://", "abfss://")) or ( + staging_location.startswith("https://") + and ".blob.core.windows.net" in staging_location + ): + output_uri = os.path.join(staging_location, str(uuid.uuid4())) + sdf.write.parquet(output_uri) + return _list_azure_files(output_uri) + elif staging_location.startswith("hdfs://"): + output_uri = os.path.join(staging_location, str(uuid.uuid4())) + sdf.write.parquet(output_uri) + spark_session = get_spark_session_or_start_new_with_repoconfig( + store_config=self._config.offline_store + ) + return _list_hdfs_files(spark_session, output_uri) else: - raise NotImplementedError() + raise NotImplementedError( + "to_remote_storage is only implemented for file://, s3://, gs://, azure, and hdfs uri schemes" + ) @property def metadata(self) -> Optional[RetrievalMetadata]: @@ -523,6 +1209,101 @@ def metadata(self) -> Optional[RetrievalMetadata]: return self._metadata +def _register_iceberg_source_as_temp_view( + spark_session: SparkSession, + data_source: "DataSource", + config: "RepoConfig", +) -> None: + """Register an IcebergSource as a Spark temp view for SQL queries. + + Loads the table through PyIceberg (which handles all catalog types and + storage backends), scans it to Arrow, and creates a temp view. + """ + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, + ) + + assert isinstance(data_source, IcebergSource) + view_name = f"iceberg_tmp_{data_source.iceberg_table}" + + iceberg_table = _load_pyiceberg_table(data_source) + arrow_table = iceberg_table.scan().to_arrow() + df = spark_session.createDataFrame(arrow_table.to_pandas()) + df.createOrReplaceTempView(view_name) + + +def _load_pyiceberg_table(data_source: "DataSource"): + """Load an Iceberg table via PyIceberg, respecting the source's catalog_type.""" + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, + ) + + assert isinstance(data_source, IcebergSource) + fqn = f"{data_source.namespace}.{data_source.iceberg_table}" + + if data_source.catalog_type != "rest": + catalog_client = data_source.get_catalog_client() + return catalog_client.load_table(fqn) + + from pyiceberg.catalog import load_catalog + + catalog_config: Dict[str, str] = { + "type": "rest", + "uri": data_source.endpoint, + "warehouse": data_source.warehouse, + } + if data_source.token_env_var: + token = os.environ.get(data_source.token_env_var) + if token: + catalog_config["token"] = token + catalog = load_catalog(data_source.catalog_name, **catalog_config) + return catalog.load_table(fqn) + + +def _pull_from_iceberg_source( + spark_session: SparkSession, + data_source: "DataSource", + join_key_columns: List[str], + feature_name_columns: List[str], + timestamp_field: str, + created_timestamp_column: Optional[str], + start_date: Optional[datetime], + end_date: Optional[datetime], + config: "RepoConfig", +) -> "SparkRetrievalJob": + """Read from an IcebergSource using Spark. + + Loads the table via PyIceberg into a temp view, then queries with time filter. + """ + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, + ) + + assert isinstance(data_source, IcebergSource) + + tmp_view = f"iceberg_tmp_{data_source.iceberg_table}" + + # Register the source as a temp view (reuses the same resolution logic) + _register_iceberg_source_as_temp_view(spark_session, data_source, config) + + all_columns = join_key_columns + feature_name_columns + [timestamp_field] + if created_timestamp_column: + all_columns.append(created_timestamp_column) + select_cols = ", ".join(all_columns) if feature_name_columns else "*" + + timestamp_filter = get_timestamp_filter_sql( + start_date, end_date, timestamp_field, tz=timezone.utc, quote_fields=False + ) + query = f"SELECT {select_cols} FROM {tmp_view} WHERE {timestamp_filter}" + + return SparkRetrievalJob( + spark_session=spark_session, + query=query, + full_feature_names=False, + config=config, + ) + + def get_spark_session_or_start_new_with_repoconfig( store_config: SparkOfflineStoreConfig, ) -> SparkSession: @@ -540,6 +1321,181 @@ def get_spark_session_or_start_new_with_repoconfig( return spark_session +def _gather_all_entities( + fv_query_contexts: List[offline_utils.FeatureViewQueryContext], +) -> List[str]: + # Why: ensure a unified entity set across feature views to align UNION schemas. + all_entities: List[str] = [] + for ctx in fv_query_contexts: + for e in ctx.entities: + if e not in all_entities: + all_entities.append(e) + return all_entities + + +def _create_temp_entity_union_view( + spark_session: SparkSession, + tmp_view_name: str, + feature_views: List[FeatureView], + fv_query_contexts: List[offline_utils.FeatureViewQueryContext], + start_date: datetime, + end_date: datetime, + date_partition_column_formats: List[Optional[str]], +) -> None: + # Why: derive distinct entity keys observed in the time window without requiring an entity_df upfront. + start_date_str = _format_datetime(start_date) + end_date_str = _format_datetime(end_date) + + # Compute the unified entity set to align schemas in the UNION. + all_entities = _gather_all_entities(fv_query_contexts) + + per_view_selects: List[str] = [] + for fv, ctx, date_format in zip( + feature_views, fv_query_contexts, date_partition_column_formats + ): + assert fv.batch_source is not None + from_expression = fv.batch_source.get_table_query_string() + timestamp_field = fv.batch_source.timestamp_field or "event_timestamp" + date_partition_column = getattr(fv.batch_source, "date_partition_column", None) + partition_clause = "" + if date_partition_column and date_format: + partition_clause = ( + f" AND {date_partition_column} >= '{start_date.strftime(date_format)}'" + f" AND {date_partition_column} <= '{end_date.strftime(date_format)}'" + ) + + # Fill missing entity columns with NULL and cast to STRING to keep UNION schemas aligned. + select_entities: List[str] = [] + ctx_entities_set = set(ctx.entities) + for col in all_entities: + if col in ctx_entities_set: + select_entities.append(f"CAST({col} AS STRING) AS {col}") + else: + select_entities.append(f"CAST(NULL AS STRING) AS {col}") + + per_view_selects.append( + f""" + SELECT DISTINCT {", ".join(select_entities)} + FROM {from_expression} + WHERE {timestamp_field} BETWEEN TIMESTAMP('{start_date_str}') AND TIMESTAMP('{end_date_str}'){partition_clause} + """ + ) + + union_query = "\nUNION DISTINCT\n".join([s.strip() for s in per_view_selects]) + spark_session.sql( + f"CREATE OR REPLACE TEMPORARY VIEW {tmp_view_name} AS {union_query}" + ) + + +def _make_left_table_query(end_date: datetime, tmp_view_name: str) -> Tuple[str, str]: + # Why: use a stable as-of timestamp for PIT joins when no entity timestamps are provided. + event_timestamp_col = "entity_ts" + left_table_query_string = ( + f"(SELECT *, TIMESTAMP('{_format_datetime(end_date)}') AS {event_timestamp_col} " + f"FROM {tmp_view_name})" + ) + return left_table_query_string, event_timestamp_col + + +def _entity_schema_keys_from( + all_entities: List[str], event_timestamp_col: str +) -> KeysView[str]: + # Why: pass a KeysView[str] to PIT query builder to match entity_df branch typing. + return cast( + KeysView[str], {k: None for k in (all_entities + [event_timestamp_col])}.keys() + ) + + +def _apply_bfv_transformations( + spark_session: SparkSession, + feature_views: List[FeatureView], + query_contexts: List[offline_utils.FeatureViewQueryContext], +) -> List[offline_utils.FeatureViewQueryContext]: + """ + For BatchFeatureViews, update each query context in one of two ways: + + 1. Pre-computed path shortcut: if ``offline=True`` and + ``batch_source.path`` is set, read the pre-materialized parquet + directly — avoids re-running the UDF on every training call. + 2. UDF execution: if the BFV has a transformation, run it against + the raw source and register the result as a temp view. + + Plain FeatureViews and BFVs with neither a path nor a UDF pass + through unchanged. + """ + from dataclasses import replace + + from feast.feature_view_utils import ( + get_transformation_function, + has_transformation, + resolve_feature_view_source_with_fallback, + ) + + fv_by_name = {fv.projection.name_to_use(): fv for fv in feature_views} + + updated_contexts = [] + for ctx in query_contexts: + fv = fv_by_name.get(ctx.name) + if fv is None or not isinstance(fv, BatchFeatureView): + updated_contexts.append(ctx) + continue + + # 1. Pre-computed path shortcut + if ( + getattr(fv, "offline", False) + and isinstance(fv.batch_source, SparkSource) + and fv.batch_source.path + ): + tmp_view = f"__feast_offline_{ctx.name}_{uuid.uuid4().hex[:8]}" + file_format = fv.batch_source.file_format or "parquet" + try: + df = spark_session.read.format(file_format).load(fv.batch_source.path) + df.createOrReplaceTempView(tmp_view) + updated_contexts.append(replace(ctx, table_subquery=tmp_view)) + continue + except (FileNotFoundError, PermissionError) as e: + warnings.warn( + f"Offline path '{fv.batch_source.path}' not accessible for " + f"'{ctx.name}': {e}; falling back to source query.", + RuntimeWarning, + stacklevel=2, + ) + except Exception as e: + warnings.warn( + f"Unexpected error loading offline path " + f"'{fv.batch_source.path}' for '{ctx.name}': {e}; " + f"falling back to source query.", + RuntimeWarning, + stacklevel=2, + ) + + # 2. UDF execution fallback + if has_transformation(fv): + udf = get_transformation_function(fv) + if udf is not None: + source_info = resolve_feature_view_source_with_fallback(fv) + source_query = source_info.data_source.get_table_query_string() + + timestamp_filter = get_timestamp_filter_sql( + start_date=ctx.min_event_timestamp, + end_date=ctx.max_event_timestamp, + timestamp_field=ctx.timestamp_field, + tz=timezone.utc, + quote_fields=False, + ) + source_df = spark_session.sql( + f"SELECT * FROM {source_query} WHERE {timestamp_filter}" + ) + transformed_df = udf(source_df) + tmp_view_name = "feast_bfv_" + uuid.uuid4().hex + transformed_df.createOrReplaceTempView(tmp_view_name) + ctx = replace(ctx, table_subquery=tmp_view_name) + + updated_contexts.append(ctx) + + return updated_contexts + + def _get_entity_df_event_timestamp_range( entity_df: Union[pd.DataFrame, str], entity_df_event_timestamp_col: str, @@ -637,6 +1593,10 @@ def _list_files_in_folder(folder): return files +def _filter_parquet_files(paths: List[str]) -> List[str]: + return [path for path in paths if path.endswith(".parquet")] + + def _list_hdfs_files(spark_session: SparkSession, uri: str) -> List[str]: jvm = spark_session._jvm jsc = spark_session._jsc @@ -653,6 +1613,81 @@ def _list_hdfs_files(spark_session: SparkSession, uri: str) -> List[str]: return files +def _list_gcs_files(path: str) -> List[str]: + try: + from google.cloud import storage + except ImportError as e: + from feast.errors import FeastExtrasDependencyImportError + + raise FeastExtrasDependencyImportError("gcp", str(e)) + + assert path.startswith("gs://"), "GCS path must start with gs://" + bucket_path = path[len("gs://") :] + if "/" in bucket_path: + bucket, prefix = bucket_path.split("/", 1) + else: + bucket, prefix = bucket_path, "" + + client = storage.Client() + bucket_obj = client.bucket(bucket) + blobs = bucket_obj.list_blobs(prefix=prefix) + + files = [] + for blob in blobs: + if not blob.name.endswith("/"): + files.append(f"gs://{bucket}/{blob.name}") + return files + + +def _list_azure_files(path: str) -> List[str]: + try: + from azure.identity import DefaultAzureCredential + from azure.storage.blob import BlobServiceClient + except ImportError as e: + from feast.errors import FeastExtrasDependencyImportError + + raise FeastExtrasDependencyImportError("azure", str(e)) + + parsed = urlparse(path) + scheme = parsed.scheme + + if scheme in ("wasbs", "abfs", "abfss"): + if "@" not in parsed.netloc: + raise ValueError("Azure staging URI must include container@account host") + container, account_host = parsed.netloc.split("@", 1) + account_url = f"https://{account_host}" + base = f"{scheme}://{container}@{account_host}" + prefix = parsed.path.lstrip("/") + else: + account_url = f"{parsed.scheme}://{parsed.netloc}" + container_and_prefix = parsed.path.lstrip("/").split("/", 1) + container = container_and_prefix[0] + base = f"{account_url}/{container}" + prefix = container_and_prefix[1] if len(container_and_prefix) > 1 else "" + + credential = os.environ.get("AZURE_STORAGE_KEY") or os.environ.get( + "AZURE_STORAGE_ACCOUNT_KEY" + ) + if credential: + client = BlobServiceClient(account_url=account_url, credential=credential) + else: + default_credential = DefaultAzureCredential( + exclude_shared_token_cache_credential=True + ) + client = BlobServiceClient( + account_url=account_url, credential=default_credential + ) + + container_client = client.get_container_client(container) + blobs = container_client.list_blobs(name_starts_with=prefix if prefix else None) + + files = [] + for blob in blobs: + if not blob.name.endswith("/"): + files.append(f"{base}/{blob.name}") + return files + + def _cast_data_frame( df_new: pyspark.sql.DataFrame, df_existing: pyspark.sql.DataFrame ) -> pyspark.sql.DataFrame: @@ -761,6 +1796,10 @@ def _cast_data_frame( AND subquery.event_timestamp >= entity_dataframe.entity_timestamp - {{ featureview.ttl }} * interval '1' second {% endif %} + {% if filter_by_created_timestamp and featureview.created_timestamp_column %} + AND subquery.created_timestamp <= entity_dataframe.entity_timestamp + {% endif %} + {% for entity in featureview.entities %} AND subquery.{{ entity }} = entity_dataframe.{{ entity }} {% endfor %} diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark_source.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark_source.py index bd4fb1ac817..a9c2b7e98c9 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/spark_source.py @@ -14,17 +14,17 @@ ) from feast.repo_config import RepoConfig from feast.saved_dataset import SavedDatasetStorage +from feast.table_format import TableFormat, table_format_from_proto from feast.type_map import spark_to_feast_value_type from feast.value_type import ValueType logger = logging.getLogger(__name__) -class SparkSourceFormat(Enum): +class SparkFileSourceFormat(Enum): csv = "csv" json = "json" parquet = "parquet" - delta = "delta" avro = "avro" @@ -42,6 +42,7 @@ def __init__( query: Optional[str] = None, path: Optional[str] = None, file_format: Optional[str] = None, + table_format: Optional[TableFormat] = None, created_timestamp_column: Optional[str] = None, field_mapping: Optional[Dict[str, str]] = None, description: Optional[str] = "", @@ -58,7 +59,9 @@ def __init__( table: The name of a Spark table. query: The query to be executed in Spark. path: The path to file data. - file_format: The format of the file data. + file_format: The underlying file format (parquet, avro, csv, json). + table_format: The table metadata format (iceberg, delta, hudi, etc.). + Optional and separate from file_format. created_timestamp_column: Timestamp column indicating when the row was created, used for deduplicating rows. field_mapping: A dictionary mapping of column names in this data @@ -70,7 +73,7 @@ def __init__( timestamp_field: Event timestamp field used for point-in-time joins of feature values. date_partition_column: The column to partition the data on for faster - retrieval. This is useful for large tables and will limit the number ofi + retrieval. This is useful for large tables and will limit the number of """ # If no name, use the table as the default name. if name is None and table is None: @@ -102,6 +105,7 @@ def __init__( path=path, file_format=file_format, date_partition_column_format=date_partition_column_format, + table_format=table_format, ) @property @@ -132,6 +136,13 @@ def file_format(self): """ return self.spark_options.file_format + @property + def table_format(self): + """ + Returns the table format of this feature data source. + """ + return self.spark_options.table_format + @property def date_partition_column_format(self): """ @@ -151,6 +162,7 @@ def from_proto(data_source: DataSourceProto) -> Any: query=spark_options.query, path=spark_options.path, file_format=spark_options.file_format, + table_format=spark_options.table_format, date_partition_column_format=spark_options.date_partition_column_format, date_partition_column=data_source.date_partition_column, timestamp_field=data_source.timestamp_field, @@ -217,9 +229,10 @@ def get_table_query_string(self) -> str: # If both the table query string and the actual query are null, we can load from file. spark_session = SparkSession.getActiveSession() if spark_session is None: - raise AssertionError("Could not find an active spark session.") + # Remote mode may not have an active session bound to the thread; create one on demand. + spark_session = SparkSession.builder.getOrCreate() try: - df = spark_session.read.format(self.file_format).load(self.path) + df = self._load_dataframe_from_path(spark_session) except Exception: logger.exception( "Spark read of file source failed.\n" + traceback.format_exc() @@ -230,9 +243,32 @@ def get_table_query_string(self) -> str: return f"`{tmp_table_name}`" + def _load_dataframe_from_path(self, spark_session): + """Load DataFrame from path, considering both file format and table format.""" + + if self.table_format is None: + # No table format specified, use standard file reading with file_format + return spark_session.read.format(self.file_format).load(self.path) + + # Build reader with table format and options + reader = spark_session.read.format(self.table_format.format_type.value) + + # Add table format specific options + for key, value in self.table_format.properties.items(): + reader = reader.option(key, value) + + # For catalog-based table formats like Iceberg, the path is actually a table name + # For file-based formats, it's still a file path + return reader.load(self.path) + def __eq__(self, other): - base_eq = super().__eq__(other) - if not base_eq: + # Guard before the spark-specific attribute access below: the base + # DataSource.__eq__ accepts any DataSource subclass, so a cross-type + # comparison (e.g. against a FileSource with a matching name) would + # otherwise raise AttributeError on `other.table` (#6636). + if not isinstance(other, SparkSource): + return False + if not super().__eq__(other): return False return ( self.table == other.table @@ -245,7 +281,7 @@ def __hash__(self): class SparkOptions: - allowed_formats = [format.value for format in SparkSourceFormat] + allowed_formats = [format.value for format in SparkFileSourceFormat] def __init__( self, @@ -254,19 +290,29 @@ def __init__( path: Optional[str], file_format: Optional[str], date_partition_column_format: Optional[str] = "%Y-%m-%d", + table_format: Optional[TableFormat] = None, ): - # Check that only one of the ways to load a spark dataframe can be used. We have - # to treat empty string and null the same due to proto (de)serialization. - if sum([(not (not arg)) for arg in [table, query, path]]) != 1: + # query + path is allowed: query for reads during materialization, + # path for offline write-back (offline=True) and get_historical_features. + # table must be standalone (cannot combine with query or path). + has_table = bool(table) + has_query = bool(query) + has_path = bool(path) + if has_table and (has_query or has_path): + raise ValueError("'table' cannot be combined with 'query' or 'path'.") + if not (has_table or has_query or has_path): raise ValueError( - "Exactly one of params(table, query, path) must be specified." + "At least one of params(table, query, path) must be specified." ) if path: - if not file_format: + # If table_format is specified, file_format is optional (table format determines the reader) + # If no table_format, file_format is required for basic file reading + if not table_format and not file_format: raise ValueError( - "If 'path' is specified, then 'file_format' is required." + "If 'path' is specified without 'table_format', then 'file_format' is required." ) - if file_format not in self.allowed_formats: + # Only validate file_format if it's provided (it's optional with table_format) + if file_format and file_format not in self.allowed_formats: raise ValueError( f"'file_format' should be one of {self.allowed_formats}" ) @@ -276,6 +322,7 @@ def __init__( self._path = path self._file_format = file_format self._date_partition_column_format = date_partition_column_format + self._table_format = table_format @property def table(self): @@ -317,6 +364,14 @@ def date_partition_column_format(self): def date_partition_column_format(self, date_partition_column_format): self._date_partition_column_format = date_partition_column_format + @property + def table_format(self): + return self._table_format + + @table_format.setter + def table_format(self, table_format): + self._table_format = table_format + @classmethod def from_proto(cls, spark_options_proto: DataSourceProto.SparkOptions): """ @@ -326,12 +381,18 @@ def from_proto(cls, spark_options_proto: DataSourceProto.SparkOptions): Returns: Returns a SparkOptions object based on the spark_options protobuf """ + # Parse table_format if present + table_format = None + if spark_options_proto.HasField("table_format"): + table_format = table_format_from_proto(spark_options_proto.table_format) + spark_options = cls( table=spark_options_proto.table, query=spark_options_proto.query, path=spark_options_proto.path, file_format=spark_options_proto.file_format, date_partition_column_format=spark_options_proto.date_partition_column_format, + table_format=table_format, ) return spark_options @@ -342,6 +403,10 @@ def to_proto(self) -> DataSourceProto.SparkOptions: Returns: SparkOptionsProto protobuf """ + table_format_proto = None + if self.table_format: + table_format_proto = self.table_format.to_proto() + spark_options_proto = DataSourceProto.SparkOptions( table=self.table, query=self.query, @@ -350,6 +415,9 @@ def to_proto(self) -> DataSourceProto.SparkOptions: date_partition_column_format=self.date_partition_column_format, ) + if table_format_proto: + spark_options_proto.table_format.CopyFrom(table_format_proto) + return spark_options_proto @@ -364,12 +432,14 @@ def __init__( query: Optional[str] = None, path: Optional[str] = None, file_format: Optional[str] = None, + table_format: Optional[TableFormat] = None, ): self.spark_options = SparkOptions( table=table, query=query, path=path, file_format=file_format, + table_format=table_format, ) @staticmethod @@ -380,6 +450,7 @@ def from_proto(storage_proto: SavedDatasetStorageProto) -> SavedDatasetStorage: query=spark_options.query, path=spark_options.path, file_format=spark_options.file_format, + table_format=spark_options.table_format, ) def to_proto(self) -> SavedDatasetStorageProto: @@ -391,4 +462,16 @@ def to_data_source(self) -> DataSource: query=self.spark_options.query, path=self.spark_options.path, file_format=self.spark_options.file_format, + table_format=self.spark_options.table_format, + ) + + @staticmethod + def from_data_source(data_source: DataSource) -> "SavedDatasetSparkStorage": + assert isinstance(data_source, SparkSource) + return SavedDatasetSparkStorage( + table=data_source.table, + query=data_source.query, + path=data_source.path, + file_format=data_source.file_format, + table_format=data_source.table_format, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py index b723037f1f3..e25ebd4e1df 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_offline_store/tests/data_source.py @@ -17,7 +17,7 @@ SavedDatasetSparkStorage, SparkSource, ) -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/spark_repo_configuration.py b/sdk/python/feast/infra/offline_stores/contrib/spark_repo_configuration.py index ec414f202ae..aaf56d51981 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/spark_repo_configuration.py +++ b/sdk/python/feast/infra/offline_stores/contrib/spark_repo_configuration.py @@ -1,8 +1,8 @@ from feast.infra.offline_stores.contrib.spark_offline_store.tests.data_source import ( SparkDataSourceCreator, ) -from tests.integration.feature_repos.repo_configuration import REDIS_CONFIG -from tests.integration.feature_repos.universal.online_store.redis import ( +from tests.universal.feature_repos.repo_configuration import REDIS_CONFIG +from tests.universal.feature_repos.universal.online_store.redis import ( RedisOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/connectors/upload.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/connectors/upload.py index 020fc793713..98a2b89f07e 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/connectors/upload.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/connectors/upload.py @@ -56,6 +56,7 @@ "oracle", "redshift", "memsql", + "lakehouse", } CREATE_SCHEMA_QUERY_TEMPLATE = """ diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/test_config/manual_tests.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/test_config/manual_tests.py index a31d368ea11..d658d0d0eb9 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/test_config/manual_tests.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/test_config/manual_tests.py @@ -1,7 +1,7 @@ from feast.infra.offline_stores.contrib.trino_offline_store.tests.data_source import ( TrinoSourceCreator, ) -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/tests/data_source.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/tests/data_source.py index c8fc15a6350..9a297a32e1e 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/tests/data_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/tests/data_source.py @@ -20,7 +20,7 @@ TrinoSource, ) from feast.repo_config import FeastConfigBaseModel -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/tests/test_trino_queries.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/tests/test_trino_queries.py new file mode 100644 index 00000000000..2f7071b6bd6 --- /dev/null +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/tests/test_trino_queries.py @@ -0,0 +1,48 @@ +import signal +import threading +from unittest.mock import MagicMock, patch + +from feast.infra.offline_stores.contrib.trino_offline_store.trino_queries import ( + Query, +) + + +def test_query_init_in_main_thread_registers_signals(): + """signal.signal() should work fine in main thread.""" + + # Should not raise any exception in main thread + cursor = MagicMock() + + with patch("signal.signal") as mock_signal: + query = Query(query_text="SELECT 1", cursor=cursor) + assert query.query_text == "SELECT 1" + + # Verify signal handlers are registered correctly + mock_signal.assert_any_call(signal.SIGINT, query.cancel) + mock_signal.assert_any_call(signal.SIGTERM, query.cancel) + + # Expected signal.signal to be called twice for SIGINT and SIGTERM + assert mock_signal.call_count == 2 + + +def test_query_init_in_worker_thread_does_not_raise(): + """Regression test: signal.signal() fails in non-main threads.""" + # signal.signal() raises ValueError when called outside the main thread. + # This test verifies the fix guards against that by running Query.__init__ + # in a worker thread and ensuring no exception is raised. + + errors = [] + cursor = MagicMock() + + def create_query(): + try: + query = Query(query_text="SELECT 1", cursor=cursor) + assert query.query_text == "SELECT 1" + except ValueError as e: + errors.append(e) + + thread = threading.Thread(target=create_query) + thread.start() + thread.join() + + assert not errors, f"Unexpected ValueError in worker thread: {errors}" diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino.py index aa4cb2c8a60..c9d4119f94f 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino.py @@ -1,6 +1,15 @@ +import logging import uuid from datetime import date, datetime -from typing import Any, Dict, List, Literal, Optional, Tuple, Union +from typing import ( + Any, + Dict, + List, + Literal, + Optional, + Tuple, + Union, +) import numpy as np import pandas as pd @@ -37,6 +46,8 @@ from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage +logger = logging.getLogger(__name__) + class BasicAuthModel(FeastConfigBaseModel): username: StrictStr @@ -116,7 +127,7 @@ def to_trino_auth(self): model_cls = CLASSES_BY_AUTH_TYPE[auth_type]["auth_model"] model = model_cls(**self.config) - return trino_auth_cls(**model.dict()) + return trino_auth_cls(**model.model_dump()) class TrinoOfflineStoreConfig(FeastConfigBaseModel): @@ -183,6 +194,7 @@ def __init__( full_feature_names: bool, on_demand_feature_views: Optional[List[OnDemandFeatureView]] = None, metadata: Optional[RetrievalMetadata] = None, + temp_table: Optional[str] = None, ): self._query = query self._client = client @@ -190,6 +202,8 @@ def __init__( self._full_feature_names = full_feature_names self._on_demand_feature_views = on_demand_feature_views or [] self._metadata = metadata + self._temp_table = temp_table + self._cleaned_up = False @property def full_feature_names(self) -> bool: @@ -199,11 +213,29 @@ def full_feature_names(self) -> bool: def on_demand_feature_views(self) -> List[OnDemandFeatureView]: return self._on_demand_feature_views + def _drop_temp_table(self) -> None: + if self._cleaned_up or not self._temp_table: + return + self._cleaned_up = True + try: + self._client.execute_query(f"DROP TABLE IF EXISTS {self._temp_table}") + except Exception: + logger.exception( + "Failed to drop temporary entity table %s", + self._temp_table, + ) + + def __del__(self) -> None: + self._drop_temp_table() + def _to_df_internal(self, timeout: Optional[int] = None) -> pd.DataFrame: """Return dataset as Pandas DataFrame synchronously including on demand transforms""" - results = self._client.execute_query(query_text=self._query) - self.pyarrow_schema = results.pyarrow_schema - return results.to_dataframe() + try: + results = self._client.execute_query(query_text=self._query) + self.pyarrow_schema = results.pyarrow_schema + return results.to_dataframe() + finally: + self._drop_temp_table() def _to_arrow_internal(self, timeout: Optional[int] = None) -> pyarrow.Table: """Return payrrow dataset as synchronously including on demand transforms""" @@ -234,8 +266,11 @@ def to_trino( destination_table = f"{self._client.catalog}.{self._config.offline_store.dataset}.historical_{today}_{rand_id}" # TODO: Implement the timeout logic - query = f"CREATE TABLE {destination_table} AS ({self._query})" - self._client.execute_query(query_text=query) + try: + create_query = f"CREATE TABLE {destination_table} AS ({self._query})" + self._client.execute_query(query_text=create_query) + finally: + self._drop_temp_table() return destination_table def persist( @@ -263,6 +298,8 @@ def metadata(self) -> Optional[RetrievalMetadata]: class TrinoOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -321,6 +358,7 @@ def get_historical_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, ) -> TrinoRetrievalJob: assert isinstance(config.offline_store, TrinoOfflineStoreConfig) for fv in feature_views: @@ -372,19 +410,22 @@ def get_historical_features( ) # Generate the Trino SQL query from the query context + entity_table_ref = table_reference if type(entity_df) is str: - table_reference = f"({entity_df})" + entity_table_ref = f"({entity_df})" query = offline_utils.build_point_in_time_query( query_context, - left_table_query_string=table_reference, + left_table_query_string=entity_table_ref, entity_df_event_timestamp_col=entity_df_event_timestamp_col, entity_df_columns=entity_schema.keys(), query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, full_feature_names=full_feature_names, + filter_by_created_timestamp=filter_by_created_timestamp, ) return TrinoRetrievalJob( query=query, + temp_table=table_reference if isinstance(entity_df, pd.DataFrame) else None, client=client, config=config, full_feature_names=full_feature_names, @@ -424,11 +465,16 @@ def pull_all_from_table_or_query( ) timestamp_filter = get_timestamp_filter_sql( - start_date, end_date, timestamp_field, quote_fields=False + start_date, + end_date, + timestamp_field, + quote_fields=False, + cast_style="timestamp", + date_time_separator=" ", ) query = f""" SELECT {field_string} - FROM {from_expression} + FROM ( {from_expression} ) WHERE {timestamp_filter} """ return TrinoRetrievalJob( @@ -478,8 +524,6 @@ def _upload_entity_df_and_get_entity_schema( else: raise InvalidEntityType(type(entity_df)) - # TODO: Ensure that the table expires after some time - def _get_trino_client(config: RepoConfig) -> Trino: auth = None @@ -608,6 +652,9 @@ def _get_entity_df_event_timestamp_range( {% if featureview.ttl == 0 %}{% else %} AND subquery.event_timestamp >= entity_dataframe.entity_timestamp - interval '{{ featureview.ttl }}' second {% endif %} + {% if filter_by_created_timestamp and featureview.created_timestamp_column %} + AND subquery.created_timestamp <= entity_dataframe.entity_timestamp + {% endif %} {% for entity in featureview.entities %} AND subquery.{{ entity }} = entity_dataframe.{{ entity }} {% endfor %} diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_queries.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_queries.py index 3a26583af24..ef23a5cf021 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_queries.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_queries.py @@ -1,6 +1,7 @@ from __future__ import annotations import signal +import threading from dataclasses import dataclass from enum import Enum from typing import Any, Dict, List, Optional @@ -9,6 +10,7 @@ import pandas as pd import pyarrow as pa import trino +import trino.auth from trino.dbapi import Cursor from trino.exceptions import TrinoQueryError @@ -37,7 +39,7 @@ def __init__( http_scheme: str, verify: bool, extra_credential: Optional[str], - auth: Optional[trino.Authentication], + auth: Optional[trino.auth.Authentication], ): self.host = host self.port = port @@ -91,8 +93,9 @@ def __init__(self, query_text: str, cursor: Cursor): self.status = QueryStatus.PENDING self._cursor = cursor - signal.signal(signal.SIGINT, self.cancel) - signal.signal(signal.SIGTERM, self.cancel) + if threading.current_thread() is threading.main_thread(): + signal.signal(signal.SIGINT, self.cancel) + signal.signal(signal.SIGTERM, self.cancel) def execute(self) -> Results: try: @@ -106,7 +109,9 @@ def execute(self) -> Results: self.execution_time = end_time - start_time self.status = QueryStatus.COMPLETED - return Results(data=rows, columns=self._cursor._query.columns) + query = self._cursor._query + assert query is not None, "Cursor query should not be None after execute" + return Results(data=rows, columns=query.columns) except TrinoQueryError as error: self.status = QueryStatus.ERROR raise error diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_source.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_source.py index d8768b773fb..3225b17eb4e 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_source.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_source.py @@ -146,9 +146,7 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, TrinoSource): - raise TypeError( - "Comparisons should only involve TrinoSource class objects." - ) + return False return ( super().__eq__(other) diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_type_map.py b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_type_map.py index 8d21f9a6ac5..a11298e9b81 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_type_map.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_offline_store/trino_type_map.py @@ -21,6 +21,7 @@ def trino_to_feast_value_type(trino_type_as_str: str) -> ValueType: "varchar": ValueType.STRING, "boolean": ValueType.BOOL, "real": ValueType.FLOAT, + "date": ValueType.STRING, } _trino_type_as_str: str = trino_type_as_str trino_type_as_str = trino_type_as_str.lower() @@ -68,6 +69,15 @@ def pa_to_trino_value_type(pa_type_as_str: str) -> str: if pa_type_as_str.startswith("decimal"): return trino_type.format(pa_type_as_str) + if pa_type_as_str.startswith("map<"): + return trino_type.format("varchar") + + if pa_type_as_str == "large_string": + return trino_type.format("varchar") + + if pa_type_as_str.startswith("struct<"): + return trino_type.format("varchar") + type_map = { "null": "null", "bool": "boolean", diff --git a/sdk/python/feast/infra/offline_stores/contrib/trino_repo_configuration.py b/sdk/python/feast/infra/offline_stores/contrib/trino_repo_configuration.py index 198227095d5..64a5507876c 100644 --- a/sdk/python/feast/infra/offline_stores/contrib/trino_repo_configuration.py +++ b/sdk/python/feast/infra/offline_stores/contrib/trino_repo_configuration.py @@ -1,8 +1,8 @@ from feast.infra.offline_stores.contrib.trino_offline_store.tests.data_source import ( TrinoSourceCreator, ) -from tests.integration.feature_repos.repo_configuration import REDIS_CONFIG -from tests.integration.feature_repos.universal.online_store.redis import ( +from tests.universal.feature_repos.repo_configuration import REDIS_CONFIG +from tests.universal.feature_repos.universal.online_store.redis import ( RedisOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/offline_stores/dask.py b/sdk/python/feast/infra/offline_stores/dask.py index 4d0a6664101..c9233c77d60 100644 --- a/sdk/python/feast/infra/offline_stores/dask.py +++ b/sdk/python/feast/infra/offline_stores/dask.py @@ -1,15 +1,18 @@ +import json import os import uuid -from datetime import datetime, timezone +from datetime import date, datetime, timezone from pathlib import Path -from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Union import dask import dask.dataframe as dd +import numpy as np import pandas as pd import pyarrow +import pyarrow.compute as pc import pyarrow.dataset -import pyarrow.parquet +import pyarrow.parquet as pq import pytz from feast.data_source import DataSource @@ -34,10 +37,20 @@ get_pyarrow_schema_from_batch_source, ) from feast.infra.registry.base_registry import BaseRegistry +from feast.monitoring.monitoring_utils import ( + MONITORING_DIR, + MONITORING_PARQUET_FILES, + monitoring_parquet_meta, + normalize_monitoring_row, + opt_float, +) from feast.on_demand_feature_view import OnDemandFeatureView from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage -from feast.utils import _get_requested_feature_views_to_features_dict +from feast.utils import ( + _get_requested_feature_views_to_features_dict, + compute_non_entity_date_range, +) # DaskRetrievalJob will cast string objects to string[pyarrow] from dask version 2023.7.1 # This is not the desired behavior for our use case, so we set the convert-string option to False @@ -128,26 +141,42 @@ def supports_remote_storage_export(self) -> bool: class DaskOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def get_historical_features( config: RepoConfig, feature_views: List[FeatureView], feature_refs: List[str], - entity_df: Union[pd.DataFrame, str], + entity_df: Optional[Union[pd.DataFrame, dd.DataFrame, str]], registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, + **kwargs, ) -> RetrievalJob: assert isinstance(config.offline_store, DaskOfflineStoreConfig) for fv in feature_views: assert isinstance(fv.batch_source, FileSource) - if not isinstance(entity_df, pd.DataFrame) and not isinstance( - entity_df, dd.DataFrame - ): - raise ValueError( - f"Please provide an entity_df of type {type(pd.DataFrame)} instead of type {type(entity_df)}" + non_entity_mode = entity_df is None + + if non_entity_mode: + start_date, end_date = compute_non_entity_date_range( + feature_views, + start_date=kwargs.get("start_date"), + end_date=kwargs.get("end_date"), ) + entity_df = pd.DataFrame( + {DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL: [end_date]} + ) + else: + if not isinstance(entity_df, pd.DataFrame) and not isinstance( + entity_df, dd.DataFrame + ): + raise ValueError( + f"Please provide an entity_df of type pd.DataFrame or dask.dataframe.DataFrame instead of type {type(entity_df)}" + ) entity_df_event_timestamp_col = DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL # local modifiable copy of global variable if entity_df_event_timestamp_col not in entity_df.columns: datetime_columns = entity_df.select_dtypes( @@ -171,8 +200,12 @@ def get_historical_features( registry.list_on_demand_feature_views(config.project), ) - entity_df_event_timestamp_range = _get_entity_df_event_timestamp_range( - entity_df, entity_df_event_timestamp_col + entity_df_event_timestamp_range = ( + (start_date, end_date) + if non_entity_mode + else _get_entity_df_event_timestamp_range( + entity_df, entity_df_event_timestamp_col + ) ) # Create lazy function that is only called from the RetrievalJob object @@ -192,9 +225,11 @@ def evaluate_historical_retrieval(): # Make sure all event timestamp fields are tz-aware. We default tz-naive fields to UTC entity_df_with_features[entity_df_event_timestamp_col] = ( entity_df_with_features[entity_df_event_timestamp_col].apply( - lambda x: x - if x.tzinfo is not None - else x.replace(tzinfo=timezone.utc) + lambda x: ( + x + if x.tzinfo is not None + else x.replace(tzinfo=timezone.utc) + ) ) ) @@ -260,7 +295,19 @@ def evaluate_historical_retrieval(): full_feature_names, ) - df_to_join = _merge(entity_df_with_features, df_to_join, join_keys) + # In non-entity mode, if the synthetic entity_df lacks join keys, cross join to build a snapshot + # of all entities as-of the requested timestamp, then rely on TTL and deduplication to select + # the appropriate latest rows per entity. + current_join_keys = join_keys + if non_entity_mode: + current_join_keys = [] + + # Snapshot before the merge blends the two sides together. + entity_df_columns = list(entity_df_with_features.columns) + + df_to_join = _merge( + entity_df_with_features, df_to_join, current_join_keys + ) df_to_join = _normalize_timestamp( df_to_join, timestamp_field, created_timestamp_column @@ -273,6 +320,17 @@ def evaluate_historical_retrieval(): timestamp_field, ) + if filter_by_created_timestamp and created_timestamp_column: + df_to_join = _apply_created_timestamp_cutoff( + df_to_join, + timestamp_field, + created_timestamp_column, + entity_df_event_timestamp_col, + # Join keys too: in non-entity mode they come from the + # feature view side. + set(entity_df_columns) | set(join_keys), + ) + df_to_join = _drop_duplicates( df_to_join, all_join_keys, @@ -346,7 +404,10 @@ def evaluate_func(): df[DUMMY_ENTITY_ID] = DUMMY_ENTITY_VAL columns_to_extract.add(DUMMY_ENTITY_ID) - return df[list(columns_to_extract)].persist() + if feature_name_columns: + df = df[list(columns_to_extract)] + + return df.persist() # When materializing a single feature view, we don't need full feature names. On demand transforms aren't materialized return DaskRetrievalJob( @@ -368,16 +429,23 @@ def evaluate_offline_job( # Create lazy function that is only called from the RetrievalJob object source_df = _read_datasource(data_source, config.repo_path) - source_df = _normalize_timestamp( - source_df, timestamp_field, created_timestamp_column - ) - + # Validate join keys against source columns BEFORE calling _normalize_timestamp. + # _normalize_timestamp ends with df.persist() which initialises Dask's global + # ThreadPoolExecutor. When the process subsequently exits (e.g. after raising + # FeastJoinKeysDuringMaterialization), Python's atexit handler calls + # ThreadPoolExecutor.shutdown(wait=True) which can block indefinitely, hanging + # the subprocess and preventing the parent from reading its output. + # df.columns is derived from the Parquet schema metadata — no computation needed. source_columns = set(source_df.columns) if not set(join_key_columns).issubset(source_columns): raise FeastJoinKeysDuringMaterialization( data_source.path, set(join_key_columns), source_columns ) + source_df = _normalize_timestamp( + source_df, timestamp_field, created_timestamp_column + ) + # try-catch block is added to deal with this issue https://github.com/dask/dask/issues/8939. # TODO(kevjumba): remove try catch when fix is merged upstream in Dask. try: @@ -452,9 +520,10 @@ def evaluate_func(): columns_to_extract.add(DUMMY_ENTITY_ID) # TODO: Decides if we want to field mapping for pull_latest_from_table_or_query # This is default for other offline store. - df = df[list(columns_to_extract)] - df.persist() - return df + if feature_name_columns: + df = df[list(columns_to_extract)] + + return df.persist() # When materializing a single feature view, we don't need full feature names. On demand transforms aren't materialized return DaskRetrievalJob( @@ -540,6 +609,373 @@ def offline_write_batch( writer.write_table(new_table) writer.close() + @staticmethod + def compute_monitoring_metrics( + config: RepoConfig, + data_source: DataSource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, DaskOfflineStoreConfig) + assert isinstance(data_source, FileSource) + + table = _dask_read_batch_arrow(data_source, config.repo_path) + table = _dask_filter_arrow_by_timestamp( + table, timestamp_field, start_date, end_date + ) + + results: List[Dict[str, Any]] = [] + for name, ftype in feature_columns: + if name not in table.column_names: + continue + col = table[name] + if ftype == "numeric": + m = _dask_compute_numeric_metrics(col, histogram_bins) + elif ftype == "categorical": + m = _dask_compute_categorical_metrics(col, top_n) + else: + continue + m["feature_name"] = name + results.append(m) + return results + + @staticmethod + def get_monitoring_max_timestamp( + config: RepoConfig, + data_source: DataSource, + timestamp_field: str, + ) -> Optional[datetime]: + assert isinstance(config.offline_store, DaskOfflineStoreConfig) + assert isinstance(data_source, FileSource) + + absolute_path = FileSource.get_uri_for_file_path( + repo_path=config.repo_path, + uri=data_source.file_options.uri, + ) + filesystem, path = FileSource.create_filesystem_and_path( + str(absolute_path), data_source.file_options.s3_endpoint_override + ) + try: + t = pq.read_table(path, filesystem=filesystem, columns=[timestamp_field]) + except Exception: + return None + if t.num_rows == 0: + return None + arr = t[timestamp_field] + mx = pc.max(arr) # type: ignore[attr-defined] + val = mx.as_py() + if val is None: + return None + if isinstance(val, datetime): + return val if val.tzinfo else val.replace(tzinfo=timezone.utc) + if isinstance(val, date): + return datetime.combine(val, datetime.min.time(), tzinfo=timezone.utc) + return None + + @staticmethod + def ensure_monitoring_tables(config: RepoConfig) -> None: + assert isinstance(config.offline_store, DaskOfflineStoreConfig) + base = os.path.join(_dask_monitoring_base(config), MONITORING_DIR) + os.makedirs(base, exist_ok=True) + + tables = [ + monitoring_parquet_meta(t) + for t in ("feature", "feature_view", "feature_service", "job") + ] + for fname, columns, _ in tables: + fpath = _dask_monitoring_path(config, fname) + if not os.path.isfile(fpath): + os.makedirs(os.path.dirname(fpath), exist_ok=True) + pd.DataFrame(columns=columns).to_parquet(fpath, index=False) + + @staticmethod + def save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], + ) -> None: + if not metrics: + return + assert isinstance(config.offline_store, DaskOfflineStoreConfig) + + fname, columns, pk = _dask_mon_table_meta(metric_type) + path = _dask_monitoring_path(config, fname) + _dask_parquet_upsert(path, columns, pk, metrics) + + @staticmethod + def query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, DaskOfflineStoreConfig) + + fname, columns, _ = _dask_mon_table_meta(metric_type) + path = _dask_monitoring_path(config, fname) + return _dask_parquet_query( + path, columns, project, filters, start_date, end_date + ) + + @staticmethod + def clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> None: + assert isinstance(config.offline_store, DaskOfflineStoreConfig) + + path = _dask_monitoring_path(config, MONITORING_PARQUET_FILES["feature"]) + tab = _dask_read_parquet_if_exists(path) + if tab is None or tab.num_rows == 0: + return + + df = tab.to_pandas() + mask = df["project_id"] == project + if feature_view_name is not None: + mask = mask & (df["feature_view_name"] == feature_view_name) + if feature_name is not None: + mask = mask & (df["feature_name"] == feature_name) + if data_source_type is not None: + mask = mask & (df["data_source_type"] == data_source_type) + mask = mask & (df["is_baseline"].isin([True, 1])) + df.loc[mask, "is_baseline"] = False + pq.write_table(pyarrow.Table.from_pandas(df, preserve_index=False), path) + + +def _dask_monitoring_base(config: RepoConfig) -> str: + base = config.repo_path + return str(base) if base else "." + + +def _dask_monitoring_path(config: RepoConfig, filename: str) -> str: + return os.path.join(_dask_monitoring_base(config), MONITORING_DIR, filename) + + +def _dask_mon_table_meta(metric_type: str): + return monitoring_parquet_meta(metric_type) + + +def _dask_read_parquet_if_exists(path: str) -> Optional[pyarrow.Table]: + if not os.path.isfile(path): + return None + return pq.read_table(path) + + +def _dask_read_batch_arrow( + data_source: FileSource, repo_path: Optional[Path] +) -> pyarrow.Table: + absolute_path = FileSource.get_uri_for_file_path( + repo_path=repo_path, + uri=data_source.file_options.uri, + ) + filesystem, path = FileSource.create_filesystem_and_path( + str(absolute_path), data_source.file_options.s3_endpoint_override + ) + return pq.read_table(path, filesystem=filesystem) + + +def _dask_filter_arrow_by_timestamp( + table: pyarrow.Table, + timestamp_field: str, + start_date: Optional[datetime], + end_date: Optional[datetime], +) -> pyarrow.Table: + if start_date is None and end_date is None: + return table + arr = table[timestamp_field] + mask = None + if start_date is not None: + mask = pc.greater_equal(arr, pyarrow.scalar(start_date)) # type: ignore[attr-defined] + if end_date is not None: + m2 = pc.less_equal(arr, pyarrow.scalar(end_date)) # type: ignore[attr-defined] + mask = m2 if mask is None else pc.and_(mask, m2) # type: ignore[attr-defined] + return table.filter(mask) + + +def _dask_compute_numeric_metrics( + column: pyarrow.ChunkedArray, histogram_bins: int +) -> Dict[str, Any]: + total = len(column) + null_count = column.null_count + result: Dict[str, Any] = { + "feature_type": "numeric", + "row_count": total, + "null_count": null_count, + "null_rate": null_count / total if total > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": None, + } + + valid = pc.drop_null(column) # type: ignore[attr-defined] + if len(valid) == 0: + return result + + float_array = pc.cast(valid, pyarrow.float64()) + result["mean"] = opt_float(pc.mean(float_array).as_py()) # type: ignore[attr-defined] + result["stddev"] = opt_float(pc.stddev(float_array, ddof=1).as_py()) # type: ignore[attr-defined] + + min_max = pc.min_max(float_array) # type: ignore[attr-defined] + result["min_val"] = min_max["min"].as_py() + result["max_val"] = min_max["max"].as_py() + + quantiles = pc.quantile(float_array, q=[0.50, 0.75, 0.90, 0.95, 0.99]) # type: ignore[attr-defined] + q_values = quantiles.to_pylist() + result["p50"] = q_values[0] + result["p75"] = q_values[1] + result["p90"] = q_values[2] + result["p95"] = q_values[3] + result["p99"] = q_values[4] + + np_array = float_array.to_numpy() + counts, bin_edges = np.histogram(np_array, bins=histogram_bins) + result["histogram"] = { + "bins": bin_edges.tolist(), + "counts": counts.tolist(), + "bin_width": float(bin_edges[1] - bin_edges[0]) if len(bin_edges) > 1 else 0, + } + + return result + + +def _dask_compute_categorical_metrics( + column: pyarrow.ChunkedArray, top_n: int +) -> Dict[str, Any]: + total = len(column) + null_count = column.null_count + result: Dict[str, Any] = { + "feature_type": "categorical", + "row_count": total, + "null_count": null_count, + "null_rate": null_count / total if total > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": None, + } + + valid = pc.drop_null(column) # type: ignore[attr-defined] + if len(valid) == 0: + return result + + value_counts = pc.value_counts(valid) # type: ignore[attr-defined] + entries = [ + {"value": vc["values"].as_py(), "count": vc["counts"].as_py()} + for vc in value_counts + ] + entries.sort(key=lambda x: x["count"], reverse=True) + + unique_count = len(entries) + top_entries = entries[:top_n] + other_count = sum(e["count"] for e in entries[top_n:]) + + result["histogram"] = { + "values": top_entries, + "other_count": other_count, + "unique_count": unique_count, + } + + return result + + +def _dask_parquet_upsert( + path: str, + columns: List[str], + pk_cols: List[str], + new_rows: List[Dict[str, Any]], +) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + + prepared: List[Dict[str, Any]] = [] + for row in new_rows: + r = dict(row) + if ( + "histogram" in r + and r["histogram"] is not None + and not isinstance(r["histogram"], str) + ): + r["histogram"] = json.dumps(r["histogram"]) + prepared.append(r) + + new_df = pd.DataFrame(prepared, columns=columns) + existing = _dask_read_parquet_if_exists(path) + if existing is not None: + old_df = existing.to_pandas() + combined = pd.concat([old_df, new_df], ignore_index=True) + else: + combined = new_df + + combined = combined.drop_duplicates(subset=pk_cols, keep="last") + table = pyarrow.Table.from_pandas(combined, preserve_index=False) + pq.write_table(table, path) + + +def _dask_parquet_query( + path: str, + columns: List[str], + project: str, + filters: Optional[Dict[str, Any]], + start_date: Optional[date], + end_date: Optional[date], +) -> List[Dict[str, Any]]: + tab = _dask_read_parquet_if_exists(path) + if tab is None or tab.num_rows == 0: + return [] + + df = tab.to_pandas() + if project: + df = df[df["project_id"] == project] + if filters: + for key, value in filters.items(): + if value is not None: + df = df[df[key] == value] + if "metric_date" in df.columns: + if start_date is not None: + df = df[df["metric_date"] >= start_date] + if end_date is not None: + df = df[df["metric_date"] <= end_date] + df = df.sort_values("metric_date", ascending=True) + else: + df = df.sort_values("job_id", ascending=True) + + results = [] + for _, row in df.iterrows(): + record = {c: row.get(c) for c in columns} + normalize_monitoring_row(record) + for key in ("metric_date", "computed_at"): + val = record.get(key) + if ( + val is not None + and not isinstance(val, str) + and hasattr(val, "isoformat") + ): + record[key] = val.isoformat() + results.append(record) + + return results + def _get_entity_df_event_timestamp_range( entity_df: Union[pd.DataFrame, str], @@ -606,7 +1042,10 @@ def _field_mapping( full_feature_names: bool, ) -> Tuple[dd.DataFrame, str]: # Rename columns by the field mapping dictionary if it exists - if feature_view.batch_source.field_mapping: + if ( + feature_view.batch_source is not None + and feature_view.batch_source.field_mapping + ): df_to_join = _run_dask_field_mapping( df_to_join, feature_view.batch_source.field_mapping ) @@ -761,6 +1200,32 @@ def _filter_ttl( return df_to_join +def _apply_created_timestamp_cutoff( + df_to_join: dd.DataFrame, + timestamp_field: str, + created_timestamp_column: str, + entity_df_event_timestamp_col: str, + preserved_columns: Set[str], +) -> dd.DataFrame: + # Versions created after the entity timestamp. The isna() term leaves rows with a + # null source timestamp untouched, matching _filter_ttl. + too_new = ~df_to_join[timestamp_field].isna() & ~( + df_to_join[created_timestamp_column] + <= df_to_join[entity_df_event_timestamp_col] + ) + + # Blank instead of drop, so the entity row survives when every candidate is too new. + # _drop_duplicates then prefers a real match (nulls sort first, keep="last"). + # Single assign, left lazy: a per-column loop is super-linear to optimize. + return df_to_join.assign( + **{ + column: df_to_join[column].mask(too_new) + for column in df_to_join.columns + if column not in preserved_columns + } + ) + + def _drop_duplicates( df_to_join: dd.DataFrame, all_join_keys: List[str], diff --git a/sdk/python/feast/infra/offline_stores/duckdb.py b/sdk/python/feast/infra/offline_stores/duckdb.py index 7bf96129d0b..f09d0a9f982 100644 --- a/sdk/python/feast/infra/offline_stores/duckdb.py +++ b/sdk/python/feast/infra/offline_stores/duckdb.py @@ -1,11 +1,22 @@ +from __future__ import annotations + +import json import os -from datetime import datetime +from datetime import date, datetime, timezone from pathlib import Path -from typing import Any, Callable, List, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union + +if TYPE_CHECKING: + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, + ) +import duckdb import ibis import pandas as pd import pyarrow +import pyarrow as pa +import pyarrow.parquet as pq from ibis.expr.types import Table from pydantic import StrictStr @@ -23,21 +34,70 @@ write_logged_features_ibis, ) from feast.infra.offline_stores.offline_store import OfflineStore, RetrievalJob +from feast.infra.offline_stores.offline_utils import get_timestamp_filter_sql from feast.infra.registry.base_registry import BaseRegistry +from feast.monitoring.monitoring_utils import ( + MONITORING_DIR, + MONITORING_PARQUET_FILES, + empty_categorical_metric, + empty_numeric_metric, + monitoring_parquet_meta, + normalize_monitoring_row, + opt_float, +) from feast.repo_config import FeastConfigBaseModel, RepoConfig def _read_data_source(data_source: DataSource, repo_path: str) -> Table: + from feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source import ( + IcebergSource, + ) + + if isinstance(data_source, IcebergSource): + return _read_iceberg_catalog_source(data_source, repo_path) + assert isinstance(data_source, FileSource) - if isinstance(data_source.file_format, ParquetFormat): + if isinstance(data_source.file_format, ParquetFormat) or ( + data_source.file_format is None and data_source.path.endswith(".parquet") + ): return ibis.read_parquet(data_source.path) elif isinstance(data_source.file_format, DeltaFormat): - storage_options = { - "AWS_ENDPOINT_URL": data_source.s3_endpoint_override, + if data_source.s3_endpoint_override: + storage_options = { + "AWS_ENDPOINT_URL": data_source.s3_endpoint_override, + } + return ibis.read_delta(data_source.path, storage_options=storage_options) + return ibis.read_delta(data_source.path) + + +def _read_iceberg_catalog_source(data_source: "IcebergSource", repo_path: str) -> Table: + """Read from an IcebergSource via PyIceberg. + + Loads the table through the configured catalog (REST, SQL, Hive, etc.) + and scans it to Arrow. + """ + fqn = f"{data_source.namespace}.{data_source.iceberg_table}" + + if data_source.catalog_type != "rest": + catalog_client = data_source.get_catalog_client() + table = catalog_client.load_table(fqn) + else: + from pyiceberg.catalog import load_catalog + + catalog_config: dict = { + "type": "rest", + "uri": data_source.endpoint, + "warehouse": data_source.warehouse, } + if data_source.token_env_var: + token = os.environ.get(data_source.token_env_var) + if token: + catalog_config["token"] = token + catalog = load_catalog(data_source.catalog_name, **catalog_config) + table = catalog.load_table(fqn) - return ibis.read_delta(data_source.path, storage_options=storage_options) + return ibis.memtable(table.scan().to_arrow()) def _write_data_source( @@ -62,7 +122,9 @@ def _write_data_source( ): raise SavedDatasetLocationAlreadyExists(location=file_options.uri) - if isinstance(data_source.file_format, ParquetFormat): + if data_source.file_format is None or isinstance( + data_source.file_format, ParquetFormat + ): if mode == "overwrite": table = table.to_pyarrow() @@ -112,6 +174,318 @@ def _write_data_source( ) +# ------------------------------------------------------------------ # +# DuckDB monitoring (Parquet-backed) +# ------------------------------------------------------------------ # + + +def _duckdb_monitoring_base(config: RepoConfig) -> str: + base = config.repo_path + return str(base) if base else "." + + +def _duckdb_monitoring_path(config: RepoConfig, filename: str) -> str: + return os.path.join(_duckdb_monitoring_base(config), MONITORING_DIR, filename) + + +def _duckdb_parquet_from_expression(config: RepoConfig, data_source: FileSource) -> str: + absolute_path = FileSource.get_uri_for_file_path( + repo_path=_duckdb_monitoring_base(config), + uri=data_source.file_options.uri, + ) + return str(absolute_path).replace("'", "''") + + +def _duckdb_quote_ident(name: str) -> str: + return f'"{name}"' + + +def _duckdb_ts_where(ts_filter: str) -> str: + return f"({ts_filter})" if (ts_filter and ts_filter.strip()) else "1=1" + + +def _duckdb_numeric_stats( + conn: duckdb.DuckDBPyConnection, + from_expr: str, + feature_names: List[str], + ts_filter: str, + histogram_bins: int, +) -> List[Dict[str, Any]]: + select_parts = ["COUNT(*)"] + for col in feature_names: + q = _duckdb_quote_ident(col) + c = f"CAST({q} AS DOUBLE)" + select_parts.extend( + [ + f"COUNT({q})", + f"AVG({c})", + f"STDDEV_SAMP({c})", + f"MIN({c})", + f"MAX({c})", + f"PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY {c})", + ] + ) + + tw = _duckdb_ts_where(ts_filter) + query = f"SELECT {', '.join(select_parts)} FROM {from_expr} AS _src WHERE {tw}" + row = conn.execute(query).fetchone() + + if row is None: + return [empty_numeric_metric(n) for n in feature_names] + + row_count = row[0] + results: List[Dict[str, Any]] = [] + + for i, col in enumerate(feature_names): + base = 1 + i * 10 + non_null = row[base] or 0 + null_count = row_count - non_null + + min_val = opt_float(row[base + 3]) + max_val = opt_float(row[base + 4]) + + result: Dict[str, Any] = { + "feature_name": col, + "feature_type": "numeric", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": opt_float(row[base + 1]), + "stddev": opt_float(row[base + 2]), + "min_val": min_val, + "max_val": max_val, + "p50": opt_float(row[base + 5]), + "p75": opt_float(row[base + 6]), + "p90": opt_float(row[base + 7]), + "p95": opt_float(row[base + 8]), + "p99": opt_float(row[base + 9]), + "histogram": None, + } + + if min_val is not None and max_val is not None and non_null > 0: + result["histogram"] = _duckdb_numeric_histogram( + conn, + from_expr, + col, + ts_filter, + histogram_bins, + min_val, + max_val, + ) + + results.append(result) + + return results + + +def _duckdb_numeric_histogram( + conn: duckdb.DuckDBPyConnection, + from_expr: str, + col_name: str, + ts_filter: str, + bins: int, + min_val: float, + max_val: float, +) -> Dict[str, Any]: + q_col = _duckdb_quote_ident(col_name) + + tw = _duckdb_ts_where(ts_filter) + if min_val == max_val: + row = conn.execute( + f"SELECT COUNT(*) FROM {from_expr} AS _src " + f"WHERE {q_col} IS NOT NULL AND {tw}" + ).fetchone() + cnt = row[0] if row else 0 + return {"bins": [min_val, max_val], "counts": [cnt], "bin_width": 0.0} + + bin_width = (max_val - min_val) / bins + + query = ( + f"SELECT LEAST(FLOOR((CAST({q_col} AS DOUBLE) - {min_val}) / {bin_width}) + 1, {bins}) AS bucket, " + f"COUNT(*) AS cnt " + f"FROM {from_expr} AS _src " + f"WHERE {q_col} IS NOT NULL AND {tw} " + f"GROUP BY bucket ORDER BY bucket" + ) + rows = conn.execute(query).fetchall() + + counts = [0] * bins + for bucket, cnt in rows: + b = int(bucket) + if 1 <= b <= bins: + counts[b - 1] = cnt + + bin_edges = [min_val + i * bin_width for i in range(bins + 1)] + return { + "bins": [float(b) for b in bin_edges], + "counts": counts, + "bin_width": float(bin_width), + } + + +def _duckdb_categorical_stats( + conn: duckdb.DuckDBPyConnection, + from_expr: str, + col_name: str, + ts_filter: str, + top_n: int, +) -> Dict[str, Any]: + q_col = _duckdb_quote_ident(col_name) + + tw = _duckdb_ts_where(ts_filter) + query = ( + f"WITH filtered AS (" + f" SELECT * FROM {from_expr} AS _src WHERE {tw}" + f") " + f"SELECT " + f" (SELECT COUNT(*) FROM filtered) AS row_count, " + f" (SELECT COUNT(*) - COUNT({q_col}) FROM filtered) AS null_count, " + f" (SELECT COUNT(DISTINCT {q_col}) FROM filtered " + f" WHERE {q_col} IS NOT NULL) AS unique_count, " + f" CAST({q_col} AS VARCHAR) AS value, COUNT(*) AS cnt " + f"FROM filtered WHERE {q_col} IS NOT NULL " + f"GROUP BY {q_col} " + f"ORDER BY cnt DESC LIMIT {int(top_n)}" + ) + + rows = conn.execute(query).fetchall() + + if not rows: + return empty_categorical_metric(col_name) + + row_count = rows[0][0] + null_count = rows[0][1] + unique_count = rows[0][2] + + top_entries = [{"value": r[3], "count": r[4]} for r in rows] + top_total = sum(e["count"] for e in top_entries) + other_count = (row_count - null_count) - top_total + + return { + "feature_name": col_name, + "feature_type": "categorical", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": { + "values": top_entries, + "other_count": max(other_count, 0), + "unique_count": unique_count, + }, + } + + +def _duckdb_mon_table_meta(metric_type: str): + return monitoring_parquet_meta(metric_type) + + +def _duckdb_read_parquet_if_exists(path: str) -> Optional[pa.Table]: + if not os.path.isfile(path): + return None + return pq.read_table(path) + + +def _duckdb_parquet_upsert( + path: str, + columns: List[str], + pk_cols: List[str], + new_rows: List[Dict[str, Any]], +) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + + prepared: List[Dict[str, Any]] = [] + for row in new_rows: + r = dict(row) + if ( + "histogram" in r + and r["histogram"] is not None + and not isinstance(r["histogram"], str) + ): + r["histogram"] = json.dumps(r["histogram"]) + prepared.append(r) + + new_df = pd.DataFrame(prepared, columns=columns) + existing = _duckdb_read_parquet_if_exists(path) + if existing is not None: + old_df = existing.to_pandas() + combined = pd.concat([old_df, new_df], ignore_index=True) + else: + combined = new_df + + combined = combined.drop_duplicates(subset=pk_cols, keep="last") + table = pa.Table.from_pandas(combined, preserve_index=False) + pq.write_table(table, path) + + +def _duckdb_parquet_query( + path: str, + columns: List[str], + project: str, + filters: Optional[Dict[str, Any]], + start_date: Optional[date], + end_date: Optional[date], +) -> List[Dict[str, Any]]: + tab = _duckdb_read_parquet_if_exists(path) + if tab is None or tab.num_rows == 0: + return [] + + df = tab.to_pandas() + if project: + df = df[df["project_id"] == project] + if filters: + for key, value in filters.items(): + if value is not None: + df = df[df[key] == value] + if "metric_date" in df.columns: + if start_date is not None: + df = df[df["metric_date"] >= start_date] + if end_date is not None: + df = df[df["metric_date"] <= end_date] + df = df.sort_values("metric_date", ascending=True) + else: + df = df.sort_values("job_id", ascending=True) + + results = [] + for _, row in df.iterrows(): + record = {c: row.get(c) for c in columns} + normalize_monitoring_row(record) + for key in ("metric_date", "computed_at"): + val = record.get(key) + if ( + val is not None + and not isinstance(val, str) + and hasattr(val, "isoformat") + ): + record[key] = val.isoformat() + results.append(record) + + return results + + +def _duckdb_sql_from_expression(config: RepoConfig, data_source: FileSource) -> str: + p = _duckdb_parquet_from_expression(config, data_source) + if isinstance(data_source.file_format, ParquetFormat): + return f"read_parquet('{p}')" + if isinstance(data_source.file_format, DeltaFormat): + return f"delta_scan('{p}')" + raise NotImplementedError( + "DuckDB monitoring compute supports Parquet and Delta file sources only." + ) + + class DuckDBOfflineStoreConfig(FeastConfigBaseModel): type: StrictStr = "duckdb" # """ Offline store type selector""" @@ -122,6 +496,8 @@ class DuckDBOfflineStoreConfig(FeastConfigBaseModel): class DuckDBOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -157,6 +533,7 @@ def get_historical_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, ) -> RetrievalJob: return get_historical_features_ibis( config=config, @@ -170,6 +547,7 @@ def get_historical_features( data_source_writer=_write_data_source, staging_location=config.offline_store.staging_location, staging_location_endpoint_override=config.offline_store.staging_location_endpoint_override, + filter_by_created_timestamp=filter_by_created_timestamp, ) @staticmethod @@ -228,3 +606,155 @@ def write_logged_features( logging_config=logging_config, registry=registry, ) + + @staticmethod + def compute_monitoring_metrics( + config: RepoConfig, + data_source: DataSource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, DuckDBOfflineStoreConfig) + assert isinstance(data_source, FileSource) + + from_expr = _duckdb_sql_from_expression(config, data_source) + ts_filter = get_timestamp_filter_sql( + start_date, + end_date, + timestamp_field, + tz=timezone.utc, + cast_style="timestamp", + date_time_separator=" ", + ) + + numeric_features = [n for n, t in feature_columns if t == "numeric"] + categorical_features = [n for n, t in feature_columns if t == "categorical"] + results: List[Dict[str, Any]] = [] + + conn = duckdb.connect() + if numeric_features: + results.extend( + _duckdb_numeric_stats( + conn, + from_expr, + numeric_features, + ts_filter, + histogram_bins, + ) + ) + for col_name in categorical_features: + results.append( + _duckdb_categorical_stats( + conn, + from_expr, + col_name, + ts_filter, + top_n, + ) + ) + conn.close() + return results + + @staticmethod + def get_monitoring_max_timestamp( + config: RepoConfig, + data_source: DataSource, + timestamp_field: str, + ) -> Optional[datetime]: + assert isinstance(config.offline_store, DuckDBOfflineStoreConfig) + assert isinstance(data_source, FileSource) + + from_expr = _duckdb_sql_from_expression(config, data_source) + ts_col = _duckdb_quote_ident(timestamp_field) + conn = duckdb.connect() + row = conn.execute( + f"SELECT MAX({ts_col}) AS m FROM {from_expr} AS _src" + ).fetchone() + conn.close() + + if row is None or row[0] is None: + return None + val = row[0] + if isinstance(val, datetime): + return val if val.tzinfo else val.replace(tzinfo=timezone.utc) + if isinstance(val, date): + return datetime.combine(val, datetime.min.time(), tzinfo=timezone.utc) + return None + + @staticmethod + def ensure_monitoring_tables(config: RepoConfig) -> None: + assert isinstance(config.offline_store, DuckDBOfflineStoreConfig) + base = os.path.join(_duckdb_monitoring_base(config), MONITORING_DIR) + os.makedirs(base, exist_ok=True) + + tables = [ + monitoring_parquet_meta(t) + for t in ("feature", "feature_view", "feature_service", "job") + ] + for fname, columns, _ in tables: + path = _duckdb_monitoring_path(config, fname) + if not os.path.isfile(path): + os.makedirs(os.path.dirname(path), exist_ok=True) + pd.DataFrame(columns=columns).to_parquet(path, index=False) + + @staticmethod + def save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], + ) -> None: + if not metrics: + return + assert isinstance(config.offline_store, DuckDBOfflineStoreConfig) + + fname, columns, pk = _duckdb_mon_table_meta(metric_type) + path = _duckdb_monitoring_path(config, fname) + _duckdb_parquet_upsert(path, columns, pk, metrics) + + @staticmethod + def query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, DuckDBOfflineStoreConfig) + + fname, columns, _ = _duckdb_mon_table_meta(metric_type) + path = _duckdb_monitoring_path(config, fname) + return _duckdb_parquet_query( + path, columns, project, filters, start_date, end_date + ) + + @staticmethod + def clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> None: + assert isinstance(config.offline_store, DuckDBOfflineStoreConfig) + + path = _duckdb_monitoring_path(config, MONITORING_PARQUET_FILES["feature"]) + tab = _duckdb_read_parquet_if_exists(path) + if tab is None or tab.num_rows == 0: + return + + df = tab.to_pandas() + mask = df["project_id"] == project + if feature_view_name is not None: + mask = mask & (df["feature_view_name"] == feature_view_name) + if feature_name is not None: + mask = mask & (df["feature_name"] == feature_name) + if data_source_type is not None: + mask = mask & (df["data_source_type"] == data_source_type) + mask = mask & (df["is_baseline"].isin([True, 1])) + df.loc[mask, "is_baseline"] = False + pq.write_table(pa.Table.from_pandas(df, preserve_index=False), path) diff --git a/sdk/python/feast/infra/offline_stores/file_source.py b/sdk/python/feast/infra/offline_stores/file_source.py index 12524ab4fc2..0b5dd162cb6 100644 --- a/sdk/python/feast/infra/offline_stores/file_source.py +++ b/sdk/python/feast/infra/offline_stores/file_source.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union from urllib.parse import urlparse @@ -24,6 +25,8 @@ from feast.saved_dataset import SavedDatasetStorage from feast.value_type import ValueType +logger = logging.getLogger(__name__) + @typechecked class FileSource(DataSource): @@ -94,7 +97,7 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, FileSource): - raise TypeError("Comparisons should only involve FileSource class objects.") + return False return ( super().__eq__(other) @@ -151,8 +154,43 @@ def _to_proto_impl(self) -> DataSourceProto: return data_source_proto def validate(self, config: RepoConfig): - # TODO: validate a FileSource - pass + """Validate that the file source exists and is readable. + + Checks that the path resolves to an existing Parquet or Delta file + and that the declared timestamp column is present in the schema. + """ + from feast.infra.offline_stores.file_source import FileSource + + uri = self.path + repo_path = config.repo_path if hasattr(config, "repo_path") else None + resolved = FileSource.get_uri_for_file_path(repo_path, uri) + + try: + filesystem, path = FileSystem.from_uri(resolved) + file_info = filesystem.get_file_info(path) + if file_info.type == pyarrow.fs.FileType.NotFound: + raise FileNotFoundError(f"FileSource path does not exist: {resolved}") + except Exception as e: + logger.warning("Could not validate FileSource path '%s': %s", resolved, e) + return + + try: + if isinstance(self.file_options.file_format, DeltaFormat): + return + pq_dataset = ParquetDataset(path, filesystem=filesystem) + schema = pq_dataset.schema + if self.timestamp_field and self.timestamp_field not in schema.names: + logger.warning( + "Timestamp field '%s' not found in FileSource schema at '%s'. " + "Available columns: %s", + self.timestamp_field, + resolved, + schema.names, + ) + except Exception as e: + logger.warning( + "Could not read schema from FileSource '%s': %s", resolved, e + ) @staticmethod def source_datatype_to_feast_value_type() -> Callable[[str], ValueType]: @@ -202,11 +240,18 @@ def get_table_column_names_and_types( "AWS_ENDPOINT_URL": str(self.s3_endpoint_override), } - schema = ( - DeltaTable(self.path, storage_options=storage_options) - .schema() - .to_pyarrow() - ) + delta_schema = DeltaTable( + self.path, storage_options=storage_options + ).schema() + if hasattr(delta_schema, "to_arrow"): + # deltalake >= 0.10.0 + arro3_schema = delta_schema.to_arrow() + schema = pyarrow.schema(arro3_schema) + elif hasattr(delta_schema, "to_pyarrow"): + # deltalake < 0.10.0 + schema = delta_schema.to_pyarrow() + else: + raise Exception("Unknown DeltaTable package version") else: raise Exception(f"Unknown FileFormat -> {self.file_format}") diff --git a/sdk/python/feast/infra/offline_stores/hybrid_offline_store.py b/sdk/python/feast/infra/offline_stores/hybrid_offline_store.py index b37877276c9..2da5c2f2701 100644 --- a/sdk/python/feast/infra/offline_stores/hybrid_offline_store.py +++ b/sdk/python/feast/infra/offline_stores/hybrid_offline_store.py @@ -31,6 +31,8 @@ class OfflineStoresWithConfig(FeastConfigBaseModel): class HybridOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + _instance: Optional["HybridOfflineStore"] = None _initialized: bool offline_stores: Dict[str, OfflineStore] @@ -72,6 +74,8 @@ def _get_offline_store_for_feature_view( self, feature_view: FeatureView, config: RepoConfig ) -> OfflineStore: self._initialize_offline_stores(config) + if feature_view.batch_source is None: + raise ValueError(f"Feature view '{feature_view.name}' has no batch_source.") source_type = feature_view.batch_source.source_type() store_key = self.get_source_key_from_type(source_type) if store_key is None: @@ -99,6 +103,7 @@ def get_historical_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + **kwargs, ) -> RetrievalJob: # TODO: Multiple data sources can be supported when feature store use compute engine # for getting historical features @@ -118,6 +123,9 @@ def get_historical_features( store = HybridOfflineStore()._get_offline_store_for_feature_view( feature_views[0], config ) + # The hybrid store only supports the flag if the store it delegates to does. + if kwargs.get("filter_by_created_timestamp"): + store.ensure_filter_by_created_timestamp_supported() return store.get_historical_features( config, feature_views, @@ -126,6 +134,7 @@ def get_historical_features( registry, project, full_feature_names, + **kwargs, ) @staticmethod diff --git a/sdk/python/feast/infra/offline_stores/ibis.py b/sdk/python/feast/infra/offline_stores/ibis.py index 95c5afef2db..e0aadd63b4a 100644 --- a/sdk/python/feast/infra/offline_stores/ibis.py +++ b/sdk/python/feast/infra/offline_stores/ibis.py @@ -6,7 +6,6 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union import ibis -import ibis.selectors as s import numpy as np import pandas as pd import pyarrow @@ -54,8 +53,8 @@ def pull_latest_from_table_or_query_ibis( fields = join_key_columns + feature_name_columns + [timestamp_field] if created_timestamp_column: fields.append(created_timestamp_column) - start_date = start_date.astimezone(tz=timezone.utc) - end_date = end_date.astimezone(tz=timezone.utc) + start_date = start_date.astimezone(tz=timezone.utc).replace(tzinfo=None) + end_date = end_date.astimezone(tz=timezone.utc).replace(tzinfo=None) table = data_source_reader(data_source, str(config.repo_path)) @@ -67,8 +66,8 @@ def pull_latest_from_table_or_query_ibis( table = table.filter( ibis.and_( - table[timestamp_field] >= ibis.literal(start_date), - table[timestamp_field] <= ibis.literal(end_date), + table[timestamp_field].cast("timestamp") >= ibis.literal(start_date), + table[timestamp_field].cast("timestamp") <= ibis.literal(end_date), ) ) @@ -131,7 +130,7 @@ def _generate_row_id( else: all_entities.extend([e.name for e in fv.entity_columns]) - r = ibis.literal("") + r = ibis.literal("_") for e in set(all_entities): r = r.concat(entity_table[e].cast("string")) # type: ignore @@ -154,6 +153,7 @@ def get_historical_features_ibis( staging_location: Optional[str] = None, staging_location_endpoint_override: Optional[str] = None, event_expire_timestamp_fn=None, + filter_by_created_timestamp: bool = False, ) -> RetrievalJob: entity_schema = _get_entity_schema( entity_df=entity_df, @@ -175,6 +175,10 @@ def get_historical_features_ibis( def read_fv( feature_view: FeatureView, feature_refs: List[str], full_feature_names: bool ) -> Tuple: + if feature_view.batch_source is None: + raise ValueError( + f"Feature view '{feature_view.name}' has no batch_source and cannot be queried." + ) fv_table: Table = data_source_reader( feature_view.batch_source, str(config.repo_path) ) @@ -184,17 +188,22 @@ def read_fv( fv_table = fv_table.rename({new_name: old_name}) timestamp_field = feature_view.batch_source.timestamp_field + created_timestamp_column = feature_view.batch_source.created_timestamp_column + + # deduplicate() orders by created_timestamp_column whether or not the cutoff is + # on, so this cannot be gated on it. Casting an already-UTC column is a no-op. + utc_columns = [timestamp_field] + if created_timestamp_column: + utc_columns.append(created_timestamp_column) - # TODO mutate only if tz-naive fv_table = fv_table.mutate( **{ - timestamp_field: fv_table[timestamp_field].cast( - dt.Timestamp(timezone="UTC") - ) + column: fv_table[column].cast(dt.Timestamp(timezone="UTC")) + for column in utc_columns } ) - full_name_prefix = feature_view.projection.name_alias or feature_view.name + full_name_prefix = feature_view.projection.name_to_use() feature_refs = [ fr.split(":")[1] @@ -202,19 +211,22 @@ def read_fv( if fr.startswith(f"{full_name_prefix}:") ] + # Use base name (without version) for column naming + base_name_prefix = feature_view.projection.name_alias or feature_view.name + if full_feature_names: fv_table = fv_table.rename( - {f"{full_name_prefix}__{feature}": feature for feature in feature_refs} + {f"{base_name_prefix}__{feature}": feature for feature in feature_refs} ) feature_refs = [ - f"{full_name_prefix}__{feature}" for feature in feature_refs + f"{base_name_prefix}__{feature}" for feature in feature_refs ] return ( fv_table, - feature_view.batch_source.timestamp_field, - feature_view.batch_source.created_timestamp_column, + timestamp_field, + created_timestamp_column, feature_view.projection.join_key_map or {e.name: e.name for e in feature_view.entity_columns}, feature_refs, @@ -229,6 +241,7 @@ def read_fv( ], event_timestamp_col=event_timestamp_col, event_expire_timestamp_fn=event_expire_timestamp_fn, + filter_by_created_timestamp=filter_by_created_timestamp, ) odfvs = OnDemandFeatureView.get_requested_odfvs(feature_refs, project, registry) @@ -273,9 +286,9 @@ def pull_all_from_table_or_query_ibis( timestamp_fields.append(created_timestamp_column) fields = join_key_columns + feature_name_columns + timestamp_fields if start_date: - start_date = start_date.astimezone(tz=timezone.utc) + start_date = start_date.astimezone(tz=timezone.utc).replace(tzinfo=None) if end_date: - end_date = end_date.astimezone(tz=timezone.utc) + end_date = end_date.astimezone(tz=timezone.utc).replace(tzinfo=None) table = data_source_reader(data_source, str(config.repo_path)) @@ -287,10 +300,10 @@ def pull_all_from_table_or_query_ibis( table = table.filter( ibis.and_( - table[timestamp_field] >= ibis.literal(start_date) + table[timestamp_field].cast("timestamp") >= ibis.literal(start_date) if start_date else ibis.literal(True), - table[timestamp_field] <= ibis.literal(end_date) + table[timestamp_field].cast("timestamp") <= ibis.literal(end_date) if end_date else ibis.literal(True), ) @@ -336,6 +349,8 @@ def offline_write_batch_ibis( progress: Optional[Callable[[int], Any]], data_source_writer: Callable[[pyarrow.Table, DataSource, str], None], ): + if feature_view.batch_source is None: + raise ValueError(f"Feature view '{feature_view.name}' has no batch_source.") pa_schema, column_names = get_pyarrow_schema_from_batch_source( config, feature_view.batch_source ) @@ -371,6 +386,7 @@ def point_in_time_join( feature_tables: List[Tuple[Table, str, str, Dict[str, str], List[str], timedelta]], event_timestamp_col="event_timestamp", event_expire_timestamp_fn=None, + filter_by_created_timestamp: bool = False, ): # TODO handle ttl all_entities = [event_timestamp_col] @@ -384,7 +400,7 @@ def point_in_time_join( ) in feature_tables: all_entities.extend(join_key_map.values()) - r = ibis.literal("") + r = ibis.literal("_") for e in set(all_entities): r = r.concat(entity_table[e].cast("string")) # type: ignore @@ -393,6 +409,10 @@ def point_in_time_join( acc_table = entity_table + # Track the columns we want to keep explicitly + entity_columns = list(entity_table.columns) + all_feature_cols: List[str] = [] + for ( feature_table, timestamp_field, @@ -410,7 +430,7 @@ def point_in_time_join( else: alias = "".join(random.choices(string.ascii_uppercase, k=10)) - feature_table = feature_table.alias(alias=alias).sql( + feature_table = feature_table.alias(alias).sql( f"SELECT *, {event_expire_timestamp_fn(timestamp_field, ttl)} AS event_expire_timestamp FROM {alias}" ) @@ -422,6 +442,12 @@ def point_in_time_join( feature_table[timestamp_field] <= entity_table[event_timestamp_col], ) + if filter_by_created_timestamp and created_timestamp_field: + predicates.append( + feature_table[created_timestamp_field] + <= entity_table[event_timestamp_col], + ) + if ttl: predicates.append( feature_table["event_expire_timestamp"] @@ -432,7 +458,8 @@ def point_in_time_join( entity_table, predicates, lname="", rname="{name}_y" ) - feature_table = feature_table.drop(s.endswith("_y")) + cols_to_drop_y = [c for c in feature_table.columns if c.endswith("_y")] + feature_table = feature_table.drop(*cols_to_drop_y) feature_table = deduplicate( table=feature_table, @@ -445,6 +472,9 @@ def point_in_time_join( select_cols.extend(feature_refs) feature_table = feature_table.select(select_cols) + # Track the feature columns we're adding + all_feature_cols.extend(feature_refs) + acc_table = acc_table.left_join( feature_table, predicates=[feature_table.entity_row_id == acc_table.entity_row_id], @@ -452,9 +482,9 @@ def point_in_time_join( rname="{name}_yyyy", ) - acc_table = acc_table.drop(s.endswith("_yyyy")) - - acc_table = acc_table.drop("entity_row_id") + # Select only the columns we want: entity columns (minus entity_row_id) + all feature columns + final_cols = [c for c in entity_columns if c != "entity_row_id"] + all_feature_cols + acc_table = acc_table.select(final_cols) return acc_table diff --git a/sdk/python/feast/infra/offline_stores/offline_store.py b/sdk/python/feast/infra/offline_stores/offline_store.py index 5961c1f4292..9d4092d6799 100644 --- a/sdk/python/feast/infra/offline_stores/offline_store.py +++ b/sdk/python/feast/infra/offline_stores/offline_store.py @@ -11,9 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import logging +import time import warnings from abc import ABC -from datetime import datetime +from datetime import date, datetime, timedelta, timezone from pathlib import Path from typing import ( TYPE_CHECKING, @@ -70,6 +72,23 @@ def __init__( self.max_event_timestamp = max_event_timestamp +def _extract_retrieval_metadata(job: "RetrievalJob") -> tuple: + """Return ``(feature_view_names, feature_count)`` from a RetrievalJob's metadata.""" + from feast.utils import _parse_feature_ref + + try: + meta = job.metadata + if meta: + feature_count = len(meta.features) + feature_views = list( + {_parse_feature_ref(ref)[0] for ref in meta.features if ":" in ref} + ) + return feature_views, feature_count + except (NotImplementedError, AttributeError): + pass + return [], 0 + + class RetrievalJob(ABC): """A RetrievalJob manages the execution of a query to retrieve data from the offline store.""" @@ -152,19 +171,105 @@ def to_arrow( validation_reference (optional): The validation to apply against the retrieved dataframe. timeout (optional): The query timeout if applicable. """ - features_table = self._to_arrow_internal(timeout=timeout) + start_wall = time.monotonic() + status_label = "success" + row_count = 0 + try: + features_table = self._to_arrow_internal(timeout=timeout) + row_count = features_table.num_rows + except Exception: + status_label = "error" + raise + finally: + try: + from feast import metrics as feast_metrics + + elapsed = time.monotonic() - start_wall + + if feast_metrics._config.offline_features: + feast_metrics.offline_store_request_total.labels( + method="to_arrow", status=status_label + ).inc() + feast_metrics.offline_store_request_latency_seconds.labels( + method="to_arrow" + ).observe(elapsed) + feast_metrics.offline_store_row_count.labels( + method="to_arrow" + ).observe(row_count) + + if feast_metrics._config.audit_logging: + feature_views, feature_count = _extract_retrieval_metadata(self) + end_dt = datetime.now(tz=timezone.utc) + start_dt = end_dt - timedelta(seconds=elapsed) + feast_metrics.emit_offline_audit_log( + method="to_arrow", + feature_views=feature_views, + feature_count=feature_count, + row_count=row_count, + status=status_label, + start_time=start_dt.isoformat(), + end_time=end_dt.isoformat(), + duration_ms=elapsed * 1000, + ) + except Exception: + logging.getLogger(__name__).debug( + "Failed to record offline store metrics", exc_info=True + ) + if self.on_demand_feature_views: + # Build a mapping of ODFV name to requested feature names + # This ensures we only return the features that were explicitly requested + odfv_feature_refs: Dict[str, set[str]] = {} + try: + metadata = self.metadata + except NotImplementedError: + metadata = None + + if metadata and metadata.features: + for feature_ref in metadata.features: + if ":" in feature_ref: + view_name, feature_name = feature_ref.split(":", 1) + # Check if this view_name matches any of the ODFVs + for odfv in self.on_demand_feature_views: + if ( + odfv.name == view_name + or odfv.projection.name_to_use() == view_name + ): + if view_name not in odfv_feature_refs: + odfv_feature_refs[view_name] = set() + # Store the feature name in the format that will appear in transformed_arrow + expected_col_name = ( + f"{odfv.projection.name_to_use()}__{feature_name}" + if self.full_feature_names + else feature_name + ) + odfv_feature_refs[view_name].add(expected_col_name) + for odfv in self.on_demand_feature_views: transformed_arrow = odfv.transform_arrow( features_table, self.full_feature_names ) + # Determine which columns to include from this ODFV + # If we have metadata with requested features, filter to only those + # Otherwise, include all columns (backward compatibility) + requested_features_for_odfv = ( + odfv_feature_refs.get(odfv.name) + if odfv.name in odfv_feature_refs + else odfv_feature_refs.get(odfv.projection.name_to_use()) + ) + for col in transformed_arrow.column_names: if col.startswith("__index"): continue - features_table = features_table.append_column( - col, transformed_arrow[col] - ) + # Only append the column if it was requested, or if we don't have feature metadata + if ( + requested_features_for_odfv is None + or col in requested_features_for_odfv + ): + features_table = features_table.append_column( + col, transformed_arrow[col] + ) if validation_reference: if not flags_helper.is_test(): @@ -217,6 +322,49 @@ def to_tensor( tensor_dict[column] = values return tensor_dict + def to_ray_dataset(self) -> Any: + """Convert the retrieval result to a Ray Dataset. + + This is a first-class method on every ``RetrievalJob`` regardless of + the configured offline store backend: + + * **Ray offline store** – ``RayRetrievalJob`` overrides this method and + returns the underlying ``ray.data.Dataset`` *without* materialising the + result to the driver, keeping the full computation on the cluster. + * **All other offline stores** – the default implementation here pulls + the result to an Arrow table on the driver and converts it via + ``ray.data.from_arrow()``. This is less efficient than the native + Ray path but lets every backend participate in Ray pipelines. + + Example:: + + from feast import FeatureStore + + store = FeatureStore(".") + ds = store.get_historical_features( + entity_df=entity_df, + features=["driver_stats:conv_rate"], + ).to_ray_dataset() + + # Chain Ray Data transforms directly + predictions = ds.map_batches(MyModel, num_gpus=1) + + Returns: + ray.data.Dataset: Dataset containing the retrieved feature rows. + + Raises: + ImportError: If ``ray`` is not installed. Install via + ``pip install 'feast[ray]'``. + """ + try: + import ray.data + except ImportError as e: + raise ImportError( + "Ray is required to call to_ray_dataset(). " + "Install it with: pip install 'feast[ray]'" + ) from e + return ray.data.from_arrow(self.to_arrow()) + def to_sql(self) -> str: """ Return RetrievalJob generated SQL statement if applicable. @@ -303,6 +451,16 @@ class OfflineStore(ABC): the SnowflakeOfflineStore can handle SnowflakeSources but not FileSources. """ + supports_filter_by_created_timestamp: bool = False + """Whether get_historical_features supports the filter_by_created_timestamp flag.""" + + def ensure_filter_by_created_timestamp_supported(self) -> None: + """Raises NotImplementedError if this store does not support filter_by_created_timestamp.""" + if not self.supports_filter_by_created_timestamp: + raise NotImplementedError( + f"filter_by_created_timestamp is not supported by {type(self).__name__}" + ) + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -365,6 +523,10 @@ def get_historical_features( Keyword Args: start_date: Start date for the timestamp range when retrieving features without entity_df. end_date: End date for the timestamp range when retrieving features without entity_df. By default, the current time is used. + filter_by_created_timestamp: If True, only feature values whose created timestamp (the + ``created_timestamp_column`` of the batch source) is at or before the entity row's event + timestamp are considered. Only passed through when a store declares + ``supports_filter_by_created_timestamp``. Returns: A RetrievalJob that can be executed to get the features. @@ -474,3 +636,137 @@ def get_table_column_names_and_types_from_data_source( data_source: DataSource object """ return data_source.get_table_column_names_and_types(config=config) + + @staticmethod + def compute_monitoring_metrics( + config: RepoConfig, + data_source: DataSource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, + ) -> List[Dict[str, Any]]: + """ + Compute monitoring metrics (stats, percentiles, histograms) directly + in the offline store using its native compute engine. + + Backends that don't support this should leave it unimplemented; + the monitoring service will fall back to Python-based computation. + + Args: + config: The config for the current feature store. + data_source: The data source to compute metrics from. + feature_columns: List of (feature_name, feature_type) where + feature_type is "numeric" or "categorical". + timestamp_field: Column used for time-range filtering. + start_date: Start of the time range. + end_date: End of the time range. + histogram_bins: Number of bins for numeric histograms. + top_n: Number of top values for categorical histograms. + + Returns: + A list of metric dicts, one per feature, matching the format + produced by MetricsCalculator.compute_all. + """ + raise NotImplementedError + + @staticmethod + def get_monitoring_max_timestamp( + config: RepoConfig, + data_source: DataSource, + timestamp_field: str, + ) -> Optional[datetime]: + """ + Return the maximum event timestamp from the data source. + + Used by the monitoring service to determine date ranges for + auto-compute. Backends that don't support this should leave it + unimplemented; the caller will fall back to a full-table scan. + + Args: + config: The config for the current feature store. + data_source: The data source to query. + timestamp_field: The timestamp column name. + + Returns: + The maximum timestamp, or None if no data exists. + """ + raise NotImplementedError + + # ------------------------------------------------------------------ # + # Monitoring metrics storage (native) + # ------------------------------------------------------------------ # + + MONITORING_VALID_GRANULARITIES = ( + "daily", + "weekly", + "biweekly", + "monthly", + "quarterly", + ) + + @staticmethod + def ensure_monitoring_tables(config: RepoConfig) -> None: + """Create the monitoring metrics tables if they do not exist. + + Backends that don't support native monitoring storage should + leave this unimplemented; the monitoring service will raise an + error indicating the backend lacks storage support. + """ + raise NotImplementedError + + @staticmethod + def save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], + ) -> None: + """Persist monitoring metrics (upsert semantics). + + Args: + config: The config for the current feature store. + metric_type: One of "feature", "feature_view", "feature_service". + metrics: List of metric dicts to upsert. + """ + raise NotImplementedError + + @staticmethod + def query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + """Read monitoring metrics with optional filtering. + + Args: + config: The config for the current feature store. + project: Feast project name. + metric_type: One of "feature", "feature_view", "feature_service". + filters: Column-value pairs for WHERE clauses. + start_date: Inclusive lower bound on metric_date. + end_date: Inclusive upper bound on metric_date. + + Returns: + List of metric dicts ordered by metric_date ascending. + """ + raise NotImplementedError + + @staticmethod + def clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> None: + """Set is_baseline=FALSE for matching feature metric rows. + + Used to ensure only one baseline exists per feature before + writing a new baseline. + """ + raise NotImplementedError diff --git a/sdk/python/feast/infra/offline_stores/offline_utils.py b/sdk/python/feast/infra/offline_stores/offline_utils.py index abd7ad4fe35..7ccaf965c9c 100644 --- a/sdk/python/feast/infra/offline_stores/offline_utils.py +++ b/sdk/python/feast/infra/offline_stores/offline_utils.py @@ -1,3 +1,4 @@ +import logging import uuid from dataclasses import asdict, dataclass from datetime import datetime, timedelta, timezone @@ -21,6 +22,7 @@ from feast.repo_config import RepoConfig from feast.type_map import feast_value_type_to_pa from feast.utils import _get_requested_feature_views_to_features_dict, to_naive_utc +from feast.value_type import ValueType DEFAULT_ENTITY_DF_EVENT_TIMESTAMP_COL = "event_timestamp" @@ -96,6 +98,7 @@ class FeatureViewQueryContext: date_partition_column: Optional[ str ] # this attribute is added because partition pruning affects Athena's query performance. + timestamp_field_type: Optional[str] def get_feature_view_query_context( @@ -118,6 +121,10 @@ def get_feature_view_query_context( query_context = [] for feature_view, features in feature_views_to_feature_map.items(): + if feature_view.batch_source is None: + raise ValueError( + f"Feature view '{feature_view.name}' has no batch_source and cannot be queried." + ) reverse_field_mapping = { v: k for k, v in feature_view.batch_source.field_mapping.items() } @@ -154,6 +161,10 @@ def get_feature_view_query_context( feature_view.batch_source.date_partition_column, ) + timestamp_field_type = getattr( + feature_view.batch_source, "timestamp_field_type", "" + ) + max_event_timestamp = to_naive_utc(entity_df_timestamp_range[1]).isoformat() min_event_timestamp = None if feature_view.ttl: @@ -175,6 +186,7 @@ def get_feature_view_query_context( min_event_timestamp=min_event_timestamp, max_event_timestamp=max_event_timestamp, date_partition_column=date_partition_column, + timestamp_field_type=timestamp_field_type or None, ) query_context.append(context) @@ -188,6 +200,7 @@ def build_point_in_time_query( entity_df_columns: KeysView[str], query_template: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, ) -> str: """Build point-in-time query between each feature view table and the entity dataframe for Bigquery and Redshift""" env = Environment(loader=BaseLoader()) @@ -216,6 +229,7 @@ def build_point_in_time_query( ), "featureviews": [asdict(context) for context in feature_view_query_contexts], "full_feature_names": full_feature_names, + "filter_by_created_timestamp": filter_by_created_timestamp, "final_output_feature_names": final_output_feature_names, } @@ -237,6 +251,37 @@ def get_offline_store_from_config(offline_store_config: Any) -> OfflineStore: return offline_store_class() +_PA_BASIC_TYPES = { + "int32": pa.int32(), + "int64": pa.int64(), + "double": pa.float64(), + "float": pa.float32(), + "string": pa.string(), + "binary": pa.binary(), + "bool": pa.bool_(), + "large_string": pa.large_string(), + "null": pa.null(), +} + + +def _parse_pa_type_str(pa_type_str: str) -> pa.DataType: + """Parse a PyArrow type string to preserve inner element types for nested lists.""" + pa_type_str = pa_type_str.strip() + if pa_type_str.startswith("list"): + inner = pa_type_str[len("list Tuple[pa.Schema, List[str]]: @@ -246,20 +291,48 @@ def get_pyarrow_schema_from_batch_source( pa_schema = [] column_names = [] for column_name, column_type in column_names_and_types: - pa_schema.append( - ( - column_name, - feast_value_type_to_pa( - batch_source.source_datatype_to_feast_value_type()(column_type), - timestamp_unit=timestamp_unit, - ), - ) - ) + value_type = batch_source.source_datatype_to_feast_value_type()(column_type) + if value_type in (ValueType.VALUE_LIST, ValueType.VALUE_SET): + pa_type = _parse_pa_type_str(column_type) + else: + pa_type = feast_value_type_to_pa(value_type, timestamp_unit=timestamp_unit) + pa_schema.append((column_name, pa_type)) column_names.append(column_name) return pa.schema(pa_schema), column_names +def cast_arrow_table_to_schema(table: pa.Table, pa_schema: pa.Schema) -> pa.Table: + """Cast a PyArrow table to match the target schema, handling struct/map → string. + + PyArrow cannot natively cast struct or map columns to string. When a + SQL-based offline store (BigQuery, Snowflake, Redshift) stores complex + Feast types (Map, Struct) as VARCHAR/STRING, the target schema will have + string fields while the input table may have struct/map fields (e.g. when + the caller provides Python dicts). This function serialises those columns + to JSON strings so the subsequent cast succeeds. + """ + import json as _json + + for i, field in enumerate(table.schema): + target_type = pa_schema.field(field.name).type + is_complex_source = pa.types.is_struct(field.type) or pa.types.is_map( + field.type + ) + is_string_target = pa.types.is_string(target_type) or pa.types.is_large_string( + target_type + ) + if is_complex_source and is_string_target: + col = table.column(i) + json_arr = pa.array( + [_json.dumps(v.as_py()) if v.is_valid else None for v in col], + type=target_type, + ) + table = table.set_column(i, field.name, json_arr) + + return table.cast(pa_schema) + + def enclose_in_backticks(value): # Check if the input is a list if isinstance(value, list): @@ -275,7 +348,7 @@ def get_timestamp_filter_sql( date_partition_column: Optional[str] = None, tz: Optional[timezone] = None, cast_style: Literal[ - "timestamp", "timestamp_func", "timestamptz", "raw" + "timestamp", "timestamp_func", "timestamptz", "raw", "date_func" ] = "timestamp", date_time_separator: str = "T", quote_fields: bool = True, @@ -290,10 +363,11 @@ def get_timestamp_filter_sql( date_partition_column: optional partition column (for pruning) tz: optional timezone for datetime inputs cast_style: one of: - - "timestamp": TIMESTAMP '...' → Common Sql engine Snowflake, Redshift etc. + - "timestamp": TIMESTAMP '...' → Common Sql engine Snowflake, Redshift etc. - "timestamp_func": TIMESTAMP('...') → BigQuery, Couchbase etc. - "timestamptz": '...'::timestamptz → PostgreSQL - "raw": '...' → no cast, string only + - "date_func": DATE('...') → BigQuery DATE columns date_time_separator: separator for datetime strings (default is "T") (e.g. "2023-10-01T00:00:00" or "2023-10-01 00:00:00") quote_fields: whether to quote the timestamp and partition column names @@ -319,6 +393,9 @@ def format_casted_ts(val: Union[str, datetime]) -> str: return f"TIMESTAMP '{val_str}'" elif cast_style == "timestamp_func": return f"TIMESTAMP('{val_str}')" + elif cast_style == "date_func": + date_str = val_str[:10] if len(val_str) >= 10 else val_str + return f"DATE('{date_str}')" elif cast_style == "timestamptz": return f"'{val_str}'::{cast_style}" else: @@ -354,3 +431,12 @@ def format_date(val: Union[str, datetime]) -> str: filters.append(f"{dp_field} <= '{format_date(end_date)}'") return " AND ".join(filters) if filters else "" + + +def gather_all_entities(fv_query_contexts: List[FeatureViewQueryContext]): + all_entities: List[str] = [] + for ctx in fv_query_contexts: + for e in ctx.entities: + if e not in all_entities: + all_entities.append(e) + return all_entities diff --git a/sdk/python/feast/infra/offline_stores/redshift.py b/sdk/python/feast/infra/offline_stores/redshift.py index 4ed8e6309c4..1717cbaee79 100644 --- a/sdk/python/feast/infra/offline_stores/redshift.py +++ b/sdk/python/feast/infra/offline_stores/redshift.py @@ -1,6 +1,7 @@ import contextlib +import json import uuid -from datetime import datetime, timezone +from datetime import date, datetime, timezone from pathlib import Path from typing import ( Any, @@ -40,6 +41,17 @@ ) from feast.infra.registry.base_registry import BaseRegistry from feast.infra.utils import aws_utils +from feast.monitoring.monitoring_utils import ( + MON_TABLE_FEATURE, + MON_TABLE_FEATURE_SERVICE, + MON_TABLE_FEATURE_VIEW, + MON_TABLE_JOB, + empty_categorical_metric, + empty_numeric_metric, + monitoring_table_meta, + normalize_monitoring_row, + opt_float, +) from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage @@ -93,6 +105,8 @@ def require_cluster_and_user_or_workgroup(self): class RedshiftOfflineStore(OfflineStore): + supports_filter_by_created_timestamp = True + @staticmethod def pull_latest_from_table_or_query( config: RepoConfig, @@ -208,6 +222,7 @@ def get_historical_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + filter_by_created_timestamp: bool = False, ) -> RetrievalJob: assert isinstance(config.offline_store, RedshiftOfflineStoreConfig) for fv in feature_views: @@ -266,6 +281,7 @@ def query_generator() -> Iterator[str]: entity_df_columns=entity_schema.keys(), query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, full_feature_names=full_feature_names, + filter_by_created_timestamp=filter_by_created_timestamp, ) try: @@ -353,7 +369,7 @@ def offline_write_batch( ) if table.schema != pa_schema: - table = table.cast(pa_schema) + table = offline_utils.cast_arrow_table_to_schema(table, pa_schema) redshift_options = feature_view.batch_source.redshift_options redshift_client = aws_utils.get_redshift_data_client( @@ -378,6 +394,584 @@ def offline_write_batch( fail_if_exists=False, ) + @staticmethod + def compute_monitoring_metrics( + config: RepoConfig, + data_source: DataSource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, RedshiftOfflineStoreConfig) + assert isinstance(data_source, RedshiftSource) + + from_expression = data_source.get_table_query_string() + ts_filter = get_timestamp_filter_sql( + start_date, + end_date, + timestamp_field, + tz=timezone.utc, + ) + ts_clause = ts_filter if ts_filter else "1=1" + + numeric_features = [n for n, t in feature_columns if t == "numeric"] + categorical_features = [n for n, t in feature_columns if t == "categorical"] + results: List[Dict[str, Any]] = [] + + if numeric_features: + results.extend( + _redshift_sql_numeric_stats( + config, + from_expression, + numeric_features, + ts_clause, + histogram_bins, + ) + ) + + for col_name in categorical_features: + results.append( + _redshift_sql_categorical_stats( + config, from_expression, col_name, ts_clause, top_n + ) + ) + + return results + + @staticmethod + def get_monitoring_max_timestamp( + config: RepoConfig, + data_source: DataSource, + timestamp_field: str, + ) -> Optional[datetime]: + assert isinstance(config.offline_store, RedshiftOfflineStoreConfig) + assert isinstance(data_source, RedshiftSource) + + from_expression = data_source.get_table_query_string() + q_ts = f'"{timestamp_field}"' + sql = f"SELECT MAX({q_ts}) AS max_ts FROM {from_expression} AS _src" + rows = _redshift_execute_fetch_rows(config, sql) + if not rows or not rows[0]: + return None + val = _redshift_field_value(rows[0][0]) + if val is None: + return None + if isinstance(val, datetime): + return val if val.tzinfo else val.replace(tzinfo=timezone.utc) + return parser.parse(str(val)) + + @staticmethod + def ensure_monitoring_tables(config: RepoConfig) -> None: + assert isinstance(config.offline_store, RedshiftOfflineStoreConfig) + for stmt in _REDSHIFT_MONITORING_DDL_STATEMENTS: + _redshift_execute_statement(config, stmt) + + @staticmethod + def save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], + ) -> None: + if not metrics: + return + assert isinstance(config.offline_store, RedshiftOfflineStoreConfig) + table, columns, pk_columns = monitoring_table_meta(metric_type) + for row in metrics: + _redshift_merge_metric_row(config, table, columns, pk_columns, row) + + @staticmethod + def query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, RedshiftOfflineStoreConfig) + _, columns, _ = monitoring_table_meta(metric_type) + return _redshift_mon_query( + config, metric_type, columns, project, filters, start_date, end_date + ) + + @staticmethod + def clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> None: + assert isinstance(config.offline_store, RedshiftOfflineStoreConfig) + parts = [ + f"project_id = {_redshift_sql_literal(project)}", + "is_baseline = TRUE", + ] + if feature_view_name is not None: + parts.append( + f"feature_view_name = {_redshift_sql_literal(feature_view_name)}" + ) + if feature_name is not None: + parts.append(f"feature_name = {_redshift_sql_literal(feature_name)}") + if data_source_type is not None: + parts.append( + f"data_source_type = {_redshift_sql_literal(data_source_type)}" + ) + where_sql = " AND ".join(parts) + sql = f"UPDATE {MON_TABLE_FEATURE} SET is_baseline = FALSE WHERE {where_sql}" + _redshift_execute_statement(config, sql) + + +_REDSHIFT_MONITORING_DDL_STATEMENTS = [ + f""" +CREATE TABLE IF NOT EXISTS {MON_TABLE_FEATURE} ( + project_id VARCHAR(255) NOT NULL, + feature_view_name VARCHAR(255) NOT NULL, + feature_name VARCHAR(255) NOT NULL, + metric_date DATE NOT NULL, + granularity VARCHAR(20) NOT NULL DEFAULT 'daily', + data_source_type VARCHAR(50) NOT NULL DEFAULT 'batch', + computed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_baseline BOOLEAN NOT NULL DEFAULT FALSE, + feature_type VARCHAR(50) NOT NULL, + row_count BIGINT, + null_count BIGINT, + null_rate DOUBLE PRECISION, + mean DOUBLE PRECISION, + stddev DOUBLE PRECISION, + min_val DOUBLE PRECISION, + max_val DOUBLE PRECISION, + p50 DOUBLE PRECISION, + p75 DOUBLE PRECISION, + p90 DOUBLE PRECISION, + p95 DOUBLE PRECISION, + p99 DOUBLE PRECISION, + histogram VARCHAR(65535), + PRIMARY KEY (project_id, feature_view_name, feature_name, + metric_date, granularity, data_source_type) +); +""", + f""" +CREATE TABLE IF NOT EXISTS {MON_TABLE_FEATURE_VIEW} ( + project_id VARCHAR(255) NOT NULL, + feature_view_name VARCHAR(255) NOT NULL, + metric_date DATE NOT NULL, + granularity VARCHAR(20) NOT NULL DEFAULT 'daily', + data_source_type VARCHAR(50) NOT NULL DEFAULT 'batch', + computed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_baseline BOOLEAN NOT NULL DEFAULT FALSE, + total_row_count BIGINT, + total_features INTEGER, + features_with_nulls INTEGER, + avg_null_rate DOUBLE PRECISION, + max_null_rate DOUBLE PRECISION, + PRIMARY KEY (project_id, feature_view_name, metric_date, + granularity, data_source_type) +); +""", + f""" +CREATE TABLE IF NOT EXISTS {MON_TABLE_FEATURE_SERVICE} ( + project_id VARCHAR(255) NOT NULL, + feature_service_name VARCHAR(255) NOT NULL, + metric_date DATE NOT NULL, + granularity VARCHAR(20) NOT NULL DEFAULT 'daily', + data_source_type VARCHAR(50) NOT NULL DEFAULT 'batch', + computed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + is_baseline BOOLEAN NOT NULL DEFAULT FALSE, + total_feature_views INTEGER, + total_features INTEGER, + avg_null_rate DOUBLE PRECISION, + max_null_rate DOUBLE PRECISION, + PRIMARY KEY (project_id, feature_service_name, metric_date, + granularity, data_source_type) +); +""", + f""" +CREATE TABLE IF NOT EXISTS {MON_TABLE_JOB} ( + job_id VARCHAR(36) NOT NULL, + project_id VARCHAR(255) NOT NULL, + feature_view_name VARCHAR(255), + job_type VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + parameters VARCHAR(65535), + metric_date DATE NOT NULL, + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + error_message VARCHAR(65535), + result_summary VARCHAR(65535), + PRIMARY KEY (job_id) +); +""", +] + + +def _redshift_execute_statement(config: RepoConfig, sql: str) -> str: + client = aws_utils.get_redshift_data_client(config.offline_store.region) + return aws_utils.execute_redshift_statement( + client, + config.offline_store.cluster_id, + config.offline_store.workgroup, + config.offline_store.database, + config.offline_store.user, + sql, + ) + + +def _redshift_get_statement_pages( + client: Any, statement_id: str +) -> Tuple[List[Dict[str, Any]], List[List[Dict[str, Any]]]]: + column_metadata: List[Dict[str, Any]] = [] + all_records: List[List[Dict[str, Any]]] = [] + next_token: Optional[str] = None + while True: + kwargs: Dict[str, Any] = {"Id": statement_id} + if next_token: + kwargs["NextToken"] = next_token + resp = client.get_statement_result(**kwargs) + if not column_metadata: + column_metadata = resp.get("ColumnMetadata", []) + all_records.extend(resp.get("Records", [])) + next_token = resp.get("NextToken") + if not next_token: + break + return column_metadata, all_records + + +def _redshift_execute_fetch_rows( + config: RepoConfig, sql: str +) -> List[List[Dict[str, Any]]]: + client = aws_utils.get_redshift_data_client(config.offline_store.region) + sid = aws_utils.execute_redshift_statement( + client, + config.offline_store.cluster_id, + config.offline_store.workgroup, + config.offline_store.database, + config.offline_store.user, + sql, + ) + _, records = _redshift_get_statement_pages(client, sid) + return records + + +def _redshift_field_value(field: Dict[str, Any]) -> Any: + if field.get("isNull"): + return None + if "stringValue" in field: + return field["stringValue"] + if "longValue" in field: + return field["longValue"] + if "doubleValue" in field: + return field["doubleValue"] + if "booleanValue" in field: + return field["booleanValue"] + return None + + +def _redshift_sql_literal(val: Any) -> str: + if val is None: + return "NULL" + if isinstance(val, bool): + return "TRUE" if val else "FALSE" + if isinstance(val, (int, float)) and not isinstance(val, bool): + return str(val) + if isinstance(val, date) and not isinstance(val, datetime): + return f"DATE '{val.isoformat()}'" + if isinstance(val, datetime): + s = val.isoformat(sep=" ", timespec="seconds") + return f"TIMESTAMP '{s}'" + s = str(val).replace("'", "''") + return f"'{s}'" + + +def _redshift_merge_metric_row( + config: RepoConfig, + table: str, + columns: List[str], + pk_columns: List[str], + row: Dict[str, Any], +) -> None: + non_pk = [c for c in columns if c not in pk_columns] + client = aws_utils.get_redshift_data_client(config.offline_store.region) + + select_parts = ", ".join( + f"{_redshift_sql_literal_for_column(c, row.get(c))} AS {c}" for c in columns + ) + on_clause = " AND ".join(f"t.{c} = s.{c}" for c in pk_columns) + update_set = ", ".join(f"{c} = s.{c}" for c in non_pk) + insert_cols = ", ".join(columns) + insert_vals = ", ".join(f"s.{c}" for c in columns) + + merge_sql = f""" +MERGE INTO {table} AS t +USING (SELECT {select_parts}) AS s +ON {on_clause} +WHEN MATCHED THEN UPDATE SET {update_set} +WHEN NOT MATCHED THEN INSERT ({insert_cols}) VALUES ({insert_vals}) +""".strip() + aws_utils.execute_redshift_statement( + client, + config.offline_store.cluster_id, + config.offline_store.workgroup, + config.offline_store.database, + config.offline_store.user, + merge_sql, + ) + + +def _redshift_sql_literal_for_column(column: str, val: Any) -> str: + if val is None: + return "NULL" + if column == "histogram" and val is not None: + dumped = json.dumps(val).replace("'", "''") + return f"'{dumped}'" + return _redshift_sql_literal(val) + + +def _redshift_mon_query( + config: RepoConfig, + metric_type: str, + columns: List[str], + project: str, + filters: Optional[Dict[str, Any]], + start_date: Optional[date], + end_date: Optional[date], +) -> List[Dict[str, Any]]: + table, _, _ = monitoring_table_meta(metric_type) + conditions: list = [] + if project: + conditions.append(f"project_id = {_redshift_sql_literal(project)}") + if filters: + for key, value in filters.items(): + if value is not None: + conditions.append(f'"{key}" = {_redshift_sql_literal(value)}') + if start_date: + conditions.append(f"metric_date >= DATE '{start_date.isoformat()}'") + if end_date: + conditions.append(f"metric_date <= DATE '{end_date.isoformat()}'") + where_sql = " AND ".join(conditions) if conditions else "TRUE" + col_sql = ", ".join(f'"{c}"' for c in columns) + order_col = "metric_date" if "metric_date" in columns else "job_id" + sql = ( + f'SELECT {col_sql} FROM "{table}" WHERE {where_sql} ORDER BY "{order_col}" ASC' + ) + + client = aws_utils.get_redshift_data_client(config.offline_store.region) + sid = aws_utils.execute_redshift_statement( + client, + config.offline_store.cluster_id, + config.offline_store.workgroup, + config.offline_store.database, + config.offline_store.user, + sql, + ) + meta, rows = _redshift_get_statement_pages(client, sid) + col_names = [c["name"] for c in meta] + out: List[Dict[str, Any]] = [] + for rec in rows: + record = {col_names[i]: _redshift_field_value(rec[i]) for i in range(len(rec))} + out.append(normalize_monitoring_row(record)) + return out + + +def _redshift_sql_numeric_stats( + config: RepoConfig, + from_expression: str, + feature_names: List[str], + ts_clause: str, + histogram_bins: int, +) -> List[Dict[str, Any]]: + select_parts = ["COUNT(*)"] + for col in feature_names: + q = f'"{col}"' + c = f"CAST({q} AS DOUBLE PRECISION)" + select_parts.extend( + [ + f"COUNT({q})", + f"AVG({c})", + f"STDDEV_SAMP({c})", + f"MIN({c})", + f"MAX({c})", + f"PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY {c})", + ] + ) + + query = ( + f"SELECT {', '.join(select_parts)} " + f"FROM {from_expression} AS _src WHERE {ts_clause}" + ) + rows = _redshift_execute_fetch_rows(config, query) + if not rows or not rows[0]: + return [empty_numeric_metric(n) for n in feature_names] + + row = rows[0] + row_count = int(_redshift_field_value(row[0]) or 0) + results: List[Dict[str, Any]] = [] + + for i, col in enumerate(feature_names): + base = 1 + i * 10 + non_null = int(_redshift_field_value(row[base]) or 0) + null_count = row_count - non_null + + min_val = opt_float(_redshift_field_value(row[base + 3])) + max_val = opt_float(_redshift_field_value(row[base + 4])) + + result: Dict[str, Any] = { + "feature_name": col, + "feature_type": "numeric", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": opt_float(_redshift_field_value(row[base + 1])), + "stddev": opt_float(_redshift_field_value(row[base + 2])), + "min_val": min_val, + "max_val": max_val, + "p50": opt_float(_redshift_field_value(row[base + 5])), + "p75": opt_float(_redshift_field_value(row[base + 6])), + "p90": opt_float(_redshift_field_value(row[base + 7])), + "p95": opt_float(_redshift_field_value(row[base + 8])), + "p99": opt_float(_redshift_field_value(row[base + 9])), + "histogram": None, + } + + if min_val is not None and max_val is not None and non_null > 0: + result["histogram"] = _redshift_sql_numeric_histogram( + config, + from_expression, + col, + ts_clause, + histogram_bins, + min_val, + max_val, + ) + + results.append(result) + + return results + + +def _redshift_sql_numeric_histogram( + config: RepoConfig, + from_expression: str, + col_name: str, + ts_clause: str, + bins: int, + min_val: float, + max_val: float, +) -> Dict[str, Any]: + q_col = f'"{col_name}"' + + if min_val == max_val: + sql = ( + f"SELECT COUNT(*) FROM {from_expression} AS _src " + f"WHERE {q_col} IS NOT NULL AND {ts_clause}" + ) + r = _redshift_execute_fetch_rows(config, sql) + cnt = int(_redshift_field_value(r[0][0]) or 0) if r and r[0] else 0 + return {"bins": [min_val, max_val], "counts": [cnt], "bin_width": 0.0} + + bin_width = (max_val - min_val) / bins + cast_col = f"CAST({q_col} AS DOUBLE PRECISION)" + + inner = ( + f"CASE WHEN {min_val} = {max_val} THEN 1 " + f"ELSE LEAST(GREATEST(FLOOR(({cast_col} - {min_val}) / {bin_width}) + 1, 1), {bins}) " + f"END AS bucket" + ) + + query = ( + f"SELECT bucket, COUNT(*) AS cnt FROM (" + f" SELECT {inner} " + f" FROM {from_expression} AS _src " + f" WHERE {q_col} IS NOT NULL AND {ts_clause}" + f") AS _b WHERE bucket IS NOT NULL " + f"GROUP BY bucket ORDER BY bucket" + ) + hrows = _redshift_execute_fetch_rows(config, query) + counts = [0] * bins + for hr in hrows: + bucket = int(_redshift_field_value(hr[0]) or 0) + cnt = int(_redshift_field_value(hr[1]) or 0) + if 1 <= bucket <= bins: + counts[bucket - 1] = cnt + + bin_edges = [min_val + i * bin_width for i in range(bins + 1)] + return { + "bins": [float(b) for b in bin_edges], + "counts": counts, + "bin_width": float(bin_width), + } + + +def _redshift_sql_categorical_stats( + config: RepoConfig, + from_expression: str, + col_name: str, + ts_clause: str, + top_n: int, +) -> Dict[str, Any]: + q_col = f'"{col_name}"' + + query = ( + f"WITH filtered AS (" + f" SELECT * FROM {from_expression} AS _src WHERE {ts_clause}" + f") " + f"SELECT " + f" (SELECT COUNT(*) FROM filtered) AS row_count, " + f" (SELECT COUNT(*) - COUNT({q_col}) FROM filtered) AS null_count, " + f" (SELECT COUNT(DISTINCT {q_col}) FROM filtered " + f" WHERE {q_col} IS NOT NULL) AS unique_count, " + f" CAST({q_col} AS VARCHAR(65535)) AS value, COUNT(*) AS cnt " + f"FROM filtered WHERE {q_col} IS NOT NULL " + f"GROUP BY {q_col} ORDER BY cnt DESC LIMIT {int(top_n)}" + ) + + rows = _redshift_execute_fetch_rows(config, query) + if not rows: + return empty_categorical_metric(col_name) + + row_count = int(_redshift_field_value(rows[0][0]) or 0) + null_count = int(_redshift_field_value(rows[0][1]) or 0) + unique_count = int(_redshift_field_value(rows[0][2]) or 0) + + top_entries = [ + { + "value": _redshift_field_value(r[3]), + "count": int(_redshift_field_value(r[4]) or 0), + } + for r in rows + ] + top_total = sum(e["count"] for e in top_entries) + other_count = (row_count - null_count) - top_total + + return { + "feature_name": col_name, + "feature_type": "categorical", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": { + "values": top_entries, + "other_count": max(other_count, 0), + "unique_count": unique_count, + }, + } + class RedshiftRetrievalJob(RetrievalJob): def __init__( @@ -721,6 +1315,10 @@ def _get_entity_df_event_timestamp_range( AND subquery.event_timestamp >= entity_dataframe.entity_timestamp - {{ featureview.ttl }} * interval '1' second {% endif %} + {% if filter_by_created_timestamp and featureview.created_timestamp_column %} + AND subquery.created_timestamp <= entity_dataframe.entity_timestamp + {% endif %} + {% for entity in featureview.entities %} AND subquery.{{ entity }} = entity_dataframe.{{ entity }} {% endfor %} diff --git a/sdk/python/feast/infra/offline_stores/redshift_source.py b/sdk/python/feast/infra/offline_stores/redshift_source.py index 752d3b12cf3..6c40038ceb0 100644 --- a/sdk/python/feast/infra/offline_stores/redshift_source.py +++ b/sdk/python/feast/infra/offline_stores/redshift_source.py @@ -123,9 +123,7 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, RedshiftSource): - raise TypeError( - "Comparisons should only involve RedshiftSource class objects." - ) + return False return ( super().__eq__(other) diff --git a/sdk/python/feast/infra/offline_stores/remote.py b/sdk/python/feast/infra/offline_stores/remote.py index abe75ca57e5..a32774ad085 100644 --- a/sdk/python/feast/infra/offline_stores/remote.py +++ b/sdk/python/feast/infra/offline_stores/remote.py @@ -12,7 +12,7 @@ import pyarrow.parquet from pyarrow import Schema from pyarrow._flight import FlightCallOptions, FlightDescriptor, Ticket -from pydantic import StrictInt, StrictStr +from pydantic import Field, StrictInt, StrictStr from feast import OnDemandFeatureView from feast.arrow_error_handler import arrow_client_error_handling_decorator @@ -42,6 +42,10 @@ class FeastFlightClient(fl.FlightClient): + def __init__(self, *args, connection_retries: int = 3, **kwargs): + super().__init__(*args, **kwargs) + self._connection_retries = max(0, connection_retries) + @arrow_client_error_handling_decorator def get_flight_info( self, descriptor: FlightDescriptor, options: FlightCallOptions = None @@ -71,7 +75,12 @@ def list_actions(self, options: FlightCallOptions = None): def build_arrow_flight_client( - scheme: str, host: str, port, auth_config: AuthConfig, cert: str = "" + scheme: str, + host: str, + port, + auth_config: AuthConfig, + cert: str = "", + connection_retries: int = 3, ): arrow_scheme = "grpc+tcp" if scheme == "https": @@ -88,10 +97,17 @@ def build_arrow_flight_client( if auth_config.type != AuthType.NONE.value: middlewares = [FlightAuthInterceptorFactory(auth_config)] return FeastFlightClient( - f"{arrow_scheme}://{host}:{port}", middleware=middlewares, **kwargs + f"{arrow_scheme}://{host}:{port}", + middleware=middlewares, + connection_retries=connection_retries, + **kwargs, ) - return FeastFlightClient(f"{arrow_scheme}://{host}:{port}", **kwargs) + return FeastFlightClient( + f"{arrow_scheme}://{host}:{port}", + connection_retries=connection_retries, + **kwargs, + ) class RemoteOfflineStoreConfig(FeastConfigBaseModel): @@ -109,6 +125,9 @@ class RemoteOfflineStoreConfig(FeastConfigBaseModel): """ str: Path to the public certificate when the offline server starts in TLS(SSL) mode. This may be needed if the offline server started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`. If type is 'remote', then this configuration is needed to connect to remote offline server in TLS mode. """ + connection_retries: int = Field(default=3, ge=0) + """ int: Number of retries for transient Arrow Flight errors with exponential backoff (default 3). """ + class RemoteRetrievalJob(RetrievalJob): def __init__( @@ -207,6 +226,7 @@ def get_historical_features( port=config.offline_store.port, auth_config=config.auth_config, cert=config.offline_store.cert, + connection_retries=config.offline_store.connection_retries, ) feature_view_names = [fv.name for fv in feature_views] @@ -229,6 +249,10 @@ def get_historical_features( if end_date is not None: api_parameters["end_date"] = end_date.isoformat() + if isinstance(entity_df, str): + api_parameters["entity_df_sql"] = entity_df + entity_df = None + return RemoteRetrievalJob( client=client, api=OfflineStore.get_historical_features.__name__, @@ -257,6 +281,7 @@ def pull_all_from_table_or_query( port=config.offline_store.port, auth_config=config.auth_config, cert=config.offline_store.cert, + connection_retries=config.offline_store.connection_retries, ) api_parameters = { @@ -295,6 +320,7 @@ def pull_latest_from_table_or_query( config.offline_store.port, config.auth_config, cert=config.offline_store.cert, + connection_retries=config.offline_store.connection_retries, ) api_parameters = { @@ -334,6 +360,7 @@ def write_logged_features( config.offline_store.port, config.auth_config, config.offline_store.cert, + connection_retries=config.offline_store.connection_retries, ) api_parameters = { @@ -364,6 +391,7 @@ def offline_write_batch( config.offline_store.port, config.auth_config, config.offline_store.cert, + connection_retries=config.offline_store.connection_retries, ) feature_view_names = [feature_view.name] @@ -396,6 +424,7 @@ def validate_data_source( config.offline_store.port, config.auth_config, config.offline_store.cert, + connection_retries=config.offline_store.connection_retries, ) api_parameters = { @@ -421,6 +450,7 @@ def get_table_column_names_and_types_from_data_source( config.offline_store.port, config.auth_config, config.offline_store.cert, + connection_retries=config.offline_store.connection_retries, ) api_parameters = { @@ -443,7 +473,16 @@ def get_table_column_names_and_types_from_data_source( return zip(table.column("name").to_pylist(), table.column("type").to_pylist()) -def _create_retrieval_metadata(feature_refs: List[str], entity_df: pd.DataFrame): +def _create_retrieval_metadata( + feature_refs: List[str], entity_df: Optional[Union[pd.DataFrame, str]] = None +): + if entity_df is None or isinstance(entity_df, str): + return RetrievalMetadata( + features=feature_refs, + keys=[], + min_event_timestamp=None, + max_event_timestamp=None, + ) entity_schema = _get_entity_schema( entity_df=entity_df, ) diff --git a/sdk/python/feast/infra/offline_stores/snowflake.py b/sdk/python/feast/infra/offline_stores/snowflake.py index 59a4a8aeb1f..84b829617f8 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake.py +++ b/sdk/python/feast/infra/offline_stores/snowflake.py @@ -3,7 +3,8 @@ import os import uuid import warnings -from datetime import datetime, timezone +from datetime import date, datetime, timezone +from decimal import Decimal from functools import reduce from pathlib import Path from typing import ( @@ -50,6 +51,17 @@ write_pandas, write_parquet, ) +from feast.monitoring.monitoring_utils import ( + MON_TABLE_FEATURE, + MON_TABLE_FEATURE_SERVICE, + MON_TABLE_FEATURE_VIEW, + MON_TABLE_JOB, + empty_categorical_metric, + empty_numeric_metric, + monitoring_table_meta, + normalize_monitoring_row, + opt_float, +) from feast.repo_config import FeastConfigBaseModel, RepoConfig from feast.saved_dataset import SavedDatasetStorage from feast.types import ( @@ -185,7 +197,9 @@ def pull_latest_from_table_or_query( ) select_timestamps = list( map( - lambda field_name: f"TO_VARCHAR({field_name}, 'YYYY-MM-DD\"T\"HH24:MI:SS.FFTZH:TZM') AS {field_name}", + lambda field_name: ( + f"TO_VARCHAR({field_name}, 'YYYY-MM-DD\"T\"HH24:MI:SS.FFTZH:TZM') AS {field_name}" + ), timestamp_columns, ) ) @@ -285,6 +299,8 @@ def get_historical_features( project: str, full_feature_names: bool = False, ) -> RetrievalJob: + # supports_filter_by_created_timestamp stays False: the ASOF JOIN cannot + # express a created_timestamp cutoff. assert isinstance(config.offline_store, SnowflakeOfflineStoreConfig) for fv in feature_views: assert isinstance(fv.batch_source, SnowflakeSource) @@ -407,7 +423,7 @@ def offline_write_batch( ) if table.schema != pa_schema: - table = table.cast(pa_schema) + table = offline_utils.cast_arrow_table_to_schema(table, pa_schema) with GetSnowflakeConnection(config.offline_store) as conn: snowflake_conn = conn @@ -419,6 +435,252 @@ def offline_write_batch( auto_create_table=True, ) + @staticmethod + def compute_monitoring_metrics( + config: RepoConfig, + data_source: DataSource, + feature_columns: List[Tuple[str, str]], + timestamp_field: str, + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + histogram_bins: int = 20, + top_n: int = 10, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, SnowflakeOfflineStoreConfig) + assert isinstance(data_source, SnowflakeSource) + + from_expression = data_source.get_table_query_string() + from_expression = _qualify_snowflake_from_expression( + config, data_source, from_expression + ) + ts_filter = ( + get_timestamp_filter_sql( + start_date, end_date, timestamp_field, tz=timezone.utc + ) + or "1=1" + ) + + numeric_features = [n for n, t in feature_columns if t == "numeric"] + categorical_features = [n for n, t in feature_columns if t == "categorical"] + results: List[Dict[str, Any]] = [] + + with GetSnowflakeConnection(config.offline_store) as conn: + if numeric_features: + results.extend( + _snowflake_sql_numeric_stats( + conn, + from_expression, + numeric_features, + ts_filter, + histogram_bins, + ) + ) + + for col_name in categorical_features: + results.append( + _snowflake_sql_categorical_stats( + conn, from_expression, col_name, ts_filter, top_n + ) + ) + + return results + + @staticmethod + def get_monitoring_max_timestamp( + config: RepoConfig, + data_source: DataSource, + timestamp_field: str, + ) -> Optional[datetime]: + assert isinstance(config.offline_store, SnowflakeOfflineStoreConfig) + assert isinstance(data_source, SnowflakeSource) + + from_expression = data_source.get_table_query_string() + from_expression = _qualify_snowflake_from_expression( + config, data_source, from_expression + ) + + with GetSnowflakeConnection(config.offline_store) as conn: + cursor = execute_snowflake_statement( + conn, + f'SELECT MAX("{timestamp_field}") FROM {from_expression} AS _src', + ) + row = cursor.fetchone() + + if row is None or row[0] is None: + return None + val = row[0] + if isinstance(val, pd.Timestamp): + val = val.to_pydatetime() + if isinstance(val, datetime): + return val if val.tzinfo else val.replace(tzinfo=timezone.utc) + return datetime.combine(val, datetime.min.time(), tzinfo=timezone.utc) + + @staticmethod + def ensure_monitoring_tables(config: RepoConfig) -> None: + assert isinstance(config.offline_store, SnowflakeOfflineStoreConfig) + + fq_feature = _snowflake_monitoring_table_fqn(config, MON_TABLE_FEATURE) + fq_view = _snowflake_monitoring_table_fqn(config, MON_TABLE_FEATURE_VIEW) + fq_service = _snowflake_monitoring_table_fqn(config, MON_TABLE_FEATURE_SERVICE) + + ddl_feature = f""" + CREATE TABLE IF NOT EXISTS {fq_feature} ( + "project_id" VARCHAR(255) NOT NULL, + "feature_view_name" VARCHAR(255) NOT NULL, + "feature_name" VARCHAR(255) NOT NULL, + "metric_date" DATE NOT NULL, + "granularity" VARCHAR(20) NOT NULL DEFAULT 'daily', + "data_source_type" VARCHAR(50) NOT NULL DEFAULT 'batch', + "computed_at" TIMESTAMP_TZ NOT NULL DEFAULT CURRENT_TIMESTAMP(), + "is_baseline" BOOLEAN NOT NULL DEFAULT FALSE, + "feature_type" VARCHAR(50) NOT NULL, + "row_count" BIGINT, + "null_count" BIGINT, + "null_rate" DOUBLE, + "mean" DOUBLE, + "stddev" DOUBLE, + "min_val" DOUBLE, + "max_val" DOUBLE, + "p50" DOUBLE, + "p75" DOUBLE, + "p90" DOUBLE, + "p95" DOUBLE, + "p99" DOUBLE, + "histogram" VARIANT, + PRIMARY KEY ("project_id", "feature_view_name", "feature_name", + "metric_date", "granularity", "data_source_type") + ) + """ + ddl_view = f""" + CREATE TABLE IF NOT EXISTS {fq_view} ( + "project_id" VARCHAR(255) NOT NULL, + "feature_view_name" VARCHAR(255) NOT NULL, + "metric_date" DATE NOT NULL, + "granularity" VARCHAR(20) NOT NULL DEFAULT 'daily', + "data_source_type" VARCHAR(50) NOT NULL DEFAULT 'batch', + "computed_at" TIMESTAMP_TZ NOT NULL DEFAULT CURRENT_TIMESTAMP(), + "is_baseline" BOOLEAN NOT NULL DEFAULT FALSE, + "total_row_count" BIGINT, + "total_features" INTEGER, + "features_with_nulls" INTEGER, + "avg_null_rate" DOUBLE, + "max_null_rate" DOUBLE, + PRIMARY KEY ("project_id", "feature_view_name", "metric_date", + "granularity", "data_source_type") + ) + """ + ddl_service = f""" + CREATE TABLE IF NOT EXISTS {fq_service} ( + "project_id" VARCHAR(255) NOT NULL, + "feature_service_name" VARCHAR(255) NOT NULL, + "metric_date" DATE NOT NULL, + "granularity" VARCHAR(20) NOT NULL DEFAULT 'daily', + "data_source_type" VARCHAR(50) NOT NULL DEFAULT 'batch', + "computed_at" TIMESTAMP_TZ NOT NULL DEFAULT CURRENT_TIMESTAMP(), + "is_baseline" BOOLEAN NOT NULL DEFAULT FALSE, + "total_feature_views" INTEGER, + "total_features" INTEGER, + "avg_null_rate" DOUBLE, + "max_null_rate" DOUBLE, + PRIMARY KEY ("project_id", "feature_service_name", "metric_date", + "granularity", "data_source_type") + ) + """ + + fq_job = _snowflake_monitoring_table_fqn(config, MON_TABLE_JOB) + ddl_job = f""" + CREATE TABLE IF NOT EXISTS {fq_job} ( + "job_id" VARCHAR(36) NOT NULL, + "project_id" VARCHAR(255) NOT NULL, + "feature_view_name" VARCHAR(255), + "job_type" VARCHAR(50) NOT NULL, + "status" VARCHAR(20) NOT NULL DEFAULT 'pending', + "parameters" VARCHAR, + "metric_date" DATE NOT NULL, + "started_at" TIMESTAMP_TZ, + "completed_at" TIMESTAMP_TZ, + "error_message" VARCHAR, + "result_summary" VARCHAR, + PRIMARY KEY ("job_id") + ) + """ + + with GetSnowflakeConnection(config.offline_store) as conn: + execute_snowflake_statement(conn, ddl_feature) + execute_snowflake_statement(conn, ddl_view) + execute_snowflake_statement(conn, ddl_service) + execute_snowflake_statement(conn, ddl_job) + + @staticmethod + def save_monitoring_metrics( + config: RepoConfig, + metric_type: str, + metrics: List[Dict[str, Any]], + ) -> None: + if not metrics: + return + assert isinstance(config.offline_store, SnowflakeOfflineStoreConfig) + + table, columns, pk_columns = monitoring_table_meta(metric_type) + _snowflake_mon_merge_upsert( + config.offline_store, table, columns, pk_columns, metrics + ) + + @staticmethod + def query_monitoring_metrics( + config: RepoConfig, + project: str, + metric_type: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + assert isinstance(config.offline_store, SnowflakeOfflineStoreConfig) + + _, columns, _ = monitoring_table_meta(metric_type) + return _snowflake_mon_query( + config.offline_store, + metric_type, + columns, + project, + filters, + start_date, + end_date, + ) + + @staticmethod + def clear_monitoring_baseline( + config: RepoConfig, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> None: + assert isinstance(config.offline_store, SnowflakeOfflineStoreConfig) + + fq_table = _snowflake_monitoring_table_fqn(config, MON_TABLE_FEATURE) + conditions = [f'"project_id" = {_snowflake_sql_literal(project)}'] + if feature_view_name: + conditions.append( + f'"feature_view_name" = {_snowflake_sql_literal(feature_view_name)}' + ) + if feature_name: + conditions.append( + f'"feature_name" = {_snowflake_sql_literal(feature_name)}' + ) + if data_source_type: + conditions.append( + f'"data_source_type" = {_snowflake_sql_literal(data_source_type)}' + ) + conditions.append('"is_baseline" = TRUE') + + sql = f'UPDATE {fq_table} SET "is_baseline" = FALSE WHERE ' + " AND ".join( + conditions + ) + + with GetSnowflakeConnection(config.offline_store) as conn: + execute_snowflake_statement(conn, sql) + class SnowflakeRetrievalJob(RetrievalJob): def __init__( @@ -638,6 +900,335 @@ def _get_file_names_from_copy_into(self, cursor, native_export_path) -> List[str ] +# ------------------------------------------------------------------ # +# Snowflake monitoring SQL push-down & storage helpers +# ------------------------------------------------------------------ # + + +def _escape_snowflake_sql_string(value: str) -> str: + return value.replace("'", "''") + + +def _snowflake_sql_literal(val: Any) -> str: + if val is None: + return "NULL" + if isinstance(val, bool): + return "TRUE" if val else "FALSE" + if isinstance(val, (int, float)) and not isinstance(val, bool): + if isinstance(val, float) and (np.isnan(val) or np.isinf(val)): + return "NULL" + return str(val) + if isinstance(val, Decimal): + return str(val) + if isinstance(val, date) and not isinstance(val, datetime): + return f"DATE '{val.isoformat()}'" + if isinstance(val, datetime): + dt = val if val.tzinfo else val.replace(tzinfo=timezone.utc) + return f"TIMESTAMP_TZ '{dt.isoformat()}'" + if isinstance(val, str): + return f"'{_escape_snowflake_sql_string(val)}'" + return f"'{_escape_snowflake_sql_string(str(val))}'" + + +def _qualify_snowflake_from_expression( + config: RepoConfig, + data_source: SnowflakeSource, + from_expression: str, +) -> str: + if not data_source.database and not data_source.schema and data_source.table: + return ( + f'"{config.offline_store.database}"."{config.offline_store.schema_}".' + f"{from_expression}" + ) + if not data_source.database and data_source.schema and data_source.table: + return f'"{config.offline_store.database}".{from_expression}' + return from_expression + + +def _snowflake_monitoring_table_fqn( + config: RepoConfig, + table_name: str, +) -> str: + os = config.offline_store + assert isinstance(os, SnowflakeOfflineStoreConfig) + return f'"{os.database}"."{os.schema_}"."{table_name}"' + + +def _snowflake_sql_numeric_histogram( + conn: SnowflakeConnection, + from_expression: str, + col_name: str, + ts_filter: str, + bins: int, + min_val: float, + max_val: float, +) -> Dict[str, Any]: + q_col = f'"{col_name}"' + + if min_val == max_val: + cursor = execute_snowflake_statement( + conn, + f"SELECT COUNT(*) FROM {from_expression} AS _src " + f"WHERE {q_col} IS NOT NULL AND {ts_filter}", + ) + row = cursor.fetchone() + cnt = (row or (0,))[0] + return {"bins": [min_val, max_val], "counts": [cnt], "bin_width": 0.0} + + upper = max_val + (max_val - min_val) * 1e-10 + bin_width = (max_val - min_val) / bins + + query = ( + f"SELECT WIDTH_BUCKET(CAST({q_col} AS DOUBLE), {min_val}, {upper}, {bins}) " + f"AS bucket, COUNT(*) AS cnt " + f"FROM {from_expression} AS _src " + f"WHERE {q_col} IS NOT NULL AND {ts_filter} " + f"GROUP BY bucket ORDER BY bucket" + ) + + cursor = execute_snowflake_statement(conn, query) + rows = cursor.fetchall() + + counts = [0] * bins + for bucket, cnt in rows: + if bucket is not None and 1 <= int(bucket) <= bins: + counts[int(bucket) - 1] = cnt + + bin_edges = [min_val + i * bin_width for i in range(bins + 1)] + return { + "bins": [float(b) for b in bin_edges], + "counts": counts, + "bin_width": float(bin_width), + } + + +def _snowflake_sql_numeric_stats( + conn: SnowflakeConnection, + from_expression: str, + feature_names: List[str], + ts_filter: str, + histogram_bins: int, +) -> List[Dict[str, Any]]: + select_parts = ["COUNT(*)"] + for col in feature_names: + q = f'"{col}"' + c = f"CAST({q} AS DOUBLE)" + select_parts.extend( + [ + f"COUNT({q})", + f"AVG({c})", + f"STDDEV_SAMP({c})", + f"MIN({c})", + f"MAX({c})", + f"PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY {c})", + f"PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY {c})", + ] + ) + + query = ( + f"SELECT {', '.join(select_parts)} " + f"FROM {from_expression} AS _src WHERE {ts_filter}" + ) + + cursor = execute_snowflake_statement(conn, query) + row = cursor.fetchone() + + if row is None: + return [empty_numeric_metric(n) for n in feature_names] + + row_count = row[0] + results: List[Dict[str, Any]] = [] + + for i, col in enumerate(feature_names): + base = 1 + i * 10 + non_null = row[base] or 0 + null_count = row_count - non_null + + min_val = opt_float(row[base + 3]) + max_val = opt_float(row[base + 4]) + + result: Dict[str, Any] = { + "feature_name": col, + "feature_type": "numeric", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": opt_float(row[base + 1]), + "stddev": opt_float(row[base + 2]), + "min_val": min_val, + "max_val": max_val, + "p50": opt_float(row[base + 5]), + "p75": opt_float(row[base + 6]), + "p90": opt_float(row[base + 7]), + "p95": opt_float(row[base + 8]), + "p99": opt_float(row[base + 9]), + "histogram": None, + } + + if min_val is not None and max_val is not None and non_null > 0: + result["histogram"] = _snowflake_sql_numeric_histogram( + conn, + from_expression, + col, + ts_filter, + histogram_bins, + min_val, + max_val, + ) + + results.append(result) + + return results + + +def _snowflake_sql_categorical_stats( + conn: SnowflakeConnection, + from_expression: str, + col_name: str, + ts_filter: str, + top_n: int, +) -> Dict[str, Any]: + q_col = f'"{col_name}"' + + query = ( + f"WITH filtered AS (" + f" SELECT * FROM {from_expression} AS _src WHERE {ts_filter}" + f") " + f"SELECT " + f" (SELECT COUNT(*) FROM filtered) AS row_count, " + f" (SELECT COUNT(*) - COUNT({q_col}) FROM filtered) AS null_count, " + f" (SELECT COUNT(DISTINCT {q_col}) FROM filtered " + f" WHERE {q_col} IS NOT NULL) AS unique_count, " + f" TO_VARCHAR({q_col}) AS value, COUNT(*) AS cnt " + f"FROM filtered WHERE {q_col} IS NOT NULL " + f"GROUP BY {q_col} ORDER BY cnt DESC LIMIT {int(top_n)}" + ) + + cursor = execute_snowflake_statement(conn, query) + rows = cursor.fetchall() + + if not rows: + return empty_categorical_metric(col_name) + + row_count = rows[0][0] + null_count = rows[0][1] + unique_count = rows[0][2] + + top_entries = [{"value": r[3], "count": r[4]} for r in rows] + top_total = sum(e["count"] for e in top_entries) + other_count = (row_count - null_count) - top_total + + return { + "feature_name": col_name, + "feature_type": "categorical", + "row_count": row_count, + "null_count": null_count, + "null_rate": null_count / row_count if row_count > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": { + "values": top_entries, + "other_count": max(other_count, 0), + "unique_count": unique_count, + }, + } + + +def _snowflake_mon_merge_upsert( + offline_store: SnowflakeOfflineStoreConfig, + table: str, + columns: List[str], + pk_columns: List[str], + rows: List[Dict[str, Any]], +) -> None: + fq = f'"{offline_store.database}"."{offline_store.schema_}"."{table}"' + non_pk = [c for c in columns if c not in pk_columns] + + with GetSnowflakeConnection(offline_store) as conn: + for row in rows: + select_parts: List[str] = [] + for col in columns: + val = row.get(col) + if col == "histogram": + if val is not None: + json_str = json.dumps(val) + select_parts.append( + f'PARSE_JSON({_snowflake_sql_literal(json_str)}) AS "{col}"' + ) + else: + select_parts.append(f'NULL AS "{col}"') + else: + select_parts.append(f'{_snowflake_sql_literal(val)} AS "{col}"') + + using = ", ".join(select_parts) + on_parts = [f't."{pk}" = s."{pk}"' for pk in pk_columns] + update_parts = [f't."{c}" = s."{c}"' for c in non_pk] + insert_cols = ", ".join(f'"{c}"' for c in columns) + insert_vals = ", ".join(f's."{c}"' for c in columns) + + sql = ( + f"MERGE INTO {fq} AS t " + f"USING (SELECT {using}) AS s " + f"ON {' AND '.join(on_parts)} " + f"WHEN MATCHED THEN UPDATE SET {', '.join(update_parts)} " + f"WHEN NOT MATCHED THEN INSERT ({insert_cols}) VALUES ({insert_vals})" + ) + + execute_snowflake_statement(conn, sql) + + +def _snowflake_mon_query( + offline_store: SnowflakeOfflineStoreConfig, + metric_type: str, + columns: List[str], + project: str, + filters: Optional[Dict[str, Any]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, +) -> List[Dict[str, Any]]: + table, _, _ = monitoring_table_meta(metric_type) + fq = f'"{offline_store.database}"."{offline_store.schema_}"."{table}"' + + conditions: List[str] = [] + if project: + conditions.append(f'"project_id" = {_snowflake_sql_literal(project)}') + if filters: + for key, value in filters.items(): + if value is not None: + conditions.append(f'"{key}" = {_snowflake_sql_literal(value)}') + + if start_date: + conditions.append(f'"metric_date" >= {_snowflake_sql_literal(start_date)}') + if end_date: + conditions.append(f'"metric_date" <= {_snowflake_sql_literal(end_date)}') + + col_list = ", ".join(f'"{c}"' for c in columns) + where_clause = " AND ".join(conditions) if conditions else "TRUE" + order_col = "metric_date" if "metric_date" in columns else "job_id" + sql = f'SELECT {col_list} FROM {fq} WHERE {where_clause} ORDER BY "{order_col}" ASC' + + with GetSnowflakeConnection(offline_store) as conn: + cursor = execute_snowflake_statement(conn, sql) + rows = cursor.fetchall() + + results: List[Dict[str, Any]] = [] + for row in rows: + record = dict(zip(columns, row)) + results.append(normalize_monitoring_row(record)) + + return results + + def _get_entity_schema( entity_df: Union[pd.DataFrame, str], snowflake_conn: SnowflakeConnection, diff --git a/sdk/python/feast/infra/offline_stores/snowflake_source.py b/sdk/python/feast/infra/offline_stores/snowflake_source.py index c5a95dd8395..cec00b90513 100644 --- a/sdk/python/feast/infra/offline_stores/snowflake_source.py +++ b/sdk/python/feast/infra/offline_stores/snowflake_source.py @@ -132,9 +132,7 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, SnowflakeSource): - raise TypeError( - "Comparisons should only involve SnowflakeSource class objects." - ) + return False return ( super().__eq__(other) diff --git a/sdk/python/feast/infra/online_stores/aerospike_online_store/__init__.py b/sdk/python/feast/infra/online_stores/aerospike_online_store/__init__.py new file mode 100644 index 00000000000..099892626dd --- /dev/null +++ b/sdk/python/feast/infra/online_stores/aerospike_online_store/__init__.py @@ -0,0 +1,3 @@ +from .aerospike import AerospikeOnlineStore, AerospikeOnlineStoreConfig + +__all__ = ["AerospikeOnlineStore", "AerospikeOnlineStoreConfig"] diff --git a/sdk/python/feast/infra/online_stores/aerospike_online_store/aerospike.py b/sdk/python/feast/infra/online_stores/aerospike_online_store/aerospike.py new file mode 100644 index 00000000000..208c3b0684a --- /dev/null +++ b/sdk/python/feast/infra/online_stores/aerospike_online_store/aerospike.py @@ -0,0 +1,1047 @@ +from __future__ import annotations + +import asyncio +import functools +import importlib +import threading +from datetime import datetime, timezone +from logging import getLogger +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + Literal, + Optional, + Sequence, + Set, + Tuple, + TypeVar, + Union, +) + +from pydantic import SecretStr + +try: + import aerospike + from aerospike_helpers import cdt_ctx + from aerospike_helpers.batch.records import BatchRecords + from aerospike_helpers.batch.records import Write as BatchWrite + from aerospike_helpers.operations import map_operations as map_ops + from aerospike_helpers.operations import operations as ops +except ImportError as e: + from feast.errors import FeastExtrasDependencyImportError + + raise FeastExtrasDependencyImportError("aerospike", str(e)) + +from feast.entity import Entity +from feast.feature_view import FeatureView +from feast.infra.key_encoding_utils import serialize_entity_key +from feast.infra.online_stores.online_store import OnlineStore +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import FeastConfigBaseModel, RepoConfig +from feast.type_map import ( + feast_value_type_to_python_type, + python_values_to_proto_values, +) + +logger = getLogger(__name__) + + +# Aerospike per-record batch result codes we treat specially. Anything not +# listed here is surfaced as an exception so a transient server error (e.g. +# timeout, device overload) is never silently misreported as a missing feature. +# See https://aerospike.com/docs/server/reference/errors. +_AS_OK: int = 0 +_AS_ERR_RECORD_NOT_FOUND: int = 2 # genuine "entity has never been written" +_AS_ERR_OP_NOT_APPLICABLE: int = 26 # map/list op targeted a missing CDT path + + +_AUTH_MODE_TO_CONSTANT: Dict[str, int] = { + "internal": aerospike.AUTH_INTERNAL, + "external": aerospike.AUTH_EXTERNAL, + "pki": aerospike.AUTH_PKI, +} + + +# Type alias for the prewriting-hook signature. Hooks receive the rows about +# to be written and return a (possibly transformed) row list with the same +# schema. Defined as ``Any`` in the value position to keep the public type +# hint readable; the actual contract is documented on +# ``AerospikeOnlineStoreConfig.prewriting_hook``. +PrewritingHook = Callable[ + [ + "RepoConfig", + "FeatureView", + List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + ], + List[Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]]], +] + + +# Create every Map CDT bin with an ordered map. map_get_by_key / +# map_remove_by_key on an ordered map are O(log N) in the map size instead of +# O(N), which matters on the update() background scan (which walks every +# record in the project's set) and on reads of wide feature views. The policy +# is applied on each put so map-creation on the first write picks up the +# ordering; subsequent puts keep it. +_ORDERED_MAP_POLICY: Dict[str, Any] = {"map_order": aerospike.MAP_KEY_ORDERED} + +# Aerospike server ``batch-max-requests`` defaults to 5000 on many clusters (0 +# means unlimited on newer releases). Stay well under that so materialization +# and wide feature-server requests do not trip BatchMaxRequestError (code 151). +_DEFAULT_BATCH_MAX_RECORDS: int = 1_000 + +_T = TypeVar("_T") + + +def _datetime_to_epoch_ms(dt: datetime) -> int: + """Convert a datetime to int64 epoch milliseconds. + + Aerospike has no native datetime type, so timestamps are stored as int + bins. Per the OnlineStore contract, a tz-naive timestamp is treated as UTC. + """ + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return int(dt.timestamp() * 1000) + + +def _epoch_ms_to_datetime(value: Optional[int]) -> Optional[datetime]: + """Inverse of :func:`_datetime_to_epoch_ms`. Returns a tz-aware UTC datetime.""" + if value is None: + return None + return datetime.fromtimestamp(value / 1000.0, tz=timezone.utc) + + +def _resolve_ttl(ttl_seconds: Optional[int]) -> int: + """Map the config's ``ttl_seconds`` to an Aerospike record-metadata TTL. + + * ``None`` -> use the namespace default (``TTL_NAMESPACE_DEFAULT``). + * ``0`` -> never expire (``TTL_NEVER_EXPIRE``). + * ``> 0`` -> that many seconds until expiry. + """ + if ttl_seconds is None: + return aerospike.TTL_NAMESPACE_DEFAULT + if ttl_seconds == 0: + return aerospike.TTL_NEVER_EXPIRE + return int(ttl_seconds) + + +class AerospikeOnlineStoreConfig(FeastConfigBaseModel): + """Aerospike configuration. + + Aerospike does not have a URI analogue; connections are established via a + seed list of ``(host, port)`` or ``(host, port, tls_name)`` tuples. See the + Aerospike Python client reference for the meaning of additional policies and + TLS options surfaced below, and use ``client_kwargs`` for anything not + explicitly modelled here. + """ + + type: Literal["aerospike"] = "aerospike" + """Online store type selector""" + + hosts: List[Union[Tuple[str, int], Tuple[str, int, str]]] = [("localhost", 3000)] + """Aerospike seed nodes. + + Each entry is either ``(host, port)`` or ``(host, port, tls_name)`` when TLS + is enabled. At least one seed node is required. + """ + + namespace: str = "feast" + """Default Aerospike namespace. Must be pre-configured on the cluster — + namespaces cannot be created at runtime. Used for any feature view not + listed in :attr:`namespace_overrides`.""" + + set_name_template: str = "{project}_{collection_suffix}" + """Template for the per-project Aerospike set name. Available substitutions: + ``{project}`` and ``{collection_suffix}``. Used for any feature view not + listed in :attr:`set_overrides`.""" + + collection_suffix: str = "latest" + """Suffix used by ``set_name_template`` to distinguish sets belonging to the + same project (e.g. a future multi-version layout).""" + + namespace_overrides: Dict[str, str] = {} + """Per-feature-view namespace overrides. Maps a feature view name to the + Aerospike namespace that view should be stored in. Falls back to + :attr:`namespace` for any feature view not listed. + + Lets a deployment pin hot small views to a RAM-only namespace and wider + archival views to an SSD-backed one without having to split projects:: + + namespace_overrides = { + "driver_realtime_stats": "feast_ram", + "driver_history_lookup": "feast_ssd", + } + + Every namespace listed here MUST already exist on the cluster — Aerospike + cannot create namespaces at runtime, and a missing namespace surfaces as + an opaque ``AEROSPIKE_ERR_PARAM`` on the first read or write. + """ + + set_overrides: Dict[str, str] = {} + """Per-feature-view set name overrides. Maps a feature view name to a + fully-qualified Aerospike set name. Falls back to the rendering of + :attr:`set_name_template` for any feature view not listed. + + Useful when a deployment wants each feature view in its own set so admin + operations like ``truncate`` or the ``feast apply`` deletion path can + target one view without scanning the records of the others. Tradeoff: + multi-feature-view reads on the same entity become multiple round trips + instead of one — only set this when the operational isolation is worth + that cost. + """ + + prewriting_hook: Optional[str] = None + """Optional import path of a callable applied to every batch of rows + before they are written. Used for cross-cutting concerns like PII + masking, encryption-at-rest, value coercion or dual-write fan-out. + + Format: ``"package.module.function_name"``. Resolved by import string + (rather than a Python ``Callable``) so the config survives YAML / JSON + serialisation and remote-feature-server transport. + + The hook signature is :data:`PrewritingHook`:: + + def hook( + config: RepoConfig, + table: FeatureView, + data: List[Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]]], + ) -> List[Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]]]: + ... + + The hook MUST return a row list with the same shape as its input. + Hooks that raise will fail the whole batch — there is no per-row + fallback. The resolved callable is cached on the store instance; if the + configured import string changes between calls, it is re-resolved + automatically on the next write. + """ + + user: Optional[str] = None + """Optional username for Aerospike Enterprise authentication.""" + + password: Optional[SecretStr] = None + """Optional password for Aerospike Enterprise authentication.""" + + auth_mode: Literal["internal", "external", "pki"] = "internal" + """Authentication mode. ``internal`` for CE/EE user/password, ``external`` + for LDAP/Kerberos, ``pki`` for certificate-based auth.""" + + tls: Optional[Dict[str, Any]] = None + """TLS configuration, passed through verbatim to the Aerospike client. + See the Aerospike Python client ``tls`` policy options.""" + + ttl_seconds: Optional[int] = None + """Record-level TTL, applied to every write. ``None`` uses the namespace + default, ``0`` means never expire (mapped to the client's ``-1`` sentinel). + No per-feature-view override in v1.""" + + write_timeout_ms: int = 1_000 + """Per-call write total timeout in milliseconds. This is the hard deadline + the client gives a single ``put`` / ``operate`` — including any retries — + to return a response, after which the call fails.""" + + read_timeout_ms: int = 250 + """Per-call read total timeout in milliseconds. Hard deadline for a + single-record ``get`` — including any retries — after which the call + fails.""" + + batch_total_timeout_ms: int = 2_000 + """Total timeout in milliseconds for a whole ``batch_write`` / + ``batch_operate`` call, including retries. Applies to every batch + operation ``online_read`` and ``online_write_batch`` issue.""" + + socket_timeout_ms: Optional[int] = None + """Per-attempt socket timeout in milliseconds. This is the per-retry + trigger that lets ``max_retries`` actually fire within the caller's + overall ``*_timeout_ms`` budget — without it, a single attempt can + consume the whole deadline and retries never run. Applied uniformly + to ``read``, ``write`` and ``batch`` policies. ``None`` leaves the + client default in place.""" + + max_retries: int = 2 + """Maximum number of automatic retries on transient errors.""" + + batch_max_records: int = _DEFAULT_BATCH_MAX_RECORDS + """Maximum records per ``batch_write`` / ``batch_operate`` call. + + Aerospike enforces a per-node batch size via the server ``batch-max-requests`` + setting (historically 5000). Feast chunks read and write paths to this limit + so large materializations and wide online-serving requests do not fail the + whole batch when the server cap is exceeded. + """ + + client_kwargs: Dict[str, Any] = {} + """Escape hatch for any Aerospike client configuration not surfaced above. + Merged into the client config passed to ``aerospike.client()``.""" + + +class AerospikeOnlineStore(OnlineStore): + """Aerospike implementation of the Feast :class:`OnlineStore`. + + Storage layout (one set per project): + + * Namespace: ``config.online_store.namespace`` (server-configured) + * Set: ``{project}_{collection_suffix}`` + * Key: ``serialize_entity_key(entity_key)`` (bytes) + * Bins: + + * ``features`` — Map CDT ``{"": {"": }}`` + * ``event_ts`` — Map CDT ``{"": }`` + * ``created_ts`` — top-level ```` + + Timestamps are stored as int64 epoch milliseconds because Aerospike has no + native datetime type. Tz-naive timestamps are treated as UTC per the + :class:`OnlineStore` contract. + """ + + def __init__(self) -> None: + # Kept on the instance rather than the class so two ``AerospikeOnlineStore`` + # instances can't accidentally share a cached client through class state. + self._client: Optional[aerospike.Client] = None + # Resolved prewriting hook, cached on the instance so the import + # cost is paid once. ``_prewriting_hook_spec`` records the import + # string the cache was built from so a config swap (re-binding the + # store to a different hook) re-resolves on next call. + self._prewriting_hook: Optional[PrewritingHook] = None + self._prewriting_hook_spec: Optional[str] = None + self._client_lock = threading.Lock() + + # ------------------------------------------------------------------ + # Lifecycle / connection management + # ------------------------------------------------------------------ + @staticmethod + def _chunked(items: Sequence[_T], chunk_size: int) -> Iterator[Sequence[_T]]: + """Yield slices of ``items`` no larger than ``chunk_size``.""" + if chunk_size <= 0: + raise ValueError(f"chunk_size must be positive, got {chunk_size}") + for start in range(0, len(items), chunk_size): + yield items[start : start + chunk_size] + + def _batch_max_records(self, config: RepoConfig) -> int: + store_cfg = config.online_store + if not isinstance(store_cfg, AerospikeOnlineStoreConfig): + raise RuntimeError(f"{config.online_store.type = }. It must be aerospike.") + return store_cfg.batch_max_records + + def _get_client(self, config: RepoConfig) -> aerospike.Client: + """Lazily create and cache an Aerospike client on first use. + + The underlying C client maintains its own connection pool, so a single + cached instance is safe to share across calls on this store. Creation + is guarded by a lock so concurrent first callers in a threaded feature + server do not leak extra connections. + """ + if self._client is not None: + return self._client + + with self._client_lock: + if self._client is not None: + return self._client + + if not isinstance(config.online_store, AerospikeOnlineStoreConfig): + raise RuntimeError( + f"{config.online_store.type = }. It must be aerospike." + ) + store_cfg = config.online_store + + read_policy: Dict[str, Any] = { + "total_timeout": store_cfg.read_timeout_ms, + "max_retries": store_cfg.max_retries, + } + write_policy: Dict[str, Any] = { + "total_timeout": store_cfg.write_timeout_ms, + "max_retries": store_cfg.max_retries, + } + batch_policy: Dict[str, Any] = { + "total_timeout": store_cfg.batch_total_timeout_ms, + "max_retries": store_cfg.max_retries, + } + if store_cfg.socket_timeout_ms is not None: + # socket_timeout is the per-attempt deadline; without it, + # total_timeout is the whole budget and retries never fire. + read_policy["socket_timeout"] = store_cfg.socket_timeout_ms + write_policy["socket_timeout"] = store_cfg.socket_timeout_ms + batch_policy["socket_timeout"] = store_cfg.socket_timeout_ms + + client_config: Dict[str, Any] = { + "hosts": [tuple(h) for h in store_cfg.hosts], + "policies": { + "read": read_policy, + "write": write_policy, + "batch": batch_policy, + }, + **store_cfg.client_kwargs, + } + if store_cfg.user: + if store_cfg.password is None: + raise ValueError( + "AerospikeOnlineStoreConfig.user is set but password is not." + ) + client_config["user"] = store_cfg.user + client_config["password"] = store_cfg.password.get_secret_value() + client_config["auth_mode"] = _AUTH_MODE_TO_CONSTANT[store_cfg.auth_mode] + if store_cfg.tls: + client_config["tls"] = store_cfg.tls + + self._client = aerospike.client(client_config).connect() + return self._client + + def _set_name(self, config: RepoConfig, fv_name: Optional[str] = None) -> str: + """Resolve the Aerospike set name for a feature view. + + Without ``fv_name`` returns the project-level default rendered from + ``set_name_template``. With an ``fv_name`` returns the override from + :attr:`AerospikeOnlineStoreConfig.set_overrides` if present, falling + back to the project default. Calling without ``fv_name`` is preserved + for callers (and tests) that want the project's default set without + knowing which feature views might override it. + """ + store_cfg = config.online_store + overrides = getattr(store_cfg, "set_overrides", None) or {} + if fv_name is not None and fv_name in overrides: + return overrides[fv_name] + return store_cfg.set_name_template.format( + project=config.project, + collection_suffix=store_cfg.collection_suffix, + ) + + def _namespace_for_fv( + self, config: RepoConfig, fv_name: Optional[str] = None + ) -> str: + """Resolve the Aerospike namespace for a feature view. + + Mirrors :meth:`_set_name` semantics: without ``fv_name`` returns the + store-default namespace; with one, returns the override from + :attr:`AerospikeOnlineStoreConfig.namespace_overrides` if present. + """ + store_cfg = config.online_store + overrides = getattr(store_cfg, "namespace_overrides", None) or {} + if fv_name is not None and fv_name in overrides: + return overrides[fv_name] + return store_cfg.namespace + + def _aerospike_key( + self, + config: RepoConfig, + entity_key: EntityKeyProto, + fv_name: Optional[str] = None, + ) -> Tuple[str, str, bytearray]: + """Build a ``(namespace, set, user_key)`` tuple for an entity. + + When ``fv_name`` is provided, the namespace and set name honour the + per-feature-view overrides from + :class:`AerospikeOnlineStoreConfig`. Without ``fv_name`` the + store-level defaults are used. + + The user key is returned as a ``bytearray`` rather than ``bytes``: + the Aerospike Python C client rejects ``bytes`` user keys + (``calc_digest`` raises ``"Key is invalid"``), and ``batch_read`` / + ``batch_operate`` silently hash only the first byte of a ``bytes`` + key. ``bytearray`` is the supported binary-key type. + """ + user_key = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + return ( + self._namespace_for_fv(config, fv_name), + self._set_name(config, fv_name), + bytearray(user_key), + ) + + def _resolve_prewriting_hook(self, config: RepoConfig) -> Optional[PrewritingHook]: + """Resolve and cache the configured prewriting hook, if any. + + The hook is referenced by import string in the config (so it + survives YAML/JSON round-trips and remote-feature-server transport), + and resolved here on first use. The resolved callable is cached on + the store instance — and re-resolved automatically if a subsequent + config swap rebinds the store to a different hook. + """ + spec = getattr(config.online_store, "prewriting_hook", None) + if not spec: + # A previously-set hook on this instance must be cleared if the + # caller has since unset it on the config; otherwise we'd keep + # applying a hook the user explicitly turned off. + self._prewriting_hook = None + self._prewriting_hook_spec = None + return None + if spec == self._prewriting_hook_spec and self._prewriting_hook is not None: + return self._prewriting_hook + + module_path, _, fn_name = spec.rpartition(".") + if not module_path or not fn_name: + raise ValueError( + "AerospikeOnlineStoreConfig.prewriting_hook must be a fully " + f"qualified import path 'package.module.function', got: {spec!r}" + ) + try: + module = importlib.import_module(module_path) + except ImportError as e: + raise ValueError( + f"prewriting_hook {spec!r}: could not import module " + f"{module_path!r}: {e}" + ) from e + try: + hook = getattr(module, fn_name) + except AttributeError: + raise ValueError( + f"prewriting_hook {spec!r}: module {module_path!r} has no " + f"attribute {fn_name!r}" + ) from None + if not callable(hook): + raise TypeError( + f"prewriting_hook {spec!r} resolved to a non-callable " + f"{type(hook).__name__}" + ) + self._prewriting_hook = hook + self._prewriting_hook_spec = spec + return hook + + # ------------------------------------------------------------------ + # Write path + # ------------------------------------------------------------------ + @staticmethod + def _build_batch_writes( + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + namespace: str, + set_name: str, + ) -> BatchRecords: + """Build a :class:`BatchRecords` with one :class:`BatchWrite` per row. + + Each row becomes an atomic, server-side Map-put op list: + + * ``features[][] = `` for every requested feature + (single ``map_put_items`` op keyed by feature-view name). + * ``event_ts[] = ``. + * ``created_ts = `` when provided. + + Using Map CDT ops rather than a full-record ``put`` means two writers + touching different feature views on the same entity will not clobber + each other — each write only mutates its own slot in the outer map. + + The Map CDTs are created with ``MAP_KEY_ORDERED`` so key lookups on + reads and the ``update()`` background scan stay O(log N) in the map + size. Writes use the default ``POLICY_KEY_DIGEST`` — the serialized + entity key itself is not stored on the server, saving per-record + storage that the read path never consumes (result order is preserved + by ``batch_operate`` and paired back via ``zip`` in ``online_read``). + """ + ttl_meta = {"ttl": _resolve_ttl(config.online_store.ttl_seconds)} + + batch = BatchRecords() + for entity_key, proto_values, event_timestamp, created_timestamp in data: + user_key = bytearray( + serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + ) + feature_map = { + field: feast_value_type_to_python_type(val) + for field, val in proto_values.items() + } + operations: List[Dict[str, Any]] = [ + map_ops.map_put_items( + "features", + {table.name: feature_map}, + map_policy=_ORDERED_MAP_POLICY, + ), + map_ops.map_put( + "event_ts", + table.name, + _datetime_to_epoch_ms(event_timestamp), + map_policy=_ORDERED_MAP_POLICY, + ), + ] + if created_timestamp is not None: + operations.append( + ops.write("created_ts", _datetime_to_epoch_ms(created_timestamp)) + ) + batch.batch_records.append( + BatchWrite( + key=(namespace, set_name, user_key), + ops=operations, + meta=ttl_meta, + ) + ) + return batch + + def online_write_batch( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + """Write a batch of feature rows using Aerospike's native batch-write API. + + Each row is upserted as a set of Map CDT operations on a single record, + preserving data for other feature views that share the same entity key. + """ + if not data: + if progress: + progress(0) + return + + # Hook resolution happens before the early-data check to avoid wiring + # work, but after the empty-batch check so a "no rows" call doesn't + # pay the import cost. Hooks are allowed to mutate ``data`` in place + # or return a new list; we always rebind to whatever they return. + hook = self._resolve_prewriting_hook(config) + if hook is not None: + data = hook(config, table, data) + if not data: + if progress: + progress(0) + return + + client = self._get_client(config) + namespace = self._namespace_for_fv(config, table.name) + set_name = self._set_name(config, table.name) + chunk_size = self._batch_max_records(config) + written = 0 + for chunk in self._chunked(data, chunk_size): + batch = self._build_batch_writes( + config, table, list(chunk), namespace, set_name + ) + if batch.batch_records: + client.batch_write(batch) + # Per-record result codes must be inspected: client.batch_write + # only raises if the whole request was rejected. A partial failure + # (e.g. a single-partition timeout) is otherwise silent, which in + # an online-serving path presents downstream as "model saw stale + # features" weeks after the fact. + self._raise_on_batch_errors(batch.batch_records, set_name, op="write") + written += len(chunk) + if progress: + progress(written) + + # ------------------------------------------------------------------ + # Read path + # ------------------------------------------------------------------ + def online_read( + self, + config: RepoConfig, + table: FeatureView, + entity_keys: List[EntityKeyProto], + requested_features: Optional[List[str]] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + """Read feature values for a batch of entities in a single round trip. + + Uses Aerospike's ``batch_operate`` with two server-side Map-get ops per + record. When ``requested_features`` is provided, only those feature + columns are shipped over the wire using ``map_get_by_key_list`` nested + into the feature-view's Map CDT via ``cdt_ctx_map_key``. Otherwise the + whole feature-view slot is returned. + + * features op (projected): + ``map_get_by_key_list("features", requested_features, + MAP_RETURN_KEY_VALUE, ctx=[cdt_ctx_map_key()])`` + * features op (full): + ``map_get_by_key("features", , MAP_RETURN_VALUE)`` + * event_ts op (always): + ``map_get_by_key("event_ts", , MAP_RETURN_VALUE)`` + + Per-record status codes are inspected so we can tell a genuine miss + (``RECORD_NOT_FOUND``, or a nested ``OP_NOT_APPLICABLE`` when the + feature-view slot is absent) apart from a transient server error, + which is raised rather than silently returned as a null row. Output + order matches ``entity_keys``. + """ + if not entity_keys: + return [] + + client = self._get_client(config) + ns = self._namespace_for_fv(config, table.name) + set_name = self._set_name(config, table.name) + read_ops = self._build_read_ops(table.name, requested_features) + chunk_size = self._batch_max_records(config) + + # ``ids`` and ``docs`` use immutable ``bytes`` because ``bytearray`` is + # unhashable and can't key a dict. Keys on the wire must stay + # ``bytearray`` (see ``_aerospike_key``) — we only convert here for + # lookup. + ids: List[bytes] = [] + docs: Dict[bytes, Dict[str, Any]] = {} + + for entity_chunk in self._chunked(entity_keys, chunk_size): + keys = [ + ( + ns, + set_name, + bytearray( + serialize_entity_key( + k, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + ), + ) + for k in entity_chunk + ] + batch = client.batch_operate(keys, read_ops) + chunk_ids = [bytes(user_key) for _, _, user_key in keys] + ids.extend(chunk_ids) + # batch_operate preserves request order. We pair each response with + # the original user-key rather than ``br.key[2]``: the Aerospike + # client may return the key in a different representation (e.g. only + # the first byte as a str when the write didn't use POLICY_KEY_SEND + # for reads). + for user_key, br in zip(chunk_ids, batch.batch_records): + if br.result == _AS_ERR_RECORD_NOT_FOUND: + continue + if br.result == _AS_ERR_OP_NOT_APPLICABLE: + # The record exists but the nested feature-view slot doesn't; + # treat as a miss to match the OnlineStore contract. + continue + if br.result != _AS_OK: + raise RuntimeError( + f"Aerospike batch_operate returned a non-OK status for " + f"entity (ns={ns}, set={set_name}): result={br.result}" + ) + if br.record is None: + continue + _, _, bins = br.record + raw_features = bins.get("features") if bins else None + fv_event_ts_ms = bins.get("event_ts") if bins else None + fv_features = self._normalize_projected_features(raw_features) + docs[user_key] = { + "features": {table.name: fv_features} + if fv_features is not None + else {}, + "event_timestamps": { + table.name: _epoch_ms_to_datetime(fv_event_ts_ms) + }, + } + + return self._convert_raw_docs_to_proto(ids, docs, table) + + @staticmethod + def _build_read_ops( + fv_name: str, requested_features: Optional[List[str]] + ) -> List[Dict[str, Any]]: + """Build the per-record op list for an ``online_read`` call. + + Projects ``requested_features`` server-side via + ``map_get_by_key_list`` + ``cdt_ctx_map_key`` when a projection list + is provided. Without a projection, returns the whole feature-view + submap. + """ + if requested_features: + features_op = map_ops.map_get_by_key_list( + "features", + list(requested_features), + aerospike.MAP_RETURN_KEY_VALUE, + ctx=[cdt_ctx.cdt_ctx_map_key(fv_name)], + ) + else: + features_op = map_ops.map_get_by_key( + "features", fv_name, aerospike.MAP_RETURN_VALUE + ) + return [ + features_op, + map_ops.map_get_by_key("event_ts", fv_name, aerospike.MAP_RETURN_VALUE), + ] + + @staticmethod + def _normalize_projected_features( + raw: Optional[Union[Dict[str, Any], List[Any]]], + ) -> Optional[Dict[str, Any]]: + """Convert an Aerospike features payload into a uniform ``{name: val}`` dict. + + The shape depends on which op produced the payload: + + * ``map_get_by_key("features", , MAP_RETURN_VALUE)`` returns a + ``dict`` (the inner feature-view submap). + * ``map_get_by_key_list("features", [...], MAP_RETURN_KEY_VALUE, + ctx=...)`` returns a flat ``list`` of ``[k1, v1, k2, v2, ...]`` + containing only the requested keys that exist. + """ + if raw is None: + return None + if isinstance(raw, dict): + return raw + if isinstance(raw, list): + if not raw: + return {} + return dict(zip(raw[0::2], raw[1::2])) + return None + + @staticmethod + def _raise_on_batch_errors( + batch_records: Sequence[Any], set_name: str, op: str + ) -> None: + """Raise if any per-record result code signals a failed batch write/op. + + ``client.batch_write`` and ``client.batch_operate`` only raise when the + overall request was rejected; partial failures (a single-partition + timeout, a replica quorum miss, etc.) are surfaced per record via + ``br.result`` and are otherwise silent. In an online-serving path + those silent failures later present as missing features, so we fail + loud here instead. + """ + errors = [br.result for br in batch_records if br.result != _AS_OK] + if errors: + raise RuntimeError( + f"Aerospike batch_{op} returned non-OK status codes for " + f"{len(errors)} of {len(batch_records)} records " + f"(set={set_name}): codes={errors[:10]}" + ) + + @staticmethod + def _convert_raw_docs_to_proto( + ids: List[bytes], + docs: Dict[bytes, Dict[str, Any]], + table: FeatureView, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + """Convert raw feature maps into ordered proto rows. + + The heavy lifting is done by + :func:`feast.type_map.python_values_to_proto_values`, which is + column-oriented and expects a list of values of a single type. This + helper transforms the row-oriented Aerospike lookup result into + columns, converts each column once, then reassembles rows — mirroring + the MongoDB online store's reshape so we amortize the python→proto + cost across the whole batch. + + Args: + ids: serialized entity-key bytes, in the order requested. + docs: ``{entity_id_bytes: {"features": {: {...}}, + "event_timestamps": {: datetime}}}``. Missing keys denote + "record not found". + table: FeatureView being read; provides feature name → type. + + Returns: + A list of ``(event_timestamp, feature_dict)`` the same length as + ``ids`` (``(None, None)`` for entities that had no data for this + feature view). + """ + feature_type_map = { + feature.name: feature.dtype.to_value_type() for feature in table.features + } + + raw_feature_columns: Dict[str, List[Any]] = { + feature_name: [] for feature_name in feature_type_map + } + for entity_id in ids: + doc = docs.get(entity_id) + feature_dict = doc.get("features", {}).get(table.name, {}) if doc else {} + for feature_name in feature_type_map: + raw_feature_columns[feature_name].append( + feature_dict.get(feature_name, None) + ) + + proto_feature_columns = { + feature_name: python_values_to_proto_values( + raw_values, feature_type=feature_type_map[feature_name] + ) + for feature_name, raw_values in raw_feature_columns.items() + } + + results: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] + for i, entity_id in enumerate(ids): + doc = docs.get(entity_id) + if doc is None: + results.append((None, None)) + continue + + fv_features = doc.get("features", {}).get(table.name) + if fv_features is None: + results.append((None, None)) + continue + + ts = doc.get("event_timestamps", {}).get(table.name) + row_features = { + feature_name: proto_feature_columns[feature_name][i] + for feature_name in proto_feature_columns + } + results.append((ts, row_features)) + return results + + # ------------------------------------------------------------------ + # Async wrappers + # ------------------------------------------------------------------ + # The Aerospike Python client is a synchronous C extension; there is no + # native asyncio interface. Network calls do release the GIL, so we expose + # a correct ``async`` surface by offloading each blocking call to the + # default thread-pool executor. Callers that ``await`` these methods keep + # the event loop responsive while the client talks to the cluster. + + async def online_write_batch_async( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + loop = asyncio.get_running_loop() + await loop.run_in_executor( + None, + functools.partial(self.online_write_batch, config, table, data, progress), + ) + + async def online_read_async( + self, + config: RepoConfig, + table: FeatureView, + entity_keys: List[EntityKeyProto], + requested_features: Optional[List[str]] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + loop = asyncio.get_running_loop() + return await loop.run_in_executor( + None, + functools.partial( + self.online_read, config, table, entity_keys, requested_features + ), + ) + + async def initialize(self, config: RepoConfig) -> None: + """Pre-warm the Aerospike client so the first request is hot. + + Feature servers typically call :meth:`initialize` during startup so the + TCP + handshake latency is paid upfront rather than on the first + ``online_read``. + """ + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, functools.partial(self._get_client, config)) + + async def close(self) -> None: + """Release the cached Aerospike client, if any.""" + with self._client_lock: + if self._client is None: + return + client = self._client + self._client = None + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, client.close) + + # ------------------------------------------------------------------ + # Admin paths (update / teardown) + # ------------------------------------------------------------------ + def update( + self, + config: RepoConfig, + tables_to_delete: Sequence[FeatureView], + tables_to_keep: Sequence[FeatureView], + entities_to_delete: Sequence[Entity], + entities_to_keep: Sequence[Entity], + partial: bool, + ) -> None: + """Reconcile per-feature-view data when a schema change is applied. + + Aerospike has no explicit schema, and records/sets are created lazily + on first write, so there is nothing to do for ``tables_to_keep`` or + either of the entity lists. For ``tables_to_delete`` we strip each + feature-view's slot out of the ``features`` and ``event_ts`` Map + CDTs on every record that contains it. + + Dropped feature views are grouped by their resolved + ``(namespace, set)`` (per :attr:`namespace_overrides` / + :attr:`set_overrides`), and each group is issued as one + **background scan** with a combined op list — a single server-side + pass per (ns, set), regardless of how many feature views in that + group are being dropped. Without this grouping, a deployment that + spreads feature views across multiple namespaces or sets would + either issue blind cross-namespace scans (impossible) or scan one + per dropped FV (wasteful). The scans run asynchronously server-side + and return immediately; this matches the intent of ``feast apply``, + after which the caller stops reading the dropped feature views + anyway. + """ + if not isinstance(config.online_store, AerospikeOnlineStoreConfig): + raise RuntimeError(f"{config.online_store.type = }. It must be aerospike.") + if not tables_to_delete: + return + + client = self._get_client(config) + + # Group dropped feature views by (namespace, set) so each scan only + # touches the records that could plausibly contain the dropped slot. + groups: Dict[Tuple[str, str], List[FeatureView]] = {} + for fv in tables_to_delete: + key = ( + self._namespace_for_fv(config, fv.name), + self._set_name(config, fv.name), + ) + groups.setdefault(key, []).append(fv) + + for (ns, set_name), fvs in groups.items(): + remove_ops: List[Dict[str, Any]] = [] + for fv in fvs: + remove_ops.append( + map_ops.map_remove_by_key( + "features", fv.name, aerospike.MAP_RETURN_NONE + ) + ) + remove_ops.append( + map_ops.map_remove_by_key( + "event_ts", fv.name, aerospike.MAP_RETURN_NONE + ) + ) + + scan = client.scan(ns, set_name) + scan.add_ops(remove_ops) + scan.execute_background() + + def teardown( + self, + config: RepoConfig, + tables: Sequence[FeatureView], + entities: Sequence[Entity], + ) -> None: + """Truncate every (namespace, set) the project may have written to + and close the cached client. + + Uses Aerospike's ``truncate(namespace, set, 0)`` — a set-scoped + metadata operation that clears every record in O(1) client time, + cheaper than Mongo's ``collection.drop()``. Passing ``0`` as the + cutoff means "drop everything regardless of last-update time". + + Collects the unique ``(namespace, set)`` pairs from the project's + store-level default plus every feature view in ``tables`` (each + resolved through :attr:`namespace_overrides` / + :attr:`set_overrides`). The default is always included so a + teardown invoked with an empty ``tables`` list still clears the + store-default location. + + Truncate on a non-existent set is a no-op, so calling ``teardown`` + on a project that never wrote data — or on a feature view that + was never written to — is safe. + """ + if not isinstance(config.online_store, AerospikeOnlineStoreConfig): + raise RuntimeError(f"{config.online_store.type = }. It must be aerospike.") + + client = self._get_client(config) + + pairs: Set[Tuple[str, str]] = { + (config.online_store.namespace, self._set_name(config)) + } + for fv in tables: + pairs.add( + ( + self._namespace_for_fv(config, fv.name), + self._set_name(config, fv.name), + ) + ) + # sort to keep the truncation order deterministic for tests; the + # operation is independent per (ns, set) so order has no semantic + # meaning. + for ns, set_name in sorted(pairs): + client.truncate(ns, set_name, 0) + with self._client_lock: + if self._client is not None: + self._client.close() + self._client = None diff --git a/sdk/python/feast/infra/online_stores/aerospike_online_store/aerospike_repo_configuration.py b/sdk/python/feast/infra/online_stores/aerospike_online_store/aerospike_repo_configuration.py new file mode 100644 index 00000000000..e704aca780d --- /dev/null +++ b/sdk/python/feast/infra/online_stores/aerospike_online_store/aerospike_repo_configuration.py @@ -0,0 +1,13 @@ +from tests.universal.feature_repos.integration_test_repo_config import ( + IntegrationTestRepoConfig, +) +from tests.universal.feature_repos.universal.online_store.aerospike import ( + AerospikeOnlineStoreCreator, +) + +FULL_REPO_CONFIGS = [ + IntegrationTestRepoConfig( + online_store="aerospike", + online_store_creator=AerospikeOnlineStoreCreator, + ), +] diff --git a/sdk/python/feast/infra/online_stores/cassandra_online_store/cassandra_online_store.py b/sdk/python/feast/infra/online_stores/cassandra_online_store/cassandra_online_store.py index 0870bc709db..cea3ed4f87d 100644 --- a/sdk/python/feast/infra/online_stores/cassandra_online_store/cassandra_online_store.py +++ b/sdk/python/feast/infra/online_stores/cassandra_online_store/cassandra_online_store.py @@ -70,6 +70,21 @@ E_CASSANDRA_UNKNOWN_LB_POLICY = ( "Unknown/unsupported Load Balancing Policy name in Cassandra configuration" ) +E_CASSANDRA_DC_DEFAULT_NOT_FOUND = ( + "Cassandra multi-DC: 'load_balancing.local_dc' " + "does not match any datacenter 'name' in the 'datacenters' list" +) +E_CASSANDRA_DC_CONFIG_CONFLICT = ( + "Cassandra config: 'datacenters' is mutually exclusive with " + "'hosts' and 'secure_bundle_path'" +) +E_CASSANDRA_DC_CONFIG_EMPTY = ( + "Cassandra config: 'datacenters' list must not be empty if provided" +) +E_CASSANDRA_DC_ROUTING_INVALID = ( + "Cassandra multi-DC: 'routing.read_dc' or 'routing.write_dc' " + "does not match any datacenter 'name' in the 'datacenters' list" +) # CQL command templates (that is, before replacing schema names) INSERT_CQL_4_TEMPLATE = ( @@ -153,6 +168,102 @@ class CassandraOnlineStoreConfig(FeastConfigBaseModel): request_timeout: Optional[StrictFloat] = None """Request timeout in seconds.""" + class CassandraDatacenterConfig(FeastConfigBaseModel): + """ + Per-datacenter configuration for multi-DC Cassandra deployments. + + When the top-level ``datacenters`` list is used, each entry defines + the contact points and replication settings for one datacenter. + A named Cassandra execution profile (keyed by ``name``) is created + for every entry, enabling targeted per-DC routing via the driver's + execution-profile mechanism. + """ + + name: StrictStr + """ + Datacenter name as reported by the cluster (e.g. ``datacenter1``). + Also used as the named execution-profile key and referenced by + ``routing.read_dc`` / ``routing.write_dc``. + """ + + hosts: List[StrictStr] + """Contact-point host addresses belonging to this datacenter.""" + + replication_factor: Optional[StrictInt] = None + """ + Replication factor for this datacenter. + Informational only — keyspace must be pre-created. + """ + + replication_strategy: Optional[StrictStr] = None + """ + Replication strategy class (e.g. ``'SimpleStrategy'``, + ``'NetworkTopologyStrategy'``). + Informational only — keyspace must be pre-created. + """ + + class CassandraRoutingConfig(FeastConfigBaseModel): + """ + Optional per-operation datacenter routing for multi-DC deployments. + + Allows reads and writes to be directed to different datacenters. + Both fields are optional; when omitted the default datacenter + (from ``load_balancing.local_dc`` or the first entry in + ``datacenters``) is used for that operation. + + Values must match the ``name`` of one of the entries in the + ``datacenters`` list. + + Example:: + + routing: + read_dc: dc2 # reads served from dc2 + write_dc: dc1 # writes sent to dc1 + """ + + read_dc: Optional[StrictStr] = None + """Datacenter name to use for read operations (online_read).""" + + write_dc: Optional[StrictStr] = None + """Datacenter name to use for write operations (online_write_batch).""" + + datacenters: Optional[List[CassandraDatacenterConfig]] = None + """ + Per-datacenter configuration enabling multi-DC Cassandra support. + + When set, a named Cassandra execution profile is registered for each + datacenter (profile name = ``name`` field of each entry). The default + profile is determined by ``load_balancing.local_dc``; if + ``load_balancing`` is absent the first datacenter in the list is used. + + Use ``routing`` to direct reads and writes to specific datacenters. + + Mutually exclusive with ``hosts`` and ``secure_bundle_path``. + The keyspace must already exist; Feast does not create it automatically. + + Example:: + + datacenters: + - name: dc1 + hosts: [192.168.1.1, 192.168.1.2] + replication_factor: 3 + - name: dc2 + hosts: [10.0.0.1] + replication_factor: 2 + routing: + read_dc: dc2 + write_dc: dc1 + load_balancing: + local_dc: dc1 + load_balancing_policy: TokenAwarePolicy(DCAwareRoundRobinPolicy) + """ + + routing: Optional[CassandraRoutingConfig] = None + """ + Optional read/write datacenter routing for multi-DC mode. + Ignored when ``datacenters`` is not set. + """ + class CassandraLoadBalancingPolicy(FeastConfigBaseModel): """ Configuration block related to the Cluster's load-balancing policy. @@ -173,6 +284,9 @@ class CassandraLoadBalancingPolicy(FeastConfigBaseModel): """ Details on the load-balancing policy: it will be wrapped into an execution profile if present. + + In multi-DC mode (``datacenters`` list), ``local_dc`` also selects + which datacenter's profile becomes the default execution profile. """ read_concurrency: Optional[StrictInt] = 100 @@ -202,12 +316,24 @@ class CassandraOnlineStore(OnlineStore): to issue commands. _keyspace: Cassandra keyspace all tables live in. _prepared_statements: cache of statements prepared by the driver. + _dc_execution_profiles: list of named execution-profile keys + registered in multi-DC mode (one per datacenter). + Empty in single-DC / Astra DB mode. + _read_execution_profile: execution profile used for read operations. + Set to the configured ``routing.read_dc`` profile in + multi-DC mode, ``EXEC_PROFILE_DEFAULT`` otherwise. + _write_execution_profile: execution profile used for write operations. + Set to the configured ``routing.write_dc`` profile in + multi-DC mode, ``EXEC_PROFILE_DEFAULT`` otherwise. """ _cluster: Cluster = None _session: Session = None _keyspace: str = "feast_keyspace" _prepared_statements: Dict[str, PreparedStatement] = {} + _dc_execution_profiles: List[str] + _read_execution_profile: Any = EXEC_PROFILE_DEFAULT + _write_execution_profile: Any = EXEC_PROFILE_DEFAULT def _get_session(self, config: RepoConfig): """ @@ -223,85 +349,196 @@ def _get_session(self, config: RepoConfig): if self._session: return self._session - if not self._session: - # configuration consistency checks - hosts = online_store_config.hosts - secure_bundle_path = online_store_config.secure_bundle_path + + # ------------------------------------------------------------------ + # Branch B: multi-DC mode — one named execution profile + # per datacenter + # ------------------------------------------------------------------ + if online_store_config.datacenters is not None: + # Validate: empty list is a misconfiguration + if len(online_store_config.datacenters) == 0: + raise CassandraInvalidConfig(E_CASSANDRA_DC_CONFIG_EMPTY) + + # Validate: mutually exclusive with hosts / secure_bundle_path + if online_store_config.hosts or online_store_config.secure_bundle_path: + raise CassandraInvalidConfig(E_CASSANDRA_DC_CONFIG_CONFLICT) + port = online_store_config.port or 9042 keyspace = online_store_config.keyspace username = online_store_config.username password = online_store_config.password protocol_version = online_store_config.protocol_version - db_directions = hosts or secure_bundle_path - if not db_directions or not keyspace: - raise CassandraInvalidConfig(E_CASSANDRA_NOT_CONFIGURED) - if hosts and secure_bundle_path: - raise CassandraInvalidConfig(E_CASSANDRA_MISCONFIGURED) if (username is None) ^ (password is None): raise CassandraInvalidConfig(E_CASSANDRA_INCONSISTENT_AUTH) - if username is not None: - auth_provider = PlainTextAuthProvider( - username=username, - password=password, - ) - else: - auth_provider = None - - # handling of load-balancing policy (optional) - if online_store_config.load_balancing: - # construct a proper execution profile embedding - # the configured LB policy - _lbp_name = online_store_config.load_balancing.load_balancing_policy + auth_provider = ( + PlainTextAuthProvider(username=username, password=password) + if username is not None + else None + ) + + _lbp_name = ( + online_store_config.load_balancing.load_balancing_policy + if online_store_config.load_balancing + else "DCAwareRoundRobinPolicy" + ) + + execution_profiles: Dict[Any, ExecutionProfile] = {} + all_hosts: List[str] = [] + self._dc_execution_profiles = [] # fresh list per connection + for dc in online_store_config.datacenters: + all_hosts.extend(dc.hosts) if _lbp_name == "DCAwareRoundRobinPolicy": - lb_policy = DCAwareRoundRobinPolicy( - local_dc=online_store_config.load_balancing.local_dc, - ) + lb_policy = DCAwareRoundRobinPolicy(local_dc=dc.name) elif _lbp_name == "TokenAwarePolicy(DCAwareRoundRobinPolicy)": lb_policy = TokenAwarePolicy( - DCAwareRoundRobinPolicy( - local_dc=online_store_config.load_balancing.local_dc, - ) + DCAwareRoundRobinPolicy(local_dc=dc.name) ) else: raise CassandraInvalidConfig(E_CASSANDRA_UNKNOWN_LB_POLICY) - - # wrap it up in a map of ex.profiles with a default - exe_profile = ExecutionProfile( + execution_profiles[dc.name] = ExecutionProfile( request_timeout=online_store_config.request_timeout, load_balancing_policy=lb_policy, ) - execution_profiles = {EXEC_PROFILE_DEFAULT: exe_profile} + self._dc_execution_profiles.append(dc.name) + if ( + dc.replication_factor is not None + or dc.replication_strategy is not None + ): + logger.info( + "Cassandra multi-DC: " + "replication_factor/replication_strategy " + "for DC '%s' are informational only; " + "keyspace must be pre-created.", + dc.name, + ) + + # Determine default DC + default_dc = ( + online_store_config.load_balancing.local_dc + if online_store_config.load_balancing + else online_store_config.datacenters[0].name + ) + if default_dc not in execution_profiles: + raise CassandraInvalidConfig(E_CASSANDRA_DC_DEFAULT_NOT_FOUND) + default_profile = execution_profiles[default_dc] + execution_profiles[EXEC_PROFILE_DEFAULT] = default_profile + + # Resolve read/write execution profiles from routing config + dc_names = set(self._dc_execution_profiles) + if online_store_config.routing: + # use given dc or fall back to default_dc + read_dc = online_store_config.routing.read_dc + write_dc = online_store_config.routing.write_dc + if (read_dc and read_dc not in dc_names) or ( + write_dc and write_dc not in dc_names + ): + raise CassandraInvalidConfig(E_CASSANDRA_DC_ROUTING_INVALID) + self._read_execution_profile = read_dc or default_dc + self._write_execution_profile = write_dc or default_dc else: - execution_profiles = None + self._read_execution_profile = default_dc + self._write_execution_profile = default_dc - # additional optional keyword args to Cluster cluster_kwargs = { k: v - for k, v in { - "protocol_version": protocol_version, - "execution_profiles": execution_profiles, - }.items() + for k, v in {"protocol_version": protocol_version}.items() if v is not None } + self._cluster = Cluster( + all_hosts, + port=port, + auth_provider=auth_provider, + execution_profiles=execution_profiles, + **cluster_kwargs, + ) + self._keyspace = keyspace + self._session = self._cluster.connect(self._keyspace) + return self._session - # creation of Cluster (Cassandra vs. Astra) - if hosts: - self._cluster = Cluster( - hosts, port=port, auth_provider=auth_provider, **cluster_kwargs + # ------------------------------------------------------------------ + # Branch A: original single-DC / Astra DB + # ------------------------------------------------------------------ + hosts = online_store_config.hosts + secure_bundle_path = online_store_config.secure_bundle_path + port = online_store_config.port or 9042 + keyspace = online_store_config.keyspace + username = online_store_config.username + password = online_store_config.password + protocol_version = online_store_config.protocol_version + + db_directions = hosts or secure_bundle_path + if not db_directions or not keyspace: + raise CassandraInvalidConfig(E_CASSANDRA_NOT_CONFIGURED) + if hosts and secure_bundle_path: + raise CassandraInvalidConfig(E_CASSANDRA_MISCONFIGURED) + if (username is None) ^ (password is None): + raise CassandraInvalidConfig(E_CASSANDRA_INCONSISTENT_AUTH) + + if username is not None: + auth_provider = PlainTextAuthProvider( + username=username, + password=password, + ) + else: + auth_provider = None + + # handling of load-balancing policy (optional) + if online_store_config.load_balancing: + # construct a proper execution profile embedding + # the configured LB policy + _lbp_name = online_store_config.load_balancing.load_balancing_policy + if _lbp_name == "DCAwareRoundRobinPolicy": + lb_policy = DCAwareRoundRobinPolicy( + local_dc=online_store_config.load_balancing.local_dc, ) - else: - # we use 'secure_bundle_path' - self._cluster = Cluster( - cloud={"secure_connect_bundle": secure_bundle_path}, - auth_provider=auth_provider, - **cluster_kwargs, + elif _lbp_name == "TokenAwarePolicy(DCAwareRoundRobinPolicy)": + lb_policy = TokenAwarePolicy( + DCAwareRoundRobinPolicy( + local_dc=online_store_config.load_balancing.local_dc, + ) ) + else: + raise CassandraInvalidConfig(E_CASSANDRA_UNKNOWN_LB_POLICY) + + # wrap it up in a map of ex.profiles with a default + exe_profile = ExecutionProfile( + request_timeout=online_store_config.request_timeout, + load_balancing_policy=lb_policy, + ) + lb_execution_profiles: Optional[Dict[Any, ExecutionProfile]] = { + EXEC_PROFILE_DEFAULT: exe_profile + } + else: + lb_execution_profiles = None + + # additional optional keyword args to Cluster + cluster_kwargs_branch_a: Dict[str, Any] = { + k: v + for k, v in { + "protocol_version": protocol_version, + "execution_profiles": lb_execution_profiles, + }.items() + if v is not None + } + + # creation of Cluster (Cassandra vs. Astra) + if hosts: + self._cluster = Cluster( + hosts, port=port, auth_provider=auth_provider, **cluster_kwargs_branch_a + ) + else: + # we use 'secure_bundle_path' + self._cluster = Cluster( + cloud={"secure_connect_bundle": secure_bundle_path}, + auth_provider=auth_provider, + **cluster_kwargs_branch_a, + ) - # creation of Session - self._keyspace = keyspace - self._session = self._cluster.connect(self._keyspace) + # creation of Session + self._keyspace = keyspace + self._session = self._cluster.connect(self._keyspace) return self._session @@ -498,6 +735,7 @@ def _write_rows_concurrently( insert_cql, rows, concurrency=config.online_store.write_concurrency, + execution_profile=self._write_execution_profile, ) def _read_rows_by_entity_keys( @@ -526,6 +764,7 @@ def _read_rows_by_entity_keys( select_cql, ((entity_key_bin,) for entity_key_bin in entity_key_bins), concurrency=config.online_store.read_concurrency, + execution_profile=self._read_execution_profile, ) # execute_concurrent_with_args return a sequence # of (success, result_or_exception) pairs: diff --git a/sdk/python/feast/infra/online_stores/cassandra_online_store/cassandra_repo_configuration.py b/sdk/python/feast/infra/online_stores/cassandra_online_store/cassandra_repo_configuration.py index a1d619646f7..d206395fcf5 100644 --- a/sdk/python/feast/infra/online_stores/cassandra_online_store/cassandra_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/cassandra_online_store/cassandra_repo_configuration.py @@ -14,10 +14,10 @@ # limitations under the License. # -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.cassandra import ( +from tests.universal.feature_repos.universal.online_store.cassandra import ( CassandraOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/couchbase_online_store/README.md b/sdk/python/feast/infra/online_stores/couchbase_online_store/README.md index 885250a66af..4ede92c7d6b 100644 --- a/sdk/python/feast/infra/online_stores/couchbase_online_store/README.md +++ b/sdk/python/feast/infra/online_stores/couchbase_online_store/README.md @@ -87,12 +87,13 @@ feature_vector = store.get_online_features( ], ).to_dict() pprint(feature_vector) - ``` #### Output ```python -{'acc_rate': [0.01390857808291912, 0.4063614010810852], - 'avg_daily_trips': [69, 706], - 'conv_rate': [0.6624961495399475, 0.7595928311347961], - 'driver_id': [1004, 1005]} +{ + "acc_rate": [0.01390857808291912, 0.4063614010810852], + "avg_daily_trips": [69, 706], + "conv_rate": [0.6624961495399475, 0.7595928311347961], + "driver_id": [1004, 1005], +} ``` diff --git a/sdk/python/feast/infra/online_stores/couchbase_online_store/couchbase_repo_configuration.py b/sdk/python/feast/infra/online_stores/couchbase_online_store/couchbase_repo_configuration.py index e099e6ae1b5..6637c4555ad 100644 --- a/sdk/python/feast/infra/online_stores/couchbase_online_store/couchbase_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/couchbase_online_store/couchbase_repo_configuration.py @@ -1,7 +1,7 @@ -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.couchbase import ( +from tests.universal.feature_repos.universal.online_store.couchbase import ( CouchbaseOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/dynamodb.py b/sdk/python/feast/infra/online_stores/dynamodb.py index c577159884d..b67c8a81f6b 100644 --- a/sdk/python/feast/infra/online_stores/dynamodb.py +++ b/sdk/python/feast/infra/online_stores/dynamodb.py @@ -14,8 +14,10 @@ import asyncio import contextlib import itertools +import json import logging from collections import OrderedDict, defaultdict +from concurrent.futures import ThreadPoolExecutor from datetime import datetime from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union @@ -23,13 +25,17 @@ from pydantic import StrictBool, StrictStr from feast import Entity, FeatureView, utils -from feast.infra.online_stores.helpers import compute_entity_id +from feast.infra.infra_object import InfraObject +from feast.infra.online_stores.helpers import compute_entity_id, compute_versioned_name from feast.infra.online_stores.online_store import OnlineStore from feast.infra.supported_async_methods import SupportedAsyncMethods from feast.infra.utils.aws_utils import dynamo_write_items_async +from feast.protos.feast.core.InfraObject_pb2 import InfraObject as InfraObjectProto +from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel, RepoConfig +from feast.stream_feature_view import StreamFeatureView from feast.utils import get_user_agent try: @@ -53,11 +59,13 @@ class DynamoDBOnlineStoreConfig(FeastConfigBaseModel): type: Literal["dynamodb"] = "dynamodb" """Online store type selector""" - batch_size: int = 40 - """Number of items to retrieve in a DynamoDB BatchGetItem call.""" + batch_size: int = 100 + """Number of items to retrieve in a DynamoDB BatchGetItem call. + DynamoDB supports up to 100 items per BatchGetItem request.""" endpoint_url: Union[str, None] = None - """DynamoDB local development endpoint Url, i.e. http://localhost:8000""" + """DynamoDB endpoint URL. Use for local development (e.g., http://localhost:8000) + or VPC endpoints for improved latency.""" region: StrictStr """AWS Region Name""" @@ -74,30 +82,37 @@ class DynamoDBOnlineStoreConfig(FeastConfigBaseModel): session_based_auth: bool = False """AWS session based client authentication""" - max_pool_connections: int = 10 - """Max number of connections for async Dynamodb operations""" + max_read_workers: int = 10 + """Maximum number of parallel threads for batch read operations. + Higher values improve throughput for large batch reads but increase resource usage.""" - keepalive_timeout: float = 12.0 - """Keep-alive timeout in seconds for async Dynamodb connections.""" + max_pool_connections: int = 50 + """Max number of connections for async Dynamodb operations. + Increase for high-throughput workloads.""" - connect_timeout: Union[int, float] = 60 + keepalive_timeout: float = 30.0 + """Keep-alive timeout in seconds for async Dynamodb connections. + Higher values help reuse connections under sustained load.""" + + connect_timeout: Union[int, float] = 5 """The time in seconds until a timeout exception is thrown when attempting to make - an async connection.""" + an async connection. Lower values enable faster failure detection.""" - read_timeout: Union[int, float] = 60 + read_timeout: Union[int, float] = 10 """The time in seconds until a timeout exception is thrown when attempting to read - from an async connection.""" + from an async connection. Lower values enable faster failure detection.""" - total_max_retry_attempts: Union[int, None] = None + total_max_retry_attempts: Union[int, None] = 3 """Maximum number of total attempts that will be made on a single request. Maps to `retries.total_max_attempts` in botocore.config.Config. """ - retry_mode: Union[Literal["legacy", "standard", "adaptive"], None] = None + retry_mode: Union[Literal["legacy", "standard", "adaptive"], None] = "adaptive" """The type of retry mode (aio)botocore should use. Maps to `retries.mode` in botocore.config.Config. + 'adaptive' mode provides intelligent retry with client-side rate limiting. """ @@ -111,16 +126,22 @@ class DynamoDBOnlineStore(OnlineStore): _aioboto_session: Async boto session. _aioboto_client: Async boto client. _aioboto_context_stack: Async context stack. + _type_deserializer: Cached TypeDeserializer instance for performance. """ _dynamodb_client = None _dynamodb_resource = None + # Class-level cached TypeDeserializer to avoid per-request instantiation + _type_deserializer: Optional[TypeDeserializer] = None def __init__(self): super().__init__() self._aioboto_session = None self._aioboto_client = None self._aioboto_context_stack = None + # Initialize cached TypeDeserializer if not already done + if DynamoDBOnlineStore._type_deserializer is None: + DynamoDBOnlineStore._type_deserializer = TypeDeserializer() async def initialize(self, config: RepoConfig): online_config = config.online_store @@ -133,6 +154,7 @@ async def initialize(self, config: RepoConfig): online_config.read_timeout, online_config.total_max_retry_attempts, online_config.retry_mode, + online_config.endpoint_url, ) async def close(self): @@ -153,6 +175,7 @@ async def _get_aiodynamodb_client( read_timeout: Union[int, float], total_max_retry_attempts: Union[int, None], retry_mode: Union[Literal["legacy", "standard", "adaptive"], None], + endpoint_url: Optional[str] = None, ): if self._aioboto_client is None: logger.debug("initializing the aiobotocore dynamodb client") @@ -163,16 +186,23 @@ async def _get_aiodynamodb_client( if retry_mode is not None: retries["mode"] = retry_mode - client_context = self._get_aioboto_session().create_client( - "dynamodb", - region_name=region, - config=AioConfig( + # Build client kwargs, including endpoint_url for VPC endpoints or local testing + client_kwargs: Dict[str, Any] = { + "region_name": region, + "config": AioConfig( max_pool_connections=max_pool_connections, connect_timeout=connect_timeout, read_timeout=read_timeout, retries=retries if retries else None, connector_args={"keepalive_timeout": keepalive_timeout}, ), + } + if endpoint_url: + client_kwargs["endpoint_url"] = endpoint_url + + client_context = self._get_aioboto_session().create_client( + "dynamodb", + **client_kwargs, ) self._aioboto_context_stack = contextlib.AsyncExitStack() self._aioboto_client = ( @@ -181,12 +211,10 @@ async def _get_aiodynamodb_client( return self._aioboto_client async def _aiodynamodb_close(self): - if self._aioboto_client: - await self._aioboto_client.close() - self._aioboto_client = None if self._aioboto_context_stack: await self._aioboto_context_stack.aclose() self._aioboto_context_stack = None + self._aioboto_client = None if self._aioboto_session: self._aioboto_session = None @@ -213,18 +241,45 @@ def _table_tags(online_config, table_instance) -> list[dict[str, str]]: @staticmethod def _update_tags(dynamodb_client, table_name: str, new_tags: list[dict[str, str]]): + """Update DynamoDB table tags using a diff-based approach. + + Instead of removing all tags and re-adding them (which is vulnerable to + the eventual-consistency window between UntagResource and TagResource), + this method computes the minimal set of changes needed: + + - Only removes tags that are no longer present in new_tags. + - Only adds/updates tags whose value has changed or that are new. + + This avoids the race condition described in + https://github.com/feast-dev/feast/issues/6418 where calling + TagResource immediately after UntagResource can leave a table with no + tags due to DynamoDB's asynchronous tag operations. + """ table_arn = dynamodb_client.describe_table(TableName=table_name)["Table"][ "TableArn" ] current_tags = dynamodb_client.list_tags_of_resource(ResourceArn=table_arn)[ "Tags" ] - if current_tags: - remove_keys = [tag["Key"] for tag in current_tags] - dynamodb_client.untag_resource(ResourceArn=table_arn, TagKeys=remove_keys) - if new_tags: - dynamodb_client.tag_resource(ResourceArn=table_arn, Tags=new_tags) + current_tag_map = {tag["Key"]: tag["Value"] for tag in current_tags} + new_tag_map = {tag["Key"]: tag["Value"] for tag in new_tags} + + # Remove only tags that are no longer in new_tags + keys_to_remove = [k for k in current_tag_map if k not in new_tag_map] + # Add / update only tags whose value is new or has changed + tags_to_add = [ + {"Key": k, "Value": v} + for k, v in new_tag_map.items() + if current_tag_map.get(k) != v + ] + + if keys_to_remove: + dynamodb_client.untag_resource( + ResourceArn=table_arn, TagKeys=keys_to_remove + ) + if tags_to_add: + dynamodb_client.tag_resource(ResourceArn=table_arn, Tags=tags_to_add) def update( self, @@ -329,6 +384,31 @@ def update( _get_table_name(online_config, config, table_to_delete), ) + def plan( + self, config: RepoConfig, desired_registry_proto: RegistryProto + ) -> List[InfraObject]: + online_config = config.online_store + assert isinstance(online_config, DynamoDBOnlineStoreConfig) + + # feature_views and stream_feature_views are distinct proto types + # (FeatureViewProto vs StreamFeatureViewProto); each needs its + # matching from_proto(), not one applied to both indiscriminately. + views = [ + FeatureView.from_proto(view) + for view in desired_registry_proto.feature_views + ] + [ + StreamFeatureView.from_proto(view) + for view in desired_registry_proto.stream_feature_views + ] + return [ + DynamoDBTable( + name=_get_table_name(online_config, config, view), + region=online_config.region, + endpoint_url=online_config.endpoint_url, + ) + for view in views + ] + def teardown( self, config: RepoConfig, @@ -431,6 +511,7 @@ async def online_write_batch_async( online_config.read_timeout, online_config.total_max_retry_attempts, online_config.retry_mode, + online_config.endpoint_url, ) await dynamo_write_items_async(client, table_name, items) @@ -448,6 +529,7 @@ def online_read( config: The RepoConfig for the current FeatureStore. table: Feast FeatureView. entity_keys: a list of entity keys that should be read from the FeatureStore. + requested_features: Optional list of feature names to retrieve. """ online_config = config.online_store assert isinstance(online_config, DynamoDBOnlineStoreConfig) @@ -457,31 +539,71 @@ def online_read( online_config.endpoint_url, online_config.session_based_auth, ) - table_instance = dynamodb_resource.Table( - _get_table_name(online_config, config, table) - ) + table_name = _get_table_name(online_config, config, table) batch_size = online_config.batch_size entity_ids = self._to_entity_ids(config, entity_keys) - entity_ids_iter = iter(entity_ids) - result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] + # Split entity_ids into batches upfront + batches: List[List[str]] = [] + entity_ids_iter = iter(entity_ids) while True: batch = list(itertools.islice(entity_ids_iter, batch_size)) - - # No more items to insert - if len(batch) == 0: + if not batch: break + batches.append(batch) + + if not batches: + return [] + + # For single batch, no parallelization overhead needed + if len(batches) == 1: batch_entity_ids = self._to_resource_batch_get_payload( - online_config, table_instance.name, batch + online_config, table_name, batches[0], requested_features ) - response = dynamodb_resource.batch_get_item( - RequestItems=batch_entity_ids, + response = dynamodb_resource.batch_get_item(RequestItems=batch_entity_ids) + return self._process_batch_get_response(table_name, response, batches[0]) + + # Execute batch requests in parallel for multiple batches + # Note: boto3 clients ARE thread-safe, so we can share a single client + # https://docs.aws.amazon.com/boto3/latest/guide/clients.html#multithreading-or-multiprocessing-with-clients + dynamodb_client = self._get_dynamodb_client( + online_config.region, + online_config.endpoint_url, + online_config.session_based_auth, + ) + + def fetch_batch(batch: List[str]) -> Dict[str, Any]: + batch_entity_ids = self._to_client_batch_get_payload( + online_config, table_name, batch, requested_features ) + return dynamodb_client.batch_get_item(RequestItems=batch_entity_ids) + + # Use ThreadPoolExecutor for parallel I/O + max_workers = min(len(batches), online_config.max_read_workers) + with ThreadPoolExecutor(max_workers=max_workers) as executor: + responses = list(executor.map(fetch_batch, batches)) + + # Process responses and merge results in order + # Client responses need deserialization (unlike resource responses) + if self._type_deserializer is None: + self._type_deserializer = TypeDeserializer() + deserialize = self._type_deserializer.deserialize + + def to_tbl_resp(raw_client_response): + return { + "entity_id": deserialize(raw_client_response["entity_id"]), + "event_ts": deserialize(raw_client_response["event_ts"]), + "values": deserialize(raw_client_response["values"]), + } + + result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] + for batch, response in zip(batches, responses): batch_result = self._process_batch_get_response( - table_instance.name, response, entity_ids, batch + table_name, response, batch, to_tbl_response=to_tbl_resp ) result.extend(batch_result) + return result async def online_read_async( @@ -513,7 +635,10 @@ async def online_read_async( entity_ids_iter = iter(entity_ids) table_name = _get_table_name(online_config, config, table) - deserialize = TypeDeserializer().deserialize + # Use cached TypeDeserializer for better performance + if self._type_deserializer is None: + self._type_deserializer = TypeDeserializer() + deserialize = self._type_deserializer.deserialize def to_tbl_resp(raw_client_response): return { @@ -529,7 +654,7 @@ def to_tbl_resp(raw_client_response): if not batch: break entity_id_batch = self._to_client_batch_get_payload( - online_config, table_name, batch + online_config, table_name, batch, requested_features ) batches.append(batch) entity_id_batches.append(entity_id_batch) @@ -542,6 +667,7 @@ def to_tbl_resp(raw_client_response): online_config.read_timeout, online_config.total_max_retry_attempts, online_config.retry_mode, + online_config.endpoint_url, ) response_batches = await asyncio.gather( *[ @@ -557,7 +683,6 @@ def to_tbl_resp(raw_client_response): result_batch = self._process_batch_get_response( table_name, response, - entity_ids, batch, to_tbl_response=to_tbl_resp, ) @@ -589,26 +714,6 @@ def _get_dynamodb_resource( ) return self._dynamodb_resource - def _sort_dynamodb_response( - self, - responses: list, - order: list, - to_tbl_response: Callable = lambda raw_dict: raw_dict, - ) -> Any: - """DynamoDB Batch Get Item doesn't return items in a particular order.""" - # Assign an index to order - order_with_index = {value: idx for idx, value in enumerate(order)} - # Sort table responses by index - table_responses_ordered: Any = [ - (order_with_index[tbl_res["entity_id"]], tbl_res) - for tbl_res in map(to_tbl_response, responses) - ] - table_responses_ordered = sorted( - table_responses_ordered, key=lambda tup: tup[0] - ) - _, table_responses_ordered = zip(*table_responses_ordered) - return table_responses_ordered - def _write_batch_non_duplicates( self, table_instance, @@ -630,37 +735,77 @@ def _write_batch_non_duplicates( progress(1) def _process_batch_get_response( - self, table_name, response, entity_ids, batch, **sort_kwargs - ): - response = response.get("Responses") - table_responses = response.get(table_name) + self, + table_name: str, + response: Dict[str, Any], + batch: List[str], + to_tbl_response: Callable = lambda raw_dict: raw_dict, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + """Process batch get response using O(1) dictionary lookup. - batch_result = [] - if table_responses: - table_responses = self._sort_dynamodb_response( - table_responses, entity_ids, **sort_kwargs - ) - entity_idx = 0 - for tbl_res in table_responses: - entity_id = tbl_res["entity_id"] - while entity_id != batch[entity_idx]: - batch_result.append((None, None)) - entity_idx += 1 - res = {} - for feature_name, value_bin in tbl_res["values"].items(): + DynamoDB BatchGetItem doesn't return items in a particular order, + so we use a dictionary for O(1) lookup instead of O(n log n) sorting. + + This method: + - Uses dictionary lookup instead of sorting for response ordering + - Pre-allocates the result list with None values + - Minimizes object creation in the hot path + + Args: + table_name: Name of the DynamoDB table + response: Raw response from DynamoDB batch_get_item + batch: List of entity_ids in the order they should be returned + to_tbl_response: Function to transform raw DynamoDB response items + (used for async client responses that need deserialization) + + Returns: + List of (timestamp, features) tuples in the same order as batch + """ + responses_data = response.get("Responses") + if not responses_data: + # No responses at all, return all None tuples + return [(None, None)] * len(batch) + + table_responses = responses_data.get(table_name) + if not table_responses: + # No responses for this table, return all None tuples + return [(None, None)] * len(batch) + + # Build a dictionary for O(1) lookup instead of O(n log n) sorting + response_dict: Dict[str, Any] = { + tbl_res["entity_id"]: tbl_res + for tbl_res in map(to_tbl_response, table_responses) + } + + # Pre-allocate result list with None tuples (faster than appending) + batch_size = len(batch) + result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [ + (None, None) + ] * batch_size + + # Process each entity in batch order using O(1) dict lookup + for idx, entity_id in enumerate(batch): + tbl_res = response_dict.get(entity_id) + if tbl_res is not None: + # Parse feature values + features: Dict[str, ValueProto] = {} + values_data = tbl_res["values"] + for feature_name, value_bin in values_data.items(): val = ValueProto() val.ParseFromString(value_bin.value) - res[feature_name] = val - batch_result.append((datetime.fromisoformat(tbl_res["event_ts"]), res)) - entity_idx += 1 - # Not all entities in a batch may have responses - # Pad with remaining values in batch that were not found - batch_size_nones = ((None, None),) * (len(batch) - len(batch_result)) - batch_result.extend(batch_size_nones) - return batch_result + features[feature_name] = val + + # Parse timestamp and set result + result[idx] = ( + datetime.fromisoformat(tbl_res["event_ts"]), + features, + ) + + return result @staticmethod def _to_entity_ids(config: RepoConfig, entity_keys: List[EntityKeyProto]): + """Convert entity keys to entity IDs.""" return [ compute_entity_id( entity_key, @@ -670,23 +815,392 @@ def _to_entity_ids(config: RepoConfig, entity_keys: List[EntityKeyProto]): ] @staticmethod - def _to_resource_batch_get_payload(online_config, table_name, batch): - return { - table_name: { - "Keys": [{"entity_id": entity_id} for entity_id in batch], - "ConsistentRead": online_config.consistent_reads, - } + def _to_resource_batch_get_payload( + online_config, table_name, batch, requested_features=None + ): + payload: Dict[str, Any] = { + "Keys": [{"entity_id": entity_id} for entity_id in batch], + "ConsistentRead": online_config.consistent_reads, } + projection = DynamoDBOnlineStore._build_projection_expression( + requested_features + ) + if projection: + payload["ProjectionExpression"] = projection["ProjectionExpression"] + payload["ExpressionAttributeNames"] = projection["ExpressionAttributeNames"] + return {table_name: payload} + + @staticmethod + def _to_client_batch_get_payload( + online_config, table_name, batch, requested_features=None + ): + payload: Dict[str, Any] = { + "Keys": [{"entity_id": {"S": entity_id}} for entity_id in batch], + "ConsistentRead": online_config.consistent_reads, + } + projection = DynamoDBOnlineStore._build_projection_expression( + requested_features + ) + if projection: + payload["ProjectionExpression"] = projection["ProjectionExpression"] + payload["ExpressionAttributeNames"] = projection["ExpressionAttributeNames"] + return {table_name: payload} @staticmethod - def _to_client_batch_get_payload(online_config, table_name, batch): + def _build_projection_expression( + requested_features: Optional[List[str]], + ) -> Optional[Dict[str, Any]]: + if not requested_features: + return None + attr_names: Dict[str, str] = { + "#entity_id": "entity_id", + "#event_ts": "event_ts", + "#vals": "values", + } + projections = ["#entity_id", "#event_ts"] + for i, feat in enumerate(requested_features): + alias = f"#feat{i}" + attr_names[alias] = feat + projections.append(f"#vals.{alias}") return { - table_name: { - "Keys": [{"entity_id": {"S": entity_id}} for entity_id in batch], - "ConsistentRead": online_config.consistent_reads, + "ProjectionExpression": ", ".join(projections), + "ExpressionAttributeNames": attr_names, + } + + def update_online_store( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + update_expressions: Dict[str, str], + progress: Optional[Callable[[int], Any]] = None, + ) -> None: + """ + Update features in DynamoDB using UpdateItem with custom UpdateExpression. + + This method provides DynamoDB-specific list update functionality using + native UpdateItem operations with list_append and other expressions. + + Args: + config: The RepoConfig for the current FeatureStore. + table: Feast FeatureView. + data: Feature data to update. Each tuple contains an entity key, + feature values, event timestamp, and optional created timestamp. + update_expressions: Dict mapping feature names to DynamoDB update expressions. + Examples: + - "transactions": "list_append(transactions, :new_val)" + - "recent_items": "list_append(:new_val, recent_items)" # prepend + progress: Optional progress callback function. + """ + online_config = config.online_store + assert isinstance(online_config, DynamoDBOnlineStoreConfig) + + dynamodb_resource = self._get_dynamodb_resource( + online_config.region, + online_config.endpoint_url, + online_config.session_based_auth, + ) + + table_instance = dynamodb_resource.Table( + _get_table_name(online_config, config, table) + ) + + # Process each entity update + for entity_key, features, timestamp, _ in _latest_data_to_write(data): + entity_id = compute_entity_id( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + + self._update_item_with_expression( + table_instance, + entity_id, + features, + timestamp, + update_expressions, + config, + ) + + if progress: + progress(1) + + async def update_online_store_async( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + update_expressions: Dict[str, str], + progress: Optional[Callable[[int], Any]] = None, + ) -> None: + """ + Async version of update_online_store. + """ + online_config = config.online_store + assert isinstance(online_config, DynamoDBOnlineStoreConfig) + + table_name = _get_table_name(online_config, config, table) + client = await self._get_aiodynamodb_client( + online_config.region, + online_config.max_pool_connections, + online_config.keepalive_timeout, + online_config.connect_timeout, + online_config.read_timeout, + online_config.total_max_retry_attempts, + online_config.retry_mode, + online_config.endpoint_url, + ) + + # Process each entity update + for entity_key, features, timestamp, _ in _latest_data_to_write(data): + entity_id = compute_entity_id( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + + await self._update_item_with_expression_async( + client, + table_name, + entity_id, + features, + timestamp, + update_expressions, + config, + ) + + if progress: + progress(1) + + def _update_item_with_expression( + self, + table_instance, + entity_id: str, + features: Dict[str, ValueProto], + timestamp: datetime, + update_expressions: Dict[str, str], + config: RepoConfig, + ): + """Execute DynamoDB UpdateItem with list operations via read-modify-write.""" + # Read existing item to get current values for list operations + existing_values: Dict[str, ValueProto] = {} + item_exists = False + try: + response = table_instance.get_item(Key={"entity_id": entity_id}) + if "Item" in response: + item_exists = True + if "values" in response["Item"]: + for feat_name, val_bin in response["Item"]["values"].items(): + val = ValueProto() + val.ParseFromString(val_bin.value) + existing_values[feat_name] = val + except ClientError: + pass + + # Build final feature values by applying list operations + final_features: Dict[str, ValueProto] = {} + for feature_name, value_proto in features.items(): + if feature_name in update_expressions: + final_features[feature_name] = self._apply_list_operation( + existing_values.get(feature_name), + value_proto, + update_expressions[feature_name], + ) + else: + final_features[feature_name] = value_proto + + # For new items, use put_item + if not item_exists: + item = { + "entity_id": entity_id, + "event_ts": str(utils.make_tzaware(timestamp)), + "values": {k: v.SerializeToString() for k, v in final_features.items()}, + } + table_instance.put_item(Item=item) + return + + # Build UpdateExpression for existing items + update_expr_parts: list[str] = [] + expression_attribute_values: Dict[str, Any] = {} + expression_attribute_names: Dict[str, str] = { + "#values": "values", + "#event_ts": "event_ts", + } + + update_expr_parts.append("#event_ts = :event_ts") + expression_attribute_values[":event_ts"] = str(utils.make_tzaware(timestamp)) + + for feature_name, value_proto in final_features.items(): + feat_attr = f"#feat_{feature_name}" + val_name = f":val_{feature_name}" + expression_attribute_names[feat_attr] = feature_name + expression_attribute_values[val_name] = value_proto.SerializeToString() # type: ignore[assignment] + update_expr_parts.append(f"#values.{feat_attr} = {val_name}") + + try: + table_instance.update_item( + Key={"entity_id": entity_id}, + UpdateExpression="SET " + ", ".join(update_expr_parts), + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + except ClientError as e: + logger.error(f"Failed to update item {entity_id}: {e}") + raise + + def _apply_list_operation( + self, existing: Optional[ValueProto], new_value: ValueProto, update_expr: str + ) -> ValueProto: + """Apply list operation (append/prepend) and return merged ValueProto.""" + result = ValueProto() + is_prepend = update_expr.strip().startswith("list_append(:new_val") + existing_list = self._extract_list_values(existing) if existing else [] + new_list = self._extract_list_values(new_value) + merged = new_list + existing_list if is_prepend else existing_list + new_list + self._set_list_values(result, new_value, merged) + return result + + def _extract_list_values(self, value_proto: ValueProto) -> list: + """Extract list values from ValueProto.""" + if value_proto.HasField("string_list_val"): + return list(value_proto.string_list_val.val) + elif value_proto.HasField("int32_list_val"): + return list(value_proto.int32_list_val.val) + elif value_proto.HasField("int64_list_val"): + return list(value_proto.int64_list_val.val) + elif value_proto.HasField("float_list_val"): + return list(value_proto.float_list_val.val) + elif value_proto.HasField("double_list_val"): + return list(value_proto.double_list_val.val) + elif value_proto.HasField("bool_list_val"): + return list(value_proto.bool_list_val.val) + elif value_proto.HasField("bytes_list_val"): + return list(value_proto.bytes_list_val.val) + elif value_proto.HasField("map_list_val"): + return list(value_proto.map_list_val.val) + elif value_proto.HasField("json_list_val"): + return list(value_proto.json_list_val.val) + elif value_proto.HasField("struct_list_val"): + return list(value_proto.struct_list_val.val) + return [] + + def _set_list_values( + self, result: ValueProto, template: ValueProto, values: list + ) -> None: + """Set list values on result ValueProto based on template type.""" + if template.HasField("string_list_val"): + result.string_list_val.val.extend(values) + elif template.HasField("int32_list_val"): + result.int32_list_val.val.extend(values) + elif template.HasField("int64_list_val"): + result.int64_list_val.val.extend(values) + elif template.HasField("float_list_val"): + result.float_list_val.val.extend(values) + elif template.HasField("double_list_val"): + result.double_list_val.val.extend(values) + elif template.HasField("bool_list_val"): + result.bool_list_val.val.extend(values) + elif template.HasField("bytes_list_val"): + result.bytes_list_val.val.extend(values) + elif template.HasField("map_list_val"): + result.map_list_val.val.extend(values) + elif template.HasField("json_list_val"): + result.json_list_val.val.extend(values) + elif template.HasField("struct_list_val"): + result.struct_list_val.val.extend(values) + + async def _update_item_with_expression_async( + self, + client, + table_name: str, + entity_id: str, + features: Dict[str, ValueProto], + timestamp: datetime, + update_expressions: Dict[str, str], + config: RepoConfig, + ): + """Async version of _update_item_with_expression.""" + # Read existing item + existing_values: Dict[str, ValueProto] = {} + item_exists = False + try: + response = await client.get_item( + TableName=table_name, Key={"entity_id": {"S": entity_id}} + ) + if "Item" in response: + item_exists = True + if "values" in response["Item"] and "M" in response["Item"]["values"]: + for feat_name, val_data in response["Item"]["values"]["M"].items(): + if "B" in val_data: + val = ValueProto() + val.ParseFromString(val_data["B"]) + existing_values[feat_name] = val + except ClientError: + pass + + # Build final feature values + final_features: Dict[str, ValueProto] = {} + for feature_name, value_proto in features.items(): + if feature_name in update_expressions: + final_features[feature_name] = self._apply_list_operation( + existing_values.get(feature_name), + value_proto, + update_expressions[feature_name], + ) + else: + final_features[feature_name] = value_proto + + # For new items, use put_item + if not item_exists: + item = { + "entity_id": {"S": entity_id}, + "event_ts": {"S": str(utils.make_tzaware(timestamp))}, + "values": { + "M": { + k: {"B": v.SerializeToString()} + for k, v in final_features.items() + } + }, } + await client.put_item(TableName=table_name, Item=item) + return + + # Build UpdateExpression for existing items + update_expr_parts: list[str] = [] + expression_attribute_values: Dict[str, Any] = {} + expression_attribute_names: Dict[str, str] = { + "#values": "values", + "#event_ts": "event_ts", } + update_expr_parts.append("#event_ts = :event_ts") + expression_attribute_values[":event_ts"] = { + "S": str(utils.make_tzaware(timestamp)) + } + + for feature_name, value_proto in final_features.items(): + feat_attr = f"#feat_{feature_name}" + val_name = f":val_{feature_name}" + expression_attribute_names[feat_attr] = feature_name + expression_attribute_values[val_name] = { + "B": value_proto.SerializeToString() + } + update_expr_parts.append(f"#values.{feat_attr} = {val_name}") + + try: + await client.update_item( + TableName=table_name, + Key={"entity_id": {"S": entity_id}}, + UpdateExpression="SET " + ", ".join(update_expr_parts), + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + except ClientError as e: + logger.error(f"Failed to update item {entity_id}: {e}") + raise + # Global async client functions removed - now using instance methods @@ -730,11 +1244,76 @@ def _initialize_dynamodb_resource( def _get_table_name( online_config: DynamoDBOnlineStoreConfig, config: RepoConfig, table: FeatureView ) -> str: + table_name = compute_versioned_name( + table, config.registry.enable_online_feature_view_versioning + ) return online_config.table_name_template.format( - project=config.project, table_name=table.name + project=config.project, table_name=table_name ) +DYNAMODB_INFRA_OBJECT_CLASS_TYPE = "feast.infra.online_stores.dynamodb.DynamoDBTable" + + +class DynamoDBTable(InfraObject): + """ + A DynamoDB table managed by Feast, reported by DynamoDBOnlineStore.plan() + so `feast plan` can show DynamoDB table changes. + + Uses the InfraObject proto's CustomInfra field rather than a dedicated + proto message, since that field exists precisely to let online stores + add InfraObject support without changing the core InfraObject proto. + + Note: `feast apply` does not call update()/teardown() on this object -- + DynamoDBOnlineStore.update()/teardown() perform the actual table + creation and deletion directly (see FeatureStore._should_use_plan(), + which gates the new diff-based apply path to the local/sqlite provider + only). update()/teardown() here are no-ops that satisfy InfraObject's + abstract interface. + """ + + def __init__(self, name: str, region: str, endpoint_url: Optional[str] = None): + super().__init__(name) + self.region = region + self.endpoint_url = endpoint_url + + def to_infra_object_proto(self) -> InfraObjectProto: + return InfraObjectProto( + infra_object_class_type=DYNAMODB_INFRA_OBJECT_CLASS_TYPE, + custom_infra=InfraObjectProto.CustomInfra(field=self.to_proto()), + ) + + def to_proto(self) -> bytes: + return json.dumps( + { + "name": self.name, + "region": self.region, + "endpoint_url": self.endpoint_url, + } + ).encode("utf-8") + + @staticmethod + def from_infra_object_proto( + infra_object_proto: InfraObjectProto, + ) -> "DynamoDBTable": + return DynamoDBTable.from_proto(infra_object_proto.custom_infra.field) + + @staticmethod + def from_proto(serialized: bytes) -> "DynamoDBTable": + payload = json.loads(serialized.decode("utf-8")) + return DynamoDBTable( + name=payload["name"], + region=payload["region"], + endpoint_url=payload.get("endpoint_url"), + ) + + def update(self) -> None: + pass + + def teardown(self) -> None: + pass + + def _delete_table_idempotent( dynamodb_resource, table_name: str, diff --git a/sdk/python/feast/infra/online_stores/elasticsearch_online_store/elasticsearch.py b/sdk/python/feast/infra/online_stores/elasticsearch_online_store/elasticsearch.py index 7e8e533281d..e29ba9df49a 100644 --- a/sdk/python/feast/infra/online_stores/elasticsearch_online_store/elasticsearch.py +++ b/sdk/python/feast/infra/online_stores/elasticsearch_online_store/elasticsearch.py @@ -5,11 +5,18 @@ import logging from collections import defaultdict from datetime import datetime -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union from elasticsearch import Elasticsearch, helpers from feast import Entity, FeatureView, RepoConfig +from feast.filter_models import ( + ComparisonFilter, + CompoundFilter, + FilterTranslator, + FilterType, + filters_contain_numeric_comparison, +) from feast.infra.key_encoding_utils import ( deserialize_entity_key, get_list_val_str, @@ -45,10 +52,106 @@ class ElasticSearchOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig): # The number of rows to write in a single batch write_batch_size: Optional[int] = 40 + enable_openai_compatible_store: Optional[bool] = False + + +logger = logging.getLogger(__name__) + + +class ElasticsearchFilterTranslator(FilterTranslator): + """Translates Feast filters into Elasticsearch Query DSL clauses.""" + + def __init__(self, has_value_num: bool = False): + self.has_value_num = has_value_num + + def translate(self, filters: FilterType) -> List[Dict[str, Any]]: + if filters is None: + return [] + return [self._dispatch(filters)] + + def translate_comparison(self, f: ComparisonFilter) -> Dict[str, Any]: + is_numeric = isinstance(f.value, (int, float)) and not isinstance(f.value, bool) + is_numeric_list = ( + isinstance(f.value, list) + and f.value + and isinstance(f.value[0], (int, float)) + and not isinstance(f.value[0], bool) + ) + + if self.has_value_num and (is_numeric or is_numeric_list): + field = f"{f.key}.value_num" + exact_field = field + fmt_val = f.value + fmt_list = f.value if is_numeric_list else None + else: + exact_field = f"{f.key}.value_text.keyword" + field = f"{f.key}.value_text" + fmt_val = str(f.value) + fmt_list = [str(v) for v in f.value] if isinstance(f.value, list) else None + + if f.type == "eq": + return {"term": {exact_field: fmt_val}} + elif f.type == "ne": + return {"bool": {"must_not": [{"term": {exact_field: fmt_val}}]}} + elif f.type in ("gt", "gte", "lt", "lte"): + return {"range": {field: {f.type: fmt_val}}} + elif f.type == "in": + if not isinstance(f.value, list): + raise ValueError( + f"'in' filter requires a list value, got {type(f.value)}" + ) + return {"terms": {exact_field: fmt_list}} + elif f.type == "nin": + if not isinstance(f.value, list): + raise ValueError( + f"'nin' filter requires a list value, got {type(f.value)}" + ) + return {"bool": {"must_not": [{"terms": {exact_field: fmt_list}}]}} + raise ValueError(f"Unsupported comparison operator: {f.type}") + + def translate_compound(self, f: CompoundFilter) -> Dict[str, Any]: + clauses = [self._dispatch(sub) for sub in f.filters] + if f.type == "and": + return {"bool": {"must": clauses}} + else: + return {"bool": {"should": clauses, "minimum_should_match": 1}} class ElasticSearchOnlineStore(OnlineStore): _client: Optional[Elasticsearch] = None + _index_value_num_cache: Optional[Dict[str, bool]] = None + + def _index_has_value_num(self, config: RepoConfig, index_name: str) -> bool: + """Check the actual ES index mapping for the value_num field. + + Caches the result per index so we only hit ES once. + """ + if self._index_value_num_cache is None: + self._index_value_num_cache = {} + if index_name in self._index_value_num_cache: + return self._index_value_num_cache[index_name] + try: + mapping = self._get_client(config).indices.get_mapping(index=index_name) + templates = ( + mapping.get(index_name, {}) + .get("mappings", {}) + .get("dynamic_templates", []) + ) + for tmpl in templates: + for _, tmpl_body in tmpl.items(): + props = tmpl_body.get("mapping", {}).get("properties", {}) + if "value_num" in props: + self._index_value_num_cache[index_name] = True + return True + except Exception as e: + logging.warning( + "Failed to check index mapping for value_num on '%s': %s: %s", + index_name, + type(e).__name__, + e, + ) + self._index_value_num_cache[index_name] = False + return False def _get_client(self, config: RepoConfig) -> Elasticsearch: online_store_config = config.online_store @@ -94,6 +197,7 @@ def online_write_batch( progress: Optional[Callable[[int], Any]], ) -> None: insert_values = [] + include_value_num = self._index_has_value_num(config, table.name) grouped_docs: dict[str, dict[str, Any]] = defaultdict( lambda: { "features": {}, @@ -115,7 +219,7 @@ def online_write_batch( doc_key = f"{encoded_entity_key}_{timestamp}" for feature_name, value in values.items(): - doc = _encode_feature_value(value) + doc = _encode_feature_value(value, include_value_num=include_value_num) grouped_docs[doc_key]["features"][feature_name] = doc grouped_docs[doc_key]["timestamp"] = timestamp grouped_docs[doc_key]["created_ts"] = created_ts @@ -209,6 +313,23 @@ def create_index(self, config: RepoConfig, table: FeatureView): _get_feature_view_vector_field_metadata(table), "vector_length", 512 ) + feature_properties: Dict[str, Any] = { + "feature_value": {"type": "binary"}, + "value_text": { + "type": "text", + "fields": {"keyword": {"type": "keyword", "ignore_above": 256}}, + }, + "vector_value": { + "type": "dense_vector", + "dims": vector_field_length, + "index": True, + "similarity": config.online_store.similarity, + }, + } + + if getattr(config.online_store, "enable_openai_compatible_store", False): + feature_properties["value_num"] = {"type": "double"} + index_mapping = { "dynamic_templates": [ { @@ -217,16 +338,7 @@ def create_index(self, config: RepoConfig, table: FeatureView): "match": "*", "mapping": { "type": "object", - "properties": { - "feature_value": {"type": "binary"}, - "value_text": {"type": "text"}, - "vector_value": { - "type": "dense_vector", - "dims": vector_field_length, - "index": True, - "similarity": config.online_store.similarity, - }, - }, + "properties": feature_properties, }, } } @@ -279,8 +391,7 @@ def retrieve_online_documents( requested_features: List[str], embedding: List[float], top_k: int, - *args, - **kwargs, + distance_metric: Optional[str] = None, ) -> List[ Tuple[ Optional[datetime], @@ -349,6 +460,8 @@ def retrieve_online_documents_v2( top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> List[ Tuple[ Optional[datetime], @@ -383,6 +496,27 @@ def retrieve_online_documents_v2( source_fields += composite_key_name body["_source"] = source_fields + has_value_num = self._index_has_value_num(config, es_index) + + if ( + filters + and filters_contain_numeric_comparison(filters) + and not has_value_num + ): + logger.warning( + "Numeric comparison filters (gt, gte, lt, lte) are being used " + "but this index does not have a 'value_num' field. Numeric " + "fields are stored as text, which causes lexicographic " + "comparison instead of numeric comparison (e.g. '9' > '100'). " + "To fix this, set 'enable_openai_compatible_store: true' in " + "your online_store config, then teardown and re-apply your " + "feature store to recreate indices with the value_num field." + ) + + metadata_filters = ElasticsearchFilterTranslator(has_value_num).translate( + filters + ) + if embedding: similarity = (distance_metric or config.online_store.similarity).lower() vector_field_path = ( @@ -399,37 +533,47 @@ def retrieve_online_documents_v2( f"Unsupported similarity/distance_metric: {similarity}" ) - # Hybrid search if embedding and query_string: + bool_clause: Dict[str, Any] = { + "must": [ + {"query_string": {"query": f'"{query_string}"'}}, + {"exists": {"field": vector_field_path}}, + ] + } + if metadata_filters: + bool_clause["filter"] = metadata_filters body["query"] = { "script_score": { - "query": { - "bool": { - "must": [ - {"query_string": {"query": f'"{query_string}"'}}, - {"exists": {"field": vector_field_path}}, - ] - } - }, + "query": {"bool": bool_clause}, "script": { "source": script, "params": {"query_vector": embedding}, }, } } - # Vector search only elif embedding: + filter_clauses: List[Dict[str, Any]] = [ + {"exists": {"field": vector_field_path}} + ] + filter_clauses.extend(metadata_filters) body["query"] = { "script_score": { - "query": { - "bool": {"filter": [{"exists": {"field": vector_field_path}}]} - }, + "query": {"bool": {"filter": filter_clauses}}, "script": {"source": script, "params": {"query_vector": embedding}}, } } - # Keyword search only elif query_string: - body["query"] = {"query_string": {"query": f'"{query_string}"'}} + if metadata_filters: + body["query"] = { + "bool": { + "must": [ + {"query_string": {"query": f'"{query_string}"'}}, + ], + "filter": metadata_filters, + } + } + else: + body["query"] = {"query_string": {"query": f'"{query_string}"'}} response = self._get_client(config).search(index=es_index, body=body) @@ -443,7 +587,6 @@ def retrieve_online_documents_v2( timestamp = row["_source"]["timestamp"] timestamp = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f") - # Create feature dict with all requested features feature_dict = {"distance": _to_value_proto(float(row["_score"]))} if query_string is not None: feature_dict["text_rank"] = _to_value_proto(float(row["_score"])) @@ -468,14 +611,14 @@ def _to_value_proto(value: Any) -> ValueProto: val_proto = ValueProto() if isinstance(value, ValueProto): return value - if isinstance(value, float): + if isinstance(value, bool): + val_proto.bool_val = value + elif isinstance(value, float): val_proto.float_val = value elif isinstance(value, str): val_proto.string_val = value elif isinstance(value, int): val_proto.int64_val = value - elif isinstance(value, bool): - val_proto.bool_val = value elif isinstance(value, list) and all(isinstance(v, float) for v in value): val_proto.float_list_val.val.extend(value) elif isinstance(value, dict) and "feature_value" in value: @@ -489,12 +632,15 @@ def _to_value_proto(value: Any) -> ValueProto: return val_proto -def _encode_feature_value(value: ValueProto) -> Dict[str, Any]: +def _encode_feature_value( + value: ValueProto, + include_value_num: bool = False, +) -> Dict[str, Any]: """ Encode a ValueProto into a dictionary for Elasticsearch storage. """ encoded_value = base64.b64encode(value.SerializeToString()).decode("utf-8") - result = {"feature_value": encoded_value} + result: Dict[str, Any] = {"feature_value": encoded_value} vector_val = get_list_val_str(value) if vector_val: @@ -505,8 +651,24 @@ def _encode_feature_value(value: ValueProto) -> Dict[str, Any]: result["value_text"] = value.bytes_val.decode("utf-8") elif value.HasField("int64_val"): result["value_text"] = str(value.int64_val) + if include_value_num: + result["value_num"] = value.int64_val + elif value.HasField("int32_val"): + result["value_text"] = str(value.int32_val) + if include_value_num: + result["value_num"] = value.int32_val elif value.HasField("double_val"): result["value_text"] = str(value.double_val) + if include_value_num: + result["value_num"] = value.double_val + elif value.HasField("float_val"): + result["value_text"] = str(value.float_val) + if include_value_num: + result["value_num"] = value.float_val + elif value.HasField("bool_val"): + result["value_text"] = str(value.bool_val) + if include_value_num: + result["value_num"] = 1.0 if value.bool_val else 0.0 return result diff --git a/sdk/python/feast/infra/online_stores/elasticsearch_online_store/elasticsearch_repo_configuration.py b/sdk/python/feast/infra/online_stores/elasticsearch_online_store/elasticsearch_repo_configuration.py index 4d1f2c3ca18..46e8ba7742e 100644 --- a/sdk/python/feast/infra/online_stores/elasticsearch_online_store/elasticsearch_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/elasticsearch_online_store/elasticsearch_repo_configuration.py @@ -1,7 +1,7 @@ -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.elasticsearch import ( +from tests.universal.feature_repos.universal.online_store.elasticsearch import ( ElasticSearchOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/faiss_online_store.py b/sdk/python/feast/infra/online_stores/faiss_online_store.py index 4b666f60f40..dfa7d6c376b 100644 --- a/sdk/python/feast/infra/online_stores/faiss_online_store.py +++ b/sdk/python/feast/infra/online_stores/faiss_online_store.py @@ -8,6 +8,7 @@ from feast import Entity, FeatureView, RepoConfig from feast.infra.key_encoding_utils import serialize_entity_key +from feast.infra.online_stores.helpers import compute_table_id from feast.infra.online_stores.online_store import OnlineStore from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto @@ -43,16 +44,21 @@ def teardown(self): self.entity_keys = {} +def _table_id(project: str, table: FeatureView, enable_versioning: bool = False) -> str: + return compute_table_id(project, table, enable_versioning) + + class FaissOnlineStore(OnlineStore): - _index: Optional[faiss.IndexIVFFlat] = None - _in_memory_store: InMemoryStore = InMemoryStore() - _config: Optional[FaissOnlineStoreConfig] = None _logger: logging.Logger = logging.getLogger(__name__) - def _get_index(self, config: RepoConfig) -> faiss.IndexIVFFlat: - if self._index is None or self._config is None: - raise ValueError("Index is not initialized") - return self._index + def __init__(self): + super().__init__() + self._indices: Dict[str, faiss.IndexIVFFlat] = {} + self._in_memory_stores: Dict[str, InMemoryStore] = {} + self._config: Optional[FaissOnlineStoreConfig] = None + + def _get_index(self, table_key: str) -> Optional[faiss.IndexIVFFlat]: + return self._indices.get(table_key) def update( self, @@ -63,23 +69,31 @@ def update( entities_to_keep: Sequence[Entity], partial: bool, ): - feature_views = tables_to_keep - if not feature_views: - return - - feature_names = [f.name for f in feature_views[0].features] - dimension = len(feature_names) - self._config = FaissOnlineStoreConfig(**config.online_store.dict()) - if self._index is None or not partial: - quantizer = faiss.IndexFlatL2(dimension) - self._index = faiss.IndexIVFFlat(quantizer, dimension, self._config.nlist) - self._index.train( - np.random.rand(self._config.nlist * 100, dimension).astype(np.float32) - ) - self._in_memory_store = InMemoryStore() + versioning = config.registry.enable_online_feature_view_versioning + + for table in tables_to_delete: + table_key = _table_id(config.project, table, versioning) + self._indices.pop(table_key, None) + self._in_memory_stores.pop(table_key, None) + + for table in tables_to_keep: + table_key = _table_id(config.project, table, versioning) + feature_names = [f.name for f in table.features] + dimension = len(feature_names) + + if table_key not in self._indices or not partial: + quantizer = faiss.IndexFlatL2(dimension) + index = faiss.IndexIVFFlat(quantizer, dimension, self._config.nlist) + index.train( + np.random.rand(self._config.nlist * 100, dimension).astype( + np.float32 + ) + ) + self._indices[table_key] = index + self._in_memory_stores[table_key] = InMemoryStore() - self._in_memory_store.update(feature_names, {}) + self._in_memory_stores[table_key].update(feature_names, {}) def teardown( self, @@ -87,8 +101,13 @@ def teardown( tables: Sequence[FeatureView], entities: Sequence[Entity], ): - self._index = None - self._in_memory_store.teardown() + versioning = config.registry.enable_online_feature_view_versioning + for table in tables: + table_key = _table_id(config.project, table, versioning) + self._indices.pop(table_key, None) + store = self._in_memory_stores.pop(table_key, None) + if store is not None: + store.teardown() def online_read( self, @@ -97,7 +116,12 @@ def online_read( entity_keys: List[EntityKeyProto], requested_features: Optional[List[str]] = None, ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: - if self._index is None: + versioning = config.registry.enable_online_feature_view_versioning + table_key = _table_id(config.project, table, versioning) + index = self._get_index(table_key) + in_memory_store = self._in_memory_stores.get(table_key) + + if index is None or in_memory_store is None: return [(None, None)] * len(entity_keys) results: List[Tuple[Optional[datetime], Optional[Dict[str, Any]]]] = [] @@ -105,15 +129,15 @@ def online_read( serialized_key = serialize_entity_key( entity_key, config.entity_key_serialization_version ).hex() - idx = self._in_memory_store.entity_keys.get(serialized_key, -1) + idx = in_memory_store.entity_keys.get(serialized_key, -1) if idx == -1: results.append((None, None)) else: - feature_vector = self._index.reconstruct(int(idx)) + feature_vector = index.reconstruct(int(idx)) feature_dict = { name: ValueProto(double_val=value) for name, value in zip( - self._in_memory_store.feature_names, feature_vector + in_memory_store.feature_names, feature_vector ) } results.append((None, feature_dict)) @@ -128,8 +152,16 @@ def online_write_batch( ], progress: Optional[Callable[[int], Any]], ) -> None: - if self._index is None: - self._logger.warning("Index is not initialized. Skipping write operation.") + versioning = config.registry.enable_online_feature_view_versioning + table_key = _table_id(config.project, table, versioning) + index = self._get_index(table_key) + in_memory_store = self._in_memory_stores.get(table_key) + + if index is None or in_memory_store is None: + self._logger.warning( + "Index for table '%s' is not initialized. Skipping write operation.", + table_key, + ) return feature_vectors = [] @@ -142,7 +174,7 @@ def online_write_batch( feature_vector = np.array( [ feature_dict[name].double_val - for name in self._in_memory_store.feature_names + for name in in_memory_store.feature_names ], dtype=np.float32, ) @@ -153,21 +185,17 @@ def online_write_batch( feature_vectors_array = np.array(feature_vectors) existing_indices = [ - self._in_memory_store.entity_keys.get(sk, -1) for sk in serialized_keys + in_memory_store.entity_keys.get(sk, -1) for sk in serialized_keys ] mask = np.array(existing_indices) != -1 if np.any(mask): - self._index.remove_ids( - np.array([idx for idx in existing_indices if idx != -1]) - ) + index.remove_ids(np.array([idx for idx in existing_indices if idx != -1])) - new_indices = np.arange( - self._index.ntotal, self._index.ntotal + len(feature_vectors_array) - ) - self._index.add(feature_vectors_array) + new_indices = np.arange(index.ntotal, index.ntotal + len(feature_vectors_array)) + index.add(feature_vectors_array) for sk, idx in zip(serialized_keys, new_indices): - self._in_memory_store.entity_keys[sk] = idx + in_memory_store.entity_keys[sk] = idx if progress: progress(len(data)) @@ -176,7 +204,7 @@ def retrieve_online_documents( self, config: RepoConfig, table: FeatureView, - requested_featres: List[str], + requested_features: List[str], embedding: List[float], top_k: int, distance_metric: Optional[str] = None, @@ -189,12 +217,16 @@ def retrieve_online_documents( Optional[ValueProto], ] ]: - if self._index is None: + versioning = config.registry.enable_online_feature_view_versioning + table_key = _table_id(config.project, table, versioning) + index = self._get_index(table_key) + + if index is None: self._logger.warning("Index is not initialized. Returning empty result.") return [] query_vector = np.array(embedding, dtype=np.float32).reshape(1, -1) - distances, indices = self._index.search(query_vector, top_k) + distances, indices = index.search(query_vector, top_k) results: List[ Tuple[ @@ -209,7 +241,7 @@ def retrieve_online_documents( if idx == -1: continue - feature_vector = self._index.reconstruct(int(idx)) + feature_vector = index.reconstruct(int(idx)) timestamp = Timestamp() timestamp.GetCurrentTime() @@ -237,5 +269,4 @@ async def online_read_async( entity_keys: List[EntityKeyProto], requested_features: Optional[List[str]] = None, ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: - # Implement async read if needed raise NotImplementedError("Async read is not implemented for FaissOnlineStore") diff --git a/sdk/python/feast/infra/online_stores/hazelcast_online_store/hazelcast_repo_configuration.py b/sdk/python/feast/infra/online_stores/hazelcast_online_store/hazelcast_repo_configuration.py index 5b3ea6e307b..6157f362120 100644 --- a/sdk/python/feast/infra/online_stores/hazelcast_online_store/hazelcast_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/hazelcast_online_store/hazelcast_repo_configuration.py @@ -14,10 +14,10 @@ # limitations under the License. # -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.hazelcast import ( +from tests.universal.feature_repos.universal.online_store.hazelcast import ( HazelcastOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/hbase_online_store/README.md b/sdk/python/feast/infra/online_stores/hbase_online_store/README.md index 651e4e90b80..cc5b714c713 100644 --- a/sdk/python/feast/infra/online_stores/hbase_online_store/README.md +++ b/sdk/python/feast/infra/online_stores/hbase_online_store/README.md @@ -67,7 +67,6 @@ feature_vector = store.get_online_features( ], ).to_dict() pprint(feature_vector) - ``` #### Output ``` diff --git a/sdk/python/feast/infra/online_stores/hbase_online_store/hbase_repo_configuration.py b/sdk/python/feast/infra/online_stores/hbase_online_store/hbase_repo_configuration.py index 4e32a654b55..d6089c0e3b4 100644 --- a/sdk/python/feast/infra/online_stores/hbase_online_store/hbase_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/hbase_online_store/hbase_repo_configuration.py @@ -1,7 +1,7 @@ -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.hbase import ( +from tests.universal.feature_repos.universal.online_store.hbase import ( HbaseOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/helpers.py b/sdk/python/feast/infra/online_stores/helpers.py index b657bd44d00..eed47fe7837 100644 --- a/sdk/python/feast/infra/online_stores/helpers.py +++ b/sdk/python/feast/infra/online_stores/helpers.py @@ -1,6 +1,6 @@ import struct from datetime import datetime, timezone -from typing import Any, List +from typing import Any, List, Optional, Tuple import mmh3 @@ -70,3 +70,41 @@ def _to_naive_utc(ts: datetime) -> datetime: return ts else: return ts.astimezone(tz=timezone.utc).replace(tzinfo=None) + + +def extract_text_and_num( + val: Any, compute_num: bool +) -> Tuple[Optional[str], Optional[float]]: + """Extract (value_text, value_num) from a ValueProto. + + Used by SQL-based online stores to populate the value_text and optional + value_num columns without duplicating type-dispatch logic. + """ + val_type = val.WhichOneof("val") + if val_type == "string_val": + return val.string_val, None + if val_type in ("int64_val", "int32_val", "double_val", "float_val"): + raw = getattr(val, val_type) + return str(raw), float(raw) if compute_num else None + if val_type == "bool_val": + return str(val.bool_val), ( + 1.0 if val.bool_val else 0.0 + ) if compute_num else None + return None, None + + +def compute_versioned_name(table: Any, enable_versioning: bool = False) -> str: + """Return the table name with a ``_v{N}`` suffix when versioning is enabled.""" + name = table.name + if enable_versioning: + version = getattr(table.projection, "version_tag", None) + if version is None: + version = getattr(table, "current_version_number", None) + if version is not None and version > 0: + name = f"{table.name}_v{version}" + return name + + +def compute_table_id(project: str, table: Any, enable_versioning: bool = False) -> str: + """Build the online-store table name, appending a version suffix when versioning is enabled.""" + return f"{project}_{compute_versioned_name(table, enable_versioning)}" diff --git a/sdk/python/tests/integration/offline_store/test_hybrid_offline_store.py b/sdk/python/feast/infra/online_stores/hybrid_online_store/__init__.py similarity index 100% rename from sdk/python/tests/integration/offline_store/test_hybrid_offline_store.py rename to sdk/python/feast/infra/online_stores/hybrid_online_store/__init__.py diff --git a/sdk/python/feast/infra/online_stores/hybrid_online_store/hybrid_online_store.py b/sdk/python/feast/infra/online_stores/hybrid_online_store/hybrid_online_store.py index e929e039411..8faefdbd344 100644 --- a/sdk/python/feast/infra/online_stores/hybrid_online_store/hybrid_online_store.py +++ b/sdk/python/feast/infra/online_stores/hybrid_online_store/hybrid_online_store.py @@ -21,7 +21,7 @@ Example configuration (feature_store.yaml): online_store: - type: hybrid_online_store.HybridOnlineStore + type: hybrid routing_tag: team # or any tag name you want to use for routing online_stores: - type: feast.infra.online_stores.bigtable.BigtableOnlineStore @@ -64,9 +64,7 @@ class HybridOnlineStoreConfig(FeastConfigBaseModel): online_stores: A list of OnlineStoresWithConfig, each specifying the type and config for an online store backend. """ - type: Literal["HybridOnlineStore", "hybrid_online_store.HybridOnlineStore"] = ( - "hybrid_online_store.HybridOnlineStore" - ) + type: Literal["hybrid"] = "hybrid" class OnlineStoresWithConfig(FeastConfigBaseModel): """ diff --git a/sdk/python/feast/infra/online_stores/hybrid_online_store/hybrid_online_store_repo_configuration.py b/sdk/python/feast/infra/online_stores/hybrid_online_store/hybrid_online_store_repo_configuration.py index 90a65a092d0..3d016ce7d5e 100644 --- a/sdk/python/feast/infra/online_stores/hybrid_online_store/hybrid_online_store_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/hybrid_online_store/hybrid_online_store_repo_configuration.py @@ -16,10 +16,10 @@ # It enables running integration tests with multiple online store backends. # Update this file if you add more backends or change test setup. -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.hybrid_online_store import ( +from tests.universal.feature_repos.universal.online_store.hybrid_online_store import ( HybridOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/ikv_online_store/ikv.py b/sdk/python/feast/infra/online_stores/ikv_online_store/ikv.py deleted file mode 100644 index c8f0ad65c99..00000000000 --- a/sdk/python/feast/infra/online_stores/ikv_online_store/ikv.py +++ /dev/null @@ -1,311 +0,0 @@ -from datetime import datetime, timezone -from typing import ( - Any, - Callable, - Dict, - Iterator, - List, - Literal, - Optional, - Sequence, - Tuple, -) - -from google.protobuf.timestamp_pb2 import Timestamp -from ikvpy.client import IKVReader, IKVWriter -from ikvpy.clientoptions import ClientOptions, ClientOptionsBuilder -from ikvpy.document import IKVDocument, IKVDocumentBuilder -from ikvpy.factory import create_new_reader, create_new_writer -from pydantic import StrictStr - -from feast import Entity, FeatureView, utils -from feast.infra.online_stores.helpers import compute_entity_id -from feast.infra.online_stores.online_store import OnlineStore -from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto -from feast.protos.feast.types.Value_pb2 import Value as ValueProto -from feast.repo_config import FeastConfigBaseModel, RepoConfig - -PRIMARY_KEY_FIELD_NAME: str = "_entity_key" -EVENT_CREATION_TIMESTAMP_FIELD_NAME: str = "_event_timestamp" -CREATION_TIMESTAMP_FIELD_NAME: str = "_created_timestamp" - - -class IKVOnlineStoreConfig(FeastConfigBaseModel): - """Online store config for IKV store""" - - type: Literal["ikv"] = "ikv" - """Online store type selector""" - - account_id: StrictStr - """(Required) IKV account id""" - - account_passkey: StrictStr - """(Required) IKV account passkey""" - - store_name: StrictStr - """(Required) IKV store name""" - - mount_directory: Optional[StrictStr] = None - """(Required only for reader) IKV mount point i.e. directory for storing IKV data locally.""" - - -class IKVOnlineStore(OnlineStore): - """ - IKV (inlined.io key value) store implementation of the online store interface. - """ - - # lazy initialization - _reader: Optional[IKVReader] = None - _writer: Optional[IKVWriter] = None - - def online_write_batch( - self, - config: RepoConfig, - table: FeatureView, - data: List[ - Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] - ], - progress: Optional[Callable[[int], Any]], - ) -> None: - """ - Writes a batch of feature rows to the online store. - - If a tz-naive timestamp is passed to this method, it is assumed to be UTC. - - Args: - config: The config for the current feature store. - table: Feature view to which these feature rows correspond. - data: A list of quadruplets containing feature data. Each quadruplet contains an entity - key, a dict containing feature values, an event timestamp for the row, and the created - timestamp for the row if it exists. - progress: Function to be called once a batch of rows is written to the online store, used - to show progress. - """ - self._init_writer(config=config) - assert self._writer is not None - - for entity_key, features, event_timestamp, _ in data: - entity_id: str = compute_entity_id( - entity_key, - entity_key_serialization_version=config.entity_key_serialization_version, - ) - document: IKVDocument = IKVOnlineStore._create_document( - entity_id, table, features, event_timestamp - ) - self._writer.upsert_fields(document) - if progress: - progress(1) - - def online_read( - self, - config: RepoConfig, - table: FeatureView, - entity_keys: List[EntityKeyProto], - requested_features: Optional[List[str]] = None, - ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: - """ - Reads features values for the given entity keys. - - Args: - config: The config for the current feature store. - table: The feature view whose feature values should be read. - entity_keys: The list of entity keys for which feature values should be read. - requested_features: The list of features that should be read. - - Returns: - A list of the same length as entity_keys. Each item in the list is a tuple where the first - item is the event timestamp for the row, and the second item is a dict mapping feature names - to values, which are returned in proto format. - """ - self._init_reader(config=config) - - if not len(entity_keys): - return [] - - # create IKV primary keys - primary_keys = [ - compute_entity_id(ek, config.entity_key_serialization_version) - for ek in entity_keys - ] - - # create IKV field names - if requested_features is None: - requested_features = [] - - field_names: List[Optional[str]] = [None] * (1 + len(requested_features)) - field_names[0] = EVENT_CREATION_TIMESTAMP_FIELD_NAME - for i, fn in enumerate(requested_features): - field_names[i + 1] = IKVOnlineStore._create_ikv_field_name(table, fn) - - assert self._reader is not None - value_iter = self._reader.multiget_bytes_values( - bytes_primary_keys=[], - str_primary_keys=primary_keys, - field_names=field_names, - ) - - # decode results - return [ - IKVOnlineStore._decode_fields_for_primary_key( - requested_features, value_iter - ) - for _ in range(0, len(primary_keys)) - ] - - @staticmethod - def _decode_fields_for_primary_key( - requested_features: List[str], value_iter: Iterator[Optional[bytes]] - ) -> Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]: - # decode timestamp - dt: Optional[datetime] = None - dt_bytes = next(value_iter) - if dt_bytes: - proto_timestamp = Timestamp() - proto_timestamp.ParseFromString(dt_bytes) - dt = datetime.fromtimestamp(proto_timestamp.seconds, tz=timezone.utc) - - # decode other features - features = {} - for requested_feature in requested_features: - value_proto_bytes: Optional[bytes] = next(value_iter) - if value_proto_bytes: - value_proto = ValueProto() - value_proto.ParseFromString(value_proto_bytes) - features[requested_feature] = value_proto - - return dt, features - - def update( - self, - config: RepoConfig, - tables_to_delete: Sequence[FeatureView], - tables_to_keep: Sequence[FeatureView], - entities_to_delete: Sequence[Entity], - entities_to_keep: Sequence[Entity], - partial: bool, - ): - """ - Reconciles cloud resources with the specified set of Feast objects. - - Args: - config: The config for the current feature store. - tables_to_delete: Feature views whose corresponding infrastructure should be deleted. - tables_to_keep: Feature views whose corresponding infrastructure should not be deleted, and - may need to be updated. - entities_to_delete: Entities whose corresponding infrastructure should be deleted. - entities_to_keep: Entities whose corresponding infrastructure should not be deleted, and - may need to be updated. - partial: If true, tables_to_delete and tables_to_keep are not exhaustive lists, so - infrastructure corresponding to other feature views should be not be touched. - """ - self._init_writer(config=config) - assert self._writer is not None - - # note: we assume tables_to_keep does not overlap with tables_to_delete - - for feature_view in tables_to_delete: - # each field in an IKV document is prefixed by the feature-view's name - self._writer.drop_fields_by_name_prefix([feature_view.name]) - - def teardown( - self, - config: RepoConfig, - tables: Sequence[FeatureView], - entities: Sequence[Entity], - ): - """ - Tears down all cloud resources for the specified set of Feast objects. - - Args: - config: The config for the current feature store. - tables: Feature views whose corresponding infrastructure should be deleted. - entities: Entities whose corresponding infrastructure should be deleted. - """ - self._init_writer(config=config) - assert self._writer is not None - - # drop fields corresponding to this feature-view - for feature_view in tables: - self._writer.drop_fields_by_name_prefix([feature_view.name]) - - # shutdown clients - self._writer.shutdown() - self._writer = None - - if self._reader is not None: - self._reader.shutdown() - self._reader = None - - @staticmethod - def _create_ikv_field_name(feature_view: FeatureView, feature_name: str) -> str: - return "{}_{}".format(feature_view.name, feature_name) - - @staticmethod - def _create_document( - entity_id: str, - feature_view: FeatureView, - values: Dict[str, ValueProto], - event_timestamp: datetime, - ) -> IKVDocument: - """Converts feast key-value pairs into an IKV document.""" - - # initialie builder by inserting primary key and row creation timestamp - event_timestamp_seconds = int(utils.make_tzaware(event_timestamp).timestamp()) - event_timestamp_seconds_proto = Timestamp() - event_timestamp_seconds_proto.seconds = event_timestamp_seconds - - # event_timestamp_str: str = utils.make_tzaware(event_timestamp).isoformat() - builder = ( - IKVDocumentBuilder() - .put_string_field(PRIMARY_KEY_FIELD_NAME, entity_id) - .put_bytes_field( - EVENT_CREATION_TIMESTAMP_FIELD_NAME, - event_timestamp_seconds_proto.SerializeToString(), - ) - ) - - for feature_name, feature_value in values.items(): - field_name = IKVOnlineStore._create_ikv_field_name( - feature_view, feature_name - ) - builder.put_bytes_field(field_name, feature_value.SerializeToString()) - - return builder.build() - - def _init_writer(self, config: RepoConfig): - """Initializes ikv writer client.""" - # initialize writer - if self._writer is None: - online_config = config.online_store - assert isinstance(online_config, IKVOnlineStoreConfig) - client_options = IKVOnlineStore._config_to_client_options(online_config) - - self._writer = create_new_writer(client_options) - self._writer.startup() # blocking operation - - def _init_reader(self, config: RepoConfig): - """Initializes ikv reader client.""" - # initialize reader - if self._reader is None: - online_config = config.online_store - assert isinstance(online_config, IKVOnlineStoreConfig) - client_options = IKVOnlineStore._config_to_client_options(online_config) - - if online_config.mount_directory and len(online_config.mount_directory) > 0: - self._reader = create_new_reader(client_options) - self._reader.startup() # blocking operation - - @staticmethod - def _config_to_client_options(config: IKVOnlineStoreConfig) -> ClientOptions: - """Utility for IKVOnlineStoreConfig to IKV ClientOptions conversion.""" - builder = ( - ClientOptionsBuilder() - .with_account_id(config.account_id) - .with_account_passkey(config.account_passkey) - .with_store_name(config.store_name) - ) - - if config.mount_directory and len(config.mount_directory) > 0: - builder = builder.with_mount_directory(config.mount_directory) - - return builder.build() diff --git a/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py b/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py index 37c88850ee7..00079938a86 100644 --- a/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py +++ b/sdk/python/feast/infra/online_stores/milvus_online_store/milvus.py @@ -1,9 +1,11 @@ import base64 +import logging +import re from datetime import datetime from pathlib import Path from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union -from pydantic import StrictStr +from pydantic import StrictStr, field_validator from pymilvus import ( CollectionSchema, DataType, @@ -13,11 +15,19 @@ from feast import Entity from feast.feature_view import FeatureView +from feast.filter_models import ( + ComparisonFilter, + CompoundFilter, + FilterTranslator, + FilterType, + filters_contain_numeric_comparison, +) from feast.infra.infra_object import InfraObject from feast.infra.key_encoding_utils import ( deserialize_entity_key, serialize_entity_key, ) +from feast.infra.online_stores.helpers import compute_table_id from feast.infra.online_stores.online_store import OnlineStore from feast.infra.online_stores.vector_store import VectorStoreConfig from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto @@ -42,6 +52,8 @@ to_naive_utc, ) +logger = logging.getLogger(__name__) + PROTO_TO_MILVUS_TYPE_MAPPING: Dict[ValueType, DataType] = { PROTO_VALUE_TO_VALUE_TYPE_MAP["bytes_val"]: DataType.VARCHAR, ValueType.IMAGE_BYTES: DataType.VARCHAR, @@ -56,6 +68,12 @@ PROTO_VALUE_TO_VALUE_TYPE_MAP["int64_list_val"]: DataType.FLOAT_VECTOR, PROTO_VALUE_TO_VALUE_TYPE_MAP["double_list_val"]: DataType.FLOAT_VECTOR, PROTO_VALUE_TO_VALUE_TYPE_MAP["bool_list_val"]: DataType.BINARY_VECTOR, + PROTO_VALUE_TO_VALUE_TYPE_MAP["map_val"]: DataType.VARCHAR, + PROTO_VALUE_TO_VALUE_TYPE_MAP["map_list_val"]: DataType.VARCHAR, + PROTO_VALUE_TO_VALUE_TYPE_MAP["json_val"]: DataType.VARCHAR, + PROTO_VALUE_TO_VALUE_TYPE_MAP["json_list_val"]: DataType.VARCHAR, + PROTO_VALUE_TO_VALUE_TYPE_MAP["struct_val"]: DataType.VARCHAR, + PROTO_VALUE_TO_VALUE_TYPE_MAP["struct_list_val"]: DataType.VARCHAR, } FEAST_PRIMITIVE_TO_MILVUS_TYPE_MAPPING: Dict[ @@ -81,6 +99,99 @@ FEAST_PRIMITIVE_TO_MILVUS_TYPE_MAPPING[feast_type] = DataType.VARCHAR elif base_value_type == ValueType.BOOL: FEAST_PRIMITIVE_TO_MILVUS_TYPE_MAPPING[feast_type] = DataType.BINARY_VECTOR + elif isinstance(feast_type, ComplexFeastType): + milvus_type = PROTO_TO_MILVUS_TYPE_MAPPING.get(value_type) + if milvus_type: + FEAST_PRIMITIVE_TO_MILVUS_TYPE_MAPPING[feast_type] = milvus_type + +logger = logging.getLogger(__name__) + +MILVUS_NATIVE_NUMERIC_TYPES = { + DataType.INT32, + DataType.INT64, + DataType.FLOAT, + DataType.DOUBLE, + DataType.BOOL, +} + + +def _milvus_escape_string(s: str) -> str: + """Escape a string for safe use inside a Milvus single-quoted literal. + + Backslashes must be escaped first; otherwise a trailing backslash in the + input would combine with the escaped quote to break out of the literal. + Also escapes quotes and common control characters that could disrupt + Milvus boolean expressions. + """ + return ( + s.replace("\\", "\\\\") + .replace("'", "\\'") + .replace('"', '\\"') + .replace("\n", "\\n") + .replace("\r", "\\r") + ) + + +def _milvus_fmt(value: Any) -> str: + """Format a Python value for use in a Milvus boolean expression. + + Handles numeric types natively (unquoted) so that Milvus performs + numeric comparison instead of lexicographic string comparison. + Bool is checked first because Python ``bool`` is a subclass of ``int``. + """ + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + return f"'{_milvus_escape_string(str(value))}'" + + +class MilvusFilterTranslator(FilterTranslator): + """Translates Feast filters into Milvus boolean expression strings.""" + + def translate(self, filters: FilterType) -> Optional[str]: + if filters is None: + return None + return self._dispatch(filters) + + def translate_comparison(self, f: ComparisonFilter) -> str: + key, value, op_type = f.key, f.value, f.type + + if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", key): + raise ValueError( + f"Invalid filter key: {key!r}. Keys must be valid field identifiers." + ) + + milvus_ops = {"gt": ">", "gte": ">=", "lt": "<", "lte": "<="} + + if op_type == "eq": + return f"{key} == {_milvus_fmt(value)}" + elif op_type == "ne": + return f"{key} != {_milvus_fmt(value)}" + elif op_type in milvus_ops: + return f"{key} {milvus_ops[op_type]} {_milvus_fmt(value)}" + elif op_type in ("in", "nin"): + if not isinstance(value, list): + raise ValueError( + f"'{op_type}' filter requires a list value, got {type(value)}" + ) + formatted = [_milvus_fmt(v) for v in value] + kw = "not in" if op_type == "nin" else "in" + return f"{key} {kw} [{', '.join(formatted)}]" + raise ValueError(f"Unsupported comparison operator: {op_type}") + + def translate_compound(self, f: CompoundFilter) -> str: + if not f.filters: + return "" + clauses = [] + for sub_filter in f.filters: + clause = self._dispatch(sub_filter) + if clause: + clauses.append(f"({clause})") + if not clauses: + return "" + operator = " and " if f.type == "and" else " or " + return operator.join(clauses) class MilvusOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig): @@ -101,6 +212,15 @@ class MilvusOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig): nlist: Optional[int] = 128 username: Optional[StrictStr] = "" password: Optional[StrictStr] = "" + enable_openai_compatible_store: Optional[bool] = False + varchar_max_length: Optional[int] = 65535 + + @field_validator("varchar_max_length") + @classmethod + def validate_varchar_max_length(cls, v): + if v is not None and not (1 <= v <= 65535): + raise ValueError(f"varchar_max_length must be between 1 and 65535, got {v}") + return v class MilvusOnlineStore(OnlineStore): @@ -111,8 +231,10 @@ class MilvusOnlineStore(OnlineStore): _collections: Dictionary to cache Milvus collections. """ - client: Optional[MilvusClient] = None - _collections: Dict[str, Any] = {} + def __init__(self): + super().__init__() + self.client: Optional[MilvusClient] = None + self._collections: Dict[str, Any] = {} def _get_db_path(self, config: RepoConfig) -> str: assert ( @@ -130,11 +252,13 @@ def _connect(self, config: RepoConfig) -> MilvusClient: if not self.client: if config.provider == "local" and config.online_store.path: db_path = self._get_db_path(config) - print(f"Connecting to Milvus in local mode using {db_path}") + logger.info("Connecting to Milvus in local mode using %s", db_path) self.client = MilvusClient(db_path) else: - print( - f"Connecting to Milvus remotely at {config.online_store.host}:{config.online_store.port}" + logger.info( + "Connecting to Milvus remotely at %s:%s", + config.online_store.host, + config.online_store.port, ) self.client = MilvusClient( uri=f"{config.online_store.host}:{config.online_store.port}", @@ -149,16 +273,18 @@ def _get_or_create_collection( ) -> Dict[str, Any]: self.client = self._connect(config) vector_field_dict = {k.name: k for k in table.schema if k.vector_index} - collection_name = _table_id(config.project, table) + collection_name = _table_id( + config.project, table, config.registry.enable_online_feature_view_versioning + ) if collection_name not in self._collections: # Create a composite key by combining entity fields composite_key_name = _get_composite_key_name(table) - + varchar_max_length = int(config.online_store.varchar_max_length or 65535) fields = [ FieldSchema( name=composite_key_name, dtype=DataType.VARCHAR, - max_length=512, + max_length=varchar_max_length, is_primary=True, ), FieldSchema(name="event_ts", dtype=DataType.INT64), @@ -167,10 +293,15 @@ def _get_or_create_collection( fields_to_exclude = [ "event_ts", "created_ts", + "event_timestamp", + "created_timestamp", ] fields_to_add = [f for f in table.schema if f.name not in fields_to_exclude] + use_typed = config.online_store.enable_openai_compatible_store for field in fields_to_add: dtype = FEAST_PRIMITIVE_TO_MILVUS_TYPE_MAPPING.get(field.dtype) + if dtype is None and isinstance(field.dtype, ComplexFeastType): + dtype = DataType.VARCHAR if dtype: if dtype == DataType.FLOAT_VECTOR: fields.append( @@ -180,15 +311,48 @@ def _get_or_create_collection( dim=config.online_store.embedding_dim, ) ) + elif use_typed and dtype in MILVUS_NATIVE_NUMERIC_TYPES: + fields.append( + FieldSchema( + name=field.name, + dtype=dtype, + ) + ) else: + if "max_length" in field.tags: + try: + field_max_length = int(field.tags["max_length"]) + except (ValueError, TypeError): + raise ValueError( + f"Field '{field.name}' has invalid max_length tag" + f" '{field.tags['max_length']}': must be an integer." + ) + if not (1 <= field_max_length <= 65535): + raise ValueError( + f"Field '{field.name}' max_length tag must be" + f" between 1 and 65535, got {field_max_length}" + ) + else: + field_max_length = varchar_max_length fields.append( FieldSchema( name=field.name, dtype=DataType.VARCHAR, - max_length=512, + max_length=field_max_length, ) ) - + has_vector_field = any( + f.dtype in (DataType.FLOAT_VECTOR, DataType.BINARY_VECTOR) + for f in fields + ) + if not has_vector_field: + fields.append( + FieldSchema( + name="_placeholder_vector", + dtype=DataType.FLOAT_VECTOR, + dim=1, + ) + ) schema = CollectionSchema( fields=fields, description="Feast feature view data" ) @@ -202,6 +366,7 @@ def _get_or_create_collection( schema=schema, ) index_params = self.client.prepare_index_params() + indices_added = False for vector_field in schema.fields: if ( vector_field.dtype @@ -222,10 +387,12 @@ def _get_or_create_collection( index_name=f"vector_index_{vector_field.name}", params={"nlist": config.online_store.nlist}, ) - self.client.create_index( - collection_name=collection_name, - index_params=index_params, - ) + indices_added = True + if indices_added: + self.client.create_index( + collection_name=collection_name, + index_params=index_params, + ) else: self.client.load_collection(collection_name) self._collections[collection_name] = self.client.describe_collection( @@ -253,14 +420,19 @@ def online_write_batch( entity_batch_to_insert = [] unique_entities: dict[str, dict[str, Any]] = {} required_fields = {field["name"] for field in collection["fields"]} + collection_field_types = { + field["name"]: field["type"] for field in collection["fields"] + } + schema_internal_fields = {"event_ts", "created_ts"} + collection_has_native_numerics = any( + collection_field_types.get(name) in MILVUS_NATIVE_NUMERIC_TYPES + for name in required_fields - schema_internal_fields + ) for entity_key, values_dict, timestamp, created_ts in data: - # need to construct the composite primary key also need to handle the fact that entities are a list entity_key_str = serialize_entity_key( entity_key, entity_key_serialization_version=config.entity_key_serialization_version, ).hex() - # to recover the entity key just run: - # deserialize_entity_key(bytes.fromhex(entity_key_str), entity_key_serialization_version=3) composite_key_name = _get_composite_key_name(table) timestamp_int = int(to_naive_utc(timestamp).timestamp() * 1e6) @@ -278,18 +450,34 @@ def online_write_batch( values_dict, vector_cols=vector_cols, serialize_to_string=True, + use_native_numeric_types=collection_has_native_numerics, ) + # Remove timestamp fields that are handled separately to avoid conflicts + timestamp_fields = [ + "event_timestamp", + "created_timestamp", + "event_ts", + "created_ts", + ] + for field in timestamp_fields: + values_dict.pop(field, None) + single_entity_record = { composite_key_name: entity_key_str, "event_ts": timestamp_int, "created_ts": created_ts_int, } single_entity_record.update(values_dict) - # Ensure all required fields exist, setting missing ones to empty strings for field in required_fields: if field not in single_entity_record: - single_entity_record[field] = "" + field_type = collection_field_types.get(field, DataType.VARCHAR) + if field == "_placeholder_vector": + single_entity_record[field] = [float("nan")] + else: + single_entity_record[field] = _default_for_milvus_type( + field_type + ) # Store only the latest event timestamp per entity if ( entity_key_str not in unique_entities @@ -312,10 +500,11 @@ def online_read( table: FeatureView, entity_keys: List[EntityKeyProto], requested_features: Optional[List[str]] = None, - full_feature_names: bool = False, ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: self.client = self._connect(config) - collection_name = _table_id(config.project, table) + collection_name = _table_id( + config.project, table, config.registry.enable_online_feature_view_versioning + ) collection = self._get_or_create_collection(config, table) composite_key_name = _get_composite_key_name(table) @@ -418,6 +607,19 @@ def online_read( "double_list_val", ]: getattr(val, proto_attr).val.extend(field_value) + elif proto_attr in [ + "map_val", + "map_list_val", + "struct_val", + "struct_list_val", + "json_list_val", + ]: + if isinstance(field_value, str) and field_value: + try: + proto_bytes = base64.b64decode(field_value) + val.ParseFromString(proto_bytes) + except Exception: + setattr(val, "string_val", field_value) else: setattr(val, proto_attr, field_value) else: @@ -447,18 +649,19 @@ def update( ): self.client = self._connect(config) for table in tables_to_keep: - self._collections = self._get_or_create_collection(config, table) + self._get_or_create_collection(config, table) + # Always drop the base collection plus any "_v{N}" siblings, regardless of + # the current versioning flag. This handles mixed-state repos where + # versioning was toggled on/off across applies and would otherwise leave + # orphan collections behind in Milvus. for table in tables_to_delete: - collection_name = _table_id(config.project, table) - if self._collections.get(collection_name, None): - self.client.drop_collection(collection_name) - self._collections.pop(collection_name, None) + self._drop_all_version_collections(config.project, table) def plan( self, config: RepoConfig, desired_registry_proto: RegistryProto ) -> List[InfraObject]: - raise NotImplementedError + return [] def teardown( self, @@ -467,11 +670,9 @@ def teardown( entities: Sequence[Entity], ): self.client = self._connect(config) + # See update(): drop base + all "_v{N}" siblings to handle mixed-state repos. for table in tables: - collection_name = _table_id(config.project, table) - if self._collections.get(collection_name, None): - self.client.drop_collection(collection_name) - self._collections.pop(collection_name, None) + self._drop_all_version_collections(config.project, table) def retrieve_online_documents_v2( self, @@ -482,6 +683,8 @@ def retrieve_online_documents_v2( top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> List[ Tuple[ Optional[datetime], @@ -503,10 +706,14 @@ def retrieve_online_documents_v2( List of tuples containing the event timestamp, entity key, and feature values """ entity_name_feast_primitive_type_map = { - k.name: k.dtype for k in table.entity_columns + k.name: k.dtype + for k in list(table.entity_columns) + list(table.features) + if isinstance(k.dtype, PrimitiveFeastType) } self.client = self._connect(config) - collection_name = _table_id(config.project, table) + collection_name = _table_id( + config.project, table, config.registry.enable_online_feature_view_versioning + ) collection = self._get_or_create_collection(config, table) if not config.online_store.vector_enabled: raise ValueError("Vector search is not enabled in the online store config") @@ -541,6 +748,38 @@ def retrieve_online_documents_v2( self.client.load_collection(collection_name) + if filters and filters_contain_numeric_comparison(filters): + collection_field_types = { + f["name"]: f["type"] for f in collection["fields"] + } + schema_internal_fields = {"event_ts", "created_ts"} + has_native = any( + collection_field_types.get(name) in MILVUS_NATIVE_NUMERIC_TYPES + for name in collection_field_types + if name not in schema_internal_fields + ) + if not has_native: + logger.warning( + "Numeric comparison filters (gt, gte, lt, lte) are being used " + "but this collection stores numeric fields as VARCHAR. This " + "causes lexicographic comparison instead of numeric comparison " + "(e.g. '9' > '100' is True as a string). To fix this, set " + "'enable_openai_compatible_store: true' in your online_store " + "config, then teardown and re-apply your feature store to " + "recreate collections with native numeric types." + ) + + metadata_filter_expr = MilvusFilterTranslator().translate(filters) + + def _combine_exprs(*parts: Optional[str]) -> Optional[str]: + """Combine non-empty Milvus boolean expressions with AND.""" + active = [p for p in parts if p] + if not active: + return None + if len(active) == 1: + return active[0] + return " and ".join(f"({p})" for p in active) + if ( embedding is not None and query_string is not None @@ -558,22 +797,20 @@ def retrieve_online_documents_v2( "No string fields found in the feature view for text search in hybrid mode" ) - # Create a filter expression for text search + escaped_query = _milvus_escape_string(query_string) filter_expressions = [] for field in string_field_list: if field in output_fields: - filter_expressions.append(f"{field} LIKE '%{query_string}%'") + filter_expressions.append(f"{field} LIKE '%{escaped_query}%'") - # Combine filter expressions with OR - filter_expr = " OR ".join(filter_expressions) if filter_expressions else "" + text_filter = " OR ".join(filter_expressions) if filter_expressions else "" + combined_filter = _combine_exprs(text_filter, metadata_filter_expr) - # Vector search with text filter search_params = { "metric_type": distance_metric or config.online_store.metric_type, "params": {"nprobe": 10}, } - # For hybrid search, use filter parameter instead of expr results = self.client.search( collection_name=collection_name, data=[embedding], @@ -581,7 +818,7 @@ def retrieve_online_documents_v2( search_params=search_params, limit=top_k, output_fields=output_fields, - filter=filter_expr if filter_expr else None, + filter=combined_filter, ) elif embedding is not None and config.online_store.vector_enabled: @@ -598,6 +835,7 @@ def retrieve_online_documents_v2( search_params=search_params, limit=top_k, output_fields=output_fields, + filter=metadata_filter_expr, ) elif query_string is not None: @@ -613,21 +851,24 @@ def retrieve_online_documents_v2( "No string fields found in the feature view for text search" ) + escaped_query = _milvus_escape_string(query_string) filter_expressions = [] for field in string_field_list: if field in output_fields: - filter_expressions.append(f"{field} LIKE '%{query_string}%'") + filter_expressions.append(f"{field} LIKE '%{escaped_query}%'") - filter_expr = " OR ".join(filter_expressions) + text_filter = " OR ".join(filter_expressions) - if not filter_expr: + if not text_filter: raise ValueError( "No text fields found in requested features for search" ) + combined_filter = _combine_exprs(text_filter, metadata_filter_expr) + query_results = self.client.query( collection_name=collection_name, - filter=filter_expr, + filter=combined_filter or text_filter, output_fields=output_fields, limit=top_k, ) @@ -645,9 +886,8 @@ def retrieve_online_documents_v2( for hit in hits: res = {} res_ts = None - entity_key_bytes = bytes.fromhex( - hit.get("entity", {}).get(composite_key_name, None) - ) + raw_key = hit.get("entity", {}).get(composite_key_name) + entity_key_bytes = bytes.fromhex(raw_key) if raw_key else None entity_key_proto = ( deserialize_entity_key(entity_key_bytes) if entity_key_bytes @@ -689,39 +929,93 @@ def retrieve_online_documents_v2( PrimitiveFeastType.INT32, ]: res[field] = ValueProto(int64_val=int(field_value)) + elif entity_name_feast_primitive_type_map.get( + field, PrimitiveFeastType.INVALID + ) in [ + PrimitiveFeastType.FLOAT64, + PrimitiveFeastType.FLOAT32, + ]: + res[field] = ValueProto(double_val=float(field_value)) + elif ( + entity_name_feast_primitive_type_map.get( + field, PrimitiveFeastType.INVALID + ) + == PrimitiveFeastType.BOOL + ): + res[field] = ValueProto(bool_val=bool(field_value)) elif field == composite_key_name: pass elif isinstance(field_value, bytes): val.ParseFromString(field_value) res[field] = val + elif isinstance(field_value, int): + res[field] = ValueProto(int64_val=field_value) + elif isinstance(field_value, float): + res[field] = ValueProto(double_val=field_value) else: - val.string_val = field_value + val.string_val = str(field_value) res[field] = val - distance = hit.get("distance", None) + raw_distance = hit.get("distance", None) res["distance"] = ( - ValueProto(float_val=distance) if distance else ValueProto() + ValueProto(float_val=raw_distance) + if raw_distance is not None + else ValueProto() ) result_list.append((res_ts, entity_key_proto, res if res else None)) return result_list + def _drop_all_version_collections(self, project: str, table: FeatureView) -> None: + """Drop the base collection and every ``_v{N}`` versioned sibling. + + Mirrors the ``_drop_all_version_tables`` helpers in the MySQL/PostgreSQL + online stores. Always called from ``update`` and ``teardown`` so a + repo that toggles versioning on and off does not leave orphan + collections behind in Milvus. + """ + base = f"{project}_{table.name}" + versioned_prefix = f"{base}_v" + assert self.client is not None, "Milvus client is not initialized" + for collection_name in self.client.list_collections(): + if collection_name == base or ( + collection_name.startswith(versioned_prefix) + and collection_name[len(versioned_prefix) :].isdigit() + ): + self.client.drop_collection(collection_name) + self._collections.pop(collection_name, None) + -def _table_id(project: str, table: FeatureView) -> str: - return f"{project}_{table.name}" +def _table_id(project: str, table: FeatureView, enable_versioning: bool = False) -> str: + return compute_table_id(project, table, enable_versioning) def _get_composite_key_name(table: FeatureView) -> str: return "_".join([field.name for field in table.entity_columns]) + "_pk" +_MILVUS_TYPE_DEFAULTS: Dict[DataType, Any] = { + DataType.INT32: 0, + DataType.INT64: 0, + DataType.FLOAT: 0.0, + DataType.DOUBLE: 0.0, + DataType.BOOL: False, + DataType.VARCHAR: "", +} + + +def _default_for_milvus_type(dtype: DataType) -> Any: + return _MILVUS_TYPE_DEFAULTS.get(dtype, "") + + def _extract_proto_values_to_dict( input_dict: Dict[str, Any], vector_cols: List[str], - serialize_to_string=False, + serialize_to_string: bool = False, + use_native_numeric_types: bool = False, ) -> Dict[str, Any]: numeric_vector_list_types = [ k for k in PROTO_VALUE_TO_VALUE_TYPE_MAP.keys() - if k is not None and "list" in k and "string" not in k + if k is not None and ("list" in k or "set" in k) and "string" not in k ] numeric_types = [ "double_val", @@ -746,12 +1040,19 @@ def _extract_proto_values_to_dict( if ( serialize_to_string and proto_val_type - not in ["string_val", "bytes_val"] + numeric_types + not in ["string_val", "bytes_val", "unix_timestamp_val"] + + numeric_types ): - vector_values = feature_values.SerializeToString().decode() + vector_values = base64.b64encode( + feature_values.SerializeToString() + ).decode("utf-8") elif proto_val_type == "bytes_val": byte_data = getattr(feature_values, proto_val_type) vector_values = base64.b64encode(byte_data).decode("utf-8") + elif ( + use_native_numeric_types and proto_val_type in numeric_types + ): + vector_values = getattr(feature_values, proto_val_type) else: if not isinstance(feature_values, str): vector_values = str( @@ -762,8 +1063,13 @@ def _extract_proto_values_to_dict( output_dict[feature_name] = vector_values else: if serialize_to_string: - if not isinstance(feature_values, str): - feature_values = str(feature_values) - output_dict[feature_name] = feature_values + if use_native_numeric_types and isinstance( + feature_values, (int, float) + ): + output_dict[feature_name] = feature_values + else: + if not isinstance(feature_values, str): + feature_values = str(feature_values) + output_dict[feature_name] = feature_values return output_dict diff --git a/sdk/python/feast/infra/online_stores/milvus_online_store/milvus_repo_configuration.py b/sdk/python/feast/infra/online_stores/milvus_online_store/milvus_repo_configuration.py index 174c0b53737..5fbabe17898 100644 --- a/sdk/python/feast/infra/online_stores/milvus_online_store/milvus_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/milvus_online_store/milvus_repo_configuration.py @@ -1,8 +1,8 @@ -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.repo_configuration import MILVUS_CONFIG -from tests.integration.feature_repos.universal.online_store.milvus import ( +from tests.universal.feature_repos.repo_configuration import MILVUS_CONFIG +from tests.universal.feature_repos.universal.online_store.milvus import ( MilvusOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/mongodb_online_store/__init__.py b/sdk/python/feast/infra/online_stores/mongodb_online_store/__init__.py new file mode 100644 index 00000000000..84b2dfd763e --- /dev/null +++ b/sdk/python/feast/infra/online_stores/mongodb_online_store/__init__.py @@ -0,0 +1,3 @@ +from .mongodb import MongoDBOnlineStore, MongoDBOnlineStoreConfig + +__all__ = ["MongoDBOnlineStore", "MongoDBOnlineStoreConfig"] diff --git a/sdk/python/feast/infra/online_stores/mongodb_online_store/mongodb.py b/sdk/python/feast/infra/online_stores/mongodb_online_store/mongodb.py new file mode 100644 index 00000000000..16dd572bf5b --- /dev/null +++ b/sdk/python/feast/infra/online_stores/mongodb_online_store/mongodb.py @@ -0,0 +1,858 @@ +from __future__ import annotations + +import time +from datetime import datetime +from logging import getLogger +from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union + +try: + from pymongo import AsyncMongoClient, MongoClient, UpdateOne + from pymongo.asynchronous.collection import AsyncCollection + from pymongo.collection import Collection + from pymongo.driver_info import DriverInfo + from pymongo.operations import SearchIndexModel +except ImportError as e: + from feast.errors import FeastExtrasDependencyImportError + + raise FeastExtrasDependencyImportError("mongodb", str(e)) + +import feast.version +from feast.batch_feature_view import BatchFeatureView +from feast.entity import Entity +from feast.feature_view import FeatureView +from feast.filter_models import ( + ComparisonFilter, + CompoundFilter, + FilterTranslator, + FilterType, +) +from feast.infra.key_encoding_utils import deserialize_entity_key, serialize_entity_key +from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.online_stores.vector_store import VectorStoreConfig +from feast.infra.supported_async_methods import SupportedAsyncMethods +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import FeastConfigBaseModel, RepoConfig +from feast.stream_feature_view import StreamFeatureView +from feast.type_map import ( + feast_value_type_to_python_type, + python_values_to_proto_values, +) + +logger = getLogger(__name__) + +DRIVER_METADATA = DriverInfo(name="Feast", version=feast.version.get_version()) + +_MONGO_COMPARISON_OPS: Dict[str, str] = { + "eq": "$eq", + "ne": "$ne", + "gt": "$gt", + "gte": "$gte", + "lt": "$lt", + "lte": "$lte", +} + + +class MongoDBFilterTranslator(FilterTranslator): + """Translates Feast filters into MongoDB Query Language (MQL) expressions. + + Filter keys are prefixed with the feature path so they match the + document layout used by :class:`MongoDBOnlineStore`:: + + features.. + """ + + def __init__(self, table_name: str): + self.table_name = table_name + + def _field(self, key: str) -> str: + return f"features.{self.table_name}.{key}" + + def translate(self, filters: FilterType) -> Optional[Dict[str, Any]]: + if filters is None: + return None + return self._dispatch(filters) + + def translate_comparison(self, f: ComparisonFilter) -> Dict[str, Any]: + field = self._field(f.key) + + if f.type in _MONGO_COMPARISON_OPS: + return {field: {_MONGO_COMPARISON_OPS[f.type]: f.value}} + + if f.type == "in": + if not isinstance(f.value, list): + raise ValueError( + f"'in' filter requires a list value, got {type(f.value)}" + ) + return {field: {"$in": f.value}} + + if f.type == "nin": + if not isinstance(f.value, list): + raise ValueError( + f"'nin' filter requires a list value, got {type(f.value)}" + ) + return {field: {"$nin": f.value}} + + raise ValueError(f"Unsupported comparison operator: {f.type}") + + def translate_compound(self, f: CompoundFilter) -> Dict[str, Any]: + clauses = [self._dispatch(sub) for sub in f.filters] + if f.type == "and": + return {"$and": clauses} + return {"$or": clauses} + + +class MongoDBOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig): + """MongoDB configuration. + + For a description of kwargs that may be passed to MongoClient, + see https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html + """ + + type: Literal["mongodb"] = "mongodb" + """Online store type selector""" + connection_string: str = "mongodb://localhost:27017" + database_name: str = ( + "features" # todo - consider removing, and using repo_config.project + ) + collection_suffix: str = "latest" + client_kwargs: Dict[str, Any] = {} + # vector_enabled and similarity are inherited from VectorStoreConfig + vector_index_wait_timeout: int = 60 + """Seconds to wait for a newly created Atlas Search index to become READY.""" + vector_index_wait_poll_interval: float = 1.0 + """Seconds between polls when waiting for an Atlas Search index to become READY.""" + + +class MongoDBOnlineStore(OnlineStore): + """ + MongoDB implementation of Feast OnlineStore. + + Schema: + _id: serialized_entity_key (bytes) + features: { .: } + event_timestamps: { "": datetime } + created_timestamp: datetime + + For example: + { + "_id": b"", + "features": { + "driver_stats": { + "rating": 4.91, + "trips_last_7d": 132, + }, + "pricing": { + "surge_multiplier": 1.2 + }, + }, + "event_timestamps": { + "driver_stats": "2026-01-01 12:00:00+00:00", + "pricing": "2026-01-21 12:00:00+00:00" + }, + "created_timestamp": "2026-01-21 12:00:00+00:00" + } + """ + + _client: Optional[MongoClient] = None + _collection: Optional[Collection] = None + _client_async: Optional[AsyncMongoClient] = None + _collection_async: Optional[AsyncCollection] = None + + @staticmethod + def _build_write_ops( + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + ) -> List[UpdateOne]: + """Build the list of UpdateOne upsert operations shared by the sync and async write paths. + + For each row in *data* this method: + + 1. Serializes the entity key to bytes using ``serialize_entity_key``. + 2. Converts every ``ValueProto`` feature value to its native Python type + via ``feast_value_type_to_python_type``. + 3. Constructs a ``$set`` update document that writes feature values under + ``features..``, the per-view event + timestamp under ``event_timestamps.``, and the + row-level ``created_timestamp``. + 4. Wraps that in a ``UpdateOne`` with ``upsert=True`` so that existing + entity documents are updated in-place and new ones are created on first + write. + + The caller is responsible for executing the returned operations via + ``collection.bulk_write(ops, ordered=False)`` (sync) or + ``await collection.bulk_write(ops, ordered=False)`` (async). + """ + ops = [] + for entity_key, proto_values, event_timestamp, created_timestamp in data: + entity_id = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + feature_updates = { + f"features.{table.name}.{field}": feast_value_type_to_python_type(val) + for field, val in proto_values.items() + } + update = { + "$set": { + **feature_updates, + f"event_timestamps.{table.name}": event_timestamp, + "created_timestamp": created_timestamp, + }, + } + ops.append( + UpdateOne( + filter={"_id": entity_id}, + update=update, + upsert=True, + ) + ) + return ops + + def online_write_batch( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]] = None, + ) -> None: + """ + Writes a batch of feature values to the online store. + + data: + [ + ( + entity_key_bytes, + { feature_ref: ValueProto }, + event_timestamp, + created_timestamp, + ) + ] + """ + clxn = self._get_collection(config) + ops = self._build_write_ops(config, table, data) + if ops: + clxn.bulk_write(ops, ordered=False) + if progress: + progress(len(data)) + + def online_read( + self, + config: RepoConfig, + table: FeatureView, + entity_keys: List[EntityKeyProto], + requested_features: Optional[List[str]] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + """ + Read features for a batch of entities. + + Args: + config: Feast repo configuration + table: FeatureView to read from + entity_keys: List of entity keys to read + requested_features: Optional list of specific features to read + + Returns: + List of tuples (event_timestamp, feature_dict) for each entity key + """ + clxn = self._get_collection(config) + + ids = [ + serialize_entity_key( + key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + for key in entity_keys + ] + + query_filter = {"_id": {"$in": ids}} + projection = { + "_id": 1, + f"event_timestamps.{table.name}": 1, + } + if requested_features: + projection.update( + {f"features.{table.name}.{x}": 1 for x in requested_features} + ) + else: + projection[f"features.{table.name}"] = 1 + + cursor = clxn.find(query_filter, projection=projection) + docs = {doc["_id"]: doc for doc in cursor} + + return self._convert_raw_docs_to_proto(ids, docs, table) + + def retrieve_online_documents_v2( + self, + config: RepoConfig, + table: FeatureView, + requested_features: List[str], + embedding: Optional[List[float]], + top_k: int, + distance_metric: Optional[str] = None, + query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, + ) -> List[ + Tuple[ + Optional[datetime], + Optional[EntityKeyProto], + Optional[Dict[str, ValueProto]], + ] + ]: + """Retrieve documents via MongoDB Atlas Vector Search ($vectorSearch). + + Uses the ``$vectorSearch`` aggregation stage to find the *top_k* + documents closest to the provided *embedding* vector. The method + expects that an Atlas vector search index has already been created for + the relevant field (see ``update()``). + + Returns a list of 3-tuples ``(event_timestamp, entity_key_proto, + feature_dict)`` where *feature_dict* includes the requested feature + values plus a synthetic ``distance`` key with the vector search score. + """ + if not config.online_store.vector_enabled: + raise ValueError( + "Vector search is not enabled in the online store config. " + "Set vector_enabled=True in MongoDBOnlineStoreConfig." + ) + if embedding is None: + raise ValueError( + "An embedding vector must be provided for MongoDB vector search." + ) + + clxn = self._get_collection(config) + + # Identify the vector field on this feature view + vector_fields = [f for f in table.features if f.vector_index] + if not vector_fields: + raise ValueError( + f"Feature view '{table.name}' has no fields with vector_index=True." + ) + vector_field = vector_fields[0] + path = f"features.{table.name}.{vector_field.name}" + idx_name = self._vector_search_index_name(table.name, vector_field.name) + + # BSON cannot encode numpy float types — ensure native Python floats. + query_vector = [float(v) for v in embedding] + + num_candidates = max(top_k * 10, 100) + vector_search_stage: Dict[str, Any] = { + "index": idx_name, + "path": path, + "queryVector": query_vector, + "numCandidates": num_candidates, + "limit": top_k, + } + + mql_filter = MongoDBFilterTranslator(table.name).translate(filters) + if mql_filter: + vector_search_stage["filter"] = mql_filter + + pipeline: List[Dict[str, Any]] = [ + {"$vectorSearch": vector_search_stage}, + { + "$addFields": { + "score": {"$meta": "vectorSearchScore"}, + } + }, + ] + + results: List[ + Tuple[ + Optional[datetime], + Optional[EntityKeyProto], + Optional[Dict[str, ValueProto]], + ] + ] = [] + + for doc in clxn.aggregate(pipeline): + # Deserialize entity key + entity_key_bin = doc.get("_id") + entity_key_proto = ( + deserialize_entity_key( + entity_key_bin, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + if entity_key_bin + else None + ) + + # Event timestamp + event_ts = doc.get("event_timestamps", {}).get(table.name) + + # Build feature dict from raw doc values + fv_features = doc.get("features", {}).get(table.name, {}) + + # Convert raw values → ValueProto for each requested feature + feature_dict: Dict[str, ValueProto] = {} + feature_type_map = {f.name: f.dtype.to_value_type() for f in table.features} + + for feat_name in requested_features: + raw_val = fv_features.get(feat_name) + if raw_val is not None: + vtype = feature_type_map.get(feat_name) + if vtype is not None: + protos = python_values_to_proto_values( + [raw_val], feature_type=vtype + ) + feature_dict[feat_name] = protos[0] + else: + # Fall back: try to store as-is + feature_dict[feat_name] = _python_value_to_proto(raw_val) + + # Add distance (vector search score) + score = doc.get("score", 0.0) + feature_dict["distance"] = ValueProto(float_val=float(score)) + + results.append((event_ts, entity_key_proto, feature_dict)) + + return results + + def update( + self, + config: RepoConfig, + tables_to_delete: Sequence[FeatureView], + tables_to_keep: Sequence[ + Union[BatchFeatureView, StreamFeatureView, FeatureView] + ], + entities_to_delete: Sequence[Entity], + entities_to_keep: Sequence[Entity], + partial: bool, + ): + """Prepare or update online store. + + With MongoDB, we have a loose schema and lazy creation so there is little to do here. + Nothing needs to be pre-created for the entities and tables to keep. + + The OnlineStore is a single Collection with the following Document shape. + { + "_id": "", + "features": { + "": { + "": value + } + } + } + We remove any feature views named in tables_to_delete. + The Entities are serialized in the _id. No schema needs be adjusted. + + When ``vector_enabled`` is set in the online store config, Atlas Vector + Search indexes are automatically created for feature views containing + fields with ``vector_index=True`` and dropped for deleted feature views. + """ + if not isinstance(config.online_store, MongoDBOnlineStoreConfig): + raise RuntimeError(f"{config.online_store.type = }. It must be mongodb.") + + online_config = config.online_store + clxn = self._get_collection(repo_config=config) + + # --- Remove deleted feature views (data + vector search indexes) --- + if tables_to_delete: + unset_fields = {} + for fv in tables_to_delete: + unset_fields[f"features.{fv.name}"] = "" + unset_fields[f"event_timestamps.{fv.name}"] = "" + clxn.update_many({}, {"$unset": unset_fields}) + + if online_config.vector_enabled: + self._drop_vector_indexes_for_tables(clxn, tables_to_delete) + + # --- Create vector search indexes for kept feature views --- + if online_config.vector_enabled: + self._ensure_vector_indexes(clxn, tables_to_keep, online_config) + + # Note: entities_to_delete contains Entity definitions (metadata), not entity instances. + # Like other online stores, we don't need to do anything with entities_to_delete here. + + def teardown( + self, + config: RepoConfig, + tables: Sequence[FeatureView], + entities: Sequence[Entity], + ): + """ + Drop the backing collection and close the client. + + As in update, MongoDB requires very little here. + """ + if not isinstance(config.online_store, MongoDBOnlineStoreConfig): + raise RuntimeError(f"{config.online_store.type = }. It must be mongodb.") + clxn = self._get_collection(repo_config=config) + clxn.drop() + if self._client: + self._client.close() + self._client = None + self._collection = None + + async def close(self) -> None: + """Close the async MongoDB client and release its resources.""" + if self._client_async is not None: + await self._client_async.close() + self._client_async = None + self._collection_async = None + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + # -- Vector Search helpers ------------------------------------------ + + @staticmethod + def _vector_search_index_name(fv_name: str, field_name: str) -> str: + """Canonical Atlas vector search index name for a (feature_view, field) pair.""" + return f"{fv_name}__{field_name}__vs_index" + + @staticmethod + def _vector_search_index_definition( + path: str, + num_dimensions: int, + similarity: str, + ) -> dict: + """Return a vector search index definition for ``SearchIndexModel``.""" + return { + "fields": [ + { + "type": "vector", + "path": path, + "numDimensions": num_dimensions, + "similarity": similarity, + } + ] + } + + @staticmethod + def _wait_for_index_ready( + collection: Collection, + index_name: str, + timeout: int, + poll_interval: float = 1.0, + ) -> None: + """Poll until the named Atlas Search index reaches READY status. + + Raises ``TimeoutError`` if the index does not become queryable + within *timeout* seconds. + """ + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + for idx in collection.list_search_indexes(name=index_name): + if idx.get("status") == "READY" or idx.get("queryable") is True: + return + time.sleep(poll_interval) + raise TimeoutError( + f"Atlas search index '{index_name}' did not reach READY " + f"within {timeout}s. Increase vector_index_wait_timeout in " + f"MongoDBOnlineStoreConfig if the index needs more time." + ) + + def _drop_vector_indexes_for_tables( + self, + collection: Collection, + tables: Sequence[FeatureView], + ) -> None: + """Drop all Atlas vector search indexes belonging to the given feature views.""" + existing = {idx["name"] for idx in collection.list_search_indexes()} + for fv in tables: + for field in fv.features: + idx_name = self._vector_search_index_name(fv.name, field.name) + if idx_name in existing: + logger.info("Dropping vector search index: %s", idx_name) + collection.drop_search_index(idx_name) + + def _ensure_vector_indexes( + self, + collection: Collection, + tables: Sequence[Union[BatchFeatureView, StreamFeatureView, FeatureView]], + online_config: MongoDBOnlineStoreConfig, + ) -> None: + """Create Atlas vector search indexes for vector-indexed fields if they don't exist. + + Currently creates one index per (feature_view, vector_field) pair. + A future optimization could consolidate all vector fields into a + single composite index with multiple field definitions, reducing + cluster-wide index count and memory overhead. See Atlas limits: + hard cap of 2,500 search indexes per cluster; smaller tiers (M10) + may degrade well before that. + """ + db = collection.database + if collection.name not in db.list_collection_names(): + db.create_collection(collection.name) + + existing = {idx["name"] for idx in collection.list_search_indexes()} + + for fv in tables: + vector_fields = [f for f in fv.features if f.vector_index] + for field in vector_fields: + idx_name = self._vector_search_index_name(fv.name, field.name) + if idx_name in existing: + logger.debug("Vector search index '%s' already exists", idx_name) + continue + + path = f"features.{fv.name}.{field.name}" + num_dimensions = field.vector_length + if not num_dimensions: + raise ValueError( + f"Field '{field.name}' in feature view '{fv.name}' has " + f"vector_index=True but vector_length is not set." + ) + + similarity = ( + field.vector_search_metric or online_config.similarity or "cosine" + ) + + definition = self._vector_search_index_definition( + path, num_dimensions, similarity + ) + search_index_model = SearchIndexModel( + definition=definition, + name=idx_name, + type="vectorSearch", + ) + logger.info( + "Creating Atlas vector search index '%s' on path '%s' " + "(dims=%d, similarity=%s)", + idx_name, + path, + num_dimensions, + similarity, + ) + collection.create_search_index(model=search_index_model) + self._wait_for_index_ready( + collection, + idx_name, + online_config.vector_index_wait_timeout, + online_config.vector_index_wait_poll_interval, + ) + + # -- Connection helpers --------------------------------------------- + + def _get_client(self, config: RepoConfig): + """Returns a connection to the server.""" + online_store_config = config.online_store + if not isinstance(online_store_config, MongoDBOnlineStoreConfig): + raise ValueError( + f"config.online_store should be MongoDBOnlineStoreConfig, got {online_store_config}" + ) + if self._client is None: + online_config = config.online_store + self._client = MongoClient( + online_config.connection_string, + driver=DRIVER_METADATA, + **online_config.client_kwargs, + ) + return self._client + + def _get_collection(self, repo_config: RepoConfig) -> Collection: + """Returns a connection to the online store collection.""" + if self._collection is None: + self._client = self._get_client(repo_config) + assert self._client is not None + online_config = repo_config.online_store + db = self._client[online_config.database_name] + clxn_name = f"{repo_config.project}_{online_config.collection_suffix}" + self._collection = db[clxn_name] + return self._collection + + async def _get_client_async(self, config: RepoConfig) -> AsyncMongoClient: + """Returns an async MongoDB client.""" + if self._client_async is None: + online_config = config.online_store + if not isinstance(online_config, MongoDBOnlineStoreConfig): + raise ValueError( + f"config.online_store should be MongoDBOnlineStoreConfig, got {online_config}" + ) + self._client_async = AsyncMongoClient( + online_config.connection_string, + driver=DRIVER_METADATA, + **online_config.client_kwargs, + ) + return self._client_async + + async def _get_collection_async(self, repo_config: RepoConfig) -> AsyncCollection: + """Returns an async connection to the online store collection.""" + if self._collection_async is None: + self._client_async = await self._get_client_async(repo_config) + assert self._client_async is not None + online_config = repo_config.online_store + db = self._client_async[online_config.database_name] + clxn_name = f"{repo_config.project}_{online_config.collection_suffix}" + self._collection_async = db[clxn_name] + return self._collection_async + + @property + def async_supported(self) -> SupportedAsyncMethods: + """Indicates that this online store supports async operations.""" + return SupportedAsyncMethods(read=True, write=True) + + @staticmethod + def _convert_raw_docs_to_proto( + ids: list[bytes], docs: dict[bytes, Any], table: FeatureView + ) -> List[Tuple[Optional[datetime], Optional[dict[str, ValueProto]]]]: + """Optimized converting values in documents retrieved from MongoDB (BSON) into ValueProto types. + + The conversion itself is done in feast.type_map.python_values_to_proto_values. + The issue we have is that it is column-oriented, expecting a list of proto values with a single type. + MongoDB lookups are row-oriented. Plus, we need to ensure ordering of ids. + So we transform twice to minimize calls to the python/proto converter. + + Luckily, the table, a FeatureView, provides a map from feature name to proto type + so we don't have to infer types for each feature value. + + Args: + ids: sorted list of the serialized entity ids requested. + docs: results of collection find. + table: The FeatureView of the read, providing the types. + Returns: + List of tuples (event_timestamp, feature_dict) for each entity key + """ + feature_type_map = { + feature.name: feature.dtype.to_value_type() for feature in table.features + } + + # Step 1: Extract raw values column-wise (aligned by ordered ids) + # We need to maintain alignment, so we append None for missing features + raw_feature_columns: Dict[str, List[Any]] = { + feature_name: [] for feature_name in feature_type_map + } + + for entity_id in ids: + doc = docs.get(entity_id) + feature_dict = doc.get("features", {}).get(table.name, {}) if doc else {} + + # For each expected feature, append its value or None + for feature_name in feature_type_map: + raw_feature_columns[feature_name].append( + feature_dict.get(feature_name, None) + ) + + # Step 2: Convert per feature + proto_feature_columns = {} + for feature_name, raw_values in raw_feature_columns.items(): + proto_feature_columns[feature_name] = python_values_to_proto_values( + raw_values, + feature_type=feature_type_map[feature_name], + ) + + # Step 3: Reassemble row-wise + results: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] + + for i, entity_id in enumerate(ids): + doc = docs.get(entity_id) + + if doc is None: + results.append((None, None)) + continue + + # Entity document exists (written by some other feature view), but + # this specific feature view was never written → treat as not found. + fv_features = doc.get("features", {}).get(table.name) + if fv_features is None: + results.append((None, None)) + continue + + ts = doc.get("event_timestamps", {}).get(table.name) + + row_features = { + feature_name: proto_feature_columns[feature_name][i] + for feature_name in proto_feature_columns + } + + results.append((ts, row_features)) + return results + + async def online_read_async( + self, + config: RepoConfig, + table: FeatureView, + entity_keys: List[EntityKeyProto], + requested_features: Optional[List[str]] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + """ + Asynchronously reads feature values from the online store. + + Args: + config: Feast repo configuration + table: FeatureView to read from + entity_keys: List of entity keys to read + requested_features: Optional list of specific features to read + + Returns: + List of tuples (event_timestamp, feature_dict) for each entity key + """ + clxn = await self._get_collection_async(config) + + # Serialize entity keys + ids = [ + serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + for entity_key in entity_keys + ] + + query_filter = {"_id": {"$in": ids}} + projection = { + "_id": 1, + f"event_timestamps.{table.name}": 1, + } + if requested_features: + projection.update( + {f"features.{table.name}.{x}": 1 for x in requested_features} + ) + else: + projection[f"features.{table.name}"] = 1 + + cursor = clxn.find(query_filter, projection=projection) + docs = {doc["_id"]: doc async for doc in cursor} + + # Convert to proto format + return self._convert_raw_docs_to_proto(ids, docs, table) + + async def online_write_batch_async( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]] = None, + ) -> None: + """ + Asynchronously writes a batch of feature values to the online store. + + Args: + config: Feast repo configuration + table: FeatureView to write to + data: List of tuples (entity_key, features, event_ts, created_ts) + progress: Optional progress callback + """ + clxn = await self._get_collection_async(config) + ops = self._build_write_ops(config, table, data) + if ops: + await clxn.bulk_write(ops, ordered=False) + if progress: + progress(len(data)) + + +def _python_value_to_proto(value: Any) -> ValueProto: + """Best-effort conversion of a single Python value to ValueProto.""" + if isinstance(value, float): + return ValueProto(float_val=value) + elif isinstance(value, bool): + return ValueProto(bool_val=value) + elif isinstance(value, int): + return ValueProto(int64_val=value) + elif isinstance(value, str): + return ValueProto(string_val=value) + elif isinstance(value, bytes): + return ValueProto(bytes_val=value) + elif isinstance(value, list) and all(isinstance(v, float) for v in value): + proto = ValueProto() + proto.float_list_val.val.extend(value) + return proto + return ValueProto() diff --git a/sdk/python/feast/infra/online_stores/mongodb_online_store/mongodb_repo_configuration.py b/sdk/python/feast/infra/online_stores/mongodb_online_store/mongodb_repo_configuration.py new file mode 100644 index 00000000000..c621902ba54 --- /dev/null +++ b/sdk/python/feast/infra/online_stores/mongodb_online_store/mongodb_repo_configuration.py @@ -0,0 +1,13 @@ +from tests.universal.feature_repos.integration_test_repo_config import ( + IntegrationTestRepoConfig, +) +from tests.universal.feature_repos.universal.online_store.mongodb import ( + MongoDBOnlineStoreCreator, +) + +FULL_REPO_CONFIGS = [ + IntegrationTestRepoConfig( + online_store="mongodb", + online_store_creator=MongoDBOnlineStoreCreator, + ), +] diff --git a/sdk/python/feast/infra/online_stores/mysql_online_store/README.md b/sdk/python/feast/infra/online_stores/mysql_online_store/README.md index ac38237cd11..e9675085147 100644 --- a/sdk/python/feast/infra/online_stores/mysql_online_store/README.md +++ b/sdk/python/feast/infra/online_stores/mysql_online_store/README.md @@ -25,6 +25,9 @@ online_store: user: test # mysql user, default to test password: test # mysql password, default to test database: feast # mysql database, default to feast + batch_write: false # supporting batch write and commit per batch + batch_size: 100 # batch size, default to 100 + ``` #### Apply the feature definitions in `example.py` @@ -70,7 +73,6 @@ feature_vector = store.get_online_features( ], ).to_dict() pprint(feature_vector) - ``` #### Output ``` diff --git a/sdk/python/feast/infra/online_stores/mysql_online_store/mysql.py b/sdk/python/feast/infra/online_stores/mysql_online_store/mysql.py index d44eddfbd0b..415184ea248 100644 --- a/sdk/python/feast/infra/online_stores/mysql_online_store/mysql.py +++ b/sdk/python/feast/infra/online_stores/mysql_online_store/mysql.py @@ -10,6 +10,7 @@ from feast import Entity, FeatureView, RepoConfig from feast.infra.key_encoding_utils import serialize_entity_key +from feast.infra.online_stores.helpers import compute_table_id from feast.infra.online_stores.online_store import OnlineStore from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto @@ -30,6 +31,8 @@ class MySQLOnlineStoreConfig(FeastConfigBaseModel): password: Optional[StrictStr] = None database: Optional[StrictStr] = None port: Optional[int] = None + batch_write: Optional[bool] = False + batch_size: Optional[int] = None class MySQLOnlineStore(OnlineStore): @@ -51,7 +54,7 @@ def _get_conn(self, config: RepoConfig) -> Connection: password=online_store_config.password or "test", database=online_store_config.database or "feast", port=online_store_config.port or 3306, - autocommit=True, + autocommit=(not online_store_config.batch_write), ) return self._conn @@ -68,38 +71,120 @@ def online_write_batch( cur = conn.cursor() project = config.project - - for entity_key, values, timestamp, created_ts in data: - entity_key_bin = serialize_entity_key( - entity_key, - entity_key_serialization_version=3, - ).hex() - timestamp = to_naive_utc(timestamp) - if created_ts is not None: - created_ts = to_naive_utc(created_ts) - - for feature_name, val in values.items(): - self.write_to_table( - created_ts, - cur, - entity_key_bin, - feature_name, - project, - table, - timestamp, - val, - ) - conn.commit() - if progress: - progress(1) + versioning = config.registry.enable_online_feature_view_versioning + + batch_write = config.online_store.batch_write + if not batch_write: + for entity_key, values, timestamp, created_ts in data: + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=3, + ).hex() + timestamp = to_naive_utc(timestamp) + if created_ts is not None: + created_ts = to_naive_utc(created_ts) + + for feature_name, val in values.items(): + self.write_to_table( + created_ts, + cur, + entity_key_bin, + feature_name, + project, + table, + timestamp, + val, + versioning, + ) + conn.commit() + if progress: + progress(1) + else: + batch_size = config.online_store.bacth_size + if not batch_size or batch_size < 2: + raise ValueError("Batch size must be at least 2") + insert_values = [] + for entity_key, values, timestamp, created_ts in data: + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=2, + ).hex() + timestamp = to_naive_utc(timestamp) + if created_ts is not None: + created_ts = to_naive_utc(created_ts) + + for feature_name, val in values.items(): + serialized_val = val.SerializeToString() + insert_values.append( + ( + entity_key_bin, + feature_name, + serialized_val, + timestamp, + created_ts, + ) + ) + + if len(insert_values) >= batch_size: + try: + self._execute_batch( + cur, project, table, insert_values, versioning + ) + conn.commit() + if progress: + progress(len(insert_values)) + except Exception as e: + conn.rollback() + raise e + insert_values.clear() + + if insert_values: + try: + self._execute_batch(cur, project, table, insert_values, versioning) + conn.commit() + if progress: + progress(len(insert_values)) + except Exception as e: + conn.rollback() + raise e + + def _execute_batch( + self, cur, project, table, insert_values, enable_versioning=False + ): + table_name = _table_id(project, table, enable_versioning) + stmt = f""" + INSERT INTO {table_name} + (entity_key, feature_name, value, event_ts, created_ts) + values (%s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + value = VALUES(value), + event_ts = VALUES(event_ts), + created_ts = VALUES(created_ts); + """ + try: + cur.executemany(stmt, insert_values) + except Exception as e: + first_sample = insert_values[0] if insert_values else None + raise RuntimeError( + f"Failed to execute batch insert into table '{table_name}' " + f"(rows={len(insert_values)}, sample={first_sample}): {e}" + ) from e @staticmethod def write_to_table( - created_ts, cur, entity_key_bin, feature_name, project, table, timestamp, val + created_ts, + cur, + entity_key_bin, + feature_name, + project, + table, + timestamp, + val, + enable_versioning=False, ) -> None: cur.execute( f""" - INSERT INTO {_table_id(project, table)} + INSERT INTO {_table_id(project, table, enable_versioning)} (entity_key, feature_name, value, event_ts, created_ts) values (%s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE @@ -134,6 +219,7 @@ def online_read( result: List[Tuple[Optional[datetime], Optional[Dict[str, Any]]]] = [] project = config.project + versioning = config.registry.enable_online_feature_view_versioning for entity_key in entity_keys: entity_key_bin = serialize_entity_key( entity_key, @@ -141,7 +227,7 @@ def online_read( ).hex() cur.execute( - f"SELECT feature_name, value, event_ts FROM {_table_id(project, table)} WHERE entity_key = %s", + f"SELECT feature_name, value, event_ts FROM {_table_id(project, table, versioning)} WHERE entity_key = %s", (entity_key_bin,), ) @@ -173,10 +259,11 @@ def update( conn = self._get_conn(config) cur = conn.cursor() project = config.project + versioning = config.registry.enable_online_feature_view_versioning # We don't create any special state for the entities in this implementation. for table in tables_to_keep: - table_name = _table_id(project, table) + table_name = _table_id(project, table, versioning) index_name = f"{table_name}_ek" cur.execute( f"""CREATE TABLE IF NOT EXISTS {table_name} (entity_key VARCHAR(512), @@ -199,7 +286,10 @@ def update( ) for table in tables_to_delete: - _drop_table_and_index(cur, project, table) + if versioning: + _drop_all_version_tables(cur, project, table) + else: + _drop_table_and_index(cur, _table_id(project, table)) def teardown( self, @@ -210,16 +300,30 @@ def teardown( conn = self._get_conn(config) cur = conn.cursor() project = config.project + versioning = config.registry.enable_online_feature_view_versioning for table in tables: - _drop_table_and_index(cur, project, table) + if versioning: + _drop_all_version_tables(cur, project, table) + else: + _drop_table_and_index(cur, _table_id(project, table)) -def _drop_table_and_index(cur: Cursor, project: str, table: FeatureView) -> None: - table_name = _table_id(project, table) - cur.execute(f"DROP INDEX {table_name}_ek ON {table_name};") +def _drop_table_and_index(cur: Cursor, table_name: str) -> None: cur.execute(f"DROP TABLE IF EXISTS {table_name}") -def _table_id(project: str, table: FeatureView) -> str: - return f"{project}_{table.name}" +def _drop_all_version_tables(cur: Cursor, project: str, table: FeatureView) -> None: + """Drop the base table and all versioned tables (e.g. _v1, _v2, ...).""" + base = f"{project}_{table.name}" + cur.execute( + "SELECT table_name FROM information_schema.tables " + "WHERE table_schema = DATABASE() AND (table_name = %s OR table_name REGEXP %s)", + (base, f"^{base}_v[0-9]+$"), + ) + for (name,) in cur.fetchall(): + _drop_table_and_index(cur, name) + + +def _table_id(project: str, table: FeatureView, enable_versioning: bool = False) -> str: + return compute_table_id(project, table, enable_versioning) diff --git a/sdk/python/feast/infra/online_stores/mysql_online_store/mysql_repo_configuration.py b/sdk/python/feast/infra/online_stores/mysql_online_store/mysql_repo_configuration.py index 3e92ead2d0b..2944768097e 100644 --- a/sdk/python/feast/infra/online_stores/mysql_online_store/mysql_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/mysql_online_store/mysql_repo_configuration.py @@ -1,10 +1,12 @@ -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.mysql import ( +from tests.universal.feature_repos.universal.online_store.mysql import ( + BatchWriteMySQLOnlineStoreCreator, MySQLOnlineStoreCreator, ) FULL_REPO_CONFIGS = [ IntegrationTestRepoConfig(online_store_creator=MySQLOnlineStoreCreator), + IntegrationTestRepoConfig(online_store_creator=BatchWriteMySQLOnlineStoreCreator), ] diff --git a/sdk/python/feast/infra/online_stores/online_store.py b/sdk/python/feast/infra/online_stores/online_store.py index b77185229d5..cdf06639fe0 100644 --- a/sdk/python/feast/infra/online_stores/online_store.py +++ b/sdk/python/feast/infra/online_stores/online_store.py @@ -12,14 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio +import logging +import time as _time_mod from abc import ABC, abstractmethod from datetime import datetime from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union +from google.protobuf.timestamp_pb2 import Timestamp + from feast import Entity, utils from feast.batch_feature_view import BatchFeatureView +from feast.errors import VersionedOnlineReadNotSupported from feast.feature_service import FeatureService from feast.feature_view import FeatureView +from feast.filter_models import ComparisonFilter, CompoundFilter from feast.infra.infra_object import InfraObject from feast.infra.registry.base_registry import BaseRegistry from feast.infra.supported_async_methods import SupportedAsyncMethods @@ -30,6 +36,9 @@ from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import RepoConfig from feast.stream_feature_view import StreamFeatureView +from feast.value_type import ValueType + +logger = logging.getLogger(__name__) class OnlineStore(ABC): @@ -154,6 +163,7 @@ def get_online_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: if isinstance(entity_rows, list): columnar: Dict[str, List[Any]] = {k: [] for k in entity_rows[0].keys()} @@ -185,17 +195,157 @@ def get_online_features( native_entity_values=True, ) - for table, requested_features in grouped_refs: - # Get the correct set of entity values with the correct join keys. - table_entity_values, idxs, output_len = utils._get_unique_entities( - table, + # Check for versioned reads on unsupported stores + self._check_versioned_read_support(grouped_refs) + _track_read = False + try: + from feast.metrics import _config as _metrics_config + + _track_read = _metrics_config.online_features + except Exception: + pass + + if _track_read: + import time as _time + + _read_start = _time.monotonic() + + use_precomputed = ( + isinstance(features, FeatureService) + and getattr(features, "precompute_online", False) + and not requested_on_demand_feature_views + ) + + precomputed_ok = False + if use_precomputed and grouped_refs: + assert isinstance(features, FeatureService) + first_table = grouped_refs[0][0] + first_entity_values, first_idxs, output_len = utils._get_unique_entities( + first_table, join_key_values, entity_name_to_join_key_map + ) + entity_key_protos = utils._get_entity_key_protos(first_entity_values) + + blobs = self.read_precomputed_vectors( + config, features.name, project, entity_key_protos + ) + expected_names = self._compute_expected_feature_names( + grouped_refs, full_feature_names + ) + precomputed_ok = self._try_precomputed_fast_path( + blobs, + expected_names, + online_features_response, + full_feature_names, + output_len, + grouped_refs, + registry, + project, + ) + if not precomputed_ok: + raise RuntimeError( + f"FeatureService '{features.name}' has precompute_online=True " + f"but pre-computed vectors could not be read. " + f"Run `feast precompute {features.name}` or materialize to " + f"populate vectors." + ) + + if not precomputed_ok: + self._read_features_per_fv( + config, + grouped_refs, join_key_values, entity_name_to_join_key_map, + online_features_response, + full_feature_names, + include_feature_view_version_metadata, ) + if _track_read: + from feast.metrics import track_online_store_read + + track_online_store_read(_time.monotonic() - _read_start) + + feature_types = self._build_feature_types(grouped_refs) + + if requested_on_demand_feature_views: + utils._augment_response_with_on_demand_transforms( + online_features_response, + feature_refs, + requested_on_demand_feature_views, + full_feature_names, + feature_types=feature_types, + ) + + utils._drop_unneeded_columns( + online_features_response, requested_result_row_names + ) + return OnlineResponse(online_features_response, feature_types=feature_types) + + _versioned_read_supported: Optional[bool] = None + + def _check_versioned_read_support(self, grouped_refs): + """Raise an error if versioned reads are attempted on unsupported stores.""" + if self._versioned_read_supported is None: + self._versioned_read_supported = self._is_versioned_read_supported() + + if self._versioned_read_supported: + return + for table, _ in grouped_refs: + version_tag = getattr(table.projection, "version_tag", None) + if version_tag is not None: + raise VersionedOnlineReadNotSupported( + self.__class__.__name__, version_tag + ) + + def _is_versioned_read_supported(self) -> bool: + """Check if this store type supports versioned reads (resolved once, cached).""" + from feast.infra.online_stores.sqlite import SqliteOnlineStore + + supported_types: list[type] = [SqliteOnlineStore] + for module, cls_name in ( + ("feast.infra.online_stores.mysql_online_store.mysql", "MySQLOnlineStore"), + ( + "feast.infra.online_stores.postgres_online_store.postgres", + "PostgreSQLOnlineStore", + ), + ("feast.infra.online_stores.faiss_online_store", "FaissOnlineStore"), + ("feast.infra.online_stores.redis", "RedisOnlineStore"), + ("feast.infra.online_stores.dynamodb", "DynamoDBOnlineStore"), + ( + "feast.infra.online_stores.milvus_online_store.milvus", + "MilvusOnlineStore", + ), + ): + try: + import importlib + + mod = importlib.import_module(module) + supported_types.append(getattr(mod, cls_name)) + except Exception: + pass + return isinstance(self, tuple(supported_types)) + + def _read_features_per_fv( + self, + config: RepoConfig, + grouped_refs: List, + join_key_values: Dict, + entity_name_to_join_key_map: Dict, + online_features_response, + full_feature_names: bool, + include_feature_view_version_metadata: bool, + ) -> None: + """Read features one feature-view at a time (generic path). + + Subclasses may override to batch reads more efficiently (e.g. Redis + pipeline). + """ + for table, requested_features in grouped_refs: + table_entity_values, idxs, output_len = utils._get_unique_entities( + table, join_key_values, entity_name_to_join_key_map + ) entity_key_protos = utils._get_entity_key_protos(table_entity_values) - # Fetch data for Entities. read_rows = self.online_read( config=config, table=table, @@ -203,33 +353,88 @@ def get_online_features( requested_features=requested_features, ) - feature_data = utils._convert_rows_to_protobuf( - requested_features, read_rows - ) - - # Populate the result_rows with the Features from the OnlineStore inplace. utils._populate_response_from_feature_data( - feature_data, + requested_features, + read_rows, idxs, online_features_response, full_feature_names, - requested_features, table, output_len, + include_feature_view_version_metadata, ) - if requested_on_demand_feature_views: - utils._augment_response_with_on_demand_transforms( + async def _read_features_per_fv_async( + self, + config: RepoConfig, + grouped_refs: List, + join_key_values: Dict, + entity_name_to_join_key_map: Dict, + online_features_response, + full_feature_names: bool, + include_feature_view_version_metadata: bool, + ) -> None: + """Async version of :meth:`_read_features_per_fv`. + + Reads all feature views concurrently via ``asyncio.gather``. + Subclasses may override to batch reads more efficiently. + """ + + async def query_table(table, requested_features): + table_entity_values, idxs, output_len = utils._get_unique_entities( + table, join_key_values, entity_name_to_join_key_map + ) + entity_key_protos = utils._get_entity_key_protos(table_entity_values) + read_rows = await self.online_read_async( + config=config, + table=table, + entity_keys=entity_key_protos, + requested_features=requested_features, + ) + return idxs, read_rows, output_len + + all_responses = await asyncio.gather( + *[ + query_table(table, requested_features) + for table, requested_features in grouped_refs + ] + ) + + for (idxs, read_rows, output_len), (table, requested_features) in zip( + all_responses, grouped_refs + ): + utils._populate_response_from_feature_data( + requested_features, + read_rows, + idxs, online_features_response, - feature_refs, - requested_on_demand_feature_views, full_feature_names, + table, + output_len, + include_feature_view_version_metadata, ) - utils._drop_unneeded_columns( - online_features_response, requested_result_row_names + async def read_precomputed_vectors_async( + self, + config: RepoConfig, + feature_service_name: str, + project: str, + entity_keys: List[EntityKeyProto], + ) -> List[Optional[bytes]]: + """Async version of :meth:`read_precomputed_vectors`. + + The default implementation delegates to the sync method via the event + loop executor. Online stores with native async support should override. + """ + loop = asyncio.get_event_loop() + return await loop.run_in_executor( + None, + self.read_precomputed_vectors, + config, + feature_service_name, + project, + entity_keys, ) - return OnlineResponse(online_features_response) async def get_online_features_async( self, @@ -242,6 +447,7 @@ async def get_online_features_async( registry: BaseRegistry, project: str, full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: if isinstance(entity_rows, list): columnar: Dict[str, List[Any]] = {k: [] for k in entity_rows[0].keys()} @@ -273,63 +479,349 @@ async def get_online_features_async( native_entity_values=True, ) - async def query_table(table, requested_features): - # Get the correct set of entity values with the correct join keys. - table_entity_values, idxs, output_len = utils._get_unique_entities( - table, - join_key_values, - entity_name_to_join_key_map, - ) + # Check for versioned reads on unsupported stores + self._check_versioned_read_support(grouped_refs) - entity_key_protos = utils._get_entity_key_protos(table_entity_values) + _track_read = False + try: + from feast.metrics import _config as _metrics_config - # Fetch data for Entities. - read_rows = await self.online_read_async( - config=config, - table=table, - entity_keys=entity_key_protos, - requested_features=requested_features, - ) + _track_read = _metrics_config.online_features + except Exception: + pass - return idxs, read_rows, output_len + if _track_read: + import time as _time - all_responses = await asyncio.gather( - *[ - query_table(table, requested_features) - for table, requested_features in grouped_refs - ] + _read_start = _time.monotonic() + + use_precomputed = ( + isinstance(features, FeatureService) + and getattr(features, "precompute_online", False) + and not requested_on_demand_feature_views ) - for (idxs, read_rows, output_len), (table, requested_features) in zip( - all_responses, grouped_refs - ): - feature_data = utils._convert_rows_to_protobuf( - requested_features, read_rows + precomputed_ok = False + if use_precomputed and grouped_refs: + assert isinstance(features, FeatureService) + first_table = grouped_refs[0][0] + first_entity_values, first_idxs, output_len = utils._get_unique_entities( + first_table, join_key_values, entity_name_to_join_key_map ) + entity_key_protos = utils._get_entity_key_protos(first_entity_values) - # Populate the result_rows with the Features from the OnlineStore inplace. - utils._populate_response_from_feature_data( - feature_data, - idxs, + blobs = await self.read_precomputed_vectors_async( + config, features.name, project, entity_key_protos + ) + expected_names = self._compute_expected_feature_names( + grouped_refs, full_feature_names + ) + precomputed_ok = self._try_precomputed_fast_path( + blobs, + expected_names, online_features_response, full_feature_names, - requested_features, - table, output_len, + grouped_refs, + registry, + project, + ) + if not precomputed_ok: + raise RuntimeError( + f"FeatureService '{features.name}' has precompute_online=True " + f"but pre-computed vectors could not be read. " + f"Run `feast precompute {features.name}` or materialize to " + f"populate vectors." + ) + + if not precomputed_ok: + await self._read_features_per_fv_async( + config, + grouped_refs, + join_key_values, + entity_name_to_join_key_map, + online_features_response, + full_feature_names, + include_feature_view_version_metadata, ) + if _track_read: + from feast.metrics import track_online_store_read + + track_online_store_read(_time.monotonic() - _read_start) + + feature_types = self._build_feature_types(grouped_refs) + if requested_on_demand_feature_views: utils._augment_response_with_on_demand_transforms( online_features_response, feature_refs, requested_on_demand_feature_views, full_feature_names, + feature_types=feature_types, ) utils._drop_unneeded_columns( online_features_response, requested_result_row_names ) - return OnlineResponse(online_features_response) + return OnlineResponse(online_features_response, feature_types=feature_types) + + @staticmethod + def _build_feature_types( + grouped_refs: List, + ) -> Dict[str, ValueType]: + """Build a mapping of feature names to ValueType from grouped feature view refs. + + Includes both bare names and prefixed names (feature_view__feature) so that + lookups succeed regardless of the full_feature_names setting. + """ + feature_types: Dict[str, ValueType] = {} + for table, requested_features in grouped_refs: + table_name = table.projection.name_to_use() + for field in table.features: + if field.name in requested_features: + vtype = field.dtype.to_value_type() + feature_types[field.name] = vtype + feature_types[f"{table_name}__{field.name}"] = vtype + return feature_types + + @staticmethod + def _compute_expected_feature_names( + grouped_refs: List, full_feature_names: bool + ) -> List[str]: + """Derive the deterministic feature name list for schema comparison.""" + names: List[str] = [] + for table, requested_features in grouped_refs: + table_name = table.projection.name_to_use() + for fn in requested_features: + if fn.startswith("_ts:"): + continue + names.append(f"{table_name}__{fn}" if full_feature_names else fn) + return names + + @staticmethod + def _try_precomputed_fast_path( + blobs: List[Optional[bytes]], + expected_feature_names: List[str], + online_features_response: Any, + full_feature_names: bool, + num_rows: int, + grouped_refs: List, + registry: BaseRegistry, + project: str, + ) -> bool: + """Build the response from pre-computed vectors. + + Returns True if the fast path succeeded for ALL entities, False otherwise + (caller should fall back to per-FV reads). + """ + from feast.protos.feast.core.PrecomputedFeatureVector_pb2 import ( + PrecomputedFeatureVector, + ) + from feast.protos.feast.serving.ServingService_pb2 import ( + FieldStatus, + GetOnlineFeaturesResponse, + ) + + for i, blob in enumerate(blobs): + if blob is None: + logger.warning( + "Pre-computed vector blob is None for entity index %d", i + ) + return False + + n_features = len(expected_feature_names) + expected_set = set(expected_feature_names) + + # Pre-compute feature-index-to-FV-name mapping once (not per entity). + feat_fv_names: List[Optional[str]] = [] + for fname in expected_feature_names: + feat_fv_names.append(fname.split("__", 1)[0] if "__" in fname else None) + + # Pre-compute FV TTLs once. + fv_ttls: Dict[str, Optional[int]] = {} + for table, _ in grouped_refs: + ttl = table.ttl + fv_ttls[table.projection.name_to_use()] = ( + int(ttl.total_seconds()) if ttl else None + ) + + # Check if any FV actually has a TTL — skip TTL logic entirely if not. + any_ttl = any(v is not None for v in fv_ttls.values()) + + # Parse all blobs, validate schema, build reorder map. + # Schema is typically identical for all entities, so validate against the + # first and then just verify the rest match the first (not the expected list). + first_stored_names: Optional[List[str]] = None + reorder_map: Optional[List[int]] = None + vectors: List[PrecomputedFeatureVector] = [] + + for blob in blobs: + vec = PrecomputedFeatureVector() + vec.ParseFromString(blob) # type: ignore[arg-type] + vectors.append(vec) + + if first_stored_names is None: + first_stored_names = list(vec.feature_names) + if set(first_stored_names) != expected_set: + logger.warning( + "Pre-computed vector schema mismatch: stored=%s, expected=%s", + first_stored_names, + expected_feature_names, + ) + return False + if first_stored_names != expected_feature_names: + name_to_idx = {n: i for i, n in enumerate(first_stored_names)} + reorder_map = [name_to_idx[n] for n in expected_feature_names] + else: + if ( + len(vec.feature_names) != n_features + or list(vec.feature_names) != first_stored_names + ): + logger.warning( + "Pre-computed vector schema varies across entities at index %d", + len(vectors) - 1, + ) + return False + + PRESENT = FieldStatus.PRESENT + OUTSIDE_MAX_AGE = FieldStatus.OUTSIDE_MAX_AGE + null_value = ValueProto() + null_ts = Timestamp() + + feat_values = [[null_value] * num_rows for _ in range(n_features)] + feat_statuses = [[FieldStatus.NOT_FOUND] * num_rows for _ in range(n_features)] + ts_list = [null_ts] * num_rows + + now_secs = _time_mod.time() if any_ttl else 0.0 + + for row_idx, vec in enumerate(vectors): + ts_list[row_idx] = vec.precomputed_at + + # Build per-FV expiry flags once per entity (not per feature). + fv_expired: Optional[Dict[str, bool]] = None + if any_ttl: + fv_ts_map: Dict[str, Timestamp] = { + fvt.feature_view_name: fvt.event_timestamp + for fvt in vec.fv_timestamps + } + fv_expired = {} + for fv_name, ttl_val in fv_ttls.items(): + if ttl_val is not None: + event_ts = fv_ts_map.get(fv_name) + if event_ts: + event_secs = event_ts.seconds + event_ts.nanos / 1e9 + fv_expired[fv_name] = (now_secs - event_secs) > ttl_val + else: + fv_expired[fv_name] = False + else: + fv_expired[fv_name] = False + + stored_values = vec.values + for out_idx in range(n_features): + src_idx = reorder_map[out_idx] if reorder_map else out_idx + feat_values[out_idx][row_idx] = stored_values[src_idx] + + if fv_expired: + feat_fv = feat_fv_names[out_idx] + if feat_fv and fv_expired.get(feat_fv, False): + feat_statuses[out_idx][row_idx] = OUTSIDE_MAX_AGE + continue + feat_statuses[out_idx][row_idx] = PRESENT + + online_features_response.metadata.feature_names.val.extend( + expected_feature_names + ) + for f_idx in range(n_features): + online_features_response.results.append( + GetOnlineFeaturesResponse.FeatureVector( + values=feat_values[f_idx], + statuses=feat_statuses[f_idx], + event_timestamps=ts_list, + ) + ) + return True + + def write_precomputed_vector( + self, + config: RepoConfig, + feature_service_name: str, + project: str, + entity_key: EntityKeyProto, + vector_bytes: bytes, + ) -> None: + """Write a pre-computed feature vector blob for a single entity. + + Stores the blob as a ``bytes_val`` feature under a synthetic FeatureView + named ``__precomputed__{service_name}``, using the generic + ``online_write_batch`` API so this works for every online store without + store-specific overrides. + """ + from feast.field import Field + from feast.types import Bytes + + synthetic_fv = FeatureView( + name=f"__precomputed__{feature_service_name}", + entities=[], + schema=[Field(name="vector", dtype=Bytes)], + source=None, + ) + val = ValueProto(bytes_val=vector_bytes) + ts = datetime.utcnow() + self.online_write_batch( + config=config, + table=synthetic_fv, + data=[(entity_key, {"vector": val}, ts, None)], + progress=None, + ) + + def read_precomputed_vectors( + self, + config: RepoConfig, + feature_service_name: str, + project: str, + entity_keys: List[EntityKeyProto], + ) -> List[Optional[bytes]]: + """Read pre-computed feature vector blobs for a batch of entities. + + Returns a list aligned with *entity_keys*. Each element is either the + serialized ``PrecomputedFeatureVector`` bytes or ``None`` when no + pre-computed vector exists for that entity. + + Reads from the synthetic FeatureView written by + :meth:`write_precomputed_vector` using the generic ``online_read`` API, + so this works for every online store without store-specific overrides. + """ + from feast.field import Field + from feast.types import Bytes + + synthetic_fv = FeatureView( + name=f"__precomputed__{feature_service_name}", + entities=[], + schema=[Field(name="vector", dtype=Bytes)], + source=None, + ) + try: + rows = self.online_read( + config=config, + table=synthetic_fv, + entity_keys=entity_keys, + requested_features=["vector"], + ) + except Exception: + return [None] * len(entity_keys) + + result: List[Optional[bytes]] = [] + for _ts, feature_dict in rows: + if feature_dict and "vector" in feature_dict: + val = feature_dict["vector"] + if val.HasField("bytes_val"): + result.append(val.bytes_val) + else: + result.append(None) + else: + result.append(None) + return result @abstractmethod def update( @@ -436,6 +928,8 @@ def retrieve_online_documents_v2( top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> List[ Tuple[ Optional[datetime], @@ -454,6 +948,8 @@ def retrieve_online_documents_v2( embedding: The embeddings to use for retrieval (optional) top_k: The number of documents to retrieve. query_string: The query string to search for using keyword search (bm25) (optional) + filters: Optional metadata filters (ComparisonFilter or CompoundFilter) + to narrow results before ranking. Returns: object: A list of top k closest documents to the specified embedding. Each item in the list is a tuple diff --git a/sdk/python/feast/infra/online_stores/postgres_online_store/pgvector_repo_configuration.py b/sdk/python/feast/infra/online_stores/postgres_online_store/pgvector_repo_configuration.py index 26b05613158..927c424f207 100644 --- a/sdk/python/feast/infra/online_stores/postgres_online_store/pgvector_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/postgres_online_store/pgvector_repo_configuration.py @@ -1,7 +1,7 @@ -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.postgres import ( +from tests.universal.feature_repos.universal.online_store.postgres import ( PGVectorOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/postgres_online_store/postgres.py b/sdk/python/feast/infra/online_stores/postgres_online_store/postgres.py index 717712cf233..09be88c991d 100644 --- a/sdk/python/feast/infra/online_stores/postgres_online_store/postgres.py +++ b/sdk/python/feast/infra/online_stores/postgres_online_store/postgres.py @@ -21,8 +21,19 @@ from psycopg_pool import AsyncConnectionPool, ConnectionPool from feast import Entity, FeatureView, ValueType +from feast.filter_models import ( + ComparisonFilter, + CompoundFilter, + FilterTranslator, + FilterType, + filters_contain_numeric_comparison, +) from feast.infra.key_encoding_utils import get_list_val_str, serialize_entity_key -from feast.infra.online_stores.helpers import _to_naive_utc +from feast.infra.online_stores.helpers import ( + _to_naive_utc, + compute_table_id, + extract_text_and_num, +) from feast.infra.online_stores.online_store import OnlineStore from feast.infra.online_stores.vector_store import VectorStoreConfig from feast.infra.utils.postgres.connection_utils import ( @@ -44,9 +55,110 @@ "inner_product": "<#>", } +_PG_COMPARISON_OPS: Dict[str, str] = { + "eq": "=", + "ne": "!=", + "gt": ">", + "gte": ">=", + "lt": "<", + "lte": "<=", +} + + +class PostgresFilterTranslator(FilterTranslator): + """Translates Feast filters into Postgres SQL WHERE clause fragments.""" + + def __init__(self, table_name: str, alias: Optional[str] = None): + self.table_name = table_name + self.alias = alias + + def translate(self, filters: FilterType) -> Tuple[sql.Composable, List[Any]]: + if filters is None: + return sql.SQL(""), [] + return self._dispatch(filters) + + def translate_comparison( + self, f: ComparisonFilter + ) -> Tuple[sql.Composable, List[Any]]: + key, value, op_type = f.key, f.value, f.type + ek_col = f"{self.alias}.entity_key" if self.alias else "entity_key" + + if op_type in _PG_COMPARISON_OPS: + col, db_value = _pg_filter_col_and_val(value) + clause = sql.SQL( + "{ek_col} IN (SELECT entity_key FROM {tbl} WHERE feature_name = %s AND {col} {op} %s)" + ).format( + ek_col=sql.SQL(ek_col), + tbl=sql.Identifier(self.table_name), + col=sql.Identifier(col), + op=sql.SQL(_PG_COMPARISON_OPS[op_type]), + ) + return clause, [key, db_value] + + if op_type == "in": + if not isinstance(value, list): + raise ValueError( + f"'in' filter requires a list value, got {type(value)}" + ) + placeholders = sql.SQL(", ").join([sql.Placeholder()] * len(value)) + col, _ = _pg_filter_col_and_val(value[0]) if value else ("value_text", None) + db_values = [_pg_filter_col_and_val(v)[1] for v in value] + clause = sql.SQL( + "{ek_col} IN (SELECT entity_key FROM {tbl} WHERE feature_name = %s AND {col} IN ({phs}))" + ).format( + ek_col=sql.SQL(ek_col), + tbl=sql.Identifier(self.table_name), + col=sql.Identifier(col), + phs=placeholders, + ) + return clause, [key] + db_values + + if op_type == "nin": + if not isinstance(value, list): + raise ValueError( + f"'nin' filter requires a list value, got {type(value)}" + ) + placeholders = sql.SQL(", ").join([sql.Placeholder()] * len(value)) + col, _ = _pg_filter_col_and_val(value[0]) if value else ("value_text", None) + db_values = [_pg_filter_col_and_val(v)[1] for v in value] + clause = sql.SQL( + "{ek_col} IN (SELECT entity_key FROM {tbl} WHERE feature_name = %s AND {col} NOT IN ({phs}))" + ).format( + ek_col=sql.SQL(ek_col), + tbl=sql.Identifier(self.table_name), + col=sql.Identifier(col), + phs=placeholders, + ) + return clause, [key] + db_values + + raise ValueError(f"Unknown comparison operator: {op_type}") + + def translate_compound(self, f: CompoundFilter) -> Tuple[sql.Composable, List[Any]]: + if not f.filters: + return sql.SQL(""), [] + parts: List[sql.Composable] = [] + all_params: List[Any] = [] + for sub in f.filters: + sub_clause, sub_params = self._dispatch(sub) + parts.append(sub_clause) + all_params.extend(sub_params) + joiner = sql.SQL(" AND " if f.type == "and" else " OR ") + combined = sql.SQL("(") + joiner.join(parts) + sql.SQL(")") + return combined, all_params + + +def _pg_filter_col_and_val(value: Any) -> Tuple[str, Any]: + """Return the appropriate column name and DB-ready value for a filter value.""" + if isinstance(value, bool): + return "value_num", 1.0 if value else 0.0 + if isinstance(value, (int, float)): + return "value_num", float(value) + return "value_text", str(value) + class PostgreSQLOnlineStoreConfig(PostgreSQLConfig, VectorStoreConfig): type: Literal["postgres"] = "postgres" + enable_openai_compatible_store: Optional[bool] = False class PostgreSQLOnlineStore(OnlineStore): @@ -55,6 +167,7 @@ class PostgreSQLOnlineStore(OnlineStore): _conn_async: Optional[AsyncConnection] = None _conn_pool_async: Optional[AsyncConnectionPool] = None + _table_has_value_num: Optional[Dict[str, bool]] = None @contextlib.contextmanager def _get_conn( @@ -96,6 +209,33 @@ async def _get_conn_async( await self._conn_async.set_autocommit(autocommit) yield self._conn_async + def _check_table_has_value_num( + self, cur, table_name: str, config: RepoConfig + ) -> bool: + """Check if the value_num column exists in the given table, with caching.""" + if self._table_has_value_num is None: + self._table_has_value_num = {} + if table_name in self._table_has_value_num: + return self._table_has_value_num[table_name] + + schema_name = config.online_store.db_schema or config.online_store.user + cur.execute( + """ + SELECT 1 FROM information_schema.columns + WHERE table_schema = %s AND table_name = %s AND column_name = 'value_num' + """, + (schema_name, table_name), + ) + exists = cur.fetchone() is not None + self._table_has_value_num[table_name] = exists + return exists + + @staticmethod + def _filters_need_value_num( + filters: Union[ComparisonFilter, CompoundFilter], + ) -> bool: + return filters_contain_numeric_comparison(filters) + def online_write_batch( self, config: RepoConfig, @@ -105,57 +245,71 @@ def online_write_batch( ], progress: Optional[Callable[[int], Any]], ) -> None: - # Format insert values - insert_values = [] - for entity_key, values, timestamp, created_ts in data: - entity_key_bin = serialize_entity_key( - entity_key, - entity_key_serialization_version=config.entity_key_serialization_version, + table_name = _table_id( + config.project, table, config.registry.enable_online_feature_view_versioning + ) + + with self._get_conn(config) as conn, conn.cursor() as cur: + enable_value_num = getattr( + config.online_store, "enable_openai_compatible_store", False ) - timestamp = _to_naive_utc(timestamp) - if created_ts is not None: - created_ts = _to_naive_utc(created_ts) + has_value_num_col = self._check_table_has_value_num(cur, table_name, config) + compute_value_num = has_value_num_col + if enable_value_num and not has_value_num_col: + logging.warning( + "enable_openai_compatible_store is True but value_num column " + "not found in table '%s'. Run `feast apply` to add it. " + "Writing without value_num.", + table_name, + ) + compute_value_num = False + + columns = ["entity_key", "feature_name", "value", "value_text"] + if has_value_num_col: + columns.append("value_num") + columns.extend(["vector_value", "event_ts", "created_ts"]) + + pk_cols = {"entity_key", "feature_name"} + col_ids = sql.SQL(", ").join(sql.Identifier(c) for c in columns) + placeholders = sql.SQL(", ").join([sql.Placeholder()] * len(columns)) + update_set = sql.SQL(", ").join( + sql.SQL("{} = EXCLUDED.{}").format(sql.Identifier(c), sql.Identifier(c)) + for c in columns + if c not in pk_cols + ) + sql_query = sql.SQL( + "INSERT INTO {} ({}) VALUES ({}) " + "ON CONFLICT (entity_key, feature_name) DO UPDATE SET {};" + ).format(sql.Identifier(table_name), col_ids, placeholders, update_set) + + insert_values: List[Tuple[Any, ...]] = [] + for entity_key, values, timestamp, created_ts in data: + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + timestamp = _to_naive_utc(timestamp) + if created_ts is not None: + created_ts = _to_naive_utc(created_ts) - for feature_name, val in values.items(): - vector_val = None - value_text = None + for feature_name, val in values.items(): + value_text, value_num = extract_text_and_num(val, compute_value_num) - # Check if the feature type is STRING - if val.WhichOneof("val") == "string_val": - value_text = val.string_val + vector_val = None + if config.online_store.vector_enabled: + vector_val = get_list_val_str(val) - if config.online_store.vector_enabled: - vector_val = get_list_val_str(val) - insert_values.append( - ( + row: List[Any] = [ entity_key_bin, feature_name, val.SerializeToString(), value_text, - vector_val, - timestamp, - created_ts, - ) - ) - - # Create insert query - sql_query = sql.SQL( - """ - INSERT INTO {} - (entity_key, feature_name, value, value_text, vector_value, event_ts, created_ts) - VALUES (%s, %s, %s, %s, %s, %s, %s) - ON CONFLICT (entity_key, feature_name) DO - UPDATE SET - value = EXCLUDED.value, - value_text = EXCLUDED.value_text, - vector_value = EXCLUDED.vector_value, - event_ts = EXCLUDED.event_ts, - created_ts = EXCLUDED.created_ts; - """ - ).format(sql.Identifier(_table_id(config.project, table))) + ] + if has_value_num_col: + row.append(value_num) + row.extend([vector_val, timestamp, created_ts]) + insert_values.append(tuple(row)) - # Push data into the online store - with self._get_conn(config) as conn, conn.cursor() as cur: cur.executemany(sql_query, insert_values) conn.commit() @@ -214,7 +368,13 @@ def _construct_query_and_params( FROM {} WHERE entity_key = ANY(%s) AND feature_name = ANY(%s); """ ).format( - sql.Identifier(_table_id(config.project, table)), + sql.Identifier( + _table_id( + config.project, + table, + config.registry.enable_online_feature_view_versioning, + ) + ), ) params = (keys, requested_features) else: @@ -224,7 +384,13 @@ def _construct_query_and_params( FROM {} WHERE entity_key = ANY(%s); """ ).format( - sql.Identifier(_table_id(config.project, table)), + sql.Identifier( + _table_id( + config.project, + table, + config.registry.enable_online_feature_view_versioning, + ) + ), ) params = (keys, []) return query, params @@ -304,12 +470,16 @@ def update( ), ) + versioning = config.registry.enable_online_feature_view_versioning for table in tables_to_delete: - table_name = _table_id(project, table) - cur.execute(_drop_table_and_index(table_name)) + if versioning: + _drop_all_version_tables(cur, project, table, schema_name) + else: + table_name = _table_id(project, table) + cur.execute(_drop_table_and_index(table_name)) for table in tables_to_keep: - table_name = _table_id(project, table) + table_name = _table_id(project, table, versioning) if config.online_store.vector_enabled: vector_value_type = "vector" else: @@ -320,6 +490,14 @@ def update( f.dtype.to_value_type() == ValueType.STRING for f in table.features ) + value_num_col = ( + sql.SQL("value_num DOUBLE PRECISION NULL,") + if getattr( + config.online_store, "enable_openai_compatible_store", False + ) + else sql.SQL("") + ) + cur.execute( sql.SQL( """ @@ -328,7 +506,8 @@ def update( entity_key BYTEA, feature_name TEXT, value BYTEA, - value_text TEXT NULL, -- Added for FTS + value_text TEXT NULL, + {} vector_value {} NULL, event_ts TIMESTAMPTZ, created_ts TIMESTAMPTZ, @@ -338,12 +517,25 @@ def update( """ ).format( sql.Identifier(table_name), + value_num_col, sql.SQL(vector_value_type), sql.Identifier(f"{table_name}_ek"), sql.Identifier(table_name), ) ) + if getattr( + config.online_store, "enable_openai_compatible_store", False + ): + cur.execute( + sql.SQL( + """ALTER TABLE {} ADD COLUMN IF NOT EXISTS value_num DOUBLE PRECISION NULL;""" + ).format(sql.Identifier(table_name)) + ) + if self._table_has_value_num is None: + self._table_has_value_num = {} + self._table_has_value_num[table_name] = True + if has_string_features: cur.execute( sql.SQL( @@ -363,11 +555,16 @@ def teardown( entities: Sequence[Entity], ): project = config.project + schema_name = config.online_store.db_schema or config.online_store.user + versioning = config.registry.enable_online_feature_view_versioning try: with self._get_conn(config) as conn, conn.cursor() as cur: for table in tables: - table_name = _table_id(project, table) - cur.execute(_drop_table_and_index(table_name)) + if versioning: + _drop_all_version_tables(cur, project, table, schema_name) + else: + table_name = _table_id(project, table) + cur.execute(_drop_table_and_index(table_name)) conn.commit() except Exception: logging.exception("Teardown failed") @@ -432,7 +629,9 @@ def retrieve_online_documents( ] ] = [] with self._get_conn(config, autocommit=True) as conn, conn.cursor() as cur: - table_name = _table_id(project, table) + table_name = _table_id( + project, table, config.registry.enable_online_feature_view_versioning + ) # Search query template to find the top k items that are closest to the given embedding # SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5; @@ -488,6 +687,8 @@ def retrieve_online_documents_v2( top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> List[ Tuple[ Optional[datetime], @@ -516,6 +717,16 @@ def retrieve_online_documents_v2( if embedding is None and query_string is None: raise ValueError("Either embedding or query_string must be provided") + if filters is not None: + if not getattr( + config.online_store, "enable_openai_compatible_store", False + ): + raise ValueError( + "Metadata filtering requires `enable_openai_compatible_store: true` " + "in your online store config. After setting it, run `feast apply` " + "to update the database schema." + ) + distance_metric = distance_metric or "L2" if distance_metric not in SUPPORTED_DISTANCE_METRICS_DICT: @@ -532,71 +743,184 @@ def retrieve_online_documents_v2( and feature.name in requested_features ] - table_name = _table_id(config.project, table) + table_name = _table_id( + config.project, + table, + config.registry.enable_online_feature_view_versioning, + ) with self._get_conn(config, autocommit=True) as conn, conn.cursor() as cur: + if filters is not None and self._filters_need_value_num(filters): + if not self._check_table_has_value_num(cur, table_name, config): + raise ValueError( + "Numerical filtering requires the `value_num` column. " + "Run `feast apply` to add it." + ) + query = None params: Any = None + filter_clause, filter_params = PostgresFilterTranslator( + table_name, alias="t1" + ).translate(filters) + has_filters = bool(filter_params) or ( + filters is not None + and not filter_params + and filter_clause.as_string(conn) != "" + ) + if embedding is not None and query_string is not None and string_fields: # Case 1: Hybrid Search (vector + text) tsquery_str = " & ".join(query_string.split()) + + outer_where_parts: list[sql.Composable] = [ + sql.SQL("t1.feature_name = ANY(%s)") + ] + if has_filters: + outer_where_parts.append(filter_clause) + outer_where = sql.SQL(" AND ").join(outer_where_parts) + query = sql.SQL( """ + WITH vector_candidates AS ( + SELECT entity_key, + MIN(vector_value {distance_metric_sql} %s::vector) as distance + FROM {table_name} + WHERE vector_value IS NOT NULL + GROUP BY entity_key + ORDER BY distance + LIMIT {top_k} + ), + text_candidates AS ( + SELECT entity_key, + MAX(ts_rank(to_tsvector('english', value_text), to_tsquery('english', %s))) as text_rank + FROM {table_name} + WHERE feature_name = ANY(%s) + AND to_tsvector('english', value_text) @@ to_tsquery('english', %s) + GROUP BY entity_key + ORDER BY text_rank DESC + LIMIT {top_k} + ), + all_candidates AS ( + SELECT entity_key FROM vector_candidates + UNION + SELECT entity_key FROM text_candidates + ), + scored AS ( + SELECT + ac.entity_key, + COALESCE(vc.distance, + (SELECT MIN(t.vector_value {distance_metric_sql} %s::vector) + FROM {table_name} t + WHERE t.entity_key = ac.entity_key AND t.vector_value IS NOT NULL) + ) as distance, + COALESCE(tc.text_rank, + COALESCE( + (SELECT MAX(ts_rank(to_tsvector('english', ft.value_text), to_tsquery('english', %s))) + FROM {table_name} ft + WHERE ft.entity_key = ac.entity_key AND ft.feature_name = ANY(%s) AND ft.value_text IS NOT NULL), + 0 + ) + ) as text_rank + FROM all_candidates ac + LEFT JOIN vector_candidates vc ON ac.entity_key = vc.entity_key + LEFT JOIN text_candidates tc ON ac.entity_key = tc.entity_key + ORDER BY text_rank DESC, distance + LIMIT {top_k} + ) SELECT - entity_key, - feature_name, - value, - vector_value, - vector_value {distance_metric_sql} %s::vector as distance, - ts_rank(to_tsvector('english', value_text), to_tsquery('english', %s)) as text_rank, - event_ts, - created_ts - FROM {table_name} - WHERE feature_name = ANY(%s) AND to_tsvector('english', value_text) @@ to_tsquery('english', %s) - ORDER BY distance - LIMIT {top_k} + t1.entity_key, + t1.feature_name, + t1.value, + t1.vector_value, + s.distance, + s.text_rank, + t1.event_ts, + t1.created_ts + FROM {table_name} t1 + INNER JOIN scored s ON t1.entity_key = s.entity_key + WHERE {outer_where} + ORDER BY s.text_rank DESC, s.distance """ ).format( distance_metric_sql=sql.SQL(distance_metric_sql), table_name=sql.Identifier(table_name), + outer_where=outer_where, top_k=sql.Literal(top_k), ) - params = (embedding, tsquery_str, string_fields, tsquery_str) + base_params: list[Any] = [ + embedding, + tsquery_str, + string_fields, + tsquery_str, + embedding, + tsquery_str, + string_fields, + requested_features, + ] + if has_filters: + base_params.extend(filter_params) + params = tuple(base_params) elif embedding is not None: # Case 2: Vector Search Only + outer_where_parts = [sql.SQL("t1.feature_name = ANY(%s)")] + if has_filters: + outer_where_parts.append(filter_clause) + outer_where = sql.SQL(" AND ").join(outer_where_parts) + query = sql.SQL( """ + WITH vector_matches AS ( + SELECT entity_key, + MIN(vector_value {distance_metric_sql} %s::vector) as distance + FROM {table_name} + WHERE vector_value IS NOT NULL + GROUP BY entity_key + ORDER BY distance + LIMIT {top_k} + ) SELECT - entity_key, - feature_name, - value, - vector_value, - vector_value {distance_metric_sql} %s::vector as distance, - NULL as text_rank, -- Keep consistent columns - event_ts, - created_ts - FROM {table_name} - ORDER BY distance - LIMIT {top_k} + t1.entity_key, + t1.feature_name, + t1.value, + t1.vector_value, + t2.distance, + NULL as text_rank, + t1.event_ts, + t1.created_ts + FROM {table_name} t1 + INNER JOIN vector_matches t2 ON t1.entity_key = t2.entity_key + WHERE {outer_where} + ORDER BY t2.distance """ ).format( distance_metric_sql=sql.SQL(distance_metric_sql), table_name=sql.Identifier(table_name), + outer_where=outer_where, top_k=sql.Literal(top_k), ) - params = (embedding,) + base_params = [embedding, requested_features] + if has_filters: + base_params.extend(filter_params) + params = tuple(base_params) elif query_string is not None and string_fields: # Case 3: Text Search Only tsquery_str = " & ".join(query_string.split()) + + outer_where_parts = [sql.SQL("t1.feature_name = ANY(%s)")] + if has_filters: + outer_where_parts.append(filter_clause) + outer_where = sql.SQL(" AND ").join(outer_where_parts) + query = sql.SQL( """ WITH text_matches AS ( SELECT DISTINCT entity_key, ts_rank(to_tsvector('english', value_text), to_tsquery('english', %s)) as text_rank FROM {table_name} - WHERE feature_name = ANY(%s) AND to_tsvector('english', value_text) @@ to_tsquery('english', %s) + WHERE feature_name = ANY(%s) + AND to_tsvector('english', value_text) @@ to_tsquery('english', %s) ORDER BY text_rank DESC LIMIT {top_k} ) @@ -611,14 +935,23 @@ def retrieve_online_documents_v2( t1.created_ts FROM {table_name} t1 INNER JOIN text_matches t2 ON t1.entity_key = t2.entity_key - WHERE t1.feature_name = ANY(%s) + WHERE {outer_where} ORDER BY t2.text_rank DESC """ ).format( table_name=sql.Identifier(table_name), + outer_where=outer_where, top_k=sql.Literal(top_k), ) - params = (tsquery_str, string_fields, tsquery_str, requested_features) + base_params = [ + tsquery_str, + string_fields, + tsquery_str, + requested_features, + ] + if has_filters: + base_params.extend(filter_params) + params = tuple(base_params) else: raise ValueError( @@ -685,10 +1018,11 @@ def retrieve_online_documents_v2( sorted_entities = sorted( entities_dict.values(), - key=lambda x: x["vector_distance"] - if embedding is not None - else x["text_rank"], - reverse=(embedding is None), + key=lambda x: ( + (-x["text_rank"], x["vector_distance"]) + if query_string is not None + else (x["vector_distance"],) + ), )[:top_k] result: List[ @@ -728,8 +1062,8 @@ def retrieve_online_documents_v2( return result -def _table_id(project: str, table: FeatureView) -> str: - return f"{project}_{table.name}" +def _table_id(project: str, table: FeatureView, enable_versioning: bool = False) -> str: + return compute_table_id(project, table, enable_versioning) def _drop_table_and_index(table_name): @@ -742,3 +1076,23 @@ def _drop_table_and_index(table_name): sql.Identifier(table_name), sql.Identifier(f"{table_name}_ek"), ) + + +def _drop_all_version_tables( + cur, project: str, table: FeatureView, schema_name: Optional[str] = None +) -> None: + """Drop the base table and all versioned tables (e.g. _v1, _v2, ...).""" + base = f"{project}_{table.name}" + if schema_name: + cur.execute( + "SELECT tablename FROM pg_tables " + "WHERE schemaname = %s AND (tablename = %s OR tablename ~ %s)", + (schema_name, base, f"^{base}_v[0-9]+$"), + ) + else: + cur.execute( + "SELECT tablename FROM pg_tables WHERE tablename = %s OR tablename ~ %s", + (base, f"^{base}_v[0-9]+$"), + ) + for (name,) in cur.fetchall(): + cur.execute(_drop_table_and_index(name)) diff --git a/sdk/python/feast/infra/online_stores/postgres_online_store/postgres_repo_configuration.py b/sdk/python/feast/infra/online_stores/postgres_online_store/postgres_repo_configuration.py index ea975ec808f..b28abf955ef 100644 --- a/sdk/python/feast/infra/online_stores/postgres_online_store/postgres_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/postgres_online_store/postgres_repo_configuration.py @@ -1,7 +1,7 @@ -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.postgres import ( +from tests.universal.feature_repos.universal.online_store.postgres import ( PostgresOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/qdrant_online_store/qdrant_repo_configuration.py b/sdk/python/feast/infra/online_stores/qdrant_online_store/qdrant_repo_configuration.py index eee77bb8775..0cae0fae62c 100644 --- a/sdk/python/feast/infra/online_stores/qdrant_online_store/qdrant_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/qdrant_online_store/qdrant_repo_configuration.py @@ -1,7 +1,7 @@ -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.qdrant import ( +from tests.universal.feature_repos.universal.online_store.qdrant import ( QdrantOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/redis.py b/sdk/python/feast/infra/online_stores/redis.py index 59892fcbe0f..b50161bd6de 100644 --- a/sdk/python/feast/infra/online_stores/redis.py +++ b/sdk/python/feast/infra/online_stores/redis.py @@ -32,8 +32,15 @@ from pydantic import StrictStr from feast import Entity, FeatureView, RepoConfig, utils -from feast.infra.online_stores.helpers import _mmh3, _redis_key, _redis_key_prefix +from feast.infra.key_encoding_utils import serialize_entity_key +from feast.infra.online_stores.helpers import ( + _mmh3, + _redis_key, + _redis_key_prefix, + compute_versioned_name, +) from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.supported_async_methods import SupportedAsyncMethods from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel @@ -51,6 +58,13 @@ logger = logging.getLogger(__name__) +def _versioned_fv_name(table: FeatureView, config: RepoConfig) -> str: + """Return the feature view name with version suffix when versioning is enabled.""" + return compute_versioned_name( + table, config.registry.enable_online_feature_view_versioning + ) + + class RedisType(str, Enum): redis = "redis" redis_cluster = "redis_cluster" @@ -79,6 +93,11 @@ class RedisOnlineStoreConfig(FeastConfigBaseModel): full_scan_for_deletion: Optional[bool] = True """(Optional) whether to scan for deletion of features""" + skip_dedup: bool = False + """(Optional) Skip timestamp deduplication check on writes for higher throughput. + When True, writes proceed in a single pipeline without reading existing timestamps first. + This may cause older feature values to overwrite newer ones under concurrent writers.""" + class RedisOnlineStore(OnlineStore): """ @@ -96,6 +115,10 @@ class RedisOnlineStore(OnlineStore): None ) + @property + def async_supported(self) -> SupportedAsyncMethods: + return SupportedAsyncMethods(read=True, write=True) + def delete_entity_values(self, config: RepoConfig, join_keys: List[str]): client = self._get_client(config.online_store) deleted_count = 0 @@ -113,27 +136,43 @@ def delete_entity_values(self, config: RepoConfig, join_keys: List[str]): def delete_table(self, config: RepoConfig, table: FeatureView): """ - Delete all rows in Redis for a specific feature view + Delete all rows in Redis for a specific feature view. + + Uses a two-phase pipelined approach to avoid an O(K) synchronous hkeys + call inside the scan loop (N+1 pattern). Args: config: Feast config table: Feature view to delete """ client = self._get_client(config.online_store) - deleted_count = 0 prefix = _redis_key_prefix(table.join_keys) - redis_hash_keys = [_mmh3(f"{table.name}:{f.name}") for f in table.features] - redis_hash_keys.append(bytes(f"_ts:{table.name}", "utf8")) + fv_name = _versioned_fv_name(table, config) + fv_name_bytes = bytes(fv_name, "utf8") + redis_hash_keys = [_mmh3(f"{fv_name}:{f.name}") for f in table.features] + redis_hash_keys.append(bytes(f"_ts:{fv_name}", "utf8")) + # Phase 1: collect all matching entity keys from SCAN (no per-key round trips) + scan_pattern = b"".join([prefix, b"*", config.project.encode("utf8")]) + all_keys = list(client.scan_iter(scan_pattern)) + + if not all_keys: + logger.debug(f"Deleted 0 rows for feature view {fv_name}") + return + + # Phase 2: pipeline hkeys for all collected entity keys (1 round trip) with client.pipeline(transaction=False) as pipe: - for _k in client.scan_iter( - b"".join([prefix, b"*", config.project.encode("utf8")]) - ): - _tables = { - _hk[4:] for _hk in client.hgetall(_k) if _hk.startswith(b"_ts:") - } - if bytes(table.name, "utf8") not in _tables: + for _k in all_keys: + pipe.hkeys(_k) + all_hkeys = pipe.execute() + + # Phase 3: pipeline all deletions based on phase 2 results (1 round trip) + deleted_count = 0 + with client.pipeline(transaction=False) as pipe: + for _k, field_names in zip(all_keys, all_hkeys): + _tables = {_hk[4:] for _hk in field_names if _hk.startswith(b"_ts:")} + if fv_name_bytes not in _tables: continue if len(_tables) == 1: pipe.delete(_k) @@ -142,7 +181,7 @@ def delete_table(self, config: RepoConfig, table: FeatureView): deleted_count += 1 pipe.execute() - logger.debug(f"Deleted {deleted_count} rows for feature view {table.name}") + logger.debug(f"Deleted {deleted_count} rows for feature view {fv_name}") def update( self, @@ -281,8 +320,38 @@ def online_write_batch( client = self._get_client(online_store_config) project = config.project - feature_view = table.name + feature_view = _versioned_fv_name(table, config) ts_key = f"_ts:{feature_view}" + + if online_store_config.skip_dedup: + # Single-pipeline fast path: no timestamp read, directly write all rows. + # Reduces round trips from 2 to 1. Suitable for initial loads or + # append-only pipelines where out-of-order writes are not a concern. + with client.pipeline(transaction=False) as pipe: + for entity_key, values, timestamp, _ in data: + redis_key_bin = _redis_key( + project, + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + aware_ts = utils.make_tzaware(timestamp) + ts = Timestamp() + ts.FromDatetime(aware_ts) + entity_hset: Dict[Any, Any] = {ts_key: ts.SerializeToString()} + for feature_name, val in values.items(): + f_key = _mmh3(f"{feature_view}:{feature_name}") + entity_hset[f_key] = val.SerializeToString() + pipe.hset(redis_key_bin, mapping=entity_hset) + if online_store_config.key_ttl_seconds: + pipe.expire( + name=redis_key_bin, + time=online_store_config.key_ttl_seconds, + ) + results = pipe.execute() + if progress: + progress(len(results)) + return + keys = [] # redis pipelining optimization: send multiple commands to redis server without waiting for every reply with client.pipeline(transaction=False) as pipe: @@ -301,25 +370,41 @@ def online_write_batch( # flattening the list of lists. `hmget` does the lookup assuming a list of keys in the key bin prev_event_timestamps = [i[0] for i in prev_event_timestamps] + # Latest event timestamp queued so far in this batch, per entity key. The + # reads above all happened before any write, so without this rows sharing + # an entity key would each compare against the same pre-batch value, all + # pass the staleness check, and the last one queued would win regardless + # of its event time. + batch_latest_nanos: Dict[bytes, int] = {} + for redis_key_bin, prev_event_time, (_, values, timestamp, _) in zip( keys, prev_event_timestamps, data ): - event_time_seconds = int(utils.make_tzaware(timestamp).timestamp()) - - # ignore if event_timestamp is before the event features that are currently in the feature store + # Convert incoming timestamp to millisecond-aware datetime + aware_ts = utils.make_tzaware(timestamp) + # Build protobuf timestamp with nanos + ts = Timestamp() + ts.FromDatetime(aware_ts) + # New timestamp in nanoseconds + new_total_nanos = ts.seconds * 1_000_000_000 + ts.nanos + # Compare against existing timestamp (nanosecond precision) + prev_total_nanos = 0 if prev_event_time: prev_ts = Timestamp() prev_ts.ParseFromString(prev_event_time) - if prev_ts.seconds and event_time_seconds <= prev_ts.seconds: - # TODO: somehow signal that it's not overwriting the current record? - if progress: - progress(1) - continue - - ts = Timestamp() - ts.seconds = event_time_seconds - entity_hset = dict() - entity_hset[ts_key] = ts.SerializeToString() + prev_total_nanos = prev_ts.seconds * 1_000_000_000 + prev_ts.nanos + # Whichever is newer: the stored value or an earlier row of this batch + latest_seen_nanos = max( + prev_total_nanos, batch_latest_nanos.get(redis_key_bin, 0) + ) + # Skip only if older OR exact same instant + if latest_seen_nanos and new_total_nanos <= latest_seen_nanos: + if progress: + progress(1) + continue + batch_latest_nanos[redis_key_bin] = new_total_nanos + # Store full timestamp (seconds + nanos) + entity_hset = {ts_key: ts.SerializeToString()} for feature_name, val in values.items(): f_key = _mmh3(f"{feature_view}:{feature_name}") @@ -335,30 +420,132 @@ def online_write_batch( if progress: progress(len(results)) + async def online_write_batch_async( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + """Async version of online_write_batch using the async Redis client.""" + online_store_config = config.online_store + assert isinstance(online_store_config, RedisOnlineStoreConfig) + + client = await self._get_client_async(online_store_config) + project = config.project + + feature_view = _versioned_fv_name(table, config) + ts_key = f"_ts:{feature_view}" + + if online_store_config.skip_dedup: + async with client.pipeline(transaction=False) as pipe: + for entity_key, values, timestamp, _ in data: + redis_key_bin = _redis_key( + project, + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + aware_ts = utils.make_tzaware(timestamp) + ts = Timestamp() + ts.FromDatetime(aware_ts) + entity_hset: Dict[Any, Any] = {ts_key: ts.SerializeToString()} + for feature_name, val in values.items(): + f_key = _mmh3(f"{feature_view}:{feature_name}") + entity_hset[f_key] = val.SerializeToString() + pipe.hset(redis_key_bin, mapping=entity_hset) + if online_store_config.key_ttl_seconds: + pipe.expire( + name=redis_key_bin, + time=online_store_config.key_ttl_seconds, + ) + results = await pipe.execute() + if progress: + progress(len(results)) + return + + keys = [] + async with client.pipeline(transaction=False) as pipe: + for entity_key, _, _, _ in data: + redis_key_bin = _redis_key( + project, + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + keys.append(redis_key_bin) + pipe.hmget(redis_key_bin, ts_key) + prev_event_timestamps = await pipe.execute() + + prev_event_timestamps = [i[0] for i in prev_event_timestamps] + + # See online_write_batch: guards against rows in this batch that share an + # entity key overwriting each other out of event-time order. + batch_latest_nanos: Dict[bytes, int] = {} + + async with client.pipeline(transaction=False) as pipe: + for redis_key_bin, prev_event_time, (_, values, timestamp, _) in zip( + keys, prev_event_timestamps, data + ): + aware_ts = utils.make_tzaware(timestamp) + ts = Timestamp() + ts.FromDatetime(aware_ts) + new_total_nanos = ts.seconds * 1_000_000_000 + ts.nanos + + prev_total_nanos = 0 + if prev_event_time: + prev_ts = Timestamp() + prev_ts.ParseFromString(prev_event_time) + prev_total_nanos = prev_ts.seconds * 1_000_000_000 + prev_ts.nanos + latest_seen_nanos = max( + prev_total_nanos, batch_latest_nanos.get(redis_key_bin, 0) + ) + if latest_seen_nanos and new_total_nanos <= latest_seen_nanos: + if progress: + progress(1) + continue + batch_latest_nanos[redis_key_bin] = new_total_nanos + + entity_hset = {ts_key: ts.SerializeToString()} + for feature_name, val in values.items(): + f_key = _mmh3(f"{feature_view}:{feature_name}") + entity_hset[f_key] = val.SerializeToString() + + pipe.hset(redis_key_bin, mapping=entity_hset) + if online_store_config.key_ttl_seconds: + pipe.expire( + name=redis_key_bin, time=online_store_config.key_ttl_seconds + ) + + results = await pipe.execute() + if progress: + progress(len(results)) + def _generate_redis_keys_for_entities( self, config: RepoConfig, entity_keys: List[EntityKeyProto] ) -> List[bytes]: - keys = [] - for entity_key in entity_keys: - redis_key_bin = _redis_key( - config.project, - entity_key, - entity_key_serialization_version=config.entity_key_serialization_version, - ) - keys.append(redis_key_bin) - return keys + project = config.project + version = config.entity_key_serialization_version + project_bytes = project.encode("utf-8") + return [ + serialize_entity_key(ek, entity_key_serialization_version=version) + + project_bytes + for ek in entity_keys + ] def _generate_hset_keys_for_features( self, feature_view: FeatureView, requested_features: Optional[List[str]] = None, + fv_name_override: Optional[str] = None, ) -> Tuple[List[str], List[str]]: if not requested_features: requested_features = [f.name for f in feature_view.features] - hset_keys = [_mmh3(f"{feature_view.name}:{k}") for k in requested_features] + fv_name = fv_name_override or feature_view.name + hset_keys = [_mmh3(f"{fv_name}:{k}") for k in requested_features] - ts_key = f"_ts:{feature_view.name}" + ts_key = f"_ts:{fv_name}" hset_keys.append(ts_key) requested_features.append(ts_key) @@ -369,14 +556,11 @@ def _convert_redis_values_to_protobuf( redis_values: List[List[ByteString]], feature_view: str, requested_features: List[str], - ): - result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] - for values in redis_values: - features = self._get_features_for_entity( - values, feature_view, requested_features - ) - result.append(features) - return result + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + return [ + self._get_features_for_entity(values, feature_view, requested_features) + for values in redis_values + ] def online_read( self, @@ -390,9 +574,10 @@ def online_read( client = self._get_client(online_store_config) feature_view = table + fv_name = _versioned_fv_name(table, config) requested_features, hset_keys = self._generate_hset_keys_for_features( - feature_view, requested_features + feature_view, requested_features, fv_name_override=fv_name ) keys = self._generate_redis_keys_for_entities(config, entity_keys) @@ -403,7 +588,7 @@ def online_read( redis_values = pipe.execute() return self._convert_redis_values_to_protobuf( - redis_values, feature_view.name, requested_features + redis_values, fv_name, requested_features ) async def online_read_async( @@ -418,9 +603,10 @@ async def online_read_async( client = await self._get_client_async(online_store_config) feature_view = table + fv_name = _versioned_fv_name(table, config) requested_features, hset_keys = self._generate_hset_keys_for_features( - feature_view, requested_features + feature_view, requested_features, fv_name_override=fv_name ) keys = self._generate_redis_keys_for_entities(config, entity_keys) @@ -430,9 +616,139 @@ async def online_read_async( redis_values = await pipe.execute() return self._convert_redis_values_to_protobuf( - redis_values, feature_view.name, requested_features + redis_values, fv_name, requested_features ) + def _read_features_per_fv( + self, + config: RepoConfig, + grouped_refs, + join_key_values, + entity_name_to_join_key_map, + online_features_response, + full_feature_names: bool, + include_feature_view_version_metadata: bool, + ) -> None: + """Batch all per-FV HMGET commands into a single Redis pipeline.""" + work_items = [] + for table, requested_features in grouped_refs: + table_entity_values, idxs, output_len = utils._get_unique_entities( + table, join_key_values, entity_name_to_join_key_map + ) + entity_key_protos = utils._get_entity_key_protos(table_entity_values) + fv_name = _versioned_fv_name(table, config) + redis_keys = self._generate_redis_keys_for_entities( + config, entity_key_protos + ) + req_features, hset_keys = self._generate_hset_keys_for_features( + table, requested_features, fv_name_override=fv_name + ) + work_items.append( + (table, req_features, fv_name, hset_keys, redis_keys, idxs, output_len) + ) + + if work_items: + client = self._get_client(config.online_store) + with client.pipeline(transaction=False) as pipe: + for _, _, _, hset_keys, redis_keys, _, _ in work_items: + for redis_key in redis_keys: + pipe.hmget(redis_key, hset_keys) + all_results = pipe.execute() + + offset = 0 + for ( + table, + req_features, + fv_name, + _, + redis_keys, + idxs, + output_len, + ) in work_items: + n = len(redis_keys) + redis_values = all_results[offset : offset + n] + offset += n + + read_rows = self._convert_redis_values_to_protobuf( + redis_values, fv_name, req_features + ) + + utils._populate_response_from_feature_data( + req_features, + read_rows, + idxs, + online_features_response, + full_feature_names, + table, + output_len, + include_feature_view_version_metadata, + ) + + async def _read_features_per_fv_async( + self, + config: RepoConfig, + grouped_refs, + join_key_values, + entity_name_to_join_key_map, + online_features_response, + full_feature_names: bool, + include_feature_view_version_metadata: bool, + ) -> None: + """Async version: batch all per-FV HMGET into a single async pipeline.""" + work_items = [] + for table, requested_features in grouped_refs: + table_entity_values, idxs, output_len = utils._get_unique_entities( + table, join_key_values, entity_name_to_join_key_map + ) + entity_key_protos = utils._get_entity_key_protos(table_entity_values) + fv_name = _versioned_fv_name(table, config) + redis_keys = self._generate_redis_keys_for_entities( + config, entity_key_protos + ) + req_features, hset_keys = self._generate_hset_keys_for_features( + table, requested_features, fv_name_override=fv_name + ) + work_items.append( + (table, req_features, fv_name, hset_keys, redis_keys, idxs, output_len) + ) + + if work_items: + client = await self._get_client_async(config.online_store) + async with client.pipeline(transaction=False) as pipe: + for _, _, _, hset_keys, redis_keys, _, _ in work_items: + for redis_key in redis_keys: + pipe.hmget(redis_key, hset_keys) + all_results = await pipe.execute() + + offset = 0 + for ( + table, + req_features, + fv_name, + _, + redis_keys, + idxs, + output_len, + ) in work_items: + n = len(redis_keys) + redis_values = all_results[offset : offset + n] + offset += n + + read_rows = self._convert_redis_values_to_protobuf( + redis_values, fv_name, req_features + ) + + utils._populate_response_from_feature_data( + req_features, + read_rows, + idxs, + online_features_response, + full_feature_names, + table, + output_len, + include_feature_view_version_metadata, + ) + def _get_features_for_entity( self, values: List[ByteString], @@ -442,19 +758,22 @@ def _get_features_for_entity( res_val = dict(zip(requested_features, values)) res_ts = Timestamp() - ts_val = res_val.pop(f"_ts:{feature_view}") + ts_key = f"_ts:{feature_view}" + ts_val = res_val.pop(ts_key) if ts_val: - res_ts.ParseFromString(bytes(ts_val)) + res_ts.ParseFromString(ts_val) - res = {} + res: Dict[str, ValueProto] = {} for feature_name, val_bin in res_val.items(): val = ValueProto() if val_bin: - val.ParseFromString(bytes(val_bin)) + val.ParseFromString(val_bin) res[feature_name] = val if not res: return None, None - else: - timestamp = datetime.fromtimestamp(res_ts.seconds, tz=timezone.utc) - return timestamp, res + + total_seconds = res_ts.seconds + res_ts.nanos / 1_000_000_000.0 + timestamp = datetime.fromtimestamp(total_seconds, tz=timezone.utc) + + return timestamp, res diff --git a/sdk/python/feast/infra/online_stores/remote.py b/sdk/python/feast/infra/online_stores/remote.py index ec2b05759ba..8aa5df1563e 100644 --- a/sdk/python/feast/infra/online_stores/remote.py +++ b/sdk/python/feast/infra/online_stores/remote.py @@ -13,17 +13,40 @@ # limitations under the License. import json import logging +import uuid as uuid_module from collections import defaultdict from datetime import datetime -from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple +from typing import ( + Any, + Callable, + Dict, + List, + Literal, + Mapping, + Optional, + Sequence, + Tuple, + Union, +) import requests from pydantic import StrictStr from feast import Entity, FeatureView, RepoConfig +from feast.feature_service import FeatureService +from feast.filter_models import ComparisonFilter, CompoundFilter from feast.infra.online_stores.helpers import _to_naive_utc from feast.infra.online_stores.online_store import OnlineStore +from feast.infra.registry.base_registry import BaseRegistry +from feast.online_response import OnlineResponse +from feast.permissions.client.http_auth_requests_wrapper import HttpSessionManager +from feast.protos.feast.serving.ServingService_pb2 import ( + FieldStatus, + GetOnlineFeaturesResponse, + GetOnlineFeaturesResponseMetadata, +) from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import RepeatedValue from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel from feast.rest_error_handler import rest_error_handling_decorator @@ -37,6 +60,17 @@ logger = logging.getLogger(__name__) +def _json_safe(val: Any) -> Any: + """Convert uuid.UUID objects and sets to JSON-serializable form.""" + if isinstance(val, uuid_module.UUID): + return str(val) + if isinstance(val, set): + return [str(v) if isinstance(v, uuid_module.UUID) else v for v in val] + if isinstance(val, list): + return [str(v) if isinstance(v, uuid_module.UUID) else v for v in val] + return val + + class RemoteOnlineStoreConfig(FeastConfigBaseModel): """Remote Online store config for remote online store""" @@ -51,12 +85,164 @@ class RemoteOnlineStoreConfig(FeastConfigBaseModel): """ str: Path to the public certificate when the online server starts in TLS(SSL) mode. This may be needed if the online server started with a self-signed certificate, typically this file ends with `*.crt`, `*.cer`, or `*.pem`. If type is 'remote', then this configuration is needed to connect to remote online server in TLS mode. """ + # Connection pooling configuration + connection_pool_size: int = 50 + """ int: Maximum number of connections to keep in the pool (default 50). + Increase for high-concurrency workloads. """ + + connection_idle_timeout: int = 300 + """ int: Maximum time in seconds a session can be idle before being closed (default 300 = 5 minutes). + Set to 0 to disable idle timeout. """ + + connection_retries: int = 3 + """ int: Number of retries for failed requests with exponential backoff (default 3). """ + class RemoteOnlineStore(OnlineStore): """ remote online store implementation wrapper to communicate with feast online server. """ + @staticmethod + def _proto_value_to_transport_value(proto_value: ValueProto) -> Any: + """ + Convert a proto Value to a JSON-serializable Python value suitable for + HTTP transport. Unlike ``feast_value_type_to_python_type``, this keeps + ``json_val`` as a raw string so the receiving server can reconstruct a + DataFrame whose column types match the original (string for JSON, dict + for Map/Struct). Parsing JSON strings into dicts would cause PyArrow to + infer a struct column on the server, which can crash with complex nested + types (lists inside dicts). + """ + val_attr = proto_value.WhichOneof("val") + if val_attr is None: + return None + + # Keep JSON values as raw strings for correct DataFrame reconstruction. + # Parsing them into dicts causes PyArrow to infer struct columns on the + # server whose nested lists round-trip as numpy arrays, breaking + # json.dumps during proto conversion. + if val_attr == "json_val": + return getattr(proto_value, val_attr) + if val_attr == "json_list_val": + return list(getattr(proto_value, val_attr).val) + + # Nested collection types use feast_value_type_to_python_type + # which handles recursive conversion of RepeatedValue protos. + if val_attr in ("list_val", "set_val"): + return feast_value_type_to_python_type(proto_value) + + # Map/Struct types are converted to Python dicts by + # feast_value_type_to_python_type. Serialise them to JSON strings + # so the server-side DataFrame gets VARCHAR columns instead of + # PyArrow struct columns that can crash with complex nested types. + if val_attr in ("map_val", "struct_val"): + return json.dumps(feast_value_type_to_python_type(proto_value)) + if val_attr in ("map_list_val", "struct_list_val"): + return [json.dumps(v) for v in feast_value_type_to_python_type(proto_value)] + + # UUID types are stored as strings in proto — return them directly + # to avoid feast_value_type_to_python_type converting to uuid.UUID + # objects which are not JSON-serializable. + if val_attr in ("uuid_val", "time_uuid_val"): + return getattr(proto_value, val_attr) + if val_attr in ("uuid_list_val", "time_uuid_list_val"): + return list(getattr(proto_value, val_attr).val) + if val_attr in ("uuid_set_val", "time_uuid_set_val"): + return list(getattr(proto_value, val_attr).val) + + # UnixTimestamp values are stored as int64 (epoch seconds) in proto. + # Return them directly to avoid feast_value_type_to_python_type + # converting to datetime objects which are not JSON-serializable. + if val_attr == "unix_timestamp_val": + return getattr(proto_value, val_attr) + if val_attr in ("unix_timestamp_list_val", "unix_timestamp_set_val"): + return list(getattr(proto_value, val_attr).val) + + return feast_value_type_to_python_type(proto_value) + + _STATUS_MAP = { + "PRESENT": FieldStatus.PRESENT, + "NOT_FOUND": FieldStatus.NOT_FOUND, + "NULL_VALUE": FieldStatus.NULL_VALUE, + "OUTSIDE_MAX_AGE": FieldStatus.OUTSIDE_MAX_AGE, + } + + def get_online_features( + self, + config: RepoConfig, + features: Union[List[str], FeatureService], + entity_rows: Union[ + List[Dict[str, Any]], + Mapping[str, Union[Sequence[Any], Sequence[ValueProto], RepeatedValue]], + ], + registry: BaseRegistry, + project: str, + full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, + ) -> OnlineResponse: + assert isinstance(config.online_store, RemoteOnlineStoreConfig) + + if isinstance(entity_rows, list): + columnar: Dict[str, List[Any]] = {k: [] for k in entity_rows[0].keys()} + for entity_row in entity_rows: + for key, value in entity_row.items(): + columnar[key].append(value) + entity_rows = columnar + + entities: Dict[str, List[Any]] = {} + for k, vals in entity_rows.items(): + iterable = vals.val if isinstance(vals, RepeatedValue) else vals + entities[k] = [_json_safe(v) for v in iterable] + + req_body: Dict[str, Any] = { + "entities": entities, + "full_feature_names": full_feature_names, + "include_feature_view_version_metadata": include_feature_view_version_metadata, + } + + if isinstance(features, FeatureService): + req_body["feature_service"] = features.name + else: + req_body["features"] = features + + response = get_remote_online_features(config=config, req_body=req_body) + + if response.status_code != 200: + raise RuntimeError( + f"Failed to get online features: {response.status_code} {response.text}" + ) + + resp_json = response.json() + return self._build_online_response_from_json(resp_json) + + def _build_online_response_from_json( + self, resp_json: Dict[str, Any] + ) -> OnlineResponse: + proto = GetOnlineFeaturesResponse() + + metadata = GetOnlineFeaturesResponseMetadata() + feature_names = resp_json.get("metadata", {}).get("feature_names", []) + metadata.feature_names.val.extend(feature_names) + proto.metadata.CopyFrom(metadata) + + for result in resp_json.get("results", []): + fv = GetOnlineFeaturesResponse.FeatureVector() + for val in result.get("values", []): + if val is None: + fv.values.append(ValueProto()) + else: + protos = python_values_to_proto_values([val]) + fv.values.append(protos[0]) + for status_str in result.get("statuses", []): + fv.statuses.append( + self._STATUS_MAP.get(status_str, FieldStatus.INVALID) + ) + proto.results.append(fv) + + proto.status = True + return OnlineResponse(proto) + def online_write_batch( self, config: RepoConfig, @@ -80,14 +266,14 @@ def online_write_batch( for join_key, entity_value_proto in zip( entity_key_proto.join_keys, entity_key_proto.entity_values ): - columnar_data[join_key].append( - feast_value_type_to_python_type(entity_value_proto) - ) + val = feast_value_type_to_python_type(entity_value_proto) + columnar_data[join_key].append(_json_safe(val)) - # Populate feature values + # Populate feature values – use transport-safe conversion that + # preserves JSON strings instead of parsing them into dicts. for feature_name, feature_value_proto in feature_values_proto.items(): columnar_data[feature_name].append( - feast_value_type_to_python_type(feature_value_proto) + self._proto_value_to_transport_value(feature_value_proto) ) # Populate timestamps @@ -134,6 +320,12 @@ def online_read( logger.debug("Able to retrieve the online features from feature server.") response_json = json.loads(response.text) event_ts = self._get_event_ts(response_json) + # Build feature name -> ValueType mapping so we can reconstruct + # complex types (nested collections, sets, etc.) that cannot be + # inferred from raw JSON values alone. + feature_type_map: Dict[str, ValueType] = { + f.name: f.dtype.to_value_type() for f in table.features + } # Iterating over results and converting the API results in column format to row format. result_tuples: List[ Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]] @@ -153,13 +345,16 @@ def online_read( ] == "PRESENT" ): + feature_value_type = feature_type_map.get( + feature_name, ValueType.UNKNOWN + ) message = python_values_to_proto_values( [ response_json["results"][index]["values"][ feature_value_index ] ], - ValueType.UNKNOWN, + feature_value_type, ) feature_values_dict[feature_name] = message[0] else: @@ -252,6 +447,8 @@ def retrieve_online_documents_v2( top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> List[ Tuple[ Optional[datetime], @@ -269,6 +466,7 @@ def retrieve_online_documents_v2( top_k, distance_metric, query_string, + filters=filters, api_version=2, ) response = get_remote_online_documents(config=config, req_body=req_body) @@ -277,20 +475,31 @@ def retrieve_online_documents_v2( response_json = json.loads(response.text) event_ts: Optional[datetime] = self._get_event_ts(response_json) + metadata = response_json.get("metadata") or {} + feature_names = metadata.get("feature_names") or [] + results = response_json.get("results") or [] + + if not feature_names or not results: + logger.debug( + "Empty metadata or results in retrieve_online_documents_v2 response." + ) + return [] + # Create feature name to index mapping for efficient lookup feature_name_to_index = { - name: idx - for idx, name in enumerate(response_json["metadata"]["feature_names"]) + name: idx for idx, name in enumerate(feature_names) } # Process each result row - num_results = ( - len(response_json["results"][0]["values"]) - if response_json["results"] - else 0 - ) + first_result_values = results[0].get("values") or [] + num_results = len(first_result_values) result_tuples = [] - + if requested_features is None: + requested_features = [] + else: + requested_features = list(requested_features) + if "distance" not in requested_features: + requested_features.append("distance") for row_idx in range(num_results): # Build feature values dictionary for requested features feature_values_dict = {} @@ -405,7 +614,7 @@ def _construct_online_read_api_json_request( entity_keys: List[EntityKeyProto], table: FeatureView, requested_features: Optional[List[str]] = None, - ) -> str: + ) -> dict: api_requested_features = [] if requested_features is not None: for requested_feature in requested_features: @@ -416,16 +625,13 @@ def _construct_online_read_api_json_request( for row in entity_keys: entity_key = row.join_keys[0] entity_values.append( - getattr(row.entity_values[0], row.entity_values[0].WhichOneof("val")) + getattr(row.entity_values[0], row.entity_values[0].WhichOneof("val")) # type: ignore[arg-type] ) - req_body = json.dumps( - { - "features": api_requested_features, - "entities": {entity_key: entity_values}, - } - ) - return req_body + return { + "features": api_requested_features, + "entities": {entity_key: entity_values}, + } def _construct_online_documents_api_json_request( self, @@ -434,21 +640,18 @@ def _construct_online_documents_api_json_request( embedding: Optional[List[float]] = None, top_k: Optional[int] = None, distance_metric: Optional[str] = "L2", - ) -> str: + ) -> dict: api_requested_features = [] if requested_features is not None: for requested_feature in requested_features: api_requested_features.append(f"{table.name}:{requested_feature}") - req_body = json.dumps( - { - "features": api_requested_features, - "query": embedding, - "top_k": top_k, - "distance_metric": distance_metric, - } - ) - return req_body + return { + "features": api_requested_features, + "query": embedding, + "top_k": top_k, + "distance_metric": distance_metric, + } def _construct_online_documents_v2_api_json_request( self, @@ -458,30 +661,35 @@ def _construct_online_documents_v2_api_json_request( top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, api_version: Optional[int] = 2, - ) -> str: + ) -> dict: api_requested_features = [] if requested_features is not None: for requested_feature in requested_features: api_requested_features.append(f"{table.name}:{requested_feature}") - req_body = json.dumps( - { - "features": api_requested_features, - "query": embedding, - "top_k": top_k, - "distance_metric": distance_metric, - "query_string": query_string, - "api_version": api_version, - } - ) - return req_body - - def _get_event_ts(self, response_json) -> datetime: - event_ts = "" - if len(response_json["results"]) > 1: - event_ts = response_json["results"][1]["event_timestamps"][0] - return datetime.fromisoformat(event_ts.replace("Z", "+00:00")) + body: Dict[str, Any] = { + "features": api_requested_features, + "query": embedding, + "top_k": top_k, + "distance_metric": distance_metric, + "query_string": query_string, + "api_version": api_version, + } + if filters is not None: + body["filters"] = filters.model_dump() + return body + + def _get_event_ts(self, response_json) -> Optional[datetime]: + results = response_json.get("results") or [] + if len(results) > 1: + event_timestamps = results[1].get("event_timestamps") or [] + if event_timestamps: + return datetime.fromisoformat( + event_timestamps[0].replace("Z", "+00:00") + ) + return None def _construct_entity_key_from_response( self, @@ -544,37 +752,55 @@ def teardown( ): pass + async def close(self) -> None: + """ + Close the HTTP session and release connection pool resources. + + This method is called automatically when FeatureStore.close() is invoked. + It cleans up the cached HTTP session used for connection pooling. + + Note: Since the session is shared globally, calling close() will affect + all RemoteOnlineStore instances in the same process. This is typically + fine for SDK usage where there's usually one FeatureStore per process. + """ + HttpSessionManager.close_session() + logger.debug("RemoteOnlineStore HTTP session closed") + @rest_error_handling_decorator def get_remote_online_features( - session: requests.Session, config: RepoConfig, req_body: str + session: requests.Session, config: RepoConfig, req_body: dict ) -> requests.Response: if config.online_store.cert: return session.post( f"{config.online_store.path}/get-online-features", - data=req_body, + json=req_body, verify=config.online_store.cert, ) else: return session.post( - f"{config.online_store.path}/get-online-features", data=req_body + f"{config.online_store.path}/get-online-features", json=req_body ) @rest_error_handling_decorator def get_remote_online_documents( - session: requests.Session, config: RepoConfig, req_body: str + session: requests.Session, config: RepoConfig, req_body: dict ) -> requests.Response: - if config.online_store.cert: - return session.post( - f"{config.online_store.path}/retrieve-online-documents", - data=req_body, - verify=config.online_store.cert, - ) - else: - return session.post( - f"{config.online_store.path}/retrieve-online-documents", data=req_body - ) + search_paths = ("/search", "/retrieve-online-documents") + last_response: Optional[requests.Response] = None + for path in search_paths: + url = f"{config.online_store.path}{path}" + if config.online_store.cert: + last_response = session.post( + url, json=req_body, verify=config.online_store.cert + ) + else: + last_response = session.post(url, json=req_body) + if last_response.status_code != 404: + return last_response + assert last_response is not None + return last_response @rest_error_handling_decorator diff --git a/sdk/python/feast/infra/online_stores/scylladb_online_store/__init__.py b/sdk/python/feast/infra/online_stores/scylladb_online_store/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/feast/infra/online_stores/scylladb_online_store/scylladb.py b/sdk/python/feast/infra/online_stores/scylladb_online_store/scylladb.py new file mode 100644 index 00000000000..f68d8e5aacf --- /dev/null +++ b/sdk/python/feast/infra/online_stores/scylladb_online_store/scylladb.py @@ -0,0 +1,828 @@ +import logging +import warnings +from datetime import datetime +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Literal, + Optional, + Sequence, + Tuple, + Union, +) + +from cassandra.auth import PlainTextAuthProvider +from cassandra.cluster import ( + EXEC_PROFILE_DEFAULT, + Cluster, + ExecutionProfile, + Session, +) +from cassandra.concurrent import execute_concurrent_with_args +from cassandra.policies import DCAwareRoundRobinPolicy, TokenAwarePolicy +from cassandra.query import ConsistencyLevel, PreparedStatement +from pydantic import StrictFloat, StrictInt, StrictStr + +from feast import Entity, FeatureView, RepoConfig +from feast.filter_models import ComparisonFilter, CompoundFilter +from feast.infra.key_encoding_utils import deserialize_entity_key, serialize_entity_key +from feast.infra.online_stores.online_store import OnlineStore +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import FeastConfigBaseModel + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Error messages +# --------------------------------------------------------------------------- + +E_SCYLLA_UNEXPECTED_CONFIG = ( + "Unexpected configuration object (not a ScyllaDBOnlineStoreConfig instance)" +) +E_SCYLLA_NOT_CONFIGURED = ( + "Inconsistent ScyllaDB configuration: 'hosts' and 'keyspace' are required" +) +E_SCYLLA_INCONSISTENT_AUTH = ( + "ScyllaDB username and password must be provided together or not at all" +) + +# --------------------------------------------------------------------------- +# CQL templates +# --------------------------------------------------------------------------- + + +def _build_create_table_cql(fqtable: str, dim: Optional[int]) -> str: + """Return a CREATE TABLE CQL string, optionally including the vector column. + + When *dim* is given a ``vector_value vector`` column is inserted + between the BLOB ``value`` column and ``event_ts``. When *dim* is ``None`` + the column is omitted entirely and the table schema is fully static. + """ + vec_col = f"vector_value vector,\n" if dim else "" + return ( + f"CREATE TABLE IF NOT EXISTS {fqtable} (\n" + f" entity_key TEXT,\n" + f" feature_name TEXT,\n" + f" value BLOB,\n" + f" {vec_col}" + f" event_ts TIMESTAMP,\n" + f" created_ts TIMESTAMP,\n" + f" PRIMARY KEY ((entity_key), feature_name)\n" + f");" + ) + + +DROP_TABLE_CQL = "DROP TABLE IF EXISTS {fqtable};" + +INSERT_CQL = ( + "INSERT INTO {fqtable} (feature_name, value, entity_key, event_ts, created_ts)" + " VALUES (?, ?, ?, ?, ?);" +) + +INSERT_CQL_TTL = ( + "INSERT INTO {fqtable} (feature_name, value, entity_key, event_ts, created_ts)" + " VALUES (?, ?, ?, ?, ?) USING TTL ?;" +) + +# INSERT for vector feature rows: populates both the BLOB value (for online_read +# compatibility) and the native vector_value column (for ANN search). +# No TTL variant, ScyllaDB ignores TTL on vector-indexed columns, +# a future release will fix this, +# combining TTL with vector features is rejected at feast apply time. +INSERT_VEC_CQL = ( + "INSERT INTO {fqtable}" + " (feature_name, value, entity_key, event_ts, created_ts, vector_value)" + " VALUES (?, ?, ?, ?, ?, ?);" +) + +SELECT_CQL = "SELECT {columns} FROM {fqtable} WHERE entity_key = ?;" + +# Global vector index on the main feature table. +CREATE_VECTOR_INDEX_CQL = ( + "CREATE CUSTOM INDEX IF NOT EXISTS {index_name}" + " ON {fqtable} (vector_value)" + " USING 'vector_index'" + " WITH OPTIONS = {{'similarity_function': '{sim_func}'}};" +) + +# ANN query on the main feature table. +# ALLOW FILTERING is required because feature_name is a clustering key. +ANN_SELECT_CQL = ( + "SELECT entity_key, {sim_func_call} AS score, event_ts" + " FROM {fqtable}" + " WHERE feature_name = ?" + " ORDER BY vector_value ANN OF ?" + " LIMIT ?" + " ALLOW FILTERING;" +) + +# op_name -> (template, prepare?) +_CQL_TEMPLATES: Dict[str, Tuple[str, bool]] = { + "drop": (DROP_TABLE_CQL, False), + "insert": (INSERT_CQL, True), + "insert_ttl": (INSERT_CQL_TTL, True), + "select": (SELECT_CQL, True), +} + +# Similarity function CQL expression helpers (vector_value is the fixed column name) +_SIM_FUNC_EXPR = { + "COSINE": "similarity_cosine(vector_value, ?)", + "DOT_PRODUCT": "similarity_dot_product(vector_value, ?)", + "EUCLIDEAN": "similarity_euclidean(vector_value, ?)", +} + + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- + + +class ScyllaDBOnlineStoreConfig(FeastConfigBaseModel): + """ + Configuration for the native ScyllaDB online store. + + Requires ``scylla-driver`` (``pip install scylla-driver``). + + Example ``feature_store.yaml``:: + + online_store: + type: feast_scylladb.ScyllaDBOnlineStore + hosts: + - node-0.aws_us_east_1.xxxxxxxx.clusters.scylla.cloud + keyspace: feast + username: scylla + password: pass + local_dc: AWS_US_EAST_1 + """ + + type: Literal[ + "scylladb", + "feast.infra.online_stores.scylladb_online_store.scylladb.ScyllaDBOnlineStore", + ] = "scylladb" + + # Connection + hosts: List[StrictStr] + """Contact-point host addresses.""" + + port: Optional[StrictInt] = 9042 + """CQL port (default 9042).""" + + keyspace: StrictStr = "feast_keyspace" + """Target ScyllaDB keyspace.""" + + username: Optional[StrictStr] = None + """Auth username.""" + + password: Optional[StrictStr] = None + """Auth password.""" + + local_dc: Optional[StrictStr] = None + """ + Local datacenter name. + For ScyllaDB Cloud this is the region string, e.g. ``AWS_US_EAST_1``. + """ + + request_timeout: Optional[StrictFloat] = None + """Driver request timeout in seconds.""" + + read_concurrency: Optional[StrictInt] = 100 + """Concurrency level passed to ``execute_concurrent_with_args`` for reads.""" + + write_concurrency: Optional[StrictInt] = 100 + """Concurrency level passed to ``execute_concurrent_with_args`` for writes.""" + + vector_similarity_function: StrictStr = "COSINE" + """ + Default similarity function used when creating vector indexes. + Can be overridden per-feature via the ``similarity_function`` Field tag. + Supported values: ``COSINE``, ``DOT_PRODUCT``, ``EUCLIDEAN``. + """ + + +# --------------------------------------------------------------------------- +# Vector feature helpers +# --------------------------------------------------------------------------- + + +def _get_vector_features( + table: FeatureView, default_sim_func: str +) -> List[Tuple[str, int, str]]: + """ + Return ``(feature_name, dimension, similarity_function)`` for every feature + in *table* tagged as a vector feature. + + A feature is treated as a vector feature when its tags include:: + + "vector_index": "true" + "dimensions": "" + + The similarity function defaults to *default_sim_func* but can be + overridden per-feature with a ``"similarity_function"`` tag. + """ + result = [] + for field in table.schema: + tags = field.tags or {} + if tags.get("vector_index", "").lower() != "true": + continue + dim_str = tags.get("dimensions", "") + if not dim_str: + warnings.warn( + f"Feature '{field.name}' in FeatureView '{table.name}' is tagged " + "vector_index=true but is missing a 'dimensions' tag. " + "Skipping vector table creation for this feature.", + UserWarning, + stacklevel=2, + ) + continue + sim_func = tags.get("similarity_function", default_sim_func).upper() + result.append((field.name, int(dim_str), sim_func)) + return result + + +# --------------------------------------------------------------------------- +# Store implementation +# --------------------------------------------------------------------------- + + +class ScyllaDBInvalidConfig(Exception): + pass + + +class ScyllaDBOnlineStore(OnlineStore): + """ + Native ScyllaDB online store for Feast. + + Supports both regular feature materialisation and vector similarity search + via ScyllaDB's ANN / ``vector_index`` functionality. + + **Vector features** — tag your ``Field`` definitions like this:: + + from feast import Field + from feast.types import Array, Float32 + + Field( + name="embedding", + dtype=Array(Float32), + tags={ + "vector_index": "true", + "dimensions": "768", + "similarity_function": "COSINE", # optional, default COSINE + }, + ) + + Then call ``FeatureStore.retrieve_online_documents_v2(...)`` to perform an + approximate nearest-neighbour search. + """ + + _cluster: Optional[Cluster] = None + _session: Optional[Session] = None + _keyspace: str = "feast_keyspace" + _prepared_statements: Dict[str, PreparedStatement] = {} + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _get_session(self, config: RepoConfig) -> Session: + """Return the active session, creating the cluster connection if needed.""" + online_store_config = config.online_store + if not isinstance(online_store_config, ScyllaDBOnlineStoreConfig): + raise ScyllaDBInvalidConfig(E_SCYLLA_UNEXPECTED_CONFIG) + + if self._session: + return self._session + + hosts = online_store_config.hosts + port = online_store_config.port or 9042 + keyspace = online_store_config.keyspace + username = online_store_config.username + password = online_store_config.password + local_dc = online_store_config.local_dc + + if not hosts or not keyspace: + raise ScyllaDBInvalidConfig(E_SCYLLA_NOT_CONFIGURED) + if (username is None) ^ (password is None): + raise ScyllaDBInvalidConfig(E_SCYLLA_INCONSISTENT_AUTH) + + auth_provider = ( + PlainTextAuthProvider(username=username, password=password) + if username is not None + else None + ) + + # Build execution profile + if local_dc: + lb_policy: Any = TokenAwarePolicy( + DCAwareRoundRobinPolicy(local_dc=local_dc) + ) + exe_profile = ExecutionProfile( + request_timeout=online_store_config.request_timeout, + load_balancing_policy=lb_policy, + consistency_level=ConsistencyLevel.LOCAL_QUORUM, + ) + execution_profiles: Optional[Dict] = {EXEC_PROFILE_DEFAULT: exe_profile} + elif online_store_config.request_timeout is not None: + exe_profile = ExecutionProfile( + request_timeout=online_store_config.request_timeout, + consistency_level=ConsistencyLevel.LOCAL_QUORUM, + ) + execution_profiles = {EXEC_PROFILE_DEFAULT: exe_profile} + else: + exe_profile = ExecutionProfile( + consistency_level=ConsistencyLevel.LOCAL_QUORUM, + ) + execution_profiles = {EXEC_PROFILE_DEFAULT: exe_profile} + + cluster_kwargs: Dict[str, Any] = { + k: v + for k, v in { + "execution_profiles": execution_profiles, + }.items() + if v is not None + } + + self._cluster = Cluster( + hosts, + port=port, + auth_provider=auth_provider, + **cluster_kwargs, + ) + self._keyspace = keyspace + # Connect without a keyspace first so we can create it if needed. + session = self._cluster.connect() + session.execute( + f'CREATE KEYSPACE IF NOT EXISTS "{keyspace}"' + " WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '3'};" + ) + session.set_keyspace(keyspace) + self._session = session + return self._session + + @staticmethod + def _fq_table_name(keyspace: str, project: str, table: FeatureView) -> str: + return f'"{keyspace}"."{project}_{table.name}"' + + def _get_statement( + self, config: RepoConfig, op_name: str, fqtable: str, **kwargs: Any + ) -> Any: + """ + Resolve *op_name* to a CQL statement, preparing and caching it when the + template is marked as prepareable. + """ + session = self._get_session(config) + template, do_prepare = _CQL_TEMPLATES[op_name] + cql = template.format(fqtable=fqtable, **kwargs) + if do_prepare: + if cql not in self._prepared_statements: + logger.info("Preparing %s statement on %s.", op_name, fqtable) + self._prepared_statements[cql] = session.prepare(cql) + return self._prepared_statements[cql] + return cql + + # ------------------------------------------------------------------ + # Table lifecycle helpers + # ------------------------------------------------------------------ + + def _create_table( + self, + config: RepoConfig, + project: str, + table: FeatureView, + vec_features: Optional[List[Tuple[str, int, str]]] = None, + ) -> None: + session = self._get_session(config) + fqtable = self._fq_table_name(self._keyspace, project, table) + dim = vec_features[0][1] if vec_features else None + logger.info("Creating table %s.", fqtable) + session.execute(_build_create_table_cql(fqtable, dim)) + + for _feat_name, _dim, sim_func in vec_features or []: + index_name = f"{project}_{table.name}_vec_idx" + logger.info("Creating vector index %s on %s.", index_name, fqtable) + session.execute( + CREATE_VECTOR_INDEX_CQL.format( + index_name=index_name, + fqtable=fqtable, + sim_func=sim_func, + ) + ) + + def _drop_table( + self, + config: RepoConfig, + project: str, + table: FeatureView, + vec_features: Optional[List[Tuple[str, int, str]]] = None, + ) -> None: + session = self._get_session(config) + fqtable = self._fq_table_name(self._keyspace, project, table) + logger.info("Dropping table %s.", fqtable) + session.execute(DROP_TABLE_CQL.format(fqtable=fqtable)) + + # ------------------------------------------------------------------ + # OnlineStore interface — infrastructure + # ------------------------------------------------------------------ + + def update( + self, + config: RepoConfig, + tables_to_delete: Sequence[FeatureView], + tables_to_keep: Sequence[FeatureView], + entities_to_delete: Sequence[Entity], + entities_to_keep: Sequence[Entity], + partial: bool, + ) -> None: + """Create and drop feature-store tables as required by the registry.""" + project = config.project + online_store_config = config.online_store + assert isinstance(online_store_config, ScyllaDBOnlineStoreConfig) + default_sim = online_store_config.vector_similarity_function + + for table in tables_to_keep: + vec_features = _get_vector_features(table, default_sim) + if vec_features and table.ttl: + raise ValueError( + f"FeatureView '{table.name}' has both a TTL and vector features. " + "ScyllaDB does not support TTL on vector-indexed columns. " + "Remove the TTL or the vector_index tag." + ) + self._create_table(config, project, table, vec_features=vec_features) + + for table in tables_to_delete: + vec_features = _get_vector_features(table, default_sim) + self._drop_table(config, project, table, vec_features=vec_features) + + def teardown( + self, + config: RepoConfig, + tables: Sequence[FeatureView], + entities: Sequence[Entity], + ) -> None: + """Drop all tables for the given feature views.""" + project = config.project + online_store_config = config.online_store + assert isinstance(online_store_config, ScyllaDBOnlineStoreConfig) + + for table in tables: + vec_features = _get_vector_features( + table, online_store_config.vector_similarity_function + ) + self._drop_table(config, project, table, vec_features=vec_features) + + # ------------------------------------------------------------------ + # OnlineStore interface — write + # ------------------------------------------------------------------ + + def online_write_batch( + self, + config: RepoConfig, + table: FeatureView, + data: List[ + Tuple[EntityKeyProto, Dict[str, ValueProto], datetime, Optional[datetime]] + ], + progress: Optional[Callable[[int], Any]], + ) -> None: + """ + Write a batch of feature rows to the online store. + + All features are written to the main table. For vector features, + the float-list value is additionally written to the dedicated vector + table to support ANN search. + """ + project = config.project + online_store_config = config.online_store + assert isinstance(online_store_config, ScyllaDBOnlineStoreConfig) + default_sim = online_store_config.vector_similarity_function + vec_features = _get_vector_features(table, default_sim) + + # Compute TTL in seconds from the FeatureView's ttl field (timedelta or None). + # ScyllaDB will automatically expire rows after this many seconds, covering + # both "support for ttl at retrieval" and "support for deleting expired data". + ttl_seconds: Optional[int] = ( + int(table.ttl.total_seconds()) if table.ttl else None + ) + + vec_feature_set = {fn for fn, _, _ in vec_features} + fqtable = self._fq_table_name(self._keyspace, project, table) + insert_op = "insert_ttl" if ttl_seconds is not None else "insert" + insert_stmt = self._get_statement(config, insert_op, fqtable) + + session = self._get_session(config) + + # --- non-vector rows (all features except vector features) --- + def _main_rows() -> Iterable[Tuple]: + for entity_key, values, timestamp, created_ts in data: + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ).hex() + for feature_name, val in values.items(): + if feature_name in vec_feature_set: + continue # written separately via INSERT_VEC_CQL + row: Tuple = ( + feature_name, + val.SerializeToString(), + entity_key_bin, + timestamp, + created_ts, + ) + if ttl_seconds is not None: + row = row + (ttl_seconds,) + yield row + if progress: + progress(1) + + execute_concurrent_with_args( + session, + insert_stmt, + _main_rows(), + concurrency=online_store_config.write_concurrency, + ) + # correction for the last missing call to `progress`: + if progress: + progress(1) + + # --- vector rows --- + # TTL is not used here: TTL + vector features is rejected at feast apply time, + # so this path is only reached for TTL-free feature views. + if vec_features: + vec_insert_cql = INSERT_VEC_CQL.format(fqtable=fqtable) + if vec_insert_cql not in self._prepared_statements: + self._prepared_statements[vec_insert_cql] = session.prepare( + vec_insert_cql + ) + vec_insert_stmt = self._prepared_statements[vec_insert_cql] + + def _vec_rows() -> Iterable[Tuple]: + for entity_key, values, timestamp, created_ts in data: + entity_key_bin = serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ).hex() + for feat_name in vec_feature_set: + if feat_name not in values: + continue + val = values[feat_name] + yield ( + feat_name, + val.SerializeToString(), + entity_key_bin, + timestamp, + created_ts, + list(val.float_list_val.val), + ) + + execute_concurrent_with_args( + session, + vec_insert_stmt, + _vec_rows(), + concurrency=online_store_config.write_concurrency, + ) + + # ------------------------------------------------------------------ + # OnlineStore interface — read + # ------------------------------------------------------------------ + + def online_read( + self, + config: RepoConfig, + table: FeatureView, + entity_keys: List[EntityKeyProto], + requested_features: Optional[List[str]] = None, + ) -> List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]]: + """Read feature values for the given entity keys from the regular table.""" + project = config.project + online_store_config = config.online_store + assert isinstance(online_store_config, ScyllaDBOnlineStoreConfig) + + entity_key_bins = [ + serialize_entity_key( + ek, + entity_key_serialization_version=config.entity_key_serialization_version, + ).hex() + for ek in entity_keys + ] + + fqtable = self._fq_table_name(self._keyspace, project, table) + select_stmt = self._get_statement( + config, "select", fqtable, columns="feature_name, value, event_ts" + ) + + session = self._get_session(config) + retrieval = execute_concurrent_with_args( + session, + select_stmt, + ((ek_bin,) for ek_bin in entity_key_bins), + concurrency=online_store_config.read_concurrency, + ) + + results: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = [] + for success, rows_or_exc in retrieval: + if not success: + logger.error( + "ScyllaDB read error during concurrent fetch: %s", rows_or_exc + ) + results.append((None, None)) + continue + res: Dict[str, ValueProto] = {} + res_ts: Optional[datetime] = None + for row in rows_or_exc: + if requested_features is None or row.feature_name in requested_features: + val = ValueProto() + val.ParseFromString(row.value) + res[row.feature_name] = val + res_ts = row.event_ts + results.append((res_ts, res) if res else (None, None)) + + return results + + # ------------------------------------------------------------------ + # Vector store interface + # ------------------------------------------------------------------ + + def retrieve_online_documents_v2( + self, + config: RepoConfig, + table: FeatureView, + requested_features: List[str], + embedding: Optional[List[float]], + top_k: int, + distance_metric: Optional[str] = None, + query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, + ) -> List[ + Tuple[ + Optional[datetime], + Optional[EntityKeyProto], + Optional[Dict[str, ValueProto]], + ] + ]: + """ + Approximate nearest-neighbour search using ScyllaDB's vector_index. + + The feature view must have exactly one feature tagged with + ``vector_index=true``. The query *embedding* is compared against all + stored vectors; the top *top_k* results are returned together with + their feature values fetched from the regular feature table. + + Args: + config: RepoConfig for the current FeatureStore. + table: FeatureView to search. + requested_features: Feature names to include in the returned dicts. + embedding: Query vector. Must match the indexed column's dimension. + top_k: Number of results to return. + distance_metric: Override similarity function + (``COSINE`` / ``DOT_PRODUCT`` / ``EUCLIDEAN``). Defaults to the + store-level ``vector_similarity_function`` config value or the + per-feature tag. + query_string: Unused (reserved for future hybrid text+vector search). + filters: Unused (metadata filtering not yet supported for ScyllaDB). + include_feature_view_version_metadata: Unused. + + Returns: + List of ``(event_timestamp, entity_key_proto, feature_values)`` + tuples ordered from most to least similar. + """ + if filters is not None: + raise NotImplementedError( + "Metadata filtering is not supported by the ScyllaDB online store." + ) + if embedding is None: + raise ValueError( + "retrieve_online_documents_v2 requires a non-None 'embedding' " + "for ScyllaDB ANN search." + ) + + online_store_config = config.online_store + assert isinstance(online_store_config, ScyllaDBOnlineStoreConfig) + + default_sim = ( + distance_metric.upper() + if distance_metric + else online_store_config.vector_similarity_function + ) + project = config.project + vec_features = _get_vector_features(table, default_sim) + + if not vec_features: + raise NotImplementedError( + f"FeatureView '{table.name}' has no features tagged with " + "'vector_index=true'. Cannot perform vector search." + ) + if len(vec_features) > 1: + raise ValueError( + f"FeatureView '{table.name}' has {len(vec_features)} vector features. " + "retrieve_online_documents_v2 supports exactly one vector feature " + "per feature view." + ) + + vec_feature, _dim, sim_func = vec_features[0] + if distance_metric: + # Normalize aliases: Feast core uses "L2" for Euclidean distance; + # "cosine" (lowercase) is the default in feature_store.py; + # "inner_product" / "dot" are aliases for dot-product similarity. + _METRIC_ALIASES = { + "L2": "EUCLIDEAN", + "cosine": "COSINE", + "inner_product": "DOT_PRODUCT", + "dot": "DOT_PRODUCT", + } + candidate = distance_metric.upper() + sim_func = _METRIC_ALIASES.get( + distance_metric, _METRIC_ALIASES.get(candidate, candidate) + ) + + sim_expr_template = _SIM_FUNC_EXPR.get(sim_func) + if sim_expr_template is None: + raise ValueError( + f"Unsupported similarity function '{sim_func}'. " + "Choose from: COSINE, DOT_PRODUCT, EUCLIDEAN." + ) + sim_expr = sim_expr_template + + fqtable = self._fq_table_name(self._keyspace, project, table) + ann_cql = ANN_SELECT_CQL.format( + sim_func_call=sim_expr, + fqtable=fqtable, + ) + + session = self._get_session(config) + if ann_cql not in self._prepared_statements: + self._prepared_statements[ann_cql] = session.prepare(ann_cql) + ann_stmt = self._prepared_statements[ann_cql] + # Parameter order matches the placeholders in ANN_SELECT_CQL: + # 1. similarity_(vector_value, ?) → embedding + # 2. WHERE feature_name = ? → vec_feature + # 3. ORDER BY vector_value ANN OF ? → embedding + # 4. LIMIT ? → top_k + ann_rows = list( + session.execute(ann_stmt, (embedding, vec_feature, embedding, top_k)) + ) + + if not ann_rows: + return [] + + # Ordered list of entity key bins from ANN results + entity_key_bins: List[str] = [row.entity_key for row in ann_rows] + timestamps: Dict[str, Optional[datetime]] = { + row.entity_key: row.event_ts for row in ann_rows + } + + # Batch-fetch full feature values from the same main table + select_stmt = self._get_statement( + config, "select", fqtable, columns="feature_name, value, event_ts" + ) + + retrieval = execute_concurrent_with_args( + session, + select_stmt, + ((ek_bin,) for ek_bin in entity_key_bins), + concurrency=online_store_config.read_concurrency, + ) + + # Map entity_key_bin -> feature dict, preserving ANN order + feature_map: Dict[str, Dict[str, ValueProto]] = { + ek: {} for ek in entity_key_bins + } + for ek_bin, (success, rows_or_exc) in zip(entity_key_bins, retrieval): + if not success: + logger.error("ScyllaDB ANN batch-read error: %s", rows_or_exc) + continue + for row in rows_or_exc: + if requested_features and row.feature_name not in requested_features: + continue + val = ValueProto() + val.ParseFromString(row.value) + feature_map[ek_bin][row.feature_name] = val + + # Assemble output in ANN rank order + output: List[ + Tuple[ + Optional[datetime], + Optional[EntityKeyProto], + Optional[Dict[str, ValueProto]], + ] + ] = [] + for ek_bin in entity_key_bins: + try: + ek_proto: Optional[EntityKeyProto] = deserialize_entity_key( + bytes.fromhex(ek_bin), + entity_key_serialization_version=config.entity_key_serialization_version, + ) + except Exception: + ek_proto = None + + output.append( + ( + timestamps.get(ek_bin), + ek_proto, + feature_map.get(ek_bin, {}), + ) + ) + + return output diff --git a/sdk/python/feast/infra/online_stores/singlestore_online_store/__init__.py b/sdk/python/feast/infra/online_stores/singlestore_online_store/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/feast/infra/online_stores/singlestore_online_store/singlestore_repo_configuration.py b/sdk/python/feast/infra/online_stores/singlestore_online_store/singlestore_repo_configuration.py index 2debe0f0ee1..1e5379974c2 100644 --- a/sdk/python/feast/infra/online_stores/singlestore_online_store/singlestore_repo_configuration.py +++ b/sdk/python/feast/infra/online_stores/singlestore_online_store/singlestore_repo_configuration.py @@ -1,7 +1,7 @@ -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.singlestore import ( +from tests.universal.feature_repos.universal.online_store.singlestore import ( SingleStoreOnlineStoreCreator, ) diff --git a/sdk/python/feast/infra/online_stores/snowflake.py b/sdk/python/feast/infra/online_stores/snowflake.py index f294dcad96b..d2df674ed94 100644 --- a/sdk/python/feast/infra/online_stores/snowflake.py +++ b/sdk/python/feast/infra/online_stores/snowflake.py @@ -1,4 +1,3 @@ -import itertools import os from binascii import hexlify from datetime import datetime @@ -168,20 +167,26 @@ def online_read( requested_features = requested_features if requested_features else [] + # Pre-compute serialized entity keys to avoid redundant serialization + serialized_entity_keys = [ + serialize_entity_key( + entity_key, + entity_key_serialization_version=config.entity_key_serialization_version, + ) + for entity_key in entity_keys + ] + entity_fetch_str = ",".join( [ ( "TO_BINARY(" + hexlify( - serialize_entity_key( - combo[0], - entity_key_serialization_version=config.entity_key_serialization_version, - ) - + bytes(combo[1], encoding="utf-8") + serialized_entity_key + bytes(feature, encoding="utf-8") ).__str__()[1:] + ")" ) - for combo in itertools.product(entity_keys, requested_features) + for serialized_entity_key in serialized_entity_keys + for feature in requested_features ] ) @@ -197,11 +202,7 @@ def online_read( """ df = execute_snowflake_statement(conn, query).fetch_pandas_all() - for entity_key in entity_keys: - entity_key_bin = serialize_entity_key( - entity_key, - entity_key_serialization_version=config.entity_key_serialization_version, - ) + for entity_key_bin in serialized_entity_keys: res = {} res_ts = None for index, row in df[df["entity_key"] == entity_key_bin].iterrows(): diff --git a/sdk/python/feast/infra/online_stores/sqlite.py b/sdk/python/feast/infra/online_stores/sqlite.py index 461277631f5..000e3d7bc5a 100644 --- a/sdk/python/feast/infra/online_stores/sqlite.py +++ b/sdk/python/feast/infra/online_stores/sqlite.py @@ -16,6 +16,7 @@ import os import sqlite3 import sys +import time from datetime import date, datetime, timezone from pathlib import Path from typing import ( @@ -36,20 +37,30 @@ from feast import Entity from feast.feature_view import FeatureView from feast.field import Field +from feast.filter_models import ( + ComparisonFilter, + CompoundFilter, + FilterTranslator, + FilterType, + filters_contain_numeric_comparison, +) from feast.infra.infra_object import SQLITE_INFRA_OBJECT_CLASS_TYPE, InfraObject from feast.infra.key_encoding_utils import ( deserialize_entity_key, serialize_entity_key, serialize_f32, ) +from feast.infra.online_stores.helpers import compute_table_id, extract_text_and_num from feast.infra.online_stores.online_store import OnlineStore from feast.infra.online_stores.vector_store import VectorStoreConfig +from feast.labeling.label_view import LabelView from feast.protos.feast.core.InfraObject_pb2 import InfraObject as InfraObjectProto from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.core.SqliteTable_pb2 import SqliteTable as SqliteTableProto from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.repo_config import FeastConfigBaseModel, RepoConfig +from feast.stream_feature_view import StreamFeatureView from feast.type_map import feast_value_type_to_python_type from feast.types import FEAST_VECTOR_TYPES, PrimitiveFeastType from feast.utils import ( @@ -100,6 +111,97 @@ def convert_timestamp(val: bytes): sqlite3.register_converter("timestamp", convert_timestamp) +_SQLITE_COMPARISON_OPS: Dict[str, str] = { + "eq": "=", + "ne": "!=", + "gt": ">", + "gte": ">=", + "lt": "<", + "lte": "<=", +} + + +class SqliteFilterTranslator(FilterTranslator): + """Translates Feast filters into SQLite WHERE clause fragments.""" + + def __init__(self, table_name: str, alias: Optional[str] = None): + self.table_name = table_name + self.alias = alias + + def translate(self, filters: FilterType) -> Tuple[str, List[Any]]: + if filters is None: + return "", [] + return self._dispatch(filters) + + def translate_comparison(self, f: ComparisonFilter) -> Tuple[str, List[Any]]: + key, value, op_type = f.key, f.value, f.type + ek_col = f"{self.alias}.entity_key" if self.alias else "entity_key" + + if op_type in _SQLITE_COMPARISON_OPS: + col, db_value = _sqlite_filter_col_and_val(value) + clause = ( + f"{ek_col} IN (SELECT entity_key FROM {_quote_id(self.table_name)} " + f"WHERE feature_name = ? AND {col} {_SQLITE_COMPARISON_OPS[op_type]} ?)" + ) + return clause, [key, db_value] + + if op_type == "in": + if not isinstance(value, list): + raise ValueError( + f"'in' filter requires a list value, got {type(value)}" + ) + col, _ = ( + _sqlite_filter_col_and_val(value[0]) if value else ("value_text", None) + ) + db_values = [_sqlite_filter_col_and_val(v)[1] for v in value] + placeholders = ", ".join(["?"] * len(value)) + clause = ( + f"{ek_col} IN (SELECT entity_key FROM {_quote_id(self.table_name)} " + f"WHERE feature_name = ? AND {col} IN ({placeholders}))" + ) + return clause, [key] + db_values + + if op_type == "nin": + if not isinstance(value, list): + raise ValueError( + f"'nin' filter requires a list value, got {type(value)}" + ) + col, _ = ( + _sqlite_filter_col_and_val(value[0]) if value else ("value_text", None) + ) + db_values = [_sqlite_filter_col_and_val(v)[1] for v in value] + placeholders = ", ".join(["?"] * len(value)) + clause = ( + f"{ek_col} IN (SELECT entity_key FROM {_quote_id(self.table_name)} " + f"WHERE feature_name = ? AND {col} NOT IN ({placeholders}))" + ) + return clause, [key] + db_values + + raise ValueError(f"Unknown comparison operator: {op_type}") + + def translate_compound(self, f: CompoundFilter) -> Tuple[str, List[Any]]: + if not f.filters: + return "", [] + parts: List[str] = [] + all_params: List[Any] = [] + for sub in f.filters: + sub_clause, sub_params = self._dispatch(sub) + parts.append(sub_clause) + all_params.extend(sub_params) + joiner = " AND " if f.type == "and" else " OR " + combined = "(" + joiner.join(parts) + ")" + return combined, all_params + + +def _sqlite_filter_col_and_val(value: Any) -> Tuple[str, Any]: + """Return the appropriate column name and DB-ready value for a filter value.""" + if isinstance(value, bool): + return "value_num", 1.0 if value else 0.0 + if isinstance(value, (int, float)): + return "value_num", float(value) + return "value_text", str(value) + + class SqliteOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig): """Online store config for local (SQLite-based) store""" @@ -113,6 +215,8 @@ class SqliteOnlineStoreConfig(FeastConfigBaseModel, VectorStoreConfig): text_search_enabled: bool = False + enable_openai_compatible_store: bool = False + class SqliteOnlineStore(OnlineStore): """ @@ -123,6 +227,7 @@ class SqliteOnlineStore(OnlineStore): """ _conn: Optional[sqlite3.Connection] = None + _table_has_value_num: Optional[Dict[str, bool]] = None @staticmethod def _get_db_path(config: RepoConfig) -> str: @@ -147,6 +252,27 @@ def _get_conn(self, config: RepoConfig): return self._conn + def _check_table_has_value_num( + self, conn: sqlite3.Connection, table_name: str + ) -> bool: + """Check if the value_num column exists in the given table, with caching.""" + if self._table_has_value_num is None: + self._table_has_value_num = {} + if table_name in self._table_has_value_num: + return self._table_has_value_num[table_name] + + cur = conn.execute(f"PRAGMA table_info({_quote_id(table_name)})") + columns = {row[1] for row in cur.fetchall()} + exists = "value_num" in columns + self._table_has_value_num[table_name] = exists + return exists + + @staticmethod + def _filters_need_value_num( + filters: Union[ComparisonFilter, CompoundFilter], + ) -> bool: + return filters_contain_numeric_comparison(filters) + def online_write_batch( self, config: RepoConfig, @@ -164,6 +290,43 @@ def online_write_batch( conn = self._get_conn(config) project = config.project feature_type_dict = {f.name: f.dtype for f in table.features} + table_name = _table_id( + project, table, config.registry.enable_online_feature_view_versioning + ) + + enable_value_num = getattr( + config.online_store, "enable_openai_compatible_store", False + ) + has_value_num_col = False + if enable_value_num: + has_value_num_col = self._check_table_has_value_num(conn, table_name) + if not has_value_num_col: + logging.warning( + "enable_openai_compatible_store is True but value_num column " + "not found in table '%s'. Run `feast apply` to add it. " + "Writing without value_num.", + table_name, + ) + compute_value_num = has_value_num_col + columns = ["entity_key", "feature_name", "value", "value_text"] + if has_value_num_col: + columns.append("value_num") + if config.online_store.vector_enabled: + columns.append("vector_value") + columns.extend(["event_ts", "created_ts"]) + + pk_cols = {"entity_key", "feature_name"} + col_csv = ", ".join(columns) + placeholders = ", ".join(["?"] * len(columns)) + update_set = ", ".join( + f"{c} = excluded.{c}" for c in columns if c not in pk_cols + ) + upsert_sql = ( + f"INSERT INTO {_quote_id(table_name)} ({col_csv}) " + f"VALUES ({placeholders}) " + f"ON CONFLICT(entity_key, feature_name) DO UPDATE SET {update_set};" + ) + with conn: for entity_key, values, timestamp, created_ts in data: entity_key_bin = serialize_entity_key( @@ -174,8 +337,16 @@ def online_write_batch( if created_ts is not None: created_ts = to_naive_utc(created_ts) - table_name = _table_id(project, table) for feature_name, val in values.items(): + value_text, value_num = extract_text_and_num(val, compute_value_num) + row: List[Any] = [ + entity_key_bin, + feature_name, + val.SerializeToString(), + value_text, + ] + if has_value_num_col: + row.append(value_num) if config.online_store.vector_enabled: if ( feature_type_dict.get(feature_name, None) @@ -191,43 +362,10 @@ def online_write_batch( ) # type: ignore else: val_bin = feast_value_type_to_python_type(val) - conn.execute( - f""" - INSERT INTO {table_name} (entity_key, feature_name, value, vector_value, event_ts, created_ts) - VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(entity_key, feature_name) DO UPDATE SET - value = excluded.value, - vector_value = excluded.vector_value, - event_ts = excluded.event_ts, - created_ts = excluded.created_ts; - """, - ( - entity_key_bin, # entity_key - feature_name, # feature_name - val.SerializeToString(), # value - val_bin, # vector_value - timestamp, # event_ts - created_ts, # created_ts - ), - ) - else: - conn.execute( - f""" - INSERT INTO {table_name} (entity_key, feature_name, value, event_ts, created_ts) - VALUES (?, ?, ?, ?, ?) - ON CONFLICT(entity_key, feature_name) DO UPDATE SET - value = excluded.value, - event_ts = excluded.event_ts, - created_ts = excluded.created_ts; - """, - ( - entity_key_bin, # entity_key - feature_name, # feature_name - val.SerializeToString(), # value - timestamp, # event_ts - created_ts, # created_ts - ), - ) + row.append(val_bin) + row.extend([timestamp, created_ts]) + + conn.execute(upsert_sql, tuple(row)) if progress: progress(1) @@ -254,7 +392,7 @@ def online_read( # Fetch all entities in one go cur.execute( f"SELECT entity_key, feature_name, value, event_ts " - f"FROM {_table_id(config.project, table)} " + f"FROM {_quote_id(_table_id(config.project, table, config.registry.enable_online_feature_view_versioning))} " f"WHERE entity_key IN ({','.join('?' * len(entity_keys))}) " f"ORDER BY entity_key", serialized_entity_keys, @@ -263,11 +401,7 @@ def online_read( rows = { k: list(group) for k, group in itertools.groupby(rows, key=lambda r: r[0]) } - for entity_key in entity_keys: - entity_key_bin = serialize_entity_key( - entity_key, - entity_key_serialization_version=config.entity_key_serialization_version, - ) + for entity_key_bin in serialized_entity_keys: res = {} res_ts = None for _, feature_name, val_bin, ts in rows.get(entity_key_bin, []): @@ -297,33 +431,70 @@ def update( ): conn = self._get_conn(config) project = config.project + include_value_num = getattr( + config.online_store, "enable_openai_compatible_store", False + ) + versioning = config.registry.enable_online_feature_view_versioning for table in tables_to_keep: + tbl = _table_id(project, table, versioning) + value_num_col = "value_num REAL," if include_value_num else "" conn.execute( - f"CREATE TABLE IF NOT EXISTS {_table_id(project, table)} (entity_key BLOB, feature_name TEXT, value BLOB, vector_value BLOB, event_ts timestamp, created_ts timestamp, PRIMARY KEY(entity_key, feature_name))" + f"CREATE TABLE IF NOT EXISTS {_quote_id(tbl)} (entity_key BLOB, feature_name TEXT, value BLOB, value_text TEXT, {value_num_col} vector_value BLOB, event_ts timestamp, created_ts timestamp, PRIMARY KEY(entity_key, feature_name))" ) conn.execute( - f"CREATE INDEX IF NOT EXISTS {_table_id(project, table)}_ek ON {_table_id(project, table)} (entity_key);" + f"CREATE INDEX IF NOT EXISTS {_quote_id(tbl + '_ek')} ON {_quote_id(tbl)} (entity_key);" ) + _alter_table_add_column_if_missing(conn, tbl, "value_text", "TEXT") + if include_value_num: + _alter_table_add_column_if_missing(conn, tbl, "value_num", "REAL") + if self._table_has_value_num is None: + self._table_has_value_num = {} + self._table_has_value_num[tbl] = True for table in tables_to_delete: - conn.execute(f"DROP TABLE IF EXISTS {_table_id(project, table)}") + conn.execute( + f"DROP TABLE IF EXISTS {_quote_id(_table_id(project, table, versioning))}" + ) def plan( self, config: RepoConfig, desired_registry_proto: RegistryProto ) -> List[InfraObject]: project = config.project + versioning = config.registry.enable_online_feature_view_versioning + include_value_num = getattr( + config.online_store, "enable_openai_compatible_store", False + ) + # FeatureView.from_proto() is @typechecked and only accepts a + # FeatureViewProto, so it can't be applied to stream_feature_views + # (StreamFeatureViewProto) too -- each list needs its matching class. + views = [ + FeatureView.from_proto(view) + for view in desired_registry_proto.feature_views + ] + [ + StreamFeatureView.from_proto(view) + for view in desired_registry_proto.stream_feature_views + ] infra_objects: List[InfraObject] = [ SqliteTable( path=self._get_db_path(config), - name=_table_id(project, FeatureView.from_proto(view)), + name=_table_id(project, view, versioning), + include_value_num=include_value_num, ) - for view in [ - *desired_registry_proto.feature_views, - *desired_registry_proto.stream_feature_views, - ] + for view in views ] + + for lv_proto in desired_registry_proto.label_views: + if lv_proto.spec.online: + lv = LabelView.from_proto(lv_proto) + infra_objects.append( + SqliteTable( + path=self._get_db_path(config), + name=_table_id(project, lv, versioning), + ) + ) + return infra_objects def teardown( @@ -332,10 +503,23 @@ def teardown( tables: Sequence[FeatureView], entities: Sequence[Entity], ): - try: - os.unlink(self._get_db_path(config)) - except FileNotFoundError: - pass + if self._conn is not None: + try: + self._conn.close() + finally: + self._conn = None + + db_path = self._get_db_path(config) + for attempt in range(10): + try: + os.unlink(db_path) + return + except FileNotFoundError: + return + except PermissionError: + if attempt == 9: + raise + time.sleep(0.25) def retrieve_online_documents( self, @@ -379,7 +563,9 @@ def retrieve_online_documents( # Convert the embedding to a binary format instead of using SerializeToString() query_embedding_bin = serialize_f32(embedding, vector_field_length) - table_name = _table_id(project, table) + table_name = _table_id( + project, table, config.registry.enable_online_feature_view_versioning + ) vector_field = _get_vector_field(table) cur.execute( @@ -394,9 +580,10 @@ def retrieve_online_documents( cur.execute( f""" INSERT INTO vec_table(rowid, vector_value) - select rowid, vector_value from {table_name} - where feature_name = "{vector_field}" - """ + select rowid, vector_value from {_quote_id(table_name)} + where feature_name = ? + """, + (vector_field,), ) cur.execute( f""" @@ -406,7 +593,7 @@ def retrieve_online_documents( """ ) - # Have to join this with the {table_name} to get the feature name and entity_key + # Have to join this with the main table to get the feature name and entity_key # Also the `top_k` doesn't appear to be working for some reason cur.execute( f""" @@ -426,7 +613,7 @@ def retrieve_online_documents( order by distance limit ? ) f - left join {table_name} fv + left join {_quote_id(table_name)} fv on f.rowid = fv.rowid """, (query_embedding_bin, top_k), @@ -464,10 +651,12 @@ def retrieve_online_documents_v2( config: RepoConfig, table: FeatureView, requested_features: List[str], - query: Optional[List[float]], + embedding: Optional[List[float]], top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> List[ Tuple[ Optional[datetime], @@ -481,7 +670,7 @@ def retrieve_online_documents_v2( config: Feast configuration object table: FeatureView object as the table to search requested_features: List of requested features to retrieve - query: Query embedding to search for (optional) + embedding: Query embedding to search for (optional) top_k: Number of items to return distance_metric: Distance metric to use (optional) query_string: The query string to search for using keyword search (bm25) (optional) @@ -496,6 +685,16 @@ def retrieve_online_documents_v2( "You must enable either vector search or text search in the online store config" ) + if filters is not None: + if not getattr( + config.online_store, "enable_openai_compatible_store", False + ): + raise ValueError( + "Metadata filtering requires `enable_openai_compatible_store: true` " + "in your online store config. After setting it, run `feast apply` " + "to update the database schema." + ) + conn = self._get_conn(config) cur = conn.cursor() @@ -503,11 +702,24 @@ def retrieve_online_documents_v2( _get_feature_view_vector_field_metadata(table), "vector_length", 512 ) - table_name = _table_id(config.project, table) + table_name = _table_id( + config.project, table, config.registry.enable_online_feature_view_versioning + ) vector_field = _get_vector_field(table) + if filters is not None and self._filters_need_value_num(filters): + if not self._check_table_has_value_num(conn, table_name): + raise ValueError( + "Numerical filtering requires the `value_num` column. " + "Run `feast apply` to add it." + ) + + filter_clause, filter_params = SqliteFilterTranslator( + table_name, alias="fv2" + ).translate(filters) + if online_store.vector_enabled: - query_embedding_bin = serialize_f32(query, vector_field_length) # type: ignore + query_embedding_bin = serialize_f32(embedding, vector_field_length) # type: ignore cur.execute( f""" CREATE VIRTUAL TABLE IF NOT EXISTS vec_table using vec0( @@ -518,16 +730,16 @@ def retrieve_online_documents_v2( cur.execute( f""" INSERT INTO vec_table (rowid, vector_value) - select rowid, vector_value from {table_name} - where feature_name = "{vector_field}" - """ + select rowid, vector_value from {_quote_id(table_name)} + where feature_name = ? + """, + (vector_field,), ) elif online_store.text_search_enabled: string_field_list = [ f.name for f in table.features if f.dtype == PrimitiveFeastType.STRING ] string_fields = ", ".join(string_field_list) - # TODO: swap this for a value configurable in each Field() BM25_DEFAULT_WEIGHTS = ", ".join( [ str(1.0) @@ -546,6 +758,9 @@ def retrieve_online_documents_v2( table_name, string_field_list ) cur.execute(insert_query) + filter_clause, filter_params = SqliteFilterTranslator( + table_name, alias="fv" + ).translate(filters) else: raise ValueError( @@ -553,6 +768,12 @@ def retrieve_online_documents_v2( ) if online_store.vector_enabled: + where_parts = ["fv2.feature_name != ?"] + vector_params = [vector_field] + if filter_clause: + where_parts.append(filter_clause) + where_sql = " AND ".join(where_parts) + cur.execute( f""" select @@ -573,29 +794,33 @@ def retrieve_online_documents_v2( order by distance limit ? ) f - left join {table_name} fv + left join {_quote_id(table_name)} fv on f.rowid = fv.rowid - left join {table_name} fv2 + left join {_quote_id(table_name)} fv2 on fv.entity_key = fv2.entity_key - where fv2.feature_name != "{vector_field}" + where {where_sql} """, - ( - query_embedding_bin, - top_k, - ), + [query_embedding_bin, top_k] + vector_params + filter_params, ) elif online_store.text_search_enabled: + where_parts_text = [] + if filter_clause: + where_parts_text.append(filter_clause) + where_sql_text = ( + "where " + " AND ".join(where_parts_text) if where_parts_text else "" + ) + cur.execute( f""" - select - fv.entity_key, - fv.feature_name, - fv.value, - fv.vector_value, - f.distance, - fv.event_ts, - fv.created_ts - from {table_name} fv + select + fv.entity_key, + fv.feature_name, + fv.value, + fv.vector_value, + f.distance, + fv.event_ts, + fv.created_ts + from {_quote_id(table_name)} fv inner join ( select fv_rowid, @@ -606,8 +831,9 @@ def retrieve_online_documents_v2( where search_table match ? order by distance limit ? ) f on f.entity_key = fv.entity_key + {where_sql_text} """, - (query_string, top_k), + [query_string, top_k] + filter_params, ) else: @@ -703,8 +929,38 @@ def _initialize_conn( return db -def _table_id(project: str, table: FeatureView) -> str: - return f"{project}_{table.name}" +def _alter_table_add_column_if_missing( + conn: sqlite3.Connection, + table_name: str, + column_name: str, + column_type: str, +) -> None: + """Add a column to an existing SQLite table, ignoring if it already exists. + + SQLite's ALTER TABLE ADD COLUMN doesn't support IF NOT EXISTS, so we + catch the specific OperationalError for duplicate columns and re-raise + anything else (connection failures, disk errors, etc.). + """ + try: + conn.execute( + f"ALTER TABLE {_quote_id(table_name)} ADD COLUMN {_quote_id(column_name)} {column_type}" + ) + except sqlite3.OperationalError as e: + if "duplicate column name" not in str(e).lower(): + raise + + +def _quote_id(identifier: str) -> str: + """Quote a SQLite identifier to prevent SQL injection. + + Uses the standard SQL double-quote mechanism: any embedded + double-quote characters are escaped by doubling them. + """ + return '"' + identifier.replace('"', '""') + '"' + + +def _table_id(project: str, table: Any, enable_versioning: bool = False) -> str: + return compute_table_id(project, table, enable_versioning) class SqliteTable(InfraObject): @@ -719,11 +975,13 @@ class SqliteTable(InfraObject): path: str conn: sqlite3.Connection + _include_value_num: bool - def __init__(self, path: str, name: str): + def __init__(self, path: str, name: str, include_value_num: bool = False): super().__init__(name) self.path = path self.conn = _initialize_conn(path) + self._include_value_num = include_value_num def to_infra_object_proto(self) -> InfraObjectProto: sqlite_table_proto = self.to_proto() @@ -761,12 +1019,15 @@ def update(self): sqlite_vec.load(self.conn) except ModuleNotFoundError: logging.warning("Cannot use sqlite_vec for vector search") + value_num_col = "value_num REAL," if self._include_value_num else "" self.conn.execute( f""" CREATE TABLE IF NOT EXISTS {self.name} ( entity_key BLOB, feature_name TEXT, value BLOB, + value_text TEXT, + {value_num_col} vector_value BLOB, event_ts timestamp, created_ts timestamp, @@ -777,6 +1038,11 @@ def update(self): self.conn.execute( f"CREATE INDEX IF NOT EXISTS {self.name}_ek ON {self.name} (entity_key);" ) + _alter_table_add_column_if_missing(self.conn, self.name, "value_text", "TEXT") + if self._include_value_num: + _alter_table_add_column_if_missing( + self.conn, self.name, "value_num", "REAL" + ) def teardown(self): self.conn.execute(f"DROP TABLE IF EXISTS {self.name}") @@ -814,13 +1080,14 @@ def _generate_bm25_search_insert_query( """ _string_fields = ", ".join(string_field_list) query = f"INSERT INTO search_table (entity_key, fv_rowid, {_string_fields})\nSELECT\n\tDISTINCT fv0.entity_key,\n\tfv0.rowid as fv_rowid" - from_query = f"\nFROM (select rowid, * from {table_name} where feature_name = '{string_field_list[0]}') fv0" + quoted_table = _quote_id(table_name) + from_query = f"\nFROM (select rowid, * from {quoted_table} where feature_name = '{string_field_list[0]}') fv0" for i, string_field in enumerate(string_field_list): query += f"\n\t,fv{i}.value as {string_field}" if i > 0: from_query += ( - f"\nLEFT JOIN (select rowid, * from {table_name} where feature_name = '{string_field}') fv{i}" + f"\nLEFT JOIN (select rowid, * from {quoted_table} where feature_name = '{string_field}') fv{i}" + f"\n\tON fv0.entity_key = fv{i}.entity_key" ) diff --git a/sdk/python/feast/infra/passthrough_provider.py b/sdk/python/feast/infra/passthrough_provider.py index 2f960a02822..6b51d8c9fc1 100644 --- a/sdk/python/feast/infra/passthrough_provider.py +++ b/sdk/python/feast/infra/passthrough_provider.py @@ -24,6 +24,7 @@ from feast.feature_logging import FeatureServiceLoggingSource from feast.feature_service import FeatureService from feast.feature_view import FeatureView +from feast.filter_models import ComparisonFilter, CompoundFilter from feast.infra.common.materialization_job import ( MaterializationJobStatus, MaterializationTask, @@ -142,8 +143,8 @@ def plan_infra( def update_infra( self, project: str, - tables_to_delete: Sequence[FeatureView], - tables_to_keep: Sequence[Union[FeatureView, OnDemandFeatureView]], + tables_to_delete: Sequence[BaseFeatureView], + tables_to_keep: Sequence[BaseFeatureView], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], partial: bool, @@ -167,8 +168,8 @@ def update_infra( if self.batch_engine: self.batch_engine.update( project, - tables_to_delete, - tables_to_keep, + tables_to_delete, # type: ignore[arg-type] + tables_to_keep, # type: ignore[arg-type] entities_to_delete, entities_to_keep, ) @@ -247,6 +248,7 @@ def get_online_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: return self.online_store.get_online_features( config=config, @@ -255,6 +257,7 @@ def get_online_features( registry=registry, project=project, full_feature_names=full_feature_names, + include_feature_view_version_metadata=include_feature_view_version_metadata, ) async def get_online_features_async( @@ -268,6 +271,7 @@ async def get_online_features_async( registry: BaseRegistry, project: str, full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: return await self.online_store.get_online_features_async( config=config, @@ -276,6 +280,7 @@ async def get_online_features_async( registry=registry, project=project, full_feature_names=full_feature_names, + include_feature_view_version_metadata=include_feature_view_version_metadata, ) async def online_read_async( @@ -318,21 +323,25 @@ def retrieve_online_documents_v2( config: RepoConfig, table: FeatureView, requested_features: Optional[List[str]], - query: Optional[List[float]], + embedding: Optional[List[float]], top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> List: result = [] if self.online_store: result = self.online_store.retrieve_online_documents_v2( - config, - table, - requested_features, - query, - top_k, - distance_metric, - query_string, + config=config, + table=table, + requested_features=requested_features, + embedding=embedding, + top_k=top_k, + distance_metric=distance_metric, + query_string=query_string, + filters=filters, + include_feature_view_version_metadata=include_feature_view_version_metadata, ) return result @@ -364,7 +373,10 @@ def _prep_rows_to_write_for_ingestion( # Note: A dictionary mapping of column names in this data # source to feature names in a feature table or view. Only used for feature # columns, not entity or timestamp columns. - if hasattr(feature_view, "batch_source"): + if ( + hasattr(feature_view, "batch_source") + and feature_view.batch_source is not None + ): if feature_view.batch_source.field_mapping is not None: table = _run_pyarrow_field_mapping( table, feature_view.batch_source.field_mapping @@ -410,7 +422,10 @@ async def ingest_df_async( ) def ingest_df_to_offline_store(self, feature_view: FeatureView, table: pa.Table): - if feature_view.batch_source.field_mapping is not None: + if ( + feature_view.batch_source is not None + and feature_view.batch_source.field_mapping is not None + ): table = _run_pyarrow_field_mapping( table, feature_view.batch_source.field_mapping ) @@ -466,6 +481,8 @@ def get_historical_features( full_feature_names: bool, **kwargs, ) -> RetrievalJob: + if kwargs.get("filter_by_created_timestamp"): + self.offline_store.ensure_filter_by_created_timestamp_supported() job = self.offline_store.get_historical_features( config=config, feature_views=feature_views, @@ -482,8 +499,12 @@ def get_historical_features( def retrieve_saved_dataset( self, config: RepoConfig, dataset: SavedDataset ) -> RetrievalJob: + from feast.utils import _strip_version_from_ref + feature_name_columns = [ - ref.replace(":", "__") if dataset.full_feature_names else ref.split(":")[1] + _strip_version_from_ref(ref).replace(":", "__") + if dataset.full_feature_names + else ref.split(":")[1] for ref in dataset.features ] diff --git a/sdk/python/feast/infra/provider.py b/sdk/python/feast/infra/provider.py index c2879c1e2db..c3912cd9f55 100644 --- a/sdk/python/feast/infra/provider.py +++ b/sdk/python/feast/infra/provider.py @@ -23,6 +23,7 @@ from feast.data_source import DataSource from feast.entity import Entity from feast.feature_view import FeatureView +from feast.filter_models import ComparisonFilter, CompoundFilter from feast.importer import import_class from feast.infra.infra_object import Infra from feast.infra.offline_stores.offline_store import OfflineStore, RetrievalJob @@ -43,6 +44,7 @@ "aws": "feast.infra.passthrough_provider.PassthroughProvider", "local": "feast.infra.passthrough_provider.PassthroughProvider", "azure": "feast.infra.passthrough_provider.PassthroughProvider", + "unity_catalog": "feast.infra.data_sources.contrib.iceberg_catalog.uc_provider.UnityCatalogProvider", } @@ -69,8 +71,8 @@ def async_supported(self) -> ProviderAsyncMethods: def update_infra( self, project: str, - tables_to_delete: Sequence[FeatureView], - tables_to_keep: Sequence[Union[FeatureView, OnDemandFeatureView]], + tables_to_delete: Sequence[BaseFeatureView], + tables_to_keep: Sequence[BaseFeatureView], entities_to_delete: Sequence[Entity], entities_to_keep: Sequence[Entity], partial: bool, @@ -316,6 +318,7 @@ def get_online_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: pass @@ -331,6 +334,7 @@ async def get_online_features_async( registry: BaseRegistry, project: str, full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: pass @@ -464,10 +468,12 @@ def retrieve_online_documents_v2( config: RepoConfig, table: FeatureView, requested_features: List[str], - query: Optional[List[float]], + embedding: Optional[List[float]], top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> List[ Tuple[ Optional[datetime], @@ -486,6 +492,8 @@ def retrieve_online_documents_v2( query: The query embedding to search for (optional). top_k: The number of documents to return. query_string: The query string to search for using keyword search (bm25) (optional) + filters: Optional metadata filters (ComparisonFilter or CompoundFilter) + to narrow results before ranking. Returns: A list of dictionaries, where each dictionary contains the datetime, entitykey, and a dictionary diff --git a/sdk/python/feast/infra/ray_initializer.py b/sdk/python/feast/infra/ray_initializer.py index eea21eaa321..556eb20da66 100644 --- a/sdk/python/feast/infra/ray_initializer.py +++ b/sdk/python/feast/infra/ray_initializer.py @@ -219,6 +219,58 @@ def read_csv(self, path: Union[str, List[str]], **kwargs) -> Any: """Read CSV files using standard Ray.""" return ray.data.read_csv(path, **kwargs) + def read_json(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read JSON/JSONL file(s).""" + return ray.data.read_json(path, **kwargs) + + def read_text(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read plain-text file(s).""" + return ray.data.read_text(path, **kwargs) + + def read_images(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read image files (PNG, JPEG, …) from a directory as numpy arrays.""" + return ray.data.read_images(path, **kwargs) + + def read_binary_files(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read arbitrary binary files; each row has 'path' and 'bytes' columns.""" + return ray.data.read_binary_files(path, **kwargs) + + def read_tfrecords(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read TFRecord file(s).""" + return ray.data.read_tfrecords(path, **kwargs) + + def read_webdataset(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read WebDataset tar shard(s).""" + return ray.data.read_webdataset(path, **kwargs) + + def read_mongo(self, uri: str, database: str, collection: str, **kwargs) -> Any: + """Read from a MongoDB collection.""" + return ray.data.read_mongo( + uri=uri, database=database, collection=collection, **kwargs + ) + + def read_sql(self, sql: str, connection_url: str, **kwargs) -> Any: + """Read from a SQL database. Builds the connection factory from connection_url.""" + import sqlalchemy + + def _connection_factory(): + # raw_connection() returns a DB API2-compliant connection (with .cursor()) + # from the underlying driver. engine.connect() returns a SQLAlchemy 2.0 + # Connection object which does NOT expose .cursor(), failing Ray Data's + # DB API2 compliance check. + return sqlalchemy.create_engine(connection_url).raw_connection() + + return ray.data.read_sql(sql, _connection_factory, **kwargs) + + def from_huggingface( + self, dataset_name: str, split: str = "train", **kwargs + ) -> Any: + """Load a HuggingFace dataset and convert to a Ray Dataset.""" + from datasets import load_dataset + + hf_dataset = load_dataset(dataset_name, split=split, **kwargs) + return ray.data.from_huggingface(hf_dataset) + def from_pandas(self, df: Any) -> Any: """Create dataset from pandas DataFrame using standard Ray.""" return ray.data.from_pandas(df) @@ -235,10 +287,13 @@ def __init__( self, cluster_name: str, namespace: str, - auth_token: str, - auth_server: str, + auth_token: str = "", + auth_server: str = "", skip_tls: bool = False, enable_logging: bool = False, + num_gpus: float = 0, + worker_task_options: Optional[Dict[str, Any]] = None, + runtime_env: Optional[Dict[str, Any]] = None, ): """Initialize CodeFlare Ray wrapper with cluster connection parameters.""" self.cluster_name = cluster_name @@ -247,6 +302,9 @@ def __init__( self.auth_server = auth_server self.skip_tls = skip_tls self.enable_logging = enable_logging + self.num_gpus = num_gpus + self.worker_task_options = worker_task_options or {} + self.extra_runtime_env = runtime_env or {} self.cluster = None # Authenticate and setup Ray connection @@ -254,7 +312,12 @@ def __init__( self._setup_ray_connection() def _authenticate_codeflare(self): - """Authenticate with CodeFlare SDK.""" + """Authenticate with CodeFlare SDK. Skipped for in-cluster pods with no explicit token.""" + if not self.auth_token or not self.auth_server: + logger.info( + "No auth_token/auth_server provided; assuming in-cluster auth via service account" + ) + return try: from codeflare_sdk import TokenAuthentication @@ -288,6 +351,18 @@ def _setup_ray_connection(self): "pip": ["feast"], "env_vars": {"RAY_DISABLE_IMPORT_WARNING": "1"}, } + if self.extra_runtime_env: + extra_pip = self.extra_runtime_env.get("pip", []) + if extra_pip: + runtime_env["pip"] = list( + dict.fromkeys(runtime_env["pip"] + extra_pip) + ) + extra_env_vars = self.extra_runtime_env.get("env_vars", {}) + if extra_env_vars: + runtime_env["env_vars"].update(extra_env_vars) + for k, v in self.extra_runtime_env.items(): + if k not in ("pip", "env_vars"): + runtime_env[k] = v ray.shutdown() @@ -307,54 +382,225 @@ def _setup_ray_connection(self): logger.error(f"Ray connection failed: {e}") raise + def _get_task_options(self, include_gpu: bool = False) -> Dict[str, Any]: + """Build Ray .options() kwargs for a remote task dispatch. + + Always includes the full worker_task_options passthrough dict. + num_gpus is merged in only when include_gpu=True, because I/O + operations (read_parquet, read_csv, from_pandas, from_arrow) are + pure data-movement and must not consume GPU slots. Pass + include_gpu=True only for compute/transformation tasks. + """ + opts: Dict[str, Any] = dict(self.worker_task_options) + if include_gpu and self.num_gpus: + opts["num_gpus"] = self.num_gpus + else: + # Guarantee no GPU slot is consumed for I/O tasks even if the + # caller placed num_gpus inside worker_task_options directly. + opts.pop("num_gpus", None) + return opts + # Ray Data API methods - wrapped in @ray.remote to execute on cluster workers def read_parquet(self, path: Union[str, List[str]], **kwargs) -> Any: """Read parquet files - runs remotely on KubeRay cluster workers.""" from feast.infra.ray_shared_utils import RemoteDatasetProxy @ray.remote - def _remote_read_parquet(file_path, read_kwargs): + def _remote(file_path, read_kwargs): import ray return ray.data.read_parquet(file_path, **read_kwargs) - return RemoteDatasetProxy(_remote_read_parquet.remote(path, kwargs)) + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(path, kwargs)) def read_csv(self, path: Union[str, List[str]], **kwargs) -> Any: - """Read CSV files - runs remotely on KubeRay cluster workers.""" + """Read CSV files - dispatched via @ray.remote to cluster workers.""" from feast.infra.ray_shared_utils import RemoteDatasetProxy @ray.remote - def _remote_read_csv(file_path, read_kwargs): + def _remote(file_path, read_kwargs): import ray return ray.data.read_csv(file_path, **read_kwargs) - return RemoteDatasetProxy(_remote_read_csv.remote(path, kwargs)) + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(path, kwargs)) + + def read_json(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read JSON/JSONL files - dispatched via @ray.remote to cluster workers.""" + from feast.infra.ray_shared_utils import RemoteDatasetProxy + + @ray.remote + def _remote(file_path, read_kwargs): + import ray + + return ray.data.read_json(file_path, **read_kwargs) + + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(path, kwargs)) + + def read_text(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read plain-text files - dispatched via @ray.remote to cluster workers.""" + from feast.infra.ray_shared_utils import RemoteDatasetProxy + + @ray.remote + def _remote(file_path, read_kwargs): + import ray + + return ray.data.read_text(file_path, **read_kwargs) + + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(path, kwargs)) + + def read_images(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read image directory - dispatched via @ray.remote to cluster workers.""" + from feast.infra.ray_shared_utils import RemoteDatasetProxy + + @ray.remote + def _remote(file_path, read_kwargs): + import ray + + return ray.data.read_images(file_path, **read_kwargs) + + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(path, kwargs)) + + def read_binary_files(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read binary files - dispatched via @ray.remote to cluster workers.""" + from feast.infra.ray_shared_utils import RemoteDatasetProxy + + @ray.remote + def _remote(file_path, read_kwargs): + import ray + + return ray.data.read_binary_files(file_path, **read_kwargs) + + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(path, kwargs)) + + def read_tfrecords(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read TFRecord files - dispatched via @ray.remote to cluster workers.""" + from feast.infra.ray_shared_utils import RemoteDatasetProxy + + @ray.remote + def _remote(file_path, read_kwargs): + import ray + + return ray.data.read_tfrecords(file_path, **read_kwargs) + + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(path, kwargs)) + + def read_webdataset(self, path: Union[str, List[str]], **kwargs) -> Any: + """Read WebDataset shards - dispatched via @ray.remote to cluster workers.""" + from feast.infra.ray_shared_utils import RemoteDatasetProxy + + @ray.remote + def _remote(file_path, read_kwargs): + import ray + + return ray.data.read_webdataset(file_path, **read_kwargs) + + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(path, kwargs)) + + def read_mongo(self, uri: str, database: str, collection: str, **kwargs) -> Any: + """Read from MongoDB - dispatched via @ray.remote to cluster workers.""" + from feast.infra.ray_shared_utils import RemoteDatasetProxy + + @ray.remote + def _remote(mongo_uri, db, col, read_kwargs): + import ray + + return ray.data.read_mongo( + uri=mongo_uri, database=db, collection=col, **read_kwargs + ) + + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(uri, database, collection, kwargs)) + + def read_sql(self, sql: str, connection_url: str, **kwargs) -> Any: + """Read from SQL database - dispatched via @ray.remote to cluster workers. + + Accepts connection_url (str) instead of a connection_factory callable so + the argument is serialisable across the Ray object store boundary. + """ + from feast.infra.ray_shared_utils import RemoteDatasetProxy + + @ray.remote + def _remote(sql_query, conn_url, read_kwargs): + import ray + import sqlalchemy + + def _factory(): + return sqlalchemy.create_engine(conn_url).raw_connection() + + return ray.data.read_sql(sql_query, _factory, **read_kwargs) + + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(sql, connection_url, kwargs)) + + def from_huggingface( + self, dataset_name: str, split: str = "train", **kwargs + ) -> Any: + """Load a HuggingFace dataset on a remote Ray worker and return a Ray Dataset. + + The dataset is loaded directly on the worker to avoid serializing + memory-mapped HF Dataset objects across the network and to keep + driver memory usage near zero. + """ + from feast.infra.ray_shared_utils import RemoteDatasetProxy + + @ray.remote + def _remote(ds_name, ds_split, extra_kwargs): + import ray + from datasets import load_dataset + + hf_dataset = load_dataset(ds_name, split=ds_split, **extra_kwargs) + return ray.data.from_huggingface(hf_dataset) + + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(dataset_name, split, kwargs)) def from_pandas(self, df: Any) -> Any: - """Create dataset from pandas DataFrame - runs remotely on KubeRay cluster workers.""" + """Create dataset from pandas DataFrame - dispatched via @ray.remote.""" from feast.infra.ray_shared_utils import RemoteDatasetProxy @ray.remote - def _remote_from_pandas(dataframe): + def _remote(dataframe): import ray return ray.data.from_pandas(dataframe) - return RemoteDatasetProxy(_remote_from_pandas.remote(df)) + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(df)) def from_arrow(self, table: Any) -> Any: - """Create dataset from Arrow table - runs remotely on KubeRay cluster workers.""" + """Create dataset from Arrow table - dispatched via @ray.remote.""" from feast.infra.ray_shared_utils import RemoteDatasetProxy @ray.remote - def _remote_from_arrow(arrow_table): + def _remote(arrow_table): import ray return ray.data.from_arrow(arrow_table) - return RemoteDatasetProxy(_remote_from_arrow.remote(table)) + opts = self._get_task_options() + remote_fn = _remote.options(**opts) if opts else _remote + return RemoteDatasetProxy(remote_fn.remote(table)) # Global state tracking @@ -547,10 +793,13 @@ def _initialize_kuberay(config: Any, enable_logging: bool = False) -> None: _ray_wrapper = CodeFlareRayWrapper( cluster_name=kuberay_config["cluster_name"], namespace=kuberay_config["namespace"], - auth_token=kuberay_config["auth_token"], - auth_server=kuberay_config["auth_server"], + auth_token=kuberay_config.get("auth_token", ""), + auth_server=kuberay_config.get("auth_server", ""), skip_tls=kuberay_config.get("skip_tls", False), enable_logging=enable_logging, + num_gpus=getattr(config, "num_gpus", 0) or 0, + worker_task_options=getattr(config, "worker_task_options", None), + runtime_env=kuberay_config.get("runtime_env"), ) logger.info("KubeRay cluster connection established via CodeFlare SDK") diff --git a/sdk/python/feast/infra/ray_shared_utils.py b/sdk/python/feast/infra/ray_shared_utils.py index 9614623294f..1d26074290a 100644 --- a/sdk/python/feast/infra/ray_shared_utils.py +++ b/sdk/python/feast/infra/ray_shared_utils.py @@ -14,14 +14,75 @@ def __init__(self, dataset_ref: Any): """Initialize with a reference to the remote dataset.""" self._dataset_ref = dataset_ref - def map_batches(self, func, **kwargs) -> "RemoteDatasetProxy": - """Execute map_batches remotely on cluster workers.""" + def map_batches( + self, + func, + num_gpus: float = 0, + worker_task_options: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> "RemoteDatasetProxy": + """Execute map_batches remotely on cluster workers. + + Resource options are applied at two levels: + + 1. **Orchestration task** (@ray.remote wrapper) — receives only the + non-compute keys from worker_task_options (runtime_env, max_retries, + scheduling_strategy, memory, …). Compute-scheduling keys + (num_gpus, num_cpus, accelerator_type, resources) are intentionally + excluded: the orchestration task only calls dataset.map_batches() + and holds no GPU/CPU work itself. Including num_gpus here would + waste a GPU slot for the entire operation duration and, on + GPU-constrained clusters, could cause deadlock where the orchestrator + holds a GPU while the data workers queue waiting for the same slots. + + 2. **Data workers** (inside dataset.map_batches) — receives the full + compute-scheduling subset (num_gpus, num_cpus, accelerator_type, + resources). Ray Data propagates these to the actual processing tasks. + + Args: + func: Batch transformation function. + num_gpus: Shorthand GPU count (merged into worker_task_options, + takes precedence). Kept first-class because it also drives + gpu_batch_format selection in the compute engine. + worker_task_options: Arbitrary Ray .options() kwargs (num_cpus, + memory, accelerator_type, resources, runtime_env, + max_retries, …). + **kwargs: Additional map_batches kwargs (batch_format, concurrency). + """ + # Merge num_gpus into worker_task_options; dedicated field takes precedence + opts: Dict[str, Any] = dict(worker_task_options or {}) + if num_gpus: + opts["num_gpus"] = num_gpus + + # Keys accepted by Ray Data's map_batches for per-worker scheduling + _MAP_BATCHES_RESOURCE_KEYS = { + "num_gpus", + "num_cpus", + "accelerator_type", + "resources", + } + + # Data workers get the compute-scheduling subset only + map_resource_kwargs = { + k: v for k, v in opts.items() if k in _MAP_BATCHES_RESOURCE_KEYS + } + + # Orchestration task gets the remainder (non-compute keys only) so it + # never holds GPU/CPU slots it doesn't use + orchestration_opts = { + k: v for k, v in opts.items() if k not in _MAP_BATCHES_RESOURCE_KEYS + } @ray.remote - def _remote_map_batches(dataset, function, batch_kwargs): - return dataset.map_batches(function, **batch_kwargs) + def _remote_map_batches(dataset, function, batch_kwargs, resource_kwargs): + return dataset.map_batches(function, **batch_kwargs, **resource_kwargs) - new_ref = _remote_map_batches.remote(self._dataset_ref, func, kwargs) + remote_fn = ( + _remote_map_batches.options(**orchestration_opts) + if orchestration_opts + else _remote_map_batches + ) + new_ref = remote_fn.remote(self._dataset_ref, func, kwargs, map_resource_kwargs) return RemoteDatasetProxy(new_ref) def filter(self, fn) -> "RemoteDatasetProxy": diff --git a/sdk/python/feast/infra/registry/base_registry.py b/sdk/python/feast/infra/registry/base_registry.py index 3ab2f52e349..4256c82e50d 100644 --- a/sdk/python/feast/infra/registry/base_registry.py +++ b/sdk/python/feast/infra/registry/base_registry.py @@ -24,9 +24,14 @@ from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.entity import Entity +from feast.errors import ( + ConflictingFeatureViewNames, + FeatureViewNotFoundException, +) from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.infra.infra_object import Infra +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission from feast.project import Project @@ -37,6 +42,7 @@ FeatureService as FeatureServiceProto, ) from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto +from feast.protos.feast.core.LabelView_pb2 import LabelView as LabelViewProto from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( OnDemandFeatureView as OnDemandFeatureViewProto, ) @@ -251,7 +257,11 @@ def list_feature_services( # Feature view operations @abstractmethod def apply_feature_view( - self, feature_view: BaseFeatureView, project: str, commit: bool = True + self, + feature_view: BaseFeatureView, + project: str, + commit: bool = True, + no_promote: bool = False, ): """ Registers a single feature view with Feast @@ -260,13 +270,96 @@ def apply_feature_view( feature_view: Feature view that will be registered project: Feast project that this feature view belongs to commit: Whether the change should be persisted immediately + no_promote: If True, save a new version snapshot without promoting + it to the active definition. The new version is accessible only + via explicit @v reads. """ raise NotImplementedError + def _ensure_feature_view_name_is_unique( + self, + feature_view: BaseFeatureView, + project: str, + allow_cache: bool = False, + ): + """ + Validates that no feature view name conflict exists across feature view types. + Raises ConflictingFeatureViewNames if a different type already uses the name. + + This is a defense-in-depth check for direct apply_feature_view() calls. + The primary validation happens in _validate_all_feature_views() during feast plan/apply. + """ + name = feature_view.name + new_type = type(feature_view).__name__ + + def _check_conflict(getter, not_found_exc, existing_type: str): + try: + getter(name, project, allow_cache=allow_cache) + raise ConflictingFeatureViewNames(name, existing_type, new_type) + except not_found_exc: + pass + + # Check StreamFeatureView before FeatureView since StreamFeatureView is a subclass + # Note: All getters raise FeatureViewNotFoundException (not type-specific exceptions) + if isinstance(feature_view, LabelView): + _check_conflict( + self.get_feature_view, FeatureViewNotFoundException, "FeatureView" + ) + _check_conflict( + self.get_stream_feature_view, + FeatureViewNotFoundException, + "StreamFeatureView", + ) + _check_conflict( + self.get_on_demand_feature_view, + FeatureViewNotFoundException, + "OnDemandFeatureView", + ) + elif isinstance(feature_view, StreamFeatureView): + _check_conflict( + self.get_feature_view, FeatureViewNotFoundException, "FeatureView" + ) + _check_conflict( + self.get_on_demand_feature_view, + FeatureViewNotFoundException, + "OnDemandFeatureView", + ) + _check_conflict( + self.get_label_view, FeatureViewNotFoundException, "LabelView" + ) + elif isinstance(feature_view, FeatureView): + _check_conflict( + self.get_stream_feature_view, + FeatureViewNotFoundException, + "StreamFeatureView", + ) + _check_conflict( + self.get_on_demand_feature_view, + FeatureViewNotFoundException, + "OnDemandFeatureView", + ) + _check_conflict( + self.get_label_view, FeatureViewNotFoundException, "LabelView" + ) + elif isinstance(feature_view, OnDemandFeatureView): + _check_conflict( + self.get_feature_view, FeatureViewNotFoundException, "FeatureView" + ) + _check_conflict( + self.get_stream_feature_view, + FeatureViewNotFoundException, + "StreamFeatureView", + ) + _check_conflict( + self.get_label_view, FeatureViewNotFoundException, "LabelView" + ) + @abstractmethod def delete_feature_view(self, name: str, project: str, commit: bool = True): """ - Deletes a feature view or raises an exception if not found. + Deletes a feature view of any kind (FeatureView, OnDemandFeatureView, StreamFeatureView). + Or raises an exception if not found. + Args: name: Name of feature view @@ -300,6 +393,7 @@ def list_stream_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[StreamFeatureView]: """ Retrieve a list of stream feature views from the registry @@ -308,6 +402,7 @@ def list_stream_feature_views( project: Filter stream feature views based on project name allow_cache: Whether to allow returning stream feature views from a cached registry tags: Filter by tags + skip_udf: Skip deserializing UDFs (for metadata-only operations) Returns: List of stream feature views @@ -339,6 +434,7 @@ def list_on_demand_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[OnDemandFeatureView]: """ Retrieve a list of on demand feature views from the registry @@ -347,12 +443,64 @@ def list_on_demand_feature_views( project: Filter on demand feature views based on project name allow_cache: Whether to allow returning on demand feature views from a cached registry tags: Filter by tags + skip_udf: Skip deserializing UDFs (for metadata-only operations) Returns: List of on demand feature views """ raise NotImplementedError + # Label view operations + @abstractmethod + def get_label_view( + self, name: str, project: str, allow_cache: bool = False + ) -> LabelView: + """ + Retrieves a label view. + + Args: + name: Name of label view + project: Feast project that this label view belongs to + allow_cache: Whether to allow returning this label view from a cached registry + + Returns: + Returns either the specified label view, or raises an exception if + none is found + """ + raise NotImplementedError + + @abstractmethod + def list_label_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[LabelView]: + """ + Retrieve a list of label views from the registry + + Args: + project: Filter label views based on project name + allow_cache: Whether to allow returning label views from a cached registry + tags: Filter by tags + + Returns: + List of label views + """ + raise NotImplementedError + + @abstractmethod + def delete_label_view(self, name: str, project: str, commit: bool = True): + """ + Deletes a label view or raises an exception if not found. + + Args: + name: Name of label view + project: Feast project that this label view belongs to + commit: Whether the change should be persisted immediately + """ + raise NotImplementedError + # regular feature view operations @abstractmethod def get_feature_view( @@ -378,6 +526,7 @@ def list_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[FeatureView]: """ Retrieve a list of feature views from the registry @@ -386,6 +535,7 @@ def list_feature_views( allow_cache: Allow returning feature views from the cached registry project: Filter feature views based on project name tags: Filter by tags + skip_udf: Skip deserializing UDFs (for metadata-only operations) Returns: List of feature views @@ -416,6 +566,8 @@ def list_all_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, + updated_since: Optional[datetime] = None, ) -> List[BaseFeatureView]: """ Retrieve a list of feature views of all types from the registry @@ -424,16 +576,58 @@ def list_all_feature_views( allow_cache: Allow returning feature views from the cached registry project: Filter feature views based on project name tags: Filter by tags + skip_udf: Skip deserializing UDFs (for metadata-only operations) + updated_since: Only return feature views updated at or after this timestamp Returns: List of feature views """ raise NotImplementedError + def list_feature_view_versions( + self, name: str, project: str + ) -> List[Dict[str, Any]]: + """ + List version history for a feature view. + + Args: + name: Name of feature view + project: Feast project that this feature view belongs to + + Returns: + List of version records with version, version_number, feature_view_type, + created_timestamp, and version_id. + """ + raise NotImplementedError( + "list_feature_view_versions is not implemented for this registry" + ) + + def get_feature_view_by_version( + self, name: str, project: str, version_number: int, allow_cache: bool = False + ) -> BaseFeatureView: + """ + Retrieve a feature view snapshot for a specific version number. + + Args: + name: Name of feature view + project: Feast project that this feature view belongs to + version_number: The version number to retrieve + allow_cache: Whether to allow returning from a cached registry + + Returns: + The feature view snapshot at the specified version. + + Raises: + FeatureViewVersionNotFound: if the version doesn't exist. + """ + raise NotImplementedError( + "get_feature_view_by_version is not implemented for this registry" + ) + @abstractmethod def apply_materialization( self, - feature_view: Union[FeatureView, OnDemandFeatureView], + feature_view: Union[FeatureView, OnDemandFeatureView, "LabelView"], project: str, start_date: datetime, end_date: datetime, @@ -504,6 +698,8 @@ def list_saved_datasets( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + namespace: Optional[str] = None, + collection: Optional[str] = None, ) -> List[SavedDataset]: """ Retrieves a list of all saved datasets in specified project @@ -512,6 +708,8 @@ def list_saved_datasets( project: Feast project allow_cache: Whether to allow returning this dataset from a cached registry tags: Filter by tags + namespace: Filter by logical namespace grouping + collection: Filter by collection sub-grouping within namespace Returns: Returns the list of SavedDatasets @@ -803,6 +1001,16 @@ def refresh(self, project: Optional[str] = None): """Refreshes the state of the registry cache by fetching the registry state from the remote registry store.""" raise NotImplementedError + def is_cache_valid(self) -> bool: + """Check whether the registry's local cache is still within its TTL. + + Returns True if cached data can be used without a refresh. + Subclasses that support caching should override this. + Registries without caching always return False (every read goes + to the backing store). + """ + return False + # Lineage operations def get_registry_lineage( self, @@ -932,6 +1140,11 @@ def _build_registry_proto( for on_demand_feature_view in on_demand_feature_views: registry.on_demand_feature_views.append(on_demand_feature_view.to_proto()) + # Add all label views + label_views = self.list_label_views(project=project, allow_cache=allow_cache) + for label_view in label_views: + registry.label_views.append(label_view.to_proto()) + # Add all feature services feature_services = self.list_feature_services( project=project, allow_cache=allow_cache @@ -1035,6 +1248,13 @@ def to_dict(self, project: str) -> Dict[str, List[Any]]: ) registry_dict["streamFeatureViews"].append(sfv_dict) + for label_view in sorted( + self.list_label_views(project=project), + key=lambda lv: lv.name, + ): + registry_dict["labelViews"].append( + self._message_to_sorted_dict(label_view.to_proto()) + ) for saved_dataset in sorted( self.list_saved_datasets(project=project), key=lambda item: item.name ): @@ -1066,6 +1286,8 @@ def deserialize_registry_values(serialized_proto, feast_obj_type) -> Any: return StreamFeatureViewProto.FromString(serialized_proto) if feast_obj_type == OnDemandFeatureView: return OnDemandFeatureViewProto.FromString(serialized_proto) + if feast_obj_type == LabelView: + return LabelViewProto.FromString(serialized_proto) if feast_obj_type == FeatureService: return FeatureServiceProto.FromString(serialized_proto) if feast_obj_type == Permission: diff --git a/sdk/python/feast/infra/registry/caching_registry.py b/sdk/python/feast/infra/registry/caching_registry.py index ce346272af9..81fc11ed025 100644 --- a/sdk/python/feast/infra/registry/caching_registry.py +++ b/sdk/python/feast/infra/registry/caching_registry.py @@ -3,9 +3,9 @@ import threading import warnings from abc import abstractmethod -from datetime import timedelta +from datetime import datetime, timedelta from threading import Lock -from typing import List, Optional +from typing import Any, Dict, List, Optional from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource @@ -15,6 +15,7 @@ from feast.infra.infra_object import Infra from feast.infra.registry import proto_registry_utils from feast.infra.registry.base_registry import BaseRegistry +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission from feast.project import Project @@ -22,7 +23,7 @@ from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView -from feast.utils import _utc_now +from feast.utils import _utc_now, to_naive_utc logger = logging.getLogger(__name__) @@ -121,7 +122,11 @@ def get_any_feature_view( @abstractmethod def _list_all_feature_views( - self, project: str, tags: Optional[dict[str, str]] + self, + project: str, + tags: Optional[dict[str, str]], + updated_since: Optional[datetime] = None, + **kwargs: Any, ) -> List[BaseFeatureView]: pass @@ -130,13 +135,28 @@ def list_all_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, + updated_since: Optional[datetime] = None, ) -> List[BaseFeatureView]: if allow_cache: self._refresh_cached_registry_if_necessary() - return proto_registry_utils.list_all_feature_views( - self.cached_registry_proto, project, tags + feature_views = proto_registry_utils.list_all_feature_views( + self.cached_registry_proto, project, tags, skip_udf=skip_udf + ) + if updated_since is not None: + # last_updated_timestamp from proto is offset-naive UTC; normalise for comparison + cutoff = to_naive_utc(updated_since) + feature_views = [ + fv + for fv in feature_views + if fv.last_updated_timestamp is not None + and fv.last_updated_timestamp >= cutoff + ] + else: + feature_views = self._list_all_feature_views( + project, tags, updated_since=updated_since, skip_udf=skip_udf ) - return self._list_all_feature_views(project, tags) + return feature_views @abstractmethod def _get_feature_view(self, name: str, project: str) -> FeatureView: @@ -154,7 +174,7 @@ def get_feature_view( @abstractmethod def _list_feature_views( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs: Any ) -> List[FeatureView]: pass @@ -163,13 +183,14 @@ def list_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[FeatureView]: if allow_cache: self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_feature_views( - self.cached_registry_proto, project, tags + self.cached_registry_proto, project, tags, skip_udf=skip_udf ) - return self._list_feature_views(project, tags) + return self._list_feature_views(project, tags, skip_udf=skip_udf) @abstractmethod def _get_on_demand_feature_view( @@ -189,7 +210,7 @@ def get_on_demand_feature_view( @abstractmethod def _list_on_demand_feature_views( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs: Any ) -> List[OnDemandFeatureView]: pass @@ -198,13 +219,14 @@ def list_on_demand_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[OnDemandFeatureView]: if allow_cache: self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_on_demand_feature_views( - self.cached_registry_proto, project, tags + self.cached_registry_proto, project, tags, skip_udf=skip_udf ) - return self._list_on_demand_feature_views(project, tags) + return self._list_on_demand_feature_views(project, tags, skip_udf=skip_udf) @abstractmethod def _get_stream_feature_view(self, name: str, project: str) -> StreamFeatureView: @@ -222,7 +244,7 @@ def get_stream_feature_view( @abstractmethod def _list_stream_feature_views( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs: Any ) -> List[StreamFeatureView]: pass @@ -231,13 +253,47 @@ def list_stream_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[StreamFeatureView]: if allow_cache: self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_stream_feature_views( + self.cached_registry_proto, project, tags, skip_udf=skip_udf + ) + return self._list_stream_feature_views(project, tags, skip_udf=skip_udf) + + @abstractmethod + def _get_label_view(self, name: str, project: str) -> LabelView: + pass + + def get_label_view( + self, name: str, project: str, allow_cache: bool = False + ) -> LabelView: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_label_view( + self.cached_registry_proto, name, project + ) + return self._get_label_view(name, project) + + @abstractmethod + def _list_label_views( + self, project: str, tags: Optional[dict[str, str]], **kwargs: Any + ) -> List[LabelView]: + pass + + def list_label_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[LabelView]: + if allow_cache: + self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_label_views( self.cached_registry_proto, project, tags ) - return self._list_stream_feature_views(project, tags) + return self._list_label_views(project, tags) @abstractmethod def _get_feature_service(self, name: str, project: str) -> FeatureService: @@ -288,7 +344,11 @@ def get_saved_dataset( @abstractmethod def _list_saved_datasets( - self, project: str, tags: Optional[dict[str, str]] = None + self, + project: str, + tags: Optional[dict[str, str]] = None, + namespace: Optional[str] = None, + collection: Optional[str] = None, ) -> List[SavedDataset]: pass @@ -297,13 +357,21 @@ def list_saved_datasets( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + namespace: Optional[str] = None, + collection: Optional[str] = None, ) -> List[SavedDataset]: if allow_cache: self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_saved_datasets( - self.cached_registry_proto, project, tags + self.cached_registry_proto, + project, + tags, + namespace=namespace, + collection=collection, ) - return self._list_saved_datasets(project, tags) + return self._list_saved_datasets( + project, tags, namespace=namespace, collection=collection + ) @abstractmethod def _get_validation_reference(self, name: str, project: str) -> ValidationReference: @@ -424,6 +492,13 @@ def list_projects( return proto_registry_utils.list_projects(self.cached_registry_proto, tags) return self._list_projects(tags) + def list_feature_view_versions( + self, name: str, project: str + ) -> List[Dict[str, Any]]: + raise NotImplementedError( + "list_feature_view_versions is not implemented for this registry" + ) + def refresh(self, project: Optional[str] = None): try: self.cached_registry_proto = self.proto() @@ -431,32 +506,26 @@ def refresh(self, project: Optional[str] = None): except Exception as e: logger.debug(f"Error while refreshing registry: {e}", exc_info=True) + def is_cache_valid(self) -> bool: + if ( + self.cached_registry_proto is None + or self.cached_registry_proto == RegistryProto() + ): + return False + if ( + not hasattr(self, "cached_registry_proto_created") + or self.cached_registry_proto_created is None + ): + return False + if self.cached_registry_proto_ttl.total_seconds() > 0 and _utc_now() > ( + self.cached_registry_proto_created + self.cached_registry_proto_ttl + ): + return False + return True + def _refresh_cached_registry_if_necessary(self): if self.cache_mode == "sync": - - def is_cache_expired(): - if ( - self.cached_registry_proto is None - or self.cached_registry_proto == RegistryProto() - ): - return True - - # Cache is expired if creation time is None - if ( - not hasattr(self, "cached_registry_proto_created") - or self.cached_registry_proto_created is None - ): - return True - - # Cache is expired if TTL > 0 and current time exceeds creation + TTL - if self.cached_registry_proto_ttl.total_seconds() > 0 and _utc_now() > ( - self.cached_registry_proto_created + self.cached_registry_proto_ttl - ): - return True - - return False - - if is_cache_expired(): + if not self.is_cache_valid(): if not self._refresh_lock.acquire(blocking=False): logger.debug( "Skipping refresh if lock is already held by another thread" diff --git a/sdk/python/feast/infra/registry/file.py b/sdk/python/feast/infra/registry/file.py index 0e5065223b5..4e5a71ff5d9 100644 --- a/sdk/python/feast/infra/registry/file.py +++ b/sdk/python/feast/infra/registry/file.py @@ -1,4 +1,5 @@ import json +import os import uuid from pathlib import Path from typing import Optional @@ -44,6 +45,8 @@ def _write_registry(self, registry_proto: RegistryProto): file_dir.mkdir(exist_ok=True) with open(self._filepath, mode="wb", buffering=0) as f: f.write(registry_proto.SerializeToString()) + f.flush() + os.fsync(f.fileno()) def set_project_metadata(self, project: str, key: str, value: str): """Set a custom project metadata key-value pair in the registry proto (file backend).""" diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py index 26a5b7e1689..2be5e7f2cb6 100644 --- a/sdk/python/feast/infra/registry/proto_registry_utils.py +++ b/sdk/python/feast/infra/registry/proto_registry_utils.py @@ -10,6 +10,7 @@ EntityNotFoundException, FeatureServiceNotFoundException, FeatureViewNotFoundException, + FeatureViewVersionNotFound, PermissionObjectNotFoundException, ProjectObjectNotFoundException, SavedDatasetNotFound, @@ -17,6 +18,7 @@ ) from feast.feature_service import FeatureService from feast.feature_view import FeatureView +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission from feast.project import Project @@ -56,15 +58,19 @@ def wrapper( registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]], + **kwargs, ): nonlocal cache_key, cache_value - key = tuple([id(registry_proto), registry_proto.version_id, project, tags]) + kwargs_key = tuple(sorted(kwargs.items())) if kwargs else () + key = tuple( + [id(registry_proto), registry_proto.version_id, project, tags, kwargs_key] + ) if key == cache_key: return cache_value else: - cache_value = func(registry_proto, project, tags) + cache_value = func(registry_proto, project, tags, **kwargs) cache_key = key return cache_value @@ -144,9 +150,66 @@ def get_any_feature_view( ): return OnDemandFeatureView.from_proto(on_demand_feature_view) + for label_view_proto in registry_proto.label_views: + if ( + label_view_proto.spec.name == name + and label_view_proto.spec.project == project + ): + return LabelView.from_proto(label_view_proto) + raise FeatureViewNotFoundException(name, project) +def get_feature_view_by_version( + registry_proto: RegistryProto, name: str, project: str, version_number: int +) -> BaseFeatureView: + """Retrieve a feature view snapshot for a specific version from version history.""" + from feast.protos.feast.core.FeatureView_pb2 import ( + FeatureView as FeatureViewProto, + ) + from feast.protos.feast.core.LabelView_pb2 import ( + LabelView as LabelViewProto, + ) + from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( + OnDemandFeatureView as OnDemandFeatureViewProto, + ) + from feast.protos.feast.core.StreamFeatureView_pb2 import ( + StreamFeatureView as StreamFeatureViewProto, + ) + from feast.version_utils import version_tag + + for record in registry_proto.feature_view_version_history.records: + if ( + record.feature_view_name == name + and record.project_id == project + and record.version_number == version_number + ): + fv: BaseFeatureView + if record.feature_view_type == "feature_view": + fv_proto = FeatureViewProto.FromString(record.feature_view_proto) + fv = FeatureView.from_proto(fv_proto) + elif record.feature_view_type == "stream_feature_view": + sfv_proto = StreamFeatureViewProto.FromString(record.feature_view_proto) + fv = StreamFeatureView.from_proto(sfv_proto) + elif record.feature_view_type == "on_demand_feature_view": + odfv_proto = OnDemandFeatureViewProto.FromString( + record.feature_view_proto + ) + fv = OnDemandFeatureView.from_proto(odfv_proto) + elif record.feature_view_type == "label_view": + lv_proto = LabelViewProto.FromString(record.feature_view_proto) + fv = LabelView.from_proto(lv_proto) + else: + raise ValueError( + f"Unknown feature view type: {record.feature_view_type}" + ) + + fv.current_version_number = version_number + return fv + + raise FeatureViewVersionNotFound(name, version_tag(version_number), project) + + def get_feature_view( registry_proto: RegistryProto, name: str, project: str ) -> FeatureView: @@ -235,31 +298,43 @@ def list_feature_services( @registry_proto_cache_with_tags def list_all_feature_views( - registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] + registry_proto: RegistryProto, + project: str, + tags: Optional[dict[str, str]], + skip_udf: bool = False, ) -> List[BaseFeatureView]: return ( - list_feature_views(registry_proto, project, tags) - + list_stream_feature_views(registry_proto, project, tags) - + list_on_demand_feature_views(registry_proto, project, tags) + list_feature_views(registry_proto, project, tags, skip_udf=skip_udf) + + list_stream_feature_views(registry_proto, project, tags, skip_udf=skip_udf) + + list_on_demand_feature_views(registry_proto, project, tags, skip_udf=skip_udf) + + list_label_views(registry_proto, project, tags) ) @registry_proto_cache_with_tags def list_feature_views( - registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] + registry_proto: RegistryProto, + project: str, + tags: Optional[dict[str, str]], + skip_udf: bool = False, ) -> List[FeatureView]: feature_views: List[FeatureView] = [] for feature_view_proto in registry_proto.feature_views: if feature_view_proto.spec.project == project and utils.has_all_tags( feature_view_proto.spec.tags, tags ): - feature_views.append(FeatureView.from_proto(feature_view_proto)) + feature_views.append( + FeatureView.from_proto(feature_view_proto, skip_udf=skip_udf) + ) return feature_views @registry_proto_cache_with_tags def list_stream_feature_views( - registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] + registry_proto: RegistryProto, + project: str, + tags: Optional[dict[str, str]], + skip_udf: bool = False, ) -> List[StreamFeatureView]: stream_feature_views = [] for stream_feature_view in registry_proto.stream_feature_views: @@ -267,14 +342,17 @@ def list_stream_feature_views( stream_feature_view.spec.tags, tags ): stream_feature_views.append( - StreamFeatureView.from_proto(stream_feature_view) + StreamFeatureView.from_proto(stream_feature_view, skip_udf=skip_udf) ) return stream_feature_views @registry_proto_cache_with_tags def list_on_demand_feature_views( - registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] + registry_proto: RegistryProto, + project: str, + tags: Optional[dict[str, str]], + skip_udf: bool = False, ) -> List[OnDemandFeatureView]: on_demand_feature_views = [] for on_demand_feature_view in registry_proto.on_demand_feature_views: @@ -282,11 +360,36 @@ def list_on_demand_feature_views( on_demand_feature_view.spec.tags, tags ): on_demand_feature_views.append( - OnDemandFeatureView.from_proto(on_demand_feature_view) + OnDemandFeatureView.from_proto( + on_demand_feature_view, skip_udf=skip_udf + ) ) return on_demand_feature_views +def get_label_view(registry_proto: RegistryProto, name: str, project: str) -> LabelView: + for label_view_proto in registry_proto.label_views: + if ( + label_view_proto.spec.name == name + and label_view_proto.spec.project == project + ): + return LabelView.from_proto(label_view_proto) + raise FeatureViewNotFoundException(name, project) + + +@registry_proto_cache_with_tags +def list_label_views( + registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] +) -> List[LabelView]: + label_views = [] + for label_view_proto in registry_proto.label_views: + if label_view_proto.spec.project == project and utils.has_all_tags( + label_view_proto.spec.tags, tags + ): + label_views.append(LabelView.from_proto(label_view_proto)) + return label_views + + @registry_proto_cache_with_tags def list_entities( registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] @@ -315,14 +418,23 @@ def list_data_sources( @registry_proto_cache_with_tags def list_saved_datasets( - registry_proto: RegistryProto, project: str, tags: Optional[dict[str, str]] + registry_proto: RegistryProto, + project: str, + tags: Optional[dict[str, str]], + namespace: Optional[str] = None, + collection: Optional[str] = None, ) -> List[SavedDataset]: saved_datasets = [] for saved_dataset in registry_proto.saved_datasets: - if saved_dataset.spec.project == project and utils.has_all_tags( - saved_dataset.spec.tags, tags - ): - saved_datasets.append(SavedDataset.from_proto(saved_dataset)) + if saved_dataset.spec.project != project: + continue + if not utils.has_all_tags(saved_dataset.spec.tags, tags): + continue + if namespace is not None and saved_dataset.spec.namespace != namespace: + continue + if collection is not None and saved_dataset.spec.collection != collection: + continue + saved_datasets.append(SavedDataset.from_proto(saved_dataset)) return saved_datasets diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index 9a021744dd1..62f445a5681 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -31,29 +31,39 @@ EntityNotFoundException, FeatureServiceNotFoundException, FeatureViewNotFoundException, + FeatureViewPinConflict, + FeatureViewVersionNotFound, PermissionNotFoundException, ProjectNotFoundException, ProjectObjectNotFoundException, + SavedDatasetNotFound, ValidationReferenceNotFound, ) from feast.feature_service import FeatureService -from feast.feature_view import FeatureView +from feast.feature_view import FeatureView, FeatureViewState from feast.importer import import_class from feast.infra.infra_object import Infra from feast.infra.registry import proto_registry_utils from feast.infra.registry.base_registry import BaseRegistry from feast.infra.registry.registry_store import NoopRegistryStore +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.auth_model import AuthConfig, NoAuthConfig from feast.permissions.permission import Permission from feast.project import Project from feast.project_metadata import ProjectMetadata +from feast.protos.feast.core.FeatureViewVersion_pb2 import FeatureViewVersionRecord from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.repo_config import RegistryConfig from feast.repo_contents import RepoContents from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView -from feast.utils import _utc_now +from feast.utils import _utc_now, to_naive_utc +from feast.version_utils import ( + generate_version_id, + parse_version, + version_tag, +) REGISTRY_SCHEMA_VERSION = "1" @@ -81,6 +91,7 @@ class FeastObjectType(Enum): FEATURE_VIEW = "feature view" ON_DEMAND_FEATURE_VIEW = "on demand feature view" STREAM_FEATURE_VIEW = "stream feature view" + LABEL_VIEW = "label view" FEATURE_SERVICE = "feature service" PERMISSION = "permission" @@ -103,6 +114,7 @@ def get_objects_from_registry( FeastObjectType.STREAM_FEATURE_VIEW: registry.list_stream_feature_views( project=project, ), + FeastObjectType.LABEL_VIEW: registry.list_label_views(project=project), FeastObjectType.FEATURE_SERVICE: registry.list_feature_services( project=project ), @@ -120,6 +132,7 @@ def get_objects_from_repo_contents( FeastObjectType.FEATURE_VIEW: repo_contents.feature_views, FeastObjectType.ON_DEMAND_FEATURE_VIEW: repo_contents.on_demand_feature_views, FeastObjectType.STREAM_FEATURE_VIEW: repo_contents.stream_feature_views, + FeastObjectType.LABEL_VIEW: repo_contents.label_views, FeastObjectType.FEATURE_SERVICE: repo_contents.feature_services, FeastObjectType.PERMISSION: repo_contents.permissions, } @@ -142,6 +155,12 @@ def get_registry_store_class_from_type(registry_store_type: str): def get_registry_store_class_from_scheme(registry_path: str): uri = urlparse(registry_path) + if uri.scheme == "" or ( + len(uri.scheme) == 1 and registry_path[1:3] in (":\\", ":/") + ): + registry_store_type = REGISTRY_STORE_CLASS_FOR_SCHEME["file"] + return get_registry_store_class_from_type(registry_store_type) + if uri.scheme not in REGISTRY_STORE_CLASS_FOR_SCHEME: raise Exception( f"Registry path {registry_path} has unsupported scheme {uri.scheme}. " @@ -221,6 +240,19 @@ def __init__( else False ) + self.enable_online_versioning = ( + registry_config.enable_online_feature_view_versioning + if registry_config is not None + else False + ) + + self.cache_mode = ( + registry_config.cache_mode if registry_config is not None else "sync" + ) + + self._file_mtime = None + self._file_path = None + if registry_config: registry_store_type = registry_config.registry_store_type registry_path = registry_config.path @@ -238,6 +270,13 @@ def __init__( ) ) + from feast.infra.registry.file import FileRegistryStore + + if isinstance(self._registry_store, FileRegistryStore): + self._file_path = self._registry_store._filepath + if self._file_path.exists(): + self._file_mtime = self._file_path.stat().st_mtime + try: registry_proto = self._registry_store.get_registry_proto() self.cached_registry_proto = registry_proto @@ -358,6 +397,12 @@ def list_data_sources( def apply_data_source( self, data_source: DataSource, project: str, commit: bool = True ): + """Apply a data source to the registry with project-scoped deduplication. + + Filters existing data sources by both name and project (fixes feast-dev/feast#6206), + preserving the original created_timestamp if the source already exists in the + target project. + """ now = _utc_now() if not data_source.created_timestamp: data_source.created_timestamp = now @@ -366,7 +411,10 @@ def apply_data_source( registry = self._prepare_registry_for_changes(project) for idx, existing_data_source_proto in enumerate(registry.data_sources): - if existing_data_source_proto.name == data_source.name: + if ( + existing_data_source_proto.name == data_source.name + and existing_data_source_proto.project == project + ): existing_data_source = DataSource.from_proto(existing_data_source_proto) # Check if the data source has actually changed if existing_data_source == data_source: @@ -395,7 +443,7 @@ def delete_data_source(self, name: str, project: str, commit: bool = True): for idx, data_source_proto in enumerate( self.cached_registry_proto.data_sources ): - if data_source_proto.name == name: + if data_source_proto.name == name and data_source_proto.project == project: del self.cached_registry_proto.data_sources[idx] if commit: self.commit() @@ -405,6 +453,7 @@ def delete_data_source(self, name: str, project: str, commit: bool = True): def apply_feature_service( self, feature_service: FeatureService, project: str, commit: bool = True ): + feature_service.prepare_for_apply(self, project, allow_cache=True) now = _utc_now() if not feature_service.created_timestamp: feature_service.created_timestamp = now @@ -456,8 +505,242 @@ def get_entity(self, name: str, project: str, allow_cache: bool = False) -> Enti ) return proto_registry_utils.get_entity(registry_proto, name, project) + def _infer_fv_type_string(self, feature_view) -> str: + if isinstance(feature_view, LabelView): + return "label_view" + elif isinstance(feature_view, StreamFeatureView): + return "stream_feature_view" + elif isinstance(feature_view, FeatureView): + return "feature_view" + elif isinstance(feature_view, OnDemandFeatureView): + return "on_demand_feature_view" + else: + raise ValueError(f"Unexpected feature view type: {type(feature_view)}") + + def _proto_class_for_type(self, fv_type: str): + from feast.protos.feast.core.FeatureView_pb2 import ( + FeatureView as FeatureViewProto, + ) + from feast.protos.feast.core.LabelView_pb2 import ( + LabelView as LabelViewProto, + ) + from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( + OnDemandFeatureView as OnDemandFeatureViewProto, + ) + from feast.protos.feast.core.StreamFeatureView_pb2 import ( + StreamFeatureView as StreamFeatureViewProto, + ) + + if fv_type == "label_view": + return LabelViewProto, LabelView + elif fv_type == "stream_feature_view": + return StreamFeatureViewProto, StreamFeatureView + elif fv_type == "feature_view": + return FeatureViewProto, FeatureView + elif fv_type == "on_demand_feature_view": + return OnDemandFeatureViewProto, OnDemandFeatureView + else: + raise ValueError(f"Unknown feature view type: {fv_type}") + + def _next_version_number(self, name: str, project: str) -> int: + history = self.cached_registry_proto.feature_view_version_history + max_ver = -1 + for record in history.records: + if record.feature_view_name == name and record.project_id == project: + if record.version_number > max_ver: + max_ver = record.version_number + return max_ver + 1 + + def _get_version_record( + self, name: str, project: str, version_number: int + ) -> Optional[FeatureViewVersionRecord]: + history = self.cached_registry_proto.feature_view_version_history + for record in history.records: + if ( + record.feature_view_name == name + and record.project_id == project + and record.version_number == version_number + ): + return record + return None + + def _update_metadata_fields( + self, existing_proto: Any, updated_fv: BaseFeatureView + ) -> None: + """Update non-version-significant fields without creating new version.""" + from feast.feature_view import FeatureView + from feast.labeling.label_view import LabelView + + # Metadata fields + existing_proto.spec.description = updated_fv.description + existing_proto.spec.tags.clear() + existing_proto.spec.tags.update(updated_fv.tags) + existing_proto.spec.owner = updated_fv.owner + if hasattr(existing_proto.spec, "version") and hasattr(updated_fv, "version"): + existing_proto.spec.version = getattr(updated_fv, "version") + + # Configuration fields (FeatureView / LabelView TTL) + if ( + hasattr(existing_proto.spec, "ttl") + and hasattr(updated_fv, "ttl") + and updated_fv.ttl + ): + if isinstance(updated_fv, FeatureView): + ttl_duration = updated_fv.get_ttl_duration() + if ttl_duration: + existing_proto.spec.ttl.CopyFrom(ttl_duration) + elif isinstance(updated_fv, LabelView): + from google.protobuf.duration_pb2 import Duration + + ttl_duration = Duration() + ttl_duration.FromTimedelta(updated_fv.ttl) + existing_proto.spec.ttl.CopyFrom(ttl_duration) + if hasattr(existing_proto.spec, "online") and hasattr(updated_fv, "online"): + existing_proto.spec.online = getattr(updated_fv, "online") + if hasattr(existing_proto.spec, "offline") and hasattr(updated_fv, "offline"): + existing_proto.spec.offline = getattr(updated_fv, "offline") + if hasattr(existing_proto.spec, "enable_validation") and hasattr( + updated_fv, "enable_validation" + ): + existing_proto.spec.enable_validation = getattr( + updated_fv, "enable_validation" + ) + + # Enabled/disabled state + if hasattr(existing_proto.spec, "disabled") and hasattr(updated_fv, "enabled"): + existing_proto.spec.disabled = not getattr(updated_fv, "enabled") + + # Lifecycle state — skip STATE_UNSPECIFIED so that ``feast apply`` + # does not accidentally reset an AVAILABLE_ONLINE view. + if hasattr(existing_proto.meta, "state") and hasattr(updated_fv, "state"): + state_val = getattr(updated_fv, "state") + if ( + isinstance(state_val, FeatureViewState) + and state_val != FeatureViewState.STATE_UNSPECIFIED + ): + existing_proto.meta.state = state_val.to_proto() + + # OnDemandFeatureView configuration + if hasattr(existing_proto.spec, "write_to_online_store") and hasattr( + updated_fv, "write_to_online_store" + ): + existing_proto.spec.write_to_online_store = getattr( + updated_fv, "write_to_online_store" + ) + if hasattr(existing_proto.spec, "singleton") and hasattr( + updated_fv, "singleton" + ): + existing_proto.spec.singleton = getattr(updated_fv, "singleton") + + # LabelView-specific configuration + if hasattr(existing_proto.spec, "labeler_field") and hasattr( + updated_fv, "labeler_field" + ): + existing_proto.spec.labeler_field = updated_fv.labeler_field + if hasattr(existing_proto.spec, "conflict_policy") and hasattr( + updated_fv, "conflict_policy" + ): + existing_proto.spec.conflict_policy = updated_fv.conflict_policy.to_proto() + + if hasattr(existing_proto.spec, "reference_feature_view") and hasattr( + updated_fv, "reference_feature_view" + ): + existing_proto.spec.reference_feature_view = ( + updated_fv.reference_feature_view or "" + ) + + # Data sources (treat as configuration) + if ( + hasattr(existing_proto.spec, "batch_source") + and hasattr(updated_fv, "batch_source") + and updated_fv.batch_source + ): + existing_proto.spec.batch_source.CopyFrom( + updated_fv.batch_source.to_proto() + ) + if ( + hasattr(existing_proto.spec, "stream_source") + and hasattr(updated_fv, "stream_source") + and updated_fv.stream_source + ): + existing_proto.spec.stream_source.CopyFrom( + updated_fv.stream_source.to_proto() + ) + + # Update or clear version pin + if hasattr(existing_proto.meta, "current_version_number") and hasattr( + updated_fv, "current_version_number" + ): + if updated_fv.current_version_number is not None: + existing_proto.meta.current_version_number = ( + updated_fv.current_version_number + ) + else: + # Unpin: reset to proto default (0). + # from_proto interprets cvn=0 with spec.version="latest" as None. + existing_proto.meta.current_version_number = 0 + + # Update timestamp + existing_proto.meta.last_updated_timestamp.FromDatetime(_utc_now()) + + def _save_version_record( + self, + name: str, + project: str, + version_number: int, + fv_type: str, + proto_bytes: bytes, + ): + now = _utc_now() + record = FeatureViewVersionRecord( + feature_view_name=name, + project_id=project, + version_number=version_number, + feature_view_type=fv_type, + feature_view_proto=proto_bytes, + description="", + version_id=generate_version_id(), + ) + record.created_timestamp.FromDatetime(now) + self.cached_registry_proto.feature_view_version_history.records.append(record) + + def list_feature_view_versions( + self, name: str, project: str + ) -> List[Dict[str, Any]]: + history = self.cached_registry_proto.feature_view_version_history + results = [] + for record in history.records: + if record.feature_view_name == name and record.project_id == project: + results.append( + { + "version": version_tag(record.version_number), + "version_number": record.version_number, + "feature_view_type": record.feature_view_type, + "created_timestamp": record.created_timestamp.ToDatetime(), + "version_id": record.version_id, + } + ) + results.sort(key=lambda r: r["version_number"]) + return results + + def get_feature_view_by_version( + self, name: str, project: str, version_number: int, allow_cache: bool = False + ) -> BaseFeatureView: + record = self._get_version_record(name, project, version_number) + if record is None: + raise FeatureViewVersionNotFound(name, version_tag(version_number), project) + proto_class, python_class = self._proto_class_for_type(record.feature_view_type) + snap_proto = proto_class.FromString(record.feature_view_proto) + fv = python_class.from_proto(snap_proto) + fv.current_version_number = version_number + return fv + def apply_feature_view( - self, feature_view: BaseFeatureView, project: str, commit: bool = True + self, + feature_view: BaseFeatureView, + project: str, + commit: bool = True, + no_promote: bool = False, ): feature_view.ensure_valid() @@ -466,14 +749,83 @@ def apply_feature_view( feature_view.created_timestamp = now feature_view.last_updated_timestamp = now + fv_type_str = self._infer_fv_type_string(feature_view) + is_latest, pin_version = parse_version(feature_view.version) + + if not is_latest: + # Explicit version: check if it exists (pin/revert) or not (forward declaration). + # Note: The file registry is last-write-wins for true concurrent races — + # this is a pre-existing limitation for all file registry operations. + # For multi-client environments, use the SQL registry. + record = self._get_version_record(feature_view.name, project, pin_version) + + if record is not None: + # Version exists → pin/revert to that snapshot + # Check that the user hasn't also modified the definition. + # Compare user's FV (with version="latest") against active FV. + self._prepare_registry_for_changes(project) + try: + active_fv = proto_registry_utils.get_any_feature_view( + self.cached_registry_proto, feature_view.name, project + ) + user_fv_copy = feature_view.__copy__() + user_fv_copy.version = "latest" + active_fv.version = "latest" + # Clear metadata that differs due to registry state + user_fv_copy.created_timestamp = active_fv.created_timestamp + user_fv_copy.last_updated_timestamp = ( + active_fv.last_updated_timestamp + ) + user_fv_copy.current_version_number = ( + active_fv.current_version_number + ) + if hasattr(active_fv, "materialization_intervals"): + user_fv_copy.materialization_intervals = ( + active_fv.materialization_intervals + ) + if user_fv_copy != active_fv: + raise FeatureViewPinConflict( + feature_view.name, version_tag(pin_version) + ) + except FeatureViewNotFoundException: + pass + + proto_class, python_class = self._proto_class_for_type( + record.feature_view_type + ) + snap_proto = proto_class.FromString(record.feature_view_proto) + restored_fv = python_class.from_proto(snap_proto) + restored_fv.version = feature_view.version + restored_fv.current_version_number = pin_version + restored_fv.last_updated_timestamp = now + # Apply the restored FV using the standard path below + feature_view = restored_fv + else: + # Version doesn't exist → forward declaration: create it + feature_view.current_version_number = pin_version + feature_view_proto = feature_view.to_proto() + feature_view_proto.spec.project = project + snapshot_proto_bytes = feature_view_proto.SerializeToString() + self._prepare_registry_for_changes(project) + self._save_version_record( + feature_view.name, + project, + pin_version, + fv_type_str, + snapshot_proto_bytes, + ) + # Fall through to apply the FV as active (with current_version_number set) + feature_view_proto = feature_view.to_proto() feature_view_proto.spec.project = project self._prepare_registry_for_changes(project) assert self.cached_registry_proto - self._check_conflicting_feature_view_names(feature_view) + self._check_conflicting_feature_view_names(feature_view, project) existing_feature_views_of_same_type: RepeatedCompositeFieldContainer - if isinstance(feature_view, StreamFeatureView): + if isinstance(feature_view, LabelView): + existing_feature_views_of_same_type = self.cached_registry_proto.label_views + elif isinstance(feature_view, StreamFeatureView): existing_feature_views_of_same_type = ( self.cached_registry_proto.stream_feature_views ) @@ -488,6 +840,7 @@ def apply_feature_view( else: raise ValueError(f"Unexpected feature view type: {type(feature_view)}") + old_proto_bytes = None for idx, existing_feature_view_proto in enumerate( existing_feature_views_of_same_type ): @@ -495,12 +848,22 @@ def apply_feature_view( existing_feature_view_proto.spec.name == feature_view_proto.spec.name and existing_feature_view_proto.spec.project == project ): - if ( - feature_view.__class__.from_proto(existing_feature_view_proto) - == feature_view - ): + existing_feature_view = feature_view.__class__.from_proto( + existing_feature_view_proto + ) + if not feature_view._schema_or_udf_changed(existing_feature_view): + # Update non-version-significant fields in place + self._update_metadata_fields( + existing_feature_view_proto, feature_view + ) + if commit: + self.commit() return else: + old_proto_bytes = existing_feature_view_proto.SerializeToString() + # Save a copy before deletion for no_promote restore + existing_proto_copy = type(existing_feature_view_proto)() + existing_proto_copy.CopyFrom(existing_feature_view_proto) existing_feature_view = type(feature_view).from_proto( existing_feature_view_proto ) @@ -516,6 +879,54 @@ def apply_feature_view( del existing_feature_views_of_same_type[idx] break + # Version history tracking + if is_latest: + if old_proto_bytes is not None: + # FV changed: save old as a version if first time, then save new + next_ver = self._next_version_number(feature_view.name, project) + if next_ver == 0: + self._save_version_record( + feature_view.name, project, 0, fv_type_str, old_proto_bytes + ) + next_ver = 1 + + if no_promote: + # Save version snapshot but keep the old active definition + no_promote_fv = feature_view.__copy__() + no_promote_fv.current_version_number = next_ver + no_promote_proto = no_promote_fv.to_proto() + no_promote_proto.spec.project = project + no_promote_proto_bytes = no_promote_proto.SerializeToString() + self._save_version_record( + feature_view.name, + project, + next_ver, + fv_type_str, + no_promote_proto_bytes, + ) + # Re-insert the old active definition (was deleted above) + existing_feature_views_of_same_type.append(existing_proto_copy) + if commit: + self.commit() + return + + feature_view.current_version_number = next_ver + feature_view_proto = feature_view.to_proto() + feature_view_proto.spec.project = project + new_proto_bytes = feature_view_proto.SerializeToString() + self._save_version_record( + feature_view.name, project, next_ver, fv_type_str, new_proto_bytes + ) + else: + # New FV: save as v0 + feature_view.current_version_number = 0 + feature_view_proto = feature_view.to_proto() + feature_view_proto.spec.project = project + new_proto_bytes = feature_view_proto.SerializeToString() + self._save_version_record( + feature_view.name, project, 0, fv_type_str, new_proto_bytes + ) + existing_feature_views_of_same_type.append(feature_view_proto) if commit: self.commit() @@ -525,12 +936,13 @@ def list_stream_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[StreamFeatureView]: registry_proto = self._get_registry_proto( project=project, allow_cache=allow_cache ) return proto_registry_utils.list_stream_feature_views( - registry_proto, project, tags + registry_proto, project, tags, skip_udf=skip_udf ) def list_on_demand_feature_views( @@ -538,12 +950,13 @@ def list_on_demand_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[OnDemandFeatureView]: registry_proto = self._get_registry_proto( project=project, allow_cache=allow_cache ) return proto_registry_utils.list_on_demand_feature_views( - registry_proto, project, tags + registry_proto, project, tags, skip_udf=skip_udf ) def get_on_demand_feature_view( @@ -556,6 +969,42 @@ def get_on_demand_feature_view( registry_proto, name, project ) + def list_label_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[LabelView]: + registry_proto = self._get_registry_proto( + project=project, allow_cache=allow_cache + ) + return proto_registry_utils.list_label_views(registry_proto, project, tags) + + def get_label_view( + self, name: str, project: str, allow_cache: bool = False + ) -> LabelView: + registry_proto = self._get_registry_proto( + project=project, allow_cache=allow_cache + ) + return proto_registry_utils.get_label_view(registry_proto, name, project) + + def delete_label_view(self, name: str, project: str, commit: bool = True): + self._prepare_registry_for_changes(project) + assert self.cached_registry_proto + + for idx, existing_label_view_proto in enumerate( + self.cached_registry_proto.label_views + ): + if ( + existing_label_view_proto.spec.name == name + and existing_label_view_proto.spec.project == project + ): + del self.cached_registry_proto.label_views[idx] + if commit: + self.commit() + return + raise FeatureViewNotFoundException(name, project) + def get_data_source( self, name: str, project: str, allow_cache: bool = False ) -> DataSource: @@ -566,12 +1015,18 @@ def get_data_source( def apply_materialization( self, - feature_view: Union[FeatureView, OnDemandFeatureView], + feature_view: Union[FeatureView, OnDemandFeatureView, LabelView], project: str, start_date: datetime, end_date: datetime, commit: bool = True, ): + if isinstance(feature_view, LabelView): + raise ValueError( + f"Cannot apply materialization for LabelView {feature_view.name}. " + f"Use FeatureStore.push() to write labels." + ) + self._prepare_registry_for_changes(project) assert self.cached_registry_proto @@ -589,6 +1044,9 @@ def apply_materialization( (start_date, end_date) ) existing_feature_view.last_updated_timestamp = _utc_now() + # Transition state to AVAILABLE_ONLINE after materialization. + if hasattr(existing_feature_view, "state"): + existing_feature_view.state = FeatureViewState.AVAILABLE_ONLINE feature_view_proto = existing_feature_view.to_proto() feature_view_proto.spec.project = project del self.cached_registry_proto.feature_views[idx] @@ -611,6 +1069,11 @@ def apply_materialization( (start_date, end_date) ) existing_stream_feature_view.last_updated_timestamp = _utc_now() + # Transition state to AVAILABLE_ONLINE after materialization. + if hasattr(existing_stream_feature_view, "state"): + existing_stream_feature_view.state = ( + FeatureViewState.AVAILABLE_ONLINE + ) stream_feature_view_proto = existing_stream_feature_view.to_proto() stream_feature_view_proto.spec.project = project del self.cached_registry_proto.stream_feature_views[idx] @@ -626,13 +1089,25 @@ def list_all_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, + updated_since: Optional[datetime] = None, ) -> List[BaseFeatureView]: registry_proto = self._get_registry_proto( project=project, allow_cache=allow_cache ) - return proto_registry_utils.list_all_feature_views( - registry_proto, project, tags + feature_views = proto_registry_utils.list_all_feature_views( + registry_proto, project, tags, skip_udf=skip_udf ) + if updated_since is not None: + # last_updated_timestamp from proto is offset-naive UTC; normalise for comparison + cutoff = to_naive_utc(updated_since) + feature_views = [ + fv + for fv in feature_views + if fv.last_updated_timestamp is not None + and fv.last_updated_timestamp >= cutoff + ] + return feature_views def get_any_feature_view( self, name: str, project: str, allow_cache: bool = False @@ -647,11 +1122,14 @@ def list_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[FeatureView]: registry_proto = self._get_registry_proto( project=project, allow_cache=allow_cache ) - return proto_registry_utils.list_feature_views(registry_proto, project, tags) + return proto_registry_utils.list_feature_views( + registry_proto, project, tags, skip_udf=skip_udf + ) def get_feature_view( self, name: str, project: str, allow_cache: bool = False @@ -692,6 +1170,7 @@ def delete_feature_view(self, name: str, project: str, commit: bool = True): self._prepare_registry_for_changes(project) assert self.cached_registry_proto + found = False for idx, existing_feature_view_proto in enumerate( self.cached_registry_proto.feature_views ): @@ -700,35 +1179,60 @@ def delete_feature_view(self, name: str, project: str, commit: bool = True): and existing_feature_view_proto.spec.project == project ): del self.cached_registry_proto.feature_views[idx] - if commit: - self.commit() - return + found = True + break - for idx, existing_on_demand_feature_view_proto in enumerate( - self.cached_registry_proto.on_demand_feature_views - ): - if ( - existing_on_demand_feature_view_proto.spec.name == name - and existing_on_demand_feature_view_proto.spec.project == project + if not found: + for idx, existing_on_demand_feature_view_proto in enumerate( + self.cached_registry_proto.on_demand_feature_views ): - del self.cached_registry_proto.on_demand_feature_views[idx] - if commit: - self.commit() - return + if ( + existing_on_demand_feature_view_proto.spec.name == name + and existing_on_demand_feature_view_proto.spec.project == project + ): + del self.cached_registry_proto.on_demand_feature_views[idx] + found = True + break - for idx, existing_stream_feature_view_proto in enumerate( - self.cached_registry_proto.stream_feature_views - ): - if ( - existing_stream_feature_view_proto.spec.name == name - and existing_stream_feature_view_proto.spec.project == project + if not found: + for idx, existing_stream_feature_view_proto in enumerate( + self.cached_registry_proto.stream_feature_views ): - del self.cached_registry_proto.stream_feature_views[idx] - if commit: - self.commit() - return + if ( + existing_stream_feature_view_proto.spec.name == name + and existing_stream_feature_view_proto.spec.project == project + ): + del self.cached_registry_proto.stream_feature_views[idx] + found = True + break - raise FeatureViewNotFoundException(name, project) + if not found: + for idx, existing_label_view_proto in enumerate( + self.cached_registry_proto.label_views + ): + if ( + existing_label_view_proto.spec.name == name + and existing_label_view_proto.spec.project == project + ): + del self.cached_registry_proto.label_views[idx] + found = True + break + + if not found: + raise FeatureViewNotFoundException(name, project) + + # Clean up version history for the deleted feature view + history = self.cached_registry_proto.feature_view_version_history + indices_to_remove = [ + i + for i, record in enumerate(history.records) + if record.feature_view_name == name and record.project_id == project + ] + for i in reversed(indices_to_remove): + del history.records[i] + + if commit: + self.commit() def delete_entity(self, name: str, project: str, commit: bool = True): self._prepare_registry_for_changes(project) @@ -802,11 +1306,36 @@ def list_saved_datasets( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + namespace: Optional[str] = None, + collection: Optional[str] = None, ) -> List[SavedDataset]: registry_proto = self._get_registry_proto( project=project, allow_cache=allow_cache ) - return proto_registry_utils.list_saved_datasets(registry_proto, project, tags) + return proto_registry_utils.list_saved_datasets( + registry_proto, + project, + tags, + namespace=namespace, + collection=collection, + ) + + def delete_saved_dataset(self, name: str, project: str, commit: bool = True): + self._prepare_registry_for_changes(project) + assert self.cached_registry_proto + + for idx, existing_saved_dataset_proto in enumerate( + self.cached_registry_proto.saved_datasets + ): + if ( + existing_saved_dataset_proto.spec.name == name + and existing_saved_dataset_proto.spec.project == project + ): + del self.cached_registry_proto.saved_datasets[idx] + if commit: + self.commit() + return + raise SavedDatasetNotFound(name, project=project) def apply_validation_reference( self, @@ -883,11 +1412,27 @@ def commit(self): """Commits the state of the registry cache to the remote registry store.""" if self.cached_registry_proto: self._registry_store.update_registry_proto(self.cached_registry_proto) + if self._file_path is not None and self._file_path.exists(): + try: + self._file_mtime = self._file_path.stat().st_mtime + except (OSError, FileNotFoundError): + pass def refresh(self, project: Optional[str] = None): """Refreshes the state of the registry cache by fetching the registry state from the remote registry store.""" self._get_registry_proto(project=project, allow_cache=False) + def is_cache_valid(self) -> bool: + if self.cached_registry_proto_created is None: + return False + if ( + self.cached_registry_proto_ttl.total_seconds() > 0 + and _utc_now() + > self.cached_registry_proto_created + self.cached_registry_proto_ttl + ): + return False + return True + def teardown(self): """Tears down (removes) the registry.""" self._registry_store.teardown() @@ -939,6 +1484,23 @@ def _get_registry_proto( Returns: Returns a RegistryProto object which represents the state of the registry """ with self._refresh_lock: + # For file-based registries in sync mode, check file modification time + # to detect changes immediately, not just based on TTL + file_modified = False + if ( + allow_cache + and self.cache_mode == "sync" + and self._file_path is not None + and self._file_path.exists() + ): + try: + current_mtime = self._file_path.stat().st_mtime + if self._file_mtime is None or current_mtime > self._file_mtime: + file_modified = True + self._file_mtime = current_mtime + except (OSError, FileNotFoundError): + file_modified = True + expired = (self.cached_registry_proto_created is None) or ( self.cached_registry_proto_ttl.total_seconds() > 0 # 0 ttl means infinity @@ -951,33 +1513,56 @@ def _get_registry_proto( ) ) - if allow_cache and not expired: + # Refresh if expired or file was modified (for sync mode with file registry) + if allow_cache and not expired and not file_modified: return self.cached_registry_proto - logger.info("Registry cache expired, so refreshing") + + if file_modified: + logger.info("Registry file modified, so refreshing") + else: + logger.info("Registry cache expired, so refreshing") + registry_proto = self._registry_store.get_registry_proto() self.cached_registry_proto = registry_proto self.cached_registry_proto_created = _utc_now() + + if self._file_path is not None and self._file_path.exists(): + try: + self._file_mtime = self._file_path.stat().st_mtime + except (OSError, FileNotFoundError): + pass + return registry_proto - def _check_conflicting_feature_view_names(self, feature_view: BaseFeatureView): - name_to_fv_protos = self._existing_feature_view_names_to_fvs() + def _check_conflicting_feature_view_names( + self, feature_view: BaseFeatureView, project: str + ): + name_to_fv_protos = self._existing_feature_view_names_to_fvs(project) if feature_view.name in name_to_fv_protos: if not isinstance( name_to_fv_protos.get(feature_view.name), feature_view.proto_class ): raise ConflictingFeatureViewNames(feature_view.name) - def _existing_feature_view_names_to_fvs(self) -> Dict[str, Message]: + def _existing_feature_view_names_to_fvs(self, project: str) -> Dict[str, Message]: assert self.cached_registry_proto odfvs = { fv.spec.name: fv for fv in self.cached_registry_proto.on_demand_feature_views + if fv.spec.project == project + } + fvs = { + fv.spec.name: fv + for fv in self.cached_registry_proto.feature_views + if fv.spec.project == project } - fvs = {fv.spec.name: fv for fv in self.cached_registry_proto.feature_views} sfv = { - fv.spec.name: fv for fv in self.cached_registry_proto.stream_feature_views + fv.spec.name: fv + for fv in self.cached_registry_proto.stream_feature_views + if fv.spec.project == project } - return {**odfvs, **fvs, **sfv} + lvs = {lv.spec.name: lv for lv in self.cached_registry_proto.label_views} + return {**odfvs, **fvs, **sfv, **lvs} def get_permission( self, name: str, project: str, allow_cache: bool = False @@ -1110,6 +1695,9 @@ def delete_project( for feature_view in list_feature_views: self.delete_feature_view(feature_view.name, name) + for lv in self.list_label_views(name): + self.delete_label_view(lv.name, name) + list_data_sources = self.list_data_sources(name) for data_source in list_data_sources: self.delete_data_source(data_source.name, name) diff --git a/sdk/python/feast/infra/registry/remote.py b/sdk/python/feast/infra/registry/remote.py index 8fc0db55c27..43550a49882 100644 --- a/sdk/python/feast/infra/registry/remote.py +++ b/sdk/python/feast/infra/registry/remote.py @@ -17,6 +17,7 @@ from feast.feature_view import FeatureView from feast.infra.infra_object import Infra from feast.infra.registry.base_registry import BaseRegistry +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.auth_model import AuthConfig, NoAuthConfig from feast.permissions.client.grpc_client_auth_interceptor import ( @@ -35,7 +36,10 @@ def extract_base_feature_view( any_feature_view: RegistryServer_pb2.AnyFeatureView, ) -> BaseFeatureView: + from feast.labeling.label_view import LabelView + feature_view_type = any_feature_view.WhichOneof("any_feature_view") + feature_view: BaseFeatureView if feature_view_type == "feature_view": feature_view = FeatureView.from_proto(any_feature_view.feature_view) elif feature_view_type == "on_demand_feature_view": @@ -46,6 +50,12 @@ def extract_base_feature_view( feature_view = StreamFeatureView.from_proto( any_feature_view.stream_feature_view ) + elif feature_view_type == "label_view": + feature_view = LabelView.from_proto(any_feature_view.label_view) + else: + raise ValueError( + f"Unexpected feature view type in AnyFeatureView: {feature_view_type}" + ) return feature_view @@ -64,10 +74,25 @@ class RemoteRegistryConfig(RegistryConfig): is_tls: bool = False """ bool: Set to `True` if you plan to connect to a registry server running in TLS (SSL) mode. - If you intend to add the public certificate to the trust store instead of passing it via the `cert` parameter, this field must be set to `True`. - If you are planning to add the public certificate as part of the trust store instead of passing it as a `cert` parameters then setting this field to `true` is mandatory. + If you are planning to add the public certificate as part of the trust store instead of passing it as a `cert` parameters then setting this field to `True` is mandatory. """ + client_cert: StrictStr = "" + """ str: Path to the client certificate for mTLS (mutual TLS) authentication. + Required when connecting to a server that enforces mutual TLS. + Must be provided together with `client_key`. + Typically this file ends with `*.crt` or `*.pem`. """ + + client_key: StrictStr = "" + """ str: Path to the client private key for mTLS (mutual TLS) authentication. + Must be provided together with `client_cert`. + Typically this file ends with `*.key` or `*.pem`. """ + + authority: StrictStr = "" + """ str: Override the gRPC :authority header for TLS connections. + Required when the connection address differs from the service hostname, + e.g. when connecting through a tunnel or proxy for local development. """ + class RemoteRegistry(BaseRegistry): def __init__( @@ -89,19 +114,45 @@ def __init__( def _create_grpc_channel(self, registry_config): assert isinstance(registry_config, RemoteRegistryConfig) if registry_config.cert or registry_config.is_tls: - cafile = os.getenv("SSL_CERT_FILE") or os.getenv("REQUESTS_CA_BUNDLE") - if not cafile and not registry_config.cert: + cafile = ( + registry_config.cert + or os.getenv("SSL_CERT_FILE") + or os.getenv("REQUESTS_CA_BUNDLE") + ) + if not cafile: raise EnvironmentError( "SSL_CERT_FILE or REQUESTS_CA_BUNDLE environment variable must be set to use secure TLS or set the cert parameter in feature_Store.yaml file under remote registry configuration." ) - with open( - registry_config.cert if registry_config.cert else cafile, "rb" - ) as cert_file: + if (registry_config.client_cert and not registry_config.client_key) or ( + not registry_config.client_cert and registry_config.client_key + ): + raise ValueError( + "Both client_cert and client_key must be provided for mTLS. " + "Only one was set in the remote registry configuration." + ) + + with open(cafile, "rb") as cert_file: trusted_certs = cert_file.read() + private_key: Optional[bytes] = None + certificate_chain: Optional[bytes] = None + if registry_config.client_cert and registry_config.client_key: + with open(registry_config.client_key, "rb") as key_file: + private_key = key_file.read() + with open(registry_config.client_cert, "rb") as cert_file: + certificate_chain = cert_file.read() tls_credentials = grpc.ssl_channel_credentials( - root_certificates=trusted_certs + root_certificates=trusted_certs, + private_key=private_key, + certificate_chain=certificate_chain, + ) + + options = [] + if registry_config.authority: + options.append(("grpc.default_authority", registry_config.authority)) + + return grpc.secure_channel( + registry_config.path, tls_credentials, options=options ) - return grpc.secure_channel(registry_config.path, tls_credentials) else: # Create an insecure gRPC channel return grpc.insecure_channel(registry_config.path) @@ -217,14 +268,22 @@ def list_feature_services( ] def apply_feature_view( - self, feature_view: BaseFeatureView, project: str, commit: bool = True + self, + feature_view: BaseFeatureView, + project: str, + commit: bool = True, + no_promote: bool = False, ): - if isinstance(feature_view, StreamFeatureView): + if isinstance(feature_view, LabelView): + arg_name = "label_view" + elif isinstance(feature_view, StreamFeatureView): arg_name = "stream_feature_view" elif isinstance(feature_view, FeatureView): arg_name = "feature_view" elif isinstance(feature_view, OnDemandFeatureView): arg_name = "on_demand_feature_view" + else: + raise ValueError(f"Unexpected feature view type: {type(feature_view)}") request = RegistryServer_pb2.ApplyFeatureViewRequest( feature_view=( @@ -238,6 +297,7 @@ def apply_feature_view( if arg_name == "on_demand_feature_view" else None ), + label_view=(feature_view.to_proto() if arg_name == "label_view" else None), project=project, commit=commit, ) @@ -264,6 +324,7 @@ def list_stream_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[StreamFeatureView]: request = RegistryServer_pb2.ListStreamFeatureViewsRequest( project=project, allow_cache=allow_cache, tags=tags @@ -288,6 +349,7 @@ def list_on_demand_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[OnDemandFeatureView]: request = RegistryServer_pb2.ListOnDemandFeatureViewsRequest( project=project, allow_cache=allow_cache, tags=tags @@ -298,6 +360,30 @@ def list_on_demand_feature_views( for on_demand_feature_view in response.on_demand_feature_views ] + def get_label_view( + self, name: str, project: str, allow_cache: bool = False + ) -> LabelView: + request = RegistryServer_pb2.GetLabelViewRequest( + name=name, project=project, allow_cache=allow_cache + ) + response = self.stub.GetLabelView(request) + return LabelView.from_proto(response) + + def list_label_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[LabelView]: + request = RegistryServer_pb2.ListLabelViewsRequest( + project=project, allow_cache=allow_cache, tags=tags + ) + response = self.stub.ListLabelViews(request) + return [LabelView.from_proto(label_view) for label_view in response.label_views] + + def delete_label_view(self, name: str, project: str, commit: bool = True): + self.delete_feature_view(name, project, commit) + def get_any_feature_view( self, name: str, project: str, allow_cache: bool = False ) -> BaseFeatureView: @@ -316,9 +402,19 @@ def list_all_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, + updated_since: Optional[datetime] = None, ) -> List[BaseFeatureView]: + updated_since_proto = None + if updated_since is not None: + ts = Timestamp() + ts.FromDatetime(updated_since) + updated_since_proto = ts request = RegistryServer_pb2.ListAllFeatureViewsRequest( - project=project, allow_cache=allow_cache, tags=tags + project=project, + allow_cache=allow_cache, + tags=tags, + updated_since=updated_since_proto, ) response: RegistryServer_pb2.ListAllFeatureViewsResponse = ( @@ -343,6 +439,7 @@ def list_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[FeatureView]: request = RegistryServer_pb2.ListFeatureViewsRequest( project=project, allow_cache=allow_cache, tags=tags @@ -356,12 +453,18 @@ def list_feature_views( def apply_materialization( self, - feature_view: Union[FeatureView, OnDemandFeatureView], + feature_view: Union[FeatureView, OnDemandFeatureView, LabelView], project: str, start_date: datetime, end_date: datetime, commit: bool = True, ): + if isinstance(feature_view, LabelView): + raise ValueError( + f"Cannot apply materialization for LabelView {feature_view.name}. " + f"Use FeatureStore.push() to write labels." + ) + start_date_timestamp = Timestamp() end_date_timestamp = Timestamp() start_date_timestamp.FromDatetime(start_date) @@ -408,9 +511,15 @@ def list_saved_datasets( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + namespace: Optional[str] = None, + collection: Optional[str] = None, ) -> List[SavedDataset]: request = RegistryServer_pb2.ListSavedDatasetsRequest( - project=project, allow_cache=allow_cache, tags=tags + project=project, + allow_cache=allow_cache, + tags=tags, + namespace=namespace or "", + collection=collection or "", ) response = self.stub.ListSavedDatasets(request) return [ diff --git a/sdk/python/feast/infra/registry/snowflake.py b/sdk/python/feast/infra/registry/snowflake.py index 12299572f04..fc75db20e9c 100644 --- a/sdk/python/feast/infra/registry/snowflake.py +++ b/sdk/python/feast/infra/registry/snowflake.py @@ -33,6 +33,7 @@ GetSnowflakeConnection, execute_snowflake_statement, ) +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission from feast.project import Project @@ -44,6 +45,7 @@ ) from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto from feast.protos.feast.core.InfraObject_pb2 import Infra as InfraProto +from feast.protos.feast.core.LabelView_pb2 import LabelView as LabelViewProto from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( OnDemandFeatureView as OnDemandFeatureViewProto, ) @@ -60,7 +62,7 @@ from feast.repo_config import RegistryConfig from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView -from feast.utils import _utc_now, has_all_tags +from feast.utils import _utc_now, has_all_tags, to_naive_utc logger = logging.getLogger(__name__) @@ -139,20 +141,21 @@ def __init__( with GetSnowflakeConnection(self.registry_config) as conn: sql_function_file = f"{os.path.dirname(feast.__file__)}/infra/utils/snowflake/registry/snowflake_table_creation.sql" with open(sql_function_file, "r") as file: - sql_file = file.read() - sql_cmds = sql_file.split(";") + sql_cmds = [ + cmd.strip() for cmd in file.read().split(";") if cmd.strip() + ] for command in sql_cmds: query = command.replace("REGISTRY_PATH", f"{self.registry_path}") execute_snowflake_statement(conn, query) self.purge_feast_metadata = registry_config.purge_feast_metadata - self._sync_feast_metadata_to_projects_table() - if not self.purge_feast_metadata: - self._maybe_init_project_metadata(project) + self.project = project - self.cached_registry_proto = self.proto() - self.cached_registry_proto_created = _utc_now() + # Initialize cache state before any method that may trigger + # _refresh_cached_registry_if_necessary (e.g. proto(), get_project()). self._refresh_lock = Lock() + self.cached_registry_proto = None + self.cached_registry_proto_created = None self.cached_registry_proto_ttl = timedelta( seconds=( registry_config.cache_ttl_seconds @@ -160,11 +163,17 @@ def __init__( else 0 ) ) - self.project = project + + self._sync_feast_metadata_to_projects_table() + if not self.purge_feast_metadata: + self._maybe_init_project_metadata(project) + + self.cached_registry_proto = self.proto() + self.cached_registry_proto_created = _utc_now() def _sync_feast_metadata_to_projects_table(self): - feast_metadata_projects: set = [] - projects_set: set = [] + feast_metadata_projects: set[str] = set() + projects_set: set[str] = set() with GetSnowflakeConnection(self.registry_config) as conn: query = ( @@ -184,7 +193,7 @@ def _sync_feast_metadata_to_projects_table(self): projects_set.add(row[1]["PROJECT_ID"]) # Find object in feast_metadata_projects but not in projects - projects_to_sync = set(feast_metadata_projects) - set(projects_set) + projects_to_sync = feast_metadata_projects - projects_set for project_name in projects_to_sync: self.apply_project(Project(name=project_name), commit=True) @@ -199,7 +208,7 @@ def refresh(self, project: Optional[str] = None): self.cached_registry_proto = self.proto() self.cached_registry_proto_created = _utc_now() - def _refresh_cached_registry_if_necessary(self): + def _refresh_cached_registry_if_necessary(self) -> RegistryProto: with self._refresh_lock: expired = ( self.cached_registry_proto is None @@ -220,13 +229,18 @@ def _refresh_cached_registry_if_necessary(self): logger.info("Registry cache expired, so refreshing") self.refresh() + if self.cached_registry_proto is None: + raise RuntimeError("Registry cache is unexpectedly empty after refresh") + return self.cached_registry_proto + def teardown(self): with GetSnowflakeConnection(self.registry_config) as conn: sql_function_file = f"{os.path.dirname(feast.__file__)}/infra/utils/snowflake/registry/snowflake_table_deletion.sql" with open(sql_function_file, "r") as file: - sqlFile = file.read() - sqlCommands = sqlFile.split(";") - for command in sqlCommands: + sql_cmds = [ + cmd.strip() for cmd in file.read().split(";") if cmd.strip() + ] + for command in sql_cmds: query = command.replace("REGISTRY_PATH", f"{self.registry_path}") execute_snowflake_statement(conn, query) @@ -250,6 +264,7 @@ def apply_entity(self, entity: Entity, project: str, commit: bool = True): def apply_feature_service( self, feature_service: FeatureService, project: str, commit: bool = True ): + feature_service.prepare_for_apply(self, project, allow_cache=True) return self._apply_object( "FEATURE_SERVICES", project, @@ -259,8 +274,18 @@ def apply_feature_service( ) def apply_feature_view( - self, feature_view: BaseFeatureView, project: str, commit: bool = True + self, + feature_view: BaseFeatureView, + project: str, + commit: bool = True, + no_promote: bool = False, ): + if no_promote: + raise NotImplementedError( + "Feature view versioning (no_promote) is not supported by the Snowflake registry. " + "Use the SQL registry or file registry for versioning support." + ) + feature_view.ensure_valid() fv_table_str = self._infer_fv_table(feature_view) fv_column_name = fv_table_str[:-1] return self._apply_object( @@ -363,7 +388,8 @@ def _apply_object( {proto_field_name} = TO_BINARY({proto}), last_updated_timestamp = CURRENT_TIMESTAMP() WHERE - {id_field_name.lower()} = '{name}' + project_id = '{project}' + AND {id_field_name.lower()} = '{name}' """ execute_snowflake_statement(conn, query) @@ -382,7 +408,7 @@ def _apply_object( VALUES ('{name}', '{project}', CURRENT_TIMESTAMP(), TO_BINARY({proto}), '', '') """ - elif "_FEATURE_VIEWS" in table: + elif "_FEATURE_VIEWS" in table or table == "LABEL_VIEWS": query = f""" INSERT INTO {self.registry_path}."{table}" VALUES @@ -439,17 +465,16 @@ def delete_feature_service(self, name: str, project: str, commit: bool = True): FeatureServiceNotFoundException, ) - # can you have featureviews with the same name def delete_feature_view(self, name: str, project: str, commit: bool = True): deleted_count = 0 - for table in { - "FEATURE_VIEWS", - "ON_DEMAND_FEATURE_VIEWS", - "STREAM_FEATURE_VIEWS", - }: - deleted_count += self._delete_object( - table, name, project, "FEATURE_VIEW_NAME", None - ) + _FV_TABLE_ID_COLUMNS = { + "FEATURE_VIEWS": "FEATURE_VIEW_NAME", + "ON_DEMAND_FEATURE_VIEWS": "FEATURE_VIEW_NAME", + "STREAM_FEATURE_VIEWS": "FEATURE_VIEW_NAME", + "LABEL_VIEWS": "LABEL_VIEW_NAME", + } + for table, id_col in _FV_TABLE_ID_COLUMNS.items(): + deleted_count += self._delete_object(table, name, project, id_col, None) if deleted_count == 0: raise FeatureViewNotFoundException(name, project) @@ -508,10 +533,8 @@ def get_data_source( self, name: str, project: str, allow_cache: bool = False ) -> DataSource: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.get_data_source( - self.cached_registry_proto, name, project - ) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_data_source(registry_proto, name, project) return self._get_object( "DATA_SOURCES", name, @@ -525,10 +548,8 @@ def get_data_source( def get_entity(self, name: str, project: str, allow_cache: bool = False) -> Entity: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.get_entity( - self.cached_registry_proto, name, project - ) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_entity(registry_proto, name, project) return self._get_object( "ENTITIES", name, @@ -544,9 +565,9 @@ def get_feature_service( self, name: str, project: str, allow_cache: bool = False ) -> FeatureService: if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.get_feature_service( - self.cached_registry_proto, name, project + registry_proto, name, project ) return self._get_object( "FEATURE_SERVICES", @@ -563,10 +584,8 @@ def get_feature_view( self, name: str, project: str, allow_cache: bool = False ) -> FeatureView: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.get_feature_view( - self.cached_registry_proto, name, project - ) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_feature_view(registry_proto, name, project) return self._get_object( "FEATURE_VIEWS", name, @@ -582,9 +601,9 @@ def get_any_feature_view( self, name: str, project: str, allow_cache: bool = False ) -> BaseFeatureView: if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.get_any_feature_view( - self.cached_registry_proto, name, project + registry_proto, name, project ) fv = self._get_object( "FEATURE_VIEWS", @@ -617,6 +636,17 @@ def get_any_feature_view( OnDemandFeatureView, "ON_DEMAND_FEATURE_VIEW_NAME", "ON_DEMAND_FEATURE_VIEW_PROTO", + None, + ) + if not fv: + fv = self._get_object( + "LABEL_VIEWS", + name, + project, + LabelViewProto, + LabelView, + "LABEL_VIEW_NAME", + "LABEL_VIEW_PROTO", FeatureViewNotFoundException, ) return fv @@ -626,28 +656,58 @@ def list_all_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, + updated_since: Optional[datetime] = None, ) -> List[BaseFeatureView]: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.list_all_feature_views( - self.cached_registry_proto, project, tags + registry_proto = self._refresh_cached_registry_if_necessary() + feature_views = proto_registry_utils.list_all_feature_views( + registry_proto, project, tags, skip_udf=skip_udf ) - - return ( + if updated_since is not None: + cutoff = to_naive_utc(updated_since) + feature_views = [ + fv + for fv in feature_views + if fv.last_updated_timestamp is not None + and fv.last_updated_timestamp >= cutoff + ] + return feature_views + + feature_views = ( cast( list[BaseFeatureView], - self.list_feature_views(project, allow_cache, tags), + self.list_feature_views(project, allow_cache, tags, skip_udf=skip_udf), ) + cast( list[BaseFeatureView], - self.list_stream_feature_views(project, allow_cache, tags), + self.list_stream_feature_views( + project, allow_cache, tags, skip_udf=skip_udf + ), ) + cast( list[BaseFeatureView], - self.list_on_demand_feature_views(project, allow_cache, tags), + self.list_on_demand_feature_views( + project, allow_cache, tags, skip_udf=skip_udf + ), + ) + + cast( + list[BaseFeatureView], + self.list_label_views(project, allow_cache, tags), ) ) + if updated_since is not None: + cutoff = to_naive_utc(updated_since) + feature_views = [ + fv + for fv in feature_views + if fv.last_updated_timestamp is not None + and fv.last_updated_timestamp >= cutoff + ] + + return feature_views + def get_infra(self, project: str, allow_cache: bool = False) -> Infra: infra_object = self._get_object( "MANAGED_INFRA", @@ -666,9 +726,9 @@ def get_on_demand_feature_view( self, name: str, project: str, allow_cache: bool = False ) -> OnDemandFeatureView: if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.get_on_demand_feature_view( - self.cached_registry_proto, name, project + registry_proto, name, project ) return self._get_object( "ON_DEMAND_FEATURE_VIEWS", @@ -685,10 +745,8 @@ def get_saved_dataset( self, name: str, project: str, allow_cache: bool = False ) -> SavedDataset: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.get_saved_dataset( - self.cached_registry_proto, name, project - ) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_saved_dataset(registry_proto, name, project) return self._get_object( "SAVED_DATASETS", name, @@ -704,9 +762,9 @@ def get_stream_feature_view( self, name: str, project: str, allow_cache: bool = False ): if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.get_stream_feature_view( - self.cached_registry_proto, name, project + registry_proto, name, project ) return self._get_object( "STREAM_FEATURE_VIEWS", @@ -723,9 +781,9 @@ def get_validation_reference( self, name: str, project: str, allow_cache: bool = False ) -> ValidationReference: if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.get_validation_reference( - self.cached_registry_proto, name, project + registry_proto, name, project ) return self._get_object( "VALIDATION_REFERENCES", @@ -774,10 +832,8 @@ def get_permission( self, name: str, project: str, allow_cache: bool = False ) -> Permission: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.get_permission( - self.cached_registry_proto, name, project - ) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_permission(registry_proto, name, project) return self._get_object( "PERMISSIONS", name, @@ -797,10 +853,8 @@ def list_data_sources( tags: Optional[dict[str, str]] = None, ) -> List[DataSource]: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.list_data_sources( - self.cached_registry_proto, project, tags - ) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_data_sources(registry_proto, project, tags) return self._list_objects( "DATA_SOURCES", project, @@ -817,10 +871,8 @@ def list_entities( tags: Optional[dict[str, str]] = None, ) -> List[Entity]: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.list_entities( - self.cached_registry_proto, project, tags - ) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_entities(registry_proto, project, tags) return self._list_objects( "ENTITIES", project, EntityProto, Entity, "ENTITY_PROTO", tags=tags ) @@ -832,9 +884,9 @@ def list_feature_services( tags: Optional[dict[str, str]] = None, ) -> List[FeatureService]: if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_feature_services( - self.cached_registry_proto, project, tags + registry_proto, project, tags ) return self._list_objects( "FEATURE_SERVICES", @@ -850,11 +902,12 @@ def list_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[FeatureView]: if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_feature_views( - self.cached_registry_proto, project, tags + registry_proto, project, tags, skip_udf=skip_udf ) return self._list_objects( "FEATURE_VIEWS", @@ -863,6 +916,7 @@ def list_feature_views( FeatureView, "FEATURE_VIEW_PROTO", tags=tags, + skip_udf=skip_udf, ) def list_on_demand_feature_views( @@ -870,11 +924,12 @@ def list_on_demand_feature_views( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[OnDemandFeatureView]: if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_on_demand_feature_views( - self.cached_registry_proto, project, tags + registry_proto, project, tags, skip_udf=skip_udf ) return self._list_objects( "ON_DEMAND_FEATURE_VIEWS", @@ -883,6 +938,7 @@ def list_on_demand_feature_views( OnDemandFeatureView, "ON_DEMAND_FEATURE_VIEW_PROTO", tags=tags, + skip_udf=skip_udf, ) def list_saved_datasets( @@ -890,13 +946,19 @@ def list_saved_datasets( project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + namespace: Optional[str] = None, + collection: Optional[str] = None, ) -> List[SavedDataset]: if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_saved_datasets( - self.cached_registry_proto, project, tags + registry_proto, + project, + tags, + namespace=namespace, + collection=collection, ) - return self._list_objects( + results = self._list_objects( "SAVED_DATASETS", project, SavedDatasetProto, @@ -904,17 +966,23 @@ def list_saved_datasets( "SAVED_DATASET_PROTO", tags=tags, ) + if namespace is not None: + results = [sd for sd in results if sd.namespace == namespace] + if collection is not None: + results = [sd for sd in results if sd.collection == collection] + return results def list_stream_feature_views( self, project: str, allow_cache: bool = False, tags: Optional[dict[str, str]] = None, + skip_udf: bool = False, ) -> List[StreamFeatureView]: if allow_cache: - self._refresh_cached_registry_if_necessary() + registry_proto = self._refresh_cached_registry_if_necessary() return proto_registry_utils.list_stream_feature_views( - self.cached_registry_proto, project, tags + registry_proto, project, tags, skip_udf=skip_udf ) return self._list_objects( "STREAM_FEATURE_VIEWS", @@ -922,9 +990,54 @@ def list_stream_feature_views( StreamFeatureViewProto, StreamFeatureView, "STREAM_FEATURE_VIEW_PROTO", + skip_udf=skip_udf, tags=tags, ) + def get_label_view( + self, name: str, project: str, allow_cache: bool = False + ) -> LabelView: + if allow_cache: + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_label_view(registry_proto, name, project) + return self._get_object( + "LABEL_VIEWS", + name, + project, + LabelViewProto, + LabelView, + "LABEL_VIEW_NAME", + "LABEL_VIEW_PROTO", + FeatureViewNotFoundException, + ) + + def list_label_views( + self, + project: str, + allow_cache: bool = False, + tags: Optional[dict[str, str]] = None, + ) -> List[LabelView]: + if allow_cache: + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_label_views(registry_proto, project, tags) + return self._list_objects( + "LABEL_VIEWS", + project, + LabelViewProto, + LabelView, + "LABEL_VIEW_PROTO", + tags=tags, + ) + + def delete_label_view(self, name: str, project: str, commit: bool = True): + self._delete_object( + "LABEL_VIEWS", + name, + project, + "LABEL_VIEW_NAME", + FeatureViewNotFoundException, + ) + def list_validation_references( self, project: str, @@ -948,7 +1061,20 @@ def _list_objects( python_class: Any, proto_field_name: str, tags: Optional[dict[str, str]] = None, + proto_only: bool = False, + skip_udf: bool = False, ): + """ + Args: + proto_only: If True, return raw protobuf objects without calling + from_proto(). Used by proto() to build the RegistryProto cache + efficiently — avoids the from_proto()/to_proto() round-trip and + works uniformly for all object types (entities, data sources, etc.). + skip_udf: If True, call from_proto() but skip deserializing UDFs + (dill.loads). Returns Python objects suitable for filtering and + display without requiring the UDF's source module to be installed. + Only relevant for feature view types. + """ with GetSnowflakeConnection(self.registry_config) as conn: query = f""" SELECT @@ -962,11 +1088,17 @@ def _list_objects( if not df.empty: objects = [] for row in df.iterrows(): - obj = python_class.from_proto( - proto_class.FromString(row[1][proto_field_name]) - ) - if has_all_tags(obj.tags, tags): - objects.append(obj) + proto = proto_class.FromString(row[1][proto_field_name]) + if proto_only: + objects.append(proto) + else: + obj = ( + python_class.from_proto(proto, skip_udf=skip_udf) + if skip_udf + else python_class.from_proto(proto) + ) + if has_all_tags(obj.tags, tags): + objects.append(obj) return objects return [] @@ -977,10 +1109,8 @@ def list_permissions( tags: Optional[dict[str, str]] = None, ) -> List[Permission]: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.list_permissions( - self.cached_registry_proto, project - ) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_permissions(registry_proto, project) return self._list_objects( "PERMISSIONS", project, @@ -992,7 +1122,7 @@ def list_permissions( def apply_materialization( self, - feature_view: Union[FeatureView, OnDemandFeatureView], + feature_view: Union[FeatureView, OnDemandFeatureView, LabelView], project: str, start_date: datetime, end_date: datetime, @@ -1002,7 +1132,7 @@ def apply_materialization( fv_column_name = fv_table_str[:-1] python_class, proto_class = self._infer_fv_classes(feature_view) - if python_class in {OnDemandFeatureView}: + if python_class in {OnDemandFeatureView, LabelView}: raise ValueError( f"Cannot apply materialization for feature {feature_view.name} of type {python_class}" ) @@ -1017,6 +1147,10 @@ def apply_materialization( FeatureViewNotFoundException, ) fv.materialization_intervals.append((start_date, end_date)) + if hasattr(fv, "state"): + from feast.feature_view import FeatureViewState + + fv.state = FeatureViewState.AVAILABLE_ONLINE self._apply_object( fv_table_str, project, @@ -1029,10 +1163,8 @@ def list_project_metadata( self, project: str, allow_cache: bool = False ) -> List[ProjectMetadata]: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.list_project_metadata( - self.cached_registry_proto, project - ) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_project_metadata(registry_proto, project) with GetSnowflakeConnection(self.registry_config) as conn: query = f""" SELECT @@ -1106,7 +1238,8 @@ def get_user_metadata( FROM {self.registry_path}."{fv_table_str}" WHERE - {fv_column_name}_name = '{feature_view.name}' + project_id = '{project}' + AND {fv_column_name}_name = '{feature_view.name}' LIMIT 1 """ df = execute_snowflake_statement(conn, query).fetch_pandas_all() @@ -1125,41 +1258,100 @@ def process_project(project: Project): project_name = project.name last_updated_timestamp = project.last_updated_timestamp - try: - cached_project = self.get_project(project_name, True) - except ProjectObjectNotFoundException: - cached_project = None - - allow_cache = False - - if cached_project is not None: - allow_cache = ( - last_updated_timestamp <= cached_project.last_updated_timestamp - ) - r.projects.extend([project.to_proto()]) last_updated_timestamps.append(last_updated_timestamp) - for lister, registry_proto_field in [ - (self.list_entities, r.entities), - (self.list_feature_views, r.feature_views), - (self.list_data_sources, r.data_sources), - (self.list_on_demand_feature_views, r.on_demand_feature_views), - (self.list_stream_feature_views, r.stream_feature_views), - (self.list_feature_services, r.feature_services), - (self.list_saved_datasets, r.saved_datasets), - (self.list_validation_references, r.validation_references), - (self.list_permissions, r.permissions), + # proto_only=True: return raw protos without calling from_proto(), + # which would trigger dill.loads() on UDFs and fail for cross-project + # modules. _list_objects hits the DB directly (no cache), avoiding + # infinite recursion since proto() itself builds the cache. + for ( + table, + proto_class, + python_class, + proto_field_name, + registry_proto_field, + ) in [ + ("ENTITIES", EntityProto, Entity, "ENTITY_PROTO", r.entities), + ( + "FEATURE_VIEWS", + FeatureViewProto, + FeatureView, + "FEATURE_VIEW_PROTO", + r.feature_views, + ), + ( + "DATA_SOURCES", + DataSourceProto, + DataSource, + "DATA_SOURCE_PROTO", + r.data_sources, + ), + ( + "ON_DEMAND_FEATURE_VIEWS", + OnDemandFeatureViewProto, + OnDemandFeatureView, + "ON_DEMAND_FEATURE_VIEW_PROTO", + r.on_demand_feature_views, + ), + ( + "STREAM_FEATURE_VIEWS", + StreamFeatureViewProto, + StreamFeatureView, + "STREAM_FEATURE_VIEW_PROTO", + r.stream_feature_views, + ), + ( + "FEATURE_SERVICES", + FeatureServiceProto, + FeatureService, + "FEATURE_SERVICE_PROTO", + r.feature_services, + ), + ( + "SAVED_DATASETS", + SavedDatasetProto, + SavedDataset, + "SAVED_DATASET_PROTO", + r.saved_datasets, + ), + ( + "VALIDATION_REFERENCES", + ValidationReferenceProto, + ValidationReference, + "VALIDATION_REFERENCE_PROTO", + r.validation_references, + ), + ( + "PERMISSIONS", + PermissionProto, + Permission, + "PERMISSION_PROTO", + r.permissions, + ), + ( + "LABEL_VIEWS", + LabelViewProto, + LabelView, + "LABEL_VIEW_PROTO", + r.label_views, + ), ]: - objs: List[Any] = lister(project_name, allow_cache) # type: ignore + objs = self._list_objects( + table, + project_name, + proto_class, + python_class, + proto_field_name, + proto_only=True, + ) if objs: - obj_protos = [obj.to_proto() for obj in objs] - for obj_proto in obj_protos: + for obj_proto in objs: if "spec" in obj_proto.DESCRIPTOR.fields_by_name: obj_proto.spec.project = project_name else: obj_proto.project = project_name - registry_proto_field.extend(obj_protos) + registry_proto_field.extend(objs) # This is suuuper jank. Because of https://github.com/feast-dev/feast/issues/2783, # the registry proto only has a single infra field, which we're currently setting as the "last" project. @@ -1194,7 +1386,9 @@ def _get_last_updated_metadata(self, project: str): return datetime.fromtimestamp(int(df.squeeze()), tz=timezone.utc) def _infer_fv_classes(self, feature_view): - if isinstance(feature_view, StreamFeatureView): + if isinstance(feature_view, LabelView): + python_class, proto_class = LabelView, LabelViewProto + elif isinstance(feature_view, StreamFeatureView): python_class, proto_class = StreamFeatureView, StreamFeatureViewProto elif isinstance(feature_view, FeatureView): python_class, proto_class = FeatureView, FeatureViewProto @@ -1205,7 +1399,9 @@ def _infer_fv_classes(self, feature_view): return python_class, proto_class def _infer_fv_table(self, feature_view) -> str: - if isinstance(feature_view, StreamFeatureView): + if isinstance(feature_view, LabelView): + table = "LABEL_VIEWS" + elif isinstance(feature_view, StreamFeatureView): table = "STREAM_FEATURE_VIEWS" elif isinstance(feature_view, FeatureView): table = "FEATURE_VIEWS" @@ -1303,6 +1499,7 @@ def delete_project( "FEATURE_VIEWS", "ON_DEMAND_FEATURE_VIEWS", "STREAM_FEATURE_VIEWS", + "LABEL_VIEWS", "DATA_SOURCES", "ENTITIES", "PERMISSIONS", @@ -1312,7 +1509,7 @@ def delete_project( query = f""" DELETE FROM {self.registry_path}."{table}" WHERE - project_id = '{project}' + project_id = '{name}' """ execute_snowflake_statement(conn, query) return @@ -1340,8 +1537,8 @@ def get_project( allow_cache: bool = False, ) -> Project: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.get_project(self.cached_registry_proto, name) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.get_project(registry_proto, name) return self._get_project(name) def _list_projects( @@ -1357,7 +1554,7 @@ def _list_projects( objects = [] for row in df.iterrows(): obj = Project.from_proto( - ProjectProto.FromString(row[1]["project_proto"]) + ProjectProto.FromString(row[1]["PROJECT_PROTO"]) ) if has_all_tags(obj.tags, tags): objects.append(obj) @@ -1370,8 +1567,8 @@ def list_projects( tags: Optional[dict[str, str]] = None, ) -> List[Project]: if allow_cache: - self._refresh_cached_registry_if_necessary() - return proto_registry_utils.list_projects(self.cached_registry_proto, tags) + registry_proto = self._refresh_cached_registry_if_necessary() + return proto_registry_utils.list_projects(registry_proto, tags) return self._list_projects(tags) def set_project_metadata(self, project: str, key: str, value: str): diff --git a/sdk/python/feast/infra/registry/sql.py b/sdk/python/feast/infra/registry/sql.py index 103b1f6c0a6..4f1b6c174f0 100644 --- a/sdk/python/feast/infra/registry/sql.py +++ b/sdk/python/feast/infra/registry/sql.py @@ -4,35 +4,47 @@ from datetime import datetime, timezone from enum import Enum from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Union, cast +from typing import Any, Callable, Dict, List, Literal, Optional, Union, cast -from pydantic import StrictInt, StrictStr +from pydantic import StrictInt, StrictStr, field_validator from sqlalchemy import ( # type: ignore BigInteger, Column, Index, + Integer, LargeBinary, MetaData, String, Table, Text, + bindparam, create_engine, delete, + func, insert, select, + text, update, ) +from sqlalchemy import ( + inspect as sa_inspect, +) +from sqlalchemy.dialects import mysql from sqlalchemy.engine import Engine +from sqlalchemy.exc import IntegrityError from feast import utils from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.entity import Entity from feast.errors import ( + ConcurrentVersionConflict, DataSourceObjectNotFoundException, EntityNotFoundException, FeatureServiceNotFoundException, FeatureViewNotFoundException, + FeatureViewPinConflict, + FeatureViewVersionNotFound, PermissionNotFoundException, ProjectNotFoundException, ProjectObjectNotFoundException, @@ -43,6 +55,7 @@ from feast.feature_view import FeatureView from feast.infra.infra_object import Infra from feast.infra.registry.caching_registry import CachingRegistry +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission from feast.project import Project @@ -54,6 +67,7 @@ ) from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto from feast.protos.feast.core.InfraObject_pb2 import Infra as InfraProto +from feast.protos.feast.core.LabelView_pb2 import LabelView as LabelViewProto from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( OnDemandFeatureView as OnDemandFeatureViewProto, ) @@ -71,9 +85,37 @@ from feast.saved_dataset import SavedDataset, ValidationReference from feast.stream_feature_view import StreamFeatureView from feast.utils import _utc_now +from feast.version_utils import ( + generate_version_id, + parse_version, + version_tag, +) metadata = MetaData() +# Serialized protos (and their accompanying metadata blobs) can grow well past +# 64 KB — a single FeatureView proto routinely does. On MySQL, SQLAlchemy's +# LargeBinary maps to BLOB, which silently truncates anything over 64 KB and +# later surfaces as a protobuf DecodeError when the registry is read back +# (e.g. `feast serve` failing to load). Use LONGBLOB (up to 4 GB) on MySQL while +# keeping LargeBinary's default mapping on every other dialect. +# +# "mysql" and "mariadb" are registered separately because SQLAlchemy 2.x reports +# dialect.name == "mariadb" for MariaDB connections, which would otherwise miss +# the variant and fall back to BLOB. The variants are chained (rather than passed +# as variadic dialect names to a single with_variant call) so the expression also +# works on SQLAlchemy 1.4.x, which Feast still supports and which only accepts a +# single dialect name per with_variant call. +# +# NOTE for contributors: any new binary column that stores a serialized proto or +# blob metadata must use ProtoBytes, not LargeBinary directly, or the 64 KB +# MySQL/MariaDB truncation bug reappears silently. +ProtoBytes = ( + LargeBinary() + .with_variant(mysql.LONGBLOB(), "mysql") + .with_variant(mysql.LONGBLOB(), "mariadb") +) + projects = Table( "projects", @@ -81,7 +123,7 @@ Column("project_id", String(255), primary_key=True), Column("project_name", String(255), nullable=False), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("project_proto", LargeBinary, nullable=False), + Column("project_proto", ProtoBytes, nullable=False), ) Index("idx_projects_project_id", projects.c.project_id) @@ -92,7 +134,7 @@ Column("entity_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("entity_proto", LargeBinary, nullable=False), + Column("entity_proto", ProtoBytes, nullable=False), ) Index("idx_entities_project_id", entities.c.project_id) @@ -103,7 +145,7 @@ Column("data_source_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("data_source_proto", LargeBinary, nullable=False), + Column("data_source_proto", ProtoBytes, nullable=False), ) Index("idx_data_sources_project_id", data_sources.c.project_id) @@ -114,9 +156,9 @@ Column("feature_view_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("materialized_intervals", LargeBinary, nullable=True), - Column("feature_view_proto", LargeBinary, nullable=False), - Column("user_metadata", LargeBinary, nullable=True), + Column("materialized_intervals", ProtoBytes, nullable=True), + Column("feature_view_proto", ProtoBytes, nullable=False), + Column("user_metadata", ProtoBytes, nullable=True), ) Index("idx_feature_views_project_id", feature_views.c.project_id) @@ -127,8 +169,8 @@ Column("feature_view_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("feature_view_proto", LargeBinary, nullable=False), - Column("user_metadata", LargeBinary, nullable=True), + Column("feature_view_proto", ProtoBytes, nullable=False), + Column("user_metadata", ProtoBytes, nullable=True), ) Index("idx_stream_feature_views_project_id", stream_feature_views.c.project_id) @@ -139,19 +181,31 @@ Column("feature_view_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("feature_view_proto", LargeBinary, nullable=False), - Column("user_metadata", LargeBinary, nullable=True), + Column("feature_view_proto", ProtoBytes, nullable=False), + Column("user_metadata", ProtoBytes, nullable=True), ) Index("idx_on_demand_feature_views_project_id", on_demand_feature_views.c.project_id) +label_views = Table( + "label_views", + metadata, + Column("feature_view_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), + Column("last_updated_timestamp", BigInteger, nullable=False), + Column("feature_view_proto", ProtoBytes, nullable=False), + Column("user_metadata", ProtoBytes, nullable=True), +) + +Index("idx_label_views_project_id", label_views.c.project_id) + feature_services = Table( "feature_services", metadata, Column("feature_service_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("feature_service_proto", LargeBinary, nullable=False), + Column("feature_service_proto", ProtoBytes, nullable=False), ) Index("idx_feature_services_project_id", feature_services.c.project_id) @@ -162,7 +216,7 @@ Column("saved_dataset_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("saved_dataset_proto", LargeBinary, nullable=False), + Column("saved_dataset_proto", ProtoBytes, nullable=False), ) Index("idx_saved_datasets_project_id", saved_datasets.c.project_id) @@ -173,7 +227,7 @@ Column("validation_reference_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("validation_reference_proto", LargeBinary, nullable=False), + Column("validation_reference_proto", ProtoBytes, nullable=False), ) Index("idx_validation_references_project_id", validation_references.c.project_id) @@ -183,7 +237,7 @@ Column("infra_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("infra_proto", LargeBinary, nullable=False), + Column("infra_proto", ProtoBytes, nullable=False), ) Index("idx_managed_infra_project_id", managed_infra.c.project_id) @@ -194,11 +248,29 @@ Column("permission_name", String(255), primary_key=True), Column("project_id", String(255), primary_key=True), Column("last_updated_timestamp", BigInteger, nullable=False), - Column("permission_proto", LargeBinary, nullable=False), + Column("permission_proto", ProtoBytes, nullable=False), ) Index("idx_permissions_project_id", permissions.c.project_id) +feature_view_version_history = Table( + "feature_view_version_history", + metadata, + Column("feature_view_name", String(255), primary_key=True), + Column("project_id", String(255), primary_key=True), + Column("version_number", Integer, primary_key=True), + Column("feature_view_type", String(50), nullable=False), + Column("feature_view_proto", ProtoBytes, nullable=False), + Column("created_timestamp", BigInteger, nullable=False), + Column("description", Text, nullable=True), + Column("version_id", String(36), nullable=False), +) + +Index( + "idx_fv_version_history_project_id", + feature_view_version_history.c.project_id, +) + class FeastMetadataKeys(Enum): LAST_UPDATED_TIMESTAMP = "last_updated_timestamp" @@ -237,6 +309,31 @@ class SqlRegistryConfig(RegistryConfig): thread_pool_executor_worker_count: StrictInt = 0 """ int: Number of worker threads to use for asynchronous caching in SQL Registry. If set to 0, it doesn't use ThreadPoolExecutor. """ + schema_mode: Literal["auto", "verify", "skip"] = "auto" + """ str: Controls schema creation on startup. + 'auto' (default) — creates tables if they don't exist (current behavior). + 'verify' — skips DDL; checks that all expected tables exist and raises an error if any are missing. + 'skip' — skips both creation and verification. """ + + @field_validator("read_path") + def validate_read_path(cls, read_path: Optional[str]) -> Optional[str]: + # Mirror `RegistryConfig.validate_path`: a bare `postgresql://` read_path + # must be rewritten to the psycopg3 driver too, otherwise it silently + # falls back to psycopg2 while `path` uses psycopg3. + if read_path is not None: + return cls._normalize_postgres_scheme(read_path, "read_path") + return read_path + + +class FeastRegistrySchemaError(Exception): + def __init__(self, missing_tables: List[str]) -> None: + tables = ", ".join(missing_tables) + super().__init__( + f"SQL registry schema is incomplete — missing tables: {tables}. " + "Run 'feast registry create-schema' to create them, " + "or set schema_mode='auto' to create tables on startup." + ) + class SqlRegistry(CachingRegistry): def __init__( @@ -261,23 +358,136 @@ def __init__( ) else: self.read_engine = self.write_engine - metadata.create_all(self.write_engine) + if registry_config.schema_mode == "auto": + metadata.create_all(self.write_engine) + elif registry_config.schema_mode == "verify": + self._verify_schema(self.write_engine) + if self.read_engine is not self.write_engine: + self._verify_schema(self.read_engine) + self._warn_if_narrow_blob_columns(self.write_engine) + if self.read_engine is not self.write_engine: + # A read replica can be on a different schema version (e.g. mid + # blue-green switchover), so check it independently. + self._warn_if_narrow_blob_columns(self.read_engine) self.thread_pool_executor_worker_count = ( registry_config.thread_pool_executor_worker_count ) self.purge_feast_metadata = registry_config.purge_feast_metadata + self.enable_online_versioning = ( + registry_config.enable_online_feature_view_versioning + ) super().__init__( project=project, cache_ttl_seconds=registry_config.cache_ttl_seconds, cache_mode=registry_config.cache_mode, ) - # Sync feast_metadata to projects table - # when purge_feast_metadata is set to True, Delete data from - # feast_metadata table and list_project_metadata will not return any data self._sync_feast_metadata_to_projects_table() if not self.purge_feast_metadata: self._maybe_init_project_metadata(project) + @staticmethod + def _verify_schema(engine: Engine, registry_metadata: MetaData = metadata) -> None: + """Verify that all expected registry tables exist in the database. + + Raises ``FeastRegistrySchemaError`` listing missing tables and + suggesting ``feast registry create-schema``. + """ + expected = set(registry_metadata.tables.keys()) + actual = set( + sa_inspect(engine).get_table_names(schema=registry_metadata.schema) + ) + missing = expected - actual + if missing: + raise FeastRegistrySchemaError(sorted(missing)) + + @staticmethod + def _warn_if_narrow_blob_columns( + engine: Engine, registry_metadata: MetaData = metadata + ) -> None: + """Log an error when a MySQL/MariaDB registry still has narrow BLOB columns. + + ``metadata.create_all`` only creates missing tables; it never widens + columns on tables that already exist. A registry created before the + LONGBLOB fix keeps its 64 KB ``BLOB`` proto columns, which silently + truncate large protos and later fail to deserialize. There is no + automatic migration, so surface the stale schema and point operators at + the documented ``ALTER TABLE`` migration. Logged at ERROR (not WARNING) + so monitoring pipelines that filter below ERROR still catch it. + + This only *reports* the problem; it deliberately does not refuse to + start, since a registry whose protos all fit in 64 KB is unaffected and + an upgrade should not break it. Operators run the documented migration. + + ``registry_metadata`` defaults to this module's ``metadata`` (the source + of truth for registry tables) and is a parameter only to keep the + dependency explicit and unit-testable. + + Runs once per ``SqlRegistry`` construction on MySQL/MariaDB only, via a + single ``information_schema`` query scoped to the registry's own + serialized-proto (``ProtoBytes``) columns. + It is a no-op when the engine URL specifies no default database + (``DATABASE()`` returns NULL, so the scoped query matches nothing). + Best-effort: any failure here must never block registry startup. + """ + if engine.dialect.name not in ("mysql", "mariadb"): + return + try: + registry_tables = { + table.name for table in registry_metadata.tables.values() + } + # Only serialized-proto columns (those typed ProtoBytes) are at risk. + # Scope the query to exactly those table+column names so an unrelated + # BLOB column in a shared schema — or a future non-proto BLOB column + # on a registry table — can't trigger a false-positive warning. + # Identity check works because every proto column reuses the single + # shared ProtoBytes instance (SQLAlchemy stores the type as-is). A + # column typed as plain LargeBinary would not match here; the + # name-suffix vs ProtoBytes drift check in test_sql_registry.py + # guards against that regression. + proto_columns = { + column.name + for table in registry_metadata.tables.values() + for column in table.columns + if column.type is ProtoBytes + } + if not proto_columns: + return + query = text( + "SELECT TABLE_NAME, COLUMN_NAME " + "FROM information_schema.COLUMNS " + "WHERE TABLE_SCHEMA = DATABASE() AND DATA_TYPE = 'blob' " + "AND TABLE_NAME IN :table_names " + "AND COLUMN_NAME IN :column_names" + ).bindparams( + bindparam("table_names", expanding=True), + bindparam("column_names", expanding=True), + ) + with engine.connect() as conn: + rows = conn.execute( + query, + { + "table_names": list(registry_tables), + "column_names": list(proto_columns), + }, + ).fetchall() + stale = [f"{table_name}.{column_name}" for table_name, column_name in rows] + if stale: + # NOTE: keep this doc path in sync with the actual file location. + logger.error( + "SQL registry has %d column(s) still typed BLOB (64 KB cap) " + "on this %s database: %s. Large protos (e.g. a FeatureView) " + "will be silently truncated and fail to deserialize. " + "create_all() does not migrate existing columns; run the " + "ALTER TABLE ... MODIFY ... LONGBLOB migration documented at " + "docs/reference/registries/sql.md to fix this.", + len(stale), + engine.dialect.name, + ", ".join(sorted(stale)), + ) + except Exception as e: + # Diagnostics must never break registry startup. + logger.debug("Could not check registry BLOB column widths: %s", e) + def _sync_feast_metadata_to_projects_table(self): feast_metadata_projects: dict = {} projects_set: set = [] @@ -301,15 +511,21 @@ def _sync_feast_metadata_to_projects_table(self): # Find object in feast_metadata_projects but not in projects projects_to_sync = set(feast_metadata_projects.keys()) - set(projects_set) for project_name in projects_to_sync: - self.apply_project( - Project( - name=project_name, - created_timestamp=datetime.fromtimestamp( - feast_metadata_projects[project_name], tz=timezone.utc + try: + self.apply_project( + Project( + name=project_name, + created_timestamp=datetime.fromtimestamp( + feast_metadata_projects[project_name], tz=timezone.utc + ), ), - ), - commit=True, - ) + commit=True, + ) + except IntegrityError: + logger.info( + "Project %s already created in projects table by another process.", + project_name, + ) if self.purge_feast_metadata: with self.write_engine.begin() as conn: @@ -330,6 +546,8 @@ def teardown(self): saved_datasets, validation_references, permissions, + feature_view_version_history, + label_views, }: with self.write_engine.begin() as conn: stmt = delete(t) @@ -348,7 +566,7 @@ def _get_stream_feature_view(self, name: str, project: str): ) def _list_stream_feature_views( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs ) -> List[StreamFeatureView]: return self._list_objects( stream_feature_views, @@ -357,6 +575,7 @@ def _list_stream_feature_views( StreamFeatureView, "feature_view_proto", tags=tags, + **kwargs, ) def apply_entity(self, entity: Entity, project: str, commit: bool = True): @@ -413,25 +632,53 @@ def _get_any_feature_view(self, name: str, project: str) -> BaseFeatureView: python_class=StreamFeatureView, id_field_name="feature_view_name", proto_field_name="feature_view_proto", + not_found_exception=None, + ) + + if not fv: + fv = self._get_object( + table=label_views, + name=name, + project=project, + proto_class=LabelViewProto, + python_class=LabelView, + id_field_name="feature_view_name", + proto_field_name="feature_view_proto", not_found_exception=FeatureViewNotFoundException, ) return fv def _list_all_feature_views( - self, project: str, tags: Optional[dict[str, str]] + self, + project: str, + tags: Optional[dict[str, str]], + updated_since: Optional[datetime] = None, + **kwargs, ) -> List[BaseFeatureView]: return ( cast( list[BaseFeatureView], - self._list_feature_views(project=project, tags=tags), + self._list_feature_views( + project=project, tags=tags, updated_since=updated_since, **kwargs + ), ) + cast( list[BaseFeatureView], - self._list_stream_feature_views(project=project, tags=tags), + self._list_stream_feature_views( + project=project, tags=tags, updated_since=updated_since, **kwargs + ), ) + cast( list[BaseFeatureView], - self._list_on_demand_feature_views(project=project, tags=tags), + self._list_on_demand_feature_views( + project=project, tags=tags, updated_since=updated_since, **kwargs + ), + ) + + cast( + list[BaseFeatureView], + self._list_label_views( + project=project, tags=tags, updated_since=updated_since, **kwargs + ), ) ) @@ -498,7 +745,7 @@ def _get_validation_reference(self, name: str, project: str) -> ValidationRefere ) def _list_validation_references( - self, project: str, tags: Optional[dict[str, str]] = None + self, project: str, tags: Optional[dict[str, str]] = None, **kwargs ) -> List[ValidationReference]: return self._list_objects( table=validation_references, @@ -507,13 +754,20 @@ def _list_validation_references( python_class=ValidationReference, proto_field_name="validation_reference_proto", tags=tags, + **kwargs, ) def _list_entities( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs ) -> List[Entity]: return self._list_objects( - entities, project, EntityProto, Entity, "entity_proto", tags=tags + entities, + project, + EntityProto, + Entity, + "entity_proto", + tags=tags, + **kwargs, ) def delete_entity(self, name: str, project: str, commit: bool = True): @@ -522,17 +776,37 @@ def delete_entity(self, name: str, project: str, commit: bool = True): ) def delete_feature_view(self, name: str, project: str, commit: bool = True): - deleted_count = 0 - for table in { - feature_views, - on_demand_feature_views, - stream_feature_views, - }: - deleted_count += self._delete_object( - table, name, project, "feature_view_name", None + with self.write_engine.begin() as conn: + deleted_count = 0 + for table in { + feature_views, + on_demand_feature_views, + stream_feature_views, + label_views, + }: + stmt = delete(table).where( + table.c.feature_view_name == name, + table.c.project_id == project, + ) + rows = conn.execute(stmt) + deleted_count += rows.rowcount + if deleted_count == 0: + raise FeatureViewNotFoundException(name, project) + # Clean up version history in the same transaction + stmt = delete(feature_view_version_history).where( + feature_view_version_history.c.feature_view_name == name, + feature_view_version_history.c.project_id == project, ) - if deleted_count == 0: - raise FeatureViewNotFoundException(name, project) + conn.execute(stmt) + + self.apply_project( + self.get_project(name=project, allow_cache=False), commit=True + ) + if not self.purge_feast_metadata: + with self.write_engine.begin() as conn: + self._set_last_updated_metadata(_utc_now(), project, conn) + if self.cache_mode == "sync": + self.refresh() def delete_feature_service(self, name: str, project: str, commit: bool = True): return self._delete_object( @@ -556,7 +830,7 @@ def _get_data_source(self, name: str, project: str) -> DataSource: ) def _list_data_sources( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs ) -> List[DataSource]: return self._list_objects( data_sources, @@ -565,6 +839,7 @@ def _list_data_sources( DataSource, "data_source_proto", tags=tags, + **kwargs, ) def apply_data_source( @@ -575,17 +850,233 @@ def apply_data_source( ) def apply_feature_view( - self, feature_view: BaseFeatureView, project: str, commit: bool = True + self, + feature_view: BaseFeatureView, + project: str, + commit: bool = True, + no_promote: bool = False, ): + feature_view.ensure_valid() + self._ensure_feature_view_name_is_unique(feature_view, project) fv_table = self._infer_fv_table(feature_view) + fv_type_str = self._infer_fv_type_string(feature_view) - return self._apply_object( + is_latest, pin_version = parse_version(feature_view.version) + + if not is_latest: + # Explicit version: check if it exists (pin/revert) or not (forward declaration) + snapshot = self._get_version_snapshot( + feature_view.name, project, pin_version + ) + + if snapshot is not None: + # Version exists → pin/revert to that snapshot + # Check that the user hasn't also modified the definition. + # Compare user's FV (with version="latest") against active FV. + try: + active_fv = self._get_any_feature_view(feature_view.name, project) + user_fv_copy = feature_view.__copy__() + user_fv_copy.version = "latest" + active_fv.version = "latest" + # Clear metadata that differs due to registry state + user_fv_copy.created_timestamp = active_fv.created_timestamp + user_fv_copy.last_updated_timestamp = ( + active_fv.last_updated_timestamp + ) + user_fv_copy.current_version_number = ( + active_fv.current_version_number + ) + if hasattr(active_fv, "materialization_intervals"): + user_fv_copy.materialization_intervals = ( + active_fv.materialization_intervals + ) + if user_fv_copy != active_fv: + raise FeatureViewPinConflict( + feature_view.name, version_tag(pin_version) + ) + except FeatureViewNotFoundException: + pass + + snap_type, snap_proto_bytes = snapshot + proto_class, python_class = self._proto_class_for_type(snap_type) + snap_proto = proto_class.FromString(snap_proto_bytes) + restored_fv = python_class.from_proto(snap_proto) + restored_fv.version = feature_view.version + restored_fv.current_version_number = pin_version + return self._apply_object( + fv_table, + project, + "feature_view_name", + restored_fv, + "feature_view_proto", + ) + else: + # Version doesn't exist → forward declaration: create it + feature_view.current_version_number = pin_version + snapshot_proto = feature_view.to_proto() + snapshot_proto.spec.project = project + snapshot_proto_bytes = snapshot_proto.SerializeToString() + try: + self._save_version_snapshot( + feature_view.name, + project, + pin_version, + fv_type_str, + snapshot_proto_bytes, + ) + except IntegrityError: + raise ConcurrentVersionConflict( + f"Version v{pin_version} of '{feature_view.name}' was just created by " + f"another concurrent apply. Pull latest and retry." + ) + # Apply the FV as active + return self._apply_object( + fv_table, + project, + "feature_view_name", + feature_view, + "feature_view_proto", + ) + + # Normal (latest) apply: snapshot old version if changed, then save new + # First check if the FV already exists so we can snapshot the old one. + # Use write_engine for both reads to avoid read replica lag issues. + old_proto_bytes = None + with self.write_engine.begin() as conn: + stmt = select(fv_table).where( + fv_table.c.feature_view_name == feature_view.name, + fv_table.c.project_id == project, + ) + row = conn.execute(stmt).first() + if row: + old_proto_bytes = row._mapping["feature_view_proto"] + + # Apply the object (handles idempotency check internally) + # We need to detect if _apply_object actually made a change + # by checking before/after + self._apply_object( fv_table, project, "feature_view_name", feature_view, "feature_view_proto" ) + # After apply, read the current proto to see if it changed + with self.write_engine.begin() as conn: + stmt = select(fv_table).where( + fv_table.c.feature_view_name == feature_view.name, + fv_table.c.project_id == project, + ) + row = conn.execute(stmt).first() + if row: + new_proto_bytes = row._mapping["feature_view_proto"] + else: + return # shouldn't happen + + if old_proto_bytes is not None: + # Deserialize both versions to compare schema/UDF changes + proto_class, fv_class = self._proto_class_for_type(fv_type_str) + old_proto = proto_class.FromString(old_proto_bytes) + new_proto = proto_class.FromString(new_proto_bytes) + + old_fv = fv_class.from_proto(old_proto) + new_fv = fv_class.from_proto(new_proto) + + if not new_fv._schema_or_udf_changed(old_fv): + # No version-significant change, skip version creation + return + + # Something changed (or new FV). Save version snapshot(s). + if old_proto_bytes is not None: + # Snapshot the old version first (if not already in history) + next_ver = self._get_next_version_number(feature_view.name, project) + if next_ver == 0: + # First time versioning: save old as v0 + self._save_version_snapshot( + feature_view.name, + project, + 0, + fv_type_str, + old_proto_bytes, + ) + next_ver = 1 + + # Retry loop: if a concurrent apply claimed the same version number, + # re-read MAX+1 and try again. The client said "latest" so the + # exact number doesn't matter. + max_retries = 3 + for attempt in range(max_retries): + # Update current_version_number before saving snapshot + feature_view.current_version_number = next_ver + snapshot_proto = feature_view.to_proto() + snapshot_proto.spec.project = project + snapshot_proto_bytes = snapshot_proto.SerializeToString() + + try: + # Save new as next version (with correct current_version_number) + self._save_version_snapshot( + feature_view.name, + project, + next_ver, + fv_type_str, + snapshot_proto_bytes, + ) + break + except IntegrityError: + if attempt == max_retries - 1: + raise ConcurrentVersionConflict( + f"Failed to assign version for '{feature_view.name}' after " + f"{max_retries} attempts due to concurrent applies. " + f"Please retry." + ) + # Re-read the next available version number + next_ver = self._get_next_version_number(feature_view.name, project) + + if no_promote: + # Save version snapshot but skip updating the active row. + # The new version is accessible only via explicit @v reads. + return + + # Re-serialize with updated version number + with self.write_engine.begin() as conn: + update_stmt = ( + update(fv_table) + .where( + fv_table.c.feature_view_name == feature_view.name, + fv_table.c.project_id == project, + ) + .values( + feature_view_proto=snapshot_proto_bytes, + ) + ) + conn.execute(update_stmt) + else: + # New FV: save as v0 + feature_view.current_version_number = 0 + snapshot_proto = feature_view.to_proto() + snapshot_proto.spec.project = project + snapshot_proto_bytes = snapshot_proto.SerializeToString() + self._save_version_snapshot( + feature_view.name, + project, + 0, + fv_type_str, + snapshot_proto_bytes, + ) + with self.write_engine.begin() as conn: + update_stmt = ( + update(fv_table) + .where( + fv_table.c.feature_view_name == feature_view.name, + fv_table.c.project_id == project, + ) + .values( + feature_view_proto=snapshot_proto_bytes, + ) + ) + conn.execute(update_stmt) + def apply_feature_service( self, feature_service: FeatureService, project: str, commit: bool = True ): + feature_service.prepare_for_apply(self, project, allow_cache=True) return self._apply_object( feature_services, project, @@ -605,7 +1096,7 @@ def delete_data_source(self, name: str, project: str, commit: bool = True): raise DataSourceObjectNotFoundException(name, project) def _list_feature_services( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs ) -> List[FeatureService]: return self._list_objects( feature_services, @@ -614,10 +1105,11 @@ def _list_feature_services( FeatureService, "feature_service_proto", tags=tags, + **kwargs, ) def _list_feature_views( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs ) -> List[FeatureView]: return self._list_objects( feature_views, @@ -626,22 +1118,34 @@ def _list_feature_views( FeatureView, "feature_view_proto", tags=tags, + **kwargs, ) def _list_saved_datasets( - self, project: str, tags: Optional[dict[str, str]] = None + self, + project: str, + tags: Optional[dict[str, str]] = None, + namespace: Optional[str] = None, + collection: Optional[str] = None, + **kwargs, ) -> List[SavedDataset]: - return self._list_objects( + results = self._list_objects( saved_datasets, project, SavedDatasetProto, SavedDataset, "saved_dataset_proto", tags=tags, + **kwargs, ) + if namespace is not None: + results = [sd for sd in results if sd.namespace == namespace] + if collection is not None: + results = [sd for sd in results if sd.collection == collection] + return results def _list_on_demand_feature_views( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs ) -> List[OnDemandFeatureView]: return self._list_objects( on_demand_feature_views, @@ -650,6 +1154,41 @@ def _list_on_demand_feature_views( OnDemandFeatureView, "feature_view_proto", tags=tags, + **kwargs, + ) + + def _get_label_view(self, name: str, project: str) -> LabelView: + return self._get_object( + table=label_views, + name=name, + project=project, + proto_class=LabelViewProto, + python_class=LabelView, + id_field_name="feature_view_name", + proto_field_name="feature_view_proto", + not_found_exception=FeatureViewNotFoundException, + ) + + def _list_label_views( + self, project: str, tags: Optional[dict[str, str]], **kwargs + ) -> List[LabelView]: + return self._list_objects( + label_views, + project, + LabelViewProto, + LabelView, + "feature_view_proto", + tags=tags, + **kwargs, + ) + + def delete_label_view(self, name: str, project: str, commit: bool = True): + self._delete_object( + label_views, + name, + project, + "feature_view_name", + FeatureViewNotFoundException, ) def _list_project_metadata(self, project: str) -> List[ProjectMetadata]: @@ -685,6 +1224,15 @@ def apply_saved_dataset( "saved_dataset_proto", ) + def delete_saved_dataset(self, name: str, project: str, commit: bool = True): + return self._delete_object( + saved_datasets, + name, + project, + "saved_dataset_name", + SavedDatasetNotFound, + ) + def apply_validation_reference( self, validation_reference: ValidationReference, @@ -701,7 +1249,7 @@ def apply_validation_reference( def apply_materialization( self, - feature_view: Union[FeatureView, OnDemandFeatureView], + feature_view: Union[FeatureView, OnDemandFeatureView, LabelView], project: str, start_date: datetime, end_date: datetime, @@ -710,7 +1258,7 @@ def apply_materialization( table = self._infer_fv_table(feature_view) python_class, proto_class = self._infer_fv_classes(feature_view) - if python_class in {OnDemandFeatureView}: + if python_class in {OnDemandFeatureView, LabelView}: raise ValueError( f"Cannot apply materialization for feature {feature_view.name} of type {python_class}" ) @@ -725,6 +1273,10 @@ def apply_materialization( FeatureViewNotFoundException, ) fv.materialization_intervals.append((start_date, end_date)) + if hasattr(fv, "state"): + from feast.feature_view import FeatureViewState + + fv.state = FeatureViewState.AVAILABLE_ONLINE self._apply_object( table, project, "feature_view_name", fv, "feature_view_proto" ) @@ -800,7 +1352,9 @@ def apply_user_metadata( raise FeatureViewNotFoundException(feature_view.name, project=project) def _infer_fv_table(self, feature_view): - if isinstance(feature_view, StreamFeatureView): + if isinstance(feature_view, LabelView): + table = label_views + elif isinstance(feature_view, StreamFeatureView): table = stream_feature_views elif isinstance(feature_view, FeatureView): table = feature_views @@ -811,7 +1365,9 @@ def _infer_fv_table(self, feature_view): return table def _infer_fv_classes(self, feature_view): - if isinstance(feature_view, StreamFeatureView): + if isinstance(feature_view, LabelView): + python_class, proto_class = LabelView, LabelViewProto + elif isinstance(feature_view, StreamFeatureView): python_class, proto_class = StreamFeatureView, StreamFeatureViewProto elif isinstance(feature_view, FeatureView): python_class, proto_class = FeatureView, FeatureViewProto @@ -821,6 +1377,129 @@ def _infer_fv_classes(self, feature_view): raise ValueError(f"Unexpected feature view type: {type(feature_view)}") return python_class, proto_class + def _infer_fv_type_string(self, feature_view) -> str: + from feast.labeling.label_view import LabelView + + if isinstance(feature_view, LabelView): + return "label_view" + elif isinstance(feature_view, StreamFeatureView): + return "stream_feature_view" + elif isinstance(feature_view, FeatureView): + return "feature_view" + elif isinstance(feature_view, OnDemandFeatureView): + return "on_demand_feature_view" + else: + raise ValueError(f"Unexpected feature view type: {type(feature_view)}") + + def _proto_class_for_type(self, fv_type: str): + from feast.labeling.label_view import LabelView + from feast.protos.feast.core.LabelView_pb2 import ( + LabelView as LabelViewProto, + ) + + if fv_type == "label_view": + return LabelViewProto, LabelView + elif fv_type == "stream_feature_view": + return StreamFeatureViewProto, StreamFeatureView + elif fv_type == "feature_view": + return FeatureViewProto, FeatureView + elif fv_type == "on_demand_feature_view": + return OnDemandFeatureViewProto, OnDemandFeatureView + else: + raise ValueError(f"Unknown feature view type: {fv_type}") + + def _get_next_version_number(self, name: str, project: str) -> int: + with self.write_engine.begin() as conn: + stmt = select( + func.coalesce( + func.max(feature_view_version_history.c.version_number) + 1, 0 + ) + ).where( + feature_view_version_history.c.feature_view_name == name, + feature_view_version_history.c.project_id == project, + ) + result = conn.execute(stmt).scalar() + return result or 0 + + def _save_version_snapshot( + self, + name: str, + project: str, + version_number: int, + fv_type: str, + proto_bytes: bytes, + ): + now = int(_utc_now().timestamp()) + vid = generate_version_id() + with self.write_engine.begin() as conn: + stmt = insert(feature_view_version_history).values( + feature_view_name=name, + project_id=project, + version_number=version_number, + feature_view_type=fv_type, + feature_view_proto=proto_bytes, + created_timestamp=now, + description="", + version_id=vid, + ) + conn.execute(stmt) + + def _get_version_snapshot( + self, name: str, project: str, version_number: int + ) -> Optional[tuple]: + with self.read_engine.begin() as conn: + stmt = select(feature_view_version_history).where( + feature_view_version_history.c.feature_view_name == name, + feature_view_version_history.c.project_id == project, + feature_view_version_history.c.version_number == version_number, + ) + row = conn.execute(stmt).first() + if row: + return ( + row._mapping["feature_view_type"], + row._mapping["feature_view_proto"], + ) + return None + + def get_feature_view_by_version( + self, name: str, project: str, version_number: int, allow_cache: bool = False + ) -> BaseFeatureView: + snapshot = self._get_version_snapshot(name, project, version_number) + if snapshot is None: + raise FeatureViewVersionNotFound(name, version_tag(version_number), project) + snap_type, snap_proto_bytes = snapshot + proto_class, python_class = self._proto_class_for_type(snap_type) + snap_proto = proto_class.FromString(snap_proto_bytes) + fv = python_class.from_proto(snap_proto) + fv.current_version_number = version_number + return fv + + def list_feature_view_versions( + self, name: str, project: str + ) -> List[Dict[str, Any]]: + with self.read_engine.begin() as conn: + stmt = ( + select(feature_view_version_history) + .where( + feature_view_version_history.c.feature_view_name == name, + feature_view_version_history.c.project_id == project, + ) + .order_by(feature_view_version_history.c.version_number) + ) + rows = conn.execute(stmt).all() + return [ + { + "version": version_tag(row._mapping["version_number"]), + "version_number": row._mapping["version_number"], + "feature_view_type": row._mapping["feature_view_type"], + "created_timestamp": datetime.fromtimestamp( + row._mapping["created_timestamp"], tz=timezone.utc + ), + "version_id": row._mapping["version_id"], + } + for row in rows + ] + def get_user_metadata( self, project: str, feature_view: BaseFeatureView ) -> Optional[bytes]: @@ -847,26 +1526,30 @@ def process_project(project: Project): r.projects.extend([project.to_proto()]) last_updated_timestamps.append(last_updated_timestamp) + # proto_only=True: return raw protos without calling from_proto(), + # which would trigger dill.loads() on UDFs and fail for cross-project + # modules. The _list_* helpers hit the DB directly (no cache), avoiding + # infinite recursion since proto() itself builds the cache. for lister, registry_proto_field in [ - (self.list_entities, r.entities), - (self.list_feature_views, r.feature_views), - (self.list_data_sources, r.data_sources), - (self.list_on_demand_feature_views, r.on_demand_feature_views), - (self.list_stream_feature_views, r.stream_feature_views), - (self.list_feature_services, r.feature_services), - (self.list_saved_datasets, r.saved_datasets), - (self.list_validation_references, r.validation_references), - (self.list_permissions, r.permissions), + (self._list_entities, r.entities), + (self._list_feature_views, r.feature_views), + (self._list_data_sources, r.data_sources), + (self._list_on_demand_feature_views, r.on_demand_feature_views), + (self._list_stream_feature_views, r.stream_feature_views), + (self._list_feature_services, r.feature_services), + (self._list_saved_datasets, r.saved_datasets), + (self._list_validation_references, r.validation_references), + (self._list_permissions, r.permissions), + (self._list_label_views, r.label_views), ]: - objs: List[Any] = lister(project_name, allow_cache=False) # type: ignore + objs: List[Any] = lister(project_name, tags=None, proto_only=True) # type: ignore if objs: - obj_protos = [obj.to_proto() for obj in objs] - for obj_proto in obj_protos: + for obj_proto in objs: if "spec" in obj_proto.DESCRIPTOR.fields_by_name: obj_proto.spec.project = project_name else: obj_proto.project = project_name - registry_proto_field.extend(obj_protos) + registry_proto_field.extend(objs) # This is suuuper jank. Because of https://github.com/feast-dev/feast/issues/2783, # the registry proto only has a single infra field, which we're currently setting as the "last" project. @@ -995,10 +1678,16 @@ def _apply_object( "last_updated_timestamp": update_time, "project_id": project, } - insert_stmt = insert(table).values( - values, - ) - conn.execute(insert_stmt) + try: + with conn.begin_nested(): + conn.execute(insert(table).values(values)) + except IntegrityError: + logger.info( + "Object %s in project %s already created by another " + "process, skipping.", + name, + project, + ) if not isinstance(obj, Project): self.apply_project( @@ -1028,8 +1717,15 @@ def _maybe_init_project_metadata(self, project): "last_updated_timestamp": update_time, "project_id": project, } - insert_stmt = insert(feast_metadata).values(values) - conn.execute(insert_stmt) + try: + with conn.begin_nested(): + conn.execute(insert(feast_metadata).values(values)) + except IntegrityError: + logger.info( + "Project metadata for %s already initialized by " + "another process.", + project, + ) def _delete_object( self, @@ -1088,18 +1784,56 @@ def _list_objects( python_class: Any, proto_field_name: str, tags: Optional[dict[str, str]] = None, + proto_only: bool = False, + skip_udf: bool = False, + updated_since: Optional[datetime] = None, ): + """ + Args: + proto_only: If True, return raw protobuf objects without calling + from_proto(). Used by proto() to build the RegistryProto cache + efficiently — avoids the from_proto()/to_proto() round-trip and + works uniformly for all object types (entities, data sources, etc.). + skip_udf: If True, call from_proto() but skip deserializing UDFs + (dill.loads). Returns Python objects suitable for filtering and + display without requiring the UDF's source module to be installed. + Only relevant for feature view types that contain UDFs. + """ + import inspect + + supports_skip_udf = ( + skip_udf + and "skip_udf" in inspect.signature(python_class.from_proto).parameters + ) + with self.read_engine.begin() as conn: stmt = select(table).where(table.c.project_id == project) + if updated_since is not None: + # Ensure naive datetimes are treated as UTC, consistent with + # the Python-side filters that compare against offset-naive UTC + # last_updated_timestamp values from protobuf. + if updated_since.tzinfo is None: + updated_since_utc = updated_since.replace(tzinfo=timezone.utc) + else: + updated_since_utc = updated_since.astimezone(timezone.utc) + stmt = stmt.where( + table.c.last_updated_timestamp >= int(updated_since_utc.timestamp()) + ) rows = conn.execute(stmt).all() if rows: objects = [] for row in rows: - obj = python_class.from_proto( - proto_class.FromString(row._mapping[proto_field_name]) - ) - if utils.has_all_tags(obj.tags, tags): - objects.append(obj) + proto = proto_class.FromString(row._mapping[proto_field_name]) + if proto_only: + objects.append(proto) + else: + obj = ( + python_class.from_proto(proto, skip_udf=True) + if supports_skip_udf + else python_class.from_proto(proto) + ) + if utils.has_all_tags(obj.tags, tags): + objects.append(obj) return objects return [] @@ -1170,7 +1904,7 @@ def _get_permission(self, name: str, project: str) -> Permission: ) def _list_permissions( - self, project: str, tags: Optional[dict[str, str]] + self, project: str, tags: Optional[dict[str, str]], **kwargs ) -> List[Permission]: return self._list_objects( permissions, @@ -1179,6 +1913,7 @@ def _list_permissions( Permission, "permission_proto", tags=tags, + **kwargs, ) def apply_permission( @@ -1256,6 +1991,7 @@ def delete_project( feature_views, on_demand_feature_views, stream_feature_views, + label_views, data_sources, entities, permissions, diff --git a/sdk/python/feast/infra/transformation_servers/Dockerfile b/sdk/python/feast/infra/transformation_servers/Dockerfile index cd46b0baa90..b0880e960e1 100644 --- a/sdk/python/feast/infra/transformation_servers/Dockerfile +++ b/sdk/python/feast/infra/transformation_servers/Dockerfile @@ -2,6 +2,8 @@ FROM python:3.11-slim RUN apt-get update && apt-get install -y git +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + # Copy app handler code COPY sdk/python/feast/infra/transformation_servers/app.py app.py @@ -9,13 +11,11 @@ COPY sdk/python/feast/infra/transformation_servers/app.py app.py COPY sdk/python sdk/python COPY protos protos COPY go go -COPY setup.py setup.py COPY pyproject.toml pyproject.toml COPY README.md README.md - # Install dependencies -RUN --mount=source=.git,target=.git,type=bind pip3 install --no-cache-dir '.[gcp,aws]' +RUN --mount=source=.git,target=.git,type=bind uv pip install --system --no-cache-dir '.[gcp,aws,grpcio]' # Start feature transformation server CMD [ "python", "app.py" ] diff --git a/sdk/python/feast/infra/utils/clickhouse/clickhouse_config.py b/sdk/python/feast/infra/utils/clickhouse/clickhouse_config.py index 1f163e0a81b..75167f8a60e 100644 --- a/sdk/python/feast/infra/utils/clickhouse/clickhouse_config.py +++ b/sdk/python/feast/infra/utils/clickhouse/clickhouse_config.py @@ -1,3 +1,5 @@ +from typing import Any + from pydantic import ConfigDict, StrictStr from feast.repo_config import FeastConfigBaseModel @@ -11,4 +13,8 @@ class ClickhouseConfig(FeastConfigBaseModel): password: StrictStr use_temporary_tables_for_entity_df: bool = True + # See https://github.com/ClickHouse/clickhouse-connect/blob/main/clickhouse_connect/driver/__init__.py#L51 + # Some typical ones e.g. send_receive_timeout (read_timeout), etc + additional_client_args: dict[str, Any] | None = None + model_config = ConfigDict(frozen=True) diff --git a/sdk/python/feast/infra/utils/clickhouse/connection_utils.py b/sdk/python/feast/infra/utils/clickhouse/connection_utils.py index e60922e478d..6d5f1b87052 100644 --- a/sdk/python/feast/infra/utils/clickhouse/connection_utils.py +++ b/sdk/python/feast/infra/utils/clickhouse/connection_utils.py @@ -1,18 +1,34 @@ -from functools import cache +import threading import clickhouse_connect from clickhouse_connect.driver import Client from feast.infra.utils.clickhouse.clickhouse_config import ClickhouseConfig +thread_local = threading.local() + -@cache def get_client(config: ClickhouseConfig) -> Client: - client = clickhouse_connect.get_client( - host=config.host, - port=config.port, - user=config.user, - password=config.password, - database=config.database, - ) - return client + # Clickhouse client is not thread-safe, so we need to create a separate instance for each thread. + if not hasattr(thread_local, "clickhouse_client"): + additional_client_args = config.additional_client_args + + if additional_client_args: + thread_local.clickhouse_client = clickhouse_connect.get_client( + host=config.host, + port=config.port, + user=config.user, + password=config.password, + database=config.database, + **additional_client_args, + ) + else: + thread_local.clickhouse_client = clickhouse_connect.get_client( + host=config.host, + port=config.port, + user=config.user, + password=config.password, + database=config.database, + ) + + return thread_local.clickhouse_client diff --git a/sdk/python/feast/infra/utils/postgres/connection_utils.py b/sdk/python/feast/infra/utils/postgres/connection_utils.py index a6105354617..f29f303ffd6 100644 --- a/sdk/python/feast/infra/utils/postgres/connection_utils.py +++ b/sdk/python/feast/infra/utils/postgres/connection_utils.py @@ -68,12 +68,15 @@ def _get_conninfo(config: PostgreSQLConfig) -> str: def _get_conn_kwargs(config: PostgreSQLConfig) -> Dict[str, Any]: """Get the additional `kwargs` required for connection objects.""" + search_path = (config.db_schema or config.user).strip() + if search_path != "public": + search_path += ",public" return { "sslmode": config.sslmode, "sslkey": config.sslkey_path, "sslcert": config.sslcert_path, "sslrootcert": config.sslrootcert_path, - "options": "-c search_path={}".format(config.db_schema or config.user), + "options": "-c search_path={}".format(search_path), } diff --git a/sdk/python/feast/infra/utils/postgres/postgres_config.py b/sdk/python/feast/infra/utils/postgres/postgres_config.py index a4ebb456ef1..60099ede999 100644 --- a/sdk/python/feast/infra/utils/postgres/postgres_config.py +++ b/sdk/python/feast/infra/utils/postgres/postgres_config.py @@ -21,7 +21,7 @@ class PostgreSQLConfig(FeastConfigBaseModel): db_schema: StrictStr = "public" user: StrictStr password: StrictStr - sslmode: Optional[StrictStr] = None + sslmode: Optional[StrictStr] = "require" sslkey_path: Optional[StrictStr] = None sslcert_path: Optional[StrictStr] = None sslrootcert_path: Optional[StrictStr] = None diff --git a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql index fc13332e4b0..e38d21f96cf 100644 --- a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql +++ b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_creation.sql @@ -97,3 +97,12 @@ CREATE TABLE IF NOT EXISTS REGISTRY_PATH."PERMISSIONS" ( permission_proto BINARY NOT NULL, PRIMARY KEY (permission_name, project_id) ); + +CREATE TABLE IF NOT EXISTS REGISTRY_PATH."LABEL_VIEWS" ( + label_view_name VARCHAR, + project_id VARCHAR, + last_updated_timestamp TIMESTAMP_LTZ NOT NULL, + label_view_proto BINARY NOT NULL, + user_metadata BINARY, + PRIMARY KEY (label_view_name, project_id) +); diff --git a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_deletion.sql b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_deletion.sql index 780424abd17..76d5029b50a 100644 --- a/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_deletion.sql +++ b/sdk/python/feast/infra/utils/snowflake/registry/snowflake_table_deletion.sql @@ -1,3 +1,5 @@ +DROP TABLE IF EXISTS REGISTRY_PATH."PROJECTS"; + DROP TABLE IF EXISTS REGISTRY_PATH."DATA_SOURCES"; DROP TABLE IF EXISTS REGISTRY_PATH."ENTITIES"; @@ -16,6 +18,8 @@ DROP TABLE IF EXISTS REGISTRY_PATH."SAVED_DATASETS"; DROP TABLE IF EXISTS REGISTRY_PATH."STREAM_FEATURE_VIEWS"; -DROP TABLE IF EXISTS REGISTRY_PATH."VALIDATION_REFERENCES" +DROP TABLE IF EXISTS REGISTRY_PATH."VALIDATION_REFERENCES"; + +DROP TABLE IF EXISTS REGISTRY_PATH."PERMISSIONS"; -DROP TABLE IF EXISTS REGISTRY_PATH."PERMISSIONS" +DROP TABLE IF EXISTS REGISTRY_PATH."LABEL_VIEWS"; diff --git a/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py b/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py index b9254e72699..e22d4857a19 100644 --- a/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py +++ b/sdk/python/feast/infra/utils/snowflake/snowflake_utils.py @@ -76,19 +76,15 @@ def __enter__(self): kwargs.update((k, v) for k, v in config_dict.items() if v is not None) - for k, v in kwargs.items(): - if k in ["role", "warehouse", "database", "schema_"]: - kwargs[k] = f'"{v}"' - kwargs["schema"] = kwargs.pop("schema_") # https://docs.snowflake.com/en/user-guide/python-connector-example.html#using-key-pair-authentication-key-pair-rotation # https://docs.snowflake.com/en/user-guide/key-pair-auth.html#configuring-key-pair-authentication if "private_key" in kwargs or "private_key_content" in kwargs: kwargs["private_key"] = parse_private_key_path( - kwargs.get("private_key_passphrase"), - kwargs.get("private_key"), - kwargs.get("private_key_content"), + kwargs.pop("private_key_passphrase", None), + kwargs.pop("private_key", None), + kwargs.pop("private_key_content", None), ) try: diff --git a/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_python_udfs_creation.sql b/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_python_udfs_creation.sql index e39b12c1f79..b1a0d85f526 100644 --- a/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_python_udfs_creation.sql +++ b/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_python_udfs_creation.sql @@ -1,127 +1,127 @@ -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_binary_to_bytes_proto(df BINARY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_binary_to_bytes_proto(df BINARY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_binary_to_bytes_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_varchar_to_string_proto(df VARCHAR) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_varchar_to_string_proto(df VARCHAR) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_varchar_to_string_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_array_bytes_to_list_bytes_proto(df ARRAY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_array_bytes_to_list_bytes_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_bytes_to_list_bytes_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_array_varchar_to_list_string_proto(df ARRAY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_array_varchar_to_list_string_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_varchar_to_list_string_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_array_number_to_list_int32_proto(df ARRAY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_array_number_to_list_int32_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_number_to_list_int32_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_array_number_to_list_int64_proto(df ARRAY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_array_number_to_list_int64_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_number_to_list_int64_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_array_float_to_list_double_proto(df ARRAY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_array_float_to_list_double_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_float_to_list_double_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_array_boolean_to_list_bool_proto(df ARRAY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_array_boolean_to_list_bool_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_boolean_to_list_bool_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_array_timestamp_to_list_unix_timestamp_proto(df ARRAY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_array_timestamp_to_list_unix_timestamp_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_timestamp_to_list_unix_timestamp_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_number_to_int32_proto(df NUMBER) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_number_to_int32_proto(df NUMBER) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_number_to_int32_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_number_to_int64_proto(df NUMBER) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_number_to_int64_proto(df NUMBER) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_number_to_int64_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_float_to_double_proto(df DOUBLE) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_float_to_double_proto(df DOUBLE) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_float_to_double_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_boolean_to_bool_proto(df BOOLEAN) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_boolean_to_bool_proto(df BOOLEAN) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_boolean_to_bool_boolean_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_snowflake_timestamp_to_unix_timestamp_proto(df NUMBER) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_snowflake_timestamp_to_unix_timestamp_proto(df NUMBER) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_timestamp_to_unix_timestamp_proto' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_serialize_entity_keys(names ARRAY, data ARRAY, types ARRAY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_serialize_entity_keys(names ARRAY, data ARRAY, types ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_serialize_entity_keys' IMPORTS = ('@STAGE_HOLDER/feast.zip'); -CREATE FUNCTION IF NOT EXISTS feast_PROJECT_NAME_entity_key_proto_to_string(names ARRAY, data ARRAY, types ARRAY) +CREATE OR REPLACE FUNCTION feast_PROJECT_NAME_entity_key_proto_to_string(names ARRAY, data ARRAY, types ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = 'RUNTIME_VERSION_HOLDER' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_entity_key_proto_to_string' IMPORTS = ('@STAGE_HOLDER/feast.zip') diff --git a/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_udfs.py b/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_udfs.py index 277d8e18946..c9cbb87a953 100644 --- a/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_udfs.py +++ b/sdk/python/feast/infra/utils/snowflake/snowpark/snowflake_udfs.py @@ -14,15 +14,27 @@ ) from feast.value_type import ValueType +# NOTE: The `CREATE OR REPLACE FUNCTION ...` blocks below are illustrative reference +# documentation only; they are not executed as-is. The SQL Feast actually runs is +# templated from `snowflake_python_udfs_creation.sql`, which fills in placeholders +# (stage path, project name, and Python UDF RUNTIME_VERSION) at deploy time -- see +# `SnowflakeComputeEngine.update()` in `snowflake_engine.py`. The RUNTIME_VERSION +# shown here documents Feast's default (`SnowflakeComputeEngineConfig +# .python_udf_runtime_version`, currently "3.10"); if you override that config +# field, the runtime actually deployed to Snowflake will differ from what's shown +# below. + """ CREATE OR REPLACE FUNCTION feast_snowflake_binary_to_bytes_proto(df BINARY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_binary_to_bytes_proto' IMPORTS = ('@feast_stage/feast.zip'); """ + + # ValueType.BYTES = 1 @vectorized(input=pandas.DataFrame) def feast_snowflake_binary_to_bytes_proto(df): @@ -41,11 +53,13 @@ def feast_snowflake_binary_to_bytes_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_varchar_to_string_proto(df VARCHAR) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_varchar_to_string_proto' IMPORTS = ('@feast_stage/feast.zip'); """ + + # ValueType.STRING = 2 @vectorized(input=pandas.DataFrame) def feast_snowflake_varchar_to_string_proto(df): @@ -64,11 +78,13 @@ def feast_snowflake_varchar_to_string_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_array_bytes_to_list_bytes_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_bytes_to_list_bytes_proto' IMPORTS = ('@feast_stage/feast.zip'); """ + + # ValueType.STRING_LIST = 12 @vectorized(input=pandas.DataFrame) def feast_snowflake_array_bytes_to_list_bytes_proto(df): @@ -90,7 +106,7 @@ def feast_snowflake_array_bytes_to_list_bytes_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_array_varchar_to_list_string_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_varchar_to_list_string_proto' IMPORTS = ('@feast_stage/feast.zip'); @@ -114,7 +130,7 @@ def feast_snowflake_array_varchar_to_list_string_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_array_number_to_list_int32_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_number_to_list_int32_proto' IMPORTS = ('@feast_stage/feast.zip'); @@ -138,7 +154,7 @@ def feast_snowflake_array_number_to_list_int32_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_array_number_to_list_int64_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_number_to_list_int64_proto' IMPORTS = ('@feast_stage/feast.zip'); @@ -162,7 +178,7 @@ def feast_snowflake_array_number_to_list_int64_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_array_float_to_list_double_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_float_to_list_double_proto' IMPORTS = ('@feast_stage/feast.zip'); @@ -188,7 +204,7 @@ def feast_snowflake_array_float_to_list_double_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_array_boolean_to_list_bool_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_boolean_to_list_bool_proto' IMPORTS = ('@feast_stage/feast.zip'); @@ -212,7 +228,7 @@ def feast_snowflake_array_boolean_to_list_bool_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_array_timestamp_to_list_unix_timestamp_proto(df ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_array_timestamp_to_list_unix_timestamp_proto' IMPORTS = ('@feast_stage/feast.zip'); @@ -238,11 +254,13 @@ def feast_snowflake_array_timestamp_to_list_unix_timestamp_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_number_to_int32_proto(df NUMBER) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_number_to_int32_proto' IMPORTS = ('@feast_stage/feast.zip'); """ + + # ValueType.INT32 = 3 @vectorized(input=pandas.DataFrame) def feast_snowflake_number_to_int32_proto(df): @@ -261,11 +279,13 @@ def feast_snowflake_number_to_int32_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_number_to_int64_proto(df NUMBER) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_number_to_int64_proto' IMPORTS = ('@feast_stage/feast.zip'); """ + + # ValueType.INT64 = 4 @vectorized(input=pandas.DataFrame) def feast_snowflake_number_to_int64_proto(df): @@ -286,11 +306,13 @@ def feast_snowflake_number_to_int64_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_float_to_double_proto(df DOUBLE) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_float_to_double_proto' IMPORTS = ('@feast_stage/feast.zip'); """ + + # ValueType.FLOAT = 5 & ValueType.DOUBLE = 6 @vectorized(input=pandas.DataFrame) def feast_snowflake_float_to_double_proto(df): @@ -309,11 +331,13 @@ def feast_snowflake_float_to_double_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_boolean_to_bool_proto(df BOOLEAN) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_boolean_to_bool_boolean_proto' IMPORTS = ('@feast_stage/feast.zip'); """ + + # ValueType.BOOL = 7 @vectorized(input=pandas.DataFrame) def feast_snowflake_boolean_to_bool_boolean_proto(df): @@ -332,11 +356,13 @@ def feast_snowflake_boolean_to_bool_boolean_proto(df): CREATE OR REPLACE FUNCTION feast_snowflake_timestamp_to_unix_timestamp_proto(df NUMBER) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_snowflake_timestamp_to_unix_timestamp_proto' IMPORTS = ('@feast_stage/feast.zip'); """ + + # ValueType.UNIX_TIMESTAMP = 8 @vectorized(input=pandas.DataFrame) def feast_snowflake_timestamp_to_unix_timestamp_proto(df): @@ -358,11 +384,13 @@ def feast_snowflake_timestamp_to_unix_timestamp_proto(df): CREATE OR REPLACE FUNCTION feast_serialize_entity_keys(names ARRAY, data ARRAY, types ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_serialize_entity_keys' IMPORTS = ('@feast_stage/feast.zip') """ + + # converts 1 to n many entity keys to a single binary for lookups @vectorized(input=pandas.DataFrame) def feast_serialize_entity_keys(df): @@ -405,11 +433,13 @@ def feast_serialize_entity_keys(df): CREATE OR REPLACE FUNCTION feast_entity_key_proto_to_string(names ARRAY, data ARRAY, types ARRAY) RETURNS BINARY LANGUAGE PYTHON - RUNTIME_VERSION = '3.9' + RUNTIME_VERSION = '3.10' PACKAGES = ('protobuf', 'pandas') HANDLER = 'feast.infra.utils.snowflake.snowpark.snowflake_udfs.feast_entity_key_proto_to_string' IMPORTS = ('@feast_stage/feast.zip') """ + + # converts 1 to n many entity keys to a single binary for lookups @vectorized(input=pandas.DataFrame) def feast_entity_key_proto_to_string(df): diff --git a/sdk/python/feast/labeling/__init__.py b/sdk/python/feast/labeling/__init__.py new file mode 100644 index 00000000000..de55d970ef2 --- /dev/null +++ b/sdk/python/feast/labeling/__init__.py @@ -0,0 +1,11 @@ +"""Labeling primitives for mutable annotations decoupled from immutable feature data.""" + +from feast.labeling.conflict_policy import ConflictPolicy +from feast.labeling.conflict_resolver import resolve_conflicts +from feast.labeling.label_view import LabelView + +__all__ = [ + "ConflictPolicy", + "LabelView", + "resolve_conflicts", +] diff --git a/sdk/python/feast/labeling/conflict_policy.py b/sdk/python/feast/labeling/conflict_policy.py new file mode 100644 index 00000000000..de81e2d4afc --- /dev/null +++ b/sdk/python/feast/labeling/conflict_policy.py @@ -0,0 +1,66 @@ +from enum import Enum + +from feast.protos.feast.core.LabelView_pb2 import ( + ConflictResolutionPolicy as ConflictResolutionPolicyProto, +) + + +class ConflictPolicy(Enum): + """Determines how conflicting labels from different labelers are resolved. + + When multiple labelers write labels for the same entity key, the conflict + policy controls which resolved value is returned for offline/batch reads + (training data generation, UI browse, quality metrics). + + .. note:: + + **Enforcement scope:** The conflict policy is enforced for **offline + store reads** — all batch queries (``pull_all_from_table_or_query``, + UI endpoints, training data generation) apply the configured resolution + strategy. The **online store** always uses ``LAST_WRITE_WINS`` semantics + regardless of this setting, as labeling is primarily a training-time + concern. + + Attributes: + LAST_WRITE_WINS: The most recently written label for a given entity key + takes precedence, regardless of which labeler wrote it. + LABELER_PRIORITY: Labels are ranked by a pre-configured labeler priority + order. Higher-priority labelers override lower-priority ones. + MAJORITY_VOTE: The label value that appears most frequently across all + labelers is selected. Useful for consensus-based annotation workflows. + """ + + LAST_WRITE_WINS = "last_write_wins" + LABELER_PRIORITY = "labeler_priority" + MAJORITY_VOTE = "majority_vote" + + def to_proto(self) -> int: + """Converts this ConflictPolicy to its protobuf enum value. + + Returns: + The integer protobuf enum value corresponding to this policy. + """ + return _POLICY_TO_PROTO[self] + + @classmethod + def from_proto(cls, proto_val: int) -> "ConflictPolicy": + """Creates a ConflictPolicy from a protobuf enum value. + + Args: + proto_val: The integer protobuf enum value. + + Returns: + The ConflictPolicy corresponding to the given protobuf value. + """ + return _PROTO_TO_POLICY[proto_val] + + +_POLICY_TO_PROTO = { + ConflictPolicy.LAST_WRITE_WINS: ConflictResolutionPolicyProto.LAST_WRITE_WINS, + ConflictPolicy.LABELER_PRIORITY: ConflictResolutionPolicyProto.LABELER_PRIORITY, + ConflictPolicy.MAJORITY_VOTE: ConflictResolutionPolicyProto.MAJORITY_VOTE, +} + +_PROTO_TO_POLICY: dict[int, ConflictPolicy] = { + v: k for k, v in _POLICY_TO_PROTO.items() +} diff --git a/sdk/python/feast/labeling/conflict_resolver.py b/sdk/python/feast/labeling/conflict_resolver.py new file mode 100644 index 00000000000..854500a52bd --- /dev/null +++ b/sdk/python/feast/labeling/conflict_resolver.py @@ -0,0 +1,157 @@ +"""Offline-store conflict resolution for LabelView labels. + +Applies the configured ConflictPolicy to a DataFrame containing all historical +label rows (from pull_all_from_table_or_query), producing one resolved row per +entity key. + +The online store continues to use LAST_WRITE_WINS regardless of policy — this +resolver is for offline/batch reads used in training data generation, the UI +browse/quality endpoints, and any batch pipeline consuming resolved labels. +""" + +from typing import List, Optional + +import pandas as pd + +from feast.labeling.conflict_policy import ConflictPolicy + + +def resolve_conflicts( + df: pd.DataFrame, + join_key_columns: List[str], + feature_name_columns: List[str], + timestamp_field: str, + labeler_field: str, + conflict_policy: ConflictPolicy, + labeler_priorities: Optional[List[str]] = None, +) -> pd.DataFrame: + """Resolve label conflicts by applying the configured policy. + + Args: + df: Full history DataFrame (all rows, not deduplicated). + join_key_columns: Entity key column names. + feature_name_columns: Label/feature column names. + timestamp_field: Event timestamp column name. + labeler_field: Column identifying who wrote the label. + conflict_policy: The resolution strategy to apply. + labeler_priorities: Ordered list of labelers from highest to lowest + priority. Required for LABELER_PRIORITY policy. + + Returns: + DataFrame with one resolved row per unique entity key combination. + """ + if df.empty: + return df + + if conflict_policy == ConflictPolicy.LAST_WRITE_WINS: + return _resolve_last_write_wins(df, join_key_columns, timestamp_field) + elif conflict_policy == ConflictPolicy.LABELER_PRIORITY: + return _resolve_labeler_priority( + df, join_key_columns, timestamp_field, labeler_field, labeler_priorities + ) + elif conflict_policy == ConflictPolicy.MAJORITY_VOTE: + return _resolve_majority_vote( + df, join_key_columns, feature_name_columns, timestamp_field + ) + else: + return _resolve_last_write_wins(df, join_key_columns, timestamp_field) + + +def _resolve_last_write_wins( + df: pd.DataFrame, + join_key_columns: List[str], + timestamp_field: str, +) -> pd.DataFrame: + """Keep only the row with the latest timestamp per entity.""" + df_sorted = df.sort_values(timestamp_field, ascending=False) + return df_sorted.drop_duplicates(subset=join_key_columns, keep="first").reset_index( + drop=True + ) + + +def _resolve_labeler_priority( + df: pd.DataFrame, + join_key_columns: List[str], + timestamp_field: str, + labeler_field: str, + labeler_priorities: Optional[List[str]] = None, +) -> pd.DataFrame: + """Pick the label from the highest-priority labeler per entity. + + If multiple rows exist from the same priority labeler, the latest timestamp + wins. Labelers not in the priority list are ranked lowest. + """ + if not labeler_priorities: + return _resolve_last_write_wins(df, join_key_columns, timestamp_field) + + priority_map = {name: i for i, name in enumerate(labeler_priorities)} + max_priority = len(labeler_priorities) + + df = df.copy() + df["_priority_rank"] = df[labeler_field].map( + lambda x: priority_map.get(x, max_priority) + ) + df_sorted = df.sort_values( + ["_priority_rank", timestamp_field], ascending=[True, False] + ) + result = df_sorted.drop_duplicates(subset=join_key_columns, keep="first") + result = result.drop(columns=["_priority_rank"]) + return result.reset_index(drop=True) + + +def _resolve_majority_vote( + df: pd.DataFrame, + join_key_columns: List[str], + feature_name_columns: List[str], + timestamp_field: str, +) -> pd.DataFrame: + """For each entity, pick the most common value per feature column. + + Uses the most frequent value across all labelers for each feature. + Ties are broken by recency (latest timestamp wins). + """ + if not feature_name_columns: + return _resolve_last_write_wins(df, join_key_columns, timestamp_field) + + groups = df.groupby(join_key_columns, sort=False) + resolved_rows = [] + + for keys, group in groups: + if not isinstance(keys, tuple): + keys = (keys,) + row = dict(zip(join_key_columns, keys)) + + for col in feature_name_columns: + value_counts = group[col].value_counts() + if value_counts.empty: + row[col] = None + continue + top_value = value_counts.index[0] + top_count = value_counts.iloc[0] + + # Tie-breaking: if multiple values have the same count, pick the + # one with the most recent timestamp + tied = value_counts[value_counts == top_count] + if len(tied) > 1: + tied_values = tied.index.tolist() + tied_rows = group[group[col].isin(tied_values)] + latest_row = tied_rows.sort_values( + timestamp_field, ascending=False + ).iloc[0] + row[col] = latest_row[col] + else: + row[col] = top_value + + row[timestamp_field] = group[timestamp_field].max() + if "labeler" in group.columns and "labeler" not in join_key_columns: + row["labeler"] = "majority_vote" + + resolved_rows.append(row) + + if not resolved_rows: + return df.head(0) + + result = pd.DataFrame(resolved_rows) + # Preserve column order from original + cols = [c for c in df.columns if c in result.columns] + return result[cols].reset_index(drop=True) diff --git a/sdk/python/feast/labeling/label_view.py b/sdk/python/feast/labeling/label_view.py new file mode 100644 index 00000000000..754417cc290 --- /dev/null +++ b/sdk/python/feast/labeling/label_view.py @@ -0,0 +1,446 @@ +import copy +import warnings +from datetime import timedelta +from typing import Any, Dict, List, Optional, Type + +from google.protobuf.duration_pb2 import Duration +from google.protobuf.message import Message +from typeguard import typechecked + +from feast.base_feature_view import BaseFeatureView +from feast.data_source import DataSource +from feast.entity import Entity +from feast.feature_view_projection import FeatureViewProjection +from feast.field import Field +from feast.labeling.conflict_policy import ConflictPolicy +from feast.proto_utils import serialize_data_source +from feast.protos.feast.core.LabelView_pb2 import LabelView as LabelViewProto +from feast.protos.feast.core.LabelView_pb2 import LabelViewMeta as LabelViewMetaProto +from feast.protos.feast.core.LabelView_pb2 import LabelViewSpec as LabelViewSpecProto +from feast.types import String as FeastString +from feast.types import from_value_type +from feast.value_type import ValueType + +warnings.simplefilter("once", DeprecationWarning) + + +@typechecked +class LabelView(BaseFeatureView): + """A LabelView manages mutable labels decoupled from immutable feature data. + + A LabelView defines a mutable set of labels or annotations that are kept + separate from the immutable feature data stored in regular FeatureViews. + It supports multi-labeler workflows where different sources (human reviewers, + automated safety scanners, reward models) can independently write labels for + the same entity keys. + + .. note:: + + **Enforcement scope:** + + - ``conflict_policy`` is enforced for **offline store reads** (training + data generation, UI browse/quality endpoints, batch pipelines). The + online store always uses LAST_WRITE_WINS for low-latency serving. + - The offline store always retains full write history (all writes are + appended). The online store keeps only the latest value per entity key. + + Attributes: + name: The unique name of the label view. + entities: The list of entity names associated with this label view. + ttl: How long labels are valid for online serving. ``timedelta(0)`` + means labels never expire. + source: The data source (typically a ``PushSource``) feeding label data. + entity_columns: The entity key columns in the schema. + features: The label columns (non-entity fields in the schema). + online: Whether labels are served from the online store. + description: A human-readable description. + tags: Arbitrary key-value metadata. + owner: Owner email or identifier. + labeler_field: Name of the schema field that identifies who wrote the + label (default ``"labeler"``). + conflict_policy: How conflicting labels from different labelers are + resolved (default ``ConflictPolicy.LAST_WRITE_WINS``). Enforced for + offline store reads (training, UI). Online store uses LAST_WRITE_WINS. + reference_feature_view: Optional name of the ``FeatureView`` whose + entities this label view annotates. + """ + + name: str + entities: List[str] + ttl: Optional[timedelta] + source: Optional[DataSource] + entity_columns: List[Field] + features: List[Field] + online: bool + description: str + tags: Dict[str, str] + owner: str + + labeler_field: str + conflict_policy: ConflictPolicy + reference_feature_view: Optional[str] + + def __init__( + self, + *, + name: str, + source: Optional[DataSource] = None, + schema: Optional[List[Field]] = None, + entities: Optional[List[Entity]] = None, + ttl: Optional[timedelta] = timedelta(days=0), + online: bool = True, + description: str = "", + tags: Optional[Dict[str, str]] = None, + owner: str = "", + labeler_field: str = "labeler", + conflict_policy: ConflictPolicy = ConflictPolicy.LAST_WRITE_WINS, + reference_feature_view: Optional[str] = None, + ): + """Creates a LabelView object. + + Args: + name: The unique name of this label view. + source: The data source for ingesting labels, typically a + ``PushSource``. If ``None``, labels can only be written + programmatically via ``FeatureStore.push()``. + schema: The list of ``Field`` objects describing both entity + columns and label columns. Entity columns are identified + by matching against entity join keys. + entities: The list of ``Entity`` objects whose join keys are + used to key the labels. + ttl: The time-to-live for labels in the online store. + ``timedelta(0)`` means labels never expire. ``None`` means + the label view inherits the default TTL. + online: Whether this label view should be materialized to the + online store for low-latency serving. + description: A human-readable description of what the labels + represent. + tags: A dictionary of key-value pairs for arbitrary metadata. + owner: The owner of this label view, typically an email address. + labeler_field: The name of the field in the schema that + identifies the labeler. Defaults to ``"labeler"``. + conflict_policy: The policy for resolving conflicting labels + from different labelers. Defaults to + ``ConflictPolicy.LAST_WRITE_WINS``. Enforced for offline + store reads (training, UI). Online store uses LAST_WRITE_WINS. + reference_feature_view: The name of the ``FeatureView`` whose + entities this label view annotates. This is informational + and does not create a hard dependency. + """ + self.ttl = ttl + self.entities = [] + self.source = source + + schema = schema or [] + + features: List[Field] = [] + self.entity_columns = [] + + join_keys: List[str] = [] + if entities: + for entity in entities: + join_keys.append(entity.join_key) + if entity.name != entity.join_key: + self.entities.append(entity.name) + else: + self.entities.append(entity.name) + + if len(set(join_keys)) < len(join_keys): + raise ValueError( + "A label view should not have entities that share a join key." + ) + + for field in schema: + if field.name in join_keys: + self.entity_columns.append(field) + matching_entities = ( + [e for e in entities if e.join_key == field.name] + if entities + else [] + ) + if matching_entities: + entity = matching_entities[0] + if entity.value_type != ValueType.UNKNOWN: + if from_value_type(entity.value_type) != field.dtype: + raise ValueError( + f"Entity {entity.name} has type {entity.value_type}, " + f"which does not match the inferred type {field.dtype}." + ) + else: + features.append(field) + + existing_entity_col_names = {ec.name for ec in self.entity_columns} + for jk in join_keys: + if jk not in existing_entity_col_names: + matching = [e for e in entities if e.join_key == jk] if entities else [] + if matching and matching[0].value_type != ValueType.UNKNOWN: + dtype = from_value_type(matching[0].value_type) + else: + dtype = FeastString + self.entity_columns.append(Field(name=jk, dtype=dtype)) + + self.labeler_field = labeler_field + self.conflict_policy = conflict_policy + self.reference_feature_view = reference_feature_view or "" + + super().__init__( + name=name, + features=features, + description=description, + tags=tags, + owner=owner, + source=source, + ) + self.projection.view_type = "labelView" + self.online = online + + def __hash__(self): + return super().__hash__() + + def __copy__(self): + lv = LabelView( + name=self.name, + ttl=self.ttl, + source=self.source, + schema=self.schema, + tags=self.tags, + online=self.online, + description=self.description, + owner=self.owner, + labeler_field=self.labeler_field, + conflict_policy=self.conflict_policy, + reference_feature_view=self.reference_feature_view or None, + ) + lv.entities = list(self.entities) + lv.features = copy.copy(self.features) + lv.entity_columns = copy.copy(self.entity_columns) + lv.projection = copy.copy(self.projection) + lv.version = self.version + lv.current_version_number = self.current_version_number + return lv + + def __eq__(self, other): + if not isinstance(other, LabelView): + return False + + if not super().__eq__(other): + return False + + if ( + sorted(self.entities) != sorted(other.entities) + or self.ttl != other.ttl + or self.online != other.online + or sorted(self.entity_columns) != sorted(other.entity_columns) + or self.labeler_field != other.labeler_field + or self.conflict_policy != other.conflict_policy + or self.reference_feature_view != other.reference_feature_view + ): + return False + + return True + + @property + def join_keys(self) -> List[str]: + """The entity join key column names for this label view.""" + return [ec.name for ec in self.entity_columns] + + @property + def schema(self) -> List[Field]: + """The full schema including both entity columns and label columns.""" + return list(set(self.entity_columns + self.features)) + + @property + def batch_source(self) -> Optional[DataSource]: + """The batch data source for this label view. + + If the source is a ``PushSource``, returns its underlying + ``batch_source``. Otherwise returns the source directly. + This property enables compatibility with offline store + ``get_historical_features`` implementations. + """ + from feast.data_source import PushSource + + if self.source is None: + return None + if isinstance(self.source, PushSource): + return self.source.batch_source + return self.source + + @property + def stream_source(self) -> Optional[DataSource]: + """The stream data source for this label view. + + Returns the source if it is a ``PushSource``, ``None`` otherwise. + """ + from feast.data_source import PushSource + + if self.source is not None and isinstance(self.source, PushSource): + return self.source + return None + + # --- Labeling method helpers (parsed from tags) --- + + _TAG_PREFIX_PROFILE = "feast.io/labeling-method" + _TAG_PREFIX_ROLE = "feast.io/field-role:" + _TAG_PREFIX_VALUES = "feast.io/label-values:" + _TAG_PREFIX_WIDGET = "feast.io/label-widget:" + + @property + def labeling_method(self) -> str: + """The labeling method for this label view. + + Parsed from the ``feast.io/labeling-method`` tag. Supported + methods: ``table`` (default), ``document-span``, + ``entity-form``, ``active-learning``. + """ + return self.tags.get(self._TAG_PREFIX_PROFILE, "table") + + @property + def annotation_config(self) -> Dict[str, Any]: + """Structured annotation configuration derived from tags. + + Returns a dict with:: + + { + "profile": "document-span", + "field_roles": {"source_document": "content_ref", ...}, + "label_values": {"relevance": ["relevant", "irrelevant"]}, + "label_widgets": {"relevance": "binary", ...}, + } + """ + field_roles: Dict[str, str] = {} + label_values: Dict[str, List[str]] = {} + label_widgets: Dict[str, str] = {} + + for key, value in self.tags.items(): + if key.startswith(self._TAG_PREFIX_ROLE): + field_name = key[len(self._TAG_PREFIX_ROLE) :] + field_roles[field_name] = value + elif key.startswith(self._TAG_PREFIX_VALUES): + field_name = key[len(self._TAG_PREFIX_VALUES) :] + label_values[field_name] = [v.strip() for v in value.split(",")] + elif key.startswith(self._TAG_PREFIX_WIDGET): + field_name = key[len(self._TAG_PREFIX_WIDGET) :] + label_widgets[field_name] = value + + return { + "profile": self.labeling_method, + "field_roles": field_roles, + "label_values": label_values, + "label_widgets": label_widgets, + } + + def ensure_valid(self): + """Validates the label view configuration. + + Raises: + ValueError: If the label view has no name (from ``BaseFeatureView``) + or no entities. + """ + super().ensure_valid() + if not self.entities: + raise ValueError("Label view has no entities.") + + @property + def proto_class(self) -> Type[Message]: + """The protobuf message class for LabelView.""" + return LabelViewProto + + def to_proto(self) -> LabelViewProto: + """Converts this LabelView to its protobuf representation. + + Returns: + A ``LabelViewProto`` message with the spec and metadata populated. + """ + meta = LabelViewMetaProto() + if self.created_timestamp: + meta.created_timestamp.FromDatetime(self.created_timestamp) + if self.last_updated_timestamp: + meta.last_updated_timestamp.FromDatetime(self.last_updated_timestamp) + + ttl_duration = None + if self.ttl is not None: + ttl_duration = Duration() + ttl_duration.FromTimedelta(self.ttl) + + source_proto = serialize_data_source(self.source) + + spec = LabelViewSpecProto( + name=self.name, + entities=self.entities, + entity_columns=[field.to_proto() for field in self.entity_columns], + features=[feature.to_proto() for feature in self.features], + description=self.description, + tags=self.tags, + owner=self.owner, + ttl=(ttl_duration if ttl_duration is not None else None), + online=self.online, + source=source_proto, + labeler_field=self.labeler_field, + conflict_policy=self.conflict_policy.to_proto(), # type: ignore[arg-type] + reference_feature_view=self.reference_feature_view or "", + ) + + return LabelViewProto(spec=spec, meta=meta) + + @classmethod + def from_proto(cls, label_view_proto: LabelViewProto) -> "LabelView": + """Creates a LabelView from a protobuf representation. + + Args: + label_view_proto: A ``LabelViewProto`` message to deserialize. + + Returns: + A ``LabelView`` instance populated from the protobuf data. + """ + source = ( + DataSource.from_proto(label_view_proto.spec.source) + if label_view_proto.spec.HasField("source") + else None + ) + + label_view = cls( + name=label_view_proto.spec.name, + description=label_view_proto.spec.description, + tags=dict(label_view_proto.spec.tags), + owner=label_view_proto.spec.owner, + online=label_view_proto.spec.online, + ttl=( + timedelta(days=0) + if label_view_proto.spec.ttl.ToNanoseconds() == 0 + else label_view_proto.spec.ttl.ToTimedelta() + ), + source=source, + labeler_field=label_view_proto.spec.labeler_field or "labeler", + conflict_policy=ConflictPolicy.from_proto( + label_view_proto.spec.conflict_policy + ), + reference_feature_view=( + label_view_proto.spec.reference_feature_view or None + ), + ) + + label_view.entities = list(label_view_proto.spec.entities) + + label_view.features = [ + Field.from_proto(field_proto) + for field_proto in label_view_proto.spec.features + ] + label_view.entity_columns = [ + Field.from_proto(field_proto) + for field_proto in label_view_proto.spec.entity_columns + ] + + label_view.projection = FeatureViewProjection.from_definition(label_view) + label_view.projection.view_type = "labelView" + + if label_view_proto.meta.HasField("created_timestamp"): + label_view.created_timestamp = ( + label_view_proto.meta.created_timestamp.ToDatetime() + ) + if label_view_proto.meta.HasField("last_updated_timestamp"): + label_view.last_updated_timestamp = ( + label_view_proto.meta.last_updated_timestamp.ToDatetime() + ) + + return label_view diff --git a/sdk/python/feast/lineage/registry_lineage.py b/sdk/python/feast/lineage/registry_lineage.py index 9d11ab70d53..ce038f4d1ae 100644 --- a/sdk/python/feast/lineage/registry_lineage.py +++ b/sdk/python/feast/lineage/registry_lineage.py @@ -17,6 +17,7 @@ class FeastObjectType(Enum): DATA_SOURCE = "dataSource" ENTITY = "entity" FEATURE_VIEW = "featureView" + LABEL_VIEW = "labelView" FEATURE_SERVICE = "featureService" FEATURE = "feature" @@ -82,7 +83,12 @@ def _parse_direct_relationships(self, registry: Registry) -> List[EntityRelation """Parse direct relationships between objects.""" relationships = [] - # FeatureService -> FeatureView relationships + # FeatureService -> FeatureView/LabelView relationships + label_view_names = { + lv.spec.name + for lv in registry.label_views + if hasattr(lv, "spec") and lv.spec + } for feature_service in registry.feature_services: if ( hasattr(feature_service, "spec") @@ -90,10 +96,18 @@ def _parse_direct_relationships(self, registry: Registry) -> List[EntityRelation and feature_service.spec.features ): for feature in feature_service.spec.features: + view_type_str = getattr(feature, "view_type", "") + is_label_view = ( + view_type_str == "labelView" + or feature.feature_view_name in label_view_names + ) + source_type = ( + FeastObjectType.LABEL_VIEW + if is_label_view + else FeastObjectType.FEATURE_VIEW + ) rel = EntityRelation( - source=EntityReference( - FeastObjectType.FEATURE_VIEW, feature.feature_view_name - ), + source=EntityReference(source_type, feature.feature_view_name), target=EntityReference( FeastObjectType.FEATURE_SERVICE, feature_service.spec.name, @@ -307,6 +321,75 @@ def _parse_direct_relationships(self, registry: Registry) -> List[EntityRelation ) ) + # LabelView relationships + for label_view in registry.label_views: + if hasattr(label_view, "spec") and label_view.spec: + # Entity relationships + if hasattr(label_view.spec, "entities"): + for entity_name in label_view.spec.entities: + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.ENTITY, entity_name + ), + target=EntityReference( + FeastObjectType.LABEL_VIEW, label_view.spec.name + ), + ) + ) + + # Data source relationships: LabelView uses spec.source (PushSource) + # which contains a nested batch_source + if hasattr(label_view.spec, "source") and label_view.spec.source: + source = label_view.spec.source + # Link to the push source itself + if hasattr(source, "name") and source.name: + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.DATA_SOURCE, source.name + ), + target=EntityReference( + FeastObjectType.LABEL_VIEW, label_view.spec.name + ), + ) + ) + # Link to the nested batch source + if ( + hasattr(source, "batch_source") + and source.batch_source + and hasattr(source.batch_source, "name") + and source.batch_source.name + ): + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.DATA_SOURCE, + source.batch_source.name, + ), + target=EntityReference( + FeastObjectType.LABEL_VIEW, label_view.spec.name + ), + ) + ) + elif ( + hasattr(label_view.spec, "batch_source") + and label_view.spec.batch_source + and hasattr(label_view.spec.batch_source, "name") + and label_view.spec.batch_source.name + ): + relationships.append( + EntityRelation( + source=EntityReference( + FeastObjectType.DATA_SOURCE, + label_view.spec.batch_source.name, + ), + target=EntityReference( + FeastObjectType.LABEL_VIEW, label_view.spec.name + ), + ) + ) + return relationships def _parse_indirect_relationships( @@ -325,12 +408,16 @@ def _parse_indirect_relationships( ): for feature in feature_service.spec.features: if hasattr(feature, "feature_view_name"): - # Find all relationships that target this feature view + # Find all relationships that target this feature view or label view related_sources = [ rel.source for rel in direct_relationships if rel.target.name == feature.feature_view_name - and rel.target.type == FeastObjectType.FEATURE_VIEW + and rel.target.type + in ( + FeastObjectType.FEATURE_VIEW, + FeastObjectType.LABEL_VIEW, + ) ] # Create indirect relationships to the feature service @@ -345,25 +432,24 @@ def _parse_indirect_relationships( ) ) - # Create Entity -> DataSource relationships (through feature views) - # Build a map of feature view -> data sources + # Create Entity -> DataSource relationships (through feature views and label views) + # Build a map of view -> data sources feature_view_to_data_sources: Dict[str, List[str]] = {} for rel in direct_relationships: - if ( - rel.source.type == FeastObjectType.DATA_SOURCE - and rel.target.type == FeastObjectType.FEATURE_VIEW + if rel.source.type == FeastObjectType.DATA_SOURCE and rel.target.type in ( + FeastObjectType.FEATURE_VIEW, + FeastObjectType.LABEL_VIEW, ): if rel.target.name not in feature_view_to_data_sources: feature_view_to_data_sources[rel.target.name] = [] feature_view_to_data_sources[rel.target.name].append(rel.source.name) - # For each Entity -> FeatureView relationship, create Entity -> DataSource relationships + # For each Entity -> FeatureView/LabelView relationship, create Entity -> DataSource relationships for rel in direct_relationships: - if ( - rel.source.type == FeastObjectType.ENTITY - and rel.target.type == FeastObjectType.FEATURE_VIEW + if rel.source.type == FeastObjectType.ENTITY and rel.target.type in ( + FeastObjectType.FEATURE_VIEW, + FeastObjectType.LABEL_VIEW, ): - # Find data sources that this feature view uses if rel.target.name in feature_view_to_data_sources: for data_source_name in feature_view_to_data_sources[ rel.target.name diff --git a/sdk/python/feast/loaders/yaml.py b/sdk/python/feast/loaders/yaml.py deleted file mode 100644 index 624bc47d49c..00000000000 --- a/sdk/python/feast/loaders/yaml.py +++ /dev/null @@ -1,77 +0,0 @@ -import yaml - - -def yaml_loader(yml, load_single=False): - """ - Loads one or more Feast resources from a YAML path or string. Multiple resources - can be divided by three hyphens '---' - - Args: - yml: A path ending in .yaml or .yml, or a YAML string - load_single: Expect only a single YAML resource, fail otherwise - - Returns: - Either a single YAML dictionary or a list of YAML dictionaries - - """ - - yml_content = _get_yaml_contents(yml) - yaml_strings = yml_content.strip("---").split("---") - - # Return a single resource dict - if load_single: - if len(yaml_strings) > 1: - raise Exception( - f"More than one YAML file is being loaded when only a single file is supported: ${yaml_strings}" - ) - return _yaml_to_dict(yaml_strings[0]) - - # Return a list of resource dicts - resources = [] - for yaml_string in yaml_strings: - resources.append(_yaml_to_dict(yaml_string)) - return resources - - -def _get_yaml_contents(yml: str) -> str: - """ - Returns the YAML contents from an object. If a path ending with .yaml or - .yml is passed, it will be read for its contents. If a string containing - YAML is passed, that will be returned. - - Args: - yml: Path of YAML file or string containing YAML - - Returns: - String object containing YAML - """ - if ( - isinstance(yml, str) - and yml.count("\n") == 0 - and (".yaml" in yml.lower() or ".yml" in yml.lower()) - ): - with open(yml, "r") as f: - yml_content = f.read() - - elif isinstance(yml, str): - yml_content = yml - else: - raise Exception( - f"Invalid YAML provided. Please provide either a file path or YAML string.\n" - f"Provided YAML: {yml}" - ) - return yml_content - - -def _yaml_to_dict(yaml_string): - """ - Converts a yaml string to dictionary - - Args: - yaml_string: String containing YAML - - Returns: - Dictionary containing the same object - """ - - return yaml.safe_load(yaml_string) diff --git a/sdk/python/feast/metrics.py b/sdk/python/feast/metrics.py new file mode 100644 index 00000000000..85c0ceadabe --- /dev/null +++ b/sdk/python/feast/metrics.py @@ -0,0 +1,655 @@ +# Copyright 2025 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Centralized Prometheus metrics for the Feast feature server. + +All metrics are defined here to provide a single source of truth. +Instrumentation is **opt-in**: metric recording is gated behind a +``_config`` object whose flags are only set when +``start_metrics_server()`` is called (i.e. when the feature server is +started with ``--metrics`` or ``metrics.enabled: true`` in the YAML). + +Each metric category can be individually toggled via the ``metrics`` +sub-block in ``feature_store.yaml``. When disabled, helpers +short-circuit with a fast attribute check and do zero work. + +Multiprocess support +-------------------- +Gunicorn pre-forks worker processes, so every worker gets its own copy +of the in-process metric state. To aggregate across workers we use +``prometheus_client``'s multiprocess mode: + +1. ``PROMETHEUS_MULTIPROCESS_DIR`` is set (to a temp dir if the user + has not already set it) **before** any metric objects are created. +2. Gauges specify ``multiprocess_mode`` so they aggregate correctly. +3. The metrics HTTP server uses ``MultiProcessCollector`` to read all + workers' metric files. +4. Gunicorn hooks (``post_worker_init``, ``child_exit``) are wired up + in ``feature_server.py`` to start per-worker monitoring and to + clean up dead-worker files. +""" + +import atexit +import json +import logging +import os +import shutil +import tempfile +import threading +import time +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import TYPE_CHECKING, List, Optional + +import psutil + +if TYPE_CHECKING: + from feast.feature_store import FeatureStore + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Multiprocess directory setup — MUST happen before prometheus_client import +# so that metric values are stored in shared files rather than in-process +# memory (required for Gunicorn pre-fork workers). +# --------------------------------------------------------------------------- +_prometheus_mp_dir: Optional[str] = None +_owns_mp_dir: bool = False +_owner_pid: Optional[int] = None + +if "PROMETHEUS_MULTIPROCESS_DIR" not in os.environ: + _prometheus_mp_dir = tempfile.mkdtemp(prefix="feast_metrics_") + os.environ["PROMETHEUS_MULTIPROCESS_DIR"] = _prometheus_mp_dir + _owns_mp_dir = True + _owner_pid = os.getpid() +else: + _prometheus_mp_dir = os.environ["PROMETHEUS_MULTIPROCESS_DIR"] + +# prometheus_client uses two different env var names: +# - PROMETHEUS_MULTIPROCESS_DIR (for value storage in prometheus_client.values) +# - PROMETHEUS_MULTIPROC_DIR (for MultiProcessCollector) +# Both must point to the same directory. +if "PROMETHEUS_MULTIPROC_DIR" not in os.environ: + os.environ["PROMETHEUS_MULTIPROC_DIR"] = _prometheus_mp_dir + + +def _cleanup_multiprocess_dir(): + # Only the process that created the directory may remove it. + # Forked Gunicorn workers inherit _owns_mp_dir=True but have a + # different PID; letting them delete the shared directory would + # break metrics for sibling workers and the metrics HTTP server. + if ( + _owns_mp_dir + and _owner_pid == os.getpid() + and _prometheus_mp_dir + and os.path.isdir(_prometheus_mp_dir) + ): + shutil.rmtree(_prometheus_mp_dir, ignore_errors=True) + + +atexit.register(_cleanup_multiprocess_dir) + +# Now safe to import prometheus_client — it will detect the env var. +from prometheus_client import Counter, Gauge, Histogram # noqa: E402 + + +# --------------------------------------------------------------------------- +# Per-category runtime flags +# --------------------------------------------------------------------------- +@dataclass +class _MetricsFlags: + """Runtime toggle for each metric category. + + All flags default to ``False`` (disabled). ``start_metrics_server`` + flips them on according to the user's ``MetricsConfig``. + """ + + enabled: bool = False + resource: bool = False + request: bool = False + online_features: bool = False + push: bool = False + materialization: bool = False + freshness: bool = False + offline_features: bool = False + audit_logging: bool = False + + +_config = _MetricsFlags() + + +def build_metrics_flags(metrics_config: Optional[object] = None) -> _MetricsFlags: + """Build ``_MetricsFlags`` from a ``MetricsConfig`` object. + + If *metrics_config* is ``None`` (e.g. metrics activated purely via + ``--metrics`` CLI with no YAML block), every category defaults to + enabled. Otherwise the per-category booleans are respected. + """ + if metrics_config is None: + return _MetricsFlags( + enabled=True, + resource=True, + request=True, + online_features=True, + push=True, + materialization=True, + freshness=True, + offline_features=True, + audit_logging=False, + ) + return _MetricsFlags( + enabled=True, + resource=getattr(metrics_config, "resource", True), + request=getattr(metrics_config, "request", True), + online_features=getattr(metrics_config, "online_features", True), + push=getattr(metrics_config, "push", True), + materialization=getattr(metrics_config, "materialization", True), + freshness=getattr(metrics_config, "freshness", True), + offline_features=getattr(metrics_config, "offline_features", True), + audit_logging=getattr(metrics_config, "audit_logging", False), + ) + + +# --------------------------------------------------------------------------- +# Resource metrics — multiprocess_mode="liveall" so each live worker +# reports its own CPU/memory with a ``pid`` label. +# --------------------------------------------------------------------------- +cpu_usage_gauge = Gauge( + "feast_feature_server_cpu_usage", + "CPU usage percentage of the Feast feature server process", + multiprocess_mode="liveall", +) +memory_usage_gauge = Gauge( + "feast_feature_server_memory_usage", + "Memory usage percentage of the Feast feature server process", + multiprocess_mode="liveall", +) + +# --------------------------------------------------------------------------- +# HTTP request metrics (Counters & Histograms aggregate automatically) +# --------------------------------------------------------------------------- +request_count = Counter( + "feast_feature_server_request_total", + "Total number of requests to the Feast feature server", + ["endpoint", "status"], +) +request_latency = Histogram( + "feast_feature_server_request_latency_seconds", + "Latency of requests to the Feast feature server in seconds", + ["endpoint", "feature_count", "feature_view_count"], + buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0), +) + +# --------------------------------------------------------------------------- +# Online feature retrieval metrics +# --------------------------------------------------------------------------- +online_features_request_count = Counter( + "feast_online_features_request_total", + "Total online feature retrieval requests", +) +online_features_entity_count = Histogram( + "feast_online_features_entity_count", + "Number of entity rows per online feature request", + buckets=(1, 5, 10, 25, 50, 100, 250, 500, 1000), +) +online_features_status_total = Counter( + "feast_online_features_status_total", + "Count of individual feature values by retrieval status per feature view", + ["feature_view", "status"], +) + +# --------------------------------------------------------------------------- +# Push / write metrics +# --------------------------------------------------------------------------- +push_request_count = Counter( + "feast_push_request_total", + "Total push requests to the feature store", + ["push_source", "mode"], +) + +# --------------------------------------------------------------------------- +# Materialization metrics +# --------------------------------------------------------------------------- +materialization_result_total = Counter( + "feast_materialization_result_total", + "Total materialization runs per feature view", + ["feature_view", "status"], +) +materialization_duration_seconds = Histogram( + "feast_materialization_duration_seconds", + "Duration of materialization per feature view in seconds", + ["feature_view"], + buckets=(1.0, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0, 600.0, 1800.0, 3600.0), +) + +# --------------------------------------------------------------------------- +# Sub-request timing — online store reads and ODFV transformations +# --------------------------------------------------------------------------- +online_store_read_duration_seconds = Histogram( + "feast_feature_server_online_store_read_duration_seconds", + "Duration of the online store read phase in seconds (covers all table reads including parallel async)", + buckets=(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0), +) +transformation_duration_seconds = Histogram( + "feast_feature_server_transformation_duration_seconds", + "Duration of on-demand feature view transformations on read in seconds", + ["odfv_name", "mode"], + buckets=(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0), +) +write_transformation_duration_seconds = Histogram( + "feast_feature_server_write_transformation_duration_seconds", + "Duration of on-demand feature view transformations on write in seconds", + ["odfv_name", "mode"], + buckets=(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0), +) + +# --------------------------------------------------------------------------- +# Feature freshness metrics — "max" shows the worst-case staleness across +# processes (freshness is identical regardless of which process computes it). +# --------------------------------------------------------------------------- +feature_freshness_seconds = Gauge( + "feast_feature_freshness_seconds", + "Seconds since the most recent materialization end time per feature view", + ["feature_view", "project"], + multiprocess_mode="max", +) + +# --------------------------------------------------------------------------- +# Offline store retrieval metrics +# --------------------------------------------------------------------------- +offline_store_request_total = Counter( + "feast_offline_store_request_total", + "Total offline store retrieval requests", + ["method", "status"], +) +offline_store_request_latency_seconds = Histogram( + "feast_offline_store_request_latency_seconds", + "Latency of offline store retrieval operations in seconds", + ["method"], + buckets=(0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0, 600.0), +) +offline_store_row_count = Histogram( + "feast_offline_store_row_count", + "Number of rows returned by offline store retrieval", + ["method"], + buckets=(100, 1000, 10000, 100000, 500000, 1000000, 5000000), +) + +# --------------------------------------------------------------------------- +# Audit logger — separate from the main feast logger so operators can +# route SOX-style audit entries to a dedicated sink. +# --------------------------------------------------------------------------- +audit_logger = logging.getLogger("feast.audit") + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class RequestMetricsContext: + """Mutable label holder yielded by :func:`track_request_latency`. + + Callers that need to resolve labels *inside* the ``with`` block + (e.g. ``/get-online-features`` where the feature count is only + known after ``_get_features`` succeeds) can set the attributes + on the yielded object and they will be picked up in ``finally``. + """ + + __slots__ = ("feature_count", "feature_view_count") + + def __init__(self, feature_count: str = "", feature_view_count: str = ""): + self.feature_count = feature_count + self.feature_view_count = feature_view_count + + +@contextmanager +def track_request_latency( + endpoint: str, feature_count: str = "", feature_view_count: str = "" +): + """Context manager that records endpoint latency and increments request count. + + Yields a :class:`RequestMetricsContext` whose ``feature_count`` and + ``feature_view_count`` attributes can be updated inside the block. + The final values are used when recording the histogram and counter + in ``finally``, so labels are accurate even when they depend on + work done inside the block. + + Gated by the ``request`` category flag. + """ + ctx = RequestMetricsContext(feature_count, feature_view_count) + if not _config.request: + yield ctx + return + + start = time.monotonic() + status_label = "success" + try: + yield ctx + except Exception: + status_label = "error" + raise + finally: + elapsed = time.monotonic() - start + request_latency.labels( + endpoint=endpoint, + feature_count=ctx.feature_count, + feature_view_count=ctx.feature_view_count, + ).observe(elapsed) + request_count.labels(endpoint=endpoint, status=status_label).inc() + + +def track_online_features_entities(entity_count: int): + """Record the number of entity rows in an online feature request.""" + if not _config.online_features: + return + online_features_request_count.inc() + online_features_entity_count.observe(entity_count) + + +def track_push(push_source: str, mode: str): + """Increment the push request counter.""" + if not _config.push: + return + push_request_count.labels(push_source=push_source, mode=mode).inc() + + +def track_online_store_read(duration_seconds: float): + """Record the duration of the online store read phase.""" + if not _config.online_features: + return + online_store_read_duration_seconds.observe(duration_seconds) + + +def track_feature_statuses( + feature_view_name: str, present_count: int, not_found_count: int +): + """Record the number of PRESENT vs NOT_FOUND feature values for a feature view.""" + if not _config.online_features: + return + if present_count > 0: + online_features_status_total.labels( + feature_view=feature_view_name, status="present" + ).inc(present_count) + if not_found_count > 0: + online_features_status_total.labels( + feature_view=feature_view_name, status="not_found" + ).inc(not_found_count) + + +def track_transformation(odfv_name: str, mode: str, duration_seconds: float): + """Record the duration of an on-demand feature view read-path transformation.""" + if not _config.online_features: + return + transformation_duration_seconds.labels(odfv_name=odfv_name, mode=mode).observe( + duration_seconds + ) + + +def track_write_transformation(odfv_name: str, mode: str, duration_seconds: float): + """Record the duration of an on-demand feature view write-path transformation.""" + if not _config.online_features: + return + write_transformation_duration_seconds.labels( + odfv_name=odfv_name, mode=mode + ).observe(duration_seconds) + + +def track_materialization( + feature_view_name: str, success: bool, duration_seconds: float +): + """Record materialization outcome and duration for a single feature view.""" + if not _config.materialization: + return + status = "success" if success else "failure" + materialization_result_total.labels( + feature_view=feature_view_name, status=status + ).inc() + materialization_duration_seconds.labels(feature_view=feature_view_name).observe( + duration_seconds + ) + + +def emit_online_audit_log( + *, + requestor_id: str, + entity_keys: List[str], + entity_count: int, + feature_views: List[str], + feature_count: int, + status: str, + latency_ms: float, +): + """Emit a structured JSON audit log entry for an online feature request.""" + if not _config.audit_logging: + return + audit_logger.info( + _json_dumps( + { + "event": "online_feature_request", + "timestamp": datetime.now(tz=timezone.utc).isoformat(), + "requestor_id": requestor_id, + "entity_keys": entity_keys, + "entity_count": entity_count, + "feature_views": feature_views, + "feature_count": feature_count, + "status": status, + "latency_ms": round(latency_ms, 2), + } + ) + ) + + +def emit_offline_audit_log( + *, + method: str, + feature_views: List[str], + feature_count: int, + row_count: int, + status: str, + start_time: str, + end_time: str, + duration_ms: float, +): + """Emit a structured JSON audit log entry for an offline feature retrieval.""" + if not _config.audit_logging: + return + audit_logger.info( + _json_dumps( + { + "event": "offline_feature_retrieval", + "timestamp": datetime.now(tz=timezone.utc).isoformat(), + "method": method, + "start_time": start_time, + "end_time": end_time, + "feature_views": feature_views, + "feature_count": feature_count, + "row_count": row_count, + "status": status, + "duration_ms": round(duration_ms, 2), + } + ) + ) + + +def _json_dumps(obj: dict) -> str: + return json.dumps(obj, separators=(",", ":")) + + +def update_feature_freshness( + store: "FeatureStore", +) -> None: + """ + Compute and set the freshness gauge for every feature view in the registry. + + Freshness = now - most_recent_end_time (from materialization_intervals). + A higher value means the feature data is more stale. + """ + try: + feature_views = store.list_feature_views(allow_cache=True) + stream_feature_views = store.list_stream_feature_views(allow_cache=True) + all_views = feature_views + stream_feature_views + now = datetime.now(tz=timezone.utc) + for fv in all_views: + end_time = fv.most_recent_end_time + if end_time is not None: + if end_time.tzinfo is None: + end_time = end_time.replace(tzinfo=timezone.utc) + staleness = (now - end_time).total_seconds() + feature_freshness_seconds.labels( + feature_view=fv.name, project=store.project + ).set(staleness) + except Exception: + logger.debug("Failed to update feature freshness metrics", exc_info=True) + + +def monitor_resources(interval: int = 5): + """Background thread target that updates CPU and memory usage gauges.""" + logger.debug("Starting resource monitoring with interval %d seconds", interval) + p = psutil.Process() + logger.debug("PID is %d", p.pid) + while True: + with p.oneshot(): + cpu_usage = p.cpu_percent() + memory_usage = p.memory_percent() + logger.debug("CPU usage: %s%%, Memory usage: %s%%", cpu_usage, memory_usage) + cpu_usage_gauge.set(cpu_usage) + memory_usage_gauge.set(memory_usage) + time.sleep(interval) + + +def monitor_freshness(store: "FeatureStore", interval: int = 30): + """Background thread target that periodically updates feature freshness gauges.""" + logger.debug( + "Starting feature freshness monitoring with interval %d seconds", interval + ) + while True: + update_feature_freshness(store) + time.sleep(interval) + + +# --------------------------------------------------------------------------- +# Gunicorn multiprocess helpers +# --------------------------------------------------------------------------- + + +def mark_process_dead(pid: int): + """Clean up metric files for a dead Gunicorn worker. + + Called from the Gunicorn ``child_exit`` hook so that stale worker + data no longer appears in scraped output. + """ + if not _config.enabled: + return + try: + from prometheus_client import multiprocess + + multiprocess.mark_process_dead(pid) + except Exception: + logger.debug("Failed to mark process %d as dead", pid, exc_info=True) + + +def init_worker_monitoring(): + """Start resource monitoring inside a Gunicorn worker process. + + Called from the ``post_worker_init`` hook so that each worker + tracks its own CPU/memory independently of the master. + """ + if _config.resource: + t = threading.Thread(target=monitor_resources, args=(5,), daemon=True) + t.start() + + +def init_worker_freshness_monitoring(store: "FeatureStore"): + """Start feature-freshness monitoring inside a Gunicorn worker process. + + Called from the ``post_worker_init`` hook so that each worker starts + its own freshness monitoring after the fork, not before. + """ + if _config.freshness: + t = threading.Thread(target=monitor_freshness, args=(store, 30), daemon=True) + t.start() + + +def start_metrics_server( + store: "FeatureStore", + port: int = 8000, + metrics_config: Optional["_MetricsFlags"] = None, + start_resource_monitoring: bool = True, + start_freshness_monitoring: bool = True, +): + """ + Start the Prometheus metrics HTTP server and background monitoring threads. + + Uses ``MultiProcessCollector`` so that metrics from all Gunicorn + workers are correctly aggregated when Prometheus scrapes port *port*. + + Args: + store: The FeatureStore instance (used for freshness checks). + port: TCP port for the Prometheus HTTP endpoint. + metrics_config: Optional pre-built ``_MetricsFlags``. When + ``None`` every category defaults to **enabled**. + start_resource_monitoring: Whether to start the CPU/memory + monitoring thread. Set to ``False`` when Gunicorn will + fork workers — the ``post_worker_init`` hook starts + per-worker monitoring instead. + start_freshness_monitoring: Whether to start the feature-freshness + thread here. Set to ``False`` when Gunicorn will fork + workers — the ``post_worker_init`` hook starts per-worker + freshness monitoring instead. + """ + global _config + + if metrics_config is not None: + _config = metrics_config + else: + _config = _MetricsFlags( + enabled=True, + resource=True, + request=True, + online_features=True, + push=True, + materialization=True, + freshness=True, + offline_features=True, + audit_logging=False, + ) + + from prometheus_client import CollectorRegistry, make_wsgi_app + from prometheus_client.multiprocess import MultiProcessCollector + + registry = CollectorRegistry() + MultiProcessCollector(registry) + + from wsgiref.simple_server import make_server + + httpd = make_server("", port, make_wsgi_app(registry)) + metrics_thread = threading.Thread(target=httpd.serve_forever, daemon=True) + metrics_thread.start() + logger.info( + "Prometheus metrics server started on port %d (multiprocess-safe)", port + ) + + if _config.resource and start_resource_monitoring: + resource_thread = threading.Thread( + target=monitor_resources, args=(5,), daemon=True + ) + resource_thread.start() + + if _config.freshness and start_freshness_monitoring: + freshness_thread = threading.Thread( + target=monitor_freshness, args=(store, 30), daemon=True + ) + freshness_thread.start() diff --git a/sdk/python/feast/mlflow.py b/sdk/python/feast/mlflow.py new file mode 100644 index 00000000000..f6cc359ea8b --- /dev/null +++ b/sdk/python/feast/mlflow.py @@ -0,0 +1,139 @@ +""" +``feast.mlflow`` — drop-in replacement for ``import mlflow`` with Feast superpowers. + +Any function or attribute available on the ``mlflow`` module can be accessed +via ``feast.mlflow.*``. A subset of calls are **Feast-enhanced** with +automatic tagging, lineage tracking, and feature resolution: + +- ``start_run()`` — auto-tags runs with ``feast.project`` +- ``log_model()`` — auto-saves ``feast_features.json`` +- ``register_model()`` — auto-tags model versions with ``feast.feature_service`` +- ``load_model()`` — auto-links prediction runs to training runs +- ``resolve_features()`` — Feast-only: model URI → feature service name +- ``get_training_entity_df()`` — Feast-only: recover training entity data + +All other calls (``log_params``, ``log_metrics``, ``set_tag``, +``log_artifact``, ``MlflowClient``, etc.) pass through to the raw +``mlflow`` module unchanged. + +**Store resolution order** (first match wins): + +1. Explicit ``feast.mlflow.init(store)`` call +2. Most recently created ``FeatureStore`` (auto-registered) +3. ``FeatureStore(".")`` from the current working directory +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, Optional + +if TYPE_CHECKING: + from feast import FeatureStore + +_logger = logging.getLogger(__name__) + +_MISSING = object() + +_client: Optional[Any] = None +_registered_store: Optional["FeatureStore"] = None + + +def _register_store(store: "FeatureStore") -> None: + """Called by ``FeatureStore.__init__`` to auto-register itself. + + This is an internal API — end users should call :func:`init` instead. + """ + global _registered_store + _registered_store = store + + +def _build_client() -> Any: + """Create a ``FeastMlflowClient`` using the best available store.""" + from feast.mlflow_integration.client import FeastMlflowClient + + store = _registered_store + if store is None: + try: + from feast import FeatureStore + + store = FeatureStore(".") + except Exception as exc: + raise RuntimeError( + "feast.mlflow could not auto-discover a FeatureStore. " + "Either call feast.mlflow.init(store), create a FeatureStore " + "before using feast.mlflow, or ensure feature_store.yaml " + "exists in the current directory." + ) from exc + + mlflow_cfg = getattr(store.config, "mlflow", None) + if mlflow_cfg is None or not mlflow_cfg.enabled: + raise RuntimeError( + "MLflow integration is not enabled. " + "Set mlflow.enabled=true in feature_store.yaml, or call " + "feast.mlflow.init(store) with a store whose config has " + "mlflow.enabled=true." + ) + + try: + return FeastMlflowClient(store) + except ImportError: + raise ImportError( + "mlflow package is not installed. " + "Install it with: pip install feast[mlflow]" + ) + + +def _ensure_client() -> Any: + """Return the cached client, creating it on first call.""" + global _client + if _client is None: + _client = _build_client() + return _client + + +def init(store: "FeatureStore") -> None: + """Bind ``feast.mlflow`` to a specific :class:`~feast.FeatureStore`. + + Call this once at the start of a notebook or script. All subsequent + ``feast.mlflow.*`` calls will use this store's MLflow configuration. + + This is **optional** — if you skip it, ``feast.mlflow`` will use the + most recently created ``FeatureStore`` automatically. + """ + global _client, _registered_store + _client = None + _registered_store = store + + +def get_active_run_id() -> Optional[str]: + """Return the active MLflow run ID, or ``None``.""" + return _ensure_client().active_run_id + + +def __getattr__(name: str) -> Any: + """Open delegation: Feast-enhanced client first, raw mlflow fallback. + + Lookup order for ``feast.mlflow.``: + + 1. If ``FeastMlflowClient`` has a public attribute *name*, return it. + This gives Feast-enhanced versions of ``start_run``, ``log_model``, + ``register_model``, ``load_model``, etc. + 2. Otherwise, fall back to the raw ``mlflow`` module. This makes + ``feast.mlflow.log_params``, ``feast.mlflow.set_tag``, + ``feast.mlflow.MlflowClient``, etc. work without any wrappers. + """ + if name.startswith("_"): + raise AttributeError(f"module 'feast.mlflow' has no attribute {name!r}") + + client = _ensure_client() + + client_attr = getattr(client, name, _MISSING) + if client_attr is not _MISSING: + return client_attr + + mlflow_attr = getattr(client._mlflow, name, _MISSING) + if mlflow_attr is not _MISSING: + return mlflow_attr + + raise AttributeError(f"module 'feast.mlflow' has no attribute {name!r}") diff --git a/sdk/python/feast/mlflow_integration/__init__.py b/sdk/python/feast/mlflow_integration/__init__.py new file mode 100644 index 00000000000..400b42ee9ba --- /dev/null +++ b/sdk/python/feast/mlflow_integration/__init__.py @@ -0,0 +1,39 @@ +""" +MLflow integration for Feast Feature Store. + +This module provides seamless integration between Feast and MLflow. When enabled +in feature_store.yaml, feature metadata is logged to MLflow +during get_historical_features and get_online_features calls. + +Usage: + Configure MLflow in your feature_store.yaml: + + project: my_project + # ... other config ... + + mlflow: + enabled: true + tracking_uri: https://mlflow.example.com # or set MLFLOW_TRACKING_URI + auto_log: true + + When ``tracking_uri`` is omitted, the ``MLFLOW_TRACKING_URI`` environment + variable is used. If neither is set, MLflow falls back to its own default. + + All functionality is accessed through ``store.mlflow``: + + - ``store.mlflow.start_run()`` — start an MLflow run pre-tagged with Feast metadata + - ``store.mlflow.log_model()`` — log a model with ``feast_features.json`` + - ``store.mlflow.resolve_features()`` — map an MLflow model to its feature service + - ``store.mlflow.get_training_entity_df()`` — reproduce training by pulling entity + data from a previous MLflow run's artifacts +""" + +from feast.mlflow_integration.config import MlflowConfig +from feast.mlflow_integration.entity_df_builder import FeastMlflowEntityDfError +from feast.mlflow_integration.model_resolver import FeastMlflowModelResolutionError + +__all__ = [ + "MlflowConfig", + "FeastMlflowModelResolutionError", + "FeastMlflowEntityDfError", +] diff --git a/sdk/python/feast/mlflow_integration/client.py b/sdk/python/feast/mlflow_integration/client.py new file mode 100644 index 00000000000..bb64fe44dd3 --- /dev/null +++ b/sdk/python/feast/mlflow_integration/client.py @@ -0,0 +1,313 @@ +from __future__ import annotations + +import json +import logging +import os +import re +import tempfile +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +if TYPE_CHECKING: + import pandas as pd + + from feast import FeatureStore + +_logger = logging.getLogger(__name__) + +_FLAVOR_MAP = { + "sklearn": "sklearn", + "pytorch": "pytorch", + "xgboost": "xgboost", + "lightgbm": "lightgbm", + "tensorflow": "tensorflow", + "keras": "keras", + "pyfunc": "pyfunc", +} + + +class FeastMlflowClient: + """Single integration client for all Feast–MLflow functionality. + + Composes :class:`FeastMlflowLogger`, :class:`FeastMlflowEntityDfBuilder`, + and :class:`FeastMlflowModelResolver` so that there is exactly **one** + ``mlflow`` import and **one** ``MlflowClient`` instance. + + Access via ``store.mlflow`` or ``feast.mlflow``:: + + store = FeatureStore(".") + + with store.mlflow.start_run(run_name="training"): + df = store.get_historical_features(...).to_df() + model = train(df) + store.mlflow.log_model(model, "model") + """ + + def __init__(self, store: "FeatureStore"): + import mlflow as _mlflow_mod + + self._mlflow = _mlflow_mod + self._store = store + self._tracking_uri = store.config.mlflow.get_tracking_uri() + self._client = _mlflow_mod.MlflowClient(tracking_uri=self._tracking_uri) + self._default_experiment = store.config.project + + from feast.mlflow_integration.entity_df_builder import ( + FeastMlflowEntityDfBuilder, + ) + from feast.mlflow_integration.logger import FeastMlflowLogger + from feast.mlflow_integration.model_resolver import FeastMlflowModelResolver + + self._logger_impl = FeastMlflowLogger(store, _mlflow_mod, self._client) + self._entity_df_builder = FeastMlflowEntityDfBuilder( + store, _mlflow_mod, self._client + ) + self._model_resolver = FeastMlflowModelResolver( + store, _mlflow_mod, self._client + ) + + @property + def client(self): + """The underlying ``MlflowClient`` instance.""" + return self._client + + @property + def mlflow(self): + """Escape hatch: access the raw ``mlflow`` module.""" + return self._mlflow + + @property + def active_run_id(self) -> Optional[str]: + """Return the active MLflow run ID, or ``None``.""" + run = self._mlflow.active_run() + return run.info.run_id if run else None + + # ------------------------------------------------------------------ + # Run management + # ------------------------------------------------------------------ + + def start_run( + self, + run_name: Optional[str] = None, + tags: Optional[Dict[str, str]] = None, + **kwargs: Any, + ): + """Context manager that starts an MLflow run pre-tagged with Feast metadata. + + Sets the default Feast experiment only when a run is actually started, + avoiding global side effects during ``FeatureStore.__init__``. If the + caller already set an experiment (via ``kwargs["experiment_id"]`` or a + prior ``mlflow.set_experiment``), that choice is respected. + """ + if self._tracking_uri: + self._mlflow.set_tracking_uri(self._tracking_uri) + if "experiment_id" not in kwargs: + self._mlflow.set_experiment(self._default_experiment) + + merged_tags = {"feast.project": self._store.project} + if tags: + merged_tags.update(tags) + return self._mlflow.start_run(run_name=run_name, tags=merged_tags, **kwargs) + + # ------------------------------------------------------------------ + # Model lifecycle + # ------------------------------------------------------------------ + + def log_model( + self, + model: Any, + artifact_path: str, + flavor: str = "sklearn", + **kwargs: Any, + ): + """Log a model and auto-attach ``feast_features.json``.""" + flavor_name = _FLAVOR_MAP.get(flavor, "pyfunc") + flavor_mod = getattr(self._mlflow, flavor_name, self._mlflow.pyfunc) + flavor_mod.log_model(model, artifact_path, **kwargs) + self._log_required_features() + + def _log_required_features(self) -> None: + try: + run = self._mlflow.active_run() + if run is None: + return + tags = self._client.get_run(run.info.run_id).data.tags + refs_str = tags.get("feast.feature_refs") + if not refs_str: + return + features = [r for r in refs_str.split(",") if r] + if not features: + return + + with tempfile.TemporaryDirectory() as tmp_dir: + path = os.path.join(tmp_dir, "feast_features.json") + with open(path, "w") as f: + json.dump(features, f) + self._client.log_artifact(run.info.run_id, path, artifact_path="") + except Exception as e: + _logger.debug("Failed to log feast_features.json: %s", e) + + def register_model(self, model_uri: str, name: str): + """Register a model and auto-tag the version with ``feast.feature_service``.""" + result = self._mlflow.register_model(model_uri, name) + + try: + if result.run_id: + run = self._client.get_run(result.run_id) + fs_name = run.data.tags.get("feast.feature_service") + if fs_name: + self._client.set_model_version_tag( + name, result.version, "feast.feature_service", fs_name + ) + except Exception as e: + _logger.debug("Failed to auto-tag model version: %s", e) + + return result + + def load_model(self, model_uri: str, **kwargs: Any): + """Load a model and auto-tag the active prediction run with training lineage.""" + model = self._mlflow.pyfunc.load_model(model_uri, **kwargs) + + try: + active = self._mlflow.active_run() + if active is None: + return model + + run_id = active.info.run_id + parsed = _parse_model_uri(model_uri) + if parsed is None: + return model + + model_name, version_or_alias = parsed + try: + if version_or_alias.isdigit(): + mv = self._client.get_model_version(model_name, version_or_alias) + else: + mv = self._client.get_model_version_by_alias( + model_name, version_or_alias + ) + except Exception: + return model + + self._client.set_tag(run_id, "feast.model_name", model_name) + self._client.set_tag(run_id, "feast.model_version", str(mv.version)) + + if mv.run_id: + self._client.set_tag(run_id, "feast.training_run_id", mv.run_id) + try: + training_run = self._client.get_run(mv.run_id) + fs_name = training_run.data.tags.get("feast.feature_service") + if fs_name: + self._client.set_tag(run_id, "feast.feature_service", fs_name) + except Exception: + pass + + except Exception as e: + _logger.debug("Failed to tag prediction run with training lineage: %s", e) + + return model + + # ------------------------------------------------------------------ + # Delegated to logger + # ------------------------------------------------------------------ + + def log_feature_retrieval( + self, + feature_refs: List[str], + entity_count: int, + duration_seconds: float, + retrieval_type: str = "historical", + feature_service: Optional[Any] = None, + feature_service_name: Optional[str] = None, + ) -> bool: + """Log feature retrieval metadata to the active MLflow run.""" + return self._logger_impl.log_feature_retrieval( + feature_refs=feature_refs, + entity_count=entity_count, + duration_seconds=duration_seconds, + retrieval_type=retrieval_type, + feature_service=feature_service, + feature_service_name=feature_service_name, + ) + + def log_training_dataset( + self, + df: "pd.DataFrame", + dataset_name: str = "feast_training_data", + source: Optional[str] = None, + ) -> bool: + """Log a training DataFrame as an MLflow dataset input.""" + return self._logger_impl.log_training_dataset( + df=df, dataset_name=dataset_name, source=source + ) + + def log_apply( + self, + changed_objects: List[Any], + transition_types: Optional[Dict[str, str]] = None, + ) -> bool: + """Log a feast apply operation to MLflow.""" + return self._logger_impl.log_apply( + changed_objects=changed_objects, + transition_types=transition_types, + ) + + def log_materialize( + self, + feature_view_names: List[str], + start_date: Any, + end_date: Any, + duration_seconds: float, + incremental: bool = False, + ) -> bool: + """Log a feast materialize operation to MLflow.""" + return self._logger_impl.log_materialize( + feature_view_names=feature_view_names, + start_date=start_date, + end_date=end_date, + duration_seconds=duration_seconds, + incremental=incremental, + ) + + def log_entity_df_metadata( + self, entity_df: Any, start_date: Any = None, end_date: Any = None + ) -> None: + """Log lightweight entity_df metadata to MLflow.""" + self._logger_impl.log_entity_df_metadata(entity_df, start_date, end_date) + + def log_entity_df_artifact(self, entity_df: Any) -> None: + """Upload entity DataFrame as a parquet artifact to MLflow.""" + self._logger_impl.log_entity_df_artifact(entity_df) + + # ------------------------------------------------------------------ + # Delegated to model resolver + # ------------------------------------------------------------------ + + def resolve_features(self, model_uri: str) -> str: + """Resolve which Feast feature service a registered model needs.""" + return self._model_resolver.resolve(model_uri) + + # ------------------------------------------------------------------ + # Delegated to entity df builder + # ------------------------------------------------------------------ + + def get_training_entity_df( + self, + run_id: str, + timestamp_column: str = "event_timestamp", + max_rows: Optional[int] = None, + ) -> "pd.DataFrame": + """Pull the entity DataFrame from a past MLflow run.""" + return self._entity_df_builder.get_entity_df( + run_id=run_id, + timestamp_column=timestamp_column, + max_rows=max_rows, + ) + + +def _parse_model_uri(model_uri: str) -> Optional[tuple]: + """Parse ``models://`` into a tuple.""" + pattern = r"^models:/([^/]+)/(.+)$" + match = re.match(pattern, model_uri) + if match: + return match.group(1), match.group(2) + return None diff --git a/sdk/python/feast/mlflow_integration/config.py b/sdk/python/feast/mlflow_integration/config.py new file mode 100644 index 00000000000..db1d18eb080 --- /dev/null +++ b/sdk/python/feast/mlflow_integration/config.py @@ -0,0 +1,65 @@ +import os +from typing import Optional + +from pydantic import StrictBool, StrictInt, StrictStr + +from feast.repo_config import FeastBaseModel + +MLFLOW_TAG_MAX_LENGTH = 5000 +MLFLOW_TAG_TRUNCATION_LIMIT = MLFLOW_TAG_MAX_LENGTH - 10 +MLFLOW_TAG_TRUNCATION_SLICE = MLFLOW_TAG_MAX_LENGTH - 13 + +MLFLOW_PARAM_MAX_LENGTH = 500 +MLFLOW_PARAM_TRUNCATION_LIMIT = MLFLOW_PARAM_MAX_LENGTH - 10 +MLFLOW_PARAM_TRUNCATION_SLICE = MLFLOW_PARAM_MAX_LENGTH - 13 + +DEFAULT_ENTITY_DF_MAX_ROWS = 100_000 + + +def resolve_tracking_uri(configured_uri: Optional[str] = None) -> Optional[str]: + """Return the effective MLflow tracking URI. + + Priority: + 1. Explicitly configured URI from feature_store.yaml + 2. MLFLOW_TRACKING_URI environment variable (MLflow's native convention) + 3. None — let MLflow fall back to its own defaults (local ./mlruns) + """ + if configured_uri: + return configured_uri + return os.environ.get("MLFLOW_TRACKING_URI") + + +class MlflowConfig(FeastBaseModel): + enabled: StrictBool = False + """ bool: Whether MLflow integration is enabled. Defaults to False. """ + + tracking_uri: Optional[StrictStr] = None + """ str: MLflow tracking URI. When not set, the MLFLOW_TRACKING_URI + environment variable is used. If neither is set, MLflow falls back + to its own default (local ./mlruns directory). """ + + auto_log: StrictBool = True + """ bool: Automatically log feature retrieval metadata to the active + MLflow run when get_historical_features or get_online_features is + called. Defaults to True. """ + + auto_log_entity_df: StrictBool = False + """ bool: When True, the input entity_df (or SQL query) is recorded in + the MLflow run. Defaults to False. """ + + entity_df_max_rows: StrictInt = DEFAULT_ENTITY_DF_MAX_ROWS + """ int: Maximum number of entity DataFrame rows to save as an MLflow + artifact. DataFrames exceeding this limit are skipped to avoid + OOM and slow uploads. Defaults to 100000. """ + + log_operations: StrictBool = False + """ bool: Log feast apply and materialize operations to a separate + MLflow experiment. Opt-in to avoid noise. Defaults to False. """ + + ops_experiment_suffix: StrictStr = "-feast-ops" + """ str: Suffix appended to the project name to form the MLflow + experiment name for operation logs. Defaults to '-feast-ops'. """ + + def get_tracking_uri(self) -> Optional[str]: + """Resolve the effective tracking URI for this config instance.""" + return resolve_tracking_uri(self.tracking_uri) diff --git a/sdk/python/feast/mlflow_integration/entity_df_builder.py b/sdk/python/feast/mlflow_integration/entity_df_builder.py new file mode 100644 index 00000000000..9aa0c8ba528 --- /dev/null +++ b/sdk/python/feast/mlflow_integration/entity_df_builder.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, Optional + +import pandas as pd + +if TYPE_CHECKING: + from feast import FeatureStore + +_logger = logging.getLogger(__name__) + + +class FeastMlflowEntityDfError(Exception): + """Raised when an entity DataFrame cannot be built from an MLflow run.""" + + pass + + +class FeastMlflowEntityDfBuilder: + """Reconstructs entity DataFrames from MLflow run artifacts. + + Instantiated once inside :class:`FeastMlflowClient` and reuses its + ``MlflowClient`` — no separate ``import mlflow`` needed. + """ + + def __init__(self, store: "FeatureStore", mlflow_mod: Any, client: Any): + self._store = store + self._mlflow = mlflow_mod + self._client = client + + def get_entity_df( + self, + run_id: str, + timestamp_column: str = "event_timestamp", + max_rows: Optional[int] = None, + ) -> pd.DataFrame: + """Build an entity DataFrame from an MLflow run's artifacts. + + Convention: the run should have an artifact named ``entity_df.parquet`` + (or ``entity_df.csv``), saved automatically when + ``auto_log_entity_df: true`` is set in ``feature_store.yaml``. + + Args: + run_id: The MLflow run ID. + timestamp_column: Expected name of the timestamp column in the + entity DataFrame. + max_rows: Optional limit on number of rows to load. + + Returns: + A ``pd.DataFrame`` suitable for passing to + ``store.get_historical_features(entity_df=...)``. + + Raises: + FeastMlflowEntityDfError: If run not found or no entity data + is available on the run. + """ + from mlflow.exceptions import MlflowException + + try: + self._client.get_run(run_id) + except MlflowException as e: + raise FeastMlflowEntityDfError(f"Run '{run_id}' not found: {e}") + + df = self._try_artifact(run_id, "entity_df.parquet", "parquet") + if df is not None: + if max_rows is not None: + df = df.head(max_rows) + self._validate_timestamp_col(df, timestamp_column) + return df + + df = self._try_artifact(run_id, "entity_df.csv", "csv") + if df is not None: + if max_rows is not None: + df = df.head(max_rows) + self._validate_timestamp_col(df, timestamp_column) + return df + + raise FeastMlflowEntityDfError( + f"No entity data found for run '{run_id}'. " + f"Expected artifact 'entity_df.parquet' or 'entity_df.csv'. " + f"Ensure auto_log_entity_df is enabled in feature_store.yaml." + ) + + def _try_artifact( + self, run_id: str, artifact_name: str, fmt: str + ) -> Optional[pd.DataFrame]: + try: + local_path = self._client.download_artifacts(run_id, artifact_name) + if fmt == "parquet": + return pd.read_parquet(local_path) + if fmt == "csv": + return pd.read_csv(local_path) + _logger.warning( + "Unsupported entity DataFrame format '%s' for artifact '%s'. " + "Only 'parquet' and 'csv' are supported.", + fmt, + artifact_name, + ) + return None + except Exception as e: + _logger.debug( + "Artifact '%s' not found for run '%s': %s", + artifact_name, + run_id, + e, + ) + return None + + @staticmethod + def _validate_timestamp_col(df: pd.DataFrame, col: str) -> None: + if col not in df.columns: + raise FeastMlflowEntityDfError( + f"Entity DataFrame missing required timestamp column '{col}'. " + f"Available columns: {list(df.columns)}" + ) diff --git a/sdk/python/feast/mlflow_integration/logger.py b/sdk/python/feast/mlflow_integration/logger.py new file mode 100644 index 00000000000..e538f97c7db --- /dev/null +++ b/sdk/python/feast/mlflow_integration/logger.py @@ -0,0 +1,375 @@ +from __future__ import annotations + +import logging +import time +from datetime import datetime +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +import pandas as pd + +from feast.mlflow_integration.config import ( + MLFLOW_TAG_TRUNCATION_LIMIT, + MLFLOW_TAG_TRUNCATION_SLICE, +) + +if TYPE_CHECKING: + from feast import FeatureStore + from feast.feature_service import FeatureService + +_logger = logging.getLogger(__name__) + +_WARNING_INTERVAL_SECONDS = 300 + + +class FeastMlflowLogger: + """Handles all MLflow logging for Feast feature retrieval and operations. + + Instantiated once inside :class:`FeastMlflowClient` and reuses its + ``mlflow`` module reference and ``MlflowClient`` — no duplicate + ``import mlflow`` or client construction. + """ + + def __init__(self, store: "FeatureStore", mlflow_mod: Any, client: Any): + self._store = store + self._mlflow = mlflow_mod + self._client = client + self._tracking_uri = store.config.mlflow.get_tracking_uri() + self._consecutive_failures = 0 + self._last_warning_time = 0.0 + + def _truncate_for_tag(self, value: str) -> str: + if len(value) > MLFLOW_TAG_TRUNCATION_LIMIT: + return value[:MLFLOW_TAG_TRUNCATION_SLICE] + "..." + return value + + def _report_failure(self, msg: str, exc: Exception) -> None: + self._consecutive_failures += 1 + now = time.monotonic() + if ( + self._consecutive_failures == 1 + or (now - self._last_warning_time) >= _WARNING_INTERVAL_SECONDS + ): + _logger.warning( + "%s (failures=%d): %s", msg, self._consecutive_failures, exc + ) + self._last_warning_time = now + else: + _logger.debug("%s (failures=%d): %s", msg, self._consecutive_failures, exc) + + def _report_success(self) -> None: + self._consecutive_failures = 0 + + def log_feature_retrieval( + self, + feature_refs: List[str], + entity_count: int, + duration_seconds: float, + retrieval_type: str = "historical", + feature_service: Optional["FeatureService"] = None, + feature_service_name: Optional[str] = None, + ) -> bool: + """Log feature retrieval metadata to the active MLflow run.""" + active_run = self._mlflow.active_run() + if active_run is None: + return False + + try: + run_id = active_run.info.run_id + + if self._store.project: + self._client.set_tag(run_id, "feast.project", self._store.project) + self._client.set_tag(run_id, "feast.retrieval_type", retrieval_type) + + fs_name = None + if feature_service is not None: + fs_name = feature_service.name + elif feature_service_name is not None: + fs_name = feature_service_name + if fs_name: + self._client.set_tag(run_id, "feast.feature_service", fs_name) + + fv_names = sorted({ref.split(":")[0] for ref in feature_refs if ":" in ref}) + if fv_names: + fv_str = self._truncate_for_tag(",".join(fv_names)) + self._client.set_tag(run_id, "feast.feature_views", fv_str) + + refs_str = self._truncate_for_tag(",".join(feature_refs)) + self._client.set_tag(run_id, "feast.feature_refs", refs_str) + self._client.set_tag(run_id, "feast.entity_count", str(entity_count)) + self._client.set_tag(run_id, "feast.feature_count", str(len(feature_refs))) + + self._client.log_metric( + run_id, "feast.job_submission_sec", round(duration_seconds, 4) + ) + + self._report_success() + return True + except Exception as e: + self._report_failure("Failed to log feature retrieval to MLflow", e) + return False + + def log_training_dataset( + self, + df: pd.DataFrame, + dataset_name: str = "feast_training_data", + source: Optional[str] = None, + ) -> bool: + """Log a training DataFrame as an MLflow dataset input on the active run.""" + active_run = self._mlflow.active_run() + if active_run is None: + return False + + try: + dataset = self._mlflow.data.from_pandas( + df, + name=dataset_name, + source=source or "feast.get_historical_features", + ) + self._mlflow.log_input(dataset, context="training") + return True + except Exception as e: + self._report_failure("Failed to log training dataset to MLflow", e) + return False + + def _get_or_create_experiment(self, experiment_name: str) -> str: + exp = self._client.get_experiment_by_name(experiment_name) + if exp is not None: + return exp.experiment_id + return self._client.create_experiment(experiment_name) + + def log_apply( + self, + changed_objects: List[Any], + transition_types: Optional[Dict[str, str]] = None, + ) -> bool: + """Log a feast apply operation to a dedicated MLflow experiment.""" + try: + from feast import Entity, FeatureService + from feast.feature_view import FeatureView + + project = self._store.project + mlflow_cfg = self._store.config.mlflow + ops_suffix = mlflow_cfg.ops_experiment_suffix + + experiment_name = f"{project}{ops_suffix}" + experiment_id = self._get_or_create_experiment(experiment_name) + + fv_names: List[str] = [] + fs_names: List[str] = [] + entity_names: List[str] = [] + for obj in changed_objects: + if isinstance(obj, FeatureView): + fv_names.append(obj.name) + elif isinstance(obj, FeatureService): + fs_names.append(obj.name) + elif isinstance(obj, Entity) and obj.name != "__dummy": + entity_names.append(obj.name) + + run = self._client.create_run(experiment_id, run_name=f"apply_{project}") + run_id = run.info.run_id + try: + self._client.set_tag(run_id, "feast.operation", "apply") + self._client.set_tag(run_id, "feast.project", project) + if fv_names: + self._client.set_tag( + run_id, + "feast.feature_views_changed", + self._truncate_for_tag(",".join(fv_names)), + ) + if fs_names: + self._client.set_tag( + run_id, + "feast.feature_services_changed", + self._truncate_for_tag(",".join(fs_names)), + ) + if entity_names: + self._client.set_tag( + run_id, + "feast.entities_changed", + self._truncate_for_tag(",".join(entity_names)), + ) + self._client.log_metric( + run_id, "feast.apply.feature_views_count", len(fv_names) + ) + self._client.log_metric( + run_id, "feast.apply.feature_services_count", len(fs_names) + ) + self._client.log_metric( + run_id, "feast.apply.entities_count", len(entity_names) + ) + + if transition_types: + self._log_transition_tags( + run_id, + transition_types, + fv_names, + fs_names, + entity_names, + ) + finally: + self._client.set_terminated(run_id) + + self._report_success() + return True + except Exception as e: + self._report_failure("Failed to log apply to MLflow", e) + return False + + def _log_transition_tags( + self, + run_id: str, + transition_types: Dict[str, str], + fv_names: List[str], + fs_names: List[str], + entity_names: List[str], + ) -> None: + buckets: Dict[str, Dict[str, List[str]]] = { + "feature_views": {"created": [], "updated": [], "deleted": []}, + "feature_services": {"created": [], "updated": [], "deleted": []}, + "entities": {"created": [], "updated": [], "deleted": []}, + } + + for name in fv_names: + tt = transition_types.get(name, "").upper() + if tt in ("CREATE", "UPDATE", "DELETE"): + buckets["feature_views"][tt.lower() + "d"].append(name) + + for name in fs_names: + tt = transition_types.get(name, "").upper() + if tt in ("CREATE", "UPDATE", "DELETE"): + buckets["feature_services"][tt.lower() + "d"].append(name) + + for name in entity_names: + tt = transition_types.get(name, "").upper() + if tt in ("CREATE", "UPDATE", "DELETE"): + buckets["entities"][tt.lower() + "d"].append(name) + + for obj_type, transitions in buckets.items(): + for transition, names in transitions.items(): + if names: + self._client.set_tag( + run_id, + f"feast.{obj_type}_{transition}", + self._truncate_for_tag(",".join(names)), + ) + + def log_materialize( + self, + feature_view_names: List[str], + start_date: Optional[datetime], + end_date: datetime, + duration_seconds: float, + incremental: bool = False, + ) -> bool: + """Log a feast materialize operation to a dedicated MLflow experiment.""" + try: + project = self._store.project + mlflow_cfg = self._store.config.mlflow + ops_suffix = mlflow_cfg.ops_experiment_suffix + + experiment_name = f"{project}{ops_suffix}" + experiment_id = self._get_or_create_experiment(experiment_name) + + op_type = "materialize_incremental" if incremental else "materialize" + run = self._client.create_run( + experiment_id, run_name=f"{op_type}_{project}" + ) + run_id = run.info.run_id + try: + self._client.set_tag(run_id, "feast.operation", op_type) + self._client.set_tag(run_id, "feast.project", project) + self._client.set_tag( + run_id, + "feast.materialize.feature_views", + self._truncate_for_tag(",".join(feature_view_names)), + ) + if start_date: + self._client.log_param( + run_id, + "feast.materialize.start_date", + start_date.isoformat(), + ) + self._client.log_param( + run_id, + "feast.materialize.end_date", + end_date.isoformat(), + ) + self._client.log_metric( + run_id, + "feast.materialize.duration_sec", + round(duration_seconds, 4), + ) + finally: + self._client.set_terminated(run_id) + + self._report_success() + return True + except Exception as e: + self._report_failure("Failed to log materialize to MLflow", e) + return False + + def log_entity_df_metadata( + self, entity_df: Any, start_date: Any = None, end_date: Any = None + ) -> None: + """Log lightweight entity_df metadata to MLflow. + + Uses ``set_tag`` (not ``log_param``) so the metadata can safely be + updated when ``get_historical_features`` is called multiple times + within the same MLflow run. + """ + try: + if self._mlflow.active_run() is None: + return + run_id = self._mlflow.active_run().info.run_id + + if isinstance(entity_df, str): + if len(entity_df) > MLFLOW_TAG_TRUNCATION_LIMIT: + query = entity_df[:MLFLOW_TAG_TRUNCATION_SLICE] + "..." + else: + query = entity_df + self._client.set_tag(run_id, "feast.entity_df_query", query) + self._client.set_tag(run_id, "feast.entity_df_type", "sql") + + elif isinstance(entity_df, pd.DataFrame): + self._client.set_tag(run_id, "feast.entity_df_type", "dataframe") + self._client.set_tag( + run_id, "feast.entity_df_rows", str(len(entity_df)) + ) + cols = ",".join(entity_df.columns) + if len(cols) > MLFLOW_TAG_TRUNCATION_LIMIT: + cols = cols[:MLFLOW_TAG_TRUNCATION_SLICE] + "..." + self._client.set_tag(run_id, "feast.entity_df_columns", cols) + + elif entity_df is None and (start_date or end_date): + self._client.set_tag(run_id, "feast.entity_df_type", "range") + if start_date: + self._client.set_tag(run_id, "feast.start_date", str(start_date)) + if end_date: + self._client.set_tag(run_id, "feast.end_date", str(end_date)) + + except Exception as e: + _logger.debug("Failed to log entity_df metadata to MLflow: %s", e) + + def log_entity_df_artifact(self, entity_df: Any) -> None: + """Upload entity DataFrame as a parquet artifact to MLflow.""" + try: + import os + import tempfile + + if self._mlflow.active_run() is None: + return + if not isinstance(entity_df, pd.DataFrame): + return + + mlflow_cfg = self._store.config.mlflow + run_id = self._mlflow.active_run().info.run_id + + max_rows = mlflow_cfg.entity_df_max_rows + if len(entity_df) <= max_rows: + with tempfile.TemporaryDirectory() as tmp_dir: + path = os.path.join(tmp_dir, "entity_df.parquet") + entity_df.to_parquet(path, index=False) + self._client.log_artifact(run_id, path) + + except Exception as e: + _logger.debug("Failed to log entity_df artifact to MLflow: %s", e) diff --git a/sdk/python/feast/mlflow_integration/model_resolver.py b/sdk/python/feast/mlflow_integration/model_resolver.py new file mode 100644 index 00000000000..b2d94dccbee --- /dev/null +++ b/sdk/python/feast/mlflow_integration/model_resolver.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import json +import logging +import re +from typing import TYPE_CHECKING, Any, Optional + +if TYPE_CHECKING: + from feast import FeatureStore + +_logger = logging.getLogger(__name__) + + +class FeastMlflowModelResolutionError(Exception): + """Raised when a model URI cannot be resolved to a feature service.""" + + pass + + +class FeastMlflowModelResolver: + """Resolves MLflow model URIs to Feast feature service names. + + Instantiated once inside :class:`FeastMlflowClient` and reuses its + ``MlflowClient`` — no separate ``import mlflow`` needed. + """ + + def __init__(self, store: "FeatureStore", mlflow_mod: Any, client: Any): + self._store = store + self._mlflow = mlflow_mod + self._client = client + + def resolve(self, model_uri: str) -> str: + """Resolve the Feast feature service name for a given MLflow model URI. + + Resolution order: + 1. Model version tag ``feast.feature_service`` (explicit override). + 2. Training run tag ``feast.feature_service`` (set by auto-log). + + Args: + model_uri: MLflow model URI in the form + ``models://``. + + Raises: + FeastMlflowModelResolutionError: If URI is invalid, resolution + fails, or validation against the store fails. + """ + from mlflow.exceptions import MlflowException + + pattern = r"^models:/([^/]+)/(.+)$" + match = re.match(pattern, model_uri) + if not match: + raise FeastMlflowModelResolutionError( + f"Invalid model_uri format: '{model_uri}'. " + f"Expected 'models://'." + ) + + model_name, version_or_alias = match.group(1), match.group(2) + + try: + if version_or_alias.isdigit(): + mv = self._client.get_model_version(model_name, version_or_alias) + else: + mv = self._client.get_model_version_by_alias( + model_name, version_or_alias + ) + except MlflowException as e: + raise FeastMlflowModelResolutionError( + f"Could not resolve model '{model_uri}': {e}" + ) + + tags = mv.tags or {} + if "feast.feature_service" in tags: + fs_name = tags["feast.feature_service"] + else: + fs_name = self._resolve_from_run_tags(mv) + if fs_name is None: + raise FeastMlflowModelResolutionError( + f"Could not determine feature service for model '{model_uri}'. " + f"No 'feast.feature_service' tag found on the model version or " + f"its training run. Set the tag explicitly on the model version " + f"or ensure auto_log was enabled during training." + ) + + self._validate_feature_service(fs_name, mv) + return fs_name + + def _resolve_from_run_tags(self, model_version: Any) -> Optional[str]: + try: + run = self._client.get_run(model_version.run_id) + return run.data.tags.get("feast.feature_service") + except Exception as e: + _logger.debug("Could not read run tags for model version: %s", e) + return None + + def _validate_feature_service(self, fs_name: str, model_version: Any) -> None: + try: + fs = self._store.get_feature_service(fs_name) + except Exception: + raise FeastMlflowModelResolutionError( + f"Feature service '{fs_name}' not found in the Feast registry." + ) + + if not self._has_artifact(model_version.run_id, "feast_features.json"): + return + + try: + local_path = self._client.download_artifacts( + model_version.run_id, "feast_features.json" + ) + with open(local_path) as f: + expected_features = json.load(f) + + actual_features = [] + for proj in fs.feature_view_projections: + for feat in proj.features: + actual_features.append(f"{proj.name_to_use()}:{feat.name}") + + expected_set = set(expected_features) + actual_set = set(actual_features) + + if expected_set != actual_set: + missing = expected_set - actual_set + extra = actual_set - expected_set + raise FeastMlflowModelResolutionError( + f"Feature mismatch for service '{fs_name}'. " + f"Missing: {missing}, Extra: {extra}" + ) + except FeastMlflowModelResolutionError: + raise + except Exception as e: + _logger.debug("Could not validate feast_features.json: %s", e) + + def _has_artifact(self, run_id: str, artifact_name: str) -> bool: + try: + artifacts = self._client.list_artifacts(run_id) + return any(a.path == artifact_name for a in artifacts) + except Exception: + return False diff --git a/sdk/python/feast/monitoring/__init__.py b/sdk/python/feast/monitoring/__init__.py new file mode 100644 index 00000000000..69a921060a5 --- /dev/null +++ b/sdk/python/feast/monitoring/__init__.py @@ -0,0 +1,7 @@ +from feast.monitoring.dqm_job_manager import DQMJobManager +from feast.monitoring.metrics_calculator import MetricsCalculator + +__all__ = [ + "DQMJobManager", + "MetricsCalculator", +] diff --git a/sdk/python/feast/monitoring/dqm_job_manager.py b/sdk/python/feast/monitoring/dqm_job_manager.py new file mode 100644 index 00000000000..5a0765ffa5f --- /dev/null +++ b/sdk/python/feast/monitoring/dqm_job_manager.py @@ -0,0 +1,149 @@ +import json +import logging +import uuid +from datetime import date, datetime, timezone +from typing import Any, Dict, Optional + +logger = logging.getLogger(__name__) + +JOB_STATUS_PENDING = "pending" +JOB_STATUS_RUNNING = "running" +JOB_STATUS_COMPLETED = "completed" +JOB_STATUS_FAILED = "failed" + + +class DQMJobManager: + """DQM job manager that persists jobs via the offline store abstraction.""" + + def __init__(self, offline_store, config): + self._offline_store = offline_store + self._config = config + + def ensure_table(self) -> None: + self._offline_store.ensure_monitoring_tables(self._config) + + def submit( + self, + project: str, + job_type: str, + feature_view_name: Optional[str] = None, + parameters: Optional[Dict[str, Any]] = None, + ) -> str: + job_id = str(uuid.uuid4()) + now = datetime.now(timezone.utc) + row = { + "job_id": job_id, + "project_id": project, + "feature_view_name": feature_view_name, + "job_type": job_type, + "status": JOB_STATUS_PENDING, + "parameters": json.dumps(parameters) if parameters else None, + "metric_date": now.date(), + "started_at": None, + "completed_at": None, + "error_message": None, + "result_summary": None, + } + self._offline_store.save_monitoring_metrics(self._config, "job", [row]) + return job_id + + def get_job(self, job_id: str) -> Optional[Dict[str, Any]]: + rows = self._offline_store.query_monitoring_metrics( + config=self._config, + project="", + metric_type="job", + filters={"job_id": job_id}, + ) + if not rows: + return None + record = rows[0] + for key in ("parameters", "result_summary"): + val = record.get(key) + if isinstance(val, str): + try: + record[key] = json.loads(val) + except (json.JSONDecodeError, TypeError): + pass + return record + + def update_status( + self, + job_id: str, + status: str, + error_message: Optional[str] = None, + result_summary: Optional[Dict[str, Any]] = None, + ) -> None: + job = self.get_job(job_id) + if job is None: + return + + now = datetime.now(timezone.utc) + job["status"] = status + + if status == JOB_STATUS_RUNNING: + job["started_at"] = now + elif status in (JOB_STATUS_COMPLETED, JOB_STATUS_FAILED): + job["completed_at"] = now + + if error_message is not None: + job["error_message"] = error_message + if result_summary is not None: + job["result_summary"] = json.dumps(result_summary) + + if "parameters" in job and not isinstance(job["parameters"], str): + job["parameters"] = ( + json.dumps(job["parameters"]) if job["parameters"] else None + ) + + if isinstance(job.get("metric_date"), str): + job["metric_date"] = date.fromisoformat(job["metric_date"]) + + self._offline_store.save_monitoring_metrics(self._config, "job", [job]) + + def execute_job(self, job_id: str, monitoring_service) -> Dict[str, Any]: + """Execute a DQM job synchronously. Manages status transitions.""" + job = self.get_job(job_id) + if job is None: + raise ValueError(f"Failed to find DQM job '{job_id}'") + + self.update_status(job_id, JOB_STATUS_RUNNING) + + try: + params = job.get("parameters") or {} + job_type = job["job_type"] + project = job["project_id"] + + if job_type == "auto_compute": + result = monitoring_service.auto_compute( + project=project, + feature_view_name=job.get("feature_view_name"), + ) + elif job_type == "baseline": + result = monitoring_service.compute_baseline( + project=project, + feature_view_name=job.get("feature_view_name"), + feature_names=params.get("feature_names"), + ) + elif job_type == "compute": + result = monitoring_service.compute_metrics( + project=project, + feature_view_name=job.get("feature_view_name"), + feature_names=params.get("feature_names"), + start_date=date.fromisoformat(params["start_date"]) + if params.get("start_date") + else None, + end_date=date.fromisoformat(params["end_date"]) + if params.get("end_date") + else None, + granularity=params.get("granularity", "daily"), + set_baseline=params.get("set_baseline", False), + ) + else: + raise ValueError(f"Unknown job type '{job_type}'") + + self.update_status(job_id, JOB_STATUS_COMPLETED, result_summary=result) + return result + + except Exception as e: + self.update_status(job_id, JOB_STATUS_FAILED, error_message=str(e)) + raise diff --git a/sdk/python/feast/monitoring/metrics_calculator.py b/sdk/python/feast/monitoring/metrics_calculator.py new file mode 100644 index 00000000000..1b8b3b3e7ca --- /dev/null +++ b/sdk/python/feast/monitoring/metrics_calculator.py @@ -0,0 +1,187 @@ +import logging +import math +from typing import Dict, List, Optional, Tuple + +import numpy as np +import pyarrow as pa +import pyarrow.compute as pc + +from feast.types import PrimitiveFeastType + +logger = logging.getLogger(__name__) + + +def _safe_float(val): + """Return None for None/NaN/Inf, otherwise float.""" + if val is None: + return None + f = float(val) + if math.isnan(f) or math.isinf(f): + return None + return f + + +_NUMERIC_TYPES = { + PrimitiveFeastType.INT32, + PrimitiveFeastType.INT64, + PrimitiveFeastType.FLOAT32, + PrimitiveFeastType.FLOAT64, + PrimitiveFeastType.DECIMAL, +} + +_CATEGORICAL_TYPES = { + PrimitiveFeastType.STRING, + PrimitiveFeastType.BOOL, +} + + +class MetricsCalculator: + def __init__(self, histogram_bins: int = 20, top_n: int = 10): + self.histogram_bins = histogram_bins + self.top_n = top_n + + @staticmethod + def classify_feature(dtype) -> Optional[str]: + primitive = dtype + if hasattr(dtype, "base_type"): + primitive = dtype.base_type if dtype.base_type else dtype + + if isinstance(primitive, PrimitiveFeastType): + if primitive in _NUMERIC_TYPES: + return "numeric" + if primitive in _CATEGORICAL_TYPES: + return "categorical" + return None + + @staticmethod + def classify_feature_arrow(arrow_type: pa.DataType) -> Optional[str]: + """Classify a PyArrow data type as numeric or categorical.""" + if ( + pa.types.is_integer(arrow_type) + or pa.types.is_floating(arrow_type) + or pa.types.is_decimal(arrow_type) + ): + return "numeric" + if ( + pa.types.is_string(arrow_type) + or pa.types.is_large_string(arrow_type) + or pa.types.is_boolean(arrow_type) + ): + return "categorical" + return None + + def compute_numeric(self, array: pa.Array) -> Dict: + total = len(array) + null_count = array.null_count + result = { + "feature_type": "numeric", + "row_count": total, + "null_count": null_count, + "null_rate": null_count / total if total > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": None, + } + + valid = pc.drop_null(array) # type: ignore[attr-defined] + if len(valid) == 0: + return result + + float_array = pc.cast(valid, pa.float64()) + result["mean"] = _safe_float(pc.mean(float_array).as_py()) # type: ignore[attr-defined] + result["stddev"] = _safe_float(pc.stddev(float_array, ddof=1).as_py()) # type: ignore[attr-defined] + + min_max = pc.min_max(float_array) # type: ignore[attr-defined] + result["min_val"] = min_max["min"].as_py() + result["max_val"] = min_max["max"].as_py() + + quantiles = pc.quantile(float_array, q=[0.50, 0.75, 0.90, 0.95, 0.99]) # type: ignore[attr-defined] + q_values = quantiles.to_pylist() + result["p50"] = q_values[0] + result["p75"] = q_values[1] + result["p90"] = q_values[2] + result["p95"] = q_values[3] + result["p99"] = q_values[4] + + np_array = float_array.to_numpy() + counts, bin_edges = np.histogram(np_array, bins=self.histogram_bins) + result["histogram"] = { + "bins": bin_edges.tolist(), + "counts": counts.tolist(), + "bin_width": float(bin_edges[1] - bin_edges[0]) + if len(bin_edges) > 1 + else 0, + } + + return result + + def compute_categorical(self, array: pa.Array) -> Dict: + total = len(array) + null_count = array.null_count + result = { + "feature_type": "categorical", + "row_count": total, + "null_count": null_count, + "null_rate": null_count / total if total > 0 else 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": None, + } + + valid = pc.drop_null(array) # type: ignore[attr-defined] + if len(valid) == 0: + return result + + value_counts = pc.value_counts(valid) # type: ignore[attr-defined] + entries = [ + {"value": vc["values"].as_py(), "count": vc["counts"].as_py()} + for vc in value_counts + ] + entries.sort(key=lambda x: x["count"], reverse=True) + + unique_count = len(entries) + top_entries = entries[: self.top_n] + other_count = sum(e["count"] for e in entries[self.top_n :]) + + result["histogram"] = { + "values": top_entries, + "other_count": other_count, + "unique_count": unique_count, + } + + return result + + def compute_all( + self, + table: pa.Table, + feature_fields: List[Tuple[str, str]], + ) -> List[Dict]: + results = [] + for name, ftype in feature_fields: + if name not in table.column_names: + logger.warning("Column '%s' not found in arrow table, skipping", name) + continue + column = table.column(name) + if ftype == "numeric": + metrics = self.compute_numeric(column) + elif ftype == "categorical": + metrics = self.compute_categorical(column) + else: + continue + metrics["feature_name"] = name + results.append(metrics) + return results diff --git a/sdk/python/feast/monitoring/monitoring_service.py b/sdk/python/feast/monitoring/monitoring_service.py new file mode 100644 index 00000000000..df6c16175f6 --- /dev/null +++ b/sdk/python/feast/monitoring/monitoring_service.py @@ -0,0 +1,1283 @@ +import logging +import math +import time +from collections import defaultdict +from datetime import date, datetime, timedelta, timezone +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple + +from feast.feature_logging import LOG_TIMESTAMP_FIELD, FeatureServiceLoggingSource +from feast.infra.offline_stores.offline_store import OfflineStore +from feast.monitoring.dqm_job_manager import DQMJobManager +from feast.monitoring.metrics_calculator import MetricsCalculator +from feast.monitoring.monitoring_utils import build_view_aggregate + +if TYPE_CHECKING: + from feast.feature_store import FeatureStore + +logger = logging.getLogger(__name__) + +VALID_GRANULARITIES = OfflineStore.MONITORING_VALID_GRANULARITIES + +_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) +_FAR_FUTURE = datetime(2099, 12, 31, 23, 59, 59, tzinfo=timezone.utc) + +GRANULARITY_WINDOWS = { + "daily": timedelta(days=1), + "weekly": timedelta(days=7), + "biweekly": timedelta(days=14), + "monthly": timedelta(days=30), + "quarterly": timedelta(days=90), +} + +_FLOAT_FIELDS = frozenset( + { + "null_rate", + "mean", + "stddev", + "min_val", + "max_val", + "p50", + "p75", + "p90", + "p95", + "p99", + "avg_null_rate", + "max_null_rate", + } +) + + +def _sanitize_floats(row: Dict[str, Any]) -> Dict[str, Any]: + """Replace NaN/Inf float values with None so JSON serialization succeeds.""" + for key in _FLOAT_FIELDS: + val = row.get(key) + if isinstance(val, float) and (math.isnan(val) or math.isinf(val)): + row[key] = None + return row + + +class MonitoringService: + def __init__(self, store: "FeatureStore"): + self._store = store + self._job_manager: Optional[DQMJobManager] = None + self._calculator = MetricsCalculator() + self._monitoring_tables_ensured = False + self._offline_store_cache = None + + def _get_offline_store(self): + if self._offline_store_cache is None: + self._offline_store_cache = self._store._get_provider().offline_store + return self._offline_store_cache + + def _ensure_monitoring_tables(self): + if not self._monitoring_tables_ensured: + self._get_offline_store().ensure_monitoring_tables(self._store.config) + self._monitoring_tables_ensured = True + + @property + def job_manager(self) -> DQMJobManager: + if self._job_manager is None: + self._job_manager = DQMJobManager( + self._get_offline_store(), self._store.config + ) + self._job_manager.ensure_table() + return self._job_manager + + # ------------------------------------------------------------------ # + # Auto-compute: detect dates, compute all granularities + # ------------------------------------------------------------------ # + + def auto_compute( + self, + project: Optional[str] = None, + feature_view_name: Optional[str] = None, + ) -> Dict[str, Any]: + """Detect date ranges from source data and compute all granularities. + + Also computes baseline for any features that don't have one yet + (idempotent -- features with existing baselines are skipped). + """ + start_time = time.time() + self._ensure_monitoring_tables() + if project is None: + project = self._store.config.project + + feature_views = self._resolve_feature_views(project, feature_view_name) + total_features = 0 + total_views = 0 + baseline_features = 0 + granularities_computed = set() + + for fv in feature_views: + try: + feature_fields = self._classify_fields(fv) + if not feature_fields: + continue + + max_ts = self._get_max_timestamp(fv) + if max_ts is None: + logger.warning( + "No data found for feature view '%s', skipping", fv.name + ) + continue + + now = datetime.now(timezone.utc) + + # Compute baseline for features that don't have one yet + fields_needing_baseline = self._get_features_without_baseline( + project, fv + ) + if fields_needing_baseline: + bl_fields = self._classify_fields( + fv, fields=fields_needing_baseline + ) + if bl_fields: + bl_metrics = self._compute_feature_metrics( + fv, + bl_fields, + _EPOCH, + _FAR_FUTURE, + ) + self._save_computed_metrics( + project=project, + feature_view=fv, + metrics_list=bl_metrics, + metric_date=date.today(), + granularity="baseline", + set_baseline=True, + now=now, + ) + baseline_features += len(bl_metrics) + + for granularity, window in GRANULARITY_WINDOWS.items(): + window_start = max_ts - window + metrics_list = self._compute_feature_metrics( + fv, + feature_fields, + window_start, + max_ts, + ) + self._save_computed_metrics( + project=project, + feature_view=fv, + metrics_list=metrics_list, + metric_date=window_start.date(), + granularity=granularity, + set_baseline=False, + now=now, + ) + self._compute_feature_service_metrics( + project=project, + granularity=granularity, + metric_dates=[window_start.date()], + set_baseline=False, + ) + total_features += len(metrics_list) + granularities_computed.add(granularity) + total_views += 1 + except Exception: + logger.exception( + "Failed to auto-compute metrics for feature view '%s'", fv.name + ) + + duration_ms = int((time.time() - start_time) * 1000) + + return { + "status": "completed", + "computed_feature_views": total_views, + "computed_features": total_features, + "baseline_features": baseline_features, + "granularities": sorted(granularities_computed), + "duration_ms": duration_ms, + } + + # ------------------------------------------------------------------ # + # Log source: compute metrics from feature serving logs + # ------------------------------------------------------------------ # + + def compute_log_metrics( + self, + project: str, + feature_service_name: str, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + granularity: str = "daily", + set_baseline: bool = False, + ) -> Dict[str, Any]: + """Compute monitoring metrics from feature serving logs. + + Requires the feature service to have a logging_config with a + LoggingDestination that can be converted to a DataSource. + """ + self._ensure_monitoring_tables() + if granularity not in VALID_GRANULARITIES: + raise ValueError( + f"Invalid granularity '{granularity}'. " + f"Must be one of {VALID_GRANULARITIES}" + ) + + start_time = time.time() + start_dt, end_dt = self._to_date_range(start_date, end_date) + + fs = self._store.registry.get_feature_service( + name=feature_service_name, project=project + ) + log_source = self._resolve_log_source(fs) + if log_source is None: + return { + "status": "skipped", + "reason": f"Feature service '{feature_service_name}' has no logging configured", + "duration_ms": int((time.time() - start_time) * 1000), + } + + data_source, ts_field, feature_fields, log_col_map = log_source + metrics_list = self._compute_from_source( + data_source, + ts_field, + feature_fields, + start_dt, + end_dt, + ) + + now = datetime.now(timezone.utc) + metric_date = start_dt.date() + + self._save_log_metrics( + project=project, + feature_service_name=feature_service_name, + log_col_map=log_col_map, + metrics_list=metrics_list, + metric_date=metric_date, + granularity=granularity, + set_baseline=set_baseline, + now=now, + ) + + duration_ms = int((time.time() - start_time) * 1000) + return { + "status": "completed", + "data_source_type": "log", + "feature_service_name": feature_service_name, + "granularity": granularity, + "computed_features": len(metrics_list), + "metric_date": metric_date.isoformat(), + "duration_ms": duration_ms, + } + + def auto_compute_log_metrics( + self, + project: Optional[str] = None, + feature_service_name: Optional[str] = None, + ) -> Dict[str, Any]: + """Auto-detect date ranges from log data and compute all granularities.""" + start_time = time.time() + self._ensure_monitoring_tables() + if project is None: + project = self._store.config.project + + if feature_service_name: + services = [ + self._store.registry.get_feature_service( + name=feature_service_name, project=project + ) + ] + else: + services = self._store.registry.list_feature_services(project=project) + + total_features = 0 + total_services = 0 + granularities_computed: set = set() + + for fs in services: + try: + log_source = self._resolve_log_source(fs) + if log_source is None: + continue + + data_source, ts_field, feature_fields, log_col_map = log_source + + max_ts = self._get_max_timestamp_for_source(data_source, ts_field) + if max_ts is None: + logger.warning( + "No log data found for feature service '%s', skipping", + fs.name, + ) + continue + + now = datetime.now(timezone.utc) + + for gran, window in GRANULARITY_WINDOWS.items(): + window_start = max_ts - window + metrics_list = self._compute_from_source( + data_source, + ts_field, + feature_fields, + window_start, + max_ts, + ) + self._save_log_metrics( + project=project, + feature_service_name=fs.name, + log_col_map=log_col_map, + metrics_list=metrics_list, + metric_date=window_start.date(), + granularity=gran, + set_baseline=False, + now=now, + ) + total_features += len(metrics_list) + granularities_computed.add(gran) + + total_services += 1 + except Exception: + logger.exception( + "Failed to auto-compute log metrics for feature service '%s'", + fs.name, + ) + + duration_ms = int((time.time() - start_time) * 1000) + return { + "status": "completed", + "data_source_type": "log", + "computed_feature_services": total_services, + "computed_features": total_features, + "granularities": sorted(granularities_computed), + "duration_ms": duration_ms, + } + + # ------------------------------------------------------------------ # + # Baseline: compute from all available source data + # ------------------------------------------------------------------ # + + def compute_baseline( + self, + project: Optional[str] = None, + feature_view_name: Optional[str] = None, + feature_names: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Compute baseline metrics from all available source data. + + Idempotent: only features without existing baselines are computed. + """ + start_time = time.time() + self._ensure_monitoring_tables() + if project is None: + project = self._store.config.project + + feature_views = self._resolve_feature_views(project, feature_view_name) + total_features = 0 + total_views = 0 + + for fv in feature_views: + try: + fields_needing_baseline = self._get_features_without_baseline( + project, fv, feature_names + ) + if not fields_needing_baseline: + logger.info( + "All features in '%s' already have baselines, skipping", + fv.name, + ) + continue + + feature_fields = self._classify_fields( + fv, fields=fields_needing_baseline + ) + if not feature_fields: + continue + + metrics_list = self._compute_feature_metrics( + fv, + feature_fields, + _EPOCH, + _FAR_FUTURE, + ) + + now = datetime.now(timezone.utc) + self._save_computed_metrics( + project=project, + feature_view=fv, + metrics_list=metrics_list, + metric_date=date.today(), + granularity="baseline", + set_baseline=True, + now=now, + ) + + total_features += len(metrics_list) + total_views += 1 + except Exception: + logger.exception( + "Failed to compute baseline for feature view '%s'", fv.name + ) + + duration_ms = int((time.time() - start_time) * 1000) + + return { + "status": "completed", + "computed_features": total_features, + "computed_feature_views": total_views, + "is_baseline": True, + "duration_ms": duration_ms, + } + + # ------------------------------------------------------------------ # + # Compute: explicit dates + granularity (stored) + # ------------------------------------------------------------------ # + + def compute_metrics( + self, + project: str, + feature_view_name: Optional[str] = None, + feature_names: Optional[List[str]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + granularity: str = "daily", + set_baseline: bool = False, + ) -> Dict[str, Any]: + self._ensure_monitoring_tables() + if granularity not in VALID_GRANULARITIES: + raise ValueError( + f"Invalid granularity '{granularity}'. " + f"Must be one of {VALID_GRANULARITIES}" + ) + + start_time = time.time() + start_dt, end_dt = self._to_date_range(start_date, end_date) + + feature_views = self._resolve_feature_views(project, feature_view_name) + + total_features = 0 + total_views = 0 + computed_dates: set = set() + + for fv in feature_views: + try: + fv_metrics = self._compute_for_feature_view( + project=project, + feature_view=fv, + feature_names=feature_names, + start_dt=start_dt, + end_dt=end_dt, + granularity=granularity, + set_baseline=set_baseline, + ) + total_features += fv_metrics["feature_count"] + total_views += 1 + computed_dates.update(fv_metrics["dates"]) + except Exception: + logger.exception( + "Failed to compute metrics for feature view '%s'", fv.name + ) + + total_services = self._compute_feature_service_metrics( + project=project, + granularity=granularity, + metric_dates=list(computed_dates), + set_baseline=set_baseline, + ) + + duration_ms = int((time.time() - start_time) * 1000) + + return { + "status": "completed", + "granularity": granularity, + "computed_features": total_features, + "computed_feature_views": total_views, + "computed_feature_services": total_services, + "metric_dates": sorted(d.isoformat() for d in computed_dates), + "duration_ms": duration_ms, + } + + # ------------------------------------------------------------------ # + # Transient compute (not stored) + # ------------------------------------------------------------------ # + + def compute_transient( + self, + project: str, + feature_view_name: str, + feature_names: Optional[List[str]] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> Dict[str, Any]: + """Compute metrics on-the-fly for an arbitrary date range without persisting.""" + start_time = time.time() + start_dt, end_dt = self._to_date_range(start_date, end_date) + effective_start = start_date or (date.today() - timedelta(days=1)) + effective_end = end_date or date.today() + + fv = self._store.registry.get_feature_view( + name=feature_view_name, project=project + ) + + feature_fields = self._classify_fields(fv, feature_names=feature_names) + if not feature_fields: + return { + "status": "completed", + "feature_view_name": feature_view_name, + "start_date": effective_start.isoformat(), + "end_date": effective_end.isoformat(), + "metrics": [], + "duration_ms": int((time.time() - start_time) * 1000), + } + + metrics_list = self._compute_feature_metrics( + fv, + feature_fields, + start_dt, + end_dt, + ) + + for m in metrics_list: + m["feature_view_name"] = feature_view_name + m["start_date"] = effective_start.isoformat() + m["end_date"] = effective_end.isoformat() + + return { + "status": "completed", + "feature_view_name": feature_view_name, + "start_date": effective_start.isoformat(), + "end_date": effective_end.isoformat(), + "metrics": metrics_list, + "duration_ms": int((time.time() - start_time) * 1000), + } + + # ------------------------------------------------------------------ # + # DQM Job helpers + # ------------------------------------------------------------------ # + + def submit_job( + self, + project: str, + job_type: str, + feature_view_name: Optional[str] = None, + parameters: Optional[Dict[str, Any]] = None, + ) -> str: + return self.job_manager.submit( + project=project, + job_type=job_type, + feature_view_name=feature_view_name, + parameters=parameters, + ) + + def get_job(self, job_id: str) -> Optional[Dict[str, Any]]: + return self.job_manager.get_job(job_id) + + def execute_job(self, job_id: str) -> Dict[str, Any]: + return self.job_manager.execute_job(job_id, self) + + # ------------------------------------------------------------------ # + # Read helpers (delegate to offline store) + # ------------------------------------------------------------------ # + + def _query( + self, + metric_type: str, + project: str, + filters=None, + start_date=None, + end_date=None, + ): + self._ensure_monitoring_tables() + rows = self._get_offline_store().query_monitoring_metrics( + config=self._store.config, + project=project, + metric_type=metric_type, + filters=filters, + start_date=start_date, + end_date=end_date, + ) + return [_sanitize_floats(r) for r in rows] + + def get_feature_metrics( + self, + project: str, + feature_service_name: Optional[str] = None, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + granularity: Optional[str] = None, + data_source_type: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + filters = { + "feature_view_name": feature_view_name, + "feature_name": feature_name, + "granularity": granularity, + "data_source_type": data_source_type, + } + if feature_service_name: + return self._get_metrics_by_service( + project, + feature_service_name, + lambda fv_name: self._query( + "feature", + project, + {**filters, "feature_view_name": fv_name}, + start_date, + end_date, + ), + ) + return self._query("feature", project, filters, start_date, end_date) + + def get_feature_view_metrics( + self, + project: str, + feature_service_name: Optional[str] = None, + feature_view_name: Optional[str] = None, + granularity: Optional[str] = None, + data_source_type: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + filters = { + "feature_view_name": feature_view_name, + "granularity": granularity, + "data_source_type": data_source_type, + } + if feature_service_name: + return self._get_metrics_by_service( + project, + feature_service_name, + lambda fv_name: self._query( + "feature_view", + project, + {**filters, "feature_view_name": fv_name}, + start_date, + end_date, + ), + ) + return self._query("feature_view", project, filters, start_date, end_date) + + def get_feature_service_metrics( + self, + project: str, + feature_service_name: Optional[str] = None, + granularity: Optional[str] = None, + data_source_type: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + filters = { + "feature_service_name": feature_service_name, + "granularity": granularity, + "data_source_type": data_source_type, + } + return self._query("feature_service", project, filters, start_date, end_date) + + def get_baseline( + self, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + data_source_type: Optional[str] = None, + ) -> List[Dict[str, Any]]: + filters = { + "feature_view_name": feature_view_name, + "feature_name": feature_name, + "data_source_type": data_source_type, + "is_baseline": True, + } + return self._query("feature", project, filters) + + def get_timeseries( + self, + project: str, + feature_view_name: Optional[str] = None, + feature_name: Optional[str] = None, + feature_service_name: Optional[str] = None, + granularity: Optional[str] = None, + data_source_type: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + ) -> List[Dict[str, Any]]: + return self.get_feature_metrics( + project=project, + feature_service_name=feature_service_name, + feature_view_name=feature_view_name, + feature_name=feature_name, + granularity=granularity, + data_source_type=data_source_type, + start_date=start_date, + end_date=end_date, + ) + + # ------------------------------------------------------------------ # + # Auto-baseline trigger for feast apply + # ------------------------------------------------------------------ # + + def submit_baseline_for_new_features( + self, + project: str, + feature_views: Optional[List] = None, + ) -> List[str]: + """Submit baseline DQM jobs for feature views with new features. + + Called from feast apply. Returns list of submitted job IDs. + Idempotent — only features without existing baselines are included. + """ + if project is None: + project = self._store.config.project + + if feature_views is None: + feature_views = self._store.registry.list_feature_views(project=project) + + job_ids = [] + for fv in feature_views: + new_features = self._get_features_without_baseline(project, fv) + if not new_features: + continue + + feature_names = [f.name for f in new_features] + job_id = self.job_manager.submit( + project=project, + job_type="baseline", + feature_view_name=fv.name, + parameters={"feature_names": feature_names}, + ) + job_ids.append(job_id) + logger.info( + "Queued baseline computation for '%s' features %s (job: %s)", + fv.name, + feature_names, + job_id, + ) + + return job_ids + + # ------------------------------------------------------------------ # + # Private: compute engine dispatch (SQL push-down → Python fallback) + # ------------------------------------------------------------------ # + + def _compute_feature_metrics( + self, + feature_view, + feature_fields: List[Tuple[str, str]], + start_dt: datetime, + end_dt: datetime, + ) -> List[Dict[str, Any]]: + """Compute metrics from a feature view's batch source.""" + return self._compute_from_source( + feature_view.batch_source, + feature_view.batch_source.timestamp_field, + feature_fields, + start_dt, + end_dt, + ) + + def _get_max_timestamp(self, feature_view) -> Optional[datetime]: + """Query the batch source for MAX(event_timestamp).""" + return self._get_max_timestamp_for_source( + feature_view.batch_source, + feature_view.batch_source.timestamp_field, + ) + + # ------------------------------------------------------------------ # + # Private: shared helpers (DRY) + # ------------------------------------------------------------------ # + + @staticmethod + def _to_date_range( + start_date: Optional[date], end_date: Optional[date] + ) -> Tuple[datetime, datetime]: + today = date.today() + if end_date is None: + end_date = today + if start_date is None: + start_date = end_date - timedelta(days=1) + start_dt = datetime( + start_date.year, start_date.month, start_date.day, tzinfo=timezone.utc + ) + end_dt = datetime( + end_date.year, end_date.month, end_date.day, 23, 59, 59, tzinfo=timezone.utc + ) + return start_dt, end_dt + + @staticmethod + def _classify_fields( + feature_view, + feature_names=None, + fields=None, + ) -> List[Tuple[str, str]]: + """Extract and classify features as numeric/categorical. + + Args: + feature_view: FeatureView to extract fields from (used if fields is None). + feature_names: Optional filter list of feature names. + fields: Optional pre-selected Field objects (e.g., from idempotency check). + """ + if fields is None: + fields = feature_view.features + if feature_names: + fields = [f for f in fields if f.name in feature_names] + + result = [] + for field in fields: + ftype = MetricsCalculator.classify_feature(field.dtype) + if ftype is None: + logger.warning( + "Unsupported dtype '%s' for feature '%s', skipping", + field.dtype, + field.name, + ) + continue + result.append((field.name, ftype)) + return result + + def _save_computed_metrics( + self, + project: str, + feature_view, + metrics_list: List[Dict[str, Any]], + metric_date: date, + granularity: str, + set_baseline: bool, + now: datetime, + ) -> None: + if not metrics_list: + return + + offline_store = self._get_offline_store() + config = self._store.config + + if set_baseline: + offline_store.clear_monitoring_baseline( + config=config, + project=project, + feature_view_name=feature_view.name, + ) + + for m in metrics_list: + m["project_id"] = project + m["feature_view_name"] = feature_view.name + m["metric_date"] = metric_date + m["granularity"] = granularity + m["data_source_type"] = "batch" + m["computed_at"] = now + m["is_baseline"] = set_baseline + + offline_store.save_monitoring_metrics(config, "feature", metrics_list) + + view_metric = { + "project_id": project, + "feature_view_name": feature_view.name, + "metric_date": metric_date, + "granularity": granularity, + "data_source_type": "batch", + "computed_at": now, + "is_baseline": set_baseline, + **build_view_aggregate(metrics_list), + } + offline_store.save_monitoring_metrics(config, "feature_view", [view_metric]) + + def _resolve_join_key_columns(self, feature_view) -> List[str]: + config = self._store.config + return ( + [ + entity.name + for entity in self._store.registry.list_entities(project=config.project) + if entity.name in (feature_view.entities or []) + ] + or feature_view.entities + or [] + ) + + def _get_metrics_by_service( + self, project: str, feature_service_name: str, query_fn + ): + fs = self._store.registry.get_feature_service( + name=feature_service_name, project=project + ) + fv_names = [proj.name for proj in fs.feature_view_projections] + results = [] + for fv_name in fv_names: + results.extend(query_fn(fv_name)) + return results + + def _resolve_feature_views(self, project: str, feature_view_name: Optional[str]): + if feature_view_name: + fv = self._store.registry.get_feature_view( + name=feature_view_name, project=project + ) + return [fv] + return self._store.registry.list_feature_views(project=project) + + def _get_features_without_baseline(self, project, feature_view, feature_names=None): + existing = self.get_baseline( + project=project, + feature_view_name=feature_view.name, + ) + existing_names = {m["feature_name"] for m in existing} + + fields = feature_view.features + if feature_names: + fields = [f for f in fields if f.name in feature_names] + + return [f for f in fields if f.name not in existing_names] + + def _compute_for_feature_view( + self, + project: str, + feature_view, + feature_names: Optional[List[str]], + start_dt: datetime, + end_dt: datetime, + granularity: str, + set_baseline: bool, + ) -> Dict[str, Any]: + feature_fields = self._classify_fields( + feature_view, feature_names=feature_names + ) + if not feature_fields: + return {"feature_count": 0, "dates": set()} + + metrics_list = self._compute_feature_metrics( + feature_view, + feature_fields, + start_dt, + end_dt, + ) + + now = datetime.now(timezone.utc) + metric_date = start_dt.date() + + self._save_computed_metrics( + project=project, + feature_view=feature_view, + metrics_list=metrics_list, + metric_date=metric_date, + granularity=granularity, + set_baseline=set_baseline, + now=now, + ) + + return {"feature_count": len(metrics_list), "dates": {metric_date}} + + # ------------------------------------------------------------------ # + # Private: log source helpers + # ------------------------------------------------------------------ # + + def _resolve_log_source(self, feature_service): + """Resolve log data source for a feature service. + + Returns (DataSource, timestamp_field, feature_fields, log_col_map) + or None if the feature service has no logging configured. + + ``feature_fields`` uses the raw log column names (needed for + SQL/PyArrow column access). ``log_col_map`` maps each raw log + column to ``(feature_view_name, normalized_feature_name)`` so + callers can store metrics under the correct view and feature + name — critical for drift detection across batch and log sources. + """ + if not feature_service.logging_config: + return None + + destination = feature_service.logging_config.destination + try: + data_source = destination.to_data_source() + except NotImplementedError: + logger.warning( + "Logging destination for '%s' does not support to_data_source()", + feature_service.name, + ) + return None + + logging_source = FeatureServiceLoggingSource( + feature_service, + self._store.config.project, + ) + schema = logging_source.get_schema(self._store.registry) + + skip_cols = { + LOG_TIMESTAMP_FIELD, + "__log_date", + "__request_id", + } + entity_columns = set() + view_feature_names: dict = {} + for proj in feature_service.feature_view_projections: + view_alias = proj.name_to_use() + try: + fv = self._store.registry.get_feature_view( + name=proj.name, project=self._store.config.project + ) + for ec in fv.entity_columns: + entity_columns.add(ec.name) + except Exception: + pass + for feat in proj.features: + log_col = f"{view_alias}__{feat.name}" + view_feature_names[log_col] = (proj.name, feat.name) + + feature_fields = [] + log_col_map: dict = {} + for field in schema: + if field.name in skip_cols or field.name in entity_columns: + continue + if field.name.endswith("__timestamp") or field.name.endswith("__status"): + continue + ftype = MetricsCalculator.classify_feature_arrow(field.type) + if ftype is not None: + feature_fields.append((field.name, ftype)) + if field.name in view_feature_names: + log_col_map[field.name] = view_feature_names[field.name] + + if not feature_fields: + return None + + return data_source, LOG_TIMESTAMP_FIELD, feature_fields, log_col_map + + def _get_max_timestamp_for_source(self, data_source, ts_field): + """Get MAX timestamp from an arbitrary data source. + + Prefers the offline store's native push-down; falls back to reading + the table and computing max in Python. + """ + offline_store = self._get_offline_store() + try: + return offline_store.get_monitoring_max_timestamp( + config=self._store.config, + data_source=data_source, + timestamp_field=ts_field, + ) + except NotImplementedError: + pass + + import pyarrow.compute as pc + + retrieval_job = offline_store.pull_all_from_table_or_query( + config=self._store.config, + data_source=data_source, + join_key_columns=[], + feature_name_columns=[], + timestamp_field=ts_field, + start_date=_EPOCH, + end_date=_FAR_FUTURE, + ) + + table = retrieval_job.to_arrow() + if ts_field not in table.column_names or len(table) == 0: + return None + + max_val = pc.max(table.column(ts_field)).as_py() + if max_val is None: + return None + + if isinstance(max_val, datetime): + return max_val if max_val.tzinfo else max_val.replace(tzinfo=timezone.utc) + return datetime.combine(max_val, datetime.min.time(), tzinfo=timezone.utc) + + def _compute_from_source( + self, + data_source, + ts_field: str, + feature_fields: List[Tuple[str, str]], + start_dt: datetime, + end_dt: datetime, + ) -> List[Dict[str, Any]]: + """Compute metrics from an arbitrary data source (batch or log). + + Prefers SQL push-down; falls back to Python-based computation. + """ + offline_store = self._get_offline_store() + try: + return offline_store.compute_monitoring_metrics( + config=self._store.config, + data_source=data_source, + feature_columns=feature_fields, + timestamp_field=ts_field, + start_date=start_dt, + end_date=end_dt, + histogram_bins=self._calculator.histogram_bins, + top_n=self._calculator.top_n, + ) + except NotImplementedError: + logger.debug( + "Offline store does not support compute_monitoring_metrics, " + "falling back to Python-based computation for log source" + ) + retrieval_job = offline_store.pull_all_from_table_or_query( + config=self._store.config, + data_source=data_source, + join_key_columns=[], + feature_name_columns=[name for name, _ in feature_fields], + timestamp_field=ts_field, + start_date=start_dt, + end_date=end_dt, + ) + arrow_table = retrieval_job.to_arrow() + return self._calculator.compute_all(arrow_table, feature_fields) + + def _save_log_metrics( + self, + project: str, + feature_service_name: str, + log_col_map: Dict[str, Tuple[str, str]], + metrics_list: List[Dict[str, Any]], + metric_date: date, + granularity: str, + set_baseline: bool, + now: datetime, + ) -> None: + """Save log-sourced metrics tagged with data_source_type='log'. + + Normalizes log column names (``driver_stats__conv_rate``) back to + their originating ``feature_view_name`` and ``feature_name`` so + that drift detection can join batch and log metrics on the same + feature identity. + """ + if not metrics_list: + return + + offline_store = self._get_offline_store() + config = self._store.config + + for m in metrics_list: + log_col = m.get("feature_name", "") + view_name, feat_name = log_col_map.get( + log_col, (feature_service_name, log_col) + ) + m["project_id"] = project + m["feature_view_name"] = view_name + m["feature_name"] = feat_name + m["metric_date"] = metric_date + m["granularity"] = granularity + m["data_source_type"] = "log" + m["computed_at"] = now + m["is_baseline"] = set_baseline + + offline_store.save_monitoring_metrics(config, "feature", metrics_list) + + # --- per-feature-view aggregates (grouped by originating view) --- + by_view: Dict[str, List[Dict[str, Any]]] = defaultdict(list) + for m in metrics_list: + by_view[m["feature_view_name"]].append(m) + + view_metrics = [ + { + "project_id": project, + "feature_view_name": vname, + "metric_date": metric_date, + "granularity": granularity, + "data_source_type": "log", + "computed_at": now, + "is_baseline": set_baseline, + **build_view_aggregate(vmetrics), + } + for vname, vmetrics in by_view.items() + ] + offline_store.save_monitoring_metrics(config, "feature_view", view_metrics) + + # --- feature service aggregate --- + svc_agg = build_view_aggregate(metrics_list) + svc_metric = { + "project_id": project, + "feature_service_name": feature_service_name, + "metric_date": metric_date, + "granularity": granularity, + "data_source_type": "log", + "computed_at": now, + "is_baseline": set_baseline, + "total_feature_views": len(by_view), + "total_features": svc_agg["total_features"], + "avg_null_rate": svc_agg["avg_null_rate"], + "max_null_rate": svc_agg["max_null_rate"], + } + offline_store.save_monitoring_metrics(config, "feature_service", [svc_metric]) + + def _read_batch_source(self, feature_view, feature_fields, start_dt, end_dt): + config = self._store.config + data_source = feature_view.batch_source + offline_store = self._get_offline_store() + + retrieval_job = offline_store.pull_all_from_table_or_query( + config=config, + data_source=data_source, + join_key_columns=self._resolve_join_key_columns(feature_view), + feature_name_columns=[name for name, _ in feature_fields], + timestamp_field=data_source.timestamp_field, + created_timestamp_column=data_source.created_timestamp_column, + start_date=start_dt, + end_date=end_dt, + ) + + return retrieval_job.to_arrow() + + def _compute_feature_service_metrics( + self, + project: str, + granularity: str, + metric_dates: List[date], + set_baseline: bool, + ) -> int: + if not metric_dates: + return 0 + + feature_services = self._store.registry.list_feature_services(project=project) + if not feature_services: + return 0 + + offline_store = self._get_offline_store() + config = self._store.config + now = datetime.now(timezone.utc) + count = 0 + + for fs in feature_services: + try: + fv_names = {proj.name for proj in fs.feature_view_projections} + + for metric_date in metric_dates: + fv_metrics = offline_store.query_monitoring_metrics( + config=config, + project=project, + metric_type="feature_view", + filters={ + "granularity": granularity, + "data_source_type": "batch", + }, + start_date=metric_date, + end_date=metric_date, + ) + + relevant = [ + m for m in fv_metrics if m.get("feature_view_name") in fv_names + ] + if not relevant: + continue + + null_rates = [ + m["avg_null_rate"] + for m in relevant + if m.get("avg_null_rate") is not None + ] + + service_metric = { + "project_id": project, + "feature_service_name": fs.name, + "metric_date": metric_date + if isinstance(metric_date, date) + else date.fromisoformat(str(metric_date)), + "granularity": granularity, + "data_source_type": "batch", + "computed_at": now, + "is_baseline": set_baseline, + "total_feature_views": len(relevant), + "total_features": sum( + m.get("total_features", 0) for m in relevant + ), + "avg_null_rate": ( + sum(null_rates) / len(null_rates) if null_rates else 0.0 + ), + "max_null_rate": max(null_rates) if null_rates else 0.0, + } + offline_store.save_monitoring_metrics( + config, + "feature_service", + [service_metric], + ) + count += 1 + except Exception: + logger.exception("Failed to compute service metrics for '%s'", fs.name) + + return count diff --git a/sdk/python/feast/monitoring/monitoring_utils.py b/sdk/python/feast/monitoring/monitoring_utils.py new file mode 100644 index 00000000000..0450e008a05 --- /dev/null +++ b/sdk/python/feast/monitoring/monitoring_utils.py @@ -0,0 +1,296 @@ +"""Shared constants and helpers for monitoring across all offline store backends. + +Every backend needs the same table names, column lists, primary keys, +empty-metric templates, and result-row normalization. Centralizing them +here avoids ~8x duplication and prevents column-list drift. +""" + +import json +import math +from datetime import date, datetime +from typing import Any, Dict, List, Optional, Tuple + +# ------------------------------------------------------------------ # +# Table / file names +# ------------------------------------------------------------------ # + +MON_TABLE_FEATURE = "feast_monitoring_feature_metrics" +MON_TABLE_FEATURE_VIEW = "feast_monitoring_feature_view_metrics" +MON_TABLE_FEATURE_SERVICE = "feast_monitoring_feature_service_metrics" +MON_TABLE_JOB = "feast_monitoring_jobs" + +MONITORING_DIR = "feast_monitoring" + +MONITORING_PARQUET_FILES: Dict[str, str] = { + "feature": "feast_monitoring_feature_metrics.parquet", + "feature_view": "feast_monitoring_feature_view_metrics.parquet", + "feature_service": "feast_monitoring_feature_service_metrics.parquet", + "job": "feast_monitoring_jobs.parquet", +} + +# ------------------------------------------------------------------ # +# Column definitions — (ordered, used by INSERT / SELECT / Parquet) +# ------------------------------------------------------------------ # + +FEATURE_METRICS_COLUMNS: List[str] = [ + "project_id", + "feature_view_name", + "feature_name", + "metric_date", + "granularity", + "data_source_type", + "computed_at", + "is_baseline", + "feature_type", + "row_count", + "null_count", + "null_rate", + "mean", + "stddev", + "min_val", + "max_val", + "p50", + "p75", + "p90", + "p95", + "p99", + "histogram", +] + +FEATURE_METRICS_PK: List[str] = [ + "project_id", + "feature_view_name", + "feature_name", + "metric_date", + "granularity", + "data_source_type", +] + +FEATURE_VIEW_METRICS_COLUMNS: List[str] = [ + "project_id", + "feature_view_name", + "metric_date", + "granularity", + "data_source_type", + "computed_at", + "is_baseline", + "total_row_count", + "total_features", + "features_with_nulls", + "avg_null_rate", + "max_null_rate", +] + +FEATURE_VIEW_METRICS_PK: List[str] = [ + "project_id", + "feature_view_name", + "metric_date", + "granularity", + "data_source_type", +] + +FEATURE_SERVICE_METRICS_COLUMNS: List[str] = [ + "project_id", + "feature_service_name", + "metric_date", + "granularity", + "data_source_type", + "computed_at", + "is_baseline", + "total_feature_views", + "total_features", + "avg_null_rate", + "max_null_rate", +] + +FEATURE_SERVICE_METRICS_PK: List[str] = [ + "project_id", + "feature_service_name", + "metric_date", + "granularity", + "data_source_type", +] + +JOB_COLUMNS: List[str] = [ + "job_id", + "project_id", + "feature_view_name", + "job_type", + "status", + "parameters", + "metric_date", + "started_at", + "completed_at", + "error_message", + "result_summary", +] + +JOB_PK: List[str] = [ + "job_id", +] + + +def monitoring_table_meta( + metric_type: str, +) -> Tuple[str, List[str], List[str]]: + """Return (table_name, columns, pk_columns) for a metric type. + + Raises ValueError for unknown metric types. + """ + if metric_type == "feature": + return MON_TABLE_FEATURE, FEATURE_METRICS_COLUMNS, FEATURE_METRICS_PK + if metric_type == "feature_view": + return ( + MON_TABLE_FEATURE_VIEW, + FEATURE_VIEW_METRICS_COLUMNS, + FEATURE_VIEW_METRICS_PK, + ) + if metric_type == "feature_service": + return ( + MON_TABLE_FEATURE_SERVICE, + FEATURE_SERVICE_METRICS_COLUMNS, + FEATURE_SERVICE_METRICS_PK, + ) + if metric_type == "job": + return MON_TABLE_JOB, JOB_COLUMNS, JOB_PK + raise ValueError(f"Unknown monitoring metric_type: '{metric_type}'") + + +def monitoring_parquet_meta( + metric_type: str, +) -> Tuple[str, List[str], List[str]]: + """Return (parquet_filename, columns, pk_columns) for file-based backends. + + File names match the SQL table names with a ``.parquet`` extension so + the mapping between backends is consistent. + """ + fname = MONITORING_PARQUET_FILES.get(metric_type) + if fname is None: + raise ValueError(f"Unknown monitoring metric_type: '{metric_type}'") + _, columns, pk = monitoring_table_meta(metric_type) + return fname, columns, pk + + +# ------------------------------------------------------------------ # +# Tiny helpers duplicated across backends +# ------------------------------------------------------------------ # + + +def opt_float(val: Any) -> Optional[float]: + """Safely cast a value to float, returning None for None/NaN/Inf.""" + if val is None: + return None + f = float(val) + if math.isnan(f) or math.isinf(f): + return None + return f + + +def empty_numeric_metric(feature_name: str) -> Dict[str, Any]: + """Return a metric dict with all-None stats for a numeric feature.""" + return { + "feature_name": feature_name, + "feature_type": "numeric", + "row_count": 0, + "null_count": 0, + "null_rate": 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": None, + } + + +def empty_categorical_metric(feature_name: str) -> Dict[str, Any]: + """Return a metric dict with all-None stats for a categorical feature.""" + return { + "feature_name": feature_name, + "feature_type": "categorical", + "row_count": 0, + "null_count": 0, + "null_rate": 0.0, + "mean": None, + "stddev": None, + "min_val": None, + "max_val": None, + "p50": None, + "p75": None, + "p90": None, + "p95": None, + "p99": None, + "histogram": None, + } + + +# ------------------------------------------------------------------ # +# Result-row normalization (used after SQL fetch or Parquet read) +# ------------------------------------------------------------------ # + + +def normalize_monitoring_row(record: Dict[str, Any]) -> Dict[str, Any]: + """Normalize a monitoring metric dict for JSON serialization. + + - Replaces float NaN / Inf with None (not JSON-serializable). + - Parses ``histogram`` from JSON string if needed. + - Converts ``metric_date`` / ``computed_at`` to ISO strings. + - Normalizes ``is_baseline`` to Python bool. + """ + import math + + for key, val in record.items(): + if isinstance(val, float) and (math.isnan(val) or math.isinf(val)): + record[key] = None + + hist = record.get("histogram") + if isinstance(hist, str): + try: + record["histogram"] = json.loads(hist) + except (json.JSONDecodeError, TypeError): + pass + + for key in ("metric_date", "computed_at"): + val = record.get(key) + if isinstance(val, (date, datetime)): + record[key] = val.isoformat() + + baseline = record.get("is_baseline") + if baseline is not None: + record["is_baseline"] = bool(baseline) + + return record + + +# ------------------------------------------------------------------ # +# View-level aggregate builder (shared by batch + log save paths) +# ------------------------------------------------------------------ # + + +def build_view_aggregate( + metrics_list: List[Dict[str, Any]], +) -> Dict[str, Any]: + """Compute view-level aggregate stats from per-feature metrics. + + Returns a dict with keys: total_row_count, total_features, + features_with_nulls, avg_null_rate, max_null_rate. + """ + null_rates = [ + m["null_rate"] for m in metrics_list if m.get("null_rate") is not None + ] + return { + "total_row_count": max( + (m["row_count"] for m in metrics_list if m.get("row_count") is not None), + default=0, + ), + "total_features": len(metrics_list), + "features_with_nulls": sum( + 1 for m in metrics_list if (m.get("null_count") or 0) > 0 + ), + "avg_null_rate": sum(null_rates) / len(null_rates) if null_rates else 0.0, + "max_null_rate": max(null_rates) if null_rates else 0.0, + } diff --git a/sdk/python/feast/offline_server.py b/sdk/python/feast/offline_server.py index 6bc573888fc..e82b5239767 100644 --- a/sdk/python/feast/offline_server.py +++ b/sdk/python/feast/offline_server.py @@ -7,36 +7,78 @@ from datetime import datetime from typing import Any, Dict, List, Optional, cast -import click -import pyarrow as pa -import pyarrow.flight as fl -from google.protobuf.json_format import Parse - -from feast import FeatureStore, FeatureView, utils -from feast.arrow_error_handler import arrow_server_error_handling_decorator -from feast.data_source import DataSource -from feast.feature_logging import FeatureServiceLoggingSource -from feast.feature_view import DUMMY_ENTITY_NAME -from feast.infra.offline_stores.offline_utils import get_offline_store_from_config -from feast.permissions.action import AuthzedAction -from feast.permissions.security_manager import assert_permissions -from feast.permissions.server.arrow import ( +_FIPS_CIPHER_SUITES = ":".join( + [ + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES256-GCM-SHA384", + "AES128-GCM-SHA256", + "AES256-GCM-SHA384", + ] +) + + +def _is_fips_enabled() -> bool: + try: + with open("/proc/sys/crypto/fips_enabled") as f: + return f.read().strip() == "1" + except (FileNotFoundError, PermissionError, OSError): + return False + + +def _configure_grpc_fips() -> bool: + if _is_fips_enabled() and "GRPC_SSL_CIPHER_SUITES" not in os.environ: + os.environ["GRPC_SSL_CIPHER_SUITES"] = _FIPS_CIPHER_SUITES + return True + return False + + +# On FIPS-enabled systems (notably IBM Power ppc64le), gRPC reads +# GRPC_SSL_CIPHER_SUITES during shared-library initialization. The env var +# must be set before any gRPC-linked module (pyarrow.flight) is imported. +_fips_configured = _configure_grpc_fips() + +import click # noqa: E402 +import pyarrow as pa # noqa: E402 +import pyarrow.flight as fl # noqa: E402 +from google.protobuf.json_format import Parse # noqa: E402 + +from feast import FeatureStore, FeatureView, utils # noqa: E402 +from feast.arrow_error_handler import ( # noqa: E402 + arrow_server_error_handling_decorator, +) +from feast.data_source import DataSource # noqa: E402 +from feast.errors import FeatureViewNotFoundException # noqa: E402 +from feast.feature_logging import FeatureServiceLoggingSource # noqa: E402 +from feast.feature_view import DUMMY_ENTITY_NAME # noqa: E402 +from feast.infra.offline_stores.offline_utils import ( # noqa: E402 + get_offline_store_from_config, +) +from feast.permissions.action import AuthzedAction # noqa: E402 +from feast.permissions.security_manager import assert_permissions # noqa: E402 +from feast.permissions.server.arrow import ( # noqa: E402 AuthorizationMiddlewareFactory, inject_user_details_decorator, ) -from feast.permissions.server.utils import ( +from feast.permissions.server.utils import ( # noqa: E402 AuthManagerType, ServerType, init_auth_manager, init_security_manager, str_to_auth_manager_type, ) -from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto -from feast.saved_dataset import SavedDatasetStorage +from feast.protos.feast.core.DataSource_pb2 import ( # noqa: E402 + DataSource as DataSourceProto, +) +from feast.saved_dataset import SavedDatasetStorage # noqa: E402 logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) +if _fips_configured: + logger.info("FIPS mode detected, configured FIPS-compliant gRPC cipher suites.") + class OfflineServer(fl.FlightServerBase): def __init__( @@ -199,7 +241,7 @@ def get_feature_view_by_name( ) fv = fv.with_projection(p) return fv - except Exception: + except FeatureViewNotFoundException: try: return self.store.registry.get_stream_feature_view( name=fv_name, project=project @@ -431,6 +473,13 @@ def get_historical_features(self, command: dict, key: Optional[str] = None): # Extract parameters from the internal flights dictionary entity_df_value = self.flights[key] entity_df = pa.Table.to_pandas(entity_df_value) + # Check if this is a mock/empty table (contains only 'key' column) + if len(entity_df.columns) == 1 and "key" in entity_df.columns: + entity_df = None + + # If the client sent a SQL string, use it directly + if entity_df is None and "entity_df_sql" in command: + entity_df = command["entity_df_sql"] feature_view_names = command["feature_view_names"] name_aliases = command["name_aliases"] @@ -588,6 +637,7 @@ def start_server( tls_cert_path: str = "", ): _init_auth_manager(store) + _configure_grpc_fips() tls_certificates = [] scheme = "grpc+tcp" diff --git a/sdk/python/feast/on_demand_feature_view.py b/sdk/python/feast/on_demand_feature_view.py index c985912a03e..3ab188da334 100644 --- a/sdk/python/feast/on_demand_feature_view.py +++ b/sdk/python/feast/on_demand_feature_view.py @@ -1,5 +1,6 @@ import copy import functools +import uuid import warnings from types import FunctionType from typing import Any, List, Optional, Union, cast @@ -13,9 +14,10 @@ from feast.data_source import RequestSource from feast.entity import Entity from feast.errors import RegistryInferenceFailure, SpecifiedFeaturesNotPresentError -from feast.feature_view import DUMMY_ENTITY_NAME, FeatureView +from feast.feature_view import DUMMY_ENTITY_NAME, FeatureView, FeatureViewState from feast.feature_view_projection import FeatureViewProjection from feast.field import Field, from_value_type +from feast.proto_utils import transformation_to_proto from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( OnDemandFeatureView as OnDemandFeatureViewProto, ) @@ -24,9 +26,6 @@ OnDemandFeatureViewSpec, OnDemandSource, ) -from feast.protos.feast.core.Transformation_pb2 import ( - FeatureTransformationV2 as FeatureTransformationProto, -) from feast.protos.feast.core.Transformation_pb2 import ( UserDefinedFunctionV2 as UserDefinedFunctionProto, ) @@ -37,11 +36,82 @@ from feast.transformation.substrait_transformation import SubstraitTransformation from feast.utils import _utc_now from feast.value_type import ValueType +from feast.version_utils import normalize_version_string warnings.simplefilter("once", DeprecationWarning) OnDemandSourceType = Union[FeatureView, FeatureViewProjection, RequestSource] +class ODFVErrorMessages: + """Centralized error message templates for OnDemandFeatureView.""" + + @staticmethod + def unsupported_source_type(source_type: type, supported_types: str) -> str: + return ( + f"Unsupported source type: {source_type.__name__}. " + f"Supported types are {supported_types}." + ) + + @staticmethod + def singleton_mode_requires_python(current_mode: str) -> str: + return ( + f"Singleton mode is only supported with mode='python', " + f"but mode='{current_mode}' was specified. Either disable singleton " + f"(singleton=False) or change mode to 'python'." + ) + + @staticmethod + def online_store_requires_entities() -> str: + return ( + "OnDemandFeatureView configured with write_to_online_store=True " + "must have at least one entity defined. Either add entities or " + "set write_to_online_store=False." + ) + + @staticmethod + def no_transformation_provided() -> str: + return ( + "OnDemandFeatureView must have a valid feature_transformation. " + "Provide either a udf parameter or a feature_transformation parameter." + ) + + @staticmethod + def duplicate_source_names(overlapping_names: set) -> str: + return ( + f"Source names must be unique across all source types. " + f"Found duplicate names: {overlapping_names}" + ) + + @staticmethod + def no_sources_configured() -> str: + return ( + "OnDemandFeatureView must have at least one source. " + "Add either FeatureView/FeatureViewProjection sources or RequestSource sources." + ) + + @staticmethod + def mode_transformation_mismatch( + mode: str, expected_type: str, actual_type: str + ) -> str: + return f"Mode '{mode}' requires {expected_type}, but got {actual_type}." + + @staticmethod + def unknown_source_type_in_proto(source_type: str | None) -> str: + return f"Unknown source type in protobuf: {source_type}" + + @staticmethod + def unsupported_transformation_type(transformation_type: str) -> str: + return f"Unsupported transformation type: {transformation_type}" + + @staticmethod + def backward_compatible_udf_missing() -> str: + return "Backward compatible UDF requires user_defined_function field" + + @staticmethod + def unsupported_mode_for_udf(mode: str) -> str: + return f"Unsupported mode '{mode}' for user_defined_function" + + @typechecked class OnDemandFeatureView(BaseFeatureView): """ @@ -61,23 +131,33 @@ class OnDemandFeatureView(BaseFeatureView): tags: A dictionary of key-value pairs to store arbitrary metadata. owner: The owner of the on demand feature view, typically the email of the primary maintainer. + org: The organizational unit that owns this on demand feature view (e.g. "ads", + "search"). Defaults to empty string. """ + _TRACK_METRICS_TAG = "feast:track_metrics" + _INPUT_SCHEMA_SOURCE_PREFIX = "__input_schema__" + name: str entities: Optional[List[str]] features: List[Field] source_feature_view_projections: dict[str, FeatureViewProjection] source_request_sources: dict[str, RequestSource] - feature_transformation: Transformation + feature_transformation: Optional[Transformation] mode: str description: str tags: dict[str, str] owner: str + org: str write_to_online_store: bool singleton: bool + track_metrics: bool udf: Optional[FunctionType] udf_string: Optional[str] aggregations: List[Aggregation] + enabled: bool + state: FeatureViewState + _raw_feature_transformation_proto: Optional[Any] = None def __init__( # noqa: C901 self, @@ -85,7 +165,8 @@ def __init__( # noqa: C901 name: str, entities: Optional[List[Entity]] = None, schema: Optional[List[Field]] = None, - sources: List[OnDemandSourceType], + sources: Optional[List[OnDemandSourceType]] = None, + input_schema: Optional[List[Field]] = None, udf: Optional[FunctionType] = None, udf_string: Optional[str] = "", feature_transformation: Optional[Transformation] = None, @@ -93,9 +174,13 @@ def __init__( # noqa: C901 description: str = "", tags: Optional[dict[str, str]] = None, owner: str = "", + org: str = "", write_to_online_store: bool = False, singleton: bool = False, + track_metrics: bool = False, aggregations: Optional[List[Aggregation]] = None, + version: str = "latest", + enabled: bool = True, ): """ Creates an OnDemandFeatureView object. @@ -108,6 +193,11 @@ def __init__( # noqa: C901 sources: A map from input source names to the actual input sources, which may be feature views, or request data sources. These sources serve as inputs to the udf, which will refer to them by name. + input_schema (optional): A list of Fields describing data that is accepted as input + but not stored directly as features — e.g. aggregation columns, normalization + parameters, thresholds, or other contextual values passed at request time. + When provided, sources is not required — an internal RequestSource will be + created automatically. udf: The user defined transformation function, which must take pandas dataframes as inputs. udf_string: The source code version of the udf (for diffing and displaying in Web UI) @@ -117,10 +207,17 @@ def __init__( # noqa: C901 tags (optional): A dictionary of key-value pairs to store arbitrary metadata. owner (optional): The owner of the on demand feature view, typically the email of the primary maintainer. + org (optional): The organizational unit that owns this feature view + (e.g. "ads", "search"). write_to_online_store (optional): A boolean that indicates whether to write the on demand feature view to the online store for faster retrieval. singleton (optional): A boolean that indicates whether the transformation is executed on a singleton (only applicable when mode="python"). + track_metrics (optional): Whether to emit Prometheus timing metrics + (``feast_feature_server_transformation_duration_seconds``) for + this ODFV. Defaults to ``False``. Set to ``True`` to opt in + to per-ODFV transformation duration tracking when the server + is started with metrics enabled. aggregations (optional): List of aggregations to apply before transformation. """ super().__init__( @@ -131,24 +228,49 @@ def __init__( # noqa: C901 owner=owner, ) + self.org = org + self.version = version schema = schema or [] self.entities = [e.name for e in entities] if entities else [DUMMY_ENTITY_NAME] - self.sources = sources + self.input_schema = input_schema self.mode = mode.lower() self.udf = udf self.udf_string = udf_string self.source_feature_view_projections: dict[str, FeatureViewProjection] = {} self.source_request_sources: dict[str, RequestSource] = {} - for odfv_source in sources: - if isinstance(odfv_source, RequestSource): - self.source_request_sources[odfv_source.name] = odfv_source - elif isinstance(odfv_source, FeatureViewProjection): - self.source_feature_view_projections[odfv_source.name] = odfv_source + self._input_schema_sentinel: Optional[RequestSource] = None + + # Strip any existing sentinel from sources (handles __copy__ round-trip) + effective_sources: List[OnDemandSourceType] = [ + s + for s in (sources or []) + if not ( + isinstance(s, RequestSource) + and s.name.startswith(self._INPUT_SCHEMA_SOURCE_PREFIX) + ) + ] - else: - self.source_feature_view_projections[odfv_source.name] = ( - odfv_source.projection - ) + if input_schema is not None: + # Automatically create an internal RequestSource from input_schema. + # Stored privately so it does not appear in source_request_sources for + # external consumers (e.g. the feature server, apply(), utils.py). + self._input_schema_sentinel = RequestSource( + name=f"{self._INPUT_SCHEMA_SOURCE_PREFIX}{name}", + schema=input_schema, + ) + self.source_request_sources[self._input_schema_sentinel.name] = ( + self._input_schema_sentinel + ) + elif not effective_sources: + raise ValueError( + "Either 'sources' or 'input_schema' must be provided for OnDemandFeatureView." + ) + + self.sources = effective_sources + + # Process each source with explicit type handling + for odfv_source in effective_sources: + self._add_source_to_collections(odfv_source) features: List[Field] = [] self.entity_columns = [] @@ -184,20 +306,68 @@ def __init__( # noqa: C901 features.append(field) self.features = features - self.feature_transformation = ( - feature_transformation or self.get_feature_transformation() - ) + if feature_transformation is not None: + self.feature_transformation = feature_transformation + elif self.udf is not None: + self.feature_transformation = self.get_feature_transformation() + else: + self.feature_transformation = None self.write_to_online_store = write_to_online_store self.singleton = singleton if self.singleton and self.mode != "python": - raise ValueError("Singleton is only supported for Python mode.") + raise ValueError( + ODFVErrorMessages.singleton_mode_requires_python(self.mode) + ) + self.track_metrics = track_metrics self.aggregations = aggregations or [] + self.enabled = enabled - def get_feature_transformation(self) -> Transformation: - if not self.udf: + self.state = FeatureViewState.STATE_UNSPECIFIED + + if input_schema is not None and self.aggregations: + input_field_names = {f.name for f in input_schema} + unknown = [ + agg.column + for agg in self.aggregations + if agg.column and agg.column not in input_field_names + ] + if unknown: + raise ValueError( + f"Aggregation column(s) {unknown} not found in input_schema " + f"for OnDemandFeatureView '{name}'. " + f"Available fields: {sorted(input_field_names)}" + ) + + def _add_source_to_collections(self, odfv_source: OnDemandSourceType) -> None: + """ + Add a source to the appropriate collection with explicit type checking. + + Args: + odfv_source: The source to add (RequestSource, FeatureViewProjection, or FeatureView) + + Raises: + ValueError: If the source type is not supported + """ + if isinstance(odfv_source, RequestSource): + self.source_request_sources[odfv_source.name] = odfv_source + elif isinstance(odfv_source, FeatureViewProjection): + self.source_feature_view_projections[odfv_source.name] = odfv_source + elif isinstance(odfv_source, FeatureView): + # FeatureView sources use their projection + self.source_feature_view_projections[odfv_source.name] = ( + odfv_source.projection + ) + else: raise ValueError( - "Either udf or feature_transformation must be provided to create an OnDemandFeatureView" + ODFVErrorMessages.unsupported_source_type( + type(odfv_source), + "RequestSource, FeatureViewProjection, and FeatureView", + ) ) + + def get_feature_transformation(self) -> Transformation: + if not self.udf: + raise ValueError(ODFVErrorMessages.no_transformation_provided()) if self.mode in ( TransformationMode.PANDAS, TransformationMode.PYTHON, @@ -222,26 +392,74 @@ def __copy__(self): schema=self.features, sources=list(self.source_feature_view_projections.values()) + list(self.source_request_sources.values()), + input_schema=self.input_schema, feature_transformation=self.feature_transformation, mode=self.mode, description=self.description, tags=self.tags, owner=self.owner, + org=self.org, write_to_online_store=self.write_to_online_store, singleton=self.singleton, + version=self.version, + track_metrics=self.track_metrics, + aggregations=self.aggregations, ) fv.entities = self.entities fv.features = self.features fv.projection = copy.copy(self.projection) fv.entity_columns = copy.copy(self.entity_columns) + fv.enabled = self.enabled + fv.state = self.state return fv + def _schema_or_udf_changed(self, other: "BaseFeatureView") -> bool: + """Check for OnDemandFeatureView schema/UDF changes.""" + if super()._schema_or_udf_changed(other): + return True + + if not isinstance(other, OnDemandFeatureView): + return True + + # UDF/transformation changes + # Handle None cases for feature_transformation + if ( + self.feature_transformation is None + and other.feature_transformation is not None + ): + return True + if ( + self.feature_transformation is not None + and other.feature_transformation is None + ): + return True + if ( + self.feature_transformation is not None + and other.feature_transformation is not None + and self.feature_transformation != other.feature_transformation + ): + return True + if self.mode != other.mode: + return True + if ( + self.source_feature_view_projections + != other.source_feature_view_projections + ): + return True + if self.source_request_sources != other.source_request_sources: + return True + if sorted(self.entity_columns) != sorted(other.entity_columns): + return True + if self.aggregations != other.aggregations: + return True + + # Skip configuration: write_to_online_store, singleton + return False + def __eq__(self, other): if not isinstance(other, OnDemandFeatureView): - raise TypeError( - "Comparisons should only involve OnDemandFeatureView class objects." - ) + return False # Note, no longer evaluating the base feature view layer as ODFVs can have # multiple datasources and a base_feature_view only has one source @@ -256,7 +474,11 @@ def __eq__(self, other): or self.write_to_online_store != other.write_to_online_store or sorted(self.entity_columns) != sorted(other.entity_columns) or self.singleton != other.singleton + or self.track_metrics != other.track_metrics or self.aggregations != other.aggregations + or normalize_version_string(self.version) + != normalize_version_string(other.version) + or self.org != other.org ): return False @@ -276,15 +498,86 @@ def ensure_valid(self): Validates the state of this feature view locally. Raises: - ValueError: The On Demand feature view does not have an entity when trying to use write_to_online_store. + ValueError: If the OnDemandFeatureView configuration is invalid. """ super().ensure_valid() + # Validate write_to_online_store configuration + self._validate_online_store_config() + + # Validate singleton mode configuration + self._validate_singleton_config() + + # Validate sources configuration + self._validate_sources_config() + + # Validate transformation compatibility + self._validate_transformation_config() + + def _validate_online_store_config(self) -> None: + """Validate write_to_online_store configuration.""" if self.write_to_online_store and not self.entities: + raise ValueError(ODFVErrorMessages.online_store_requires_entities()) + + def _validate_singleton_config(self) -> None: + """Validate singleton mode configuration.""" + if self.singleton and self.mode != "python": + raise ValueError( + ODFVErrorMessages.singleton_mode_requires_python(self.mode) + ) + + def _validate_sources_config(self) -> None: + """Validate sources configuration.""" + if not self.source_feature_view_projections and not self.source_request_sources: + raise ValueError(ODFVErrorMessages.no_sources_configured()) + + # Validate source names are unique + fv_names = set(self.source_feature_view_projections.keys()) + req_names = set(self.source_request_sources.keys()) + overlapping_names = fv_names.intersection(req_names) + + if overlapping_names: raise ValueError( - "On Demand Feature views require an entity if write_to_online_store=True" + ODFVErrorMessages.duplicate_source_names(overlapping_names) ) + def _validate_transformation_config(self) -> None: + """Validate transformation configuration.""" + # Aggregations provide their own transformation; no udf/feature_transformation required. + if self.aggregations: + return + + if not self.feature_transformation: + raise ValueError(ODFVErrorMessages.no_transformation_provided()) + + # Validate mode compatibility with transformation type + if self.mode in ("pandas", "python"): + from feast.transformation.pandas_transformation import PandasTransformation + from feast.transformation.python_transformation import PythonTransformation + + expected_types = (PandasTransformation, PythonTransformation) + if not isinstance(self.feature_transformation, expected_types): + raise ValueError( + ODFVErrorMessages.mode_transformation_mismatch( + self.mode, + "PandasTransformation or PythonTransformation", + type(self.feature_transformation).__name__, + ) + ) + elif self.mode == "substrait": + from feast.transformation.substrait_transformation import ( + SubstraitTransformation, + ) + + if not isinstance(self.feature_transformation, SubstraitTransformation): + raise ValueError( + ODFVErrorMessages.mode_transformation_mismatch( + self.mode, + "SubstraitTransformation", + type(self.feature_transformation).__name__, + ) + ) + def __hash__(self): return super().__hash__() @@ -300,6 +593,10 @@ def to_proto(self) -> OnDemandFeatureViewProto: meta.created_timestamp.FromDatetime(self.created_timestamp) if self.last_updated_timestamp: meta.last_updated_timestamp.FromDatetime(self.last_updated_timestamp) + if self.current_version_number is not None: + meta.current_version_number = self.current_version_number + if self.state != FeatureViewState.STATE_UNSPECIFIED: + meta.state = self.state.to_proto() sources = {} for source_name, fv_projection in self.source_feature_view_projections.items(): sources[source_name] = OnDemandSource( @@ -313,29 +610,30 @@ def to_proto(self) -> OnDemandFeatureViewProto: request_data_source=request_sources.to_proto() ) - user_defined_function_proto = cast( - UserDefinedFunctionProto, - self.feature_transformation.to_proto() - if isinstance( - self.feature_transformation, - (PandasTransformation, PythonTransformation), + # Serialize the input_schema sentinel so that from_proto() can reconstruct + # input_schema correctly; it is excluded from source_request_sources so that + # external consumers never see it as a real data source. + if self._input_schema_sentinel is not None: + sources[self._input_schema_sentinel.name] = OnDemandSource( + request_data_source=self._input_schema_sentinel.to_proto() ) - else None, - ) - substrait_transformation_proto = ( - self.feature_transformation.to_proto() - if isinstance(self.feature_transformation, SubstraitTransformation) - else None - ) + if getattr(self, "_raw_feature_transformation_proto", None) is not None: + feature_transformation = self._raw_feature_transformation_proto + else: + feature_transformation = transformation_to_proto( + self.feature_transformation + ) + + tags = dict(self.tags) if self.tags else {} + if self.track_metrics: + tags[self._TRACK_METRICS_TAG] = "true" + else: + tags.pop(self._TRACK_METRICS_TAG, None) - feature_transformation = FeatureTransformationProto( - user_defined_function=user_defined_function_proto, - substrait_transformation=substrait_transformation_proto, - ) spec = OnDemandFeatureViewSpec( name=self.name, - entities=self.entities if self.entities else None, + entities=self.entities or None, entity_columns=[ field.to_proto() for field in self.entity_columns if self.entity_columns ], @@ -344,11 +642,14 @@ def to_proto(self) -> OnDemandFeatureViewProto: feature_transformation=feature_transformation, mode=self.mode, description=self.description, - tags=self.tags, + tags=tags, owner=self.owner, + org=self.org, write_to_online_store=self.write_to_online_store, - singleton=self.singleton if self.singleton else False, - aggregations=self.aggregations, + singleton=self.singleton or False, + aggregations=[agg.to_proto() for agg in self.aggregations], + version=self.version, + disabled=not self.enabled, ) return OnDemandFeatureViewProto(spec=spec, meta=meta) @@ -368,144 +669,259 @@ def from_proto( Returns: A OnDemandFeatureView object based on the on-demand feature view protobuf. """ - sources = [] - for ( - _, - on_demand_source, - ) in on_demand_feature_view_proto.spec.sources.items(): - if on_demand_source.WhichOneof("source") == "feature_view": + # Parse sources from proto + sources = cls._parse_sources_from_proto( + on_demand_feature_view_proto, skip_udf=skip_udf + ) + + # Detect and strip input_schema sentinel from sources + input_schema: Optional[List[Field]] = None + sources_without_sentinel: List[OnDemandSourceType] = [] + for source in sources: + if isinstance(source, RequestSource) and source.name.startswith( + cls._INPUT_SCHEMA_SOURCE_PREFIX + ): + input_schema = source.schema + else: + sources_without_sentinel.append(source) + sources = sources_without_sentinel + + # Parse transformation from proto (skip UDF deserialization if requested) + transformation = cls._parse_transformation_from_proto( + on_demand_feature_view_proto, skip_udf=skip_udf + ) + + # Parse optional fields with defaults + optional_fields = cls._parse_optional_fields_from_proto( + on_demand_feature_view_proto + ) + + # Extract track_metrics from proto tags and strip the internal key + # so it doesn't leak into user-facing self.tags. + proto_tags = dict(on_demand_feature_view_proto.spec.tags) + track_metrics = ( + proto_tags.pop(cls._TRACK_METRICS_TAG, "false").lower() == "true" + ) + + # Create the OnDemandFeatureView object + on_demand_feature_view_obj = cls( + name=on_demand_feature_view_proto.spec.name, + schema=cls._parse_features_from_proto(on_demand_feature_view_proto), + sources=cast(List[OnDemandSourceType], sources), + input_schema=input_schema, + feature_transformation=transformation, + mode=on_demand_feature_view_proto.spec.mode or "pandas", + description=on_demand_feature_view_proto.spec.description, + tags=proto_tags, + owner=on_demand_feature_view_proto.spec.owner, + org=on_demand_feature_view_proto.spec.org, + write_to_online_store=optional_fields["write_to_online_store"], + singleton=optional_fields["singleton"], + track_metrics=track_metrics, + aggregations=optional_fields["aggregations"], + ) + + # Set additional attributes that aren't part of the constructor + on_demand_feature_view_obj.entities = optional_fields["entities"] + on_demand_feature_view_obj.entity_columns = optional_fields["entity_columns"] + on_demand_feature_view_obj.enabled = ( + not on_demand_feature_view_proto.spec.disabled + ) + + # Restore lifecycle state from meta. + on_demand_feature_view_obj.state = FeatureViewState.from_proto( + on_demand_feature_view_proto.meta.state + ) + + # FeatureViewProjections are not saved in the OnDemandFeatureView proto. + # Create the default projection. + on_demand_feature_view_obj.projection = FeatureViewProjection.from_definition( + on_demand_feature_view_obj + ) + + # Restore version fields. + spec_version = on_demand_feature_view_proto.spec.version + on_demand_feature_view_obj.version = spec_version or "latest" + cvn = on_demand_feature_view_proto.meta.current_version_number + if cvn > 0: + on_demand_feature_view_obj.current_version_number = cvn + elif cvn == 0 and spec_version and spec_version.lower() != "latest": + on_demand_feature_view_obj.current_version_number = 0 + else: + on_demand_feature_view_obj.current_version_number = None + + if skip_udf and on_demand_feature_view_proto.spec.HasField( + "feature_transformation" + ): + on_demand_feature_view_obj._raw_feature_transformation_proto = ( + on_demand_feature_view_proto.spec.feature_transformation + ) + + # Set timestamps if present + cls._set_timestamps_from_proto( + on_demand_feature_view_proto, on_demand_feature_view_obj + ) + + return on_demand_feature_view_obj + + @classmethod + def _parse_sources_from_proto( + cls, proto: OnDemandFeatureViewProto, skip_udf: bool = False + ) -> List[OnDemandSourceType]: + """Parse and convert sources from the protobuf representation.""" + sources: List[OnDemandSourceType] = [] + for _, on_demand_source in proto.spec.sources.items(): + source_type = on_demand_source.WhichOneof("source") + + if source_type == "feature_view": sources.append( - FeatureView.from_proto(on_demand_source.feature_view).projection + FeatureView.from_proto( + on_demand_source.feature_view, skip_udf=skip_udf + ).projection ) - elif on_demand_source.WhichOneof("source") == "feature_view_projection": + elif source_type == "feature_view_projection": sources.append( FeatureViewProjection.from_proto( on_demand_source.feature_view_projection ) ) - else: + elif source_type == "request_data_source": sources.append( RequestSource.from_proto(on_demand_source.request_data_source) ) + else: + raise ValueError( + ODFVErrorMessages.unknown_source_type_in_proto(source_type) + ) - if ( - on_demand_feature_view_proto.spec.feature_transformation.WhichOneof( - "transformation" - ) - == "user_defined_function" - and on_demand_feature_view_proto.spec.feature_transformation.user_defined_function.body_text - != "" - and on_demand_feature_view_proto.spec.mode == "pandas" - ): - transformation = PandasTransformation.from_proto( - on_demand_feature_view_proto.spec.feature_transformation.user_defined_function - ) - elif ( - on_demand_feature_view_proto.spec.feature_transformation.WhichOneof( - "transformation" - ) - == "user_defined_function" - and on_demand_feature_view_proto.spec.feature_transformation.user_defined_function.body_text - != "" - and on_demand_feature_view_proto.spec.mode == "python" - ): - transformation = PythonTransformation.from_proto( - on_demand_feature_view_proto.spec.feature_transformation.user_defined_function - ) - elif ( - on_demand_feature_view_proto.spec.feature_transformation.WhichOneof( - "transformation" - ) - == "substrait_transformation" - ): - transformation = SubstraitTransformation.from_proto( - on_demand_feature_view_proto.spec.feature_transformation.substrait_transformation - ) - elif ( - hasattr(on_demand_feature_view_proto.spec, "user_defined_function") - and on_demand_feature_view_proto.spec.feature_transformation.user_defined_function.body_text - == "" - ): - backwards_compatible_udf = UserDefinedFunctionProto( - name=on_demand_feature_view_proto.spec.user_defined_function.name, - body=on_demand_feature_view_proto.spec.user_defined_function.body, - body_text=on_demand_feature_view_proto.spec.user_defined_function.body_text, - ) - transformation = PandasTransformation.from_proto( - user_defined_function_proto=backwards_compatible_udf, + return sources + + @classmethod + def _parse_transformation_from_proto( + cls, proto: OnDemandFeatureViewProto, skip_udf: bool = False + ) -> Optional[Transformation]: + """Parse and convert the transformation from the protobuf representation.""" + if skip_udf: + return None + + feature_transformation = proto.spec.feature_transformation + transformation_type = feature_transformation.WhichOneof("transformation") + mode = proto.spec.mode + + if transformation_type == "user_defined_function": + udf_proto = feature_transformation.user_defined_function + + # Check for non-empty UDF body + if udf_proto.body_text: + if mode == "pandas": + return PandasTransformation.from_proto(udf_proto) + elif mode == "python": + return PythonTransformation.from_proto(udf_proto) + else: + raise ValueError(ODFVErrorMessages.unsupported_mode_for_udf(mode)) + else: + # Handle backward compatibility case with empty body_text + return cls._handle_backward_compatible_udf(proto) + + elif transformation_type == "substrait_transformation": + return SubstraitTransformation.from_proto( + feature_transformation.substrait_transformation ) + elif transformation_type is None: + if proto.spec.aggregations: + return None + # Handle backward compatibility case where feature_transformation is cleared + return cls._handle_backward_compatible_udf(proto) else: - raise ValueError("At least one transformation type needs to be provided") + raise ValueError( + ODFVErrorMessages.unsupported_transformation_type(transformation_type) + ) - if hasattr(on_demand_feature_view_proto.spec, "write_to_online_store"): - write_to_online_store = ( - on_demand_feature_view_proto.spec.write_to_online_store + @classmethod + def _handle_backward_compatible_udf( + cls, proto: OnDemandFeatureViewProto + ) -> Transformation: + """Handle backward compatibility for UDFs with empty body_text.""" + if not hasattr(proto.spec, "user_defined_function"): + raise ValueError(ODFVErrorMessages.backward_compatible_udf_missing()) + + old_udf = proto.spec.user_defined_function + backwards_compatible_udf = UserDefinedFunctionProto( + name=old_udf.name, + body=old_udf.body, + body_text=old_udf.body_text, + ) + return PandasTransformation.from_proto( + user_defined_function_proto=backwards_compatible_udf, + ) + + @classmethod + def _parse_features_from_proto(cls, proto: OnDemandFeatureViewProto) -> List[Field]: + """Parse features from the protobuf representation.""" + return [ + Field( + name=feature.name, + dtype=from_value_type(ValueType(feature.value_type)), + vector_index=feature.vector_index, + vector_length=feature.vector_length, + vector_search_metric=feature.vector_search_metric, ) - else: - write_to_online_store = False - if hasattr(on_demand_feature_view_proto.spec, "entities"): - entities = list(on_demand_feature_view_proto.spec.entities) - else: - entities = [] - if hasattr(on_demand_feature_view_proto.spec, "entity_columns"): + for feature in proto.spec.features + ] + + @classmethod + def _parse_optional_fields_from_proto(cls, proto: OnDemandFeatureViewProto) -> dict: + """Parse optional fields from protobuf with appropriate defaults.""" + spec = proto.spec + + # Parse write_to_online_store + write_to_online_store = False + if hasattr(spec, "write_to_online_store"): + write_to_online_store = spec.write_to_online_store + + # Parse entities + entities = [] + if hasattr(spec, "entities"): + entities = list(spec.entities) + + # Parse entity_columns + entity_columns = [] + if hasattr(spec, "entity_columns"): entity_columns = [ - Field.from_proto(field_proto) - for field_proto in on_demand_feature_view_proto.spec.entity_columns + Field.from_proto(field_proto) for field_proto in spec.entity_columns ] - else: - entity_columns = [] + + # Parse singleton singleton = False - if hasattr(on_demand_feature_view_proto.spec, "singleton"): - singleton = on_demand_feature_view_proto.spec.singleton + if hasattr(spec, "singleton"): + singleton = spec.singleton + # Parse aggregations aggregations = [] - if hasattr(on_demand_feature_view_proto.spec, "aggregations"): + if hasattr(spec, "aggregations"): aggregations = [ Aggregation.from_proto(aggregation_proto) - for aggregation_proto in on_demand_feature_view_proto.spec.aggregations + for aggregation_proto in spec.aggregations ] - on_demand_feature_view_obj = cls( - name=on_demand_feature_view_proto.spec.name, - schema=[ - Field( - name=feature.name, - dtype=from_value_type(ValueType(feature.value_type)), - vector_index=feature.vector_index, - vector_length=feature.vector_length, - vector_search_metric=feature.vector_search_metric, - ) - for feature in on_demand_feature_view_proto.spec.features - ], - sources=cast(List[OnDemandSourceType], sources), - feature_transformation=transformation, - mode=on_demand_feature_view_proto.spec.mode or "pandas", - description=on_demand_feature_view_proto.spec.description, - tags=dict(on_demand_feature_view_proto.spec.tags), - owner=on_demand_feature_view_proto.spec.owner, - write_to_online_store=write_to_online_store, - singleton=singleton, - aggregations=aggregations, - ) - - on_demand_feature_view_obj.entities = entities - on_demand_feature_view_obj.entity_columns = entity_columns - # FeatureViewProjections are not saved in the OnDemandFeatureView proto. - # Create the default projection. - on_demand_feature_view_obj.projection = FeatureViewProjection.from_definition( - on_demand_feature_view_obj - ) + return { + "write_to_online_store": write_to_online_store, + "entities": entities, + "entity_columns": entity_columns, + "singleton": singleton, + "aggregations": aggregations, + } - if on_demand_feature_view_proto.meta.HasField("created_timestamp"): - on_demand_feature_view_obj.created_timestamp = ( - on_demand_feature_view_proto.meta.created_timestamp.ToDatetime() - ) - if on_demand_feature_view_proto.meta.HasField("last_updated_timestamp"): - on_demand_feature_view_obj.last_updated_timestamp = ( - on_demand_feature_view_proto.meta.last_updated_timestamp.ToDatetime() - ) + @classmethod + def _set_timestamps_from_proto( + cls, proto: OnDemandFeatureViewProto, obj: "OnDemandFeatureView" + ) -> None: + """Set timestamp fields on the object if they exist in the proto.""" + if proto.meta.HasField("created_timestamp"): + obj.created_timestamp = proto.meta.created_timestamp.ToDatetime() - return on_demand_feature_view_obj + if proto.meta.HasField("last_updated_timestamp"): + obj.last_updated_timestamp = proto.meta.last_updated_timestamp.ToDatetime() def get_request_data_schema(self) -> dict[str, ValueType]: schema: dict[str, ValueType] = {} @@ -521,6 +937,10 @@ def get_request_data_schema(self) -> dict[str, ValueType]: raise TypeError( f"Request source schema is not correct type: ${str(type(request_source.schema))}" ) + # Include fields from the input_schema sentinel (stored privately) + if self._input_schema_sentinel is not None: + for field in self._input_schema_sentinel.schema: + schema[field.name] = field.dtype.to_value_type() return schema def _get_projected_feature_name(self, feature: str) -> str: @@ -541,10 +961,29 @@ def transform_ibis( "The feature_transformation is not SubstraitTransformation type while calling transform_ibis()." ) + # Apply common preprocessing to ensure both full and short feature names exist + ibis_table, columns_to_cleanup = self._preprocess_ibis_table(ibis_table) + + # Apply the transformation + transformed_table = self.feature_transformation.transform_ibis(ibis_table) + + # Clean up temporary columns + if columns_to_cleanup: + transformed_table = transformed_table.drop(*columns_to_cleanup) + + # Apply final column renaming based on full_feature_names preference + return self._postprocess_ibis_table(transformed_table, full_feature_names) + + def _preprocess_ibis_table(self, ibis_table): + """ + Preprocess ibis table to ensure both full and short feature names exist. + Returns the modified table and columns that need cleanup. + """ columns_to_cleanup = [] for source_fv_projection in self.source_feature_view_projections.values(): for feature in source_fv_projection.features: full_feature_ref = f"{source_fv_projection.name}__{feature.name}" + if full_feature_ref in ibis_table.columns: # Make sure the partial feature name is always present ibis_table = ibis_table.mutate( @@ -552,24 +991,29 @@ def transform_ibis( ) columns_to_cleanup.append(feature.name) elif feature.name in ibis_table.columns: + # Make sure the full feature name is always present ibis_table = ibis_table.mutate( **{full_feature_ref: ibis_table[feature.name]} ) columns_to_cleanup.append(full_feature_ref) - transformed_table = self.feature_transformation.transform_ibis(ibis_table) - - transformed_table = transformed_table.drop(*columns_to_cleanup) + return ibis_table, columns_to_cleanup + def _postprocess_ibis_table(self, transformed_table, full_feature_names: bool): + """ + Apply final column renaming to match the desired naming convention. + """ rename_columns: dict[str, str] = {} for feature in self.features: short_name = feature.name long_name = self._get_projected_feature_name(feature.name) + if short_name in transformed_table.columns and full_feature_names: rename_columns[short_name] = long_name - elif not full_feature_names: + elif long_name in transformed_table.columns and not full_feature_names: rename_columns[long_name] = short_name + # Apply renamings for rename_from, rename_to in rename_columns.items(): if rename_from in transformed_table.columns: transformed_table = transformed_table.rename(**{rename_to: rename_from}) @@ -583,10 +1027,31 @@ def transform_arrow( ) -> pyarrow.Table: if not isinstance(pa_table, pyarrow.Table): raise TypeError("transform_arrow only accepts pyarrow.Table") + + # Apply common preprocessing to ensure both full and short feature names exist + pa_table, columns_to_cleanup = self._preprocess_arrow_table(pa_table) + + # Apply the transformation + assert self.feature_transformation is not None + transformed_table = self.feature_transformation.transform_arrow( + pa_table, self.features + ) + + # Clean up temporary columns and apply final renaming + return self._postprocess_arrow_table( + transformed_table, columns_to_cleanup, full_feature_names + ) + + def _preprocess_arrow_table(self, pa_table: pyarrow.Table): + """ + Preprocess pyarrow table to ensure both full and short feature names exist. + Returns the modified table and columns that need cleanup. + """ columns_to_cleanup = [] for source_fv_projection in self.source_feature_view_projections.values(): for feature in source_fv_projection.features: full_feature_ref = f"{source_fv_projection.name}__{feature.name}" + if full_feature_ref in pa_table.column_names: # Make sure the partial feature name is always present pa_table = pa_table.append_column( @@ -600,65 +1065,109 @@ def transform_arrow( ) columns_to_cleanup.append(full_feature_ref) - df_with_transformed_features: pyarrow.Table = ( - self.feature_transformation.transform_arrow(pa_table, self.features) - ) + return pa_table, columns_to_cleanup - # Work out whether the correct columns names are used. + def _postprocess_arrow_table( + self, + transformed_table: pyarrow.Table, + columns_to_cleanup: list[str], + full_feature_names: bool, + ) -> pyarrow.Table: + """ + Clean up temporary columns and apply final column renaming. + """ + # Determine final column names rename_columns: dict[str, str] = {} for feature in self.features: short_name = feature.name long_name = self._get_projected_feature_name(feature.name) - if ( - short_name in df_with_transformed_features.column_names - and full_feature_names - ): + + if short_name in transformed_table.column_names and full_feature_names: rename_columns[short_name] = long_name - elif not full_feature_names: + elif long_name in transformed_table.column_names and not full_feature_names: rename_columns[long_name] = short_name - # Cleanup extra columns used for transformation + # Clean up temporary columns for col in columns_to_cleanup: - if col in df_with_transformed_features.column_names: - df_with_transformed_features = df_with_transformed_features.drop(col) - return df_with_transformed_features.rename_columns( - [ - rename_columns.get(c, c) - for c in df_with_transformed_features.column_names - ] - ) + if col in transformed_table.column_names: + transformed_table = transformed_table.drop(col) + + # Apply column renaming + final_column_names = [ + rename_columns.get(c, c) for c in transformed_table.column_names + ] + return transformed_table.rename_columns(final_column_names) def transform_dict( self, feature_dict: dict[str, Any], # type: ignore ) -> dict[str, Any]: - # we need a mapping from full feature name to short and back to do a renaming - # The simplest thing to do is to make the full reference, copy the columns with the short reference - # and rerun - columns_to_cleanup: list[str] = [] - for source_fv_projection in self.source_feature_view_projections.values(): - for feature in source_fv_projection.features: - full_feature_ref = f"{source_fv_projection.name}__{feature.name}" - if full_feature_ref in feature_dict.keys(): - # Make sure the partial feature name is always present - feature_dict[feature.name] = feature_dict[full_feature_ref] - columns_to_cleanup.append(str(feature.name)) - elif feature.name in feature_dict.keys(): - # Make sure the full feature name is always present - feature_dict[full_feature_ref] = feature_dict[feature.name] - columns_to_cleanup.append(str(full_feature_ref)) + """ + Transform a dictionary of features using the configured transformation. + Handles both singleton and batch transformations. + Args: + feature_dict: Dictionary containing input features + + Returns: + Dictionary with transformed features + """ + # Preprocess to ensure both full and short feature names exist + preprocessed_dict, columns_to_cleanup = self._preprocess_feature_dict( + feature_dict + ) + + # Apply the appropriate transformation based on mode + assert self.feature_transformation is not None if self.singleton and self.mode == "python": - output_dict: dict[str, Any] = ( - self.feature_transformation.transform_singleton(feature_dict) + output_dict = self.feature_transformation.transform_singleton( + preprocessed_dict ) else: - output_dict = self.feature_transformation.transform(feature_dict) + output_dict = self.feature_transformation.transform(preprocessed_dict) + + # Clean up temporary columns for feature_name in columns_to_cleanup: - del output_dict[feature_name] + if feature_name in output_dict: + del output_dict[feature_name] + return output_dict + def _preprocess_feature_dict( + self, feature_dict: dict[str, Any] + ) -> tuple[dict[str, Any], list[str]]: + """ + Preprocess feature dictionary to ensure both full and short feature names exist. + Returns the modified dictionary and columns that need cleanup. + """ + # Create a copy to avoid modifying the original + preprocessed_dict = feature_dict.copy() + columns_to_cleanup = [] + + for source_fv_projection in self.source_feature_view_projections.values(): + for feature in source_fv_projection.features: + full_feature_ref = f"{source_fv_projection.name}__{feature.name}" + + if full_feature_ref in feature_dict: + # Make sure the partial feature name is always present + preprocessed_dict[feature.name] = feature_dict[full_feature_ref] + columns_to_cleanup.append(feature.name) + elif feature.name in feature_dict: + # Make sure the full feature name is always present + preprocessed_dict[full_feature_ref] = feature_dict[feature.name] + columns_to_cleanup.append(full_feature_ref) + + return preprocessed_dict, columns_to_cleanup + def infer_features(self) -> None: + if self.aggregations and not self.feature_transformation: + if not self.features: + raise RegistryInferenceFailure( + "OnDemandFeatureView", + f"Could not infer Features for the feature view '{self.name}'.", + ) + return + assert self.feature_transformation is not None random_input = self._construct_random_input(singleton=self.singleton) inferred_features = self.feature_transformation.infer_features( random_input=random_input, singleton=self.singleton @@ -667,18 +1176,10 @@ def infer_features(self) -> None: if self.features: missing_features = [] for specified_feature in self.features: - if ( - specified_feature not in inferred_features - and "Array" not in specified_feature.dtype.__str__() + if not self._feature_exists_in_inferred( + specified_feature, inferred_features ): missing_features.append(specified_feature) - elif "Array" in specified_feature.dtype.__str__(): - if specified_feature.name not in [ - f.name for f in inferred_features - ]: - missing_features.append(specified_feature) - else: - pass if missing_features: raise SpecifiedFeaturesNotPresentError( missing_features, inferred_features, self.name @@ -692,17 +1193,117 @@ def infer_features(self) -> None: f"Could not infer Features for the feature view '{self.name}'.", ) + def _feature_exists_in_inferred( + self, specified_feature: Field, inferred_features: List[Field] + ) -> bool: + """ + Check if a specified feature exists in the inferred features list. + Handles both regular features and array types properly. + + Args: + specified_feature: The feature to check for + inferred_features: List of inferred features + + Returns: + True if the feature exists in the inferred features, False otherwise + """ + # Check for exact feature match first + if specified_feature in inferred_features: + return True + + # For array types, we need to check by name since array types + # might have different representations between specified and inferred + if self._is_array_type(specified_feature.dtype): + inferred_feature_names = {f.name for f in inferred_features} + return specified_feature.name in inferred_feature_names + + return False + + def _is_array_type(self, dtype) -> bool: + """Check if the dtype represents an array type.""" + # Use proper type checking instead of string comparison + dtype_str = str(dtype) + return "Array" in dtype_str or "List" in dtype_str or "Set" in dtype_str + def _construct_random_input( self, singleton: bool = False ) -> dict[str, Union[list[Any], Any]]: - rand_dict_value: dict[ValueType, Union[list[Any], Any]] = { - ValueType.BYTES: [str.encode("hello world")], - ValueType.PDF_BYTES: [ - b"%PDF-1.3\n3 0 obj\n<>\nendobj\n4 0 obj\n<>\nstream\nx\x9c\x15\xcc1\x0e\x820\x18@\xe1\x9dS\xbcM]jk$\xd5\xd5(\x83!\x86\xa1\x17\xf8\xa3\xa5`LIh+\xd7W\xc6\xf7\r\xef\xc0\xbd\xd2\xaa\xb6,\xd5\xc5\xb1o\x0c\xa6VZ\xe3znn%\xf3o\xab\xb1\xe7\xa3:Y\xdc\x8bm\xeb\xf3&1\xc8\xd7\xd3\x97\xc82\xe6\x81\x87\xe42\xcb\x87Vb(\x12<\xdd<=}Jc\x0cL\x91\xee\xda$\xb5\xc3\xbd\xd7\xe9\x0f\x8d\x97 $\nendstream\nendobj\n1 0 obj\n<>\nendobj\n5 0 obj\n<>\nendobj\n2 0 obj\n<<\n/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n/Font <<\n/F1 5 0 R\n>>\n/XObject <<\n>>\n>>\nendobj\n6 0 obj\n<<\n/Producer (PyFPDF 1.7.2 http://pyfpdf.googlecode.com/)\n/Title (This is a sample title.)\n/Author (Francisco Javier Arceo)\n/CreationDate (D:20250312165548)\n>>\nendobj\n7 0 obj\n<<\n/Type /Catalog\n/Pages 1 0 R\n/OpenAction [3 0 R /FitH null]\n/PageLayout /OneColumn\n>>\nendobj\nxref\n0 8\n0000000000 65535 f \n0000000272 00000 n \n0000000455 00000 n \n0000000009 00000 n \n0000000087 00000 n \n0000000359 00000 n \n0000000559 00000 n \n0000000734 00000 n \ntrailer\n<<\n/Size 8\n/Root 7 0 R\n/Info 6 0 R\n>>\nstartxref\n837\n%%EOF\n" - ], - ValueType.IMAGE_BYTES: [ - b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c\x1c $.' \",#\x1c\x1c(7),01444\x1f'9=82<.342\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x01\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00\xaa\xff\xd9" - ], + """ + Construct random input data for feature inference. + + Args: + singleton: Whether to use singleton values (single values instead of lists) + + Returns: + Dictionary with random sample data for each source feature + """ + # Get sample values for each ValueType + sample_values = self._get_sample_values_by_type() + + # Convert to singleton values if needed + if singleton: + sample_values = {k: v[0] for k, v in sample_values.items()} + + # Default value for missing types + default_value = None if not singleton else [None] + + feature_dict = {} + + # Add feature view projection features + for feature_view_projection in self.source_feature_view_projections.values(): + for feature in feature_view_projection.features: + value_type = feature.dtype.to_value_type() + sample_value = sample_values.get(value_type, default_value) + + # Add both full and short feature references + feature_dict[f"{feature_view_projection.name}__{feature.name}"] = ( + sample_value + ) + feature_dict[feature.name] = sample_value + + # Add request source features + for request_data in self.source_request_sources.values(): + for field in request_data.schema: + value_type = field.dtype.to_value_type() + sample_value = sample_values.get(value_type, default_value) + feature_dict[field.name] = sample_value + + # Add input_schema fields (stored privately outside source_request_sources) + if self._input_schema_sentinel is not None: + for field in self._input_schema_sentinel.schema: + value_type = field.dtype.to_value_type() + sample_value = sample_values.get(value_type, default_value) + feature_dict[field.name] = sample_value + + return feature_dict + + def _get_sample_values_by_type(self) -> dict[ValueType, list[Any]]: + """ + Get sample values for each supported ValueType. + Centralizes the mapping between ValueTypes and their sample values. + + Returns: + Dictionary mapping ValueType to sample values + """ + # Sample PDF bytes for testing + pdf_sample = ( + b"%PDF-1.3\n3 0 obj\n<>\nendobj\n" + b"4 0 obj\n<>\nstream\nx\x9c\x15\xcc1\x0e\x820\x18@\xe1\x9dS\xbcM]jk$\xd5\xd5(\x83!\x86\xa1\x17\xf8\xa3\xa5`LIh+\xd7W\xc6\xf7\r\xef\xc0\xbd\xd2\xaa\xb6,\xd5\xc5\xb1o\x0c\xa6VZ\xe3znn%\xf3o\xab\xb1\xe7\xa3:Y\xdc\x8bm\xeb\xf3&1\xc8\xd7\xd3\x97\xc82\xe6\x81\x87\xe42\xcb\x87Vb(\x12<\xdd<=}Jc\x0cL\x91\xee\xda$\xb5\xc3\xbd\xd7\xe9\x0f\x8d\x97 $\nendstream\nendobj\n" + b"1 0 obj\n<>\nendobj\n" + b"5 0 obj\n<>\nendobj\n" + b"2 0 obj\n<<\n/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]\n/Font <<\n/F1 5 0 R\n>>\n/XObject <<\n>>\n>>\nendobj\n" + b"6 0 obj\n<<\n/Producer (PyFPDF 1.7.2 http://pyfpdf.googlecode.com/)\n/Title (This is a sample title.)\n/Author (Francisco Javier Arceo)\n/CreationDate (D:20250312165548)\n>>\nendobj\n" + b"7 0 obj\n<<\n/Type /Catalog\n/Pages 1 0 R\n/OpenAction [3 0 R /FitH null]\n/PageLayout /OneColumn\n>>\nendobj\n" + b"xref\n0 8\n0000000000 65535 f \n0000000272 00000 n \n0000000455 00000 n \n0000000009 00000 n \n0000000087 00000 n \n0000000359 00000 n \n0000000559 00000 n \n0000000734 00000 n \n" + b"trailer\n<<\n/Size 8\n/Root 7 0 R\n/Info 6 0 R\n>>\nstartxref\n837\n%%EOF\n" + ) + + # Sample image bytes (minimal JPEG) + image_sample = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c\x1c $.' \",#\x1c\x1c(7),01444\x1f'9=82<.342\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x01\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00\xaa\xff\xd9" + + return { + # Basic types + ValueType.BYTES: [b"hello world"], ValueType.STRING: ["hello world"], ValueType.INT32: [1], ValueType.INT64: [1], @@ -710,7 +1311,14 @@ def _construct_random_input( ValueType.FLOAT: [1.0], ValueType.BOOL: [True], ValueType.UNIX_TIMESTAMP: [_utc_now()], - ValueType.BYTES_LIST: [[str.encode("hello world")]], + # Special binary types + ValueType.PDF_BYTES: [pdf_sample], + ValueType.IMAGE_BYTES: [image_sample], + # UUID types + ValueType.UUID: [uuid.uuid4()], + ValueType.TIME_UUID: [uuid.uuid1()], + # List types + ValueType.BYTES_LIST: [[b"hello world"]], ValueType.STRING_LIST: [["hello world"]], ValueType.INT32_LIST: [[1]], ValueType.INT64_LIST: [[1]], @@ -718,29 +1326,20 @@ def _construct_random_input( ValueType.FLOAT_LIST: [[1.0]], ValueType.BOOL_LIST: [[True]], ValueType.UNIX_TIMESTAMP_LIST: [[_utc_now()]], + ValueType.UUID_LIST: [[uuid.uuid4(), uuid.uuid4()]], + ValueType.TIME_UUID_LIST: [[uuid.uuid1(), uuid.uuid1()]], + # Set types + ValueType.BYTES_SET: [{b"hello world", b"foo bar"}], + ValueType.STRING_SET: [{"hello world", "foo bar"}], + ValueType.INT32_SET: [{1, 2}], + ValueType.INT64_SET: [{1, 2}], + ValueType.DOUBLE_SET: [{1.0, 2.0}], + ValueType.FLOAT_SET: [{1.0, 2.0}], + ValueType.BOOL_SET: [{True, False}], + ValueType.UNIX_TIMESTAMP_SET: [{_utc_now()}], + ValueType.UUID_SET: [{uuid.uuid4(), uuid.uuid4()}], + ValueType.TIME_UUID_SET: [{uuid.uuid1(), uuid.uuid1()}], } - if singleton: - rand_dict_value = {k: rand_dict_value[k][0] for k in rand_dict_value} - - rand_missing_value = [None] if singleton else None - feature_dict = {} - for feature_view_projection in self.source_feature_view_projections.values(): - for feature in feature_view_projection.features: - feature_dict[f"{feature_view_projection.name}__{feature.name}"] = ( - rand_dict_value.get( - feature.dtype.to_value_type(), rand_missing_value - ) - ) - feature_dict[f"{feature.name}"] = rand_dict_value.get( - feature.dtype.to_value_type(), rand_missing_value - ) - for request_data in self.source_request_sources.values(): - for field in request_data.schema: - feature_dict[f"{field.name}"] = rand_dict_value.get( - field.dtype.to_value_type(), rand_missing_value - ) - - return feature_dict @staticmethod def get_requested_odfvs( @@ -763,20 +1362,27 @@ def on_demand_feature_view( name: Optional[str] = None, entities: Optional[List[Entity]] = None, schema: list[Field], - sources: list[ - Union[ - FeatureView, - RequestSource, - FeatureViewProjection, + sources: Optional[ + list[ + Union[ + FeatureView, + RequestSource, + FeatureViewProjection, + ] ] - ], + ] = None, + input_schema: Optional[list[Field]] = None, + aggregations: Optional[List[Aggregation]] = None, mode: str = "pandas", description: str = "", tags: Optional[dict[str, str]] = None, owner: str = "", + org: str = "", write_to_online_store: bool = False, singleton: bool = False, + track_metrics: bool = False, explode: bool = False, + version: str = "latest", ): """ Creates an OnDemandFeatureView object with the given user function as udf. @@ -789,15 +1395,23 @@ def on_demand_feature_view( sources: A map from input source names to the actual input sources, which may be feature views, or request data sources. These sources serve as inputs to the udf, which will refer to them by name. + input_schema (optional): A list of Fields describing data that is accepted as input + but not stored directly as features — e.g. aggregation columns, normalization + parameters, thresholds, or other contextual values passed at request time. + When provided, sources is not required. mode: The mode of execution (e.g,. Pandas or Python Native) description (optional): A human-readable description. tags (optional): A dictionary of key-value pairs to store arbitrary metadata. owner (optional): The owner of the on demand feature view, typically the email of the primary maintainer. + org (optional): The organizational unit that owns this on demand feature view + (e.g. "ads", "search"). Defaults to empty string. write_to_online_store (optional): A boolean that indicates whether to write the on demand feature view to the online store for faster retrieval. singleton (optional): A boolean that indicates whether the transformation is executed on a singleton (only applicable when mode="python"). + track_metrics (optional): Whether to emit Prometheus timing metrics for this ODFV. + Defaults to False. Set to True to opt in when the server is started with metrics. explode (optional): A boolean that indicates whether the transformation explodes the input data into multiple rows. """ @@ -814,16 +1428,21 @@ def decorator(user_function): on_demand_feature_view_obj = OnDemandFeatureView( name=name if name is not None else user_function.__name__, sources=sources, + input_schema=input_schema, schema=schema, mode=mode, description=description, tags=tags, owner=owner, + org=org, write_to_online_store=write_to_online_store, entities=entities, singleton=singleton, + track_metrics=track_metrics, + aggregations=aggregations, udf=user_function, udf_string=udf_string, + version=version, ) functools.update_wrapper( wrapper=on_demand_feature_view_obj, wrapped=user_function diff --git a/sdk/python/feast/online_response.py b/sdk/python/feast/online_response.py index 2491a28badc..9ee1e65074d 100644 --- a/sdk/python/feast/online_response.py +++ b/sdk/python/feast/online_response.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, List, TypeAlias, Union +import uuid as uuid_module +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union import pandas as pd import pyarrow as pa @@ -21,13 +22,12 @@ from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse from feast.torch_wrapper import get_torch from feast.type_map import feast_value_type_to_python_type +from feast.value_type import ValueType if TYPE_CHECKING: - import torch - - TorchTensor: TypeAlias = torch.Tensor + from torch import Tensor as TorchTensor else: - TorchTensor: TypeAlias = Any + TorchTensor = Any TIMESTAMP_POSTFIX: str = "__ts" @@ -37,14 +37,20 @@ class OnlineResponse: Defines an online response in feast. """ - def __init__(self, online_response_proto: GetOnlineFeaturesResponse): + def __init__( + self, + online_response_proto: GetOnlineFeaturesResponse, + feature_types: Optional[Dict[str, ValueType]] = None, + ): """ Construct a native online response from its protobuf version. Args: online_response_proto: GetOnlineResponse proto object to construct from. + feature_types: Optional mapping of feature names to ValueType for type-aware deserialization. """ self.proto = online_response_proto + self._feature_types = feature_types or {} # Delete DUMMY_ENTITY_ID from proto if it exists for idx, val in enumerate(self.proto.metadata.feature_names.val): if val == DUMMY_ENTITY_ID: @@ -65,8 +71,10 @@ def to_dict(self, include_event_timestamps: bool = False) -> Dict[str, Any]: for feature_ref, feature_vector in zip( self.proto.metadata.feature_names.val, self.proto.results ): + feature_type = self._feature_types.get(feature_ref) response[feature_ref] = [ - feast_value_type_to_python_type(v) for v in feature_vector.values + feast_value_type_to_python_type(v, feature_type) + for v in feature_vector.values ] if include_event_timestamps: @@ -94,8 +102,9 @@ def to_arrow(self, include_event_timestamps: bool = False) -> pa.Table: Args: include_event_timestamps: bool Optionally include feature timestamps in the table """ - - return pa.Table.from_pydict(self.to_dict(include_event_timestamps)) + result = self.to_dict(include_event_timestamps) + result = _convert_uuids_for_arrow(result) + return pa.Table.from_pydict(result) def to_tensor( self, @@ -140,3 +149,31 @@ def to_tensor( values # Return as-is for strings or unsupported types ) return tensor_dict + + +def _convert_uuids_for_arrow(result: Dict[str, List[Any]]) -> Dict[str, List[Any]]: + """Convert uuid.UUID objects and sets to Arrow-compatible types.""" + for key, values in result.items(): + first_valid = next((v for v in values if v is not None), None) + if isinstance(first_valid, uuid_module.UUID): + result[key] = [ + str(v) if isinstance(v, uuid_module.UUID) else v for v in values + ] + elif isinstance(first_valid, list): + inner = next((e for e in first_valid if e is not None), None) + if isinstance(inner, uuid_module.UUID): + result[key] = [ + [str(e) if isinstance(e, uuid_module.UUID) else e for e in v] + if isinstance(v, list) + else v + for v in values + ] + elif isinstance(first_valid, set): + inner = next((e for e in first_valid if e is not None), None) + if isinstance(inner, uuid_module.UUID): + result[key] = [ + [str(e) for e in v] if isinstance(v, set) else v for v in values + ] + else: + result[key] = [list(v) if isinstance(v, set) else v for v in values] + return result diff --git a/sdk/python/feast/openlineage/__init__.py b/sdk/python/feast/openlineage/__init__.py new file mode 100644 index 00000000000..b3c44849788 --- /dev/null +++ b/sdk/python/feast/openlineage/__init__.py @@ -0,0 +1,86 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +OpenLineage integration for Feast Feature Store. + +This module provides **native integration** between Feast and OpenLineage for +automatic data lineage tracking. When enabled in feature_store.yaml, lineage +events are emitted automatically for: + +- Feature store registry changes (apply operations) +- Feature materialization (batch and streaming) - START, COMPLETE, FAIL events + +Lineage Graph Structure: + feast apply creates a lineage graph matching Feast UI: + + DataSources + Entities → feast_feature_views_{project} → FeatureViews + FeatureViews → feature_service_{name} → FeatureServices + + Each dataset includes: + - Schema with feature names, types, descriptions, and tags + - Feast-specific facets with metadata (TTL, entities, owner, etc.) + +Usage: + Simply configure OpenLineage in your feature_store.yaml: + + .. code-block:: yaml + + project: my_project + # ... other config ... + + openlineage: + enabled: true + transport_type: http + transport_url: http://localhost:5000 + transport_endpoint: api/v1/lineage + namespace: my_namespace # Optional: defaults to project name + + Then use Feast normally - lineage events are emitted automatically! + + .. code-block:: python + + from feast import FeatureStore + + fs = FeatureStore(repo_path="feature_repo") + fs.apply([entity, feature_view, feature_service]) # Emits lineage + fs.materialize(start, end) # Emits START/COMPLETE/FAIL events +""" + +from feast.openlineage.client import FeastOpenLineageClient +from feast.openlineage.config import OpenLineageConfig, OpenLineageConsumerConfig +from feast.openlineage.emitter import FeastOpenLineageEmitter +from feast.openlineage.facets import ( + FeastDataSourceFacet, + FeastEntityFacet, + FeastFeatureServiceFacet, + FeastFeatureViewFacet, + FeastMaterializationFacet, + FeastProjectFacet, +) + +__all__ = [ + # Main classes (used internally by native integration) + "FeastOpenLineageClient", + "FeastOpenLineageEmitter", + "OpenLineageConfig", + "OpenLineageConsumerConfig", + # Facets (custom Feast metadata in lineage events) + "FeastFeatureViewFacet", + "FeastFeatureServiceFacet", + "FeastDataSourceFacet", + "FeastEntityFacet", + "FeastMaterializationFacet", + "FeastProjectFacet", +] diff --git a/sdk/python/feast/openlineage/client.py b/sdk/python/feast/openlineage/client.py new file mode 100644 index 00000000000..d97bc482663 --- /dev/null +++ b/sdk/python/feast/openlineage/client.py @@ -0,0 +1,337 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Feast OpenLineage Client. + +This module provides a wrapper around the OpenLineage client that is +specifically designed for Feast Feature Store operations. +""" + +import logging +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +if TYPE_CHECKING: + from feast import FeatureStore + +from feast.openlineage.config import OpenLineageConfig + +try: + from openlineage.client import OpenLineageClient + from openlineage.client.event_v2 import ( + DatasetEvent, + Job, + JobEvent, + Run, + RunEvent, + RunState, + set_producer, + ) + + OPENLINEAGE_AVAILABLE = True +except ImportError: + OPENLINEAGE_AVAILABLE = False + OpenLineageClient = None # type: ignore[misc,assignment] + +logger = logging.getLogger(__name__) + + +class FeastOpenLineageClient: + """ + OpenLineage client wrapper for Feast Feature Store. + + This client provides convenient methods for emitting OpenLineage events + from Feast operations like materialization, feature retrieval, and + registry changes. + + Example:: + + from feast.openlineage import FeastOpenLineageClient, OpenLineageConfig + + config = OpenLineageConfig( + transport_type="http", + transport_url="http://localhost:5000", + ) + client = FeastOpenLineageClient(config) + + # Emit lineage for a feature store + client.emit_registry_lineage(feature_store.registry) + """ + + def __init__( + self, + config: Optional[OpenLineageConfig] = None, + feature_store: Optional["FeatureStore"] = None, + ): + """ + Initialize the Feast OpenLineage client. + + Args: + config: OpenLineage configuration. If not provided, will try to + load from environment variables. + feature_store: Optional FeatureStore instance for context. + """ + self._local_processor = None + + if not OPENLINEAGE_AVAILABLE: + logger.warning( + "OpenLineage is not installed. Lineage events will not be emitted. " + "Install with: pip install openlineage-python" + ) + self._client = None + self._config = config or OpenLineageConfig(enabled=False) + self._feature_store = feature_store + return + + self._config = config or OpenLineageConfig.from_env() + self._feature_store = feature_store + + if not self._config.enabled: + logger.info("OpenLineage integration is disabled") + self._client = None + return + + # Set producer + set_producer(self._config.producer) + + # Initialize the OpenLineage client + try: + transport_config = self._config.get_transport_config() + if transport_config is None: + self._client = OpenLineageClient() + else: + self._client = OpenLineageClient(config={"transport": transport_config}) + logger.info( + f"OpenLineage client initialized with {self._config.transport_type or 'default'} transport" + ) + except Exception as e: + logger.error(f"Failed to initialize OpenLineage client: {e}") + self._client = None + + @property + def is_enabled(self) -> bool: + """Check if the OpenLineage client is enabled and available.""" + return self._client is not None and self._config.enabled + + @property + def config(self) -> OpenLineageConfig: + """Get the OpenLineage configuration.""" + return self._config + + @property + def namespace(self) -> str: + """Get the default namespace.""" + return self._config.namespace + + def set_local_processor(self, processor) -> None: + """Attach a local OpenLineageProcessor so emitted events are also ingested locally.""" + self._local_processor = processor + + def _event_to_dict(self, event: Any) -> Optional[Dict[str, Any]]: + """Convert an OL event object to a plain dict for the local processor.""" + try: + if hasattr(event, "to_dict"): + return event.to_dict() + if hasattr(event, "__dict__"): + import json + + return json.loads(json.dumps(event, default=str)) + except Exception as e: + logger.debug(f"Could not serialize event for local processor: {e}") + return None + + def emit(self, event: Any) -> bool: + """ + Emit an OpenLineage event. + + Args: + event: OpenLineage event (RunEvent, DatasetEvent, or JobEvent) + + Returns: + True if the event was emitted successfully, False otherwise + """ + if not self.is_enabled or self._client is None: + logger.debug("OpenLineage is disabled, skipping event emission") + return False + + try: + self._client.emit(event) + + if self._local_processor is not None: + event_dict = self._event_to_dict(event) + if event_dict: + try: + self._local_processor.process_event(event_dict) + except Exception as le: + logger.debug(f"Local processor ingest failed (non-fatal): {le}") + + return True + except Exception as e: + logger.error(f"Failed to emit OpenLineage event: {e}") + return False + + def emit_run_event( + self, + job_name: str, + run_id: str, + event_type: "RunState", + inputs: Optional[List[Any]] = None, + outputs: Optional[List[Any]] = None, + job_facets: Optional[Dict[str, Any]] = None, + run_facets: Optional[Dict[str, Any]] = None, + namespace: Optional[str] = None, + ) -> bool: + """ + Emit a RunEvent for a Feast operation. + + Args: + job_name: Name of the job + run_id: Unique run identifier (UUID) + event_type: Type of event (START, COMPLETE, FAIL, etc.) + inputs: List of input datasets + outputs: List of output datasets + job_facets: Additional job facets + run_facets: Additional run facets + namespace: Optional namespace for the job (defaults to client namespace) + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled: + return False + + from datetime import datetime, timezone + + try: + event = RunEvent( + eventTime=datetime.now(timezone.utc).isoformat(), + eventType=event_type, + run=Run(runId=run_id, facets=run_facets or {}), + job=Job( + namespace=namespace or self.namespace, + name=job_name, + facets=job_facets or {}, + ), + inputs=inputs or [], + outputs=outputs or [], + ) + return self.emit(event) + except Exception as e: + logger.error(f"Failed to create RunEvent: {e}") + return False + + def emit_dataset_event( + self, + dataset_name: str, + namespace: Optional[str] = None, + facets: Optional[Dict[str, Any]] = None, + ) -> bool: + """ + Emit a DatasetEvent for a Feast dataset (data source, feature view). + + Args: + dataset_name: Name of the dataset + namespace: Optional namespace (defaults to client namespace) + facets: Dataset facets + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled: + return False + + from datetime import datetime, timezone + + from openlineage.client.event_v2 import StaticDataset + + try: + event = DatasetEvent( + eventTime=datetime.now(timezone.utc).isoformat(), + dataset=StaticDataset( + namespace=namespace or self.namespace, + name=dataset_name, + facets=facets or {}, + ), + ) + return self.emit(event) + except Exception as e: + logger.error(f"Failed to create DatasetEvent: {e}") + return False + + def emit_job_event( + self, + job_name: str, + inputs: Optional[List[Any]] = None, + outputs: Optional[List[Any]] = None, + job_facets: Optional[Dict[str, Any]] = None, + ) -> bool: + """ + Emit a JobEvent for a Feast job definition. + + Args: + job_name: Name of the job + inputs: List of input datasets + outputs: List of output datasets + job_facets: Job facets + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled: + return False + + from datetime import datetime, timezone + + try: + event = JobEvent( + eventTime=datetime.now(timezone.utc).isoformat(), + job=Job( + namespace=self.namespace, + name=job_name, + facets=job_facets or {}, + ), + inputs=inputs or [], + outputs=outputs or [], + ) + return self.emit(event) + except Exception as e: + logger.error(f"Failed to create JobEvent: {e}") + return False + + def close(self, timeout: float = 5.0) -> bool: + """ + Close the OpenLineage client and flush any pending events. + + Args: + timeout: Maximum time to wait for pending events + + Returns: + True if closed successfully, False otherwise + """ + if self._client is not None: + try: + return self._client.close(timeout) + except Exception as e: + logger.error(f"Error closing OpenLineage client: {e}") + return False + return True + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + self.close() + return False diff --git a/sdk/python/feast/openlineage/config.py b/sdk/python/feast/openlineage/config.py new file mode 100644 index 00000000000..9380df3b8cf --- /dev/null +++ b/sdk/python/feast/openlineage/config.py @@ -0,0 +1,236 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Configuration classes for Feast OpenLineage integration. +""" + +import os +from dataclasses import dataclass, field +from typing import Any, Dict, Optional + + +@dataclass +class OpenLineageConsumerConfig: + """ + Configuration for the OpenLineage consumer (event receiver). + + Attributes: + enabled: Whether the consumer is enabled + store_type: Storage backend type ('sql' uses the SQL registry DB) + connection_string: Optional separate DB connection string + api_key: API key for authenticating producers sending events + namespace_mapping: Map of OL namespace -> Feast project for RBAC scoping + """ + + enabled: bool = False + store_type: str = "sql" + connection_string: Optional[str] = None + api_key: Optional[str] = None + namespace_mapping: Dict[str, str] = field(default_factory=dict) + + @classmethod + def from_dict(cls, config_dict: Dict[str, Any]) -> "OpenLineageConsumerConfig": + return cls( + enabled=config_dict.get("enabled", False), + store_type=config_dict.get("store_type", "sql"), + connection_string=config_dict.get("connection_string"), + api_key=config_dict.get("api_key"), + namespace_mapping=config_dict.get("namespace_mapping", {}), + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "enabled": self.enabled, + "store_type": self.store_type, + "connection_string": self.connection_string, + "api_key": self.api_key, + "namespace_mapping": self.namespace_mapping, + } + + +@dataclass +class OpenLineageConfig: + """ + Configuration for OpenLineage integration. + + Attributes: + enabled: Whether OpenLineage integration is enabled + transport_type: Type of transport (http, console, file, kafka), or None to use + OpenLineage SDK defaults + transport_url: URL for HTTP transport + transport_endpoint: API endpoint for HTTP transport + api_key: Optional API key for authentication + namespace: Default namespace for Feast jobs and datasets + producer: Producer identifier for OpenLineage events + emit_on_apply: Emit lineage events when feast apply is called + emit_on_materialize: Emit lineage events during materialization + additional_config: Additional transport-specific configuration + consumer: Consumer (event receiver) configuration + """ + + enabled: bool = True + transport_type: Optional[str] = None + transport_url: Optional[str] = None + transport_endpoint: str = "api/v1/lineage" + api_key: Optional[str] = None + namespace: str = "feast" + producer: str = "feast" + emit_on_apply: bool = True + emit_on_materialize: bool = True + additional_config: Dict[str, Any] = field(default_factory=dict) + consumer: OpenLineageConsumerConfig = field( + default_factory=OpenLineageConsumerConfig + ) + + @classmethod + def from_dict(cls, config_dict: Dict[str, Any]) -> "OpenLineageConfig": + """ + Create OpenLineageConfig from a dictionary. + + Args: + config_dict: Dictionary containing configuration values + + Returns: + OpenLineageConfig instance + """ + consumer_dict = config_dict.get("consumer", {}) + consumer = ( + OpenLineageConsumerConfig.from_dict(consumer_dict) + if consumer_dict + else OpenLineageConsumerConfig() + ) + + return cls( + enabled=config_dict.get("enabled", True), + transport_type=config_dict.get("transport_type"), + transport_url=config_dict.get("transport_url"), + transport_endpoint=config_dict.get("transport_endpoint", "api/v1/lineage"), + api_key=config_dict.get("api_key"), + namespace=config_dict.get("namespace", "feast"), + producer=config_dict.get("producer", "feast"), + emit_on_apply=config_dict.get("emit_on_apply", True), + emit_on_materialize=config_dict.get("emit_on_materialize", True), + additional_config=config_dict.get("additional_config", {}), + consumer=consumer, + ) + + @classmethod + def from_env(cls) -> "OpenLineageConfig": + """ + Create OpenLineageConfig from environment variables. + + Environment variables: + FEAST_OPENLINEAGE_ENABLED: Enable/disable OpenLineage (default: true) + FEAST_OPENLINEAGE_TRANSPORT_TYPE: Transport type (default: None, uses OL SDK defaults) + FEAST_OPENLINEAGE_URL: HTTP transport URL + FEAST_OPENLINEAGE_ENDPOINT: API endpoint (default: api/v1/lineage) + FEAST_OPENLINEAGE_API_KEY: API key for authentication + FEAST_OPENLINEAGE_NAMESPACE: Default namespace (default: feast) + FEAST_OPENLINEAGE_PRODUCER: Producer identifier + + Returns: + OpenLineageConfig instance + """ + consumer = OpenLineageConsumerConfig( + enabled=os.getenv("FEAST_OPENLINEAGE_CONSUMER_ENABLED", "false").lower() + == "true", + store_type=os.getenv("FEAST_OPENLINEAGE_CONSUMER_STORE_TYPE", "sql"), + connection_string=os.getenv("FEAST_OPENLINEAGE_CONSUMER_CONNECTION_STRING"), + api_key=os.getenv("FEAST_OPENLINEAGE_CONSUMER_API_KEY"), + ) + + return cls( + enabled=os.getenv("FEAST_OPENLINEAGE_ENABLED", "true").lower() == "true", + transport_type=os.getenv("FEAST_OPENLINEAGE_TRANSPORT_TYPE"), + transport_url=os.getenv("FEAST_OPENLINEAGE_URL"), + transport_endpoint=os.getenv( + "FEAST_OPENLINEAGE_ENDPOINT", "api/v1/lineage" + ), + api_key=os.getenv("FEAST_OPENLINEAGE_API_KEY"), + namespace=os.getenv("FEAST_OPENLINEAGE_NAMESPACE", "feast"), + producer=os.getenv("FEAST_OPENLINEAGE_PRODUCER", "feast"), + emit_on_apply=os.getenv("FEAST_OPENLINEAGE_EMIT_ON_APPLY", "true").lower() + == "true", + emit_on_materialize=os.getenv( + "FEAST_OPENLINEAGE_EMIT_ON_MATERIALIZE", "true" + ).lower() + == "true", + consumer=consumer, + ) + + @property + def consumer_api_key(self) -> Optional[str]: + return self.consumer.api_key if self.consumer else None + + def to_dict(self) -> Dict[str, Any]: + """ + Convert configuration to dictionary. + + Returns: + Dictionary representation of the configuration + """ + result = { + "enabled": self.enabled, + "transport_type": self.transport_type, + "transport_url": self.transport_url, + "transport_endpoint": self.transport_endpoint, + "api_key": self.api_key, + "namespace": self.namespace, + "producer": self.producer, + "emit_on_apply": self.emit_on_apply, + "emit_on_materialize": self.emit_on_materialize, + "additional_config": self.additional_config, + } + if self.consumer: + result["consumer"] = self.consumer.to_dict() + return result + + def get_transport_config(self) -> Optional[Dict[str, Any]]: + """ + Get transport-specific configuration for OpenLineage client. + + Returns: + Dictionary with transport configuration, or None if transport_type + is not set (allowing the OpenLineage SDK to use its own defaults). + """ + if not self.transport_type: + return None + + config: Dict[str, Any] = {"type": self.transport_type} + + if self.transport_type == "http": + if not self.transport_url: + raise ValueError("transport_url is required for HTTP transport") + config["url"] = self.transport_url + config["endpoint"] = self.transport_endpoint + if self.api_key: + config["auth"] = { + "type": "api_key", + "apiKey": self.api_key, + } + elif self.transport_type == "file": + config["log_file_path"] = self.additional_config.get( + "log_file_path", "openlineage_events.json" + ) + elif self.transport_type == "kafka": + config["bootstrap_servers"] = self.additional_config.get( + "bootstrap_servers" + ) + config["topic"] = self.additional_config.get("topic", "openlineage.events") + + # Merge additional config + config.update(self.additional_config) + + return config diff --git a/sdk/python/feast/openlineage/consumer.py b/sdk/python/feast/openlineage/consumer.py new file mode 100644 index 00000000000..995de4d131f --- /dev/null +++ b/sdk/python/feast/openlineage/consumer.py @@ -0,0 +1,345 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +OpenLineage consumer for Feast. + +Provides FastAPI router endpoints for receiving OpenLineage events +(POST /api/v1/lineage) and querying lineage data, making Feast +act as an OpenLineage consumer alongside its existing producer role. +""" + +import json +import logging +from typing import Any, List, Optional + +from fastapi import APIRouter, Header, HTTPException, Query, Request +from fastapi.responses import JSONResponse + +from feast.openlineage.config import OpenLineageConfig +from feast.openlineage.processor import OpenLineageProcessor +from feast.openlineage.store import OpenLineageStore + +logger = logging.getLogger(__name__) + + +def _safe_parse_json(val: Optional[str]) -> Optional[Any]: + if not val: + return None + try: + return json.loads(val) + except (json.JSONDecodeError, TypeError): + return None + + +def _verify_api_key( + expected_key: Optional[str], + x_api_key: Optional[str] = Header(None, alias="X-API-Key"), + authorization: Optional[str] = Header(None), +) -> bool: + if not expected_key: + return True + + if x_api_key and x_api_key == expected_key: + return True + + if authorization: + parts = authorization.split(" ", 1) + if ( + len(parts) == 2 + and parts[0].lower() == "bearer" + and parts[1] == expected_key + ): + return True + + return False + + +def get_consumer_router( + config: OpenLineageConfig, + store: OpenLineageStore, + processor: OpenLineageProcessor, + get_allowed_namespaces=None, +) -> APIRouter: + """ + Create FastAPI router for the OpenLineage consumer endpoints. + + Args: + config: OpenLineage configuration with consumer settings + store: The lineage store instance + processor: The event processor instance + get_allowed_namespaces: Optional callable that returns allowed namespaces + for the current user (for RBAC filtering). If None, all namespaces visible. + """ + router = APIRouter() + + # ── Producer-facing: receive events ── + + @router.post("/v1/lineage") + async def receive_lineage_event( + request: Request, + x_api_key: Optional[str] = Header(None, alias="X-API-Key"), + authorization: Optional[str] = Header(None), + ): + """ + Receive an OpenLineage event. + + Compatible with the standard OpenLineage API endpoint. + Accepts RunEvent, DatasetEvent, or JobEvent. + """ + api_key = getattr(config, "consumer_api_key", None) + if not _verify_api_key(api_key, x_api_key, authorization): + raise HTTPException(status_code=401, detail="Invalid API key") + + try: + body = await request.json() + except Exception: + raise HTTPException(status_code=400, detail="Invalid JSON body") + + if isinstance(body, list): + results = processor.process_batch(body) + return JSONResponse( + status_code=200 if results["failed"] == 0 else 207, + content={ + "status": "success" + if results["failed"] == 0 + else "partial_success", + "summary": { + "received": results["received"], + "successful": results["successful"], + "failed": results["failed"], + }, + }, + ) + else: + try: + event_id = processor.process_event(body) + return JSONResponse(status_code=201, content={"event_id": event_id}) + except Exception as e: + logger.error(f"Failed to process event: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @router.post("/v1/lineage/batch") + async def receive_lineage_batch( + request: Request, + x_api_key: Optional[str] = Header(None, alias="X-API-Key"), + authorization: Optional[str] = Header(None), + ): + """Receive a batch of OpenLineage events.""" + api_key = getattr(config, "consumer_api_key", None) + if not _verify_api_key(api_key, x_api_key, authorization): + raise HTTPException(status_code=401, detail="Invalid API key") + + try: + body = await request.json() + except Exception: + raise HTTPException(status_code=400, detail="Invalid JSON body") + + if not isinstance(body, list): + raise HTTPException(status_code=400, detail="Expected array of events") + + results = processor.process_batch(body) + status = 204 if results["failed"] == 0 else 200 + if status == 204: + return JSONResponse(status_code=204, content=None) + + return JSONResponse( + status_code=200, + content={ + "status": "partial_success", + "summary": { + "received": results["received"], + "successful": results["successful"], + "failed": results["failed"], + }, + }, + ) + + # ── Admin endpoints ── + + @router.delete("/lineage/openlineage/reset") + async def reset_lineage( + namespace: Optional[str] = Query(None), + x_api_key: Optional[str] = Header(None, alias="X-API-Key"), + authorization: Optional[str] = Header(None), + ): + """ + Purge OpenLineage data. Requires API key. + + If ?namespace=X is provided, only that namespace's data is deleted. + Otherwise, all OpenLineage data is purged. + """ + api_key = getattr(config, "consumer_api_key", None) + if not _verify_api_key(api_key, x_api_key, authorization): + raise HTTPException(status_code=401, detail="Invalid API key") + + if namespace: + store.purge_namespace(namespace) + return {"status": "success", "message": f"Purged namespace: {namespace}"} + else: + store.purge_all() + return {"status": "success", "message": "Purged all OpenLineage data"} + + # ── Query endpoints ── + + @router.get("/lineage/openlineage/events") + def list_events( + namespace: Optional[str] = Query(None), + job_name: Optional[str] = Query(None), + limit: int = Query(100, ge=1, le=1000), + offset: int = Query(0, ge=0), + ): + """List stored OpenLineage events with optional filtering.""" + ns_filter = _get_namespace_filter(get_allowed_namespaces) + events = store.get_events( + namespace=namespace, + job_name=job_name, + limit=limit, + offset=offset, + namespaces=ns_filter if not namespace else None, + ) + return {"events": events, "total": len(events)} + + @router.get("/lineage/openlineage/jobs") + def list_jobs(): + """List all jobs from all OpenLineage producers.""" + ns_filter = _get_namespace_filter(get_allowed_namespaces) + jobs = store.get_jobs(namespaces=ns_filter) + return {"jobs": jobs} + + @router.get("/lineage/openlineage/datasets") + def list_datasets(): + """List all datasets from OpenLineage events.""" + ns_filter = _get_namespace_filter(get_allowed_namespaces) + datasets = store.get_datasets(namespaces=ns_filter) + return {"datasets": datasets} + + @router.get("/lineage/openlineage/graph/{node_type}/{namespace}/{name}") + def get_lineage_graph( + node_type: str, + namespace: str, + name: str, + depth: int = Query(10, ge=1, le=50), + direction: str = Query("both", pattern="^(both|upstream|downstream)$"), + ): + """ + Get the lineage graph for a specific node. + + Traverses upstream and/or downstream from the given node + up to the specified depth. + """ + valid_types = ["job", "dataset"] + if node_type not in valid_types: + raise HTTPException( + status_code=400, + detail=f"node_type must be one of: {valid_types}", + ) + + ns_filter = _get_namespace_filter(get_allowed_namespaces) + graph = store.get_lineage_graph( + node_type=node_type, + namespace=namespace, + name=name, + depth=depth, + direction=direction, + allowed_namespaces=ns_filter, + ) + return graph + + @router.get("/lineage/openlineage/graph") + def get_full_lineage_graph(): + """ + Get all lineage edges (RBAC-filtered by namespace). + + Includes symlink edges that connect datasets across producers + when they reference the same physical data (via SymlinksDatasetFacet + or matching dataSource URIs). + """ + ns_filter = _get_namespace_filter(get_allowed_namespaces) + edges = store.get_all_lineage_edges(namespaces=ns_filter) + datasets = store.get_datasets(namespaces=ns_filter) + jobs = store.get_jobs(namespaces=ns_filter) + + nodes = [] + for ds in datasets: + facets = _safe_parse_json(ds.get("facets_json")) + schema = _safe_parse_json(ds.get("schema_json")) + nodes.append( + { + "type": "dataset", + "namespace": ds["dataset_namespace"], + "name": ds["dataset_name"], + "producer": ds.get("producer"), + "feast_object_type": ds.get("feast_object_type"), + "feast_object_name": ds.get("feast_object_name"), + "feast_project": ds.get("feast_project"), + "schema": schema, + "description": ds.get("description"), + "source_type": ds.get("source_type"), + "facets": facets, + } + ) + for job in jobs: + facets = _safe_parse_json(job.get("facets_json")) + nodes.append( + { + "type": "job", + "namespace": job["job_namespace"], + "name": job["job_name"], + "producer": job.get("producer"), + "job_type": job.get("job_type"), + "description": job.get("description"), + "facets": facets, + } + ) + + symlinks = store.get_all_symlinks() + + return {"nodes": nodes, "edges": edges, "symlinks": symlinks} + + # ── Run history endpoints ── + + @router.get("/lineage/openlineage/runs") + def list_runs( + job_namespace: Optional[str] = Query(None), + job_name: Optional[str] = Query(None), + limit: int = Query(50, ge=1, le=500), + offset: int = Query(0, ge=0), + ): + """List runs, optionally filtered by job namespace and name.""" + runs = store.get_runs( + job_namespace=job_namespace, + job_name=job_name, + limit=limit, + offset=offset, + ) + return {"runs": runs, "total": len(runs)} + + @router.get("/lineage/openlineage/runs/{run_id}") + def get_run_detail(run_id: str): + """Get a single run with its input and output datasets.""" + run = store.get_run_detail(run_id) + if not run: + raise HTTPException(status_code=404, detail=f"Run {run_id} not found") + return run + + def _get_namespace_filter(ns_callable) -> Optional[List[str]]: + if ns_callable: + try: + return ns_callable() + except Exception: + return None + return None + + return router diff --git a/sdk/python/feast/openlineage/emitter.py b/sdk/python/feast/openlineage/emitter.py new file mode 100644 index 00000000000..b20168186c8 --- /dev/null +++ b/sdk/python/feast/openlineage/emitter.py @@ -0,0 +1,1035 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Feast OpenLineage Emitter. + +This module provides high-level functions for emitting OpenLineage events +from Feast operations like materialization, feature retrieval, and registry changes. +""" + +import logging +import uuid +from datetime import datetime +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union + +if TYPE_CHECKING: + from feast import FeatureService, FeatureView + from feast.infra.registry.base_registry import BaseRegistry + from feast.labeling.label_view import LabelView + from feast.on_demand_feature_view import OnDemandFeatureView + from feast.stream_feature_view import StreamFeatureView + +from feast.openlineage.client import FeastOpenLineageClient +from feast.openlineage.config import OpenLineageConfig + +try: + from openlineage.client.event_v2 import ( + InputDataset, + OutputDataset, + RunState, + ) + + OPENLINEAGE_AVAILABLE = True +except ImportError: + OPENLINEAGE_AVAILABLE = False + +logger = logging.getLogger(__name__) + + +class FeastOpenLineageEmitter: + """ + High-level emitter for Feast OpenLineage events. + + This class provides methods for emitting lineage events for various + Feast operations including: + - Registry apply (feature view, feature service definitions) + - Materialization (batch and incremental) + - Feature retrieval (online and historical) + - Data source relationships + + Example: + from feast import FeatureStore + from feast.openlineage import FeastOpenLineageEmitter, OpenLineageConfig + + config = OpenLineageConfig(transport_type="http", transport_url="http://localhost:5000") + emitter = FeastOpenLineageEmitter(config) + + fs = FeatureStore(repo_path="feature_repo") + + # Emit lineage for registry + emitter.emit_registry_lineage(fs.registry, fs.project) + """ + + def __init__( + self, + config: Optional[OpenLineageConfig] = None, + client: Optional[FeastOpenLineageClient] = None, + ): + """ + Initialize the Feast OpenLineage Emitter. + + Args: + config: OpenLineage configuration + client: Optional pre-configured FeastOpenLineageClient + """ + self._config = config or OpenLineageConfig.from_env() + self._client = client or FeastOpenLineageClient(self._config) + + @property + def is_enabled(self) -> bool: + """Check if the emitter is enabled.""" + return self._client.is_enabled + + @property + def namespace(self) -> str: + """Get the default namespace.""" + return self._config.namespace + + def _get_namespace(self, project: str) -> str: + """ + Get the OpenLineage namespace for a project. + + By default, uses the Feast project name as the namespace. + If an explicit namespace is configured (not the default "feast"), + it will be used as a prefix: {namespace}/{project} + + Args: + project: Feast project name + + Returns: + OpenLineage namespace string + """ + # If namespace is default "feast", just use project name + if self._config.namespace == "feast": + return project + # If custom namespace is configured, use it as prefix + return f"{self._config.namespace}/{project}" + + def emit_registry_lineage( + self, + registry: "BaseRegistry", + project: str, + allow_cache: bool = True, + ) -> List[bool]: + """ + Emit lineage events for all objects in a Feast registry. + + This method emits JobEvents for feature views and feature services, + and DatasetEvents for data sources and entities. + + Args: + registry: Feast registry + project: Project name + allow_cache: Whether to use cached registry data + + Returns: + List of success/failure indicators for each event + """ + if not self.is_enabled: + return [] + + from feast.feature_view import FeatureView + from feast.labeling.label_view import LabelView + from feast.on_demand_feature_view import OnDemandFeatureView + from feast.stream_feature_view import StreamFeatureView + + results = [] + + # Get all feature views at once (includes FeatureView, StreamFeatureView, OnDemandFeatureView, LabelView) + all_feature_views: list = [] + try: + all_feature_views = registry.list_all_feature_views( + project=project, allow_cache=allow_cache + ) + except Exception as e: + logger.error(f"Error listing all feature views: {e}") + + # Emit lineage events for each feature view type + for fv in all_feature_views: + try: + if isinstance(fv, OnDemandFeatureView): + result = self.emit_on_demand_feature_view_lineage(fv, project) + elif isinstance(fv, StreamFeatureView): + result = self.emit_stream_feature_view_lineage(fv, project) + elif isinstance(fv, LabelView): + result = self.emit_label_view_lineage(fv, project) + elif isinstance(fv, FeatureView): + result = self.emit_feature_view_lineage(fv, project) + else: + continue + results.append(result) + except Exception as e: + logger.error(f"Error emitting lineage for feature view {fv.name}: {e}") + + # Emit events for feature services + try: + feature_services = registry.list_feature_services( + project=project, allow_cache=allow_cache + ) + for fs in feature_services: + result = self.emit_feature_service_lineage( + fs, all_feature_views, project + ) + results.append(result) + except Exception as e: + logger.error(f"Error emitting feature service lineage: {e}") + + logger.info( + f"Emitted {sum(results)}/{len(results)} lineage events for registry" + ) + return results + + def emit_feature_view_lineage( + self, + feature_view: "FeatureView", + project: str, + ) -> bool: + """ + Emit lineage for a feature view definition. + + Args: + feature_view: The feature view + project: Project name + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled: + return False + + from feast.openlineage.mappers import feature_view_to_job + + try: + namespace = self._get_namespace(project) + job, inputs, outputs = feature_view_to_job( + feature_view, + namespace=namespace, + ) + + # Emit a RunEvent with COMPLETE state to create lineage connection + result = self._client.emit_run_event( + job_name=job.name, + run_id=str(uuid.uuid4()), + event_type=RunState.COMPLETE, + inputs=inputs, + outputs=outputs, + job_facets=job.facets, + namespace=namespace, + ) + return result + except Exception as e: + logger.error( + f"Error emitting feature view lineage for {feature_view.name}: {e}" + ) + return False + + def emit_label_view_lineage( + self, + label_view: "LabelView", + project: str, + ) -> bool: + """ + Emit lineage for a label view definition. + + Args: + label_view: The label view + project: Project name + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled: + return False + + from feast.openlineage.mappers import feature_view_to_job + + try: + namespace = self._get_namespace(project) + job, inputs, outputs = feature_view_to_job( + label_view, + namespace=namespace, + job_type="LABEL_VIEW", + ) + + result = self._client.emit_run_event( + job_name=job.name, + run_id=str(uuid.uuid4()), + event_type=RunState.COMPLETE, + inputs=inputs, + outputs=outputs, + job_facets=job.facets, + namespace=namespace, + ) + return result + except Exception as e: + logger.error( + f"Error emitting label view lineage for {label_view.name}: {e}" + ) + return False + + def emit_stream_feature_view_lineage( + self, + stream_feature_view: "StreamFeatureView", + project: str, + ) -> bool: + """ + Emit lineage for a stream feature view definition. + + Args: + stream_feature_view: The stream feature view + project: Project name + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled: + return False + + from feast.openlineage.mappers import feature_view_to_job + + try: + namespace = self._get_namespace(project) + # StreamFeatureView inherits from FeatureView + job, inputs, outputs = feature_view_to_job( + stream_feature_view, + namespace=namespace, + ) + + # Emit a RunEvent with COMPLETE state to create lineage connection + return self._client.emit_run_event( + job_name=f"stream_{job.name}", + run_id=str(uuid.uuid4()), + event_type=RunState.COMPLETE, + inputs=inputs, + outputs=outputs, + job_facets=job.facets, + namespace=namespace, + ) + except Exception as e: + logger.error( + f"Error emitting stream feature view lineage for {stream_feature_view.name}: {e}" + ) + return False + + def emit_on_demand_feature_view_lineage( + self, + odfv: "OnDemandFeatureView", + project: str, + ) -> bool: + """ + Emit lineage for an on-demand feature view definition. + + Args: + odfv: The on-demand feature view + project: Project name + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled: + return False + + from feast.openlineage.facets import FeastFeatureViewFacet + from feast.openlineage.mappers import feast_field_to_schema_field + + try: + from openlineage.client.facet_v2 import schema_dataset + + namespace = self._get_namespace(project) + + # Build inputs from sources + inputs = [] + for source_name, fv_proj in odfv.source_feature_view_projections.items(): + inputs.append( + InputDataset( + namespace=namespace, + name=fv_proj.name, + ) + ) + + for source_name, req_source in odfv.source_request_sources.items(): + inputs.append( + InputDataset( + namespace=namespace, + name=f"request_source_{source_name}", + ) + ) + + # Build output + output_facets = {} + if odfv.features: + output_facets["schema"] = schema_dataset.SchemaDatasetFacet( + fields=[feast_field_to_schema_field(f) for f in odfv.features] + ) + + outputs = [ + OutputDataset( + namespace=namespace, + name=odfv.name, + facets=output_facets, # type: ignore[arg-type] + ) + ] + + # Build job facets + job_facets = { + "feast_featureView": FeastFeatureViewFacet( + name=odfv.name, + ttl_seconds=0, + entities=[], + features=[f.name for f in odfv.features] if odfv.features else [], + online_enabled=True, + offline_enabled=True, + mode="ON_DEMAND", + description=odfv.description if odfv.description else "", + owner=odfv.owner if hasattr(odfv, "owner") and odfv.owner else "", + tags=odfv.tags if odfv.tags else {}, + ) + } + + # Emit a RunEvent with COMPLETE state to create lineage connection + return self._client.emit_run_event( + job_name=f"on_demand_feature_view_{odfv.name}", + run_id=str(uuid.uuid4()), + event_type=RunState.COMPLETE, + inputs=inputs, + outputs=outputs, + job_facets=job_facets, + namespace=namespace, + ) + except Exception as e: + logger.error( + f"Error emitting on-demand feature view lineage for {odfv.name}: {e}" + ) + return False + + def emit_feature_service_lineage( + self, + feature_service: "FeatureService", + feature_views: List[ + Union["FeatureView", "OnDemandFeatureView", "StreamFeatureView"] + ], + project: str, + ) -> bool: + """ + Emit lineage for a feature service definition. + + Args: + feature_service: The feature service + feature_views: List of all available feature views (FeatureView, + OnDemandFeatureView, StreamFeatureView) + project: Project name + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled: + return False + + from feast.openlineage.mappers import feature_service_to_job + + try: + # Find the feature views referenced by this service + namespace = self._get_namespace(project) + fv_names = {proj.name for proj in feature_service.feature_view_projections} + referenced_fvs = [fv for fv in feature_views if fv.name in fv_names] + + job, inputs, outputs = feature_service_to_job( + feature_service, + referenced_fvs, + namespace=namespace, + ) + + # Emit a RunEvent with COMPLETE state to create lineage connection + return self._client.emit_run_event( + job_name=job.name, + run_id=str(uuid.uuid4()), + event_type=RunState.COMPLETE, + inputs=inputs, + outputs=outputs, + job_facets=job.facets, + namespace=namespace, + ) + except Exception as e: + logger.error( + f"Error emitting feature service lineage for {feature_service.name}: {e}" + ) + return False + + def emit_materialize_start( + self, + feature_views: List["FeatureView"], + start_date: Optional[datetime], + end_date: datetime, + project: str, + run_id: Optional[str] = None, + ) -> Tuple[str, bool]: + """ + Emit a START event for a materialization run. + + Args: + feature_views: Feature views being materialized + start_date: Start of materialization window (None for incremental) + end_date: End of materialization window + project: Project name + run_id: Optional run ID (will be generated if not provided) + + Returns: + Tuple of (run_id, success) + """ + if not self.is_enabled or not self._config.emit_on_materialize: + return "", False + + from feast.openlineage.facets import FeastMaterializationFacet + from feast.openlineage.mappers import ( + data_source_to_dataset, + online_store_to_dataset, + ) + + run_id = run_id or str(uuid.uuid4()) + + try: + namespace = self._get_namespace(project) + + # Build inputs (data sources) - include both batch and stream sources + inputs = [] + seen_sources = set() # Track source names to avoid duplicates + + for fv in feature_views: + # Add batch source + if hasattr(fv, "batch_source") and fv.batch_source: + source_name = getattr(fv.batch_source, "name", None) + if source_name and source_name not in seen_sources: + seen_sources.add(source_name) + inputs.append( + data_source_to_dataset( + fv.batch_source, + namespace=namespace, + as_input=True, + ) + ) + + # Add stream source (e.g., PushSource) + if hasattr(fv, "stream_source") and fv.stream_source: + source_name = getattr(fv.stream_source, "name", None) + if source_name and source_name not in seen_sources: + seen_sources.add(source_name) + inputs.append( + data_source_to_dataset( + fv.stream_source, + namespace=namespace, + as_input=True, + ) + ) + + # Add entities as inputs (use direct name for consistency with emit_apply) + if hasattr(fv, "entities") and fv.entities: + for entity_name in fv.entities: + if entity_name and entity_name != "__dummy": + if entity_name not in seen_sources: + seen_sources.add(entity_name) + inputs.append( + InputDataset( + namespace=namespace, + name=entity_name, + ) + ) + + # Build outputs (online store entries) + outputs = [ + online_store_to_dataset( + store_type="online_store", + feature_view_name=fv.name, + namespace=namespace, + ) + for fv in feature_views + ] + + # Build run facets + run_facets = { + "feast_materialization": FeastMaterializationFacet( + feature_views=[fv.name for fv in feature_views], + start_date=start_date.isoformat() if start_date else None, + end_date=end_date.isoformat() if end_date else None, + project=project, + ) + } + + success = self._client.emit_run_event( + job_name=f"materialize_{project}", + run_id=run_id, + event_type=RunState.START, + inputs=inputs, + outputs=outputs, + run_facets=run_facets, + namespace=namespace, + ) + + return run_id, success + except Exception as e: + logger.error(f"Error emitting materialize start event: {e}") + return run_id, False + + def emit_materialize_complete( + self, + run_id: str, + feature_views: List["FeatureView"], + project: str, + rows_written: Optional[int] = None, + ) -> bool: + """ + Emit a COMPLETE event for a materialization run. + + Args: + run_id: Run ID from the start event + feature_views: Feature views that were materialized + project: Project name + rows_written: Optional count of rows written + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled or not self._config.emit_on_materialize: + return False + + from feast.openlineage.facets import FeastMaterializationFacet + from feast.openlineage.mappers import online_store_to_dataset + + try: + namespace = self._get_namespace(project) + + outputs = [ + online_store_to_dataset( + store_type="online_store", + feature_view_name=fv.name, + namespace=namespace, + ) + for fv in feature_views + ] + + run_facets = { + "feast_materialization": FeastMaterializationFacet( + feature_views=[fv.name for fv in feature_views], + project=project, + rows_written=rows_written, + ) + } + + return self._client.emit_run_event( + job_name=f"materialize_{project}", + run_id=run_id, + event_type=RunState.COMPLETE, + outputs=outputs, + run_facets=run_facets, + namespace=namespace, + ) + except Exception as e: + logger.error(f"Error emitting materialize complete event: {e}") + return False + + def emit_materialize_fail( + self, + run_id: str, + project: str, + error_message: Optional[str] = None, + ) -> bool: + """ + Emit a FAIL event for a materialization run. + + Args: + run_id: Run ID from the start event + project: Project name + error_message: Optional error message + + Returns: + True if successful, False otherwise + """ + if not self.is_enabled or not self._config.emit_on_materialize: + return False + + try: + from openlineage.client.facet_v2 import error_message_run + + namespace = self._get_namespace(project) + run_facets = {} + if error_message: + run_facets["errorMessage"] = error_message_run.ErrorMessageRunFacet( + message=error_message, + programmingLanguage="python", + ) + + return self._client.emit_run_event( + job_name=f"materialize_{project}", + run_id=run_id, + event_type=RunState.FAIL, + run_facets=run_facets, + namespace=namespace, + ) + except Exception as e: + logger.error(f"Error emitting materialize fail event: {e}") + return False + + def emit_apply( + self, + objects: List[Any], + project: str, + ) -> List[bool]: + """ + Emit lineage for a feast apply operation. + + Creates two jobs to match Feast UI lineage model: + 1. feast_feature_views_{project}: DataSources + Entities → FeatureViews + 2. feast_feature_services_{project}: FeatureViews → FeatureServices + + This creates a lineage graph matching Feast UI:: + + DataSource ──→ FeatureView ──→ FeatureService + ↑ + Entity + + + Args: + objects: List of Feast objects being applied + project: Project name + + Returns: + List of success/failure indicators + """ + if not self.is_enabled or not self._config.emit_on_apply: + return [] + + from feast import Entity, FeatureService + from feast.data_source import DataSource + from feast.feature_view import FeatureView + from feast.on_demand_feature_view import OnDemandFeatureView + from feast.openlineage.facets import FeastProjectFacet + from feast.openlineage.mappers import ( + data_source_to_dataset, + entity_to_dataset, + feast_field_to_schema_field, + ) + from feast.stream_feature_view import StreamFeatureView + + try: + from openlineage.client.facet_v2 import schema_dataset + + namespace = self._get_namespace(project) + results = [] + + # Categorize objects + data_sources: List[DataSource] = [] + entities: List[Entity] = [] + feature_views: List[Union[FeatureView, OnDemandFeatureView]] = [] + on_demand_feature_views: List[OnDemandFeatureView] = [] + feature_services: List[FeatureService] = [] + + for obj in objects: + if isinstance(obj, StreamFeatureView): + feature_views.append(obj) + elif isinstance(obj, OnDemandFeatureView): + on_demand_feature_views.append(obj) + elif isinstance(obj, FeatureView): + feature_views.append(obj) + elif isinstance(obj, FeatureService): + feature_services.append(obj) + elif isinstance(obj, DataSource): + data_sources.append(obj) + elif isinstance(obj, Entity): + if obj.name != "__dummy": + entities.append(obj) + + # ============================================================ + # Job 1: DataSources + Entities → FeatureViews + # This matches: DataSource → FeatureView and Entity → FeatureView + # ============================================================ + if feature_views or on_demand_feature_views: + fv_inputs = [] + seen_inputs: set = set() + + # Add explicit data sources + for ds in data_sources: + if ds.name and ds.name not in seen_inputs: + seen_inputs.add(ds.name) + fv_inputs.append( + data_source_to_dataset( + ds, namespace=namespace, as_input=True + ) + ) + + # Add entities (using direct name to match Feast UI) + for entity in entities: + if entity.name not in seen_inputs: + seen_inputs.add(entity.name) + fv_inputs.append(entity_to_dataset(entity, namespace=namespace)) + + # Also add data sources from feature views + for fv in feature_views: + if hasattr(fv, "batch_source") and fv.batch_source: + source_name = getattr(fv.batch_source, "name", None) + if source_name and source_name not in seen_inputs: + seen_inputs.add(source_name) + fv_inputs.append( + data_source_to_dataset( + fv.batch_source, namespace=namespace, as_input=True + ) + ) + if hasattr(fv, "stream_source") and fv.stream_source: + source_name = getattr(fv.stream_source, "name", None) + if source_name and source_name not in seen_inputs: + seen_inputs.add(source_name) + fv_inputs.append( + data_source_to_dataset( + fv.stream_source, namespace=namespace, as_input=True + ) + ) + + # Build FeatureView outputs + from openlineage.client.facet_v2 import documentation_dataset + + from feast.openlineage.facets import FeastFeatureViewFacet + + fv_outputs = [] + for fv in feature_views: + output_facets: Dict[str, Any] = {} + + # Add schema with features (includes tags in description) + if fv.features: + output_facets["schema"] = schema_dataset.SchemaDatasetFacet( + fields=[feast_field_to_schema_field(f) for f in fv.features] + ) + + # Add documentation facet with description + if hasattr(fv, "description") and fv.description: + output_facets["documentation"] = ( + documentation_dataset.DocumentationDatasetFacet( + description=fv.description + ) + ) + + # Add Feast-specific facet with full metadata + ttl_seconds = 0 + if hasattr(fv, "ttl") and fv.ttl: + ttl_seconds = int(fv.ttl.total_seconds()) + + output_facets["feast_featureView"] = FeastFeatureViewFacet( + name=fv.name, + ttl_seconds=ttl_seconds, + entities=list(fv.entities) + if hasattr(fv, "entities") and fv.entities + else [], + features=[f.name for f in fv.features] if fv.features else [], + online_enabled=fv.online if hasattr(fv, "online") else True, + description=fv.description + if hasattr(fv, "description") + else "", + owner=fv.owner if hasattr(fv, "owner") else "", + tags=fv.tags if hasattr(fv, "tags") else {}, + ) + + fv_outputs.append( + OutputDataset( + namespace=namespace, + name=fv.name, + facets=output_facets, + ) + ) + + for odfv in on_demand_feature_views: + output_facets = {} + + # Add schema with features (includes tags in description) + if odfv.features: + output_facets["schema"] = schema_dataset.SchemaDatasetFacet( + fields=[ + feast_field_to_schema_field(f) for f in odfv.features + ] + ) + + # Add documentation facet with description + if hasattr(odfv, "description") and odfv.description: + output_facets["documentation"] = ( + documentation_dataset.DocumentationDatasetFacet( + description=odfv.description + ) + ) + + # Add Feast-specific facet with full metadata + output_facets["feast_featureView"] = FeastFeatureViewFacet( + name=odfv.name, + ttl_seconds=0, + entities=list(odfv.entities) + if hasattr(odfv, "entities") and odfv.entities + else [], + features=[f.name for f in odfv.features] + if odfv.features + else [], + online_enabled=True, + description=odfv.description + if hasattr(odfv, "description") + else "", + owner=odfv.owner if hasattr(odfv, "owner") else "", + tags=odfv.tags if hasattr(odfv, "tags") else {}, + ) + + fv_outputs.append( + OutputDataset( + namespace=namespace, + name=odfv.name, + facets=output_facets, + ) + ) + + # Emit Job 1: Feature Views job + job_facets = { + "feast_project": FeastProjectFacet( + project_name=project, + ) + } + + result1 = self._client.emit_run_event( + job_name=f"feast_feature_views_{project}", + run_id=str(uuid.uuid4()), + event_type=RunState.COMPLETE, + inputs=fv_inputs, + outputs=fv_outputs, + job_facets=job_facets, + namespace=namespace, + ) + results.append(result1) + + if result1: + logger.info( + f"✓ Emitted feature views lineage for '{project}' " + f"({len(fv_inputs)} inputs → {len(fv_outputs)} outputs)" + ) + + # ============================================================ + # Jobs for FeatureServices: One job per FeatureService + # Each job shows: FeatureViews (that are part of this FS) → FeatureService + # This matches Feast UI where links are only shown for actual membership + # ============================================================ + for fs in feature_services: + fs_inputs = [] + all_fs_features = [] # Collect all features for schema + fv_names_in_fs = [] # Track feature view names + + # Only include FeatureViews that are actually part of this FeatureService + for proj in fs.feature_view_projections: + fv_name = proj.name + if fv_name: + fv_names_in_fs.append(fv_name) + # Find the feature view to get schema + input_facets: Dict[str, Any] = {} + + # Use projection features if specified, otherwise use all from FV + proj_features = proj.features if proj.features else [] + + for fv in feature_views + on_demand_feature_views: + if fv.name == fv_name: + # Use projection features if available, else all FV features + features_to_use = ( + proj_features + if proj_features + else (fv.features if fv.features else []) + ) + if features_to_use: + input_facets["schema"] = ( + schema_dataset.SchemaDatasetFacet( + fields=[ + feast_field_to_schema_field(f) + for f in features_to_use + ] + ) + ) + # Collect features for FS output schema + all_fs_features.extend(features_to_use) + break + + fs_inputs.append( + InputDataset( + namespace=namespace, + name=fv_name, + facets=input_facets, + ) + ) + + # Build FeatureService output with schema and metadata + fs_output_facets: Dict[str, Any] = {} + + # Add schema with all features from constituent feature views + if all_fs_features: + fs_output_facets["schema"] = schema_dataset.SchemaDatasetFacet( + fields=[feast_field_to_schema_field(f) for f in all_fs_features] + ) + + # Add documentation with feature view list + if fv_names_in_fs: + from openlineage.client.facet_v2 import documentation_dataset + + fs_output_facets["documentation"] = ( + documentation_dataset.DocumentationDatasetFacet( + description=( + f"Feature Service '{fs.name}' aggregates features from: " + f"{', '.join(fv_names_in_fs)}. " + f"Total features: {len(all_fs_features)}." + ) + ) + ) + + # Add Feast-specific facet with detailed metadata + from feast.openlineage.facets import FeastFeatureServiceFacet + + fs_output_facets["feast_featureService"] = FeastFeatureServiceFacet( + name=fs.name, + feature_views=fv_names_in_fs, + feature_count=len(all_fs_features), + description=fs.description if fs.description else "", + owner=fs.owner if fs.owner else "", + tags=fs.tags if fs.tags else {}, + logging_enabled=getattr(fs, "logging", None) is not None, + ) + + fs_output = OutputDataset( + namespace=namespace, + name=fs.name, + facets=fs_output_facets, + ) + + # Emit a job for this specific FeatureService + job_facets = { + "feast_project": FeastProjectFacet( + project_name=project, + ) + } + + result = self._client.emit_run_event( + job_name=f"feature_service_{fs.name}", # Prefix to avoid conflict with dataset + run_id=str(uuid.uuid4()), + event_type=RunState.COMPLETE, + inputs=fs_inputs, + outputs=[fs_output], + job_facets=job_facets, + namespace=namespace, + ) + results.append(result) + + return results + + except Exception as e: + logger.error(f"Error emitting project lineage for {project}: {e}") + return [False] + + def close(self): + """Close the underlying client.""" + self._client.close() diff --git a/sdk/python/feast/openlineage/facets.py b/sdk/python/feast/openlineage/facets.py new file mode 100644 index 00000000000..d350b74f0df --- /dev/null +++ b/sdk/python/feast/openlineage/facets.py @@ -0,0 +1,281 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Custom OpenLineage facets for Feast Feature Store. + +These facets extend the standard OpenLineage facets to capture Feast-specific +metadata about feature views, feature services, data sources, and entities. +""" + +from typing import Dict, List, Optional + +import attr + +try: + from openlineage.client.generated.base import DatasetFacet, JobFacet, RunFacet + from openlineage.client.utils import RedactMixin + + OPENLINEAGE_AVAILABLE = True +except ImportError: + # Provide stub classes when OpenLineage is not installed + OPENLINEAGE_AVAILABLE = False + + class RedactMixin: # type: ignore[no-redef] + pass + + @attr.define + class JobFacet: # type: ignore[no-redef] + _producer: str = attr.field(default="") + _schemaURL: str = attr.field(default="") + + def __attrs_post_init__(self): + pass + + @attr.define + class DatasetFacet: # type: ignore[no-redef] + _producer: str = attr.field(default="") + _schemaURL: str = attr.field(default="") + _deleted: bool = attr.field(default=None) + + def __attrs_post_init__(self): + pass + + @attr.define + class RunFacet: # type: ignore[no-redef] + _producer: str = attr.field(default="") + _schemaURL: str = attr.field(default="") + + def __attrs_post_init__(self): + pass + + +# Schema URL base for Feast facets +FEAST_FACET_SCHEMA_BASE = "https://feast.dev/spec/facets/1-0-0" + + +@attr.define(kw_only=True) +class FeastFeatureViewFacet(JobFacet): + """ + Custom facet for Feast Feature View metadata. + + This facet captures Feast-specific metadata about feature views including + TTL, entities, online/offline status, and transformation mode. + + Attributes: + name: Feature view name + ttl_seconds: Time-to-live in seconds (0 means no TTL) + entities: List of entity names associated with the feature view + features: List of feature names in the feature view + online_enabled: Whether online retrieval is enabled + offline_enabled: Whether offline retrieval is enabled + mode: Transformation mode (PYTHON, PANDAS, RAY, SPARK, SQL, etc.) + description: Human-readable description + owner: Owner of the feature view + tags: Key-value tags + """ + + name: str = attr.field() + ttl_seconds: int = attr.field(default=0) + entities: List[str] = attr.field(factory=list) + features: List[str] = attr.field(factory=list) + online_enabled: bool = attr.field(default=True) + offline_enabled: bool = attr.field(default=False) + mode: Optional[str] = attr.field(default=None) + description: str = attr.field(default="") + owner: str = attr.field(default="") + tags: Dict[str, str] = attr.field(factory=dict) + + @staticmethod + def _get_schema() -> str: + return f"{FEAST_FACET_SCHEMA_BASE}/FeastFeatureViewFacet.json" + + +@attr.define(kw_only=True) +class FeastFeatureServiceFacet(JobFacet): + """ + Custom facet for Feast Feature Service metadata. + + This facet captures metadata about feature services which aggregate + multiple feature views for serving. + + Attributes: + name: Feature service name + feature_views: List of feature view names included in the service + feature_count: Total number of features in the service + description: Human-readable description + owner: Owner of the feature service + tags: Key-value tags + logging_enabled: Whether feature logging is enabled + """ + + name: str = attr.field() + feature_views: List[str] = attr.field(factory=list) + feature_count: int = attr.field(default=0) + description: str = attr.field(default="") + owner: str = attr.field(default="") + tags: Dict[str, str] = attr.field(factory=dict) + logging_enabled: bool = attr.field(default=False) + + @staticmethod + def _get_schema() -> str: + return f"{FEAST_FACET_SCHEMA_BASE}/FeastFeatureServiceFacet.json" + + +@attr.define(kw_only=True) +class FeastDataSourceFacet(DatasetFacet): + """ + Custom facet for Feast Data Source metadata. + + This facet captures metadata about data sources including their type, + configuration, and field mappings. + + Attributes: + name: Data source name + source_type: Type of data source (file, bigquery, snowflake, etc.) + timestamp_field: Name of the timestamp field + created_timestamp_field: Name of the created timestamp field + field_mapping: Mapping from source fields to feature names + description: Human-readable description + tags: Key-value tags + """ + + name: str = attr.field() + source_type: str = attr.field() + timestamp_field: Optional[str] = attr.field(default=None) + created_timestamp_field: Optional[str] = attr.field(default=None) + field_mapping: Dict[str, str] = attr.field(factory=dict) + description: str = attr.field(default="") + tags: Dict[str, str] = attr.field(factory=dict) + + @staticmethod + def _get_schema() -> str: + return f"{FEAST_FACET_SCHEMA_BASE}/FeastDataSourceFacet.json" + + +@attr.define(kw_only=True) +class FeastEntityFacet(DatasetFacet): + """ + Custom facet for Feast Entity metadata. + + This facet captures metadata about entities which define the keys + for feature lookups. + + Attributes: + name: Entity name + join_keys: List of join key column names + value_type: Data type of the entity + description: Human-readable description + owner: Owner of the entity + tags: Key-value tags + """ + + name: str = attr.field() + join_keys: List[str] = attr.field(factory=list) + value_type: str = attr.field(default="STRING") + description: str = attr.field(default="") + owner: str = attr.field(default="") + tags: Dict[str, str] = attr.field(factory=dict) + + @staticmethod + def _get_schema() -> str: + return f"{FEAST_FACET_SCHEMA_BASE}/FeastEntityFacet.json" + + +@attr.define(kw_only=True) +class FeastMaterializationFacet(RunFacet): + """ + Custom facet for Feast Materialization run metadata. + + This facet captures information about feature materialization runs + including the time range, feature views being materialized, and statistics. + + Attributes: + feature_views: List of feature view names being materialized + start_date: Start date of the materialization window + end_date: End date of the materialization window + project: Feast project name + rows_written: Number of rows written (if available) + online_store_type: Type of online store being written to + offline_store_type: Type of offline store being read from + """ + + feature_views: List[str] = attr.field(factory=list) + start_date: Optional[str] = attr.field(default=None) + end_date: Optional[str] = attr.field(default=None) + project: str = attr.field(default="") + rows_written: Optional[int] = attr.field(default=None) + online_store_type: str = attr.field(default="") + offline_store_type: str = attr.field(default="") + + @staticmethod + def _get_schema() -> str: + return f"{FEAST_FACET_SCHEMA_BASE}/FeastMaterializationFacet.json" + + +@attr.define(kw_only=True) +class FeastRetrievalFacet(RunFacet): + """ + Custom facet for Feast Feature Retrieval run metadata. + + This facet captures information about feature retrieval operations + including whether it's online or historical, the feature service used, + and retrieval statistics. + + Attributes: + retrieval_type: Type of retrieval (online, historical) + feature_service: Name of the feature service used (if any) + feature_views: List of feature view names queried + features: List of feature names retrieved + entity_count: Number of entities queried + full_feature_names: Whether full feature names were used + """ + + retrieval_type: str = attr.field() # "online" or "historical" + feature_service: Optional[str] = attr.field(default=None) + feature_views: List[str] = attr.field(factory=list) + features: List[str] = attr.field(factory=list) + entity_count: Optional[int] = attr.field(default=None) + full_feature_names: bool = attr.field(default=False) + + @staticmethod + def _get_schema() -> str: + return f"{FEAST_FACET_SCHEMA_BASE}/FeastRetrievalFacet.json" + + +@attr.define(kw_only=True) +class FeastProjectFacet(JobFacet): + """ + Custom facet for Feast Project metadata. + + This facet captures information about the Feast project context + for lineage events. + + Attributes: + project_name: Name of the Feast project + provider: Infrastructure provider (local, gcp, aws, etc.) + online_store_type: Type of online store + offline_store_type: Type of offline store + registry_type: Type of registry (file, sql, etc.) + """ + + project_name: str = attr.field() + provider: str = attr.field(default="local") + online_store_type: str = attr.field(default="") + offline_store_type: str = attr.field(default="") + registry_type: str = attr.field(default="file") + + @staticmethod + def _get_schema() -> str: + return f"{FEAST_FACET_SCHEMA_BASE}/FeastProjectFacet.json" diff --git a/sdk/python/feast/openlineage/mappers.py b/sdk/python/feast/openlineage/mappers.py new file mode 100644 index 00000000000..9be9096aed2 --- /dev/null +++ b/sdk/python/feast/openlineage/mappers.py @@ -0,0 +1,546 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Mapping utilities for converting Feast objects to OpenLineage objects. + +This module provides functions to map Feast entities like FeatureViews, +FeatureServices, DataSources, and Entities to their OpenLineage equivalents. +""" + +from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Union + +if TYPE_CHECKING: + from feast import Entity, FeatureService, FeatureView + from feast.data_source import DataSource + from feast.field import Field + from feast.labeling.label_view import LabelView + from feast.on_demand_feature_view import OnDemandFeatureView + from feast.stream_feature_view import StreamFeatureView + +try: + from openlineage.client.event_v2 import ( + InputDataset, + Job, + OutputDataset, + ) + from openlineage.client.facet_v2 import ( + datasource_dataset, + documentation_dataset, + schema_dataset, + ) + + OPENLINEAGE_AVAILABLE = True +except ImportError: + OPENLINEAGE_AVAILABLE = False + + +def _check_openlineage_available(): + """Check if OpenLineage is available and raise if not.""" + if not OPENLINEAGE_AVAILABLE: + raise ImportError( + "OpenLineage is not installed. Please install it with: " + "pip install openlineage-python" + ) + + +def feast_field_to_schema_field( + field: "Field", +) -> "schema_dataset.SchemaDatasetFacetFields": + """ + Convert a Feast Field to an OpenLineage schema field. + + Args: + field: Feast Field object + + Returns: + OpenLineage SchemaDatasetFacetFields object + """ + _check_openlineage_available() + + # Build description with tags + description_parts = [] + + # Add description if present + if hasattr(field, "description") and field.description: + description_parts.append(field.description) + + # Add tags if present + if hasattr(field, "tags") and field.tags: + tags_str = ", ".join(f"{k}={v}" for k, v in field.tags.items()) + description_parts.append(f"[Tags: {tags_str}]") + + description = " ".join(description_parts) if description_parts else None + + return schema_dataset.SchemaDatasetFacetFields( + name=field.name, + type=str(field.dtype) if field.dtype else None, + description=description, + ) + + +def data_source_to_dataset( + data_source: "DataSource", + namespace: str = "feast", + as_input: bool = True, +) -> Any: + """ + Convert a Feast DataSource to an OpenLineage Dataset. + + Args: + data_source: Feast DataSource object + namespace: OpenLineage namespace + as_input: Whether to create an InputDataset (True) or OutputDataset (False) + + Returns: + OpenLineage InputDataset or OutputDataset object + """ + _check_openlineage_available() + + from feast.openlineage.facets import FeastDataSourceFacet + + # Determine source type and name + source_type = type(data_source).__name__ + source_name = data_source.name if data_source.name else f"unnamed_{source_type}" + + # Build namespace based on source type + dataset_namespace = _get_data_source_namespace(data_source, namespace) + + # Build facets + facets: Dict[str, Any] = {} + + # Add datasource facet + facets["dataSource"] = datasource_dataset.DatasourceDatasetFacet( + name=source_name, + uri=_get_data_source_uri(data_source), + ) + + # Add Feast-specific facet + facets["feast_dataSource"] = FeastDataSourceFacet( + name=source_name, + source_type=source_type, + timestamp_field=data_source.timestamp_field + if hasattr(data_source, "timestamp_field") + else None, + created_timestamp_field=data_source.created_timestamp_column + if hasattr(data_source, "created_timestamp_column") + else None, + field_mapping=data_source.field_mapping + if hasattr(data_source, "field_mapping") + else {}, + description=data_source.description + if hasattr(data_source, "description") + else "", + tags=data_source.tags if hasattr(data_source, "tags") else {}, + ) + + # Add documentation if available + if hasattr(data_source, "description") and data_source.description: + facets["documentation"] = documentation_dataset.DocumentationDatasetFacet( + description=data_source.description + ) + + if as_input: + return InputDataset( + namespace=dataset_namespace, + name=source_name, + facets=facets, + ) + else: + return OutputDataset( + namespace=dataset_namespace, + name=source_name, + facets=facets, + ) + + +def _get_data_source_namespace( + data_source: "DataSource", default_namespace: str +) -> str: + """ + Get the OpenLineage namespace for a data source. + + Uses the same namespace as other Feast objects to ensure proper + lineage connections in the graph. + + Args: + data_source: Feast DataSource + default_namespace: Default namespace to use + + Returns: + Namespace string + """ + # Use consistent namespace to ensure lineage graph connects properly + return default_namespace + + +def _get_data_source_uri(data_source: "DataSource") -> str: + """ + Get the URI for a data source. + + Args: + data_source: Feast DataSource + + Returns: + URI string representing the data source location + """ + if hasattr(data_source, "path") and data_source.path: + return data_source.path + elif hasattr(data_source, "table") and data_source.table: + return f"table://{data_source.table}" + elif hasattr(data_source, "query") and data_source.query: + return f"query://{hash(data_source.query)}" + else: + return f"feast://{data_source.name if data_source.name else 'unnamed'}" + + +def feature_view_to_job( + feature_view: "Union[FeatureView, LabelView]", + namespace: str = "feast", + include_schema: bool = True, + job_type: str = "FEATURE_VIEW", +) -> Tuple["Job", List["InputDataset"], List["OutputDataset"]]: + """ + Convert a Feast FeatureView or LabelView to an OpenLineage Job with inputs/outputs. + + A FeatureView/LabelView represents a transformation from data sources to features/labels, + so it maps to an OpenLineage Job with: + - Inputs: The batch and stream sources + - Outputs: The feature/label view itself (as a logical dataset) + + Args: + feature_view: Feast FeatureView or LabelView object + namespace: OpenLineage namespace + include_schema: Whether to include schema information + job_type: Type identifier (FEATURE_VIEW, LABEL_VIEW, etc.) + + Returns: + Tuple of (Job, list of InputDatasets, list of OutputDatasets) + """ + _check_openlineage_available() + + from feast.openlineage.facets import FeastFeatureViewFacet + + # Create job facets + job_facets: Dict[str, Any] = {} + + # Add Feast-specific facet + ttl_seconds = 0 + if feature_view.ttl: + ttl_seconds = int(feature_view.ttl.total_seconds()) + + job_facets["feast_featureView"] = FeastFeatureViewFacet( + name=feature_view.name, + ttl_seconds=ttl_seconds, + entities=feature_view.entities if feature_view.entities else [], + features=[f.name for f in feature_view.features] + if feature_view.features + else [], + online_enabled=feature_view.online, + offline_enabled=getattr(feature_view, "offline", False), + mode=str(feature_view.mode) + if hasattr(feature_view, "mode") and feature_view.mode + else None, + description=feature_view.description if feature_view.description else "", + owner=feature_view.owner if feature_view.owner else "", + tags=feature_view.tags if feature_view.tags else {}, + ) + + # Add documentation + if feature_view.description: + job_facets["documentation"] = documentation_dataset.DocumentationDatasetFacet( + description=feature_view.description + ) + + # Create job + job = Job( + namespace=namespace, + name=f"feature_view_{feature_view.name}", + facets=job_facets, + ) + + # Create input datasets from sources + inputs: List[InputDataset] = [] + + # Add data sources as inputs + if hasattr(feature_view, "batch_source") and feature_view.batch_source: + inputs.append( + data_source_to_dataset( + feature_view.batch_source, namespace=namespace, as_input=True + ) + ) + + if hasattr(feature_view, "stream_source") and feature_view.stream_source: + inputs.append( + data_source_to_dataset( + feature_view.stream_source, namespace=namespace, as_input=True + ) + ) + + # Add entities as inputs (they appear as nodes in lineage) + if feature_view.entities: + for entity_name in feature_view.entities: + if entity_name and entity_name != "__dummy": + inputs.append( + InputDataset( + namespace=namespace, + name=entity_name, + ) + ) + + # Create output dataset (the feature view itself as a logical dataset) + output_facets: Dict[str, Any] = {} + + if include_schema and feature_view.features: + output_facets["schema"] = schema_dataset.SchemaDatasetFacet( + fields=[feast_field_to_schema_field(f) for f in feature_view.features] + ) + + outputs = [ + OutputDataset( + namespace=namespace, + name=feature_view.name, + facets=output_facets, + ) + ] + + return job, inputs, outputs + + +def feature_service_to_job( + feature_service: "FeatureService", + feature_views: List[ + Union["FeatureView", "OnDemandFeatureView", "StreamFeatureView"] + ], + namespace: str = "feast", +) -> Tuple["Job", List["InputDataset"], List["OutputDataset"]]: + """ + Convert a Feast FeatureService to an OpenLineage Job with inputs/outputs. + + A FeatureService aggregates multiple feature views, so it maps to an + OpenLineage Job with: + - Inputs: The feature views it consumes + - Outputs: The aggregated feature set + + Args: + feature_service: Feast FeatureService object + feature_views: List of all available feature views (FeatureView, + OnDemandFeatureView, StreamFeatureView) + namespace: OpenLineage namespace + + Returns: + Tuple of (Job, list of InputDatasets, list of OutputDatasets) + """ + _check_openlineage_available() + + from feast.openlineage.facets import FeastFeatureServiceFacet + + # Create job facets + job_facets: Dict[str, Any] = {} + + # Get feature view names + fv_names = [proj.name for proj in feature_service.feature_view_projections] + + # Build a lookup map for feature views by name + fv_by_name = {fv.name: fv for fv in feature_views} + + # Count total features + # When proj.features is empty/None, it means "all features from that feature view" + # In that case, look up the actual feature view to get the real count + total_features = 0 + for proj in feature_service.feature_view_projections: + if proj.features: + total_features += len(proj.features) + elif proj.name in fv_by_name: + fv = fv_by_name[proj.name] + if hasattr(fv, "features") and fv.features: + total_features += len(fv.features) + + # Add Feast-specific facet + job_facets["feast_featureService"] = FeastFeatureServiceFacet( + name=feature_service.name, + feature_views=fv_names, + feature_count=total_features, + description=feature_service.description if feature_service.description else "", + owner=feature_service.owner if feature_service.owner else "", + tags=feature_service.tags if feature_service.tags else {}, + logging_enabled=getattr(feature_service, "logging", None) is not None, + ) + + # Add documentation + if feature_service.description: + job_facets["documentation"] = documentation_dataset.DocumentationDatasetFacet( + description=feature_service.description + ) + + # Create job + job = Job( + namespace=namespace, + name=f"feature_service_{feature_service.name}", + facets=job_facets, + ) + + # Create input datasets from feature views + inputs: List[InputDataset] = [] + all_features = [] + + for fv in feature_views: + input_facets: Dict[str, Any] = {} + if fv.features: + input_facets["schema"] = schema_dataset.SchemaDatasetFacet( + fields=[feast_field_to_schema_field(f) for f in fv.features] + ) + all_features.extend(fv.features) + + inputs.append( + InputDataset( + namespace=namespace, + name=fv.name, + facets=input_facets, + ) + ) + + # Create output dataset (the feature service as a logical aggregation) + output_facets: Dict[str, Any] = {} + if all_features: + output_facets["schema"] = schema_dataset.SchemaDatasetFacet( + fields=[feast_field_to_schema_field(f) for f in all_features] + ) + + outputs = [ + OutputDataset( + namespace=namespace, + name=feature_service.name, + facets=output_facets, + ) + ] + + return job, inputs, outputs + + +def entity_to_dataset( + entity: "Entity", + namespace: str = "feast", +) -> "InputDataset": + """ + Convert a Feast Entity to an OpenLineage InputDataset. + + Entities define the keys for feature lookups and can be represented + as datasets with schema information. + + Args: + entity: Feast Entity object + namespace: OpenLineage namespace + + Returns: + OpenLineage InputDataset object + """ + _check_openlineage_available() + + from feast.openlineage.facets import FeastEntityFacet + + facets: Dict[str, Any] = {} + + # Add entity facet + facets["feast_entity"] = FeastEntityFacet( + name=entity.name, + join_keys=[entity.join_key] if entity.join_key else [], + value_type=str(entity.value_type) if entity.value_type else "STRING", + description=entity.description if entity.description else "", + owner=entity.owner if hasattr(entity, "owner") and entity.owner else "", + tags=entity.tags if entity.tags else {}, + ) + + # Add schema for join keys + if entity.join_key: + facets["schema"] = schema_dataset.SchemaDatasetFacet( + fields=[ + schema_dataset.SchemaDatasetFacetFields( + name=entity.join_key, + type=str(entity.value_type) if entity.value_type else "STRING", + ) + ] + ) + + # Add documentation + if entity.description: + facets["documentation"] = documentation_dataset.DocumentationDatasetFacet( + description=entity.description + ) + + return InputDataset( + namespace=namespace, + name=entity.name, + facets=facets, + ) + + +def online_store_to_dataset( + store_type: str, + feature_view_name: str, + namespace: str = "feast", +) -> "OutputDataset": + """ + Create an OpenLineage OutputDataset for an online store. + + Args: + store_type: Type of online store (redis, sqlite, dynamodb, etc.) + feature_view_name: Name of the feature view being stored + namespace: OpenLineage namespace + + Returns: + OpenLineage OutputDataset object + """ + _check_openlineage_available() + + return OutputDataset( + namespace=namespace, + name=f"online_store_{feature_view_name}", + facets={ + "dataSource": datasource_dataset.DatasourceDatasetFacet( + name=f"{store_type}_online_store", + uri=f"{store_type}://feast/{feature_view_name}", + ) + }, + ) + + +def offline_store_to_dataset( + store_type: str, + feature_view_name: str, + namespace: str = "feast", +) -> "InputDataset": + """ + Create an OpenLineage InputDataset for an offline store. + + Args: + store_type: Type of offline store (file, bigquery, snowflake, etc.) + feature_view_name: Name of the feature view being read + namespace: OpenLineage namespace + + Returns: + OpenLineage InputDataset object + """ + _check_openlineage_available() + + return InputDataset( + namespace=f"{namespace}/offline_store/{store_type}", + name=feature_view_name, + facets={ + "dataSource": datasource_dataset.DatasourceDatasetFacet( + name=f"{store_type}_offline_store", + uri=f"{store_type}://feast/{feature_view_name}", + ) + }, + ) diff --git a/sdk/python/feast/openlineage/models.py b/sdk/python/feast/openlineage/models.py new file mode 100644 index 00000000000..ec79cf3620d --- /dev/null +++ b/sdk/python/feast/openlineage/models.py @@ -0,0 +1,218 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +SQLAlchemy table definitions for the OpenLineage consumer lineage store. + +Uses SQLAlchemy Core (Table + Column) pattern consistent with Feast's +existing SQL registry in feast/infra/registry/sql.py. +""" + +from sqlalchemy import ( + BigInteger, + Column, + Index, + MetaData, + String, + Text, + UniqueConstraint, +) + +ol_metadata = MetaData() + +openlineage_events = ( + "openlineage_events", + ol_metadata, + Column("event_id", String(255), primary_key=True), + Column("event_type", String(50), nullable=False), + Column("event_time", BigInteger, nullable=False), + Column("producer", String(512), nullable=True), + Column("job_namespace", String(512), nullable=False), + Column("job_name", String(512), nullable=False), + Column("run_id", String(255), nullable=True), + Column("event_json", Text, nullable=False), + Column("created_at", BigInteger, nullable=False), +) + +openlineage_jobs = ( + "openlineage_jobs", + ol_metadata, + Column("job_namespace", String(512), primary_key=True), + Column("job_name", String(512), primary_key=True), + Column("job_type", String(100), nullable=True), + Column("producer", String(512), nullable=True), + Column("description", Text, nullable=True), + Column("facets_json", Text, nullable=True), + Column("latest_run_id", String(255), nullable=True), + Column("updated_at", BigInteger, nullable=False), +) + +openlineage_datasets = ( + "openlineage_datasets", + ol_metadata, + Column("dataset_namespace", String(512), primary_key=True), + Column("dataset_name", String(512), primary_key=True), + Column("source_type", String(100), nullable=True), + Column("producer", String(512), nullable=True), + Column("description", Text, nullable=True), + Column("schema_json", Text, nullable=True), + Column("facets_json", Text, nullable=True), + Column("feast_object_type", String(100), nullable=True), + Column("feast_object_name", String(255), nullable=True), + Column("feast_project", String(255), nullable=True), + Column("updated_at", BigInteger, nullable=False), +) + +openlineage_runs = ( + "openlineage_runs", + ol_metadata, + Column("run_id", String(255), primary_key=True), + Column("job_namespace", String(512), nullable=False), + Column("job_name", String(512), nullable=False), + Column("state", String(50), nullable=False), + Column("started_at", BigInteger, nullable=True), + Column("ended_at", BigInteger, nullable=True), + Column("facets_json", Text, nullable=True), + Column("updated_at", BigInteger, nullable=False), +) + +openlineage_run_io = ( + "openlineage_run_io", + ol_metadata, + Column("run_id", String(255), nullable=False), + Column("dataset_namespace", String(512), nullable=False), + Column("dataset_name", String(512), nullable=False), + Column("io_type", String(50), nullable=False), + Column("facets_json", Text, nullable=True), + UniqueConstraint( + "run_id", + "dataset_namespace", + "dataset_name", + "io_type", + name="uq_run_io", + ), +) + +openlineage_lineage_edges = ( + "openlineage_lineage_edges", + ol_metadata, + Column("source_type", String(50), nullable=False), + Column("source_namespace", String(512), nullable=False), + Column("source_name", String(512), nullable=False), + Column("target_type", String(50), nullable=False), + Column("target_namespace", String(512), nullable=False), + Column("target_name", String(512), nullable=False), + Column("edge_type", String(50), nullable=True), + Column("updated_at", BigInteger, nullable=False), + UniqueConstraint( + "source_type", + "source_namespace", + "source_name", + "target_type", + "target_namespace", + "target_name", + name="uq_lineage_edge", + ), +) + +openlineage_dataset_symlinks = ( + "openlineage_dataset_symlinks", + ol_metadata, + Column("dataset_namespace", String(512), nullable=False), + Column("dataset_name", String(512), nullable=False), + Column("linked_namespace", String(512), nullable=False), + Column("linked_name", String(512), nullable=False), + Column("link_type", String(50), nullable=False), + Column("updated_at", BigInteger, nullable=False), + UniqueConstraint( + "dataset_namespace", + "dataset_name", + "linked_namespace", + "linked_name", + name="uq_dataset_symlink", + ), +) + + +def _build_tables(): + """Build Table objects from the tuple definitions above.""" + from sqlalchemy import Table + + tables = {} + for name, tbl_def in [ + ("events", openlineage_events), + ("jobs", openlineage_jobs), + ("datasets", openlineage_datasets), + ("runs", openlineage_runs), + ("run_io", openlineage_run_io), + ("lineage_edges", openlineage_lineage_edges), + ("dataset_symlinks", openlineage_dataset_symlinks), + ]: + tbl_name = tbl_def[0] + meta = tbl_def[1] + columns = tbl_def[2:] + tables[name] = Table(tbl_name, meta, *columns) + return tables + + +OL_TABLES = _build_tables() + +idx_events_time = Index( + "idx_ol_events_time", + OL_TABLES["events"].c.event_time, +) +idx_events_job = Index( + "idx_ol_events_job", + OL_TABLES["events"].c.job_namespace, + OL_TABLES["events"].c.job_name, +) +idx_runs_job = Index( + "idx_ol_runs_job", + OL_TABLES["runs"].c.job_namespace, + OL_TABLES["runs"].c.job_name, +) +idx_run_io_run = Index( + "idx_ol_run_io_run", + OL_TABLES["run_io"].c.run_id, +) +idx_run_io_dataset = Index( + "idx_ol_run_io_dataset", + OL_TABLES["run_io"].c.dataset_namespace, + OL_TABLES["run_io"].c.dataset_name, +) +idx_lineage_source = Index( + "idx_ol_lineage_source", + OL_TABLES["lineage_edges"].c.source_namespace, + OL_TABLES["lineage_edges"].c.source_name, +) +idx_lineage_target = Index( + "idx_ol_lineage_target", + OL_TABLES["lineage_edges"].c.target_namespace, + OL_TABLES["lineage_edges"].c.target_name, +) +idx_datasets_feast = Index( + "idx_ol_datasets_feast", + OL_TABLES["datasets"].c.feast_project, + OL_TABLES["datasets"].c.feast_object_type, +) +idx_symlinks_dataset = Index( + "idx_ol_symlinks_dataset", + OL_TABLES["dataset_symlinks"].c.dataset_namespace, + OL_TABLES["dataset_symlinks"].c.dataset_name, +) +idx_symlinks_linked = Index( + "idx_ol_symlinks_linked", + OL_TABLES["dataset_symlinks"].c.linked_namespace, + OL_TABLES["dataset_symlinks"].c.linked_name, +) diff --git a/sdk/python/feast/openlineage/processor.py b/sdk/python/feast/openlineage/processor.py new file mode 100644 index 00000000000..bbacf377b96 --- /dev/null +++ b/sdk/python/feast/openlineage/processor.py @@ -0,0 +1,383 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +OpenLineage event processor for Feast. + +Parses incoming OpenLineage events (RunEvent, DatasetEvent, JobEvent), +extracts metadata, and builds the lineage graph in the store. +""" + +import logging +import uuid +from typing import Any, Dict, List, Optional + +from feast.openlineage.store import OpenLineageStore + +logger = logging.getLogger(__name__) + + +class OpenLineageProcessor: + """ + Processes OpenLineage events and stores them in the lineage store. + + Handles RunEvent, DatasetEvent, and JobEvent types, extracting + jobs, datasets, runs, I/O relationships, and building lineage edges. + """ + + def __init__( + self, + store: OpenLineageStore, + namespace_mapping: Optional[Dict[str, str]] = None, + ): + self._store = store + self._namespace_mapping = namespace_mapping or {} + + def process_event(self, event: Dict[str, Any]) -> str: + """ + Process a single OpenLineage event. + + Determines the event type and delegates to the appropriate handler. + Returns the event ID. + """ + event_id = str(uuid.uuid4()) + + if "run" in event and "job" in event: + self._process_run_event(event_id, event) + elif "dataset" in event and "job" not in event and "run" not in event: + self._process_dataset_event(event_id, event) + elif "job" in event and "run" not in event: + self._process_job_event(event_id, event) + else: + logger.warning(f"Unknown event structure, storing as raw event: {event_id}") + self._store.store_event(event_id, event) + + return event_id + + def process_batch(self, events: List[Dict[str, Any]]) -> Dict[str, Any]: + """Process a batch of OpenLineage events.""" + successful = 0 + failed = 0 + event_ids: List[str] = [] + + for event in events: + try: + event_id = self.process_event(event) + successful += 1 + event_ids.append(event_id) + except Exception as e: + logger.error(f"Failed to process event: {e}") + failed += 1 + + return { + "received": len(events), + "successful": successful, + "failed": failed, + "event_ids": event_ids, + } + + def _process_run_event(self, event_id: str, event: Dict[str, Any]): + job = event.get("job", {}) + run = event.get("run", {}) + event_type = event.get("eventType", "OTHER") + producer = event.get("producer") + + job_namespace = job.get("namespace", "") + job_name = job.get("name", "") + run_id = run.get("runId", "") + + self._store.store_event(event_id, event) + + self._store.upsert_job(job_namespace, job_name, job, producer=producer) + + run_facets = run.get("facets", {}) + self._store.upsert_run(run_id, job_namespace, job_name, event_type, run_facets) + + inputs = event.get("inputs", []) + outputs = event.get("outputs", []) + + for inp in inputs: + ds_namespace = inp.get("namespace", job_namespace) + ds_name = inp.get("name", "") + ds_facets = inp.get("facets", {}) + + feast_mapping = self._resolve_feast_mapping(ds_namespace, ds_name) + self._store.upsert_dataset( + ds_namespace, ds_name, ds_facets, feast_mapping, producer=producer + ) + self._store.store_run_io(run_id, ds_namespace, ds_name, "INPUT", ds_facets) + self._process_dataset_symlinks(ds_namespace, ds_name, ds_facets) + + self._store.upsert_lineage_edge( + source_type="dataset", + source_namespace=ds_namespace, + source_name=ds_name, + target_type="job", + target_namespace=job_namespace, + target_name=job_name, + edge_type="input", + ) + + for out in outputs: + ds_namespace = out.get("namespace", job_namespace) + ds_name = out.get("name", "") + ds_facets = out.get("facets", {}) + + feast_mapping = self._resolve_feast_mapping(ds_namespace, ds_name) + self._store.upsert_dataset( + ds_namespace, ds_name, ds_facets, feast_mapping, producer=producer + ) + self._store.store_run_io(run_id, ds_namespace, ds_name, "OUTPUT", ds_facets) + self._process_dataset_symlinks(ds_namespace, ds_name, ds_facets) + + self._store.upsert_lineage_edge( + source_type="job", + source_namespace=job_namespace, + source_name=job_name, + target_type="dataset", + target_namespace=ds_namespace, + target_name=ds_name, + edge_type="output", + ) + + self._build_dataset_to_dataset_edges(inputs, outputs, job_namespace) + + def _process_dataset_event(self, event_id: str, event: Dict[str, Any]): + dataset = event.get("dataset", {}) + ds_namespace = dataset.get("namespace", "") + ds_name = dataset.get("name", "") + ds_facets = dataset.get("facets", {}) + producer = event.get("producer") + + self._store.store_event(event_id, event) + + feast_mapping = self._resolve_feast_mapping(ds_namespace, ds_name) + self._store.upsert_dataset( + ds_namespace, ds_name, ds_facets, feast_mapping, producer=producer + ) + self._process_dataset_symlinks(ds_namespace, ds_name, ds_facets) + + def _process_job_event(self, event_id: str, event: Dict[str, Any]): + job = event.get("job", {}) + job_namespace = job.get("namespace", "") + job_name = job.get("name", "") + producer = event.get("producer") + + self._store.store_event(event_id, event) + + self._store.upsert_job(job_namespace, job_name, job, producer=producer) + + inputs = event.get("inputs", []) + outputs = event.get("outputs", []) + + for inp in inputs: + ds_namespace = inp.get("namespace", job_namespace) + ds_name = inp.get("name", "") + ds_facets = inp.get("facets", {}) + + feast_mapping = self._resolve_feast_mapping(ds_namespace, ds_name) + self._store.upsert_dataset( + ds_namespace, ds_name, ds_facets, feast_mapping, producer=producer + ) + self._process_dataset_symlinks(ds_namespace, ds_name, ds_facets) + + self._store.upsert_lineage_edge( + source_type="dataset", + source_namespace=ds_namespace, + source_name=ds_name, + target_type="job", + target_namespace=job_namespace, + target_name=job_name, + edge_type="input", + ) + + for out in outputs: + ds_namespace = out.get("namespace", job_namespace) + ds_name = out.get("name", "") + ds_facets = out.get("facets", {}) + + feast_mapping = self._resolve_feast_mapping(ds_namespace, ds_name) + self._store.upsert_dataset( + ds_namespace, ds_name, ds_facets, feast_mapping, producer=producer + ) + self._process_dataset_symlinks(ds_namespace, ds_name, ds_facets) + + self._store.upsert_lineage_edge( + source_type="job", + source_namespace=job_namespace, + source_name=job_name, + target_type="dataset", + target_namespace=ds_namespace, + target_name=ds_name, + edge_type="output", + ) + + def _build_dataset_to_dataset_edges( + self, + inputs: List[Dict], + outputs: List[Dict], + job_namespace: str, + ): + """ + Build transitive dataset-to-dataset edges through a job. + + For each input dataset and output dataset of the same job, + create an edge: input_dataset -> output_dataset. + This enables dataset-level lineage traversal without + requiring the UI to understand jobs. + """ + for inp in inputs: + in_ns = inp.get("namespace", job_namespace) + in_name = inp.get("name", "") + for out in outputs: + out_ns = out.get("namespace", job_namespace) + out_name = out.get("name", "") + if in_name and out_name: + self._store.upsert_lineage_edge( + source_type="dataset", + source_namespace=in_ns, + source_name=in_name, + target_type="dataset", + target_namespace=out_ns, + target_name=out_name, + edge_type="derived", + ) + + def _process_dataset_symlinks( + self, + ds_namespace: str, + ds_name: str, + ds_facets: Dict[str, Any], + ): + """ + Process SymlinksDatasetFacet and dataSource URI to create + cross-producer dataset links. + + OpenLineage SymlinksDatasetFacet spec: + { + "symlinks": { + "identifiers": [ + {"namespace": "...", "name": "...", "type": "TABLE"} + ] + } + } + + Also auto-links datasets sharing the same dataSource URI + across different namespaces. + """ + symlinks_facet = ds_facets.get("symlinks", {}) + identifiers = symlinks_facet.get("identifiers", []) + for ident in identifiers: + linked_ns = ident.get("namespace", "") + linked_name = ident.get("name", "") + link_type = ident.get("type", "symlink") + if ( + linked_ns + and linked_name + and (linked_ns != ds_namespace or linked_name != ds_name) + ): + self._store.upsert_dataset_symlink( + ds_namespace, + ds_name, + linked_ns, + linked_name, + link_type, + ) + self._store.upsert_dataset(linked_ns, linked_name) + self._store.upsert_lineage_edge( + source_type="dataset", + source_namespace=ds_namespace, + source_name=ds_name, + target_type="dataset", + target_namespace=linked_ns, + target_name=linked_name, + edge_type="symlink", + ) + self._store.upsert_lineage_edge( + source_type="dataset", + source_namespace=linked_ns, + source_name=linked_name, + target_type="dataset", + target_namespace=ds_namespace, + target_name=ds_name, + edge_type="symlink", + ) + + ds_uri = ds_facets.get("dataSource", {}).get("uri", "") + if ds_uri: + existing = self._store.find_datasets_by_uri(ds_uri) + for match in existing: + m_ns = match["namespace"] + m_name = match["name"] + if m_ns != ds_namespace or m_name != ds_name: + self._store.upsert_dataset_symlink( + ds_namespace, + ds_name, + m_ns, + m_name, + "dataSource_uri", + ) + self._store.upsert_lineage_edge( + source_type="dataset", + source_namespace=ds_namespace, + source_name=ds_name, + target_type="dataset", + target_namespace=m_ns, + target_name=m_name, + edge_type="symlink", + ) + self._store.upsert_lineage_edge( + source_type="dataset", + source_namespace=m_ns, + source_name=m_name, + target_type="dataset", + target_namespace=ds_namespace, + target_name=ds_name, + edge_type="symlink", + ) + + def _resolve_feast_mapping( + self, namespace: str, dataset_name: str + ) -> Optional[Dict[str, str]]: + """ + Attempt to map an OpenLineage dataset to a Feast registry object. + + Mapping rules: + 1. If the namespace matches a Feast project (directly or via namespace_mapping), + check if the dataset name matches a known Feast object naming pattern. + 2. Feast apply emits datasets with names like the FeatureView/FeatureService name. + 3. Feast materialize emits datasets named 'online_store_{fv_name}'. + """ + feast_project = self._namespace_mapping.get(namespace, namespace) + + if dataset_name.startswith("online_store_"): + fv_name = dataset_name[len("online_store_") :] + return { + "type": "featureView", + "name": fv_name, + "project": feast_project, + } + + if dataset_name.startswith("request_source_"): + return { + "type": "dataSource", + "name": dataset_name, + "project": feast_project, + } + + return { + "type": "unknown", + "name": dataset_name, + "project": feast_project, + } diff --git a/sdk/python/feast/openlineage/store.py b/sdk/python/feast/openlineage/store.py new file mode 100644 index 00000000000..3ffc83b928e --- /dev/null +++ b/sdk/python/feast/openlineage/store.py @@ -0,0 +1,801 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +OpenLineage lineage store for Feast. + +Provides CRUD operations for OpenLineage events, jobs, datasets, runs, +and lineage edges using SQLAlchemy Core, consistent with Feast's SQL registry. +""" + +import json +import logging +import time +from typing import Any, Dict, List, Optional + +from sqlalchemy import create_engine, select +from sqlalchemy.engine import Engine + +from feast.openlineage.models import OL_TABLES, ol_metadata + +logger = logging.getLogger(__name__) + + +class OpenLineageStore: + """ + Storage layer for OpenLineage lineage data. + + Stores events, jobs, datasets, runs, and lineage graph edges + in PostgreSQL (or SQLite for dev/testing). + """ + + def __init__( + self, engine: Optional[Engine] = None, connection_string: Optional[str] = None + ): + if engine: + self._engine = engine + elif connection_string: + self._engine = create_engine(connection_string) + else: + raise ValueError("Either engine or connection_string must be provided") + + def initialize(self): + ol_metadata.create_all(self._engine) + logger.info("OpenLineage store tables created/verified") + + @property + def engine(self) -> Engine: + return self._engine + + def store_event(self, event_id: str, event_data: Dict[str, Any]): + now = int(time.time() * 1000) + job = event_data.get("job", {}) + run = event_data.get("run", {}) + + row = { + "event_id": event_id, + "event_type": event_data.get("eventType", "UNKNOWN"), + "event_time": _parse_timestamp(event_data.get("eventTime", "")), + "producer": event_data.get("producer"), + "job_namespace": job.get("namespace", ""), + "job_name": job.get("name", ""), + "run_id": run.get("runId"), + "event_json": json.dumps(event_data), + "created_at": now, + } + + tbl = OL_TABLES["events"] + with self._engine.begin() as conn: + conn.execute(tbl.insert().values(**row)) + + def upsert_job( + self, + namespace: str, + name: str, + job_data: Dict[str, Any], + producer: Optional[str] = None, + ): + now = int(time.time() * 1000) + facets = job_data.get("facets", {}) + job_type = None + if "jobType" in facets: + jt = facets["jobType"] + job_type = jt.get("processingType", jt.get("integration")) + + description = None + if "documentation" in facets: + description = facets["documentation"].get("description") + + tbl = OL_TABLES["jobs"] + with self._engine.begin() as conn: + existing = conn.execute( + select(tbl).where( + tbl.c.job_namespace == namespace, + tbl.c.job_name == name, + ) + ).first() + + if existing: + update_vals = { + "job_type": job_type or existing.job_type, + "description": description or existing.description, + "facets_json": json.dumps(facets) + if facets + else existing.facets_json, + "updated_at": now, + } + if producer: + update_vals["producer"] = producer + conn.execute( + tbl.update() + .where(tbl.c.job_namespace == namespace, tbl.c.job_name == name) + .values(**update_vals) + ) + else: + conn.execute( + tbl.insert().values( + job_namespace=namespace, + job_name=name, + job_type=job_type, + producer=producer, + description=description, + facets_json=json.dumps(facets) if facets else None, + updated_at=now, + ) + ) + + def upsert_dataset( + self, + namespace: str, + name: str, + facets: Optional[Dict[str, Any]] = None, + feast_mapping: Optional[Dict[str, str]] = None, + producer: Optional[str] = None, + ): + now = int(time.time() * 1000) + facets = facets or {} + + schema_json = None + if "schema" in facets: + schema_json = json.dumps(facets["schema"]) + + description = None + if "documentation" in facets: + description = facets["documentation"].get("description") + + source_type = None + if "dataSource" in facets: + source_type = facets["dataSource"].get("name") + + feast_obj_type = feast_mapping.get("type") if feast_mapping else None + feast_obj_name = feast_mapping.get("name") if feast_mapping else None + feast_project = feast_mapping.get("project") if feast_mapping else None + + tbl = OL_TABLES["datasets"] + with self._engine.begin() as conn: + existing = conn.execute( + select(tbl).where( + tbl.c.dataset_namespace == namespace, + tbl.c.dataset_name == name, + ) + ).first() + + values = { + "source_type": source_type, + "description": description, + "schema_json": schema_json, + "facets_json": json.dumps(facets) if facets else None, + "updated_at": now, + } + if producer: + values["producer"] = producer + if feast_obj_type: + values["feast_object_type"] = feast_obj_type + if feast_obj_name: + values["feast_object_name"] = feast_obj_name + if feast_project: + values["feast_project"] = feast_project + + if existing: + conn.execute( + tbl.update() + .where( + tbl.c.dataset_namespace == namespace, + tbl.c.dataset_name == name, + ) + .values(**values) + ) + else: + values["dataset_namespace"] = namespace + values["dataset_name"] = name + conn.execute(tbl.insert().values(**values)) + + def upsert_run( + self, + run_id: str, + job_namespace: str, + job_name: str, + state: str, + facets: Optional[Dict] = None, + ): + now = int(time.time() * 1000) + tbl = OL_TABLES["runs"] + + with self._engine.begin() as conn: + existing = conn.execute(select(tbl).where(tbl.c.run_id == run_id)).first() + + if existing: + update_vals: Dict[str, Any] = {"state": state, "updated_at": now} + if state in ("COMPLETE", "FAIL", "ABORT"): + update_vals["ended_at"] = now + if facets: + update_vals["facets_json"] = json.dumps(facets) + conn.execute( + tbl.update().where(tbl.c.run_id == run_id).values(**update_vals) + ) + else: + conn.execute( + tbl.insert().values( + run_id=run_id, + job_namespace=job_namespace, + job_name=job_name, + state=state, + started_at=now if state == "START" else None, + ended_at=now + if state in ("COMPLETE", "FAIL", "ABORT") + else None, + facets_json=json.dumps(facets) if facets else None, + updated_at=now, + ) + ) + + tbl_jobs = OL_TABLES["jobs"] + conn.execute( + tbl_jobs.update() + .where( + tbl_jobs.c.job_namespace == job_namespace, + tbl_jobs.c.job_name == job_name, + ) + .values(latest_run_id=run_id, updated_at=now) + ) + + def store_run_io( + self, + run_id: str, + dataset_namespace: str, + dataset_name: str, + io_type: str, + facets: Optional[Dict] = None, + ): + tbl = OL_TABLES["run_io"] + with self._engine.begin() as conn: + existing = conn.execute( + select(tbl).where( + tbl.c.run_id == run_id, + tbl.c.dataset_namespace == dataset_namespace, + tbl.c.dataset_name == dataset_name, + tbl.c.io_type == io_type, + ) + ).first() + + if not existing: + conn.execute( + tbl.insert().values( + run_id=run_id, + dataset_namespace=dataset_namespace, + dataset_name=dataset_name, + io_type=io_type, + facets_json=json.dumps(facets) if facets else None, + ) + ) + + def upsert_lineage_edge( + self, + source_type: str, + source_namespace: str, + source_name: str, + target_type: str, + target_namespace: str, + target_name: str, + edge_type: Optional[str] = None, + ): + now = int(time.time() * 1000) + tbl = OL_TABLES["lineage_edges"] + with self._engine.begin() as conn: + existing = conn.execute( + select(tbl).where( + tbl.c.source_type == source_type, + tbl.c.source_namespace == source_namespace, + tbl.c.source_name == source_name, + tbl.c.target_type == target_type, + tbl.c.target_namespace == target_namespace, + tbl.c.target_name == target_name, + ) + ).first() + + if existing: + conn.execute( + tbl.update() + .where( + tbl.c.source_type == source_type, + tbl.c.source_namespace == source_namespace, + tbl.c.source_name == source_name, + tbl.c.target_type == target_type, + tbl.c.target_namespace == target_namespace, + tbl.c.target_name == target_name, + ) + .values(edge_type=edge_type, updated_at=now) + ) + else: + conn.execute( + tbl.insert().values( + source_type=source_type, + source_namespace=source_namespace, + source_name=source_name, + target_type=target_type, + target_namespace=target_namespace, + target_name=target_name, + edge_type=edge_type, + updated_at=now, + ) + ) + + # ── Query methods ── + + def get_events( + self, + namespace: Optional[str] = None, + job_name: Optional[str] = None, + limit: int = 100, + offset: int = 0, + namespaces: Optional[List[str]] = None, + ) -> List[Dict[str, Any]]: + tbl = OL_TABLES["events"] + query = ( + select(tbl).order_by(tbl.c.event_time.desc()).limit(limit).offset(offset) + ) + + if namespace: + query = query.where(tbl.c.job_namespace == namespace) + elif namespaces: + query = query.where(tbl.c.job_namespace.in_(namespaces)) + if job_name: + query = query.where(tbl.c.job_name == job_name) + + with self._engine.connect() as conn: + rows = conn.execute(query).fetchall() + return [dict(row._mapping) for row in rows] + + def get_jobs(self, namespaces: Optional[List[str]] = None) -> List[Dict[str, Any]]: + tbl = OL_TABLES["jobs"] + query = select(tbl).order_by(tbl.c.updated_at.desc()) + if namespaces: + query = query.where(tbl.c.job_namespace.in_(namespaces)) + + with self._engine.connect() as conn: + rows = conn.execute(query).fetchall() + return [dict(row._mapping) for row in rows] + + def get_datasets( + self, namespaces: Optional[List[str]] = None + ) -> List[Dict[str, Any]]: + tbl = OL_TABLES["datasets"] + query = select(tbl).order_by(tbl.c.updated_at.desc()) + if namespaces: + query = query.where(tbl.c.dataset_namespace.in_(namespaces)) + + with self._engine.connect() as conn: + rows = conn.execute(query).fetchall() + return [dict(row._mapping) for row in rows] + + def get_lineage_graph( + self, + node_type: str, + namespace: str, + name: str, + depth: int = 10, + direction: str = "both", + allowed_namespaces: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """ + Get the lineage graph for a node, traversing upstream and/or downstream. + + Returns a dict with 'nodes' and 'edges' suitable for UI rendering. + """ + nodes: Dict[str, Dict[str, Any]] = {} + edges: List[Dict[str, Any]] = [] + + tbl = OL_TABLES["lineage_edges"] + + with self._engine.connect() as conn: + if direction in ("both", "downstream"): + self._traverse( + conn, + tbl, + node_type, + namespace, + name, + depth, + "downstream", + nodes, + edges, + allowed_namespaces, + ) + if direction in ("both", "upstream"): + self._traverse( + conn, + tbl, + node_type, + namespace, + name, + depth, + "upstream", + nodes, + edges, + allowed_namespaces, + ) + + root_key = f"{node_type}:{namespace}:{name}" + if root_key not in nodes: + nodes[root_key] = { + "type": node_type, + "namespace": namespace, + "name": name, + } + + return { + "nodes": list(nodes.values()), + "edges": edges, + } + + def _traverse( + self, + conn, + tbl, + node_type: str, + namespace: str, + name: str, + depth: int, + direction: str, + nodes: Dict[str, Dict], + edges: List[Dict], + allowed_namespaces: Optional[List[str]], + ): + visited = set() + queue = [(node_type, namespace, name, 0)] + + while queue: + n_type, n_ns, n_name, d = queue.pop(0) + key = f"{n_type}:{n_ns}:{n_name}" + + if key in visited or d > depth: + continue + visited.add(key) + + if direction == "downstream": + query = select(tbl).where( + tbl.c.source_type == n_type, + tbl.c.source_namespace == n_ns, + tbl.c.source_name == n_name, + ) + else: + query = select(tbl).where( + tbl.c.target_type == n_type, + tbl.c.target_namespace == n_ns, + tbl.c.target_name == n_name, + ) + + rows = conn.execute(query).fetchall() + for row in rows: + r = row._mapping + src_key = ( + f"{r['source_type']}:{r['source_namespace']}:{r['source_name']}" + ) + tgt_key = ( + f"{r['target_type']}:{r['target_namespace']}:{r['target_name']}" + ) + + if allowed_namespaces: + if ( + r["source_namespace"] not in allowed_namespaces + or r["target_namespace"] not in allowed_namespaces + ): + continue + + edge = { + "source_type": r["source_type"], + "source_namespace": r["source_namespace"], + "source_name": r["source_name"], + "target_type": r["target_type"], + "target_namespace": r["target_namespace"], + "target_name": r["target_name"], + "edge_type": r.get("edge_type"), + } + if edge not in edges: + edges.append(edge) + + for node_key, nt, ns, nn in [ + ( + src_key, + r["source_type"], + r["source_namespace"], + r["source_name"], + ), + ( + tgt_key, + r["target_type"], + r["target_namespace"], + r["target_name"], + ), + ]: + if node_key not in nodes: + nodes[node_key] = { + "type": nt, + "namespace": ns, + "name": nn, + } + + if direction == "downstream": + next_key = tgt_key + next_type = r["target_type"] + next_ns = r["target_namespace"] + next_name = r["target_name"] + else: + next_key = src_key + next_type = r["source_type"] + next_ns = r["source_namespace"] + next_name = r["source_name"] + + if next_key not in visited: + queue.append((next_type, next_ns, next_name, d + 1)) + + def upsert_dataset_symlink( + self, + dataset_namespace: str, + dataset_name: str, + linked_namespace: str, + linked_name: str, + link_type: str = "symlink", + ): + """Store a symlink between two dataset identifiers (bidirectional lookup).""" + now = int(time.time() * 1000) + tbl = OL_TABLES["dataset_symlinks"] + with self._engine.begin() as conn: + existing = conn.execute( + select(tbl).where( + tbl.c.dataset_namespace == dataset_namespace, + tbl.c.dataset_name == dataset_name, + tbl.c.linked_namespace == linked_namespace, + tbl.c.linked_name == linked_name, + ) + ).first() + if existing: + conn.execute( + tbl.update() + .where( + tbl.c.dataset_namespace == dataset_namespace, + tbl.c.dataset_name == dataset_name, + tbl.c.linked_namespace == linked_namespace, + tbl.c.linked_name == linked_name, + ) + .values(link_type=link_type, updated_at=now) + ) + else: + conn.execute( + tbl.insert().values( + dataset_namespace=dataset_namespace, + dataset_name=dataset_name, + linked_namespace=linked_namespace, + linked_name=linked_name, + link_type=link_type, + updated_at=now, + ) + ) + + def get_dataset_aliases(self, namespace: str, name: str) -> List[Dict[str, str]]: + """Get all known aliases for a dataset (both directions).""" + tbl = OL_TABLES["dataset_symlinks"] + results = [] + with self._engine.connect() as conn: + rows = conn.execute( + select(tbl).where( + tbl.c.dataset_namespace == namespace, + tbl.c.dataset_name == name, + ) + ).fetchall() + for r in rows: + results.append( + { + "namespace": r._mapping["linked_namespace"], + "name": r._mapping["linked_name"], + "link_type": r._mapping["link_type"], + } + ) + + rows = conn.execute( + select(tbl).where( + tbl.c.linked_namespace == namespace, + tbl.c.linked_name == name, + ) + ).fetchall() + for r in rows: + results.append( + { + "namespace": r._mapping["dataset_namespace"], + "name": r._mapping["dataset_name"], + "link_type": r._mapping["link_type"], + } + ) + return results + + def find_datasets_by_uri(self, uri: str) -> List[Dict[str, str]]: + """Find all datasets whose dataSource facet contains the given URI.""" + tbl = OL_TABLES["datasets"] + with self._engine.connect() as conn: + rows = conn.execute( + select( + tbl.c.dataset_namespace, tbl.c.dataset_name, tbl.c.facets_json + ).where(tbl.c.facets_json.isnot(None)) + ).fetchall() + + matches = [] + for r in rows: + try: + facets = json.loads(r._mapping["facets_json"]) + ds_uri = facets.get("dataSource", {}).get("uri", "") + if ds_uri and ds_uri == uri: + matches.append( + { + "namespace": r._mapping["dataset_namespace"], + "name": r._mapping["dataset_name"], + } + ) + except (json.JSONDecodeError, AttributeError): + pass + return matches + + def get_all_symlinks(self) -> List[Dict[str, Any]]: + """Get all dataset symlinks.""" + tbl = OL_TABLES["dataset_symlinks"] + with self._engine.connect() as conn: + rows = conn.execute(select(tbl)).fetchall() + return [dict(r._mapping) for r in rows] + + # ── Cleanup methods ── + + def purge_all(self): + """Delete all data from all OpenLineage tables.""" + table_order = [ + "run_io", + "runs", + "lineage_edges", + "dataset_symlinks", + "events", + "datasets", + "jobs", + ] + with self._engine.begin() as conn: + for tbl_name in table_order: + conn.execute(OL_TABLES[tbl_name].delete()) + logger.info("Purged all OpenLineage data") + + def purge_namespace(self, namespace: str): + """Delete all data associated with a specific namespace.""" + with self._engine.begin() as conn: + tbl_runs = OL_TABLES["runs"] + run_ids_q = select(tbl_runs.c.run_id).where( + tbl_runs.c.job_namespace == namespace + ) + run_ids = [r[0] for r in conn.execute(run_ids_q).fetchall()] + if run_ids: + tbl_rio = OL_TABLES["run_io"] + conn.execute(tbl_rio.delete().where(tbl_rio.c.run_id.in_(run_ids))) + conn.execute(tbl_runs.delete().where(tbl_runs.c.run_id.in_(run_ids))) + + tbl_ev = OL_TABLES["events"] + conn.execute(tbl_ev.delete().where(tbl_ev.c.job_namespace == namespace)) + + tbl_edges = OL_TABLES["lineage_edges"] + conn.execute( + tbl_edges.delete().where( + (tbl_edges.c.source_namespace == namespace) + | (tbl_edges.c.target_namespace == namespace) + ) + ) + + tbl_sym = OL_TABLES["dataset_symlinks"] + conn.execute( + tbl_sym.delete().where( + (tbl_sym.c.dataset_namespace == namespace) + | (tbl_sym.c.linked_namespace == namespace) + ) + ) + + tbl_ds = OL_TABLES["datasets"] + conn.execute(tbl_ds.delete().where(tbl_ds.c.dataset_namespace == namespace)) + + tbl_jobs = OL_TABLES["jobs"] + conn.execute(tbl_jobs.delete().where(tbl_jobs.c.job_namespace == namespace)) + + logger.info(f"Purged OpenLineage data for namespace: {namespace}") + + # ── Run query methods ── + + def get_runs( + self, + job_namespace: Optional[str] = None, + job_name: Optional[str] = None, + limit: int = 50, + offset: int = 0, + ) -> List[Dict[str, Any]]: + """Get runs, optionally filtered by job.""" + tbl = OL_TABLES["runs"] + query = ( + select(tbl).order_by(tbl.c.updated_at.desc()).limit(limit).offset(offset) + ) + if job_namespace: + query = query.where(tbl.c.job_namespace == job_namespace) + if job_name: + query = query.where(tbl.c.job_name == job_name) + with self._engine.connect() as conn: + rows = conn.execute(query).fetchall() + return [dict(row._mapping) for row in rows] + + def get_run_detail(self, run_id: str) -> Optional[Dict[str, Any]]: + """Get a single run with its I/O datasets.""" + tbl_runs = OL_TABLES["runs"] + tbl_rio = OL_TABLES["run_io"] + with self._engine.connect() as conn: + run_row = conn.execute( + select(tbl_runs).where(tbl_runs.c.run_id == run_id) + ).first() + if not run_row: + return None + run = dict(run_row._mapping) + + io_rows = conn.execute( + select(tbl_rio).where(tbl_rio.c.run_id == run_id) + ).fetchall() + run["inputs"] = [] + run["outputs"] = [] + for io_row in io_rows: + io = dict(io_row._mapping) + entry = { + "namespace": io["dataset_namespace"], + "name": io["dataset_name"], + "facets": _safe_parse_json(io.get("facets_json")), + } + if io["io_type"] == "INPUT": + run["inputs"].append(entry) + else: + run["outputs"].append(entry) + run["facets"] = _safe_parse_json(run.pop("facets_json", None)) + return run + + def get_all_lineage_edges( + self, namespaces: Optional[List[str]] = None + ) -> List[Dict[str, Any]]: + tbl = OL_TABLES["lineage_edges"] + query = select(tbl) + if namespaces: + query = query.where( + tbl.c.source_namespace.in_(namespaces) + | tbl.c.target_namespace.in_(namespaces) + ) + with self._engine.connect() as conn: + rows = conn.execute(query).fetchall() + return [dict(row._mapping) for row in rows] + + +def _safe_parse_json(val: Optional[str]) -> Optional[Any]: + if not val: + return None + try: + return json.loads(val) + except (json.JSONDecodeError, TypeError): + return None + + +def _parse_timestamp(ts_str: str) -> int: + if not ts_str: + return int(time.time() * 1000) + try: + from datetime import datetime + + if ts_str.endswith("Z"): + ts_str = ts_str[:-1] + "+00:00" + dt = datetime.fromisoformat(ts_str) + return int(dt.timestamp() * 1000) + except (ValueError, TypeError): + return int(time.time() * 1000) diff --git a/sdk/python/feast/permissions/auth/oidc_token_parser.py b/sdk/python/feast/permissions/auth/oidc_token_parser.py index ffff7e7ad34..e2b8aeb79cf 100644 --- a/sdk/python/feast/permissions/auth/oidc_token_parser.py +++ b/sdk/python/feast/permissions/auth/oidc_token_parser.py @@ -1,5 +1,6 @@ import logging import os +import ssl from typing import Optional from unittest.mock import Mock @@ -21,8 +22,13 @@ class OidcTokenParser(TokenParser): """ - A `TokenParser` to use an OIDC server to retrieve the user details. - Server settings are retrieved from the `auth` configurationof the Feature store. + A ``TokenParser`` to use an OIDC server to retrieve the user details. + Server settings are retrieved from the ``auth`` configuration of the Feature store. + + Incoming tokens that contain a ``kubernetes.io`` claim (i.e. Kubernetes + service-account tokens) are handled via a lightweight TokenReview that + extracts only the namespace — no RBAC queries needed. All other tokens + follow the standard OIDC/Keycloak JWKS validation path. """ _auth_config: OidcAuthConfig @@ -30,8 +36,46 @@ class OidcTokenParser(TokenParser): def __init__(self, auth_config: OidcAuthConfig): self._auth_config = auth_config self.oidc_discovery_service = OIDCDiscoveryService( - self._auth_config.auth_discovery_url + self._auth_config.auth_discovery_url, + verify_ssl=self._auth_config.verify_ssl, + ca_cert_path=self._auth_config.ca_cert_path, ) + self._k8s_auth_api = None + self._jwks_client: Optional[PyJWKClient] = None # Initialize it lazily. + + def _get_jwks_client(self) -> PyJWKClient: + """Lazily build and cache a parser-lifetime ``PyJWKClient``. + + A per-request client starts with a cold JWK-set cache, forcing a + full HTTPS fetch of the JWKS document on every authenticated + request. Reusing one client lets PyJWT cache the JWK set for + ``jwks_cache_lifespan_seconds``, which also bounds two staleness + windows: a key the IdP has removed keeps validating, and a + rotation that reuses an existing ``kid`` keeps failing, for at + most that long. Rotations that introduce a new ``kid`` recover + immediately (``PyJWKClient.get_signing_key`` refreshes and + retries once on a cache miss). + """ + if self._jwks_client is None: + ssl_ctx = ssl.create_default_context() + if not self._auth_config.verify_ssl: + ssl_ctx.check_hostname = False + ssl_ctx.verify_mode = ssl.CERT_NONE + elif self._auth_config.ca_cert_path and os.path.exists( + self._auth_config.ca_cert_path + ): + ssl_ctx.load_verify_locations(self._auth_config.ca_cert_path) + self._jwks_client = PyJWKClient( + self.oidc_discovery_service.get_jwks_url(), + headers={"User-agent": "custom-user-agent"}, + ssl_context=ssl_ctx, + # Explicit so upgrades cannot silently change the staleness + # window documented above, and so a hung IdP bounds how long + # a fetch can block the serving path. + lifespan=self._auth_config.jwks_cache_lifespan_seconds, + timeout=self._auth_config.jwks_request_timeout_seconds, + ) + return self._jwks_client async def _validate_token(self, access_token: str): """ @@ -50,79 +94,217 @@ async def _validate_token(self, access_token: str): await oauth_2_scheme(request=request) + @staticmethod + def _extract_username_or_raise_error(data: dict) -> str: + """Extract the username from the decoded JWT, or raise if the token + carries none of the recognised identity claims. + + Human identity claims take precedence: ``preferred_username`` (Keycloak + default), then ``upn`` (Azure AD / Entra ID). Client-credentials + (app-only) tokens carry neither, so the calling application's own + identity is used instead: ``azp`` (Entra ID v2 tokens), ``appid`` + (Entra ID v1 tokens), and finally ``sub``, which every issuer sets. + A claim present with a null or non-string value is skipped rather than + returned, so it never shadows a usable later claim. Because ``sub`` is a + mandatory string JWT claim, the raise below fires only for a malformed + token that provides none of the five as a string. + """ + for claim in ("preferred_username", "upn", "azp", "appid", "sub"): + value = data.get(claim) + if isinstance(value, str): + return value + logger.debug( + f"No usable username claim; token claims present: {list(data.keys())}" + ) + raise AuthenticationError( + "Missing username claim in access token: expected one of " + "preferred_username, upn, azp, appid or sub." + ) + + @staticmethod + def _extract_claim(data: dict, *keys: str, expected_type: type = list): + """Walk *keys* into *data* and return the leaf value, or ``expected_type()`` if any key is missing or the wrong type.""" + node = data + path = ".".join(keys) + for key in keys: + if not isinstance(node, dict) or key not in node: + logger.debug( + f"Missing {key} in access token claim path '{path}'. Defaulting to {expected_type()}." + ) + return expected_type() + node = node[key] + if not isinstance(node, expected_type): + logger.debug( + f"Expected {expected_type.__name__} at '{path}', got {type(node).__name__}. Defaulting to {expected_type()}." + ) + return expected_type() + return node + + @staticmethod + def _is_ssl_error(exc: BaseException) -> bool: + """Walk the exception chain looking for SSL-related errors.""" + current: Optional[BaseException] = exc + while current is not None: + if isinstance(current, ssl.SSLError): + return True + current = current.__cause__ or current.__context__ + return False + + def _decode_token(self, access_token: str) -> dict: + """Fetch the JWKS signing key and decode + verify the JWT. + + Signature and expiry are always verified. Audience and issuer are + verified only when ``audience`` / ``issuer`` are set on + ``OidcAuthConfig``; both default to off, because the claim values a + provider puts in the token can legitimately differ from its discovery + metadata (e.g. Entra ID v1.0 tokens validated against a v2.0 + discovery document). + """ + signing_key = self._get_jwks_client().get_signing_key_from_jwt(access_token) + expected_audience = self._auth_config.audience + expected_issuer = self._auth_config.issuer + return jwt.decode( + access_token, + signing_key.key, + algorithms=["RS256"], + # "account" preserves the historical Keycloak-shaped default; it + # is inert while verify_aud is off. + audience=expected_audience if expected_audience is not None else "account", + issuer=expected_issuer, + options={ + "verify_aud": expected_audience is not None, + "verify_signature": True, + "verify_exp": True, + "verify_iss": expected_issuer is not None, + }, + leeway=10, # accepts tokens generated up to 10 seconds in the past, in case of clock skew + ) + async def user_details_from_access_token(self, access_token: str) -> User: """ - Validate the access token then decode it to extract the user credential and roles. + Validate the access token then decode it to extract the user credentials, + roles, and groups. + + A single unverified decode is performed upfront for lightweight routing: + intra-server communication, Kubernetes SA tokens (identified by the + ``kubernetes.io`` claim), or standard OIDC/Keycloak JWKS validation. Returns: - User: Current user, with associated roles. + User: Current user, with associated roles, groups, or namespaces. Raises: AuthenticationError if any error happens. """ + try: + unverified = jwt.decode(access_token, options={"verify_signature": False}) + except jwt.exceptions.DecodeError as e: + raise AuthenticationError(f"Failed to decode token: {e}") - # check if intra server communication - user = self._get_intra_comm_user(access_token) + user = self._get_intra_comm_user(unverified) if user: return user + if isinstance(unverified.get("kubernetes.io"), dict): + logger.debug("Detected kubernetes.io claim — validating via TokenReview") + try: + return await self._validate_k8s_sa_token_and_extract_namespace( + access_token + ) + except AuthenticationError: + raise + except Exception as e: + logger.error(f"Kubernetes token validation failed: {e}") + raise AuthenticationError(f"Kubernetes token validation failed: {e}") + + # Standard OIDC / Keycloak flow try: await self._validate_token(access_token) logger.debug("Token successfully validated.") except Exception as e: + if self._is_ssl_error(e): + logger.error( + "OIDC provider SSL certificate verification failed. " + "If using a self-signed certificate, set verify_ssl: false " + "or provide a CA certificate via ca_cert_path." + ) logger.error(f"Token validation failed: {e}") raise AuthenticationError(f"Invalid token: {e}") - optional_custom_headers = {"User-agent": "custom-user-agent"} - jwks_client = PyJWKClient( - self.oidc_discovery_service.get_jwks_url(), headers=optional_custom_headers - ) - try: - signing_key = jwks_client.get_signing_key_from_jwt(access_token) - data = jwt.decode( - access_token, - signing_key.key, - algorithms=["RS256"], - audience="account", - options={ - "verify_aud": False, - "verify_signature": True, - "verify_exp": True, - }, - leeway=10, # accepts tokens generated up to 10 seconds in the past, in case of clock skew - ) + data = self._decode_token(access_token) - if "preferred_username" not in data: - raise AuthenticationError( - "Missing preferred_username field in access token." - ) - current_user = data["preferred_username"] - - if "resource_access" not in data: - logger.warning("Missing resource_access field in access token.") - client_id = self._auth_config.client_id - if client_id not in data["resource_access"]: - logger.warning( - f"Missing resource_access.{client_id} field in access token. Defaulting to empty roles." + current_user = self._extract_username_or_raise_error(data) + roles = ( + self._extract_claim( + data, "resource_access", self._auth_config.client_id, "roles" ) - roles = [] - else: - roles = data["resource_access"][client_id]["roles"] + if self._auth_config.client_id + else [] + ) + # Entra ID emits app roles in the top-level `roles` claim instead of + # Keycloak's nested `resource_access..roles`. Merge both + # shapes for every issuer, preserving order and dropping duplicates. + roles = list(dict.fromkeys(roles + self._extract_claim(data, "roles"))) + groups = self._extract_claim(data, "groups") - logger.info(f"Extracted user {current_user} and roles {roles}") - return User(username=current_user, roles=roles) - except jwt.exceptions.InvalidTokenError: + logger.info( + f"Extracted user {current_user} with roles {roles}, groups {groups}" + ) + return User( + username=current_user, + roles=roles, + groups=groups, + ) + except jwt.exceptions.PyJWTError as e: + if self._is_ssl_error(e): + logger.error( + "OIDC JWKS endpoint SSL certificate verification failed. " + "If using a self-signed certificate, set verify_ssl: false " + "or provide a CA certificate via ca_cert_path." + ) logger.exception("Exception while parsing the token:") raise AuthenticationError("Invalid token.") - def _get_intra_comm_user(self, access_token: str) -> Optional[User]: + async def _validate_k8s_sa_token_and_extract_namespace( + self, access_token: str + ) -> User: + """Validate a K8s SA token via TokenReview and extract the namespace. + + Lightweight alternative to full KubernetesTokenParser — only validates + the token and extracts the namespace from the authenticated identity. + No RBAC queries (RoleBindings, ClusterRoleBindings) are performed, + so the server SA needs only ``tokenreviews/create`` permission. + """ + from kubernetes import client, config + + if self._k8s_auth_api is None: + config.load_incluster_config() + self._k8s_auth_api = client.AuthenticationV1Api() + + token_review = client.V1TokenReview( + spec=client.V1TokenReviewSpec(token=access_token) + ) + auth_api: client.AuthenticationV1Api = self._k8s_auth_api + response = auth_api.create_token_review(token_review) + + if not response.status.authenticated: + raise AuthenticationError( + f"Kubernetes token validation failed: {response.status.error}" + ) + + username = getattr(response.status.user, "username", "") or "" + namespaces = [] + if username.startswith("system:serviceaccount:") and username.count(":") >= 3: + namespaces.append(username.split(":")[2]) + + logger.info(f"SA token validated — user: {username}, namespaces: {namespaces}") + return User(username=username, roles=[], groups=[], namespaces=namespaces) + + @staticmethod + def _get_intra_comm_user(decoded_token: dict) -> Optional[User]: intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") if intra_communication_base64: - decoded_token = jwt.decode( - access_token, options={"verify_signature": False} - ) if "preferred_username" in decoded_token: preferred_username: str = decoded_token["preferred_username"] if ( diff --git a/sdk/python/feast/permissions/auth_model.py b/sdk/python/feast/permissions/auth_model.py index 179a8bc0c0e..4648a068bce 100644 --- a/sdk/python/feast/permissions/auth_model.py +++ b/sdk/python/feast/permissions/auth_model.py @@ -1,65 +1,85 @@ -# -------------------------------------------------------------------- -# Extends OIDC client auth model with an optional `token` field. -# Works on Pydantic v2-only. -# -# Accepted credential sets (exactly **one** of): -# 1 pre-issued `token` -# 2 `client_secret` (client-credentials flow) -# 3 `username` + `password` + `client_secret` (ROPG) -# -------------------------------------------------------------------- from __future__ import annotations -from typing import Literal, Optional +from typing import Literal, Optional, Tuple -from pydantic import ConfigDict, model_validator +from pydantic import ConfigDict, Field, model_validator from feast.repo_config import FeastConfigBaseModel +def _check_mutually_exclusive(**groups: Tuple[object, ...]) -> None: + """Validate that at most one named group is configured, and completely. + + Each *group* is a tuple of field values. + A group is **active** only when *all* its values are truthy. + A group is **partial** (error) when *any* but not *all* values are truthy. + At most one active group may exist. + """ + partial = [name for name, vals in groups.items() if any(vals) and not all(vals)] + if partial: + raise ValueError( + f"Incomplete configuration for '{partial[0]}': " + f"configure all of these fields together, or none at all. " + f"Check the documentation for valid credential combinations." + ) + active = [name for name, vals in groups.items() if all(vals)] + if len(active) > 1: + raise ValueError( + f"Only one of [{', '.join(groups)}] may be set, " + f"but got: {', '.join(active)}" + ) + + class AuthConfig(FeastConfigBaseModel): type: Literal["oidc", "kubernetes", "no_auth"] = "no_auth" class OidcAuthConfig(AuthConfig): auth_discovery_url: str - client_id: str + client_id: Optional[str] = None + ui_client_id: Optional[str] = None + verify_ssl: bool = True + ca_cert_path: str = "" + # When set, incoming tokens must carry a matching `aud` / `iss` claim; + # when left unset (the default), the corresponding claim is not verified. + # Set these to the values your IdP puts in the token itself, which may + # differ from the discovery document (e.g. Entra ID v1.0 tokens validated + # against a v2.0 discovery URL). + audience: Optional[str] = None + issuer: Optional[str] = None + # How long the fetched JWK set is reused before the server refetches it. + # This also bounds how long a key the IdP has revoked keeps validating + # tokens, so lower it if your provider rotates or revokes aggressively; + # every reduction costs a corresponding increase in JWKS fetches. + jwks_cache_lifespan_seconds: int = Field(default=300, gt=0) + # Network timeout for the JWKS fetch. This fetch happens inline on the + # request path, so an unresponsive IdP blocks serving for at most this + # long. + jwks_request_timeout_seconds: float = Field(default=10, gt=0) class OidcClientAuthConfig(OidcAuthConfig): - # any **one** of the four fields below is sufficient + auth_discovery_url: Optional[str] = None # type: ignore[assignment] + client_id: Optional[str] = None + username: Optional[str] = None password: Optional[str] = None client_secret: Optional[str] = None - token: Optional[str] = None # pre-issued `token` + token: Optional[str] = None + token_env_var: Optional[str] = None @model_validator(mode="after") - def _validate_credentials(cls, values): - """Enforce exactly one valid credential set.""" - d = values.__dict__ if hasattr(values, "__dict__") else values - - has_user_pass = bool(d.get("username")) and bool(d.get("password")) - has_secret = bool(d.get("client_secret")) - has_token = bool(d.get("token")) - - # 1 static token - if has_token and not (has_user_pass or has_secret): - return values - - # 2 client_credentials - if has_secret and not has_user_pass and not has_token: - return values - - # 3 ROPG - if has_user_pass and has_secret and not has_token: - return values - - raise ValueError( - "Invalid OIDC client auth combination: " - "provide either\n" - " • token\n" - " • client_secret (without username/password)\n" - " • username + password + client_secret" + def _validate_credentials(self): + network = (self.client_secret, self.auth_discovery_url, self.client_id) + if self.username or self.password: + network += (self.username, self.password) + + _check_mutually_exclusive( + token=(self.token,), + token_env_var=(self.token_env_var,), + client_credentials=network, ) + return self class NoAuthConfig(AuthConfig): @@ -67,7 +87,6 @@ class NoAuthConfig(AuthConfig): class KubernetesAuthConfig(AuthConfig): - # Optional user token for users (not service accounts) user_token: Optional[str] = None model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow") diff --git a/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py b/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py index ba02fab8d8d..5b8eb0ffb2f 100644 --- a/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py +++ b/sdk/python/feast/permissions/client/http_auth_requests_wrapper.py @@ -1,5 +1,12 @@ +import logging +import threading +import time +from typing import Optional + import requests from requests import Session +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry from feast.permissions.auth.auth_type import AuthType from feast.permissions.auth_model import ( @@ -7,6 +14,8 @@ ) from feast.permissions.client.client_auth_token import get_auth_token +logger = logging.getLogger(__name__) + class AuthenticatedRequestsSession(Session): def __init__(self, auth_token: str): @@ -14,9 +23,234 @@ def __init__(self, auth_token: str): self.headers.update({"Authorization": f"Bearer {auth_token}"}) -def get_http_auth_requests_session(auth_config: AuthConfig) -> Session: - if auth_config.type == AuthType.NONE.value: - request_session = requests.session() - else: - request_session = AuthenticatedRequestsSession(get_auth_token(auth_config)) - return request_session +class HttpSessionManager: + """ + Manages HTTP sessions with connection pooling for improved performance. + + This class provides: + - Session caching based on auth configuration + - Connection pooling via HTTPAdapter + - Automatic retry with exponential backoff + - Thread-safe session management + - Automatic idle timeout (closes stale sessions) + + Configuration can be customized via feature_store.yaml: + ```yaml + online_store: + type: remote + path: http://localhost:6566 + connection_pool_size: 50 # Max connections in pool + connection_idle_timeout: 300 # Seconds before idle session closes + connection_retries: 3 # Retry count with backoff + ``` + """ + + _session: Optional[Session] = None + _session_auth_type: Optional[str] = None + _session_last_used: Optional[float] = None + _session_config_hash: Optional[int] = None + _lock = threading.Lock() + + # Default configuration (can be overridden via feature_store.yaml) + DEFAULT_POOL_CONNECTIONS = 10 # Number of connection pools to cache + DEFAULT_POOL_MAXSIZE = 50 # Max connections per pool + DEFAULT_MAX_RETRIES = 3 # Number of retries + DEFAULT_BACKOFF_FACTOR = 0.5 # Backoff factor for retries + DEFAULT_MAX_IDLE_SECONDS = 300 # 5 minutes + + # Current active configuration (updated when session is created) + _pool_maxsize: int = DEFAULT_POOL_MAXSIZE + _max_retries: int = DEFAULT_MAX_RETRIES + _max_idle_seconds: int = DEFAULT_MAX_IDLE_SECONDS + + @classmethod + def get_session( + cls, + auth_config: AuthConfig, + pool_maxsize: Optional[int] = None, + max_idle_seconds: Optional[int] = None, + max_retries: Optional[int] = None, + ) -> Session: + """ + Get or create a cached HTTP session with connection pooling. + + The session is cached and reused across requests. A new session + is created if: + - No session exists + - Auth type changes + - Configuration changes + - Session has been idle longer than max_idle_seconds + + Args: + auth_config: Authentication configuration + pool_maxsize: Max connections in pool (default: 50) + max_idle_seconds: Idle timeout in seconds (default: 300, 0 to disable) + max_retries: Number of retries (default: 3) + + Returns: + A requests Session configured with connection pooling + """ + auth_type = auth_config.type if auth_config else AuthType.NONE.value + current_time = time.time() + + # Use provided values or defaults + pool_maxsize = ( + pool_maxsize if pool_maxsize is not None else cls.DEFAULT_POOL_MAXSIZE + ) + max_idle_seconds = ( + max_idle_seconds + if max_idle_seconds is not None + else cls.DEFAULT_MAX_IDLE_SECONDS + ) + max_retries = ( + max_retries if max_retries is not None else cls.DEFAULT_MAX_RETRIES + ) + + # Create config hash to detect configuration changes + config_hash = hash((auth_type, pool_maxsize, max_idle_seconds, max_retries)) + + with cls._lock: + # Check if session has been idle too long (if timeout is enabled) + if ( + cls._session is not None + and cls._session_last_used is not None + and cls._max_idle_seconds > 0 + ): + idle_time = current_time - cls._session_last_used + if idle_time > cls._max_idle_seconds: + logger.debug( + f"Session idle for {idle_time:.1f}s (max: {cls._max_idle_seconds}s), " + "closing stale session" + ) + cls._close_session_internal() + + # Check if we can reuse the cached session (same auth type and config) + if ( + cls._session is not None + and cls._session_auth_type == auth_type + and cls._session_config_hash == config_hash + ): + # For authenticated sessions, update the token in case it expired + if auth_type != AuthType.NONE.value: + try: + auth_token = get_auth_token(auth_config) + cls._session.headers.update( + {"Authorization": f"Bearer {auth_token}"} + ) + except Exception as e: + logger.warning(f"Failed to refresh auth token: {e}") + raise + + # Update last used time + cls._session_last_used = current_time + return cls._session + + # Close existing session if auth type or config changed + if cls._session is not None: + cls._close_session_internal() + + # Create new session with connection pooling + if auth_type == AuthType.NONE.value: + session = requests.Session() + else: + auth_token = get_auth_token(auth_config) + session = AuthenticatedRequestsSession(auth_token) + + # Configure retry strategy with exponential backoff + retry_strategy = Retry( + total=max_retries, + backoff_factor=cls.DEFAULT_BACKOFF_FACTOR, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST", "PUT", "DELETE"], + ) + + # Create HTTP adapter with connection pooling + adapter = HTTPAdapter( + pool_connections=cls.DEFAULT_POOL_CONNECTIONS, + pool_maxsize=pool_maxsize, + max_retries=retry_strategy, + ) + + # Mount adapter for both HTTP and HTTPS + session.mount("http://", adapter) + session.mount("https://", adapter) + + # Set keep-alive header + session.headers.update({"Connection": "keep-alive"}) + + # Cache the session and track configuration + cls._session = session + cls._session_auth_type = auth_type + cls._session_last_used = current_time + cls._session_config_hash = config_hash + cls._pool_maxsize = pool_maxsize + cls._max_retries = max_retries + cls._max_idle_seconds = max_idle_seconds + + idle_timeout_str = ( + f"{max_idle_seconds}s" if max_idle_seconds > 0 else "disabled" + ) + logger.debug( + f"Created new HTTP session with connection pooling: " + f"pool_maxsize={pool_maxsize}, retries={max_retries}, " + f"idle_timeout={idle_timeout_str}" + ) + + return session + + @classmethod + def _close_session_internal(cls) -> None: + """ + Internal method to close session without acquiring lock. + Must be called while holding cls._lock. + """ + if cls._session is not None: + try: + cls._session.close() + except Exception: + pass + cls._session = None + cls._session_auth_type = None + cls._session_last_used = None + cls._session_config_hash = None + + @classmethod + def close_session(cls) -> None: + """ + Close the cached HTTP session and release resources. + + Call this method during application shutdown to clean up. + """ + with cls._lock: + cls._close_session_internal() + logger.debug("HTTP session closed") + + +def get_http_auth_requests_session( + auth_config: AuthConfig, + pool_maxsize: Optional[int] = None, + max_idle_seconds: Optional[int] = None, + max_retries: Optional[int] = None, +) -> Session: + """ + Get an HTTP session with connection pooling and optional authentication. + + This function returns a cached session that reuses TCP/TLS connections + for improved performance. Configuration can be customized via parameters + or defaults are used. + + Args: + auth_config: Authentication configuration + pool_maxsize: Max connections in pool (default: 50) + max_idle_seconds: Idle timeout in seconds (default: 300, 0 to disable) + max_retries: Number of retries (default: 3) + + Returns: + A requests Session configured for the given auth config + """ + return HttpSessionManager.get_session( + auth_config, + pool_maxsize=pool_maxsize, + max_idle_seconds=max_idle_seconds, + max_retries=max_retries, + ) diff --git a/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py b/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py index 30476316c12..bdc6159a2f8 100644 --- a/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py +++ b/sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py @@ -29,4 +29,4 @@ def get_token(self): f"No Auth client manager implemented for the auth type:{self.auth_config.type}" ) - return jwt.encode(payload, "") + return jwt.encode(payload, "", algorithm="none") diff --git a/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py b/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py index 0cee687d08f..ac94b8713ad 100644 --- a/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py +++ b/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py @@ -22,7 +22,7 @@ def get_token(self): "sub": f":::{intra_communication_base64}", # Subject claim } - return jwt.encode(payload, "") + return jwt.encode(payload, "", algorithm="none") # Check if user token is provided in config (for external users) if hasattr(self.auth_config, "user_token") and self.auth_config.user_token: diff --git a/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py index e7db600cab5..37e613dafc1 100644 --- a/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py +++ b/sdk/python/feast/permissions/client/oidc_authentication_client_manager.py @@ -1,5 +1,6 @@ import logging import os +from typing import Optional import jwt import requests @@ -10,6 +11,8 @@ logger = logging.getLogger(__name__) +SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" + class OidcAuthClientManager(AuthenticationClientManager): def __init__(self, auth_config: OidcClientAuthConfig): @@ -17,24 +20,66 @@ def __init__(self, auth_config: OidcClientAuthConfig): def get_token(self): intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64") - # If intra server communication call if intra_communication_base64: payload = { - "preferred_username": f"{intra_communication_base64}", # Subject claim + "preferred_username": f"{intra_communication_base64}", } + return jwt.encode(payload, "", algorithm="none") - return jwt.encode(payload, "") + if self.auth_config.token: + return self.auth_config.token + elif self.auth_config.token_env_var: + env_token = os.getenv(self.auth_config.token_env_var) + if env_token: + return env_token + else: + raise PermissionError( + f"token_env_var='{self.auth_config.token_env_var}' is configured " + f"but the environment variable is not set or is empty." + ) + elif self.auth_config.client_secret: + return self._fetch_token_from_idp() + else: + env_token = os.getenv("FEAST_OIDC_TOKEN") + if env_token: + return env_token - # Fetch the token endpoint from the discovery URL - token_endpoint = OIDCDiscoveryService( - self.auth_config.auth_discovery_url - ).get_token_url() + sa_token = self._read_sa_token() + if sa_token: + return sa_token - # 1) pre-issued JWT supplied in config - if getattr(self.auth_config, "token", None): - return self.auth_config.token + raise PermissionError( + "No OIDC token source configured. Provide one of: " + "'token', 'token_env_var', 'client_secret' (with " + "'auth_discovery_url' and 'client_id'), set the " + "FEAST_OIDC_TOKEN environment variable, or run inside " + "a Kubernetes pod with a mounted service account token." + ) + + @staticmethod + def _read_sa_token() -> Optional[str]: + """Read the Kubernetes service account token from the standard mount path.""" + if os.path.isfile(SA_TOKEN_PATH): + with open(SA_TOKEN_PATH) as f: + token = f.read().strip() + if token: + return token + return None + + def _fetch_token_from_idp(self) -> str: + """Obtain an access token via client_credentials or ROPG flow.""" + if self.auth_config.auth_discovery_url is None: + raise ValueError( + "auth_discovery_url is required for IDP token fetch " + "(client_credentials or ROPG flow)." + ) + discovery = OIDCDiscoveryService( + self.auth_config.auth_discovery_url, + verify_ssl=self.auth_config.verify_ssl, + ca_cert_path=self.auth_config.ca_cert_path, + ) + token_endpoint = discovery.get_token_url() - # 2) client_credentials if self.auth_config.client_secret and not ( self.auth_config.username and self.auth_config.password ): @@ -43,7 +88,6 @@ def get_token(self): "client_id": self.auth_config.client_id, "client_secret": self.auth_config.client_secret, } - # 3) ROPG (username + password + client_secret) else: token_request_body = { "grant_type": "password", @@ -52,11 +96,15 @@ def get_token(self): "username": self.auth_config.username, "password": self.auth_config.password, } - headers = {"Content-Type": "application/x-www-form-urlencoded"} + headers = {"Content-Type": "application/x-www-form-urlencoded"} token_response = requests.post( - token_endpoint, data=token_request_body, headers=headers + token_endpoint, + data=token_request_body, + headers=headers, + verify=discovery._get_verify(), ) + if token_response.status_code == 200: access_token = token_response.json()["access_token"] if not access_token: diff --git a/sdk/python/feast/permissions/enforcer.py b/sdk/python/feast/permissions/enforcer.py index 2cb6608a2d8..4db0241ef5a 100644 --- a/sdk/python/feast/permissions/enforcer.py +++ b/sdk/python/feast/permissions/enforcer.py @@ -43,9 +43,9 @@ def enforce_policy( # If no permissions are defined, deny access to all resources # This is a security measure to prevent unauthorized access logger.warning("No permissions defined - denying access to all resources") - if not filter_only: - raise FeastPermissionError("No permissions defined - access denied") - return [] + raise FeastPermissionError( + "Permissions are not defined - access denied for all resources" + ) _permitted_resources: list[FeastObject] = [] for resource in resources: @@ -71,17 +71,42 @@ def enforce_policy( if evaluator.is_decided(): grant, explanations = evaluator.grant() - if not grant and not filter_only: + if not grant: + if filter_only and p.name_patterns: + continue logger.error(f"Permission denied: {','.join(explanations)}") raise FeastPermissionError(",".join(explanations)) - if grant: - logger.debug( - f"Permission granted for {type(resource).__name__}:{resource.name}" - ) - _permitted_resources.append(resource) + logger.debug( + f"Permission granted for {type(resource).__name__}:{resource.name}" + ) + _permitted_resources.append(resource) break else: - message = f"No permissions defined to manage {actions} on {type(resource)}/{resource.name}." - logger.exception(f"**PERMISSION NOT GRANTED**: {message}") - raise FeastPermissionError(message) + if not filter_only: + message = f"No permissions defined to manage {actions} on {type(resource)}/{resource.name}." + logger.exception(f"**PERMISSION NOT GRANTED**: {message}") + raise FeastPermissionError(message) + else: + # filter_only=True: Check if there are permissions for this resource type + resource_type_permissions = [ + p + for p in permissions + if any(isinstance(resource, t) for t in p.types) # type: ignore + ] + if not resource_type_permissions: + # No permissions exist for this resource type - should raise error + message = f"No permissions defined to manage {actions} on {type(resource)}/{resource.name}." + logger.exception(f"**PERMISSION NOT GRANTED**: {message}") + raise FeastPermissionError(message) + elif not any(p.name_patterns for p in resource_type_permissions): + # Permissions exist for this resource type but no name_patterns - should raise error + message = f"No permissions defined to manage {actions} on {type(resource)}/{resource.name}." + logger.exception(f"**PERMISSION NOT GRANTED**: {message}") + raise FeastPermissionError(message) + else: + # Permissions exist for this resource type with name_patterns - filter out this resource + logger.debug( + f"Filtering out {type(resource).__name__}:{resource.name} - no matching permissions" + ) + continue return _permitted_resources diff --git a/sdk/python/feast/permissions/oidc_service.py b/sdk/python/feast/permissions/oidc_service.py index 73d0ec8f1b7..48edccd3d44 100644 --- a/sdk/python/feast/permissions/oidc_service.py +++ b/sdk/python/feast/permissions/oidc_service.py @@ -1,9 +1,15 @@ +import os + import requests class OIDCDiscoveryService: - def __init__(self, discovery_url: str): + def __init__( + self, discovery_url: str, verify_ssl: bool = True, ca_cert_path: str = "" + ): self.discovery_url = discovery_url + self._verify_ssl = verify_ssl + self._ca_cert_path = ca_cert_path self._discovery_data = None # Initialize it lazily. @property @@ -13,9 +19,16 @@ def discovery_data(self): self._discovery_data = self._fetch_discovery_data() return self._discovery_data + def _get_verify(self): + if not self._verify_ssl: + return False + if self._ca_cert_path and os.path.exists(self._ca_cert_path): + return self._ca_cert_path + return True + def _fetch_discovery_data(self) -> dict: try: - response = requests.get(self.discovery_url) + response = requests.get(self.discovery_url, verify=self._get_verify()) response.raise_for_status() return response.json() except requests.RequestException as e: diff --git a/sdk/python/feast/permissions/permission.py b/sdk/python/feast/permissions/permission.py index 964ca743e71..64af612c704 100644 --- a/sdk/python/feast/permissions/permission.py +++ b/sdk/python/feast/permissions/permission.py @@ -84,7 +84,7 @@ def __init__( def __eq__(self, other): if not isinstance(other, Permission): - raise TypeError("Comparisons should only involve Permission class objects.") + return False if ( self.name != other.name @@ -277,4 +277,5 @@ def get_type_class_from_permission_type(permission_type: str): "VALIDATION_REFERENCE": "feast.saved_dataset.ValidationReference", "SAVED_DATASET": "feast.saved_dataset.SavedDataset", "PERMISSION": "feast.permissions.permission.Permission", + "LABEL_VIEW": "feast.labeling.label_view.LabelView", } diff --git a/sdk/python/feast/permissions/policy.py b/sdk/python/feast/permissions/policy.py index 30cbb73e992..f50ef863e6f 100644 --- a/sdk/python/feast/permissions/policy.py +++ b/sdk/python/feast/permissions/policy.py @@ -80,9 +80,7 @@ def __init__( def __eq__(self, other): if not isinstance(other, RoleBasedPolicy): - raise TypeError( - "Comparisons should only involve RoleBasedPolicy class objects." - ) + return False if sorted(self.roles) != sorted(other.roles): return False @@ -148,9 +146,7 @@ def __init__( def __eq__(self, other): if not isinstance(other, GroupBasedPolicy): - raise TypeError( - "Comparisons should only involve GroupBasedPolicy class objects." - ) + return False if sorted(self.groups) != sorted(other.groups): return False @@ -206,9 +202,7 @@ def __init__( def __eq__(self, other): if not isinstance(other, NamespaceBasedPolicy): - raise TypeError( - "Comparisons should only involve NamespaceBasedPolicy class objects." - ) + return False if sorted(self.namespaces) != sorted(other.namespaces): return False @@ -270,9 +264,7 @@ def __init__( def __eq__(self, other): if not isinstance(other, CombinedGroupNamespacePolicy): - raise TypeError( - "Comparisons should only involve CombinedGroupNamespacePolicy class objects." - ) + return False if sorted(self.groups) != sorted(other.groups) or sorted( self.namespaces diff --git a/sdk/python/feast/permissions/security_manager.py b/sdk/python/feast/permissions/security_manager.py index 1c0bd2aa0c1..6c02922c986 100644 --- a/sdk/python/feast/permissions/security_manager.py +++ b/sdk/python/feast/permissions/security_manager.py @@ -31,6 +31,9 @@ def __init__( self._current_user: ContextVar[Optional[User]] = ContextVar( "current_user", default=None ) + self._current_project: ContextVar[Optional[str]] = ContextVar( + "current_project", default=None + ) def set_current_user(self, current_user: User): """ @@ -38,6 +41,12 @@ def set_current_user(self, current_user: User): """ self._current_user.set(current_user) + def set_current_project(self, project: Optional[str]): + return self._current_project.set(project) + + def reset_current_project(self, token): + self._current_project.reset(token) + @property def current_user(self) -> Optional[User]: """ @@ -51,9 +60,10 @@ def current_user(self) -> Optional[User]: def permissions(self) -> list[Permission]: """ Returns: - list[Permission]: the list of `Permission` configured in the Feast registry. + list[Permission]: the `Permission`s configured for the current request's project. """ - return self._registry.list_permissions(project=self._project) + project = self._current_project.get() or self._project + return self._registry.list_permissions(project=project) def assert_permissions( self, diff --git a/sdk/python/feast/permissions/server/grpc.py b/sdk/python/feast/permissions/server/grpc.py index 9feea47a6cd..805f05ecaae 100644 --- a/sdk/python/feast/permissions/server/grpc.py +++ b/sdk/python/feast/permissions/server/grpc.py @@ -30,4 +30,20 @@ def intercept_service(self, continuation, handler_call_details): logger.debug(f"User is: {current_user}") sm.set_current_user(current_user) - return continuation(handler_call_details) + handler = continuation(handler_call_details) + if sm is None or handler is None or handler.unary_unary is None: + return handler + behavior = handler.unary_unary + + def project_aware_behavior(request, context): + token = sm.set_current_project(getattr(request, "project", None)) + try: + return behavior(request, context) + finally: + sm.reset_current_project(token) + + return grpc.unary_unary_rpc_method_handler( + project_aware_behavior, + request_deserializer=handler.request_deserializer, + response_serializer=handler.response_serializer, + ) diff --git a/sdk/python/feast/permissions/server/rest.py b/sdk/python/feast/permissions/server/rest.py index 05b2cfdd9ac..edf66ff2dec 100644 --- a/sdk/python/feast/permissions/server/rest.py +++ b/sdk/python/feast/permissions/server/rest.py @@ -2,7 +2,7 @@ A module with utility functions to support authorizing the REST servers using the FastAPI framework. """ -from typing import Any +from typing import Any, AsyncIterator, Optional from fastapi import HTTPException from fastapi.requests import Request @@ -13,13 +13,30 @@ from feast.permissions.security_manager import get_security_manager -async def inject_user_details(request: Request) -> Any: +def _extract_project_from_request(request: Request) -> Optional[str]: + """Best-effort project extraction for REST authorization scoping. + + Most registry REST endpoints pass ``project`` as a query parameter. + When absent (e.g. POST body-only), ``grpc_call`` still scopes from the + protobuf request's ``project`` field. """ - A function to extract the authorization token from a user request, extract the user details and propagate them to the - current security manager, if any. + project = request.query_params.get("project") + if project: + return project + return None + + +async def inject_user_details(request: Request) -> AsyncIterator[Any]: + """ + Extract the authorization token from a user request, propagate user details + to the security manager, and scope permissions to the request project when + available (query param ``project``). + + Yields so project context is reset after the request completes. """ sm = get_security_manager() current_user = None + project_token = None if sm is not None: try: auth_manager = get_auth_manager() @@ -38,9 +55,18 @@ async def inject_user_details(request: Request) -> Any: ) sm.set_current_user(current_user) + project = _extract_project_from_request(request) + if project is not None: + project_token = sm.set_current_project(project) + except HTTPException: + raise except Exception: raise HTTPException( status_code=401, detail="Invalid or expired access token" ) - return current_user + try: + yield current_user + finally: + if sm is not None and project_token is not None: + sm.reset_current_project(project_token) diff --git a/sdk/python/feast/project.py b/sdk/python/feast/project.py index b9cbaddcaad..309f6eb76b5 100644 --- a/sdk/python/feast/project.py +++ b/sdk/python/feast/project.py @@ -82,7 +82,7 @@ def __hash__(self) -> int: def __eq__(self, other): if not isinstance(other, Project): - raise TypeError("Comparisons should only involve Project class objects.") + return False if ( self.name != other.name diff --git a/sdk/python/feast/project_metadata.py b/sdk/python/feast/project_metadata.py index 64488a03629..37394a6706b 100644 --- a/sdk/python/feast/project_metadata.py +++ b/sdk/python/feast/project_metadata.py @@ -60,9 +60,7 @@ def __hash__(self) -> int: def __eq__(self, other): if not isinstance(other, ProjectMetadata): - raise TypeError( - "Comparisons should only involve ProjectMetadata class objects." - ) + return False if ( self.project_name != other.project_name diff --git a/sdk/python/feast/proto_json.py b/sdk/python/feast/proto_json.py index 487dc4284f3..91cec0c2b66 100644 --- a/sdk/python/feast/proto_json.py +++ b/sdk/python/feast/proto_json.py @@ -63,6 +63,18 @@ def to_json_object(printer: _Printer, message: ProtoMessage) -> JsonObject: # to JSON. The parse back result will be different from original message. if which is None or which == "null_val": return None + elif which in ("list_val", "set_val"): + # Nested collection: RepeatedValue containing Values + repeated = getattr(message, which) + value: JsonObject = [ + printer._MessageToJsonObject(inner_val) for inner_val in repeated.val + ] + elif which == "zoned_timestamp_val": + # ZonedTimestamp is a message; render it as an ISO 8601 string in its + # stored zone so the result is JSON-serializable (the raw message is not). + from feast.feature_server_utils import _zoned_timestamp_to_str + + value = _zoned_timestamp_to_str(message.zoned_timestamp_val) elif "_list_" in which: value = list(getattr(message, which).val) else: @@ -86,6 +98,19 @@ def from_json_object( if len(value) == 0: # Clear will mark the struct as modified so it will be created even if there are no values message.int64_list_val.Clear() + elif isinstance(value[0], list) or ( + value[0] is None and any(isinstance(v, list) for v in value) + ): + # Nested collection (list of lists). + # Check any() to handle cases where the first element is None + # (empty inner collections round-trip through proto as None). + # Default to list_val since JSON transport loses the + # outer/inner set distinction. + rv = RepeatedValue() + for inner in value: + inner_val = rv.val.add() + from_json_object(parser, inner, inner_val) + message.list_val.CopyFrom(rv) elif isinstance(value[0], bool): message.bool_list_val.val.extend(value) elif isinstance(value[0], str): diff --git a/sdk/python/feast/proto_utils.py b/sdk/python/feast/proto_utils.py new file mode 100644 index 00000000000..a81a9fd6c23 --- /dev/null +++ b/sdk/python/feast/proto_utils.py @@ -0,0 +1,101 @@ +# Copyright 2019 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Utility functions for protobuf serialization of Feast objects. +""" + +from typing import TYPE_CHECKING, Any, Optional, Union + +from google.protobuf.message import Message + +from feast.protos.feast.core.Transformation_pb2 import ( + FeatureTransformationV2 as FeatureTransformationProto, +) +from feast.protos.feast.core.Transformation_pb2 import ( + SubstraitTransformationV2 as SubstraitTransformationProto, +) +from feast.protos.feast.core.Transformation_pb2 import ( + UserDefinedFunctionV2 as UserDefinedFunctionProto, +) + +if TYPE_CHECKING: + from feast.data_source import DataSource + from feast.transformation.mode import TransformationMode + + +def serialize_data_source(source: Optional["DataSource"]) -> Optional[Message]: + """Serialize a data source to proto with class type annotation. + + Args: + source: The data source to serialize, or None. + + Returns: + The serialized proto with data_source_class_type set, or None if source is None. + """ + if source is None: + return None + proto = source.to_proto() + proto.data_source_class_type = ( + f"{source.__class__.__module__}.{source.__class__.__name__}" + ) + return proto + + +def transformation_to_proto( + transformation: Optional[Any], +) -> Optional[FeatureTransformationProto]: + """Convert a transformation to FeatureTransformationProto. + + Args: + transformation: The transformation object with a to_proto() method. + + Returns: + A FeatureTransformationProto wrapping the transformation, or None. + """ + if transformation is None: + return None + + if not hasattr(transformation, "to_proto"): + return None + + transformation_proto = transformation.to_proto() + + if isinstance(transformation_proto, UserDefinedFunctionProto): + return FeatureTransformationProto( + user_defined_function=transformation_proto, + ) + elif isinstance(transformation_proto, SubstraitTransformationProto): + return FeatureTransformationProto( + substrait_transformation=transformation_proto, + ) + return None + + +def mode_to_string(mode: Optional[Union["TransformationMode", str]]) -> str: + """Convert mode to string value. + + Args: + mode: A TransformationMode enum or string, or None. + + Returns: + The string representation of the mode, or empty string if None. + """ + from feast.transformation.mode import TransformationMode + + if mode is None: + return "" + if isinstance(mode, TransformationMode): + return mode.value + return mode diff --git a/sdk/python/feast/protos/feast/core/Aggregation_pb2.py b/sdk/python/feast/protos/feast/core/Aggregation_pb2.py index 922f8f40aa2..44013acd55d 100644 --- a/sdk/python/feast/protos/feast/core/Aggregation_pb2.py +++ b/sdk/python/feast/protos/feast/core/Aggregation_pb2.py @@ -15,7 +15,7 @@ from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66\x65\x61st/core/Aggregation.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\"\x92\x01\n\x0b\x41ggregation\x12\x0e\n\x06\x63olumn\x18\x01 \x01(\t\x12\x10\n\x08\x66unction\x18\x02 \x01(\t\x12.\n\x0btime_window\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x31\n\x0eslide_interval\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationBU\n\x10\x66\x65\x61st.proto.coreB\x10\x41ggregationProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66\x65\x61st/core/Aggregation.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\"\xa0\x01\n\x0b\x41ggregation\x12\x0e\n\x06\x63olumn\x18\x01 \x01(\t\x12\x10\n\x08\x66unction\x18\x02 \x01(\t\x12.\n\x0btime_window\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x31\n\x0eslide_interval\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x0c\n\x04name\x18\x05 \x01(\tBU\n\x10\x66\x65\x61st.proto.coreB\x10\x41ggregationProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -24,5 +24,5 @@ _globals['DESCRIPTOR']._options = None _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\020AggregationProtoZ/github.com/feast-dev/feast/go/protos/feast/core' _globals['_AGGREGATION']._serialized_start=77 - _globals['_AGGREGATION']._serialized_end=223 + _globals['_AGGREGATION']._serialized_end=237 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Aggregation_pb2.pyi b/sdk/python/feast/protos/feast/core/Aggregation_pb2.pyi index ceb8b1f8131..4c6bd7c089c 100644 --- a/sdk/python/feast/protos/feast/core/Aggregation_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/Aggregation_pb2.pyi @@ -22,12 +22,14 @@ class Aggregation(google.protobuf.message.Message): FUNCTION_FIELD_NUMBER: builtins.int TIME_WINDOW_FIELD_NUMBER: builtins.int SLIDE_INTERVAL_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int column: builtins.str function: builtins.str @property def time_window(self) -> google.protobuf.duration_pb2.Duration: ... @property def slide_interval(self) -> google.protobuf.duration_pb2.Duration: ... + name: builtins.str def __init__( self, *, @@ -35,8 +37,9 @@ class Aggregation(google.protobuf.message.Message): function: builtins.str = ..., time_window: google.protobuf.duration_pb2.Duration | None = ..., slide_interval: google.protobuf.duration_pb2.Duration | None = ..., + name: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["slide_interval", b"slide_interval", "time_window", b"time_window"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["column", b"column", "function", b"function", "slide_interval", b"slide_interval", "time_window", b"time_window"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["column", b"column", "function", b"function", "name", b"name", "slide_interval", b"slide_interval", "time_window", b"time_window"]) -> None: ... global___Aggregation = Aggregation diff --git a/sdk/python/feast/protos/feast/core/DataFormat_pb2.py b/sdk/python/feast/protos/feast/core/DataFormat_pb2.py index a3883dcec3b..b90958cb325 100644 --- a/sdk/python/feast/protos/feast/core/DataFormat_pb2.py +++ b/sdk/python/feast/protos/feast/core/DataFormat_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/DataFormat.proto\x12\nfeast.core\"\xb2\x01\n\nFileFormat\x12>\n\x0eparquet_format\x18\x01 \x01(\x0b\x32$.feast.core.FileFormat.ParquetFormatH\x00\x12:\n\x0c\x64\x65lta_format\x18\x02 \x01(\x0b\x32\".feast.core.FileFormat.DeltaFormatH\x00\x1a\x0f\n\rParquetFormat\x1a\r\n\x0b\x44\x65ltaFormatB\x08\n\x06\x66ormat\"\xb7\x02\n\x0cStreamFormat\x12:\n\x0b\x61vro_format\x18\x01 \x01(\x0b\x32#.feast.core.StreamFormat.AvroFormatH\x00\x12<\n\x0cproto_format\x18\x02 \x01(\x0b\x32$.feast.core.StreamFormat.ProtoFormatH\x00\x12:\n\x0bjson_format\x18\x03 \x01(\x0b\x32#.feast.core.StreamFormat.JsonFormatH\x00\x1a!\n\x0bProtoFormat\x12\x12\n\nclass_path\x18\x01 \x01(\t\x1a!\n\nAvroFormat\x12\x13\n\x0bschema_json\x18\x01 \x01(\t\x1a!\n\nJsonFormat\x12\x13\n\x0bschema_json\x18\x01 \x01(\tB\x08\n\x06\x66ormatBT\n\x10\x66\x65\x61st.proto.coreB\x0f\x44\x61taFormatProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/DataFormat.proto\x12\nfeast.core\"\xa8\x01\n\nFileFormat\x12>\n\x0eparquet_format\x18\x01 \x01(\x0b\x32$.feast.core.FileFormat.ParquetFormatH\x00\x12?\n\x0c\x64\x65lta_format\x18\x02 \x01(\x0b\x32#.feast.core.TableFormat.DeltaFormatB\x02\x18\x01H\x00\x1a\x0f\n\rParquetFormatB\x08\n\x06\x66ormat\"\xf9\x05\n\x0bTableFormat\x12?\n\x0eiceberg_format\x18\x01 \x01(\x0b\x32%.feast.core.TableFormat.IcebergFormatH\x00\x12;\n\x0c\x64\x65lta_format\x18\x02 \x01(\x0b\x32#.feast.core.TableFormat.DeltaFormatH\x00\x12\x39\n\x0bhudi_format\x18\x03 \x01(\x0b\x32\".feast.core.TableFormat.HudiFormatH\x00\x1a\xb1\x01\n\rIcebergFormat\x12\x0f\n\x07\x63\x61talog\x18\x01 \x01(\t\x12\x11\n\tnamespace\x18\x02 \x01(\t\x12I\n\nproperties\x18\x03 \x03(\x0b\x32\x35.feast.core.TableFormat.IcebergFormat.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\xa6\x01\n\x0b\x44\x65ltaFormat\x12\x1b\n\x13\x63heckpoint_location\x18\x01 \x01(\t\x12G\n\nproperties\x18\x02 \x03(\x0b\x32\x33.feast.core.TableFormat.DeltaFormat.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\xc9\x01\n\nHudiFormat\x12\x12\n\ntable_type\x18\x01 \x01(\t\x12\x12\n\nrecord_key\x18\x02 \x01(\t\x12\x18\n\x10precombine_field\x18\x03 \x01(\t\x12\x46\n\nproperties\x18\x04 \x03(\x0b\x32\x32.feast.core.TableFormat.HudiFormat.PropertiesEntry\x1a\x31\n\x0fPropertiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x08\n\x06\x66ormat\"\xb7\x02\n\x0cStreamFormat\x12:\n\x0b\x61vro_format\x18\x01 \x01(\x0b\x32#.feast.core.StreamFormat.AvroFormatH\x00\x12<\n\x0cproto_format\x18\x02 \x01(\x0b\x32$.feast.core.StreamFormat.ProtoFormatH\x00\x12:\n\x0bjson_format\x18\x03 \x01(\x0b\x32#.feast.core.StreamFormat.JsonFormatH\x00\x1a!\n\x0bProtoFormat\x12\x12\n\nclass_path\x18\x01 \x01(\t\x1a!\n\nAvroFormat\x12\x13\n\x0bschema_json\x18\x01 \x01(\t\x1a!\n\nJsonFormat\x12\x13\n\x0bschema_json\x18\x01 \x01(\tB\x08\n\x06\x66ormatBT\n\x10\x66\x65\x61st.proto.coreB\x0f\x44\x61taFormatProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -22,18 +22,38 @@ if _descriptor._USE_C_DESCRIPTORS == False: _globals['DESCRIPTOR']._options = None _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\017DataFormatProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_FILEFORMAT'].fields_by_name['delta_format']._options = None + _globals['_FILEFORMAT'].fields_by_name['delta_format']._serialized_options = b'\030\001' + _globals['_TABLEFORMAT_ICEBERGFORMAT_PROPERTIESENTRY']._options = None + _globals['_TABLEFORMAT_ICEBERGFORMAT_PROPERTIESENTRY']._serialized_options = b'8\001' + _globals['_TABLEFORMAT_DELTAFORMAT_PROPERTIESENTRY']._options = None + _globals['_TABLEFORMAT_DELTAFORMAT_PROPERTIESENTRY']._serialized_options = b'8\001' + _globals['_TABLEFORMAT_HUDIFORMAT_PROPERTIESENTRY']._options = None + _globals['_TABLEFORMAT_HUDIFORMAT_PROPERTIESENTRY']._serialized_options = b'8\001' _globals['_FILEFORMAT']._serialized_start=44 - _globals['_FILEFORMAT']._serialized_end=222 - _globals['_FILEFORMAT_PARQUETFORMAT']._serialized_start=182 - _globals['_FILEFORMAT_PARQUETFORMAT']._serialized_end=197 - _globals['_FILEFORMAT_DELTAFORMAT']._serialized_start=199 - _globals['_FILEFORMAT_DELTAFORMAT']._serialized_end=212 - _globals['_STREAMFORMAT']._serialized_start=225 - _globals['_STREAMFORMAT']._serialized_end=536 - _globals['_STREAMFORMAT_PROTOFORMAT']._serialized_start=423 - _globals['_STREAMFORMAT_PROTOFORMAT']._serialized_end=456 - _globals['_STREAMFORMAT_AVROFORMAT']._serialized_start=458 - _globals['_STREAMFORMAT_AVROFORMAT']._serialized_end=491 - _globals['_STREAMFORMAT_JSONFORMAT']._serialized_start=493 - _globals['_STREAMFORMAT_JSONFORMAT']._serialized_end=526 + _globals['_FILEFORMAT']._serialized_end=212 + _globals['_FILEFORMAT_PARQUETFORMAT']._serialized_start=187 + _globals['_FILEFORMAT_PARQUETFORMAT']._serialized_end=202 + _globals['_TABLEFORMAT']._serialized_start=215 + _globals['_TABLEFORMAT']._serialized_end=976 + _globals['_TABLEFORMAT_ICEBERGFORMAT']._serialized_start=416 + _globals['_TABLEFORMAT_ICEBERGFORMAT']._serialized_end=593 + _globals['_TABLEFORMAT_ICEBERGFORMAT_PROPERTIESENTRY']._serialized_start=544 + _globals['_TABLEFORMAT_ICEBERGFORMAT_PROPERTIESENTRY']._serialized_end=593 + _globals['_TABLEFORMAT_DELTAFORMAT']._serialized_start=596 + _globals['_TABLEFORMAT_DELTAFORMAT']._serialized_end=762 + _globals['_TABLEFORMAT_DELTAFORMAT_PROPERTIESENTRY']._serialized_start=544 + _globals['_TABLEFORMAT_DELTAFORMAT_PROPERTIESENTRY']._serialized_end=593 + _globals['_TABLEFORMAT_HUDIFORMAT']._serialized_start=765 + _globals['_TABLEFORMAT_HUDIFORMAT']._serialized_end=966 + _globals['_TABLEFORMAT_HUDIFORMAT_PROPERTIESENTRY']._serialized_start=544 + _globals['_TABLEFORMAT_HUDIFORMAT_PROPERTIESENTRY']._serialized_end=593 + _globals['_STREAMFORMAT']._serialized_start=979 + _globals['_STREAMFORMAT']._serialized_end=1290 + _globals['_STREAMFORMAT_PROTOFORMAT']._serialized_start=1177 + _globals['_STREAMFORMAT_PROTOFORMAT']._serialized_end=1210 + _globals['_STREAMFORMAT_AVROFORMAT']._serialized_start=1212 + _globals['_STREAMFORMAT_AVROFORMAT']._serialized_end=1245 + _globals['_STREAMFORMAT_JSONFORMAT']._serialized_start=1247 + _globals['_STREAMFORMAT_JSONFORMAT']._serialized_end=1280 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/DataFormat_pb2.pyi b/sdk/python/feast/protos/feast/core/DataFormat_pb2.pyi index 1f904e9886a..193fb82a776 100644 --- a/sdk/python/feast/protos/feast/core/DataFormat_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/DataFormat_pb2.pyi @@ -17,7 +17,9 @@ See the License for the specific language governing permissions and limitations under the License. """ import builtins +import collections.abc import google.protobuf.descriptor +import google.protobuf.internal.containers import google.protobuf.message import sys @@ -42,32 +44,174 @@ class FileFormat(google.protobuf.message.Message): self, ) -> None: ... + PARQUET_FORMAT_FIELD_NUMBER: builtins.int + DELTA_FORMAT_FIELD_NUMBER: builtins.int + @property + def parquet_format(self) -> global___FileFormat.ParquetFormat: ... + @property + def delta_format(self) -> global___TableFormat.DeltaFormat: + """Deprecated: Delta Lake is a table format, not a file format. + Use TableFormat.DeltaFormat instead for Delta Lake support. + """ + def __init__( + self, + *, + parquet_format: global___FileFormat.ParquetFormat | None = ..., + delta_format: global___TableFormat.DeltaFormat | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["delta_format", b"delta_format", "format", b"format", "parquet_format", b"parquet_format"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["delta_format", b"delta_format", "format", b"format", "parquet_format", b"parquet_format"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["format", b"format"]) -> typing_extensions.Literal["parquet_format", "delta_format"] | None: ... + +global___FileFormat = FileFormat + +class TableFormat(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class IcebergFormat(google.protobuf.message.Message): + """Defines options for Apache Iceberg table format""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class PropertiesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + CATALOG_FIELD_NUMBER: builtins.int + NAMESPACE_FIELD_NUMBER: builtins.int + PROPERTIES_FIELD_NUMBER: builtins.int + catalog: builtins.str + """Optional catalog name for the Iceberg table""" + namespace: builtins.str + """Optional namespace (schema/database) within the catalog""" + @property + def properties(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Additional properties for Iceberg configuration + Examples: warehouse location, snapshot-id, as-of-timestamp, etc. + """ + def __init__( + self, + *, + catalog: builtins.str = ..., + namespace: builtins.str = ..., + properties: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["catalog", b"catalog", "namespace", b"namespace", "properties", b"properties"]) -> None: ... + class DeltaFormat(google.protobuf.message.Message): - """Defines options for delta data format""" + """Defines options for Delta Lake table format""" DESCRIPTOR: google.protobuf.descriptor.Descriptor + class PropertiesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + CHECKPOINT_LOCATION_FIELD_NUMBER: builtins.int + PROPERTIES_FIELD_NUMBER: builtins.int + checkpoint_location: builtins.str + """Optional checkpoint location for Delta transaction logs""" + @property + def properties(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Additional properties for Delta configuration + Examples: auto-optimize settings, vacuum settings, etc. + """ def __init__( self, + *, + checkpoint_location: builtins.str = ..., + properties: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["checkpoint_location", b"checkpoint_location", "properties", b"properties"]) -> None: ... - PARQUET_FORMAT_FIELD_NUMBER: builtins.int + class HudiFormat(google.protobuf.message.Message): + """Defines options for Apache Hudi table format""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class PropertiesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + TABLE_TYPE_FIELD_NUMBER: builtins.int + RECORD_KEY_FIELD_NUMBER: builtins.int + PRECOMBINE_FIELD_FIELD_NUMBER: builtins.int + PROPERTIES_FIELD_NUMBER: builtins.int + table_type: builtins.str + """Type of Hudi table (COPY_ON_WRITE or MERGE_ON_READ)""" + record_key: builtins.str + """Field(s) that uniquely identify a record""" + precombine_field: builtins.str + """Field used to determine the latest version of a record""" + @property + def properties(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Additional properties for Hudi configuration + Examples: compaction strategy, indexing options, etc. + """ + def __init__( + self, + *, + table_type: builtins.str = ..., + record_key: builtins.str = ..., + precombine_field: builtins.str = ..., + properties: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["precombine_field", b"precombine_field", "properties", b"properties", "record_key", b"record_key", "table_type", b"table_type"]) -> None: ... + + ICEBERG_FORMAT_FIELD_NUMBER: builtins.int DELTA_FORMAT_FIELD_NUMBER: builtins.int + HUDI_FORMAT_FIELD_NUMBER: builtins.int @property - def parquet_format(self) -> global___FileFormat.ParquetFormat: ... + def iceberg_format(self) -> global___TableFormat.IcebergFormat: ... @property - def delta_format(self) -> global___FileFormat.DeltaFormat: ... + def delta_format(self) -> global___TableFormat.DeltaFormat: ... + @property + def hudi_format(self) -> global___TableFormat.HudiFormat: ... def __init__( self, *, - parquet_format: global___FileFormat.ParquetFormat | None = ..., - delta_format: global___FileFormat.DeltaFormat | None = ..., + iceberg_format: global___TableFormat.IcebergFormat | None = ..., + delta_format: global___TableFormat.DeltaFormat | None = ..., + hudi_format: global___TableFormat.HudiFormat | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["delta_format", b"delta_format", "format", b"format", "parquet_format", b"parquet_format"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["delta_format", b"delta_format", "format", b"format", "parquet_format", b"parquet_format"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["format", b"format"]) -> typing_extensions.Literal["parquet_format", "delta_format"] | None: ... + def HasField(self, field_name: typing_extensions.Literal["delta_format", b"delta_format", "format", b"format", "hudi_format", b"hudi_format", "iceberg_format", b"iceberg_format"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["delta_format", b"delta_format", "format", b"format", "hudi_format", b"hudi_format", "iceberg_format", b"iceberg_format"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["format", b"format"]) -> typing_extensions.Literal["iceberg_format", "delta_format", "hudi_format"] | None: ... -global___FileFormat = FileFormat +global___TableFormat = TableFormat class StreamFormat(google.protobuf.message.Message): """Defines the data format encoding features/entity data in data streams""" diff --git a/sdk/python/feast/protos/feast/core/DataSource_pb2.py b/sdk/python/feast/protos/feast/core/DataSource_pb2.py index cb06cca5c10..3ad9ab1807b 100644 --- a/sdk/python/feast/protos/feast/core/DataSource_pb2.py +++ b/sdk/python/feast/protos/feast/core/DataSource_pb2.py @@ -19,7 +19,7 @@ from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/DataSource.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataFormat.proto\x1a\x17\x66\x65\x61st/types/Value.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\"\xd9\x17\n\nDataSource\x12\x0c\n\x04name\x18\x14 \x01(\t\x12\x0f\n\x07project\x18\x15 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x17 \x01(\t\x12.\n\x04tags\x18\x18 \x03(\x0b\x32 .feast.core.DataSource.TagsEntry\x12\r\n\x05owner\x18\x19 \x01(\t\x12/\n\x04type\x18\x01 \x01(\x0e\x32!.feast.core.DataSource.SourceType\x12?\n\rfield_mapping\x18\x02 \x03(\x0b\x32(.feast.core.DataSource.FieldMappingEntry\x12\x17\n\x0ftimestamp_field\x18\x03 \x01(\t\x12\x1d\n\x15\x64\x61te_partition_column\x18\x04 \x01(\t\x12 \n\x18\x63reated_timestamp_column\x18\x05 \x01(\t\x12\x1e\n\x16\x64\x61ta_source_class_type\x18\x11 \x01(\t\x12,\n\x0c\x62\x61tch_source\x18\x1a \x01(\x0b\x32\x16.feast.core.DataSource\x12/\n\x04meta\x18\x32 \x01(\x0b\x32!.feast.core.DataSource.SourceMeta\x12:\n\x0c\x66ile_options\x18\x0b \x01(\x0b\x32\".feast.core.DataSource.FileOptionsH\x00\x12\x42\n\x10\x62igquery_options\x18\x0c \x01(\x0b\x32&.feast.core.DataSource.BigQueryOptionsH\x00\x12<\n\rkafka_options\x18\r \x01(\x0b\x32#.feast.core.DataSource.KafkaOptionsH\x00\x12@\n\x0fkinesis_options\x18\x0e \x01(\x0b\x32%.feast.core.DataSource.KinesisOptionsH\x00\x12\x42\n\x10redshift_options\x18\x0f \x01(\x0b\x32&.feast.core.DataSource.RedshiftOptionsH\x00\x12I\n\x14request_data_options\x18\x12 \x01(\x0b\x32).feast.core.DataSource.RequestDataOptionsH\x00\x12\x44\n\x0e\x63ustom_options\x18\x10 \x01(\x0b\x32*.feast.core.DataSource.CustomSourceOptionsH\x00\x12\x44\n\x11snowflake_options\x18\x13 \x01(\x0b\x32\'.feast.core.DataSource.SnowflakeOptionsH\x00\x12:\n\x0cpush_options\x18\x16 \x01(\x0b\x32\".feast.core.DataSource.PushOptionsH\x00\x12<\n\rspark_options\x18\x1b \x01(\x0b\x32#.feast.core.DataSource.SparkOptionsH\x00\x12<\n\rtrino_options\x18\x1e \x01(\x0b\x32#.feast.core.DataSource.TrinoOptionsH\x00\x12>\n\x0e\x61thena_options\x18# \x01(\x0b\x32$.feast.core.DataSource.AthenaOptionsH\x00\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x33\n\x11\x46ieldMappingEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\xf5\x01\n\nSourceMeta\x12:\n\x16\x65\x61rliestEventTimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x38\n\x14latestEventTimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11\x63reated_timestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a\x65\n\x0b\x46ileOptions\x12+\n\x0b\x66ile_format\x18\x01 \x01(\x0b\x32\x16.feast.core.FileFormat\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x1c\n\x14s3_endpoint_override\x18\x03 \x01(\t\x1a/\n\x0f\x42igQueryOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x1a,\n\x0cTrinoOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x1a\xae\x01\n\x0cKafkaOptions\x12\x1f\n\x17kafka_bootstrap_servers\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x30\n\x0emessage_format\x18\x03 \x01(\x0b\x32\x18.feast.core.StreamFormat\x12<\n\x19watermark_delay_threshold\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x1a\x66\n\x0eKinesisOptions\x12\x0e\n\x06region\x18\x01 \x01(\t\x12\x13\n\x0bstream_name\x18\x02 \x01(\t\x12/\n\rrecord_format\x18\x03 \x01(\x0b\x32\x18.feast.core.StreamFormat\x1aQ\n\x0fRedshiftOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0e\n\x06schema\x18\x03 \x01(\t\x12\x10\n\x08\x64\x61tabase\x18\x04 \x01(\t\x1aT\n\rAthenaOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x10\n\x08\x64\x61tabase\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x61ta_source\x18\x04 \x01(\t\x1aX\n\x10SnowflakeOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0e\n\x06schema\x18\x03 \x01(\t\x12\x10\n\x08\x64\x61tabase\x18\x04 \x01(\tJ\x04\x08\x05\x10\x06\x1au\n\x0cSparkOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x13\n\x0b\x66ile_format\x18\x04 \x01(\t\x12$\n\x1c\x64\x61te_partition_column_format\x18\x05 \x01(\t\x1a,\n\x13\x43ustomSourceOptions\x12\x15\n\rconfiguration\x18\x01 \x01(\x0c\x1a\xf7\x01\n\x12RequestDataOptions\x12Z\n\x11\x64\x65precated_schema\x18\x02 \x03(\x0b\x32?.feast.core.DataSource.RequestDataOptions.DeprecatedSchemaEntry\x12)\n\x06schema\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x1aT\n\x15\x44\x65precatedSchemaEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum:\x02\x38\x01J\x04\x08\x01\x10\x02\x1a\x13\n\x0bPushOptionsJ\x04\x08\x01\x10\x02\"\xf8\x01\n\nSourceType\x12\x0b\n\x07INVALID\x10\x00\x12\x0e\n\nBATCH_FILE\x10\x01\x12\x13\n\x0f\x42\x41TCH_SNOWFLAKE\x10\x08\x12\x12\n\x0e\x42\x41TCH_BIGQUERY\x10\x02\x12\x12\n\x0e\x42\x41TCH_REDSHIFT\x10\x05\x12\x10\n\x0cSTREAM_KAFKA\x10\x03\x12\x12\n\x0eSTREAM_KINESIS\x10\x04\x12\x11\n\rCUSTOM_SOURCE\x10\x06\x12\x12\n\x0eREQUEST_SOURCE\x10\x07\x12\x0f\n\x0bPUSH_SOURCE\x10\t\x12\x0f\n\x0b\x42\x41TCH_TRINO\x10\n\x12\x0f\n\x0b\x42\x41TCH_SPARK\x10\x0b\x12\x10\n\x0c\x42\x41TCH_ATHENA\x10\x0c\x42\t\n\x07optionsJ\x04\x08\x06\x10\x0b\"=\n\x0e\x44\x61taSourceList\x12+\n\x0b\x64\x61tasources\x18\x01 \x03(\x0b\x32\x16.feast.core.DataSourceBT\n\x10\x66\x65\x61st.proto.coreB\x0f\x44\x61taSourceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/DataSource.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataFormat.proto\x1a\x17\x66\x65\x61st/types/Value.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\"\xba\x18\n\nDataSource\x12\x0c\n\x04name\x18\x14 \x01(\t\x12\x0f\n\x07project\x18\x15 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x17 \x01(\t\x12.\n\x04tags\x18\x18 \x03(\x0b\x32 .feast.core.DataSource.TagsEntry\x12\r\n\x05owner\x18\x19 \x01(\t\x12/\n\x04type\x18\x01 \x01(\x0e\x32!.feast.core.DataSource.SourceType\x12?\n\rfield_mapping\x18\x02 \x03(\x0b\x32(.feast.core.DataSource.FieldMappingEntry\x12\x17\n\x0ftimestamp_field\x18\x03 \x01(\t\x12\x1d\n\x15\x64\x61te_partition_column\x18\x04 \x01(\t\x12 \n\x18\x63reated_timestamp_column\x18\x05 \x01(\t\x12\x1c\n\x14timestamp_field_type\x18\x1c \x01(\t\x12\x1e\n\x16\x64\x61ta_source_class_type\x18\x11 \x01(\t\x12,\n\x0c\x62\x61tch_source\x18\x1a \x01(\x0b\x32\x16.feast.core.DataSource\x12/\n\x04meta\x18\x32 \x01(\x0b\x32!.feast.core.DataSource.SourceMeta\x12:\n\x0c\x66ile_options\x18\x0b \x01(\x0b\x32\".feast.core.DataSource.FileOptionsH\x00\x12\x42\n\x10\x62igquery_options\x18\x0c \x01(\x0b\x32&.feast.core.DataSource.BigQueryOptionsH\x00\x12<\n\rkafka_options\x18\r \x01(\x0b\x32#.feast.core.DataSource.KafkaOptionsH\x00\x12@\n\x0fkinesis_options\x18\x0e \x01(\x0b\x32%.feast.core.DataSource.KinesisOptionsH\x00\x12\x42\n\x10redshift_options\x18\x0f \x01(\x0b\x32&.feast.core.DataSource.RedshiftOptionsH\x00\x12I\n\x14request_data_options\x18\x12 \x01(\x0b\x32).feast.core.DataSource.RequestDataOptionsH\x00\x12\x44\n\x0e\x63ustom_options\x18\x10 \x01(\x0b\x32*.feast.core.DataSource.CustomSourceOptionsH\x00\x12\x44\n\x11snowflake_options\x18\x13 \x01(\x0b\x32\'.feast.core.DataSource.SnowflakeOptionsH\x00\x12:\n\x0cpush_options\x18\x16 \x01(\x0b\x32\".feast.core.DataSource.PushOptionsH\x00\x12<\n\rspark_options\x18\x1b \x01(\x0b\x32#.feast.core.DataSource.SparkOptionsH\x00\x12<\n\rtrino_options\x18\x1e \x01(\x0b\x32#.feast.core.DataSource.TrinoOptionsH\x00\x12>\n\x0e\x61thena_options\x18# \x01(\x0b\x32$.feast.core.DataSource.AthenaOptionsH\x00\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x33\n\x11\x46ieldMappingEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\xf5\x01\n\nSourceMeta\x12:\n\x16\x65\x61rliestEventTimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x38\n\x14latestEventTimestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11\x63reated_timestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a\x65\n\x0b\x46ileOptions\x12+\n\x0b\x66ile_format\x18\x01 \x01(\x0b\x32\x16.feast.core.FileFormat\x12\x0b\n\x03uri\x18\x02 \x01(\t\x12\x1c\n\x14s3_endpoint_override\x18\x03 \x01(\t\x1a/\n\x0f\x42igQueryOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x1a,\n\x0cTrinoOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x1a\xae\x01\n\x0cKafkaOptions\x12\x1f\n\x17kafka_bootstrap_servers\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x30\n\x0emessage_format\x18\x03 \x01(\x0b\x32\x18.feast.core.StreamFormat\x12<\n\x19watermark_delay_threshold\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\x1a\x66\n\x0eKinesisOptions\x12\x0e\n\x06region\x18\x01 \x01(\t\x12\x13\n\x0bstream_name\x18\x02 \x01(\t\x12/\n\rrecord_format\x18\x03 \x01(\x0b\x32\x18.feast.core.StreamFormat\x1aQ\n\x0fRedshiftOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0e\n\x06schema\x18\x03 \x01(\t\x12\x10\n\x08\x64\x61tabase\x18\x04 \x01(\t\x1aT\n\rAthenaOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x10\n\x08\x64\x61tabase\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x61ta_source\x18\x04 \x01(\t\x1aX\n\x10SnowflakeOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0e\n\x06schema\x18\x03 \x01(\t\x12\x10\n\x08\x64\x61tabase\x18\x04 \x01(\tJ\x04\x08\x05\x10\x06\x1a\xa4\x01\n\x0cSparkOptions\x12\r\n\x05table\x18\x01 \x01(\t\x12\r\n\x05query\x18\x02 \x01(\t\x12\x0c\n\x04path\x18\x03 \x01(\t\x12\x13\n\x0b\x66ile_format\x18\x04 \x01(\t\x12$\n\x1c\x64\x61te_partition_column_format\x18\x05 \x01(\t\x12-\n\x0ctable_format\x18\x06 \x01(\x0b\x32\x17.feast.core.TableFormat\x1a,\n\x13\x43ustomSourceOptions\x12\x15\n\rconfiguration\x18\x01 \x01(\x0c\x1a\xf7\x01\n\x12RequestDataOptions\x12Z\n\x11\x64\x65precated_schema\x18\x02 \x03(\x0b\x32?.feast.core.DataSource.RequestDataOptions.DeprecatedSchemaEntry\x12)\n\x06schema\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x1aT\n\x15\x44\x65precatedSchemaEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0e\x32\x1b.feast.types.ValueType.Enum:\x02\x38\x01J\x04\x08\x01\x10\x02\x1a\x13\n\x0bPushOptionsJ\x04\x08\x01\x10\x02\"\x8b\x02\n\nSourceType\x12\x0b\n\x07INVALID\x10\x00\x12\x0e\n\nBATCH_FILE\x10\x01\x12\x13\n\x0f\x42\x41TCH_SNOWFLAKE\x10\x08\x12\x12\n\x0e\x42\x41TCH_BIGQUERY\x10\x02\x12\x12\n\x0e\x42\x41TCH_REDSHIFT\x10\x05\x12\x10\n\x0cSTREAM_KAFKA\x10\x03\x12\x12\n\x0eSTREAM_KINESIS\x10\x04\x12\x11\n\rCUSTOM_SOURCE\x10\x06\x12\x12\n\x0eREQUEST_SOURCE\x10\x07\x12\x0f\n\x0bPUSH_SOURCE\x10\t\x12\x0f\n\x0b\x42\x41TCH_TRINO\x10\n\x12\x0f\n\x0b\x42\x41TCH_SPARK\x10\x0b\x12\x10\n\x0c\x42\x41TCH_ATHENA\x10\x0c\x12\x11\n\rBATCH_ICEBERG\x10\rB\t\n\x07optionsJ\x04\x08\x06\x10\x0b\"=\n\x0e\x44\x61taSourceList\x12+\n\x0b\x64\x61tasources\x18\x01 \x03(\x0b\x32\x16.feast.core.DataSourceBT\n\x10\x66\x65\x61st.proto.coreB\x0f\x44\x61taSourceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,41 +34,41 @@ _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._options = None _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._serialized_options = b'8\001' _globals['_DATASOURCE']._serialized_start=189 - _globals['_DATASOURCE']._serialized_end=3222 - _globals['_DATASOURCE_TAGSENTRY']._serialized_start=1436 - _globals['_DATASOURCE_TAGSENTRY']._serialized_end=1479 - _globals['_DATASOURCE_FIELDMAPPINGENTRY']._serialized_start=1481 - _globals['_DATASOURCE_FIELDMAPPINGENTRY']._serialized_end=1532 - _globals['_DATASOURCE_SOURCEMETA']._serialized_start=1535 - _globals['_DATASOURCE_SOURCEMETA']._serialized_end=1780 - _globals['_DATASOURCE_FILEOPTIONS']._serialized_start=1782 - _globals['_DATASOURCE_FILEOPTIONS']._serialized_end=1883 - _globals['_DATASOURCE_BIGQUERYOPTIONS']._serialized_start=1885 - _globals['_DATASOURCE_BIGQUERYOPTIONS']._serialized_end=1932 - _globals['_DATASOURCE_TRINOOPTIONS']._serialized_start=1934 - _globals['_DATASOURCE_TRINOOPTIONS']._serialized_end=1978 - _globals['_DATASOURCE_KAFKAOPTIONS']._serialized_start=1981 - _globals['_DATASOURCE_KAFKAOPTIONS']._serialized_end=2155 - _globals['_DATASOURCE_KINESISOPTIONS']._serialized_start=2157 - _globals['_DATASOURCE_KINESISOPTIONS']._serialized_end=2259 - _globals['_DATASOURCE_REDSHIFTOPTIONS']._serialized_start=2261 - _globals['_DATASOURCE_REDSHIFTOPTIONS']._serialized_end=2342 - _globals['_DATASOURCE_ATHENAOPTIONS']._serialized_start=2344 - _globals['_DATASOURCE_ATHENAOPTIONS']._serialized_end=2428 - _globals['_DATASOURCE_SNOWFLAKEOPTIONS']._serialized_start=2430 - _globals['_DATASOURCE_SNOWFLAKEOPTIONS']._serialized_end=2518 - _globals['_DATASOURCE_SPARKOPTIONS']._serialized_start=2520 - _globals['_DATASOURCE_SPARKOPTIONS']._serialized_end=2637 - _globals['_DATASOURCE_CUSTOMSOURCEOPTIONS']._serialized_start=2639 - _globals['_DATASOURCE_CUSTOMSOURCEOPTIONS']._serialized_end=2683 - _globals['_DATASOURCE_REQUESTDATAOPTIONS']._serialized_start=2686 - _globals['_DATASOURCE_REQUESTDATAOPTIONS']._serialized_end=2933 - _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._serialized_start=2843 - _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._serialized_end=2927 - _globals['_DATASOURCE_PUSHOPTIONS']._serialized_start=2935 - _globals['_DATASOURCE_PUSHOPTIONS']._serialized_end=2954 - _globals['_DATASOURCE_SOURCETYPE']._serialized_start=2957 - _globals['_DATASOURCE_SOURCETYPE']._serialized_end=3205 - _globals['_DATASOURCELIST']._serialized_start=3224 - _globals['_DATASOURCELIST']._serialized_end=3285 + _globals['_DATASOURCE']._serialized_end=3319 + _globals['_DATASOURCE_TAGSENTRY']._serialized_start=1466 + _globals['_DATASOURCE_TAGSENTRY']._serialized_end=1509 + _globals['_DATASOURCE_FIELDMAPPINGENTRY']._serialized_start=1511 + _globals['_DATASOURCE_FIELDMAPPINGENTRY']._serialized_end=1562 + _globals['_DATASOURCE_SOURCEMETA']._serialized_start=1565 + _globals['_DATASOURCE_SOURCEMETA']._serialized_end=1810 + _globals['_DATASOURCE_FILEOPTIONS']._serialized_start=1812 + _globals['_DATASOURCE_FILEOPTIONS']._serialized_end=1913 + _globals['_DATASOURCE_BIGQUERYOPTIONS']._serialized_start=1915 + _globals['_DATASOURCE_BIGQUERYOPTIONS']._serialized_end=1962 + _globals['_DATASOURCE_TRINOOPTIONS']._serialized_start=1964 + _globals['_DATASOURCE_TRINOOPTIONS']._serialized_end=2008 + _globals['_DATASOURCE_KAFKAOPTIONS']._serialized_start=2011 + _globals['_DATASOURCE_KAFKAOPTIONS']._serialized_end=2185 + _globals['_DATASOURCE_KINESISOPTIONS']._serialized_start=2187 + _globals['_DATASOURCE_KINESISOPTIONS']._serialized_end=2289 + _globals['_DATASOURCE_REDSHIFTOPTIONS']._serialized_start=2291 + _globals['_DATASOURCE_REDSHIFTOPTIONS']._serialized_end=2372 + _globals['_DATASOURCE_ATHENAOPTIONS']._serialized_start=2374 + _globals['_DATASOURCE_ATHENAOPTIONS']._serialized_end=2458 + _globals['_DATASOURCE_SNOWFLAKEOPTIONS']._serialized_start=2460 + _globals['_DATASOURCE_SNOWFLAKEOPTIONS']._serialized_end=2548 + _globals['_DATASOURCE_SPARKOPTIONS']._serialized_start=2551 + _globals['_DATASOURCE_SPARKOPTIONS']._serialized_end=2715 + _globals['_DATASOURCE_CUSTOMSOURCEOPTIONS']._serialized_start=2717 + _globals['_DATASOURCE_CUSTOMSOURCEOPTIONS']._serialized_end=2761 + _globals['_DATASOURCE_REQUESTDATAOPTIONS']._serialized_start=2764 + _globals['_DATASOURCE_REQUESTDATAOPTIONS']._serialized_end=3011 + _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._serialized_start=2921 + _globals['_DATASOURCE_REQUESTDATAOPTIONS_DEPRECATEDSCHEMAENTRY']._serialized_end=3005 + _globals['_DATASOURCE_PUSHOPTIONS']._serialized_start=3013 + _globals['_DATASOURCE_PUSHOPTIONS']._serialized_end=3032 + _globals['_DATASOURCE_SOURCETYPE']._serialized_start=3035 + _globals['_DATASOURCE_SOURCETYPE']._serialized_end=3302 + _globals['_DATASOURCELIST']._serialized_start=3321 + _globals['_DATASOURCELIST']._serialized_end=3382 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/DataSource_pb2.pyi b/sdk/python/feast/protos/feast/core/DataSource_pb2.pyi index 668d83525cf..0b77320af32 100644 --- a/sdk/python/feast/protos/feast/core/DataSource_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/DataSource_pb2.pyi @@ -39,7 +39,7 @@ DESCRIPTOR: google.protobuf.descriptor.FileDescriptor class DataSource(google.protobuf.message.Message): """Defines a Data Source that can be used source Feature data - Next available id: 28 + Next available id: 29 """ DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -63,10 +63,11 @@ class DataSource(google.protobuf.message.Message): BATCH_TRINO: DataSource._SourceType.ValueType # 10 BATCH_SPARK: DataSource._SourceType.ValueType # 11 BATCH_ATHENA: DataSource._SourceType.ValueType # 12 + BATCH_ICEBERG: DataSource._SourceType.ValueType # 13 class SourceType(_SourceType, metaclass=_SourceTypeEnumTypeWrapper): """Type of Data Source. - Next available id: 12 + Next available id: 14 """ INVALID: DataSource.SourceType.ValueType # 0 @@ -82,6 +83,7 @@ class DataSource(google.protobuf.message.Message): BATCH_TRINO: DataSource.SourceType.ValueType # 10 BATCH_SPARK: DataSource.SourceType.ValueType # 11 BATCH_ATHENA: DataSource.SourceType.ValueType # 12 + BATCH_ICEBERG: DataSource.SourceType.ValueType # 13 class TagsEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -369,6 +371,7 @@ class DataSource(google.protobuf.message.Message): PATH_FIELD_NUMBER: builtins.int FILE_FORMAT_FIELD_NUMBER: builtins.int DATE_PARTITION_COLUMN_FORMAT_FIELD_NUMBER: builtins.int + TABLE_FORMAT_FIELD_NUMBER: builtins.int table: builtins.str """Table name""" query: builtins.str @@ -379,6 +382,9 @@ class DataSource(google.protobuf.message.Message): """Format of files at `path` (e.g. parquet, avro, etc)""" date_partition_column_format: builtins.str """Date Format of date partition column (e.g. %Y-%m-%d)""" + @property + def table_format(self) -> feast.core.DataFormat_pb2.TableFormat: + """Table Format (e.g. iceberg, delta, hudi)""" def __init__( self, *, @@ -387,8 +393,10 @@ class DataSource(google.protobuf.message.Message): path: builtins.str = ..., file_format: builtins.str = ..., date_partition_column_format: builtins.str = ..., + table_format: feast.core.DataFormat_pb2.TableFormat | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["date_partition_column_format", b"date_partition_column_format", "file_format", b"file_format", "path", b"path", "query", b"query", "table", b"table"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["table_format", b"table_format"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["date_partition_column_format", b"date_partition_column_format", "file_format", b"file_format", "path", b"path", "query", b"query", "table", b"table", "table_format", b"table_format"]) -> None: ... class CustomSourceOptions(google.protobuf.message.Message): """Defines configuration for custom third-party data sources.""" @@ -463,6 +471,7 @@ class DataSource(google.protobuf.message.Message): TIMESTAMP_FIELD_FIELD_NUMBER: builtins.int DATE_PARTITION_COLUMN_FIELD_NUMBER: builtins.int CREATED_TIMESTAMP_COLUMN_FIELD_NUMBER: builtins.int + TIMESTAMP_FIELD_TYPE_FIELD_NUMBER: builtins.int DATA_SOURCE_CLASS_TYPE_FIELD_NUMBER: builtins.int BATCH_SOURCE_FIELD_NUMBER: builtins.int META_FIELD_NUMBER: builtins.int @@ -500,6 +509,10 @@ class DataSource(google.protobuf.message.Message): """ created_timestamp_column: builtins.str """Must specify creation timestamp column name""" + timestamp_field_type: builtins.str + """(Optional) Type of the timestamp_field column ("TIMESTAMP" or "DATE"). + When set to "DATE", SQL generation uses date-only comparisons. + """ data_source_class_type: builtins.str """This is an internal field that is represents the python class for the data source object a proto object represents. This should be set by feast, and not by users. @@ -548,6 +561,7 @@ class DataSource(google.protobuf.message.Message): timestamp_field: builtins.str = ..., date_partition_column: builtins.str = ..., created_timestamp_column: builtins.str = ..., + timestamp_field_type: builtins.str = ..., data_source_class_type: builtins.str = ..., batch_source: global___DataSource | None = ..., meta: global___DataSource.SourceMeta | None = ..., @@ -565,7 +579,7 @@ class DataSource(google.protobuf.message.Message): athena_options: global___DataSource.AthenaOptions | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["athena_options", b"athena_options", "batch_source", b"batch_source", "bigquery_options", b"bigquery_options", "custom_options", b"custom_options", "file_options", b"file_options", "kafka_options", b"kafka_options", "kinesis_options", b"kinesis_options", "meta", b"meta", "options", b"options", "push_options", b"push_options", "redshift_options", b"redshift_options", "request_data_options", b"request_data_options", "snowflake_options", b"snowflake_options", "spark_options", b"spark_options", "trino_options", b"trino_options"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["athena_options", b"athena_options", "batch_source", b"batch_source", "bigquery_options", b"bigquery_options", "created_timestamp_column", b"created_timestamp_column", "custom_options", b"custom_options", "data_source_class_type", b"data_source_class_type", "date_partition_column", b"date_partition_column", "description", b"description", "field_mapping", b"field_mapping", "file_options", b"file_options", "kafka_options", b"kafka_options", "kinesis_options", b"kinesis_options", "meta", b"meta", "name", b"name", "options", b"options", "owner", b"owner", "project", b"project", "push_options", b"push_options", "redshift_options", b"redshift_options", "request_data_options", b"request_data_options", "snowflake_options", b"snowflake_options", "spark_options", b"spark_options", "tags", b"tags", "timestamp_field", b"timestamp_field", "trino_options", b"trino_options", "type", b"type"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["athena_options", b"athena_options", "batch_source", b"batch_source", "bigquery_options", b"bigquery_options", "created_timestamp_column", b"created_timestamp_column", "custom_options", b"custom_options", "data_source_class_type", b"data_source_class_type", "date_partition_column", b"date_partition_column", "description", b"description", "field_mapping", b"field_mapping", "file_options", b"file_options", "kafka_options", b"kafka_options", "kinesis_options", b"kinesis_options", "meta", b"meta", "name", b"name", "options", b"options", "owner", b"owner", "project", b"project", "push_options", b"push_options", "redshift_options", b"redshift_options", "request_data_options", b"request_data_options", "snowflake_options", b"snowflake_options", "spark_options", b"spark_options", "tags", b"tags", "timestamp_field", b"timestamp_field", "timestamp_field_type", b"timestamp_field_type", "trino_options", b"trino_options", "type", b"type"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["options", b"options"]) -> typing_extensions.Literal["file_options", "bigquery_options", "kafka_options", "kinesis_options", "redshift_options", "request_data_options", "custom_options", "snowflake_options", "push_options", "spark_options", "trino_options", "athena_options"] | None: ... global___DataSource = DataSource diff --git a/sdk/python/feast/protos/feast/core/FeatureService_pb2.py b/sdk/python/feast/protos/feast/core/FeatureService_pb2.py index 7ef36079691..24940b4ca33 100644 --- a/sdk/python/feast/protos/feast/core/FeatureService_pb2.py +++ b/sdk/python/feast/protos/feast/core/FeatureService_pb2.py @@ -16,7 +16,7 @@ from feast.protos.feast.core import FeatureViewProjection_pb2 as feast_dot_core_dot_FeatureViewProjection__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x66\x65\x61st/core/FeatureService.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&feast/core/FeatureViewProjection.proto\"l\n\x0e\x46\x65\x61tureService\x12,\n\x04spec\x18\x01 \x01(\x0b\x32\x1e.feast.core.FeatureServiceSpec\x12,\n\x04meta\x18\x02 \x01(\x0b\x32\x1e.feast.core.FeatureServiceMeta\"\xa4\x02\n\x12\x46\x65\x61tureServiceSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x33\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32!.feast.core.FeatureViewProjection\x12\x36\n\x04tags\x18\x04 \x03(\x0b\x32(.feast.core.FeatureServiceSpec.TagsEntry\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\r\n\x05owner\x18\x06 \x01(\t\x12\x31\n\x0elogging_config\x18\x07 \x01(\x0b\x32\x19.feast.core.LoggingConfig\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x87\x01\n\x12\x46\x65\x61tureServiceMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xd1\x08\n\rLoggingConfig\x12\x13\n\x0bsample_rate\x18\x01 \x01(\x02\x12\x45\n\x10\x66ile_destination\x18\x03 \x01(\x0b\x32).feast.core.LoggingConfig.FileDestinationH\x00\x12M\n\x14\x62igquery_destination\x18\x04 \x01(\x0b\x32-.feast.core.LoggingConfig.BigQueryDestinationH\x00\x12M\n\x14redshift_destination\x18\x05 \x01(\x0b\x32-.feast.core.LoggingConfig.RedshiftDestinationH\x00\x12O\n\x15snowflake_destination\x18\x06 \x01(\x0b\x32..feast.core.LoggingConfig.SnowflakeDestinationH\x00\x12I\n\x12\x63ustom_destination\x18\x07 \x01(\x0b\x32+.feast.core.LoggingConfig.CustomDestinationH\x00\x12I\n\x12\x61thena_destination\x18\x08 \x01(\x0b\x32+.feast.core.LoggingConfig.AthenaDestinationH\x00\x12`\n\x1e\x63ouchbase_columnar_destination\x18\t \x01(\x0b\x32\x36.feast.core.LoggingConfig.CouchbaseColumnarDestinationH\x00\x1aS\n\x0f\x46ileDestination\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x1c\n\x14s3_endpoint_override\x18\x02 \x01(\t\x12\x14\n\x0cpartition_by\x18\x03 \x03(\t\x1a(\n\x13\x42igQueryDestination\x12\x11\n\ttable_ref\x18\x01 \x01(\t\x1a)\n\x13RedshiftDestination\x12\x12\n\ntable_name\x18\x01 \x01(\t\x1a\'\n\x11\x41thenaDestination\x12\x12\n\ntable_name\x18\x01 \x01(\t\x1a*\n\x14SnowflakeDestination\x12\x12\n\ntable_name\x18\x01 \x01(\t\x1a\x99\x01\n\x11\x43ustomDestination\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12G\n\x06\x63onfig\x18\x02 \x03(\x0b\x32\x37.feast.core.LoggingConfig.CustomDestination.ConfigEntry\x1a-\n\x0b\x43onfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aS\n\x1c\x43ouchbaseColumnarDestination\x12\x10\n\x08\x64\x61tabase\x18\x01 \x01(\t\x12\r\n\x05scope\x18\x02 \x01(\t\x12\x12\n\ncollection\x18\x03 \x01(\tB\r\n\x0b\x64\x65stination\"I\n\x12\x46\x65\x61tureServiceList\x12\x33\n\x0f\x66\x65\x61tureservices\x18\x01 \x03(\x0b\x32\x1a.feast.core.FeatureServiceBX\n\x10\x66\x65\x61st.proto.coreB\x13\x46\x65\x61tureServiceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x66\x65\x61st/core/FeatureService.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&feast/core/FeatureViewProjection.proto\"l\n\x0e\x46\x65\x61tureService\x12,\n\x04spec\x18\x01 \x01(\x0b\x32\x1e.feast.core.FeatureServiceSpec\x12,\n\x04meta\x18\x02 \x01(\x0b\x32\x1e.feast.core.FeatureServiceMeta\"\xbf\x02\n\x12\x46\x65\x61tureServiceSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x33\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32!.feast.core.FeatureViewProjection\x12\x36\n\x04tags\x18\x04 \x03(\x0b\x32(.feast.core.FeatureServiceSpec.TagsEntry\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\r\n\x05owner\x18\x06 \x01(\t\x12\x31\n\x0elogging_config\x18\x07 \x01(\x0b\x32\x19.feast.core.LoggingConfig\x12\x19\n\x11precompute_online\x18\x08 \x01(\x08\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x87\x01\n\x12\x46\x65\x61tureServiceMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xd1\x08\n\rLoggingConfig\x12\x13\n\x0bsample_rate\x18\x01 \x01(\x02\x12\x45\n\x10\x66ile_destination\x18\x03 \x01(\x0b\x32).feast.core.LoggingConfig.FileDestinationH\x00\x12M\n\x14\x62igquery_destination\x18\x04 \x01(\x0b\x32-.feast.core.LoggingConfig.BigQueryDestinationH\x00\x12M\n\x14redshift_destination\x18\x05 \x01(\x0b\x32-.feast.core.LoggingConfig.RedshiftDestinationH\x00\x12O\n\x15snowflake_destination\x18\x06 \x01(\x0b\x32..feast.core.LoggingConfig.SnowflakeDestinationH\x00\x12I\n\x12\x63ustom_destination\x18\x07 \x01(\x0b\x32+.feast.core.LoggingConfig.CustomDestinationH\x00\x12I\n\x12\x61thena_destination\x18\x08 \x01(\x0b\x32+.feast.core.LoggingConfig.AthenaDestinationH\x00\x12`\n\x1e\x63ouchbase_columnar_destination\x18\t \x01(\x0b\x32\x36.feast.core.LoggingConfig.CouchbaseColumnarDestinationH\x00\x1aS\n\x0f\x46ileDestination\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x1c\n\x14s3_endpoint_override\x18\x02 \x01(\t\x12\x14\n\x0cpartition_by\x18\x03 \x03(\t\x1a(\n\x13\x42igQueryDestination\x12\x11\n\ttable_ref\x18\x01 \x01(\t\x1a)\n\x13RedshiftDestination\x12\x12\n\ntable_name\x18\x01 \x01(\t\x1a\'\n\x11\x41thenaDestination\x12\x12\n\ntable_name\x18\x01 \x01(\t\x1a*\n\x14SnowflakeDestination\x12\x12\n\ntable_name\x18\x01 \x01(\t\x1a\x99\x01\n\x11\x43ustomDestination\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12G\n\x06\x63onfig\x18\x02 \x03(\x0b\x32\x37.feast.core.LoggingConfig.CustomDestination.ConfigEntry\x1a-\n\x0b\x43onfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aS\n\x1c\x43ouchbaseColumnarDestination\x12\x10\n\x08\x64\x61tabase\x18\x01 \x01(\t\x12\r\n\x05scope\x18\x02 \x01(\t\x12\x12\n\ncollection\x18\x03 \x01(\tB\r\n\x0b\x64\x65stination\"I\n\x12\x46\x65\x61tureServiceList\x12\x33\n\x0f\x66\x65\x61tureservices\x18\x01 \x03(\x0b\x32\x1a.feast.core.FeatureServiceBX\n\x10\x66\x65\x61st.proto.coreB\x13\x46\x65\x61tureServiceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -31,29 +31,29 @@ _globals['_FEATURESERVICE']._serialized_start=120 _globals['_FEATURESERVICE']._serialized_end=228 _globals['_FEATURESERVICESPEC']._serialized_start=231 - _globals['_FEATURESERVICESPEC']._serialized_end=523 - _globals['_FEATURESERVICESPEC_TAGSENTRY']._serialized_start=480 - _globals['_FEATURESERVICESPEC_TAGSENTRY']._serialized_end=523 - _globals['_FEATURESERVICEMETA']._serialized_start=526 - _globals['_FEATURESERVICEMETA']._serialized_end=661 - _globals['_LOGGINGCONFIG']._serialized_start=664 - _globals['_LOGGINGCONFIG']._serialized_end=1769 - _globals['_LOGGINGCONFIG_FILEDESTINATION']._serialized_start=1260 - _globals['_LOGGINGCONFIG_FILEDESTINATION']._serialized_end=1343 - _globals['_LOGGINGCONFIG_BIGQUERYDESTINATION']._serialized_start=1345 - _globals['_LOGGINGCONFIG_BIGQUERYDESTINATION']._serialized_end=1385 - _globals['_LOGGINGCONFIG_REDSHIFTDESTINATION']._serialized_start=1387 - _globals['_LOGGINGCONFIG_REDSHIFTDESTINATION']._serialized_end=1428 - _globals['_LOGGINGCONFIG_ATHENADESTINATION']._serialized_start=1430 - _globals['_LOGGINGCONFIG_ATHENADESTINATION']._serialized_end=1469 - _globals['_LOGGINGCONFIG_SNOWFLAKEDESTINATION']._serialized_start=1471 - _globals['_LOGGINGCONFIG_SNOWFLAKEDESTINATION']._serialized_end=1513 - _globals['_LOGGINGCONFIG_CUSTOMDESTINATION']._serialized_start=1516 - _globals['_LOGGINGCONFIG_CUSTOMDESTINATION']._serialized_end=1669 - _globals['_LOGGINGCONFIG_CUSTOMDESTINATION_CONFIGENTRY']._serialized_start=1624 - _globals['_LOGGINGCONFIG_CUSTOMDESTINATION_CONFIGENTRY']._serialized_end=1669 - _globals['_LOGGINGCONFIG_COUCHBASECOLUMNARDESTINATION']._serialized_start=1671 - _globals['_LOGGINGCONFIG_COUCHBASECOLUMNARDESTINATION']._serialized_end=1754 - _globals['_FEATURESERVICELIST']._serialized_start=1771 - _globals['_FEATURESERVICELIST']._serialized_end=1844 + _globals['_FEATURESERVICESPEC']._serialized_end=550 + _globals['_FEATURESERVICESPEC_TAGSENTRY']._serialized_start=507 + _globals['_FEATURESERVICESPEC_TAGSENTRY']._serialized_end=550 + _globals['_FEATURESERVICEMETA']._serialized_start=553 + _globals['_FEATURESERVICEMETA']._serialized_end=688 + _globals['_LOGGINGCONFIG']._serialized_start=691 + _globals['_LOGGINGCONFIG']._serialized_end=1796 + _globals['_LOGGINGCONFIG_FILEDESTINATION']._serialized_start=1287 + _globals['_LOGGINGCONFIG_FILEDESTINATION']._serialized_end=1370 + _globals['_LOGGINGCONFIG_BIGQUERYDESTINATION']._serialized_start=1372 + _globals['_LOGGINGCONFIG_BIGQUERYDESTINATION']._serialized_end=1412 + _globals['_LOGGINGCONFIG_REDSHIFTDESTINATION']._serialized_start=1414 + _globals['_LOGGINGCONFIG_REDSHIFTDESTINATION']._serialized_end=1455 + _globals['_LOGGINGCONFIG_ATHENADESTINATION']._serialized_start=1457 + _globals['_LOGGINGCONFIG_ATHENADESTINATION']._serialized_end=1496 + _globals['_LOGGINGCONFIG_SNOWFLAKEDESTINATION']._serialized_start=1498 + _globals['_LOGGINGCONFIG_SNOWFLAKEDESTINATION']._serialized_end=1540 + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION']._serialized_start=1543 + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION']._serialized_end=1696 + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION_CONFIGENTRY']._serialized_start=1651 + _globals['_LOGGINGCONFIG_CUSTOMDESTINATION_CONFIGENTRY']._serialized_end=1696 + _globals['_LOGGINGCONFIG_COUCHBASECOLUMNARDESTINATION']._serialized_start=1698 + _globals['_LOGGINGCONFIG_COUCHBASECOLUMNARDESTINATION']._serialized_end=1781 + _globals['_FEATURESERVICELIST']._serialized_start=1798 + _globals['_FEATURESERVICELIST']._serialized_end=1871 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi index 6d5879e52cb..a3e6158ad91 100644 --- a/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/FeatureService_pb2.pyi @@ -65,6 +65,7 @@ class FeatureServiceSpec(google.protobuf.message.Message): DESCRIPTION_FIELD_NUMBER: builtins.int OWNER_FIELD_NUMBER: builtins.int LOGGING_CONFIG_FIELD_NUMBER: builtins.int + PRECOMPUTE_ONLINE_FIELD_NUMBER: builtins.int name: builtins.str """Name of the Feature Service. Must be unique. Not updated.""" project: builtins.str @@ -84,6 +85,10 @@ class FeatureServiceSpec(google.protobuf.message.Message): @property def logging_config(self) -> global___LoggingConfig: """(optional) if provided logging will be enabled for this feature service.""" + precompute_online: builtins.bool + """When true, a pre-computed feature vector is maintained per entity for this + service, enabling single-read online retrieval instead of per-feature-view reads. + """ def __init__( self, *, @@ -94,9 +99,10 @@ class FeatureServiceSpec(google.protobuf.message.Message): description: builtins.str = ..., owner: builtins.str = ..., logging_config: global___LoggingConfig | None = ..., + precompute_online: builtins.bool = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["logging_config", b"logging_config"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "features", b"features", "logging_config", b"logging_config", "name", b"name", "owner", b"owner", "project", b"project", "tags", b"tags"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "features", b"features", "logging_config", b"logging_config", "name", b"name", "owner", b"owner", "precompute_online", b"precompute_online", "project", b"project", "tags", b"tags"]) -> None: ... global___FeatureServiceSpec = FeatureServiceSpec diff --git a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py index b47d4fe392f..67839983d81 100644 --- a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py +++ b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.py @@ -16,7 +16,7 @@ from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&feast/core/FeatureViewProjection.proto\x12\nfeast.core\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\"\xba\x03\n\x15\x46\x65\x61tureViewProjection\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x1f\n\x17\x66\x65\x61ture_view_name_alias\x18\x03 \x01(\t\x12\x32\n\x0f\x66\x65\x61ture_columns\x18\x02 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12G\n\x0cjoin_key_map\x18\x04 \x03(\x0b\x32\x31.feast.core.FeatureViewProjection.JoinKeyMapEntry\x12\x17\n\x0ftimestamp_field\x18\x05 \x01(\t\x12\x1d\n\x15\x64\x61te_partition_column\x18\x06 \x01(\t\x12 \n\x18\x63reated_timestamp_column\x18\x07 \x01(\t\x12,\n\x0c\x62\x61tch_source\x18\x08 \x01(\x0b\x32\x16.feast.core.DataSource\x12-\n\rstream_source\x18\t \x01(\x0b\x32\x16.feast.core.DataSource\x1a\x31\n\x0fJoinKeyMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42Z\n\x10\x66\x65\x61st.proto.coreB\x15\x46\x65\x61tureReferenceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&feast/core/FeatureViewProjection.proto\x12\nfeast.core\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\"\xf7\x03\n\x15\x46\x65\x61tureViewProjection\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x1f\n\x17\x66\x65\x61ture_view_name_alias\x18\x03 \x01(\t\x12\x32\n\x0f\x66\x65\x61ture_columns\x18\x02 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12G\n\x0cjoin_key_map\x18\x04 \x03(\x0b\x32\x31.feast.core.FeatureViewProjection.JoinKeyMapEntry\x12\x17\n\x0ftimestamp_field\x18\x05 \x01(\t\x12\x1d\n\x15\x64\x61te_partition_column\x18\x06 \x01(\t\x12 \n\x18\x63reated_timestamp_column\x18\x07 \x01(\t\x12,\n\x0c\x62\x61tch_source\x18\x08 \x01(\x0b\x32\x16.feast.core.DataSource\x12-\n\rstream_source\x18\t \x01(\x0b\x32\x16.feast.core.DataSource\x12\x18\n\x0bversion_tag\x18\n \x01(\x05H\x00\x88\x01\x01\x12\x11\n\tview_type\x18\x0b \x01(\t\x1a\x31\n\x0fJoinKeyMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0e\n\x0c_version_tagBZ\n\x10\x66\x65\x61st.proto.coreB\x15\x46\x65\x61tureReferenceProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -27,7 +27,7 @@ _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._options = None _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_options = b'8\001' _globals['_FEATUREVIEWPROJECTION']._serialized_start=110 - _globals['_FEATUREVIEWPROJECTION']._serialized_end=552 - _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_start=503 - _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_end=552 + _globals['_FEATUREVIEWPROJECTION']._serialized_end=613 + _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_start=548 + _globals['_FEATUREVIEWPROJECTION_JOINKEYMAPENTRY']._serialized_end=597 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi index 6b44ad4a931..4566cfd5aec 100644 --- a/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/FeatureViewProjection_pb2.pyi @@ -49,6 +49,8 @@ class FeatureViewProjection(google.protobuf.message.Message): CREATED_TIMESTAMP_COLUMN_FIELD_NUMBER: builtins.int BATCH_SOURCE_FIELD_NUMBER: builtins.int STREAM_SOURCE_FIELD_NUMBER: builtins.int + VERSION_TAG_FIELD_NUMBER: builtins.int + VIEW_TYPE_FIELD_NUMBER: builtins.int feature_view_name: builtins.str """The feature view name""" feature_view_name_alias: builtins.str @@ -68,6 +70,12 @@ class FeatureViewProjection(google.protobuf.message.Message): @property def stream_source(self) -> feast.core.DataSource_pb2.DataSource: """Streaming DataSource from where this view can consume "online" feature data.""" + version_tag: builtins.int + """Optional version tag for version-qualified feature references (e.g., @v2).""" + view_type: builtins.str + """Distinguishes the source view type (e.g., "featureView", "labelView"). + Empty string or unset means "featureView" for backward compatibility. + """ def __init__( self, *, @@ -80,8 +88,11 @@ class FeatureViewProjection(google.protobuf.message.Message): created_timestamp_column: builtins.str = ..., batch_source: feast.core.DataSource_pb2.DataSource | None = ..., stream_source: feast.core.DataSource_pb2.DataSource | None = ..., + version_tag: builtins.int | None = ..., + view_type: builtins.str = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "stream_source", b"stream_source"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "created_timestamp_column", b"created_timestamp_column", "date_partition_column", b"date_partition_column", "feature_columns", b"feature_columns", "feature_view_name", b"feature_view_name", "feature_view_name_alias", b"feature_view_name_alias", "join_key_map", b"join_key_map", "stream_source", b"stream_source", "timestamp_field", b"timestamp_field"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["_version_tag", b"_version_tag", "batch_source", b"batch_source", "stream_source", b"stream_source", "version_tag", b"version_tag"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["_version_tag", b"_version_tag", "batch_source", b"batch_source", "created_timestamp_column", b"created_timestamp_column", "date_partition_column", b"date_partition_column", "feature_columns", b"feature_columns", "feature_view_name", b"feature_view_name", "feature_view_name_alias", b"feature_view_name_alias", "join_key_map", b"join_key_map", "stream_source", b"stream_source", "timestamp_field", b"timestamp_field", "version_tag", b"version_tag", "view_type", b"view_type"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["_version_tag", b"_version_tag"]) -> typing_extensions.Literal["version_tag"] | None: ... global___FeatureViewProjection = FeatureViewProjection diff --git a/sdk/python/feast/protos/feast/core/FeatureViewVersion_pb2.py b/sdk/python/feast/protos/feast/core/FeatureViewVersion_pb2.py new file mode 100644 index 00000000000..88bc21c2a8f --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureViewVersion_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/FeatureViewVersion.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#feast/core/FeatureViewVersion.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf8\x01\n\x18\x46\x65\x61tureViewVersionRecord\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x12\n\nproject_id\x18\x02 \x01(\t\x12\x16\n\x0eversion_number\x18\x03 \x01(\x05\x12\x19\n\x11\x66\x65\x61ture_view_type\x18\x04 \x01(\t\x12\x1a\n\x12\x66\x65\x61ture_view_proto\x18\x05 \x01(\x0c\x12\x35\n\x11\x63reated_timestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0b\x64\x65scription\x18\x07 \x01(\t\x12\x12\n\nversion_id\x18\x08 \x01(\t\"R\n\x19\x46\x65\x61tureViewVersionHistory\x12\x35\n\x07records\x18\x01 \x03(\x0b\x32$.feast.core.FeatureViewVersionRecordB\\\n\x10\x66\x65\x61st.proto.coreB\x17\x46\x65\x61tureViewVersionProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.FeatureViewVersion_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\027FeatureViewVersionProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_FEATUREVIEWVERSIONRECORD']._serialized_start=85 + _globals['_FEATUREVIEWVERSIONRECORD']._serialized_end=333 + _globals['_FEATUREVIEWVERSIONHISTORY']._serialized_start=335 + _globals['_FEATUREVIEWVERSIONHISTORY']._serialized_end=417 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/FeatureViewVersion_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureViewVersion_pb2.pyi new file mode 100644 index 00000000000..a6dba9d53d4 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureViewVersion_pb2.pyi @@ -0,0 +1,87 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2024 The Feast Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class FeatureViewVersionRecord(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_VIEW_NAME_FIELD_NUMBER: builtins.int + PROJECT_ID_FIELD_NUMBER: builtins.int + VERSION_NUMBER_FIELD_NUMBER: builtins.int + FEATURE_VIEW_TYPE_FIELD_NUMBER: builtins.int + FEATURE_VIEW_PROTO_FIELD_NUMBER: builtins.int + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + VERSION_ID_FIELD_NUMBER: builtins.int + feature_view_name: builtins.str + project_id: builtins.str + version_number: builtins.int + feature_view_type: builtins.str + """"feature_view" | "stream_feature_view" | "on_demand_feature_view" """ + feature_view_proto: builtins.bytes + """serialized FV proto snapshot""" + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + description: builtins.str + version_id: builtins.str + """auto-generated UUID for unique identification""" + def __init__( + self, + *, + feature_view_name: builtins.str = ..., + project_id: builtins.str = ..., + version_number: builtins.int = ..., + feature_view_type: builtins.str = ..., + feature_view_proto: builtins.bytes = ..., + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + description: builtins.str = ..., + version_id: builtins.str = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "description", b"description", "feature_view_name", b"feature_view_name", "feature_view_proto", b"feature_view_proto", "feature_view_type", b"feature_view_type", "project_id", b"project_id", "version_id", b"version_id", "version_number", b"version_number"]) -> None: ... + +global___FeatureViewVersionRecord = FeatureViewVersionRecord + +class FeatureViewVersionHistory(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RECORDS_FIELD_NUMBER: builtins.int + @property + def records(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___FeatureViewVersionRecord]: ... + def __init__( + self, + *, + records: collections.abc.Iterable[global___FeatureViewVersionRecord] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["records", b"records"]) -> None: ... + +global___FeatureViewVersionHistory = FeatureViewVersionHistory diff --git a/sdk/python/feast/protos/feast/core/FeatureViewVersion_pb2_grpc.py b/sdk/python/feast/protos/feast/core/FeatureViewVersion_pb2_grpc.py new file mode 100644 index 00000000000..2daafffebfc --- /dev/null +++ b/sdk/python/feast/protos/feast/core/FeatureViewVersion_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/FeatureView_pb2.py b/sdk/python/feast/protos/feast/core/FeatureView_pb2.py index 9a59255375f..71766558c82 100644 --- a/sdk/python/feast/protos/feast/core/FeatureView_pb2.py +++ b/sdk/python/feast/protos/feast/core/FeatureView_pb2.py @@ -19,7 +19,7 @@ from feast.protos.feast.core import Transformation_pb2 as feast_dot_core_dot_Transformation__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66\x65\x61st/core/FeatureView.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\"c\n\x0b\x46\x65\x61tureView\x12)\n\x04spec\x18\x01 \x01(\x0b\x32\x1b.feast.core.FeatureViewSpec\x12)\n\x04meta\x18\x02 \x01(\x0b\x32\x1b.feast.core.FeatureViewMeta\"\xd4\x04\n\x0f\x46\x65\x61tureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x65ntities\x18\x03 \x03(\t\x12+\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x33\n\x04tags\x18\x05 \x03(\x0b\x32%.feast.core.FeatureViewSpec.TagsEntry\x12&\n\x03ttl\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12,\n\x0c\x62\x61tch_source\x18\x07 \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0e\n\x06online\x18\x08 \x01(\x08\x12-\n\rstream_source\x18\t \x01(\x0b\x32\x16.feast.core.DataSource\x12\x13\n\x0b\x64\x65scription\x18\n \x01(\t\x12\r\n\x05owner\x18\x0b \x01(\t\x12\x31\n\x0e\x65ntity_columns\x18\x0c \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x0f\n\x07offline\x18\r \x01(\x08\x12\x31\n\x0csource_views\x18\x0e \x03(\x0b\x32\x1b.feast.core.FeatureViewSpec\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\x0f \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x0c\n\x04mode\x18\x10 \x01(\t\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xcc\x01\n\x0f\x46\x65\x61tureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x46\n\x19materialization_intervals\x18\x03 \x03(\x0b\x32#.feast.core.MaterializationInterval\"w\n\x17MaterializationInterval\x12.\n\nstart_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"@\n\x0f\x46\x65\x61tureViewList\x12-\n\x0c\x66\x65\x61tureviews\x18\x01 \x03(\x0b\x32\x17.feast.core.FeatureViewBU\n\x10\x66\x65\x61st.proto.coreB\x10\x46\x65\x61tureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x66\x65\x61st/core/FeatureView.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\"c\n\x0b\x46\x65\x61tureView\x12)\n\x04spec\x18\x01 \x01(\x0b\x32\x1b.feast.core.FeatureViewSpec\x12)\n\x04meta\x18\x02 \x01(\x0b\x32\x1b.feast.core.FeatureViewMeta\"\x9f\x05\n\x0f\x46\x65\x61tureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x65ntities\x18\x03 \x03(\t\x12+\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x33\n\x04tags\x18\x05 \x03(\x0b\x32%.feast.core.FeatureViewSpec.TagsEntry\x12&\n\x03ttl\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12,\n\x0c\x62\x61tch_source\x18\x07 \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0e\n\x06online\x18\x08 \x01(\x08\x12-\n\rstream_source\x18\t \x01(\x0b\x32\x16.feast.core.DataSource\x12\x13\n\x0b\x64\x65scription\x18\n \x01(\t\x12\r\n\x05owner\x18\x0b \x01(\t\x12\x31\n\x0e\x65ntity_columns\x18\x0c \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x0f\n\x07offline\x18\r \x01(\x08\x12\x31\n\x0csource_views\x18\x0e \x03(\x0b\x32\x1b.feast.core.FeatureViewSpec\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\x0f \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x0c\n\x04mode\x18\x10 \x01(\t\x12\x19\n\x11\x65nable_validation\x18\x11 \x01(\x08\x12\x0f\n\x07version\x18\x12 \x01(\t\x12\x0b\n\x03org\x18\x13 \x01(\t\x12\x10\n\x08\x64isabled\x18\x14 \x01(\x08\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xad\x02\n\x0f\x46\x65\x61tureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x46\n\x19materialization_intervals\x18\x03 \x03(\x0b\x32#.feast.core.MaterializationInterval\x12\x1e\n\x16\x63urrent_version_number\x18\x04 \x01(\x05\x12\x12\n\nversion_id\x18\x05 \x01(\t\x12+\n\x05state\x18\x06 \x01(\x0e\x32\x1c.feast.core.FeatureViewState\"w\n\x17MaterializationInterval\x12.\n\nstart_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"@\n\x0f\x46\x65\x61tureViewList\x12-\n\x0c\x66\x65\x61tureviews\x18\x01 \x03(\x0b\x32\x17.feast.core.FeatureView*n\n\x10\x46\x65\x61tureViewState\x12\x15\n\x11STATE_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x43REATED\x10\x01\x12\r\n\tGENERATED\x10\x02\x12\x11\n\rMATERIALIZING\x10\x03\x12\x14\n\x10\x41VAILABLE_ONLINE\x10\x04\x42U\n\x10\x66\x65\x61st.proto.coreB\x10\x46\x65\x61tureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -29,16 +29,18 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\020FeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/core' _globals['_FEATUREVIEWSPEC_TAGSENTRY']._options = None _globals['_FEATUREVIEWSPEC_TAGSENTRY']._serialized_options = b'8\001' + _globals['_FEATUREVIEWSTATE']._serialized_start=1463 + _globals['_FEATUREVIEWSTATE']._serialized_end=1573 _globals['_FEATUREVIEW']._serialized_start=197 _globals['_FEATUREVIEW']._serialized_end=296 _globals['_FEATUREVIEWSPEC']._serialized_start=299 - _globals['_FEATUREVIEWSPEC']._serialized_end=895 - _globals['_FEATUREVIEWSPEC_TAGSENTRY']._serialized_start=852 - _globals['_FEATUREVIEWSPEC_TAGSENTRY']._serialized_end=895 - _globals['_FEATUREVIEWMETA']._serialized_start=898 - _globals['_FEATUREVIEWMETA']._serialized_end=1102 - _globals['_MATERIALIZATIONINTERVAL']._serialized_start=1104 - _globals['_MATERIALIZATIONINTERVAL']._serialized_end=1223 - _globals['_FEATUREVIEWLIST']._serialized_start=1225 - _globals['_FEATUREVIEWLIST']._serialized_end=1289 + _globals['_FEATUREVIEWSPEC']._serialized_end=970 + _globals['_FEATUREVIEWSPEC_TAGSENTRY']._serialized_start=927 + _globals['_FEATUREVIEWSPEC_TAGSENTRY']._serialized_end=970 + _globals['_FEATUREVIEWMETA']._serialized_start=973 + _globals['_FEATUREVIEWMETA']._serialized_end=1274 + _globals['_MATERIALIZATIONINTERVAL']._serialized_start=1276 + _globals['_MATERIALIZATIONINTERVAL']._serialized_end=1395 + _globals['_FEATUREVIEWLIST']._serialized_start=1397 + _globals['_FEATUREVIEWLIST']._serialized_end=1461 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/FeatureView_pb2.pyi b/sdk/python/feast/protos/feast/core/FeatureView_pb2.pyi index 30a0bcf5679..e73bc66a555 100644 --- a/sdk/python/feast/protos/feast/core/FeatureView_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/FeatureView_pb2.pyi @@ -24,17 +24,59 @@ import feast.core.Transformation_pb2 import google.protobuf.descriptor import google.protobuf.duration_pb2 import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import google.protobuf.timestamp_pb2 import sys +import typing -if sys.version_info >= (3, 8): +if sys.version_info >= (3, 10): import typing as typing_extensions else: import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor +class _FeatureViewState: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _FeatureViewStateEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_FeatureViewState.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + STATE_UNSPECIFIED: _FeatureViewState.ValueType # 0 + """Default value for backward compatibility. Treated as AVAILABLE_ONLINE + for existing feature views that predate the state machine. + """ + CREATED: _FeatureViewState.ValueType # 1 + """Feature view has been registered via feast apply but no data is available.""" + GENERATED: _FeatureViewState.ValueType # 2 + """Feature engineering / offline data generation is complete. + The feature view is ready to be materialized. + """ + MATERIALIZING: _FeatureViewState.ValueType # 3 + """Materialization is currently in progress.""" + AVAILABLE_ONLINE: _FeatureViewState.ValueType # 4 + """Materialization completed. Features are available in the online store.""" + +class FeatureViewState(_FeatureViewState, metaclass=_FeatureViewStateEnumTypeWrapper): + """Lifecycle state of a feature view.""" + +STATE_UNSPECIFIED: FeatureViewState.ValueType # 0 +"""Default value for backward compatibility. Treated as AVAILABLE_ONLINE +for existing feature views that predate the state machine. +""" +CREATED: FeatureViewState.ValueType # 1 +"""Feature view has been registered via feast apply but no data is available.""" +GENERATED: FeatureViewState.ValueType # 2 +"""Feature engineering / offline data generation is complete. +The feature view is ready to be materialized. +""" +MATERIALIZING: FeatureViewState.ValueType # 3 +"""Materialization is currently in progress.""" +AVAILABLE_ONLINE: FeatureViewState.ValueType # 4 +"""Materialization completed. Features are available in the online store.""" +global___FeatureViewState = FeatureViewState + class FeatureView(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -58,7 +100,7 @@ class FeatureView(google.protobuf.message.Message): global___FeatureView = FeatureView class FeatureViewSpec(google.protobuf.message.Message): - """Next available id: 17 + """Next available id: 20 TODO(adchia): refactor common fields from this and ODFV into separate metadata proto """ @@ -95,6 +137,10 @@ class FeatureViewSpec(google.protobuf.message.Message): SOURCE_VIEWS_FIELD_NUMBER: builtins.int FEATURE_TRANSFORMATION_FIELD_NUMBER: builtins.int MODE_FIELD_NUMBER: builtins.int + ENABLE_VALIDATION_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + ORG_FIELD_NUMBER: builtins.int + DISABLED_FIELD_NUMBER: builtins.int name: builtins.str """Name of the feature view. Must be unique. Not updated.""" project: builtins.str @@ -117,14 +163,18 @@ class FeatureViewSpec(google.protobuf.message.Message): """ @property def batch_source(self) -> feast.core.DataSource_pb2.DataSource: - """Batch/Offline DataSource where this view can retrieve offline feature data.""" + """Batch/Offline DataSource where this view can retrieve offline feature data. + Optional: if not set, the feature view has no associated batch data source (e.g. purely derived views). + """ online: builtins.bool """Whether these features should be served online or not This is also used to determine whether the features should be written to the online store """ @property def stream_source(self) -> feast.core.DataSource_pb2.DataSource: - """Streaming DataSource from where this view can consume "online" feature data.""" + """Streaming DataSource from where this view can consume "online" feature data. + Optional: only required for streaming feature views. + """ description: builtins.str """Description of the feature view.""" owner: builtins.str @@ -140,7 +190,18 @@ class FeatureViewSpec(google.protobuf.message.Message): def feature_transformation(self) -> feast.core.Transformation_pb2.FeatureTransformationV2: """Feature transformation for batch feature views""" mode: builtins.str - """The transformation mode (e.g., "python", "pandas", "ray", "spark", "sql")""" + """The transformation mode (e.g., "python", "pandas", "spark", "sql", "ray")""" + enable_validation: builtins.bool + """Whether schema validation is enabled during materialization""" + version: builtins.str + """User-specified version pin (e.g. "latest", "v2", "version2")""" + org: builtins.str + """Organizational unit that owns this feature view (e.g. "ads", "search").""" + disabled: builtins.bool + """Whether this feature view is disabled for serving and materialization. + When true, the feature view will not serve online features or be materialized. + Defaults to false (enabled) for backward compatibility. + """ def __init__( self, *, @@ -160,9 +221,13 @@ class FeatureViewSpec(google.protobuf.message.Message): source_views: collections.abc.Iterable[global___FeatureViewSpec] | None = ..., feature_transformation: feast.core.Transformation_pb2.FeatureTransformationV2 | None = ..., mode: builtins.str = ..., + enable_validation: builtins.bool = ..., + version: builtins.str = ..., + org: builtins.str = ..., + disabled: builtins.bool = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "feature_transformation", b"feature_transformation", "stream_source", b"stream_source", "ttl", b"ttl"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "description", b"description", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "offline", b"offline", "online", b"online", "owner", b"owner", "project", b"project", "source_views", b"source_views", "stream_source", b"stream_source", "tags", b"tags", "ttl", b"ttl"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "description", b"description", "disabled", b"disabled", "enable_validation", b"enable_validation", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "offline", b"offline", "online", b"online", "org", b"org", "owner", b"owner", "project", b"project", "source_views", b"source_views", "stream_source", b"stream_source", "tags", b"tags", "ttl", b"ttl", "version", b"version"]) -> None: ... global___FeatureViewSpec = FeatureViewSpec @@ -172,6 +237,9 @@ class FeatureViewMeta(google.protobuf.message.Message): CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int MATERIALIZATION_INTERVALS_FIELD_NUMBER: builtins.int + CURRENT_VERSION_NUMBER_FIELD_NUMBER: builtins.int + VERSION_ID_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int @property def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: """Time where this Feature View is created""" @@ -181,15 +249,24 @@ class FeatureViewMeta(google.protobuf.message.Message): @property def materialization_intervals(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MaterializationInterval]: """List of pairs (start_time, end_time) for which this feature view has been materialized.""" + current_version_number: builtins.int + """The current version number of this feature view in the version history.""" + version_id: builtins.str + """Auto-generated UUID identifying this specific version.""" + state: global___FeatureViewState.ValueType + """Lifecycle state of this feature view.""" def __init__( self, *, created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., materialization_intervals: collections.abc.Iterable[global___MaterializationInterval] | None = ..., + current_version_number: builtins.int = ..., + version_id: builtins.str = ..., + state: global___FeatureViewState.ValueType = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp", "materialization_intervals", b"materialization_intervals"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "current_version_number", b"current_version_number", "last_updated_timestamp", b"last_updated_timestamp", "materialization_intervals", b"materialization_intervals", "state", b"state", "version_id", b"version_id"]) -> None: ... global___FeatureViewMeta = FeatureViewMeta diff --git a/sdk/python/feast/protos/feast/core/LabelView_pb2.py b/sdk/python/feast/protos/feast/core/LabelView_pb2.py new file mode 100644 index 00000000000..d8fb720a1e2 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/LabelView_pb2.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/LabelView.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 +from feast.protos.feast.core import Feature_pb2 as feast_dot_core_dot_Feature__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1a\x66\x65\x61st/core/LabelView.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\"]\n\tLabelView\x12\'\n\x04spec\x18\x01 \x01(\x0b\x32\x19.feast.core.LabelViewSpec\x12\'\n\x04meta\x18\x02 \x01(\x0b\x32\x19.feast.core.LabelViewMeta\"\x96\x04\n\rLabelViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x65ntities\x18\x03 \x03(\t\x12+\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x31\n\x04tags\x18\x05 \x03(\x0b\x32#.feast.core.LabelViewSpec.TagsEntry\x12&\n\x03ttl\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12&\n\x06source\x18\x07 \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0e\n\x06online\x18\x08 \x01(\x08\x12\x13\n\x0b\x64\x65scription\x18\t \x01(\t\x12\r\n\x05owner\x18\n \x01(\t\x12\x31\n\x0e\x65ntity_columns\x18\x0b \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x15\n\rlabeler_field\x18\x0c \x01(\t\x12=\n\x0f\x63onflict_policy\x18\r \x01(\x0e\x32$.feast.core.ConflictResolutionPolicy\x12\x1a\n\x0eretain_history\x18\x0e \x01(\x08\x42\x02\x18\x01\x12\x1e\n\x16reference_feature_view\x18\x0f \x01(\t\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x82\x01\n\rLabelViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp*X\n\x18\x43onflictResolutionPolicy\x12\x13\n\x0fLAST_WRITE_WINS\x10\x00\x12\x14\n\x10LABELER_PRIORITY\x10\x01\x12\x11\n\rMAJORITY_VOTE\x10\x02\x42S\n\x10\x66\x65\x61st.proto.coreB\x0eLabelViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.LabelView_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\016LabelViewProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_LABELVIEWSPEC_TAGSENTRY']._options = None + _globals['_LABELVIEWSPEC_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LABELVIEWSPEC'].fields_by_name['retain_history']._options = None + _globals['_LABELVIEWSPEC'].fields_by_name['retain_history']._serialized_options = b'\030\001' + _globals['_CONFLICTRESOLUTIONPOLICY']._serialized_start=927 + _globals['_CONFLICTRESOLUTIONPOLICY']._serialized_end=1015 + _globals['_LABELVIEW']._serialized_start=162 + _globals['_LABELVIEW']._serialized_end=255 + _globals['_LABELVIEWSPEC']._serialized_start=258 + _globals['_LABELVIEWSPEC']._serialized_end=792 + _globals['_LABELVIEWSPEC_TAGSENTRY']._serialized_start=749 + _globals['_LABELVIEWSPEC_TAGSENTRY']._serialized_end=792 + _globals['_LABELVIEWMETA']._serialized_start=795 + _globals['_LABELVIEWMETA']._serialized_end=925 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/LabelView_pb2.pyi b/sdk/python/feast/protos/feast/core/LabelView_pb2.pyi new file mode 100644 index 00000000000..e8f70a82000 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/LabelView_pb2.pyi @@ -0,0 +1,195 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file + +Copyright 2026 The Feast Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +import builtins +import collections.abc +import feast.core.DataSource_pb2 +import feast.core.Feature_pb2 +import google.protobuf.descriptor +import google.protobuf.duration_pb2 +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _ConflictResolutionPolicy: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ConflictResolutionPolicyEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ConflictResolutionPolicy.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + LAST_WRITE_WINS: _ConflictResolutionPolicy.ValueType # 0 + LABELER_PRIORITY: _ConflictResolutionPolicy.ValueType # 1 + MAJORITY_VOTE: _ConflictResolutionPolicy.ValueType # 2 + +class ConflictResolutionPolicy(_ConflictResolutionPolicy, metaclass=_ConflictResolutionPolicyEnumTypeWrapper): ... + +LAST_WRITE_WINS: ConflictResolutionPolicy.ValueType # 0 +LABELER_PRIORITY: ConflictResolutionPolicy.ValueType # 1 +MAJORITY_VOTE: ConflictResolutionPolicy.ValueType # 2 +global___ConflictResolutionPolicy = ConflictResolutionPolicy + +class LabelView(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SPEC_FIELD_NUMBER: builtins.int + META_FIELD_NUMBER: builtins.int + @property + def spec(self) -> global___LabelViewSpec: ... + @property + def meta(self) -> global___LabelViewMeta: ... + def __init__( + self, + *, + spec: global___LabelViewSpec | None = ..., + meta: global___LabelViewMeta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "spec", b"spec"]) -> None: ... + +global___LabelView = LabelView + +class LabelViewSpec(google.protobuf.message.Message): + """Next available id: 16""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ENTITIES_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + TTL_FIELD_NUMBER: builtins.int + SOURCE_FIELD_NUMBER: builtins.int + ONLINE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + OWNER_FIELD_NUMBER: builtins.int + ENTITY_COLUMNS_FIELD_NUMBER: builtins.int + LABELER_FIELD_FIELD_NUMBER: builtins.int + CONFLICT_POLICY_FIELD_NUMBER: builtins.int + RETAIN_HISTORY_FIELD_NUMBER: builtins.int + REFERENCE_FEATURE_VIEW_FIELD_NUMBER: builtins.int + name: builtins.str + """Name of the label view. Must be unique. Not updated.""" + project: builtins.str + """Name of Feast project that this label view belongs to.""" + @property + def entities(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """List of names of entities associated with this label view.""" + @property + def features(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """List of specifications for each label field defined as part of this label view.""" + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """User defined metadata.""" + @property + def ttl(self) -> google.protobuf.duration_pb2.Duration: + """Labels older than ttl will be excluded from online serving.""" + @property + def source(self) -> feast.core.DataSource_pb2.DataSource: + """DataSource (typically a PushSource) that feeds label data into this view.""" + online: builtins.bool + """Whether labels should be served from the online store.""" + description: builtins.str + """Description of the label view.""" + owner: builtins.str + """Owner of the label view.""" + @property + def entity_columns(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Feature_pb2.FeatureSpecV2]: + """List of specifications for each entity column.""" + labeler_field: builtins.str + """The schema field that identifies the labeler (e.g. "labeler").""" + conflict_policy: global___ConflictResolutionPolicy.ValueType + """How conflicting labels from different labelers are resolved. + Enforced for offline store reads; online store uses LAST_WRITE_WINS. + """ + retain_history: builtins.bool + """DEPRECATED: retain_history is always true (offline store always appends). + Field kept for backwards wire compatibility; ignored by the SDK. + """ + reference_feature_view: builtins.str + """Optional name of the FeatureView whose entities this label view annotates.""" + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + entities: collections.abc.Iterable[builtins.str] | None = ..., + features: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ttl: google.protobuf.duration_pb2.Duration | None = ..., + source: feast.core.DataSource_pb2.DataSource | None = ..., + online: builtins.bool = ..., + description: builtins.str = ..., + owner: builtins.str = ..., + entity_columns: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., + labeler_field: builtins.str = ..., + conflict_policy: global___ConflictResolutionPolicy.ValueType = ..., + retain_history: builtins.bool = ..., + reference_feature_view: builtins.str = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["source", b"source", "ttl", b"ttl"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["conflict_policy", b"conflict_policy", "description", b"description", "entities", b"entities", "entity_columns", b"entity_columns", "features", b"features", "labeler_field", b"labeler_field", "name", b"name", "online", b"online", "owner", b"owner", "project", b"project", "reference_feature_view", b"reference_feature_view", "retain_history", b"retain_history", "source", b"source", "tags", b"tags", "ttl", b"ttl"]) -> None: ... + +global___LabelViewSpec = LabelViewSpec + +class LabelViewMeta(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int + LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + @property + def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time when this Label View was created.""" + @property + def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Time when this Label View was last updated.""" + def __init__( + self, + *, + created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> None: ... + +global___LabelViewMeta = LabelViewMeta diff --git a/sdk/python/feast/protos/feast/core/LabelView_pb2_grpc.py b/sdk/python/feast/protos/feast/core/LabelView_pb2_grpc.py new file mode 100644 index 00000000000..2daafffebfc --- /dev/null +++ b/sdk/python/feast/protos/feast/core/LabelView_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py index 5b8ec9b11f6..1c264ec06c5 100644 --- a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py +++ b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.py @@ -21,7 +21,7 @@ from feast.protos.feast.core import Aggregation_pb2 as feast_dot_core_dot_Aggregation__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$feast/core/OnDemandFeatureView.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a&feast/core/FeatureViewProjection.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\x1a\x1c\x66\x65\x61st/core/Aggregation.proto\"{\n\x13OnDemandFeatureView\x12\x31\n\x04spec\x18\x01 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewSpec\x12\x31\n\x04meta\x18\x02 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewMeta\"\xbf\x05\n\x17OnDemandFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12+\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x41\n\x07sources\x18\x04 \x03(\x0b\x32\x30.feast.core.OnDemandFeatureViewSpec.SourcesEntry\x12\x42\n\x15user_defined_function\x18\x05 \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\n \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12;\n\x04tags\x18\x07 \x03(\x0b\x32-.feast.core.OnDemandFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12\x0c\n\x04mode\x18\x0b \x01(\t\x12\x1d\n\x15write_to_online_store\x18\x0c \x01(\x08\x12\x10\n\x08\x65ntities\x18\r \x03(\t\x12\x31\n\x0e\x65ntity_columns\x18\x0e \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x11\n\tsingleton\x18\x0f \x01(\x08\x12-\n\x0c\x61ggregations\x18\x10 \x03(\x0b\x32\x17.feast.core.Aggregation\x1aJ\n\x0cSourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.core.OnDemandSource:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x01\n\x17OnDemandFeatureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc8\x01\n\x0eOnDemandSource\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x44\n\x17\x66\x65\x61ture_view_projection\x18\x03 \x01(\x0b\x32!.feast.core.FeatureViewProjectionH\x00\x12\x35\n\x13request_data_source\x18\x02 \x01(\x0b\x32\x16.feast.core.DataSourceH\x00\x42\x08\n\x06source\"H\n\x13UserDefinedFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\x0c\x12\x11\n\tbody_text\x18\x03 \x01(\t:\x02\x18\x01\"X\n\x17OnDemandFeatureViewList\x12=\n\x14ondemandfeatureviews\x18\x01 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureViewB]\n\x10\x66\x65\x61st.proto.coreB\x18OnDemandFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$feast/core/OnDemandFeatureView.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a&feast/core/FeatureViewProjection.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\x1a\x1c\x66\x65\x61st/core/Aggregation.proto\"{\n\x13OnDemandFeatureView\x12\x31\n\x04spec\x18\x01 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewSpec\x12\x31\n\x04meta\x18\x02 \x01(\x0b\x32#.feast.core.OnDemandFeatureViewMeta\"\xef\x05\n\x17OnDemandFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12+\n\x08\x66\x65\x61tures\x18\x03 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x41\n\x07sources\x18\x04 \x03(\x0b\x32\x30.feast.core.OnDemandFeatureViewSpec.SourcesEntry\x12\x42\n\x15user_defined_function\x18\x05 \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\n \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12;\n\x04tags\x18\x07 \x03(\x0b\x32-.feast.core.OnDemandFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12\x0c\n\x04mode\x18\x0b \x01(\t\x12\x1d\n\x15write_to_online_store\x18\x0c \x01(\x08\x12\x10\n\x08\x65ntities\x18\r \x03(\t\x12\x31\n\x0e\x65ntity_columns\x18\x0e \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x11\n\tsingleton\x18\x0f \x01(\x08\x12-\n\x0c\x61ggregations\x18\x10 \x03(\x0b\x32\x17.feast.core.Aggregation\x12\x0f\n\x07version\x18\x11 \x01(\t\x12\x0b\n\x03org\x18\x12 \x01(\t\x12\x10\n\x08\x64isabled\x18\x13 \x01(\x08\x1aJ\n\x0cSourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.core.OnDemandSource:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xed\x01\n\x17OnDemandFeatureViewMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1e\n\x16\x63urrent_version_number\x18\x03 \x01(\x05\x12\x12\n\nversion_id\x18\x04 \x01(\t\x12+\n\x05state\x18\x05 \x01(\x0e\x32\x1c.feast.core.FeatureViewState\"\xc8\x01\n\x0eOnDemandSource\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x44\n\x17\x66\x65\x61ture_view_projection\x18\x03 \x01(\x0b\x32!.feast.core.FeatureViewProjectionH\x00\x12\x35\n\x13request_data_source\x18\x02 \x01(\x0b\x32\x16.feast.core.DataSourceH\x00\x42\x08\n\x06source\"H\n\x13UserDefinedFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\x0c\x12\x11\n\tbody_text\x18\x03 \x01(\t:\x02\x18\x01\"X\n\x17OnDemandFeatureViewList\x12=\n\x14ondemandfeatureviews\x18\x01 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureViewB]\n\x10\x66\x65\x61st.proto.coreB\x18OnDemandFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -40,17 +40,17 @@ _globals['_ONDEMANDFEATUREVIEW']._serialized_start=273 _globals['_ONDEMANDFEATUREVIEW']._serialized_end=396 _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_start=399 - _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_end=1102 - _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_start=983 - _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_end=1057 - _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=1059 - _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=1102 - _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_start=1105 - _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_end=1245 - _globals['_ONDEMANDSOURCE']._serialized_start=1248 - _globals['_ONDEMANDSOURCE']._serialized_end=1448 - _globals['_USERDEFINEDFUNCTION']._serialized_start=1450 - _globals['_USERDEFINEDFUNCTION']._serialized_end=1522 - _globals['_ONDEMANDFEATUREVIEWLIST']._serialized_start=1524 - _globals['_ONDEMANDFEATUREVIEWLIST']._serialized_end=1612 + _globals['_ONDEMANDFEATUREVIEWSPEC']._serialized_end=1150 + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_start=1031 + _globals['_ONDEMANDFEATUREVIEWSPEC_SOURCESENTRY']._serialized_end=1105 + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=1107 + _globals['_ONDEMANDFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=1150 + _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_start=1153 + _globals['_ONDEMANDFEATUREVIEWMETA']._serialized_end=1390 + _globals['_ONDEMANDSOURCE']._serialized_start=1393 + _globals['_ONDEMANDSOURCE']._serialized_end=1593 + _globals['_USERDEFINEDFUNCTION']._serialized_start=1595 + _globals['_USERDEFINEDFUNCTION']._serialized_end=1667 + _globals['_ONDEMANDFEATUREVIEWLIST']._serialized_start=1669 + _globals['_ONDEMANDFEATUREVIEWLIST']._serialized_end=1757 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi index c424c442ee7..9b5db304df7 100644 --- a/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/OnDemandFeatureView_pb2.pyi @@ -59,7 +59,7 @@ class OnDemandFeatureView(google.protobuf.message.Message): global___OnDemandFeatureView = OnDemandFeatureView class OnDemandFeatureViewSpec(google.protobuf.message.Message): - """Next available id: 9""" + """Next available id: 19""" DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -110,6 +110,9 @@ class OnDemandFeatureViewSpec(google.protobuf.message.Message): ENTITY_COLUMNS_FIELD_NUMBER: builtins.int SINGLETON_FIELD_NUMBER: builtins.int AGGREGATIONS_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + ORG_FIELD_NUMBER: builtins.int + DISABLED_FIELD_NUMBER: builtins.int name: builtins.str """Name of the feature view. Must be unique. Not updated.""" project: builtins.str @@ -144,6 +147,15 @@ class OnDemandFeatureViewSpec(google.protobuf.message.Message): @property def aggregations(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Aggregation_pb2.Aggregation]: """Aggregation definitions""" + version: builtins.str + """User-specified version pin (e.g. "latest", "v2", "version2")""" + org: builtins.str + """Organizational unit that owns this feature view (e.g. "ads", "search").""" + disabled: builtins.bool + """Whether this feature view is disabled for serving. + When true, the feature view will not serve features. + Defaults to false (enabled) for backward compatibility. + """ def __init__( self, *, @@ -162,9 +174,12 @@ class OnDemandFeatureViewSpec(google.protobuf.message.Message): entity_columns: collections.abc.Iterable[feast.core.Feature_pb2.FeatureSpecV2] | None = ..., singleton: builtins.bool = ..., aggregations: collections.abc.Iterable[feast.core.Aggregation_pb2.Aggregation] | None = ..., + version: builtins.str = ..., + org: builtins.str = ..., + disabled: builtins.bool = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["feature_transformation", b"feature_transformation", "user_defined_function", b"user_defined_function"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["aggregations", b"aggregations", "description", b"description", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "owner", b"owner", "project", b"project", "singleton", b"singleton", "sources", b"sources", "tags", b"tags", "user_defined_function", b"user_defined_function", "write_to_online_store", b"write_to_online_store"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["aggregations", b"aggregations", "description", b"description", "disabled", b"disabled", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "org", b"org", "owner", b"owner", "project", b"project", "singleton", b"singleton", "sources", b"sources", "tags", b"tags", "user_defined_function", b"user_defined_function", "version", b"version", "write_to_online_store", b"write_to_online_store"]) -> None: ... global___OnDemandFeatureViewSpec = OnDemandFeatureViewSpec @@ -173,20 +188,32 @@ class OnDemandFeatureViewMeta(google.protobuf.message.Message): CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int + CURRENT_VERSION_NUMBER_FIELD_NUMBER: builtins.int + VERSION_ID_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int @property def created_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: """Time where this Feature View is created""" @property def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: """Time where this Feature View is last updated""" + current_version_number: builtins.int + """The current version number of this feature view in the version history.""" + version_id: builtins.str + """Auto-generated UUID identifying this specific version.""" + state: feast.core.FeatureView_pb2.FeatureViewState.ValueType + """Lifecycle state of this feature view.""" def __init__( self, *, created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + current_version_number: builtins.int = ..., + version_id: builtins.str = ..., + state: feast.core.FeatureView_pb2.FeatureViewState.ValueType = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "current_version_number", b"current_version_number", "last_updated_timestamp", b"last_updated_timestamp", "state", b"state", "version_id", b"version_id"]) -> None: ... global___OnDemandFeatureViewMeta = OnDemandFeatureViewMeta diff --git a/sdk/python/feast/protos/feast/core/Permission_pb2.py b/sdk/python/feast/protos/feast/core/Permission_pb2.py index 706fd2eec47..f44540c45e0 100644 --- a/sdk/python/feast/protos/feast/core/Permission_pb2.py +++ b/sdk/python/feast/protos/feast/core/Permission_pb2.py @@ -16,7 +16,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/Permission.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Policy.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\nPermission\x12(\n\x04spec\x18\x01 \x01(\x0b\x32\x1a.feast.core.PermissionSpec\x12(\n\x04meta\x18\x02 \x01(\x0b\x32\x1a.feast.core.PermissionMeta\"\xa0\x06\n\x0ePermissionSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\x05types\x18\x03 \x03(\x0e\x32\x1f.feast.core.PermissionSpec.Type\x12\x15\n\rname_patterns\x18\x04 \x03(\t\x12\x43\n\rrequired_tags\x18\x05 \x03(\x0b\x32,.feast.core.PermissionSpec.RequiredTagsEntry\x12\x39\n\x07\x61\x63tions\x18\x06 \x03(\x0e\x32(.feast.core.PermissionSpec.AuthzedAction\x12\"\n\x06policy\x18\x07 \x01(\x0b\x32\x12.feast.core.Policy\x12\x32\n\x04tags\x18\x08 \x03(\x0b\x32$.feast.core.PermissionSpec.TagsEntry\x1a\x33\n\x11RequiredTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x89\x01\n\rAuthzedAction\x12\n\n\x06\x43REATE\x10\x00\x12\x0c\n\x08\x44\x45SCRIBE\x10\x01\x12\n\n\x06UPDATE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03\x12\x0f\n\x0bREAD_ONLINE\x10\x04\x12\x10\n\x0cREAD_OFFLINE\x10\x05\x12\x10\n\x0cWRITE_ONLINE\x10\x06\x12\x11\n\rWRITE_OFFLINE\x10\x07\"\xe1\x01\n\x04Type\x12\x10\n\x0c\x46\x45\x41TURE_VIEW\x10\x00\x12\x1a\n\x16ON_DEMAND_FEATURE_VIEW\x10\x01\x12\x16\n\x12\x42\x41TCH_FEATURE_VIEW\x10\x02\x12\x17\n\x13STREAM_FEATURE_VIEW\x10\x03\x12\n\n\x06\x45NTITY\x10\x04\x12\x13\n\x0f\x46\x45\x41TURE_SERVICE\x10\x05\x12\x0f\n\x0b\x44\x41TA_SOURCE\x10\x06\x12\x18\n\x14VALIDATION_REFERENCE\x10\x07\x12\x11\n\rSAVED_DATASET\x10\x08\x12\x0e\n\nPERMISSION\x10\t\x12\x0b\n\x07PROJECT\x10\n\"\x83\x01\n\x0ePermissionMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBT\n\x10\x66\x65\x61st.proto.coreB\x0fPermissionProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x66\x65\x61st/core/Permission.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Policy.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"`\n\nPermission\x12(\n\x04spec\x18\x01 \x01(\x0b\x32\x1a.feast.core.PermissionSpec\x12(\n\x04meta\x18\x02 \x01(\x0b\x32\x1a.feast.core.PermissionMeta\"\xb0\x06\n\x0ePermissionSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\x05types\x18\x03 \x03(\x0e\x32\x1f.feast.core.PermissionSpec.Type\x12\x15\n\rname_patterns\x18\x04 \x03(\t\x12\x43\n\rrequired_tags\x18\x05 \x03(\x0b\x32,.feast.core.PermissionSpec.RequiredTagsEntry\x12\x39\n\x07\x61\x63tions\x18\x06 \x03(\x0e\x32(.feast.core.PermissionSpec.AuthzedAction\x12\"\n\x06policy\x18\x07 \x01(\x0b\x32\x12.feast.core.Policy\x12\x32\n\x04tags\x18\x08 \x03(\x0b\x32$.feast.core.PermissionSpec.TagsEntry\x1a\x33\n\x11RequiredTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x89\x01\n\rAuthzedAction\x12\n\n\x06\x43REATE\x10\x00\x12\x0c\n\x08\x44\x45SCRIBE\x10\x01\x12\n\n\x06UPDATE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03\x12\x0f\n\x0bREAD_ONLINE\x10\x04\x12\x10\n\x0cREAD_OFFLINE\x10\x05\x12\x10\n\x0cWRITE_ONLINE\x10\x06\x12\x11\n\rWRITE_OFFLINE\x10\x07\"\xf1\x01\n\x04Type\x12\x10\n\x0c\x46\x45\x41TURE_VIEW\x10\x00\x12\x1a\n\x16ON_DEMAND_FEATURE_VIEW\x10\x01\x12\x16\n\x12\x42\x41TCH_FEATURE_VIEW\x10\x02\x12\x17\n\x13STREAM_FEATURE_VIEW\x10\x03\x12\n\n\x06\x45NTITY\x10\x04\x12\x13\n\x0f\x46\x45\x41TURE_SERVICE\x10\x05\x12\x0f\n\x0b\x44\x41TA_SOURCE\x10\x06\x12\x18\n\x14VALIDATION_REFERENCE\x10\x07\x12\x11\n\rSAVED_DATASET\x10\x08\x12\x0e\n\nPERMISSION\x10\t\x12\x0b\n\x07PROJECT\x10\n\x12\x0e\n\nLABEL_VIEW\x10\x0b\"\x83\x01\n\x0ePermissionMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBT\n\x10\x66\x65\x61st.proto.coreB\x0fPermissionProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -31,7 +31,7 @@ _globals['_PERMISSION']._serialized_start=101 _globals['_PERMISSION']._serialized_end=197 _globals['_PERMISSIONSPEC']._serialized_start=200 - _globals['_PERMISSIONSPEC']._serialized_end=1000 + _globals['_PERMISSIONSPEC']._serialized_end=1016 _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_start=536 _globals['_PERMISSIONSPEC_REQUIREDTAGSENTRY']._serialized_end=587 _globals['_PERMISSIONSPEC_TAGSENTRY']._serialized_start=589 @@ -39,7 +39,7 @@ _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_start=635 _globals['_PERMISSIONSPEC_AUTHZEDACTION']._serialized_end=772 _globals['_PERMISSIONSPEC_TYPE']._serialized_start=775 - _globals['_PERMISSIONSPEC_TYPE']._serialized_end=1000 - _globals['_PERMISSIONMETA']._serialized_start=1003 - _globals['_PERMISSIONMETA']._serialized_end=1134 + _globals['_PERMISSIONSPEC_TYPE']._serialized_end=1016 + _globals['_PERMISSIONMETA']._serialized_start=1019 + _globals['_PERMISSIONMETA']._serialized_end=1150 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Permission_pb2.pyi b/sdk/python/feast/protos/feast/core/Permission_pb2.pyi index b2387d29465..9f9ec301728 100644 --- a/sdk/python/feast/protos/feast/core/Permission_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/Permission_pb2.pyi @@ -87,6 +87,7 @@ class PermissionSpec(google.protobuf.message.Message): SAVED_DATASET: PermissionSpec._Type.ValueType # 8 PERMISSION: PermissionSpec._Type.ValueType # 9 PROJECT: PermissionSpec._Type.ValueType # 10 + LABEL_VIEW: PermissionSpec._Type.ValueType # 11 class Type(_Type, metaclass=_TypeEnumTypeWrapper): ... FEATURE_VIEW: PermissionSpec.Type.ValueType # 0 @@ -100,6 +101,7 @@ class PermissionSpec(google.protobuf.message.Message): SAVED_DATASET: PermissionSpec.Type.ValueType # 8 PERMISSION: PermissionSpec.Type.ValueType # 9 PROJECT: PermissionSpec.Type.ValueType # 10 + LABEL_VIEW: PermissionSpec.Type.ValueType # 11 class RequiredTagsEntry(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor diff --git a/sdk/python/feast/protos/feast/core/PrecomputedFeatureVector_pb2.py b/sdk/python/feast/protos/feast/core/PrecomputedFeatureVector_pb2.py new file mode 100644 index 00000000000..f86f32238cb --- /dev/null +++ b/sdk/python/feast/protos/feast/core/PrecomputedFeatureVector_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: feast/core/PrecomputedFeatureVector.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)feast/core/PrecomputedFeatureVector.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17\x66\x65\x61st/types/Value.proto\"\xc2\x01\n\x18PrecomputedFeatureVector\x12\x15\n\rfeature_names\x18\x01 \x03(\t\x12\"\n\x06values\x18\x02 \x03(\x0b\x32\x12.feast.types.Value\x12\x37\n\rfv_timestamps\x18\x03 \x03(\x0b\x32 .feast.core.FeatureViewTimestamp\x12\x32\n\x0eprecomputed_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"f\n\x14\x46\x65\x61tureViewTimestamp\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x33\n\x0f\x65vent_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampBb\n\x10\x66\x65\x61st.proto.coreB\x1dPrecomputedFeatureVectorProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.core.PrecomputedFeatureVector_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\035PrecomputedFeatureVectorProtoZ/github.com/feast-dev/feast/go/protos/feast/core' + _globals['_PRECOMPUTEDFEATUREVECTOR']._serialized_start=116 + _globals['_PRECOMPUTEDFEATUREVECTOR']._serialized_end=310 + _globals['_FEATUREVIEWTIMESTAMP']._serialized_start=312 + _globals['_FEATUREVIEWTIMESTAMP']._serialized_end=414 +# @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/PrecomputedFeatureVector_pb2.pyi b/sdk/python/feast/protos/feast/core/PrecomputedFeatureVector_pb2.pyi new file mode 100644 index 00000000000..919f1b59973 --- /dev/null +++ b/sdk/python/feast/protos/feast/core/PrecomputedFeatureVector_pb2.pyi @@ -0,0 +1,76 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import collections.abc +import feast.types.Value_pb2 +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.timestamp_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class PrecomputedFeatureVector(google.protobuf.message.Message): + """A pre-computed feature vector stores all features for a FeatureService + as a single serialized blob per entity, enabling O(1) online retrieval. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_NAMES_FIELD_NUMBER: builtins.int + VALUES_FIELD_NUMBER: builtins.int + FV_TIMESTAMPS_FIELD_NUMBER: builtins.int + PRECOMPUTED_AT_FIELD_NUMBER: builtins.int + @property + def feature_names(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Fully-qualified feature names in deterministic order, e.g. "fv1__feat1".""" + @property + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.types.Value_pb2.Value]: + """Feature values in the same order as feature_names.""" + @property + def fv_timestamps(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___FeatureViewTimestamp]: + """Per-feature-view event timestamps for TTL enforcement at read time.""" + @property + def precomputed_at(self) -> google.protobuf.timestamp_pb2.Timestamp: + """Wall-clock time when this vector was assembled.""" + def __init__( + self, + *, + feature_names: collections.abc.Iterable[builtins.str] | None = ..., + values: collections.abc.Iterable[feast.types.Value_pb2.Value] | None = ..., + fv_timestamps: collections.abc.Iterable[global___FeatureViewTimestamp] | None = ..., + precomputed_at: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["precomputed_at", b"precomputed_at"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_names", b"feature_names", "fv_timestamps", b"fv_timestamps", "precomputed_at", b"precomputed_at", "values", b"values"]) -> None: ... + +global___PrecomputedFeatureVector = PrecomputedFeatureVector + +class FeatureViewTimestamp(google.protobuf.message.Message): + """Event timestamp associated with a specific feature view within the vector.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEATURE_VIEW_NAME_FIELD_NUMBER: builtins.int + EVENT_TIMESTAMP_FIELD_NUMBER: builtins.int + feature_view_name: builtins.str + @property + def event_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... + def __init__( + self, + *, + feature_view_name: builtins.str = ..., + event_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["event_timestamp", b"event_timestamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["event_timestamp", b"event_timestamp", "feature_view_name", b"feature_view_name"]) -> None: ... + +global___FeatureViewTimestamp = FeatureViewTimestamp diff --git a/sdk/python/feast/protos/feast/core/PrecomputedFeatureVector_pb2_grpc.py b/sdk/python/feast/protos/feast/core/PrecomputedFeatureVector_pb2_grpc.py new file mode 100644 index 00000000000..2daafffebfc --- /dev/null +++ b/sdk/python/feast/protos/feast/core/PrecomputedFeatureVector_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/sdk/python/feast/protos/feast/core/Registry_pb2.py b/sdk/python/feast/protos/feast/core/Registry_pb2.py index 671958d80c7..1c20c3ae654 100644 --- a/sdk/python/feast/protos/feast/core/Registry_pb2.py +++ b/sdk/python/feast/protos/feast/core/Registry_pb2.py @@ -25,9 +25,11 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 from feast.protos.feast.core import Permission_pb2 as feast_dot_core_dot_Permission__pb2 from feast.protos.feast.core import Project_pb2 as feast_dot_core_dot_Project__pb2 +from feast.protos.feast.core import FeatureViewVersion_pb2 as feast_dot_core_dot_FeatureViewVersion__pb2 +from feast.protos.feast.core import LabelView_pb2 as feast_dot_core_dot_LabelView__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19\x66\x65\x61st/core/Registry.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Entity.proto\x1a\x1f\x66\x65\x61st/core/FeatureService.proto\x1a\x1d\x66\x65\x61st/core/FeatureTable.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\x1c\x66\x65\x61st/core/InfraObject.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\"feast/core/StreamFeatureView.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1d\x66\x65\x61st/core/SavedDataset.proto\x1a\"feast/core/ValidationProfile.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/Permission.proto\x1a\x18\x66\x65\x61st/core/Project.proto\"\xff\x05\n\x08Registry\x12$\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x12.feast.core.Entity\x12\x30\n\x0e\x66\x65\x61ture_tables\x18\x02 \x03(\x0b\x32\x18.feast.core.FeatureTable\x12.\n\rfeature_views\x18\x06 \x03(\x0b\x32\x17.feast.core.FeatureView\x12,\n\x0c\x64\x61ta_sources\x18\x0c \x03(\x0b\x32\x16.feast.core.DataSource\x12@\n\x17on_demand_feature_views\x18\x08 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureView\x12;\n\x14stream_feature_views\x18\x0e \x03(\x0b\x32\x1d.feast.core.StreamFeatureView\x12\x34\n\x10\x66\x65\x61ture_services\x18\x07 \x03(\x0b\x32\x1a.feast.core.FeatureService\x12\x30\n\x0esaved_datasets\x18\x0b \x03(\x0b\x32\x18.feast.core.SavedDataset\x12>\n\x15validation_references\x18\r \x03(\x0b\x32\x1f.feast.core.ValidationReference\x12 \n\x05infra\x18\n \x01(\x0b\x32\x11.feast.core.Infra\x12\x39\n\x10project_metadata\x18\x0f \x03(\x0b\x32\x1b.feast.core.ProjectMetadataB\x02\x18\x01\x12\x1f\n\x17registry_schema_version\x18\x03 \x01(\t\x12\x12\n\nversion_id\x18\x04 \x01(\t\x12\x30\n\x0clast_updated\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12+\n\x0bpermissions\x18\x10 \x03(\x0b\x32\x16.feast.core.Permission\x12%\n\x08projects\x18\x11 \x03(\x0b\x32\x13.feast.core.Project\"8\n\x0fProjectMetadata\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x14\n\x0cproject_uuid\x18\x02 \x01(\tBR\n\x10\x66\x65\x61st.proto.coreB\rRegistryProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19\x66\x65\x61st/core/Registry.proto\x12\nfeast.core\x1a\x17\x66\x65\x61st/core/Entity.proto\x1a\x1f\x66\x65\x61st/core/FeatureService.proto\x1a\x1d\x66\x65\x61st/core/FeatureTable.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\x1c\x66\x65\x61st/core/InfraObject.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\"feast/core/StreamFeatureView.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1d\x66\x65\x61st/core/SavedDataset.proto\x1a\"feast/core/ValidationProfile.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/Permission.proto\x1a\x18\x66\x65\x61st/core/Project.proto\x1a#feast/core/FeatureViewVersion.proto\x1a\x1a\x66\x65\x61st/core/LabelView.proto\"\xf8\x06\n\x08Registry\x12$\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x12.feast.core.Entity\x12\x30\n\x0e\x66\x65\x61ture_tables\x18\x02 \x03(\x0b\x32\x18.feast.core.FeatureTable\x12.\n\rfeature_views\x18\x06 \x03(\x0b\x32\x17.feast.core.FeatureView\x12,\n\x0c\x64\x61ta_sources\x18\x0c \x03(\x0b\x32\x16.feast.core.DataSource\x12@\n\x17on_demand_feature_views\x18\x08 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureView\x12;\n\x14stream_feature_views\x18\x0e \x03(\x0b\x32\x1d.feast.core.StreamFeatureView\x12\x34\n\x10\x66\x65\x61ture_services\x18\x07 \x03(\x0b\x32\x1a.feast.core.FeatureService\x12\x30\n\x0esaved_datasets\x18\x0b \x03(\x0b\x32\x18.feast.core.SavedDataset\x12>\n\x15validation_references\x18\r \x03(\x0b\x32\x1f.feast.core.ValidationReference\x12 \n\x05infra\x18\n \x01(\x0b\x32\x11.feast.core.Infra\x12\x39\n\x10project_metadata\x18\x0f \x03(\x0b\x32\x1b.feast.core.ProjectMetadataB\x02\x18\x01\x12\x1f\n\x17registry_schema_version\x18\x03 \x01(\t\x12\x12\n\nversion_id\x18\x04 \x01(\t\x12\x30\n\x0clast_updated\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12+\n\x0bpermissions\x18\x10 \x03(\x0b\x32\x16.feast.core.Permission\x12%\n\x08projects\x18\x11 \x03(\x0b\x32\x13.feast.core.Project\x12K\n\x1c\x66\x65\x61ture_view_version_history\x18\x12 \x01(\x0b\x32%.feast.core.FeatureViewVersionHistory\x12*\n\x0blabel_views\x18\x13 \x03(\x0b\x32\x15.feast.core.LabelView\"8\n\x0fProjectMetadata\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x14\n\x0cproject_uuid\x18\x02 \x01(\tBR\n\x10\x66\x65\x61st.proto.coreB\rRegistryProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -37,8 +39,8 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\020feast.proto.coreB\rRegistryProtoZ/github.com/feast-dev/feast/go/protos/feast/core' _globals['_REGISTRY'].fields_by_name['project_metadata']._options = None _globals['_REGISTRY'].fields_by_name['project_metadata']._serialized_options = b'\030\001' - _globals['_REGISTRY']._serialized_start=449 - _globals['_REGISTRY']._serialized_end=1216 - _globals['_PROJECTMETADATA']._serialized_start=1218 - _globals['_PROJECTMETADATA']._serialized_end=1274 + _globals['_REGISTRY']._serialized_start=514 + _globals['_REGISTRY']._serialized_end=1402 + _globals['_PROJECTMETADATA']._serialized_start=1404 + _globals['_PROJECTMETADATA']._serialized_end=1460 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/Registry_pb2.pyi b/sdk/python/feast/protos/feast/core/Registry_pb2.pyi index fca49c75481..55c6e5600d8 100644 --- a/sdk/python/feast/protos/feast/core/Registry_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/Registry_pb2.pyi @@ -1,19 +1,19 @@ """ @generated by mypy-protobuf. Do not edit manually! isort:skip_file - -* Copyright 2020 The Feast Authors -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and + +* Copyright 2020 The Feast Authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and * limitations under the License. """ import builtins @@ -22,8 +22,10 @@ import feast.core.DataSource_pb2 import feast.core.Entity_pb2 import feast.core.FeatureService_pb2 import feast.core.FeatureTable_pb2 +import feast.core.FeatureViewVersion_pb2 import feast.core.FeatureView_pb2 import feast.core.InfraObject_pb2 +import feast.core.LabelView_pb2 import feast.core.OnDemandFeatureView_pb2 import feast.core.Permission_pb2 import feast.core.Project_pb2 @@ -44,7 +46,7 @@ else: DESCRIPTOR: google.protobuf.descriptor.FileDescriptor class Registry(google.protobuf.message.Message): - """Next id: 18""" + """Next id: 20""" DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -64,6 +66,8 @@ class Registry(google.protobuf.message.Message): LAST_UPDATED_FIELD_NUMBER: builtins.int PERMISSIONS_FIELD_NUMBER: builtins.int PROJECTS_FIELD_NUMBER: builtins.int + FEATURE_VIEW_VERSION_HISTORY_FIELD_NUMBER: builtins.int + LABEL_VIEWS_FIELD_NUMBER: builtins.int @property def entities(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Entity_pb2.Entity]: ... @property @@ -97,6 +101,10 @@ class Registry(google.protobuf.message.Message): def permissions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Permission_pb2.Permission]: ... @property def projects(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.Project_pb2.Project]: ... + @property + def feature_view_version_history(self) -> feast.core.FeatureViewVersion_pb2.FeatureViewVersionHistory: ... + @property + def label_views(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.LabelView_pb2.LabelView]: ... def __init__( self, *, @@ -116,9 +124,11 @@ class Registry(google.protobuf.message.Message): last_updated: google.protobuf.timestamp_pb2.Timestamp | None = ..., permissions: collections.abc.Iterable[feast.core.Permission_pb2.Permission] | None = ..., projects: collections.abc.Iterable[feast.core.Project_pb2.Project] | None = ..., + feature_view_version_history: feast.core.FeatureViewVersion_pb2.FeatureViewVersionHistory | None = ..., + label_views: collections.abc.Iterable[feast.core.LabelView_pb2.LabelView] | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["infra", b"infra", "last_updated", b"last_updated"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["data_sources", b"data_sources", "entities", b"entities", "feature_services", b"feature_services", "feature_tables", b"feature_tables", "feature_views", b"feature_views", "infra", b"infra", "last_updated", b"last_updated", "on_demand_feature_views", b"on_demand_feature_views", "permissions", b"permissions", "project_metadata", b"project_metadata", "projects", b"projects", "registry_schema_version", b"registry_schema_version", "saved_datasets", b"saved_datasets", "stream_feature_views", b"stream_feature_views", "validation_references", b"validation_references", "version_id", b"version_id"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["feature_view_version_history", b"feature_view_version_history", "infra", b"infra", "last_updated", b"last_updated"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["data_sources", b"data_sources", "entities", b"entities", "feature_services", b"feature_services", "feature_tables", b"feature_tables", "feature_view_version_history", b"feature_view_version_history", "feature_views", b"feature_views", "infra", b"infra", "label_views", b"label_views", "last_updated", b"last_updated", "on_demand_feature_views", b"on_demand_feature_views", "permissions", b"permissions", "project_metadata", b"project_metadata", "projects", b"projects", "registry_schema_version", b"registry_schema_version", "saved_datasets", b"saved_datasets", "stream_feature_views", b"stream_feature_views", "validation_references", b"validation_references", "version_id", b"version_id"]) -> None: ... global___Registry = Registry diff --git a/sdk/python/feast/protos/feast/core/SavedDataset_pb2.py b/sdk/python/feast/protos/feast/core/SavedDataset_pb2.py index fe1e2d49eac..8e394dec7ec 100644 --- a/sdk/python/feast/protos/feast/core/SavedDataset_pb2.py +++ b/sdk/python/feast/protos/feast/core/SavedDataset_pb2.py @@ -16,7 +16,7 @@ from feast.protos.feast.core import DataSource_pb2 as feast_dot_core_dot_DataSource__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x66\x65\x61st/core/SavedDataset.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\"\xa5\x02\n\x10SavedDatasetSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x66\x65\x61tures\x18\x03 \x03(\t\x12\x11\n\tjoin_keys\x18\x04 \x03(\t\x12\x1a\n\x12\x66ull_feature_names\x18\x05 \x01(\x08\x12\x30\n\x07storage\x18\x06 \x01(\x0b\x32\x1f.feast.core.SavedDatasetStorage\x12\x1c\n\x14\x66\x65\x61ture_service_name\x18\x08 \x01(\t\x12\x34\n\x04tags\x18\x07 \x03(\x0b\x32&.feast.core.SavedDatasetSpec.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa9\x04\n\x13SavedDatasetStorage\x12:\n\x0c\x66ile_storage\x18\x04 \x01(\x0b\x32\".feast.core.DataSource.FileOptionsH\x00\x12\x42\n\x10\x62igquery_storage\x18\x05 \x01(\x0b\x32&.feast.core.DataSource.BigQueryOptionsH\x00\x12\x42\n\x10redshift_storage\x18\x06 \x01(\x0b\x32&.feast.core.DataSource.RedshiftOptionsH\x00\x12\x44\n\x11snowflake_storage\x18\x07 \x01(\x0b\x32\'.feast.core.DataSource.SnowflakeOptionsH\x00\x12<\n\rtrino_storage\x18\x08 \x01(\x0b\x32#.feast.core.DataSource.TrinoOptionsH\x00\x12<\n\rspark_storage\x18\t \x01(\x0b\x32#.feast.core.DataSource.SparkOptionsH\x00\x12\x44\n\x0e\x63ustom_storage\x18\n \x01(\x0b\x32*.feast.core.DataSource.CustomSourceOptionsH\x00\x12>\n\x0e\x61thena_storage\x18\x0b \x01(\x0b\x32$.feast.core.DataSource.AthenaOptionsH\x00\x42\x06\n\x04kind\"\xf7\x01\n\x10SavedDatasetMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13min_event_timestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13max_event_timestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"f\n\x0cSavedDataset\x12*\n\x04spec\x18\x01 \x01(\x0b\x32\x1c.feast.core.SavedDatasetSpec\x12*\n\x04meta\x18\x02 \x01(\x0b\x32\x1c.feast.core.SavedDatasetMetaBV\n\x10\x66\x65\x61st.proto.coreB\x11SavedDatasetProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x66\x65\x61st/core/SavedDataset.proto\x12\nfeast.core\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\"\xe1\x02\n\x10SavedDatasetSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x66\x65\x61tures\x18\x03 \x03(\t\x12\x11\n\tjoin_keys\x18\x04 \x03(\t\x12\x1a\n\x12\x66ull_feature_names\x18\x05 \x01(\x08\x12\x30\n\x07storage\x18\x06 \x01(\x0b\x32\x1f.feast.core.SavedDatasetStorage\x12\x1c\n\x14\x66\x65\x61ture_service_name\x18\x08 \x01(\t\x12\x34\n\x04tags\x18\x07 \x03(\x0b\x32&.feast.core.SavedDatasetSpec.TagsEntry\x12\x11\n\tnamespace\x18\t \x01(\t\x12\x12\n\ncollection\x18\n \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x0b \x01(\t\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa9\x04\n\x13SavedDatasetStorage\x12:\n\x0c\x66ile_storage\x18\x04 \x01(\x0b\x32\".feast.core.DataSource.FileOptionsH\x00\x12\x42\n\x10\x62igquery_storage\x18\x05 \x01(\x0b\x32&.feast.core.DataSource.BigQueryOptionsH\x00\x12\x42\n\x10redshift_storage\x18\x06 \x01(\x0b\x32&.feast.core.DataSource.RedshiftOptionsH\x00\x12\x44\n\x11snowflake_storage\x18\x07 \x01(\x0b\x32\'.feast.core.DataSource.SnowflakeOptionsH\x00\x12<\n\rtrino_storage\x18\x08 \x01(\x0b\x32#.feast.core.DataSource.TrinoOptionsH\x00\x12<\n\rspark_storage\x18\t \x01(\x0b\x32#.feast.core.DataSource.SparkOptionsH\x00\x12\x44\n\x0e\x63ustom_storage\x18\n \x01(\x0b\x32*.feast.core.DataSource.CustomSourceOptionsH\x00\x12>\n\x0e\x61thena_storage\x18\x0b \x01(\x0b\x32$.feast.core.DataSource.AthenaOptionsH\x00\x42\x06\n\x04kind\"\xf7\x01\n\x10SavedDatasetMeta\x12\x35\n\x11\x63reated_timestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13min_event_timestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x37\n\x13max_event_timestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"f\n\x0cSavedDataset\x12*\n\x04spec\x18\x01 \x01(\x0b\x32\x1c.feast.core.SavedDatasetSpec\x12*\n\x04meta\x18\x02 \x01(\x0b\x32\x1c.feast.core.SavedDatasetMetaBV\n\x10\x66\x65\x61st.proto.coreB\x11SavedDatasetProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -27,13 +27,13 @@ _globals['_SAVEDDATASETSPEC_TAGSENTRY']._options = None _globals['_SAVEDDATASETSPEC_TAGSENTRY']._serialized_options = b'8\001' _globals['_SAVEDDATASETSPEC']._serialized_start=108 - _globals['_SAVEDDATASETSPEC']._serialized_end=401 - _globals['_SAVEDDATASETSPEC_TAGSENTRY']._serialized_start=358 - _globals['_SAVEDDATASETSPEC_TAGSENTRY']._serialized_end=401 - _globals['_SAVEDDATASETSTORAGE']._serialized_start=404 - _globals['_SAVEDDATASETSTORAGE']._serialized_end=957 - _globals['_SAVEDDATASETMETA']._serialized_start=960 - _globals['_SAVEDDATASETMETA']._serialized_end=1207 - _globals['_SAVEDDATASET']._serialized_start=1209 - _globals['_SAVEDDATASET']._serialized_end=1311 + _globals['_SAVEDDATASETSPEC']._serialized_end=461 + _globals['_SAVEDDATASETSPEC_TAGSENTRY']._serialized_start=418 + _globals['_SAVEDDATASETSPEC_TAGSENTRY']._serialized_end=461 + _globals['_SAVEDDATASETSTORAGE']._serialized_start=464 + _globals['_SAVEDDATASETSTORAGE']._serialized_end=1017 + _globals['_SAVEDDATASETMETA']._serialized_start=1020 + _globals['_SAVEDDATASETMETA']._serialized_end=1267 + _globals['_SAVEDDATASET']._serialized_start=1269 + _globals['_SAVEDDATASET']._serialized_end=1371 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/SavedDataset_pb2.pyi b/sdk/python/feast/protos/feast/core/SavedDataset_pb2.pyi index 47525b64ede..2fbf5e05271 100644 --- a/sdk/python/feast/protos/feast/core/SavedDataset_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/SavedDataset_pb2.pyi @@ -58,6 +58,9 @@ class SavedDatasetSpec(google.protobuf.message.Message): STORAGE_FIELD_NUMBER: builtins.int FEATURE_SERVICE_NAME_FIELD_NUMBER: builtins.int TAGS_FIELD_NUMBER: builtins.int + NAMESPACE_FIELD_NUMBER: builtins.int + COLLECTION_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int name: builtins.str """Name of the dataset. Must be unique since it's possible to overwrite dataset by name""" project: builtins.str @@ -77,6 +80,18 @@ class SavedDatasetSpec(google.protobuf.message.Message): @property def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: """User defined metadata""" + namespace: builtins.str + """Optional logical namespace for hierarchical grouping. + Maps to the top-level prefix in Iceberg REST Catalog API. + Empty string means not set (no namespace scoping). + """ + collection: builtins.str + """Optional sub-grouping within a namespace. + Maps to the namespace level in Iceberg REST Catalog API. + Empty string means not set (dataset sits directly under namespace). + """ + description: builtins.str + """Description of the saved dataset.""" def __init__( self, *, @@ -88,9 +103,12 @@ class SavedDatasetSpec(google.protobuf.message.Message): storage: global___SavedDatasetStorage | None = ..., feature_service_name: builtins.str = ..., tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + namespace: builtins.str = ..., + collection: builtins.str = ..., + description: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["storage", b"storage"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["feature_service_name", b"feature_service_name", "features", b"features", "full_feature_names", b"full_feature_names", "join_keys", b"join_keys", "name", b"name", "project", b"project", "storage", b"storage", "tags", b"tags"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["collection", b"collection", "description", b"description", "feature_service_name", b"feature_service_name", "features", b"features", "full_feature_names", b"full_feature_names", "join_keys", b"join_keys", "name", b"name", "namespace", b"namespace", "project", b"project", "storage", b"storage", "tags", b"tags"]) -> None: ... global___SavedDatasetSpec = SavedDatasetSpec diff --git a/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.py b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.py index ba19088edd6..9222c1bd2ae 100644 --- a/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.py +++ b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.py @@ -21,7 +21,7 @@ from feast.protos.feast.core import Transformation_pb2 as feast_dot_core_dot_Transformation__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/core/StreamFeatureView.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1c\x66\x65\x61st/core/Aggregation.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\"o\n\x11StreamFeatureView\x12/\n\x04spec\x18\x01 \x01(\x0b\x32!.feast.core.StreamFeatureViewSpec\x12)\n\x04meta\x18\x02 \x01(\x0b\x32\x1b.feast.core.FeatureViewMeta\"\xa8\x05\n\x15StreamFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x65ntities\x18\x03 \x03(\t\x12+\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x31\n\x0e\x65ntity_columns\x18\x05 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x39\n\x04tags\x18\x07 \x03(\x0b\x32+.feast.core.StreamFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12&\n\x03ttl\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12,\n\x0c\x62\x61tch_source\x18\n \x01(\x0b\x32\x16.feast.core.DataSource\x12-\n\rstream_source\x18\x0b \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0e\n\x06online\x18\x0c \x01(\x08\x12\x42\n\x15user_defined_function\x18\r \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x0c\n\x04mode\x18\x0e \x01(\t\x12-\n\x0c\x61ggregations\x18\x0f \x03(\x0b\x32\x17.feast.core.Aggregation\x12\x17\n\x0ftimestamp_field\x18\x10 \x01(\t\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\x11 \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42[\n\x10\x66\x65\x61st.proto.coreB\x16StreamFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/core/StreamFeatureView.proto\x12\nfeast.core\x1a\x1egoogle/protobuf/duration.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\x18\x66\x65\x61st/core/Feature.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1c\x66\x65\x61st/core/Aggregation.proto\x1a\x1f\x66\x65\x61st/core/Transformation.proto\"o\n\x11StreamFeatureView\x12/\n\x04spec\x18\x01 \x01(\x0b\x32!.feast.core.StreamFeatureViewSpec\x12)\n\x04meta\x18\x02 \x01(\x0b\x32\x1b.feast.core.FeatureViewMeta\"\xbe\x06\n\x15StreamFeatureViewSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x10\n\x08\x65ntities\x18\x03 \x03(\t\x12+\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x31\n\x0e\x65ntity_columns\x18\x05 \x03(\x0b\x32\x19.feast.core.FeatureSpecV2\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x39\n\x04tags\x18\x07 \x03(\x0b\x32+.feast.core.StreamFeatureViewSpec.TagsEntry\x12\r\n\x05owner\x18\x08 \x01(\t\x12&\n\x03ttl\x18\t \x01(\x0b\x32\x19.google.protobuf.Duration\x12,\n\x0c\x62\x61tch_source\x18\n \x01(\x0b\x32\x16.feast.core.DataSource\x12-\n\rstream_source\x18\x0b \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0e\n\x06online\x18\x0c \x01(\x08\x12\x42\n\x15user_defined_function\x18\r \x01(\x0b\x32\x1f.feast.core.UserDefinedFunctionB\x02\x18\x01\x12\x0c\n\x04mode\x18\x0e \x01(\t\x12-\n\x0c\x61ggregations\x18\x0f \x03(\x0b\x32\x17.feast.core.Aggregation\x12\x17\n\x0ftimestamp_field\x18\x10 \x01(\t\x12\x43\n\x16\x66\x65\x61ture_transformation\x18\x11 \x01(\x0b\x32#.feast.core.FeatureTransformationV2\x12\x15\n\renable_tiling\x18\x12 \x01(\x08\x12\x32\n\x0ftiling_hop_size\x18\x13 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x19\n\x11\x65nable_validation\x18\x14 \x01(\x08\x12\x0f\n\x07version\x18\x15 \x01(\t\x12\x0b\n\x03org\x18\x16 \x01(\t\x12\x10\n\x08\x64isabled\x18\x17 \x01(\x08\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42[\n\x10\x66\x65\x61st.proto.coreB\x16StreamFeatureViewProtoZ/github.com/feast-dev/feast/go/protos/feast/coreb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,7 +36,7 @@ _globals['_STREAMFEATUREVIEW']._serialized_start=268 _globals['_STREAMFEATUREVIEW']._serialized_end=379 _globals['_STREAMFEATUREVIEWSPEC']._serialized_start=382 - _globals['_STREAMFEATUREVIEWSPEC']._serialized_end=1062 - _globals['_STREAMFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=1019 - _globals['_STREAMFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=1062 + _globals['_STREAMFEATUREVIEWSPEC']._serialized_end=1212 + _globals['_STREAMFEATUREVIEWSPEC_TAGSENTRY']._serialized_start=1169 + _globals['_STREAMFEATUREVIEWSPEC_TAGSENTRY']._serialized_end=1212 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.pyi b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.pyi index 70e897a2f21..3fa504654f4 100644 --- a/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.pyi +++ b/sdk/python/feast/protos/feast/core/StreamFeatureView_pb2.pyi @@ -59,7 +59,7 @@ class StreamFeatureView(google.protobuf.message.Message): global___StreamFeatureView = StreamFeatureView class StreamFeatureViewSpec(google.protobuf.message.Message): - """Next available id: 17""" + """Next available id: 23""" DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -95,6 +95,12 @@ class StreamFeatureViewSpec(google.protobuf.message.Message): AGGREGATIONS_FIELD_NUMBER: builtins.int TIMESTAMP_FIELD_FIELD_NUMBER: builtins.int FEATURE_TRANSFORMATION_FIELD_NUMBER: builtins.int + ENABLE_TILING_FIELD_NUMBER: builtins.int + TILING_HOP_SIZE_FIELD_NUMBER: builtins.int + ENABLE_VALIDATION_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + ORG_FIELD_NUMBER: builtins.int + DISABLED_FIELD_NUMBER: builtins.int name: builtins.str """Name of the feature view. Must be unique. Not updated.""" project: builtins.str @@ -143,6 +149,24 @@ class StreamFeatureViewSpec(google.protobuf.message.Message): @property def feature_transformation(self) -> feast.core.Transformation_pb2.FeatureTransformationV2: """Oneof with {user_defined_function, on_demand_substrait_transformation}""" + enable_tiling: builtins.bool + """Enable tiling for efficient window aggregation""" + @property + def tiling_hop_size(self) -> google.protobuf.duration_pb2.Duration: + """Hop size for tiling (e.g., 5 minutes). Determines the granularity of pre-aggregated tiles. + If not specified, defaults to 5 minutes. Only used when enable_tiling is true. + """ + enable_validation: builtins.bool + """Whether schema validation is enabled during materialization""" + version: builtins.str + """User-specified version pin (e.g. "latest", "v2", "version2")""" + org: builtins.str + """Organizational unit that owns this stream feature view (e.g. "ads", "search").""" + disabled: builtins.bool + """Whether this feature view is disabled for serving and materialization. + When true, the feature view will not serve online features or be materialized. + Defaults to false (enabled) for backward compatibility. + """ def __init__( self, *, @@ -163,8 +187,14 @@ class StreamFeatureViewSpec(google.protobuf.message.Message): aggregations: collections.abc.Iterable[feast.core.Aggregation_pb2.Aggregation] | None = ..., timestamp_field: builtins.str = ..., feature_transformation: feast.core.Transformation_pb2.FeatureTransformationV2 | None = ..., + enable_tiling: builtins.bool = ..., + tiling_hop_size: google.protobuf.duration_pb2.Duration | None = ..., + enable_validation: builtins.bool = ..., + version: builtins.str = ..., + org: builtins.str = ..., + disabled: builtins.bool = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "feature_transformation", b"feature_transformation", "stream_source", b"stream_source", "ttl", b"ttl", "user_defined_function", b"user_defined_function"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["aggregations", b"aggregations", "batch_source", b"batch_source", "description", b"description", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "online", b"online", "owner", b"owner", "project", b"project", "stream_source", b"stream_source", "tags", b"tags", "timestamp_field", b"timestamp_field", "ttl", b"ttl", "user_defined_function", b"user_defined_function"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["batch_source", b"batch_source", "feature_transformation", b"feature_transformation", "stream_source", b"stream_source", "tiling_hop_size", b"tiling_hop_size", "ttl", b"ttl", "user_defined_function", b"user_defined_function"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["aggregations", b"aggregations", "batch_source", b"batch_source", "description", b"description", "disabled", b"disabled", "enable_tiling", b"enable_tiling", "enable_validation", b"enable_validation", "entities", b"entities", "entity_columns", b"entity_columns", "feature_transformation", b"feature_transformation", "features", b"features", "mode", b"mode", "name", b"name", "online", b"online", "org", b"org", "owner", b"owner", "project", b"project", "stream_source", b"stream_source", "tags", b"tags", "tiling_hop_size", b"tiling_hop_size", "timestamp_field", b"timestamp_field", "ttl", b"ttl", "user_defined_function", b"user_defined_function", "version", b"version"]) -> None: ... global___StreamFeatureViewSpec = StreamFeatureViewSpec diff --git a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py index 8b346fcd672..95e5c449912 100644 --- a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py +++ b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.py @@ -24,11 +24,12 @@ from feast.protos.feast.core import SavedDataset_pb2 as feast_dot_core_dot_SavedDataset__pb2 from feast.protos.feast.core import ValidationProfile_pb2 as feast_dot_core_dot_ValidationProfile__pb2 from feast.protos.feast.core import InfraObject_pb2 as feast_dot_core_dot_InfraObject__pb2 +from feast.protos.feast.core import LabelView_pb2 as feast_dot_core_dot_LabelView__pb2 from feast.protos.feast.core import Permission_pb2 as feast_dot_core_dot_Permission__pb2 from feast.protos.feast.core import Project_pb2 as feast_dot_core_dot_Project__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#feast/registry/RegistryServer.proto\x12\x0e\x66\x65\x61st.registry\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x19\x66\x65\x61st/core/Registry.proto\x1a\x17\x66\x65\x61st/core/Entity.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\"feast/core/StreamFeatureView.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\x1f\x66\x65\x61st/core/FeatureService.proto\x1a\x1d\x66\x65\x61st/core/SavedDataset.proto\x1a\"feast/core/ValidationProfile.proto\x1a\x1c\x66\x65\x61st/core/InfraObject.proto\x1a\x1b\x66\x65\x61st/core/Permission.proto\x1a\x18\x66\x65\x61st/core/Project.proto\"/\n\x10PaginationParams\x12\x0c\n\x04page\x18\x01 \x01(\x05\x12\r\n\x05limit\x18\x02 \x01(\x05\"4\n\rSortingParams\x12\x0f\n\x07sort_by\x18\x01 \x01(\t\x12\x12\n\nsort_order\x18\x02 \x01(\t\"\x83\x01\n\x12PaginationMetadata\x12\x0c\n\x04page\x18\x01 \x01(\x05\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x13\n\x0btotal_count\x18\x03 \x01(\x05\x12\x13\n\x0btotal_pages\x18\x04 \x01(\x05\x12\x10\n\x08has_next\x18\x05 \x01(\x08\x12\x14\n\x0chas_previous\x18\x06 \x01(\x08\"!\n\x0eRefreshRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\"W\n\x12UpdateInfraRequest\x12 \n\x05infra\x18\x01 \x01(\x0b\x32\x11.feast.core.Infra\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"7\n\x0fGetInfraRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"B\n\x1aListProjectMetadataRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"T\n\x1bListProjectMetadataResponse\x12\x35\n\x10project_metadata\x18\x01 \x03(\x0b\x32\x1b.feast.core.ProjectMetadata\"\xcb\x01\n\x1b\x41pplyMaterializationRequest\x12-\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureView\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\nstart_date\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_date\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\"Y\n\x12\x41pplyEntityRequest\x12\"\n\x06\x65ntity\x18\x01 \x01(\x0b\x32\x12.feast.core.Entity\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"F\n\x10GetEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x8b\x02\n\x13ListEntitiesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12;\n\x04tags\x18\x03 \x03(\x0b\x32-.feast.registry.ListEntitiesRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"t\n\x14ListEntitiesResponse\x12$\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x12.feast.core.Entity\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"D\n\x13\x44\x65leteEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"f\n\x16\x41pplyDataSourceRequest\x12+\n\x0b\x64\x61ta_source\x18\x01 \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x91\x02\n\x16ListDataSourcesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListDataSourcesRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x7f\n\x17ListDataSourcesResponse\x12,\n\x0c\x64\x61ta_sources\x18\x01 \x03(\x0b\x32\x16.feast.core.DataSource\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"H\n\x17\x44\x65leteDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\x81\x02\n\x17\x41pplyFeatureViewRequest\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x12\x0f\n\x07project\x18\x04 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\x42\x13\n\x11\x62\x61se_feature_view\"K\n\x15GetFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x93\x02\n\x17ListFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12?\n\x04tags\x18\x03 \x03(\x0b\x32\x31.feast.registry.ListFeatureViewsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x82\x01\n\x18ListFeatureViewsResponse\x12.\n\rfeature_views\x18\x01 \x03(\x0b\x32\x17.feast.core.FeatureView\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"I\n\x18\x44\x65leteFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\xd6\x01\n\x0e\x41nyFeatureView\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x42\x12\n\x10\x61ny_feature_view\"N\n\x18GetAnyFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"U\n\x19GetAnyFeatureViewResponse\x12\x38\n\x10\x61ny_feature_view\x18\x01 \x01(\x0b\x32\x1e.feast.registry.AnyFeatureView\"\xe8\x02\n\x1aListAllFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListAllFeatureViewsRequest.TagsEntry\x12\x0e\n\x06\x65ntity\x18\x04 \x01(\t\x12\x0f\n\x07\x66\x65\x61ture\x18\x05 \x01(\t\x12\x17\n\x0f\x66\x65\x61ture_service\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x61ta_source\x18\x07 \x01(\t\x12\x34\n\npagination\x18\x08 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\t \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x01\n\x1bListAllFeatureViewsResponse\x12\x35\n\rfeature_views\x18\x01 \x03(\x0b\x32\x1e.feast.registry.AnyFeatureView\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"Q\n\x1bGetStreamFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x9f\x02\n\x1dListStreamFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x45\n\x04tags\x18\x03 \x03(\x0b\x32\x37.feast.registry.ListStreamFeatureViewsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x95\x01\n\x1eListStreamFeatureViewsResponse\x12;\n\x14stream_feature_views\x18\x01 \x03(\x0b\x32\x1d.feast.core.StreamFeatureView\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"S\n\x1dGetOnDemandFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xa3\x02\n\x1fListOnDemandFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListOnDemandFeatureViewsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9c\x01\n ListOnDemandFeatureViewsResponse\x12@\n\x17on_demand_feature_views\x18\x01 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureView\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"r\n\x1a\x41pplyFeatureServiceRequest\x12\x33\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\x0b\x32\x1a.feast.core.FeatureService\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"N\n\x18GetFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xaf\x02\n\x1aListFeatureServicesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListFeatureServicesRequest.TagsEntry\x12\x14\n\x0c\x66\x65\x61ture_view\x18\x04 \x01(\t\x12\x34\n\npagination\x18\x05 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x06 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8b\x01\n\x1bListFeatureServicesResponse\x12\x34\n\x10\x66\x65\x61ture_services\x18\x01 \x03(\x0b\x32\x1a.feast.core.FeatureService\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"L\n\x1b\x44\x65leteFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"l\n\x18\x41pplySavedDatasetRequest\x12/\n\rsaved_dataset\x18\x01 \x01(\x0b\x32\x18.feast.core.SavedDataset\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"L\n\x16GetSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x95\x02\n\x18ListSavedDatasetsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12@\n\x04tags\x18\x03 \x03(\x0b\x32\x32.feast.registry.ListSavedDatasetsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x85\x01\n\x19ListSavedDatasetsResponse\x12\x30\n\x0esaved_datasets\x18\x01 \x03(\x0b\x32\x18.feast.core.SavedDataset\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"J\n\x19\x44\x65leteSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\x81\x01\n\x1f\x41pplyValidationReferenceRequest\x12=\n\x14validation_reference\x18\x01 \x01(\x0b\x32\x1f.feast.core.ValidationReference\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"S\n\x1dGetValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xa3\x02\n\x1fListValidationReferencesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListValidationReferencesRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9a\x01\n ListValidationReferencesResponse\x12>\n\x15validation_references\x18\x01 \x03(\x0b\x32\x1f.feast.core.ValidationReference\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"Q\n DeleteValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"e\n\x16\x41pplyPermissionRequest\x12*\n\npermission\x18\x01 \x01(\x0b\x32\x16.feast.core.Permission\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetPermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x91\x02\n\x16ListPermissionsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListPermissionsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"~\n\x17ListPermissionsResponse\x12+\n\x0bpermissions\x18\x01 \x03(\x0b\x32\x16.feast.core.Permission\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"H\n\x17\x44\x65letePermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"K\n\x13\x41pplyProjectRequest\x12$\n\x07project\x18\x01 \x01(\x0b\x32\x13.feast.core.Project\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\"6\n\x11GetProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"\xfa\x01\n\x13ListProjectsRequest\x12\x13\n\x0b\x61llow_cache\x18\x01 \x01(\x08\x12;\n\x04tags\x18\x02 \x03(\x0b\x32-.feast.registry.ListProjectsRequest.TagsEntry\x12\x34\n\npagination\x18\x03 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x04 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"u\n\x14ListProjectsResponse\x12%\n\x08projects\x18\x01 \x03(\x0b\x32\x13.feast.core.Project\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"4\n\x14\x44\x65leteProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\"-\n\x0f\x45ntityReference\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"r\n\x0e\x45ntityRelation\x12/\n\x06source\x18\x01 \x01(\x0b\x32\x1f.feast.registry.EntityReference\x12/\n\x06target\x18\x02 \x01(\x0b\x32\x1f.feast.registry.EntityReference\"\xdf\x01\n\x19GetRegistryLineageRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x1a\n\x12\x66ilter_object_type\x18\x03 \x01(\t\x12\x1a\n\x12\x66ilter_object_name\x18\x04 \x01(\t\x12\x34\n\npagination\x18\x05 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x06 \x01(\x0b\x32\x1d.feast.registry.SortingParams\"\xa8\x02\n\x1aGetRegistryLineageResponse\x12\x35\n\rrelationships\x18\x01 \x03(\x0b\x32\x1e.feast.registry.EntityRelation\x12>\n\x16indirect_relationships\x18\x02 \x03(\x0b\x32\x1e.feast.registry.EntityRelation\x12\x44\n\x18relationships_pagination\x18\x03 \x01(\x0b\x32\".feast.registry.PaginationMetadata\x12M\n!indirect_relationships_pagination\x18\x04 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"\xef\x01\n\x1dGetObjectRelationshipsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0bobject_type\x18\x02 \x01(\t\x12\x13\n\x0bobject_name\x18\x03 \x01(\t\x12\x18\n\x10include_indirect\x18\x04 \x01(\x08\x12\x13\n\x0b\x61llow_cache\x18\x05 \x01(\x08\x12\x34\n\npagination\x18\x06 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x07 \x01(\x0b\x32\x1d.feast.registry.SortingParams\"\x8f\x01\n\x1eGetObjectRelationshipsResponse\x12\x35\n\rrelationships\x18\x01 \x03(\x0b\x32\x1e.feast.registry.EntityRelation\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"\xb0\x02\n\x07\x46\x65\x61ture\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_view\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\r\n\x05owner\x18\x05 \x01(\t\x12\x35\n\x11\x63reated_timestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x04tags\x18\x08 \x03(\x0b\x32!.feast.registry.Feature.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc5\x01\n\x13ListFeaturesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_view\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x06 \x01(\x08\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\"y\n\x14ListFeaturesResponse\x12)\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0b\x32\x17.feast.registry.Feature\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"]\n\x11GetFeatureRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_view\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x04 \x01(\x08\x32\xde#\n\x0eRegistryServer\x12K\n\x0b\x41pplyEntity\x12\".feast.registry.ApplyEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\tGetEntity\x12 .feast.registry.GetEntityRequest\x1a\x12.feast.core.Entity\"\x00\x12[\n\x0cListEntities\x12#.feast.registry.ListEntitiesRequest\x1a$.feast.registry.ListEntitiesResponse\"\x00\x12M\n\x0c\x44\x65leteEntity\x12#.feast.registry.DeleteEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyDataSource\x12&.feast.registry.ApplyDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetDataSource\x12$.feast.registry.GetDataSourceRequest\x1a\x16.feast.core.DataSource\"\x00\x12\x64\n\x0fListDataSources\x12&.feast.registry.ListDataSourcesRequest\x1a\'.feast.registry.ListDataSourcesResponse\"\x00\x12U\n\x10\x44\x65leteDataSource\x12\'.feast.registry.DeleteDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x10\x41pplyFeatureView\x12\'.feast.registry.ApplyFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x44\x65leteFeatureView\x12(.feast.registry.DeleteFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x11GetAnyFeatureView\x12(.feast.registry.GetAnyFeatureViewRequest\x1a).feast.registry.GetAnyFeatureViewResponse\"\x00\x12p\n\x13ListAllFeatureViews\x12*.feast.registry.ListAllFeatureViewsRequest\x1a+.feast.registry.ListAllFeatureViewsResponse\"\x00\x12R\n\x0eGetFeatureView\x12%.feast.registry.GetFeatureViewRequest\x1a\x17.feast.core.FeatureView\"\x00\x12g\n\x10ListFeatureViews\x12\'.feast.registry.ListFeatureViewsRequest\x1a(.feast.registry.ListFeatureViewsResponse\"\x00\x12\x64\n\x14GetStreamFeatureView\x12+.feast.registry.GetStreamFeatureViewRequest\x1a\x1d.feast.core.StreamFeatureView\"\x00\x12y\n\x16ListStreamFeatureViews\x12-.feast.registry.ListStreamFeatureViewsRequest\x1a..feast.registry.ListStreamFeatureViewsResponse\"\x00\x12j\n\x16GetOnDemandFeatureView\x12-.feast.registry.GetOnDemandFeatureViewRequest\x1a\x1f.feast.core.OnDemandFeatureView\"\x00\x12\x7f\n\x18ListOnDemandFeatureViews\x12/.feast.registry.ListOnDemandFeatureViewsRequest\x1a\x30.feast.registry.ListOnDemandFeatureViewsResponse\"\x00\x12[\n\x13\x41pplyFeatureService\x12*.feast.registry.ApplyFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x11GetFeatureService\x12(.feast.registry.GetFeatureServiceRequest\x1a\x1a.feast.core.FeatureService\"\x00\x12p\n\x13ListFeatureServices\x12*.feast.registry.ListFeatureServicesRequest\x1a+.feast.registry.ListFeatureServicesResponse\"\x00\x12]\n\x14\x44\x65leteFeatureService\x12+.feast.registry.DeleteFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x41pplySavedDataset\x12(.feast.registry.ApplySavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x0fGetSavedDataset\x12&.feast.registry.GetSavedDatasetRequest\x1a\x18.feast.core.SavedDataset\"\x00\x12j\n\x11ListSavedDatasets\x12(.feast.registry.ListSavedDatasetsRequest\x1a).feast.registry.ListSavedDatasetsResponse\"\x00\x12Y\n\x12\x44\x65leteSavedDataset\x12).feast.registry.DeleteSavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x65\n\x18\x41pplyValidationReference\x12/.feast.registry.ApplyValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x16GetValidationReference\x12-.feast.registry.GetValidationReferenceRequest\x1a\x1f.feast.core.ValidationReference\"\x00\x12\x7f\n\x18ListValidationReferences\x12/.feast.registry.ListValidationReferencesRequest\x1a\x30.feast.registry.ListValidationReferencesResponse\"\x00\x12g\n\x19\x44\x65leteValidationReference\x12\x30.feast.registry.DeleteValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyPermission\x12&.feast.registry.ApplyPermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetPermission\x12$.feast.registry.GetPermissionRequest\x1a\x16.feast.core.Permission\"\x00\x12\x64\n\x0fListPermissions\x12&.feast.registry.ListPermissionsRequest\x1a\'.feast.registry.ListPermissionsResponse\"\x00\x12U\n\x10\x44\x65letePermission\x12\'.feast.registry.DeletePermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12M\n\x0c\x41pplyProject\x12#.feast.registry.ApplyProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x46\n\nGetProject\x12!.feast.registry.GetProjectRequest\x1a\x13.feast.core.Project\"\x00\x12[\n\x0cListProjects\x12#.feast.registry.ListProjectsRequest\x1a$.feast.registry.ListProjectsResponse\"\x00\x12O\n\rDeleteProject\x12$.feast.registry.DeleteProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x14\x41pplyMaterialization\x12+.feast.registry.ApplyMaterializationRequest\x1a\x16.google.protobuf.Empty\"\x00\x12p\n\x13ListProjectMetadata\x12*.feast.registry.ListProjectMetadataRequest\x1a+.feast.registry.ListProjectMetadataResponse\"\x00\x12K\n\x0bUpdateInfra\x12\".feast.registry.UpdateInfraRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x08GetInfra\x12\x1f.feast.registry.GetInfraRequest\x1a\x11.feast.core.Infra\"\x00\x12:\n\x06\x43ommit\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x07Refresh\x12\x1e.feast.registry.RefreshRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x37\n\x05Proto\x12\x16.google.protobuf.Empty\x1a\x14.feast.core.Registry\"\x00\x12m\n\x12GetRegistryLineage\x12).feast.registry.GetRegistryLineageRequest\x1a*.feast.registry.GetRegistryLineageResponse\"\x00\x12y\n\x16GetObjectRelationships\x12-.feast.registry.GetObjectRelationshipsRequest\x1a..feast.registry.GetObjectRelationshipsResponse\"\x00\x12[\n\x0cListFeatures\x12#.feast.registry.ListFeaturesRequest\x1a$.feast.registry.ListFeaturesResponse\"\x00\x12J\n\nGetFeature\x12!.feast.registry.GetFeatureRequest\x1a\x17.feast.registry.Feature\"\x00\x42\x35Z3github.com/feast-dev/feast/go/protos/feast/registryb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#feast/registry/RegistryServer.proto\x12\x0e\x66\x65\x61st.registry\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x19\x66\x65\x61st/core/Registry.proto\x1a\x17\x66\x65\x61st/core/Entity.proto\x1a\x1b\x66\x65\x61st/core/DataSource.proto\x1a\x1c\x66\x65\x61st/core/FeatureView.proto\x1a\"feast/core/StreamFeatureView.proto\x1a$feast/core/OnDemandFeatureView.proto\x1a\x1f\x66\x65\x61st/core/FeatureService.proto\x1a\x1d\x66\x65\x61st/core/SavedDataset.proto\x1a\"feast/core/ValidationProfile.proto\x1a\x1c\x66\x65\x61st/core/InfraObject.proto\x1a\x1a\x66\x65\x61st/core/LabelView.proto\x1a\x1b\x66\x65\x61st/core/Permission.proto\x1a\x18\x66\x65\x61st/core/Project.proto\"/\n\x10PaginationParams\x12\x0c\n\x04page\x18\x01 \x01(\x05\x12\r\n\x05limit\x18\x02 \x01(\x05\"4\n\rSortingParams\x12\x0f\n\x07sort_by\x18\x01 \x01(\t\x12\x12\n\nsort_order\x18\x02 \x01(\t\"\x83\x01\n\x12PaginationMetadata\x12\x0c\n\x04page\x18\x01 \x01(\x05\x12\r\n\x05limit\x18\x02 \x01(\x05\x12\x13\n\x0btotal_count\x18\x03 \x01(\x05\x12\x13\n\x0btotal_pages\x18\x04 \x01(\x05\x12\x10\n\x08has_next\x18\x05 \x01(\x08\x12\x14\n\x0chas_previous\x18\x06 \x01(\x08\"!\n\x0eRefreshRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\"W\n\x12UpdateInfraRequest\x12 \n\x05infra\x18\x01 \x01(\x0b\x32\x11.feast.core.Infra\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"7\n\x0fGetInfraRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"B\n\x1aListProjectMetadataRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"T\n\x1bListProjectMetadataResponse\x12\x35\n\x10project_metadata\x18\x01 \x03(\x0b\x32\x1b.feast.core.ProjectMetadata\"\xcb\x01\n\x1b\x41pplyMaterializationRequest\x12-\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureView\x12\x0f\n\x07project\x18\x02 \x01(\t\x12.\n\nstart_date\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12,\n\x08\x65nd_date\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\"Y\n\x12\x41pplyEntityRequest\x12\"\n\x06\x65ntity\x18\x01 \x01(\x0b\x32\x12.feast.core.Entity\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"F\n\x10GetEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x8b\x02\n\x13ListEntitiesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12;\n\x04tags\x18\x03 \x03(\x0b\x32-.feast.registry.ListEntitiesRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"t\n\x14ListEntitiesResponse\x12$\n\x08\x65ntities\x18\x01 \x03(\x0b\x32\x12.feast.core.Entity\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"D\n\x13\x44\x65leteEntityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"f\n\x16\x41pplyDataSourceRequest\x12+\n\x0b\x64\x61ta_source\x18\x01 \x01(\x0b\x32\x16.feast.core.DataSource\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x91\x02\n\x16ListDataSourcesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListDataSourcesRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x7f\n\x17ListDataSourcesResponse\x12,\n\x0c\x64\x61ta_sources\x18\x01 \x03(\x0b\x32\x16.feast.core.DataSource\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"H\n\x17\x44\x65leteDataSourceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\xae\x02\n\x17\x41pplyFeatureViewRequest\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x12+\n\nlabel_view\x18\x06 \x01(\x0b\x32\x15.feast.core.LabelViewH\x00\x12\x0f\n\x07project\x18\x04 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x05 \x01(\x08\x42\x13\n\x11\x62\x61se_feature_view\"K\n\x15GetFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x93\x02\n\x17ListFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12?\n\x04tags\x18\x03 \x03(\x0b\x32\x31.feast.registry.ListFeatureViewsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x82\x01\n\x18ListFeatureViewsResponse\x12.\n\rfeature_views\x18\x01 \x03(\x0b\x32\x17.feast.core.FeatureView\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"I\n\x18\x44\x65leteFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\x83\x02\n\x0e\x41nyFeatureView\x12/\n\x0c\x66\x65\x61ture_view\x18\x01 \x01(\x0b\x32\x17.feast.core.FeatureViewH\x00\x12\x41\n\x16on_demand_feature_view\x18\x02 \x01(\x0b\x32\x1f.feast.core.OnDemandFeatureViewH\x00\x12<\n\x13stream_feature_view\x18\x03 \x01(\x0b\x32\x1d.feast.core.StreamFeatureViewH\x00\x12+\n\nlabel_view\x18\x04 \x01(\x0b\x32\x15.feast.core.LabelViewH\x00\x42\x12\n\x10\x61ny_feature_view\"N\n\x18GetAnyFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"U\n\x19GetAnyFeatureViewResponse\x12\x38\n\x10\x61ny_feature_view\x18\x01 \x01(\x0b\x32\x1e.feast.registry.AnyFeatureView\"\x9b\x03\n\x1aListAllFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListAllFeatureViewsRequest.TagsEntry\x12\x0e\n\x06\x65ntity\x18\x04 \x01(\t\x12\x0f\n\x07\x66\x65\x61ture\x18\x05 \x01(\t\x12\x17\n\x0f\x66\x65\x61ture_service\x18\x06 \x01(\t\x12\x13\n\x0b\x64\x61ta_source\x18\x07 \x01(\t\x12\x34\n\npagination\x18\x08 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\t \x01(\x0b\x32\x1d.feast.registry.SortingParams\x12\x31\n\rupdated_since\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x01\n\x1bListAllFeatureViewsResponse\x12\x35\n\rfeature_views\x18\x01 \x03(\x0b\x32\x1e.feast.registry.AnyFeatureView\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"Q\n\x1bGetStreamFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x9f\x02\n\x1dListStreamFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x45\n\x04tags\x18\x03 \x03(\x0b\x32\x37.feast.registry.ListStreamFeatureViewsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x95\x01\n\x1eListStreamFeatureViewsResponse\x12;\n\x14stream_feature_views\x18\x01 \x03(\x0b\x32\x1d.feast.core.StreamFeatureView\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"S\n\x1dGetOnDemandFeatureViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xa3\x02\n\x1fListOnDemandFeatureViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListOnDemandFeatureViewsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9c\x01\n ListOnDemandFeatureViewsResponse\x12@\n\x17on_demand_feature_views\x18\x01 \x03(\x0b\x32\x1f.feast.core.OnDemandFeatureView\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"I\n\x13GetLabelViewRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x8f\x02\n\x15ListLabelViewsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12=\n\x04tags\x18\x03 \x03(\x0b\x32/.feast.registry.ListLabelViewsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"|\n\x16ListLabelViewsResponse\x12*\n\x0blabel_views\x18\x01 \x03(\x0b\x32\x15.feast.core.LabelView\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"r\n\x1a\x41pplyFeatureServiceRequest\x12\x33\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\x0b\x32\x1a.feast.core.FeatureService\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"N\n\x18GetFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xaf\x02\n\x1aListFeatureServicesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x42\n\x04tags\x18\x03 \x03(\x0b\x32\x34.feast.registry.ListFeatureServicesRequest.TagsEntry\x12\x14\n\x0c\x66\x65\x61ture_view\x18\x04 \x01(\t\x12\x34\n\npagination\x18\x05 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x06 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8b\x01\n\x1bListFeatureServicesResponse\x12\x34\n\x10\x66\x65\x61ture_services\x18\x01 \x03(\x0b\x32\x1a.feast.core.FeatureService\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"L\n\x1b\x44\x65leteFeatureServiceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"l\n\x18\x41pplySavedDatasetRequest\x12/\n\rsaved_dataset\x18\x01 \x01(\x0b\x32\x18.feast.core.SavedDataset\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"L\n\x16GetSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xbc\x02\n\x18ListSavedDatasetsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12@\n\x04tags\x18\x03 \x03(\x0b\x32\x32.feast.registry.ListSavedDatasetsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x12\x11\n\tnamespace\x18\x06 \x01(\t\x12\x12\n\ncollection\x18\x07 \x01(\t\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x85\x01\n\x19ListSavedDatasetsResponse\x12\x30\n\x0esaved_datasets\x18\x01 \x03(\x0b\x32\x18.feast.core.SavedDataset\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"J\n\x19\x44\x65leteSavedDatasetRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"\xd0\x03\n!CreateDatasetFromRetrievalRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x1c\n\x14\x66\x65\x61ture_service_name\x18\x03 \x01(\t\x12\x10\n\x08\x66\x65\x61tures\x18\x04 \x03(\t\x12\x1a\n\x12\x65ntity_source_type\x18\x05 \x01(\t\x12\x1a\n\x12\x65ntity_source_path\x18\x06 \x01(\t\x12\x13\n\x0b\x65ntity_keys\x18\x08 \x03(\t\x12\x15\n\rentity_values\x18\t \x01(\t\x12\x12\n\nstart_date\x18\n \x01(\t\x12\x10\n\x08\x65nd_date\x18\x0b \x01(\t\x12\x15\n\rextra_columns\x18\x0c \x01(\t\x12\x14\n\x0cstorage_type\x18\r \x01(\t\x12\x14\n\x0cstorage_path\x18\x0e \x01(\t\x12I\n\x04tags\x18\x0f \x03(\x0b\x32;.feast.registry.CreateDatasetFromRetrievalRequest.TagsEntry\x12\x17\n\x0f\x61llow_overwrite\x18\x10 \x01(\x08\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"D\n\"CreateDatasetFromRetrievalResponse\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\"E\n\x15GetDatasetDataRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\r\n\x05limit\x18\x03 \x01(\x05\"\x1c\n\nTabularRow\x12\x0e\n\x06values\x18\x01 \x03(\t\"|\n\x16GetDatasetDataResponse\x12\x0f\n\x07\x63olumns\x18\x01 \x03(\t\x12(\n\x04rows\x18\x02 \x03(\x0b\x32\x1a.feast.registry.TabularRow\x12\x12\n\ntotal_rows\x18\x03 \x01(\x05\x12\x13\n\x0bsample_size\x18\x04 \x01(\x05\",\n\x1aGetDatasetJobStatusRequest\x12\x0e\n\x06job_id\x18\x01 \x01(\t\"\x9d\x01\n\x1bGetDatasetJobStatusResponse\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12\x14\n\x0c\x64\x61taset_name\x18\x02 \x01(\t\x12\x0f\n\x07project\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x12\n\ncreated_at\x18\x05 \x01(\t\x12\x14\n\x0c\x63ompleted_at\x18\x06 \x01(\t\x12\r\n\x05\x65rror\x18\x07 \x01(\t\"@\n\x16ListDatasetJobsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x15\n\rstatus_filter\x18\x02 \x01(\t\"T\n\x17ListDatasetJobsResponse\x12\x39\n\x04jobs\x18\x01 \x03(\x0b\x32+.feast.registry.GetDatasetJobStatusResponse\"\x81\x01\n\x1f\x41pplyValidationReferenceRequest\x12=\n\x14validation_reference\x18\x01 \x01(\x0b\x32\x1f.feast.core.ValidationReference\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"S\n\x1dGetValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\xa3\x02\n\x1fListValidationReferencesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12G\n\x04tags\x18\x03 \x03(\x0b\x32\x39.feast.registry.ListValidationReferencesRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9a\x01\n ListValidationReferencesResponse\x12>\n\x15validation_references\x18\x01 \x03(\x0b\x32\x1f.feast.core.ValidationReference\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"Q\n DeleteValidationReferenceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"e\n\x16\x41pplyPermissionRequest\x12*\n\npermission\x18\x01 \x01(\x0b\x32\x16.feast.core.Permission\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"J\n\x14GetPermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x03 \x01(\x08\"\x91\x02\n\x16ListPermissionsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12>\n\x04tags\x18\x03 \x03(\x0b\x32\x30.feast.registry.ListPermissionsRequest.TagsEntry\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"~\n\x17ListPermissionsResponse\x12+\n\x0bpermissions\x18\x01 \x03(\x0b\x32\x16.feast.core.Permission\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"H\n\x17\x44\x65letePermissionRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07project\x18\x02 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x03 \x01(\x08\"K\n\x13\x41pplyProjectRequest\x12$\n\x07project\x18\x01 \x01(\x0b\x32\x13.feast.core.Project\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\"6\n\x11GetProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\"\xfa\x01\n\x13ListProjectsRequest\x12\x13\n\x0b\x61llow_cache\x18\x01 \x01(\x08\x12;\n\x04tags\x18\x02 \x03(\x0b\x32-.feast.registry.ListProjectsRequest.TagsEntry\x12\x34\n\npagination\x18\x03 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x04 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"u\n\x14ListProjectsResponse\x12%\n\x08projects\x18\x01 \x03(\x0b\x32\x13.feast.core.Project\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"4\n\x14\x44\x65leteProjectRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x63ommit\x18\x02 \x01(\x08\"-\n\x0f\x45ntityReference\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"r\n\x0e\x45ntityRelation\x12/\n\x06source\x18\x01 \x01(\x0b\x32\x1f.feast.registry.EntityReference\x12/\n\x06target\x18\x02 \x01(\x0b\x32\x1f.feast.registry.EntityReference\"\xdf\x01\n\x19GetRegistryLineageRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x02 \x01(\x08\x12\x1a\n\x12\x66ilter_object_type\x18\x03 \x01(\t\x12\x1a\n\x12\x66ilter_object_name\x18\x04 \x01(\t\x12\x34\n\npagination\x18\x05 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x06 \x01(\x0b\x32\x1d.feast.registry.SortingParams\"\xa8\x02\n\x1aGetRegistryLineageResponse\x12\x35\n\rrelationships\x18\x01 \x03(\x0b\x32\x1e.feast.registry.EntityRelation\x12>\n\x16indirect_relationships\x18\x02 \x03(\x0b\x32\x1e.feast.registry.EntityRelation\x12\x44\n\x18relationships_pagination\x18\x03 \x01(\x0b\x32\".feast.registry.PaginationMetadata\x12M\n!indirect_relationships_pagination\x18\x04 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"\xef\x01\n\x1dGetObjectRelationshipsRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x13\n\x0bobject_type\x18\x02 \x01(\t\x12\x13\n\x0bobject_name\x18\x03 \x01(\t\x12\x18\n\x10include_indirect\x18\x04 \x01(\x08\x12\x13\n\x0b\x61llow_cache\x18\x05 \x01(\x08\x12\x34\n\npagination\x18\x06 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x07 \x01(\x0b\x32\x1d.feast.registry.SortingParams\"\x8f\x01\n\x1eGetObjectRelationshipsResponse\x12\x35\n\rrelationships\x18\x01 \x03(\x0b\x32\x1e.feast.registry.EntityRelation\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"\xbe\x02\n\x07\x46\x65\x61ture\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_view\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\r\n\x05owner\x18\x05 \x01(\t\x12\x35\n\x11\x63reated_timestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12:\n\x16last_updated_timestamp\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x04tags\x18\x08 \x03(\x0b\x32!.feast.registry.Feature.TagsEntry\x12\x0c\n\x04kind\x18\t \x01(\t\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xd3\x01\n\x13ListFeaturesRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_view\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x06 \x01(\x08\x12\x34\n\npagination\x18\x04 \x01(\x0b\x32 .feast.registry.PaginationParams\x12.\n\x07sorting\x18\x05 \x01(\x0b\x32\x1d.feast.registry.SortingParams\x12\x0c\n\x04kind\x18\x07 \x01(\t\"y\n\x14ListFeaturesResponse\x12)\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0b\x32\x17.feast.registry.Feature\x12\x36\n\npagination\x18\x02 \x01(\x0b\x32\".feast.registry.PaginationMetadata\"]\n\x11GetFeatureRequest\x12\x0f\n\x07project\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_view\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x13\n\x0b\x61llow_cache\x18\x04 \x01(\x08\x32\xd2(\n\x0eRegistryServer\x12K\n\x0b\x41pplyEntity\x12\".feast.registry.ApplyEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\tGetEntity\x12 .feast.registry.GetEntityRequest\x1a\x12.feast.core.Entity\"\x00\x12[\n\x0cListEntities\x12#.feast.registry.ListEntitiesRequest\x1a$.feast.registry.ListEntitiesResponse\"\x00\x12M\n\x0c\x44\x65leteEntity\x12#.feast.registry.DeleteEntityRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyDataSource\x12&.feast.registry.ApplyDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetDataSource\x12$.feast.registry.GetDataSourceRequest\x1a\x16.feast.core.DataSource\"\x00\x12\x64\n\x0fListDataSources\x12&.feast.registry.ListDataSourcesRequest\x1a\'.feast.registry.ListDataSourcesResponse\"\x00\x12U\n\x10\x44\x65leteDataSource\x12\'.feast.registry.DeleteDataSourceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x10\x41pplyFeatureView\x12\'.feast.registry.ApplyFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x44\x65leteFeatureView\x12(.feast.registry.DeleteFeatureViewRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x11GetAnyFeatureView\x12(.feast.registry.GetAnyFeatureViewRequest\x1a).feast.registry.GetAnyFeatureViewResponse\"\x00\x12p\n\x13ListAllFeatureViews\x12*.feast.registry.ListAllFeatureViewsRequest\x1a+.feast.registry.ListAllFeatureViewsResponse\"\x00\x12R\n\x0eGetFeatureView\x12%.feast.registry.GetFeatureViewRequest\x1a\x17.feast.core.FeatureView\"\x00\x12g\n\x10ListFeatureViews\x12\'.feast.registry.ListFeatureViewsRequest\x1a(.feast.registry.ListFeatureViewsResponse\"\x00\x12\x64\n\x14GetStreamFeatureView\x12+.feast.registry.GetStreamFeatureViewRequest\x1a\x1d.feast.core.StreamFeatureView\"\x00\x12y\n\x16ListStreamFeatureViews\x12-.feast.registry.ListStreamFeatureViewsRequest\x1a..feast.registry.ListStreamFeatureViewsResponse\"\x00\x12j\n\x16GetOnDemandFeatureView\x12-.feast.registry.GetOnDemandFeatureViewRequest\x1a\x1f.feast.core.OnDemandFeatureView\"\x00\x12\x7f\n\x18ListOnDemandFeatureViews\x12/.feast.registry.ListOnDemandFeatureViewsRequest\x1a\x30.feast.registry.ListOnDemandFeatureViewsResponse\"\x00\x12L\n\x0cGetLabelView\x12#.feast.registry.GetLabelViewRequest\x1a\x15.feast.core.LabelView\"\x00\x12\x61\n\x0eListLabelViews\x12%.feast.registry.ListLabelViewsRequest\x1a&.feast.registry.ListLabelViewsResponse\"\x00\x12[\n\x13\x41pplyFeatureService\x12*.feast.registry.ApplyFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12[\n\x11GetFeatureService\x12(.feast.registry.GetFeatureServiceRequest\x1a\x1a.feast.core.FeatureService\"\x00\x12p\n\x13ListFeatureServices\x12*.feast.registry.ListFeatureServicesRequest\x1a+.feast.registry.ListFeatureServicesResponse\"\x00\x12]\n\x14\x44\x65leteFeatureService\x12+.feast.registry.DeleteFeatureServiceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12W\n\x11\x41pplySavedDataset\x12(.feast.registry.ApplySavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12U\n\x0fGetSavedDataset\x12&.feast.registry.GetSavedDatasetRequest\x1a\x18.feast.core.SavedDataset\"\x00\x12j\n\x11ListSavedDatasets\x12(.feast.registry.ListSavedDatasetsRequest\x1a).feast.registry.ListSavedDatasetsResponse\"\x00\x12Y\n\x12\x44\x65leteSavedDataset\x12).feast.registry.DeleteSavedDatasetRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x85\x01\n\x1a\x43reateDatasetFromRetrieval\x12\x31.feast.registry.CreateDatasetFromRetrievalRequest\x1a\x32.feast.registry.CreateDatasetFromRetrievalResponse\"\x00\x12\x61\n\x0eGetDatasetData\x12%.feast.registry.GetDatasetDataRequest\x1a&.feast.registry.GetDatasetDataResponse\"\x00\x12p\n\x13GetDatasetJobStatus\x12*.feast.registry.GetDatasetJobStatusRequest\x1a+.feast.registry.GetDatasetJobStatusResponse\"\x00\x12\x64\n\x0fListDatasetJobs\x12&.feast.registry.ListDatasetJobsRequest\x1a\'.feast.registry.ListDatasetJobsResponse\"\x00\x12\x65\n\x18\x41pplyValidationReference\x12/.feast.registry.ApplyValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12j\n\x16GetValidationReference\x12-.feast.registry.GetValidationReferenceRequest\x1a\x1f.feast.core.ValidationReference\"\x00\x12\x7f\n\x18ListValidationReferences\x12/.feast.registry.ListValidationReferencesRequest\x1a\x30.feast.registry.ListValidationReferencesResponse\"\x00\x12g\n\x19\x44\x65leteValidationReference\x12\x30.feast.registry.DeleteValidationReferenceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12S\n\x0f\x41pplyPermission\x12&.feast.registry.ApplyPermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12O\n\rGetPermission\x12$.feast.registry.GetPermissionRequest\x1a\x16.feast.core.Permission\"\x00\x12\x64\n\x0fListPermissions\x12&.feast.registry.ListPermissionsRequest\x1a\'.feast.registry.ListPermissionsResponse\"\x00\x12U\n\x10\x44\x65letePermission\x12\'.feast.registry.DeletePermissionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12M\n\x0c\x41pplyProject\x12#.feast.registry.ApplyProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x46\n\nGetProject\x12!.feast.registry.GetProjectRequest\x1a\x13.feast.core.Project\"\x00\x12[\n\x0cListProjects\x12#.feast.registry.ListProjectsRequest\x1a$.feast.registry.ListProjectsResponse\"\x00\x12O\n\rDeleteProject\x12$.feast.registry.DeleteProjectRequest\x1a\x16.google.protobuf.Empty\"\x00\x12]\n\x14\x41pplyMaterialization\x12+.feast.registry.ApplyMaterializationRequest\x1a\x16.google.protobuf.Empty\"\x00\x12p\n\x13ListProjectMetadata\x12*.feast.registry.ListProjectMetadataRequest\x1a+.feast.registry.ListProjectMetadataResponse\"\x00\x12K\n\x0bUpdateInfra\x12\".feast.registry.UpdateInfraRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x08GetInfra\x12\x1f.feast.registry.GetInfraRequest\x1a\x11.feast.core.Infra\"\x00\x12:\n\x06\x43ommit\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x07Refresh\x12\x1e.feast.registry.RefreshRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x37\n\x05Proto\x12\x16.google.protobuf.Empty\x1a\x14.feast.core.Registry\"\x00\x12m\n\x12GetRegistryLineage\x12).feast.registry.GetRegistryLineageRequest\x1a*.feast.registry.GetRegistryLineageResponse\"\x00\x12y\n\x16GetObjectRelationships\x12-.feast.registry.GetObjectRelationshipsRequest\x1a..feast.registry.GetObjectRelationshipsResponse\"\x00\x12[\n\x0cListFeatures\x12#.feast.registry.ListFeaturesRequest\x1a$.feast.registry.ListFeaturesResponse\"\x00\x12J\n\nGetFeature\x12!.feast.registry.GetFeatureRequest\x1a\x17.feast.registry.Feature\"\x00\x42\x35Z3github.com/feast-dev/feast/go/protos/feast/registryb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -48,10 +49,14 @@ _globals['_LISTSTREAMFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_options = b'8\001' _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._options = None _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_LISTLABELVIEWSREQUEST_TAGSENTRY']._options = None + _globals['_LISTLABELVIEWSREQUEST_TAGSENTRY']._serialized_options = b'8\001' _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._options = None _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._serialized_options = b'8\001' _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._options = None _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._serialized_options = b'8\001' + _globals['_CREATEDATASETFROMRETRIEVALREQUEST_TAGSENTRY']._options = None + _globals['_CREATEDATASETFROMRETRIEVALREQUEST_TAGSENTRY']._serialized_options = b'8\001' _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._options = None _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._serialized_options = b'8\001' _globals['_LISTPERMISSIONSREQUEST_TAGSENTRY']._options = None @@ -60,170 +65,198 @@ _globals['_LISTPROJECTSREQUEST_TAGSENTRY']._serialized_options = b'8\001' _globals['_FEATURE_TAGSENTRY']._options = None _globals['_FEATURE_TAGSENTRY']._serialized_options = b'8\001' - _globals['_PAGINATIONPARAMS']._serialized_start=487 - _globals['_PAGINATIONPARAMS']._serialized_end=534 - _globals['_SORTINGPARAMS']._serialized_start=536 - _globals['_SORTINGPARAMS']._serialized_end=588 - _globals['_PAGINATIONMETADATA']._serialized_start=591 - _globals['_PAGINATIONMETADATA']._serialized_end=722 - _globals['_REFRESHREQUEST']._serialized_start=724 - _globals['_REFRESHREQUEST']._serialized_end=757 - _globals['_UPDATEINFRAREQUEST']._serialized_start=759 - _globals['_UPDATEINFRAREQUEST']._serialized_end=846 - _globals['_GETINFRAREQUEST']._serialized_start=848 - _globals['_GETINFRAREQUEST']._serialized_end=903 - _globals['_LISTPROJECTMETADATAREQUEST']._serialized_start=905 - _globals['_LISTPROJECTMETADATAREQUEST']._serialized_end=971 - _globals['_LISTPROJECTMETADATARESPONSE']._serialized_start=973 - _globals['_LISTPROJECTMETADATARESPONSE']._serialized_end=1057 - _globals['_APPLYMATERIALIZATIONREQUEST']._serialized_start=1060 - _globals['_APPLYMATERIALIZATIONREQUEST']._serialized_end=1263 - _globals['_APPLYENTITYREQUEST']._serialized_start=1265 - _globals['_APPLYENTITYREQUEST']._serialized_end=1354 - _globals['_GETENTITYREQUEST']._serialized_start=1356 - _globals['_GETENTITYREQUEST']._serialized_end=1426 - _globals['_LISTENTITIESREQUEST']._serialized_start=1429 - _globals['_LISTENTITIESREQUEST']._serialized_end=1696 - _globals['_LISTENTITIESREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTENTITIESREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTENTITIESRESPONSE']._serialized_start=1698 - _globals['_LISTENTITIESRESPONSE']._serialized_end=1814 - _globals['_DELETEENTITYREQUEST']._serialized_start=1816 - _globals['_DELETEENTITYREQUEST']._serialized_end=1884 - _globals['_APPLYDATASOURCEREQUEST']._serialized_start=1886 - _globals['_APPLYDATASOURCEREQUEST']._serialized_end=1988 - _globals['_GETDATASOURCEREQUEST']._serialized_start=1990 - _globals['_GETDATASOURCEREQUEST']._serialized_end=2064 - _globals['_LISTDATASOURCESREQUEST']._serialized_start=2067 - _globals['_LISTDATASOURCESREQUEST']._serialized_end=2340 - _globals['_LISTDATASOURCESREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTDATASOURCESREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTDATASOURCESRESPONSE']._serialized_start=2342 - _globals['_LISTDATASOURCESRESPONSE']._serialized_end=2469 - _globals['_DELETEDATASOURCEREQUEST']._serialized_start=2471 - _globals['_DELETEDATASOURCEREQUEST']._serialized_end=2543 - _globals['_APPLYFEATUREVIEWREQUEST']._serialized_start=2546 - _globals['_APPLYFEATUREVIEWREQUEST']._serialized_end=2803 - _globals['_GETFEATUREVIEWREQUEST']._serialized_start=2805 - _globals['_GETFEATUREVIEWREQUEST']._serialized_end=2880 - _globals['_LISTFEATUREVIEWSREQUEST']._serialized_start=2883 - _globals['_LISTFEATUREVIEWSREQUEST']._serialized_end=3158 - _globals['_LISTFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTFEATUREVIEWSRESPONSE']._serialized_start=3161 - _globals['_LISTFEATUREVIEWSRESPONSE']._serialized_end=3291 - _globals['_DELETEFEATUREVIEWREQUEST']._serialized_start=3293 - _globals['_DELETEFEATUREVIEWREQUEST']._serialized_end=3366 - _globals['_ANYFEATUREVIEW']._serialized_start=3369 - _globals['_ANYFEATUREVIEW']._serialized_end=3583 - _globals['_GETANYFEATUREVIEWREQUEST']._serialized_start=3585 - _globals['_GETANYFEATUREVIEWREQUEST']._serialized_end=3663 - _globals['_GETANYFEATUREVIEWRESPONSE']._serialized_start=3665 - _globals['_GETANYFEATUREVIEWRESPONSE']._serialized_end=3750 - _globals['_LISTALLFEATUREVIEWSREQUEST']._serialized_start=3753 - _globals['_LISTALLFEATUREVIEWSREQUEST']._serialized_end=4113 - _globals['_LISTALLFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTALLFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTALLFEATUREVIEWSRESPONSE']._serialized_start=4116 - _globals['_LISTALLFEATUREVIEWSRESPONSE']._serialized_end=4256 - _globals['_GETSTREAMFEATUREVIEWREQUEST']._serialized_start=4258 - _globals['_GETSTREAMFEATUREVIEWREQUEST']._serialized_end=4339 - _globals['_LISTSTREAMFEATUREVIEWSREQUEST']._serialized_start=4342 - _globals['_LISTSTREAMFEATUREVIEWSREQUEST']._serialized_end=4629 - _globals['_LISTSTREAMFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTSTREAMFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTSTREAMFEATUREVIEWSRESPONSE']._serialized_start=4632 - _globals['_LISTSTREAMFEATUREVIEWSRESPONSE']._serialized_end=4781 - _globals['_GETONDEMANDFEATUREVIEWREQUEST']._serialized_start=4783 - _globals['_GETONDEMANDFEATUREVIEWREQUEST']._serialized_end=4866 - _globals['_LISTONDEMANDFEATUREVIEWSREQUEST']._serialized_start=4869 - _globals['_LISTONDEMANDFEATUREVIEWSREQUEST']._serialized_end=5160 - _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTONDEMANDFEATUREVIEWSRESPONSE']._serialized_start=5163 - _globals['_LISTONDEMANDFEATUREVIEWSRESPONSE']._serialized_end=5319 - _globals['_APPLYFEATURESERVICEREQUEST']._serialized_start=5321 - _globals['_APPLYFEATURESERVICEREQUEST']._serialized_end=5435 - _globals['_GETFEATURESERVICEREQUEST']._serialized_start=5437 - _globals['_GETFEATURESERVICEREQUEST']._serialized_end=5515 - _globals['_LISTFEATURESERVICESREQUEST']._serialized_start=5518 - _globals['_LISTFEATURESERVICESREQUEST']._serialized_end=5821 - _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTFEATURESERVICESRESPONSE']._serialized_start=5824 - _globals['_LISTFEATURESERVICESRESPONSE']._serialized_end=5963 - _globals['_DELETEFEATURESERVICEREQUEST']._serialized_start=5965 - _globals['_DELETEFEATURESERVICEREQUEST']._serialized_end=6041 - _globals['_APPLYSAVEDDATASETREQUEST']._serialized_start=6043 - _globals['_APPLYSAVEDDATASETREQUEST']._serialized_end=6151 - _globals['_GETSAVEDDATASETREQUEST']._serialized_start=6153 - _globals['_GETSAVEDDATASETREQUEST']._serialized_end=6229 - _globals['_LISTSAVEDDATASETSREQUEST']._serialized_start=6232 - _globals['_LISTSAVEDDATASETSREQUEST']._serialized_end=6509 - _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTSAVEDDATASETSRESPONSE']._serialized_start=6512 - _globals['_LISTSAVEDDATASETSRESPONSE']._serialized_end=6645 - _globals['_DELETESAVEDDATASETREQUEST']._serialized_start=6647 - _globals['_DELETESAVEDDATASETREQUEST']._serialized_end=6721 - _globals['_APPLYVALIDATIONREFERENCEREQUEST']._serialized_start=6724 - _globals['_APPLYVALIDATIONREFERENCEREQUEST']._serialized_end=6853 - _globals['_GETVALIDATIONREFERENCEREQUEST']._serialized_start=6855 - _globals['_GETVALIDATIONREFERENCEREQUEST']._serialized_end=6938 - _globals['_LISTVALIDATIONREFERENCESREQUEST']._serialized_start=6941 - _globals['_LISTVALIDATIONREFERENCESREQUEST']._serialized_end=7232 - _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTVALIDATIONREFERENCESRESPONSE']._serialized_start=7235 - _globals['_LISTVALIDATIONREFERENCESRESPONSE']._serialized_end=7389 - _globals['_DELETEVALIDATIONREFERENCEREQUEST']._serialized_start=7391 - _globals['_DELETEVALIDATIONREFERENCEREQUEST']._serialized_end=7472 - _globals['_APPLYPERMISSIONREQUEST']._serialized_start=7474 - _globals['_APPLYPERMISSIONREQUEST']._serialized_end=7575 - _globals['_GETPERMISSIONREQUEST']._serialized_start=7577 - _globals['_GETPERMISSIONREQUEST']._serialized_end=7651 - _globals['_LISTPERMISSIONSREQUEST']._serialized_start=7654 - _globals['_LISTPERMISSIONSREQUEST']._serialized_end=7927 - _globals['_LISTPERMISSIONSREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTPERMISSIONSREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTPERMISSIONSRESPONSE']._serialized_start=7929 - _globals['_LISTPERMISSIONSRESPONSE']._serialized_end=8055 - _globals['_DELETEPERMISSIONREQUEST']._serialized_start=8057 - _globals['_DELETEPERMISSIONREQUEST']._serialized_end=8129 - _globals['_APPLYPROJECTREQUEST']._serialized_start=8131 - _globals['_APPLYPROJECTREQUEST']._serialized_end=8206 - _globals['_GETPROJECTREQUEST']._serialized_start=8208 - _globals['_GETPROJECTREQUEST']._serialized_end=8262 - _globals['_LISTPROJECTSREQUEST']._serialized_start=8265 - _globals['_LISTPROJECTSREQUEST']._serialized_end=8515 - _globals['_LISTPROJECTSREQUEST_TAGSENTRY']._serialized_start=1653 - _globals['_LISTPROJECTSREQUEST_TAGSENTRY']._serialized_end=1696 - _globals['_LISTPROJECTSRESPONSE']._serialized_start=8517 - _globals['_LISTPROJECTSRESPONSE']._serialized_end=8634 - _globals['_DELETEPROJECTREQUEST']._serialized_start=8636 - _globals['_DELETEPROJECTREQUEST']._serialized_end=8688 - _globals['_ENTITYREFERENCE']._serialized_start=8690 - _globals['_ENTITYREFERENCE']._serialized_end=8735 - _globals['_ENTITYRELATION']._serialized_start=8737 - _globals['_ENTITYRELATION']._serialized_end=8851 - _globals['_GETREGISTRYLINEAGEREQUEST']._serialized_start=8854 - _globals['_GETREGISTRYLINEAGEREQUEST']._serialized_end=9077 - _globals['_GETREGISTRYLINEAGERESPONSE']._serialized_start=9080 - _globals['_GETREGISTRYLINEAGERESPONSE']._serialized_end=9376 - _globals['_GETOBJECTRELATIONSHIPSREQUEST']._serialized_start=9379 - _globals['_GETOBJECTRELATIONSHIPSREQUEST']._serialized_end=9618 - _globals['_GETOBJECTRELATIONSHIPSRESPONSE']._serialized_start=9621 - _globals['_GETOBJECTRELATIONSHIPSRESPONSE']._serialized_end=9764 - _globals['_FEATURE']._serialized_start=9767 - _globals['_FEATURE']._serialized_end=10071 - _globals['_FEATURE_TAGSENTRY']._serialized_start=1653 - _globals['_FEATURE_TAGSENTRY']._serialized_end=1696 - _globals['_LISTFEATURESREQUEST']._serialized_start=10074 - _globals['_LISTFEATURESREQUEST']._serialized_end=10271 - _globals['_LISTFEATURESRESPONSE']._serialized_start=10273 - _globals['_LISTFEATURESRESPONSE']._serialized_end=10394 - _globals['_GETFEATUREREQUEST']._serialized_start=10396 - _globals['_GETFEATUREREQUEST']._serialized_end=10489 - _globals['_REGISTRYSERVER']._serialized_start=10492 - _globals['_REGISTRYSERVER']._serialized_end=15066 + _globals['_PAGINATIONPARAMS']._serialized_start=515 + _globals['_PAGINATIONPARAMS']._serialized_end=562 + _globals['_SORTINGPARAMS']._serialized_start=564 + _globals['_SORTINGPARAMS']._serialized_end=616 + _globals['_PAGINATIONMETADATA']._serialized_start=619 + _globals['_PAGINATIONMETADATA']._serialized_end=750 + _globals['_REFRESHREQUEST']._serialized_start=752 + _globals['_REFRESHREQUEST']._serialized_end=785 + _globals['_UPDATEINFRAREQUEST']._serialized_start=787 + _globals['_UPDATEINFRAREQUEST']._serialized_end=874 + _globals['_GETINFRAREQUEST']._serialized_start=876 + _globals['_GETINFRAREQUEST']._serialized_end=931 + _globals['_LISTPROJECTMETADATAREQUEST']._serialized_start=933 + _globals['_LISTPROJECTMETADATAREQUEST']._serialized_end=999 + _globals['_LISTPROJECTMETADATARESPONSE']._serialized_start=1001 + _globals['_LISTPROJECTMETADATARESPONSE']._serialized_end=1085 + _globals['_APPLYMATERIALIZATIONREQUEST']._serialized_start=1088 + _globals['_APPLYMATERIALIZATIONREQUEST']._serialized_end=1291 + _globals['_APPLYENTITYREQUEST']._serialized_start=1293 + _globals['_APPLYENTITYREQUEST']._serialized_end=1382 + _globals['_GETENTITYREQUEST']._serialized_start=1384 + _globals['_GETENTITYREQUEST']._serialized_end=1454 + _globals['_LISTENTITIESREQUEST']._serialized_start=1457 + _globals['_LISTENTITIESREQUEST']._serialized_end=1724 + _globals['_LISTENTITIESREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTENTITIESREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTENTITIESRESPONSE']._serialized_start=1726 + _globals['_LISTENTITIESRESPONSE']._serialized_end=1842 + _globals['_DELETEENTITYREQUEST']._serialized_start=1844 + _globals['_DELETEENTITYREQUEST']._serialized_end=1912 + _globals['_APPLYDATASOURCEREQUEST']._serialized_start=1914 + _globals['_APPLYDATASOURCEREQUEST']._serialized_end=2016 + _globals['_GETDATASOURCEREQUEST']._serialized_start=2018 + _globals['_GETDATASOURCEREQUEST']._serialized_end=2092 + _globals['_LISTDATASOURCESREQUEST']._serialized_start=2095 + _globals['_LISTDATASOURCESREQUEST']._serialized_end=2368 + _globals['_LISTDATASOURCESREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTDATASOURCESREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTDATASOURCESRESPONSE']._serialized_start=2370 + _globals['_LISTDATASOURCESRESPONSE']._serialized_end=2497 + _globals['_DELETEDATASOURCEREQUEST']._serialized_start=2499 + _globals['_DELETEDATASOURCEREQUEST']._serialized_end=2571 + _globals['_APPLYFEATUREVIEWREQUEST']._serialized_start=2574 + _globals['_APPLYFEATUREVIEWREQUEST']._serialized_end=2876 + _globals['_GETFEATUREVIEWREQUEST']._serialized_start=2878 + _globals['_GETFEATUREVIEWREQUEST']._serialized_end=2953 + _globals['_LISTFEATUREVIEWSREQUEST']._serialized_start=2956 + _globals['_LISTFEATUREVIEWSREQUEST']._serialized_end=3231 + _globals['_LISTFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTFEATUREVIEWSRESPONSE']._serialized_start=3234 + _globals['_LISTFEATUREVIEWSRESPONSE']._serialized_end=3364 + _globals['_DELETEFEATUREVIEWREQUEST']._serialized_start=3366 + _globals['_DELETEFEATUREVIEWREQUEST']._serialized_end=3439 + _globals['_ANYFEATUREVIEW']._serialized_start=3442 + _globals['_ANYFEATUREVIEW']._serialized_end=3701 + _globals['_GETANYFEATUREVIEWREQUEST']._serialized_start=3703 + _globals['_GETANYFEATUREVIEWREQUEST']._serialized_end=3781 + _globals['_GETANYFEATUREVIEWRESPONSE']._serialized_start=3783 + _globals['_GETANYFEATUREVIEWRESPONSE']._serialized_end=3868 + _globals['_LISTALLFEATUREVIEWSREQUEST']._serialized_start=3871 + _globals['_LISTALLFEATUREVIEWSREQUEST']._serialized_end=4282 + _globals['_LISTALLFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTALLFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTALLFEATUREVIEWSRESPONSE']._serialized_start=4285 + _globals['_LISTALLFEATUREVIEWSRESPONSE']._serialized_end=4425 + _globals['_GETSTREAMFEATUREVIEWREQUEST']._serialized_start=4427 + _globals['_GETSTREAMFEATUREVIEWREQUEST']._serialized_end=4508 + _globals['_LISTSTREAMFEATUREVIEWSREQUEST']._serialized_start=4511 + _globals['_LISTSTREAMFEATUREVIEWSREQUEST']._serialized_end=4798 + _globals['_LISTSTREAMFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTSTREAMFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTSTREAMFEATUREVIEWSRESPONSE']._serialized_start=4801 + _globals['_LISTSTREAMFEATUREVIEWSRESPONSE']._serialized_end=4950 + _globals['_GETONDEMANDFEATUREVIEWREQUEST']._serialized_start=4952 + _globals['_GETONDEMANDFEATUREVIEWREQUEST']._serialized_end=5035 + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST']._serialized_start=5038 + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST']._serialized_end=5329 + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTONDEMANDFEATUREVIEWSREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTONDEMANDFEATUREVIEWSRESPONSE']._serialized_start=5332 + _globals['_LISTONDEMANDFEATUREVIEWSRESPONSE']._serialized_end=5488 + _globals['_GETLABELVIEWREQUEST']._serialized_start=5490 + _globals['_GETLABELVIEWREQUEST']._serialized_end=5563 + _globals['_LISTLABELVIEWSREQUEST']._serialized_start=5566 + _globals['_LISTLABELVIEWSREQUEST']._serialized_end=5837 + _globals['_LISTLABELVIEWSREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTLABELVIEWSREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTLABELVIEWSRESPONSE']._serialized_start=5839 + _globals['_LISTLABELVIEWSRESPONSE']._serialized_end=5963 + _globals['_APPLYFEATURESERVICEREQUEST']._serialized_start=5965 + _globals['_APPLYFEATURESERVICEREQUEST']._serialized_end=6079 + _globals['_GETFEATURESERVICEREQUEST']._serialized_start=6081 + _globals['_GETFEATURESERVICEREQUEST']._serialized_end=6159 + _globals['_LISTFEATURESERVICESREQUEST']._serialized_start=6162 + _globals['_LISTFEATURESERVICESREQUEST']._serialized_end=6465 + _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTFEATURESERVICESREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTFEATURESERVICESRESPONSE']._serialized_start=6468 + _globals['_LISTFEATURESERVICESRESPONSE']._serialized_end=6607 + _globals['_DELETEFEATURESERVICEREQUEST']._serialized_start=6609 + _globals['_DELETEFEATURESERVICEREQUEST']._serialized_end=6685 + _globals['_APPLYSAVEDDATASETREQUEST']._serialized_start=6687 + _globals['_APPLYSAVEDDATASETREQUEST']._serialized_end=6795 + _globals['_GETSAVEDDATASETREQUEST']._serialized_start=6797 + _globals['_GETSAVEDDATASETREQUEST']._serialized_end=6873 + _globals['_LISTSAVEDDATASETSREQUEST']._serialized_start=6876 + _globals['_LISTSAVEDDATASETSREQUEST']._serialized_end=7192 + _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTSAVEDDATASETSREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTSAVEDDATASETSRESPONSE']._serialized_start=7195 + _globals['_LISTSAVEDDATASETSRESPONSE']._serialized_end=7328 + _globals['_DELETESAVEDDATASETREQUEST']._serialized_start=7330 + _globals['_DELETESAVEDDATASETREQUEST']._serialized_end=7404 + _globals['_CREATEDATASETFROMRETRIEVALREQUEST']._serialized_start=7407 + _globals['_CREATEDATASETFROMRETRIEVALREQUEST']._serialized_end=7871 + _globals['_CREATEDATASETFROMRETRIEVALREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_CREATEDATASETFROMRETRIEVALREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_CREATEDATASETFROMRETRIEVALRESPONSE']._serialized_start=7873 + _globals['_CREATEDATASETFROMRETRIEVALRESPONSE']._serialized_end=7941 + _globals['_GETDATASETDATAREQUEST']._serialized_start=7943 + _globals['_GETDATASETDATAREQUEST']._serialized_end=8012 + _globals['_TABULARROW']._serialized_start=8014 + _globals['_TABULARROW']._serialized_end=8042 + _globals['_GETDATASETDATARESPONSE']._serialized_start=8044 + _globals['_GETDATASETDATARESPONSE']._serialized_end=8168 + _globals['_GETDATASETJOBSTATUSREQUEST']._serialized_start=8170 + _globals['_GETDATASETJOBSTATUSREQUEST']._serialized_end=8214 + _globals['_GETDATASETJOBSTATUSRESPONSE']._serialized_start=8217 + _globals['_GETDATASETJOBSTATUSRESPONSE']._serialized_end=8374 + _globals['_LISTDATASETJOBSREQUEST']._serialized_start=8376 + _globals['_LISTDATASETJOBSREQUEST']._serialized_end=8440 + _globals['_LISTDATASETJOBSRESPONSE']._serialized_start=8442 + _globals['_LISTDATASETJOBSRESPONSE']._serialized_end=8526 + _globals['_APPLYVALIDATIONREFERENCEREQUEST']._serialized_start=8529 + _globals['_APPLYVALIDATIONREFERENCEREQUEST']._serialized_end=8658 + _globals['_GETVALIDATIONREFERENCEREQUEST']._serialized_start=8660 + _globals['_GETVALIDATIONREFERENCEREQUEST']._serialized_end=8743 + _globals['_LISTVALIDATIONREFERENCESREQUEST']._serialized_start=8746 + _globals['_LISTVALIDATIONREFERENCESREQUEST']._serialized_end=9037 + _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTVALIDATIONREFERENCESREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTVALIDATIONREFERENCESRESPONSE']._serialized_start=9040 + _globals['_LISTVALIDATIONREFERENCESRESPONSE']._serialized_end=9194 + _globals['_DELETEVALIDATIONREFERENCEREQUEST']._serialized_start=9196 + _globals['_DELETEVALIDATIONREFERENCEREQUEST']._serialized_end=9277 + _globals['_APPLYPERMISSIONREQUEST']._serialized_start=9279 + _globals['_APPLYPERMISSIONREQUEST']._serialized_end=9380 + _globals['_GETPERMISSIONREQUEST']._serialized_start=9382 + _globals['_GETPERMISSIONREQUEST']._serialized_end=9456 + _globals['_LISTPERMISSIONSREQUEST']._serialized_start=9459 + _globals['_LISTPERMISSIONSREQUEST']._serialized_end=9732 + _globals['_LISTPERMISSIONSREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTPERMISSIONSREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTPERMISSIONSRESPONSE']._serialized_start=9734 + _globals['_LISTPERMISSIONSRESPONSE']._serialized_end=9860 + _globals['_DELETEPERMISSIONREQUEST']._serialized_start=9862 + _globals['_DELETEPERMISSIONREQUEST']._serialized_end=9934 + _globals['_APPLYPROJECTREQUEST']._serialized_start=9936 + _globals['_APPLYPROJECTREQUEST']._serialized_end=10011 + _globals['_GETPROJECTREQUEST']._serialized_start=10013 + _globals['_GETPROJECTREQUEST']._serialized_end=10067 + _globals['_LISTPROJECTSREQUEST']._serialized_start=10070 + _globals['_LISTPROJECTSREQUEST']._serialized_end=10320 + _globals['_LISTPROJECTSREQUEST_TAGSENTRY']._serialized_start=1681 + _globals['_LISTPROJECTSREQUEST_TAGSENTRY']._serialized_end=1724 + _globals['_LISTPROJECTSRESPONSE']._serialized_start=10322 + _globals['_LISTPROJECTSRESPONSE']._serialized_end=10439 + _globals['_DELETEPROJECTREQUEST']._serialized_start=10441 + _globals['_DELETEPROJECTREQUEST']._serialized_end=10493 + _globals['_ENTITYREFERENCE']._serialized_start=10495 + _globals['_ENTITYREFERENCE']._serialized_end=10540 + _globals['_ENTITYRELATION']._serialized_start=10542 + _globals['_ENTITYRELATION']._serialized_end=10656 + _globals['_GETREGISTRYLINEAGEREQUEST']._serialized_start=10659 + _globals['_GETREGISTRYLINEAGEREQUEST']._serialized_end=10882 + _globals['_GETREGISTRYLINEAGERESPONSE']._serialized_start=10885 + _globals['_GETREGISTRYLINEAGERESPONSE']._serialized_end=11181 + _globals['_GETOBJECTRELATIONSHIPSREQUEST']._serialized_start=11184 + _globals['_GETOBJECTRELATIONSHIPSREQUEST']._serialized_end=11423 + _globals['_GETOBJECTRELATIONSHIPSRESPONSE']._serialized_start=11426 + _globals['_GETOBJECTRELATIONSHIPSRESPONSE']._serialized_end=11569 + _globals['_FEATURE']._serialized_start=11572 + _globals['_FEATURE']._serialized_end=11890 + _globals['_FEATURE_TAGSENTRY']._serialized_start=1681 + _globals['_FEATURE_TAGSENTRY']._serialized_end=1724 + _globals['_LISTFEATURESREQUEST']._serialized_start=11893 + _globals['_LISTFEATURESREQUEST']._serialized_end=12104 + _globals['_LISTFEATURESRESPONSE']._serialized_start=12106 + _globals['_LISTFEATURESRESPONSE']._serialized_end=12227 + _globals['_GETFEATUREREQUEST']._serialized_start=12229 + _globals['_GETFEATUREREQUEST']._serialized_end=12322 + _globals['_REGISTRYSERVER']._serialized_start=12325 + _globals['_REGISTRYSERVER']._serialized_end=17527 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi index a1f1b99365d..e76775d0004 100644 --- a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi +++ b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2.pyi @@ -9,6 +9,7 @@ import feast.core.Entity_pb2 import feast.core.FeatureService_pb2 import feast.core.FeatureView_pb2 import feast.core.InfraObject_pb2 +import feast.core.LabelView_pb2 import feast.core.OnDemandFeatureView_pb2 import feast.core.Permission_pb2 import feast.core.Project_pb2 @@ -477,6 +478,7 @@ class ApplyFeatureViewRequest(google.protobuf.message.Message): FEATURE_VIEW_FIELD_NUMBER: builtins.int ON_DEMAND_FEATURE_VIEW_FIELD_NUMBER: builtins.int STREAM_FEATURE_VIEW_FIELD_NUMBER: builtins.int + LABEL_VIEW_FIELD_NUMBER: builtins.int PROJECT_FIELD_NUMBER: builtins.int COMMIT_FIELD_NUMBER: builtins.int @property @@ -485,6 +487,8 @@ class ApplyFeatureViewRequest(google.protobuf.message.Message): def on_demand_feature_view(self) -> feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView: ... @property def stream_feature_view(self) -> feast.core.StreamFeatureView_pb2.StreamFeatureView: ... + @property + def label_view(self) -> feast.core.LabelView_pb2.LabelView: ... project: builtins.str commit: builtins.bool def __init__( @@ -493,12 +497,13 @@ class ApplyFeatureViewRequest(google.protobuf.message.Message): feature_view: feast.core.FeatureView_pb2.FeatureView | None = ..., on_demand_feature_view: feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView | None = ..., stream_feature_view: feast.core.StreamFeatureView_pb2.StreamFeatureView | None = ..., + label_view: feast.core.LabelView_pb2.LabelView | None = ..., project: builtins.str = ..., commit: builtins.bool = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["base_feature_view", b"base_feature_view", "feature_view", b"feature_view", "on_demand_feature_view", b"on_demand_feature_view", "stream_feature_view", b"stream_feature_view"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["base_feature_view", b"base_feature_view", "commit", b"commit", "feature_view", b"feature_view", "on_demand_feature_view", b"on_demand_feature_view", "project", b"project", "stream_feature_view", b"stream_feature_view"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["base_feature_view", b"base_feature_view"]) -> typing_extensions.Literal["feature_view", "on_demand_feature_view", "stream_feature_view"] | None: ... + def HasField(self, field_name: typing_extensions.Literal["base_feature_view", b"base_feature_view", "feature_view", b"feature_view", "label_view", b"label_view", "on_demand_feature_view", b"on_demand_feature_view", "stream_feature_view", b"stream_feature_view"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["base_feature_view", b"base_feature_view", "commit", b"commit", "feature_view", b"feature_view", "label_view", b"label_view", "on_demand_feature_view", b"on_demand_feature_view", "project", b"project", "stream_feature_view", b"stream_feature_view"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["base_feature_view", b"base_feature_view"]) -> typing_extensions.Literal["feature_view", "on_demand_feature_view", "stream_feature_view", "label_view"] | None: ... global___ApplyFeatureViewRequest = ApplyFeatureViewRequest @@ -613,22 +618,26 @@ class AnyFeatureView(google.protobuf.message.Message): FEATURE_VIEW_FIELD_NUMBER: builtins.int ON_DEMAND_FEATURE_VIEW_FIELD_NUMBER: builtins.int STREAM_FEATURE_VIEW_FIELD_NUMBER: builtins.int + LABEL_VIEW_FIELD_NUMBER: builtins.int @property def feature_view(self) -> feast.core.FeatureView_pb2.FeatureView: ... @property def on_demand_feature_view(self) -> feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView: ... @property def stream_feature_view(self) -> feast.core.StreamFeatureView_pb2.StreamFeatureView: ... + @property + def label_view(self) -> feast.core.LabelView_pb2.LabelView: ... def __init__( self, *, feature_view: feast.core.FeatureView_pb2.FeatureView | None = ..., on_demand_feature_view: feast.core.OnDemandFeatureView_pb2.OnDemandFeatureView | None = ..., stream_feature_view: feast.core.StreamFeatureView_pb2.StreamFeatureView | None = ..., + label_view: feast.core.LabelView_pb2.LabelView | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["any_feature_view", b"any_feature_view", "feature_view", b"feature_view", "on_demand_feature_view", b"on_demand_feature_view", "stream_feature_view", b"stream_feature_view"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["any_feature_view", b"any_feature_view", "feature_view", b"feature_view", "on_demand_feature_view", b"on_demand_feature_view", "stream_feature_view", b"stream_feature_view"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["any_feature_view", b"any_feature_view"]) -> typing_extensions.Literal["feature_view", "on_demand_feature_view", "stream_feature_view"] | None: ... + def HasField(self, field_name: typing_extensions.Literal["any_feature_view", b"any_feature_view", "feature_view", b"feature_view", "label_view", b"label_view", "on_demand_feature_view", b"on_demand_feature_view", "stream_feature_view", b"stream_feature_view"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["any_feature_view", b"any_feature_view", "feature_view", b"feature_view", "label_view", b"label_view", "on_demand_feature_view", b"on_demand_feature_view", "stream_feature_view", b"stream_feature_view"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["any_feature_view", b"any_feature_view"]) -> typing_extensions.Literal["feature_view", "on_demand_feature_view", "stream_feature_view", "label_view"] | None: ... global___AnyFeatureView = AnyFeatureView @@ -695,6 +704,7 @@ class ListAllFeatureViewsRequest(google.protobuf.message.Message): DATA_SOURCE_FIELD_NUMBER: builtins.int PAGINATION_FIELD_NUMBER: builtins.int SORTING_FIELD_NUMBER: builtins.int + UPDATED_SINCE_FIELD_NUMBER: builtins.int project: builtins.str allow_cache: builtins.bool @property @@ -707,6 +717,8 @@ class ListAllFeatureViewsRequest(google.protobuf.message.Message): def pagination(self) -> global___PaginationParams: ... @property def sorting(self) -> global___SortingParams: ... + @property + def updated_since(self) -> google.protobuf.timestamp_pb2.Timestamp: ... def __init__( self, *, @@ -719,9 +731,10 @@ class ListAllFeatureViewsRequest(google.protobuf.message.Message): data_source: builtins.str = ..., pagination: global___PaginationParams | None = ..., sorting: global___SortingParams | None = ..., + updated_since: google.protobuf.timestamp_pb2.Timestamp | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["pagination", b"pagination", "sorting", b"sorting"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "data_source", b"data_source", "entity", b"entity", "feature", b"feature", "feature_service", b"feature_service", "pagination", b"pagination", "project", b"project", "sorting", b"sorting", "tags", b"tags"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["pagination", b"pagination", "sorting", b"sorting", "updated_since", b"updated_since"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "data_source", b"data_source", "entity", b"entity", "feature", b"feature", "feature_service", b"feature_service", "pagination", b"pagination", "project", b"project", "sorting", b"sorting", "tags", b"tags", "updated_since", b"updated_since"]) -> None: ... global___ListAllFeatureViewsRequest = ListAllFeatureViewsRequest @@ -919,6 +932,93 @@ class ListOnDemandFeatureViewsResponse(google.protobuf.message.Message): global___ListOnDemandFeatureViewsResponse = ListOnDemandFeatureViewsResponse +class GetLabelViewRequest(google.protobuf.message.Message): + """LabelViews""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + allow_cache: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "name", b"name", "project", b"project"]) -> None: ... + +global___GetLabelViewRequest = GetLabelViewRequest + +class ListLabelViewsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PROJECT_FIELD_NUMBER: builtins.int + ALLOW_CACHE_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + PAGINATION_FIELD_NUMBER: builtins.int + SORTING_FIELD_NUMBER: builtins.int + project: builtins.str + allow_cache: builtins.bool + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + @property + def pagination(self) -> global___PaginationParams: ... + @property + def sorting(self) -> global___SortingParams: ... + def __init__( + self, + *, + project: builtins.str = ..., + allow_cache: builtins.bool = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + pagination: global___PaginationParams | None = ..., + sorting: global___SortingParams | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["pagination", b"pagination", "sorting", b"sorting"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "pagination", b"pagination", "project", b"project", "sorting", b"sorting", "tags", b"tags"]) -> None: ... + +global___ListLabelViewsRequest = ListLabelViewsRequest + +class ListLabelViewsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + LABEL_VIEWS_FIELD_NUMBER: builtins.int + PAGINATION_FIELD_NUMBER: builtins.int + @property + def label_views(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[feast.core.LabelView_pb2.LabelView]: ... + @property + def pagination(self) -> global___PaginationMetadata: ... + def __init__( + self, + *, + label_views: collections.abc.Iterable[feast.core.LabelView_pb2.LabelView] | None = ..., + pagination: global___PaginationMetadata | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["pagination", b"pagination"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["label_views", b"label_views", "pagination", b"pagination"]) -> None: ... + +global___ListLabelViewsResponse = ListLabelViewsResponse + class ApplyFeatureServiceRequest(google.protobuf.message.Message): """FeatureServices""" @@ -1118,6 +1218,8 @@ class ListSavedDatasetsRequest(google.protobuf.message.Message): TAGS_FIELD_NUMBER: builtins.int PAGINATION_FIELD_NUMBER: builtins.int SORTING_FIELD_NUMBER: builtins.int + NAMESPACE_FIELD_NUMBER: builtins.int + COLLECTION_FIELD_NUMBER: builtins.int project: builtins.str allow_cache: builtins.bool @property @@ -1126,6 +1228,10 @@ class ListSavedDatasetsRequest(google.protobuf.message.Message): def pagination(self) -> global___PaginationParams: ... @property def sorting(self) -> global___SortingParams: ... + namespace: builtins.str + """Optional logical namespace filter. Empty string means no filter.""" + collection: builtins.str + """Optional collection filter. Empty string means no filter.""" def __init__( self, *, @@ -1134,9 +1240,11 @@ class ListSavedDatasetsRequest(google.protobuf.message.Message): tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., pagination: global___PaginationParams | None = ..., sorting: global___SortingParams | None = ..., + namespace: builtins.str = ..., + collection: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["pagination", b"pagination", "sorting", b"sorting"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "pagination", b"pagination", "project", b"project", "sorting", b"sorting", "tags", b"tags"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "collection", b"collection", "namespace", b"namespace", "pagination", b"pagination", "project", b"project", "sorting", b"sorting", "tags", b"tags"]) -> None: ... global___ListSavedDatasetsRequest = ListSavedDatasetsRequest @@ -1180,6 +1288,237 @@ class DeleteSavedDatasetRequest(google.protobuf.message.Message): global___DeleteSavedDatasetRequest = DeleteSavedDatasetRequest +class CreateDatasetFromRetrievalRequest(google.protobuf.message.Message): + """SavedDataset Operations""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class TagsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + FEATURE_SERVICE_NAME_FIELD_NUMBER: builtins.int + FEATURES_FIELD_NUMBER: builtins.int + ENTITY_SOURCE_TYPE_FIELD_NUMBER: builtins.int + ENTITY_SOURCE_PATH_FIELD_NUMBER: builtins.int + ENTITY_KEYS_FIELD_NUMBER: builtins.int + ENTITY_VALUES_FIELD_NUMBER: builtins.int + START_DATE_FIELD_NUMBER: builtins.int + END_DATE_FIELD_NUMBER: builtins.int + EXTRA_COLUMNS_FIELD_NUMBER: builtins.int + STORAGE_TYPE_FIELD_NUMBER: builtins.int + STORAGE_PATH_FIELD_NUMBER: builtins.int + TAGS_FIELD_NUMBER: builtins.int + ALLOW_OVERWRITE_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + feature_service_name: builtins.str + @property + def features(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + entity_source_type: builtins.str + entity_source_path: builtins.str + @property + def entity_keys(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + entity_values: builtins.str + start_date: builtins.str + end_date: builtins.str + extra_columns: builtins.str + storage_type: builtins.str + storage_path: builtins.str + @property + def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + allow_overwrite: builtins.bool + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + feature_service_name: builtins.str = ..., + features: collections.abc.Iterable[builtins.str] | None = ..., + entity_source_type: builtins.str = ..., + entity_source_path: builtins.str = ..., + entity_keys: collections.abc.Iterable[builtins.str] | None = ..., + entity_values: builtins.str = ..., + start_date: builtins.str = ..., + end_date: builtins.str = ..., + extra_columns: builtins.str = ..., + storage_type: builtins.str = ..., + storage_path: builtins.str = ..., + tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + allow_overwrite: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_overwrite", b"allow_overwrite", "end_date", b"end_date", "entity_keys", b"entity_keys", "entity_source_path", b"entity_source_path", "entity_source_type", b"entity_source_type", "entity_values", b"entity_values", "extra_columns", b"extra_columns", "feature_service_name", b"feature_service_name", "features", b"features", "name", b"name", "project", b"project", "start_date", b"start_date", "storage_path", b"storage_path", "storage_type", b"storage_type", "tags", b"tags"]) -> None: ... + +global___CreateDatasetFromRetrievalRequest = CreateDatasetFromRetrievalRequest + +class CreateDatasetFromRetrievalResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + JOB_ID_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + job_id: builtins.str + status: builtins.str + def __init__( + self, + *, + job_id: builtins.str = ..., + status: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["job_id", b"job_id", "status", b"status"]) -> None: ... + +global___CreateDatasetFromRetrievalResponse = CreateDatasetFromRetrievalResponse + +class GetDatasetDataRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + LIMIT_FIELD_NUMBER: builtins.int + name: builtins.str + project: builtins.str + limit: builtins.int + def __init__( + self, + *, + name: builtins.str = ..., + project: builtins.str = ..., + limit: builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["limit", b"limit", "name", b"name", "project", b"project"]) -> None: ... + +global___GetDatasetDataRequest = GetDatasetDataRequest + +class TabularRow(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUES_FIELD_NUMBER: builtins.int + @property + def values(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + def __init__( + self, + *, + values: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["values", b"values"]) -> None: ... + +global___TabularRow = TabularRow + +class GetDatasetDataResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + COLUMNS_FIELD_NUMBER: builtins.int + ROWS_FIELD_NUMBER: builtins.int + TOTAL_ROWS_FIELD_NUMBER: builtins.int + SAMPLE_SIZE_FIELD_NUMBER: builtins.int + @property + def columns(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + @property + def rows(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___TabularRow]: ... + total_rows: builtins.int + sample_size: builtins.int + def __init__( + self, + *, + columns: collections.abc.Iterable[builtins.str] | None = ..., + rows: collections.abc.Iterable[global___TabularRow] | None = ..., + total_rows: builtins.int = ..., + sample_size: builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["columns", b"columns", "rows", b"rows", "sample_size", b"sample_size", "total_rows", b"total_rows"]) -> None: ... + +global___GetDatasetDataResponse = GetDatasetDataResponse + +class GetDatasetJobStatusRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + JOB_ID_FIELD_NUMBER: builtins.int + job_id: builtins.str + def __init__( + self, + *, + job_id: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["job_id", b"job_id"]) -> None: ... + +global___GetDatasetJobStatusRequest = GetDatasetJobStatusRequest + +class GetDatasetJobStatusResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + JOB_ID_FIELD_NUMBER: builtins.int + DATASET_NAME_FIELD_NUMBER: builtins.int + PROJECT_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + CREATED_AT_FIELD_NUMBER: builtins.int + COMPLETED_AT_FIELD_NUMBER: builtins.int + ERROR_FIELD_NUMBER: builtins.int + job_id: builtins.str + dataset_name: builtins.str + project: builtins.str + status: builtins.str + created_at: builtins.str + completed_at: builtins.str + error: builtins.str + def __init__( + self, + *, + job_id: builtins.str = ..., + dataset_name: builtins.str = ..., + project: builtins.str = ..., + status: builtins.str = ..., + created_at: builtins.str = ..., + completed_at: builtins.str = ..., + error: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["completed_at", b"completed_at", "created_at", b"created_at", "dataset_name", b"dataset_name", "error", b"error", "job_id", b"job_id", "project", b"project", "status", b"status"]) -> None: ... + +global___GetDatasetJobStatusResponse = GetDatasetJobStatusResponse + +class ListDatasetJobsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROJECT_FIELD_NUMBER: builtins.int + STATUS_FILTER_FIELD_NUMBER: builtins.int + project: builtins.str + status_filter: builtins.str + def __init__( + self, + *, + project: builtins.str = ..., + status_filter: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["project", b"project", "status_filter", b"status_filter"]) -> None: ... + +global___ListDatasetJobsRequest = ListDatasetJobsRequest + +class ListDatasetJobsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + JOBS_FIELD_NUMBER: builtins.int + @property + def jobs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___GetDatasetJobStatusResponse]: ... + def __init__( + self, + *, + jobs: collections.abc.Iterable[global___GetDatasetJobStatusResponse] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["jobs", b"jobs"]) -> None: ... + +global___ListDatasetJobsResponse = ListDatasetJobsResponse + class ApplyValidationReferenceRequest(google.protobuf.message.Message): """ValidationReference""" @@ -1738,6 +2077,7 @@ class Feature(google.protobuf.message.Message): CREATED_TIMESTAMP_FIELD_NUMBER: builtins.int LAST_UPDATED_TIMESTAMP_FIELD_NUMBER: builtins.int TAGS_FIELD_NUMBER: builtins.int + KIND_FIELD_NUMBER: builtins.int name: builtins.str feature_view: builtins.str type: builtins.str @@ -1749,6 +2089,8 @@ class Feature(google.protobuf.message.Message): def last_updated_timestamp(self) -> google.protobuf.timestamp_pb2.Timestamp: ... @property def tags(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... + kind: builtins.str + """Semantic kind: "feature" (input/predictor) or "label" (target/annotation).""" def __init__( self, *, @@ -1760,9 +2102,10 @@ class Feature(google.protobuf.message.Message): created_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., last_updated_timestamp: google.protobuf.timestamp_pb2.Timestamp | None = ..., tags: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + kind: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "last_updated_timestamp", b"last_updated_timestamp"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "description", b"description", "feature_view", b"feature_view", "last_updated_timestamp", b"last_updated_timestamp", "name", b"name", "owner", b"owner", "tags", b"tags", "type", b"type"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["created_timestamp", b"created_timestamp", "description", b"description", "feature_view", b"feature_view", "kind", b"kind", "last_updated_timestamp", b"last_updated_timestamp", "name", b"name", "owner", b"owner", "tags", b"tags", "type", b"type"]) -> None: ... global___Feature = Feature @@ -1775,6 +2118,7 @@ class ListFeaturesRequest(google.protobuf.message.Message): ALLOW_CACHE_FIELD_NUMBER: builtins.int PAGINATION_FIELD_NUMBER: builtins.int SORTING_FIELD_NUMBER: builtins.int + KIND_FIELD_NUMBER: builtins.int project: builtins.str feature_view: builtins.str name: builtins.str @@ -1783,6 +2127,8 @@ class ListFeaturesRequest(google.protobuf.message.Message): def pagination(self) -> global___PaginationParams: ... @property def sorting(self) -> global___SortingParams: ... + kind: builtins.str + """Filter by kind: "feature" or "label". Empty means no filter.""" def __init__( self, *, @@ -1792,9 +2138,10 @@ class ListFeaturesRequest(google.protobuf.message.Message): allow_cache: builtins.bool = ..., pagination: global___PaginationParams | None = ..., sorting: global___SortingParams | None = ..., + kind: builtins.str = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["pagination", b"pagination", "sorting", b"sorting"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "feature_view", b"feature_view", "name", b"name", "pagination", b"pagination", "project", b"project", "sorting", b"sorting"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_cache", b"allow_cache", "feature_view", b"feature_view", "kind", b"kind", "name", b"name", "pagination", b"pagination", "project", b"project", "sorting", b"sorting"]) -> None: ... global___ListFeaturesRequest = ListFeaturesRequest diff --git a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py index 84de666b6b1..e06f3a811df 100644 --- a/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py +++ b/sdk/python/feast/protos/feast/registry/RegistryServer_pb2_grpc.py @@ -7,6 +7,7 @@ from feast.protos.feast.core import FeatureService_pb2 as feast_dot_core_dot_FeatureService__pb2 from feast.protos.feast.core import FeatureView_pb2 as feast_dot_core_dot_FeatureView__pb2 from feast.protos.feast.core import InfraObject_pb2 as feast_dot_core_dot_InfraObject__pb2 +from feast.protos.feast.core import LabelView_pb2 as feast_dot_core_dot_LabelView__pb2 from feast.protos.feast.core import OnDemandFeatureView_pb2 as feast_dot_core_dot_OnDemandFeatureView__pb2 from feast.protos.feast.core import Permission_pb2 as feast_dot_core_dot_Permission__pb2 from feast.protos.feast.core import Project_pb2 as feast_dot_core_dot_Project__pb2 @@ -117,6 +118,16 @@ def __init__(self, channel): request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsRequest.SerializeToString, response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsResponse.FromString, ) + self.GetLabelView = channel.unary_unary( + '/feast.registry.RegistryServer/GetLabelView', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetLabelViewRequest.SerializeToString, + response_deserializer=feast_dot_core_dot_LabelView__pb2.LabelView.FromString, + ) + self.ListLabelViews = channel.unary_unary( + '/feast.registry.RegistryServer/ListLabelViews', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListLabelViewsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListLabelViewsResponse.FromString, + ) self.ApplyFeatureService = channel.unary_unary( '/feast.registry.RegistryServer/ApplyFeatureService', request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyFeatureServiceRequest.SerializeToString, @@ -157,6 +168,26 @@ def __init__(self, channel): request_serializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteSavedDatasetRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, ) + self.CreateDatasetFromRetrieval = channel.unary_unary( + '/feast.registry.RegistryServer/CreateDatasetFromRetrieval', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.CreateDatasetFromRetrievalRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.CreateDatasetFromRetrievalResponse.FromString, + ) + self.GetDatasetData = channel.unary_unary( + '/feast.registry.RegistryServer/GetDatasetData', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetDatasetDataRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetDatasetDataResponse.FromString, + ) + self.GetDatasetJobStatus = channel.unary_unary( + '/feast.registry.RegistryServer/GetDatasetJobStatus', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetDatasetJobStatusRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetDatasetJobStatusResponse.FromString, + ) + self.ListDatasetJobs = channel.unary_unary( + '/feast.registry.RegistryServer/ListDatasetJobs', + request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListDatasetJobsRequest.SerializeToString, + response_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListDatasetJobsResponse.FromString, + ) self.ApplyValidationReference = channel.unary_unary( '/feast.registry.RegistryServer/ApplyValidationReference', request_serializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyValidationReferenceRequest.SerializeToString, @@ -391,6 +422,19 @@ def ListOnDemandFeatureViews(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetLabelView(self, request, context): + """LabelView RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListLabelViews(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def ApplyFeatureService(self, request, context): """FeatureService RPCs """ @@ -441,6 +485,31 @@ def DeleteSavedDataset(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def CreateDatasetFromRetrieval(self, request, context): + """SavedDataset Operation RPCs + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetDatasetData(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetDatasetJobStatus(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListDatasetJobs(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def ApplyValidationReference(self, request, context): """ValidationReference RPCs """ @@ -677,6 +746,16 @@ def add_RegistryServerServicer_to_server(servicer, server): request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsRequest.FromString, response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListOnDemandFeatureViewsResponse.SerializeToString, ), + 'GetLabelView': grpc.unary_unary_rpc_method_handler( + servicer.GetLabelView, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetLabelViewRequest.FromString, + response_serializer=feast_dot_core_dot_LabelView__pb2.LabelView.SerializeToString, + ), + 'ListLabelViews': grpc.unary_unary_rpc_method_handler( + servicer.ListLabelViews, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListLabelViewsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListLabelViewsResponse.SerializeToString, + ), 'ApplyFeatureService': grpc.unary_unary_rpc_method_handler( servicer.ApplyFeatureService, request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyFeatureServiceRequest.FromString, @@ -717,6 +796,26 @@ def add_RegistryServerServicer_to_server(servicer, server): request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.DeleteSavedDatasetRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, ), + 'CreateDatasetFromRetrieval': grpc.unary_unary_rpc_method_handler( + servicer.CreateDatasetFromRetrieval, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.CreateDatasetFromRetrievalRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.CreateDatasetFromRetrievalResponse.SerializeToString, + ), + 'GetDatasetData': grpc.unary_unary_rpc_method_handler( + servicer.GetDatasetData, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetDatasetDataRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetDatasetDataResponse.SerializeToString, + ), + 'GetDatasetJobStatus': grpc.unary_unary_rpc_method_handler( + servicer.GetDatasetJobStatus, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.GetDatasetJobStatusRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.GetDatasetJobStatusResponse.SerializeToString, + ), + 'ListDatasetJobs': grpc.unary_unary_rpc_method_handler( + servicer.ListDatasetJobs, + request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ListDatasetJobsRequest.FromString, + response_serializer=feast_dot_registry_dot_RegistryServer__pb2.ListDatasetJobsResponse.SerializeToString, + ), 'ApplyValidationReference': grpc.unary_unary_rpc_method_handler( servicer.ApplyValidationReference, request_deserializer=feast_dot_registry_dot_RegistryServer__pb2.ApplyValidationReferenceRequest.FromString, @@ -1148,6 +1247,40 @@ def ListOnDemandFeatureViews(request, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod + def GetLabelView(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetLabelView', + feast_dot_registry_dot_RegistryServer__pb2.GetLabelViewRequest.SerializeToString, + feast_dot_core_dot_LabelView__pb2.LabelView.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListLabelViews(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListLabelViews', + feast_dot_registry_dot_RegistryServer__pb2.ListLabelViewsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListLabelViewsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod def ApplyFeatureService(request, target, @@ -1284,6 +1417,74 @@ def DeleteSavedDataset(request, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod + def CreateDatasetFromRetrieval(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/CreateDatasetFromRetrieval', + feast_dot_registry_dot_RegistryServer__pb2.CreateDatasetFromRetrievalRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.CreateDatasetFromRetrievalResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetDatasetData(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetDatasetData', + feast_dot_registry_dot_RegistryServer__pb2.GetDatasetDataRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.GetDatasetDataResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetDatasetJobStatus(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/GetDatasetJobStatus', + feast_dot_registry_dot_RegistryServer__pb2.GetDatasetJobStatusRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.GetDatasetJobStatusResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ListDatasetJobs(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/feast.registry.RegistryServer/ListDatasetJobs', + feast_dot_registry_dot_RegistryServer__pb2.ListDatasetJobsRequest.SerializeToString, + feast_dot_registry_dot_RegistryServer__pb2.ListDatasetJobsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod def ApplyValidationReference(request, target, diff --git a/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.py b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.py index ce4db37a658..586938289ae 100644 --- a/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.py +++ b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.py @@ -13,32 +13,41 @@ from feast.protos.feast.serving import ServingService_pb2 as feast_dot_serving_dot_ServingService__pb2 +from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x66\x65\x61st/serving/GrpcServer.proto\x1a\"feast/serving/ServingService.proto\"\xb3\x01\n\x0bPushRequest\x12,\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0b\x32\x1a.PushRequest.FeaturesEntry\x12\x1b\n\x13stream_feature_view\x18\x02 \x01(\t\x12\x1c\n\x14\x61llow_registry_cache\x18\x03 \x01(\x08\x12\n\n\x02to\x18\x04 \x01(\t\x1a/\n\rFeaturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x1e\n\x0cPushResponse\x12\x0e\n\x06status\x18\x01 \x01(\x08\"\xc1\x01\n\x19WriteToOnlineStoreRequest\x12:\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0b\x32(.WriteToOnlineStoreRequest.FeaturesEntry\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x02 \x01(\t\x12\x1c\n\x14\x61llow_registry_cache\x18\x03 \x01(\x08\x1a/\n\rFeaturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\",\n\x1aWriteToOnlineStoreResponse\x12\x0e\n\x06status\x18\x01 \x01(\x08\x32\xf1\x01\n\x11GrpcFeatureServer\x12%\n\x04Push\x12\x0c.PushRequest\x1a\r.PushResponse\"\x00\x12M\n\x12WriteToOnlineStore\x12\x1a.WriteToOnlineStoreRequest\x1a\x1b.WriteToOnlineStoreResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponseB4Z2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x66\x65\x61st/serving/GrpcServer.proto\x1a\"feast/serving/ServingService.proto\x1a\x17\x66\x65\x61st/types/Value.proto\"\xb6\x02\n\x0bPushRequest\x12,\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0b\x32\x1a.PushRequest.FeaturesEntry\x12\x1b\n\x13stream_feature_view\x18\x02 \x01(\t\x12\x1c\n\x14\x61llow_registry_cache\x18\x03 \x01(\x08\x12\n\n\x02to\x18\x04 \x01(\t\x12\x37\n\x0etyped_features\x18\x05 \x03(\x0b\x32\x1f.PushRequest.TypedFeaturesEntry\x1a/\n\rFeaturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aH\n\x12TypedFeaturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\"\x1e\n\x0cPushResponse\x12\x0e\n\x06status\x18\x01 \x01(\x08\"\xd2\x02\n\x19WriteToOnlineStoreRequest\x12:\n\x08\x66\x65\x61tures\x18\x01 \x03(\x0b\x32(.WriteToOnlineStoreRequest.FeaturesEntry\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x02 \x01(\t\x12\x1c\n\x14\x61llow_registry_cache\x18\x03 \x01(\x08\x12\x45\n\x0etyped_features\x18\x04 \x03(\x0b\x32-.WriteToOnlineStoreRequest.TypedFeaturesEntry\x1a/\n\rFeaturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aH\n\x12TypedFeaturesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\",\n\x1aWriteToOnlineStoreResponse\x12\x0e\n\x06status\x18\x01 \x01(\x08\x32\xf1\x01\n\x11GrpcFeatureServer\x12%\n\x04Push\x12\x0c.PushRequest\x1a\r.PushResponse\"\x00\x12M\n\x12WriteToOnlineStore\x12\x1a.WriteToOnlineStoreRequest\x1a\x1b.WriteToOnlineStoreResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponseB]\n\x13\x66\x65\x61st.proto.servingB\x12GrpcServerAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'feast.serving.GrpcServer_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: _globals['DESCRIPTOR']._options = None - _globals['DESCRIPTOR']._serialized_options = b'Z2github.com/feast-dev/feast/go/protos/feast/serving' + _globals['DESCRIPTOR']._serialized_options = b'\n\023feast.proto.servingB\022GrpcServerAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/serving' _globals['_PUSHREQUEST_FEATURESENTRY']._options = None _globals['_PUSHREQUEST_FEATURESENTRY']._serialized_options = b'8\001' + _globals['_PUSHREQUEST_TYPEDFEATURESENTRY']._options = None + _globals['_PUSHREQUEST_TYPEDFEATURESENTRY']._serialized_options = b'8\001' _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._options = None _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._serialized_options = b'8\001' - _globals['_PUSHREQUEST']._serialized_start=71 - _globals['_PUSHREQUEST']._serialized_end=250 - _globals['_PUSHREQUEST_FEATURESENTRY']._serialized_start=203 - _globals['_PUSHREQUEST_FEATURESENTRY']._serialized_end=250 - _globals['_PUSHRESPONSE']._serialized_start=252 - _globals['_PUSHRESPONSE']._serialized_end=282 - _globals['_WRITETOONLINESTOREREQUEST']._serialized_start=285 - _globals['_WRITETOONLINESTOREREQUEST']._serialized_end=478 - _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._serialized_start=203 - _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._serialized_end=250 - _globals['_WRITETOONLINESTORERESPONSE']._serialized_start=480 - _globals['_WRITETOONLINESTORERESPONSE']._serialized_end=524 - _globals['_GRPCFEATURESERVER']._serialized_start=527 - _globals['_GRPCFEATURESERVER']._serialized_end=768 + _globals['_WRITETOONLINESTOREREQUEST_TYPEDFEATURESENTRY']._options = None + _globals['_WRITETOONLINESTOREREQUEST_TYPEDFEATURESENTRY']._serialized_options = b'8\001' + _globals['_PUSHREQUEST']._serialized_start=96 + _globals['_PUSHREQUEST']._serialized_end=406 + _globals['_PUSHREQUEST_FEATURESENTRY']._serialized_start=285 + _globals['_PUSHREQUEST_FEATURESENTRY']._serialized_end=332 + _globals['_PUSHREQUEST_TYPEDFEATURESENTRY']._serialized_start=334 + _globals['_PUSHREQUEST_TYPEDFEATURESENTRY']._serialized_end=406 + _globals['_PUSHRESPONSE']._serialized_start=408 + _globals['_PUSHRESPONSE']._serialized_end=438 + _globals['_WRITETOONLINESTOREREQUEST']._serialized_start=441 + _globals['_WRITETOONLINESTOREREQUEST']._serialized_end=779 + _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._serialized_start=285 + _globals['_WRITETOONLINESTOREREQUEST_FEATURESENTRY']._serialized_end=332 + _globals['_WRITETOONLINESTOREREQUEST_TYPEDFEATURESENTRY']._serialized_start=334 + _globals['_WRITETOONLINESTOREREQUEST_TYPEDFEATURESENTRY']._serialized_end=406 + _globals['_WRITETOONLINESTORERESPONSE']._serialized_start=781 + _globals['_WRITETOONLINESTORERESPONSE']._serialized_end=825 + _globals['_GRPCFEATURESERVER']._serialized_start=828 + _globals['_GRPCFEATURESERVER']._serialized_end=1069 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.pyi b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.pyi index 54964f46e58..a83cd87a16e 100644 --- a/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.pyi +++ b/sdk/python/feast/protos/feast/serving/GrpcServer_pb2.pyi @@ -4,6 +4,7 @@ isort:skip_file """ import builtins import collections.abc +import feast.types.Value_pb2 import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.message @@ -34,15 +35,35 @@ class PushRequest(google.protobuf.message.Message): ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + class TypedFeaturesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> feast.types.Value_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: feast.types.Value_pb2.Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + FEATURES_FIELD_NUMBER: builtins.int STREAM_FEATURE_VIEW_FIELD_NUMBER: builtins.int ALLOW_REGISTRY_CACHE_FIELD_NUMBER: builtins.int TO_FIELD_NUMBER: builtins.int + TYPED_FEATURES_FIELD_NUMBER: builtins.int @property def features(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... stream_feature_view: builtins.str allow_registry_cache: builtins.bool to: builtins.str + @property + def typed_features(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, feast.types.Value_pb2.Value]: ... def __init__( self, *, @@ -50,8 +71,9 @@ class PushRequest(google.protobuf.message.Message): stream_feature_view: builtins.str = ..., allow_registry_cache: builtins.bool = ..., to: builtins.str = ..., + typed_features: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.Value] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["allow_registry_cache", b"allow_registry_cache", "features", b"features", "stream_feature_view", b"stream_feature_view", "to", b"to"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_registry_cache", b"allow_registry_cache", "features", b"features", "stream_feature_view", b"stream_feature_view", "to", b"to", "typed_features", b"typed_features"]) -> None: ... global___PushRequest = PushRequest @@ -87,21 +109,42 @@ class WriteToOnlineStoreRequest(google.protobuf.message.Message): ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + class TypedFeaturesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> feast.types.Value_pb2.Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: feast.types.Value_pb2.Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + FEATURES_FIELD_NUMBER: builtins.int FEATURE_VIEW_NAME_FIELD_NUMBER: builtins.int ALLOW_REGISTRY_CACHE_FIELD_NUMBER: builtins.int + TYPED_FEATURES_FIELD_NUMBER: builtins.int @property def features(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: ... feature_view_name: builtins.str allow_registry_cache: builtins.bool + @property + def typed_features(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, feast.types.Value_pb2.Value]: ... def __init__( self, *, features: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., feature_view_name: builtins.str = ..., allow_registry_cache: builtins.bool = ..., + typed_features: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.Value] | None = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["allow_registry_cache", b"allow_registry_cache", "feature_view_name", b"feature_view_name", "features", b"features"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["allow_registry_cache", b"allow_registry_cache", "feature_view_name", b"feature_view_name", "features", b"features", "typed_features", b"typed_features"]) -> None: ... global___WriteToOnlineStoreRequest = WriteToOnlineStoreRequest diff --git a/sdk/python/feast/protos/feast/serving/ServingService_pb2.py b/sdk/python/feast/protos/feast/serving/ServingService_pb2.py index fa866640577..82ade7eb988 100644 --- a/sdk/python/feast/protos/feast/serving/ServingService_pb2.py +++ b/sdk/python/feast/protos/feast/serving/ServingService_pb2.py @@ -16,7 +16,7 @@ from feast.protos.feast.types import Value_pb2 as feast_dot_types_dot_Value__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/serving/ServingService.proto\x12\rfeast.serving\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17\x66\x65\x61st/types/Value.proto\"\x1c\n\x1aGetFeastServingInfoRequest\".\n\x1bGetFeastServingInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"E\n\x12\x46\x65\x61tureReferenceV2\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x02 \x01(\t\"\xfd\x02\n\x1aGetOnlineFeaturesRequestV2\x12\x33\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32!.feast.serving.FeatureReferenceV2\x12H\n\x0b\x65ntity_rows\x18\x02 \x03(\x0b\x32\x33.feast.serving.GetOnlineFeaturesRequestV2.EntityRow\x12\x0f\n\x07project\x18\x05 \x01(\t\x1a\xce\x01\n\tEntityRow\x12-\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12O\n\x06\x66ields\x18\x02 \x03(\x0b\x32?.feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry\x1a\x41\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\"\x1a\n\x0b\x46\x65\x61tureList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\xc8\x03\n\x18GetOnlineFeaturesRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12G\n\x08\x65ntities\x18\x03 \x03(\x0b\x32\x35.feast.serving.GetOnlineFeaturesRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12T\n\x0frequest_context\x18\x05 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRequest.RequestContextEntry\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\xd2\x02\n\x19GetOnlineFeaturesResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12G\n\x07results\x18\x02 \x03(\x0b\x32\x36.feast.serving.GetOnlineFeaturesResponse.FeatureVector\x12\x0e\n\x06status\x18\x03 \x01(\x08\x1a\x97\x01\n\rFeatureVector\x12\"\n\x06values\x18\x01 \x03(\x0b\x32\x12.feast.types.Value\x12,\n\x08statuses\x18\x02 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"V\n!GetOnlineFeaturesResponseMetadata\x12\x31\n\rfeature_names\x18\x01 \x01(\x0b\x32\x1a.feast.serving.FeatureList*[\n\x0b\x46ieldStatus\x12\x0b\n\x07INVALID\x10\x00\x12\x0b\n\x07PRESENT\x10\x01\x12\x0e\n\nNULL_VALUE\x10\x02\x12\r\n\tNOT_FOUND\x10\x03\x12\x13\n\x0fOUTSIDE_MAX_AGE\x10\x04\x32\xe6\x01\n\x0eServingService\x12l\n\x13GetFeastServingInfo\x12).feast.serving.GetFeastServingInfoRequest\x1a*.feast.serving.GetFeastServingInfoResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponseBZ\n\x13\x66\x65\x61st.proto.servingB\x0fServingAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"feast/serving/ServingService.proto\x12\rfeast.serving\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17\x66\x65\x61st/types/Value.proto\"\x1c\n\x1aGetFeastServingInfoRequest\".\n\x1bGetFeastServingInfoResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"E\n\x12\x46\x65\x61tureReferenceV2\x12\x19\n\x11\x66\x65\x61ture_view_name\x18\x01 \x01(\t\x12\x14\n\x0c\x66\x65\x61ture_name\x18\x02 \x01(\t\"\xfd\x02\n\x1aGetOnlineFeaturesRequestV2\x12\x33\n\x08\x66\x65\x61tures\x18\x04 \x03(\x0b\x32!.feast.serving.FeatureReferenceV2\x12H\n\x0b\x65ntity_rows\x18\x02 \x03(\x0b\x32\x33.feast.serving.GetOnlineFeaturesRequestV2.EntityRow\x12\x0f\n\x07project\x18\x05 \x01(\t\x1a\xce\x01\n\tEntityRow\x12-\n\ttimestamp\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12O\n\x06\x66ields\x18\x02 \x03(\x0b\x32?.feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry\x1a\x41\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\"\x1a\n\x0b\x46\x65\x61tureList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\xf7\x03\n\x18GetOnlineFeaturesRequest\x12\x19\n\x0f\x66\x65\x61ture_service\x18\x01 \x01(\tH\x00\x12.\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1a.feast.serving.FeatureListH\x00\x12G\n\x08\x65ntities\x18\x03 \x03(\x0b\x32\x35.feast.serving.GetOnlineFeaturesRequest.EntitiesEntry\x12\x1a\n\x12\x66ull_feature_names\x18\x04 \x01(\x08\x12T\n\x0frequest_context\x18\x05 \x03(\x0b\x32;.feast.serving.GetOnlineFeaturesRequest.RequestContextEntry\x12-\n%include_feature_view_version_metadata\x18\x06 \x01(\x08\x1aK\n\rEntitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x1aQ\n\x13RequestContextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.feast.types.RepeatedValue:\x02\x38\x01\x42\x06\n\x04kind\"\xd2\x02\n\x19GetOnlineFeaturesResponse\x12\x42\n\x08metadata\x18\x01 \x01(\x0b\x32\x30.feast.serving.GetOnlineFeaturesResponseMetadata\x12G\n\x07results\x18\x02 \x03(\x0b\x32\x36.feast.serving.GetOnlineFeaturesResponse.FeatureVector\x12\x0e\n\x06status\x18\x03 \x01(\x08\x1a\x97\x01\n\rFeatureVector\x12\"\n\x06values\x18\x01 \x03(\x0b\x32\x12.feast.types.Value\x12,\n\x08statuses\x18\x02 \x03(\x0e\x32\x1a.feast.serving.FieldStatus\x12\x34\n\x10\x65vent_timestamps\x18\x03 \x03(\x0b\x32\x1a.google.protobuf.Timestamp\"4\n\x13\x46\x65\x61tureViewMetadata\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\x05\"\x99\x01\n!GetOnlineFeaturesResponseMetadata\x12\x31\n\rfeature_names\x18\x01 \x01(\x0b\x32\x1a.feast.serving.FeatureList\x12\x41\n\x15\x66\x65\x61ture_view_metadata\x18\x02 \x03(\x0b\x32\".feast.serving.FeatureViewMetadata*[\n\x0b\x46ieldStatus\x12\x0b\n\x07INVALID\x10\x00\x12\x0b\n\x07PRESENT\x10\x01\x12\x0e\n\nNULL_VALUE\x10\x02\x12\r\n\tNOT_FOUND\x10\x03\x12\x13\n\x0fOUTSIDE_MAX_AGE\x10\x04\x32\xe6\x01\n\x0eServingService\x12l\n\x13GetFeastServingInfo\x12).feast.serving.GetFeastServingInfoRequest\x1a*.feast.serving.GetFeastServingInfoResponse\x12\x66\n\x11GetOnlineFeatures\x12\'.feast.serving.GetOnlineFeaturesRequest\x1a(.feast.serving.GetOnlineFeaturesResponseBZ\n\x13\x66\x65\x61st.proto.servingB\x0fServingAPIProtoZ2github.com/feast-dev/feast/go/protos/feast/servingb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -30,8 +30,8 @@ _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_options = b'8\001' _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._options = None _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_options = b'8\001' - _globals['_FIELDSTATUS']._serialized_start=1560 - _globals['_FIELDSTATUS']._serialized_end=1651 + _globals['_FIELDSTATUS']._serialized_start=1729 + _globals['_FIELDSTATUS']._serialized_end=1820 _globals['_GETFEASTSERVINGINFOREQUEST']._serialized_start=111 _globals['_GETFEASTSERVINGINFOREQUEST']._serialized_end=139 _globals['_GETFEASTSERVINGINFORESPONSE']._serialized_start=141 @@ -47,17 +47,19 @@ _globals['_FEATURELIST']._serialized_start=644 _globals['_FEATURELIST']._serialized_end=670 _globals['_GETONLINEFEATURESREQUEST']._serialized_start=673 - _globals['_GETONLINEFEATURESREQUEST']._serialized_end=1129 - _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_start=963 - _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_end=1038 - _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1040 - _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1121 - _globals['_GETONLINEFEATURESRESPONSE']._serialized_start=1132 - _globals['_GETONLINEFEATURESRESPONSE']._serialized_end=1470 - _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_start=1319 - _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_end=1470 - _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_start=1472 - _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_end=1558 - _globals['_SERVINGSERVICE']._serialized_start=1654 - _globals['_SERVINGSERVICE']._serialized_end=1884 + _globals['_GETONLINEFEATURESREQUEST']._serialized_end=1176 + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_start=1010 + _globals['_GETONLINEFEATURESREQUEST_ENTITIESENTRY']._serialized_end=1085 + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_start=1087 + _globals['_GETONLINEFEATURESREQUEST_REQUESTCONTEXTENTRY']._serialized_end=1168 + _globals['_GETONLINEFEATURESRESPONSE']._serialized_start=1179 + _globals['_GETONLINEFEATURESRESPONSE']._serialized_end=1517 + _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_start=1366 + _globals['_GETONLINEFEATURESRESPONSE_FEATUREVECTOR']._serialized_end=1517 + _globals['_FEATUREVIEWMETADATA']._serialized_start=1519 + _globals['_FEATUREVIEWMETADATA']._serialized_end=1571 + _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_start=1574 + _globals['_GETONLINEFEATURESRESPONSEMETADATA']._serialized_end=1727 + _globals['_SERVINGSERVICE']._serialized_start=1823 + _globals['_SERVINGSERVICE']._serialized_end=2053 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi b/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi index 3c5e57ae45a..1804ce0428e 100644 --- a/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi +++ b/sdk/python/feast/protos/feast/serving/ServingService_pb2.pyi @@ -253,6 +253,7 @@ class GetOnlineFeaturesRequest(google.protobuf.message.Message): ENTITIES_FIELD_NUMBER: builtins.int FULL_FEATURE_NAMES_FIELD_NUMBER: builtins.int REQUEST_CONTEXT_FIELD_NUMBER: builtins.int + INCLUDE_FEATURE_VIEW_VERSION_METADATA_FIELD_NUMBER: builtins.int feature_service: builtins.str @property def features(self) -> global___FeatureList: ... @@ -268,6 +269,8 @@ class GetOnlineFeaturesRequest(google.protobuf.message.Message): (was moved to dedicated parameter to avoid unnecessary separation logic on serving side) A map of variable name -> list of values """ + include_feature_view_version_metadata: builtins.bool + """Whether to include feature view version metadata in the response""" def __init__( self, *, @@ -276,9 +279,10 @@ class GetOnlineFeaturesRequest(google.protobuf.message.Message): entities: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.RepeatedValue] | None = ..., full_feature_names: builtins.bool = ..., request_context: collections.abc.Mapping[builtins.str, feast.types.Value_pb2.RepeatedValue] | None = ..., + include_feature_view_version_metadata: builtins.bool = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["feature_service", b"feature_service", "features", b"features", "kind", b"kind"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "kind", b"kind", "request_context", b"request_context"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["entities", b"entities", "feature_service", b"feature_service", "features", b"features", "full_feature_names", b"full_feature_names", "include_feature_view_version_metadata", b"include_feature_view_version_metadata", "kind", b"kind", "request_context", b"request_context"]) -> None: ... def WhichOneof(self, oneof_group: typing_extensions.Literal["kind", b"kind"]) -> typing_extensions.Literal["feature_service", "features"] | None: ... global___GetOnlineFeaturesRequest = GetOnlineFeaturesRequest @@ -330,18 +334,43 @@ class GetOnlineFeaturesResponse(google.protobuf.message.Message): global___GetOnlineFeaturesResponse = GetOnlineFeaturesResponse +class FeatureViewMetadata(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + name: builtins.str + """Feature view name (e.g., "driver_stats")""" + version: builtins.int + """Version number (e.g., 2)""" + def __init__( + self, + *, + name: builtins.str = ..., + version: builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["name", b"name", "version", b"version"]) -> None: ... + +global___FeatureViewMetadata = FeatureViewMetadata + class GetOnlineFeaturesResponseMetadata(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor FEATURE_NAMES_FIELD_NUMBER: builtins.int + FEATURE_VIEW_METADATA_FIELD_NUMBER: builtins.int + @property + def feature_names(self) -> global___FeatureList: + """Clean feature names without @v2 syntax""" @property - def feature_names(self) -> global___FeatureList: ... + def feature_view_metadata(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___FeatureViewMetadata]: + """Only populated when requested""" def __init__( self, *, feature_names: global___FeatureList | None = ..., + feature_view_metadata: collections.abc.Iterable[global___FeatureViewMetadata] | None = ..., ) -> None: ... def HasField(self, field_name: typing_extensions.Literal["feature_names", b"feature_names"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["feature_names", b"feature_names"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["feature_names", b"feature_names", "feature_view_metadata", b"feature_view_metadata"]) -> None: ... global___GetOnlineFeaturesResponseMetadata = GetOnlineFeaturesResponseMetadata diff --git a/sdk/python/feast/protos/feast/types/Value_pb2.py b/sdk/python/feast/protos/feast/types/Value_pb2.py index 18ee3311808..96d93ceef56 100644 --- a/sdk/python/feast/protos/feast/types/Value_pb2.py +++ b/sdk/python/feast/protos/feast/types/Value_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x66\x65\x61st/types/Value.proto\x12\x0b\x66\x65\x61st.types\"\x97\x02\n\tValueType\"\x89\x02\n\x04\x45num\x12\x0b\n\x07INVALID\x10\x00\x12\t\n\x05\x42YTES\x10\x01\x12\n\n\x06STRING\x10\x02\x12\t\n\x05INT32\x10\x03\x12\t\n\x05INT64\x10\x04\x12\n\n\x06\x44OUBLE\x10\x05\x12\t\n\x05\x46LOAT\x10\x06\x12\x08\n\x04\x42OOL\x10\x07\x12\x12\n\x0eUNIX_TIMESTAMP\x10\x08\x12\x0e\n\nBYTES_LIST\x10\x0b\x12\x0f\n\x0bSTRING_LIST\x10\x0c\x12\x0e\n\nINT32_LIST\x10\r\x12\x0e\n\nINT64_LIST\x10\x0e\x12\x0f\n\x0b\x44OUBLE_LIST\x10\x0f\x12\x0e\n\nFLOAT_LIST\x10\x10\x12\r\n\tBOOL_LIST\x10\x11\x12\x17\n\x13UNIX_TIMESTAMP_LIST\x10\x12\x12\x08\n\x04NULL\x10\x13\"\x82\x05\n\x05Value\x12\x13\n\tbytes_val\x18\x01 \x01(\x0cH\x00\x12\x14\n\nstring_val\x18\x02 \x01(\tH\x00\x12\x13\n\tint32_val\x18\x03 \x01(\x05H\x00\x12\x13\n\tint64_val\x18\x04 \x01(\x03H\x00\x12\x14\n\ndouble_val\x18\x05 \x01(\x01H\x00\x12\x13\n\tfloat_val\x18\x06 \x01(\x02H\x00\x12\x12\n\x08\x62ool_val\x18\x07 \x01(\x08H\x00\x12\x1c\n\x12unix_timestamp_val\x18\x08 \x01(\x03H\x00\x12\x30\n\x0e\x62ytes_list_val\x18\x0b \x01(\x0b\x32\x16.feast.types.BytesListH\x00\x12\x32\n\x0fstring_list_val\x18\x0c \x01(\x0b\x32\x17.feast.types.StringListH\x00\x12\x30\n\x0eint32_list_val\x18\r \x01(\x0b\x32\x16.feast.types.Int32ListH\x00\x12\x30\n\x0eint64_list_val\x18\x0e \x01(\x0b\x32\x16.feast.types.Int64ListH\x00\x12\x32\n\x0f\x64ouble_list_val\x18\x0f \x01(\x0b\x32\x17.feast.types.DoubleListH\x00\x12\x30\n\x0e\x66loat_list_val\x18\x10 \x01(\x0b\x32\x16.feast.types.FloatListH\x00\x12.\n\rbool_list_val\x18\x11 \x01(\x0b\x32\x15.feast.types.BoolListH\x00\x12\x39\n\x17unix_timestamp_list_val\x18\x12 \x01(\x0b\x32\x16.feast.types.Int64ListH\x00\x12%\n\x08null_val\x18\x13 \x01(\x0e\x32\x11.feast.types.NullH\x00\x42\x05\n\x03val\"\x18\n\tBytesList\x12\x0b\n\x03val\x18\x01 \x03(\x0c\"\x19\n\nStringList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\x18\n\tInt32List\x12\x0b\n\x03val\x18\x01 \x03(\x05\"\x18\n\tInt64List\x12\x0b\n\x03val\x18\x01 \x03(\x03\"\x19\n\nDoubleList\x12\x0b\n\x03val\x18\x01 \x03(\x01\"\x18\n\tFloatList\x12\x0b\n\x03val\x18\x01 \x03(\x02\"\x17\n\x08\x42oolList\x12\x0b\n\x03val\x18\x01 \x03(\x08\"0\n\rRepeatedValue\x12\x1f\n\x03val\x18\x01 \x03(\x0b\x32\x12.feast.types.Value*\x10\n\x04Null\x12\x08\n\x04NULL\x10\x00\x42Q\n\x11\x66\x65\x61st.proto.typesB\nValueProtoZ0github.com/feast-dev/feast/go/protos/feast/typesb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x66\x65\x61st/types/Value.proto\x12\x0b\x66\x65\x61st.types\"\xb7\x05\n\tValueType\"\xa9\x05\n\x04\x45num\x12\x0b\n\x07INVALID\x10\x00\x12\t\n\x05\x42YTES\x10\x01\x12\n\n\x06STRING\x10\x02\x12\t\n\x05INT32\x10\x03\x12\t\n\x05INT64\x10\x04\x12\n\n\x06\x44OUBLE\x10\x05\x12\t\n\x05\x46LOAT\x10\x06\x12\x08\n\x04\x42OOL\x10\x07\x12\x12\n\x0eUNIX_TIMESTAMP\x10\x08\x12\x0e\n\nBYTES_LIST\x10\x0b\x12\x0f\n\x0bSTRING_LIST\x10\x0c\x12\x0e\n\nINT32_LIST\x10\r\x12\x0e\n\nINT64_LIST\x10\x0e\x12\x0f\n\x0b\x44OUBLE_LIST\x10\x0f\x12\x0e\n\nFLOAT_LIST\x10\x10\x12\r\n\tBOOL_LIST\x10\x11\x12\x17\n\x13UNIX_TIMESTAMP_LIST\x10\x12\x12\x08\n\x04NULL\x10\x13\x12\x07\n\x03MAP\x10\x14\x12\x0c\n\x08MAP_LIST\x10\x15\x12\r\n\tBYTES_SET\x10\x16\x12\x0e\n\nSTRING_SET\x10\x17\x12\r\n\tINT32_SET\x10\x18\x12\r\n\tINT64_SET\x10\x19\x12\x0e\n\nDOUBLE_SET\x10\x1a\x12\r\n\tFLOAT_SET\x10\x1b\x12\x0c\n\x08\x42OOL_SET\x10\x1c\x12\x16\n\x12UNIX_TIMESTAMP_SET\x10\x1d\x12\x08\n\x04JSON\x10 \x12\r\n\tJSON_LIST\x10!\x12\n\n\x06STRUCT\x10\"\x12\x0f\n\x0bSTRUCT_LIST\x10#\x12\x08\n\x04UUID\x10$\x12\r\n\tTIME_UUID\x10%\x12\r\n\tUUID_LIST\x10&\x12\x12\n\x0eTIME_UUID_LIST\x10\'\x12\x0c\n\x08UUID_SET\x10(\x12\x11\n\rTIME_UUID_SET\x10)\x12\x0e\n\nVALUE_LIST\x10*\x12\r\n\tVALUE_SET\x10+\x12\x0b\n\x07\x44\x45\x43IMAL\x10,\x12\x10\n\x0c\x44\x45\x43IMAL_LIST\x10-\x12\x0f\n\x0b\x44\x45\x43IMAL_SET\x10.\x12\x0e\n\nSCALAR_MAP\x10/\x12\x13\n\x0fZONED_TIMESTAMP\x10\x30\"\xc6\x0e\n\x05Value\x12\x13\n\tbytes_val\x18\x01 \x01(\x0cH\x00\x12\x14\n\nstring_val\x18\x02 \x01(\tH\x00\x12\x13\n\tint32_val\x18\x03 \x01(\x05H\x00\x12\x13\n\tint64_val\x18\x04 \x01(\x03H\x00\x12\x14\n\ndouble_val\x18\x05 \x01(\x01H\x00\x12\x13\n\tfloat_val\x18\x06 \x01(\x02H\x00\x12\x12\n\x08\x62ool_val\x18\x07 \x01(\x08H\x00\x12\x1c\n\x12unix_timestamp_val\x18\x08 \x01(\x03H\x00\x12\x30\n\x0e\x62ytes_list_val\x18\x0b \x01(\x0b\x32\x16.feast.types.BytesListH\x00\x12\x32\n\x0fstring_list_val\x18\x0c \x01(\x0b\x32\x17.feast.types.StringListH\x00\x12\x30\n\x0eint32_list_val\x18\r \x01(\x0b\x32\x16.feast.types.Int32ListH\x00\x12\x30\n\x0eint64_list_val\x18\x0e \x01(\x0b\x32\x16.feast.types.Int64ListH\x00\x12\x32\n\x0f\x64ouble_list_val\x18\x0f \x01(\x0b\x32\x17.feast.types.DoubleListH\x00\x12\x30\n\x0e\x66loat_list_val\x18\x10 \x01(\x0b\x32\x16.feast.types.FloatListH\x00\x12.\n\rbool_list_val\x18\x11 \x01(\x0b\x32\x15.feast.types.BoolListH\x00\x12\x39\n\x17unix_timestamp_list_val\x18\x12 \x01(\x0b\x32\x16.feast.types.Int64ListH\x00\x12%\n\x08null_val\x18\x13 \x01(\x0e\x32\x11.feast.types.NullH\x00\x12#\n\x07map_val\x18\x14 \x01(\x0b\x32\x10.feast.types.MapH\x00\x12,\n\x0cmap_list_val\x18\x15 \x01(\x0b\x32\x14.feast.types.MapListH\x00\x12.\n\rbytes_set_val\x18\x16 \x01(\x0b\x32\x15.feast.types.BytesSetH\x00\x12\x30\n\x0estring_set_val\x18\x17 \x01(\x0b\x32\x16.feast.types.StringSetH\x00\x12.\n\rint32_set_val\x18\x18 \x01(\x0b\x32\x15.feast.types.Int32SetH\x00\x12.\n\rint64_set_val\x18\x19 \x01(\x0b\x32\x15.feast.types.Int64SetH\x00\x12\x30\n\x0e\x64ouble_set_val\x18\x1a \x01(\x0b\x32\x16.feast.types.DoubleSetH\x00\x12.\n\rfloat_set_val\x18\x1b \x01(\x0b\x32\x15.feast.types.FloatSetH\x00\x12,\n\x0c\x62ool_set_val\x18\x1c \x01(\x0b\x32\x14.feast.types.BoolSetH\x00\x12\x37\n\x16unix_timestamp_set_val\x18\x1d \x01(\x0b\x32\x15.feast.types.Int64SetH\x00\x12\x12\n\x08json_val\x18 \x01(\tH\x00\x12\x30\n\rjson_list_val\x18! \x01(\x0b\x32\x17.feast.types.StringListH\x00\x12&\n\nstruct_val\x18\" \x01(\x0b\x32\x10.feast.types.MapH\x00\x12/\n\x0fstruct_list_val\x18# \x01(\x0b\x32\x14.feast.types.MapListH\x00\x12\x12\n\x08uuid_val\x18$ \x01(\tH\x00\x12\x17\n\rtime_uuid_val\x18% \x01(\tH\x00\x12\x30\n\ruuid_list_val\x18& \x01(\x0b\x32\x17.feast.types.StringListH\x00\x12\x35\n\x12time_uuid_list_val\x18\' \x01(\x0b\x32\x17.feast.types.StringListH\x00\x12.\n\x0cuuid_set_val\x18( \x01(\x0b\x32\x16.feast.types.StringSetH\x00\x12\x33\n\x11time_uuid_set_val\x18) \x01(\x0b\x32\x16.feast.types.StringSetH\x00\x12.\n\x08list_val\x18* \x01(\x0b\x32\x1a.feast.types.RepeatedValueH\x00\x12-\n\x07set_val\x18+ \x01(\x0b\x32\x1a.feast.types.RepeatedValueH\x00\x12\x15\n\x0b\x64\x65\x63imal_val\x18, \x01(\tH\x00\x12\x33\n\x10\x64\x65\x63imal_list_val\x18- \x01(\x0b\x32\x17.feast.types.StringListH\x00\x12\x31\n\x0f\x64\x65\x63imal_set_val\x18. \x01(\x0b\x32\x16.feast.types.StringSetH\x00\x12\x30\n\x0escalar_map_val\x18/ \x01(\x0b\x32\x16.feast.types.ScalarMapH\x00\x12:\n\x13zoned_timestamp_val\x18\x30 \x01(\x0b\x32\x1b.feast.types.ZonedTimestampH\x00\x42\x05\n\x03val\"6\n\x0eZonedTimestamp\x12\x16\n\x0eunix_timestamp\x18\x01 \x01(\x03\x12\x0c\n\x04zone\x18\x02 \x01(\t\"\x18\n\tBytesList\x12\x0b\n\x03val\x18\x01 \x03(\x0c\"\x19\n\nStringList\x12\x0b\n\x03val\x18\x01 \x03(\t\"\x18\n\tInt32List\x12\x0b\n\x03val\x18\x01 \x03(\x05\"\x18\n\tInt64List\x12\x0b\n\x03val\x18\x01 \x03(\x03\"\x19\n\nDoubleList\x12\x0b\n\x03val\x18\x01 \x03(\x01\"\x18\n\tFloatList\x12\x0b\n\x03val\x18\x01 \x03(\x02\"\x17\n\x08\x42oolList\x12\x0b\n\x03val\x18\x01 \x03(\x08\"\x17\n\x08\x42ytesSet\x12\x0b\n\x03val\x18\x01 \x03(\x0c\"\x18\n\tStringSet\x12\x0b\n\x03val\x18\x01 \x03(\t\"\x17\n\x08Int32Set\x12\x0b\n\x03val\x18\x01 \x03(\x05\"\x17\n\x08Int64Set\x12\x0b\n\x03val\x18\x01 \x03(\x03\"\x18\n\tDoubleSet\x12\x0b\n\x03val\x18\x01 \x03(\x01\"\x17\n\x08\x46loatSet\x12\x0b\n\x03val\x18\x01 \x03(\x02\"\x16\n\x07\x42oolSet\x12\x0b\n\x03val\x18\x01 \x03(\x08\"m\n\x03Map\x12&\n\x03val\x18\x01 \x03(\x0b\x32\x19.feast.types.Map.ValEntry\x1a>\n\x08ValEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value:\x02\x38\x01\"(\n\x07MapList\x12\x1d\n\x03val\x18\x01 \x03(\x0b\x32\x10.feast.types.Map\"0\n\rRepeatedValue\x12\x1f\n\x03val\x18\x01 \x03(\x0b\x32\x12.feast.types.Value\"\xef\x01\n\x06MapKey\x12\x13\n\tint32_key\x18\x01 \x01(\x05H\x00\x12\x13\n\tint64_key\x18\x02 \x01(\x03H\x00\x12\x13\n\tfloat_key\x18\x03 \x01(\x02H\x00\x12\x14\n\ndouble_key\x18\x04 \x01(\x01H\x00\x12\x12\n\x08\x62ool_key\x18\x05 \x01(\x08H\x00\x12\x1c\n\x12unix_timestamp_key\x18\x06 \x01(\x03H\x00\x12\x13\n\tbytes_key\x18\x07 \x01(\x0cH\x00\x12\x12\n\x08uuid_key\x18\x08 \x01(\tH\x00\x12\x17\n\rtime_uuid_key\x18\t \x01(\tH\x00\x12\x15\n\x0b\x64\x65\x63imal_key\x18\n \x01(\tH\x00\x42\x05\n\x03key\"U\n\x0eScalarMapEntry\x12 \n\x03key\x18\x01 \x01(\x0b\x32\x13.feast.types.MapKey\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.feast.types.Value\"5\n\tScalarMap\x12(\n\x03val\x18\x01 \x03(\x0b\x32\x1b.feast.types.ScalarMapEntry*\x10\n\x04Null\x12\x08\n\x04NULL\x10\x00\x42Q\n\x11\x66\x65\x61st.proto.typesB\nValueProtoZ0github.com/feast-dev/feast/go/protos/feast/typesb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -22,28 +22,58 @@ if _descriptor._USE_C_DESCRIPTORS == False: _globals['DESCRIPTOR']._options = None _globals['DESCRIPTOR']._serialized_options = b'\n\021feast.proto.typesB\nValueProtoZ0github.com/feast-dev/feast/go/protos/feast/types' - _globals['_NULL']._serialized_start=1200 - _globals['_NULL']._serialized_end=1216 + _globals['_MAP_VALENTRY']._options = None + _globals['_MAP_VALENTRY']._serialized_options = b'8\001' + _globals['_NULL']._serialized_start=3605 + _globals['_NULL']._serialized_end=3621 _globals['_VALUETYPE']._serialized_start=41 - _globals['_VALUETYPE']._serialized_end=320 + _globals['_VALUETYPE']._serialized_end=736 _globals['_VALUETYPE_ENUM']._serialized_start=55 - _globals['_VALUETYPE_ENUM']._serialized_end=320 - _globals['_VALUE']._serialized_start=323 - _globals['_VALUE']._serialized_end=965 - _globals['_BYTESLIST']._serialized_start=967 - _globals['_BYTESLIST']._serialized_end=991 - _globals['_STRINGLIST']._serialized_start=993 - _globals['_STRINGLIST']._serialized_end=1018 - _globals['_INT32LIST']._serialized_start=1020 - _globals['_INT32LIST']._serialized_end=1044 - _globals['_INT64LIST']._serialized_start=1046 - _globals['_INT64LIST']._serialized_end=1070 - _globals['_DOUBLELIST']._serialized_start=1072 - _globals['_DOUBLELIST']._serialized_end=1097 - _globals['_FLOATLIST']._serialized_start=1099 - _globals['_FLOATLIST']._serialized_end=1123 - _globals['_BOOLLIST']._serialized_start=1125 - _globals['_BOOLLIST']._serialized_end=1148 - _globals['_REPEATEDVALUE']._serialized_start=1150 - _globals['_REPEATEDVALUE']._serialized_end=1198 + _globals['_VALUETYPE_ENUM']._serialized_end=736 + _globals['_VALUE']._serialized_start=739 + _globals['_VALUE']._serialized_end=2601 + _globals['_ZONEDTIMESTAMP']._serialized_start=2603 + _globals['_ZONEDTIMESTAMP']._serialized_end=2657 + _globals['_BYTESLIST']._serialized_start=2659 + _globals['_BYTESLIST']._serialized_end=2683 + _globals['_STRINGLIST']._serialized_start=2685 + _globals['_STRINGLIST']._serialized_end=2710 + _globals['_INT32LIST']._serialized_start=2712 + _globals['_INT32LIST']._serialized_end=2736 + _globals['_INT64LIST']._serialized_start=2738 + _globals['_INT64LIST']._serialized_end=2762 + _globals['_DOUBLELIST']._serialized_start=2764 + _globals['_DOUBLELIST']._serialized_end=2789 + _globals['_FLOATLIST']._serialized_start=2791 + _globals['_FLOATLIST']._serialized_end=2815 + _globals['_BOOLLIST']._serialized_start=2817 + _globals['_BOOLLIST']._serialized_end=2840 + _globals['_BYTESSET']._serialized_start=2842 + _globals['_BYTESSET']._serialized_end=2865 + _globals['_STRINGSET']._serialized_start=2867 + _globals['_STRINGSET']._serialized_end=2891 + _globals['_INT32SET']._serialized_start=2893 + _globals['_INT32SET']._serialized_end=2916 + _globals['_INT64SET']._serialized_start=2918 + _globals['_INT64SET']._serialized_end=2941 + _globals['_DOUBLESET']._serialized_start=2943 + _globals['_DOUBLESET']._serialized_end=2967 + _globals['_FLOATSET']._serialized_start=2969 + _globals['_FLOATSET']._serialized_end=2992 + _globals['_BOOLSET']._serialized_start=2994 + _globals['_BOOLSET']._serialized_end=3016 + _globals['_MAP']._serialized_start=3018 + _globals['_MAP']._serialized_end=3127 + _globals['_MAP_VALENTRY']._serialized_start=3065 + _globals['_MAP_VALENTRY']._serialized_end=3127 + _globals['_MAPLIST']._serialized_start=3129 + _globals['_MAPLIST']._serialized_end=3169 + _globals['_REPEATEDVALUE']._serialized_start=3171 + _globals['_REPEATEDVALUE']._serialized_end=3219 + _globals['_MAPKEY']._serialized_start=3222 + _globals['_MAPKEY']._serialized_end=3461 + _globals['_SCALARMAPENTRY']._serialized_start=3463 + _globals['_SCALARMAPENTRY']._serialized_end=3548 + _globals['_SCALARMAP']._serialized_start=3550 + _globals['_SCALARMAP']._serialized_end=3603 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/feast/protos/feast/types/Value_pb2.pyi b/sdk/python/feast/protos/feast/types/Value_pb2.pyi index 15e4870e6a1..17871fb94d4 100644 --- a/sdk/python/feast/protos/feast/types/Value_pb2.pyi +++ b/sdk/python/feast/protos/feast/types/Value_pb2.pyi @@ -72,6 +72,33 @@ class ValueType(google.protobuf.message.Message): BOOL_LIST: ValueType._Enum.ValueType # 17 UNIX_TIMESTAMP_LIST: ValueType._Enum.ValueType # 18 NULL: ValueType._Enum.ValueType # 19 + MAP: ValueType._Enum.ValueType # 20 + MAP_LIST: ValueType._Enum.ValueType # 21 + BYTES_SET: ValueType._Enum.ValueType # 22 + STRING_SET: ValueType._Enum.ValueType # 23 + INT32_SET: ValueType._Enum.ValueType # 24 + INT64_SET: ValueType._Enum.ValueType # 25 + DOUBLE_SET: ValueType._Enum.ValueType # 26 + FLOAT_SET: ValueType._Enum.ValueType # 27 + BOOL_SET: ValueType._Enum.ValueType # 28 + UNIX_TIMESTAMP_SET: ValueType._Enum.ValueType # 29 + JSON: ValueType._Enum.ValueType # 32 + JSON_LIST: ValueType._Enum.ValueType # 33 + STRUCT: ValueType._Enum.ValueType # 34 + STRUCT_LIST: ValueType._Enum.ValueType # 35 + UUID: ValueType._Enum.ValueType # 36 + TIME_UUID: ValueType._Enum.ValueType # 37 + UUID_LIST: ValueType._Enum.ValueType # 38 + TIME_UUID_LIST: ValueType._Enum.ValueType # 39 + UUID_SET: ValueType._Enum.ValueType # 40 + TIME_UUID_SET: ValueType._Enum.ValueType # 41 + VALUE_LIST: ValueType._Enum.ValueType # 42 + VALUE_SET: ValueType._Enum.ValueType # 43 + DECIMAL: ValueType._Enum.ValueType # 44 + DECIMAL_LIST: ValueType._Enum.ValueType # 45 + DECIMAL_SET: ValueType._Enum.ValueType # 46 + SCALAR_MAP: ValueType._Enum.ValueType # 47 + ZONED_TIMESTAMP: ValueType._Enum.ValueType # 48 class Enum(_Enum, metaclass=_EnumEnumTypeWrapper): ... INVALID: ValueType.Enum.ValueType # 0 @@ -92,6 +119,33 @@ class ValueType(google.protobuf.message.Message): BOOL_LIST: ValueType.Enum.ValueType # 17 UNIX_TIMESTAMP_LIST: ValueType.Enum.ValueType # 18 NULL: ValueType.Enum.ValueType # 19 + MAP: ValueType.Enum.ValueType # 20 + MAP_LIST: ValueType.Enum.ValueType # 21 + BYTES_SET: ValueType.Enum.ValueType # 22 + STRING_SET: ValueType.Enum.ValueType # 23 + INT32_SET: ValueType.Enum.ValueType # 24 + INT64_SET: ValueType.Enum.ValueType # 25 + DOUBLE_SET: ValueType.Enum.ValueType # 26 + FLOAT_SET: ValueType.Enum.ValueType # 27 + BOOL_SET: ValueType.Enum.ValueType # 28 + UNIX_TIMESTAMP_SET: ValueType.Enum.ValueType # 29 + JSON: ValueType.Enum.ValueType # 32 + JSON_LIST: ValueType.Enum.ValueType # 33 + STRUCT: ValueType.Enum.ValueType # 34 + STRUCT_LIST: ValueType.Enum.ValueType # 35 + UUID: ValueType.Enum.ValueType # 36 + TIME_UUID: ValueType.Enum.ValueType # 37 + UUID_LIST: ValueType.Enum.ValueType # 38 + TIME_UUID_LIST: ValueType.Enum.ValueType # 39 + UUID_SET: ValueType.Enum.ValueType # 40 + TIME_UUID_SET: ValueType.Enum.ValueType # 41 + VALUE_LIST: ValueType.Enum.ValueType # 42 + VALUE_SET: ValueType.Enum.ValueType # 43 + DECIMAL: ValueType.Enum.ValueType # 44 + DECIMAL_LIST: ValueType.Enum.ValueType # 45 + DECIMAL_SET: ValueType.Enum.ValueType # 46 + SCALAR_MAP: ValueType.Enum.ValueType # 47 + ZONED_TIMESTAMP: ValueType.Enum.ValueType # 48 def __init__( self, @@ -119,6 +173,33 @@ class Value(google.protobuf.message.Message): BOOL_LIST_VAL_FIELD_NUMBER: builtins.int UNIX_TIMESTAMP_LIST_VAL_FIELD_NUMBER: builtins.int NULL_VAL_FIELD_NUMBER: builtins.int + MAP_VAL_FIELD_NUMBER: builtins.int + MAP_LIST_VAL_FIELD_NUMBER: builtins.int + BYTES_SET_VAL_FIELD_NUMBER: builtins.int + STRING_SET_VAL_FIELD_NUMBER: builtins.int + INT32_SET_VAL_FIELD_NUMBER: builtins.int + INT64_SET_VAL_FIELD_NUMBER: builtins.int + DOUBLE_SET_VAL_FIELD_NUMBER: builtins.int + FLOAT_SET_VAL_FIELD_NUMBER: builtins.int + BOOL_SET_VAL_FIELD_NUMBER: builtins.int + UNIX_TIMESTAMP_SET_VAL_FIELD_NUMBER: builtins.int + JSON_VAL_FIELD_NUMBER: builtins.int + JSON_LIST_VAL_FIELD_NUMBER: builtins.int + STRUCT_VAL_FIELD_NUMBER: builtins.int + STRUCT_LIST_VAL_FIELD_NUMBER: builtins.int + UUID_VAL_FIELD_NUMBER: builtins.int + TIME_UUID_VAL_FIELD_NUMBER: builtins.int + UUID_LIST_VAL_FIELD_NUMBER: builtins.int + TIME_UUID_LIST_VAL_FIELD_NUMBER: builtins.int + UUID_SET_VAL_FIELD_NUMBER: builtins.int + TIME_UUID_SET_VAL_FIELD_NUMBER: builtins.int + LIST_VAL_FIELD_NUMBER: builtins.int + SET_VAL_FIELD_NUMBER: builtins.int + DECIMAL_VAL_FIELD_NUMBER: builtins.int + DECIMAL_LIST_VAL_FIELD_NUMBER: builtins.int + DECIMAL_SET_VAL_FIELD_NUMBER: builtins.int + SCALAR_MAP_VAL_FIELD_NUMBER: builtins.int + ZONED_TIMESTAMP_VAL_FIELD_NUMBER: builtins.int bytes_val: builtins.bytes string_val: builtins.str int32_val: builtins.int @@ -144,6 +225,56 @@ class Value(google.protobuf.message.Message): @property def unix_timestamp_list_val(self) -> global___Int64List: ... null_val: global___Null.ValueType + @property + def map_val(self) -> global___Map: ... + @property + def map_list_val(self) -> global___MapList: ... + @property + def bytes_set_val(self) -> global___BytesSet: ... + @property + def string_set_val(self) -> global___StringSet: ... + @property + def int32_set_val(self) -> global___Int32Set: ... + @property + def int64_set_val(self) -> global___Int64Set: ... + @property + def double_set_val(self) -> global___DoubleSet: ... + @property + def float_set_val(self) -> global___FloatSet: ... + @property + def bool_set_val(self) -> global___BoolSet: ... + @property + def unix_timestamp_set_val(self) -> global___Int64Set: ... + json_val: builtins.str + @property + def json_list_val(self) -> global___StringList: ... + @property + def struct_val(self) -> global___Map: ... + @property + def struct_list_val(self) -> global___MapList: ... + uuid_val: builtins.str + time_uuid_val: builtins.str + @property + def uuid_list_val(self) -> global___StringList: ... + @property + def time_uuid_list_val(self) -> global___StringList: ... + @property + def uuid_set_val(self) -> global___StringSet: ... + @property + def time_uuid_set_val(self) -> global___StringSet: ... + @property + def list_val(self) -> global___RepeatedValue: ... + @property + def set_val(self) -> global___RepeatedValue: ... + decimal_val: builtins.str + @property + def decimal_list_val(self) -> global___StringList: ... + @property + def decimal_set_val(self) -> global___StringSet: ... + @property + def scalar_map_val(self) -> global___ScalarMap: ... + @property + def zoned_timestamp_val(self) -> global___ZonedTimestamp: ... def __init__( self, *, @@ -164,13 +295,66 @@ class Value(google.protobuf.message.Message): bool_list_val: global___BoolList | None = ..., unix_timestamp_list_val: global___Int64List | None = ..., null_val: global___Null.ValueType = ..., + map_val: global___Map | None = ..., + map_list_val: global___MapList | None = ..., + bytes_set_val: global___BytesSet | None = ..., + string_set_val: global___StringSet | None = ..., + int32_set_val: global___Int32Set | None = ..., + int64_set_val: global___Int64Set | None = ..., + double_set_val: global___DoubleSet | None = ..., + float_set_val: global___FloatSet | None = ..., + bool_set_val: global___BoolSet | None = ..., + unix_timestamp_set_val: global___Int64Set | None = ..., + json_val: builtins.str = ..., + json_list_val: global___StringList | None = ..., + struct_val: global___Map | None = ..., + struct_list_val: global___MapList | None = ..., + uuid_val: builtins.str = ..., + time_uuid_val: builtins.str = ..., + uuid_list_val: global___StringList | None = ..., + time_uuid_list_val: global___StringList | None = ..., + uuid_set_val: global___StringSet | None = ..., + time_uuid_set_val: global___StringSet | None = ..., + list_val: global___RepeatedValue | None = ..., + set_val: global___RepeatedValue | None = ..., + decimal_val: builtins.str = ..., + decimal_list_val: global___StringList | None = ..., + decimal_set_val: global___StringSet | None = ..., + scalar_map_val: global___ScalarMap | None = ..., + zoned_timestamp_val: global___ZonedTimestamp | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["bool_list_val", b"bool_list_val", "bool_val", b"bool_val", "bytes_list_val", b"bytes_list_val", "bytes_val", b"bytes_val", "double_list_val", b"double_list_val", "double_val", b"double_val", "float_list_val", b"float_list_val", "float_val", b"float_val", "int32_list_val", b"int32_list_val", "int32_val", b"int32_val", "int64_list_val", b"int64_list_val", "int64_val", b"int64_val", "null_val", b"null_val", "string_list_val", b"string_list_val", "string_val", b"string_val", "unix_timestamp_list_val", b"unix_timestamp_list_val", "unix_timestamp_val", b"unix_timestamp_val", "val", b"val"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["bool_list_val", b"bool_list_val", "bool_val", b"bool_val", "bytes_list_val", b"bytes_list_val", "bytes_val", b"bytes_val", "double_list_val", b"double_list_val", "double_val", b"double_val", "float_list_val", b"float_list_val", "float_val", b"float_val", "int32_list_val", b"int32_list_val", "int32_val", b"int32_val", "int64_list_val", b"int64_list_val", "int64_val", b"int64_val", "null_val", b"null_val", "string_list_val", b"string_list_val", "string_val", b"string_val", "unix_timestamp_list_val", b"unix_timestamp_list_val", "unix_timestamp_val", b"unix_timestamp_val", "val", b"val"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions.Literal["val", b"val"]) -> typing_extensions.Literal["bytes_val", "string_val", "int32_val", "int64_val", "double_val", "float_val", "bool_val", "unix_timestamp_val", "bytes_list_val", "string_list_val", "int32_list_val", "int64_list_val", "double_list_val", "float_list_val", "bool_list_val", "unix_timestamp_list_val", "null_val"] | None: ... + def HasField(self, field_name: typing_extensions.Literal["bool_list_val", b"bool_list_val", "bool_set_val", b"bool_set_val", "bool_val", b"bool_val", "bytes_list_val", b"bytes_list_val", "bytes_set_val", b"bytes_set_val", "bytes_val", b"bytes_val", "decimal_list_val", b"decimal_list_val", "decimal_set_val", b"decimal_set_val", "decimal_val", b"decimal_val", "double_list_val", b"double_list_val", "double_set_val", b"double_set_val", "double_val", b"double_val", "float_list_val", b"float_list_val", "float_set_val", b"float_set_val", "float_val", b"float_val", "int32_list_val", b"int32_list_val", "int32_set_val", b"int32_set_val", "int32_val", b"int32_val", "int64_list_val", b"int64_list_val", "int64_set_val", b"int64_set_val", "int64_val", b"int64_val", "json_list_val", b"json_list_val", "json_val", b"json_val", "list_val", b"list_val", "map_list_val", b"map_list_val", "map_val", b"map_val", "null_val", b"null_val", "scalar_map_val", b"scalar_map_val", "set_val", b"set_val", "string_list_val", b"string_list_val", "string_set_val", b"string_set_val", "string_val", b"string_val", "struct_list_val", b"struct_list_val", "struct_val", b"struct_val", "time_uuid_list_val", b"time_uuid_list_val", "time_uuid_set_val", b"time_uuid_set_val", "time_uuid_val", b"time_uuid_val", "unix_timestamp_list_val", b"unix_timestamp_list_val", "unix_timestamp_set_val", b"unix_timestamp_set_val", "unix_timestamp_val", b"unix_timestamp_val", "uuid_list_val", b"uuid_list_val", "uuid_set_val", b"uuid_set_val", "uuid_val", b"uuid_val", "val", b"val", "zoned_timestamp_val", b"zoned_timestamp_val"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["bool_list_val", b"bool_list_val", "bool_set_val", b"bool_set_val", "bool_val", b"bool_val", "bytes_list_val", b"bytes_list_val", "bytes_set_val", b"bytes_set_val", "bytes_val", b"bytes_val", "decimal_list_val", b"decimal_list_val", "decimal_set_val", b"decimal_set_val", "decimal_val", b"decimal_val", "double_list_val", b"double_list_val", "double_set_val", b"double_set_val", "double_val", b"double_val", "float_list_val", b"float_list_val", "float_set_val", b"float_set_val", "float_val", b"float_val", "int32_list_val", b"int32_list_val", "int32_set_val", b"int32_set_val", "int32_val", b"int32_val", "int64_list_val", b"int64_list_val", "int64_set_val", b"int64_set_val", "int64_val", b"int64_val", "json_list_val", b"json_list_val", "json_val", b"json_val", "list_val", b"list_val", "map_list_val", b"map_list_val", "map_val", b"map_val", "null_val", b"null_val", "scalar_map_val", b"scalar_map_val", "set_val", b"set_val", "string_list_val", b"string_list_val", "string_set_val", b"string_set_val", "string_val", b"string_val", "struct_list_val", b"struct_list_val", "struct_val", b"struct_val", "time_uuid_list_val", b"time_uuid_list_val", "time_uuid_set_val", b"time_uuid_set_val", "time_uuid_val", b"time_uuid_val", "unix_timestamp_list_val", b"unix_timestamp_list_val", "unix_timestamp_set_val", b"unix_timestamp_set_val", "unix_timestamp_val", b"unix_timestamp_val", "uuid_list_val", b"uuid_list_val", "uuid_set_val", b"uuid_set_val", "uuid_val", b"uuid_val", "val", b"val", "zoned_timestamp_val", b"zoned_timestamp_val"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["val", b"val"]) -> typing_extensions.Literal["bytes_val", "string_val", "int32_val", "int64_val", "double_val", "float_val", "bool_val", "unix_timestamp_val", "bytes_list_val", "string_list_val", "int32_list_val", "int64_list_val", "double_list_val", "float_list_val", "bool_list_val", "unix_timestamp_list_val", "null_val", "map_val", "map_list_val", "bytes_set_val", "string_set_val", "int32_set_val", "int64_set_val", "double_set_val", "float_set_val", "bool_set_val", "unix_timestamp_set_val", "json_val", "json_list_val", "struct_val", "struct_list_val", "uuid_val", "time_uuid_val", "uuid_list_val", "time_uuid_list_val", "uuid_set_val", "time_uuid_set_val", "list_val", "set_val", "decimal_val", "decimal_list_val", "decimal_set_val", "scalar_map_val", "zoned_timestamp_val"] | None: ... global___Value = Value +class ZonedTimestamp(google.protobuf.message.Message): + """A timezone-aware datetime: the UTC instant plus its originating zone, so a + zoned datetime round-trips losslessly (unlike UNIX_TIMESTAMP, which is decoded + as UTC and discards the original zone). + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + UNIX_TIMESTAMP_FIELD_NUMBER: builtins.int + ZONE_FIELD_NUMBER: builtins.int + unix_timestamp: builtins.int + """Epoch seconds (UTC instant), same convention as unix_timestamp_val.""" + zone: builtins.str + """IANA zone name (e.g. "America/Los_Angeles") or a fixed-offset string + (e.g. "-07:00", "UTC"). Empty string is treated as UTC on read. + """ + def __init__( + self, + *, + unix_timestamp: builtins.int = ..., + zone: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["unix_timestamp", b"unix_timestamp", "zone", b"zone"]) -> None: ... + +global___ZonedTimestamp = ZonedTimestamp + class BytesList(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -276,6 +460,158 @@ class BoolList(google.protobuf.message.Message): global___BoolList = BoolList +class BytesSet(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bytes]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.bytes] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___BytesSet = BytesSet + +class StringSet(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___StringSet = StringSet + +class Int32Set(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.int] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___Int32Set = Int32Set + +class Int64Set(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.int] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___Int64Set = Int64Set + +class DoubleSet(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.float] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___DoubleSet = DoubleSet + +class FloatSet(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.float] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___FloatSet = FloatSet + +class BoolSet(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bool]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[builtins.bool] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___BoolSet = BoolSet + +class Map(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class ValEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___Value: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___Value]: ... + def __init__( + self, + *, + val: collections.abc.Mapping[builtins.str, global___Value] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___Map = Map + +class MapList(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Map]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[global___Map] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___MapList = MapList + class RepeatedValue(google.protobuf.message.Message): """This is to avoid an issue of being unable to specify `repeated value` in oneofs or maps In JSON "val" field can be omitted @@ -294,3 +630,87 @@ class RepeatedValue(google.protobuf.message.Message): def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... global___RepeatedValue = RepeatedValue + +class MapKey(google.protobuf.message.Message): + """Map key for maps with non-string keys. + Excludes string (handled by Map) and all collection types (not valid as keys). + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + INT32_KEY_FIELD_NUMBER: builtins.int + INT64_KEY_FIELD_NUMBER: builtins.int + FLOAT_KEY_FIELD_NUMBER: builtins.int + DOUBLE_KEY_FIELD_NUMBER: builtins.int + BOOL_KEY_FIELD_NUMBER: builtins.int + UNIX_TIMESTAMP_KEY_FIELD_NUMBER: builtins.int + BYTES_KEY_FIELD_NUMBER: builtins.int + UUID_KEY_FIELD_NUMBER: builtins.int + TIME_UUID_KEY_FIELD_NUMBER: builtins.int + DECIMAL_KEY_FIELD_NUMBER: builtins.int + int32_key: builtins.int + int64_key: builtins.int + float_key: builtins.float + double_key: builtins.float + bool_key: builtins.bool + unix_timestamp_key: builtins.int + bytes_key: builtins.bytes + uuid_key: builtins.str + time_uuid_key: builtins.str + decimal_key: builtins.str + def __init__( + self, + *, + int32_key: builtins.int = ..., + int64_key: builtins.int = ..., + float_key: builtins.float = ..., + double_key: builtins.float = ..., + bool_key: builtins.bool = ..., + unix_timestamp_key: builtins.int = ..., + bytes_key: builtins.bytes = ..., + uuid_key: builtins.str = ..., + time_uuid_key: builtins.str = ..., + decimal_key: builtins.str = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["bool_key", b"bool_key", "bytes_key", b"bytes_key", "decimal_key", b"decimal_key", "double_key", b"double_key", "float_key", b"float_key", "int32_key", b"int32_key", "int64_key", b"int64_key", "key", b"key", "time_uuid_key", b"time_uuid_key", "unix_timestamp_key", b"unix_timestamp_key", "uuid_key", b"uuid_key"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["bool_key", b"bool_key", "bytes_key", b"bytes_key", "decimal_key", b"decimal_key", "double_key", b"double_key", "float_key", b"float_key", "int32_key", b"int32_key", "int64_key", b"int64_key", "key", b"key", "time_uuid_key", b"time_uuid_key", "unix_timestamp_key", b"unix_timestamp_key", "uuid_key", b"uuid_key"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["key", b"key"]) -> typing_extensions.Literal["int32_key", "int64_key", "float_key", "double_key", "bool_key", "unix_timestamp_key", "bytes_key", "uuid_key", "time_uuid_key", "decimal_key"] | None: ... + +global___MapKey = MapKey + +class ScalarMapEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + @property + def key(self) -> global___MapKey: ... + @property + def value(self) -> global___Value: ... + def __init__( + self, + *, + key: global___MapKey | None = ..., + value: global___Value | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + +global___ScalarMapEntry = ScalarMapEntry + +class ScalarMap(google.protobuf.message.Message): + """Map with non-string keys. For string-keyed maps use Map.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VAL_FIELD_NUMBER: builtins.int + @property + def val(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ScalarMapEntry]: ... + def __init__( + self, + *, + val: collections.abc.Iterable[global___ScalarMapEntry] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["val", b"val"]) -> None: ... + +global___ScalarMap = ScalarMap diff --git a/sdk/python/feast/registry_server.py b/sdk/python/feast/registry_server.py index f033c5201ec..cd479aac8e8 100644 --- a/sdk/python/feast/registry_server.py +++ b/sdk/python/feast/registry_server.py @@ -1,7 +1,7 @@ import logging from concurrent import futures from datetime import datetime, timezone -from typing import Any, List, Optional, Union, cast +from typing import Any, Callable, List, Optional, cast import grpc from google.protobuf.empty_pb2 import Empty @@ -12,7 +12,7 @@ from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.entity import Entity -from feast.errors import FeastObjectNotFoundException, FeatureViewNotFoundException +from feast.errors import FeastObjectNotFoundException, FeastPermissionError from feast.feast_object import FeastObject from feast.feature_view import FeatureView from feast.grpc_error_interceptor import ErrorInterceptor @@ -24,6 +24,8 @@ from feast.permissions.security_manager import ( assert_permissions, assert_permissions_to_update, + get_security_manager, + is_auth_necessary, permitted_resources, ) from feast.permissions.server.grpc import AuthInterceptor @@ -35,6 +37,7 @@ str_to_auth_manager_type, ) from feast.project import Project +from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto from feast.protos.feast.registry import RegistryServer_pb2, RegistryServer_pb2_grpc from feast.protos.feast.registry.RegistryServer_pb2 import Feature, ListFeaturesResponse from feast.saved_dataset import SavedDataset, ValidationReference @@ -146,6 +149,8 @@ def sort_key(obj): def _build_any_feature_view_proto(feature_view: BaseFeatureView): + from feast.labeling.label_view import LabelView + if isinstance(feature_view, StreamFeatureView): arg_name = "stream_feature_view" feature_view_proto = feature_view.to_proto() @@ -155,6 +160,11 @@ def _build_any_feature_view_proto(feature_view: BaseFeatureView): elif isinstance(feature_view, OnDemandFeatureView): arg_name = "on_demand_feature_view" feature_view_proto = feature_view.to_proto() + elif isinstance(feature_view, LabelView): + arg_name = "label_view" + feature_view_proto = feature_view.to_proto() + else: + raise ValueError(f"Unexpected feature view type: {type(feature_view)}") return RegistryServer_pb2.AnyFeatureView( feature_view=feature_view_proto if arg_name == "feature_view" else None, @@ -164,13 +174,107 @@ def _build_any_feature_view_proto(feature_view: BaseFeatureView): on_demand_feature_view=feature_view_proto if arg_name == "on_demand_feature_view" else None, + label_view=feature_view_proto if arg_name == "label_view" else None, ) class RegistryServer(RegistryServer_pb2_grpc.RegistryServerServicer): - def __init__(self, registry: BaseRegistry) -> None: + def __init__(self, registry: BaseRegistry, store=None) -> None: super().__init__() self.proxied_registry = registry + self.store = store + + def Proto(self, request: Empty, context) -> RegistryProto: + """Build a RegistryProto from individually RBAC-filtered list calls. + + The ``RegistryServer.Proto`` RPC must honor the same permission checks as the + other RPCs rather than returning ``proxied_registry.proto()`` directly, which + would bypass RBAC and expose every object (entities, feature views, data + sources, permissions, projects, etc.) regardless of authorization. + + Each object type is filtered with ``permitted_resources(..., DESCRIBE)``: under + ``NoAuthConfig`` this is a no-op (the full registry is returned, so remote + registries keep working), while with auth enabled the caller only sees the + objects they are permitted to ``DESCRIBE``. + """ + + def describable(resources: list) -> list: + return permitted_resources( + resources=cast(list[FeastObject], resources), + actions=AuthzedAction.DESCRIBE, + ) + + registry_proto = RegistryProto() + + for project in describable(self.proxied_registry.list_projects()): + registry_proto.projects.append(project.to_proto()) + project_name = project.name + + for entity in describable( + self.proxied_registry.list_entities(project=project_name) + ): + registry_proto.entities.append(entity.to_proto()) + + for data_source in describable( + self.proxied_registry.list_data_sources(project=project_name) + ): + registry_proto.data_sources.append(data_source.to_proto()) + + for feature_view in describable( + self.proxied_registry.list_feature_views(project=project_name) + ): + registry_proto.feature_views.append(feature_view.to_proto()) + + for stream_feature_view in describable( + self.proxied_registry.list_stream_feature_views(project=project_name) + ): + registry_proto.stream_feature_views.append( + stream_feature_view.to_proto() + ) + + for on_demand_feature_view in describable( + self.proxied_registry.list_on_demand_feature_views(project=project_name) + ): + registry_proto.on_demand_feature_views.append( + on_demand_feature_view.to_proto() + ) + + for label_view in describable( + self.proxied_registry.list_label_views(project=project_name) + ): + registry_proto.label_views.append(label_view.to_proto()) + + for feature_service in describable( + self.proxied_registry.list_feature_services(project=project_name) + ): + registry_proto.feature_services.append(feature_service.to_proto()) + + for saved_dataset in describable( + self.proxied_registry.list_saved_datasets(project=project_name) + ): + registry_proto.saved_datasets.append(saved_dataset.to_proto()) + + for validation_reference in describable( + self.proxied_registry.list_validation_references(project=project_name) + ): + registry_proto.validation_references.append( + validation_reference.to_proto() + ) + + for permission in describable( + self.proxied_registry.list_permissions(project=project_name) + ): + registry_proto.permissions.append(permission.to_proto()) + + # Carry the registry's real last_updated/version_id rather than stamping "now": + # this proto is rebuilt from individual list calls (for RBAC filtering), but it must + # not look like a fresh commit on every call — clients such as the remote feature + # server key cache freshness off this metadata. Reading these two scalar fields from + # the source proto leaks nothing RBAC-protected (no objects are copied from it). + source_proto = self.proxied_registry.proto() + registry_proto.last_updated.CopyFrom(source_proto.last_updated) + registry_proto.version_id = source_proto.version_id + return registry_proto def ApplyEntity(self, request: RegistryServer_pb2.ApplyEntityRequest, context): entity = cast( @@ -341,23 +445,60 @@ def GetAnyFeatureView( def ApplyFeatureView( self, request: RegistryServer_pb2.ApplyFeatureViewRequest, context ): + from feast.labeling.label_view import LabelView + feature_view_type = request.WhichOneof("base_feature_view") + feature_view_meta: BaseFeatureView + if feature_view_type == "feature_view": - feature_view = FeatureView.from_proto(request.feature_view) + feature_view_meta = FeatureView.from_proto( + request.feature_view, skip_udf=True + ) elif feature_view_type == "on_demand_feature_view": - feature_view = OnDemandFeatureView.from_proto( - request.on_demand_feature_view + feature_view_meta = OnDemandFeatureView.from_proto( + request.on_demand_feature_view, skip_udf=True ) elif feature_view_type == "stream_feature_view": - feature_view = StreamFeatureView.from_proto(request.stream_feature_view) + feature_view_meta = StreamFeatureView.from_proto( + request.stream_feature_view, skip_udf=True + ) + elif feature_view_type == "label_view": + feature_view_meta = LabelView.from_proto(request.label_view) + else: + raise ValueError(f"Unexpected feature view type: {feature_view_type}") + + getter: Callable + if isinstance(feature_view_meta, StreamFeatureView): + getter = self.proxied_registry.get_stream_feature_view + elif isinstance(feature_view_meta, FeatureView): + getter = self.proxied_registry.get_feature_view + elif isinstance(feature_view_meta, OnDemandFeatureView): + getter = self.proxied_registry.get_on_demand_feature_view + elif isinstance(feature_view_meta, LabelView): + getter = self.proxied_registry.get_label_view + else: + getter = self.proxied_registry.get_feature_view assert_permissions_to_update( - resource=feature_view, - # Will replace with the new get_any_feature_view method later - getter=self.proxied_registry.get_feature_view, + resource=cast(FeastObject, feature_view_meta), + getter=cast(Callable, getter), project=request.project, ) + feature_view: BaseFeatureView + if feature_view_type == "feature_view": + feature_view = FeatureView.from_proto(request.feature_view) + elif feature_view_type == "on_demand_feature_view": + feature_view = OnDemandFeatureView.from_proto( + request.on_demand_feature_view + ) + elif feature_view_type == "stream_feature_view": + feature_view = StreamFeatureView.from_proto(request.stream_feature_view) + elif feature_view_type == "label_view": + feature_view = LabelView.from_proto(request.label_view) + else: + raise ValueError(f"Unexpected feature view type: {feature_view_type}") + ( self.proxied_registry.apply_feature_view( feature_view=feature_view, @@ -379,6 +520,7 @@ def ListFeatureViews( project=request.project, allow_cache=request.allow_cache, tags=dict(request.tags), + skip_udf=True, ), ), actions=AuthzedAction.DESCRIBE, @@ -397,13 +539,27 @@ def ListFeatureViews( def ListAllFeatureViews( self, request: RegistryServer_pb2.ListAllFeatureViewsRequest, context ): + from feast.labeling.label_view import LabelView + + updated_since = None + if request.HasField("updated_since"): + updated_since = request.updated_since.ToDatetime().replace( + tzinfo=timezone.utc + ) + all_feature_views = cast( list[FeastObject], - self.proxied_registry.list_all_feature_views( - project=request.project, - allow_cache=request.allow_cache, - tags=dict(request.tags), - ), + [ + fv + for fv in self.proxied_registry.list_all_feature_views( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + skip_udf=True, + updated_since=updated_since, + ) + if not isinstance(fv, LabelView) + ], ) if ( @@ -531,19 +687,12 @@ def ListAllFeatureViews( def DeleteFeatureView( self, request: RegistryServer_pb2.DeleteFeatureViewRequest, context ): - feature_view: Union[StreamFeatureView, FeatureView] - - try: - feature_view = self.proxied_registry.get_stream_feature_view( - name=request.name, project=request.project, allow_cache=False - ) - except FeatureViewNotFoundException: - feature_view = self.proxied_registry.get_feature_view( - name=request.name, project=request.project, allow_cache=False - ) + feature_view = self.proxied_registry.get_any_feature_view( + name=request.name, project=request.project, allow_cache=False + ) assert_permissions( - resource=feature_view, + resource=cast(FeastObject, feature_view), actions=[AuthzedAction.DELETE], ) self.proxied_registry.delete_feature_view( @@ -575,6 +724,7 @@ def ListStreamFeatureViews( project=request.project, allow_cache=request.allow_cache, tags=dict(request.tags), + skip_udf=True, ), ), actions=AuthzedAction.DESCRIBE, @@ -616,6 +766,7 @@ def ListOnDemandFeatureViews( project=request.project, allow_cache=request.allow_cache, tags=dict(request.tags), + skip_udf=True, ), ), actions=AuthzedAction.DESCRIBE, @@ -633,6 +784,40 @@ def ListOnDemandFeatureViews( pagination=pagination_metadata, ) + def GetLabelView(self, request: RegistryServer_pb2.GetLabelViewRequest, context): + return assert_permissions( + resource=self.proxied_registry.get_label_view( + name=request.name, + project=request.project, + allow_cache=request.allow_cache, + ), + actions=[AuthzedAction.DESCRIBE], + ).to_proto() + + def ListLabelViews( + self, request: RegistryServer_pb2.ListLabelViewsRequest, context + ): + paginated_label_views, pagination_metadata = apply_pagination_and_sorting( + permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_label_views( + project=request.project, + allow_cache=request.allow_cache, + tags=dict(request.tags), + ), + ), + actions=AuthzedAction.DESCRIBE, + ), + pagination=request.pagination, + sorting=request.sorting, + ) + + return RegistryServer_pb2.ListLabelViewsResponse( + label_views=[label_view.to_proto() for label_view in paginated_label_views], + pagination=pagination_metadata, + ) + def ApplyFeatureService( self, request: RegistryServer_pb2.ApplyFeatureServiceRequest, context ): @@ -757,6 +942,8 @@ def GetSavedDataset( def ListSavedDatasets( self, request: RegistryServer_pb2.ListSavedDatasetsRequest, context ): + namespace_filter = request.namespace if request.namespace else None + collection_filter = request.collection if request.collection else None paginated_saved_datasets, pagination_metadata = apply_pagination_and_sorting( permitted_resources( resources=cast( @@ -765,6 +952,8 @@ def ListSavedDatasets( project=request.project, allow_cache=request.allow_cache, tags=dict(request.tags), + namespace=namespace_filter, + collection=collection_filter, ), ), actions=AuthzedAction.DESCRIBE, @@ -796,6 +985,280 @@ def DeleteSavedDataset( return Empty() + def _require_store(self): + if not self.store: + raise Exception("FeatureStore is not available for dataset operations.") + + def resolve_feature_service_metadata( + self, feature_service_name: str, project: str + ) -> tuple: + """Resolve features and join keys from a feature service. + + Returns: + (features, join_keys) where features is a list of "fv:feature" refs + and join_keys is a list of entity join key column names. + """ + self._require_store() + fs = self.proxied_registry.get_feature_service( + feature_service_name, project, allow_cache=True + ) + features = [] + join_keys: set = set() + for proj in fs.feature_view_projections: + fv_name = proj.name_to_use() + for f in proj.features: + features.append(f"{fv_name}:{f.name}") + try: + fv = self.proxied_registry.get_feature_view( + proj.name, project, allow_cache=True + ) + for jk in fv.join_keys: + join_keys.add(jk) + except Exception: + pass + return features, list(join_keys) + + def CreateDatasetFromRetrieval( + self, + request: RegistryServer_pb2.CreateDatasetFromRetrievalRequest, + context, + ): + import pandas as pd + + from feast.dataset_job_manager import DatasetJob, get_dataset_job_manager + from feast.dataset_utils import ( + build_entity_df_from_inline, + build_saved_dataset_storage, + coerce_value, + ) + from feast.infra.offline_stores.file_source import SavedDatasetFileStorage + + self._require_store() + + dummy_dataset = SavedDataset( + name=request.name, + features=[], + join_keys=[], + storage=SavedDatasetFileStorage(path=""), + ) + assert_permissions(resource=dummy_dataset, actions=[AuthzedAction.CREATE]) + + store = self.store + job_manager = get_dataset_job_manager() + dataset_name = request.name.strip() + project = request.project.strip() + + entity_source_type = request.entity_source_type + entity_keys = list(request.entity_keys) + entity_values = request.entity_values + start_date = request.start_date or None + end_date = request.end_date or None + extra_columns = request.extra_columns or None + entity_source_path = request.entity_source_path or None + feature_service_name = request.feature_service_name or None + features_list = list(request.features) if request.features else None + storage_type = request.storage_type + storage_path = request.storage_path + tags = dict(request.tags) if request.tags else {} + allow_overwrite = request.allow_overwrite + + def _execute_create(job: DatasetJob): + token = store.set_current_project(project) + try: + if entity_source_type == "inline": + entity_df = build_entity_df_from_inline( + entity_keys=entity_keys, + entity_values=entity_values, + start_date=start_date, + end_date=end_date, + extra_columns=extra_columns, + ) + else: + entity_df = pd.read_parquet(entity_source_path) + for col in ["event_timestamp", "datetime", "timestamp"]: + if col in entity_df.columns: + entity_df[col] = pd.to_datetime(entity_df[col]) + break + if extra_columns: + for col_line in extra_columns.strip().split("\n"): + col_line = col_line.strip() + if "=" in col_line: + col_name, col_value = col_line.split("=", 1) + col_name = col_name.strip() + col_value = col_value.strip() + if col_name: + entity_df[col_name] = coerce_value(col_value) + + features = ( + store.get_feature_service(feature_service_name) + if feature_service_name + else features_list + ) + storage = build_saved_dataset_storage( + storage_type, storage_path.strip() + ) + + store.create_dataset_from_retrieval( + name=dataset_name, + entity_df=entity_df, + features=features, + storage=storage, + tags=tags, + allow_overwrite=allow_overwrite, + ) + finally: + store.reset_current_project(token) + + job = job_manager.submit_job( + name=dataset_name, + project=project, + task_fn=_execute_create, + metadata={ + "storage_type": storage_type, + "storage_path": storage_path, + }, + ) + + return RegistryServer_pb2.CreateDatasetFromRetrievalResponse( + job_id=job.job_id, + status=job.status.value, + ) + + def GetDatasetData( + self, + request: RegistryServer_pb2.GetDatasetDataRequest, + context, + ): + import json + + import pandas as pd + + self._require_store() + + project = request.project or self.store.project + token = self.store.set_current_project(project) + try: + dataset = self.store.registry.get_saved_dataset( + request.name, self.store.project + ) + assert_permissions(resource=dataset, actions=[AuthzedAction.READ_OFFLINE]) + + limit = request.limit if request.limit > 0 else 10 + df = self.store.retrieve_dataset_data(request.name, limit=limit) + finally: + self.store.reset_current_project(token) + + if df.empty: + return RegistryServer_pb2.GetDatasetDataResponse( + columns=[], rows=[], total_rows=0, sample_size=0 + ) + + for col in df.columns: + if pd.api.types.is_datetime64_any_dtype(df[col]): + df[col] = df[col].astype(str) + + columns = list(df.columns) + rows = [] + for _, row in df.iterrows(): + rows.append( + RegistryServer_pb2.TabularRow( + values=[json.dumps(v, default=str) for v in row.tolist()] + ) + ) + + return RegistryServer_pb2.GetDatasetDataResponse( + columns=columns, + rows=rows, + total_rows=len(df), + sample_size=len(df), + ) + + def GetDatasetJobStatus( + self, + request: RegistryServer_pb2.GetDatasetJobStatusRequest, + context, + ): + from feast.dataset_job_manager import get_dataset_job_manager + from feast.infra.offline_stores.file_source import SavedDatasetFileStorage + + job_manager = get_dataset_job_manager() + job = job_manager.get_job(request.job_id) + if not job: + raise FeastObjectNotFoundException( + f"Dataset job '{request.job_id}' not found" + ) + + dummy_dataset = SavedDataset( + name=job.name, + features=[], + join_keys=[], + storage=SavedDatasetFileStorage(path=""), + ) + assert_permissions(resource=dummy_dataset, actions=[AuthzedAction.DESCRIBE]) + + return RegistryServer_pb2.GetDatasetJobStatusResponse( + job_id=job.job_id, + dataset_name=job.name, + project=job.project, + status=job.status.value, + created_at=job.created_at or "", + completed_at=job.completed_at or "", + error=job.error or "", + ) + + def ListDatasetJobs( + self, + request: RegistryServer_pb2.ListDatasetJobsRequest, + context, + ): + from feast.dataset_job_manager import JobStatus as JS + from feast.dataset_job_manager import get_dataset_job_manager + from feast.infra.offline_stores.file_source import SavedDatasetFileStorage + + job_manager = get_dataset_job_manager() + status_filter = None + if request.status_filter: + try: + status_filter = JS(request.status_filter) + except ValueError: + pass + + jobs = job_manager.list_jobs( + project=request.project or None, status=status_filter + ) + + permitted_jobs = [] + for j in jobs: + dummy_dataset = SavedDataset( + name=j.name, + features=[], + join_keys=[], + storage=SavedDatasetFileStorage(path=""), + ) + try: + assert_permissions( + resource=dummy_dataset, actions=[AuthzedAction.DESCRIBE] + ) + permitted_jobs.append(j) + except Exception: + continue + + job_responses = [] + for j in permitted_jobs: + job_responses.append( + RegistryServer_pb2.GetDatasetJobStatusResponse( + job_id=j.job_id, + dataset_name=j.name, + project=j.project, + status=j.status.value, + created_at=j.created_at or "", + completed_at=j.completed_at or "", + error=j.error or "", + ) + ) + + return RegistryServer_pb2.ListDatasetJobsResponse(jobs=job_responses) + def ApplyValidationReference( self, request: RegistryServer_pb2.ApplyValidationReferenceRequest, context ): @@ -874,6 +1337,13 @@ def DeleteValidationReference( def ListProjectMetadata( self, request: RegistryServer_pb2.ListProjectMetadataRequest, context ): + try: + project = self.proxied_registry.get_project( + name=request.project, allow_cache=True + ) + assert_permissions(resource=project, actions=[AuthzedAction.DESCRIBE]) + except FeastObjectNotFoundException: + pass return RegistryServer_pb2.ListProjectMetadataResponse( project_metadata=[ project_metadata.to_proto() @@ -906,6 +1376,12 @@ def ApplyMaterialization( return Empty() def UpdateInfra(self, request: RegistryServer_pb2.UpdateInfraRequest, context): + # Create-or-update so first remote apply can write infra for a new project. + assert_permissions_to_update( + resource=Project(name=request.project), + getter=self.proxied_registry.get_project, + project=request.project, + ) self.proxied_registry.update_infra( infra=Infra.from_proto(request.infra), project=request.project, @@ -914,6 +1390,19 @@ def UpdateInfra(self, request: RegistryServer_pb2.UpdateInfraRequest, context): return Empty() def GetInfra(self, request: RegistryServer_pb2.GetInfraRequest, context): + # plan() calls get_infra before the project is created on a shared remote + # registry. Mirror ListProjectMetadata: authorize when present, and for a + # missing project require CREATE (or allow when auth is off). + try: + project = self.proxied_registry.get_project( + name=request.project, allow_cache=True + ) + assert_permissions(resource=project, actions=[AuthzedAction.DESCRIBE]) + except FeastObjectNotFoundException: + assert_permissions( + resource=Project(name=request.project), + actions=[AuthzedAction.CREATE], + ) return self.proxied_registry.get_infra( project=request.project, allow_cache=request.allow_cache ).to_proto() @@ -1011,17 +1500,26 @@ def GetProject(self, request: RegistryServer_pb2.GetProjectRequest, context): ).to_proto() def ListProjects(self, request: RegistryServer_pb2.ListProjectsRequest, context): - paginated_projects, pagination_metadata = apply_pagination_and_sorting( - permitted_resources( - resources=cast( - list[FeastObject], - self.proxied_registry.list_projects( - allow_cache=request.allow_cache, - tags=dict(request.tags), - ), + from feast.constants import PROTECTED_PROJECT_TAG + + permitted_projects = permitted_resources( + resources=cast( + list[FeastObject], + self.proxied_registry.list_projects( + allow_cache=request.allow_cache, + tags=dict(request.tags), ), - actions=AuthzedAction.DESCRIBE, ), + actions=AuthzedAction.DESCRIBE, + ) + + # Exclude protected projects after RBAC check + visible_projects = [ + p for p in permitted_projects if p.tags.get(PROTECTED_PROJECT_TAG) != "true" + ] + + paginated_projects, pagination_metadata = apply_pagination_and_sorting( + visible_projects, pagination=request.pagination, sorting=request.sorting, ) @@ -1046,6 +1544,13 @@ def DeleteProject(self, request: RegistryServer_pb2.DeleteProjectRequest, contex def GetRegistryLineage( self, request: RegistryServer_pb2.GetRegistryLineageRequest, context ): + try: + project = self.proxied_registry.get_project( + name=request.project, allow_cache=True + ) + assert_permissions(resource=project, actions=[AuthzedAction.DESCRIBE]) + except FeastObjectNotFoundException: + pass direct_relationships, indirect_relationships = ( self.proxied_registry.get_registry_lineage( project=request.project, @@ -1084,6 +1589,13 @@ def GetObjectRelationships( self, request: RegistryServer_pb2.GetObjectRelationshipsRequest, context ): """Get relationships for a specific object.""" + try: + project = self.proxied_registry.get_project( + name=request.project, allow_cache=True + ) + assert_permissions(resource=project, actions=[AuthzedAction.DESCRIBE]) + except FeastObjectNotFoundException: + pass relationships = self.proxied_registry.get_object_relationships( project=request.project, object_type=request.object_type, @@ -1104,38 +1616,70 @@ def GetObjectRelationships( ) def Commit(self, request, context): + # Per-object mutations are authorized in Apply*/Delete* RPCs. + # Requiring UPDATE on every project breaks shared-registry multi-tenant + # commits (remote feastRef with different feastProjects). Require CREATE + # or UPDATE on at least one project when auth is enabled instead. + projects = cast( + list[FeastObject], + list(self.proxied_registry.list_projects(allow_cache=True)), + ) + if projects and is_auth_necessary(get_security_manager()): + can_update = permitted_resources( + resources=projects, actions=AuthzedAction.UPDATE + ) + can_create = permitted_resources( + resources=projects, actions=AuthzedAction.CREATE + ) + if not can_update and not can_create: + raise FeastPermissionError( + "Not authorized to commit registry changes: " + "CREATE or UPDATE permission required on at least one project" + ) self.proxied_registry.commit() return Empty() def Refresh(self, request, context): + # Use create-or-update authorization so first apply of a new project over + # a remote registry can refresh before the project exists yet. + assert_permissions_to_update( + resource=Project(name=request.project), + getter=self.proxied_registry.get_project, + project=request.project, + ) self.proxied_registry.refresh(request.project) return Empty() - def Proto(self, request, context): - return self.proxied_registry.proto() - def ListFeatures(self, request: RegistryServer_pb2.ListFeaturesRequest, context): """ List all features in the registry, optionally filtered by project, feature_view, or name. """ + from feast.labeling.label_view import LabelView + allow_cache = request.allow_cache if hasattr(request, "allow_cache") else True feature_views = self.proxied_registry.list_all_feature_views( project=request.project, allow_cache=allow_cache, + skip_udf=True, ) permitted_fvs = permitted_resources( resources=cast(list[FeastObject], feature_views), actions=AuthzedAction.DESCRIBE, ) + requested_kind = ( + request.kind if hasattr(request, "kind") and request.kind else "" + ) features = [] for fv in permitted_fvs: fv_name = getattr(fv, "name", None) + kind = "label" if isinstance(fv, LabelView) else "feature" + if requested_kind and kind != requested_kind: + continue for feature in getattr(fv, "features", []): if request.feature_view and fv_name != request.feature_view: continue if request.name and feature.name != request.name: continue - # Get owner and timestamps from feature view owner = "" created_timestamp = None last_updated_timestamp = None @@ -1161,6 +1705,7 @@ def ListFeatures(self, request: RegistryServer_pb2.ListFeaturesRequest, context) created_timestamp=created_timestamp, last_updated_timestamp=last_updated_timestamp, tags=getattr(feature, "tags", {}), + kind=kind, ) ) paginated_features, pagination_metadata = apply_pagination_and_sorting( @@ -1176,6 +1721,8 @@ def GetFeature(self, request: RegistryServer_pb2.GetFeatureRequest, context): """ Get a single feature by project, feature_view, and name. """ + from feast.labeling.label_view import LabelView + allow_cache = getattr(request, "allow_cache", True) feature_views = self.proxied_registry.list_all_feature_views( project=request.project, @@ -1189,7 +1736,6 @@ def GetFeature(self, request: RegistryServer_pb2.GetFeatureRequest, context): fv_name = getattr(fv, "name", None) for feature in getattr(fv, "features", []): if fv_name == request.feature_view and feature.name == request.name: - # Get owner and timestamps from feature view owner = "" created_timestamp = None last_updated_timestamp = None @@ -1214,12 +1760,64 @@ def GetFeature(self, request: RegistryServer_pb2.GetFeatureRequest, context): created_timestamp=created_timestamp, last_updated_timestamp=last_updated_timestamp, tags=getattr(feature, "tags", {}), + kind="label" if isinstance(fv, LabelView) else "feature", ) raise FeastObjectNotFoundException( f"Feature {request.name} not found in feature view {request.feature_view} in project {request.project}" ) +def _sync_protected_project_tag(store: FeatureStore): + """Sync the protected project tag based on FEAST_PROTECTED_PROJECT env var. + + When FEAST_PROTECTED_PROJECT=true, tags the project as protected in the + shared registry. When the env var is absent or false, removes the tag + if it was previously set — allowing temporary protection that can be + reversed by removing the annotation from the FeatureStore CR. + """ + import os + + from feast.constants import FEAST_PROTECTED_PROJECT_ENV, PROTECTED_PROJECT_TAG + + should_protect = os.environ.get(FEAST_PROTECTED_PROJECT_ENV, "").lower() == "true" + + try: + existing = store.registry.get_project(name=store.project, allow_cache=False) + except Exception: + if should_protect: + from feast.project import Project + + project = Project( + name=store.project, + tags={PROTECTED_PROJECT_TAG: "true"}, + ) + store.registry.apply_project(project, commit=True) + logger.info( + "Tagged project '%s' as protected (%s=true)", + store.project, + PROTECTED_PROJECT_TAG, + ) + return + + is_protected = existing.tags.get(PROTECTED_PROJECT_TAG) == "true" + + if should_protect and not is_protected: + existing.tags[PROTECTED_PROJECT_TAG] = "true" + store.registry.apply_project(existing, commit=True) + logger.info( + "Tagged project '%s' as protected (%s=true)", + store.project, + PROTECTED_PROJECT_TAG, + ) + elif not should_protect and is_protected: + del existing.tags[PROTECTED_PROJECT_TAG] + store.registry.apply_project(existing, commit=True) + logger.info( + "Removed protected tag from project '%s'", + store.project, + ) + + def start_server( store: FeatureStore, port: int, @@ -1227,6 +1825,8 @@ def start_server( tls_key_path: str = "", tls_cert_path: str = "", ): + _sync_protected_project_tag(store) + auth_manager_type = str_to_auth_manager_type(store.config.auth_config.type) init_security_manager(auth_type=auth_manager_type, fs=store) init_auth_manager( @@ -1240,7 +1840,7 @@ def start_server( interceptors=_grpc_interceptors(auth_manager_type), ) RegistryServer_pb2_grpc.add_RegistryServerServicer_to_server( - RegistryServer(store.registry), server + RegistryServer(store.registry, store=store), server ) # Add health check service to server health_servicer = health.HealthServicer() diff --git a/sdk/python/feast/repo_config.py b/sdk/python/feast/repo_config.py index 895002948f1..775a62aab57 100644 --- a/sdk/python/feast/repo_config.py +++ b/sdk/python/feast/repo_config.py @@ -50,6 +50,8 @@ "k8s": "feast.infra.compute_engines.kubernetes.k8s_engine.KubernetesComputeEngine", "spark.engine": "feast.infra.compute_engines.spark.compute.SparkComputeEngine", "ray.engine": "feast.infra.compute_engines.ray.compute.RayComputeEngine", + "flink.engine": "feast.infra.compute_engines.flink.compute.FlinkComputeEngine", + "spark_application": "feast.infra.compute_engines.spark_application.compute.SparkApplicationComputeEngine", } LEGACY_ONLINE_STORE_CLASS_FOR_TYPE = { @@ -58,7 +60,6 @@ "feast.infra.online_stores.contrib.cassandra_online_store.cassandra_online_store.CassandraOnlineStore": "feast.infra.online_stores.cassandra_online_store.cassandra_online_store.CassandraOnlineStore", "feast.infra.online_stores.contrib.mysql_online_store.mysql.MySQLOnlineStore": "feast.infra.online_stores.mysql_online_store.mysql.MySQLOnlineStore", "feast.infra.online_stores.contrib.hazelcast_online_store.hazelcast_online_store.HazelcastOnlineStore": "feast.infra.online_stores.hazelcast_online_store.hazelcast_online_store.HazelcastOnlineStore", - "feast.infra.online_stores.contrib.ikv_online_store.ikv.IKVOnlineStore": "feast.infra.online_stores.ikv_online_store.ikv.IKVOnlineStore", "feast.infra.online_stores.contrib.elasticsearch.ElasticSearchOnlineStore": "feast.infra.online_stores.elasticsearch_online_store.elasticsearch.ElasticSearchOnlineStore", "feast.infra.online_stores.contrib.singlestore_online_store.singlestore.SingleStoreOnlineStore": "feast.infra.online_stores.singlestore_online_store.singlestore.SingleStoreOnlineStore", "feast.infra.online_stores.contrib.qdrant.QdrantOnlineStore": "feast.infra.online_stores.qdrant_online_store.qdrant.QdrantOnlineStore", @@ -66,6 +67,7 @@ } ONLINE_STORE_CLASS_FOR_TYPE = { + "aerospike": "feast.infra.online_stores.aerospike_online_store.AerospikeOnlineStore", "sqlite": "feast.infra.online_stores.sqlite.SqliteOnlineStore", "datastore": "feast.infra.online_stores.datastore.DatastoreOnlineStore", "redis": "feast.infra.online_stores.redis.RedisOnlineStore", @@ -75,15 +77,17 @@ "postgres": "feast.infra.online_stores.postgres_online_store.postgres.PostgreSQLOnlineStore", "hbase": "feast.infra.online_stores.hbase_online_store.hbase.HbaseOnlineStore", "cassandra": "feast.infra.online_stores.cassandra_online_store.cassandra_online_store.CassandraOnlineStore", + "scylladb": "feast.infra.online_stores.scylladb_online_store.scylladb.ScyllaDBOnlineStore", "mysql": "feast.infra.online_stores.mysql_online_store.mysql.MySQLOnlineStore", "hazelcast": "feast.infra.online_stores.hazelcast_online_store.hazelcast_online_store.HazelcastOnlineStore", - "ikv": "feast.infra.online_stores.ikv_online_store.ikv.IKVOnlineStore", "elasticsearch": "feast.infra.online_stores.elasticsearch_online_store.elasticsearch.ElasticSearchOnlineStore", "remote": "feast.infra.online_stores.remote.RemoteOnlineStore", "singlestore": "feast.infra.online_stores.singlestore_online_store.singlestore.SingleStoreOnlineStore", "qdrant": "feast.infra.online_stores.qdrant_online_store.qdrant.QdrantOnlineStore", "couchbase.online": "feast.infra.online_stores.couchbase_online_store.couchbase.CouchbaseOnlineStore", "milvus": "feast.infra.online_stores.milvus_online_store.milvus.MilvusOnlineStore", + "mongodb": "feast.infra.online_stores.mongodb_online_store.MongoDBOnlineStore", + "hybrid": "feast.infra.online_stores.hybrid_online_store.hybrid_online_store.HybridOnlineStore", **LEGACY_ONLINE_STORE_CLASS_FOR_TYPE, } @@ -103,6 +107,7 @@ "couchbase.offline": "feast.infra.offline_stores.contrib.couchbase_offline_store.couchbase.CouchbaseColumnarOfflineStore", "clickhouse": "feast.infra.offline_stores.contrib.clickhouse_offline_store.clickhouse.ClickhouseOfflineStore", "ray": "feast.infra.offline_stores.contrib.ray_offline_store.ray.RayOfflineStore", + "oracle": "feast.infra.offline_stores.contrib.oracle_offline_store.oracle.OracleOfflineStore", } FEATURE_SERVER_CONFIG_CLASS_FOR_TYPE = { @@ -119,6 +124,24 @@ "oidc_client": "feast.permissions.auth_model.OidcClientAuthConfig", } +_OIDC_CLIENT_KEYS = frozenset( + {"client_secret", "token", "token_env_var", "username", "password"} +) + + +def _is_oidc_client_config(auth_dict: dict) -> bool: + """Decide whether an OIDC auth dict should be routed to OidcClientAuthConfig. + + True when the dict carries any client-credential key, or when it is a bare + ``{"type": "oidc"}`` dict with no server-side keys (auth_discovery_url / + client_id), which signals token-passthrough via FEAST_OIDC_TOKEN. + """ + if auth_dict.get("type") != AuthType.OIDC.value: + return False + has_client_keys = bool(_OIDC_CLIENT_KEYS & auth_dict.keys()) + has_server_keys = "auth_discovery_url" in auth_dict + return has_client_keys or not has_server_keys + class FeastBaseModel(BaseModel): """Feast Pydantic Configuration Class""" @@ -132,6 +155,13 @@ class FeastConfigBaseModel(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") +class McpRegistryConfig(FeastBaseModel): + """MCP (Model Context Protocol) configuration for the registry REST server.""" + + enabled: StrictBool = False + """ bool: Enable MCP support on the REST registry server. """ + + class RegistryConfig(FeastBaseModel): """Metadata Store Configuration. Configuration that relates to reading from and writing to the Feast registry.""" @@ -166,22 +196,194 @@ class RegistryConfig(FeastBaseModel): Once this is set to True, it cannot be reverted back to False. Reverting back to False will only reset the project but not all the projects""" + enable_online_feature_view_versioning: StrictBool = False + """ bool: Enable versioned online store tables and version-qualified reads + (e.g., 'fv@v2:feature'). When True, each schema version gets its own + online store table and can be queried independently. Version history + tracking in the registry is always active regardless of this setting. """ + + mcp: Optional[McpRegistryConfig] = None + """ McpRegistryConfig: MCP (Model Context Protocol) configuration for the registry REST server. """ + + @staticmethod + def _normalize_postgres_scheme(value: str, field_name: str) -> str: + """Rewrite a bare ``postgresql://`` URL to the psycopg3 driver, with a warning. + + SQLAlchemy resolves a bare ``postgresql://`` to the psycopg2 driver, while + feast standardizes on psycopg3 (``postgresql+psycopg``). Pass an explicit + ``postgresql+psycopg2`` to keep psycopg2. Shared by the ``path`` and + ``read_path`` validators so both endpoints normalize identically. + """ + if value.startswith("postgresql://"): + _logger.warning( + f"The `{field_name}` of the `RegistryConfig` starts with a plain " + "`postgresql` string. We are updating this to `postgresql+psycopg` " + "to ensure that the `psycopg3` driver is used by `sqlalchemy`. If " + f"you want to use `psycopg2` pass `postgresql+psycopg2` explicitly " + f"to `{field_name}`. To silence this warning, pass `postgresql+psycopg` " + f"explicitly to `{field_name}`." + ) + # Rewrite only the leading scheme, not any later occurrence (e.g. + # inside credentials or a query string). + return "postgresql+psycopg://" + value[len("postgresql://") :] + return value + @field_validator("path") def validate_path(cls, path: str, values: ValidationInfo) -> str: if values.data.get("registry_type") == "sql": - if path.startswith("postgresql://"): - _logger.warning( - "The `path` of the `RegistryConfig` starts with a plain " - "`postgresql` string. We are updating this to `postgresql+psycopg` " - "to ensure that the `psycopg3` driver is used by `sqlalchemy`. If " - "you want to use `psycopg2` pass `postgresql+psycopg2` explicitely " - "to `path`. To silence this warning, pass `postgresql+psycopg` " - "explicitely to `path`." - ) - return path.replace("postgresql://", "postgresql+psycopg://") + return cls._normalize_postgres_scheme(path, "path") return path +class MaterializationConfig(BaseModel): + """Configuration options for feature materialization behavior.""" + + pull_latest_features: StrictBool = False + """ bool: If true, feature retrieval jobs will only pull the latest feature values for each entity. + If false, feature retrieval jobs will pull all feature values within the specified time range. """ + + online_write_batch_size: Optional[int] = Field(default=None, gt=0) + """ int: Number of rows to write to online store per batch during materialization. + If None (default), all rows are written in a single batch for backward compatibility. + Set to a positive integer (e.g., 10000) to enable batched writes. + Supported compute engines: local, spark, ray. """ + + +class DataQualityMonitoringConfig(FeastConfigBaseModel): + """Data Quality Monitoring configuration.""" + + auto_baseline: StrictBool = True + """Whether baseline distribution is computed automatically on ``feast apply``.""" + + +class OpenLineageConsumerConfig(FeastBaseModel): + """Configuration for the OpenLineage consumer (event receiver).""" + + enabled: StrictBool = False + """ bool: Whether the consumer is enabled. """ + + store_type: StrictStr = "sql" + """ str: Storage backend type. Currently only 'sql' is supported. """ + + connection_string: Optional[StrictStr] = None + """ str: Optional separate database connection string. """ + + api_key: Optional[StrictStr] = None + """ str: API key for authenticating producers sending events. """ + + namespace_mapping: Optional[Dict[str, str]] = None + """ dict: Map of OL namespace -> Feast project for RBAC scoping. """ + + +class OpenLineageConfig(FeastBaseModel): + """Configuration for OpenLineage integration. + + This enables automatic data lineage tracking for Feast operations like + materialization, feature retrieval, and registry changes. + + Example configuration in feature_store.yaml: + openlineage: + enabled: true + transport_type: http + transport_url: http://localhost:5000 + transport_endpoint: api/v1/lineage + namespace: feast + """ + + enabled: StrictBool = False + """ bool: Whether OpenLineage integration is enabled. Defaults to False. """ + + transport_type: Optional[StrictStr] = None + """ str: Type of transport (http, console, file, kafka). Defaults to None (uses OpenLineage SDK defaults). """ + + transport_url: Optional[StrictStr] = None + """ str: URL for HTTP transport. Required when transport_type is 'http'. """ + + transport_endpoint: StrictStr = "api/v1/lineage" + """ str: API endpoint for HTTP transport. Defaults to 'api/v1/lineage'. """ + + api_key: Optional[StrictStr] = None + """ str: Optional API key for authentication with the lineage server. """ + + namespace: StrictStr = "feast" + """ str: Default namespace for Feast jobs and datasets. """ + + producer: StrictStr = "feast" + """ str: Producer identifier for OpenLineage events. """ + + emit_on_apply: StrictBool = True + """ bool: Emit lineage events when 'feast apply' is called. """ + + emit_on_materialize: StrictBool = True + """ bool: Emit lineage events during materialization. """ + + consumer: Optional[OpenLineageConsumerConfig] = None + """ OpenLineageConsumerConfig: Consumer (event receiver) configuration. """ + + def to_openlineage_config(self): + """Convert to feast.openlineage.OpenLineageConfig.""" + from feast.openlineage.config import OpenLineageConfig as OLConfig + from feast.openlineage.config import ( + OpenLineageConsumerConfig as OLConsumerConfig, + ) + + consumer = None + if self.consumer: + consumer = OLConsumerConfig( + enabled=self.consumer.enabled, + store_type=self.consumer.store_type, + connection_string=self.consumer.connection_string, + api_key=self.consumer.api_key, + namespace_mapping=self.consumer.namespace_mapping or {}, + ) + + return OLConfig( + enabled=self.enabled, + transport_type=self.transport_type, + transport_url=self.transport_url, + transport_endpoint=self.transport_endpoint, + api_key=self.api_key, + namespace=self.namespace, + producer=self.producer, + emit_on_apply=self.emit_on_apply, + emit_on_materialize=self.emit_on_materialize, + consumer=consumer or OLConsumerConfig(), + ) + + +class EmbeddingModelConfig(FeastConfigBaseModel): + """Configuration for the query-time embedding model used by the feature server. + + Required when using ``openai_search`` or the + ``/v1/vector_stores/{vector_store_id}/search`` endpoint. + + **Sentence Transformers** (default) — runs locally, no API key required. + Ideal for air-gapped or cost-sensitive deployments. Requires the + ``sentence-transformers`` package (``pip install sentence-transformers``). + + Example in ``feature_store.yaml``:: + + embedding_model: + provider: sentence_transformers # default; can be omitted + model: all-MiniLM-L6-v2 + + Custom providers can be plugged in by implementing the + :class:`~feast.embedder.EmbeddingProvider` protocol and passing an + instance to :class:`~feast.feature_store.FeatureStore`. + """ + + provider: str = "sentence_transformers" + """Embedding backend to use. Supported values: + ``'sentence_transformers'`` (default).""" + + model: str + """Model identifier. + + Any HuggingFace model name compatible with ``SentenceTransformer``, + e.g. ``'all-MiniLM-L6-v2'``, ``'BAAI/bge-small-en-v1.5'``. + """ + + class RepoConfig(FeastBaseModel): """Repo config. Typically loaded from `feature_store.yaml`""" @@ -221,6 +423,13 @@ class RepoConfig(FeastBaseModel): feature_server: Optional[Any] = None """ FeatureServerConfig: Feature server configuration (optional depending on provider) """ + embedding_model: Optional[EmbeddingModelConfig] = Field( + None, alias="embedding_model" + ) + """ EmbeddingModelConfig: Embedding model configuration. + Required when using openai_search or the + OpenAI-compatible vector store search endpoint. """ + flags: Any = None """ Flags (deprecated field): Feature flags for experimental features """ @@ -239,6 +448,22 @@ class RepoConfig(FeastBaseModel): coerce_tz_aware: Optional[bool] = True """ If True, coerces entity_df timestamp columns to be timezone aware (to UTC by default). """ + materialization_config: MaterializationConfig = Field( + MaterializationConfig(), alias="materialization" + ) + """ MaterializationConfig: Configuration options for feature materialization behavior. """ + + openlineage_config: Optional[OpenLineageConfig] = Field(None, alias="openlineage") + """ Configuration for OpenLineage data lineage integration (optional). """ + + mlflow_config: Optional[Any] = Field(None, alias="mlflow") + """ MlflowConfig: Configuration for MLflow experiment tracking integration (optional). """ + + data_quality_monitoring_config: Optional[DataQualityMonitoringConfig] = Field( + None, alias="data_quality_monitoring" + ) + """ DataQualityMonitoringConfig: Data Quality Monitoring configuration (optional). """ + def __init__(self, **data: Any): super().__init__(**data) @@ -274,6 +499,16 @@ def __init__(self, **data: Any): self.feature_server["type"] )(**self.feature_server) + # Initialize OpenLineage configuration + self._openlineage: Optional[OpenLineageConfig] = None + if "openlineage" in data: + self.openlineage_config = data["openlineage"] + + # Initialize MLflow configuration + self._mlflow = None + if "mlflow" in data: + self.mlflow_config = data["mlflow"] + if self.entity_key_serialization_version < 3: warnings.warn( "The serialization version below 3 are deprecated. " @@ -293,10 +528,11 @@ def registry(self): # This may be a custom registry store, which does not need a 'registry_type' self._registry = RegistryConfig(**self.registry_config) elif isinstance(self.registry_config, str): - # User passed in just a path to file registry - self._registry = get_registry_config_from_type("file")( - path=self.registry_config - ) + # Let Registry.__init__ auto-detect the correct store class + # from the URI scheme (e.g. gs:// -> GCSRegistryStore). + # Previously this hardcoded "file" type, which broke gs:// and + # s3:// paths because FileRegistryStore uses pathlib.Path. + self._registry = RegistryConfig(path=self.registry_config) elif self.registry_config: self._registry = self.registry_config return self._registry @@ -320,26 +556,12 @@ def offline_store(self): def auth_config(self): if not self._auth: if isinstance(self.auth, Dict): - # treat this auth block as *client-side* OIDC when it matches - # 1) ROPG – username + password + client_secret - # 2) client-credentials – client_secret only - # 3) static token – token - is_oidc_client = self.auth.get("type") == AuthType.OIDC.value and ( - ( - "username" in self.auth - and "password" in self.auth - and "client_secret" in self.auth - ) # 1 - or ( - "client_secret" in self.auth - and "username" not in self.auth - and "password" not in self.auth - ) # 2 - or ("token" in self.auth) # 3 + config_type = ( + "oidc_client" + if _is_oidc_client_config(self.auth) + else self.auth.get("type") ) - self._auth = get_auth_config_from_type( - "oidc_client" if is_oidc_client else self.auth.get("type") - )(**self.auth) + self._auth = get_auth_config_from_type(config_type)(**self.auth) elif isinstance(self.auth, str): self._auth = get_auth_config_from_type(self.auth)() elif self.auth: @@ -377,6 +599,28 @@ def batch_engine(self): return self._batch_engine + @property + def openlineage(self) -> Optional[OpenLineageConfig]: + """Get the OpenLineage configuration.""" + if not self._openlineage: + if isinstance(self.openlineage_config, Dict): + self._openlineage = OpenLineageConfig(**self.openlineage_config) + elif self.openlineage_config: + self._openlineage = self.openlineage_config + return self._openlineage + + @property + def mlflow(self): + """Get the MLflow configuration.""" + if not self._mlflow: + if isinstance(self.mlflow_config, Dict): + from feast.mlflow_integration.config import MlflowConfig + + self._mlflow = MlflowConfig(**self.mlflow_config) + elif self.mlflow_config: + self._mlflow = self.mlflow_config + return self._mlflow + @model_validator(mode="before") def _validate_auth_config(cls, values: Any) -> Any: from feast.permissions.auth_model import AuthConfig @@ -434,6 +678,7 @@ def _validate_online_store_config(cls, values: Any) -> Any: online_config_class(**values["online_store"]) except ValidationError as e: raise e + return values @model_validator(mode="before") @@ -489,14 +734,28 @@ def _validate_feature_server_config(cls, values: Any) -> Any: @field_validator("project") @classmethod - def _validate_project_name(cls, v: str) -> str: + def _validate_project_name(cls, v: str, info: ValidationInfo) -> str: + # Deferred import to avoid circular dependency during package initialization. from feast.repo_operations import is_valid_name + sqlite_compatible = False + + online_store = info.data.get("online_store") if info else None + if online_store is None or online_store == {}: + sqlite_compatible = True + elif isinstance(online_store, dict): + sqlite_compatible = online_store.get("type", "sqlite") == "sqlite" + if not is_valid_name(v): raise ValueError( f"Project name, {v}, should only have " f"alphanumerical values, underscores, and hyphens but not start with an underscore or hyphen." ) + + if sqlite_compatible and "-" in v: + raise ValueError( + "Project names for SQLite online stores cannot contain hyphens because they are used in table names." + ) return v @field_validator("flags") diff --git a/sdk/python/feast/repo_contents.py b/sdk/python/feast/repo_contents.py index d65f6ac7bb9..6dac9769317 100644 --- a/sdk/python/feast/repo_contents.py +++ b/sdk/python/feast/repo_contents.py @@ -17,6 +17,7 @@ from feast.entity import Entity from feast.feature_service import FeatureService from feast.feature_view import FeatureView +from feast.labeling.label_view import LabelView from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.permission import Permission from feast.project import Project @@ -34,6 +35,7 @@ class RepoContents(NamedTuple): feature_views: List[FeatureView] on_demand_feature_views: List[OnDemandFeatureView] stream_feature_views: List[StreamFeatureView] + label_views: List[LabelView] entities: List[Entity] feature_services: List[FeatureService] permissions: List[Permission] @@ -49,6 +51,7 @@ def to_registry_proto(self) -> RegistryProto: registry_proto.on_demand_feature_views.extend( [fv.to_proto() for fv in self.on_demand_feature_views] ) + registry_proto.label_views.extend([lv.to_proto() for lv in self.label_views]) registry_proto.feature_services.extend( [fs.to_proto() for fs in self.feature_services] ) diff --git a/sdk/python/feast/repo_operations.py b/sdk/python/feast/repo_operations.py index 8eae581a260..02747ac4b54 100644 --- a/sdk/python/feast/repo_operations.py +++ b/sdk/python/feast/repo_operations.py @@ -1,526 +1,657 @@ -import base64 -import importlib -import json -import logging -import os -import random -import re -import sys -import tempfile -from importlib.abc import Loader -from importlib.machinery import ModuleSpec -from pathlib import Path -from typing import List, Optional, Set, Union - -import click -from click.exceptions import BadParameter - -from feast import PushSource -from feast.batch_feature_view import BatchFeatureView -from feast.constants import FEATURE_STORE_YAML_ENV_NAME -from feast.data_source import DataSource, KafkaSource, KinesisSource -from feast.diff.registry_diff import extract_objects_for_keep_delete_update_add -from feast.entity import Entity -from feast.feature_service import FeatureService -from feast.feature_store import FeatureStore -from feast.feature_view import DUMMY_ENTITY, FeatureView -from feast.file_utils import replace_str_in_file -from feast.infra.registry.base_registry import BaseRegistry -from feast.infra.registry.registry import FEAST_OBJECT_TYPES, FeastObjectType, Registry -from feast.names import adjectives, animals -from feast.on_demand_feature_view import OnDemandFeatureView -from feast.permissions.permission import Permission -from feast.project import Project -from feast.repo_config import RepoConfig -from feast.repo_contents import RepoContents -from feast.stream_feature_view import StreamFeatureView - -logger = logging.getLogger(__name__) - - -def py_path_to_module(path: Path) -> str: - return ( - str(path.relative_to(os.getcwd()))[: -len(".py")] - .replace("./", "") - .replace("/", ".") - .replace("\\", ".") - ) - - -def read_feastignore(repo_root: Path) -> List[str]: - """Read .feastignore in the repo root directory (if exists) and return the list of user-defined ignore paths""" - feast_ignore = repo_root / ".feastignore" - if not feast_ignore.is_file(): - return [] - lines = feast_ignore.read_text().strip().split("\n") - ignore_paths = [] - for line in lines: - # Remove everything after the first occurance of "#" symbol (comments) - if line.find("#") >= 0: - line = line[: line.find("#")] - # Strip leading or ending whitespaces - line = line.strip() - # Add this processed line to ignore_paths if it's not empty - if len(line) > 0: - ignore_paths.append(line) - return ignore_paths - - -def get_ignore_files(repo_root: Path, ignore_paths: List[str]) -> Set[Path]: - """Get all ignore files that match any of the user-defined ignore paths""" - ignore_files = set() - for ignore_path in set(ignore_paths): - # ignore_path may contains matchers (* or **). Use glob() to match user-defined path to actual paths - for matched_path in repo_root.glob(ignore_path): - if matched_path.is_file(): - # If the matched path is a file, add that to ignore_files set - ignore_files.add(matched_path.resolve()) - else: - # Otherwise, list all Python files in that directory and add all of them to ignore_files set - ignore_files |= { - sub_path.resolve() - for sub_path in matched_path.glob("**/*.py") - if sub_path.is_file() - } - return ignore_files - - -def get_repo_files(repo_root: Path) -> List[Path]: - """Get the list of all repo files, ignoring undesired files & directories specified in .feastignore""" - # Read ignore paths from .feastignore and create a set of all files that match any of these paths - ignore_paths = read_feastignore(repo_root) + [ - ".git", - ".feastignore", - ".venv", - ".pytest_cache", - "__pycache__", - ".ipynb_checkpoints", - ] - ignore_files = get_ignore_files(repo_root, ignore_paths) - - # List all Python files in the root directory (recursively) - repo_files = { - p.resolve() - for p in repo_root.glob("**/*.py") - if p.is_file() and "__init__.py" != p.name - } - # Ignore all files that match any of the ignore paths in .feastignore - repo_files -= ignore_files - - # Sort repo_files to read them in the same order every time - return sorted(repo_files) - - -def parse_repo(repo_root: Path) -> RepoContents: - """ - Collects unique Feast object definitions from the given feature repo. - - Specifically, if an object foo has already been added, bar will still be added if - (bar == foo), but not if (bar is foo). This ensures that import statements will - not result in duplicates, but defining two equal objects will. - """ - res = RepoContents( - projects=[], - data_sources=[], - entities=[], - feature_views=[], - feature_services=[], - on_demand_feature_views=[], - stream_feature_views=[], - permissions=[], - ) - - for repo_file in get_repo_files(repo_root): - module_path = py_path_to_module(repo_file) - module = importlib.import_module(module_path) - - for attr_name in dir(module): - obj = getattr(module, attr_name) - - if isinstance(obj, DataSource) and not any( - (obj is ds) for ds in res.data_sources - ): - res.data_sources.append(obj) - - # Handle batch sources defined within stream sources. - if ( - isinstance(obj, PushSource) - or isinstance(obj, KafkaSource) - or isinstance(obj, KinesisSource) - ): - batch_source = obj.batch_source - - if batch_source and not any( - (batch_source is ds) for ds in res.data_sources - ): - res.data_sources.append(batch_source) - if ( - isinstance(obj, FeatureView) - and not any((obj is fv) for fv in res.feature_views) - and not isinstance(obj, StreamFeatureView) - and not isinstance(obj, BatchFeatureView) - ): - res.feature_views.append(obj) - - # Handle batch sources defined with feature views. - batch_source = obj.batch_source - assert batch_source - if not any((batch_source is ds) for ds in res.data_sources): - res.data_sources.append(batch_source) - - # Handle stream sources defined with feature views. - if obj.stream_source: - stream_source = obj.stream_source - if not any((stream_source is ds) for ds in res.data_sources): - res.data_sources.append(stream_source) - elif isinstance(obj, StreamFeatureView) and not any( - (obj is sfv) for sfv in res.stream_feature_views - ): - res.stream_feature_views.append(obj) - - # Handle batch sources defined with feature views. - batch_source = obj.batch_source - if not any((batch_source is ds) for ds in res.data_sources): - res.data_sources.append(batch_source) - - # Handle stream sources defined with feature views. - assert obj.stream_source - stream_source = obj.stream_source - if not any((stream_source is ds) for ds in res.data_sources): - res.data_sources.append(stream_source) - elif isinstance(obj, BatchFeatureView) and not any( - (obj is bfv) for bfv in res.feature_views - ): - res.feature_views.append(obj) - - # Handle batch sources defined with feature views. - batch_source = obj.batch_source - if not any((batch_source is ds) for ds in res.data_sources): - res.data_sources.append(batch_source) - elif isinstance(obj, Entity) and not any( - (obj is entity) for entity in res.entities - ): - res.entities.append(obj) - elif isinstance(obj, FeatureService) and not any( - (obj is fs) for fs in res.feature_services - ): - res.feature_services.append(obj) - elif isinstance(obj, OnDemandFeatureView) and not any( - (obj is odfv) for odfv in res.on_demand_feature_views - ): - res.on_demand_feature_views.append(obj) - elif isinstance(obj, Permission) and not any( - (obj is p) for p in res.permissions - ): - res.permissions.append(obj) - elif isinstance(obj, Project) and not any((obj is p) for p in res.projects): - res.projects.append(obj) - - res.entities.append(DUMMY_ENTITY) - return res - - -def plan(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool): - os.chdir(repo_path) - repo = _get_repo_contents(repo_path, repo_config.project, repo_config) - for project in repo.projects: - repo_config.project = project.name - store, registry = _get_store_and_registry(repo_config) - # TODO: When we support multiple projects in a single repo, we should filter repo contents by project - if not skip_source_validation: - provider = store._get_provider() - data_sources = [t.batch_source for t in repo.feature_views] - # Make sure the data source used by this feature view is supported by Feast - for data_source in data_sources: - provider.validate_data_source(store.config, data_source) - - registry_diff, infra_diff, _ = store.plan(repo) - click.echo(registry_diff.to_string()) - click.echo(infra_diff.to_string()) - - -def _get_repo_contents( - repo_path, - project_name: Optional[str] = None, - repo_config: Optional[RepoConfig] = None, -): - sys.dont_write_bytecode = True - repo = parse_repo(repo_path) - - if len(repo.projects) < 1: - if project_name: - print( - f"No project found in the repository. Using project name {project_name} defined in feature_store.yaml" - ) - project_description = ( - repo_config.project_description if repo_config else None - ) - repo.projects.append( - Project(name=project_name, description=project_description or "") - ) - else: - print( - "No project found in the repository. Either define Project in repository or define a project in feature_store.yaml" - ) - sys.exit(1) - elif len(repo.projects) == 1: - if repo.projects[0].name != project_name: - print( - "Project object name should match with the project name defined in feature_store.yaml" - ) - sys.exit(1) - else: - print( - "Multiple projects found in the repository. Currently no support for multiple projects" - ) - sys.exit(1) - - return repo - - -def _get_store_and_registry(repo_config): - store = FeatureStore(config=repo_config) - registry = store.registry - return store, registry - - -def extract_objects_for_apply_delete(project, registry, repo): - # TODO(achals): This code path should be refactored to handle added & kept entities separately. - ( - _, - objs_to_delete, - objs_to_update, - objs_to_add, - ) = extract_objects_for_keep_delete_update_add(registry, project, repo) - - all_to_apply: List[ - Union[ - Entity, - FeatureView, - OnDemandFeatureView, - StreamFeatureView, - FeatureService, - ] - ] = [] - for object_type in FEAST_OBJECT_TYPES: - to_apply = set(objs_to_add[object_type]).union(objs_to_update[object_type]) - all_to_apply.extend(to_apply) - - all_to_delete: List[ - Union[ - Entity, - FeatureView, - OnDemandFeatureView, - StreamFeatureView, - FeatureService, - ] - ] = [] - for object_type in FEAST_OBJECT_TYPES: - all_to_delete.extend(objs_to_delete[object_type]) - - return ( - all_to_apply, - all_to_delete, - set(objs_to_add[FeastObjectType.FEATURE_VIEW]).union( - set(objs_to_update[FeastObjectType.FEATURE_VIEW]) - ), - objs_to_delete[FeastObjectType.FEATURE_VIEW], - ) - - -def apply_total_with_repo_instance( - store: FeatureStore, - project_name: str, - registry: BaseRegistry, - repo: RepoContents, - skip_source_validation: bool, -): - if not skip_source_validation: - provider = store._get_provider() - data_sources = [t.batch_source for t in repo.feature_views] - # Make sure the data source used by this feature view is supported by Feast - for data_source in data_sources: - provider.validate_data_source(store.config, data_source) - - # For each object in the registry, determine whether it should be kept or deleted. - ( - all_to_apply, - all_to_delete, - views_to_keep, - views_to_delete, - ) = extract_objects_for_apply_delete(project_name, registry, repo) - - if store._should_use_plan(): - registry_diff, infra_diff, new_infra = store.plan(repo) - click.echo(registry_diff.to_string()) - - store._apply_diffs(registry_diff, infra_diff, new_infra) - click.echo(infra_diff.to_string()) - else: - store.apply(all_to_apply, objects_to_delete=all_to_delete, partial=False) - log_infra_changes(views_to_keep, views_to_delete) - - -def log_infra_changes( - views_to_keep: Set[FeatureView], views_to_delete: Set[FeatureView] -): - from colorama import Fore, Style - - for view in views_to_keep: - click.echo( - f"Deploying infrastructure for {Style.BRIGHT + Fore.GREEN}{view.name}{Style.RESET_ALL}" - ) - for view in views_to_delete: - click.echo( - f"Removing infrastructure for {Style.BRIGHT + Fore.RED}{view.name}{Style.RESET_ALL}" - ) - - -def create_feature_store( - ctx: click.Context, -) -> FeatureStore: - repo = ctx.obj["CHDIR"] - # If we received a base64 encoded version of feature_store.yaml, use that - config_base64 = os.getenv(FEATURE_STORE_YAML_ENV_NAME) - if config_base64: - print("Received base64 encoded feature_store.yaml") - config_bytes = base64.b64decode(config_base64) - # Create a new unique directory for writing feature_store.yaml - repo_path = Path(tempfile.mkdtemp()) - with open(repo_path / "feature_store.yaml", "wb") as f: - f.write(config_bytes) - return FeatureStore(repo_path=str(repo_path.resolve())) - else: - fs_yaml_file = ctx.obj["FS_YAML_FILE"] - cli_check_repo(repo, fs_yaml_file) - return FeatureStore(repo_path=str(repo), fs_yaml_file=fs_yaml_file) - - -def apply_total(repo_config: RepoConfig, repo_path: Path, skip_source_validation: bool): - os.chdir(repo_path) - repo = _get_repo_contents(repo_path, repo_config.project, repo_config) - for project in repo.projects: - repo_config.project = project.name - store, registry = _get_store_and_registry(repo_config) - if not is_valid_name(project.name): - print( - f"{project.name} is not valid. Project name should only have " - f"alphanumerical values, underscores, and hyphens but not start with an underscore or hyphen." - ) - sys.exit(1) - # TODO: When we support multiple projects in a single repo, we should filter repo contents by project. Currently there is no way to associate Feast objects to project. - print(f"Applying changes for project {project.name}") - apply_total_with_repo_instance( - store, project.name, registry, repo, skip_source_validation - ) - - -def teardown(repo_config: RepoConfig, repo_path: Optional[str]): - # Cannot pass in both repo_path and repo_config to FeatureStore. - feature_store = FeatureStore(repo_path=repo_path, config=repo_config) - feature_store.teardown() - - -def registry_dump(repo_config: RepoConfig, repo_path: Path) -> str: - """For debugging only: output contents of the metadata registry""" - registry_config = repo_config.registry - project = repo_config.project - registry = Registry( - project, - registry_config=registry_config, - repo_path=repo_path, - auth_config=repo_config.auth_config, - ) - registry_dict = registry.to_dict(project=project) - return json.dumps(registry_dict, indent=2, sort_keys=True) - - -def cli_check_repo(repo_path: Path, fs_yaml_file: Path): - sys.path.append(str(repo_path)) - if not fs_yaml_file.exists(): - print( - f"Can't find feature repo configuration file at {fs_yaml_file}. " - "Make sure you're running feast from an initialized feast repository." - ) - sys.exit(1) - - -def init_repo(repo_name: str, template: str): - import os - from pathlib import Path - from shutil import copytree - - from colorama import Fore, Style - - if not is_valid_name(repo_name): - raise BadParameter( - message="Name should be alphanumeric values, underscores, and hyphens but not start with an underscore or hyphen", - param_hint="PROJECT_DIRECTORY", - ) - repo_path = Path(os.path.join(Path.cwd(), repo_name)) - repo_path.mkdir(exist_ok=True) - repo_config_path = repo_path / "feature_store.yaml" - - if repo_config_path.exists(): - new_directory = os.path.relpath(repo_path, os.getcwd()) - - print( - f"The directory {Style.BRIGHT + Fore.GREEN}{new_directory}{Style.RESET_ALL} contains an existing feature " - f"store repository that may cause a conflict" - ) - print() - sys.exit(1) - - # Copy template directory - template_path = str(Path(Path(__file__).parent / "templates" / template).absolute()) - if not os.path.exists(template_path): - raise IOError(f"Could not find template {template}") - copytree(template_path, str(repo_path), dirs_exist_ok=True) - - # Rename gitignore files back to .gitignore - for gitignore_path in repo_path.rglob("gitignore"): - gitignore_path.rename(gitignore_path.with_name(".gitignore")) - - # Seed the repository - bootstrap_path = repo_path / "bootstrap.py" - if os.path.exists(bootstrap_path): - import importlib.util - - spec = importlib.util.spec_from_file_location("bootstrap", str(bootstrap_path)) - assert isinstance(spec, ModuleSpec) - bootstrap = importlib.util.module_from_spec(spec) - assert isinstance(spec.loader, Loader) - spec.loader.exec_module(bootstrap) - bootstrap.bootstrap() # type: ignore - os.remove(bootstrap_path) - - # Template the feature_store.yaml file - feature_store_yaml_path = repo_path / "feature_repo" / "feature_store.yaml" - replace_str_in_file( - feature_store_yaml_path, "project: my_project", f"project: {repo_name}" - ) - - # Remove the __pycache__ folder if it exists - import shutil - - shutil.rmtree(repo_path / "__pycache__", ignore_errors=True) - - import click - - click.echo() - click.echo( - f"Creating a new Feast repository in {Style.BRIGHT + Fore.GREEN}{repo_path}{Style.RESET_ALL}." - ) - click.echo() - - -def is_valid_name(name: str) -> bool: - """A name should be alphanumeric values, underscores, and hyphens but not start with an underscore""" - return ( - not name.startswith(("_", "-")) and re.compile(r"[^\w-]+").search(name) is None - ) - - -def generate_project_name() -> str: - """Generates a unique project name""" - return f"{random.choice(adjectives)}_{random.choice(animals)}" +import base64 +import importlib +import json +import logging +import os +import random +import re +import sys +import tempfile +from importlib.abc import Loader +from importlib.machinery import ModuleSpec +from pathlib import Path +from typing import List, Optional, Set, Union + +import click +from click.exceptions import BadParameter + +from feast import PushSource +from feast.batch_feature_view import BatchFeatureView +from feast.constants import FEATURE_STORE_YAML_ENV_NAME +from feast.data_source import DataSource, KafkaSource, KinesisSource +from feast.diff.registry_diff import extract_objects_for_keep_delete_update_add +from feast.entity import Entity +from feast.feature_service import FeatureService +from feast.feature_store import FeatureStore +from feast.feature_view import DUMMY_ENTITY, FeatureView +from feast.file_utils import replace_str_in_file +from feast.infra.registry.base_registry import BaseRegistry +from feast.infra.registry.registry import FEAST_OBJECT_TYPES, FeastObjectType, Registry +from feast.labeling.label_view import LabelView +from feast.names import adjectives, animals +from feast.on_demand_feature_view import OnDemandFeatureView +from feast.permissions.permission import Permission +from feast.project import Project +from feast.repo_config import RepoConfig +from feast.repo_contents import RepoContents +from feast.stream_feature_view import StreamFeatureView + +logger = logging.getLogger(__name__) + + +def py_path_to_module(path: Path) -> str: + return ( + str(path.relative_to(os.getcwd()))[: -len(".py")] + .replace("./", "") + .replace("/", ".") + .replace("\\", ".") + ) + + +def read_feastignore(repo_root: Path) -> List[str]: + """Read .feastignore in the repo root directory (if exists) and return the list of user-defined ignore paths""" + feast_ignore = repo_root / ".feastignore" + if not feast_ignore.is_file(): + return [] + lines = feast_ignore.read_text().strip().split("\n") + ignore_paths = [] + for line in lines: + # Remove everything after the first occurance of "#" symbol (comments) + if line.find("#") >= 0: + line = line[: line.find("#")] + # Strip leading or ending whitespaces + line = line.strip() + # Add this processed line to ignore_paths if it's not empty + if len(line) > 0: + ignore_paths.append(line) + return ignore_paths + + +def get_ignore_files(repo_root: Path, ignore_paths: List[str]) -> Set[Path]: + """Get all ignore files that match any of the user-defined ignore paths""" + ignore_files = set() + for ignore_path in set(ignore_paths): + # ignore_path may contains matchers (* or **). Use glob() to match user-defined path to actual paths + for matched_path in repo_root.glob(ignore_path): + if matched_path.is_file(): + # If the matched path is a file, add that to ignore_files set + ignore_files.add(matched_path.resolve()) + else: + # Otherwise, list all Python files in that directory and add all of them to ignore_files set + ignore_files |= { + sub_path.resolve() + for sub_path in matched_path.glob("**/*.py") + if sub_path.is_file() + } + return ignore_files + + +def get_repo_files(repo_root: Path) -> List[Path]: + """Get the list of all repo files, ignoring undesired files & directories specified in .feastignore""" + # Read ignore paths from .feastignore and create a set of all files that match any of these paths + ignore_paths = read_feastignore(repo_root) + [ + ".git", + ".feastignore", + ".venv", + "**/.ipynb_checkpoints", + "**/.pytest_cache", + "**/__pycache__", + ] + ignore_files = get_ignore_files(repo_root, ignore_paths) + + # List all Python files in the root directory (recursively) + repo_files = { + p.resolve() + for p in repo_root.glob("**/*.py") + if p.is_file() and "__init__.py" != p.name + } + # Ignore all files that match any of the ignore paths in .feastignore + repo_files -= ignore_files + + # Sort repo_files to read them in the same order every time + return sorted(repo_files) + + +def parse_repo(repo_root: Path) -> RepoContents: + """ + Collects unique Feast object definitions from the given feature repo. + + Specifically, if an object foo has already been added, bar will still be added if + (bar == foo), but not if (bar is foo). This ensures that import statements will + not result in duplicates, but defining two equal objects will. + """ + res = RepoContents( + projects=[], + data_sources=[], + entities=[], + feature_views=[], + feature_services=[], + on_demand_feature_views=[], + stream_feature_views=[], + label_views=[], + permissions=[], + ) + + for repo_file in get_repo_files(repo_root): + module_path = py_path_to_module(repo_file) + module = importlib.import_module(module_path) + + for attr_name in dir(module): + obj = getattr(module, attr_name) + + if isinstance(obj, DataSource) and not any( + (obj is ds) for ds in res.data_sources + ): + res.data_sources.append(obj) + + # Handle batch sources defined within stream sources. + if ( + isinstance(obj, PushSource) + or isinstance(obj, KafkaSource) + or isinstance(obj, KinesisSource) + ): + batch_source = obj.batch_source + + if batch_source and not any( + (batch_source is ds) for ds in res.data_sources + ): + res.data_sources.append(batch_source) + if ( + isinstance(obj, FeatureView) + and not any((obj is fv) for fv in res.feature_views) + and not isinstance(obj, StreamFeatureView) + and not isinstance(obj, BatchFeatureView) + ): + res.feature_views.append(obj) + + # Handle batch sources defined with feature views. + batch_source = obj.batch_source + if batch_source is not None and not any( + (batch_source is ds) for ds in res.data_sources + ): + res.data_sources.append(batch_source) + + # Handle stream sources defined with feature views. + if obj.stream_source: + stream_source = obj.stream_source + if not any((stream_source is ds) for ds in res.data_sources): + res.data_sources.append(stream_source) + elif isinstance(obj, StreamFeatureView) and not any( + (obj is sfv) for sfv in res.stream_feature_views + ): + res.stream_feature_views.append(obj) + + # Handle batch sources defined with feature views. + batch_source = obj.batch_source + if batch_source is not None and not any( + (batch_source is ds) for ds in res.data_sources + ): + res.data_sources.append(batch_source) + assert obj.stream_source + stream_source = obj.stream_source + if not any((stream_source is ds) for ds in res.data_sources): + res.data_sources.append(stream_source) + elif isinstance(obj, BatchFeatureView) and not any( + (obj is bfv) for bfv in res.feature_views + ): + res.feature_views.append(obj) + + # Handle batch sources defined with feature views. + batch_source = obj.batch_source + if batch_source is not None and not any( + (batch_source is ds) for ds in res.data_sources + ): + res.data_sources.append(batch_source) + elif isinstance(obj, Entity) and not any( + (obj is entity) for entity in res.entities + ): + res.entities.append(obj) + elif isinstance(obj, FeatureService) and not any( + (obj is fs) for fs in res.feature_services + ): + res.feature_services.append(obj) + elif isinstance(obj, OnDemandFeatureView) and not any( + (obj is odfv) for odfv in res.on_demand_feature_views + ): + res.on_demand_feature_views.append(obj) + elif isinstance(obj, LabelView) and not any( + (obj is lv) for lv in res.label_views + ): + res.label_views.append(obj) + if obj.source is not None and not any( + (obj.source is ds) for ds in res.data_sources + ): + res.data_sources.append(obj.source) + if ( + isinstance(obj.source, PushSource) + and obj.source.batch_source + and not any( + (obj.source.batch_source is ds) for ds in res.data_sources + ) + ): + res.data_sources.append(obj.source.batch_source) + elif isinstance(obj, Permission) and not any( + (obj is p) for p in res.permissions + ): + res.permissions.append(obj) + elif isinstance(obj, Project) and not any((obj is p) for p in res.projects): + res.projects.append(obj) + + res.entities.append(DUMMY_ENTITY) + return res + + +def plan( + repo_config: RepoConfig, + repo_path: Path, + skip_source_validation: bool, + skip_feature_view_validation: bool = False, +): + os.chdir(repo_path) + repo = _get_repo_contents(repo_path, repo_config.project, repo_config) + for project in repo.projects: + repo_config.project = project.name + store, registry = _get_store_and_registry(repo_config) + # TODO: When we support multiple projects in a single repo, we should filter repo contents by project + if not skip_source_validation: + provider = store._get_provider() + data_sources = [ + t.batch_source for t in repo.feature_views if t.batch_source is not None + ] + # Make sure the data source used by this feature view is supported by Feast + for data_source in data_sources: + provider.validate_data_source(store.config, data_source) + + registry_diff, infra_diff, _ = store.plan( + repo, skip_feature_view_validation=skip_feature_view_validation + ) + click.echo(registry_diff.to_string()) + click.echo(infra_diff.to_string()) + + +def _get_repo_contents( + repo_path, + project_name: Optional[str] = None, + repo_config: Optional[RepoConfig] = None, +): + sys.dont_write_bytecode = True + repo = parse_repo(repo_path) + + if len(repo.projects) < 1: + if project_name: + print( + f"No project found in the repository. Using project name {project_name} defined in feature_store.yaml" + ) + project_description = ( + repo_config.project_description if repo_config else None + ) + repo.projects.append( + Project(name=project_name, description=project_description or "") + ) + else: + print( + "No project found in the repository. Either define Project in repository or define a project in feature_store.yaml" + ) + sys.exit(1) + elif len(repo.projects) == 1: + if repo.projects[0].name != project_name: + print( + "Project object name should match with the project name defined in feature_store.yaml" + ) + sys.exit(1) + else: + print( + "Multiple projects found in the repository. Currently no support for multiple projects" + ) + sys.exit(1) + + return repo + + +def _get_store_and_registry(repo_config): + store = FeatureStore(config=repo_config) + registry = store.registry + return store, registry + + +def extract_objects_for_apply_delete(project, registry, repo): + # TODO(achals): This code path should be refactored to handle added & kept entities separately. + ( + _, + objs_to_delete, + objs_to_update, + objs_to_add, + ) = extract_objects_for_keep_delete_update_add(registry, project, repo) + + all_to_apply: List[ + Union[ + Entity, + FeatureView, + OnDemandFeatureView, + StreamFeatureView, + FeatureService, + ] + ] = [] + for object_type in FEAST_OBJECT_TYPES: + to_apply = set(objs_to_add[object_type]).union(objs_to_update[object_type]) + all_to_apply.extend(to_apply) + + all_to_delete: List[ + Union[ + Entity, + FeatureView, + OnDemandFeatureView, + StreamFeatureView, + FeatureService, + ] + ] = [] + for object_type in FEAST_OBJECT_TYPES: + all_to_delete.extend(objs_to_delete[object_type]) + + return ( + all_to_apply, + all_to_delete, + set(objs_to_add[FeastObjectType.FEATURE_VIEW]).union( + set(objs_to_update[FeastObjectType.FEATURE_VIEW]) + ), + objs_to_delete[FeastObjectType.FEATURE_VIEW], + ) + + +def apply_total_with_repo_instance( + store: FeatureStore, + project_name: str, + registry: BaseRegistry, + repo: RepoContents, + skip_source_validation: bool, + skip_feature_view_validation: bool = False, + no_promote: bool = False, +): + if not skip_source_validation: + provider = store._get_provider() + data_sources = [ + t.batch_source for t in repo.feature_views if t.batch_source is not None + ] + # Make sure the data source used by this feature view is supported by Feast + for data_source in data_sources: + provider.validate_data_source(store.config, data_source) + + # For each object in the registry, determine whether it should be kept or deleted. + ( + all_to_apply, + all_to_delete, + views_to_keep, + views_to_delete, + ) = extract_objects_for_apply_delete(project_name, registry, repo) + + try: + if store._should_use_plan(): + # Planning phase - compute diffs first without progress bars + registry_diff, infra_diff, new_infra = store.plan( + repo, + skip_feature_view_validation=skip_feature_view_validation, + ) + click.echo(registry_diff.to_string()) + + # Only show progress bars if there are actual infrastructure changes + progress_ctx = None + if len(infra_diff.infra_object_diffs) > 0: + from feast.diff.apply_progress import ApplyProgressContext + + progress_ctx = ApplyProgressContext() + progress_ctx.start_overall_progress() + + # Apply phase + store._apply_diffs( + registry_diff, + infra_diff, + new_infra, + progress_ctx=progress_ctx, + no_promote=no_promote, + ) + click.echo(infra_diff.to_string()) + else: + # Legacy apply path - no progress bars for legacy path + store.apply( + all_to_apply, + objects_to_delete=all_to_delete, + partial=False, + skip_feature_view_validation=skip_feature_view_validation, + no_promote=no_promote, + ) + log_infra_changes(views_to_keep, views_to_delete) + finally: + # Cleanup is handled in the new _apply_diffs method + pass + + # Submit baseline jobs if needed + dqm = store.config.data_quality_monitoring_config + if dqm is not None and dqm.auto_baseline: + _submit_baseline_jobs(store, project_name, repo) + + +def _submit_baseline_jobs(store, project_name, repo): + try: + from feast.monitoring.monitoring_service import MonitoringService + + svc = MonitoringService(store) + feature_views = list(repo.feature_views) + if not feature_views: + return + + job_ids = svc.submit_baseline_for_new_features( + project=project_name, feature_views=feature_views + ) + if not job_ids: + return + + import threading + + def _run_baseline_jobs(): + for job_id in job_ids: + try: + svc.execute_job(job_id) + click.echo(f" ✓ Baseline computed (job: {job_id})") + except Exception: + logging.getLogger(__name__).debug( + "Baseline job %s execution failed (non-critical)", + job_id, + exc_info=True, + ) + + click.echo( + f" → Computing baseline metrics in background ({len(job_ids)} job(s))..." + ) + thread = threading.Thread(target=_run_baseline_jobs) + thread.start() + except Exception: + logging.getLogger(__name__).debug( + "Monitoring baseline submission skipped (non-critical)", exc_info=True + ) + + +def log_infra_changes( + views_to_keep: Set[FeatureView], views_to_delete: Set[FeatureView] +): + from colorama import Fore, Style + + for view in views_to_keep: + click.echo( + f"Deploying infrastructure for {Style.BRIGHT + Fore.GREEN}{view.name}{Style.RESET_ALL}" + ) + for view in views_to_delete: + click.echo( + f"Removing infrastructure for {Style.BRIGHT + Fore.RED}{view.name}{Style.RESET_ALL}" + ) + + +def create_feature_store( + ctx: click.Context, +) -> FeatureStore: + repo = ctx.obj["CHDIR"] + # If we received a base64 encoded version of feature_store.yaml, use that + config_base64 = os.getenv(FEATURE_STORE_YAML_ENV_NAME) + if config_base64: + print("Received base64 encoded feature_store.yaml") + config_bytes = base64.b64decode(config_base64) + # Create a new unique directory for writing feature_store.yaml + repo_path = Path(tempfile.mkdtemp()) + with open(repo_path / "feature_store.yaml", "wb") as f: + f.write(config_bytes) + return FeatureStore(repo_path=str(repo_path.resolve())) + else: + fs_yaml_file = ctx.obj["FS_YAML_FILE"] + cli_check_repo(repo, fs_yaml_file) + return FeatureStore(repo_path=str(repo), fs_yaml_file=fs_yaml_file) + + +def apply_total( + repo_config: RepoConfig, + repo_path: Path, + skip_source_validation: bool, + skip_feature_view_validation: bool = False, + no_promote: bool = False, +): + os.chdir(repo_path) + repo = _get_repo_contents(repo_path, repo_config.project, repo_config) + for project in repo.projects: + repo_config.project = project.name + store, registry = _get_store_and_registry(repo_config) + if not is_valid_name(project.name): + print( + f"{project.name} is not valid. Project name should only have " + f"alphanumerical values, underscores, and hyphens but not start with an underscore or hyphen." + ) + sys.exit(1) + # TODO: When we support multiple projects in a single repo, we should filter repo contents by project. Currently there is no way to associate Feast objects to project. + print(f"Applying changes for project {project.name}") + apply_total_with_repo_instance( + store, + project.name, + registry, + repo, + skip_source_validation, + skip_feature_view_validation, + no_promote=no_promote, + ) + + +def teardown(repo_config: RepoConfig, repo_path: Optional[str]): + # Cannot pass in both repo_path and repo_config to FeatureStore. + feature_store = FeatureStore(repo_path=repo_path, config=repo_config) + feature_store.teardown() + + +def registry_dump(repo_config: RepoConfig, repo_path: Path) -> str: + """For debugging only: output contents of the metadata registry""" + registry_config = repo_config.registry + project = repo_config.project + registry = Registry( + project, + registry_config=registry_config, + repo_path=repo_path, + auth_config=repo_config.auth_config, + ) + registry_dict = registry.to_dict(project=project) + return json.dumps(registry_dict, indent=2, sort_keys=True) + + +def cli_check_repo(repo_path: Path, fs_yaml_file: Path): + sys.path.append(str(repo_path)) + if not fs_yaml_file.exists(): + print( + f"Can't find feature repo configuration file at {fs_yaml_file}. " + "Make sure you're running feast from an initialized feast repository." + ) + sys.exit(1) + + +def init_repo(repo_name: str, template: str, repo_path: Optional[str] = None): + import os + from pathlib import Path + from shutil import copytree + + from colorama import Fore, Style + + # Validate project name + if not is_valid_name(repo_name): + raise BadParameter( + message="Name should be alphanumeric values, underscores, and hyphens but not start with an underscore or hyphen", + param_hint="PROJECT_DIRECTORY", + ) + + # Determine where to create the repository + if repo_path: + # User specified a custom path + target_path = Path(repo_path).resolve() + target_path.mkdir(parents=True, exist_ok=True) + display_path = repo_path + else: + # Default behavior: create subdirectory with project name + target_path = Path(os.path.join(Path.cwd(), repo_name)) + target_path.mkdir(exist_ok=True) + display_path = repo_name + + repo_config_path = target_path / "feature_store.yaml" + + if repo_config_path.exists(): + print( + f"The directory {Style.BRIGHT + Fore.GREEN}{display_path}{Style.RESET_ALL} contains an existing feature " + f"store repository that may cause a conflict" + ) + print() + sys.exit(1) + + # Copy template directory + template_path = str(Path(Path(__file__).parent / "templates" / template).absolute()) + if not os.path.exists(template_path): + raise IOError(f"Could not find template {template}") + copytree(template_path, str(target_path), dirs_exist_ok=True) + + # Rename gitignore files back to .gitignore + for gitignore_path in target_path.rglob("gitignore"): + gitignore_path.rename(gitignore_path.with_name(".gitignore")) + + # Seed the repository + bootstrap_path = target_path / "bootstrap.py" + if os.path.exists(bootstrap_path): + import importlib.util + + spec = importlib.util.spec_from_file_location("bootstrap", str(bootstrap_path)) + assert isinstance(spec, ModuleSpec) + bootstrap = importlib.util.module_from_spec(spec) + assert isinstance(spec.loader, Loader) + spec.loader.exec_module(bootstrap) + bootstrap.bootstrap() # type: ignore + os.remove(bootstrap_path) + + # Template the feature_store.yaml file + feature_store_yaml_path = target_path / "feature_repo" / "feature_store.yaml" + replace_str_in_file( + feature_store_yaml_path, "project: my_project", f"project: {repo_name}" + ) + + # Remove the __pycache__ folder if it exists + import shutil + + shutil.rmtree(target_path / "__pycache__", ignore_errors=True) + + import click + + click.echo() + click.echo( + f"Creating a new Feast repository in {Style.BRIGHT + Fore.GREEN}{target_path}{Style.RESET_ALL}." + ) + click.echo() + + +def is_valid_name(name: str) -> bool: + """A name should be alphanumeric values, underscores, and hyphens but not start with an underscore""" + return ( + not name.startswith(("_", "-")) and re.compile(r"[^\w-]+").search(name) is None + ) + + +def generate_project_name() -> str: + """Generates a unique project name""" + return f"{random.choice(adjectives)}_{random.choice(animals)}" diff --git a/sdk/python/feast/rest_error_handler.py b/sdk/python/feast/rest_error_handler.py index fc802866f94..a4a4d685597 100644 --- a/sdk/python/feast/rest_error_handler.py +++ b/sdk/python/feast/rest_error_handler.py @@ -13,45 +13,97 @@ def rest_error_handling_decorator(func): + """ + Decorator that provides HTTP session management and error handling for REST API calls. + + This decorator: + - Provides a cached HTTP session with connection pooling for improved performance + - Wraps session methods to add logging and error handling + - Maps Feast-specific errors from API responses + + The session is reused across requests (connection pooling), which saves + TCP/TLS handshake overhead on subsequent calls. + + Connection pool settings can be configured via feature_store.yaml: + ```yaml + online_store: + type: remote + path: http://localhost:6566 + connection_pool_size: 50 # Max connections in pool + connection_idle_timeout: 300 # Seconds before idle session closes (0 to disable) + connection_retries: 3 # Retry count with backoff + ``` + """ + @wraps(func) def wrapper(config: RepoConfig, *args, **kwargs): assert isinstance(config, RepoConfig) - # Get a Session object - with get_http_auth_requests_session(config.auth_config) as session: - # Define a wrapper for session methods - def method_wrapper(method_name): - original_method = getattr(session, method_name) - - @wraps(original_method) - def wrapped_method(*args, **kwargs): - logger.debug( - f"Calling {method_name} with args: {args}, kwargs: {kwargs}" - ) - response = original_method(*args, **kwargs) - logger.debug( - f"{method_name} response status code: {response.status_code}" - ) - - try: - response.raise_for_status() - except requests.RequestException: - logger.debug(f"response.json() = {response.json()}") - mapped_error = FeastError.from_error_detail(response.json()) - logger.debug(f"mapped_error = {str(mapped_error)}") - if mapped_error is not None: - raise mapped_error - return response - - return wrapped_method - - # Enhance session methods - session.get = method_wrapper("get") # type: ignore[method-assign] - session.post = method_wrapper("post") # type: ignore[method-assign] - session.put = method_wrapper("put") # type: ignore[method-assign] - session.delete = method_wrapper("delete") # type: ignore[method-assign] - - # Pass the enhanced session object to the decorated function - return func(session, config, *args, **kwargs) + pool_maxsize = None + max_idle_seconds = None + max_retries = None + + if config.online_store is not None: + attr_map = { + "pool_maxsize": "connection_pool_size", + "max_idle_seconds": "connection_idle_timeout", + "max_retries": "connection_retries", + } + conn_config = { + key: getattr(config.online_store, attr_name, None) + for key, attr_name in attr_map.items() + } + pool_maxsize = conn_config["pool_maxsize"] + max_idle_seconds = conn_config["max_idle_seconds"] + max_retries = conn_config["max_retries"] + + session = get_http_auth_requests_session( + config.auth_config, + pool_maxsize=pool_maxsize, + max_idle_seconds=max_idle_seconds, + max_retries=max_retries, + ) + + _wrap_session_methods(session) + + return func(session, config, *args, **kwargs) return wrapper + + +_ATTR_WRAPPED = "_feast_methods_wrapped" + + +def _wrap_session_methods(session: requests.Session) -> None: + """ + Wrap session HTTP methods with logging and Feast error mapping. + + Wraps each method exactly once. Subsequent calls are no-ops, preventing + the unbounded nesting that leads to ``RecursionError`` when a cached + session is reused across many requests. + """ + if getattr(session, _ATTR_WRAPPED, False): + return + + for method_name in ("get", "post", "put", "delete"): + original_method = getattr(session, method_name) + + @wraps(original_method) + def wrapped_method(*args, _orig=original_method, _name=method_name, **kwargs): + logger.debug(f"Calling {_name} with args: {args}, kwargs: {kwargs}") + response = _orig(*args, **kwargs) + logger.debug(f"{_name} response status code: {response.status_code}") + + try: + response.raise_for_status() + except requests.RequestException: + logger.debug(f"response.json() = {response.json()}") + mapped_error = FeastError.from_error_detail(response.json()) + logger.debug(f"mapped_error = {str(mapped_error)}") + if mapped_error is not None: + raise mapped_error + return response + + setattr(session, method_name, wrapped_method) + + object.__setattr__(session, _ATTR_WRAPPED, True) diff --git a/sdk/python/feast/saved_dataset.py b/sdk/python/feast/saved_dataset.py index 4a3043a8731..32877bbd017 100644 --- a/sdk/python/feast/saved_dataset.py +++ b/sdk/python/feast/saved_dataset.py @@ -34,6 +34,7 @@ def __new__(cls, name, bases, dct): _DATA_SOURCE_TO_SAVED_DATASET_STORAGE = { "FileSource": "feast.infra.offline_stores.file_source.SavedDatasetFileStorage", + "SparkSource": "feast.infra.offline_stores.contrib.spark_offline_store.spark_source.SavedDatasetSparkStorage", } @@ -82,6 +83,9 @@ class SavedDataset: storage: SavedDatasetStorage tags: Dict[str, str] feature_service_name: Optional[str] = None + namespace: str = "" + collection: str = "" + description: str = "" created_timestamp: Optional[datetime] = None last_updated_timestamp: Optional[datetime] = None @@ -100,6 +104,9 @@ def __init__( full_feature_names: bool = False, tags: Optional[Dict[str, str]] = None, feature_service_name: Optional[str] = None, + namespace: str = "", + collection: str = "", + description: str = "", ): self.name = name self.features = features @@ -108,6 +115,9 @@ def __init__( self.full_feature_names = full_feature_names self.tags = tags or {} self.feature_service_name = feature_service_name + self.namespace = namespace + self.collection = collection + self.description = description self._retrieval_job = None @@ -123,9 +133,7 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, SavedDataset): - raise TypeError( - "Comparisons should only involve SavedDataset class objects." - ) + return False if ( self.name != other.name @@ -135,6 +143,9 @@ def __eq__(self, other): or self.full_feature_names != other.full_feature_names or self.tags != other.tags or self.feature_service_name != other.feature_service_name + or self.namespace != other.namespace + or self.collection != other.collection + or self.description != other.description ): return False @@ -155,6 +166,9 @@ def from_proto(saved_dataset_proto: SavedDatasetProto): full_feature_names=saved_dataset_proto.spec.full_feature_names, storage=SavedDatasetStorage.from_proto(saved_dataset_proto.spec.storage), tags=dict(saved_dataset_proto.spec.tags.items()), + namespace=saved_dataset_proto.spec.namespace, + collection=saved_dataset_proto.spec.collection, + description=saved_dataset_proto.spec.description, ) if saved_dataset_proto.spec.feature_service_name: @@ -201,6 +215,9 @@ def to_proto(self) -> SavedDatasetProto: full_feature_names=self.full_feature_names, storage=self.storage.to_proto(), tags=self.tags, + namespace=self.namespace, + collection=self.collection, + description=self.description, ) if self.feature_service_name: spec.feature_service_name = self.feature_service_name diff --git a/sdk/python/feast/stream_feature_view.py b/sdk/python/feast/stream_feature_view.py index ff6d603333d..0538345069f 100644 --- a/sdk/python/feast/stream_feature_view.py +++ b/sdk/python/feast/stream_feature_view.py @@ -6,15 +6,18 @@ from typing import Any, Dict, List, Optional, Tuple, Type, Union import dill +from google.protobuf.duration_pb2 import Duration from google.protobuf.message import Message from typeguard import typechecked from feast import flags_helper, utils from feast.aggregation import Aggregation +from feast.base_feature_view import BaseFeatureView from feast.data_source import DataSource from feast.entity import Entity from feast.feature_view import FeatureView from feast.field import Field +from feast.proto_utils import mode_to_string, serialize_data_source from feast.protos.feast.core.DataSource_pb2 import DataSource as DataSourceProto from feast.protos.feast.core.OnDemandFeatureView_pb2 import ( UserDefinedFunction as UserDefinedFunctionProto, @@ -56,13 +59,16 @@ class StreamFeatureView(FeatureView): columns. If not specified, can be inferred from the underlying data source. source: The stream source of data where this group of features is stored. aggregations: List of aggregations registered with the stream feature view. - mode: The mode of execution. timestamp_field: Must be specified if aggregations are specified. Defines the timestamp column on which to aggregate windows. + enable_tiling: Enable tiling optimization for efficient windowed aggregations. + tiling_hop_size: Time interval between tiles (e.g., 5 minutes). Defaults to 5 minutes. online: A boolean indicating whether online retrieval, and write to online store is enabled for this feature view. offline: A boolean indicating whether offline retrieval, and write to offline store is enabled for this feature view. description: A human-readable description. tags: A dictionary of key-value pairs to store arbitrary metadata. owner: The owner of the stream feature view, typically the email of the primary maintainer. + org: The organizational unit that owns this stream feature view (e.g. "ads", "search"). + Defaults to empty string. udf: The user defined transformation function. This transformation function should have all of the corresponding imports imported within the function. udf_string: The string representation of the user defined transformation function. feature_transformation: The transformation to apply to the features. @@ -84,14 +90,19 @@ class StreamFeatureView(FeatureView): description: str tags: Dict[str, str] owner: str - aggregations: List[Aggregation] + org: str mode: Union[TransformationMode, str] - timestamp_field: str materialization_intervals: List[Tuple[datetime, datetime]] udf: Optional[FunctionType] udf_string: Optional[str] feature_transformation: Optional[Transformation] stream_engine: Optional[Dict[str, Any]] = None + aggregations: List[Aggregation] + timestamp_field: str + enable_tiling: bool + tiling_hop_size: Optional[timedelta] + _raw_udf_proto: Optional[Any] = None + _raw_feature_transformation_proto: Optional[Any] = None def __init__( self, @@ -106,6 +117,7 @@ def __init__( offline: bool = False, description: str = "", owner: str = "", + org: str = "", schema: Optional[List[Field]] = None, aggregations: Optional[List[Aggregation]] = None, mode: Union[str, TransformationMode] = TransformationMode.PYTHON, @@ -114,6 +126,10 @@ def __init__( udf_string: Optional[str] = "", feature_transformation: Optional[Transformation] = None, stream_engine: Optional[Dict[str, Any]] = None, + enable_tiling: bool = False, + tiling_hop_size: Optional[timedelta] = None, + enable_validation: bool = False, + version: str = "latest", ): if not flags_helper.is_test(): warnings.warn( @@ -136,15 +152,33 @@ def __init__( "aggregations must have a timestamp field associated with them to perform the aggregations" ) - self.aggregations = aggregations or [] self.mode = mode - self.timestamp_field = timestamp_field or "" self.udf = udf self.udf_string = udf_string + self.aggregations = aggregations or [] + self.timestamp_field = timestamp_field or "" self.feature_transformation = ( feature_transformation or self.get_feature_transformation() ) self.stream_engine = stream_engine + self.enable_tiling = enable_tiling + self.tiling_hop_size = tiling_hop_size + + if enable_tiling and self.aggregations: + effective_hop_size = tiling_hop_size or timedelta(minutes=5) + time_windows = [ + agg.time_window + for agg in self.aggregations + if agg.time_window is not None + ] + if time_windows: + min_window_size = min(time_windows) + if effective_hop_size >= min_window_size: + raise ValueError( + f"tiling_hop_size ({effective_hop_size}) must be smaller than " + f"the minimum aggregation time_window ({min_window_size}). " + f"If hop_size >= window_size, the tiling algorithm will produce incorrect results." + ) super().__init__( name=name, @@ -155,10 +189,13 @@ def __init__( offline=offline, description=description, owner=owner, + org=org, schema=schema, source=source, # type: ignore[arg-type] mode=mode, sink_source=sink_source, + enable_validation=enable_validation, + version=version, ) def get_feature_transformation(self) -> Optional[Transformation]: @@ -170,7 +207,8 @@ def get_feature_transformation(self) -> Optional[Transformation]: TransformationMode.PYTHON, TransformationMode.SPARK_SQL, TransformationMode.SPARK, - ) or self.mode in ("pandas", "python", "spark_sql", "spark"): + TransformationMode.FLINK, + ) or self.mode in ("pandas", "python", "spark_sql", "spark", "flink"): return Transformation( mode=self.mode, udf=self.udf, udf_string=self.udf_string or "" ) @@ -179,9 +217,38 @@ def get_feature_transformation(self) -> Optional[Transformation]: f"Unsupported transformation mode: {self.mode} for StreamFeatureView" ) + def _schema_or_udf_changed(self, other: "BaseFeatureView") -> bool: + """Check for StreamFeatureView schema/UDF changes.""" + if super()._schema_or_udf_changed(other): + return True + + if not isinstance(other, StreamFeatureView): + return True + + # UDF changes + if self.udf and other.udf: + self_code = getattr(self.udf, "__code__", None) + other_code = getattr(other.udf, "__code__", None) + if self_code and other_code: + if self_code.co_code != other_code.co_code: + return True + elif self.udf != other.udf: # One is None + return True + + if self.udf_string != other.udf_string: + return True + if self.aggregations != other.aggregations: + return True + if self.timestamp_field != other.timestamp_field: + return True + if self.mode != other.mode: + return True + + return False + def __eq__(self, other): if not isinstance(other, StreamFeatureView): - raise TypeError("Comparisons should only involve StreamFeatureViews") + return False if not super().__eq__(other): return False @@ -209,18 +276,16 @@ def to_proto(self): meta = self.to_proto_meta() ttl_duration = self.get_ttl_duration() - batch_source_proto = None - if self.batch_source: - batch_source_proto = self.batch_source.to_proto() - batch_source_proto.data_source_class_type = f"{self.batch_source.__class__.__module__}.{self.batch_source.__class__.__name__}" - - stream_source_proto = None - if self.stream_source: - stream_source_proto = self.stream_source.to_proto() - stream_source_proto.data_source_class_type = f"{self.stream_source.__class__.__module__}.{self.stream_source.__class__.__name__}" + batch_source_proto = serialize_data_source(self.batch_source) + stream_source_proto = serialize_data_source(self.stream_source) udf_proto, feature_transformation = None, None - if self.udf: + if getattr(self, "_raw_udf_proto", None) is not None: + udf_proto = self._raw_udf_proto + feature_transformation = getattr( + self, "_raw_feature_transformation_proto", None + ) + elif self.udf: udf_proto = UserDefinedFunctionProto( name=self.udf.__name__, body=dill.dumps(self.udf, recurse=True), @@ -236,9 +301,11 @@ def to_proto(self): user_defined_function=udf_proto_v2, ) - mode = ( - self.mode.value if isinstance(self.mode, TransformationMode) else self.mode - ) + # Serialize tiling configuration + tiling_hop_size_duration = None + if self.tiling_hop_size is not None: + tiling_hop_size_duration = Duration() + tiling_hop_size_duration.FromTimedelta(self.tiling_hop_size) spec = StreamFeatureViewSpecProto( name=self.name, @@ -250,19 +317,25 @@ def to_proto(self): description=self.description, tags=self.tags, owner=self.owner, + org=self.org, ttl=ttl_duration, online=self.online, - batch_source=batch_source_proto or None, - stream_source=stream_source_proto or None, + batch_source=batch_source_proto, + stream_source=stream_source_proto, timestamp_field=self.timestamp_field, aggregations=[agg.to_proto() for agg in self.aggregations], - mode=mode, + mode=mode_to_string(self.mode), + enable_tiling=self.enable_tiling, + tiling_hop_size=tiling_hop_size_duration, + enable_validation=self.enable_validation, + version=self.version, + disabled=not self.enabled, ) return StreamFeatureViewProto(spec=spec, meta=meta) @classmethod - def from_proto(cls, sfv_proto): + def from_proto(cls, sfv_proto, skip_udf: bool = False): batch_source = ( DataSource.from_proto(sfv_proto.spec.batch_source) if sfv_proto.spec.HasField("batch_source") @@ -275,7 +348,7 @@ def from_proto(cls, sfv_proto): ) udf = ( dill.loads(sfv_proto.spec.user_defined_function.body) - if sfv_proto.spec.HasField("user_defined_function") + if sfv_proto.spec.HasField("user_defined_function") and not skip_udf else None ) udf_string = ( @@ -293,6 +366,7 @@ def from_proto(cls, sfv_proto): description=sfv_proto.spec.description, tags=dict(sfv_proto.spec.tags), owner=sfv_proto.spec.owner, + org=sfv_proto.spec.org, online=sfv_proto.spec.online, schema=[ Field.from_proto(field_proto) for field_proto in sfv_proto.spec.features @@ -311,6 +385,15 @@ def from_proto(cls, sfv_proto): for agg_proto in sfv_proto.spec.aggregations ], timestamp_field=sfv_proto.spec.timestamp_field, + enable_tiling=sfv_proto.spec.enable_tiling, + tiling_hop_size=( + sfv_proto.spec.tiling_hop_size.ToTimedelta() + if sfv_proto.spec.HasField("tiling_hop_size") + and sfv_proto.spec.tiling_hop_size.ToNanoseconds() != 0 + else None + ), + enable_validation=sfv_proto.spec.enable_validation, + version=sfv_proto.spec.version or "latest", ) if batch_source: @@ -319,6 +402,30 @@ def from_proto(cls, sfv_proto): if stream_source: stream_feature_view.stream_source = stream_source + # Restore current_version_number from meta. + spec_version = sfv_proto.spec.version + cvn = sfv_proto.meta.current_version_number + if cvn > 0: + stream_feature_view.current_version_number = cvn + elif cvn == 0 and spec_version and spec_version.lower() != "latest": + stream_feature_view.current_version_number = 0 + else: + stream_feature_view.current_version_number = None + + stream_feature_view.enabled = not sfv_proto.spec.disabled + + # Restore lifecycle state from meta (SFV uses FeatureViewMeta which has state). + from feast.feature_view import FeatureViewState + + stream_feature_view.state = FeatureViewState.from_proto(sfv_proto.meta.state) + + if skip_udf and sfv_proto.spec.HasField("user_defined_function"): + stream_feature_view._raw_udf_proto = sfv_proto.spec.user_defined_function + if skip_udf and sfv_proto.spec.HasField("feature_transformation"): + stream_feature_view._raw_feature_transformation_proto = ( + sfv_proto.spec.feature_transformation + ) + stream_feature_view.entities = list(sfv_proto.spec.entities) stream_feature_view.features = [ @@ -357,6 +464,7 @@ def __copy__(self): online=self.online, description=self.description, owner=self.owner, + org=self.org, aggregations=self.aggregations, mode=self.mode, timestamp_field=self.timestamp_field, @@ -364,7 +472,11 @@ def __copy__(self): udf=self.udf, udf_string=self.udf_string, feature_transformation=self.feature_transformation, + enable_validation=self.enable_validation, + version=self.version, ) + fv.enabled = self.enabled + fv.state = self.state fv.entities = self.entities fv.features = copy.copy(self.features) fv.entity_columns = copy.copy(self.entity_columns) @@ -384,11 +496,14 @@ def stream_feature_view( online: Optional[bool] = True, description: Optional[str] = "", owner: Optional[str] = "", + org: Optional[str] = "", schema: Optional[List[Field]] = None, source: Optional[DataSource] = None, aggregations: Optional[List[Aggregation]] = None, mode: Optional[str] = "spark", timestamp_field: Optional[str] = "", + enable_validation: bool = False, + version: str = "latest", ): """ Creates an StreamFeatureView object with the given user function as udf. @@ -417,9 +532,12 @@ def decorator(user_function): tags=tags, online=online, owner=owner, + org=org, aggregations=aggregations, mode=mode, timestamp_field=timestamp_field, + enable_validation=enable_validation, + version=version, ) functools.update_wrapper(wrapper=stream_feature_view_obj, wrapped=user_function) return stream_feature_view_obj diff --git a/sdk/python/feast/table_format.py b/sdk/python/feast/table_format.py new file mode 100644 index 00000000000..829d8a6e19e --- /dev/null +++ b/sdk/python/feast/table_format.py @@ -0,0 +1,564 @@ +# Copyright 2020 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from abc import ABC, abstractmethod +from enum import Enum +from typing import TYPE_CHECKING, Dict, Optional + +if TYPE_CHECKING: + from feast.protos.feast.core.DataFormat_pb2 import TableFormat as TableFormatProto + + +class TableFormatType(Enum): + """Enum for supported table formats""" + + DELTA = "delta" + ICEBERG = "iceberg" + HUDI = "hudi" + + +class TableFormat(ABC): + """ + Abstract base class for table formats. + + Table formats encapsulate metadata and configuration specific to different + table storage formats like Iceberg, Delta Lake, Hudi, etc. They provide + a unified interface for configuring table-specific properties that are + used when reading from or writing to these advanced table formats. + + This base class defines the contract that all table format implementations + must follow, including serialization/deserialization capabilities and + property management. + + Attributes: + format_type (TableFormatType): The type of table format (iceberg, delta, hudi). + properties (Dict[str, str]): Dictionary of format-specific properties. + + Examples: + Table formats are typically used with data sources to specify + advanced table metadata and reading options: + + >>> from feast.table_format import IcebergFormat + >>> iceberg_format = IcebergFormat( + ... catalog="my_catalog", + ... namespace="my_namespace" + ... ) + >>> iceberg_format.set_property("snapshot-id", "123456789") + """ + + def __init__( + self, format_type: TableFormatType, properties: Optional[Dict[str, str]] = None + ): + self.format_type = format_type + self.properties = properties or {} + + @abstractmethod + def to_dict(self) -> Dict: + """Convert table format to dictionary representation""" + pass + + @classmethod + @abstractmethod + def from_dict(cls, data: Dict) -> "TableFormat": + """Create table format from dictionary representation""" + pass + + def get_property(self, key: str, default: Optional[str] = None) -> Optional[str]: + """Get a table format property""" + return self.properties.get(key, default) + + def set_property(self, key: str, value: str) -> None: + """Set a table format property""" + self.properties[key] = value + + +class IcebergFormat(TableFormat): + """ + Apache Iceberg table format configuration. + + Iceberg is an open table format for huge analytic datasets. This class provides + configuration for Iceberg-specific properties including catalog configuration, + namespace settings, and table-level properties for reading and writing Iceberg tables. + + Args: + catalog (Optional[str]): Name of the Iceberg catalog to use. The catalog manages + table metadata and provides access to tables. + namespace (Optional[str]): Namespace (schema/database) within the catalog where + the table is located. + properties (Optional[Dict[str, str]]): Properties for configuring Iceberg + catalog and table operations (e.g., warehouse location, snapshot-id, + as-of-timestamp, file format, compression, partitioning). + + Attributes: + catalog (str): The Iceberg catalog name. + namespace (str): The namespace within the catalog. + properties (Dict[str, str]): Iceberg configuration properties. + + Examples: + Basic Iceberg configuration: + + >>> iceberg_format = IcebergFormat( + ... catalog="my_catalog", + ... namespace="my_database" + ... ) + + Advanced configuration with properties: + + >>> iceberg_format = IcebergFormat( + ... catalog="spark_catalog", + ... namespace="lakehouse", + ... properties={ + ... "warehouse": "s3://my-bucket/warehouse", + ... "catalog-impl": "org.apache.iceberg.spark.SparkCatalog", + ... "format-version": "2", + ... "write.parquet.compression-codec": "snappy" + ... } + ... ) + + Reading from a specific snapshot: + + >>> iceberg_format = IcebergFormat(catalog="my_catalog", namespace="db") + >>> iceberg_format.set_property("snapshot-id", "123456789") + + Time travel queries: + + >>> iceberg_format.set_property("as-of-timestamp", "1648684800000") + """ + + def __init__( + self, + catalog: Optional[str] = None, + namespace: Optional[str] = None, + properties: Optional[Dict[str, str]] = None, + ): + super().__init__(TableFormatType.ICEBERG, properties) + self.catalog = catalog + self.namespace = namespace + + # Add catalog and namespace to properties if provided + if catalog: + self.properties["iceberg.catalog"] = catalog + if namespace: + self.properties["iceberg.namespace"] = namespace + + def to_dict(self) -> Dict: + return { + "format_type": self.format_type.value, + "catalog": self.catalog, + "namespace": self.namespace, + "properties": self.properties, + } + + @classmethod + def from_dict(cls, data: Dict) -> "IcebergFormat": + return cls( + catalog=data.get("catalog"), + namespace=data.get("namespace"), + properties=data.get("properties", {}), + ) + + def to_proto(self) -> "TableFormatProto": + """Convert to protobuf TableFormat message""" + from feast.protos.feast.core.DataFormat_pb2 import ( + TableFormat as TableFormatProto, + ) + + iceberg_proto = TableFormatProto.IcebergFormat( + catalog=self.catalog or "", + namespace=self.namespace or "", + properties=self.properties, + ) + return TableFormatProto(iceberg_format=iceberg_proto) + + @classmethod + def from_proto(cls, proto: "TableFormatProto") -> "IcebergFormat": + """Create from protobuf TableFormat message""" + iceberg_proto = proto.iceberg_format + return cls( + catalog=iceberg_proto.catalog if iceberg_proto.catalog else None, + namespace=iceberg_proto.namespace if iceberg_proto.namespace else None, + properties=dict(iceberg_proto.properties), + ) + + +class DeltaFormat(TableFormat): + """ + Delta Lake table format configuration. + + Delta Lake is an open-source storage layer that brings ACID transactions to Apache Spark + and big data workloads. This class provides configuration for Delta-specific properties + including table properties, checkpoint locations, and versioning options. + + Args: + checkpoint_location (Optional[str]): Location for storing Delta transaction logs + and checkpoints. Required for streaming operations. + properties (Optional[Dict[str, str]]): Properties for configuring Delta table + behavior (e.g., auto-optimize, vacuum settings, data skipping). + + Attributes: + checkpoint_location (str): Path to checkpoint storage location. + properties (Dict[str, str]): Delta table configuration properties. + + Examples: + Basic Delta configuration: + + >>> delta_format = DeltaFormat() + + Configuration with table properties: + + >>> delta_format = DeltaFormat( + ... properties={ + ... "delta.autoOptimize.optimizeWrite": "true", + ... "delta.autoOptimize.autoCompact": "true", + ... "delta.tuneFileSizesForRewrites": "true" + ... } + ... ) + + Streaming configuration with checkpoint: + + >>> delta_format = DeltaFormat( + ... checkpoint_location="s3://my-bucket/checkpoints/my_table" + ... ) + + Time travel - reading specific version: + + >>> delta_format = DeltaFormat() + >>> delta_format.set_property("versionAsOf", "5") + + Time travel - reading at specific timestamp: + + >>> delta_format.set_property("timestampAsOf", "2023-01-01 00:00:00") + """ + + def __init__( + self, + checkpoint_location: Optional[str] = None, + properties: Optional[Dict[str, str]] = None, + ): + super().__init__(TableFormatType.DELTA, properties) + self.checkpoint_location = checkpoint_location + + # Add checkpoint location to properties if provided + if checkpoint_location: + self.properties["delta.checkpointLocation"] = checkpoint_location + + def to_dict(self) -> Dict: + return { + "format_type": self.format_type.value, + "checkpoint_location": self.checkpoint_location, + "properties": self.properties, + } + + @classmethod + def from_dict(cls, data: Dict) -> "DeltaFormat": + return cls( + checkpoint_location=data.get("checkpoint_location"), + properties=data.get("properties", {}), + ) + + def to_proto(self) -> "TableFormatProto": + """Convert to protobuf TableFormat message""" + from feast.protos.feast.core.DataFormat_pb2 import ( + TableFormat as TableFormatProto, + ) + + delta_proto = TableFormatProto.DeltaFormat( + checkpoint_location=self.checkpoint_location or "", + properties=self.properties, + ) + return TableFormatProto(delta_format=delta_proto) + + @classmethod + def from_proto(cls, proto: "TableFormatProto") -> "DeltaFormat": + """Create from protobuf TableFormat message""" + delta_proto = proto.delta_format + return cls( + checkpoint_location=delta_proto.checkpoint_location + if delta_proto.checkpoint_location + else None, + properties=dict(delta_proto.properties), + ) + + +class HudiFormat(TableFormat): + """ + Apache Hudi table format configuration. + + Apache Hudi is a data management framework used to simplify incremental data processing + and data pipeline development. This class provides configuration for Hudi-specific + properties including table type, record keys, and write operations. + + Args: + table_type (Optional[str]): Type of Hudi table. Options are: + - "COPY_ON_WRITE": Stores data in columnar format (Parquet) and rewrites entire files + - "MERGE_ON_READ": Stores data using combination of columnar and row-based formats + record_key (Optional[str]): Field(s) that uniquely identify a record. Can be a single + field or comma-separated list for composite keys. + precombine_field (Optional[str]): Field used to determine the latest version of a record + when multiple updates exist (usually a timestamp or version field). + properties (Optional[Dict[str, str]]): Additional Hudi table properties for + configuring compaction, indexing, and other Hudi features. + + Attributes: + table_type (str): The Hudi table type (COPY_ON_WRITE or MERGE_ON_READ). + record_key (str): The record key field(s). + precombine_field (str): The field used for record deduplication. + properties (Dict[str, str]): Additional Hudi configuration properties. + + Examples: + Basic Hudi configuration: + + >>> hudi_format = HudiFormat( + ... table_type="COPY_ON_WRITE", + ... record_key="user_id", + ... precombine_field="timestamp" + ... ) + + Configuration with composite record key: + + >>> hudi_format = HudiFormat( + ... table_type="MERGE_ON_READ", + ... record_key="user_id,event_type", + ... precombine_field="event_timestamp" + ... ) + + Advanced configuration with table properties: + + >>> hudi_format = HudiFormat( + ... table_type="COPY_ON_WRITE", + ... record_key="id", + ... precombine_field="updated_at", + ... properties={ + ... "hoodie.compaction.strategy": "org.apache.hudi.table.action.compact.strategy.LogFileSizeBasedCompactionStrategy", + ... "hoodie.index.type": "BLOOM", + ... "hoodie.bloom.index.parallelism": "100" + ... } + ... ) + + Reading incremental data: + + >>> hudi_format = HudiFormat(table_type="COPY_ON_WRITE") + >>> hudi_format.set_property("hoodie.datasource.query.type", "incremental") + >>> hudi_format.set_property("hoodie.datasource.read.begin.instanttime", "20230101000000") + """ + + def __init__( + self, + table_type: Optional[str] = None, # COPY_ON_WRITE or MERGE_ON_READ + record_key: Optional[str] = None, + precombine_field: Optional[str] = None, + properties: Optional[Dict[str, str]] = None, + ): + super().__init__(TableFormatType.HUDI, properties) + self.table_type = table_type + self.record_key = record_key + self.precombine_field = precombine_field + + # Add Hudi-specific properties if provided + if table_type: + self.properties["hoodie.datasource.write.table.type"] = table_type + if record_key: + self.properties["hoodie.datasource.write.recordkey.field"] = record_key + if precombine_field: + self.properties["hoodie.datasource.write.precombine.field"] = ( + precombine_field + ) + + def to_dict(self) -> Dict: + return { + "format_type": self.format_type.value, + "table_type": self.table_type, + "record_key": self.record_key, + "precombine_field": self.precombine_field, + "properties": self.properties, + } + + @classmethod + def from_dict(cls, data: Dict) -> "HudiFormat": + return cls( + table_type=data.get("table_type"), + record_key=data.get("record_key"), + precombine_field=data.get("precombine_field"), + properties=data.get("properties", {}), + ) + + def to_proto(self) -> "TableFormatProto": + """Convert to protobuf TableFormat message""" + from feast.protos.feast.core.DataFormat_pb2 import ( + TableFormat as TableFormatProto, + ) + + hudi_proto = TableFormatProto.HudiFormat( + table_type=self.table_type or "", + record_key=self.record_key or "", + precombine_field=self.precombine_field or "", + properties=self.properties, + ) + return TableFormatProto(hudi_format=hudi_proto) + + @classmethod + def from_proto(cls, proto: "TableFormatProto") -> "HudiFormat": + """Create from protobuf TableFormat message""" + hudi_proto = proto.hudi_format + return cls( + table_type=hudi_proto.table_type if hudi_proto.table_type else None, + record_key=hudi_proto.record_key if hudi_proto.record_key else None, + precombine_field=hudi_proto.precombine_field + if hudi_proto.precombine_field + else None, + properties=dict(hudi_proto.properties), + ) + + +def create_table_format(format_type: TableFormatType, **kwargs) -> TableFormat: + """ + Factory function to create appropriate TableFormat instance based on type. + + This is a convenience function that creates the correct TableFormat subclass + based on the provided format type, passing through any additional keyword arguments + to the constructor. + + Args: + format_type (TableFormatType): The type of table format to create. + **kwargs: Additional keyword arguments passed to the format constructor. + + Returns: + TableFormat: An instance of the appropriate TableFormat subclass. + + Raises: + ValueError: If an unsupported format_type is provided. + + Examples: + Create an Iceberg format: + + >>> iceberg_format = create_table_format( + ... TableFormatType.ICEBERG, + ... catalog="my_catalog", + ... namespace="my_db" + ... ) + + Create a Delta format: + + >>> delta_format = create_table_format( + ... TableFormatType.DELTA, + ... checkpoint_location="s3://bucket/checkpoints" + ... ) + """ + if format_type == TableFormatType.ICEBERG: + return IcebergFormat(**kwargs) + elif format_type == TableFormatType.DELTA: + return DeltaFormat(**kwargs) + elif format_type == TableFormatType.HUDI: + return HudiFormat(**kwargs) + else: + raise ValueError(f"Unknown table format type: {format_type}") + + +def table_format_from_dict(data: Dict) -> TableFormat: + """ + Create TableFormat instance from dictionary representation. + + This function deserializes a dictionary (typically from JSON or protobuf) + back into the appropriate TableFormat instance. The dictionary must contain + a 'format_type' field that indicates which format class to instantiate. + + Args: + data (Dict): Dictionary containing table format configuration. Must include + 'format_type' field with value 'iceberg', 'delta', or 'hudi'. + + Returns: + TableFormat: An instance of the appropriate TableFormat subclass. + + Raises: + ValueError: If format_type is not recognized. + KeyError: If format_type field is missing from data. + + Examples: + Deserialize an Iceberg format: + + >>> data = { + ... "format_type": "iceberg", + ... "catalog": "my_catalog", + ... "namespace": "my_db" + ... } + >>> iceberg_format = table_format_from_dict(data) + """ + if "format_type" not in data: + raise KeyError("Missing 'format_type' field in data") + format_type = data["format_type"] + + if format_type == TableFormatType.ICEBERG.value: + return IcebergFormat.from_dict(data) + elif format_type == TableFormatType.DELTA.value: + return DeltaFormat.from_dict(data) + elif format_type == TableFormatType.HUDI.value: + return HudiFormat.from_dict(data) + else: + raise ValueError(f"Unknown table format type: {format_type}") + + +def table_format_from_json(json_str: str) -> TableFormat: + """ + Create TableFormat instance from JSON string. + + This is a convenience function that parses a JSON string and creates + the appropriate TableFormat instance. Useful for loading table format + configurations from files or network requests. + + Args: + json_str (str): JSON string containing table format configuration. + + Returns: + TableFormat: An instance of the appropriate TableFormat subclass. + + Raises: + json.JSONDecodeError: If the JSON string is invalid. + ValueError: If format_type is not recognized. + KeyError: If format_type field is missing. + + Examples: + Load from JSON string: + + >>> json_config = '{"format_type": "delta", "checkpoint_location": "s3://bucket/checkpoints"}' + >>> delta_format = table_format_from_json(json_config) + """ + data = json.loads(json_str) + return table_format_from_dict(data) + + +def table_format_from_proto(proto: "TableFormatProto") -> TableFormat: + """ + Create TableFormat instance from protobuf TableFormat message. + + Args: + proto: TableFormat protobuf message + + Returns: + TableFormat: An instance of the appropriate TableFormat subclass. + + Raises: + ValueError: If the proto doesn't contain a recognized format. + """ + + which_format = proto.WhichOneof("format") + + if which_format == "iceberg_format": + return IcebergFormat.from_proto(proto) + elif which_format == "delta_format": + return DeltaFormat.from_proto(proto) + elif which_format == "hudi_format": + return HudiFormat.from_proto(proto) + else: + raise ValueError(f"Unknown table format in proto: {which_format}") diff --git a/sdk/python/feast/templates/athena/feature_repo/test_workflow.py b/sdk/python/feast/templates/athena/feature_repo/test_workflow.py index 8d6479da80e..8bbdb07f161 100644 --- a/sdk/python/feast/templates/athena/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/athena/feature_repo/test_workflow.py @@ -50,6 +50,7 @@ def test_end_to_end(): ], online=True, source=driver_hourly_stats, + version="latest", ) # apply repository diff --git a/sdk/python/feast/templates/aws/README.md b/sdk/python/feast/templates/aws/README.md index 008a338e984..10ae4887551 100644 --- a/sdk/python/feast/templates/aws/README.md +++ b/sdk/python/feast/templates/aws/README.md @@ -2,7 +2,7 @@ A quick view of what's in this repository: * `data/` contains raw demo parquet data -* `example_repo.py` contains demo feature definitions +* `feature_definitions.py` contains demo feature definitions * `feature_store.yaml` contains a demo setup configuring where data sources are * `test_workflow.py` showcases how to run all key Feast commands, including defining, retrieving, and pushing features. diff --git a/sdk/python/feast/templates/aws/bootstrap.py b/sdk/python/feast/templates/aws/bootstrap.py index 63e5b50203b..9ec3d322b19 100644 --- a/sdk/python/feast/templates/aws/bootstrap.py +++ b/sdk/python/feast/templates/aws/bootstrap.py @@ -55,7 +55,7 @@ def bootstrap(): ) repo_path = pathlib.Path(__file__).parent.absolute() / "feature_repo" - example_py_file = repo_path / "example_repo.py" + example_py_file = repo_path / "feature_definitions.py" replace_str_in_file(example_py_file, "%REDSHIFT_DATABASE%", database) config_file = repo_path / "feature_store.yaml" diff --git a/sdk/python/feast/templates/aws/feature_repo/example_repo.py b/sdk/python/feast/templates/aws/feature_repo/feature_definitions.py similarity index 99% rename from sdk/python/feast/templates/aws/feature_repo/example_repo.py rename to sdk/python/feast/templates/aws/feature_repo/feature_definitions.py index dd5a9c925b7..8d4faf4c74c 100644 --- a/sdk/python/feast/templates/aws/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/aws/feature_repo/feature_definitions.py @@ -57,6 +57,7 @@ # Tags are user defined key/value pairs that are attached to each # feature view tags={"team": "driver_performance"}, + version="latest", ) # Define a request data source which encodes features / information only @@ -119,6 +120,7 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: online=True, source=driver_stats_push_source, # Changed from above tags={"team": "driver_performance"}, + version="latest", ) diff --git a/sdk/python/feast/templates/cassandra/bootstrap.py b/sdk/python/feast/templates/cassandra/bootstrap.py index 16c82316258..cfd1fddacf2 100644 --- a/sdk/python/feast/templates/cassandra/bootstrap.py +++ b/sdk/python/feast/templates/cassandra/bootstrap.py @@ -273,8 +273,8 @@ def bootstrap(): driver_stats_path = data_path / "driver_stats.parquet" driver_df.to_parquet(path=str(driver_stats_path), allow_truncated_timestamps=True) - # example_repo.py - example_py_file = repo_path / "example_repo.py" + # feature_definitions.py + example_py_file = repo_path / "feature_definitions.py" replace_str_in_file( example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) ) diff --git a/sdk/python/feast/templates/hazelcast/feature_repo/example_repo.py b/sdk/python/feast/templates/cassandra/feature_repo/feature_definitions.py similarity index 99% rename from sdk/python/feast/templates/hazelcast/feature_repo/example_repo.py rename to sdk/python/feast/templates/cassandra/feature_repo/feature_definitions.py index 131f1bcaa61..21b2985409e 100644 --- a/sdk/python/feast/templates/hazelcast/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/cassandra/feature_repo/feature_definitions.py @@ -52,6 +52,7 @@ # Tags are user defined key/value pairs that are attached to each # feature view tags={"team": "driver_performance"}, + version="latest", ) # Define a request data source which encodes features / information only @@ -114,6 +115,7 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: online=True, source=driver_stats_push_source, # Changed from above tags={"team": "driver_performance"}, + version="latest", ) diff --git a/sdk/python/feast/templates/couchbase/feature_repo/example_repo.py b/sdk/python/feast/templates/couchbase/feature_repo/feature_definitions.py similarity index 99% rename from sdk/python/feast/templates/couchbase/feature_repo/example_repo.py rename to sdk/python/feast/templates/couchbase/feature_repo/feature_definitions.py index 363ba3c4664..802f251ca5a 100644 --- a/sdk/python/feast/templates/couchbase/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/couchbase/feature_repo/feature_definitions.py @@ -47,6 +47,7 @@ # Tags are user defined key/value pairs that are attached to each # feature view tags={"team": "driver_performance"}, + version="latest", ) # Define a request data source which encodes features / information only @@ -109,6 +110,7 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: online=True, source=driver_stats_push_source, # Changed from above tags={"team": "driver_performance"}, + version="latest", ) diff --git a/sdk/python/feast/templates/gcp/README.md b/sdk/python/feast/templates/gcp/README.md index bc9e51769c9..7f28fdbfa2f 100644 --- a/sdk/python/feast/templates/gcp/README.md +++ b/sdk/python/feast/templates/gcp/README.md @@ -2,7 +2,7 @@ A quick view of what's in this repository: * `data/` contains raw demo parquet data -* `example_repo.py` contains demo feature definitions +* `feature_definitions.py` contains demo feature definitions * `feature_store.yaml` contains a demo setup configuring where data sources are * `test_workflow.py` showcases how to run all key Feast commands, including defining, retrieving, and pushing features. diff --git a/sdk/python/feast/templates/gcp/feature_repo/example_repo.py b/sdk/python/feast/templates/gcp/feature_repo/feature_definitions.py similarity index 99% rename from sdk/python/feast/templates/gcp/feature_repo/example_repo.py rename to sdk/python/feast/templates/gcp/feature_repo/feature_definitions.py index 81e06c72018..5eda02ea5d2 100644 --- a/sdk/python/feast/templates/gcp/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/gcp/feature_repo/feature_definitions.py @@ -61,6 +61,7 @@ # Tags are user defined key/value pairs that are attached to each # feature view tags={"team": "driver_performance"}, + version="latest", ) # Define a request data source which encodes features / information only @@ -123,6 +124,7 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: online=True, source=driver_stats_push_source, # Changed from above tags={"team": "driver_performance"}, + version="latest", ) diff --git a/sdk/python/feast/templates/hazelcast/bootstrap.py b/sdk/python/feast/templates/hazelcast/bootstrap.py index 7a2b49d2493..9db778c2bd0 100644 --- a/sdk/python/feast/templates/hazelcast/bootstrap.py +++ b/sdk/python/feast/templates/hazelcast/bootstrap.py @@ -163,8 +163,8 @@ def bootstrap(): driver_stats_path = data_path / "driver_stats.parquet" driver_df.to_parquet(path=str(driver_stats_path), allow_truncated_timestamps=True) - # example_repo.py - example_py_file = repo_path / "example_repo.py" + # feature_definitions.py + example_py_file = repo_path / "feature_definitions.py" replace_str_in_file( example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) ) diff --git a/sdk/python/feast/templates/hbase/feature_repo/example_repo.py b/sdk/python/feast/templates/hazelcast/feature_repo/feature_definitions.py similarity index 99% rename from sdk/python/feast/templates/hbase/feature_repo/example_repo.py rename to sdk/python/feast/templates/hazelcast/feature_repo/feature_definitions.py index 131f1bcaa61..21b2985409e 100644 --- a/sdk/python/feast/templates/hbase/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/hazelcast/feature_repo/feature_definitions.py @@ -52,6 +52,7 @@ # Tags are user defined key/value pairs that are attached to each # feature view tags={"team": "driver_performance"}, + version="latest", ) # Define a request data source which encodes features / information only @@ -114,6 +115,7 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: online=True, source=driver_stats_push_source, # Changed from above tags={"team": "driver_performance"}, + version="latest", ) diff --git a/sdk/python/feast/templates/hbase/bootstrap.py b/sdk/python/feast/templates/hbase/bootstrap.py index 94be8e441da..e8f59ee2caf 100644 --- a/sdk/python/feast/templates/hbase/bootstrap.py +++ b/sdk/python/feast/templates/hbase/bootstrap.py @@ -22,7 +22,7 @@ def bootstrap(): driver_stats_path = data_path / "driver_stats.parquet" driver_df.to_parquet(path=str(driver_stats_path), allow_truncated_timestamps=True) - example_py_file = repo_path / "example_repo.py" + example_py_file = repo_path / "feature_definitions.py" replace_str_in_file( example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) ) diff --git a/sdk/python/feast/templates/cassandra/feature_repo/example_repo.py b/sdk/python/feast/templates/hbase/feature_repo/feature_definitions.py similarity index 99% rename from sdk/python/feast/templates/cassandra/feature_repo/example_repo.py rename to sdk/python/feast/templates/hbase/feature_repo/feature_definitions.py index 131f1bcaa61..21b2985409e 100644 --- a/sdk/python/feast/templates/cassandra/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/hbase/feature_repo/feature_definitions.py @@ -52,6 +52,7 @@ # Tags are user defined key/value pairs that are attached to each # feature view tags={"team": "driver_performance"}, + version="latest", ) # Define a request data source which encodes features / information only @@ -114,6 +115,7 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: online=True, source=driver_stats_push_source, # Changed from above tags={"team": "driver_performance"}, + version="latest", ) diff --git a/sdk/python/feast/templates/local/README.md b/sdk/python/feast/templates/local/README.md index 1e617cc442f..0f223bc9850 100644 --- a/sdk/python/feast/templates/local/README.md +++ b/sdk/python/feast/templates/local/README.md @@ -3,7 +3,7 @@ If you haven't already, check out the quickstart guide on Feast's website (http: uses this repo. A quick view of what's in this repository's `feature_repo/` directory: * `data/` contains raw demo parquet data -* `feature_repo/example_repo.py` contains demo feature definitions +* `feature_repo/feature_definitions.py` contains demo feature definitions * `feature_repo/feature_store.yaml` contains a demo setup configuring where data sources are * `feature_repo/test_workflow.py` showcases how to run all key Feast commands, including defining, retrieving, and pushing features. diff --git a/sdk/python/feast/templates/local/bootstrap.py b/sdk/python/feast/templates/local/bootstrap.py index 9f6a5a6c969..1732bb1ef78 100644 --- a/sdk/python/feast/templates/local/bootstrap.py +++ b/sdk/python/feast/templates/local/bootstrap.py @@ -7,6 +7,9 @@ def bootstrap(): import pathlib from datetime import datetime, timedelta + import pyarrow as pa + import pyarrow.parquet as pq + from feast.driver_test_data import create_driver_hourly_stats_df repo_path = pathlib.Path(__file__).parent.absolute() / "feature_repo" @@ -23,7 +26,26 @@ def bootstrap(): driver_stats_path = data_path / "driver_stats.parquet" driver_df.to_parquet(path=str(driver_stats_path), allow_truncated_timestamps=True) - example_py_file = repo_path / "example_repo.py" + # Create an empty parquet file for the label view batch source + # Use explicit pyarrow schema to ensure proper column types even with zero rows + label_schema = pa.schema( + [ + ("driver_id", pa.int64()), + ("is_reliable", pa.int64()), + ("quality_score", pa.float32()), + ("reviewer_notes", pa.string()), + ("labeler", pa.string()), + ("event_timestamp", pa.timestamp("ns")), + ] + ) + label_table = pa.table( + {field.name: pa.array([], type=field.type) for field in label_schema}, + schema=label_schema, + ) + label_data_path = data_path / "driver_quality_labels.parquet" + pq.write_table(label_table, str(label_data_path)) + + example_py_file = repo_path / "feature_definitions.py" replace_str_in_file(example_py_file, "%PROJECT_NAME%", str(project_name)) replace_str_in_file( example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) @@ -31,6 +53,11 @@ def bootstrap(): replace_str_in_file( example_py_file, "%LOGGING_PATH%", str(data_path.relative_to(repo_path)) ) + replace_str_in_file( + example_py_file, + "%LABEL_DATA_PATH%", + str(label_data_path.relative_to(repo_path)), + ) if __name__ == "__main__": diff --git a/sdk/python/feast/templates/local/feature_repo/feature_definitions.py b/sdk/python/feast/templates/local/feature_repo/feature_definitions.py new file mode 100644 index 00000000000..9851b454316 --- /dev/null +++ b/sdk/python/feast/templates/local/feature_repo/feature_definitions.py @@ -0,0 +1,207 @@ +# This is an example feature definition file + +from datetime import timedelta + +import pandas as pd + +from feast import ( + ConflictPolicy, + Entity, + FeatureService, + FeatureView, + Field, + FileSource, + LabelView, + Project, + PushSource, + RequestSource, +) +from feast.feature_logging import LoggingConfig +from feast.infra.offline_stores.file_source import FileLoggingDestination +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Float32, Float64, Int64, Json, Map, String, Struct + +# Define a project for the feature repo +project = Project(name="%PROJECT_NAME%", description="A project for driver statistics") + +# Define an entity for the driver. You can think of an entity as a primary key used to +# fetch features. +driver = Entity(name="driver", join_keys=["driver_id"]) + +# Read data from parquet files. Parquet is convenient for local development mode. For +# production, you can use your favorite DWH, such as BigQuery. See Feast documentation +# for more info. +driver_stats_source = FileSource( + name="driver_hourly_stats_source", + path="%PARQUET_PATH%", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +# Our parquet files contain sample data that includes a driver_id column, timestamps and +# three feature column. Here we define a Feature View that will allow us to serve this +# data to our model online. +driver_stats_fv = FeatureView( + # The unique name of this feature view. Two feature views in a single + # project cannot have the same name + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=1), + # The list of features defined below act as a schema to both define features + # for both materialization of features into a store, and are used as references + # during retrieval for building a training dataset or serving features + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64, description="Average daily trips"), + Field( + name="driver_metadata", + dtype=Map, + description="Driver metadata as key-value pairs", + ), + Field( + name="driver_config", dtype=Json, description="Driver configuration as JSON" + ), + Field( + name="driver_profile", + dtype=Struct({"name": String, "age": String}), + description="Driver profile as a typed struct", + ), + ], + online=True, + source=driver_stats_source, + # Tags are user defined key/value pairs that are attached to each + # feature view + tags={"team": "driver_performance"}, + enable_validation=True, + version="latest", +) + +# Define a request data source which encodes features / information only +# available at request time (e.g. part of the user initiated HTTP request) +input_request = RequestSource( + name="vals_to_add", + schema=[ + Field(name="val_to_add", dtype=Int64), + Field(name="val_to_add_2", dtype=Int64), + ], +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fv, input_request], + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +# This groups features into a model version +driver_activity_v1 = FeatureService( + name="driver_activity_v1", + features=[ + driver_stats_fv[["conv_rate"]], # Sub-selects a feature from a feature view + transformed_conv_rate, # Selects all features from the feature view + ], + logging_config=LoggingConfig( + destination=FileLoggingDestination(path="%LOGGING_PATH%") + ), +) +driver_activity_v2 = FeatureService( + name="driver_activity_v2", features=[driver_stats_fv, transformed_conv_rate] +) + +# Defines a way to push data (to be available offline, online or both) into Feast. +driver_stats_push_source = PushSource( + name="driver_stats_push_source", + batch_source=driver_stats_source, +) + +# Defines a slightly modified version of the feature view from above, where the source +# has been changed to the push source. This allows fresh features to be directly pushed +# to the online store for this feature view. +driver_stats_fresh_fv = FeatureView( + name="driver_hourly_stats_fresh", + entities=[driver], + ttl=timedelta(days=1), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + Field(name="driver_metadata", dtype=Map), + Field(name="driver_config", dtype=Json), + Field(name="driver_profile", dtype=Struct({"name": String, "age": String})), + ], + online=True, + source=driver_stats_push_source, # Changed from above + tags={"team": "driver_performance"}, + version="latest", +) + + +# Define an on demand feature view which can generate new features based on +# existing feature views and RequestSource features +@on_demand_feature_view( + sources=[driver_stats_fresh_fv, input_request], # relies on fresh version of FV + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], +) +def transformed_conv_rate_fresh(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = inputs["conv_rate"] + inputs["val_to_add"] + df["conv_rate_plus_val2"] = inputs["conv_rate"] + inputs["val_to_add_2"] + return df + + +driver_activity_v3 = FeatureService( + name="driver_activity_v3", + features=[driver_stats_fresh_fv, transformed_conv_rate_fresh], +) + +# --- Label Views --- +# Label views manage mutable human labels for training data, RLHF, and evaluation. +# They use PushSources so labels can be submitted from the UI or external tools. + +driver_quality_labels_source = PushSource( + name="driver_quality_labels_push", + batch_source=FileSource( + name="driver_quality_labels_batch", + path="%LABEL_DATA_PATH%", + timestamp_field="event_timestamp", + ), +) + +driver_quality_labels = LabelView( + name="driver_quality_labels", + entities=[driver], + schema=[ + Field(name="is_reliable", dtype=Int64), + Field(name="quality_score", dtype=Float32), + Field(name="reviewer_notes", dtype=String), + Field(name="labeler", dtype=String), + ], + source=driver_quality_labels_source, + labeler_field="labeler", + conflict_policy=ConflictPolicy.LAST_WRITE_WINS, + description="Human quality labels for drivers - used for model training and evaluation", + tags={ + "feast.io/labeling-method": "table", + "feast.io/field-role:is_reliable": "label", + "feast.io/field-role:quality_score": "label", + "feast.io/field-role:reviewer_notes": "metadata", + "feast.io/label-values:is_reliable": "1 0", + "feast.io/label-widget:is_reliable": "binary", + "feast.io/label-widget:quality_score": "number", + "feast.io/label-widget:reviewer_notes": "text", + }, +) diff --git a/sdk/python/feast/templates/local/feature_repo/test_workflow.py b/sdk/python/feast/templates/local/feature_repo/test_workflow.py index eebeb113115..82175972321 100644 --- a/sdk/python/feast/templates/local/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/local/feature_repo/test_workflow.py @@ -1,3 +1,4 @@ +import json import subprocess from datetime import datetime @@ -45,6 +46,11 @@ def run_demo(): "conv_rate": [1.0], "acc_rate": [1.0], "avg_daily_trips": [1000], + "driver_metadata": [{"vehicle_type": "truck", "rating": "5.0"}], + "driver_config": [ + json.dumps({"max_distance_km": 500, "preferred_zones": ["north"]}) + ], + "driver_profile": [{"name": "driver_1001_updated", "age": "30"}], } ) print(event_df) @@ -115,6 +121,9 @@ def fetch_online_features(store, source: str = ""): else: features_to_fetch = [ "driver_hourly_stats:acc_rate", + "driver_hourly_stats:driver_metadata", + "driver_hourly_stats:driver_config", + "driver_hourly_stats:driver_profile", "transformed_conv_rate:conv_rate_plus_val1", "transformed_conv_rate:conv_rate_plus_val2", ] diff --git a/sdk/python/feast/templates/milvus/README.md b/sdk/python/feast/templates/milvus/README.md index 1e617cc442f..0f223bc9850 100644 --- a/sdk/python/feast/templates/milvus/README.md +++ b/sdk/python/feast/templates/milvus/README.md @@ -3,7 +3,7 @@ If you haven't already, check out the quickstart guide on Feast's website (http: uses this repo. A quick view of what's in this repository's `feature_repo/` directory: * `data/` contains raw demo parquet data -* `feature_repo/example_repo.py` contains demo feature definitions +* `feature_repo/feature_definitions.py` contains demo feature definitions * `feature_repo/feature_store.yaml` contains a demo setup configuring where data sources are * `feature_repo/test_workflow.py` showcases how to run all key Feast commands, including defining, retrieving, and pushing features. diff --git a/sdk/python/feast/templates/milvus/bootstrap.py b/sdk/python/feast/templates/milvus/bootstrap.py index 9f6a5a6c969..bd180ade01e 100644 --- a/sdk/python/feast/templates/milvus/bootstrap.py +++ b/sdk/python/feast/templates/milvus/bootstrap.py @@ -23,7 +23,7 @@ def bootstrap(): driver_stats_path = data_path / "driver_stats.parquet" driver_df.to_parquet(path=str(driver_stats_path), allow_truncated_timestamps=True) - example_py_file = repo_path / "example_repo.py" + example_py_file = repo_path / "feature_definitions.py" replace_str_in_file(example_py_file, "%PROJECT_NAME%", str(project_name)) replace_str_in_file( example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) diff --git a/sdk/python/feast/templates/milvus/feature_repo/__init__.py b/sdk/python/feast/templates/milvus/feature_repo/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/feast/templates/local/feature_repo/example_repo.py b/sdk/python/feast/templates/milvus/feature_repo/feature_definitions.py similarity index 99% rename from sdk/python/feast/templates/local/feature_repo/example_repo.py rename to sdk/python/feast/templates/milvus/feature_repo/feature_definitions.py index e2fd0a891cf..31f3af3c26d 100644 --- a/sdk/python/feast/templates/local/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/milvus/feature_repo/feature_definitions.py @@ -58,6 +58,7 @@ # Tags are user defined key/value pairs that are attached to each # feature view tags={"team": "driver_performance"}, + version="latest", ) # Define a request data source which encodes features / information only @@ -123,6 +124,7 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: online=True, source=driver_stats_push_source, # Changed from above tags={"team": "driver_performance"}, + version="latest", ) diff --git a/sdk/python/feast/templates/milvus/feature_repo/feature_store.yaml b/sdk/python/feast/templates/milvus/feature_repo/feature_store.yaml new file mode 100644 index 00000000000..df884a3fddb --- /dev/null +++ b/sdk/python/feast/templates/milvus/feature_repo/feature_store.yaml @@ -0,0 +1,13 @@ +project: my_project +# By default, the registry is a file (but can be turned into a more scalable SQL-backed registry) +registry: data/registry.db +# The provider primarily specifies default offline / online stores & storing the registry in a given cloud +provider: local +online_store: + type: milvus + path: data/online_store.db + vector_enabled: true +entity_key_serialization_version: 3 +# By default, no_auth for authentication and authorization, other possible values kubernetes and oidc. Refer the documentation for more details. +auth: + type: no_auth \ No newline at end of file diff --git a/sdk/python/feast/templates/milvus/feature_repo/test_workflow.py b/sdk/python/feast/templates/milvus/feature_repo/test_workflow.py new file mode 100644 index 00000000000..eebeb113115 --- /dev/null +++ b/sdk/python/feast/templates/milvus/feature_repo/test_workflow.py @@ -0,0 +1,130 @@ +import subprocess +from datetime import datetime + +import pandas as pd + +from feast import FeatureStore +from feast.data_source import PushMode + + +def run_demo(): + store = FeatureStore(repo_path=".") + print("\n--- Run feast apply ---") + subprocess.run(["feast", "apply"]) + + print("\n--- Historical features for training ---") + fetch_historical_features_entity_df(store, for_batch_scoring=False) + + print("\n--- Historical features for batch scoring ---") + fetch_historical_features_entity_df(store, for_batch_scoring=True) + + print("\n--- Load features into online store ---") + store.materialize_incremental(end_date=datetime.now()) + + print("\n--- Online features ---") + fetch_online_features(store) + + print("\n--- Online features retrieved (instead) through a feature service---") + fetch_online_features(store, source="feature_service") + + print( + "\n--- Online features retrieved (using feature service v3, which uses a feature view with a push source---" + ) + fetch_online_features(store, source="push") + + print("\n--- Simulate a stream event ingestion of the hourly stats df ---") + event_df = pd.DataFrame.from_dict( + { + "driver_id": [1001], + "event_timestamp": [ + datetime.now(), + ], + "created": [ + datetime.now(), + ], + "conv_rate": [1.0], + "acc_rate": [1.0], + "avg_daily_trips": [1000], + } + ) + print(event_df) + store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) + + print("\n--- Online features again with updated values from a stream push---") + fetch_online_features(store, source="push") + + print("\n--- Run feast teardown ---") + subprocess.run(["feast", "teardown"]) + + +def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): + # Note: see https://docs.feast.dev/getting-started/concepts/feature-retrieval for more details on how to retrieve + # for all entities in the offline store instead + entity_df = pd.DataFrame.from_dict( + { + # entity's join key -> entity values + "driver_id": [1001, 1002, 1003], + # "event_timestamp" (reserved key) -> timestamps + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + ], + # (optional) label name -> label values. Feast does not process these + "label_driver_reported_satisfaction": [1, 5, 3], + # values we're using for an on-demand transformation + "val_to_add": [1, 2, 3], + "val_to_add_2": [10, 20, 30], + } + ) + # For batch scoring, we want the latest timestamps + if for_batch_scoring: + entity_df["event_timestamp"] = pd.to_datetime("now", utc=True) + + training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ], + ).to_df() + print(training_df.head()) + + +def fetch_online_features(store, source: str = ""): + entity_rows = [ + # {join_key: entity_value} + { + "driver_id": 1001, + "val_to_add": 1000, + "val_to_add_2": 2000, + }, + { + "driver_id": 1002, + "val_to_add": 1001, + "val_to_add_2": 2002, + }, + ] + if source == "feature_service": + features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") + else: + features_to_fetch = [ + "driver_hourly_stats:acc_rate", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ] + returned_features = store.get_online_features( + features=features_to_fetch, + entity_rows=entity_rows, + ).to_dict() + for key, value in sorted(returned_features.items()): + print(key, " : ", value) + + +if __name__ == "__main__": + run_demo() diff --git a/sdk/python/feast/templates/postgres/bootstrap.py b/sdk/python/feast/templates/postgres/bootstrap.py index 6ed13e4e39a..37daacb0df0 100644 --- a/sdk/python/feast/templates/postgres/bootstrap.py +++ b/sdk/python/feast/templates/postgres/bootstrap.py @@ -29,11 +29,25 @@ def bootstrap(): postgres_schema = click.prompt("Postgres schema", default="public") postgres_user = click.prompt("Postgres user") postgres_password = click.prompt("Postgres password", hide_input=True) + postgres_sslmode = click.prompt( + "Postgres sslmode (disable, allow, prefer, require, verify-ca, verify-full)", + default="require", + ) if click.confirm( 'Should I upload example data to Postgres (overwriting "feast_driver_hourly_stats" table)?', default=True, ): + config = PostgreSQLConfig( + host=postgres_host, + port=int(postgres_port), + database=postgres_database, + db_schema=postgres_schema, + user=postgres_user, + password=postgres_password, + sslmode=postgres_sslmode, + ) + db_connection = psycopg.connect( conninfo=( f"postgresql://{postgres_user}" @@ -42,6 +56,7 @@ def bootstrap(): f":{int(postgres_port)}" f"/{postgres_database}" ), + sslmode=postgres_sslmode, options=f"-c search_path={postgres_schema}", ) @@ -49,14 +64,7 @@ def bootstrap(): cur.execute('DROP TABLE IF EXISTS "feast_driver_hourly_stats"') df_to_postgres_table( - config=PostgreSQLConfig( - host=postgres_host, - port=int(postgres_port), - database=postgres_database, - db_schema=postgres_schema, - user=postgres_user, - password=postgres_password, - ), + config=config, df=driver_df, table_name="feast_driver_hourly_stats", ) @@ -67,6 +75,7 @@ def bootstrap(): replace_str_in_file(config_file, "DB_SCHEMA", postgres_schema) replace_str_in_file(config_file, "DB_USERNAME", postgres_user) replace_str_in_file(config_file, "DB_PASSWORD", postgres_password) + replace_str_in_file(config_file, "DB_SSLMODE", postgres_sslmode) if __name__ == "__main__": diff --git a/sdk/python/feast/templates/postgres/feature_repo/example_repo.py b/sdk/python/feast/templates/postgres/feature_repo/feature_definitions.py similarity index 99% rename from sdk/python/feast/templates/postgres/feature_repo/example_repo.py rename to sdk/python/feast/templates/postgres/feature_repo/feature_definitions.py index 0d1783e1e5e..073f18e43f5 100644 --- a/sdk/python/feast/templates/postgres/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/postgres/feature_repo/feature_definitions.py @@ -44,6 +44,7 @@ # Tags are user defined key/value pairs that are attached to each # feature view tags={"team": "driver_performance"}, + version="latest", ) # Define a request data source which encodes features / information only @@ -106,6 +107,7 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: online=True, source=driver_stats_push_source, # Changed from above tags={"team": "driver_performance"}, + version="latest", ) diff --git a/sdk/python/feast/templates/postgres/feature_repo/feature_store.yaml b/sdk/python/feast/templates/postgres/feature_repo/feature_store.yaml index 0663ff0ad97..ed80a5ff3e9 100644 --- a/sdk/python/feast/templates/postgres/feature_repo/feature_store.yaml +++ b/sdk/python/feast/templates/postgres/feature_repo/feature_store.yaml @@ -2,7 +2,7 @@ project: my_project provider: local registry: registry_type: sql - path: postgresql://postgres:mysecretpassword@127.0.0.1:55001/feast + path: postgresql://DB_USERNAME:DB_PASSWORD@DB_HOST:DB_PORT/DB_NAME?sslmode=DB_SSLMODE cache_ttl_seconds: 60 sqlalchemy_config_kwargs: echo: false @@ -15,6 +15,7 @@ online_store: db_schema: DB_SCHEMA user: DB_USERNAME password: DB_PASSWORD + sslmode: DB_SSLMODE offline_store: type: postgres host: DB_HOST @@ -23,4 +24,5 @@ offline_store: db_schema: DB_SCHEMA user: DB_USERNAME password: DB_PASSWORD + sslmode: DB_SSLMODE entity_key_serialization_version: 3 diff --git a/sdk/python/feast/templates/pytorch_nlp/README.md b/sdk/python/feast/templates/pytorch_nlp/README.md new file mode 100644 index 00000000000..5e26d264d4b --- /dev/null +++ b/sdk/python/feast/templates/pytorch_nlp/README.md @@ -0,0 +1,631 @@ +# PyTorch NLP Sentiment Analysis with Feast + +This template demonstrates how to build a complete sentiment analysis pipeline using **Feast** (Feature Store) with **PyTorch** and **Hugging Face Transformers**. It showcases modern MLOps practices for NLP including feature engineering, model serving, and real-time inference. + +## 🎯 What You'll Learn + +- **Feast Fundamentals**: Feature stores, entities, feature views, and services +- **NLP Feature Engineering**: Text preprocessing and feature extraction patterns +- **PyTorch Integration**: Using pre-trained Hugging Face models with Feast +- **Real-time Serving**: Online feature serving for production inference +- **MLOps Patterns**: Model versioning, performance monitoring, and data governance + +## 🚀 Quick Start + +### Prerequisites + +- Python 3.8+ +- pip or conda for package management + +### 1. Initialize the Project + +```bash +feast init my-sentiment-project -t pytorch_nlp +cd my-sentiment-project +``` + +### 2. Install Dependencies + +```bash +# Install Feast with NLP support (includes PyTorch, transformers, and ML utilities) +pip install feast[nlp] +``` + +### 3. Apply and Materialize Features + +```bash +cd feature_repo +feast apply +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +### 4. Start Feature Server + +```bash +feast serve --host 0.0.0.0 --port 6566 +``` + +### 5. Test with Python (Optional) + +```bash +python test_workflow.py +``` + +## 📊 What's Included + +### Sample Dataset +- **1000 synthetic text samples** with sentiment labels (positive/negative/neutral) +- **Engineered features**: text length, word count, emoji count, etc. +- **User context**: aggregated user statistics and behavior patterns +- **Dynamic timestamps** generated within the past 30 days for realistic demo experience + +### Feature Engineering Pipeline +- **Text Features**: Content, metadata, and linguistic characteristics +- **User Features**: Historical sentiment patterns and engagement metrics +- **Real-time Features**: On-demand sentiment prediction using pre-trained models + +### Model Integration +- **Pre-trained Models**: CardiffNLP Twitter-RoBERTa for sentiment analysis +- **Embedding Generation**: Text vectorization for similarity and clustering +- **Confidence Scoring**: Prediction confidence and probability distributions + +## 🌐 HTTP Feature Server + +Once you've started the feature server with `feast serve`, you can query features via HTTP API: + +### Basic Materialized Features + +Query stored text and user features: + +```bash +curl -X POST \ + "http://localhost:6566/get-online-features" \ + -H "Content-Type: application/json" \ + -d '{ + "features": [ + "text_features:text_content", + "text_features:sentiment_label", + "user_stats:user_avg_sentiment" + ], + "entities": { + "text_id": ["text_0000", "text_0001"], + "user_id": ["user_080", "user_091"] + } + }' +``` + +**Example Response:** +```json +{ + "metadata": {"feature_names": ["text_id","user_id","sentiment_label","text_content","user_avg_sentiment"]}, + "results": [ + {"values": ["text_0000"], "statuses": ["PRESENT"]}, + {"values": ["user_080"], "statuses": ["PRESENT"]}, + {"values": ["positive"], "statuses": ["PRESENT"]}, + {"values": ["Having an amazing day at the beach with friends!"], "statuses": ["PRESENT"]}, + {"values": [0.905], "statuses": ["PRESENT"]} + ] +} +``` + +### On-Demand Sentiment Predictions + +Get real-time sentiment analysis: + +```bash +curl -X POST \ + "http://localhost:6566/get-online-features" \ + -H "Content-Type: application/json" \ + -d '{ + "features": [ + "sentiment_prediction:predicted_sentiment", + "sentiment_prediction:sentiment_confidence", + "sentiment_prediction:positive_prob" + ], + "entities": { + "input_text": ["I love this amazing product!", "This service is terrible"], + "model_name": ["cardiffnlp/twitter-roberta-base-sentiment-latest", "cardiffnlp/twitter-roberta-base-sentiment-latest"] + } + }' +``` + +### Feature Service (Complete Feature Set) + +Query using predefined feature service: + +```bash +curl -X POST \ + "http://localhost:6566/get-online-features" \ + -H "Content-Type: application/json" \ + -d '{ + "feature_service": "sentiment_analysis_v2", + "entities": { + "text_id": ["text_0000"], + "user_id": ["user_080"], + "input_text": ["This is an amazing experience!"], + "model_name": ["cardiffnlp/twitter-roberta-base-sentiment-latest"] + } + }' +``` + +**Note**: Use actual entity combinations from your generated data. Run `head data/sentiment_data.parquet` to see available `text_id` and `user_id` values. + +## 🏗️ Project Structure + +``` +my-sentiment-project/ +├── README.md # This file +└── feature_repo/ + ├── feature_store.yaml # Feast configuration + ├── example_repo.py # Feature definitions (uses pre-loaded artifacts) + ├── static_artifacts.py # Static artifacts loading (models, lookup tables) + ├── test_workflow.py # Complete demo workflow + └── data/ # Generated sample data + └── sentiment_data.parquet +``` + +## 🔧 Key Components + +### Entities +- **`text`**: Unique identifier for text samples +- **`user`**: User who created the content + +### Feature Views +- **`text_features`**: Raw text content and engineered features +- **`user_stats`**: User-level aggregated statistics and behavior + +### On-Demand Features +- **`sentiment_prediction`**: Real-time sentiment analysis using PyTorch models +- **Features**: predicted sentiment, confidence scores, probability distributions, embeddings + +### Feature Services +- **`sentiment_analysis_v1`**: Basic sentiment features for simple models +- **`sentiment_analysis_v2`**: Advanced features with user context +- **`sentiment_training_features`**: Historical features for model training + +## ⚙️ Configuration + +This template is configured for **local development** using SQLite - no external dependencies required! + +### Current Configuration (`feature_store.yaml`) + +```yaml +project: my_project +provider: local # Local provider (no cloud) +registry: data/registry.db # SQLite registry +online_store: + type: sqlite # SQLite online store (NOT Redis) + path: data/online_store.db # Local SQLite file +offline_store: + type: file # Local file-based offline store +``` + +### Why SQLite? +- ✅ **Zero setup** - Works immediately after `feast init` +- ✅ **Self-contained** - All data in local files +- ✅ **No external services** - No Redis/cloud required +- ✅ **Perfect for demos** - Easy to share and understand + +## 🚀 Static Artifacts Loading + +This template demonstrates **static artifacts loading** - a performance optimization that loads models, lookup tables, and other artifacts once at feature server startup instead of on each request. + +### What are Static Artifacts? + +Static artifacts are pre-loaded resources that remain constant during server operation: +- **Small ML models** (sentiment analysis, classification, small neural networks) +- **Lookup tables and mappings** (label encoders, category mappings) +- **Configuration data** (model parameters, feature mappings) +- **Pre-computed embeddings** (user embeddings, item features) + +### Performance Benefits + +**Before (Per-Request Loading):** +```python +def sentiment_prediction(inputs): + # ❌ Model loads on every request - slow! + model = pipeline("sentiment-analysis", model="...") + return model(inputs["text"]) +``` + +**After (Startup Loading):** +```python +# ✅ Model loads once at server startup +def sentiment_prediction(inputs): + global _sentiment_model # Pre-loaded model + return _sentiment_model(inputs["text"]) +``` + +**Performance Impact:** +- 🚀 **10-100x faster** inference (no model loading overhead) +- 💾 **Lower memory usage** (shared model across requests) +- ⚡ **Better scalability** (consistent response times) + +### How It Works + +1. **Startup**: Feast server loads `static_artifacts.py` during initialization +2. **Loading**: `load_artifacts(app)` function stores models in `app.state` +3. **Access**: On-demand feature views access pre-loaded artifacts via global references + +```python +# static_artifacts.py - Define what to load +def load_artifacts(app: FastAPI): + app.state.sentiment_model = load_sentiment_model() + app.state.lookup_tables = load_lookup_tables() + + # Update global references for easy access + import example_repo + + example_repo._sentiment_model = app.state.sentiment_model + example_repo._lookup_tables = app.state.lookup_tables + + +# example_repo.py - Use pre-loaded artifacts +_sentiment_model = None # Set by static_artifacts.py + + +def sentiment_prediction(inputs): + global _sentiment_model + if _sentiment_model is not None: + return _sentiment_model(inputs["text"]) + else: + return fallback_predictions() +``` + +### Scope and Limitations + +**✅ Great for:** +- Small to medium models (< 1GB) +- Fast-loading models (sentiment analysis, classification) +- Lookup tables and reference data +- Configuration parameters +- Pre-computed embeddings + +**❌ Not recommended for:** +- **Large Language Models (LLMs)** - Use dedicated serving solutions like vLLM, TGI, or TensorRT-LLM +- Models requiring GPU clusters +- Frequently updated models +- Models with complex initialization dependencies + +**Note:** Feast is optimized for feature serving, not large model inference. For production LLM workloads, use specialized model serving platforms. + +### Customizing Static Artifacts + +To add your own artifacts, modify `static_artifacts.py`: + +```python +def load_custom_embeddings(): + """Load pre-computed user embeddings.""" + embeddings_file = Path(__file__).parent / "data" / "user_embeddings.npy" + if embeddings_file.exists(): + import numpy as np + + return {"embeddings": np.load(embeddings_file)} + return None + + +def load_artifacts(app: FastAPI): + # Load your custom artifacts + app.state.custom_embeddings = load_custom_embeddings() + app.state.config_params = {"threshold": 0.7, "top_k": 10} + + # Make them available to feature views + import example_repo + + example_repo._custom_embeddings = app.state.custom_embeddings +``` + +## 📚 Detailed Usage + +### 1. Feature Store Setup + +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") +``` + +### 2. Training Data Retrieval + +```python +# Get historical features for model training +from datetime import datetime +import pandas as pd + +entity_df = pd.DataFrame( + { + "text_id": ["text_0000", "text_0001", "text_0002"], + "user_id": [ + "user_080", + "user_091", + "user_052", + ], # Use actual generated user IDs + "event_timestamp": [ + datetime.now(), + datetime.now(), + datetime.now(), + ], # Current timestamps + } +) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "text_features:text_content", + "text_features:sentiment_label", + "text_features:text_length", + "user_stats:user_avg_sentiment", + ], +).to_df() + +print(f"Retrieved {len(training_df)} training samples") +print(training_df.head()) +``` + +### 3. Real-time Inference + +```python +# Get features for online serving (use actual entity combinations) +entity_rows = [ + {"text_id": "text_0000", "user_id": "user_080"}, + {"text_id": "text_0001", "user_id": "user_091"}, +] + +online_features = store.get_online_features( + features=store.get_feature_service("sentiment_analysis_v1"), + entity_rows=entity_rows, +).to_dict() + +print("Online features:", online_features) +``` + +### 4. On-Demand Sentiment Prediction + +```python +# Real-time sentiment analysis +prediction_rows = [ + { + "input_text": "I love this product!", + "model_name": "cardiffnlp/twitter-roberta-base-sentiment-latest", + } +] + +predictions = store.get_online_features( + features=[ + "sentiment_prediction:predicted_sentiment", + "sentiment_prediction:sentiment_confidence", + ], + entity_rows=prediction_rows, +).to_dict() +``` + +## 🚀 Complete End-to-End Demo + +Here's a step-by-step walkthrough of the entire template workflow: + +### 1. Initialize and Setup + +```bash +# Create new project +feast init my-sentiment-demo -t pytorch_nlp +cd my-sentiment-demo + +# Install dependencies +pip install torch>=2.0.0 transformers>=4.30.0 + +# Navigate to feature repository +cd feature_repo +``` + +### 2. Apply Feature Store Configuration + +```bash +# Register entities, feature views, and services +feast apply +``` + +**Expected Output:** +``` +Created entity text +Created entity user +Created feature view text_features +Created feature view user_stats +Created on demand feature view sentiment_prediction +Created feature service sentiment_analysis_v1 +Created feature service sentiment_analysis_v2 +``` + +### 3. Materialize Features + +```bash +# Load features into online store +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +**Expected Output:** +``` +Materializing 2 feature views to 2025-XX-XX XX:XX:XX+00:00 into the sqlite online store. +text_features: ████████████████████████████████████████ +user_stats: ████████████████████████████████████████ +``` + +### 4. Start Feature Server + +```bash +# Start HTTP feature server +feast serve --host 0.0.0.0 --port 6566 +``` + +**Expected Output:** +``` +Starting gunicorn 23.0.0 +Listening at: http://0.0.0.0:6566 +``` + +### 5. Query Features + +In a new terminal, test the feature server: + +```bash +# Check actual entity IDs in your data +python -c " +import pandas as pd +df = pd.read_parquet('data/sentiment_data.parquet') +print('Sample entities:', df.head()) +" + +# Test with actual entity combinations +curl -X POST \ + "http://localhost:6566/get-online-features" \ + -H "Content-Type: application/json" \ + -d '{ + "features": ["text_features:text_content", "text_features:sentiment_label"], + "entities": { + "text_id": ["text_0000"], + "user_id": ["user_XXX"] + } + }' | jq +``` + +## 🎮 Customization Examples + +### Adding New Features + + +```python +# In example_repo.py, add to text_features_fv schema: +Field(name="hashtag_count", dtype=Int64, description="Number of hashtags"), +Field(name="mention_count", dtype=Int64, description="Number of @mentions"), +Field(name="url_count", dtype=Int64, description="Number of URLs"), +``` + + +### Using Different Models + +```python +# In the sentiment_prediction function, change model: +model_name = "nlptown/bert-base-multilingual-uncased-sentiment" +# or +model_name = "distilbert-base-uncased-finetuned-sst-2-english" +``` + +### Adding Custom Transformations + +```python +@on_demand_feature_view( + sources=[text_input_request], + schema=[Field(name="toxicity_score", dtype=Float32)], +) +def toxicity_detection(inputs: pd.DataFrame) -> pd.DataFrame: + # Implement toxicity detection logic + pass +``` + +## 📈 Production Considerations + +### Scaling to Production + +1. **Cloud Deployment**: Use AWS, GCP, or Azure providers instead of local +2. **Vector Store**: Replace SQLite with Milvus for similarity search +3. **Model Serving**: Deploy models with KServe or other serving framework +4. **Monitoring**: Add feature drift detection and model performance tracking + +### Performance Optimization + +**Current Architecture:** +- ✅ **Static artifacts loading** at server startup (see `static_artifacts.py`) +- ✅ **Pre-loaded models** cached in memory for fast inference +- CPU-only operation to avoid multiprocessing issues +- SQLite-based storage for fast local access + +**Implemented Optimizations:** +- **Startup-time Model Loading**: ✅ Models load once at server startup via `static_artifacts.py` +- **Memory-efficient Caching**: ✅ Models stored in `app.state` and accessed via global references +- **Fallback Handling**: ✅ Graceful degradation when artifacts fail to load + +**Additional Production Optimizations:** +1. **Batch Inference**: Process multiple texts together for efficiency +2. **Feature Materialization**: Pre-compute expensive features offline +3. **Async Processing**: Use async patterns for real-time serving +4. **Model Serving Layer**: Use dedicated model servers (TorchServe, vLLM) for large models + +### Production Configuration Examples + +**Note**: The demo uses SQLite (above). These are examples for production deployment: + +```yaml +# feature_store.yaml for AWS production (requires Redis setup) +project: sentiment_analysis_prod +provider: aws +registry: s3://my-bucket/feast/registry.pb +online_store: + type: redis # Requires separate Redis server + connection_string: redis://my-redis-cluster:6379 +offline_store: + type: bigquery + project_id: my-gcp-project + +# feature_store.yaml for GCP production (requires cloud services) +project: sentiment_analysis_prod +provider: gcp +registry: gs://my-bucket/feast/registry.pb +online_store: + type: redis # Requires separate Redis server + connection_string: redis://my-redis-cluster:6379 +offline_store: + type: bigquery + project_id: my-gcp-project +``` + +## 🤝 Contributing + +This template is designed to be extended and customized: + +1. **Add new feature transformations** in `example_repo.py` +2. **Experiment with different models** in the `sentiment_prediction` function +3. **Extend the test workflow** with additional evaluation metrics +4. **Add new data sources** (Twitter API, product reviews, etc.) + +## 📖 Resources + +- [Feast Documentation](https://docs.feast.dev/) +- [Hugging Face Transformers](https://huggingface.co/docs/transformers/) +- [PyTorch Documentation](https://pytorch.org/docs/) + +## 🐛 Troubleshooting + +### Common Issues + +**ImportError: No module named 'transformers'** +```bash +pip install torch transformers +``` + +**Model download timeout** +```python +# Set environment variable for Hugging Face cache +export HF_HOME=/path/to/cache +``` + +**Feature store initialization fails** +```bash +# Reset the feature store +feast teardown +feast apply +``` + +**On-demand features return defaults** +- This is expected if PyTorch/transformers aren't installed +- The template includes fallback dummy predictions for demonstration + +### Getting Help + +- Check the [Feast GitHub Issues](https://github.com/feast-dev/feast/issues) +- Join the [Feast Slack Community](https://slack.feast.dev/) +- Review the [PyTorch Forums](https://discuss.pytorch.org/) + +--- + +**Happy Feature Engineering! 🎉** + +Built with ❤️ using Feast, PyTorch, and Hugging Face. diff --git a/sdk/python/feast/templates/pytorch_nlp/__init__.py b/sdk/python/feast/templates/pytorch_nlp/__init__.py new file mode 100644 index 00000000000..de76b0a8f66 --- /dev/null +++ b/sdk/python/feast/templates/pytorch_nlp/__init__.py @@ -0,0 +1 @@ +# Empty file to make this a Python package diff --git a/sdk/python/feast/templates/pytorch_nlp/bootstrap.py b/sdk/python/feast/templates/pytorch_nlp/bootstrap.py new file mode 100644 index 00000000000..6aad854747e --- /dev/null +++ b/sdk/python/feast/templates/pytorch_nlp/bootstrap.py @@ -0,0 +1,305 @@ +import pathlib +import random +from datetime import datetime, timedelta + +import pandas as pd +import pyarrow as pa +import pyarrow.parquet as pq + +from feast.file_utils import replace_str_in_file + + +def create_sentiment_data(num_samples: int = 1000) -> pd.DataFrame: + """Generate sentiment analysis dataset using BERTweet classifier predictions.""" + + # Diverse realistic text samples from various domains + sample_texts = [ + # Social media / tweets style + "Having an amazing day at the beach with friends!", + "Traffic is horrible today, going to be late for everything", + "Just finished my morning coffee, time to start work", + "This weather is perfect for a weekend getaway", + "Frustrated with this constant construction noise", + "The sunset tonight is absolutely breathtaking", + "Finally got tickets to the concert I wanted!", + "My phone battery died right when I needed it most", + "Loving the new album that just dropped today", + "Can't believe how long this line is taking", + # Product reviews / opinions + "This phone has incredible battery life and camera quality", + "The delivery was late and packaging was damaged", + "Pretty standard laptop, does what it's supposed to do", + "Amazing customer service, resolved my issue quickly", + "The quality is terrible for the price, disappointed", + "Works fine, good value for money, as described", + "Best purchase I've made this year, highly recommend", + "Returned this item, didn't work as advertised", + "Decent product but could be better for the cost", + "Exceeded my expectations, will buy again", + # General experiences + "Learning something new always makes me happy", + "Dealing with technical issues is draining my energy", + "The meeting went okay, covered the basic topics", + "Excited about the weekend plans with family", + "Another day of debugging code, the struggle continues", + "Really enjoying this book I started reading", + "The restaurant service was disappointing tonight", + "Nothing special planned, just a quiet evening", + "Great presentation today, audience was engaged", + "Feeling overwhelmed with all these deadlines", + # News / current events style + "The new policy changes will benefit small businesses", + "This decision could have negative environmental impact", + "The research findings are interesting but inconclusive", + "Economic indicators suggest stable growth ahead", + "Mixed reactions to the announcement yesterday", + "The data shows promising results across demographics", + "Public opinion remains divided on this issue", + "Significant improvements in the healthcare system", + "Concerns raised about the new regulations", + "Standard quarterly results meeting projections", + ] + + # Try to use BERTweet sentiment classifier, fallback to rule-based if not available + try: + from transformers import pipeline + + print(" 🤖 Loading BERTweet sentiment classifier...") + + # Use BERTweet model specifically trained for Twitter sentiment + sentiment_classifier = pipeline( + "sentiment-analysis", + model="finiteautomata/bertweet-base-sentiment-analysis", + return_all_scores=True, + ) + use_real_classifier = True + print(" ✅ BERTweet sentiment classifier loaded successfully") + + except ImportError: + print(" ⚠️ Transformers not available, using rule-based sentiment") + print(" 💡 For real classifier: pip install transformers torch") + use_real_classifier = False + except Exception as e: + print(f" ⚠️ Could not load BERTweet ({e}), using rule-based sentiment") + use_real_classifier = False + + # Generate data + data = [] + # Use current time and generate data within the last 30 days + now = datetime.now() + start_date = now - timedelta(days=30) + + # Extend sample texts by cycling through them to reach num_samples + all_texts = (sample_texts * (num_samples // len(sample_texts) + 1))[:num_samples] + + for i, base_text in enumerate(all_texts): + # Add some realistic variations to make texts more diverse + text = base_text + + # Occasionally add emphasis or emoji + if random.random() < 0.15: + text = text + "!" + elif random.random() < 0.1: + text = text + "..." + elif random.random() < 0.08: + if any( + word in text.lower() + for word in ["amazing", "love", "great", "best", "happy", "excited"] + ): + text = text + " 😊" + elif random.random() < 0.08: + if any( + word in text.lower() + for word in ["terrible", "disappointed", "frustrated", "horrible"] + ): + text = text + " 😞" + + # Get sentiment from real classifier or fallback + if use_real_classifier: + try: + predictions = sentiment_classifier(text)[0] + + # Find highest confidence prediction + best_pred = max(predictions, key=lambda x: x["score"]) + sentiment_label = best_pred[ + "label" + ].upper() # BERTweet returns 'POS', 'NEG', 'NEU' + sentiment_score = best_pred["score"] + + # Map BERTweet labels to our format + label_map = {"POS": "positive", "NEG": "negative", "NEU": "neutral"} + sentiment_label = label_map.get( + sentiment_label, sentiment_label.lower() + ) + + except Exception as e: + print(f" ⚠️ Classifier error for text {i}: {e}") + # Fallback to simple rule-based + sentiment_label, sentiment_score = _rule_based_sentiment(text) + else: + # Rule-based fallback + sentiment_label, sentiment_score = _rule_based_sentiment(text) + + # Generate engineered features + text_length = len(text) + word_count = len(text.split()) + exclamation_count = text.count("!") + caps_ratio = sum(1 for c in text if c.isupper()) / len(text) if text else 0 + emoji_count = sum(1 for c in text if ord(c) > 127) # Simple emoji detection + + # Random timestamp within the past 30 days + days_offset = random.randint(0, 30) + hours_offset = random.randint(0, 23) + minutes_offset = random.randint(0, 59) + event_timestamp = start_date + timedelta( + days=days_offset, hours=hours_offset, minutes=minutes_offset + ) + + data.append( + { + "text_id": f"text_{i:04d}", + "user_id": f"user_{random.randint(1, 100):03d}", + "text_content": text, + "sentiment_label": sentiment_label, + "sentiment_score": round(sentiment_score, 3), + "text_length": text_length, + "word_count": word_count, + "exclamation_count": exclamation_count, + "caps_ratio": round(caps_ratio, 3), + "emoji_count": emoji_count, + "event_timestamp": pd.Timestamp(event_timestamp, tz="UTC"), + "created": pd.Timestamp.now(tz="UTC").round("ms"), + } + ) + + df = pd.DataFrame(data) + + # Calculate user-level aggregations + user_stats = ( + df.groupby("user_id") + .agg({"sentiment_score": "mean", "text_id": "count", "text_length": "mean"}) + .rename( + columns={ + "sentiment_score": "user_avg_sentiment", + "text_id": "user_text_count", + "text_length": "user_avg_text_length", + } + ) + .round(3) + .reset_index() + ) + + # Merge user stats back to main dataframe + df = df.merge(user_stats, on="user_id", how="left") + + return df + + +def _rule_based_sentiment(text: str) -> tuple[str, float]: + """Fallback rule-based sentiment analysis when BERTweet is not available.""" + text_lower = text.lower() + + positive_words = [ + "amazing", + "love", + "great", + "excellent", + "wonderful", + "perfect", + "outstanding", + "fantastic", + "best", + "happy", + "good", + "awesome", + "incredible", + "beautiful", + "excited", + "enjoying", + ] + negative_words = [ + "terrible", + "horrible", + "awful", + "worst", + "bad", + "disappointed", + "frustrated", + "angry", + "sad", + "broken", + "failed", + "poor", + "draining", + "overwhelming", + "disappointing", + ] + + positive_count = sum(1 for word in positive_words if word in text_lower) + negative_count = sum(1 for word in negative_words if word in text_lower) + + if positive_count > negative_count: + return "positive", random.uniform(0.6, 0.9) + elif negative_count > positive_count: + return "negative", random.uniform(0.6, 0.9) + else: + return "neutral", random.uniform(0.5, 0.7) + + +def bootstrap(): + """Bootstrap the pytorch_nlp template with sample data.""" + repo_path = pathlib.Path(__file__).parent.absolute() / "feature_repo" + raw_project_name = pathlib.Path(__file__).parent.absolute().name + + # Sanitize project name for SQLite compatibility (no hyphens allowed) + project_name = raw_project_name.replace("-", "_") + if project_name != raw_project_name: + print(f" ℹ️ Project name sanitized: '{raw_project_name}' → '{project_name}'") + print(" 💡 SQLite table names cannot contain hyphens") + + data_path = repo_path / "data" + data_path.mkdir(exist_ok=True) + + print("🎭 Setting up sentiment analysis data for PyTorch NLP demonstration...") + + parquet_file = data_path / "sentiment_data.parquet" + + # Generate sentiment data + print(" 📝 Generating synthetic sentiment analysis dataset...") + df = create_sentiment_data(num_samples=1000) + + # Save to parquet + table = pa.Table.from_pandas(df) + pq.write_table(table, parquet_file) + + print(f" ✅ Created sentiment dataset with {len(df)} samples") + print(" 📊 Sentiment distribution:") + sentiment_counts = df["sentiment_label"].value_counts() + for sentiment, count in sentiment_counts.items(): + print(f" - {sentiment.capitalize()}: {count} samples") + + # Replace template placeholders + example_py_file = repo_path / "example_repo.py" + replace_str_in_file(example_py_file, "%PROJECT_NAME%", str(project_name)) + + test_workflow_file = repo_path / "test_workflow.py" + replace_str_in_file(test_workflow_file, "%PROJECT_NAME%", str(project_name)) + + print("🚀 PyTorch NLP template initialized successfully!") + + print("\n🎯 To get started:") + print(f" 1. cd {project_name}") + print(" 2. pip install -r requirements.txt") + print(" 3. cd feature_repo") + print(" 4. feast apply") + print(" 5. feast materialize") + print(" 6. python test_workflow.py") + print("\n💡 This template demonstrates:") + print(" - Text feature engineering with Feast") + print(" - PyTorch + Hugging Face transformers integration") + print(" - Sentiment analysis with pre-trained models") + print(" - Online and offline feature serving") + + +if __name__ == "__main__": + bootstrap() diff --git a/sdk/python/feast/templates/pytorch_nlp/feature_repo/__init__.py b/sdk/python/feast/templates/pytorch_nlp/feature_repo/__init__.py new file mode 100644 index 00000000000..5d81b19ce14 --- /dev/null +++ b/sdk/python/feast/templates/pytorch_nlp/feature_repo/__init__.py @@ -0,0 +1 @@ +# This file is auto-generated by the Feast project template diff --git a/sdk/python/feast/templates/pytorch_nlp/feature_repo/example_repo.py b/sdk/python/feast/templates/pytorch_nlp/feature_repo/example_repo.py new file mode 100644 index 00000000000..80d1b9aa7bd --- /dev/null +++ b/sdk/python/feast/templates/pytorch_nlp/feature_repo/example_repo.py @@ -0,0 +1,277 @@ +""" +PyTorch NLP Sentiment Analysis Feature Repository + +This template demonstrates sentiment analysis using: +- Text feature engineering for NLP +- PyTorch + Hugging Face transformers integration +- On-demand sentiment prediction features +- Online and offline feature serving patterns +""" + +from datetime import timedelta +from pathlib import Path + +import pandas as pd + +from feast import ( + Entity, + FeatureService, + FeatureView, + Field, + FileSource, + RequestSource, + ValueType, +) +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Array, Float32, Int64, String + +try: + # Import static artifacts helpers (available when feature server loads artifacts) + from static_artifacts import get_lookup_tables, get_sentiment_model +except ImportError: + # Fallback for when static_artifacts.py is not available + get_sentiment_model = None + get_lookup_tables = None + +# Global references for static artifacts (set by feature server) +_sentiment_model = None +_lookup_tables: dict = {} + +# Configuration +repo_path = Path(__file__).parent +data_path = repo_path / "data" + +# Define entities - primary keys for joining data +text_entity = Entity( + name="text", + join_keys=["text_id"], + value_type=ValueType.STRING, + description="Unique identifier for text samples", +) + +user_entity = Entity( + name="user", + join_keys=["user_id"], + value_type=ValueType.STRING, + description="User who created the text content", +) + +# Data source - points to the parquet file created by bootstrap +sentiment_source = FileSource( + name="sentiment_data_source", + path=str(data_path / "sentiment_data.parquet"), + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +# Feature view for text metadata and engineered features +text_features_fv = FeatureView( + name="text_features", + entities=[text_entity], + ttl=timedelta(days=7), # Keep features for 7 days + schema=[ + Field(name="text_content", dtype=String, description="Raw text content"), + Field( + name="sentiment_label", + dtype=String, + description="Ground truth sentiment label", + ), + Field( + name="sentiment_score", + dtype=Float32, + description="Ground truth sentiment score", + ), + Field(name="text_length", dtype=Int64, description="Character count of text"), + Field(name="word_count", dtype=Int64, description="Word count of text"), + Field( + name="exclamation_count", + dtype=Int64, + description="Number of exclamation marks", + ), + Field(name="caps_ratio", dtype=Float32, description="Ratio of capital letters"), + Field( + name="emoji_count", dtype=Int64, description="Number of emoji characters" + ), + ], + online=True, + source=sentiment_source, + tags={"team": "nlp", "domain": "sentiment_analysis"}, + version="latest", +) + +# Feature view for user-level aggregations +user_stats_fv = FeatureView( + name="user_stats", + entities=[user_entity], + ttl=timedelta(days=30), # User stats change less frequently + schema=[ + Field( + name="user_avg_sentiment", + dtype=Float32, + description="User's average sentiment score", + ), + Field( + name="user_text_count", + dtype=Int64, + description="Total number of texts by user", + ), + Field( + name="user_avg_text_length", + dtype=Float32, + description="User's average text length", + ), + ], + online=True, + source=sentiment_source, + tags={"team": "nlp", "domain": "user_behavior"}, + version="latest", +) + +# Request source for real-time inference +text_input_request = RequestSource( + name="text_input", + schema=[ + Field( + name="input_text", + dtype=String, + description="Text to analyze at request time", + ), + Field( + name="model_name", dtype=String, description="Model to use for prediction" + ), + ], +) + + +# On-demand feature view for real-time sentiment prediction +@on_demand_feature_view( + sources=[text_input_request], + schema=[ + Field(name="predicted_sentiment", dtype=String), + Field(name="sentiment_confidence", dtype=Float32), + Field(name="positive_prob", dtype=Float32), + Field(name="negative_prob", dtype=Float32), + Field(name="neutral_prob", dtype=Float32), + Field(name="text_embedding", dtype=Array(Float32)), + ], +) +def sentiment_prediction(inputs: pd.DataFrame) -> pd.DataFrame: + """ + Real-time sentiment prediction using pre-loaded static artifacts. + + This function demonstrates how to use static artifacts (pre-loaded models, + lookup tables) for efficient real-time inference. Models are loaded once + at feature server startup rather than on each request. + """ + try: + import numpy as np + except ImportError: + # Fallback to dummy predictions if numpy isn't available + + df = pd.DataFrame() + df["predicted_sentiment"] = ["neutral"] * len(inputs) + df["sentiment_confidence"] = [0.5] * len(inputs) + df["positive_prob"] = [0.33] * len(inputs) + df["negative_prob"] = [0.33] * len(inputs) + df["neutral_prob"] = [0.34] * len(inputs) + df["text_embedding"] = [[0.0] * 384] * len(inputs) + return df + + # Get pre-loaded static artifacts from global references + # These are loaded once at startup via static_artifacts.py + global _sentiment_model, _lookup_tables + + sentiment_model = _sentiment_model + lookup_tables = _lookup_tables + + # Use lookup table for label mapping (from static artifacts) + label_map = lookup_tables.get( + "sentiment_labels", + {"LABEL_0": "negative", "LABEL_1": "neutral", "LABEL_2": "positive"}, + ) + + results = [] + + for text in inputs["input_text"]: + try: + if sentiment_model is not None: + # Use pre-loaded model for prediction + predictions = sentiment_model(text) + + # Parse results using static lookup tables + scores = { + label_map.get(pred["label"], pred["label"]): pred["score"] + for pred in predictions + } + + # Get best prediction + best_pred = max(predictions, key=lambda x: x["score"]) + predicted_sentiment = label_map.get( + best_pred["label"], best_pred["label"] + ) + confidence = best_pred["score"] + else: + # Fallback when model is not available + predicted_sentiment = "neutral" + confidence = 0.5 + scores = {"positive": 0.33, "negative": 0.33, "neutral": 0.34} + + # Generate dummy embeddings (in production, use pre-loaded embeddings) + embedding = np.random.rand(384).tolist() + + results.append( + { + "predicted_sentiment": predicted_sentiment, + "sentiment_confidence": np.float32(confidence), + "positive_prob": np.float32(scores.get("positive", 0.0)), + "negative_prob": np.float32(scores.get("negative", 0.0)), + "neutral_prob": np.float32(scores.get("neutral", 0.0)), + "text_embedding": [np.float32(x) for x in embedding], + } + ) + + except Exception: + # Fallback for individual text processing errors + results.append( + { + "predicted_sentiment": "neutral", + "sentiment_confidence": np.float32(0.5), + "positive_prob": np.float32(0.33), + "negative_prob": np.float32(0.33), + "neutral_prob": np.float32(0.34), + "text_embedding": [np.float32(0.0)] * 384, + } + ) + + return pd.DataFrame(results) + + +# Feature services group related features for model serving +sentiment_analysis_v1 = FeatureService( + name="sentiment_analysis_v1", + features=[ + text_features_fv[["text_content", "text_length", "word_count"]], + sentiment_prediction, + ], + description="Basic sentiment analysis features for model v1", +) + +sentiment_analysis_v2 = FeatureService( + name="sentiment_analysis_v2", + features=[ + text_features_fv, # All text features + user_stats_fv[["user_avg_sentiment", "user_text_count"]], # User context + sentiment_prediction, # Real-time predictions + ], + description="Advanced sentiment analysis with user context for model v2", +) + +# Feature service for training data (historical features only) +sentiment_training_features = FeatureService( + name="sentiment_training_features", + features=[ + text_features_fv, + user_stats_fv, + ], + description="Historical features for model training and evaluation", +) diff --git a/sdk/python/feast/templates/pytorch_nlp/feature_repo/feature_store.yaml b/sdk/python/feast/templates/pytorch_nlp/feature_repo/feature_store.yaml new file mode 100644 index 00000000000..e7c306623c0 --- /dev/null +++ b/sdk/python/feast/templates/pytorch_nlp/feature_repo/feature_store.yaml @@ -0,0 +1,9 @@ +project: my_project +provider: local +registry: data/registry.db +online_store: + type: sqlite + path: data/online_store.db +offline_store: + type: file +entity_key_serialization_version: 3 \ No newline at end of file diff --git a/sdk/python/feast/templates/pytorch_nlp/feature_repo/static_artifacts.py b/sdk/python/feast/templates/pytorch_nlp/feature_repo/static_artifacts.py new file mode 100644 index 00000000000..6f7a5ae3091 --- /dev/null +++ b/sdk/python/feast/templates/pytorch_nlp/feature_repo/static_artifacts.py @@ -0,0 +1,139 @@ +""" +Static Artifacts Loading for PyTorch NLP Template + +This module demonstrates how to load static artifacts (models, lookup tables, etc.) +into the Feast feature server at startup for efficient real-time inference. + +Supported artifact types: +- Small ML models (transformers, scikit-learn, etc.) +- Lookup tables and reference data +- Configuration parameters +- Pre-computed embeddings + +Note: Feast is not optimized for large language models. For LLM inference, +use dedicated model serving solutions like vLLM, TensorRT-LLM, or TGI. +""" + +from pathlib import Path +from typing import Any, Dict, Optional + +from fastapi import FastAPI +from fastapi.logger import logger + + +def load_sentiment_model(): + """Load sentiment analysis model for real-time inference.""" + try: + from transformers import pipeline + + logger.info("Loading sentiment analysis model...") + model = pipeline( + "sentiment-analysis", + model="cardiffnlp/twitter-roberta-base-sentiment-latest", + tokenizer="cardiffnlp/twitter-roberta-base-sentiment-latest", + return_all_scores=True, + device="cpu", # Force CPU to avoid MPS forking issues on macOS + ) + logger.info("✅ Sentiment analysis model loaded successfully") + return model + except ImportError: + logger.warning( + "⚠️ Transformers not available, sentiment model will use fallback" + ) + return None + except Exception as e: + logger.warning(f"⚠️ Failed to load sentiment model: {e}") + return None + + +def load_lookup_tables() -> Dict[str, Any]: + """Load static lookup tables for feature engineering.""" + # Example: Load static mappings that are expensive to compute at request time + return { + "sentiment_labels": { + "LABEL_0": "negative", + "LABEL_1": "neutral", + "LABEL_2": "positive", + }, + "emoji_sentiment": {"😊": "positive", "😞": "negative", "😐": "neutral"}, + "domain_categories": {"twitter.com": "social", "news.com": "news"}, + } + + +def load_user_embeddings() -> Optional[Dict[str, Any]]: + """Load pre-computed user embeddings if available.""" + # Example: Load static user embeddings for recommendation features + embeddings_file = Path(__file__).parent / "data" / "user_embeddings.npy" + + if embeddings_file.exists(): + try: + import numpy as np + + embeddings = np.load(embeddings_file) + logger.info(f"✅ Loaded user embeddings: {embeddings.shape}") + return {"embeddings": embeddings} + except Exception as e: + logger.warning(f"⚠️ Failed to load user embeddings: {e}") + + return None + + +def load_artifacts(app: FastAPI): + """ + Main function called by Feast feature server to load static artifacts. + + This function is called during server startup and should store artifacts + in app.state for access by on-demand feature views. + """ + logger.info("🔄 Loading static artifacts for PyTorch NLP template...") + + # Load sentiment analysis model + app.state.sentiment_model = load_sentiment_model() + + # Load lookup tables + app.state.lookup_tables = load_lookup_tables() + + # Load user embeddings (optional) + app.state.user_embeddings = load_user_embeddings() + + # Also set global references for easier access from on-demand feature views + try: + import example_repo + + example_repo._sentiment_model = app.state.sentiment_model + example_repo._lookup_tables = app.state.lookup_tables + logger.info("✅ Global artifact references updated") + except ImportError: + logger.warning("⚠️ Could not update global artifact references") + + logger.info("✅ Static artifacts loading completed") + + +def get_static_artifact(app_state: Any, name: str) -> Any: + """ + Helper function to safely access static artifacts from app.state. + + Args: + app_state: FastAPI app.state object + name: Name of the artifact to retrieve + + Returns: + The requested artifact or None if not found + """ + return getattr(app_state, name, None) + + +# Convenience functions for accessing specific artifacts +def get_sentiment_model(app_state: Any): + """Get the pre-loaded sentiment analysis model.""" + return get_static_artifact(app_state, "sentiment_model") + + +def get_lookup_tables(app_state: Any) -> Dict[str, Any]: + """Get the pre-loaded lookup tables.""" + return get_static_artifact(app_state, "lookup_tables") or {} + + +def get_user_embeddings(app_state: Any): + """Get the pre-loaded user embeddings.""" + return get_static_artifact(app_state, "user_embeddings") diff --git a/sdk/python/feast/templates/pytorch_nlp/feature_repo/test_workflow.py b/sdk/python/feast/templates/pytorch_nlp/feature_repo/test_workflow.py new file mode 100644 index 00000000000..efca72fc3bc --- /dev/null +++ b/sdk/python/feast/templates/pytorch_nlp/feature_repo/test_workflow.py @@ -0,0 +1,350 @@ +""" +PyTorch NLP Sentiment Analysis - Complete Test Workflow + +This script demonstrates the full lifecycle of a sentiment analysis project using Feast: +1. Feature store setup and deployment +2. Historical feature retrieval for model training +3. Online feature serving for real-time inference +4. Integration with PyTorch and Hugging Face models +5. Performance evaluation and monitoring +""" + +import subprocess +from datetime import datetime, timedelta + +import pandas as pd + +from feast import FeatureStore + + +def run_demo(): + """Run the complete PyTorch NLP sentiment analysis demo.""" + print("🎭 PyTorch NLP Sentiment Analysis Demo") + print("=====================================") + + store = FeatureStore(repo_path=".") + + # 1. Deploy feature definitions + print("\n🚀 Step 1: Deploy feature definitions") + print("--- Run feast apply ---") + subprocess.run(["feast", "apply"]) + + # 2. Materialize features to online store + print("\n💾 Step 2: Materialize features to online store") + print("--- Load features into online store ---") + store.materialize_incremental(end_date=datetime.now()) + + # 3. Demonstrate historical feature retrieval for training + print("\n📚 Step 3: Historical features for model training") + training_data = fetch_historical_features_for_training(store) + + # 4. Simulate model training (conceptual) + print("\n🏋️ Step 4: Simulate model training") + simulate_model_training(training_data) + + # 5. Online feature serving for real-time inference + print("\n⚡ Step 5: Real-time inference with online features") + test_online_inference(store) + + # 6. Demonstrate on-demand feature views + print("\n🔮 Step 6: On-demand sentiment prediction") + test_on_demand_sentiment_prediction(store) + + # 7. Feature service usage + print("\n🎯 Step 7: Feature services for model versioning") + test_feature_services(store) + + # 8. Performance evaluation + print("\n📊 Step 8: Model performance evaluation") + evaluate_model_performance(store) + + print("\n✨ Demo completed successfully!") + print("\n📖 Next steps:") + print(" - Modify the sentiment data in data/sentiment_data.parquet") + print(" - Experiment with different models in example_repo.py") + print(" - Add more feature engineering transformations") + print(" - Deploy to production with cloud providers (AWS, GCP, etc.)") + + +def fetch_historical_features_for_training(store: FeatureStore) -> pd.DataFrame: + """Fetch historical features for model training with point-in-time correctness.""" + # Create entity DataFrame for training + # In practice, this would come from your ML pipeline or data warehouse + entity_df = pd.DataFrame.from_dict( + { + "text_id": [ + "text_0001", + "text_0002", + "text_0003", + "text_0004", + "text_0005", + "text_0010", + "text_0015", + "text_0020", + "text_0025", + "text_0030", + ], + "user_id": [ + "user_001", + "user_002", + "user_001", + "user_003", + "user_002", + "user_001", + "user_004", + "user_003", + "user_005", + "user_001", + ], + "event_timestamp": [ + datetime(2023, 6, 15, 10, 0, 0), + datetime(2023, 6, 15, 11, 30, 0), + datetime(2023, 6, 15, 14, 15, 0), + datetime(2023, 6, 16, 9, 45, 0), + datetime(2023, 6, 16, 13, 20, 0), + datetime(2023, 6, 17, 8, 30, 0), + datetime(2023, 6, 17, 16, 45, 0), + datetime(2023, 6, 18, 12, 10, 0), + datetime(2023, 6, 18, 15, 30, 0), + datetime(2023, 6, 19, 11, 0, 0), + ], + } + ) + + # Fetch historical features using the training feature service + print(" 📊 Retrieving training dataset with point-in-time correctness...") + training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "text_features:text_content", + "text_features:sentiment_label", + "text_features:sentiment_score", + "text_features:text_length", + "text_features:word_count", + "text_features:exclamation_count", + "text_features:caps_ratio", + "text_features:emoji_count", + "user_stats:user_avg_sentiment", + "user_stats:user_text_count", + ], + ).to_df() + + print(f" ✅ Retrieved {len(training_df)} training samples") + print(" 📋 Sample training data:") + print( + training_df[ + ["text_content", "sentiment_label", "text_length", "word_count"] + ].head(3) + ) + + return training_df + + +def simulate_model_training(training_data: pd.DataFrame): + """Simulate model training process (conceptual implementation).""" + print(" 🧠 Training sentiment analysis model...") + + # In a real implementation, you would: + # 1. Split data into train/validation/test + # 2. Tokenize text using transformers tokenizer + # 3. Fine-tune a pre-trained model (BERT, RoBERTa, etc.) + # 4. Evaluate performance metrics + # 5. Save the trained model + + print(f" 📊 Training data shape: {training_data.shape}") + + if not training_data.empty: + # Simple statistics as a proxy for training + sentiment_dist = training_data["sentiment_label"].value_counts() + avg_text_length = training_data["text_length"].mean() + + print(" 📈 Sentiment distribution:") + for sentiment, count in sentiment_dist.items(): + print( + f" {sentiment}: {count} samples ({count / len(training_data) * 100:.1f}%)" + ) + + print(f" 📏 Average text length: {avg_text_length:.1f} characters") + print(" ✅ Model training simulation completed!") + else: + print(" ⚠️ No training data available") + + +def test_online_inference(store: FeatureStore): + """Test online feature serving for real-time inference.""" + print(" ⚡ Testing real-time feature serving...") + + # Entity rows for online inference + entity_rows = [ + {"text_id": "text_0001", "user_id": "user_001"}, + {"text_id": "text_0002", "user_id": "user_002"}, + {"text_id": "text_0005", "user_id": "user_002"}, + ] + + # Fetch online features + online_features = store.get_online_features( + features=[ + "text_features:text_content", + "text_features:text_length", + "text_features:word_count", + "user_stats:user_avg_sentiment", + ], + entity_rows=entity_rows, + ).to_dict() + + print(" 📊 Retrieved online features:") + for key, values in online_features.items(): + if key in ["text_content"]: + # Truncate long text for display + display_values = [ + str(v)[:50] + "..." if len(str(v)) > 50 else str(v) for v in values + ] + print(f" {key}: {display_values}") + else: + print(f" {key}: {values}") + + +def test_on_demand_sentiment_prediction(store: FeatureStore): + """Test on-demand feature views for real-time sentiment prediction.""" + print(" 🔮 Testing on-demand sentiment prediction...") + + # Request data for on-demand features + entity_rows = [ + { + "input_text": "I love this product! It's absolutely amazing and works perfectly!", + "model_name": "cardiffnlp/twitter-roberta-base-sentiment-latest", + }, + { + "input_text": "This is terrible quality. Completely disappointed with the purchase.", + "model_name": "cardiffnlp/twitter-roberta-base-sentiment-latest", + }, + { + "input_text": "The product is okay. Nothing special but it does work as expected.", + "model_name": "cardiffnlp/twitter-roberta-base-sentiment-latest", + }, + ] + + try: + # Get on-demand predictions + predictions = store.get_online_features( + features=[ + "sentiment_prediction:predicted_sentiment", + "sentiment_prediction:sentiment_confidence", + "sentiment_prediction:positive_prob", + "sentiment_prediction:negative_prob", + "sentiment_prediction:neutral_prob", + ], + entity_rows=entity_rows, + ).to_dict() + + print(" 🎯 Prediction results:") + for i in range(len(entity_rows)): + text = entity_rows[i]["input_text"][:60] + "..." + sentiment = predictions["predicted_sentiment"][i] + confidence = predictions["sentiment_confidence"][i] + print(f" Text: {text}") + print(f" Predicted: {sentiment} (confidence: {confidence:.3f})") + print( + f" Probabilities: P={predictions['positive_prob'][i]:.3f}, " + f"N={predictions['negative_prob'][i]:.3f}, " + f"Neu={predictions['neutral_prob'][i]:.3f}" + ) + print() + + except Exception as e: + print(f" ⚠️ On-demand prediction failed: {e}") + print( + " 💡 This is expected if PyTorch/transformers dependencies are not installed" + ) + print(" 📦 Install with: pip install torch transformers") + + +def test_feature_services(store: FeatureStore): + """Test different feature services for model versioning.""" + print(" 🎯 Testing feature services...") + + entity_rows = [{"text_id": "text_0001", "user_id": "user_001"}] + + # Test basic sentiment analysis service (v1) + print(" 📦 Testing sentiment_analysis_v1 feature service...") + try: + features_v1 = store.get_online_features( + features=store.get_feature_service("sentiment_analysis_v1"), + entity_rows=entity_rows, + ).to_dict() + print(f" ✅ Retrieved {len(features_v1)} feature types for v1") + except Exception as e: + print(f" ⚠️ Feature service v1 failed: {e}") + + # Test advanced sentiment analysis service (v2) + print(" 📦 Testing sentiment_analysis_v2 feature service...") + try: + features_v2 = store.get_online_features( + features=store.get_feature_service("sentiment_analysis_v2"), + entity_rows=entity_rows, + ).to_dict() + print(f" ✅ Retrieved {len(features_v2)} feature types for v2") + except Exception as e: + print(f" ⚠️ Feature service v2 failed: {e}") + + +def evaluate_model_performance(store: FeatureStore): + """Evaluate model performance using historical features.""" + print(" 📊 Evaluating model performance...") + + try: + # Get a sample of historical data for evaluation + entity_df = pd.DataFrame( + { + "text_id": [f"text_{i:04d}" for i in range(1, 21)], + "user_id": [f"user_{(i % 5) + 1:03d}" for i in range(1, 21)], + "event_timestamp": [ + datetime.now() - timedelta(hours=i) for i in range(20) + ], + } + ) + + # Fetch features and labels + eval_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "text_features:text_content", + "text_features:sentiment_label", + "text_features:sentiment_score", + ], + ).to_df() + + if not eval_df.empty and "sentiment_label" in eval_df.columns: + # Calculate basic performance metrics + sentiment_dist = eval_df["sentiment_label"].value_counts() + avg_score = ( + eval_df["sentiment_score"].mean() + if "sentiment_score" in eval_df.columns + else 0 + ) + + print(" 📈 Performance summary:") + print(f" Evaluation samples: {len(eval_df)}") + print(f" Average sentiment score: {avg_score:.3f}") + print(" Class distribution:") + for sentiment, count in sentiment_dist.items(): + print( + f" {sentiment}: {count} ({count / len(eval_df) * 100:.1f}%)" + ) + + # In a real implementation, you would: + # 1. Compare predicted vs actual labels + # 2. Calculate accuracy, precision, recall, F1-score + # 3. Generate confusion matrix + # 4. Analyze error cases + # 5. Monitor model drift over time + + else: + print(" ⚠️ No evaluation data available") + + except Exception as e: + print(f" ⚠️ Evaluation failed: {e}") + + +if __name__ == "__main__": + run_demo() diff --git a/sdk/python/feast/templates/pytorch_nlp/gitignore b/sdk/python/feast/templates/pytorch_nlp/gitignore new file mode 100644 index 00000000000..88196cd1f22 --- /dev/null +++ b/sdk/python/feast/templates/pytorch_nlp/gitignore @@ -0,0 +1,180 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be added to the global gitignore or merged into this project gitignore. For a PyCharm +# project, it is recommended to include the following directory in .gitignore: +# https://intellij-support.jetbrains.com/hc/en/articles/206544839 + +# Feast-specific +feature_repo/data/*.db +feature_repo/data/*.db.lock +feature_repo/data/online_store.db* +feature_repo/data/registry.db* +feature_repo/data/registry.pb* + +# Model cache +.cache/ +huggingface/ +transformers_cache/ + +# MacOS +.DS_Store + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Logs +*.log \ No newline at end of file diff --git a/sdk/python/feast/templates/rag/README.md b/sdk/python/feast/templates/rag/README.md new file mode 100644 index 00000000000..a6aa153d6a2 --- /dev/null +++ b/sdk/python/feast/templates/rag/README.md @@ -0,0 +1,99 @@ +# City Information Q&A — RAG Demo with Feast + +A complete Retrieval-Augmented Generation (RAG) demo using Feast for feature management and Milvus for vector search. + + +## Project Structure + +``` +rag/ +├── feature_repo/ +│ ├── data/ +│ │ └── city_wikipedia_summaries_with_embeddings.parquet # Sample data (US cities) +│ ├── example_repo.py # Entity, Feature Views, Feature Service definitions +│ ├── feature_store.yaml # Feast config (Milvus online store, file offline store) +│ └── test_workflow.py # End-to-end demo: apply → materialize → search +└── README.md +``` + +## Quick Start + +### 1. Initialize the template + +```bash +feast init -t rag my_city_qa +cd my_city_qa/feature_repo +``` + +### 2. Install dependencies + +```bash +pip install feast torch transformers pymilvus +``` + +### 3. Apply feature definitions + +```bash +feast apply +``` + +### 4. Explore in the Feast UI + +```bash +feast ui +``` + +### 5. Run the demo workflow + +```bash +python test_workflow.py +``` + + +## Key Commands + +| Command | Description | +|---------|-------------| +| `feast apply` | Register entities, feature views, and feature services | +| `feast materialize --disable-event-timestamp` | Load parquet data into the online store (Milvus) for vector search. Optionally add `-v city_summary_embeddings -v city_metadata` to materialize only those views. | +| `feast feature-views list` | List registered feature views | +| `feast entities list` | List registered entities | +| `feast feature-services list` | List registered feature services | +| `feast ui` | Start the Feast UI at http://localhost:8888 | + + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ City Q&A Pipeline │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ User Question │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ Embed Query │ (MiniLM 384-dim) │ +│ └─────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────┐ │ +│ │ city_summary_embeddings (Milvus) │ ← Vector Search │ +│ │ - vector (COSINE similarity) │ │ +│ │ - sentence_chunks │ │ +│ └─────────────────────────────────────┘ │ +│ │ │ +│ ▼ (top-k city_ids) │ +│ ┌─────────────────────────────────────┐ │ +│ │ city_metadata (Feast Online Store) │ ← Metadata Lookup │ +│ │ - state │ │ +│ │ - wiki_summary │ │ +│ └─────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────┐ │ +│ │ LLM Answer │ (optional: GPT/Claude) │ +│ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` diff --git a/sdk/python/feast/templates/rag/__init__.py b/sdk/python/feast/templates/rag/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/sdk/python/feast/templates/rag/__init__.py @@ -0,0 +1 @@ + diff --git a/sdk/python/feast/templates/rag/bootstrap.py b/sdk/python/feast/templates/rag/bootstrap.py new file mode 100644 index 00000000000..a84fb23a639 --- /dev/null +++ b/sdk/python/feast/templates/rag/bootstrap.py @@ -0,0 +1,56 @@ +def bootstrap(): + # Bootstrap() is called from init_repo() during `feast init` + import pathlib + from datetime import datetime + + import numpy as np + import pandas as pd + + repo_path = pathlib.Path(__file__).parent.absolute() / "feature_repo" + data_path = repo_path / "data" + data_path.mkdir(exist_ok=True) + + # Minimal city data with embeddings (384-d to match feature_store embedding_dim) + embedding_dim = 384 + now = datetime.now().replace(microsecond=0, tzinfo=None) + cities = [ + ( + 1, + "New York", + "New York", + "New York City is the most populous city in the United States.", + ), + ( + 2, + "Los Angeles", + "California", + "Los Angeles is the second most populous city in the United States.", + ), + ( + 3, + "Chicago", + "Illinois", + "Chicago is the third most populous city in the United States.", + ), + ] + rows = [] + for city_id, city_name, state, wiki_summary in cities: + vec = np.random.randn(embedding_dim).astype(np.float32) + vec = (vec / np.linalg.norm(vec)).tolist() + rows.append( + { + "city_id": city_id, + "event_timestamp": pd.Timestamp(now), + "vector": vec, + "sentence_chunks": wiki_summary[:200], + "state": f"{city_name}, {state}", + "wiki_summary": wiki_summary, + } + ) + df = pd.DataFrame(rows) + parquet_path = data_path / "city_wikipedia_summaries_with_embeddings.parquet" + df.to_parquet(path=str(parquet_path), index=False) + + +if __name__ == "__main__": + bootstrap() diff --git a/sdk/python/feast/templates/rag/feature_repo/__init__.py b/sdk/python/feast/templates/rag/feature_repo/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/sdk/python/feast/templates/rag/feature_repo/__init__.py @@ -0,0 +1 @@ + diff --git a/sdk/python/feast/templates/rag/feature_repo/example_repo.py b/sdk/python/feast/templates/rag/feature_repo/example_repo.py new file mode 100644 index 00000000000..af2ea96b1db --- /dev/null +++ b/sdk/python/feast/templates/rag/feature_repo/example_repo.py @@ -0,0 +1,137 @@ +from datetime import timedelta + +from feast import ( + Entity, + FeatureService, + FeatureView, + Field, + FileSource, + PushSource, +) +from feast.data_format import ParquetFormat +from feast.types import Array, Float32, String +from feast.value_type import ValueType + +# Entity: Identifies each city document/chunk in the knowledge base +city = Entity( + name="city_id", + value_type=ValueType.INT64, + description="Unique identifier for each city Wikipedia summary (document chunk ID).", + join_keys=["city_id"], +) + +# Data Source: Parquet file containing city summaries with pre-computed embeddings +city_summaries_source = FileSource( + name="city_summaries_source", + file_format=ParquetFormat(), + path="./data/city_wikipedia_summaries_with_embeddings.parquet", + timestamp_field="event_timestamp", + description="Wikipedia summaries of US cities (batch).", +) + +# Push Source: same schema as batch; allows near real-time ingestion of new/updated docs +city_summaries_push_source = PushSource( + name="city_summaries_push_source", + batch_source=city_summaries_source, + description="Push source for real-time updates to city summaries/embeddings.", +) + +# Feature View 1: City embeddings for semantic/vector search (RAG retrieval) +city_summary_embeddings = FeatureView( + name="city_summary_embeddings", + description="City Wikipedia summaries with embeddings for semantic search. ", + entities=[city], + schema=[ + Field( + name="vector", + dtype=Array(Float32), + description="384-dimensional sentence embedding for semantic similarity search (MiniLM).", + vector_index=True, + vector_search_metric="COSINE", + ), + Field( + name="sentence_chunks", + dtype=String, + description="Chunked sentences from the Wikipedia summary.", + ), + ], + source=city_summaries_source, + ttl=timedelta(days=1), + online=True, + tags={"team": "ml-platform", "use_case": "city_qa", "type": "vector"}, +) + +# Feature View 2: City metadata for scalar lookups (no vector search) +city_metadata = FeatureView( + name="city_metadata", + description="City metadata including state and full Wikipedia summary. ", + entities=[city], + schema=[ + Field( + name="state", + dtype=String, + description="US state where the city is located (e.g., 'New York, New York').", + ), + Field( + name="wiki_summary", + dtype=String, + description="Full Wikipedia summary of the city.", + ), + ], + source=city_summaries_source, + ttl=timedelta(hours=2), + online=True, + tags={"team": "ml-platform", "use_case": "city_qa", "type": "metadata"}, +) + +# Feature View 3: Fresh embeddings (PushSource) for near real-time doc updates +city_summary_embeddings_realtime = FeatureView( + name="city_summary_embeddings_realtime", + description="Same as city_summary_embeddings but with real-time ingestion (PushSource).", + entities=[city], + schema=[ + Field( + name="vector", + dtype=Array(Float32), + description="384-dimensional sentence embedding for semantic similarity search.", + vector_index=True, + vector_search_metric="COSINE", + ), + Field( + name="sentence_chunks", + dtype=String, + description="Chunked sentences from the Wikipedia summary.", + ), + ], + source=city_summaries_push_source, + ttl=timedelta(hours=2), + online=True, + tags={ + "team": "ml-platform", + "use_case": "city_qa", + "type": "vector", + "ingestion": "push", + }, +) + +# Feature Service: Bundles features for the City Q&A retrieval endpoint +city_qa_v1 = FeatureService( + name="city_qa_v1", + features=[ + city_summary_embeddings, + city_metadata, + ], + description="Feature service for City Information Q&A. ", + tags={"team": "ml-platform", "version": "v1"}, +) + +# Feature service that includes push-backed and request-time features +city_qa_v2 = FeatureService( + name="city_qa_v2", + features=[ + city_summary_embeddings_realtime, + city_metadata, + ], + description="City Q&A with push ingestion and request-time context (query_text, user_id).", + tags={"team": "ml-platform", "version": "v2"}, +) diff --git a/infra/feast-operator/test/e2e_rhoai/resources/feature_repo/feature_store.yaml b/sdk/python/feast/templates/rag/feature_repo/feature_store.yaml old mode 100755 new mode 100644 similarity index 52% rename from infra/feast-operator/test/e2e_rhoai/resources/feature_repo/feature_store.yaml rename to sdk/python/feast/templates/rag/feature_repo/feature_store.yaml index f8f9cc293dc..658d710b247 --- a/infra/feast-operator/test/e2e_rhoai/resources/feature_repo/feature_store.yaml +++ b/sdk/python/feast/templates/rag/feature_repo/feature_store.yaml @@ -1,4 +1,7 @@ -project: rag +project: my_project + +project_description: | + This project is a smart City Q&A assistant that gives you accurate, detailed answers about any city instantly. provider: local registry: data/registry.db online_store: @@ -8,9 +11,10 @@ online_store: embedding_dim: 384 index_type: "FLAT" metric_type: "COSINE" + offline_store: type: file entity_key_serialization_version: 3 +# By default, no_auth for authentication and authorization auth: - type: no_auth - + type: no_auth diff --git a/sdk/python/feast/templates/rag/feature_repo/test_workflow.py b/sdk/python/feast/templates/rag/feature_repo/test_workflow.py new file mode 100644 index 00000000000..7a321208795 --- /dev/null +++ b/sdk/python/feast/templates/rag/feature_repo/test_workflow.py @@ -0,0 +1,155 @@ +from datetime import datetime + +import pandas as pd +import torch +import torch.nn.functional as F +from example_repo import ( + city, + city_metadata, + city_qa_v1, + city_qa_v2, + city_summary_embeddings, + city_summary_embeddings_realtime, +) +from transformers import AutoModel, AutoTokenizer + +from feast import FeatureStore +from feast.data_source import PushMode + +TOKENIZER = "sentence-transformers/all-MiniLM-L6-v2" +MODEL = "sentence-transformers/all-MiniLM-L6-v2" + + +def mean_pooling(model_output, attention_mask): + token_embeddings = model_output[0] + input_mask_expanded = ( + attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() + ) + return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp( + input_mask_expanded.sum(1), min=1e-9 + ) + + +def run_model(sentences, tokenizer, model): + encoded_input = tokenizer( + sentences, padding=True, truncation=True, return_tensors="pt" + ) + with torch.no_grad(): + model_output = model(**encoded_input) + sentence_embeddings = mean_pooling(model_output, encoded_input["attention_mask"]) + sentence_embeddings = F.normalize(sentence_embeddings, p=2, dim=1) + return sentence_embeddings + + +def run_demo(): + print("City Information Q&A - RAG Demo") + + store = FeatureStore(repo_path=".") + print("\n[1/7] Applying feature definitions...") + store.apply( + [ + city, + city_summary_embeddings, + city_metadata, + city_summary_embeddings_realtime, + city_qa_v1, + city_qa_v2, + ] + ) + print( + " Entity, 3 feature views (batch + push), 2 feature services registered." + ) + + print("\n[2/7] Materializing batch views into online store...") + store.materialize( + feature_views=["city_summary_embeddings", "city_metadata"], + start_date=datetime(1970, 1, 1), + end_date=datetime.now(), + disable_event_timestamp=True, + ) + print(" city_summary_embeddings, city_metadata materialized from parquet.") + + print("\n[3/7] Verifying batch data...") + df = pd.read_parquet("./data/city_wikipedia_summaries_with_embeddings.parquet") + embedding_length = len(df["vector"][0]) + print(f" Parquet: {len(df)} rows, {embedding_length}-dim vectors.") + + tokenizer = AutoTokenizer.from_pretrained(TOKENIZER) + model = AutoModel.from_pretrained(MODEL) + + print("\n[4/7] Pushing one document to PushSource...") + push_text = "Demo pushed city for testing RAG and real-time ingestion." + push_embedding = run_model([push_text], tokenizer, model) + push_vector = push_embedding.detach().cpu().numpy().tolist()[0] + push_df = pd.DataFrame.from_dict( + { + "city_id": [99999], + "event_timestamp": [datetime.now()], + "vector": [push_vector], + "sentence_chunks": [push_text], + } + ) + store.push("city_summaries_push_source", push_df, to=PushMode.ONLINE) + print(" Pushed city_id=99999 to city_summary_embeddings_realtime.") + + print("\n[5/7] Vector search (batch view)...") + question = "the most populous city in the state of New York" + print(f' Query: "{question}"') + + query_embedding = run_model(question, tokenizer, model) + query = query_embedding.detach().cpu().numpy().tolist()[0] + + features_batch = store.retrieve_online_documents_v2( + features=[ + "city_summary_embeddings:vector", + "city_summary_embeddings:city_id", + "city_summary_embeddings:sentence_chunks", + ], + query=query, + top_k=3, + ) + print(" Top 3 (city_summary_embeddings):") + results_batch_df = features_batch.to_df() + print(results_batch_df[["city_id", "sentence_chunks", "distance"]].to_string()) + + print("\n[6/7] Vector search (realtime view, includes pushed doc)...") + question_realtime = "Demo pushed city for testing RAG" + query_realtime = ( + run_model([question_realtime], tokenizer, model) + .detach() + .cpu() + .numpy() + .tolist()[0] + ) + features_realtime = store.retrieve_online_documents_v2( + features=[ + "city_summary_embeddings_realtime:vector", + "city_summary_embeddings_realtime:city_id", + "city_summary_embeddings_realtime:sentence_chunks", + ], + query=query_realtime, + top_k=3, + ) + results_realtime_df = features_realtime.to_df() + print(" Top 3 (city_summary_embeddings_realtime):") + print(results_realtime_df[["city_id", "sentence_chunks", "distance"]].to_string()) + if any(results_realtime_df["city_id"] == 99999): + print(" Pushed doc (city_id=99999) in results.") + + print("\n[7/7] Metadata via Feature Service V2 (city_qa_v2)...") + top_city_id = int(results_batch_df["city_id"].iloc[0]) + + metadata_features = store.get_online_features( + features=store.get_feature_service("city_qa_v2"), + entity_rows=[{"city_id": top_city_id}], + ).to_dict() + + print(f" city_id={top_city_id} -> state: {metadata_features['state'][0]}") + print(f" wiki_summary: {metadata_features['wiki_summary'][0][:200]}...") + + store.teardown() + print("\nDone.") + + +if __name__ == "__main__": + run_demo() diff --git a/sdk/python/feast/templates/rag/gitignore b/sdk/python/feast/templates/rag/gitignore new file mode 100644 index 00000000000..2d7ceaab468 --- /dev/null +++ b/sdk/python/feast/templates/rag/gitignore @@ -0,0 +1,33 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +venv/ +.venv + +# Pytest +.cache +.pytest_cache/ +.coverage +*.log + +# Jupyter Notebook +.ipynb_checkpoints + +# IDEs and Editors +.vscode/ +.idea/ + +# OS generated files +.DS_Store + +# Feast +.feast/ diff --git a/sdk/python/feast/templates/ray/README.md b/sdk/python/feast/templates/ray/README.md index dd2d8c9f7f1..4004b8e9a59 100644 --- a/sdk/python/feast/templates/ray/README.md +++ b/sdk/python/feast/templates/ray/README.md @@ -8,7 +8,7 @@ This template demonstrates Feast's Ray integration, showcasing both the **Ray Of ray_template/ ├── feature_repo/ │ ├── feature_store.yaml # Ray offline store + compute engine config -│ ├── example_repo.py # Feature definitions with Ray optimizations +│ ├── feature_definitions.py # Feature definitions with Ray optimizations │ ├── test_workflow.py # Demo script showing Ray capabilities │ └── data/ # Sample datasets (generated by bootstrap) │ ├── driver_stats.parquet diff --git a/sdk/python/feast/templates/ray/bootstrap.py b/sdk/python/feast/templates/ray/bootstrap.py index 30f5bf7dd00..6baf387d921 100644 --- a/sdk/python/feast/templates/ray/bootstrap.py +++ b/sdk/python/feast/templates/ray/bootstrap.py @@ -69,8 +69,8 @@ def bootstrap(): path=str(customer_profile_path), allow_truncated_timestamps=True ) - # Update the example_repo.py file with actual paths - example_py_file = repo_path / "example_repo.py" + # Update the feature_definitions.py file with actual paths + example_py_file = repo_path / "feature_definitions.py" replace_str_in_file(example_py_file, "%PROJECT_NAME%", str(project_name)) replace_str_in_file( example_py_file, "%PARQUET_PATH%", str(driver_stats_path.relative_to(repo_path)) diff --git a/sdk/python/feast/templates/ray/feature_repo/example_repo.py b/sdk/python/feast/templates/ray/feature_repo/feature_definitions.py similarity index 87% rename from sdk/python/feast/templates/ray/feature_repo/example_repo.py rename to sdk/python/feast/templates/ray/feature_repo/feature_definitions.py index 0b2df66de34..741f38cf889 100644 --- a/sdk/python/feast/templates/ray/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/ray/feature_repo/feature_definitions.py @@ -8,7 +8,7 @@ from pathlib import Path from feast import Entity, FeatureService, FeatureView, Field, ValueType -from feast.infra.offline_stores.file_source import FileSource +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import RaySource from feast.on_demand_feature_view import on_demand_feature_view from feast.types import Float32, Float64, Int64 @@ -30,17 +30,21 @@ join_keys=["customer_id"], ) -# Data sources - Ray offline store works with FileSource -# These will be processed by Ray for efficient distributed data access -driver_hourly_stats = FileSource( +# Data sources — RaySource is the recommended data source for the Ray offline +# store. It tells Feast how to load a Ray Dataset from any Ray Data-supported +# format (Parquet, CSV, JSON, HuggingFace datasets, MongoDB, SQL, and more). +# Here we read local Parquet files that are generated by bootstrap.py. +driver_hourly_stats = RaySource( name="driver_hourly_stats", + reader_type="parquet", path=f"{CURRENT_DIR}/%PARQUET_PATH%", timestamp_field="event_timestamp", created_timestamp_column="created", ) -customer_daily_profile = FileSource( +customer_daily_profile = RaySource( name="customer_daily_profile", + reader_type="parquet", path=f"{CURRENT_DIR}/data/customer_daily_profile.parquet", timestamp_field="event_timestamp", created_timestamp_column="created", @@ -59,6 +63,7 @@ online=True, source=driver_hourly_stats, tags={"team": "driver_performance", "processing": "ray"}, + version="latest", ) customer_daily_profile_view = FeatureView( @@ -73,6 +78,7 @@ online=True, source=customer_daily_profile, tags={"team": "customer_analytics", "processing": "ray"}, + version="latest", ) diff --git a/sdk/python/feast/templates/ray/feature_repo/test_workflow.py b/sdk/python/feast/templates/ray/feature_repo/test_workflow.py index 8caa4cb6380..721a122d5e1 100644 --- a/sdk/python/feast/templates/ray/feature_repo/test_workflow.py +++ b/sdk/python/feast/templates/ray/feature_repo/test_workflow.py @@ -83,12 +83,28 @@ def run_demo(): ], ) - # Convert to DataFrame - Ray processes this efficiently - historical_df = historical_features.to_df() - print(f" ✓ Retrieved {len(historical_df)} historical feature rows") - print(f" ✓ Features: {list(historical_df.columns)}") - - # Show sample of the data + # to_ray_dataset() returns the result as a Ray Dataset — no data is + # collected on the driver when the Ray offline store is used. This is + # the preferred output format for Ray Train, Ray Serve, and other + # distributed ML workloads. + ray_ds = historical_features.to_ray_dataset() + print(f" ✓ Retrieved Ray Dataset with {ray_ds.count()} rows") + print(f" ✓ Schema: {ray_ds.schema()}") + + # For non-Ray downstream consumers (pandas, scikit-learn, …) chain + # to_df() on the same RetrievalJob instead: + historical_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "customer_daily_profile:current_balance", + "customer_daily_profile:avg_passenger_count", + "customer_daily_profile:lifetime_trip_count", + ], + ).to_df() + print(f" ✓ Same features as pandas DataFrame: {list(historical_df.columns)}") print("\n Sample historical features:") print(historical_df.head(3).to_string(index=False)) diff --git a/sdk/python/feast/templates/ray_rag/README.md b/sdk/python/feast/templates/ray_rag/README.md index e1289826c6a..0cade9af9aa 100644 --- a/sdk/python/feast/templates/ray_rag/README.md +++ b/sdk/python/feast/templates/ray_rag/README.md @@ -16,7 +16,7 @@ RAG (Retrieval-Augmented Generation) template using Feast with Ray for distribut ray_rag/ ├── feature_repo/ │ ├── feature_store.yaml # Ray + Milvus configuration -│ ├── example_repo.py # Feature definitions with Ray UDF +│ ├── feature_definitions.py # Feature definitions with Ray UDF │ ├── test_workflow.py # End-to-end demo │ └── data/ │ └── raw_movies.parquet # Sample IMDB dataset (10 movies) @@ -110,7 +110,7 @@ results = store.retrieve_online_documents_v2( # Display results with metadata for i in range(len(results["document_id_pk"])): - print(f"{i+1}. {results['movie_name'][i]}") + print(f"{i + 1}. {results['movie_name'][i]}") print(f" Director: {results['movie_director'][i]}") print(f" Distance: {results['distance'][i]:.3f}") ``` diff --git a/sdk/python/feast/templates/ray_rag/bootstrap.py b/sdk/python/feast/templates/ray_rag/bootstrap.py index 752cee0a5c8..a903969eaef 100644 --- a/sdk/python/feast/templates/ray_rag/bootstrap.py +++ b/sdk/python/feast/templates/ray_rag/bootstrap.py @@ -74,7 +74,7 @@ def bootstrap(): pq.write_table(table, parquet_file) print(f" ✅ Created sample dataset with {len(sample_data)} movies") - example_py_file = repo_path / "example_repo.py" + example_py_file = repo_path / "feature_definitions.py" replace_str_in_file(example_py_file, "%PROJECT_NAME%", str(project_name)) print("🚀 Ray RAG template initialized successfully!") diff --git a/sdk/python/feast/templates/ray_rag/feature_repo/example_repo.py b/sdk/python/feast/templates/ray_rag/feature_repo/feature_definitions.py similarity index 100% rename from sdk/python/feast/templates/ray_rag/feature_repo/example_repo.py rename to sdk/python/feast/templates/ray_rag/feature_repo/feature_definitions.py diff --git a/sdk/python/feast/templates/snowflake/__init__.py b/sdk/python/feast/templates/snowflake/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/feast/templates/snowflake/bootstrap.py b/sdk/python/feast/templates/snowflake/bootstrap.py index 2224dc53596..9401447afca 100644 --- a/sdk/python/feast/templates/snowflake/bootstrap.py +++ b/sdk/python/feast/templates/snowflake/bootstrap.py @@ -1,6 +1,6 @@ import click -import snowflake.connector +import snowflake.connector from feast.file_utils import replace_str_in_file from feast.infra.utils.snowflake.snowflake_utils import ( execute_snowflake_statement, diff --git a/sdk/python/feast/templates/snowflake/feature_repo/__init__.py b/sdk/python/feast/templates/snowflake/feature_repo/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/feast/templates/snowflake/feature_repo/driver_repo.py b/sdk/python/feast/templates/snowflake/feature_repo/driver_repo.py index dd05dac8455..7526d096ef5 100644 --- a/sdk/python/feast/templates/snowflake/feature_repo/driver_repo.py +++ b/sdk/python/feast/templates/snowflake/feature_repo/driver_repo.py @@ -64,6 +64,7 @@ # Tags are user defined key/value pairs that are attached to each # feature view tags={"team": "driver_performance"}, + version="latest", ) # Define a request data source which encodes features / information only @@ -126,6 +127,7 @@ def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: online=True, source=driver_stats_push_source, # Changed from above tags={"team": "driver_performance"}, + version="latest", ) diff --git a/sdk/python/feast/templates/spark/feature_repo/example_repo.py b/sdk/python/feast/templates/spark/feature_repo/feature_definitions.py similarity index 97% rename from sdk/python/feast/templates/spark/feature_repo/example_repo.py rename to sdk/python/feast/templates/spark/feature_repo/feature_definitions.py index 8ad48f53fc4..e89d9b0fc1e 100644 --- a/sdk/python/feast/templates/spark/feature_repo/example_repo.py +++ b/sdk/python/feast/templates/spark/feature_repo/feature_definitions.py @@ -54,6 +54,7 @@ online=True, source=driver_hourly_stats, tags={}, + version="latest", ) customer_daily_profile_view = FeatureView( name="customer_daily_profile", @@ -67,6 +68,7 @@ online=True, source=customer_daily_profile, tags={}, + version="latest", ) driver_stats_fs = FeatureService( diff --git a/sdk/python/feast/transformation/factory.py b/sdk/python/feast/transformation/factory.py index 16d7a7570d5..a181b7dea69 100644 --- a/sdk/python/feast/transformation/factory.py +++ b/sdk/python/feast/transformation/factory.py @@ -7,6 +7,7 @@ "sql": "feast.transformation.sql_transformation.SQLTransformation", "spark_sql": "feast.transformation.spark_transformation.SparkTransformation", "spark": "feast.transformation.spark_transformation.SparkTransformation", + "flink": "feast.transformation.flink_transformation.FlinkTransformation", "ray": "feast.transformation.ray_transformation.RayTransformation", } diff --git a/sdk/python/feast/transformation/flink_transformation.py b/sdk/python/feast/transformation/flink_transformation.py new file mode 100644 index 00000000000..83b929c4c96 --- /dev/null +++ b/sdk/python/feast/transformation/flink_transformation.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from typing import Any, Callable, Optional, cast + +from feast.transformation.base import Transformation +from feast.transformation.mode import TransformationMode + + +class FlinkTransformation(Transformation): + """Transformation wrapper for Flink compute-engine UDFs. + + The UDF is expected to accept PyFlink Table objects and return a PyFlink + Table. + """ + + def __new__( + cls, + udf: Optional[Callable[..., Any]] = None, + udf_string: Optional[str] = None, + name: Optional[str] = None, + tags: Optional[dict[str, str]] = None, + description: str = "", + owner: str = "", + *args, + **kwargs, + ) -> "FlinkTransformation": + if udf is None and udf_string is None: + return cast("FlinkTransformation", object.__new__(cls)) + if udf is None: + raise ValueError("udf parameter cannot be None") + if udf_string is None: + raise ValueError("udf_string parameter cannot be None") + return cast( + "FlinkTransformation", + super(FlinkTransformation, cls).__new__( + cls, + mode=TransformationMode.FLINK, + udf=udf, + name=name, + udf_string=udf_string, + tags=tags, + description=description, + owner=owner, + ), + ) + + def __init__( + self, + udf: Optional[Callable[..., Any]] = None, + udf_string: Optional[str] = None, + name: Optional[str] = None, + tags: Optional[dict[str, str]] = None, + description: str = "", + owner: str = "", + *args, + **kwargs, + ) -> None: + if udf is None and udf_string is None: + return + if udf is None: + raise ValueError("udf parameter cannot be None") + if udf_string is None: + raise ValueError("udf_string parameter cannot be None") + super().__init__( + mode=TransformationMode.FLINK, + udf=udf, + name=name, + udf_string=udf_string, + tags=tags, + description=description, + owner=owner, + ) + + def transform(self, *inputs: Any) -> Any: + return self.udf(*inputs) + + def infer_features(self, *args, **kwargs) -> Any: + pass diff --git a/sdk/python/feast/transformation/mode.py b/sdk/python/feast/transformation/mode.py index 44d38d8e99c..bd6fdf22424 100644 --- a/sdk/python/feast/transformation/mode.py +++ b/sdk/python/feast/transformation/mode.py @@ -6,6 +6,7 @@ class TransformationMode(Enum): PANDAS = "pandas" SPARK_SQL = "spark_sql" SPARK = "spark" + FLINK = "flink" RAY = "ray" SQL = "sql" SUBSTRAIT = "substrait" diff --git a/sdk/python/feast/transformation/pandas_transformation.py b/sdk/python/feast/transformation/pandas_transformation.py index 6e073c30100..d14f240ebe8 100644 --- a/sdk/python/feast/transformation/pandas_transformation.py +++ b/sdk/python/feast/transformation/pandas_transformation.py @@ -132,9 +132,7 @@ def infer_features( def __eq__(self, other): if not isinstance(other, PandasTransformation): - raise TypeError( - "Comparisons should only involve PandasTransformation class objects." - ) + return False if ( self.udf_string != other.udf_string diff --git a/sdk/python/feast/transformation/python_transformation.py b/sdk/python/feast/transformation/python_transformation.py index 68e9eee95f6..1fd8390f9c3 100644 --- a/sdk/python/feast/transformation/python_transformation.py +++ b/sdk/python/feast/transformation/python_transformation.py @@ -143,9 +143,7 @@ def infer_features( def __eq__(self, other): if not isinstance(other, PythonTransformation): - raise TypeError( - "Comparisons should only involve PythonTransformation class objects." - ) + return False if ( self.udf_string != other.udf_string diff --git a/sdk/python/feast/transformation/ray_transformation.py b/sdk/python/feast/transformation/ray_transformation.py index b592ce8b0d7..1fe9f03fb0a 100644 --- a/sdk/python/feast/transformation/ray_transformation.py +++ b/sdk/python/feast/transformation/ray_transformation.py @@ -270,9 +270,7 @@ def infer_features( def __eq__(self, other): if not isinstance(other, RayTransformation): - raise TypeError( - "Comparisons should only involve RayTransformation class objects." - ) + return False if ( self.udf_string != other.udf_string diff --git a/sdk/python/feast/transformation/substrait_transformation.py b/sdk/python/feast/transformation/substrait_transformation.py index 476159cd003..bf821364304 100644 --- a/sdk/python/feast/transformation/substrait_transformation.py +++ b/sdk/python/feast/transformation/substrait_transformation.py @@ -133,9 +133,7 @@ def infer_features( def __eq__(self, other): if not isinstance(other, SubstraitTransformation): - raise TypeError( - "Comparisons should only involve SubstraitTransformation class objects." - ) + return False return ( self.substrait_plan == other.substrait_plan @@ -175,7 +173,14 @@ def from_ibis(cls, user_function, sources): import ibis import ibis.expr.datatypes as dt - from ibis_substrait.compiler.core import SubstraitCompiler + + try: + from ibis_substrait.compiler.core import SubstraitCompiler + except ImportError: + raise ImportError( + "Failed to use substrait transformation: 'ibis-substrait' package is not installed. " + "Install it with: `pip install ibis-substrait and only if https://github.com/ibis-project/ibis-substrait/issues/1309 issue is resolved." + ) compiler = SubstraitCompiler() diff --git a/sdk/python/feast/type_map.py b/sdk/python/feast/type_map.py index ebf6f0eae19..3dc174ab696 100644 --- a/sdk/python/feast/type_map.py +++ b/sdk/python/feast/type_map.py @@ -15,8 +15,10 @@ import decimal import json import logging +import re +import uuid as uuid_module from collections import defaultdict -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from typing import ( TYPE_CHECKING, Any, @@ -39,15 +41,29 @@ from feast.protos.feast.types.Value_pb2 import ( BoolList, + BoolSet, BytesList, + BytesSet, DoubleList, + DoubleSet, FloatList, + FloatSet, Int32List, + Int32Set, Int64List, + Int64Set, + Map, + MapKey, + MapList, + RepeatedValue, + ScalarMap, + ScalarMapEntry, StringList, + StringSet, + ZonedTimestamp, ) from feast.protos.feast.types.Value_pb2 import Value as ProtoValue -from feast.value_type import ListType, ValueType +from feast.value_type import ListType, SetType, ValueType if TYPE_CHECKING: import pyarrow @@ -58,7 +74,70 @@ logger = logging.getLogger(__name__) -def feast_value_type_to_python_type(field_value_proto: ProtoValue) -> Any: +def _zone_name(tzinfo: Optional[Any]) -> str: + """Return a storable zone string for a datetime's tzinfo. + + Prefers the IANA name (e.g. ``zoneinfo.ZoneInfo`` key) so DST is preserved; + falls back to a fixed-offset string (e.g. ``-07:00``). A naive datetime + (``tzinfo is None``) yields ``""``, which decodes back as UTC. + """ + if tzinfo is None: + return "" + key = getattr(tzinfo, "key", None) # zoneinfo.ZoneInfo + if key: + return key + name = str(tzinfo) + # zoneinfo prints as the key; pytz prints the name; offsets print as "UTC-07:00" + return name + + +def _zone_from_name(zone: str): + """Resolve a stored zone string back to a tzinfo. Empty → UTC.""" + if not zone: + return timezone.utc + try: + from zoneinfo import ZoneInfo + + return ZoneInfo(zone) + except Exception: + # Not an IANA name. It may be a fixed-offset string produced by + # ``_zone_name`` (e.g. "UTC", "UTC-07:00", "+05:30"); parse it so the + # original offset is preserved on round trips rather than silently + # shifting the wall-clock time to UTC. + offset = _fixed_offset_from_name(zone) + if offset is not None: + return offset + logger.warning("Could not resolve zone %r; decoding as UTC", zone) + return timezone.utc + + +def _fixed_offset_from_name(zone: str) -> Optional[timezone]: + """Parse a fixed-offset zone string into a ``timezone``. + + Accepts the forms ``_zone_name`` emits for offset-only tzinfos: a bare + ``UTC``/``GMT``, or an offset like ``UTC-07:00``, ``-07:00``, ``+05:30`` or + ``+0530``. Returns ``None`` if the string is not a recognizable offset. + """ + text = zone.strip() + if text in ("UTC", "GMT"): + return timezone.utc + match = re.fullmatch( + r"(?:UTC|GMT)?([+-])(\d{2}):?(\d{2})", + text, + ) + if not match: + return None + sign, hours, minutes = match.groups() + delta = timedelta(hours=int(hours), minutes=int(minutes)) + if sign == "-": + delta = -delta + return timezone(delta) + + +def feast_value_type_to_python_type( + field_value_proto: ProtoValue, + feature_type: Optional[ValueType] = None, +) -> Any: """ Converts field value Proto to Dict and returns each field's Feast Value Type value in their respective Python value. @@ -74,7 +153,50 @@ def feast_value_type_to_python_type(field_value_proto: ProtoValue) -> Any: return None val = getattr(field_value_proto, val_attr) - # If it's a _LIST type extract the list. + # Handle JSON types — stored as strings but returned as parsed Python objects + if val_attr == "json_val": + try: + return json.loads(val) + except (json.JSONDecodeError, TypeError): + return val + elif val_attr == "json_list_val": + result = [] + for v in val.val: + if isinstance(v, str): + try: + result.append(json.loads(v)) + except (json.JSONDecodeError, TypeError): + result.append(v) + else: + result.append(v) + return result + + # Handle nested collection types (list_val, set_val) + if val_attr in ("list_val", "set_val"): + return _handle_nested_collection_value(val) + + # Handle Struct types — stored using Map proto, returned as dicts + if val_attr == "struct_val": + return _handle_map_value(val) + elif val_attr == "struct_list_val": + return _handle_map_list_value(val) + + # Handle Map and MapList types FIRST (before generic list processing) + if val_attr == "map_val": + return _handle_map_value(val) + elif val_attr == "map_list_val": + return _handle_map_list_value(val) + elif val_attr == "scalar_map_val": + return _handle_scalar_map_value(val) + + # Zoned timestamp: a (instant, zone) message → tz-aware datetime in its own zone. + if val_attr == "zoned_timestamp_val": + if val.unix_timestamp == NULL_TIMESTAMP_INT_VALUE: + return None + tz = _zone_from_name(val.zone) + return datetime.fromtimestamp(val.unix_timestamp, tz=tz) + + # If it's a _LIST or _SET type extract the values. if hasattr(val, "val"): val = list(val.val) @@ -88,16 +210,130 @@ def feast_value_type_to_python_type(field_value_proto: ProtoValue) -> Any: ) for v in val ] + elif val_attr == "unix_timestamp_set_val": + val = set( + [ + ( + datetime.fromtimestamp(v, tz=timezone.utc) + if v != NULL_TIMESTAMP_INT_VALUE + else None + ) + for v in val + ] + ) elif val_attr == "unix_timestamp_val": val = ( datetime.fromtimestamp(val, tz=timezone.utc) if val != NULL_TIMESTAMP_INT_VALUE else None ) + # Convert _SET types to Python sets + elif val_attr.endswith("_set_val") and val_attr != "unix_timestamp_set_val": + val = set(val) + + # Convert UUID values to uuid.UUID objects + if val_attr in ("uuid_val", "time_uuid_val"): + return uuid_module.UUID(val) if isinstance(val, str) else val + if val_attr in ("uuid_list_val", "time_uuid_list_val"): + return [uuid_module.UUID(v) if isinstance(v, str) else v for v in val] + if val_attr in ("uuid_set_val", "time_uuid_set_val"): + return {uuid_module.UUID(v) if isinstance(v, str) else v for v in val} + + # Convert DECIMAL values to decimal.Decimal objects + if val_attr == "decimal_val": + return decimal.Decimal(val) if isinstance(val, str) else val + if val_attr == "decimal_list_val": + return [decimal.Decimal(v) if isinstance(v, str) else v for v in val] + if val_attr == "decimal_set_val": + return {decimal.Decimal(v) if isinstance(v, str) else v for v in val} + + # Backward compatibility: handle UUIDs stored as string_val/string_list_val with feature_type hint + if feature_type in (ValueType.UUID, ValueType.TIME_UUID) and isinstance(val, str): + return uuid_module.UUID(val) + if feature_type in (ValueType.UUID_LIST, ValueType.TIME_UUID_LIST) and isinstance( + val, list + ): + return [uuid_module.UUID(v) if isinstance(v, str) else v for v in val] + if feature_type in (ValueType.UUID_SET, ValueType.TIME_UUID_SET) and isinstance( + val, set + ): + return {uuid_module.UUID(v) if isinstance(v, str) else v for v in val} return val +def _handle_map_value(map_message) -> Dict[str, Any]: + """Handle Map proto message containing map val.""" + result = {} + + for key, value in map_message.val.items(): + # Recursively handle the Value message + result[key] = feast_value_type_to_python_type(value) + + return result + + +def _handle_map_list_value(map_list_message) -> List[Dict[str, Any]]: + """Handle MapList proto message containing repeated Map val.""" + result = [] + + for map_item in map_list_message.val: + # Handle each Map in the list + processed_map = _handle_map_value(map_item) + result.append(processed_map) + + return result + + +def _handle_nested_collection_value(repeated_value) -> List[Any]: + """Handle nested collection proto (RepeatedValue containing Values). + + Each inner Value is itself a list/set proto. We recursively convert + each inner Value to a Python list/set via feast_value_type_to_python_type. + """ + result = [] + for inner_value in repeated_value.val: + result.append(feast_value_type_to_python_type(inner_value)) + return result + + +def _map_key_to_python_value(map_key: MapKey) -> Any: + """Convert a MapKey proto to its Python equivalent.""" + key_attr = map_key.WhichOneof("key") + if key_attr is None: + return None + val = getattr(map_key, key_attr) + if key_attr in ("int32_key", "int64_key"): + return int(val) + if key_attr in ("float_key", "double_key"): + return float(val) + if key_attr == "bool_key": + return bool(val) + if key_attr == "unix_timestamp_key": + return ( + datetime.fromtimestamp(val, tz=timezone.utc) + if val != NULL_TIMESTAMP_INT_VALUE + else None + ) + if key_attr == "bytes_key": + return bytes(val) + if key_attr in ("uuid_key", "time_uuid_key"): + return uuid_module.UUID(val) + if key_attr == "decimal_key": + return decimal.Decimal(val) + return val + + +def _handle_scalar_map_value(value_map_message: ScalarMap) -> Dict[Any, Any]: + """Handle ScalarMap proto message (repeated ScalarMapEntry) → Python dict.""" + result: Dict[Any, Any] = {} + for entry in value_map_message.val: + key = _map_key_to_python_value(entry.key) + value = feast_value_type_to_python_type(entry.value) + result[key] = value + return result + + def feast_value_type_to_pandas_type(value_type: ValueType) -> Any: value_type_to_pandas_type: Dict[ValueType, str] = { ValueType.FLOAT: "float", @@ -108,8 +344,15 @@ def feast_value_type_to_pandas_type(value_type: ValueType) -> Any: ValueType.BYTES: "bytes", ValueType.BOOL: "bool", ValueType.UNIX_TIMESTAMP: "datetime64[ns]", + ValueType.UUID: "str", + ValueType.TIME_UUID: "str", + ValueType.DECIMAL: "object", } - if value_type.name.endswith("_LIST"): + if ( + value_type.name in ("MAP", "JSON", "STRUCT", "VALUE_LIST", "VALUE_SET") + or value_type.name.endswith("_LIST") + or value_type.name.endswith("_SET") + ): return "object" if value_type in value_type_to_pandas_type: return value_type_to_pandas_type[value_type] @@ -167,6 +410,8 @@ def python_type_to_feast_value_type( "datetime64[ns, utc]": ValueType.UNIX_TIMESTAMP, "date": ValueType.UNIX_TIMESTAMP, "category": ValueType.STRING, + "uuid": ValueType.UUID, + "decimal": ValueType.DECIMAL, } if type_name in type_map: @@ -178,6 +423,9 @@ def python_type_to_feast_value_type( actual_type = type(value).__name__.lower() if actual_type == "str": return ValueType.STRING + # Check if it's a dictionary (could be a Map) + elif actual_type == "dict": + return ValueType.MAP # If it's a different type wrapped in object, try to infer from the value elif actual_type in type_map: return type_map[actual_type] @@ -187,14 +435,18 @@ def python_type_to_feast_value_type( return ValueType[item_type.name + "_LIST"] if isinstance(value, (list, np.ndarray)): + # Check if it's a list of maps + if value and isinstance(value[0], dict): + return ValueType.MAP_LIST # if the value's type is "ndarray" and we couldn't infer from "value.dtype" # this is most probably array of "object", # so we need to iterate over objects and try to infer type of each item if not recurse: raise ValueError( f"Value type for field {name} is {type(value)} but " - f"recursion is not allowed. Array types can only be one level " - f"deep." + f"recursion is not allowed. Nested collection types cannot be " + f"inferred automatically; use an explicit Field dtype instead " + f"(e.g., dtype=Array(Array(Int32)))." ) # This is the final type which we infer from the list @@ -222,6 +474,47 @@ def python_type_to_feast_value_type( return ValueType.UNKNOWN return ValueType[common_item_value_type.name + "_LIST"] + # Check if it's a set (Set type) + if isinstance(value, set): + if not recurse: + raise ValueError( + f"Value type for field {name} is {type(value)} but " + f"recursion is not allowed. Set types can only be one level " + f"deep." + ) + + # Infer the type from set elements + common_set_item_type = None + for item in value: + if isinstance(item, ProtoValue): + current_set_item_type: ValueType = _proto_value_to_value_type(item) + else: + # Get the type from the current item, only one level deep + current_set_item_type = python_type_to_feast_value_type( + name=name, value=item, recurse=False + ) + # Validate whether the type stays consistent + if ( + common_set_item_type + and not common_set_item_type == current_set_item_type + ): + raise ValueError( + f"Set value type for field {name} is inconsistent. " + f"{common_set_item_type} different from " + f"{current_set_item_type}." + ) + common_set_item_type = current_set_item_type + if common_set_item_type is None: + return ValueType.UNKNOWN + return ValueType[common_set_item_type.name + "_SET"] + + # Check if it's a dictionary (Map type) + if isinstance(value, dict): + # Non-string keys require ScalarMap; string keys (or empty dict) use Map + if value and not isinstance(next(iter(value)), str): + return ValueType.SCALAR_MAP + return ValueType.MAP + raise ValueError( f"Value with native type {type_name} cannot be converted into Feast value type" ) @@ -275,6 +568,33 @@ def _convert_value_type_str_to_value_type(type_str: str) -> ValueType: "FLOAT_LIST": ValueType.FLOAT_LIST, "BOOL_LIST": ValueType.BOOL_LIST, "UNIX_TIMESTAMP_LIST": ValueType.UNIX_TIMESTAMP_LIST, + "MAP": ValueType.MAP, + "MAP_LIST": ValueType.MAP_LIST, + "JSON": ValueType.JSON, + "JSON_LIST": ValueType.JSON_LIST, + "STRUCT": ValueType.STRUCT, + "STRUCT_LIST": ValueType.STRUCT_LIST, + "BYTES_SET": ValueType.BYTES_SET, + "STRING_SET": ValueType.STRING_SET, + "INT32_SET": ValueType.INT32_SET, + "INT64_SET": ValueType.INT64_SET, + "DOUBLE_SET": ValueType.DOUBLE_SET, + "FLOAT_SET": ValueType.FLOAT_SET, + "BOOL_SET": ValueType.BOOL_SET, + "UNIX_TIMESTAMP_SET": ValueType.UNIX_TIMESTAMP_SET, + "UUID": ValueType.UUID, + "TIME_UUID": ValueType.TIME_UUID, + "UUID_LIST": ValueType.UUID_LIST, + "TIME_UUID_LIST": ValueType.TIME_UUID_LIST, + "UUID_SET": ValueType.UUID_SET, + "TIME_UUID_SET": ValueType.TIME_UUID_SET, + "VALUE_LIST": ValueType.VALUE_LIST, + "VALUE_SET": ValueType.VALUE_SET, + "DECIMAL": ValueType.DECIMAL, + "DECIMAL_LIST": ValueType.DECIMAL_LIST, + "DECIMAL_SET": ValueType.DECIMAL_SET, + "SCALAR_MAP": ValueType.SCALAR_MAP, + "ZONED_TIMESTAMP": ValueType.ZONED_TIMESTAMP, } return type_map.get(type_str, ValueType.STRING) @@ -306,6 +626,57 @@ def _type_err(item, dtype): ValueType.STRING_LIST: (StringList, "string_list_val", [np.str_, str]), ValueType.BOOL_LIST: (BoolList, "bool_list_val", [np.bool_, bool]), ValueType.BYTES_LIST: (BytesList, "bytes_list_val", [np.bytes_, bytes]), + ValueType.UUID_LIST: ( + StringList, + "uuid_list_val", + [np.str_, str, uuid_module.UUID], + ), + ValueType.TIME_UUID_LIST: ( + StringList, + "time_uuid_list_val", + [np.str_, str, uuid_module.UUID], + ), + ValueType.DECIMAL_LIST: ( + StringList, + "decimal_list_val", + [np.str_, str, decimal.Decimal], + ), +} + +PYTHON_SET_VALUE_TYPE_TO_PROTO_VALUE: Dict[ + ValueType, Tuple[SetType, str, List[Type]] +] = { + ValueType.FLOAT_SET: ( + FloatSet, + "float_set_val", + [np.float32, np.float64, float], + ), + ValueType.DOUBLE_SET: ( + DoubleSet, + "double_set_val", + [np.float64, np.float32, float], + ), + ValueType.INT32_SET: (Int32Set, "int32_set_val", [np.int64, np.int32, int]), + ValueType.INT64_SET: (Int64Set, "int64_set_val", [np.int64, np.int32, int]), + ValueType.UNIX_TIMESTAMP_SET: ( + Int64Set, + "unix_timestamp_set_val", + [np.datetime64, np.int64, np.int32, int, datetime, Timestamp], + ), + ValueType.STRING_SET: (StringSet, "string_set_val", [np.str_, str]), + ValueType.BOOL_SET: (BoolSet, "bool_set_val", [np.bool_, bool]), + ValueType.BYTES_SET: (BytesSet, "bytes_set_val", [np.bytes_, bytes]), + ValueType.UUID_SET: (StringSet, "uuid_set_val", [np.str_, str, uuid_module.UUID]), + ValueType.TIME_UUID_SET: ( + StringSet, + "time_uuid_set_val", + [np.str_, str, uuid_module.UUID], + ), + ValueType.DECIMAL_SET: ( + StringSet, + "decimal_set_val", + [np.str_, str, decimal.Decimal], + ), } PYTHON_SCALAR_VALUE_TYPE_TO_PROTO_VALUE: Dict[ @@ -331,6 +702,9 @@ def _type_err(item, dtype): ValueType.BYTES: ("bytes_val", lambda x: x, {bytes}), ValueType.IMAGE_BYTES: ("bytes_val", lambda x: x, {bytes}), ValueType.BOOL: ("bool_val", lambda x: x, {bool, np.bool_, int, np.int_}), + ValueType.UUID: ("uuid_val", lambda x: str(x), {str, uuid_module.UUID}), + ValueType.TIME_UUID: ("time_uuid_val", lambda x: str(x), {str, uuid_module.UUID}), + ValueType.DECIMAL: ("decimal_val", lambda x: str(x), {decimal.Decimal, str}), } @@ -358,158 +732,766 @@ def _python_datetime_to_int_timestamp( return int_timestamps -def _python_value_to_proto_value( +def _convert_timestamp_collection_to_proto( + values: List[Any], + proto_field: str, + proto_type: type, +) -> List[ProtoValue]: + """Convert timestamp collection values (list or set) to proto. + + Args: + values: List of timestamp collections to convert. + proto_field: The proto field name (e.g., 'unix_timestamp_list_val'). + proto_type: The proto type class (e.g., Int64List). + + Returns: + List of ProtoValue with converted timestamps. + """ + result = [] + for value in values: + if value is not None: + result.append( + ProtoValue( + **{ + proto_field: proto_type( + val=_python_datetime_to_int_timestamp(value) + ) + } # type: ignore + ) + ) + else: + result.append(ProtoValue()) + return result + + +def _convert_bool_collection_to_proto( + values: List[Any], + proto_field: str, + proto_type: type, +) -> List[ProtoValue]: + """Convert boolean collection values (list or set) to proto. + + ProtoValue does not support direct conversion of np.bool_, so we need to + explicitly convert each element to Python bool. + + Args: + values: List of boolean collections to convert. + proto_field: The proto field name (e.g., 'bool_list_val'). + proto_type: The proto type class (e.g., BoolList). + + Returns: + List of ProtoValue with converted booleans. + """ + result = [] + for value in values: + if value is not None: + result.append( + ProtoValue(**{proto_field: proto_type(val=[bool(e) for e in value])}) # type: ignore + ) + else: + result.append(ProtoValue()) + return result + + +def _validate_collection_item_types( + sample: Any, + valid_types: List[Type], + feast_value_type: ValueType, +) -> None: + """Validate that collection items match expected types. + + Args: + sample: A sample collection value to check. + valid_types: List of valid Python types for items. + feast_value_type: The Feast value type for error messages. + + Raises: + TypeError: If any item in sample is not a valid type. + """ + if sample is None: + return + if all(type(item) in valid_types for item in sample if item is not None): + return + + # to_numpy() upcasts INT32/INT64 with NULL to Float64 automatically + int_collection_types = [ + ValueType.INT32_LIST, + ValueType.INT64_LIST, + ValueType.INT32_SET, + ValueType.INT64_SET, + ] + for item in sample: + if item is None: + continue # None elements in STRING_LIST are replaced with ""; for other types they are dropped + if type(item) not in valid_types: + if feast_value_type in int_collection_types: + # Check if the float values are due to NULL upcast + if not any(np.isnan(i) for i in sample if isinstance(i, float)): + logger.error( + f"{feast_value_type.name} has NULL values. to_numpy() upcasts to Float64 automatically." + ) + raise _type_err(item, valid_types[0]) + + +def _python_set_to_proto_values( feast_value_type: ValueType, values: List[Any] ) -> List[ProtoValue]: """ - Converts a Python (native, pandas) value to a Feast Proto Value based - on a provided value type + Converts Python set values to Feast Proto Values. Args: - feast_value_type: The target value type - values: List of Values that will be converted + feast_value_type: The target set value type + values: List of set values that will be converted Returns: List of Feast Value Proto """ - # ToDo: make a better sample for type checks (more than one element) - sample = next(filter(_non_empty_value, values), None) # first not empty value + # Feature can be set but None is still valid + if feast_value_type not in PYTHON_SET_VALUE_TYPE_TO_PROTO_VALUE: + return [] - # Detect list type and handle separately - if "list" in feast_value_type.name.lower(): - # Feature can be list but None is still valid - if feast_value_type in PYTHON_LIST_VALUE_TYPE_TO_PROTO_VALUE: - proto_type, field_name, valid_types = PYTHON_LIST_VALUE_TYPE_TO_PROTO_VALUE[ - feast_value_type - ] + set_proto_type, set_field_name, set_valid_types = ( + PYTHON_SET_VALUE_TYPE_TO_PROTO_VALUE[feast_value_type] + ) - # Bytes to array type conversion - if isinstance(sample, (bytes, bytearray)): - # Bytes of an array containing elements of bytes not supported - if feast_value_type == ValueType.BYTES_LIST: - raise _type_err(sample, ValueType.BYTES_LIST) - - json_sample = json.loads(sample) - if isinstance(json_sample, list): - json_values = [json.loads(value) for value in values] - if feast_value_type == ValueType.BOOL_LIST: - json_values = [ - [bool(item) for item in list_item] - for list_item in json_values - ] - return [ - ProtoValue(**{field_name: proto_type(val=v)}) # type: ignore - for v in json_values - ] - raise _type_err(sample, valid_types[0]) - - if sample is not None and not all( - type(item) in valid_types for item in sample - ): - # to_numpy() in utils._convert_arrow_to_proto() upcasts values of type Array of INT32 or INT64 with NULL values to Float64 automatically. - for item in sample: - if type(item) not in valid_types: - if feast_value_type in [ - ValueType.INT32_LIST, - ValueType.INT64_LIST, - ]: - if not any(np.isnan(item) for item in sample): - logger.error( - "Array of Int32 or Int64 type has NULL values. to_numpy() upcasts to Float64 automatically." - ) - raise _type_err(item, valid_types[0]) - - if feast_value_type == ValueType.UNIX_TIMESTAMP_LIST: - return [ - ( - # ProtoValue does actually accept `np.int_` but the typing complains. - ProtoValue( - unix_timestamp_list_val=Int64List( - val=_python_datetime_to_int_timestamp(value) # type: ignore - ) - ) - if value is not None - else ProtoValue() - ) - for value in values + # Convert set to list for proto (proto doesn't have native set type) + def convert_set_to_list(value: Any) -> Any: + if value is None: + return None + if isinstance(value, set): + return list(value) + if isinstance(value, (list, tuple, np.ndarray)): + return list(set(value)) + return value + + converted_values = [convert_set_to_list(v) for v in values] + sample = next(filter(_non_empty_value, converted_values), None) + + # Bytes to array type conversion + if isinstance(sample, (bytes, bytearray)): + if feast_value_type == ValueType.BYTES_SET: + raise _type_err(sample, ValueType.BYTES_SET) + + json_sample = json.loads(sample) + if isinstance(json_sample, list): + json_values = [ + json.loads(value) if value is not None else None + for value in converted_values + ] + if feast_value_type == ValueType.BOOL_SET: + json_values = [ + [bool(item) for item in list_item] + if list_item is not None + else None + for list_item in json_values ] + return [ + ProtoValue(**{set_field_name: set_proto_type(val=v)}) # type: ignore[arg-type] + if v is not None + else ProtoValue() + for v in json_values + ] + raise _type_err(sample, set_valid_types[0]) + + # Validate item types using shared helper + _validate_collection_item_types(sample, set_valid_types, feast_value_type) + + # Handle special types using shared helpers + if feast_value_type == ValueType.UNIX_TIMESTAMP_SET: + return _convert_timestamp_collection_to_proto( + converted_values, "unix_timestamp_set_val", Int64Set + ) + if feast_value_type == ValueType.BOOL_SET: + return _convert_bool_collection_to_proto( + converted_values, set_field_name, set_proto_type + ) + + if feast_value_type in (ValueType.UUID_SET, ValueType.TIME_UUID_SET): + # uuid.UUID objects must be converted to str for StringSet proto. + return [ + ( + ProtoValue( + **{set_field_name: set_proto_type(val=[str(e) for e in value])} # type: ignore[arg-type, misc] + ) + if value is not None + else ProtoValue() + ) + for value in converted_values + ] + + if feast_value_type == ValueType.DECIMAL_SET: + # decimal.Decimal objects must be converted to str for StringSet proto. + return [ + ( + ProtoValue( + **{set_field_name: set_proto_type(val=[str(e) for e in value])} # type: ignore[arg-type, misc] + ) + if value is not None + else ProtoValue() + ) + for value in converted_values + ] + + # Generic set conversion + return [ + ProtoValue(**{set_field_name: set_proto_type(val=value)}) # type: ignore[arg-type] + if value is not None + else ProtoValue() + for value in converted_values + ] + + +# Per-type default values substituted for None elements inside list columns. +# Protobuf repeated fields do not accept None, so we replace with a +# type-appropriate zero/empty value. +_LIST_NONE_DEFAULTS: Dict[ValueType, Any] = { + ValueType.STRING_LIST: "", + ValueType.BYTES_LIST: b"", + ValueType.INT32_LIST: 0, + ValueType.INT64_LIST: 0, + ValueType.FLOAT_LIST: 0.0, + ValueType.DOUBLE_LIST: 0.0, + ValueType.BOOL_LIST: False, + ValueType.UNIX_TIMESTAMP_LIST: NULL_TIMESTAMP_INT_VALUE, + ValueType.UUID_LIST: "", + ValueType.TIME_UUID_LIST: "", + ValueType.DECIMAL_LIST: "", +} + + +def _sanitize_list_value(value: Any, feast_value_type: ValueType) -> Any: + """Convert ndarray to list and replace None elements with a type-appropriate default. + + Arrow/Athena may deserialize array columns as numpy.ndarray with object dtype + instead of plain Python lists. Protobuf repeated fields do not accept ndarrays + or None elements, so we normalise here before building proto messages. + """ + if isinstance(value, np.ndarray): + value = value.tolist() + none_default = _LIST_NONE_DEFAULTS.get(feast_value_type) + if none_default is not None and isinstance(value, list): + value = [none_default if v is None else v for v in value] + return value + + +def _convert_list_values_to_proto( + feast_value_type: ValueType, + values: List[Any], + sample: Any, +) -> List[ProtoValue]: + """Convert list-type values to proto. + + Args: + feast_value_type: The target list value type. + values: List of list values to convert. + sample: First non-empty value for type checking. + + Returns: + List of ProtoValue. + """ + if feast_value_type not in PYTHON_LIST_VALUE_TYPE_TO_PROTO_VALUE: + raise Exception(f"Unsupported list type: {feast_value_type}") + + proto_type, field_name, valid_types = PYTHON_LIST_VALUE_TYPE_TO_PROTO_VALUE[ + feast_value_type + ] + + values = [ + _sanitize_list_value(v, feast_value_type) if v is not None else v + for v in values + ] + if sample is not None: + sample = _sanitize_list_value(sample, feast_value_type) + + # Bytes to array type conversion + if isinstance(sample, (bytes, bytearray)): + if feast_value_type == ValueType.BYTES_LIST: + raise _type_err(sample, ValueType.BYTES_LIST) + + json_sample = json.loads(sample) + if isinstance(json_sample, list): + json_values = [json.loads(value) for value in values] if feast_value_type == ValueType.BOOL_LIST: - # ProtoValue does not support conversion of np.bool_ so we need to convert it to support np.bool_. - return [ - ( - ProtoValue( - **{field_name: proto_type(val=[bool(e) for e in value])} # type: ignore - ) - if value is not None - else ProtoValue() - ) - for value in values + json_values = [ + [bool(item) for item in list_item] for list_item in json_values ] return [ - ( - ProtoValue(**{field_name: proto_type(val=value)}) # type: ignore - if value is not None - else ProtoValue() - ) - for value in values + ProtoValue(**{field_name: proto_type(val=v)}) # type: ignore[arg-type] + for v in json_values ] + raise _type_err(sample, valid_types[0]) - # Handle scalar types below - else: - if sample is None: - # all input values are None - return [ProtoValue()] * len(values) - - if feast_value_type == ValueType.UNIX_TIMESTAMP: - int_timestamps = _python_datetime_to_int_timestamp(values) - # ProtoValue does actually accept `np.int_` but the typing complains. - return [ProtoValue(unix_timestamp_val=ts) for ts in int_timestamps] # type: ignore - - ( - field_name, - func, - valid_scalar_types, - ) = PYTHON_SCALAR_VALUE_TYPE_TO_PROTO_VALUE[feast_value_type] - if valid_scalar_types: - if (sample == 0 or sample == 0.0) and feast_value_type != ValueType.BOOL: - # Numpy convert 0 to int. However, in the feature view definition, the type of column may be a float. - # So, if value is 0, type validation must pass if scalar_types are either int or float. - allowed_types = {np.int64, int, np.float64, float, decimal.Decimal} - assert type(sample) in allowed_types, ( - f"Type `{type(sample)}` not in {allowed_types}" + # Validate item types using shared helper + _validate_collection_item_types(sample, valid_types, feast_value_type) + + # Handle special types using shared helpers + if feast_value_type == ValueType.UNIX_TIMESTAMP_LIST: + return _convert_timestamp_collection_to_proto( + values, "unix_timestamp_list_val", Int64List + ) + if feast_value_type == ValueType.BOOL_LIST: + return _convert_bool_collection_to_proto(values, field_name, proto_type) + + if feast_value_type in (ValueType.UUID_LIST, ValueType.TIME_UUID_LIST): + # uuid.UUID objects must be converted to str for StringList proto. + return [ + ( + ProtoValue( + **{field_name: proto_type(val=[str(e) for e in value])} # type: ignore[arg-type, misc] + ) + if value is not None + else ProtoValue() + ) + for value in values + ] + + if feast_value_type == ValueType.DECIMAL_LIST: + # decimal.Decimal objects must be converted to str for StringList proto. + return [ + ( + ProtoValue( + **{field_name: proto_type(val=[str(e) for e in value])} # type: ignore[arg-type, misc] ) + if value is not None + else ProtoValue() + ) + for value in values + ] + + # Generic list conversion + return [ + ProtoValue(**{field_name: proto_type(val=value)}) # type: ignore[arg-type] + if value is not None + else ProtoValue() + for value in values + ] + + +def _is_array_like(value: Any) -> bool: + """Return True if *value* is array-like (numpy array or any sized, + non-string, non-bytes container). Array-like values in a scalar + feature column cannot be mapped to a protobuf scalar field and are + therefore always treated as null.""" + return isinstance(value, np.ndarray) or ( + hasattr(value, "__len__") and not isinstance(value, (str, bytes)) + ) + + +def _convert_scalar_values_to_proto( + feast_value_type: ValueType, + values: List[Any], + sample: Any, +) -> List[ProtoValue]: + """Convert scalar-type values to proto. + + Args: + feast_value_type: The target scalar value type. + values: List of scalar values to convert. + sample: First non-empty value for type checking. + + Returns: + List of ProtoValue. + """ + if sample is None: + # All input values are None + return [ProtoValue()] * len(values) + + if feast_value_type == ValueType.UNIX_TIMESTAMP: + out: List[Any] = [None] * len(values) + clean_indices: List[int] = [] + clean_values: List[Any] = [] + for i, value in enumerate(values): + if _is_array_like(value) or value is None: + out[i] = ProtoValue() else: - assert type(sample) in valid_scalar_types, ( - f"Type `{type(sample)}` not in {valid_scalar_types}" + clean_indices.append(i) + clean_values.append(value) + if clean_values: + timestamps = _python_datetime_to_int_timestamp(clean_values) + for i, ts in zip(clean_indices, timestamps): + out[i] = ProtoValue(unix_timestamp_val=ts) # type: ignore + return out + + if feast_value_type == ValueType.ZONED_TIMESTAMP: + # Lossless zoned datetime: store the UTC instant plus the originating zone. + # Only datetime values are accepted; a naive datetime keeps zone="" (UTC). + out = [] + for value in values: + if _is_array_like(value) or value is None or pd.isnull(value): + out.append(ProtoValue()) + elif isinstance(value, datetime): + # A naive datetime is interpreted as UTC for the instant, but keeps + # zone="" (the "unzoned" sentinel, which decode maps back to UTC). + dt = ( + value + if value.tzinfo is not None + else value.replace(tzinfo=timezone.utc) ) - if feast_value_type == ValueType.BOOL: - # ProtoValue does not support conversion of np.bool_ so we need to convert it to support np.bool_. - return [ - ( + out.append( + ProtoValue( + zoned_timestamp_val=ZonedTimestamp( + unix_timestamp=int(dt.timestamp()), + zone=_zone_name(value.tzinfo), + ) + ) # type: ignore + ) + else: + raise TypeError( + f"ZONED_TIMESTAMP expects datetime values, got {type(value)}" + ) + return out + + field_name, func, valid_scalar_types = PYTHON_SCALAR_VALUE_TYPE_TO_PROTO_VALUE[ + feast_value_type + ] + + # Validate scalar types. The caller guarantees that *sample* is not + # array-like (array-like values are filtered out when picking the sample + # for scalar columns in python_values_to_proto_values). + if valid_scalar_types: + try: + is_zero = sample == 0 or sample == 0.0 + except (ValueError, TypeError): + is_zero = False + if is_zero and feast_value_type != ValueType.BOOL: + # Numpy converts 0 to int, but column type may be float + allowed_types = {np.int64, int, np.float64, float, decimal.Decimal} + assert type(sample) in allowed_types, ( + f"Type `{type(sample)}` not in {allowed_types}" + ) + else: + assert type(sample) in valid_scalar_types, ( + f"Type `{type(sample)}` not in {valid_scalar_types}" + ) + + # Handle BOOL specially due to np.bool_ conversion requirement + if feast_value_type == ValueType.BOOL: + out = [] + for value in values: + if _is_array_like(value): + # Array-like value in a scalar BOOL column: treat as null. + out.append(ProtoValue()) + elif not pd.isnull(value): + out.append( ProtoValue( **{ field_name: func( - bool(value) if type(value) is np.bool_ else value # type: ignore + bool(value) if type(value) is np.bool_ else value ) } + ) # type: ignore + ) + else: + out.append(ProtoValue()) + return out + + # Generic scalar conversion + out = [] + for value in values: + if isinstance(value, ProtoValue): + out.append(value) + elif _is_array_like(value): + # Array-like value in a scalar column: always treat as null. + # pd.isnull() is vectorised and would return an ndarray here, + # making `not pd.isnull(value)` raise ValueError. + out.append(ProtoValue()) + elif not pd.isnull(value): + out.append(ProtoValue(**{field_name: func(value)})) + else: + out.append(ProtoValue()) + return out + + +def _python_value_to_proto_value( + feast_value_type: ValueType, values: List[Any] +) -> List[ProtoValue]: + """ + Converts a Python (native, pandas) value to a Feast Proto Value based + on a provided value type. + + Args: + feast_value_type: The target value type + values: List of Values that will be converted + + Returns: + List of Feast Value Proto + """ + # Handle nested collection types (VALUE_LIST, VALUE_SET) + if feast_value_type in (ValueType.VALUE_LIST, ValueType.VALUE_SET): + return _convert_nested_collection_to_proto(feast_value_type, values) + + # Handle Map types + if feast_value_type == ValueType.MAP: + result = [] + for value in values: + if value is None: + result.append(ProtoValue()) + else: + if isinstance(value, str): + value = json.loads(value) + if not isinstance(value, dict): + raise TypeError( + f"Expected dict for MAP type, got {type(value).__name__}: {value!r}" ) - if not pd.isnull(value) - else ProtoValue() + result.append(ProtoValue(map_val=_python_dict_to_map_proto(value))) + return result + + if feast_value_type == ValueType.MAP_LIST: + result = [] + for value in values: + if value is None: + result.append(ProtoValue()) + else: + if isinstance(value, str): + value = json.loads(value) + if not isinstance(value, list): + raise TypeError( + f"Expected list for MAP_LIST type, got {type(value).__name__}: {value!r}" + ) + result.append( + ProtoValue(map_list_val=_python_list_to_map_list_proto(value)) ) - for value in values - ] - if feast_value_type in PYTHON_SCALAR_VALUE_TYPE_TO_PROTO_VALUE: - out = [] - for value in values: - if isinstance(value, ProtoValue): - out.append(value) - elif not pd.isnull(value): - out.append(ProtoValue(**{field_name: func(value)})) + return result + + if feast_value_type == ValueType.SCALAR_MAP: + result = [] + for value in values: + if value is None: + result.append(ProtoValue()) + else: + if not isinstance(value, dict): + raise TypeError( + f"Expected dict for SCALAR_MAP type, got {type(value).__name__}: {value!r}" + ) + result.append( + ProtoValue(scalar_map_val=_python_dict_to_scalar_map_proto(value)) + ) + return result + + # Handle JSON type — serialize Python objects as JSON strings + if feast_value_type == ValueType.JSON: + result = [] + for value in values: + if value is None: + result.append(ProtoValue()) + else: + if isinstance(value, str): + try: + json.loads(value) + except (json.JSONDecodeError, TypeError) as e: + raise ValueError( + f"Invalid JSON string for JSON type: {e}" + ) from e + json_str = value else: - out.append(ProtoValue()) - return out + json_str = json.dumps(value) + result.append(ProtoValue(json_val=json_str)) + return result + + if feast_value_type == ValueType.JSON_LIST: + result = [] + for value in values: + if value is None: + result.append(ProtoValue()) + else: + json_strings = [] + for v in value: + if isinstance(v, str): + try: + json.loads(v) + except (json.JSONDecodeError, TypeError) as e: + raise ValueError( + f"Invalid JSON string in JSON_LIST: {e}" + ) from e + json_strings.append(v) + else: + json_strings.append(json.dumps(v)) + result.append(ProtoValue(json_list_val=StringList(val=json_strings))) + return result + + # Handle Struct type — reuses Map proto for storage + if feast_value_type == ValueType.STRUCT: + result = [] + for value in values: + if value is None: + result.append(ProtoValue()) + else: + if isinstance(value, str): + value = json.loads(value) + if not isinstance(value, dict): + value = ( + dict(value) + if hasattr(value, "items") + else {"_value": str(value)} + ) + result.append(ProtoValue(struct_val=_python_dict_to_map_proto(value))) + return result + + if feast_value_type == ValueType.STRUCT_LIST: + result = [] + for value in values: + if value is None: + result.append(ProtoValue()) + else: + if isinstance(value, str): + value = json.loads(value) + result.append( + ProtoValue(struct_list_val=_python_list_to_map_list_proto(value)) + ) + return result + + # Get sample for type checking + sample = next(filter(_non_empty_value, values), None) + + # Dispatch to appropriate converter based on type category + type_name_lower = feast_value_type.name.lower() + + if "list" in type_name_lower: + return _convert_list_values_to_proto(feast_value_type, values, sample) + + if "set" in type_name_lower: + return _python_set_to_proto_values(feast_value_type, values) + + # Scalar types — pick a sample that is not array-like so that the type + # validation in _convert_scalar_values_to_proto always receives a plain + # scalar (array-like values in a scalar column are treated as null). + if ( + feast_value_type in PYTHON_SCALAR_VALUE_TYPE_TO_PROTO_VALUE + or feast_value_type == ValueType.UNIX_TIMESTAMP + or feast_value_type == ValueType.ZONED_TIMESTAMP + ): + scalar_sample = next( + (v for v in values if _non_empty_value(v) and not _is_array_like(v)), + None, + ) + return _convert_scalar_values_to_proto(feast_value_type, values, scalar_sample) + + raise Exception(f"Unsupported data type: {feast_value_type}") + + +def _convert_nested_collection_to_proto( + feast_value_type: ValueType, values: List[Any] +) -> List[ProtoValue]: + """Convert nested collection values (list-of-lists, list-of-sets, etc.) to proto.""" + val_attr = "list_val" if feast_value_type == ValueType.VALUE_LIST else "set_val" + + result = [] + for value in values: + if value is None: + result.append(ProtoValue()) + else: + inner_values = [] + for inner_collection in value: + if inner_collection is None: + inner_values.append(ProtoValue()) + else: + inner_list = list(inner_collection) + if len(inner_list) == 0: + # Empty inner collection: store as empty ProtoValue + inner_values.append(ProtoValue()) + elif any( + isinstance(item, (list, set, tuple, np.ndarray)) + for item in inner_list + ): + # Deeper nesting (3+ levels): recurse using VALUE_LIST + inner_proto = _convert_nested_collection_to_proto( + ValueType.VALUE_LIST, [inner_list] + ) + inner_values.append(inner_proto[0]) + else: + # Leaf level: wrap as a single list-typed Value + proto_vals = python_values_to_proto_values( + [inner_list], ValueType.UNKNOWN + ) + inner_values.append(proto_vals[0]) + repeated = RepeatedValue(val=inner_values) + proto = ProtoValue() + getattr(proto, val_attr).CopyFrom(repeated) + result.append(proto) + return result + + +def _python_dict_to_map_proto(python_dict: Dict[str, Any]) -> Map: + """Convert a Python dictionary to a Map proto message.""" + map_proto = Map() + for key, value in python_dict.items(): + # Handle None values explicitly + if value is None: + map_proto.val[key].CopyFrom( + ProtoValue() + ) # Empty ProtoValue represents None + continue + + if isinstance(value, dict): + # Nested map + nested_map_proto = _python_dict_to_map_proto(value) + map_proto.val[key].CopyFrom(ProtoValue(map_val=nested_map_proto)) + elif isinstance(value, list) and value and isinstance(value[0], dict): + # List of maps (MapList) + map_list_proto = _python_list_to_map_list_proto(value) + map_proto.val[key].CopyFrom(ProtoValue(map_list_val=map_list_proto)) + else: + # Handle scalar values and regular lists + # Let python_values_to_proto_values infer the type + proto_values = python_values_to_proto_values([value], ValueType.UNKNOWN) + map_proto.val[key].CopyFrom(proto_values[0]) + return map_proto - raise Exception(f"Unsupported data type: ${str(type(values[0]))}") + +def _python_list_to_map_list_proto(python_list: List[Dict[str, Any]]) -> MapList: + """Convert a Python list of dictionaries to a MapList proto message.""" + map_list_proto = MapList() + + for item in python_list: + if isinstance(item, dict): + map_proto = _python_dict_to_map_proto(item) + map_list_proto.val.append(map_proto) + else: + raise ValueError(f"MapList can only contain dictionaries, got {type(item)}") + + return map_list_proto + + +def _python_value_to_map_key_proto(key: Any) -> MapKey: + """Convert a Python value to a MapKey proto for use in ScalarMap entries.""" + # bool must be checked before int since bool is a subclass of int + if isinstance(key, (bool, np.bool_)): + return MapKey(bool_key=bool(key)) + if isinstance(key, np.int32): + return MapKey(int32_key=int(key)) + if isinstance(key, (int, np.integer)): + return MapKey(int64_key=int(key)) + if isinstance(key, np.float32): + return MapKey(float_key=float(key)) + if isinstance(key, (float, np.floating)): + return MapKey(double_key=float(key)) + if isinstance(key, uuid_module.UUID): + return MapKey(uuid_key=str(key)) + if isinstance(key, decimal.Decimal): + return MapKey(decimal_key=str(key)) + if isinstance(key, bytes): + return MapKey(bytes_key=key) + if isinstance(key, (datetime, pd.Timestamp)): + ts = int(pd.Timestamp(key).timestamp()) + return MapKey(unix_timestamp_key=ts) + raise TypeError( + f"Unsupported key type for SCALAR_MAP: {type(key).__name__}. " + "Supported non-string key types: int, float, bool, uuid.UUID, " + "decimal.Decimal, bytes, datetime." + ) + + +def _python_dict_to_scalar_map_proto(python_dict: Dict[Any, Any]) -> ScalarMap: + """Convert a Python dictionary with non-string keys to a ScalarMap proto.""" + value_map_proto = ScalarMap() + for key, value in python_dict.items(): + map_key = _python_value_to_map_key_proto(key) + if value is None: + value_proto = ProtoValue() + else: + value_proto = python_values_to_proto_values([value], ValueType.UNKNOWN)[0] + value_map_proto.val.append(ScalarMapEntry(key=map_key, value=value_proto)) + return value_map_proto def python_values_to_proto_values( @@ -528,6 +1510,8 @@ def python_values_to_proto_values( value_type = python_type_to_feast_value_type("", sample) if value_type == ValueType.UNKNOWN: + if all(v is None for v in values): + return [ProtoValue() for _ in values] raise TypeError("Couldn't infer value type from empty value") proto_values = _python_value_to_proto_value(value_type, values) @@ -548,6 +1532,7 @@ def python_values_to_proto_values( "string_val": ValueType.STRING, "bytes_val": ValueType.BYTES, "bool_val": ValueType.BOOL, + "unix_timestamp_val": ValueType.UNIX_TIMESTAMP, "int32_list_val": ValueType.INT32_LIST, "int64_list_val": ValueType.INT64_LIST, "double_list_val": ValueType.DOUBLE_LIST, @@ -555,6 +1540,33 @@ def python_values_to_proto_values( "string_list_val": ValueType.STRING_LIST, "bytes_list_val": ValueType.BYTES_LIST, "bool_list_val": ValueType.BOOL_LIST, + "unix_timestamp_list_val": ValueType.UNIX_TIMESTAMP_LIST, + "map_val": ValueType.MAP, + "map_list_val": ValueType.MAP_LIST, + "json_val": ValueType.JSON, + "json_list_val": ValueType.JSON_LIST, + "struct_val": ValueType.STRUCT, + "struct_list_val": ValueType.STRUCT_LIST, + "list_val": ValueType.VALUE_LIST, + "set_val": ValueType.VALUE_SET, + "int32_set_val": ValueType.INT32_SET, + "int64_set_val": ValueType.INT64_SET, + "double_set_val": ValueType.DOUBLE_SET, + "float_set_val": ValueType.FLOAT_SET, + "string_set_val": ValueType.STRING_SET, + "bytes_set_val": ValueType.BYTES_SET, + "bool_set_val": ValueType.BOOL_SET, + "unix_timestamp_set_val": ValueType.UNIX_TIMESTAMP_SET, + "uuid_set_val": ValueType.UUID_SET, + "time_uuid_set_val": ValueType.TIME_UUID_SET, + "uuid_val": ValueType.UUID, + "time_uuid_val": ValueType.TIME_UUID, + "uuid_list_val": ValueType.UUID_LIST, + "time_uuid_list_val": ValueType.TIME_UUID_LIST, + "decimal_val": ValueType.DECIMAL, + "decimal_list_val": ValueType.DECIMAL_LIST, + "decimal_set_val": ValueType.DECIMAL_SET, + "scalar_map_val": ValueType.SCALAR_MAP, } VALUE_TYPE_TO_PROTO_VALUE_MAP: Dict[ValueType, str] = { @@ -582,10 +1594,20 @@ def pa_to_feast_value_type(pa_type_as_str: str) -> ValueType: is_list = False if pa_type_as_str.startswith("list", "") + inner_str = pa_type_as_str[len("list ValueType: "BOOL": ValueType.BOOL, "BOOLEAN": ValueType.BOOL, # legacy sql data type "NULL": ValueType.NULL, + "JSON": ValueType.JSON, + "STRUCT": ValueType.STRUCT, + "RECORD": ValueType.STRUCT, } value_type = type_map.get(bq_type_as_str, ValueType.STRING) @@ -655,6 +1680,7 @@ def mssql_to_feast_value_type(mssql_type_as_str: str) -> ValueType: "nchar": ValueType.STRING, "nvarchar": ValueType.STRING, "nvarchar(max)": ValueType.STRING, + "json": ValueType.JSON, "real": ValueType.FLOAT, "smallint": ValueType.INT32, "tinyint": ValueType.INT32, @@ -668,6 +1694,61 @@ def mssql_to_feast_value_type(mssql_type_as_str: str) -> ValueType: return type_map[mssql_type_as_str.lower()] +def oracle_to_feast_value_type(oracle_type_as_str: str) -> ValueType: + """Convert an Oracle/ibis type string to a Feast ValueType. + + Handles type strings returned by ibis schema introspection for the + Oracle backend (e.g. "int64", "float64", "string", "timestamp", "decimal") + as well as Oracle native type names. + """ + type_str = oracle_type_as_str.lower().strip() + + # Handle parameterized types like "decimal(10, 2)" + if "(" in type_str: + type_str = type_str.split("(")[0].strip() + + type_map: Dict[str, ValueType] = { + # Ibis types returned by Oracle backend + "int8": ValueType.INT32, + "int16": ValueType.INT32, + "int32": ValueType.INT32, + "int64": ValueType.INT64, + "float16": ValueType.FLOAT, + "float32": ValueType.FLOAT, + "float64": ValueType.DOUBLE, + "decimal": ValueType.DOUBLE, + "string": ValueType.STRING, + "binary": ValueType.BYTES, + "boolean": ValueType.BOOL, + "timestamp": ValueType.UNIX_TIMESTAMP, + "date": ValueType.UNIX_TIMESTAMP, + "time": ValueType.UNIX_TIMESTAMP, + "null": ValueType.NULL, + # Oracle native type names + "number": ValueType.DOUBLE, + "varchar2": ValueType.STRING, + "nvarchar2": ValueType.STRING, + "char": ValueType.STRING, + "nchar": ValueType.STRING, + "clob": ValueType.STRING, + "nclob": ValueType.STRING, + "blob": ValueType.BYTES, + "raw": ValueType.BYTES, + "long raw": ValueType.BYTES, + "long": ValueType.STRING, + "integer": ValueType.INT32, + "smallint": ValueType.INT32, + "float": ValueType.DOUBLE, + "double precision": ValueType.DOUBLE, + "real": ValueType.FLOAT, + "binary_float": ValueType.FLOAT, + "binary_double": ValueType.DOUBLE, + "interval": ValueType.UNIX_TIMESTAMP, + } + + return type_map.get(type_str, ValueType.STRING) + + def pa_to_mssql_type(pa_type: "pyarrow.DataType") -> str: # PyArrow types: https://arrow.apache.org/docs/python/api/datatypes.html # MS Sql types: https://docs.microsoft.com/en-us/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver16 @@ -684,6 +1765,13 @@ def pa_to_mssql_type(pa_type: "pyarrow.DataType") -> str: if pa_type_as_str.startswith("decimal"): return pa_type_as_str + if pa_type_as_str.startswith("map<"): + return "nvarchar(max)" + if pa_type_as_str == "large_string": + return "nvarchar(max)" + if pa_type_as_str.startswith("struct<") or pa_type_as_str.startswith("struct{"): + return "nvarchar(max)" + # We have to take into account how arrow types map to parquet types as well. # For example, null type maps to int32 in parquet, so we have to use int4 in Redshift. # Other mappings have also been adjusted accordingly. @@ -724,7 +1812,8 @@ def redshift_to_feast_value_type(redshift_type_as_str: str) -> ValueType: "varchar": ValueType.STRING, "timestamp": ValueType.UNIX_TIMESTAMP, "timestamptz": ValueType.UNIX_TIMESTAMP, - "super": ValueType.BYTES, + "super": ValueType.MAP, + "json": ValueType.JSON, # skip date, geometry, hllsketch, time, timetz } @@ -745,6 +1834,10 @@ def snowflake_type_to_feast_value_type(snowflake_type: str) -> ValueType: "TIMESTAMP_TZ": ValueType.UNIX_TIMESTAMP, "TIMESTAMP_LTZ": ValueType.UNIX_TIMESTAMP, "TIMESTAMP_NTZ": ValueType.UNIX_TIMESTAMP, + "VARIANT": ValueType.MAP, + "OBJECT": ValueType.MAP, + "ARRAY": ValueType.STRING_LIST, + "JSON": ValueType.JSON, } return type_map[snowflake_type] @@ -767,6 +1860,15 @@ def _convert_value_name_to_snowflake_udf(value_name: str, project_name: str) -> "FLOAT_LIST": f"feast_{project_name}_snowflake_array_float_to_list_double_proto", "BOOL_LIST": f"feast_{project_name}_snowflake_array_boolean_to_list_bool_proto", "UNIX_TIMESTAMP_LIST": f"feast_{project_name}_snowflake_array_timestamp_to_list_unix_timestamp_proto", + "UUID": f"feast_{project_name}_snowflake_varchar_to_string_proto", + "TIME_UUID": f"feast_{project_name}_snowflake_varchar_to_string_proto", + "UUID_LIST": f"feast_{project_name}_snowflake_array_varchar_to_list_string_proto", + "TIME_UUID_LIST": f"feast_{project_name}_snowflake_array_varchar_to_list_string_proto", + "UUID_SET": f"feast_{project_name}_snowflake_array_varchar_to_list_string_proto", + "TIME_UUID_SET": f"feast_{project_name}_snowflake_array_varchar_to_list_string_proto", + "DECIMAL": f"feast_{project_name}_snowflake_varchar_to_string_proto", + "DECIMAL_LIST": f"feast_{project_name}_snowflake_array_varchar_to_list_string_proto", + "DECIMAL_SET": f"feast_{project_name}_snowflake_array_varchar_to_list_string_proto", } return name_map[value_name].upper() @@ -791,6 +1893,15 @@ def pa_to_redshift_value_type(pa_type: "pyarrow.DataType") -> str: if pa_type_as_str.startswith("list"): return "super" + if pa_type_as_str.startswith("map<"): + return "super" + + if pa_type_as_str == "large_string": + return "super" + + if pa_type_as_str.startswith("struct<"): + return "super" + # We have to take into account how arrow types map to parquet types as well. # For example, null type maps to int32 in parquet, so we have to use int4 in Redshift. # Other mappings have also been adjusted accordingly. @@ -827,8 +1938,7 @@ def _non_empty_value(value: Any) -> bool: def spark_to_feast_value_type(spark_type_as_str: str) -> ValueType: - # TODO not all spark types are convertible - # Current non-convertible types: interval, map, struct, structfield, binary + # Current non-convertible types: interval, struct, structfield, binary type_map: Dict[str, ValueType] = { "null": ValueType.UNKNOWN, "byte": ValueType.BYTES, @@ -854,14 +1964,24 @@ def spark_to_feast_value_type(spark_type_as_str: str) -> ValueType: "array": ValueType.UNIX_TIMESTAMP_LIST, "array": ValueType.UNIX_TIMESTAMP_LIST, } - if spark_type_as_str.startswith("decimal"): - spark_type_as_str = "decimal" - if spark_type_as_str.startswith("array Iterator[np.dtype]: @@ -888,6 +2008,12 @@ def arrow_to_pg_type(t_str: str) -> str: try: if t_str.startswith("timestamp") or t_str.startswith("datetime"): return "timestamptz" if "tz=" in t_str else "timestamp" + if t_str.startswith("map<"): + return "jsonb" + if t_str == "large_string": + return "jsonb" + if t_str.startswith("struct<") or t_str.startswith("struct{"): + return "jsonb" return { "null": "null", "bool": "boolean", @@ -921,7 +2047,7 @@ def pg_type_to_feast_value_type(type_str: str) -> ValueType: "bigint": ValueType.INT64, "smallint": ValueType.INT32, "integer": ValueType.INT32, - "real": ValueType.DOUBLE, + "real": ValueType.FLOAT, "double precision": ValueType.DOUBLE, "boolean[]": ValueType.BOOL_LIST, "bytea[]": ValueType.BYTES_LIST, @@ -932,7 +2058,7 @@ def pg_type_to_feast_value_type(type_str: str) -> ValueType: "text[]": ValueType.STRING_LIST, "character[]": ValueType.STRING_LIST, "bigint[]": ValueType.INT64_LIST, - "real[]": ValueType.DOUBLE_LIST, + "real[]": ValueType.FLOAT_LIST, "double precision[]": ValueType.DOUBLE_LIST, "character": ValueType.STRING, "character varying": ValueType.STRING, @@ -946,8 +2072,12 @@ def pg_type_to_feast_value_type(type_str: str) -> ValueType: "timestamp with time zone[]": ValueType.UNIX_TIMESTAMP_LIST, "numeric[]": ValueType.DOUBLE_LIST, "numeric": ValueType.DOUBLE, - "uuid": ValueType.STRING, - "uuid[]": ValueType.STRING_LIST, + "uuid": ValueType.UUID, + "uuid[]": ValueType.UUID_LIST, + "json": ValueType.MAP, + "jsonb": ValueType.MAP, + "json[]": ValueType.MAP_LIST, + "jsonb[]": ValueType.MAP_LIST, } value = ( type_map[type_str.lower()] @@ -981,7 +2111,28 @@ def feast_value_type_to_pa( ValueType.BYTES_LIST: pyarrow.list_(pyarrow.binary()), ValueType.BOOL_LIST: pyarrow.list_(pyarrow.bool_()), ValueType.UNIX_TIMESTAMP_LIST: pyarrow.list_(pyarrow.timestamp(timestamp_unit)), + ValueType.MAP: pyarrow.map_(pyarrow.string(), pyarrow.string()), + ValueType.MAP_LIST: pyarrow.list_( + pyarrow.map_(pyarrow.string(), pyarrow.string()) + ), + ValueType.JSON: pyarrow.large_string(), + ValueType.JSON_LIST: pyarrow.list_(pyarrow.large_string()), + ValueType.STRUCT: pyarrow.struct([]), + ValueType.STRUCT_LIST: pyarrow.list_(pyarrow.struct([])), + # Placeholder: inner type is unknown from ValueType alone. + # Callers needing accurate inner types should use from_feast_to_pyarrow_type() with a FeastType. + ValueType.VALUE_LIST: pyarrow.list_(pyarrow.list_(pyarrow.string())), + ValueType.VALUE_SET: pyarrow.list_(pyarrow.list_(pyarrow.string())), ValueType.NULL: pyarrow.null(), + ValueType.UUID: pyarrow.string(), + ValueType.TIME_UUID: pyarrow.string(), + ValueType.UUID_LIST: pyarrow.list_(pyarrow.string()), + ValueType.TIME_UUID_LIST: pyarrow.list_(pyarrow.string()), + ValueType.UUID_SET: pyarrow.list_(pyarrow.string()), + ValueType.TIME_UUID_SET: pyarrow.list_(pyarrow.string()), + ValueType.DECIMAL: pyarrow.string(), + ValueType.DECIMAL_LIST: pyarrow.list_(pyarrow.string()), + ValueType.DECIMAL_SET: pyarrow.list_(pyarrow.string()), } return type_map[feast_type] @@ -1047,23 +2198,62 @@ def pg_type_code_to_arrow(code: int) -> str: def athena_to_feast_value_type(athena_type_as_str: str) -> ValueType: # Type names from https://docs.aws.amazon.com/athena/latest/ug/data-types.html + athena_type = athena_type_as_str.lower().strip() + if athena_type.startswith("array"): + inner_type_match = re.search(r"(?:<|\[)(.+)(?:>|\])", athena_type) + if inner_type_match: + inner_type = inner_type_match.group(1).strip() + inner_feast_type = athena_to_feast_value_type(inner_type) + + list_mapping = { + ValueType.BYTES: ValueType.BYTES_LIST, + ValueType.STRING: ValueType.STRING_LIST, + ValueType.INT32: ValueType.INT32_LIST, + ValueType.INT64: ValueType.INT64_LIST, + ValueType.DOUBLE: ValueType.DOUBLE_LIST, + ValueType.FLOAT: ValueType.FLOAT_LIST, + ValueType.BOOL: ValueType.BOOL_LIST, + ValueType.UNIX_TIMESTAMP: ValueType.UNIX_TIMESTAMP_LIST, + ValueType.MAP: ValueType.MAP_LIST, + ValueType.JSON: ValueType.JSON_LIST, + ValueType.STRUCT: ValueType.STRUCT_LIST, + ValueType.UUID: ValueType.UUID_LIST, + ValueType.DECIMAL: ValueType.DECIMAL_LIST, + } + return list_mapping.get(inner_feast_type, ValueType.VALUE_LIST) + return ValueType.VALUE_LIST + + base_type = re.split(r"[(<\[]", athena_type)[0].strip() + + if "timestamp" in base_type or "time" in base_type or "date" in base_type: + return ValueType.UNIX_TIMESTAMP + type_map = { - "null": ValueType.UNKNOWN, + "null": ValueType.UNKNOWN, # There is a null type, but this preserves backwards compat "boolean": ValueType.BOOL, "tinyint": ValueType.INT32, "smallint": ValueType.INT32, "int": ValueType.INT32, + "integer": ValueType.INT32, "bigint": ValueType.INT64, "double": ValueType.DOUBLE, "float": ValueType.FLOAT, + "real": ValueType.FLOAT, + "decimal": ValueType.DECIMAL, "binary": ValueType.BYTES, + "varbinary": ValueType.BYTES, "char": ValueType.STRING, "varchar": ValueType.STRING, "string": ValueType.STRING, - "timestamp": ValueType.UNIX_TIMESTAMP, - # skip date,decimal,array,map,struct + "json": ValueType.JSON, + "struct": ValueType.STRUCT, + "row": ValueType.STRUCT, + "map": ValueType.MAP, + "uuid": ValueType.UUID, + "ipaddress": ValueType.STRING, } - return type_map[athena_type_as_str.lower()] + + return type_map.get(base_type, ValueType.UNKNOWN) def pa_to_athena_value_type(pa_type: "pyarrow.DataType") -> str: @@ -1079,6 +2269,18 @@ def pa_to_athena_value_type(pa_type: "pyarrow.DataType") -> str: if pa_type_as_str.startswith("python_values_to_proto_values"): return pa_type_as_str + if pa_type_as_str.startswith("list"): + return "array" + + if pa_type_as_str.startswith("map<"): + return "string" + + if pa_type_as_str == "large_string": + return "string" + + if pa_type_as_str.startswith("struct<"): + return "string" + # We have to take into account how arrow types map to parquet types as well. # For example, null type maps to int32 in parquet, so we have to use int4 in Redshift. # Other mappings have also been adjusted accordingly. @@ -1120,7 +2322,7 @@ def cb_columnar_type_to_feast_value_type(type_str: str) -> ValueType: "object": ValueType.UNKNOWN, "array": ValueType.UNKNOWN, "multiset": ValueType.UNKNOWN, - "uuid": ValueType.STRING, + "uuid": ValueType.UUID, } value = ( type_map[type_str.lower()] @@ -1132,6 +2334,35 @@ def cb_columnar_type_to_feast_value_type(type_str: str) -> ValueType: return value +def mongodb_to_feast_value_type(type_str: str) -> ValueType: + """Map a Python type string (as inferred from pymongo documents) to a Feast ValueType. + + The type strings are produced by + ``feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb_source._infer_python_type_str``. + Unrecognised strings are mapped to ``ValueType.UNKNOWN``. + """ + type_map: Dict[str, ValueType] = { + "str": ValueType.STRING, + "string": ValueType.STRING, + "int": ValueType.INT64, + "int64": ValueType.INT64, + "float": ValueType.DOUBLE, + "float64": ValueType.DOUBLE, + "bool": ValueType.BOOL, + "bytes": ValueType.BYTES, + "datetime": ValueType.UNIX_TIMESTAMP, + "list": ValueType.UNKNOWN, + "dict": ValueType.UNKNOWN, + "list[str]": ValueType.STRING_LIST, + "list[int]": ValueType.INT64_LIST, + "list[float]": ValueType.DOUBLE_LIST, + "list[bool]": ValueType.BOOL_LIST, + "list[bytes]": ValueType.BYTES_LIST, + "list[datetime]": ValueType.UNIX_TIMESTAMP_LIST, + } + return type_map.get(type_str, ValueType.UNKNOWN) + + def convert_scalar_column( series: pd.Series, value_type: ValueType, target_pandas_type: str ) -> pd.Series: @@ -1146,8 +2377,12 @@ def convert_scalar_column( return series.astype("boolean") elif value_type == ValueType.STRING: return series.astype("string") + elif value_type in [ValueType.UUID, ValueType.TIME_UUID]: + return series.astype("string") elif value_type == ValueType.UNIX_TIMESTAMP: return pd.to_datetime(series, unit="s", errors="coerce") + elif value_type in (ValueType.JSON, ValueType.STRUCT, ValueType.MAP): + return series else: return series.astype(target_pandas_type) @@ -1163,6 +2398,18 @@ def convert_array_column(series: pd.Series, value_type: ValueType) -> pd.Series: ValueType.STRING_LIST: object, ValueType.BYTES_LIST: object, ValueType.UNIX_TIMESTAMP_LIST: "datetime64[s]", + ValueType.UUID_LIST: object, + ValueType.TIME_UUID_LIST: object, + ValueType.BYTES_SET: object, + ValueType.STRING_SET: object, + ValueType.INT32_SET: np.int32, + ValueType.INT64_SET: np.int64, + ValueType.FLOAT_SET: np.float32, + ValueType.DOUBLE_SET: np.float64, + ValueType.BOOL_SET: np.bool_, + ValueType.UNIX_TIMESTAMP_SET: "datetime64[s]", + ValueType.UUID_SET: object, + ValueType.TIME_UUID_SET: object, } target_dtype = base_type_map.get(value_type, object) diff --git a/sdk/python/feast/types.py b/sdk/python/feast/types.py index ddc98115ad1..ca913e06bb8 100644 --- a/sdk/python/feast/types.py +++ b/sdk/python/feast/types.py @@ -25,6 +25,9 @@ "BYTES": "BYTES", "PDF_BYTES": "PDF_BYTES", "IMAGE_BYTES": "IMAGE_BYTES", + "UUID": "UUID", + "TIME_UUID": "TIME_UUID", + "DECIMAL": "DECIMAL", "STRING": "STRING", "INT32": "INT32", "INT64": "INT64", @@ -32,6 +35,10 @@ "FLOAT32": "FLOAT", "BOOL": "BOOL", "UNIX_TIMESTAMP": "UNIX_TIMESTAMP", + "MAP": "MAP", + "JSON": "JSON", + "SCALAR_MAP": "SCALAR_MAP", + "ZONED_TIMESTAMP": "ZONED_TIMESTAMP", } @@ -83,6 +90,13 @@ class PrimitiveFeastType(Enum): UNIX_TIMESTAMP = 8 PDF_BYTES = 9 IMAGE_BYTES = 10 + MAP = 11 + JSON = 12 + UUID = 13 + TIME_UUID = 14 + DECIMAL = 15 + SCALAR_MAP = 16 + ZONED_TIMESTAMP = 17 def to_value_type(self) -> ValueType: """ @@ -115,6 +129,13 @@ def __hash__(self): Float32 = PrimitiveFeastType.FLOAT32 Float64 = PrimitiveFeastType.FLOAT64 UnixTimestamp = PrimitiveFeastType.UNIX_TIMESTAMP +Map = PrimitiveFeastType.MAP +Json = PrimitiveFeastType.JSON +Uuid = PrimitiveFeastType.UUID +TimeUuid = PrimitiveFeastType.TIME_UUID +Decimal = PrimitiveFeastType.DECIMAL +ScalarMap = PrimitiveFeastType.SCALAR_MAP +ZonedTimestamp = PrimitiveFeastType.ZONED_TIMESTAMP SUPPORTED_BASE_TYPES = [ Invalid, @@ -128,6 +149,12 @@ def __hash__(self): Float32, Float64, UnixTimestamp, + Map, + Json, + Uuid, + TimeUuid, + Decimal, + ZonedTimestamp, ] PRIMITIVE_FEAST_TYPES_TO_STRING = { @@ -142,6 +169,13 @@ def __hash__(self): "FLOAT32": "Float32", "FLOAT64": "Float64", "UNIX_TIMESTAMP": "UnixTimestamp", + "MAP": "Map", + "JSON": "Json", + "UUID": "Uuid", + "TIME_UUID": "TimeUuid", + "DECIMAL": "Decimal", + "SCALAR_MAP": "ScalarMap", + "ZONED_TIMESTAMP": "ZonedTimestamp", } @@ -155,24 +189,135 @@ class Array(ComplexFeastType): base_type: Union[PrimitiveFeastType, ComplexFeastType] - def __init__(self, base_type: Union[PrimitiveFeastType, ComplexFeastType]): - if base_type not in SUPPORTED_BASE_TYPES: - raise ValueError( - f"Type {type(base_type)} is currently not supported as a base type for Array." - ) + def __init__(self, base_type: Union[PrimitiveFeastType, "ComplexFeastType"]): + # Allow Struct, Array, and Set as base types for nested collections + if not isinstance(base_type, (Struct, Array, Set)): + # Arrays do not support ZonedTimestamp: there is no ZONED_TIMESTAMP_LIST + # ValueType for it to map to. + supported_array_types = [ + t for t in SUPPORTED_BASE_TYPES if t not in (ZonedTimestamp,) + ] + if base_type not in supported_array_types: + raise ValueError( + f"Type {type(base_type)} is currently not supported as a base type for Array." + ) self.base_type = base_type def to_value_type(self) -> ValueType: + if isinstance(self.base_type, Struct): + return ValueType.STRUCT_LIST + if isinstance(self.base_type, (Array, Set)): + return ValueType.VALUE_LIST assert isinstance(self.base_type, PrimitiveFeastType) value_type_name = PRIMITIVE_FEAST_TYPES_TO_VALUE_TYPES[self.base_type.name] value_type_list_name = value_type_name + "_LIST" return ValueType[value_type_list_name] + def __eq__(self, other): + if isinstance(other, Array): + return self.base_type == other.base_type + return False + + def __hash__(self): + return hash(("Array", hash(self.base_type))) + def __str__(self): return f"Array({self.base_type})" +class Set(ComplexFeastType): + """ + A Set represents a set of unique values of a given type. + + Attributes: + base_type: The base type of the set. + """ + + base_type: Union[PrimitiveFeastType, ComplexFeastType] + + def __init__(self, base_type: Union[PrimitiveFeastType, ComplexFeastType]): + # Allow Array and Set as base types for nested collections + if not isinstance(base_type, (Array, Set)): + # Sets do not support MAP as a base type, nor ZonedTimestamp (there is no + # ZONED_TIMESTAMP_SET ValueType for it to map to). + supported_set_types = [ + t for t in SUPPORTED_BASE_TYPES if t not in (Map, ZonedTimestamp) + ] + if base_type not in supported_set_types: + raise ValueError( + f"Type {type(base_type)} is currently not supported as a base type for Set." + ) + + self.base_type = base_type + + def to_value_type(self) -> ValueType: + if isinstance(self.base_type, (Array, Set)): + return ValueType.VALUE_SET + assert isinstance(self.base_type, PrimitiveFeastType) + value_type_name = PRIMITIVE_FEAST_TYPES_TO_VALUE_TYPES[self.base_type.name] + value_type_set_name = value_type_name + "_SET" + return ValueType[value_type_set_name] + + def __eq__(self, other): + if isinstance(other, Set): + return self.base_type == other.base_type + return False + + def __hash__(self): + return hash(("Set", hash(self.base_type))) + + def __str__(self): + return f"Set({self.base_type})" + + +class Struct(ComplexFeastType): + """ + A Struct represents a structured type with named, typed fields. + + Attributes: + fields: A dictionary mapping field names to their FeastTypes. + """ + + fields: Dict[str, Union[PrimitiveFeastType, "ComplexFeastType"]] + + def __init__( + self, fields: Dict[str, Union[PrimitiveFeastType, "ComplexFeastType"]] + ): + if not fields: + raise ValueError("Struct must have at least one field.") + self.fields = fields + + def to_value_type(self) -> ValueType: + return ValueType.STRUCT + + def to_pyarrow_type(self) -> pyarrow.DataType: + pa_fields = [] + for name, feast_type in self.fields.items(): + pa_type = from_feast_to_pyarrow_type(feast_type) + pa_fields.append(pyarrow.field(name, pa_type)) + return pyarrow.struct(pa_fields) + + def __str__(self): + field_strs = ", ".join( + f"{name}: {ftype}" for name, ftype in self.fields.items() + ) + return f"Struct({{{field_strs}}})" + + def __eq__(self, other): + if isinstance(other, Struct): + return self.fields == other.fields + return False + + def __hash__(self): + return hash( + ( + "Struct", + tuple((k, hash(v)) for k, v in sorted(self.fields.items())), + ) + ) + + FeastType = Union[ComplexFeastType, PrimitiveFeastType] VALUE_TYPES_TO_FEAST_TYPES: Dict["ValueType", FeastType] = { @@ -195,6 +340,29 @@ def __str__(self): ValueType.FLOAT_LIST: Array(Float32), ValueType.BOOL_LIST: Array(Bool), ValueType.UNIX_TIMESTAMP_LIST: Array(UnixTimestamp), + ValueType.MAP: Map, + ValueType.MAP_LIST: Array(Map), + ValueType.JSON: Json, + ValueType.JSON_LIST: Array(Json), + ValueType.BYTES_SET: Set(Bytes), + ValueType.STRING_SET: Set(String), + ValueType.INT32_SET: Set(Int32), + ValueType.INT64_SET: Set(Int64), + ValueType.DOUBLE_SET: Set(Float64), + ValueType.FLOAT_SET: Set(Float32), + ValueType.BOOL_SET: Set(Bool), + ValueType.UNIX_TIMESTAMP_SET: Set(UnixTimestamp), + ValueType.UUID: Uuid, + ValueType.TIME_UUID: TimeUuid, + ValueType.UUID_LIST: Array(Uuid), + ValueType.TIME_UUID_LIST: Array(TimeUuid), + ValueType.UUID_SET: Set(Uuid), + ValueType.TIME_UUID_SET: Set(TimeUuid), + ValueType.DECIMAL: Decimal, + ValueType.DECIMAL_LIST: Array(Decimal), + ValueType.DECIMAL_SET: Set(Decimal), + ValueType.SCALAR_MAP: ScalarMap, + ValueType.ZONED_TIMESTAMP: ZonedTimestamp, } FEAST_TYPES_TO_PYARROW_TYPES = { @@ -206,6 +374,13 @@ def __str__(self): Float64: pyarrow.float64(), # Note: datetime only supports microseconds https://github.com/python/cpython/blob/3.8/Lib/datetime.py#L1559 UnixTimestamp: pyarrow.timestamp("us", tz=_utc_now().tzname()), + # Per-value zone is carried in the proto; the Arrow column type is tz-aware UTC. + ZonedTimestamp: pyarrow.timestamp("us", tz="UTC"), + Map: pyarrow.map_(pyarrow.string(), pyarrow.string()), + Json: pyarrow.large_string(), + Uuid: pyarrow.string(), + TimeUuid: pyarrow.string(), + Decimal: pyarrow.string(), } FEAST_VECTOR_TYPES: List[Union[ValueType, PrimitiveFeastType, ComplexFeastType]] = [ @@ -214,6 +389,7 @@ def __str__(self): ValueType.INT64_LIST, ValueType.FLOAT_LIST, ValueType.BOOL_LIST, + ValueType.MAP_LIST, ] for k in VALUE_TYPES_TO_FEAST_TYPES: if k in FEAST_VECTOR_TYPES: @@ -233,12 +409,29 @@ def from_feast_to_pyarrow_type(feast_type: FeastType) -> pyarrow.DataType: assert isinstance(feast_type, (ComplexFeastType, PrimitiveFeastType)), ( f"Expected FeastType, got {type(feast_type)}" ) + if isinstance(feast_type, Struct): + return feast_type.to_pyarrow_type() if isinstance(feast_type, PrimitiveFeastType): if feast_type in FEAST_TYPES_TO_PYARROW_TYPES: return FEAST_TYPES_TO_PYARROW_TYPES[feast_type] - elif isinstance(feast_type, ComplexFeastType): - # Handle the case when feast_type is an instance of ComplexFeastType - pass + elif isinstance(feast_type, Array): + base_type = feast_type.base_type + if isinstance(base_type, Struct): + return pyarrow.list_(base_type.to_pyarrow_type()) + if isinstance(base_type, (Array, Set)): + return pyarrow.list_(from_feast_to_pyarrow_type(base_type)) + if isinstance(base_type, PrimitiveFeastType): + if base_type == Map: + return pyarrow.list_(pyarrow.map_(pyarrow.string(), pyarrow.string())) + if base_type in FEAST_TYPES_TO_PYARROW_TYPES: + return pyarrow.list_(FEAST_TYPES_TO_PYARROW_TYPES[base_type]) + elif isinstance(feast_type, Set): + base_type = feast_type.base_type + if isinstance(base_type, (Array, Set)): + return pyarrow.list_(from_feast_to_pyarrow_type(base_type)) + if isinstance(base_type, PrimitiveFeastType): + if base_type in FEAST_TYPES_TO_PYARROW_TYPES: + return pyarrow.list_(FEAST_TYPES_TO_PYARROW_TYPES[base_type]) raise ValueError(f"Could not convert Feast type {feast_type} to PyArrow type.") @@ -258,6 +451,21 @@ def from_value_type( if value_type in VALUE_TYPES_TO_FEAST_TYPES: return VALUE_TYPES_TO_FEAST_TYPES[value_type] + # Struct types cannot be looked up from the dict because they require + # field definitions. Return a default placeholder Struct that can be + # enriched later from Field tags / schema metadata. + if value_type == ValueType.STRUCT: + return Struct({"_value": String}) + if value_type == ValueType.STRUCT_LIST: + return Array(Struct({"_value": String})) + + # Nested collection types use placeholder inner types. + # Real inner type is restored from Field tags during deserialization. + if value_type == ValueType.VALUE_LIST: + return Array(Array(String)) + if value_type == ValueType.VALUE_SET: + return Set(Array(String)) + raise ValueError(f"Could not convert value type {value_type} to FeastType.") @@ -276,6 +484,18 @@ def from_feast_type( Raises: ValueError: The conversion could not be performed. """ + # Handle Struct types directly since they are not in the dict + if isinstance(feast_type, Struct): + return ValueType.STRUCT + if isinstance(feast_type, Array) and isinstance(feast_type.base_type, Struct): + return ValueType.STRUCT_LIST + + # Handle nested collection types + if isinstance(feast_type, Array) and isinstance(feast_type.base_type, (Array, Set)): + return ValueType.VALUE_LIST + if isinstance(feast_type, Set) and isinstance(feast_type.base_type, (Array, Set)): + return ValueType.VALUE_SET + if feast_type in VALUE_TYPES_TO_FEAST_TYPES.values(): return list(VALUE_TYPES_TO_FEAST_TYPES.keys())[ list(VALUE_TYPES_TO_FEAST_TYPES.values()).index(feast_type) diff --git a/sdk/python/feast/ui/package.json b/sdk/python/feast/ui/package.json index 6b65265755c..f4975fe2c21 100644 --- a/sdk/python/feast/ui/package.json +++ b/sdk/python/feast/ui/package.json @@ -6,7 +6,7 @@ "@elastic/datemath": "^5.0.3", "@elastic/eui": "^72.0.0", "@emotion/react": "^11.9.0", - "@feast-dev/feast-ui": "0.55.0", + "@feast-dev/feast-ui": "0.57.0", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.2.0", "@testing-library/user-event": "^13.5.0", diff --git a/sdk/python/feast/ui/yarn.lock b/sdk/python/feast/ui/yarn.lock index 152bf0fb3fd..7007b8c72ec 100644 --- a/sdk/python/feast/ui/yarn.lock +++ b/sdk/python/feast/ui/yarn.lock @@ -1575,10 +1575,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@feast-dev/feast-ui@0.55.0": - version "0.55.0" - resolved "https://registry.yarnpkg.com/@feast-dev/feast-ui/-/feast-ui-0.55.0.tgz#dd90ae8a96fdf3a5da5dd12e1d740c4b78faf0a5" - integrity sha512-T+j5ZwPafIsnsUFcSUENh+fR9aKmlfMkJhtKUzKreqg/5bxXRAo0fKIBWkKFSV+KsIAkDCwa8R7V9N5C/eeiKQ== +"@feast-dev/feast-ui@0.57.0": + version "0.57.0" + resolved "https://registry.yarnpkg.com/@feast-dev/feast-ui/-/feast-ui-0.57.0.tgz#0877b7f7fb39dd5cc74aabd9e77b79ff043d5a0a" + integrity sha512-lNegnVW1HnRT3NB54vaEuaO8pNDU5xNI/tDJNfz8HSXmvJcZ5Xiz1c/go9sRskOClUMrC4xJN/kw9F7IVMsnCw== dependencies: "@elastic/datemath" "^5.0.3" "@elastic/eui" "^95.12.0" @@ -7985,9 +7985,9 @@ node-emoji@^1.10.0: lodash "^4.17.21" node-forge@^1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + version "1.3.3" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751" + integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg== node-int64@^0.4.0: version "0.4.0" diff --git a/sdk/python/feast/ui_server.py b/sdk/python/feast/ui_server.py index 8e201bcf944..574aeacc257 100644 --- a/sdk/python/feast/ui_server.py +++ b/sdk/python/feast/ui_server.py @@ -1,8 +1,9 @@ import json -import threading +import logging from importlib import resources as importlib_resources -from typing import Callable, Optional +from typing import Any, Dict, List, Optional +import pandas as pd import uvicorn from fastapi import FastAPI, Response, status from fastapi.middleware.cors import CORSMiddleware @@ -11,16 +12,867 @@ import feast +logger = logging.getLogger(__name__) -class SaveDocumentRequest(BaseModel): - file_path: str - data: dict + +def _safe_error_response( + operation: str, status_code: int = status.HTTP_400_BAD_REQUEST +) -> Response: + """Return a generic error response without exposing internal details.""" + return Response( + content=json.dumps( + {"detail": f"{operation} failed. Check server logs for details."} + ), + status_code=status_code, + media_type="application/json", + ) + + +def _build_auth_config_json(store: "feast.FeatureStore") -> str: + """Build a JSON string with auth config from feature_store.yaml for the UI.""" + from feast.permissions.auth_model import AuthConfig, OidcAuthConfig + + auth_cfg = getattr(store.config, "auth_config", None) + if not isinstance(auth_cfg, AuthConfig): + return json.dumps({"auth_type": "no_auth"}) + + auth_type = auth_cfg.type if auth_cfg else "no_auth" + + config: Dict[str, str] = {"auth_type": auth_type} + if auth_type == "oidc" and isinstance(auth_cfg, OidcAuthConfig): + discovery_url = auth_cfg.auth_discovery_url + if "/realms/" in discovery_url: + base = discovery_url.split("/realms/")[0] + realm = discovery_url.split("/realms/")[1].split("/")[0] + config["url"] = base + config["realm"] = realm + config["auth_discovery_url"] = discovery_url + config["client_id"] = auth_cfg.ui_client_id or auth_cfg.client_id or "" + + return json.dumps(config) + + +def _build_projects_list( + store: "feast.FeatureStore", + project_id: str, + root_path: str, +): + """Build the projects list for the UI.""" + discovered_projects = [] + registry_path_template = f"{root_path}/api/v1" + + try: + projects = store.registry.list_projects(allow_cache=True) + for proj in projects: + discovered_projects.append( + { + "name": proj.name.replace("_", " ").title(), + "description": proj.description or f"Project: {proj.name}", + "id": proj.name, + "registryPath": registry_path_template, + } + ) + except Exception: + pass + + if not discovered_projects: + discovered_projects.append( + { + "name": "Project", + "description": "Test project", + "id": project_id, + "registryPath": registry_path_template, + } + ) + + if len(discovered_projects) > 1: + all_projects_entry = { + "name": "All Projects", + "description": "View data across all projects", + "id": "all", + "registryPath": registry_path_template, + } + discovered_projects.insert(0, all_projects_entry) + + return {"projects": discovered_projects} + + +def _setup_rest_mode(app: FastAPI, store: "feast.FeatureStore"): + """Mount the REST registry API routes on the UI server under /api/v1.""" + from fastapi import Depends, Request + from fastapi.responses import JSONResponse + + from feast.api.registry.rest import register_all_routes + from feast.errors import FeastObjectNotFoundException, FeastPermissionError + from feast.permissions.server.utils import ( + ServerType, + init_auth_manager, + init_security_manager, + str_to_auth_manager_type, + ) + from feast.registry_server import RegistryServer + + grpc_handler = RegistryServer(store.registry, store=store) + + auth_cfg = store.config.auth_config + dependencies = [] + if auth_cfg and auth_cfg.type != "no_auth": + from feast.permissions.server.rest import inject_user_details + + auth_type = str_to_auth_manager_type(auth_cfg.type) + init_security_manager(auth_type=auth_type, fs=store) + init_auth_manager( + auth_type=auth_type, + server_type=ServerType.REST, + auth_config=auth_cfg, + ) + dependencies.append(Depends(inject_user_details)) + + rest_app = FastAPI(root_path="/api/v1", dependencies=dependencies) + + @rest_app.exception_handler(FeastPermissionError) + async def feast_permission_error_handler( + request: Request, exc: FeastPermissionError + ): + return JSONResponse( + status_code=403, + content={ + "status_code": 403, + "detail": str(exc), + "error_type": "FeastPermissionError", + }, + ) + + @rest_app.exception_handler(FeastObjectNotFoundException) + async def feast_object_not_found_handler( + request: Request, exc: FeastObjectNotFoundException + ): + return JSONResponse( + status_code=404, + content={ + "status_code": 404, + "detail": str(exc), + "error_type": "FeastObjectNotFoundException", + }, + ) + + register_all_routes(rest_app, grpc_handler, store=store) + + class PushRequest(BaseModel): + push_source_name: str + df: Dict[str, List] + to: Optional[str] = "online" + + @rest_app.post("/push") + def push_labels(request: PushRequest): + """Push label data to a LabelView (or any PushSource-backed FeatureView).""" + try: + df = pd.DataFrame(request.df) + if "event_timestamp" in df.columns: + df["event_timestamp"] = pd.to_datetime( + df["event_timestamp"], utc=True + ).dt.tz_localize(None) + to = request.to or "online" + if to == "online_and_offline": + store.push( + request.push_source_name, + df, + to=feast.data_source.PushMode.ONLINE_AND_OFFLINE, + ) + elif to == "offline": + store.push( + request.push_source_name, df, to=feast.data_source.PushMode.OFFLINE + ) + else: + store.push( + request.push_source_name, df, to=feast.data_source.PushMode.ONLINE + ) + return {"status": "ok"} + except Exception: + logger.exception("Push failed") + return _safe_error_response("Push") + + class GetOnlineFeaturesRequest(BaseModel): + feature_view: str + entity_keys: Dict[str, List] + + @rest_app.post("/get-online-labels") + def get_online_labels(request: GetOnlineFeaturesRequest): + """Retrieve current label values from the online store for given entity keys.""" + try: + fv_name = request.feature_view + + fv: Any = None + try: + fv = store.get_feature_view(fv_name) + except Exception: + pass + + if fv is None: + try: + fv = store.registry.get_label_view(fv_name, store.project) + except Exception: + pass + + if fv is None: + return Response( + content=json.dumps( + {"detail": f"Feature view or label view '{fv_name}' not found"} + ), + status_code=status.HTTP_404_NOT_FOUND, + media_type="application/json", + ) + + feature_refs = [f"{fv_name}:{f.name}" for f in fv.features] + + result = store.get_online_features( + features=feature_refs, + entity_rows=[ + {k: v[i] for k, v in request.entity_keys.items()} + for i in range(len(next(iter(request.entity_keys.values())))) + ], + ) + + result_dict = result.to_dict() + return {"results": result_dict} + except Exception: + logger.exception("get-online-labels failed") + return _safe_error_response("Get online labels") + + class ListLabelsRequest(BaseModel): + feature_view: str + limit: Optional[int] = 100 + + @rest_app.post("/list-labels") + def list_labels(request: ListLabelsRequest): + """List resolved labels from the offline store with conflict policy enforcement.""" + try: + from feast.labeling.conflict_policy import ConflictPolicy + from feast.labeling.conflict_resolver import resolve_conflicts + + fv_name = request.feature_view + fv: Any = None + try: + fv = store.registry.get_label_view(fv_name, store.project) + except Exception: + try: + fv = store.get_feature_view(fv_name) + except Exception: + pass + + if fv is None: + return Response( + content=json.dumps({"detail": f"Label view '{fv_name}' not found"}), + status_code=status.HTTP_404_NOT_FOUND, + media_type="application/json", + ) + + batch_source = getattr(fv, "batch_source", None) + if batch_source is None: + return Response( + content=json.dumps({"detail": f"No batch source for '{fv_name}'"}), + status_code=status.HTTP_400_BAD_REQUEST, + media_type="application/json", + ) + + feature_names = [f.name for f in fv.features] + join_keys = ( + fv.join_keys + if hasattr(fv, "join_keys") + else (fv.entities if fv.entities else []) + ) + + provider = store._get_provider() + timestamp_field = batch_source.timestamp_field + + conflict_policy = getattr( + fv, "conflict_policy", ConflictPolicy.LAST_WRITE_WINS + ) + labeler_field = getattr(fv, "labeler_field", "labeler") + + try: + job = provider.offline_store.pull_all_from_table_or_query( + config=store.config, + data_source=batch_source, + join_key_columns=join_keys, + feature_name_columns=feature_names, + timestamp_field=timestamp_field, + ) + df = job.to_df() + except Exception: + return { + "labels": [], + "columns": join_keys + feature_names, + "feature_names": feature_names, + "entity_names": join_keys, + "total_count": 0, + "total_entities": 0, + "conflict_policy": conflict_policy.value, + "hint": "No label data found yet. Push labels to get started.", + } + + df = resolve_conflicts( + df=df, + join_key_columns=join_keys, + feature_name_columns=feature_names, + timestamp_field=timestamp_field, + labeler_field=labeler_field, + conflict_policy=conflict_policy, + ) + + limit = request.limit or 100 + df = df.head(limit) + + result = df.to_dict(orient="records") + for row in result: + for k, v in row.items(): + if pd.isna(v): + row[k] = None + elif hasattr(v, "isoformat"): + row[k] = v.isoformat() + + return { + "labels": result, + "columns": list(df.columns), + "feature_names": feature_names, + "entity_names": join_keys, + "total_count": len(result), + "total_entities": len(result), + "conflict_policy": conflict_policy.value, + } + except Exception: + logger.exception("list-labels failed") + return _safe_error_response("List labels") + + class LabelQualityRequest(BaseModel): + feature_view: str + + @rest_app.post("/label-quality") + def label_quality(request: LabelQualityRequest): + """Compute label quality metrics with conflict policy enforcement.""" + try: + from datetime import datetime, timezone + + from feast.labeling.conflict_policy import ConflictPolicy + from feast.labeling.conflict_resolver import resolve_conflicts + + fv_name = request.feature_view + fv: Any = None + try: + fv = store.registry.get_label_view(fv_name, store.project) + except Exception: + try: + fv = store.get_feature_view(fv_name) + except Exception: + pass + + if fv is None: + return Response( + content=json.dumps({"detail": f"Label view '{fv_name}' not found"}), + status_code=status.HTTP_404_NOT_FOUND, + media_type="application/json", + ) + + batch_source = getattr(fv, "batch_source", None) + if batch_source is None: + return Response( + content=json.dumps({"detail": f"No batch source for '{fv_name}'"}), + status_code=status.HTTP_400_BAD_REQUEST, + media_type="application/json", + ) + + feature_names = [f.name for f in fv.features] + join_keys = ( + fv.join_keys + if hasattr(fv, "join_keys") + else (fv.entities if fv.entities else []) + ) + + provider = store._get_provider() + timestamp_field = batch_source.timestamp_field + labeler_field = getattr(fv, "labeler_field", "labeler") + conflict_policy = getattr( + fv, "conflict_policy", ConflictPolicy.LAST_WRITE_WINS + ) + + try: + job = provider.offline_store.pull_all_from_table_or_query( + config=store.config, + data_source=batch_source, + join_key_columns=join_keys, + feature_name_columns=feature_names, + timestamp_field=timestamp_field, + ) + raw_df = job.to_df() + except Exception: + return { + "total_entities": 0, + "total_annotations": 0, + "feature_names": feature_names, + "distributions": {}, + "coverage_pct": {}, + "null_counts": {}, + "labeler_stats": {}, + "staleness_seconds": None, + "oldest_label_ts": None, + "newest_label_ts": None, + "labeler_field": labeler_field, + "conflict_policy": conflict_policy.value, + "hint": "No label data found yet. Push labels to compute quality metrics.", + } + + total_annotations = len(raw_df) + + # Labeler stats computed on raw data (before resolution) + labeler_stats: Dict[str, int] = {} + if labeler_field and labeler_field in raw_df.columns: + labeler_stats = ( + raw_df[labeler_field].dropna().astype(str).value_counts().to_dict() + ) + + # Resolve conflicts to get one row per entity + df = resolve_conflicts( + df=raw_df, + join_key_columns=join_keys, + feature_name_columns=feature_names, + timestamp_field=timestamp_field, + labeler_field=labeler_field, + conflict_policy=conflict_policy, + ) + + total_entities = len(df) + ts_col = timestamp_field + + staleness_seconds = None + oldest_ts = None + newest_ts = None + if ts_col in df.columns and not df[ts_col].empty: + ts_series = pd.to_datetime(df[ts_col], utc=True) + oldest_ts = ts_series.min().isoformat() + newest_ts = ts_series.max().isoformat() + staleness_seconds = ( + datetime.now(timezone.utc) - ts_series.max() + ).total_seconds() + + distributions: Dict[str, Dict[str, int]] = {} + coverage: Dict[str, float] = {} + null_counts: Dict[str, int] = {} + + for fn in feature_names: + if fn in df.columns: + col = df[fn] + nulls = int(col.isna().sum()) + total = len(col) + null_counts[fn] = nulls + coverage[fn] = ((total - nulls) / total * 100) if total > 0 else 0.0 + non_null = col.dropna() + distributions[fn] = ( + non_null.astype(str).value_counts().head(20).to_dict() + ) + else: + null_counts[fn] = total_entities + coverage[fn] = 0.0 + distributions[fn] = {} + + return { + "total_entities": total_entities, + "total_annotations": total_annotations, + "feature_names": feature_names, + "distributions": distributions, + "coverage_pct": coverage, + "null_counts": null_counts, + "labeler_stats": labeler_stats, + "staleness_seconds": staleness_seconds, + "oldest_label_ts": oldest_ts, + "newest_label_ts": newest_ts, + "labeler_field": labeler_field, + "conflict_policy": conflict_policy.value, + } + except Exception: + logger.exception("Label quality failed") + return _safe_error_response("Label quality") + + # --- Active Learning Endpoint (offline-store-based, backend-agnostic) --- + + class ActiveLearningRequest(BaseModel): + feature_view: str + reference_feature_view: Optional[str] = None + limit: Optional[int] = 50 + + @rest_app.post("/active-learning/candidates") + def active_learning_candidates(request: ActiveLearningRequest): + """Find entities that exist in a reference feature view but have NOT been labeled yet.""" + try: + fv_name = request.feature_view + fv: Any = None + try: + fv = store.registry.get_label_view(fv_name, store.project) + except Exception: + pass + + if fv is None: + return Response( + content=json.dumps({"detail": f"Label view '{fv_name}' not found"}), + status_code=status.HTTP_404_NOT_FOUND, + media_type="application/json", + ) + + ref_fv_name = request.reference_feature_view or ( + getattr(fv, "reference_feature_view", None) or "" + ) + + if not ref_fv_name: + return Response( + content=json.dumps( + { + "detail": "No reference feature view specified. " + "Provide a feature view containing all entities." + } + ), + status_code=status.HTTP_400_BAD_REQUEST, + media_type="application/json", + ) + + ref_fv: Any = None + try: + ref_fv = store.get_feature_view(ref_fv_name) + except Exception: + pass + + if ref_fv is None: + return Response( + content=json.dumps( + {"detail": f"Reference feature view '{ref_fv_name}' not found"} + ), + status_code=status.HTTP_404_NOT_FOUND, + media_type="application/json", + ) + + provider = store._get_provider() + + # Get all entities from reference feature view + ref_batch_source = getattr(ref_fv, "batch_source", None) + if ref_batch_source is None: + return Response( + content=json.dumps( + { + "detail": f"No batch source for reference view '{ref_fv_name}'" + } + ), + status_code=status.HTTP_400_BAD_REQUEST, + media_type="application/json", + ) + + ref_join_keys = ( + ref_fv.join_keys + if hasattr(ref_fv, "join_keys") + else (ref_fv.entities if ref_fv.entities else []) + ) + ref_feature_names = [f.name for f in ref_fv.features] + + try: + ref_job = provider.offline_store.pull_all_from_table_or_query( + config=store.config, + data_source=ref_batch_source, + join_key_columns=ref_join_keys, + feature_name_columns=ref_feature_names[:1], + timestamp_field=ref_batch_source.timestamp_field, + created_timestamp_column=getattr( + ref_batch_source, "created_timestamp_column", None + ), + ) + ref_df = ref_job.to_df() + except Exception: + return Response( + content=json.dumps( + { + "detail": f"Could not read reference feature view '{ref_fv_name}'. Ensure data exists." + } + ), + status_code=status.HTTP_400_BAD_REQUEST, + media_type="application/json", + ) + + # Get labeled entities from the label view + label_batch_source = getattr(fv, "batch_source", None) + label_join_keys = ( + fv.join_keys + if hasattr(fv, "join_keys") + else (fv.entities if fv.entities else []) + ) + label_feature_names = [f.name for f in fv.features] + + labeled_keys: set = set() + if label_batch_source: + try: + label_job = provider.offline_store.pull_all_from_table_or_query( + config=store.config, + data_source=label_batch_source, + join_key_columns=label_join_keys, + feature_name_columns=label_feature_names, + timestamp_field=label_batch_source.timestamp_field, + created_timestamp_column=getattr( + label_batch_source, + "created_timestamp_column", + None, + ), + ) + label_df = label_job.to_df() + if not label_df.empty and label_join_keys: + labeled_keys = set( + label_df[label_join_keys[0]].dropna().astype(str) + ) + except Exception: + labeled_keys = set() + + # Find unlabeled entities (in ref but not in labels) + common_key = label_join_keys[0] if label_join_keys else ref_join_keys[0] + unlabeled_entities: List[Dict[str, Any]] = [] + + if common_key in ref_df.columns: + seen_keys: set = set() + for _, row in ref_df.iterrows(): + key_val = ( + str(row[common_key]) if pd.notna(row[common_key]) else None + ) + if ( + key_val + and key_val not in labeled_keys + and key_val not in seen_keys + ): + seen_keys.add(key_val) + entity = {common_key: row[common_key]} + unlabeled_entities.append(entity) + if len(unlabeled_entities) >= (request.limit or 50): + break + + return { + "unlabeled_entities": unlabeled_entities, + "total_labeled": len(labeled_keys), + "total_unlabeled": len(unlabeled_entities), + "entity_names": label_join_keys, + "feature_names": label_feature_names, + "label_view": fv_name, + "reference_feature_view": ref_fv_name, + } + except Exception: + logger.exception("Active learning candidates failed") + return _safe_error_response("Active learning candidates") + + # --- Production Label Management Endpoints --- + + class BatchPushRequest(BaseModel): + push_source_name: str + data: List[Dict[str, Any]] + to: Optional[str] = "online" + + @rest_app.post("/batch-push") + def batch_push(request: BatchPushRequest): + """Batch push labels from CSV/parquet upload or external systems (Argilla, Label Studio).""" + try: + df = pd.DataFrame(request.data) + to = request.to or "online" + if to == "online_and_offline": + store.push( + request.push_source_name, + df, + to=feast.data_source.PushMode.ONLINE_AND_OFFLINE, + ) + elif to == "offline": + store.push( + request.push_source_name, df, to=feast.data_source.PushMode.OFFLINE + ) + else: + store.push( + request.push_source_name, df, to=feast.data_source.PushMode.ONLINE + ) + return {"status": "ok", "rows_pushed": len(df)} + except Exception: + logger.exception("Batch push failed") + return _safe_error_response("Batch push") + + class WebhookPushRequest(BaseModel): + push_source_name: str + records: List[Dict[str, Any]] + api_key: Optional[str] = None + + @rest_app.post("/webhook/label-ingest") + def webhook_label_ingest(request: WebhookPushRequest): + """Webhook endpoint for external annotation tools (Argilla, Label Studio) to push labels.""" + try: + from datetime import datetime, timezone + + for record in request.records: + if "event_timestamp" not in record: + record["event_timestamp"] = datetime.now(timezone.utc).isoformat() + + df = pd.DataFrame(request.records) + store.push( + request.push_source_name, + df, + to=feast.data_source.PushMode.ONLINE_AND_OFFLINE, + ) + return { + "status": "ok", + "records_ingested": len(request.records), + "push_source": request.push_source_name, + } + except Exception: + logger.exception("Webhook label ingest failed") + return _safe_error_response("Webhook label ingest") + + class TrainingExportRequest(BaseModel): + feature_service: str + entity_df: Dict[str, List] + start_date: Optional[str] = None + end_date: Optional[str] = None + + @rest_app.post("/training-dataset/export") + def export_training_dataset(request: TrainingExportRequest): + """Generate a training dataset using get_historical_features and return as JSON (downloadable).""" + try: + from datetime import datetime, timezone + + entity_df = pd.DataFrame(request.entity_df) + + if "event_timestamp" not in entity_df.columns: + if request.end_date: + ts = pd.Timestamp(request.end_date, tz="UTC") + else: + ts = pd.Timestamp(datetime.now(timezone.utc)) + entity_df["event_timestamp"] = ts + + fs = store.get_feature_service(request.feature_service) + training_df = store.get_historical_features( + entity_df=entity_df, + features=fs, + ).to_df() + + result = training_df.to_dict(orient="records") + for row in result: + for k, v in row.items(): + if pd.isna(v): + row[k] = None + elif hasattr(v, "isoformat"): + row[k] = v.isoformat() + + return { + "data": result, + "columns": list(training_df.columns), + "row_count": len(training_df), + "feature_service": request.feature_service, + } + except Exception: + logger.exception("Training dataset export failed") + return _safe_error_response("Training dataset export") + + @rest_app.get("/webhook/config/{label_view_name}") + def webhook_config(label_view_name: str): + """Return webhook configuration info for integrating external annotation tools.""" + try: + fv = store.registry.get_label_view(label_view_name, store.project) + push_source_name = fv.source.name if fv.source else None + feature_names = [f.name for f in fv.features] + entity_names = [ec.name for ec in fv.entity_columns] + labeler_field = getattr(fv, "labeler_field", None) + + return { + "label_view": label_view_name, + "push_source_name": push_source_name, + "webhook_url": "/api/v1/webhook/label-ingest", + "batch_url": "/api/v1/batch-push", + "required_fields": entity_names + + feature_names + + ([labeler_field] if labeler_field else []), + "entity_fields": entity_names, + "label_fields": feature_names, + "labeler_field": labeler_field, + "payload_example": { + "push_source_name": push_source_name, + "records": [ + { + **{e: f"<{e}_value>" for e in entity_names}, + **{f: f"<{f}_value>" for f in feature_names}, + **( + {"event_timestamp": "2026-01-01T00:00:00Z"} + if True + else {} + ), + **( + {labeler_field: ""} if labeler_field else {} + ), + } + ], + }, + } + except Exception: + logger.exception("Webhook config lookup failed") + return _safe_error_response("Webhook config", status.HTTP_404_NOT_FOUND) + + @rest_app.get("/annotation-config/{label_view_name}") + def annotation_config(label_view_name: str): + """Return annotation profile and field role config parsed from LabelView tags. + + The UI uses this to select the right annotation method and map + user interactions to schema fields. + """ + try: + fv = store.registry.get_label_view(label_view_name, store.project) + + tags = dict(getattr(fv, "tags", {})) + profile = tags.get("feast.io/labeling-method", "table") + + field_roles: Dict[str, str] = {} + label_values: Dict[str, List] = {} + label_widgets: Dict[str, str] = {} + + for key, value in tags.items(): + if key.startswith("feast.io/field-role:"): + field_name = key[len("feast.io/field-role:") :] + field_roles[field_name] = value + elif key.startswith("feast.io/label-values:"): + field_name = key[len("feast.io/label-values:") :] + label_values[field_name] = [v.strip() for v in value.split(",")] + elif key.startswith("feast.io/label-widget:"): + field_name = key[len("feast.io/label-widget:") :] + label_widgets[field_name] = value + + entity_names = fv.entities if fv.entities else [] + feature_names = [f.name for f in fv.features] + labeler_field = getattr(fv, "labeler_field", "labeler") + push_source_name = fv.source.name if fv.source else None + + return { + "label_view": label_view_name, + "profile": profile, + "field_roles": field_roles, + "label_values": label_values, + "label_widgets": label_widgets, + "entities": entity_names, + "features": feature_names, + "labeler_field": labeler_field, + "push_source_name": push_source_name, + } + except Exception: + logger.exception("Annotation config lookup failed") + return _safe_error_response("Annotation config", status.HTTP_404_NOT_FOUND) + + app.mount("/api/v1", rest_app) + + @app.get("/health") + def health(): + try: + store.registry.list_projects(allow_cache=True) + return Response(status_code=status.HTTP_200_OK) + except Exception: + return Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE) + + logger.info("REST registry API mounted at /api/v1") def get_app( store: "feast.FeatureStore", project_id: str, - registry_ttl_secs: int, root_path: str = "", ): app = FastAPI() @@ -33,140 +885,321 @@ def get_app( allow_headers=["*"], ) - # Asynchronously refresh registry, notifying shutdown and canceling the active timer if the app is shutting down - registry_proto = None - shutting_down = False - active_timer: Optional[threading.Timer] = None - - def async_refresh(): - store.refresh_registry() - nonlocal registry_proto - registry_proto = store.registry.proto() - if shutting_down: - return - nonlocal active_timer - active_timer = threading.Timer(registry_ttl_secs, async_refresh) - active_timer.start() - - @app.on_event("shutdown") - def shutdown_event(): - nonlocal shutting_down - shutting_down = True - if active_timer: - active_timer.cancel() - - async_refresh() + _setup_rest_mode(app, store) ui_dir_ref = importlib_resources.files(__spec__.parent) / "ui/build/" # type: ignore[name-defined, arg-type] with importlib_resources.as_file(ui_dir_ref) as ui_dir: - # Initialize with the projects-list.json file - with ui_dir.joinpath("projects-list.json").open(mode="w") as f: - # Get all projects from the registry - discovered_projects = [] - registry = store.registry.proto() - - # Use the projects list from the registry - if registry and registry.projects and len(registry.projects) > 0: - for proj in registry.projects: - if proj.spec and proj.spec.name: - discovered_projects.append( - { - "name": proj.spec.name.replace("_", " ").title(), - "description": proj.spec.description - or f"Project: {proj.spec.name}", - "id": proj.spec.name, - "registryPath": f"{root_path}/registry", + + @app.get("/projects-list.json") + def get_projects_list(): + return _build_projects_list(store, project_id, root_path) + + @app.get("/api/mlflow-runs") + def get_mlflow_runs(max_results: int = 50): + """Return MLflow runs linked to this Feast project via auto-logging.""" + mlflow_cfg = getattr(store.config, "mlflow", None) + if not mlflow_cfg or not mlflow_cfg.enabled: + return {"runs": [], "mlflow_uri": None} + + try: + import mlflow + + tracking_uri = mlflow_cfg.get_tracking_uri() + mlflow_ui_base = tracking_uri or mlflow.get_tracking_uri() or "" + client = mlflow.MlflowClient(tracking_uri=tracking_uri) + + project_name = store.config.project + experiment = client.get_experiment_by_name(project_name) + if experiment is None: + return {"runs": [], "mlflow_uri": mlflow_ui_base or None} + experiment_ids = [experiment.experiment_id] + + safe_project = project_name.replace("\\", "\\\\").replace("'", "\\'") + filter_str = ( + f"tags.`feast.project` = '{safe_project}' " + f"AND tags.`feast.retrieval_type` != ''" + ) + + max_results = min(max(max_results, 1), 200) + runs = client.search_runs( + experiment_ids=experiment_ids, + filter_string=filter_str, + max_results=max_results, + order_by=["start_time DESC"], + ) + + run_id_to_models: Dict[str, List[dict]] = {} + try: + for rm in client.search_registered_models(): + for mv in rm.latest_versions or []: + if mv.run_id: + run_id_to_models.setdefault(mv.run_id, []).append( + { + "model_name": rm.name, + "version": mv.version, + "stage": mv.current_stage, + "mlflow_url": ( + f"{mlflow_ui_base}/#/models/" + f"{rm.name}/versions/{mv.version}" + ), + } + ) + except Exception: + pass + + result = [] + for run in runs: + run_tags = run.data.tags + run_params = run.data.params + fv_raw = run_tags.get("feast.feature_views", "") + refs_raw = run_tags.get( + "feast.feature_refs", + run_params.get("feast.feature_refs", ""), + ) + result.append( + { + "run_id": run.info.run_id, + "run_name": run.info.run_name, + "status": run.info.status, + "start_time": run.info.start_time, + "feature_service": run_tags.get("feast.feature_service"), + "feature_views": [v for v in fv_raw.split(",") if v], + "feature_refs": [v for v in refs_raw.split(",") if v], + "retrieval_type": run_tags.get("feast.retrieval_type"), + "entity_count": run_tags.get( + "feast.entity_count", + run_params.get("feast.entity_count"), + ), + "mlflow_url": ( + f"{mlflow_ui_base}/#/experiments/" + f"{run.info.experiment_id}/runs/{run.info.run_id}" + ), + "registered_models": run_id_to_models.get( + run.info.run_id, [] + ), + } + ) + + return {"runs": result, "mlflow_uri": mlflow_ui_base or None} + except ImportError: + return { + "runs": [], + "mlflow_uri": None, + "error": "mlflow is not installed", + } + except Exception: + return { + "runs": [], + "mlflow_uri": None, + "error": "Failed to fetch MLflow runs", + } + + _feature_usage_cache: Dict = {"data": None, "timestamp": 0.0} + _FEATURE_USAGE_TTL_SECONDS = 300 + + @app.get("/api/mlflow-feature-usage") + def get_mlflow_feature_usage(): + """Return per-feature-view usage stats aggregated from MLflow runs. + + Caches results for 5 minutes to avoid hammering the MLflow server. + """ + import time as _time + + mlflow_cfg = getattr(store.config, "mlflow", None) + if not mlflow_cfg or not mlflow_cfg.enabled: + return {"feature_usage": {}, "mlflow_enabled": False} + + now = _time.monotonic() + if ( + _feature_usage_cache["data"] is not None + and (now - _feature_usage_cache["timestamp"]) + < _FEATURE_USAGE_TTL_SECONDS + ): + return _feature_usage_cache["data"] + + try: + import mlflow + + tracking_uri = mlflow_cfg.get_tracking_uri() + client = mlflow.MlflowClient(tracking_uri=tracking_uri) + project_name = store.config.project + + experiment = client.get_experiment_by_name(project_name) + if experiment is None: + result = {"feature_usage": {}, "mlflow_enabled": True} + _feature_usage_cache["data"] = result + _feature_usage_cache["timestamp"] = now + return result + + safe_project = project_name.replace("\\", "\\\\").replace("'", "\\'") + filter_str = ( + f"tags.`feast.project` = '{safe_project}' " + f"AND tags.`feast.retrieval_type` != ''" + ) + runs = client.search_runs( + experiment_ids=[experiment.experiment_id], + filter_string=filter_str, + max_results=200, + order_by=["start_time DESC"], + ) + + run_id_to_models: Dict[str, List[str]] = {} + try: + for rm in client.search_registered_models(): + for mv in rm.latest_versions or []: + if mv.run_id: + run_id_to_models.setdefault(mv.run_id, []).append( + rm.name + ) + except Exception: + pass + + usage: Dict[str, dict] = {} + for run in runs: + refs_raw = run.data.tags.get("feast.feature_refs", "") + fv_names = set() + for ref in refs_raw.split(","): + ref = ref.strip() + if ":" in ref: + fv_names.add(ref.split(":")[0]) + + run_models = run_id_to_models.get(run.info.run_id, []) + + for fv_name in fv_names: + if fv_name not in usage: + usage[fv_name] = { + "run_count": 0, + "last_used": None, + "models": [], } - ) - else: - # If no projects in registry, use the current project from feature_store.yaml - discovered_projects.append( - { - "name": "Project", - "description": "Test project", - "id": project_id, - "registryPath": f"{root_path}/registry", - } - ) - - # Add "All Projects" option at the beginning if there are multiple projects - if len(discovered_projects) > 1: - all_projects_entry = { - "name": "All Projects", - "description": "View data across all projects", - "id": "all", - "registryPath": f"{root_path}/registry", + usage[fv_name]["run_count"] += 1 + run_ts = run.info.start_time + if usage[fv_name]["last_used"] is None or ( + run_ts and run_ts > usage[fv_name]["last_used"] + ): + usage[fv_name]["last_used"] = run_ts + for m in run_models: + if m not in usage[fv_name]["models"]: + usage[fv_name]["models"].append(m) + + result = {"feature_usage": usage, "mlflow_enabled": True} + _feature_usage_cache["data"] = result + _feature_usage_cache["timestamp"] = now + return result + except ImportError: + return { + "feature_usage": {}, + "mlflow_enabled": False, + "error": "mlflow is not installed", + } + except Exception as e: + logger.debug("Failed to fetch feature usage: %s", e) + return { + "feature_usage": {}, + "mlflow_enabled": True, + "error": "Failed to fetch usage data", } - discovered_projects.insert(0, all_projects_entry) - - projects_dict = {"projects": discovered_projects} - f.write(json.dumps(projects_dict)) - - @app.get("/registry") - def read_registry(): - if registry_proto is None: - return Response( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE - ) # Service Unavailable - return Response( - content=registry_proto.SerializeToString(), - media_type="application/octet-stream", - ) - @app.get("/health") - def health(): - return ( - Response(status_code=status.HTTP_200_OK) - if registry_proto - else Response(status_code=status.HTTP_503_SERVICE_UNAVAILABLE) - ) + @app.get("/api/mlflow-feature-models") + def get_mlflow_feature_models(): + """Return a mapping of feature_ref -> registered models that use it. - @app.post("/save-document") - async def save_document_endpoint(request: SaveDocumentRequest): - try: - import os - from pathlib import Path + Walks the MLflow Model Registry, inspects the training run for each + model's latest version(s), reads the ``feast.feature_refs`` tag, and + inverts it into a reverse index so the UI can show which registered + models depend on a given feature. + """ + mlflow_cfg = getattr(store.config, "mlflow", None) + if not mlflow_cfg or not mlflow_cfg.enabled: + return {"feature_models": {}} - file_path = Path(request.file_path).resolve() - if not str(file_path).startswith(os.getcwd()): - return {"error": "Invalid file path"} + try: + import mlflow - base_name = file_path.stem - labels_file = file_path.parent / f"{base_name}-labels.json" + tracking_uri = mlflow_cfg.get_tracking_uri() + mlflow_ui_base = tracking_uri or mlflow.get_tracking_uri() or "" + client = mlflow.MlflowClient(tracking_uri=tracking_uri) + project_name = store.config.project - with open(labels_file, "w", encoding="utf-8") as file: - json.dump(request.data, file, indent=2, ensure_ascii=False) + feature_models: Dict[str, List[dict]] = {} - return {"success": True, "saved_to": str(labels_file)} - except Exception as e: - return {"error": str(e)} + for rm in client.search_registered_models(): + model_name = rm.name + latest_versions = rm.latest_versions or [] + for mv in latest_versions: + if not mv.run_id: + continue + try: + run = client.get_run(mv.run_id) + except Exception: + continue - # For all other paths (such as paths that would otherwise be handled by react router), pass to React - @app.api_route("/p/{path_name:path}", methods=["GET"]) - def catch_all(): - filename = ui_dir.joinpath("index.html") + tags = run.data.tags + if tags.get("feast.project") != project_name: + continue - with open(filename) as f: - content = f.read() + refs_raw = tags.get("feast.feature_refs", "") + feature_refs = [r for r in refs_raw.split(",") if r] - return Response(content, media_type="text/html") + model_info = { + "model_name": model_name, + "version": mv.version, + "stage": mv.current_stage, + "mlflow_url": ( + f"{mlflow_ui_base}/#/models/" + f"{model_name}/versions/{mv.version}" + ), + } - app.mount( - "/", - StaticFiles(directory=ui_dir, html=True), - name="site", - ) + for ref in feature_refs: + feature_models.setdefault(ref, []).append(model_info) + + return {"feature_models": feature_models} + except ImportError: + return { + "feature_models": {}, + "error": "mlflow is not installed", + } + except Exception as e: + logger.debug("Failed to fetch MLflow feature-model mapping: %s", e) + return { + "feature_models": {}, + "error": "Failed to fetch model data", + } + + auth_config_json = _build_auth_config_json(store) + + def _serve_index(): + filename = ui_dir.joinpath("index.html") + with open(filename) as f: + content = f.read() + if auth_config_json: + tag = f'' + content = content.replace("", f"{tag}\n", 1) + return Response(content, media_type="text/html") - return app + @app.get("/") + def serve_root(): + return _serve_index() + + @app.api_route("/p/{path_name:path}", methods=["GET"]) + def catch_all(): + return _serve_index() + + app.mount( + "/", + StaticFiles(directory=ui_dir, html=True), + name="site", + ) + + return app def start_server( store: "feast.FeatureStore", host: str, port: int, - get_registry_dump: Callable, project_id: str, - registry_ttl_sec: int, root_path: str = "", tls_key_path: str = "", tls_cert_path: str = "", @@ -174,9 +1207,11 @@ def start_server( app = get_app( store, project_id, - registry_ttl_sec, root_path, ) + + logger.info(f"Starting Feast UI server on {host}:{port}") + if tls_key_path and tls_cert_path: uvicorn.run( app, diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index 01ffa774ccd..831ed622d06 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -1,10 +1,12 @@ import copy import itertools +import logging import os +import threading import typing import warnings from collections import Counter, defaultdict -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import ( Any, @@ -25,6 +27,7 @@ from dateutil.tz import tzlocal from google.protobuf.timestamp_pb2 import Timestamp +from feast.aggregation import aggregation_specs_to_agg_ops from feast.constants import FEAST_FS_YAML_FILE_PATH_ENV_NAME from feast.entity import Entity from feast.errors import ( @@ -44,7 +47,7 @@ from feast.protos.feast.types.Value_pb2 import RepeatedValue as RepeatedValueProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto from feast.type_map import python_values_to_proto_values -from feast.types import ComplexFeastType, PrimitiveFeastType, from_feast_to_pyarrow_type +from feast.types import ComplexFeastType, PrimitiveFeastType from feast.value_type import ValueType from feast.version import get_version @@ -52,6 +55,7 @@ from feast.base_feature_view import BaseFeatureView from feast.feature_service import FeatureService from feast.feature_view import FeatureView + from feast.feature_view_projection import FeatureViewProjection from feast.infra.registry.base_registry import BaseRegistry from feast.on_demand_feature_view import OnDemandFeatureView @@ -59,6 +63,55 @@ USER_AGENT = "{}/{}".format(APPLICATION_NAME, get_version()) +def _parse_feature_ref(ref: str) -> Tuple[str, Optional[int], str]: + """Parse 'fv_name@version:feature' into (fv_name, version_number, feature_name). + + If no @version is present, version_number is None (meaning 'latest'). + Examples: + 'driver_stats:trips' -> ('driver_stats', None, 'trips') + 'driver_stats@v2:trips' -> ('driver_stats', 2, 'trips') + 'driver_stats@latest:trips' -> ('driver_stats', None, 'trips') + """ + import re + + colon_idx = ref.find(":") + if colon_idx < 0: + raise ValueError( + f"Invalid feature reference '{ref}'. Expected format: ':' " + f"or '@:'" + ) + + fv_part = ref[:colon_idx] + feature_name = ref[colon_idx + 1 :] + + at_idx = fv_part.find("@") + if at_idx < 0: + return (fv_part, None, feature_name) + + fv_name = fv_part[:at_idx] + version_str = fv_part[at_idx + 1 :] + + if not version_str or version_str.lower() == "latest": + return (fv_name, None, feature_name) + + # Parse version number from formats like "v2", "V2" + match = re.match(r"^[vV](\d+)$", version_str) + if not match: + # Not a recognized version format — treat entire fv_part as the name + return (fv_part, None, feature_name) + + return (fv_name, int(match.group(1)), feature_name) + + +def _strip_version_from_ref(ref: str) -> str: + """Strip @version from a feature reference, returning 'fv_name:feature'. + + Used to produce clean refs for output column naming. + """ + fv_name, _, feature_name = _parse_feature_ref(ref) + return f"{fv_name}:{feature_name}" + + def get_user_agent(): return USER_AGENT @@ -71,6 +124,37 @@ def make_tzaware(t: datetime) -> datetime: return t +def compute_non_entity_date_range( + feature_views: List["FeatureView"], + start_date: Optional[datetime] = None, + end_date: Optional[datetime] = None, + default_window_days: int = 30, +) -> Tuple[datetime, datetime]: + if end_date is None: + end_date = datetime.now(tz=timezone.utc) + else: + end_date = make_tzaware(end_date) + + if start_date is None: + max_ttl_seconds = max( + ( + int(fv.ttl.total_seconds()) + for fv in feature_views + if fv.ttl and isinstance(fv.ttl, timedelta) + ), + default=0, + ) + start_date = end_date - timedelta( + seconds=max_ttl_seconds + if max_ttl_seconds > 0 + else default_window_days * 86400 + ) + else: + start_date = make_tzaware(start_date) + + return start_date, end_date + + def make_df_tzaware(t: pd.DataFrame) -> pd.DataFrame: """Make all datetime type columns tzaware; leave everything else intact.""" df = t.copy() # don't modify incoming dataframe inplace @@ -117,9 +201,12 @@ def _get_requested_feature_views_to_features_dict( ) for ref in feature_refs: - ref_parts = ref.split(":") - feature_view_from_ref = ref_parts[0] - feature_from_ref = ref_parts[1] + fv_name, version_num, feature_from_ref = _parse_feature_ref(ref) + # Build the key that matches projection.name_to_use() + if version_num is not None: + feature_view_from_ref = f"{fv_name}@v{version_num}" + else: + feature_view_from_ref = fv_name found = False for fv in feature_views: @@ -151,9 +238,26 @@ def _get_column_names( and reverse-mapped created timestamp column that will be passed into the query to the offline store. """ + if feature_view.batch_source is None: + raise ValueError( + f"Feature view '{feature_view.name}' has no batch_source and cannot be used for offline retrieval." + ) + # if we have mapped fields, use the original field names in the call to the offline store timestamp_field = feature_view.batch_source.timestamp_field - feature_names = [feature.name for feature in feature_view.features] + + # For feature views with aggregations, read INPUT columns from aggregations. + # This applies to StreamFeatureView, BatchFeatureView, + # or any FeatureView that has aggregations. + if hasattr(feature_view, "aggregations") and feature_view.aggregations: + # Extract unique input columns from aggregations, preserving order + feature_names = list( + dict.fromkeys(agg.column for agg in feature_view.aggregations) + ) + else: + # For regular feature views, use the feature names + feature_names = [feature.name for feature in feature_view.features] + created_timestamp_column = feature_view.batch_source.created_timestamp_column from feast.feature_view import DUMMY_ENTITY_ID @@ -252,6 +356,58 @@ def _coerce_datetime(ts): return ts +def _columns_to_proto_values( + table: pyarrow.RecordBatch, + columns: List[Tuple[str, ValueType]], + allow_missing: bool = False, +) -> Dict[str, List[ValueProto]]: + """Convert table columns to proto values dict. + + Args: + table: PyArrow RecordBatch containing the data. + columns: List of (column_name, value_type) tuples to convert. + allow_missing: If True, skip columns not found in table. If False, raise ValueError. + + Returns: + Dict mapping column names to lists of ValueProto. + """ + result: Dict[str, List[ValueProto]] = {} + for column, value_type in columns: + if column in table.column_names: + result[column] = python_values_to_proto_values( + table.column(column).to_numpy(zero_copy_only=False), value_type + ) + elif not allow_missing: + raise ValueError(f"Column {column} not found in table") + return result + + +def _build_entity_keys( + num_rows: int, + join_keys: Dict[str, ValueType], + proto_values: Dict[str, List[ValueProto]], +) -> List[EntityKeyProto]: + """Build entity key protos for each row. + + Args: + num_rows: Number of rows to generate entity keys for. + join_keys: Dict mapping join key names to their value types. + proto_values: Dict mapping column names to lists of ValueProto values. + + Returns: + List of EntityKeyProto, one per row. + """ + return [ + EntityKeyProto( + join_keys=list(join_keys.keys()), + entity_values=[ + proto_values[k][idx] for k in join_keys if k in proto_values + ], + ) + for idx in range(num_rows) + ] + + def _convert_arrow_to_proto( table: Union[pyarrow.Table, pyarrow.RecordBatch], feature_view: Union["FeatureView", "BaseFeatureView", "OnDemandFeatureView"], @@ -277,25 +433,21 @@ def _convert_arrow_fv_to_proto( if isinstance(table, pyarrow.Table): table = table.to_batches()[0] + if feature_view.batch_source is None: + raise ValueError( + f"Feature view '{feature_view.name}' has no batch_source and cannot be converted to proto." + ) + # TODO: This will break if the feature view has aggregations or transformations columns = [ (field.name, field.dtype.to_value_type()) for field in feature_view.features ] + list(join_keys.items()) - proto_values_by_column = { - column: python_values_to_proto_values( - table.column(column).to_numpy(zero_copy_only=False), value_type - ) - for column, value_type in columns - } + proto_values_by_column = _columns_to_proto_values( + table, columns, allow_missing=False + ) - entity_keys = [ - EntityKeyProto( - join_keys=join_keys, - entity_values=[proto_values_by_column[k][idx] for k in join_keys], - ) - for idx in range(table.num_rows) - ] + entity_keys = _build_entity_keys(table.num_rows, join_keys, proto_values_by_column) # Serialize the features per row feature_dict = { @@ -343,62 +495,36 @@ def _convert_arrow_odfv_to_proto( (field.name, field.dtype.to_value_type()) for field in feature_view.features ] + list(join_keys.items()) - proto_values_by_column = { - column: python_values_to_proto_values( - table.column(column).to_numpy(zero_copy_only=False), value_type - ) - for column, value_type in columns - if column in table.column_names - } + # Convert columns that exist in the table + proto_values_by_column = _columns_to_proto_values( + table, columns, allow_missing=True + ) - # Ensure join keys are included in proto_values_by_column, but check if they exist first + # Ensure join keys are included, creating null values if missing from table for join_key, value_type in join_keys.items(): if join_key not in proto_values_by_column: - # Check if the join key exists in the table before trying to access it if join_key in table.column_names: proto_values_by_column[join_key] = python_values_to_proto_values( table.column(join_key).to_numpy(zero_copy_only=False), value_type ) else: - # Create null/default values if the join key isn't in the table - null_column = [None] * table.num_rows + # Create null proto values directly (no need to build a PyArrow array) proto_values_by_column[join_key] = python_values_to_proto_values( - null_column, value_type + [None] * table.num_rows, value_type ) - # Adding On Demand Features + # Cache column names set to avoid recreating list in loop + column_names = {c[0] for c in columns} + + # Adding On Demand Features that are missing from proto_values for feature in feature_view.features: - if ( - feature.name in [c[0] for c in columns] - and feature.name not in proto_values_by_column - ): - # initializing the column as null - null_column = pyarrow.array( - [None] * table.num_rows, - type=from_feast_to_pyarrow_type(feature.dtype), - ) - updated_table = pyarrow.RecordBatch.from_arrays( - table.columns + [null_column], - schema=table.schema.append( - pyarrow.field(feature.name, null_column.type) # type: ignore[attr-defined] - ), - ) + if feature.name in column_names and feature.name not in proto_values_by_column: + # Create null proto values directly (more efficient than building PyArrow array) proto_values_by_column[feature.name] = python_values_to_proto_values( - updated_table.column(feature.name).to_numpy(zero_copy_only=False), - feature.dtype.to_value_type(), + [None] * table.num_rows, feature.dtype.to_value_type() ) - entity_keys = [ - EntityKeyProto( - join_keys=join_keys, - entity_values=[ - proto_values_by_column[k][idx] - for k in join_keys - if k in proto_values_by_column - ], - ) - for idx in range(table.num_rows) - ] + entity_keys = _build_entity_keys(table.num_rows, join_keys, proto_values_by_column) # Serialize the features per row feature_dict = { @@ -407,7 +533,7 @@ def _convert_arrow_odfv_to_proto( if feature.name in proto_values_by_column } if feature_view.write_to_online_store: - table_columns = [col.name for col in table.schema] + table_columns = {col.name for col in table.schema} for feature in feature_view.schema: if feature.name not in feature_dict and feature.name in table_columns: feature_dict[feature.name] = proto_values_by_column[feature.name] @@ -415,11 +541,10 @@ def _convert_arrow_odfv_to_proto( features = [dict(zip(feature_dict, vars)) for vars in zip(*feature_dict.values())] # We need to artificially add event_timestamps and created_timestamps - event_timestamps = [] - timestamp_values = pd.to_datetime([_utc_now() for i in range(table.num_rows)]) - - for val in timestamp_values: - event_timestamps.append(_coerce_datetime(val)) + now = _utc_now() + event_timestamps = [ + _coerce_datetime(pd.Timestamp(now)) for _ in range(table.num_rows) + ] # setting them equivalent created_timestamps = event_timestamps @@ -454,7 +579,7 @@ def _validate_feature_refs(feature_refs: List[str], full_feature_names: bool = F ref for ref, occurrences in Counter(feature_refs).items() if occurrences > 1 ] else: - feature_names = [ref.split(":")[1] for ref in feature_refs] + feature_names = [_parse_feature_ref(ref)[2] for ref in feature_refs] collided_feature_names = [ ref for ref, occurrences in Counter(feature_names).items() @@ -501,7 +626,12 @@ def _group_feature_refs( on_demand_view_features = defaultdict(set) for ref in features: - view_name, feat_name = ref.split(":") + fv_name, version_num, feat_name = _parse_feature_ref(ref) + # Build the key that matches projection.name_to_use() + if version_num is not None: + view_name = f"{fv_name}@v{version_num}" + else: + view_name = fv_name if view_name in view_index: if hasattr(view_index[view_name], "write_to_online_store"): tmp_feat_name = [ @@ -538,49 +668,6 @@ def _group_feature_refs( return fvs_result, odfvs_result -def construct_response_feature_vector( - values_vector: Iterable[Any], - statuses_vector: Iterable[Any], - timestamp_vector: Iterable[Any], - mapping_indexes: Iterable[List[int]], - output_len: int, -) -> GetOnlineFeaturesResponse.FeatureVector: - values_output: Iterable[Any] = [None] * output_len - statuses_output: Iterable[Any] = [None] * output_len - timestamp_output: Iterable[Any] = [None] * output_len - - for i, destinations in enumerate(mapping_indexes): - for idx in destinations: - values_output[idx] = values_vector[i] # type: ignore[index] - statuses_output[idx] = statuses_vector[i] # type: ignore[index] - timestamp_output[idx] = timestamp_vector[i] # type: ignore[index] - - return GetOnlineFeaturesResponse.FeatureVector( - values=values_output, - statuses=statuses_output, - event_timestamps=timestamp_output, - ) - - -def _get_aggregate_operations(agg_specs) -> dict: - """ - Convert Aggregation specs to agg_ops format for PandasBackend. - - Reused from LocalFeatureBuilder logic. - TODO: This logic is duplicated from feast.infra.compute_engines.local.feature_builder.LocalFeatureBuilder._get_aggregate_operations(). - Consider refactoring to a shared utility module in the future. - """ - agg_ops = {} - for agg in agg_specs: - if agg.time_window is not None: - raise ValueError( - "Time window aggregation is not supported in online serving." - ) - alias = f"{agg.function}_{agg.column}" - agg_ops[alias] = (agg.function, agg.column) - return agg_ops - - def _apply_aggregations_to_response( response_data: Union[pyarrow.Table, Dict[str, List[Any]]], aggregations, @@ -616,7 +703,12 @@ def _apply_aggregations_to_response( return response_data # Convert aggregations to agg_ops format - agg_ops = _get_aggregate_operations(aggregations) + agg_ops = aggregation_specs_to_agg_ops( + aggregations, + time_window_unsupported_error_message=( + "Time window aggregation is not supported in online serving." + ), + ) # Apply aggregations using PandasBackend if group_keys: @@ -637,6 +729,7 @@ def _augment_response_with_on_demand_transforms( feature_refs: List[str], requested_on_demand_feature_views: List["OnDemandFeatureView"], full_feature_names: bool, + feature_types: Optional[Dict[str, "ValueType"]] = None, ): """Computes on demand feature values and adds them to the result rows. @@ -659,7 +752,7 @@ def _augment_response_with_on_demand_transforms( odfv_feature_refs = defaultdict(list) for feature_ref in feature_refs: - view_name, feature_name = feature_ref.split(":") + view_name, _, feature_name = _parse_feature_ref(feature_ref) if view_name in requested_odfv_feature_names: odfv_feature_refs[view_name].append( f"{requested_odfv_map[view_name].projection.name_to_use()}__{feature_name}" @@ -667,15 +760,33 @@ def _augment_response_with_on_demand_transforms( else feature_name ) - initial_response = OnlineResponse(online_features_response) + initial_response = OnlineResponse( + online_features_response, feature_types=feature_types + ) initial_response_arrow: Optional[pyarrow.Table] = None initial_response_dict: Optional[Dict[str, List[Any]]] = None + def _is_metrics_active(): + try: + from feast.metrics import _config + + return _config.online_features + except Exception: + return False + + _metrics_active = _is_metrics_active() + # Apply on demand transformations and augment the result rows odfv_result_names = set() for odfv_name, _feature_refs in odfv_feature_refs.items(): odfv = requested_odfv_map[odfv_name] if not odfv.write_to_online_store: + _should_track = _metrics_active and getattr(odfv, "track_metrics", False) + if _should_track: + import time as _time + + _transform_start = _time.monotonic() + # Apply aggregations if configured. if odfv.aggregations: if odfv.mode == "python": @@ -696,27 +807,76 @@ def _augment_response_with_on_demand_transforms( odfv.entities, odfv.mode, ) + continue # Apply transformation. Note: aggregations and transformation configs are mutually exclusive # TODO: Fix to make it work for having both aggregation and transformation # ticket: https://github.com/feast-dev/feast/issues/5689 - elif odfv.mode == "python": + # A UDF should only get features from the sources it declared. Drop + # features that belong to other feature views so it can't read them by + # accident (#6158). Join keys, request data and the declared features + # stay. substrait filters on its own, so leave it alone. + declared_source_names = { + projection.name + for projection in odfv.source_feature_view_projections.values() + } + undeclared_columns: set[str] = set() + # features the caller asked for that aren't one of our sources + for feature_ref in feature_refs: + ref_view_name, _, ref_feature_name = _parse_feature_ref(feature_ref) + if ref_view_name in requested_odfv_feature_names: + continue + if ref_view_name not in declared_source_names: + undeclared_columns.add(ref_feature_name) + undeclared_columns.add(f"{ref_view_name}__{ref_feature_name}") + # features that are only in the response because another requested + # ODFV needs them as input + for other_odfv in requested_on_demand_feature_views: + for projection in other_odfv.source_feature_view_projections.values(): + if projection.name in declared_source_names: + continue + for feature in projection.features: + undeclared_columns.add(feature.name) + undeclared_columns.add(f"{projection.name}__{feature.name}") + + if odfv.mode == "python": if initial_response_dict is None: initial_response_dict = initial_response.to_dict() + odfv_input_dict = { + k: v + for k, v in initial_response_dict.items() + if k not in undeclared_columns + } transformed_features_dict: Dict[str, List[Any]] = odfv.transform_dict( - initial_response_dict + odfv_input_dict ) elif odfv.mode in {"pandas", "substrait"}: if initial_response_arrow is None: initial_response_arrow = initial_response.to_arrow() + odfv_input_arrow = initial_response_arrow + if odfv.mode == "pandas": + odfv_input_arrow = initial_response_arrow.select( + [ + c + for c in initial_response_arrow.column_names + if c not in undeclared_columns + ] + ) transformed_features_arrow = odfv.transform_arrow( - initial_response_arrow, full_feature_names + odfv_input_arrow, full_feature_names ) else: raise Exception( f"Invalid OnDemandFeatureMode: {odfv.mode}. Expected one of 'pandas', 'python', or 'substrait'." ) + if _should_track: + from feast.metrics import track_transformation + + track_transformation( + odfv_name, odfv.mode, _time.monotonic() - _transform_start + ) + transformed_features = ( transformed_features_dict if odfv.mode == "python" @@ -776,13 +936,20 @@ def _get_entity_maps( ) -> Tuple[Dict[str, str], Dict[str, ValueType], Set[str]]: # TODO(felixwang9817): Support entities that have different types for different feature views. entities = registry.list_entities(project, allow_cache=True) + + entity_by_name: Dict[str, "Entity"] = {entity.name: entity for entity in entities} + entity_name_to_join_key_map: Dict[str, str] = {} entity_type_map: Dict[str, ValueType] = {} for entity in entities: entity_name_to_join_key_map[entity.name] = entity.join_key for feature_view in feature_views: for entity_name in feature_view.entities: - entity = registry.get_entity(entity_name, project, allow_cache=True) + entity = entity_by_name.get(entity_name) + if entity is None: + from feast.errors import EntityNotFoundException + + raise EntityNotFoundException(entity_name, project=project) # User directly uses join_key as the entity reference in the entity_rows for the # entity mapping case. entity_name = feature_view.projection.join_key_map.get( @@ -985,98 +1152,6 @@ def ensure_request_data_values_exist( raise RequestDataNotFoundInEntityRowsException(feature_names=missing_features) -def _populate_response_from_feature_data( - feature_data: Iterable[ - Tuple[ - Iterable[Timestamp], Iterable["FieldStatus.ValueType"], Iterable[ValueProto] - ] - ], - indexes: Iterable[List[int]], - online_features_response: GetOnlineFeaturesResponse, - full_feature_names: bool, - requested_features: Iterable[str], - table: "FeatureView", - output_len: int, -): - """Populate the GetOnlineFeaturesResponse with feature data. - - This method assumes that `_read_from_online_store` returns data for each - combination of Entities in `entity_rows` in the same order as they - are provided. - - Args: - feature_data: A list of data in Protobuf form which was retrieved from the OnlineStore. - indexes: A list of indexes which should be the same length as `feature_data`. Each list - of indexes corresponds to a set of result rows in `online_features_response`. - online_features_response: The object to populate. - full_feature_names: A boolean that provides the option to add the feature view prefixes to the feature names, - changing them from the format "feature" to "feature_view__feature" (e.g., "daily_transactions" changes to - "customer_fv__daily_transactions"). - requested_features: The names of the features in `feature_data`. This should be ordered in the same way as the - data in `feature_data`. - table: The FeatureView that `feature_data` was retrieved from. - output_len: The number of result rows in `online_features_response`. - """ - # Add the feature names to the response. - table_name = table.projection.name_to_use() - requested_feature_refs = [ - f"{table_name}__{feature_name}" if full_feature_names else feature_name - for feature_name in requested_features - ] - online_features_response.metadata.feature_names.val.extend(requested_feature_refs) - - # Process each feature vector in a single pass - for timestamp_vector, statuses_vector, values_vector in feature_data: - response_vector = construct_response_feature_vector( - values_vector, statuses_vector, timestamp_vector, indexes, output_len - ) - online_features_response.results.append(response_vector) - - -def _populate_response_from_feature_data_v2( - feature_data: Iterable[ - Tuple[ - Iterable[Timestamp], Iterable["FieldStatus.ValueType"], Iterable[ValueProto] - ] - ], - indexes: Iterable[List[int]], - online_features_response: GetOnlineFeaturesResponse, - requested_features: Iterable[str], - output_len: int, -): - """Populate the GetOnlineFeaturesResponse with feature data. - - This method assumes that `_read_from_online_store` returns data for each - combination of Entities in `entity_rows` in the same order as they - are provided. - - Args: - feature_data: A list of data in Protobuf form which was retrieved from the OnlineStore. - indexes: A list of indexes which should be the same length as `feature_data`. Each list - of indexes corresponds to a set of result rows in `online_features_response`. - online_features_response: The object to populate. - full_feature_names: A boolean that provides the option to add the feature view prefixes to the feature names, - changing them from the format "feature" to "feature_view__feature" (e.g., "daily_transactions" changes to - "customer_fv__daily_transactions"). - requested_features: The names of the features in `feature_data`. This should be ordered in the same way as the - data in `feature_data`. - output_len: The number of result rows in `online_features_response`. - """ - # Add the feature names to the response. - requested_feature_refs = [(feature_name) for feature_name in requested_features] - online_features_response.metadata.feature_names.val.extend(requested_feature_refs) - - timestamps, statuses, values = zip(*feature_data) - - # Populate the result with data fetched from the OnlineStore - # which is guaranteed to be aligned with `requested_features`. - for timestamp_vector, statuses_vector, values_vector in feature_data: - response_vector = construct_response_feature_vector( - values_vector, statuses_vector, timestamp_vector, indexes, output_len - ) - online_features_response.results.append(response_vector) - - def _convert_entity_key_to_proto_to_dict( entity_key_vals: List[EntityKeyProto], ) -> Dict[str, List[ValueProto]]: @@ -1108,6 +1183,15 @@ def _get_features( _feature_refs = [] if isinstance(_features, FeatureService): + # Create cache key for feature service resolution + cache_key = f"{_features.name}:{project}:{hash(tuple(str(fv) for fv in _features.feature_view_projections))}" + + # Check cache first if caching is enabled and available + if allow_cache and hasattr(registry, "_feature_service_cache"): + if cache_key in registry._feature_service_cache: + return registry._feature_service_cache[cache_key] + + # Resolve feature service from registry feature_service_from_registry = registry.get_feature_service( _features.name, project, allow_cache ) @@ -1117,10 +1201,16 @@ def _get_features( "inconsistent with the version from the registry. Potentially a newer version " "of the FeatureService has been applied to the registry." ) + + # Build feature reference list for projection in feature_service_from_registry.feature_view_projections: _feature_refs.extend( [f"{projection.name_to_use()}:{f.name}" for f in projection.features] ) + + # Cache the result if caching is enabled and available + if allow_cache and hasattr(registry, "_feature_service_cache"): + registry._feature_service_cache[cache_key] = _feature_refs else: assert isinstance(_features, list) _feature_refs = _features @@ -1145,6 +1235,19 @@ def _list_feature_views( return feature_views +def _merge_projection_features( + target: "FeatureViewProjection", + other: "FeatureViewProjection", +) -> None: + existing_names = {feature.name for feature in target.features} + merged = list(target.features) + for feature in other.features: + if feature.name not in existing_names: + existing_names.add(feature.name) + merged.append(feature) + target.features = merged + + def _get_feature_views_to_use( registry: "BaseRegistry", project, @@ -1158,16 +1261,86 @@ def _get_feature_views_to_use( if isinstance(features, FeatureService): feature_views = [ - (projection.name, projection) + (projection.name, None, projection) for projection in features.feature_view_projections ] else: assert features is not None - feature_views = [(feature.split(":")[0], None) for feature in features] # type: ignore[misc] + # Parse version-qualified refs: 'fv@v2:feat' -> ('fv', 2, None) + parsed = [] + seen = set() + for feature in features: + fv_name, version_num, _ = _parse_feature_ref(feature) + key = (fv_name, version_num) + if key not in seen: + seen.add(key) + parsed.append((fv_name, version_num, None)) + feature_views = parsed # type: ignore[assignment] fvs_to_use, od_fvs_to_use = [], [] - for name, projection in feature_views: - fv = registry.get_any_feature_view(name, project, allow_cache) + fvs_by_projection_key: Dict[str, "FeatureView"] = {} + + def _append_or_merge_source_fv(fv_with_projection: "FeatureView") -> None: + # Index every (source) feature view by its effective projection key so + # that a regular FeatureView and an ODFV source projection resolving to + # the same key are merged into a single entry instead of duplicated. + # Without this, requesting ["src_fv:a", "odfv_b:b_out"] where odfv_b + # sources src_fv appends two src_fv entries and later raises + # "KeyError: Feature a not found in projection src_fv". + projection_key = fv_with_projection.projection.name_to_use() + existing = fvs_by_projection_key.get(projection_key) + if existing is None: + fvs_by_projection_key[projection_key] = fv_with_projection + fvs_to_use.append(fv_with_projection) + else: + _merge_projection_features( + existing.projection, fv_with_projection.projection + ) + + for name, version_num, projection in feature_views: + if version_num is not None: + if not getattr(registry, "enable_online_versioning", False): + raise ValueError( + f"Version-qualified ref '{name}@v{version_num}' not supported: " + f"online versioning is disabled. Set 'enable_online_feature_view_versioning: true' " + f"under 'registry' in feature_store.yaml." + ) + # Version-qualified reference: look up the specific version snapshot + try: + fv = registry.get_feature_view_by_version( + name, project, version_num, allow_cache + ) + except NotImplementedError: + # Fall back for v0 on registries that don't implement versioned lookup + if version_num == 0: + fv = registry.get_any_feature_view(name, project, allow_cache) + else: + raise + # Set version_tag on the projection so name_to_use() returns versioned key + if hasattr(fv, "projection") and fv.projection is not None: + fv.projection.version_tag = version_num + else: + fv = registry.get_any_feature_view(name, project, allow_cache) + + if hasattr(fv, "enabled") and not fv.enabled: + raise ValueError( + f"Feature view '{name}' is disabled and cannot serve features. " + f"Enable it with `feast feature-views enable {name}` or set enabled=True." + ) + + # Enforce lifecycle state gate: only serve if state is AVAILABLE_ONLINE + # or STATE_UNSPECIFIED (backward compat for pre-state feature views). + if hasattr(fv, "state"): + from feast.feature_view import FeatureViewState + + if isinstance(fv.state, FeatureViewState) and fv.state not in ( + FeatureViewState.STATE_UNSPECIFIED, + FeatureViewState.AVAILABLE_ONLINE, + ): + raise ValueError( + f"Feature view '{name}' is in state '{fv.state.name}' " + f"and cannot serve features. Only AVAILABLE_ONLINE feature views can serve." + ) if isinstance(fv, OnDemandFeatureView): od_fvs_to_use.append( @@ -1187,10 +1360,8 @@ def _get_feature_views_to_use( source_fv.entities = [] # type: ignore[attr-defined] source_fv.entity_columns = [] # type: ignore[attr-defined] - if source_fv not in fvs_to_use: - fvs_to_use.append( - source_fv.with_projection(copy.copy(source_projection)) - ) + new_source_fv = source_fv.with_projection(copy.copy(source_projection)) + _append_or_merge_source_fv(new_source_fv) else: if ( hide_dummy_entity @@ -1199,9 +1370,11 @@ def _get_feature_views_to_use( ): fv.entities = [] # type: ignore[attr-defined] fv.entity_columns = [] # type: ignore[attr-defined] - fvs_to_use.append( - fv.with_projection(copy.copy(projection)) if projection else fv - ) + if projection: + fv = fv.with_projection(copy.copy(projection)) + if version_num is not None: + fv.projection.version_tag = version_num + _append_or_merge_source_fv(fv) # type: ignore[arg-type] return (fvs_to_use, od_fvs_to_use) @@ -1243,13 +1416,20 @@ def _get_online_request_context( requested_on_demand_feature_views, ) - requested_result_row_names = { - feat_ref.replace(":", "__") for feat_ref in _feature_refs - } - if not full_feature_names: - requested_result_row_names = { - name.rpartition("__")[-1] for name in requested_result_row_names - } + # Build expected result names, including version tag when present so + # multi-version queries (e.g. fv@v1:feat, fv@v2:feat) match the response. + requested_result_row_names = set() + for feat_ref in _feature_refs: + fv_name, version_num, feature_name = _parse_feature_ref(feat_ref) + if full_feature_names: + if version_num is not None: + requested_result_row_names.add( + f"{fv_name}@v{version_num}__{feature_name}" + ) + else: + requested_result_row_names.add(f"{fv_name}__{feature_name}") + else: + requested_result_row_names.add(feature_name) feature_views = list(view for view, _ in grouped_refs) @@ -1274,6 +1454,96 @@ def _get_online_request_context( ) +_feature_resolution_cache: Dict[tuple, tuple] = {} +_feature_resolution_cache_lock = threading.Lock() +_feature_resolution_registry_ts: Optional[datetime] = None + +_logger = logging.getLogger(__name__) + + +def _get_cached_request_context( + registry, + project: str, + features: Union[List[str], "FeatureService"], + full_feature_names: bool, +): + """Return the output of _get_online_request_context plus resolved ODFV + entity join keys, using a cache that is invalidated whenever the + registry refreshes or its cache TTL expires.""" + from feast.feature_service import FeatureService as _FS + + global _feature_resolution_cache, _feature_resolution_registry_ts + + registry_ts = getattr(registry, "cached_registry_proto_created", None) + + if isinstance(features, _FS): + features_key: tuple = ("__fs__", features.name) + else: + features_key = tuple(features) + + cache_key = (features_key, project, full_feature_names) + + is_cache_valid = getattr(registry, "is_cache_valid", None) + registry_cache_valid = is_cache_valid() if callable(is_cache_valid) else False + + if registry_ts is not None and registry_cache_valid: + with _feature_resolution_cache_lock: + if registry_ts != _feature_resolution_registry_ts: + _feature_resolution_cache.clear() + _feature_resolution_registry_ts = registry_ts + _logger.debug("Feature resolution cache cleared (registry refreshed)") + else: + cached = _feature_resolution_cache.get(cache_key) + if cached is not None: + return cached + + ctx = _get_online_request_context(registry, project, features, full_feature_names) + + ( + feature_refs, + requested_on_demand_feature_views, + entity_name_to_join_key_map, + entity_type_map, + join_keys_set, + grouped_refs, + requested_result_row_names, + needed_request_data, + entityless_case, + ) = ctx + + odfv_join_keys: set = set() + for on_demand_feature_view in requested_on_demand_feature_views: + entities_for_odfv = getattr(on_demand_feature_view, "entities", []) + if len(entities_for_odfv) > 0 and isinstance(entities_for_odfv[0], str): + entities_for_odfv = [ + registry.get_entity(entity_name, project, allow_cache=True) + for entity_name in entities_for_odfv + ] + odfv_join_keys.update(entities_for_odfv) + + result = ( + feature_refs, + requested_on_demand_feature_views, + entity_name_to_join_key_map, + entity_type_map, + join_keys_set | odfv_join_keys, + grouped_refs, + frozenset(requested_result_row_names), + needed_request_data, + entityless_case, + ) + + registry_ts_after = getattr(registry, "cached_registry_proto_created", None) + if registry_ts_after is not None: + with _feature_resolution_cache_lock: + if registry_ts_after != _feature_resolution_registry_ts: + _feature_resolution_cache.clear() + _feature_resolution_registry_ts = registry_ts_after + _feature_resolution_cache[cache_key] = result + + return result + + def _prepare_entities_to_read_from_online_store( registry, project, @@ -1293,10 +1563,13 @@ def _prepare_entities_to_read_from_online_store( entity_type_map, join_keys_set, grouped_refs, - requested_result_row_names, + requested_result_row_names_frozen, needed_request_data, entityless_case, - ) = _get_online_request_context(registry, project, features, full_feature_names) + ) = _get_cached_request_context(registry, project, features, full_feature_names) + + # Mutable copy — downstream code adds join keys to this set. + requested_result_row_names = set(requested_result_row_names_frozen) # Extract Sequence from RepeatedValue Protobuf. entity_value_lists: Dict[str, Union[List[Any], List[ValueProto]]] = { @@ -1318,23 +1591,6 @@ def _prepare_entities_to_read_from_online_store( num_rows = _validate_entity_values(entity_proto_values) - odfv_entities: List[Entity] = [] - request_source_keys: List[str] = [] - for on_demand_feature_view in requested_on_demand_feature_views: - entities_for_odfv = getattr(on_demand_feature_view, "entities", []) - if len(entities_for_odfv) > 0 and isinstance(entities_for_odfv[0], str): - entities_for_odfv = [ - registry.get_entity(entity_name, project, allow_cache=True) - for entity_name in entities_for_odfv - ] - odfv_entities.extend(entities_for_odfv) - for source in on_demand_feature_view.source_request_sources: - source_schema = on_demand_feature_view.source_request_sources[source].schema - for column in source_schema: - request_source_keys.append(column.name) - - join_keys_set.update(set(odfv_entities)) - join_key_values: Dict[str, List[ValueProto]] = {} request_data_features: Dict[str, List[ValueProto]] = {} # Entity rows may be either entities or request data. @@ -1395,36 +1651,108 @@ def _get_entity_key_protos( return entity_key_protos -def _convert_rows_to_protobuf( +def _populate_response_from_feature_data( requested_features: List[str], read_rows: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]], -) -> List[Tuple[List[Timestamp], List["FieldStatus.ValueType"], List[ValueProto]]]: - # Pre-calculate the length to avoid repeated calculations - n_rows = len(read_rows) + indexes: Iterable[List[int]], + online_features_response: GetOnlineFeaturesResponse, + full_feature_names: bool, + table: "FeatureView", + output_len: int, + include_feature_view_version_metadata: bool = False, +): + """Populate the GetOnlineFeaturesResponse from raw online_read rows. + + Converts raw rows from the OnlineStore into protobuf FeatureVectors and + appends them to the response. This method assumes that ``online_read`` + returns data for each unique entity in the same order as ``indexes``. + + Args: + requested_features: The names of the features to extract from + each row. Determines the order of FeatureVectors in the response. + read_rows: Raw output from ``OnlineStore.online_read`` — a list of + ``(event_timestamp, feature_dict)`` tuples, one per unique entity. + ``feature_dict`` may be ``None`` when the entity is not found. + indexes: A tuple of lists that maps each unique entity (by position + in ``read_rows``) to one or more output positions in the response. + Used to fan-out deduplicated reads back to the original request rows. + online_features_response: The protobuf response object to populate. + full_feature_names: If True, feature names are prefixed with the + feature view name (e.g. ``"driver_fv__trips_today"``). + table: The FeatureView that ``read_rows`` was retrieved from. + output_len: Total number of result rows in the response. + include_feature_view_version_metadata: If True, version metadata + for the feature view is added to the response. + """ + n_features = len(requested_features) + + table_name = table.projection.name_to_use() + clean_table_name = table.projection.name_alias or table.projection.name + feature_refs = [ + f"{table_name}__{fn}" if full_feature_names else fn for fn in requested_features + ] + online_features_response.metadata.feature_names.val.extend(feature_refs) + + if include_feature_view_version_metadata: + existing_names = [ + fvm.name for fvm in online_features_response.metadata.feature_view_metadata + ] + if clean_table_name not in existing_names: + fv_metadata = online_features_response.metadata.feature_view_metadata.add() + fv_metadata.name = clean_table_name + fv_metadata.version = getattr(table, "current_version_number", 0) or 0 - # Create single instances of commonly used values null_value = ValueProto() - null_status = FieldStatus.NOT_FOUND - null_timestamp = Timestamp() - present_status = FieldStatus.PRESENT - - requested_features_vectors = [] - for feature_name in requested_features: - ts_vector = [null_timestamp] * n_rows - status_vector = [null_status] * n_rows - value_vector = [null_value] * n_rows - for idx, read_row in enumerate(read_rows): - row_ts_proto = Timestamp() - row_ts, feature_data = read_row - # TODO (Ly): reuse whatever timestamp if row_ts is None? - if row_ts is not None: - row_ts_proto.FromDatetime(row_ts) - ts_vector[idx] = row_ts_proto - if (feature_data is not None) and (feature_name in feature_data): - status_vector[idx] = present_status - value_vector[idx] = feature_data[feature_name] - requested_features_vectors.append((ts_vector, status_vector, value_vector)) - return requested_features_vectors + null_ts = Timestamp() + PRESENT = FieldStatus.PRESENT + NOT_FOUND = FieldStatus.NOT_FOUND + + row_ts_protos = [] + for row_ts, _ in read_rows: + ts = Timestamp() + if row_ts is not None: + ts.FromDatetime(row_ts) + row_ts_protos.append(ts) + + ts_template = [null_ts] * output_len + indexes_tuple = tuple(indexes) + for row_idx, destinations in enumerate(indexes_tuple): + ts = row_ts_protos[row_idx] + for out_idx in destinations: + ts_template[out_idx] = ts + + feat_values = [[null_value] * output_len for _ in range(n_features)] + feat_statuses = [[NOT_FOUND] * output_len for _ in range(n_features)] + + feat_idx_map = {name: i for i, name in enumerate(requested_features)} + for row_idx, destinations in enumerate(indexes_tuple): + _, feature_data = read_rows[row_idx] + if feature_data is None: + continue + for feat_name, feat_val in feature_data.items(): + f_idx = feat_idx_map.get(feat_name) + if f_idx is not None: + for out_idx in destinations: + feat_values[f_idx][out_idx] = feat_val + feat_statuses[f_idx][out_idx] = PRESENT + + try: + from feast.metrics import track_feature_statuses + + _present = sum(s == PRESENT for row in feat_statuses for s in row) + _not_found = (n_features * output_len) - _present + track_feature_statuses(table.name, _present, _not_found) + except Exception: + pass + + for f_idx in range(n_features): + online_features_response.results.append( + GetOnlineFeaturesResponse.FeatureVector( + values=feat_values[f_idx], + statuses=feat_statuses[f_idx], + event_timestamps=list(ts_template), + ) + ) def has_all_tags( @@ -1523,3 +1851,24 @@ def _get_feature_view_vector_field_metadata( if not vector_fields: return None return vector_fields[0] + + +def _distance_to_score(distance: float, metric: Optional[str] = None) -> float: + """Convert a raw distance value into a higher-is-better relevance score. + + OpenAI clients treat ``score`` as higher-is-better (used with + ``score_threshold``). Online stores return raw *distance* values where + lower is better, so a metric-aware conversion is required. + + * **L2 / euclidean**: ``1 / (1 + distance)`` — maps [0, inf) to (0, 1] + * **cosine** (pgvector ``<=>`` returns ``1 - cosine_similarity``): + ``1 - distance`` — recovers cosine similarity, clamped >= 0 + * **inner_product / ip / dot** (pgvector ``<#>`` returns ``-dot(a, b)``): + ``-distance`` — recovers the raw inner product + """ + m = (metric or "L2").lower() + if m in ("cosine",): + return max(0.0, 1.0 - distance) + if m in ("inner_product", "ip", "dot"): + return -distance + return 1.0 / (1.0 + distance) diff --git a/sdk/python/feast/value_type.py b/sdk/python/feast/value_type.py index 8cf748c9392..0ea16002f80 100644 --- a/sdk/python/feast/value_type.py +++ b/sdk/python/feast/value_type.py @@ -16,12 +16,19 @@ from feast.protos.feast.types.Value_pb2 import ( BoolList, + BoolSet, BytesList, + BytesSet, DoubleList, + DoubleSet, FloatList, + FloatSet, Int32List, + Int32Set, Int64List, + Int64Set, StringList, + StringSet, ) @@ -48,8 +55,35 @@ class ValueType(enum.Enum): BOOL_LIST = 17 UNIX_TIMESTAMP_LIST = 18 NULL = 19 - PDF_BYTES = 20 - IMAGE_BYTES = 21 + MAP = 20 + MAP_LIST = 21 + BYTES_SET = 22 + STRING_SET = 23 + INT32_SET = 24 + INT64_SET = 25 + DOUBLE_SET = 26 + FLOAT_SET = 27 + BOOL_SET = 28 + UNIX_TIMESTAMP_SET = 29 + PDF_BYTES = 30 + IMAGE_BYTES = 31 + JSON = 32 + JSON_LIST = 33 + STRUCT = 34 + STRUCT_LIST = 35 + UUID = 36 + TIME_UUID = 37 + UUID_LIST = 38 + TIME_UUID_LIST = 39 + UUID_SET = 40 + TIME_UUID_SET = 41 + VALUE_LIST = 42 + VALUE_SET = 43 + DECIMAL = 44 + DECIMAL_LIST = 45 + DECIMAL_SET = 46 + SCALAR_MAP = 47 + ZONED_TIMESTAMP = 48 ListType = Union[ @@ -61,3 +95,13 @@ class ValueType(enum.Enum): Type[Int64List], Type[StringList], ] + +SetType = Union[ + Type[BoolSet], + Type[BytesSet], + Type[DoubleSet], + Type[FloatSet], + Type[Int32Set], + Type[Int64Set], + Type[StringSet], +] diff --git a/sdk/python/feast/vector_store_utils.py b/sdk/python/feast/vector_store_utils.py new file mode 100644 index 00000000000..e04a7bb4936 --- /dev/null +++ b/sdk/python/feast/vector_store_utils.py @@ -0,0 +1,66 @@ +import hashlib +from typing import Any, Dict, List, Optional + +from feast.errors import FeatureViewNotFoundException +from feast.feature_view import FeatureView + + +def feature_view_to_vs_id(project: str, feature_view_name: str) -> str: + digest = hashlib.sha256(f"{project}:{feature_view_name}".encode()).hexdigest()[:24] + return f"vs_{digest}" + + +def _has_vector_index(fv: FeatureView) -> bool: + return any(getattr(f, "vector_index", False) for f in fv.schema) + + +def build_vector_store_object(project: str, fv: FeatureView) -> Dict[str, Any]: + return { + "id": feature_view_to_vs_id(project, fv.name), + "object": "vector_store", + "name": fv.name, + "status": "completed", + "created_at": int(fv.created_timestamp.timestamp()) + if fv.created_timestamp + else 0, + } + + +class VectorStoreRegistry: + """Cached mapping from vs_{hash} IDs to FeatureViews. + + Built once at server startup, refreshed on registry TTL cycle. + """ + + def __init__(self, store: Any): + self._store = store + self._id_to_fv: Dict[str, FeatureView] = {} + self._vector_store_objects: List[Dict[str, Any]] = [] + self.refresh() + + def refresh(self) -> None: + project = self._store.project + id_to_fv: Dict[str, FeatureView] = {} + objects: List[Dict[str, Any]] = [] + for fv in self._store.list_feature_views(): + if _has_vector_index(fv): + vs_id = feature_view_to_vs_id(project, fv.name) + id_to_fv[vs_id] = fv + objects.append(build_vector_store_object(project, fv)) + self._id_to_fv = id_to_fv + self._vector_store_objects = objects + + def resolve(self, vs_id: str) -> FeatureView: + fv = self._id_to_fv.get(vs_id) + if fv is None: + raise FeatureViewNotFoundException(vs_id) + return fv + + def list_vector_stores(self) -> List[Dict[str, Any]]: + return self._vector_store_objects + + def get_vector_store(self, vs_id: str) -> Optional[Dict[str, Any]]: + fv = self._id_to_fv.get(vs_id) + if fv is None: + return None + return build_vector_store_object(self._store.project, fv) diff --git a/sdk/python/feast/version_utils.py b/sdk/python/feast/version_utils.py new file mode 100644 index 00000000000..5811e89e936 --- /dev/null +++ b/sdk/python/feast/version_utils.py @@ -0,0 +1,51 @@ +import re +import uuid +from typing import Tuple + +LATEST_VERSION = "latest" +_VERSION_PATTERN = re.compile(r"^v(?:ersion)?(\d+)$", re.IGNORECASE) + + +def parse_version(version: str) -> Tuple[bool, int]: + """Parse a version string into (is_latest, version_number). + + Accepts "latest", "vN", or "versionN" (case-insensitive). + Returns (True, 0) for "latest", (False, N) for pinned versions. + + Raises: + ValueError: If the version string is invalid. + """ + if not version or version.lower() == LATEST_VERSION: + return True, 0 + + match = _VERSION_PATTERN.match(version) + if not match: + raise ValueError( + f"Invalid version string '{version}'. " + f"Expected 'latest', 'vN', or 'versionN' (e.g. 'v2', 'version3')." + ) + return False, int(match.group(1)) + + +def normalize_version_string(version: str) -> str: + """Normalize a version string for comparison. + + Empty string and "latest" both normalize to "latest". + "v2" and "version2" both normalize to "v2". + """ + if not version or version.lower() == LATEST_VERSION: + return LATEST_VERSION + is_latest, num = parse_version(version) + if is_latest: + return LATEST_VERSION + return version_tag(num) + + +def version_tag(n: int) -> str: + """Convert an integer version number to the canonical short form 'vN'.""" + return f"v{n}" + + +def generate_version_id() -> str: + """Generate a UUID for a version record.""" + return str(uuid.uuid4()) diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 8a1c5b70c3b..a81987bd0be 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -1,5 +1,5 @@ [tool.ruff] -exclude = [".git","__pycache__","docs/conf.py","dist","feast/protos","feast/embedded_go/lib","feast/infra/utils/snowflake/snowpark/snowflake_udfs.py"] +exclude = [".git","__pycache__","docs/conf.py","dist","feast/protos","feast/embedded_go/lib","feast/infra/utils/snowflake/snowpark/snowflake_udfs.py","**/node_modules","**/*_pb2.py","**/*_pb2.pyi","**/*_pb2_grpc.py"] [tool.ruff.lint] select = ["E","F","W","I"] @@ -10,6 +10,16 @@ known-first-party = ["feast", "feast_serving_server", "feast_core_server"] default-section = "third-party" [tool.mypy] -files = ["feast","tests"] +files = ["feast", "tests"] ignore_missing_imports = true exclude = ["feast/embedded_go/lib"] +# Performance optimizations +incremental = true +cache_dir = ".mypy_cache" +sqlite_cache = true +warn_unused_configs = true +show_column_numbers = true + +[[tool.mypy.overrides]] +module = "transformers.*" +follow_imports = "skip" diff --git a/sdk/python/pytest.ini b/sdk/python/pytest.ini index d79459c0d0e..837c2112be2 100644 --- a/sdk/python/pytest.ini +++ b/sdk/python/pytest.ini @@ -1,20 +1,29 @@ [pytest] asyncio_mode = auto - -markers = - universal_offline_stores: mark a test as using all offline stores. - universal_online_stores: mark a test as using all online stores. - rbac_remote_integration_test: mark a integration test related to rbac and remote functionality. - env = IS_TEST=True - filterwarnings = - error::_pytest.warning_types.PytestConfigWarning - error::_pytest.warning_types.PytestUnhandledCoroutineWarning + error::pytest.PytestConfigWarning + error:.*was never awaited.*:RuntimeWarning ignore::DeprecationWarning:pyspark.sql.pandas.*: ignore::DeprecationWarning:pyspark.sql.connect.*: ignore::DeprecationWarning:httpx.*: ignore::DeprecationWarning:happybase.*: ignore::DeprecationWarning:pkg_resources.*: - ignore::FutureWarning:ibis_substrait.compiler.*: + +markers = + universal_offline_stores: Tests using all offline stores + universal_online_stores: Tests using all online stores + rbac_remote_integration_test: RBAC and remote functionality tests + integration: Integration tests (slower, requires services) + benchmark: Benchmark tests + slow: Tests taking >30 seconds + cloud: Tests requiring cloud credentials + local_only: Tests that run entirely locally + xdist_group: Group tests to run in the same xdist worker + mongodb: Tests requiring MongoDB + +timeout = 300 +timeout_method = thread + +addopts = --tb=short -v --durations=20 --strict-markers diff --git a/sdk/python/requirements/py3.10-ci-requirements.txt b/sdk/python/requirements/py3.10-ci-requirements.txt index 36097195ebe..d431f142320 100644 --- a/sdk/python/requirements/py3.10-ci-requirements.txt +++ b/sdk/python/requirements/py3.10-ci-requirements.txt @@ -1,144 +1,144 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.10 --no-strip-extras setup.py --extra ci --generate-hashes --output-file sdk/python/requirements/py3.10-ci-requirements.txt -accelerate==1.10.1 \ - --hash=sha256:3621cff60b9a27ce798857ece05e2b9f56fcc71631cfb31ccf71f0359c311f11 \ - --hash=sha256:3dea89e433420e4bfac0369cae7e36dcd6a56adfcfd38cdda145c6225eab5df8 +# uv pip compile -p 3.10 --no-strip-extras pyproject.toml --extra ci --generate-hashes --output-file sdk/python/requirements/py3.10-ci-requirements.txt +accelerate==1.14.0 \ + --hash=sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d \ + --hash=sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6 # via docling-ibm-models -aiobotocore==2.23.1 \ - --hash=sha256:a59f2a78629b97d52f10936b79c73de64e481a8c44a62c1871f088df6c1afc4f \ - --hash=sha256:d81c54d2eae2406ea9a473fea518fed580cf37bc4fc51ce43ba81546e5305114 - # via feast (setup.py) -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +aiobotocore==3.8.0 \ + --hash=sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670 \ + --hash=sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + # via feast (pyproject.toml) +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.0 \ - --hash=sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6 \ - --hash=sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a \ - --hash=sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e \ - --hash=sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971 \ - --hash=sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54 \ - --hash=sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2 \ - --hash=sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96 \ - --hash=sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469 \ - --hash=sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26 \ - --hash=sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0 \ - --hash=sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29 \ - --hash=sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885 \ - --hash=sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579 \ - --hash=sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a \ - --hash=sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1 \ - --hash=sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b \ - --hash=sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852 \ - --hash=sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865 \ - --hash=sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad \ - --hash=sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092 \ - --hash=sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7 \ - --hash=sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b \ - --hash=sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8 \ - --hash=sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820 \ - --hash=sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd \ - --hash=sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee \ - --hash=sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4 \ - --hash=sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67 \ - --hash=sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d \ - --hash=sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1 \ - --hash=sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d \ - --hash=sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114 \ - --hash=sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1 \ - --hash=sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755 \ - --hash=sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04 \ - --hash=sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca \ - --hash=sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1 \ - --hash=sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae \ - --hash=sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793 \ - --hash=sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e \ - --hash=sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a \ - --hash=sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8 \ - --hash=sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602 \ - --hash=sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778 \ - --hash=sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f \ - --hash=sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49 \ - --hash=sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77 \ - --hash=sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7 \ - --hash=sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f \ - --hash=sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0 \ - --hash=sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519 \ - --hash=sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d \ - --hash=sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370 \ - --hash=sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7 \ - --hash=sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247 \ - --hash=sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d \ - --hash=sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80 \ - --hash=sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9 \ - --hash=sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c \ - --hash=sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d \ - --hash=sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020 \ - --hash=sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504 \ - --hash=sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca \ - --hash=sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731 \ - --hash=sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd \ - --hash=sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6 \ - --hash=sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21 \ - --hash=sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1 \ - --hash=sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620 \ - --hash=sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64 \ - --hash=sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8 \ - --hash=sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3 \ - --hash=sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5 \ - --hash=sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985 \ - --hash=sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813 \ - --hash=sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed \ - --hash=sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20 \ - --hash=sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03 \ - --hash=sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702 \ - --hash=sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6 \ - --hash=sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed \ - --hash=sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353 \ - --hash=sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108 \ - --hash=sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f \ - --hash=sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409 \ - --hash=sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7 \ - --hash=sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3 \ - --hash=sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9 \ - --hash=sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba \ - --hash=sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe \ - --hash=sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c \ - --hash=sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861 \ - --hash=sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a \ - --hash=sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0 \ - --hash=sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390 \ - --hash=sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3 \ - --hash=sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90 \ - --hash=sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802 \ - --hash=sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163 \ - --hash=sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d \ - --hash=sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430 \ - --hash=sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a \ - --hash=sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b \ - --hash=sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6 \ - --hash=sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b \ - --hash=sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657 \ - --hash=sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c \ - --hash=sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93 \ - --hash=sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f \ - --hash=sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82 \ - --hash=sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31 \ - --hash=sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc \ - --hash=sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52 \ - --hash=sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c \ - --hash=sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a \ - --hash=sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52 \ - --hash=sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7 \ - --hash=sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b \ - --hash=sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147 \ - --hash=sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 # via # aiobotocore # fsspec -aioitertools==0.12.0 \ - --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \ - --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796 + # kubernetes +aioitertools==0.13.0 \ + --hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \ + --hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c # via aiobotocore aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ @@ -148,19 +148,29 @@ alabaster==0.7.16 \ --hash=sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65 \ --hash=sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 # via sphinx +alembic==1.18.5 \ + --hash=sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc \ + --hash=sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e + # via mlflow altair==4.2.2 \ --hash=sha256:39399a267c49b30d102c10411e67ab26374156a84b1aeb9fcd15140429ba49c5 \ --hash=sha256:8b45ebeaf8557f2d760c5c77b79f02ae12aee7c46c27c06014febab6f849bc87 # via great-expectations -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via fastapi +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via + # elasticsearch # httpx + # httpx2 # jupyter-server # mcp # sse-starlette @@ -204,9 +214,9 @@ argon2-cffi-bindings==25.1.0 \ --hash=sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520 \ --hash=sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb # via argon2-cffi -arrow==1.3.0 \ - --hash=sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80 \ - --hash=sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85 +arrow==1.4.0 \ + --hash=sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205 \ + --hash=sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7 # via isoduration asn1crypto==1.5.1 \ --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ @@ -214,14 +224,14 @@ asn1crypto==1.5.1 \ # via snowflake-connector-python assertpy==1.1 \ --hash=sha256:acc64329934ad71a3221de185517a43af33e373bb44dc05b5a9b174394ef4833 - # via feast (setup.py) -asttokens==3.0.0 \ - --hash=sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7 \ - --hash=sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2 + # via feast (pyproject.toml) +asttokens==3.0.2 \ + --hash=sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2 \ + --hash=sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933 # via stack-data -async-lru==2.0.5 \ - --hash=sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb \ - --hash=sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943 +async-lru==2.3.0 \ + --hash=sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6 \ + --hash=sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315 # via jupyterlab async-property==0.2.2 \ --hash=sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380 \ @@ -233,116 +243,208 @@ async-timeout==5.0.1 \ # via # aiohttp # redis -atpublic==5.1 \ - --hash=sha256:135783dbd887fbddb6ef032d104da70c124f2b44b9e2d79df07b9da5334825e3 \ - --hash=sha256:abc1f4b3dbdd841cc3539e4b5e4f3ad41d658359de704e30cb36da4d4e9d3022 +atpublic==7.0.0 \ + --hash=sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e \ + --hash=sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b # via ibis-framework -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via + # feast (pyproject.toml) # aiohttp # jsonlines # jsonschema + # openlineage-python # referencing -azure-core==1.35.1 \ - --hash=sha256:12da0c9e08e48e198f9158b56ddbe33b421477e1dc98c2e1c8f9e254d92c468b \ - --hash=sha256:435d05d6df0fff2f73fb3c15493bb4721ede14203f1ff1382aa6b6b2bdd7e562 +azure-core==1.41.0 \ + --hash=sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d \ + --hash=sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a # via # azure-identity # azure-storage-blob -azure-identity==1.25.1 \ - --hash=sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456 \ - --hash=sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651 - # via feast (setup.py) -azure-storage-blob==12.26.0 \ - --hash=sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f \ - --hash=sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe - # via feast (setup.py) -babel==2.17.0 \ - --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ - --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 +azure-identity==1.25.3 \ + --hash=sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6 \ + --hash=sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c + # via feast (pyproject.toml) +azure-storage-blob==12.30.0 \ + --hash=sha256:2cd74d4d5731e5eb6b8d5c5056ee115a5e88f8fdf22517b739836fda685018be \ + --hash=sha256:d415ac50b67a8da6b3ae7e9f1014b1b55cd7aafa0b8d4ca9b380568dc7360423 + # via feast (pyproject.toml) +babel==2.18.0 \ + --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ + --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 # via # jupyterlab-server # sphinx -beautifulsoup4==4.14.2 \ - --hash=sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e \ - --hash=sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515 +backports-zstd==1.6.0 \ + --hash=sha256:0308990ffc998df3c7ed35276bde049728b5c3956203cae40d80893576a41459 \ + --hash=sha256:03b7c59c71f7a597e2bcb3f8368371e9a660a1bdf1c37afc1f1ad1496a013c19 \ + --hash=sha256:0466b14723f3b7697669c00ee66fe16e30e25636b286b0a923fa86fa3d8a753c \ + --hash=sha256:068ef3d8c18815a2e3a752f766313e19910e7c50939b956923748d9c04ebcb1b \ + --hash=sha256:08793876172551a930ce4d65c712cd516184d1a97070d4a1193e05bf0cf7040d \ + --hash=sha256:0eb4281f402b94d397b7482f6d9efd04c28274e4ed6eb57eb1f87bdd091a6a87 \ + --hash=sha256:10b61850c4112952e05aa6e6cce8c9a5936fbeadb321e154216705cc76a14afa \ + --hash=sha256:15b1aae0f64cd742df4bba1d989d0a09a6ec619202543fdba684640454541fd3 \ + --hash=sha256:17efb3d11137de5166dd51eedab9c36ad633402acba386eee8d715213ea47e49 \ + --hash=sha256:1a99710fbb225d459d66def4dc2bb2cd4a9a0bdc8b799fc0621cfdd863be9c93 \ + --hash=sha256:1d146926e997d2d3de8212bdcbf4985344a2622ca3bec458d8908000a84fd883 \ + --hash=sha256:1e20b3ecd0a711be82e964aca28554eabbc31ee69a20e5e7b8fd42268af46212 \ + --hash=sha256:23a793f2fed4dbf0517319759a2cded0b0dd8e8d3797fe30badd5693e320c175 \ + --hash=sha256:25b5ddc789480072551af571a746e9500356b2aff0499861cf2ca07ea7431e68 \ + --hash=sha256:28eef3881164f3c23ce58ed59e4684103bdd279583eb2d299858c9e9b72fde9a \ + --hash=sha256:28fecd73459d74910ae1987ab84b7bef690d3dd860948430dd5555108b006daf \ + --hash=sha256:2914abea516704bdafb2090acd3f15b5f9debecfabd15b8dd8285b2ad3b92209 \ + --hash=sha256:2ace939e4d620e119423606f2d3d7115f8707733bf57f279ad9a9383f875986f \ + --hash=sha256:2ba9ac10fc393e5123a08802e0e895a107cb4a66b9973d2844dbd8a343111e59 \ + --hash=sha256:2ddab55a5f54dec8acfad68ef70f1c704fd21919990ddc238afbd6f496e61c6a \ + --hash=sha256:2f723219335387d7546412d8141e0303590600949b4184a1391a0c6a3c756058 \ + --hash=sha256:30576f49b82328ec8af16c11100efe52ca88526f71bbe100ef6b4e707dc13bf2 \ + --hash=sha256:32f04d54ec1fdf3aa648b24a10b1c9234ed2046cc4af7a8850cbc236c05d42f3 \ + --hash=sha256:3b6713371f8987a1178df93cb36f29eef191f224021e2d656b2f11ce60d26816 \ + --hash=sha256:3e689af303df287142770abe3a48bbefd24dab4a09da5807d0e1fa8c75bab026 \ + --hash=sha256:460fd6b3f338c659507ae36cfd6b58ac9942a2ff233c5cf574416dfec0451a84 \ + --hash=sha256:481a1e9bd8f419fdc625307aa20234687f99368c75df511ef589693c5fea4c6f \ + --hash=sha256:4b6c8b02ab0ccb2431bb7bc238be91d158b308915e7b07937388e540466fe7e7 \ + --hash=sha256:4c68a9ed2df0cca51d774c521e68a34d2e3d9ebfc687ef8096adfd4f345b551d \ + --hash=sha256:5918fc6b31437208721276964323933cd86077b8d5b469c59c1b3fd2c8220a05 \ + --hash=sha256:5e9a8370c8ed873083d5de956d6b2e60adbad31e52d7a11111c96ef01d1910ae \ + --hash=sha256:6430b34a2ae6fcc604672f4f913102563473d9a015bdca1ce8c95041cc1f2677 \ + --hash=sha256:64b94d7a836568926a3309ff510c7f8261b881b341fd4992cabf4f0998878f8a \ + --hash=sha256:65048ed08c5124f05ff9f355ab9703014bb2dbe7f8d9948ce193685b1775f442 \ + --hash=sha256:66cf8038893c7708ec345ffb3ac63c775d10f430f323ac2f0334fdb6a397c57c \ + --hash=sha256:6c73ae37dbf9207727ac095dedef864c05d836eaec962a47b3b64eaadaf1c6b6 \ + --hash=sha256:711e6b98f8924e8b4a61ff97ab6321f33de024e1ed6a32f5123763aeda8459be \ + --hash=sha256:7293fefe15f0e5852bdb4ad1e0e26f3cbd4d3e61c19f751ecc4ff34bc1eb237d \ + --hash=sha256:73000459db113a658c4fb0510100ef0e79137b5828bf957b7709aacae4eb1b87 \ + --hash=sha256:75578c71644b031118ce938855a53530708db7f4af6e83e2f8840d5a1de990f8 \ + --hash=sha256:7741e44f7938ec94f9a52678c8d19b7bc548522ffdc39c9e4481af8db545fa9a \ + --hash=sha256:79284c1dd702f4f24ed1a36e51555c907dd237b6c0d829595978f4089a2aeea9 \ + --hash=sha256:7c2b1f4a640c51130caa92cef5bf72bd3c3dbbcfbf814c37403aa0601b1811b0 \ + --hash=sha256:7d3f64c503af7b60115b97c16feaf75bd191ef2c978d5c0c7725a6682bef63c5 \ + --hash=sha256:80a7859ffe70bf239d7a2ce15293bdeb5b4280ff7dc326ffab312b0e254dbb24 \ + --hash=sha256:8219d6fceae6b39535c4ac323dba0923d10f781d59962ff3504e693fdcafa92c \ + --hash=sha256:83391ef5935cc0f329b1abca414ae20ffe40d335fc21a4b5e664f08a74317d5f \ + --hash=sha256:83415af3c64550a56cc20b4cce59bbaa81f21d28466d7adf98feff011ecbc66d \ + --hash=sha256:839faf90a7eb525a401978dc925df8c44bd12526e8ba1529b9f8a7106e729637 \ + --hash=sha256:84f92e5a60a78c72ccda79d0417d311a1f6da18f446423ed411726d545bf7b56 \ + --hash=sha256:8872a0e9f1af975966b5be6af7eebd3dc4046f15e470b719316516dc3d137cd6 \ + --hash=sha256:8c298785e2fadeab82342040f2d9ce764ce500e6da6a6d99a2de514e63580b5a \ + --hash=sha256:97e8a9674652496c7612b528085dd5a296c052a2edc466ca1bfb7b0b27820413 \ + --hash=sha256:994167ff6551b9c1ce226e0aab16295b98c94507b5701aa60d2c32b7d50796b1 \ + --hash=sha256:9cb75e33131946fabd6319061df3b8b1d588fe0963183280e9b5f49f7772fc09 \ + --hash=sha256:a13cfa3410a75e4cb87abdb669aaf79da861cb79299159054ff8f77b9671bc40 \ + --hash=sha256:a3c17e6a267d13de9cbf14bf2ebfa87e03d26692456fc67d2dbed9da4f479b18 \ + --hash=sha256:a4ae7ed5a6d813450cc2d818284ea3db9721edcef50a56aae42ea06feec38c6e \ + --hash=sha256:a838296f5b84c920172fb579cac894d255c1fc25457c7234613ddcfa385e49b7 \ + --hash=sha256:ab70eace272d6f122b121c057e436709b50a28abf30d97aab28433c08f4a4095 \ + --hash=sha256:ae106fe16e36efc60ab098d02478d30aa0e31e1420eb4ecf0116459253bc6361 \ + --hash=sha256:aeef8563b82ed4af328f98e5041c1b4800d86f68f857ffd1577d4d47dc9aa6cd \ + --hash=sha256:b067b1ef9c8e41fb0882c828aa37829938b5c0dab067eca72b23fc24c563b9da \ + --hash=sha256:b0ddbcd2866b8ff1a2836e4b0e4d44788f5b992d83fac75a38cda8f9a2bee079 \ + --hash=sha256:b4bddfcfb6679215d6f4dc5f79a1f9301af339480d70527a14b57a1f2e6b6cbf \ + --hash=sha256:b7bc9a0b66097f03820a54316d2fdd0beb38859cf98f10d63e94c55450ed8920 \ + --hash=sha256:b81b4cf3d6e0ad7ac92bef248f49fafc954262c5fb0f7e19d6aac497e5a856b2 \ + --hash=sha256:b951113113ed4b8d173418a4f155c14b739dace626b3fa3f82be1831958d39e4 \ + --hash=sha256:beb43e9885202c8d4f3762319ed4d5e98e197622afbff8439fbbdd81d08938b9 \ + --hash=sha256:c14fa5dc39a804f1b92d63506f450eca5c59647a18d197d1a564b89dac1be1ce \ + --hash=sha256:c2d1ccfe088e8279d605011a3575619a74526c261be357695b3258c0f636115a \ + --hash=sha256:c4fc41b2df5529cad5ceb230319e82728096d4b353ce8d4df68a2ec37e291bb8 \ + --hash=sha256:d6b9b06323e3ba947c0003b2d70e02f33c90c36bc6262a92eb8201afc4a1aa08 \ + --hash=sha256:d6e78d5e28f812b39f92397806ecddd4a6f3bf35531a8c039a1f187abc931af8 \ + --hash=sha256:dd085eafa2aac6f883afd28210a3231f717f25409a1e44a39bb7b04c8c5b5646 \ + --hash=sha256:df27b57d214a3124fbe4e933ef5a903d4567f154260d9aece8c797a987f2a205 \ + --hash=sha256:e39258a09b1c7ca70b5e94a5c5ccfe4700b4250b8077cfeab31d0f79565d4c9b \ + --hash=sha256:e514c71ca72f3b56bd8fbda1a6a5b7d1100a2764b42a3c74a38841f25f9b00ab \ + --hash=sha256:e73a550dbeb84e8fa50f8385f7735e9a4735b465851ef617d02f80ab10e44e7e \ + --hash=sha256:e80bceebc9b58e959bede9b26cafe15b5b9526f3533a6dd06330c5da73cb9329 \ + --hash=sha256:ece8e7288db5b827ef8c64b2f78519f1a173a8991a625978fce02eccd7654fe9 \ + --hash=sha256:ef132cfb638e9a86bd5dc07fb4e1cb895bc55bce6bb5e759366e8b160d0747e2 \ + --hash=sha256:f69365ee2b836939137de024a302395a1cb8654fb6dc5ffef6381105259c8f87 \ + --hash=sha256:f8f5c1c7c69a4b00889e52d9304a918a5b49010f9645768eb5fd0ad404f790ba \ + --hash=sha256:fa305a84087e10d7a85e8a8a3dcba8cdbda4868f2180173b264b7b488fd37c55 \ + --hash=sha256:fbb746522ebfc11155f1cd688e2c48ef3d74125e38b63eabdaab068a055c3e88 + # via clickhouse-connect +beautifulsoup4==4.15.0 \ + --hash=sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7 \ + --hash=sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9 # via # docling # nbconvert -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -bleach[css]==6.2.0 \ - --hash=sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e \ - --hash=sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +bleach[css]==6.4.0 \ + --hash=sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452 \ + --hash=sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081 # via nbconvert -boto3==1.38.27 \ - --hash=sha256:94bd7fdd92d5701b362d4df100d21e28f8307a67ff56b6a8b0398119cf22f859 \ - --hash=sha256:95f5fe688795303a8a15e8b7e7f255cadab35eae459d00cc281a4fd77252ea80 - # via - # feast (setup.py) - # ikvpy +blinker==1.9.0 \ + --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ + --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc + # via flask +boto3==1.43.46 \ + --hash=sha256:66c0d943b049a46a492ec4ec2ebe73c930b1842c7137bee83aad6d93e95d4d96 \ + --hash=sha256:69453e2c1bcb9fd9806527ab99950cacfc2826cb0dce9a3a0414d19270c06c3c + # via + # feast (pyproject.toml) # moto # snowflake-connector-python -botocore==1.38.46 \ - --hash=sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e \ - --hash=sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b +botocore==1.43.46 \ + --hash=sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533 \ + --hash=sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 # via # aiobotocore # boto3 # moto # s3transfer # snowflake-connector-python -build==1.3.0 \ - --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ - --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 +build==1.5.0 \ + --hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \ + --hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647 # via - # feast (setup.py) + # feast (pyproject.toml) # pip-tools # singlestoredb -cachetools==6.2.1 \ - --hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \ - --hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201 - # via google-auth -cassandra-driver==3.29.2 \ - --hash=sha256:06ad489e4df2cc7f41d3aca8bd8ddeb8071c4fb98240ed07f1dcd9b5180fd879 \ - --hash=sha256:1e89de04809d02bb1d5d03c0946a7baaaf85e93d7e6414885b4ea2616efe9de0 \ - --hash=sha256:2039201ae5d9b7c7ce0930af7138d2637ca16a4c7aaae2fbdd4355fbaf3003c5 \ - --hash=sha256:52edc6d4bd7d07b10dc08b7f044dbc2ebe24ad7009c23a65e0916faed1a34065 \ - --hash=sha256:69aa53f1bdb23487765faa92eef57366637878eafc412f46af999e722353b22f \ - --hash=sha256:6c74610f56a4c53863a5d44a2af9c6c3405da19d51966fabd85d7f927d5c6abc \ - --hash=sha256:70d4d0dce373943308ad461a425fc70a23d0f524859367b8c6fc292400f39954 \ - --hash=sha256:7104e5043e9cc98136d7fafe2418cbc448dacb4e1866fe38ff5be76f227437ef \ - --hash=sha256:7d348c769aa6c37919e7d6247e8cf09c23d387b7834a340408bd7d611f174d80 \ - --hash=sha256:8067fad22e76e250c3846507d804f90b53e943bba442fa1b26583bcac692aaf1 \ - --hash=sha256:83dc9399cdabe482fd3095ca54ec227212d8c491b563a7276f6c100e30ee856c \ - --hash=sha256:957208093ff2353230d0d83edf8c8e8582e4f2999d9a33292be6558fec943562 \ - --hash=sha256:9e36437288d6cd6f6c74b8ee5997692126e24adc2da3d031dc11c7dfea8bc220 \ - --hash=sha256:a1e994a82b2e6ab022c5aec24e03ad49fca5f3d47e566a145de34eb0e768473a \ - --hash=sha256:a66b20c421d8fb21f18bd0ac713de6f09c5c25b6ab3d6043c3779b9c012d7c98 \ - --hash=sha256:a8c496318e3c136cf12ab21e1598fee4b48ea1c71746ea8cc9d32e4dcd09cb93 \ - --hash=sha256:b86427fab4d5a96e91ad82bb9338d4101ae4d3758ba96c356e0198da3de4d350 \ - --hash=sha256:c25b42e1a99f377a933d79ae93ea27601e337a5abb7bb843a0e951cf1b3836f7 \ - --hash=sha256:c4310a7d0457f51a63fb019d8ef501588c491141362b53097fbc62fa06559b7c \ - --hash=sha256:c4a005bc0b4fd8b5716ad931e1cc788dbd45967b0bcbdc3dfde33c7f9fde40d4 \ - --hash=sha256:c53700b0d1f8c1d777eaa9e9fb6d17839d9a83f27a61649e0cbaa15d9d3df34b \ - --hash=sha256:c5a9aab2367e8aad48ae853847a5a8985749ac5f102676de2c119b33fef13b42 \ - --hash=sha256:c86b0a796ff67d66de7df5f85243832a4dc853217f6a3eade84694f6f4fae151 \ - --hash=sha256:d180183451bec81c15e0441fa37a63dc52c6489e860e832cadd854373b423141 \ - --hash=sha256:d70353b6d9d6e01e2b261efccfe90ce0aa6f416588e6e626ca2ed0aff6b540cf \ - --hash=sha256:e31cee01a6fc8cf7f32e443fa0031bdc75eed46126831b7a807ab167b4dc1316 \ - --hash=sha256:e7f1dfa33c3d93350057d6dc163bb92748b6e6a164c408c75cf2c59be0a203b7 \ - --hash=sha256:e967c1341a651f03bdc466f3835d72d3c0a0648b562035e6d780fa0b796c02f6 \ - --hash=sha256:eb3a9f24fc84324d426a69dc35df66de550833072a4d9a4d63d72fda8fcaecb9 \ - --hash=sha256:ee0ebe8eb4fb007d8001ffcd1c3828b74defeb01075d8a1f1116ae9c60f75541 \ - --hash=sha256:f9df1e6ae4201eb2eae899cb0649d46b3eb0843f075199b51360bc9d59679a31 - # via feast (setup.py) -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +cachetools==6.2.6 \ + --hash=sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6 \ + --hash=sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda + # via + # mlflow-skinny + # mlflow-tracing + # pyiceberg + # pymilvus +cassandra-driver==3.30.1 \ + --hash=sha256:0c6a3e1428f7c6a9aa6c944b9c47a37cd2cdbeb5b5a82d42c33afdd56f14e398 \ + --hash=sha256:0ea769cf9925206b8458db10477b78699660fbdaed7262157e43b8f21817f838 \ + --hash=sha256:187bd1457e414bc019a0635603ea0001baada914942198ed23993198e44dec74 \ + --hash=sha256:222c704c72a5880cfe9c89f04b375d03d1152dbdb6ec8c25a325267b8bc2ca4e \ + --hash=sha256:30686850b9e0d0d8326ee07219113170b4e1d041e4d1434521a2f342c471108e \ + --hash=sha256:3c32513a2f2c6832d4fe3464e11be59dc21096dba296b18ec3454bc8354a78ba \ + --hash=sha256:3cb90ec9ac9d44cbfe17f1e9cb643dbc5f401e62980835aa6669a2fd5792863b \ + --hash=sha256:546ad047abf96a6d7922d057010b4cfb22e9d473db7b907956bc7c6a8029f7d9 \ + --hash=sha256:61bf2ded460fb0d2e909b70c9d193588b71148540bd85e29fe6954e00bb1a869 \ + --hash=sha256:6c5ae0218e52944f92ddde07a03cfa941fa4a10097d501a18cfa1b525c92d013 \ + --hash=sha256:71f3ccc8c60f41c83306c60704f7b1ee1a8ccd312aae286bf75d318aa47d0e71 \ + --hash=sha256:799af594364475656d9eea69606713fbaa98b10da82cc6d90286322477c72828 \ + --hash=sha256:80df7b705f71e1a6dae77aaea682cbe45fdd74b962cfcc6c6b0c05195f48b7c2 \ + --hash=sha256:84b2cc578f542332b678fc3fcc801c6ef8110a377d3fd38c27eab5ec5f2d2b79 \ + --hash=sha256:87822d6cce2c586aa72733a9dd1858276cee795834f7ad949c783bf19cf7a3a7 \ + --hash=sha256:8f43abccb5b5a761582e4ba6d44a7ef4274b61b0bd3f8e3159af40ca2c10c0fa \ + --hash=sha256:921403aa76c66d78d00c4a3dff542f989fa584b7450e3cb1dd2c267b4a5a55f0 \ + --hash=sha256:9b54f7c6cbb1d9bb6a8b9f8ab6034e6f761527e97af0603aa1e9635720df03a0 \ + --hash=sha256:9ff317f79b09eba52a44d46d8c4773e47533dfdd8b9ba4984268dc4a1fe5206e \ + --hash=sha256:ab853bd2806899c5612c3e4ac2a355a7811084d06692e4460255e3b0a4fa556a \ + --hash=sha256:af5c6f5d7d5a86725b908aba12d9554df60234c15bfe053a8175b15db791ada8 \ + --hash=sha256:c0995dccff3df739a1df6e10da9f3fc95bbbc8b38bacc4cfb6172655f8351cba \ + --hash=sha256:e10784b4f7d77332dc1a5c72855977a0d797160fd8ba50785414caed07fce95b \ + --hash=sha256:e43d873de5f8dadf4c8dcc4fab87d66a8495137e844ae3c184fbc90a54c05bc0 \ + --hash=sha256:eba7931a615207aab84ebaa000330024f1c614aa86ca58d119556edd338883ce \ + --hash=sha256:ec04eebfadc74039b1f7df84356a822539992c7dc0b013fedbb5a33d9295af86 + # via feast (pyproject.toml) +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # clickhouse-connect # docling @@ -353,287 +455,300 @@ certifi==2025.10.5 \ # minio # requests # snowflake-connector-python -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b - # via - # feast (setup.py) +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f + # via + # feast (pyproject.toml) # argon2-cffi-bindings # cryptography - # ikvpy - # snowflake-connector-python -cfgv==3.4.0 \ - --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ - --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 +cfgv==3.5.0 \ + --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ + --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 # via pre-commit -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via # requests # snowflake-connector-python -click==8.2.1 \ - --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ - --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask + # flask # geomet # great-expectations + # mlflow-skinny # pip-tools + # pyiceberg # ray # typer # uvicorn -clickhouse-connect==0.9.2 \ - --hash=sha256:02d1d73223493b51922168fc6161908d520de9c2e4015aa1bef3019ab1e8b7d3 \ - --hash=sha256:07c0f5098e9e4c6fa19d7471bc225ca1cf3ace99f01e8dda8be48df947cf0ddf \ - --hash=sha256:0b43416e922c7efd8c636cba7c9dcbeed8e26125d1d0fdf2387e86f6457cbe51 \ - --hash=sha256:113abbc5fa6413322b806fa35455960cb09dd829f3eb951db66164b335daab60 \ - --hash=sha256:13d6dc0776a760f6cbbb473ec75d152b697405e8c7061d5ad320df6fd9df687c \ - --hash=sha256:169fb9d9cf191095d4d06622af856d3c38a5fd4dba3f53f203dd2ac9807858c4 \ - --hash=sha256:19aa0bdfcb93117243a657df2d0f3c4dbf4849d6295afc199efbf680de43daf9 \ - --hash=sha256:1b4bd8f2864ebf1007ea59a2fbaa9fec8b5518030c1ad582645b8690db582305 \ - --hash=sha256:2532df7ee9003ebf1ffb559de39db27c733710ba2d178654aa8ddf8464291391 \ - --hash=sha256:2ab3fd64bbcf52a6a13ff3cf2a74d5c571e3c82d3c6a1d210562daec4ba2ba6b \ - --hash=sha256:2b1cb1542c2e6d1ede32c3d7aed86c06bafe6b5a9d4bb645b5c04f48389f8a4a \ - --hash=sha256:2c1bbe7d541fc776dc04fc0f416431e5245e4b872f5a8375a01094f336bcac57 \ - --hash=sha256:322163909d79b31f981fecdba9f6fe87279c62f6145454e6a191d204b9dd2adb \ - --hash=sha256:3364ca5e08986128d25a8926696d4f1bfb4ceb893b9b2cf93999287c1e98f92f \ - --hash=sha256:340e1353eddc2a5ea5ce073e4665d7ba10c2491254544ef802b592d8128a04e0 \ - --hash=sha256:3bea1477347e93fb14e7bab63f3282ebd8bc23cc9ad76c128534ed9168e2256f \ - --hash=sha256:3bfcf901eb94a218298753980d130bef2d1fa2e9e5845b0385d405092d09661f \ - --hash=sha256:4926616368b0a34173b1db5e567f06e855508ffc0db4d8ce5b3db4eb9bace5c1 \ - --hash=sha256:4cf6d857ee4f2ccd2c008189e521d33e35b462bd3425e4b599fa33800e832e30 \ - --hash=sha256:4d2f9bb8fe9a3fe570057195a1d91f7a6ee8b8ca323e1b74a70c270179482516 \ - --hash=sha256:525e91c972f79dd40ae515b05894f2a2e8c9d0d26da3f1adff2656d1fde250f4 \ - --hash=sha256:567f258de589b0930da43141397febd685acb3f4cf16f517c599bde4b63d35ad \ - --hash=sha256:5b9ce69c06109de2cf5827de7cdb12cf9688b5b4420463ebbfe959959f85e5c7 \ - --hash=sha256:6461bf077736d990a7198c2f352dcc7919bebdf2952993d26d18b8f25f500f7e \ - --hash=sha256:653dd934e545db2b5dda3c436ae68149ca6031149bf928cd7714921c45a66bb9 \ - --hash=sha256:655d6670741ec87497b8b37b7eaada0ec50757b3816c500ec9b7d63775741c92 \ - --hash=sha256:6c878f9eee3157455d623f1e5c233bb47e3f474f1ad8bc53358d7efb65ec199c \ - --hash=sha256:6d77bb17001cf5d88cbae3a579ab7bd76904058f8270bdd5ba3f09015f9efb55 \ - --hash=sha256:7d735cd382cacac5bb846c9a7f97e4b2de806978eca726cb90599c173b15e925 \ - --hash=sha256:7ee21425c7f259641c82ed024d2c0d769ccb1e818bfaa4b63d34636521fda266 \ - --hash=sha256:8521459ff9f7cc88d52f232ab006cd1d383a7b58f25022861f10770e6acde86e \ - --hash=sha256:86510682f80d1c72c12b84cf272db670d963a2f005c26fdf8268fbd494b1e6f3 \ - --hash=sha256:8b666caa538732fadeae6b50ea5d5fd67583be9438c79bd1de9ae69ae6ec0c72 \ - --hash=sha256:8c0b3c2a9ca71bd78839a360a47a5df9b95c280a795f8aee083a3de01e9085fc \ - --hash=sha256:8c9a482eaf951aaf9aa9630fd33b832454a56e1612fd0780c50fbaf2d1e8b977 \ - --hash=sha256:98ac02aa3a27cef3f841b7b2d1817b244ef2215badb3cf94719b3889369e95c3 \ - --hash=sha256:9ab13be35a6c92489d68b6bd4b32afcbedaa9e7c4aacd15a2f1a4fba761a33d8 \ - --hash=sha256:9b7a09548d5542badcfdbe3d62fccc4148124e6da7f061dfbcbdf041b730d372 \ - --hash=sha256:9f2c5e89b78526796379b63c31c0170f0c7000c5321aaad68eb6a70084520eba \ - --hash=sha256:a147cf722d28f49ede19566b32d4e46cafd6a1854f78252d803d8a58fb825b64 \ - --hash=sha256:a4b056da400f671e3f3b96e87eaa1457e511a8781a9587cb3be39c2ce9da3289 \ - --hash=sha256:aead65896287f3322b92b2235e783ab6ea4d03724fc8adc8efc4eddd95ffe91f \ - --hash=sha256:b909b56c4a5184245c7a414d0c4c154ba19fd6c147c6446e265c4c12923156a2 \ - --hash=sha256:ba413a95f992decb47040d887d673e58958f0efb6c1e1cc892c78cfd18885b02 \ - --hash=sha256:bbfbfea214f314db92dcd403ee0b748082c712a65c285c390672ff5421d3cd2a \ - --hash=sha256:bd434920d1f7b72496f368bd6d72538d8bbb3c6bef561fdb75cbc7e5732131c4 \ - --hash=sha256:bd47456aa88fcbc853380a55ef873b173f306b39d13978c4df0a7ba054324999 \ - --hash=sha256:c114ace0c34956ef8d6f7a23a3fe7857bb7b7812e4f85ff04ad2eaffa23ee133 \ - --hash=sha256:c5a78d95ba68e27e6acc768ea0f4f38ebe11b74ae9b00359ae005c6be793b012 \ - --hash=sha256:c8d5f63228da99aa54deee054c7b1f94862df7a3167eb1f960b93e60553b92da \ - --hash=sha256:ca6c200e74321a17b1e12da5fdb5ab104f3c0fcfc1e6389d29fe06ee4d13367b \ - --hash=sha256:d243a01531fecde2570cdc1dc2ad3e4737eb65d2e54dbcb0f5e776323cc90606 \ - --hash=sha256:d2531cf9ac36e5d371d141d8642e159d5081d22db1698c17e23e857894cb60d4 \ - --hash=sha256:d45f35c1734e1a8664edc31015fc490f53e6c912f359416fcd717b1815e28d56 \ - --hash=sha256:db2719ada3cf51aa94d98a4fb01778e795a26b1dbd5843d87d5f89120ef5bed6 \ - --hash=sha256:dd4b6464bb5a61ba913d9fbc5613365b3972a94fd71db7a02e1ae65970986109 \ - --hash=sha256:df25751bb5aa29e40b8b127500ea6a7f89031644c6b6bf9887c1dabe8df2bc36 \ - --hash=sha256:ebc8517d00e94d3677382d01b9c4ff10e212183ec05a72e774dec78b75458fa0 \ - --hash=sha256:ef549994c63d20e0ba1c7af3dfcf64daa9578467a665925ed46aabe682de4fb8 \ - --hash=sha256:f32a862c66174f986f3b64beebe8f336c3e2964c3a562e9deafc3e49277ab65b \ - --hash=sha256:f33efa9e6f08d277305e31b16666efc3299eb86155f0fd7152cf3e022256572b \ - --hash=sha256:f3816d1e07eed15a884bd81b827dccade4857e3ee4e9d81ea573e977e3cffc08 \ - --hash=sha256:f77444cb6e2d961c08fe2f1db130bfa587f3ad71ef09e46e083cf9f9dae90f00 - # via feast (setup.py) -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e - # via dask +clickhouse-connect==1.6.0 \ + --hash=sha256:01367cc75ddd3081f750ccba65c252385feff72f38f68613ea02dd056d047922 \ + --hash=sha256:064144e1a7f6075ade3c13b8ce49517ca92f86c0c4842bd2abb52a475c026025 \ + --hash=sha256:069f105173cd04263d3bb710381edae341dc9a3aa42fb4673017a617a5a1237a \ + --hash=sha256:06bd2e045a7ec1406f9bd39c92bcc77027258d8969128033cea5adb8602111bd \ + --hash=sha256:0856f42be01ab4957789b8cec99e91311d88bfe82be37ef2d230d36a6a111cff \ + --hash=sha256:163eb12e7aaeefe9e51a483d3ad40502e15ac2ef94cf1f5b7fe8e9f5303d161b \ + --hash=sha256:1a84eb6d545e2647a51b3b4ffa0da4a49f308aaaa096e58c44fb749c171a7d4a \ + --hash=sha256:2d5888d76be3bf1ddea5a210522572d66abcaf651b45ff7c5a1014ec08fe7565 \ + --hash=sha256:2f8b8060a8277509db77b14a01ed70683a0d9becba7a9994a5e59e42e9f98b37 \ + --hash=sha256:30a183095bc367dcc4695869754e36ce2177264d0ef79421990ee24700810ee0 \ + --hash=sha256:30bb77e7aa842550d5265c4aaa3e432b01440fd116d63e7e79ae79c380971ade \ + --hash=sha256:34969b2a57d9372921aef2b0b51fb858d81ce952c5249b6cb5a0d459e846854c \ + --hash=sha256:3cc6bdeafc743739f809671eb3c8fe9af1701981a43c624d9f5bea1e1409e5c4 \ + --hash=sha256:4372dbcbd7b1c897c8d7062832468f82af27fa731c7488421ec7f0a882e1580b \ + --hash=sha256:444564bad257b8923005431d661e347e5360cd686aae67091de3796a465cfec1 \ + --hash=sha256:483e34a7841dbf3d2863e784a129503cc37a910a05464f2f48ce3e9b2f0fc007 \ + --hash=sha256:508d3b7cc893b5d889b92702cc7dc4a1c975f59ee8da383054076969e4cb17c3 \ + --hash=sha256:5135f91841e9df5827faa24f3b2436d4ac8242e74a88a8d47922e31a4383b712 \ + --hash=sha256:51905921e7c98a11e7551663d263f28a715b14fdda191569fc71ddcde964ce52 \ + --hash=sha256:6570d07223ea5e4c4c21fece323cc8741b7711d31c6cb3fda8d1a62fa2d5b8d4 \ + --hash=sha256:691782a911a0696bb1ff644a309f4c6940473ee48d6c5171b13dab96d8cf8ecd \ + --hash=sha256:6bcdb6039bb55870229ec46784840f1420362e3979c74abdc6a53d1ae95718eb \ + --hash=sha256:7043480f1459ddd791480e3ea560a1def81dfd6462eab0c9ada54946e72918af \ + --hash=sha256:743787fd9d1505f55854b333b81b3fa92bf950d988b66a553aaa55c7bfd2f70c \ + --hash=sha256:75390e6ff6b398a88e0cac4907f785763fc9e476314cbafcfb24553bdbd92d28 \ + --hash=sha256:794735a62d0a46688d50652876bcb50116c5e912577e9a469ef6ed1aa52b4d7b \ + --hash=sha256:79caafbd2b1364d4b63c2e7c625cb424c8572b9cedc62b4ac2289cfbdf875487 \ + --hash=sha256:7fdea3ba73b77c636a30fc74c7f2c4c53ed0fcbf0da13498ff4122c701f10362 \ + --hash=sha256:80ca5778ce35d1f658741b993a9e7e17457d3f7a854e904aff72354b29990079 \ + --hash=sha256:8128dfe4a17aef265d5ba2bb5f2f5a0d5fa76e6aca357c71a17da378c1b30ea6 \ + --hash=sha256:8a5c244a31dd2f2d512c7b306e41e592dcaf9550926a96e05bd3222c6a44dacc \ + --hash=sha256:8bdef691c6c66a28301cb1f070f633dfbd60ccdca9580df96984325f1f79e803 \ + --hash=sha256:8d4303987b36ca075cbbe2cff3439ad017f56e1cf2bd936f83b2e6be222ae62f \ + --hash=sha256:9090c972f3d3ab40fc046913cf51c611345d603c922483e3239633f2cda16a3c \ + --hash=sha256:9a7af9bf7b992e5098ed9a0c2edf18d19c6acb2ccc77ee1d2da4e7e5d9dc7ddc \ + --hash=sha256:9dac7ce419ced91f035bd26a86dd49067461547cb9e3b9b1d7c5975b460b650c \ + --hash=sha256:9e64e332983fb268350abc76119c14da7b0640373a77ecf4f310240c3fdb4b6d \ + --hash=sha256:a337751b60c9d15eb9865cfaa32caeb6d81497a9fd3dad248ae112765e97c7f1 \ + --hash=sha256:a34e72d1ae6f71be491b2d44bd10d082d04c9e2e2a04d4774498e965d933b21d \ + --hash=sha256:a5285b9005d5a3ba41d6f148c957e4df8e37a38ad27835a4ad9c31fe21083149 \ + --hash=sha256:a7cf3b7ddffbf0b0bb77cb65935561ae48c49b4e6d0db11188279b068c297862 \ + --hash=sha256:b94bf383735abb90b1a6dc9e89a44f09b84a71a73348be0f01c7c2fb8500fdea \ + --hash=sha256:bd8de68d176a807fd16b85c3c5639c25bbdc81c203217f9e985e9560357fecd5 \ + --hash=sha256:bdd153037df79b684a30372395681bd673b562e9d6c5159c3a08bffc38ad8185 \ + --hash=sha256:c62bd3cc58aa0f6d4cac7012d6e5698cdc23111e3938a809596a5af8459470bd \ + --hash=sha256:c793706c87ab04b29a58e5bc2c0f53806c257859e9ceb859b2840c5aabe56402 \ + --hash=sha256:c924a2dea34578eda62e99a95988134ddaf4f66aa3f842ebd0980973b6acad56 \ + --hash=sha256:ca3d4b70d1fa92e62ab130dec495b470de6e0e56a944cd3c8ea049cb0f137c20 \ + --hash=sha256:cc03b0dd248faca290bced68a575e41331752bfd5863e3ab126382caa547aa1b \ + --hash=sha256:cd4c1dee496ad3a811237ff8066084c18cdaa46a046f9e0eeacce107ac29c83c \ + --hash=sha256:cf66e5bcfba8a183e40485efcfc80329ec75104e22dd02485fe73c0ab9073b19 \ + --hash=sha256:d541a1a75f08d62b7e0059f77efeff26f2b0829fa0d543e1e1edcce270c78a06 \ + --hash=sha256:d56348f1a39c8bd872106c4af92faa532fbdd8d66e72c588e9b4dd676ca2dbbd \ + --hash=sha256:e02c6abbfe777f4d0afdf7726ac30168514c46390b15e12ecf95d873c7b20969 \ + --hash=sha256:e4eb8b83b848f4ca4b984899acd62b0a60f310cbaa559d83519b7477968392ed \ + --hash=sha256:ed28787dd00a34b003fe24de4953e15b172058d875279336d4ee5d8f585acc50 \ + --hash=sha256:effd020411e4669f21d8b9f7f95d2342145ff6a2e32b49eae416fbd733334e53 \ + --hash=sha256:f8957fa2fd400d1f002dded822b3e667b7941e48107d8d2470e1935a12af9937 \ + --hash=sha256:fe2736f4cb520c4403f1f6b078ed3ed6f41a6ee8972328082240001853d8639e \ + --hash=sha256:fe4bdd4e09f52bff46c2f920877a08a26a44d91fdb5fc0be76edb67395440192 + # via feast (pyproject.toml) +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + # via + # dask + # mlflow-skinny colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via - # feast (setup.py) + # feast (pyproject.toml) # great-expectations comm==0.2.3 \ --hash=sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971 \ @@ -641,6 +756,65 @@ comm==0.2.3 \ # via # ipykernel # ipywidgets +contourpy==1.3.2 \ + --hash=sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f \ + --hash=sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92 \ + --hash=sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16 \ + --hash=sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f \ + --hash=sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f \ + --hash=sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7 \ + --hash=sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e \ + --hash=sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08 \ + --hash=sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841 \ + --hash=sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5 \ + --hash=sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2 \ + --hash=sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415 \ + --hash=sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878 \ + --hash=sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0 \ + --hash=sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab \ + --hash=sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445 \ + --hash=sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43 \ + --hash=sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c \ + --hash=sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823 \ + --hash=sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69 \ + --hash=sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15 \ + --hash=sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef \ + --hash=sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5 \ + --hash=sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73 \ + --hash=sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9 \ + --hash=sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912 \ + --hash=sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5 \ + --hash=sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85 \ + --hash=sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d \ + --hash=sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631 \ + --hash=sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2 \ + --hash=sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54 \ + --hash=sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773 \ + --hash=sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934 \ + --hash=sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a \ + --hash=sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441 \ + --hash=sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422 \ + --hash=sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532 \ + --hash=sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739 \ + --hash=sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b \ + --hash=sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f \ + --hash=sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1 \ + --hash=sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87 \ + --hash=sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52 \ + --hash=sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1 \ + --hash=sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd \ + --hash=sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989 \ + --hash=sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb \ + --hash=sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f \ + --hash=sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad \ + --hash=sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9 \ + --hash=sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512 \ + --hash=sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd \ + --hash=sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83 \ + --hash=sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe \ + --hash=sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0 \ + --hash=sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c + # via matplotlib couchbase==4.3.2 \ --hash=sha256:032a180afd6621358b2c73543b9c5db9939b442fc3ad6d54417c36c8a8f65838 \ --hash=sha256:11ce688ed46edf8387bf51866618c7b4e06399e7fb34a6df002764996c109d1f \ @@ -673,7 +847,7 @@ couchbase==4.3.2 \ --hash=sha256:c139c594118e1df6932491c26b825ac777fd201f49cda303892065e607f9c3ef \ --hash=sha256:c18b7c937e63e6d869371d9c4b0a0f9cc1a87dba48950a4276e294c091d76ce5 \ --hash=sha256:c50ae993c81d81ed0f02a37cbe034825408195b9fe29980b3379e2e05b2e1bec - # via feast (setup.py) + # via feast (pyproject.toml) couchbase-columnar==1.0.0 \ --hash=sha256:08b0947ee67f8fb15949e9323d60e5dbb44fb2d86d09f6e997a0bdcde6cd2b15 \ --hash=sha256:0f5ea6a24a73008a2f5a6e3aae51a49f4bb360b198a1f3d2ca4bb22044fe9671 \ @@ -706,208 +880,232 @@ couchbase-columnar==1.0.0 \ --hash=sha256:fa8fbddf971a2391543bc7dafaf3b581ad1a69c1fa0a474295b38a6fd8aed54f \ --hash=sha256:fc0fad2d386c5b5df7aaaccd8751e01caa886cc640cc8c92523dd07c4e7be519 \ --hash=sha256:fc4efa3e15190c3731478006de494b046bc57785e9c8ae99ac8b375a91683e38 - # via feast (setup.py) -coverage[toml]==7.10.7 \ - --hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \ - --hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \ - --hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \ - --hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \ - --hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \ - --hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \ - --hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \ - --hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \ - --hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \ - --hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \ - --hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \ - --hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \ - --hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \ - --hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \ - --hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \ - --hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \ - --hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \ - --hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \ - --hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \ - --hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \ - --hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \ - --hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \ - --hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \ - --hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \ - --hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \ - --hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \ - --hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \ - --hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \ - --hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \ - --hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \ - --hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \ - --hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \ - --hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \ - --hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \ - --hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \ - --hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \ - --hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \ - --hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \ - --hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \ - --hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \ - --hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \ - --hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \ - --hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \ - --hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \ - --hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \ - --hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \ - --hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \ - --hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \ - --hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \ - --hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \ - --hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \ - --hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \ - --hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \ - --hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \ - --hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \ - --hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \ - --hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \ - --hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \ - --hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \ - --hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \ - --hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \ - --hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \ - --hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \ - --hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \ - --hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \ - --hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \ - --hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \ - --hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \ - --hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \ - --hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \ - --hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \ - --hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \ - --hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \ - --hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \ - --hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \ - --hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \ - --hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \ - --hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \ - --hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \ - --hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \ - --hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \ - --hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \ - --hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \ - --hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \ - --hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \ - --hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \ - --hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \ - --hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \ - --hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \ - --hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \ - --hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \ - --hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \ - --hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \ - --hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \ - --hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \ - --hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \ - --hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \ - --hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \ - --hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \ - --hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \ - --hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \ - --hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \ - --hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \ - --hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3 + # via feast (pyproject.toml) +coverage[toml]==7.15.2 \ + --hash=sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2 \ + --hash=sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e \ + --hash=sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db \ + --hash=sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf \ + --hash=sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c \ + --hash=sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8 \ + --hash=sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443 \ + --hash=sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a \ + --hash=sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145 \ + --hash=sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9 \ + --hash=sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2 \ + --hash=sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376 \ + --hash=sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138 \ + --hash=sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578 \ + --hash=sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c \ + --hash=sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88 \ + --hash=sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036 \ + --hash=sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c \ + --hash=sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071 \ + --hash=sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a \ + --hash=sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d \ + --hash=sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b \ + --hash=sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a \ + --hash=sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b \ + --hash=sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050 \ + --hash=sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846 \ + --hash=sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d \ + --hash=sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1 \ + --hash=sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660 \ + --hash=sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40 \ + --hash=sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026 \ + --hash=sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0 \ + --hash=sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b \ + --hash=sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0 \ + --hash=sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa \ + --hash=sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d \ + --hash=sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658 \ + --hash=sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89 \ + --hash=sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072 \ + --hash=sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199 \ + --hash=sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446 \ + --hash=sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743 \ + --hash=sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7 \ + --hash=sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1 \ + --hash=sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287 \ + --hash=sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5 \ + --hash=sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be \ + --hash=sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688 \ + --hash=sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934 \ + --hash=sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7 \ + --hash=sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440 \ + --hash=sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984 \ + --hash=sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc \ + --hash=sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d \ + --hash=sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098 \ + --hash=sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6 \ + --hash=sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629 \ + --hash=sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b \ + --hash=sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee \ + --hash=sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f \ + --hash=sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1 \ + --hash=sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad \ + --hash=sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3 \ + --hash=sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9 \ + --hash=sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3 \ + --hash=sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a \ + --hash=sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296 \ + --hash=sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1 \ + --hash=sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd \ + --hash=sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73 \ + --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f \ + --hash=sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0 \ + --hash=sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6 \ + --hash=sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5 \ + --hash=sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1 \ + --hash=sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589 \ + --hash=sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688 \ + --hash=sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487 \ + --hash=sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9 \ + --hash=sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd \ + --hash=sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee \ + --hash=sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d \ + --hash=sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d \ + --hash=sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb \ + --hash=sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a \ + --hash=sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328 \ + --hash=sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635 \ + --hash=sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188 \ + --hash=sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c \ + --hash=sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243 \ + --hash=sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a # via pytest-cov -cryptography==43.0.3 \ - --hash=sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362 \ - --hash=sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4 \ - --hash=sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa \ - --hash=sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83 \ - --hash=sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff \ - --hash=sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805 \ - --hash=sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6 \ - --hash=sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664 \ - --hash=sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08 \ - --hash=sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e \ - --hash=sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18 \ - --hash=sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f \ - --hash=sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73 \ - --hash=sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5 \ - --hash=sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984 \ - --hash=sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd \ - --hash=sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3 \ - --hash=sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e \ - --hash=sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405 \ - --hash=sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2 \ - --hash=sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c \ - --hash=sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995 \ - --hash=sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73 \ - --hash=sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16 \ - --hash=sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7 \ - --hash=sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd \ - --hash=sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7 - # via - # feast (setup.py) +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b + # via + # feast (pyproject.toml) # azure-identity # azure-storage-blob + # google-auth # great-expectations # jwcrypto # moto # msal + # oracledb # pyjwt # pyopenssl # snowflake-connector-python # types-pyopenssl # types-redis -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) -datasets==4.0.0 \ - --hash=sha256:7ef95e62025fd122882dbce6cb904c8cd3fbc829de6669a5eb939c77d50e203d \ - --hash=sha256:9657e7140a9050db13443ba21cb5de185af8af944479b00e7ff1e00a61c8dbf1 - # via feast (setup.py) -db-dtypes==1.4.3 \ - --hash=sha256:a1c92b819af947fae1701d80a71f2a0eac08f825ca52cf0c68aeba80577ae966 \ - --hash=sha256:d3cb08c32939d24e05501f17694be8c1c54cfb4620d61fb56fb311e8c3d8885e +cycler==0.12.1 \ + --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ + --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c + # via matplotlib +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) +databricks-sdk==0.122.0 \ + --hash=sha256:7d998ec47f580be6be1cfe066be78038054767575b7437af5e4f76b67dde49c2 \ + --hash=sha256:eb11be13f0c02fc4ffc6e2672ad47478ce26ac4b310bd26916af8ba56e374e98 + # via + # mlflow-skinny + # mlflow-tracing +datasets==5.0.1 \ + --hash=sha256:9fbf73688f8c18f7529b4fe592abd04015f81d1e58001e4bac73ffb2b39d7cc4 \ + --hash=sha256:ce22bb851efd7494f08aad33b940803784434f6e77763d00679a0dc45fcf686a + # via feast (pyproject.toml) +db-dtypes==1.7.1 \ + --hash=sha256:450b94cd010cfa6d4d13cca2593009eba4fbe15516084c332cde1a47bbce0625 \ + --hash=sha256:75adf3bdf3174a6474ea4162d9a345ea0b3102ec4fd603e69ec26b6e8a99145a # via # google-cloud-bigquery # pandas-gbq -debugpy==1.8.17 \ - --hash=sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1 \ - --hash=sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3 \ - --hash=sha256:17e456da14848d618662354e1dccfd5e5fb75deec3d1d48dc0aa0baacda55860 \ - --hash=sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc \ - --hash=sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4 \ - --hash=sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088 \ - --hash=sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670 \ - --hash=sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef \ - --hash=sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf \ - --hash=sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420 \ - --hash=sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464 \ - --hash=sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c \ - --hash=sha256:8deb4e31cd575c9f9370042876e078ca118117c1b5e1f22c32befcfbb6955f0c \ - --hash=sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a \ - --hash=sha256:b13eea5587e44f27f6c48588b5ad56dcb74a4f3a5f89250443c94587f3eb2ea1 \ - --hash=sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4 \ - --hash=sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f \ - --hash=sha256:b75868b675949a96ab51abc114c7163f40ff0d8f7d6d5fd63f8932fd38e9c6d7 \ - --hash=sha256:bb1bbf92317e1f35afcf3ef0450219efb3afe00be79d8664b250ac0933b9015f \ - --hash=sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542 \ - --hash=sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f \ - --hash=sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840 \ - --hash=sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83 \ - --hash=sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da \ - --hash=sha256:e851beb536a427b5df8aa7d0c7835b29a13812f41e46292ff80b2ef77327355a \ - --hash=sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464 \ - --hash=sha256:eaa85bce251feca8e4c87ce3b954aba84b8c645b90f0e6a515c00394a9f5c0e7 \ - --hash=sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d \ - --hash=sha256:f2ac8055a0c4a09b30b931100996ba49ef334c6947e7ae365cdd870416d7513e \ - --hash=sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e +dbt-artifacts-parser==0.14.0 \ + --hash=sha256:c4da799afa80a3c2c6c7e51c3b5d2681be9fdd53174b3909a3ce71cfb43d5148 \ + --hash=sha256:e983510193cacaf6e6152e41cccfafc6cece592899f306f3143dc2f8b15ea394 + # via feast (pyproject.toml) +debugpy==1.8.21 \ + --hash=sha256:0042da0ecd0a8b50dc4a54395ecd870d258d73fa18776f50c91fdcabdcad2675 \ + --hash=sha256:0fddfdc130ac6d8bfc0415b0409822fa901c8f310e5c945ac5653a0352532344 \ + --hash=sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88 \ + --hash=sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c \ + --hash=sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1 \ + --hash=sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e \ + --hash=sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9 \ + --hash=sha256:4e70cc8b5079f885cb43910924ee0aab73b8b6b2a14eff23afdd9895d86e79eb \ + --hash=sha256:4e7c2d784d78ad4b71a5f8cd7b59c167719ec8a7a0211dbb3eb1bfeda78bc4e2 \ + --hash=sha256:72b5d676c4cbfac3bac5bb01c138a4656e843f93f03ce2a5f4e394ad49fbee73 \ + --hash=sha256:84c564d8cc701d41843b29a92814c1f1bef6798724ca9d675c284ad9f6a547d7 \ + --hash=sha256:8eeab7b5462f683452c57c0126aaa5ec4e974ddb705f39ba87dff8818c8e08f9 \ + --hash=sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782 \ + --hash=sha256:9f5171176a0084b95d2ebe55a4d1f7b2a75b74c5dbec577ebd3a85c740551c36 \ + --hash=sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e \ + --hash=sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6 \ + --hash=sha256:a7fe47fd23da57b9e0bec3f4a8ee65a2dc55782455ed7f2141d75ab5d2eaeef5 \ + --hash=sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0 \ + --hash=sha256:aa9d941d6dfe3d0407e4b3ca0b9ec466030e260fbf1174094f68785680f66db6 \ + --hash=sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92 \ + --hash=sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c \ + --hash=sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176 \ + --hash=sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264 \ + --hash=sha256:e935f9dc0501be523c8a8e1853c39432e1354e9ece717ae5998fd2371c4542c3 \ + --hash=sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2 \ + --hash=sha256:f15c10084f9861b5e8414a48f18f8e4aadf51a98a59e72c16aa28281ca994672 \ + --hash=sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc \ + --hash=sha256:f843a8b08c2edeaf9b1582eed4f25441af21a297c22ff16bf76a662557aa9c9e \ + --hash=sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8 \ + --hash=sha256:ffd932c6796afadab6993ec96745918a8cb2444dbd392074f769db5ea40ab440 # via ipykernel -decorator==5.2.1 \ - --hash=sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360 \ - --hash=sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a +decorator==5.3.1 \ + --hash=sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82 \ + --hash=sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c # via ipython defusedxml==0.7.1 \ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 - # via nbconvert + # via + # docling-core + # nbconvert deltalake==0.25.5 \ --hash=sha256:0b36afba5936f74c42920c06d140535e6efc8361f659770014944d8e69fbca09 \ --hash=sha256:0ca70e824fd7bcd16aeaaf9a43800eb9dc6c5d05b7854328c4cb4a240643ef78 \ @@ -916,134 +1114,123 @@ deltalake==0.25.5 \ --hash=sha256:76be7e1ed8d13f2dc933361057a44a47a89e6112d4f5ea0a73fb510bedd96efc \ --hash=sha256:cb1c7e826fd7c3bdd3676c7471d3b551e1a3674e44cd8e3747a0017a2c0292b7 \ --hash=sha256:e8f0d24bf64455f702da8402307b22e01f91e0f76694f7c5e33c9513011e8d29 - # via feast (setup.py) + # via feast (pyproject.toml) +deprecated==1.3.1 \ + --hash=sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f \ + --hash=sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223 + # via cassandra-driver deprecation==2.1.0 \ --hash=sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff \ --hash=sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a # via python-keycloak -dill==0.3.8 \ - --hash=sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca \ - --hash=sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7 +dill==0.3.9 \ + --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ + --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c # via - # feast (setup.py) + # feast (pyproject.toml) # datasets # multiprocess -distlib==0.4.0 \ - --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ - --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d +distlib==0.4.3 \ + --hash=sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b \ + --hash=sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed # via virtualenv -docker==7.1.0 \ - --hash=sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c \ - --hash=sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0 - # via testcontainers +dnspython==2.8.0 \ + --hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \ + --hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f + # via + # feast (pyproject.toml) + # pymongo +docker==7.2.0 \ + --hash=sha256:a3f45fdeb9165e2d25d9a1d02ddf3bc70fb572cf5ebbf9b58558c22caf29b71f \ + --hash=sha256:cebb93773d334f778e023a7ee352a8d6e13ab1bd3b863a4d4a59dec897df43ac + # via + # mlflow + # testcontainers docling==2.27.0 \ --hash=sha256:1288ed75b27e33bf94daff34faffc6d11b7d7ccc13e3df84fb24adad3991f72d \ --hash=sha256:faba35662612a2c687a3a463e501d95f645316436084af92a0442ce162429a3d - # via feast (setup.py) -docling-core[chunking]==2.48.4 \ - --hash=sha256:367675c1165d0934ae498fa57ca2d27ef0468aad74dc44a5ab061f5d87882ea1 \ - --hash=sha256:d87ce3021cdae3d073ce7572a2396b69be3cde82ebf9a74d4bad1e1cdfdfd524 + # via feast (pyproject.toml) +docling-core[chunking]==2.84.0 \ + --hash=sha256:2aee881b94138234c703298066b5ed94999490323ae6a29cbe5110f9248b83dd \ + --hash=sha256:331035d5032be683a6d66d476146652491d2c75e4562e04da3f1a8d989d74bfc # via # docling # docling-ibm-models # docling-parse -docling-ibm-models==3.9.1 \ - --hash=sha256:ac6cd1c2be93437cbb5c1f1a1a4030792a38859a1655b14f25cbc8aec760c351 \ - --hash=sha256:f2d845703877a3ca8853b57775eb8e88a7a9503d4fa110500a2550b8d63d0098 +docling-ibm-models==3.13.3 \ + --hash=sha256:50fc83d244ce2dc43158e02155d1caa68374d7a169d0f34e67d7eb14f396ffe6 \ + --hash=sha256:8c8b55e80b3d20fc74d85ca49a4d064578ef75b9f7251eec42fc9df6af426218 # via docling -docling-parse==4.5.0 \ - --hash=sha256:0a1a5f3e2f11ea74ab28d9c04b9391fa4b929c4af045c16bfb0da1e377646e54 \ - --hash=sha256:217fe2466ca2723bdecbdb162ca73891c1746ec15b8d99ec203f8df3305091a5 \ - --hash=sha256:24360a0985a8f76ff99c39e533d208bb57427caf96b9ceb585090cd10558f87a \ - --hash=sha256:256019969f1edc08b051a90fe739430593aaf7cd59fb18a2e00745f18533ce43 \ - --hash=sha256:368ebdb22ec03aa29b25d2684e51c74f6e167ab6809cd7bb5bb5b97cfe21bf8c \ - --hash=sha256:41ae6a7f0139d48b9ce8e0a7c43be003e6fa9382919a7efa76153bd1cdbb5e21 \ - --hash=sha256:4c8906d076219a18f4f86b1fec4e4cc3699460e78c88a5731ead48dfbb71835a \ - --hash=sha256:52df1c5bbafe5199c090bf47eb802c2fe40173fb438200f9a7cbe401aa1eed74 \ - --hash=sha256:5688fe4281dac16e807496c0b19587e25c53a9542d12f36b3a8fb2e66de78eb2 \ - --hash=sha256:8beb4f2c79c676b93ab3a14f86586adb51c3d5a2e3c1a902186e4cd6ed0a2e45 \ - --hash=sha256:99e353ab01ac5c81318b67f42c4fc83ac4a0b5b4783bc566f19656204acf45f0 \ - --hash=sha256:9bf94bc213bedd6d880d94eface2285e9e344da5452a23b3a8d0fedecb5d3ec1 \ - --hash=sha256:9d02c43d3185f5f4a6d5aaad38e69e07bbd1f965fd62f331bd9dfc006a637604 \ - --hash=sha256:a5f0bcdd6c84acc3f3a4c1f0fb96be7e9cff7a0bdff85f2f13caa80d2a9fac8f \ - --hash=sha256:affdecc41ed18f1a82c56edac2b815535e3cc07e2b0f8ffaee7e4adfb1333f0e \ - --hash=sha256:b7c9e8954118331438eb8da6058da0e3caf12735b47a86af9521e44465bbb2d4 \ - --hash=sha256:c3dba06a3cb8797587c90f5aa10cc2c51803d8f5cd67342ea948288a30503868 \ - --hash=sha256:c49193988b56133149584fed70b176de85c95fe698849b2acf68fde9df3a93e5 \ - --hash=sha256:d0ea05741721a76cfca6559d7cac283f2b2953915745b439be0ca8557864bb33 \ - --hash=sha256:d84186662e4780375de28b1bcb18112b04bd8e6aedb787d96544cc0d687f9629 \ - --hash=sha256:d8b2a25262a09e956516c4439ae143a66a55212f0ef9945928159caf1346408f \ - --hash=sha256:da6e535463bcb19a64f3099bb73b299e1f6f49a1ef3b0b3ea4fa62e2790ad875 \ - --hash=sha256:dac5e9907cd6fd020bc1620082dacb9b99bfc9ee4001c55c4e4ce156edf3b617 \ - --hash=sha256:e78f648c3a8af5ddb7dcc30c6c4270e9d3257366396a020ad60657de98bf88f5 \ - --hash=sha256:e8b283a93860cdf43a93296e1721e25daeb8eede14417b9f188f0f52c010d6b5 \ - --hash=sha256:e9223485df491432f5549dd4566c6649ff32f54370701a004673e27e6fa94a9e \ - --hash=sha256:ee02646e7a158c9f67d8df0052b544f1240d3c28eefa4658603931c13eac4435 \ - --hash=sha256:f830409eb96b063ae9f3f4e676f760b0d9738bcb0708ba6b840b7e0c84c490bd \ - --hash=sha256:f983d65703a165b76775c3e4b2a5cade4757216eb88faf5c0c86a9b33f38549a +docling-parse==4.7.3 \ + --hash=sha256:1790e7e4ae202d67875c1c48fd6f8ef5c51d10b0c23157e4989b8673f2f31308 \ + --hash=sha256:281347b3e937c1a5ffa6f8774ee603b64a0899fe8a6885573dec7eb48a3421d8 \ + --hash=sha256:29c91f78c877ae4637011efdb478f20a571e6794be924795b3469958a6401cd6 \ + --hash=sha256:32a2a8aedc56e82e2e3337b7afb83070db1fcfde86cbd93bba80ef2e331b6c13 \ + --hash=sha256:3b04459cc97a8a4929622e341b9981e23987a63af07db599afc5e1c4d389060b \ + --hash=sha256:45ec74bda63738c72e9f3989d19ef6ea7e3b1d61328ffc68d55b1b18eb6c4002 \ + --hash=sha256:53bd45241dca228715800afa0f96fdc826f7c234e9effcd5cefc86026ff19301 \ + --hash=sha256:5936e6bcb7969c2a13f38ecc75cada3b0919422dc845e96da4b0b7b3bbc394ce \ + --hash=sha256:5fc8f4770f9f6f90ba25f52451864a64394ddb158aea3a8fdda46a208c029cf6 \ + --hash=sha256:659234b800c094525476c6a97e771cd61491201e0c9f4af8ee6d39df9758bcae \ + --hash=sha256:65e0653d9617d38e73bab069dc3e7960668ff4a6b0ff45a7635c3790eeed8a08 \ + --hash=sha256:66896bbe925073e4d48f18ec29dcd611a390d6b2378fae72125e77b020cd5664 \ + --hash=sha256:6cb4fe8c62de06b70e6b38c4bd608f41ea3e9d7154a4e05f9a3c4d8944fe3a25 \ + --hash=sha256:75522790df921b6be5d86cf26d184a4af97c1c65e2d22698a9516bc049c398cf \ + --hash=sha256:91b9fbe8209922f46bbd8c6fd1a44193a4c364ff3fa398af7bcc8aaa404567d9 \ + --hash=sha256:978e7e7032760385264896871ae87cb3a04081766cc966c57e9750ce803162ac \ + --hash=sha256:9d18a5b1f7eecabed631c497a19f19d281a0d86f24bfe5d239e3df89bdc4df32 \ + --hash=sha256:a6e0f9e18d808c87ce0fe1900c74a3496a42743f4bba7ed4dd83a0e6e168644a \ + --hash=sha256:bd23eeb479355316fe807703220439fd1de1df4ca0145a49c35f71b184f87254 \ + --hash=sha256:c5a416ae2e1761914ee8d7dbfbe3858e106c876b5a7fccaa3917c038e2f126ec \ + --hash=sha256:ca64977a19ecd580a48f22137a30470d7ccf0995b2c25a74136c6facec7c617d \ + --hash=sha256:d3d86c51f9ce35a1b40b2f410f7271d9bd5fc58e7240f4cae7fdd2cef757e671 \ + --hash=sha256:d89231aa4fba3e38b80c11beb8edc07569e934c1f3935b51f57904fefe958ba5 \ + --hash=sha256:dc32b6f25a673e41b9a8112b6b841284f60dbac9427b7848a03b435460f74aee \ + --hash=sha256:dffd19ed373b0da5cea124606b183489a8686c3d18643e94485be1bdda5713ea \ + --hash=sha256:ef691045623863624f2cb7347572d0262a53cb84940ef7dd851d9f13a2eb8833 \ + --hash=sha256:f4a93f91f97055e19cade33bb957d83f8615f1d2a0103b89827aca16b31a3e22 # via docling docutils==0.19 \ --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \ --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc # via sphinx -duckdb==1.1.3 \ - --hash=sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586 \ - --hash=sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd \ - --hash=sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e \ - --hash=sha256:09c68522c30fc38fc972b8a75e9201616b96ae6da3444585f14cf0d116008c95 \ - --hash=sha256:0a55169d2d2e2e88077d91d4875104b58de45eff6a17a59c7dc41562c73df4be \ - --hash=sha256:0ba6baa0af33ded836b388b09433a69b8bec00263247f6bf0a05c65c897108d3 \ - --hash=sha256:183ac743f21c6a4d6adfd02b69013d5fd78e5e2cd2b4db023bc8a95457d4bc5d \ - --hash=sha256:1aa3abec8e8995a03ff1a904b0e66282d19919f562dd0a1de02f23169eeec461 \ - --hash=sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce \ - --hash=sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24 \ - --hash=sha256:2141c6b28162199999075d6031b5d63efeb97c1e68fb3d797279d31c65676269 \ - --hash=sha256:252d9b17d354beb9057098d4e5d5698e091a4f4a0d38157daeea5fc0ec161670 \ - --hash=sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9 \ - --hash=sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3 \ - --hash=sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289 \ - --hash=sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1 \ - --hash=sha256:4f0e2e5a6f5a53b79aee20856c027046fba1d73ada6178ed8467f53c3877d5e0 \ - --hash=sha256:51c6d79e05b4a0933672b1cacd6338f882158f45ef9903aef350c4427d9fc898 \ - --hash=sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8 \ - --hash=sha256:5ace6e4b1873afdd38bd6cc8fcf90310fb2d454f29c39a61d0c0cf1a24ad6c8d \ - --hash=sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff \ - --hash=sha256:6411e21a2128d478efbd023f2bdff12464d146f92bc3e9c49247240448ace5a6 \ - --hash=sha256:647f17bd126170d96a38a9a6f25fca47ebb0261e5e44881e3782989033c94686 \ - --hash=sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c \ - --hash=sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31 \ - --hash=sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7 \ - --hash=sha256:80158f4c7c7ada46245837d5b6869a336bbaa28436fbb0537663fa324a2750cd \ - --hash=sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898 \ - --hash=sha256:8ee97ec337794c162c0638dda3b4a30a483d0587deda22d45e1909036ff0b739 \ - --hash=sha256:911d58c22645bfca4a5a049ff53a0afd1537bc18fedb13bc440b2e5af3c46148 \ - --hash=sha256:9c619e4849837c8c83666f2cd5c6c031300cd2601e9564b47aa5de458ff6e69d \ - --hash=sha256:9d0767ada9f06faa5afcf63eb7ba1befaccfbcfdac5ff86f0168c673dd1f47aa \ - --hash=sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4 \ - --hash=sha256:a1f83c7217c188b7ab42e6a0963f42070d9aed114f6200e3c923c8899c090f16 \ - --hash=sha256:a1fa0c502f257fa9caca60b8b1478ec0f3295f34bb2efdc10776fc731b8a6c5f \ - --hash=sha256:a30dd599b8090ea6eafdfb5a9f1b872d78bac318b6914ada2d35c7974d643640 \ - --hash=sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38 \ - --hash=sha256:a4748635875fc3c19a7320a6ae7410f9295557450c0ebab6d6712de12640929a \ - --hash=sha256:b74e121ab65dbec5290f33ca92301e3a4e81797966c8d9feef6efdf05fc6dafd \ - --hash=sha256:c443d3d502335e69fc1e35295fcfd1108f72cb984af54c536adfd7875e79cee5 \ - --hash=sha256:c5336939d83837af52731e02b6a78a446794078590aa71fd400eb17f083dda3e \ - --hash=sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378 \ - --hash=sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9 \ - --hash=sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee \ - --hash=sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32 \ - --hash=sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537 \ - --hash=sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989 \ - --hash=sha256:ecb1dc9062c1cc4d2d88a5e5cd8cc72af7818ab5a3c0f796ef0ffd60cfd3efb4 \ - --hash=sha256:eeacb598120040e9591f5a4edecad7080853aa8ac27e62d280f151f8c862afa3 \ - --hash=sha256:f549af9f7416573ee48db1cf8c9d27aeed245cb015f4b4f975289418c6cf7320 \ - --hash=sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3 \ - --hash=sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b +duckdb==1.5.5 \ + --hash=sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053 \ + --hash=sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4 \ + --hash=sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c \ + --hash=sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a \ + --hash=sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528 \ + --hash=sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c \ + --hash=sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282 \ + --hash=sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f \ + --hash=sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7 \ + --hash=sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f \ + --hash=sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c \ + --hash=sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb \ + --hash=sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433 \ + --hash=sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8 \ + --hash=sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28 \ + --hash=sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238 \ + --hash=sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece \ + --hash=sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1 \ + --hash=sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151 \ + --hash=sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2 \ + --hash=sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353 \ + --hash=sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc \ + --hash=sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079 \ + --hash=sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a \ + --hash=sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6 \ + --hash=sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe \ + --hash=sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de \ + --hash=sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0 \ + --hash=sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e \ + --hash=sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f \ + --hash=sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf \ + --hash=sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae \ + --hash=sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6 \ + --hash=sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe \ + --hash=sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366 # via ibis-framework -dunamai==1.25.0 \ - --hash=sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab \ - --hash=sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1 - # via poetry-dynamic-versioning durationpy==0.10 \ --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 @@ -1051,36 +1238,32 @@ durationpy==0.10 \ easyocr==1.7.2 \ --hash=sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c # via docling -elastic-transport==9.1.0 \ - --hash=sha256:1590e44a25b0fe208107d5e8d7dea15c070525f3ac9baafbe4cb659cd14f073d \ - --hash=sha256:369fa56874c74daae4ea10cbf40636d139f38f42bec0e006b9cd45a168ee7fce +elastic-transport==9.4.2 \ + --hash=sha256:33dc89bb1855faa8b98ae8b036405a39c562778dbcdbe4a00a2eaf753148556c \ + --hash=sha256:366f4614f4544c5fb5d780c82f57af8f30492b44a68ed20750390aa81e20c2ea # via elasticsearch -elasticsearch==9.1.1 \ - --hash=sha256:2a5c27c57ca3dd3365f665c82c9dcd8666ccfb550d5b07c688c21ec636c104e5 \ - --hash=sha256:be20acda2a97591a9a6cf4981fc398ee6fca3291cf9e7a9e52b6a9f41a46d393 - # via feast (setup.py) +elasticsearch==9.4.1 \ + --hash=sha256:1d78fdfba97a903ec35a5eb5808a74e33392b7c620bd5f742d465a3a26c27d75 \ + --hash=sha256:71ab71c3d1b20fd88c2922fb82c3277cce7ea03c160686e7b9368b265c2b4cac + # via feast (pyproject.toml) entrypoints==0.4 \ --hash=sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4 \ --hash=sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f # via altair -environs==9.5.0 \ - --hash=sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124 \ - --hash=sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9 - # via pymilvus et-xmlfile==2.0.0 \ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \ --hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54 # via openpyxl -exceptiongroup==1.3.0 \ - --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ - --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 +exceptiongroup==1.3.1 \ + --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ + --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via # anyio # ipython # pytest -execnet==2.1.1 \ - --hash=sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc \ - --hash=sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3 +execnet==2.1.2 \ + --hash=sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd \ + --hash=sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec # via pytest-xdist executing==2.2.1 \ --hash=sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4 \ @@ -1112,24 +1295,27 @@ faiss-cpu==1.10.0 \ --hash=sha256:e02af3696a6b9e1f9072e502f48095a305de2163c42ceb1f6f6b1db9e7ffe574 \ --hash=sha256:e71f7e24d5b02d3a51df47b77bd10f394a1b48a8331d5c817e71e9e27a8a75ac \ --hash=sha256:f71c5860c860df2320299f9e4f2ca1725beb559c04acb1cf961ed24e6218277a - # via feast (setup.py) -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 # via - # feast (setup.py) + # feast (pyproject.toml) + # milvus-lite +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e + # via + # feast (pyproject.toml) # fastapi-mcp + # mlflow-skinny fastapi-mcp==0.4.0 \ --hash=sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec \ --hash=sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d - # via feast (setup.py) -fastjsonschema==2.21.2 \ - --hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \ - --hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de + # via feast (pyproject.toml) +fastjsonschema==2.22.1 \ + --hash=sha256:0b83d1ce8d7845b959dcb20e1a5c3c8883b6541d9c52ab02cce5166b75ec805f \ + --hash=sha256:cf377ff5c9a6f4f3125fb35f75a2c5767bd824ffbcf62c209a93cd48d1453999 # via nbformat -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 +filelock==3.32.0 \ + --hash=sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402 \ + --hash=sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3 # via # datasets # huggingface-hub @@ -1142,6 +1328,62 @@ filetype==1.2.0 \ --hash=sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb \ --hash=sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25 # via docling +flask==3.1.3 \ + --hash=sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb \ + --hash=sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c + # via mlflow +fonttools==4.63.0 \ + --hash=sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69 \ + --hash=sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c \ + --hash=sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac \ + --hash=sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096 \ + --hash=sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d \ + --hash=sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68 \ + --hash=sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616 \ + --hash=sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78 \ + --hash=sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f \ + --hash=sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b \ + --hash=sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b \ + --hash=sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02 \ + --hash=sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d \ + --hash=sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f \ + --hash=sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8 \ + --hash=sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272 \ + --hash=sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49 \ + --hash=sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419 \ + --hash=sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001 \ + --hash=sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03 \ + --hash=sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196 \ + --hash=sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9 \ + --hash=sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e \ + --hash=sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5 \ + --hash=sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007 \ + --hash=sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380 \ + --hash=sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8 \ + --hash=sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27 \ + --hash=sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40 \ + --hash=sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e \ + --hash=sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0 \ + --hash=sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263 \ + --hash=sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb \ + --hash=sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94 \ + --hash=sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b \ + --hash=sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6 \ + --hash=sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579 \ + --hash=sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4 \ + --hash=sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59 \ + --hash=sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0 \ + --hash=sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e \ + --hash=sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be \ + --hash=sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd \ + --hash=sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18 \ + --hash=sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22 \ + --hash=sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0 \ + --hash=sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b \ + --hash=sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b \ + --hash=sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af \ + --hash=sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745 + # via matplotlib fqdn==1.5.1 \ --hash=sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f \ --hash=sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014 @@ -1280,24 +1522,35 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -fsspec[http]==2024.9.0 \ - --hash=sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8 \ - --hash=sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b +fsspec[http]==2026.6.0 \ + --hash=sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1 \ + --hash=sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a # via - # feast (setup.py) + # feast (pyproject.toml) # dask # datasets # huggingface-hub + # pyiceberg # torch -geomet==0.2.1.post1 \ - --hash=sha256:91d754f7c298cbfcabd3befdb69c641c27fe75e808b27aa55028605761d17e95 \ - --hash=sha256:a41a1e336b381416d6cbed7f1745c848e91defaa4d4c1bdc1312732e46ffad2b - # via cassandra-driver -google-api-core[grpc]==2.26.0 \ - --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ - --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 - # via - # feast (setup.py) +geomet==1.1.0 \ + --hash=sha256:4372fe4e286a34acc6f2e9308284850bd8c4aa5bc12065e2abbd4995900db12f \ + --hash=sha256:51e92231a0ef6aaa63ac20c443377ba78a303fd2ecd179dc3567de79f3c11605 + # via + # cassandra-driver + # scylla-driver +gitdb==4.0.12 \ + --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ + --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf + # via gitpython +gitpython==3.1.57 \ + --hash=sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf \ + --hash=sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488 + # via mlflow-skinny +google-api-core[grpc]==2.33.0 \ + --hash=sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb \ + --hash=sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc + # via + # feast (pyproject.toml) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -1305,10 +1558,11 @@ google-api-core[grpc]==2.26.0 \ # google-cloud-datastore # google-cloud-storage # pandas-gbq -google-auth==2.41.1 \ - --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ - --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 +google-auth[pyopenssl]==2.56.2 \ + --hash=sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6 \ + --hash=sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051 # via + # databricks-sdk # google-api-core # google-auth-oauthlib # google-cloud-bigquery @@ -1317,165 +1571,177 @@ google-auth==2.41.1 \ # google-cloud-core # google-cloud-datastore # google-cloud-storage - # kubernetes # pandas-gbq # pydata-google-auth -google-auth-oauthlib==1.2.2 \ - --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ - --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-auth-oauthlib==1.4.0 \ + --hash=sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2 \ + --hash=sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 # via # pandas-gbq # pydata-google-auth -google-cloud-bigquery[pandas]==3.38.0 \ - --hash=sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520 \ - --hash=sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6 +google-cloud-bigquery[pandas]==3.42.2 \ + --hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \ + --hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq -google-cloud-bigquery-storage==2.33.1 \ - --hash=sha256:24952aba0d69acc4d6bfbdc7a09dddbb728496b1780bd224f1056361a1b51044 \ - --hash=sha256:3fd25bef364ac5fb9bbd6560f0dd11b90b1845883df8e0a8c706ad53d00fc23b - # via feast (setup.py) -google-cloud-bigtable==2.33.0 \ - --hash=sha256:8d25c73dd41cea9436d14c9f03b91f94b05ff4afd47ce9a575bf18f8e6985747 \ - --hash=sha256:cd4f7178cde30113900318ec7e2fd793116752c97e0922c185c69b645a52656d - # via feast (setup.py) -google-cloud-core==2.4.3 \ - --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ - --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e +google-cloud-bigquery-storage==2.39.0 \ + --hash=sha256:8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 \ + --hash=sha256:d5afd90ad06cf24d9167316cca70ab5b344e880fc13031d7392aa78ee76b8bb6 + # via feast (pyproject.toml) +google-cloud-bigtable==2.41.0 \ + --hash=sha256:36da0fc730952bfe48ae91dee11015459c9046fa1499d0efb73a61bbb6edbe9b \ + --hash=sha256:d0c424bae6238eb16ffc0922be79da00d20d23e0750ea46742ffd4542c50fd26 + # via feast (pyproject.toml) +google-cloud-core==2.6.0 \ + --hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \ + --hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.21.0 \ - --hash=sha256:eee454dd4a55f5b327f9f344928ff1a09a6f77c23d5e3d908ad31a13cc2f4073 \ - --hash=sha256:f303f27cd1983383f20bd227019cd8a7897419e0ec6b878367c58c66245f9d9b - # via feast (setup.py) +google-cloud-datastore==2.26.0 \ + --hash=sha256:1ac4882f033fcb90bd75d1171b46400ecd5def23768598843d9c041e0c0b4a6a \ + --hash=sha256:1bde5694308468da5c932153a6b0331d5b54e1c21de366b6a75ddb0000fd5f4b + # via feast (pyproject.toml) google-cloud-storage==2.19.0 \ --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via feast (setup.py) -google-crc32c==1.7.1 \ - --hash=sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db \ - --hash=sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337 \ - --hash=sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c \ - --hash=sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242 \ - --hash=sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e \ - --hash=sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472 \ - --hash=sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194 \ - --hash=sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3 \ - --hash=sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582 \ - --hash=sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d \ - --hash=sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6 \ - --hash=sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82 \ - --hash=sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06 \ - --hash=sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349 \ - --hash=sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a \ - --hash=sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d \ - --hash=sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48 \ - --hash=sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb \ - --hash=sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315 \ - --hash=sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589 \ - --hash=sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76 \ - --hash=sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65 \ - --hash=sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6 \ - --hash=sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127 \ - --hash=sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53 \ - --hash=sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603 \ - --hash=sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35 \ - --hash=sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9 \ - --hash=sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638 \ - --hash=sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9 \ - --hash=sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77 \ - --hash=sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14 \ - --hash=sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b \ - --hash=sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb + # via feast (pyproject.toml) +google-crc32c==1.8.0 \ + --hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \ + --hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \ + --hash=sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff \ + --hash=sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 \ + --hash=sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411 \ + --hash=sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a \ + --hash=sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 \ + --hash=sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb \ + --hash=sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa \ + --hash=sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962 \ + --hash=sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215 \ + --hash=sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b \ + --hash=sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27 \ + --hash=sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113 \ + --hash=sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f \ + --hash=sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f \ + --hash=sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d \ + --hash=sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 \ + --hash=sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092 \ + --hash=sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7 \ + --hash=sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2 \ + --hash=sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93 \ + --hash=sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8 \ + --hash=sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21 \ + --hash=sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 \ + --hash=sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2 \ + --hash=sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc \ + --hash=sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454 \ + --hash=sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2 \ + --hash=sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733 \ + --hash=sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697 \ + --hash=sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651 \ + --hash=sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c # via # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 +google-resumable-media==2.10.0 \ + --hash=sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c \ + --hash=sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.70.0 \ - --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ - --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +googleapis-common-protos[grpc]==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # grpc-google-iam-v1 # grpcio-status +graphene==3.4.3 \ + --hash=sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa \ + --hash=sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71 + # via mlflow +graphql-core==3.2.11 \ + --hash=sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0 \ + --hash=sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802 + # via + # graphene + # graphql-relay +graphql-relay==3.2.0 \ + --hash=sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c \ + --hash=sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5 + # via graphene great-expectations==0.18.8 \ --hash=sha256:ab41cfa3de829a4f77bdcd4a23244684cbb67fdacc734d38910164cd02ec95b6 \ --hash=sha256:c1205bede593f679e22e0b3826d6ae1623c439cafd553f9f0bc2b0fd441f6ed9 - # via feast (setup.py) -grpc-google-iam-v1==0.14.2 \ - --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ - --hash=sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20 + # via feast (pyproject.toml) +grpc-google-iam-v1==0.14.4 \ + --hash=sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038 \ + --hash=sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 # via google-cloud-bigtable -grpcio==1.62.3 \ - --hash=sha256:059444f0ed5dba73ab7dd0ee7e8e6b606df4130d2b0a9f010f84da4ab9f6c2d8 \ - --hash=sha256:114f2a865886ff33f85d70670e971fe0e3d252a1209656fefa5470286e3fcc76 \ - --hash=sha256:13571a5b868dcc308a55d36669a2d17d9dcd6ec8335213f6c49cc68da7305abe \ - --hash=sha256:1ac0944e9e3ee3e20825226d1e17985e9f88487055c475986cf0922a7d806d8a \ - --hash=sha256:1de3d04d9a4ec31ebe848ae1fe61e4cbc367fb9495cbf6c54368e60609a998d9 \ - --hash=sha256:216740723fc5971429550c374a0c039723b9d4dcaf7ba05227b7e0a500b06417 \ - --hash=sha256:25cd75dc73c5269932413e517be778640402f18cf9a81147e68645bd8af18ab0 \ - --hash=sha256:325c56ce94d738c31059cf91376f625d3effdff8f85c96660a5fd6395d5a707f \ - --hash=sha256:3737e5ef0aa0fcdfeaf3b4ecc1a6be78b494549b28aec4b7f61b5dc357f7d8be \ - --hash=sha256:377babc817e8b4186aed7ed56e832867c513e4e9b6c3503565c344ffdef440d4 \ - --hash=sha256:3fb7d966a976d762a31346353a19fce4afcffbeda3027dd563bc8cb521fcf799 \ - --hash=sha256:43670a25b752b7ed960fcec3db50ae5886dc0df897269b3f5119cde9b731745f \ - --hash=sha256:4439bbd759636e37b66841117a66444b454937e27f0125205d2d117d7827c643 \ - --hash=sha256:454a6aed4ebd56198d37e1f3be6f1c70838e33dd62d1e2cea12f2bcb08efecc5 \ - --hash=sha256:4c9c1502c76cadbf2e145061b63af077b08d5677afcef91970d6db87b30e2f8b \ - --hash=sha256:4dab8b64c438e19c763a6332b55e5efdbecfb7c55ae59a42c38c81ed27955fa5 \ - --hash=sha256:56757d3e4cf5d4b98a30f2c5456151607261c891fa2298a4554848dcbf83083d \ - --hash=sha256:57823dc7299c4f258ae9c32fd327d29f729d359c34d7612b36e48ed45b3ab8d0 \ - --hash=sha256:582bd03e9c3d1bd1162eb51fa0f1a35633d66e73f4f36702d3b8484a8b45eda7 \ - --hash=sha256:620165df24aae3d5b3e84cb8dd6b98f6ed49aed04126186bbf43061e301d6a21 \ - --hash=sha256:646c14e9f3356d3f34a65b58b0f8d08daa741ba1d4fcd4966b79407543332154 \ - --hash=sha256:668211f3699bbee4deaf1d6e6b8df59328bf63f077bf2dc9b8bfa4a17df4a279 \ - --hash=sha256:6be243f3954b0ca709f56f9cae926c84ac96e1cce19844711e647a1f1db88b99 \ - --hash=sha256:6da20a1ae010a988bc4ed47850f1122de0a88e18cd2f901fcf56007be1fc6c30 \ - --hash=sha256:7349cd7445ac65fbe1b744dcab9cc1ec02dae2256941a2e67895926cbf7422b4 \ - --hash=sha256:74f3fc9b93290e58264844f5bc46df4c58a94c4287a277dbcf75344fc6c37ca4 \ - --hash=sha256:75a4e9ac7ff185cad529f35934c5d711b88aca48b90c70e195f5657da50ce321 \ - --hash=sha256:7b33c1807d4ac564a3027d06f21a2220c116ceacaaef614deb96b3341ee58896 \ - --hash=sha256:807176971c504c598976f5a9ea62363cffbbbb6c7509d9808c2342b020880fa2 \ - --hash=sha256:80a82fdee14dc27e9299248b7aabd5a8739a1cf6b76c78aa2b848158b44a99d5 \ - --hash=sha256:81b7c121c4e52a0749bf0759185b8d5cfa48a786cd7d411cdab08269813e0aab \ - --hash=sha256:8257cc9e55fb0e2149a652d9dc14c023720f9e73c9145776e07c97e0a553922e \ - --hash=sha256:8a5f00b2508937952d23a1767739e95bbbe1120f8a66d10187d5e971d56bb55c \ - --hash=sha256:8ae2e7a390b2cdd2a95d3bf3b3385245eeb48a5e853943cb46139666462c2d1a \ - --hash=sha256:940459d81685549afdfe13a6de102c52ea4cdda093477baa53056884aadf7c48 \ - --hash=sha256:9c4aae4e683776c319169d87e7891b67b75e3f1c0beeb877902ea148b0585164 \ - --hash=sha256:9d5f8e0050a179b3bce9189b522dc91008d44f08c757a7c310e0fd06b4d3d147 \ - --hash=sha256:a1b85d35a7d9638c03321dfe466645b87e23c30df1266f9e04bbb5f44e7579a9 \ - --hash=sha256:a82410d7620c07cb32624e38f2a106980564dfef9dbe78f5b295cda9ef217c03 \ - --hash=sha256:abfe64811177e681edc81d9d9d1bd23edc5f599bd9846650864769264ace30cd \ - --hash=sha256:ac9783d5679c8da612465168c820fd0b916e70ec5496c840bddba0be7f2d124c \ - --hash=sha256:b033d50bd41e506e3b579775f54a30c16c222e0d88847ac8098d2eca2a7454cc \ - --hash=sha256:b097347441b86a8c3ad9579abaf5e5f7f82b1d74a898f47360433b2bca0e4536 \ - --hash=sha256:b708401ede2c4cb8943e8a713988fcfe6cbea105b07cd7fa7c8a9f137c22bddb \ - --hash=sha256:bd900e666bb68fff49703084be14407cd73b8a5752a7590cea98ec22de24fb5d \ - --hash=sha256:c118cfc80e2402a5595be36e9245ffd9b0e146f426cc40bdf60015bf183f8373 \ - --hash=sha256:c175b252d063af388523a397dbe8edbc4319761f5ee892a8a0f5890acc067362 \ - --hash=sha256:c8bb1a7aa82af6c7713cdf9dcb8f4ea1024ac7ce82bb0a0a82a49aea5237da34 \ - --hash=sha256:c95a0b76a44c548e6bd8c5f7dbecf89c77e2e16d3965be817b57769c4a30bea2 \ - --hash=sha256:e202e3f963480ca067a261179b1ac610c0f0272cb4a7942d11b7e2b3fc99c3aa \ - --hash=sha256:e9ffdb7bc9ccd56ec201aec3eab3432e1e820335b5a16ad2b37e094218dcd7a6 \ - --hash=sha256:ea7ca66a58421411c6486fa5015fe7704e2816ff0b4ec4fb779ad5e1cbbdabf3 \ - --hash=sha256:f2ff8ac447765e173842b554b31307b98b3bb1852710903ebb936e7efb7df6e5 \ - --hash=sha256:f5def814c5a4c90c8fe389c526ab881f4a28b7e239b23ed8e02dd02934dfaa1a - # via - # feast (setup.py) +grpcio==1.83.0 \ + --hash=sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df \ + --hash=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 \ + --hash=sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5 \ + --hash=sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df \ + --hash=sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930 \ + --hash=sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf \ + --hash=sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9 \ + --hash=sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33 \ + --hash=sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da \ + --hash=sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03 \ + --hash=sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa \ + --hash=sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223 \ + --hash=sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9 \ + --hash=sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4 \ + --hash=sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf \ + --hash=sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881 \ + --hash=sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af \ + --hash=sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5 \ + --hash=sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0 \ + --hash=sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727 \ + --hash=sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb \ + --hash=sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a \ + --hash=sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735 \ + --hash=sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16 \ + --hash=sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49 \ + --hash=sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1 \ + --hash=sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24 \ + --hash=sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b \ + --hash=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f \ + --hash=sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57 \ + --hash=sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf \ + --hash=sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f \ + --hash=sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c \ + --hash=sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969 \ + --hash=sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd \ + --hash=sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c \ + --hash=sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b \ + --hash=sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61 \ + --hash=sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404 \ + --hash=sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617 \ + --hash=sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40 \ + --hash=sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45 \ + --hash=sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc \ + --hash=sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c \ + --hash=sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745 \ + --hash=sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45 \ + --hash=sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c \ + --hash=sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0 \ + --hash=sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d \ + --hash=sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9 \ + --hash=sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8 + # via + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-datastore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking @@ -1483,282 +1749,325 @@ grpcio==1.62.3 \ # grpcio-status # grpcio-testing # grpcio-tools - # ikvpy + # milvus-lite # pymilvus # qdrant-client -grpcio-health-checking==1.62.3 \ - --hash=sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93 \ - --hash=sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d - # via feast (setup.py) -grpcio-reflection==1.62.3 \ - --hash=sha256:a48ef37df81a3bada78261fc92ef382f061112f989d1312398b945cc69838b9c \ - --hash=sha256:cb84682933c400bddf94dd94f928d1c6570f500b6dd255973d4bfb495b82585f - # via feast (setup.py) -grpcio-status==1.62.3 \ - --hash=sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485 \ - --hash=sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8 - # via - # google-api-core - # ikvpy -grpcio-testing==1.62.3 \ - --hash=sha256:06a4d7eb30d22f91368aa7f48bfc33563da13b9d951314455ca8c9c987fb75bb \ - --hash=sha256:f63577f28aaa95ea525124a0fd63c3429d71f769f4179b13f5e6cbc54979bfab - # via feast (setup.py) -grpcio-tools==1.62.3 \ - --hash=sha256:0a52cc9444df978438b8d2332c0ca99000521895229934a59f94f37ed896b133 \ - --hash=sha256:0a8c0c4724ae9c2181b7dbc9b186df46e4f62cb18dc184e46d06c0ebeccf569e \ - --hash=sha256:0cb3a3436ac119cbd37a7d3331d9bdf85dad21a6ac233a3411dff716dcbf401e \ - --hash=sha256:11c625eebefd1fd40a228fc8bae385e448c7e32a6ae134e43cf13bbc23f902b7 \ - --hash=sha256:11f363570dea661dde99e04a51bd108a5807b5df32a6f8bdf4860e34e94a4dbf \ - --hash=sha256:141d028bf5762d4a97f981c501da873589df3f7e02f4c1260e1921e565b376fa \ - --hash=sha256:1c989246c2aebc13253f08be32538a4039a64e12d9c18f6d662d7aee641dc8b5 \ - --hash=sha256:1da38070738da53556a4b35ab67c1b9884a5dd48fa2f243db35dc14079ea3d0c \ - --hash=sha256:27cd9ef5c5d68d5ed104b6dcb96fe9c66b82050e546c9e255716903c3d8f0373 \ - --hash=sha256:2e02d3b96f2d0e4bab9ceaa30f37d4f75571e40c6272e95364bff3125a64d184 \ - --hash=sha256:2f968b049c2849540751ec2100ab05e8086c24bead769ca734fdab58698408c1 \ - --hash=sha256:350a80485e302daaa95d335a931f97b693e170e02d43767ab06552c708808950 \ - --hash=sha256:3eae6ea76d62fcac091e1f15c2dcedf1dc3f114f8df1a972a8a0745e89f4cf61 \ - --hash=sha256:47a5c093ab256dec5714a7a345f8cc89315cb57c298b276fa244f37a0ba507f0 \ - --hash=sha256:5782883a27d3fae8c425b29a9d3dcf5f47d992848a1b76970da3b5a28d424b26 \ - --hash=sha256:6a56d344b0bab30bf342a67e33d386b0b3c4e65868ffe93c341c51e1a8853ca5 \ - --hash=sha256:6c3064610826f50bd69410c63101954676edc703e03f9e8f978a135f1aaf97c1 \ - --hash=sha256:703f46e0012af83a36082b5f30341113474ed0d91e36640da713355cd0ea5d23 \ - --hash=sha256:710fecf6a171dcbfa263a0a3e7070e0df65ba73158d4c539cec50978f11dad5d \ - --hash=sha256:7c7136015c3d62c3eef493efabaf9e3380e3e66d24ee8e94c01cb71377f57833 \ - --hash=sha256:7cc83023acd8bc72cf74c2edbe85b52098501d5b74d8377bfa06f3e929803492 \ - --hash=sha256:7f2483ea232bd72d98a6dc6d7aefd97e5bc80b15cd909b9e356d6f3e326b6e43 \ - --hash=sha256:7ff7d58a45b75df67d25f8f144936a3e44aabd91afec833ee06826bd02b7fbe7 \ - --hash=sha256:8ad0473af5544f89fc5a1ece8676dd03bdf160fb3230f967e05d0f4bf89620e3 \ - --hash=sha256:8c5d22b252dcef11dd1e0fbbe5bbfb9b4ae048e8880d33338215e8ccbdb03edc \ - --hash=sha256:8e62cc7164b0b7c5128e637e394eb2ef3db0e61fc798e80c301de3b2379203ed \ - --hash=sha256:962c84b4da0f3b14b3cdb10bc3837ebc5f136b67d919aea8d7bb3fd3df39528a \ - --hash=sha256:ace43b26d88a58dcff16c20d23ff72b04d0a415f64d2820f4ff06b1166f50557 \ - --hash=sha256:b47d0dda1bdb0a0ba7a9a6de88e5a1ed61f07fad613964879954961e36d49193 \ - --hash=sha256:b77f9f9cee87cd798f0fe26b7024344d1b03a7cd2d2cba7035f8433b13986325 \ - --hash=sha256:b881fd9505a84457e9f7e99362eeedd86497b659030cf57c6f0070df6d9c2b9b \ - --hash=sha256:bfda6ee8990997a9df95c5606f3096dae65f09af7ca03a1e9ca28f088caca5cf \ - --hash=sha256:c3a1ac9d394f8e229eb28eec2e04b9a6f5433fa19c9d32f1cb6066e3c5114a1d \ - --hash=sha256:c8ad5cce554e2fcaf8842dee5d9462583b601a3a78f8b76a153c38c963f58c10 \ - --hash=sha256:ca246dffeca0498be9b4e1ee169b62e64694b0f92e6d0be2573e65522f39eea9 \ - --hash=sha256:ca4f5eeadbb57cf03317d6a2857823239a63a59cc935f5bd6cf6e8b7af7a7ecc \ - --hash=sha256:d102b9b21c4e1e40af9a2ab3c6d41afba6bd29c0aa50ca013bf85c99cdc44ac5 \ - --hash=sha256:db3bc9fa39afc5e4e2767da4459df82b095ef0cab2f257707be06c44a1c2c3e5 \ - --hash=sha256:dc9ad9950119d8ae27634e68b7663cc8d340ae535a0f80d85a55e56a6973ab1f \ - --hash=sha256:e02d7c1a02e3814c94ba0cfe43d93e872c758bd8fd5c2797f894d0c49b4a1dfc \ - --hash=sha256:e0898d412a434e768a0c7e365acabe13ff1558b767e400936e26b5b6ed1ee51f \ - --hash=sha256:e18e15287c31baf574fcdf8251fb7f997d64e96c6ecf467906e576da0a079af6 \ - --hash=sha256:ec279dcf3518201fc592c65002754f58a6b542798cd7f3ecd4af086422f33f29 \ - --hash=sha256:ec6fbded0c61afe6f84e3c2a43e6d656791d95747d6d28b73eff1af64108c434 \ - --hash=sha256:eec73a005443061f4759b71a056f745e3b000dc0dc125c9f20560232dfbcbd14 \ - --hash=sha256:f3d812daffd0c2d2794756bd45a353f89e55dc8f91eb2fc840c51b9f6be62667 \ - --hash=sha256:f4b1615adf67bd8bb71f3464146a6f9949972d06d21a4f5e87e73f6464d97f57 \ - --hash=sha256:f6831fdec2b853c9daa3358535c55eed3694325889aa714070528cf8f92d7d6d - # via feast (setup.py) +grpcio-health-checking==1.81.1 \ + --hash=sha256:cbc6a4171825ec64389de2f062d296ba129a5c27eefd0dd55fa837909184bdf9 \ + --hash=sha256:ecc61480e25058a4a04e11e4ab6900ad7439b32e60a8ce4ece7d9f219221c85d + # via feast (pyproject.toml) +grpcio-reflection==1.81.1 \ + --hash=sha256:3d7160000f5fdd0e241f9b1a3d402f15ebcd5f281be105b374a025f38c6434a8 \ + --hash=sha256:d82fc4ac39dd5dcfd63e577075f6a68eac4a765e66a453914bbf57caf35665d0 + # via feast (pyproject.toml) +grpcio-status==1.83.0 \ + --hash=sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae \ + --hash=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 + # via google-api-core +grpcio-testing==1.81.1 \ + --hash=sha256:69792db25781cefd2479d12285ca6bebe589e2aa1d64b4b8d18cf35f58c6b810 \ + --hash=sha256:f381121d7203f4380b1c01e6c8f15aa220bafdc372e8ed82eddf79a16149abcc + # via feast (pyproject.toml) +grpcio-tools==1.81.1 \ + --hash=sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a \ + --hash=sha256:136e90906af0df51ad929713244ba812d0dbb1844b4f467d5d86bdb054698f90 \ + --hash=sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4 \ + --hash=sha256:1d602b410b2b2addc434cace9ce4fe2035974a3078228f98ffa049a5c90acc2f \ + --hash=sha256:204de03b539a4b08772c6553b92bcc112cbc965e0ac22f909f6d133b8ac33a8c \ + --hash=sha256:21bb3ba90e6d8df1ff663d4ee39a4e5b25a64e8ed4902476ca9ded0954d3917a \ + --hash=sha256:2baa7e735f35b2a648144c03348a126097b13e101d3c242d5edb6ac91437ccbe \ + --hash=sha256:2c306c307f8f74cddc4056fdbb6f1da55de087a21120efbd02bd915daa5a52fd \ + --hash=sha256:3389e705460efa3f3758141ba5520e6743b131c9576197c944fb9cbe49048126 \ + --hash=sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5 \ + --hash=sha256:3c8611d6e4e859ac5373422ef27c4b7540cf98c9991c9abc6722613ef72b13aa \ + --hash=sha256:3d604b4fd114b79ebb9f865bf3e04fd3ae93c704e1fad96f7fd03b0865c263b7 \ + --hash=sha256:3dca56016d90a710c4d9861bae793dc089c1430a90c79ce672e948ddb65fa539 \ + --hash=sha256:43baf71dc60fd653062da2e95e95c73b35dd130be8f9fa3d544c3af3f808a290 \ + --hash=sha256:43c528655b226375013036692d8db4cd59060c1f41dd62c77f4d17b69f6ce828 \ + --hash=sha256:53ef76cc3b0493ff734a5e8c39d5b519e1822236fcccdfe7677c5e1efd767761 \ + --hash=sha256:690e6dcaa8b8a7886ce206ba344e2127211597e1a1ddab73df9f3d80c8f6707e \ + --hash=sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c \ + --hash=sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e \ + --hash=sha256:7746e508d4239a02f7e93638be5bc0ebb0120ddb796f7506aaae9d47a4599d97 \ + --hash=sha256:7d168ea26390717d0462c0d0408331dc98a60fc7f7e6118afac9b73f5a66d87c \ + --hash=sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3 \ + --hash=sha256:801d9d8ab5cddf8f8e064225292f0713427011252a07828a6b54e2ed64d534de \ + --hash=sha256:8161f398f957a376cae7385ea7c8684f439d460ef702b528912da3bcb31fc515 \ + --hash=sha256:82740248eb6f3b6a38988cb5e64adb7303af9ea5cb4197c8ed08c1fabc767440 \ + --hash=sha256:87b25ca0e27373a4a32a629a4ba976f5764b9887dd50d6fe017d38009a0363e8 \ + --hash=sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593 \ + --hash=sha256:8a17d8ceeb6a855fadf39f5171c80a382d97c4db98d5943eca553497fdebf84b \ + --hash=sha256:9b6ba8a72cfda576508701a7c0bbeebe6f6f9843320d4f12e74efd19ddccd965 \ + --hash=sha256:9d383724bcd67244b6def9e9164c640ee9380c0b7534ee7545a6fb0022a59afe \ + --hash=sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1 \ + --hash=sha256:a28d231455ab6e3558299f7d831a73c8be8ee6b7ec614ecf39eb50c0ed15767f \ + --hash=sha256:a882382507bb5ec6d7edc9648053dfd3bc8f9285cde56a6fa9b9a83b4bd07f1c \ + --hash=sha256:a987c85dcbe1b32066d7acd46266d1a428aecbd629331bf5b853e74c835bf876 \ + --hash=sha256:a9c6fcc68c9d5a208967bfe4fd3224d3c3be9a950c3e827e8f4b17e15c2dc555 \ + --hash=sha256:ac47a9ea1224df8b653072614e6f0207e9fbfe63fdabaa5918a60ca5fc931b88 \ + --hash=sha256:ad7a997c07bd345e84842e60561e7e2cc090ce6c4e1d2f0407e31b85b40fc49a \ + --hash=sha256:b62d254c214faa3773eac709376ae25cf7abff1a76ba5fc4dbcd7b14fc4e4ae6 \ + --hash=sha256:b6bd163ece4535726e5292b845ed80ae9b2cae73ba091c7d6c66033c430e3857 \ + --hash=sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a \ + --hash=sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e \ + --hash=sha256:bd0b68dc76b10b3384b9b6e9f59202b83dcaafd8098eb644759a69316686acf8 \ + --hash=sha256:bd6c3bf3ea6a61eb58c54368d72ada591f2a270f3a31a32e8536e773337e76d9 \ + --hash=sha256:bdbdc927be2e0ea13c32564a72ee31d712a716fb6f8c0d53d37a77d8277c272c \ + --hash=sha256:cb08172b7b629e75cb33866928d319a3196540a725eaab628ba721007140f1af \ + --hash=sha256:cc410b621dd85193766c12dca2e238696199a27a65d2b31b6f0a4c6c0043ff26 \ + --hash=sha256:eac4bb645ceff0c147cc720a40ae68f97427eaafb4968e866dd8fcc20d3d4831 \ + --hash=sha256:f3eb15849979ca7bb864ce81a74d68b0f225a7f111ed3fe212bfc08cf9812b10 \ + --hash=sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec \ + --hash=sha256:f8cb64f87c45ccca8234fa47e6b21f09e43801ff11b556deecb461b3b3e9f292 \ + --hash=sha256:fc3d2a41a7a4467fa03b391394fffada9291fe8feebc8679b526f6bc36942b25 + # via feast (pyproject.toml) gunicorn==23.0.0 \ --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec # via - # feast (setup.py) + # feast (pyproject.toml) + # mlflow # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore + # httpcore2 # uvicorn -h2==4.3.0 \ - --hash=sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1 \ - --hash=sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd +h2==4.4.0 \ + --hash=sha256:46b551bdcdc7e83cf5c04d0bf93badb8a939bd2287d9fee1abb23a445b9e0580 \ + --hash=sha256:6acffe1aeab79098d7eb0f8385c1add11f2c7a94815f6fa2b7060eeddee3d87c # via httpx happybase==1.3.0 \ --hash=sha256:43b6275d2865fc1364680a03f085491cd85d8b84db3c5aa94d25186685dfd87e \ --hash=sha256:f2644cf1ef9d662fbe6f709fcfd66bf13e949f3efd4745a3230cf5f904fb7839 - # via feast (setup.py) -hazelcast-python-client==5.5.0 \ - --hash=sha256:c797c23c219971d225f8590f6359692c14059c26baa15c2762c95667ae38b90a \ - --hash=sha256:dc8d7c1f494e02994238759ad45a9d9d54a569b8e12f198a0efa6e192774b16d - # via feast (setup.py) -hf-xet==1.1.10 \ - --hash=sha256:0a0005fd08f002180f7a12d4e13b22be277725bc23ed0529f8add5c7a6309c06 \ - --hash=sha256:408aef343800a2102374a883f283ff29068055c111f003ff840733d3b715bb97 \ - --hash=sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045 \ - --hash=sha256:686083aca1a6669bc85c21c0563551cbcdaa5cf7876a91f3d074a030b577231d \ - --hash=sha256:6b6bceb6361c80c1cc42b5a7b4e3efd90e64630bcf11224dcac50ef30a47e435 \ - --hash=sha256:71081925383b66b24eedff3013f8e6bbd41215c3338be4b94ba75fd75b21513b \ - --hash=sha256:eae7c1fc8a664e54753ffc235e11427ca61f4b0477d757cc4eb9ae374b69f09c \ - --hash=sha256:f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f + # via feast (pyproject.toml) +hazelcast-python-client==5.7.0 \ + --hash=sha256:6a218295af38730bd1781a6b2fefbf24851f1d6abb472f0bfe927a281b3f83b7 \ + --hash=sha256:79e7c6e77453e53af29bb2454504e1b8d3a422527813116ee44379c18ee9849f + # via feast (pyproject.toml) +hf-xet==1.5.2 \ + --hash=sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed \ + --hash=sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d \ + --hash=sha256:4a5ecb9cda8512ba2aa8ee5d37c87a1422992165892d653098c7b90247481c3b \ + --hash=sha256:580e59e29bf37aece1f2b68537de1e3fb04f43a23d910dcf6f128280b5bfbba4 \ + --hash=sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f \ + --hash=sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47 \ + --hash=sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025 \ + --hash=sha256:8764488197c1d7b1378c8438c18d2eea902e150dbca0b0f0d2d32603fb9b5576 \ + --hash=sha256:8d7446f72abbf7e01ca5ff131786bc2e74a56393462c17a6bf1e303fbab81db4 \ + --hash=sha256:bee28c619622d36968056532fd49cf2b35ca75099b1d616c31a618a893491380 \ + --hash=sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097 \ + --hash=sha256:d6f9c58549407b84b9a5383afd68db0acc42345326a3159990b36a5ca8a20e4e \ + --hash=sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c \ + --hash=sha256:e396ab0faf6298199ad7a95305c3ca8498cb825978a6485be6d00587ee4ec577 \ + --hash=sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65 \ + --hash=sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799 \ + --hash=sha256:fd3add255549e8ef58fa35b2e42dc016961c050600444e7d77d030ba6b57120e # via huggingface-hub -hiredis==2.4.0 \ - --hash=sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027 \ - --hash=sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577 \ - --hash=sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603 \ - --hash=sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869 \ - --hash=sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228 \ - --hash=sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79 \ - --hash=sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb \ - --hash=sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33 \ - --hash=sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77 \ - --hash=sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9 \ - --hash=sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd \ - --hash=sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a \ - --hash=sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8 \ - --hash=sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9 \ - --hash=sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901 \ - --hash=sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7 \ - --hash=sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0 \ - --hash=sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d \ - --hash=sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213 \ - --hash=sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8 \ - --hash=sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389 \ - --hash=sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330 \ - --hash=sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1 \ - --hash=sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2 \ - --hash=sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288 \ - --hash=sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba \ - --hash=sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded \ - --hash=sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca \ - --hash=sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109 \ - --hash=sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8 \ - --hash=sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea \ - --hash=sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6 \ - --hash=sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8 \ - --hash=sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0 \ - --hash=sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5 \ - --hash=sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5 \ - --hash=sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182 \ - --hash=sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73 \ - --hash=sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af \ - --hash=sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac \ - --hash=sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204 \ - --hash=sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277 \ - --hash=sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212 \ - --hash=sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41 \ - --hash=sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e \ - --hash=sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c \ - --hash=sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2 \ - --hash=sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4 \ - --hash=sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348 \ - --hash=sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b \ - --hash=sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e \ - --hash=sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995 \ - --hash=sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371 \ - --hash=sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613 \ - --hash=sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b \ - --hash=sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d \ - --hash=sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708 \ - --hash=sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08 \ - --hash=sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca \ - --hash=sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12 \ - --hash=sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8 \ - --hash=sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829 \ - --hash=sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38 \ - --hash=sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825 \ - --hash=sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67 \ - --hash=sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056 \ - --hash=sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376 \ - --hash=sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214 \ - --hash=sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982 \ - --hash=sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034 \ - --hash=sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1 \ - --hash=sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f \ - --hash=sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9 \ - --hash=sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5 \ - --hash=sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390 \ - --hash=sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0 \ - --hash=sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d \ - --hash=sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57 \ - --hash=sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe \ - --hash=sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2 \ - --hash=sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415 \ - --hash=sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c \ - --hash=sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b \ - --hash=sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853 \ - --hash=sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36 \ - --hash=sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae \ - --hash=sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd \ - --hash=sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187 \ - --hash=sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c \ - --hash=sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542 \ - --hash=sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d \ - --hash=sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79 \ - --hash=sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7 \ - --hash=sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def - # via feast (setup.py) -hpack==4.1.0 \ - --hash=sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496 \ - --hash=sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca +hiredis==3.4.0 \ + --hash=sha256:02298551060cbfe17b1acb02e9d066eadd51e37357369736f8fc1f5533875255 \ + --hash=sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc \ + --hash=sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865 \ + --hash=sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c \ + --hash=sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec \ + --hash=sha256:06c163607f163a300cba9ef02fd8a234dd644a208bfafe972614f72e5287b9ba \ + --hash=sha256:0731dd9f2bf4d3417947a3cbbca4dd4d9fa6ad7cecfd4e16439c17a4562d6304 \ + --hash=sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419 \ + --hash=sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320 \ + --hash=sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b \ + --hash=sha256:0bcb630add6bc9ea136fce691ddff0c46aa91cb860df4ca789fe44127eb7e90d \ + --hash=sha256:12ea5facb5b08fa23e4c101ec2151f3a3de8ecec412fec58dbde0a6eebca02c7 \ + --hash=sha256:12eca9aea1450d1a85dc15574a985c227e52abbc2b6466f48ad2aa3b82124701 \ + --hash=sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f \ + --hash=sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017 \ + --hash=sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc \ + --hash=sha256:16a3ea8193c5f4ee6e8daafe0126207cef38eec5a923c97047e6985d9bb1b61f \ + --hash=sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4 \ + --hash=sha256:1bfb9ccfb13be63883e5f2e5ff7f6fc87bf256f8243af594257dfbed9dbc3cf0 \ + --hash=sha256:1c9e141b420dae63ea21303a7e8672282326cae67472768e9a3f6e7f0990bd28 \ + --hash=sha256:1d8c21fb85f2a3de9328cf5bace84b63da00028e771b92335ff4fdc00121dd5a \ + --hash=sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5 \ + --hash=sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016 \ + --hash=sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240 \ + --hash=sha256:282c4310af72afbe18b07d416459f4febeaeb805a067a7df790136e0e550fcb2 \ + --hash=sha256:2c432cce38190c3c13b6c28d6840ee85db50765ad510134007f1cfb3844dce73 \ + --hash=sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591 \ + --hash=sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea \ + --hash=sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980 \ + --hash=sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b \ + --hash=sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb \ + --hash=sha256:386b556f48fb0f9f09696b9d37f1cae554123fbd9f091d0ca23087f2aca02887 \ + --hash=sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b \ + --hash=sha256:4190bd07dd7879a8a7ddbb2a4f74d402721f3898276e35beb98851b85b5f539c \ + --hash=sha256:423570ac4d2665c0d55d8707b7859a331e9001da5e437b7b7a23e0acbce770c9 \ + --hash=sha256:43004b0b48abc628dda1ac3ac4871e1326c126f8cd9f11164d61934d827d7a3b \ + --hash=sha256:4404c557fd49bcfe24dff41f1209e4221c76d1607df2fb2dfd39474b5b086dcb \ + --hash=sha256:44660a91e0fbc803c29b337c1a9194c8d7b4cd3a3868d28f747cbec2df165483 \ + --hash=sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582 \ + --hash=sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2 \ + --hash=sha256:45c6c296056641b5df37cedafe7d1553f33bc247e2f81603a4d038b39261879b \ + --hash=sha256:46e041fc7a83eaa989ef5fe09526bc2353a6ba39b4ce5191684eabefc02e17b0 \ + --hash=sha256:4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e \ + --hash=sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d \ + --hash=sha256:4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d \ + --hash=sha256:4e5530d7a6c71c1a9c3b850b08bbc8178ffc9556e3aef7f9c808fe9ff91a6f11 \ + --hash=sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763 \ + --hash=sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584 \ + --hash=sha256:5359caad5b57da0bce11d2880f22617ba3710f0866121a924745447848448034 \ + --hash=sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79 \ + --hash=sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9 \ + --hash=sha256:64cf54c1dad35830bf1d86b343ad3c55aed443af51c5ccead8afd1b547aca196 \ + --hash=sha256:6774f1fe2723001ca0cd42bf5d8b1235301226273915c581c5c1260d4d114c43 \ + --hash=sha256:68c81c04e0d8778c01163dd2bc6f0e0236fc7f650c291526f10d4faccd99e5a0 \ + --hash=sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb \ + --hash=sha256:69d0326f20354ce278cbb86f5ae47cb390e22bb94a66877031038af907c42fa5 \ + --hash=sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22 \ + --hash=sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b \ + --hash=sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9 \ + --hash=sha256:7b159315df71a009375227384aa4219f98c2342ccd8eb33e3f4b58654f426376 \ + --hash=sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203 \ + --hash=sha256:7f7fc1535f6e1a190089eae46dee25f0c6b72bb221d377be07092803b8208733 \ + --hash=sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f \ + --hash=sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13 \ + --hash=sha256:85a9abf2d16b4cb112f6cc6c32236d763b34d21c69b00c2f81a4ff3016fcc1d6 \ + --hash=sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c \ + --hash=sha256:899d5cea72c9a6c2d9b6e7b223de62c9e9f541f48267cc7135764070548735ce \ + --hash=sha256:89e95f50ec4d29645ba2da9010e8717861585dc9f3df354a367f43807a6db3c7 \ + --hash=sha256:8aaaab18314fd25453b5cf59c8cdca4110e419455bcb4c0737d19d4151513e75 \ + --hash=sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1 \ + --hash=sha256:8f2c8569460aa456a294d5e9a3579a382b825dc2e283e3cc398323dcfb6a3dee \ + --hash=sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151 \ + --hash=sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a \ + --hash=sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d \ + --hash=sha256:98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c \ + --hash=sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f \ + --hash=sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d \ + --hash=sha256:a315009b441a0105a373a9a780ebb1c6f7d9ead88ac6ea5f2a15791353c6f590 \ + --hash=sha256:a3796094f616f72976ff51e4dc1a016e753c0f9af5393b2df96920b6bae1e19b \ + --hash=sha256:a427976ba339da624367613d7eba52a1516e6d5c7f8988dc8fb888fbcf52d8b6 \ + --hash=sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb \ + --hash=sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530 \ + --hash=sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4 \ + --hash=sha256:b0c29ac4f50fc61a904acfe76e4ce6bc37e16ab5a98089cb411476d466e1bb36 \ + --hash=sha256:b32afb576705fc5a02440d416ef476ddbcc827a20ff6c4a9d557882bc6b75135 \ + --hash=sha256:b6228afacadb19a858289319d72797023e2cb048f2f930b68b7fe57eaed5fb6b \ + --hash=sha256:b98f44bf873731d9ce99e6b3eeb6196bf7b27693ad9f6de0fba986b2b0845127 \ + --hash=sha256:bb44efa4fa3e3ed7779ad0ade3c08ed5d75ca7a6336893e9a4f2722093b4168a \ + --hash=sha256:bb8b2dd9c00d80ad2870ffbb51ce9af99fb2194c082d7b09f4125d90e21426f6 \ + --hash=sha256:bca175f02a2b0150ffe7f5dc8bf49c798f34d2c7024d17ace0ec97a7583560e3 \ + --hash=sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59 \ + --hash=sha256:c2245c46b4ced5f689469e6dcdfc8a0895bf873840a6600f5ea759cdf1b26a8b \ + --hash=sha256:c8c6337e7ad187e8039d06e4b33ee5148dcd73928819d94087e649eb37316a09 \ + --hash=sha256:ccc5c660e31d788ca534a20f2ccb7a80b946b960e18ed4e1db950fcac122b405 \ + --hash=sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643 \ + --hash=sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c \ + --hash=sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4 \ + --hash=sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d \ + --hash=sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352 \ + --hash=sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81 \ + --hash=sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5 \ + --hash=sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092 \ + --hash=sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4 \ + --hash=sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad \ + --hash=sha256:e29267ecdd08758926f1a9221af2671d90f475480c40aff409921b1f362f1bd5 \ + --hash=sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d \ + --hash=sha256:ed1dba2695f6de009c67d63b39ff978cb43b8a79362f697acedffb7743e50d21 \ + --hash=sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1 \ + --hash=sha256:f3c67f39b112dc35f68d5b59ee111db6121f037d1a60cf3840ecffbb2ec5686b \ + --hash=sha256:f7c7596fbb2b5202e943180353958e89014e763c7f25877a92f70bbde6cd7f19 \ + --hash=sha256:ff28eca77a57751488df90b0f385b472fd78e140a4d45097224648a5737acdf5 \ + --hash=sha256:ff6217da86a63a6a5ea954d9b9ed67916d9d94a566b44aa98e68ae34b40ebbbf + # via feast (pyproject.toml) +hpack==4.2.0 \ + --hash=sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0 \ + --hash=sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986 # via h2 httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httpcore2==2.9.1 \ + --hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \ + --hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26 + # via httpx2 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn httpx[http2]==0.27.2 \ --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 # via - # feast (setup.py) + # feast (pyproject.toml) + # datasets # fastapi-mcp # jupyterlab - # mcp + # openlineage-python # python-keycloak # qdrant-client -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d +httpx2==2.9.1 \ + --hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \ + --hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a # via mcp -huggingface-hub==0.35.3 \ - --hash=sha256:0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba \ - --hash=sha256:350932eaa5cc6a4747efae85126ee220e4ef1b54e29d31c3b45c5612ddf0b32a +huggingface-hub==0.36.2 \ + --hash=sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a \ + --hash=sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270 # via # accelerate # datasets # docling # docling-ibm-models + # sentence-transformers # timm # tokenizers # transformers @@ -1766,70 +2075,62 @@ hyperframe==6.1.0 \ --hash=sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5 \ --hash=sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08 # via h2 -ibis-framework[duckdb, mssql]==9.5.0 \ - --hash=sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0 \ - --hash=sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84 - # via - # feast (setup.py) - # ibis-substrait -ibis-substrait==4.0.1 \ - --hash=sha256:107ca49383a3cca2fdc88f67ea2f0172620c16fa8f39c9c52305af85dd6180b4 \ - --hash=sha256:614810a173d096fbc49d87a9b419e2162a3c25d8efda1a4d57a389ce56b9041f - # via feast (setup.py) -identify==2.6.15 \ - --hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \ - --hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf +ibis-framework[duckdb, mssql, oracle]==12.0.0 \ + --hash=sha256:0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 \ + --hash=sha256:238624f2c14fdab8382ca2f4f667c3cdb81e29844cd5f8db8a325d0743767c61 + # via feast (pyproject.toml) +identify==2.6.19 \ + --hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \ + --hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842 # via pre-commit -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx + # httpx2 # jsonschema # requests # snowflake-connector-python # yarl -ikvpy==0.0.36 \ - --hash=sha256:b0edf6fb6482877940f6c2b5d59a7fabe30cb554b13b88ca52805f043cfda5b3 \ - --hash=sha256:c0ce7dfb61456c283c9ba2cdeb68b3647f245c3905bca652ca2a1068804939d1 - # via feast (setup.py) -imageio==2.37.0 \ - --hash=sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed \ - --hash=sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996 +imageio==2.37.4 \ + --hash=sha256:1ab2e22c8debf700f24c3ac43e8f95f3b3a8110c83b93411e97b4b0b2cd1c7e6 \ + --hash=sha256:e45cbc5e83502047fb138f7f585f7f105a136a57eea5f4b3cfc6ce1b52720bd3 # via scikit-image -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a +imagesize==2.0.0 \ + --hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \ + --hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3 # via sphinx -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==8.9.0 \ + --hash=sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee \ + --hash=sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f # via # build # dask -importlib-resources==6.5.2 \ - --hash=sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c \ - --hash=sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec + # mlflow-skinny +importlib-resources==7.1.0 \ + --hash=sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708 \ + --hash=sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1 # via happybase -iniconfig==2.1.0 \ - --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ - --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via pytest -ipykernel==7.0.0 \ - --hash=sha256:06aef83f27adbce00b23345aa70f749f907dc4ac6f4a41fe7bf5f780dc506225 \ - --hash=sha256:28793cecaa6a669e3be80eb6d24803202388b6a955929b0a4e13404d8c92062b +ipykernel==7.3.0 \ + --hash=sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057 \ + --hash=sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09 # via jupyterlab -ipython==8.37.0 \ - --hash=sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216 \ - --hash=sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2 +ipython==8.39.0 \ + --hash=sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624 \ + --hash=sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f # via # great-expectations # ipykernel # ipywidgets -ipywidgets==8.1.7 \ - --hash=sha256:15f1ac050b9ccbefd45dccfbb2ef6bed0029d8278682d569d71b8dd96bee0376 \ - --hash=sha256:764f2602d25471c213919b8a1997df04bef869251db4ca8efba1b76b1bd9f7bb +ipywidgets==8.1.8 \ + --hash=sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668 \ + --hash=sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e # via great-expectations isodate==0.7.2 \ --hash=sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15 \ @@ -1839,51 +2140,55 @@ isoduration==20.11.0 \ --hash=sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9 \ --hash=sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042 # via jsonschema -jedi==0.19.2 \ - --hash=sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0 \ - --hash=sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9 +itsdangerous==2.2.0 \ + --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ + --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 + # via flask +jedi==0.20.0 \ + --hash=sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67 \ + --hash=sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011 # via ipython jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via - # feast (setup.py) + # feast (pyproject.toml) # altair + # flask # great-expectations # jupyter-server # jupyterlab # jupyterlab-server # moto # nbconvert - # poetry-dynamic-versioning # sphinx # torch -jmespath==1.0.1 \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 # via # aiobotocore # boto3 # botocore -joblib==1.5.2 \ - --hash=sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55 \ - --hash=sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241 +joblib==1.5.3 \ + --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ + --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 # via scikit-learn -json5==0.12.1 \ - --hash=sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990 \ - --hash=sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5 +json5==0.15.0 \ + --hash=sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618 \ + --hash=sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71 # via jupyterlab-server -jsonlines==3.1.0 \ - --hash=sha256:2579cb488d96f815b0eb81629e3e6b0332da0962a18fa3532958f7ba14a5c37f \ - --hash=sha256:632f5e38f93dfcb1ac8c4e09780b92af3a55f38f26e7c47ae85109d420b6ad39 +jsonlines==4.0.0 \ + --hash=sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74 \ + --hash=sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55 # via docling-ibm-models jsonpatch==1.33 \ --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c # via great-expectations -jsonpointer==3.0.0 \ - --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ - --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef +jsonpointer==3.1.1 \ + --hash=sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900 \ + --hash=sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca # via # jsonpatch # jsonschema @@ -1891,11 +2196,10 @@ jsonref==1.1.0 \ --hash=sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552 \ --hash=sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9 # via docling-core -jsonschema[format-nongpl]==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 +jsonschema[format-nongpl]==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce # via - # feast (setup.py) # altair # docling-core # great-expectations @@ -1908,82 +2212,208 @@ jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -jupyter-client==8.6.3 \ - --hash=sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419 \ - --hash=sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f +jupyter-builder==1.1.1 \ + --hash=sha256:1a13977912b08deda77fce2c803940131c27cf77a27ed64b9ffca25aa0ed7e6c \ + --hash=sha256:f9c14bc55c0488a073f62af12d468936fcf9ecb7e9dd802f6f9c33de46ad70db + # via + # jupyterlab + # notebook +jupyter-client==8.9.1 \ + --hash=sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81 \ + --hash=sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa # via # ipykernel # jupyter-server # nbclient -jupyter-core==5.8.1 \ - --hash=sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941 \ - --hash=sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0 +jupyter-core==5.9.1 \ + --hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \ + --hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 # via # ipykernel + # jupyter-builder # jupyter-client # jupyter-server # jupyterlab # nbclient # nbconvert # nbformat -jupyter-events==0.12.0 \ - --hash=sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb \ - --hash=sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b +jupyter-events==0.12.1 \ + --hash=sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf \ + --hash=sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3 # via jupyter-server -jupyter-lsp==2.3.0 \ - --hash=sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245 \ - --hash=sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f +jupyter-lsp==2.3.1 \ + --hash=sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81 \ + --hash=sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6 # via jupyterlab -jupyter-server==2.17.0 \ - --hash=sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5 \ - --hash=sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f +jupyter-server==2.20.0 \ + --hash=sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14 \ + --hash=sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc # via # jupyter-lsp # jupyterlab # jupyterlab-server # notebook # notebook-shim -jupyter-server-terminals==0.5.3 \ - --hash=sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa \ - --hash=sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269 +jupyter-server-terminals==0.5.4 \ + --hash=sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14 \ + --hash=sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5 # via jupyter-server -jupyterlab==4.4.9 \ - --hash=sha256:394c902827350c017430a8370b9f40c03c098773084bc53930145c146d3d2cb2 \ - --hash=sha256:ea55aca8269909016d5fde2dc09b97128bc931230183fe7e2920ede5154ad9c2 +jupyterlab==4.6.2 \ + --hash=sha256:5964447036629adfcd3fc0969effc1da6f47d2cbd0a60b2c2eea7c31be0ec6a8 \ + --hash=sha256:e18ce8b34f3de350e93cd5b2c4f3ae884cbe266eb76bf5d6825a4ed34c13bcff # via notebook jupyterlab-pygments==0.3.0 \ --hash=sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d \ --hash=sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780 # via nbconvert -jupyterlab-server==2.27.3 \ - --hash=sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4 \ - --hash=sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4 +jupyterlab-server==2.28.0 \ + --hash=sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c \ + --hash=sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968 # via # jupyterlab # notebook -jupyterlab-widgets==3.0.15 \ - --hash=sha256:2920888a0c2922351a9202817957a68c07d99673504d6cd37345299e971bb08b \ - --hash=sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c +jupyterlab-widgets==3.0.16 \ + --hash=sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0 \ + --hash=sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8 # via ipywidgets -jwcrypto==1.5.6 \ - --hash=sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789 \ - --hash=sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039 +jwcrypto==1.5.8 \ + --hash=sha256:85aeb475f808d56bbc2f2ed1f6f73e6a317c4011a4321505f02f0aed695a3742 \ + --hash=sha256:c3d7114b6f6e65b52f6b7da817eb8cb8423e1da31e1ef13508447c81ecbdcc34 # via python-keycloak -kubernetes==33.1.0 \ - --hash=sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5 \ - --hash=sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993 - # via feast (setup.py) -lark==1.3.0 \ - --hash=sha256:80661f261fb2584a9828a097a2432efd575af27d20be0fd35d17f0fe37253831 \ - --hash=sha256:9a3839d0ca5e1faf7cfa3460e420e859b66bcbde05b634e73c369c8244c5fa48 +kiwisolver==1.5.0 \ + --hash=sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9 \ + --hash=sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679 \ + --hash=sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0 \ + --hash=sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8 \ + --hash=sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276 \ + --hash=sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96 \ + --hash=sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e \ + --hash=sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac \ + --hash=sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f \ + --hash=sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a \ + --hash=sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15 \ + --hash=sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7 \ + --hash=sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368 \ + --hash=sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02 \ + --hash=sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9 \ + --hash=sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681 \ + --hash=sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57 \ + --hash=sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27 \ + --hash=sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4 \ + --hash=sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920 \ + --hash=sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374 \ + --hash=sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3 \ + --hash=sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa \ + --hash=sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23 \ + --hash=sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859 \ + --hash=sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb \ + --hash=sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d \ + --hash=sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc \ + --hash=sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581 \ + --hash=sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c \ + --hash=sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099 \ + --hash=sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05 \ + --hash=sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9 \ + --hash=sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd \ + --hash=sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc \ + --hash=sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796 \ + --hash=sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303 \ + --hash=sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca \ + --hash=sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314 \ + --hash=sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489 \ + --hash=sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57 \ + --hash=sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1 \ + --hash=sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797 \ + --hash=sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021 \ + --hash=sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db \ + --hash=sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22 \ + --hash=sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028 \ + --hash=sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083 \ + --hash=sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65 \ + --hash=sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588 \ + --hash=sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0 \ + --hash=sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a \ + --hash=sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1 \ + --hash=sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c \ + --hash=sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac \ + --hash=sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476 \ + --hash=sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53 \ + --hash=sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3 \ + --hash=sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4 \ + --hash=sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615 \ + --hash=sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb \ + --hash=sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18 \ + --hash=sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b \ + --hash=sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1 \ + --hash=sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2 \ + --hash=sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c \ + --hash=sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac \ + --hash=sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d \ + --hash=sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf \ + --hash=sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2 \ + --hash=sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f \ + --hash=sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f \ + --hash=sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4 \ + --hash=sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9 \ + --hash=sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e \ + --hash=sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737 \ + --hash=sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b \ + --hash=sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed \ + --hash=sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3 \ + --hash=sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7 \ + --hash=sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08 \ + --hash=sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e \ + --hash=sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902 \ + --hash=sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd \ + --hash=sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6 \ + --hash=sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310 \ + --hash=sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537 \ + --hash=sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554 \ + --hash=sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e \ + --hash=sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87 \ + --hash=sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a \ + --hash=sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c \ + --hash=sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79 \ + --hash=sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e \ + --hash=sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16 \ + --hash=sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1 \ + --hash=sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875 \ + --hash=sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd \ + --hash=sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0 \ + --hash=sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9 \ + --hash=sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646 \ + --hash=sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657 \ + --hash=sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4 \ + --hash=sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232 \ + --hash=sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 \ + --hash=sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 \ + --hash=sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309 \ + --hash=sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede \ + --hash=sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2 \ + --hash=sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203 \ + --hash=sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7 \ + --hash=sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df \ + --hash=sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c \ + --hash=sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167 \ + --hash=sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3 \ + --hash=sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09 \ + --hash=sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398 + # via matplotlib +kubernetes==36.0.3 \ + --hash=sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7 \ + --hash=sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f + # via feast (pyproject.toml) +lark==1.3.1 \ + --hash=sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905 \ + --hash=sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12 # via rfc3987-syntax -latex2mathml==3.78.1 \ - --hash=sha256:f089b6d75e85b937f99693c93e8c16c0804008672c3dd2a3d25affd36f238100 \ - --hash=sha256:f941db80bf41db33f31df87b304e8b588f8166b813b0257c11c98f7a9d0aac71 +latex2mathml==3.81.0 \ + --hash=sha256:4b959cdc3cac8686bc0e3e5aece8127dfb1b81ca1241bed8e00ef31b82bb4022 \ + --hash=sha256:d317710393fe20579aea39cfe8928fa2ad9b8780896e585326c75e89c1d1d1a4 # via docling-core -lazy-loader==0.4 \ - --hash=sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc \ - --hash=sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1 +lazy-loader==0.5 \ + --hash=sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3 \ + --hash=sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005 # via scikit-image locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ @@ -2126,48 +2556,64 @@ lxml==5.4.0 \ # docling # python-docx # python-pptx -lz4==4.4.4 \ - --hash=sha256:017f8d269a739405a59d68a4d63d23a8df23e3bb2c70aa069b7563af08dfdffb \ - --hash=sha256:070fd0627ec4393011251a094e08ed9fdcc78cb4e7ab28f507638eee4e39abda \ - --hash=sha256:18ae4fe3bafb344dbd09f976d45cbf49c05c34416f2462828f9572c1fa6d5af7 \ - --hash=sha256:1ea7f07329f85a8eda4d8cf937b87f27f0ac392c6400f18bea2c667c8b7f8ecc \ - --hash=sha256:23ae267494fdd80f0d2a131beff890cf857f1b812ee72dbb96c3204aab725553 \ - --hash=sha256:2f4f2965c98ab254feddf6b5072854a6935adab7bc81412ec4fe238f07b85f62 \ - --hash=sha256:30ebbc5b76b4f0018988825a7e9ce153be4f0d4eba34e6c1f2fcded120573e88 \ - --hash=sha256:33e01e18e4561b0381b2c33d58e77ceee850a5067f0ece945064cbaac2176962 \ - --hash=sha256:38730927ad51beb42ab8dbc5555270bfbe86167ba734265f88bbd799fced1004 \ - --hash=sha256:4134b9fd70ac41954c080b772816bb1afe0c8354ee993015a83430031d686a4c \ - --hash=sha256:45e7c954546de4f85d895aa735989d77f87dd649f503ce1c8a71a151b092ed36 \ - --hash=sha256:4ab1537bd3b3bfbafd3c8847e06827129794488304f21945fc2f5b669649d94f \ - --hash=sha256:57fd20c5fc1a49d1bbd170836fccf9a338847e73664f8e313dce6ac91b8c1e02 \ - --hash=sha256:585b42eb37ab16a278c3a917ec23b2beef175aa669f4120142b97aebf90ef775 \ - --hash=sha256:6b56aa9eef830bf6443acd8c4e18b208a8993dc32e0d6ef4263ecfa6afb3f599 \ - --hash=sha256:6ea715bb3357ea1665f77874cf8f55385ff112553db06f3742d3cdcec08633f7 \ - --hash=sha256:714f9298c86f8e7278f1c6af23e509044782fa8220eb0260f8f8f1632f820550 \ - --hash=sha256:80dd27d7d680ea02c261c226acf1d41de2fd77af4fb2da62b278a9376e380de0 \ - --hash=sha256:8ccab8f7f7b82f9fa9fc3b0ba584d353bd5aa818d5821d77d5b9447faad2aaad \ - --hash=sha256:900912e8a7cf74b4a2bea18a3594ae0bf1138f99919c20017167b6e05f760aa4 \ - --hash=sha256:9b7d6dddfd01b49aedb940fdcaf32f41dc58c926ba35f4e31866aeec2f32f4f4 \ - --hash=sha256:a355223a284f42a723c120ce68827de66d5cb872a38732b3d5abbf544fa2fe26 \ - --hash=sha256:a760a175b46325b2bb33b1f2bbfb8aa21b48e1b9653e29c10b6834f9bb44ead4 \ - --hash=sha256:a8474c91de47733856c6686df3c4aca33753741da7e757979369c2c0d32918ba \ - --hash=sha256:b28228197775b7b5096898851d59ef43ccaf151136f81d9c436bc9ba560bc2ba \ - --hash=sha256:bd1add57b6fe1f96bed2d529de085e9378a3ac04b86f116d10506f85b68e97fc \ - --hash=sha256:d0be9f68240231e1e44118a4ebfecd8a5d4184f0bdf5c591c98dd6ade9720afd \ - --hash=sha256:d21d1a2892a2dcc193163dd13eaadabb2c1b803807a5117d8f8588b22eaf9f12 \ - --hash=sha256:d33a5105cd96ebd32c3e78d7ece6123a9d2fb7c18b84dec61f27837d9e0c496c \ - --hash=sha256:dac522788296a9a02a39f620970dea86c38e141e21e51238f1b5e9fa629f8e69 \ - --hash=sha256:dc64d6dfa7a89397529b22638939e70d85eaedc1bd68e30a29c78bfb65d4f715 \ - --hash=sha256:ddfc7194cd206496c445e9e5b0c47f970ce982c725c87bd22de028884125b68f \ - --hash=sha256:e3fc90f766401684740978cd781d73b9685bd81b5dbf7257542ef9de4612e4d2 \ - --hash=sha256:e43e9d48b2daf80e486213128b0763deed35bbb7a59b66d1681e205e1702d735 \ - --hash=sha256:e9cb387c33f014dae4db8cb4ba789c8d2a0a6d045ddff6be13f6c8d9def1d2a6 \ - --hash=sha256:e9ec5d45ea43684f87c316542af061ef5febc6a6b322928f059ce1fb289c298a \ - --hash=sha256:ed6eb9f8deaf25ee4f6fad9625d0955183fdc90c52b6f79a76b7f209af1b6e54 \ - --hash=sha256:f170abb8416c4efca48e76cac2c86c3185efdf841aecbe5c190121c42828ced0 \ - --hash=sha256:f4c21648d81e0dda38b4720dccc9006ae33b0e9e7ffe88af6bf7d4ec124e2fba \ - --hash=sha256:f5024d3ca2383470f7c4ef4d0ed8eabad0b22b23eeefde1c192cf1a38d5e9f78 \ - --hash=sha256:fff9f3a1ed63d45cb6514bfb8293005dc4141341ce3500abdfeb76124c0b9b2e +lz4==4.4.5 \ + --hash=sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc \ + --hash=sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f \ + --hash=sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d \ + --hash=sha256:13254bd78fef50105872989a2dc3418ff09aefc7d0765528adc21646a7288294 \ + --hash=sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397 \ + --hash=sha256:1dd4d91d25937c2441b9fc0f4af01704a2d09f30a38c5798bc1d1b5a15ec9581 \ + --hash=sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50 \ + --hash=sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d \ + --hash=sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e \ + --hash=sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd \ + --hash=sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1 \ + --hash=sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e \ + --hash=sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7 \ + --hash=sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668 \ + --hash=sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64 \ + --hash=sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c \ + --hash=sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0 \ + --hash=sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb \ + --hash=sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43 \ + --hash=sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901 \ + --hash=sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f \ + --hash=sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c \ + --hash=sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a \ + --hash=sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33 \ + --hash=sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989 \ + --hash=sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5 \ + --hash=sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd \ + --hash=sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f \ + --hash=sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b \ + --hash=sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004 \ + --hash=sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be \ + --hash=sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9 \ + --hash=sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba \ + --hash=sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d \ + --hash=sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d \ + --hash=sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832 \ + --hash=sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301 \ + --hash=sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a \ + --hash=sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67 \ + --hash=sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9 \ + --hash=sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f \ + --hash=sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6 \ + --hash=sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb \ + --hash=sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d \ + --hash=sha256:d64141085864918392c3159cdad15b102a620a67975c786777874e1e90ef15ce \ + --hash=sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4 \ + --hash=sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7 \ + --hash=sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22 \ + --hash=sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758 \ + --hash=sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e \ + --hash=sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67 \ + --hash=sha256:e64e61f29cf95afb43549063d8433b46352baf0c8a70aa45e2585618fcf59d86 \ + --hash=sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3 \ + --hash=sha256:f32b9e65d70f3684532358255dc053f143835c5f5991e28a5ac4c93ce94b9ea7 \ + --hash=sha256:f6538aaaedd091d6e5abdaa19b99e6e82697d67518f114721b5248709b639fad \ + --hash=sha256:f9b8bde9909a010c75b3aea58ec3910393b758f3c219beed67063693df854db0 \ + --hash=sha256:ff1b50aeeec64df5603f17984e4b5be6166058dcf8f1e26a3da40d7a0f6ab547 # via # clickhouse-connect # trino @@ -2175,13 +2621,17 @@ makefun==1.16.0 \ --hash=sha256:43baa4c3e7ae2b17de9ceac20b669e9a67ceeadff31581007cca20a07bbe42c4 \ --hash=sha256:e14601831570bff1f6d7e68828bcd30d2f5856f24bad5de0ccb22921ceebc947 # via great-expectations -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +mako==1.3.12 \ + --hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \ + --hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a + # via alembic +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich -marko==2.2.1 \ - --hash=sha256:31e9a18b35c113e506ace5594716fa3df2872f8955908e279bc551f3eb1f0db8 \ - --hash=sha256:e29d7e071a3b0cb2f7cc4c500d55f893dc5a45d85a8298dde6cb4e4dffd794d3 +marko==2.2.3 \ + --hash=sha256:8e1d7a0387281e59dfbc52a381b58c570156970e36b2bbe047f8a3a2f368cacc \ + --hash=sha256:e31ec2875383bc62f9093d16babed5a2c2cde601c00d834ea935a2222120ec19 # via docling markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ @@ -2274,178 +2724,235 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via + # flask # jinja2 + # mako # nbconvert # werkzeug -marshmallow==3.26.1 \ - --hash=sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c \ - --hash=sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6 - # via - # environs - # great-expectations -matplotlib-inline==0.1.7 \ - --hash=sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90 \ - --hash=sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca +marshmallow==3.26.2 \ + --hash=sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73 \ + --hash=sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57 + # via great-expectations +matplotlib==3.10.9 \ + --hash=sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9 \ + --hash=sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42 \ + --hash=sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d \ + --hash=sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b \ + --hash=sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37 \ + --hash=sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b \ + --hash=sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456 \ + --hash=sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc \ + --hash=sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f \ + --hash=sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6 \ + --hash=sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2 \ + --hash=sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4 \ + --hash=sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320 \ + --hash=sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20 \ + --hash=sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf \ + --hash=sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c \ + --hash=sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80 \ + --hash=sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9 \ + --hash=sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716 \ + --hash=sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585 \ + --hash=sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb \ + --hash=sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38 \ + --hash=sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4 \ + --hash=sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2 \ + --hash=sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217 \ + --hash=sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838 \ + --hash=sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4 \ + --hash=sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda \ + --hash=sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb \ + --hash=sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f \ + --hash=sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f \ + --hash=sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f \ + --hash=sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c \ + --hash=sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb \ + --hash=sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b \ + --hash=sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285 \ + --hash=sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294 \ + --hash=sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65 \ + --hash=sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e \ + --hash=sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d \ + --hash=sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f \ + --hash=sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8 \ + --hash=sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39 \ + --hash=sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6 \ + --hash=sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f \ + --hash=sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf \ + --hash=sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2 \ + --hash=sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe \ + --hash=sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99 \ + --hash=sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb \ + --hash=sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8 \ + --hash=sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1 \ + --hash=sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921 \ + --hash=sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba \ + --hash=sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358 + # via mlflow +matplotlib-inline==0.2.2 \ + --hash=sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6 \ + --hash=sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79 # via # ipykernel # ipython -mcp==1.12.4 \ - --hash=sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5 \ - --hash=sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789 +mcp==2.0.0 \ + --hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \ + --hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6 # via fastapi-mcp +mcp-types==2.0.0 \ + --hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \ + --hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379 + # via mcp mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -milvus-lite==2.4.12 \ - --hash=sha256:20087663e7b4385050b7ad08f1f03404426d4c87b1ff91d5a8723eee7fd49e88 \ - --hash=sha256:334037ebbab60243b5d8b43d54ca2f835d81d48c3cda0c6a462605e588deb05d \ - --hash=sha256:a0f3a5ddbfd19f4a6b842b2fd3445693c796cde272b701a1646a94c1ac45d3d7 \ - --hash=sha256:e8d4f7cdd5f731efd6faeee3715d280fd91a5f9b4d89312664d56401f65b1473 - # via - # feast (setup.py) - # pymilvus +milvus-lite==3.1.1 \ + --hash=sha256:0cbf8386a2e99b6464a5eda14cf8e31934b0e287e36c831e293aee614d9efc2e \ + --hash=sha256:0f343c9bf1e291d6c2fa615a52a57090ebbaf96b84bfb71f7eb73cd95cd7fc1c + # via feast (pyproject.toml) minio==7.2.11 \ --hash=sha256:153582ed52ff3b5005ba558e1f25bfe1e9e834f7f0745e594777f28e3e81e1a0 \ --hash=sha256:4db95a21fe1e2022ec975292d8a1f83bd5b18f830d23d42a4518ac7a5281d7c5 - # via feast (setup.py) -mistune==3.1.4 \ - --hash=sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d \ - --hash=sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164 + # via feast (pyproject.toml) +mistune==3.3.4 \ + --hash=sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe \ + --hash=sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a # via # great-expectations # nbconvert -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) +mlflow==3.2.0 \ + --hash=sha256:db97b925cc8afba15caf3749dcb4a95be83f9608e974f23253fbbc1d675247ea \ + --hash=sha256:e96bd42238ea8b477691c8a8f6e8bdbf9247415ad7892e6e885994c6940bcf74 + # via feast (pyproject.toml) +mlflow-skinny==3.2.0 \ + --hash=sha256:b359ec082a0a966e4e8e80f03d850da7fa677ebe57e67b1c0877029e5eeee443 \ + --hash=sha256:ec33a6fc164973e3b4d208e4ab8bec118ea93ff890ffbd08817b66468235ed71 + # via mlflow +mlflow-tracing==3.2.0 \ + --hash=sha256:4180d48b6b68a70b3e37987def3b0689d3f4ba722f5d2b98344c3717d2289b99 \ + --hash=sha256:6f3dd940752ca28871b09880e9426d1293460822faa8706b33af1d50c29a0355 + # via mlflow +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via + # feast (pyproject.toml) + # pyiceberg mock==2.0.0 \ --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba - # via feast (setup.py) + # via feast (pyproject.toml) moto==4.2.14 \ --hash=sha256:6d242dbbabe925bb385ddb6958449e5c827670b13b8e153ed63f91dbdb50372c \ --hash=sha256:8f9263ca70b646f091edcc93e97cda864a542e6d16ed04066b1370ed217bd190 - # via feast (setup.py) + # via feast (pyproject.toml) mpire[dill]==2.10.2 \ --hash=sha256:d627707f7a8d02aa4c7f7d59de399dec5290945ddf7fbd36cbb1d6ebb37a51fb \ --hash=sha256:f66a321e93fadff34585a4bfa05e95bd946cf714b442f51c529038eb45773d97 @@ -2454,9 +2961,9 @@ mpmath==1.3.0 \ --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c # via sympy -msal==1.34.0 \ - --hash=sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f \ - --hash=sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1 +msal==1.37.0 \ + --hash=sha256:1b1672a33ee467c1d70b341bb16cafd51bb3c817147a95b93263794b03971bec \ + --hash=sha256:dd17e95a7c71bce75e8108113438ba7c4a086b3bcad4f57a8c09b7af3d753c2d # via # azure-identity # msal-extensions @@ -2464,234 +2971,242 @@ msal-extensions==1.3.1 \ --hash=sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca \ --hash=sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4 # via azure-identity -msgpack==1.1.2 \ - --hash=sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2 \ - --hash=sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014 \ - --hash=sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931 \ - --hash=sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b \ - --hash=sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b \ - --hash=sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999 \ - --hash=sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029 \ - --hash=sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0 \ - --hash=sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9 \ - --hash=sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c \ - --hash=sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8 \ - --hash=sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f \ - --hash=sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a \ - --hash=sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42 \ - --hash=sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e \ - --hash=sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f \ - --hash=sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7 \ - --hash=sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb \ - --hash=sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef \ - --hash=sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf \ - --hash=sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245 \ - --hash=sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794 \ - --hash=sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af \ - --hash=sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff \ - --hash=sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e \ - --hash=sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296 \ - --hash=sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030 \ - --hash=sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833 \ - --hash=sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939 \ - --hash=sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa \ - --hash=sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90 \ - --hash=sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c \ - --hash=sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717 \ - --hash=sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406 \ - --hash=sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a \ - --hash=sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251 \ - --hash=sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2 \ - --hash=sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7 \ - --hash=sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e \ - --hash=sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b \ - --hash=sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844 \ - --hash=sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9 \ - --hash=sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87 \ - --hash=sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b \ - --hash=sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c \ - --hash=sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23 \ - --hash=sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c \ - --hash=sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e \ - --hash=sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620 \ - --hash=sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69 \ - --hash=sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f \ - --hash=sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68 \ - --hash=sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27 \ - --hash=sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46 \ - --hash=sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa \ - --hash=sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00 \ - --hash=sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9 \ - --hash=sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84 \ - --hash=sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e \ - --hash=sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20 \ - --hash=sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e \ - --hash=sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162 +msgpack==1.2.1 \ + --hash=sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833 \ + --hash=sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a \ + --hash=sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647 \ + --hash=sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d \ + --hash=sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064 \ + --hash=sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107 \ + --hash=sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac \ + --hash=sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720 \ + --hash=sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c \ + --hash=sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06 \ + --hash=sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895 \ + --hash=sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde \ + --hash=sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203 \ + --hash=sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc \ + --hash=sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74 \ + --hash=sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22 \ + --hash=sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8 \ + --hash=sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35 \ + --hash=sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb \ + --hash=sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9 \ + --hash=sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8 \ + --hash=sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6 \ + --hash=sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07 \ + --hash=sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056 \ + --hash=sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4 \ + --hash=sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7 \ + --hash=sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1 \ + --hash=sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24 \ + --hash=sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355 \ + --hash=sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0 \ + --hash=sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce \ + --hash=sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f \ + --hash=sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707 \ + --hash=sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66 \ + --hash=sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b \ + --hash=sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e \ + --hash=sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d \ + --hash=sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8 \ + --hash=sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7 \ + --hash=sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73 \ + --hash=sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402 \ + --hash=sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a \ + --hash=sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d \ + --hash=sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c \ + --hash=sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273 \ + --hash=sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b \ + --hash=sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d \ + --hash=sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb \ + --hash=sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4 \ + --hash=sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190 \ + --hash=sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5 \ + --hash=sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a \ + --hash=sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7 \ + --hash=sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1 \ + --hash=sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889 \ + --hash=sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c \ + --hash=sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155 \ + --hash=sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24 \ + --hash=sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6 \ + --hash=sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1 \ + --hash=sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d \ + --hash=sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7 \ + --hash=sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64 \ + --hash=sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2 \ + --hash=sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc \ + --hash=sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c # via ray -multidict==6.7.0 \ - --hash=sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32 \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ - --hash=sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff \ - --hash=sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8 \ - --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ - --hash=sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 \ - --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ - --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6 \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ - --hash=sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912 \ - --hash=sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c \ - --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6 \ - --hash=sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84 \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ - --hash=sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd \ - --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0 \ - --hash=sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e \ - --hash=sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 \ - --hash=sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc \ - --hash=sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7 \ - --hash=sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4 \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111 \ - --hash=sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e \ - --hash=sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84 \ - --hash=sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851 \ - --hash=sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb \ - --hash=sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ - --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5 \ - --hash=sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb \ - --hash=sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349 \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ - --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ - --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34 \ - --hash=sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85 \ - --hash=sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ - --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ - --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ - --hash=sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91 \ - --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40 \ - --hash=sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73 \ - --hash=sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4 \ - --hash=sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687 \ - --hash=sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via # aiobotocore # aiohttp # yarl -multiprocess==0.70.16 \ - --hash=sha256:0dfd078c306e08d46d7a8d06fb120313d87aa43af60d66da43ffff40b44d2f41 \ - --hash=sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1 \ - --hash=sha256:37b55f71c07e2d741374998c043b9520b626a8dddc8b3129222ca4f1a06ef67a \ - --hash=sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee \ - --hash=sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3 \ - --hash=sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435 \ - --hash=sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a \ - --hash=sha256:ba8c31889abf4511c7308a8c52bb4a30b9d590e7f58523302ba00237702ca054 \ - --hash=sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02 \ - --hash=sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec \ - --hash=sha256:e7b9d0f307cd9bd50851afaac0dba2cb6c44449efff697df7c7645f7d3f2be3a \ - --hash=sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e +multiprocess==0.70.17 \ + --hash=sha256:1d52f068357acd1e5bbc670b273ef8f81d57863235d9fbf9314751886e141968 \ + --hash=sha256:20c28ca19079a6c879258103a6d60b94d4ffe2d9da07dda93fb1c8bc6243f522 \ + --hash=sha256:27b8409c02b5dd89d336107c101dfbd1530a2cd4fd425fc27dcb7adb6e0b47bf \ + --hash=sha256:2818af14c52446b9617d1b0755fa70ca2f77c28b25ed97bdaa2c69a22c47b46c \ + --hash=sha256:2884701445d0177aec5bd5f6ee0df296773e4fb65b11903b94c613fb46cfb7d1 \ + --hash=sha256:2b12e081df87ab755190e227341b2c3b17ee6587e9c82fecddcbe6aa812cd7f7 \ + --hash=sha256:2ea0939b0f4760a16a548942c65c76ff5afd81fbf1083c56ae75e21faf92e426 \ + --hash=sha256:349525099a0c9ac5936f0488b5ee73199098dac3ac899d81d326d238f9fd3ccd \ + --hash=sha256:38357ca266b51a2e22841b755d9a91e4bb7b937979a54d411677111716c32744 \ + --hash=sha256:4ae2f11a3416809ebc9a48abfc8b14ecce0652a0944731a1493a3c1ba44ff57a \ + --hash=sha256:7ddb24e5bcdb64e90ec5543a1f05a39463068b6d3b804aa3f2a4e16ec28562d6 \ + --hash=sha256:a0f01cd9d079af7a8296f521dc03859d1a414d14c1e2b6e676ef789333421c95 \ + --hash=sha256:a22a6b1a482b80eab53078418bb0f7025e4f7d93cc8e1f36481477a023884861 \ + --hash=sha256:c2c82d0375baed8d8dd0d8c38eb87c5ae9c471f8e384ad203a36f095ee860f67 \ + --hash=sha256:c3feb874ba574fbccfb335980020c1ac631fbf2a3f7bee4e2042ede62558a021 \ + --hash=sha256:d729f55198a3579f6879766a6d9b72b42d4b320c0dcb7844afb774d75b573c62 # via # datasets # mpire @@ -2724,7 +3239,7 @@ mypy==1.11.2 \ --hash=sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b \ --hash=sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d # via - # feast (setup.py) + # feast (pyproject.toml) # sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ @@ -2733,14 +3248,14 @@ mypy-extensions==1.1.0 \ mypy-protobuf==3.3.0 \ --hash=sha256:15604f6943b16c05db646903261e3b3e775cf7f7990b7c37b03d043a907b650d \ --hash=sha256:24f3b0aecb06656e983f58e07c732a90577b9d7af3e1066fc2b663bbf0370248 - # via feast (setup.py) -nbclient==0.10.2 \ - --hash=sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d \ - --hash=sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193 + # via feast (pyproject.toml) +nbclient==0.11.0 \ + --hash=sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a \ + --hash=sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895 # via nbconvert -nbconvert==7.16.6 \ - --hash=sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b \ - --hash=sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582 +nbconvert==7.17.1 \ + --hash=sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2 \ + --hash=sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8 # via jupyter-server nbformat==5.10.4 \ --hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \ @@ -2750,9 +3265,9 @@ nbformat==5.10.4 \ # jupyter-server # nbclient # nbconvert -nest-asyncio==1.6.0 \ - --hash=sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe \ - --hash=sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c +nest-asyncio2==1.7.2 \ + --hash=sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8 \ + --hash=sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01 # via ipykernel networkx==3.4.2 \ --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \ @@ -2781,13 +3296,13 @@ ninja==1.13.0 \ --hash=sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa \ --hash=sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e # via easyocr -nodeenv==1.9.1 \ - --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ - --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 +nodeenv==1.10.0 \ + --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ + --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb # via pre-commit -notebook==7.4.7 \ - --hash=sha256:362b7c95527f7dd3c4c84d410b782872fd9c734fb2524c11dd92758527b6eda6 \ - --hash=sha256:3f0a04027dfcee8a876de48fba13ab77ec8c12f72f848a222ed7f5081b9e342a +notebook==7.6.1 \ + --hash=sha256:0b45fd1010668dd4808c40914d957706dbf044a677d28764dc881b74dfcede82 \ + --hash=sha256:6ea1e4c926f0dc490444ddcc335797a3dacda470a805d01031872fb119988ba2 # via great-expectations notebook-shim==0.2.4 \ --hash=sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef \ @@ -2852,9 +3367,10 @@ numpy==2.2.6 \ --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 # via - # feast (setup.py) + # feast (pyproject.toml) # accelerate # altair + # contourpy # dask # datasets # db-dtypes @@ -2864,15 +3380,18 @@ numpy==2.2.6 \ # great-expectations # ibis-framework # imageio + # matplotlib + # milvus-lite + # mlflow # opencv-python-headless # pandas # pandas-gbq - # pyarrow # qdrant-client # safetensors # scikit-image # scikit-learn # scipy + # sentence-transformers # shapely # tifffile # torchvision @@ -2880,116 +3399,178 @@ numpy==2.2.6 \ oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 - # via - # kubernetes - # requests-oauthlib -opencv-python-headless==4.12.0.88 \ - --hash=sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e \ - --hash=sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9 \ - --hash=sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154 \ - --hash=sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528 \ - --hash=sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0 \ - --hash=sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09 \ - --hash=sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd - # via - # docling-ibm-models - # easyocr + # via requests-oauthlib +opencv-python-headless==5.0.0.93 \ + --hash=sha256:030ca5e0837a2963ab36ef896baa9767eb8d2b83353fb28af5a521e40dd8756f \ + --hash=sha256:09a872a157c1376ab922a69bbf22f9a95bcc7b658a9d8b436a60212b02b2eeb4 \ + --hash=sha256:10818d91510e05c04568ae12b5cd120779c70c01bf897b001a6221fe430df80f \ + --hash=sha256:1e55af3abfb462eeeabe5c775f12bdb36216d8a93a3583d69e6bd6e1d6ba7d00 \ + --hash=sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e \ + --hash=sha256:840bd717c21e5c11cadadc022a823315ea417f961213d06b4df010e019eb16f4 \ + --hash=sha256:b82f9831daab90b725c7c1ee1b36cb5732c367096ac76d119e64e14eb70d5f3c \ + --hash=sha256:c6bcd96b185975ea240d22cfdb15a1f6d080cc95264cfbe2621f21bb144d89b9 \ + --hash=sha256:ed709fdf9aa0bd1f2ed8549e71d19449b03a675bb581eb292285f6861953be37 + # via easyocr +openlineage-python==1.52.0 \ + --hash=sha256:e1bd73b3066021eecf69cc5c3571f6fd7e253768ea941f2c76f18969ea0ecb20 + # via feast (pyproject.toml) openpyxl==3.1.5 \ --hash=sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2 \ --hash=sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050 # via docling -orjson==3.11.3 \ - --hash=sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904 \ - --hash=sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873 \ - --hash=sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d \ - --hash=sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710 \ - --hash=sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f \ - --hash=sha256:124d5ba71fee9c9902c4a7baa9425e663f7f0aecf73d31d54fe3dd357d62c1a7 \ - --hash=sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce \ - --hash=sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a \ - --hash=sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077 \ - --hash=sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b \ - --hash=sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f \ - --hash=sha256:215c595c792a87d4407cb72dd5e0f6ee8e694ceeb7f9102b533c5a9bf2a916bb \ - --hash=sha256:22724d80ee5a815a44fc76274bb7ba2e7464f5564aacb6ecddaa9970a83e3225 \ - --hash=sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae \ - --hash=sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7 \ - --hash=sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451 \ - --hash=sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55 \ - --hash=sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804 \ - --hash=sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca \ - --hash=sha256:3d721fee37380a44f9d9ce6c701b3960239f4fb3d5ceea7f31cbd43882edaa2f \ - --hash=sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f \ - --hash=sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667 \ - --hash=sha256:56afaf1e9b02302ba636151cfc49929c1bb66b98794291afd0e5f20fecaf757c \ - --hash=sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467 \ - --hash=sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a \ - --hash=sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27 \ - --hash=sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb \ - --hash=sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b \ - --hash=sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204 \ - --hash=sha256:73b92a5b69f31b1a58c0c7e31080aeaec49c6e01b9522e71ff38d08f15aa56de \ - --hash=sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167 \ - --hash=sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1 \ - --hash=sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee \ - --hash=sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f \ - --hash=sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d \ - --hash=sha256:8ab962931015f170b97a3dd7bd933399c1bae8ed8ad0fb2a7151a5654b6941c7 \ - --hash=sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a \ - --hash=sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b \ - --hash=sha256:8e531abd745f51f8035e207e75e049553a86823d189a51809c078412cefb399a \ - --hash=sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc \ - --hash=sha256:913f629adef31d2d350d41c051ce7e33cf0fd06a5d1cb28d49b1899b23b903aa \ - --hash=sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770 \ - --hash=sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120 \ - --hash=sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2 \ - --hash=sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f \ - --hash=sha256:9d8787bdfbb65a85ea76d0e96a3b1bed7bf0fbcb16d40408dc1172ad784a49d2 \ - --hash=sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc \ - --hash=sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43 \ - --hash=sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872 \ - --hash=sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e \ - --hash=sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be \ - --hash=sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810 \ - --hash=sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6 \ - --hash=sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2 \ - --hash=sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91 \ - --hash=sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc \ - --hash=sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424 \ - --hash=sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e \ - --hash=sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23 \ - --hash=sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1 \ - --hash=sha256:c5189a5dab8b0312eadaf9d58d3049b6a52c454256493a557405e77a3d67ab7f \ - --hash=sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d \ - --hash=sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4 \ - --hash=sha256:d2489b241c19582b3f1430cc5d732caefc1aaf378d97e7fb95b9e56bed11725f \ - --hash=sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229 \ - --hash=sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d \ - --hash=sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf \ - --hash=sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4 \ - --hash=sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038 \ - --hash=sha256:e0a23b41f8f98b4e61150a03f83e4f0d566880fe53519d445a962929a4d21045 \ - --hash=sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633 \ - --hash=sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064 \ - --hash=sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc \ - --hash=sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049 \ - --hash=sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b \ - --hash=sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824 \ - --hash=sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c \ - --hash=sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6 \ - --hash=sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2 \ - --hash=sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e \ - --hash=sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1 \ - --hash=sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569 \ - --hash=sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c - # via trino +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef + # via + # mcp + # mlflow-skinny + # mlflow-tracing + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-sdk==1.44.0 \ + --hash=sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b \ + --hash=sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad + # via + # mlflow-skinny + # mlflow-tracing +opentelemetry-semantic-conventions==0.65b0 \ + --hash=sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb \ + --hash=sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60 + # via opentelemetry-sdk +oracledb==4.0.2 \ + --hash=sha256:0101b88c15ae628af556506840ae876e12bdb75506ef342e4990ac1758bad1eb \ + --hash=sha256:04fd67692b3cdea2ac7135e6f87c82ce9035f3caa0891a26710a4c654f1e375e \ + --hash=sha256:087fdf5bc36b03dc3c55f7abc944a163ba8edc9c802bc8baf91698a52041ee13 \ + --hash=sha256:0a0be8111db80bb697ec66a34688a6985589e3f72789afe28e3b162b2c4cdd2b \ + --hash=sha256:0a380ab72853487ea2764c5df772f35026b4219868fc3eba68e193c9aea230ac \ + --hash=sha256:14124b5e8e4ba087f105ee389ee43e407840220a0ba2504de68eed903f51a711 \ + --hash=sha256:1b89c02670bafc9b1fcc0d15175f097bdc1968bc36ee4d66aa63aee09bfbbe30 \ + --hash=sha256:1d8933cfb94d9e947cb88dfc87bb2696120b388b89db14394133990585277fb3 \ + --hash=sha256:33bcb765bf97056764c38481dc1dd959e4b61f3a240baacc645d7f06038ce225 \ + --hash=sha256:36e72ffaaf589041dac72ddf589d694c0a9ac4576662a04628966d4ea089d6ea \ + --hash=sha256:3bde3d507d8f50faa9480e99222fe5fa04dc0db2cb8b57746092022ac69ae93d \ + --hash=sha256:41bb34ba8fe76019a958f6d0beba2baf9169ede0bf94e1a22f05f3ca7676f8d7 \ + --hash=sha256:543a7398ebd778d40a839b76a331afa93f506ac143691a6de59ee31ae2e1c4aa \ + --hash=sha256:567c093138d32f06512572de5b5d52459bea82b4e47098e9d668dae759965ccb \ + --hash=sha256:579f2c568433523a990cde5bea73c980d144754dc54d3ab2cd37efd670dc31d6 \ + --hash=sha256:58e71a3c1e294a99e39b086adf42074d48287548bf8edfa720c9507106186bbb \ + --hash=sha256:5c1f13ba535d9f0fc61e1987720988612cf9424667d491fc344ba681f3336a17 \ + --hash=sha256:5fe6e07ed29f84a6656e0580b4e662109d3bb90fc13d8f98ae3a56a67c75b80e \ + --hash=sha256:60cd8a6045674ab481ff33b0bed448f656e7d9abd7fc88c8f349abda5351aad7 \ + --hash=sha256:60f4a4847024f42e8b3be3c7d2597d682ef24f9342b5ebf1695a1bef2aa5be96 \ + --hash=sha256:6444be4991f33754cd98f624cecef3eb98db3e87f8ac14e9b65bd3591c9dd252 \ + --hash=sha256:6c8f3e34a077d7d2d39aa25c28b1aa53fd4fd6aa2437e4d48d9f18b2b28b95e4 \ + --hash=sha256:6d4effb098af3dbcc1fcf785b304523afee1b9a311a8bd9c62bd3b4669198970 \ + --hash=sha256:756db3529dc0f88251f9cbc349cc9c144af2424aae12c1ce24be2fbd9c1b1610 \ + --hash=sha256:78d4a1cc1482b5927a9962c7766aeeae616a85eedbe49af8b797148ac1c1ac32 \ + --hash=sha256:7d023f02f678937bcb6acce7c976569233d193e00c918d7e7b68580db1f68f45 \ + --hash=sha256:864ad6a838e8d41031f117052ee2fac85453b6ecd483d1da6e8fd87701807e5e \ + --hash=sha256:8a7a45fb1042f7258dfbd0b58044a6743c2e8112d0ef5ef9064e17a52968520f \ + --hash=sha256:93b4d2cd17a93224711fadd28f77e956e5c6575cd923f24bfe0b1a2a581beb79 \ + --hash=sha256:9d11723018b6aeae035f4e1feae4150b7cca1161023c2d68d957d7310fe0dd56 \ + --hash=sha256:a1f46b01a089e0e0dd44c4ac4c5c79903760976346c74a698955c1fb76d68bf5 \ + --hash=sha256:a45a13e33509db5bd3622a05cb2e4a6cb41d56d1bfd2a32caeacd26f4ce8b988 \ + --hash=sha256:a814307ca8a6bf0649d8bf0a650a72f23030d9af052a2d4a481b94b6ab50d5a4 \ + --hash=sha256:af4121112f0b68e8d61ce46a086c6aec8256fa2ca32d2e6467195a6c575c207b \ + --hash=sha256:b14d52c09b3c7a8273b3640ba06675acec5000dab0356ef288587c46c30bbbb4 \ + --hash=sha256:b4f2b51837248d4acfb323a64c48d89b62467b0f73d1211c99db9b80df0cdfbc \ + --hash=sha256:b5d203426f0d191842b4cfd87c223163ccc57f7e7875ec7ed073f163de2229af \ + --hash=sha256:b82d3d83bdc6246e53f8ad8b598d2508e9f5d983e352ce2e8a71a020200ba2d6 \ + --hash=sha256:c193d2636f9c2e8549d24cc53947a8381c7b9488e0dae9cea5cf5bce30dc7313 \ + --hash=sha256:cb70b231254ad82ff0639b8688e0e528100a984df65fcd7624bae88fb930e700 \ + --hash=sha256:cbe3a75b463a334da9b3d76026062bcfb24ec563b7df291f700c9f7eefcbeefe \ + --hash=sha256:d5a82a449d96fb89a93346d88128ad618526ffa290376d4b57fbcee1eedcd19f \ + --hash=sha256:d8a83c120d8177f344bedf7698bc1af5f6fd031851031d3e20ced632286e3e84 \ + --hash=sha256:db0d76199b6dffd721bda48dad4eeea6603942ce2e246f3b9301d85af6bda0ee \ + --hash=sha256:f5ed684ab419603d53981e0f32b51da673e21a42d7dce1bf3f215a9301c7c108 \ + --hash=sha256:f8ac435434193a1d88fcda6bfd9387c5562240d4f2e8de1f9ed20c64d5f0ff90 \ + --hash=sha256:ff8a239de950acb600ab4fbe1b349fb085a75383bc9a163e50cab362293fc936 + # via ibis-framework +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via + # pymilvus + # trino overrides==7.7.0 \ --hash=sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a \ --hash=sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49 # via jupyter-server -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f +packaging==25.0 \ + --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ + --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via # accelerate # build @@ -2997,14 +3578,12 @@ packaging==24.2 \ # datasets # db-dtypes # deprecation - # dunamai # faiss-cpu # google-cloud-bigquery # great-expectations # gunicorn # huggingface-hub # ibis-framework - # ibis-substrait # ipykernel # jupyter-events # jupyter-server @@ -3012,7 +3591,11 @@ packaging==24.2 \ # jupyterlab-server # lazy-loader # marshmallow + # matplotlib + # mlflow-skinny + # mlflow-tracing # nbconvert + # openlineage-python # pandas-gbq # pytest # ray @@ -3020,6 +3603,7 @@ packaging==24.2 \ # snowflake-connector-python # sphinx # transformers + # wheel pandas==2.3.3 \ --hash=sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7 \ --hash=sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593 \ @@ -3077,7 +3661,7 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # altair # dask # datasets @@ -3087,24 +3671,25 @@ pandas==2.3.3 \ # google-cloud-bigquery # great-expectations # ibis-framework + # mlflow # pandas-gbq # pymilvus # snowflake-connector-python -pandas-gbq==0.29.2 \ - --hash=sha256:99f6b642bd8340e96c2acdf7d717108aa0d31c703c2f284817ba7dc3b16110b2 \ - --hash=sha256:a3a28e7de4f8456fc4b8e9b7472b9bc470d2566f89c4cd997599e504f5a0a706 +pandas-gbq==0.30.0 \ + --hash=sha256:8fe811786e4ad2e0d4608e897534207d9fbe768ab3168f766a99f0cb4cd5ed20 \ + --hash=sha256:d9b4454b17aee3c23ef1dfcfd91df6e2b77f1e69e1e4b28467701cd75850664a # via google-cloud-bigquery pandocfilters==1.5.1 \ --hash=sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e \ --hash=sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc # via nbconvert -parsimonious==0.10.0 \ - --hash=sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c \ - --hash=sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f +parsimonious==0.11.0 \ + --hash=sha256:32e3818abf9f05b3b9f3b6d87d128645e30177e91f614d2277d88a0aea98fae2 \ + --hash=sha256:e080377d98957beec053580d38ae54fcdf7c470fb78670ba4bf8b5f9d5cad2a9 # via singlestoredb -parso==0.8.5 \ - --hash=sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a \ - --hash=sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887 +parso==0.8.7 \ + --hash=sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c \ + --hash=sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1 # via jedi parsy==2.2 \ --hash=sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 \ @@ -3114,9 +3699,9 @@ partd==1.4.2 \ --hash=sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f \ --hash=sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c # via dask -pbr==7.0.1 \ - --hash=sha256:32df5156fbeccb6f8a858d1ebc4e465dcf47d6cc7a4895d5df9aa951c712fc35 \ - --hash=sha256:3ecbcb11d2b8551588ec816b3756b1eb4394186c3b689b17e04850dfc20f7e57 +pbr==7.0.3 \ + --hash=sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29 \ + --hash=sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b # via mock pexpect==4.9.0 \ --hash=sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 \ @@ -3230,24 +3815,25 @@ pillow==11.3.0 \ --hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \ --hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3 # via - # feast (setup.py) + # feast (pyproject.toml) # docling # docling-core # docling-ibm-models # docling-parse # easyocr # imageio + # matplotlib # python-pptx # scikit-image # torchvision -pip==25.2 \ - --hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \ - --hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717 +pip==26.1.2 \ + --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ + --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 # via pip-tools -pip-tools==7.5.1 \ - --hash=sha256:a051a94794ba52df9acad2d7c9b0b09ae001617db458a543f8287fea7b89c2cf \ - --hash=sha256:f5ff803823529edc0e6e40c86b1aa7da7266fb1078093c8beea4e5b77877036a - # via feast (setup.py) +pip-tools==7.6.0 \ + --hash=sha256:4bd99155b6d8de358a214b0865e1a2855a453570c1a83d40f7b564870b8657be \ + --hash=sha256:c1c59f7844df4866fa9542d3f50d1f44be537ac0027cb50b2563d6a992853981 + # via feast (pyproject.toml) platformdirs==3.11.0 \ --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e @@ -3266,14 +3852,6 @@ ply==3.11 \ --hash=sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3 \ --hash=sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce # via thriftpy2 -poetry-core==1.9.1 \ - --hash=sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0 \ - --hash=sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8 - # via feast (setup.py) -poetry-dynamic-versioning==1.9.1 \ - --hash=sha256:65a0c814e6d30d4807734a3c34edf261fd7cc3b340dbd23b6a33ee41f7d0b547 \ - --hash=sha256:d6e7b9df817aa2ca4946cd695c6c89e1379d2e6c640f008a9b6170d081a9da48 - # via feast (setup.py) portalocker==3.2.0 \ --hash=sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac \ --hash=sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968 @@ -3281,165 +3859,164 @@ portalocker==3.2.0 \ pre-commit==3.3.1 \ --hash=sha256:218e9e3f7f7f3271ebc355a15598a4d3893ad9fc7b57fe446db75644543323b9 \ --hash=sha256:733f78c9a056cdd169baa6cd4272d51ecfda95346ef8a89bf93712706021b907 - # via feast (setup.py) -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 + # via feast (pyproject.toml) +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 # via - # feast (setup.py) + # feast (pyproject.toml) # jupyter-server -prompt-toolkit==3.0.52 \ - --hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \ - --hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 +prompt-toolkit==3.0.53 \ + --hash=sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2 \ + --hash=sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6 # via ipython -propcache==0.4.1 \ - --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ - --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ - --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ - --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ - --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ - --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ - --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ - --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ - --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ - --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274 \ + --hash=sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.26.1 \ - --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ - --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +proto-plus==1.28.2 \ + --hash=sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501 \ + --hash=sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52 # via # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==4.25.8 \ - --hash=sha256:077ff8badf2acf8bc474406706ad890466274191a48d0abd3bd6987107c9cde5 \ - --hash=sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59 \ - --hash=sha256:27d498ffd1f21fb81d987a041c32d07857d1d107909f5134ba3350e1ce80a4af \ - --hash=sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0 \ - --hash=sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd \ - --hash=sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0 \ - --hash=sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7 \ - --hash=sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9 \ - --hash=sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f \ - --hash=sha256:d552c53d0415449c8d17ced5c341caba0d89dbf433698e1436c8fa0aae7808a3 \ - --hash=sha256:f4510b93a3bec6eba8fd8f1093e9d7fb0d4a24d1a81377c10c0e5bbfe9e4ed24 - # via - # feast (setup.py) +protobuf==6.33.6 \ + --hash=sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326 \ + --hash=sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901 \ + --hash=sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3 \ + --hash=sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a \ + --hash=sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135 \ + --hash=sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e \ + --hash=sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3 \ + --hash=sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2 \ + --hash=sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593 \ + --hash=sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf + # via + # feast (pyproject.toml) + # databricks-sdk # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -3451,13 +4028,13 @@ protobuf==4.25.8 \ # grpcio-status # grpcio-testing # grpcio-tools - # ikvpy + # mlflow-skinny + # mlflow-tracing # mypy-protobuf # proto-plus # pymilvus # qdrant-client # ray - # substrait psutil==5.9.0 \ --hash=sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5 \ --hash=sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a \ @@ -3492,83 +4069,73 @@ psutil==5.9.0 \ --hash=sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c \ --hash=sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3 # via - # feast (setup.py) + # feast (pyproject.toml) # accelerate # ipykernel -psycopg[binary, pool]==3.2.5 \ - --hash=sha256:b782130983e5b3de30b4c529623d3687033b4dafa05bb661fc6bf45837ca5879 \ - --hash=sha256:f5f750611c67cb200e85b408882f29265c66d1de7f813add4f8125978bfd70e8 - # via feast (setup.py) -psycopg-binary==3.2.5 \ - --hash=sha256:02fb96091e2fb3ea1470b113fef08953baaedbca1d39a3f72d82cb615177846c \ - --hash=sha256:11e3ed8b94c750d54fc3e4502dd930fb0fd041629845b6a7ce089873ac9756b0 \ - --hash=sha256:1494827c43265820d5dcdc6f8086521bc7dd04b9da8831310978a788cdcd2e62 \ - --hash=sha256:21b839f9bfd77ed074f7f71464a43f453400c57d038a0ba0716329a28e335897 \ - --hash=sha256:23a1dc61abb8f7cc702472ab29554167a9421842f976c201ceb3b722c0299769 \ - --hash=sha256:274e852f9e61252bc8e80a0a43d300ba352d40219e856733054023a3bb960eb4 \ - --hash=sha256:28bd5cb2324567e5e70f07fe1d646398d6b0e210e28b49be0e69593590a59980 \ - --hash=sha256:2b053eae21dd3a6828b516a1171e1274d1af5f7c07d2d9a8f597f2e19c732168 \ - --hash=sha256:2cbb8649cfdacbd14e17f5ab78edc52d33350013888518c73e90c5d17d7bea55 \ - --hash=sha256:2cc86657c05e09c701e97f87132cd58e0d55381dd568520081ac1fe7580a9bbb \ - --hash=sha256:2d10ce4c39eb9631381a0c3792727946a4391e843625a7ee9579ac6bb11495a5 \ - --hash=sha256:2d22a15e45f43d36ed35aed4d5261f8ef6ab7d9b84ee075576ca56ae03b9e0aa \ - --hash=sha256:2dbaf32c18c0d11c4480016b89c9c5cadb7b64c55de7f181d222b189bd13a558 \ - --hash=sha256:32b5673736f04c36ccbf8012800fe5bc01b46dac22c5d59e41b043bebaad9d3d \ - --hash=sha256:375149006e21d58ed8aba640e0295d8e636043064c433af94eb58057f9b96877 \ - --hash=sha256:393ab353196d364858b47317d27804ecc58ab56dbde32217bd67f0f2f2980662 \ - --hash=sha256:39e2cd10bf15442d95c3f48376b25dc33360418ea6c3c05884d8bf42407768c0 \ - --hash=sha256:3d2e57a1d06f3968e49e948ba374f21a7d8dcf44f37d582a4aeddeb7c85ce239 \ - --hash=sha256:3eb71cfc35116e4a8e336b7e785f1fe06ca23b4516a48ea91facd577d1a1fdf6 \ - --hash=sha256:3f893c0ed3d5c7b83b76b1f8f7d3ca5a03e38bcd3cab5d65b5c25a0d1064aca4 \ - --hash=sha256:473f6827cf1faf3924eb77146d1e85126a1b5e48a88053b8d8b78dd29e971d78 \ - --hash=sha256:48f97936145cb7de18b95d85670b2d3e2c257277263272be05815b74fb0ef195 \ - --hash=sha256:48fcb12a0a72fdfe4102bdb1252a7366e8d73a2c89fe6ce5923be890de367c2f \ - --hash=sha256:4914dc60f2fddf0884464985e31d775aa865b665471fa156ec2f56fa72a1a097 \ - --hash=sha256:51a96d9fe51f718912b4a0089784f1f32d800217499fd0f0095b888506aba4c5 \ - --hash=sha256:5244bebaa9734a236b7157fb57c065b6c0f2344281916187bd73f951df1899e0 \ - --hash=sha256:5b81342e139ddccfa417832089cd213bd4beacd7a1462ca4019cafe71682d177 \ - --hash=sha256:5d2253189aa4cca0a425e2ca896d1a29760cd3a2b10ab12194e4e827a566505c \ - --hash=sha256:5fd017d7ed71c58f19b0f614e7bfb8f01ec862bacb67ae584f494d090956102e \ - --hash=sha256:605f70e267222d567fc40de7813ee3fb29f8145a1a20aa6fd3dc62baba9312f1 \ - --hash=sha256:60d0f36a42a822e43c4c7472df8a0c980c0f32e5d74ed871333c423a4e942f11 \ - --hash=sha256:62965045cc0fe3dc5dd55d39779620b225ef75962825c7b1b533033cb91810bd \ - --hash=sha256:65162a9cc3f86d70b1d895dbda506e3c079f80d082eb41c54d3f6d33a00b3965 \ - --hash=sha256:659f2c675d478b1bc01b95a8d3ded74fa939b370e71ffbecd496f617b215eb05 \ - --hash=sha256:6b581da13126b8715c0c0585cd37ce934c9864d44b2a4019f5487c0b943275e6 \ - --hash=sha256:71d82dbc7c6c7f5746468e7992e5483aa45b12250d78d220a2431ab88795825c \ - --hash=sha256:7376b13504396da9678b646f5338462347da01286b2a688a0d8493ec764683a2 \ - --hash=sha256:7623659d44a6aa032be4a066c658ba45009d768c2481526fbef7c609702af116 \ - --hash=sha256:7a94020821723a6a210206ddb458001f3ed27e1e6a0555b9422bebf7ead8ff37 \ - --hash=sha256:7d5f1bfc848a94e0d63fe693adee4f88bd9e5c415ecb4c9c17d2d44eba6795a6 \ - --hash=sha256:7efe6c732fd2d7e22d72dc4f7cf9b644020adacfff61b0a8a151343da8e661c0 \ - --hash=sha256:8a602d9fdb567cca090ca19ac3ebf10219065be2a4f8cf9eb8356cffb5a7ab1d \ - --hash=sha256:8cd9ebf335262e864d740f9dad3f672f61162cc0d4825a5eb5cf50df334a688f \ - --hash=sha256:8e6f2bef5aed021fbdf46323d3cd8847bf960efb56394698644a8ee2306f8892 \ - --hash=sha256:93221d5a759bd39b1face1d7d887d2b9ede3e55aefaff8eacf1b663ccdcd204b \ - --hash=sha256:9639289b72f9339721982e156527c296693236d6192ccc31412ab36fccd1683c \ - --hash=sha256:98efaedf2bf79f4d563ca039a57a025b72847bd80568f54709cc39fc1404772c \ - --hash=sha256:9abe093a303e25ac58774a11241150e2fe2947358d1ca12521ad03c90b131060 \ - --hash=sha256:a4321ee8180982d70458d3e8378e31448901bf0ee40fe0d410a87413578f4098 \ - --hash=sha256:a82211a43372cba9b1555a110e84e679deec2dc9463ae4c736977dad99dca5ed \ - --hash=sha256:a91b0e096fdfeb52d86bb8f5ee25dc22483d6960af9b968e6b381a8ec5bfbf82 \ - --hash=sha256:b5e0acbc991472188c9df40eb56d8a97ad3ad00d4de560b8b74bdc2d94041a8f \ - --hash=sha256:b6b5a4542aca4095ab35e184517cb0d18895ba4b6661c92865b431fa7b7974d8 \ - --hash=sha256:ba4a610882171bdaae0779f14e0ff45f3ee271fd2dbf16cdadfc81bd67323232 \ - --hash=sha256:bc5bd9bf5f5894923b78a41c5becd52d6bced1e1e43744855bd85cb341376ca6 \ - --hash=sha256:c37eb3be7a6be93f4925ccf52bbfa60244da6c63201770a709dd81a3d2d08534 \ - --hash=sha256:c3c5fa3d4fa0a651cefab391b783f89bc5e331afa0a4e93c9b16141993fa05c8 \ - --hash=sha256:ca5e36a3e7480a5c09aed99ecdb8e6554b21485c3b064297fe77f7b1b5806106 \ - --hash=sha256:d4e0c1b1aa5283f6d9a384ffc7a8400d25386bb98fdb9bddae446e4ef4da7366 \ - --hash=sha256:dc8bc40d82d1ee8dec136e10707c7f3147a6322fd8014e174a0f3446fb793649 \ - --hash=sha256:de576c49d7deab2b78088feb24e1f6ae3e16a0020e8496cdd3b8543f5e350e87 \ - --hash=sha256:e7d215a43343d91ba08301865f059d9518818d66a222a85fb425e4156716f5a6 \ - --hash=sha256:eb8293d66c6a4ddc72fceb7ad0e111cb196cc394954ae0f9b63c251d97f1b00e \ - --hash=sha256:ee6d8f489a9b116ea8dc797664a50671585a4ca20573359f067858e1231cc217 \ - --hash=sha256:efb878d08dd49d7d9d18512e791b418a1171d08f935475eec98305f0886b7c14 +psycopg[binary, pool]==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via feast (pyproject.toml) +psycopg-binary==3.3.4 \ + --hash=sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070 \ + --hash=sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c \ + --hash=sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc \ + --hash=sha256:13a7f380824c35896dcac7fe0f61440f7ca49d6dc73f3c13a9a4471e6a3b302e \ + --hash=sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68 \ + --hash=sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae \ + --hash=sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829 \ + --hash=sha256:22cdbf5f91ef7bb91fe0c5757e1962d3127a8010256eefd9c61fcaf441802097 \ + --hash=sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c \ + --hash=sha256:276904e3452d6a23d474ef9a21eee19f20eed3d53ddd2576af033827e0ba0992 \ + --hash=sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97 \ + --hash=sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6 \ + --hash=sha256:32a6fbf8481e3a370d0d72b860d35948a693cb01281da217f7b2f307636e591a \ + --hash=sha256:41f2ec0fea529832982bcb6c9415de3c86264ebe562b77a467c0fbcd7efbba8d \ + --hash=sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228 \ + --hash=sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e \ + --hash=sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4 \ + --hash=sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41 \ + --hash=sha256:574ea21a9651958f1535c5a1c649c7409e9168bcbffa29a3f2f961f58b322949 \ + --hash=sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9 \ + --hash=sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089 \ + --hash=sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf \ + --hash=sha256:612a627d733f695b1de1f9b4bd511c15f999a5d8b915d444bbd7dd71cf3370da \ + --hash=sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578 \ + --hash=sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014 \ + --hash=sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e \ + --hash=sha256:7465bfe6087d2d5b42d4c53b9b11ca9f218e477317a4a162a10e3c19e984ba8e \ + --hash=sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31 \ + --hash=sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652 \ + --hash=sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b \ + --hash=sha256:7f7668f30b9dd5163197e5cbf4e0efd54e00f0a859cc566ce56cfc31f4054839 \ + --hash=sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277 \ + --hash=sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7 \ + --hash=sha256:ab8cca8ef8fb1ccf5b048ae5bd78ba55b9e4b5d472e3ce5ca39ff4d2a9c249e4 \ + --hash=sha256:ad3bc94054876155549fdaedf4a46d1ec69d39a5bcee377148afe498e84c4b8e \ + --hash=sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70 \ + --hash=sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf \ + --hash=sha256:b7bfff1ca23732b488cbca3076fc11bc98d520ee122514fdb17a8e20d3338f5a \ + --hash=sha256:bdef84570ebbce1d42b4e7ea952d21c414c5f118ad02fee00c5625f35e134429 \ + --hash=sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8 \ + --hash=sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3 \ + --hash=sha256:cf7f73a4a792bc5db58a4b385d8a1467e8d468f7548702fb0ed1e9b7501b1c13 \ + --hash=sha256:cffc3408d77a27973f33e5d909b624cce683db5fc25964b02fe0aae7886c1007 \ + --hash=sha256:d7b4d40c153fa352ab3cca530f3a0baedf7621b2ebcbd7f084009522c21788fc \ + --hash=sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38 \ + --hash=sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9 \ + --hash=sha256:e2631da29253a98bd496e6c4813b24e09a4fe3fb2a9e88513305d6f8747cce95 \ + --hash=sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d \ + --hash=sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16 \ + --hash=sha256:eb4eed2079c01a4850bf467deacfab56d356d4225040170af03dc9958321242d \ + --hash=sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260 \ + --hash=sha256:f80e3f2b5331dbbf0901bcb658056c03eeb2c1ef31d774afb0d61598b242e744 \ + --hash=sha256:f9b1c2533af01cd7648378599f82b0b8ae32f293296e6eec5753a625bc97ef28 \ + --hash=sha256:fa1cbc10768a796c96d3243656016bf4e337c81c71097270bb7b0ad6210d9765 \ + --hash=sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7 # via psycopg -psycopg-pool==3.2.6 \ - --hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \ - --hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7 +psycopg-pool==3.3.1 \ + --hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \ + --hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c # via psycopg ptyprocess==0.7.0 \ --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \ @@ -3583,7 +4150,7 @@ pure-eval==0.2.3 \ py==1.11.0 \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via feast (setup.py) + # via feast (pyproject.toml) py-cpuinfo==9.0.0 \ --hash=sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690 \ --hash=sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5 @@ -3592,63 +4159,70 @@ py4j==0.10.9.9 \ --hash=sha256:c7c26e4158defb37b0bb124933163641a2ff6e3a3913f7811b0ddbe07ed61533 \ --hash=sha256:f694cad19efa5bd1dee4f3e5270eb406613c974394035e5bfc4ec1aba870b879 # via pyspark -pyarrow==17.0.0 \ - --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ - --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ - --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ - --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ - --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ - --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ - --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ - --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ - --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ - --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ - --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ - --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ - --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ - --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ - --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ - --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ - --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ - --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ - --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ - --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ - --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ - --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ - --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ - --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ - --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ - --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ - --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ - --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ - --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ - --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ - --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ - --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ - --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ - --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ - --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ - --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 - # via - # feast (setup.py) +pyarrow==21.0.0 \ + --hash=sha256:067c66ca29aaedae08218569a114e413b26e742171f526e828e1064fcdec13f4 \ + --hash=sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623 \ + --hash=sha256:0c4e75d13eb76295a49e0ea056eb18dbd87d81450bfeb8afa19a7e5a75ae2ad7 \ + --hash=sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636 \ + --hash=sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7 \ + --hash=sha256:203003786c9fd253ebcafa44b03c06983c9c8d06c3145e37f1b76a1f317aeae1 \ + --hash=sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10 \ + --hash=sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51 \ + --hash=sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd \ + --hash=sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8 \ + --hash=sha256:3b4d97e297741796fead24867a8dabf86c87e4584ccc03167e4a811f50fdf74d \ + --hash=sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569 \ + --hash=sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e \ + --hash=sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc \ + --hash=sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6 \ + --hash=sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c \ + --hash=sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82 \ + --hash=sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79 \ + --hash=sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6 \ + --hash=sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10 \ + --hash=sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61 \ + --hash=sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d \ + --hash=sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb \ + --hash=sha256:898afce396b80fdda05e3086b4256f8677c671f7b1d27a6976fa011d3fd0a86e \ + --hash=sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e \ + --hash=sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594 \ + --hash=sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634 \ + --hash=sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da \ + --hash=sha256:a7f6524e3747e35f80744537c78e7302cd41deee8baa668d56d55f77d9c464b3 \ + --hash=sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876 \ + --hash=sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e \ + --hash=sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a \ + --hash=sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b \ + --hash=sha256:cdc4c17afda4dab2a9c0b79148a43a7f4e1094916b3e18d8975bfd6d6d52241f \ + --hash=sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18 \ + --hash=sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe \ + --hash=sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99 \ + --hash=sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26 \ + --hash=sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d \ + --hash=sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a \ + --hash=sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd \ + --hash=sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503 \ + --hash=sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79 + # via + # feast (pyproject.toml) # dask # datasets # db-dtypes # deltalake # google-cloud-bigquery # ibis-framework + # milvus-lite + # mlflow # pandas-gbq # snowflake-connector-python pyarrow-hotfix==0.7 \ --hash=sha256:3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 \ --hash=sha256:59399cd58bdd978b2e42816a4183a55c6472d4e33d183351b6069f11ed42661d # via ibis-framework -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 @@ -3656,62 +4230,48 @@ pyasn1-modules==0.4.2 \ pybindgen==0.22.0 \ --hash=sha256:21612f4806a2af25a175716d7e694563af7e10c704538a350cb595d187952f6f \ --hash=sha256:5b4837d3138ac56863d93fe462f1dac39fb87bd50898e0da4c57fefd645437ac - # via feast (setup.py) -pyclipper==1.3.0.post6 \ - --hash=sha256:04214d23cf79f4ddcde36e299dea9f23f07abb88fa47ef399bf0e819438bbefd \ - --hash=sha256:106b8622cd9fb07d80cbf9b1d752334c55839203bae962376a8c59087788af26 \ - --hash=sha256:16cc1705a915896d2aff52131c427df02265631279eac849ebda766432714cc0 \ - --hash=sha256:188fbfd1d30d02247f92c25ce856f5f3c75d841251f43367dbcf10935bc48f38 \ - --hash=sha256:1c03f1ae43b18ee07730c3c774cc3cf88a10c12a4b097239b33365ec24a0a14a \ - --hash=sha256:1fd56855ca92fa7eb0d8a71cf3a24b80b9724c8adcc89b385bbaa8924e620156 \ - --hash=sha256:2737df106b8487103916147fe30f887aff439d9f2bd2f67c9d9b5c13eac88ccf \ - --hash=sha256:28bb590ae79e6beb15794eaee12b6f1d769589572d33e494faf5aa3b1f31b9fa \ - --hash=sha256:2e257009030815853528ba4b2ef7fb7e172683a3f4255a63f00bde34cfab8b58 \ - --hash=sha256:32cd7fb9c1c893eb87f82a072dbb5e26224ea7cebbad9dc306d67e1ac62dd229 \ - --hash=sha256:33ab72260f144693e1f7735e93276c3031e1ed243a207eff1f8b98c7162ba22c \ - --hash=sha256:3404dfcb3415eee863564b5f49be28a8c7fb99ad5e31c986bcc33c8d47d97df7 \ - --hash=sha256:383f3433b968f2e4b0843f338c1f63b85392b6e1d936de722e8c5d4f577dbff5 \ - --hash=sha256:3d58202de8b8da4d1559afbda4e90a8c260a5373672b6d7bc5448c4614385144 \ - --hash=sha256:3e5e65176506da6335f6cbab497ae1a29772064467fa69f66de6bab4b6304d34 \ - --hash=sha256:42bff0102fa7a7f2abdd795a2594654d62b786d0c6cd67b72d469114fdeb608c \ - --hash=sha256:47a214f201ff930595a30649c2a063f78baa3a8f52e1f38da19f7930c90ed80c \ - --hash=sha256:48dd55fbd55f63902cad511432ec332368cbbbc1dd2110c0c6c1e9edd735713a \ - --hash=sha256:491ec1bfd2ee3013269c2b652dde14a85539480e0fb82f89bb12198fa59fff82 \ - --hash=sha256:58eae2ff92a8cae1331568df076c4c5775bf946afab0068b217f0cf8e188eb3c \ - --hash=sha256:5c9c80b5c46eef38ba3f12dd818dc87f5f2a0853ba914b6f91b133232315f526 \ - --hash=sha256:6363b9d79ba1b5d8f32d1623e797c1e9f994600943402e68d5266067bdde173e \ - --hash=sha256:640f20975727994d4abacd07396f564e9e5665ba5cb66ceb36b300c281f84fa4 \ - --hash=sha256:6893f9b701f3132d86018594d99b724200b937a3a3ddfe1be0432c4ff0284e6e \ - --hash=sha256:793b0aa54b914257aa7dc76b793dd4dcfb3c84011d48df7e41ba02b571616eaf \ - --hash=sha256:851b3e58106c62a5534a1201295fe20c21714dee2eda68081b37ddb0367e6caa \ - --hash=sha256:903176952a159c4195b8be55e597978e24804c838c7a9b12024c39704d341f72 \ - --hash=sha256:9699e98862dadefd0bea2360c31fa61ca553c660cbf6fb44993acde1b959f58f \ - --hash=sha256:9cbdc517e75e647aa9bf6e356b3a3d2e3af344f82af38e36031eb46ba0ab5425 \ - --hash=sha256:a01f182d8938c1dc515e8508ed2442f7eebd2c25c7d5cb29281f583c1a8008a4 \ - --hash=sha256:a63002f6bb0f1efa87c0b81634cbb571066f237067e23707dabf746306c92ba5 \ - --hash=sha256:aa0e7268f8ceba218964bc3a482a5e9d32e352e8c3538b03f69a6b3db979078d \ - --hash=sha256:aa604f8665ade434f9eafcd23f89435057d5d09427dfb4554c5e6d19f6d8aa1a \ - --hash=sha256:ace1f0753cf71c5c5f6488b8feef5dd0fa8b976ad86b24bb51f708f513df4aac \ - --hash=sha256:b15113ec4fc423b58e9ae80aa95cf5a0802f02d8f02a98a46af3d7d66ff0cc0e \ - --hash=sha256:c05ae2ea878fdfa31dd375326f6191b03de98a9602cc9c2b6d4ff960b20a974c \ - --hash=sha256:c4247e7c44b34c87acbf38f99d48fb1acaf5da4a2cf4dcd601a9b24d431be4ef \ - --hash=sha256:c92e41301a8f25f9adcd90954512038ed5f774a2b8c04a4a9db261b78ff75e3a \ - --hash=sha256:cf0a535cfa02b207435928e991c60389671fe1ea1dfae79170973f82f52335b2 \ - --hash=sha256:cf5ca2b9358d30a395ac6e14b3154a9fd1f9b557ad7153ea15cf697e88d07ce1 \ - --hash=sha256:d3f9da96f83b8892504923beb21a481cd4516c19be1d39eb57a92ef1c9a29548 \ - --hash=sha256:d6d129d0c2587f2f5904d201a4021f859afbb45fada4261c9fdedb2205b09d23 \ - --hash=sha256:dbc828641667142751b1127fd5c4291663490cf05689c85be4c5bcc89aaa236a \ - --hash=sha256:e2cd8600bd16d209d5d45a33b45c278e1cc8bedc169af1a1f2187b581c521395 \ - --hash=sha256:e3aab10e3c10ed8fa60c608fb87c040089b83325c937f98f06450cf9fcfdaf1d \ - --hash=sha256:e5ff68fa770ac654c7974fc78792978796f068bd274e95930c0691c31e192889 \ - --hash=sha256:ed6e50c6e87ed190141573615d54118869bd63e9cd91ca5660d2ca926bf25110 \ - --hash=sha256:f129284d2c7bcd213d11c0f35e1ae506a1144ce4954e9d1734d63b120b0a1b58 \ - --hash=sha256:fa0f5e78cfa8262277bb3d0225537b3c2a90ef68fd90a229d5d24cf49955dcf4 \ - --hash=sha256:fb1e52cf4ee0a9fa8b2254ed589cc51b0c989efc58fa8804289aca94a21253f7 + # via feast (pyproject.toml) +pyclipper==1.4.0 \ + --hash=sha256:0a4d2736fb3c42e8eb1d38bf27a720d1015526c11e476bded55138a977c17d9d \ + --hash=sha256:0b74a9dd44b22a7fd35d65fb1ceeba57f3817f34a97a28c3255556362e491447 \ + --hash=sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b \ + --hash=sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f \ + --hash=sha256:1b6c8d75ba20c6433c9ea8f1a0feb7e4d3ac06a09ad1fd6d571afc1ddf89b869 \ + --hash=sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140 \ + --hash=sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04 \ + --hash=sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a \ + --hash=sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4 \ + --hash=sha256:58e29d7443d7cc0e83ee9daf43927730386629786d00c63b04fe3b53ac01462c \ + --hash=sha256:6a97b961f182b92d899ca88c1bb3632faea2e00ce18d07c5f789666ebb021ca4 \ + --hash=sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826 \ + --hash=sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9 \ + --hash=sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c \ + --hash=sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e \ + --hash=sha256:8d42b07a2f6cfe2d9b87daf345443583f00a14e856927782fde52f3a255e305a \ + --hash=sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1 \ + --hash=sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87 \ + --hash=sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39 \ + --hash=sha256:a8d2b5fb75ebe57e21ce61e79a9131edec2622ff23cc665e4d1d1f201bc1a801 \ + --hash=sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9 \ + --hash=sha256:adcb7ca33c5bdc33cd775e8b3eadad54873c802a6d909067a57348bcb96e7a2d \ + --hash=sha256:b3b3630051b53ad2564cb079e088b112dd576e3d91038338ad1cc7915e0f14dc \ + --hash=sha256:bafad70d2679c187120e8c44e1f9a8b06150bad8c0aecf612ad7dfbfa9510f73 \ + --hash=sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286 \ + --hash=sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c \ + --hash=sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872 \ + --hash=sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037 \ + --hash=sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca \ + --hash=sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e \ + --hash=sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832 \ + --hash=sha256:e9b973467d9c5fa9bc30bb6ac95f9f4d7c3d9fc25f6cf2d1cc972088e5955c01 \ + --hash=sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303 \ + --hash=sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1 \ + --hash=sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6 \ + --hash=sha256:fd24849d2b94ec749ceac7c34c9f01010d23b6e9d9216cf2238b8481160e703d # via easyocr -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi pycryptodome==3.23.0 \ --hash=sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4 \ @@ -3756,11 +4316,12 @@ pycryptodome==3.23.0 \ --hash=sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be \ --hash=sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7 # via minio -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) + # dbt-artifacts-parser # docling # docling-core # docling-ibm-models @@ -3769,259 +4330,404 @@ pydantic==2.10.6 \ # fastapi-mcp # great-expectations # mcp + # mcp-types + # mlflow-skinny + # mlflow-tracing # pydantic-settings + # pyiceberg # qdrant-client -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pydantic-settings==2.11.0 \ - --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ - --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f # via # docling + # docling-core # fastapi-mcp - # mcp pydata-google-auth==1.9.1 \ --hash=sha256:0a51ce41c601ca0bc69b8795bf58bedff74b4a6a007c9106c7cbcdec00eaced2 \ --hash=sha256:75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 # via pandas-gbq -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via - # feast (setup.py) + # feast (pyproject.toml) # ipython # mpire # nbconvert # rich # sphinx -pyjwt[crypto]==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb - # via - # feast (setup.py) +pyiceberg==0.11.1 \ + --hash=sha256:138983e96a5c473aae1e1cd6143aacf02ccc9bbbf6180f15fa2588f99531188b \ + --hash=sha256:1663d79fc8400903992c63f79b3908b9298c623138e8929bf36c559231e082d3 \ + --hash=sha256:1d6b6f0c1e7dd8357f1ba56524bfc870d04ad3c00979db291784a7145497ad3b \ + --hash=sha256:23ed91f8d2fa75a109708338f80a7addd2f37c93a8b0e6c9d220f1427939b81f \ + --hash=sha256:366fe0d5a74e3cf1d4e7cbf3c49e308da60e7835ea268667be9185388f05d7a5 \ + --hash=sha256:4d8790f420ebc484236017edba59182cf2a21bd3e4224a0bd0760a9c7268e96a \ + --hash=sha256:6400774e6820760eb6c322f6feace43fe7267deb9f8d508f10bf258887a9c4d5 \ + --hash=sha256:65a7ad892a570045b0de2db6af17119162880aebc05a0c125ce2db7dab36f17e \ + --hash=sha256:6cf94756fb6a822d20a5a64f44840e6633ebf8b1deb3ce01057bff1cc03b01c2 \ + --hash=sha256:7770e7bdee34549e8baf28b8626e6552de7894d869465a7c95fd9af33b4cbeb0 \ + --hash=sha256:856c7fca5ed780ed44f60bceb92d6b311ebc008a2249415b8f6045201d4f5530 \ + --hash=sha256:89c347170a691e3b8d072519f36790b9c19a2263ec0af0144c873994ecfe55eb \ + --hash=sha256:8bd52c1891ae74cee21a4ebe8325953310a2e0af5352d70f47f5461422fcce2d \ + --hash=sha256:8c62636a1e9d8a1fc74ffb70383939b9cd93f2c9ee8e12015a50dd75c98a989e \ + --hash=sha256:a0f958cbca18d05846e3081dfff8575e73d45595441d659847479656dc76f91d \ + --hash=sha256:ae27ba4d37925d5b2cff192acaa70c8bb114d632bbc527cc91fea0370702b866 \ + --hash=sha256:b347d3cc8510f8fbe191956fcda7da372ebb3302789acefca08e352345959003 \ + --hash=sha256:b4b91f1b1bac135c077326d30a1876f917e6c36b844a2e329b148fb9cb7aa660 \ + --hash=sha256:b7ec5db19feab98a31fcd5caccf4a9a4e83f96933d1ca393ba7aea665710c2bb \ + --hash=sha256:ba98d6a41ec0b7c81dd85d764f15653d6abbbbd69d92630677c43f92dd50d924 \ + --hash=sha256:bba3a35b4648694783aeae5b77c235a57191c8b1b375c8602b03ae56a6cf4fe7 \ + --hash=sha256:ccac408873f65b8954858ef08d4b098841301e232afc1b14e3eada489d303a7f \ + --hash=sha256:cd423b8ee2f75fc9db09158875abe5e2c952a26ae5e521c3265ab2f9d3511ddf \ + --hash=sha256:cec0616d2ba6e7dda6327089a2f34ec723aa9ac2c389857ef0b83f65fb135dd6 \ + --hash=sha256:db66a4e0fdfbf4090631d59c3f65e960d9a5561e9259f6f3993cbe91e396837e \ + --hash=sha256:ddb360da76c62c7c23ec3da40e1af48e6712a563905fea2d1a8911ff7a3b6c4d \ + --hash=sha256:e08c268e12d54ea629a1f123bf7cb9587bdd57fd7bcdac22394dc6272a18668f \ + --hash=sha256:e273242cdca56029af694d7ce18075d47a74d034326d663ff6dd2655a6f44825 \ + --hash=sha256:eb3a0a3e630ee89758eb96b39b456f4697732351fb0c080e9498ea578f9b71f9 + # via feast (pyproject.toml) +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + # via + # feast (pyproject.toml) + # mcp # msal # singlestoredb # snowflake-connector-python -pylatexenc==2.10 \ - --hash=sha256:3dd8fd84eb46dc30bee1e23eaab8d8fb5a7f507347b23e5f38ad9675c84f40d3 +pylatexenc==2.11 \ + --hash=sha256:305a072a99ce736246049c9da05841b9d718c0f7ea8888f5f596cf15cb621053 \ + --hash=sha256:e78e7391d6c104f1ed150e21cfaa58016cdb50aa54406a2eecb793649ffdfdd0 # via docling -pymilvus==2.4.9 \ - --hash=sha256:0937663700007c23a84cfc0656160b301f6ff9247aaec4c96d599a6b43572136 \ - --hash=sha256:45313607d2c164064bdc44e0f933cb6d6afa92e9efcc7f357c5240c57db58fbe - # via feast (setup.py) -pymssql==2.3.2 \ - --hash=sha256:06883bc9bdb297ae9132d9371b5b1a3a223c8f93dd6a87d1c112c6a688f26d53 \ - --hash=sha256:0768d90f96ae3267d7561d3bcfe94dd671d107489e870388b12570c3debbc552 \ - --hash=sha256:0831c5c95aab0b9aba5142dc97e28f59c4130e1c34ffc13ecbfdd4d2fe45b8a0 \ - --hash=sha256:08facd25a50a7279385d1ffcee9d6d83c4e361db1af38e14519a87d7b1cadb10 \ - --hash=sha256:0a20a17db870fb0e446a6d6bf7664aaf84af7be58ab1fad025cafa4e092507a1 \ - --hash=sha256:1037053e6c74d6fe14c428cc942968b4e4bf06854706a83fe8e822e475e3f107 \ - --hash=sha256:148b7714fff5a5b7ce038e92b02dd9bf68fe442c181a3aae32148e7b13f6db95 \ - --hash=sha256:18089641b687be1ebd0f64f0d1ff977478a397ffa1af372bdf10dbec29cf6d2e \ - --hash=sha256:1afda7b7022eff9451bd83e3f64c450a1a8cdff4ba8b8e399866dcd2cb861a1e \ - --hash=sha256:1bc33ed9af6d8ebea2d49144cd2317b7ae1105dd51dddfd46982c90c8f0cf6ab \ - --hash=sha256:1c99dba4bf5b1ce10657e9e2885f18ba9179190251b63d1498e7d6d72e64f1ce \ - --hash=sha256:1cdc2619e7b4192b8d6619fd52ba8a2eae18b38b376f8649fb8f0727c4e88ff9 \ - --hash=sha256:22b1ce3a48f28ee7d06ebc9ed94276d0bf1c99051ee1df3d2377b74721bd62ef \ - --hash=sha256:22fb0fdd3b889bc10abbe3aa2abe7a008b30a6367b9ba159412d185d7d8fda9d \ - --hash=sha256:235c230e56d8c8c5f289e665c538f31d967fec302d05ad269dcd64fa9d6eb3b7 \ - --hash=sha256:23f5e2e2bdba1cf7cecbac66dd07de7631a8efca5692efee18ff46ebc087b757 \ - --hash=sha256:2568944db3888996e161b40ad06c1b9e0fbb6cfcb38279a3abb98ece7a8e1c4a \ - --hash=sha256:26bdb7abd5f107b6be422635f03e2cecaa52a5f4c394a205014586abbff9e72a \ - --hash=sha256:2a44a0898dacba4e25cac8778d0ed112e297883fe862204e447081888da78dc4 \ - --hash=sha256:2f4093b95f1f3a1232687fc92f652aaf675eb423db8549c16d146b91ac2f0eba \ - --hash=sha256:33ad813092f8fb8f74578c5b5e37c818c4ae130fd4047cb28f0b256f2f107367 \ - --hash=sha256:3870085a49e5332bc67ecb24f217c586977d5352eb51598244fc7bc278eee3e1 \ - --hash=sha256:41d09e1b2534229b288c37b88c1de3d964317af2c7eec58bfb97e01d679eba27 \ - --hash=sha256:5904d78e61668ec89761d3ae01efd4b42b31d820f612929f449e93cd23ba3c54 \ - --hash=sha256:6019d2939963112662288704f608f31634038bffcfd5cad1bc79cb167edb3cc1 \ - --hash=sha256:72b6599963b6e066998c4b27b7bf207684c243b12b1e5dcc180b2af22802ae6c \ - --hash=sha256:73fac766b448613d7ae26e6b304b2cb8a7ffebccaa373633bad3b3cbcc829935 \ - --hash=sha256:793a93da1521fa66bf02b3b873065e22bf14bda5570e005ce3d5fae0776d7b92 \ - --hash=sha256:79cdc3ed1da3129ba56232127db86279728c4328595e2532ed4d0da6379a5c72 \ - --hash=sha256:82ed3dd560d3fb222d26ce3a7373f46dc3ad1d50b6e6417ef7399e87fa9aefe1 \ - --hash=sha256:8cd806380d362d4cef2d925a6baee6a4b2b151a92cac2cab5c4bfabed4be4849 \ - --hash=sha256:9361593a89c9162fc631baf648a87e2666373382d9d54aacfb19edab9ceb2007 \ - --hash=sha256:97fbd8491ad3ece0adcb321acec6db48b8fe37bc74af4c91bb657d4d9347d634 \ - --hash=sha256:9e3d6fada7fbe7a5f5fafc420673f777bab3f399c78fa44e29de6a8cbc36e515 \ - --hash=sha256:a3f9e7eb813dfeab6d01bf6474049bb76b0521235159d3e969ec82df384eac49 \ - --hash=sha256:aa08b6203b2b5ed5ce47f80d5c529459181300d7e0d0c1e84390a4d01d45e509 \ - --hash=sha256:ab48de09864fa6f49c575ef569f6773981d0cd905ff7288b5b185f8079a5a21f \ - --hash=sha256:ab912d1178d5977e421cf9c4d4071958b223cbe4a2b6dd64611d521aa6bb7187 \ - --hash=sha256:ae02cc1594f0addd748bf5ac1ccc7a73c03846ada9c553663c381b242b586606 \ - --hash=sha256:b0c2b11aca16617cacaf385fb94134e73ba0216a924f9b85778cc7e3d3713361 \ - --hash=sha256:b14cc65369d1425f2fb517609113465a0f55f19a49648160f2d10be4cb43ff4d \ - --hash=sha256:b156b15165f7a0bbb392a124d8e2d678145c93e5bfcfef3b637e4d87eadcc85b \ - --hash=sha256:b16d5880f7028442d6c49c94801ce9bff3af8af0fbda7c6039febb936714aed5 \ - --hash=sha256:b3eb201c402bcf4f5b9399df0bb20d522636d2e87d1c6957a0b6d772ee636c61 \ - --hash=sha256:bac6f355c454f94b0e15a04b7841236e5c5c4ef44d2d1beed00a3ad7b50ccc53 \ - --hash=sha256:c24ba6aedb9b5540b56f3e74bff92b687c6e90c00650823385729c7e55923cf5 \ - --hash=sha256:cbe9058b6520be74463476ff2cdb17bbab5ff60b60b3ed7bd8bd2d086bdfd9bd \ - --hash=sha256:cc13c2e0f1b8efc3a46941de9db768fa59937b5a54081ec0cb0ff0da17d1fff3 \ - --hash=sha256:dd5fe7552edc81628e4242b4671f7bad5ff1ec790bae5c7615d989375620edac \ - --hash=sha256:eb629b5fb0376fbf39d575cf1365e504b84877b19f9e8d53caa5228fed56894a \ - --hash=sha256:ee8ee2c7c227c413ad9b88ddba1cb6a25e28c217ae73ecac1c7a6b8c29003604 \ - --hash=sha256:ef0d29c705db552f9e75230f946b0ca9db0db903c5c9ee79ce8b88ad25ea9670 \ - --hash=sha256:f1791f4627c42fe2d2833c884d036b0c5c8cf628f2cdfa3536191c217acf729e \ - --hash=sha256:f282e701dca155b3e7f1644d7e3b60c201ca5f3be8045bce34042d3c737d63ee \ - --hash=sha256:f2b1da4e68d618c7972e583ae19f386ae620258acb61564e8067c203f27cd769 \ - --hash=sha256:f9737c06b13ca2012b9900185fa3af72a37941c532da2e6373dd7c9ab16abddf \ - --hash=sha256:fb8a7b197aaf466a7577ca6690aa9d747081b653ab212d052d71f3cc10587c3b \ - --hash=sha256:fdd774b26407babd0205ef85a098f90553e6b3da77a22322a1e7d2cb51f742c0 - # via feast (setup.py) -pymysql==1.1.2 \ - --hash=sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03 \ - --hash=sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 - # via feast (setup.py) -pyodbc==5.2.0 \ - --hash=sha256:057b8ede91b21d9f0ef58210d1ca1aad704e641ca68ac6b02f109d86b61d7402 \ - --hash=sha256:0dae0fb86078c87acf135dbe5afd3c7d15d52ab0db5965c44159e84058c3e2fb \ - --hash=sha256:0e4412f8e608db2a4be5bcc75f9581f386ed6a427dbcb5eac795049ba6fc205e \ - --hash=sha256:113f904b9852c12f10c7a3288f5a3563ecdbbefe3ccc829074a9eb8255edcd29 \ - --hash=sha256:207f16b7e9bf09c591616429ebf2b47127e879aad21167ac15158910dc9bbcda \ - --hash=sha256:26844d780045bbc3514d5c2f0d89e7fda7df7db0bd24292eb6902046f5730885 \ - --hash=sha256:26b7f8324fa01c09fe4843ad8adb0b131299ef263a1fb9e63830c9cd1d5c45e4 \ - --hash=sha256:26d2d8fd53b71204c755abc53b0379df4e23fd9a40faf211e1cb87e8a32470f0 \ - --hash=sha256:2b5323be83fedc79a6d1e1b96e67bdc368c1d3f1562b8f8184b735acdd749ae9 \ - --hash=sha256:4627779f0a608b51ce2d2fe6d1d395384e65ca36248bf9dbb6d7cf2c8fda1cab \ - --hash=sha256:4d997d3b6551273647825c734158ca8a6f682df269f6b3975f2499c01577ddec \ - --hash=sha256:4fde753fcea625bfaed36edae34c2fba15bf0b5d0ea27474ee038ef47b684d1d \ - --hash=sha256:5102007a8c78dd2fc1c1b6f6147de8cfc020f81013e4b46c33e66aaa7d1bf7b1 \ - --hash=sha256:5f0ecbc7067467df95c9b8bd38fb2682c4a13a3402d77dccaddf1e145cea8cc0 \ - --hash=sha256:600ef6f562f609f5612ffaa8a93827249150aa3030c867937c87b24a1608967e \ - --hash=sha256:6493b9c7506ca964b80ad638d0dc82869df7058255d71f04fdd1405e88bcb36b \ - --hash=sha256:74135cb10c1dcdbd99fe429c61539c232140e62939fa7c69b0a373cc552e4a08 \ - --hash=sha256:770e1ac2e7bdf31439bf1d57a1d34ae37d6151216367e8e3f6cdc275006c8bb0 \ - --hash=sha256:7e3cbc7075a46c411b531ada557c4aef13d034060a70077717124cabc1717e2d \ - --hash=sha256:96d3127f28c0dacf18da7ae009cd48eac532d3dcc718a334b86a3c65f6a5ef5c \ - --hash=sha256:97d086a8f7a302b74c9c2e77bedf954a603b19168af900d4d3a97322e773df63 \ - --hash=sha256:9e8f4ee2c523bbe85124540ffad62a3b62ae481f012e390ef93e0602b6302e5e \ - --hash=sha256:9f7badd0055221a744d76c11440c0856fd2846ed53b6555cf8f0a8893a3e4b03 \ - --hash=sha256:a27996b6d27e275dfb5fe8a34087ba1cacadfd1439e636874ef675faea5149d9 \ - --hash=sha256:ad633c52f4f4e7691daaa2278d6e6ebb2fe4ae7709e610e22c7dd1a1d620cf8b \ - --hash=sha256:b1f5686b142759c5b2bdbeaa0692622c2ebb1f10780eb3c174b85f5607fbcf55 \ - --hash=sha256:b77556349746fb90416a48bd114cd7323f7e2559a4b263dada935f9b406ba59b \ - --hash=sha256:be43d1ece4f2cf4d430996689d89a1a15aeb3a8da8262527e5ced5aee27e89c3 \ - --hash=sha256:d287121eeaa562b9ab3d4c52fa77c793dfedd127049273eb882a05d3d67a8ce8 \ - --hash=sha256:d57843b9792994f9e73b91667da6452a4f2d7caaa2499598783eb972c4b6eb93 \ - --hash=sha256:dc5342d1d09466f9e76e3979551f9205a01ff0ea78b02d2d889171e8c3c4fb9c \ - --hash=sha256:de1ee7ec2eb326b7be5e2c4ce20d472c5ef1a6eb838d126d1d26779ff5486e49 \ - --hash=sha256:de8be39809c8ddeeee26a4b876a6463529cd487a60d1393eb2a93e9bcd44a8f5 \ - --hash=sha256:e04de873607fb960e71953c164c83e8e5d9291ce0d69e688e54947b254b04902 \ - --hash=sha256:eaf42c4bd323b8fd01f1cd900cca2d09232155f9b8f0b9bcd0be66763588ce64 \ - --hash=sha256:eb0850e3e3782f57457feed297e220bb20c3e8fd7550d7a6b6bb96112bd9b6fe \ - --hash=sha256:f1f38adc47d36af392475cd4aaae0f35652fdc9e8364bf155810fe1be591336f - # via - # feast (setup.py) +pymilvus==3.0.1 \ + --hash=sha256:c02389059088b18d6e598cd175541e445c772fab4926c5e527c4913be34887f1 \ + --hash=sha256:c5a8d5c1fa1de7b416e3529d383d8cc2e7da2170433ffa4a2d9087e14f70171a + # via feast (pyproject.toml) +pymongo==4.17.0 \ + --hash=sha256:0ff6bd2f735ab5356541e3e57d5b7dbfbc3f2ee1ccb10b6b0f82d58af69d1d8e \ + --hash=sha256:1175563375d682260f613a96fb7a53dce746ed752bfd924eab61de3bc5bfde34 \ + --hash=sha256:1195370a77baf003b59b10e91ecc4706297197f0dd9d29c840cc556dc08f7cee \ + --hash=sha256:12c4fded3a9f1d6a687e36ebd384ac6d00b9b00de1969aa74048e7051ec2a713 \ + --hash=sha256:15d3f3d732aecac1f8d481bde4029755615639bd3076f258a2147210aec8515a \ + --hash=sha256:20323b0b1c1d33770ad1fc68d429c757734ce9ad3594421c3d6618f10572b1b9 \ + --hash=sha256:2a0d5ac205728c86e0a02192f1aa5f865b0d7d51f8df6101c01a69a7fc620d72 \ + --hash=sha256:2db66aa8dd253a0fc1fad3b0d23d5b3993f7ebde02fbbd7727128debf2853675 \ + --hash=sha256:2e190827834fce70ecdf9d46796c6dbc0ce08ea87dc2ff5bc6f3f5579b605cb9 \ + --hash=sha256:320b34457b20bbcc79997801f95d25ce00472915ca5241167242b42c4359e027 \ + --hash=sha256:3689ea34f6b647c7d1e7bdc60fcfb214b2789ed1359a7fb96569c69f50e5f18f \ + --hash=sha256:37a8385c29881b43eab31f584100fa0eaddedd5607adf010147ba1810118be90 \ + --hash=sha256:3987e96e7c7be4083d42e8ac2cc6c0d5b78db9973c90fce42ae800b616ca6b20 \ + --hash=sha256:4141e6c6a339789b2974efa00ecd9409101672d77a0e3ee2cc3839eedf8ec4df \ + --hash=sha256:422fa50d7d7f5c22ea0953554396c9ef95684a2d775f860bd75a7b510538dfca \ + --hash=sha256:47b021363cd923ace5edc7a1d63c0ff8a6d9d43859b8a1ba23645f5afae63221 \ + --hash=sha256:485c8a8eaa4c739f00a331fc73757898ee7c092c214a79e63866ff76aaf282ff \ + --hash=sha256:48bbc576677b50af043df870d84ded67cc3a9b4aa7553201beef4da5dc050a0a \ + --hash=sha256:4ae22fafca69dd3c78261969e999782ac5fc23b76cf8cccfbc3707982a74cc3d \ + --hash=sha256:50e8f8e23c6df7c6d6929f5e734980b227706e73ee847517c9ba5af90f7fc466 \ + --hash=sha256:51e1915761f65f2aaabd0ba691a31d56551d3f19d1263c2d6bf261730603de5f \ + --hash=sha256:5376ad67bb30ae910d83affcf997f706d9dee37e8b5dad8b6fedb0626e262d85 \ + --hash=sha256:5960519b4d7168f1ecdd3ea10c81b2aedeb9423651aca953cfbc8e76705d3b38 \ + --hash=sha256:5a5de048e6da5c18e27cc2437e8c15b3b0cdc8385c15b41178b0caa3322a09c2 \ + --hash=sha256:5ab3b8ff79e0dfc49b68f3c925e8cc735ea95c60efaed84cfe75692dffcaac2a \ + --hash=sha256:64837adbbd72073301af51bb0fc80e3d7707fe5527cea1033ba0320f0b2f881b \ + --hash=sha256:6877214bff5f06f6884a9fc8d9016a4a7a5f51f537f5c51ac3a576f93e7dfb32 \ + --hash=sha256:68fca71e05ee5da23a8d73cee8379dfb3d26e609a377cae731d742771ed96946 \ + --hash=sha256:6c5f62862d0f87be481fa1fe8cb811994486773c94a2b61e509285e3f2890763 \ + --hash=sha256:6fe0de9d0f6791abce3471230b32b4817bf89d27b1182b6a550e1ec0fa72aa9a \ + --hash=sha256:70ffa08ba641468cc068cf46c06b34f01a8ce3489f6411309fcb5ceabe6b2fc0 \ + --hash=sha256:757f2a4c0c2c46cab87df0333681ce69e86c9d5b45bc5203ceba5410b3489e59 \ + --hash=sha256:75bc3aa5b94fdb7138d357ec6ca61cd97e0c79f4f7f0bd3efe9639b15cc50942 \ + --hash=sha256:77aa4bc164b4de60d5db193b322f0f5b6ead716e831031bfdef8e8bd92205556 \ + --hash=sha256:7db10678814cdf7ea39fd308c6f41395cfa7b29d904bcd7895288963d8f892ba \ + --hash=sha256:809ec74de3b9148ae43fa8df9faf53470f511c8d384f13b99d6f671f2a379f15 \ + --hash=sha256:8446ff4bfcb6ec2a2e50998c860986a1e992136f998b7f53e7a717fb8aa5a0b9 \ + --hash=sha256:8a1be016198a03fd7727cdd55998964bfa4e5a6fd9733c8e95830628cef34d29 \ + --hash=sha256:8e97e03fa13327c87e3fdc5656acd01e71817f0c1dc3221cd8f30de136bf4ec3 \ + --hash=sha256:93641192644fa1ee0f34030e774fd31022a27ad11ba22cb1716142231524f8bd \ + --hash=sha256:9543d8f84c2e5608565c08ac679774811e6730770d8a645439b073422a4276fb \ + --hash=sha256:9828485f72f63c7d802e0ec41f71906f633c2692621ab3af55ca990186b091b1 \ + --hash=sha256:9eb5d63a3c518cb0804ed678f5e2b875af032d89a7cf57a57360322cf6a4d222 \ + --hash=sha256:a431b737816bf4cddd4fa0fcef04e424ad36b7692734a64150f872fb8f3208be \ + --hash=sha256:a8f9c40a09bb7d4b9fc8b1da65ecf6efa79bda5cb2756f39d9b6940fac1d19ae \ + --hash=sha256:addd0498ebbdc6354227f6ed457ed9fce442d48a3bb30d5b5bad33e104996561 \ + --hash=sha256:b24598dc3c2feccbc83b43044be48145a0dc4f9bee49ef923e3d707d54a55d85 \ + --hash=sha256:b2dfcc795f5b9fedbe179a11fdf6051581479d196582a3fe819a92a00e9b9969 \ + --hash=sha256:b4384700cffc3f1dd98e088bc0072dedf6d7d68a230bb4b972665cf69c071c1e \ + --hash=sha256:b93b22eedc62598cf5ee9d8c8007a8e9121c50fd88137012d8985500e9dc3151 \ + --hash=sha256:ba2195d4f386f839a52a23ea1cfd60ffaaba78a3d7841db51b7e433001139918 \ + --hash=sha256:bb3ebc86782049f6928dcc583008287cb1c17d463501c94a620f035f5b4fd463 \ + --hash=sha256:bd835cdb37a1adec359dd072c24f8bb14809e2644fde86fab4ee2fc9719b9483 \ + --hash=sha256:c2292144505fb12156b981bd440f3dc994a883da06ac726c0c8692ccdbc1c510 \ + --hash=sha256:c4979e7e8887862bbb44d203f00cc8263a3f27237876fa691b6beba23e40e6d8 \ + --hash=sha256:c5c8e180cb2cabe37300e1e36c60aa4f2ff956cc579f0142135a5d2cba252243 \ + --hash=sha256:c797f8a80957134f6dd9690367a0f8f5906d672119af2c6aa55f0c527b656bed \ + --hash=sha256:c9786665926a09630c5d420c79762cfadbff35a9438bcbc4c81a9fb5ab9228b7 \ + --hash=sha256:cee36b3c0d0354f880fa7a7fdcdaf2bb5e542c2281e25c1bfadf8cfe21eba7d2 \ + --hash=sha256:d53ffa94b2340dbf6b055e09a0090618c60482c158ecfc9565642fc996bf0944 \ + --hash=sha256:df4a644af9ae132d4bfdb2e9516ea51a615fd881caddfbfbd071cf1354844479 \ + --hash=sha256:dff3de1294fbbc1db0ba6b511f77b8e540601d092538a31312e99c8a91a78b1e \ + --hash=sha256:e46767f28dea610e02edf6c5d956ce615c3c7790ea396660b9b1efd5c5ead2e0 \ + --hash=sha256:e4fab10f8403169ce92f3cea921609d9ee81107306caae06c08f592d4b8ad2b5 \ + --hash=sha256:e537e95514dae1aaa718f481ec03151a0f0394bcd05f1322896d8fc1330cb729 \ + --hash=sha256:e68c76b84e0c132d9dbf9307f12ff8185702328187a87b9aca8c941303873433 \ + --hash=sha256:e816db649ba5d7de0568cf3a9f287a9dc9aad21cf0ca667ab156a7ef47fca0b0 \ + --hash=sha256:f09645e0ce4e3825fa0baa8254064a716ed0be33f78feeedd4731016cb8aaa17 \ + --hash=sha256:f3ee3d241ed77a4fc99ce3cff3b289c3ebce37f61fdd7349d3592c23b82c8784 \ + --hash=sha256:faf03e4c2aafd6de626dbd30ba246d369ae33f47f10629d1bbe40f72115027a6 \ + --hash=sha256:ff5aa3f1c7e3f08eb0e7a016c91ba468b1850ccfd63d9b1f12f56350f4974cef + # via feast (pyproject.toml) +pymssql==2.3.13 \ + --hash=sha256:0a7e6431925572bc75fb47929ae8ca5b0aac26abfe8b98d4c08daf117b5657f1 \ + --hash=sha256:0fddd24efe9d18bbf174fab7c6745b0927773718387f5517cf8082241f721a68 \ + --hash=sha256:123c55ee41bc7a82c76db12e2eb189b50d0d7a11222b4f8789206d1cda3b33b9 \ + --hash=sha256:1493f63d213607f708a5722aa230776ada726ccdb94097fab090a1717a2534e0 \ + --hash=sha256:152be40c0d7f5e4b1323f7728b0a01f3ee0082190cfbadf84b2c2e930d57e00e \ + --hash=sha256:16c5957a3c9e51a03276bfd76a22431e2bc4c565e2e95f2cbb3559312edda230 \ + --hash=sha256:17942dc9474693ab2229a8a6013e5b9cb1312a5251207552141bb85fcce8c131 \ + --hash=sha256:1c6d0b2d7961f159a07e4f0d8cc81f70ceab83f5e7fd1e832a2d069e1d67ee4e \ + --hash=sha256:2137e904b1a65546be4ccb96730a391fcd5a85aab8a0632721feb5d7e39cfbce \ + --hash=sha256:2b056eb175955f7fb715b60dc1c0c624969f4d24dbdcf804b41ab1e640a2b131 \ + --hash=sha256:30918bb044242865c01838909777ef5e0f1b9ecd7f5882346aefa57f4414b29c \ + --hash=sha256:319810b89aa64b99d9c5c01518752c813938df230496fa2c4c6dda0603f04c4c \ + --hash=sha256:476f6f06b2ae5dfbfa0b169a6ecdd0d9ddfedb07f2d6dc97d2dd630ff2d6789a \ + --hash=sha256:48631c7b9fd14a1bd5675c521b6082590bf700b7961c65638d237817b3fde735 \ + --hash=sha256:4aa18944a121f996178e26cadc598abdbf73759f03dc3cd74263fdab1b28cd96 \ + --hash=sha256:4b834c34e7600369eee7bc877948b53eb0fe6f3689f0888d005ae47dd53c0a66 \ + --hash=sha256:4d87237500def5f743a52e415cd369d632907212154fcc7b4e13f264b4e30021 \ + --hash=sha256:51e42c5defc3667f0803c7ade85db0e6f24b9a1c5a18fcdfa2d09c36bff9b065 \ + --hash=sha256:5c045c0f1977a679cc30d5acd9da3f8aeb2dc6e744895b26444b4a2f20dad9a0 \ + --hash=sha256:5c2e55b6513f9c5a2f58543233ed40baaa7f91c79e64a5f961ea3fc57a700b80 \ + --hash=sha256:612ac062027d2118879f11a5986e9d9d82d07ca3545bb98c93200b68826ea687 \ + --hash=sha256:6a6c0783d97f57133573a03aad3017917dbdf7831a65e0d84ccf2a85e183ca66 \ + --hash=sha256:79c759db6e991eeae473b000c2e0a7fb8da799b2da469fe5a10d30916315e0b5 \ + --hash=sha256:7d7037d2b5b907acc7906d0479924db2935a70c720450c41339146a4ada2b93d \ + --hash=sha256:7f9b1d5aef2b5f47a7f9d9733caee4d66772681e8f798a0f5e4739a8bdab408c \ + --hash=sha256:8d66ce0a249d2e3b57369048d71e1f00d08dfb90a758d134da0250ae7bc739c1 \ + --hash=sha256:910404e0ec85c4cc7c633ec3df9b04a35f23bb74a844dd377a387026ae635e3a \ + --hash=sha256:a930adda87bdd8351a5637cf73d6491936f34e525a5e513068a6eac742f69cdb \ + --hash=sha256:aa5e07eff7e6e8bd4ba22c30e4cb8dd073e138cd272090603609a15cc5dbc75b \ + --hash=sha256:b0af51904764811da0bfe4b057b1d72dee11a399ce9ed5770875162772740c8a \ + --hash=sha256:c0ea72641cb0f8bce7ad8565dbdbda4a7437aa58bce045f2a3a788d71af2e4be \ + --hash=sha256:c690f1869dadbf4201b7f51317fceff6e5d8f5175cec6a4a813e06b0dca2d6ed \ + --hash=sha256:cf4f32b4a05b66f02cb7d55a0f3bcb0574a6f8cf0bee4bea6f7b104038364733 \ + --hash=sha256:d663c908414a6a032f04d17628138b1782af916afc0df9fefac4751fa394c3ac \ + --hash=sha256:d94da3a55545c5b6926cb4d1c6469396f0ae32ad5d6932c513f7a0bf569b4799 \ + --hash=sha256:db77da1a3fc9b5b5c5400639d79d7658ba7ad620957100c5b025be608b562193 \ + --hash=sha256:e053b443e842f9e1698fcb2b23a4bff1ff3d410894d880064e754ad823d541e5 \ + --hash=sha256:e7c31f192da9d30f0e03ad99e548120a8740a675302e2f04fa8c929f7cbee771 \ + --hash=sha256:eb3275985c23479e952d6462ae6c8b2b6993ab6b99a92805a9c17942cf3d5b3d \ + --hash=sha256:f1897c1b767cc143e77d285123ae5fd4fa7379a1bfec5c515d38826caf084eb6 \ + --hash=sha256:f5d995a80996235ed32102a93067ce6a7143cce3bfd4e5042bf600020fc08456 \ + --hash=sha256:fc5482969c813b0a45ce51c41844ae5bfa8044ad5ef8b4820ef6de7d4545b7f2 \ + --hash=sha256:ff5be7ab1d643dbce2ee3424d2ef9ae8e4146cf75bd20946bc7a6108e3ad1e47 + # via feast (pyproject.toml) +pymysql==1.2.0 \ + --hash=sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 \ + --hash=sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33 + # via feast (pyproject.toml) +pyodbc==5.3.0 \ + --hash=sha256:01166162149adf2b8a6dc21a212718f205cabbbdff4047dc0c415af3fd85867e \ + --hash=sha256:0263323fc47082c2bf02562f44149446bbbfe91450d271e44bffec0c3143bfb1 \ + --hash=sha256:08b2439500e212625471d32f8fde418075a5ddec556e095e5a4ba56d61df2dc6 \ + --hash=sha256:0df7ff47fab91ea05548095b00e5eb87ed88ddf4648c58c67b4db95ea4913e23 \ + --hash=sha256:101313a21d2654df856a60e4a13763e4d9f6c5d3fd974bcf3fc6b4e86d1bbe8e \ + --hash=sha256:13656184faa3f2d5c6f19b701b8f247342ed581484f58bf39af7315c054e69db \ + --hash=sha256:1629af4706e9228d79dabb4863c11cceb22a6dab90700db0ef449074f0150c0d \ + --hash=sha256:197bb6ddafe356a916b8ee1b8752009057fce58e216e887e2174b24c7ab99269 \ + --hash=sha256:2035c7dfb71677cd5be64d3a3eb0779560279f0a8dc6e33673499498caa88937 \ + --hash=sha256:25b6766e56748eb1fc1d567d863e06cbb7b7c749a41dfed85db0031e696fa39a \ + --hash=sha256:25c4cfb2c08e77bc6e82f666d7acd52f0e52a0401b1876e60f03c73c3b8aedc0 \ + --hash=sha256:2eb7151ed0a1959cae65b6ac0454f5c8bbcd2d8bafeae66483c09d58b0c7a7fc \ + --hash=sha256:2fe0e063d8fb66efd0ac6dc39236c4de1a45f17c33eaded0d553d21c199f4d05 \ + --hash=sha256:349a9abae62a968b98f6bbd23d2825151f8d9de50b3a8f5f3271b48958fdb672 \ + --hash=sha256:363311bd40320b4a61454bebf7c38b243cd67c762ed0f8a5219de3ec90c96353 \ + --hash=sha256:3cc472c8ae2feea5b4512e23b56e2b093d64f7cbc4b970af51da488429ff7818 \ + --hash=sha256:3f1bdb3ce6480a17afaaef4b5242b356d4997a872f39e96f015cabef00613797 \ + --hash=sha256:452e7911a35ee12a56b111ac5b596d6ed865b83fcde8427127913df53132759e \ + --hash=sha256:46185a1a7f409761716c71de7b95e7bbb004390c650d00b0b170193e3d6224bb \ + --hash=sha256:46869b9a6555ff003ed1d8ebad6708423adf2a5c88e1a578b9f029fb1435186e \ + --hash=sha256:58635a1cc859d5af3f878c85910e5d7228fe5c406d4571bffcdd281375a54b39 \ + --hash=sha256:5cbe4d753723c8a8f65020b7a259183ef5f14307587165ce37e8c7e251951852 \ + --hash=sha256:5ceaed87ba2ea848c11223f66f629ef121f6ebe621f605cde9cfdee4fd9f4b68 \ + --hash=sha256:5dd3d5e469f89a3112cf8b0658c43108a4712fad65e576071e4dd44d2bd763c7 \ + --hash=sha256:5ebf6b5d989395efe722b02b010cb9815698a4d681921bf5db1c0e1195ac1bde \ + --hash=sha256:6132554ffbd7910524d643f13ce17f4a72f3a6824b0adef4e9a7f66efac96350 \ + --hash=sha256:6682cdec78f1302d0c559422c8e00991668e039ed63dece8bf99ef62173376a5 \ + --hash=sha256:676031723aac7dcbbd2813bddda0e8abf171b20ec218ab8dfb21d64a193430ea \ + --hash=sha256:705903acf6f43c44fc64e764578d9a88649eb21bf7418d78677a9d2e337f56f2 \ + --hash=sha256:729c535341bb09c476f219d6f7ab194bcb683c4a0a368010f1cb821a35136f05 \ + --hash=sha256:74528fe148980d0c735c0ebb4a4dc74643ac4574337c43c1006ac4d09593f92d \ + --hash=sha256:754d052030d00c3ac38da09ceb9f3e240e8dd1c11da8906f482d5419c65b9ef5 \ + --hash=sha256:7713c740a10f33df3cb08f49a023b7e1e25de0c7c99650876bbe717bc95ee780 \ + --hash=sha256:7e9ab0b91de28a5ab838ac4db0253d7cc8ce2452efe4ad92ee6a57b922bf0c24 \ + --hash=sha256:8339d3094858893c1a68ee1af93efc4dff18b8b65de54d99104b99af6306320d \ + --hash=sha256:8aa396c6d6af52ccd51b8c8a5bffbb46fd44e52ce07ea4272c1d28e5e5b12722 \ + --hash=sha256:9b987a25a384f31e373903005554230f5a6d59af78bce62954386736a902a4b3 \ + --hash=sha256:9cd3f0a9796b3e1170a9fa168c7e7ca81879142f30e20f46663b882db139b7d2 \ + --hash=sha256:a48d731432abaee5256ed6a19a3e1528b8881f9cb25cb9cf72d8318146ea991b \ + --hash=sha256:ac23feb7ddaa729f6b840639e92f83ff0ccaa7072801d944f1332cd5f5b05f47 \ + --hash=sha256:af4d8c9842fc4a6360c31c35508d6594d5a3b39922f61b282c2b4c9d9da99514 \ + --hash=sha256:afe7c4ac555a8d10a36234788fc6cfc22a86ce37fc5ba88a1f75b3e6696665dc \ + --hash=sha256:b180bc5e49b74fd40a24ef5b0fe143d0c234ac1506febe810d7434bf47cb925b \ + --hash=sha256:b35b9983ad300e5aea82b8d1661fc9d3afe5868de527ee6bd252dd550e61ecd6 \ + --hash=sha256:bc834567c2990584b9726cba365834d039380c9dbbcef3030ddeb00c6541b943 \ + --hash=sha256:bfeb3e34795d53b7d37e66dd54891d4f9c13a3889a8f5fe9640e56a82d770955 \ + --hash=sha256:c25dc9c41f61573bdcf61a3408c34b65e4c0f821b8f861ca7531b1353b389804 \ + --hash=sha256:c2eb0b08e24fe5c40c7ebe9240c5d3bd2f18cd5617229acee4b0a0484dc226f2 \ + --hash=sha256:c5c30c5cd40b751f77bbc73edd32c4498630939bcd4e72ee7e6c9a4b982cc5ca \ + --hash=sha256:c67e7f2ce649155ea89beb54d3b42d83770488f025cf3b6f39ca82e9c598a02e \ + --hash=sha256:c68d9c225a97aedafb7fff1c0e1bfe293093f77da19eaf200d0e988fa2718d16 \ + --hash=sha256:c6ccb5315ec9e081f5cbd66f36acbc820ad172b8fa3736cf7f993cdf69bd8a96 \ + --hash=sha256:c79df54bbc25bce9f2d87094e7b39089c28428df5443d1902b0cc5f43fd2da6f \ + --hash=sha256:cf18797a12e70474e1b7f5027deeeccea816372497e3ff2d46b15bec2d18a0cc \ + --hash=sha256:d255f6b117d05cfc046a5201fdf39535264045352ea536c35777cf66d321fbb8 \ + --hash=sha256:d32c3259762bef440707098010035bbc83d1c73d81a434018ab8c688158bd3bb \ + --hash=sha256:d89a7f2e24227150c13be8164774b7e1f9678321a4248f1356a465b9cc17d31e \ + --hash=sha256:e3c39de3005fff3ae79246f952720d44affc6756b4b85398da4c5ea76bf8f506 \ + --hash=sha256:e981db84fee4cebec67f41bd266e1e7926665f1b99c3f8f4ea73cd7f7666e381 \ + --hash=sha256:ebc3be93f61ea0553db88589e683ace12bf975baa954af4834ab89f5ee7bf8ae \ + --hash=sha256:f1ad0e93612a6201621853fc661209d82ff2a35892b7d590106fe8f97d9f1f2a \ + --hash=sha256:f927b440c38ade1668f0da64047ffd20ec34e32d817f9a60d07553301324b364 \ + --hash=sha256:fc5ac4f2165f7088e74ecec5413b5c304247949f9702c8853b0e43023b4187e8 \ + --hash=sha256:fe77eb9dcca5fc1300c9121f81040cc9011d28cff383e2c35416e9ec06d4bc95 + # via + # feast (pyproject.toml) # ibis-framework -pyopenssl==25.1.0 \ - --hash=sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab \ - --hash=sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b +pyopenssl==26.3.0 \ + --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ + --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 # via snowflake-connector-python -pyparsing==3.2.5 \ - --hash=sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6 \ - --hash=sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e - # via great-expectations +pyparsing==3.3.2 \ + --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ + --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc + # via + # great-expectations + # matplotlib + # pyiceberg pypdfium2==4.30.0 \ --hash=sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e \ --hash=sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29 \ @@ -4043,14 +4749,101 @@ pyproject-hooks==1.2.0 \ # via # build # pip-tools -pyspark==4.0.1 \ - --hash=sha256:9d1f22d994f60369228397e3479003ffe2dd736ba79165003246ff7bd48e2c73 - # via feast (setup.py) +pyroaring==1.1.0 \ + --hash=sha256:01212da3752d6486adcca98c9d353f8fb8e36513e05062cdd0feebc4211dbe70 \ + --hash=sha256:0183b26c6e25a14e32b2aed7095a19491d6c835096032b33bc00b4ae995495bf \ + --hash=sha256:043dbbaa905f7288c515ac06a96b67a3763f35e9ae06f0c0278c0d9964d16760 \ + --hash=sha256:0dfb6cf50fd8898179e460e699a6b8326ca508c627d083f7bf62f769fe1717d5 \ + --hash=sha256:0f1d76ef29034017eb2cceebd5fa0504d6ced218ce6432f99da5adecbe038269 \ + --hash=sha256:100585f438b293112e2c52e45a442835837c8a0267dd1e513bafec35628f8ecb \ + --hash=sha256:13660386ea8905ee4d42c21a6275463e2dc7d31e0b5d65eec210aa7043ad96f4 \ + --hash=sha256:19c9bb8b32f17fe63e54e78f88a7a67fe2dca352bf79d7d3b43f0a901db5ea3b \ + --hash=sha256:19d0c81c865d63791fe20e5b38733b66f4f962e677ae7e8b3d3c4947ac6e752f \ + --hash=sha256:1fc112b9a9890f89cc645a16604783ed7fa25299f149b0ef7b45a5e2e3c1f31f \ + --hash=sha256:212f471da113eba24a0fedbd1ef078bfed34b792e8f48dba3f7301f1bca2678a \ + --hash=sha256:25d65257258f7a3df8b872ddfe18f3681f20b3bc2a3093f08fc80c66338051f4 \ + --hash=sha256:370d191b0d1b32bbd99452ef5f0485f22fcc4bf7404d33b821d0ce2459951152 \ + --hash=sha256:381eda673442c389993f8b0db2dbf5d02ea8ea9aac6ba736f64cc1ffb6c96885 \ + --hash=sha256:39eff7dd06c163c22d0a9f9fd72d27e671457bea8cdb71215382a10512539e1d \ + --hash=sha256:3beb40eb1220d1ce4fb3661bb019e9a21857e5bb294fe8c1c5016aeb6e82318c \ + --hash=sha256:3d04c3eb039cf2295e4d375ef81fd6e3cae7cefe3b5c457ccaf78d7d6f963a5d \ + --hash=sha256:44a9f9719ed18e86627286f90057f9b7e22f6ba1d952c9793f9600b5c14e8680 \ + --hash=sha256:462b0952277d3100a90ae890ae641d3fb3561b10cfea542e02468f0bef7700a5 \ + --hash=sha256:499f0a5d03d64e7b846ee749e87721fb8516da7233baafb24ab69aa63bcf080b \ + --hash=sha256:4a2c7ecd74e7b3ded54ad771a2b7c0aa08aa98f78e43500fa970d68d684bef0c \ + --hash=sha256:4ae81a0353af50d6885720b9119783b8be7bcd079e727ea1c4fca3f452fe95ba \ + --hash=sha256:4c443e9f942b6089efe8c9b264576e9d116f90be28a315679375bba2d8a915d6 \ + --hash=sha256:51dd2490a64ad4ed53c4fb58ef1ee3f84f6cbd97cdb47abd9065c9f714ab72ef \ + --hash=sha256:51ebe5e6f48e3dc9df91a4cb62137ef72e1469acd6f37479abd9991f6d945cc9 \ + --hash=sha256:532ae6bb1d3431d9956ef07589dd5c8dd918301a83d937c7dc6e511b1364d76a \ + --hash=sha256:532e53191d8dd29dedfc5202cbb45632f7df751b207a7f6d6860fb7067c7fe11 \ + --hash=sha256:53acecba8f898e96b84d4139356e30719c70358177e270055901d3ec1cb0e34c \ + --hash=sha256:5599b691e5e993f7fdd9880429d1c485eae03dc392ba5b2dbe2eef0434bea1a5 \ + --hash=sha256:55c9d0b708a65308eaa95c53d9de56a2b4425812395f6e57a4d9ad8efb081bf9 \ + --hash=sha256:562fa04bbfd41144d1276ed79505007557c161371450d68a1d71fc83dc01d083 \ + --hash=sha256:56a67794188275f8897a8f1fa64d6313c48241bebbdef38833063e7281b29ef8 \ + --hash=sha256:591e2ed4d60443dafd9075c1f72e9aaf359ccf5120e32a8c340c2b2ae3da45e7 \ + --hash=sha256:5e337f8c5b3c2e0c27da83fc2cb702684a47eee907a960cfee964fcb5344515b \ + --hash=sha256:5eb031237e9d39cbdfc9276facacdd88e27aefb58940bd8b56b878dfd38d6022 \ + --hash=sha256:61a8eabee99104ca197b6e7cce05dc4f27f503be52881800cd370eb5a5152d3f \ + --hash=sha256:650db21c10f42ff2b09ef02c10a779a3d59d0c7512552f3844738b30adbcb8a5 \ + --hash=sha256:66aa6a321fbc598f26d5e66050a7f145c2253f3fe5737b589841ff0cbe5cb177 \ + --hash=sha256:67650460c65bdd7b4f5078d9c955aa38f64627d02cb48f9cfb24eae84bca2aba \ + --hash=sha256:6a1d4c59d5b23c01d62f86d57ceefd0c0977de0425aafa7069f2d70563fed3b8 \ + --hash=sha256:71fec09bd42f8c33ac3b762cd00c5db842eb583ffd0e361739ce1c17ad078a6a \ + --hash=sha256:72f68a16b00b35481d9b3bfe897ecd8a1f7da69efd92ba5b17347ca11c21cb0d \ + --hash=sha256:731a7a9e050758986d5757eea10f9ccb08f9c3ef514ce0335f4a90e126f81131 \ + --hash=sha256:7caf95de39ce869ea0978068521cf6faa7350574fd1734ad6c63e5ed8cd06baa \ + --hash=sha256:81ebbc0c880c8a10f13118632e5c0d59159ceada8b651bba18f2e6dc70efdeda \ + --hash=sha256:8376c62e60a49e8fe51ffce569f74beed6e53fb58e5d6739f954c8c51ec0bec3 \ + --hash=sha256:83815b3a4cb9c3b17096c70833373610d5c93e89c134548cf43436c90326bfb3 \ + --hash=sha256:8a5fbcb86e44f1c0c9c052917eee67a04cbac9de7392fb4bc77c140ff4a7e471 \ + --hash=sha256:8cff06d18c9a30f8547a92757078aa345db1ba5b22e3082a05f64e50b384e27a \ + --hash=sha256:8f1f56004e8f1c1489bf279c25f1fa4764252cd9af5fb35675774268a4a615ba \ + --hash=sha256:8f5194c0544b08e56e108dad096c468c67f8ac60eec42763aed36ccbd3565346 \ + --hash=sha256:90ab2f00c09eed5bd986a80c8641e2dc10e7aca1a2d892d89a44b396e39c08ea \ + --hash=sha256:92643c9dd303de8960c3dbed93a28b8d87da5ed0a7776568979f379d7bc8a885 \ + --hash=sha256:974fb6fa4c4a682c633c4d7fa6087ccd717804726f9db1f771f1c2ea4f3990b7 \ + --hash=sha256:986efb3aec7655d69c14db2309a2072dbf181bdb906091fede83ad18e316cdaf \ + --hash=sha256:9882a204178cc8c915e0ce30abb4bdd1668e383c571b06649d5ed272d9625877 \ + --hash=sha256:99c42fe1449acfbf130da65e66b4d5b2726aba4497be359bae7672e38a15fc62 \ + --hash=sha256:9cf8608c9d6cb6bff9c624744f7a2ba8ab12276f097a07930490fe0e2219e9be \ + --hash=sha256:9d9f196007f0b15ea19c21732faacaea83cbf5946b6db4949b3b98cf871c93f0 \ + --hash=sha256:9dc61b7e38aca2ef384ac22b87a9c2b1e8769a1e15d916f93ea6a9a4551fc1d7 \ + --hash=sha256:a23cd023985b5f2ba23e84e1fadaeacde3c8a59e1d2adb3fe782e99db1e22387 \ + --hash=sha256:a31276323fdd355505883162f1dca4d7acc3d6ec2159c85ddef863f1fe9e8f05 \ + --hash=sha256:a6343249ce9401dd90c3b934fd9a4a618b6fc455f27a9aa11d198eebd4743137 \ + --hash=sha256:a98d1147fe1d3195053b67b474bccc0be5021506765d27f613a943c8c99f9e4c \ + --hash=sha256:abc0f0ce22464864fea208315d25e999e45cb5ee646ac1ca11d314a6a51dbe4a \ + --hash=sha256:b490f2d22df30affbfdcbe4f7896f321edb72a8dc0cbe5f38adec3de5b947c25 \ + --hash=sha256:b8ac1bc26223befbca986551521f37f4c1670dfe26fccb2f0fc2775e75be99c1 \ + --hash=sha256:c14fc6bd65e5624f76b90297b081222261476978f795f60d48745553617ddceb \ + --hash=sha256:c967ddb5e137c604ec94c7120d2f420ac9a9ce27b0e44987c7134b0eb5fe7a60 \ + --hash=sha256:c9f30ca28b991a920b446ed3ee19c7ecafcc49c46db592abf89cf239a7bb45f4 \ + --hash=sha256:cc2d113bf6236aa873dc710b38a856808c6054efd3a1a403c5acfceaf87138f1 \ + --hash=sha256:cc998e81748b72659802a7b434b39292308a5f4ab8dd4bf4739d65de1d4ed2bf \ + --hash=sha256:cdf2b8ab0effc335a9e61cf2a1e14c2295b999a656569b35f35ea3dc55a8931f \ + --hash=sha256:d2706a89242a347be20805147d58a38f4f4d8f6846228c4ee8dfd3587113719c \ + --hash=sha256:d9127feb5356ba3a92bdffa04c1bf6bcbc8d436369f78badf441018c3029dd63 \ + --hash=sha256:d92a0f4c7e6bb7deeafac68c79c92ef9340895fe825cf1a31078443753ab6756 \ + --hash=sha256:e8b3bfad0ae3ef0e67b40c193863dce8b7d79de545dadbe53c19acc3ace38f66 \ + --hash=sha256:e9864e19109e76111befc75d799d334e7365eb4189607aa734053c12e7840fa5 \ + --hash=sha256:e9c2b9aa8decdcf40ed8f4c887092c20a272f8c32215c3fee65e9db92ecf418e \ + --hash=sha256:e9caf67c0c05d27bab0d88cf193e73023f624c6714f1ff807c7d4b135b19d36a \ + --hash=sha256:eead129046822cb0fd47c78740b81bdaffd0515c0bb0306a2318acf0f0540b58 \ + --hash=sha256:f02e4021397ae02a139defdc6813b9942ab163de90affddd4ce4efbac299f619 \ + --hash=sha256:f742ebdd879063d2a92f4d57708886c720e854bc84ca240733f220936be0cb7e \ + --hash=sha256:fdf484d26016e0c016f23f2b635d2899daec034565fdcc062ed6b10f3b26a3f4 \ + --hash=sha256:fe0b9f0baf190663df37571cbb8a77370e7bd3a66068276d48f813c53ceba530 + # via pyiceberg +pyspark==4.2.0 \ + --hash=sha256:5ad689d53570ee1674193fd4f9bda065f0db3be9363a27d2a3406cc457b70b61 + # via feast (pyproject.toml) pytest==7.4.4 \ --hash=sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280 \ --hash=sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 # via - # feast (setup.py) + # feast (pyproject.toml) # pytest-asyncio # pytest-benchmark # pytest-cov @@ -4063,203 +4856,221 @@ pytest==7.4.4 \ pytest-asyncio==0.23.8 \ --hash=sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2 \ --hash=sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-benchmark==3.4.1 \ --hash=sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809 \ --hash=sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47 - # via feast (setup.py) -pytest-cov==7.0.0 \ - --hash=sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1 \ - --hash=sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861 - # via feast (setup.py) + # via feast (pyproject.toml) +pytest-cov==7.1.0 \ + --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ + --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 + # via feast (pyproject.toml) pytest-env==1.1.3 \ --hash=sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc \ --hash=sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b - # via feast (setup.py) + # via feast (pyproject.toml) pytest-lazy-fixture==0.6.3 \ --hash=sha256:0e7d0c7f74ba33e6e80905e9bfd81f9d15ef9a790de97993e34213deb5ad10ac \ --hash=sha256:e0b379f38299ff27a653f03eaa69b08a6fd4484e46fd1c9907d984b9f9daeda6 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-mock==1.10.4 \ --hash=sha256:43ce4e9dd5074993e7c021bb1c22cbb5363e612a2b5a76bc6d956775b10758b7 \ --hash=sha256:5bf5771b1db93beac965a7347dc81c675ec4090cb841e49d9d34637a25c30568 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-ordering==0.6 \ --hash=sha256:27fba3fc265f5d0f8597e7557885662c1bdc1969497cd58aff6ed21c3b617de2 \ --hash=sha256:3f314a178dbeb6777509548727dc69edf22d6d9a2867bf2d310ab85c403380b6 \ --hash=sha256:561ad653626bb171da78e682f6d39ac33bb13b3e272d406cd555adb6b006bda6 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-timeout==1.4.2 \ --hash=sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76 \ --hash=sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-xdist==3.8.0 \ --hash=sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88 \ --hash=sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1 - # via feast (setup.py) -python-bidi==0.6.6 \ - --hash=sha256:00081439e969c9d9d2ede8eccef4e91397f601931c4f02864edccb760c8f1db5 \ - --hash=sha256:00622f54a80826a918b22a2d6d5481bb3f669147e17bac85c81136b6ffbe7c06 \ - --hash=sha256:02255a04e26520b19081f7d378881b39050f5893e2fb4d65da81b849f58f4f76 \ - --hash=sha256:0781c3c63b4bc3b37273de2076cb9b875436ae19be0ff04752914d02a4375790 \ - --hash=sha256:07c9f000671b187319bacebb9e98d8b75005ccd16aa41b9d4411e66813c467bb \ - --hash=sha256:07db4c7da502593bd6e39c07b3a38733704070de0cbf92a7b7277b7be8867dd9 \ - --hash=sha256:09d4da6b5851d0df01d7313a11d22f308fdfb0e12461f7262e0f55c521ccc0f1 \ - --hash=sha256:0c298868017614d6b7e0e31293775ebe6622e87009d95e1ecd0abdc1fa5228a2 \ - --hash=sha256:0eb12b724cc99853e0e0425b54c1c2219492486afaca106c827204b4189504db \ - --hash=sha256:125a815f2b20313a2f6d331aa84abdd07de7d270985b056e6729390a4cda90df \ - --hash=sha256:166060a31c10aa3ffadd52cf10a3c9c2b8d78d844e0f2c5801e2ed511d3ec316 \ - --hash=sha256:183fee39bd2de787f632376bd5ba0d5f1daf6a09d3ebfaa211df25d62223e531 \ - --hash=sha256:1d627f8cfeba70fe4e0ec27b35615c938a483cbef2d9eb7e1e42400d2196019e \ - --hash=sha256:207b0a7082ec38045910d37700a0dd73c10d4ffccb22a4fd0391d7e9ce241672 \ - --hash=sha256:2150ac84f7b15f00f8cd9e29fee7edb4639b7ed2cd9e3d23e2dfd83098f719b7 \ - --hash=sha256:25fa21b46dc80ac7099d2dee424b634eb1f76b2308d518e505a626c55cdbf7b1 \ - --hash=sha256:27cf629a0ef983a25cfd62c6238ee1e742e35552409d5c1b43f6d22945adc4c2 \ - --hash=sha256:2d139bab64962731b5288edb1b6db76060c5a5183187efa590499951cd230b02 \ - --hash=sha256:33bd0ba5eedf18315a1475ac0f215b5134e48011b7320aedc2fb97df31d4e5bf \ - --hash=sha256:3428331e7ce0d58c15b5a57e18a43a12e28f8733086066e6fd75b0ded80e1cae \ - --hash=sha256:3564e574db1a0b3826ed6e646dc7206602189c31194d8da412007477ce653174 \ - --hash=sha256:35adfb9fed3e72b9043a5c00b6ab69e4b33d53d2d8f8b9f60d4df700f77bc2c0 \ - --hash=sha256:39eed023add8c53684f1de96cb72b4309cc4d412745f59b5d0dab48e6b88317b \ - --hash=sha256:3e17441d31a8665a44f5f42dba7646bbcd3c51ae6657dd019f6a7bb12618b12f \ - --hash=sha256:4142467ec0caa063aca894ca8f1e8a4d9ca6834093c06b0ad5e7aa98dc801079 \ - --hash=sha256:41fde9b4bb45c0e1b3283599e7539c82624ef8a8d3115da76b06160d923aab09 \ - --hash=sha256:43a0409570c618d93706dc875b1d33b4adfe67144f6f2ebeb32d85d8bbdb85ed \ - --hash=sha256:471c1a5fcdbb3de47377d74a7f1017216d9464e5428ca4e66f863e49dca73393 \ - --hash=sha256:485f2ee109e7aa73efc165b90a6d90da52546801413540c08b7133fe729d5e0a \ - --hash=sha256:490f8fe09ed423bfe00531f215e3b87e6000b8170408a0ead6ea5626f644b1d1 \ - --hash=sha256:493a844891e23264411b01df58ba77d5dbb0045da3787f4195f50a56bfb847d9 \ - --hash=sha256:4bb186c8da4bdc953893504bba93f41d5b412fd767ba5661ff606f22950ec609 \ - --hash=sha256:4eb3f28ca5e2f7238eaf67126c7634ec35603cbfbbe9b9b340ffee4a3314455f \ - --hash=sha256:4ecfd1d0f6d2927eb2114b55a63b298766b85fc9f0c9aaacb4e8df3e0468538a \ - --hash=sha256:4ff1eba0ff87e04bd35d7e164203ad6e5ce19f0bac0bdf673134c0b78d919608 \ - --hash=sha256:53122c3492fe3df871eb682c17eb848e24aa702946622ab78141c7027775519f \ - --hash=sha256:534bc7c84159b6e4b777f5fb9122902d6e19223c4242f5b94417de1afcfe2fd9 \ - --hash=sha256:5351efb4e86281eb26c420066fade935cd670c0c0960edc323b80d0b94a0bc19 \ - --hash=sha256:53d7d3a550d176df99dd0bb0cc2da16b40634f11c8b9f5715777441d679c0a62 \ - --hash=sha256:5506ba56380140b3cb3504029de014d21eb8874c5e081d88495f8775f6ed90bc \ - --hash=sha256:57c0ca449a116c4f804422111b3345281c4e69c733c4556fa216644ec9907078 \ - --hash=sha256:589c5b24a8c4b5e07a1e97654020734bf16ed01a4353911ab663a37aaf1c281d \ - --hash=sha256:5c9f798dd49b24bb1a9d90f065ef25c7bffa94c04c554f1fc02d0aea0a9b10b0 \ - --hash=sha256:61cf12f6b7d0b9bb37838a5f045e6acbd91e838b57f0369c55319bb3969ffa4d \ - --hash=sha256:6255ad385bb90aa39f8340967eef35657e52f8ed011773d37113cafa0ed5eefd \ - --hash=sha256:63f7a9eaec31078e7611ab958b6e18e796c05b63ca50c1f7298311dc1e15ac3e \ - --hash=sha256:646e83862dadfee00b75c93a930015e9f1cb924b26c34319a75aef65fcb3ddfa \ - --hash=sha256:686642a52acdeffb1d9a593a284d07b175c63877c596fa3ccceeb2649ced1dd8 \ - --hash=sha256:691822fac1d6f3caf12e667dd8b41956485c78b211032747c5f97822ba208726 \ - --hash=sha256:69c02316a4f72a168ea6f66b90d845086e2f2d2de6b08eb32c576db36582177c \ - --hash=sha256:6a4f4c664b2594d2d6be6a31c9254e784d6d5c1b17edfdccb5f0fac317a1cd5e \ - --hash=sha256:6c84d901fad5fe3b58a329c0b4a5c9d93a2d5430d150ad41f0e1165fc75ff439 \ - --hash=sha256:6cc626d2f77cac470b3167a28d4975744f3d99f5eaf8f5c2048ac9c0b9cba9dc \ - --hash=sha256:6dfa55611022f95058bb7deb2ac20755ae8abbe1104f87515f561e4a56944ba1 \ - --hash=sha256:76a1cd320993ba3e91a567e97f057a03f2c6b493096b3fff8b5630f51a38e7eb \ - --hash=sha256:7906229befa0cea2fe0278a934a27f657b68ce07a2606b1244f814a38b4ab42a \ - --hash=sha256:7d395e537a34d59e776fcdf50a50786d1a82084849d55cf644f4969ef8156643 \ - --hash=sha256:804c74d070f4e85c6976e55cdbb3f4ead5ec5d7ea0cfad8f18f5464be5174ec9 \ - --hash=sha256:825d15e547a9a2da5501966db672d6c8a5a063c041b2741ba32cc9775694b0ff \ - --hash=sha256:82c7f6bb3dfc4f61aecb2290f1ea24bb2450a5cbc94ee8abe5d6278b67859e0b \ - --hash=sha256:82e0befbc1078a964c6b6f2f7a616ae8015b52fdcd2f03979abf0fb1f2f18b48 \ - --hash=sha256:8706addd827840c2c3b3a9963060d9b979b43801cc9be982efa9644facd3ed26 \ - --hash=sha256:87a5489189b0a852da0129df77f0cc8e874b7b1ab1f968a209d340477906f076 \ - --hash=sha256:8b5f648ee8e9f4ac0400f71e671934b39837d7031496e0edde867a303344d758 \ - --hash=sha256:91a8cb8feac5d0042e2897042fe7bbbeab5dea1ab785f4b7d0c0bbbf6bc7aefd \ - --hash=sha256:91c12d58cec15385817f8b2c7c56de8e37523f05926f2de0e59199d3e50e1516 \ - --hash=sha256:92eb89f9d8aa0c877cb49fc6356c7f5566e819ea29306992e26be59a5ce468d7 \ - --hash=sha256:965e6f2182e7b9352f2d79221f6c49502a307a9778d7d87d82dc36bb1ffecbab \ - --hash=sha256:994534e47260d712c3b3291a6ab55b46cdbfd78a879ef95d14b27bceebfd4049 \ - --hash=sha256:9a9de76229ac22cb6bd40b56a8f7f0c42cbdff985dbd14b65bac955acf070594 \ - --hash=sha256:a138a7607b459414431a5cdcf5834624d6f87911a8863b51dd363a1e2e5744ab \ - --hash=sha256:a2a49b506ed21f762ebf332de6de689bc4912e24dcc3b85f120b34e5f01e541a \ - --hash=sha256:a525bcb77b8edbfdcf8b199dbed24556e6d1436af8f5fa392f6cdc93ed79b4af \ - --hash=sha256:a6ac2a3ec5ccc3736e29bb201f27bd33707bfde774d3d222826aa181552590b2 \ - --hash=sha256:ada1aecd32773c61b16f7c9f74d9ec1b57ea433e2083e08ca387c5cd4b0ceaed \ - --hash=sha256:af828457e46b31542569b4391014e6645023f6144de1dabf9fce7e9683235c25 \ - --hash=sha256:b144a1b8766fa6a536cc0feb6fdd29d91af7a82a0c09d89db5fc0b79d5678d7d \ - --hash=sha256:b271cd05cb40f47eb4600de79a8e47f8579d81ce35f5650b39b7860d018c3ece \ - --hash=sha256:b31f5562839e7ecea881ba337f9d39716e2e0e6b3ba395e824620ee5060050ff \ - --hash=sha256:b53b8b061b67908b5b436abede8c450c8d2fa965cb713d541688f552b4cfa3d3 \ - --hash=sha256:b65b4105998436405a3e6bca60cbf9714f6a08099b16c0cf4752a4a3a70eb45b \ - --hash=sha256:b8a83f28c104ef3b86ad60219d885b31728eb40c644f414f505068a6ecba3575 \ - --hash=sha256:b9498ead7f09eee272ff9c45900a8dcdc50a9558e126420a71d15774cc98bb44 \ - --hash=sha256:bbbcb28474b71e3ad05d8bd483348efe41fb7dfef6bd3046f3072baa0954d746 \ - --hash=sha256:bd5b3aa43d5222f1deef9894356a42f2443486501405977cda3aad0f23e20f9d \ - --hash=sha256:c07e4d6d8c8f574aa135436207a37bba522443a8490b0ba720b54d343dfde1a7 \ - --hash=sha256:c0e715b500b09cefccaddb7087978dcd755443b9620aa1cc7b441824253cf2b8 \ - --hash=sha256:c48a755ca8ba3f2b242d6795d4a60e83ca580cc4fa270a3aaa8af05d93b7ba7f \ - --hash=sha256:c4c0255940e6ff98fb05f9d5de3ffcaab7b60d821d4ca072b50c4f871b036562 \ - --hash=sha256:c4e08753d32d633f5ecb5eb02624272eeffaa6d5c6f4f9ddf012637bcaabfc0a \ - --hash=sha256:d1dcd7a82ae00b86821fce627e310791f56da90924f15877cfda844e340679de \ - --hash=sha256:d941a6a8a7159982d904982cfe0feb0a794913c5592d8137ccae0d518b2575e4 \ - --hash=sha256:da4949496e563b51f53ff34aad5a9f4c3aaf06f4180cf3bcb42bec649486c8f1 \ - --hash=sha256:dc8b0566cef5277f127a80e7546b52393050e5a572f08a352ca220e3f94807cf \ - --hash=sha256:de020488c334c31916ee7526c1a867bf632516c1c2a0420d14d10b79f00761c7 \ - --hash=sha256:e2f227ee564e0241e57269043bdfa13025d08d0919b349f5c686e8cfc0540dbf \ - --hash=sha256:e4a6251e212f828bb10ea69e0aa6b92b54f00bf56526b490fe890ca5f4333ec1 \ - --hash=sha256:e7e36601edda15e67527560b1c00108b0d27831260b6b251cf7c6dd110645c03 \ - --hash=sha256:e7edb0d1baf45c70384e700e10d723a13aabe116e14453cbf099eea4dd763e28 \ - --hash=sha256:e8bf3e396f9ebe8f4f81e92fa4c98c50160d60c58964b89c8ff4ee0c482befaa \ - --hash=sha256:e99e9ae745ba283f0230ac50af3f91657dd0b763778f88e4f0cbbc53b3e45d6e \ - --hash=sha256:edae3dd8e595a40d3cdd6ff8b6d9f3860cd17f674792ea05bba5bf5f1b36e5ab \ - --hash=sha256:f1020fcd3c8f1b93091730e3e16810d3741cbf69c6bacaa9d6a95fb15032848f \ - --hash=sha256:f1d3e139ca3963201994ee7f45d51dce6015166462cffa025daf95508547e503 \ - --hash=sha256:f60afe457a37bd908fdc7b520c07620b1a7cc006e08b6e3e70474025b4f5e5c7 \ - --hash=sha256:fb750d3d5ac028e8afd62d000928a2110dbca012fee68b1a325a38caa03dc50b \ - --hash=sha256:fd9bf9736269ad5cb0d215308fd44e1e02fe591cb9fbb7927d83492358c7ed5f \ - --hash=sha256:fe31aa2d2be1c79300bda36b1a3daf8c2dda963539e0c6eedeb9882fc8c15491 \ - --hash=sha256:fefea733a1acaaf0c0daba8ccd5e161b9419efb62d8f6f4c679c51ef754ee750 + # via feast (pyproject.toml) +python-bidi==0.6.11 \ + --hash=sha256:034090c597af250d699299d7e7f1e83eb016f9e47b3b707bd89ab2bdec77bce0 \ + --hash=sha256:0608bddcc1c53dfa5293499de13ca9935b31aa46d1c722c404a88c703d1a4e47 \ + --hash=sha256:0d339c4ebf3c2501d7569971a22e762fb45d8af7e19a0573afe260d69b35fb0d \ + --hash=sha256:0d496f9fc21b7457e12395e54088ab99776966c663b4cd4a74770c7a6418ab59 \ + --hash=sha256:19bae96ff1ee76b3a7dc962598b69426538e3021460cf85a4017437548ab6947 \ + --hash=sha256:19d13c70b1f7bb5dc69866a11624f3b78123807b4ede18f4f99f656ed86babec \ + --hash=sha256:1a639a2c62f825e72eb42901fd1c69296d09c192ae003195e6cf773f92d6bb42 \ + --hash=sha256:1ad5f712a32be30d28eb96e119e85818747a43cea253de6c3160b464b62a5619 \ + --hash=sha256:1b41cc6bc9ad78a12da5f987da15e931c771f18ceca58f2fe8ed50f253490a97 \ + --hash=sha256:1ffd728f1f7866ff7399906bbc17ef5cf010b90ce56a1e94937b28a1a4cf5a7d \ + --hash=sha256:20ab47a4098577fc9a82816c330c89ff597c31ba69c98bc6a1b6a5737b03de05 \ + --hash=sha256:239a00b2adb5f897d11d7b7a491d759fb9883e71a71cfd90c4147a733b4df4d3 \ + --hash=sha256:2b208fc26ab2c7adfd2c4d84753a5045f664e0bec19ca462af2406132e62f92e \ + --hash=sha256:2d6970b09f5a3102c0aa192f5258c585742ab4ebd94f637a635ad3448ccba567 \ + --hash=sha256:2ff75d1befc335cbe85f834e81554a024f94d9b5d1dd75a5bd99af81a1cb783c \ + --hash=sha256:302f4ca7dabfe447e707d40e98520551181036c15750bdd1e73292ad8b3d8e75 \ + --hash=sha256:30d543b5baf9fca5ef5ec95647aa07c5e38fc7fa0f18be0c61d1d6c0a1032c6c \ + --hash=sha256:337ec315f8b286399e411f5bfc339fc2124db07757409b0595ceb46de087c4cd \ + --hash=sha256:3611d13b53d4c899c4f2a7cd8eb897064e8b5546c3c5d1037dd6209c82858a27 \ + --hash=sha256:36f23f12d9b1c56ed8d82e11f56c6cba7bc3f614ee73374bc7772bb2270e0966 \ + --hash=sha256:380b70d615647646dbe06f7d95dc30b8fdc9b596dbfa1cd3814feb9805c0c8f2 \ + --hash=sha256:397f7f289eba6ce25d99dbea99f873d699bf9aa030074e7fb746d8f93c2fb6f9 \ + --hash=sha256:3bdb64ee0a74951465cd4a761e1029e73806ff43b2fe5be98643e52da5cabb66 \ + --hash=sha256:3da9e536546f7c62c0da595c8f71a096e0b9a80e94cfd0f329b7b200ef81e7d5 \ + --hash=sha256:41c7d1914c1f33954aa2ab0e2c309be5d1d4afc75ce524762eaab4dd825c5ab8 \ + --hash=sha256:43f3e81bdd36f49171b7de6cf471086df503c29555f6f7f035ebe8f8ec1da779 \ + --hash=sha256:490118f4af5a3535923c76be07e937ef1cb4cf84c489b37471aa95455fe923b3 \ + --hash=sha256:493655043ebea6fec0edb76053bdad46f1f7352a759ca4deb733f13b3c09839c \ + --hash=sha256:495a76c881d78ab87b57c5270679c9bc3c1de36d8c6596d5e3a5a1b5f9c57471 \ + --hash=sha256:4ac819cac1abb15486c48af3399a5c726e89f0977a3aff205ac162533186e756 \ + --hash=sha256:4ccf1fe9ecb3b02a1a11b103cd2557e2653d82c141b6e2dccec8177e6af5c4bb \ + --hash=sha256:4d00757ef7bbbf14d8628f9bdb6b0e168d5e7b03fec20da3226624f11bffce89 \ + --hash=sha256:51915502898c45e9cb36636e974aca068fc5cdb9f06b794f49abea5b12f02016 \ + --hash=sha256:519eb90bedb04b5cbba0bfc8062984a5ba054a019d81ada34053dc67397d61cb \ + --hash=sha256:539d99efe02f4981171ed57bbc085094ef780405ca14663550a56c6c3e265c34 \ + --hash=sha256:555cdf9303c40bae1ab512ca427f1f0316a574bc0a48db22eec76ec0fd1213cf \ + --hash=sha256:55f27bd2cdeca96f46448a741d393575fbedfc70e38e09dbb030ed98dba8cf2a \ + --hash=sha256:55fa5b22cc8c220a6daf077570a8e29d3b607bd978bd3dbc04b445d01a17de57 \ + --hash=sha256:565d819fddb2bbc58c42ca5c97d73da7567f181115011e8f04c76b9d08378dcd \ + --hash=sha256:56807e0dde88d5ab96880c9d668bda7bca1df83cd30d20cbff5d6b6e6c1e9276 \ + --hash=sha256:56dfa3c1c13cb89da39a4e8f2f9442e4291de01877511a13f830125d36e9ce5b \ + --hash=sha256:56f27c1edfd15c12c9c348378ccd79166930d720cf316b1181a0a0ade2146253 \ + --hash=sha256:57aa56b1eca5f63ebdd25fe8fad02eab7797fc45ad1efc5eed2a830e2bd2038d \ + --hash=sha256:5dd49c11ae87ee6ced68594faa78b34a89a1a3c43647fa6157765f9726f9daf3 \ + --hash=sha256:5f3e1743b2d43377c4da5d687a03430a27f5769e158a9f75a50b05e7e82f4d21 \ + --hash=sha256:5f9ed312d445a691c0afec995ca4f726abe357d8da500e389f77fb874fa7ef76 \ + --hash=sha256:60150bffbe43bfc1e356c0c2900ba643a02ce09c37bc0c553c4d83e5c3de63ad \ + --hash=sha256:60d9bf6c60c022657637f64897e63dc3f5b1c07cb7f0e1ead6150aff5150c5ab \ + --hash=sha256:619ee3fe03daec8d3ce12239f0c22455676a063b2bcde361caecd788fae5b8d5 \ + --hash=sha256:62a6b700f3d7a2c4d52a5dccca765711a2414e734360b00365e155698a26e461 \ + --hash=sha256:63d9dcc714d549a5118ebc93b7a7c903a0c1feec8e57f5fcacf98d984b76325b \ + --hash=sha256:6623683fe39b9fbf508e3069f17e8e9cab26143f9d9f89c8a8f45424c052df4f \ + --hash=sha256:68c3e570f2908ac39db0b269f5648e257fab5204344b9bf6dae80852ae4cbe1c \ + --hash=sha256:69d1f4ee17644e8aeac93a7238ee2f28d79b0180815441eb511b77e6585aa971 \ + --hash=sha256:6c92d1cad16f9ec2f2a3ae439a0bc3a8e4189ec227987bed03d5b4056d5eb9c5 \ + --hash=sha256:6ca92a4e460f7e25e434a6d7982d94a4765ca242f527995da70abb5a32003b8a \ + --hash=sha256:6d9ef69c108b31f38e1f281a55fdedad7774bc1e952a45c8c14a18e891eee397 \ + --hash=sha256:6dc112e2239f69913273cbb0c050ca816b145b32037d4266c430159c5ddcdab2 \ + --hash=sha256:721697187f4da67dafc26f63488f463fa35ff2de8668d959fda773cccdbef0eb \ + --hash=sha256:73c38c604bfc01647c69ce38e5cf8b4206e2ede91ca3eb8e5d79b7409f17e0b3 \ + --hash=sha256:74457b43db34f984252e915828b5d1a4042a771f44e853a5643506d01562eccb \ + --hash=sha256:7738cfc7ee9fcdff3fef76b40007982ce7704010a51df307c4a51d378f5ff1ba \ + --hash=sha256:777fb44a6f1e91d6adde36e815024c210b0208f35d00762880257b8fb04dc849 \ + --hash=sha256:788c11c84b520ea973992cc95751d56b783ff5857df504c1347d99cacd2fcfe7 \ + --hash=sha256:79df1099a08e53edb678236d4d76d8de4e3901bafc84ce1788b71f9b96547325 \ + --hash=sha256:7c4dc0d1328321aeb08054be654e1d39f171d5aae8dca931c9332860f18f57d7 \ + --hash=sha256:7cb34dd8d22ca1847653c0e1cbba8fd96de09c24b8c83b7805eab270c81e863a \ + --hash=sha256:8013c1e6267a929be98d10c3a617171709c8226bd33c6180ec65e560c35b6df8 \ + --hash=sha256:80cfe97e2d65981be877ae5bd2338c6919a7cc1171fc308c8b8c7c306ba6ffd8 \ + --hash=sha256:83c780f7e4c3dd3f020db75dc425567981e65c6d5571b3c0372203d0df92c834 \ + --hash=sha256:83ee87feb5eafc0442e1db0014dad20d52a2a7a140b6cddc8f7bc65918f0a7b4 \ + --hash=sha256:879c3fba3e7511c7d01020449d970ca7d6a593f20cfc47c014d3d229a38930b2 \ + --hash=sha256:8b6b7fce8f47578be9aebf5a0b6b2d6c157b4e97af7586ecf13bfca5d128deea \ + --hash=sha256:8eb09af209cd660fa9689f6ce9e61e73c8afa4829ae61801deea7f6e32263800 \ + --hash=sha256:8fbb6d222b50324fb9d49b6ff0f8566fa97b907a68c00e6622fcf34463104f4a \ + --hash=sha256:9435acc52438c3c8f5142b9a17a622927618e80a32eed707343bc375cb51cebe \ + --hash=sha256:946b7dbec4e64017680f1a66b3a8534659d889018ac83bd2abf958278e6f62b0 \ + --hash=sha256:94f7f66bd7680d3f2085adaeea91c01b3281a79bb5041db1a51cbe8af36a9d88 \ + --hash=sha256:959335cd3814cb767fb832c5c71cbc838ccd9231a812ef2cb43092a216a91d5b \ + --hash=sha256:969ee7db3e169fcc0b2d2d094826e03cc5798dfd6b3571a340ea883672396cb1 \ + --hash=sha256:9af2b5c26a3eb960699dff040535a86dc2c0f708087b2d63bcfd6452fe9d0664 \ + --hash=sha256:9d18916c97855d68515ca2b3581948b6d864c304cd386210ab5aff5e0fb9be2c \ + --hash=sha256:a1115f3f02eb836b39e0c986a6b9e92c4377a1e5a680409650b02ec6b1a795e6 \ + --hash=sha256:a1b5ee069001bf7f4fff109598a9396caa96bb35e1b906b2c6d1bab9f9b2c4bd \ + --hash=sha256:a4f4a0cd24c09df748bfdc207b089c00e7af19f3151063f4cd74ac658290186b \ + --hash=sha256:a52f7ad9ef9091e81869e5d255e796755ccf542ade14dda17647cb7d7ffe1b9c \ + --hash=sha256:a873e6f8ab28b5a56201d5cdb98f4c7d2ff88e636a58659c5ac271e287001658 \ + --hash=sha256:a9aa2890661b238730e680ccd6eb06f4da625b2dbc1730052dae6f2d88957192 \ + --hash=sha256:ab8af1cc29c3f400a0f781d50f8ab52a8c63adf2fcef16f14641cd97e06a80e0 \ + --hash=sha256:acccb6d90e694684db2d314db2e2b0d3b8949bf1cfaec6d9808a8889f548add7 \ + --hash=sha256:ad5d9b8e8a6c330208eba413db506de58f21dbf88a1f1d5d75ef5f9e0e714adf \ + --hash=sha256:af7711cc6eeeadcb1aae877e0736a68e111c73a29562da6106c5e2fbb4dd83b2 \ + --hash=sha256:b2c759e13ebb81edaac3041697328bb1ca8433b55281723879f6b54e74881240 \ + --hash=sha256:b433840a924c8788f0abbda15d22c71dc636e2078d7d3ba39369cebe4bef74b8 \ + --hash=sha256:bde89739a979d9eb3ac48c4882c8a42dd528a7708b0faa99b90b551588bd5f8d \ + --hash=sha256:c17393753ddb77377f754ba2a88cacfe3056040a8a5dff8d43f9d5e51bbe1edc \ + --hash=sha256:c26cd9d81f820159026b1c99905ca12bf13892e3f6a9303359fef42fe6f39e50 \ + --hash=sha256:c6aca20472d7040bc1f0aadb510cb20d40b3d6caa4c8bf82d075ba2650a41652 \ + --hash=sha256:c6b3c853f99172ef22e5a16c8114cf243e351c8e70f72a894164088c2c99d9cb \ + --hash=sha256:caee7ee3662eab1411b44fef8571b87273cb5235061d7463ebd10e412ac07986 \ + --hash=sha256:cd564b8c583eba2d230d02d0467fd840045f08856b6524555cfff7f32af63c72 \ + --hash=sha256:ce2ef68dce82ae4067cf28910224d77b45eb2c2b3904db6eb670d1cddb8d0eb2 \ + --hash=sha256:cf4e88a6fec81b7155a487cbbea7753a3d9a76dc4d391b4f8958b37227ef2c12 \ + --hash=sha256:d14f2d400c75e07d1154299463a3d4d14aa5565b05088b2d9b314ccd9fd6dc3a \ + --hash=sha256:d52ec8ccdc2fd5c61749d876a9d1eb0ea6543c1676722e1e3fb9d7800852131c \ + --hash=sha256:d5695d87969fed5b3799b8a98848cb04fe2873fded46efe9f3f2f341efd1b829 \ + --hash=sha256:d7291e13496cb74fc1b71f7f1e3628586afefa531102bb4fa7af9c2d543efc3c \ + --hash=sha256:d73a821873c52635321196cfb8d3a231d7917ca284cf2bcd9422f6deb19db7ca \ + --hash=sha256:da51a3a2940478219f19249fcf7cd45e8ddc197982be22efa43c4763e9d2eb57 \ + --hash=sha256:dc69d594682fbedec7d0cc81cd4d5f14fb40b6b365587238fafb91509cfea020 \ + --hash=sha256:dfbb9ba8343a60daf4ced67c11d551dafe9a3c94892c326e4c216fa2e6eca802 \ + --hash=sha256:e142820537e08d567c2c017e26df05f88856a03f0f3948209cfcdc39617df363 \ + --hash=sha256:e4ebc24ac38e50676f65daf7ba6c568789660cb60d6dcf2606d4310dba826721 \ + --hash=sha256:e6c474618a7b6a50c10007f8a9edb50def6d98d297a07a34dc2fb82c344f2b8b \ + --hash=sha256:f0ee4e80dd3b2a46868dcc73e00aa59f99c0e1de54a2ba783faf0fddba2ccce2 \ + --hash=sha256:f237ebb570fd8bbe479b6967374d82b7f0b26f9452c276bdd5f793d83e7062bd \ + --hash=sha256:f400a30573774a1e90c0d50d43c35d9c004afcf53de801bbfd259f84ea80f31c \ + --hash=sha256:f563d20481f7d316adf605bb94d5b7182acecdbc4d431d60473e9b1d526d0210 \ + --hash=sha256:f7a8429d0d65232e314b4f494825c1205abcc3039f42bf7da80424a15b731709 \ + --hash=sha256:f8655ac559dcfce7179b150ebcc207982d4d60e67b3089b2032eed3a0a78c07a \ + --hash=sha256:f99162d6c6c9522c46eb213f1bc932829c2602131676c92f081cb865b8ef6784 \ + --hash=sha256:fa2848c3116d619870d114471fcd4a9f1aa43587a002111d7af1e6582f1b57e9 \ + --hash=sha256:fc3b0a6e2460f68de9ab98f71e3098bb21bb984563417ad104dec7ab08ebcadc \ + --hash=sha256:fccd1808bb427d6a6d34168461bef551faa93ce3dda489fcef9073bcd9b34a5e \ + --hash=sha256:ff675d036823bff05a2e0f8cdb13f5414274ff517d13b0d57c15527015eb6acb # via easyocr python-dateutil==2.9.0 \ --hash=sha256:78e73e19c63f5b20ffa567001531680d939dc042bf7850431877645523c66709 \ --hash=sha256:cbf2f1da5e6083ac2fbfd4da39a25f34312230110440f424a14c7558bb85d82e # via - # feast (setup.py) + # feast (pyproject.toml) # aiobotocore # arrow # botocore # elasticsearch # google-cloud-bigquery + # graphene # great-expectations # ibis-framework # jupyter-client # kubernetes + # matplotlib # moto + # openlineage-python # pandas + # strictyaml # trino python-docx==1.2.0 \ --hash=sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7 \ --hash=sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce # via docling -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via - # environs # pydantic-settings + # pymilvus # testcontainers # uvicorn -python-json-logger==4.0.0 \ - --hash=sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2 \ - --hash=sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f +python-json-logger==4.1.0 \ + --hash=sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2 \ + --hash=sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195 # via jupyter-events python-keycloak==4.2.2 \ --hash=sha256:1d43a1accd4a038ed39317fcb3eb78211df6c75bbcbc4c482c99ee76327136f2 \ --hash=sha256:5137fd87c69031a372a578df96bae96b9aead2c9dad976613bc978e9e0246a1e - # via feast (setup.py) -python-multipart==0.0.20 \ - --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \ - --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13 + # via feast (pyproject.toml) +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 # via mcp python-pptx==1.0.2 \ --hash=sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba \ --hash=sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095 # via docling -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via - # clickhouse-connect # great-expectations - # ibis-framework # pandas # snowflake-connector-python # trino @@ -4338,19 +5149,21 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # accelerate # dask # datasets # docling-core # easyocr # huggingface-hub - # ibis-substrait # jupyter-events # kubernetes + # mlflow-skinny + # openlineage-python # pre-commit # ray # responses + # scylla-driver # timm # transformers # uvicorn @@ -4451,35 +5264,34 @@ pyzmq==27.1.0 \ # ipykernel # jupyter-client # jupyter-server -qdrant-client==1.15.1 \ - --hash=sha256:2b975099b378382f6ca1cfb43f0d59e541be6e16a5892f282a4b8de7eff5cb63 \ - --hash=sha256:631f1f3caebfad0fd0c1fba98f41be81d9962b7bf3ca653bed3b727c0e0cbe0e - # via feast (setup.py) -ray==2.50.0 \ - --hash=sha256:0062a7fa563f424665b1cdec0fee9c70708da807ecc5916c49925a21438897d0 \ - --hash=sha256:03b65fce8ede26e2a52365f20967c09b3a35e9d7d2b856645dcc680435a47e43 \ - --hash=sha256:135c70c92bd3c3050441c905576e871b879dca486649218c45241a3ec4572399 \ - --hash=sha256:214a006489419c786715cb2a81b3c52c88e08ea3295f48ba97ab69b888d8f818 \ - --hash=sha256:240ff5ecdf11b9e98160961c31dd7415ad08477ba9073597db67b8a9b152c33f \ - --hash=sha256:3e6006be94c5686ca476c1125b9577b41a9c8b33703e667500b51be246b2a9db \ - --hash=sha256:619b2e6b880d5af126df1dfcdaa27314edd43d2c6d6315e01d1e311839abffe4 \ - --hash=sha256:645295d605caf2606310f9fb62ba83e24a23d8f4329b0405ccff99d0055b7e05 \ - --hash=sha256:7c230fed59628ade5d75c57801844d437ef5fabf6015982f9bfe8647924d3e63 \ - --hash=sha256:816a438260a4323fc67b81197e722f36ac8bd39501cfa61d93f88f1870c02ac5 \ - --hash=sha256:838d2ca6065c1255b6c70f1d2041ca0b6ab3abab9083303b65d7b3138c022de2 \ - --hash=sha256:ae61e2db0a71a47d36bae6d5dece4d1b1e9661a8529204e03a54679fca82dfa1 \ - --hash=sha256:b139239f30140e982f7bc5fb1cc26948a0a76c980eba371efdde3fb045d0a7e4 \ - --hash=sha256:b164d6af420c7a38a8323f8ff3788b8c57cef4679b3488354a12371aac01874e \ - --hash=sha256:b725bd053fa010e9cc01b51d5ac989c10c7641638e3f9c4719d64e5143d4320f \ - --hash=sha256:c3da96695e3f63bd0eecad6d30ec7cf6d0309a8738e31ac270957812bcc1cd8b \ - --hash=sha256:c831bffba0f6f0dfa25b65c5b800022c7234cbec1b98a8ede17f97cc825f700d \ - --hash=sha256:cfde02a1cb114481758abfe40b4b8c31afe199150887372251466256756911cb \ - --hash=sha256:e683981b51ff534c09a48b3a5b65fc61886de3da8aae8738239591dab2b67ce5 - # via feast (setup.py) -redis==4.6.0 \ - --hash=sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d \ - --hash=sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c - # via feast (setup.py) +qdrant-client==1.18.0 \ + --hash=sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd \ + --hash=sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4 + # via feast (pyproject.toml) +ray==2.56.1 \ + --hash=sha256:44bc0000c5bfad85b2ff6e0ef91e95f901d1a2d2fdd72f94f08a046eb494cd61 \ + --hash=sha256:49aa1f8aa1799a6b63adc8e4edd600b949b2bb648461abedcbcccd56c8ef0f82 \ + --hash=sha256:4df45f33cc176b11c69191efdcfb4aceaeea22fecfa784f6485b6504ef8c8b26 \ + --hash=sha256:58b8c037a9f2b7b7866439cb6fe19d452ba3961f2b62d0a247dc3adf2ca45265 \ + --hash=sha256:5eebd776cd461edebc5874d2dfba3005147b6b45645d8a16a6549311c5efffea \ + --hash=sha256:7005a13785c7478c3d183b6f1e7a344c069c4c4fb187707c4fc31ca8c84d8d9f \ + --hash=sha256:76f6268a669c1d9910f1d7b73903de3ede9850ed94d3e28318d495559bd37d0e \ + --hash=sha256:7fdc47de4e230f0db7c6c668a9e161f864dac548bd32230986e0b0c36e386eb5 \ + --hash=sha256:8052573ee5ef8c4fdd7aeb6a257c80542e69c48f3f6d117101f95c970ffdc7e2 \ + --hash=sha256:81f2db202cc31bc3f5c4acf9ef154d10f1f39ece602d9d2d3108875c49bf01c3 \ + --hash=sha256:8fdd6b096215906cf1f9acdc7898c9d6140606f2d27245778b8385a9f19e6cb0 \ + --hash=sha256:93dedab658334af81877b6ed840c4e5f85e2e0b1b3641b20eaaee37cad835cc6 \ + --hash=sha256:ad87fa57c5c69c77edab14820226868842648de7181894dba4fb1e874a0ab27e \ + --hash=sha256:ae91fe578fadea38c13a208a0fec27e1ced238ccdf5fff95ef3e30ac3071dec9 \ + --hash=sha256:c102fb09e82c2e10828d5c21cfb744e27bca690b287c7d0a77d0d883c0c86907 \ + --hash=sha256:e5d3173696831134c76bd09451dfe95c32d72c271253b0d6b09d2df9994aa660 \ + --hash=sha256:e7003a47a42ef2ad33ec0b34dc5b6afb03f63fe59465e6f4c8f6d05492d9e4a6 \ + --hash=sha256:ea372c7f95b14f1f76f0bdd2abc9602c56e88bc30699d2228f78133e7749b2f1 + # via feast (pyproject.toml) +redis==7.4.1 \ + --hash=sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e \ + --hash=sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + # via feast (pyproject.toml) referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 @@ -4487,132 +5299,132 @@ referencing==0.37.0 \ # jsonschema # jsonschema-specifications # jupyter-events -regex==2025.9.18 \ - --hash=sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5 \ - --hash=sha256:039a9d7195fd88c943d7c777d4941e8ef736731947becce773c31a1009cb3c35 \ - --hash=sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282 \ - --hash=sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef \ - --hash=sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41 \ - --hash=sha256:065b6956749379d41db2625f880b637d4acc14c0a4de0d25d609a62850e96d36 \ - --hash=sha256:0716e4d6e58853d83f6563f3cf25c281ff46cf7107e5f11879e32cb0b59797d9 \ - --hash=sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3 \ - --hash=sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788 \ - --hash=sha256:0cc3521060162d02bd36927e20690129200e5ac9d2c6d32b70368870b122db25 \ - --hash=sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac \ - --hash=sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56 \ - --hash=sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946 \ - --hash=sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203 \ - --hash=sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788 \ - --hash=sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12 \ - --hash=sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e \ - --hash=sha256:16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442 \ - --hash=sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d \ - --hash=sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af \ - --hash=sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3 \ - --hash=sha256:1e85f73ef7095f0380208269055ae20524bfde3f27c5384126ddccf20382a638 \ - --hash=sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23 \ - --hash=sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4 \ - --hash=sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494 \ - --hash=sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1 \ - --hash=sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2 \ - --hash=sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096 \ - --hash=sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5 \ - --hash=sha256:34d674cbba70c9398074c8a1fcc1a79739d65d1105de2a3c695e2b05ea728251 \ - --hash=sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d \ - --hash=sha256:385c9b769655cb65ea40b6eea6ff763cbb6d69b3ffef0b0db8208e1833d4e746 \ - --hash=sha256:3acc471d1dd7e5ff82e6cacb3b286750decd949ecd4ae258696d04f019817ef8 \ - --hash=sha256:3b524d010973f2e1929aeb635418d468d869a5f77b52084d9f74c272189c251d \ - --hash=sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77 \ - --hash=sha256:3dbcfcaa18e9480669030d07371713c10b4f1a41f791ffa5cb1a99f24e777f40 \ - --hash=sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e \ - --hash=sha256:431bd2a8726b000eb6f12429c9b438a24062a535d06783a93d2bcbad3698f8a8 \ - --hash=sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e \ - --hash=sha256:47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450 \ - --hash=sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad \ - --hash=sha256:4a12a06c268a629cb67cc1d009b7bb0be43e289d00d5111f86a2efd3b1949444 \ - --hash=sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f \ - --hash=sha256:4baeb1b16735ac969a7eeecc216f1f8b7caf60431f38a2671ae601f716a32d25 \ - --hash=sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb \ - --hash=sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352 \ - --hash=sha256:50e8290707f2fb8e314ab3831e594da71e062f1d623b05266f8cfe4db4949afd \ - --hash=sha256:51076980cd08cd13c88eb7365427ae27f0d94e7cebe9ceb2bb9ffdae8fc4d82a \ - --hash=sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a \ - --hash=sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3 \ - --hash=sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425 \ - --hash=sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379 \ - --hash=sha256:5db95ff632dbabc8c38c4e82bf545ab78d902e81160e6e455598014f0abe66b9 \ - --hash=sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d \ - --hash=sha256:6479d5555122433728760e5f29edb4c2b79655a8deb681a141beb5c8a025baea \ - --hash=sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d \ - --hash=sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d \ - --hash=sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743 \ - --hash=sha256:6b498437c026a3d5d0be0020023ff76d70ae4d77118e92f6f26c9d0423452446 \ - --hash=sha256:726177ade8e481db669e76bf99de0b278783be8acd11cef71165327abd1f170a \ - --hash=sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742 \ - --hash=sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47 \ - --hash=sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164 \ - --hash=sha256:7e2b414deae99166e22c005e154a5513ac31493db178d8aec92b3269c9cce8c9 \ - --hash=sha256:828446870bd7dee4e0cbeed767f07961aa07f0ea3129f38b3ccecebc9742e0b8 \ - --hash=sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a \ - --hash=sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0 \ - --hash=sha256:87f681bfca84ebd265278b5daa1dcb57f4db315da3b5d044add7c30c10442e61 \ - --hash=sha256:8900b3208e022570ae34328712bef6696de0804c122933414014bae791437ab2 \ - --hash=sha256:895197241fccf18c0cea7550c80e75f185b8bd55b6924fcae269a1a92c614a07 \ - --hash=sha256:8e5f41ad24a1e0b5dfcf4c4e5d9f5bd54c895feb5708dd0c1d0d35693b24d478 \ - --hash=sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea \ - --hash=sha256:9098e29b3ea4ffffeade423f6779665e2a4f8db64e699c0ed737ef0db6ba7b12 \ - --hash=sha256:90b6b7a2d0f45b7ecaaee1aec6b362184d6596ba2092dd583ffba1b78dd0231c \ - --hash=sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783 \ - --hash=sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7 \ - --hash=sha256:a021217b01be2d51632ce056d7a837d3fa37c543ede36e39d14063176a26ae29 \ - --hash=sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68 \ - --hash=sha256:a295916890f4df0902e4286bc7223ee7f9e925daa6dcdec4192364255b70561a \ - --hash=sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e \ - --hash=sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b \ - --hash=sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368 \ - --hash=sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282 \ - --hash=sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306 \ - --hash=sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01 \ - --hash=sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95 \ - --hash=sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb \ - --hash=sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29 \ - --hash=sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a \ - --hash=sha256:c204e93bf32cd7a77151d44b05eb36f469d0898e3fba141c026a26b79d9914a0 \ - --hash=sha256:c28821d5637866479ec4cc23b8c990f5bc6dd24e5e4384ba4a11d38a526e1414 \ - --hash=sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4 \ - --hash=sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129 \ - --hash=sha256:c81b892af4a38286101502eae7aec69f7cd749a893d9987a92776954f3943408 \ - --hash=sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb \ - --hash=sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6 \ - --hash=sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f \ - --hash=sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773 \ - --hash=sha256:d4a691494439287c08ddb9b5793da605ee80299dd31e95fa3f323fac3c33d9d4 \ - --hash=sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730 \ - --hash=sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a \ - --hash=sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571 \ - --hash=sha256:dfbde38f38004703c35666a1e1c088b778e35d55348da2b7b278914491698d6a \ - --hash=sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459 \ - --hash=sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90 \ - --hash=sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab \ - --hash=sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f \ - --hash=sha256:ef8d10cc0989565bcbe45fb4439f044594d5c2b8919d3d229ea2c4238f1d55b0 \ - --hash=sha256:f04d2f20da4053d96c08f7fde6e1419b7ec9dbcee89c96e3d731fca77f411b95 \ - --hash=sha256:f2f422214a03fab16bfa495cfec72bee4aaa5731843b771860a471282f1bf74f \ - --hash=sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b \ - --hash=sha256:f5cca697da89b9f8ea44115ce3130f6c54c22f541943ac8e9900461edc2b8bd4 \ - --hash=sha256:fb137ec7c5c54f34a25ff9b31f6b7b0c2757be80176435bf367111e3f71d72df \ - --hash=sha256:fb967eb441b0f15ae610b7069bdb760b929f267efbf522e814bbbfffdf125ce2 \ - --hash=sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2 - # via - # feast (setup.py) +regex==2026.7.19 \ + --hash=sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0 \ + --hash=sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6 \ + --hash=sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62 \ + --hash=sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af \ + --hash=sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc \ + --hash=sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13 \ + --hash=sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd \ + --hash=sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951 \ + --hash=sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc \ + --hash=sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511 \ + --hash=sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12 \ + --hash=sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518 \ + --hash=sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db \ + --hash=sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae \ + --hash=sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009 \ + --hash=sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986 \ + --hash=sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1 \ + --hash=sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a \ + --hash=sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2 \ + --hash=sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0 \ + --hash=sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78 \ + --hash=sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d \ + --hash=sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4 \ + --hash=sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0 \ + --hash=sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11 \ + --hash=sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52 \ + --hash=sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e \ + --hash=sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902 \ + --hash=sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11 \ + --hash=sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6 \ + --hash=sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba \ + --hash=sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e \ + --hash=sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac \ + --hash=sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939 \ + --hash=sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb \ + --hash=sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc \ + --hash=sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095 \ + --hash=sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b \ + --hash=sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b \ + --hash=sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220 \ + --hash=sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c \ + --hash=sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae \ + --hash=sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3 \ + --hash=sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44 \ + --hash=sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665 \ + --hash=sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5 \ + --hash=sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97 \ + --hash=sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218 \ + --hash=sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864 \ + --hash=sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e \ + --hash=sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4 \ + --hash=sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda \ + --hash=sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459 \ + --hash=sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18 \ + --hash=sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3 \ + --hash=sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5 \ + --hash=sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a \ + --hash=sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035 \ + --hash=sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa \ + --hash=sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5 \ + --hash=sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78 \ + --hash=sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20 \ + --hash=sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a \ + --hash=sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a \ + --hash=sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a \ + --hash=sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965 \ + --hash=sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5 \ + --hash=sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797 \ + --hash=sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276 \ + --hash=sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c \ + --hash=sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547 \ + --hash=sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9 \ + --hash=sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d \ + --hash=sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1 \ + --hash=sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68 \ + --hash=sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a \ + --hash=sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd \ + --hash=sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c \ + --hash=sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6 \ + --hash=sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82 \ + --hash=sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7 \ + --hash=sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15 \ + --hash=sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e \ + --hash=sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38 \ + --hash=sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96 \ + --hash=sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2 \ + --hash=sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8 \ + --hash=sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732 \ + --hash=sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966 \ + --hash=sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053 \ + --hash=sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3 \ + --hash=sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0 \ + --hash=sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f \ + --hash=sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e \ + --hash=sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327 \ + --hash=sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac \ + --hash=sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6 \ + --hash=sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2 \ + --hash=sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a \ + --hash=sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435 \ + --hash=sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5 \ + --hash=sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d \ + --hash=sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312 \ + --hash=sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b \ + --hash=sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40 \ + --hash=sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974 \ + --hash=sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404 \ + --hash=sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff \ + --hash=sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf \ + --hash=sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175 \ + --hash=sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da \ + --hash=sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d \ + --hash=sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1 \ + --hash=sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2 + # via + # feast (pyproject.toml) # parsimonious # transformers -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # feast (setup.py) + # feast (pyproject.toml) # azure-core + # databricks-sdk # datasets # docker # docling @@ -4624,8 +5436,12 @@ requests==2.32.5 \ # huggingface-hub # jupyterlab-server # kubernetes + # mlflow-skinny # moto # msal + # openlineage-python + # pyiceberg + # pymilvus # python-keycloak # ray # requests-oauthlib @@ -4646,9 +5462,9 @@ requests-toolbelt==1.0.0 \ --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 # via python-keycloak -responses==0.25.8 \ - --hash=sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c \ - --hash=sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4 +responses==0.26.2 \ + --hash=sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364 \ + --hash=sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8 # via moto rfc3339-validator==0.1.4 \ --hash=sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b \ @@ -4666,176 +5482,133 @@ rfc3987-syntax==1.1.0 \ --hash=sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f \ --hash=sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d # via jsonschema -rich==13.9.4 \ - --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ - --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 +rich==14.3.4 \ + --hash=sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952 \ + --hash=sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9 # via # fastapi-mcp # ibis-framework + # pyiceberg # typer -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==0.30.0 \ + --hash=sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f \ + --hash=sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136 \ + --hash=sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3 \ + --hash=sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7 \ + --hash=sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65 \ + --hash=sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4 \ + --hash=sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169 \ + --hash=sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf \ + --hash=sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4 \ + --hash=sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2 \ + --hash=sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c \ + --hash=sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4 \ + --hash=sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3 \ + --hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \ + --hash=sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7 \ + --hash=sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89 \ + --hash=sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85 \ + --hash=sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6 \ + --hash=sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa \ + --hash=sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb \ + --hash=sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6 \ + --hash=sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87 \ + --hash=sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856 \ + --hash=sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4 \ + --hash=sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f \ + --hash=sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53 \ + --hash=sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229 \ + --hash=sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad \ + --hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \ + --hash=sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db \ + --hash=sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038 \ + --hash=sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27 \ + --hash=sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00 \ + --hash=sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18 \ + --hash=sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083 \ + --hash=sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c \ + --hash=sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738 \ + --hash=sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898 \ + --hash=sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e \ + --hash=sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7 \ + --hash=sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08 \ + --hash=sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6 \ + --hash=sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551 \ + --hash=sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e \ + --hash=sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288 \ + --hash=sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df \ + --hash=sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0 \ + --hash=sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2 \ + --hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \ + --hash=sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0 \ + --hash=sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464 \ + --hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \ + --hash=sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404 \ + --hash=sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7 \ + --hash=sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139 \ + --hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \ + --hash=sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb \ + --hash=sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15 \ + --hash=sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff \ + --hash=sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed \ + --hash=sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6 \ + --hash=sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e \ + --hash=sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95 \ + --hash=sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d \ + --hash=sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950 \ + --hash=sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3 \ + --hash=sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5 \ + --hash=sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97 \ + --hash=sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e \ + --hash=sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e \ + --hash=sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b \ + --hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \ + --hash=sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad \ + --hash=sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8 \ + --hash=sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425 \ + --hash=sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221 \ + --hash=sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d \ + --hash=sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825 \ + --hash=sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51 \ + --hash=sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e \ + --hash=sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f \ + --hash=sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8 \ + --hash=sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f \ + --hash=sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d \ + --hash=sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07 \ + --hash=sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877 \ + --hash=sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31 \ + --hash=sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58 \ + --hash=sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94 \ + --hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \ + --hash=sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000 \ + --hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \ + --hash=sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1 \ + --hash=sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7 \ + --hash=sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7 \ + --hash=sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40 \ + --hash=sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d \ + --hash=sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0 \ + --hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \ + --hash=sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f \ + --hash=sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a \ + --hash=sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7 \ + --hash=sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419 \ + --hash=sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 \ + --hash=sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a \ + --hash=sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9 \ + --hash=sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be \ + --hash=sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed \ + --hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a \ + --hash=sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d \ + --hash=sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324 \ + --hash=sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f \ + --hash=sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2 \ + --hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f \ + --hash=sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5 # via # jsonschema # referencing -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via google-auth rtree==1.4.1 \ --hash=sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c \ --hash=sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0 \ @@ -4853,47 +5626,48 @@ ruamel-yaml==0.17.17 \ --hash=sha256:9751de4cbb57d4bfbf8fc394e125ed4a2f170fbff3dc3d78abf50be85924f8be \ --hash=sha256:9af3ec5d7f8065582f3aa841305465025d0afd26c5fb54e15b964e11838fc74f # via great-expectations -ruff==0.14.0 \ - --hash=sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206 \ - --hash=sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2 \ - --hash=sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7 \ - --hash=sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f \ - --hash=sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3 \ - --hash=sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57 \ - --hash=sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02 \ - --hash=sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8 \ - --hash=sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543 \ - --hash=sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d \ - --hash=sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8 \ - --hash=sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296 \ - --hash=sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd \ - --hash=sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7 \ - --hash=sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2 \ - --hash=sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e \ - --hash=sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e \ - --hash=sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730 \ - --hash=sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c - # via feast (setup.py) -s3transfer==0.13.1 \ - --hash=sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724 \ - --hash=sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf +ruff==0.16.0 \ + --hash=sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17 \ + --hash=sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d \ + --hash=sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af \ + --hash=sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09 \ + --hash=sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522 \ + --hash=sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472 \ + --hash=sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0 \ + --hash=sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b \ + --hash=sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9 \ + --hash=sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213 \ + --hash=sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb \ + --hash=sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed \ + --hash=sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717 \ + --hash=sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a \ + --hash=sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81 \ + --hash=sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982 \ + --hash=sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e \ + --hash=sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4 + # via feast (pyproject.toml) +s3transfer==0.19.2 \ + --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \ + --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25 # via boto3 -safetensors[torch]==0.6.2 \ - --hash=sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd \ - --hash=sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9 \ - --hash=sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f \ - --hash=sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19 \ - --hash=sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1 \ - --hash=sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a \ - --hash=sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba \ - --hash=sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce \ - --hash=sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c \ - --hash=sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1 \ - --hash=sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b \ - --hash=sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5 \ - --hash=sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac \ - --hash=sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7 \ - --hash=sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda +safetensors[torch]==0.8.0 \ + --hash=sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358 \ + --hash=sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f \ + --hash=sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d \ + --hash=sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d \ + --hash=sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0 \ + --hash=sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc \ + --hash=sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235 \ + --hash=sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98 \ + --hash=sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4 \ + --hash=sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846 \ + --hash=sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca \ + --hash=sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0 \ + --hash=sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25 \ + --hash=sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452 \ + --hash=sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d \ + --hash=sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78 \ + --hash=sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774 # via # accelerate # docling-ibm-models @@ -4955,7 +5729,10 @@ scikit-learn==1.7.2 \ --hash=sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b \ --hash=sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615 \ --hash=sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33 - # via feast (setup.py) + # via + # feast (pyproject.toml) + # mlflow + # sentence-transformers scipy==1.15.3 \ --hash=sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477 \ --hash=sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c \ @@ -5007,29 +5784,71 @@ scipy==1.15.3 \ # docling # easyocr # great-expectations + # mlflow # scikit-image # scikit-learn -semchunk==2.2.2 \ - --hash=sha256:940e89896e64eeb01de97ba60f51c8c7b96c6a3951dfcf574f25ce2146752f52 \ - --hash=sha256:94ca19020c013c073abdfd06d79a7c13637b91738335f3b8cdb5655ee7cc94d2 + # sentence-transformers +scylla-driver==3.29.11 \ + --hash=sha256:0a93dd4af8995ac08335e581aca393a2bca64c3d4748ec359b9400797f2a7449 \ + --hash=sha256:17b19807a8690a4df2e52bb23327a746715e7076f5f28b45a6ce4556b2a00dd7 \ + --hash=sha256:1ea1fd011d8cf74b706c1b6ae982437597abf545e6d53f3a79905c97831db16a \ + --hash=sha256:241bffc347888dbeae48d842e58a7d20915ee518beb46467d9b7cd5a0ad501ed \ + --hash=sha256:2546a6646eead8225c8a0440c744c50ca85147fe61a712fb68e08be11ab21560 \ + --hash=sha256:2a4116f2da833f76db6e59b4c0534b72291cac478ca4b5944abbdd52b1635d94 \ + --hash=sha256:3094c90a49c172b1c09bbf44b46ea0c2c284e35cfd0222bc0062492de05131c0 \ + --hash=sha256:39f48ebe02d17e7cc6f3ba34f9748c971932eaa11d6557ab7d5fae1631987a55 \ + --hash=sha256:45050fd2028f07e236b7b3a555d4d17a1d88ea8ba8b5609ae1d14730a65b1e4f \ + --hash=sha256:57e8fffaf0803c0a406cc0e331a738c5d305fb28c9f5834d29c077fa134a8820 \ + --hash=sha256:617a8c459b30403002d38d7f99e3ed160da7671c7e62b0647e5fbd89539c6aff \ + --hash=sha256:677b5b0312275a958c80dbd3d1686e4a60c805ddae290a4b56551245c0a07a95 \ + --hash=sha256:6ee63f2503590f683e4b6c39c0070f567cc825fabd20f74cfe053d926a930ffc \ + --hash=sha256:70dda8384ae23472cfdf6c798d20e59639aa1e4796937417088151adb4acf4a9 \ + --hash=sha256:74668f6d4bca9f05f0826b8b464f363e7c1c7ac1cbbf373bae638bdc68780b8e \ + --hash=sha256:8072842efb2ff7c9761563485156fb0b6a11c68b028e32b6a807a70d226dbbbd \ + --hash=sha256:844e61a13edf0d12f95a64f6dd8a9c0fd0b0919570e10631e33c640acff59db7 \ + --hash=sha256:8ed1b2ec3634d3b25609a3a587de420a41b435f497e963434a8b5b42fa32c88e \ + --hash=sha256:9fb9bc8ae114434c3418ca8ba0bca00fb7afd4943a52694b5bde3f8681a0eea9 \ + --hash=sha256:a8f644d73713be556e99d6355cd2de41503ee50bff990865d71887e1f9f18b46 \ + --hash=sha256:abb5f98b4efbcb6257d1b97eea9c2bb95ea509e8983c2b8e021810ef460cb390 \ + --hash=sha256:acb38ed6acae2185bdef6834569bb95acd858ec91a8269c555e912127946e998 \ + --hash=sha256:ae36b731481c9ded343811132bcce4f68c9f2de8af8a51248fcea4d9c3b36d52 \ + --hash=sha256:b5a4851afb2a6d8410f55e7344b0589f71289338ff35c6901135c1a683381149 \ + --hash=sha256:bf33948e181c469f63108c01f319078051819c147f6446ea56b71975656efc1e \ + --hash=sha256:cd0d872882a4c098eacc83b832a042ea0b5165e247790d378e215c2a9f205d44 \ + --hash=sha256:cdd43ebb03db2441b920033d032a824612aa7f4daa58e88563e0a2036d6bd860 \ + --hash=sha256:ce1cc363d2b9c9e5a0def22b85c13c1bde6f26d1abca427b408bd18f0fd3828c \ + --hash=sha256:cf98016b6ce61dc30d80ac5bc491e0a84aa9da09e326fead5bba888d55f56126 \ + --hash=sha256:dda6b9f7fe48c6a0623e629a6bc0497dff132ca8a2a8897cd1f4281b5b12ecfe \ + --hash=sha256:e74576dba8992b4dac6ea3fe4c8cfd92f5318aa08b20142aff6423f20e56da7d \ + --hash=sha256:e7d899f1b5bc6d9441db01b05bd3c317b99be23525e6baae789ee59d759187fc \ + --hash=sha256:ee188f93fc931d9ce6231b550260988312445ecd2ea336445ec98b225ace47fb \ + --hash=sha256:f222606faeb11f7d41197f16b97cfc3c9578608bd5c2813d51d30c4dc42e675b \ + --hash=sha256:f3e2d06685fb1e6dad8905b0efa9423155852cf1b2a0d2b1aad3687cd488e4a4 + # via feast (pyproject.toml) +semchunk==3.2.5 \ + --hash=sha256:ee15e9a06a69a411937dd8fcf0a25d7ef389c5195863140436872a02c95b0218 \ + --hash=sha256:fd09cc5f380bd010b8ca773bd81893f7eaf11d37dd8362a83d46cedaf5dae076 # via docling-core -send2trash==1.8.3 \ - --hash=sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 \ - --hash=sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf +send2trash==2.1.0 \ + --hash=sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c \ + --hash=sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459 # via jupyter-server -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c - # via - # feast (setup.py) +sentence-transformers==5.6.1 \ + --hash=sha256:16af5d682ef66672b076d58599a23905800e850ec2bfb1865938306bf684ad72 \ + --hash=sha256:cefbb17b6325a982a4732c8c49fb013375392687049d1de3d435c4b04060680b + # via feast (pyproject.toml) +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 + # via + # feast (pyproject.toml) # grpcio-tools - # jupyterlab # pandas-gbq # pbr # pip-tools # pydata-google-auth - # pymilvus # singlestoredb + # torch shapely==2.1.2 \ --hash=sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9 \ --hash=sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b \ @@ -5101,69 +5920,72 @@ singlestoredb==1.7.2 \ --hash=sha256:92bc932df8b124a3c88b552210f9e0bb11cba4bdfbc9e7568c1582c00f0e8bcb \ --hash=sha256:c2a23b2b22f1e76cb0d53c99250de9a600bec9621766e25ae379c50914d6436a \ --hash=sha256:fba7f30f7fddb88e656e4309157d9e0016b6b1127d5adf348ba831bf77872d07 - # via feast (setup.py) + # via feast (pyproject.toml) six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via - # azure-core - # geomet # happybase # kubernetes # mock # python-dateutil # rfc3339-validator # thriftpy2 +smmap==5.0.3 \ + --hash=sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c \ + --hash=sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f + # via gitdb sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc # via - # anyio + # elastic-transport + # elasticsearch # httpx -snowballstemmer==3.0.1 \ - --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ - --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 +snowballstemmer==3.1.1 \ + --hash=sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 \ + --hash=sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260 # via sphinx -snowflake-connector-python[pandas]==3.18.0 \ - --hash=sha256:0af10b207af3d2de2b130e89018d49a60f2e5cfe841f3bf459e58f2e1c4c4506 \ - --hash=sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628 \ - --hash=sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4 \ - --hash=sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e \ - --hash=sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee \ - --hash=sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9 \ - --hash=sha256:32b1abfea32561d817b0a2f80b06d936cb32712af06bf7b848a428bfd857a10a \ - --hash=sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a \ - --hash=sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728 \ - --hash=sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800 \ - --hash=sha256:4ed2d593f1983939d5d8d88b212d86fd4f14f0ceefc1df9882b4a18534adbde9 \ - --hash=sha256:51eb789a09dc6c62119cfabd044fba1a6b8378206f05a1e83ddb2e9cb49acc0b \ - --hash=sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc \ - --hash=sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb \ - --hash=sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba \ - --hash=sha256:783a9ab206563d7b52fdcdd7a72af62de811d3381ca64132fd3445537b4d041b \ - --hash=sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f \ - --hash=sha256:8d3e96e1d09b07edca6c1f6ca675b6fdd05a4a7e428e4cdf6fb697d87b9f60fc \ - --hash=sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a \ - --hash=sha256:a8c570edff5a4888840dbe1e9e65c5e4d77d55c5c800cd359fe0903a769201e0 \ - --hash=sha256:aeeb181a156333480f60b5f8ddbb3d087e288b4509adbef7993236defe4d7570 \ - --hash=sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca \ - --hash=sha256:b99f261c82be92224ac20c8c12bdf26ce3ed5dfd8a3df8a97f15a1e11c46ad27 \ - --hash=sha256:bd1de3038b6d7059ca59f93e105aba2a673151c693cc4292f72f38bfaf147df2 \ - --hash=sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e \ - --hash=sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00 - # via feast (setup.py) +snowflake-connector-python[pandas]==4.7.1 \ + --hash=sha256:051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117 \ + --hash=sha256:052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7 \ + --hash=sha256:0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317 \ + --hash=sha256:167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf \ + --hash=sha256:171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a \ + --hash=sha256:1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6 \ + --hash=sha256:22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf \ + --hash=sha256:2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b \ + --hash=sha256:4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a \ + --hash=sha256:4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754 \ + --hash=sha256:5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9 \ + --hash=sha256:571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d \ + --hash=sha256:5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35 \ + --hash=sha256:7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0 \ + --hash=sha256:7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5 \ + --hash=sha256:81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57 \ + --hash=sha256:8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a \ + --hash=sha256:92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3 \ + --hash=sha256:b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45 \ + --hash=sha256:d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa \ + --hash=sha256:e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327 \ + --hash=sha256:f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c \ + --hash=sha256:f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7 \ + --hash=sha256:f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52 \ + --hash=sha256:fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1 \ + --hash=sha256:ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b + # via feast (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via snowflake-connector-python -soupsieve==2.8 \ - --hash=sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c \ - --hash=sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f +soupsieve==2.9.1 \ + --hash=sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c \ + --hash=sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba # via beautifulsoup4 sphinx==6.2.1 \ --hash=sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b \ --hash=sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912 - # via feast (setup.py) + # via feast (pyproject.toml) sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 @@ -5188,133 +6010,102 @@ sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via sphinx -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -sqlglot[rs]==25.20.2 \ - --hash=sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab \ - --hash=sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567 - # via - # feast (setup.py) +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via + # feast (pyproject.toml) + # alembic + # mlflow +sqlglot[rs]==30.14.0 \ + --hash=sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9 \ + --hash=sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636 + # via + # feast (pyproject.toml) # ibis-framework -sqlglotrs==0.2.12 \ - --hash=sha256:0338c7770a5cb5bb0ec1dcbe5206359fe9b83da0aba8dde53b9e7bd1afc89a22 \ - --hash=sha256:057a8db59a6c4bcdc42831e7ad01f41cf9e7f388ed5b139816adafbcacf2f591 \ - --hash=sha256:065835e7f2be50ba83895b64d044a39dab9d95098fff995427365e4bd8bc7bc6 \ - --hash=sha256:08e8be22da77c964be76ab4438da2c77096f5871088466ca950ee1b4712a97d4 \ - --hash=sha256:147cda8412f45af290ad190d9a98b5829a5f46a575ce768279ccebf9b7b53785 \ - --hash=sha256:155b0d59e34851b119c7ff0b2c7968c7b51667c1a1c2abefe1ac7244b3c1d78e \ - --hash=sha256:17b289ef0f25a7c034d183c588345e2b56622f7f64a85d1020633a75f8e3ac96 \ - --hash=sha256:1fc98b7649445e726a492841b8b8b39a4e5724ec2787cd1436404ebccf42519a \ - --hash=sha256:2554ead3126c83864a4b7e48e8e7e1bc23faf7160a6f28d3db967661cf529c9e \ - --hash=sha256:2824fc87fd7e41a785150ff042c7934e1fff97c6ccd59e4d96bebf6697a90762 \ - --hash=sha256:2db7e6cd41ef88c2ac647ad0258f87906de822955dec8f14e91829083047d784 \ - --hash=sha256:315f7f7bbfedf0c87d98068e62363454e986bdd05baa165b7fb448b5c6fe9f1a \ - --hash=sha256:327bfc2d71449f4dffba93d63f0565c4a1fa818143b1cfbc3f936fa8c9bcce10 \ - --hash=sha256:39a6ef72cf271db93ec6019847b7832defa9f4013c1e66851ca9c0a11c010c0c \ - --hash=sha256:4364116b7b0c72b841de6acd149a002bfc8fe360125989d4f39debd387c874d8 \ - --hash=sha256:4c07d3dba9c3ae8b56a0e45a9e47aa2a2c6ed95870c5bcc67dacaadb873843ff \ - --hash=sha256:4ceb28cf2ee3850cd745167cebe59a5fc3d506b32e9c81307938d8d272c1d670 \ - --hash=sha256:4ec38035523d54ba33de1e2b5562de4938254b61e1df48eb1db0e26ea189de28 \ - --hash=sha256:5026eada48f258ce9ad26fa41994b2ea5404bef2c3df9cb5cb2a159112a6269f \ - --hash=sha256:59499adc27a70a72170db9241404a18d4829cd3a83a076b9e112ad365c4b1452 \ - --hash=sha256:5be231acf95920bed473524dd1cac93e4cb320ed7e6ae937531b232c54cfc232 \ - --hash=sha256:67e288759d2be822db2175d0025c1f61283b019f2cc3e2577f31ad0ef3b5854d \ - --hash=sha256:6aacab6e20d92be3ca76f7358fa12346f29985e2d408660c764b7f1c75cc40ee \ - --hash=sha256:6ef3a827f2980aad17af4f8548297c93c4989d4cd3f64b9bcb7443952c542423 \ - --hash=sha256:732516bffffc70f172306ad8bc747dd9f16512cdbc09475abe6ad6f744479dee \ - --hash=sha256:76e4e1765c6be438329e234e56a6772537f6de16c4bb5ba7170e344664cccdf7 \ - --hash=sha256:7b553cdb9e8afcfea5466815e865f874f6f51aaace4fb4101670e150f7bbfe5a \ - --hash=sha256:7c79c43c5cde1f4017641032f11770ed8111c963dccc096cd15df906d4fb46a4 \ - --hash=sha256:8174aa227193d0a755f4515e6c3883be4681c9b669a65c2316f09be27b84be4d \ - --hash=sha256:8a18b3a09c32788d1ee2d0610ab35af862413c56b65f8ad8bc0131701f03103b \ - --hash=sha256:8f268aea3d2ebc05cb9148bb487f21e532f8af1b0a4aed6b7374703aadfb6a7c \ - --hash=sha256:91971032603d05428fd42a978084110afb2a4c0975e4343b075f69a23889e3da \ - --hash=sha256:9334f6c394a671a630c61339d52fb7da1a72eca057570f039b2a4035d2e39380 \ - --hash=sha256:954ccd912391ab5922adb23159ebcc0c5dccb468381e2a1ce92117cb4b0f0ed3 \ - --hash=sha256:9597865efc40e5c41af7719106c7620e1338aaa64646726652c63bae14225391 \ - --hash=sha256:97b2c74fcdd89f0d4458c0e2b5783989be99a1e0b2d627797688ab716ad9391b \ - --hash=sha256:989ccc5dc6b38da937481b6eb2dc1fc0b13676fe129697b874828e577984d7ef \ - --hash=sha256:9c4c6f6fe1c54fff614f9d0b2dd7a6bf948bda87ce51a245dcd3f447f20c8b74 \ - --hash=sha256:9d5b9a9d6259b72258f6764f88a89faa3c648438bd1b2c3a9598b725d42bf6f2 \ - --hash=sha256:a266c9047726d83c51a8ec3d5278ceb9caf131307c9c93c4ceefd99c0116e538 \ - --hash=sha256:a4a2cacb31f75e242c7b9ff4afae1d95f548df8441444114376d8007cc91b55b \ - --hash=sha256:aaf86275a3388da1ed2161645aa346bfca3ee6e1dc0e2115867db9e78f1caddd \ - --hash=sha256:ab676d2d7da28907a139ad5fc20dee0890054967bac0b18e653ac048837c9ea1 \ - --hash=sha256:acc25d651eb663332157c2e5d2736516cddf4cd0effe67a887723934de5051d1 \ - --hash=sha256:b10bf6b71961b31951bf4dff937d8d5d399ea1b3bd47fb5c5810386710fe7dfb \ - --hash=sha256:b40601e67f5abae5d09d23f92394dbd735539de469ce663b596eb42bf77d2c54 \ - --hash=sha256:b6020825e58af6e2795e6dcb69639f5500e45e1da78f1b1abd74c4d11083a249 \ - --hash=sha256:bc1807c6222e32fc9bf6f5c7e12b85c4b72f12227800d40c1693244c198b33bb \ - --hash=sha256:bd6c4e6a7670f761c8e69b45d6d302a4d37a3cddb1fdca2ad90e54b77858fe80 \ - --hash=sha256:bf3e2eab11f06f1df13c0f85b3e26fbab0b7e8a5d189e5edfed951bc85f6bd48 \ - --hash=sha256:c3d62905ce74a48714b7662ad95efe299fad62f193be4b482a327af060f98710 \ - --hash=sha256:c3e0edde0fdf598561e7404ac56fb4b12276394ee5155b5365e42434c6f287a3 \ - --hash=sha256:c64066d13bd2e5e788b845c933c765af9991faa93982e273b623019a1161fadc \ - --hash=sha256:c8bf7ae29c0fc66e9c998d7f8e6f6fc26309c6eb5a4728e1443cb628218bc307 \ - --hash=sha256:d2827c7bf7e57496f9b95658bcd2395cfb0c51adc3023cd3386988337dfaf6a5 \ - --hash=sha256:e7b2da43b2a6a85807df6c56b2627abe244aff28fdf9a4940d38d749cb4b8e3e \ - --hash=sha256:ebc162a599fac86e59f899631716752fbc7f89598e94729eadb707e54db371b2 \ - --hash=sha256:f0a2ddeab27a94447270b7a240770a31a3afed0a972d60085205baec990ad76a \ - --hash=sha256:f104a98182761d4613f920eda7ec5fc921afb3608f7db648206ce06dd10a6be5 \ - --hash=sha256:f83ad3fb4ea57218c0e65d3499e31c9bb3051bbb5dccbb11593eaf1640964b51 \ - --hash=sha256:fa1ae834fb78bd52bb76e3c8d02cb79f45717ab1f02f4ad8154bf33a5408a502 + # sqlglotc +sqlglotc==30.14.0 \ + --hash=sha256:0337b982675e17f6dd9a1ed7d944fdef4f81a2aaff6a4494d50492aa18fb0b4e \ + --hash=sha256:07ee5ed7f8b13cc8e918a31c328933c3f4fd155f5b4360bbbf305445b0f88e73 \ + --hash=sha256:13441a60a212faef4c658b61394cb22b922bf80c1bb544a5397bed67d5549aeb \ + --hash=sha256:13f24fa61b21bf21d6734fd9eeaffa8965dc70228b39994005488688bbc033bc \ + --hash=sha256:13ffa7852e59a6b12e3dbd96686c594847bd6bf4b1a41f1cd8975b0f90e2a522 \ + --hash=sha256:17951d2587cdfb39086e2b06e6d3bfb15628232a295580c6aecda881a4770824 \ + --hash=sha256:20c01558912dfc4a06b8484459337efa6fe98f3e45602e213d888fde56f0410e \ + --hash=sha256:331c600908846a4fee6ffbda0cd82e4b1550612503c761166010a3776334aaf0 \ + --hash=sha256:47f7ab74779572fd51f616522ca597b034158150a7a312a3dbe907d103f5d908 \ + --hash=sha256:49157e4dca20cdab9187dec83f51268d2e3c883ebc9b31afe5e5cf11954bd38e \ + --hash=sha256:5410ff65f26b57e6a9d60f57dd46e8d7aa96b70d1faca562b3931a2b03eb4d56 \ + --hash=sha256:68c259b9ef4ac630c6c04f4e6217f130f0856abe6146133226442e82e1b54ce4 \ + --hash=sha256:745bd364e8a5032c4ead9dd843bfa00c5b35a1d816801de7cfb1eeab08f24b8d \ + --hash=sha256:849520c4b9057408e9198f019791538d5bb77c648b3fc6410db1727acdac90ad \ + --hash=sha256:968f446a8304e782d6778389de561cad9f984b89c3efea0474d8ec89ae17f02c \ + --hash=sha256:9918233d827dcc2b1842a8fe3cbc8d915ab71d9ce0100b566e283edb53950867 \ + --hash=sha256:acb7f577a8e060fb1cb1c9df1b1de1187fc7c53d54539cb6cdc16bd558adf2a5 \ + --hash=sha256:b1f414c6a6a3c3b56b981e51f9fdd1b1cf95dee5de2592c4b4547803309a57d4 \ + --hash=sha256:bc3c4ff8f268e2558aed03638ee002d2187cdf4faa15bf34b6f40558abe29e6e \ + --hash=sha256:c9c9cf31aac1414ccde5e69849bf438b3c74ae763db00651bdf71955ebadc214 \ + --hash=sha256:dfe8296ee3e505c4f3ae22aee4b0556e1263410e02f274ead0bdb723cd1c4147 + # via sqlglot +sqlglotrs==0.13.0 \ + --hash=sha256:6b934a244b16f26fca50974328a2ebc7689583c59f06203cebb46e2e6e8d93a7 \ + --hash=sha256:ad1ad158234af0f8ba5054ca51bd17a7c1e3f81b4798c7970ebf7953fe08ddcb # via sqlglot sqlite-vec==0.1.6 \ --hash=sha256:77491bcaa6d496f2acb5cc0d0ff0b8964434f141523c121e313f9a7d8088dee3 \ @@ -5322,297 +6113,323 @@ sqlite-vec==0.1.6 \ --hash=sha256:823b0493add80d7fe82ab0fe25df7c0703f4752941aee1c7b2b02cec9656cb24 \ --hash=sha256:c65bcfd90fa2f41f9000052bcb8bb75d38240b2dae49225389eca6c3136d3f0c \ --hash=sha256:fdca35f7ee3243668a055255d4dee4dea7eed5a06da8cad409f89facf4595361 - # via feast (setup.py) + # via feast (pyproject.toml) sqlparams==6.2.0 \ --hash=sha256:3744a2ad16f71293db6505b21fd5229b4757489a9b09f3553656a1ae97ba7ca5 \ --hash=sha256:63b32ed9051bdc52e7e8b38bc4f78aed51796cdd9135e730f4c6a7db1048dedf # via singlestoredb -sse-starlette==3.0.2 \ - --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ - --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +sqlparse==0.5.5 \ + --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \ + --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e + # via mlflow-skinny +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 \ + --hash=sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627 # via mcp stack-data==0.6.3 \ --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \ --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695 # via ipython -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via + # feast (pyproject.toml) # fastapi # mcp -substrait==0.24.2 \ - --hash=sha256:743cc352e96b0927b2cd37cd5a8fdac0a96a68df9600bd104fc36aebd222a836 \ - --hash=sha256:d1d475833566fa9d67eed3273456883c0568486ccced92b524b31709d2817e19 - # via ibis-substrait + # sse-starlette +strictyaml==1.7.3 \ + --hash=sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407 \ + --hash=sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7 + # via pyiceberg sympy==1.14.0 \ --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 # via torch -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 # via - # feast (setup.py) + # feast (pyproject.toml) # docling-core # docling-parse tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via + # feast (pyproject.toml) + # pyiceberg terminado==0.18.1 \ --hash=sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 \ --hash=sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e # via # jupyter-server # jupyter-server-terminals -testcontainers==4.9.0 \ - --hash=sha256:2cd6af070109ff68c1ab5389dc89c86c2dc3ab30a21ca734b2cb8f0f80ad479e \ - --hash=sha256:c6fee929990972c40bf6b91b7072c94064ff3649b405a14fde0274c8b2479d32 - # via feast (setup.py) +testcontainers==4.15.0 \ + --hash=sha256:085cde086337632e19002719460b7b80bbab2bdd51bb3ea04f77d0de96504706 \ + --hash=sha256:8796c14e76604031ad39cf0ed3b8e9806283a1fbf5270965c2b1c594caa31b74 + # via feast (pyproject.toml) threadpoolctl==3.6.0 \ --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e # via scikit-learn -thriftpy2==0.5.3 \ - --hash=sha256:08d8699d318b6a8fe9e9fd4c2234ec7912462d90cc636c371b4f4f6500a44328 \ - --hash=sha256:0f36f80a038dbfc2b3b048151ca4732f310ebd0385cdf20e7864d781d5d6f582 \ - --hash=sha256:13c0316a1a9b6f7840d9c084a5a1fa2e419ae86645e45530593558704e792d7f \ - --hash=sha256:195beb93caa104879d808e87d92962fff8d59d40486590fd653b5aeb7774420e \ - --hash=sha256:1bb6c0482663887f2a9ab98453ab0ca20a3e1f2336a500b7da12af33614c0d75 \ - --hash=sha256:2023abcc504e4fc8825419964ecfab904244b0bc189d0082380d481ecba951d7 \ - --hash=sha256:236a7d4627b1aa692a901ca45d7dfa4e516bcd3f309efc18ac69671b31789e39 \ - --hash=sha256:29b09fa1fb77f1927ac4ce21d8f8f6663d8917f75780aa6bad57ca9473d0a3b3 \ - --hash=sha256:2d3c0403673a3b7fc38304cf89e07c792f44ac6aa3b15c12e6cc411a85d10af3 \ - --hash=sha256:2eb14e24febbeca84d603e88a24db9ccb4a1437f90e9a862dcee02dc0a2194c2 \ - --hash=sha256:2fb20edf082965487bddfba03b2c05bb50db38ceda3111540cb2353949fdb29e \ - --hash=sha256:387c04d02f23ae83415cb2de35a88ba79321619af25cf34a481cabc367ddf1aa \ - --hash=sha256:3c00c340114c3041961906a628e70e6b6b5805ee691e682c290cec3513e77efc \ - --hash=sha256:3da0e3c1a5c17f67a203d9814853dd1d8fe8b0ec69a26d30d6b634e4c0e2c87c \ - --hash=sha256:3e2fa8c3d2b1505d2a463c090d9e771b8fed0eda8b01b0365e7547ba106bf2db \ - --hash=sha256:455440132b01b3a895001dc59ecf6056e8fd041ad6e745ff22391cf3a1f8361e \ - --hash=sha256:523c480a4b3aad480e4738c32f97b3f356ed998e6549f5f55eb6f7852474cfff \ - --hash=sha256:524d69102843fed087e30c6edc5b99f7b42b768d88bf910787add60e37e2a2a5 \ - --hash=sha256:53e761ea5eab24bb3520f8adecdbb633e69dda7cf9678ca2eb6ad1952cc56540 \ - --hash=sha256:5e799f6c4caf79a5566bc14941e768b132c533bed66e0a5ef0a127a74f98acab \ - --hash=sha256:6196d7d5adb6214ed21633ec57a222c90a6a66498cdd9f8da7c85c7514c7c439 \ - --hash=sha256:6384a142514982b380431b7d8811f137c5ec9cf5cf3affa33884b7ce4a51c8c2 \ - --hash=sha256:67ec304d83510d0ec83338ef029ea3bae91fdbb3bf0504f7990dd446b483773e \ - --hash=sha256:6efccde429f93740943bc4c0e2664a49f0799ed21663cc26b16a823e8719040f \ - --hash=sha256:710b7f3d9fdbb5788b37b4b694d30fa37a3c08c0d52aeb1def57a5140fa2f4fd \ - --hash=sha256:716c36885e29a9480bdabe117235967b5fe36dc179aca80cd8ef9e12866e4cef \ - --hash=sha256:72a1cdfd3bcf16b667379f8277e83295a34ae74227e54711c657305bf4c9e63b \ - --hash=sha256:73832e79732bab15920bed700a5efbe2b6e9dbd35710e815e870feb873b15059 \ - --hash=sha256:74dc6ed8c9098b66a17b916dd898abc32801a8ef0b439a407bce4f11c1b3da34 \ - --hash=sha256:772c9c1015d05177e37b9a547bcd27b560fc888ecd1e179ebf7f114ec467914d \ - --hash=sha256:86489105abb39c6ff93c3d270cf1474f7fddd380847f2b6bc8d09e5d0f0a23ab \ - --hash=sha256:8a6652e823e0ae6fa9f73b62c1a2ed04d7c0e1ac402c7ca7c509f9f14fbcc80f \ - --hash=sha256:9460a8284881854e210907eeb1761f44afacb4d164d1b6ecfddc184ed1b03277 \ - --hash=sha256:94806a0c3436189a75efac4ab067cdab7298876fee40cc0006300cc1d7982055 \ - --hash=sha256:9cba3454b4e5e05102d2dbfadd3a9a66c19488c6aa18c147bbeff2097ae67f04 \ - --hash=sha256:9eda43701a94def9d063550b0d8261630c40ade312c35b1f6e4804859c783ed8 \ - --hash=sha256:a57c67a880c9da2d252a6244e9ccf7b08850388c7afa4f0e98cb60fdca904a09 \ - --hash=sha256:a7a00b772783847c0c48a43e098b64f5741ca5a2e52e6c66d9b753765cd93ed3 \ - --hash=sha256:a7f2913ea3beac18767784059f02a67751d99094d4a368b350911784f0e09709 \ - --hash=sha256:ab47f689b0003ec63a881e5ad4f46046f62632da5168b0665fd369a3561eaa2a \ - --hash=sha256:addcc3ba9c106758e9073ab205e4bcf9a9540acb335fafa0184a1fa8e76a78cb \ - --hash=sha256:ade0165ba060b97333bc7a927229e992441bfa17bb8e13ea05590c2ec3551b17 \ - --hash=sha256:b208f3c23f916ca0517285c11748ca1fcf43a2ac2224ea5eef8bcba464a20652 \ - --hash=sha256:b5670936016aeaeb7111c96661ced36541211c0e82eb357a9bee5d4176ebbcff \ - --hash=sha256:be23631c152323dd3d7d51368dadcec75e60e90e4662be4f2b8ada208c61fa34 \ - --hash=sha256:bf69d246c39d0ce4ed922b6e00e643ca514cdf40010b00b46f82b0f758a840b3 \ - --hash=sha256:c01e0da29120709d46cb4310944fc717f28ce097d8845c4c29e111ff98c9deff \ - --hash=sha256:c0cf4418810ecf984f6d7f538988175c459f6bd5c85d94b878ebb11dbdbfa62b \ - --hash=sha256:ca4d554f8fc79c8152119bbd576e5d6a5c11e907e0baf467fb4676b1d274558a \ - --hash=sha256:d5080c1c4bd13c4431613a2c0cd607c5e3a07a496a865a0d01f534401d3b09c7 \ - --hash=sha256:e6a77d3d190f1c2726cfb11d1115678fcfa4b0ff509bd8bb38e451c629d9383c \ - --hash=sha256:eb440b7d8e7460f6969016d77e25ebfdee2aa6d5fed95aecf2bd59310c2c5530 \ - --hash=sha256:ec49907ee15513ca2344540c4ad2bf1945c41a6c0236d589eebd32be8298faa9 \ - --hash=sha256:f2ccb893ae687ff946902d96a5615a93847a7868bc5d66f51caf5ccf46466314 \ - --hash=sha256:f4210c10b686fe4a32b121f618b407aaccc7a72021c6d64fa181a09df72c4d89 \ - --hash=sha256:f4d122a82cf7cd4743a2640199b066a994f6527802c2dd16f2e4fffc15efa2a3 \ - --hash=sha256:f768756c0c105e98a3760fc7b4f4df12e25f5334b204060bb6bdab3ce1599e11 \ - --hash=sha256:f965fff2f2f323ddb5d9cb7fabe33c4c9f008955dbb59728ffc3111557b87793 \ - --hash=sha256:fb86f4c0cfcb39949a53dcc689e3758594a18724753861ba9f59646b72417383 \ - --hash=sha256:fd4c6131ca6e919f03263cc83b713f1797bc20126a858da8518ed49b3e32c334 \ - --hash=sha256:fdc5676b52fa6a3009d205360eb9ba257b8b4813883ed52797a20838bcc45dde +thriftpy2==0.6.0 \ + --hash=sha256:033021acfc347f3e51cf107b189a5efafcd1e974a8217a4d066d93719b2bf353 \ + --hash=sha256:0345c8ba40f7b98c24c1ecabfc04ff512ed930ab86ff277572bd279b69a0a252 \ + --hash=sha256:0a0249bf9004d241cf6fd1ed1879209ab7641f7e09456323a839afb6c9213b58 \ + --hash=sha256:0aa86f5d83a49567fa6ac81c7f78ffe8e5cf68b57cf3f7f7c55dc1486f5e9bbb \ + --hash=sha256:0c2f823bb691dd71c9c81170026dc52ace5b750881799960ea9f992919eb9731 \ + --hash=sha256:1245c36b82f34aa26f049e6529f40ad34d9be8d12bd0131559847c8476b98ce0 \ + --hash=sha256:151d299e8e3694a6cc0f2f2cda01d5744080742649de958c5cdcbebb4306205f \ + --hash=sha256:16eecfd34bd541b75172ba0d69ea90b874611a7361d18906fb6d95955089cc30 \ + --hash=sha256:182f54a7248c8ecf1320cf26d1fc9115765df6b1f2589c1d2d0df7049a5b27d4 \ + --hash=sha256:210345281d41b3a76d263b555d3e3cc482103738441bdb92a73033a4e9a042e1 \ + --hash=sha256:265588b8cdb3fe1097db43bf35fb208edc69d9350a2bec3a737d6357d0a5650d \ + --hash=sha256:28fd55960a6d42207060536109928a4615fbbd6874c0ddd8a705b47075f1d2d0 \ + --hash=sha256:29ff125e40c8016b4d3bf48e6d727bd93d2892451b47bfe57ba896944ecbdb0c \ + --hash=sha256:2ae866adf9b112c7ab30c1a90d878a5d6f2d40244fbc46ec8477425d802f4ac5 \ + --hash=sha256:2bf891c2d441b1edddfc62f16ab3031ac7506cba5b77680654179dbe93d8b7ec \ + --hash=sha256:2c88b0d356ea18ce026a52aa7c2110693db77fd52d5ac7553616635a7f165bbd \ + --hash=sha256:3090d9cabc2c31c92ae943f36d539a20adfd82697f50e996ce94f0b279e9e1e4 \ + --hash=sha256:3359a7c4eb4c281bf54bd760dcc03c43299f0d529dd2a5018be2f1fbebf0cbbd \ + --hash=sha256:375cca06f36f54339838c7f8251b64a777d5ff1277f8b91999487fad1a8e2d73 \ + --hash=sha256:386d8c4a5677fd94fd16c1af2adf39f59698af1e4ef0c3c026083f278540e352 \ + --hash=sha256:44365bb5098d0a91c44382e7df0ad44d5b9a588d29d9071aca4a2867f704aaf1 \ + --hash=sha256:443e502b428d325c57aec0947991857e8dc0589d0464181f35bd48f870cd5d18 \ + --hash=sha256:4550cbb6629c9f6186ab22cb497f262408e1a90ae10b8653599f2717f518f0df \ + --hash=sha256:4b8714e6eb37d973bdd231a46ce1c639417fe8035d4d3224f832e4a6afd05d5f \ + --hash=sha256:4cdcd8ae0b1017c5d7cac3595f1ac43ab9c0471cf700156d62d246248c734a35 \ + --hash=sha256:5ad583efb91402c0aada9cb4df0b6255607ceb83048dd2f629c09858594977ff \ + --hash=sha256:621c263f99274d51a9a1deecf301845f1408d497bdafed682db6155132a99cf4 \ + --hash=sha256:64860404663009a41a529f1f13c012e1ccf4a814b123581729f98c2b103ef362 \ + --hash=sha256:690d53df7b154d817d5b1dc5d24ba95045b670862005b8321f307f011a0738b2 \ + --hash=sha256:6dbea13f82de14b3db06cbc66e2060af4bf1070442398ddf42fa71ab188db627 \ + --hash=sha256:6f78d1ceac9545b87857c0e9b4e28a42fb98f8c6991d6b0e4099a012c35d2e73 \ + --hash=sha256:7068cae451be320c41b1442d5e2ec06dae050f1d3883918096f9cc3fcc791e89 \ + --hash=sha256:7afbd9bbe89866dbd9221f4c7e7321f4d0519772245d1b216b5ff1d50b8c0af7 \ + --hash=sha256:851981ded8bb42da66213cf95d1dd5eaf06c5d6b3a95725a18eddd58ec992b6b \ + --hash=sha256:852e538b4866ed776126349161d9fdc37387b1e15ab46805f98dcdee25fee4b5 \ + --hash=sha256:8b19d19661fc9a71f19b7c432c413ab65025e5dd11fbd192bd9169afb13b9ce0 \ + --hash=sha256:8e62d9c36bcfe6b85bec7b754accb97e2fa7b6a7c9a0c37f824d85eba699e7b8 \ + --hash=sha256:8eb0566b4501c27ea51f2e593531122703946969564fe526a5ff1b18be0ad49a \ + --hash=sha256:8f393607d54091e8976e297f8fd7399606d12cd8c2b8852353f07ad5ddac4224 \ + --hash=sha256:91df1fa70a99ac08dc449d6fda24a14992a1836d571928f217c40c66fd63fcc8 \ + --hash=sha256:98128abaa1bac8c4f60d08af641b981ba56486269532c03e99a1d48c9d6f9aa9 \ + --hash=sha256:98a7911f5ca3d6f809377fa8eaad8295687a106dd7bdd15624b267270d0da2ab \ + --hash=sha256:a07d4c466ad1b8c146dd7b893d2c2e735c3e530abfcef0c741a464001e828155 \ + --hash=sha256:a7c4aba79ef5fa41017d814016037f5ba29ecae889cea3d37108a4677ecb3aac \ + --hash=sha256:aa57de5929ada67d2f753442ce04dcc35561899558a1566f39f6e0c893cbc54b \ + --hash=sha256:ac3f7143da2d1f6087128d47d348ba0d92fe7f59ff476919f8d0f78fa5720f7b \ + --hash=sha256:ad18b3082a56119e0fb19ad4d47556ee24ce076466fff42b0d0a75a20d69a2e0 \ + --hash=sha256:b23462a349d4e7c6c77e8f6e735fb24dccdde14dd445c5eca76a9aaca7111f08 \ + --hash=sha256:b361152c24fd5c791220de9966b00696578c9884a2bb67e4759d4dfe05fd7049 \ + --hash=sha256:b51b5259dc344482ab21b768dfc7f54d51d9133665e72890831725068f69f24a \ + --hash=sha256:b57f367d7b0e1bc9e5c9b0ff34febdb3fe440f1fe8d75903ae71301bc06072c0 \ + --hash=sha256:b8dc65d2e7951b7e81c17b92857db7c19d6b3dd442d2d8600b5bd5040aa43ce6 \ + --hash=sha256:bc320e960347e5f9d27e8b4a9fc7044b2b26bfe4522bb4957e741fc1d1ebd2f0 \ + --hash=sha256:bdf77ba7a8987a239eb57cf840d62669741f8b92b61a617e63898caed31da898 \ + --hash=sha256:bf96b64400da2f411b43c4c81c2e20b09e3300d86766a52f42393696c8962f11 \ + --hash=sha256:c37f5dbfe95579549485c33167854784358f559feda703ccc058719ca0efd8aa \ + --hash=sha256:c41312c6edad5e875613719236f1ca6bba9310df40b1adc9308248e1bdb7a1ea \ + --hash=sha256:cafa1d43bcc69129a4855fd3973ab7983bb2274c407e5ff572af5dc196e00313 \ + --hash=sha256:cb98556e919be3e6ba9bca629dbddccfaa33b95a0fe7503052849b124e4f88cd \ + --hash=sha256:cd2e2c4dcc30c373e317d39b044afa6d9a090bec11d186f25841f70bc520bbb5 \ + --hash=sha256:e6c4fb1e7f51f8858f348ed9c31bb408b61274942d18b549ec163bb480c987a0 \ + --hash=sha256:eccab0281667caab0c055882b3bbb8e346bb0181e55f37477e3e5e3f5b7a96dd \ + --hash=sha256:f59f74c3779aa47223ba0a9e23ef10d2af5a873ed3624c78303f62a679d1b63e \ + --hash=sha256:f6b86112cca7bd04151ce248d781763ea5f74cc18d148476c6d16cee32db81ac \ + --hash=sha256:f837ab85ae93b118766b8b28a1cec47a1daddee303e1f986a595c56379062a5c # via happybase tifffile==2025.5.10 \ --hash=sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103 \ --hash=sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad # via scikit-image -timm==1.0.20 \ - --hash=sha256:7468d32a410c359181c1ef961f49c7e213286e0c342bfb898b99534a4221fc54 \ - --hash=sha256:f6e62f780358476691996c47aa49de87b95cc507edf923c3042f74a07e45b7fe - # via feast (setup.py) -tinycss2==1.4.0 \ - --hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \ - --hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 +timm==1.0.28 \ + --hash=sha256:3789d313fdd5541a327b60180d70dbb4bdec73db8ff0655e413db3c3d134a9a4 \ + --hash=sha256:e577b88da96b3a722ea5e2f042455ce6f715d398304d8e63b17d126ed7d89968 + # via feast (pyproject.toml) +tinycss2==1.5.1 \ + --hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \ + --hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957 # via bleach -tokenizers==0.22.1 \ - --hash=sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a \ - --hash=sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446 \ - --hash=sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7 \ - --hash=sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73 \ - --hash=sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a \ - --hash=sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9 \ - --hash=sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138 \ - --hash=sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc \ - --hash=sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390 \ - --hash=sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f \ - --hash=sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82 \ - --hash=sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879 \ - --hash=sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21 \ - --hash=sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4 \ - --hash=sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214 +tokenizers==0.22.2 \ + --hash=sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113 \ + --hash=sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37 \ + --hash=sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e \ + --hash=sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001 \ + --hash=sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee \ + --hash=sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7 \ + --hash=sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd \ + --hash=sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4 \ + --hash=sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012 \ + --hash=sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67 \ + --hash=sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a \ + --hash=sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5 \ + --hash=sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917 \ + --hash=sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c \ + --hash=sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195 \ + --hash=sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4 \ + --hash=sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a \ + --hash=sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc \ + --hash=sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92 \ + --hash=sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5 \ + --hash=sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48 \ + --hash=sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b \ + --hash=sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c \ + --hash=sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5 # via transformers toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 - # via + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 + # via + # alembic # build # coverage # fastapi-mcp + # jupyter-builder # jupyterlab + # milvus-lite # mypy # pip-tools # pytest # pytest-env # singlestoredb -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via - # poetry-dynamic-versioning - # snowflake-connector-python -toolz==0.12.1 \ - --hash=sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85 \ - --hash=sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 + # via snowflake-connector-python +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # altair # dask # ibis-framework # partd -torch==2.8.0 \ - --hash=sha256:06fcee8000e5c62a9f3e52a688b9c5abb7c6228d0e56e3452983416025c41381 \ - --hash=sha256:0be92c08b44009d4131d1ff7a8060d10bafdb7ddcb7359ef8d8c5169007ea905 \ - --hash=sha256:1a62a1ec4b0498930e2543535cf70b1bef8c777713de7ceb84cd79115f553767 \ - --hash=sha256:220a06fd7af8b653c35d359dfe1aaf32f65aa85befa342629f716acb134b9710 \ - --hash=sha256:2b2f96814e0345f5a5aed9bf9734efa913678ed19caf6dc2cddb7930672d6128 \ - --hash=sha256:2f4ac52f0130275d7517b03a33d2493bab3693c83dcfadf4f81688ea82147d2e \ - --hash=sha256:5128fe752a355d9308e56af1ad28b15266fe2da5948660fad44de9e3a9e36e8c \ - --hash=sha256:5ae0524688fb6707c57a530c2325e13bb0090b745ba7b4a2cd6a3ce262572916 \ - --hash=sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0 \ - --hash=sha256:65616ca8ec6f43245e1f5f296603e33923f4c30f93d65e103d9e50c25b35150b \ - --hash=sha256:659df54119ae03e83a800addc125856effda88b016dfc54d9f65215c3975be16 \ - --hash=sha256:7b677e17f5a3e69fdef7eb3b9da72622f8d322692930297e4ccb52fefc6c8211 \ - --hash=sha256:83c13411a26fac3d101fe8035a6b0476ae606deb8688e904e796a3534c197def \ - --hash=sha256:89aa9ee820bb39d4d72b794345cccef106b574508dd17dbec457949678c76011 \ - --hash=sha256:8c7ef765e27551b2fbfc0f41bcf270e1292d9bf79f8e0724848b1682be6e80aa \ - --hash=sha256:8f0a9d617a66509ded240add3754e462430a6c1fc5589f86c17b433dd808f97a \ - --hash=sha256:a3f16a58a9a800f589b26d47ee15aca3acf065546137fc2af039876135f4c760 \ - --hash=sha256:a7242b86f42be98ac674b88a4988643b9bc6145437ec8f048fea23f72feb5eca \ - --hash=sha256:b2aca0939fb7e4d842561febbd4ffda67a8e958ff725c1c27e244e85e982173c \ - --hash=sha256:c12fa219f51a933d5f80eeb3a7a5d0cbe9168c0a14bbb4055f1979431660879b \ - --hash=sha256:da6afa31c13b669d4ba49d8a2169f0db2c3ec6bec4af898aa714f401d4c38904 \ - --hash=sha256:e2fab4153768d433f8ed9279c8133a114a034a61e77a3a104dcdf54388838705 \ - --hash=sha256:e8e5bf982e87e2b59d932769938b698858c64cc53753894be25629bdf5cf2f46 \ - --hash=sha256:e9f071f5b52a9f6970dc8a919694b27a91ae9dc08898b2b988abbef5eddfd1ae - # via - # feast (setup.py) +torch==2.13.0 \ + --hash=sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d \ + --hash=sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c \ + --hash=sha256:0ab4b69f3ee03a62a002cfbf77b1ca5e88aceb4ea64cb4388bb28f638ddbb045 \ + --hash=sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005 \ + --hash=sha256:2bd30b6b730d987fa386ce3898933762c5cb8cc82eb0535211d787cc3ce2dfeb \ + --hash=sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027 \ + --hash=sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc \ + --hash=sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09 \ + --hash=sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6 \ + --hash=sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2 \ + --hash=sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd \ + --hash=sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4 \ + --hash=sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7 \ + --hash=sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b \ + --hash=sha256:94f0de129916f77b8dc2c7a8eff644cfeddfe59e39c9f55e9f6e17543410281d \ + --hash=sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330 \ + --hash=sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c \ + --hash=sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e \ + --hash=sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8 \ + --hash=sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1 \ + --hash=sha256:c78b7b4d04461855a764cf01bae9a462bb88bc93defcfa11235cbc8fdf3e12c4 \ + --hash=sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92 \ + --hash=sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c \ + --hash=sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8 + # via + # feast (pyproject.toml) # accelerate # docling-ibm-models # easyocr # safetensors + # sentence-transformers # timm # torchvision -torchvision==0.23.0 \ - --hash=sha256:01dc33ee24c79148aee7cdbcf34ae8a3c9da1674a591e781577b716d233b1fa6 \ - --hash=sha256:07d069cb29691ff566e3b7f11f20d91044f079e1dbdc9d72e0655899a9b06938 \ - --hash=sha256:09bfde260e7963a15b80c9e442faa9f021c7e7f877ac0a36ca6561b367185013 \ - --hash=sha256:1c37e325e09a184b730c3ef51424f383ec5745378dc0eca244520aca29722600 \ - --hash=sha256:2a3299d2b1d5a7aed2d3b6ffb69c672ca8830671967eb1cee1497bacd82fe47b \ - --hash=sha256:2df618e1143805a7673aaf82cb5720dd9112d4e771983156aaf2ffff692eebf9 \ - --hash=sha256:2f7fd6c15f3697e80627b77934f77705f3bc0e98278b989b2655de01f6903e1d \ - --hash=sha256:31c583ba27426a3a04eca8c05450524105c1564db41be6632f7536ef405a6de2 \ - --hash=sha256:35c27941831b653f5101edfe62c03d196c13f32139310519e8228f35eae0e96a \ - --hash=sha256:3932bf67256f2d095ce90a9f826f6033694c818856f4bb26794cf2ce64253e53 \ - --hash=sha256:49aa20e21f0c2bd458c71d7b449776cbd5f16693dd5807195a820612b8a229b7 \ - --hash=sha256:4e7d31c43bc7cbecbb1a5652ac0106b436aa66e26437585fc2c4b2cf04d6014c \ - --hash=sha256:6c74cbc1cbee26dd4f35f989cd80dccc40411f258dee476b29871dee4b483af0 \ - --hash=sha256:6dd7c4d329a0e03157803031bc856220c6155ef08c26d4f5bbac938acecf0948 \ - --hash=sha256:7266871daca00ad46d1c073e55d972179d12a58fa5c9adec9a3db9bbed71284a \ - --hash=sha256:76bc4c0b63d5114aa81281390f8472a12a6a35ce9906e67ea6044e5af4cab60c \ - --hash=sha256:83ee5bf827d61a8af14620c0a61d8608558638ac9c3bac8adb7b27138e2147d1 \ - --hash=sha256:a2e45272abe7b8bf0d06c405e78521b5757be1bd0ed7e5cd78120f7fdd4cbf35 \ - --hash=sha256:a76fafe113b2977be3a21bf78f115438c1f88631d7a87203acb3dd6ae55889e6 \ - --hash=sha256:a9e9d7552d34547b80843eaf64ab0737b19b2e8bec2514286b8cfd30861ca8b5 \ - --hash=sha256:b190db205f90206c230fc2f91cbdfd5733334babc0e0d19bddb90a40b8cf26c2 \ - --hash=sha256:b9e2dabf0da9c8aa9ea241afb63a8f3e98489e706b22ac3f30416a1be377153b \ - --hash=sha256:dc7ce5accbbb8c9df9a79f8cef6a6df042f28e2250a6ae0d2ca70b06473fa03b \ - --hash=sha256:e0e2c04a91403e8dd3af9756c6a024a1d9c0ed9c0d592a8314ded8f4fe30d440 - # via - # feast (setup.py) +torchvision==0.28.0 \ + --hash=sha256:028a3d481b37d785605620d7cdad897064c5a55bae2aa1f2658766333e291940 \ + --hash=sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49 \ + --hash=sha256:2a1ef4b6f4bf5828b48cfad97372c8982db906830884b2868ba5c3df937a7d81 \ + --hash=sha256:3557cc7b539f46dabcda2b6f2b14017ccbeef024de466d4fc5835fc3f287f769 \ + --hash=sha256:36beb0782976906069ca03d4c9aacaf4b6b838b06ed6c20960ea9c51cce7acdd \ + --hash=sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b \ + --hash=sha256:46f581979c010ad6da6bd85ee602aa707e1ff44312670223b7a0ee517ad06d47 \ + --hash=sha256:546fd85345cf8652f6cd099d4f9884b0ca5c2f3fae78689a21dd2f35ea6b622f \ + --hash=sha256:5a38bc6da3d72621be003400b66f66a2b4c6d644fde05f680c2cb7ca8cf8dd6c \ + --hash=sha256:5cf78ebc401ce64ae19b8c55de866bb836797d559a4de9c25ccbe74cfa642d3a \ + --hash=sha256:62c7d110f86a039245b587e4fae60278c649f3bd42ff79cfbc1178eca4e72542 \ + --hash=sha256:6dfb0f45e2b4ceb4e76f158c3fbb5f44387099f3c466e3423a09ab665a194aba \ + --hash=sha256:7e80f543b22503d9415e126db5f0ff3917036925e38560ee6b9ae38c571a4002 \ + --hash=sha256:7e9dd6f60d6e15f8dc27d4f877fdb6002fc70d70272412135f1c2ff9cfa08d3b \ + --hash=sha256:7fad44dc9582570c7d92c4487d36ac46998f40cc39b438e8b8f5111a935ce4e8 \ + --hash=sha256:83fe6c020866a85acd7d97deccc45ff11d66daf42916d04396a4309c66c0ccb8 \ + --hash=sha256:87dc16b2df427c1318ad335f1e2be2b3b15b2cf20f7934c83b0505a48425ee5d \ + --hash=sha256:89f90e29b0966352811b12589f3a3c61943bf2bb9487b9d7bbec10efb1096bb5 \ + --hash=sha256:904cf89af220f8c6b2ed0296bb5065b474ce43b77558e48b2bf9de8b0ba17204 \ + --hash=sha256:9a45ea67235d965ef52187130d20002a4de20c54ea3d927a24286961d268dc37 \ + --hash=sha256:ad7b3a439265cc3739a4ab5b4c998c0e38ea99c0ee7ca4dea35c5d0b099ec237 \ + --hash=sha256:bb6dd6918460ed89cc7644adcc2402991474d6933cf1ce92b390641cb233fddf \ + --hash=sha256:d483b4aa3f5237569053f749cd1a2b5bb548ca456e40461a5dd087f21149d123 \ + --hash=sha256:e9f54c30cd52e3ef7fd034cc69b7bb7e0964e1c8f8743e018ab92e95b40f9eee + # via + # feast (pyproject.toml) # docling-ibm-models # easyocr # timm -tornado==6.5.2 \ - --hash=sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c \ - --hash=sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6 \ - --hash=sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef \ - --hash=sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4 \ - --hash=sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0 \ - --hash=sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e \ - --hash=sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882 \ - --hash=sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04 \ - --hash=sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0 \ - --hash=sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af \ - --hash=sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f \ - --hash=sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108 +tornado==6.5.7 \ + --hash=sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163 \ + --hash=sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2 \ + --hash=sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92 \ + --hash=sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b \ + --hash=sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972 \ + --hash=sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100 \ + --hash=sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4 \ + --hash=sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5 \ + --hash=sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4 \ + --hash=sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796 # via # ipykernel # jupyter-client @@ -5620,27 +6437,28 @@ tornado==6.5.2 \ # jupyterlab # notebook # terminado -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 # via - # feast (setup.py) + # feast (pyproject.toml) # datasets # docling # docling-ibm-models # great-expectations # huggingface-hub - # milvus-lite # mpire # semchunk + # sentence-transformers # transformers -traitlets==5.14.3 \ - --hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \ - --hash=sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f +traitlets==5.15.1 \ + --hash=sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92 \ + --hash=sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722 # via # ipykernel # ipython # ipywidgets + # jupyter-builder # jupyter-client # jupyter-core # jupyter-events @@ -5650,21 +6468,114 @@ traitlets==5.14.3 \ # nbclient # nbconvert # nbformat -transformers==4.57.0 \ - --hash=sha256:9d7c6d098c026e40d897e017ed1f481ab803cbac041021dbc6ae6100e4949b55 \ - --hash=sha256:d045753f3d93f9216e693cdb168698dfd2e9d3aad1bb72579a5d60ebf1545a8b +transformers==4.57.6 \ + --hash=sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550 \ + --hash=sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3 # via - # feast (setup.py) + # feast (pyproject.toml) # docling-core # docling-ibm-models -trino==0.336.0 \ - --hash=sha256:389150841446949119c3c2c13c1a51bb4be1a27818e40ae40dd3701f36c02550 \ - --hash=sha256:e82339e9fffe5c6c51de3bfdf28f083e3ae5945a4502739ab2094a0d08d68070 - # via feast (setup.py) -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) + # sentence-transformers +tree-sitter==0.26.0 \ + --hash=sha256:00289bfe7978f3e0dc0ce69813a20fa9f44ea4c100b3ec62043e5eb74ccfc3a2 \ + --hash=sha256:0f8793fd18ad7eec276ed4b51c097b4bf2002b357259b66b0d75db1f3f41c754 \ + --hash=sha256:10f0d4eb94aa7242dcb7f554bcd24dd7ba1c114f00d58759ba08c7a46c8ec51a \ + --hash=sha256:17a1c5cfd3a05d5c7c86bf4282b6ef8092c91dc0a98390499669c3fedb7d1814 \ + --hash=sha256:1d6fe0e8fb4df77b5ee816228e2c4475a63d8cc1d4d3a7ffd7097b2b87fc3e95 \ + --hash=sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90 \ + --hash=sha256:26c996c1edfee86e977bb3f5462e74fcec0d0b0db1e85a3c475875763caa03be \ + --hash=sha256:2f941cea06128c1f74f8937a8e2a90c7db49cf4be6647cd9e07d92a306d91517 \ + --hash=sha256:30a88be89ff1f2755297f81e8080d88b795dd98720c3f9fa2acf93873182cc95 \ + --hash=sha256:335294ce0504fcefde5245dff596778ffaf820205b98ae0b549c72e48855f1d8 \ + --hash=sha256:3f3c44339dd34fe8eb2b8d5aa7610660499a795f70376b130bbee7a437337280 \ + --hash=sha256:514a9bf8993e5210e7970736aaf6020d1759b670e195ef17b1c48f586aa30736 \ + --hash=sha256:526a165a2cb1d1f79e247d400f0e0acd8d49a817d6f312d543513af200b1f886 \ + --hash=sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa \ + --hash=sha256:5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4 \ + --hash=sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3 \ + --hash=sha256:6189c6c340c7384357711e3d92645e96bfb79f7a502f86de1ebdb23eb43f7dab \ + --hash=sha256:6cb2bd20efb2544c19ac54486ab7cb8ec7b36f913bbe1ce95df84acb96743d9c \ + --hash=sha256:7075ef857ef86f327dbb72d1e2574dda78db5754b3a1fca6506acd7fe5d561a7 \ + --hash=sha256:763627db05db34f12333081bd7422cc1c675893d373cc870b3e9249e200700e4 \ + --hash=sha256:7bcbadfa614326debef581957d5c780a9d7f66065c13deea61aa21d1dd36263f \ + --hash=sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52 \ + --hash=sha256:8ff2e0750b7daa722302838356d7b65e303829b7eb73c915df127ddba115e1d1 \ + --hash=sha256:918d89529786873f0982a0f59c2a303cd065fbfd1b903d71a8e4e1584f67b42e \ + --hash=sha256:93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f \ + --hash=sha256:94550e13b6ae576969da40246f4c4abb206380b5375ad43f26dd9151d55438e3 \ + --hash=sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c \ + --hash=sha256:b31a8195d2f224224c530ac814632d98c1dcc123d227442c07c736e86b70d564 \ + --hash=sha256:b40c219edccc4564530c96f8f1556f6202b37cda964d1cbd7bd2b7e68b40a245 \ + --hash=sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084 \ + --hash=sha256:bc6cb01d5ee75c85424aa1f1c72a82d8f07fd52539a0f3c4a6ed3e8721079b84 \ + --hash=sha256:c56581ad256c4195a21bfe449fed5d44a02fe83a4a7d6e70e6ec302c881191c7 \ + --hash=sha256:ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37 \ + --hash=sha256:dea4b4e27d49e9ec5b785d4f994da000e6726882fcc6ad05ec98478500c71aef \ + --hash=sha256:e9e46b664887d8c1014f1fb33e09454bbdd9ec1fe29b7fd02dde7b46bc1bb81a \ + --hash=sha256:ed0889dbed843ce45ede9f5169c0b2dea2222f12685844a03fadb81f12705867 \ + --hash=sha256:f289be0225ba2ace8e87d6c9639b2bc9ff2b5271afb7c5d39282a4a00e248682 \ + --hash=sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c \ + --hash=sha256:f9997ba61368c48ed54e715676afadf703947a1542464e39d047764fb3624b01 \ + --hash=sha256:ff527388df14cb5009f9274faf78cc69a7393ae6acf3b04784b8acca249519c5 \ + --hash=sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa + # via docling-core +tree-sitter-c==0.24.2 \ + --hash=sha256:1628584df0299b5a340aa63f8e67b6c97c91517f52fa7e7a4c557e40adb330a9 \ + --hash=sha256:4a2f4371cd816cc3153458f69062135ebb2ea5f275ddd90494e5c823d778204a \ + --hash=sha256:4d4579a8b54f0a442f903d88d3304cab77cd5c2031d4015baa4f2f8e15d6dcb7 \ + --hash=sha256:5041ef67eb68ce6bc8bb0b1f8ef3a5585ce523dae0c7eec109ab0627dd75aede \ + --hash=sha256:82842c5a5f2acd93f4de10038c33ac179c8979defc39376f990348d6289e933b \ + --hash=sha256:97bc80a224d48215d4e6e6376bf30d114f4c317b8145ff1b02afe785d4ba7bdd \ + --hash=sha256:abb549225091f7b25df2dd3a0143ece6e208f7055d8bcb4700b41ee79b9ef1e1 \ + --hash=sha256:c098bedcd5ac86ff93fa734d51d1dd86aed40fd5ed7d634c7af11380a0469969 \ + --hash=sha256:e2b42e8e22202c251f8629306f9321233542e07a6e01611b5fe83489272143eb + # via docling-core +tree-sitter-javascript==0.25.0 \ + --hash=sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b \ + --hash=sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54 \ + --hash=sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38 \ + --hash=sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b \ + --hash=sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1 \ + --hash=sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75 \ + --hash=sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc \ + --hash=sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc \ + --hash=sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c + # via docling-core +tree-sitter-python==0.25.0 \ + --hash=sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb \ + --hash=sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361 \ + --hash=sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762 \ + --hash=sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683 \ + --hash=sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5 \ + --hash=sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76 \ + --hash=sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac \ + --hash=sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b \ + --hash=sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d + # via docling-core +tree-sitter-typescript==0.23.2 \ + --hash=sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7 \ + --hash=sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478 \ + --hash=sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9 \ + --hash=sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31 \ + --hash=sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d \ + --hash=sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0 \ + --hash=sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8 \ + --hash=sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c + # via docling-core +trino==0.338.0 \ + --hash=sha256:093952f8a53b6f47ab357b77cbe92f1d66453290f7d8bcc4b74bd662e5ff2704 \ + --hash=sha256:1103d249cb96ba8f88f699588581965e2be088c16b9f8178e34cb64aa8a6c011 + # via feast (pyproject.toml) +truststore==0.10.4 \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 + # via + # httpcore2 + # httpx2 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) typer==0.12.5 \ --hash=sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b \ --hash=sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722 @@ -5672,65 +6583,66 @@ typer==0.12.5 \ # docling # docling-core # fastapi-mcp -types-cffi==1.17.0.20250915 \ - --hash=sha256:4362e20368f78dabd5c56bca8004752cc890e07a71605d9e0d9e069dbaac8c06 \ - --hash=sha256:cef4af1116c83359c11bb4269283c50f0688e9fc1d7f0eeb390f3661546da52c +types-cffi==2.0.0.20260518 \ + --hash=sha256:5b68a215a95d0eac4203b58e766ff7fe40c2e091b1fa1a9e54111f04cc560084 \ + --hash=sha256:f9707e66c13454789a58f8843d1ded4a66f1e9c8b10bd24d5eb5e0f25c0c5472 # via types-pyopenssl types-protobuf==3.19.22 \ --hash=sha256:d291388678af91bb045fafa864f142dc4ac22f5d4cdca097c7d8d8a32fa9b3ab \ --hash=sha256:d2b26861b0cb46a3c8669b0df507b7ef72e487da66d61f9f3576aa76ce028a83 # via - # feast (setup.py) + # feast (pyproject.toml) # mypy-protobuf -types-pymysql==1.1.0.20250916 \ - --hash=sha256:873eb9836bb5e3de4368cc7010ca72775f86e9692a5c7810f8c7f48da082e55b \ - --hash=sha256:98d75731795fcc06723a192786662bdfa760e1e00f22809c104fbb47bac5e29b - # via feast (setup.py) +types-pymysql==1.2.0.20260724 \ + --hash=sha256:71327a3fea0f680a280445f1b475c2307f68de05c2d3302a79df56af93c574cd \ + --hash=sha256:8493cf87e8df0a7b4e8c84305bc0aae69d3a8af9c6da7bfbb9a4c5c3f97d4a82 + # via feast (pyproject.toml) types-pyopenssl==24.1.0.20240722 \ --hash=sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39 \ --hash=sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54 # via types-redis -types-python-dateutil==2.9.0.20251008 \ - --hash=sha256:b9a5232c8921cf7661b29c163ccc56055c418ab2c6eabe8f917cbcc73a4c4157 \ - --hash=sha256:c3826289c170c93ebd8360c3485311187df740166dbab9dd3b792e69f2bc1f9c - # via - # feast (setup.py) - # arrow -types-pytz==2025.2.0.20250809 \ - --hash=sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5 \ - --hash=sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db - # via feast (setup.py) -types-pyyaml==6.0.12.20250915 \ - --hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \ - --hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6 - # via feast (setup.py) +types-python-dateutil==2.9.0.20260716 \ + --hash=sha256:1ae41d51a5c5f6bbeeb7f1f34df7086d55ff1605cf88d2ed71a7a276e1a7794e \ + --hash=sha256:1d55d1c3024bdb4861bb6a6622c9ec800c433d87bdc5b16fb84cdd0eed4ef2cb + # via feast (pyproject.toml) +types-pytz==2026.3.1.20260727 \ + --hash=sha256:42ac44e83645bfceb46597342c8fb8ec028a1407e2feae9c5c539986eab6b57a \ + --hash=sha256:4364075b6867dd15b210bb8c1d29727d609917129b45600defe1d4b3eda5ecb9 + # via feast (pyproject.toml) +types-pyyaml==6.0.12.20260724 \ + --hash=sha256:3c1ce1bb73cd5ec02e90390c2b1f00e810d241d8825fd73ff359696839271b6b \ + --hash=sha256:d57db930a4b2efbc57cf430ec8882765d246929432fa253092f383902329a453 + # via feast (pyproject.toml) types-redis==4.6.0.20241004 \ --hash=sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e \ --hash=sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed - # via feast (setup.py) + # via feast (pyproject.toml) types-requests==2.30.0.0 \ --hash=sha256:c6cf08e120ca9f0dc4fa4e32c3f953c3fba222bcc1db6b97695bce8da1ba9864 \ --hash=sha256:dec781054324a70ba64430ae9e62e7e9c8e4618c185a5cb3f87a6738251b5a31 - # via feast (setup.py) -types-setuptools==80.9.0.20250822 \ - --hash=sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965 \ - --hash=sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3 + # via feast (pyproject.toml) +types-setuptools==83.0.0.20260724 \ + --hash=sha256:eed1a91e1ed9d8ec9f3e71908e5bb17272e41e0852d37d1485ecd9cd33165e1a \ + --hash=sha256:fe2801176b667f1e8209f8571b0839ca6c34318d743825aeacf3f29970c8d46f # via - # feast (setup.py) + # feast (pyproject.toml) # types-cffi -types-tabulate==0.9.0.20241207 \ - --hash=sha256:ac1ac174750c0a385dfd248edc6279fa328aaf4ea317915ab879a2ec47833230 \ - --hash=sha256:b8dad1343c2a8ba5861c5441370c3e35908edd234ff036d4298708a1d4cf8a85 - # via feast (setup.py) +types-tabulate==0.10.0.20260508 \ + --hash=sha256:8e51f159e8b24976849706ae2ed1dc9adba8ebbd080b17e494ebb66a8cc92c74 \ + --hash=sha256:b1e1a2d0456fbd655a71690b09a7aaeffdf2978d32049184ea436492aa51d20a + # via feast (pyproject.toml) types-urllib3==1.26.25.14 \ --hash=sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f \ --hash=sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e # via types-requests -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiobotocore + # aiohttp # aiosignal + # alembic # anyio # async-lru # azure-core @@ -5738,28 +6650,42 @@ typing-extensions==4.15.0 \ # azure-storage-blob # beautifulsoup4 # couchbase-columnar + # cryptography # docling-core # elasticsearch # exceptiongroup # fastapi + # graphene # great-expectations + # grpcio + # httpx2 # huggingface-hub # ibis-framework # ipython + # jupyter-client + # jupyterlab # jwcrypto + # mcp + # mcp-types # minio # mistune + # mlflow-skinny # multidict # mypy + # opentelemetry-api + # opentelemetry-sdk + # opentelemetry-semantic-conventions + # oracledb # psycopg # psycopg-pool # pydantic # pydantic-core + # pyjwt # pyopenssl # python-docx # python-pptx # referencing - # rich + # sentence-transformers # snowflake-connector-python # sqlalchemy # starlette @@ -5772,114 +6698,36 @@ typing-extensions==4.15.0 \ typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via pydantic-settings -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via pandas -tzlocal==5.3.1 \ - --hash=sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd \ - --hash=sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via + # arrow + # ibis-framework + # pandas +tzlocal==5.4.4 \ + --hash=sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4 \ + --hash=sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15 # via # great-expectations # trino -ujson==5.11.0 \ - --hash=sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80 \ - --hash=sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef \ - --hash=sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a \ - --hash=sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840 \ - --hash=sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53 \ - --hash=sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076 \ - --hash=sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab \ - --hash=sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d \ - --hash=sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89 \ - --hash=sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c \ - --hash=sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb \ - --hash=sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c \ - --hash=sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823 \ - --hash=sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5 \ - --hash=sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac \ - --hash=sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d \ - --hash=sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723 \ - --hash=sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6 \ - --hash=sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0 \ - --hash=sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328 \ - --hash=sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0 \ - --hash=sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04 \ - --hash=sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25 \ - --hash=sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49 \ - --hash=sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec \ - --hash=sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3 \ - --hash=sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3 \ - --hash=sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc \ - --hash=sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a \ - --hash=sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9 \ - --hash=sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302 \ - --hash=sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694 \ - --hash=sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547 \ - --hash=sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01 \ - --hash=sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02 \ - --hash=sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6 \ - --hash=sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60 \ - --hash=sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915 \ - --hash=sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835 \ - --hash=sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701 \ - --hash=sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702 \ - --hash=sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50 \ - --hash=sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6 \ - --hash=sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc \ - --hash=sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a \ - --hash=sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c \ - --hash=sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c \ - --hash=sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03 \ - --hash=sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629 \ - --hash=sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105 \ - --hash=sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844 \ - --hash=sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241 \ - --hash=sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a \ - --hash=sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26 \ - --hash=sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac \ - --hash=sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596 \ - --hash=sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9 \ - --hash=sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752 \ - --hash=sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138 \ - --hash=sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b \ - --hash=sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba \ - --hash=sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5 \ - --hash=sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018 \ - --hash=sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362 \ - --hash=sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746 \ - --hash=sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f \ - --hash=sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88 \ - --hash=sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638 \ - --hash=sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b \ - --hash=sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9 \ - --hash=sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db \ - --hash=sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f \ - --hash=sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58 \ - --hash=sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b \ - --hash=sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433 \ - --hash=sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0 \ - --hash=sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764 \ - --hash=sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c \ - --hash=sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34 \ - --hash=sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052 \ - --hash=sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba \ - --hash=sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c \ - --hash=sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc \ - --hash=sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39 - # via pymilvus uri-template==1.3.0 \ --hash=sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7 \ --hash=sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363 # via jsonschema -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via - # feast (setup.py) + # feast (pyproject.toml) # botocore # clickhouse-connect + # databricks-sdk # docker # elastic-transport # great-expectations @@ -5889,178 +6737,192 @@ urllib3==2.5.0 \ # requests # responses # testcontainers -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # mcp + # mlflow-skinny # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn virtualenv==20.23.0 \ --hash=sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e \ --hash=sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924 # via - # feast (setup.py) + # feast (pyproject.toml) # pre-commit -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn -wcwidth==0.2.14 \ - --hash=sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605 \ - --hash=sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 +wcwidth==0.8.2 \ + --hash=sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda \ + --hash=sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85 # via prompt-toolkit -webcolors==24.11.1 \ - --hash=sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9 \ - --hash=sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6 +webcolors==25.10.0 \ + --hash=sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d \ + --hash=sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf # via jsonschema webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ @@ -6074,461 +6936,534 @@ websocket-client==1.9.0 \ # via # jupyter-server # kubernetes -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -werkzeug==3.1.3 \ - --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ - --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 - # via moto -wheel==0.45.1 \ - --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ - --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 +werkzeug==3.1.8 \ + --hash=sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50 \ + --hash=sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44 + # via + # flask + # moto +wheel==0.47.0 \ + --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \ + --hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3 # via # pip-tools # singlestoredb -widgetsnbextension==4.0.14 \ - --hash=sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575 \ - --hash=sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af +widgetsnbextension==4.0.15 \ + --hash=sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366 \ + --hash=sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9 # via ipywidgets -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 # via # aiobotocore + # deprecated # testcontainers xlsxwriter==3.2.9 \ --hash=sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c \ --hash=sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3 # via python-pptx -xmltodict==1.0.2 \ - --hash=sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649 \ - --hash=sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d +xmltodict==1.0.4 \ + --hash=sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61 \ + --hash=sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a # via moto -xxhash==3.6.0 \ - --hash=sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad \ - --hash=sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c \ - --hash=sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3 \ - --hash=sha256:01be0c5b500c5362871fc9cfdf58c69b3e5c4f531a82229ddb9eb1eb14138004 \ - --hash=sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b \ - --hash=sha256:02ea4cb627c76f48cd9fb37cf7ab22bd51e57e1b519807234b473faebe526796 \ - --hash=sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f \ - --hash=sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c \ - --hash=sha256:0d50101e57aad86f4344ca9b32d091a2135a9d0a4396f19133426c88025b09f1 \ - --hash=sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1 \ - --hash=sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0 \ - --hash=sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec \ - --hash=sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d \ - --hash=sha256:18b242455eccdfcd1fa4134c431a30737d2b4f045770f8fe84356b3469d4b919 \ - --hash=sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67 \ - --hash=sha256:1fc1ed882d1e8df932a66e2999429ba6cc4d5172914c904ab193381fba825360 \ - --hash=sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799 \ - --hash=sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679 \ - --hash=sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef \ - --hash=sha256:2762bfff264c4e73c0e507274b40634ff465e025f0eaf050897e88ec8367575d \ - --hash=sha256:277175a73900ad43a8caeb8b99b9604f21fe8d7c842f2f9061a364a7e220ddb7 \ - --hash=sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8 \ - --hash=sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa \ - --hash=sha256:2ab89a6b80f22214b43d98693c30da66af910c04f9858dd39c8e570749593d7e \ - --hash=sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa \ - --hash=sha256:2f171a900d59d51511209f7476933c34a0c2c711078d3c80e74e0fe4f38680ec \ - --hash=sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4 \ - --hash=sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad \ - --hash=sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7 \ - --hash=sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5 \ - --hash=sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11 \ - --hash=sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae \ - --hash=sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d \ - --hash=sha256:44e342e8cc11b4e79dae5c57f2fb6360c3c20cc57d32049af8f567f5b4bcb5f4 \ - --hash=sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6 \ - --hash=sha256:45aae0c9df92e7fa46fbb738737324a563c727990755ec1965a6a339ea10a1df \ - --hash=sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058 \ - --hash=sha256:4903530e866b7a9c1eadfd3fa2fbe1b97d3aed4739a80abf506eb9318561c850 \ - --hash=sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2 \ - --hash=sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d \ - --hash=sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89 \ - --hash=sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e \ - --hash=sha256:4da8168ae52c01ac64c511d6f4a709479da8b7a4a1d7621ed51652f93747dffa \ - --hash=sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6 \ - --hash=sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb \ - --hash=sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3 \ - --hash=sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b \ - --hash=sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4 \ - --hash=sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db \ - --hash=sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119 \ - --hash=sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec \ - --hash=sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518 \ - --hash=sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296 \ - --hash=sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033 \ - --hash=sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729 \ - --hash=sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca \ - --hash=sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063 \ - --hash=sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5 \ - --hash=sha256:6551880383f0e6971dc23e512c9ccc986147ce7bfa1cd2e4b520b876c53e9f3d \ - --hash=sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f \ - --hash=sha256:6965e0e90f1f0e6cb78da568c13d4a348eeb7f40acfd6d43690a666a459458b8 \ - --hash=sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42 \ - --hash=sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e \ - --hash=sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392 \ - --hash=sha256:780b90c313348f030b811efc37b0fa1431163cb8db8064cf88a7936b6ce5f222 \ - --hash=sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f \ - --hash=sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd \ - --hash=sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77 \ - --hash=sha256:7c35c4cdc65f2a29f34425c446f2f5cdcd0e3c34158931e1cc927ece925ab802 \ - --hash=sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d \ - --hash=sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1 \ - --hash=sha256:7dac94fad14a3d1c92affb661021e1d5cbcf3876be5f5b4d90730775ccb7ac41 \ - --hash=sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374 \ - --hash=sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263 \ - --hash=sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71 \ - --hash=sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13 \ - --hash=sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8 \ - --hash=sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc \ - --hash=sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62 \ - --hash=sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11 \ - --hash=sha256:9085e798c163ce310d91f8aa6b325dda3c2944c93c6ce1edb314030d4167cc65 \ - --hash=sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0 \ - --hash=sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b \ - --hash=sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2 \ - --hash=sha256:97460eec202017f719e839a0d3551fbc0b2fcc9c6c6ffaa5af85bbd5de432788 \ - --hash=sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6 \ - --hash=sha256:9e040d3e762f84500961791fa3709ffa4784d4dcd7690afc655c095e02fff05f \ - --hash=sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc \ - --hash=sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e \ - --hash=sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702 \ - --hash=sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405 \ - --hash=sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f \ - --hash=sha256:a75ffc1bd5def584129774c158e108e5d768e10b75813f2b32650bb041066ed6 \ - --hash=sha256:a87f271a33fad0e5bf3be282be55d78df3a45ae457950deb5241998790326f87 \ - --hash=sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3 \ - --hash=sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a \ - --hash=sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b \ - --hash=sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b \ - --hash=sha256:b0359391c3dad6de872fefb0cf5b69d55b0655c55ee78b1bb7a568979b2ce96b \ - --hash=sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8 \ - --hash=sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db \ - --hash=sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99 \ - --hash=sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a \ - --hash=sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2 \ - --hash=sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204 \ - --hash=sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b \ - --hash=sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546 \ - --hash=sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95 \ - --hash=sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9 \ - --hash=sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54 \ - --hash=sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06 \ - --hash=sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c \ - --hash=sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152 \ - --hash=sha256:c2f9ccd5c4be370939a2e17602fbc49995299203da72a3429db013d44d590e86 \ - --hash=sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4 \ - --hash=sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93 \ - --hash=sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd \ - --hash=sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd \ - --hash=sha256:cc604dc06027dbeb8281aeac5899c35fcfe7c77b25212833709f0bff4ce74d2a \ - --hash=sha256:cfbc5b91397c8c2972fdac13fb3e4ed2f7f8ccac85cd2c644887557780a9b6e2 \ - --hash=sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248 \ - --hash=sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd \ - --hash=sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6 \ - --hash=sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf \ - --hash=sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7 \ - --hash=sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490 \ - --hash=sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0 \ - --hash=sha256:e4ff728a2894e7f436b9e94c667b0f426b9c74b71f900cf37d5468c6b5da0536 \ - --hash=sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb \ - --hash=sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829 \ - --hash=sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746 \ - --hash=sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07 \ - --hash=sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292 \ - --hash=sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6 \ - --hash=sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd \ - --hash=sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7 \ - --hash=sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d \ - --hash=sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0 \ - --hash=sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee \ - --hash=sha256:ffc578717a347baf25be8397cb10d2528802d24f94cfc005c0e44fef44b5cdd6 +xxhash==3.8.1 \ + --hash=sha256:000435984a0469b0f822fe76f35bddea0f96a4d6521b3339a60a6428cdee1edc \ + --hash=sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3 \ + --hash=sha256:01cab782f8a0a05ecad2c63d7ef10f7ab475f660e0d6419d069418c14d88de7c \ + --hash=sha256:0204701e6d01f64254e0e5ff4255812b1febe027ddd7dda63372e27f98b5e91f \ + --hash=sha256:027dee4355f3fcc41481650d846cf6cfc895c85a1ab7acd063063821a0df5b4c \ + --hash=sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec \ + --hash=sha256:0418ec8b2331b9d4d575fc9284427e8e69449d7172e99e1a86fcdd1f51a0a937 \ + --hash=sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92 \ + --hash=sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a \ + --hash=sha256:09a204dd4bb0823daf938cdd0dc8057d5f1e14fe3cbde929424255f23f9de872 \ + --hash=sha256:0dfdf19b0d5433a75d61f19dc85737af0f0b95e445c1ad69c855115d05efed45 \ + --hash=sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3 \ + --hash=sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31 \ + --hash=sha256:1153265daa10750a9bf8e9b01753d7618024a300925591efaf16b1b7fa536699 \ + --hash=sha256:115772daeb71b2f3b9381177017f53e6cf3f3439c840737fdabd21aba6e54920 \ + --hash=sha256:12a3cf79dadbab9631230ebc4c51c7c60f1e9cdfb890c15fb733eaafe2e7713c \ + --hash=sha256:12eaeaa9ab8b9e6033a1fa5f6b338aaf55ff4df4bee11b59fd6ee03b19186ee4 \ + --hash=sha256:131324f719957b988861714de7d6ddf57b47abec3b0cc691302ffeaba0e05e10 \ + --hash=sha256:15790b686f8723b845fec6f612a343beb815a25c83117a7fa408d7c8ee5aa8fd \ + --hash=sha256:1731407102b9332cd3c9dadee07db498bc3d437b95d752b5b1a5f7eb730a3738 \ + --hash=sha256:1b86ae798a976ccbc1d02af6ccb98f5b4d24756b1f65e995f11d10fe071f486f \ + --hash=sha256:1c332dd48b8cb050da2bb2a3c96d72b1664168650a250ef9718e423df7989e05 \ + --hash=sha256:1da930bbcac3e8fbe2191850e2abb57977a99348c12c4b385e1058ac1b0a9ecc \ + --hash=sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329 \ + --hash=sha256:1ffcc98d8878e449e86dec008cea6f44cfd3a954d2ef24ae7d1cc9f725beec7d \ + --hash=sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215 \ + --hash=sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724 \ + --hash=sha256:23e710118a5778a45db740b431943a3f2a82a571a052c2768cce6544d9c8c62e \ + --hash=sha256:264710bd335016f303763ce1275c6486df30bb57c2245c91b224c983d7ac39b8 \ + --hash=sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62 \ + --hash=sha256:27a9e475157f7315826118e3f3127909a0fe25f1b43d3d3be9c584f9d265f937 \ + --hash=sha256:27cfc2f1ed76f956f36dfe0c56e5f5a3e94cd91eb78b893f63e2ef2ae404fcdf \ + --hash=sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d \ + --hash=sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75 \ + --hash=sha256:2f1c68394818e0595569c2ff3cbc1e6d5a36a434e796f5c526b987b80c8a8c62 \ + --hash=sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8 \ + --hash=sha256:314d05fbc55719ae2438eaaba77bf2508ca4f030b26fa4c9c8c380e81c48fa33 \ + --hash=sha256:32a94ad2763e0263d9102037d349002c3d3c401e42770542c3eeb4801f311661 \ + --hash=sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0 \ + --hash=sha256:345b07b78e2bf583d71682aa34ae5b5fab575f7a1cb31e10263ebbc6f89f8c42 \ + --hash=sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893 \ + --hash=sha256:358650d5bda9c635da699c53adf4e8134af492ecc79c960f917eebf088bb6799 \ + --hash=sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887 \ + --hash=sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629 \ + --hash=sha256:38c887aedb696ef8bca19983206d270848558cfae4a91afa6a2fb05dde58ffc5 \ + --hash=sha256:39c9d5b61508b0bb68f29e54546de0ed2a74943c6a18585535a7e37356f1dd12 \ + --hash=sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf \ + --hash=sha256:3bc0fa90830df1e1277f33cc6e55de9990b83c0319fd8c7412866cfde38b025e \ + --hash=sha256:3c0d84c5f2e086b120bae4e7f551cbda804c1deb10d958478bed4f89ba286dfe \ + --hash=sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913 \ + --hash=sha256:402db908ea70eaf9800d9182a66596fc86f36655df8f63fdecf7c11da741d86f \ + --hash=sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e \ + --hash=sha256:4482380b462ca9e59994d072a877ecadd1cf51102daeeab2db696f96ab763723 \ + --hash=sha256:44c89d915a75c11d2547eaee9098fcd80398987c4bff2974a0497a925bf92c07 \ + --hash=sha256:454d78e786602278a2a4383d08048482052f4f0c61fa677ca590af08914d9bca \ + --hash=sha256:460261045936975193bfd20549a0de1cd52a33b405cbb972f0d80940c42266cd \ + --hash=sha256:46b39976d008e2a845758650f0ff7136bca004f40da0c8798bd37ac37860154f \ + --hash=sha256:498017fbf2d13a768b3110d084bde39f2bd8664c1de0b8084f8ccc84425b7c88 \ + --hash=sha256:49aa8692507835dcc1e8ad8021f20c74c2dc13d83b5112e87877faa2a0035b20 \ + --hash=sha256:4b512261801b1e5fde7b6ebf2fef7977339c620cbbca88a0040ad9ad134f4d02 \ + --hash=sha256:4bec8b2c909bcfae9a0dc702346007e02a8c9ba5bbde83ffb224aa194f4f9efc \ + --hash=sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478 \ + --hash=sha256:4d6e88ddb3c741fbf29e1e7faf429880f8cd1d7aff4303247435a549726b4fb1 \ + --hash=sha256:4df57c0b161ec1b3ed0526a67b0db0914b557e86ee8aae51887aec941b261542 \ + --hash=sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa \ + --hash=sha256:4ef09bbc2519a93cd0f95f2ceb5f7b85919dffea643278e02362bf40e3c4bed1 \ + --hash=sha256:5013be3bea7612852c62a7437f3302c1cfb91ca7e703b194459db0b2b2e0d792 \ + --hash=sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed \ + --hash=sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647 \ + --hash=sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55 \ + --hash=sha256:53f3ed9118397074ff63a79b66b7fec1c84c782eecde35c5bc94e420a971c231 \ + --hash=sha256:559e3cabe522231909f9de98ef06929edbd53782046bd21aae0c72db6f2a0775 \ + --hash=sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398 \ + --hash=sha256:57f80a898544db78ec6b0be6183bd1bc008933193d4199f5cde36b0e6bd5e062 \ + --hash=sha256:58346024d47e84f7d8b3e7f5d6faa1d58acbbe49a8771497872059f58c1d8ea5 \ + --hash=sha256:594131ce1aad18db3689781f806db1b065cdaa04f4df36b4c038d2013aefd0bf \ + --hash=sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22 \ + --hash=sha256:5c566b123dce7e4867ca518434cdfb9f84e5023771235b2e3107a26c9a41cbd8 \ + --hash=sha256:5d3dfb1f0ff146da7952867a9414f0c7a29762f8825a84879592612fd6139342 \ + --hash=sha256:5da703225374e3a4c8d4fd90e26fe7213a52004ec77f88b42b42e9e86d8c6d57 \ + --hash=sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104 \ + --hash=sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb \ + --hash=sha256:602efcad4a42c184e81d43a2b7e6e4f524d619878f2b6ee2ba469011f47c8147 \ + --hash=sha256:614bca2c7cfa87ec95b703e691c3c5eb6c448b6dabbe9776ac53883152951729 \ + --hash=sha256:632a34590c090d1285ed5efa5a02be919f3f9a56a64bd25f693fe1e2d27a27fb \ + --hash=sha256:64af54dd1c3a45a27c04942f9a1a4683322bdd127f4745cca4e02549c1d2d2bb \ + --hash=sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170 \ + --hash=sha256:656256c9f9303e47f07d5cb8ae4468285370adfafd7ba48aea33a458e7697626 \ + --hash=sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65 \ + --hash=sha256:6c7574528bc922f8757f34dd78ed60ab52b1c7973b630f5eae7ba33ec133ce71 \ + --hash=sha256:6cee733fe4ccb1737e0997135283c82341e5cfa9cf214b165f9087fb663aaf4f \ + --hash=sha256:6cf633fe83b1d4e6519d7259b33afe40fbba5d3f438730156971dd0cf7730610 \ + --hash=sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113 \ + --hash=sha256:717b12fdc51819833704e85e6926d76981ffa3f780ef92e33ebb8b26d46bb230 \ + --hash=sha256:7258ee276e8772599bc19e14b36f6260306e21b637190cd7cb489a2449d48684 \ + --hash=sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629 \ + --hash=sha256:7345007c12780985de4fd740148776d1eee18c0d41407c6fa1e48c5450304fe5 \ + --hash=sha256:747476436f6891b9773374ce8d48edcc8b12cb5b61b67c6fb6289633747d088f \ + --hash=sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9 \ + --hash=sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276 \ + --hash=sha256:78c794b643d214f1522e7a288bcf5a2de120d26cd170516749a4009dc92722c9 \ + --hash=sha256:7b5f97ecfede10d5b2870383620e2d25c8561e217c7bf9081073802b54248d2b \ + --hash=sha256:7dc4bdf008f77c88d544849c48c1a40faf25a5eff6cc466de2e8edc37c191fce \ + --hash=sha256:81f4ed9ca9644bc95cd976bfe10f7a4cafab8ffdc3aed52877d4600e445be7ef \ + --hash=sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc \ + --hash=sha256:8304be0982130954b7fd3aad18e2c6f8ee40254bc3d2e635991c16d77c91e2bd \ + --hash=sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061 \ + --hash=sha256:836f11d4474d3228e9909d97216faa4f7505df41cfaf3927eb29809de785a78d \ + --hash=sha256:83b9130b80b216d56fdf9e87131946b353c9627930c061955a101ea82b09fed9 \ + --hash=sha256:83d879362ddd0fedd3f2ab8ce7cce3da2049a6d51d16da8af73011c6edf4752f \ + --hash=sha256:848182a391fffdc25605443e832f5b443f25498edeccf9a64343fd84421ca04b \ + --hash=sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc \ + --hash=sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56 \ + --hash=sha256:89b11a5cdd441aa463f6d34ca0241602bc09b001a76994b6059828494108c673 \ + --hash=sha256:89df64c10adfe340fb00330042537cdd6bf0d8d78bad73f29cfe5427eed7b084 \ + --hash=sha256:8ea8a141eeced4f6262ab6dd71c681ac546a558c30bb586abe087d814b5f85ea \ + --hash=sha256:8f454166c2ffed45636c8d501741e649851ba2f346c4eb73a64c07ac00428f20 \ + --hash=sha256:8f759eed402448c2bdbb492e4fba1f20668ffe29688605ea61f0f67f9e4e386d \ + --hash=sha256:9043877a917be88ccf230aa5667c1bd059bce80f4c2727e4defa1b29b7f48b08 \ + --hash=sha256:942bc86e9be6fdd6e1175048f5fe8f8fdaaf2309dd1323ef1e155a69cd346780 \ + --hash=sha256:947a585bcaa235702b7c59433b485489397f9a163b3f56058b9463a46fd9b74c \ + --hash=sha256:950ac754d16daea42038f38e7465eb84cda4d08d7343c1c915771b29470f065a \ + --hash=sha256:98d8ac1129b4dd39098cffed94d1284aceb61c3aa396757ccc736ac392e4cee5 \ + --hash=sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437 \ + --hash=sha256:9b2ce44bf8f4a1d01f418b3110ff8dff32fd3f3e836c0e06333c3725f243fa6c \ + --hash=sha256:9d45eee3a95a8b61e5b568580caac91f1502ddb731aaf8f4aa448a98660b2fb4 \ + --hash=sha256:9db455cb649dcfe4504d6d68a6d83a7315a99a3ca59871dc3ff840671f99adba \ + --hash=sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396 \ + --hash=sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0 \ + --hash=sha256:9f23083e1bd9d901f844af7a126727c486e7eada9a1a6791c8f7e73f94fac656 \ + --hash=sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907 \ + --hash=sha256:a5cd96f6dcdf4fa657b2d95668d71d58455248f98712ecffaa9c528edf40ccae \ + --hash=sha256:a5eed9d41995a83f3332b4e3396abb7f433cac584222bd7e305b606d8353861e \ + --hash=sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a \ + --hash=sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda \ + --hash=sha256:a98b2f95cab589e0f5e92c48431afb4d56238b8bf6668edcc66166180e9b509b \ + --hash=sha256:ad52a0e4bcc0ba956a953a169d1feec2734a64981d689e4fc8f490f7bf91af60 \ + --hash=sha256:af0c9fedc4a2c24e8664953882fe8185f3790b8338c9c700f76f5ad660817711 \ + --hash=sha256:afe6380a0e9653a87aa1e6e88fb47718113e5563c7a1cb2bcc23c1d8e17e3961 \ + --hash=sha256:affb37f152e55b5e4494bb9d0107f7bb08515c6704fbed82d9f61214d74adc17 \ + --hash=sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf \ + --hash=sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46 \ + --hash=sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2 \ + --hash=sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6 \ + --hash=sha256:b3e1107fe5ca030f946dfa59fdbb66b5df121c8432f14b0bdd282d17b297f4eb \ + --hash=sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a \ + --hash=sha256:b6fa3116e40e14e7782fb1a9f872f94b5997de21127c95545ce40196ac1351c5 \ + --hash=sha256:bb70573d2995d23932e2871120f78d798ebc3572e54c09e694a18ced95c5f8d9 \ + --hash=sha256:bbcdf9c92d21c65bc75426eecea724c8fa0d35a6e201fdf1630011d4cc3aa685 \ + --hash=sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315 \ + --hash=sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e \ + --hash=sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc \ + --hash=sha256:c240939e963653054fc7e4a17c382829cda4aa88a7daf0af841715dbded1b497 \ + --hash=sha256:c31a2649bcf1fe97cf11c79848d761df33ac46b3896942d31b640557b486ff6b \ + --hash=sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e \ + --hash=sha256:c73b6f652f0745425aa6378319c331293b5341756262e9408ed3d45f183375e6 \ + --hash=sha256:c85949d02c85adf6d786eb94858e124989a632a4e65739835b2fc5761827fac3 \ + --hash=sha256:c919f38cd3f0b5e8d30b81fd6cac688cf9221560340f0c35cbbb8b2bd77ad6ac \ + --hash=sha256:c959f88160b13b4e730b0d75b459b7929fc0d2225c284c9683ac95d6feeeac6a \ + --hash=sha256:cb3fe820c27593f170770d6c8d791936cf6275d9269405fbb7b30a55363c10c8 \ + --hash=sha256:cf399fac542a1c7a4734a435b93df2c55e858c7d31abf6c1bdf46f9ae67fbfd0 \ + --hash=sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068 \ + --hash=sha256:d247b34bf433c92b41689318fd25d246313cab2275a6a47e2efac178b80d6efe \ + --hash=sha256:d48acabb1e5cb0071009f80d71d7f01b6ba2c1d4b869b1352bb5df3f11bf7dfd \ + --hash=sha256:d5006c65ec507a333479e76e00e2c368781f16c24ededa764763956b32a0e93e \ + --hash=sha256:d58ce8b6cfa9c4d2f230557f69caf7c06369e318015d0b19485095bc2c5963ab \ + --hash=sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838 \ + --hash=sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297 \ + --hash=sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c \ + --hash=sha256:db77278a6eddadbf44ce5aae2fee5ebb4d061f026b1ce2130d058cd4d7a7b670 \ + --hash=sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975 \ + --hash=sha256:e14800b9b10bb39d7a60ad4a310e403164d7b8988a27ae933d4e40618a44088e \ + --hash=sha256:e2a845687219ba3214126f14a8a5861f97c9e065a7d0b8252adb6df13eea86fb \ + --hash=sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0 \ + --hash=sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1 \ + --hash=sha256:e605e0b8abca9457abd5bee737e086ab145a20c25083ef1113013612268872ff \ + --hash=sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc \ + --hash=sha256:e710ad822c493fb80a4fbc1e3d0a807b1422cb90adbe64378f98291b7fa48fef \ + --hash=sha256:ea6a3e734b0fd41b82784a400be946821900daebe610c050a5e0760838a34f99 \ + --hash=sha256:ec55d80e9b8a519d742669e0b49e8ce9e6747be42bf3c138158b6543a9c8e489 \ + --hash=sha256:ed8bcdab6692fd4ad0dd6241807a24a640a376764460023b8d462d745e6b7b27 \ + --hash=sha256:f377012b86c0a23a1df0cf5a1b05aa7187649e472f71c7892e5f2c2815bbe74f \ + --hash=sha256:f6114692261eff4266386cdec0f7d87eee24e317ab397c218b7ae6a76b4c6339 \ + --hash=sha256:f8044cf4c77f37968b8c4cbcbf7a0f355d8a437877ae18eba23e3aad953a6cc7 \ + --hash=sha256:f8ed8940435834141061da26d27c4dd0d18fb69777bf431f5c6cc46b43349113 \ + --hash=sha256:f93e408255ddce525189bf11feaa1be7ee35e55f486c299c97d9caa68d724a5b \ + --hash=sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1 # via datasets -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8 \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf \ - --hash=sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890 \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737 \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba \ - --hash=sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9 \ - --hash=sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0 \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147 \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e \ - --hash=sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2 \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6 \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750 \ + --hash=sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621 \ + --hash=sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13 \ + --hash=sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1 \ + --hash=sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2 \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata zstandard==0.25.0 \ --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \ @@ -6631,5 +7566,5 @@ zstandard==0.25.0 \ --hash=sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551 \ --hash=sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01 # via - # clickhouse-connect + # pyiceberg # trino diff --git a/sdk/python/requirements/py3.10-minimal-requirements.txt b/sdk/python/requirements/py3.10-minimal-requirements.txt index 32ea73edcc9..47ad051f9e7 100644 --- a/sdk/python/requirements/py3.10-minimal-requirements.txt +++ b/sdk/python/requirements/py3.10-minimal-requirements.txt @@ -1,152 +1,160 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.10 --no-strip-extras setup.py --extra minimal --generate-hashes --output-file sdk/python/requirements/py3.10-minimal-requirements.txt -aiobotocore==2.23.1 \ - --hash=sha256:a59f2a78629b97d52f10936b79c73de64e481a8c44a62c1871f088df6c1afc4f \ - --hash=sha256:d81c54d2eae2406ea9a473fea518fed580cf37bc4fc51ce43ba81546e5305114 - # via feast (setup.py) -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +# uv pip compile -p 3.10 --no-strip-extras pyproject.toml --extra minimal --generate-hashes --output-file sdk/python/requirements/py3.10-minimal-requirements.txt +aiobotocore==3.8.0 \ + --hash=sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670 \ + --hash=sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + # via feast (pyproject.toml) +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.0 \ - --hash=sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6 \ - --hash=sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a \ - --hash=sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e \ - --hash=sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971 \ - --hash=sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54 \ - --hash=sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2 \ - --hash=sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96 \ - --hash=sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469 \ - --hash=sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26 \ - --hash=sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0 \ - --hash=sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29 \ - --hash=sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885 \ - --hash=sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579 \ - --hash=sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a \ - --hash=sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1 \ - --hash=sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b \ - --hash=sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852 \ - --hash=sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865 \ - --hash=sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad \ - --hash=sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092 \ - --hash=sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7 \ - --hash=sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b \ - --hash=sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8 \ - --hash=sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820 \ - --hash=sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd \ - --hash=sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee \ - --hash=sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4 \ - --hash=sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67 \ - --hash=sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d \ - --hash=sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1 \ - --hash=sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d \ - --hash=sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114 \ - --hash=sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1 \ - --hash=sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755 \ - --hash=sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04 \ - --hash=sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca \ - --hash=sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1 \ - --hash=sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae \ - --hash=sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793 \ - --hash=sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e \ - --hash=sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a \ - --hash=sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8 \ - --hash=sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602 \ - --hash=sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778 \ - --hash=sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f \ - --hash=sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49 \ - --hash=sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77 \ - --hash=sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7 \ - --hash=sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f \ - --hash=sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0 \ - --hash=sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519 \ - --hash=sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d \ - --hash=sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370 \ - --hash=sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7 \ - --hash=sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247 \ - --hash=sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d \ - --hash=sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80 \ - --hash=sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9 \ - --hash=sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c \ - --hash=sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d \ - --hash=sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020 \ - --hash=sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504 \ - --hash=sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca \ - --hash=sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731 \ - --hash=sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd \ - --hash=sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6 \ - --hash=sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21 \ - --hash=sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1 \ - --hash=sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620 \ - --hash=sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64 \ - --hash=sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8 \ - --hash=sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3 \ - --hash=sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5 \ - --hash=sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985 \ - --hash=sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813 \ - --hash=sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed \ - --hash=sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20 \ - --hash=sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03 \ - --hash=sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702 \ - --hash=sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6 \ - --hash=sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed \ - --hash=sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353 \ - --hash=sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108 \ - --hash=sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f \ - --hash=sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409 \ - --hash=sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7 \ - --hash=sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3 \ - --hash=sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9 \ - --hash=sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba \ - --hash=sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe \ - --hash=sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c \ - --hash=sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861 \ - --hash=sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a \ - --hash=sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0 \ - --hash=sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390 \ - --hash=sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3 \ - --hash=sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90 \ - --hash=sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802 \ - --hash=sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163 \ - --hash=sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d \ - --hash=sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430 \ - --hash=sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a \ - --hash=sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b \ - --hash=sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6 \ - --hash=sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b \ - --hash=sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657 \ - --hash=sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c \ - --hash=sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93 \ - --hash=sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f \ - --hash=sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82 \ - --hash=sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31 \ - --hash=sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc \ - --hash=sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52 \ - --hash=sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c \ - --hash=sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a \ - --hash=sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52 \ - --hash=sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7 \ - --hash=sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b \ - --hash=sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147 \ - --hash=sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4 - # via aiobotocore -aioitertools==0.12.0 \ - --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \ - --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 + # via + # aiobotocore + # kubernetes +aioitertools==0.13.0 \ + --hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \ + --hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c # via aiobotocore aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 # via aiohttp -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via + # fastapi + # typer +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # httpx + # httpx2 # mcp # sse-starlette # starlette @@ -155,459 +163,444 @@ asn1crypto==1.5.1 \ --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 # via snowflake-connector-python +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy async-timeout==5.0.1 \ --hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3 # via # aiohttp # redis -atpublic==5.1 \ - --hash=sha256:135783dbd887fbddb6ef032d104da70c124f2b44b9e2d79df07b9da5334825e3 \ - --hash=sha256:abc1f4b3dbdd841cc3539e4b5e4f3ad41d658359de704e30cb36da4d4e9d3022 +atpublic==7.0.0 \ + --hash=sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e \ + --hash=sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b # via ibis-framework -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via + # feast (pyproject.toml) # aiohttp # jsonschema # referencing -backports-datetime-fromisoformat==2.0.3 \ - --hash=sha256:1314d4923c1509aa9696712a7bc0c7160d3b7acf72adafbbe6c558d523f5d491 \ - --hash=sha256:1ea2cc84224937d6b9b4c07f5cb7c667f2bde28c255645ba27f8a675a7af8234 \ - --hash=sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45 \ - --hash=sha256:24d574cb4072e1640b00864e94c4c89858033936ece3fc0e1c6f7179f120d0a8 \ - --hash=sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06 \ - --hash=sha256:35a144fd681a0bea1013ccc4cd3fd4dc758ea17ee23dca019c02b82ec46fc0c4 \ - --hash=sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22 \ - --hash=sha256:4024e6d35a9fdc1b3fd6ac7a673bd16cb176c7e0b952af6428b7129a70f72cce \ - --hash=sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0 \ - --hash=sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58 \ - --hash=sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005 \ - --hash=sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7 \ - --hash=sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01 \ - --hash=sha256:5ba00ead8d9d82fd6123eb4891c566d30a293454e54e32ff7ead7644f5f7e575 \ - --hash=sha256:5e2dcc94dc9c9ab8704409d86fcb5236316e9dcef6feed8162287634e3568f4c \ - --hash=sha256:5e410383f5d6a449a529d074e88af8bc80020bb42b402265f9c02c8358c11da5 \ - --hash=sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4 \ - --hash=sha256:61c74710900602637d2d145dda9720c94e303380803bf68811b2a151deec75c2 \ - --hash=sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d \ - --hash=sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc \ - --hash=sha256:63d39709e17eb72685d052ac82acf0763e047f57c86af1b791505b1fec96915d \ - --hash=sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147 \ - --hash=sha256:7100adcda5e818b5a894ad0626e38118bb896a347f40ebed8981155675b9ba7b \ - --hash=sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4 \ - --hash=sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6 \ - --hash=sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4 \ - --hash=sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039 \ - --hash=sha256:9735695a66aad654500b0193525e590c693ab3368478ce07b34b443a1ea5e824 \ - --hash=sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35 \ - --hash=sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf \ - --hash=sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061 \ - --hash=sha256:b2d5117dce805d8a2f78baeddc8c6127281fa0a5e2c40c6dd992ba6b2b367876 \ - --hash=sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d \ - --hash=sha256:b750ecba3a8815ad8bc48311552f3f8ab99dd2326d29df7ff670d9c49321f48f \ - --hash=sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c \ - --hash=sha256:d0a7c5f875068efe106f62233bc712d50db4d07c13c7db570175c7857a7b5dbd \ - --hash=sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d \ - --hash=sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75 \ - --hash=sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443 \ - --hash=sha256:e81b26497a17c29595bc7df20bc6a872ceea5f8c9d6537283945d4b6396aec10 \ - --hash=sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1 \ - --hash=sha256:ece59af54ebf67ecbfbbf3ca9066f5687879e36527ad69d8b6e3ac565d565a62 \ - --hash=sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b \ - --hash=sha256:f2797593760da6bcc32c4a13fa825af183cd4bfd333c60b3dbf84711afca26ef \ - --hash=sha256:fa2de871801d824c255fac7e5e7e50f2be6c9c376fd9268b40c54b5e9da91f42 \ - --hash=sha256:fb35f607bd1cbe37b896379d5f5ed4dc298b536f4b959cb63180e05cacc0539d \ - --hash=sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32 - # via marshmallow -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -boto3==1.38.27 \ - --hash=sha256:94bd7fdd92d5701b362d4df100d21e28f8307a67ff56b6a8b0398119cf22f859 \ - --hash=sha256:95f5fe688795303a8a15e8b7e7f255cadab35eae459d00cc281a4fd77252ea80 +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +boto3==1.43.46 \ + --hash=sha256:66c0d943b049a46a492ec4ec2ebe73c930b1842c7137bee83aad6d93e95d4d96 \ + --hash=sha256:69453e2c1bcb9fd9806527ab99950cacfc2826cb0dce9a3a0414d19270c06c3c # via - # feast (setup.py) + # feast (pyproject.toml) # snowflake-connector-python -botocore==1.38.46 \ - --hash=sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e \ - --hash=sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b +botocore==1.43.46 \ + --hash=sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533 \ + --hash=sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 # via # aiobotocore # boto3 # s3transfer # snowflake-connector-python -cachetools==6.2.1 \ - --hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \ - --hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201 - # via google-auth -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +cachetools==7.1.6 \ + --hash=sha256:2c12e255780330af28b91bb7fb96cce4c766f04e38396b9a24510190a5827096 \ + --hash=sha256:c7a79e7f30ba9943c1cefd08cc36f006aaae086e017af9166f1d59d6170c47e1 + # via pymilvus +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # httpcore # httpx # kubernetes # requests # snowflake-connector-python -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f # via - # feast (setup.py) + # feast (pyproject.toml) # cryptography - # snowflake-connector-python -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via # requests # snowflake-connector-python -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask - # typer # uvicorn -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a # via dask colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via feast (setup.py) -cryptography==46.0.0 \ - --hash=sha256:07a1be54f995ce14740bf8bbe1cc35f7a37760f992f73cf9f98a2a60b9b97419 \ - --hash=sha256:0f58183453032727a65e6605240e7a3824fd1d6a7e75d2b537e280286ab79a52 \ - --hash=sha256:16b5ac72a965ec9d1e34d9417dbce235d45fa04dac28634384e3ce40dfc66495 \ - --hash=sha256:1b4fba84166d906a22027f0d958e42f3a4dbbb19c28ea71f0fb7812380b04e3c \ - --hash=sha256:1d2073313324226fd846e6b5fc340ed02d43fd7478f584741bd6b791c33c9fee \ - --hash=sha256:249c41f2bbfa026615e7bdca47e4a66135baa81b08509ab240a2e666f6af5966 \ - --hash=sha256:274f8b2eb3616709f437326185eb563eb4e5813d01ebe2029b61bfe7d9995fbb \ - --hash=sha256:2fc30be952dd4334801d345d134c9ef0e9ccbaa8c3e1bc18925cbc4247b3e29c \ - --hash=sha256:32670ca085150ff36b438c17f2dfc54146fe4a074ebf0a76d72fb1b419a974bc \ - --hash=sha256:35aa1a44bd3e0efc3ef09cf924b3a0e2a57eda84074556f4506af2d294076685 \ - --hash=sha256:3738f50215211cee1974193a1809348d33893696ce119968932ea117bcbc9b1d \ - --hash=sha256:378eff89b040cbce6169528f130ee75dceeb97eef396a801daec03b696434f06 \ - --hash=sha256:399ef4c9be67f3902e5ca1d80e64b04498f8b56c19e1bc8d0825050ea5290410 \ - --hash=sha256:40ee4ce3c34acaa5bc347615ec452c74ae8ff7db973a98c97c62293120f668c6 \ - --hash=sha256:4bc257c2d5d865ed37d0bd7c500baa71f939a7952c424f28632298d80ccd5ec1 \ - --hash=sha256:4f70cbade61a16f5e238c4b0eb4e258d177a2fcb59aa0aae1236594f7b0ae338 \ - --hash=sha256:523153480d7575a169933f083eb47b1edd5fef45d87b026737de74ffeb300f69 \ - --hash=sha256:6460866a92143a24e3ed68eaeb6e98d0cedd85d7d9a8ab1fc293ec91850b1b38 \ - --hash=sha256:65e9117ebed5b16b28154ed36b164c20021f3a480e9cbb4b4a2a59b95e74c25d \ - --hash=sha256:6c39fd5cd9b7526afa69d64b5e5645a06e1b904f342584b3885254400b63f1b3 \ - --hash=sha256:6d8945bc120dcd90ae39aa841afddaeafc5f2e832809dc54fb906e3db829dfdc \ - --hash=sha256:75d2ddde8f1766ab2db48ed7f2aa3797aeb491ea8dfe9b4c074201aec00f5c16 \ - --hash=sha256:77e3bd53c9c189cea361bc18ceb173959f8b2dd8f8d984ae118e9ac641410252 \ - --hash=sha256:7f3f88df0c9b248dcc2e76124f9140621aca187ccc396b87bc363f890acf3a30 \ - --hash=sha256:80a548a5862d6912a45557a101092cd6c64ae1475b82cef50ee305d14a75f598 \ - --hash=sha256:834af45296083d892e23430e3b11df77e2ac5c042caede1da29c9bf59016f4d2 \ - --hash=sha256:83af84ebe7b6e9b6de05050c79f8cc0173c864ce747b53abce6a11e940efdc0d \ - --hash=sha256:88c09da8a94ac27798f6b62de6968ac78bb94805b5d272dbcfd5fdc8c566999f \ - --hash=sha256:8e8b222eb54e3e7d3743a7c2b1f7fa7df7a9add790307bb34327c88ec85fe087 \ - --hash=sha256:91585fc9e696abd7b3e48a463a20dda1a5c0eeeca4ba60fa4205a79527694390 \ - --hash=sha256:99f64a6d15f19f3afd78720ad2978f6d8d4c68cd4eb600fab82ab1a7c2071dca \ - --hash=sha256:9aa85222f03fdb30defabc7a9e1e3d4ec76eb74ea9fe1504b2800844f9c98440 \ - --hash=sha256:ab3a14cecc741c8c03ad0ad46dfbf18de25218551931a23bca2731d46c706d83 \ - --hash=sha256:b8e7db4ce0b7297e88f3d02e6ee9a39382e0efaf1e8974ad353120a2b5a57ef7 \ - --hash=sha256:bbaa5eef3c19c66613317dc61e211b48d5f550db009c45e1c28b59d5a9b7812a \ - --hash=sha256:be7479f9504bfb46628544ec7cb4637fe6af8b70445d4455fbb9c395ad9b7290 \ - --hash=sha256:bf1961037309ee0bdf874ccba9820b1c2f720c2016895c44d8eb2316226c1ad5 \ - --hash=sha256:c1f6ccd6f2eef3b2eb52837f0463e853501e45a916b3fc42e5d93cf244a4b97b \ - --hash=sha256:c3648d6a5878fd1c9a22b1d43fa75efc069d5f54de12df95c638ae7ba88701d0 \ - --hash=sha256:c39f0947d50f74b1b3523cec3931315072646286fb462995eb998f8136779319 \ - --hash=sha256:c3cd09b1490c1509bf3892bde9cef729795fae4a2fee0621f19be3321beca7e4 \ - --hash=sha256:c457ad3f151d5fb380be99425b286167b358f76d97ad18b188b68097193ed95a \ - --hash=sha256:c9c4121f9a41cc3d02164541d986f59be31548ad355a5c96ac50703003c50fb7 \ - --hash=sha256:d14eaf1569d6252280516bedaffdd65267428cdbc3a8c2d6de63753cf0863d5e \ - --hash=sha256:d1eccae15d5c28c74b2bea228775c63ac5b6c36eedb574e002440c0bc28750d3 \ - --hash=sha256:d349af4d76a93562f1dce4d983a4a34d01cb22b48635b0d2a0b8372cdb4a8136 \ - --hash=sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0 \ - --hash=sha256:da7f93551d39d462263b6b5c9056c49f780b9200bf9fc2656d7c88c7bdb9b363 \ - --hash=sha256:df932ac70388be034b2e046e34d636245d5eeb8140db24a6b4c2268cd2073270 \ - --hash=sha256:f09a3a108223e319168b7557810596631a8cb864657b0c16ed7a6017f0be9433 \ - --hash=sha256:f85e6a7d42ad60024fa1347b1d4ef82c4df517a4deb7f829d301f1a92ded038c \ - --hash=sha256:f9aaf2a91302e1490c068d2f3af7df4137ac2b36600f5bd26e53d9ec320412d3 \ - --hash=sha256:f9f85d9cf88e3ba2b2b6da3c2310d1cf75bdf04a5bc1a2e972603054f82c4dd5 \ - --hash=sha256:fe9ff1139b2b1f59a5a0b538bbd950f8660a39624bbe10cf3640d17574f973bb + # via feast (pyproject.toml) +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b # via + # google-auth + # pyjwt # pyopenssl # snowflake-connector-python -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) -db-dtypes==1.4.3 \ - --hash=sha256:a1c92b819af947fae1701d80a71f2a0eac08f825ca52cf0c68aeba80577ae966 \ - --hash=sha256:d3cb08c32939d24e05501f17694be8c1c54cfb4620d61fb56fb311e8c3d8885e +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) +db-dtypes==1.7.1 \ + --hash=sha256:450b94cd010cfa6d4d13cca2593009eba4fbe15516084c332cde1a47bbce0625 \ + --hash=sha256:75adf3bdf3174a6474ea4162d9a345ea0b3102ec4fd603e69ec26b6e8a99145a # via # google-cloud-bigquery # pandas-gbq dill==0.3.9 \ --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c - # via feast (setup.py) -duckdb==1.1.3 \ - --hash=sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586 \ - --hash=sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd \ - --hash=sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e \ - --hash=sha256:09c68522c30fc38fc972b8a75e9201616b96ae6da3444585f14cf0d116008c95 \ - --hash=sha256:0a55169d2d2e2e88077d91d4875104b58de45eff6a17a59c7dc41562c73df4be \ - --hash=sha256:0ba6baa0af33ded836b388b09433a69b8bec00263247f6bf0a05c65c897108d3 \ - --hash=sha256:183ac743f21c6a4d6adfd02b69013d5fd78e5e2cd2b4db023bc8a95457d4bc5d \ - --hash=sha256:1aa3abec8e8995a03ff1a904b0e66282d19919f562dd0a1de02f23169eeec461 \ - --hash=sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce \ - --hash=sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24 \ - --hash=sha256:2141c6b28162199999075d6031b5d63efeb97c1e68fb3d797279d31c65676269 \ - --hash=sha256:252d9b17d354beb9057098d4e5d5698e091a4f4a0d38157daeea5fc0ec161670 \ - --hash=sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9 \ - --hash=sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3 \ - --hash=sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289 \ - --hash=sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1 \ - --hash=sha256:4f0e2e5a6f5a53b79aee20856c027046fba1d73ada6178ed8467f53c3877d5e0 \ - --hash=sha256:51c6d79e05b4a0933672b1cacd6338f882158f45ef9903aef350c4427d9fc898 \ - --hash=sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8 \ - --hash=sha256:5ace6e4b1873afdd38bd6cc8fcf90310fb2d454f29c39a61d0c0cf1a24ad6c8d \ - --hash=sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff \ - --hash=sha256:6411e21a2128d478efbd023f2bdff12464d146f92bc3e9c49247240448ace5a6 \ - --hash=sha256:647f17bd126170d96a38a9a6f25fca47ebb0261e5e44881e3782989033c94686 \ - --hash=sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c \ - --hash=sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31 \ - --hash=sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7 \ - --hash=sha256:80158f4c7c7ada46245837d5b6869a336bbaa28436fbb0537663fa324a2750cd \ - --hash=sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898 \ - --hash=sha256:8ee97ec337794c162c0638dda3b4a30a483d0587deda22d45e1909036ff0b739 \ - --hash=sha256:911d58c22645bfca4a5a049ff53a0afd1537bc18fedb13bc440b2e5af3c46148 \ - --hash=sha256:9c619e4849837c8c83666f2cd5c6c031300cd2601e9564b47aa5de458ff6e69d \ - --hash=sha256:9d0767ada9f06faa5afcf63eb7ba1befaccfbcfdac5ff86f0168c673dd1f47aa \ - --hash=sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4 \ - --hash=sha256:a1f83c7217c188b7ab42e6a0963f42070d9aed114f6200e3c923c8899c090f16 \ - --hash=sha256:a1fa0c502f257fa9caca60b8b1478ec0f3295f34bb2efdc10776fc731b8a6c5f \ - --hash=sha256:a30dd599b8090ea6eafdfb5a9f1b872d78bac318b6914ada2d35c7974d643640 \ - --hash=sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38 \ - --hash=sha256:a4748635875fc3c19a7320a6ae7410f9295557450c0ebab6d6712de12640929a \ - --hash=sha256:b74e121ab65dbec5290f33ca92301e3a4e81797966c8d9feef6efdf05fc6dafd \ - --hash=sha256:c443d3d502335e69fc1e35295fcfd1108f72cb984af54c536adfd7875e79cee5 \ - --hash=sha256:c5336939d83837af52731e02b6a78a446794078590aa71fd400eb17f083dda3e \ - --hash=sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378 \ - --hash=sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9 \ - --hash=sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee \ - --hash=sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32 \ - --hash=sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537 \ - --hash=sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989 \ - --hash=sha256:ecb1dc9062c1cc4d2d88a5e5cd8cc72af7818ab5a3c0f796ef0ffd60cfd3efb4 \ - --hash=sha256:eeacb598120040e9591f5a4edecad7080853aa8ac27e62d280f151f8c862afa3 \ - --hash=sha256:f549af9f7416573ee48db1cf8c9d27aeed245cb015f4b4f975289418c6cf7320 \ - --hash=sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3 \ - --hash=sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b + # via feast (pyproject.toml) +duckdb==1.5.5 \ + --hash=sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053 \ + --hash=sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4 \ + --hash=sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c \ + --hash=sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a \ + --hash=sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528 \ + --hash=sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c \ + --hash=sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282 \ + --hash=sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f \ + --hash=sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7 \ + --hash=sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f \ + --hash=sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c \ + --hash=sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb \ + --hash=sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433 \ + --hash=sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8 \ + --hash=sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28 \ + --hash=sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238 \ + --hash=sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece \ + --hash=sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1 \ + --hash=sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151 \ + --hash=sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2 \ + --hash=sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353 \ + --hash=sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc \ + --hash=sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079 \ + --hash=sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a \ + --hash=sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6 \ + --hash=sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe \ + --hash=sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de \ + --hash=sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0 \ + --hash=sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e \ + --hash=sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f \ + --hash=sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf \ + --hash=sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae \ + --hash=sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6 \ + --hash=sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe \ + --hash=sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366 # via ibis-framework durationpy==0.10 \ --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 # via kubernetes -environs==9.5.0 \ - --hash=sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124 \ - --hash=sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9 - # via pymilvus -exceptiongroup==1.3.0 \ - --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ - --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 +exceptiongroup==1.3.1 \ + --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ + --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via anyio -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 +faiss-cpu==1.14.3 \ + --hash=sha256:0a5eb27184123c7ac1060c6b862978eabf0e30c1369ccf8bdb1497d35c06ad3d \ + --hash=sha256:1d734cfa9ac90b6a5dfed3a27cb706d05f22824703dafc3969b4e2071877a31c \ + --hash=sha256:20414d5c98fb6ab9eedfb3df79841865911c2d67b6efe829dc46adfeab6f51b1 \ + --hash=sha256:54b0ea832be5bce75ef9abf1297fe4613d760bf7cfe7a963da040cb0314e8f58 \ + --hash=sha256:8780b526c06e57aad90a8c4655dfba2ff1b3195bd600ff4499752fc45159c9fc \ + --hash=sha256:a9369863290a3f0e033757e4c10577b6ef7431f1cede394dabd0a137e4e2ed45 \ + --hash=sha256:b28ba083e8c02f2c9be03783402537fd3f00d27e68799c44bf88931500ee12ec \ + --hash=sha256:cdcb90850cb4b7c27d270839b37bcc0dc8d1fb6a62d1e13053e51ac95061ca25 \ + --hash=sha256:ea2340f675db59af8db6da4535b541ce6948d68a008989c656c292c7b0c77127 \ + --hash=sha256:f278003d00c30c90cf118b98428bb96f73eba4b9dadac6993c788966d2e983bd \ + --hash=sha256:f9d0e84d909194f63f027bbd3c1e35e905e48c9345c2db6e6f24da09a6bc5906 + # via milvus-lite +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp fastapi-mcp==0.4.0 \ --hash=sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec \ --hash=sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d - # via feast (setup.py) -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 + # via feast (pyproject.toml) +filelock==3.32.0 \ + --hash=sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402 \ + --hash=sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3 # via snowflake-connector-python frozenlist==1.8.0 \ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ @@ -743,17 +736,17 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -fsspec==2024.9.0 \ - --hash=sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8 \ - --hash=sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b +fsspec==2026.7.0 \ + --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \ + --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88 # via - # feast (setup.py) + # feast (pyproject.toml) # dask -google-api-core[grpc]==2.26.0 \ - --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ - --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 +google-api-core[grpc]==2.33.0 \ + --hash=sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb \ + --hash=sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc # via - # feast (setup.py) + # feast (pyproject.toml) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -761,9 +754,9 @@ google-api-core[grpc]==2.26.0 \ # google-cloud-datastore # google-cloud-storage # pandas-gbq -google-auth==2.41.1 \ - --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ - --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 +google-auth[pyopenssl]==2.56.2 \ + --hash=sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6 \ + --hash=sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051 # via # google-api-core # google-auth-oauthlib @@ -773,395 +766,515 @@ google-auth==2.41.1 \ # google-cloud-core # google-cloud-datastore # google-cloud-storage - # kubernetes # pandas-gbq # pydata-google-auth -google-auth-oauthlib==1.2.2 \ - --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ - --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-auth-oauthlib==1.4.0 \ + --hash=sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2 \ + --hash=sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 # via # pandas-gbq # pydata-google-auth -google-cloud-bigquery[pandas]==3.38.0 \ - --hash=sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520 \ - --hash=sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6 +google-cloud-bigquery[pandas]==3.42.2 \ + --hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \ + --hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq -google-cloud-bigquery-storage==2.33.1 \ - --hash=sha256:24952aba0d69acc4d6bfbdc7a09dddbb728496b1780bd224f1056361a1b51044 \ - --hash=sha256:3fd25bef364ac5fb9bbd6560f0dd11b90b1845883df8e0a8c706ad53d00fc23b - # via feast (setup.py) -google-cloud-bigtable==2.33.0 \ - --hash=sha256:8d25c73dd41cea9436d14c9f03b91f94b05ff4afd47ce9a575bf18f8e6985747 \ - --hash=sha256:cd4f7178cde30113900318ec7e2fd793116752c97e0922c185c69b645a52656d - # via feast (setup.py) -google-cloud-core==2.4.3 \ - --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ - --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e +google-cloud-bigquery-storage==2.39.0 \ + --hash=sha256:8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 \ + --hash=sha256:d5afd90ad06cf24d9167316cca70ab5b344e880fc13031d7392aa78ee76b8bb6 + # via feast (pyproject.toml) +google-cloud-bigtable==2.41.0 \ + --hash=sha256:36da0fc730952bfe48ae91dee11015459c9046fa1499d0efb73a61bbb6edbe9b \ + --hash=sha256:d0c424bae6238eb16ffc0922be79da00d20d23e0750ea46742ffd4542c50fd26 + # via feast (pyproject.toml) +google-cloud-core==2.6.0 \ + --hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \ + --hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.21.0 \ - --hash=sha256:eee454dd4a55f5b327f9f344928ff1a09a6f77c23d5e3d908ad31a13cc2f4073 \ - --hash=sha256:f303f27cd1983383f20bd227019cd8a7897419e0ec6b878367c58c66245f9d9b - # via feast (setup.py) +google-cloud-datastore==2.26.0 \ + --hash=sha256:1ac4882f033fcb90bd75d1171b46400ecd5def23768598843d9c041e0c0b4a6a \ + --hash=sha256:1bde5694308468da5c932153a6b0331d5b54e1c21de366b6a75ddb0000fd5f4b + # via feast (pyproject.toml) google-cloud-storage==2.19.0 \ --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via feast (setup.py) -google-crc32c==1.7.1 \ - --hash=sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db \ - --hash=sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337 \ - --hash=sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c \ - --hash=sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242 \ - --hash=sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e \ - --hash=sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472 \ - --hash=sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194 \ - --hash=sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3 \ - --hash=sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582 \ - --hash=sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d \ - --hash=sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6 \ - --hash=sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82 \ - --hash=sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06 \ - --hash=sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349 \ - --hash=sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a \ - --hash=sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d \ - --hash=sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48 \ - --hash=sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb \ - --hash=sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315 \ - --hash=sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589 \ - --hash=sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76 \ - --hash=sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65 \ - --hash=sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6 \ - --hash=sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127 \ - --hash=sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53 \ - --hash=sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603 \ - --hash=sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35 \ - --hash=sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9 \ - --hash=sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638 \ - --hash=sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9 \ - --hash=sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77 \ - --hash=sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14 \ - --hash=sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b \ - --hash=sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb + # via feast (pyproject.toml) +google-crc32c==1.8.0 \ + --hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \ + --hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \ + --hash=sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff \ + --hash=sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 \ + --hash=sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411 \ + --hash=sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a \ + --hash=sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 \ + --hash=sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb \ + --hash=sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa \ + --hash=sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962 \ + --hash=sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215 \ + --hash=sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b \ + --hash=sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27 \ + --hash=sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113 \ + --hash=sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f \ + --hash=sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f \ + --hash=sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d \ + --hash=sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 \ + --hash=sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092 \ + --hash=sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7 \ + --hash=sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2 \ + --hash=sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93 \ + --hash=sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8 \ + --hash=sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21 \ + --hash=sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 \ + --hash=sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2 \ + --hash=sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc \ + --hash=sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454 \ + --hash=sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2 \ + --hash=sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733 \ + --hash=sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697 \ + --hash=sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651 \ + --hash=sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c # via # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 +google-resumable-media==2.10.0 \ + --hash=sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c \ + --hash=sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.70.0 \ - --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ - --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +googleapis-common-protos[grpc]==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # grpc-google-iam-v1 # grpcio-status -grpc-google-iam-v1==0.14.2 \ - --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ - --hash=sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20 +grpc-google-iam-v1==0.14.4 \ + --hash=sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038 \ + --hash=sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 # via google-cloud-bigtable -grpcio==1.62.3 \ - --hash=sha256:059444f0ed5dba73ab7dd0ee7e8e6b606df4130d2b0a9f010f84da4ab9f6c2d8 \ - --hash=sha256:114f2a865886ff33f85d70670e971fe0e3d252a1209656fefa5470286e3fcc76 \ - --hash=sha256:13571a5b868dcc308a55d36669a2d17d9dcd6ec8335213f6c49cc68da7305abe \ - --hash=sha256:1ac0944e9e3ee3e20825226d1e17985e9f88487055c475986cf0922a7d806d8a \ - --hash=sha256:1de3d04d9a4ec31ebe848ae1fe61e4cbc367fb9495cbf6c54368e60609a998d9 \ - --hash=sha256:216740723fc5971429550c374a0c039723b9d4dcaf7ba05227b7e0a500b06417 \ - --hash=sha256:25cd75dc73c5269932413e517be778640402f18cf9a81147e68645bd8af18ab0 \ - --hash=sha256:325c56ce94d738c31059cf91376f625d3effdff8f85c96660a5fd6395d5a707f \ - --hash=sha256:3737e5ef0aa0fcdfeaf3b4ecc1a6be78b494549b28aec4b7f61b5dc357f7d8be \ - --hash=sha256:377babc817e8b4186aed7ed56e832867c513e4e9b6c3503565c344ffdef440d4 \ - --hash=sha256:3fb7d966a976d762a31346353a19fce4afcffbeda3027dd563bc8cb521fcf799 \ - --hash=sha256:43670a25b752b7ed960fcec3db50ae5886dc0df897269b3f5119cde9b731745f \ - --hash=sha256:4439bbd759636e37b66841117a66444b454937e27f0125205d2d117d7827c643 \ - --hash=sha256:454a6aed4ebd56198d37e1f3be6f1c70838e33dd62d1e2cea12f2bcb08efecc5 \ - --hash=sha256:4c9c1502c76cadbf2e145061b63af077b08d5677afcef91970d6db87b30e2f8b \ - --hash=sha256:4dab8b64c438e19c763a6332b55e5efdbecfb7c55ae59a42c38c81ed27955fa5 \ - --hash=sha256:56757d3e4cf5d4b98a30f2c5456151607261c891fa2298a4554848dcbf83083d \ - --hash=sha256:57823dc7299c4f258ae9c32fd327d29f729d359c34d7612b36e48ed45b3ab8d0 \ - --hash=sha256:582bd03e9c3d1bd1162eb51fa0f1a35633d66e73f4f36702d3b8484a8b45eda7 \ - --hash=sha256:620165df24aae3d5b3e84cb8dd6b98f6ed49aed04126186bbf43061e301d6a21 \ - --hash=sha256:646c14e9f3356d3f34a65b58b0f8d08daa741ba1d4fcd4966b79407543332154 \ - --hash=sha256:668211f3699bbee4deaf1d6e6b8df59328bf63f077bf2dc9b8bfa4a17df4a279 \ - --hash=sha256:6be243f3954b0ca709f56f9cae926c84ac96e1cce19844711e647a1f1db88b99 \ - --hash=sha256:6da20a1ae010a988bc4ed47850f1122de0a88e18cd2f901fcf56007be1fc6c30 \ - --hash=sha256:7349cd7445ac65fbe1b744dcab9cc1ec02dae2256941a2e67895926cbf7422b4 \ - --hash=sha256:74f3fc9b93290e58264844f5bc46df4c58a94c4287a277dbcf75344fc6c37ca4 \ - --hash=sha256:75a4e9ac7ff185cad529f35934c5d711b88aca48b90c70e195f5657da50ce321 \ - --hash=sha256:7b33c1807d4ac564a3027d06f21a2220c116ceacaaef614deb96b3341ee58896 \ - --hash=sha256:807176971c504c598976f5a9ea62363cffbbbb6c7509d9808c2342b020880fa2 \ - --hash=sha256:80a82fdee14dc27e9299248b7aabd5a8739a1cf6b76c78aa2b848158b44a99d5 \ - --hash=sha256:81b7c121c4e52a0749bf0759185b8d5cfa48a786cd7d411cdab08269813e0aab \ - --hash=sha256:8257cc9e55fb0e2149a652d9dc14c023720f9e73c9145776e07c97e0a553922e \ - --hash=sha256:8a5f00b2508937952d23a1767739e95bbbe1120f8a66d10187d5e971d56bb55c \ - --hash=sha256:8ae2e7a390b2cdd2a95d3bf3b3385245eeb48a5e853943cb46139666462c2d1a \ - --hash=sha256:940459d81685549afdfe13a6de102c52ea4cdda093477baa53056884aadf7c48 \ - --hash=sha256:9c4aae4e683776c319169d87e7891b67b75e3f1c0beeb877902ea148b0585164 \ - --hash=sha256:9d5f8e0050a179b3bce9189b522dc91008d44f08c757a7c310e0fd06b4d3d147 \ - --hash=sha256:a1b85d35a7d9638c03321dfe466645b87e23c30df1266f9e04bbb5f44e7579a9 \ - --hash=sha256:a82410d7620c07cb32624e38f2a106980564dfef9dbe78f5b295cda9ef217c03 \ - --hash=sha256:abfe64811177e681edc81d9d9d1bd23edc5f599bd9846650864769264ace30cd \ - --hash=sha256:ac9783d5679c8da612465168c820fd0b916e70ec5496c840bddba0be7f2d124c \ - --hash=sha256:b033d50bd41e506e3b579775f54a30c16c222e0d88847ac8098d2eca2a7454cc \ - --hash=sha256:b097347441b86a8c3ad9579abaf5e5f7f82b1d74a898f47360433b2bca0e4536 \ - --hash=sha256:b708401ede2c4cb8943e8a713988fcfe6cbea105b07cd7fa7c8a9f137c22bddb \ - --hash=sha256:bd900e666bb68fff49703084be14407cd73b8a5752a7590cea98ec22de24fb5d \ - --hash=sha256:c118cfc80e2402a5595be36e9245ffd9b0e146f426cc40bdf60015bf183f8373 \ - --hash=sha256:c175b252d063af388523a397dbe8edbc4319761f5ee892a8a0f5890acc067362 \ - --hash=sha256:c8bb1a7aa82af6c7713cdf9dcb8f4ea1024ac7ce82bb0a0a82a49aea5237da34 \ - --hash=sha256:c95a0b76a44c548e6bd8c5f7dbecf89c77e2e16d3965be817b57769c4a30bea2 \ - --hash=sha256:e202e3f963480ca067a261179b1ac610c0f0272cb4a7942d11b7e2b3fc99c3aa \ - --hash=sha256:e9ffdb7bc9ccd56ec201aec3eab3432e1e820335b5a16ad2b37e094218dcd7a6 \ - --hash=sha256:ea7ca66a58421411c6486fa5015fe7704e2816ff0b4ec4fb779ad5e1cbbdabf3 \ - --hash=sha256:f2ff8ac447765e173842b554b31307b98b3bb1852710903ebb936e7efb7df6e5 \ - --hash=sha256:f5def814c5a4c90c8fe389c526ab881f4a28b7e239b23ed8e02dd02934dfaa1a +grpcio==1.83.0 \ + --hash=sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df \ + --hash=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 \ + --hash=sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5 \ + --hash=sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df \ + --hash=sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930 \ + --hash=sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf \ + --hash=sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9 \ + --hash=sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33 \ + --hash=sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da \ + --hash=sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03 \ + --hash=sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa \ + --hash=sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223 \ + --hash=sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9 \ + --hash=sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4 \ + --hash=sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf \ + --hash=sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881 \ + --hash=sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af \ + --hash=sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5 \ + --hash=sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0 \ + --hash=sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727 \ + --hash=sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb \ + --hash=sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a \ + --hash=sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735 \ + --hash=sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16 \ + --hash=sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49 \ + --hash=sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1 \ + --hash=sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24 \ + --hash=sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b \ + --hash=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f \ + --hash=sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57 \ + --hash=sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf \ + --hash=sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f \ + --hash=sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c \ + --hash=sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969 \ + --hash=sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd \ + --hash=sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c \ + --hash=sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b \ + --hash=sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61 \ + --hash=sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404 \ + --hash=sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617 \ + --hash=sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40 \ + --hash=sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45 \ + --hash=sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc \ + --hash=sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c \ + --hash=sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745 \ + --hash=sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45 \ + --hash=sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c \ + --hash=sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0 \ + --hash=sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d \ + --hash=sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9 \ + --hash=sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8 # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-datastore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking # grpcio-reflection # grpcio-status + # milvus-lite # pymilvus -grpcio-health-checking==1.62.3 \ - --hash=sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93 \ - --hash=sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d - # via feast (setup.py) -grpcio-reflection==1.62.3 \ - --hash=sha256:a48ef37df81a3bada78261fc92ef382f061112f989d1312398b945cc69838b9c \ - --hash=sha256:cb84682933c400bddf94dd94f928d1c6570f500b6dd255973d4bfb495b82585f - # via feast (setup.py) -grpcio-status==1.62.3 \ - --hash=sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485 \ - --hash=sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8 +grpcio-health-checking==1.83.0 \ + --hash=sha256:7d8b47a5bfbc699d4aee0fc7a27f5d0265eb23a6a64db5ac2d8b749a0f8a9911 \ + --hash=sha256:ad6bc4d5a1103ad704d25ccd82def300dfa27996b185cab3e4cceeef2c6867d4 + # via feast (pyproject.toml) +grpcio-reflection==1.83.0 \ + --hash=sha256:6a2a30a462ac2c3c6d149734a8fe655fa7617c17fa2cda9949006f3bf07f5b3e \ + --hash=sha256:9d4151f68f18a85aa5b0eff5d493ad13f9421f96c8d9c01c840b353dc979be93 + # via feast (pyproject.toml) +grpcio-status==1.83.0 \ + --hash=sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae \ + --hash=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 # via google-api-core -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore + # httpcore2 # uvicorn -hiredis==2.4.0 \ - --hash=sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027 \ - --hash=sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577 \ - --hash=sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603 \ - --hash=sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869 \ - --hash=sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228 \ - --hash=sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79 \ - --hash=sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb \ - --hash=sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33 \ - --hash=sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77 \ - --hash=sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9 \ - --hash=sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd \ - --hash=sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a \ - --hash=sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8 \ - --hash=sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9 \ - --hash=sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901 \ - --hash=sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7 \ - --hash=sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0 \ - --hash=sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d \ - --hash=sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213 \ - --hash=sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8 \ - --hash=sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389 \ - --hash=sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330 \ - --hash=sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1 \ - --hash=sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2 \ - --hash=sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288 \ - --hash=sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba \ - --hash=sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded \ - --hash=sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca \ - --hash=sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109 \ - --hash=sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8 \ - --hash=sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea \ - --hash=sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6 \ - --hash=sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8 \ - --hash=sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0 \ - --hash=sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5 \ - --hash=sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5 \ - --hash=sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182 \ - --hash=sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73 \ - --hash=sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af \ - --hash=sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac \ - --hash=sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204 \ - --hash=sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277 \ - --hash=sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212 \ - --hash=sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41 \ - --hash=sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e \ - --hash=sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c \ - --hash=sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2 \ - --hash=sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4 \ - --hash=sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348 \ - --hash=sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b \ - --hash=sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e \ - --hash=sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995 \ - --hash=sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371 \ - --hash=sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613 \ - --hash=sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b \ - --hash=sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d \ - --hash=sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708 \ - --hash=sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08 \ - --hash=sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca \ - --hash=sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12 \ - --hash=sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8 \ - --hash=sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829 \ - --hash=sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38 \ - --hash=sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825 \ - --hash=sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67 \ - --hash=sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056 \ - --hash=sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376 \ - --hash=sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214 \ - --hash=sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982 \ - --hash=sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034 \ - --hash=sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1 \ - --hash=sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f \ - --hash=sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9 \ - --hash=sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5 \ - --hash=sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390 \ - --hash=sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0 \ - --hash=sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d \ - --hash=sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57 \ - --hash=sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe \ - --hash=sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2 \ - --hash=sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415 \ - --hash=sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c \ - --hash=sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b \ - --hash=sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853 \ - --hash=sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36 \ - --hash=sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae \ - --hash=sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd \ - --hash=sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187 \ - --hash=sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c \ - --hash=sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542 \ - --hash=sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d \ - --hash=sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79 \ - --hash=sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7 \ - --hash=sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def - # via feast (setup.py) +hiredis==3.4.0 \ + --hash=sha256:02298551060cbfe17b1acb02e9d066eadd51e37357369736f8fc1f5533875255 \ + --hash=sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc \ + --hash=sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865 \ + --hash=sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c \ + --hash=sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec \ + --hash=sha256:06c163607f163a300cba9ef02fd8a234dd644a208bfafe972614f72e5287b9ba \ + --hash=sha256:0731dd9f2bf4d3417947a3cbbca4dd4d9fa6ad7cecfd4e16439c17a4562d6304 \ + --hash=sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419 \ + --hash=sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320 \ + --hash=sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b \ + --hash=sha256:0bcb630add6bc9ea136fce691ddff0c46aa91cb860df4ca789fe44127eb7e90d \ + --hash=sha256:12ea5facb5b08fa23e4c101ec2151f3a3de8ecec412fec58dbde0a6eebca02c7 \ + --hash=sha256:12eca9aea1450d1a85dc15574a985c227e52abbc2b6466f48ad2aa3b82124701 \ + --hash=sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f \ + --hash=sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017 \ + --hash=sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc \ + --hash=sha256:16a3ea8193c5f4ee6e8daafe0126207cef38eec5a923c97047e6985d9bb1b61f \ + --hash=sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4 \ + --hash=sha256:1bfb9ccfb13be63883e5f2e5ff7f6fc87bf256f8243af594257dfbed9dbc3cf0 \ + --hash=sha256:1c9e141b420dae63ea21303a7e8672282326cae67472768e9a3f6e7f0990bd28 \ + --hash=sha256:1d8c21fb85f2a3de9328cf5bace84b63da00028e771b92335ff4fdc00121dd5a \ + --hash=sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5 \ + --hash=sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016 \ + --hash=sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240 \ + --hash=sha256:282c4310af72afbe18b07d416459f4febeaeb805a067a7df790136e0e550fcb2 \ + --hash=sha256:2c432cce38190c3c13b6c28d6840ee85db50765ad510134007f1cfb3844dce73 \ + --hash=sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591 \ + --hash=sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea \ + --hash=sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980 \ + --hash=sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b \ + --hash=sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb \ + --hash=sha256:386b556f48fb0f9f09696b9d37f1cae554123fbd9f091d0ca23087f2aca02887 \ + --hash=sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b \ + --hash=sha256:4190bd07dd7879a8a7ddbb2a4f74d402721f3898276e35beb98851b85b5f539c \ + --hash=sha256:423570ac4d2665c0d55d8707b7859a331e9001da5e437b7b7a23e0acbce770c9 \ + --hash=sha256:43004b0b48abc628dda1ac3ac4871e1326c126f8cd9f11164d61934d827d7a3b \ + --hash=sha256:4404c557fd49bcfe24dff41f1209e4221c76d1607df2fb2dfd39474b5b086dcb \ + --hash=sha256:44660a91e0fbc803c29b337c1a9194c8d7b4cd3a3868d28f747cbec2df165483 \ + --hash=sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582 \ + --hash=sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2 \ + --hash=sha256:45c6c296056641b5df37cedafe7d1553f33bc247e2f81603a4d038b39261879b \ + --hash=sha256:46e041fc7a83eaa989ef5fe09526bc2353a6ba39b4ce5191684eabefc02e17b0 \ + --hash=sha256:4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e \ + --hash=sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d \ + --hash=sha256:4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d \ + --hash=sha256:4e5530d7a6c71c1a9c3b850b08bbc8178ffc9556e3aef7f9c808fe9ff91a6f11 \ + --hash=sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763 \ + --hash=sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584 \ + --hash=sha256:5359caad5b57da0bce11d2880f22617ba3710f0866121a924745447848448034 \ + --hash=sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79 \ + --hash=sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9 \ + --hash=sha256:64cf54c1dad35830bf1d86b343ad3c55aed443af51c5ccead8afd1b547aca196 \ + --hash=sha256:6774f1fe2723001ca0cd42bf5d8b1235301226273915c581c5c1260d4d114c43 \ + --hash=sha256:68c81c04e0d8778c01163dd2bc6f0e0236fc7f650c291526f10d4faccd99e5a0 \ + --hash=sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb \ + --hash=sha256:69d0326f20354ce278cbb86f5ae47cb390e22bb94a66877031038af907c42fa5 \ + --hash=sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22 \ + --hash=sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b \ + --hash=sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9 \ + --hash=sha256:7b159315df71a009375227384aa4219f98c2342ccd8eb33e3f4b58654f426376 \ + --hash=sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203 \ + --hash=sha256:7f7fc1535f6e1a190089eae46dee25f0c6b72bb221d377be07092803b8208733 \ + --hash=sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f \ + --hash=sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13 \ + --hash=sha256:85a9abf2d16b4cb112f6cc6c32236d763b34d21c69b00c2f81a4ff3016fcc1d6 \ + --hash=sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c \ + --hash=sha256:899d5cea72c9a6c2d9b6e7b223de62c9e9f541f48267cc7135764070548735ce \ + --hash=sha256:89e95f50ec4d29645ba2da9010e8717861585dc9f3df354a367f43807a6db3c7 \ + --hash=sha256:8aaaab18314fd25453b5cf59c8cdca4110e419455bcb4c0737d19d4151513e75 \ + --hash=sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1 \ + --hash=sha256:8f2c8569460aa456a294d5e9a3579a382b825dc2e283e3cc398323dcfb6a3dee \ + --hash=sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151 \ + --hash=sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a \ + --hash=sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d \ + --hash=sha256:98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c \ + --hash=sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f \ + --hash=sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d \ + --hash=sha256:a315009b441a0105a373a9a780ebb1c6f7d9ead88ac6ea5f2a15791353c6f590 \ + --hash=sha256:a3796094f616f72976ff51e4dc1a016e753c0f9af5393b2df96920b6bae1e19b \ + --hash=sha256:a427976ba339da624367613d7eba52a1516e6d5c7f8988dc8fb888fbcf52d8b6 \ + --hash=sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb \ + --hash=sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530 \ + --hash=sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4 \ + --hash=sha256:b0c29ac4f50fc61a904acfe76e4ce6bc37e16ab5a98089cb411476d466e1bb36 \ + --hash=sha256:b32afb576705fc5a02440d416ef476ddbcc827a20ff6c4a9d557882bc6b75135 \ + --hash=sha256:b6228afacadb19a858289319d72797023e2cb048f2f930b68b7fe57eaed5fb6b \ + --hash=sha256:b98f44bf873731d9ce99e6b3eeb6196bf7b27693ad9f6de0fba986b2b0845127 \ + --hash=sha256:bb44efa4fa3e3ed7779ad0ade3c08ed5d75ca7a6336893e9a4f2722093b4168a \ + --hash=sha256:bb8b2dd9c00d80ad2870ffbb51ce9af99fb2194c082d7b09f4125d90e21426f6 \ + --hash=sha256:bca175f02a2b0150ffe7f5dc8bf49c798f34d2c7024d17ace0ec97a7583560e3 \ + --hash=sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59 \ + --hash=sha256:c2245c46b4ced5f689469e6dcdfc8a0895bf873840a6600f5ea759cdf1b26a8b \ + --hash=sha256:c8c6337e7ad187e8039d06e4b33ee5148dcd73928819d94087e649eb37316a09 \ + --hash=sha256:ccc5c660e31d788ca534a20f2ccb7a80b946b960e18ed4e1db950fcac122b405 \ + --hash=sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643 \ + --hash=sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c \ + --hash=sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4 \ + --hash=sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d \ + --hash=sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352 \ + --hash=sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81 \ + --hash=sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5 \ + --hash=sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092 \ + --hash=sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4 \ + --hash=sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad \ + --hash=sha256:e29267ecdd08758926f1a9221af2671d90f475480c40aff409921b1f362f1bd5 \ + --hash=sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d \ + --hash=sha256:ed1dba2695f6de009c67d63b39ff978cb43b8a79362f697acedffb7743e50d21 \ + --hash=sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1 \ + --hash=sha256:f3c67f39b112dc35f68d5b59ee111db6121f037d1a60cf3840ecffbb2ec5686b \ + --hash=sha256:f7c7596fbb2b5202e943180353958e89014e763c7f25877a92f70bbde6cd7f19 \ + --hash=sha256:ff28eca77a57751488df90b0f385b472fd78e140a4d45097224648a5737acdf5 \ + --hash=sha256:ff6217da86a63a6a5ea954d9b9ed67916d9d94a566b44aa98e68ae34b40ebbbf + # via feast (pyproject.toml) httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httpcore2==2.9.1 \ + --hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \ + --hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26 + # via httpx2 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - # via - # fastapi-mcp - # mcp -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d + # via fastapi-mcp +httpx2==2.9.1 \ + --hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \ + --hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a # via mcp -ibis-framework[duckdb]==9.5.0 \ - --hash=sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0 \ - --hash=sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84 - # via feast (setup.py) -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +ibis-framework[duckdb]==12.0.0 \ + --hash=sha256:0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 \ + --hash=sha256:238624f2c14fdab8382ca2f4f667c3cdb81e29844cd5f8db8a325d0743767c61 + # via feast (pyproject.toml) +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx + # httpx2 # requests # snowflake-connector-python # yarl -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc # via dask jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via feast (setup.py) -jmespath==1.0.1 \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe + # via feast (pyproject.toml) +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 # via # aiobotocore # boto3 # botocore -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 - # via - # feast (setup.py) - # mcp +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via mcp jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -kubernetes==33.1.0 \ - --hash=sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5 \ - --hash=sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993 - # via feast (setup.py) +kubernetes==36.0.3 \ + --hash=sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7 \ + --hash=sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f + # via feast (pyproject.toml) +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ --hash=sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 # via partd -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ @@ -1254,339 +1367,328 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -marshmallow==4.0.1 \ - --hash=sha256:72f14ef346f81269dbddee891bac547dda1501e9e08b6a809756ea3dbb7936a1 \ - --hash=sha256:e1d860bd262737cb2d34e1541b84cb52c32c72c9474e3fe6f30f137ef8b0d97f - # via environs -mcp==1.12.4 \ - --hash=sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5 \ - --hash=sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789 +mcp==2.0.0 \ + --hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \ + --hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6 # via fastapi-mcp +mcp-types==2.0.0 \ + --hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \ + --hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379 + # via mcp mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -milvus-lite==2.4.12 \ - --hash=sha256:20087663e7b4385050b7ad08f1f03404426d4c87b1ff91d5a8723eee7fd49e88 \ - --hash=sha256:334037ebbab60243b5d8b43d54ca2f835d81d48c3cda0c6a462605e588deb05d \ - --hash=sha256:a0f3a5ddbfd19f4a6b842b2fd3445693c796cde272b701a1646a94c1ac45d3d7 \ - --hash=sha256:e8d4f7cdd5f731efd6faeee3715d280fd91a5f9b4d89312664d56401f65b1473 - # via - # feast (setup.py) - # pymilvus -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) -multidict==6.7.0 \ - --hash=sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32 \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ - --hash=sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff \ - --hash=sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8 \ - --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ - --hash=sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 \ - --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ - --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6 \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ - --hash=sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912 \ - --hash=sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c \ - --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6 \ - --hash=sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84 \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ - --hash=sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd \ - --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0 \ - --hash=sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e \ - --hash=sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 \ - --hash=sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc \ - --hash=sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7 \ - --hash=sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4 \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111 \ - --hash=sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e \ - --hash=sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84 \ - --hash=sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851 \ - --hash=sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb \ - --hash=sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ - --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5 \ - --hash=sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb \ - --hash=sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349 \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ - --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ - --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34 \ - --hash=sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85 \ - --hash=sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ - --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ - --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ - --hash=sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91 \ - --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40 \ - --hash=sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73 \ - --hash=sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4 \ - --hash=sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687 \ - --hash=sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 +milvus-lite==3.1.1 \ + --hash=sha256:0cbf8386a2e99b6464a5eda14cf8e31934b0e287e36c831e293aee614d9efc2e \ + --hash=sha256:0f343c9bf1e291d6c2fa615a52a57090ebbaf96b84bfb71f7eb73cd95cd7fc1c + # via feast (pyproject.toml) +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via feast (pyproject.toml) +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via # aiobotocore # aiohttp # yarl -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mypy==2.3.0 \ + --hash=sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491 \ + --hash=sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762 \ + --hash=sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5 \ + --hash=sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654 \ + --hash=sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd \ + --hash=sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe \ + --hash=sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc \ + --hash=sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3 \ + --hash=sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5 \ + --hash=sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329 \ + --hash=sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b \ + --hash=sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3 \ + --hash=sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7 \ + --hash=sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805 \ + --hash=sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f \ + --hash=sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e \ + --hash=sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7 \ + --hash=sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494 \ + --hash=sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373 \ + --hash=sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88 \ + --hash=sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee \ + --hash=sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2 \ + --hash=sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757 \ + --hash=sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a \ + --hash=sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b \ + --hash=sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461 \ + --hash=sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97 \ + --hash=sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4 \ + --hash=sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117 \ + --hash=sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36 \ + --hash=sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac \ + --hash=sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5 \ + --hash=sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556 \ + --hash=sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c \ + --hash=sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88 \ + --hash=sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568 \ + --hash=sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595 \ + --hash=sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f \ + --hash=sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1 \ + --hash=sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef \ + --hash=sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6 \ + --hash=sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db \ + --hash=sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff \ + --hash=sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60 \ + --hash=sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c # via sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ @@ -1649,25 +1751,105 @@ numpy==2.2.6 \ --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes + # faiss-cpu # ibis-framework + # milvus-lite # pandas # pandas-gbq - # pyarrow oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 - # via - # kubernetes - # requests-oauthlib -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f + # via requests-oauthlib +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef + # via mcp +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via pymilvus +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # dask # db-dtypes + # faiss-cpu # google-cloud-bigquery # gunicorn # ibis-framework @@ -1730,7 +1912,7 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery @@ -1738,9 +1920,9 @@ pandas==2.3.3 \ # pandas-gbq # pymilvus # snowflake-connector-python -pandas-gbq==0.29.2 \ - --hash=sha256:99f6b642bd8340e96c2acdf7d717108aa0d31c703c2f284817ba7dc3b16110b2 \ - --hash=sha256:a3a28e7de4f8456fc4b8e9b7472b9bc470d2566f89c4cd997599e504f5a0a706 +pandas-gbq==0.35.0 \ + --hash=sha256:258de481019566611031919997bf9c1ece4ca30a4dd02d3fc3664b251d446182 \ + --hash=sha256:596c908487ef0649a161e86ef272c00c267e581b95dc5ee0dc3518545b33bcfc # via google-cloud-bigquery parsy==2.2 \ --hash=sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 \ @@ -1750,164 +1932,162 @@ partd==1.4.2 \ --hash=sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f \ --hash=sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c # via dask -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via mypy -platformdirs==4.5.0 \ - --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ - --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 # via snowflake-connector-python -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via feast (setup.py) -propcache==0.4.1 \ - --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ - --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ - --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ - --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ - --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ - --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ - --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ - --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ - --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ - --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via feast (pyproject.toml) +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274 \ + --hash=sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.26.1 \ - --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ - --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +proto-plus==1.28.2 \ + --hash=sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501 \ + --hash=sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52 # via # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==6.32.1 \ - --hash=sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346 \ - --hash=sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4 \ - --hash=sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1 \ - --hash=sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085 \ - --hash=sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1 \ - --hash=sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710 \ - --hash=sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122 \ - --hash=sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281 \ - --hash=sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d +protobuf==7.35.1 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -1919,235 +2099,275 @@ protobuf==6.32.1 \ # grpcio-status # proto-plus # pymilvus -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 - # via feast (setup.py) -psycopg[c, pool]==3.2.5 \ - --hash=sha256:b782130983e5b3de30b4c529623d3687033b4dafa05bb661fc6bf45837ca5879 \ - --hash=sha256:f5f750611c67cb200e85b408882f29265c66d1de7f813add4f8125978bfd70e8 - # via feast (setup.py) -psycopg-c==3.2.5 \ - --hash=sha256:57ad4cfd28de278c424aaceb1f2ad5c7910466e315dfe84e403f3c7a0a2ce81b +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # feast (pyproject.toml) + # pandas-gbq +psycopg[c, pool]==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via feast (pyproject.toml) +psycopg-c==3.3.4 \ + --hash=sha256:ed8106128b2d04359c185fc9641b4409abfce4d0b6fb1d1ff6800646e27f1a22 # via psycopg -psycopg-pool==3.2.6 \ - --hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \ - --hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7 +psycopg-pool==3.3.1 \ + --hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \ + --hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c # via psycopg -pyarrow==17.0.0 \ - --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ - --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ - --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ - --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ - --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ - --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ - --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ - --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ - --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ - --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ - --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ - --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ - --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ - --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ - --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ - --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ - --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ - --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ - --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ - --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ - --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ - --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ - --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ - --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ - --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ - --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ - --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ - --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ - --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ - --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ - --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ - --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ - --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ - --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ - --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ - --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 +pyarrow==25.0.0 \ + --hash=sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be \ + --hash=sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc \ + --hash=sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec \ + --hash=sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849 \ + --hash=sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e \ + --hash=sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8 \ + --hash=sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d \ + --hash=sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537 \ + --hash=sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062 \ + --hash=sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e \ + --hash=sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104 \ + --hash=sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517 \ + --hash=sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e \ + --hash=sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5 \ + --hash=sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887 \ + --hash=sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1 \ + --hash=sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b \ + --hash=sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be \ + --hash=sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778 \ + --hash=sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e \ + --hash=sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194 \ + --hash=sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2 \ + --hash=sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9 \ + --hash=sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b \ + --hash=sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18 \ + --hash=sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7 \ + --hash=sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc \ + --hash=sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62 \ + --hash=sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0 \ + --hash=sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f \ + --hash=sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e \ + --hash=sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862 \ + --hash=sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50 \ + --hash=sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe \ + --hash=sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3 \ + --hash=sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a \ + --hash=sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4 \ + --hash=sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af \ + --hash=sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b \ + --hash=sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0 \ + --hash=sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f \ + --hash=sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec \ + --hash=sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery # ibis-framework + # milvus-lite # pandas-gbq # snowflake-connector-python pyarrow-hotfix==0.7 \ --hash=sha256:3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 \ --hash=sha256:59399cd58bdd978b2e42816a4183a55c6472d4e33d183351b6069f11ed42661d # via ibis-framework -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 # via google-auth -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi # fastapi-mcp # mcp + # mcp-types # pydantic-settings -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pydantic-settings==2.11.0 \ - --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ - --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c - # via - # fastapi-mcp - # mcp +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f + # via fastapi-mcp pydata-google-auth==1.9.1 \ --hash=sha256:0a51ce41c601ca0bc69b8795bf58bedff74b4a6a007c9106c7cbcdec00eaced2 \ --hash=sha256:75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 # via pandas-gbq -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via - # feast (setup.py) + # feast (pyproject.toml) # rich -pyjwt==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via - # feast (setup.py) + # feast (pyproject.toml) + # mcp # snowflake-connector-python -pymilvus==2.4.9 \ - --hash=sha256:0937663700007c23a84cfc0656160b301f6ff9247aaec4c96d599a6b43572136 \ - --hash=sha256:45313607d2c164064bdc44e0f933cb6d6afa92e9efcc7f357c5240c57db58fbe - # via feast (setup.py) -pymysql==1.1.2 \ - --hash=sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03 \ - --hash=sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 - # via feast (setup.py) -pyopenssl==25.3.0 \ - --hash=sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6 \ - --hash=sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329 +pymilvus==3.0.1 \ + --hash=sha256:c02389059088b18d6e598cd175541e445c772fab4926c5e527c4913be34887f1 \ + --hash=sha256:c5a8d5c1fa1de7b416e3529d383d8cc2e7da2170433ffa4a2d9087e14f70171a + # via feast (pyproject.toml) +pymysql==1.2.0 \ + --hash=sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 \ + --hash=sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33 + # via feast (pyproject.toml) +pyopenssl==26.3.0 \ + --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ + --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 # via snowflake-connector-python python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ @@ -2159,22 +2379,21 @@ python-dateutil==2.9.0.post0 \ # ibis-framework # kubernetes # pandas -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via - # environs # pydantic-settings + # pymilvus # uvicorn -python-multipart==0.0.20 \ - --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \ - --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13 +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 # via mcp -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via - # ibis-framework # pandas # snowflake-connector-python pyyaml==6.0.3 \ @@ -2252,30 +2471,31 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # kubernetes # uvicorn -redis==4.6.0 \ - --hash=sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d \ - --hash=sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c - # via feast (setup.py) +redis==7.4.1 \ + --hash=sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e \ + --hash=sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + # via feast (pyproject.toml) referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 # via # jsonschema # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # google-api-core # google-cloud-bigquery # google-cloud-storage # kubernetes + # pymilvus # requests-oauthlib # snowflake-connector-python requests-oauthlib==2.0.0 \ @@ -2284,188 +2504,143 @@ requests-oauthlib==2.0.0 \ # via # google-auth-oauthlib # kubernetes -rich==13.9.4 \ - --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ - --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via # fastapi-mcp # ibis-framework # typer -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==0.30.0 \ + --hash=sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f \ + --hash=sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136 \ + --hash=sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3 \ + --hash=sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7 \ + --hash=sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65 \ + --hash=sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4 \ + --hash=sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169 \ + --hash=sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf \ + --hash=sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4 \ + --hash=sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2 \ + --hash=sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c \ + --hash=sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4 \ + --hash=sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3 \ + --hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \ + --hash=sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7 \ + --hash=sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89 \ + --hash=sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85 \ + --hash=sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6 \ + --hash=sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa \ + --hash=sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb \ + --hash=sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6 \ + --hash=sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87 \ + --hash=sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856 \ + --hash=sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4 \ + --hash=sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f \ + --hash=sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53 \ + --hash=sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229 \ + --hash=sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad \ + --hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \ + --hash=sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db \ + --hash=sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038 \ + --hash=sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27 \ + --hash=sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00 \ + --hash=sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18 \ + --hash=sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083 \ + --hash=sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c \ + --hash=sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738 \ + --hash=sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898 \ + --hash=sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e \ + --hash=sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7 \ + --hash=sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08 \ + --hash=sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6 \ + --hash=sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551 \ + --hash=sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e \ + --hash=sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288 \ + --hash=sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df \ + --hash=sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0 \ + --hash=sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2 \ + --hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \ + --hash=sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0 \ + --hash=sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464 \ + --hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \ + --hash=sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404 \ + --hash=sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7 \ + --hash=sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139 \ + --hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \ + --hash=sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb \ + --hash=sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15 \ + --hash=sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff \ + --hash=sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed \ + --hash=sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6 \ + --hash=sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e \ + --hash=sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95 \ + --hash=sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d \ + --hash=sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950 \ + --hash=sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3 \ + --hash=sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5 \ + --hash=sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97 \ + --hash=sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e \ + --hash=sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e \ + --hash=sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b \ + --hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \ + --hash=sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad \ + --hash=sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8 \ + --hash=sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425 \ + --hash=sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221 \ + --hash=sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d \ + --hash=sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825 \ + --hash=sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51 \ + --hash=sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e \ + --hash=sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f \ + --hash=sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8 \ + --hash=sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f \ + --hash=sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d \ + --hash=sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07 \ + --hash=sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877 \ + --hash=sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31 \ + --hash=sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58 \ + --hash=sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94 \ + --hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \ + --hash=sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000 \ + --hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \ + --hash=sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1 \ + --hash=sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7 \ + --hash=sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7 \ + --hash=sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40 \ + --hash=sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d \ + --hash=sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0 \ + --hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \ + --hash=sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f \ + --hash=sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a \ + --hash=sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7 \ + --hash=sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419 \ + --hash=sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 \ + --hash=sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a \ + --hash=sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9 \ + --hash=sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be \ + --hash=sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed \ + --hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a \ + --hash=sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d \ + --hash=sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324 \ + --hash=sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f \ + --hash=sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2 \ + --hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f \ + --hash=sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5 # via # jsonschema # referencing -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via google-auth -s3transfer==0.13.1 \ - --hash=sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724 \ - --hash=sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf +s3transfer==0.19.2 \ + --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \ + --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25 # via boto3 -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq # pydata-google-auth - # pymilvus shellingham==1.5.4 \ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de @@ -2476,780 +2651,750 @@ six==1.17.0 \ # via # kubernetes # python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio -snowflake-connector-python[pandas]==3.18.0 \ - --hash=sha256:0af10b207af3d2de2b130e89018d49a60f2e5cfe841f3bf459e58f2e1c4c4506 \ - --hash=sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628 \ - --hash=sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4 \ - --hash=sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e \ - --hash=sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee \ - --hash=sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9 \ - --hash=sha256:32b1abfea32561d817b0a2f80b06d936cb32712af06bf7b848a428bfd857a10a \ - --hash=sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a \ - --hash=sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728 \ - --hash=sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800 \ - --hash=sha256:4ed2d593f1983939d5d8d88b212d86fd4f14f0ceefc1df9882b4a18534adbde9 \ - --hash=sha256:51eb789a09dc6c62119cfabd044fba1a6b8378206f05a1e83ddb2e9cb49acc0b \ - --hash=sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc \ - --hash=sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb \ - --hash=sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba \ - --hash=sha256:783a9ab206563d7b52fdcdd7a72af62de811d3381ca64132fd3445537b4d041b \ - --hash=sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f \ - --hash=sha256:8d3e96e1d09b07edca6c1f6ca675b6fdd05a4a7e428e4cdf6fb697d87b9f60fc \ - --hash=sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a \ - --hash=sha256:a8c570edff5a4888840dbe1e9e65c5e4d77d55c5c800cd359fe0903a769201e0 \ - --hash=sha256:aeeb181a156333480f60b5f8ddbb3d087e288b4509adbef7993236defe4d7570 \ - --hash=sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca \ - --hash=sha256:b99f261c82be92224ac20c8c12bdf26ce3ed5dfd8a3df8a97f15a1e11c46ad27 \ - --hash=sha256:bd1de3038b6d7059ca59f93e105aba2a673151c693cc4292f72f38bfaf147df2 \ - --hash=sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e \ - --hash=sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00 - # via feast (setup.py) +snowflake-connector-python[pandas]==4.7.1 \ + --hash=sha256:051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117 \ + --hash=sha256:052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7 \ + --hash=sha256:0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317 \ + --hash=sha256:167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf \ + --hash=sha256:171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a \ + --hash=sha256:1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6 \ + --hash=sha256:22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf \ + --hash=sha256:2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b \ + --hash=sha256:4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a \ + --hash=sha256:4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754 \ + --hash=sha256:5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9 \ + --hash=sha256:571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d \ + --hash=sha256:5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35 \ + --hash=sha256:7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0 \ + --hash=sha256:7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5 \ + --hash=sha256:81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57 \ + --hash=sha256:8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a \ + --hash=sha256:92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3 \ + --hash=sha256:b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45 \ + --hash=sha256:d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa \ + --hash=sha256:e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327 \ + --hash=sha256:f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c \ + --hash=sha256:f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7 \ + --hash=sha256:f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52 \ + --hash=sha256:fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1 \ + --hash=sha256:ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b + # via feast (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via snowflake-connector-python -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -sqlglot==25.20.2 \ - --hash=sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab \ - --hash=sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567 +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via feast (pyproject.toml) +sqlglot==30.14.0 \ + --hash=sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9 \ + --hash=sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636 # via ibis-framework -sse-starlette==3.0.2 \ - --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ - --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 \ + --hash=sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627 # via mcp -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via + # feast (pyproject.toml) # fastapi # mcp -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via feast (setup.py) + # sse-starlette +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via feast (pyproject.toml) tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via feast (pyproject.toml) toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via # fastapi-mcp + # milvus-lite # mypy -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 # via snowflake-connector-python -toolz==0.12.1 \ - --hash=sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85 \ - --hash=sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # dask # ibis-framework # partd -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via feast (pyproject.toml) +truststore==0.10.4 \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 # via - # feast (setup.py) - # milvus-lite -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) -typer==0.19.2 \ - --hash=sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9 \ - --hash=sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca + # httpcore2 + # httpx2 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) +typer==0.27.0 \ + --hash=sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5 \ + --hash=sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1 # via fastapi-mcp -types-pymysql==1.1.0.20250916 \ - --hash=sha256:873eb9836bb5e3de4368cc7010ca72775f86e9692a5c7810f8c7f48da082e55b \ - --hash=sha256:98d75731795fcc06723a192786662bdfa760e1e00f22809c104fbb47bac5e29b - # via feast (setup.py) -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +types-pymysql==1.2.0.20260724 \ + --hash=sha256:71327a3fea0f680a280445f1b475c2307f68de05c2d3302a79df56af93c574cd \ + --hash=sha256:8493cf87e8df0a7b4e8c84305bc0aae69d3a8af9c6da7bfbb9a4c5c3f97d4a82 + # via feast (pyproject.toml) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiobotocore + # aiohttp # aiosignal # anyio # cryptography # exceptiongroup # fastapi + # grpcio + # httpx2 # ibis-framework - # marshmallow + # mcp + # mcp-types # multidict # mypy + # opentelemetry-api # psycopg # psycopg-pool # pydantic # pydantic-core + # pyjwt # pyopenssl # referencing - # rich # snowflake-connector-python # sqlalchemy # starlette # typeguard - # typer # typing-inspection # uvicorn typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via pydantic-settings -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via pandas -ujson==5.11.0 \ - --hash=sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80 \ - --hash=sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef \ - --hash=sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a \ - --hash=sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840 \ - --hash=sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53 \ - --hash=sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076 \ - --hash=sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab \ - --hash=sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d \ - --hash=sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89 \ - --hash=sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c \ - --hash=sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb \ - --hash=sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c \ - --hash=sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823 \ - --hash=sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5 \ - --hash=sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac \ - --hash=sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d \ - --hash=sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723 \ - --hash=sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6 \ - --hash=sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0 \ - --hash=sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328 \ - --hash=sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0 \ - --hash=sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04 \ - --hash=sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25 \ - --hash=sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49 \ - --hash=sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec \ - --hash=sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3 \ - --hash=sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3 \ - --hash=sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc \ - --hash=sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a \ - --hash=sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9 \ - --hash=sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302 \ - --hash=sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694 \ - --hash=sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547 \ - --hash=sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01 \ - --hash=sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02 \ - --hash=sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6 \ - --hash=sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60 \ - --hash=sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915 \ - --hash=sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835 \ - --hash=sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701 \ - --hash=sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702 \ - --hash=sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50 \ - --hash=sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6 \ - --hash=sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc \ - --hash=sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a \ - --hash=sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c \ - --hash=sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c \ - --hash=sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03 \ - --hash=sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629 \ - --hash=sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105 \ - --hash=sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844 \ - --hash=sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241 \ - --hash=sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a \ - --hash=sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26 \ - --hash=sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac \ - --hash=sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596 \ - --hash=sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9 \ - --hash=sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752 \ - --hash=sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138 \ - --hash=sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b \ - --hash=sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba \ - --hash=sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5 \ - --hash=sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018 \ - --hash=sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362 \ - --hash=sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746 \ - --hash=sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f \ - --hash=sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88 \ - --hash=sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638 \ - --hash=sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b \ - --hash=sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9 \ - --hash=sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db \ - --hash=sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f \ - --hash=sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58 \ - --hash=sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b \ - --hash=sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433 \ - --hash=sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0 \ - --hash=sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764 \ - --hash=sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c \ - --hash=sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34 \ - --hash=sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052 \ - --hash=sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba \ - --hash=sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c \ - --hash=sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc \ - --hash=sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39 - # via pymilvus -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via + # ibis-framework + # pandas +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via # botocore # kubernetes # requests -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # mcp # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn websocket-client==1.9.0 \ --hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \ --hash=sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef # via kubernetes -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 # via aiobotocore -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8 \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf \ - --hash=sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890 \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737 \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba \ - --hash=sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9 \ - --hash=sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0 \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147 \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e \ - --hash=sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2 \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6 \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750 \ + --hash=sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621 \ + --hash=sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13 \ + --hash=sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1 \ + --hash=sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2 \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata diff --git a/sdk/python/requirements/py3.10-minimal-sdist-requirements-build.txt b/sdk/python/requirements/py3.10-minimal-sdist-requirements-build.txt index 75742bd5987..f7837102091 100644 --- a/sdk/python/requirements/py3.10-minimal-sdist-requirements-build.txt +++ b/sdk/python/requirements/py3.10-minimal-sdist-requirements-build.txt @@ -4,100 +4,154 @@ # # pybuild-deps compile --generate-hashes --output-file=sdk/python/requirements/py3.10-minimal-sdist-requirements-build.txt sdk/python/requirements/py3.10-minimal-sdist-requirements.txt # +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy calver==2025.3.31 \ --hash=sha256:07511edf5e7fa75ae97445c8c5921240e0fe62937289a3ebe6963eddd3c691b6 \ --hash=sha256:255d1a70bba8f97dc1eee3af4240ed35980508da69257feef94c79e5c6545fc7 # via trove-classifiers -cffi==2.0.0 \ - --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ - --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ - --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ - --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ - --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ - --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ - --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ - --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ - --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ - --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ - --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ - --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ - --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ - --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ - --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ - --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ - --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ - --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ - --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ - --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ - --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ - --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ - --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ - --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ - --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ - --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ - --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ - --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ - --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ - --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ - --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ - --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ - --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ - --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ - --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ - --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ - --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ - --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ - --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ - --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ - --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ - --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ - --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ - --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ - --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ - --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ - --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ - --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ - --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ - --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ - --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ - --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ - --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ - --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ - --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ - --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ - --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ - --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ - --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ - --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ - --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ - --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ - --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ - --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ - --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ - --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ - --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ - --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ - --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ - --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ - --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ - --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ - --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ - --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ - --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f # via cryptography coherent-licensed==0.5.2 \ --hash=sha256:d8071403ce742d3ac3592ddc4fb7057a46caffb415b928b4d52802e5f208416d \ --hash=sha256:edccc5193ca2786f8fb3f0c7374637a143985f781f7eaa21aca3af2bd634b82f - # via zipp + # via + # importlib-metadata + # zipp cython==3.0.12 \ --hash=sha256:0038c9bae46c459669390e53a1ec115f8096b2e4647ae007ff1bf4e6dee92806 \ --hash=sha256:0faa5e39e5c8cdf6f9c3b1c3f24972826e45911e7f5b99cf99453fca5432f45e \ @@ -166,20 +220,123 @@ cython==3.0.12 \ # via # numpy # pandas - # pyarrow # pyyaml # snowflake-connector-python # sqlalchemy - # uvloop -dunamai==1.25.0 \ - --hash=sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab \ - --hash=sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1 +cython==3.1.1 \ + --hash=sha256:011cdcbf7725f0cfc1abc55ec83d326e788050711272131daf3cc24a19c34bb2 \ + --hash=sha256:020089f9c9f10269181f17660a2cada7d4577bd8eea24b7d2b14e6b64b6996be \ + --hash=sha256:07621e044f332d18139df2ccfcc930151fd323c2f61a58c82f304cffc9eb5280 \ + --hash=sha256:0de7adff5b42d2556d073e9f321c2faa639a17fb195ec1de130327f60ec209d8 \ + --hash=sha256:0e3ccec55e2a534a712db14c6617b66f65ad149c014fad518fc3920f6edde770 \ + --hash=sha256:0f7954b0b4b3302655d3caa6924261de5907a4e129bc22ace52fe9ae0cd5a758 \ + --hash=sha256:10f0434916994fe213ea7749268b88d77e3ebcbd1b99542cf64bb7d180f45470 \ + --hash=sha256:12e00b88147b03c148a95365f89dc1c45a0fc52f9c35aa75ff770ef65b615839 \ + --hash=sha256:141ffd6279411c562f6b707adc56b63e965a4fd7f21db83f5d4fcbd8c50ac546 \ + --hash=sha256:16d9870654946375b28280371d370d541641d1071da123d0d64d2c7ebba0cc56 \ + --hash=sha256:23b886a6c8a50b1101ccef2f2f3dc9c699b77633ef5bb5007090226c2ad3f9c2 \ + --hash=sha256:24c640c0746d984789fe2787a098f06cda456ef2dd78b90164d17884b350839a \ + --hash=sha256:263cb0e497910fb5e0a361ad1393b6d728b092178afecc56e8a786f3739960c3 \ + --hash=sha256:268420b92307ae6c5a16e3cf0e2ba1ae3c861650e992893922a0ce08db07cfdb \ + --hash=sha256:28b174f41718a7041cfbe0f48913020875ff1aaa4793942b2451ac6d2baf3f07 \ + --hash=sha256:307f216ed319ea07644f2ef9974406c830f01bc8e677e2147e9bfcdf9e3ca8ad \ + --hash=sha256:3192a61c2a532d3faccdff508bc8427de9530b587888218bfc0226eb33a84e11 \ + --hash=sha256:3fa4bd840de63509c74867b4b092541720a01db1e07351206011c34e0777dc96 \ + --hash=sha256:402f86c00b08f875cd0990f0c4dc52eb3e0bc5d630066cdf3c798631976f1937 \ + --hash=sha256:40f50b07c479eaf33981d81cad274c68cf9fb81dbe79cbf991f59491c88a4705 \ + --hash=sha256:426d78565eb91d3366569b20e92b8f14bffef5f57b2acd05b60bbb9ce5c056a1 \ + --hash=sha256:505ccd413669d5132a53834d792c707974248088c4f60c497deb1b416e366397 \ + --hash=sha256:50ad80e2f438e9127a87c10927e6ac16a987df39c248b19ab2cd31330129be3c \ + --hash=sha256:54a8934cb3bf13b1f8f6cbdae8e382e25a26e67de08ea6ebfd0a467131b67227 \ + --hash=sha256:56c6768a6f601f93daab7c2487f9f110548a896a91e00a6e119445ada2575323 \ + --hash=sha256:64915259276482fa23417b284d1fdc7e3a618ee2f819bb6ea7f974c075633df6 \ + --hash=sha256:689c1aad373556bd2ab1aa1c2dad8939a2891465a1fbd2cbbdd42b488fb40ec8 \ + --hash=sha256:6ea77ad1e649cec38f8622ba28dcdfbe7bf519bc132abbcf5df759b3975b5a73 \ + --hash=sha256:7489559e6c5ecbba49d535c2e03cf77c2594a3190b6aca7da5b508ba1664a89a \ + --hash=sha256:755a991601b27dd3555310d0f95b19a05e622a80d7b4e7a91fa6f5f3ef3f3b80 \ + --hash=sha256:7da069ca769903c5dee56c5f7ab47b2b7b91030eee48912630db5f4f3ec5954a \ + --hash=sha256:7e5cad896af896482240979b996bf4136b0d18dc40c56c72c5641bf0ea085dfb \ + --hash=sha256:7fff6526bb6f4eea615663117b86de6ede0d17c477b600d3d8302be3502bd3c3 \ + --hash=sha256:83b2af5c327f7da4f08afc34fddfaf6d24fa0c000b6b70a527c8125e493b6080 \ + --hash=sha256:873aac4ac0b0fb197557c0ac15458b780b9221daa4a716881cbd1a9016c8459f \ + --hash=sha256:8aaa29e763adf3496ab9d371e3caed8da5d3ce5ff8fb57433e2a2f2b5036e5c8 \ + --hash=sha256:953046c190fa9ab9a09a546a909b847cdbb4c1fe34e9bfa4a15b6ee1585a86aa \ + --hash=sha256:9b61b99205308c96b1162de59bd67ecadcad3d166a4a1f03a3d9e826c39cd375 \ + --hash=sha256:9d7dc0e4d0cd491fac679a61e9ede348c64ca449f99a284f9a01851aa1dbc7f6 \ + --hash=sha256:a19188ecd385cdc649e3fec370f38d5fd7f1651aeed0b3fb403180f38fc88e8a \ + --hash=sha256:a585796939b09b3205b1980e4a55e745c0251e45a5c637afbcac3c6cc9ad6f90 \ + --hash=sha256:a92f6bd395eadea6eed722a8188d3bdd49db1c9fa3c38710456d6148ab71bad7 \ + --hash=sha256:ab644415458d782c16ba7252de9cec1e3125371641cafea2e53a8c1cf85dd58d \ + --hash=sha256:af8f62cc9339b75fe8434325083e6a7cae88c9c21efd74bbb6ba4e3623219469 \ + --hash=sha256:b181158b5761bdaf40f6854f016ab7ddff64d3db4fca55cb3ca0f73813dd76d6 \ + --hash=sha256:b194a65a0fd91f305d2d1e7010f44111774a28533e1e44dd2a76e7de81a219b9 \ + --hash=sha256:b68f1bc80387554eb43f2b62795c173bed9e37201f39dc5084ac437c90a79c9f \ + --hash=sha256:c360823e1063784efc2335617e0f28573d7a594c5a8a05d85e850a9621cccb1f \ + --hash=sha256:c5cb6c054daadaf01a88c8f49f3edd9e829c9b76a82cbb4269e3f9878254540b \ + --hash=sha256:c740a10cd0f50321d048c8ca318eefb4c42b8bffef982dcd89c946d374192702 \ + --hash=sha256:c8b8be01fd40b3e38a76c60a524f956548a3a7566e5530a833a48a695f3d6c12 \ + --hash=sha256:cb5661941707bd41ec7a9c273d698113ac50392444f785088e9d9706c6a5937b \ + --hash=sha256:cd748fab8e4426dbcb2e0fa2979558333934d24365e0de5672fbabfe337d880c \ + --hash=sha256:cdf53dc4b2a13bd072d6c2c18ac073dbf0f798555bc27ba4f7546a275eb16a0f \ + --hash=sha256:ce82070ccf92c3599d331b9eaaefd9d4562976fb86a8d6bccf05c4a0b8389f2a \ + --hash=sha256:d14186bd96783d13b8fd0e5b289f2e137a8a25479638b73a1c7e4a99a8d70753 \ + --hash=sha256:dee554f0a589377bdaea0eb70e212bf3f35dc6a51a2aa86c9351345e21fd2f07 \ + --hash=sha256:dfa500fd7ae95ca152a5f8062b870532fa3e27efcef6d00612e1f28b9f72615f \ + --hash=sha256:dff0e7dd53a0ca35b64cda843253d5cac944db26663dc097b3a1adf2c49514ad \ + --hash=sha256:e000f0533eedf3d6dfbe30bb3c58a054c58f0a7778390342fa577a0dc47adab3 \ + --hash=sha256:e851ab66a31794e40df1bc6f649cdc56c998c637f5a1b9410c97a90f6b6cb855 \ + --hash=sha256:fd689910002adfac8734f237cdea1573e38345f27ed7fd445482813b65a29457 + # via grpcio +cython==3.2.9 \ + --hash=sha256:114b2dee0fa1daa48a59574d848da0ff1b6bdb725a755e9b92fad14962e1ff8d \ + --hash=sha256:1c2974742433be5c36ae1df1e761ce0063753fc2702316d5a3062b0a4e94a1df \ + --hash=sha256:23e80bc885c599e72072e18d0746df82d394b73100c1e153cda7359e6e59fe09 \ + --hash=sha256:2751b7c0cb13135aadcc1cc8fe223be98d458cd1132611d31675e768a5e4a2e9 \ + --hash=sha256:2b1756ddc3bc0cd4341a515fc420c3e25e13c249f5537159b3fb0bff8d19e55c \ + --hash=sha256:41637c644d7224d2d3170ee312077c2173693f0d4a0da1c82a0cffa3680a42dc \ + --hash=sha256:44b7fc417933d65bf31bce00337ff318efa3ea59daed21d10fe842d41e657c08 \ + --hash=sha256:4b1fd5a9c03f72a18618668a8e90d569442ed742f910e3ad003dcc9348e9598b \ + --hash=sha256:4b871ad97dd7fb1cbf56f6238c54423febd310afc1d9d9bc70c69c89b7ce57fc \ + --hash=sha256:522e277c29ebee304e73f3a78307549d09908b0c63098421a12dffc8d779fd74 \ + --hash=sha256:56d95c0674c25f281c6ae8f1d17bd425d6c2818bb304ff781831bb5d00d04b0b \ + --hash=sha256:57a6a78d14f7dd7d6062d9bca694e2a8c1c14113b6ceceea076abcd1161fdc5a \ + --hash=sha256:61d4abbf84f77c8d19361d05d9f51d65d8d95e74f736eae55fa1aed8a1430469 \ + --hash=sha256:63a42e131112072f912b66734088abc946c6bc42cb7454461a6dfdd2d09d3bae \ + --hash=sha256:681980fcc82b346ac83bc7d59e68ca1eba74ed9576f5d8584761d0c6c68c45d0 \ + --hash=sha256:788ea800618d20158166d09f60835aab50922ae1cfe3108c93fa72e173b7002d \ + --hash=sha256:7c534d782757710e39f3c9582214d7a28a0d68c86e78ee3df1d729cbc270e133 \ + --hash=sha256:7d41baea51ea00f9237f75af498577827493bca5e9b45bbd4e351543727e589a \ + --hash=sha256:8c843dd85857cd0d0da055489f448d032866120cfb094ce16205a1ff54d06353 \ + --hash=sha256:92989da161a7d18a7ad4baebc49289b2b77556d5a94916f90140ba26aecf6892 \ + --hash=sha256:944dc8747f640b3527649c566a5fc75ee0c15e80642ea2fdae4fe6378e1a9d4a \ + --hash=sha256:9d3cba9c4770cf76370fead8d2ae71bc474f3ef67ae4119e455d025726bcdc1c \ + --hash=sha256:a2b0e87f6b80790c929308ca0831d686f7a180feab684fe8cd4a4380bd96aaca \ + --hash=sha256:a3fc783d12202d1b064b6f125772d85f00e36e62eee6b2e415f56d8fd2d2e7b2 \ + --hash=sha256:a5cd9c5f138cb052130b40ad3b6976d2180c35348410995812678f4636bd8f94 \ + --hash=sha256:c6d4a92a87d238231564b5cbf27a8d1e46b7e62040c66b5ff33778a16b22295e \ + --hash=sha256:cac792b0bde1c86d8f832e513cd061b7e682181cc5a6d843d487e6c8ae9d5fcb \ + --hash=sha256:ccb0bc6b8437cfcb04459a02bc5ecc58bf161ea11659438945ab5a1392ee39fe \ + --hash=sha256:d15c4d4ba8a3ad284124384f769465bfba45512aaa4c81caea8a165f79fc042a \ + --hash=sha256:d249c9022ab13286b17bd66f30609e800c5f95efeecb06168990c7a66cecde6c \ + --hash=sha256:ddb43637b7749b88644df4319e1c36f767e7fb71a92bb9942558c8de2f1cc5f4 \ + --hash=sha256:e75ec625d8f8781ced690b7a2f5c2d138067711cf24bb8fb68c872c30c2fefe5 \ + --hash=sha256:e8ec2d5a7b798c84d6779e7ca5318fd928b8fe9dd021106d714dc2e77cdc7555 \ + --hash=sha256:e9b6ebc6c74b4318eaa4e51e520dc8b95ebc7b262953c3ecb24131104681f14e \ + --hash=sha256:e9c39962f749bd40c6e8c5063d498bd3749947888306ffedb99a0cab3a69f995 \ + --hash=sha256:efd54fe07f808e7e82f6a04f370457ca02e770c948f0e2f83345ccc7ec8bb829 \ + --hash=sha256:f25b402a6f1eed34af2af57603a9aadcf594dacaced1655e925eb317fba3f337 \ + --hash=sha256:f2fe4bc1b8fa4130563b94a3614db9ea17e92a455bed00e6a5d03eaff434b33c \ + --hash=sha256:f815cd3387bd88ca9eeabc357ce4bdf884eef0ab2949db0fbd5e0b69fe5ee422 # via - # poetry-dynamic-versioning - # uv-dynamic-versioning -exceptiongroup==1.3.0 \ - --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ - --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 + # pyarrow + # uvloop +dunamai==1.26.1 \ + --hash=sha256:2727d939c5b4257cb01ea404372803b477f5176e5a347c43beaf89cd5072e853 \ + --hash=sha256:3b46007bd65b00b4824ead0a1aee365fd22d0ec2b9c219497d4fd48f52860c8b + # via uv-dynamic-versioning +exceptiongroup==1.3.1 \ + --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ + --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via scikit-build-core expandvars==1.1.2 \ --hash=sha256:6c5822b7b756a99a356b915dd1267f52ab8a4efaa135963bd7f4bd5d368f71d7 \ @@ -202,7 +359,6 @@ flit-core==3.12.0 \ # idna # jinja2 # markdown-it-py - # marshmallow # mdurl # mypy-extensions # packaging @@ -215,27 +371,22 @@ flit-core==3.12.0 \ # sphinxcontrib-qthelp # sphinxcontrib-serializinghtml # tomli + # truststore # typing-extensions # wheel flit-scm==1.7.0 \ --hash=sha256:961bd6fb24f31bba75333c234145fff88e6de0a90fc0f7e5e7c79deca69f6bb2 \ --hash=sha256:9e864caa8a63f708f5bb2f1b5b53eedcd4da75ec2cc6221a64cea7aa5c9eae1a # via exceptiongroup -gitdb==4.0.12 \ - --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ - --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf - # via gitpython -gitpython==3.1.45 \ - --hash=sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c \ - --hash=sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77 - # via pymilvus hatch-fancy-pypi-readme==25.1.0 \ --hash=sha256:9c58ed3dff90d51f43414ce37009ad1d5b0f08ffc9fc216998a06380f01c0045 \ --hash=sha256:ce0134c40d63d874ac48f48ccc678b8f3b62b8e50e9318520d2bffc752eedaf3 # via # attrs # httpcore + # httpcore2 # httpx + # httpx2 # jsonschema # pydantic hatch-vcs==0.4.0 \ @@ -255,9 +406,9 @@ hatch-vcs==0.5.0 \ # via # filelock # platformdirs -hatchling==1.27.0 \ - --hash=sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6 \ - --hash=sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b +hatchling==1.31.0 \ + --hash=sha256:6b48ad4068a482ed7239b3a8215bc55b47aad3345d58dfc94e553c5d2d46211b \ + --hash=sha256:aac80bec8b6fe35e8480f1c335be8910fa210a0e6f735a139be205dadcacb544 # via # annotated-types # atpublic @@ -271,16 +422,22 @@ hatchling==1.27.0 \ # hatch-fancy-pypi-readme # hatch-vcs # httpcore + # httpcore2 # httpx + # httpx2 + # ibis-framework # jsonschema # jsonschema-specifications # mcp + # mcp-types + # opentelemetry-api # platformdirs # pyarrow-hotfix # pydantic # pydantic-settings # pygments # python-multipart + # redis # referencing # scikit-build-core # starlette @@ -292,9 +449,168 @@ hatchling==1.27.0 \ jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via - # poetry-dynamic-versioning - # uv-dynamic-versioning + # via uv-dynamic-versioning +libcst==1.8.6 \ + --hash=sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30 \ + --hash=sha256:06fc56335a45d61b7c1b856bfab4587b84cfe31e9d6368f60bb3c9129d900f58 \ + --hash=sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d \ + --hash=sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a \ + --hash=sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9 \ + --hash=sha256:0cbe17067055829607c5ba4afa46bfa4d0dd554c0b5a583546e690b7367a29b6 \ + --hash=sha256:16cfe0cfca5fd840e1fb2c30afb628b023d3085b30c3484a79b61eae9d6fe7ba \ + --hash=sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105 \ + --hash=sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330 \ + --hash=sha256:203ec2a83f259baf686b9526268cd23d048d38be5589594ef143aee50a4faf7e \ + --hash=sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1 \ + --hash=sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786 \ + --hash=sha256:25fc7a1303cad7639ad45ec38c06789b4540b7258e9a108924aaa2c132af4aca \ + --hash=sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012 \ + --hash=sha256:351ab879c2fd20d9cb2844ed1ea3e617ed72854d3d1e2b0880ede9c3eea43ba8 \ + --hash=sha256:36473e47cb199b7e6531d653ee6ffed057de1d179301e6c67f651f3af0b499d6 \ + --hash=sha256:3649a813660fbffd7bc24d3f810b1f75ac98bd40d9d6f56d1f0ee38579021073 \ + --hash=sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86 \ + --hash=sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474 \ + --hash=sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8 \ + --hash=sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa \ + --hash=sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42 \ + --hash=sha256:455f49a93aea4070132c30ebb6c07c2dea0ba6c1fde5ffde59fc45dbb9cfbe4b \ + --hash=sha256:4d7bbdd35f3abdfb5ac5d1a674923572dab892b126a58da81ff2726102d6ec2e \ + --hash=sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196 \ + --hash=sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93 \ + --hash=sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58 \ + --hash=sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1 \ + --hash=sha256:59a7e388c57d21d63722018978a8ddba7b176e3a99bd34b9b84a576ed53f2978 \ + --hash=sha256:5dcaaebc835dfe5755bc85f9b186fb7e2895dda78e805e577fef1011d51d5a5c \ + --hash=sha256:6366ab2107425bf934b0c83311177f2a371bfc757ee8c6ad4a602d7cbcc2f363 \ + --hash=sha256:6421a930b028c5ef4a943b32a5a78b7f1bf15138214525a2088f11acbb7d3d64 \ + --hash=sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d \ + --hash=sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996 \ + --hash=sha256:6aa11df6c58812f731172b593fcb485d7ba09ccc3b52fea6c7f26a43377dc748 \ + --hash=sha256:6b23d14a7fc0addd9795795763af26b185deb7c456b1e7cc4d5228e69dab5ce8 \ + --hash=sha256:6cad63e3a26556b020b634d25a8703b605c0e0b491426b3e6b9e12ed20f09100 \ + --hash=sha256:6d8b67874f2188399a71a71731e1ba2d1a2c3173b7565d1cc7ffb32e8fbaba5b \ + --hash=sha256:72cca15800ffc00ba25788e4626189fe0bc5fe2a0c1cb4294bce2e4df21cc073 \ + --hash=sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb \ + --hash=sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4 \ + --hash=sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde \ + --hash=sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f \ + --hash=sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f \ + --hash=sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661 \ + --hash=sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1 \ + --hash=sha256:98fa1ca321c81fb1f02e5c43f956ca543968cc1a30b264fd8e0a2e1b0b0bf106 \ + --hash=sha256:a20c5182af04332cc94d8520792befda06d73daf2865e6dddc5161c72ea92cb9 \ + --hash=sha256:b0d8c364c44ae343937f474b2e492c1040df96d94530377c2f9263fb77096e4f \ + --hash=sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c \ + --hash=sha256:b6c1248cc62952a3a005792b10cdef2a4e130847be9c74f33a7d617486f7e532 \ + --hash=sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4 \ + --hash=sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9 \ + --hash=sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82 \ + --hash=sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7 \ + --hash=sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28 \ + --hash=sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf \ + --hash=sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e \ + --hash=sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5 \ + --hash=sha256:cb2679ef532f9fa5be5c5a283b6357cb6e9888a8dd889c4bb2b01845a29d8c0b \ + --hash=sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d \ + --hash=sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47 \ + --hash=sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09 \ + --hash=sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b \ + --hash=sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4 + # via pyarrow +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ @@ -386,23 +702,25 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -maturin==1.9.6 \ - --hash=sha256:0246202377c49449315305209f45c8ecef6e2d6bd27a04b5b6f1ab3e4ea47238 \ - --hash=sha256:1b39a5d82572c240d20d9e8be024d722dfb311d330c5e28ddeb615211755941a \ - --hash=sha256:26e3ab1a42a7145824210e9d763f6958f2c46afb1245ddd0bab7d78b1f59bb3f \ - --hash=sha256:2c2ae37144811d365509889ed7220b0598487f1278c2441829c3abf56cc6324a \ - --hash=sha256:5263dda3f71feef2e4122baf5c4620e4b3710dbb7f2121f85a337182de214369 \ - --hash=sha256:5c2252b0956bb331460ac750c805ddf0d9b44442449fc1f16e3b66941689d0bc \ - --hash=sha256:7ab827c6e8c022eb2e1e7fb6deede54549c8460b20ccc2e9268cc6e8cde957a8 \ - --hash=sha256:7f506eb358386d94d6ec3208c003130cf4b69cab26034fc0cbbf8bf83afa4c2e \ - --hash=sha256:7f53d3b1d8396d3fea3e1ee5fd37558bca5719090f3d194ba1c02b0b56327ae3 \ - --hash=sha256:ac02a30083553d2a781c10cd6f5480119bf6692fd177e743267406cad2ad198c \ - --hash=sha256:f2c58d29ebdd4346fd004e6be213d071fdd94a77a16aa91474a21a4f9dbf6309 \ - --hash=sha256:f2d6984ab690af509f525dbd2b130714207c06ebb14a5814edbe1e42b17ae0de \ - --hash=sha256:f5bac167700fbb6f8c8ed1a97b494522554b4432d7578e11403b894b6a91d99f \ - --hash=sha256:fe78262c2800c92f67d1ce3c0f6463f958a692cc67bfb572e5dbf5b4b696a8ba +maturin==1.14.1 \ + --hash=sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894 \ + --hash=sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0 \ + --hash=sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd \ + --hash=sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8 \ + --hash=sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4 \ + --hash=sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666 \ + --hash=sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df \ + --hash=sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224 \ + --hash=sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69 \ + --hash=sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65 \ + --hash=sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d \ + --hash=sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e \ + --hash=sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c \ + --hash=sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed # via + # ast-serialize # cryptography + # orjson # pydantic-core # rpds-py # watchfiles @@ -481,167 +799,237 @@ numpy==2.2.6 \ # via # pandas # pyarrow -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via - # dunamai # hatchling # pyproject-metadata # scikit-build-core + # setuptools-git-versioning # setuptools-scm -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # vcs-versioning + # wheel +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via # hatchling # mypy # scikit-build-core -pdm-backend==2.4.5 \ - --hash=sha256:56c019c440308adad5d057c08cbb777e65f43b991a3b0920749781258972fe5b \ - --hash=sha256:7f6c780b529baaf88947e12203af46b3ed45cff719b04c870b186ba9cdc2ddab +pdm-backend==2.4.9 \ + --hash=sha256:8d1064751dadb0c40b1026b46826a448d85c8228a564985c62fb53d4aba538a1 \ + --hash=sha256:c41a852ccbf4b567b033db9b2ae78660d7a4ea49307719959ab88a01f0aabb2e # via + # annotated-doc # fastapi # typer -pkgconfig==1.5.5 \ - --hash=sha256:d20023bbeb42ee6d428a0fac6e0904631f545985a10cdd71a20aa58bc47a4209 \ - --hash=sha256:deb4163ef11f75b520d822d9505c1f462761b4309b1bb713d08689759ea8b899 +pkgconfig==1.6.0 \ + --hash=sha256:4a5a6631ce937fafac457104a40d558785a658bbdca5c49b6295bc3fd651907f \ + --hash=sha256:98e71754855e9563838d952a160eb577edabb57782e49853edb5381927e6bea1 # via aiohttp pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via hatchling -poetry-core==1.9.1 \ - --hash=sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0 \ - --hash=sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8 +poetry-core==2.4.1 \ + --hash=sha256:89dceb6c10e9c6d8650a16183400e3c9ff9ddee13b0a81023b5575334a2b3744 \ + --hash=sha256:acf06f9537cd2625bdaec926d95d90b557ba15353bc71d27a3a8a441042b5316 # via + # aiohappyeyeballs # dunamai - # ibis-framework - # ibis-substrait # pkgconfig - # poetry-dynamic-versioning # rich - # rsa # tomlkit -poetry-core==2.2.1 \ - --hash=sha256:97e50d8593c8729d3f49364b428583e044087ee3def1e010c6496db76bd65ac5 \ - --hash=sha256:bdfce710edc10bfcf9ab35041605c480829be4ab23f5bc01202cfe5db8f125ab - # via aiohappyeyeballs -poetry-dynamic-versioning==1.9.1 \ - --hash=sha256:65a0c814e6d30d4807734a3c34edf261fd7cc3b340dbd23b6a33ee41f7d0b547 \ - --hash=sha256:d6e7b9df817aa2ca4946cd695c6c89e1379d2e6c640f008a9b6170d081a9da48 - # via ibis-framework -pybind11==3.0.1 \ - --hash=sha256:9c0f40056a016da59bab516efb523089139fcc6f2ba7e4930854c61efb932051 \ - --hash=sha256:aa8f0aa6e0a94d3b64adfc38f560f33f15e589be2175e103c0a33c6bce55ee89 +pybind11-global==3.0.4 \ + --hash=sha256:95b693c3d646c6b7217a97156a36b6d40305505d4a5a728082da6fe75ada29f5 \ + --hash=sha256:a73e2ebd29f4ee5d7c754b495d555cd703f2cd26c84abe360ee56411dcd7834d + # via pybind11 +pybind11==3.0.4 \ + --hash=sha256:3286b59c8a774b9ee650169302dd5a4eedc30a8617905a0560dd8ee44775130c \ + --hash=sha256:961720ee652da51d531b7b2451a6bd2bc042b0106e6d9baa48ecb7d58034ce63 # via duckdb -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pyproject-metadata==0.9.1 \ - --hash=sha256:b8b2253dd1b7062b78cf949a115f02ba7fa4114aabe63fa10528e9e1a954a816 \ - --hash=sha256:ee5efde548c3ed9b75a354fc319d5afd25e9585fa918a34f62f904cc731973ad +pyproject-metadata==0.12.1 \ + --hash=sha256:8809a4df6fe08279b39a8890669506ed3158e0617855ac9aff098fcbe772ae4c \ + --hash=sha256:f7162d580a96386a8eb096da06215f981f547d1490f03055ef99e323bc2da427 # via meson-python -scikit-build-core==0.11.6 \ - --hash=sha256:5982ccd839735be99cfd3b92a8847c6c196692f476c215da84b79d2ad12f9f1b \ - --hash=sha256:ce6d8fe64e6b4c759ea0fb95d2f8a68f60d2df31c2989838633b8ec930736360 +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via libcst +scikit-build-core==1.0.3 \ + --hash=sha256:a4d7a05978ee37975c37743510c8991e2debce7ef83afb0a07c0c576fd4f16e8 \ + --hash=sha256:ae95427b7d3c14a6cf8bbfd4d901f6138ab64c99e20cbe8ea7d75cd26093f085 # via + # duckdb # patchelf + # pyarrow # pybind11 + # pybind11-global semantic-version==2.10.0 \ --hash=sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c \ --hash=sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177 # via setuptools-rust -setuptools-rust==1.12.0 \ - --hash=sha256:7e7db90547f224a835b45f5ad90c983340828a345554a9a660bdb2de8605dcdd \ - --hash=sha256:d94a93f0c97751c17014565f07bdc324bee45d396cd1bba83d8e7af92b945f0c - # via maturin -setuptools-scm==7.1.0 \ - --hash=sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27 \ - --hash=sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e - # via python-dateutil -setuptools-scm==8.3.1 \ - --hash=sha256:332ca0d43791b818b841213e76b1971b7711a960761c5bea5fc5cdb5196fbce3 \ - --hash=sha256:3d555e92b75dacd037d32bafdf94f97af51ea29ae8c7b234cf94b7a5bd242a63 +setuptools-git-versioning==3.1.0 \ + --hash=sha256:3a68f3fd58a2a5e86b0792435cfa9d8e569ab60ee5e4c29228c09da9b637bf18 \ + --hash=sha256:612dfcf184addac9e1c2216f4f229724b2390e5bf613fb925ae80b84f2529172 + # via toolz +setuptools-rust==1.13.0 \ + --hash=sha256:666439c4d90e968bb625bcf87ec0aa02b4f49e599d2f5dc255c633a04e8eca99 \ + --hash=sha256:f2afcf4baeee689910ce49cfa8aad4e08cce72f417449bcc32891b8664fdc726 # via - # hatch-vcs - # urllib3 -setuptools-scm==9.2.1 \ - --hash=sha256:2f5b21e45205e47ed3976e58bc29c38f49bb9b6f07c625c7fd2823871694b244 \ - --hash=sha256:84c9b3f39ffb77ba00dd92a06e025e4f66dc69ab8df1c2a225a0678e76311f8d + # libcst + # maturin +setuptools-scm==10.2.1 \ + --hash=sha256:4fa7dd82cf8c800df59c9a288c90299b1657ff1ecfc3f5cc00287c5dbf5e27a9 \ + --hash=sha256:b7c82f4102d389ee57dc66ccdb4f9b4bca3c40ba83b43f1f63d68ccd72db2580 # via # anyio + # cachetools + # dask # duckdb # flit-scm # hatch-vcs - # httpx-sse # importlib-metadata + # libcst # pluggy # pyarrow # pybindgen - # pymilvus # setuptools-rust - # sniffio # sqlglot - # substrait # tabulate # tenacity # tqdm # typeguard - # ujson + # urllib3 # zipp -smmap==5.0.2 \ - --hash=sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5 \ - --hash=sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e - # via gitdb -tomli==2.0.2 \ - --hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \ - --hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed - # via setuptools-scm -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 +setuptools-scm==7.1.0 \ + --hash=sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27 \ + --hash=sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e + # via python-dateutil +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via # fastapi-mcp # flit-scm @@ -654,80 +1042,89 @@ tomli==2.3.0 \ # propcache # psycopg-c # scikit-build-core + # setuptools-git-versioning # setuptools-scm + # vcs-versioning # versioneer # yarl -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via - # poetry-dynamic-versioning - # uv-dynamic-versioning -trove-classifiers==2025.9.11.17 \ - --hash=sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd \ - --hash=sha256:931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 + # via uv-dynamic-versioning +trove-classifiers==2026.6.1.19 \ + --hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \ + --hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745 # via hatchling types-psutil==7.0.0.20250218 \ --hash=sha256:1447a30c282aafefcf8941ece854e1100eee7b0296a9d9be9977292f0269b121 \ --hash=sha256:1e642cdafe837b240295b23b1cbd4691d80b08a07d29932143cbbae30eb0db9c # via mypy -types-setuptools==80.9.0.20250822 \ - --hash=sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965 \ - --hash=sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3 +types-setuptools==83.0.0.20260724 \ + --hash=sha256:eed1a91e1ed9d8ec9f3e71908e5bb17272e41e0852d37d1485ecd9cd33165e1a \ + --hash=sha256:fe2801176b667f1e8209f8571b0839ca6c34318d743825aeacf3f29970c8d46f # via mypy -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via - # exceptiongroup # mypy - # pydantic-core + # scikit-build-core # setuptools-scm -uv-dynamic-versioning==0.11.2 \ - --hash=sha256:4c785ea02e00b93896d015fd3613c451735af1715a7f5f52e8e4e1d76e6ec3c5 \ - --hash=sha256:563ab4b7a93904eb8fb8fd9f2959dad853d62baa71f3cdb0233acf8a749d512f - # via mcp + # vcs-versioning +uv-dynamic-versioning==0.14.0 \ + --hash=sha256:574fbc07e87ace45c01d55967ad3b864871257b98ff5b8ac87c261227ac8db5b \ + --hash=sha256:e087c346a786e98d41292ac2315180fb700cedfb30565fc973d64ce11a112387 + # via + # httpcore2 + # httpx2 + # mcp + # mcp-types +vcs-versioning==2.2.3 \ + --hash=sha256:4f7873af76f5cc24e701608354f376f5eb70b317d663787ace57a7edd7a27506 \ + --hash=sha256:c21e284aa98c8269996791a19d6173baf953cf9996ef42ec3ebcbdc151e18b9b + # via setuptools-scm versioneer==0.29 \ --hash=sha256:0f1a137bb5d6811e96a79bb0486798aeae9b9c6efc24b389659cebb0ee396cb9 \ --hash=sha256:5ab283b9857211d61b53318b7c792cf68e798e765ee17c27ade9f6c924235731 # via - # dask # pandas # partd -wheel==0.45.1 \ - --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ - --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 +wheel==0.47.0 \ + --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \ + --hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3 # via # async-timeout - # cachetools # google-crc32c - # httpx-sse + # grpcio + # grpcio-health-checking + # grpcio-reflection + # grpcio-status + # libcst # meson # mmh3 # pandas # psycopg # psycopg-c # psycopg-pool - # pymilvus + # pycparser # python-dateutil + # setuptools-git-versioning # shellingham # snowflake-connector-python - # tabulate - # tqdm # tzdata # uvloop + # wrapt # The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 # via # aiobotocore # aiohttp # aiosignal # anyio # async-timeout - # backports-datetime-fromisoformat # cachetools # calver # certifi @@ -736,20 +1133,22 @@ setuptools==80.9.0 \ # cryptography # dask # dill - # duckdb # frozenlist - # gitpython # google-api-core # google-cloud-bigquery # google-crc32c # googleapis-common-protos # greenlet # grpc-google-iam-v1 + # grpcio + # grpcio-health-checking + # grpcio-reflection + # grpcio-status # gunicorn # httptools - # httpx-sse - # ibis-substrait # importlib-metadata + # libcst + # librt # markupsafe # maturin # meson @@ -761,40 +1160,50 @@ setuptools==80.9.0 \ # pathspec # pluggy # prometheus-client - # propcache # proto-plus # psutil # psycopg - # psycopg-c # psycopg-pool - # pyarrow # pyasn1 # pyasn1-modules + # pycparser # pyjwt - # pymilvus # pymysql - # python-dateutil + # python-dotenv # pyyaml + # requests + # setuptools-git-versioning # setuptools-rust # setuptools-scm # shellingham - # sniffio + # snowballstemmer # snowflake-connector-python # sqlalchemy + # sqlglot # sse-starlette - # substrait # tabulate # tenacity + # toolz # tqdm # trove-classifiers # typeguard - # types-pymysql - # types-setuptools # tzdata - # ujson # uvloop + # vcs-versioning # versioneer # websockets # wrapt # yarl # zipp +setuptools==80.3.1 \ + --hash=sha256:31e2c58dbb67c99c289f51c16d899afedae292b978f8051efaf6262d8212f927 \ + --hash=sha256:ea8e00d7992054c4c592aeb892f6ad51fe1b4d90cc6947cc45c45717c40ec537 + # via psycopg-c +setuptools==83.0.0 \ + --hash=sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef \ + --hash=sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3 + # via + # propcache + # python-dateutil + # types-pymysql + # types-setuptools diff --git a/sdk/python/requirements/py3.10-minimal-sdist-requirements.txt b/sdk/python/requirements/py3.10-minimal-sdist-requirements.txt index a08c1764e12..b4a45c99dc2 100644 --- a/sdk/python/requirements/py3.10-minimal-sdist-requirements.txt +++ b/sdk/python/requirements/py3.10-minimal-sdist-requirements.txt @@ -1,138 +1,139 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.10 --no-strip-extras setup.py --extra minimal-sdist-build --no-emit-package milvus-lite --generate-hashes --output-file sdk/python/requirements/py3.10-minimal-sdist-requirements.txt -aiobotocore==2.23.1 \ - --hash=sha256:a59f2a78629b97d52f10936b79c73de64e481a8c44a62c1871f088df6c1afc4f \ - --hash=sha256:d81c54d2eae2406ea9a473fea518fed580cf37bc4fc51ce43ba81546e5305114 - # via feast (setup.py) -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +# uv pip compile -p 3.10 --no-strip-extras pyproject.toml --extra minimal-sdist-build --no-emit-package milvus-lite --no-emit-package pymilvus --no-emit-package faiss-cpu --generate-hashes --output-file sdk/python/requirements/py3.10-minimal-sdist-requirements.txt +aiobotocore==3.8.0 \ + --hash=sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670 \ + --hash=sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + # via feast (pyproject.toml) +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.0 \ - --hash=sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6 \ - --hash=sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a \ - --hash=sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e \ - --hash=sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971 \ - --hash=sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54 \ - --hash=sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2 \ - --hash=sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96 \ - --hash=sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469 \ - --hash=sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26 \ - --hash=sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0 \ - --hash=sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29 \ - --hash=sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885 \ - --hash=sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579 \ - --hash=sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a \ - --hash=sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1 \ - --hash=sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b \ - --hash=sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852 \ - --hash=sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865 \ - --hash=sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad \ - --hash=sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092 \ - --hash=sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7 \ - --hash=sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b \ - --hash=sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8 \ - --hash=sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820 \ - --hash=sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd \ - --hash=sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee \ - --hash=sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4 \ - --hash=sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67 \ - --hash=sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d \ - --hash=sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1 \ - --hash=sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d \ - --hash=sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114 \ - --hash=sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1 \ - --hash=sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755 \ - --hash=sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04 \ - --hash=sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca \ - --hash=sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1 \ - --hash=sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae \ - --hash=sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793 \ - --hash=sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e \ - --hash=sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a \ - --hash=sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8 \ - --hash=sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602 \ - --hash=sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778 \ - --hash=sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f \ - --hash=sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49 \ - --hash=sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77 \ - --hash=sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7 \ - --hash=sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f \ - --hash=sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0 \ - --hash=sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519 \ - --hash=sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d \ - --hash=sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370 \ - --hash=sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7 \ - --hash=sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247 \ - --hash=sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d \ - --hash=sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80 \ - --hash=sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9 \ - --hash=sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c \ - --hash=sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d \ - --hash=sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020 \ - --hash=sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504 \ - --hash=sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca \ - --hash=sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731 \ - --hash=sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd \ - --hash=sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6 \ - --hash=sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21 \ - --hash=sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1 \ - --hash=sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620 \ - --hash=sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64 \ - --hash=sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8 \ - --hash=sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3 \ - --hash=sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5 \ - --hash=sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985 \ - --hash=sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813 \ - --hash=sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed \ - --hash=sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20 \ - --hash=sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03 \ - --hash=sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702 \ - --hash=sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6 \ - --hash=sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed \ - --hash=sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353 \ - --hash=sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108 \ - --hash=sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f \ - --hash=sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409 \ - --hash=sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7 \ - --hash=sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3 \ - --hash=sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9 \ - --hash=sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba \ - --hash=sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe \ - --hash=sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c \ - --hash=sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861 \ - --hash=sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a \ - --hash=sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0 \ - --hash=sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390 \ - --hash=sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3 \ - --hash=sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90 \ - --hash=sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802 \ - --hash=sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163 \ - --hash=sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d \ - --hash=sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430 \ - --hash=sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a \ - --hash=sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b \ - --hash=sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6 \ - --hash=sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b \ - --hash=sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657 \ - --hash=sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c \ - --hash=sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93 \ - --hash=sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f \ - --hash=sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82 \ - --hash=sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31 \ - --hash=sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc \ - --hash=sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52 \ - --hash=sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c \ - --hash=sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a \ - --hash=sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52 \ - --hash=sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7 \ - --hash=sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b \ - --hash=sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147 \ - --hash=sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4 - # via aiobotocore -aioitertools==0.12.0 \ - --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \ - --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 + # via + # aiobotocore + # kubernetes +aioitertools==0.13.0 \ + --hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \ + --hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c # via aiobotocore aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ @@ -142,15 +143,22 @@ alabaster==1.0.0 \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b # via sphinx -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via + # fastapi + # typer +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # httpx + # httpx2 # mcp # sse-starlette # starlette @@ -159,372 +167,365 @@ asn1crypto==1.5.1 \ --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 # via snowflake-connector-python +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy async-timeout==5.0.1 \ --hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3 # via # aiohttp # redis -atpublic==5.1 \ - --hash=sha256:135783dbd887fbddb6ef032d104da70c124f2b44b9e2d79df07b9da5334825e3 \ - --hash=sha256:abc1f4b3dbdd841cc3539e4b5e4f3ad41d658359de704e30cb36da4d4e9d3022 +atpublic==7.0.0 \ + --hash=sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e \ + --hash=sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b # via ibis-framework -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via + # feast (pyproject.toml) # aiohttp # jsonschema # referencing -babel==2.17.0 \ - --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ - --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 +babel==2.18.0 \ + --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ + --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 # via sphinx -backports-datetime-fromisoformat==2.0.3 \ - --hash=sha256:1314d4923c1509aa9696712a7bc0c7160d3b7acf72adafbbe6c558d523f5d491 \ - --hash=sha256:1ea2cc84224937d6b9b4c07f5cb7c667f2bde28c255645ba27f8a675a7af8234 \ - --hash=sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45 \ - --hash=sha256:24d574cb4072e1640b00864e94c4c89858033936ece3fc0e1c6f7179f120d0a8 \ - --hash=sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06 \ - --hash=sha256:35a144fd681a0bea1013ccc4cd3fd4dc758ea17ee23dca019c02b82ec46fc0c4 \ - --hash=sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22 \ - --hash=sha256:4024e6d35a9fdc1b3fd6ac7a673bd16cb176c7e0b952af6428b7129a70f72cce \ - --hash=sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0 \ - --hash=sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58 \ - --hash=sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005 \ - --hash=sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7 \ - --hash=sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01 \ - --hash=sha256:5ba00ead8d9d82fd6123eb4891c566d30a293454e54e32ff7ead7644f5f7e575 \ - --hash=sha256:5e2dcc94dc9c9ab8704409d86fcb5236316e9dcef6feed8162287634e3568f4c \ - --hash=sha256:5e410383f5d6a449a529d074e88af8bc80020bb42b402265f9c02c8358c11da5 \ - --hash=sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4 \ - --hash=sha256:61c74710900602637d2d145dda9720c94e303380803bf68811b2a151deec75c2 \ - --hash=sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d \ - --hash=sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc \ - --hash=sha256:63d39709e17eb72685d052ac82acf0763e047f57c86af1b791505b1fec96915d \ - --hash=sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147 \ - --hash=sha256:7100adcda5e818b5a894ad0626e38118bb896a347f40ebed8981155675b9ba7b \ - --hash=sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4 \ - --hash=sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6 \ - --hash=sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4 \ - --hash=sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039 \ - --hash=sha256:9735695a66aad654500b0193525e590c693ab3368478ce07b34b443a1ea5e824 \ - --hash=sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35 \ - --hash=sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf \ - --hash=sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061 \ - --hash=sha256:b2d5117dce805d8a2f78baeddc8c6127281fa0a5e2c40c6dd992ba6b2b367876 \ - --hash=sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d \ - --hash=sha256:b750ecba3a8815ad8bc48311552f3f8ab99dd2326d29df7ff670d9c49321f48f \ - --hash=sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c \ - --hash=sha256:d0a7c5f875068efe106f62233bc712d50db4d07c13c7db570175c7857a7b5dbd \ - --hash=sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d \ - --hash=sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75 \ - --hash=sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443 \ - --hash=sha256:e81b26497a17c29595bc7df20bc6a872ceea5f8c9d6537283945d4b6396aec10 \ - --hash=sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1 \ - --hash=sha256:ece59af54ebf67ecbfbbf3ca9066f5687879e36527ad69d8b6e3ac565d565a62 \ - --hash=sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b \ - --hash=sha256:f2797593760da6bcc32c4a13fa825af183cd4bfd333c60b3dbf84711afca26ef \ - --hash=sha256:fa2de871801d824c255fac7e5e7e50f2be6c9c376fd9268b40c54b5e9da91f42 \ - --hash=sha256:fb35f607bd1cbe37b896379d5f5ed4dc298b536f4b959cb63180e05cacc0539d \ - --hash=sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32 - # via marshmallow -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -boto3==1.38.27 \ - --hash=sha256:94bd7fdd92d5701b362d4df100d21e28f8307a67ff56b6a8b0398119cf22f859 \ - --hash=sha256:95f5fe688795303a8a15e8b7e7f255cadab35eae459d00cc281a4fd77252ea80 - # via - # feast (setup.py) +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +boto3==1.43.46 \ + --hash=sha256:66c0d943b049a46a492ec4ec2ebe73c930b1842c7137bee83aad6d93e95d4d96 \ + --hash=sha256:69453e2c1bcb9fd9806527ab99950cacfc2826cb0dce9a3a0414d19270c06c3c + # via + # feast (pyproject.toml) # snowflake-connector-python -botocore==1.38.46 \ - --hash=sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e \ - --hash=sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b +botocore==1.43.46 \ + --hash=sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533 \ + --hash=sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 # via # aiobotocore # boto3 # s3transfer # snowflake-connector-python -cachetools==6.2.1 \ - --hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \ - --hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201 - # via google-auth +cachetools==7.1.6 \ + --hash=sha256:2c12e255780330af28b91bb7fb96cce4c766f04e38396b9a24510190a5827096 \ + --hash=sha256:c7a79e7f30ba9943c1cefd08cc36f006aaae086e017af9166f1d59d6170c47e1 + # via pymilvus calver==2025.3.31 \ --hash=sha256:07511edf5e7fa75ae97445c8c5921240e0fe62937289a3ebe6963eddd3c691b6 \ --hash=sha256:255d1a70bba8f97dc1eee3af4240ed35980508da69257feef94c79e5c6545fc7 - # via feast (setup.py) -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 + # via feast (pyproject.toml) +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # httpcore # httpx # kubernetes # requests # snowflake-connector-python -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b - # via - # feast (setup.py) +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f + # via + # feast (pyproject.toml) # cryptography - # snowflake-connector-python -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via # requests # snowflake-connector-python -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask - # typer # uvicorn -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a # via dask colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via feast (setup.py) -cryptography==46.0.0 \ - --hash=sha256:07a1be54f995ce14740bf8bbe1cc35f7a37760f992f73cf9f98a2a60b9b97419 \ - --hash=sha256:0f58183453032727a65e6605240e7a3824fd1d6a7e75d2b537e280286ab79a52 \ - --hash=sha256:16b5ac72a965ec9d1e34d9417dbce235d45fa04dac28634384e3ce40dfc66495 \ - --hash=sha256:1b4fba84166d906a22027f0d958e42f3a4dbbb19c28ea71f0fb7812380b04e3c \ - --hash=sha256:1d2073313324226fd846e6b5fc340ed02d43fd7478f584741bd6b791c33c9fee \ - --hash=sha256:249c41f2bbfa026615e7bdca47e4a66135baa81b08509ab240a2e666f6af5966 \ - --hash=sha256:274f8b2eb3616709f437326185eb563eb4e5813d01ebe2029b61bfe7d9995fbb \ - --hash=sha256:2fc30be952dd4334801d345d134c9ef0e9ccbaa8c3e1bc18925cbc4247b3e29c \ - --hash=sha256:32670ca085150ff36b438c17f2dfc54146fe4a074ebf0a76d72fb1b419a974bc \ - --hash=sha256:35aa1a44bd3e0efc3ef09cf924b3a0e2a57eda84074556f4506af2d294076685 \ - --hash=sha256:3738f50215211cee1974193a1809348d33893696ce119968932ea117bcbc9b1d \ - --hash=sha256:378eff89b040cbce6169528f130ee75dceeb97eef396a801daec03b696434f06 \ - --hash=sha256:399ef4c9be67f3902e5ca1d80e64b04498f8b56c19e1bc8d0825050ea5290410 \ - --hash=sha256:40ee4ce3c34acaa5bc347615ec452c74ae8ff7db973a98c97c62293120f668c6 \ - --hash=sha256:4bc257c2d5d865ed37d0bd7c500baa71f939a7952c424f28632298d80ccd5ec1 \ - --hash=sha256:4f70cbade61a16f5e238c4b0eb4e258d177a2fcb59aa0aae1236594f7b0ae338 \ - --hash=sha256:523153480d7575a169933f083eb47b1edd5fef45d87b026737de74ffeb300f69 \ - --hash=sha256:6460866a92143a24e3ed68eaeb6e98d0cedd85d7d9a8ab1fc293ec91850b1b38 \ - --hash=sha256:65e9117ebed5b16b28154ed36b164c20021f3a480e9cbb4b4a2a59b95e74c25d \ - --hash=sha256:6c39fd5cd9b7526afa69d64b5e5645a06e1b904f342584b3885254400b63f1b3 \ - --hash=sha256:6d8945bc120dcd90ae39aa841afddaeafc5f2e832809dc54fb906e3db829dfdc \ - --hash=sha256:75d2ddde8f1766ab2db48ed7f2aa3797aeb491ea8dfe9b4c074201aec00f5c16 \ - --hash=sha256:77e3bd53c9c189cea361bc18ceb173959f8b2dd8f8d984ae118e9ac641410252 \ - --hash=sha256:7f3f88df0c9b248dcc2e76124f9140621aca187ccc396b87bc363f890acf3a30 \ - --hash=sha256:80a548a5862d6912a45557a101092cd6c64ae1475b82cef50ee305d14a75f598 \ - --hash=sha256:834af45296083d892e23430e3b11df77e2ac5c042caede1da29c9bf59016f4d2 \ - --hash=sha256:83af84ebe7b6e9b6de05050c79f8cc0173c864ce747b53abce6a11e940efdc0d \ - --hash=sha256:88c09da8a94ac27798f6b62de6968ac78bb94805b5d272dbcfd5fdc8c566999f \ - --hash=sha256:8e8b222eb54e3e7d3743a7c2b1f7fa7df7a9add790307bb34327c88ec85fe087 \ - --hash=sha256:91585fc9e696abd7b3e48a463a20dda1a5c0eeeca4ba60fa4205a79527694390 \ - --hash=sha256:99f64a6d15f19f3afd78720ad2978f6d8d4c68cd4eb600fab82ab1a7c2071dca \ - --hash=sha256:9aa85222f03fdb30defabc7a9e1e3d4ec76eb74ea9fe1504b2800844f9c98440 \ - --hash=sha256:ab3a14cecc741c8c03ad0ad46dfbf18de25218551931a23bca2731d46c706d83 \ - --hash=sha256:b8e7db4ce0b7297e88f3d02e6ee9a39382e0efaf1e8974ad353120a2b5a57ef7 \ - --hash=sha256:bbaa5eef3c19c66613317dc61e211b48d5f550db009c45e1c28b59d5a9b7812a \ - --hash=sha256:be7479f9504bfb46628544ec7cb4637fe6af8b70445d4455fbb9c395ad9b7290 \ - --hash=sha256:bf1961037309ee0bdf874ccba9820b1c2f720c2016895c44d8eb2316226c1ad5 \ - --hash=sha256:c1f6ccd6f2eef3b2eb52837f0463e853501e45a916b3fc42e5d93cf244a4b97b \ - --hash=sha256:c3648d6a5878fd1c9a22b1d43fa75efc069d5f54de12df95c638ae7ba88701d0 \ - --hash=sha256:c39f0947d50f74b1b3523cec3931315072646286fb462995eb998f8136779319 \ - --hash=sha256:c3cd09b1490c1509bf3892bde9cef729795fae4a2fee0621f19be3321beca7e4 \ - --hash=sha256:c457ad3f151d5fb380be99425b286167b358f76d97ad18b188b68097193ed95a \ - --hash=sha256:c9c4121f9a41cc3d02164541d986f59be31548ad355a5c96ac50703003c50fb7 \ - --hash=sha256:d14eaf1569d6252280516bedaffdd65267428cdbc3a8c2d6de63753cf0863d5e \ - --hash=sha256:d1eccae15d5c28c74b2bea228775c63ac5b6c36eedb574e002440c0bc28750d3 \ - --hash=sha256:d349af4d76a93562f1dce4d983a4a34d01cb22b48635b0d2a0b8372cdb4a8136 \ - --hash=sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0 \ - --hash=sha256:da7f93551d39d462263b6b5c9056c49f780b9200bf9fc2656d7c88c7bdb9b363 \ - --hash=sha256:df932ac70388be034b2e046e34d636245d5eeb8140db24a6b4c2268cd2073270 \ - --hash=sha256:f09a3a108223e319168b7557810596631a8cb864657b0c16ed7a6017f0be9433 \ - --hash=sha256:f85e6a7d42ad60024fa1347b1d4ef82c4df517a4deb7f829d301f1a92ded038c \ - --hash=sha256:f9aaf2a91302e1490c068d2f3af7df4137ac2b36600f5bd26e53d9ec320412d3 \ - --hash=sha256:f9f85d9cf88e3ba2b2b6da3c2310d1cf75bdf04a5bc1a2e972603054f82c4dd5 \ - --hash=sha256:fe9ff1139b2b1f59a5a0b538bbd950f8660a39624bbe10cf3640d17574f973bb - # via + # via feast (pyproject.toml) +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b + # via + # google-auth + # pyjwt # pyopenssl # snowflake-connector-python cython==3.0.12 \ @@ -592,115 +593,90 @@ cython==3.0.12 \ --hash=sha256:fe030d4a00afb2844f5f70896b7f2a1a0d7da09bf3aa3d884cbe5f73fff5d310 \ --hash=sha256:feb86122a823937cc06e4c029d80ff69f082ebb0b959ab52a5af6cdd271c5dc3 \ --hash=sha256:ff5c0b6a65b08117d0534941d404833d516dac422eee88c6b4fd55feb409a5ed - # via feast (setup.py) -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) -db-dtypes==1.4.3 \ - --hash=sha256:a1c92b819af947fae1701d80a71f2a0eac08f825ca52cf0c68aeba80577ae966 \ - --hash=sha256:d3cb08c32939d24e05501f17694be8c1c54cfb4620d61fb56fb311e8c3d8885e + # via feast (pyproject.toml) +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) +db-dtypes==1.7.1 \ + --hash=sha256:450b94cd010cfa6d4d13cca2593009eba4fbe15516084c332cde1a47bbce0625 \ + --hash=sha256:75adf3bdf3174a6474ea4162d9a345ea0b3102ec4fd603e69ec26b6e8a99145a # via # google-cloud-bigquery # pandas-gbq dill==0.3.9 \ --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c - # via feast (setup.py) + # via feast (pyproject.toml) docutils==0.21.2 \ --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # via sphinx -duckdb==1.1.3 \ - --hash=sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586 \ - --hash=sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd \ - --hash=sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e \ - --hash=sha256:09c68522c30fc38fc972b8a75e9201616b96ae6da3444585f14cf0d116008c95 \ - --hash=sha256:0a55169d2d2e2e88077d91d4875104b58de45eff6a17a59c7dc41562c73df4be \ - --hash=sha256:0ba6baa0af33ded836b388b09433a69b8bec00263247f6bf0a05c65c897108d3 \ - --hash=sha256:183ac743f21c6a4d6adfd02b69013d5fd78e5e2cd2b4db023bc8a95457d4bc5d \ - --hash=sha256:1aa3abec8e8995a03ff1a904b0e66282d19919f562dd0a1de02f23169eeec461 \ - --hash=sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce \ - --hash=sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24 \ - --hash=sha256:2141c6b28162199999075d6031b5d63efeb97c1e68fb3d797279d31c65676269 \ - --hash=sha256:252d9b17d354beb9057098d4e5d5698e091a4f4a0d38157daeea5fc0ec161670 \ - --hash=sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9 \ - --hash=sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3 \ - --hash=sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289 \ - --hash=sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1 \ - --hash=sha256:4f0e2e5a6f5a53b79aee20856c027046fba1d73ada6178ed8467f53c3877d5e0 \ - --hash=sha256:51c6d79e05b4a0933672b1cacd6338f882158f45ef9903aef350c4427d9fc898 \ - --hash=sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8 \ - --hash=sha256:5ace6e4b1873afdd38bd6cc8fcf90310fb2d454f29c39a61d0c0cf1a24ad6c8d \ - --hash=sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff \ - --hash=sha256:6411e21a2128d478efbd023f2bdff12464d146f92bc3e9c49247240448ace5a6 \ - --hash=sha256:647f17bd126170d96a38a9a6f25fca47ebb0261e5e44881e3782989033c94686 \ - --hash=sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c \ - --hash=sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31 \ - --hash=sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7 \ - --hash=sha256:80158f4c7c7ada46245837d5b6869a336bbaa28436fbb0537663fa324a2750cd \ - --hash=sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898 \ - --hash=sha256:8ee97ec337794c162c0638dda3b4a30a483d0587deda22d45e1909036ff0b739 \ - --hash=sha256:911d58c22645bfca4a5a049ff53a0afd1537bc18fedb13bc440b2e5af3c46148 \ - --hash=sha256:9c619e4849837c8c83666f2cd5c6c031300cd2601e9564b47aa5de458ff6e69d \ - --hash=sha256:9d0767ada9f06faa5afcf63eb7ba1befaccfbcfdac5ff86f0168c673dd1f47aa \ - --hash=sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4 \ - --hash=sha256:a1f83c7217c188b7ab42e6a0963f42070d9aed114f6200e3c923c8899c090f16 \ - --hash=sha256:a1fa0c502f257fa9caca60b8b1478ec0f3295f34bb2efdc10776fc731b8a6c5f \ - --hash=sha256:a30dd599b8090ea6eafdfb5a9f1b872d78bac318b6914ada2d35c7974d643640 \ - --hash=sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38 \ - --hash=sha256:a4748635875fc3c19a7320a6ae7410f9295557450c0ebab6d6712de12640929a \ - --hash=sha256:b74e121ab65dbec5290f33ca92301e3a4e81797966c8d9feef6efdf05fc6dafd \ - --hash=sha256:c443d3d502335e69fc1e35295fcfd1108f72cb984af54c536adfd7875e79cee5 \ - --hash=sha256:c5336939d83837af52731e02b6a78a446794078590aa71fd400eb17f083dda3e \ - --hash=sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378 \ - --hash=sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9 \ - --hash=sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee \ - --hash=sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32 \ - --hash=sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537 \ - --hash=sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989 \ - --hash=sha256:ecb1dc9062c1cc4d2d88a5e5cd8cc72af7818ab5a3c0f796ef0ffd60cfd3efb4 \ - --hash=sha256:eeacb598120040e9591f5a4edecad7080853aa8ac27e62d280f151f8c862afa3 \ - --hash=sha256:f549af9f7416573ee48db1cf8c9d27aeed245cb015f4b4f975289418c6cf7320 \ - --hash=sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3 \ - --hash=sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b +duckdb==1.5.5 \ + --hash=sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053 \ + --hash=sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4 \ + --hash=sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c \ + --hash=sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a \ + --hash=sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528 \ + --hash=sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c \ + --hash=sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282 \ + --hash=sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f \ + --hash=sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7 \ + --hash=sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f \ + --hash=sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c \ + --hash=sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb \ + --hash=sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433 \ + --hash=sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8 \ + --hash=sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28 \ + --hash=sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238 \ + --hash=sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece \ + --hash=sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1 \ + --hash=sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151 \ + --hash=sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2 \ + --hash=sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353 \ + --hash=sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc \ + --hash=sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079 \ + --hash=sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a \ + --hash=sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6 \ + --hash=sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe \ + --hash=sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de \ + --hash=sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0 \ + --hash=sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e \ + --hash=sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f \ + --hash=sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf \ + --hash=sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae \ + --hash=sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6 \ + --hash=sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe \ + --hash=sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366 # via ibis-framework -dunamai==1.25.0 \ - --hash=sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab \ - --hash=sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1 - # via poetry-dynamic-versioning durationpy==0.10 \ --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 # via kubernetes -environs==9.5.0 \ - --hash=sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124 \ - --hash=sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9 - # via pymilvus -exceptiongroup==1.3.0 \ - --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ - --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 +exceptiongroup==1.3.1 \ + --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ + --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via # anyio # scikit-build-core -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp fastapi-mcp==0.4.0 \ --hash=sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec \ --hash=sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d - # via feast (setup.py) -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 + # via feast (pyproject.toml) +filelock==3.32.0 \ + --hash=sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402 \ + --hash=sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3 # via snowflake-connector-python flit-core==3.12.0 \ --hash=sha256:18f63100d6f94385c6ed57a72073443e1a71a4acb4339491615d0f16d6ff01b2 \ --hash=sha256:e7a0304069ea895172e3c7bb703292e992c5d1555dd1233ab7b5621b5b69e62c - # via feast (setup.py) + # via feast (pyproject.toml) frozenlist==1.8.0 \ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ @@ -835,17 +811,17 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -fsspec==2024.9.0 \ - --hash=sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8 \ - --hash=sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b +fsspec==2026.7.0 \ + --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \ + --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88 # via - # feast (setup.py) + # feast (pyproject.toml) # dask -google-api-core[grpc]==2.26.0 \ - --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ - --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 +google-api-core[grpc]==2.33.0 \ + --hash=sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb \ + --hash=sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc # via - # feast (setup.py) + # feast (pyproject.toml) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -853,9 +829,9 @@ google-api-core[grpc]==2.26.0 \ # google-cloud-datastore # google-cloud-storage # pandas-gbq -google-auth==2.41.1 \ - --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ - --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 +google-auth[pyopenssl]==2.56.2 \ + --hash=sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6 \ + --hash=sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051 # via # google-api-core # google-auth-oauthlib @@ -865,479 +841,617 @@ google-auth==2.41.1 \ # google-cloud-core # google-cloud-datastore # google-cloud-storage - # kubernetes # pandas-gbq # pydata-google-auth -google-auth-oauthlib==1.2.2 \ - --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ - --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-auth-oauthlib==1.4.0 \ + --hash=sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2 \ + --hash=sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 # via # pandas-gbq # pydata-google-auth -google-cloud-bigquery[pandas]==3.38.0 \ - --hash=sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520 \ - --hash=sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6 +google-cloud-bigquery[pandas]==3.42.2 \ + --hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \ + --hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq -google-cloud-bigquery-storage==2.33.1 \ - --hash=sha256:24952aba0d69acc4d6bfbdc7a09dddbb728496b1780bd224f1056361a1b51044 \ - --hash=sha256:3fd25bef364ac5fb9bbd6560f0dd11b90b1845883df8e0a8c706ad53d00fc23b - # via feast (setup.py) -google-cloud-bigtable==2.33.0 \ - --hash=sha256:8d25c73dd41cea9436d14c9f03b91f94b05ff4afd47ce9a575bf18f8e6985747 \ - --hash=sha256:cd4f7178cde30113900318ec7e2fd793116752c97e0922c185c69b645a52656d - # via feast (setup.py) -google-cloud-core==2.4.3 \ - --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ - --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e +google-cloud-bigquery-storage==2.39.0 \ + --hash=sha256:8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 \ + --hash=sha256:d5afd90ad06cf24d9167316cca70ab5b344e880fc13031d7392aa78ee76b8bb6 + # via feast (pyproject.toml) +google-cloud-bigtable==2.41.0 \ + --hash=sha256:36da0fc730952bfe48ae91dee11015459c9046fa1499d0efb73a61bbb6edbe9b \ + --hash=sha256:d0c424bae6238eb16ffc0922be79da00d20d23e0750ea46742ffd4542c50fd26 + # via feast (pyproject.toml) +google-cloud-core==2.6.0 \ + --hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \ + --hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.21.0 \ - --hash=sha256:eee454dd4a55f5b327f9f344928ff1a09a6f77c23d5e3d908ad31a13cc2f4073 \ - --hash=sha256:f303f27cd1983383f20bd227019cd8a7897419e0ec6b878367c58c66245f9d9b - # via feast (setup.py) +google-cloud-datastore==2.26.0 \ + --hash=sha256:1ac4882f033fcb90bd75d1171b46400ecd5def23768598843d9c041e0c0b4a6a \ + --hash=sha256:1bde5694308468da5c932153a6b0331d5b54e1c21de366b6a75ddb0000fd5f4b + # via feast (pyproject.toml) google-cloud-storage==2.19.0 \ --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via feast (setup.py) -google-crc32c==1.7.1 \ - --hash=sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db \ - --hash=sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337 \ - --hash=sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c \ - --hash=sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242 \ - --hash=sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e \ - --hash=sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472 \ - --hash=sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194 \ - --hash=sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3 \ - --hash=sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582 \ - --hash=sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d \ - --hash=sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6 \ - --hash=sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82 \ - --hash=sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06 \ - --hash=sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349 \ - --hash=sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a \ - --hash=sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d \ - --hash=sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48 \ - --hash=sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb \ - --hash=sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315 \ - --hash=sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589 \ - --hash=sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76 \ - --hash=sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65 \ - --hash=sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6 \ - --hash=sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127 \ - --hash=sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53 \ - --hash=sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603 \ - --hash=sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35 \ - --hash=sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9 \ - --hash=sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638 \ - --hash=sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9 \ - --hash=sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77 \ - --hash=sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14 \ - --hash=sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b \ - --hash=sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb + # via feast (pyproject.toml) +google-crc32c==1.8.0 \ + --hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \ + --hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \ + --hash=sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff \ + --hash=sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 \ + --hash=sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411 \ + --hash=sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a \ + --hash=sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 \ + --hash=sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb \ + --hash=sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa \ + --hash=sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962 \ + --hash=sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215 \ + --hash=sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b \ + --hash=sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27 \ + --hash=sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113 \ + --hash=sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f \ + --hash=sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f \ + --hash=sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d \ + --hash=sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 \ + --hash=sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092 \ + --hash=sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7 \ + --hash=sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2 \ + --hash=sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93 \ + --hash=sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8 \ + --hash=sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21 \ + --hash=sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 \ + --hash=sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2 \ + --hash=sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc \ + --hash=sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454 \ + --hash=sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2 \ + --hash=sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733 \ + --hash=sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697 \ + --hash=sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651 \ + --hash=sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c # via # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 +google-resumable-media==2.10.0 \ + --hash=sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c \ + --hash=sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.70.0 \ - --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ - --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +googleapis-common-protos[grpc]==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # grpc-google-iam-v1 # grpcio-status -greenlet==3.2.4 \ - --hash=sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b \ - --hash=sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735 \ - --hash=sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079 \ - --hash=sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d \ - --hash=sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433 \ - --hash=sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58 \ - --hash=sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52 \ - --hash=sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31 \ - --hash=sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246 \ - --hash=sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f \ - --hash=sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671 \ - --hash=sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8 \ - --hash=sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d \ - --hash=sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f \ - --hash=sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0 \ - --hash=sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd \ - --hash=sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337 \ - --hash=sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0 \ - --hash=sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633 \ - --hash=sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b \ - --hash=sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa \ - --hash=sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31 \ - --hash=sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9 \ - --hash=sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b \ - --hash=sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4 \ - --hash=sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc \ - --hash=sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c \ - --hash=sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98 \ - --hash=sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f \ - --hash=sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c \ - --hash=sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590 \ - --hash=sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3 \ - --hash=sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2 \ - --hash=sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9 \ - --hash=sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5 \ - --hash=sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02 \ - --hash=sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0 \ - --hash=sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1 \ - --hash=sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c \ - --hash=sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594 \ - --hash=sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5 \ - --hash=sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d \ - --hash=sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a \ - --hash=sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6 \ - --hash=sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b \ - --hash=sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df \ - --hash=sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945 \ - --hash=sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae \ - --hash=sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb \ - --hash=sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504 \ - --hash=sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb \ - --hash=sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01 \ - --hash=sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c \ - --hash=sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968 - # via feast (setup.py) -grpc-google-iam-v1==0.14.2 \ - --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ - --hash=sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20 +greenlet==3.5.4 \ + --hash=sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20 \ + --hash=sha256:07bd44616608d873d06735b63ef1a88191d6ca57c8d291d6559c71bc14c0893c \ + --hash=sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994 \ + --hash=sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8 \ + --hash=sha256:123aa379c962ed5fe90a880327e0c3066124ac64ec99e12a238be9fd8eb3db3d \ + --hash=sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9 \ + --hash=sha256:13b980043cb1b3134e81ea469da1250ddcc6bfe6d245bbaa59168d9cdc8f228f \ + --hash=sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809 \ + --hash=sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c \ + --hash=sha256:188e4d142f243051d92a1f5c244a741da02dddc070a0620c842804d7b56d008c \ + --hash=sha256:1e1a4a684b16c45ba324e60b32a4386a87722bcb815d2a149d2182f9b401ca72 \ + --hash=sha256:1f17e362d78e37559e0506c5a7d066bdd45073c36a0127a543e8a0df27242ff3 \ + --hash=sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02 \ + --hash=sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c \ + --hash=sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c \ + --hash=sha256:2cdaadc3d31445a8f782bde3cd37e49a2c2a9c6da6daf76a3e34c683b271a3c7 \ + --hash=sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec \ + --hash=sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c \ + --hash=sha256:32802705c2c1ff25e8237b3bdacf2594fa02be80af8a66703eb7853ea7e68686 \ + --hash=sha256:3529a8a933582ad19e224792cac7372489526576b75b4c124e8e4f29948f4861 \ + --hash=sha256:39169a11d87a6a263afda3e9a27d1df16d0f919d40a4837cc73986c9884c0dd8 \ + --hash=sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0 \ + --hash=sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4 \ + --hash=sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9 \ + --hash=sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3 \ + --hash=sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9 \ + --hash=sha256:4ab9f0704bccf6d3b38e0d2130b7b33271cff11453690da074fa280c3aa8e8e7 \ + --hash=sha256:57aa201b351f7c7c75627c60d29e4d5b97a07d37efeb62b903466fca42c097d7 \ + --hash=sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd \ + --hash=sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3 \ + --hash=sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2 \ + --hash=sha256:69173331fbc5d64bfac0065d7e22c39cfcd089e9b18d125bdcd5079363b09616 \ + --hash=sha256:70bdfacdc183dac838b2a0aaff2dd6134a457c52fe68a9c6bbab435483d2b9df \ + --hash=sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf \ + --hash=sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0 \ + --hash=sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a \ + --hash=sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f \ + --hash=sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22 \ + --hash=sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356 \ + --hash=sha256:89f3738167bab8c1084b94e23023d41d247117ac149fa0fbcb5bd4cf6262b353 \ + --hash=sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e \ + --hash=sha256:9667862a2e38ad379f11b845daeda22c8989186def44f06962c9c4c05e556da7 \ + --hash=sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5 \ + --hash=sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8 \ + --hash=sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde \ + --hash=sha256:a2d614cb2372c7101a12ea8b96dd56f81c986d247c5a73db67063f3ed1ca4a52 \ + --hash=sha256:ac5bf81d79d2c8eeb2ef6359b2e1687a1e9ebf46c2b1f970da9a9255df51d190 \ + --hash=sha256:adf2244d7f69409925a8f22ed22cc5f93cdfe5c9dc87ff3476be2c2aaae61a05 \ + --hash=sha256:ae53534b5dec0f4c2ec26f898f538dc8ea1ca3ef2927d597a9439e40a09da937 \ + --hash=sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867 \ + --hash=sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d \ + --hash=sha256:b7c895310363f310361e0fe2072af85269d2a2a285cd04c0c59e79a5e3670dcf \ + --hash=sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f \ + --hash=sha256:bd3d1145f603b2db19feb9078c2e6855eb7c67e15580c010ed815cee519b86fd \ + --hash=sha256:c38c902a0986eba1f6e7ba1ab39ad5195926abde90f3fe080e08212db62176da \ + --hash=sha256:c3fe76c2cac86b4f7a1e92865ac0a54384deb05c92986287c1a7110d9bd53071 \ + --hash=sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88 \ + --hash=sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17 \ + --hash=sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c \ + --hash=sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66 \ + --hash=sha256:cd320d998cbaa032932830448e39abf3c6a12901295e386e8114db926e10cffb \ + --hash=sha256:cef589bc65fae02d10bca2ac341191c5b33acc2967892ebf4fcbd10eabb7a74c \ + --hash=sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25 \ + --hash=sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0 \ + --hash=sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927 \ + --hash=sha256:dc418cf4c873357964d6624445ed09472e50def990c65dd4e76fc3ba8cd9cef6 \ + --hash=sha256:dfc41ae893d9ceaf22c824f2153a88b30651b20e8758c2cd9ac143f23640563c \ + --hash=sha256:e849e6e139b9671adeac505f72fc05f4af7fd1921faef40295e214fc3b361b59 \ + --hash=sha256:e883de250e299654b1f1680f72a1a9f9ba62c9bd1bce84099c90657349a8dfbb \ + --hash=sha256:e9a5e3406e3ed8125ae1a3b37c12f3434e2b1f0fa053197c5557895b4fb09606 \ + --hash=sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef \ + --hash=sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3 \ + --hash=sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da \ + --hash=sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132 \ + --hash=sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7 \ + --hash=sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f \ + --hash=sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2 \ + --hash=sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f \ + --hash=sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667 + # via feast (pyproject.toml) +grpc-google-iam-v1==0.14.4 \ + --hash=sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038 \ + --hash=sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 # via google-cloud-bigtable -grpcio==1.62.3 \ - --hash=sha256:059444f0ed5dba73ab7dd0ee7e8e6b606df4130d2b0a9f010f84da4ab9f6c2d8 \ - --hash=sha256:114f2a865886ff33f85d70670e971fe0e3d252a1209656fefa5470286e3fcc76 \ - --hash=sha256:13571a5b868dcc308a55d36669a2d17d9dcd6ec8335213f6c49cc68da7305abe \ - --hash=sha256:1ac0944e9e3ee3e20825226d1e17985e9f88487055c475986cf0922a7d806d8a \ - --hash=sha256:1de3d04d9a4ec31ebe848ae1fe61e4cbc367fb9495cbf6c54368e60609a998d9 \ - --hash=sha256:216740723fc5971429550c374a0c039723b9d4dcaf7ba05227b7e0a500b06417 \ - --hash=sha256:25cd75dc73c5269932413e517be778640402f18cf9a81147e68645bd8af18ab0 \ - --hash=sha256:325c56ce94d738c31059cf91376f625d3effdff8f85c96660a5fd6395d5a707f \ - --hash=sha256:3737e5ef0aa0fcdfeaf3b4ecc1a6be78b494549b28aec4b7f61b5dc357f7d8be \ - --hash=sha256:377babc817e8b4186aed7ed56e832867c513e4e9b6c3503565c344ffdef440d4 \ - --hash=sha256:3fb7d966a976d762a31346353a19fce4afcffbeda3027dd563bc8cb521fcf799 \ - --hash=sha256:43670a25b752b7ed960fcec3db50ae5886dc0df897269b3f5119cde9b731745f \ - --hash=sha256:4439bbd759636e37b66841117a66444b454937e27f0125205d2d117d7827c643 \ - --hash=sha256:454a6aed4ebd56198d37e1f3be6f1c70838e33dd62d1e2cea12f2bcb08efecc5 \ - --hash=sha256:4c9c1502c76cadbf2e145061b63af077b08d5677afcef91970d6db87b30e2f8b \ - --hash=sha256:4dab8b64c438e19c763a6332b55e5efdbecfb7c55ae59a42c38c81ed27955fa5 \ - --hash=sha256:56757d3e4cf5d4b98a30f2c5456151607261c891fa2298a4554848dcbf83083d \ - --hash=sha256:57823dc7299c4f258ae9c32fd327d29f729d359c34d7612b36e48ed45b3ab8d0 \ - --hash=sha256:582bd03e9c3d1bd1162eb51fa0f1a35633d66e73f4f36702d3b8484a8b45eda7 \ - --hash=sha256:620165df24aae3d5b3e84cb8dd6b98f6ed49aed04126186bbf43061e301d6a21 \ - --hash=sha256:646c14e9f3356d3f34a65b58b0f8d08daa741ba1d4fcd4966b79407543332154 \ - --hash=sha256:668211f3699bbee4deaf1d6e6b8df59328bf63f077bf2dc9b8bfa4a17df4a279 \ - --hash=sha256:6be243f3954b0ca709f56f9cae926c84ac96e1cce19844711e647a1f1db88b99 \ - --hash=sha256:6da20a1ae010a988bc4ed47850f1122de0a88e18cd2f901fcf56007be1fc6c30 \ - --hash=sha256:7349cd7445ac65fbe1b744dcab9cc1ec02dae2256941a2e67895926cbf7422b4 \ - --hash=sha256:74f3fc9b93290e58264844f5bc46df4c58a94c4287a277dbcf75344fc6c37ca4 \ - --hash=sha256:75a4e9ac7ff185cad529f35934c5d711b88aca48b90c70e195f5657da50ce321 \ - --hash=sha256:7b33c1807d4ac564a3027d06f21a2220c116ceacaaef614deb96b3341ee58896 \ - --hash=sha256:807176971c504c598976f5a9ea62363cffbbbb6c7509d9808c2342b020880fa2 \ - --hash=sha256:80a82fdee14dc27e9299248b7aabd5a8739a1cf6b76c78aa2b848158b44a99d5 \ - --hash=sha256:81b7c121c4e52a0749bf0759185b8d5cfa48a786cd7d411cdab08269813e0aab \ - --hash=sha256:8257cc9e55fb0e2149a652d9dc14c023720f9e73c9145776e07c97e0a553922e \ - --hash=sha256:8a5f00b2508937952d23a1767739e95bbbe1120f8a66d10187d5e971d56bb55c \ - --hash=sha256:8ae2e7a390b2cdd2a95d3bf3b3385245eeb48a5e853943cb46139666462c2d1a \ - --hash=sha256:940459d81685549afdfe13a6de102c52ea4cdda093477baa53056884aadf7c48 \ - --hash=sha256:9c4aae4e683776c319169d87e7891b67b75e3f1c0beeb877902ea148b0585164 \ - --hash=sha256:9d5f8e0050a179b3bce9189b522dc91008d44f08c757a7c310e0fd06b4d3d147 \ - --hash=sha256:a1b85d35a7d9638c03321dfe466645b87e23c30df1266f9e04bbb5f44e7579a9 \ - --hash=sha256:a82410d7620c07cb32624e38f2a106980564dfef9dbe78f5b295cda9ef217c03 \ - --hash=sha256:abfe64811177e681edc81d9d9d1bd23edc5f599bd9846650864769264ace30cd \ - --hash=sha256:ac9783d5679c8da612465168c820fd0b916e70ec5496c840bddba0be7f2d124c \ - --hash=sha256:b033d50bd41e506e3b579775f54a30c16c222e0d88847ac8098d2eca2a7454cc \ - --hash=sha256:b097347441b86a8c3ad9579abaf5e5f7f82b1d74a898f47360433b2bca0e4536 \ - --hash=sha256:b708401ede2c4cb8943e8a713988fcfe6cbea105b07cd7fa7c8a9f137c22bddb \ - --hash=sha256:bd900e666bb68fff49703084be14407cd73b8a5752a7590cea98ec22de24fb5d \ - --hash=sha256:c118cfc80e2402a5595be36e9245ffd9b0e146f426cc40bdf60015bf183f8373 \ - --hash=sha256:c175b252d063af388523a397dbe8edbc4319761f5ee892a8a0f5890acc067362 \ - --hash=sha256:c8bb1a7aa82af6c7713cdf9dcb8f4ea1024ac7ce82bb0a0a82a49aea5237da34 \ - --hash=sha256:c95a0b76a44c548e6bd8c5f7dbecf89c77e2e16d3965be817b57769c4a30bea2 \ - --hash=sha256:e202e3f963480ca067a261179b1ac610c0f0272cb4a7942d11b7e2b3fc99c3aa \ - --hash=sha256:e9ffdb7bc9ccd56ec201aec3eab3432e1e820335b5a16ad2b37e094218dcd7a6 \ - --hash=sha256:ea7ca66a58421411c6486fa5015fe7704e2816ff0b4ec4fb779ad5e1cbbdabf3 \ - --hash=sha256:f2ff8ac447765e173842b554b31307b98b3bb1852710903ebb936e7efb7df6e5 \ - --hash=sha256:f5def814c5a4c90c8fe389c526ab881f4a28b7e239b23ed8e02dd02934dfaa1a - # via - # feast (setup.py) +grpcio==1.83.0 \ + --hash=sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df \ + --hash=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 \ + --hash=sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5 \ + --hash=sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df \ + --hash=sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930 \ + --hash=sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf \ + --hash=sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9 \ + --hash=sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33 \ + --hash=sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da \ + --hash=sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03 \ + --hash=sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa \ + --hash=sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223 \ + --hash=sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9 \ + --hash=sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4 \ + --hash=sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf \ + --hash=sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881 \ + --hash=sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af \ + --hash=sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5 \ + --hash=sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0 \ + --hash=sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727 \ + --hash=sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb \ + --hash=sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a \ + --hash=sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735 \ + --hash=sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16 \ + --hash=sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49 \ + --hash=sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1 \ + --hash=sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24 \ + --hash=sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b \ + --hash=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f \ + --hash=sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57 \ + --hash=sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf \ + --hash=sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f \ + --hash=sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c \ + --hash=sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969 \ + --hash=sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd \ + --hash=sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c \ + --hash=sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b \ + --hash=sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61 \ + --hash=sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404 \ + --hash=sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617 \ + --hash=sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40 \ + --hash=sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45 \ + --hash=sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc \ + --hash=sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c \ + --hash=sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745 \ + --hash=sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45 \ + --hash=sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c \ + --hash=sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0 \ + --hash=sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d \ + --hash=sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9 \ + --hash=sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8 + # via + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-datastore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking # grpcio-reflection # grpcio-status + # milvus-lite # pymilvus -grpcio-health-checking==1.62.3 \ - --hash=sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93 \ - --hash=sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d - # via feast (setup.py) -grpcio-reflection==1.62.3 \ - --hash=sha256:a48ef37df81a3bada78261fc92ef382f061112f989d1312398b945cc69838b9c \ - --hash=sha256:cb84682933c400bddf94dd94f928d1c6570f500b6dd255973d4bfb495b82585f - # via feast (setup.py) -grpcio-status==1.62.3 \ - --hash=sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485 \ - --hash=sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8 +grpcio-health-checking==1.83.0 \ + --hash=sha256:7d8b47a5bfbc699d4aee0fc7a27f5d0265eb23a6a64db5ac2d8b749a0f8a9911 \ + --hash=sha256:ad6bc4d5a1103ad704d25ccd82def300dfa27996b185cab3e4cceeef2c6867d4 + # via feast (pyproject.toml) +grpcio-reflection==1.83.0 \ + --hash=sha256:6a2a30a462ac2c3c6d149734a8fe655fa7617c17fa2cda9949006f3bf07f5b3e \ + --hash=sha256:9d4151f68f18a85aa5b0eff5d493ad13f9421f96c8d9c01c840b353dc979be93 + # via feast (pyproject.toml) +grpcio-status==1.83.0 \ + --hash=sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae \ + --hash=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 # via google-api-core -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore + # httpcore2 # uvicorn hatch-fancy-pypi-readme==25.1.0 \ --hash=sha256:9c58ed3dff90d51f43414ce37009ad1d5b0f08ffc9fc216998a06380f01c0045 \ --hash=sha256:ce0134c40d63d874ac48f48ccc678b8f3b62b8e50e9318520d2bffc752eedaf3 - # via feast (setup.py) + # via feast (pyproject.toml) hatch-vcs==0.4.0 \ --hash=sha256:093810748fe01db0d451fabcf2c1ac2688caefd232d4ede967090b1c1b07d9f7 \ --hash=sha256:b8a2b6bee54cf6f9fc93762db73890017ae59c9081d1038a41f16235ceaf8b2c - # via feast (setup.py) -hatchling==1.27.0 \ - --hash=sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6 \ - --hash=sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b + # via feast (pyproject.toml) +hatchling==1.31.0 \ + --hash=sha256:6b48ad4068a482ed7239b3a8215bc55b47aad3345d58dfc94e553c5d2d46211b \ + --hash=sha256:aac80bec8b6fe35e8480f1c335be8910fa210a0e6f735a139be205dadcacb544 # via - # feast (setup.py) + # feast (pyproject.toml) # hatch-fancy-pypi-readme # hatch-vcs -hiredis==2.4.0 \ - --hash=sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027 \ - --hash=sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577 \ - --hash=sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603 \ - --hash=sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869 \ - --hash=sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228 \ - --hash=sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79 \ - --hash=sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb \ - --hash=sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33 \ - --hash=sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77 \ - --hash=sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9 \ - --hash=sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd \ - --hash=sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a \ - --hash=sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8 \ - --hash=sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9 \ - --hash=sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901 \ - --hash=sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7 \ - --hash=sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0 \ - --hash=sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d \ - --hash=sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213 \ - --hash=sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8 \ - --hash=sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389 \ - --hash=sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330 \ - --hash=sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1 \ - --hash=sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2 \ - --hash=sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288 \ - --hash=sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba \ - --hash=sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded \ - --hash=sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca \ - --hash=sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109 \ - --hash=sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8 \ - --hash=sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea \ - --hash=sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6 \ - --hash=sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8 \ - --hash=sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0 \ - --hash=sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5 \ - --hash=sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5 \ - --hash=sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182 \ - --hash=sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73 \ - --hash=sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af \ - --hash=sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac \ - --hash=sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204 \ - --hash=sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277 \ - --hash=sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212 \ - --hash=sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41 \ - --hash=sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e \ - --hash=sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c \ - --hash=sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2 \ - --hash=sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4 \ - --hash=sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348 \ - --hash=sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b \ - --hash=sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e \ - --hash=sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995 \ - --hash=sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371 \ - --hash=sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613 \ - --hash=sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b \ - --hash=sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d \ - --hash=sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708 \ - --hash=sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08 \ - --hash=sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca \ - --hash=sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12 \ - --hash=sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8 \ - --hash=sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829 \ - --hash=sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38 \ - --hash=sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825 \ - --hash=sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67 \ - --hash=sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056 \ - --hash=sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376 \ - --hash=sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214 \ - --hash=sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982 \ - --hash=sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034 \ - --hash=sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1 \ - --hash=sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f \ - --hash=sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9 \ - --hash=sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5 \ - --hash=sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390 \ - --hash=sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0 \ - --hash=sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d \ - --hash=sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57 \ - --hash=sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe \ - --hash=sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2 \ - --hash=sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415 \ - --hash=sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c \ - --hash=sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b \ - --hash=sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853 \ - --hash=sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36 \ - --hash=sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae \ - --hash=sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd \ - --hash=sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187 \ - --hash=sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c \ - --hash=sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542 \ - --hash=sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d \ - --hash=sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79 \ - --hash=sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7 \ - --hash=sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def - # via feast (setup.py) +hiredis==3.4.0 \ + --hash=sha256:02298551060cbfe17b1acb02e9d066eadd51e37357369736f8fc1f5533875255 \ + --hash=sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc \ + --hash=sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865 \ + --hash=sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c \ + --hash=sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec \ + --hash=sha256:06c163607f163a300cba9ef02fd8a234dd644a208bfafe972614f72e5287b9ba \ + --hash=sha256:0731dd9f2bf4d3417947a3cbbca4dd4d9fa6ad7cecfd4e16439c17a4562d6304 \ + --hash=sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419 \ + --hash=sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320 \ + --hash=sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b \ + --hash=sha256:0bcb630add6bc9ea136fce691ddff0c46aa91cb860df4ca789fe44127eb7e90d \ + --hash=sha256:12ea5facb5b08fa23e4c101ec2151f3a3de8ecec412fec58dbde0a6eebca02c7 \ + --hash=sha256:12eca9aea1450d1a85dc15574a985c227e52abbc2b6466f48ad2aa3b82124701 \ + --hash=sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f \ + --hash=sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017 \ + --hash=sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc \ + --hash=sha256:16a3ea8193c5f4ee6e8daafe0126207cef38eec5a923c97047e6985d9bb1b61f \ + --hash=sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4 \ + --hash=sha256:1bfb9ccfb13be63883e5f2e5ff7f6fc87bf256f8243af594257dfbed9dbc3cf0 \ + --hash=sha256:1c9e141b420dae63ea21303a7e8672282326cae67472768e9a3f6e7f0990bd28 \ + --hash=sha256:1d8c21fb85f2a3de9328cf5bace84b63da00028e771b92335ff4fdc00121dd5a \ + --hash=sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5 \ + --hash=sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016 \ + --hash=sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240 \ + --hash=sha256:282c4310af72afbe18b07d416459f4febeaeb805a067a7df790136e0e550fcb2 \ + --hash=sha256:2c432cce38190c3c13b6c28d6840ee85db50765ad510134007f1cfb3844dce73 \ + --hash=sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591 \ + --hash=sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea \ + --hash=sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980 \ + --hash=sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b \ + --hash=sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb \ + --hash=sha256:386b556f48fb0f9f09696b9d37f1cae554123fbd9f091d0ca23087f2aca02887 \ + --hash=sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b \ + --hash=sha256:4190bd07dd7879a8a7ddbb2a4f74d402721f3898276e35beb98851b85b5f539c \ + --hash=sha256:423570ac4d2665c0d55d8707b7859a331e9001da5e437b7b7a23e0acbce770c9 \ + --hash=sha256:43004b0b48abc628dda1ac3ac4871e1326c126f8cd9f11164d61934d827d7a3b \ + --hash=sha256:4404c557fd49bcfe24dff41f1209e4221c76d1607df2fb2dfd39474b5b086dcb \ + --hash=sha256:44660a91e0fbc803c29b337c1a9194c8d7b4cd3a3868d28f747cbec2df165483 \ + --hash=sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582 \ + --hash=sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2 \ + --hash=sha256:45c6c296056641b5df37cedafe7d1553f33bc247e2f81603a4d038b39261879b \ + --hash=sha256:46e041fc7a83eaa989ef5fe09526bc2353a6ba39b4ce5191684eabefc02e17b0 \ + --hash=sha256:4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e \ + --hash=sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d \ + --hash=sha256:4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d \ + --hash=sha256:4e5530d7a6c71c1a9c3b850b08bbc8178ffc9556e3aef7f9c808fe9ff91a6f11 \ + --hash=sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763 \ + --hash=sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584 \ + --hash=sha256:5359caad5b57da0bce11d2880f22617ba3710f0866121a924745447848448034 \ + --hash=sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79 \ + --hash=sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9 \ + --hash=sha256:64cf54c1dad35830bf1d86b343ad3c55aed443af51c5ccead8afd1b547aca196 \ + --hash=sha256:6774f1fe2723001ca0cd42bf5d8b1235301226273915c581c5c1260d4d114c43 \ + --hash=sha256:68c81c04e0d8778c01163dd2bc6f0e0236fc7f650c291526f10d4faccd99e5a0 \ + --hash=sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb \ + --hash=sha256:69d0326f20354ce278cbb86f5ae47cb390e22bb94a66877031038af907c42fa5 \ + --hash=sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22 \ + --hash=sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b \ + --hash=sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9 \ + --hash=sha256:7b159315df71a009375227384aa4219f98c2342ccd8eb33e3f4b58654f426376 \ + --hash=sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203 \ + --hash=sha256:7f7fc1535f6e1a190089eae46dee25f0c6b72bb221d377be07092803b8208733 \ + --hash=sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f \ + --hash=sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13 \ + --hash=sha256:85a9abf2d16b4cb112f6cc6c32236d763b34d21c69b00c2f81a4ff3016fcc1d6 \ + --hash=sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c \ + --hash=sha256:899d5cea72c9a6c2d9b6e7b223de62c9e9f541f48267cc7135764070548735ce \ + --hash=sha256:89e95f50ec4d29645ba2da9010e8717861585dc9f3df354a367f43807a6db3c7 \ + --hash=sha256:8aaaab18314fd25453b5cf59c8cdca4110e419455bcb4c0737d19d4151513e75 \ + --hash=sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1 \ + --hash=sha256:8f2c8569460aa456a294d5e9a3579a382b825dc2e283e3cc398323dcfb6a3dee \ + --hash=sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151 \ + --hash=sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a \ + --hash=sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d \ + --hash=sha256:98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c \ + --hash=sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f \ + --hash=sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d \ + --hash=sha256:a315009b441a0105a373a9a780ebb1c6f7d9ead88ac6ea5f2a15791353c6f590 \ + --hash=sha256:a3796094f616f72976ff51e4dc1a016e753c0f9af5393b2df96920b6bae1e19b \ + --hash=sha256:a427976ba339da624367613d7eba52a1516e6d5c7f8988dc8fb888fbcf52d8b6 \ + --hash=sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb \ + --hash=sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530 \ + --hash=sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4 \ + --hash=sha256:b0c29ac4f50fc61a904acfe76e4ce6bc37e16ab5a98089cb411476d466e1bb36 \ + --hash=sha256:b32afb576705fc5a02440d416ef476ddbcc827a20ff6c4a9d557882bc6b75135 \ + --hash=sha256:b6228afacadb19a858289319d72797023e2cb048f2f930b68b7fe57eaed5fb6b \ + --hash=sha256:b98f44bf873731d9ce99e6b3eeb6196bf7b27693ad9f6de0fba986b2b0845127 \ + --hash=sha256:bb44efa4fa3e3ed7779ad0ade3c08ed5d75ca7a6336893e9a4f2722093b4168a \ + --hash=sha256:bb8b2dd9c00d80ad2870ffbb51ce9af99fb2194c082d7b09f4125d90e21426f6 \ + --hash=sha256:bca175f02a2b0150ffe7f5dc8bf49c798f34d2c7024d17ace0ec97a7583560e3 \ + --hash=sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59 \ + --hash=sha256:c2245c46b4ced5f689469e6dcdfc8a0895bf873840a6600f5ea759cdf1b26a8b \ + --hash=sha256:c8c6337e7ad187e8039d06e4b33ee5148dcd73928819d94087e649eb37316a09 \ + --hash=sha256:ccc5c660e31d788ca534a20f2ccb7a80b946b960e18ed4e1db950fcac122b405 \ + --hash=sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643 \ + --hash=sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c \ + --hash=sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4 \ + --hash=sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d \ + --hash=sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352 \ + --hash=sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81 \ + --hash=sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5 \ + --hash=sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092 \ + --hash=sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4 \ + --hash=sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad \ + --hash=sha256:e29267ecdd08758926f1a9221af2671d90f475480c40aff409921b1f362f1bd5 \ + --hash=sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d \ + --hash=sha256:ed1dba2695f6de009c67d63b39ff978cb43b8a79362f697acedffb7743e50d21 \ + --hash=sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1 \ + --hash=sha256:f3c67f39b112dc35f68d5b59ee111db6121f037d1a60cf3840ecffbb2ec5686b \ + --hash=sha256:f7c7596fbb2b5202e943180353958e89014e763c7f25877a92f70bbde6cd7f19 \ + --hash=sha256:ff28eca77a57751488df90b0f385b472fd78e140a4d45097224648a5737acdf5 \ + --hash=sha256:ff6217da86a63a6a5ea954d9b9ed67916d9d94a566b44aa98e68ae34b40ebbbf + # via feast (pyproject.toml) httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httpcore2==2.9.1 \ + --hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \ + --hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26 + # via httpx2 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - # via - # fastapi-mcp - # mcp -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d + # via fastapi-mcp +httpx2==2.9.1 \ + --hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \ + --hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a # via mcp -ibis-framework[duckdb]==9.5.0 \ - --hash=sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0 \ - --hash=sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84 - # via - # feast (setup.py) - # ibis-substrait -ibis-substrait==4.0.1 \ - --hash=sha256:107ca49383a3cca2fdc88f67ea2f0172620c16fa8f39c9c52305af85dd6180b4 \ - --hash=sha256:614810a173d096fbc49d87a9b419e2162a3c25d8efda1a4d57a389ce56b9041f - # via feast (setup.py) -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +ibis-framework[duckdb]==12.0.0 \ + --hash=sha256:0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 \ + --hash=sha256:238624f2c14fdab8382ca2f4f667c3cdb81e29844cd5f8db8a325d0743767c61 + # via feast (pyproject.toml) +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx + # httpx2 # requests # snowflake-connector-python # yarl -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a +imagesize==2.0.0 \ + --hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \ + --hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3 # via sphinx -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc # via dask jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via - # feast (setup.py) - # poetry-dynamic-versioning + # feast (pyproject.toml) # sphinx -jmespath==1.0.1 \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 # via # aiobotocore # boto3 # botocore -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 - # via - # feast (setup.py) - # mcp +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via mcp jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -kubernetes==33.1.0 \ - --hash=sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5 \ - --hash=sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993 - # via feast (setup.py) +kubernetes==36.0.3 \ + --hash=sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7 \ + --hash=sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f + # via feast (pyproject.toml) +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ --hash=sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 # via partd -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ @@ -1430,14 +1544,14 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -marshmallow==4.0.1 \ - --hash=sha256:72f14ef346f81269dbddee891bac547dda1501e9e08b6a809756ea3dbb7936a1 \ - --hash=sha256:e1d860bd262737cb2d34e1541b84cb52c32c72c9474e3fe6f30f137ef8b0d97f - # via environs -mcp==1.12.4 \ - --hash=sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5 \ - --hash=sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789 +mcp==2.0.0 \ + --hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \ + --hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6 # via fastapi-mcp +mcp-types==2.0.0 \ + --hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \ + --hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379 + # via mcp mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba @@ -1446,325 +1560,318 @@ meson==1.7.1 \ --hash=sha256:155780a5be87f6dd7f427ad8bcbf0f2b2c5f62ee5fdacca7caa9de8439a24b89 \ --hash=sha256:6d9cbc9ce87a70243c75e4cc668ee3f206ab50b184beb0a08ece948112f19bd7 # via - # feast (setup.py) + # feast (pyproject.toml) # meson-python meson-python==0.15.0 \ --hash=sha256:3ae38253ff02b2e947a05e362a2eaf5a9a09d133c5666b4123399ee5fbf2e591 \ --hash=sha256:fddb73eecd49e89c1c41c87937cd89c2d0b65a1c63ba28238681d4bd9484d26f - # via feast (setup.py) -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) -multidict==6.7.0 \ - --hash=sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32 \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ - --hash=sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff \ - --hash=sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8 \ - --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ - --hash=sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 \ - --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ - --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6 \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ - --hash=sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912 \ - --hash=sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c \ - --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6 \ - --hash=sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84 \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ - --hash=sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd \ - --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0 \ - --hash=sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e \ - --hash=sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 \ - --hash=sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc \ - --hash=sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7 \ - --hash=sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4 \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111 \ - --hash=sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e \ - --hash=sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84 \ - --hash=sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851 \ - --hash=sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb \ - --hash=sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ - --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5 \ - --hash=sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb \ - --hash=sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349 \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ - --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ - --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34 \ - --hash=sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85 \ - --hash=sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ - --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ - --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ - --hash=sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91 \ - --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40 \ - --hash=sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73 \ - --hash=sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4 \ - --hash=sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687 \ - --hash=sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 + # via feast (pyproject.toml) +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via feast (pyproject.toml) +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via # aiobotocore # aiohttp # yarl -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mypy==2.3.0 \ + --hash=sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491 \ + --hash=sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762 \ + --hash=sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5 \ + --hash=sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654 \ + --hash=sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd \ + --hash=sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe \ + --hash=sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc \ + --hash=sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3 \ + --hash=sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5 \ + --hash=sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329 \ + --hash=sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b \ + --hash=sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3 \ + --hash=sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7 \ + --hash=sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805 \ + --hash=sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f \ + --hash=sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e \ + --hash=sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7 \ + --hash=sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494 \ + --hash=sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373 \ + --hash=sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88 \ + --hash=sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee \ + --hash=sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2 \ + --hash=sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757 \ + --hash=sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a \ + --hash=sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b \ + --hash=sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461 \ + --hash=sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97 \ + --hash=sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4 \ + --hash=sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117 \ + --hash=sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36 \ + --hash=sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac \ + --hash=sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5 \ + --hash=sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556 \ + --hash=sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c \ + --hash=sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88 \ + --hash=sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568 \ + --hash=sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595 \ + --hash=sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f \ + --hash=sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1 \ + --hash=sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef \ + --hash=sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6 \ + --hash=sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db \ + --hash=sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff \ + --hash=sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60 \ + --hash=sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c # via sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ @@ -1827,37 +1934,116 @@ numpy==2.2.6 \ --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes + # faiss-cpu # ibis-framework + # milvus-lite # pandas # pandas-gbq - # pyarrow oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 - # via - # kubernetes - # requests-oauthlib -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f + # via requests-oauthlib +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef + # via mcp +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via pymilvus +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # dask # db-dtypes - # dunamai + # faiss-cpu # google-cloud-bigquery # gunicorn # hatchling # ibis-framework - # ibis-substrait # pandas-gbq # pyproject-metadata # scikit-build-core # setuptools-scm # snowflake-connector-python # sphinx + # vcs-versioning pandas==2.3.3 \ --hash=sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7 \ --hash=sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593 \ @@ -1915,7 +2101,7 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery @@ -1923,9 +2109,9 @@ pandas==2.3.3 \ # pandas-gbq # pymilvus # snowflake-connector-python -pandas-gbq==0.29.2 \ - --hash=sha256:99f6b642bd8340e96c2acdf7d717108aa0d31c703c2f284817ba7dc3b16110b2 \ - --hash=sha256:a3a28e7de4f8456fc4b8e9b7472b9bc470d2566f89c4cd997599e504f5a0a706 +pandas-gbq==0.35.0 \ + --hash=sha256:258de481019566611031919997bf9c1ece4ca30a4dd02d3fc3664b251d446182 \ + --hash=sha256:596c908487ef0649a161e86ef272c00c267e581b95dc5ee0dc3518545b33bcfc # via google-cloud-bigquery parsy==2.2 \ --hash=sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 \ @@ -1945,180 +2131,170 @@ patchelf==0.17.2.4 \ --hash=sha256:ae44cb3c857d50f54b99e5697aa978726ada33a8a6129d4b8b7ffd28b996652d \ --hash=sha256:d842b51f0401460f3b1f3a3a67d2c266a8f515a5adfbfa6e7b656cb3ac2ed8bc \ --hash=sha256:d9b35ebfada70c02679ad036407d9724ffe1255122ba4ac5e4be5868618a5689 - # via feast (setup.py) -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # via feast (pyproject.toml) +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via # hatchling # mypy # scikit-build-core -platformdirs==4.5.0 \ - --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ - --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 # via snowflake-connector-python pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via hatchling -poetry-core==1.9.1 \ - --hash=sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0 \ - --hash=sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8 - # via feast (setup.py) -poetry-dynamic-versioning==1.9.1 \ - --hash=sha256:65a0c814e6d30d4807734a3c34edf261fd7cc3b340dbd23b6a33ee41f7d0b547 \ - --hash=sha256:d6e7b9df817aa2ca4946cd695c6c89e1379d2e6c640f008a9b6170d081a9da48 - # via feast (setup.py) -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via feast (setup.py) -propcache==0.4.1 \ - --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ - --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ - --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ - --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ - --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ - --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ - --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ - --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ - --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ - --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via feast (pyproject.toml) +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274 \ + --hash=sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.26.1 \ - --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ - --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +proto-plus==1.28.2 \ + --hash=sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501 \ + --hash=sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52 # via # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==6.32.1 \ - --hash=sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346 \ - --hash=sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4 \ - --hash=sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1 \ - --hash=sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085 \ - --hash=sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1 \ - --hash=sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710 \ - --hash=sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122 \ - --hash=sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281 \ - --hash=sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d - # via - # feast (setup.py) +protobuf==7.35.1 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a + # via + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -2130,84 +2306,103 @@ protobuf==6.32.1 \ # grpcio-status # proto-plus # pymilvus - # substrait -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 - # via feast (setup.py) -psycopg[c, pool]==3.2.5 \ - --hash=sha256:b782130983e5b3de30b4c529623d3687033b4dafa05bb661fc6bf45837ca5879 \ - --hash=sha256:f5f750611c67cb200e85b408882f29265c66d1de7f813add4f8125978bfd70e8 - # via feast (setup.py) -psycopg-c==3.2.5 \ - --hash=sha256:57ad4cfd28de278c424aaceb1f2ad5c7910466e315dfe84e403f3c7a0a2ce81b +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # feast (pyproject.toml) + # pandas-gbq +psycopg[c, pool]==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via feast (pyproject.toml) +psycopg-c==3.3.4 \ + --hash=sha256:ed8106128b2d04359c185fc9641b4409abfce4d0b6fb1d1ff6800646e27f1a22 # via psycopg -psycopg-pool==3.2.6 \ - --hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \ - --hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7 +psycopg-pool==3.3.1 \ + --hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \ + --hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c # via psycopg -pyarrow==17.0.0 \ - --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ - --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ - --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ - --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ - --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ - --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ - --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ - --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ - --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ - --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ - --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ - --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ - --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ - --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ - --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ - --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ - --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ - --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ - --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ - --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ - --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ - --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ - --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ - --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ - --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ - --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ - --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ - --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ - --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ - --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ - --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ - --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ - --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ - --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ - --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ - --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 - # via - # feast (setup.py) +pyarrow==25.0.0 \ + --hash=sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be \ + --hash=sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc \ + --hash=sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec \ + --hash=sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849 \ + --hash=sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e \ + --hash=sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8 \ + --hash=sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d \ + --hash=sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537 \ + --hash=sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062 \ + --hash=sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e \ + --hash=sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104 \ + --hash=sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517 \ + --hash=sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e \ + --hash=sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5 \ + --hash=sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887 \ + --hash=sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1 \ + --hash=sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b \ + --hash=sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be \ + --hash=sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778 \ + --hash=sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e \ + --hash=sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194 \ + --hash=sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2 \ + --hash=sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9 \ + --hash=sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b \ + --hash=sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18 \ + --hash=sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7 \ + --hash=sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc \ + --hash=sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62 \ + --hash=sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0 \ + --hash=sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f \ + --hash=sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e \ + --hash=sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862 \ + --hash=sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50 \ + --hash=sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe \ + --hash=sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3 \ + --hash=sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a \ + --hash=sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4 \ + --hash=sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af \ + --hash=sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b \ + --hash=sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0 \ + --hash=sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f \ + --hash=sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec \ + --hash=sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f + # via + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery # ibis-framework + # milvus-lite # pandas-gbq # snowflake-connector-python pyarrow-hotfix==0.7 \ --hash=sha256:3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 \ --hash=sha256:59399cd58bdd978b2e42816a4183a55c6472d4e33d183351b6069f11ed42661d # via ibis-framework -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 @@ -2215,160 +2410,176 @@ pyasn1-modules==0.4.2 \ pybindgen==0.22.0 \ --hash=sha256:21612f4806a2af25a175716d7e694563af7e10c704538a350cb595d187952f6f \ --hash=sha256:5b4837d3138ac56863d93fe462f1dac39fb87bd50898e0da4c57fefd645437ac - # via feast (setup.py) -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 + # via feast (pyproject.toml) +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi # fastapi-mcp # mcp + # mcp-types # pydantic-settings -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pydantic-settings==2.11.0 \ - --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ - --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c - # via - # fastapi-mcp - # mcp +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f + # via fastapi-mcp pydata-google-auth==1.9.1 \ --hash=sha256:0a51ce41c601ca0bc69b8795bf58bedff74b4a6a007c9106c7cbcdec00eaced2 \ --hash=sha256:75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 # via pandas-gbq -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via - # feast (setup.py) + # feast (pyproject.toml) # rich # sphinx -pyjwt==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via - # feast (setup.py) + # feast (pyproject.toml) + # mcp # snowflake-connector-python -pymilvus==2.4.9 \ - --hash=sha256:0937663700007c23a84cfc0656160b301f6ff9247aaec4c96d599a6b43572136 \ - --hash=sha256:45313607d2c164064bdc44e0f933cb6d6afa92e9efcc7f357c5240c57db58fbe - # via feast (setup.py) -pymysql==1.1.2 \ - --hash=sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03 \ - --hash=sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 - # via feast (setup.py) -pyopenssl==25.3.0 \ - --hash=sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6 \ - --hash=sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329 +pymysql==1.2.0 \ + --hash=sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 \ + --hash=sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33 + # via feast (pyproject.toml) +pyopenssl==26.3.0 \ + --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ + --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 # via snowflake-connector-python -pyproject-metadata==0.9.1 \ - --hash=sha256:b8b2253dd1b7062b78cf949a115f02ba7fa4114aabe63fa10528e9e1a954a816 \ - --hash=sha256:ee5efde548c3ed9b75a354fc319d5afd25e9585fa918a34f62f904cc731973ad +pyproject-metadata==0.12.1 \ + --hash=sha256:8809a4df6fe08279b39a8890669506ed3158e0617855ac9aff098fcbe772ae4c \ + --hash=sha256:f7162d580a96386a8eb096da06215f981f547d1490f03055ef99e323bc2da427 # via meson-python python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ @@ -2380,22 +2591,21 @@ python-dateutil==2.9.0.post0 \ # ibis-framework # kubernetes # pandas -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via - # environs # pydantic-settings + # pymilvus # uvicorn -python-multipart==0.0.20 \ - --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \ - --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13 +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 # via mcp -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via - # ibis-framework # pandas # snowflake-connector-python pyyaml==6.0.3 \ @@ -2473,31 +2683,31 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # dask - # ibis-substrait # kubernetes # uvicorn -redis==4.6.0 \ - --hash=sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d \ - --hash=sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c - # via feast (setup.py) +redis==7.4.1 \ + --hash=sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e \ + --hash=sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + # via feast (pyproject.toml) referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 # via # jsonschema # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # google-api-core # google-cloud-bigquery # google-cloud-storage # kubernetes + # pymilvus # requests-oauthlib # snowflake-connector-python # sphinx @@ -2507,196 +2717,151 @@ requests-oauthlib==2.0.0 \ # via # google-auth-oauthlib # kubernetes -rich==13.9.4 \ - --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ - --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via # fastapi-mcp # ibis-framework # typer -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==0.30.0 \ + --hash=sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f \ + --hash=sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136 \ + --hash=sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3 \ + --hash=sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7 \ + --hash=sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65 \ + --hash=sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4 \ + --hash=sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169 \ + --hash=sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf \ + --hash=sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4 \ + --hash=sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2 \ + --hash=sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c \ + --hash=sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4 \ + --hash=sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3 \ + --hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \ + --hash=sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7 \ + --hash=sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89 \ + --hash=sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85 \ + --hash=sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6 \ + --hash=sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa \ + --hash=sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb \ + --hash=sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6 \ + --hash=sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87 \ + --hash=sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856 \ + --hash=sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4 \ + --hash=sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f \ + --hash=sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53 \ + --hash=sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229 \ + --hash=sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad \ + --hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \ + --hash=sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db \ + --hash=sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038 \ + --hash=sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27 \ + --hash=sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00 \ + --hash=sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18 \ + --hash=sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083 \ + --hash=sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c \ + --hash=sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738 \ + --hash=sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898 \ + --hash=sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e \ + --hash=sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7 \ + --hash=sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08 \ + --hash=sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6 \ + --hash=sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551 \ + --hash=sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e \ + --hash=sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288 \ + --hash=sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df \ + --hash=sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0 \ + --hash=sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2 \ + --hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \ + --hash=sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0 \ + --hash=sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464 \ + --hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \ + --hash=sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404 \ + --hash=sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7 \ + --hash=sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139 \ + --hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \ + --hash=sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb \ + --hash=sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15 \ + --hash=sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff \ + --hash=sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed \ + --hash=sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6 \ + --hash=sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e \ + --hash=sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95 \ + --hash=sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d \ + --hash=sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950 \ + --hash=sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3 \ + --hash=sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5 \ + --hash=sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97 \ + --hash=sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e \ + --hash=sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e \ + --hash=sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b \ + --hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \ + --hash=sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad \ + --hash=sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8 \ + --hash=sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425 \ + --hash=sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221 \ + --hash=sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d \ + --hash=sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825 \ + --hash=sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51 \ + --hash=sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e \ + --hash=sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f \ + --hash=sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8 \ + --hash=sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f \ + --hash=sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d \ + --hash=sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07 \ + --hash=sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877 \ + --hash=sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31 \ + --hash=sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58 \ + --hash=sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94 \ + --hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \ + --hash=sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000 \ + --hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \ + --hash=sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1 \ + --hash=sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7 \ + --hash=sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7 \ + --hash=sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40 \ + --hash=sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d \ + --hash=sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0 \ + --hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \ + --hash=sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f \ + --hash=sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a \ + --hash=sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7 \ + --hash=sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419 \ + --hash=sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 \ + --hash=sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a \ + --hash=sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9 \ + --hash=sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be \ + --hash=sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed \ + --hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a \ + --hash=sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d \ + --hash=sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324 \ + --hash=sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f \ + --hash=sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2 \ + --hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f \ + --hash=sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5 # via # jsonschema # referencing -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via google-auth -s3transfer==0.13.1 \ - --hash=sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724 \ - --hash=sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf +s3transfer==0.19.2 \ + --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \ + --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25 # via boto3 -scikit-build-core==0.11.6 \ - --hash=sha256:5982ccd839735be99cfd3b92a8847c6c196692f476c215da84b79d2ad12f9f1b \ - --hash=sha256:ce6d8fe64e6b4c759ea0fb95d2f8a68f60d2df31c2989838633b8ec930736360 - # via feast (setup.py) -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c - # via - # feast (setup.py) +scikit-build-core==1.0.3 \ + --hash=sha256:a4d7a05978ee37975c37743510c8991e2debce7ef83afb0a07c0c576fd4f16e8 \ + --hash=sha256:ae95427b7d3c14a6cf8bbfd4d901f6138ab64c99e20cbe8ea7d75cd26093f085 + # via feast (pyproject.toml) +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 + # via + # feast (pyproject.toml) # pandas-gbq # pydata-google-auth - # pymilvus # setuptools-scm -setuptools-scm==9.2.1 \ - --hash=sha256:2f5b21e45205e47ed3976e58bc29c38f49bb9b6f07c625c7fd2823871694b244 \ - --hash=sha256:84c9b3f39ffb77ba00dd92a06e025e4f66dc69ab8df1c2a225a0678e76311f8d +setuptools-scm==10.2.1 \ + --hash=sha256:4fa7dd82cf8c800df59c9a288c90299b1657ff1ecfc3f5cc00287c5dbf5e27a9 \ + --hash=sha256:b7c82f4102d389ee57dc66ccdb4f9b4bca3c40ba83b43f1f63d68ccd72db2580 # via hatch-vcs shellingham==1.5.4 \ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ @@ -2708,42 +2873,38 @@ six==1.17.0 \ # via # kubernetes # python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio -snowballstemmer==3.0.1 \ - --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ - --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 +snowballstemmer==3.1.1 \ + --hash=sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 \ + --hash=sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260 # via sphinx -snowflake-connector-python[pandas]==3.18.0 \ - --hash=sha256:0af10b207af3d2de2b130e89018d49a60f2e5cfe841f3bf459e58f2e1c4c4506 \ - --hash=sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628 \ - --hash=sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4 \ - --hash=sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e \ - --hash=sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee \ - --hash=sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9 \ - --hash=sha256:32b1abfea32561d817b0a2f80b06d936cb32712af06bf7b848a428bfd857a10a \ - --hash=sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a \ - --hash=sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728 \ - --hash=sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800 \ - --hash=sha256:4ed2d593f1983939d5d8d88b212d86fd4f14f0ceefc1df9882b4a18534adbde9 \ - --hash=sha256:51eb789a09dc6c62119cfabd044fba1a6b8378206f05a1e83ddb2e9cb49acc0b \ - --hash=sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc \ - --hash=sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb \ - --hash=sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba \ - --hash=sha256:783a9ab206563d7b52fdcdd7a72af62de811d3381ca64132fd3445537b4d041b \ - --hash=sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f \ - --hash=sha256:8d3e96e1d09b07edca6c1f6ca675b6fdd05a4a7e428e4cdf6fb697d87b9f60fc \ - --hash=sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a \ - --hash=sha256:a8c570edff5a4888840dbe1e9e65c5e4d77d55c5c800cd359fe0903a769201e0 \ - --hash=sha256:aeeb181a156333480f60b5f8ddbb3d087e288b4509adbef7993236defe4d7570 \ - --hash=sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca \ - --hash=sha256:b99f261c82be92224ac20c8c12bdf26ce3ed5dfd8a3df8a97f15a1e11c46ad27 \ - --hash=sha256:bd1de3038b6d7059ca59f93e105aba2a673151c693cc4292f72f38bfaf147df2 \ - --hash=sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e \ - --hash=sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00 - # via feast (setup.py) +snowflake-connector-python[pandas]==4.7.1 \ + --hash=sha256:051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117 \ + --hash=sha256:052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7 \ + --hash=sha256:0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317 \ + --hash=sha256:167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf \ + --hash=sha256:171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a \ + --hash=sha256:1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6 \ + --hash=sha256:22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf \ + --hash=sha256:2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b \ + --hash=sha256:4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a \ + --hash=sha256:4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754 \ + --hash=sha256:5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9 \ + --hash=sha256:571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d \ + --hash=sha256:5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35 \ + --hash=sha256:7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0 \ + --hash=sha256:7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5 \ + --hash=sha256:81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57 \ + --hash=sha256:8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a \ + --hash=sha256:92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3 \ + --hash=sha256:b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45 \ + --hash=sha256:d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa \ + --hash=sha256:e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327 \ + --hash=sha256:f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c \ + --hash=sha256:f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7 \ + --hash=sha256:f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52 \ + --hash=sha256:fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1 \ + --hash=sha256:ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b + # via feast (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 @@ -2751,7 +2912,7 @@ sortedcontainers==2.4.0 \ sphinx==8.1.3 \ --hash=sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2 \ --hash=sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927 - # via feast (setup.py) + # via feast (pyproject.toml) sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 @@ -2776,767 +2937,745 @@ sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via sphinx -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -sqlglot==25.20.2 \ - --hash=sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab \ - --hash=sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567 +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via feast (pyproject.toml) +sqlglot==30.14.0 \ + --hash=sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9 \ + --hash=sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636 # via ibis-framework -sse-starlette==3.0.2 \ - --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ - --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 \ + --hash=sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627 # via mcp -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via + # feast (pyproject.toml) # fastapi # mcp -substrait==0.24.2 \ - --hash=sha256:743cc352e96b0927b2cd37cd5a8fdac0a96a68df9600bd104fc36aebd222a836 \ - --hash=sha256:d1d475833566fa9d67eed3273456883c0568486ccced92b524b31709d2817e19 - # via ibis-substrait -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via feast (setup.py) + # sse-starlette +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via feast (pyproject.toml) tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via feast (pyproject.toml) toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via # fastapi-mcp # hatch-fancy-pypi-readme # hatchling # meson-python + # milvus-lite # mypy # scikit-build-core # setuptools-scm # sphinx -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via - # poetry-dynamic-versioning - # snowflake-connector-python -toolz==0.12.1 \ - --hash=sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85 \ - --hash=sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d + # vcs-versioning +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 + # via snowflake-connector-python +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # dask # ibis-framework # partd -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 - # via - # feast (setup.py) - # milvus-lite -trove-classifiers==2025.9.11.17 \ - --hash=sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd \ - --hash=sha256:931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via feast (pyproject.toml) +trove-classifiers==2026.6.1.19 \ + --hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \ + --hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745 # via hatchling -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) -typer==0.19.2 \ - --hash=sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9 \ - --hash=sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca +truststore==0.10.4 \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 + # via + # httpcore2 + # httpx2 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) +typer==0.27.0 \ + --hash=sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5 \ + --hash=sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1 # via fastapi-mcp types-psutil==7.0.0.20250218 \ --hash=sha256:1447a30c282aafefcf8941ece854e1100eee7b0296a9d9be9977292f0269b121 \ --hash=sha256:1e642cdafe837b240295b23b1cbd4691d80b08a07d29932143cbbae30eb0db9c - # via feast (setup.py) -types-pymysql==1.1.0.20250916 \ - --hash=sha256:873eb9836bb5e3de4368cc7010ca72775f86e9692a5c7810f8c7f48da082e55b \ - --hash=sha256:98d75731795fcc06723a192786662bdfa760e1e00f22809c104fbb47bac5e29b - # via feast (setup.py) -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via feast (pyproject.toml) +types-pymysql==1.2.0.20260724 \ + --hash=sha256:71327a3fea0f680a280445f1b475c2307f68de05c2d3302a79df56af93c574cd \ + --hash=sha256:8493cf87e8df0a7b4e8c84305bc0aae69d3a8af9c6da7bfbb9a4c5c3f97d4a82 + # via feast (pyproject.toml) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiobotocore + # aiohttp # aiosignal # anyio # cryptography # exceptiongroup # fastapi + # grpcio + # httpx2 # ibis-framework - # marshmallow + # mcp + # mcp-types # multidict # mypy + # opentelemetry-api # psycopg # psycopg-pool # pydantic # pydantic-core + # pyjwt # pyopenssl # referencing - # rich + # scikit-build-core + # setuptools-scm # snowflake-connector-python # sqlalchemy # starlette # typeguard - # typer # typing-inspection # uvicorn + # vcs-versioning typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via pydantic-settings -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via pandas -ujson==5.11.0 \ - --hash=sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80 \ - --hash=sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef \ - --hash=sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a \ - --hash=sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840 \ - --hash=sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53 \ - --hash=sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076 \ - --hash=sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab \ - --hash=sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d \ - --hash=sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89 \ - --hash=sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c \ - --hash=sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb \ - --hash=sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c \ - --hash=sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823 \ - --hash=sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5 \ - --hash=sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac \ - --hash=sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d \ - --hash=sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723 \ - --hash=sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6 \ - --hash=sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0 \ - --hash=sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328 \ - --hash=sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0 \ - --hash=sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04 \ - --hash=sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25 \ - --hash=sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49 \ - --hash=sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec \ - --hash=sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3 \ - --hash=sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3 \ - --hash=sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc \ - --hash=sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a \ - --hash=sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9 \ - --hash=sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302 \ - --hash=sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694 \ - --hash=sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547 \ - --hash=sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01 \ - --hash=sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02 \ - --hash=sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6 \ - --hash=sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60 \ - --hash=sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915 \ - --hash=sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835 \ - --hash=sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701 \ - --hash=sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702 \ - --hash=sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50 \ - --hash=sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6 \ - --hash=sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc \ - --hash=sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a \ - --hash=sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c \ - --hash=sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c \ - --hash=sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03 \ - --hash=sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629 \ - --hash=sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105 \ - --hash=sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844 \ - --hash=sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241 \ - --hash=sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a \ - --hash=sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26 \ - --hash=sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac \ - --hash=sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596 \ - --hash=sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9 \ - --hash=sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752 \ - --hash=sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138 \ - --hash=sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b \ - --hash=sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba \ - --hash=sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5 \ - --hash=sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018 \ - --hash=sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362 \ - --hash=sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746 \ - --hash=sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f \ - --hash=sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88 \ - --hash=sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638 \ - --hash=sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b \ - --hash=sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9 \ - --hash=sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db \ - --hash=sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f \ - --hash=sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58 \ - --hash=sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b \ - --hash=sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433 \ - --hash=sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0 \ - --hash=sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764 \ - --hash=sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c \ - --hash=sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34 \ - --hash=sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052 \ - --hash=sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba \ - --hash=sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c \ - --hash=sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc \ - --hash=sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39 - # via pymilvus -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via + # ibis-framework + # pandas +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via # botocore # kubernetes # requests -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # mcp # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +vcs-versioning==2.2.3 \ + --hash=sha256:4f7873af76f5cc24e701608354f376f5eb70b317d663787ace57a7edd7a27506 \ + --hash=sha256:c21e284aa98c8269996791a19d6173baf953cf9996ef42ec3ebcbdc151e18b9b + # via setuptools-scm +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn websocket-client==1.9.0 \ --hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \ --hash=sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef # via kubernetes -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 # via aiobotocore -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8 \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf \ - --hash=sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890 \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737 \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba \ - --hash=sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9 \ - --hash=sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0 \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147 \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e \ - --hash=sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2 \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6 \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750 \ + --hash=sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621 \ + --hash=sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13 \ + --hash=sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1 \ + --hash=sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2 \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata # The following packages were excluded from the output: # milvus-lite +# pymilvus +# faiss-cpu diff --git a/sdk/python/requirements/py3.10-requirements.txt b/sdk/python/requirements/py3.10-requirements.txt index 34753401037..ed4c78340eb 100644 --- a/sdk/python/requirements/py3.10-requirements.txt +++ b/sdk/python/requirements/py3.10-requirements.txt @@ -1,256 +1,367 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.10 --no-strip-extras setup.py --generate-hashes --output-file sdk/python/requirements/py3.10-requirements.txt -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +# uv pip compile -p 3.10 --no-strip-extras pyproject.toml --generate-hashes --output-file sdk/python/requirements/py3.10-requirements.txt +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via fastapi +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # starlette # watchfiles -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 - # via - # jsonschema - # referencing -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via feast (pyproject.toml) +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via requests -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via requests -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # uvicorn -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a # via dask colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via feast (setup.py) -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) + # via feast (pyproject.toml) +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) dill==0.3.9 \ --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c - # via feast (setup.py) -exceptiongroup==1.3.0 \ - --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ - --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 + # via feast (pyproject.toml) +exceptiongroup==1.3.1 \ + --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ + --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via anyio -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 - # via feast (setup.py) -fsspec==2025.9.0 \ - --hash=sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19 \ - --hash=sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7 +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e + # via feast (pyproject.toml) +fsspec==2026.7.0 \ + --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \ + --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88 # via dask -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via uvicorn -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # requests -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc # via dask jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via feast (setup.py) -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 - # via feast (setup.py) -jsonschema-specifications==2025.9.1 \ - --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ - --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d - # via jsonschema + # via feast (pyproject.toml) +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ --hash=sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 @@ -346,168 +457,161 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via feast (pyproject.toml) +mypy==2.3.0 \ + --hash=sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491 \ + --hash=sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762 \ + --hash=sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5 \ + --hash=sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654 \ + --hash=sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd \ + --hash=sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe \ + --hash=sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc \ + --hash=sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3 \ + --hash=sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5 \ + --hash=sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329 \ + --hash=sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b \ + --hash=sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3 \ + --hash=sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7 \ + --hash=sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805 \ + --hash=sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f \ + --hash=sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e \ + --hash=sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7 \ + --hash=sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494 \ + --hash=sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373 \ + --hash=sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88 \ + --hash=sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee \ + --hash=sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2 \ + --hash=sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757 \ + --hash=sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a \ + --hash=sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b \ + --hash=sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461 \ + --hash=sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97 \ + --hash=sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4 \ + --hash=sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117 \ + --hash=sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36 \ + --hash=sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac \ + --hash=sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5 \ + --hash=sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556 \ + --hash=sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c \ + --hash=sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88 \ + --hash=sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568 \ + --hash=sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595 \ + --hash=sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f \ + --hash=sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1 \ + --hash=sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef \ + --hash=sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6 \ + --hash=sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db \ + --hash=sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff \ + --hash=sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60 \ + --hash=sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c # via sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ @@ -570,12 +674,12 @@ numpy==2.2.6 \ --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \ --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # pandas -packaging==25.0 \ - --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ - --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # dask # gunicorn @@ -636,216 +740,247 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # dask partd==1.4.2 \ --hash=sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f \ --hash=sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c # via dask -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via mypy -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via feast (setup.py) -protobuf==6.32.1 \ - --hash=sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346 \ - --hash=sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4 \ - --hash=sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1 \ - --hash=sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085 \ - --hash=sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1 \ - --hash=sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710 \ - --hash=sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122 \ - --hash=sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281 \ - --hash=sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d - # via feast (setup.py) -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 - # via feast (setup.py) -pyarrow==21.0.0 \ - --hash=sha256:067c66ca29aaedae08218569a114e413b26e742171f526e828e1064fcdec13f4 \ - --hash=sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623 \ - --hash=sha256:0c4e75d13eb76295a49e0ea056eb18dbd87d81450bfeb8afa19a7e5a75ae2ad7 \ - --hash=sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636 \ - --hash=sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7 \ - --hash=sha256:203003786c9fd253ebcafa44b03c06983c9c8d06c3145e37f1b76a1f317aeae1 \ - --hash=sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10 \ - --hash=sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51 \ - --hash=sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd \ - --hash=sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8 \ - --hash=sha256:3b4d97e297741796fead24867a8dabf86c87e4584ccc03167e4a811f50fdf74d \ - --hash=sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569 \ - --hash=sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e \ - --hash=sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc \ - --hash=sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6 \ - --hash=sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c \ - --hash=sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82 \ - --hash=sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79 \ - --hash=sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6 \ - --hash=sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10 \ - --hash=sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61 \ - --hash=sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d \ - --hash=sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb \ - --hash=sha256:898afce396b80fdda05e3086b4256f8677c671f7b1d27a6976fa011d3fd0a86e \ - --hash=sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e \ - --hash=sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594 \ - --hash=sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634 \ - --hash=sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da \ - --hash=sha256:a7f6524e3747e35f80744537c78e7302cd41deee8baa668d56d55f77d9c464b3 \ - --hash=sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876 \ - --hash=sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e \ - --hash=sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a \ - --hash=sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b \ - --hash=sha256:cdc4c17afda4dab2a9c0b79148a43a7f4e1094916b3e18d8975bfd6d6d52241f \ - --hash=sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18 \ - --hash=sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe \ - --hash=sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99 \ - --hash=sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26 \ - --hash=sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d \ - --hash=sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a \ - --hash=sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd \ - --hash=sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503 \ - --hash=sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79 +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via feast (pyproject.toml) +protobuf==7.35.1 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a + # via feast (pyproject.toml) +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via feast (pyproject.toml) +pyarrow==25.0.0 \ + --hash=sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be \ + --hash=sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc \ + --hash=sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec \ + --hash=sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849 \ + --hash=sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e \ + --hash=sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8 \ + --hash=sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d \ + --hash=sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537 \ + --hash=sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062 \ + --hash=sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e \ + --hash=sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104 \ + --hash=sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517 \ + --hash=sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e \ + --hash=sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5 \ + --hash=sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887 \ + --hash=sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1 \ + --hash=sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b \ + --hash=sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be \ + --hash=sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778 \ + --hash=sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e \ + --hash=sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194 \ + --hash=sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2 \ + --hash=sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9 \ + --hash=sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b \ + --hash=sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18 \ + --hash=sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7 \ + --hash=sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc \ + --hash=sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62 \ + --hash=sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0 \ + --hash=sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f \ + --hash=sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e \ + --hash=sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862 \ + --hash=sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50 \ + --hash=sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe \ + --hash=sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3 \ + --hash=sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a \ + --hash=sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4 \ + --hash=sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af \ + --hash=sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b \ + --hash=sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0 \ + --hash=sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f \ + --hash=sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec \ + --hash=sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f # via - # feast (setup.py) + # feast (pyproject.toml) # dask -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b - # via feast (setup.py) -pyjwt==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb - # via feast (setup.py) +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via feast (pyproject.toml) +pyjwt==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + # via feast (pyproject.toml) python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via pandas -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via uvicorn -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via pandas pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ @@ -922,322 +1057,161 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # uvicorn -referencing==0.37.0 \ - --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ - --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 - # via - # jsonschema - # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via feast (setup.py) -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 - # via - # jsonschema - # referencing +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via feast (pyproject.toml) six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 - # via fastapi -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via feast (setup.py) +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via feast (pyproject.toml) +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 + # via + # feast (pyproject.toml) + # fastapi +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via feast (pyproject.toml) tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via feast (pyproject.toml) toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via mypy -toolz==1.0.0 \ - --hash=sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236 \ - --hash=sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02 +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # dask # partd -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 - # via feast (setup.py) -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via feast (pyproject.toml) +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via # anyio # exceptiongroup @@ -1245,248 +1219,308 @@ typing-extensions==4.15.0 \ # mypy # pydantic # pydantic-core - # referencing + # pyjwt # sqlalchemy # starlette # typeguard + # typing-inspection # uvicorn -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 + # via + # fastapi + # pydantic +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 # via pandas -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via requests -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-ci-requirements.txt b/sdk/python/requirements/py3.11-ci-requirements.txt index 6284d422fb4..b1243a03d23 100644 --- a/sdk/python/requirements/py3.11-ci-requirements.txt +++ b/sdk/python/requirements/py3.11-ci-requirements.txt @@ -1,150 +1,150 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.11 --no-strip-extras setup.py --extra ci --generate-hashes --output-file sdk/python/requirements/py3.11-ci-requirements.txt -accelerate==1.10.1 \ - --hash=sha256:3621cff60b9a27ce798857ece05e2b9f56fcc71631cfb31ccf71f0359c311f11 \ - --hash=sha256:3dea89e433420e4bfac0369cae7e36dcd6a56adfcfd38cdda145c6225eab5df8 +# uv pip compile -p 3.11 --no-strip-extras pyproject.toml --extra ci --generate-hashes --output-file sdk/python/requirements/py3.11-ci-requirements.txt +accelerate==1.14.0 \ + --hash=sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d \ + --hash=sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6 # via docling-ibm-models -aiobotocore==2.23.1 \ - --hash=sha256:a59f2a78629b97d52f10936b79c73de64e481a8c44a62c1871f088df6c1afc4f \ - --hash=sha256:d81c54d2eae2406ea9a473fea518fed580cf37bc4fc51ce43ba81546e5305114 - # via feast (setup.py) -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +aiobotocore==3.8.0 \ + --hash=sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670 \ + --hash=sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + # via feast (pyproject.toml) +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.0 \ - --hash=sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6 \ - --hash=sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a \ - --hash=sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e \ - --hash=sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971 \ - --hash=sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54 \ - --hash=sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2 \ - --hash=sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96 \ - --hash=sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469 \ - --hash=sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26 \ - --hash=sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0 \ - --hash=sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29 \ - --hash=sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885 \ - --hash=sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579 \ - --hash=sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a \ - --hash=sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1 \ - --hash=sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b \ - --hash=sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852 \ - --hash=sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865 \ - --hash=sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad \ - --hash=sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092 \ - --hash=sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7 \ - --hash=sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b \ - --hash=sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8 \ - --hash=sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820 \ - --hash=sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd \ - --hash=sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee \ - --hash=sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4 \ - --hash=sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67 \ - --hash=sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d \ - --hash=sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1 \ - --hash=sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d \ - --hash=sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114 \ - --hash=sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1 \ - --hash=sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755 \ - --hash=sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04 \ - --hash=sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca \ - --hash=sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1 \ - --hash=sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae \ - --hash=sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793 \ - --hash=sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e \ - --hash=sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a \ - --hash=sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8 \ - --hash=sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602 \ - --hash=sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778 \ - --hash=sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f \ - --hash=sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49 \ - --hash=sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77 \ - --hash=sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7 \ - --hash=sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f \ - --hash=sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0 \ - --hash=sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519 \ - --hash=sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d \ - --hash=sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370 \ - --hash=sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7 \ - --hash=sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247 \ - --hash=sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d \ - --hash=sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80 \ - --hash=sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9 \ - --hash=sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c \ - --hash=sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d \ - --hash=sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020 \ - --hash=sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504 \ - --hash=sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca \ - --hash=sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731 \ - --hash=sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd \ - --hash=sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6 \ - --hash=sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21 \ - --hash=sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1 \ - --hash=sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620 \ - --hash=sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64 \ - --hash=sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8 \ - --hash=sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3 \ - --hash=sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5 \ - --hash=sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985 \ - --hash=sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813 \ - --hash=sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed \ - --hash=sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20 \ - --hash=sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03 \ - --hash=sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702 \ - --hash=sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6 \ - --hash=sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed \ - --hash=sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353 \ - --hash=sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108 \ - --hash=sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f \ - --hash=sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409 \ - --hash=sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7 \ - --hash=sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3 \ - --hash=sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9 \ - --hash=sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba \ - --hash=sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe \ - --hash=sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c \ - --hash=sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861 \ - --hash=sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a \ - --hash=sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0 \ - --hash=sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390 \ - --hash=sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3 \ - --hash=sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90 \ - --hash=sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802 \ - --hash=sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163 \ - --hash=sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d \ - --hash=sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430 \ - --hash=sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a \ - --hash=sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b \ - --hash=sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6 \ - --hash=sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b \ - --hash=sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657 \ - --hash=sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c \ - --hash=sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93 \ - --hash=sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f \ - --hash=sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82 \ - --hash=sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31 \ - --hash=sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc \ - --hash=sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52 \ - --hash=sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c \ - --hash=sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a \ - --hash=sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52 \ - --hash=sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7 \ - --hash=sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b \ - --hash=sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147 \ - --hash=sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 # via # aiobotocore # aiohttp-cors # fsspec + # kubernetes # ray aiohttp-cors==0.8.1 \ --hash=sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d \ --hash=sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403 # via ray -aioitertools==0.12.0 \ - --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \ - --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796 +aioitertools==0.13.0 \ + --hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \ + --hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c # via aiobotocore aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ @@ -154,19 +154,29 @@ alabaster==0.7.16 \ --hash=sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65 \ --hash=sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 # via sphinx +alembic==1.18.5 \ + --hash=sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc \ + --hash=sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e + # via mlflow altair==4.2.2 \ --hash=sha256:39399a267c49b30d102c10411e67ab26374156a84b1aeb9fcd15140429ba49c5 \ --hash=sha256:8b45ebeaf8557f2d760c5c77b79f02ae12aee7c46c27c06014febab6f849bc87 # via great-expectations -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via fastapi +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via + # elasticsearch # httpx + # httpx2 # jupyter-server # mcp # sse-starlette @@ -210,9 +220,9 @@ argon2-cffi-bindings==25.1.0 \ --hash=sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520 \ --hash=sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb # via argon2-cffi -arrow==1.3.0 \ - --hash=sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80 \ - --hash=sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85 +arrow==1.4.0 \ + --hash=sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205 \ + --hash=sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7 # via isoduration asn1crypto==1.5.1 \ --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ @@ -220,14 +230,14 @@ asn1crypto==1.5.1 \ # via snowflake-connector-python assertpy==1.1 \ --hash=sha256:acc64329934ad71a3221de185517a43af33e373bb44dc05b5a9b174394ef4833 - # via feast (setup.py) -asttokens==3.0.0 \ - --hash=sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7 \ - --hash=sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2 + # via feast (pyproject.toml) +asttokens==3.0.2 \ + --hash=sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2 \ + --hash=sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933 # via stack-data -async-lru==2.0.5 \ - --hash=sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb \ - --hash=sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943 +async-lru==2.3.0 \ + --hash=sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6 \ + --hash=sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315 # via jupyterlab async-property==0.2.2 \ --hash=sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380 \ @@ -237,38 +247,128 @@ async-timeout==5.0.1 \ --hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3 # via redis -atpublic==5.1 \ - --hash=sha256:135783dbd887fbddb6ef032d104da70c124f2b44b9e2d79df07b9da5334825e3 \ - --hash=sha256:abc1f4b3dbdd841cc3539e4b5e4f3ad41d658359de704e30cb36da4d4e9d3022 +atpublic==7.0.0 \ + --hash=sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e \ + --hash=sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b # via ibis-framework -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via + # feast (pyproject.toml) # aiohttp # jsonlines # jsonschema + # openlineage-python # referencing -azure-core==1.35.1 \ - --hash=sha256:12da0c9e08e48e198f9158b56ddbe33b421477e1dc98c2e1c8f9e254d92c468b \ - --hash=sha256:435d05d6df0fff2f73fb3c15493bb4721ede14203f1ff1382aa6b6b2bdd7e562 +azure-core==1.41.0 \ + --hash=sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d \ + --hash=sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a # via # azure-identity # azure-storage-blob -azure-identity==1.25.1 \ - --hash=sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456 \ - --hash=sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651 - # via feast (setup.py) -azure-storage-blob==12.26.0 \ - --hash=sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f \ - --hash=sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe - # via feast (setup.py) -babel==2.17.0 \ - --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ - --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 +azure-identity==1.25.3 \ + --hash=sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6 \ + --hash=sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c + # via feast (pyproject.toml) +azure-storage-blob==12.30.0 \ + --hash=sha256:2cd74d4d5731e5eb6b8d5c5056ee115a5e88f8fdf22517b739836fda685018be \ + --hash=sha256:d415ac50b67a8da6b3ae7e9f1014b1b55cd7aafa0b8d4ca9b380568dc7360423 + # via feast (pyproject.toml) +babel==2.18.0 \ + --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ + --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 # via # jupyterlab-server # sphinx +backports-zstd==1.6.0 \ + --hash=sha256:0308990ffc998df3c7ed35276bde049728b5c3956203cae40d80893576a41459 \ + --hash=sha256:03b7c59c71f7a597e2bcb3f8368371e9a660a1bdf1c37afc1f1ad1496a013c19 \ + --hash=sha256:0466b14723f3b7697669c00ee66fe16e30e25636b286b0a923fa86fa3d8a753c \ + --hash=sha256:068ef3d8c18815a2e3a752f766313e19910e7c50939b956923748d9c04ebcb1b \ + --hash=sha256:08793876172551a930ce4d65c712cd516184d1a97070d4a1193e05bf0cf7040d \ + --hash=sha256:0eb4281f402b94d397b7482f6d9efd04c28274e4ed6eb57eb1f87bdd091a6a87 \ + --hash=sha256:10b61850c4112952e05aa6e6cce8c9a5936fbeadb321e154216705cc76a14afa \ + --hash=sha256:15b1aae0f64cd742df4bba1d989d0a09a6ec619202543fdba684640454541fd3 \ + --hash=sha256:17efb3d11137de5166dd51eedab9c36ad633402acba386eee8d715213ea47e49 \ + --hash=sha256:1a99710fbb225d459d66def4dc2bb2cd4a9a0bdc8b799fc0621cfdd863be9c93 \ + --hash=sha256:1d146926e997d2d3de8212bdcbf4985344a2622ca3bec458d8908000a84fd883 \ + --hash=sha256:1e20b3ecd0a711be82e964aca28554eabbc31ee69a20e5e7b8fd42268af46212 \ + --hash=sha256:23a793f2fed4dbf0517319759a2cded0b0dd8e8d3797fe30badd5693e320c175 \ + --hash=sha256:25b5ddc789480072551af571a746e9500356b2aff0499861cf2ca07ea7431e68 \ + --hash=sha256:28eef3881164f3c23ce58ed59e4684103bdd279583eb2d299858c9e9b72fde9a \ + --hash=sha256:28fecd73459d74910ae1987ab84b7bef690d3dd860948430dd5555108b006daf \ + --hash=sha256:2914abea516704bdafb2090acd3f15b5f9debecfabd15b8dd8285b2ad3b92209 \ + --hash=sha256:2ace939e4d620e119423606f2d3d7115f8707733bf57f279ad9a9383f875986f \ + --hash=sha256:2ba9ac10fc393e5123a08802e0e895a107cb4a66b9973d2844dbd8a343111e59 \ + --hash=sha256:2ddab55a5f54dec8acfad68ef70f1c704fd21919990ddc238afbd6f496e61c6a \ + --hash=sha256:2f723219335387d7546412d8141e0303590600949b4184a1391a0c6a3c756058 \ + --hash=sha256:30576f49b82328ec8af16c11100efe52ca88526f71bbe100ef6b4e707dc13bf2 \ + --hash=sha256:32f04d54ec1fdf3aa648b24a10b1c9234ed2046cc4af7a8850cbc236c05d42f3 \ + --hash=sha256:3b6713371f8987a1178df93cb36f29eef191f224021e2d656b2f11ce60d26816 \ + --hash=sha256:3e689af303df287142770abe3a48bbefd24dab4a09da5807d0e1fa8c75bab026 \ + --hash=sha256:460fd6b3f338c659507ae36cfd6b58ac9942a2ff233c5cf574416dfec0451a84 \ + --hash=sha256:481a1e9bd8f419fdc625307aa20234687f99368c75df511ef589693c5fea4c6f \ + --hash=sha256:4b6c8b02ab0ccb2431bb7bc238be91d158b308915e7b07937388e540466fe7e7 \ + --hash=sha256:4c68a9ed2df0cca51d774c521e68a34d2e3d9ebfc687ef8096adfd4f345b551d \ + --hash=sha256:5918fc6b31437208721276964323933cd86077b8d5b469c59c1b3fd2c8220a05 \ + --hash=sha256:5e9a8370c8ed873083d5de956d6b2e60adbad31e52d7a11111c96ef01d1910ae \ + --hash=sha256:6430b34a2ae6fcc604672f4f913102563473d9a015bdca1ce8c95041cc1f2677 \ + --hash=sha256:64b94d7a836568926a3309ff510c7f8261b881b341fd4992cabf4f0998878f8a \ + --hash=sha256:65048ed08c5124f05ff9f355ab9703014bb2dbe7f8d9948ce193685b1775f442 \ + --hash=sha256:66cf8038893c7708ec345ffb3ac63c775d10f430f323ac2f0334fdb6a397c57c \ + --hash=sha256:6c73ae37dbf9207727ac095dedef864c05d836eaec962a47b3b64eaadaf1c6b6 \ + --hash=sha256:711e6b98f8924e8b4a61ff97ab6321f33de024e1ed6a32f5123763aeda8459be \ + --hash=sha256:7293fefe15f0e5852bdb4ad1e0e26f3cbd4d3e61c19f751ecc4ff34bc1eb237d \ + --hash=sha256:73000459db113a658c4fb0510100ef0e79137b5828bf957b7709aacae4eb1b87 \ + --hash=sha256:75578c71644b031118ce938855a53530708db7f4af6e83e2f8840d5a1de990f8 \ + --hash=sha256:7741e44f7938ec94f9a52678c8d19b7bc548522ffdc39c9e4481af8db545fa9a \ + --hash=sha256:79284c1dd702f4f24ed1a36e51555c907dd237b6c0d829595978f4089a2aeea9 \ + --hash=sha256:7c2b1f4a640c51130caa92cef5bf72bd3c3dbbcfbf814c37403aa0601b1811b0 \ + --hash=sha256:7d3f64c503af7b60115b97c16feaf75bd191ef2c978d5c0c7725a6682bef63c5 \ + --hash=sha256:80a7859ffe70bf239d7a2ce15293bdeb5b4280ff7dc326ffab312b0e254dbb24 \ + --hash=sha256:8219d6fceae6b39535c4ac323dba0923d10f781d59962ff3504e693fdcafa92c \ + --hash=sha256:83391ef5935cc0f329b1abca414ae20ffe40d335fc21a4b5e664f08a74317d5f \ + --hash=sha256:83415af3c64550a56cc20b4cce59bbaa81f21d28466d7adf98feff011ecbc66d \ + --hash=sha256:839faf90a7eb525a401978dc925df8c44bd12526e8ba1529b9f8a7106e729637 \ + --hash=sha256:84f92e5a60a78c72ccda79d0417d311a1f6da18f446423ed411726d545bf7b56 \ + --hash=sha256:8872a0e9f1af975966b5be6af7eebd3dc4046f15e470b719316516dc3d137cd6 \ + --hash=sha256:8c298785e2fadeab82342040f2d9ce764ce500e6da6a6d99a2de514e63580b5a \ + --hash=sha256:97e8a9674652496c7612b528085dd5a296c052a2edc466ca1bfb7b0b27820413 \ + --hash=sha256:994167ff6551b9c1ce226e0aab16295b98c94507b5701aa60d2c32b7d50796b1 \ + --hash=sha256:9cb75e33131946fabd6319061df3b8b1d588fe0963183280e9b5f49f7772fc09 \ + --hash=sha256:a13cfa3410a75e4cb87abdb669aaf79da861cb79299159054ff8f77b9671bc40 \ + --hash=sha256:a3c17e6a267d13de9cbf14bf2ebfa87e03d26692456fc67d2dbed9da4f479b18 \ + --hash=sha256:a4ae7ed5a6d813450cc2d818284ea3db9721edcef50a56aae42ea06feec38c6e \ + --hash=sha256:a838296f5b84c920172fb579cac894d255c1fc25457c7234613ddcfa385e49b7 \ + --hash=sha256:ab70eace272d6f122b121c057e436709b50a28abf30d97aab28433c08f4a4095 \ + --hash=sha256:ae106fe16e36efc60ab098d02478d30aa0e31e1420eb4ecf0116459253bc6361 \ + --hash=sha256:aeef8563b82ed4af328f98e5041c1b4800d86f68f857ffd1577d4d47dc9aa6cd \ + --hash=sha256:b067b1ef9c8e41fb0882c828aa37829938b5c0dab067eca72b23fc24c563b9da \ + --hash=sha256:b0ddbcd2866b8ff1a2836e4b0e4d44788f5b992d83fac75a38cda8f9a2bee079 \ + --hash=sha256:b4bddfcfb6679215d6f4dc5f79a1f9301af339480d70527a14b57a1f2e6b6cbf \ + --hash=sha256:b7bc9a0b66097f03820a54316d2fdd0beb38859cf98f10d63e94c55450ed8920 \ + --hash=sha256:b81b4cf3d6e0ad7ac92bef248f49fafc954262c5fb0f7e19d6aac497e5a856b2 \ + --hash=sha256:b951113113ed4b8d173418a4f155c14b739dace626b3fa3f82be1831958d39e4 \ + --hash=sha256:beb43e9885202c8d4f3762319ed4d5e98e197622afbff8439fbbdd81d08938b9 \ + --hash=sha256:c14fa5dc39a804f1b92d63506f450eca5c59647a18d197d1a564b89dac1be1ce \ + --hash=sha256:c2d1ccfe088e8279d605011a3575619a74526c261be357695b3258c0f636115a \ + --hash=sha256:c4fc41b2df5529cad5ceb230319e82728096d4b353ce8d4df68a2ec37e291bb8 \ + --hash=sha256:d6b9b06323e3ba947c0003b2d70e02f33c90c36bc6262a92eb8201afc4a1aa08 \ + --hash=sha256:d6e78d5e28f812b39f92397806ecddd4a6f3bf35531a8c039a1f187abc931af8 \ + --hash=sha256:dd085eafa2aac6f883afd28210a3231f717f25409a1e44a39bb7b04c8c5b5646 \ + --hash=sha256:df27b57d214a3124fbe4e933ef5a903d4567f154260d9aece8c797a987f2a205 \ + --hash=sha256:e39258a09b1c7ca70b5e94a5c5ccfe4700b4250b8077cfeab31d0f79565d4c9b \ + --hash=sha256:e514c71ca72f3b56bd8fbda1a6a5b7d1100a2764b42a3c74a38841f25f9b00ab \ + --hash=sha256:e73a550dbeb84e8fa50f8385f7735e9a4735b465851ef617d02f80ab10e44e7e \ + --hash=sha256:e80bceebc9b58e959bede9b26cafe15b5b9526f3533a6dd06330c5da73cb9329 \ + --hash=sha256:ece8e7288db5b827ef8c64b2f78519f1a173a8991a625978fce02eccd7654fe9 \ + --hash=sha256:ef132cfb638e9a86bd5dc07fb4e1cb895bc55bce6bb5e759366e8b160d0747e2 \ + --hash=sha256:f69365ee2b836939137de024a302395a1cb8654fb6dc5ffef6381105259c8f87 \ + --hash=sha256:f8f5c1c7c69a4b00889e52d9304a918a5b49010f9645768eb5fd0ad404f790ba \ + --hash=sha256:fa305a84087e10d7a85e8a8a3dcba8cdbda4868f2180173b264b7b488fd37c55 \ + --hash=sha256:fbb746522ebfc11155f1cd688e2c48ef3d74125e38b63eabdaab068a055c3e88 + # via clickhouse-connect bcrypt==5.0.0 \ --hash=sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4 \ --hash=sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a \ @@ -334,84 +434,86 @@ bcrypt==5.0.0 \ --hash=sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822 \ --hash=sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b # via paramiko -beautifulsoup4==4.14.2 \ - --hash=sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e \ - --hash=sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515 +beautifulsoup4==4.15.0 \ + --hash=sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7 \ + --hash=sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9 # via # docling # nbconvert -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -bleach[css]==6.2.0 \ - --hash=sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e \ - --hash=sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +bleach[css]==6.4.0 \ + --hash=sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452 \ + --hash=sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081 # via nbconvert -boto3==1.38.27 \ - --hash=sha256:94bd7fdd92d5701b362d4df100d21e28f8307a67ff56b6a8b0398119cf22f859 \ - --hash=sha256:95f5fe688795303a8a15e8b7e7f255cadab35eae459d00cc281a4fd77252ea80 - # via - # feast (setup.py) - # ikvpy +blinker==1.9.0 \ + --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ + --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc + # via flask +boto3==1.43.46 \ + --hash=sha256:66c0d943b049a46a492ec4ec2ebe73c930b1842c7137bee83aad6d93e95d4d96 \ + --hash=sha256:69453e2c1bcb9fd9806527ab99950cacfc2826cb0dce9a3a0414d19270c06c3c + # via + # feast (pyproject.toml) # moto # snowflake-connector-python -botocore==1.38.46 \ - --hash=sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e \ - --hash=sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b +botocore==1.43.46 \ + --hash=sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533 \ + --hash=sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 # via # aiobotocore # boto3 # moto # s3transfer # snowflake-connector-python -build==1.3.0 \ - --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ - --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 +build==1.5.0 \ + --hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \ + --hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647 # via - # feast (setup.py) + # feast (pyproject.toml) # pip-tools # singlestoredb -cachetools==6.2.1 \ - --hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \ - --hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201 - # via google-auth -cassandra-driver==3.29.2 \ - --hash=sha256:06ad489e4df2cc7f41d3aca8bd8ddeb8071c4fb98240ed07f1dcd9b5180fd879 \ - --hash=sha256:1e89de04809d02bb1d5d03c0946a7baaaf85e93d7e6414885b4ea2616efe9de0 \ - --hash=sha256:2039201ae5d9b7c7ce0930af7138d2637ca16a4c7aaae2fbdd4355fbaf3003c5 \ - --hash=sha256:52edc6d4bd7d07b10dc08b7f044dbc2ebe24ad7009c23a65e0916faed1a34065 \ - --hash=sha256:69aa53f1bdb23487765faa92eef57366637878eafc412f46af999e722353b22f \ - --hash=sha256:6c74610f56a4c53863a5d44a2af9c6c3405da19d51966fabd85d7f927d5c6abc \ - --hash=sha256:70d4d0dce373943308ad461a425fc70a23d0f524859367b8c6fc292400f39954 \ - --hash=sha256:7104e5043e9cc98136d7fafe2418cbc448dacb4e1866fe38ff5be76f227437ef \ - --hash=sha256:7d348c769aa6c37919e7d6247e8cf09c23d387b7834a340408bd7d611f174d80 \ - --hash=sha256:8067fad22e76e250c3846507d804f90b53e943bba442fa1b26583bcac692aaf1 \ - --hash=sha256:83dc9399cdabe482fd3095ca54ec227212d8c491b563a7276f6c100e30ee856c \ - --hash=sha256:957208093ff2353230d0d83edf8c8e8582e4f2999d9a33292be6558fec943562 \ - --hash=sha256:9e36437288d6cd6f6c74b8ee5997692126e24adc2da3d031dc11c7dfea8bc220 \ - --hash=sha256:a1e994a82b2e6ab022c5aec24e03ad49fca5f3d47e566a145de34eb0e768473a \ - --hash=sha256:a66b20c421d8fb21f18bd0ac713de6f09c5c25b6ab3d6043c3779b9c012d7c98 \ - --hash=sha256:a8c496318e3c136cf12ab21e1598fee4b48ea1c71746ea8cc9d32e4dcd09cb93 \ - --hash=sha256:b86427fab4d5a96e91ad82bb9338d4101ae4d3758ba96c356e0198da3de4d350 \ - --hash=sha256:c25b42e1a99f377a933d79ae93ea27601e337a5abb7bb843a0e951cf1b3836f7 \ - --hash=sha256:c4310a7d0457f51a63fb019d8ef501588c491141362b53097fbc62fa06559b7c \ - --hash=sha256:c4a005bc0b4fd8b5716ad931e1cc788dbd45967b0bcbdc3dfde33c7f9fde40d4 \ - --hash=sha256:c53700b0d1f8c1d777eaa9e9fb6d17839d9a83f27a61649e0cbaa15d9d3df34b \ - --hash=sha256:c5a9aab2367e8aad48ae853847a5a8985749ac5f102676de2c119b33fef13b42 \ - --hash=sha256:c86b0a796ff67d66de7df5f85243832a4dc853217f6a3eade84694f6f4fae151 \ - --hash=sha256:d180183451bec81c15e0441fa37a63dc52c6489e860e832cadd854373b423141 \ - --hash=sha256:d70353b6d9d6e01e2b261efccfe90ce0aa6f416588e6e626ca2ed0aff6b540cf \ - --hash=sha256:e31cee01a6fc8cf7f32e443fa0031bdc75eed46126831b7a807ab167b4dc1316 \ - --hash=sha256:e7f1dfa33c3d93350057d6dc163bb92748b6e6a164c408c75cf2c59be0a203b7 \ - --hash=sha256:e967c1341a651f03bdc466f3835d72d3c0a0648b562035e6d780fa0b796c02f6 \ - --hash=sha256:eb3a9f24fc84324d426a69dc35df66de550833072a4d9a4d63d72fda8fcaecb9 \ - --hash=sha256:ee0ebe8eb4fb007d8001ffcd1c3828b74defeb01075d8a1f1116ae9c60f75541 \ - --hash=sha256:f9df1e6ae4201eb2eae899cb0649d46b3eb0843f075199b51360bc9d59679a31 - # via feast (setup.py) -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +cachetools==6.2.6 \ + --hash=sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6 \ + --hash=sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda + # via + # mlflow-skinny + # mlflow-tracing + # pyiceberg + # pymilvus +cassandra-driver==3.30.1 \ + --hash=sha256:0c6a3e1428f7c6a9aa6c944b9c47a37cd2cdbeb5b5a82d42c33afdd56f14e398 \ + --hash=sha256:0ea769cf9925206b8458db10477b78699660fbdaed7262157e43b8f21817f838 \ + --hash=sha256:187bd1457e414bc019a0635603ea0001baada914942198ed23993198e44dec74 \ + --hash=sha256:222c704c72a5880cfe9c89f04b375d03d1152dbdb6ec8c25a325267b8bc2ca4e \ + --hash=sha256:30686850b9e0d0d8326ee07219113170b4e1d041e4d1434521a2f342c471108e \ + --hash=sha256:3c32513a2f2c6832d4fe3464e11be59dc21096dba296b18ec3454bc8354a78ba \ + --hash=sha256:3cb90ec9ac9d44cbfe17f1e9cb643dbc5f401e62980835aa6669a2fd5792863b \ + --hash=sha256:546ad047abf96a6d7922d057010b4cfb22e9d473db7b907956bc7c6a8029f7d9 \ + --hash=sha256:61bf2ded460fb0d2e909b70c9d193588b71148540bd85e29fe6954e00bb1a869 \ + --hash=sha256:6c5ae0218e52944f92ddde07a03cfa941fa4a10097d501a18cfa1b525c92d013 \ + --hash=sha256:71f3ccc8c60f41c83306c60704f7b1ee1a8ccd312aae286bf75d318aa47d0e71 \ + --hash=sha256:799af594364475656d9eea69606713fbaa98b10da82cc6d90286322477c72828 \ + --hash=sha256:80df7b705f71e1a6dae77aaea682cbe45fdd74b962cfcc6c6b0c05195f48b7c2 \ + --hash=sha256:84b2cc578f542332b678fc3fcc801c6ef8110a377d3fd38c27eab5ec5f2d2b79 \ + --hash=sha256:87822d6cce2c586aa72733a9dd1858276cee795834f7ad949c783bf19cf7a3a7 \ + --hash=sha256:8f43abccb5b5a761582e4ba6d44a7ef4274b61b0bd3f8e3159af40ca2c10c0fa \ + --hash=sha256:921403aa76c66d78d00c4a3dff542f989fa584b7450e3cb1dd2c267b4a5a55f0 \ + --hash=sha256:9b54f7c6cbb1d9bb6a8b9f8ab6034e6f761527e97af0603aa1e9635720df03a0 \ + --hash=sha256:9ff317f79b09eba52a44d46d8c4773e47533dfdd8b9ba4984268dc4a1fe5206e \ + --hash=sha256:ab853bd2806899c5612c3e4ac2a355a7811084d06692e4460255e3b0a4fa556a \ + --hash=sha256:af5c6f5d7d5a86725b908aba12d9554df60234c15bfe053a8175b15db791ada8 \ + --hash=sha256:c0995dccff3df739a1df6e10da9f3fc95bbbc8b38bacc4cfb6172655f8351cba \ + --hash=sha256:e10784b4f7d77332dc1a5c72855977a0d797160fd8ba50785414caed07fce95b \ + --hash=sha256:e43d873de5f8dadf4c8dcc4fab87d66a8495137e844ae3c184fbc90a54c05bc0 \ + --hash=sha256:eba7931a615207aab84ebaa000330024f1c614aa86ca58d119556edd338883ce \ + --hash=sha256:ec04eebfadc74039b1f7df84356a822539992c7dc0b013fedbb5a33d9295af86 + # via feast (pyproject.toml) +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # clickhouse-connect # docling @@ -422,296 +524,309 @@ certifi==2025.10.5 \ # minio # requests # snowflake-connector-python -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b - # via - # feast (setup.py) +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f + # via + # feast (pyproject.toml) # argon2-cffi-bindings # cryptography - # ikvpy # pynacl - # snowflake-connector-python -cfgv==3.4.0 \ - --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ - --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 +cfgv==3.5.0 \ + --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ + --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 # via pre-commit -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via # requests # snowflake-connector-python -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask + # flask # geomet # great-expectations + # mlflow-skinny # pip-tools + # pyiceberg # ray # typer # uvicorn -clickhouse-connect==0.9.2 \ - --hash=sha256:02d1d73223493b51922168fc6161908d520de9c2e4015aa1bef3019ab1e8b7d3 \ - --hash=sha256:07c0f5098e9e4c6fa19d7471bc225ca1cf3ace99f01e8dda8be48df947cf0ddf \ - --hash=sha256:0b43416e922c7efd8c636cba7c9dcbeed8e26125d1d0fdf2387e86f6457cbe51 \ - --hash=sha256:113abbc5fa6413322b806fa35455960cb09dd829f3eb951db66164b335daab60 \ - --hash=sha256:13d6dc0776a760f6cbbb473ec75d152b697405e8c7061d5ad320df6fd9df687c \ - --hash=sha256:169fb9d9cf191095d4d06622af856d3c38a5fd4dba3f53f203dd2ac9807858c4 \ - --hash=sha256:19aa0bdfcb93117243a657df2d0f3c4dbf4849d6295afc199efbf680de43daf9 \ - --hash=sha256:1b4bd8f2864ebf1007ea59a2fbaa9fec8b5518030c1ad582645b8690db582305 \ - --hash=sha256:2532df7ee9003ebf1ffb559de39db27c733710ba2d178654aa8ddf8464291391 \ - --hash=sha256:2ab3fd64bbcf52a6a13ff3cf2a74d5c571e3c82d3c6a1d210562daec4ba2ba6b \ - --hash=sha256:2b1cb1542c2e6d1ede32c3d7aed86c06bafe6b5a9d4bb645b5c04f48389f8a4a \ - --hash=sha256:2c1bbe7d541fc776dc04fc0f416431e5245e4b872f5a8375a01094f336bcac57 \ - --hash=sha256:322163909d79b31f981fecdba9f6fe87279c62f6145454e6a191d204b9dd2adb \ - --hash=sha256:3364ca5e08986128d25a8926696d4f1bfb4ceb893b9b2cf93999287c1e98f92f \ - --hash=sha256:340e1353eddc2a5ea5ce073e4665d7ba10c2491254544ef802b592d8128a04e0 \ - --hash=sha256:3bea1477347e93fb14e7bab63f3282ebd8bc23cc9ad76c128534ed9168e2256f \ - --hash=sha256:3bfcf901eb94a218298753980d130bef2d1fa2e9e5845b0385d405092d09661f \ - --hash=sha256:4926616368b0a34173b1db5e567f06e855508ffc0db4d8ce5b3db4eb9bace5c1 \ - --hash=sha256:4cf6d857ee4f2ccd2c008189e521d33e35b462bd3425e4b599fa33800e832e30 \ - --hash=sha256:4d2f9bb8fe9a3fe570057195a1d91f7a6ee8b8ca323e1b74a70c270179482516 \ - --hash=sha256:525e91c972f79dd40ae515b05894f2a2e8c9d0d26da3f1adff2656d1fde250f4 \ - --hash=sha256:567f258de589b0930da43141397febd685acb3f4cf16f517c599bde4b63d35ad \ - --hash=sha256:5b9ce69c06109de2cf5827de7cdb12cf9688b5b4420463ebbfe959959f85e5c7 \ - --hash=sha256:6461bf077736d990a7198c2f352dcc7919bebdf2952993d26d18b8f25f500f7e \ - --hash=sha256:653dd934e545db2b5dda3c436ae68149ca6031149bf928cd7714921c45a66bb9 \ - --hash=sha256:655d6670741ec87497b8b37b7eaada0ec50757b3816c500ec9b7d63775741c92 \ - --hash=sha256:6c878f9eee3157455d623f1e5c233bb47e3f474f1ad8bc53358d7efb65ec199c \ - --hash=sha256:6d77bb17001cf5d88cbae3a579ab7bd76904058f8270bdd5ba3f09015f9efb55 \ - --hash=sha256:7d735cd382cacac5bb846c9a7f97e4b2de806978eca726cb90599c173b15e925 \ - --hash=sha256:7ee21425c7f259641c82ed024d2c0d769ccb1e818bfaa4b63d34636521fda266 \ - --hash=sha256:8521459ff9f7cc88d52f232ab006cd1d383a7b58f25022861f10770e6acde86e \ - --hash=sha256:86510682f80d1c72c12b84cf272db670d963a2f005c26fdf8268fbd494b1e6f3 \ - --hash=sha256:8b666caa538732fadeae6b50ea5d5fd67583be9438c79bd1de9ae69ae6ec0c72 \ - --hash=sha256:8c0b3c2a9ca71bd78839a360a47a5df9b95c280a795f8aee083a3de01e9085fc \ - --hash=sha256:8c9a482eaf951aaf9aa9630fd33b832454a56e1612fd0780c50fbaf2d1e8b977 \ - --hash=sha256:98ac02aa3a27cef3f841b7b2d1817b244ef2215badb3cf94719b3889369e95c3 \ - --hash=sha256:9ab13be35a6c92489d68b6bd4b32afcbedaa9e7c4aacd15a2f1a4fba761a33d8 \ - --hash=sha256:9b7a09548d5542badcfdbe3d62fccc4148124e6da7f061dfbcbdf041b730d372 \ - --hash=sha256:9f2c5e89b78526796379b63c31c0170f0c7000c5321aaad68eb6a70084520eba \ - --hash=sha256:a147cf722d28f49ede19566b32d4e46cafd6a1854f78252d803d8a58fb825b64 \ - --hash=sha256:a4b056da400f671e3f3b96e87eaa1457e511a8781a9587cb3be39c2ce9da3289 \ - --hash=sha256:aead65896287f3322b92b2235e783ab6ea4d03724fc8adc8efc4eddd95ffe91f \ - --hash=sha256:b909b56c4a5184245c7a414d0c4c154ba19fd6c147c6446e265c4c12923156a2 \ - --hash=sha256:ba413a95f992decb47040d887d673e58958f0efb6c1e1cc892c78cfd18885b02 \ - --hash=sha256:bbfbfea214f314db92dcd403ee0b748082c712a65c285c390672ff5421d3cd2a \ - --hash=sha256:bd434920d1f7b72496f368bd6d72538d8bbb3c6bef561fdb75cbc7e5732131c4 \ - --hash=sha256:bd47456aa88fcbc853380a55ef873b173f306b39d13978c4df0a7ba054324999 \ - --hash=sha256:c114ace0c34956ef8d6f7a23a3fe7857bb7b7812e4f85ff04ad2eaffa23ee133 \ - --hash=sha256:c5a78d95ba68e27e6acc768ea0f4f38ebe11b74ae9b00359ae005c6be793b012 \ - --hash=sha256:c8d5f63228da99aa54deee054c7b1f94862df7a3167eb1f960b93e60553b92da \ - --hash=sha256:ca6c200e74321a17b1e12da5fdb5ab104f3c0fcfc1e6389d29fe06ee4d13367b \ - --hash=sha256:d243a01531fecde2570cdc1dc2ad3e4737eb65d2e54dbcb0f5e776323cc90606 \ - --hash=sha256:d2531cf9ac36e5d371d141d8642e159d5081d22db1698c17e23e857894cb60d4 \ - --hash=sha256:d45f35c1734e1a8664edc31015fc490f53e6c912f359416fcd717b1815e28d56 \ - --hash=sha256:db2719ada3cf51aa94d98a4fb01778e795a26b1dbd5843d87d5f89120ef5bed6 \ - --hash=sha256:dd4b6464bb5a61ba913d9fbc5613365b3972a94fd71db7a02e1ae65970986109 \ - --hash=sha256:df25751bb5aa29e40b8b127500ea6a7f89031644c6b6bf9887c1dabe8df2bc36 \ - --hash=sha256:ebc8517d00e94d3677382d01b9c4ff10e212183ec05a72e774dec78b75458fa0 \ - --hash=sha256:ef549994c63d20e0ba1c7af3dfcf64daa9578467a665925ed46aabe682de4fb8 \ - --hash=sha256:f32a862c66174f986f3b64beebe8f336c3e2964c3a562e9deafc3e49277ab65b \ - --hash=sha256:f33efa9e6f08d277305e31b16666efc3299eb86155f0fd7152cf3e022256572b \ - --hash=sha256:f3816d1e07eed15a884bd81b827dccade4857e3ee4e9d81ea573e977e3cffc08 \ - --hash=sha256:f77444cb6e2d961c08fe2f1db130bfa587f3ad71ef09e46e083cf9f9dae90f00 - # via feast (setup.py) -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e - # via dask -codeflare-sdk==0.31.1 \ - --hash=sha256:76fc797903374832592f016b515cc2a504ef2903fc1ebc32886a4d74978f5658 \ - --hash=sha256:df94cf0b74ab412384695d507ef177a856b4fbad1acb612a2f7808ab01dc8bd8 - # via feast (setup.py) +clickhouse-connect==1.6.0 \ + --hash=sha256:01367cc75ddd3081f750ccba65c252385feff72f38f68613ea02dd056d047922 \ + --hash=sha256:064144e1a7f6075ade3c13b8ce49517ca92f86c0c4842bd2abb52a475c026025 \ + --hash=sha256:069f105173cd04263d3bb710381edae341dc9a3aa42fb4673017a617a5a1237a \ + --hash=sha256:06bd2e045a7ec1406f9bd39c92bcc77027258d8969128033cea5adb8602111bd \ + --hash=sha256:0856f42be01ab4957789b8cec99e91311d88bfe82be37ef2d230d36a6a111cff \ + --hash=sha256:163eb12e7aaeefe9e51a483d3ad40502e15ac2ef94cf1f5b7fe8e9f5303d161b \ + --hash=sha256:1a84eb6d545e2647a51b3b4ffa0da4a49f308aaaa096e58c44fb749c171a7d4a \ + --hash=sha256:2d5888d76be3bf1ddea5a210522572d66abcaf651b45ff7c5a1014ec08fe7565 \ + --hash=sha256:2f8b8060a8277509db77b14a01ed70683a0d9becba7a9994a5e59e42e9f98b37 \ + --hash=sha256:30a183095bc367dcc4695869754e36ce2177264d0ef79421990ee24700810ee0 \ + --hash=sha256:30bb77e7aa842550d5265c4aaa3e432b01440fd116d63e7e79ae79c380971ade \ + --hash=sha256:34969b2a57d9372921aef2b0b51fb858d81ce952c5249b6cb5a0d459e846854c \ + --hash=sha256:3cc6bdeafc743739f809671eb3c8fe9af1701981a43c624d9f5bea1e1409e5c4 \ + --hash=sha256:4372dbcbd7b1c897c8d7062832468f82af27fa731c7488421ec7f0a882e1580b \ + --hash=sha256:444564bad257b8923005431d661e347e5360cd686aae67091de3796a465cfec1 \ + --hash=sha256:483e34a7841dbf3d2863e784a129503cc37a910a05464f2f48ce3e9b2f0fc007 \ + --hash=sha256:508d3b7cc893b5d889b92702cc7dc4a1c975f59ee8da383054076969e4cb17c3 \ + --hash=sha256:5135f91841e9df5827faa24f3b2436d4ac8242e74a88a8d47922e31a4383b712 \ + --hash=sha256:51905921e7c98a11e7551663d263f28a715b14fdda191569fc71ddcde964ce52 \ + --hash=sha256:6570d07223ea5e4c4c21fece323cc8741b7711d31c6cb3fda8d1a62fa2d5b8d4 \ + --hash=sha256:691782a911a0696bb1ff644a309f4c6940473ee48d6c5171b13dab96d8cf8ecd \ + --hash=sha256:6bcdb6039bb55870229ec46784840f1420362e3979c74abdc6a53d1ae95718eb \ + --hash=sha256:7043480f1459ddd791480e3ea560a1def81dfd6462eab0c9ada54946e72918af \ + --hash=sha256:743787fd9d1505f55854b333b81b3fa92bf950d988b66a553aaa55c7bfd2f70c \ + --hash=sha256:75390e6ff6b398a88e0cac4907f785763fc9e476314cbafcfb24553bdbd92d28 \ + --hash=sha256:794735a62d0a46688d50652876bcb50116c5e912577e9a469ef6ed1aa52b4d7b \ + --hash=sha256:79caafbd2b1364d4b63c2e7c625cb424c8572b9cedc62b4ac2289cfbdf875487 \ + --hash=sha256:7fdea3ba73b77c636a30fc74c7f2c4c53ed0fcbf0da13498ff4122c701f10362 \ + --hash=sha256:80ca5778ce35d1f658741b993a9e7e17457d3f7a854e904aff72354b29990079 \ + --hash=sha256:8128dfe4a17aef265d5ba2bb5f2f5a0d5fa76e6aca357c71a17da378c1b30ea6 \ + --hash=sha256:8a5c244a31dd2f2d512c7b306e41e592dcaf9550926a96e05bd3222c6a44dacc \ + --hash=sha256:8bdef691c6c66a28301cb1f070f633dfbd60ccdca9580df96984325f1f79e803 \ + --hash=sha256:8d4303987b36ca075cbbe2cff3439ad017f56e1cf2bd936f83b2e6be222ae62f \ + --hash=sha256:9090c972f3d3ab40fc046913cf51c611345d603c922483e3239633f2cda16a3c \ + --hash=sha256:9a7af9bf7b992e5098ed9a0c2edf18d19c6acb2ccc77ee1d2da4e7e5d9dc7ddc \ + --hash=sha256:9dac7ce419ced91f035bd26a86dd49067461547cb9e3b9b1d7c5975b460b650c \ + --hash=sha256:9e64e332983fb268350abc76119c14da7b0640373a77ecf4f310240c3fdb4b6d \ + --hash=sha256:a337751b60c9d15eb9865cfaa32caeb6d81497a9fd3dad248ae112765e97c7f1 \ + --hash=sha256:a34e72d1ae6f71be491b2d44bd10d082d04c9e2e2a04d4774498e965d933b21d \ + --hash=sha256:a5285b9005d5a3ba41d6f148c957e4df8e37a38ad27835a4ad9c31fe21083149 \ + --hash=sha256:a7cf3b7ddffbf0b0bb77cb65935561ae48c49b4e6d0db11188279b068c297862 \ + --hash=sha256:b94bf383735abb90b1a6dc9e89a44f09b84a71a73348be0f01c7c2fb8500fdea \ + --hash=sha256:bd8de68d176a807fd16b85c3c5639c25bbdc81c203217f9e985e9560357fecd5 \ + --hash=sha256:bdd153037df79b684a30372395681bd673b562e9d6c5159c3a08bffc38ad8185 \ + --hash=sha256:c62bd3cc58aa0f6d4cac7012d6e5698cdc23111e3938a809596a5af8459470bd \ + --hash=sha256:c793706c87ab04b29a58e5bc2c0f53806c257859e9ceb859b2840c5aabe56402 \ + --hash=sha256:c924a2dea34578eda62e99a95988134ddaf4f66aa3f842ebd0980973b6acad56 \ + --hash=sha256:ca3d4b70d1fa92e62ab130dec495b470de6e0e56a944cd3c8ea049cb0f137c20 \ + --hash=sha256:cc03b0dd248faca290bced68a575e41331752bfd5863e3ab126382caa547aa1b \ + --hash=sha256:cd4c1dee496ad3a811237ff8066084c18cdaa46a046f9e0eeacce107ac29c83c \ + --hash=sha256:cf66e5bcfba8a183e40485efcfc80329ec75104e22dd02485fe73c0ab9073b19 \ + --hash=sha256:d541a1a75f08d62b7e0059f77efeff26f2b0829fa0d543e1e1edcce270c78a06 \ + --hash=sha256:d56348f1a39c8bd872106c4af92faa532fbdd8d66e72c588e9b4dd676ca2dbbd \ + --hash=sha256:e02c6abbfe777f4d0afdf7726ac30168514c46390b15e12ecf95d873c7b20969 \ + --hash=sha256:e4eb8b83b848f4ca4b984899acd62b0a60f310cbaa559d83519b7477968392ed \ + --hash=sha256:ed28787dd00a34b003fe24de4953e15b172058d875279336d4ee5d8f585acc50 \ + --hash=sha256:effd020411e4669f21d8b9f7f95d2342145ff6a2e32b49eae416fbd733334e53 \ + --hash=sha256:f8957fa2fd400d1f002dded822b3e667b7941e48107d8d2470e1935a12af9937 \ + --hash=sha256:fe2736f4cb520c4403f1f6b078ed3ed6f41a6ee8972328082240001853d8639e \ + --hash=sha256:fe4bdd4e09f52bff46c2f920877a08a26a44d91fdb5fc0be76edb67395440192 + # via feast (pyproject.toml) +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + # via + # dask + # mlflow-skinny +codeflare-sdk==0.38.2 \ + --hash=sha256:4b2b7e005fdf86181b89c9e75f31613ad56d58e3ca068eb24ce450946f7d5548 \ + --hash=sha256:4e17c7c5f970f21733fc12169cfa13d6198d03374fb0320c62c20712dc9f3ae7 + # via feast (pyproject.toml) colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via - # feast (setup.py) + # feast (pyproject.toml) # great-expectations -colorful==0.5.7 \ - --hash=sha256:495dd3a23151a9568cee8a90fc1174c902ad7ef06655f50b6bddf9e80008da69 \ - --hash=sha256:c5452179b56601c178b03d468a5326cc1fe37d9be81d24d0d6bdab36c4b93ad8 +colorful==0.5.8 \ + --hash=sha256:a9381fdda3337fbaba5771991020abc69676afa102646650b759927892875992 \ + --hash=sha256:bb16502b198be2f1c42ba3c52c703d5f651d826076817185f0294c1a549a7445 # via ray comm==0.2.3 \ --hash=sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971 \ @@ -719,6 +834,80 @@ comm==0.2.3 \ # via # ipykernel # ipywidgets +contourpy==1.3.3 \ + --hash=sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69 \ + --hash=sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc \ + --hash=sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880 \ + --hash=sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a \ + --hash=sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8 \ + --hash=sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc \ + --hash=sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470 \ + --hash=sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5 \ + --hash=sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263 \ + --hash=sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b \ + --hash=sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5 \ + --hash=sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381 \ + --hash=sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3 \ + --hash=sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4 \ + --hash=sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e \ + --hash=sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f \ + --hash=sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772 \ + --hash=sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286 \ + --hash=sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42 \ + --hash=sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301 \ + --hash=sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77 \ + --hash=sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7 \ + --hash=sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411 \ + --hash=sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 \ + --hash=sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 \ + --hash=sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a \ + --hash=sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b \ + --hash=sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db \ + --hash=sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 \ + --hash=sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620 \ + --hash=sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989 \ + --hash=sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea \ + --hash=sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67 \ + --hash=sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5 \ + --hash=sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d \ + --hash=sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36 \ + --hash=sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99 \ + --hash=sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1 \ + --hash=sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e \ + --hash=sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b \ + --hash=sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8 \ + --hash=sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d \ + --hash=sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7 \ + --hash=sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7 \ + --hash=sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339 \ + --hash=sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1 \ + --hash=sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659 \ + --hash=sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4 \ + --hash=sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f \ + --hash=sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20 \ + --hash=sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36 \ + --hash=sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb \ + --hash=sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d \ + --hash=sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8 \ + --hash=sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0 \ + --hash=sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b \ + --hash=sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7 \ + --hash=sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe \ + --hash=sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77 \ + --hash=sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497 \ + --hash=sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd \ + --hash=sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1 \ + --hash=sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216 \ + --hash=sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13 \ + --hash=sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae \ + --hash=sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae \ + --hash=sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77 \ + --hash=sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3 \ + --hash=sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f \ + --hash=sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff \ + --hash=sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9 \ + --hash=sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a + # via matplotlib couchbase==4.3.2 \ --hash=sha256:032a180afd6621358b2c73543b9c5db9939b442fc3ad6d54417c36c8a8f65838 \ --hash=sha256:11ce688ed46edf8387bf51866618c7b4e06399e7fb34a6df002764996c109d1f \ @@ -751,7 +940,7 @@ couchbase==4.3.2 \ --hash=sha256:c139c594118e1df6932491c26b825ac777fd201f49cda303892065e607f9c3ef \ --hash=sha256:c18b7c937e63e6d869371d9c4b0a0f9cc1a87dba48950a4276e294c091d76ce5 \ --hash=sha256:c50ae993c81d81ed0f02a37cbe034825408195b9fe29980b3379e2e05b2e1bec - # via feast (setup.py) + # via feast (pyproject.toml) couchbase-columnar==1.0.0 \ --hash=sha256:08b0947ee67f8fb15949e9323d60e5dbb44fb2d86d09f6e997a0bdcde6cd2b15 \ --hash=sha256:0f5ea6a24a73008a2f5a6e3aae51a49f4bb360b198a1f3d2ca4bb22044fe9671 \ @@ -784,210 +973,234 @@ couchbase-columnar==1.0.0 \ --hash=sha256:fa8fbddf971a2391543bc7dafaf3b581ad1a69c1fa0a474295b38a6fd8aed54f \ --hash=sha256:fc0fad2d386c5b5df7aaaccd8751e01caa886cc640cc8c92523dd07c4e7be519 \ --hash=sha256:fc4efa3e15190c3731478006de494b046bc57785e9c8ae99ac8b375a91683e38 - # via feast (setup.py) -coverage[toml]==7.10.7 \ - --hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \ - --hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \ - --hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \ - --hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \ - --hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \ - --hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \ - --hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \ - --hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \ - --hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \ - --hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \ - --hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \ - --hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \ - --hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \ - --hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \ - --hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \ - --hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \ - --hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \ - --hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \ - --hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \ - --hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \ - --hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \ - --hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \ - --hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \ - --hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \ - --hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \ - --hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \ - --hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \ - --hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \ - --hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \ - --hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \ - --hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \ - --hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \ - --hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \ - --hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \ - --hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \ - --hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \ - --hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \ - --hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \ - --hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \ - --hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \ - --hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \ - --hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \ - --hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \ - --hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \ - --hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \ - --hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \ - --hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \ - --hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \ - --hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \ - --hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \ - --hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \ - --hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \ - --hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \ - --hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \ - --hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \ - --hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \ - --hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \ - --hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \ - --hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \ - --hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \ - --hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \ - --hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \ - --hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \ - --hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \ - --hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \ - --hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \ - --hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \ - --hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \ - --hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \ - --hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \ - --hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \ - --hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \ - --hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \ - --hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \ - --hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \ - --hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \ - --hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \ - --hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \ - --hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \ - --hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \ - --hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \ - --hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \ - --hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \ - --hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \ - --hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \ - --hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \ - --hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \ - --hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \ - --hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \ - --hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \ - --hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \ - --hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \ - --hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \ - --hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \ - --hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \ - --hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \ - --hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \ - --hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \ - --hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \ - --hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \ - --hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \ - --hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \ - --hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \ - --hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3 + # via feast (pyproject.toml) +coverage[toml]==7.15.2 \ + --hash=sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2 \ + --hash=sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e \ + --hash=sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db \ + --hash=sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf \ + --hash=sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c \ + --hash=sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8 \ + --hash=sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443 \ + --hash=sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a \ + --hash=sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145 \ + --hash=sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9 \ + --hash=sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2 \ + --hash=sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376 \ + --hash=sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138 \ + --hash=sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578 \ + --hash=sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c \ + --hash=sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88 \ + --hash=sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036 \ + --hash=sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c \ + --hash=sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071 \ + --hash=sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a \ + --hash=sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d \ + --hash=sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b \ + --hash=sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a \ + --hash=sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b \ + --hash=sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050 \ + --hash=sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846 \ + --hash=sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d \ + --hash=sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1 \ + --hash=sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660 \ + --hash=sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40 \ + --hash=sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026 \ + --hash=sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0 \ + --hash=sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b \ + --hash=sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0 \ + --hash=sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa \ + --hash=sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d \ + --hash=sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658 \ + --hash=sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89 \ + --hash=sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072 \ + --hash=sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199 \ + --hash=sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446 \ + --hash=sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743 \ + --hash=sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7 \ + --hash=sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1 \ + --hash=sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287 \ + --hash=sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5 \ + --hash=sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be \ + --hash=sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688 \ + --hash=sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934 \ + --hash=sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7 \ + --hash=sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440 \ + --hash=sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984 \ + --hash=sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc \ + --hash=sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d \ + --hash=sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098 \ + --hash=sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6 \ + --hash=sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629 \ + --hash=sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b \ + --hash=sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee \ + --hash=sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f \ + --hash=sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1 \ + --hash=sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad \ + --hash=sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3 \ + --hash=sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9 \ + --hash=sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3 \ + --hash=sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a \ + --hash=sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296 \ + --hash=sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1 \ + --hash=sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd \ + --hash=sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73 \ + --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f \ + --hash=sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0 \ + --hash=sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6 \ + --hash=sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5 \ + --hash=sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1 \ + --hash=sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589 \ + --hash=sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688 \ + --hash=sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487 \ + --hash=sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9 \ + --hash=sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd \ + --hash=sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee \ + --hash=sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d \ + --hash=sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d \ + --hash=sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb \ + --hash=sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a \ + --hash=sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328 \ + --hash=sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635 \ + --hash=sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188 \ + --hash=sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c \ + --hash=sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243 \ + --hash=sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a # via pytest-cov -cryptography==43.0.3 \ - --hash=sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362 \ - --hash=sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4 \ - --hash=sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa \ - --hash=sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83 \ - --hash=sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff \ - --hash=sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805 \ - --hash=sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6 \ - --hash=sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664 \ - --hash=sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08 \ - --hash=sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e \ - --hash=sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18 \ - --hash=sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f \ - --hash=sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73 \ - --hash=sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5 \ - --hash=sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984 \ - --hash=sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd \ - --hash=sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3 \ - --hash=sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e \ - --hash=sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405 \ - --hash=sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2 \ - --hash=sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c \ - --hash=sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995 \ - --hash=sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73 \ - --hash=sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16 \ - --hash=sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7 \ - --hash=sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd \ - --hash=sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7 - # via - # feast (setup.py) +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b + # via + # feast (pyproject.toml) # azure-identity # azure-storage-blob # codeflare-sdk + # google-auth # great-expectations # jwcrypto # moto # msal + # oracledb # paramiko # pyjwt # pyopenssl # snowflake-connector-python # types-pyopenssl # types-redis -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) -datasets==4.0.0 \ - --hash=sha256:7ef95e62025fd122882dbce6cb904c8cd3fbc829de6669a5eb939c77d50e203d \ - --hash=sha256:9657e7140a9050db13443ba21cb5de185af8af944479b00e7ff1e00a61c8dbf1 - # via feast (setup.py) -db-dtypes==1.4.3 \ - --hash=sha256:a1c92b819af947fae1701d80a71f2a0eac08f825ca52cf0c68aeba80577ae966 \ - --hash=sha256:d3cb08c32939d24e05501f17694be8c1c54cfb4620d61fb56fb311e8c3d8885e +cycler==0.12.1 \ + --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ + --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c + # via matplotlib +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) +databricks-sdk==0.122.0 \ + --hash=sha256:7d998ec47f580be6be1cfe066be78038054767575b7437af5e4f76b67dde49c2 \ + --hash=sha256:eb11be13f0c02fc4ffc6e2672ad47478ce26ac4b310bd26916af8ba56e374e98 + # via + # mlflow-skinny + # mlflow-tracing +datasets==5.0.1 \ + --hash=sha256:9fbf73688f8c18f7529b4fe592abd04015f81d1e58001e4bac73ffb2b39d7cc4 \ + --hash=sha256:ce22bb851efd7494f08aad33b940803784434f6e77763d00679a0dc45fcf686a + # via feast (pyproject.toml) +db-dtypes==1.7.1 \ + --hash=sha256:450b94cd010cfa6d4d13cca2593009eba4fbe15516084c332cde1a47bbce0625 \ + --hash=sha256:75adf3bdf3174a6474ea4162d9a345ea0b3102ec4fd603e69ec26b6e8a99145a # via # google-cloud-bigquery # pandas-gbq -debugpy==1.8.17 \ - --hash=sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1 \ - --hash=sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3 \ - --hash=sha256:17e456da14848d618662354e1dccfd5e5fb75deec3d1d48dc0aa0baacda55860 \ - --hash=sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc \ - --hash=sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4 \ - --hash=sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088 \ - --hash=sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670 \ - --hash=sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef \ - --hash=sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf \ - --hash=sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420 \ - --hash=sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464 \ - --hash=sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c \ - --hash=sha256:8deb4e31cd575c9f9370042876e078ca118117c1b5e1f22c32befcfbb6955f0c \ - --hash=sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a \ - --hash=sha256:b13eea5587e44f27f6c48588b5ad56dcb74a4f3a5f89250443c94587f3eb2ea1 \ - --hash=sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4 \ - --hash=sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f \ - --hash=sha256:b75868b675949a96ab51abc114c7163f40ff0d8f7d6d5fd63f8932fd38e9c6d7 \ - --hash=sha256:bb1bbf92317e1f35afcf3ef0450219efb3afe00be79d8664b250ac0933b9015f \ - --hash=sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542 \ - --hash=sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f \ - --hash=sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840 \ - --hash=sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83 \ - --hash=sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da \ - --hash=sha256:e851beb536a427b5df8aa7d0c7835b29a13812f41e46292ff80b2ef77327355a \ - --hash=sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464 \ - --hash=sha256:eaa85bce251feca8e4c87ce3b954aba84b8c645b90f0e6a515c00394a9f5c0e7 \ - --hash=sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d \ - --hash=sha256:f2ac8055a0c4a09b30b931100996ba49ef334c6947e7ae365cdd870416d7513e \ - --hash=sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e +dbt-artifacts-parser==0.14.0 \ + --hash=sha256:c4da799afa80a3c2c6c7e51c3b5d2681be9fdd53174b3909a3ce71cfb43d5148 \ + --hash=sha256:e983510193cacaf6e6152e41cccfafc6cece592899f306f3143dc2f8b15ea394 + # via feast (pyproject.toml) +debugpy==1.8.21 \ + --hash=sha256:0042da0ecd0a8b50dc4a54395ecd870d258d73fa18776f50c91fdcabdcad2675 \ + --hash=sha256:0fddfdc130ac6d8bfc0415b0409822fa901c8f310e5c945ac5653a0352532344 \ + --hash=sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88 \ + --hash=sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c \ + --hash=sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1 \ + --hash=sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e \ + --hash=sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9 \ + --hash=sha256:4e70cc8b5079f885cb43910924ee0aab73b8b6b2a14eff23afdd9895d86e79eb \ + --hash=sha256:4e7c2d784d78ad4b71a5f8cd7b59c167719ec8a7a0211dbb3eb1bfeda78bc4e2 \ + --hash=sha256:72b5d676c4cbfac3bac5bb01c138a4656e843f93f03ce2a5f4e394ad49fbee73 \ + --hash=sha256:84c564d8cc701d41843b29a92814c1f1bef6798724ca9d675c284ad9f6a547d7 \ + --hash=sha256:8eeab7b5462f683452c57c0126aaa5ec4e974ddb705f39ba87dff8818c8e08f9 \ + --hash=sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782 \ + --hash=sha256:9f5171176a0084b95d2ebe55a4d1f7b2a75b74c5dbec577ebd3a85c740551c36 \ + --hash=sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e \ + --hash=sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6 \ + --hash=sha256:a7fe47fd23da57b9e0bec3f4a8ee65a2dc55782455ed7f2141d75ab5d2eaeef5 \ + --hash=sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0 \ + --hash=sha256:aa9d941d6dfe3d0407e4b3ca0b9ec466030e260fbf1174094f68785680f66db6 \ + --hash=sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92 \ + --hash=sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c \ + --hash=sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176 \ + --hash=sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264 \ + --hash=sha256:e935f9dc0501be523c8a8e1853c39432e1354e9ece717ae5998fd2371c4542c3 \ + --hash=sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2 \ + --hash=sha256:f15c10084f9861b5e8414a48f18f8e4aadf51a98a59e72c16aa28281ca994672 \ + --hash=sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc \ + --hash=sha256:f843a8b08c2edeaf9b1582eed4f25441af21a297c22ff16bf76a662557aa9c9e \ + --hash=sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8 \ + --hash=sha256:ffd932c6796afadab6993ec96745918a8cb2444dbd392074f769db5ea40ab440 # via ipykernel -decorator==5.2.1 \ - --hash=sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360 \ - --hash=sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a +decorator==5.3.1 \ + --hash=sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82 \ + --hash=sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c # via ipython defusedxml==0.7.1 \ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 - # via nbconvert + # via + # docling-core + # nbconvert deltalake==0.25.5 \ --hash=sha256:0b36afba5936f74c42920c06d140535e6efc8361f659770014944d8e69fbca09 \ --hash=sha256:0ca70e824fd7bcd16aeaaf9a43800eb9dc6c5d05b7854328c4cb4a240643ef78 \ @@ -996,134 +1209,123 @@ deltalake==0.25.5 \ --hash=sha256:76be7e1ed8d13f2dc933361057a44a47a89e6112d4f5ea0a73fb510bedd96efc \ --hash=sha256:cb1c7e826fd7c3bdd3676c7471d3b551e1a3674e44cd8e3747a0017a2c0292b7 \ --hash=sha256:e8f0d24bf64455f702da8402307b22e01f91e0f76694f7c5e33c9513011e8d29 - # via feast (setup.py) + # via feast (pyproject.toml) +deprecated==1.3.1 \ + --hash=sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f \ + --hash=sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223 + # via cassandra-driver deprecation==2.1.0 \ --hash=sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff \ --hash=sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a # via python-keycloak -dill==0.3.8 \ - --hash=sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca \ - --hash=sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7 +dill==0.3.9 \ + --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ + --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c # via - # feast (setup.py) + # feast (pyproject.toml) # datasets # multiprocess -distlib==0.4.0 \ - --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ - --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d +distlib==0.4.3 \ + --hash=sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b \ + --hash=sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed # via virtualenv -docker==7.1.0 \ - --hash=sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c \ - --hash=sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0 - # via testcontainers +dnspython==2.8.0 \ + --hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \ + --hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f + # via + # feast (pyproject.toml) + # pymongo +docker==7.2.0 \ + --hash=sha256:a3f45fdeb9165e2d25d9a1d02ddf3bc70fb572cf5ebbf9b58558c22caf29b71f \ + --hash=sha256:cebb93773d334f778e023a7ee352a8d6e13ab1bd3b863a4d4a59dec897df43ac + # via + # mlflow + # testcontainers docling==2.27.0 \ --hash=sha256:1288ed75b27e33bf94daff34faffc6d11b7d7ccc13e3df84fb24adad3991f72d \ --hash=sha256:faba35662612a2c687a3a463e501d95f645316436084af92a0442ce162429a3d - # via feast (setup.py) -docling-core[chunking]==2.48.4 \ - --hash=sha256:367675c1165d0934ae498fa57ca2d27ef0468aad74dc44a5ab061f5d87882ea1 \ - --hash=sha256:d87ce3021cdae3d073ce7572a2396b69be3cde82ebf9a74d4bad1e1cdfdfd524 + # via feast (pyproject.toml) +docling-core[chunking]==2.84.0 \ + --hash=sha256:2aee881b94138234c703298066b5ed94999490323ae6a29cbe5110f9248b83dd \ + --hash=sha256:331035d5032be683a6d66d476146652491d2c75e4562e04da3f1a8d989d74bfc # via # docling # docling-ibm-models # docling-parse -docling-ibm-models==3.9.1 \ - --hash=sha256:ac6cd1c2be93437cbb5c1f1a1a4030792a38859a1655b14f25cbc8aec760c351 \ - --hash=sha256:f2d845703877a3ca8853b57775eb8e88a7a9503d4fa110500a2550b8d63d0098 +docling-ibm-models==3.13.3 \ + --hash=sha256:50fc83d244ce2dc43158e02155d1caa68374d7a169d0f34e67d7eb14f396ffe6 \ + --hash=sha256:8c8b55e80b3d20fc74d85ca49a4d064578ef75b9f7251eec42fc9df6af426218 # via docling -docling-parse==4.5.0 \ - --hash=sha256:0a1a5f3e2f11ea74ab28d9c04b9391fa4b929c4af045c16bfb0da1e377646e54 \ - --hash=sha256:217fe2466ca2723bdecbdb162ca73891c1746ec15b8d99ec203f8df3305091a5 \ - --hash=sha256:24360a0985a8f76ff99c39e533d208bb57427caf96b9ceb585090cd10558f87a \ - --hash=sha256:256019969f1edc08b051a90fe739430593aaf7cd59fb18a2e00745f18533ce43 \ - --hash=sha256:368ebdb22ec03aa29b25d2684e51c74f6e167ab6809cd7bb5bb5b97cfe21bf8c \ - --hash=sha256:41ae6a7f0139d48b9ce8e0a7c43be003e6fa9382919a7efa76153bd1cdbb5e21 \ - --hash=sha256:4c8906d076219a18f4f86b1fec4e4cc3699460e78c88a5731ead48dfbb71835a \ - --hash=sha256:52df1c5bbafe5199c090bf47eb802c2fe40173fb438200f9a7cbe401aa1eed74 \ - --hash=sha256:5688fe4281dac16e807496c0b19587e25c53a9542d12f36b3a8fb2e66de78eb2 \ - --hash=sha256:8beb4f2c79c676b93ab3a14f86586adb51c3d5a2e3c1a902186e4cd6ed0a2e45 \ - --hash=sha256:99e353ab01ac5c81318b67f42c4fc83ac4a0b5b4783bc566f19656204acf45f0 \ - --hash=sha256:9bf94bc213bedd6d880d94eface2285e9e344da5452a23b3a8d0fedecb5d3ec1 \ - --hash=sha256:9d02c43d3185f5f4a6d5aaad38e69e07bbd1f965fd62f331bd9dfc006a637604 \ - --hash=sha256:a5f0bcdd6c84acc3f3a4c1f0fb96be7e9cff7a0bdff85f2f13caa80d2a9fac8f \ - --hash=sha256:affdecc41ed18f1a82c56edac2b815535e3cc07e2b0f8ffaee7e4adfb1333f0e \ - --hash=sha256:b7c9e8954118331438eb8da6058da0e3caf12735b47a86af9521e44465bbb2d4 \ - --hash=sha256:c3dba06a3cb8797587c90f5aa10cc2c51803d8f5cd67342ea948288a30503868 \ - --hash=sha256:c49193988b56133149584fed70b176de85c95fe698849b2acf68fde9df3a93e5 \ - --hash=sha256:d0ea05741721a76cfca6559d7cac283f2b2953915745b439be0ca8557864bb33 \ - --hash=sha256:d84186662e4780375de28b1bcb18112b04bd8e6aedb787d96544cc0d687f9629 \ - --hash=sha256:d8b2a25262a09e956516c4439ae143a66a55212f0ef9945928159caf1346408f \ - --hash=sha256:da6e535463bcb19a64f3099bb73b299e1f6f49a1ef3b0b3ea4fa62e2790ad875 \ - --hash=sha256:dac5e9907cd6fd020bc1620082dacb9b99bfc9ee4001c55c4e4ce156edf3b617 \ - --hash=sha256:e78f648c3a8af5ddb7dcc30c6c4270e9d3257366396a020ad60657de98bf88f5 \ - --hash=sha256:e8b283a93860cdf43a93296e1721e25daeb8eede14417b9f188f0f52c010d6b5 \ - --hash=sha256:e9223485df491432f5549dd4566c6649ff32f54370701a004673e27e6fa94a9e \ - --hash=sha256:ee02646e7a158c9f67d8df0052b544f1240d3c28eefa4658603931c13eac4435 \ - --hash=sha256:f830409eb96b063ae9f3f4e676f760b0d9738bcb0708ba6b840b7e0c84c490bd \ - --hash=sha256:f983d65703a165b76775c3e4b2a5cade4757216eb88faf5c0c86a9b33f38549a +docling-parse==4.7.3 \ + --hash=sha256:1790e7e4ae202d67875c1c48fd6f8ef5c51d10b0c23157e4989b8673f2f31308 \ + --hash=sha256:281347b3e937c1a5ffa6f8774ee603b64a0899fe8a6885573dec7eb48a3421d8 \ + --hash=sha256:29c91f78c877ae4637011efdb478f20a571e6794be924795b3469958a6401cd6 \ + --hash=sha256:32a2a8aedc56e82e2e3337b7afb83070db1fcfde86cbd93bba80ef2e331b6c13 \ + --hash=sha256:3b04459cc97a8a4929622e341b9981e23987a63af07db599afc5e1c4d389060b \ + --hash=sha256:45ec74bda63738c72e9f3989d19ef6ea7e3b1d61328ffc68d55b1b18eb6c4002 \ + --hash=sha256:53bd45241dca228715800afa0f96fdc826f7c234e9effcd5cefc86026ff19301 \ + --hash=sha256:5936e6bcb7969c2a13f38ecc75cada3b0919422dc845e96da4b0b7b3bbc394ce \ + --hash=sha256:5fc8f4770f9f6f90ba25f52451864a64394ddb158aea3a8fdda46a208c029cf6 \ + --hash=sha256:659234b800c094525476c6a97e771cd61491201e0c9f4af8ee6d39df9758bcae \ + --hash=sha256:65e0653d9617d38e73bab069dc3e7960668ff4a6b0ff45a7635c3790eeed8a08 \ + --hash=sha256:66896bbe925073e4d48f18ec29dcd611a390d6b2378fae72125e77b020cd5664 \ + --hash=sha256:6cb4fe8c62de06b70e6b38c4bd608f41ea3e9d7154a4e05f9a3c4d8944fe3a25 \ + --hash=sha256:75522790df921b6be5d86cf26d184a4af97c1c65e2d22698a9516bc049c398cf \ + --hash=sha256:91b9fbe8209922f46bbd8c6fd1a44193a4c364ff3fa398af7bcc8aaa404567d9 \ + --hash=sha256:978e7e7032760385264896871ae87cb3a04081766cc966c57e9750ce803162ac \ + --hash=sha256:9d18a5b1f7eecabed631c497a19f19d281a0d86f24bfe5d239e3df89bdc4df32 \ + --hash=sha256:a6e0f9e18d808c87ce0fe1900c74a3496a42743f4bba7ed4dd83a0e6e168644a \ + --hash=sha256:bd23eeb479355316fe807703220439fd1de1df4ca0145a49c35f71b184f87254 \ + --hash=sha256:c5a416ae2e1761914ee8d7dbfbe3858e106c876b5a7fccaa3917c038e2f126ec \ + --hash=sha256:ca64977a19ecd580a48f22137a30470d7ccf0995b2c25a74136c6facec7c617d \ + --hash=sha256:d3d86c51f9ce35a1b40b2f410f7271d9bd5fc58e7240f4cae7fdd2cef757e671 \ + --hash=sha256:d89231aa4fba3e38b80c11beb8edc07569e934c1f3935b51f57904fefe958ba5 \ + --hash=sha256:dc32b6f25a673e41b9a8112b6b841284f60dbac9427b7848a03b435460f74aee \ + --hash=sha256:dffd19ed373b0da5cea124606b183489a8686c3d18643e94485be1bdda5713ea \ + --hash=sha256:ef691045623863624f2cb7347572d0262a53cb84940ef7dd851d9f13a2eb8833 \ + --hash=sha256:f4a93f91f97055e19cade33bb957d83f8615f1d2a0103b89827aca16b31a3e22 # via docling docutils==0.19 \ --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \ --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc # via sphinx -duckdb==1.1.3 \ - --hash=sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586 \ - --hash=sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd \ - --hash=sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e \ - --hash=sha256:09c68522c30fc38fc972b8a75e9201616b96ae6da3444585f14cf0d116008c95 \ - --hash=sha256:0a55169d2d2e2e88077d91d4875104b58de45eff6a17a59c7dc41562c73df4be \ - --hash=sha256:0ba6baa0af33ded836b388b09433a69b8bec00263247f6bf0a05c65c897108d3 \ - --hash=sha256:183ac743f21c6a4d6adfd02b69013d5fd78e5e2cd2b4db023bc8a95457d4bc5d \ - --hash=sha256:1aa3abec8e8995a03ff1a904b0e66282d19919f562dd0a1de02f23169eeec461 \ - --hash=sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce \ - --hash=sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24 \ - --hash=sha256:2141c6b28162199999075d6031b5d63efeb97c1e68fb3d797279d31c65676269 \ - --hash=sha256:252d9b17d354beb9057098d4e5d5698e091a4f4a0d38157daeea5fc0ec161670 \ - --hash=sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9 \ - --hash=sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3 \ - --hash=sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289 \ - --hash=sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1 \ - --hash=sha256:4f0e2e5a6f5a53b79aee20856c027046fba1d73ada6178ed8467f53c3877d5e0 \ - --hash=sha256:51c6d79e05b4a0933672b1cacd6338f882158f45ef9903aef350c4427d9fc898 \ - --hash=sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8 \ - --hash=sha256:5ace6e4b1873afdd38bd6cc8fcf90310fb2d454f29c39a61d0c0cf1a24ad6c8d \ - --hash=sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff \ - --hash=sha256:6411e21a2128d478efbd023f2bdff12464d146f92bc3e9c49247240448ace5a6 \ - --hash=sha256:647f17bd126170d96a38a9a6f25fca47ebb0261e5e44881e3782989033c94686 \ - --hash=sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c \ - --hash=sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31 \ - --hash=sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7 \ - --hash=sha256:80158f4c7c7ada46245837d5b6869a336bbaa28436fbb0537663fa324a2750cd \ - --hash=sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898 \ - --hash=sha256:8ee97ec337794c162c0638dda3b4a30a483d0587deda22d45e1909036ff0b739 \ - --hash=sha256:911d58c22645bfca4a5a049ff53a0afd1537bc18fedb13bc440b2e5af3c46148 \ - --hash=sha256:9c619e4849837c8c83666f2cd5c6c031300cd2601e9564b47aa5de458ff6e69d \ - --hash=sha256:9d0767ada9f06faa5afcf63eb7ba1befaccfbcfdac5ff86f0168c673dd1f47aa \ - --hash=sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4 \ - --hash=sha256:a1f83c7217c188b7ab42e6a0963f42070d9aed114f6200e3c923c8899c090f16 \ - --hash=sha256:a1fa0c502f257fa9caca60b8b1478ec0f3295f34bb2efdc10776fc731b8a6c5f \ - --hash=sha256:a30dd599b8090ea6eafdfb5a9f1b872d78bac318b6914ada2d35c7974d643640 \ - --hash=sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38 \ - --hash=sha256:a4748635875fc3c19a7320a6ae7410f9295557450c0ebab6d6712de12640929a \ - --hash=sha256:b74e121ab65dbec5290f33ca92301e3a4e81797966c8d9feef6efdf05fc6dafd \ - --hash=sha256:c443d3d502335e69fc1e35295fcfd1108f72cb984af54c536adfd7875e79cee5 \ - --hash=sha256:c5336939d83837af52731e02b6a78a446794078590aa71fd400eb17f083dda3e \ - --hash=sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378 \ - --hash=sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9 \ - --hash=sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee \ - --hash=sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32 \ - --hash=sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537 \ - --hash=sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989 \ - --hash=sha256:ecb1dc9062c1cc4d2d88a5e5cd8cc72af7818ab5a3c0f796ef0ffd60cfd3efb4 \ - --hash=sha256:eeacb598120040e9591f5a4edecad7080853aa8ac27e62d280f151f8c862afa3 \ - --hash=sha256:f549af9f7416573ee48db1cf8c9d27aeed245cb015f4b4f975289418c6cf7320 \ - --hash=sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3 \ - --hash=sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b +duckdb==1.5.5 \ + --hash=sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053 \ + --hash=sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4 \ + --hash=sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c \ + --hash=sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a \ + --hash=sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528 \ + --hash=sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c \ + --hash=sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282 \ + --hash=sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f \ + --hash=sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7 \ + --hash=sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f \ + --hash=sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c \ + --hash=sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb \ + --hash=sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433 \ + --hash=sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8 \ + --hash=sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28 \ + --hash=sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238 \ + --hash=sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece \ + --hash=sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1 \ + --hash=sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151 \ + --hash=sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2 \ + --hash=sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353 \ + --hash=sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc \ + --hash=sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079 \ + --hash=sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a \ + --hash=sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6 \ + --hash=sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe \ + --hash=sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de \ + --hash=sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0 \ + --hash=sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e \ + --hash=sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f \ + --hash=sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf \ + --hash=sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae \ + --hash=sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6 \ + --hash=sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe \ + --hash=sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366 # via ibis-framework -dunamai==1.25.0 \ - --hash=sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab \ - --hash=sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1 - # via poetry-dynamic-versioning durationpy==0.10 \ --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 @@ -1131,33 +1333,29 @@ durationpy==0.10 \ easyocr==1.7.2 \ --hash=sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c # via docling -elastic-transport==9.1.0 \ - --hash=sha256:1590e44a25b0fe208107d5e8d7dea15c070525f3ac9baafbe4cb659cd14f073d \ - --hash=sha256:369fa56874c74daae4ea10cbf40636d139f38f42bec0e006b9cd45a168ee7fce +elastic-transport==9.4.2 \ + --hash=sha256:33dc89bb1855faa8b98ae8b036405a39c562778dbcdbe4a00a2eaf753148556c \ + --hash=sha256:366f4614f4544c5fb5d780c82f57af8f30492b44a68ed20750390aa81e20c2ea # via elasticsearch -elasticsearch==9.1.1 \ - --hash=sha256:2a5c27c57ca3dd3365f665c82c9dcd8666ccfb550d5b07c688c21ec636c104e5 \ - --hash=sha256:be20acda2a97591a9a6cf4981fc398ee6fca3291cf9e7a9e52b6a9f41a46d393 - # via feast (setup.py) +elasticsearch==9.4.1 \ + --hash=sha256:1d78fdfba97a903ec35a5eb5808a74e33392b7c620bd5f742d465a3a26c27d75 \ + --hash=sha256:71ab71c3d1b20fd88c2922fb82c3277cce7ea03c160686e7b9368b265c2b4cac + # via feast (pyproject.toml) entrypoints==0.4 \ --hash=sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4 \ --hash=sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f # via altair -environs==9.5.0 \ - --hash=sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124 \ - --hash=sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9 - # via pymilvus et-xmlfile==2.0.0 \ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \ --hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54 # via openpyxl -execnet==2.1.1 \ - --hash=sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc \ - --hash=sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3 +execnet==2.1.2 \ + --hash=sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd \ + --hash=sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec # via pytest-xdist -executing==1.2.0 \ - --hash=sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc \ - --hash=sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107 +executing==2.2.1 \ + --hash=sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4 \ + --hash=sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017 # via # codeflare-sdk # stack-data @@ -1187,24 +1385,27 @@ faiss-cpu==1.10.0 \ --hash=sha256:e02af3696a6b9e1f9072e502f48095a305de2163c42ceb1f6f6b1db9e7ffe574 \ --hash=sha256:e71f7e24d5b02d3a51df47b77bd10f394a1b48a8331d5c817e71e9e27a8a75ac \ --hash=sha256:f71c5860c860df2320299f9e4f2ca1725beb559c04acb1cf961ed24e6218277a - # via feast (setup.py) -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 # via - # feast (setup.py) + # feast (pyproject.toml) + # milvus-lite +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e + # via + # feast (pyproject.toml) # fastapi-mcp + # mlflow-skinny fastapi-mcp==0.4.0 \ --hash=sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec \ --hash=sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d - # via feast (setup.py) -fastjsonschema==2.21.2 \ - --hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \ - --hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de + # via feast (pyproject.toml) +fastjsonschema==2.22.1 \ + --hash=sha256:0b83d1ce8d7845b959dcb20e1a5c3c8883b6541d9c52ab02cce5166b75ec805f \ + --hash=sha256:cf377ff5c9a6f4f3125fb35f75a2c5767bd824ffbcf62c209a93cd48d1453999 # via nbformat -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 +filelock==3.32.0 \ + --hash=sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402 \ + --hash=sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3 # via # datasets # huggingface-hub @@ -1217,6 +1418,62 @@ filetype==1.2.0 \ --hash=sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb \ --hash=sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25 # via docling +flask==3.1.3 \ + --hash=sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb \ + --hash=sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c + # via mlflow +fonttools==4.63.0 \ + --hash=sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69 \ + --hash=sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c \ + --hash=sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac \ + --hash=sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096 \ + --hash=sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d \ + --hash=sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68 \ + --hash=sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616 \ + --hash=sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78 \ + --hash=sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f \ + --hash=sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b \ + --hash=sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b \ + --hash=sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02 \ + --hash=sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d \ + --hash=sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f \ + --hash=sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8 \ + --hash=sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272 \ + --hash=sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49 \ + --hash=sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419 \ + --hash=sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001 \ + --hash=sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03 \ + --hash=sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196 \ + --hash=sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9 \ + --hash=sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e \ + --hash=sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5 \ + --hash=sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007 \ + --hash=sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380 \ + --hash=sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8 \ + --hash=sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27 \ + --hash=sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40 \ + --hash=sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e \ + --hash=sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0 \ + --hash=sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263 \ + --hash=sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb \ + --hash=sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94 \ + --hash=sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b \ + --hash=sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6 \ + --hash=sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579 \ + --hash=sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4 \ + --hash=sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59 \ + --hash=sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0 \ + --hash=sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e \ + --hash=sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be \ + --hash=sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd \ + --hash=sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18 \ + --hash=sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22 \ + --hash=sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0 \ + --hash=sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b \ + --hash=sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b \ + --hash=sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af \ + --hash=sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745 + # via matplotlib fqdn==1.5.1 \ --hash=sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f \ --hash=sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014 @@ -1355,25 +1612,36 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -fsspec[http]==2024.9.0 \ - --hash=sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8 \ - --hash=sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b +fsspec[http]==2026.6.0 \ + --hash=sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1 \ + --hash=sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a # via - # feast (setup.py) + # feast (pyproject.toml) # dask # datasets # huggingface-hub + # pyiceberg # ray # torch -geomet==0.2.1.post1 \ - --hash=sha256:91d754f7c298cbfcabd3befdb69c641c27fe75e808b27aa55028605761d17e95 \ - --hash=sha256:a41a1e336b381416d6cbed7f1745c848e91defaa4d4c1bdc1312732e46ffad2b - # via cassandra-driver -google-api-core[grpc]==2.26.0 \ - --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ - --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 - # via - # feast (setup.py) +geomet==1.1.0 \ + --hash=sha256:4372fe4e286a34acc6f2e9308284850bd8c4aa5bc12065e2abbd4995900db12f \ + --hash=sha256:51e92231a0ef6aaa63ac20c443377ba78a303fd2ecd179dc3567de79f3c11605 + # via + # cassandra-driver + # scylla-driver +gitdb==4.0.12 \ + --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ + --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf + # via gitpython +gitpython==3.1.57 \ + --hash=sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf \ + --hash=sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488 + # via mlflow-skinny +google-api-core[grpc]==2.33.0 \ + --hash=sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb \ + --hash=sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc + # via + # feast (pyproject.toml) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -1382,10 +1650,11 @@ google-api-core[grpc]==2.26.0 \ # google-cloud-storage # opencensus # pandas-gbq -google-auth==2.41.1 \ - --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ - --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 +google-auth[pyopenssl]==2.56.2 \ + --hash=sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6 \ + --hash=sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051 # via + # databricks-sdk # google-api-core # google-auth-oauthlib # google-cloud-bigquery @@ -1394,165 +1663,177 @@ google-auth==2.41.1 \ # google-cloud-core # google-cloud-datastore # google-cloud-storage - # kubernetes # pandas-gbq # pydata-google-auth -google-auth-oauthlib==1.2.2 \ - --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ - --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-auth-oauthlib==1.4.0 \ + --hash=sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2 \ + --hash=sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 # via # pandas-gbq # pydata-google-auth -google-cloud-bigquery[pandas]==3.38.0 \ - --hash=sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520 \ - --hash=sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6 +google-cloud-bigquery[pandas]==3.42.2 \ + --hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \ + --hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq -google-cloud-bigquery-storage==2.33.1 \ - --hash=sha256:24952aba0d69acc4d6bfbdc7a09dddbb728496b1780bd224f1056361a1b51044 \ - --hash=sha256:3fd25bef364ac5fb9bbd6560f0dd11b90b1845883df8e0a8c706ad53d00fc23b - # via feast (setup.py) -google-cloud-bigtable==2.33.0 \ - --hash=sha256:8d25c73dd41cea9436d14c9f03b91f94b05ff4afd47ce9a575bf18f8e6985747 \ - --hash=sha256:cd4f7178cde30113900318ec7e2fd793116752c97e0922c185c69b645a52656d - # via feast (setup.py) -google-cloud-core==2.4.3 \ - --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ - --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e +google-cloud-bigquery-storage==2.39.0 \ + --hash=sha256:8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 \ + --hash=sha256:d5afd90ad06cf24d9167316cca70ab5b344e880fc13031d7392aa78ee76b8bb6 + # via feast (pyproject.toml) +google-cloud-bigtable==2.41.0 \ + --hash=sha256:36da0fc730952bfe48ae91dee11015459c9046fa1499d0efb73a61bbb6edbe9b \ + --hash=sha256:d0c424bae6238eb16ffc0922be79da00d20d23e0750ea46742ffd4542c50fd26 + # via feast (pyproject.toml) +google-cloud-core==2.6.0 \ + --hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \ + --hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.21.0 \ - --hash=sha256:eee454dd4a55f5b327f9f344928ff1a09a6f77c23d5e3d908ad31a13cc2f4073 \ - --hash=sha256:f303f27cd1983383f20bd227019cd8a7897419e0ec6b878367c58c66245f9d9b - # via feast (setup.py) +google-cloud-datastore==2.26.0 \ + --hash=sha256:1ac4882f033fcb90bd75d1171b46400ecd5def23768598843d9c041e0c0b4a6a \ + --hash=sha256:1bde5694308468da5c932153a6b0331d5b54e1c21de366b6a75ddb0000fd5f4b + # via feast (pyproject.toml) google-cloud-storage==2.19.0 \ --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via feast (setup.py) -google-crc32c==1.7.1 \ - --hash=sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db \ - --hash=sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337 \ - --hash=sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c \ - --hash=sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242 \ - --hash=sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e \ - --hash=sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472 \ - --hash=sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194 \ - --hash=sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3 \ - --hash=sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582 \ - --hash=sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d \ - --hash=sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6 \ - --hash=sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82 \ - --hash=sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06 \ - --hash=sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349 \ - --hash=sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a \ - --hash=sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d \ - --hash=sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48 \ - --hash=sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb \ - --hash=sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315 \ - --hash=sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589 \ - --hash=sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76 \ - --hash=sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65 \ - --hash=sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6 \ - --hash=sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127 \ - --hash=sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53 \ - --hash=sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603 \ - --hash=sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35 \ - --hash=sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9 \ - --hash=sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638 \ - --hash=sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9 \ - --hash=sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77 \ - --hash=sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14 \ - --hash=sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b \ - --hash=sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb + # via feast (pyproject.toml) +google-crc32c==1.8.0 \ + --hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \ + --hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \ + --hash=sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff \ + --hash=sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 \ + --hash=sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411 \ + --hash=sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a \ + --hash=sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 \ + --hash=sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb \ + --hash=sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa \ + --hash=sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962 \ + --hash=sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215 \ + --hash=sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b \ + --hash=sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27 \ + --hash=sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113 \ + --hash=sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f \ + --hash=sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f \ + --hash=sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d \ + --hash=sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 \ + --hash=sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092 \ + --hash=sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7 \ + --hash=sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2 \ + --hash=sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93 \ + --hash=sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8 \ + --hash=sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21 \ + --hash=sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 \ + --hash=sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2 \ + --hash=sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc \ + --hash=sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454 \ + --hash=sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2 \ + --hash=sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733 \ + --hash=sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697 \ + --hash=sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651 \ + --hash=sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c # via # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 +google-resumable-media==2.10.0 \ + --hash=sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c \ + --hash=sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.70.0 \ - --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ - --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +googleapis-common-protos[grpc]==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # grpc-google-iam-v1 # grpcio-status +graphene==3.4.3 \ + --hash=sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa \ + --hash=sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71 + # via mlflow +graphql-core==3.2.11 \ + --hash=sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0 \ + --hash=sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802 + # via + # graphene + # graphql-relay +graphql-relay==3.2.0 \ + --hash=sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c \ + --hash=sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5 + # via graphene great-expectations==0.18.8 \ --hash=sha256:ab41cfa3de829a4f77bdcd4a23244684cbb67fdacc734d38910164cd02ec95b6 \ --hash=sha256:c1205bede593f679e22e0b3826d6ae1623c439cafd553f9f0bc2b0fd441f6ed9 - # via feast (setup.py) -grpc-google-iam-v1==0.14.2 \ - --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ - --hash=sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20 + # via feast (pyproject.toml) +grpc-google-iam-v1==0.14.4 \ + --hash=sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038 \ + --hash=sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 # via google-cloud-bigtable -grpcio==1.62.3 \ - --hash=sha256:059444f0ed5dba73ab7dd0ee7e8e6b606df4130d2b0a9f010f84da4ab9f6c2d8 \ - --hash=sha256:114f2a865886ff33f85d70670e971fe0e3d252a1209656fefa5470286e3fcc76 \ - --hash=sha256:13571a5b868dcc308a55d36669a2d17d9dcd6ec8335213f6c49cc68da7305abe \ - --hash=sha256:1ac0944e9e3ee3e20825226d1e17985e9f88487055c475986cf0922a7d806d8a \ - --hash=sha256:1de3d04d9a4ec31ebe848ae1fe61e4cbc367fb9495cbf6c54368e60609a998d9 \ - --hash=sha256:216740723fc5971429550c374a0c039723b9d4dcaf7ba05227b7e0a500b06417 \ - --hash=sha256:25cd75dc73c5269932413e517be778640402f18cf9a81147e68645bd8af18ab0 \ - --hash=sha256:325c56ce94d738c31059cf91376f625d3effdff8f85c96660a5fd6395d5a707f \ - --hash=sha256:3737e5ef0aa0fcdfeaf3b4ecc1a6be78b494549b28aec4b7f61b5dc357f7d8be \ - --hash=sha256:377babc817e8b4186aed7ed56e832867c513e4e9b6c3503565c344ffdef440d4 \ - --hash=sha256:3fb7d966a976d762a31346353a19fce4afcffbeda3027dd563bc8cb521fcf799 \ - --hash=sha256:43670a25b752b7ed960fcec3db50ae5886dc0df897269b3f5119cde9b731745f \ - --hash=sha256:4439bbd759636e37b66841117a66444b454937e27f0125205d2d117d7827c643 \ - --hash=sha256:454a6aed4ebd56198d37e1f3be6f1c70838e33dd62d1e2cea12f2bcb08efecc5 \ - --hash=sha256:4c9c1502c76cadbf2e145061b63af077b08d5677afcef91970d6db87b30e2f8b \ - --hash=sha256:4dab8b64c438e19c763a6332b55e5efdbecfb7c55ae59a42c38c81ed27955fa5 \ - --hash=sha256:56757d3e4cf5d4b98a30f2c5456151607261c891fa2298a4554848dcbf83083d \ - --hash=sha256:57823dc7299c4f258ae9c32fd327d29f729d359c34d7612b36e48ed45b3ab8d0 \ - --hash=sha256:582bd03e9c3d1bd1162eb51fa0f1a35633d66e73f4f36702d3b8484a8b45eda7 \ - --hash=sha256:620165df24aae3d5b3e84cb8dd6b98f6ed49aed04126186bbf43061e301d6a21 \ - --hash=sha256:646c14e9f3356d3f34a65b58b0f8d08daa741ba1d4fcd4966b79407543332154 \ - --hash=sha256:668211f3699bbee4deaf1d6e6b8df59328bf63f077bf2dc9b8bfa4a17df4a279 \ - --hash=sha256:6be243f3954b0ca709f56f9cae926c84ac96e1cce19844711e647a1f1db88b99 \ - --hash=sha256:6da20a1ae010a988bc4ed47850f1122de0a88e18cd2f901fcf56007be1fc6c30 \ - --hash=sha256:7349cd7445ac65fbe1b744dcab9cc1ec02dae2256941a2e67895926cbf7422b4 \ - --hash=sha256:74f3fc9b93290e58264844f5bc46df4c58a94c4287a277dbcf75344fc6c37ca4 \ - --hash=sha256:75a4e9ac7ff185cad529f35934c5d711b88aca48b90c70e195f5657da50ce321 \ - --hash=sha256:7b33c1807d4ac564a3027d06f21a2220c116ceacaaef614deb96b3341ee58896 \ - --hash=sha256:807176971c504c598976f5a9ea62363cffbbbb6c7509d9808c2342b020880fa2 \ - --hash=sha256:80a82fdee14dc27e9299248b7aabd5a8739a1cf6b76c78aa2b848158b44a99d5 \ - --hash=sha256:81b7c121c4e52a0749bf0759185b8d5cfa48a786cd7d411cdab08269813e0aab \ - --hash=sha256:8257cc9e55fb0e2149a652d9dc14c023720f9e73c9145776e07c97e0a553922e \ - --hash=sha256:8a5f00b2508937952d23a1767739e95bbbe1120f8a66d10187d5e971d56bb55c \ - --hash=sha256:8ae2e7a390b2cdd2a95d3bf3b3385245eeb48a5e853943cb46139666462c2d1a \ - --hash=sha256:940459d81685549afdfe13a6de102c52ea4cdda093477baa53056884aadf7c48 \ - --hash=sha256:9c4aae4e683776c319169d87e7891b67b75e3f1c0beeb877902ea148b0585164 \ - --hash=sha256:9d5f8e0050a179b3bce9189b522dc91008d44f08c757a7c310e0fd06b4d3d147 \ - --hash=sha256:a1b85d35a7d9638c03321dfe466645b87e23c30df1266f9e04bbb5f44e7579a9 \ - --hash=sha256:a82410d7620c07cb32624e38f2a106980564dfef9dbe78f5b295cda9ef217c03 \ - --hash=sha256:abfe64811177e681edc81d9d9d1bd23edc5f599bd9846650864769264ace30cd \ - --hash=sha256:ac9783d5679c8da612465168c820fd0b916e70ec5496c840bddba0be7f2d124c \ - --hash=sha256:b033d50bd41e506e3b579775f54a30c16c222e0d88847ac8098d2eca2a7454cc \ - --hash=sha256:b097347441b86a8c3ad9579abaf5e5f7f82b1d74a898f47360433b2bca0e4536 \ - --hash=sha256:b708401ede2c4cb8943e8a713988fcfe6cbea105b07cd7fa7c8a9f137c22bddb \ - --hash=sha256:bd900e666bb68fff49703084be14407cd73b8a5752a7590cea98ec22de24fb5d \ - --hash=sha256:c118cfc80e2402a5595be36e9245ffd9b0e146f426cc40bdf60015bf183f8373 \ - --hash=sha256:c175b252d063af388523a397dbe8edbc4319761f5ee892a8a0f5890acc067362 \ - --hash=sha256:c8bb1a7aa82af6c7713cdf9dcb8f4ea1024ac7ce82bb0a0a82a49aea5237da34 \ - --hash=sha256:c95a0b76a44c548e6bd8c5f7dbecf89c77e2e16d3965be817b57769c4a30bea2 \ - --hash=sha256:e202e3f963480ca067a261179b1ac610c0f0272cb4a7942d11b7e2b3fc99c3aa \ - --hash=sha256:e9ffdb7bc9ccd56ec201aec3eab3432e1e820335b5a16ad2b37e094218dcd7a6 \ - --hash=sha256:ea7ca66a58421411c6486fa5015fe7704e2816ff0b4ec4fb779ad5e1cbbdabf3 \ - --hash=sha256:f2ff8ac447765e173842b554b31307b98b3bb1852710903ebb936e7efb7df6e5 \ - --hash=sha256:f5def814c5a4c90c8fe389c526ab881f4a28b7e239b23ed8e02dd02934dfaa1a - # via - # feast (setup.py) +grpcio==1.83.0 \ + --hash=sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df \ + --hash=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 \ + --hash=sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5 \ + --hash=sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df \ + --hash=sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930 \ + --hash=sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf \ + --hash=sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9 \ + --hash=sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33 \ + --hash=sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da \ + --hash=sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03 \ + --hash=sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa \ + --hash=sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223 \ + --hash=sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9 \ + --hash=sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4 \ + --hash=sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf \ + --hash=sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881 \ + --hash=sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af \ + --hash=sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5 \ + --hash=sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0 \ + --hash=sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727 \ + --hash=sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb \ + --hash=sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a \ + --hash=sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735 \ + --hash=sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16 \ + --hash=sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49 \ + --hash=sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1 \ + --hash=sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24 \ + --hash=sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b \ + --hash=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f \ + --hash=sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57 \ + --hash=sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf \ + --hash=sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f \ + --hash=sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c \ + --hash=sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969 \ + --hash=sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd \ + --hash=sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c \ + --hash=sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b \ + --hash=sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61 \ + --hash=sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404 \ + --hash=sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617 \ + --hash=sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40 \ + --hash=sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45 \ + --hash=sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc \ + --hash=sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c \ + --hash=sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745 \ + --hash=sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45 \ + --hash=sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c \ + --hash=sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0 \ + --hash=sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d \ + --hash=sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9 \ + --hash=sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8 + # via + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-datastore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking @@ -1560,283 +1841,326 @@ grpcio==1.62.3 \ # grpcio-status # grpcio-testing # grpcio-tools - # ikvpy + # milvus-lite # pymilvus # qdrant-client # ray -grpcio-health-checking==1.62.3 \ - --hash=sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93 \ - --hash=sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d - # via feast (setup.py) -grpcio-reflection==1.62.3 \ - --hash=sha256:a48ef37df81a3bada78261fc92ef382f061112f989d1312398b945cc69838b9c \ - --hash=sha256:cb84682933c400bddf94dd94f928d1c6570f500b6dd255973d4bfb495b82585f - # via feast (setup.py) -grpcio-status==1.62.3 \ - --hash=sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485 \ - --hash=sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8 - # via - # google-api-core - # ikvpy -grpcio-testing==1.62.3 \ - --hash=sha256:06a4d7eb30d22f91368aa7f48bfc33563da13b9d951314455ca8c9c987fb75bb \ - --hash=sha256:f63577f28aaa95ea525124a0fd63c3429d71f769f4179b13f5e6cbc54979bfab - # via feast (setup.py) -grpcio-tools==1.62.3 \ - --hash=sha256:0a52cc9444df978438b8d2332c0ca99000521895229934a59f94f37ed896b133 \ - --hash=sha256:0a8c0c4724ae9c2181b7dbc9b186df46e4f62cb18dc184e46d06c0ebeccf569e \ - --hash=sha256:0cb3a3436ac119cbd37a7d3331d9bdf85dad21a6ac233a3411dff716dcbf401e \ - --hash=sha256:11c625eebefd1fd40a228fc8bae385e448c7e32a6ae134e43cf13bbc23f902b7 \ - --hash=sha256:11f363570dea661dde99e04a51bd108a5807b5df32a6f8bdf4860e34e94a4dbf \ - --hash=sha256:141d028bf5762d4a97f981c501da873589df3f7e02f4c1260e1921e565b376fa \ - --hash=sha256:1c989246c2aebc13253f08be32538a4039a64e12d9c18f6d662d7aee641dc8b5 \ - --hash=sha256:1da38070738da53556a4b35ab67c1b9884a5dd48fa2f243db35dc14079ea3d0c \ - --hash=sha256:27cd9ef5c5d68d5ed104b6dcb96fe9c66b82050e546c9e255716903c3d8f0373 \ - --hash=sha256:2e02d3b96f2d0e4bab9ceaa30f37d4f75571e40c6272e95364bff3125a64d184 \ - --hash=sha256:2f968b049c2849540751ec2100ab05e8086c24bead769ca734fdab58698408c1 \ - --hash=sha256:350a80485e302daaa95d335a931f97b693e170e02d43767ab06552c708808950 \ - --hash=sha256:3eae6ea76d62fcac091e1f15c2dcedf1dc3f114f8df1a972a8a0745e89f4cf61 \ - --hash=sha256:47a5c093ab256dec5714a7a345f8cc89315cb57c298b276fa244f37a0ba507f0 \ - --hash=sha256:5782883a27d3fae8c425b29a9d3dcf5f47d992848a1b76970da3b5a28d424b26 \ - --hash=sha256:6a56d344b0bab30bf342a67e33d386b0b3c4e65868ffe93c341c51e1a8853ca5 \ - --hash=sha256:6c3064610826f50bd69410c63101954676edc703e03f9e8f978a135f1aaf97c1 \ - --hash=sha256:703f46e0012af83a36082b5f30341113474ed0d91e36640da713355cd0ea5d23 \ - --hash=sha256:710fecf6a171dcbfa263a0a3e7070e0df65ba73158d4c539cec50978f11dad5d \ - --hash=sha256:7c7136015c3d62c3eef493efabaf9e3380e3e66d24ee8e94c01cb71377f57833 \ - --hash=sha256:7cc83023acd8bc72cf74c2edbe85b52098501d5b74d8377bfa06f3e929803492 \ - --hash=sha256:7f2483ea232bd72d98a6dc6d7aefd97e5bc80b15cd909b9e356d6f3e326b6e43 \ - --hash=sha256:7ff7d58a45b75df67d25f8f144936a3e44aabd91afec833ee06826bd02b7fbe7 \ - --hash=sha256:8ad0473af5544f89fc5a1ece8676dd03bdf160fb3230f967e05d0f4bf89620e3 \ - --hash=sha256:8c5d22b252dcef11dd1e0fbbe5bbfb9b4ae048e8880d33338215e8ccbdb03edc \ - --hash=sha256:8e62cc7164b0b7c5128e637e394eb2ef3db0e61fc798e80c301de3b2379203ed \ - --hash=sha256:962c84b4da0f3b14b3cdb10bc3837ebc5f136b67d919aea8d7bb3fd3df39528a \ - --hash=sha256:ace43b26d88a58dcff16c20d23ff72b04d0a415f64d2820f4ff06b1166f50557 \ - --hash=sha256:b47d0dda1bdb0a0ba7a9a6de88e5a1ed61f07fad613964879954961e36d49193 \ - --hash=sha256:b77f9f9cee87cd798f0fe26b7024344d1b03a7cd2d2cba7035f8433b13986325 \ - --hash=sha256:b881fd9505a84457e9f7e99362eeedd86497b659030cf57c6f0070df6d9c2b9b \ - --hash=sha256:bfda6ee8990997a9df95c5606f3096dae65f09af7ca03a1e9ca28f088caca5cf \ - --hash=sha256:c3a1ac9d394f8e229eb28eec2e04b9a6f5433fa19c9d32f1cb6066e3c5114a1d \ - --hash=sha256:c8ad5cce554e2fcaf8842dee5d9462583b601a3a78f8b76a153c38c963f58c10 \ - --hash=sha256:ca246dffeca0498be9b4e1ee169b62e64694b0f92e6d0be2573e65522f39eea9 \ - --hash=sha256:ca4f5eeadbb57cf03317d6a2857823239a63a59cc935f5bd6cf6e8b7af7a7ecc \ - --hash=sha256:d102b9b21c4e1e40af9a2ab3c6d41afba6bd29c0aa50ca013bf85c99cdc44ac5 \ - --hash=sha256:db3bc9fa39afc5e4e2767da4459df82b095ef0cab2f257707be06c44a1c2c3e5 \ - --hash=sha256:dc9ad9950119d8ae27634e68b7663cc8d340ae535a0f80d85a55e56a6973ab1f \ - --hash=sha256:e02d7c1a02e3814c94ba0cfe43d93e872c758bd8fd5c2797f894d0c49b4a1dfc \ - --hash=sha256:e0898d412a434e768a0c7e365acabe13ff1558b767e400936e26b5b6ed1ee51f \ - --hash=sha256:e18e15287c31baf574fcdf8251fb7f997d64e96c6ecf467906e576da0a079af6 \ - --hash=sha256:ec279dcf3518201fc592c65002754f58a6b542798cd7f3ecd4af086422f33f29 \ - --hash=sha256:ec6fbded0c61afe6f84e3c2a43e6d656791d95747d6d28b73eff1af64108c434 \ - --hash=sha256:eec73a005443061f4759b71a056f745e3b000dc0dc125c9f20560232dfbcbd14 \ - --hash=sha256:f3d812daffd0c2d2794756bd45a353f89e55dc8f91eb2fc840c51b9f6be62667 \ - --hash=sha256:f4b1615adf67bd8bb71f3464146a6f9949972d06d21a4f5e87e73f6464d97f57 \ - --hash=sha256:f6831fdec2b853c9daa3358535c55eed3694325889aa714070528cf8f92d7d6d - # via feast (setup.py) +grpcio-health-checking==1.81.1 \ + --hash=sha256:cbc6a4171825ec64389de2f062d296ba129a5c27eefd0dd55fa837909184bdf9 \ + --hash=sha256:ecc61480e25058a4a04e11e4ab6900ad7439b32e60a8ce4ece7d9f219221c85d + # via feast (pyproject.toml) +grpcio-reflection==1.81.1 \ + --hash=sha256:3d7160000f5fdd0e241f9b1a3d402f15ebcd5f281be105b374a025f38c6434a8 \ + --hash=sha256:d82fc4ac39dd5dcfd63e577075f6a68eac4a765e66a453914bbf57caf35665d0 + # via feast (pyproject.toml) +grpcio-status==1.83.0 \ + --hash=sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae \ + --hash=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 + # via google-api-core +grpcio-testing==1.81.1 \ + --hash=sha256:69792db25781cefd2479d12285ca6bebe589e2aa1d64b4b8d18cf35f58c6b810 \ + --hash=sha256:f381121d7203f4380b1c01e6c8f15aa220bafdc372e8ed82eddf79a16149abcc + # via feast (pyproject.toml) +grpcio-tools==1.81.1 \ + --hash=sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a \ + --hash=sha256:136e90906af0df51ad929713244ba812d0dbb1844b4f467d5d86bdb054698f90 \ + --hash=sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4 \ + --hash=sha256:1d602b410b2b2addc434cace9ce4fe2035974a3078228f98ffa049a5c90acc2f \ + --hash=sha256:204de03b539a4b08772c6553b92bcc112cbc965e0ac22f909f6d133b8ac33a8c \ + --hash=sha256:21bb3ba90e6d8df1ff663d4ee39a4e5b25a64e8ed4902476ca9ded0954d3917a \ + --hash=sha256:2baa7e735f35b2a648144c03348a126097b13e101d3c242d5edb6ac91437ccbe \ + --hash=sha256:2c306c307f8f74cddc4056fdbb6f1da55de087a21120efbd02bd915daa5a52fd \ + --hash=sha256:3389e705460efa3f3758141ba5520e6743b131c9576197c944fb9cbe49048126 \ + --hash=sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5 \ + --hash=sha256:3c8611d6e4e859ac5373422ef27c4b7540cf98c9991c9abc6722613ef72b13aa \ + --hash=sha256:3d604b4fd114b79ebb9f865bf3e04fd3ae93c704e1fad96f7fd03b0865c263b7 \ + --hash=sha256:3dca56016d90a710c4d9861bae793dc089c1430a90c79ce672e948ddb65fa539 \ + --hash=sha256:43baf71dc60fd653062da2e95e95c73b35dd130be8f9fa3d544c3af3f808a290 \ + --hash=sha256:43c528655b226375013036692d8db4cd59060c1f41dd62c77f4d17b69f6ce828 \ + --hash=sha256:53ef76cc3b0493ff734a5e8c39d5b519e1822236fcccdfe7677c5e1efd767761 \ + --hash=sha256:690e6dcaa8b8a7886ce206ba344e2127211597e1a1ddab73df9f3d80c8f6707e \ + --hash=sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c \ + --hash=sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e \ + --hash=sha256:7746e508d4239a02f7e93638be5bc0ebb0120ddb796f7506aaae9d47a4599d97 \ + --hash=sha256:7d168ea26390717d0462c0d0408331dc98a60fc7f7e6118afac9b73f5a66d87c \ + --hash=sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3 \ + --hash=sha256:801d9d8ab5cddf8f8e064225292f0713427011252a07828a6b54e2ed64d534de \ + --hash=sha256:8161f398f957a376cae7385ea7c8684f439d460ef702b528912da3bcb31fc515 \ + --hash=sha256:82740248eb6f3b6a38988cb5e64adb7303af9ea5cb4197c8ed08c1fabc767440 \ + --hash=sha256:87b25ca0e27373a4a32a629a4ba976f5764b9887dd50d6fe017d38009a0363e8 \ + --hash=sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593 \ + --hash=sha256:8a17d8ceeb6a855fadf39f5171c80a382d97c4db98d5943eca553497fdebf84b \ + --hash=sha256:9b6ba8a72cfda576508701a7c0bbeebe6f6f9843320d4f12e74efd19ddccd965 \ + --hash=sha256:9d383724bcd67244b6def9e9164c640ee9380c0b7534ee7545a6fb0022a59afe \ + --hash=sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1 \ + --hash=sha256:a28d231455ab6e3558299f7d831a73c8be8ee6b7ec614ecf39eb50c0ed15767f \ + --hash=sha256:a882382507bb5ec6d7edc9648053dfd3bc8f9285cde56a6fa9b9a83b4bd07f1c \ + --hash=sha256:a987c85dcbe1b32066d7acd46266d1a428aecbd629331bf5b853e74c835bf876 \ + --hash=sha256:a9c6fcc68c9d5a208967bfe4fd3224d3c3be9a950c3e827e8f4b17e15c2dc555 \ + --hash=sha256:ac47a9ea1224df8b653072614e6f0207e9fbfe63fdabaa5918a60ca5fc931b88 \ + --hash=sha256:ad7a997c07bd345e84842e60561e7e2cc090ce6c4e1d2f0407e31b85b40fc49a \ + --hash=sha256:b62d254c214faa3773eac709376ae25cf7abff1a76ba5fc4dbcd7b14fc4e4ae6 \ + --hash=sha256:b6bd163ece4535726e5292b845ed80ae9b2cae73ba091c7d6c66033c430e3857 \ + --hash=sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a \ + --hash=sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e \ + --hash=sha256:bd0b68dc76b10b3384b9b6e9f59202b83dcaafd8098eb644759a69316686acf8 \ + --hash=sha256:bd6c3bf3ea6a61eb58c54368d72ada591f2a270f3a31a32e8536e773337e76d9 \ + --hash=sha256:bdbdc927be2e0ea13c32564a72ee31d712a716fb6f8c0d53d37a77d8277c272c \ + --hash=sha256:cb08172b7b629e75cb33866928d319a3196540a725eaab628ba721007140f1af \ + --hash=sha256:cc410b621dd85193766c12dca2e238696199a27a65d2b31b6f0a4c6c0043ff26 \ + --hash=sha256:eac4bb645ceff0c147cc720a40ae68f97427eaafb4968e866dd8fcc20d3d4831 \ + --hash=sha256:f3eb15849979ca7bb864ce81a74d68b0f225a7f111ed3fe212bfc08cf9812b10 \ + --hash=sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec \ + --hash=sha256:f8cb64f87c45ccca8234fa47e6b21f09e43801ff11b556deecb461b3b3e9f292 \ + --hash=sha256:fc3d2a41a7a4467fa03b391394fffada9291fe8feebc8679b526f6bc36942b25 + # via feast (pyproject.toml) gunicorn==23.0.0 \ --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec # via - # feast (setup.py) + # feast (pyproject.toml) + # mlflow # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore + # httpcore2 # uvicorn -h2==4.3.0 \ - --hash=sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1 \ - --hash=sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd +h2==4.4.0 \ + --hash=sha256:46b551bdcdc7e83cf5c04d0bf93badb8a939bd2287d9fee1abb23a445b9e0580 \ + --hash=sha256:6acffe1aeab79098d7eb0f8385c1add11f2c7a94815f6fa2b7060eeddee3d87c # via httpx happybase==1.3.0 \ --hash=sha256:43b6275d2865fc1364680a03f085491cd85d8b84db3c5aa94d25186685dfd87e \ --hash=sha256:f2644cf1ef9d662fbe6f709fcfd66bf13e949f3efd4745a3230cf5f904fb7839 - # via feast (setup.py) -hazelcast-python-client==5.5.0 \ - --hash=sha256:c797c23c219971d225f8590f6359692c14059c26baa15c2762c95667ae38b90a \ - --hash=sha256:dc8d7c1f494e02994238759ad45a9d9d54a569b8e12f198a0efa6e192774b16d - # via feast (setup.py) -hf-xet==1.1.10 \ - --hash=sha256:0a0005fd08f002180f7a12d4e13b22be277725bc23ed0529f8add5c7a6309c06 \ - --hash=sha256:408aef343800a2102374a883f283ff29068055c111f003ff840733d3b715bb97 \ - --hash=sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045 \ - --hash=sha256:686083aca1a6669bc85c21c0563551cbcdaa5cf7876a91f3d074a030b577231d \ - --hash=sha256:6b6bceb6361c80c1cc42b5a7b4e3efd90e64630bcf11224dcac50ef30a47e435 \ - --hash=sha256:71081925383b66b24eedff3013f8e6bbd41215c3338be4b94ba75fd75b21513b \ - --hash=sha256:eae7c1fc8a664e54753ffc235e11427ca61f4b0477d757cc4eb9ae374b69f09c \ - --hash=sha256:f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f + # via feast (pyproject.toml) +hazelcast-python-client==5.7.0 \ + --hash=sha256:6a218295af38730bd1781a6b2fefbf24851f1d6abb472f0bfe927a281b3f83b7 \ + --hash=sha256:79e7c6e77453e53af29bb2454504e1b8d3a422527813116ee44379c18ee9849f + # via feast (pyproject.toml) +hf-xet==1.5.2 \ + --hash=sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed \ + --hash=sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d \ + --hash=sha256:4a5ecb9cda8512ba2aa8ee5d37c87a1422992165892d653098c7b90247481c3b \ + --hash=sha256:580e59e29bf37aece1f2b68537de1e3fb04f43a23d910dcf6f128280b5bfbba4 \ + --hash=sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f \ + --hash=sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47 \ + --hash=sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025 \ + --hash=sha256:8764488197c1d7b1378c8438c18d2eea902e150dbca0b0f0d2d32603fb9b5576 \ + --hash=sha256:8d7446f72abbf7e01ca5ff131786bc2e74a56393462c17a6bf1e303fbab81db4 \ + --hash=sha256:bee28c619622d36968056532fd49cf2b35ca75099b1d616c31a618a893491380 \ + --hash=sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097 \ + --hash=sha256:d6f9c58549407b84b9a5383afd68db0acc42345326a3159990b36a5ca8a20e4e \ + --hash=sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c \ + --hash=sha256:e396ab0faf6298199ad7a95305c3ca8498cb825978a6485be6d00587ee4ec577 \ + --hash=sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65 \ + --hash=sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799 \ + --hash=sha256:fd3add255549e8ef58fa35b2e42dc016961c050600444e7d77d030ba6b57120e # via huggingface-hub -hiredis==2.4.0 \ - --hash=sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027 \ - --hash=sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577 \ - --hash=sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603 \ - --hash=sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869 \ - --hash=sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228 \ - --hash=sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79 \ - --hash=sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb \ - --hash=sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33 \ - --hash=sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77 \ - --hash=sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9 \ - --hash=sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd \ - --hash=sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a \ - --hash=sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8 \ - --hash=sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9 \ - --hash=sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901 \ - --hash=sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7 \ - --hash=sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0 \ - --hash=sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d \ - --hash=sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213 \ - --hash=sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8 \ - --hash=sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389 \ - --hash=sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330 \ - --hash=sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1 \ - --hash=sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2 \ - --hash=sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288 \ - --hash=sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba \ - --hash=sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded \ - --hash=sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca \ - --hash=sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109 \ - --hash=sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8 \ - --hash=sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea \ - --hash=sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6 \ - --hash=sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8 \ - --hash=sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0 \ - --hash=sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5 \ - --hash=sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5 \ - --hash=sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182 \ - --hash=sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73 \ - --hash=sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af \ - --hash=sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac \ - --hash=sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204 \ - --hash=sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277 \ - --hash=sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212 \ - --hash=sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41 \ - --hash=sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e \ - --hash=sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c \ - --hash=sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2 \ - --hash=sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4 \ - --hash=sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348 \ - --hash=sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b \ - --hash=sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e \ - --hash=sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995 \ - --hash=sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371 \ - --hash=sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613 \ - --hash=sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b \ - --hash=sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d \ - --hash=sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708 \ - --hash=sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08 \ - --hash=sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca \ - --hash=sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12 \ - --hash=sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8 \ - --hash=sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829 \ - --hash=sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38 \ - --hash=sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825 \ - --hash=sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67 \ - --hash=sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056 \ - --hash=sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376 \ - --hash=sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214 \ - --hash=sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982 \ - --hash=sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034 \ - --hash=sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1 \ - --hash=sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f \ - --hash=sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9 \ - --hash=sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5 \ - --hash=sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390 \ - --hash=sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0 \ - --hash=sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d \ - --hash=sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57 \ - --hash=sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe \ - --hash=sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2 \ - --hash=sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415 \ - --hash=sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c \ - --hash=sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b \ - --hash=sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853 \ - --hash=sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36 \ - --hash=sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae \ - --hash=sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd \ - --hash=sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187 \ - --hash=sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c \ - --hash=sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542 \ - --hash=sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d \ - --hash=sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79 \ - --hash=sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7 \ - --hash=sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def - # via feast (setup.py) -hpack==4.1.0 \ - --hash=sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496 \ - --hash=sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca +hiredis==3.4.0 \ + --hash=sha256:02298551060cbfe17b1acb02e9d066eadd51e37357369736f8fc1f5533875255 \ + --hash=sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc \ + --hash=sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865 \ + --hash=sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c \ + --hash=sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec \ + --hash=sha256:06c163607f163a300cba9ef02fd8a234dd644a208bfafe972614f72e5287b9ba \ + --hash=sha256:0731dd9f2bf4d3417947a3cbbca4dd4d9fa6ad7cecfd4e16439c17a4562d6304 \ + --hash=sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419 \ + --hash=sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320 \ + --hash=sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b \ + --hash=sha256:0bcb630add6bc9ea136fce691ddff0c46aa91cb860df4ca789fe44127eb7e90d \ + --hash=sha256:12ea5facb5b08fa23e4c101ec2151f3a3de8ecec412fec58dbde0a6eebca02c7 \ + --hash=sha256:12eca9aea1450d1a85dc15574a985c227e52abbc2b6466f48ad2aa3b82124701 \ + --hash=sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f \ + --hash=sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017 \ + --hash=sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc \ + --hash=sha256:16a3ea8193c5f4ee6e8daafe0126207cef38eec5a923c97047e6985d9bb1b61f \ + --hash=sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4 \ + --hash=sha256:1bfb9ccfb13be63883e5f2e5ff7f6fc87bf256f8243af594257dfbed9dbc3cf0 \ + --hash=sha256:1c9e141b420dae63ea21303a7e8672282326cae67472768e9a3f6e7f0990bd28 \ + --hash=sha256:1d8c21fb85f2a3de9328cf5bace84b63da00028e771b92335ff4fdc00121dd5a \ + --hash=sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5 \ + --hash=sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016 \ + --hash=sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240 \ + --hash=sha256:282c4310af72afbe18b07d416459f4febeaeb805a067a7df790136e0e550fcb2 \ + --hash=sha256:2c432cce38190c3c13b6c28d6840ee85db50765ad510134007f1cfb3844dce73 \ + --hash=sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591 \ + --hash=sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea \ + --hash=sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980 \ + --hash=sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b \ + --hash=sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb \ + --hash=sha256:386b556f48fb0f9f09696b9d37f1cae554123fbd9f091d0ca23087f2aca02887 \ + --hash=sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b \ + --hash=sha256:4190bd07dd7879a8a7ddbb2a4f74d402721f3898276e35beb98851b85b5f539c \ + --hash=sha256:423570ac4d2665c0d55d8707b7859a331e9001da5e437b7b7a23e0acbce770c9 \ + --hash=sha256:43004b0b48abc628dda1ac3ac4871e1326c126f8cd9f11164d61934d827d7a3b \ + --hash=sha256:4404c557fd49bcfe24dff41f1209e4221c76d1607df2fb2dfd39474b5b086dcb \ + --hash=sha256:44660a91e0fbc803c29b337c1a9194c8d7b4cd3a3868d28f747cbec2df165483 \ + --hash=sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582 \ + --hash=sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2 \ + --hash=sha256:45c6c296056641b5df37cedafe7d1553f33bc247e2f81603a4d038b39261879b \ + --hash=sha256:46e041fc7a83eaa989ef5fe09526bc2353a6ba39b4ce5191684eabefc02e17b0 \ + --hash=sha256:4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e \ + --hash=sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d \ + --hash=sha256:4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d \ + --hash=sha256:4e5530d7a6c71c1a9c3b850b08bbc8178ffc9556e3aef7f9c808fe9ff91a6f11 \ + --hash=sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763 \ + --hash=sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584 \ + --hash=sha256:5359caad5b57da0bce11d2880f22617ba3710f0866121a924745447848448034 \ + --hash=sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79 \ + --hash=sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9 \ + --hash=sha256:64cf54c1dad35830bf1d86b343ad3c55aed443af51c5ccead8afd1b547aca196 \ + --hash=sha256:6774f1fe2723001ca0cd42bf5d8b1235301226273915c581c5c1260d4d114c43 \ + --hash=sha256:68c81c04e0d8778c01163dd2bc6f0e0236fc7f650c291526f10d4faccd99e5a0 \ + --hash=sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb \ + --hash=sha256:69d0326f20354ce278cbb86f5ae47cb390e22bb94a66877031038af907c42fa5 \ + --hash=sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22 \ + --hash=sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b \ + --hash=sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9 \ + --hash=sha256:7b159315df71a009375227384aa4219f98c2342ccd8eb33e3f4b58654f426376 \ + --hash=sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203 \ + --hash=sha256:7f7fc1535f6e1a190089eae46dee25f0c6b72bb221d377be07092803b8208733 \ + --hash=sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f \ + --hash=sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13 \ + --hash=sha256:85a9abf2d16b4cb112f6cc6c32236d763b34d21c69b00c2f81a4ff3016fcc1d6 \ + --hash=sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c \ + --hash=sha256:899d5cea72c9a6c2d9b6e7b223de62c9e9f541f48267cc7135764070548735ce \ + --hash=sha256:89e95f50ec4d29645ba2da9010e8717861585dc9f3df354a367f43807a6db3c7 \ + --hash=sha256:8aaaab18314fd25453b5cf59c8cdca4110e419455bcb4c0737d19d4151513e75 \ + --hash=sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1 \ + --hash=sha256:8f2c8569460aa456a294d5e9a3579a382b825dc2e283e3cc398323dcfb6a3dee \ + --hash=sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151 \ + --hash=sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a \ + --hash=sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d \ + --hash=sha256:98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c \ + --hash=sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f \ + --hash=sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d \ + --hash=sha256:a315009b441a0105a373a9a780ebb1c6f7d9ead88ac6ea5f2a15791353c6f590 \ + --hash=sha256:a3796094f616f72976ff51e4dc1a016e753c0f9af5393b2df96920b6bae1e19b \ + --hash=sha256:a427976ba339da624367613d7eba52a1516e6d5c7f8988dc8fb888fbcf52d8b6 \ + --hash=sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb \ + --hash=sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530 \ + --hash=sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4 \ + --hash=sha256:b0c29ac4f50fc61a904acfe76e4ce6bc37e16ab5a98089cb411476d466e1bb36 \ + --hash=sha256:b32afb576705fc5a02440d416ef476ddbcc827a20ff6c4a9d557882bc6b75135 \ + --hash=sha256:b6228afacadb19a858289319d72797023e2cb048f2f930b68b7fe57eaed5fb6b \ + --hash=sha256:b98f44bf873731d9ce99e6b3eeb6196bf7b27693ad9f6de0fba986b2b0845127 \ + --hash=sha256:bb44efa4fa3e3ed7779ad0ade3c08ed5d75ca7a6336893e9a4f2722093b4168a \ + --hash=sha256:bb8b2dd9c00d80ad2870ffbb51ce9af99fb2194c082d7b09f4125d90e21426f6 \ + --hash=sha256:bca175f02a2b0150ffe7f5dc8bf49c798f34d2c7024d17ace0ec97a7583560e3 \ + --hash=sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59 \ + --hash=sha256:c2245c46b4ced5f689469e6dcdfc8a0895bf873840a6600f5ea759cdf1b26a8b \ + --hash=sha256:c8c6337e7ad187e8039d06e4b33ee5148dcd73928819d94087e649eb37316a09 \ + --hash=sha256:ccc5c660e31d788ca534a20f2ccb7a80b946b960e18ed4e1db950fcac122b405 \ + --hash=sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643 \ + --hash=sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c \ + --hash=sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4 \ + --hash=sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d \ + --hash=sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352 \ + --hash=sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81 \ + --hash=sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5 \ + --hash=sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092 \ + --hash=sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4 \ + --hash=sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad \ + --hash=sha256:e29267ecdd08758926f1a9221af2671d90f475480c40aff409921b1f362f1bd5 \ + --hash=sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d \ + --hash=sha256:ed1dba2695f6de009c67d63b39ff978cb43b8a79362f697acedffb7743e50d21 \ + --hash=sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1 \ + --hash=sha256:f3c67f39b112dc35f68d5b59ee111db6121f037d1a60cf3840ecffbb2ec5686b \ + --hash=sha256:f7c7596fbb2b5202e943180353958e89014e763c7f25877a92f70bbde6cd7f19 \ + --hash=sha256:ff28eca77a57751488df90b0f385b472fd78e140a4d45097224648a5737acdf5 \ + --hash=sha256:ff6217da86a63a6a5ea954d9b9ed67916d9d94a566b44aa98e68ae34b40ebbbf + # via feast (pyproject.toml) +hpack==4.2.0 \ + --hash=sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0 \ + --hash=sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986 # via h2 httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httpcore2==2.9.1 \ + --hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \ + --hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26 + # via httpx2 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn httpx[http2]==0.27.2 \ --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 # via - # feast (setup.py) + # feast (pyproject.toml) + # datasets # fastapi-mcp # jupyterlab - # mcp + # openlineage-python # python-keycloak # qdrant-client -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d +httpx2==2.9.1 \ + --hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \ + --hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a # via mcp -huggingface-hub==0.35.3 \ - --hash=sha256:0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba \ - --hash=sha256:350932eaa5cc6a4747efae85126ee220e4ef1b54e29d31c3b45c5612ddf0b32a +huggingface-hub==0.36.2 \ + --hash=sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a \ + --hash=sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270 # via # accelerate # datasets # docling # docling-ibm-models + # sentence-transformers # timm # tokenizers # transformers @@ -1844,67 +2168,58 @@ hyperframe==6.1.0 \ --hash=sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5 \ --hash=sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08 # via h2 -ibis-framework[duckdb, mssql]==9.5.0 \ - --hash=sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0 \ - --hash=sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84 - # via - # feast (setup.py) - # ibis-substrait -ibis-substrait==4.0.1 \ - --hash=sha256:107ca49383a3cca2fdc88f67ea2f0172620c16fa8f39c9c52305af85dd6180b4 \ - --hash=sha256:614810a173d096fbc49d87a9b419e2162a3c25d8efda1a4d57a389ce56b9041f - # via feast (setup.py) -identify==2.6.15 \ - --hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \ - --hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf +ibis-framework[duckdb, mssql, oracle]==12.0.0 \ + --hash=sha256:0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 \ + --hash=sha256:238624f2c14fdab8382ca2f4f667c3cdb81e29844cd5f8db8a325d0743767c61 + # via feast (pyproject.toml) +identify==2.6.19 \ + --hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \ + --hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842 # via pre-commit -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx + # httpx2 # jsonschema # requests # snowflake-connector-python # yarl -ikvpy==0.0.36 \ - --hash=sha256:b0edf6fb6482877940f6c2b5d59a7fabe30cb554b13b88ca52805f043cfda5b3 \ - --hash=sha256:c0ce7dfb61456c283c9ba2cdeb68b3647f245c3905bca652ca2a1068804939d1 - # via feast (setup.py) -imageio==2.37.0 \ - --hash=sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed \ - --hash=sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996 +imageio==2.37.4 \ + --hash=sha256:1ab2e22c8debf700f24c3ac43e8f95f3b3a8110c83b93411e97b4b0b2cd1c7e6 \ + --hash=sha256:e45cbc5e83502047fb138f7f585f7f105a136a57eea5f4b3cfc6ce1b52720bd3 # via scikit-image -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a +imagesize==2.0.0 \ + --hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \ + --hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3 # via sphinx -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==8.9.0 \ + --hash=sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee \ + --hash=sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f # via # dask - # opentelemetry-api -importlib-resources==6.5.2 \ - --hash=sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c \ - --hash=sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec + # mlflow-skinny +importlib-resources==7.1.0 \ + --hash=sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708 \ + --hash=sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1 # via happybase -iniconfig==2.1.0 \ - --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ - --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via pytest -invoke==2.2.1 \ - --hash=sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8 \ - --hash=sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707 +invoke==3.0.3 \ + --hash=sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c \ + --hash=sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053 # via paramiko -ipykernel==7.0.0 \ - --hash=sha256:06aef83f27adbce00b23345aa70f749f907dc4ac6f4a41fe7bf5f780dc506225 \ - --hash=sha256:28793cecaa6a669e3be80eb6d24803202388b6a955929b0a4e13404d8c92062b +ipykernel==7.3.0 \ + --hash=sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057 \ + --hash=sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09 # via jupyterlab -ipython==9.6.0 \ - --hash=sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731 \ - --hash=sha256:5f77efafc886d2f023442479b8149e7d86547ad0a979e9da9f045d252f648196 +ipython==9.10.1 \ + --hash=sha256:82d18ae9fb9164ded080c71ef92a182ee35ee7db2395f67616034bebb020a232 \ + --hash=sha256:e170e9b2a44312484415bdb750492699bf329233b03f2557a9692cce6466ada4 # via # great-expectations # ipykernel @@ -1927,51 +2242,55 @@ isoduration==20.11.0 \ --hash=sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9 \ --hash=sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042 # via jsonschema -jedi==0.19.2 \ - --hash=sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0 \ - --hash=sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9 +itsdangerous==2.2.0 \ + --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ + --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 + # via flask +jedi==0.20.0 \ + --hash=sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67 \ + --hash=sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011 # via ipython jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via - # feast (setup.py) + # feast (pyproject.toml) # altair + # flask # great-expectations # jupyter-server # jupyterlab # jupyterlab-server # moto # nbconvert - # poetry-dynamic-versioning # sphinx # torch -jmespath==1.0.1 \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 # via # aiobotocore # boto3 # botocore -joblib==1.5.2 \ - --hash=sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55 \ - --hash=sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241 +joblib==1.5.3 \ + --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ + --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 # via scikit-learn -json5==0.12.1 \ - --hash=sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990 \ - --hash=sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5 +json5==0.15.0 \ + --hash=sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618 \ + --hash=sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71 # via jupyterlab-server -jsonlines==3.1.0 \ - --hash=sha256:2579cb488d96f815b0eb81629e3e6b0332da0962a18fa3532958f7ba14a5c37f \ - --hash=sha256:632f5e38f93dfcb1ac8c4e09780b92af3a55f38f26e7c47ae85109d420b6ad39 +jsonlines==4.0.0 \ + --hash=sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74 \ + --hash=sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55 # via docling-ibm-models jsonpatch==1.33 \ --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c # via great-expectations -jsonpointer==3.0.0 \ - --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ - --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef +jsonpointer==3.1.1 \ + --hash=sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900 \ + --hash=sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca # via # jsonpatch # jsonschema @@ -1979,11 +2298,10 @@ jsonref==1.1.0 \ --hash=sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552 \ --hash=sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9 # via docling-core -jsonschema[format-nongpl]==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 +jsonschema[format-nongpl]==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce # via - # feast (setup.py) # altair # docling-core # great-expectations @@ -1996,84 +2314,215 @@ jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -jupyter-client==8.6.3 \ - --hash=sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419 \ - --hash=sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f +jupyter-builder==1.1.1 \ + --hash=sha256:1a13977912b08deda77fce2c803940131c27cf77a27ed64b9ffca25aa0ed7e6c \ + --hash=sha256:f9c14bc55c0488a073f62af12d468936fcf9ecb7e9dd802f6f9c33de46ad70db + # via + # jupyterlab + # notebook +jupyter-client==8.9.1 \ + --hash=sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81 \ + --hash=sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa # via # ipykernel # jupyter-server # nbclient -jupyter-core==5.8.1 \ - --hash=sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941 \ - --hash=sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0 +jupyter-core==5.9.1 \ + --hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \ + --hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 # via # ipykernel + # jupyter-builder # jupyter-client # jupyter-server # jupyterlab # nbclient # nbconvert # nbformat -jupyter-events==0.12.0 \ - --hash=sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb \ - --hash=sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b +jupyter-events==0.12.1 \ + --hash=sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf \ + --hash=sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3 # via jupyter-server -jupyter-lsp==2.3.0 \ - --hash=sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245 \ - --hash=sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f +jupyter-lsp==2.3.1 \ + --hash=sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81 \ + --hash=sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6 # via jupyterlab -jupyter-server==2.17.0 \ - --hash=sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5 \ - --hash=sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f +jupyter-server==2.20.0 \ + --hash=sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14 \ + --hash=sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc # via # jupyter-lsp # jupyterlab # jupyterlab-server # notebook # notebook-shim -jupyter-server-terminals==0.5.3 \ - --hash=sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa \ - --hash=sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269 +jupyter-server-terminals==0.5.4 \ + --hash=sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14 \ + --hash=sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5 # via jupyter-server -jupyterlab==4.4.9 \ - --hash=sha256:394c902827350c017430a8370b9f40c03c098773084bc53930145c146d3d2cb2 \ - --hash=sha256:ea55aca8269909016d5fde2dc09b97128bc931230183fe7e2920ede5154ad9c2 +jupyterlab==4.6.2 \ + --hash=sha256:5964447036629adfcd3fc0969effc1da6f47d2cbd0a60b2c2eea7c31be0ec6a8 \ + --hash=sha256:e18ce8b34f3de350e93cd5b2c4f3ae884cbe266eb76bf5d6825a4ed34c13bcff # via notebook jupyterlab-pygments==0.3.0 \ --hash=sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d \ --hash=sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780 # via nbconvert -jupyterlab-server==2.27.3 \ - --hash=sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4 \ - --hash=sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4 +jupyterlab-server==2.28.0 \ + --hash=sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c \ + --hash=sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968 # via # jupyterlab # notebook -jupyterlab-widgets==3.0.15 \ - --hash=sha256:2920888a0c2922351a9202817957a68c07d99673504d6cd37345299e971bb08b \ - --hash=sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c +jupyterlab-widgets==3.0.16 \ + --hash=sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0 \ + --hash=sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8 # via ipywidgets -jwcrypto==1.5.6 \ - --hash=sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789 \ - --hash=sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039 +jwcrypto==1.5.8 \ + --hash=sha256:85aeb475f808d56bbc2f2ed1f6f73e6a317c4011a4321505f02f0aed695a3742 \ + --hash=sha256:c3d7114b6f6e65b52f6b7da817eb8cb8423e1da31e1ef13508447c81ecbdcc34 # via python-keycloak -kubernetes==33.1.0 \ - --hash=sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5 \ - --hash=sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993 +kiwisolver==1.5.0 \ + --hash=sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9 \ + --hash=sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679 \ + --hash=sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0 \ + --hash=sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8 \ + --hash=sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276 \ + --hash=sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96 \ + --hash=sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e \ + --hash=sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac \ + --hash=sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f \ + --hash=sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a \ + --hash=sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15 \ + --hash=sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7 \ + --hash=sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368 \ + --hash=sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02 \ + --hash=sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9 \ + --hash=sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681 \ + --hash=sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57 \ + --hash=sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27 \ + --hash=sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4 \ + --hash=sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920 \ + --hash=sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374 \ + --hash=sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3 \ + --hash=sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa \ + --hash=sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23 \ + --hash=sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859 \ + --hash=sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb \ + --hash=sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d \ + --hash=sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc \ + --hash=sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581 \ + --hash=sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c \ + --hash=sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099 \ + --hash=sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05 \ + --hash=sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9 \ + --hash=sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd \ + --hash=sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc \ + --hash=sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796 \ + --hash=sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303 \ + --hash=sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca \ + --hash=sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314 \ + --hash=sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489 \ + --hash=sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57 \ + --hash=sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1 \ + --hash=sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797 \ + --hash=sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021 \ + --hash=sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db \ + --hash=sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22 \ + --hash=sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028 \ + --hash=sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083 \ + --hash=sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65 \ + --hash=sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588 \ + --hash=sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0 \ + --hash=sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a \ + --hash=sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1 \ + --hash=sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c \ + --hash=sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac \ + --hash=sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476 \ + --hash=sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53 \ + --hash=sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3 \ + --hash=sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4 \ + --hash=sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615 \ + --hash=sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb \ + --hash=sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18 \ + --hash=sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b \ + --hash=sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1 \ + --hash=sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2 \ + --hash=sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c \ + --hash=sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac \ + --hash=sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d \ + --hash=sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf \ + --hash=sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2 \ + --hash=sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f \ + --hash=sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f \ + --hash=sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4 \ + --hash=sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9 \ + --hash=sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e \ + --hash=sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737 \ + --hash=sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b \ + --hash=sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed \ + --hash=sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3 \ + --hash=sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7 \ + --hash=sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08 \ + --hash=sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e \ + --hash=sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902 \ + --hash=sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd \ + --hash=sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6 \ + --hash=sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310 \ + --hash=sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537 \ + --hash=sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554 \ + --hash=sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e \ + --hash=sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87 \ + --hash=sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a \ + --hash=sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c \ + --hash=sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79 \ + --hash=sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e \ + --hash=sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16 \ + --hash=sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1 \ + --hash=sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875 \ + --hash=sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd \ + --hash=sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0 \ + --hash=sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9 \ + --hash=sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646 \ + --hash=sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657 \ + --hash=sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4 \ + --hash=sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232 \ + --hash=sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 \ + --hash=sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 \ + --hash=sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309 \ + --hash=sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede \ + --hash=sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2 \ + --hash=sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203 \ + --hash=sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7 \ + --hash=sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df \ + --hash=sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c \ + --hash=sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167 \ + --hash=sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3 \ + --hash=sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09 \ + --hash=sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398 + # via matplotlib +kube-authkit==0.4.0 \ + --hash=sha256:1df61ac392fca96c8f5ae8c3d6e9918f1e1655d212434b3c3da5f92cc23b660d \ + --hash=sha256:3bf5fc6ddc882498040118c907628ea68789f9a947454c241972008be59601a3 + # via codeflare-sdk +kubernetes==36.0.3 \ + --hash=sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7 \ + --hash=sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f # via - # feast (setup.py) + # feast (pyproject.toml) # codeflare-sdk -lark==1.3.0 \ - --hash=sha256:80661f261fb2584a9828a097a2432efd575af27d20be0fd35d17f0fe37253831 \ - --hash=sha256:9a3839d0ca5e1faf7cfa3460e420e859b66bcbde05b634e73c369c8244c5fa48 + # kube-authkit +lark==1.3.1 \ + --hash=sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905 \ + --hash=sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12 # via rfc3987-syntax -latex2mathml==3.78.1 \ - --hash=sha256:f089b6d75e85b937f99693c93e8c16c0804008672c3dd2a3d25affd36f238100 \ - --hash=sha256:f941db80bf41db33f31df87b304e8b588f8166b813b0257c11c98f7a9d0aac71 +latex2mathml==3.81.0 \ + --hash=sha256:4b959cdc3cac8686bc0e3e5aece8127dfb1b81ca1241bed8e00ef31b82bb4022 \ + --hash=sha256:d317710393fe20579aea39cfe8928fa2ad9b8780896e585326c75e89c1d1d1a4 # via docling-core -lazy-loader==0.4 \ - --hash=sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc \ - --hash=sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1 +lazy-loader==0.5 \ + --hash=sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3 \ + --hash=sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005 # via scikit-image locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ @@ -2216,48 +2665,64 @@ lxml==5.4.0 \ # docling # python-docx # python-pptx -lz4==4.4.4 \ - --hash=sha256:017f8d269a739405a59d68a4d63d23a8df23e3bb2c70aa069b7563af08dfdffb \ - --hash=sha256:070fd0627ec4393011251a094e08ed9fdcc78cb4e7ab28f507638eee4e39abda \ - --hash=sha256:18ae4fe3bafb344dbd09f976d45cbf49c05c34416f2462828f9572c1fa6d5af7 \ - --hash=sha256:1ea7f07329f85a8eda4d8cf937b87f27f0ac392c6400f18bea2c667c8b7f8ecc \ - --hash=sha256:23ae267494fdd80f0d2a131beff890cf857f1b812ee72dbb96c3204aab725553 \ - --hash=sha256:2f4f2965c98ab254feddf6b5072854a6935adab7bc81412ec4fe238f07b85f62 \ - --hash=sha256:30ebbc5b76b4f0018988825a7e9ce153be4f0d4eba34e6c1f2fcded120573e88 \ - --hash=sha256:33e01e18e4561b0381b2c33d58e77ceee850a5067f0ece945064cbaac2176962 \ - --hash=sha256:38730927ad51beb42ab8dbc5555270bfbe86167ba734265f88bbd799fced1004 \ - --hash=sha256:4134b9fd70ac41954c080b772816bb1afe0c8354ee993015a83430031d686a4c \ - --hash=sha256:45e7c954546de4f85d895aa735989d77f87dd649f503ce1c8a71a151b092ed36 \ - --hash=sha256:4ab1537bd3b3bfbafd3c8847e06827129794488304f21945fc2f5b669649d94f \ - --hash=sha256:57fd20c5fc1a49d1bbd170836fccf9a338847e73664f8e313dce6ac91b8c1e02 \ - --hash=sha256:585b42eb37ab16a278c3a917ec23b2beef175aa669f4120142b97aebf90ef775 \ - --hash=sha256:6b56aa9eef830bf6443acd8c4e18b208a8993dc32e0d6ef4263ecfa6afb3f599 \ - --hash=sha256:6ea715bb3357ea1665f77874cf8f55385ff112553db06f3742d3cdcec08633f7 \ - --hash=sha256:714f9298c86f8e7278f1c6af23e509044782fa8220eb0260f8f8f1632f820550 \ - --hash=sha256:80dd27d7d680ea02c261c226acf1d41de2fd77af4fb2da62b278a9376e380de0 \ - --hash=sha256:8ccab8f7f7b82f9fa9fc3b0ba584d353bd5aa818d5821d77d5b9447faad2aaad \ - --hash=sha256:900912e8a7cf74b4a2bea18a3594ae0bf1138f99919c20017167b6e05f760aa4 \ - --hash=sha256:9b7d6dddfd01b49aedb940fdcaf32f41dc58c926ba35f4e31866aeec2f32f4f4 \ - --hash=sha256:a355223a284f42a723c120ce68827de66d5cb872a38732b3d5abbf544fa2fe26 \ - --hash=sha256:a760a175b46325b2bb33b1f2bbfb8aa21b48e1b9653e29c10b6834f9bb44ead4 \ - --hash=sha256:a8474c91de47733856c6686df3c4aca33753741da7e757979369c2c0d32918ba \ - --hash=sha256:b28228197775b7b5096898851d59ef43ccaf151136f81d9c436bc9ba560bc2ba \ - --hash=sha256:bd1add57b6fe1f96bed2d529de085e9378a3ac04b86f116d10506f85b68e97fc \ - --hash=sha256:d0be9f68240231e1e44118a4ebfecd8a5d4184f0bdf5c591c98dd6ade9720afd \ - --hash=sha256:d21d1a2892a2dcc193163dd13eaadabb2c1b803807a5117d8f8588b22eaf9f12 \ - --hash=sha256:d33a5105cd96ebd32c3e78d7ece6123a9d2fb7c18b84dec61f27837d9e0c496c \ - --hash=sha256:dac522788296a9a02a39f620970dea86c38e141e21e51238f1b5e9fa629f8e69 \ - --hash=sha256:dc64d6dfa7a89397529b22638939e70d85eaedc1bd68e30a29c78bfb65d4f715 \ - --hash=sha256:ddfc7194cd206496c445e9e5b0c47f970ce982c725c87bd22de028884125b68f \ - --hash=sha256:e3fc90f766401684740978cd781d73b9685bd81b5dbf7257542ef9de4612e4d2 \ - --hash=sha256:e43e9d48b2daf80e486213128b0763deed35bbb7a59b66d1681e205e1702d735 \ - --hash=sha256:e9cb387c33f014dae4db8cb4ba789c8d2a0a6d045ddff6be13f6c8d9def1d2a6 \ - --hash=sha256:e9ec5d45ea43684f87c316542af061ef5febc6a6b322928f059ce1fb289c298a \ - --hash=sha256:ed6eb9f8deaf25ee4f6fad9625d0955183fdc90c52b6f79a76b7f209af1b6e54 \ - --hash=sha256:f170abb8416c4efca48e76cac2c86c3185efdf841aecbe5c190121c42828ced0 \ - --hash=sha256:f4c21648d81e0dda38b4720dccc9006ae33b0e9e7ffe88af6bf7d4ec124e2fba \ - --hash=sha256:f5024d3ca2383470f7c4ef4d0ed8eabad0b22b23eeefde1c192cf1a38d5e9f78 \ - --hash=sha256:fff9f3a1ed63d45cb6514bfb8293005dc4141341ce3500abdfeb76124c0b9b2e +lz4==4.4.5 \ + --hash=sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc \ + --hash=sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f \ + --hash=sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d \ + --hash=sha256:13254bd78fef50105872989a2dc3418ff09aefc7d0765528adc21646a7288294 \ + --hash=sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397 \ + --hash=sha256:1dd4d91d25937c2441b9fc0f4af01704a2d09f30a38c5798bc1d1b5a15ec9581 \ + --hash=sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50 \ + --hash=sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d \ + --hash=sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e \ + --hash=sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd \ + --hash=sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1 \ + --hash=sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e \ + --hash=sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7 \ + --hash=sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668 \ + --hash=sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64 \ + --hash=sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c \ + --hash=sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0 \ + --hash=sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb \ + --hash=sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43 \ + --hash=sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901 \ + --hash=sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f \ + --hash=sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c \ + --hash=sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a \ + --hash=sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33 \ + --hash=sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989 \ + --hash=sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5 \ + --hash=sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd \ + --hash=sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f \ + --hash=sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b \ + --hash=sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004 \ + --hash=sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be \ + --hash=sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9 \ + --hash=sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba \ + --hash=sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d \ + --hash=sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d \ + --hash=sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832 \ + --hash=sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301 \ + --hash=sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a \ + --hash=sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67 \ + --hash=sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9 \ + --hash=sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f \ + --hash=sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6 \ + --hash=sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb \ + --hash=sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d \ + --hash=sha256:d64141085864918392c3159cdad15b102a620a67975c786777874e1e90ef15ce \ + --hash=sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4 \ + --hash=sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7 \ + --hash=sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22 \ + --hash=sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758 \ + --hash=sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e \ + --hash=sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67 \ + --hash=sha256:e64e61f29cf95afb43549063d8433b46352baf0c8a70aa45e2585618fcf59d86 \ + --hash=sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3 \ + --hash=sha256:f32b9e65d70f3684532358255dc053f143835c5f5991e28a5ac4c93ce94b9ea7 \ + --hash=sha256:f6538aaaedd091d6e5abdaa19b99e6e82697d67518f114721b5248709b639fad \ + --hash=sha256:f9b8bde9909a010c75b3aea58ec3910393b758f3c219beed67063693df854db0 \ + --hash=sha256:ff1b50aeeec64df5603f17984e4b5be6166058dcf8f1e26a3da40d7a0f6ab547 # via # clickhouse-connect # trino @@ -2265,13 +2730,17 @@ makefun==1.16.0 \ --hash=sha256:43baa4c3e7ae2b17de9ceac20b669e9a67ceeadff31581007cca20a07bbe42c4 \ --hash=sha256:e14601831570bff1f6d7e68828bcd30d2f5856f24bad5de0ccb22921ceebc947 # via great-expectations -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +mako==1.3.12 \ + --hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \ + --hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a + # via alembic +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich -marko==2.2.1 \ - --hash=sha256:31e9a18b35c113e506ace5594716fa3df2872f8955908e279bc551f3eb1f0db8 \ - --hash=sha256:e29d7e071a3b0cb2f7cc4c500d55f893dc5a45d85a8298dde6cb4e4dffd794d3 +marko==2.2.3 \ + --hash=sha256:8e1d7a0387281e59dfbc52a381b58c570156970e36b2bbe047f8a3a2f368cacc \ + --hash=sha256:e31ec2875383bc62f9093d16babed5a2c2cde601c00d834ea935a2222120ec19 # via docling markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ @@ -2364,178 +2833,226 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via + # flask # jinja2 + # mako # nbconvert # werkzeug -marshmallow==3.26.1 \ - --hash=sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c \ - --hash=sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6 - # via - # environs - # great-expectations -matplotlib-inline==0.1.7 \ - --hash=sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90 \ - --hash=sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca +marshmallow==3.26.2 \ + --hash=sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73 \ + --hash=sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57 + # via great-expectations +matplotlib==3.11.1 \ + --hash=sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2 \ + --hash=sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464 \ + --hash=sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1 \ + --hash=sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb \ + --hash=sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb \ + --hash=sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a \ + --hash=sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b \ + --hash=sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1 \ + --hash=sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3 \ + --hash=sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2 \ + --hash=sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741 \ + --hash=sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a \ + --hash=sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987 \ + --hash=sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be \ + --hash=sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf \ + --hash=sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f \ + --hash=sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191 \ + --hash=sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18 \ + --hash=sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b \ + --hash=sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30 \ + --hash=sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f \ + --hash=sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e \ + --hash=sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f \ + --hash=sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f \ + --hash=sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f \ + --hash=sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda \ + --hash=sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481 \ + --hash=sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6 \ + --hash=sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099 \ + --hash=sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78 \ + --hash=sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2 \ + --hash=sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319 \ + --hash=sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685 \ + --hash=sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83 \ + --hash=sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3 \ + --hash=sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407 \ + --hash=sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2 \ + --hash=sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d \ + --hash=sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea \ + --hash=sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472 \ + --hash=sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f \ + --hash=sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c \ + --hash=sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae \ + --hash=sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74 \ + --hash=sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99 \ + --hash=sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb + # via mlflow +matplotlib-inline==0.2.2 \ + --hash=sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6 \ + --hash=sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79 # via # ipykernel # ipython -mcp==1.12.4 \ - --hash=sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5 \ - --hash=sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789 +mcp==2.0.0 \ + --hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \ + --hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6 # via fastapi-mcp +mcp-types==2.0.0 \ + --hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \ + --hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379 + # via mcp mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -milvus-lite==2.4.12 \ - --hash=sha256:20087663e7b4385050b7ad08f1f03404426d4c87b1ff91d5a8723eee7fd49e88 \ - --hash=sha256:334037ebbab60243b5d8b43d54ca2f835d81d48c3cda0c6a462605e588deb05d \ - --hash=sha256:a0f3a5ddbfd19f4a6b842b2fd3445693c796cde272b701a1646a94c1ac45d3d7 \ - --hash=sha256:e8d4f7cdd5f731efd6faeee3715d280fd91a5f9b4d89312664d56401f65b1473 - # via - # feast (setup.py) - # pymilvus +milvus-lite==3.1.1 \ + --hash=sha256:0cbf8386a2e99b6464a5eda14cf8e31934b0e287e36c831e293aee614d9efc2e \ + --hash=sha256:0f343c9bf1e291d6c2fa615a52a57090ebbaf96b84bfb71f7eb73cd95cd7fc1c + # via feast (pyproject.toml) minio==7.2.11 \ --hash=sha256:153582ed52ff3b5005ba558e1f25bfe1e9e834f7f0745e594777f28e3e81e1a0 \ --hash=sha256:4db95a21fe1e2022ec975292d8a1f83bd5b18f830d23d42a4518ac7a5281d7c5 - # via feast (setup.py) -mistune==3.1.4 \ - --hash=sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d \ - --hash=sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164 + # via feast (pyproject.toml) +mistune==3.3.4 \ + --hash=sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe \ + --hash=sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a # via # great-expectations # nbconvert -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) +mlflow==3.2.0 \ + --hash=sha256:db97b925cc8afba15caf3749dcb4a95be83f9608e974f23253fbbc1d675247ea \ + --hash=sha256:e96bd42238ea8b477691c8a8f6e8bdbf9247415ad7892e6e885994c6940bcf74 + # via feast (pyproject.toml) +mlflow-skinny==3.2.0 \ + --hash=sha256:b359ec082a0a966e4e8e80f03d850da7fa677ebe57e67b1c0877029e5eeee443 \ + --hash=sha256:ec33a6fc164973e3b4d208e4ab8bec118ea93ff890ffbd08817b66468235ed71 + # via mlflow +mlflow-tracing==3.2.0 \ + --hash=sha256:4180d48b6b68a70b3e37987def3b0689d3f4ba722f5d2b98344c3717d2289b99 \ + --hash=sha256:6f3dd940752ca28871b09880e9426d1293460822faa8706b33af1d50c29a0355 + # via mlflow +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via + # feast (pyproject.toml) + # pyiceberg mock==2.0.0 \ --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba - # via feast (setup.py) + # via feast (pyproject.toml) moto==4.2.14 \ --hash=sha256:6d242dbbabe925bb385ddb6958449e5c827670b13b8e153ed63f91dbdb50372c \ --hash=sha256:8f9263ca70b646f091edcc93e97cda864a542e6d16ed04066b1370ed217bd190 - # via feast (setup.py) + # via feast (pyproject.toml) mpire[dill]==2.10.2 \ --hash=sha256:d627707f7a8d02aa4c7f7d59de399dec5290945ddf7fbd36cbb1d6ebb37a51fb \ --hash=sha256:f66a321e93fadff34585a4bfa05e95bd946cf714b442f51c529038eb45773d97 @@ -2544,9 +3061,9 @@ mpmath==1.3.0 \ --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c # via sympy -msal==1.34.0 \ - --hash=sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f \ - --hash=sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1 +msal==1.37.0 \ + --hash=sha256:1b1672a33ee467c1d70b341bb16cafd51bb3c817147a95b93263794b03971bec \ + --hash=sha256:dd17e95a7c71bce75e8108113438ba7c4a086b3bcad4f57a8c09b7af3d753c2d # via # azure-identity # msal-extensions @@ -2554,234 +3071,242 @@ msal-extensions==1.3.1 \ --hash=sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca \ --hash=sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4 # via azure-identity -msgpack==1.1.2 \ - --hash=sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2 \ - --hash=sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014 \ - --hash=sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931 \ - --hash=sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b \ - --hash=sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b \ - --hash=sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999 \ - --hash=sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029 \ - --hash=sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0 \ - --hash=sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9 \ - --hash=sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c \ - --hash=sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8 \ - --hash=sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f \ - --hash=sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a \ - --hash=sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42 \ - --hash=sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e \ - --hash=sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f \ - --hash=sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7 \ - --hash=sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb \ - --hash=sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef \ - --hash=sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf \ - --hash=sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245 \ - --hash=sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794 \ - --hash=sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af \ - --hash=sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff \ - --hash=sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e \ - --hash=sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296 \ - --hash=sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030 \ - --hash=sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833 \ - --hash=sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939 \ - --hash=sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa \ - --hash=sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90 \ - --hash=sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c \ - --hash=sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717 \ - --hash=sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406 \ - --hash=sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a \ - --hash=sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251 \ - --hash=sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2 \ - --hash=sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7 \ - --hash=sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e \ - --hash=sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b \ - --hash=sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844 \ - --hash=sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9 \ - --hash=sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87 \ - --hash=sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b \ - --hash=sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c \ - --hash=sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23 \ - --hash=sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c \ - --hash=sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e \ - --hash=sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620 \ - --hash=sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69 \ - --hash=sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f \ - --hash=sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68 \ - --hash=sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27 \ - --hash=sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46 \ - --hash=sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa \ - --hash=sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00 \ - --hash=sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9 \ - --hash=sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84 \ - --hash=sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e \ - --hash=sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20 \ - --hash=sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e \ - --hash=sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162 +msgpack==1.2.1 \ + --hash=sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833 \ + --hash=sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a \ + --hash=sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647 \ + --hash=sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d \ + --hash=sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064 \ + --hash=sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107 \ + --hash=sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac \ + --hash=sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720 \ + --hash=sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c \ + --hash=sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06 \ + --hash=sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895 \ + --hash=sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde \ + --hash=sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203 \ + --hash=sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc \ + --hash=sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74 \ + --hash=sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22 \ + --hash=sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8 \ + --hash=sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35 \ + --hash=sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb \ + --hash=sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9 \ + --hash=sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8 \ + --hash=sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6 \ + --hash=sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07 \ + --hash=sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056 \ + --hash=sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4 \ + --hash=sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7 \ + --hash=sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1 \ + --hash=sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24 \ + --hash=sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355 \ + --hash=sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0 \ + --hash=sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce \ + --hash=sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f \ + --hash=sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707 \ + --hash=sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66 \ + --hash=sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b \ + --hash=sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e \ + --hash=sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d \ + --hash=sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8 \ + --hash=sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7 \ + --hash=sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73 \ + --hash=sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402 \ + --hash=sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a \ + --hash=sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d \ + --hash=sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c \ + --hash=sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273 \ + --hash=sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b \ + --hash=sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d \ + --hash=sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb \ + --hash=sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4 \ + --hash=sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190 \ + --hash=sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5 \ + --hash=sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a \ + --hash=sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7 \ + --hash=sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1 \ + --hash=sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889 \ + --hash=sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c \ + --hash=sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155 \ + --hash=sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24 \ + --hash=sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6 \ + --hash=sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1 \ + --hash=sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d \ + --hash=sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7 \ + --hash=sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64 \ + --hash=sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2 \ + --hash=sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc \ + --hash=sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c # via ray -multidict==6.7.0 \ - --hash=sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32 \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ - --hash=sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff \ - --hash=sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8 \ - --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ - --hash=sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 \ - --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ - --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6 \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ - --hash=sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912 \ - --hash=sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c \ - --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6 \ - --hash=sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84 \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ - --hash=sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd \ - --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0 \ - --hash=sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e \ - --hash=sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 \ - --hash=sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc \ - --hash=sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7 \ - --hash=sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4 \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111 \ - --hash=sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e \ - --hash=sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84 \ - --hash=sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851 \ - --hash=sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb \ - --hash=sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ - --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5 \ - --hash=sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb \ - --hash=sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349 \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ - --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ - --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34 \ - --hash=sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85 \ - --hash=sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ - --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ - --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ - --hash=sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91 \ - --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40 \ - --hash=sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73 \ - --hash=sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4 \ - --hash=sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687 \ - --hash=sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via # aiobotocore # aiohttp # yarl -multiprocess==0.70.16 \ - --hash=sha256:0dfd078c306e08d46d7a8d06fb120313d87aa43af60d66da43ffff40b44d2f41 \ - --hash=sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1 \ - --hash=sha256:37b55f71c07e2d741374998c043b9520b626a8dddc8b3129222ca4f1a06ef67a \ - --hash=sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee \ - --hash=sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3 \ - --hash=sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435 \ - --hash=sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a \ - --hash=sha256:ba8c31889abf4511c7308a8c52bb4a30b9d590e7f58523302ba00237702ca054 \ - --hash=sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02 \ - --hash=sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec \ - --hash=sha256:e7b9d0f307cd9bd50851afaac0dba2cb6c44449efff697df7c7645f7d3f2be3a \ - --hash=sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e +multiprocess==0.70.17 \ + --hash=sha256:1d52f068357acd1e5bbc670b273ef8f81d57863235d9fbf9314751886e141968 \ + --hash=sha256:20c28ca19079a6c879258103a6d60b94d4ffe2d9da07dda93fb1c8bc6243f522 \ + --hash=sha256:27b8409c02b5dd89d336107c101dfbd1530a2cd4fd425fc27dcb7adb6e0b47bf \ + --hash=sha256:2818af14c52446b9617d1b0755fa70ca2f77c28b25ed97bdaa2c69a22c47b46c \ + --hash=sha256:2884701445d0177aec5bd5f6ee0df296773e4fb65b11903b94c613fb46cfb7d1 \ + --hash=sha256:2b12e081df87ab755190e227341b2c3b17ee6587e9c82fecddcbe6aa812cd7f7 \ + --hash=sha256:2ea0939b0f4760a16a548942c65c76ff5afd81fbf1083c56ae75e21faf92e426 \ + --hash=sha256:349525099a0c9ac5936f0488b5ee73199098dac3ac899d81d326d238f9fd3ccd \ + --hash=sha256:38357ca266b51a2e22841b755d9a91e4bb7b937979a54d411677111716c32744 \ + --hash=sha256:4ae2f11a3416809ebc9a48abfc8b14ecce0652a0944731a1493a3c1ba44ff57a \ + --hash=sha256:7ddb24e5bcdb64e90ec5543a1f05a39463068b6d3b804aa3f2a4e16ec28562d6 \ + --hash=sha256:a0f01cd9d079af7a8296f521dc03859d1a414d14c1e2b6e676ef789333421c95 \ + --hash=sha256:a22a6b1a482b80eab53078418bb0f7025e4f7d93cc8e1f36481477a023884861 \ + --hash=sha256:c2c82d0375baed8d8dd0d8c38eb87c5ae9c471f8e384ad203a36f095ee860f67 \ + --hash=sha256:c3feb874ba574fbccfb335980020c1ac631fbf2a3f7bee4e2042ede62558a021 \ + --hash=sha256:d729f55198a3579f6879766a6d9b72b42d4b320c0dcb7844afb774d75b573c62 # via # datasets # mpire @@ -2814,7 +3339,7 @@ mypy==1.11.2 \ --hash=sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b \ --hash=sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d # via - # feast (setup.py) + # feast (pyproject.toml) # sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ @@ -2823,14 +3348,18 @@ mypy-extensions==1.1.0 \ mypy-protobuf==3.3.0 \ --hash=sha256:15604f6943b16c05db646903261e3b3e775cf7f7990b7c37b03d043a907b650d \ --hash=sha256:24f3b0aecb06656e983f58e07c732a90577b9d7af3e1066fc2b663bbf0370248 - # via feast (setup.py) -nbclient==0.10.2 \ - --hash=sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d \ - --hash=sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193 + # via feast (pyproject.toml) +narwhals==2.24.0 \ + --hash=sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489 \ + --hash=sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d + # via scikit-learn +nbclient==0.11.0 \ + --hash=sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a \ + --hash=sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895 # via nbconvert -nbconvert==7.16.6 \ - --hash=sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b \ - --hash=sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582 +nbconvert==7.17.1 \ + --hash=sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2 \ + --hash=sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8 # via jupyter-server nbformat==5.10.4 \ --hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \ @@ -2840,13 +3369,13 @@ nbformat==5.10.4 \ # jupyter-server # nbclient # nbconvert -nest-asyncio==1.6.0 \ - --hash=sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe \ - --hash=sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c +nest-asyncio2==1.7.2 \ + --hash=sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8 \ + --hash=sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01 # via ipykernel -networkx==3.5 \ - --hash=sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec \ - --hash=sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037 +networkx==3.6.1 \ + --hash=sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509 \ + --hash=sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762 # via # scikit-image # torch @@ -2871,13 +3400,13 @@ ninja==1.13.0 \ --hash=sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa \ --hash=sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e # via easyocr -nodeenv==1.9.1 \ - --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ - --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 +nodeenv==1.10.0 \ + --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ + --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb # via pre-commit -notebook==7.4.7 \ - --hash=sha256:362b7c95527f7dd3c4c84d410b782872fd9c734fb2524c11dd92758527b6eda6 \ - --hash=sha256:3f0a04027dfcee8a876de48fba13ab77ec8c12f72f848a222ed7f5081b9e342a +notebook==7.6.1 \ + --hash=sha256:0b45fd1010668dd4808c40914d957706dbf044a677d28764dc881b74dfcede82 \ + --hash=sha256:6ea1e4c926f0dc490444ddcc335797a3dacda470a805d01031872fb119988ba2 # via great-expectations notebook-shim==0.2.4 \ --hash=sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef \ @@ -2885,85 +3414,84 @@ notebook-shim==0.2.4 \ # via # jupyterlab # notebook -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf - # via - # feast (setup.py) +numpy==2.4.6 \ + --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ + --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ + --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ + --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ + --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ + --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ + --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ + --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ + --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ + --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ + --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ + --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ + --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ + --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ + --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ + --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ + --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ + --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ + --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ + --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ + --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ + --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ + --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ + --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ + --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ + --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ + --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 + # via + # feast (pyproject.toml) # accelerate # altair + # contourpy # dask # datasets # db-dtypes @@ -2973,16 +3501,19 @@ numpy==2.3.3 \ # great-expectations # ibis-framework # imageio + # matplotlib + # milvus-lite + # mlflow # opencv-python-headless # pandas # pandas-gbq - # pyarrow # qdrant-client # ray # safetensors # scikit-image # scikit-learn # scipy + # sentence-transformers # shapely # tifffile # torchvision @@ -2990,9 +3521,7 @@ numpy==2.3.3 \ oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 - # via - # kubernetes - # requests-oauthlib + # via requests-oauthlib opencensus==0.11.4 \ --hash=sha256:a18487ce68bc19900336e0ff4655c5a116daf10c1b3685ece8d971bddad6a864 \ --hash=sha256:cbef87d8b8773064ab60e5c2a1ced58bbaa38a6d052c41aec224958ce544eff2 @@ -3001,17 +3530,20 @@ opencensus-context==0.1.3 \ --hash=sha256:073bb0590007af276853009fac7e4bab1d523c3f03baf4cb4511ca38967c6039 \ --hash=sha256:a03108c3c10d8c80bb5ddf5c8a1f033161fa61972a9917f9b9b3a18517f0088c # via opencensus -opencv-python-headless==4.11.0.86 \ - --hash=sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b \ - --hash=sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca \ - --hash=sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca \ - --hash=sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb \ - --hash=sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798 \ - --hash=sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81 \ - --hash=sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b - # via - # docling-ibm-models - # easyocr +opencv-python-headless==5.0.0.93 \ + --hash=sha256:030ca5e0837a2963ab36ef896baa9767eb8d2b83353fb28af5a521e40dd8756f \ + --hash=sha256:09a872a157c1376ab922a69bbf22f9a95bcc7b658a9d8b436a60212b02b2eeb4 \ + --hash=sha256:10818d91510e05c04568ae12b5cd120779c70c01bf897b001a6221fe430df80f \ + --hash=sha256:1e55af3abfb462eeeabe5c775f12bdb36216d8a93a3583d69e6bd6e1d6ba7d00 \ + --hash=sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e \ + --hash=sha256:840bd717c21e5c11cadadc022a823315ea417f961213d06b4df010e019eb16f4 \ + --hash=sha256:b82f9831daab90b725c7c1ee1b36cb5732c367096ac76d119e64e14eb70d5f3c \ + --hash=sha256:c6bcd96b185975ea240d22cfdb15a1f6d080cc95264cfbe2621f21bb144d89b9 \ + --hash=sha256:ed709fdf9aa0bd1f2ed8549e71d19449b03a675bb581eb292285f6861953be37 + # via easyocr +openlineage-python==1.52.0 \ + --hash=sha256:e1bd73b3066021eecf69cc5c3571f6fd7e253768ea941f2c76f18969ea0ecb20 + # via feast (pyproject.toml) openpyxl==3.1.5 \ --hash=sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2 \ --hash=sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050 @@ -3020,123 +3552,170 @@ openshift-client==1.0.18 \ --hash=sha256:be3979440cfd96788146a3a1650dabe939d4d516eea0b39f87e66d2ab39495b1 \ --hash=sha256:d8a84080307ccd9556f6c62a3707a3e6507baedee36fa425754f67db9ded528b # via codeflare-sdk -opentelemetry-api==1.37.0 \ - --hash=sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7 \ - --hash=sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47 +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef # via + # mcp + # mlflow-skinny + # mlflow-tracing # opentelemetry-exporter-prometheus # opentelemetry-sdk # opentelemetry-semantic-conventions -opentelemetry-exporter-prometheus==0.58b0 \ - --hash=sha256:02005033a7a108ab9f3000ff3aa49e2d03a8893b5bf3431322ffa246affbf951 \ - --hash=sha256:70f2627b4bb82bac65a1fcf95f6e0dcce9e823dd47379ced854753a7e14dfc93 +opentelemetry-exporter-prometheus==0.65b0 \ + --hash=sha256:2777cbf41c403c119e10f418fce5d645c956b47f673a2ee120285d1d0c6df2d5 \ + --hash=sha256:3b3d24b586d0ad9712c7b52b7d19c8a9dfbb318b9b284121b5f95e90ed019367 # via ray -opentelemetry-proto==1.27.0 \ - --hash=sha256:33c9345d91dafd8a74fc3d7576c5a38f18b7fdf8d02983ac67485386132aedd6 \ - --hash=sha256:b133873de5581a50063e1e4b29cdcf0c5e253a8c2d8dc1229add20a4c3830ace +opentelemetry-proto==1.44.0 \ + --hash=sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56 \ + --hash=sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3 # via ray -opentelemetry-sdk==1.37.0 \ - --hash=sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c \ - --hash=sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5 +opentelemetry-sdk==1.44.0 \ + --hash=sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b \ + --hash=sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad # via + # mlflow-skinny + # mlflow-tracing # opentelemetry-exporter-prometheus # ray -opentelemetry-semantic-conventions==0.58b0 \ - --hash=sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28 \ - --hash=sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25 +opentelemetry-semantic-conventions==0.65b0 \ + --hash=sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb \ + --hash=sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60 # via opentelemetry-sdk -orjson==3.11.3 \ - --hash=sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904 \ - --hash=sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873 \ - --hash=sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d \ - --hash=sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710 \ - --hash=sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f \ - --hash=sha256:124d5ba71fee9c9902c4a7baa9425e663f7f0aecf73d31d54fe3dd357d62c1a7 \ - --hash=sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce \ - --hash=sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a \ - --hash=sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077 \ - --hash=sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b \ - --hash=sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f \ - --hash=sha256:215c595c792a87d4407cb72dd5e0f6ee8e694ceeb7f9102b533c5a9bf2a916bb \ - --hash=sha256:22724d80ee5a815a44fc76274bb7ba2e7464f5564aacb6ecddaa9970a83e3225 \ - --hash=sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae \ - --hash=sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7 \ - --hash=sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451 \ - --hash=sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55 \ - --hash=sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804 \ - --hash=sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca \ - --hash=sha256:3d721fee37380a44f9d9ce6c701b3960239f4fb3d5ceea7f31cbd43882edaa2f \ - --hash=sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f \ - --hash=sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667 \ - --hash=sha256:56afaf1e9b02302ba636151cfc49929c1bb66b98794291afd0e5f20fecaf757c \ - --hash=sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467 \ - --hash=sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a \ - --hash=sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27 \ - --hash=sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb \ - --hash=sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b \ - --hash=sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204 \ - --hash=sha256:73b92a5b69f31b1a58c0c7e31080aeaec49c6e01b9522e71ff38d08f15aa56de \ - --hash=sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167 \ - --hash=sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1 \ - --hash=sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee \ - --hash=sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f \ - --hash=sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d \ - --hash=sha256:8ab962931015f170b97a3dd7bd933399c1bae8ed8ad0fb2a7151a5654b6941c7 \ - --hash=sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a \ - --hash=sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b \ - --hash=sha256:8e531abd745f51f8035e207e75e049553a86823d189a51809c078412cefb399a \ - --hash=sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc \ - --hash=sha256:913f629adef31d2d350d41c051ce7e33cf0fd06a5d1cb28d49b1899b23b903aa \ - --hash=sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770 \ - --hash=sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120 \ - --hash=sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2 \ - --hash=sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f \ - --hash=sha256:9d8787bdfbb65a85ea76d0e96a3b1bed7bf0fbcb16d40408dc1172ad784a49d2 \ - --hash=sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc \ - --hash=sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43 \ - --hash=sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872 \ - --hash=sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e \ - --hash=sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be \ - --hash=sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810 \ - --hash=sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6 \ - --hash=sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2 \ - --hash=sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91 \ - --hash=sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc \ - --hash=sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424 \ - --hash=sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e \ - --hash=sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23 \ - --hash=sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1 \ - --hash=sha256:c5189a5dab8b0312eadaf9d58d3049b6a52c454256493a557405e77a3d67ab7f \ - --hash=sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d \ - --hash=sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4 \ - --hash=sha256:d2489b241c19582b3f1430cc5d732caefc1aaf378d97e7fb95b9e56bed11725f \ - --hash=sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229 \ - --hash=sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d \ - --hash=sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf \ - --hash=sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4 \ - --hash=sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038 \ - --hash=sha256:e0a23b41f8f98b4e61150a03f83e4f0d566880fe53519d445a962929a4d21045 \ - --hash=sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633 \ - --hash=sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064 \ - --hash=sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc \ - --hash=sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049 \ - --hash=sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b \ - --hash=sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824 \ - --hash=sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c \ - --hash=sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6 \ - --hash=sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2 \ - --hash=sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e \ - --hash=sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1 \ - --hash=sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569 \ - --hash=sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c - # via trino +oracledb==4.0.2 \ + --hash=sha256:0101b88c15ae628af556506840ae876e12bdb75506ef342e4990ac1758bad1eb \ + --hash=sha256:04fd67692b3cdea2ac7135e6f87c82ce9035f3caa0891a26710a4c654f1e375e \ + --hash=sha256:087fdf5bc36b03dc3c55f7abc944a163ba8edc9c802bc8baf91698a52041ee13 \ + --hash=sha256:0a0be8111db80bb697ec66a34688a6985589e3f72789afe28e3b162b2c4cdd2b \ + --hash=sha256:0a380ab72853487ea2764c5df772f35026b4219868fc3eba68e193c9aea230ac \ + --hash=sha256:14124b5e8e4ba087f105ee389ee43e407840220a0ba2504de68eed903f51a711 \ + --hash=sha256:1b89c02670bafc9b1fcc0d15175f097bdc1968bc36ee4d66aa63aee09bfbbe30 \ + --hash=sha256:1d8933cfb94d9e947cb88dfc87bb2696120b388b89db14394133990585277fb3 \ + --hash=sha256:33bcb765bf97056764c38481dc1dd959e4b61f3a240baacc645d7f06038ce225 \ + --hash=sha256:36e72ffaaf589041dac72ddf589d694c0a9ac4576662a04628966d4ea089d6ea \ + --hash=sha256:3bde3d507d8f50faa9480e99222fe5fa04dc0db2cb8b57746092022ac69ae93d \ + --hash=sha256:41bb34ba8fe76019a958f6d0beba2baf9169ede0bf94e1a22f05f3ca7676f8d7 \ + --hash=sha256:543a7398ebd778d40a839b76a331afa93f506ac143691a6de59ee31ae2e1c4aa \ + --hash=sha256:567c093138d32f06512572de5b5d52459bea82b4e47098e9d668dae759965ccb \ + --hash=sha256:579f2c568433523a990cde5bea73c980d144754dc54d3ab2cd37efd670dc31d6 \ + --hash=sha256:58e71a3c1e294a99e39b086adf42074d48287548bf8edfa720c9507106186bbb \ + --hash=sha256:5c1f13ba535d9f0fc61e1987720988612cf9424667d491fc344ba681f3336a17 \ + --hash=sha256:5fe6e07ed29f84a6656e0580b4e662109d3bb90fc13d8f98ae3a56a67c75b80e \ + --hash=sha256:60cd8a6045674ab481ff33b0bed448f656e7d9abd7fc88c8f349abda5351aad7 \ + --hash=sha256:60f4a4847024f42e8b3be3c7d2597d682ef24f9342b5ebf1695a1bef2aa5be96 \ + --hash=sha256:6444be4991f33754cd98f624cecef3eb98db3e87f8ac14e9b65bd3591c9dd252 \ + --hash=sha256:6c8f3e34a077d7d2d39aa25c28b1aa53fd4fd6aa2437e4d48d9f18b2b28b95e4 \ + --hash=sha256:6d4effb098af3dbcc1fcf785b304523afee1b9a311a8bd9c62bd3b4669198970 \ + --hash=sha256:756db3529dc0f88251f9cbc349cc9c144af2424aae12c1ce24be2fbd9c1b1610 \ + --hash=sha256:78d4a1cc1482b5927a9962c7766aeeae616a85eedbe49af8b797148ac1c1ac32 \ + --hash=sha256:7d023f02f678937bcb6acce7c976569233d193e00c918d7e7b68580db1f68f45 \ + --hash=sha256:864ad6a838e8d41031f117052ee2fac85453b6ecd483d1da6e8fd87701807e5e \ + --hash=sha256:8a7a45fb1042f7258dfbd0b58044a6743c2e8112d0ef5ef9064e17a52968520f \ + --hash=sha256:93b4d2cd17a93224711fadd28f77e956e5c6575cd923f24bfe0b1a2a581beb79 \ + --hash=sha256:9d11723018b6aeae035f4e1feae4150b7cca1161023c2d68d957d7310fe0dd56 \ + --hash=sha256:a1f46b01a089e0e0dd44c4ac4c5c79903760976346c74a698955c1fb76d68bf5 \ + --hash=sha256:a45a13e33509db5bd3622a05cb2e4a6cb41d56d1bfd2a32caeacd26f4ce8b988 \ + --hash=sha256:a814307ca8a6bf0649d8bf0a650a72f23030d9af052a2d4a481b94b6ab50d5a4 \ + --hash=sha256:af4121112f0b68e8d61ce46a086c6aec8256fa2ca32d2e6467195a6c575c207b \ + --hash=sha256:b14d52c09b3c7a8273b3640ba06675acec5000dab0356ef288587c46c30bbbb4 \ + --hash=sha256:b4f2b51837248d4acfb323a64c48d89b62467b0f73d1211c99db9b80df0cdfbc \ + --hash=sha256:b5d203426f0d191842b4cfd87c223163ccc57f7e7875ec7ed073f163de2229af \ + --hash=sha256:b82d3d83bdc6246e53f8ad8b598d2508e9f5d983e352ce2e8a71a020200ba2d6 \ + --hash=sha256:c193d2636f9c2e8549d24cc53947a8381c7b9488e0dae9cea5cf5bce30dc7313 \ + --hash=sha256:cb70b231254ad82ff0639b8688e0e528100a984df65fcd7624bae88fb930e700 \ + --hash=sha256:cbe3a75b463a334da9b3d76026062bcfb24ec563b7df291f700c9f7eefcbeefe \ + --hash=sha256:d5a82a449d96fb89a93346d88128ad618526ffa290376d4b57fbcee1eedcd19f \ + --hash=sha256:d8a83c120d8177f344bedf7698bc1af5f6fd031851031d3e20ced632286e3e84 \ + --hash=sha256:db0d76199b6dffd721bda48dad4eeea6603942ce2e246f3b9301d85af6bda0ee \ + --hash=sha256:f5ed684ab419603d53981e0f32b51da673e21a42d7dce1bf3f215a9301c7c108 \ + --hash=sha256:f8ac435434193a1d88fcda6bfd9387c5562240d4f2e8de1f9ed20c64d5f0ff90 \ + --hash=sha256:ff8a239de950acb600ab4fbe1b349fb085a75383bc9a163e50cab362293fc936 + # via ibis-framework +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via + # pymilvus + # trino overrides==7.7.0 \ --hash=sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a \ --hash=sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49 # via jupyter-server -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f +packaging==25.0 \ + --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ + --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via # accelerate # build @@ -3144,14 +3723,12 @@ packaging==24.2 \ # datasets # db-dtypes # deprecation - # dunamai # faiss-cpu # google-cloud-bigquery # great-expectations # gunicorn # huggingface-hub # ibis-framework - # ibis-substrait # ipykernel # jupyter-events # jupyter-server @@ -3159,7 +3736,11 @@ packaging==24.2 \ # jupyterlab-server # lazy-loader # marshmallow + # matplotlib + # mlflow-skinny + # mlflow-tracing # nbconvert + # openlineage-python # pandas-gbq # pytest # ray @@ -3167,6 +3748,7 @@ packaging==24.2 \ # snowflake-connector-python # sphinx # transformers + # wheel pandas==2.3.3 \ --hash=sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7 \ --hash=sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593 \ @@ -3224,7 +3806,7 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # altair # dask # datasets @@ -3234,29 +3816,30 @@ pandas==2.3.3 \ # google-cloud-bigquery # great-expectations # ibis-framework + # mlflow # pandas-gbq # pymilvus # ray # snowflake-connector-python -pandas-gbq==0.29.2 \ - --hash=sha256:99f6b642bd8340e96c2acdf7d717108aa0d31c703c2f284817ba7dc3b16110b2 \ - --hash=sha256:a3a28e7de4f8456fc4b8e9b7472b9bc470d2566f89c4cd997599e504f5a0a706 +pandas-gbq==0.30.0 \ + --hash=sha256:8fe811786e4ad2e0d4608e897534207d9fbe768ab3168f766a99f0cb4cd5ed20 \ + --hash=sha256:d9b4454b17aee3c23ef1dfcfd91df6e2b77f1e69e1e4b28467701cd75850664a # via google-cloud-bigquery pandocfilters==1.5.1 \ --hash=sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e \ --hash=sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc # via nbconvert -paramiko==4.0.0 \ - --hash=sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9 \ - --hash=sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f +paramiko==5.0.0 \ + --hash=sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79 \ + --hash=sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c # via openshift-client -parsimonious==0.10.0 \ - --hash=sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c \ - --hash=sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f +parsimonious==0.11.0 \ + --hash=sha256:32e3818abf9f05b3b9f3b6d87d128645e30177e91f614d2277d88a0aea98fae2 \ + --hash=sha256:e080377d98957beec053580d38ae54fcdf7c470fb78670ba4bf8b5f9d5cad2a9 # via singlestoredb -parso==0.8.5 \ - --hash=sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a \ - --hash=sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887 +parso==0.8.7 \ + --hash=sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c \ + --hash=sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1 # via jedi parsy==2.2 \ --hash=sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 \ @@ -3266,9 +3849,9 @@ partd==1.4.2 \ --hash=sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f \ --hash=sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c # via dask -pbr==7.0.1 \ - --hash=sha256:32df5156fbeccb6f8a858d1ebc4e465dcf47d6cc7a4895d5df9aa951c712fc35 \ - --hash=sha256:3ecbcb11d2b8551588ec816b3756b1eb4394186c3b689b17e04850dfc20f7e57 +pbr==7.0.3 \ + --hash=sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29 \ + --hash=sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b # via mock pexpect==4.9.0 \ --hash=sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 \ @@ -3382,24 +3965,25 @@ pillow==11.3.0 \ --hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \ --hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3 # via - # feast (setup.py) + # feast (pyproject.toml) # docling # docling-core # docling-ibm-models # docling-parse # easyocr # imageio + # matplotlib # python-pptx # scikit-image # torchvision -pip==25.2 \ - --hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \ - --hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717 +pip==26.1.2 \ + --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ + --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 # via pip-tools -pip-tools==7.5.1 \ - --hash=sha256:a051a94794ba52df9acad2d7c9b0b09ae001617db458a543f8287fea7b89c2cf \ - --hash=sha256:f5ff803823529edc0e6e40c86b1aa7da7266fb1078093c8beea4e5b77877036a - # via feast (setup.py) +pip-tools==7.6.0 \ + --hash=sha256:4bd99155b6d8de358a214b0865e1a2855a453570c1a83d40f7b564870b8657be \ + --hash=sha256:c1c59f7844df4866fa9542d3f50d1f44be537ac0027cb50b2563d6a992853981 + # via feast (pyproject.toml) platformdirs==3.11.0 \ --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e @@ -3418,14 +4002,6 @@ ply==3.11 \ --hash=sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3 \ --hash=sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce # via thriftpy2 -poetry-core==1.9.1 \ - --hash=sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0 \ - --hash=sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8 - # via feast (setup.py) -poetry-dynamic-versioning==1.9.1 \ - --hash=sha256:65a0c814e6d30d4807734a3c34edf261fd7cc3b340dbd23b6a33ee41f7d0b547 \ - --hash=sha256:d6e7b9df817aa2ca4946cd695c6c89e1379d2e6c640f008a9b6170d081a9da48 - # via feast (setup.py) portalocker==3.2.0 \ --hash=sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac \ --hash=sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968 @@ -3433,167 +4009,166 @@ portalocker==3.2.0 \ pre-commit==3.3.1 \ --hash=sha256:218e9e3f7f7f3271ebc355a15598a4d3893ad9fc7b57fe446db75644543323b9 \ --hash=sha256:733f78c9a056cdd169baa6cd4272d51ecfda95346ef8a89bf93712706021b907 - # via feast (setup.py) -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 + # via feast (pyproject.toml) +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 # via - # feast (setup.py) + # feast (pyproject.toml) # jupyter-server # opentelemetry-exporter-prometheus # ray -prompt-toolkit==3.0.52 \ - --hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \ - --hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 +prompt-toolkit==3.0.53 \ + --hash=sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2 \ + --hash=sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6 # via ipython -propcache==0.4.1 \ - --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ - --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ - --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ - --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ - --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ - --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ - --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ - --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ - --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ - --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274 \ + --hash=sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.26.1 \ - --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ - --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +proto-plus==1.28.2 \ + --hash=sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501 \ + --hash=sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52 # via # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==4.25.8 \ - --hash=sha256:077ff8badf2acf8bc474406706ad890466274191a48d0abd3bd6987107c9cde5 \ - --hash=sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59 \ - --hash=sha256:27d498ffd1f21fb81d987a041c32d07857d1d107909f5134ba3350e1ce80a4af \ - --hash=sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0 \ - --hash=sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd \ - --hash=sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0 \ - --hash=sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7 \ - --hash=sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9 \ - --hash=sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f \ - --hash=sha256:d552c53d0415449c8d17ced5c341caba0d89dbf433698e1436c8fa0aae7808a3 \ - --hash=sha256:f4510b93a3bec6eba8fd8f1093e9d7fb0d4a24d1a81377c10c0e5bbfe9e4ed24 - # via - # feast (setup.py) +protobuf==6.33.6 \ + --hash=sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326 \ + --hash=sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901 \ + --hash=sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3 \ + --hash=sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a \ + --hash=sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135 \ + --hash=sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e \ + --hash=sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3 \ + --hash=sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2 \ + --hash=sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593 \ + --hash=sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf + # via + # feast (pyproject.toml) + # databricks-sdk # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -3605,14 +4180,14 @@ protobuf==4.25.8 \ # grpcio-status # grpcio-testing # grpcio-tools - # ikvpy + # mlflow-skinny + # mlflow-tracing # mypy-protobuf # opentelemetry-proto # proto-plus # pymilvus # qdrant-client # ray - # substrait psutil==5.9.0 \ --hash=sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5 \ --hash=sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a \ @@ -3647,83 +4222,73 @@ psutil==5.9.0 \ --hash=sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c \ --hash=sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3 # via - # feast (setup.py) + # feast (pyproject.toml) # accelerate # ipykernel -psycopg[binary, pool]==3.2.5 \ - --hash=sha256:b782130983e5b3de30b4c529623d3687033b4dafa05bb661fc6bf45837ca5879 \ - --hash=sha256:f5f750611c67cb200e85b408882f29265c66d1de7f813add4f8125978bfd70e8 - # via feast (setup.py) -psycopg-binary==3.2.5 \ - --hash=sha256:02fb96091e2fb3ea1470b113fef08953baaedbca1d39a3f72d82cb615177846c \ - --hash=sha256:11e3ed8b94c750d54fc3e4502dd930fb0fd041629845b6a7ce089873ac9756b0 \ - --hash=sha256:1494827c43265820d5dcdc6f8086521bc7dd04b9da8831310978a788cdcd2e62 \ - --hash=sha256:21b839f9bfd77ed074f7f71464a43f453400c57d038a0ba0716329a28e335897 \ - --hash=sha256:23a1dc61abb8f7cc702472ab29554167a9421842f976c201ceb3b722c0299769 \ - --hash=sha256:274e852f9e61252bc8e80a0a43d300ba352d40219e856733054023a3bb960eb4 \ - --hash=sha256:28bd5cb2324567e5e70f07fe1d646398d6b0e210e28b49be0e69593590a59980 \ - --hash=sha256:2b053eae21dd3a6828b516a1171e1274d1af5f7c07d2d9a8f597f2e19c732168 \ - --hash=sha256:2cbb8649cfdacbd14e17f5ab78edc52d33350013888518c73e90c5d17d7bea55 \ - --hash=sha256:2cc86657c05e09c701e97f87132cd58e0d55381dd568520081ac1fe7580a9bbb \ - --hash=sha256:2d10ce4c39eb9631381a0c3792727946a4391e843625a7ee9579ac6bb11495a5 \ - --hash=sha256:2d22a15e45f43d36ed35aed4d5261f8ef6ab7d9b84ee075576ca56ae03b9e0aa \ - --hash=sha256:2dbaf32c18c0d11c4480016b89c9c5cadb7b64c55de7f181d222b189bd13a558 \ - --hash=sha256:32b5673736f04c36ccbf8012800fe5bc01b46dac22c5d59e41b043bebaad9d3d \ - --hash=sha256:375149006e21d58ed8aba640e0295d8e636043064c433af94eb58057f9b96877 \ - --hash=sha256:393ab353196d364858b47317d27804ecc58ab56dbde32217bd67f0f2f2980662 \ - --hash=sha256:39e2cd10bf15442d95c3f48376b25dc33360418ea6c3c05884d8bf42407768c0 \ - --hash=sha256:3d2e57a1d06f3968e49e948ba374f21a7d8dcf44f37d582a4aeddeb7c85ce239 \ - --hash=sha256:3eb71cfc35116e4a8e336b7e785f1fe06ca23b4516a48ea91facd577d1a1fdf6 \ - --hash=sha256:3f893c0ed3d5c7b83b76b1f8f7d3ca5a03e38bcd3cab5d65b5c25a0d1064aca4 \ - --hash=sha256:473f6827cf1faf3924eb77146d1e85126a1b5e48a88053b8d8b78dd29e971d78 \ - --hash=sha256:48f97936145cb7de18b95d85670b2d3e2c257277263272be05815b74fb0ef195 \ - --hash=sha256:48fcb12a0a72fdfe4102bdb1252a7366e8d73a2c89fe6ce5923be890de367c2f \ - --hash=sha256:4914dc60f2fddf0884464985e31d775aa865b665471fa156ec2f56fa72a1a097 \ - --hash=sha256:51a96d9fe51f718912b4a0089784f1f32d800217499fd0f0095b888506aba4c5 \ - --hash=sha256:5244bebaa9734a236b7157fb57c065b6c0f2344281916187bd73f951df1899e0 \ - --hash=sha256:5b81342e139ddccfa417832089cd213bd4beacd7a1462ca4019cafe71682d177 \ - --hash=sha256:5d2253189aa4cca0a425e2ca896d1a29760cd3a2b10ab12194e4e827a566505c \ - --hash=sha256:5fd017d7ed71c58f19b0f614e7bfb8f01ec862bacb67ae584f494d090956102e \ - --hash=sha256:605f70e267222d567fc40de7813ee3fb29f8145a1a20aa6fd3dc62baba9312f1 \ - --hash=sha256:60d0f36a42a822e43c4c7472df8a0c980c0f32e5d74ed871333c423a4e942f11 \ - --hash=sha256:62965045cc0fe3dc5dd55d39779620b225ef75962825c7b1b533033cb91810bd \ - --hash=sha256:65162a9cc3f86d70b1d895dbda506e3c079f80d082eb41c54d3f6d33a00b3965 \ - --hash=sha256:659f2c675d478b1bc01b95a8d3ded74fa939b370e71ffbecd496f617b215eb05 \ - --hash=sha256:6b581da13126b8715c0c0585cd37ce934c9864d44b2a4019f5487c0b943275e6 \ - --hash=sha256:71d82dbc7c6c7f5746468e7992e5483aa45b12250d78d220a2431ab88795825c \ - --hash=sha256:7376b13504396da9678b646f5338462347da01286b2a688a0d8493ec764683a2 \ - --hash=sha256:7623659d44a6aa032be4a066c658ba45009d768c2481526fbef7c609702af116 \ - --hash=sha256:7a94020821723a6a210206ddb458001f3ed27e1e6a0555b9422bebf7ead8ff37 \ - --hash=sha256:7d5f1bfc848a94e0d63fe693adee4f88bd9e5c415ecb4c9c17d2d44eba6795a6 \ - --hash=sha256:7efe6c732fd2d7e22d72dc4f7cf9b644020adacfff61b0a8a151343da8e661c0 \ - --hash=sha256:8a602d9fdb567cca090ca19ac3ebf10219065be2a4f8cf9eb8356cffb5a7ab1d \ - --hash=sha256:8cd9ebf335262e864d740f9dad3f672f61162cc0d4825a5eb5cf50df334a688f \ - --hash=sha256:8e6f2bef5aed021fbdf46323d3cd8847bf960efb56394698644a8ee2306f8892 \ - --hash=sha256:93221d5a759bd39b1face1d7d887d2b9ede3e55aefaff8eacf1b663ccdcd204b \ - --hash=sha256:9639289b72f9339721982e156527c296693236d6192ccc31412ab36fccd1683c \ - --hash=sha256:98efaedf2bf79f4d563ca039a57a025b72847bd80568f54709cc39fc1404772c \ - --hash=sha256:9abe093a303e25ac58774a11241150e2fe2947358d1ca12521ad03c90b131060 \ - --hash=sha256:a4321ee8180982d70458d3e8378e31448901bf0ee40fe0d410a87413578f4098 \ - --hash=sha256:a82211a43372cba9b1555a110e84e679deec2dc9463ae4c736977dad99dca5ed \ - --hash=sha256:a91b0e096fdfeb52d86bb8f5ee25dc22483d6960af9b968e6b381a8ec5bfbf82 \ - --hash=sha256:b5e0acbc991472188c9df40eb56d8a97ad3ad00d4de560b8b74bdc2d94041a8f \ - --hash=sha256:b6b5a4542aca4095ab35e184517cb0d18895ba4b6661c92865b431fa7b7974d8 \ - --hash=sha256:ba4a610882171bdaae0779f14e0ff45f3ee271fd2dbf16cdadfc81bd67323232 \ - --hash=sha256:bc5bd9bf5f5894923b78a41c5becd52d6bced1e1e43744855bd85cb341376ca6 \ - --hash=sha256:c37eb3be7a6be93f4925ccf52bbfa60244da6c63201770a709dd81a3d2d08534 \ - --hash=sha256:c3c5fa3d4fa0a651cefab391b783f89bc5e331afa0a4e93c9b16141993fa05c8 \ - --hash=sha256:ca5e36a3e7480a5c09aed99ecdb8e6554b21485c3b064297fe77f7b1b5806106 \ - --hash=sha256:d4e0c1b1aa5283f6d9a384ffc7a8400d25386bb98fdb9bddae446e4ef4da7366 \ - --hash=sha256:dc8bc40d82d1ee8dec136e10707c7f3147a6322fd8014e174a0f3446fb793649 \ - --hash=sha256:de576c49d7deab2b78088feb24e1f6ae3e16a0020e8496cdd3b8543f5e350e87 \ - --hash=sha256:e7d215a43343d91ba08301865f059d9518818d66a222a85fb425e4156716f5a6 \ - --hash=sha256:eb8293d66c6a4ddc72fceb7ad0e111cb196cc394954ae0f9b63c251d97f1b00e \ - --hash=sha256:ee6d8f489a9b116ea8dc797664a50671585a4ca20573359f067858e1231cc217 \ - --hash=sha256:efb878d08dd49d7d9d18512e791b418a1171d08f935475eec98305f0886b7c14 +psycopg[binary, pool]==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via feast (pyproject.toml) +psycopg-binary==3.3.4 \ + --hash=sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070 \ + --hash=sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c \ + --hash=sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc \ + --hash=sha256:13a7f380824c35896dcac7fe0f61440f7ca49d6dc73f3c13a9a4471e6a3b302e \ + --hash=sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68 \ + --hash=sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae \ + --hash=sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829 \ + --hash=sha256:22cdbf5f91ef7bb91fe0c5757e1962d3127a8010256eefd9c61fcaf441802097 \ + --hash=sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c \ + --hash=sha256:276904e3452d6a23d474ef9a21eee19f20eed3d53ddd2576af033827e0ba0992 \ + --hash=sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97 \ + --hash=sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6 \ + --hash=sha256:32a6fbf8481e3a370d0d72b860d35948a693cb01281da217f7b2f307636e591a \ + --hash=sha256:41f2ec0fea529832982bcb6c9415de3c86264ebe562b77a467c0fbcd7efbba8d \ + --hash=sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228 \ + --hash=sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e \ + --hash=sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4 \ + --hash=sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41 \ + --hash=sha256:574ea21a9651958f1535c5a1c649c7409e9168bcbffa29a3f2f961f58b322949 \ + --hash=sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9 \ + --hash=sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089 \ + --hash=sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf \ + --hash=sha256:612a627d733f695b1de1f9b4bd511c15f999a5d8b915d444bbd7dd71cf3370da \ + --hash=sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578 \ + --hash=sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014 \ + --hash=sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e \ + --hash=sha256:7465bfe6087d2d5b42d4c53b9b11ca9f218e477317a4a162a10e3c19e984ba8e \ + --hash=sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31 \ + --hash=sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652 \ + --hash=sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b \ + --hash=sha256:7f7668f30b9dd5163197e5cbf4e0efd54e00f0a859cc566ce56cfc31f4054839 \ + --hash=sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277 \ + --hash=sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7 \ + --hash=sha256:ab8cca8ef8fb1ccf5b048ae5bd78ba55b9e4b5d472e3ce5ca39ff4d2a9c249e4 \ + --hash=sha256:ad3bc94054876155549fdaedf4a46d1ec69d39a5bcee377148afe498e84c4b8e \ + --hash=sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70 \ + --hash=sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf \ + --hash=sha256:b7bfff1ca23732b488cbca3076fc11bc98d520ee122514fdb17a8e20d3338f5a \ + --hash=sha256:bdef84570ebbce1d42b4e7ea952d21c414c5f118ad02fee00c5625f35e134429 \ + --hash=sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8 \ + --hash=sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3 \ + --hash=sha256:cf7f73a4a792bc5db58a4b385d8a1467e8d468f7548702fb0ed1e9b7501b1c13 \ + --hash=sha256:cffc3408d77a27973f33e5d909b624cce683db5fc25964b02fe0aae7886c1007 \ + --hash=sha256:d7b4d40c153fa352ab3cca530f3a0baedf7621b2ebcbd7f084009522c21788fc \ + --hash=sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38 \ + --hash=sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9 \ + --hash=sha256:e2631da29253a98bd496e6c4813b24e09a4fe3fb2a9e88513305d6f8747cce95 \ + --hash=sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d \ + --hash=sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16 \ + --hash=sha256:eb4eed2079c01a4850bf467deacfab56d356d4225040170af03dc9958321242d \ + --hash=sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260 \ + --hash=sha256:f80e3f2b5331dbbf0901bcb658056c03eeb2c1ef31d774afb0d61598b242e744 \ + --hash=sha256:f9b1c2533af01cd7648378599f82b0b8ae32f293296e6eec5753a625bc97ef28 \ + --hash=sha256:fa1cbc10768a796c96d3243656016bf4e337c81c71097270bb7b0ad6210d9765 \ + --hash=sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7 # via psycopg -psycopg-pool==3.2.6 \ - --hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \ - --hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7 +psycopg-pool==3.3.1 \ + --hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \ + --hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c # via psycopg ptyprocess==0.7.0 \ --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \ @@ -3738,70 +4303,79 @@ pure-eval==0.2.3 \ py==1.11.0 \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via feast (setup.py) + # via feast (pyproject.toml) py-cpuinfo==9.0.0 \ --hash=sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690 \ --hash=sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5 # via pytest-benchmark -py-spy==0.4.1 \ - --hash=sha256:1fb8bf71ab8df95a95cc387deed6552934c50feef2cf6456bc06692a5508fd0c \ - --hash=sha256:4972c21890b6814017e39ac233c22572c4a61fd874524ebc5ccab0f2237aee0a \ - --hash=sha256:532d3525538254d1859b49de1fbe9744df6b8865657c9f0e444bf36ce3f19226 \ - --hash=sha256:6a80ec05eb8a6883863a367c6a4d4f2d57de68466f7956b6367d4edd5c61bb29 \ - --hash=sha256:809094208c6256c8f4ccadd31e9a513fe2429253f48e20066879239ba12cd8cc \ - --hash=sha256:d92e522bd40e9bf7d87c204033ce5bb5c828fca45fa28d970f58d71128069fdc \ - --hash=sha256:e53aa53daa2e47c2eef97dd2455b47bb3a7e7f962796a86cc3e7dbde8e6f4db4 \ - --hash=sha256:ee776b9d512a011d1ad3907ed53ae32ce2f3d9ff3e1782236554e22103b5c084 +py-spy==0.4.2 \ + --hash=sha256:142887e984a4e541071c99a4401ff8c3770f255d329dbd0f64e8c1dd51882cce \ + --hash=sha256:1ccf688393105111684435f035bc14ec3f22117dd2b85b2414612cf27a22755a \ + --hash=sha256:24720573f95230653b457671a1dcc3c5a381fcf4e92677761e328a430ad251b2 \ + --hash=sha256:7f1c6d9b0e2379ead5bf792df43f4cf36153aa79e6dda4fb8ac7740cf8017110 \ + --hash=sha256:8b06a353c177677e4e1701b288d8c58e2f8d4208ee81a8048d9f72ba800918f8 \ + --hash=sha256:90e600b27bb6bb40479637baca5a5b4bc2ba3395c93d889e672315d93042c4ae \ + --hash=sha256:a0e6f6810ccf0fc5e64e85e0182a5b626c4496eec01b14fb8755154b363a4831 \ + --hash=sha256:aeb0323409199c785f730645e9f4bb7a7b9ca2c481f2c331a55642b5d13fa52f # via ray py4j==0.10.9.9 \ --hash=sha256:c7c26e4158defb37b0bb124933163641a2ff6e3a3913f7811b0ddbe07ed61533 \ --hash=sha256:f694cad19efa5bd1dee4f3e5270eb406613c974394035e5bfc4ec1aba870b879 # via pyspark -pyarrow==17.0.0 \ - --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ - --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ - --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ - --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ - --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ - --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ - --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ - --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ - --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ - --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ - --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ - --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ - --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ - --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ - --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ - --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ - --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ - --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ - --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ - --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ - --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ - --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ - --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ - --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ - --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ - --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ - --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ - --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ - --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ - --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ - --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ - --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ - --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ - --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ - --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ - --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 - # via - # feast (setup.py) +pyarrow==21.0.0 \ + --hash=sha256:067c66ca29aaedae08218569a114e413b26e742171f526e828e1064fcdec13f4 \ + --hash=sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623 \ + --hash=sha256:0c4e75d13eb76295a49e0ea056eb18dbd87d81450bfeb8afa19a7e5a75ae2ad7 \ + --hash=sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636 \ + --hash=sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7 \ + --hash=sha256:203003786c9fd253ebcafa44b03c06983c9c8d06c3145e37f1b76a1f317aeae1 \ + --hash=sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10 \ + --hash=sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51 \ + --hash=sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd \ + --hash=sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8 \ + --hash=sha256:3b4d97e297741796fead24867a8dabf86c87e4584ccc03167e4a811f50fdf74d \ + --hash=sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569 \ + --hash=sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e \ + --hash=sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc \ + --hash=sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6 \ + --hash=sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c \ + --hash=sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82 \ + --hash=sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79 \ + --hash=sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6 \ + --hash=sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10 \ + --hash=sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61 \ + --hash=sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d \ + --hash=sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb \ + --hash=sha256:898afce396b80fdda05e3086b4256f8677c671f7b1d27a6976fa011d3fd0a86e \ + --hash=sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e \ + --hash=sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594 \ + --hash=sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634 \ + --hash=sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da \ + --hash=sha256:a7f6524e3747e35f80744537c78e7302cd41deee8baa668d56d55f77d9c464b3 \ + --hash=sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876 \ + --hash=sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e \ + --hash=sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a \ + --hash=sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b \ + --hash=sha256:cdc4c17afda4dab2a9c0b79148a43a7f4e1094916b3e18d8975bfd6d6d52241f \ + --hash=sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18 \ + --hash=sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe \ + --hash=sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99 \ + --hash=sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26 \ + --hash=sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d \ + --hash=sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a \ + --hash=sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd \ + --hash=sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503 \ + --hash=sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79 + # via + # feast (pyproject.toml) # dask # datasets # db-dtypes # deltalake # google-cloud-bigquery # ibis-framework + # milvus-lite + # mlflow # pandas-gbq # ray # snowflake-connector-python @@ -3809,12 +4383,10 @@ pyarrow-hotfix==0.7 \ --hash=sha256:3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 \ --hash=sha256:59399cd58bdd978b2e42816a4183a55c6472d4e33d183351b6069f11ed42661d # via ibis-framework -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 @@ -3822,62 +4394,48 @@ pyasn1-modules==0.4.2 \ pybindgen==0.22.0 \ --hash=sha256:21612f4806a2af25a175716d7e694563af7e10c704538a350cb595d187952f6f \ --hash=sha256:5b4837d3138ac56863d93fe462f1dac39fb87bd50898e0da4c57fefd645437ac - # via feast (setup.py) -pyclipper==1.3.0.post6 \ - --hash=sha256:04214d23cf79f4ddcde36e299dea9f23f07abb88fa47ef399bf0e819438bbefd \ - --hash=sha256:106b8622cd9fb07d80cbf9b1d752334c55839203bae962376a8c59087788af26 \ - --hash=sha256:16cc1705a915896d2aff52131c427df02265631279eac849ebda766432714cc0 \ - --hash=sha256:188fbfd1d30d02247f92c25ce856f5f3c75d841251f43367dbcf10935bc48f38 \ - --hash=sha256:1c03f1ae43b18ee07730c3c774cc3cf88a10c12a4b097239b33365ec24a0a14a \ - --hash=sha256:1fd56855ca92fa7eb0d8a71cf3a24b80b9724c8adcc89b385bbaa8924e620156 \ - --hash=sha256:2737df106b8487103916147fe30f887aff439d9f2bd2f67c9d9b5c13eac88ccf \ - --hash=sha256:28bb590ae79e6beb15794eaee12b6f1d769589572d33e494faf5aa3b1f31b9fa \ - --hash=sha256:2e257009030815853528ba4b2ef7fb7e172683a3f4255a63f00bde34cfab8b58 \ - --hash=sha256:32cd7fb9c1c893eb87f82a072dbb5e26224ea7cebbad9dc306d67e1ac62dd229 \ - --hash=sha256:33ab72260f144693e1f7735e93276c3031e1ed243a207eff1f8b98c7162ba22c \ - --hash=sha256:3404dfcb3415eee863564b5f49be28a8c7fb99ad5e31c986bcc33c8d47d97df7 \ - --hash=sha256:383f3433b968f2e4b0843f338c1f63b85392b6e1d936de722e8c5d4f577dbff5 \ - --hash=sha256:3d58202de8b8da4d1559afbda4e90a8c260a5373672b6d7bc5448c4614385144 \ - --hash=sha256:3e5e65176506da6335f6cbab497ae1a29772064467fa69f66de6bab4b6304d34 \ - --hash=sha256:42bff0102fa7a7f2abdd795a2594654d62b786d0c6cd67b72d469114fdeb608c \ - --hash=sha256:47a214f201ff930595a30649c2a063f78baa3a8f52e1f38da19f7930c90ed80c \ - --hash=sha256:48dd55fbd55f63902cad511432ec332368cbbbc1dd2110c0c6c1e9edd735713a \ - --hash=sha256:491ec1bfd2ee3013269c2b652dde14a85539480e0fb82f89bb12198fa59fff82 \ - --hash=sha256:58eae2ff92a8cae1331568df076c4c5775bf946afab0068b217f0cf8e188eb3c \ - --hash=sha256:5c9c80b5c46eef38ba3f12dd818dc87f5f2a0853ba914b6f91b133232315f526 \ - --hash=sha256:6363b9d79ba1b5d8f32d1623e797c1e9f994600943402e68d5266067bdde173e \ - --hash=sha256:640f20975727994d4abacd07396f564e9e5665ba5cb66ceb36b300c281f84fa4 \ - --hash=sha256:6893f9b701f3132d86018594d99b724200b937a3a3ddfe1be0432c4ff0284e6e \ - --hash=sha256:793b0aa54b914257aa7dc76b793dd4dcfb3c84011d48df7e41ba02b571616eaf \ - --hash=sha256:851b3e58106c62a5534a1201295fe20c21714dee2eda68081b37ddb0367e6caa \ - --hash=sha256:903176952a159c4195b8be55e597978e24804c838c7a9b12024c39704d341f72 \ - --hash=sha256:9699e98862dadefd0bea2360c31fa61ca553c660cbf6fb44993acde1b959f58f \ - --hash=sha256:9cbdc517e75e647aa9bf6e356b3a3d2e3af344f82af38e36031eb46ba0ab5425 \ - --hash=sha256:a01f182d8938c1dc515e8508ed2442f7eebd2c25c7d5cb29281f583c1a8008a4 \ - --hash=sha256:a63002f6bb0f1efa87c0b81634cbb571066f237067e23707dabf746306c92ba5 \ - --hash=sha256:aa0e7268f8ceba218964bc3a482a5e9d32e352e8c3538b03f69a6b3db979078d \ - --hash=sha256:aa604f8665ade434f9eafcd23f89435057d5d09427dfb4554c5e6d19f6d8aa1a \ - --hash=sha256:ace1f0753cf71c5c5f6488b8feef5dd0fa8b976ad86b24bb51f708f513df4aac \ - --hash=sha256:b15113ec4fc423b58e9ae80aa95cf5a0802f02d8f02a98a46af3d7d66ff0cc0e \ - --hash=sha256:c05ae2ea878fdfa31dd375326f6191b03de98a9602cc9c2b6d4ff960b20a974c \ - --hash=sha256:c4247e7c44b34c87acbf38f99d48fb1acaf5da4a2cf4dcd601a9b24d431be4ef \ - --hash=sha256:c92e41301a8f25f9adcd90954512038ed5f774a2b8c04a4a9db261b78ff75e3a \ - --hash=sha256:cf0a535cfa02b207435928e991c60389671fe1ea1dfae79170973f82f52335b2 \ - --hash=sha256:cf5ca2b9358d30a395ac6e14b3154a9fd1f9b557ad7153ea15cf697e88d07ce1 \ - --hash=sha256:d3f9da96f83b8892504923beb21a481cd4516c19be1d39eb57a92ef1c9a29548 \ - --hash=sha256:d6d129d0c2587f2f5904d201a4021f859afbb45fada4261c9fdedb2205b09d23 \ - --hash=sha256:dbc828641667142751b1127fd5c4291663490cf05689c85be4c5bcc89aaa236a \ - --hash=sha256:e2cd8600bd16d209d5d45a33b45c278e1cc8bedc169af1a1f2187b581c521395 \ - --hash=sha256:e3aab10e3c10ed8fa60c608fb87c040089b83325c937f98f06450cf9fcfdaf1d \ - --hash=sha256:e5ff68fa770ac654c7974fc78792978796f068bd274e95930c0691c31e192889 \ - --hash=sha256:ed6e50c6e87ed190141573615d54118869bd63e9cd91ca5660d2ca926bf25110 \ - --hash=sha256:f129284d2c7bcd213d11c0f35e1ae506a1144ce4954e9d1734d63b120b0a1b58 \ - --hash=sha256:fa0f5e78cfa8262277bb3d0225537b3c2a90ef68fd90a229d5d24cf49955dcf4 \ - --hash=sha256:fb1e52cf4ee0a9fa8b2254ed589cc51b0c989efc58fa8804289aca94a21253f7 + # via feast (pyproject.toml) +pyclipper==1.4.0 \ + --hash=sha256:0a4d2736fb3c42e8eb1d38bf27a720d1015526c11e476bded55138a977c17d9d \ + --hash=sha256:0b74a9dd44b22a7fd35d65fb1ceeba57f3817f34a97a28c3255556362e491447 \ + --hash=sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b \ + --hash=sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f \ + --hash=sha256:1b6c8d75ba20c6433c9ea8f1a0feb7e4d3ac06a09ad1fd6d571afc1ddf89b869 \ + --hash=sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140 \ + --hash=sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04 \ + --hash=sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a \ + --hash=sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4 \ + --hash=sha256:58e29d7443d7cc0e83ee9daf43927730386629786d00c63b04fe3b53ac01462c \ + --hash=sha256:6a97b961f182b92d899ca88c1bb3632faea2e00ce18d07c5f789666ebb021ca4 \ + --hash=sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826 \ + --hash=sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9 \ + --hash=sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c \ + --hash=sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e \ + --hash=sha256:8d42b07a2f6cfe2d9b87daf345443583f00a14e856927782fde52f3a255e305a \ + --hash=sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1 \ + --hash=sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87 \ + --hash=sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39 \ + --hash=sha256:a8d2b5fb75ebe57e21ce61e79a9131edec2622ff23cc665e4d1d1f201bc1a801 \ + --hash=sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9 \ + --hash=sha256:adcb7ca33c5bdc33cd775e8b3eadad54873c802a6d909067a57348bcb96e7a2d \ + --hash=sha256:b3b3630051b53ad2564cb079e088b112dd576e3d91038338ad1cc7915e0f14dc \ + --hash=sha256:bafad70d2679c187120e8c44e1f9a8b06150bad8c0aecf612ad7dfbfa9510f73 \ + --hash=sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286 \ + --hash=sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c \ + --hash=sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872 \ + --hash=sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037 \ + --hash=sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca \ + --hash=sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e \ + --hash=sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832 \ + --hash=sha256:e9b973467d9c5fa9bc30bb6ac95f9f4d7c3d9fc25f6cf2d1cc972088e5955c01 \ + --hash=sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303 \ + --hash=sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1 \ + --hash=sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6 \ + --hash=sha256:fd24849d2b94ec749ceac7c34c9f01010d23b6e9d9216cf2238b8481160e703d # via easyocr -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi pycryptodome==3.23.0 \ --hash=sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4 \ @@ -3922,12 +4480,13 @@ pycryptodome==3.23.0 \ --hash=sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be \ --hash=sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7 # via minio -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # codeflare-sdk + # dbt-artifacts-parser # docling # docling-core # docling-ibm-models @@ -3936,290 +4495,434 @@ pydantic==2.10.6 \ # fastapi-mcp # great-expectations # mcp + # mcp-types + # mlflow-skinny + # mlflow-tracing # pydantic-settings + # pyiceberg # qdrant-client # ray -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pydantic-settings==2.11.0 \ - --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ - --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f # via # docling + # docling-core # fastapi-mcp - # mcp pydata-google-auth==1.9.1 \ --hash=sha256:0a51ce41c601ca0bc69b8795bf58bedff74b4a6a007c9106c7cbcdec00eaced2 \ --hash=sha256:75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 # via pandas-gbq -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via - # feast (setup.py) + # feast (pyproject.toml) # ipython # ipython-pygments-lexers # mpire # nbconvert # rich # sphinx -pyjwt[crypto]==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb - # via - # feast (setup.py) +pyiceberg==0.11.1 \ + --hash=sha256:138983e96a5c473aae1e1cd6143aacf02ccc9bbbf6180f15fa2588f99531188b \ + --hash=sha256:1663d79fc8400903992c63f79b3908b9298c623138e8929bf36c559231e082d3 \ + --hash=sha256:1d6b6f0c1e7dd8357f1ba56524bfc870d04ad3c00979db291784a7145497ad3b \ + --hash=sha256:23ed91f8d2fa75a109708338f80a7addd2f37c93a8b0e6c9d220f1427939b81f \ + --hash=sha256:366fe0d5a74e3cf1d4e7cbf3c49e308da60e7835ea268667be9185388f05d7a5 \ + --hash=sha256:4d8790f420ebc484236017edba59182cf2a21bd3e4224a0bd0760a9c7268e96a \ + --hash=sha256:6400774e6820760eb6c322f6feace43fe7267deb9f8d508f10bf258887a9c4d5 \ + --hash=sha256:65a7ad892a570045b0de2db6af17119162880aebc05a0c125ce2db7dab36f17e \ + --hash=sha256:6cf94756fb6a822d20a5a64f44840e6633ebf8b1deb3ce01057bff1cc03b01c2 \ + --hash=sha256:7770e7bdee34549e8baf28b8626e6552de7894d869465a7c95fd9af33b4cbeb0 \ + --hash=sha256:856c7fca5ed780ed44f60bceb92d6b311ebc008a2249415b8f6045201d4f5530 \ + --hash=sha256:89c347170a691e3b8d072519f36790b9c19a2263ec0af0144c873994ecfe55eb \ + --hash=sha256:8bd52c1891ae74cee21a4ebe8325953310a2e0af5352d70f47f5461422fcce2d \ + --hash=sha256:8c62636a1e9d8a1fc74ffb70383939b9cd93f2c9ee8e12015a50dd75c98a989e \ + --hash=sha256:a0f958cbca18d05846e3081dfff8575e73d45595441d659847479656dc76f91d \ + --hash=sha256:ae27ba4d37925d5b2cff192acaa70c8bb114d632bbc527cc91fea0370702b866 \ + --hash=sha256:b347d3cc8510f8fbe191956fcda7da372ebb3302789acefca08e352345959003 \ + --hash=sha256:b4b91f1b1bac135c077326d30a1876f917e6c36b844a2e329b148fb9cb7aa660 \ + --hash=sha256:b7ec5db19feab98a31fcd5caccf4a9a4e83f96933d1ca393ba7aea665710c2bb \ + --hash=sha256:ba98d6a41ec0b7c81dd85d764f15653d6abbbbd69d92630677c43f92dd50d924 \ + --hash=sha256:bba3a35b4648694783aeae5b77c235a57191c8b1b375c8602b03ae56a6cf4fe7 \ + --hash=sha256:ccac408873f65b8954858ef08d4b098841301e232afc1b14e3eada489d303a7f \ + --hash=sha256:cd423b8ee2f75fc9db09158875abe5e2c952a26ae5e521c3265ab2f9d3511ddf \ + --hash=sha256:cec0616d2ba6e7dda6327089a2f34ec723aa9ac2c389857ef0b83f65fb135dd6 \ + --hash=sha256:db66a4e0fdfbf4090631d59c3f65e960d9a5561e9259f6f3993cbe91e396837e \ + --hash=sha256:ddb360da76c62c7c23ec3da40e1af48e6712a563905fea2d1a8911ff7a3b6c4d \ + --hash=sha256:e08c268e12d54ea629a1f123bf7cb9587bdd57fd7bcdac22394dc6272a18668f \ + --hash=sha256:e273242cdca56029af694d7ce18075d47a74d034326d663ff6dd2655a6f44825 \ + --hash=sha256:eb3a0a3e630ee89758eb96b39b456f4697732351fb0c080e9498ea578f9b71f9 + # via feast (pyproject.toml) +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + # via + # feast (pyproject.toml) + # kube-authkit + # mcp # msal # singlestoredb # snowflake-connector-python -pylatexenc==2.10 \ - --hash=sha256:3dd8fd84eb46dc30bee1e23eaab8d8fb5a7f507347b23e5f38ad9675c84f40d3 +pylatexenc==2.11 \ + --hash=sha256:305a072a99ce736246049c9da05841b9d718c0f7ea8888f5f596cf15cb621053 \ + --hash=sha256:e78e7391d6c104f1ed150e21cfaa58016cdb50aa54406a2eecb793649ffdfdd0 # via docling -pymilvus==2.4.9 \ - --hash=sha256:0937663700007c23a84cfc0656160b301f6ff9247aaec4c96d599a6b43572136 \ - --hash=sha256:45313607d2c164064bdc44e0f933cb6d6afa92e9efcc7f357c5240c57db58fbe - # via feast (setup.py) -pymssql==2.3.2 \ - --hash=sha256:06883bc9bdb297ae9132d9371b5b1a3a223c8f93dd6a87d1c112c6a688f26d53 \ - --hash=sha256:0768d90f96ae3267d7561d3bcfe94dd671d107489e870388b12570c3debbc552 \ - --hash=sha256:0831c5c95aab0b9aba5142dc97e28f59c4130e1c34ffc13ecbfdd4d2fe45b8a0 \ - --hash=sha256:08facd25a50a7279385d1ffcee9d6d83c4e361db1af38e14519a87d7b1cadb10 \ - --hash=sha256:0a20a17db870fb0e446a6d6bf7664aaf84af7be58ab1fad025cafa4e092507a1 \ - --hash=sha256:1037053e6c74d6fe14c428cc942968b4e4bf06854706a83fe8e822e475e3f107 \ - --hash=sha256:148b7714fff5a5b7ce038e92b02dd9bf68fe442c181a3aae32148e7b13f6db95 \ - --hash=sha256:18089641b687be1ebd0f64f0d1ff977478a397ffa1af372bdf10dbec29cf6d2e \ - --hash=sha256:1afda7b7022eff9451bd83e3f64c450a1a8cdff4ba8b8e399866dcd2cb861a1e \ - --hash=sha256:1bc33ed9af6d8ebea2d49144cd2317b7ae1105dd51dddfd46982c90c8f0cf6ab \ - --hash=sha256:1c99dba4bf5b1ce10657e9e2885f18ba9179190251b63d1498e7d6d72e64f1ce \ - --hash=sha256:1cdc2619e7b4192b8d6619fd52ba8a2eae18b38b376f8649fb8f0727c4e88ff9 \ - --hash=sha256:22b1ce3a48f28ee7d06ebc9ed94276d0bf1c99051ee1df3d2377b74721bd62ef \ - --hash=sha256:22fb0fdd3b889bc10abbe3aa2abe7a008b30a6367b9ba159412d185d7d8fda9d \ - --hash=sha256:235c230e56d8c8c5f289e665c538f31d967fec302d05ad269dcd64fa9d6eb3b7 \ - --hash=sha256:23f5e2e2bdba1cf7cecbac66dd07de7631a8efca5692efee18ff46ebc087b757 \ - --hash=sha256:2568944db3888996e161b40ad06c1b9e0fbb6cfcb38279a3abb98ece7a8e1c4a \ - --hash=sha256:26bdb7abd5f107b6be422635f03e2cecaa52a5f4c394a205014586abbff9e72a \ - --hash=sha256:2a44a0898dacba4e25cac8778d0ed112e297883fe862204e447081888da78dc4 \ - --hash=sha256:2f4093b95f1f3a1232687fc92f652aaf675eb423db8549c16d146b91ac2f0eba \ - --hash=sha256:33ad813092f8fb8f74578c5b5e37c818c4ae130fd4047cb28f0b256f2f107367 \ - --hash=sha256:3870085a49e5332bc67ecb24f217c586977d5352eb51598244fc7bc278eee3e1 \ - --hash=sha256:41d09e1b2534229b288c37b88c1de3d964317af2c7eec58bfb97e01d679eba27 \ - --hash=sha256:5904d78e61668ec89761d3ae01efd4b42b31d820f612929f449e93cd23ba3c54 \ - --hash=sha256:6019d2939963112662288704f608f31634038bffcfd5cad1bc79cb167edb3cc1 \ - --hash=sha256:72b6599963b6e066998c4b27b7bf207684c243b12b1e5dcc180b2af22802ae6c \ - --hash=sha256:73fac766b448613d7ae26e6b304b2cb8a7ffebccaa373633bad3b3cbcc829935 \ - --hash=sha256:793a93da1521fa66bf02b3b873065e22bf14bda5570e005ce3d5fae0776d7b92 \ - --hash=sha256:79cdc3ed1da3129ba56232127db86279728c4328595e2532ed4d0da6379a5c72 \ - --hash=sha256:82ed3dd560d3fb222d26ce3a7373f46dc3ad1d50b6e6417ef7399e87fa9aefe1 \ - --hash=sha256:8cd806380d362d4cef2d925a6baee6a4b2b151a92cac2cab5c4bfabed4be4849 \ - --hash=sha256:9361593a89c9162fc631baf648a87e2666373382d9d54aacfb19edab9ceb2007 \ - --hash=sha256:97fbd8491ad3ece0adcb321acec6db48b8fe37bc74af4c91bb657d4d9347d634 \ - --hash=sha256:9e3d6fada7fbe7a5f5fafc420673f777bab3f399c78fa44e29de6a8cbc36e515 \ - --hash=sha256:a3f9e7eb813dfeab6d01bf6474049bb76b0521235159d3e969ec82df384eac49 \ - --hash=sha256:aa08b6203b2b5ed5ce47f80d5c529459181300d7e0d0c1e84390a4d01d45e509 \ - --hash=sha256:ab48de09864fa6f49c575ef569f6773981d0cd905ff7288b5b185f8079a5a21f \ - --hash=sha256:ab912d1178d5977e421cf9c4d4071958b223cbe4a2b6dd64611d521aa6bb7187 \ - --hash=sha256:ae02cc1594f0addd748bf5ac1ccc7a73c03846ada9c553663c381b242b586606 \ - --hash=sha256:b0c2b11aca16617cacaf385fb94134e73ba0216a924f9b85778cc7e3d3713361 \ - --hash=sha256:b14cc65369d1425f2fb517609113465a0f55f19a49648160f2d10be4cb43ff4d \ - --hash=sha256:b156b15165f7a0bbb392a124d8e2d678145c93e5bfcfef3b637e4d87eadcc85b \ - --hash=sha256:b16d5880f7028442d6c49c94801ce9bff3af8af0fbda7c6039febb936714aed5 \ - --hash=sha256:b3eb201c402bcf4f5b9399df0bb20d522636d2e87d1c6957a0b6d772ee636c61 \ - --hash=sha256:bac6f355c454f94b0e15a04b7841236e5c5c4ef44d2d1beed00a3ad7b50ccc53 \ - --hash=sha256:c24ba6aedb9b5540b56f3e74bff92b687c6e90c00650823385729c7e55923cf5 \ - --hash=sha256:cbe9058b6520be74463476ff2cdb17bbab5ff60b60b3ed7bd8bd2d086bdfd9bd \ - --hash=sha256:cc13c2e0f1b8efc3a46941de9db768fa59937b5a54081ec0cb0ff0da17d1fff3 \ - --hash=sha256:dd5fe7552edc81628e4242b4671f7bad5ff1ec790bae5c7615d989375620edac \ - --hash=sha256:eb629b5fb0376fbf39d575cf1365e504b84877b19f9e8d53caa5228fed56894a \ - --hash=sha256:ee8ee2c7c227c413ad9b88ddba1cb6a25e28c217ae73ecac1c7a6b8c29003604 \ - --hash=sha256:ef0d29c705db552f9e75230f946b0ca9db0db903c5c9ee79ce8b88ad25ea9670 \ - --hash=sha256:f1791f4627c42fe2d2833c884d036b0c5c8cf628f2cdfa3536191c217acf729e \ - --hash=sha256:f282e701dca155b3e7f1644d7e3b60c201ca5f3be8045bce34042d3c737d63ee \ - --hash=sha256:f2b1da4e68d618c7972e583ae19f386ae620258acb61564e8067c203f27cd769 \ - --hash=sha256:f9737c06b13ca2012b9900185fa3af72a37941c532da2e6373dd7c9ab16abddf \ - --hash=sha256:fb8a7b197aaf466a7577ca6690aa9d747081b653ab212d052d71f3cc10587c3b \ - --hash=sha256:fdd774b26407babd0205ef85a098f90553e6b3da77a22322a1e7d2cb51f742c0 - # via feast (setup.py) -pymysql==1.1.2 \ - --hash=sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03 \ - --hash=sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 - # via feast (setup.py) -pynacl==1.6.0 \ - --hash=sha256:04f20784083014e265ad58c1b2dd562c3e35864b5394a14ab54f5d150ee9e53e \ - --hash=sha256:10d755cf2a455d8c0f8c767a43d68f24d163b8fe93ccfaabfa7bafd26be58d73 \ - --hash=sha256:140373378e34a1f6977e573033d1dd1de88d2a5d90ec6958c9485b2fd9f3eb90 \ - --hash=sha256:16c60daceee88d04f8d41d0a4004a7ed8d9a5126b997efd2933e08e93a3bd850 \ - --hash=sha256:16dd347cdc8ae0b0f6187a2608c0af1c8b7ecbbe6b4a06bff8253c192f696990 \ - --hash=sha256:25720bad35dfac34a2bcdd61d9e08d6bfc6041bebc7751d9c9f2446cf1e77d64 \ - --hash=sha256:2d6cd56ce4998cb66a6c112fda7b1fdce5266c9f05044fa72972613bef376d15 \ - --hash=sha256:347dcddce0b4d83ed3f32fd00379c83c425abee5a9d2cd0a2c84871334eaff64 \ - --hash=sha256:4853c154dc16ea12f8f3ee4b7e763331876316cc3a9f06aeedf39bcdca8f9995 \ - --hash=sha256:49c336dd80ea54780bcff6a03ee1a476be1612423010472e60af83452aa0f442 \ - --hash=sha256:4a25cfede801f01e54179b8ff9514bd7b5944da560b7040939732d1804d25419 \ - --hash=sha256:51fed9fe1bec9e7ff9af31cd0abba179d0e984a2960c77e8e5292c7e9b7f7b5d \ - --hash=sha256:536703b8f90e911294831a7fbcd0c062b837f3ccaa923d92a6254e11178aaf42 \ - --hash=sha256:5789f016e08e5606803161ba24de01b5a345d24590a80323379fc4408832d290 \ - --hash=sha256:6b08eab48c9669d515a344fb0ef27e2cbde847721e34bba94a343baa0f33f1f4 \ - --hash=sha256:6b393bc5e5a0eb86bb85b533deb2d2c815666665f840a09e0aa3362bb6088736 \ - --hash=sha256:84709cea8f888e618c21ed9a0efdb1a59cc63141c403db8bf56c469b71ad56f2 \ - --hash=sha256:8bfaa0a28a1ab718bad6239979a5a57a8d1506d0caf2fba17e524dbb409441cf \ - --hash=sha256:bbcc4452a1eb10cd5217318c822fde4be279c9de8567f78bad24c773c21254f8 \ - --hash=sha256:cb36deafe6e2bce3b286e5d1f3e1c246e0ccdb8808ddb4550bb2792f2df298f2 \ - --hash=sha256:cf831615cc16ba324240de79d925eacae8265b7691412ac6b24221db157f6bd1 \ - --hash=sha256:dcdeb41c22ff3c66eef5e63049abf7639e0db4edee57ba70531fc1b6b133185d \ - --hash=sha256:dea103a1afcbc333bc0e992e64233d360d393d1e63d0bc88554f572365664348 \ - --hash=sha256:ef214b90556bb46a485b7da8258e59204c244b1b5b576fb71848819b468c44a7 \ - --hash=sha256:f3482abf0f9815e7246d461fab597aa179b7524628a4bc36f86a7dc418d2608d \ - --hash=sha256:f46386c24a65383a9081d68e9c2de909b1834ec74ff3013271f1bca9c2d233eb \ - --hash=sha256:f4b3824920e206b4f52abd7de621ea7a44fd3cb5c8daceb7c3612345dfc54f2e +pymilvus==3.0.1 \ + --hash=sha256:c02389059088b18d6e598cd175541e445c772fab4926c5e527c4913be34887f1 \ + --hash=sha256:c5a8d5c1fa1de7b416e3529d383d8cc2e7da2170433ffa4a2d9087e14f70171a + # via feast (pyproject.toml) +pymongo==4.17.0 \ + --hash=sha256:0ff6bd2f735ab5356541e3e57d5b7dbfbc3f2ee1ccb10b6b0f82d58af69d1d8e \ + --hash=sha256:1175563375d682260f613a96fb7a53dce746ed752bfd924eab61de3bc5bfde34 \ + --hash=sha256:1195370a77baf003b59b10e91ecc4706297197f0dd9d29c840cc556dc08f7cee \ + --hash=sha256:12c4fded3a9f1d6a687e36ebd384ac6d00b9b00de1969aa74048e7051ec2a713 \ + --hash=sha256:15d3f3d732aecac1f8d481bde4029755615639bd3076f258a2147210aec8515a \ + --hash=sha256:20323b0b1c1d33770ad1fc68d429c757734ce9ad3594421c3d6618f10572b1b9 \ + --hash=sha256:2a0d5ac205728c86e0a02192f1aa5f865b0d7d51f8df6101c01a69a7fc620d72 \ + --hash=sha256:2db66aa8dd253a0fc1fad3b0d23d5b3993f7ebde02fbbd7727128debf2853675 \ + --hash=sha256:2e190827834fce70ecdf9d46796c6dbc0ce08ea87dc2ff5bc6f3f5579b605cb9 \ + --hash=sha256:320b34457b20bbcc79997801f95d25ce00472915ca5241167242b42c4359e027 \ + --hash=sha256:3689ea34f6b647c7d1e7bdc60fcfb214b2789ed1359a7fb96569c69f50e5f18f \ + --hash=sha256:37a8385c29881b43eab31f584100fa0eaddedd5607adf010147ba1810118be90 \ + --hash=sha256:3987e96e7c7be4083d42e8ac2cc6c0d5b78db9973c90fce42ae800b616ca6b20 \ + --hash=sha256:4141e6c6a339789b2974efa00ecd9409101672d77a0e3ee2cc3839eedf8ec4df \ + --hash=sha256:422fa50d7d7f5c22ea0953554396c9ef95684a2d775f860bd75a7b510538dfca \ + --hash=sha256:47b021363cd923ace5edc7a1d63c0ff8a6d9d43859b8a1ba23645f5afae63221 \ + --hash=sha256:485c8a8eaa4c739f00a331fc73757898ee7c092c214a79e63866ff76aaf282ff \ + --hash=sha256:48bbc576677b50af043df870d84ded67cc3a9b4aa7553201beef4da5dc050a0a \ + --hash=sha256:4ae22fafca69dd3c78261969e999782ac5fc23b76cf8cccfbc3707982a74cc3d \ + --hash=sha256:50e8f8e23c6df7c6d6929f5e734980b227706e73ee847517c9ba5af90f7fc466 \ + --hash=sha256:51e1915761f65f2aaabd0ba691a31d56551d3f19d1263c2d6bf261730603de5f \ + --hash=sha256:5376ad67bb30ae910d83affcf997f706d9dee37e8b5dad8b6fedb0626e262d85 \ + --hash=sha256:5960519b4d7168f1ecdd3ea10c81b2aedeb9423651aca953cfbc8e76705d3b38 \ + --hash=sha256:5a5de048e6da5c18e27cc2437e8c15b3b0cdc8385c15b41178b0caa3322a09c2 \ + --hash=sha256:5ab3b8ff79e0dfc49b68f3c925e8cc735ea95c60efaed84cfe75692dffcaac2a \ + --hash=sha256:64837adbbd72073301af51bb0fc80e3d7707fe5527cea1033ba0320f0b2f881b \ + --hash=sha256:6877214bff5f06f6884a9fc8d9016a4a7a5f51f537f5c51ac3a576f93e7dfb32 \ + --hash=sha256:68fca71e05ee5da23a8d73cee8379dfb3d26e609a377cae731d742771ed96946 \ + --hash=sha256:6c5f62862d0f87be481fa1fe8cb811994486773c94a2b61e509285e3f2890763 \ + --hash=sha256:6fe0de9d0f6791abce3471230b32b4817bf89d27b1182b6a550e1ec0fa72aa9a \ + --hash=sha256:70ffa08ba641468cc068cf46c06b34f01a8ce3489f6411309fcb5ceabe6b2fc0 \ + --hash=sha256:757f2a4c0c2c46cab87df0333681ce69e86c9d5b45bc5203ceba5410b3489e59 \ + --hash=sha256:75bc3aa5b94fdb7138d357ec6ca61cd97e0c79f4f7f0bd3efe9639b15cc50942 \ + --hash=sha256:77aa4bc164b4de60d5db193b322f0f5b6ead716e831031bfdef8e8bd92205556 \ + --hash=sha256:7db10678814cdf7ea39fd308c6f41395cfa7b29d904bcd7895288963d8f892ba \ + --hash=sha256:809ec74de3b9148ae43fa8df9faf53470f511c8d384f13b99d6f671f2a379f15 \ + --hash=sha256:8446ff4bfcb6ec2a2e50998c860986a1e992136f998b7f53e7a717fb8aa5a0b9 \ + --hash=sha256:8a1be016198a03fd7727cdd55998964bfa4e5a6fd9733c8e95830628cef34d29 \ + --hash=sha256:8e97e03fa13327c87e3fdc5656acd01e71817f0c1dc3221cd8f30de136bf4ec3 \ + --hash=sha256:93641192644fa1ee0f34030e774fd31022a27ad11ba22cb1716142231524f8bd \ + --hash=sha256:9543d8f84c2e5608565c08ac679774811e6730770d8a645439b073422a4276fb \ + --hash=sha256:9828485f72f63c7d802e0ec41f71906f633c2692621ab3af55ca990186b091b1 \ + --hash=sha256:9eb5d63a3c518cb0804ed678f5e2b875af032d89a7cf57a57360322cf6a4d222 \ + --hash=sha256:a431b737816bf4cddd4fa0fcef04e424ad36b7692734a64150f872fb8f3208be \ + --hash=sha256:a8f9c40a09bb7d4b9fc8b1da65ecf6efa79bda5cb2756f39d9b6940fac1d19ae \ + --hash=sha256:addd0498ebbdc6354227f6ed457ed9fce442d48a3bb30d5b5bad33e104996561 \ + --hash=sha256:b24598dc3c2feccbc83b43044be48145a0dc4f9bee49ef923e3d707d54a55d85 \ + --hash=sha256:b2dfcc795f5b9fedbe179a11fdf6051581479d196582a3fe819a92a00e9b9969 \ + --hash=sha256:b4384700cffc3f1dd98e088bc0072dedf6d7d68a230bb4b972665cf69c071c1e \ + --hash=sha256:b93b22eedc62598cf5ee9d8c8007a8e9121c50fd88137012d8985500e9dc3151 \ + --hash=sha256:ba2195d4f386f839a52a23ea1cfd60ffaaba78a3d7841db51b7e433001139918 \ + --hash=sha256:bb3ebc86782049f6928dcc583008287cb1c17d463501c94a620f035f5b4fd463 \ + --hash=sha256:bd835cdb37a1adec359dd072c24f8bb14809e2644fde86fab4ee2fc9719b9483 \ + --hash=sha256:c2292144505fb12156b981bd440f3dc994a883da06ac726c0c8692ccdbc1c510 \ + --hash=sha256:c4979e7e8887862bbb44d203f00cc8263a3f27237876fa691b6beba23e40e6d8 \ + --hash=sha256:c5c8e180cb2cabe37300e1e36c60aa4f2ff956cc579f0142135a5d2cba252243 \ + --hash=sha256:c797f8a80957134f6dd9690367a0f8f5906d672119af2c6aa55f0c527b656bed \ + --hash=sha256:c9786665926a09630c5d420c79762cfadbff35a9438bcbc4c81a9fb5ab9228b7 \ + --hash=sha256:cee36b3c0d0354f880fa7a7fdcdaf2bb5e542c2281e25c1bfadf8cfe21eba7d2 \ + --hash=sha256:d53ffa94b2340dbf6b055e09a0090618c60482c158ecfc9565642fc996bf0944 \ + --hash=sha256:df4a644af9ae132d4bfdb2e9516ea51a615fd881caddfbfbd071cf1354844479 \ + --hash=sha256:dff3de1294fbbc1db0ba6b511f77b8e540601d092538a31312e99c8a91a78b1e \ + --hash=sha256:e46767f28dea610e02edf6c5d956ce615c3c7790ea396660b9b1efd5c5ead2e0 \ + --hash=sha256:e4fab10f8403169ce92f3cea921609d9ee81107306caae06c08f592d4b8ad2b5 \ + --hash=sha256:e537e95514dae1aaa718f481ec03151a0f0394bcd05f1322896d8fc1330cb729 \ + --hash=sha256:e68c76b84e0c132d9dbf9307f12ff8185702328187a87b9aca8c941303873433 \ + --hash=sha256:e816db649ba5d7de0568cf3a9f287a9dc9aad21cf0ca667ab156a7ef47fca0b0 \ + --hash=sha256:f09645e0ce4e3825fa0baa8254064a716ed0be33f78feeedd4731016cb8aaa17 \ + --hash=sha256:f3ee3d241ed77a4fc99ce3cff3b289c3ebce37f61fdd7349d3592c23b82c8784 \ + --hash=sha256:faf03e4c2aafd6de626dbd30ba246d369ae33f47f10629d1bbe40f72115027a6 \ + --hash=sha256:ff5aa3f1c7e3f08eb0e7a016c91ba468b1850ccfd63d9b1f12f56350f4974cef + # via feast (pyproject.toml) +pymssql==2.3.13 \ + --hash=sha256:0a7e6431925572bc75fb47929ae8ca5b0aac26abfe8b98d4c08daf117b5657f1 \ + --hash=sha256:0fddd24efe9d18bbf174fab7c6745b0927773718387f5517cf8082241f721a68 \ + --hash=sha256:123c55ee41bc7a82c76db12e2eb189b50d0d7a11222b4f8789206d1cda3b33b9 \ + --hash=sha256:1493f63d213607f708a5722aa230776ada726ccdb94097fab090a1717a2534e0 \ + --hash=sha256:152be40c0d7f5e4b1323f7728b0a01f3ee0082190cfbadf84b2c2e930d57e00e \ + --hash=sha256:16c5957a3c9e51a03276bfd76a22431e2bc4c565e2e95f2cbb3559312edda230 \ + --hash=sha256:17942dc9474693ab2229a8a6013e5b9cb1312a5251207552141bb85fcce8c131 \ + --hash=sha256:1c6d0b2d7961f159a07e4f0d8cc81f70ceab83f5e7fd1e832a2d069e1d67ee4e \ + --hash=sha256:2137e904b1a65546be4ccb96730a391fcd5a85aab8a0632721feb5d7e39cfbce \ + --hash=sha256:2b056eb175955f7fb715b60dc1c0c624969f4d24dbdcf804b41ab1e640a2b131 \ + --hash=sha256:30918bb044242865c01838909777ef5e0f1b9ecd7f5882346aefa57f4414b29c \ + --hash=sha256:319810b89aa64b99d9c5c01518752c813938df230496fa2c4c6dda0603f04c4c \ + --hash=sha256:476f6f06b2ae5dfbfa0b169a6ecdd0d9ddfedb07f2d6dc97d2dd630ff2d6789a \ + --hash=sha256:48631c7b9fd14a1bd5675c521b6082590bf700b7961c65638d237817b3fde735 \ + --hash=sha256:4aa18944a121f996178e26cadc598abdbf73759f03dc3cd74263fdab1b28cd96 \ + --hash=sha256:4b834c34e7600369eee7bc877948b53eb0fe6f3689f0888d005ae47dd53c0a66 \ + --hash=sha256:4d87237500def5f743a52e415cd369d632907212154fcc7b4e13f264b4e30021 \ + --hash=sha256:51e42c5defc3667f0803c7ade85db0e6f24b9a1c5a18fcdfa2d09c36bff9b065 \ + --hash=sha256:5c045c0f1977a679cc30d5acd9da3f8aeb2dc6e744895b26444b4a2f20dad9a0 \ + --hash=sha256:5c2e55b6513f9c5a2f58543233ed40baaa7f91c79e64a5f961ea3fc57a700b80 \ + --hash=sha256:612ac062027d2118879f11a5986e9d9d82d07ca3545bb98c93200b68826ea687 \ + --hash=sha256:6a6c0783d97f57133573a03aad3017917dbdf7831a65e0d84ccf2a85e183ca66 \ + --hash=sha256:79c759db6e991eeae473b000c2e0a7fb8da799b2da469fe5a10d30916315e0b5 \ + --hash=sha256:7d7037d2b5b907acc7906d0479924db2935a70c720450c41339146a4ada2b93d \ + --hash=sha256:7f9b1d5aef2b5f47a7f9d9733caee4d66772681e8f798a0f5e4739a8bdab408c \ + --hash=sha256:8d66ce0a249d2e3b57369048d71e1f00d08dfb90a758d134da0250ae7bc739c1 \ + --hash=sha256:910404e0ec85c4cc7c633ec3df9b04a35f23bb74a844dd377a387026ae635e3a \ + --hash=sha256:a930adda87bdd8351a5637cf73d6491936f34e525a5e513068a6eac742f69cdb \ + --hash=sha256:aa5e07eff7e6e8bd4ba22c30e4cb8dd073e138cd272090603609a15cc5dbc75b \ + --hash=sha256:b0af51904764811da0bfe4b057b1d72dee11a399ce9ed5770875162772740c8a \ + --hash=sha256:c0ea72641cb0f8bce7ad8565dbdbda4a7437aa58bce045f2a3a788d71af2e4be \ + --hash=sha256:c690f1869dadbf4201b7f51317fceff6e5d8f5175cec6a4a813e06b0dca2d6ed \ + --hash=sha256:cf4f32b4a05b66f02cb7d55a0f3bcb0574a6f8cf0bee4bea6f7b104038364733 \ + --hash=sha256:d663c908414a6a032f04d17628138b1782af916afc0df9fefac4751fa394c3ac \ + --hash=sha256:d94da3a55545c5b6926cb4d1c6469396f0ae32ad5d6932c513f7a0bf569b4799 \ + --hash=sha256:db77da1a3fc9b5b5c5400639d79d7658ba7ad620957100c5b025be608b562193 \ + --hash=sha256:e053b443e842f9e1698fcb2b23a4bff1ff3d410894d880064e754ad823d541e5 \ + --hash=sha256:e7c31f192da9d30f0e03ad99e548120a8740a675302e2f04fa8c929f7cbee771 \ + --hash=sha256:eb3275985c23479e952d6462ae6c8b2b6993ab6b99a92805a9c17942cf3d5b3d \ + --hash=sha256:f1897c1b767cc143e77d285123ae5fd4fa7379a1bfec5c515d38826caf084eb6 \ + --hash=sha256:f5d995a80996235ed32102a93067ce6a7143cce3bfd4e5042bf600020fc08456 \ + --hash=sha256:fc5482969c813b0a45ce51c41844ae5bfa8044ad5ef8b4820ef6de7d4545b7f2 \ + --hash=sha256:ff5be7ab1d643dbce2ee3424d2ef9ae8e4146cf75bd20946bc7a6108e3ad1e47 + # via feast (pyproject.toml) +pymysql==1.2.0 \ + --hash=sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 \ + --hash=sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33 + # via feast (pyproject.toml) +pynacl==1.6.2 \ + --hash=sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c \ + --hash=sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574 \ + --hash=sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4 \ + --hash=sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130 \ + --hash=sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b \ + --hash=sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590 \ + --hash=sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444 \ + --hash=sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634 \ + --hash=sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87 \ + --hash=sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa \ + --hash=sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594 \ + --hash=sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0 \ + --hash=sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e \ + --hash=sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c \ + --hash=sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0 \ + --hash=sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c \ + --hash=sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577 \ + --hash=sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145 \ + --hash=sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88 \ + --hash=sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14 \ + --hash=sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6 \ + --hash=sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465 \ + --hash=sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0 \ + --hash=sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2 \ + --hash=sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9 # via paramiko -pyodbc==5.2.0 \ - --hash=sha256:057b8ede91b21d9f0ef58210d1ca1aad704e641ca68ac6b02f109d86b61d7402 \ - --hash=sha256:0dae0fb86078c87acf135dbe5afd3c7d15d52ab0db5965c44159e84058c3e2fb \ - --hash=sha256:0e4412f8e608db2a4be5bcc75f9581f386ed6a427dbcb5eac795049ba6fc205e \ - --hash=sha256:113f904b9852c12f10c7a3288f5a3563ecdbbefe3ccc829074a9eb8255edcd29 \ - --hash=sha256:207f16b7e9bf09c591616429ebf2b47127e879aad21167ac15158910dc9bbcda \ - --hash=sha256:26844d780045bbc3514d5c2f0d89e7fda7df7db0bd24292eb6902046f5730885 \ - --hash=sha256:26b7f8324fa01c09fe4843ad8adb0b131299ef263a1fb9e63830c9cd1d5c45e4 \ - --hash=sha256:26d2d8fd53b71204c755abc53b0379df4e23fd9a40faf211e1cb87e8a32470f0 \ - --hash=sha256:2b5323be83fedc79a6d1e1b96e67bdc368c1d3f1562b8f8184b735acdd749ae9 \ - --hash=sha256:4627779f0a608b51ce2d2fe6d1d395384e65ca36248bf9dbb6d7cf2c8fda1cab \ - --hash=sha256:4d997d3b6551273647825c734158ca8a6f682df269f6b3975f2499c01577ddec \ - --hash=sha256:4fde753fcea625bfaed36edae34c2fba15bf0b5d0ea27474ee038ef47b684d1d \ - --hash=sha256:5102007a8c78dd2fc1c1b6f6147de8cfc020f81013e4b46c33e66aaa7d1bf7b1 \ - --hash=sha256:5f0ecbc7067467df95c9b8bd38fb2682c4a13a3402d77dccaddf1e145cea8cc0 \ - --hash=sha256:600ef6f562f609f5612ffaa8a93827249150aa3030c867937c87b24a1608967e \ - --hash=sha256:6493b9c7506ca964b80ad638d0dc82869df7058255d71f04fdd1405e88bcb36b \ - --hash=sha256:74135cb10c1dcdbd99fe429c61539c232140e62939fa7c69b0a373cc552e4a08 \ - --hash=sha256:770e1ac2e7bdf31439bf1d57a1d34ae37d6151216367e8e3f6cdc275006c8bb0 \ - --hash=sha256:7e3cbc7075a46c411b531ada557c4aef13d034060a70077717124cabc1717e2d \ - --hash=sha256:96d3127f28c0dacf18da7ae009cd48eac532d3dcc718a334b86a3c65f6a5ef5c \ - --hash=sha256:97d086a8f7a302b74c9c2e77bedf954a603b19168af900d4d3a97322e773df63 \ - --hash=sha256:9e8f4ee2c523bbe85124540ffad62a3b62ae481f012e390ef93e0602b6302e5e \ - --hash=sha256:9f7badd0055221a744d76c11440c0856fd2846ed53b6555cf8f0a8893a3e4b03 \ - --hash=sha256:a27996b6d27e275dfb5fe8a34087ba1cacadfd1439e636874ef675faea5149d9 \ - --hash=sha256:ad633c52f4f4e7691daaa2278d6e6ebb2fe4ae7709e610e22c7dd1a1d620cf8b \ - --hash=sha256:b1f5686b142759c5b2bdbeaa0692622c2ebb1f10780eb3c174b85f5607fbcf55 \ - --hash=sha256:b77556349746fb90416a48bd114cd7323f7e2559a4b263dada935f9b406ba59b \ - --hash=sha256:be43d1ece4f2cf4d430996689d89a1a15aeb3a8da8262527e5ced5aee27e89c3 \ - --hash=sha256:d287121eeaa562b9ab3d4c52fa77c793dfedd127049273eb882a05d3d67a8ce8 \ - --hash=sha256:d57843b9792994f9e73b91667da6452a4f2d7caaa2499598783eb972c4b6eb93 \ - --hash=sha256:dc5342d1d09466f9e76e3979551f9205a01ff0ea78b02d2d889171e8c3c4fb9c \ - --hash=sha256:de1ee7ec2eb326b7be5e2c4ce20d472c5ef1a6eb838d126d1d26779ff5486e49 \ - --hash=sha256:de8be39809c8ddeeee26a4b876a6463529cd487a60d1393eb2a93e9bcd44a8f5 \ - --hash=sha256:e04de873607fb960e71953c164c83e8e5d9291ce0d69e688e54947b254b04902 \ - --hash=sha256:eaf42c4bd323b8fd01f1cd900cca2d09232155f9b8f0b9bcd0be66763588ce64 \ - --hash=sha256:eb0850e3e3782f57457feed297e220bb20c3e8fd7550d7a6b6bb96112bd9b6fe \ - --hash=sha256:f1f38adc47d36af392475cd4aaae0f35652fdc9e8364bf155810fe1be591336f - # via - # feast (setup.py) +pyodbc==5.3.0 \ + --hash=sha256:01166162149adf2b8a6dc21a212718f205cabbbdff4047dc0c415af3fd85867e \ + --hash=sha256:0263323fc47082c2bf02562f44149446bbbfe91450d271e44bffec0c3143bfb1 \ + --hash=sha256:08b2439500e212625471d32f8fde418075a5ddec556e095e5a4ba56d61df2dc6 \ + --hash=sha256:0df7ff47fab91ea05548095b00e5eb87ed88ddf4648c58c67b4db95ea4913e23 \ + --hash=sha256:101313a21d2654df856a60e4a13763e4d9f6c5d3fd974bcf3fc6b4e86d1bbe8e \ + --hash=sha256:13656184faa3f2d5c6f19b701b8f247342ed581484f58bf39af7315c054e69db \ + --hash=sha256:1629af4706e9228d79dabb4863c11cceb22a6dab90700db0ef449074f0150c0d \ + --hash=sha256:197bb6ddafe356a916b8ee1b8752009057fce58e216e887e2174b24c7ab99269 \ + --hash=sha256:2035c7dfb71677cd5be64d3a3eb0779560279f0a8dc6e33673499498caa88937 \ + --hash=sha256:25b6766e56748eb1fc1d567d863e06cbb7b7c749a41dfed85db0031e696fa39a \ + --hash=sha256:25c4cfb2c08e77bc6e82f666d7acd52f0e52a0401b1876e60f03c73c3b8aedc0 \ + --hash=sha256:2eb7151ed0a1959cae65b6ac0454f5c8bbcd2d8bafeae66483c09d58b0c7a7fc \ + --hash=sha256:2fe0e063d8fb66efd0ac6dc39236c4de1a45f17c33eaded0d553d21c199f4d05 \ + --hash=sha256:349a9abae62a968b98f6bbd23d2825151f8d9de50b3a8f5f3271b48958fdb672 \ + --hash=sha256:363311bd40320b4a61454bebf7c38b243cd67c762ed0f8a5219de3ec90c96353 \ + --hash=sha256:3cc472c8ae2feea5b4512e23b56e2b093d64f7cbc4b970af51da488429ff7818 \ + --hash=sha256:3f1bdb3ce6480a17afaaef4b5242b356d4997a872f39e96f015cabef00613797 \ + --hash=sha256:452e7911a35ee12a56b111ac5b596d6ed865b83fcde8427127913df53132759e \ + --hash=sha256:46185a1a7f409761716c71de7b95e7bbb004390c650d00b0b170193e3d6224bb \ + --hash=sha256:46869b9a6555ff003ed1d8ebad6708423adf2a5c88e1a578b9f029fb1435186e \ + --hash=sha256:58635a1cc859d5af3f878c85910e5d7228fe5c406d4571bffcdd281375a54b39 \ + --hash=sha256:5cbe4d753723c8a8f65020b7a259183ef5f14307587165ce37e8c7e251951852 \ + --hash=sha256:5ceaed87ba2ea848c11223f66f629ef121f6ebe621f605cde9cfdee4fd9f4b68 \ + --hash=sha256:5dd3d5e469f89a3112cf8b0658c43108a4712fad65e576071e4dd44d2bd763c7 \ + --hash=sha256:5ebf6b5d989395efe722b02b010cb9815698a4d681921bf5db1c0e1195ac1bde \ + --hash=sha256:6132554ffbd7910524d643f13ce17f4a72f3a6824b0adef4e9a7f66efac96350 \ + --hash=sha256:6682cdec78f1302d0c559422c8e00991668e039ed63dece8bf99ef62173376a5 \ + --hash=sha256:676031723aac7dcbbd2813bddda0e8abf171b20ec218ab8dfb21d64a193430ea \ + --hash=sha256:705903acf6f43c44fc64e764578d9a88649eb21bf7418d78677a9d2e337f56f2 \ + --hash=sha256:729c535341bb09c476f219d6f7ab194bcb683c4a0a368010f1cb821a35136f05 \ + --hash=sha256:74528fe148980d0c735c0ebb4a4dc74643ac4574337c43c1006ac4d09593f92d \ + --hash=sha256:754d052030d00c3ac38da09ceb9f3e240e8dd1c11da8906f482d5419c65b9ef5 \ + --hash=sha256:7713c740a10f33df3cb08f49a023b7e1e25de0c7c99650876bbe717bc95ee780 \ + --hash=sha256:7e9ab0b91de28a5ab838ac4db0253d7cc8ce2452efe4ad92ee6a57b922bf0c24 \ + --hash=sha256:8339d3094858893c1a68ee1af93efc4dff18b8b65de54d99104b99af6306320d \ + --hash=sha256:8aa396c6d6af52ccd51b8c8a5bffbb46fd44e52ce07ea4272c1d28e5e5b12722 \ + --hash=sha256:9b987a25a384f31e373903005554230f5a6d59af78bce62954386736a902a4b3 \ + --hash=sha256:9cd3f0a9796b3e1170a9fa168c7e7ca81879142f30e20f46663b882db139b7d2 \ + --hash=sha256:a48d731432abaee5256ed6a19a3e1528b8881f9cb25cb9cf72d8318146ea991b \ + --hash=sha256:ac23feb7ddaa729f6b840639e92f83ff0ccaa7072801d944f1332cd5f5b05f47 \ + --hash=sha256:af4d8c9842fc4a6360c31c35508d6594d5a3b39922f61b282c2b4c9d9da99514 \ + --hash=sha256:afe7c4ac555a8d10a36234788fc6cfc22a86ce37fc5ba88a1f75b3e6696665dc \ + --hash=sha256:b180bc5e49b74fd40a24ef5b0fe143d0c234ac1506febe810d7434bf47cb925b \ + --hash=sha256:b35b9983ad300e5aea82b8d1661fc9d3afe5868de527ee6bd252dd550e61ecd6 \ + --hash=sha256:bc834567c2990584b9726cba365834d039380c9dbbcef3030ddeb00c6541b943 \ + --hash=sha256:bfeb3e34795d53b7d37e66dd54891d4f9c13a3889a8f5fe9640e56a82d770955 \ + --hash=sha256:c25dc9c41f61573bdcf61a3408c34b65e4c0f821b8f861ca7531b1353b389804 \ + --hash=sha256:c2eb0b08e24fe5c40c7ebe9240c5d3bd2f18cd5617229acee4b0a0484dc226f2 \ + --hash=sha256:c5c30c5cd40b751f77bbc73edd32c4498630939bcd4e72ee7e6c9a4b982cc5ca \ + --hash=sha256:c67e7f2ce649155ea89beb54d3b42d83770488f025cf3b6f39ca82e9c598a02e \ + --hash=sha256:c68d9c225a97aedafb7fff1c0e1bfe293093f77da19eaf200d0e988fa2718d16 \ + --hash=sha256:c6ccb5315ec9e081f5cbd66f36acbc820ad172b8fa3736cf7f993cdf69bd8a96 \ + --hash=sha256:c79df54bbc25bce9f2d87094e7b39089c28428df5443d1902b0cc5f43fd2da6f \ + --hash=sha256:cf18797a12e70474e1b7f5027deeeccea816372497e3ff2d46b15bec2d18a0cc \ + --hash=sha256:d255f6b117d05cfc046a5201fdf39535264045352ea536c35777cf66d321fbb8 \ + --hash=sha256:d32c3259762bef440707098010035bbc83d1c73d81a434018ab8c688158bd3bb \ + --hash=sha256:d89a7f2e24227150c13be8164774b7e1f9678321a4248f1356a465b9cc17d31e \ + --hash=sha256:e3c39de3005fff3ae79246f952720d44affc6756b4b85398da4c5ea76bf8f506 \ + --hash=sha256:e981db84fee4cebec67f41bd266e1e7926665f1b99c3f8f4ea73cd7f7666e381 \ + --hash=sha256:ebc3be93f61ea0553db88589e683ace12bf975baa954af4834ab89f5ee7bf8ae \ + --hash=sha256:f1ad0e93612a6201621853fc661209d82ff2a35892b7d590106fe8f97d9f1f2a \ + --hash=sha256:f927b440c38ade1668f0da64047ffd20ec34e32d817f9a60d07553301324b364 \ + --hash=sha256:fc5ac4f2165f7088e74ecec5413b5c304247949f9702c8853b0e43023b4187e8 \ + --hash=sha256:fe77eb9dcca5fc1300c9121f81040cc9011d28cff383e2c35416e9ec06d4bc95 + # via + # feast (pyproject.toml) # ibis-framework -pyopenssl==25.1.0 \ - --hash=sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab \ - --hash=sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b +pyopenssl==26.3.0 \ + --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ + --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 # via snowflake-connector-python -pyparsing==3.2.5 \ - --hash=sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6 \ - --hash=sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e - # via great-expectations +pyparsing==3.3.2 \ + --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ + --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc + # via + # great-expectations + # matplotlib + # pyiceberg pypdfium2==4.30.0 \ --hash=sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e \ --hash=sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29 \ @@ -4241,14 +4944,101 @@ pyproject-hooks==1.2.0 \ # via # build # pip-tools -pyspark==4.0.1 \ - --hash=sha256:9d1f22d994f60369228397e3479003ffe2dd736ba79165003246ff7bd48e2c73 - # via feast (setup.py) +pyroaring==1.1.0 \ + --hash=sha256:01212da3752d6486adcca98c9d353f8fb8e36513e05062cdd0feebc4211dbe70 \ + --hash=sha256:0183b26c6e25a14e32b2aed7095a19491d6c835096032b33bc00b4ae995495bf \ + --hash=sha256:043dbbaa905f7288c515ac06a96b67a3763f35e9ae06f0c0278c0d9964d16760 \ + --hash=sha256:0dfb6cf50fd8898179e460e699a6b8326ca508c627d083f7bf62f769fe1717d5 \ + --hash=sha256:0f1d76ef29034017eb2cceebd5fa0504d6ced218ce6432f99da5adecbe038269 \ + --hash=sha256:100585f438b293112e2c52e45a442835837c8a0267dd1e513bafec35628f8ecb \ + --hash=sha256:13660386ea8905ee4d42c21a6275463e2dc7d31e0b5d65eec210aa7043ad96f4 \ + --hash=sha256:19c9bb8b32f17fe63e54e78f88a7a67fe2dca352bf79d7d3b43f0a901db5ea3b \ + --hash=sha256:19d0c81c865d63791fe20e5b38733b66f4f962e677ae7e8b3d3c4947ac6e752f \ + --hash=sha256:1fc112b9a9890f89cc645a16604783ed7fa25299f149b0ef7b45a5e2e3c1f31f \ + --hash=sha256:212f471da113eba24a0fedbd1ef078bfed34b792e8f48dba3f7301f1bca2678a \ + --hash=sha256:25d65257258f7a3df8b872ddfe18f3681f20b3bc2a3093f08fc80c66338051f4 \ + --hash=sha256:370d191b0d1b32bbd99452ef5f0485f22fcc4bf7404d33b821d0ce2459951152 \ + --hash=sha256:381eda673442c389993f8b0db2dbf5d02ea8ea9aac6ba736f64cc1ffb6c96885 \ + --hash=sha256:39eff7dd06c163c22d0a9f9fd72d27e671457bea8cdb71215382a10512539e1d \ + --hash=sha256:3beb40eb1220d1ce4fb3661bb019e9a21857e5bb294fe8c1c5016aeb6e82318c \ + --hash=sha256:3d04c3eb039cf2295e4d375ef81fd6e3cae7cefe3b5c457ccaf78d7d6f963a5d \ + --hash=sha256:44a9f9719ed18e86627286f90057f9b7e22f6ba1d952c9793f9600b5c14e8680 \ + --hash=sha256:462b0952277d3100a90ae890ae641d3fb3561b10cfea542e02468f0bef7700a5 \ + --hash=sha256:499f0a5d03d64e7b846ee749e87721fb8516da7233baafb24ab69aa63bcf080b \ + --hash=sha256:4a2c7ecd74e7b3ded54ad771a2b7c0aa08aa98f78e43500fa970d68d684bef0c \ + --hash=sha256:4ae81a0353af50d6885720b9119783b8be7bcd079e727ea1c4fca3f452fe95ba \ + --hash=sha256:4c443e9f942b6089efe8c9b264576e9d116f90be28a315679375bba2d8a915d6 \ + --hash=sha256:51dd2490a64ad4ed53c4fb58ef1ee3f84f6cbd97cdb47abd9065c9f714ab72ef \ + --hash=sha256:51ebe5e6f48e3dc9df91a4cb62137ef72e1469acd6f37479abd9991f6d945cc9 \ + --hash=sha256:532ae6bb1d3431d9956ef07589dd5c8dd918301a83d937c7dc6e511b1364d76a \ + --hash=sha256:532e53191d8dd29dedfc5202cbb45632f7df751b207a7f6d6860fb7067c7fe11 \ + --hash=sha256:53acecba8f898e96b84d4139356e30719c70358177e270055901d3ec1cb0e34c \ + --hash=sha256:5599b691e5e993f7fdd9880429d1c485eae03dc392ba5b2dbe2eef0434bea1a5 \ + --hash=sha256:55c9d0b708a65308eaa95c53d9de56a2b4425812395f6e57a4d9ad8efb081bf9 \ + --hash=sha256:562fa04bbfd41144d1276ed79505007557c161371450d68a1d71fc83dc01d083 \ + --hash=sha256:56a67794188275f8897a8f1fa64d6313c48241bebbdef38833063e7281b29ef8 \ + --hash=sha256:591e2ed4d60443dafd9075c1f72e9aaf359ccf5120e32a8c340c2b2ae3da45e7 \ + --hash=sha256:5e337f8c5b3c2e0c27da83fc2cb702684a47eee907a960cfee964fcb5344515b \ + --hash=sha256:5eb031237e9d39cbdfc9276facacdd88e27aefb58940bd8b56b878dfd38d6022 \ + --hash=sha256:61a8eabee99104ca197b6e7cce05dc4f27f503be52881800cd370eb5a5152d3f \ + --hash=sha256:650db21c10f42ff2b09ef02c10a779a3d59d0c7512552f3844738b30adbcb8a5 \ + --hash=sha256:66aa6a321fbc598f26d5e66050a7f145c2253f3fe5737b589841ff0cbe5cb177 \ + --hash=sha256:67650460c65bdd7b4f5078d9c955aa38f64627d02cb48f9cfb24eae84bca2aba \ + --hash=sha256:6a1d4c59d5b23c01d62f86d57ceefd0c0977de0425aafa7069f2d70563fed3b8 \ + --hash=sha256:71fec09bd42f8c33ac3b762cd00c5db842eb583ffd0e361739ce1c17ad078a6a \ + --hash=sha256:72f68a16b00b35481d9b3bfe897ecd8a1f7da69efd92ba5b17347ca11c21cb0d \ + --hash=sha256:731a7a9e050758986d5757eea10f9ccb08f9c3ef514ce0335f4a90e126f81131 \ + --hash=sha256:7caf95de39ce869ea0978068521cf6faa7350574fd1734ad6c63e5ed8cd06baa \ + --hash=sha256:81ebbc0c880c8a10f13118632e5c0d59159ceada8b651bba18f2e6dc70efdeda \ + --hash=sha256:8376c62e60a49e8fe51ffce569f74beed6e53fb58e5d6739f954c8c51ec0bec3 \ + --hash=sha256:83815b3a4cb9c3b17096c70833373610d5c93e89c134548cf43436c90326bfb3 \ + --hash=sha256:8a5fbcb86e44f1c0c9c052917eee67a04cbac9de7392fb4bc77c140ff4a7e471 \ + --hash=sha256:8cff06d18c9a30f8547a92757078aa345db1ba5b22e3082a05f64e50b384e27a \ + --hash=sha256:8f1f56004e8f1c1489bf279c25f1fa4764252cd9af5fb35675774268a4a615ba \ + --hash=sha256:8f5194c0544b08e56e108dad096c468c67f8ac60eec42763aed36ccbd3565346 \ + --hash=sha256:90ab2f00c09eed5bd986a80c8641e2dc10e7aca1a2d892d89a44b396e39c08ea \ + --hash=sha256:92643c9dd303de8960c3dbed93a28b8d87da5ed0a7776568979f379d7bc8a885 \ + --hash=sha256:974fb6fa4c4a682c633c4d7fa6087ccd717804726f9db1f771f1c2ea4f3990b7 \ + --hash=sha256:986efb3aec7655d69c14db2309a2072dbf181bdb906091fede83ad18e316cdaf \ + --hash=sha256:9882a204178cc8c915e0ce30abb4bdd1668e383c571b06649d5ed272d9625877 \ + --hash=sha256:99c42fe1449acfbf130da65e66b4d5b2726aba4497be359bae7672e38a15fc62 \ + --hash=sha256:9cf8608c9d6cb6bff9c624744f7a2ba8ab12276f097a07930490fe0e2219e9be \ + --hash=sha256:9d9f196007f0b15ea19c21732faacaea83cbf5946b6db4949b3b98cf871c93f0 \ + --hash=sha256:9dc61b7e38aca2ef384ac22b87a9c2b1e8769a1e15d916f93ea6a9a4551fc1d7 \ + --hash=sha256:a23cd023985b5f2ba23e84e1fadaeacde3c8a59e1d2adb3fe782e99db1e22387 \ + --hash=sha256:a31276323fdd355505883162f1dca4d7acc3d6ec2159c85ddef863f1fe9e8f05 \ + --hash=sha256:a6343249ce9401dd90c3b934fd9a4a618b6fc455f27a9aa11d198eebd4743137 \ + --hash=sha256:a98d1147fe1d3195053b67b474bccc0be5021506765d27f613a943c8c99f9e4c \ + --hash=sha256:abc0f0ce22464864fea208315d25e999e45cb5ee646ac1ca11d314a6a51dbe4a \ + --hash=sha256:b490f2d22df30affbfdcbe4f7896f321edb72a8dc0cbe5f38adec3de5b947c25 \ + --hash=sha256:b8ac1bc26223befbca986551521f37f4c1670dfe26fccb2f0fc2775e75be99c1 \ + --hash=sha256:c14fc6bd65e5624f76b90297b081222261476978f795f60d48745553617ddceb \ + --hash=sha256:c967ddb5e137c604ec94c7120d2f420ac9a9ce27b0e44987c7134b0eb5fe7a60 \ + --hash=sha256:c9f30ca28b991a920b446ed3ee19c7ecafcc49c46db592abf89cf239a7bb45f4 \ + --hash=sha256:cc2d113bf6236aa873dc710b38a856808c6054efd3a1a403c5acfceaf87138f1 \ + --hash=sha256:cc998e81748b72659802a7b434b39292308a5f4ab8dd4bf4739d65de1d4ed2bf \ + --hash=sha256:cdf2b8ab0effc335a9e61cf2a1e14c2295b999a656569b35f35ea3dc55a8931f \ + --hash=sha256:d2706a89242a347be20805147d58a38f4f4d8f6846228c4ee8dfd3587113719c \ + --hash=sha256:d9127feb5356ba3a92bdffa04c1bf6bcbc8d436369f78badf441018c3029dd63 \ + --hash=sha256:d92a0f4c7e6bb7deeafac68c79c92ef9340895fe825cf1a31078443753ab6756 \ + --hash=sha256:e8b3bfad0ae3ef0e67b40c193863dce8b7d79de545dadbe53c19acc3ace38f66 \ + --hash=sha256:e9864e19109e76111befc75d799d334e7365eb4189607aa734053c12e7840fa5 \ + --hash=sha256:e9c2b9aa8decdcf40ed8f4c887092c20a272f8c32215c3fee65e9db92ecf418e \ + --hash=sha256:e9caf67c0c05d27bab0d88cf193e73023f624c6714f1ff807c7d4b135b19d36a \ + --hash=sha256:eead129046822cb0fd47c78740b81bdaffd0515c0bb0306a2318acf0f0540b58 \ + --hash=sha256:f02e4021397ae02a139defdc6813b9942ab163de90affddd4ce4efbac299f619 \ + --hash=sha256:f742ebdd879063d2a92f4d57708886c720e854bc84ca240733f220936be0cb7e \ + --hash=sha256:fdf484d26016e0c016f23f2b635d2899daec034565fdcc062ed6b10f3b26a3f4 \ + --hash=sha256:fe0b9f0baf190663df37571cbb8a77370e7bd3a66068276d48f813c53ceba530 + # via pyiceberg +pyspark==4.2.0 \ + --hash=sha256:5ad689d53570ee1674193fd4f9bda065f0db3be9363a27d2a3406cc457b70b61 + # via feast (pyproject.toml) pytest==7.4.4 \ --hash=sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280 \ --hash=sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 # via - # feast (setup.py) + # feast (pyproject.toml) # pytest-asyncio # pytest-benchmark # pytest-cov @@ -4261,203 +5051,221 @@ pytest==7.4.4 \ pytest-asyncio==0.23.8 \ --hash=sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2 \ --hash=sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-benchmark==3.4.1 \ --hash=sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809 \ --hash=sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47 - # via feast (setup.py) -pytest-cov==7.0.0 \ - --hash=sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1 \ - --hash=sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861 - # via feast (setup.py) + # via feast (pyproject.toml) +pytest-cov==7.1.0 \ + --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ + --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 + # via feast (pyproject.toml) pytest-env==1.1.3 \ --hash=sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc \ --hash=sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b - # via feast (setup.py) + # via feast (pyproject.toml) pytest-lazy-fixture==0.6.3 \ --hash=sha256:0e7d0c7f74ba33e6e80905e9bfd81f9d15ef9a790de97993e34213deb5ad10ac \ --hash=sha256:e0b379f38299ff27a653f03eaa69b08a6fd4484e46fd1c9907d984b9f9daeda6 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-mock==1.10.4 \ --hash=sha256:43ce4e9dd5074993e7c021bb1c22cbb5363e612a2b5a76bc6d956775b10758b7 \ --hash=sha256:5bf5771b1db93beac965a7347dc81c675ec4090cb841e49d9d34637a25c30568 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-ordering==0.6 \ --hash=sha256:27fba3fc265f5d0f8597e7557885662c1bdc1969497cd58aff6ed21c3b617de2 \ --hash=sha256:3f314a178dbeb6777509548727dc69edf22d6d9a2867bf2d310ab85c403380b6 \ --hash=sha256:561ad653626bb171da78e682f6d39ac33bb13b3e272d406cd555adb6b006bda6 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-timeout==1.4.2 \ --hash=sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76 \ --hash=sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-xdist==3.8.0 \ --hash=sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88 \ --hash=sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1 - # via feast (setup.py) -python-bidi==0.6.6 \ - --hash=sha256:00081439e969c9d9d2ede8eccef4e91397f601931c4f02864edccb760c8f1db5 \ - --hash=sha256:00622f54a80826a918b22a2d6d5481bb3f669147e17bac85c81136b6ffbe7c06 \ - --hash=sha256:02255a04e26520b19081f7d378881b39050f5893e2fb4d65da81b849f58f4f76 \ - --hash=sha256:0781c3c63b4bc3b37273de2076cb9b875436ae19be0ff04752914d02a4375790 \ - --hash=sha256:07c9f000671b187319bacebb9e98d8b75005ccd16aa41b9d4411e66813c467bb \ - --hash=sha256:07db4c7da502593bd6e39c07b3a38733704070de0cbf92a7b7277b7be8867dd9 \ - --hash=sha256:09d4da6b5851d0df01d7313a11d22f308fdfb0e12461f7262e0f55c521ccc0f1 \ - --hash=sha256:0c298868017614d6b7e0e31293775ebe6622e87009d95e1ecd0abdc1fa5228a2 \ - --hash=sha256:0eb12b724cc99853e0e0425b54c1c2219492486afaca106c827204b4189504db \ - --hash=sha256:125a815f2b20313a2f6d331aa84abdd07de7d270985b056e6729390a4cda90df \ - --hash=sha256:166060a31c10aa3ffadd52cf10a3c9c2b8d78d844e0f2c5801e2ed511d3ec316 \ - --hash=sha256:183fee39bd2de787f632376bd5ba0d5f1daf6a09d3ebfaa211df25d62223e531 \ - --hash=sha256:1d627f8cfeba70fe4e0ec27b35615c938a483cbef2d9eb7e1e42400d2196019e \ - --hash=sha256:207b0a7082ec38045910d37700a0dd73c10d4ffccb22a4fd0391d7e9ce241672 \ - --hash=sha256:2150ac84f7b15f00f8cd9e29fee7edb4639b7ed2cd9e3d23e2dfd83098f719b7 \ - --hash=sha256:25fa21b46dc80ac7099d2dee424b634eb1f76b2308d518e505a626c55cdbf7b1 \ - --hash=sha256:27cf629a0ef983a25cfd62c6238ee1e742e35552409d5c1b43f6d22945adc4c2 \ - --hash=sha256:2d139bab64962731b5288edb1b6db76060c5a5183187efa590499951cd230b02 \ - --hash=sha256:33bd0ba5eedf18315a1475ac0f215b5134e48011b7320aedc2fb97df31d4e5bf \ - --hash=sha256:3428331e7ce0d58c15b5a57e18a43a12e28f8733086066e6fd75b0ded80e1cae \ - --hash=sha256:3564e574db1a0b3826ed6e646dc7206602189c31194d8da412007477ce653174 \ - --hash=sha256:35adfb9fed3e72b9043a5c00b6ab69e4b33d53d2d8f8b9f60d4df700f77bc2c0 \ - --hash=sha256:39eed023add8c53684f1de96cb72b4309cc4d412745f59b5d0dab48e6b88317b \ - --hash=sha256:3e17441d31a8665a44f5f42dba7646bbcd3c51ae6657dd019f6a7bb12618b12f \ - --hash=sha256:4142467ec0caa063aca894ca8f1e8a4d9ca6834093c06b0ad5e7aa98dc801079 \ - --hash=sha256:41fde9b4bb45c0e1b3283599e7539c82624ef8a8d3115da76b06160d923aab09 \ - --hash=sha256:43a0409570c618d93706dc875b1d33b4adfe67144f6f2ebeb32d85d8bbdb85ed \ - --hash=sha256:471c1a5fcdbb3de47377d74a7f1017216d9464e5428ca4e66f863e49dca73393 \ - --hash=sha256:485f2ee109e7aa73efc165b90a6d90da52546801413540c08b7133fe729d5e0a \ - --hash=sha256:490f8fe09ed423bfe00531f215e3b87e6000b8170408a0ead6ea5626f644b1d1 \ - --hash=sha256:493a844891e23264411b01df58ba77d5dbb0045da3787f4195f50a56bfb847d9 \ - --hash=sha256:4bb186c8da4bdc953893504bba93f41d5b412fd767ba5661ff606f22950ec609 \ - --hash=sha256:4eb3f28ca5e2f7238eaf67126c7634ec35603cbfbbe9b9b340ffee4a3314455f \ - --hash=sha256:4ecfd1d0f6d2927eb2114b55a63b298766b85fc9f0c9aaacb4e8df3e0468538a \ - --hash=sha256:4ff1eba0ff87e04bd35d7e164203ad6e5ce19f0bac0bdf673134c0b78d919608 \ - --hash=sha256:53122c3492fe3df871eb682c17eb848e24aa702946622ab78141c7027775519f \ - --hash=sha256:534bc7c84159b6e4b777f5fb9122902d6e19223c4242f5b94417de1afcfe2fd9 \ - --hash=sha256:5351efb4e86281eb26c420066fade935cd670c0c0960edc323b80d0b94a0bc19 \ - --hash=sha256:53d7d3a550d176df99dd0bb0cc2da16b40634f11c8b9f5715777441d679c0a62 \ - --hash=sha256:5506ba56380140b3cb3504029de014d21eb8874c5e081d88495f8775f6ed90bc \ - --hash=sha256:57c0ca449a116c4f804422111b3345281c4e69c733c4556fa216644ec9907078 \ - --hash=sha256:589c5b24a8c4b5e07a1e97654020734bf16ed01a4353911ab663a37aaf1c281d \ - --hash=sha256:5c9f798dd49b24bb1a9d90f065ef25c7bffa94c04c554f1fc02d0aea0a9b10b0 \ - --hash=sha256:61cf12f6b7d0b9bb37838a5f045e6acbd91e838b57f0369c55319bb3969ffa4d \ - --hash=sha256:6255ad385bb90aa39f8340967eef35657e52f8ed011773d37113cafa0ed5eefd \ - --hash=sha256:63f7a9eaec31078e7611ab958b6e18e796c05b63ca50c1f7298311dc1e15ac3e \ - --hash=sha256:646e83862dadfee00b75c93a930015e9f1cb924b26c34319a75aef65fcb3ddfa \ - --hash=sha256:686642a52acdeffb1d9a593a284d07b175c63877c596fa3ccceeb2649ced1dd8 \ - --hash=sha256:691822fac1d6f3caf12e667dd8b41956485c78b211032747c5f97822ba208726 \ - --hash=sha256:69c02316a4f72a168ea6f66b90d845086e2f2d2de6b08eb32c576db36582177c \ - --hash=sha256:6a4f4c664b2594d2d6be6a31c9254e784d6d5c1b17edfdccb5f0fac317a1cd5e \ - --hash=sha256:6c84d901fad5fe3b58a329c0b4a5c9d93a2d5430d150ad41f0e1165fc75ff439 \ - --hash=sha256:6cc626d2f77cac470b3167a28d4975744f3d99f5eaf8f5c2048ac9c0b9cba9dc \ - --hash=sha256:6dfa55611022f95058bb7deb2ac20755ae8abbe1104f87515f561e4a56944ba1 \ - --hash=sha256:76a1cd320993ba3e91a567e97f057a03f2c6b493096b3fff8b5630f51a38e7eb \ - --hash=sha256:7906229befa0cea2fe0278a934a27f657b68ce07a2606b1244f814a38b4ab42a \ - --hash=sha256:7d395e537a34d59e776fcdf50a50786d1a82084849d55cf644f4969ef8156643 \ - --hash=sha256:804c74d070f4e85c6976e55cdbb3f4ead5ec5d7ea0cfad8f18f5464be5174ec9 \ - --hash=sha256:825d15e547a9a2da5501966db672d6c8a5a063c041b2741ba32cc9775694b0ff \ - --hash=sha256:82c7f6bb3dfc4f61aecb2290f1ea24bb2450a5cbc94ee8abe5d6278b67859e0b \ - --hash=sha256:82e0befbc1078a964c6b6f2f7a616ae8015b52fdcd2f03979abf0fb1f2f18b48 \ - --hash=sha256:8706addd827840c2c3b3a9963060d9b979b43801cc9be982efa9644facd3ed26 \ - --hash=sha256:87a5489189b0a852da0129df77f0cc8e874b7b1ab1f968a209d340477906f076 \ - --hash=sha256:8b5f648ee8e9f4ac0400f71e671934b39837d7031496e0edde867a303344d758 \ - --hash=sha256:91a8cb8feac5d0042e2897042fe7bbbeab5dea1ab785f4b7d0c0bbbf6bc7aefd \ - --hash=sha256:91c12d58cec15385817f8b2c7c56de8e37523f05926f2de0e59199d3e50e1516 \ - --hash=sha256:92eb89f9d8aa0c877cb49fc6356c7f5566e819ea29306992e26be59a5ce468d7 \ - --hash=sha256:965e6f2182e7b9352f2d79221f6c49502a307a9778d7d87d82dc36bb1ffecbab \ - --hash=sha256:994534e47260d712c3b3291a6ab55b46cdbfd78a879ef95d14b27bceebfd4049 \ - --hash=sha256:9a9de76229ac22cb6bd40b56a8f7f0c42cbdff985dbd14b65bac955acf070594 \ - --hash=sha256:a138a7607b459414431a5cdcf5834624d6f87911a8863b51dd363a1e2e5744ab \ - --hash=sha256:a2a49b506ed21f762ebf332de6de689bc4912e24dcc3b85f120b34e5f01e541a \ - --hash=sha256:a525bcb77b8edbfdcf8b199dbed24556e6d1436af8f5fa392f6cdc93ed79b4af \ - --hash=sha256:a6ac2a3ec5ccc3736e29bb201f27bd33707bfde774d3d222826aa181552590b2 \ - --hash=sha256:ada1aecd32773c61b16f7c9f74d9ec1b57ea433e2083e08ca387c5cd4b0ceaed \ - --hash=sha256:af828457e46b31542569b4391014e6645023f6144de1dabf9fce7e9683235c25 \ - --hash=sha256:b144a1b8766fa6a536cc0feb6fdd29d91af7a82a0c09d89db5fc0b79d5678d7d \ - --hash=sha256:b271cd05cb40f47eb4600de79a8e47f8579d81ce35f5650b39b7860d018c3ece \ - --hash=sha256:b31f5562839e7ecea881ba337f9d39716e2e0e6b3ba395e824620ee5060050ff \ - --hash=sha256:b53b8b061b67908b5b436abede8c450c8d2fa965cb713d541688f552b4cfa3d3 \ - --hash=sha256:b65b4105998436405a3e6bca60cbf9714f6a08099b16c0cf4752a4a3a70eb45b \ - --hash=sha256:b8a83f28c104ef3b86ad60219d885b31728eb40c644f414f505068a6ecba3575 \ - --hash=sha256:b9498ead7f09eee272ff9c45900a8dcdc50a9558e126420a71d15774cc98bb44 \ - --hash=sha256:bbbcb28474b71e3ad05d8bd483348efe41fb7dfef6bd3046f3072baa0954d746 \ - --hash=sha256:bd5b3aa43d5222f1deef9894356a42f2443486501405977cda3aad0f23e20f9d \ - --hash=sha256:c07e4d6d8c8f574aa135436207a37bba522443a8490b0ba720b54d343dfde1a7 \ - --hash=sha256:c0e715b500b09cefccaddb7087978dcd755443b9620aa1cc7b441824253cf2b8 \ - --hash=sha256:c48a755ca8ba3f2b242d6795d4a60e83ca580cc4fa270a3aaa8af05d93b7ba7f \ - --hash=sha256:c4c0255940e6ff98fb05f9d5de3ffcaab7b60d821d4ca072b50c4f871b036562 \ - --hash=sha256:c4e08753d32d633f5ecb5eb02624272eeffaa6d5c6f4f9ddf012637bcaabfc0a \ - --hash=sha256:d1dcd7a82ae00b86821fce627e310791f56da90924f15877cfda844e340679de \ - --hash=sha256:d941a6a8a7159982d904982cfe0feb0a794913c5592d8137ccae0d518b2575e4 \ - --hash=sha256:da4949496e563b51f53ff34aad5a9f4c3aaf06f4180cf3bcb42bec649486c8f1 \ - --hash=sha256:dc8b0566cef5277f127a80e7546b52393050e5a572f08a352ca220e3f94807cf \ - --hash=sha256:de020488c334c31916ee7526c1a867bf632516c1c2a0420d14d10b79f00761c7 \ - --hash=sha256:e2f227ee564e0241e57269043bdfa13025d08d0919b349f5c686e8cfc0540dbf \ - --hash=sha256:e4a6251e212f828bb10ea69e0aa6b92b54f00bf56526b490fe890ca5f4333ec1 \ - --hash=sha256:e7e36601edda15e67527560b1c00108b0d27831260b6b251cf7c6dd110645c03 \ - --hash=sha256:e7edb0d1baf45c70384e700e10d723a13aabe116e14453cbf099eea4dd763e28 \ - --hash=sha256:e8bf3e396f9ebe8f4f81e92fa4c98c50160d60c58964b89c8ff4ee0c482befaa \ - --hash=sha256:e99e9ae745ba283f0230ac50af3f91657dd0b763778f88e4f0cbbc53b3e45d6e \ - --hash=sha256:edae3dd8e595a40d3cdd6ff8b6d9f3860cd17f674792ea05bba5bf5f1b36e5ab \ - --hash=sha256:f1020fcd3c8f1b93091730e3e16810d3741cbf69c6bacaa9d6a95fb15032848f \ - --hash=sha256:f1d3e139ca3963201994ee7f45d51dce6015166462cffa025daf95508547e503 \ - --hash=sha256:f60afe457a37bd908fdc7b520c07620b1a7cc006e08b6e3e70474025b4f5e5c7 \ - --hash=sha256:fb750d3d5ac028e8afd62d000928a2110dbca012fee68b1a325a38caa03dc50b \ - --hash=sha256:fd9bf9736269ad5cb0d215308fd44e1e02fe591cb9fbb7927d83492358c7ed5f \ - --hash=sha256:fe31aa2d2be1c79300bda36b1a3daf8c2dda963539e0c6eedeb9882fc8c15491 \ - --hash=sha256:fefea733a1acaaf0c0daba8ccd5e161b9419efb62d8f6f4c679c51ef754ee750 + # via feast (pyproject.toml) +python-bidi==0.6.11 \ + --hash=sha256:034090c597af250d699299d7e7f1e83eb016f9e47b3b707bd89ab2bdec77bce0 \ + --hash=sha256:0608bddcc1c53dfa5293499de13ca9935b31aa46d1c722c404a88c703d1a4e47 \ + --hash=sha256:0d339c4ebf3c2501d7569971a22e762fb45d8af7e19a0573afe260d69b35fb0d \ + --hash=sha256:0d496f9fc21b7457e12395e54088ab99776966c663b4cd4a74770c7a6418ab59 \ + --hash=sha256:19bae96ff1ee76b3a7dc962598b69426538e3021460cf85a4017437548ab6947 \ + --hash=sha256:19d13c70b1f7bb5dc69866a11624f3b78123807b4ede18f4f99f656ed86babec \ + --hash=sha256:1a639a2c62f825e72eb42901fd1c69296d09c192ae003195e6cf773f92d6bb42 \ + --hash=sha256:1ad5f712a32be30d28eb96e119e85818747a43cea253de6c3160b464b62a5619 \ + --hash=sha256:1b41cc6bc9ad78a12da5f987da15e931c771f18ceca58f2fe8ed50f253490a97 \ + --hash=sha256:1ffd728f1f7866ff7399906bbc17ef5cf010b90ce56a1e94937b28a1a4cf5a7d \ + --hash=sha256:20ab47a4098577fc9a82816c330c89ff597c31ba69c98bc6a1b6a5737b03de05 \ + --hash=sha256:239a00b2adb5f897d11d7b7a491d759fb9883e71a71cfd90c4147a733b4df4d3 \ + --hash=sha256:2b208fc26ab2c7adfd2c4d84753a5045f664e0bec19ca462af2406132e62f92e \ + --hash=sha256:2d6970b09f5a3102c0aa192f5258c585742ab4ebd94f637a635ad3448ccba567 \ + --hash=sha256:2ff75d1befc335cbe85f834e81554a024f94d9b5d1dd75a5bd99af81a1cb783c \ + --hash=sha256:302f4ca7dabfe447e707d40e98520551181036c15750bdd1e73292ad8b3d8e75 \ + --hash=sha256:30d543b5baf9fca5ef5ec95647aa07c5e38fc7fa0f18be0c61d1d6c0a1032c6c \ + --hash=sha256:337ec315f8b286399e411f5bfc339fc2124db07757409b0595ceb46de087c4cd \ + --hash=sha256:3611d13b53d4c899c4f2a7cd8eb897064e8b5546c3c5d1037dd6209c82858a27 \ + --hash=sha256:36f23f12d9b1c56ed8d82e11f56c6cba7bc3f614ee73374bc7772bb2270e0966 \ + --hash=sha256:380b70d615647646dbe06f7d95dc30b8fdc9b596dbfa1cd3814feb9805c0c8f2 \ + --hash=sha256:397f7f289eba6ce25d99dbea99f873d699bf9aa030074e7fb746d8f93c2fb6f9 \ + --hash=sha256:3bdb64ee0a74951465cd4a761e1029e73806ff43b2fe5be98643e52da5cabb66 \ + --hash=sha256:3da9e536546f7c62c0da595c8f71a096e0b9a80e94cfd0f329b7b200ef81e7d5 \ + --hash=sha256:41c7d1914c1f33954aa2ab0e2c309be5d1d4afc75ce524762eaab4dd825c5ab8 \ + --hash=sha256:43f3e81bdd36f49171b7de6cf471086df503c29555f6f7f035ebe8f8ec1da779 \ + --hash=sha256:490118f4af5a3535923c76be07e937ef1cb4cf84c489b37471aa95455fe923b3 \ + --hash=sha256:493655043ebea6fec0edb76053bdad46f1f7352a759ca4deb733f13b3c09839c \ + --hash=sha256:495a76c881d78ab87b57c5270679c9bc3c1de36d8c6596d5e3a5a1b5f9c57471 \ + --hash=sha256:4ac819cac1abb15486c48af3399a5c726e89f0977a3aff205ac162533186e756 \ + --hash=sha256:4ccf1fe9ecb3b02a1a11b103cd2557e2653d82c141b6e2dccec8177e6af5c4bb \ + --hash=sha256:4d00757ef7bbbf14d8628f9bdb6b0e168d5e7b03fec20da3226624f11bffce89 \ + --hash=sha256:51915502898c45e9cb36636e974aca068fc5cdb9f06b794f49abea5b12f02016 \ + --hash=sha256:519eb90bedb04b5cbba0bfc8062984a5ba054a019d81ada34053dc67397d61cb \ + --hash=sha256:539d99efe02f4981171ed57bbc085094ef780405ca14663550a56c6c3e265c34 \ + --hash=sha256:555cdf9303c40bae1ab512ca427f1f0316a574bc0a48db22eec76ec0fd1213cf \ + --hash=sha256:55f27bd2cdeca96f46448a741d393575fbedfc70e38e09dbb030ed98dba8cf2a \ + --hash=sha256:55fa5b22cc8c220a6daf077570a8e29d3b607bd978bd3dbc04b445d01a17de57 \ + --hash=sha256:565d819fddb2bbc58c42ca5c97d73da7567f181115011e8f04c76b9d08378dcd \ + --hash=sha256:56807e0dde88d5ab96880c9d668bda7bca1df83cd30d20cbff5d6b6e6c1e9276 \ + --hash=sha256:56dfa3c1c13cb89da39a4e8f2f9442e4291de01877511a13f830125d36e9ce5b \ + --hash=sha256:56f27c1edfd15c12c9c348378ccd79166930d720cf316b1181a0a0ade2146253 \ + --hash=sha256:57aa56b1eca5f63ebdd25fe8fad02eab7797fc45ad1efc5eed2a830e2bd2038d \ + --hash=sha256:5dd49c11ae87ee6ced68594faa78b34a89a1a3c43647fa6157765f9726f9daf3 \ + --hash=sha256:5f3e1743b2d43377c4da5d687a03430a27f5769e158a9f75a50b05e7e82f4d21 \ + --hash=sha256:5f9ed312d445a691c0afec995ca4f726abe357d8da500e389f77fb874fa7ef76 \ + --hash=sha256:60150bffbe43bfc1e356c0c2900ba643a02ce09c37bc0c553c4d83e5c3de63ad \ + --hash=sha256:60d9bf6c60c022657637f64897e63dc3f5b1c07cb7f0e1ead6150aff5150c5ab \ + --hash=sha256:619ee3fe03daec8d3ce12239f0c22455676a063b2bcde361caecd788fae5b8d5 \ + --hash=sha256:62a6b700f3d7a2c4d52a5dccca765711a2414e734360b00365e155698a26e461 \ + --hash=sha256:63d9dcc714d549a5118ebc93b7a7c903a0c1feec8e57f5fcacf98d984b76325b \ + --hash=sha256:6623683fe39b9fbf508e3069f17e8e9cab26143f9d9f89c8a8f45424c052df4f \ + --hash=sha256:68c3e570f2908ac39db0b269f5648e257fab5204344b9bf6dae80852ae4cbe1c \ + --hash=sha256:69d1f4ee17644e8aeac93a7238ee2f28d79b0180815441eb511b77e6585aa971 \ + --hash=sha256:6c92d1cad16f9ec2f2a3ae439a0bc3a8e4189ec227987bed03d5b4056d5eb9c5 \ + --hash=sha256:6ca92a4e460f7e25e434a6d7982d94a4765ca242f527995da70abb5a32003b8a \ + --hash=sha256:6d9ef69c108b31f38e1f281a55fdedad7774bc1e952a45c8c14a18e891eee397 \ + --hash=sha256:6dc112e2239f69913273cbb0c050ca816b145b32037d4266c430159c5ddcdab2 \ + --hash=sha256:721697187f4da67dafc26f63488f463fa35ff2de8668d959fda773cccdbef0eb \ + --hash=sha256:73c38c604bfc01647c69ce38e5cf8b4206e2ede91ca3eb8e5d79b7409f17e0b3 \ + --hash=sha256:74457b43db34f984252e915828b5d1a4042a771f44e853a5643506d01562eccb \ + --hash=sha256:7738cfc7ee9fcdff3fef76b40007982ce7704010a51df307c4a51d378f5ff1ba \ + --hash=sha256:777fb44a6f1e91d6adde36e815024c210b0208f35d00762880257b8fb04dc849 \ + --hash=sha256:788c11c84b520ea973992cc95751d56b783ff5857df504c1347d99cacd2fcfe7 \ + --hash=sha256:79df1099a08e53edb678236d4d76d8de4e3901bafc84ce1788b71f9b96547325 \ + --hash=sha256:7c4dc0d1328321aeb08054be654e1d39f171d5aae8dca931c9332860f18f57d7 \ + --hash=sha256:7cb34dd8d22ca1847653c0e1cbba8fd96de09c24b8c83b7805eab270c81e863a \ + --hash=sha256:8013c1e6267a929be98d10c3a617171709c8226bd33c6180ec65e560c35b6df8 \ + --hash=sha256:80cfe97e2d65981be877ae5bd2338c6919a7cc1171fc308c8b8c7c306ba6ffd8 \ + --hash=sha256:83c780f7e4c3dd3f020db75dc425567981e65c6d5571b3c0372203d0df92c834 \ + --hash=sha256:83ee87feb5eafc0442e1db0014dad20d52a2a7a140b6cddc8f7bc65918f0a7b4 \ + --hash=sha256:879c3fba3e7511c7d01020449d970ca7d6a593f20cfc47c014d3d229a38930b2 \ + --hash=sha256:8b6b7fce8f47578be9aebf5a0b6b2d6c157b4e97af7586ecf13bfca5d128deea \ + --hash=sha256:8eb09af209cd660fa9689f6ce9e61e73c8afa4829ae61801deea7f6e32263800 \ + --hash=sha256:8fbb6d222b50324fb9d49b6ff0f8566fa97b907a68c00e6622fcf34463104f4a \ + --hash=sha256:9435acc52438c3c8f5142b9a17a622927618e80a32eed707343bc375cb51cebe \ + --hash=sha256:946b7dbec4e64017680f1a66b3a8534659d889018ac83bd2abf958278e6f62b0 \ + --hash=sha256:94f7f66bd7680d3f2085adaeea91c01b3281a79bb5041db1a51cbe8af36a9d88 \ + --hash=sha256:959335cd3814cb767fb832c5c71cbc838ccd9231a812ef2cb43092a216a91d5b \ + --hash=sha256:969ee7db3e169fcc0b2d2d094826e03cc5798dfd6b3571a340ea883672396cb1 \ + --hash=sha256:9af2b5c26a3eb960699dff040535a86dc2c0f708087b2d63bcfd6452fe9d0664 \ + --hash=sha256:9d18916c97855d68515ca2b3581948b6d864c304cd386210ab5aff5e0fb9be2c \ + --hash=sha256:a1115f3f02eb836b39e0c986a6b9e92c4377a1e5a680409650b02ec6b1a795e6 \ + --hash=sha256:a1b5ee069001bf7f4fff109598a9396caa96bb35e1b906b2c6d1bab9f9b2c4bd \ + --hash=sha256:a4f4a0cd24c09df748bfdc207b089c00e7af19f3151063f4cd74ac658290186b \ + --hash=sha256:a52f7ad9ef9091e81869e5d255e796755ccf542ade14dda17647cb7d7ffe1b9c \ + --hash=sha256:a873e6f8ab28b5a56201d5cdb98f4c7d2ff88e636a58659c5ac271e287001658 \ + --hash=sha256:a9aa2890661b238730e680ccd6eb06f4da625b2dbc1730052dae6f2d88957192 \ + --hash=sha256:ab8af1cc29c3f400a0f781d50f8ab52a8c63adf2fcef16f14641cd97e06a80e0 \ + --hash=sha256:acccb6d90e694684db2d314db2e2b0d3b8949bf1cfaec6d9808a8889f548add7 \ + --hash=sha256:ad5d9b8e8a6c330208eba413db506de58f21dbf88a1f1d5d75ef5f9e0e714adf \ + --hash=sha256:af7711cc6eeeadcb1aae877e0736a68e111c73a29562da6106c5e2fbb4dd83b2 \ + --hash=sha256:b2c759e13ebb81edaac3041697328bb1ca8433b55281723879f6b54e74881240 \ + --hash=sha256:b433840a924c8788f0abbda15d22c71dc636e2078d7d3ba39369cebe4bef74b8 \ + --hash=sha256:bde89739a979d9eb3ac48c4882c8a42dd528a7708b0faa99b90b551588bd5f8d \ + --hash=sha256:c17393753ddb77377f754ba2a88cacfe3056040a8a5dff8d43f9d5e51bbe1edc \ + --hash=sha256:c26cd9d81f820159026b1c99905ca12bf13892e3f6a9303359fef42fe6f39e50 \ + --hash=sha256:c6aca20472d7040bc1f0aadb510cb20d40b3d6caa4c8bf82d075ba2650a41652 \ + --hash=sha256:c6b3c853f99172ef22e5a16c8114cf243e351c8e70f72a894164088c2c99d9cb \ + --hash=sha256:caee7ee3662eab1411b44fef8571b87273cb5235061d7463ebd10e412ac07986 \ + --hash=sha256:cd564b8c583eba2d230d02d0467fd840045f08856b6524555cfff7f32af63c72 \ + --hash=sha256:ce2ef68dce82ae4067cf28910224d77b45eb2c2b3904db6eb670d1cddb8d0eb2 \ + --hash=sha256:cf4e88a6fec81b7155a487cbbea7753a3d9a76dc4d391b4f8958b37227ef2c12 \ + --hash=sha256:d14f2d400c75e07d1154299463a3d4d14aa5565b05088b2d9b314ccd9fd6dc3a \ + --hash=sha256:d52ec8ccdc2fd5c61749d876a9d1eb0ea6543c1676722e1e3fb9d7800852131c \ + --hash=sha256:d5695d87969fed5b3799b8a98848cb04fe2873fded46efe9f3f2f341efd1b829 \ + --hash=sha256:d7291e13496cb74fc1b71f7f1e3628586afefa531102bb4fa7af9c2d543efc3c \ + --hash=sha256:d73a821873c52635321196cfb8d3a231d7917ca284cf2bcd9422f6deb19db7ca \ + --hash=sha256:da51a3a2940478219f19249fcf7cd45e8ddc197982be22efa43c4763e9d2eb57 \ + --hash=sha256:dc69d594682fbedec7d0cc81cd4d5f14fb40b6b365587238fafb91509cfea020 \ + --hash=sha256:dfbb9ba8343a60daf4ced67c11d551dafe9a3c94892c326e4c216fa2e6eca802 \ + --hash=sha256:e142820537e08d567c2c017e26df05f88856a03f0f3948209cfcdc39617df363 \ + --hash=sha256:e4ebc24ac38e50676f65daf7ba6c568789660cb60d6dcf2606d4310dba826721 \ + --hash=sha256:e6c474618a7b6a50c10007f8a9edb50def6d98d297a07a34dc2fb82c344f2b8b \ + --hash=sha256:f0ee4e80dd3b2a46868dcc73e00aa59f99c0e1de54a2ba783faf0fddba2ccce2 \ + --hash=sha256:f237ebb570fd8bbe479b6967374d82b7f0b26f9452c276bdd5f793d83e7062bd \ + --hash=sha256:f400a30573774a1e90c0d50d43c35d9c004afcf53de801bbfd259f84ea80f31c \ + --hash=sha256:f563d20481f7d316adf605bb94d5b7182acecdbc4d431d60473e9b1d526d0210 \ + --hash=sha256:f7a8429d0d65232e314b4f494825c1205abcc3039f42bf7da80424a15b731709 \ + --hash=sha256:f8655ac559dcfce7179b150ebcc207982d4d60e67b3089b2032eed3a0a78c07a \ + --hash=sha256:f99162d6c6c9522c46eb213f1bc932829c2602131676c92f081cb865b8ef6784 \ + --hash=sha256:fa2848c3116d619870d114471fcd4a9f1aa43587a002111d7af1e6582f1b57e9 \ + --hash=sha256:fc3b0a6e2460f68de9ab98f71e3098bb21bb984563417ad104dec7ab08ebcadc \ + --hash=sha256:fccd1808bb427d6a6d34168461bef551faa93ce3dda489fcef9073bcd9b34a5e \ + --hash=sha256:ff675d036823bff05a2e0f8cdb13f5414274ff517d13b0d57c15527015eb6acb # via easyocr python-dateutil==2.9.0 \ --hash=sha256:78e73e19c63f5b20ffa567001531680d939dc042bf7850431877645523c66709 \ --hash=sha256:cbf2f1da5e6083ac2fbfd4da39a25f34312230110440f424a14c7558bb85d82e # via - # feast (setup.py) + # feast (pyproject.toml) # aiobotocore # arrow # botocore # elasticsearch # google-cloud-bigquery + # graphene # great-expectations # ibis-framework # jupyter-client # kubernetes + # matplotlib # moto + # openlineage-python # pandas + # strictyaml # trino python-docx==1.2.0 \ --hash=sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7 \ --hash=sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce # via docling -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via - # environs # pydantic-settings + # pymilvus # testcontainers # uvicorn -python-json-logger==4.0.0 \ - --hash=sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2 \ - --hash=sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f +python-json-logger==4.1.0 \ + --hash=sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2 \ + --hash=sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195 # via jupyter-events python-keycloak==4.2.2 \ --hash=sha256:1d43a1accd4a038ed39317fcb3eb78211df6c75bbcbc4c482c99ee76327136f2 \ --hash=sha256:5137fd87c69031a372a578df96bae96b9aead2c9dad976613bc978e9e0246a1e - # via feast (setup.py) -python-multipart==0.0.20 \ - --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \ - --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13 + # via feast (pyproject.toml) +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 # via mcp python-pptx==1.0.2 \ --hash=sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba \ --hash=sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095 # via docling -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via - # clickhouse-connect # great-expectations - # ibis-framework # pandas # snowflake-connector-python # trino @@ -4536,20 +5344,22 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # accelerate # dask # datasets # docling-core # easyocr # huggingface-hub - # ibis-substrait # jupyter-events # kubernetes + # mlflow-skinny + # openlineage-python # openshift-client # pre-commit # ray # responses + # scylla-driver # timm # transformers # uvicorn @@ -4650,40 +5460,34 @@ pyzmq==27.1.0 \ # ipykernel # jupyter-client # jupyter-server -qdrant-client==1.15.1 \ - --hash=sha256:2b975099b378382f6ca1cfb43f0d59e541be6e16a5892f282a4b8de7eff5cb63 \ - --hash=sha256:631f1f3caebfad0fd0c1fba98f41be81d9962b7bf3ca653bed3b727c0e0cbe0e - # via feast (setup.py) -ray[data, default]==2.47.1 \ - --hash=sha256:106817f80087d21d24e63f6e56ea5ab7c387a25105eb65e6b783551f569534ea \ - --hash=sha256:21f2689c1bbc688f9cd31a18bae2c9582027e91b508073849441167bb5077816 \ - --hash=sha256:252a471e8afb918b105cdbffb4cbebb0143baad75a06c8ffcde27ac317579ccb \ - --hash=sha256:322049c4546cf67e5efdad90c371c5508acbb193e5aaaf4038103c6c5ce1f578 \ - --hash=sha256:36a30930e8d265e708df96f37f6f1f5484f4b97090d505912f992e045a69d310 \ - --hash=sha256:3eaeaeec3bbe2ca6493e530c30473d84b8580a7ac3256bb9183d8c63def5a92f \ - --hash=sha256:44900a1a72cb3bfb331db160a8975737c25945a97f376c70e72ccf35adf3b744 \ - --hash=sha256:48961229614b2b56a535be510c8abc76e99a9aa7fa195b5c949bd0c6c69af40a \ - --hash=sha256:601f23ba89918b7b3ffebf967328f7bdb605deaf8c103aad7820dc2722fe450c \ - --hash=sha256:6c7b4abe112c4d698243e30023bcbffe2c2c9a68416b95a6a0d50f9ca5725545 \ - --hash=sha256:6fc7df8657b8df684b77c2d1b643137ad745aa1c12ade34743f06cca79003df0 \ - --hash=sha256:7c03a1e366d3a868a55f8c2f728f5ce35ac85ddf093ac81d0c1a35bf1c25c377 \ - --hash=sha256:84a96b4720175a0000521a48eb7aa915f3b419bb5cd6172d8dee005c3f23b813 \ - --hash=sha256:8cd625d469ce15391e5f1f44ddf8dd30b2380f917603fa0172661229acb0011f \ - --hash=sha256:a640d447e0e6cf63f85b9220c883ec02bb2b8e40a9c1d84efa012795c769ba68 \ - --hash=sha256:bd1cba64070db06bbf79c0e075cdc4529193e2d0b19564f4f057b4193b29e912 \ - --hash=sha256:c21720f283a3df360ddec002a592ddfbaf520faf4cb1b86562a7b7c196ad96a0 \ - --hash=sha256:cd4e7eb475487364b5209963b17cefedcb7fbd3a816fdb6def7ea533ebd72424 \ - --hash=sha256:d6ed6d182e25d6f77179dc77bc97a749c81765b13cb671a46db3203029389663 \ - --hash=sha256:db5ff652e9035f03c65e1742a706b76519f6e8a6744cc005396053ac8766fc46 \ - --hash=sha256:dee02ba9b8cd45c4eadc457183f6d80f1701b85f966d02cdacd5b11867cb7375 \ - --hash=sha256:e578929f58b3f0c59c7544a96d864e26278238b755d13cd19ae798070c848e57 \ - --hash=sha256:e6d9c78e53ac89cabbc4056aecfec53c506c692e3132af9dae941d6180ef462f \ - --hash=sha256:feeba1e715cfd8737d3adcd2018d0cdabb7c6084fa4b093e638e6c7d42f3c956 +qdrant-client==1.18.0 \ + --hash=sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd \ + --hash=sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4 + # via feast (pyproject.toml) +ray[data, default]==2.55.1 \ + --hash=sha256:0053fd5b400f7ac56263aa1bbd3d68fb79341b08b8dc697c88782d5aca7b3ed4 \ + --hash=sha256:0ea2f670a7725833ad2333a8c46ab69865ad06c8e5de9f65695e0f8f35331cec \ + --hash=sha256:137f9006eee28caab8260803cca314f37bbda3fc94fdfa31c770b5d019626ad8 \ + --hash=sha256:1380e043eb57cde69b7e9199c6f2558ceeb8f0fc41c97d1d5e50ea042115f302 \ + --hash=sha256:156ed3e72ad95b645d2006cd71a8dddbcc89b56bfc00027f6225adf78bd9cb74 \ + --hash=sha256:263705f6bab29e7622a94f82da25fd7f9cead76cdf89a07aab28f79cdf8f9d95 \ + --hash=sha256:26541f69bb55607ef8335baac75b2ed12ff2ce02d56313219b29eda003039221 \ + --hash=sha256:2d5786661e192148719accc959def6cdcabd7a24cd9008005bf3d0e3c8cfd529 \ + --hash=sha256:4e618d61e1b14b6fde9a586151f3fd9d435b0b85048b997bcaa7f4a533747b2b \ + --hash=sha256:5e56d2e8f304cafe990c198a2b894f5b813de018998cd7212869201f6dc17cff \ + --hash=sha256:86e618e9ad8c6a24331c788eb599cee9838a62d2e10dfca0227743be06cf551c \ + --hash=sha256:9ad56704c8bd7e92130162f9c58e4ef473609515637673d5a36e761f95335206 \ + --hash=sha256:b062045c64c2bce39a51661624f7292c7bbf30f2a9d878627aae31d46da5712d \ + --hash=sha256:b415d590e062f248907e0fe42994943f11726b7178fcf4b1cf5546721fb1a5f8 \ + --hash=sha256:baf2ec89df7838cabdef493ff9bdbec1e6a6452f8bc696ad0c1b8a6198721745 \ + --hash=sha256:bb49fbbe53a1d931e1f92d17f9271338f0b738885f8f70b7f531aa33f019d8af \ + --hash=sha256:d5382da181c03ee2f502ef46cf0ae4bbc30157b5bd9a67d7651f6a272528a85a \ + --hash=sha256:f9844a9272ef2e6eb5771025866072cf4234cf4c7cc1a31e235b7de7111864be # via codeflare-sdk -redis==4.6.0 \ - --hash=sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d \ - --hash=sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c - # via feast (setup.py) +redis==7.4.1 \ + --hash=sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e \ + --hash=sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + # via feast (pyproject.toml) referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 @@ -4691,132 +5495,132 @@ referencing==0.37.0 \ # jsonschema # jsonschema-specifications # jupyter-events -regex==2025.9.18 \ - --hash=sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5 \ - --hash=sha256:039a9d7195fd88c943d7c777d4941e8ef736731947becce773c31a1009cb3c35 \ - --hash=sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282 \ - --hash=sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef \ - --hash=sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41 \ - --hash=sha256:065b6956749379d41db2625f880b637d4acc14c0a4de0d25d609a62850e96d36 \ - --hash=sha256:0716e4d6e58853d83f6563f3cf25c281ff46cf7107e5f11879e32cb0b59797d9 \ - --hash=sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3 \ - --hash=sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788 \ - --hash=sha256:0cc3521060162d02bd36927e20690129200e5ac9d2c6d32b70368870b122db25 \ - --hash=sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac \ - --hash=sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56 \ - --hash=sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946 \ - --hash=sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203 \ - --hash=sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788 \ - --hash=sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12 \ - --hash=sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e \ - --hash=sha256:16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442 \ - --hash=sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d \ - --hash=sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af \ - --hash=sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3 \ - --hash=sha256:1e85f73ef7095f0380208269055ae20524bfde3f27c5384126ddccf20382a638 \ - --hash=sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23 \ - --hash=sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4 \ - --hash=sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494 \ - --hash=sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1 \ - --hash=sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2 \ - --hash=sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096 \ - --hash=sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5 \ - --hash=sha256:34d674cbba70c9398074c8a1fcc1a79739d65d1105de2a3c695e2b05ea728251 \ - --hash=sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d \ - --hash=sha256:385c9b769655cb65ea40b6eea6ff763cbb6d69b3ffef0b0db8208e1833d4e746 \ - --hash=sha256:3acc471d1dd7e5ff82e6cacb3b286750decd949ecd4ae258696d04f019817ef8 \ - --hash=sha256:3b524d010973f2e1929aeb635418d468d869a5f77b52084d9f74c272189c251d \ - --hash=sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77 \ - --hash=sha256:3dbcfcaa18e9480669030d07371713c10b4f1a41f791ffa5cb1a99f24e777f40 \ - --hash=sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e \ - --hash=sha256:431bd2a8726b000eb6f12429c9b438a24062a535d06783a93d2bcbad3698f8a8 \ - --hash=sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e \ - --hash=sha256:47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450 \ - --hash=sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad \ - --hash=sha256:4a12a06c268a629cb67cc1d009b7bb0be43e289d00d5111f86a2efd3b1949444 \ - --hash=sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f \ - --hash=sha256:4baeb1b16735ac969a7eeecc216f1f8b7caf60431f38a2671ae601f716a32d25 \ - --hash=sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb \ - --hash=sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352 \ - --hash=sha256:50e8290707f2fb8e314ab3831e594da71e062f1d623b05266f8cfe4db4949afd \ - --hash=sha256:51076980cd08cd13c88eb7365427ae27f0d94e7cebe9ceb2bb9ffdae8fc4d82a \ - --hash=sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a \ - --hash=sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3 \ - --hash=sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425 \ - --hash=sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379 \ - --hash=sha256:5db95ff632dbabc8c38c4e82bf545ab78d902e81160e6e455598014f0abe66b9 \ - --hash=sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d \ - --hash=sha256:6479d5555122433728760e5f29edb4c2b79655a8deb681a141beb5c8a025baea \ - --hash=sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d \ - --hash=sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d \ - --hash=sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743 \ - --hash=sha256:6b498437c026a3d5d0be0020023ff76d70ae4d77118e92f6f26c9d0423452446 \ - --hash=sha256:726177ade8e481db669e76bf99de0b278783be8acd11cef71165327abd1f170a \ - --hash=sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742 \ - --hash=sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47 \ - --hash=sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164 \ - --hash=sha256:7e2b414deae99166e22c005e154a5513ac31493db178d8aec92b3269c9cce8c9 \ - --hash=sha256:828446870bd7dee4e0cbeed767f07961aa07f0ea3129f38b3ccecebc9742e0b8 \ - --hash=sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a \ - --hash=sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0 \ - --hash=sha256:87f681bfca84ebd265278b5daa1dcb57f4db315da3b5d044add7c30c10442e61 \ - --hash=sha256:8900b3208e022570ae34328712bef6696de0804c122933414014bae791437ab2 \ - --hash=sha256:895197241fccf18c0cea7550c80e75f185b8bd55b6924fcae269a1a92c614a07 \ - --hash=sha256:8e5f41ad24a1e0b5dfcf4c4e5d9f5bd54c895feb5708dd0c1d0d35693b24d478 \ - --hash=sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea \ - --hash=sha256:9098e29b3ea4ffffeade423f6779665e2a4f8db64e699c0ed737ef0db6ba7b12 \ - --hash=sha256:90b6b7a2d0f45b7ecaaee1aec6b362184d6596ba2092dd583ffba1b78dd0231c \ - --hash=sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783 \ - --hash=sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7 \ - --hash=sha256:a021217b01be2d51632ce056d7a837d3fa37c543ede36e39d14063176a26ae29 \ - --hash=sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68 \ - --hash=sha256:a295916890f4df0902e4286bc7223ee7f9e925daa6dcdec4192364255b70561a \ - --hash=sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e \ - --hash=sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b \ - --hash=sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368 \ - --hash=sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282 \ - --hash=sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306 \ - --hash=sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01 \ - --hash=sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95 \ - --hash=sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb \ - --hash=sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29 \ - --hash=sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a \ - --hash=sha256:c204e93bf32cd7a77151d44b05eb36f469d0898e3fba141c026a26b79d9914a0 \ - --hash=sha256:c28821d5637866479ec4cc23b8c990f5bc6dd24e5e4384ba4a11d38a526e1414 \ - --hash=sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4 \ - --hash=sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129 \ - --hash=sha256:c81b892af4a38286101502eae7aec69f7cd749a893d9987a92776954f3943408 \ - --hash=sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb \ - --hash=sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6 \ - --hash=sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f \ - --hash=sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773 \ - --hash=sha256:d4a691494439287c08ddb9b5793da605ee80299dd31e95fa3f323fac3c33d9d4 \ - --hash=sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730 \ - --hash=sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a \ - --hash=sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571 \ - --hash=sha256:dfbde38f38004703c35666a1e1c088b778e35d55348da2b7b278914491698d6a \ - --hash=sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459 \ - --hash=sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90 \ - --hash=sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab \ - --hash=sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f \ - --hash=sha256:ef8d10cc0989565bcbe45fb4439f044594d5c2b8919d3d229ea2c4238f1d55b0 \ - --hash=sha256:f04d2f20da4053d96c08f7fde6e1419b7ec9dbcee89c96e3d731fca77f411b95 \ - --hash=sha256:f2f422214a03fab16bfa495cfec72bee4aaa5731843b771860a471282f1bf74f \ - --hash=sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b \ - --hash=sha256:f5cca697da89b9f8ea44115ce3130f6c54c22f541943ac8e9900461edc2b8bd4 \ - --hash=sha256:fb137ec7c5c54f34a25ff9b31f6b7b0c2757be80176435bf367111e3f71d72df \ - --hash=sha256:fb967eb441b0f15ae610b7069bdb760b929f267efbf522e814bbbfffdf125ce2 \ - --hash=sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2 - # via - # feast (setup.py) +regex==2026.7.19 \ + --hash=sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0 \ + --hash=sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6 \ + --hash=sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62 \ + --hash=sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af \ + --hash=sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc \ + --hash=sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13 \ + --hash=sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd \ + --hash=sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951 \ + --hash=sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc \ + --hash=sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511 \ + --hash=sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12 \ + --hash=sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518 \ + --hash=sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db \ + --hash=sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae \ + --hash=sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009 \ + --hash=sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986 \ + --hash=sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1 \ + --hash=sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a \ + --hash=sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2 \ + --hash=sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0 \ + --hash=sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78 \ + --hash=sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d \ + --hash=sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4 \ + --hash=sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0 \ + --hash=sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11 \ + --hash=sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52 \ + --hash=sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e \ + --hash=sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902 \ + --hash=sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11 \ + --hash=sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6 \ + --hash=sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba \ + --hash=sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e \ + --hash=sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac \ + --hash=sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939 \ + --hash=sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb \ + --hash=sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc \ + --hash=sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095 \ + --hash=sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b \ + --hash=sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b \ + --hash=sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220 \ + --hash=sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c \ + --hash=sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae \ + --hash=sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3 \ + --hash=sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44 \ + --hash=sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665 \ + --hash=sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5 \ + --hash=sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97 \ + --hash=sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218 \ + --hash=sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864 \ + --hash=sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e \ + --hash=sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4 \ + --hash=sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda \ + --hash=sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459 \ + --hash=sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18 \ + --hash=sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3 \ + --hash=sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5 \ + --hash=sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a \ + --hash=sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035 \ + --hash=sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa \ + --hash=sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5 \ + --hash=sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78 \ + --hash=sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20 \ + --hash=sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a \ + --hash=sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a \ + --hash=sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a \ + --hash=sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965 \ + --hash=sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5 \ + --hash=sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797 \ + --hash=sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276 \ + --hash=sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c \ + --hash=sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547 \ + --hash=sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9 \ + --hash=sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d \ + --hash=sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1 \ + --hash=sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68 \ + --hash=sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a \ + --hash=sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd \ + --hash=sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c \ + --hash=sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6 \ + --hash=sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82 \ + --hash=sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7 \ + --hash=sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15 \ + --hash=sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e \ + --hash=sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38 \ + --hash=sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96 \ + --hash=sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2 \ + --hash=sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8 \ + --hash=sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732 \ + --hash=sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966 \ + --hash=sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053 \ + --hash=sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3 \ + --hash=sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0 \ + --hash=sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f \ + --hash=sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e \ + --hash=sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327 \ + --hash=sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac \ + --hash=sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6 \ + --hash=sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2 \ + --hash=sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a \ + --hash=sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435 \ + --hash=sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5 \ + --hash=sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d \ + --hash=sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312 \ + --hash=sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b \ + --hash=sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40 \ + --hash=sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974 \ + --hash=sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404 \ + --hash=sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff \ + --hash=sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf \ + --hash=sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175 \ + --hash=sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da \ + --hash=sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d \ + --hash=sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1 \ + --hash=sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2 + # via + # feast (pyproject.toml) # parsimonious # transformers -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # feast (setup.py) + # feast (pyproject.toml) # azure-core + # databricks-sdk # datasets # docker # docling @@ -4827,9 +5631,14 @@ requests==2.32.5 \ # great-expectations # huggingface-hub # jupyterlab-server + # kube-authkit # kubernetes + # mlflow-skinny # moto # msal + # openlineage-python + # pyiceberg + # pymilvus # python-keycloak # ray # requests-oauthlib @@ -4850,9 +5659,9 @@ requests-toolbelt==1.0.0 \ --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 # via python-keycloak -responses==0.25.8 \ - --hash=sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c \ - --hash=sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4 +responses==0.26.2 \ + --hash=sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364 \ + --hash=sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8 # via moto rfc3339-validator==0.1.4 \ --hash=sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b \ @@ -4870,177 +5679,135 @@ rfc3987-syntax==1.1.0 \ --hash=sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f \ --hash=sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d # via jsonschema -rich==13.9.4 \ - --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ - --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 +rich==14.3.4 \ + --hash=sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952 \ + --hash=sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9 # via # codeflare-sdk # fastapi-mcp # ibis-framework + # pyiceberg # typer -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==2026.6.3 \ + --hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \ + --hash=sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680 \ + --hash=sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9 \ + --hash=sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538 \ + --hash=sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804 \ + --hash=sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf \ + --hash=sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4 \ + --hash=sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97 \ + --hash=sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6 \ + --hash=sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96 \ + --hash=sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a \ + --hash=sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187 \ + --hash=sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975 \ + --hash=sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f \ + --hash=sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703 \ + --hash=sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9 \ + --hash=sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127 \ + --hash=sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f \ + --hash=sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa \ + --hash=sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05 \ + --hash=sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171 \ + --hash=sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba \ + --hash=sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c \ + --hash=sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223 \ + --hash=sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4 \ + --hash=sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885 \ + --hash=sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698 \ + --hash=sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f \ + --hash=sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7 \ + --hash=sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed \ + --hash=sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f \ + --hash=sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf \ + --hash=sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e \ + --hash=sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f \ + --hash=sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24 \ + --hash=sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a \ + --hash=sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41 \ + --hash=sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc \ + --hash=sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d \ + --hash=sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146 \ + --hash=sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e \ + --hash=sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e \ + --hash=sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4 \ + --hash=sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12 \ + --hash=sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7 \ + --hash=sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261 \ + --hash=sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6 \ + --hash=sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5 \ + --hash=sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93 \ + --hash=sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7 \ + --hash=sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda \ + --hash=sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8 \ + --hash=sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342 \ + --hash=sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c \ + --hash=sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb \ + --hash=sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0 \ + --hash=sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77 \ + --hash=sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3 \ + --hash=sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885 \ + --hash=sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826 \ + --hash=sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617 \ + --hash=sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb \ + --hash=sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577 \ + --hash=sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80 \ + --hash=sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e \ + --hash=sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945 \ + --hash=sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90 \ + --hash=sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7 \ + --hash=sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0 \ + --hash=sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140 \ + --hash=sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822 \ + --hash=sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba \ + --hash=sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9 \ + --hash=sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4 \ + --hash=sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a \ + --hash=sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8 \ + --hash=sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf \ + --hash=sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4 \ + --hash=sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324 \ + --hash=sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53 \ + --hash=sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b \ + --hash=sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41 \ + --hash=sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9 \ + --hash=sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca \ + --hash=sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1 \ + --hash=sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d \ + --hash=sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690 \ + --hash=sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107 \ + --hash=sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2 \ + --hash=sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76 \ + --hash=sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d \ + --hash=sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af \ + --hash=sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6 \ + --hash=sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db \ + --hash=sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369 \ + --hash=sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd \ + --hash=sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911 \ + --hash=sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504 \ + --hash=sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a \ + --hash=sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9 \ + --hash=sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13 \ + --hash=sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc \ + --hash=sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278 \ + --hash=sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868 \ + --hash=sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2 \ + --hash=sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd \ + --hash=sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4 \ + --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6 \ + --hash=sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9 \ + --hash=sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00 \ + --hash=sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f \ + --hash=sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e \ + --hash=sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442 \ + --hash=sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da \ + --hash=sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90 \ + --hash=sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef # via # jsonschema # referencing -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via google-auth rtree==1.4.1 \ --hash=sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c \ --hash=sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0 \ @@ -5058,198 +5825,271 @@ ruamel-yaml==0.17.17 \ --hash=sha256:9751de4cbb57d4bfbf8fc394e125ed4a2f170fbff3dc3d78abf50be85924f8be \ --hash=sha256:9af3ec5d7f8065582f3aa841305465025d0afd26c5fb54e15b964e11838fc74f # via great-expectations -ruff==0.14.0 \ - --hash=sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206 \ - --hash=sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2 \ - --hash=sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7 \ - --hash=sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f \ - --hash=sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3 \ - --hash=sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57 \ - --hash=sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02 \ - --hash=sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8 \ - --hash=sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543 \ - --hash=sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d \ - --hash=sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8 \ - --hash=sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296 \ - --hash=sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd \ - --hash=sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7 \ - --hash=sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2 \ - --hash=sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e \ - --hash=sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e \ - --hash=sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730 \ - --hash=sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c - # via feast (setup.py) -s3transfer==0.13.1 \ - --hash=sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724 \ - --hash=sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf +ruff==0.16.0 \ + --hash=sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17 \ + --hash=sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d \ + --hash=sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af \ + --hash=sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09 \ + --hash=sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522 \ + --hash=sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472 \ + --hash=sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0 \ + --hash=sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b \ + --hash=sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9 \ + --hash=sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213 \ + --hash=sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb \ + --hash=sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed \ + --hash=sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717 \ + --hash=sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a \ + --hash=sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81 \ + --hash=sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982 \ + --hash=sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e \ + --hash=sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4 + # via feast (pyproject.toml) +s3transfer==0.19.2 \ + --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \ + --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25 # via boto3 -safetensors[torch]==0.6.2 \ - --hash=sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd \ - --hash=sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9 \ - --hash=sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f \ - --hash=sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19 \ - --hash=sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1 \ - --hash=sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a \ - --hash=sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba \ - --hash=sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce \ - --hash=sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c \ - --hash=sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1 \ - --hash=sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b \ - --hash=sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5 \ - --hash=sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac \ - --hash=sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7 \ - --hash=sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda +safetensors[torch]==0.8.0 \ + --hash=sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358 \ + --hash=sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f \ + --hash=sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d \ + --hash=sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d \ + --hash=sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0 \ + --hash=sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc \ + --hash=sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235 \ + --hash=sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98 \ + --hash=sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4 \ + --hash=sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846 \ + --hash=sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca \ + --hash=sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0 \ + --hash=sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25 \ + --hash=sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452 \ + --hash=sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d \ + --hash=sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78 \ + --hash=sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774 # via # accelerate # docling-ibm-models # timm # transformers -scikit-image==0.25.2 \ - --hash=sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641 \ - --hash=sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341 \ - --hash=sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd \ - --hash=sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed \ - --hash=sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063 \ - --hash=sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054 \ - --hash=sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f \ - --hash=sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da \ - --hash=sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb \ - --hash=sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d \ - --hash=sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824 \ - --hash=sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09 \ - --hash=sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173 \ - --hash=sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b \ - --hash=sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147 \ - --hash=sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2 \ - --hash=sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99 \ - --hash=sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78 \ - --hash=sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0 \ - --hash=sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc \ - --hash=sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde \ - --hash=sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17 +scikit-image==0.26.0 \ + --hash=sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf \ + --hash=sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650 \ + --hash=sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb \ + --hash=sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581 \ + --hash=sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f \ + --hash=sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9 \ + --hash=sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d \ + --hash=sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e \ + --hash=sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2 \ + --hash=sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394 \ + --hash=sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37 \ + --hash=sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70 \ + --hash=sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44 \ + --hash=sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8 \ + --hash=sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc \ + --hash=sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab \ + --hash=sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c \ + --hash=sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3 \ + --hash=sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59 \ + --hash=sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af \ + --hash=sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd \ + --hash=sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466 \ + --hash=sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21 \ + --hash=sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37 \ + --hash=sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7 \ + --hash=sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5 \ + --hash=sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953 \ + --hash=sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49 \ + --hash=sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604 \ + --hash=sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219 \ + --hash=sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7 \ + --hash=sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0 \ + --hash=sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1 \ + --hash=sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd \ + --hash=sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0 \ + --hash=sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b \ + --hash=sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4 \ + --hash=sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f \ + --hash=sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505 \ + --hash=sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a \ + --hash=sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1 \ + --hash=sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd \ + --hash=sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932 \ + --hash=sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c \ + --hash=sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d \ + --hash=sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578 \ + --hash=sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa \ + --hash=sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331 \ + --hash=sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62 # via easyocr -scikit-learn==1.7.2 \ - --hash=sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1 \ - --hash=sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7 \ - --hash=sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c \ - --hash=sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda \ - --hash=sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a \ - --hash=sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c \ - --hash=sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18 \ - --hash=sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f \ - --hash=sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973 \ - --hash=sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290 \ - --hash=sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c \ - --hash=sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f \ - --hash=sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0 \ - --hash=sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8 \ - --hash=sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d \ - --hash=sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96 \ - --hash=sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1 \ - --hash=sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106 \ - --hash=sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61 \ - --hash=sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c \ - --hash=sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8 \ - --hash=sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1 \ - --hash=sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe \ - --hash=sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476 \ - --hash=sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44 \ - --hash=sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8 \ - --hash=sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e \ - --hash=sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5 \ - --hash=sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b \ - --hash=sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615 \ - --hash=sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33 - # via feast (setup.py) -scipy==1.16.2 \ - --hash=sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925 \ - --hash=sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232 \ - --hash=sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1 \ - --hash=sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88 \ - --hash=sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e \ - --hash=sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351 \ - --hash=sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70 \ - --hash=sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9 \ - --hash=sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851 \ - --hash=sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d \ - --hash=sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104 \ - --hash=sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d \ - --hash=sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936 \ - --hash=sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1 \ - --hash=sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77 \ - --hash=sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9 \ - --hash=sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3 \ - --hash=sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef \ - --hash=sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8 \ - --hash=sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4 \ - --hash=sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7 \ - --hash=sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92 \ - --hash=sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548 \ - --hash=sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4 \ - --hash=sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9 \ - --hash=sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374 \ - --hash=sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f \ - --hash=sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70 \ - --hash=sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173 \ - --hash=sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d \ - --hash=sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831 \ - --hash=sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88 \ - --hash=sha256:91e9e8a37befa5a69e9cacbe0bcb79ae5afb4a0b130fd6db6ee6cc0d491695fa \ - --hash=sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb \ - --hash=sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d \ - --hash=sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a \ - --hash=sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8 \ - --hash=sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e \ - --hash=sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b \ - --hash=sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb \ - --hash=sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c \ - --hash=sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6 \ - --hash=sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7 \ - --hash=sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e \ - --hash=sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371 \ - --hash=sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7 \ - --hash=sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779 \ - --hash=sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f \ - --hash=sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5 \ - --hash=sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2 \ - --hash=sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1 \ - --hash=sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3 \ - --hash=sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac \ - --hash=sha256:f3bf75a6dcecab62afde4d1f973f1692be013110cad5338007927db8da73249c \ - --hash=sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff \ - --hash=sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9 \ - --hash=sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f \ - --hash=sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472 \ - --hash=sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0 \ - --hash=sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c \ - --hash=sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21 +scikit-learn==1.9.0 \ + --hash=sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa \ + --hash=sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8 \ + --hash=sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b \ + --hash=sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42 \ + --hash=sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb \ + --hash=sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2 \ + --hash=sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60 \ + --hash=sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac \ + --hash=sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28 \ + --hash=sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05 \ + --hash=sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283 \ + --hash=sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949 \ + --hash=sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913 \ + --hash=sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277 \ + --hash=sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713 \ + --hash=sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a \ + --hash=sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1 \ + --hash=sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759 \ + --hash=sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f \ + --hash=sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673 \ + --hash=sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666 \ + --hash=sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119 \ + --hash=sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557 \ + --hash=sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162 \ + --hash=sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b \ + --hash=sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e \ + --hash=sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714 \ + --hash=sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96 \ + --hash=sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c \ + --hash=sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8 \ + --hash=sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa + # via + # feast (pyproject.toml) + # mlflow + # sentence-transformers +scipy==1.17.1 \ + --hash=sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0 \ + --hash=sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458 \ + --hash=sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118 \ + --hash=sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39 \ + --hash=sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e \ + --hash=sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6 \ + --hash=sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec \ + --hash=sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21 \ + --hash=sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1 \ + --hash=sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6 \ + --hash=sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce \ + --hash=sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8 \ + --hash=sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448 \ + --hash=sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 \ + --hash=sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b \ + --hash=sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 \ + --hash=sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4 \ + --hash=sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9 \ + --hash=sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b \ + --hash=sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082 \ + --hash=sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464 \ + --hash=sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87 \ + --hash=sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c \ + --hash=sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369 \ + --hash=sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad \ + --hash=sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f \ + --hash=sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c \ + --hash=sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475 \ + --hash=sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd \ + --hash=sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866 \ + --hash=sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d \ + --hash=sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6 \ + --hash=sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb \ + --hash=sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca \ + --hash=sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0 \ + --hash=sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca \ + --hash=sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d \ + --hash=sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee \ + --hash=sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4 \ + --hash=sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717 \ + --hash=sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49 \ + --hash=sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2 \ + --hash=sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a \ + --hash=sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350 \ + --hash=sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950 \ + --hash=sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b \ + --hash=sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 \ + --hash=sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444 \ + --hash=sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068 \ + --hash=sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff \ + --hash=sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a \ + --hash=sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50 \ + --hash=sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696 \ + --hash=sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21 \ + --hash=sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c \ + --hash=sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484 \ + --hash=sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118 \ + --hash=sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3 \ + --hash=sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea \ + --hash=sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293 \ + --hash=sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76 # via # docling # easyocr # great-expectations + # mlflow # scikit-image # scikit-learn -semchunk==2.2.2 \ - --hash=sha256:940e89896e64eeb01de97ba60f51c8c7b96c6a3951dfcf574f25ce2146752f52 \ - --hash=sha256:94ca19020c013c073abdfd06d79a7c13637b91738335f3b8cdb5655ee7cc94d2 + # sentence-transformers +scylla-driver==3.29.11 \ + --hash=sha256:0a93dd4af8995ac08335e581aca393a2bca64c3d4748ec359b9400797f2a7449 \ + --hash=sha256:17b19807a8690a4df2e52bb23327a746715e7076f5f28b45a6ce4556b2a00dd7 \ + --hash=sha256:1ea1fd011d8cf74b706c1b6ae982437597abf545e6d53f3a79905c97831db16a \ + --hash=sha256:241bffc347888dbeae48d842e58a7d20915ee518beb46467d9b7cd5a0ad501ed \ + --hash=sha256:2546a6646eead8225c8a0440c744c50ca85147fe61a712fb68e08be11ab21560 \ + --hash=sha256:2a4116f2da833f76db6e59b4c0534b72291cac478ca4b5944abbdd52b1635d94 \ + --hash=sha256:3094c90a49c172b1c09bbf44b46ea0c2c284e35cfd0222bc0062492de05131c0 \ + --hash=sha256:39f48ebe02d17e7cc6f3ba34f9748c971932eaa11d6557ab7d5fae1631987a55 \ + --hash=sha256:45050fd2028f07e236b7b3a555d4d17a1d88ea8ba8b5609ae1d14730a65b1e4f \ + --hash=sha256:57e8fffaf0803c0a406cc0e331a738c5d305fb28c9f5834d29c077fa134a8820 \ + --hash=sha256:617a8c459b30403002d38d7f99e3ed160da7671c7e62b0647e5fbd89539c6aff \ + --hash=sha256:677b5b0312275a958c80dbd3d1686e4a60c805ddae290a4b56551245c0a07a95 \ + --hash=sha256:6ee63f2503590f683e4b6c39c0070f567cc825fabd20f74cfe053d926a930ffc \ + --hash=sha256:70dda8384ae23472cfdf6c798d20e59639aa1e4796937417088151adb4acf4a9 \ + --hash=sha256:74668f6d4bca9f05f0826b8b464f363e7c1c7ac1cbbf373bae638bdc68780b8e \ + --hash=sha256:8072842efb2ff7c9761563485156fb0b6a11c68b028e32b6a807a70d226dbbbd \ + --hash=sha256:844e61a13edf0d12f95a64f6dd8a9c0fd0b0919570e10631e33c640acff59db7 \ + --hash=sha256:8ed1b2ec3634d3b25609a3a587de420a41b435f497e963434a8b5b42fa32c88e \ + --hash=sha256:9fb9bc8ae114434c3418ca8ba0bca00fb7afd4943a52694b5bde3f8681a0eea9 \ + --hash=sha256:a8f644d73713be556e99d6355cd2de41503ee50bff990865d71887e1f9f18b46 \ + --hash=sha256:abb5f98b4efbcb6257d1b97eea9c2bb95ea509e8983c2b8e021810ef460cb390 \ + --hash=sha256:acb38ed6acae2185bdef6834569bb95acd858ec91a8269c555e912127946e998 \ + --hash=sha256:ae36b731481c9ded343811132bcce4f68c9f2de8af8a51248fcea4d9c3b36d52 \ + --hash=sha256:b5a4851afb2a6d8410f55e7344b0589f71289338ff35c6901135c1a683381149 \ + --hash=sha256:bf33948e181c469f63108c01f319078051819c147f6446ea56b71975656efc1e \ + --hash=sha256:cd0d872882a4c098eacc83b832a042ea0b5165e247790d378e215c2a9f205d44 \ + --hash=sha256:cdd43ebb03db2441b920033d032a824612aa7f4daa58e88563e0a2036d6bd860 \ + --hash=sha256:ce1cc363d2b9c9e5a0def22b85c13c1bde6f26d1abca427b408bd18f0fd3828c \ + --hash=sha256:cf98016b6ce61dc30d80ac5bc491e0a84aa9da09e326fead5bba888d55f56126 \ + --hash=sha256:dda6b9f7fe48c6a0623e629a6bc0497dff132ca8a2a8897cd1f4281b5b12ecfe \ + --hash=sha256:e74576dba8992b4dac6ea3fe4c8cfd92f5318aa08b20142aff6423f20e56da7d \ + --hash=sha256:e7d899f1b5bc6d9441db01b05bd3c317b99be23525e6baae789ee59d759187fc \ + --hash=sha256:ee188f93fc931d9ce6231b550260988312445ecd2ea336445ec98b225ace47fb \ + --hash=sha256:f222606faeb11f7d41197f16b97cfc3c9578608bd5c2813d51d30c4dc42e675b \ + --hash=sha256:f3e2d06685fb1e6dad8905b0efa9423155852cf1b2a0d2b1aad3687cd488e4a4 + # via feast (pyproject.toml) +semchunk==3.2.5 \ + --hash=sha256:ee15e9a06a69a411937dd8fcf0a25d7ef389c5195863140436872a02c95b0218 \ + --hash=sha256:fd09cc5f380bd010b8ca773bd81893f7eaf11d37dd8362a83d46cedaf5dae076 # via docling-core -send2trash==1.8.3 \ - --hash=sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 \ - --hash=sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf +send2trash==2.1.0 \ + --hash=sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c \ + --hash=sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459 # via jupyter-server -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c - # via - # feast (setup.py) +sentence-transformers==5.6.1 \ + --hash=sha256:16af5d682ef66672b076d58599a23905800e850ec2bfb1865938306bf684ad72 \ + --hash=sha256:cefbb17b6325a982a4732c8c49fb013375392687049d1de3d435c4b04060680b + # via feast (pyproject.toml) +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 + # via + # feast (pyproject.toml) # grpcio-tools - # jupyterlab # pandas-gbq # pbr # pip-tools # pydata-google-auth - # pymilvus # singlestoredb + # torch shapely==2.1.2 \ --hash=sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9 \ --hash=sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b \ @@ -5321,13 +6161,11 @@ singlestoredb==1.7.2 \ --hash=sha256:92bc932df8b124a3c88b552210f9e0bb11cba4bdfbc9e7568c1582c00f0e8bcb \ --hash=sha256:c2a23b2b22f1e76cb0d53c99250de9a600bec9621766e25ae379c50914d6436a \ --hash=sha256:fba7f30f7fddb88e656e4309157d9e0016b6b1127d5adf348ba831bf77872d07 - # via feast (setup.py) + # via feast (pyproject.toml) six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via - # azure-core - # geomet # happybase # kubernetes # mock @@ -5336,60 +6174,65 @@ six==1.17.0 \ # python-dateutil # rfc3339-validator # thriftpy2 -smart-open==7.3.1 \ - --hash=sha256:b33fee8dffd206f189d5e704106a8723afb4210d2ff47e0e1f7fbe436187a990 \ - --hash=sha256:e243b2e7f69d6c0c96dd763d6fbbedbb4e0e4fc6d74aa007acc5b018d523858c +smart-open==8.0.1 \ + --hash=sha256:18b1c4496003c6902be17c15f032b5c319f307c89c6ae9e6b028b508bed8b2cf \ + --hash=sha256:3e97f90e92a952cb57863dfe132082c400a52eeeb27c067692fb51dbcc5b0089 # via ray +smmap==5.0.3 \ + --hash=sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c \ + --hash=sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f + # via gitdb sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc # via - # anyio + # elastic-transport + # elasticsearch # httpx -snowballstemmer==3.0.1 \ - --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ - --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 +snowballstemmer==3.1.1 \ + --hash=sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 \ + --hash=sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260 # via sphinx -snowflake-connector-python[pandas]==3.18.0 \ - --hash=sha256:0af10b207af3d2de2b130e89018d49a60f2e5cfe841f3bf459e58f2e1c4c4506 \ - --hash=sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628 \ - --hash=sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4 \ - --hash=sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e \ - --hash=sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee \ - --hash=sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9 \ - --hash=sha256:32b1abfea32561d817b0a2f80b06d936cb32712af06bf7b848a428bfd857a10a \ - --hash=sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a \ - --hash=sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728 \ - --hash=sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800 \ - --hash=sha256:4ed2d593f1983939d5d8d88b212d86fd4f14f0ceefc1df9882b4a18534adbde9 \ - --hash=sha256:51eb789a09dc6c62119cfabd044fba1a6b8378206f05a1e83ddb2e9cb49acc0b \ - --hash=sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc \ - --hash=sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb \ - --hash=sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba \ - --hash=sha256:783a9ab206563d7b52fdcdd7a72af62de811d3381ca64132fd3445537b4d041b \ - --hash=sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f \ - --hash=sha256:8d3e96e1d09b07edca6c1f6ca675b6fdd05a4a7e428e4cdf6fb697d87b9f60fc \ - --hash=sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a \ - --hash=sha256:a8c570edff5a4888840dbe1e9e65c5e4d77d55c5c800cd359fe0903a769201e0 \ - --hash=sha256:aeeb181a156333480f60b5f8ddbb3d087e288b4509adbef7993236defe4d7570 \ - --hash=sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca \ - --hash=sha256:b99f261c82be92224ac20c8c12bdf26ce3ed5dfd8a3df8a97f15a1e11c46ad27 \ - --hash=sha256:bd1de3038b6d7059ca59f93e105aba2a673151c693cc4292f72f38bfaf147df2 \ - --hash=sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e \ - --hash=sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00 - # via feast (setup.py) +snowflake-connector-python[pandas]==4.7.1 \ + --hash=sha256:051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117 \ + --hash=sha256:052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7 \ + --hash=sha256:0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317 \ + --hash=sha256:167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf \ + --hash=sha256:171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a \ + --hash=sha256:1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6 \ + --hash=sha256:22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf \ + --hash=sha256:2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b \ + --hash=sha256:4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a \ + --hash=sha256:4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754 \ + --hash=sha256:5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9 \ + --hash=sha256:571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d \ + --hash=sha256:5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35 \ + --hash=sha256:7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0 \ + --hash=sha256:7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5 \ + --hash=sha256:81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57 \ + --hash=sha256:8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a \ + --hash=sha256:92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3 \ + --hash=sha256:b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45 \ + --hash=sha256:d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa \ + --hash=sha256:e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327 \ + --hash=sha256:f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c \ + --hash=sha256:f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7 \ + --hash=sha256:f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52 \ + --hash=sha256:fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1 \ + --hash=sha256:ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b + # via feast (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via snowflake-connector-python -soupsieve==2.8 \ - --hash=sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c \ - --hash=sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f +soupsieve==2.9.1 \ + --hash=sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c \ + --hash=sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba # via beautifulsoup4 sphinx==6.2.1 \ --hash=sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b \ --hash=sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912 - # via feast (setup.py) + # via feast (pyproject.toml) sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 @@ -5414,133 +6257,102 @@ sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via sphinx -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -sqlglot[rs]==25.20.2 \ - --hash=sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab \ - --hash=sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567 - # via - # feast (setup.py) +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via + # feast (pyproject.toml) + # alembic + # mlflow +sqlglot[rs]==30.14.0 \ + --hash=sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9 \ + --hash=sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636 + # via + # feast (pyproject.toml) # ibis-framework -sqlglotrs==0.2.12 \ - --hash=sha256:0338c7770a5cb5bb0ec1dcbe5206359fe9b83da0aba8dde53b9e7bd1afc89a22 \ - --hash=sha256:057a8db59a6c4bcdc42831e7ad01f41cf9e7f388ed5b139816adafbcacf2f591 \ - --hash=sha256:065835e7f2be50ba83895b64d044a39dab9d95098fff995427365e4bd8bc7bc6 \ - --hash=sha256:08e8be22da77c964be76ab4438da2c77096f5871088466ca950ee1b4712a97d4 \ - --hash=sha256:147cda8412f45af290ad190d9a98b5829a5f46a575ce768279ccebf9b7b53785 \ - --hash=sha256:155b0d59e34851b119c7ff0b2c7968c7b51667c1a1c2abefe1ac7244b3c1d78e \ - --hash=sha256:17b289ef0f25a7c034d183c588345e2b56622f7f64a85d1020633a75f8e3ac96 \ - --hash=sha256:1fc98b7649445e726a492841b8b8b39a4e5724ec2787cd1436404ebccf42519a \ - --hash=sha256:2554ead3126c83864a4b7e48e8e7e1bc23faf7160a6f28d3db967661cf529c9e \ - --hash=sha256:2824fc87fd7e41a785150ff042c7934e1fff97c6ccd59e4d96bebf6697a90762 \ - --hash=sha256:2db7e6cd41ef88c2ac647ad0258f87906de822955dec8f14e91829083047d784 \ - --hash=sha256:315f7f7bbfedf0c87d98068e62363454e986bdd05baa165b7fb448b5c6fe9f1a \ - --hash=sha256:327bfc2d71449f4dffba93d63f0565c4a1fa818143b1cfbc3f936fa8c9bcce10 \ - --hash=sha256:39a6ef72cf271db93ec6019847b7832defa9f4013c1e66851ca9c0a11c010c0c \ - --hash=sha256:4364116b7b0c72b841de6acd149a002bfc8fe360125989d4f39debd387c874d8 \ - --hash=sha256:4c07d3dba9c3ae8b56a0e45a9e47aa2a2c6ed95870c5bcc67dacaadb873843ff \ - --hash=sha256:4ceb28cf2ee3850cd745167cebe59a5fc3d506b32e9c81307938d8d272c1d670 \ - --hash=sha256:4ec38035523d54ba33de1e2b5562de4938254b61e1df48eb1db0e26ea189de28 \ - --hash=sha256:5026eada48f258ce9ad26fa41994b2ea5404bef2c3df9cb5cb2a159112a6269f \ - --hash=sha256:59499adc27a70a72170db9241404a18d4829cd3a83a076b9e112ad365c4b1452 \ - --hash=sha256:5be231acf95920bed473524dd1cac93e4cb320ed7e6ae937531b232c54cfc232 \ - --hash=sha256:67e288759d2be822db2175d0025c1f61283b019f2cc3e2577f31ad0ef3b5854d \ - --hash=sha256:6aacab6e20d92be3ca76f7358fa12346f29985e2d408660c764b7f1c75cc40ee \ - --hash=sha256:6ef3a827f2980aad17af4f8548297c93c4989d4cd3f64b9bcb7443952c542423 \ - --hash=sha256:732516bffffc70f172306ad8bc747dd9f16512cdbc09475abe6ad6f744479dee \ - --hash=sha256:76e4e1765c6be438329e234e56a6772537f6de16c4bb5ba7170e344664cccdf7 \ - --hash=sha256:7b553cdb9e8afcfea5466815e865f874f6f51aaace4fb4101670e150f7bbfe5a \ - --hash=sha256:7c79c43c5cde1f4017641032f11770ed8111c963dccc096cd15df906d4fb46a4 \ - --hash=sha256:8174aa227193d0a755f4515e6c3883be4681c9b669a65c2316f09be27b84be4d \ - --hash=sha256:8a18b3a09c32788d1ee2d0610ab35af862413c56b65f8ad8bc0131701f03103b \ - --hash=sha256:8f268aea3d2ebc05cb9148bb487f21e532f8af1b0a4aed6b7374703aadfb6a7c \ - --hash=sha256:91971032603d05428fd42a978084110afb2a4c0975e4343b075f69a23889e3da \ - --hash=sha256:9334f6c394a671a630c61339d52fb7da1a72eca057570f039b2a4035d2e39380 \ - --hash=sha256:954ccd912391ab5922adb23159ebcc0c5dccb468381e2a1ce92117cb4b0f0ed3 \ - --hash=sha256:9597865efc40e5c41af7719106c7620e1338aaa64646726652c63bae14225391 \ - --hash=sha256:97b2c74fcdd89f0d4458c0e2b5783989be99a1e0b2d627797688ab716ad9391b \ - --hash=sha256:989ccc5dc6b38da937481b6eb2dc1fc0b13676fe129697b874828e577984d7ef \ - --hash=sha256:9c4c6f6fe1c54fff614f9d0b2dd7a6bf948bda87ce51a245dcd3f447f20c8b74 \ - --hash=sha256:9d5b9a9d6259b72258f6764f88a89faa3c648438bd1b2c3a9598b725d42bf6f2 \ - --hash=sha256:a266c9047726d83c51a8ec3d5278ceb9caf131307c9c93c4ceefd99c0116e538 \ - --hash=sha256:a4a2cacb31f75e242c7b9ff4afae1d95f548df8441444114376d8007cc91b55b \ - --hash=sha256:aaf86275a3388da1ed2161645aa346bfca3ee6e1dc0e2115867db9e78f1caddd \ - --hash=sha256:ab676d2d7da28907a139ad5fc20dee0890054967bac0b18e653ac048837c9ea1 \ - --hash=sha256:acc25d651eb663332157c2e5d2736516cddf4cd0effe67a887723934de5051d1 \ - --hash=sha256:b10bf6b71961b31951bf4dff937d8d5d399ea1b3bd47fb5c5810386710fe7dfb \ - --hash=sha256:b40601e67f5abae5d09d23f92394dbd735539de469ce663b596eb42bf77d2c54 \ - --hash=sha256:b6020825e58af6e2795e6dcb69639f5500e45e1da78f1b1abd74c4d11083a249 \ - --hash=sha256:bc1807c6222e32fc9bf6f5c7e12b85c4b72f12227800d40c1693244c198b33bb \ - --hash=sha256:bd6c4e6a7670f761c8e69b45d6d302a4d37a3cddb1fdca2ad90e54b77858fe80 \ - --hash=sha256:bf3e2eab11f06f1df13c0f85b3e26fbab0b7e8a5d189e5edfed951bc85f6bd48 \ - --hash=sha256:c3d62905ce74a48714b7662ad95efe299fad62f193be4b482a327af060f98710 \ - --hash=sha256:c3e0edde0fdf598561e7404ac56fb4b12276394ee5155b5365e42434c6f287a3 \ - --hash=sha256:c64066d13bd2e5e788b845c933c765af9991faa93982e273b623019a1161fadc \ - --hash=sha256:c8bf7ae29c0fc66e9c998d7f8e6f6fc26309c6eb5a4728e1443cb628218bc307 \ - --hash=sha256:d2827c7bf7e57496f9b95658bcd2395cfb0c51adc3023cd3386988337dfaf6a5 \ - --hash=sha256:e7b2da43b2a6a85807df6c56b2627abe244aff28fdf9a4940d38d749cb4b8e3e \ - --hash=sha256:ebc162a599fac86e59f899631716752fbc7f89598e94729eadb707e54db371b2 \ - --hash=sha256:f0a2ddeab27a94447270b7a240770a31a3afed0a972d60085205baec990ad76a \ - --hash=sha256:f104a98182761d4613f920eda7ec5fc921afb3608f7db648206ce06dd10a6be5 \ - --hash=sha256:f83ad3fb4ea57218c0e65d3499e31c9bb3051bbb5dccbb11593eaf1640964b51 \ - --hash=sha256:fa1ae834fb78bd52bb76e3c8d02cb79f45717ab1f02f4ad8154bf33a5408a502 + # sqlglotc +sqlglotc==30.14.0 \ + --hash=sha256:0337b982675e17f6dd9a1ed7d944fdef4f81a2aaff6a4494d50492aa18fb0b4e \ + --hash=sha256:07ee5ed7f8b13cc8e918a31c328933c3f4fd155f5b4360bbbf305445b0f88e73 \ + --hash=sha256:13441a60a212faef4c658b61394cb22b922bf80c1bb544a5397bed67d5549aeb \ + --hash=sha256:13f24fa61b21bf21d6734fd9eeaffa8965dc70228b39994005488688bbc033bc \ + --hash=sha256:13ffa7852e59a6b12e3dbd96686c594847bd6bf4b1a41f1cd8975b0f90e2a522 \ + --hash=sha256:17951d2587cdfb39086e2b06e6d3bfb15628232a295580c6aecda881a4770824 \ + --hash=sha256:20c01558912dfc4a06b8484459337efa6fe98f3e45602e213d888fde56f0410e \ + --hash=sha256:331c600908846a4fee6ffbda0cd82e4b1550612503c761166010a3776334aaf0 \ + --hash=sha256:47f7ab74779572fd51f616522ca597b034158150a7a312a3dbe907d103f5d908 \ + --hash=sha256:49157e4dca20cdab9187dec83f51268d2e3c883ebc9b31afe5e5cf11954bd38e \ + --hash=sha256:5410ff65f26b57e6a9d60f57dd46e8d7aa96b70d1faca562b3931a2b03eb4d56 \ + --hash=sha256:68c259b9ef4ac630c6c04f4e6217f130f0856abe6146133226442e82e1b54ce4 \ + --hash=sha256:745bd364e8a5032c4ead9dd843bfa00c5b35a1d816801de7cfb1eeab08f24b8d \ + --hash=sha256:849520c4b9057408e9198f019791538d5bb77c648b3fc6410db1727acdac90ad \ + --hash=sha256:968f446a8304e782d6778389de561cad9f984b89c3efea0474d8ec89ae17f02c \ + --hash=sha256:9918233d827dcc2b1842a8fe3cbc8d915ab71d9ce0100b566e283edb53950867 \ + --hash=sha256:acb7f577a8e060fb1cb1c9df1b1de1187fc7c53d54539cb6cdc16bd558adf2a5 \ + --hash=sha256:b1f414c6a6a3c3b56b981e51f9fdd1b1cf95dee5de2592c4b4547803309a57d4 \ + --hash=sha256:bc3c4ff8f268e2558aed03638ee002d2187cdf4faa15bf34b6f40558abe29e6e \ + --hash=sha256:c9c9cf31aac1414ccde5e69849bf438b3c74ae763db00651bdf71955ebadc214 \ + --hash=sha256:dfe8296ee3e505c4f3ae22aee4b0556e1263410e02f274ead0bdb723cd1c4147 + # via sqlglot +sqlglotrs==0.13.0 \ + --hash=sha256:6b934a244b16f26fca50974328a2ebc7689583c59f06203cebb46e2e6e8d93a7 \ + --hash=sha256:ad1ad158234af0f8ba5054ca51bd17a7c1e3f81b4798c7970ebf7953fe08ddcb # via sqlglot sqlite-vec==0.1.6 \ --hash=sha256:77491bcaa6d496f2acb5cc0d0ff0b8964434f141523c121e313f9a7d8088dee3 \ @@ -5548,290 +6360,313 @@ sqlite-vec==0.1.6 \ --hash=sha256:823b0493add80d7fe82ab0fe25df7c0703f4752941aee1c7b2b02cec9656cb24 \ --hash=sha256:c65bcfd90fa2f41f9000052bcb8bb75d38240b2dae49225389eca6c3136d3f0c \ --hash=sha256:fdca35f7ee3243668a055255d4dee4dea7eed5a06da8cad409f89facf4595361 - # via feast (setup.py) + # via feast (pyproject.toml) sqlparams==6.2.0 \ --hash=sha256:3744a2ad16f71293db6505b21fd5229b4757489a9b09f3553656a1ae97ba7ca5 \ --hash=sha256:63b32ed9051bdc52e7e8b38bc4f78aed51796cdd9135e730f4c6a7db1048dedf # via singlestoredb -sse-starlette==3.0.2 \ - --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ - --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +sqlparse==0.5.5 \ + --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \ + --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e + # via mlflow-skinny +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 \ + --hash=sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627 # via mcp stack-data==0.6.3 \ --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \ --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695 # via ipython -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via + # feast (pyproject.toml) # fastapi # mcp -substrait==0.24.2 \ - --hash=sha256:743cc352e96b0927b2cd37cd5a8fdac0a96a68df9600bd104fc36aebd222a836 \ - --hash=sha256:d1d475833566fa9d67eed3273456883c0568486ccced92b524b31709d2817e19 - # via ibis-substrait + # sse-starlette +strictyaml==1.7.3 \ + --hash=sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407 \ + --hash=sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7 + # via pyiceberg sympy==1.14.0 \ --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 # via torch -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 # via - # feast (setup.py) + # feast (pyproject.toml) # docling-core # docling-parse tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via + # feast (pyproject.toml) + # pyiceberg terminado==0.18.1 \ --hash=sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 \ --hash=sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e # via # jupyter-server # jupyter-server-terminals -testcontainers==4.9.0 \ - --hash=sha256:2cd6af070109ff68c1ab5389dc89c86c2dc3ab30a21ca734b2cb8f0f80ad479e \ - --hash=sha256:c6fee929990972c40bf6b91b7072c94064ff3649b405a14fde0274c8b2479d32 - # via feast (setup.py) +testcontainers==4.15.0 \ + --hash=sha256:085cde086337632e19002719460b7b80bbab2bdd51bb3ea04f77d0de96504706 \ + --hash=sha256:8796c14e76604031ad39cf0ed3b8e9806283a1fbf5270965c2b1c594caa31b74 + # via feast (pyproject.toml) threadpoolctl==3.6.0 \ --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e # via scikit-learn -thriftpy2==0.5.3 \ - --hash=sha256:08d8699d318b6a8fe9e9fd4c2234ec7912462d90cc636c371b4f4f6500a44328 \ - --hash=sha256:0f36f80a038dbfc2b3b048151ca4732f310ebd0385cdf20e7864d781d5d6f582 \ - --hash=sha256:13c0316a1a9b6f7840d9c084a5a1fa2e419ae86645e45530593558704e792d7f \ - --hash=sha256:195beb93caa104879d808e87d92962fff8d59d40486590fd653b5aeb7774420e \ - --hash=sha256:1bb6c0482663887f2a9ab98453ab0ca20a3e1f2336a500b7da12af33614c0d75 \ - --hash=sha256:2023abcc504e4fc8825419964ecfab904244b0bc189d0082380d481ecba951d7 \ - --hash=sha256:236a7d4627b1aa692a901ca45d7dfa4e516bcd3f309efc18ac69671b31789e39 \ - --hash=sha256:29b09fa1fb77f1927ac4ce21d8f8f6663d8917f75780aa6bad57ca9473d0a3b3 \ - --hash=sha256:2d3c0403673a3b7fc38304cf89e07c792f44ac6aa3b15c12e6cc411a85d10af3 \ - --hash=sha256:2eb14e24febbeca84d603e88a24db9ccb4a1437f90e9a862dcee02dc0a2194c2 \ - --hash=sha256:2fb20edf082965487bddfba03b2c05bb50db38ceda3111540cb2353949fdb29e \ - --hash=sha256:387c04d02f23ae83415cb2de35a88ba79321619af25cf34a481cabc367ddf1aa \ - --hash=sha256:3c00c340114c3041961906a628e70e6b6b5805ee691e682c290cec3513e77efc \ - --hash=sha256:3da0e3c1a5c17f67a203d9814853dd1d8fe8b0ec69a26d30d6b634e4c0e2c87c \ - --hash=sha256:3e2fa8c3d2b1505d2a463c090d9e771b8fed0eda8b01b0365e7547ba106bf2db \ - --hash=sha256:455440132b01b3a895001dc59ecf6056e8fd041ad6e745ff22391cf3a1f8361e \ - --hash=sha256:523c480a4b3aad480e4738c32f97b3f356ed998e6549f5f55eb6f7852474cfff \ - --hash=sha256:524d69102843fed087e30c6edc5b99f7b42b768d88bf910787add60e37e2a2a5 \ - --hash=sha256:53e761ea5eab24bb3520f8adecdbb633e69dda7cf9678ca2eb6ad1952cc56540 \ - --hash=sha256:5e799f6c4caf79a5566bc14941e768b132c533bed66e0a5ef0a127a74f98acab \ - --hash=sha256:6196d7d5adb6214ed21633ec57a222c90a6a66498cdd9f8da7c85c7514c7c439 \ - --hash=sha256:6384a142514982b380431b7d8811f137c5ec9cf5cf3affa33884b7ce4a51c8c2 \ - --hash=sha256:67ec304d83510d0ec83338ef029ea3bae91fdbb3bf0504f7990dd446b483773e \ - --hash=sha256:6efccde429f93740943bc4c0e2664a49f0799ed21663cc26b16a823e8719040f \ - --hash=sha256:710b7f3d9fdbb5788b37b4b694d30fa37a3c08c0d52aeb1def57a5140fa2f4fd \ - --hash=sha256:716c36885e29a9480bdabe117235967b5fe36dc179aca80cd8ef9e12866e4cef \ - --hash=sha256:72a1cdfd3bcf16b667379f8277e83295a34ae74227e54711c657305bf4c9e63b \ - --hash=sha256:73832e79732bab15920bed700a5efbe2b6e9dbd35710e815e870feb873b15059 \ - --hash=sha256:74dc6ed8c9098b66a17b916dd898abc32801a8ef0b439a407bce4f11c1b3da34 \ - --hash=sha256:772c9c1015d05177e37b9a547bcd27b560fc888ecd1e179ebf7f114ec467914d \ - --hash=sha256:86489105abb39c6ff93c3d270cf1474f7fddd380847f2b6bc8d09e5d0f0a23ab \ - --hash=sha256:8a6652e823e0ae6fa9f73b62c1a2ed04d7c0e1ac402c7ca7c509f9f14fbcc80f \ - --hash=sha256:9460a8284881854e210907eeb1761f44afacb4d164d1b6ecfddc184ed1b03277 \ - --hash=sha256:94806a0c3436189a75efac4ab067cdab7298876fee40cc0006300cc1d7982055 \ - --hash=sha256:9cba3454b4e5e05102d2dbfadd3a9a66c19488c6aa18c147bbeff2097ae67f04 \ - --hash=sha256:9eda43701a94def9d063550b0d8261630c40ade312c35b1f6e4804859c783ed8 \ - --hash=sha256:a57c67a880c9da2d252a6244e9ccf7b08850388c7afa4f0e98cb60fdca904a09 \ - --hash=sha256:a7a00b772783847c0c48a43e098b64f5741ca5a2e52e6c66d9b753765cd93ed3 \ - --hash=sha256:a7f2913ea3beac18767784059f02a67751d99094d4a368b350911784f0e09709 \ - --hash=sha256:ab47f689b0003ec63a881e5ad4f46046f62632da5168b0665fd369a3561eaa2a \ - --hash=sha256:addcc3ba9c106758e9073ab205e4bcf9a9540acb335fafa0184a1fa8e76a78cb \ - --hash=sha256:ade0165ba060b97333bc7a927229e992441bfa17bb8e13ea05590c2ec3551b17 \ - --hash=sha256:b208f3c23f916ca0517285c11748ca1fcf43a2ac2224ea5eef8bcba464a20652 \ - --hash=sha256:b5670936016aeaeb7111c96661ced36541211c0e82eb357a9bee5d4176ebbcff \ - --hash=sha256:be23631c152323dd3d7d51368dadcec75e60e90e4662be4f2b8ada208c61fa34 \ - --hash=sha256:bf69d246c39d0ce4ed922b6e00e643ca514cdf40010b00b46f82b0f758a840b3 \ - --hash=sha256:c01e0da29120709d46cb4310944fc717f28ce097d8845c4c29e111ff98c9deff \ - --hash=sha256:c0cf4418810ecf984f6d7f538988175c459f6bd5c85d94b878ebb11dbdbfa62b \ - --hash=sha256:ca4d554f8fc79c8152119bbd576e5d6a5c11e907e0baf467fb4676b1d274558a \ - --hash=sha256:d5080c1c4bd13c4431613a2c0cd607c5e3a07a496a865a0d01f534401d3b09c7 \ - --hash=sha256:e6a77d3d190f1c2726cfb11d1115678fcfa4b0ff509bd8bb38e451c629d9383c \ - --hash=sha256:eb440b7d8e7460f6969016d77e25ebfdee2aa6d5fed95aecf2bd59310c2c5530 \ - --hash=sha256:ec49907ee15513ca2344540c4ad2bf1945c41a6c0236d589eebd32be8298faa9 \ - --hash=sha256:f2ccb893ae687ff946902d96a5615a93847a7868bc5d66f51caf5ccf46466314 \ - --hash=sha256:f4210c10b686fe4a32b121f618b407aaccc7a72021c6d64fa181a09df72c4d89 \ - --hash=sha256:f4d122a82cf7cd4743a2640199b066a994f6527802c2dd16f2e4fffc15efa2a3 \ - --hash=sha256:f768756c0c105e98a3760fc7b4f4df12e25f5334b204060bb6bdab3ce1599e11 \ - --hash=sha256:f965fff2f2f323ddb5d9cb7fabe33c4c9f008955dbb59728ffc3111557b87793 \ - --hash=sha256:fb86f4c0cfcb39949a53dcc689e3758594a18724753861ba9f59646b72417383 \ - --hash=sha256:fd4c6131ca6e919f03263cc83b713f1797bc20126a858da8518ed49b3e32c334 \ - --hash=sha256:fdc5676b52fa6a3009d205360eb9ba257b8b4813883ed52797a20838bcc45dde +thriftpy2==0.6.0 \ + --hash=sha256:033021acfc347f3e51cf107b189a5efafcd1e974a8217a4d066d93719b2bf353 \ + --hash=sha256:0345c8ba40f7b98c24c1ecabfc04ff512ed930ab86ff277572bd279b69a0a252 \ + --hash=sha256:0a0249bf9004d241cf6fd1ed1879209ab7641f7e09456323a839afb6c9213b58 \ + --hash=sha256:0aa86f5d83a49567fa6ac81c7f78ffe8e5cf68b57cf3f7f7c55dc1486f5e9bbb \ + --hash=sha256:0c2f823bb691dd71c9c81170026dc52ace5b750881799960ea9f992919eb9731 \ + --hash=sha256:1245c36b82f34aa26f049e6529f40ad34d9be8d12bd0131559847c8476b98ce0 \ + --hash=sha256:151d299e8e3694a6cc0f2f2cda01d5744080742649de958c5cdcbebb4306205f \ + --hash=sha256:16eecfd34bd541b75172ba0d69ea90b874611a7361d18906fb6d95955089cc30 \ + --hash=sha256:182f54a7248c8ecf1320cf26d1fc9115765df6b1f2589c1d2d0df7049a5b27d4 \ + --hash=sha256:210345281d41b3a76d263b555d3e3cc482103738441bdb92a73033a4e9a042e1 \ + --hash=sha256:265588b8cdb3fe1097db43bf35fb208edc69d9350a2bec3a737d6357d0a5650d \ + --hash=sha256:28fd55960a6d42207060536109928a4615fbbd6874c0ddd8a705b47075f1d2d0 \ + --hash=sha256:29ff125e40c8016b4d3bf48e6d727bd93d2892451b47bfe57ba896944ecbdb0c \ + --hash=sha256:2ae866adf9b112c7ab30c1a90d878a5d6f2d40244fbc46ec8477425d802f4ac5 \ + --hash=sha256:2bf891c2d441b1edddfc62f16ab3031ac7506cba5b77680654179dbe93d8b7ec \ + --hash=sha256:2c88b0d356ea18ce026a52aa7c2110693db77fd52d5ac7553616635a7f165bbd \ + --hash=sha256:3090d9cabc2c31c92ae943f36d539a20adfd82697f50e996ce94f0b279e9e1e4 \ + --hash=sha256:3359a7c4eb4c281bf54bd760dcc03c43299f0d529dd2a5018be2f1fbebf0cbbd \ + --hash=sha256:375cca06f36f54339838c7f8251b64a777d5ff1277f8b91999487fad1a8e2d73 \ + --hash=sha256:386d8c4a5677fd94fd16c1af2adf39f59698af1e4ef0c3c026083f278540e352 \ + --hash=sha256:44365bb5098d0a91c44382e7df0ad44d5b9a588d29d9071aca4a2867f704aaf1 \ + --hash=sha256:443e502b428d325c57aec0947991857e8dc0589d0464181f35bd48f870cd5d18 \ + --hash=sha256:4550cbb6629c9f6186ab22cb497f262408e1a90ae10b8653599f2717f518f0df \ + --hash=sha256:4b8714e6eb37d973bdd231a46ce1c639417fe8035d4d3224f832e4a6afd05d5f \ + --hash=sha256:4cdcd8ae0b1017c5d7cac3595f1ac43ab9c0471cf700156d62d246248c734a35 \ + --hash=sha256:5ad583efb91402c0aada9cb4df0b6255607ceb83048dd2f629c09858594977ff \ + --hash=sha256:621c263f99274d51a9a1deecf301845f1408d497bdafed682db6155132a99cf4 \ + --hash=sha256:64860404663009a41a529f1f13c012e1ccf4a814b123581729f98c2b103ef362 \ + --hash=sha256:690d53df7b154d817d5b1dc5d24ba95045b670862005b8321f307f011a0738b2 \ + --hash=sha256:6dbea13f82de14b3db06cbc66e2060af4bf1070442398ddf42fa71ab188db627 \ + --hash=sha256:6f78d1ceac9545b87857c0e9b4e28a42fb98f8c6991d6b0e4099a012c35d2e73 \ + --hash=sha256:7068cae451be320c41b1442d5e2ec06dae050f1d3883918096f9cc3fcc791e89 \ + --hash=sha256:7afbd9bbe89866dbd9221f4c7e7321f4d0519772245d1b216b5ff1d50b8c0af7 \ + --hash=sha256:851981ded8bb42da66213cf95d1dd5eaf06c5d6b3a95725a18eddd58ec992b6b \ + --hash=sha256:852e538b4866ed776126349161d9fdc37387b1e15ab46805f98dcdee25fee4b5 \ + --hash=sha256:8b19d19661fc9a71f19b7c432c413ab65025e5dd11fbd192bd9169afb13b9ce0 \ + --hash=sha256:8e62d9c36bcfe6b85bec7b754accb97e2fa7b6a7c9a0c37f824d85eba699e7b8 \ + --hash=sha256:8eb0566b4501c27ea51f2e593531122703946969564fe526a5ff1b18be0ad49a \ + --hash=sha256:8f393607d54091e8976e297f8fd7399606d12cd8c2b8852353f07ad5ddac4224 \ + --hash=sha256:91df1fa70a99ac08dc449d6fda24a14992a1836d571928f217c40c66fd63fcc8 \ + --hash=sha256:98128abaa1bac8c4f60d08af641b981ba56486269532c03e99a1d48c9d6f9aa9 \ + --hash=sha256:98a7911f5ca3d6f809377fa8eaad8295687a106dd7bdd15624b267270d0da2ab \ + --hash=sha256:a07d4c466ad1b8c146dd7b893d2c2e735c3e530abfcef0c741a464001e828155 \ + --hash=sha256:a7c4aba79ef5fa41017d814016037f5ba29ecae889cea3d37108a4677ecb3aac \ + --hash=sha256:aa57de5929ada67d2f753442ce04dcc35561899558a1566f39f6e0c893cbc54b \ + --hash=sha256:ac3f7143da2d1f6087128d47d348ba0d92fe7f59ff476919f8d0f78fa5720f7b \ + --hash=sha256:ad18b3082a56119e0fb19ad4d47556ee24ce076466fff42b0d0a75a20d69a2e0 \ + --hash=sha256:b23462a349d4e7c6c77e8f6e735fb24dccdde14dd445c5eca76a9aaca7111f08 \ + --hash=sha256:b361152c24fd5c791220de9966b00696578c9884a2bb67e4759d4dfe05fd7049 \ + --hash=sha256:b51b5259dc344482ab21b768dfc7f54d51d9133665e72890831725068f69f24a \ + --hash=sha256:b57f367d7b0e1bc9e5c9b0ff34febdb3fe440f1fe8d75903ae71301bc06072c0 \ + --hash=sha256:b8dc65d2e7951b7e81c17b92857db7c19d6b3dd442d2d8600b5bd5040aa43ce6 \ + --hash=sha256:bc320e960347e5f9d27e8b4a9fc7044b2b26bfe4522bb4957e741fc1d1ebd2f0 \ + --hash=sha256:bdf77ba7a8987a239eb57cf840d62669741f8b92b61a617e63898caed31da898 \ + --hash=sha256:bf96b64400da2f411b43c4c81c2e20b09e3300d86766a52f42393696c8962f11 \ + --hash=sha256:c37f5dbfe95579549485c33167854784358f559feda703ccc058719ca0efd8aa \ + --hash=sha256:c41312c6edad5e875613719236f1ca6bba9310df40b1adc9308248e1bdb7a1ea \ + --hash=sha256:cafa1d43bcc69129a4855fd3973ab7983bb2274c407e5ff572af5dc196e00313 \ + --hash=sha256:cb98556e919be3e6ba9bca629dbddccfaa33b95a0fe7503052849b124e4f88cd \ + --hash=sha256:cd2e2c4dcc30c373e317d39b044afa6d9a090bec11d186f25841f70bc520bbb5 \ + --hash=sha256:e6c4fb1e7f51f8858f348ed9c31bb408b61274942d18b549ec163bb480c987a0 \ + --hash=sha256:eccab0281667caab0c055882b3bbb8e346bb0181e55f37477e3e5e3f5b7a96dd \ + --hash=sha256:f59f74c3779aa47223ba0a9e23ef10d2af5a873ed3624c78303f62a679d1b63e \ + --hash=sha256:f6b86112cca7bd04151ce248d781763ea5f74cc18d148476c6d16cee32db81ac \ + --hash=sha256:f837ab85ae93b118766b8b28a1cec47a1daddee303e1f986a595c56379062a5c # via happybase -tifffile==2025.10.4 \ - --hash=sha256:2e437c16ab211be5bcdc79f71b4907359115f1f83b5d919e7c297c29725d3e38 \ - --hash=sha256:7687d691e49026053181470cec70fa9250e3a586b2041041297e38b10bbd34e1 +tifffile==2026.3.3 \ + --hash=sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7 \ + --hash=sha256:e8be15c94273113d31ecb7aa3a39822189dd11c4967e3cc88c178f1ad2fd1170 # via scikit-image -timm==1.0.20 \ - --hash=sha256:7468d32a410c359181c1ef961f49c7e213286e0c342bfb898b99534a4221fc54 \ - --hash=sha256:f6e62f780358476691996c47aa49de87b95cc507edf923c3042f74a07e45b7fe - # via feast (setup.py) -tinycss2==1.4.0 \ - --hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \ - --hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 +timm==1.0.28 \ + --hash=sha256:3789d313fdd5541a327b60180d70dbb4bdec73db8ff0655e413db3c3d134a9a4 \ + --hash=sha256:e577b88da96b3a722ea5e2f042455ce6f715d398304d8e63b17d126ed7d89968 + # via feast (pyproject.toml) +tinycss2==1.5.1 \ + --hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \ + --hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957 # via bleach -tokenizers==0.22.1 \ - --hash=sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a \ - --hash=sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446 \ - --hash=sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7 \ - --hash=sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73 \ - --hash=sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a \ - --hash=sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9 \ - --hash=sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138 \ - --hash=sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc \ - --hash=sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390 \ - --hash=sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f \ - --hash=sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82 \ - --hash=sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879 \ - --hash=sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21 \ - --hash=sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4 \ - --hash=sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214 +tokenizers==0.22.2 \ + --hash=sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113 \ + --hash=sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37 \ + --hash=sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e \ + --hash=sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001 \ + --hash=sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee \ + --hash=sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7 \ + --hash=sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd \ + --hash=sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4 \ + --hash=sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012 \ + --hash=sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67 \ + --hash=sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a \ + --hash=sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5 \ + --hash=sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917 \ + --hash=sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c \ + --hash=sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195 \ + --hash=sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4 \ + --hash=sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a \ + --hash=sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc \ + --hash=sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92 \ + --hash=sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5 \ + --hash=sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48 \ + --hash=sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b \ + --hash=sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c \ + --hash=sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5 # via transformers toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via # coverage # fastapi-mcp -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via - # poetry-dynamic-versioning - # snowflake-connector-python -toolz==0.12.1 \ - --hash=sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85 \ - --hash=sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 + # via snowflake-connector-python +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # altair # dask # ibis-framework # partd -torch==2.8.0 \ - --hash=sha256:06fcee8000e5c62a9f3e52a688b9c5abb7c6228d0e56e3452983416025c41381 \ - --hash=sha256:0be92c08b44009d4131d1ff7a8060d10bafdb7ddcb7359ef8d8c5169007ea905 \ - --hash=sha256:1a62a1ec4b0498930e2543535cf70b1bef8c777713de7ceb84cd79115f553767 \ - --hash=sha256:220a06fd7af8b653c35d359dfe1aaf32f65aa85befa342629f716acb134b9710 \ - --hash=sha256:2b2f96814e0345f5a5aed9bf9734efa913678ed19caf6dc2cddb7930672d6128 \ - --hash=sha256:2f4ac52f0130275d7517b03a33d2493bab3693c83dcfadf4f81688ea82147d2e \ - --hash=sha256:5128fe752a355d9308e56af1ad28b15266fe2da5948660fad44de9e3a9e36e8c \ - --hash=sha256:5ae0524688fb6707c57a530c2325e13bb0090b745ba7b4a2cd6a3ce262572916 \ - --hash=sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0 \ - --hash=sha256:65616ca8ec6f43245e1f5f296603e33923f4c30f93d65e103d9e50c25b35150b \ - --hash=sha256:659df54119ae03e83a800addc125856effda88b016dfc54d9f65215c3975be16 \ - --hash=sha256:7b677e17f5a3e69fdef7eb3b9da72622f8d322692930297e4ccb52fefc6c8211 \ - --hash=sha256:83c13411a26fac3d101fe8035a6b0476ae606deb8688e904e796a3534c197def \ - --hash=sha256:89aa9ee820bb39d4d72b794345cccef106b574508dd17dbec457949678c76011 \ - --hash=sha256:8c7ef765e27551b2fbfc0f41bcf270e1292d9bf79f8e0724848b1682be6e80aa \ - --hash=sha256:8f0a9d617a66509ded240add3754e462430a6c1fc5589f86c17b433dd808f97a \ - --hash=sha256:a3f16a58a9a800f589b26d47ee15aca3acf065546137fc2af039876135f4c760 \ - --hash=sha256:a7242b86f42be98ac674b88a4988643b9bc6145437ec8f048fea23f72feb5eca \ - --hash=sha256:b2aca0939fb7e4d842561febbd4ffda67a8e958ff725c1c27e244e85e982173c \ - --hash=sha256:c12fa219f51a933d5f80eeb3a7a5d0cbe9168c0a14bbb4055f1979431660879b \ - --hash=sha256:da6afa31c13b669d4ba49d8a2169f0db2c3ec6bec4af898aa714f401d4c38904 \ - --hash=sha256:e2fab4153768d433f8ed9279c8133a114a034a61e77a3a104dcdf54388838705 \ - --hash=sha256:e8e5bf982e87e2b59d932769938b698858c64cc53753894be25629bdf5cf2f46 \ - --hash=sha256:e9f071f5b52a9f6970dc8a919694b27a91ae9dc08898b2b988abbef5eddfd1ae - # via - # feast (setup.py) +torch==2.13.0 \ + --hash=sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d \ + --hash=sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c \ + --hash=sha256:0ab4b69f3ee03a62a002cfbf77b1ca5e88aceb4ea64cb4388bb28f638ddbb045 \ + --hash=sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005 \ + --hash=sha256:2bd30b6b730d987fa386ce3898933762c5cb8cc82eb0535211d787cc3ce2dfeb \ + --hash=sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027 \ + --hash=sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc \ + --hash=sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09 \ + --hash=sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6 \ + --hash=sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2 \ + --hash=sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd \ + --hash=sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4 \ + --hash=sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7 \ + --hash=sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b \ + --hash=sha256:94f0de129916f77b8dc2c7a8eff644cfeddfe59e39c9f55e9f6e17543410281d \ + --hash=sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330 \ + --hash=sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c \ + --hash=sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e \ + --hash=sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8 \ + --hash=sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1 \ + --hash=sha256:c78b7b4d04461855a764cf01bae9a462bb88bc93defcfa11235cbc8fdf3e12c4 \ + --hash=sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92 \ + --hash=sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c \ + --hash=sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8 + # via + # feast (pyproject.toml) # accelerate # docling-ibm-models # easyocr # safetensors + # sentence-transformers # timm # torchvision -torchvision==0.23.0 \ - --hash=sha256:01dc33ee24c79148aee7cdbcf34ae8a3c9da1674a591e781577b716d233b1fa6 \ - --hash=sha256:07d069cb29691ff566e3b7f11f20d91044f079e1dbdc9d72e0655899a9b06938 \ - --hash=sha256:09bfde260e7963a15b80c9e442faa9f021c7e7f877ac0a36ca6561b367185013 \ - --hash=sha256:1c37e325e09a184b730c3ef51424f383ec5745378dc0eca244520aca29722600 \ - --hash=sha256:2a3299d2b1d5a7aed2d3b6ffb69c672ca8830671967eb1cee1497bacd82fe47b \ - --hash=sha256:2df618e1143805a7673aaf82cb5720dd9112d4e771983156aaf2ffff692eebf9 \ - --hash=sha256:2f7fd6c15f3697e80627b77934f77705f3bc0e98278b989b2655de01f6903e1d \ - --hash=sha256:31c583ba27426a3a04eca8c05450524105c1564db41be6632f7536ef405a6de2 \ - --hash=sha256:35c27941831b653f5101edfe62c03d196c13f32139310519e8228f35eae0e96a \ - --hash=sha256:3932bf67256f2d095ce90a9f826f6033694c818856f4bb26794cf2ce64253e53 \ - --hash=sha256:49aa20e21f0c2bd458c71d7b449776cbd5f16693dd5807195a820612b8a229b7 \ - --hash=sha256:4e7d31c43bc7cbecbb1a5652ac0106b436aa66e26437585fc2c4b2cf04d6014c \ - --hash=sha256:6c74cbc1cbee26dd4f35f989cd80dccc40411f258dee476b29871dee4b483af0 \ - --hash=sha256:6dd7c4d329a0e03157803031bc856220c6155ef08c26d4f5bbac938acecf0948 \ - --hash=sha256:7266871daca00ad46d1c073e55d972179d12a58fa5c9adec9a3db9bbed71284a \ - --hash=sha256:76bc4c0b63d5114aa81281390f8472a12a6a35ce9906e67ea6044e5af4cab60c \ - --hash=sha256:83ee5bf827d61a8af14620c0a61d8608558638ac9c3bac8adb7b27138e2147d1 \ - --hash=sha256:a2e45272abe7b8bf0d06c405e78521b5757be1bd0ed7e5cd78120f7fdd4cbf35 \ - --hash=sha256:a76fafe113b2977be3a21bf78f115438c1f88631d7a87203acb3dd6ae55889e6 \ - --hash=sha256:a9e9d7552d34547b80843eaf64ab0737b19b2e8bec2514286b8cfd30861ca8b5 \ - --hash=sha256:b190db205f90206c230fc2f91cbdfd5733334babc0e0d19bddb90a40b8cf26c2 \ - --hash=sha256:b9e2dabf0da9c8aa9ea241afb63a8f3e98489e706b22ac3f30416a1be377153b \ - --hash=sha256:dc7ce5accbbb8c9df9a79f8cef6a6df042f28e2250a6ae0d2ca70b06473fa03b \ - --hash=sha256:e0e2c04a91403e8dd3af9756c6a024a1d9c0ed9c0d592a8314ded8f4fe30d440 - # via - # feast (setup.py) +torchvision==0.28.0 \ + --hash=sha256:028a3d481b37d785605620d7cdad897064c5a55bae2aa1f2658766333e291940 \ + --hash=sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49 \ + --hash=sha256:2a1ef4b6f4bf5828b48cfad97372c8982db906830884b2868ba5c3df937a7d81 \ + --hash=sha256:3557cc7b539f46dabcda2b6f2b14017ccbeef024de466d4fc5835fc3f287f769 \ + --hash=sha256:36beb0782976906069ca03d4c9aacaf4b6b838b06ed6c20960ea9c51cce7acdd \ + --hash=sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b \ + --hash=sha256:46f581979c010ad6da6bd85ee602aa707e1ff44312670223b7a0ee517ad06d47 \ + --hash=sha256:546fd85345cf8652f6cd099d4f9884b0ca5c2f3fae78689a21dd2f35ea6b622f \ + --hash=sha256:5a38bc6da3d72621be003400b66f66a2b4c6d644fde05f680c2cb7ca8cf8dd6c \ + --hash=sha256:5cf78ebc401ce64ae19b8c55de866bb836797d559a4de9c25ccbe74cfa642d3a \ + --hash=sha256:62c7d110f86a039245b587e4fae60278c649f3bd42ff79cfbc1178eca4e72542 \ + --hash=sha256:6dfb0f45e2b4ceb4e76f158c3fbb5f44387099f3c466e3423a09ab665a194aba \ + --hash=sha256:7e80f543b22503d9415e126db5f0ff3917036925e38560ee6b9ae38c571a4002 \ + --hash=sha256:7e9dd6f60d6e15f8dc27d4f877fdb6002fc70d70272412135f1c2ff9cfa08d3b \ + --hash=sha256:7fad44dc9582570c7d92c4487d36ac46998f40cc39b438e8b8f5111a935ce4e8 \ + --hash=sha256:83fe6c020866a85acd7d97deccc45ff11d66daf42916d04396a4309c66c0ccb8 \ + --hash=sha256:87dc16b2df427c1318ad335f1e2be2b3b15b2cf20f7934c83b0505a48425ee5d \ + --hash=sha256:89f90e29b0966352811b12589f3a3c61943bf2bb9487b9d7bbec10efb1096bb5 \ + --hash=sha256:904cf89af220f8c6b2ed0296bb5065b474ce43b77558e48b2bf9de8b0ba17204 \ + --hash=sha256:9a45ea67235d965ef52187130d20002a4de20c54ea3d927a24286961d268dc37 \ + --hash=sha256:ad7b3a439265cc3739a4ab5b4c998c0e38ea99c0ee7ca4dea35c5d0b099ec237 \ + --hash=sha256:bb6dd6918460ed89cc7644adcc2402991474d6933cf1ce92b390641cb233fddf \ + --hash=sha256:d483b4aa3f5237569053f749cd1a2b5bb548ca456e40461a5dd087f21149d123 \ + --hash=sha256:e9f54c30cd52e3ef7fd034cc69b7bb7e0964e1c8f8743e018ab92e95b40f9eee + # via + # feast (pyproject.toml) # docling-ibm-models # easyocr # timm -tornado==6.5.2 \ - --hash=sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c \ - --hash=sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6 \ - --hash=sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef \ - --hash=sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4 \ - --hash=sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0 \ - --hash=sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e \ - --hash=sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882 \ - --hash=sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04 \ - --hash=sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0 \ - --hash=sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af \ - --hash=sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f \ - --hash=sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108 +tornado==6.5.7 \ + --hash=sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163 \ + --hash=sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2 \ + --hash=sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92 \ + --hash=sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b \ + --hash=sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972 \ + --hash=sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100 \ + --hash=sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4 \ + --hash=sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5 \ + --hash=sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4 \ + --hash=sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796 # via # ipykernel # jupyter-client @@ -5839,27 +6674,28 @@ tornado==6.5.2 \ # jupyterlab # notebook # terminado -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 # via - # feast (setup.py) + # feast (pyproject.toml) # datasets # docling # docling-ibm-models # great-expectations # huggingface-hub - # milvus-lite # mpire # semchunk + # sentence-transformers # transformers -traitlets==5.14.3 \ - --hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \ - --hash=sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f +traitlets==5.15.1 \ + --hash=sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92 \ + --hash=sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722 # via # ipykernel # ipython # ipywidgets + # jupyter-builder # jupyter-client # jupyter-core # jupyter-events @@ -5869,21 +6705,114 @@ traitlets==5.14.3 \ # nbclient # nbconvert # nbformat -transformers==4.57.0 \ - --hash=sha256:9d7c6d098c026e40d897e017ed1f481ab803cbac041021dbc6ae6100e4949b55 \ - --hash=sha256:d045753f3d93f9216e693cdb168698dfd2e9d3aad1bb72579a5d60ebf1545a8b +transformers==4.57.6 \ + --hash=sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550 \ + --hash=sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3 # via - # feast (setup.py) + # feast (pyproject.toml) # docling-core # docling-ibm-models -trino==0.336.0 \ - --hash=sha256:389150841446949119c3c2c13c1a51bb4be1a27818e40ae40dd3701f36c02550 \ - --hash=sha256:e82339e9fffe5c6c51de3bfdf28f083e3ae5945a4502739ab2094a0d08d68070 - # via feast (setup.py) -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) + # sentence-transformers +tree-sitter==0.26.0 \ + --hash=sha256:00289bfe7978f3e0dc0ce69813a20fa9f44ea4c100b3ec62043e5eb74ccfc3a2 \ + --hash=sha256:0f8793fd18ad7eec276ed4b51c097b4bf2002b357259b66b0d75db1f3f41c754 \ + --hash=sha256:10f0d4eb94aa7242dcb7f554bcd24dd7ba1c114f00d58759ba08c7a46c8ec51a \ + --hash=sha256:17a1c5cfd3a05d5c7c86bf4282b6ef8092c91dc0a98390499669c3fedb7d1814 \ + --hash=sha256:1d6fe0e8fb4df77b5ee816228e2c4475a63d8cc1d4d3a7ffd7097b2b87fc3e95 \ + --hash=sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90 \ + --hash=sha256:26c996c1edfee86e977bb3f5462e74fcec0d0b0db1e85a3c475875763caa03be \ + --hash=sha256:2f941cea06128c1f74f8937a8e2a90c7db49cf4be6647cd9e07d92a306d91517 \ + --hash=sha256:30a88be89ff1f2755297f81e8080d88b795dd98720c3f9fa2acf93873182cc95 \ + --hash=sha256:335294ce0504fcefde5245dff596778ffaf820205b98ae0b549c72e48855f1d8 \ + --hash=sha256:3f3c44339dd34fe8eb2b8d5aa7610660499a795f70376b130bbee7a437337280 \ + --hash=sha256:514a9bf8993e5210e7970736aaf6020d1759b670e195ef17b1c48f586aa30736 \ + --hash=sha256:526a165a2cb1d1f79e247d400f0e0acd8d49a817d6f312d543513af200b1f886 \ + --hash=sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa \ + --hash=sha256:5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4 \ + --hash=sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3 \ + --hash=sha256:6189c6c340c7384357711e3d92645e96bfb79f7a502f86de1ebdb23eb43f7dab \ + --hash=sha256:6cb2bd20efb2544c19ac54486ab7cb8ec7b36f913bbe1ce95df84acb96743d9c \ + --hash=sha256:7075ef857ef86f327dbb72d1e2574dda78db5754b3a1fca6506acd7fe5d561a7 \ + --hash=sha256:763627db05db34f12333081bd7422cc1c675893d373cc870b3e9249e200700e4 \ + --hash=sha256:7bcbadfa614326debef581957d5c780a9d7f66065c13deea61aa21d1dd36263f \ + --hash=sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52 \ + --hash=sha256:8ff2e0750b7daa722302838356d7b65e303829b7eb73c915df127ddba115e1d1 \ + --hash=sha256:918d89529786873f0982a0f59c2a303cd065fbfd1b903d71a8e4e1584f67b42e \ + --hash=sha256:93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f \ + --hash=sha256:94550e13b6ae576969da40246f4c4abb206380b5375ad43f26dd9151d55438e3 \ + --hash=sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c \ + --hash=sha256:b31a8195d2f224224c530ac814632d98c1dcc123d227442c07c736e86b70d564 \ + --hash=sha256:b40c219edccc4564530c96f8f1556f6202b37cda964d1cbd7bd2b7e68b40a245 \ + --hash=sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084 \ + --hash=sha256:bc6cb01d5ee75c85424aa1f1c72a82d8f07fd52539a0f3c4a6ed3e8721079b84 \ + --hash=sha256:c56581ad256c4195a21bfe449fed5d44a02fe83a4a7d6e70e6ec302c881191c7 \ + --hash=sha256:ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37 \ + --hash=sha256:dea4b4e27d49e9ec5b785d4f994da000e6726882fcc6ad05ec98478500c71aef \ + --hash=sha256:e9e46b664887d8c1014f1fb33e09454bbdd9ec1fe29b7fd02dde7b46bc1bb81a \ + --hash=sha256:ed0889dbed843ce45ede9f5169c0b2dea2222f12685844a03fadb81f12705867 \ + --hash=sha256:f289be0225ba2ace8e87d6c9639b2bc9ff2b5271afb7c5d39282a4a00e248682 \ + --hash=sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c \ + --hash=sha256:f9997ba61368c48ed54e715676afadf703947a1542464e39d047764fb3624b01 \ + --hash=sha256:ff527388df14cb5009f9274faf78cc69a7393ae6acf3b04784b8acca249519c5 \ + --hash=sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa + # via docling-core +tree-sitter-c==0.24.2 \ + --hash=sha256:1628584df0299b5a340aa63f8e67b6c97c91517f52fa7e7a4c557e40adb330a9 \ + --hash=sha256:4a2f4371cd816cc3153458f69062135ebb2ea5f275ddd90494e5c823d778204a \ + --hash=sha256:4d4579a8b54f0a442f903d88d3304cab77cd5c2031d4015baa4f2f8e15d6dcb7 \ + --hash=sha256:5041ef67eb68ce6bc8bb0b1f8ef3a5585ce523dae0c7eec109ab0627dd75aede \ + --hash=sha256:82842c5a5f2acd93f4de10038c33ac179c8979defc39376f990348d6289e933b \ + --hash=sha256:97bc80a224d48215d4e6e6376bf30d114f4c317b8145ff1b02afe785d4ba7bdd \ + --hash=sha256:abb549225091f7b25df2dd3a0143ece6e208f7055d8bcb4700b41ee79b9ef1e1 \ + --hash=sha256:c098bedcd5ac86ff93fa734d51d1dd86aed40fd5ed7d634c7af11380a0469969 \ + --hash=sha256:e2b42e8e22202c251f8629306f9321233542e07a6e01611b5fe83489272143eb + # via docling-core +tree-sitter-javascript==0.25.0 \ + --hash=sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b \ + --hash=sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54 \ + --hash=sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38 \ + --hash=sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b \ + --hash=sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1 \ + --hash=sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75 \ + --hash=sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc \ + --hash=sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc \ + --hash=sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c + # via docling-core +tree-sitter-python==0.25.0 \ + --hash=sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb \ + --hash=sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361 \ + --hash=sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762 \ + --hash=sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683 \ + --hash=sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5 \ + --hash=sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76 \ + --hash=sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac \ + --hash=sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b \ + --hash=sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d + # via docling-core +tree-sitter-typescript==0.23.2 \ + --hash=sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7 \ + --hash=sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478 \ + --hash=sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9 \ + --hash=sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31 \ + --hash=sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d \ + --hash=sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0 \ + --hash=sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8 \ + --hash=sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c + # via docling-core +trino==0.338.0 \ + --hash=sha256:093952f8a53b6f47ab357b77cbe92f1d66453290f7d8bcc4b74bd662e5ff2704 \ + --hash=sha256:1103d249cb96ba8f88f699588581965e2be088c16b9f8178e34cb64aa8a6c011 + # via feast (pyproject.toml) +truststore==0.10.4 \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 + # via + # httpcore2 + # httpx2 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) typer==0.12.5 \ --hash=sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b \ --hash=sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722 @@ -5891,65 +6820,65 @@ typer==0.12.5 \ # docling # docling-core # fastapi-mcp -types-cffi==1.17.0.20250915 \ - --hash=sha256:4362e20368f78dabd5c56bca8004752cc890e07a71605d9e0d9e069dbaac8c06 \ - --hash=sha256:cef4af1116c83359c11bb4269283c50f0688e9fc1d7f0eeb390f3661546da52c +types-cffi==2.0.0.20260518 \ + --hash=sha256:5b68a215a95d0eac4203b58e766ff7fe40c2e091b1fa1a9e54111f04cc560084 \ + --hash=sha256:f9707e66c13454789a58f8843d1ded4a66f1e9c8b10bd24d5eb5e0f25c0c5472 # via types-pyopenssl types-protobuf==3.19.22 \ --hash=sha256:d291388678af91bb045fafa864f142dc4ac22f5d4cdca097c7d8d8a32fa9b3ab \ --hash=sha256:d2b26861b0cb46a3c8669b0df507b7ef72e487da66d61f9f3576aa76ce028a83 # via - # feast (setup.py) + # feast (pyproject.toml) # mypy-protobuf -types-pymysql==1.1.0.20250916 \ - --hash=sha256:873eb9836bb5e3de4368cc7010ca72775f86e9692a5c7810f8c7f48da082e55b \ - --hash=sha256:98d75731795fcc06723a192786662bdfa760e1e00f22809c104fbb47bac5e29b - # via feast (setup.py) +types-pymysql==1.2.0.20260724 \ + --hash=sha256:71327a3fea0f680a280445f1b475c2307f68de05c2d3302a79df56af93c574cd \ + --hash=sha256:8493cf87e8df0a7b4e8c84305bc0aae69d3a8af9c6da7bfbb9a4c5c3f97d4a82 + # via feast (pyproject.toml) types-pyopenssl==24.1.0.20240722 \ --hash=sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39 \ --hash=sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54 # via types-redis -types-python-dateutil==2.9.0.20251008 \ - --hash=sha256:b9a5232c8921cf7661b29c163ccc56055c418ab2c6eabe8f917cbcc73a4c4157 \ - --hash=sha256:c3826289c170c93ebd8360c3485311187df740166dbab9dd3b792e69f2bc1f9c - # via - # feast (setup.py) - # arrow -types-pytz==2025.2.0.20250809 \ - --hash=sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5 \ - --hash=sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db - # via feast (setup.py) -types-pyyaml==6.0.12.20250915 \ - --hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \ - --hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6 - # via feast (setup.py) +types-python-dateutil==2.9.0.20260716 \ + --hash=sha256:1ae41d51a5c5f6bbeeb7f1f34df7086d55ff1605cf88d2ed71a7a276e1a7794e \ + --hash=sha256:1d55d1c3024bdb4861bb6a6622c9ec800c433d87bdc5b16fb84cdd0eed4ef2cb + # via feast (pyproject.toml) +types-pytz==2026.3.1.20260727 \ + --hash=sha256:42ac44e83645bfceb46597342c8fb8ec028a1407e2feae9c5c539986eab6b57a \ + --hash=sha256:4364075b6867dd15b210bb8c1d29727d609917129b45600defe1d4b3eda5ecb9 + # via feast (pyproject.toml) +types-pyyaml==6.0.12.20260724 \ + --hash=sha256:3c1ce1bb73cd5ec02e90390c2b1f00e810d241d8825fd73ff359696839271b6b \ + --hash=sha256:d57db930a4b2efbc57cf430ec8882765d246929432fa253092f383902329a453 + # via feast (pyproject.toml) types-redis==4.6.0.20241004 \ --hash=sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e \ --hash=sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed - # via feast (setup.py) + # via feast (pyproject.toml) types-requests==2.30.0.0 \ --hash=sha256:c6cf08e120ca9f0dc4fa4e32c3f953c3fba222bcc1db6b97695bce8da1ba9864 \ --hash=sha256:dec781054324a70ba64430ae9e62e7e9c8e4618c185a5cb3f87a6738251b5a31 - # via feast (setup.py) -types-setuptools==80.9.0.20250822 \ - --hash=sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965 \ - --hash=sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3 + # via feast (pyproject.toml) +types-setuptools==83.0.0.20260724 \ + --hash=sha256:eed1a91e1ed9d8ec9f3e71908e5bb17272e41e0852d37d1485ecd9cd33165e1a \ + --hash=sha256:fe2801176b667f1e8209f8571b0839ca6c34318d743825aeacf3f29970c8d46f # via - # feast (setup.py) + # feast (pyproject.toml) # types-cffi -types-tabulate==0.9.0.20241207 \ - --hash=sha256:ac1ac174750c0a385dfd248edc6279fa328aaf4ea317915ab879a2ec47833230 \ - --hash=sha256:b8dad1343c2a8ba5861c5441370c3e35908edd234ff036d4298708a1d4cf8a85 - # via feast (setup.py) +types-tabulate==0.10.0.20260508 \ + --hash=sha256:8e51f159e8b24976849706ae2ed1dc9adba8ebbd080b17e494ebb66a8cc92c74 \ + --hash=sha256:b1e1a2d0456fbd655a71690b09a7aaeffdf2978d32049184ea436492aa51d20a + # via feast (pyproject.toml) types-urllib3==1.26.25.14 \ --hash=sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f \ --hash=sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e # via types-requests -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiohttp # aiosignal + # alembic # anyio # azure-core # azure-identity @@ -5958,16 +6887,25 @@ typing-extensions==4.15.0 \ # docling-core # elasticsearch # fastapi + # graphene # great-expectations + # grpcio + # httpx2 # huggingface-hub # ibis-framework # ipython + # jupyter-client + # jupyterlab # jwcrypto + # mcp + # mcp-types # minio + # mlflow-skinny # mypy # opentelemetry-api # opentelemetry-sdk # opentelemetry-semantic-conventions + # oracledb # psycopg # psycopg-pool # pydantic @@ -5976,6 +6914,7 @@ typing-extensions==4.15.0 \ # python-docx # python-pptx # referencing + # sentence-transformers # snowflake-connector-python # sqlalchemy # starlette @@ -5987,296 +6926,233 @@ typing-extensions==4.15.0 \ typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via pydantic-settings -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via pandas -tzlocal==5.3.1 \ - --hash=sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd \ - --hash=sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via + # arrow + # ibis-framework + # pandas +tzlocal==5.4.4 \ + --hash=sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4 \ + --hash=sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15 # via # great-expectations # trino -ujson==5.11.0 \ - --hash=sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80 \ - --hash=sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef \ - --hash=sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a \ - --hash=sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840 \ - --hash=sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53 \ - --hash=sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076 \ - --hash=sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab \ - --hash=sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d \ - --hash=sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89 \ - --hash=sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c \ - --hash=sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb \ - --hash=sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c \ - --hash=sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823 \ - --hash=sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5 \ - --hash=sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac \ - --hash=sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d \ - --hash=sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723 \ - --hash=sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6 \ - --hash=sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0 \ - --hash=sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328 \ - --hash=sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0 \ - --hash=sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04 \ - --hash=sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25 \ - --hash=sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49 \ - --hash=sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec \ - --hash=sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3 \ - --hash=sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3 \ - --hash=sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc \ - --hash=sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a \ - --hash=sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9 \ - --hash=sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302 \ - --hash=sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694 \ - --hash=sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547 \ - --hash=sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01 \ - --hash=sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02 \ - --hash=sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6 \ - --hash=sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60 \ - --hash=sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915 \ - --hash=sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835 \ - --hash=sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701 \ - --hash=sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702 \ - --hash=sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50 \ - --hash=sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6 \ - --hash=sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc \ - --hash=sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a \ - --hash=sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c \ - --hash=sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c \ - --hash=sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03 \ - --hash=sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629 \ - --hash=sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105 \ - --hash=sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844 \ - --hash=sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241 \ - --hash=sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a \ - --hash=sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26 \ - --hash=sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac \ - --hash=sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596 \ - --hash=sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9 \ - --hash=sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752 \ - --hash=sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138 \ - --hash=sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b \ - --hash=sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba \ - --hash=sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5 \ - --hash=sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018 \ - --hash=sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362 \ - --hash=sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746 \ - --hash=sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f \ - --hash=sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88 \ - --hash=sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638 \ - --hash=sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b \ - --hash=sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9 \ - --hash=sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db \ - --hash=sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f \ - --hash=sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58 \ - --hash=sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b \ - --hash=sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433 \ - --hash=sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0 \ - --hash=sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764 \ - --hash=sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c \ - --hash=sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34 \ - --hash=sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052 \ - --hash=sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba \ - --hash=sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c \ - --hash=sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc \ - --hash=sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39 - # via pymilvus uri-template==1.3.0 \ --hash=sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7 \ --hash=sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363 # via jsonschema -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via - # feast (setup.py) + # feast (pyproject.toml) # botocore # clickhouse-connect + # databricks-sdk # docker # elastic-transport # great-expectations + # kube-authkit # kubernetes # minio # qdrant-client # requests # responses # testcontainers -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # mcp + # mlflow-skinny # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn virtualenv==20.23.0 \ --hash=sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e \ --hash=sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924 # via - # feast (setup.py) + # feast (pyproject.toml) # pre-commit # ray -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn -wcwidth==0.2.14 \ - --hash=sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605 \ - --hash=sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 +wcwidth==0.8.2 \ + --hash=sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda \ + --hash=sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85 # via prompt-toolkit -webcolors==24.11.1 \ - --hash=sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9 \ - --hash=sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6 +webcolors==25.10.0 \ + --hash=sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d \ + --hash=sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf # via jsonschema webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ @@ -6290,462 +7166,535 @@ websocket-client==1.9.0 \ # via # jupyter-server # kubernetes -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -werkzeug==3.1.3 \ - --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ - --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 - # via moto -wheel==0.45.1 \ - --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ - --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 +werkzeug==3.1.8 \ + --hash=sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50 \ + --hash=sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44 + # via + # flask + # moto +wheel==0.47.0 \ + --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \ + --hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3 # via # pip-tools # singlestoredb -widgetsnbextension==4.0.14 \ - --hash=sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575 \ - --hash=sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af +widgetsnbextension==4.0.15 \ + --hash=sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366 \ + --hash=sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9 # via ipywidgets -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 # via # aiobotocore + # deprecated # smart-open # testcontainers xlsxwriter==3.2.9 \ --hash=sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c \ --hash=sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3 # via python-pptx -xmltodict==1.0.2 \ - --hash=sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649 \ - --hash=sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d +xmltodict==1.0.4 \ + --hash=sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61 \ + --hash=sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a # via moto -xxhash==3.6.0 \ - --hash=sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad \ - --hash=sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c \ - --hash=sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3 \ - --hash=sha256:01be0c5b500c5362871fc9cfdf58c69b3e5c4f531a82229ddb9eb1eb14138004 \ - --hash=sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b \ - --hash=sha256:02ea4cb627c76f48cd9fb37cf7ab22bd51e57e1b519807234b473faebe526796 \ - --hash=sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f \ - --hash=sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c \ - --hash=sha256:0d50101e57aad86f4344ca9b32d091a2135a9d0a4396f19133426c88025b09f1 \ - --hash=sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1 \ - --hash=sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0 \ - --hash=sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec \ - --hash=sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d \ - --hash=sha256:18b242455eccdfcd1fa4134c431a30737d2b4f045770f8fe84356b3469d4b919 \ - --hash=sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67 \ - --hash=sha256:1fc1ed882d1e8df932a66e2999429ba6cc4d5172914c904ab193381fba825360 \ - --hash=sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799 \ - --hash=sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679 \ - --hash=sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef \ - --hash=sha256:2762bfff264c4e73c0e507274b40634ff465e025f0eaf050897e88ec8367575d \ - --hash=sha256:277175a73900ad43a8caeb8b99b9604f21fe8d7c842f2f9061a364a7e220ddb7 \ - --hash=sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8 \ - --hash=sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa \ - --hash=sha256:2ab89a6b80f22214b43d98693c30da66af910c04f9858dd39c8e570749593d7e \ - --hash=sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa \ - --hash=sha256:2f171a900d59d51511209f7476933c34a0c2c711078d3c80e74e0fe4f38680ec \ - --hash=sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4 \ - --hash=sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad \ - --hash=sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7 \ - --hash=sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5 \ - --hash=sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11 \ - --hash=sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae \ - --hash=sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d \ - --hash=sha256:44e342e8cc11b4e79dae5c57f2fb6360c3c20cc57d32049af8f567f5b4bcb5f4 \ - --hash=sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6 \ - --hash=sha256:45aae0c9df92e7fa46fbb738737324a563c727990755ec1965a6a339ea10a1df \ - --hash=sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058 \ - --hash=sha256:4903530e866b7a9c1eadfd3fa2fbe1b97d3aed4739a80abf506eb9318561c850 \ - --hash=sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2 \ - --hash=sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d \ - --hash=sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89 \ - --hash=sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e \ - --hash=sha256:4da8168ae52c01ac64c511d6f4a709479da8b7a4a1d7621ed51652f93747dffa \ - --hash=sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6 \ - --hash=sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb \ - --hash=sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3 \ - --hash=sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b \ - --hash=sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4 \ - --hash=sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db \ - --hash=sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119 \ - --hash=sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec \ - --hash=sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518 \ - --hash=sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296 \ - --hash=sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033 \ - --hash=sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729 \ - --hash=sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca \ - --hash=sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063 \ - --hash=sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5 \ - --hash=sha256:6551880383f0e6971dc23e512c9ccc986147ce7bfa1cd2e4b520b876c53e9f3d \ - --hash=sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f \ - --hash=sha256:6965e0e90f1f0e6cb78da568c13d4a348eeb7f40acfd6d43690a666a459458b8 \ - --hash=sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42 \ - --hash=sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e \ - --hash=sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392 \ - --hash=sha256:780b90c313348f030b811efc37b0fa1431163cb8db8064cf88a7936b6ce5f222 \ - --hash=sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f \ - --hash=sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd \ - --hash=sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77 \ - --hash=sha256:7c35c4cdc65f2a29f34425c446f2f5cdcd0e3c34158931e1cc927ece925ab802 \ - --hash=sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d \ - --hash=sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1 \ - --hash=sha256:7dac94fad14a3d1c92affb661021e1d5cbcf3876be5f5b4d90730775ccb7ac41 \ - --hash=sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374 \ - --hash=sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263 \ - --hash=sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71 \ - --hash=sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13 \ - --hash=sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8 \ - --hash=sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc \ - --hash=sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62 \ - --hash=sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11 \ - --hash=sha256:9085e798c163ce310d91f8aa6b325dda3c2944c93c6ce1edb314030d4167cc65 \ - --hash=sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0 \ - --hash=sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b \ - --hash=sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2 \ - --hash=sha256:97460eec202017f719e839a0d3551fbc0b2fcc9c6c6ffaa5af85bbd5de432788 \ - --hash=sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6 \ - --hash=sha256:9e040d3e762f84500961791fa3709ffa4784d4dcd7690afc655c095e02fff05f \ - --hash=sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc \ - --hash=sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e \ - --hash=sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702 \ - --hash=sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405 \ - --hash=sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f \ - --hash=sha256:a75ffc1bd5def584129774c158e108e5d768e10b75813f2b32650bb041066ed6 \ - --hash=sha256:a87f271a33fad0e5bf3be282be55d78df3a45ae457950deb5241998790326f87 \ - --hash=sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3 \ - --hash=sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a \ - --hash=sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b \ - --hash=sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b \ - --hash=sha256:b0359391c3dad6de872fefb0cf5b69d55b0655c55ee78b1bb7a568979b2ce96b \ - --hash=sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8 \ - --hash=sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db \ - --hash=sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99 \ - --hash=sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a \ - --hash=sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2 \ - --hash=sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204 \ - --hash=sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b \ - --hash=sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546 \ - --hash=sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95 \ - --hash=sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9 \ - --hash=sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54 \ - --hash=sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06 \ - --hash=sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c \ - --hash=sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152 \ - --hash=sha256:c2f9ccd5c4be370939a2e17602fbc49995299203da72a3429db013d44d590e86 \ - --hash=sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4 \ - --hash=sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93 \ - --hash=sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd \ - --hash=sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd \ - --hash=sha256:cc604dc06027dbeb8281aeac5899c35fcfe7c77b25212833709f0bff4ce74d2a \ - --hash=sha256:cfbc5b91397c8c2972fdac13fb3e4ed2f7f8ccac85cd2c644887557780a9b6e2 \ - --hash=sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248 \ - --hash=sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd \ - --hash=sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6 \ - --hash=sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf \ - --hash=sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7 \ - --hash=sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490 \ - --hash=sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0 \ - --hash=sha256:e4ff728a2894e7f436b9e94c667b0f426b9c74b71f900cf37d5468c6b5da0536 \ - --hash=sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb \ - --hash=sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829 \ - --hash=sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746 \ - --hash=sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07 \ - --hash=sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292 \ - --hash=sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6 \ - --hash=sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd \ - --hash=sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7 \ - --hash=sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d \ - --hash=sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0 \ - --hash=sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee \ - --hash=sha256:ffc578717a347baf25be8397cb10d2528802d24f94cfc005c0e44fef44b5cdd6 +xxhash==3.8.1 \ + --hash=sha256:000435984a0469b0f822fe76f35bddea0f96a4d6521b3339a60a6428cdee1edc \ + --hash=sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3 \ + --hash=sha256:01cab782f8a0a05ecad2c63d7ef10f7ab475f660e0d6419d069418c14d88de7c \ + --hash=sha256:0204701e6d01f64254e0e5ff4255812b1febe027ddd7dda63372e27f98b5e91f \ + --hash=sha256:027dee4355f3fcc41481650d846cf6cfc895c85a1ab7acd063063821a0df5b4c \ + --hash=sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec \ + --hash=sha256:0418ec8b2331b9d4d575fc9284427e8e69449d7172e99e1a86fcdd1f51a0a937 \ + --hash=sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92 \ + --hash=sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a \ + --hash=sha256:09a204dd4bb0823daf938cdd0dc8057d5f1e14fe3cbde929424255f23f9de872 \ + --hash=sha256:0dfdf19b0d5433a75d61f19dc85737af0f0b95e445c1ad69c855115d05efed45 \ + --hash=sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3 \ + --hash=sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31 \ + --hash=sha256:1153265daa10750a9bf8e9b01753d7618024a300925591efaf16b1b7fa536699 \ + --hash=sha256:115772daeb71b2f3b9381177017f53e6cf3f3439c840737fdabd21aba6e54920 \ + --hash=sha256:12a3cf79dadbab9631230ebc4c51c7c60f1e9cdfb890c15fb733eaafe2e7713c \ + --hash=sha256:12eaeaa9ab8b9e6033a1fa5f6b338aaf55ff4df4bee11b59fd6ee03b19186ee4 \ + --hash=sha256:131324f719957b988861714de7d6ddf57b47abec3b0cc691302ffeaba0e05e10 \ + --hash=sha256:15790b686f8723b845fec6f612a343beb815a25c83117a7fa408d7c8ee5aa8fd \ + --hash=sha256:1731407102b9332cd3c9dadee07db498bc3d437b95d752b5b1a5f7eb730a3738 \ + --hash=sha256:1b86ae798a976ccbc1d02af6ccb98f5b4d24756b1f65e995f11d10fe071f486f \ + --hash=sha256:1c332dd48b8cb050da2bb2a3c96d72b1664168650a250ef9718e423df7989e05 \ + --hash=sha256:1da930bbcac3e8fbe2191850e2abb57977a99348c12c4b385e1058ac1b0a9ecc \ + --hash=sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329 \ + --hash=sha256:1ffcc98d8878e449e86dec008cea6f44cfd3a954d2ef24ae7d1cc9f725beec7d \ + --hash=sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215 \ + --hash=sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724 \ + --hash=sha256:23e710118a5778a45db740b431943a3f2a82a571a052c2768cce6544d9c8c62e \ + --hash=sha256:264710bd335016f303763ce1275c6486df30bb57c2245c91b224c983d7ac39b8 \ + --hash=sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62 \ + --hash=sha256:27a9e475157f7315826118e3f3127909a0fe25f1b43d3d3be9c584f9d265f937 \ + --hash=sha256:27cfc2f1ed76f956f36dfe0c56e5f5a3e94cd91eb78b893f63e2ef2ae404fcdf \ + --hash=sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d \ + --hash=sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75 \ + --hash=sha256:2f1c68394818e0595569c2ff3cbc1e6d5a36a434e796f5c526b987b80c8a8c62 \ + --hash=sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8 \ + --hash=sha256:314d05fbc55719ae2438eaaba77bf2508ca4f030b26fa4c9c8c380e81c48fa33 \ + --hash=sha256:32a94ad2763e0263d9102037d349002c3d3c401e42770542c3eeb4801f311661 \ + --hash=sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0 \ + --hash=sha256:345b07b78e2bf583d71682aa34ae5b5fab575f7a1cb31e10263ebbc6f89f8c42 \ + --hash=sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893 \ + --hash=sha256:358650d5bda9c635da699c53adf4e8134af492ecc79c960f917eebf088bb6799 \ + --hash=sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887 \ + --hash=sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629 \ + --hash=sha256:38c887aedb696ef8bca19983206d270848558cfae4a91afa6a2fb05dde58ffc5 \ + --hash=sha256:39c9d5b61508b0bb68f29e54546de0ed2a74943c6a18585535a7e37356f1dd12 \ + --hash=sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf \ + --hash=sha256:3bc0fa90830df1e1277f33cc6e55de9990b83c0319fd8c7412866cfde38b025e \ + --hash=sha256:3c0d84c5f2e086b120bae4e7f551cbda804c1deb10d958478bed4f89ba286dfe \ + --hash=sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913 \ + --hash=sha256:402db908ea70eaf9800d9182a66596fc86f36655df8f63fdecf7c11da741d86f \ + --hash=sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e \ + --hash=sha256:4482380b462ca9e59994d072a877ecadd1cf51102daeeab2db696f96ab763723 \ + --hash=sha256:44c89d915a75c11d2547eaee9098fcd80398987c4bff2974a0497a925bf92c07 \ + --hash=sha256:454d78e786602278a2a4383d08048482052f4f0c61fa677ca590af08914d9bca \ + --hash=sha256:460261045936975193bfd20549a0de1cd52a33b405cbb972f0d80940c42266cd \ + --hash=sha256:46b39976d008e2a845758650f0ff7136bca004f40da0c8798bd37ac37860154f \ + --hash=sha256:498017fbf2d13a768b3110d084bde39f2bd8664c1de0b8084f8ccc84425b7c88 \ + --hash=sha256:49aa8692507835dcc1e8ad8021f20c74c2dc13d83b5112e87877faa2a0035b20 \ + --hash=sha256:4b512261801b1e5fde7b6ebf2fef7977339c620cbbca88a0040ad9ad134f4d02 \ + --hash=sha256:4bec8b2c909bcfae9a0dc702346007e02a8c9ba5bbde83ffb224aa194f4f9efc \ + --hash=sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478 \ + --hash=sha256:4d6e88ddb3c741fbf29e1e7faf429880f8cd1d7aff4303247435a549726b4fb1 \ + --hash=sha256:4df57c0b161ec1b3ed0526a67b0db0914b557e86ee8aae51887aec941b261542 \ + --hash=sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa \ + --hash=sha256:4ef09bbc2519a93cd0f95f2ceb5f7b85919dffea643278e02362bf40e3c4bed1 \ + --hash=sha256:5013be3bea7612852c62a7437f3302c1cfb91ca7e703b194459db0b2b2e0d792 \ + --hash=sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed \ + --hash=sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647 \ + --hash=sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55 \ + --hash=sha256:53f3ed9118397074ff63a79b66b7fec1c84c782eecde35c5bc94e420a971c231 \ + --hash=sha256:559e3cabe522231909f9de98ef06929edbd53782046bd21aae0c72db6f2a0775 \ + --hash=sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398 \ + --hash=sha256:57f80a898544db78ec6b0be6183bd1bc008933193d4199f5cde36b0e6bd5e062 \ + --hash=sha256:58346024d47e84f7d8b3e7f5d6faa1d58acbbe49a8771497872059f58c1d8ea5 \ + --hash=sha256:594131ce1aad18db3689781f806db1b065cdaa04f4df36b4c038d2013aefd0bf \ + --hash=sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22 \ + --hash=sha256:5c566b123dce7e4867ca518434cdfb9f84e5023771235b2e3107a26c9a41cbd8 \ + --hash=sha256:5d3dfb1f0ff146da7952867a9414f0c7a29762f8825a84879592612fd6139342 \ + --hash=sha256:5da703225374e3a4c8d4fd90e26fe7213a52004ec77f88b42b42e9e86d8c6d57 \ + --hash=sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104 \ + --hash=sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb \ + --hash=sha256:602efcad4a42c184e81d43a2b7e6e4f524d619878f2b6ee2ba469011f47c8147 \ + --hash=sha256:614bca2c7cfa87ec95b703e691c3c5eb6c448b6dabbe9776ac53883152951729 \ + --hash=sha256:632a34590c090d1285ed5efa5a02be919f3f9a56a64bd25f693fe1e2d27a27fb \ + --hash=sha256:64af54dd1c3a45a27c04942f9a1a4683322bdd127f4745cca4e02549c1d2d2bb \ + --hash=sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170 \ + --hash=sha256:656256c9f9303e47f07d5cb8ae4468285370adfafd7ba48aea33a458e7697626 \ + --hash=sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65 \ + --hash=sha256:6c7574528bc922f8757f34dd78ed60ab52b1c7973b630f5eae7ba33ec133ce71 \ + --hash=sha256:6cee733fe4ccb1737e0997135283c82341e5cfa9cf214b165f9087fb663aaf4f \ + --hash=sha256:6cf633fe83b1d4e6519d7259b33afe40fbba5d3f438730156971dd0cf7730610 \ + --hash=sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113 \ + --hash=sha256:717b12fdc51819833704e85e6926d76981ffa3f780ef92e33ebb8b26d46bb230 \ + --hash=sha256:7258ee276e8772599bc19e14b36f6260306e21b637190cd7cb489a2449d48684 \ + --hash=sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629 \ + --hash=sha256:7345007c12780985de4fd740148776d1eee18c0d41407c6fa1e48c5450304fe5 \ + --hash=sha256:747476436f6891b9773374ce8d48edcc8b12cb5b61b67c6fb6289633747d088f \ + --hash=sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9 \ + --hash=sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276 \ + --hash=sha256:78c794b643d214f1522e7a288bcf5a2de120d26cd170516749a4009dc92722c9 \ + --hash=sha256:7b5f97ecfede10d5b2870383620e2d25c8561e217c7bf9081073802b54248d2b \ + --hash=sha256:7dc4bdf008f77c88d544849c48c1a40faf25a5eff6cc466de2e8edc37c191fce \ + --hash=sha256:81f4ed9ca9644bc95cd976bfe10f7a4cafab8ffdc3aed52877d4600e445be7ef \ + --hash=sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc \ + --hash=sha256:8304be0982130954b7fd3aad18e2c6f8ee40254bc3d2e635991c16d77c91e2bd \ + --hash=sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061 \ + --hash=sha256:836f11d4474d3228e9909d97216faa4f7505df41cfaf3927eb29809de785a78d \ + --hash=sha256:83b9130b80b216d56fdf9e87131946b353c9627930c061955a101ea82b09fed9 \ + --hash=sha256:83d879362ddd0fedd3f2ab8ce7cce3da2049a6d51d16da8af73011c6edf4752f \ + --hash=sha256:848182a391fffdc25605443e832f5b443f25498edeccf9a64343fd84421ca04b \ + --hash=sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc \ + --hash=sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56 \ + --hash=sha256:89b11a5cdd441aa463f6d34ca0241602bc09b001a76994b6059828494108c673 \ + --hash=sha256:89df64c10adfe340fb00330042537cdd6bf0d8d78bad73f29cfe5427eed7b084 \ + --hash=sha256:8ea8a141eeced4f6262ab6dd71c681ac546a558c30bb586abe087d814b5f85ea \ + --hash=sha256:8f454166c2ffed45636c8d501741e649851ba2f346c4eb73a64c07ac00428f20 \ + --hash=sha256:8f759eed402448c2bdbb492e4fba1f20668ffe29688605ea61f0f67f9e4e386d \ + --hash=sha256:9043877a917be88ccf230aa5667c1bd059bce80f4c2727e4defa1b29b7f48b08 \ + --hash=sha256:942bc86e9be6fdd6e1175048f5fe8f8fdaaf2309dd1323ef1e155a69cd346780 \ + --hash=sha256:947a585bcaa235702b7c59433b485489397f9a163b3f56058b9463a46fd9b74c \ + --hash=sha256:950ac754d16daea42038f38e7465eb84cda4d08d7343c1c915771b29470f065a \ + --hash=sha256:98d8ac1129b4dd39098cffed94d1284aceb61c3aa396757ccc736ac392e4cee5 \ + --hash=sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437 \ + --hash=sha256:9b2ce44bf8f4a1d01f418b3110ff8dff32fd3f3e836c0e06333c3725f243fa6c \ + --hash=sha256:9d45eee3a95a8b61e5b568580caac91f1502ddb731aaf8f4aa448a98660b2fb4 \ + --hash=sha256:9db455cb649dcfe4504d6d68a6d83a7315a99a3ca59871dc3ff840671f99adba \ + --hash=sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396 \ + --hash=sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0 \ + --hash=sha256:9f23083e1bd9d901f844af7a126727c486e7eada9a1a6791c8f7e73f94fac656 \ + --hash=sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907 \ + --hash=sha256:a5cd96f6dcdf4fa657b2d95668d71d58455248f98712ecffaa9c528edf40ccae \ + --hash=sha256:a5eed9d41995a83f3332b4e3396abb7f433cac584222bd7e305b606d8353861e \ + --hash=sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a \ + --hash=sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda \ + --hash=sha256:a98b2f95cab589e0f5e92c48431afb4d56238b8bf6668edcc66166180e9b509b \ + --hash=sha256:ad52a0e4bcc0ba956a953a169d1feec2734a64981d689e4fc8f490f7bf91af60 \ + --hash=sha256:af0c9fedc4a2c24e8664953882fe8185f3790b8338c9c700f76f5ad660817711 \ + --hash=sha256:afe6380a0e9653a87aa1e6e88fb47718113e5563c7a1cb2bcc23c1d8e17e3961 \ + --hash=sha256:affb37f152e55b5e4494bb9d0107f7bb08515c6704fbed82d9f61214d74adc17 \ + --hash=sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf \ + --hash=sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46 \ + --hash=sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2 \ + --hash=sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6 \ + --hash=sha256:b3e1107fe5ca030f946dfa59fdbb66b5df121c8432f14b0bdd282d17b297f4eb \ + --hash=sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a \ + --hash=sha256:b6fa3116e40e14e7782fb1a9f872f94b5997de21127c95545ce40196ac1351c5 \ + --hash=sha256:bb70573d2995d23932e2871120f78d798ebc3572e54c09e694a18ced95c5f8d9 \ + --hash=sha256:bbcdf9c92d21c65bc75426eecea724c8fa0d35a6e201fdf1630011d4cc3aa685 \ + --hash=sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315 \ + --hash=sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e \ + --hash=sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc \ + --hash=sha256:c240939e963653054fc7e4a17c382829cda4aa88a7daf0af841715dbded1b497 \ + --hash=sha256:c31a2649bcf1fe97cf11c79848d761df33ac46b3896942d31b640557b486ff6b \ + --hash=sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e \ + --hash=sha256:c73b6f652f0745425aa6378319c331293b5341756262e9408ed3d45f183375e6 \ + --hash=sha256:c85949d02c85adf6d786eb94858e124989a632a4e65739835b2fc5761827fac3 \ + --hash=sha256:c919f38cd3f0b5e8d30b81fd6cac688cf9221560340f0c35cbbb8b2bd77ad6ac \ + --hash=sha256:c959f88160b13b4e730b0d75b459b7929fc0d2225c284c9683ac95d6feeeac6a \ + --hash=sha256:cb3fe820c27593f170770d6c8d791936cf6275d9269405fbb7b30a55363c10c8 \ + --hash=sha256:cf399fac542a1c7a4734a435b93df2c55e858c7d31abf6c1bdf46f9ae67fbfd0 \ + --hash=sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068 \ + --hash=sha256:d247b34bf433c92b41689318fd25d246313cab2275a6a47e2efac178b80d6efe \ + --hash=sha256:d48acabb1e5cb0071009f80d71d7f01b6ba2c1d4b869b1352bb5df3f11bf7dfd \ + --hash=sha256:d5006c65ec507a333479e76e00e2c368781f16c24ededa764763956b32a0e93e \ + --hash=sha256:d58ce8b6cfa9c4d2f230557f69caf7c06369e318015d0b19485095bc2c5963ab \ + --hash=sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838 \ + --hash=sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297 \ + --hash=sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c \ + --hash=sha256:db77278a6eddadbf44ce5aae2fee5ebb4d061f026b1ce2130d058cd4d7a7b670 \ + --hash=sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975 \ + --hash=sha256:e14800b9b10bb39d7a60ad4a310e403164d7b8988a27ae933d4e40618a44088e \ + --hash=sha256:e2a845687219ba3214126f14a8a5861f97c9e065a7d0b8252adb6df13eea86fb \ + --hash=sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0 \ + --hash=sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1 \ + --hash=sha256:e605e0b8abca9457abd5bee737e086ab145a20c25083ef1113013612268872ff \ + --hash=sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc \ + --hash=sha256:e710ad822c493fb80a4fbc1e3d0a807b1422cb90adbe64378f98291b7fa48fef \ + --hash=sha256:ea6a3e734b0fd41b82784a400be946821900daebe610c050a5e0760838a34f99 \ + --hash=sha256:ec55d80e9b8a519d742669e0b49e8ce9e6747be42bf3c138158b6543a9c8e489 \ + --hash=sha256:ed8bcdab6692fd4ad0dd6241807a24a640a376764460023b8d462d745e6b7b27 \ + --hash=sha256:f377012b86c0a23a1df0cf5a1b05aa7187649e472f71c7892e5f2c2815bbe74f \ + --hash=sha256:f6114692261eff4266386cdec0f7d87eee24e317ab397c218b7ae6a76b4c6339 \ + --hash=sha256:f8044cf4c77f37968b8c4cbcbf7a0f355d8a437877ae18eba23e3aad953a6cc7 \ + --hash=sha256:f8ed8940435834141061da26d27c4dd0d18fb69777bf431f5c6cc46b43349113 \ + --hash=sha256:f93e408255ddce525189bf11feaa1be7ee35e55f486c299c97d9caa68d724a5b \ + --hash=sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1 # via datasets -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8 \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf \ - --hash=sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890 \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737 \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba \ - --hash=sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9 \ - --hash=sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0 \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147 \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e \ - --hash=sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2 \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6 \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750 \ + --hash=sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621 \ + --hash=sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13 \ + --hash=sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1 \ + --hash=sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2 \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata zstandard==0.25.0 \ --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \ @@ -6848,5 +7797,5 @@ zstandard==0.25.0 \ --hash=sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551 \ --hash=sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01 # via - # clickhouse-connect + # pyiceberg # trino diff --git a/sdk/python/requirements/py3.11-minimal-requirements.txt b/sdk/python/requirements/py3.11-minimal-requirements.txt index 08412591cea..dc313f87fdc 100644 --- a/sdk/python/requirements/py3.11-minimal-requirements.txt +++ b/sdk/python/requirements/py3.11-minimal-requirements.txt @@ -1,152 +1,160 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.11 --no-strip-extras setup.py --extra minimal --generate-hashes --output-file sdk/python/requirements/py3.11-minimal-requirements.txt -aiobotocore==2.23.1 \ - --hash=sha256:a59f2a78629b97d52f10936b79c73de64e481a8c44a62c1871f088df6c1afc4f \ - --hash=sha256:d81c54d2eae2406ea9a473fea518fed580cf37bc4fc51ce43ba81546e5305114 - # via feast (setup.py) -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +# uv pip compile -p 3.11 --no-strip-extras pyproject.toml --extra minimal --generate-hashes --output-file sdk/python/requirements/py3.11-minimal-requirements.txt +aiobotocore==3.8.0 \ + --hash=sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670 \ + --hash=sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + # via feast (pyproject.toml) +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.0 \ - --hash=sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6 \ - --hash=sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a \ - --hash=sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e \ - --hash=sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971 \ - --hash=sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54 \ - --hash=sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2 \ - --hash=sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96 \ - --hash=sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469 \ - --hash=sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26 \ - --hash=sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0 \ - --hash=sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29 \ - --hash=sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885 \ - --hash=sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579 \ - --hash=sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a \ - --hash=sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1 \ - --hash=sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b \ - --hash=sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852 \ - --hash=sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865 \ - --hash=sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad \ - --hash=sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092 \ - --hash=sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7 \ - --hash=sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b \ - --hash=sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8 \ - --hash=sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820 \ - --hash=sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd \ - --hash=sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee \ - --hash=sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4 \ - --hash=sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67 \ - --hash=sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d \ - --hash=sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1 \ - --hash=sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d \ - --hash=sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114 \ - --hash=sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1 \ - --hash=sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755 \ - --hash=sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04 \ - --hash=sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca \ - --hash=sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1 \ - --hash=sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae \ - --hash=sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793 \ - --hash=sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e \ - --hash=sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a \ - --hash=sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8 \ - --hash=sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602 \ - --hash=sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778 \ - --hash=sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f \ - --hash=sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49 \ - --hash=sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77 \ - --hash=sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7 \ - --hash=sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f \ - --hash=sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0 \ - --hash=sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519 \ - --hash=sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d \ - --hash=sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370 \ - --hash=sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7 \ - --hash=sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247 \ - --hash=sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d \ - --hash=sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80 \ - --hash=sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9 \ - --hash=sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c \ - --hash=sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d \ - --hash=sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020 \ - --hash=sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504 \ - --hash=sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca \ - --hash=sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731 \ - --hash=sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd \ - --hash=sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6 \ - --hash=sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21 \ - --hash=sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1 \ - --hash=sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620 \ - --hash=sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64 \ - --hash=sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8 \ - --hash=sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3 \ - --hash=sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5 \ - --hash=sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985 \ - --hash=sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813 \ - --hash=sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed \ - --hash=sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20 \ - --hash=sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03 \ - --hash=sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702 \ - --hash=sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6 \ - --hash=sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed \ - --hash=sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353 \ - --hash=sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108 \ - --hash=sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f \ - --hash=sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409 \ - --hash=sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7 \ - --hash=sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3 \ - --hash=sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9 \ - --hash=sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba \ - --hash=sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe \ - --hash=sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c \ - --hash=sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861 \ - --hash=sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a \ - --hash=sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0 \ - --hash=sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390 \ - --hash=sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3 \ - --hash=sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90 \ - --hash=sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802 \ - --hash=sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163 \ - --hash=sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d \ - --hash=sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430 \ - --hash=sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a \ - --hash=sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b \ - --hash=sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6 \ - --hash=sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b \ - --hash=sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657 \ - --hash=sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c \ - --hash=sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93 \ - --hash=sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f \ - --hash=sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82 \ - --hash=sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31 \ - --hash=sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc \ - --hash=sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52 \ - --hash=sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c \ - --hash=sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a \ - --hash=sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52 \ - --hash=sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7 \ - --hash=sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b \ - --hash=sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147 \ - --hash=sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4 - # via aiobotocore -aioitertools==0.12.0 \ - --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \ - --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 + # via + # aiobotocore + # kubernetes +aioitertools==0.13.0 \ + --hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \ + --hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c # via aiobotocore aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 # via aiohttp -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via + # fastapi + # typer +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # httpx + # httpx2 # mcp # sse-starlette # starlette @@ -155,404 +163,438 @@ asn1crypto==1.5.1 \ --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 # via snowflake-connector-python +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy async-timeout==5.0.1 \ --hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3 # via redis -atpublic==5.1 \ - --hash=sha256:135783dbd887fbddb6ef032d104da70c124f2b44b9e2d79df07b9da5334825e3 \ - --hash=sha256:abc1f4b3dbdd841cc3539e4b5e4f3ad41d658359de704e30cb36da4d4e9d3022 +atpublic==7.0.0 \ + --hash=sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e \ + --hash=sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b # via ibis-framework -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via + # feast (pyproject.toml) # aiohttp # jsonschema # referencing -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -boto3==1.38.27 \ - --hash=sha256:94bd7fdd92d5701b362d4df100d21e28f8307a67ff56b6a8b0398119cf22f859 \ - --hash=sha256:95f5fe688795303a8a15e8b7e7f255cadab35eae459d00cc281a4fd77252ea80 +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +boto3==1.43.46 \ + --hash=sha256:66c0d943b049a46a492ec4ec2ebe73c930b1842c7137bee83aad6d93e95d4d96 \ + --hash=sha256:69453e2c1bcb9fd9806527ab99950cacfc2826cb0dce9a3a0414d19270c06c3c # via - # feast (setup.py) + # feast (pyproject.toml) # snowflake-connector-python -botocore==1.38.46 \ - --hash=sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e \ - --hash=sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b +botocore==1.43.46 \ + --hash=sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533 \ + --hash=sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 # via # aiobotocore # boto3 # s3transfer # snowflake-connector-python -cachetools==6.2.1 \ - --hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \ - --hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201 - # via google-auth -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +cachetools==7.1.6 \ + --hash=sha256:2c12e255780330af28b91bb7fb96cce4c766f04e38396b9a24510190a5827096 \ + --hash=sha256:c7a79e7f30ba9943c1cefd08cc36f006aaae086e017af9166f1d59d6170c47e1 + # via pymilvus +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # httpcore # httpx # kubernetes # requests # snowflake-connector-python -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f # via - # feast (setup.py) + # feast (pyproject.toml) # cryptography - # snowflake-connector-python -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via # requests # snowflake-connector-python -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask - # typer # uvicorn -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a # via dask colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via feast (setup.py) -cryptography==46.0.0 \ - --hash=sha256:07a1be54f995ce14740bf8bbe1cc35f7a37760f992f73cf9f98a2a60b9b97419 \ - --hash=sha256:0f58183453032727a65e6605240e7a3824fd1d6a7e75d2b537e280286ab79a52 \ - --hash=sha256:16b5ac72a965ec9d1e34d9417dbce235d45fa04dac28634384e3ce40dfc66495 \ - --hash=sha256:1b4fba84166d906a22027f0d958e42f3a4dbbb19c28ea71f0fb7812380b04e3c \ - --hash=sha256:1d2073313324226fd846e6b5fc340ed02d43fd7478f584741bd6b791c33c9fee \ - --hash=sha256:249c41f2bbfa026615e7bdca47e4a66135baa81b08509ab240a2e666f6af5966 \ - --hash=sha256:274f8b2eb3616709f437326185eb563eb4e5813d01ebe2029b61bfe7d9995fbb \ - --hash=sha256:2fc30be952dd4334801d345d134c9ef0e9ccbaa8c3e1bc18925cbc4247b3e29c \ - --hash=sha256:32670ca085150ff36b438c17f2dfc54146fe4a074ebf0a76d72fb1b419a974bc \ - --hash=sha256:35aa1a44bd3e0efc3ef09cf924b3a0e2a57eda84074556f4506af2d294076685 \ - --hash=sha256:3738f50215211cee1974193a1809348d33893696ce119968932ea117bcbc9b1d \ - --hash=sha256:378eff89b040cbce6169528f130ee75dceeb97eef396a801daec03b696434f06 \ - --hash=sha256:399ef4c9be67f3902e5ca1d80e64b04498f8b56c19e1bc8d0825050ea5290410 \ - --hash=sha256:40ee4ce3c34acaa5bc347615ec452c74ae8ff7db973a98c97c62293120f668c6 \ - --hash=sha256:4bc257c2d5d865ed37d0bd7c500baa71f939a7952c424f28632298d80ccd5ec1 \ - --hash=sha256:4f70cbade61a16f5e238c4b0eb4e258d177a2fcb59aa0aae1236594f7b0ae338 \ - --hash=sha256:523153480d7575a169933f083eb47b1edd5fef45d87b026737de74ffeb300f69 \ - --hash=sha256:6460866a92143a24e3ed68eaeb6e98d0cedd85d7d9a8ab1fc293ec91850b1b38 \ - --hash=sha256:65e9117ebed5b16b28154ed36b164c20021f3a480e9cbb4b4a2a59b95e74c25d \ - --hash=sha256:6c39fd5cd9b7526afa69d64b5e5645a06e1b904f342584b3885254400b63f1b3 \ - --hash=sha256:6d8945bc120dcd90ae39aa841afddaeafc5f2e832809dc54fb906e3db829dfdc \ - --hash=sha256:75d2ddde8f1766ab2db48ed7f2aa3797aeb491ea8dfe9b4c074201aec00f5c16 \ - --hash=sha256:77e3bd53c9c189cea361bc18ceb173959f8b2dd8f8d984ae118e9ac641410252 \ - --hash=sha256:7f3f88df0c9b248dcc2e76124f9140621aca187ccc396b87bc363f890acf3a30 \ - --hash=sha256:80a548a5862d6912a45557a101092cd6c64ae1475b82cef50ee305d14a75f598 \ - --hash=sha256:834af45296083d892e23430e3b11df77e2ac5c042caede1da29c9bf59016f4d2 \ - --hash=sha256:83af84ebe7b6e9b6de05050c79f8cc0173c864ce747b53abce6a11e940efdc0d \ - --hash=sha256:88c09da8a94ac27798f6b62de6968ac78bb94805b5d272dbcfd5fdc8c566999f \ - --hash=sha256:8e8b222eb54e3e7d3743a7c2b1f7fa7df7a9add790307bb34327c88ec85fe087 \ - --hash=sha256:91585fc9e696abd7b3e48a463a20dda1a5c0eeeca4ba60fa4205a79527694390 \ - --hash=sha256:99f64a6d15f19f3afd78720ad2978f6d8d4c68cd4eb600fab82ab1a7c2071dca \ - --hash=sha256:9aa85222f03fdb30defabc7a9e1e3d4ec76eb74ea9fe1504b2800844f9c98440 \ - --hash=sha256:ab3a14cecc741c8c03ad0ad46dfbf18de25218551931a23bca2731d46c706d83 \ - --hash=sha256:b8e7db4ce0b7297e88f3d02e6ee9a39382e0efaf1e8974ad353120a2b5a57ef7 \ - --hash=sha256:bbaa5eef3c19c66613317dc61e211b48d5f550db009c45e1c28b59d5a9b7812a \ - --hash=sha256:be7479f9504bfb46628544ec7cb4637fe6af8b70445d4455fbb9c395ad9b7290 \ - --hash=sha256:bf1961037309ee0bdf874ccba9820b1c2f720c2016895c44d8eb2316226c1ad5 \ - --hash=sha256:c1f6ccd6f2eef3b2eb52837f0463e853501e45a916b3fc42e5d93cf244a4b97b \ - --hash=sha256:c3648d6a5878fd1c9a22b1d43fa75efc069d5f54de12df95c638ae7ba88701d0 \ - --hash=sha256:c39f0947d50f74b1b3523cec3931315072646286fb462995eb998f8136779319 \ - --hash=sha256:c3cd09b1490c1509bf3892bde9cef729795fae4a2fee0621f19be3321beca7e4 \ - --hash=sha256:c457ad3f151d5fb380be99425b286167b358f76d97ad18b188b68097193ed95a \ - --hash=sha256:c9c4121f9a41cc3d02164541d986f59be31548ad355a5c96ac50703003c50fb7 \ - --hash=sha256:d14eaf1569d6252280516bedaffdd65267428cdbc3a8c2d6de63753cf0863d5e \ - --hash=sha256:d1eccae15d5c28c74b2bea228775c63ac5b6c36eedb574e002440c0bc28750d3 \ - --hash=sha256:d349af4d76a93562f1dce4d983a4a34d01cb22b48635b0d2a0b8372cdb4a8136 \ - --hash=sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0 \ - --hash=sha256:da7f93551d39d462263b6b5c9056c49f780b9200bf9fc2656d7c88c7bdb9b363 \ - --hash=sha256:df932ac70388be034b2e046e34d636245d5eeb8140db24a6b4c2268cd2073270 \ - --hash=sha256:f09a3a108223e319168b7557810596631a8cb864657b0c16ed7a6017f0be9433 \ - --hash=sha256:f85e6a7d42ad60024fa1347b1d4ef82c4df517a4deb7f829d301f1a92ded038c \ - --hash=sha256:f9aaf2a91302e1490c068d2f3af7df4137ac2b36600f5bd26e53d9ec320412d3 \ - --hash=sha256:f9f85d9cf88e3ba2b2b6da3c2310d1cf75bdf04a5bc1a2e972603054f82c4dd5 \ - --hash=sha256:fe9ff1139b2b1f59a5a0b538bbd950f8660a39624bbe10cf3640d17574f973bb + # via feast (pyproject.toml) +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b # via + # google-auth + # pyjwt # pyopenssl # snowflake-connector-python -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) -db-dtypes==1.4.3 \ - --hash=sha256:a1c92b819af947fae1701d80a71f2a0eac08f825ca52cf0c68aeba80577ae966 \ - --hash=sha256:d3cb08c32939d24e05501f17694be8c1c54cfb4620d61fb56fb311e8c3d8885e +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) +db-dtypes==1.7.1 \ + --hash=sha256:450b94cd010cfa6d4d13cca2593009eba4fbe15516084c332cde1a47bbce0625 \ + --hash=sha256:75adf3bdf3174a6474ea4162d9a345ea0b3102ec4fd603e69ec26b6e8a99145a # via # google-cloud-bigquery # pandas-gbq dill==0.3.9 \ --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c - # via feast (setup.py) -duckdb==1.1.3 \ - --hash=sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586 \ - --hash=sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd \ - --hash=sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e \ - --hash=sha256:09c68522c30fc38fc972b8a75e9201616b96ae6da3444585f14cf0d116008c95 \ - --hash=sha256:0a55169d2d2e2e88077d91d4875104b58de45eff6a17a59c7dc41562c73df4be \ - --hash=sha256:0ba6baa0af33ded836b388b09433a69b8bec00263247f6bf0a05c65c897108d3 \ - --hash=sha256:183ac743f21c6a4d6adfd02b69013d5fd78e5e2cd2b4db023bc8a95457d4bc5d \ - --hash=sha256:1aa3abec8e8995a03ff1a904b0e66282d19919f562dd0a1de02f23169eeec461 \ - --hash=sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce \ - --hash=sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24 \ - --hash=sha256:2141c6b28162199999075d6031b5d63efeb97c1e68fb3d797279d31c65676269 \ - --hash=sha256:252d9b17d354beb9057098d4e5d5698e091a4f4a0d38157daeea5fc0ec161670 \ - --hash=sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9 \ - --hash=sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3 \ - --hash=sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289 \ - --hash=sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1 \ - --hash=sha256:4f0e2e5a6f5a53b79aee20856c027046fba1d73ada6178ed8467f53c3877d5e0 \ - --hash=sha256:51c6d79e05b4a0933672b1cacd6338f882158f45ef9903aef350c4427d9fc898 \ - --hash=sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8 \ - --hash=sha256:5ace6e4b1873afdd38bd6cc8fcf90310fb2d454f29c39a61d0c0cf1a24ad6c8d \ - --hash=sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff \ - --hash=sha256:6411e21a2128d478efbd023f2bdff12464d146f92bc3e9c49247240448ace5a6 \ - --hash=sha256:647f17bd126170d96a38a9a6f25fca47ebb0261e5e44881e3782989033c94686 \ - --hash=sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c \ - --hash=sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31 \ - --hash=sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7 \ - --hash=sha256:80158f4c7c7ada46245837d5b6869a336bbaa28436fbb0537663fa324a2750cd \ - --hash=sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898 \ - --hash=sha256:8ee97ec337794c162c0638dda3b4a30a483d0587deda22d45e1909036ff0b739 \ - --hash=sha256:911d58c22645bfca4a5a049ff53a0afd1537bc18fedb13bc440b2e5af3c46148 \ - --hash=sha256:9c619e4849837c8c83666f2cd5c6c031300cd2601e9564b47aa5de458ff6e69d \ - --hash=sha256:9d0767ada9f06faa5afcf63eb7ba1befaccfbcfdac5ff86f0168c673dd1f47aa \ - --hash=sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4 \ - --hash=sha256:a1f83c7217c188b7ab42e6a0963f42070d9aed114f6200e3c923c8899c090f16 \ - --hash=sha256:a1fa0c502f257fa9caca60b8b1478ec0f3295f34bb2efdc10776fc731b8a6c5f \ - --hash=sha256:a30dd599b8090ea6eafdfb5a9f1b872d78bac318b6914ada2d35c7974d643640 \ - --hash=sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38 \ - --hash=sha256:a4748635875fc3c19a7320a6ae7410f9295557450c0ebab6d6712de12640929a \ - --hash=sha256:b74e121ab65dbec5290f33ca92301e3a4e81797966c8d9feef6efdf05fc6dafd \ - --hash=sha256:c443d3d502335e69fc1e35295fcfd1108f72cb984af54c536adfd7875e79cee5 \ - --hash=sha256:c5336939d83837af52731e02b6a78a446794078590aa71fd400eb17f083dda3e \ - --hash=sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378 \ - --hash=sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9 \ - --hash=sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee \ - --hash=sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32 \ - --hash=sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537 \ - --hash=sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989 \ - --hash=sha256:ecb1dc9062c1cc4d2d88a5e5cd8cc72af7818ab5a3c0f796ef0ffd60cfd3efb4 \ - --hash=sha256:eeacb598120040e9591f5a4edecad7080853aa8ac27e62d280f151f8c862afa3 \ - --hash=sha256:f549af9f7416573ee48db1cf8c9d27aeed245cb015f4b4f975289418c6cf7320 \ - --hash=sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3 \ - --hash=sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b + # via feast (pyproject.toml) +duckdb==1.5.5 \ + --hash=sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053 \ + --hash=sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4 \ + --hash=sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c \ + --hash=sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a \ + --hash=sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528 \ + --hash=sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c \ + --hash=sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282 \ + --hash=sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f \ + --hash=sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7 \ + --hash=sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f \ + --hash=sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c \ + --hash=sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb \ + --hash=sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433 \ + --hash=sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8 \ + --hash=sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28 \ + --hash=sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238 \ + --hash=sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece \ + --hash=sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1 \ + --hash=sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151 \ + --hash=sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2 \ + --hash=sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353 \ + --hash=sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc \ + --hash=sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079 \ + --hash=sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a \ + --hash=sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6 \ + --hash=sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe \ + --hash=sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de \ + --hash=sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0 \ + --hash=sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e \ + --hash=sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f \ + --hash=sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf \ + --hash=sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae \ + --hash=sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6 \ + --hash=sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe \ + --hash=sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366 # via ibis-framework durationpy==0.10 \ --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 # via kubernetes -environs==9.5.0 \ - --hash=sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124 \ - --hash=sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9 - # via pymilvus -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 +faiss-cpu==1.14.3 \ + --hash=sha256:0a5eb27184123c7ac1060c6b862978eabf0e30c1369ccf8bdb1497d35c06ad3d \ + --hash=sha256:1d734cfa9ac90b6a5dfed3a27cb706d05f22824703dafc3969b4e2071877a31c \ + --hash=sha256:20414d5c98fb6ab9eedfb3df79841865911c2d67b6efe829dc46adfeab6f51b1 \ + --hash=sha256:54b0ea832be5bce75ef9abf1297fe4613d760bf7cfe7a963da040cb0314e8f58 \ + --hash=sha256:8780b526c06e57aad90a8c4655dfba2ff1b3195bd600ff4499752fc45159c9fc \ + --hash=sha256:a9369863290a3f0e033757e4c10577b6ef7431f1cede394dabd0a137e4e2ed45 \ + --hash=sha256:b28ba083e8c02f2c9be03783402537fd3f00d27e68799c44bf88931500ee12ec \ + --hash=sha256:cdcb90850cb4b7c27d270839b37bcc0dc8d1fb6a62d1e13053e51ac95061ca25 \ + --hash=sha256:ea2340f675db59af8db6da4535b541ce6948d68a008989c656c292c7b0c77127 \ + --hash=sha256:f278003d00c30c90cf118b98428bb96f73eba4b9dadac6993c788966d2e983bd \ + --hash=sha256:f9d0e84d909194f63f027bbd3c1e35e905e48c9345c2db6e6f24da09a6bc5906 + # via milvus-lite +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp fastapi-mcp==0.4.0 \ --hash=sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec \ --hash=sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d - # via feast (setup.py) -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 + # via feast (pyproject.toml) +filelock==3.32.0 \ + --hash=sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402 \ + --hash=sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3 # via snowflake-connector-python frozenlist==1.8.0 \ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ @@ -688,17 +730,17 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -fsspec==2024.9.0 \ - --hash=sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8 \ - --hash=sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b +fsspec==2026.7.0 \ + --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \ + --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88 # via - # feast (setup.py) + # feast (pyproject.toml) # dask -google-api-core[grpc]==2.26.0 \ - --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ - --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 +google-api-core[grpc]==2.33.0 \ + --hash=sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb \ + --hash=sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc # via - # feast (setup.py) + # feast (pyproject.toml) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -706,9 +748,9 @@ google-api-core[grpc]==2.26.0 \ # google-cloud-datastore # google-cloud-storage # pandas-gbq -google-auth==2.41.1 \ - --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ - --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 +google-auth[pyopenssl]==2.56.2 \ + --hash=sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6 \ + --hash=sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051 # via # google-api-core # google-auth-oauthlib @@ -718,395 +760,515 @@ google-auth==2.41.1 \ # google-cloud-core # google-cloud-datastore # google-cloud-storage - # kubernetes # pandas-gbq # pydata-google-auth -google-auth-oauthlib==1.2.2 \ - --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ - --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-auth-oauthlib==1.4.0 \ + --hash=sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2 \ + --hash=sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 # via # pandas-gbq # pydata-google-auth -google-cloud-bigquery[pandas]==3.38.0 \ - --hash=sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520 \ - --hash=sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6 +google-cloud-bigquery[pandas]==3.42.2 \ + --hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \ + --hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq -google-cloud-bigquery-storage==2.33.1 \ - --hash=sha256:24952aba0d69acc4d6bfbdc7a09dddbb728496b1780bd224f1056361a1b51044 \ - --hash=sha256:3fd25bef364ac5fb9bbd6560f0dd11b90b1845883df8e0a8c706ad53d00fc23b - # via feast (setup.py) -google-cloud-bigtable==2.33.0 \ - --hash=sha256:8d25c73dd41cea9436d14c9f03b91f94b05ff4afd47ce9a575bf18f8e6985747 \ - --hash=sha256:cd4f7178cde30113900318ec7e2fd793116752c97e0922c185c69b645a52656d - # via feast (setup.py) -google-cloud-core==2.4.3 \ - --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ - --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e +google-cloud-bigquery-storage==2.39.0 \ + --hash=sha256:8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 \ + --hash=sha256:d5afd90ad06cf24d9167316cca70ab5b344e880fc13031d7392aa78ee76b8bb6 + # via feast (pyproject.toml) +google-cloud-bigtable==2.41.0 \ + --hash=sha256:36da0fc730952bfe48ae91dee11015459c9046fa1499d0efb73a61bbb6edbe9b \ + --hash=sha256:d0c424bae6238eb16ffc0922be79da00d20d23e0750ea46742ffd4542c50fd26 + # via feast (pyproject.toml) +google-cloud-core==2.6.0 \ + --hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \ + --hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.21.0 \ - --hash=sha256:eee454dd4a55f5b327f9f344928ff1a09a6f77c23d5e3d908ad31a13cc2f4073 \ - --hash=sha256:f303f27cd1983383f20bd227019cd8a7897419e0ec6b878367c58c66245f9d9b - # via feast (setup.py) +google-cloud-datastore==2.26.0 \ + --hash=sha256:1ac4882f033fcb90bd75d1171b46400ecd5def23768598843d9c041e0c0b4a6a \ + --hash=sha256:1bde5694308468da5c932153a6b0331d5b54e1c21de366b6a75ddb0000fd5f4b + # via feast (pyproject.toml) google-cloud-storage==2.19.0 \ --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via feast (setup.py) -google-crc32c==1.7.1 \ - --hash=sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db \ - --hash=sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337 \ - --hash=sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c \ - --hash=sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242 \ - --hash=sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e \ - --hash=sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472 \ - --hash=sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194 \ - --hash=sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3 \ - --hash=sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582 \ - --hash=sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d \ - --hash=sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6 \ - --hash=sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82 \ - --hash=sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06 \ - --hash=sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349 \ - --hash=sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a \ - --hash=sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d \ - --hash=sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48 \ - --hash=sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb \ - --hash=sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315 \ - --hash=sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589 \ - --hash=sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76 \ - --hash=sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65 \ - --hash=sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6 \ - --hash=sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127 \ - --hash=sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53 \ - --hash=sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603 \ - --hash=sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35 \ - --hash=sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9 \ - --hash=sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638 \ - --hash=sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9 \ - --hash=sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77 \ - --hash=sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14 \ - --hash=sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b \ - --hash=sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb + # via feast (pyproject.toml) +google-crc32c==1.8.0 \ + --hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \ + --hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \ + --hash=sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff \ + --hash=sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 \ + --hash=sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411 \ + --hash=sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a \ + --hash=sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 \ + --hash=sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb \ + --hash=sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa \ + --hash=sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962 \ + --hash=sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215 \ + --hash=sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b \ + --hash=sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27 \ + --hash=sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113 \ + --hash=sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f \ + --hash=sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f \ + --hash=sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d \ + --hash=sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 \ + --hash=sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092 \ + --hash=sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7 \ + --hash=sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2 \ + --hash=sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93 \ + --hash=sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8 \ + --hash=sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21 \ + --hash=sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 \ + --hash=sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2 \ + --hash=sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc \ + --hash=sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454 \ + --hash=sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2 \ + --hash=sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733 \ + --hash=sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697 \ + --hash=sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651 \ + --hash=sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c # via # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 +google-resumable-media==2.10.0 \ + --hash=sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c \ + --hash=sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.70.0 \ - --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ - --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +googleapis-common-protos[grpc]==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # grpc-google-iam-v1 # grpcio-status -grpc-google-iam-v1==0.14.2 \ - --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ - --hash=sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20 +grpc-google-iam-v1==0.14.4 \ + --hash=sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038 \ + --hash=sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 # via google-cloud-bigtable -grpcio==1.62.3 \ - --hash=sha256:059444f0ed5dba73ab7dd0ee7e8e6b606df4130d2b0a9f010f84da4ab9f6c2d8 \ - --hash=sha256:114f2a865886ff33f85d70670e971fe0e3d252a1209656fefa5470286e3fcc76 \ - --hash=sha256:13571a5b868dcc308a55d36669a2d17d9dcd6ec8335213f6c49cc68da7305abe \ - --hash=sha256:1ac0944e9e3ee3e20825226d1e17985e9f88487055c475986cf0922a7d806d8a \ - --hash=sha256:1de3d04d9a4ec31ebe848ae1fe61e4cbc367fb9495cbf6c54368e60609a998d9 \ - --hash=sha256:216740723fc5971429550c374a0c039723b9d4dcaf7ba05227b7e0a500b06417 \ - --hash=sha256:25cd75dc73c5269932413e517be778640402f18cf9a81147e68645bd8af18ab0 \ - --hash=sha256:325c56ce94d738c31059cf91376f625d3effdff8f85c96660a5fd6395d5a707f \ - --hash=sha256:3737e5ef0aa0fcdfeaf3b4ecc1a6be78b494549b28aec4b7f61b5dc357f7d8be \ - --hash=sha256:377babc817e8b4186aed7ed56e832867c513e4e9b6c3503565c344ffdef440d4 \ - --hash=sha256:3fb7d966a976d762a31346353a19fce4afcffbeda3027dd563bc8cb521fcf799 \ - --hash=sha256:43670a25b752b7ed960fcec3db50ae5886dc0df897269b3f5119cde9b731745f \ - --hash=sha256:4439bbd759636e37b66841117a66444b454937e27f0125205d2d117d7827c643 \ - --hash=sha256:454a6aed4ebd56198d37e1f3be6f1c70838e33dd62d1e2cea12f2bcb08efecc5 \ - --hash=sha256:4c9c1502c76cadbf2e145061b63af077b08d5677afcef91970d6db87b30e2f8b \ - --hash=sha256:4dab8b64c438e19c763a6332b55e5efdbecfb7c55ae59a42c38c81ed27955fa5 \ - --hash=sha256:56757d3e4cf5d4b98a30f2c5456151607261c891fa2298a4554848dcbf83083d \ - --hash=sha256:57823dc7299c4f258ae9c32fd327d29f729d359c34d7612b36e48ed45b3ab8d0 \ - --hash=sha256:582bd03e9c3d1bd1162eb51fa0f1a35633d66e73f4f36702d3b8484a8b45eda7 \ - --hash=sha256:620165df24aae3d5b3e84cb8dd6b98f6ed49aed04126186bbf43061e301d6a21 \ - --hash=sha256:646c14e9f3356d3f34a65b58b0f8d08daa741ba1d4fcd4966b79407543332154 \ - --hash=sha256:668211f3699bbee4deaf1d6e6b8df59328bf63f077bf2dc9b8bfa4a17df4a279 \ - --hash=sha256:6be243f3954b0ca709f56f9cae926c84ac96e1cce19844711e647a1f1db88b99 \ - --hash=sha256:6da20a1ae010a988bc4ed47850f1122de0a88e18cd2f901fcf56007be1fc6c30 \ - --hash=sha256:7349cd7445ac65fbe1b744dcab9cc1ec02dae2256941a2e67895926cbf7422b4 \ - --hash=sha256:74f3fc9b93290e58264844f5bc46df4c58a94c4287a277dbcf75344fc6c37ca4 \ - --hash=sha256:75a4e9ac7ff185cad529f35934c5d711b88aca48b90c70e195f5657da50ce321 \ - --hash=sha256:7b33c1807d4ac564a3027d06f21a2220c116ceacaaef614deb96b3341ee58896 \ - --hash=sha256:807176971c504c598976f5a9ea62363cffbbbb6c7509d9808c2342b020880fa2 \ - --hash=sha256:80a82fdee14dc27e9299248b7aabd5a8739a1cf6b76c78aa2b848158b44a99d5 \ - --hash=sha256:81b7c121c4e52a0749bf0759185b8d5cfa48a786cd7d411cdab08269813e0aab \ - --hash=sha256:8257cc9e55fb0e2149a652d9dc14c023720f9e73c9145776e07c97e0a553922e \ - --hash=sha256:8a5f00b2508937952d23a1767739e95bbbe1120f8a66d10187d5e971d56bb55c \ - --hash=sha256:8ae2e7a390b2cdd2a95d3bf3b3385245eeb48a5e853943cb46139666462c2d1a \ - --hash=sha256:940459d81685549afdfe13a6de102c52ea4cdda093477baa53056884aadf7c48 \ - --hash=sha256:9c4aae4e683776c319169d87e7891b67b75e3f1c0beeb877902ea148b0585164 \ - --hash=sha256:9d5f8e0050a179b3bce9189b522dc91008d44f08c757a7c310e0fd06b4d3d147 \ - --hash=sha256:a1b85d35a7d9638c03321dfe466645b87e23c30df1266f9e04bbb5f44e7579a9 \ - --hash=sha256:a82410d7620c07cb32624e38f2a106980564dfef9dbe78f5b295cda9ef217c03 \ - --hash=sha256:abfe64811177e681edc81d9d9d1bd23edc5f599bd9846650864769264ace30cd \ - --hash=sha256:ac9783d5679c8da612465168c820fd0b916e70ec5496c840bddba0be7f2d124c \ - --hash=sha256:b033d50bd41e506e3b579775f54a30c16c222e0d88847ac8098d2eca2a7454cc \ - --hash=sha256:b097347441b86a8c3ad9579abaf5e5f7f82b1d74a898f47360433b2bca0e4536 \ - --hash=sha256:b708401ede2c4cb8943e8a713988fcfe6cbea105b07cd7fa7c8a9f137c22bddb \ - --hash=sha256:bd900e666bb68fff49703084be14407cd73b8a5752a7590cea98ec22de24fb5d \ - --hash=sha256:c118cfc80e2402a5595be36e9245ffd9b0e146f426cc40bdf60015bf183f8373 \ - --hash=sha256:c175b252d063af388523a397dbe8edbc4319761f5ee892a8a0f5890acc067362 \ - --hash=sha256:c8bb1a7aa82af6c7713cdf9dcb8f4ea1024ac7ce82bb0a0a82a49aea5237da34 \ - --hash=sha256:c95a0b76a44c548e6bd8c5f7dbecf89c77e2e16d3965be817b57769c4a30bea2 \ - --hash=sha256:e202e3f963480ca067a261179b1ac610c0f0272cb4a7942d11b7e2b3fc99c3aa \ - --hash=sha256:e9ffdb7bc9ccd56ec201aec3eab3432e1e820335b5a16ad2b37e094218dcd7a6 \ - --hash=sha256:ea7ca66a58421411c6486fa5015fe7704e2816ff0b4ec4fb779ad5e1cbbdabf3 \ - --hash=sha256:f2ff8ac447765e173842b554b31307b98b3bb1852710903ebb936e7efb7df6e5 \ - --hash=sha256:f5def814c5a4c90c8fe389c526ab881f4a28b7e239b23ed8e02dd02934dfaa1a +grpcio==1.83.0 \ + --hash=sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df \ + --hash=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 \ + --hash=sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5 \ + --hash=sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df \ + --hash=sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930 \ + --hash=sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf \ + --hash=sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9 \ + --hash=sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33 \ + --hash=sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da \ + --hash=sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03 \ + --hash=sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa \ + --hash=sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223 \ + --hash=sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9 \ + --hash=sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4 \ + --hash=sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf \ + --hash=sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881 \ + --hash=sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af \ + --hash=sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5 \ + --hash=sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0 \ + --hash=sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727 \ + --hash=sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb \ + --hash=sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a \ + --hash=sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735 \ + --hash=sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16 \ + --hash=sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49 \ + --hash=sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1 \ + --hash=sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24 \ + --hash=sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b \ + --hash=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f \ + --hash=sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57 \ + --hash=sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf \ + --hash=sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f \ + --hash=sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c \ + --hash=sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969 \ + --hash=sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd \ + --hash=sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c \ + --hash=sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b \ + --hash=sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61 \ + --hash=sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404 \ + --hash=sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617 \ + --hash=sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40 \ + --hash=sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45 \ + --hash=sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc \ + --hash=sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c \ + --hash=sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745 \ + --hash=sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45 \ + --hash=sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c \ + --hash=sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0 \ + --hash=sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d \ + --hash=sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9 \ + --hash=sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8 # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-datastore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking # grpcio-reflection # grpcio-status + # milvus-lite # pymilvus -grpcio-health-checking==1.62.3 \ - --hash=sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93 \ - --hash=sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d - # via feast (setup.py) -grpcio-reflection==1.62.3 \ - --hash=sha256:a48ef37df81a3bada78261fc92ef382f061112f989d1312398b945cc69838b9c \ - --hash=sha256:cb84682933c400bddf94dd94f928d1c6570f500b6dd255973d4bfb495b82585f - # via feast (setup.py) -grpcio-status==1.62.3 \ - --hash=sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485 \ - --hash=sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8 +grpcio-health-checking==1.83.0 \ + --hash=sha256:7d8b47a5bfbc699d4aee0fc7a27f5d0265eb23a6a64db5ac2d8b749a0f8a9911 \ + --hash=sha256:ad6bc4d5a1103ad704d25ccd82def300dfa27996b185cab3e4cceeef2c6867d4 + # via feast (pyproject.toml) +grpcio-reflection==1.83.0 \ + --hash=sha256:6a2a30a462ac2c3c6d149734a8fe655fa7617c17fa2cda9949006f3bf07f5b3e \ + --hash=sha256:9d4151f68f18a85aa5b0eff5d493ad13f9421f96c8d9c01c840b353dc979be93 + # via feast (pyproject.toml) +grpcio-status==1.83.0 \ + --hash=sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae \ + --hash=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 # via google-api-core -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore + # httpcore2 # uvicorn -hiredis==2.4.0 \ - --hash=sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027 \ - --hash=sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577 \ - --hash=sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603 \ - --hash=sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869 \ - --hash=sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228 \ - --hash=sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79 \ - --hash=sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb \ - --hash=sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33 \ - --hash=sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77 \ - --hash=sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9 \ - --hash=sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd \ - --hash=sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a \ - --hash=sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8 \ - --hash=sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9 \ - --hash=sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901 \ - --hash=sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7 \ - --hash=sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0 \ - --hash=sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d \ - --hash=sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213 \ - --hash=sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8 \ - --hash=sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389 \ - --hash=sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330 \ - --hash=sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1 \ - --hash=sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2 \ - --hash=sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288 \ - --hash=sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba \ - --hash=sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded \ - --hash=sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca \ - --hash=sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109 \ - --hash=sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8 \ - --hash=sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea \ - --hash=sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6 \ - --hash=sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8 \ - --hash=sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0 \ - --hash=sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5 \ - --hash=sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5 \ - --hash=sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182 \ - --hash=sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73 \ - --hash=sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af \ - --hash=sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac \ - --hash=sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204 \ - --hash=sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277 \ - --hash=sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212 \ - --hash=sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41 \ - --hash=sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e \ - --hash=sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c \ - --hash=sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2 \ - --hash=sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4 \ - --hash=sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348 \ - --hash=sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b \ - --hash=sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e \ - --hash=sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995 \ - --hash=sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371 \ - --hash=sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613 \ - --hash=sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b \ - --hash=sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d \ - --hash=sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708 \ - --hash=sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08 \ - --hash=sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca \ - --hash=sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12 \ - --hash=sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8 \ - --hash=sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829 \ - --hash=sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38 \ - --hash=sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825 \ - --hash=sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67 \ - --hash=sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056 \ - --hash=sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376 \ - --hash=sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214 \ - --hash=sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982 \ - --hash=sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034 \ - --hash=sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1 \ - --hash=sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f \ - --hash=sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9 \ - --hash=sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5 \ - --hash=sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390 \ - --hash=sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0 \ - --hash=sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d \ - --hash=sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57 \ - --hash=sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe \ - --hash=sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2 \ - --hash=sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415 \ - --hash=sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c \ - --hash=sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b \ - --hash=sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853 \ - --hash=sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36 \ - --hash=sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae \ - --hash=sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd \ - --hash=sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187 \ - --hash=sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c \ - --hash=sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542 \ - --hash=sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d \ - --hash=sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79 \ - --hash=sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7 \ - --hash=sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def - # via feast (setup.py) +hiredis==3.4.0 \ + --hash=sha256:02298551060cbfe17b1acb02e9d066eadd51e37357369736f8fc1f5533875255 \ + --hash=sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc \ + --hash=sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865 \ + --hash=sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c \ + --hash=sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec \ + --hash=sha256:06c163607f163a300cba9ef02fd8a234dd644a208bfafe972614f72e5287b9ba \ + --hash=sha256:0731dd9f2bf4d3417947a3cbbca4dd4d9fa6ad7cecfd4e16439c17a4562d6304 \ + --hash=sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419 \ + --hash=sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320 \ + --hash=sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b \ + --hash=sha256:0bcb630add6bc9ea136fce691ddff0c46aa91cb860df4ca789fe44127eb7e90d \ + --hash=sha256:12ea5facb5b08fa23e4c101ec2151f3a3de8ecec412fec58dbde0a6eebca02c7 \ + --hash=sha256:12eca9aea1450d1a85dc15574a985c227e52abbc2b6466f48ad2aa3b82124701 \ + --hash=sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f \ + --hash=sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017 \ + --hash=sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc \ + --hash=sha256:16a3ea8193c5f4ee6e8daafe0126207cef38eec5a923c97047e6985d9bb1b61f \ + --hash=sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4 \ + --hash=sha256:1bfb9ccfb13be63883e5f2e5ff7f6fc87bf256f8243af594257dfbed9dbc3cf0 \ + --hash=sha256:1c9e141b420dae63ea21303a7e8672282326cae67472768e9a3f6e7f0990bd28 \ + --hash=sha256:1d8c21fb85f2a3de9328cf5bace84b63da00028e771b92335ff4fdc00121dd5a \ + --hash=sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5 \ + --hash=sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016 \ + --hash=sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240 \ + --hash=sha256:282c4310af72afbe18b07d416459f4febeaeb805a067a7df790136e0e550fcb2 \ + --hash=sha256:2c432cce38190c3c13b6c28d6840ee85db50765ad510134007f1cfb3844dce73 \ + --hash=sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591 \ + --hash=sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea \ + --hash=sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980 \ + --hash=sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b \ + --hash=sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb \ + --hash=sha256:386b556f48fb0f9f09696b9d37f1cae554123fbd9f091d0ca23087f2aca02887 \ + --hash=sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b \ + --hash=sha256:4190bd07dd7879a8a7ddbb2a4f74d402721f3898276e35beb98851b85b5f539c \ + --hash=sha256:423570ac4d2665c0d55d8707b7859a331e9001da5e437b7b7a23e0acbce770c9 \ + --hash=sha256:43004b0b48abc628dda1ac3ac4871e1326c126f8cd9f11164d61934d827d7a3b \ + --hash=sha256:4404c557fd49bcfe24dff41f1209e4221c76d1607df2fb2dfd39474b5b086dcb \ + --hash=sha256:44660a91e0fbc803c29b337c1a9194c8d7b4cd3a3868d28f747cbec2df165483 \ + --hash=sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582 \ + --hash=sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2 \ + --hash=sha256:45c6c296056641b5df37cedafe7d1553f33bc247e2f81603a4d038b39261879b \ + --hash=sha256:46e041fc7a83eaa989ef5fe09526bc2353a6ba39b4ce5191684eabefc02e17b0 \ + --hash=sha256:4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e \ + --hash=sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d \ + --hash=sha256:4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d \ + --hash=sha256:4e5530d7a6c71c1a9c3b850b08bbc8178ffc9556e3aef7f9c808fe9ff91a6f11 \ + --hash=sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763 \ + --hash=sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584 \ + --hash=sha256:5359caad5b57da0bce11d2880f22617ba3710f0866121a924745447848448034 \ + --hash=sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79 \ + --hash=sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9 \ + --hash=sha256:64cf54c1dad35830bf1d86b343ad3c55aed443af51c5ccead8afd1b547aca196 \ + --hash=sha256:6774f1fe2723001ca0cd42bf5d8b1235301226273915c581c5c1260d4d114c43 \ + --hash=sha256:68c81c04e0d8778c01163dd2bc6f0e0236fc7f650c291526f10d4faccd99e5a0 \ + --hash=sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb \ + --hash=sha256:69d0326f20354ce278cbb86f5ae47cb390e22bb94a66877031038af907c42fa5 \ + --hash=sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22 \ + --hash=sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b \ + --hash=sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9 \ + --hash=sha256:7b159315df71a009375227384aa4219f98c2342ccd8eb33e3f4b58654f426376 \ + --hash=sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203 \ + --hash=sha256:7f7fc1535f6e1a190089eae46dee25f0c6b72bb221d377be07092803b8208733 \ + --hash=sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f \ + --hash=sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13 \ + --hash=sha256:85a9abf2d16b4cb112f6cc6c32236d763b34d21c69b00c2f81a4ff3016fcc1d6 \ + --hash=sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c \ + --hash=sha256:899d5cea72c9a6c2d9b6e7b223de62c9e9f541f48267cc7135764070548735ce \ + --hash=sha256:89e95f50ec4d29645ba2da9010e8717861585dc9f3df354a367f43807a6db3c7 \ + --hash=sha256:8aaaab18314fd25453b5cf59c8cdca4110e419455bcb4c0737d19d4151513e75 \ + --hash=sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1 \ + --hash=sha256:8f2c8569460aa456a294d5e9a3579a382b825dc2e283e3cc398323dcfb6a3dee \ + --hash=sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151 \ + --hash=sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a \ + --hash=sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d \ + --hash=sha256:98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c \ + --hash=sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f \ + --hash=sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d \ + --hash=sha256:a315009b441a0105a373a9a780ebb1c6f7d9ead88ac6ea5f2a15791353c6f590 \ + --hash=sha256:a3796094f616f72976ff51e4dc1a016e753c0f9af5393b2df96920b6bae1e19b \ + --hash=sha256:a427976ba339da624367613d7eba52a1516e6d5c7f8988dc8fb888fbcf52d8b6 \ + --hash=sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb \ + --hash=sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530 \ + --hash=sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4 \ + --hash=sha256:b0c29ac4f50fc61a904acfe76e4ce6bc37e16ab5a98089cb411476d466e1bb36 \ + --hash=sha256:b32afb576705fc5a02440d416ef476ddbcc827a20ff6c4a9d557882bc6b75135 \ + --hash=sha256:b6228afacadb19a858289319d72797023e2cb048f2f930b68b7fe57eaed5fb6b \ + --hash=sha256:b98f44bf873731d9ce99e6b3eeb6196bf7b27693ad9f6de0fba986b2b0845127 \ + --hash=sha256:bb44efa4fa3e3ed7779ad0ade3c08ed5d75ca7a6336893e9a4f2722093b4168a \ + --hash=sha256:bb8b2dd9c00d80ad2870ffbb51ce9af99fb2194c082d7b09f4125d90e21426f6 \ + --hash=sha256:bca175f02a2b0150ffe7f5dc8bf49c798f34d2c7024d17ace0ec97a7583560e3 \ + --hash=sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59 \ + --hash=sha256:c2245c46b4ced5f689469e6dcdfc8a0895bf873840a6600f5ea759cdf1b26a8b \ + --hash=sha256:c8c6337e7ad187e8039d06e4b33ee5148dcd73928819d94087e649eb37316a09 \ + --hash=sha256:ccc5c660e31d788ca534a20f2ccb7a80b946b960e18ed4e1db950fcac122b405 \ + --hash=sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643 \ + --hash=sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c \ + --hash=sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4 \ + --hash=sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d \ + --hash=sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352 \ + --hash=sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81 \ + --hash=sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5 \ + --hash=sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092 \ + --hash=sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4 \ + --hash=sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad \ + --hash=sha256:e29267ecdd08758926f1a9221af2671d90f475480c40aff409921b1f362f1bd5 \ + --hash=sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d \ + --hash=sha256:ed1dba2695f6de009c67d63b39ff978cb43b8a79362f697acedffb7743e50d21 \ + --hash=sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1 \ + --hash=sha256:f3c67f39b112dc35f68d5b59ee111db6121f037d1a60cf3840ecffbb2ec5686b \ + --hash=sha256:f7c7596fbb2b5202e943180353958e89014e763c7f25877a92f70bbde6cd7f19 \ + --hash=sha256:ff28eca77a57751488df90b0f385b472fd78e140a4d45097224648a5737acdf5 \ + --hash=sha256:ff6217da86a63a6a5ea954d9b9ed67916d9d94a566b44aa98e68ae34b40ebbbf + # via feast (pyproject.toml) httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httpcore2==2.9.1 \ + --hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \ + --hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26 + # via httpx2 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - # via - # fastapi-mcp - # mcp -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d + # via fastapi-mcp +httpx2==2.9.1 \ + --hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \ + --hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a # via mcp -ibis-framework[duckdb]==9.5.0 \ - --hash=sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0 \ - --hash=sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84 - # via feast (setup.py) -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +ibis-framework[duckdb]==12.0.0 \ + --hash=sha256:0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 \ + --hash=sha256:238624f2c14fdab8382ca2f4f667c3cdb81e29844cd5f8db8a325d0743767c61 + # via feast (pyproject.toml) +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx + # httpx2 # requests # snowflake-connector-python # yarl -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc # via dask jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via feast (setup.py) -jmespath==1.0.1 \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe + # via feast (pyproject.toml) +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 # via # aiobotocore # boto3 # botocore -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 - # via - # feast (setup.py) - # mcp +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via mcp jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -kubernetes==33.1.0 \ - --hash=sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5 \ - --hash=sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993 - # via feast (setup.py) +kubernetes==36.0.3 \ + --hash=sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7 \ + --hash=sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f + # via feast (pyproject.toml) +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ --hash=sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 # via partd -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ @@ -1199,439 +1361,506 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -marshmallow==4.0.1 \ - --hash=sha256:72f14ef346f81269dbddee891bac547dda1501e9e08b6a809756ea3dbb7936a1 \ - --hash=sha256:e1d860bd262737cb2d34e1541b84cb52c32c72c9474e3fe6f30f137ef8b0d97f - # via environs -mcp==1.12.4 \ - --hash=sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5 \ - --hash=sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789 +mcp==2.0.0 \ + --hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \ + --hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6 # via fastapi-mcp +mcp-types==2.0.0 \ + --hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \ + --hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379 + # via mcp mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -milvus-lite==2.4.12 \ - --hash=sha256:20087663e7b4385050b7ad08f1f03404426d4c87b1ff91d5a8723eee7fd49e88 \ - --hash=sha256:334037ebbab60243b5d8b43d54ca2f835d81d48c3cda0c6a462605e588deb05d \ - --hash=sha256:a0f3a5ddbfd19f4a6b842b2fd3445693c796cde272b701a1646a94c1ac45d3d7 \ - --hash=sha256:e8d4f7cdd5f731efd6faeee3715d280fd91a5f9b4d89312664d56401f65b1473 - # via - # feast (setup.py) - # pymilvus -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) -multidict==6.7.0 \ - --hash=sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32 \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ - --hash=sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff \ - --hash=sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8 \ - --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ - --hash=sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 \ - --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ - --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6 \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ - --hash=sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912 \ - --hash=sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c \ - --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6 \ - --hash=sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84 \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ - --hash=sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd \ - --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0 \ - --hash=sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e \ - --hash=sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 \ - --hash=sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc \ - --hash=sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7 \ - --hash=sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4 \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111 \ - --hash=sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e \ - --hash=sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84 \ - --hash=sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851 \ - --hash=sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb \ - --hash=sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ - --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5 \ - --hash=sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb \ - --hash=sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349 \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ - --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ - --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34 \ - --hash=sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85 \ - --hash=sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ - --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ - --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ - --hash=sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91 \ - --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40 \ - --hash=sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73 \ - --hash=sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4 \ - --hash=sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687 \ - --hash=sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 +milvus-lite==3.1.1 \ + --hash=sha256:0cbf8386a2e99b6464a5eda14cf8e31934b0e287e36c831e293aee614d9efc2e \ + --hash=sha256:0f343c9bf1e291d6c2fa615a52a57090ebbaf96b84bfb71f7eb73cd95cd7fc1c + # via feast (pyproject.toml) +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via feast (pyproject.toml) +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via # aiobotocore # aiohttp # yarl -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mypy==2.3.0 \ + --hash=sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491 \ + --hash=sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762 \ + --hash=sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5 \ + --hash=sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654 \ + --hash=sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd \ + --hash=sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe \ + --hash=sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc \ + --hash=sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3 \ + --hash=sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5 \ + --hash=sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329 \ + --hash=sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b \ + --hash=sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3 \ + --hash=sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7 \ + --hash=sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805 \ + --hash=sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f \ + --hash=sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e \ + --hash=sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7 \ + --hash=sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494 \ + --hash=sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373 \ + --hash=sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88 \ + --hash=sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee \ + --hash=sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2 \ + --hash=sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757 \ + --hash=sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a \ + --hash=sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b \ + --hash=sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461 \ + --hash=sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97 \ + --hash=sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4 \ + --hash=sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117 \ + --hash=sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36 \ + --hash=sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac \ + --hash=sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5 \ + --hash=sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556 \ + --hash=sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c \ + --hash=sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88 \ + --hash=sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568 \ + --hash=sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595 \ + --hash=sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f \ + --hash=sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1 \ + --hash=sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef \ + --hash=sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6 \ + --hash=sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db \ + --hash=sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff \ + --hash=sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60 \ + --hash=sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c # via sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf +numpy==2.4.6 \ + --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ + --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ + --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ + --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ + --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ + --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ + --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ + --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ + --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ + --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ + --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ + --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ + --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ + --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ + --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ + --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ + --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ + --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ + --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ + --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ + --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ + --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ + --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ + --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ + --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ + --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ + --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes + # faiss-cpu # ibis-framework + # milvus-lite # pandas # pandas-gbq - # pyarrow oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 - # via - # kubernetes - # requests-oauthlib -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f + # via requests-oauthlib +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef + # via mcp +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via pymilvus +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # dask # db-dtypes + # faiss-cpu # google-cloud-bigquery # gunicorn # ibis-framework @@ -1694,7 +1923,7 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery @@ -1702,9 +1931,9 @@ pandas==2.3.3 \ # pandas-gbq # pymilvus # snowflake-connector-python -pandas-gbq==0.29.2 \ - --hash=sha256:99f6b642bd8340e96c2acdf7d717108aa0d31c703c2f284817ba7dc3b16110b2 \ - --hash=sha256:a3a28e7de4f8456fc4b8e9b7472b9bc470d2566f89c4cd997599e504f5a0a706 +pandas-gbq==0.35.0 \ + --hash=sha256:258de481019566611031919997bf9c1ece4ca30a4dd02d3fc3664b251d446182 \ + --hash=sha256:596c908487ef0649a161e86ef272c00c267e581b95dc5ee0dc3518545b33bcfc # via google-cloud-bigquery parsy==2.2 \ --hash=sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 \ @@ -1714,164 +1943,162 @@ partd==1.4.2 \ --hash=sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f \ --hash=sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c # via dask -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via mypy -platformdirs==4.5.0 \ - --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ - --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 # via snowflake-connector-python -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via feast (setup.py) -propcache==0.4.1 \ - --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ - --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ - --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ - --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ - --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ - --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ - --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ - --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ - --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ - --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via feast (pyproject.toml) +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274 \ + --hash=sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.26.1 \ - --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ - --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +proto-plus==1.28.2 \ + --hash=sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501 \ + --hash=sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52 # via # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==6.32.1 \ - --hash=sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346 \ - --hash=sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4 \ - --hash=sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1 \ - --hash=sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085 \ - --hash=sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1 \ - --hash=sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710 \ - --hash=sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122 \ - --hash=sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281 \ - --hash=sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d +protobuf==7.35.1 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -1883,235 +2110,275 @@ protobuf==6.32.1 \ # grpcio-status # proto-plus # pymilvus -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 - # via feast (setup.py) -psycopg[c, pool]==3.2.5 \ - --hash=sha256:b782130983e5b3de30b4c529623d3687033b4dafa05bb661fc6bf45837ca5879 \ - --hash=sha256:f5f750611c67cb200e85b408882f29265c66d1de7f813add4f8125978bfd70e8 - # via feast (setup.py) -psycopg-c==3.2.5 \ - --hash=sha256:57ad4cfd28de278c424aaceb1f2ad5c7910466e315dfe84e403f3c7a0a2ce81b +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # feast (pyproject.toml) + # pandas-gbq +psycopg[c, pool]==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via feast (pyproject.toml) +psycopg-c==3.3.4 \ + --hash=sha256:ed8106128b2d04359c185fc9641b4409abfce4d0b6fb1d1ff6800646e27f1a22 # via psycopg -psycopg-pool==3.2.6 \ - --hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \ - --hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7 +psycopg-pool==3.3.1 \ + --hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \ + --hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c # via psycopg -pyarrow==17.0.0 \ - --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ - --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ - --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ - --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ - --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ - --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ - --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ - --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ - --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ - --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ - --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ - --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ - --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ - --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ - --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ - --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ - --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ - --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ - --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ - --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ - --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ - --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ - --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ - --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ - --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ - --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ - --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ - --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ - --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ - --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ - --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ - --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ - --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ - --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ - --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ - --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 +pyarrow==25.0.0 \ + --hash=sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be \ + --hash=sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc \ + --hash=sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec \ + --hash=sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849 \ + --hash=sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e \ + --hash=sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8 \ + --hash=sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d \ + --hash=sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537 \ + --hash=sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062 \ + --hash=sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e \ + --hash=sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104 \ + --hash=sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517 \ + --hash=sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e \ + --hash=sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5 \ + --hash=sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887 \ + --hash=sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1 \ + --hash=sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b \ + --hash=sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be \ + --hash=sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778 \ + --hash=sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e \ + --hash=sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194 \ + --hash=sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2 \ + --hash=sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9 \ + --hash=sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b \ + --hash=sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18 \ + --hash=sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7 \ + --hash=sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc \ + --hash=sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62 \ + --hash=sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0 \ + --hash=sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f \ + --hash=sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e \ + --hash=sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862 \ + --hash=sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50 \ + --hash=sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe \ + --hash=sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3 \ + --hash=sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a \ + --hash=sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4 \ + --hash=sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af \ + --hash=sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b \ + --hash=sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0 \ + --hash=sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f \ + --hash=sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec \ + --hash=sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery # ibis-framework + # milvus-lite # pandas-gbq # snowflake-connector-python pyarrow-hotfix==0.7 \ --hash=sha256:3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 \ --hash=sha256:59399cd58bdd978b2e42816a4183a55c6472d4e33d183351b6069f11ed42661d # via ibis-framework -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 # via google-auth -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi # fastapi-mcp # mcp + # mcp-types # pydantic-settings -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pydantic-settings==2.11.0 \ - --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ - --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c - # via - # fastapi-mcp - # mcp +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f + # via fastapi-mcp pydata-google-auth==1.9.1 \ --hash=sha256:0a51ce41c601ca0bc69b8795bf58bedff74b4a6a007c9106c7cbcdec00eaced2 \ --hash=sha256:75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 # via pandas-gbq -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via - # feast (setup.py) + # feast (pyproject.toml) # rich -pyjwt==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via - # feast (setup.py) + # feast (pyproject.toml) + # mcp # snowflake-connector-python -pymilvus==2.4.9 \ - --hash=sha256:0937663700007c23a84cfc0656160b301f6ff9247aaec4c96d599a6b43572136 \ - --hash=sha256:45313607d2c164064bdc44e0f933cb6d6afa92e9efcc7f357c5240c57db58fbe - # via feast (setup.py) -pymysql==1.1.2 \ - --hash=sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03 \ - --hash=sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 - # via feast (setup.py) -pyopenssl==25.3.0 \ - --hash=sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6 \ - --hash=sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329 +pymilvus==3.0.1 \ + --hash=sha256:c02389059088b18d6e598cd175541e445c772fab4926c5e527c4913be34887f1 \ + --hash=sha256:c5a8d5c1fa1de7b416e3529d383d8cc2e7da2170433ffa4a2d9087e14f70171a + # via feast (pyproject.toml) +pymysql==1.2.0 \ + --hash=sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 \ + --hash=sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33 + # via feast (pyproject.toml) +pyopenssl==26.3.0 \ + --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ + --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 # via snowflake-connector-python python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ @@ -2123,22 +2390,21 @@ python-dateutil==2.9.0.post0 \ # ibis-framework # kubernetes # pandas -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via - # environs # pydantic-settings + # pymilvus # uvicorn -python-multipart==0.0.20 \ - --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \ - --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13 +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 # via mcp -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via - # ibis-framework # pandas # snowflake-connector-python pyyaml==6.0.3 \ @@ -2216,30 +2482,31 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # kubernetes # uvicorn -redis==4.6.0 \ - --hash=sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d \ - --hash=sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c - # via feast (setup.py) +redis==7.4.1 \ + --hash=sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e \ + --hash=sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + # via feast (pyproject.toml) referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 # via # jsonschema # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # google-api-core # google-cloud-bigquery # google-cloud-storage # kubernetes + # pymilvus # requests-oauthlib # snowflake-connector-python requests-oauthlib==2.0.0 \ @@ -2248,188 +2515,144 @@ requests-oauthlib==2.0.0 \ # via # google-auth-oauthlib # kubernetes -rich==13.9.4 \ - --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ - --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via # fastapi-mcp # ibis-framework # typer -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==2026.6.3 \ + --hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \ + --hash=sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680 \ + --hash=sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9 \ + --hash=sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538 \ + --hash=sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804 \ + --hash=sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf \ + --hash=sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4 \ + --hash=sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97 \ + --hash=sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6 \ + --hash=sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96 \ + --hash=sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a \ + --hash=sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187 \ + --hash=sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975 \ + --hash=sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f \ + --hash=sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703 \ + --hash=sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9 \ + --hash=sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127 \ + --hash=sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f \ + --hash=sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa \ + --hash=sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05 \ + --hash=sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171 \ + --hash=sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba \ + --hash=sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c \ + --hash=sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223 \ + --hash=sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4 \ + --hash=sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885 \ + --hash=sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698 \ + --hash=sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f \ + --hash=sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7 \ + --hash=sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed \ + --hash=sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f \ + --hash=sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf \ + --hash=sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e \ + --hash=sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f \ + --hash=sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24 \ + --hash=sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a \ + --hash=sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41 \ + --hash=sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc \ + --hash=sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d \ + --hash=sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146 \ + --hash=sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e \ + --hash=sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e \ + --hash=sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4 \ + --hash=sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12 \ + --hash=sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7 \ + --hash=sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261 \ + --hash=sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6 \ + --hash=sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5 \ + --hash=sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93 \ + --hash=sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7 \ + --hash=sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda \ + --hash=sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8 \ + --hash=sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342 \ + --hash=sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c \ + --hash=sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb \ + --hash=sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0 \ + --hash=sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77 \ + --hash=sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3 \ + --hash=sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885 \ + --hash=sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826 \ + --hash=sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617 \ + --hash=sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb \ + --hash=sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577 \ + --hash=sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80 \ + --hash=sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e \ + --hash=sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945 \ + --hash=sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90 \ + --hash=sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7 \ + --hash=sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0 \ + --hash=sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140 \ + --hash=sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822 \ + --hash=sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba \ + --hash=sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9 \ + --hash=sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4 \ + --hash=sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a \ + --hash=sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8 \ + --hash=sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf \ + --hash=sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4 \ + --hash=sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324 \ + --hash=sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53 \ + --hash=sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b \ + --hash=sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41 \ + --hash=sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9 \ + --hash=sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca \ + --hash=sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1 \ + --hash=sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d \ + --hash=sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690 \ + --hash=sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107 \ + --hash=sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2 \ + --hash=sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76 \ + --hash=sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d \ + --hash=sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af \ + --hash=sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6 \ + --hash=sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db \ + --hash=sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369 \ + --hash=sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd \ + --hash=sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911 \ + --hash=sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504 \ + --hash=sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a \ + --hash=sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9 \ + --hash=sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13 \ + --hash=sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc \ + --hash=sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278 \ + --hash=sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868 \ + --hash=sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2 \ + --hash=sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd \ + --hash=sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4 \ + --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6 \ + --hash=sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9 \ + --hash=sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00 \ + --hash=sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f \ + --hash=sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e \ + --hash=sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442 \ + --hash=sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da \ + --hash=sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90 \ + --hash=sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef # via # jsonschema # referencing -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via google-auth -s3transfer==0.13.1 \ - --hash=sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724 \ - --hash=sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf +s3transfer==0.19.2 \ + --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \ + --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25 # via boto3 -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq # pydata-google-auth - # pymilvus shellingham==1.5.4 \ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de @@ -2440,209 +2663,223 @@ six==1.17.0 \ # via # kubernetes # python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio -snowflake-connector-python[pandas]==3.18.0 \ - --hash=sha256:0af10b207af3d2de2b130e89018d49a60f2e5cfe841f3bf459e58f2e1c4c4506 \ - --hash=sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628 \ - --hash=sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4 \ - --hash=sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e \ - --hash=sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee \ - --hash=sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9 \ - --hash=sha256:32b1abfea32561d817b0a2f80b06d936cb32712af06bf7b848a428bfd857a10a \ - --hash=sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a \ - --hash=sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728 \ - --hash=sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800 \ - --hash=sha256:4ed2d593f1983939d5d8d88b212d86fd4f14f0ceefc1df9882b4a18534adbde9 \ - --hash=sha256:51eb789a09dc6c62119cfabd044fba1a6b8378206f05a1e83ddb2e9cb49acc0b \ - --hash=sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc \ - --hash=sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb \ - --hash=sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba \ - --hash=sha256:783a9ab206563d7b52fdcdd7a72af62de811d3381ca64132fd3445537b4d041b \ - --hash=sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f \ - --hash=sha256:8d3e96e1d09b07edca6c1f6ca675b6fdd05a4a7e428e4cdf6fb697d87b9f60fc \ - --hash=sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a \ - --hash=sha256:a8c570edff5a4888840dbe1e9e65c5e4d77d55c5c800cd359fe0903a769201e0 \ - --hash=sha256:aeeb181a156333480f60b5f8ddbb3d087e288b4509adbef7993236defe4d7570 \ - --hash=sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca \ - --hash=sha256:b99f261c82be92224ac20c8c12bdf26ce3ed5dfd8a3df8a97f15a1e11c46ad27 \ - --hash=sha256:bd1de3038b6d7059ca59f93e105aba2a673151c693cc4292f72f38bfaf147df2 \ - --hash=sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e \ - --hash=sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00 - # via feast (setup.py) +snowflake-connector-python[pandas]==4.7.1 \ + --hash=sha256:051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117 \ + --hash=sha256:052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7 \ + --hash=sha256:0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317 \ + --hash=sha256:167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf \ + --hash=sha256:171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a \ + --hash=sha256:1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6 \ + --hash=sha256:22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf \ + --hash=sha256:2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b \ + --hash=sha256:4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a \ + --hash=sha256:4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754 \ + --hash=sha256:5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9 \ + --hash=sha256:571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d \ + --hash=sha256:5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35 \ + --hash=sha256:7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0 \ + --hash=sha256:7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5 \ + --hash=sha256:81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57 \ + --hash=sha256:8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a \ + --hash=sha256:92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3 \ + --hash=sha256:b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45 \ + --hash=sha256:d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa \ + --hash=sha256:e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327 \ + --hash=sha256:f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c \ + --hash=sha256:f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7 \ + --hash=sha256:f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52 \ + --hash=sha256:fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1 \ + --hash=sha256:ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b + # via feast (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via snowflake-connector-python -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -sqlglot==25.20.2 \ - --hash=sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab \ - --hash=sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567 +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via feast (pyproject.toml) +sqlglot==30.14.0 \ + --hash=sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9 \ + --hash=sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636 # via ibis-framework -sse-starlette==3.0.2 \ - --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ - --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 \ + --hash=sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627 # via mcp -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via + # feast (pyproject.toml) # fastapi # mcp -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via feast (setup.py) + # sse-starlette +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via feast (pyproject.toml) tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via feast (pyproject.toml) toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via fastapi-mcp -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 # via snowflake-connector-python -toolz==0.12.1 \ - --hash=sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85 \ - --hash=sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # dask # ibis-framework # partd -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via feast (pyproject.toml) +truststore==0.10.4 \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 # via - # feast (setup.py) - # milvus-lite -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) -typer==0.19.2 \ - --hash=sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9 \ - --hash=sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca + # httpcore2 + # httpx2 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) +typer==0.27.0 \ + --hash=sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5 \ + --hash=sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1 # via fastapi-mcp -types-pymysql==1.1.0.20250916 \ - --hash=sha256:873eb9836bb5e3de4368cc7010ca72775f86e9692a5c7810f8c7f48da082e55b \ - --hash=sha256:98d75731795fcc06723a192786662bdfa760e1e00f22809c104fbb47bac5e29b - # via feast (setup.py) -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +types-pymysql==1.2.0.20260724 \ + --hash=sha256:71327a3fea0f680a280445f1b475c2307f68de05c2d3302a79df56af93c574cd \ + --hash=sha256:8493cf87e8df0a7b4e8c84305bc0aae69d3a8af9c6da7bfbb9a4c5c3f97d4a82 + # via feast (pyproject.toml) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiohttp # aiosignal # anyio # fastapi + # grpcio + # httpx2 # ibis-framework + # mcp + # mcp-types # mypy + # opentelemetry-api # psycopg # psycopg-pool # pydantic @@ -2653,559 +2890,514 @@ typing-extensions==4.15.0 \ # sqlalchemy # starlette # typeguard - # typer # typing-inspection typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via pydantic-settings -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via pandas -ujson==5.11.0 \ - --hash=sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80 \ - --hash=sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef \ - --hash=sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a \ - --hash=sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840 \ - --hash=sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53 \ - --hash=sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076 \ - --hash=sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab \ - --hash=sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d \ - --hash=sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89 \ - --hash=sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c \ - --hash=sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb \ - --hash=sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c \ - --hash=sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823 \ - --hash=sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5 \ - --hash=sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac \ - --hash=sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d \ - --hash=sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723 \ - --hash=sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6 \ - --hash=sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0 \ - --hash=sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328 \ - --hash=sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0 \ - --hash=sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04 \ - --hash=sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25 \ - --hash=sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49 \ - --hash=sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec \ - --hash=sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3 \ - --hash=sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3 \ - --hash=sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc \ - --hash=sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a \ - --hash=sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9 \ - --hash=sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302 \ - --hash=sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694 \ - --hash=sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547 \ - --hash=sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01 \ - --hash=sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02 \ - --hash=sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6 \ - --hash=sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60 \ - --hash=sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915 \ - --hash=sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835 \ - --hash=sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701 \ - --hash=sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702 \ - --hash=sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50 \ - --hash=sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6 \ - --hash=sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc \ - --hash=sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a \ - --hash=sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c \ - --hash=sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c \ - --hash=sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03 \ - --hash=sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629 \ - --hash=sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105 \ - --hash=sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844 \ - --hash=sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241 \ - --hash=sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a \ - --hash=sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26 \ - --hash=sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac \ - --hash=sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596 \ - --hash=sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9 \ - --hash=sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752 \ - --hash=sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138 \ - --hash=sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b \ - --hash=sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba \ - --hash=sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5 \ - --hash=sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018 \ - --hash=sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362 \ - --hash=sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746 \ - --hash=sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f \ - --hash=sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88 \ - --hash=sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638 \ - --hash=sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b \ - --hash=sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9 \ - --hash=sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db \ - --hash=sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f \ - --hash=sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58 \ - --hash=sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b \ - --hash=sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433 \ - --hash=sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0 \ - --hash=sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764 \ - --hash=sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c \ - --hash=sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34 \ - --hash=sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052 \ - --hash=sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba \ - --hash=sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c \ - --hash=sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc \ - --hash=sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39 - # via pymilvus -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via + # ibis-framework + # pandas +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via # botocore # kubernetes # requests -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # mcp # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn websocket-client==1.9.0 \ --hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \ --hash=sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef # via kubernetes -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 # via aiobotocore -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8 \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf \ - --hash=sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890 \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737 \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba \ - --hash=sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9 \ - --hash=sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0 \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147 \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e \ - --hash=sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2 \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6 \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750 \ + --hash=sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621 \ + --hash=sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13 \ + --hash=sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1 \ + --hash=sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2 \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata diff --git a/sdk/python/requirements/py3.11-minimal-sdist-requirements-build.txt b/sdk/python/requirements/py3.11-minimal-sdist-requirements-build.txt index cb8f68d20f1..5d4cb44ab0e 100644 --- a/sdk/python/requirements/py3.11-minimal-sdist-requirements-build.txt +++ b/sdk/python/requirements/py3.11-minimal-sdist-requirements-build.txt @@ -4,100 +4,154 @@ # # pybuild-deps compile --generate-hashes --output-file=sdk/python/requirements/py3.11-minimal-sdist-requirements-build.txt sdk/python/requirements/py3.11-minimal-sdist-requirements.txt # +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy calver==2025.3.31 \ --hash=sha256:07511edf5e7fa75ae97445c8c5921240e0fe62937289a3ebe6963eddd3c691b6 \ --hash=sha256:255d1a70bba8f97dc1eee3af4240ed35980508da69257feef94c79e5c6545fc7 # via trove-classifiers -cffi==2.0.0 \ - --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ - --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ - --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ - --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ - --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ - --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ - --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ - --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ - --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ - --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ - --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ - --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ - --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ - --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ - --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ - --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ - --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ - --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ - --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ - --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ - --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ - --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ - --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ - --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ - --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ - --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ - --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ - --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ - --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ - --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ - --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ - --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ - --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ - --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ - --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ - --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ - --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ - --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ - --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ - --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ - --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ - --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ - --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ - --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ - --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ - --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ - --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ - --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ - --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ - --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ - --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ - --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ - --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ - --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ - --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ - --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ - --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ - --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ - --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ - --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ - --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ - --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ - --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ - --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ - --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ - --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ - --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ - --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ - --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ - --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ - --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ - --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ - --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ - --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ - --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f # via cryptography coherent-licensed==0.5.2 \ --hash=sha256:d8071403ce742d3ac3592ddc4fb7057a46caffb415b928b4d52802e5f208416d \ --hash=sha256:edccc5193ca2786f8fb3f0c7374637a143985f781f7eaa21aca3af2bd634b82f - # via zipp + # via + # importlib-metadata + # zipp cython==3.0.12 \ --hash=sha256:0038c9bae46c459669390e53a1ec115f8096b2e4647ae007ff1bf4e6dee92806 \ --hash=sha256:0faa5e39e5c8cdf6f9c3b1c3f24972826e45911e7f5b99cf99453fca5432f45e \ @@ -166,17 +220,120 @@ cython==3.0.12 \ # via # numpy # pandas - # pyarrow # pyyaml # snowflake-connector-python # sqlalchemy - # uvloop -dunamai==1.25.0 \ - --hash=sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab \ - --hash=sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1 +cython==3.1.1 \ + --hash=sha256:011cdcbf7725f0cfc1abc55ec83d326e788050711272131daf3cc24a19c34bb2 \ + --hash=sha256:020089f9c9f10269181f17660a2cada7d4577bd8eea24b7d2b14e6b64b6996be \ + --hash=sha256:07621e044f332d18139df2ccfcc930151fd323c2f61a58c82f304cffc9eb5280 \ + --hash=sha256:0de7adff5b42d2556d073e9f321c2faa639a17fb195ec1de130327f60ec209d8 \ + --hash=sha256:0e3ccec55e2a534a712db14c6617b66f65ad149c014fad518fc3920f6edde770 \ + --hash=sha256:0f7954b0b4b3302655d3caa6924261de5907a4e129bc22ace52fe9ae0cd5a758 \ + --hash=sha256:10f0434916994fe213ea7749268b88d77e3ebcbd1b99542cf64bb7d180f45470 \ + --hash=sha256:12e00b88147b03c148a95365f89dc1c45a0fc52f9c35aa75ff770ef65b615839 \ + --hash=sha256:141ffd6279411c562f6b707adc56b63e965a4fd7f21db83f5d4fcbd8c50ac546 \ + --hash=sha256:16d9870654946375b28280371d370d541641d1071da123d0d64d2c7ebba0cc56 \ + --hash=sha256:23b886a6c8a50b1101ccef2f2f3dc9c699b77633ef5bb5007090226c2ad3f9c2 \ + --hash=sha256:24c640c0746d984789fe2787a098f06cda456ef2dd78b90164d17884b350839a \ + --hash=sha256:263cb0e497910fb5e0a361ad1393b6d728b092178afecc56e8a786f3739960c3 \ + --hash=sha256:268420b92307ae6c5a16e3cf0e2ba1ae3c861650e992893922a0ce08db07cfdb \ + --hash=sha256:28b174f41718a7041cfbe0f48913020875ff1aaa4793942b2451ac6d2baf3f07 \ + --hash=sha256:307f216ed319ea07644f2ef9974406c830f01bc8e677e2147e9bfcdf9e3ca8ad \ + --hash=sha256:3192a61c2a532d3faccdff508bc8427de9530b587888218bfc0226eb33a84e11 \ + --hash=sha256:3fa4bd840de63509c74867b4b092541720a01db1e07351206011c34e0777dc96 \ + --hash=sha256:402f86c00b08f875cd0990f0c4dc52eb3e0bc5d630066cdf3c798631976f1937 \ + --hash=sha256:40f50b07c479eaf33981d81cad274c68cf9fb81dbe79cbf991f59491c88a4705 \ + --hash=sha256:426d78565eb91d3366569b20e92b8f14bffef5f57b2acd05b60bbb9ce5c056a1 \ + --hash=sha256:505ccd413669d5132a53834d792c707974248088c4f60c497deb1b416e366397 \ + --hash=sha256:50ad80e2f438e9127a87c10927e6ac16a987df39c248b19ab2cd31330129be3c \ + --hash=sha256:54a8934cb3bf13b1f8f6cbdae8e382e25a26e67de08ea6ebfd0a467131b67227 \ + --hash=sha256:56c6768a6f601f93daab7c2487f9f110548a896a91e00a6e119445ada2575323 \ + --hash=sha256:64915259276482fa23417b284d1fdc7e3a618ee2f819bb6ea7f974c075633df6 \ + --hash=sha256:689c1aad373556bd2ab1aa1c2dad8939a2891465a1fbd2cbbdd42b488fb40ec8 \ + --hash=sha256:6ea77ad1e649cec38f8622ba28dcdfbe7bf519bc132abbcf5df759b3975b5a73 \ + --hash=sha256:7489559e6c5ecbba49d535c2e03cf77c2594a3190b6aca7da5b508ba1664a89a \ + --hash=sha256:755a991601b27dd3555310d0f95b19a05e622a80d7b4e7a91fa6f5f3ef3f3b80 \ + --hash=sha256:7da069ca769903c5dee56c5f7ab47b2b7b91030eee48912630db5f4f3ec5954a \ + --hash=sha256:7e5cad896af896482240979b996bf4136b0d18dc40c56c72c5641bf0ea085dfb \ + --hash=sha256:7fff6526bb6f4eea615663117b86de6ede0d17c477b600d3d8302be3502bd3c3 \ + --hash=sha256:83b2af5c327f7da4f08afc34fddfaf6d24fa0c000b6b70a527c8125e493b6080 \ + --hash=sha256:873aac4ac0b0fb197557c0ac15458b780b9221daa4a716881cbd1a9016c8459f \ + --hash=sha256:8aaa29e763adf3496ab9d371e3caed8da5d3ce5ff8fb57433e2a2f2b5036e5c8 \ + --hash=sha256:953046c190fa9ab9a09a546a909b847cdbb4c1fe34e9bfa4a15b6ee1585a86aa \ + --hash=sha256:9b61b99205308c96b1162de59bd67ecadcad3d166a4a1f03a3d9e826c39cd375 \ + --hash=sha256:9d7dc0e4d0cd491fac679a61e9ede348c64ca449f99a284f9a01851aa1dbc7f6 \ + --hash=sha256:a19188ecd385cdc649e3fec370f38d5fd7f1651aeed0b3fb403180f38fc88e8a \ + --hash=sha256:a585796939b09b3205b1980e4a55e745c0251e45a5c637afbcac3c6cc9ad6f90 \ + --hash=sha256:a92f6bd395eadea6eed722a8188d3bdd49db1c9fa3c38710456d6148ab71bad7 \ + --hash=sha256:ab644415458d782c16ba7252de9cec1e3125371641cafea2e53a8c1cf85dd58d \ + --hash=sha256:af8f62cc9339b75fe8434325083e6a7cae88c9c21efd74bbb6ba4e3623219469 \ + --hash=sha256:b181158b5761bdaf40f6854f016ab7ddff64d3db4fca55cb3ca0f73813dd76d6 \ + --hash=sha256:b194a65a0fd91f305d2d1e7010f44111774a28533e1e44dd2a76e7de81a219b9 \ + --hash=sha256:b68f1bc80387554eb43f2b62795c173bed9e37201f39dc5084ac437c90a79c9f \ + --hash=sha256:c360823e1063784efc2335617e0f28573d7a594c5a8a05d85e850a9621cccb1f \ + --hash=sha256:c5cb6c054daadaf01a88c8f49f3edd9e829c9b76a82cbb4269e3f9878254540b \ + --hash=sha256:c740a10cd0f50321d048c8ca318eefb4c42b8bffef982dcd89c946d374192702 \ + --hash=sha256:c8b8be01fd40b3e38a76c60a524f956548a3a7566e5530a833a48a695f3d6c12 \ + --hash=sha256:cb5661941707bd41ec7a9c273d698113ac50392444f785088e9d9706c6a5937b \ + --hash=sha256:cd748fab8e4426dbcb2e0fa2979558333934d24365e0de5672fbabfe337d880c \ + --hash=sha256:cdf53dc4b2a13bd072d6c2c18ac073dbf0f798555bc27ba4f7546a275eb16a0f \ + --hash=sha256:ce82070ccf92c3599d331b9eaaefd9d4562976fb86a8d6bccf05c4a0b8389f2a \ + --hash=sha256:d14186bd96783d13b8fd0e5b289f2e137a8a25479638b73a1c7e4a99a8d70753 \ + --hash=sha256:dee554f0a589377bdaea0eb70e212bf3f35dc6a51a2aa86c9351345e21fd2f07 \ + --hash=sha256:dfa500fd7ae95ca152a5f8062b870532fa3e27efcef6d00612e1f28b9f72615f \ + --hash=sha256:dff0e7dd53a0ca35b64cda843253d5cac944db26663dc097b3a1adf2c49514ad \ + --hash=sha256:e000f0533eedf3d6dfbe30bb3c58a054c58f0a7778390342fa577a0dc47adab3 \ + --hash=sha256:e851ab66a31794e40df1bc6f649cdc56c998c637f5a1b9410c97a90f6b6cb855 \ + --hash=sha256:fd689910002adfac8734f237cdea1573e38345f27ed7fd445482813b65a29457 + # via grpcio +cython==3.2.9 \ + --hash=sha256:114b2dee0fa1daa48a59574d848da0ff1b6bdb725a755e9b92fad14962e1ff8d \ + --hash=sha256:1c2974742433be5c36ae1df1e761ce0063753fc2702316d5a3062b0a4e94a1df \ + --hash=sha256:23e80bc885c599e72072e18d0746df82d394b73100c1e153cda7359e6e59fe09 \ + --hash=sha256:2751b7c0cb13135aadcc1cc8fe223be98d458cd1132611d31675e768a5e4a2e9 \ + --hash=sha256:2b1756ddc3bc0cd4341a515fc420c3e25e13c249f5537159b3fb0bff8d19e55c \ + --hash=sha256:41637c644d7224d2d3170ee312077c2173693f0d4a0da1c82a0cffa3680a42dc \ + --hash=sha256:44b7fc417933d65bf31bce00337ff318efa3ea59daed21d10fe842d41e657c08 \ + --hash=sha256:4b1fd5a9c03f72a18618668a8e90d569442ed742f910e3ad003dcc9348e9598b \ + --hash=sha256:4b871ad97dd7fb1cbf56f6238c54423febd310afc1d9d9bc70c69c89b7ce57fc \ + --hash=sha256:522e277c29ebee304e73f3a78307549d09908b0c63098421a12dffc8d779fd74 \ + --hash=sha256:56d95c0674c25f281c6ae8f1d17bd425d6c2818bb304ff781831bb5d00d04b0b \ + --hash=sha256:57a6a78d14f7dd7d6062d9bca694e2a8c1c14113b6ceceea076abcd1161fdc5a \ + --hash=sha256:61d4abbf84f77c8d19361d05d9f51d65d8d95e74f736eae55fa1aed8a1430469 \ + --hash=sha256:63a42e131112072f912b66734088abc946c6bc42cb7454461a6dfdd2d09d3bae \ + --hash=sha256:681980fcc82b346ac83bc7d59e68ca1eba74ed9576f5d8584761d0c6c68c45d0 \ + --hash=sha256:788ea800618d20158166d09f60835aab50922ae1cfe3108c93fa72e173b7002d \ + --hash=sha256:7c534d782757710e39f3c9582214d7a28a0d68c86e78ee3df1d729cbc270e133 \ + --hash=sha256:7d41baea51ea00f9237f75af498577827493bca5e9b45bbd4e351543727e589a \ + --hash=sha256:8c843dd85857cd0d0da055489f448d032866120cfb094ce16205a1ff54d06353 \ + --hash=sha256:92989da161a7d18a7ad4baebc49289b2b77556d5a94916f90140ba26aecf6892 \ + --hash=sha256:944dc8747f640b3527649c566a5fc75ee0c15e80642ea2fdae4fe6378e1a9d4a \ + --hash=sha256:9d3cba9c4770cf76370fead8d2ae71bc474f3ef67ae4119e455d025726bcdc1c \ + --hash=sha256:a2b0e87f6b80790c929308ca0831d686f7a180feab684fe8cd4a4380bd96aaca \ + --hash=sha256:a3fc783d12202d1b064b6f125772d85f00e36e62eee6b2e415f56d8fd2d2e7b2 \ + --hash=sha256:a5cd9c5f138cb052130b40ad3b6976d2180c35348410995812678f4636bd8f94 \ + --hash=sha256:c6d4a92a87d238231564b5cbf27a8d1e46b7e62040c66b5ff33778a16b22295e \ + --hash=sha256:cac792b0bde1c86d8f832e513cd061b7e682181cc5a6d843d487e6c8ae9d5fcb \ + --hash=sha256:ccb0bc6b8437cfcb04459a02bc5ecc58bf161ea11659438945ab5a1392ee39fe \ + --hash=sha256:d15c4d4ba8a3ad284124384f769465bfba45512aaa4c81caea8a165f79fc042a \ + --hash=sha256:d249c9022ab13286b17bd66f30609e800c5f95efeecb06168990c7a66cecde6c \ + --hash=sha256:ddb43637b7749b88644df4319e1c36f767e7fb71a92bb9942558c8de2f1cc5f4 \ + --hash=sha256:e75ec625d8f8781ced690b7a2f5c2d138067711cf24bb8fb68c872c30c2fefe5 \ + --hash=sha256:e8ec2d5a7b798c84d6779e7ca5318fd928b8fe9dd021106d714dc2e77cdc7555 \ + --hash=sha256:e9b6ebc6c74b4318eaa4e51e520dc8b95ebc7b262953c3ecb24131104681f14e \ + --hash=sha256:e9c39962f749bd40c6e8c5063d498bd3749947888306ffedb99a0cab3a69f995 \ + --hash=sha256:efd54fe07f808e7e82f6a04f370457ca02e770c948f0e2f83345ccc7ec8bb829 \ + --hash=sha256:f25b402a6f1eed34af2af57603a9aadcf594dacaced1655e925eb317fba3f337 \ + --hash=sha256:f2fe4bc1b8fa4130563b94a3614db9ea17e92a455bed00e6a5d03eaff434b33c \ + --hash=sha256:f815cd3387bd88ca9eeabc357ce4bdf884eef0ab2949db0fbd5e0b69fe5ee422 # via - # poetry-dynamic-versioning - # uv-dynamic-versioning + # pyarrow + # uvloop +dunamai==1.26.1 \ + --hash=sha256:2727d939c5b4257cb01ea404372803b477f5176e5a347c43beaf89cd5072e853 \ + --hash=sha256:3b46007bd65b00b4824ead0a1aee365fd22d0ec2b9c219497d4fd48f52860c8b + # via uv-dynamic-versioning expandvars==1.1.2 \ --hash=sha256:6c5822b7b756a99a356b915dd1267f52ab8a4efaa135963bd7f4bd5d368f71d7 \ --hash=sha256:d1652fe4e61914f5b88ada93aaedb396446f55ae4621de45c8cb9f66e5712526 @@ -197,13 +354,12 @@ flit-core==3.12.0 \ # idna # jinja2 # markdown-it-py - # marshmallow # mdurl # mypy-extensions # packaging # pathspec # pyproject-metadata - # roman-numerals-py + # roman-numerals # sphinx # sphinxcontrib-applehelp # sphinxcontrib-devhelp @@ -211,23 +367,18 @@ flit-core==3.12.0 \ # sphinxcontrib-qthelp # sphinxcontrib-serializinghtml # tomli + # truststore # typing-extensions # wheel -gitdb==4.0.12 \ - --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ - --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf - # via gitpython -gitpython==3.1.45 \ - --hash=sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c \ - --hash=sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77 - # via pymilvus hatch-fancy-pypi-readme==25.1.0 \ --hash=sha256:9c58ed3dff90d51f43414ce37009ad1d5b0f08ffc9fc216998a06380f01c0045 \ --hash=sha256:ce0134c40d63d874ac48f48ccc678b8f3b62b8e50e9318520d2bffc752eedaf3 # via # attrs # httpcore + # httpcore2 # httpx + # httpx2 # jsonschema # pydantic hatch-vcs==0.4.0 \ @@ -247,9 +398,9 @@ hatch-vcs==0.5.0 \ # via # filelock # platformdirs -hatchling==1.27.0 \ - --hash=sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6 \ - --hash=sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b +hatchling==1.31.0 \ + --hash=sha256:6b48ad4068a482ed7239b3a8215bc55b47aad3345d58dfc94e553c5d2d46211b \ + --hash=sha256:aac80bec8b6fe35e8480f1c335be8910fa210a0e6f735a139be205dadcacb544 # via # annotated-types # atpublic @@ -263,16 +414,22 @@ hatchling==1.27.0 \ # hatch-fancy-pypi-readme # hatch-vcs # httpcore + # httpcore2 # httpx + # httpx2 + # ibis-framework # jsonschema # jsonschema-specifications # mcp + # mcp-types + # opentelemetry-api # platformdirs # pyarrow-hotfix # pydantic # pydantic-settings # pygments # python-multipart + # redis # referencing # scikit-build-core # starlette @@ -284,9 +441,168 @@ hatchling==1.27.0 \ jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via - # poetry-dynamic-versioning - # uv-dynamic-versioning + # via uv-dynamic-versioning +libcst==1.8.6 \ + --hash=sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30 \ + --hash=sha256:06fc56335a45d61b7c1b856bfab4587b84cfe31e9d6368f60bb3c9129d900f58 \ + --hash=sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d \ + --hash=sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a \ + --hash=sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9 \ + --hash=sha256:0cbe17067055829607c5ba4afa46bfa4d0dd554c0b5a583546e690b7367a29b6 \ + --hash=sha256:16cfe0cfca5fd840e1fb2c30afb628b023d3085b30c3484a79b61eae9d6fe7ba \ + --hash=sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105 \ + --hash=sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330 \ + --hash=sha256:203ec2a83f259baf686b9526268cd23d048d38be5589594ef143aee50a4faf7e \ + --hash=sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1 \ + --hash=sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786 \ + --hash=sha256:25fc7a1303cad7639ad45ec38c06789b4540b7258e9a108924aaa2c132af4aca \ + --hash=sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012 \ + --hash=sha256:351ab879c2fd20d9cb2844ed1ea3e617ed72854d3d1e2b0880ede9c3eea43ba8 \ + --hash=sha256:36473e47cb199b7e6531d653ee6ffed057de1d179301e6c67f651f3af0b499d6 \ + --hash=sha256:3649a813660fbffd7bc24d3f810b1f75ac98bd40d9d6f56d1f0ee38579021073 \ + --hash=sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86 \ + --hash=sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474 \ + --hash=sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8 \ + --hash=sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa \ + --hash=sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42 \ + --hash=sha256:455f49a93aea4070132c30ebb6c07c2dea0ba6c1fde5ffde59fc45dbb9cfbe4b \ + --hash=sha256:4d7bbdd35f3abdfb5ac5d1a674923572dab892b126a58da81ff2726102d6ec2e \ + --hash=sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196 \ + --hash=sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93 \ + --hash=sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58 \ + --hash=sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1 \ + --hash=sha256:59a7e388c57d21d63722018978a8ddba7b176e3a99bd34b9b84a576ed53f2978 \ + --hash=sha256:5dcaaebc835dfe5755bc85f9b186fb7e2895dda78e805e577fef1011d51d5a5c \ + --hash=sha256:6366ab2107425bf934b0c83311177f2a371bfc757ee8c6ad4a602d7cbcc2f363 \ + --hash=sha256:6421a930b028c5ef4a943b32a5a78b7f1bf15138214525a2088f11acbb7d3d64 \ + --hash=sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d \ + --hash=sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996 \ + --hash=sha256:6aa11df6c58812f731172b593fcb485d7ba09ccc3b52fea6c7f26a43377dc748 \ + --hash=sha256:6b23d14a7fc0addd9795795763af26b185deb7c456b1e7cc4d5228e69dab5ce8 \ + --hash=sha256:6cad63e3a26556b020b634d25a8703b605c0e0b491426b3e6b9e12ed20f09100 \ + --hash=sha256:6d8b67874f2188399a71a71731e1ba2d1a2c3173b7565d1cc7ffb32e8fbaba5b \ + --hash=sha256:72cca15800ffc00ba25788e4626189fe0bc5fe2a0c1cb4294bce2e4df21cc073 \ + --hash=sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb \ + --hash=sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4 \ + --hash=sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde \ + --hash=sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f \ + --hash=sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f \ + --hash=sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661 \ + --hash=sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1 \ + --hash=sha256:98fa1ca321c81fb1f02e5c43f956ca543968cc1a30b264fd8e0a2e1b0b0bf106 \ + --hash=sha256:a20c5182af04332cc94d8520792befda06d73daf2865e6dddc5161c72ea92cb9 \ + --hash=sha256:b0d8c364c44ae343937f474b2e492c1040df96d94530377c2f9263fb77096e4f \ + --hash=sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c \ + --hash=sha256:b6c1248cc62952a3a005792b10cdef2a4e130847be9c74f33a7d617486f7e532 \ + --hash=sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4 \ + --hash=sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9 \ + --hash=sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82 \ + --hash=sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7 \ + --hash=sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28 \ + --hash=sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf \ + --hash=sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e \ + --hash=sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5 \ + --hash=sha256:cb2679ef532f9fa5be5c5a283b6357cb6e9888a8dd889c4bb2b01845a29d8c0b \ + --hash=sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d \ + --hash=sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47 \ + --hash=sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09 \ + --hash=sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b \ + --hash=sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4 + # via pyarrow +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ @@ -378,32 +694,36 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -maturin==1.9.6 \ - --hash=sha256:0246202377c49449315305209f45c8ecef6e2d6bd27a04b5b6f1ab3e4ea47238 \ - --hash=sha256:1b39a5d82572c240d20d9e8be024d722dfb311d330c5e28ddeb615211755941a \ - --hash=sha256:26e3ab1a42a7145824210e9d763f6958f2c46afb1245ddd0bab7d78b1f59bb3f \ - --hash=sha256:2c2ae37144811d365509889ed7220b0598487f1278c2441829c3abf56cc6324a \ - --hash=sha256:5263dda3f71feef2e4122baf5c4620e4b3710dbb7f2121f85a337182de214369 \ - --hash=sha256:5c2252b0956bb331460ac750c805ddf0d9b44442449fc1f16e3b66941689d0bc \ - --hash=sha256:7ab827c6e8c022eb2e1e7fb6deede54549c8460b20ccc2e9268cc6e8cde957a8 \ - --hash=sha256:7f506eb358386d94d6ec3208c003130cf4b69cab26034fc0cbbf8bf83afa4c2e \ - --hash=sha256:7f53d3b1d8396d3fea3e1ee5fd37558bca5719090f3d194ba1c02b0b56327ae3 \ - --hash=sha256:ac02a30083553d2a781c10cd6f5480119bf6692fd177e743267406cad2ad198c \ - --hash=sha256:f2c58d29ebdd4346fd004e6be213d071fdd94a77a16aa91474a21a4f9dbf6309 \ - --hash=sha256:f2d6984ab690af509f525dbd2b130714207c06ebb14a5814edbe1e42b17ae0de \ - --hash=sha256:f5bac167700fbb6f8c8ed1a97b494522554b4432d7578e11403b894b6a91d99f \ - --hash=sha256:fe78262c2800c92f67d1ce3c0f6463f958a692cc67bfb572e5dbf5b4b696a8ba +maturin==1.14.1 \ + --hash=sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894 \ + --hash=sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0 \ + --hash=sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd \ + --hash=sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8 \ + --hash=sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4 \ + --hash=sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666 \ + --hash=sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df \ + --hash=sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224 \ + --hash=sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69 \ + --hash=sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65 \ + --hash=sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d \ + --hash=sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e \ + --hash=sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c \ + --hash=sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed # via + # ast-serialize # cryptography + # orjson # pydantic-core # rpds-py # watchfiles meson-python==0.15.0 \ --hash=sha256:3ae38253ff02b2e947a05e362a2eaf5a9a09d133c5666b4123399ee5fbf2e591 \ --hash=sha256:fddb73eecd49e89c1c41c87937cd89c2d0b65a1c63ba28238681d4bd9484d26f - # via - # numpy - # pandas + # via pandas +meson-python==0.20.0 \ + --hash=sha256:6a744cf0c09e76ecbdc58cfa374b48c8902dac1b74479628238634efbf36aec9 \ + --hash=sha256:6d9726ae6cd37e22f210c74b364b30180a68c20442e97ff09f3c566a414af738 + # via numpy meson==1.7.1 \ --hash=sha256:155780a5be87f6dd7f427ad8bcbf0f2b2c5f62ee5fdacca7caa9de8439a24b89 \ --hash=sha256:6d9cbc9ce87a70243c75e4cc668ee3f206ab50b184beb0a08ece948112f19bd7 @@ -414,304 +734,384 @@ mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf +numpy==2.4.6 \ + --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ + --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ + --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ + --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ + --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ + --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ + --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ + --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ + --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ + --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ + --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ + --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ + --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ + --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ + --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ + --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ + --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ + --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ + --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ + --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ + --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ + --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ + --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ + --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ + --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ + --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ + --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 # via # pandas # pyarrow -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via - # dunamai # hatchling + # meson-python # pyproject-metadata # scikit-build-core + # setuptools-git-versioning # setuptools-scm -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # vcs-versioning + # wheel +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via # hatchling # mypy # scikit-build-core -pdm-backend==2.4.5 \ - --hash=sha256:56c019c440308adad5d057c08cbb777e65f43b991a3b0920749781258972fe5b \ - --hash=sha256:7f6c780b529baaf88947e12203af46b3ed45cff719b04c870b186ba9cdc2ddab +pdm-backend==2.4.9 \ + --hash=sha256:8d1064751dadb0c40b1026b46826a448d85c8228a564985c62fb53d4aba538a1 \ + --hash=sha256:c41a852ccbf4b567b033db9b2ae78660d7a4ea49307719959ab88a01f0aabb2e # via + # annotated-doc # fastapi # typer -pkgconfig==1.5.5 \ - --hash=sha256:d20023bbeb42ee6d428a0fac6e0904631f545985a10cdd71a20aa58bc47a4209 \ - --hash=sha256:deb4163ef11f75b520d822d9505c1f462761b4309b1bb713d08689759ea8b899 +pkgconfig==1.6.0 \ + --hash=sha256:4a5a6631ce937fafac457104a40d558785a658bbdca5c49b6295bc3fd651907f \ + --hash=sha256:98e71754855e9563838d952a160eb577edabb57782e49853edb5381927e6bea1 # via aiohttp pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via hatchling -poetry-core==1.9.1 \ - --hash=sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0 \ - --hash=sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8 +poetry-core==2.4.1 \ + --hash=sha256:89dceb6c10e9c6d8650a16183400e3c9ff9ddee13b0a81023b5575334a2b3744 \ + --hash=sha256:acf06f9537cd2625bdaec926d95d90b557ba15353bc71d27a3a8a441042b5316 # via + # aiohappyeyeballs # dunamai - # ibis-framework - # ibis-substrait # pkgconfig - # poetry-dynamic-versioning # rich - # rsa # tomlkit -poetry-core==2.2.1 \ - --hash=sha256:97e50d8593c8729d3f49364b428583e044087ee3def1e010c6496db76bd65ac5 \ - --hash=sha256:bdfce710edc10bfcf9ab35041605c480829be4ab23f5bc01202cfe5db8f125ab - # via aiohappyeyeballs -poetry-dynamic-versioning==1.9.1 \ - --hash=sha256:65a0c814e6d30d4807734a3c34edf261fd7cc3b340dbd23b6a33ee41f7d0b547 \ - --hash=sha256:d6e7b9df817aa2ca4946cd695c6c89e1379d2e6c640f008a9b6170d081a9da48 - # via ibis-framework -pybind11==3.0.1 \ - --hash=sha256:9c0f40056a016da59bab516efb523089139fcc6f2ba7e4930854c61efb932051 \ - --hash=sha256:aa8f0aa6e0a94d3b64adfc38f560f33f15e589be2175e103c0a33c6bce55ee89 +pybind11-global==3.0.4 \ + --hash=sha256:95b693c3d646c6b7217a97156a36b6d40305505d4a5a728082da6fe75ada29f5 \ + --hash=sha256:a73e2ebd29f4ee5d7c754b495d555cd703f2cd26c84abe360ee56411dcd7834d + # via pybind11 +pybind11==3.0.4 \ + --hash=sha256:3286b59c8a774b9ee650169302dd5a4eedc30a8617905a0560dd8ee44775130c \ + --hash=sha256:961720ee652da51d531b7b2451a6bd2bc042b0106e6d9baa48ecb7d58034ce63 # via duckdb -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pyproject-metadata==0.9.1 \ - --hash=sha256:b8b2253dd1b7062b78cf949a115f02ba7fa4114aabe63fa10528e9e1a954a816 \ - --hash=sha256:ee5efde548c3ed9b75a354fc319d5afd25e9585fa918a34f62f904cc731973ad +pyproject-metadata==0.12.1 \ + --hash=sha256:8809a4df6fe08279b39a8890669506ed3158e0617855ac9aff098fcbe772ae4c \ + --hash=sha256:f7162d580a96386a8eb096da06215f981f547d1490f03055ef99e323bc2da427 # via meson-python -scikit-build-core==0.11.6 \ - --hash=sha256:5982ccd839735be99cfd3b92a8847c6c196692f476c215da84b79d2ad12f9f1b \ - --hash=sha256:ce6d8fe64e6b4c759ea0fb95d2f8a68f60d2df31c2989838633b8ec930736360 +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via libcst +scikit-build-core==1.0.3 \ + --hash=sha256:a4d7a05978ee37975c37743510c8991e2debce7ef83afb0a07c0c576fd4f16e8 \ + --hash=sha256:ae95427b7d3c14a6cf8bbfd4d901f6138ab64c99e20cbe8ea7d75cd26093f085 # via + # duckdb # patchelf + # pyarrow # pybind11 + # pybind11-global semantic-version==2.10.0 \ --hash=sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c \ --hash=sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177 # via setuptools-rust -setuptools-rust==1.12.0 \ - --hash=sha256:7e7db90547f224a835b45f5ad90c983340828a345554a9a660bdb2de8605dcdd \ - --hash=sha256:d94a93f0c97751c17014565f07bdc324bee45d396cd1bba83d8e7af92b945f0c - # via maturin -setuptools-scm==7.1.0 \ - --hash=sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27 \ - --hash=sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e - # via python-dateutil -setuptools-scm==8.3.1 \ - --hash=sha256:332ca0d43791b818b841213e76b1971b7711a960761c5bea5fc5cdb5196fbce3 \ - --hash=sha256:3d555e92b75dacd037d32bafdf94f97af51ea29ae8c7b234cf94b7a5bd242a63 +setuptools-git-versioning==3.1.0 \ + --hash=sha256:3a68f3fd58a2a5e86b0792435cfa9d8e569ab60ee5e4c29228c09da9b637bf18 \ + --hash=sha256:612dfcf184addac9e1c2216f4f229724b2390e5bf613fb925ae80b84f2529172 + # via toolz +setuptools-rust==1.13.0 \ + --hash=sha256:666439c4d90e968bb625bcf87ec0aa02b4f49e599d2f5dc255c633a04e8eca99 \ + --hash=sha256:f2afcf4baeee689910ce49cfa8aad4e08cce72f417449bcc32891b8664fdc726 # via - # hatch-vcs - # urllib3 -setuptools-scm==9.2.1 \ - --hash=sha256:2f5b21e45205e47ed3976e58bc29c38f49bb9b6f07c625c7fd2823871694b244 \ - --hash=sha256:84c9b3f39ffb77ba00dd92a06e025e4f66dc69ab8df1c2a225a0678e76311f8d + # libcst + # maturin +setuptools-scm==10.2.1 \ + --hash=sha256:4fa7dd82cf8c800df59c9a288c90299b1657ff1ecfc3f5cc00287c5dbf5e27a9 \ + --hash=sha256:b7c82f4102d389ee57dc66ccdb4f9b4bca3c40ba83b43f1f63d68ccd72db2580 # via # anyio + # cachetools + # dask # duckdb # hatch-vcs - # httpx-sse # importlib-metadata + # libcst # pluggy # pyarrow # pybindgen - # pymilvus # setuptools-rust - # sniffio # sqlglot - # substrait # tabulate # tenacity # tqdm # typeguard - # ujson + # urllib3 # zipp -smmap==5.0.2 \ - --hash=sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5 \ - --hash=sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e - # via gitdb -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 +setuptools-scm==7.1.0 \ + --hash=sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27 \ + --hash=sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e + # via python-dateutil +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via fastapi-mcp -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via - # poetry-dynamic-versioning - # uv-dynamic-versioning -trove-classifiers==2025.9.11.17 \ - --hash=sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd \ - --hash=sha256:931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 + # via uv-dynamic-versioning +trove-classifiers==2026.6.1.19 \ + --hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \ + --hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745 # via hatchling types-psutil==7.0.0.20250218 \ --hash=sha256:1447a30c282aafefcf8941ece854e1100eee7b0296a9d9be9977292f0269b121 \ --hash=sha256:1e642cdafe837b240295b23b1cbd4691d80b08a07d29932143cbbae30eb0db9c # via mypy -types-setuptools==80.9.0.20250822 \ - --hash=sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965 \ - --hash=sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3 +types-setuptools==83.0.0.20260724 \ + --hash=sha256:eed1a91e1ed9d8ec9f3e71908e5bb17272e41e0852d37d1485ecd9cd33165e1a \ + --hash=sha256:fe2801176b667f1e8209f8571b0839ca6c34318d743825aeacf3f29970c8d46f # via mypy -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via # mypy - # pydantic-core # setuptools-scm -uv-dynamic-versioning==0.11.2 \ - --hash=sha256:4c785ea02e00b93896d015fd3613c451735af1715a7f5f52e8e4e1d76e6ec3c5 \ - --hash=sha256:563ab4b7a93904eb8fb8fd9f2959dad853d62baa71f3cdb0233acf8a749d512f - # via mcp +uv-dynamic-versioning==0.14.0 \ + --hash=sha256:574fbc07e87ace45c01d55967ad3b864871257b98ff5b8ac87c261227ac8db5b \ + --hash=sha256:e087c346a786e98d41292ac2315180fb700cedfb30565fc973d64ce11a112387 + # via + # httpcore2 + # httpx2 + # mcp + # mcp-types +vcs-versioning==2.2.3 \ + --hash=sha256:4f7873af76f5cc24e701608354f376f5eb70b317d663787ace57a7edd7a27506 \ + --hash=sha256:c21e284aa98c8269996791a19d6173baf953cf9996ef42ec3ebcbdc151e18b9b + # via setuptools-scm versioneer==0.29 \ --hash=sha256:0f1a137bb5d6811e96a79bb0486798aeae9b9c6efc24b389659cebb0ee396cb9 \ --hash=sha256:5ab283b9857211d61b53318b7c792cf68e798e765ee17c27ade9f6c924235731 # via - # dask # pandas # partd -wheel==0.45.1 \ - --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ - --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 +wheel==0.47.0 \ + --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \ + --hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3 # via # async-timeout - # cachetools # google-crc32c - # httpx-sse + # grpcio + # grpcio-health-checking + # grpcio-reflection + # grpcio-status + # libcst # meson # mmh3 # pandas # psycopg # psycopg-c # psycopg-pool - # pymilvus + # pycparser # python-dateutil + # setuptools-git-versioning # shellingham # snowflake-connector-python - # tabulate - # tqdm # tzdata # uvloop + # wrapt # The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 # via # aiobotocore # aiohttp @@ -726,20 +1126,22 @@ setuptools==80.9.0 \ # cryptography # dask # dill - # duckdb # frozenlist - # gitpython # google-api-core # google-cloud-bigquery # google-crc32c # googleapis-common-protos # greenlet # grpc-google-iam-v1 + # grpcio + # grpcio-health-checking + # grpcio-reflection + # grpcio-status # gunicorn # httptools - # httpx-sse - # ibis-substrait # importlib-metadata + # libcst + # librt # markupsafe # maturin # meson @@ -751,40 +1153,50 @@ setuptools==80.9.0 \ # pathspec # pluggy # prometheus-client - # propcache # proto-plus # psutil # psycopg - # psycopg-c # psycopg-pool - # pyarrow # pyasn1 # pyasn1-modules + # pycparser # pyjwt - # pymilvus # pymysql - # python-dateutil + # python-dotenv # pyyaml + # requests + # setuptools-git-versioning # setuptools-rust # setuptools-scm # shellingham - # sniffio + # snowballstemmer # snowflake-connector-python # sqlalchemy + # sqlglot # sse-starlette - # substrait # tabulate # tenacity + # toolz # tqdm # trove-classifiers # typeguard - # types-pymysql - # types-setuptools # tzdata - # ujson # uvloop + # vcs-versioning # versioneer # websockets # wrapt # yarl # zipp +setuptools==80.3.1 \ + --hash=sha256:31e2c58dbb67c99c289f51c16d899afedae292b978f8051efaf6262d8212f927 \ + --hash=sha256:ea8e00d7992054c4c592aeb892f6ad51fe1b4d90cc6947cc45c45717c40ec537 + # via psycopg-c +setuptools==83.0.0 \ + --hash=sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef \ + --hash=sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3 + # via + # propcache + # python-dateutil + # types-pymysql + # types-setuptools diff --git a/sdk/python/requirements/py3.11-minimal-sdist-requirements.txt b/sdk/python/requirements/py3.11-minimal-sdist-requirements.txt index 6f5e8dc6bb2..5648439b025 100644 --- a/sdk/python/requirements/py3.11-minimal-sdist-requirements.txt +++ b/sdk/python/requirements/py3.11-minimal-sdist-requirements.txt @@ -1,138 +1,139 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.11 --no-strip-extras setup.py --extra minimal-sdist-build --no-emit-package milvus-lite --generate-hashes --output-file sdk/python/requirements/py3.11-minimal-sdist-requirements.txt -aiobotocore==2.23.1 \ - --hash=sha256:a59f2a78629b97d52f10936b79c73de64e481a8c44a62c1871f088df6c1afc4f \ - --hash=sha256:d81c54d2eae2406ea9a473fea518fed580cf37bc4fc51ce43ba81546e5305114 - # via feast (setup.py) -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +# uv pip compile -p 3.11 --no-strip-extras pyproject.toml --extra minimal-sdist-build --no-emit-package milvus-lite --no-emit-package pymilvus --no-emit-package faiss-cpu --generate-hashes --output-file sdk/python/requirements/py3.11-minimal-sdist-requirements.txt +aiobotocore==3.8.0 \ + --hash=sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670 \ + --hash=sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + # via feast (pyproject.toml) +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.0 \ - --hash=sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6 \ - --hash=sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a \ - --hash=sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e \ - --hash=sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971 \ - --hash=sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54 \ - --hash=sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2 \ - --hash=sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96 \ - --hash=sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469 \ - --hash=sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26 \ - --hash=sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0 \ - --hash=sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29 \ - --hash=sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885 \ - --hash=sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579 \ - --hash=sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a \ - --hash=sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1 \ - --hash=sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b \ - --hash=sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852 \ - --hash=sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865 \ - --hash=sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad \ - --hash=sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092 \ - --hash=sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7 \ - --hash=sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b \ - --hash=sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8 \ - --hash=sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820 \ - --hash=sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd \ - --hash=sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee \ - --hash=sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4 \ - --hash=sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67 \ - --hash=sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d \ - --hash=sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1 \ - --hash=sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d \ - --hash=sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114 \ - --hash=sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1 \ - --hash=sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755 \ - --hash=sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04 \ - --hash=sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca \ - --hash=sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1 \ - --hash=sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae \ - --hash=sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793 \ - --hash=sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e \ - --hash=sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a \ - --hash=sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8 \ - --hash=sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602 \ - --hash=sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778 \ - --hash=sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f \ - --hash=sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49 \ - --hash=sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77 \ - --hash=sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7 \ - --hash=sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f \ - --hash=sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0 \ - --hash=sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519 \ - --hash=sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d \ - --hash=sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370 \ - --hash=sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7 \ - --hash=sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247 \ - --hash=sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d \ - --hash=sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80 \ - --hash=sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9 \ - --hash=sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c \ - --hash=sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d \ - --hash=sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020 \ - --hash=sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504 \ - --hash=sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca \ - --hash=sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731 \ - --hash=sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd \ - --hash=sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6 \ - --hash=sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21 \ - --hash=sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1 \ - --hash=sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620 \ - --hash=sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64 \ - --hash=sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8 \ - --hash=sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3 \ - --hash=sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5 \ - --hash=sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985 \ - --hash=sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813 \ - --hash=sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed \ - --hash=sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20 \ - --hash=sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03 \ - --hash=sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702 \ - --hash=sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6 \ - --hash=sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed \ - --hash=sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353 \ - --hash=sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108 \ - --hash=sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f \ - --hash=sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409 \ - --hash=sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7 \ - --hash=sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3 \ - --hash=sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9 \ - --hash=sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba \ - --hash=sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe \ - --hash=sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c \ - --hash=sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861 \ - --hash=sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a \ - --hash=sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0 \ - --hash=sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390 \ - --hash=sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3 \ - --hash=sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90 \ - --hash=sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802 \ - --hash=sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163 \ - --hash=sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d \ - --hash=sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430 \ - --hash=sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a \ - --hash=sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b \ - --hash=sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6 \ - --hash=sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b \ - --hash=sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657 \ - --hash=sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c \ - --hash=sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93 \ - --hash=sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f \ - --hash=sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82 \ - --hash=sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31 \ - --hash=sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc \ - --hash=sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52 \ - --hash=sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c \ - --hash=sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a \ - --hash=sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52 \ - --hash=sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7 \ - --hash=sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b \ - --hash=sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147 \ - --hash=sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4 - # via aiobotocore -aioitertools==0.12.0 \ - --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \ - --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 + # via + # aiobotocore + # kubernetes +aioitertools==0.13.0 \ + --hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \ + --hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c # via aiobotocore aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ @@ -142,15 +143,22 @@ alabaster==1.0.0 \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b # via sphinx -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via + # fastapi + # typer +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # httpx + # httpx2 # mcp # sse-starlette # starlette @@ -159,321 +167,363 @@ asn1crypto==1.5.1 \ --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 # via snowflake-connector-python +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy async-timeout==5.0.1 \ --hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3 # via redis -atpublic==5.1 \ - --hash=sha256:135783dbd887fbddb6ef032d104da70c124f2b44b9e2d79df07b9da5334825e3 \ - --hash=sha256:abc1f4b3dbdd841cc3539e4b5e4f3ad41d658359de704e30cb36da4d4e9d3022 +atpublic==7.0.0 \ + --hash=sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e \ + --hash=sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b # via ibis-framework -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via + # feast (pyproject.toml) # aiohttp # jsonschema # referencing -babel==2.17.0 \ - --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ - --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 +babel==2.18.0 \ + --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ + --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 # via sphinx -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -boto3==1.38.27 \ - --hash=sha256:94bd7fdd92d5701b362d4df100d21e28f8307a67ff56b6a8b0398119cf22f859 \ - --hash=sha256:95f5fe688795303a8a15e8b7e7f255cadab35eae459d00cc281a4fd77252ea80 - # via - # feast (setup.py) +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +boto3==1.43.46 \ + --hash=sha256:66c0d943b049a46a492ec4ec2ebe73c930b1842c7137bee83aad6d93e95d4d96 \ + --hash=sha256:69453e2c1bcb9fd9806527ab99950cacfc2826cb0dce9a3a0414d19270c06c3c + # via + # feast (pyproject.toml) # snowflake-connector-python -botocore==1.38.46 \ - --hash=sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e \ - --hash=sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b +botocore==1.43.46 \ + --hash=sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533 \ + --hash=sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 # via # aiobotocore # boto3 # s3transfer # snowflake-connector-python -cachetools==6.2.1 \ - --hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \ - --hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201 - # via google-auth +cachetools==7.1.6 \ + --hash=sha256:2c12e255780330af28b91bb7fb96cce4c766f04e38396b9a24510190a5827096 \ + --hash=sha256:c7a79e7f30ba9943c1cefd08cc36f006aaae086e017af9166f1d59d6170c47e1 + # via pymilvus calver==2025.3.31 \ --hash=sha256:07511edf5e7fa75ae97445c8c5921240e0fe62937289a3ebe6963eddd3c691b6 \ --hash=sha256:255d1a70bba8f97dc1eee3af4240ed35980508da69257feef94c79e5c6545fc7 - # via feast (setup.py) -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 + # via feast (pyproject.toml) +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # httpcore # httpx # kubernetes # requests # snowflake-connector-python -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b - # via - # feast (setup.py) +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f + # via + # feast (pyproject.toml) # cryptography - # snowflake-connector-python -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via # requests # snowflake-connector-python -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask - # typer # uvicorn -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a # via dask colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via feast (setup.py) -cryptography==46.0.0 \ - --hash=sha256:07a1be54f995ce14740bf8bbe1cc35f7a37760f992f73cf9f98a2a60b9b97419 \ - --hash=sha256:0f58183453032727a65e6605240e7a3824fd1d6a7e75d2b537e280286ab79a52 \ - --hash=sha256:16b5ac72a965ec9d1e34d9417dbce235d45fa04dac28634384e3ce40dfc66495 \ - --hash=sha256:1b4fba84166d906a22027f0d958e42f3a4dbbb19c28ea71f0fb7812380b04e3c \ - --hash=sha256:1d2073313324226fd846e6b5fc340ed02d43fd7478f584741bd6b791c33c9fee \ - --hash=sha256:249c41f2bbfa026615e7bdca47e4a66135baa81b08509ab240a2e666f6af5966 \ - --hash=sha256:274f8b2eb3616709f437326185eb563eb4e5813d01ebe2029b61bfe7d9995fbb \ - --hash=sha256:2fc30be952dd4334801d345d134c9ef0e9ccbaa8c3e1bc18925cbc4247b3e29c \ - --hash=sha256:32670ca085150ff36b438c17f2dfc54146fe4a074ebf0a76d72fb1b419a974bc \ - --hash=sha256:35aa1a44bd3e0efc3ef09cf924b3a0e2a57eda84074556f4506af2d294076685 \ - --hash=sha256:3738f50215211cee1974193a1809348d33893696ce119968932ea117bcbc9b1d \ - --hash=sha256:378eff89b040cbce6169528f130ee75dceeb97eef396a801daec03b696434f06 \ - --hash=sha256:399ef4c9be67f3902e5ca1d80e64b04498f8b56c19e1bc8d0825050ea5290410 \ - --hash=sha256:40ee4ce3c34acaa5bc347615ec452c74ae8ff7db973a98c97c62293120f668c6 \ - --hash=sha256:4bc257c2d5d865ed37d0bd7c500baa71f939a7952c424f28632298d80ccd5ec1 \ - --hash=sha256:4f70cbade61a16f5e238c4b0eb4e258d177a2fcb59aa0aae1236594f7b0ae338 \ - --hash=sha256:523153480d7575a169933f083eb47b1edd5fef45d87b026737de74ffeb300f69 \ - --hash=sha256:6460866a92143a24e3ed68eaeb6e98d0cedd85d7d9a8ab1fc293ec91850b1b38 \ - --hash=sha256:65e9117ebed5b16b28154ed36b164c20021f3a480e9cbb4b4a2a59b95e74c25d \ - --hash=sha256:6c39fd5cd9b7526afa69d64b5e5645a06e1b904f342584b3885254400b63f1b3 \ - --hash=sha256:6d8945bc120dcd90ae39aa841afddaeafc5f2e832809dc54fb906e3db829dfdc \ - --hash=sha256:75d2ddde8f1766ab2db48ed7f2aa3797aeb491ea8dfe9b4c074201aec00f5c16 \ - --hash=sha256:77e3bd53c9c189cea361bc18ceb173959f8b2dd8f8d984ae118e9ac641410252 \ - --hash=sha256:7f3f88df0c9b248dcc2e76124f9140621aca187ccc396b87bc363f890acf3a30 \ - --hash=sha256:80a548a5862d6912a45557a101092cd6c64ae1475b82cef50ee305d14a75f598 \ - --hash=sha256:834af45296083d892e23430e3b11df77e2ac5c042caede1da29c9bf59016f4d2 \ - --hash=sha256:83af84ebe7b6e9b6de05050c79f8cc0173c864ce747b53abce6a11e940efdc0d \ - --hash=sha256:88c09da8a94ac27798f6b62de6968ac78bb94805b5d272dbcfd5fdc8c566999f \ - --hash=sha256:8e8b222eb54e3e7d3743a7c2b1f7fa7df7a9add790307bb34327c88ec85fe087 \ - --hash=sha256:91585fc9e696abd7b3e48a463a20dda1a5c0eeeca4ba60fa4205a79527694390 \ - --hash=sha256:99f64a6d15f19f3afd78720ad2978f6d8d4c68cd4eb600fab82ab1a7c2071dca \ - --hash=sha256:9aa85222f03fdb30defabc7a9e1e3d4ec76eb74ea9fe1504b2800844f9c98440 \ - --hash=sha256:ab3a14cecc741c8c03ad0ad46dfbf18de25218551931a23bca2731d46c706d83 \ - --hash=sha256:b8e7db4ce0b7297e88f3d02e6ee9a39382e0efaf1e8974ad353120a2b5a57ef7 \ - --hash=sha256:bbaa5eef3c19c66613317dc61e211b48d5f550db009c45e1c28b59d5a9b7812a \ - --hash=sha256:be7479f9504bfb46628544ec7cb4637fe6af8b70445d4455fbb9c395ad9b7290 \ - --hash=sha256:bf1961037309ee0bdf874ccba9820b1c2f720c2016895c44d8eb2316226c1ad5 \ - --hash=sha256:c1f6ccd6f2eef3b2eb52837f0463e853501e45a916b3fc42e5d93cf244a4b97b \ - --hash=sha256:c3648d6a5878fd1c9a22b1d43fa75efc069d5f54de12df95c638ae7ba88701d0 \ - --hash=sha256:c39f0947d50f74b1b3523cec3931315072646286fb462995eb998f8136779319 \ - --hash=sha256:c3cd09b1490c1509bf3892bde9cef729795fae4a2fee0621f19be3321beca7e4 \ - --hash=sha256:c457ad3f151d5fb380be99425b286167b358f76d97ad18b188b68097193ed95a \ - --hash=sha256:c9c4121f9a41cc3d02164541d986f59be31548ad355a5c96ac50703003c50fb7 \ - --hash=sha256:d14eaf1569d6252280516bedaffdd65267428cdbc3a8c2d6de63753cf0863d5e \ - --hash=sha256:d1eccae15d5c28c74b2bea228775c63ac5b6c36eedb574e002440c0bc28750d3 \ - --hash=sha256:d349af4d76a93562f1dce4d983a4a34d01cb22b48635b0d2a0b8372cdb4a8136 \ - --hash=sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0 \ - --hash=sha256:da7f93551d39d462263b6b5c9056c49f780b9200bf9fc2656d7c88c7bdb9b363 \ - --hash=sha256:df932ac70388be034b2e046e34d636245d5eeb8140db24a6b4c2268cd2073270 \ - --hash=sha256:f09a3a108223e319168b7557810596631a8cb864657b0c16ed7a6017f0be9433 \ - --hash=sha256:f85e6a7d42ad60024fa1347b1d4ef82c4df517a4deb7f829d301f1a92ded038c \ - --hash=sha256:f9aaf2a91302e1490c068d2f3af7df4137ac2b36600f5bd26e53d9ec320412d3 \ - --hash=sha256:f9f85d9cf88e3ba2b2b6da3c2310d1cf75bdf04a5bc1a2e972603054f82c4dd5 \ - --hash=sha256:fe9ff1139b2b1f59a5a0b538bbd950f8660a39624bbe10cf3640d17574f973bb - # via + # via feast (pyproject.toml) +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b + # via + # google-auth + # pyjwt # pyopenssl # snowflake-connector-python cython==3.0.12 \ @@ -541,109 +591,84 @@ cython==3.0.12 \ --hash=sha256:fe030d4a00afb2844f5f70896b7f2a1a0d7da09bf3aa3d884cbe5f73fff5d310 \ --hash=sha256:feb86122a823937cc06e4c029d80ff69f082ebb0b959ab52a5af6cdd271c5dc3 \ --hash=sha256:ff5c0b6a65b08117d0534941d404833d516dac422eee88c6b4fd55feb409a5ed - # via feast (setup.py) -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) -db-dtypes==1.4.3 \ - --hash=sha256:a1c92b819af947fae1701d80a71f2a0eac08f825ca52cf0c68aeba80577ae966 \ - --hash=sha256:d3cb08c32939d24e05501f17694be8c1c54cfb4620d61fb56fb311e8c3d8885e + # via feast (pyproject.toml) +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) +db-dtypes==1.7.1 \ + --hash=sha256:450b94cd010cfa6d4d13cca2593009eba4fbe15516084c332cde1a47bbce0625 \ + --hash=sha256:75adf3bdf3174a6474ea4162d9a345ea0b3102ec4fd603e69ec26b6e8a99145a # via # google-cloud-bigquery # pandas-gbq dill==0.3.9 \ --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c - # via feast (setup.py) -docutils==0.21.2 \ - --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ - --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 + # via feast (pyproject.toml) +docutils==0.22.4 \ + --hash=sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968 \ + --hash=sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de # via sphinx -duckdb==1.1.3 \ - --hash=sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586 \ - --hash=sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd \ - --hash=sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e \ - --hash=sha256:09c68522c30fc38fc972b8a75e9201616b96ae6da3444585f14cf0d116008c95 \ - --hash=sha256:0a55169d2d2e2e88077d91d4875104b58de45eff6a17a59c7dc41562c73df4be \ - --hash=sha256:0ba6baa0af33ded836b388b09433a69b8bec00263247f6bf0a05c65c897108d3 \ - --hash=sha256:183ac743f21c6a4d6adfd02b69013d5fd78e5e2cd2b4db023bc8a95457d4bc5d \ - --hash=sha256:1aa3abec8e8995a03ff1a904b0e66282d19919f562dd0a1de02f23169eeec461 \ - --hash=sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce \ - --hash=sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24 \ - --hash=sha256:2141c6b28162199999075d6031b5d63efeb97c1e68fb3d797279d31c65676269 \ - --hash=sha256:252d9b17d354beb9057098d4e5d5698e091a4f4a0d38157daeea5fc0ec161670 \ - --hash=sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9 \ - --hash=sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3 \ - --hash=sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289 \ - --hash=sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1 \ - --hash=sha256:4f0e2e5a6f5a53b79aee20856c027046fba1d73ada6178ed8467f53c3877d5e0 \ - --hash=sha256:51c6d79e05b4a0933672b1cacd6338f882158f45ef9903aef350c4427d9fc898 \ - --hash=sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8 \ - --hash=sha256:5ace6e4b1873afdd38bd6cc8fcf90310fb2d454f29c39a61d0c0cf1a24ad6c8d \ - --hash=sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff \ - --hash=sha256:6411e21a2128d478efbd023f2bdff12464d146f92bc3e9c49247240448ace5a6 \ - --hash=sha256:647f17bd126170d96a38a9a6f25fca47ebb0261e5e44881e3782989033c94686 \ - --hash=sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c \ - --hash=sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31 \ - --hash=sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7 \ - --hash=sha256:80158f4c7c7ada46245837d5b6869a336bbaa28436fbb0537663fa324a2750cd \ - --hash=sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898 \ - --hash=sha256:8ee97ec337794c162c0638dda3b4a30a483d0587deda22d45e1909036ff0b739 \ - --hash=sha256:911d58c22645bfca4a5a049ff53a0afd1537bc18fedb13bc440b2e5af3c46148 \ - --hash=sha256:9c619e4849837c8c83666f2cd5c6c031300cd2601e9564b47aa5de458ff6e69d \ - --hash=sha256:9d0767ada9f06faa5afcf63eb7ba1befaccfbcfdac5ff86f0168c673dd1f47aa \ - --hash=sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4 \ - --hash=sha256:a1f83c7217c188b7ab42e6a0963f42070d9aed114f6200e3c923c8899c090f16 \ - --hash=sha256:a1fa0c502f257fa9caca60b8b1478ec0f3295f34bb2efdc10776fc731b8a6c5f \ - --hash=sha256:a30dd599b8090ea6eafdfb5a9f1b872d78bac318b6914ada2d35c7974d643640 \ - --hash=sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38 \ - --hash=sha256:a4748635875fc3c19a7320a6ae7410f9295557450c0ebab6d6712de12640929a \ - --hash=sha256:b74e121ab65dbec5290f33ca92301e3a4e81797966c8d9feef6efdf05fc6dafd \ - --hash=sha256:c443d3d502335e69fc1e35295fcfd1108f72cb984af54c536adfd7875e79cee5 \ - --hash=sha256:c5336939d83837af52731e02b6a78a446794078590aa71fd400eb17f083dda3e \ - --hash=sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378 \ - --hash=sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9 \ - --hash=sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee \ - --hash=sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32 \ - --hash=sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537 \ - --hash=sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989 \ - --hash=sha256:ecb1dc9062c1cc4d2d88a5e5cd8cc72af7818ab5a3c0f796ef0ffd60cfd3efb4 \ - --hash=sha256:eeacb598120040e9591f5a4edecad7080853aa8ac27e62d280f151f8c862afa3 \ - --hash=sha256:f549af9f7416573ee48db1cf8c9d27aeed245cb015f4b4f975289418c6cf7320 \ - --hash=sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3 \ - --hash=sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b +duckdb==1.5.5 \ + --hash=sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053 \ + --hash=sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4 \ + --hash=sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c \ + --hash=sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a \ + --hash=sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528 \ + --hash=sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c \ + --hash=sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282 \ + --hash=sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f \ + --hash=sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7 \ + --hash=sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f \ + --hash=sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c \ + --hash=sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb \ + --hash=sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433 \ + --hash=sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8 \ + --hash=sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28 \ + --hash=sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238 \ + --hash=sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece \ + --hash=sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1 \ + --hash=sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151 \ + --hash=sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2 \ + --hash=sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353 \ + --hash=sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc \ + --hash=sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079 \ + --hash=sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a \ + --hash=sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6 \ + --hash=sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe \ + --hash=sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de \ + --hash=sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0 \ + --hash=sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e \ + --hash=sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f \ + --hash=sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf \ + --hash=sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae \ + --hash=sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6 \ + --hash=sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe \ + --hash=sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366 # via ibis-framework -dunamai==1.25.0 \ - --hash=sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab \ - --hash=sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1 - # via poetry-dynamic-versioning durationpy==0.10 \ --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 # via kubernetes -environs==9.5.0 \ - --hash=sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124 \ - --hash=sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9 - # via pymilvus -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp fastapi-mcp==0.4.0 \ --hash=sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec \ --hash=sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d - # via feast (setup.py) -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 + # via feast (pyproject.toml) +filelock==3.32.0 \ + --hash=sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402 \ + --hash=sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3 # via snowflake-connector-python flit-core==3.12.0 \ --hash=sha256:18f63100d6f94385c6ed57a72073443e1a71a4acb4339491615d0f16d6ff01b2 \ --hash=sha256:e7a0304069ea895172e3c7bb703292e992c5d1555dd1233ab7b5621b5b69e62c - # via feast (setup.py) + # via feast (pyproject.toml) frozenlist==1.8.0 \ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ @@ -778,17 +803,17 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -fsspec==2024.9.0 \ - --hash=sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8 \ - --hash=sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b +fsspec==2026.7.0 \ + --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \ + --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88 # via - # feast (setup.py) + # feast (pyproject.toml) # dask -google-api-core[grpc]==2.26.0 \ - --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ - --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 +google-api-core[grpc]==2.33.0 \ + --hash=sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb \ + --hash=sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc # via - # feast (setup.py) + # feast (pyproject.toml) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -796,9 +821,9 @@ google-api-core[grpc]==2.26.0 \ # google-cloud-datastore # google-cloud-storage # pandas-gbq -google-auth==2.41.1 \ - --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ - --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 +google-auth[pyopenssl]==2.56.2 \ + --hash=sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6 \ + --hash=sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051 # via # google-api-core # google-auth-oauthlib @@ -808,479 +833,617 @@ google-auth==2.41.1 \ # google-cloud-core # google-cloud-datastore # google-cloud-storage - # kubernetes # pandas-gbq # pydata-google-auth -google-auth-oauthlib==1.2.2 \ - --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ - --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-auth-oauthlib==1.4.0 \ + --hash=sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2 \ + --hash=sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 # via # pandas-gbq # pydata-google-auth -google-cloud-bigquery[pandas]==3.38.0 \ - --hash=sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520 \ - --hash=sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6 +google-cloud-bigquery[pandas]==3.42.2 \ + --hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \ + --hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq -google-cloud-bigquery-storage==2.33.1 \ - --hash=sha256:24952aba0d69acc4d6bfbdc7a09dddbb728496b1780bd224f1056361a1b51044 \ - --hash=sha256:3fd25bef364ac5fb9bbd6560f0dd11b90b1845883df8e0a8c706ad53d00fc23b - # via feast (setup.py) -google-cloud-bigtable==2.33.0 \ - --hash=sha256:8d25c73dd41cea9436d14c9f03b91f94b05ff4afd47ce9a575bf18f8e6985747 \ - --hash=sha256:cd4f7178cde30113900318ec7e2fd793116752c97e0922c185c69b645a52656d - # via feast (setup.py) -google-cloud-core==2.4.3 \ - --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ - --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e +google-cloud-bigquery-storage==2.39.0 \ + --hash=sha256:8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 \ + --hash=sha256:d5afd90ad06cf24d9167316cca70ab5b344e880fc13031d7392aa78ee76b8bb6 + # via feast (pyproject.toml) +google-cloud-bigtable==2.41.0 \ + --hash=sha256:36da0fc730952bfe48ae91dee11015459c9046fa1499d0efb73a61bbb6edbe9b \ + --hash=sha256:d0c424bae6238eb16ffc0922be79da00d20d23e0750ea46742ffd4542c50fd26 + # via feast (pyproject.toml) +google-cloud-core==2.6.0 \ + --hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \ + --hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.21.0 \ - --hash=sha256:eee454dd4a55f5b327f9f344928ff1a09a6f77c23d5e3d908ad31a13cc2f4073 \ - --hash=sha256:f303f27cd1983383f20bd227019cd8a7897419e0ec6b878367c58c66245f9d9b - # via feast (setup.py) +google-cloud-datastore==2.26.0 \ + --hash=sha256:1ac4882f033fcb90bd75d1171b46400ecd5def23768598843d9c041e0c0b4a6a \ + --hash=sha256:1bde5694308468da5c932153a6b0331d5b54e1c21de366b6a75ddb0000fd5f4b + # via feast (pyproject.toml) google-cloud-storage==2.19.0 \ --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via feast (setup.py) -google-crc32c==1.7.1 \ - --hash=sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db \ - --hash=sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337 \ - --hash=sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c \ - --hash=sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242 \ - --hash=sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e \ - --hash=sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472 \ - --hash=sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194 \ - --hash=sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3 \ - --hash=sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582 \ - --hash=sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d \ - --hash=sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6 \ - --hash=sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82 \ - --hash=sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06 \ - --hash=sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349 \ - --hash=sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a \ - --hash=sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d \ - --hash=sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48 \ - --hash=sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb \ - --hash=sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315 \ - --hash=sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589 \ - --hash=sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76 \ - --hash=sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65 \ - --hash=sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6 \ - --hash=sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127 \ - --hash=sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53 \ - --hash=sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603 \ - --hash=sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35 \ - --hash=sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9 \ - --hash=sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638 \ - --hash=sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9 \ - --hash=sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77 \ - --hash=sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14 \ - --hash=sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b \ - --hash=sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb + # via feast (pyproject.toml) +google-crc32c==1.8.0 \ + --hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \ + --hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \ + --hash=sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff \ + --hash=sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 \ + --hash=sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411 \ + --hash=sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a \ + --hash=sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 \ + --hash=sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb \ + --hash=sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa \ + --hash=sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962 \ + --hash=sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215 \ + --hash=sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b \ + --hash=sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27 \ + --hash=sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113 \ + --hash=sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f \ + --hash=sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f \ + --hash=sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d \ + --hash=sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 \ + --hash=sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092 \ + --hash=sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7 \ + --hash=sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2 \ + --hash=sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93 \ + --hash=sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8 \ + --hash=sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21 \ + --hash=sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 \ + --hash=sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2 \ + --hash=sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc \ + --hash=sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454 \ + --hash=sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2 \ + --hash=sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733 \ + --hash=sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697 \ + --hash=sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651 \ + --hash=sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c # via # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 +google-resumable-media==2.10.0 \ + --hash=sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c \ + --hash=sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.70.0 \ - --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ - --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +googleapis-common-protos[grpc]==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # grpc-google-iam-v1 # grpcio-status -greenlet==3.2.4 \ - --hash=sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b \ - --hash=sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735 \ - --hash=sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079 \ - --hash=sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d \ - --hash=sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433 \ - --hash=sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58 \ - --hash=sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52 \ - --hash=sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31 \ - --hash=sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246 \ - --hash=sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f \ - --hash=sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671 \ - --hash=sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8 \ - --hash=sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d \ - --hash=sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f \ - --hash=sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0 \ - --hash=sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd \ - --hash=sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337 \ - --hash=sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0 \ - --hash=sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633 \ - --hash=sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b \ - --hash=sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa \ - --hash=sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31 \ - --hash=sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9 \ - --hash=sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b \ - --hash=sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4 \ - --hash=sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc \ - --hash=sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c \ - --hash=sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98 \ - --hash=sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f \ - --hash=sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c \ - --hash=sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590 \ - --hash=sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3 \ - --hash=sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2 \ - --hash=sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9 \ - --hash=sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5 \ - --hash=sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02 \ - --hash=sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0 \ - --hash=sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1 \ - --hash=sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c \ - --hash=sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594 \ - --hash=sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5 \ - --hash=sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d \ - --hash=sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a \ - --hash=sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6 \ - --hash=sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b \ - --hash=sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df \ - --hash=sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945 \ - --hash=sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae \ - --hash=sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb \ - --hash=sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504 \ - --hash=sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb \ - --hash=sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01 \ - --hash=sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c \ - --hash=sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968 - # via feast (setup.py) -grpc-google-iam-v1==0.14.2 \ - --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ - --hash=sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20 +greenlet==3.5.4 \ + --hash=sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20 \ + --hash=sha256:07bd44616608d873d06735b63ef1a88191d6ca57c8d291d6559c71bc14c0893c \ + --hash=sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994 \ + --hash=sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8 \ + --hash=sha256:123aa379c962ed5fe90a880327e0c3066124ac64ec99e12a238be9fd8eb3db3d \ + --hash=sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9 \ + --hash=sha256:13b980043cb1b3134e81ea469da1250ddcc6bfe6d245bbaa59168d9cdc8f228f \ + --hash=sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809 \ + --hash=sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c \ + --hash=sha256:188e4d142f243051d92a1f5c244a741da02dddc070a0620c842804d7b56d008c \ + --hash=sha256:1e1a4a684b16c45ba324e60b32a4386a87722bcb815d2a149d2182f9b401ca72 \ + --hash=sha256:1f17e362d78e37559e0506c5a7d066bdd45073c36a0127a543e8a0df27242ff3 \ + --hash=sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02 \ + --hash=sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c \ + --hash=sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c \ + --hash=sha256:2cdaadc3d31445a8f782bde3cd37e49a2c2a9c6da6daf76a3e34c683b271a3c7 \ + --hash=sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec \ + --hash=sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c \ + --hash=sha256:32802705c2c1ff25e8237b3bdacf2594fa02be80af8a66703eb7853ea7e68686 \ + --hash=sha256:3529a8a933582ad19e224792cac7372489526576b75b4c124e8e4f29948f4861 \ + --hash=sha256:39169a11d87a6a263afda3e9a27d1df16d0f919d40a4837cc73986c9884c0dd8 \ + --hash=sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0 \ + --hash=sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4 \ + --hash=sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9 \ + --hash=sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3 \ + --hash=sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9 \ + --hash=sha256:4ab9f0704bccf6d3b38e0d2130b7b33271cff11453690da074fa280c3aa8e8e7 \ + --hash=sha256:57aa201b351f7c7c75627c60d29e4d5b97a07d37efeb62b903466fca42c097d7 \ + --hash=sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd \ + --hash=sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3 \ + --hash=sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2 \ + --hash=sha256:69173331fbc5d64bfac0065d7e22c39cfcd089e9b18d125bdcd5079363b09616 \ + --hash=sha256:70bdfacdc183dac838b2a0aaff2dd6134a457c52fe68a9c6bbab435483d2b9df \ + --hash=sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf \ + --hash=sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0 \ + --hash=sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a \ + --hash=sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f \ + --hash=sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22 \ + --hash=sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356 \ + --hash=sha256:89f3738167bab8c1084b94e23023d41d247117ac149fa0fbcb5bd4cf6262b353 \ + --hash=sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e \ + --hash=sha256:9667862a2e38ad379f11b845daeda22c8989186def44f06962c9c4c05e556da7 \ + --hash=sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5 \ + --hash=sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8 \ + --hash=sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde \ + --hash=sha256:a2d614cb2372c7101a12ea8b96dd56f81c986d247c5a73db67063f3ed1ca4a52 \ + --hash=sha256:ac5bf81d79d2c8eeb2ef6359b2e1687a1e9ebf46c2b1f970da9a9255df51d190 \ + --hash=sha256:adf2244d7f69409925a8f22ed22cc5f93cdfe5c9dc87ff3476be2c2aaae61a05 \ + --hash=sha256:ae53534b5dec0f4c2ec26f898f538dc8ea1ca3ef2927d597a9439e40a09da937 \ + --hash=sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867 \ + --hash=sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d \ + --hash=sha256:b7c895310363f310361e0fe2072af85269d2a2a285cd04c0c59e79a5e3670dcf \ + --hash=sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f \ + --hash=sha256:bd3d1145f603b2db19feb9078c2e6855eb7c67e15580c010ed815cee519b86fd \ + --hash=sha256:c38c902a0986eba1f6e7ba1ab39ad5195926abde90f3fe080e08212db62176da \ + --hash=sha256:c3fe76c2cac86b4f7a1e92865ac0a54384deb05c92986287c1a7110d9bd53071 \ + --hash=sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88 \ + --hash=sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17 \ + --hash=sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c \ + --hash=sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66 \ + --hash=sha256:cd320d998cbaa032932830448e39abf3c6a12901295e386e8114db926e10cffb \ + --hash=sha256:cef589bc65fae02d10bca2ac341191c5b33acc2967892ebf4fcbd10eabb7a74c \ + --hash=sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25 \ + --hash=sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0 \ + --hash=sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927 \ + --hash=sha256:dc418cf4c873357964d6624445ed09472e50def990c65dd4e76fc3ba8cd9cef6 \ + --hash=sha256:dfc41ae893d9ceaf22c824f2153a88b30651b20e8758c2cd9ac143f23640563c \ + --hash=sha256:e849e6e139b9671adeac505f72fc05f4af7fd1921faef40295e214fc3b361b59 \ + --hash=sha256:e883de250e299654b1f1680f72a1a9f9ba62c9bd1bce84099c90657349a8dfbb \ + --hash=sha256:e9a5e3406e3ed8125ae1a3b37c12f3434e2b1f0fa053197c5557895b4fb09606 \ + --hash=sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef \ + --hash=sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3 \ + --hash=sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da \ + --hash=sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132 \ + --hash=sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7 \ + --hash=sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f \ + --hash=sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2 \ + --hash=sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f \ + --hash=sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667 + # via feast (pyproject.toml) +grpc-google-iam-v1==0.14.4 \ + --hash=sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038 \ + --hash=sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 # via google-cloud-bigtable -grpcio==1.62.3 \ - --hash=sha256:059444f0ed5dba73ab7dd0ee7e8e6b606df4130d2b0a9f010f84da4ab9f6c2d8 \ - --hash=sha256:114f2a865886ff33f85d70670e971fe0e3d252a1209656fefa5470286e3fcc76 \ - --hash=sha256:13571a5b868dcc308a55d36669a2d17d9dcd6ec8335213f6c49cc68da7305abe \ - --hash=sha256:1ac0944e9e3ee3e20825226d1e17985e9f88487055c475986cf0922a7d806d8a \ - --hash=sha256:1de3d04d9a4ec31ebe848ae1fe61e4cbc367fb9495cbf6c54368e60609a998d9 \ - --hash=sha256:216740723fc5971429550c374a0c039723b9d4dcaf7ba05227b7e0a500b06417 \ - --hash=sha256:25cd75dc73c5269932413e517be778640402f18cf9a81147e68645bd8af18ab0 \ - --hash=sha256:325c56ce94d738c31059cf91376f625d3effdff8f85c96660a5fd6395d5a707f \ - --hash=sha256:3737e5ef0aa0fcdfeaf3b4ecc1a6be78b494549b28aec4b7f61b5dc357f7d8be \ - --hash=sha256:377babc817e8b4186aed7ed56e832867c513e4e9b6c3503565c344ffdef440d4 \ - --hash=sha256:3fb7d966a976d762a31346353a19fce4afcffbeda3027dd563bc8cb521fcf799 \ - --hash=sha256:43670a25b752b7ed960fcec3db50ae5886dc0df897269b3f5119cde9b731745f \ - --hash=sha256:4439bbd759636e37b66841117a66444b454937e27f0125205d2d117d7827c643 \ - --hash=sha256:454a6aed4ebd56198d37e1f3be6f1c70838e33dd62d1e2cea12f2bcb08efecc5 \ - --hash=sha256:4c9c1502c76cadbf2e145061b63af077b08d5677afcef91970d6db87b30e2f8b \ - --hash=sha256:4dab8b64c438e19c763a6332b55e5efdbecfb7c55ae59a42c38c81ed27955fa5 \ - --hash=sha256:56757d3e4cf5d4b98a30f2c5456151607261c891fa2298a4554848dcbf83083d \ - --hash=sha256:57823dc7299c4f258ae9c32fd327d29f729d359c34d7612b36e48ed45b3ab8d0 \ - --hash=sha256:582bd03e9c3d1bd1162eb51fa0f1a35633d66e73f4f36702d3b8484a8b45eda7 \ - --hash=sha256:620165df24aae3d5b3e84cb8dd6b98f6ed49aed04126186bbf43061e301d6a21 \ - --hash=sha256:646c14e9f3356d3f34a65b58b0f8d08daa741ba1d4fcd4966b79407543332154 \ - --hash=sha256:668211f3699bbee4deaf1d6e6b8df59328bf63f077bf2dc9b8bfa4a17df4a279 \ - --hash=sha256:6be243f3954b0ca709f56f9cae926c84ac96e1cce19844711e647a1f1db88b99 \ - --hash=sha256:6da20a1ae010a988bc4ed47850f1122de0a88e18cd2f901fcf56007be1fc6c30 \ - --hash=sha256:7349cd7445ac65fbe1b744dcab9cc1ec02dae2256941a2e67895926cbf7422b4 \ - --hash=sha256:74f3fc9b93290e58264844f5bc46df4c58a94c4287a277dbcf75344fc6c37ca4 \ - --hash=sha256:75a4e9ac7ff185cad529f35934c5d711b88aca48b90c70e195f5657da50ce321 \ - --hash=sha256:7b33c1807d4ac564a3027d06f21a2220c116ceacaaef614deb96b3341ee58896 \ - --hash=sha256:807176971c504c598976f5a9ea62363cffbbbb6c7509d9808c2342b020880fa2 \ - --hash=sha256:80a82fdee14dc27e9299248b7aabd5a8739a1cf6b76c78aa2b848158b44a99d5 \ - --hash=sha256:81b7c121c4e52a0749bf0759185b8d5cfa48a786cd7d411cdab08269813e0aab \ - --hash=sha256:8257cc9e55fb0e2149a652d9dc14c023720f9e73c9145776e07c97e0a553922e \ - --hash=sha256:8a5f00b2508937952d23a1767739e95bbbe1120f8a66d10187d5e971d56bb55c \ - --hash=sha256:8ae2e7a390b2cdd2a95d3bf3b3385245eeb48a5e853943cb46139666462c2d1a \ - --hash=sha256:940459d81685549afdfe13a6de102c52ea4cdda093477baa53056884aadf7c48 \ - --hash=sha256:9c4aae4e683776c319169d87e7891b67b75e3f1c0beeb877902ea148b0585164 \ - --hash=sha256:9d5f8e0050a179b3bce9189b522dc91008d44f08c757a7c310e0fd06b4d3d147 \ - --hash=sha256:a1b85d35a7d9638c03321dfe466645b87e23c30df1266f9e04bbb5f44e7579a9 \ - --hash=sha256:a82410d7620c07cb32624e38f2a106980564dfef9dbe78f5b295cda9ef217c03 \ - --hash=sha256:abfe64811177e681edc81d9d9d1bd23edc5f599bd9846650864769264ace30cd \ - --hash=sha256:ac9783d5679c8da612465168c820fd0b916e70ec5496c840bddba0be7f2d124c \ - --hash=sha256:b033d50bd41e506e3b579775f54a30c16c222e0d88847ac8098d2eca2a7454cc \ - --hash=sha256:b097347441b86a8c3ad9579abaf5e5f7f82b1d74a898f47360433b2bca0e4536 \ - --hash=sha256:b708401ede2c4cb8943e8a713988fcfe6cbea105b07cd7fa7c8a9f137c22bddb \ - --hash=sha256:bd900e666bb68fff49703084be14407cd73b8a5752a7590cea98ec22de24fb5d \ - --hash=sha256:c118cfc80e2402a5595be36e9245ffd9b0e146f426cc40bdf60015bf183f8373 \ - --hash=sha256:c175b252d063af388523a397dbe8edbc4319761f5ee892a8a0f5890acc067362 \ - --hash=sha256:c8bb1a7aa82af6c7713cdf9dcb8f4ea1024ac7ce82bb0a0a82a49aea5237da34 \ - --hash=sha256:c95a0b76a44c548e6bd8c5f7dbecf89c77e2e16d3965be817b57769c4a30bea2 \ - --hash=sha256:e202e3f963480ca067a261179b1ac610c0f0272cb4a7942d11b7e2b3fc99c3aa \ - --hash=sha256:e9ffdb7bc9ccd56ec201aec3eab3432e1e820335b5a16ad2b37e094218dcd7a6 \ - --hash=sha256:ea7ca66a58421411c6486fa5015fe7704e2816ff0b4ec4fb779ad5e1cbbdabf3 \ - --hash=sha256:f2ff8ac447765e173842b554b31307b98b3bb1852710903ebb936e7efb7df6e5 \ - --hash=sha256:f5def814c5a4c90c8fe389c526ab881f4a28b7e239b23ed8e02dd02934dfaa1a - # via - # feast (setup.py) +grpcio==1.83.0 \ + --hash=sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df \ + --hash=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 \ + --hash=sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5 \ + --hash=sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df \ + --hash=sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930 \ + --hash=sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf \ + --hash=sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9 \ + --hash=sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33 \ + --hash=sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da \ + --hash=sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03 \ + --hash=sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa \ + --hash=sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223 \ + --hash=sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9 \ + --hash=sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4 \ + --hash=sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf \ + --hash=sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881 \ + --hash=sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af \ + --hash=sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5 \ + --hash=sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0 \ + --hash=sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727 \ + --hash=sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb \ + --hash=sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a \ + --hash=sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735 \ + --hash=sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16 \ + --hash=sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49 \ + --hash=sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1 \ + --hash=sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24 \ + --hash=sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b \ + --hash=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f \ + --hash=sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57 \ + --hash=sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf \ + --hash=sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f \ + --hash=sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c \ + --hash=sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969 \ + --hash=sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd \ + --hash=sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c \ + --hash=sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b \ + --hash=sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61 \ + --hash=sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404 \ + --hash=sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617 \ + --hash=sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40 \ + --hash=sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45 \ + --hash=sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc \ + --hash=sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c \ + --hash=sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745 \ + --hash=sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45 \ + --hash=sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c \ + --hash=sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0 \ + --hash=sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d \ + --hash=sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9 \ + --hash=sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8 + # via + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-datastore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking # grpcio-reflection # grpcio-status + # milvus-lite # pymilvus -grpcio-health-checking==1.62.3 \ - --hash=sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93 \ - --hash=sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d - # via feast (setup.py) -grpcio-reflection==1.62.3 \ - --hash=sha256:a48ef37df81a3bada78261fc92ef382f061112f989d1312398b945cc69838b9c \ - --hash=sha256:cb84682933c400bddf94dd94f928d1c6570f500b6dd255973d4bfb495b82585f - # via feast (setup.py) -grpcio-status==1.62.3 \ - --hash=sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485 \ - --hash=sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8 +grpcio-health-checking==1.83.0 \ + --hash=sha256:7d8b47a5bfbc699d4aee0fc7a27f5d0265eb23a6a64db5ac2d8b749a0f8a9911 \ + --hash=sha256:ad6bc4d5a1103ad704d25ccd82def300dfa27996b185cab3e4cceeef2c6867d4 + # via feast (pyproject.toml) +grpcio-reflection==1.83.0 \ + --hash=sha256:6a2a30a462ac2c3c6d149734a8fe655fa7617c17fa2cda9949006f3bf07f5b3e \ + --hash=sha256:9d4151f68f18a85aa5b0eff5d493ad13f9421f96c8d9c01c840b353dc979be93 + # via feast (pyproject.toml) +grpcio-status==1.83.0 \ + --hash=sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae \ + --hash=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 # via google-api-core -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore + # httpcore2 # uvicorn hatch-fancy-pypi-readme==25.1.0 \ --hash=sha256:9c58ed3dff90d51f43414ce37009ad1d5b0f08ffc9fc216998a06380f01c0045 \ --hash=sha256:ce0134c40d63d874ac48f48ccc678b8f3b62b8e50e9318520d2bffc752eedaf3 - # via feast (setup.py) + # via feast (pyproject.toml) hatch-vcs==0.4.0 \ --hash=sha256:093810748fe01db0d451fabcf2c1ac2688caefd232d4ede967090b1c1b07d9f7 \ --hash=sha256:b8a2b6bee54cf6f9fc93762db73890017ae59c9081d1038a41f16235ceaf8b2c - # via feast (setup.py) -hatchling==1.27.0 \ - --hash=sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6 \ - --hash=sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b + # via feast (pyproject.toml) +hatchling==1.31.0 \ + --hash=sha256:6b48ad4068a482ed7239b3a8215bc55b47aad3345d58dfc94e553c5d2d46211b \ + --hash=sha256:aac80bec8b6fe35e8480f1c335be8910fa210a0e6f735a139be205dadcacb544 # via - # feast (setup.py) + # feast (pyproject.toml) # hatch-fancy-pypi-readme # hatch-vcs -hiredis==2.4.0 \ - --hash=sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027 \ - --hash=sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577 \ - --hash=sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603 \ - --hash=sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869 \ - --hash=sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228 \ - --hash=sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79 \ - --hash=sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb \ - --hash=sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33 \ - --hash=sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77 \ - --hash=sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9 \ - --hash=sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd \ - --hash=sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a \ - --hash=sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8 \ - --hash=sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9 \ - --hash=sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901 \ - --hash=sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7 \ - --hash=sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0 \ - --hash=sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d \ - --hash=sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213 \ - --hash=sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8 \ - --hash=sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389 \ - --hash=sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330 \ - --hash=sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1 \ - --hash=sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2 \ - --hash=sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288 \ - --hash=sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba \ - --hash=sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded \ - --hash=sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca \ - --hash=sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109 \ - --hash=sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8 \ - --hash=sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea \ - --hash=sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6 \ - --hash=sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8 \ - --hash=sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0 \ - --hash=sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5 \ - --hash=sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5 \ - --hash=sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182 \ - --hash=sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73 \ - --hash=sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af \ - --hash=sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac \ - --hash=sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204 \ - --hash=sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277 \ - --hash=sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212 \ - --hash=sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41 \ - --hash=sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e \ - --hash=sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c \ - --hash=sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2 \ - --hash=sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4 \ - --hash=sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348 \ - --hash=sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b \ - --hash=sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e \ - --hash=sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995 \ - --hash=sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371 \ - --hash=sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613 \ - --hash=sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b \ - --hash=sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d \ - --hash=sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708 \ - --hash=sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08 \ - --hash=sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca \ - --hash=sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12 \ - --hash=sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8 \ - --hash=sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829 \ - --hash=sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38 \ - --hash=sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825 \ - --hash=sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67 \ - --hash=sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056 \ - --hash=sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376 \ - --hash=sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214 \ - --hash=sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982 \ - --hash=sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034 \ - --hash=sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1 \ - --hash=sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f \ - --hash=sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9 \ - --hash=sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5 \ - --hash=sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390 \ - --hash=sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0 \ - --hash=sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d \ - --hash=sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57 \ - --hash=sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe \ - --hash=sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2 \ - --hash=sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415 \ - --hash=sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c \ - --hash=sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b \ - --hash=sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853 \ - --hash=sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36 \ - --hash=sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae \ - --hash=sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd \ - --hash=sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187 \ - --hash=sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c \ - --hash=sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542 \ - --hash=sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d \ - --hash=sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79 \ - --hash=sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7 \ - --hash=sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def - # via feast (setup.py) +hiredis==3.4.0 \ + --hash=sha256:02298551060cbfe17b1acb02e9d066eadd51e37357369736f8fc1f5533875255 \ + --hash=sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc \ + --hash=sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865 \ + --hash=sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c \ + --hash=sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec \ + --hash=sha256:06c163607f163a300cba9ef02fd8a234dd644a208bfafe972614f72e5287b9ba \ + --hash=sha256:0731dd9f2bf4d3417947a3cbbca4dd4d9fa6ad7cecfd4e16439c17a4562d6304 \ + --hash=sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419 \ + --hash=sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320 \ + --hash=sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b \ + --hash=sha256:0bcb630add6bc9ea136fce691ddff0c46aa91cb860df4ca789fe44127eb7e90d \ + --hash=sha256:12ea5facb5b08fa23e4c101ec2151f3a3de8ecec412fec58dbde0a6eebca02c7 \ + --hash=sha256:12eca9aea1450d1a85dc15574a985c227e52abbc2b6466f48ad2aa3b82124701 \ + --hash=sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f \ + --hash=sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017 \ + --hash=sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc \ + --hash=sha256:16a3ea8193c5f4ee6e8daafe0126207cef38eec5a923c97047e6985d9bb1b61f \ + --hash=sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4 \ + --hash=sha256:1bfb9ccfb13be63883e5f2e5ff7f6fc87bf256f8243af594257dfbed9dbc3cf0 \ + --hash=sha256:1c9e141b420dae63ea21303a7e8672282326cae67472768e9a3f6e7f0990bd28 \ + --hash=sha256:1d8c21fb85f2a3de9328cf5bace84b63da00028e771b92335ff4fdc00121dd5a \ + --hash=sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5 \ + --hash=sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016 \ + --hash=sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240 \ + --hash=sha256:282c4310af72afbe18b07d416459f4febeaeb805a067a7df790136e0e550fcb2 \ + --hash=sha256:2c432cce38190c3c13b6c28d6840ee85db50765ad510134007f1cfb3844dce73 \ + --hash=sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591 \ + --hash=sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea \ + --hash=sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980 \ + --hash=sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b \ + --hash=sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb \ + --hash=sha256:386b556f48fb0f9f09696b9d37f1cae554123fbd9f091d0ca23087f2aca02887 \ + --hash=sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b \ + --hash=sha256:4190bd07dd7879a8a7ddbb2a4f74d402721f3898276e35beb98851b85b5f539c \ + --hash=sha256:423570ac4d2665c0d55d8707b7859a331e9001da5e437b7b7a23e0acbce770c9 \ + --hash=sha256:43004b0b48abc628dda1ac3ac4871e1326c126f8cd9f11164d61934d827d7a3b \ + --hash=sha256:4404c557fd49bcfe24dff41f1209e4221c76d1607df2fb2dfd39474b5b086dcb \ + --hash=sha256:44660a91e0fbc803c29b337c1a9194c8d7b4cd3a3868d28f747cbec2df165483 \ + --hash=sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582 \ + --hash=sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2 \ + --hash=sha256:45c6c296056641b5df37cedafe7d1553f33bc247e2f81603a4d038b39261879b \ + --hash=sha256:46e041fc7a83eaa989ef5fe09526bc2353a6ba39b4ce5191684eabefc02e17b0 \ + --hash=sha256:4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e \ + --hash=sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d \ + --hash=sha256:4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d \ + --hash=sha256:4e5530d7a6c71c1a9c3b850b08bbc8178ffc9556e3aef7f9c808fe9ff91a6f11 \ + --hash=sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763 \ + --hash=sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584 \ + --hash=sha256:5359caad5b57da0bce11d2880f22617ba3710f0866121a924745447848448034 \ + --hash=sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79 \ + --hash=sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9 \ + --hash=sha256:64cf54c1dad35830bf1d86b343ad3c55aed443af51c5ccead8afd1b547aca196 \ + --hash=sha256:6774f1fe2723001ca0cd42bf5d8b1235301226273915c581c5c1260d4d114c43 \ + --hash=sha256:68c81c04e0d8778c01163dd2bc6f0e0236fc7f650c291526f10d4faccd99e5a0 \ + --hash=sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb \ + --hash=sha256:69d0326f20354ce278cbb86f5ae47cb390e22bb94a66877031038af907c42fa5 \ + --hash=sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22 \ + --hash=sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b \ + --hash=sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9 \ + --hash=sha256:7b159315df71a009375227384aa4219f98c2342ccd8eb33e3f4b58654f426376 \ + --hash=sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203 \ + --hash=sha256:7f7fc1535f6e1a190089eae46dee25f0c6b72bb221d377be07092803b8208733 \ + --hash=sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f \ + --hash=sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13 \ + --hash=sha256:85a9abf2d16b4cb112f6cc6c32236d763b34d21c69b00c2f81a4ff3016fcc1d6 \ + --hash=sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c \ + --hash=sha256:899d5cea72c9a6c2d9b6e7b223de62c9e9f541f48267cc7135764070548735ce \ + --hash=sha256:89e95f50ec4d29645ba2da9010e8717861585dc9f3df354a367f43807a6db3c7 \ + --hash=sha256:8aaaab18314fd25453b5cf59c8cdca4110e419455bcb4c0737d19d4151513e75 \ + --hash=sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1 \ + --hash=sha256:8f2c8569460aa456a294d5e9a3579a382b825dc2e283e3cc398323dcfb6a3dee \ + --hash=sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151 \ + --hash=sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a \ + --hash=sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d \ + --hash=sha256:98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c \ + --hash=sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f \ + --hash=sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d \ + --hash=sha256:a315009b441a0105a373a9a780ebb1c6f7d9ead88ac6ea5f2a15791353c6f590 \ + --hash=sha256:a3796094f616f72976ff51e4dc1a016e753c0f9af5393b2df96920b6bae1e19b \ + --hash=sha256:a427976ba339da624367613d7eba52a1516e6d5c7f8988dc8fb888fbcf52d8b6 \ + --hash=sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb \ + --hash=sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530 \ + --hash=sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4 \ + --hash=sha256:b0c29ac4f50fc61a904acfe76e4ce6bc37e16ab5a98089cb411476d466e1bb36 \ + --hash=sha256:b32afb576705fc5a02440d416ef476ddbcc827a20ff6c4a9d557882bc6b75135 \ + --hash=sha256:b6228afacadb19a858289319d72797023e2cb048f2f930b68b7fe57eaed5fb6b \ + --hash=sha256:b98f44bf873731d9ce99e6b3eeb6196bf7b27693ad9f6de0fba986b2b0845127 \ + --hash=sha256:bb44efa4fa3e3ed7779ad0ade3c08ed5d75ca7a6336893e9a4f2722093b4168a \ + --hash=sha256:bb8b2dd9c00d80ad2870ffbb51ce9af99fb2194c082d7b09f4125d90e21426f6 \ + --hash=sha256:bca175f02a2b0150ffe7f5dc8bf49c798f34d2c7024d17ace0ec97a7583560e3 \ + --hash=sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59 \ + --hash=sha256:c2245c46b4ced5f689469e6dcdfc8a0895bf873840a6600f5ea759cdf1b26a8b \ + --hash=sha256:c8c6337e7ad187e8039d06e4b33ee5148dcd73928819d94087e649eb37316a09 \ + --hash=sha256:ccc5c660e31d788ca534a20f2ccb7a80b946b960e18ed4e1db950fcac122b405 \ + --hash=sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643 \ + --hash=sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c \ + --hash=sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4 \ + --hash=sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d \ + --hash=sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352 \ + --hash=sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81 \ + --hash=sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5 \ + --hash=sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092 \ + --hash=sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4 \ + --hash=sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad \ + --hash=sha256:e29267ecdd08758926f1a9221af2671d90f475480c40aff409921b1f362f1bd5 \ + --hash=sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d \ + --hash=sha256:ed1dba2695f6de009c67d63b39ff978cb43b8a79362f697acedffb7743e50d21 \ + --hash=sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1 \ + --hash=sha256:f3c67f39b112dc35f68d5b59ee111db6121f037d1a60cf3840ecffbb2ec5686b \ + --hash=sha256:f7c7596fbb2b5202e943180353958e89014e763c7f25877a92f70bbde6cd7f19 \ + --hash=sha256:ff28eca77a57751488df90b0f385b472fd78e140a4d45097224648a5737acdf5 \ + --hash=sha256:ff6217da86a63a6a5ea954d9b9ed67916d9d94a566b44aa98e68ae34b40ebbbf + # via feast (pyproject.toml) httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httpcore2==2.9.1 \ + --hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \ + --hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26 + # via httpx2 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - # via - # fastapi-mcp - # mcp -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d + # via fastapi-mcp +httpx2==2.9.1 \ + --hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \ + --hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a # via mcp -ibis-framework[duckdb]==9.5.0 \ - --hash=sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0 \ - --hash=sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84 - # via - # feast (setup.py) - # ibis-substrait -ibis-substrait==4.0.1 \ - --hash=sha256:107ca49383a3cca2fdc88f67ea2f0172620c16fa8f39c9c52305af85dd6180b4 \ - --hash=sha256:614810a173d096fbc49d87a9b419e2162a3c25d8efda1a4d57a389ce56b9041f - # via feast (setup.py) -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +ibis-framework[duckdb]==12.0.0 \ + --hash=sha256:0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 \ + --hash=sha256:238624f2c14fdab8382ca2f4f667c3cdb81e29844cd5f8db8a325d0743767c61 + # via feast (pyproject.toml) +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx + # httpx2 # requests # snowflake-connector-python # yarl -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a +imagesize==2.0.0 \ + --hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \ + --hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3 # via sphinx -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc # via dask jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via - # feast (setup.py) - # poetry-dynamic-versioning + # feast (pyproject.toml) # sphinx -jmespath==1.0.1 \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 # via # aiobotocore # boto3 # botocore -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 - # via - # feast (setup.py) - # mcp +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via mcp jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -kubernetes==33.1.0 \ - --hash=sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5 \ - --hash=sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993 - # via feast (setup.py) +kubernetes==36.0.3 \ + --hash=sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7 \ + --hash=sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f + # via feast (pyproject.toml) +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ --hash=sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 # via partd -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ @@ -1373,14 +1536,14 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -marshmallow==4.0.1 \ - --hash=sha256:72f14ef346f81269dbddee891bac547dda1501e9e08b6a809756ea3dbb7936a1 \ - --hash=sha256:e1d860bd262737cb2d34e1541b84cb52c32c72c9474e3fe6f30f137ef8b0d97f - # via environs -mcp==1.12.4 \ - --hash=sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5 \ - --hash=sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789 +mcp==2.0.0 \ + --hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \ + --hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6 # via fastapi-mcp +mcp-types==2.0.0 \ + --hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \ + --hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379 + # via mcp mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba @@ -1389,437 +1552,507 @@ meson==1.7.1 \ --hash=sha256:155780a5be87f6dd7f427ad8bcbf0f2b2c5f62ee5fdacca7caa9de8439a24b89 \ --hash=sha256:6d9cbc9ce87a70243c75e4cc668ee3f206ab50b184beb0a08ece948112f19bd7 # via - # feast (setup.py) + # feast (pyproject.toml) # meson-python meson-python==0.15.0 \ --hash=sha256:3ae38253ff02b2e947a05e362a2eaf5a9a09d133c5666b4123399ee5fbf2e591 \ --hash=sha256:fddb73eecd49e89c1c41c87937cd89c2d0b65a1c63ba28238681d4bd9484d26f - # via feast (setup.py) -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) -multidict==6.7.0 \ - --hash=sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32 \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ - --hash=sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff \ - --hash=sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8 \ - --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ - --hash=sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 \ - --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ - --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6 \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ - --hash=sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912 \ - --hash=sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c \ - --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6 \ - --hash=sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84 \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ - --hash=sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd \ - --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0 \ - --hash=sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e \ - --hash=sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 \ - --hash=sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc \ - --hash=sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7 \ - --hash=sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4 \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111 \ - --hash=sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e \ - --hash=sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84 \ - --hash=sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851 \ - --hash=sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb \ - --hash=sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ - --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5 \ - --hash=sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb \ - --hash=sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349 \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ - --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ - --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34 \ - --hash=sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85 \ - --hash=sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ - --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ - --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ - --hash=sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91 \ - --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40 \ - --hash=sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73 \ - --hash=sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4 \ - --hash=sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687 \ - --hash=sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 + # via feast (pyproject.toml) +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via feast (pyproject.toml) +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via # aiobotocore # aiohttp # yarl -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mypy==2.3.0 \ + --hash=sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491 \ + --hash=sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762 \ + --hash=sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5 \ + --hash=sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654 \ + --hash=sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd \ + --hash=sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe \ + --hash=sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc \ + --hash=sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3 \ + --hash=sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5 \ + --hash=sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329 \ + --hash=sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b \ + --hash=sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3 \ + --hash=sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7 \ + --hash=sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805 \ + --hash=sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f \ + --hash=sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e \ + --hash=sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7 \ + --hash=sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494 \ + --hash=sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373 \ + --hash=sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88 \ + --hash=sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee \ + --hash=sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2 \ + --hash=sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757 \ + --hash=sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a \ + --hash=sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b \ + --hash=sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461 \ + --hash=sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97 \ + --hash=sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4 \ + --hash=sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117 \ + --hash=sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36 \ + --hash=sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac \ + --hash=sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5 \ + --hash=sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556 \ + --hash=sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c \ + --hash=sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88 \ + --hash=sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568 \ + --hash=sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595 \ + --hash=sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f \ + --hash=sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1 \ + --hash=sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef \ + --hash=sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6 \ + --hash=sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db \ + --hash=sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff \ + --hash=sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60 \ + --hash=sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c # via sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf - # via - # feast (setup.py) +numpy==2.4.6 \ + --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ + --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ + --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ + --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ + --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ + --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ + --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ + --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ + --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ + --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ + --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ + --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ + --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ + --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ + --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ + --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ + --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ + --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ + --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ + --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ + --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ + --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ + --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ + --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ + --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ + --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ + --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 + # via + # feast (pyproject.toml) # dask # db-dtypes + # faiss-cpu # ibis-framework + # milvus-lite # pandas # pandas-gbq - # pyarrow oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 - # via - # kubernetes - # requests-oauthlib -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f + # via requests-oauthlib +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef + # via mcp +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via pymilvus +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # dask # db-dtypes - # dunamai + # faiss-cpu # google-cloud-bigquery # gunicorn # hatchling # ibis-framework - # ibis-substrait # pandas-gbq # pyproject-metadata # scikit-build-core # setuptools-scm # snowflake-connector-python # sphinx + # vcs-versioning pandas==2.3.3 \ --hash=sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7 \ --hash=sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593 \ @@ -1877,7 +2110,7 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery @@ -1885,9 +2118,9 @@ pandas==2.3.3 \ # pandas-gbq # pymilvus # snowflake-connector-python -pandas-gbq==0.29.2 \ - --hash=sha256:99f6b642bd8340e96c2acdf7d717108aa0d31c703c2f284817ba7dc3b16110b2 \ - --hash=sha256:a3a28e7de4f8456fc4b8e9b7472b9bc470d2566f89c4cd997599e504f5a0a706 +pandas-gbq==0.35.0 \ + --hash=sha256:258de481019566611031919997bf9c1ece4ca30a4dd02d3fc3664b251d446182 \ + --hash=sha256:596c908487ef0649a161e86ef272c00c267e581b95dc5ee0dc3518545b33bcfc # via google-cloud-bigquery parsy==2.2 \ --hash=sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 \ @@ -1907,180 +2140,170 @@ patchelf==0.17.2.4 \ --hash=sha256:ae44cb3c857d50f54b99e5697aa978726ada33a8a6129d4b8b7ffd28b996652d \ --hash=sha256:d842b51f0401460f3b1f3a3a67d2c266a8f515a5adfbfa6e7b656cb3ac2ed8bc \ --hash=sha256:d9b35ebfada70c02679ad036407d9724ffe1255122ba4ac5e4be5868618a5689 - # via feast (setup.py) -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # via feast (pyproject.toml) +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via # hatchling # mypy # scikit-build-core -platformdirs==4.5.0 \ - --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ - --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 # via snowflake-connector-python pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via hatchling -poetry-core==1.9.1 \ - --hash=sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0 \ - --hash=sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8 - # via feast (setup.py) -poetry-dynamic-versioning==1.9.1 \ - --hash=sha256:65a0c814e6d30d4807734a3c34edf261fd7cc3b340dbd23b6a33ee41f7d0b547 \ - --hash=sha256:d6e7b9df817aa2ca4946cd695c6c89e1379d2e6c640f008a9b6170d081a9da48 - # via feast (setup.py) -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via feast (setup.py) -propcache==0.4.1 \ - --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ - --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ - --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ - --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ - --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ - --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ - --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ - --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ - --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ - --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via feast (pyproject.toml) +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274 \ + --hash=sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.26.1 \ - --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ - --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +proto-plus==1.28.2 \ + --hash=sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501 \ + --hash=sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52 # via # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==6.32.1 \ - --hash=sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346 \ - --hash=sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4 \ - --hash=sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1 \ - --hash=sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085 \ - --hash=sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1 \ - --hash=sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710 \ - --hash=sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122 \ - --hash=sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281 \ - --hash=sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d - # via - # feast (setup.py) +protobuf==7.35.1 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a + # via + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -2092,84 +2315,103 @@ protobuf==6.32.1 \ # grpcio-status # proto-plus # pymilvus - # substrait -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 - # via feast (setup.py) -psycopg[c, pool]==3.2.5 \ - --hash=sha256:b782130983e5b3de30b4c529623d3687033b4dafa05bb661fc6bf45837ca5879 \ - --hash=sha256:f5f750611c67cb200e85b408882f29265c66d1de7f813add4f8125978bfd70e8 - # via feast (setup.py) -psycopg-c==3.2.5 \ - --hash=sha256:57ad4cfd28de278c424aaceb1f2ad5c7910466e315dfe84e403f3c7a0a2ce81b +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # feast (pyproject.toml) + # pandas-gbq +psycopg[c, pool]==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via feast (pyproject.toml) +psycopg-c==3.3.4 \ + --hash=sha256:ed8106128b2d04359c185fc9641b4409abfce4d0b6fb1d1ff6800646e27f1a22 # via psycopg -psycopg-pool==3.2.6 \ - --hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \ - --hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7 +psycopg-pool==3.3.1 \ + --hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \ + --hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c # via psycopg -pyarrow==17.0.0 \ - --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ - --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ - --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ - --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ - --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ - --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ - --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ - --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ - --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ - --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ - --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ - --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ - --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ - --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ - --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ - --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ - --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ - --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ - --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ - --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ - --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ - --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ - --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ - --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ - --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ - --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ - --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ - --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ - --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ - --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ - --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ - --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ - --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ - --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ - --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ - --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 - # via - # feast (setup.py) +pyarrow==25.0.0 \ + --hash=sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be \ + --hash=sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc \ + --hash=sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec \ + --hash=sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849 \ + --hash=sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e \ + --hash=sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8 \ + --hash=sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d \ + --hash=sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537 \ + --hash=sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062 \ + --hash=sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e \ + --hash=sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104 \ + --hash=sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517 \ + --hash=sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e \ + --hash=sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5 \ + --hash=sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887 \ + --hash=sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1 \ + --hash=sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b \ + --hash=sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be \ + --hash=sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778 \ + --hash=sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e \ + --hash=sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194 \ + --hash=sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2 \ + --hash=sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9 \ + --hash=sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b \ + --hash=sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18 \ + --hash=sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7 \ + --hash=sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc \ + --hash=sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62 \ + --hash=sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0 \ + --hash=sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f \ + --hash=sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e \ + --hash=sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862 \ + --hash=sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50 \ + --hash=sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe \ + --hash=sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3 \ + --hash=sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a \ + --hash=sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4 \ + --hash=sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af \ + --hash=sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b \ + --hash=sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0 \ + --hash=sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f \ + --hash=sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec \ + --hash=sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f + # via + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery # ibis-framework + # milvus-lite # pandas-gbq # snowflake-connector-python pyarrow-hotfix==0.7 \ --hash=sha256:3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 \ --hash=sha256:59399cd58bdd978b2e42816a4183a55c6472d4e33d183351b6069f11ed42661d # via ibis-framework -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 @@ -2177,160 +2419,176 @@ pyasn1-modules==0.4.2 \ pybindgen==0.22.0 \ --hash=sha256:21612f4806a2af25a175716d7e694563af7e10c704538a350cb595d187952f6f \ --hash=sha256:5b4837d3138ac56863d93fe462f1dac39fb87bd50898e0da4c57fefd645437ac - # via feast (setup.py) -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 + # via feast (pyproject.toml) +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi # fastapi-mcp # mcp + # mcp-types # pydantic-settings -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pydantic-settings==2.11.0 \ - --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ - --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c - # via - # fastapi-mcp - # mcp +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f + # via fastapi-mcp pydata-google-auth==1.9.1 \ --hash=sha256:0a51ce41c601ca0bc69b8795bf58bedff74b4a6a007c9106c7cbcdec00eaced2 \ --hash=sha256:75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 # via pandas-gbq -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via - # feast (setup.py) + # feast (pyproject.toml) # rich # sphinx -pyjwt==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via - # feast (setup.py) + # feast (pyproject.toml) + # mcp # snowflake-connector-python -pymilvus==2.4.9 \ - --hash=sha256:0937663700007c23a84cfc0656160b301f6ff9247aaec4c96d599a6b43572136 \ - --hash=sha256:45313607d2c164064bdc44e0f933cb6d6afa92e9efcc7f357c5240c57db58fbe - # via feast (setup.py) -pymysql==1.1.2 \ - --hash=sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03 \ - --hash=sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 - # via feast (setup.py) -pyopenssl==25.3.0 \ - --hash=sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6 \ - --hash=sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329 +pymysql==1.2.0 \ + --hash=sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 \ + --hash=sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33 + # via feast (pyproject.toml) +pyopenssl==26.3.0 \ + --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ + --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 # via snowflake-connector-python -pyproject-metadata==0.9.1 \ - --hash=sha256:b8b2253dd1b7062b78cf949a115f02ba7fa4114aabe63fa10528e9e1a954a816 \ - --hash=sha256:ee5efde548c3ed9b75a354fc319d5afd25e9585fa918a34f62f904cc731973ad +pyproject-metadata==0.12.1 \ + --hash=sha256:8809a4df6fe08279b39a8890669506ed3158e0617855ac9aff098fcbe772ae4c \ + --hash=sha256:f7162d580a96386a8eb096da06215f981f547d1490f03055ef99e323bc2da427 # via meson-python python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ @@ -2342,22 +2600,21 @@ python-dateutil==2.9.0.post0 \ # ibis-framework # kubernetes # pandas -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via - # environs # pydantic-settings + # pymilvus # uvicorn -python-multipart==0.0.20 \ - --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \ - --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13 +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 # via mcp -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via - # ibis-framework # pandas # snowflake-connector-python pyyaml==6.0.3 \ @@ -2435,31 +2692,31 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # dask - # ibis-substrait # kubernetes # uvicorn -redis==4.6.0 \ - --hash=sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d \ - --hash=sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c - # via feast (setup.py) +redis==7.4.1 \ + --hash=sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e \ + --hash=sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + # via feast (pyproject.toml) referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 # via # jsonschema # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # google-api-core # google-cloud-bigquery # google-cloud-storage # kubernetes + # pymilvus # requests-oauthlib # snowflake-connector-python # sphinx @@ -2469,200 +2726,156 @@ requests-oauthlib==2.0.0 \ # via # google-auth-oauthlib # kubernetes -rich==13.9.4 \ - --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ - --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via # fastapi-mcp # ibis-framework # typer -roman-numerals-py==3.1.0 \ - --hash=sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c \ - --hash=sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d +roman-numerals==4.1.0 \ + --hash=sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2 \ + --hash=sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7 # via sphinx -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==2026.6.3 \ + --hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \ + --hash=sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680 \ + --hash=sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9 \ + --hash=sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538 \ + --hash=sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804 \ + --hash=sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf \ + --hash=sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4 \ + --hash=sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97 \ + --hash=sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6 \ + --hash=sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96 \ + --hash=sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a \ + --hash=sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187 \ + --hash=sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975 \ + --hash=sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f \ + --hash=sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703 \ + --hash=sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9 \ + --hash=sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127 \ + --hash=sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f \ + --hash=sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa \ + --hash=sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05 \ + --hash=sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171 \ + --hash=sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba \ + --hash=sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c \ + --hash=sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223 \ + --hash=sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4 \ + --hash=sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885 \ + --hash=sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698 \ + --hash=sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f \ + --hash=sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7 \ + --hash=sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed \ + --hash=sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f \ + --hash=sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf \ + --hash=sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e \ + --hash=sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f \ + --hash=sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24 \ + --hash=sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a \ + --hash=sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41 \ + --hash=sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc \ + --hash=sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d \ + --hash=sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146 \ + --hash=sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e \ + --hash=sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e \ + --hash=sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4 \ + --hash=sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12 \ + --hash=sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7 \ + --hash=sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261 \ + --hash=sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6 \ + --hash=sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5 \ + --hash=sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93 \ + --hash=sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7 \ + --hash=sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda \ + --hash=sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8 \ + --hash=sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342 \ + --hash=sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c \ + --hash=sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb \ + --hash=sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0 \ + --hash=sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77 \ + --hash=sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3 \ + --hash=sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885 \ + --hash=sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826 \ + --hash=sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617 \ + --hash=sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb \ + --hash=sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577 \ + --hash=sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80 \ + --hash=sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e \ + --hash=sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945 \ + --hash=sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90 \ + --hash=sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7 \ + --hash=sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0 \ + --hash=sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140 \ + --hash=sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822 \ + --hash=sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba \ + --hash=sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9 \ + --hash=sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4 \ + --hash=sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a \ + --hash=sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8 \ + --hash=sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf \ + --hash=sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4 \ + --hash=sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324 \ + --hash=sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53 \ + --hash=sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b \ + --hash=sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41 \ + --hash=sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9 \ + --hash=sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca \ + --hash=sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1 \ + --hash=sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d \ + --hash=sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690 \ + --hash=sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107 \ + --hash=sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2 \ + --hash=sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76 \ + --hash=sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d \ + --hash=sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af \ + --hash=sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6 \ + --hash=sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db \ + --hash=sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369 \ + --hash=sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd \ + --hash=sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911 \ + --hash=sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504 \ + --hash=sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a \ + --hash=sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9 \ + --hash=sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13 \ + --hash=sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc \ + --hash=sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278 \ + --hash=sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868 \ + --hash=sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2 \ + --hash=sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd \ + --hash=sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4 \ + --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6 \ + --hash=sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9 \ + --hash=sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00 \ + --hash=sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f \ + --hash=sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e \ + --hash=sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442 \ + --hash=sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da \ + --hash=sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90 \ + --hash=sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef # via # jsonschema # referencing -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via google-auth -s3transfer==0.13.1 \ - --hash=sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724 \ - --hash=sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf +s3transfer==0.19.2 \ + --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \ + --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25 # via boto3 -scikit-build-core==0.11.6 \ - --hash=sha256:5982ccd839735be99cfd3b92a8847c6c196692f476c215da84b79d2ad12f9f1b \ - --hash=sha256:ce6d8fe64e6b4c759ea0fb95d2f8a68f60d2df31c2989838633b8ec930736360 - # via feast (setup.py) -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c - # via - # feast (setup.py) +scikit-build-core==1.0.3 \ + --hash=sha256:a4d7a05978ee37975c37743510c8991e2debce7ef83afb0a07c0c576fd4f16e8 \ + --hash=sha256:ae95427b7d3c14a6cf8bbfd4d901f6138ab64c99e20cbe8ea7d75cd26093f085 + # via feast (pyproject.toml) +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 + # via + # feast (pyproject.toml) # pandas-gbq # pydata-google-auth - # pymilvus # setuptools-scm -setuptools-scm==9.2.1 \ - --hash=sha256:2f5b21e45205e47ed3976e58bc29c38f49bb9b6f07c625c7fd2823871694b244 \ - --hash=sha256:84c9b3f39ffb77ba00dd92a06e025e4f66dc69ab8df1c2a225a0678e76311f8d +setuptools-scm==10.2.1 \ + --hash=sha256:4fa7dd82cf8c800df59c9a288c90299b1657ff1ecfc3f5cc00287c5dbf5e27a9 \ + --hash=sha256:b7c82f4102d389ee57dc66ccdb4f9b4bca3c40ba83b43f1f63d68ccd72db2580 # via hatch-vcs shellingham==1.5.4 \ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ @@ -2674,50 +2887,46 @@ six==1.17.0 \ # via # kubernetes # python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio -snowballstemmer==3.0.1 \ - --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ - --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 +snowballstemmer==3.1.1 \ + --hash=sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 \ + --hash=sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260 # via sphinx -snowflake-connector-python[pandas]==3.18.0 \ - --hash=sha256:0af10b207af3d2de2b130e89018d49a60f2e5cfe841f3bf459e58f2e1c4c4506 \ - --hash=sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628 \ - --hash=sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4 \ - --hash=sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e \ - --hash=sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee \ - --hash=sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9 \ - --hash=sha256:32b1abfea32561d817b0a2f80b06d936cb32712af06bf7b848a428bfd857a10a \ - --hash=sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a \ - --hash=sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728 \ - --hash=sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800 \ - --hash=sha256:4ed2d593f1983939d5d8d88b212d86fd4f14f0ceefc1df9882b4a18534adbde9 \ - --hash=sha256:51eb789a09dc6c62119cfabd044fba1a6b8378206f05a1e83ddb2e9cb49acc0b \ - --hash=sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc \ - --hash=sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb \ - --hash=sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba \ - --hash=sha256:783a9ab206563d7b52fdcdd7a72af62de811d3381ca64132fd3445537b4d041b \ - --hash=sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f \ - --hash=sha256:8d3e96e1d09b07edca6c1f6ca675b6fdd05a4a7e428e4cdf6fb697d87b9f60fc \ - --hash=sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a \ - --hash=sha256:a8c570edff5a4888840dbe1e9e65c5e4d77d55c5c800cd359fe0903a769201e0 \ - --hash=sha256:aeeb181a156333480f60b5f8ddbb3d087e288b4509adbef7993236defe4d7570 \ - --hash=sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca \ - --hash=sha256:b99f261c82be92224ac20c8c12bdf26ce3ed5dfd8a3df8a97f15a1e11c46ad27 \ - --hash=sha256:bd1de3038b6d7059ca59f93e105aba2a673151c693cc4292f72f38bfaf147df2 \ - --hash=sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e \ - --hash=sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00 - # via feast (setup.py) +snowflake-connector-python[pandas]==4.7.1 \ + --hash=sha256:051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117 \ + --hash=sha256:052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7 \ + --hash=sha256:0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317 \ + --hash=sha256:167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf \ + --hash=sha256:171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a \ + --hash=sha256:1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6 \ + --hash=sha256:22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf \ + --hash=sha256:2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b \ + --hash=sha256:4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a \ + --hash=sha256:4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754 \ + --hash=sha256:5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9 \ + --hash=sha256:571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d \ + --hash=sha256:5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35 \ + --hash=sha256:7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0 \ + --hash=sha256:7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5 \ + --hash=sha256:81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57 \ + --hash=sha256:8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a \ + --hash=sha256:92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3 \ + --hash=sha256:b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45 \ + --hash=sha256:d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa \ + --hash=sha256:e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327 \ + --hash=sha256:f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c \ + --hash=sha256:f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7 \ + --hash=sha256:f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52 \ + --hash=sha256:fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1 \ + --hash=sha256:ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b + # via feast (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via snowflake-connector-python -sphinx==8.2.3 \ - --hash=sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348 \ - --hash=sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3 - # via feast (setup.py) +sphinx==9.0.4 \ + --hash=sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3 \ + --hash=sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb + # via feast (pyproject.toml) sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 @@ -2742,187 +2951,199 @@ sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via sphinx -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -sqlglot==25.20.2 \ - --hash=sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab \ - --hash=sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567 +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via feast (pyproject.toml) +sqlglot==30.14.0 \ + --hash=sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9 \ + --hash=sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636 # via ibis-framework -sse-starlette==3.0.2 \ - --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ - --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 \ + --hash=sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627 # via mcp -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via + # feast (pyproject.toml) # fastapi # mcp -substrait==0.24.2 \ - --hash=sha256:743cc352e96b0927b2cd37cd5a8fdac0a96a68df9600bd104fc36aebd222a836 \ - --hash=sha256:d1d475833566fa9d67eed3273456883c0568486ccced92b524b31709d2817e19 - # via ibis-substrait -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via feast (setup.py) + # sse-starlette +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via feast (pyproject.toml) tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via feast (pyproject.toml) toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via fastapi-mcp -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via - # poetry-dynamic-versioning - # snowflake-connector-python -toolz==0.12.1 \ - --hash=sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85 \ - --hash=sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 + # via snowflake-connector-python +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # dask # ibis-framework # partd -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 - # via - # feast (setup.py) - # milvus-lite -trove-classifiers==2025.9.11.17 \ - --hash=sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd \ - --hash=sha256:931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via feast (pyproject.toml) +trove-classifiers==2026.6.1.19 \ + --hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \ + --hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745 # via hatchling -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) -typer==0.19.2 \ - --hash=sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9 \ - --hash=sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca +truststore==0.10.4 \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 + # via + # httpcore2 + # httpx2 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) +typer==0.27.0 \ + --hash=sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5 \ + --hash=sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1 # via fastapi-mcp types-psutil==7.0.0.20250218 \ --hash=sha256:1447a30c282aafefcf8941ece854e1100eee7b0296a9d9be9977292f0269b121 \ --hash=sha256:1e642cdafe837b240295b23b1cbd4691d80b08a07d29932143cbbae30eb0db9c - # via feast (setup.py) -types-pymysql==1.1.0.20250916 \ - --hash=sha256:873eb9836bb5e3de4368cc7010ca72775f86e9692a5c7810f8c7f48da082e55b \ - --hash=sha256:98d75731795fcc06723a192786662bdfa760e1e00f22809c104fbb47bac5e29b - # via feast (setup.py) -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via feast (pyproject.toml) +types-pymysql==1.2.0.20260724 \ + --hash=sha256:71327a3fea0f680a280445f1b475c2307f68de05c2d3302a79df56af93c574cd \ + --hash=sha256:8493cf87e8df0a7b4e8c84305bc0aae69d3a8af9c6da7bfbb9a4c5c3f97d4a82 + # via feast (pyproject.toml) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiohttp # aiosignal # anyio # fastapi + # grpcio + # httpx2 # ibis-framework + # mcp + # mcp-types # mypy + # opentelemetry-api # psycopg # psycopg-pool # pydantic @@ -2933,562 +3154,523 @@ typing-extensions==4.15.0 \ # sqlalchemy # starlette # typeguard - # typer # typing-inspection typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via pydantic-settings -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via pandas -ujson==5.11.0 \ - --hash=sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80 \ - --hash=sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef \ - --hash=sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a \ - --hash=sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840 \ - --hash=sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53 \ - --hash=sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076 \ - --hash=sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab \ - --hash=sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d \ - --hash=sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89 \ - --hash=sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c \ - --hash=sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb \ - --hash=sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c \ - --hash=sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823 \ - --hash=sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5 \ - --hash=sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac \ - --hash=sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d \ - --hash=sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723 \ - --hash=sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6 \ - --hash=sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0 \ - --hash=sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328 \ - --hash=sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0 \ - --hash=sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04 \ - --hash=sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25 \ - --hash=sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49 \ - --hash=sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec \ - --hash=sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3 \ - --hash=sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3 \ - --hash=sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc \ - --hash=sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a \ - --hash=sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9 \ - --hash=sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302 \ - --hash=sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694 \ - --hash=sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547 \ - --hash=sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01 \ - --hash=sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02 \ - --hash=sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6 \ - --hash=sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60 \ - --hash=sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915 \ - --hash=sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835 \ - --hash=sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701 \ - --hash=sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702 \ - --hash=sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50 \ - --hash=sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6 \ - --hash=sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc \ - --hash=sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a \ - --hash=sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c \ - --hash=sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c \ - --hash=sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03 \ - --hash=sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629 \ - --hash=sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105 \ - --hash=sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844 \ - --hash=sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241 \ - --hash=sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a \ - --hash=sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26 \ - --hash=sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac \ - --hash=sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596 \ - --hash=sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9 \ - --hash=sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752 \ - --hash=sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138 \ - --hash=sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b \ - --hash=sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba \ - --hash=sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5 \ - --hash=sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018 \ - --hash=sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362 \ - --hash=sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746 \ - --hash=sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f \ - --hash=sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88 \ - --hash=sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638 \ - --hash=sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b \ - --hash=sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9 \ - --hash=sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db \ - --hash=sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f \ - --hash=sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58 \ - --hash=sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b \ - --hash=sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433 \ - --hash=sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0 \ - --hash=sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764 \ - --hash=sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c \ - --hash=sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34 \ - --hash=sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052 \ - --hash=sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba \ - --hash=sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c \ - --hash=sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc \ - --hash=sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39 - # via pymilvus -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via + # ibis-framework + # pandas +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via # botocore # kubernetes # requests -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # mcp # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +vcs-versioning==2.2.3 \ + --hash=sha256:4f7873af76f5cc24e701608354f376f5eb70b317d663787ace57a7edd7a27506 \ + --hash=sha256:c21e284aa98c8269996791a19d6173baf953cf9996ef42ec3ebcbdc151e18b9b + # via setuptools-scm +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn websocket-client==1.9.0 \ --hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \ --hash=sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef # via kubernetes -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 # via aiobotocore -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8 \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf \ - --hash=sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890 \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737 \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba \ - --hash=sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9 \ - --hash=sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0 \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147 \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e \ - --hash=sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2 \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6 \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750 \ + --hash=sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621 \ + --hash=sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13 \ + --hash=sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1 \ + --hash=sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2 \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata # The following packages were excluded from the output: # milvus-lite +# pymilvus +# faiss-cpu diff --git a/sdk/python/requirements/py3.11-requirements.txt b/sdk/python/requirements/py3.11-requirements.txt index 497ac1bc0d9..839f9f4fcea 100644 --- a/sdk/python/requirements/py3.11-requirements.txt +++ b/sdk/python/requirements/py3.11-requirements.txt @@ -1,252 +1,363 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.11 --no-strip-extras setup.py --generate-hashes --output-file sdk/python/requirements/py3.11-requirements.txt -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +# uv pip compile -p 3.11 --no-strip-extras pyproject.toml --generate-hashes --output-file sdk/python/requirements/py3.11-requirements.txt +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via fastapi +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # starlette # watchfiles -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 - # via - # jsonschema - # referencing -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via feast (pyproject.toml) +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via requests -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via requests -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # uvicorn -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a # via dask colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via feast (setup.py) -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) + # via feast (pyproject.toml) +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) dill==0.3.9 \ --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c - # via feast (setup.py) -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 - # via feast (setup.py) -fsspec==2025.9.0 \ - --hash=sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19 \ - --hash=sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7 + # via feast (pyproject.toml) +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e + # via feast (pyproject.toml) +fsspec==2026.7.0 \ + --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \ + --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88 # via dask -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via uvicorn -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # requests -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc # via dask jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via feast (setup.py) -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 - # via feast (setup.py) -jsonschema-specifications==2025.9.1 \ - --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ - --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d - # via jsonschema + # via feast (pyproject.toml) +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ --hash=sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 @@ -342,255 +453,246 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via feast (pyproject.toml) +mypy==2.3.0 \ + --hash=sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491 \ + --hash=sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762 \ + --hash=sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5 \ + --hash=sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654 \ + --hash=sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd \ + --hash=sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe \ + --hash=sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc \ + --hash=sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3 \ + --hash=sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5 \ + --hash=sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329 \ + --hash=sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b \ + --hash=sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3 \ + --hash=sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7 \ + --hash=sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805 \ + --hash=sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f \ + --hash=sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e \ + --hash=sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7 \ + --hash=sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494 \ + --hash=sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373 \ + --hash=sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88 \ + --hash=sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee \ + --hash=sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2 \ + --hash=sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757 \ + --hash=sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a \ + --hash=sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b \ + --hash=sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461 \ + --hash=sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97 \ + --hash=sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4 \ + --hash=sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117 \ + --hash=sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36 \ + --hash=sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac \ + --hash=sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5 \ + --hash=sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556 \ + --hash=sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c \ + --hash=sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88 \ + --hash=sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568 \ + --hash=sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595 \ + --hash=sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f \ + --hash=sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1 \ + --hash=sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef \ + --hash=sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6 \ + --hash=sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db \ + --hash=sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff \ + --hash=sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60 \ + --hash=sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c # via sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf +numpy==2.4.6 \ + --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ + --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ + --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ + --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ + --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ + --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ + --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ + --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ + --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ + --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ + --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ + --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ + --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ + --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ + --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ + --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ + --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ + --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ + --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ + --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ + --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ + --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ + --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ + --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ + --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ + --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ + --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # pandas -packaging==25.0 \ - --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ - --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # dask # gunicorn @@ -651,216 +753,247 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # dask partd==1.4.2 \ --hash=sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f \ --hash=sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c # via dask -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via mypy -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via feast (setup.py) -protobuf==6.32.1 \ - --hash=sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346 \ - --hash=sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4 \ - --hash=sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1 \ - --hash=sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085 \ - --hash=sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1 \ - --hash=sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710 \ - --hash=sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122 \ - --hash=sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281 \ - --hash=sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d - # via feast (setup.py) -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 - # via feast (setup.py) -pyarrow==21.0.0 \ - --hash=sha256:067c66ca29aaedae08218569a114e413b26e742171f526e828e1064fcdec13f4 \ - --hash=sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623 \ - --hash=sha256:0c4e75d13eb76295a49e0ea056eb18dbd87d81450bfeb8afa19a7e5a75ae2ad7 \ - --hash=sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636 \ - --hash=sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7 \ - --hash=sha256:203003786c9fd253ebcafa44b03c06983c9c8d06c3145e37f1b76a1f317aeae1 \ - --hash=sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10 \ - --hash=sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51 \ - --hash=sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd \ - --hash=sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8 \ - --hash=sha256:3b4d97e297741796fead24867a8dabf86c87e4584ccc03167e4a811f50fdf74d \ - --hash=sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569 \ - --hash=sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e \ - --hash=sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc \ - --hash=sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6 \ - --hash=sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c \ - --hash=sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82 \ - --hash=sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79 \ - --hash=sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6 \ - --hash=sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10 \ - --hash=sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61 \ - --hash=sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d \ - --hash=sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb \ - --hash=sha256:898afce396b80fdda05e3086b4256f8677c671f7b1d27a6976fa011d3fd0a86e \ - --hash=sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e \ - --hash=sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594 \ - --hash=sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634 \ - --hash=sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da \ - --hash=sha256:a7f6524e3747e35f80744537c78e7302cd41deee8baa668d56d55f77d9c464b3 \ - --hash=sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876 \ - --hash=sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e \ - --hash=sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a \ - --hash=sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b \ - --hash=sha256:cdc4c17afda4dab2a9c0b79148a43a7f4e1094916b3e18d8975bfd6d6d52241f \ - --hash=sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18 \ - --hash=sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe \ - --hash=sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99 \ - --hash=sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26 \ - --hash=sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d \ - --hash=sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a \ - --hash=sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd \ - --hash=sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503 \ - --hash=sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79 +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via feast (pyproject.toml) +protobuf==7.35.1 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a + # via feast (pyproject.toml) +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via feast (pyproject.toml) +pyarrow==25.0.0 \ + --hash=sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be \ + --hash=sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc \ + --hash=sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec \ + --hash=sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849 \ + --hash=sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e \ + --hash=sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8 \ + --hash=sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d \ + --hash=sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537 \ + --hash=sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062 \ + --hash=sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e \ + --hash=sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104 \ + --hash=sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517 \ + --hash=sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e \ + --hash=sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5 \ + --hash=sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887 \ + --hash=sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1 \ + --hash=sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b \ + --hash=sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be \ + --hash=sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778 \ + --hash=sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e \ + --hash=sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194 \ + --hash=sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2 \ + --hash=sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9 \ + --hash=sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b \ + --hash=sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18 \ + --hash=sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7 \ + --hash=sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc \ + --hash=sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62 \ + --hash=sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0 \ + --hash=sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f \ + --hash=sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e \ + --hash=sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862 \ + --hash=sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50 \ + --hash=sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe \ + --hash=sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3 \ + --hash=sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a \ + --hash=sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4 \ + --hash=sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af \ + --hash=sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b \ + --hash=sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0 \ + --hash=sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f \ + --hash=sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec \ + --hash=sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f # via - # feast (setup.py) + # feast (pyproject.toml) # dask -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b - # via feast (setup.py) -pyjwt==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb - # via feast (setup.py) +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via feast (pyproject.toml) +pyjwt==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + # via feast (pyproject.toml) python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via pandas -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via uvicorn -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via pandas pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ @@ -937,525 +1070,418 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # uvicorn -referencing==0.37.0 \ - --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ - --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 - # via - # jsonschema - # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via feast (setup.py) -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 - # via - # jsonschema - # referencing +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via feast (pyproject.toml) six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 - # via fastapi -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via feast (setup.py) +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via feast (pyproject.toml) +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 + # via + # feast (pyproject.toml) + # fastapi +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via feast (pyproject.toml) tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via feast (pyproject.toml) toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -toolz==1.0.0 \ - --hash=sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236 \ - --hash=sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02 + # via feast (pyproject.toml) +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # dask # partd -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 - # via feast (setup.py) -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via feast (pyproject.toml) +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via # anyio # fastapi # mypy # pydantic # pydantic-core - # referencing # sqlalchemy # starlette # typeguard -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 + # typing-inspection +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 + # via + # fastapi + # pydantic +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 # via pandas -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via requests -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata diff --git a/sdk/python/requirements/py3.12-ci-requirements.txt b/sdk/python/requirements/py3.12-ci-requirements.txt index 438408049d2..bef84732a3a 100644 --- a/sdk/python/requirements/py3.12-ci-requirements.txt +++ b/sdk/python/requirements/py3.12-ci-requirements.txt @@ -1,150 +1,151 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.12 --no-strip-extras setup.py --extra ci --generate-hashes --output-file sdk/python/requirements/py3.12-ci-requirements.txt -accelerate==1.10.1 \ - --hash=sha256:3621cff60b9a27ce798857ece05e2b9f56fcc71631cfb31ccf71f0359c311f11 \ - --hash=sha256:3dea89e433420e4bfac0369cae7e36dcd6a56adfcfd38cdda145c6225eab5df8 +# uv pip compile -p 3.12 --no-strip-extras pyproject.toml --extra ci --generate-hashes --output-file sdk/python/requirements/py3.12-ci-requirements.txt +accelerate==1.14.0 \ + --hash=sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d \ + --hash=sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6 # via docling-ibm-models -aiobotocore==2.23.1 \ - --hash=sha256:a59f2a78629b97d52f10936b79c73de64e481a8c44a62c1871f088df6c1afc4f \ - --hash=sha256:d81c54d2eae2406ea9a473fea518fed580cf37bc4fc51ce43ba81546e5305114 - # via feast (setup.py) -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +aiobotocore==3.8.0 \ + --hash=sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670 \ + --hash=sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + # via feast (pyproject.toml) +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.0 \ - --hash=sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6 \ - --hash=sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a \ - --hash=sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e \ - --hash=sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971 \ - --hash=sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54 \ - --hash=sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2 \ - --hash=sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96 \ - --hash=sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469 \ - --hash=sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26 \ - --hash=sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0 \ - --hash=sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29 \ - --hash=sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885 \ - --hash=sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579 \ - --hash=sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a \ - --hash=sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1 \ - --hash=sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b \ - --hash=sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852 \ - --hash=sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865 \ - --hash=sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad \ - --hash=sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092 \ - --hash=sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7 \ - --hash=sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b \ - --hash=sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8 \ - --hash=sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820 \ - --hash=sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd \ - --hash=sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee \ - --hash=sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4 \ - --hash=sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67 \ - --hash=sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d \ - --hash=sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1 \ - --hash=sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d \ - --hash=sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114 \ - --hash=sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1 \ - --hash=sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755 \ - --hash=sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04 \ - --hash=sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca \ - --hash=sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1 \ - --hash=sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae \ - --hash=sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793 \ - --hash=sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e \ - --hash=sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a \ - --hash=sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8 \ - --hash=sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602 \ - --hash=sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778 \ - --hash=sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f \ - --hash=sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49 \ - --hash=sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77 \ - --hash=sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7 \ - --hash=sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f \ - --hash=sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0 \ - --hash=sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519 \ - --hash=sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d \ - --hash=sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370 \ - --hash=sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7 \ - --hash=sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247 \ - --hash=sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d \ - --hash=sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80 \ - --hash=sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9 \ - --hash=sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c \ - --hash=sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d \ - --hash=sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020 \ - --hash=sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504 \ - --hash=sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca \ - --hash=sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731 \ - --hash=sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd \ - --hash=sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6 \ - --hash=sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21 \ - --hash=sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1 \ - --hash=sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620 \ - --hash=sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64 \ - --hash=sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8 \ - --hash=sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3 \ - --hash=sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5 \ - --hash=sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985 \ - --hash=sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813 \ - --hash=sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed \ - --hash=sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20 \ - --hash=sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03 \ - --hash=sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702 \ - --hash=sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6 \ - --hash=sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed \ - --hash=sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353 \ - --hash=sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108 \ - --hash=sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f \ - --hash=sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409 \ - --hash=sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7 \ - --hash=sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3 \ - --hash=sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9 \ - --hash=sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba \ - --hash=sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe \ - --hash=sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c \ - --hash=sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861 \ - --hash=sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a \ - --hash=sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0 \ - --hash=sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390 \ - --hash=sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3 \ - --hash=sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90 \ - --hash=sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802 \ - --hash=sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163 \ - --hash=sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d \ - --hash=sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430 \ - --hash=sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a \ - --hash=sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b \ - --hash=sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6 \ - --hash=sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b \ - --hash=sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657 \ - --hash=sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c \ - --hash=sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93 \ - --hash=sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f \ - --hash=sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82 \ - --hash=sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31 \ - --hash=sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc \ - --hash=sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52 \ - --hash=sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c \ - --hash=sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a \ - --hash=sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52 \ - --hash=sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7 \ - --hash=sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b \ - --hash=sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147 \ - --hash=sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 # via # aiobotocore # aiohttp-cors # fsspec + # kubernetes + # mlflow # ray aiohttp-cors==0.8.1 \ --hash=sha256:3180cf304c5c712d626b9162b195b1db7ddf976a2a25172b35bb2448b890a80d \ --hash=sha256:ccacf9cb84b64939ea15f859a146af1f662a6b1d68175754a07315e305fb1403 # via ray -aioitertools==0.12.0 \ - --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \ - --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796 +aioitertools==0.13.0 \ + --hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \ + --hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c # via aiobotocore aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ @@ -154,19 +155,29 @@ alabaster==0.7.16 \ --hash=sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65 \ --hash=sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 # via sphinx +alembic==1.18.5 \ + --hash=sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc \ + --hash=sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e + # via mlflow altair==4.2.2 \ --hash=sha256:39399a267c49b30d102c10411e67ab26374156a84b1aeb9fcd15140429ba49c5 \ --hash=sha256:8b45ebeaf8557f2d760c5c77b79f02ae12aee7c46c27c06014febab6f849bc87 # via great-expectations -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via fastapi +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via + # elasticsearch # httpx + # httpx2 # jupyter-server # mcp # sse-starlette @@ -210,9 +221,9 @@ argon2-cffi-bindings==25.1.0 \ --hash=sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520 \ --hash=sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb # via argon2-cffi -arrow==1.3.0 \ - --hash=sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80 \ - --hash=sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85 +arrow==1.4.0 \ + --hash=sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205 \ + --hash=sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7 # via isoduration asn1crypto==1.5.1 \ --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ @@ -220,51 +231,141 @@ asn1crypto==1.5.1 \ # via snowflake-connector-python assertpy==1.1 \ --hash=sha256:acc64329934ad71a3221de185517a43af33e373bb44dc05b5a9b174394ef4833 - # via feast (setup.py) -asttokens==3.0.0 \ - --hash=sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7 \ - --hash=sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2 + # via feast (pyproject.toml) +asttokens==3.0.2 \ + --hash=sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2 \ + --hash=sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933 # via stack-data -async-lru==2.0.5 \ - --hash=sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb \ - --hash=sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943 +async-lru==2.3.0 \ + --hash=sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6 \ + --hash=sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315 # via jupyterlab async-property==0.2.2 \ --hash=sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380 \ --hash=sha256:8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7 # via python-keycloak -atpublic==5.1 \ - --hash=sha256:135783dbd887fbddb6ef032d104da70c124f2b44b9e2d79df07b9da5334825e3 \ - --hash=sha256:abc1f4b3dbdd841cc3539e4b5e4f3ad41d658359de704e30cb36da4d4e9d3022 +atpublic==7.0.0 \ + --hash=sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e \ + --hash=sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b # via ibis-framework -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via + # feast (pyproject.toml) # aiohttp # jsonlines # jsonschema + # openlineage-python # referencing -azure-core==1.35.1 \ - --hash=sha256:12da0c9e08e48e198f9158b56ddbe33b421477e1dc98c2e1c8f9e254d92c468b \ - --hash=sha256:435d05d6df0fff2f73fb3c15493bb4721ede14203f1ff1382aa6b6b2bdd7e562 +azure-core==1.41.0 \ + --hash=sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d \ + --hash=sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a # via # azure-identity # azure-storage-blob -azure-identity==1.25.1 \ - --hash=sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456 \ - --hash=sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651 - # via feast (setup.py) -azure-storage-blob==12.26.0 \ - --hash=sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f \ - --hash=sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe - # via feast (setup.py) -babel==2.17.0 \ - --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ - --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 +azure-identity==1.25.3 \ + --hash=sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6 \ + --hash=sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c + # via feast (pyproject.toml) +azure-storage-blob==12.30.0 \ + --hash=sha256:2cd74d4d5731e5eb6b8d5c5056ee115a5e88f8fdf22517b739836fda685018be \ + --hash=sha256:d415ac50b67a8da6b3ae7e9f1014b1b55cd7aafa0b8d4ca9b380568dc7360423 + # via feast (pyproject.toml) +babel==2.18.0 \ + --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ + --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 # via # jupyterlab-server # sphinx +backports-zstd==1.6.0 \ + --hash=sha256:0308990ffc998df3c7ed35276bde049728b5c3956203cae40d80893576a41459 \ + --hash=sha256:03b7c59c71f7a597e2bcb3f8368371e9a660a1bdf1c37afc1f1ad1496a013c19 \ + --hash=sha256:0466b14723f3b7697669c00ee66fe16e30e25636b286b0a923fa86fa3d8a753c \ + --hash=sha256:068ef3d8c18815a2e3a752f766313e19910e7c50939b956923748d9c04ebcb1b \ + --hash=sha256:08793876172551a930ce4d65c712cd516184d1a97070d4a1193e05bf0cf7040d \ + --hash=sha256:0eb4281f402b94d397b7482f6d9efd04c28274e4ed6eb57eb1f87bdd091a6a87 \ + --hash=sha256:10b61850c4112952e05aa6e6cce8c9a5936fbeadb321e154216705cc76a14afa \ + --hash=sha256:15b1aae0f64cd742df4bba1d989d0a09a6ec619202543fdba684640454541fd3 \ + --hash=sha256:17efb3d11137de5166dd51eedab9c36ad633402acba386eee8d715213ea47e49 \ + --hash=sha256:1a99710fbb225d459d66def4dc2bb2cd4a9a0bdc8b799fc0621cfdd863be9c93 \ + --hash=sha256:1d146926e997d2d3de8212bdcbf4985344a2622ca3bec458d8908000a84fd883 \ + --hash=sha256:1e20b3ecd0a711be82e964aca28554eabbc31ee69a20e5e7b8fd42268af46212 \ + --hash=sha256:23a793f2fed4dbf0517319759a2cded0b0dd8e8d3797fe30badd5693e320c175 \ + --hash=sha256:25b5ddc789480072551af571a746e9500356b2aff0499861cf2ca07ea7431e68 \ + --hash=sha256:28eef3881164f3c23ce58ed59e4684103bdd279583eb2d299858c9e9b72fde9a \ + --hash=sha256:28fecd73459d74910ae1987ab84b7bef690d3dd860948430dd5555108b006daf \ + --hash=sha256:2914abea516704bdafb2090acd3f15b5f9debecfabd15b8dd8285b2ad3b92209 \ + --hash=sha256:2ace939e4d620e119423606f2d3d7115f8707733bf57f279ad9a9383f875986f \ + --hash=sha256:2ba9ac10fc393e5123a08802e0e895a107cb4a66b9973d2844dbd8a343111e59 \ + --hash=sha256:2ddab55a5f54dec8acfad68ef70f1c704fd21919990ddc238afbd6f496e61c6a \ + --hash=sha256:2f723219335387d7546412d8141e0303590600949b4184a1391a0c6a3c756058 \ + --hash=sha256:30576f49b82328ec8af16c11100efe52ca88526f71bbe100ef6b4e707dc13bf2 \ + --hash=sha256:32f04d54ec1fdf3aa648b24a10b1c9234ed2046cc4af7a8850cbc236c05d42f3 \ + --hash=sha256:3b6713371f8987a1178df93cb36f29eef191f224021e2d656b2f11ce60d26816 \ + --hash=sha256:3e689af303df287142770abe3a48bbefd24dab4a09da5807d0e1fa8c75bab026 \ + --hash=sha256:460fd6b3f338c659507ae36cfd6b58ac9942a2ff233c5cf574416dfec0451a84 \ + --hash=sha256:481a1e9bd8f419fdc625307aa20234687f99368c75df511ef589693c5fea4c6f \ + --hash=sha256:4b6c8b02ab0ccb2431bb7bc238be91d158b308915e7b07937388e540466fe7e7 \ + --hash=sha256:4c68a9ed2df0cca51d774c521e68a34d2e3d9ebfc687ef8096adfd4f345b551d \ + --hash=sha256:5918fc6b31437208721276964323933cd86077b8d5b469c59c1b3fd2c8220a05 \ + --hash=sha256:5e9a8370c8ed873083d5de956d6b2e60adbad31e52d7a11111c96ef01d1910ae \ + --hash=sha256:6430b34a2ae6fcc604672f4f913102563473d9a015bdca1ce8c95041cc1f2677 \ + --hash=sha256:64b94d7a836568926a3309ff510c7f8261b881b341fd4992cabf4f0998878f8a \ + --hash=sha256:65048ed08c5124f05ff9f355ab9703014bb2dbe7f8d9948ce193685b1775f442 \ + --hash=sha256:66cf8038893c7708ec345ffb3ac63c775d10f430f323ac2f0334fdb6a397c57c \ + --hash=sha256:6c73ae37dbf9207727ac095dedef864c05d836eaec962a47b3b64eaadaf1c6b6 \ + --hash=sha256:711e6b98f8924e8b4a61ff97ab6321f33de024e1ed6a32f5123763aeda8459be \ + --hash=sha256:7293fefe15f0e5852bdb4ad1e0e26f3cbd4d3e61c19f751ecc4ff34bc1eb237d \ + --hash=sha256:73000459db113a658c4fb0510100ef0e79137b5828bf957b7709aacae4eb1b87 \ + --hash=sha256:75578c71644b031118ce938855a53530708db7f4af6e83e2f8840d5a1de990f8 \ + --hash=sha256:7741e44f7938ec94f9a52678c8d19b7bc548522ffdc39c9e4481af8db545fa9a \ + --hash=sha256:79284c1dd702f4f24ed1a36e51555c907dd237b6c0d829595978f4089a2aeea9 \ + --hash=sha256:7c2b1f4a640c51130caa92cef5bf72bd3c3dbbcfbf814c37403aa0601b1811b0 \ + --hash=sha256:7d3f64c503af7b60115b97c16feaf75bd191ef2c978d5c0c7725a6682bef63c5 \ + --hash=sha256:80a7859ffe70bf239d7a2ce15293bdeb5b4280ff7dc326ffab312b0e254dbb24 \ + --hash=sha256:8219d6fceae6b39535c4ac323dba0923d10f781d59962ff3504e693fdcafa92c \ + --hash=sha256:83391ef5935cc0f329b1abca414ae20ffe40d335fc21a4b5e664f08a74317d5f \ + --hash=sha256:83415af3c64550a56cc20b4cce59bbaa81f21d28466d7adf98feff011ecbc66d \ + --hash=sha256:839faf90a7eb525a401978dc925df8c44bd12526e8ba1529b9f8a7106e729637 \ + --hash=sha256:84f92e5a60a78c72ccda79d0417d311a1f6da18f446423ed411726d545bf7b56 \ + --hash=sha256:8872a0e9f1af975966b5be6af7eebd3dc4046f15e470b719316516dc3d137cd6 \ + --hash=sha256:8c298785e2fadeab82342040f2d9ce764ce500e6da6a6d99a2de514e63580b5a \ + --hash=sha256:97e8a9674652496c7612b528085dd5a296c052a2edc466ca1bfb7b0b27820413 \ + --hash=sha256:994167ff6551b9c1ce226e0aab16295b98c94507b5701aa60d2c32b7d50796b1 \ + --hash=sha256:9cb75e33131946fabd6319061df3b8b1d588fe0963183280e9b5f49f7772fc09 \ + --hash=sha256:a13cfa3410a75e4cb87abdb669aaf79da861cb79299159054ff8f77b9671bc40 \ + --hash=sha256:a3c17e6a267d13de9cbf14bf2ebfa87e03d26692456fc67d2dbed9da4f479b18 \ + --hash=sha256:a4ae7ed5a6d813450cc2d818284ea3db9721edcef50a56aae42ea06feec38c6e \ + --hash=sha256:a838296f5b84c920172fb579cac894d255c1fc25457c7234613ddcfa385e49b7 \ + --hash=sha256:ab70eace272d6f122b121c057e436709b50a28abf30d97aab28433c08f4a4095 \ + --hash=sha256:ae106fe16e36efc60ab098d02478d30aa0e31e1420eb4ecf0116459253bc6361 \ + --hash=sha256:aeef8563b82ed4af328f98e5041c1b4800d86f68f857ffd1577d4d47dc9aa6cd \ + --hash=sha256:b067b1ef9c8e41fb0882c828aa37829938b5c0dab067eca72b23fc24c563b9da \ + --hash=sha256:b0ddbcd2866b8ff1a2836e4b0e4d44788f5b992d83fac75a38cda8f9a2bee079 \ + --hash=sha256:b4bddfcfb6679215d6f4dc5f79a1f9301af339480d70527a14b57a1f2e6b6cbf \ + --hash=sha256:b7bc9a0b66097f03820a54316d2fdd0beb38859cf98f10d63e94c55450ed8920 \ + --hash=sha256:b81b4cf3d6e0ad7ac92bef248f49fafc954262c5fb0f7e19d6aac497e5a856b2 \ + --hash=sha256:b951113113ed4b8d173418a4f155c14b739dace626b3fa3f82be1831958d39e4 \ + --hash=sha256:beb43e9885202c8d4f3762319ed4d5e98e197622afbff8439fbbdd81d08938b9 \ + --hash=sha256:c14fa5dc39a804f1b92d63506f450eca5c59647a18d197d1a564b89dac1be1ce \ + --hash=sha256:c2d1ccfe088e8279d605011a3575619a74526c261be357695b3258c0f636115a \ + --hash=sha256:c4fc41b2df5529cad5ceb230319e82728096d4b353ce8d4df68a2ec37e291bb8 \ + --hash=sha256:d6b9b06323e3ba947c0003b2d70e02f33c90c36bc6262a92eb8201afc4a1aa08 \ + --hash=sha256:d6e78d5e28f812b39f92397806ecddd4a6f3bf35531a8c039a1f187abc931af8 \ + --hash=sha256:dd085eafa2aac6f883afd28210a3231f717f25409a1e44a39bb7b04c8c5b5646 \ + --hash=sha256:df27b57d214a3124fbe4e933ef5a903d4567f154260d9aece8c797a987f2a205 \ + --hash=sha256:e39258a09b1c7ca70b5e94a5c5ccfe4700b4250b8077cfeab31d0f79565d4c9b \ + --hash=sha256:e514c71ca72f3b56bd8fbda1a6a5b7d1100a2764b42a3c74a38841f25f9b00ab \ + --hash=sha256:e73a550dbeb84e8fa50f8385f7735e9a4735b465851ef617d02f80ab10e44e7e \ + --hash=sha256:e80bceebc9b58e959bede9b26cafe15b5b9526f3533a6dd06330c5da73cb9329 \ + --hash=sha256:ece8e7288db5b827ef8c64b2f78519f1a173a8991a625978fce02eccd7654fe9 \ + --hash=sha256:ef132cfb638e9a86bd5dc07fb4e1cb895bc55bce6bb5e759366e8b160d0747e2 \ + --hash=sha256:f69365ee2b836939137de024a302395a1cb8654fb6dc5ffef6381105259c8f87 \ + --hash=sha256:f8f5c1c7c69a4b00889e52d9304a918a5b49010f9645768eb5fd0ad404f790ba \ + --hash=sha256:fa305a84087e10d7a85e8a8a3dcba8cdbda4868f2180173b264b7b488fd37c55 \ + --hash=sha256:fbb746522ebfc11155f1cd688e2c48ef3d74125e38b63eabdaab068a055c3e88 + # via clickhouse-connect bcrypt==5.0.0 \ --hash=sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4 \ --hash=sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a \ @@ -330,84 +431,86 @@ bcrypt==5.0.0 \ --hash=sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822 \ --hash=sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b # via paramiko -beautifulsoup4==4.14.2 \ - --hash=sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e \ - --hash=sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515 +beautifulsoup4==4.15.0 \ + --hash=sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7 \ + --hash=sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9 # via # docling # nbconvert -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -bleach[css]==6.2.0 \ - --hash=sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e \ - --hash=sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +bleach[css]==6.4.0 \ + --hash=sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452 \ + --hash=sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081 # via nbconvert -boto3==1.38.27 \ - --hash=sha256:94bd7fdd92d5701b362d4df100d21e28f8307a67ff56b6a8b0398119cf22f859 \ - --hash=sha256:95f5fe688795303a8a15e8b7e7f255cadab35eae459d00cc281a4fd77252ea80 - # via - # feast (setup.py) - # ikvpy +blinker==1.9.0 \ + --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ + --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc + # via flask +boto3==1.43.46 \ + --hash=sha256:66c0d943b049a46a492ec4ec2ebe73c930b1842c7137bee83aad6d93e95d4d96 \ + --hash=sha256:69453e2c1bcb9fd9806527ab99950cacfc2826cb0dce9a3a0414d19270c06c3c + # via + # feast (pyproject.toml) # moto # snowflake-connector-python -botocore==1.38.46 \ - --hash=sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e \ - --hash=sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b +botocore==1.43.46 \ + --hash=sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533 \ + --hash=sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 # via # aiobotocore # boto3 # moto # s3transfer # snowflake-connector-python -build==1.3.0 \ - --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ - --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 +build==1.5.0 \ + --hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \ + --hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647 # via - # feast (setup.py) + # feast (pyproject.toml) # pip-tools # singlestoredb -cachetools==6.2.1 \ - --hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \ - --hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201 - # via google-auth -cassandra-driver==3.29.2 \ - --hash=sha256:06ad489e4df2cc7f41d3aca8bd8ddeb8071c4fb98240ed07f1dcd9b5180fd879 \ - --hash=sha256:1e89de04809d02bb1d5d03c0946a7baaaf85e93d7e6414885b4ea2616efe9de0 \ - --hash=sha256:2039201ae5d9b7c7ce0930af7138d2637ca16a4c7aaae2fbdd4355fbaf3003c5 \ - --hash=sha256:52edc6d4bd7d07b10dc08b7f044dbc2ebe24ad7009c23a65e0916faed1a34065 \ - --hash=sha256:69aa53f1bdb23487765faa92eef57366637878eafc412f46af999e722353b22f \ - --hash=sha256:6c74610f56a4c53863a5d44a2af9c6c3405da19d51966fabd85d7f927d5c6abc \ - --hash=sha256:70d4d0dce373943308ad461a425fc70a23d0f524859367b8c6fc292400f39954 \ - --hash=sha256:7104e5043e9cc98136d7fafe2418cbc448dacb4e1866fe38ff5be76f227437ef \ - --hash=sha256:7d348c769aa6c37919e7d6247e8cf09c23d387b7834a340408bd7d611f174d80 \ - --hash=sha256:8067fad22e76e250c3846507d804f90b53e943bba442fa1b26583bcac692aaf1 \ - --hash=sha256:83dc9399cdabe482fd3095ca54ec227212d8c491b563a7276f6c100e30ee856c \ - --hash=sha256:957208093ff2353230d0d83edf8c8e8582e4f2999d9a33292be6558fec943562 \ - --hash=sha256:9e36437288d6cd6f6c74b8ee5997692126e24adc2da3d031dc11c7dfea8bc220 \ - --hash=sha256:a1e994a82b2e6ab022c5aec24e03ad49fca5f3d47e566a145de34eb0e768473a \ - --hash=sha256:a66b20c421d8fb21f18bd0ac713de6f09c5c25b6ab3d6043c3779b9c012d7c98 \ - --hash=sha256:a8c496318e3c136cf12ab21e1598fee4b48ea1c71746ea8cc9d32e4dcd09cb93 \ - --hash=sha256:b86427fab4d5a96e91ad82bb9338d4101ae4d3758ba96c356e0198da3de4d350 \ - --hash=sha256:c25b42e1a99f377a933d79ae93ea27601e337a5abb7bb843a0e951cf1b3836f7 \ - --hash=sha256:c4310a7d0457f51a63fb019d8ef501588c491141362b53097fbc62fa06559b7c \ - --hash=sha256:c4a005bc0b4fd8b5716ad931e1cc788dbd45967b0bcbdc3dfde33c7f9fde40d4 \ - --hash=sha256:c53700b0d1f8c1d777eaa9e9fb6d17839d9a83f27a61649e0cbaa15d9d3df34b \ - --hash=sha256:c5a9aab2367e8aad48ae853847a5a8985749ac5f102676de2c119b33fef13b42 \ - --hash=sha256:c86b0a796ff67d66de7df5f85243832a4dc853217f6a3eade84694f6f4fae151 \ - --hash=sha256:d180183451bec81c15e0441fa37a63dc52c6489e860e832cadd854373b423141 \ - --hash=sha256:d70353b6d9d6e01e2b261efccfe90ce0aa6f416588e6e626ca2ed0aff6b540cf \ - --hash=sha256:e31cee01a6fc8cf7f32e443fa0031bdc75eed46126831b7a807ab167b4dc1316 \ - --hash=sha256:e7f1dfa33c3d93350057d6dc163bb92748b6e6a164c408c75cf2c59be0a203b7 \ - --hash=sha256:e967c1341a651f03bdc466f3835d72d3c0a0648b562035e6d780fa0b796c02f6 \ - --hash=sha256:eb3a9f24fc84324d426a69dc35df66de550833072a4d9a4d63d72fda8fcaecb9 \ - --hash=sha256:ee0ebe8eb4fb007d8001ffcd1c3828b74defeb01075d8a1f1116ae9c60f75541 \ - --hash=sha256:f9df1e6ae4201eb2eae899cb0649d46b3eb0843f075199b51360bc9d59679a31 - # via feast (setup.py) -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +cachetools==6.2.6 \ + --hash=sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6 \ + --hash=sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda + # via + # mlflow-skinny + # mlflow-tracing + # pyiceberg + # pymilvus +cassandra-driver==3.30.1 \ + --hash=sha256:0c6a3e1428f7c6a9aa6c944b9c47a37cd2cdbeb5b5a82d42c33afdd56f14e398 \ + --hash=sha256:0ea769cf9925206b8458db10477b78699660fbdaed7262157e43b8f21817f838 \ + --hash=sha256:187bd1457e414bc019a0635603ea0001baada914942198ed23993198e44dec74 \ + --hash=sha256:222c704c72a5880cfe9c89f04b375d03d1152dbdb6ec8c25a325267b8bc2ca4e \ + --hash=sha256:30686850b9e0d0d8326ee07219113170b4e1d041e4d1434521a2f342c471108e \ + --hash=sha256:3c32513a2f2c6832d4fe3464e11be59dc21096dba296b18ec3454bc8354a78ba \ + --hash=sha256:3cb90ec9ac9d44cbfe17f1e9cb643dbc5f401e62980835aa6669a2fd5792863b \ + --hash=sha256:546ad047abf96a6d7922d057010b4cfb22e9d473db7b907956bc7c6a8029f7d9 \ + --hash=sha256:61bf2ded460fb0d2e909b70c9d193588b71148540bd85e29fe6954e00bb1a869 \ + --hash=sha256:6c5ae0218e52944f92ddde07a03cfa941fa4a10097d501a18cfa1b525c92d013 \ + --hash=sha256:71f3ccc8c60f41c83306c60704f7b1ee1a8ccd312aae286bf75d318aa47d0e71 \ + --hash=sha256:799af594364475656d9eea69606713fbaa98b10da82cc6d90286322477c72828 \ + --hash=sha256:80df7b705f71e1a6dae77aaea682cbe45fdd74b962cfcc6c6b0c05195f48b7c2 \ + --hash=sha256:84b2cc578f542332b678fc3fcc801c6ef8110a377d3fd38c27eab5ec5f2d2b79 \ + --hash=sha256:87822d6cce2c586aa72733a9dd1858276cee795834f7ad949c783bf19cf7a3a7 \ + --hash=sha256:8f43abccb5b5a761582e4ba6d44a7ef4274b61b0bd3f8e3159af40ca2c10c0fa \ + --hash=sha256:921403aa76c66d78d00c4a3dff542f989fa584b7450e3cb1dd2c267b4a5a55f0 \ + --hash=sha256:9b54f7c6cbb1d9bb6a8b9f8ab6034e6f761527e97af0603aa1e9635720df03a0 \ + --hash=sha256:9ff317f79b09eba52a44d46d8c4773e47533dfdd8b9ba4984268dc4a1fe5206e \ + --hash=sha256:ab853bd2806899c5612c3e4ac2a355a7811084d06692e4460255e3b0a4fa556a \ + --hash=sha256:af5c6f5d7d5a86725b908aba12d9554df60234c15bfe053a8175b15db791ada8 \ + --hash=sha256:c0995dccff3df739a1df6e10da9f3fc95bbbc8b38bacc4cfb6172655f8351cba \ + --hash=sha256:e10784b4f7d77332dc1a5c72855977a0d797160fd8ba50785414caed07fce95b \ + --hash=sha256:e43d873de5f8dadf4c8dcc4fab87d66a8495137e844ae3c184fbc90a54c05bc0 \ + --hash=sha256:eba7931a615207aab84ebaa000330024f1c614aa86ca58d119556edd338883ce \ + --hash=sha256:ec04eebfadc74039b1f7df84356a822539992c7dc0b013fedbb5a33d9295af86 + # via feast (pyproject.toml) +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # clickhouse-connect # docling @@ -418,296 +521,309 @@ certifi==2025.10.5 \ # minio # requests # snowflake-connector-python -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b - # via - # feast (setup.py) +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f + # via + # feast (pyproject.toml) # argon2-cffi-bindings # cryptography - # ikvpy # pynacl - # snowflake-connector-python -cfgv==3.4.0 \ - --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ - --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 +cfgv==3.5.0 \ + --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ + --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 # via pre-commit -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via # requests # snowflake-connector-python -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask + # flask # geomet # great-expectations + # mlflow-skinny # pip-tools + # pyiceberg # ray # typer # uvicorn -clickhouse-connect==0.9.2 \ - --hash=sha256:02d1d73223493b51922168fc6161908d520de9c2e4015aa1bef3019ab1e8b7d3 \ - --hash=sha256:07c0f5098e9e4c6fa19d7471bc225ca1cf3ace99f01e8dda8be48df947cf0ddf \ - --hash=sha256:0b43416e922c7efd8c636cba7c9dcbeed8e26125d1d0fdf2387e86f6457cbe51 \ - --hash=sha256:113abbc5fa6413322b806fa35455960cb09dd829f3eb951db66164b335daab60 \ - --hash=sha256:13d6dc0776a760f6cbbb473ec75d152b697405e8c7061d5ad320df6fd9df687c \ - --hash=sha256:169fb9d9cf191095d4d06622af856d3c38a5fd4dba3f53f203dd2ac9807858c4 \ - --hash=sha256:19aa0bdfcb93117243a657df2d0f3c4dbf4849d6295afc199efbf680de43daf9 \ - --hash=sha256:1b4bd8f2864ebf1007ea59a2fbaa9fec8b5518030c1ad582645b8690db582305 \ - --hash=sha256:2532df7ee9003ebf1ffb559de39db27c733710ba2d178654aa8ddf8464291391 \ - --hash=sha256:2ab3fd64bbcf52a6a13ff3cf2a74d5c571e3c82d3c6a1d210562daec4ba2ba6b \ - --hash=sha256:2b1cb1542c2e6d1ede32c3d7aed86c06bafe6b5a9d4bb645b5c04f48389f8a4a \ - --hash=sha256:2c1bbe7d541fc776dc04fc0f416431e5245e4b872f5a8375a01094f336bcac57 \ - --hash=sha256:322163909d79b31f981fecdba9f6fe87279c62f6145454e6a191d204b9dd2adb \ - --hash=sha256:3364ca5e08986128d25a8926696d4f1bfb4ceb893b9b2cf93999287c1e98f92f \ - --hash=sha256:340e1353eddc2a5ea5ce073e4665d7ba10c2491254544ef802b592d8128a04e0 \ - --hash=sha256:3bea1477347e93fb14e7bab63f3282ebd8bc23cc9ad76c128534ed9168e2256f \ - --hash=sha256:3bfcf901eb94a218298753980d130bef2d1fa2e9e5845b0385d405092d09661f \ - --hash=sha256:4926616368b0a34173b1db5e567f06e855508ffc0db4d8ce5b3db4eb9bace5c1 \ - --hash=sha256:4cf6d857ee4f2ccd2c008189e521d33e35b462bd3425e4b599fa33800e832e30 \ - --hash=sha256:4d2f9bb8fe9a3fe570057195a1d91f7a6ee8b8ca323e1b74a70c270179482516 \ - --hash=sha256:525e91c972f79dd40ae515b05894f2a2e8c9d0d26da3f1adff2656d1fde250f4 \ - --hash=sha256:567f258de589b0930da43141397febd685acb3f4cf16f517c599bde4b63d35ad \ - --hash=sha256:5b9ce69c06109de2cf5827de7cdb12cf9688b5b4420463ebbfe959959f85e5c7 \ - --hash=sha256:6461bf077736d990a7198c2f352dcc7919bebdf2952993d26d18b8f25f500f7e \ - --hash=sha256:653dd934e545db2b5dda3c436ae68149ca6031149bf928cd7714921c45a66bb9 \ - --hash=sha256:655d6670741ec87497b8b37b7eaada0ec50757b3816c500ec9b7d63775741c92 \ - --hash=sha256:6c878f9eee3157455d623f1e5c233bb47e3f474f1ad8bc53358d7efb65ec199c \ - --hash=sha256:6d77bb17001cf5d88cbae3a579ab7bd76904058f8270bdd5ba3f09015f9efb55 \ - --hash=sha256:7d735cd382cacac5bb846c9a7f97e4b2de806978eca726cb90599c173b15e925 \ - --hash=sha256:7ee21425c7f259641c82ed024d2c0d769ccb1e818bfaa4b63d34636521fda266 \ - --hash=sha256:8521459ff9f7cc88d52f232ab006cd1d383a7b58f25022861f10770e6acde86e \ - --hash=sha256:86510682f80d1c72c12b84cf272db670d963a2f005c26fdf8268fbd494b1e6f3 \ - --hash=sha256:8b666caa538732fadeae6b50ea5d5fd67583be9438c79bd1de9ae69ae6ec0c72 \ - --hash=sha256:8c0b3c2a9ca71bd78839a360a47a5df9b95c280a795f8aee083a3de01e9085fc \ - --hash=sha256:8c9a482eaf951aaf9aa9630fd33b832454a56e1612fd0780c50fbaf2d1e8b977 \ - --hash=sha256:98ac02aa3a27cef3f841b7b2d1817b244ef2215badb3cf94719b3889369e95c3 \ - --hash=sha256:9ab13be35a6c92489d68b6bd4b32afcbedaa9e7c4aacd15a2f1a4fba761a33d8 \ - --hash=sha256:9b7a09548d5542badcfdbe3d62fccc4148124e6da7f061dfbcbdf041b730d372 \ - --hash=sha256:9f2c5e89b78526796379b63c31c0170f0c7000c5321aaad68eb6a70084520eba \ - --hash=sha256:a147cf722d28f49ede19566b32d4e46cafd6a1854f78252d803d8a58fb825b64 \ - --hash=sha256:a4b056da400f671e3f3b96e87eaa1457e511a8781a9587cb3be39c2ce9da3289 \ - --hash=sha256:aead65896287f3322b92b2235e783ab6ea4d03724fc8adc8efc4eddd95ffe91f \ - --hash=sha256:b909b56c4a5184245c7a414d0c4c154ba19fd6c147c6446e265c4c12923156a2 \ - --hash=sha256:ba413a95f992decb47040d887d673e58958f0efb6c1e1cc892c78cfd18885b02 \ - --hash=sha256:bbfbfea214f314db92dcd403ee0b748082c712a65c285c390672ff5421d3cd2a \ - --hash=sha256:bd434920d1f7b72496f368bd6d72538d8bbb3c6bef561fdb75cbc7e5732131c4 \ - --hash=sha256:bd47456aa88fcbc853380a55ef873b173f306b39d13978c4df0a7ba054324999 \ - --hash=sha256:c114ace0c34956ef8d6f7a23a3fe7857bb7b7812e4f85ff04ad2eaffa23ee133 \ - --hash=sha256:c5a78d95ba68e27e6acc768ea0f4f38ebe11b74ae9b00359ae005c6be793b012 \ - --hash=sha256:c8d5f63228da99aa54deee054c7b1f94862df7a3167eb1f960b93e60553b92da \ - --hash=sha256:ca6c200e74321a17b1e12da5fdb5ab104f3c0fcfc1e6389d29fe06ee4d13367b \ - --hash=sha256:d243a01531fecde2570cdc1dc2ad3e4737eb65d2e54dbcb0f5e776323cc90606 \ - --hash=sha256:d2531cf9ac36e5d371d141d8642e159d5081d22db1698c17e23e857894cb60d4 \ - --hash=sha256:d45f35c1734e1a8664edc31015fc490f53e6c912f359416fcd717b1815e28d56 \ - --hash=sha256:db2719ada3cf51aa94d98a4fb01778e795a26b1dbd5843d87d5f89120ef5bed6 \ - --hash=sha256:dd4b6464bb5a61ba913d9fbc5613365b3972a94fd71db7a02e1ae65970986109 \ - --hash=sha256:df25751bb5aa29e40b8b127500ea6a7f89031644c6b6bf9887c1dabe8df2bc36 \ - --hash=sha256:ebc8517d00e94d3677382d01b9c4ff10e212183ec05a72e774dec78b75458fa0 \ - --hash=sha256:ef549994c63d20e0ba1c7af3dfcf64daa9578467a665925ed46aabe682de4fb8 \ - --hash=sha256:f32a862c66174f986f3b64beebe8f336c3e2964c3a562e9deafc3e49277ab65b \ - --hash=sha256:f33efa9e6f08d277305e31b16666efc3299eb86155f0fd7152cf3e022256572b \ - --hash=sha256:f3816d1e07eed15a884bd81b827dccade4857e3ee4e9d81ea573e977e3cffc08 \ - --hash=sha256:f77444cb6e2d961c08fe2f1db130bfa587f3ad71ef09e46e083cf9f9dae90f00 - # via feast (setup.py) -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e - # via dask -codeflare-sdk==0.31.1 \ - --hash=sha256:76fc797903374832592f016b515cc2a504ef2903fc1ebc32886a4d74978f5658 \ - --hash=sha256:df94cf0b74ab412384695d507ef177a856b4fbad1acb612a2f7808ab01dc8bd8 - # via feast (setup.py) +clickhouse-connect==1.6.0 \ + --hash=sha256:01367cc75ddd3081f750ccba65c252385feff72f38f68613ea02dd056d047922 \ + --hash=sha256:064144e1a7f6075ade3c13b8ce49517ca92f86c0c4842bd2abb52a475c026025 \ + --hash=sha256:069f105173cd04263d3bb710381edae341dc9a3aa42fb4673017a617a5a1237a \ + --hash=sha256:06bd2e045a7ec1406f9bd39c92bcc77027258d8969128033cea5adb8602111bd \ + --hash=sha256:0856f42be01ab4957789b8cec99e91311d88bfe82be37ef2d230d36a6a111cff \ + --hash=sha256:163eb12e7aaeefe9e51a483d3ad40502e15ac2ef94cf1f5b7fe8e9f5303d161b \ + --hash=sha256:1a84eb6d545e2647a51b3b4ffa0da4a49f308aaaa096e58c44fb749c171a7d4a \ + --hash=sha256:2d5888d76be3bf1ddea5a210522572d66abcaf651b45ff7c5a1014ec08fe7565 \ + --hash=sha256:2f8b8060a8277509db77b14a01ed70683a0d9becba7a9994a5e59e42e9f98b37 \ + --hash=sha256:30a183095bc367dcc4695869754e36ce2177264d0ef79421990ee24700810ee0 \ + --hash=sha256:30bb77e7aa842550d5265c4aaa3e432b01440fd116d63e7e79ae79c380971ade \ + --hash=sha256:34969b2a57d9372921aef2b0b51fb858d81ce952c5249b6cb5a0d459e846854c \ + --hash=sha256:3cc6bdeafc743739f809671eb3c8fe9af1701981a43c624d9f5bea1e1409e5c4 \ + --hash=sha256:4372dbcbd7b1c897c8d7062832468f82af27fa731c7488421ec7f0a882e1580b \ + --hash=sha256:444564bad257b8923005431d661e347e5360cd686aae67091de3796a465cfec1 \ + --hash=sha256:483e34a7841dbf3d2863e784a129503cc37a910a05464f2f48ce3e9b2f0fc007 \ + --hash=sha256:508d3b7cc893b5d889b92702cc7dc4a1c975f59ee8da383054076969e4cb17c3 \ + --hash=sha256:5135f91841e9df5827faa24f3b2436d4ac8242e74a88a8d47922e31a4383b712 \ + --hash=sha256:51905921e7c98a11e7551663d263f28a715b14fdda191569fc71ddcde964ce52 \ + --hash=sha256:6570d07223ea5e4c4c21fece323cc8741b7711d31c6cb3fda8d1a62fa2d5b8d4 \ + --hash=sha256:691782a911a0696bb1ff644a309f4c6940473ee48d6c5171b13dab96d8cf8ecd \ + --hash=sha256:6bcdb6039bb55870229ec46784840f1420362e3979c74abdc6a53d1ae95718eb \ + --hash=sha256:7043480f1459ddd791480e3ea560a1def81dfd6462eab0c9ada54946e72918af \ + --hash=sha256:743787fd9d1505f55854b333b81b3fa92bf950d988b66a553aaa55c7bfd2f70c \ + --hash=sha256:75390e6ff6b398a88e0cac4907f785763fc9e476314cbafcfb24553bdbd92d28 \ + --hash=sha256:794735a62d0a46688d50652876bcb50116c5e912577e9a469ef6ed1aa52b4d7b \ + --hash=sha256:79caafbd2b1364d4b63c2e7c625cb424c8572b9cedc62b4ac2289cfbdf875487 \ + --hash=sha256:7fdea3ba73b77c636a30fc74c7f2c4c53ed0fcbf0da13498ff4122c701f10362 \ + --hash=sha256:80ca5778ce35d1f658741b993a9e7e17457d3f7a854e904aff72354b29990079 \ + --hash=sha256:8128dfe4a17aef265d5ba2bb5f2f5a0d5fa76e6aca357c71a17da378c1b30ea6 \ + --hash=sha256:8a5c244a31dd2f2d512c7b306e41e592dcaf9550926a96e05bd3222c6a44dacc \ + --hash=sha256:8bdef691c6c66a28301cb1f070f633dfbd60ccdca9580df96984325f1f79e803 \ + --hash=sha256:8d4303987b36ca075cbbe2cff3439ad017f56e1cf2bd936f83b2e6be222ae62f \ + --hash=sha256:9090c972f3d3ab40fc046913cf51c611345d603c922483e3239633f2cda16a3c \ + --hash=sha256:9a7af9bf7b992e5098ed9a0c2edf18d19c6acb2ccc77ee1d2da4e7e5d9dc7ddc \ + --hash=sha256:9dac7ce419ced91f035bd26a86dd49067461547cb9e3b9b1d7c5975b460b650c \ + --hash=sha256:9e64e332983fb268350abc76119c14da7b0640373a77ecf4f310240c3fdb4b6d \ + --hash=sha256:a337751b60c9d15eb9865cfaa32caeb6d81497a9fd3dad248ae112765e97c7f1 \ + --hash=sha256:a34e72d1ae6f71be491b2d44bd10d082d04c9e2e2a04d4774498e965d933b21d \ + --hash=sha256:a5285b9005d5a3ba41d6f148c957e4df8e37a38ad27835a4ad9c31fe21083149 \ + --hash=sha256:a7cf3b7ddffbf0b0bb77cb65935561ae48c49b4e6d0db11188279b068c297862 \ + --hash=sha256:b94bf383735abb90b1a6dc9e89a44f09b84a71a73348be0f01c7c2fb8500fdea \ + --hash=sha256:bd8de68d176a807fd16b85c3c5639c25bbdc81c203217f9e985e9560357fecd5 \ + --hash=sha256:bdd153037df79b684a30372395681bd673b562e9d6c5159c3a08bffc38ad8185 \ + --hash=sha256:c62bd3cc58aa0f6d4cac7012d6e5698cdc23111e3938a809596a5af8459470bd \ + --hash=sha256:c793706c87ab04b29a58e5bc2c0f53806c257859e9ceb859b2840c5aabe56402 \ + --hash=sha256:c924a2dea34578eda62e99a95988134ddaf4f66aa3f842ebd0980973b6acad56 \ + --hash=sha256:ca3d4b70d1fa92e62ab130dec495b470de6e0e56a944cd3c8ea049cb0f137c20 \ + --hash=sha256:cc03b0dd248faca290bced68a575e41331752bfd5863e3ab126382caa547aa1b \ + --hash=sha256:cd4c1dee496ad3a811237ff8066084c18cdaa46a046f9e0eeacce107ac29c83c \ + --hash=sha256:cf66e5bcfba8a183e40485efcfc80329ec75104e22dd02485fe73c0ab9073b19 \ + --hash=sha256:d541a1a75f08d62b7e0059f77efeff26f2b0829fa0d543e1e1edcce270c78a06 \ + --hash=sha256:d56348f1a39c8bd872106c4af92faa532fbdd8d66e72c588e9b4dd676ca2dbbd \ + --hash=sha256:e02c6abbfe777f4d0afdf7726ac30168514c46390b15e12ecf95d873c7b20969 \ + --hash=sha256:e4eb8b83b848f4ca4b984899acd62b0a60f310cbaa559d83519b7477968392ed \ + --hash=sha256:ed28787dd00a34b003fe24de4953e15b172058d875279336d4ee5d8f585acc50 \ + --hash=sha256:effd020411e4669f21d8b9f7f95d2342145ff6a2e32b49eae416fbd733334e53 \ + --hash=sha256:f8957fa2fd400d1f002dded822b3e667b7941e48107d8d2470e1935a12af9937 \ + --hash=sha256:fe2736f4cb520c4403f1f6b078ed3ed6f41a6ee8972328082240001853d8639e \ + --hash=sha256:fe4bdd4e09f52bff46c2f920877a08a26a44d91fdb5fc0be76edb67395440192 + # via feast (pyproject.toml) +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a + # via + # dask + # mlflow-skinny +codeflare-sdk==0.38.2 \ + --hash=sha256:4b2b7e005fdf86181b89c9e75f31613ad56d58e3ca068eb24ce450946f7d5548 \ + --hash=sha256:4e17c7c5f970f21733fc12169cfa13d6198d03374fb0320c62c20712dc9f3ae7 + # via feast (pyproject.toml) colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via - # feast (setup.py) + # feast (pyproject.toml) # great-expectations -colorful==0.5.7 \ - --hash=sha256:495dd3a23151a9568cee8a90fc1174c902ad7ef06655f50b6bddf9e80008da69 \ - --hash=sha256:c5452179b56601c178b03d468a5326cc1fe37d9be81d24d0d6bdab36c4b93ad8 +colorful==0.5.8 \ + --hash=sha256:a9381fdda3337fbaba5771991020abc69676afa102646650b759927892875992 \ + --hash=sha256:bb16502b198be2f1c42ba3c52c703d5f651d826076817185f0294c1a549a7445 # via ray comm==0.2.3 \ --hash=sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971 \ @@ -715,6 +831,80 @@ comm==0.2.3 \ # via # ipykernel # ipywidgets +contourpy==1.3.3 \ + --hash=sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69 \ + --hash=sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc \ + --hash=sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880 \ + --hash=sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a \ + --hash=sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8 \ + --hash=sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc \ + --hash=sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470 \ + --hash=sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5 \ + --hash=sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263 \ + --hash=sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b \ + --hash=sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5 \ + --hash=sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381 \ + --hash=sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3 \ + --hash=sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4 \ + --hash=sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e \ + --hash=sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f \ + --hash=sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772 \ + --hash=sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286 \ + --hash=sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42 \ + --hash=sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301 \ + --hash=sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77 \ + --hash=sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7 \ + --hash=sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411 \ + --hash=sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1 \ + --hash=sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 \ + --hash=sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a \ + --hash=sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b \ + --hash=sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db \ + --hash=sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6 \ + --hash=sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620 \ + --hash=sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989 \ + --hash=sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea \ + --hash=sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67 \ + --hash=sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5 \ + --hash=sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d \ + --hash=sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36 \ + --hash=sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99 \ + --hash=sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1 \ + --hash=sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e \ + --hash=sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b \ + --hash=sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8 \ + --hash=sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d \ + --hash=sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7 \ + --hash=sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7 \ + --hash=sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339 \ + --hash=sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1 \ + --hash=sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659 \ + --hash=sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4 \ + --hash=sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f \ + --hash=sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20 \ + --hash=sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36 \ + --hash=sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb \ + --hash=sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d \ + --hash=sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8 \ + --hash=sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0 \ + --hash=sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b \ + --hash=sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7 \ + --hash=sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe \ + --hash=sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77 \ + --hash=sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497 \ + --hash=sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd \ + --hash=sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1 \ + --hash=sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216 \ + --hash=sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13 \ + --hash=sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae \ + --hash=sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae \ + --hash=sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77 \ + --hash=sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3 \ + --hash=sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f \ + --hash=sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff \ + --hash=sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9 \ + --hash=sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a + # via matplotlib couchbase==4.3.2 \ --hash=sha256:032a180afd6621358b2c73543b9c5db9939b442fc3ad6d54417c36c8a8f65838 \ --hash=sha256:11ce688ed46edf8387bf51866618c7b4e06399e7fb34a6df002764996c109d1f \ @@ -747,7 +937,7 @@ couchbase==4.3.2 \ --hash=sha256:c139c594118e1df6932491c26b825ac777fd201f49cda303892065e607f9c3ef \ --hash=sha256:c18b7c937e63e6d869371d9c4b0a0f9cc1a87dba48950a4276e294c091d76ce5 \ --hash=sha256:c50ae993c81d81ed0f02a37cbe034825408195b9fe29980b3379e2e05b2e1bec - # via feast (setup.py) + # via feast (pyproject.toml) couchbase-columnar==1.0.0 \ --hash=sha256:08b0947ee67f8fb15949e9323d60e5dbb44fb2d86d09f6e997a0bdcde6cd2b15 \ --hash=sha256:0f5ea6a24a73008a2f5a6e3aae51a49f4bb360b198a1f3d2ca4bb22044fe9671 \ @@ -780,210 +970,238 @@ couchbase-columnar==1.0.0 \ --hash=sha256:fa8fbddf971a2391543bc7dafaf3b581ad1a69c1fa0a474295b38a6fd8aed54f \ --hash=sha256:fc0fad2d386c5b5df7aaaccd8751e01caa886cc640cc8c92523dd07c4e7be519 \ --hash=sha256:fc4efa3e15190c3731478006de494b046bc57785e9c8ae99ac8b375a91683e38 - # via feast (setup.py) -coverage[toml]==7.10.7 \ - --hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \ - --hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \ - --hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \ - --hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \ - --hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \ - --hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \ - --hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \ - --hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \ - --hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \ - --hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \ - --hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \ - --hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \ - --hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \ - --hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \ - --hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \ - --hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \ - --hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \ - --hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \ - --hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \ - --hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \ - --hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \ - --hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \ - --hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \ - --hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \ - --hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \ - --hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \ - --hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \ - --hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \ - --hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \ - --hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \ - --hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \ - --hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \ - --hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \ - --hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \ - --hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \ - --hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \ - --hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \ - --hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \ - --hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \ - --hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \ - --hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \ - --hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \ - --hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \ - --hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \ - --hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \ - --hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \ - --hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \ - --hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \ - --hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \ - --hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \ - --hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \ - --hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \ - --hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \ - --hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \ - --hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \ - --hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \ - --hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \ - --hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \ - --hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \ - --hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \ - --hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \ - --hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \ - --hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \ - --hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \ - --hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \ - --hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \ - --hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \ - --hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \ - --hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \ - --hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \ - --hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \ - --hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \ - --hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \ - --hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \ - --hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \ - --hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \ - --hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \ - --hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \ - --hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \ - --hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \ - --hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \ - --hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \ - --hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \ - --hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \ - --hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \ - --hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \ - --hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \ - --hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \ - --hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \ - --hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \ - --hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \ - --hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \ - --hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \ - --hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \ - --hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \ - --hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \ - --hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \ - --hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \ - --hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \ - --hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \ - --hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \ - --hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \ - --hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \ - --hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3 + # via feast (pyproject.toml) +coverage[toml]==7.15.2 \ + --hash=sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2 \ + --hash=sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e \ + --hash=sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db \ + --hash=sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf \ + --hash=sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c \ + --hash=sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8 \ + --hash=sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443 \ + --hash=sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a \ + --hash=sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145 \ + --hash=sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9 \ + --hash=sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2 \ + --hash=sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376 \ + --hash=sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138 \ + --hash=sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578 \ + --hash=sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c \ + --hash=sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88 \ + --hash=sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036 \ + --hash=sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c \ + --hash=sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071 \ + --hash=sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a \ + --hash=sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d \ + --hash=sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b \ + --hash=sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a \ + --hash=sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b \ + --hash=sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050 \ + --hash=sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846 \ + --hash=sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d \ + --hash=sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1 \ + --hash=sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660 \ + --hash=sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40 \ + --hash=sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026 \ + --hash=sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0 \ + --hash=sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b \ + --hash=sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0 \ + --hash=sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa \ + --hash=sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d \ + --hash=sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658 \ + --hash=sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89 \ + --hash=sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072 \ + --hash=sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199 \ + --hash=sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446 \ + --hash=sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743 \ + --hash=sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7 \ + --hash=sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1 \ + --hash=sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287 \ + --hash=sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5 \ + --hash=sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be \ + --hash=sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688 \ + --hash=sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934 \ + --hash=sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7 \ + --hash=sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440 \ + --hash=sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984 \ + --hash=sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc \ + --hash=sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d \ + --hash=sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098 \ + --hash=sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6 \ + --hash=sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629 \ + --hash=sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b \ + --hash=sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee \ + --hash=sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f \ + --hash=sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1 \ + --hash=sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad \ + --hash=sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3 \ + --hash=sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9 \ + --hash=sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3 \ + --hash=sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a \ + --hash=sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296 \ + --hash=sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1 \ + --hash=sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd \ + --hash=sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73 \ + --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f \ + --hash=sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0 \ + --hash=sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6 \ + --hash=sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5 \ + --hash=sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1 \ + --hash=sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589 \ + --hash=sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688 \ + --hash=sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487 \ + --hash=sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9 \ + --hash=sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd \ + --hash=sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee \ + --hash=sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d \ + --hash=sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d \ + --hash=sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb \ + --hash=sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a \ + --hash=sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328 \ + --hash=sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635 \ + --hash=sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188 \ + --hash=sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c \ + --hash=sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243 \ + --hash=sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a # via pytest-cov -cryptography==43.0.3 \ - --hash=sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362 \ - --hash=sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4 \ - --hash=sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa \ - --hash=sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83 \ - --hash=sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff \ - --hash=sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805 \ - --hash=sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6 \ - --hash=sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664 \ - --hash=sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08 \ - --hash=sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e \ - --hash=sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18 \ - --hash=sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f \ - --hash=sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73 \ - --hash=sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5 \ - --hash=sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984 \ - --hash=sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd \ - --hash=sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3 \ - --hash=sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e \ - --hash=sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405 \ - --hash=sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2 \ - --hash=sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c \ - --hash=sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995 \ - --hash=sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73 \ - --hash=sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16 \ - --hash=sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7 \ - --hash=sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd \ - --hash=sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7 - # via - # feast (setup.py) +cryptography==48.0.1 \ + --hash=sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a \ + --hash=sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02 \ + --hash=sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471 \ + --hash=sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f \ + --hash=sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa \ + --hash=sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a \ + --hash=sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1 \ + --hash=sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225 \ + --hash=sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92 \ + --hash=sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6 \ + --hash=sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24 \ + --hash=sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1 \ + --hash=sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f \ + --hash=sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72 \ + --hash=sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6 \ + --hash=sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8 \ + --hash=sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577 \ + --hash=sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67 \ + --hash=sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a \ + --hash=sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429 \ + --hash=sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1 \ + --hash=sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd \ + --hash=sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9 \ + --hash=sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265 \ + --hash=sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a \ + --hash=sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475 \ + --hash=sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d \ + --hash=sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3 \ + --hash=sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c \ + --hash=sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1 \ + --hash=sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac \ + --hash=sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6 \ + --hash=sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2 \ + --hash=sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08 \ + --hash=sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c \ + --hash=sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b \ + --hash=sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401 \ + --hash=sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158 \ + --hash=sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8 \ + --hash=sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9 \ + --hash=sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411 \ + --hash=sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4 \ + --hash=sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991 \ + --hash=sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17 \ + --hash=sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242 \ + --hash=sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691 \ + --hash=sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41 \ + --hash=sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345 \ + --hash=sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46 + # via + # feast (pyproject.toml) # azure-identity # azure-storage-blob # codeflare-sdk + # google-auth # great-expectations # jwcrypto + # mlflow # moto # msal + # oracledb # paramiko # pyjwt # pyopenssl # snowflake-connector-python # types-pyopenssl # types-redis -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) -datasets==4.0.0 \ - --hash=sha256:7ef95e62025fd122882dbce6cb904c8cd3fbc829de6669a5eb939c77d50e203d \ - --hash=sha256:9657e7140a9050db13443ba21cb5de185af8af944479b00e7ff1e00a61c8dbf1 - # via feast (setup.py) -db-dtypes==1.4.3 \ - --hash=sha256:a1c92b819af947fae1701d80a71f2a0eac08f825ca52cf0c68aeba80577ae966 \ - --hash=sha256:d3cb08c32939d24e05501f17694be8c1c54cfb4620d61fb56fb311e8c3d8885e +cycler==0.12.1 \ + --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ + --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c + # via matplotlib +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) +databricks-sdk==0.122.0 \ + --hash=sha256:7d998ec47f580be6be1cfe066be78038054767575b7437af5e4f76b67dde49c2 \ + --hash=sha256:eb11be13f0c02fc4ffc6e2672ad47478ce26ac4b310bd26916af8ba56e374e98 + # via + # mlflow-skinny + # mlflow-tracing +datasets==5.0.1 \ + --hash=sha256:9fbf73688f8c18f7529b4fe592abd04015f81d1e58001e4bac73ffb2b39d7cc4 \ + --hash=sha256:ce22bb851efd7494f08aad33b940803784434f6e77763d00679a0dc45fcf686a + # via feast (pyproject.toml) +db-dtypes==1.7.1 \ + --hash=sha256:450b94cd010cfa6d4d13cca2593009eba4fbe15516084c332cde1a47bbce0625 \ + --hash=sha256:75adf3bdf3174a6474ea4162d9a345ea0b3102ec4fd603e69ec26b6e8a99145a # via # google-cloud-bigquery # pandas-gbq -debugpy==1.8.17 \ - --hash=sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1 \ - --hash=sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3 \ - --hash=sha256:17e456da14848d618662354e1dccfd5e5fb75deec3d1d48dc0aa0baacda55860 \ - --hash=sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc \ - --hash=sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4 \ - --hash=sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088 \ - --hash=sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670 \ - --hash=sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef \ - --hash=sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf \ - --hash=sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420 \ - --hash=sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464 \ - --hash=sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c \ - --hash=sha256:8deb4e31cd575c9f9370042876e078ca118117c1b5e1f22c32befcfbb6955f0c \ - --hash=sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a \ - --hash=sha256:b13eea5587e44f27f6c48588b5ad56dcb74a4f3a5f89250443c94587f3eb2ea1 \ - --hash=sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4 \ - --hash=sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f \ - --hash=sha256:b75868b675949a96ab51abc114c7163f40ff0d8f7d6d5fd63f8932fd38e9c6d7 \ - --hash=sha256:bb1bbf92317e1f35afcf3ef0450219efb3afe00be79d8664b250ac0933b9015f \ - --hash=sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542 \ - --hash=sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f \ - --hash=sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840 \ - --hash=sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83 \ - --hash=sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da \ - --hash=sha256:e851beb536a427b5df8aa7d0c7835b29a13812f41e46292ff80b2ef77327355a \ - --hash=sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464 \ - --hash=sha256:eaa85bce251feca8e4c87ce3b954aba84b8c645b90f0e6a515c00394a9f5c0e7 \ - --hash=sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d \ - --hash=sha256:f2ac8055a0c4a09b30b931100996ba49ef334c6947e7ae365cdd870416d7513e \ - --hash=sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e +dbt-artifacts-parser==0.14.0 \ + --hash=sha256:c4da799afa80a3c2c6c7e51c3b5d2681be9fdd53174b3909a3ce71cfb43d5148 \ + --hash=sha256:e983510193cacaf6e6152e41cccfafc6cece592899f306f3143dc2f8b15ea394 + # via feast (pyproject.toml) +debugpy==1.8.21 \ + --hash=sha256:0042da0ecd0a8b50dc4a54395ecd870d258d73fa18776f50c91fdcabdcad2675 \ + --hash=sha256:0fddfdc130ac6d8bfc0415b0409822fa901c8f310e5c945ac5653a0352532344 \ + --hash=sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88 \ + --hash=sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c \ + --hash=sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1 \ + --hash=sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e \ + --hash=sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9 \ + --hash=sha256:4e70cc8b5079f885cb43910924ee0aab73b8b6b2a14eff23afdd9895d86e79eb \ + --hash=sha256:4e7c2d784d78ad4b71a5f8cd7b59c167719ec8a7a0211dbb3eb1bfeda78bc4e2 \ + --hash=sha256:72b5d676c4cbfac3bac5bb01c138a4656e843f93f03ce2a5f4e394ad49fbee73 \ + --hash=sha256:84c564d8cc701d41843b29a92814c1f1bef6798724ca9d675c284ad9f6a547d7 \ + --hash=sha256:8eeab7b5462f683452c57c0126aaa5ec4e974ddb705f39ba87dff8818c8e08f9 \ + --hash=sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782 \ + --hash=sha256:9f5171176a0084b95d2ebe55a4d1f7b2a75b74c5dbec577ebd3a85c740551c36 \ + --hash=sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e \ + --hash=sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6 \ + --hash=sha256:a7fe47fd23da57b9e0bec3f4a8ee65a2dc55782455ed7f2141d75ab5d2eaeef5 \ + --hash=sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0 \ + --hash=sha256:aa9d941d6dfe3d0407e4b3ca0b9ec466030e260fbf1174094f68785680f66db6 \ + --hash=sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92 \ + --hash=sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c \ + --hash=sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176 \ + --hash=sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264 \ + --hash=sha256:e935f9dc0501be523c8a8e1853c39432e1354e9ece717ae5998fd2371c4542c3 \ + --hash=sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2 \ + --hash=sha256:f15c10084f9861b5e8414a48f18f8e4aadf51a98a59e72c16aa28281ca994672 \ + --hash=sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc \ + --hash=sha256:f843a8b08c2edeaf9b1582eed4f25441af21a297c22ff16bf76a662557aa9c9e \ + --hash=sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8 \ + --hash=sha256:ffd932c6796afadab6993ec96745918a8cb2444dbd392074f769db5ea40ab440 # via ipykernel -decorator==5.2.1 \ - --hash=sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360 \ - --hash=sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a +decorator==5.3.1 \ + --hash=sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82 \ + --hash=sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c # via ipython defusedxml==0.7.1 \ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 - # via nbconvert + # via + # docling-core + # nbconvert deltalake==0.25.5 \ --hash=sha256:0b36afba5936f74c42920c06d140535e6efc8361f659770014944d8e69fbca09 \ --hash=sha256:0ca70e824fd7bcd16aeaaf9a43800eb9dc6c5d05b7854328c4cb4a240643ef78 \ @@ -992,134 +1210,123 @@ deltalake==0.25.5 \ --hash=sha256:76be7e1ed8d13f2dc933361057a44a47a89e6112d4f5ea0a73fb510bedd96efc \ --hash=sha256:cb1c7e826fd7c3bdd3676c7471d3b551e1a3674e44cd8e3747a0017a2c0292b7 \ --hash=sha256:e8f0d24bf64455f702da8402307b22e01f91e0f76694f7c5e33c9513011e8d29 - # via feast (setup.py) + # via feast (pyproject.toml) +deprecated==1.3.1 \ + --hash=sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f \ + --hash=sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223 + # via cassandra-driver deprecation==2.1.0 \ --hash=sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff \ --hash=sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a # via python-keycloak -dill==0.3.8 \ - --hash=sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca \ - --hash=sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7 +dill==0.3.9 \ + --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ + --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c # via - # feast (setup.py) + # feast (pyproject.toml) # datasets # multiprocess -distlib==0.4.0 \ - --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ - --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d +distlib==0.4.3 \ + --hash=sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b \ + --hash=sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed # via virtualenv -docker==7.1.0 \ - --hash=sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c \ - --hash=sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0 - # via testcontainers +dnspython==2.8.0 \ + --hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \ + --hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f + # via + # feast (pyproject.toml) + # pymongo +docker==7.2.0 \ + --hash=sha256:a3f45fdeb9165e2d25d9a1d02ddf3bc70fb572cf5ebbf9b58558c22caf29b71f \ + --hash=sha256:cebb93773d334f778e023a7ee352a8d6e13ab1bd3b863a4d4a59dec897df43ac + # via + # mlflow + # testcontainers docling==2.27.0 \ --hash=sha256:1288ed75b27e33bf94daff34faffc6d11b7d7ccc13e3df84fb24adad3991f72d \ --hash=sha256:faba35662612a2c687a3a463e501d95f645316436084af92a0442ce162429a3d - # via feast (setup.py) -docling-core[chunking]==2.48.4 \ - --hash=sha256:367675c1165d0934ae498fa57ca2d27ef0468aad74dc44a5ab061f5d87882ea1 \ - --hash=sha256:d87ce3021cdae3d073ce7572a2396b69be3cde82ebf9a74d4bad1e1cdfdfd524 + # via feast (pyproject.toml) +docling-core[chunking]==2.84.0 \ + --hash=sha256:2aee881b94138234c703298066b5ed94999490323ae6a29cbe5110f9248b83dd \ + --hash=sha256:331035d5032be683a6d66d476146652491d2c75e4562e04da3f1a8d989d74bfc # via # docling # docling-ibm-models # docling-parse -docling-ibm-models==3.9.1 \ - --hash=sha256:ac6cd1c2be93437cbb5c1f1a1a4030792a38859a1655b14f25cbc8aec760c351 \ - --hash=sha256:f2d845703877a3ca8853b57775eb8e88a7a9503d4fa110500a2550b8d63d0098 +docling-ibm-models==3.13.3 \ + --hash=sha256:50fc83d244ce2dc43158e02155d1caa68374d7a169d0f34e67d7eb14f396ffe6 \ + --hash=sha256:8c8b55e80b3d20fc74d85ca49a4d064578ef75b9f7251eec42fc9df6af426218 # via docling -docling-parse==4.5.0 \ - --hash=sha256:0a1a5f3e2f11ea74ab28d9c04b9391fa4b929c4af045c16bfb0da1e377646e54 \ - --hash=sha256:217fe2466ca2723bdecbdb162ca73891c1746ec15b8d99ec203f8df3305091a5 \ - --hash=sha256:24360a0985a8f76ff99c39e533d208bb57427caf96b9ceb585090cd10558f87a \ - --hash=sha256:256019969f1edc08b051a90fe739430593aaf7cd59fb18a2e00745f18533ce43 \ - --hash=sha256:368ebdb22ec03aa29b25d2684e51c74f6e167ab6809cd7bb5bb5b97cfe21bf8c \ - --hash=sha256:41ae6a7f0139d48b9ce8e0a7c43be003e6fa9382919a7efa76153bd1cdbb5e21 \ - --hash=sha256:4c8906d076219a18f4f86b1fec4e4cc3699460e78c88a5731ead48dfbb71835a \ - --hash=sha256:52df1c5bbafe5199c090bf47eb802c2fe40173fb438200f9a7cbe401aa1eed74 \ - --hash=sha256:5688fe4281dac16e807496c0b19587e25c53a9542d12f36b3a8fb2e66de78eb2 \ - --hash=sha256:8beb4f2c79c676b93ab3a14f86586adb51c3d5a2e3c1a902186e4cd6ed0a2e45 \ - --hash=sha256:99e353ab01ac5c81318b67f42c4fc83ac4a0b5b4783bc566f19656204acf45f0 \ - --hash=sha256:9bf94bc213bedd6d880d94eface2285e9e344da5452a23b3a8d0fedecb5d3ec1 \ - --hash=sha256:9d02c43d3185f5f4a6d5aaad38e69e07bbd1f965fd62f331bd9dfc006a637604 \ - --hash=sha256:a5f0bcdd6c84acc3f3a4c1f0fb96be7e9cff7a0bdff85f2f13caa80d2a9fac8f \ - --hash=sha256:affdecc41ed18f1a82c56edac2b815535e3cc07e2b0f8ffaee7e4adfb1333f0e \ - --hash=sha256:b7c9e8954118331438eb8da6058da0e3caf12735b47a86af9521e44465bbb2d4 \ - --hash=sha256:c3dba06a3cb8797587c90f5aa10cc2c51803d8f5cd67342ea948288a30503868 \ - --hash=sha256:c49193988b56133149584fed70b176de85c95fe698849b2acf68fde9df3a93e5 \ - --hash=sha256:d0ea05741721a76cfca6559d7cac283f2b2953915745b439be0ca8557864bb33 \ - --hash=sha256:d84186662e4780375de28b1bcb18112b04bd8e6aedb787d96544cc0d687f9629 \ - --hash=sha256:d8b2a25262a09e956516c4439ae143a66a55212f0ef9945928159caf1346408f \ - --hash=sha256:da6e535463bcb19a64f3099bb73b299e1f6f49a1ef3b0b3ea4fa62e2790ad875 \ - --hash=sha256:dac5e9907cd6fd020bc1620082dacb9b99bfc9ee4001c55c4e4ce156edf3b617 \ - --hash=sha256:e78f648c3a8af5ddb7dcc30c6c4270e9d3257366396a020ad60657de98bf88f5 \ - --hash=sha256:e8b283a93860cdf43a93296e1721e25daeb8eede14417b9f188f0f52c010d6b5 \ - --hash=sha256:e9223485df491432f5549dd4566c6649ff32f54370701a004673e27e6fa94a9e \ - --hash=sha256:ee02646e7a158c9f67d8df0052b544f1240d3c28eefa4658603931c13eac4435 \ - --hash=sha256:f830409eb96b063ae9f3f4e676f760b0d9738bcb0708ba6b840b7e0c84c490bd \ - --hash=sha256:f983d65703a165b76775c3e4b2a5cade4757216eb88faf5c0c86a9b33f38549a +docling-parse==4.7.3 \ + --hash=sha256:1790e7e4ae202d67875c1c48fd6f8ef5c51d10b0c23157e4989b8673f2f31308 \ + --hash=sha256:281347b3e937c1a5ffa6f8774ee603b64a0899fe8a6885573dec7eb48a3421d8 \ + --hash=sha256:29c91f78c877ae4637011efdb478f20a571e6794be924795b3469958a6401cd6 \ + --hash=sha256:32a2a8aedc56e82e2e3337b7afb83070db1fcfde86cbd93bba80ef2e331b6c13 \ + --hash=sha256:3b04459cc97a8a4929622e341b9981e23987a63af07db599afc5e1c4d389060b \ + --hash=sha256:45ec74bda63738c72e9f3989d19ef6ea7e3b1d61328ffc68d55b1b18eb6c4002 \ + --hash=sha256:53bd45241dca228715800afa0f96fdc826f7c234e9effcd5cefc86026ff19301 \ + --hash=sha256:5936e6bcb7969c2a13f38ecc75cada3b0919422dc845e96da4b0b7b3bbc394ce \ + --hash=sha256:5fc8f4770f9f6f90ba25f52451864a64394ddb158aea3a8fdda46a208c029cf6 \ + --hash=sha256:659234b800c094525476c6a97e771cd61491201e0c9f4af8ee6d39df9758bcae \ + --hash=sha256:65e0653d9617d38e73bab069dc3e7960668ff4a6b0ff45a7635c3790eeed8a08 \ + --hash=sha256:66896bbe925073e4d48f18ec29dcd611a390d6b2378fae72125e77b020cd5664 \ + --hash=sha256:6cb4fe8c62de06b70e6b38c4bd608f41ea3e9d7154a4e05f9a3c4d8944fe3a25 \ + --hash=sha256:75522790df921b6be5d86cf26d184a4af97c1c65e2d22698a9516bc049c398cf \ + --hash=sha256:91b9fbe8209922f46bbd8c6fd1a44193a4c364ff3fa398af7bcc8aaa404567d9 \ + --hash=sha256:978e7e7032760385264896871ae87cb3a04081766cc966c57e9750ce803162ac \ + --hash=sha256:9d18a5b1f7eecabed631c497a19f19d281a0d86f24bfe5d239e3df89bdc4df32 \ + --hash=sha256:a6e0f9e18d808c87ce0fe1900c74a3496a42743f4bba7ed4dd83a0e6e168644a \ + --hash=sha256:bd23eeb479355316fe807703220439fd1de1df4ca0145a49c35f71b184f87254 \ + --hash=sha256:c5a416ae2e1761914ee8d7dbfbe3858e106c876b5a7fccaa3917c038e2f126ec \ + --hash=sha256:ca64977a19ecd580a48f22137a30470d7ccf0995b2c25a74136c6facec7c617d \ + --hash=sha256:d3d86c51f9ce35a1b40b2f410f7271d9bd5fc58e7240f4cae7fdd2cef757e671 \ + --hash=sha256:d89231aa4fba3e38b80c11beb8edc07569e934c1f3935b51f57904fefe958ba5 \ + --hash=sha256:dc32b6f25a673e41b9a8112b6b841284f60dbac9427b7848a03b435460f74aee \ + --hash=sha256:dffd19ed373b0da5cea124606b183489a8686c3d18643e94485be1bdda5713ea \ + --hash=sha256:ef691045623863624f2cb7347572d0262a53cb84940ef7dd851d9f13a2eb8833 \ + --hash=sha256:f4a93f91f97055e19cade33bb957d83f8615f1d2a0103b89827aca16b31a3e22 # via docling docutils==0.19 \ --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \ --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc # via sphinx -duckdb==1.1.3 \ - --hash=sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586 \ - --hash=sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd \ - --hash=sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e \ - --hash=sha256:09c68522c30fc38fc972b8a75e9201616b96ae6da3444585f14cf0d116008c95 \ - --hash=sha256:0a55169d2d2e2e88077d91d4875104b58de45eff6a17a59c7dc41562c73df4be \ - --hash=sha256:0ba6baa0af33ded836b388b09433a69b8bec00263247f6bf0a05c65c897108d3 \ - --hash=sha256:183ac743f21c6a4d6adfd02b69013d5fd78e5e2cd2b4db023bc8a95457d4bc5d \ - --hash=sha256:1aa3abec8e8995a03ff1a904b0e66282d19919f562dd0a1de02f23169eeec461 \ - --hash=sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce \ - --hash=sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24 \ - --hash=sha256:2141c6b28162199999075d6031b5d63efeb97c1e68fb3d797279d31c65676269 \ - --hash=sha256:252d9b17d354beb9057098d4e5d5698e091a4f4a0d38157daeea5fc0ec161670 \ - --hash=sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9 \ - --hash=sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3 \ - --hash=sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289 \ - --hash=sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1 \ - --hash=sha256:4f0e2e5a6f5a53b79aee20856c027046fba1d73ada6178ed8467f53c3877d5e0 \ - --hash=sha256:51c6d79e05b4a0933672b1cacd6338f882158f45ef9903aef350c4427d9fc898 \ - --hash=sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8 \ - --hash=sha256:5ace6e4b1873afdd38bd6cc8fcf90310fb2d454f29c39a61d0c0cf1a24ad6c8d \ - --hash=sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff \ - --hash=sha256:6411e21a2128d478efbd023f2bdff12464d146f92bc3e9c49247240448ace5a6 \ - --hash=sha256:647f17bd126170d96a38a9a6f25fca47ebb0261e5e44881e3782989033c94686 \ - --hash=sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c \ - --hash=sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31 \ - --hash=sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7 \ - --hash=sha256:80158f4c7c7ada46245837d5b6869a336bbaa28436fbb0537663fa324a2750cd \ - --hash=sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898 \ - --hash=sha256:8ee97ec337794c162c0638dda3b4a30a483d0587deda22d45e1909036ff0b739 \ - --hash=sha256:911d58c22645bfca4a5a049ff53a0afd1537bc18fedb13bc440b2e5af3c46148 \ - --hash=sha256:9c619e4849837c8c83666f2cd5c6c031300cd2601e9564b47aa5de458ff6e69d \ - --hash=sha256:9d0767ada9f06faa5afcf63eb7ba1befaccfbcfdac5ff86f0168c673dd1f47aa \ - --hash=sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4 \ - --hash=sha256:a1f83c7217c188b7ab42e6a0963f42070d9aed114f6200e3c923c8899c090f16 \ - --hash=sha256:a1fa0c502f257fa9caca60b8b1478ec0f3295f34bb2efdc10776fc731b8a6c5f \ - --hash=sha256:a30dd599b8090ea6eafdfb5a9f1b872d78bac318b6914ada2d35c7974d643640 \ - --hash=sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38 \ - --hash=sha256:a4748635875fc3c19a7320a6ae7410f9295557450c0ebab6d6712de12640929a \ - --hash=sha256:b74e121ab65dbec5290f33ca92301e3a4e81797966c8d9feef6efdf05fc6dafd \ - --hash=sha256:c443d3d502335e69fc1e35295fcfd1108f72cb984af54c536adfd7875e79cee5 \ - --hash=sha256:c5336939d83837af52731e02b6a78a446794078590aa71fd400eb17f083dda3e \ - --hash=sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378 \ - --hash=sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9 \ - --hash=sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee \ - --hash=sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32 \ - --hash=sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537 \ - --hash=sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989 \ - --hash=sha256:ecb1dc9062c1cc4d2d88a5e5cd8cc72af7818ab5a3c0f796ef0ffd60cfd3efb4 \ - --hash=sha256:eeacb598120040e9591f5a4edecad7080853aa8ac27e62d280f151f8c862afa3 \ - --hash=sha256:f549af9f7416573ee48db1cf8c9d27aeed245cb015f4b4f975289418c6cf7320 \ - --hash=sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3 \ - --hash=sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b +duckdb==1.5.5 \ + --hash=sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053 \ + --hash=sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4 \ + --hash=sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c \ + --hash=sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a \ + --hash=sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528 \ + --hash=sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c \ + --hash=sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282 \ + --hash=sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f \ + --hash=sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7 \ + --hash=sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f \ + --hash=sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c \ + --hash=sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb \ + --hash=sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433 \ + --hash=sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8 \ + --hash=sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28 \ + --hash=sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238 \ + --hash=sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece \ + --hash=sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1 \ + --hash=sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151 \ + --hash=sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2 \ + --hash=sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353 \ + --hash=sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc \ + --hash=sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079 \ + --hash=sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a \ + --hash=sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6 \ + --hash=sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe \ + --hash=sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de \ + --hash=sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0 \ + --hash=sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e \ + --hash=sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f \ + --hash=sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf \ + --hash=sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae \ + --hash=sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6 \ + --hash=sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe \ + --hash=sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366 # via ibis-framework -dunamai==1.25.0 \ - --hash=sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab \ - --hash=sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1 - # via poetry-dynamic-versioning durationpy==0.10 \ --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 @@ -1127,33 +1334,29 @@ durationpy==0.10 \ easyocr==1.7.2 \ --hash=sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c # via docling -elastic-transport==9.1.0 \ - --hash=sha256:1590e44a25b0fe208107d5e8d7dea15c070525f3ac9baafbe4cb659cd14f073d \ - --hash=sha256:369fa56874c74daae4ea10cbf40636d139f38f42bec0e006b9cd45a168ee7fce +elastic-transport==9.4.2 \ + --hash=sha256:33dc89bb1855faa8b98ae8b036405a39c562778dbcdbe4a00a2eaf753148556c \ + --hash=sha256:366f4614f4544c5fb5d780c82f57af8f30492b44a68ed20750390aa81e20c2ea # via elasticsearch -elasticsearch==9.1.1 \ - --hash=sha256:2a5c27c57ca3dd3365f665c82c9dcd8666ccfb550d5b07c688c21ec636c104e5 \ - --hash=sha256:be20acda2a97591a9a6cf4981fc398ee6fca3291cf9e7a9e52b6a9f41a46d393 - # via feast (setup.py) +elasticsearch==9.4.1 \ + --hash=sha256:1d78fdfba97a903ec35a5eb5808a74e33392b7c620bd5f742d465a3a26c27d75 \ + --hash=sha256:71ab71c3d1b20fd88c2922fb82c3277cce7ea03c160686e7b9368b265c2b4cac + # via feast (pyproject.toml) entrypoints==0.4 \ --hash=sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4 \ --hash=sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f # via altair -environs==9.5.0 \ - --hash=sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124 \ - --hash=sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9 - # via pymilvus et-xmlfile==2.0.0 \ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \ --hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54 # via openpyxl -execnet==2.1.1 \ - --hash=sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc \ - --hash=sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3 +execnet==2.1.2 \ + --hash=sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd \ + --hash=sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec # via pytest-xdist -executing==1.2.0 \ - --hash=sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc \ - --hash=sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107 +executing==2.2.1 \ + --hash=sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4 \ + --hash=sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017 # via # codeflare-sdk # stack-data @@ -1183,24 +1386,27 @@ faiss-cpu==1.10.0 \ --hash=sha256:e02af3696a6b9e1f9072e502f48095a305de2163c42ceb1f6f6b1db9e7ffe574 \ --hash=sha256:e71f7e24d5b02d3a51df47b77bd10f394a1b48a8331d5c817e71e9e27a8a75ac \ --hash=sha256:f71c5860c860df2320299f9e4f2ca1725beb559c04acb1cf961ed24e6218277a - # via feast (setup.py) -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 # via - # feast (setup.py) + # feast (pyproject.toml) + # milvus-lite +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e + # via + # feast (pyproject.toml) # fastapi-mcp + # mlflow-skinny fastapi-mcp==0.4.0 \ --hash=sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec \ --hash=sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d - # via feast (setup.py) -fastjsonschema==2.21.2 \ - --hash=sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 \ - --hash=sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de + # via feast (pyproject.toml) +fastjsonschema==2.22.1 \ + --hash=sha256:0b83d1ce8d7845b959dcb20e1a5c3c8883b6541d9c52ab02cce5166b75ec805f \ + --hash=sha256:cf377ff5c9a6f4f3125fb35f75a2c5767bd824ffbcf62c209a93cd48d1453999 # via nbformat -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 +filelock==3.32.0 \ + --hash=sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402 \ + --hash=sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3 # via # datasets # huggingface-hub @@ -1213,6 +1419,68 @@ filetype==1.2.0 \ --hash=sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb \ --hash=sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25 # via docling +flask==3.1.3 \ + --hash=sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb \ + --hash=sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c + # via + # flask-cors + # mlflow +flask-cors==6.0.5 \ + --hash=sha256:30c5031552cd59f620ac0c8211dac45b345d3b2df310e7721879e4f46ef9c601 \ + --hash=sha256:68fcf75693e961f3af26683b23c4b9a8fb6b64de17d20d0c37b95e8de7ab2ed8 + # via mlflow +fonttools==4.63.0 \ + --hash=sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69 \ + --hash=sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c \ + --hash=sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac \ + --hash=sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096 \ + --hash=sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d \ + --hash=sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68 \ + --hash=sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616 \ + --hash=sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78 \ + --hash=sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f \ + --hash=sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b \ + --hash=sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b \ + --hash=sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02 \ + --hash=sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d \ + --hash=sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f \ + --hash=sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8 \ + --hash=sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272 \ + --hash=sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49 \ + --hash=sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419 \ + --hash=sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001 \ + --hash=sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03 \ + --hash=sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196 \ + --hash=sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9 \ + --hash=sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e \ + --hash=sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5 \ + --hash=sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007 \ + --hash=sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380 \ + --hash=sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8 \ + --hash=sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27 \ + --hash=sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40 \ + --hash=sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e \ + --hash=sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0 \ + --hash=sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263 \ + --hash=sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb \ + --hash=sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94 \ + --hash=sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b \ + --hash=sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6 \ + --hash=sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579 \ + --hash=sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4 \ + --hash=sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59 \ + --hash=sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0 \ + --hash=sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e \ + --hash=sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be \ + --hash=sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd \ + --hash=sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18 \ + --hash=sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22 \ + --hash=sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0 \ + --hash=sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b \ + --hash=sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b \ + --hash=sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af \ + --hash=sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745 + # via matplotlib fqdn==1.5.1 \ --hash=sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f \ --hash=sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014 @@ -1351,25 +1619,36 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -fsspec[http]==2024.9.0 \ - --hash=sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8 \ - --hash=sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b +fsspec[http]==2026.6.0 \ + --hash=sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1 \ + --hash=sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a # via - # feast (setup.py) + # feast (pyproject.toml) # dask # datasets # huggingface-hub + # pyiceberg # ray # torch -geomet==0.2.1.post1 \ - --hash=sha256:91d754f7c298cbfcabd3befdb69c641c27fe75e808b27aa55028605761d17e95 \ - --hash=sha256:a41a1e336b381416d6cbed7f1745c848e91defaa4d4c1bdc1312732e46ffad2b - # via cassandra-driver -google-api-core[grpc]==2.26.0 \ - --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ - --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 - # via - # feast (setup.py) +geomet==1.1.0 \ + --hash=sha256:4372fe4e286a34acc6f2e9308284850bd8c4aa5bc12065e2abbd4995900db12f \ + --hash=sha256:51e92231a0ef6aaa63ac20c443377ba78a303fd2ecd179dc3567de79f3c11605 + # via + # cassandra-driver + # scylla-driver +gitdb==4.0.12 \ + --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ + --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf + # via gitpython +gitpython==3.1.57 \ + --hash=sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf \ + --hash=sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488 + # via mlflow-skinny +google-api-core[grpc]==2.33.0 \ + --hash=sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb \ + --hash=sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc + # via + # feast (pyproject.toml) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -1378,10 +1657,11 @@ google-api-core[grpc]==2.26.0 \ # google-cloud-storage # opencensus # pandas-gbq -google-auth==2.41.1 \ - --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ - --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 +google-auth[pyopenssl]==2.56.2 \ + --hash=sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6 \ + --hash=sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051 # via + # databricks-sdk # google-api-core # google-auth-oauthlib # google-cloud-bigquery @@ -1390,165 +1670,177 @@ google-auth==2.41.1 \ # google-cloud-core # google-cloud-datastore # google-cloud-storage - # kubernetes # pandas-gbq # pydata-google-auth -google-auth-oauthlib==1.2.2 \ - --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ - --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-auth-oauthlib==1.4.0 \ + --hash=sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2 \ + --hash=sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 # via # pandas-gbq # pydata-google-auth -google-cloud-bigquery[pandas]==3.38.0 \ - --hash=sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520 \ - --hash=sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6 +google-cloud-bigquery[pandas]==3.42.2 \ + --hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \ + --hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq -google-cloud-bigquery-storage==2.33.1 \ - --hash=sha256:24952aba0d69acc4d6bfbdc7a09dddbb728496b1780bd224f1056361a1b51044 \ - --hash=sha256:3fd25bef364ac5fb9bbd6560f0dd11b90b1845883df8e0a8c706ad53d00fc23b - # via feast (setup.py) -google-cloud-bigtable==2.33.0 \ - --hash=sha256:8d25c73dd41cea9436d14c9f03b91f94b05ff4afd47ce9a575bf18f8e6985747 \ - --hash=sha256:cd4f7178cde30113900318ec7e2fd793116752c97e0922c185c69b645a52656d - # via feast (setup.py) -google-cloud-core==2.4.3 \ - --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ - --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e +google-cloud-bigquery-storage==2.39.0 \ + --hash=sha256:8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 \ + --hash=sha256:d5afd90ad06cf24d9167316cca70ab5b344e880fc13031d7392aa78ee76b8bb6 + # via feast (pyproject.toml) +google-cloud-bigtable==2.41.0 \ + --hash=sha256:36da0fc730952bfe48ae91dee11015459c9046fa1499d0efb73a61bbb6edbe9b \ + --hash=sha256:d0c424bae6238eb16ffc0922be79da00d20d23e0750ea46742ffd4542c50fd26 + # via feast (pyproject.toml) +google-cloud-core==2.6.0 \ + --hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \ + --hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.21.0 \ - --hash=sha256:eee454dd4a55f5b327f9f344928ff1a09a6f77c23d5e3d908ad31a13cc2f4073 \ - --hash=sha256:f303f27cd1983383f20bd227019cd8a7897419e0ec6b878367c58c66245f9d9b - # via feast (setup.py) +google-cloud-datastore==2.26.0 \ + --hash=sha256:1ac4882f033fcb90bd75d1171b46400ecd5def23768598843d9c041e0c0b4a6a \ + --hash=sha256:1bde5694308468da5c932153a6b0331d5b54e1c21de366b6a75ddb0000fd5f4b + # via feast (pyproject.toml) google-cloud-storage==2.19.0 \ --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via feast (setup.py) -google-crc32c==1.7.1 \ - --hash=sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db \ - --hash=sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337 \ - --hash=sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c \ - --hash=sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242 \ - --hash=sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e \ - --hash=sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472 \ - --hash=sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194 \ - --hash=sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3 \ - --hash=sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582 \ - --hash=sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d \ - --hash=sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6 \ - --hash=sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82 \ - --hash=sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06 \ - --hash=sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349 \ - --hash=sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a \ - --hash=sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d \ - --hash=sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48 \ - --hash=sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb \ - --hash=sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315 \ - --hash=sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589 \ - --hash=sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76 \ - --hash=sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65 \ - --hash=sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6 \ - --hash=sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127 \ - --hash=sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53 \ - --hash=sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603 \ - --hash=sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35 \ - --hash=sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9 \ - --hash=sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638 \ - --hash=sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9 \ - --hash=sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77 \ - --hash=sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14 \ - --hash=sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b \ - --hash=sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb + # via feast (pyproject.toml) +google-crc32c==1.8.0 \ + --hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \ + --hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \ + --hash=sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff \ + --hash=sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 \ + --hash=sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411 \ + --hash=sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a \ + --hash=sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 \ + --hash=sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb \ + --hash=sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa \ + --hash=sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962 \ + --hash=sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215 \ + --hash=sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b \ + --hash=sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27 \ + --hash=sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113 \ + --hash=sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f \ + --hash=sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f \ + --hash=sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d \ + --hash=sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 \ + --hash=sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092 \ + --hash=sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7 \ + --hash=sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2 \ + --hash=sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93 \ + --hash=sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8 \ + --hash=sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21 \ + --hash=sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 \ + --hash=sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2 \ + --hash=sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc \ + --hash=sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454 \ + --hash=sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2 \ + --hash=sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733 \ + --hash=sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697 \ + --hash=sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651 \ + --hash=sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c # via # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 +google-resumable-media==2.10.0 \ + --hash=sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c \ + --hash=sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.70.0 \ - --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ - --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +googleapis-common-protos[grpc]==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # grpc-google-iam-v1 # grpcio-status +graphene==3.4.3 \ + --hash=sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa \ + --hash=sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71 + # via mlflow +graphql-core==3.2.11 \ + --hash=sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0 \ + --hash=sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802 + # via + # graphene + # graphql-relay +graphql-relay==3.2.0 \ + --hash=sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c \ + --hash=sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5 + # via graphene great-expectations==0.18.8 \ --hash=sha256:ab41cfa3de829a4f77bdcd4a23244684cbb67fdacc734d38910164cd02ec95b6 \ --hash=sha256:c1205bede593f679e22e0b3826d6ae1623c439cafd553f9f0bc2b0fd441f6ed9 - # via feast (setup.py) -grpc-google-iam-v1==0.14.2 \ - --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ - --hash=sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20 + # via feast (pyproject.toml) +grpc-google-iam-v1==0.14.4 \ + --hash=sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038 \ + --hash=sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 # via google-cloud-bigtable -grpcio==1.62.3 \ - --hash=sha256:059444f0ed5dba73ab7dd0ee7e8e6b606df4130d2b0a9f010f84da4ab9f6c2d8 \ - --hash=sha256:114f2a865886ff33f85d70670e971fe0e3d252a1209656fefa5470286e3fcc76 \ - --hash=sha256:13571a5b868dcc308a55d36669a2d17d9dcd6ec8335213f6c49cc68da7305abe \ - --hash=sha256:1ac0944e9e3ee3e20825226d1e17985e9f88487055c475986cf0922a7d806d8a \ - --hash=sha256:1de3d04d9a4ec31ebe848ae1fe61e4cbc367fb9495cbf6c54368e60609a998d9 \ - --hash=sha256:216740723fc5971429550c374a0c039723b9d4dcaf7ba05227b7e0a500b06417 \ - --hash=sha256:25cd75dc73c5269932413e517be778640402f18cf9a81147e68645bd8af18ab0 \ - --hash=sha256:325c56ce94d738c31059cf91376f625d3effdff8f85c96660a5fd6395d5a707f \ - --hash=sha256:3737e5ef0aa0fcdfeaf3b4ecc1a6be78b494549b28aec4b7f61b5dc357f7d8be \ - --hash=sha256:377babc817e8b4186aed7ed56e832867c513e4e9b6c3503565c344ffdef440d4 \ - --hash=sha256:3fb7d966a976d762a31346353a19fce4afcffbeda3027dd563bc8cb521fcf799 \ - --hash=sha256:43670a25b752b7ed960fcec3db50ae5886dc0df897269b3f5119cde9b731745f \ - --hash=sha256:4439bbd759636e37b66841117a66444b454937e27f0125205d2d117d7827c643 \ - --hash=sha256:454a6aed4ebd56198d37e1f3be6f1c70838e33dd62d1e2cea12f2bcb08efecc5 \ - --hash=sha256:4c9c1502c76cadbf2e145061b63af077b08d5677afcef91970d6db87b30e2f8b \ - --hash=sha256:4dab8b64c438e19c763a6332b55e5efdbecfb7c55ae59a42c38c81ed27955fa5 \ - --hash=sha256:56757d3e4cf5d4b98a30f2c5456151607261c891fa2298a4554848dcbf83083d \ - --hash=sha256:57823dc7299c4f258ae9c32fd327d29f729d359c34d7612b36e48ed45b3ab8d0 \ - --hash=sha256:582bd03e9c3d1bd1162eb51fa0f1a35633d66e73f4f36702d3b8484a8b45eda7 \ - --hash=sha256:620165df24aae3d5b3e84cb8dd6b98f6ed49aed04126186bbf43061e301d6a21 \ - --hash=sha256:646c14e9f3356d3f34a65b58b0f8d08daa741ba1d4fcd4966b79407543332154 \ - --hash=sha256:668211f3699bbee4deaf1d6e6b8df59328bf63f077bf2dc9b8bfa4a17df4a279 \ - --hash=sha256:6be243f3954b0ca709f56f9cae926c84ac96e1cce19844711e647a1f1db88b99 \ - --hash=sha256:6da20a1ae010a988bc4ed47850f1122de0a88e18cd2f901fcf56007be1fc6c30 \ - --hash=sha256:7349cd7445ac65fbe1b744dcab9cc1ec02dae2256941a2e67895926cbf7422b4 \ - --hash=sha256:74f3fc9b93290e58264844f5bc46df4c58a94c4287a277dbcf75344fc6c37ca4 \ - --hash=sha256:75a4e9ac7ff185cad529f35934c5d711b88aca48b90c70e195f5657da50ce321 \ - --hash=sha256:7b33c1807d4ac564a3027d06f21a2220c116ceacaaef614deb96b3341ee58896 \ - --hash=sha256:807176971c504c598976f5a9ea62363cffbbbb6c7509d9808c2342b020880fa2 \ - --hash=sha256:80a82fdee14dc27e9299248b7aabd5a8739a1cf6b76c78aa2b848158b44a99d5 \ - --hash=sha256:81b7c121c4e52a0749bf0759185b8d5cfa48a786cd7d411cdab08269813e0aab \ - --hash=sha256:8257cc9e55fb0e2149a652d9dc14c023720f9e73c9145776e07c97e0a553922e \ - --hash=sha256:8a5f00b2508937952d23a1767739e95bbbe1120f8a66d10187d5e971d56bb55c \ - --hash=sha256:8ae2e7a390b2cdd2a95d3bf3b3385245eeb48a5e853943cb46139666462c2d1a \ - --hash=sha256:940459d81685549afdfe13a6de102c52ea4cdda093477baa53056884aadf7c48 \ - --hash=sha256:9c4aae4e683776c319169d87e7891b67b75e3f1c0beeb877902ea148b0585164 \ - --hash=sha256:9d5f8e0050a179b3bce9189b522dc91008d44f08c757a7c310e0fd06b4d3d147 \ - --hash=sha256:a1b85d35a7d9638c03321dfe466645b87e23c30df1266f9e04bbb5f44e7579a9 \ - --hash=sha256:a82410d7620c07cb32624e38f2a106980564dfef9dbe78f5b295cda9ef217c03 \ - --hash=sha256:abfe64811177e681edc81d9d9d1bd23edc5f599bd9846650864769264ace30cd \ - --hash=sha256:ac9783d5679c8da612465168c820fd0b916e70ec5496c840bddba0be7f2d124c \ - --hash=sha256:b033d50bd41e506e3b579775f54a30c16c222e0d88847ac8098d2eca2a7454cc \ - --hash=sha256:b097347441b86a8c3ad9579abaf5e5f7f82b1d74a898f47360433b2bca0e4536 \ - --hash=sha256:b708401ede2c4cb8943e8a713988fcfe6cbea105b07cd7fa7c8a9f137c22bddb \ - --hash=sha256:bd900e666bb68fff49703084be14407cd73b8a5752a7590cea98ec22de24fb5d \ - --hash=sha256:c118cfc80e2402a5595be36e9245ffd9b0e146f426cc40bdf60015bf183f8373 \ - --hash=sha256:c175b252d063af388523a397dbe8edbc4319761f5ee892a8a0f5890acc067362 \ - --hash=sha256:c8bb1a7aa82af6c7713cdf9dcb8f4ea1024ac7ce82bb0a0a82a49aea5237da34 \ - --hash=sha256:c95a0b76a44c548e6bd8c5f7dbecf89c77e2e16d3965be817b57769c4a30bea2 \ - --hash=sha256:e202e3f963480ca067a261179b1ac610c0f0272cb4a7942d11b7e2b3fc99c3aa \ - --hash=sha256:e9ffdb7bc9ccd56ec201aec3eab3432e1e820335b5a16ad2b37e094218dcd7a6 \ - --hash=sha256:ea7ca66a58421411c6486fa5015fe7704e2816ff0b4ec4fb779ad5e1cbbdabf3 \ - --hash=sha256:f2ff8ac447765e173842b554b31307b98b3bb1852710903ebb936e7efb7df6e5 \ - --hash=sha256:f5def814c5a4c90c8fe389c526ab881f4a28b7e239b23ed8e02dd02934dfaa1a - # via - # feast (setup.py) +grpcio==1.83.0 \ + --hash=sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df \ + --hash=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 \ + --hash=sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5 \ + --hash=sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df \ + --hash=sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930 \ + --hash=sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf \ + --hash=sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9 \ + --hash=sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33 \ + --hash=sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da \ + --hash=sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03 \ + --hash=sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa \ + --hash=sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223 \ + --hash=sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9 \ + --hash=sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4 \ + --hash=sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf \ + --hash=sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881 \ + --hash=sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af \ + --hash=sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5 \ + --hash=sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0 \ + --hash=sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727 \ + --hash=sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb \ + --hash=sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a \ + --hash=sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735 \ + --hash=sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16 \ + --hash=sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49 \ + --hash=sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1 \ + --hash=sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24 \ + --hash=sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b \ + --hash=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f \ + --hash=sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57 \ + --hash=sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf \ + --hash=sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f \ + --hash=sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c \ + --hash=sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969 \ + --hash=sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd \ + --hash=sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c \ + --hash=sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b \ + --hash=sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61 \ + --hash=sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404 \ + --hash=sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617 \ + --hash=sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40 \ + --hash=sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45 \ + --hash=sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc \ + --hash=sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c \ + --hash=sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745 \ + --hash=sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45 \ + --hash=sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c \ + --hash=sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0 \ + --hash=sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d \ + --hash=sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9 \ + --hash=sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8 + # via + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-datastore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking @@ -1556,283 +1848,330 @@ grpcio==1.62.3 \ # grpcio-status # grpcio-testing # grpcio-tools - # ikvpy + # milvus-lite # pymilvus # qdrant-client # ray -grpcio-health-checking==1.62.3 \ - --hash=sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93 \ - --hash=sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d - # via feast (setup.py) -grpcio-reflection==1.62.3 \ - --hash=sha256:a48ef37df81a3bada78261fc92ef382f061112f989d1312398b945cc69838b9c \ - --hash=sha256:cb84682933c400bddf94dd94f928d1c6570f500b6dd255973d4bfb495b82585f - # via feast (setup.py) -grpcio-status==1.62.3 \ - --hash=sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485 \ - --hash=sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8 - # via - # google-api-core - # ikvpy -grpcio-testing==1.62.3 \ - --hash=sha256:06a4d7eb30d22f91368aa7f48bfc33563da13b9d951314455ca8c9c987fb75bb \ - --hash=sha256:f63577f28aaa95ea525124a0fd63c3429d71f769f4179b13f5e6cbc54979bfab - # via feast (setup.py) -grpcio-tools==1.62.3 \ - --hash=sha256:0a52cc9444df978438b8d2332c0ca99000521895229934a59f94f37ed896b133 \ - --hash=sha256:0a8c0c4724ae9c2181b7dbc9b186df46e4f62cb18dc184e46d06c0ebeccf569e \ - --hash=sha256:0cb3a3436ac119cbd37a7d3331d9bdf85dad21a6ac233a3411dff716dcbf401e \ - --hash=sha256:11c625eebefd1fd40a228fc8bae385e448c7e32a6ae134e43cf13bbc23f902b7 \ - --hash=sha256:11f363570dea661dde99e04a51bd108a5807b5df32a6f8bdf4860e34e94a4dbf \ - --hash=sha256:141d028bf5762d4a97f981c501da873589df3f7e02f4c1260e1921e565b376fa \ - --hash=sha256:1c989246c2aebc13253f08be32538a4039a64e12d9c18f6d662d7aee641dc8b5 \ - --hash=sha256:1da38070738da53556a4b35ab67c1b9884a5dd48fa2f243db35dc14079ea3d0c \ - --hash=sha256:27cd9ef5c5d68d5ed104b6dcb96fe9c66b82050e546c9e255716903c3d8f0373 \ - --hash=sha256:2e02d3b96f2d0e4bab9ceaa30f37d4f75571e40c6272e95364bff3125a64d184 \ - --hash=sha256:2f968b049c2849540751ec2100ab05e8086c24bead769ca734fdab58698408c1 \ - --hash=sha256:350a80485e302daaa95d335a931f97b693e170e02d43767ab06552c708808950 \ - --hash=sha256:3eae6ea76d62fcac091e1f15c2dcedf1dc3f114f8df1a972a8a0745e89f4cf61 \ - --hash=sha256:47a5c093ab256dec5714a7a345f8cc89315cb57c298b276fa244f37a0ba507f0 \ - --hash=sha256:5782883a27d3fae8c425b29a9d3dcf5f47d992848a1b76970da3b5a28d424b26 \ - --hash=sha256:6a56d344b0bab30bf342a67e33d386b0b3c4e65868ffe93c341c51e1a8853ca5 \ - --hash=sha256:6c3064610826f50bd69410c63101954676edc703e03f9e8f978a135f1aaf97c1 \ - --hash=sha256:703f46e0012af83a36082b5f30341113474ed0d91e36640da713355cd0ea5d23 \ - --hash=sha256:710fecf6a171dcbfa263a0a3e7070e0df65ba73158d4c539cec50978f11dad5d \ - --hash=sha256:7c7136015c3d62c3eef493efabaf9e3380e3e66d24ee8e94c01cb71377f57833 \ - --hash=sha256:7cc83023acd8bc72cf74c2edbe85b52098501d5b74d8377bfa06f3e929803492 \ - --hash=sha256:7f2483ea232bd72d98a6dc6d7aefd97e5bc80b15cd909b9e356d6f3e326b6e43 \ - --hash=sha256:7ff7d58a45b75df67d25f8f144936a3e44aabd91afec833ee06826bd02b7fbe7 \ - --hash=sha256:8ad0473af5544f89fc5a1ece8676dd03bdf160fb3230f967e05d0f4bf89620e3 \ - --hash=sha256:8c5d22b252dcef11dd1e0fbbe5bbfb9b4ae048e8880d33338215e8ccbdb03edc \ - --hash=sha256:8e62cc7164b0b7c5128e637e394eb2ef3db0e61fc798e80c301de3b2379203ed \ - --hash=sha256:962c84b4da0f3b14b3cdb10bc3837ebc5f136b67d919aea8d7bb3fd3df39528a \ - --hash=sha256:ace43b26d88a58dcff16c20d23ff72b04d0a415f64d2820f4ff06b1166f50557 \ - --hash=sha256:b47d0dda1bdb0a0ba7a9a6de88e5a1ed61f07fad613964879954961e36d49193 \ - --hash=sha256:b77f9f9cee87cd798f0fe26b7024344d1b03a7cd2d2cba7035f8433b13986325 \ - --hash=sha256:b881fd9505a84457e9f7e99362eeedd86497b659030cf57c6f0070df6d9c2b9b \ - --hash=sha256:bfda6ee8990997a9df95c5606f3096dae65f09af7ca03a1e9ca28f088caca5cf \ - --hash=sha256:c3a1ac9d394f8e229eb28eec2e04b9a6f5433fa19c9d32f1cb6066e3c5114a1d \ - --hash=sha256:c8ad5cce554e2fcaf8842dee5d9462583b601a3a78f8b76a153c38c963f58c10 \ - --hash=sha256:ca246dffeca0498be9b4e1ee169b62e64694b0f92e6d0be2573e65522f39eea9 \ - --hash=sha256:ca4f5eeadbb57cf03317d6a2857823239a63a59cc935f5bd6cf6e8b7af7a7ecc \ - --hash=sha256:d102b9b21c4e1e40af9a2ab3c6d41afba6bd29c0aa50ca013bf85c99cdc44ac5 \ - --hash=sha256:db3bc9fa39afc5e4e2767da4459df82b095ef0cab2f257707be06c44a1c2c3e5 \ - --hash=sha256:dc9ad9950119d8ae27634e68b7663cc8d340ae535a0f80d85a55e56a6973ab1f \ - --hash=sha256:e02d7c1a02e3814c94ba0cfe43d93e872c758bd8fd5c2797f894d0c49b4a1dfc \ - --hash=sha256:e0898d412a434e768a0c7e365acabe13ff1558b767e400936e26b5b6ed1ee51f \ - --hash=sha256:e18e15287c31baf574fcdf8251fb7f997d64e96c6ecf467906e576da0a079af6 \ - --hash=sha256:ec279dcf3518201fc592c65002754f58a6b542798cd7f3ecd4af086422f33f29 \ - --hash=sha256:ec6fbded0c61afe6f84e3c2a43e6d656791d95747d6d28b73eff1af64108c434 \ - --hash=sha256:eec73a005443061f4759b71a056f745e3b000dc0dc125c9f20560232dfbcbd14 \ - --hash=sha256:f3d812daffd0c2d2794756bd45a353f89e55dc8f91eb2fc840c51b9f6be62667 \ - --hash=sha256:f4b1615adf67bd8bb71f3464146a6f9949972d06d21a4f5e87e73f6464d97f57 \ - --hash=sha256:f6831fdec2b853c9daa3358535c55eed3694325889aa714070528cf8f92d7d6d - # via feast (setup.py) -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec - # via - # feast (setup.py) +grpcio-health-checking==1.81.1 \ + --hash=sha256:cbc6a4171825ec64389de2f062d296ba129a5c27eefd0dd55fa837909184bdf9 \ + --hash=sha256:ecc61480e25058a4a04e11e4ab6900ad7439b32e60a8ce4ece7d9f219221c85d + # via feast (pyproject.toml) +grpcio-reflection==1.81.1 \ + --hash=sha256:3d7160000f5fdd0e241f9b1a3d402f15ebcd5f281be105b374a025f38c6434a8 \ + --hash=sha256:d82fc4ac39dd5dcfd63e577075f6a68eac4a765e66a453914bbf57caf35665d0 + # via feast (pyproject.toml) +grpcio-status==1.83.0 \ + --hash=sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae \ + --hash=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 + # via google-api-core +grpcio-testing==1.81.1 \ + --hash=sha256:69792db25781cefd2479d12285ca6bebe589e2aa1d64b4b8d18cf35f58c6b810 \ + --hash=sha256:f381121d7203f4380b1c01e6c8f15aa220bafdc372e8ed82eddf79a16149abcc + # via feast (pyproject.toml) +grpcio-tools==1.81.1 \ + --hash=sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a \ + --hash=sha256:136e90906af0df51ad929713244ba812d0dbb1844b4f467d5d86bdb054698f90 \ + --hash=sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4 \ + --hash=sha256:1d602b410b2b2addc434cace9ce4fe2035974a3078228f98ffa049a5c90acc2f \ + --hash=sha256:204de03b539a4b08772c6553b92bcc112cbc965e0ac22f909f6d133b8ac33a8c \ + --hash=sha256:21bb3ba90e6d8df1ff663d4ee39a4e5b25a64e8ed4902476ca9ded0954d3917a \ + --hash=sha256:2baa7e735f35b2a648144c03348a126097b13e101d3c242d5edb6ac91437ccbe \ + --hash=sha256:2c306c307f8f74cddc4056fdbb6f1da55de087a21120efbd02bd915daa5a52fd \ + --hash=sha256:3389e705460efa3f3758141ba5520e6743b131c9576197c944fb9cbe49048126 \ + --hash=sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5 \ + --hash=sha256:3c8611d6e4e859ac5373422ef27c4b7540cf98c9991c9abc6722613ef72b13aa \ + --hash=sha256:3d604b4fd114b79ebb9f865bf3e04fd3ae93c704e1fad96f7fd03b0865c263b7 \ + --hash=sha256:3dca56016d90a710c4d9861bae793dc089c1430a90c79ce672e948ddb65fa539 \ + --hash=sha256:43baf71dc60fd653062da2e95e95c73b35dd130be8f9fa3d544c3af3f808a290 \ + --hash=sha256:43c528655b226375013036692d8db4cd59060c1f41dd62c77f4d17b69f6ce828 \ + --hash=sha256:53ef76cc3b0493ff734a5e8c39d5b519e1822236fcccdfe7677c5e1efd767761 \ + --hash=sha256:690e6dcaa8b8a7886ce206ba344e2127211597e1a1ddab73df9f3d80c8f6707e \ + --hash=sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c \ + --hash=sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e \ + --hash=sha256:7746e508d4239a02f7e93638be5bc0ebb0120ddb796f7506aaae9d47a4599d97 \ + --hash=sha256:7d168ea26390717d0462c0d0408331dc98a60fc7f7e6118afac9b73f5a66d87c \ + --hash=sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3 \ + --hash=sha256:801d9d8ab5cddf8f8e064225292f0713427011252a07828a6b54e2ed64d534de \ + --hash=sha256:8161f398f957a376cae7385ea7c8684f439d460ef702b528912da3bcb31fc515 \ + --hash=sha256:82740248eb6f3b6a38988cb5e64adb7303af9ea5cb4197c8ed08c1fabc767440 \ + --hash=sha256:87b25ca0e27373a4a32a629a4ba976f5764b9887dd50d6fe017d38009a0363e8 \ + --hash=sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593 \ + --hash=sha256:8a17d8ceeb6a855fadf39f5171c80a382d97c4db98d5943eca553497fdebf84b \ + --hash=sha256:9b6ba8a72cfda576508701a7c0bbeebe6f6f9843320d4f12e74efd19ddccd965 \ + --hash=sha256:9d383724bcd67244b6def9e9164c640ee9380c0b7534ee7545a6fb0022a59afe \ + --hash=sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1 \ + --hash=sha256:a28d231455ab6e3558299f7d831a73c8be8ee6b7ec614ecf39eb50c0ed15767f \ + --hash=sha256:a882382507bb5ec6d7edc9648053dfd3bc8f9285cde56a6fa9b9a83b4bd07f1c \ + --hash=sha256:a987c85dcbe1b32066d7acd46266d1a428aecbd629331bf5b853e74c835bf876 \ + --hash=sha256:a9c6fcc68c9d5a208967bfe4fd3224d3c3be9a950c3e827e8f4b17e15c2dc555 \ + --hash=sha256:ac47a9ea1224df8b653072614e6f0207e9fbfe63fdabaa5918a60ca5fc931b88 \ + --hash=sha256:ad7a997c07bd345e84842e60561e7e2cc090ce6c4e1d2f0407e31b85b40fc49a \ + --hash=sha256:b62d254c214faa3773eac709376ae25cf7abff1a76ba5fc4dbcd7b14fc4e4ae6 \ + --hash=sha256:b6bd163ece4535726e5292b845ed80ae9b2cae73ba091c7d6c66033c430e3857 \ + --hash=sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a \ + --hash=sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e \ + --hash=sha256:bd0b68dc76b10b3384b9b6e9f59202b83dcaafd8098eb644759a69316686acf8 \ + --hash=sha256:bd6c3bf3ea6a61eb58c54368d72ada591f2a270f3a31a32e8536e773337e76d9 \ + --hash=sha256:bdbdc927be2e0ea13c32564a72ee31d712a716fb6f8c0d53d37a77d8277c272c \ + --hash=sha256:cb08172b7b629e75cb33866928d319a3196540a725eaab628ba721007140f1af \ + --hash=sha256:cc410b621dd85193766c12dca2e238696199a27a65d2b31b6f0a4c6c0043ff26 \ + --hash=sha256:eac4bb645ceff0c147cc720a40ae68f97427eaafb4968e866dd8fcc20d3d4831 \ + --hash=sha256:f3eb15849979ca7bb864ce81a74d68b0f225a7f111ed3fe212bfc08cf9812b10 \ + --hash=sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec \ + --hash=sha256:f8cb64f87c45ccca8234fa47e6b21f09e43801ff11b556deecb461b3b3e9f292 \ + --hash=sha256:fc3d2a41a7a4467fa03b391394fffada9291fe8feebc8679b526f6bc36942b25 + # via feast (pyproject.toml) +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf + # via + # feast (pyproject.toml) + # mlflow # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore + # httpcore2 # uvicorn -h2==4.3.0 \ - --hash=sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1 \ - --hash=sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd +h2==4.4.0 \ + --hash=sha256:46b551bdcdc7e83cf5c04d0bf93badb8a939bd2287d9fee1abb23a445b9e0580 \ + --hash=sha256:6acffe1aeab79098d7eb0f8385c1add11f2c7a94815f6fa2b7060eeddee3d87c # via httpx happybase==1.3.0 \ --hash=sha256:43b6275d2865fc1364680a03f085491cd85d8b84db3c5aa94d25186685dfd87e \ --hash=sha256:f2644cf1ef9d662fbe6f709fcfd66bf13e949f3efd4745a3230cf5f904fb7839 - # via feast (setup.py) -hazelcast-python-client==5.5.0 \ - --hash=sha256:c797c23c219971d225f8590f6359692c14059c26baa15c2762c95667ae38b90a \ - --hash=sha256:dc8d7c1f494e02994238759ad45a9d9d54a569b8e12f198a0efa6e192774b16d - # via feast (setup.py) -hf-xet==1.1.10 \ - --hash=sha256:0a0005fd08f002180f7a12d4e13b22be277725bc23ed0529f8add5c7a6309c06 \ - --hash=sha256:408aef343800a2102374a883f283ff29068055c111f003ff840733d3b715bb97 \ - --hash=sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045 \ - --hash=sha256:686083aca1a6669bc85c21c0563551cbcdaa5cf7876a91f3d074a030b577231d \ - --hash=sha256:6b6bceb6361c80c1cc42b5a7b4e3efd90e64630bcf11224dcac50ef30a47e435 \ - --hash=sha256:71081925383b66b24eedff3013f8e6bbd41215c3338be4b94ba75fd75b21513b \ - --hash=sha256:eae7c1fc8a664e54753ffc235e11427ca61f4b0477d757cc4eb9ae374b69f09c \ - --hash=sha256:f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f + # via feast (pyproject.toml) +hazelcast-python-client==5.7.0 \ + --hash=sha256:6a218295af38730bd1781a6b2fefbf24851f1d6abb472f0bfe927a281b3f83b7 \ + --hash=sha256:79e7c6e77453e53af29bb2454504e1b8d3a422527813116ee44379c18ee9849f + # via feast (pyproject.toml) +hf-xet==1.5.2 \ + --hash=sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed \ + --hash=sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d \ + --hash=sha256:4a5ecb9cda8512ba2aa8ee5d37c87a1422992165892d653098c7b90247481c3b \ + --hash=sha256:580e59e29bf37aece1f2b68537de1e3fb04f43a23d910dcf6f128280b5bfbba4 \ + --hash=sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f \ + --hash=sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47 \ + --hash=sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025 \ + --hash=sha256:8764488197c1d7b1378c8438c18d2eea902e150dbca0b0f0d2d32603fb9b5576 \ + --hash=sha256:8d7446f72abbf7e01ca5ff131786bc2e74a56393462c17a6bf1e303fbab81db4 \ + --hash=sha256:bee28c619622d36968056532fd49cf2b35ca75099b1d616c31a618a893491380 \ + --hash=sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097 \ + --hash=sha256:d6f9c58549407b84b9a5383afd68db0acc42345326a3159990b36a5ca8a20e4e \ + --hash=sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c \ + --hash=sha256:e396ab0faf6298199ad7a95305c3ca8498cb825978a6485be6d00587ee4ec577 \ + --hash=sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65 \ + --hash=sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799 \ + --hash=sha256:fd3add255549e8ef58fa35b2e42dc016961c050600444e7d77d030ba6b57120e # via huggingface-hub -hiredis==2.4.0 \ - --hash=sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027 \ - --hash=sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577 \ - --hash=sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603 \ - --hash=sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869 \ - --hash=sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228 \ - --hash=sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79 \ - --hash=sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb \ - --hash=sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33 \ - --hash=sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77 \ - --hash=sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9 \ - --hash=sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd \ - --hash=sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a \ - --hash=sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8 \ - --hash=sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9 \ - --hash=sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901 \ - --hash=sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7 \ - --hash=sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0 \ - --hash=sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d \ - --hash=sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213 \ - --hash=sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8 \ - --hash=sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389 \ - --hash=sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330 \ - --hash=sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1 \ - --hash=sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2 \ - --hash=sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288 \ - --hash=sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba \ - --hash=sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded \ - --hash=sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca \ - --hash=sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109 \ - --hash=sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8 \ - --hash=sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea \ - --hash=sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6 \ - --hash=sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8 \ - --hash=sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0 \ - --hash=sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5 \ - --hash=sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5 \ - --hash=sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182 \ - --hash=sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73 \ - --hash=sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af \ - --hash=sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac \ - --hash=sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204 \ - --hash=sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277 \ - --hash=sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212 \ - --hash=sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41 \ - --hash=sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e \ - --hash=sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c \ - --hash=sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2 \ - --hash=sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4 \ - --hash=sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348 \ - --hash=sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b \ - --hash=sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e \ - --hash=sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995 \ - --hash=sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371 \ - --hash=sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613 \ - --hash=sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b \ - --hash=sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d \ - --hash=sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708 \ - --hash=sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08 \ - --hash=sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca \ - --hash=sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12 \ - --hash=sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8 \ - --hash=sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829 \ - --hash=sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38 \ - --hash=sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825 \ - --hash=sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67 \ - --hash=sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056 \ - --hash=sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376 \ - --hash=sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214 \ - --hash=sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982 \ - --hash=sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034 \ - --hash=sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1 \ - --hash=sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f \ - --hash=sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9 \ - --hash=sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5 \ - --hash=sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390 \ - --hash=sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0 \ - --hash=sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d \ - --hash=sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57 \ - --hash=sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe \ - --hash=sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2 \ - --hash=sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415 \ - --hash=sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c \ - --hash=sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b \ - --hash=sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853 \ - --hash=sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36 \ - --hash=sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae \ - --hash=sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd \ - --hash=sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187 \ - --hash=sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c \ - --hash=sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542 \ - --hash=sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d \ - --hash=sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79 \ - --hash=sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7 \ - --hash=sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def - # via feast (setup.py) -hpack==4.1.0 \ - --hash=sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496 \ - --hash=sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca +hiredis==3.4.0 \ + --hash=sha256:02298551060cbfe17b1acb02e9d066eadd51e37357369736f8fc1f5533875255 \ + --hash=sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc \ + --hash=sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865 \ + --hash=sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c \ + --hash=sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec \ + --hash=sha256:06c163607f163a300cba9ef02fd8a234dd644a208bfafe972614f72e5287b9ba \ + --hash=sha256:0731dd9f2bf4d3417947a3cbbca4dd4d9fa6ad7cecfd4e16439c17a4562d6304 \ + --hash=sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419 \ + --hash=sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320 \ + --hash=sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b \ + --hash=sha256:0bcb630add6bc9ea136fce691ddff0c46aa91cb860df4ca789fe44127eb7e90d \ + --hash=sha256:12ea5facb5b08fa23e4c101ec2151f3a3de8ecec412fec58dbde0a6eebca02c7 \ + --hash=sha256:12eca9aea1450d1a85dc15574a985c227e52abbc2b6466f48ad2aa3b82124701 \ + --hash=sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f \ + --hash=sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017 \ + --hash=sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc \ + --hash=sha256:16a3ea8193c5f4ee6e8daafe0126207cef38eec5a923c97047e6985d9bb1b61f \ + --hash=sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4 \ + --hash=sha256:1bfb9ccfb13be63883e5f2e5ff7f6fc87bf256f8243af594257dfbed9dbc3cf0 \ + --hash=sha256:1c9e141b420dae63ea21303a7e8672282326cae67472768e9a3f6e7f0990bd28 \ + --hash=sha256:1d8c21fb85f2a3de9328cf5bace84b63da00028e771b92335ff4fdc00121dd5a \ + --hash=sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5 \ + --hash=sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016 \ + --hash=sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240 \ + --hash=sha256:282c4310af72afbe18b07d416459f4febeaeb805a067a7df790136e0e550fcb2 \ + --hash=sha256:2c432cce38190c3c13b6c28d6840ee85db50765ad510134007f1cfb3844dce73 \ + --hash=sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591 \ + --hash=sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea \ + --hash=sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980 \ + --hash=sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b \ + --hash=sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb \ + --hash=sha256:386b556f48fb0f9f09696b9d37f1cae554123fbd9f091d0ca23087f2aca02887 \ + --hash=sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b \ + --hash=sha256:4190bd07dd7879a8a7ddbb2a4f74d402721f3898276e35beb98851b85b5f539c \ + --hash=sha256:423570ac4d2665c0d55d8707b7859a331e9001da5e437b7b7a23e0acbce770c9 \ + --hash=sha256:43004b0b48abc628dda1ac3ac4871e1326c126f8cd9f11164d61934d827d7a3b \ + --hash=sha256:4404c557fd49bcfe24dff41f1209e4221c76d1607df2fb2dfd39474b5b086dcb \ + --hash=sha256:44660a91e0fbc803c29b337c1a9194c8d7b4cd3a3868d28f747cbec2df165483 \ + --hash=sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582 \ + --hash=sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2 \ + --hash=sha256:45c6c296056641b5df37cedafe7d1553f33bc247e2f81603a4d038b39261879b \ + --hash=sha256:46e041fc7a83eaa989ef5fe09526bc2353a6ba39b4ce5191684eabefc02e17b0 \ + --hash=sha256:4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e \ + --hash=sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d \ + --hash=sha256:4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d \ + --hash=sha256:4e5530d7a6c71c1a9c3b850b08bbc8178ffc9556e3aef7f9c808fe9ff91a6f11 \ + --hash=sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763 \ + --hash=sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584 \ + --hash=sha256:5359caad5b57da0bce11d2880f22617ba3710f0866121a924745447848448034 \ + --hash=sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79 \ + --hash=sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9 \ + --hash=sha256:64cf54c1dad35830bf1d86b343ad3c55aed443af51c5ccead8afd1b547aca196 \ + --hash=sha256:6774f1fe2723001ca0cd42bf5d8b1235301226273915c581c5c1260d4d114c43 \ + --hash=sha256:68c81c04e0d8778c01163dd2bc6f0e0236fc7f650c291526f10d4faccd99e5a0 \ + --hash=sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb \ + --hash=sha256:69d0326f20354ce278cbb86f5ae47cb390e22bb94a66877031038af907c42fa5 \ + --hash=sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22 \ + --hash=sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b \ + --hash=sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9 \ + --hash=sha256:7b159315df71a009375227384aa4219f98c2342ccd8eb33e3f4b58654f426376 \ + --hash=sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203 \ + --hash=sha256:7f7fc1535f6e1a190089eae46dee25f0c6b72bb221d377be07092803b8208733 \ + --hash=sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f \ + --hash=sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13 \ + --hash=sha256:85a9abf2d16b4cb112f6cc6c32236d763b34d21c69b00c2f81a4ff3016fcc1d6 \ + --hash=sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c \ + --hash=sha256:899d5cea72c9a6c2d9b6e7b223de62c9e9f541f48267cc7135764070548735ce \ + --hash=sha256:89e95f50ec4d29645ba2da9010e8717861585dc9f3df354a367f43807a6db3c7 \ + --hash=sha256:8aaaab18314fd25453b5cf59c8cdca4110e419455bcb4c0737d19d4151513e75 \ + --hash=sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1 \ + --hash=sha256:8f2c8569460aa456a294d5e9a3579a382b825dc2e283e3cc398323dcfb6a3dee \ + --hash=sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151 \ + --hash=sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a \ + --hash=sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d \ + --hash=sha256:98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c \ + --hash=sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f \ + --hash=sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d \ + --hash=sha256:a315009b441a0105a373a9a780ebb1c6f7d9ead88ac6ea5f2a15791353c6f590 \ + --hash=sha256:a3796094f616f72976ff51e4dc1a016e753c0f9af5393b2df96920b6bae1e19b \ + --hash=sha256:a427976ba339da624367613d7eba52a1516e6d5c7f8988dc8fb888fbcf52d8b6 \ + --hash=sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb \ + --hash=sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530 \ + --hash=sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4 \ + --hash=sha256:b0c29ac4f50fc61a904acfe76e4ce6bc37e16ab5a98089cb411476d466e1bb36 \ + --hash=sha256:b32afb576705fc5a02440d416ef476ddbcc827a20ff6c4a9d557882bc6b75135 \ + --hash=sha256:b6228afacadb19a858289319d72797023e2cb048f2f930b68b7fe57eaed5fb6b \ + --hash=sha256:b98f44bf873731d9ce99e6b3eeb6196bf7b27693ad9f6de0fba986b2b0845127 \ + --hash=sha256:bb44efa4fa3e3ed7779ad0ade3c08ed5d75ca7a6336893e9a4f2722093b4168a \ + --hash=sha256:bb8b2dd9c00d80ad2870ffbb51ce9af99fb2194c082d7b09f4125d90e21426f6 \ + --hash=sha256:bca175f02a2b0150ffe7f5dc8bf49c798f34d2c7024d17ace0ec97a7583560e3 \ + --hash=sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59 \ + --hash=sha256:c2245c46b4ced5f689469e6dcdfc8a0895bf873840a6600f5ea759cdf1b26a8b \ + --hash=sha256:c8c6337e7ad187e8039d06e4b33ee5148dcd73928819d94087e649eb37316a09 \ + --hash=sha256:ccc5c660e31d788ca534a20f2ccb7a80b946b960e18ed4e1db950fcac122b405 \ + --hash=sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643 \ + --hash=sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c \ + --hash=sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4 \ + --hash=sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d \ + --hash=sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352 \ + --hash=sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81 \ + --hash=sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5 \ + --hash=sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092 \ + --hash=sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4 \ + --hash=sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad \ + --hash=sha256:e29267ecdd08758926f1a9221af2671d90f475480c40aff409921b1f362f1bd5 \ + --hash=sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d \ + --hash=sha256:ed1dba2695f6de009c67d63b39ff978cb43b8a79362f697acedffb7743e50d21 \ + --hash=sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1 \ + --hash=sha256:f3c67f39b112dc35f68d5b59ee111db6121f037d1a60cf3840ecffbb2ec5686b \ + --hash=sha256:f7c7596fbb2b5202e943180353958e89014e763c7f25877a92f70bbde6cd7f19 \ + --hash=sha256:ff28eca77a57751488df90b0f385b472fd78e140a4d45097224648a5737acdf5 \ + --hash=sha256:ff6217da86a63a6a5ea954d9b9ed67916d9d94a566b44aa98e68ae34b40ebbbf + # via feast (pyproject.toml) +hpack==4.2.0 \ + --hash=sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0 \ + --hash=sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986 # via h2 httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httpcore2==2.9.1 \ + --hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \ + --hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26 + # via httpx2 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn httpx[http2]==0.27.2 \ --hash=sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0 \ --hash=sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2 # via - # feast (setup.py) + # feast (pyproject.toml) + # datasets # fastapi-mcp # jupyterlab - # mcp + # openlineage-python # python-keycloak # qdrant-client -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d +httpx2==2.9.1 \ + --hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \ + --hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a # via mcp -huggingface-hub==0.35.3 \ - --hash=sha256:0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba \ - --hash=sha256:350932eaa5cc6a4747efae85126ee220e4ef1b54e29d31c3b45c5612ddf0b32a +huey==3.3.0 \ + --hash=sha256:a7b1581aaae5f70540faafcf3306d74fa138ec7bad520f81150bb70b224eaddf \ + --hash=sha256:e0c2a1542e6c3acb894821cde895cf9dbf72e42deb25f55b2d44096aa30f8f24 + # via mlflow +huggingface-hub==0.36.2 \ + --hash=sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a \ + --hash=sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270 # via # accelerate # datasets # docling # docling-ibm-models + # sentence-transformers # timm # tokenizers # transformers @@ -1840,65 +2179,56 @@ hyperframe==6.1.0 \ --hash=sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5 \ --hash=sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08 # via h2 -ibis-framework[duckdb, mssql]==9.5.0 \ - --hash=sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0 \ - --hash=sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84 - # via - # feast (setup.py) - # ibis-substrait -ibis-substrait==4.0.1 \ - --hash=sha256:107ca49383a3cca2fdc88f67ea2f0172620c16fa8f39c9c52305af85dd6180b4 \ - --hash=sha256:614810a173d096fbc49d87a9b419e2162a3c25d8efda1a4d57a389ce56b9041f - # via feast (setup.py) -identify==2.6.15 \ - --hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \ - --hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf +ibis-framework[duckdb, mssql, oracle]==12.0.0 \ + --hash=sha256:0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 \ + --hash=sha256:238624f2c14fdab8382ca2f4f667c3cdb81e29844cd5f8db8a325d0743767c61 + # via feast (pyproject.toml) +identify==2.6.19 \ + --hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \ + --hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842 # via pre-commit -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx + # httpx2 # jsonschema # requests # snowflake-connector-python # yarl -ikvpy==0.0.36 \ - --hash=sha256:b0edf6fb6482877940f6c2b5d59a7fabe30cb554b13b88ca52805f043cfda5b3 \ - --hash=sha256:c0ce7dfb61456c283c9ba2cdeb68b3647f245c3905bca652ca2a1068804939d1 - # via feast (setup.py) -imageio==2.37.0 \ - --hash=sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed \ - --hash=sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996 +imageio==2.37.4 \ + --hash=sha256:1ab2e22c8debf700f24c3ac43e8f95f3b3a8110c83b93411e97b4b0b2cd1c7e6 \ + --hash=sha256:e45cbc5e83502047fb138f7f585f7f105a136a57eea5f4b3cfc6ce1b52720bd3 # via scikit-image -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a +imagesize==2.0.0 \ + --hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \ + --hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3 # via sphinx -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd - # via opentelemetry-api -importlib-resources==6.5.2 \ - --hash=sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c \ - --hash=sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc + # via mlflow-skinny +importlib-resources==7.1.0 \ + --hash=sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708 \ + --hash=sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1 # via happybase -iniconfig==2.1.0 \ - --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ - --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via pytest -invoke==2.2.1 \ - --hash=sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8 \ - --hash=sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707 +invoke==3.0.3 \ + --hash=sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c \ + --hash=sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053 # via paramiko -ipykernel==7.0.0 \ - --hash=sha256:06aef83f27adbce00b23345aa70f749f907dc4ac6f4a41fe7bf5f780dc506225 \ - --hash=sha256:28793cecaa6a669e3be80eb6d24803202388b6a955929b0a4e13404d8c92062b +ipykernel==7.3.0 \ + --hash=sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057 \ + --hash=sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09 # via jupyterlab -ipython==9.6.0 \ - --hash=sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731 \ - --hash=sha256:5f77efafc886d2f023442479b8149e7d86547ad0a979e9da9f045d252f648196 +ipython==9.12.0 \ + --hash=sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4 \ + --hash=sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d # via # great-expectations # ipykernel @@ -1921,51 +2251,55 @@ isoduration==20.11.0 \ --hash=sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9 \ --hash=sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042 # via jsonschema -jedi==0.19.2 \ - --hash=sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0 \ - --hash=sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9 +itsdangerous==2.2.0 \ + --hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \ + --hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173 + # via flask +jedi==0.20.0 \ + --hash=sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67 \ + --hash=sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011 # via ipython jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via - # feast (setup.py) + # feast (pyproject.toml) # altair + # flask # great-expectations # jupyter-server # jupyterlab # jupyterlab-server # moto # nbconvert - # poetry-dynamic-versioning # sphinx # torch -jmespath==1.0.1 \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 # via # aiobotocore # boto3 # botocore -joblib==1.5.2 \ - --hash=sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55 \ - --hash=sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241 +joblib==1.5.3 \ + --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ + --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 # via scikit-learn -json5==0.12.1 \ - --hash=sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990 \ - --hash=sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5 +json5==0.15.0 \ + --hash=sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618 \ + --hash=sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71 # via jupyterlab-server -jsonlines==3.1.0 \ - --hash=sha256:2579cb488d96f815b0eb81629e3e6b0332da0962a18fa3532958f7ba14a5c37f \ - --hash=sha256:632f5e38f93dfcb1ac8c4e09780b92af3a55f38f26e7c47ae85109d420b6ad39 +jsonlines==4.0.0 \ + --hash=sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74 \ + --hash=sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55 # via docling-ibm-models jsonpatch==1.33 \ --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c # via great-expectations -jsonpointer==3.0.0 \ - --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ - --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef +jsonpointer==3.1.1 \ + --hash=sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900 \ + --hash=sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca # via # jsonpatch # jsonschema @@ -1973,11 +2307,10 @@ jsonref==1.1.0 \ --hash=sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552 \ --hash=sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9 # via docling-core -jsonschema[format-nongpl]==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 +jsonschema[format-nongpl]==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce # via - # feast (setup.py) # altair # docling-core # great-expectations @@ -1990,84 +2323,215 @@ jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -jupyter-client==8.6.3 \ - --hash=sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419 \ - --hash=sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f +jupyter-builder==1.1.1 \ + --hash=sha256:1a13977912b08deda77fce2c803940131c27cf77a27ed64b9ffca25aa0ed7e6c \ + --hash=sha256:f9c14bc55c0488a073f62af12d468936fcf9ecb7e9dd802f6f9c33de46ad70db + # via + # jupyterlab + # notebook +jupyter-client==8.9.1 \ + --hash=sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81 \ + --hash=sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa # via # ipykernel # jupyter-server # nbclient -jupyter-core==5.8.1 \ - --hash=sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941 \ - --hash=sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0 +jupyter-core==5.9.1 \ + --hash=sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508 \ + --hash=sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 # via # ipykernel + # jupyter-builder # jupyter-client # jupyter-server # jupyterlab # nbclient # nbconvert # nbformat -jupyter-events==0.12.0 \ - --hash=sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb \ - --hash=sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b +jupyter-events==0.12.1 \ + --hash=sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf \ + --hash=sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3 # via jupyter-server -jupyter-lsp==2.3.0 \ - --hash=sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245 \ - --hash=sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f +jupyter-lsp==2.3.1 \ + --hash=sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81 \ + --hash=sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6 # via jupyterlab -jupyter-server==2.17.0 \ - --hash=sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5 \ - --hash=sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f +jupyter-server==2.20.0 \ + --hash=sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14 \ + --hash=sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc # via # jupyter-lsp # jupyterlab # jupyterlab-server # notebook # notebook-shim -jupyter-server-terminals==0.5.3 \ - --hash=sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa \ - --hash=sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269 +jupyter-server-terminals==0.5.4 \ + --hash=sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14 \ + --hash=sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5 # via jupyter-server -jupyterlab==4.4.9 \ - --hash=sha256:394c902827350c017430a8370b9f40c03c098773084bc53930145c146d3d2cb2 \ - --hash=sha256:ea55aca8269909016d5fde2dc09b97128bc931230183fe7e2920ede5154ad9c2 +jupyterlab==4.6.2 \ + --hash=sha256:5964447036629adfcd3fc0969effc1da6f47d2cbd0a60b2c2eea7c31be0ec6a8 \ + --hash=sha256:e18ce8b34f3de350e93cd5b2c4f3ae884cbe266eb76bf5d6825a4ed34c13bcff # via notebook jupyterlab-pygments==0.3.0 \ --hash=sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d \ --hash=sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780 # via nbconvert -jupyterlab-server==2.27.3 \ - --hash=sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4 \ - --hash=sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4 +jupyterlab-server==2.28.0 \ + --hash=sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c \ + --hash=sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968 # via # jupyterlab # notebook -jupyterlab-widgets==3.0.15 \ - --hash=sha256:2920888a0c2922351a9202817957a68c07d99673504d6cd37345299e971bb08b \ - --hash=sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c +jupyterlab-widgets==3.0.16 \ + --hash=sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0 \ + --hash=sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8 # via ipywidgets -jwcrypto==1.5.6 \ - --hash=sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789 \ - --hash=sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039 +jwcrypto==1.5.8 \ + --hash=sha256:85aeb475f808d56bbc2f2ed1f6f73e6a317c4011a4321505f02f0aed695a3742 \ + --hash=sha256:c3d7114b6f6e65b52f6b7da817eb8cb8423e1da31e1ef13508447c81ecbdcc34 # via python-keycloak -kubernetes==33.1.0 \ - --hash=sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5 \ - --hash=sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993 +kiwisolver==1.5.0 \ + --hash=sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9 \ + --hash=sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679 \ + --hash=sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0 \ + --hash=sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8 \ + --hash=sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276 \ + --hash=sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96 \ + --hash=sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e \ + --hash=sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac \ + --hash=sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f \ + --hash=sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a \ + --hash=sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15 \ + --hash=sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7 \ + --hash=sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368 \ + --hash=sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02 \ + --hash=sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9 \ + --hash=sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681 \ + --hash=sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57 \ + --hash=sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27 \ + --hash=sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4 \ + --hash=sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920 \ + --hash=sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374 \ + --hash=sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3 \ + --hash=sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa \ + --hash=sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23 \ + --hash=sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859 \ + --hash=sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb \ + --hash=sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d \ + --hash=sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc \ + --hash=sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581 \ + --hash=sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c \ + --hash=sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099 \ + --hash=sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05 \ + --hash=sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9 \ + --hash=sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd \ + --hash=sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc \ + --hash=sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796 \ + --hash=sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303 \ + --hash=sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca \ + --hash=sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314 \ + --hash=sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489 \ + --hash=sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57 \ + --hash=sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1 \ + --hash=sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797 \ + --hash=sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021 \ + --hash=sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db \ + --hash=sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22 \ + --hash=sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028 \ + --hash=sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083 \ + --hash=sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65 \ + --hash=sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588 \ + --hash=sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0 \ + --hash=sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a \ + --hash=sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1 \ + --hash=sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c \ + --hash=sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac \ + --hash=sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476 \ + --hash=sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53 \ + --hash=sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3 \ + --hash=sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4 \ + --hash=sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615 \ + --hash=sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb \ + --hash=sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18 \ + --hash=sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b \ + --hash=sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1 \ + --hash=sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2 \ + --hash=sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c \ + --hash=sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac \ + --hash=sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d \ + --hash=sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf \ + --hash=sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2 \ + --hash=sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f \ + --hash=sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f \ + --hash=sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4 \ + --hash=sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9 \ + --hash=sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e \ + --hash=sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737 \ + --hash=sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b \ + --hash=sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed \ + --hash=sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3 \ + --hash=sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7 \ + --hash=sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08 \ + --hash=sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e \ + --hash=sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902 \ + --hash=sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd \ + --hash=sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6 \ + --hash=sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310 \ + --hash=sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537 \ + --hash=sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554 \ + --hash=sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e \ + --hash=sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87 \ + --hash=sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a \ + --hash=sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c \ + --hash=sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79 \ + --hash=sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e \ + --hash=sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16 \ + --hash=sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1 \ + --hash=sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875 \ + --hash=sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd \ + --hash=sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0 \ + --hash=sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9 \ + --hash=sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646 \ + --hash=sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657 \ + --hash=sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4 \ + --hash=sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232 \ + --hash=sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 \ + --hash=sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384 \ + --hash=sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309 \ + --hash=sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede \ + --hash=sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2 \ + --hash=sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203 \ + --hash=sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7 \ + --hash=sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df \ + --hash=sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c \ + --hash=sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167 \ + --hash=sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3 \ + --hash=sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09 \ + --hash=sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398 + # via matplotlib +kube-authkit==0.4.0 \ + --hash=sha256:1df61ac392fca96c8f5ae8c3d6e9918f1e1655d212434b3c3da5f92cc23b660d \ + --hash=sha256:3bf5fc6ddc882498040118c907628ea68789f9a947454c241972008be59601a3 + # via codeflare-sdk +kubernetes==36.0.3 \ + --hash=sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7 \ + --hash=sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f # via - # feast (setup.py) + # feast (pyproject.toml) # codeflare-sdk -lark==1.3.0 \ - --hash=sha256:80661f261fb2584a9828a097a2432efd575af27d20be0fd35d17f0fe37253831 \ - --hash=sha256:9a3839d0ca5e1faf7cfa3460e420e859b66bcbde05b634e73c369c8244c5fa48 + # kube-authkit +lark==1.3.1 \ + --hash=sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905 \ + --hash=sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12 # via rfc3987-syntax -latex2mathml==3.78.1 \ - --hash=sha256:f089b6d75e85b937f99693c93e8c16c0804008672c3dd2a3d25affd36f238100 \ - --hash=sha256:f941db80bf41db33f31df87b304e8b588f8166b813b0257c11c98f7a9d0aac71 +latex2mathml==3.81.0 \ + --hash=sha256:4b959cdc3cac8686bc0e3e5aece8127dfb1b81ca1241bed8e00ef31b82bb4022 \ + --hash=sha256:d317710393fe20579aea39cfe8928fa2ad9b8780896e585326c75e89c1d1d1a4 # via docling-core -lazy-loader==0.4 \ - --hash=sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc \ - --hash=sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1 +lazy-loader==0.5 \ + --hash=sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3 \ + --hash=sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005 # via scikit-image locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ @@ -2210,48 +2674,64 @@ lxml==5.4.0 \ # docling # python-docx # python-pptx -lz4==4.4.4 \ - --hash=sha256:017f8d269a739405a59d68a4d63d23a8df23e3bb2c70aa069b7563af08dfdffb \ - --hash=sha256:070fd0627ec4393011251a094e08ed9fdcc78cb4e7ab28f507638eee4e39abda \ - --hash=sha256:18ae4fe3bafb344dbd09f976d45cbf49c05c34416f2462828f9572c1fa6d5af7 \ - --hash=sha256:1ea7f07329f85a8eda4d8cf937b87f27f0ac392c6400f18bea2c667c8b7f8ecc \ - --hash=sha256:23ae267494fdd80f0d2a131beff890cf857f1b812ee72dbb96c3204aab725553 \ - --hash=sha256:2f4f2965c98ab254feddf6b5072854a6935adab7bc81412ec4fe238f07b85f62 \ - --hash=sha256:30ebbc5b76b4f0018988825a7e9ce153be4f0d4eba34e6c1f2fcded120573e88 \ - --hash=sha256:33e01e18e4561b0381b2c33d58e77ceee850a5067f0ece945064cbaac2176962 \ - --hash=sha256:38730927ad51beb42ab8dbc5555270bfbe86167ba734265f88bbd799fced1004 \ - --hash=sha256:4134b9fd70ac41954c080b772816bb1afe0c8354ee993015a83430031d686a4c \ - --hash=sha256:45e7c954546de4f85d895aa735989d77f87dd649f503ce1c8a71a151b092ed36 \ - --hash=sha256:4ab1537bd3b3bfbafd3c8847e06827129794488304f21945fc2f5b669649d94f \ - --hash=sha256:57fd20c5fc1a49d1bbd170836fccf9a338847e73664f8e313dce6ac91b8c1e02 \ - --hash=sha256:585b42eb37ab16a278c3a917ec23b2beef175aa669f4120142b97aebf90ef775 \ - --hash=sha256:6b56aa9eef830bf6443acd8c4e18b208a8993dc32e0d6ef4263ecfa6afb3f599 \ - --hash=sha256:6ea715bb3357ea1665f77874cf8f55385ff112553db06f3742d3cdcec08633f7 \ - --hash=sha256:714f9298c86f8e7278f1c6af23e509044782fa8220eb0260f8f8f1632f820550 \ - --hash=sha256:80dd27d7d680ea02c261c226acf1d41de2fd77af4fb2da62b278a9376e380de0 \ - --hash=sha256:8ccab8f7f7b82f9fa9fc3b0ba584d353bd5aa818d5821d77d5b9447faad2aaad \ - --hash=sha256:900912e8a7cf74b4a2bea18a3594ae0bf1138f99919c20017167b6e05f760aa4 \ - --hash=sha256:9b7d6dddfd01b49aedb940fdcaf32f41dc58c926ba35f4e31866aeec2f32f4f4 \ - --hash=sha256:a355223a284f42a723c120ce68827de66d5cb872a38732b3d5abbf544fa2fe26 \ - --hash=sha256:a760a175b46325b2bb33b1f2bbfb8aa21b48e1b9653e29c10b6834f9bb44ead4 \ - --hash=sha256:a8474c91de47733856c6686df3c4aca33753741da7e757979369c2c0d32918ba \ - --hash=sha256:b28228197775b7b5096898851d59ef43ccaf151136f81d9c436bc9ba560bc2ba \ - --hash=sha256:bd1add57b6fe1f96bed2d529de085e9378a3ac04b86f116d10506f85b68e97fc \ - --hash=sha256:d0be9f68240231e1e44118a4ebfecd8a5d4184f0bdf5c591c98dd6ade9720afd \ - --hash=sha256:d21d1a2892a2dcc193163dd13eaadabb2c1b803807a5117d8f8588b22eaf9f12 \ - --hash=sha256:d33a5105cd96ebd32c3e78d7ece6123a9d2fb7c18b84dec61f27837d9e0c496c \ - --hash=sha256:dac522788296a9a02a39f620970dea86c38e141e21e51238f1b5e9fa629f8e69 \ - --hash=sha256:dc64d6dfa7a89397529b22638939e70d85eaedc1bd68e30a29c78bfb65d4f715 \ - --hash=sha256:ddfc7194cd206496c445e9e5b0c47f970ce982c725c87bd22de028884125b68f \ - --hash=sha256:e3fc90f766401684740978cd781d73b9685bd81b5dbf7257542ef9de4612e4d2 \ - --hash=sha256:e43e9d48b2daf80e486213128b0763deed35bbb7a59b66d1681e205e1702d735 \ - --hash=sha256:e9cb387c33f014dae4db8cb4ba789c8d2a0a6d045ddff6be13f6c8d9def1d2a6 \ - --hash=sha256:e9ec5d45ea43684f87c316542af061ef5febc6a6b322928f059ce1fb289c298a \ - --hash=sha256:ed6eb9f8deaf25ee4f6fad9625d0955183fdc90c52b6f79a76b7f209af1b6e54 \ - --hash=sha256:f170abb8416c4efca48e76cac2c86c3185efdf841aecbe5c190121c42828ced0 \ - --hash=sha256:f4c21648d81e0dda38b4720dccc9006ae33b0e9e7ffe88af6bf7d4ec124e2fba \ - --hash=sha256:f5024d3ca2383470f7c4ef4d0ed8eabad0b22b23eeefde1c192cf1a38d5e9f78 \ - --hash=sha256:fff9f3a1ed63d45cb6514bfb8293005dc4141341ce3500abdfeb76124c0b9b2e +lz4==4.4.5 \ + --hash=sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc \ + --hash=sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f \ + --hash=sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d \ + --hash=sha256:13254bd78fef50105872989a2dc3418ff09aefc7d0765528adc21646a7288294 \ + --hash=sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397 \ + --hash=sha256:1dd4d91d25937c2441b9fc0f4af01704a2d09f30a38c5798bc1d1b5a15ec9581 \ + --hash=sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50 \ + --hash=sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d \ + --hash=sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e \ + --hash=sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd \ + --hash=sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1 \ + --hash=sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e \ + --hash=sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7 \ + --hash=sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668 \ + --hash=sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64 \ + --hash=sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c \ + --hash=sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0 \ + --hash=sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb \ + --hash=sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43 \ + --hash=sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901 \ + --hash=sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f \ + --hash=sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c \ + --hash=sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a \ + --hash=sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33 \ + --hash=sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989 \ + --hash=sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5 \ + --hash=sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd \ + --hash=sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f \ + --hash=sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b \ + --hash=sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004 \ + --hash=sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be \ + --hash=sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9 \ + --hash=sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba \ + --hash=sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d \ + --hash=sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d \ + --hash=sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832 \ + --hash=sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301 \ + --hash=sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a \ + --hash=sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67 \ + --hash=sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9 \ + --hash=sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f \ + --hash=sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6 \ + --hash=sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb \ + --hash=sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d \ + --hash=sha256:d64141085864918392c3159cdad15b102a620a67975c786777874e1e90ef15ce \ + --hash=sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4 \ + --hash=sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7 \ + --hash=sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22 \ + --hash=sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758 \ + --hash=sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e \ + --hash=sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67 \ + --hash=sha256:e64e61f29cf95afb43549063d8433b46352baf0c8a70aa45e2585618fcf59d86 \ + --hash=sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3 \ + --hash=sha256:f32b9e65d70f3684532358255dc053f143835c5f5991e28a5ac4c93ce94b9ea7 \ + --hash=sha256:f6538aaaedd091d6e5abdaa19b99e6e82697d67518f114721b5248709b639fad \ + --hash=sha256:f9b8bde9909a010c75b3aea58ec3910393b758f3c219beed67063693df854db0 \ + --hash=sha256:ff1b50aeeec64df5603f17984e4b5be6166058dcf8f1e26a3da40d7a0f6ab547 # via # clickhouse-connect # trino @@ -2259,13 +2739,17 @@ makefun==1.16.0 \ --hash=sha256:43baa4c3e7ae2b17de9ceac20b669e9a67ceeadff31581007cca20a07bbe42c4 \ --hash=sha256:e14601831570bff1f6d7e68828bcd30d2f5856f24bad5de0ccb22921ceebc947 # via great-expectations -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +mako==1.3.12 \ + --hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \ + --hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a + # via alembic +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich -marko==2.2.1 \ - --hash=sha256:31e9a18b35c113e506ace5594716fa3df2872f8955908e279bc551f3eb1f0db8 \ - --hash=sha256:e29d7e071a3b0cb2f7cc4c500d55f893dc5a45d85a8298dde6cb4e4dffd794d3 +marko==2.2.3 \ + --hash=sha256:8e1d7a0387281e59dfbc52a381b58c570156970e36b2bbe047f8a3a2f368cacc \ + --hash=sha256:e31ec2875383bc62f9093d16babed5a2c2cde601c00d834ea935a2222120ec19 # via docling markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ @@ -2358,178 +2842,226 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via + # flask # jinja2 + # mako # nbconvert # werkzeug -marshmallow==3.26.1 \ - --hash=sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c \ - --hash=sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6 - # via - # environs - # great-expectations -matplotlib-inline==0.1.7 \ - --hash=sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90 \ - --hash=sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca +marshmallow==3.26.2 \ + --hash=sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73 \ + --hash=sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57 + # via great-expectations +matplotlib==3.11.1 \ + --hash=sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2 \ + --hash=sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464 \ + --hash=sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1 \ + --hash=sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb \ + --hash=sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb \ + --hash=sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a \ + --hash=sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b \ + --hash=sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1 \ + --hash=sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3 \ + --hash=sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2 \ + --hash=sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741 \ + --hash=sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a \ + --hash=sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987 \ + --hash=sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be \ + --hash=sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf \ + --hash=sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f \ + --hash=sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191 \ + --hash=sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18 \ + --hash=sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b \ + --hash=sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30 \ + --hash=sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f \ + --hash=sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e \ + --hash=sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f \ + --hash=sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f \ + --hash=sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f \ + --hash=sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda \ + --hash=sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481 \ + --hash=sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6 \ + --hash=sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099 \ + --hash=sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78 \ + --hash=sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2 \ + --hash=sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319 \ + --hash=sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685 \ + --hash=sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83 \ + --hash=sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3 \ + --hash=sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407 \ + --hash=sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2 \ + --hash=sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d \ + --hash=sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea \ + --hash=sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472 \ + --hash=sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f \ + --hash=sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c \ + --hash=sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae \ + --hash=sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74 \ + --hash=sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99 \ + --hash=sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb + # via mlflow +matplotlib-inline==0.2.2 \ + --hash=sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6 \ + --hash=sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79 # via # ipykernel # ipython -mcp==1.12.4 \ - --hash=sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5 \ - --hash=sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789 +mcp==2.0.0 \ + --hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \ + --hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6 # via fastapi-mcp +mcp-types==2.0.0 \ + --hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \ + --hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379 + # via mcp mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -milvus-lite==2.4.12 \ - --hash=sha256:20087663e7b4385050b7ad08f1f03404426d4c87b1ff91d5a8723eee7fd49e88 \ - --hash=sha256:334037ebbab60243b5d8b43d54ca2f835d81d48c3cda0c6a462605e588deb05d \ - --hash=sha256:a0f3a5ddbfd19f4a6b842b2fd3445693c796cde272b701a1646a94c1ac45d3d7 \ - --hash=sha256:e8d4f7cdd5f731efd6faeee3715d280fd91a5f9b4d89312664d56401f65b1473 - # via - # feast (setup.py) - # pymilvus +milvus-lite==3.1.1 \ + --hash=sha256:0cbf8386a2e99b6464a5eda14cf8e31934b0e287e36c831e293aee614d9efc2e \ + --hash=sha256:0f343c9bf1e291d6c2fa615a52a57090ebbaf96b84bfb71f7eb73cd95cd7fc1c + # via feast (pyproject.toml) minio==7.2.11 \ --hash=sha256:153582ed52ff3b5005ba558e1f25bfe1e9e834f7f0745e594777f28e3e81e1a0 \ --hash=sha256:4db95a21fe1e2022ec975292d8a1f83bd5b18f830d23d42a4518ac7a5281d7c5 - # via feast (setup.py) -mistune==3.1.4 \ - --hash=sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d \ - --hash=sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164 + # via feast (pyproject.toml) +mistune==3.3.4 \ + --hash=sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe \ + --hash=sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a # via # great-expectations # nbconvert -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) +mlflow==3.14.0 \ + --hash=sha256:5a1f818fa003035c724162096ce3ded7bc7bc47a1cae595df6173961983f4718 \ + --hash=sha256:dbf77f7cdb5b5c0ec59b4671c61730b1b914b4dff7a2892e267a547cb5454f56 + # via feast (pyproject.toml) +mlflow-skinny==3.14.0 \ + --hash=sha256:a4880e086365871ef9d78e727a34ea5fb1ce615689579998d48e8c65ee1665a9 \ + --hash=sha256:e50f4506422c7737157ae6643c165122af7898345f2e828fa93c4f10128653cf + # via mlflow +mlflow-tracing==3.14.0 \ + --hash=sha256:854488dd18068f15e2a56f1cc7b8868c611d09ea39068d0a691a3f07e0048cae \ + --hash=sha256:c2f701e001d35964f23fbbdfdda36c818a76c157b912ae83781199fd714be09a + # via mlflow +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via + # feast (pyproject.toml) + # pyiceberg mock==2.0.0 \ --hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \ --hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba - # via feast (setup.py) + # via feast (pyproject.toml) moto==4.2.14 \ --hash=sha256:6d242dbbabe925bb385ddb6958449e5c827670b13b8e153ed63f91dbdb50372c \ --hash=sha256:8f9263ca70b646f091edcc93e97cda864a542e6d16ed04066b1370ed217bd190 - # via feast (setup.py) + # via feast (pyproject.toml) mpire[dill]==2.10.2 \ --hash=sha256:d627707f7a8d02aa4c7f7d59de399dec5290945ddf7fbd36cbb1d6ebb37a51fb \ --hash=sha256:f66a321e93fadff34585a4bfa05e95bd946cf714b442f51c529038eb45773d97 @@ -2538,9 +3070,9 @@ mpmath==1.3.0 \ --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c # via sympy -msal==1.34.0 \ - --hash=sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f \ - --hash=sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1 +msal==1.37.0 \ + --hash=sha256:1b1672a33ee467c1d70b341bb16cafd51bb3c817147a95b93263794b03971bec \ + --hash=sha256:dd17e95a7c71bce75e8108113438ba7c4a086b3bcad4f57a8c09b7af3d753c2d # via # azure-identity # msal-extensions @@ -2548,234 +3080,242 @@ msal-extensions==1.3.1 \ --hash=sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca \ --hash=sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4 # via azure-identity -msgpack==1.1.2 \ - --hash=sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2 \ - --hash=sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014 \ - --hash=sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931 \ - --hash=sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b \ - --hash=sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b \ - --hash=sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999 \ - --hash=sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029 \ - --hash=sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0 \ - --hash=sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9 \ - --hash=sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c \ - --hash=sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8 \ - --hash=sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f \ - --hash=sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a \ - --hash=sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42 \ - --hash=sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e \ - --hash=sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f \ - --hash=sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7 \ - --hash=sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb \ - --hash=sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef \ - --hash=sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf \ - --hash=sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245 \ - --hash=sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794 \ - --hash=sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af \ - --hash=sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff \ - --hash=sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e \ - --hash=sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296 \ - --hash=sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030 \ - --hash=sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833 \ - --hash=sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939 \ - --hash=sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa \ - --hash=sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90 \ - --hash=sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c \ - --hash=sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717 \ - --hash=sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406 \ - --hash=sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a \ - --hash=sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251 \ - --hash=sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2 \ - --hash=sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7 \ - --hash=sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e \ - --hash=sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b \ - --hash=sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844 \ - --hash=sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9 \ - --hash=sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87 \ - --hash=sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b \ - --hash=sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c \ - --hash=sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23 \ - --hash=sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c \ - --hash=sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e \ - --hash=sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620 \ - --hash=sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69 \ - --hash=sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f \ - --hash=sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68 \ - --hash=sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27 \ - --hash=sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46 \ - --hash=sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa \ - --hash=sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00 \ - --hash=sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9 \ - --hash=sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84 \ - --hash=sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e \ - --hash=sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20 \ - --hash=sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e \ - --hash=sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162 +msgpack==1.2.1 \ + --hash=sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833 \ + --hash=sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a \ + --hash=sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647 \ + --hash=sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d \ + --hash=sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064 \ + --hash=sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107 \ + --hash=sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac \ + --hash=sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720 \ + --hash=sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c \ + --hash=sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06 \ + --hash=sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895 \ + --hash=sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde \ + --hash=sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203 \ + --hash=sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc \ + --hash=sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74 \ + --hash=sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22 \ + --hash=sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8 \ + --hash=sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35 \ + --hash=sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb \ + --hash=sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9 \ + --hash=sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8 \ + --hash=sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6 \ + --hash=sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07 \ + --hash=sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056 \ + --hash=sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4 \ + --hash=sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7 \ + --hash=sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1 \ + --hash=sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24 \ + --hash=sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355 \ + --hash=sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0 \ + --hash=sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce \ + --hash=sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f \ + --hash=sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707 \ + --hash=sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66 \ + --hash=sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b \ + --hash=sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e \ + --hash=sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d \ + --hash=sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8 \ + --hash=sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7 \ + --hash=sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73 \ + --hash=sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402 \ + --hash=sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a \ + --hash=sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d \ + --hash=sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c \ + --hash=sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273 \ + --hash=sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b \ + --hash=sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d \ + --hash=sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb \ + --hash=sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4 \ + --hash=sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190 \ + --hash=sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5 \ + --hash=sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a \ + --hash=sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7 \ + --hash=sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1 \ + --hash=sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889 \ + --hash=sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c \ + --hash=sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155 \ + --hash=sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24 \ + --hash=sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6 \ + --hash=sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1 \ + --hash=sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d \ + --hash=sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7 \ + --hash=sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64 \ + --hash=sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2 \ + --hash=sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc \ + --hash=sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c # via ray -multidict==6.7.0 \ - --hash=sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32 \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ - --hash=sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff \ - --hash=sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8 \ - --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ - --hash=sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 \ - --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ - --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6 \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ - --hash=sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912 \ - --hash=sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c \ - --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6 \ - --hash=sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84 \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ - --hash=sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd \ - --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0 \ - --hash=sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e \ - --hash=sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 \ - --hash=sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc \ - --hash=sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7 \ - --hash=sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4 \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111 \ - --hash=sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e \ - --hash=sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84 \ - --hash=sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851 \ - --hash=sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb \ - --hash=sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ - --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5 \ - --hash=sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb \ - --hash=sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349 \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ - --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ - --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34 \ - --hash=sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85 \ - --hash=sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ - --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ - --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ - --hash=sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91 \ - --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40 \ - --hash=sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73 \ - --hash=sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4 \ - --hash=sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687 \ - --hash=sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via # aiobotocore # aiohttp # yarl -multiprocess==0.70.16 \ - --hash=sha256:0dfd078c306e08d46d7a8d06fb120313d87aa43af60d66da43ffff40b44d2f41 \ - --hash=sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1 \ - --hash=sha256:37b55f71c07e2d741374998c043b9520b626a8dddc8b3129222ca4f1a06ef67a \ - --hash=sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee \ - --hash=sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3 \ - --hash=sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435 \ - --hash=sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a \ - --hash=sha256:ba8c31889abf4511c7308a8c52bb4a30b9d590e7f58523302ba00237702ca054 \ - --hash=sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02 \ - --hash=sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec \ - --hash=sha256:e7b9d0f307cd9bd50851afaac0dba2cb6c44449efff697df7c7645f7d3f2be3a \ - --hash=sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e +multiprocess==0.70.17 \ + --hash=sha256:1d52f068357acd1e5bbc670b273ef8f81d57863235d9fbf9314751886e141968 \ + --hash=sha256:20c28ca19079a6c879258103a6d60b94d4ffe2d9da07dda93fb1c8bc6243f522 \ + --hash=sha256:27b8409c02b5dd89d336107c101dfbd1530a2cd4fd425fc27dcb7adb6e0b47bf \ + --hash=sha256:2818af14c52446b9617d1b0755fa70ca2f77c28b25ed97bdaa2c69a22c47b46c \ + --hash=sha256:2884701445d0177aec5bd5f6ee0df296773e4fb65b11903b94c613fb46cfb7d1 \ + --hash=sha256:2b12e081df87ab755190e227341b2c3b17ee6587e9c82fecddcbe6aa812cd7f7 \ + --hash=sha256:2ea0939b0f4760a16a548942c65c76ff5afd81fbf1083c56ae75e21faf92e426 \ + --hash=sha256:349525099a0c9ac5936f0488b5ee73199098dac3ac899d81d326d238f9fd3ccd \ + --hash=sha256:38357ca266b51a2e22841b755d9a91e4bb7b937979a54d411677111716c32744 \ + --hash=sha256:4ae2f11a3416809ebc9a48abfc8b14ecce0652a0944731a1493a3c1ba44ff57a \ + --hash=sha256:7ddb24e5bcdb64e90ec5543a1f05a39463068b6d3b804aa3f2a4e16ec28562d6 \ + --hash=sha256:a0f01cd9d079af7a8296f521dc03859d1a414d14c1e2b6e676ef789333421c95 \ + --hash=sha256:a22a6b1a482b80eab53078418bb0f7025e4f7d93cc8e1f36481477a023884861 \ + --hash=sha256:c2c82d0375baed8d8dd0d8c38eb87c5ae9c471f8e384ad203a36f095ee860f67 \ + --hash=sha256:c3feb874ba574fbccfb335980020c1ac631fbf2a3f7bee4e2042ede62558a021 \ + --hash=sha256:d729f55198a3579f6879766a6d9b72b42d4b320c0dcb7844afb774d75b573c62 # via # datasets # mpire @@ -2808,7 +3348,7 @@ mypy==1.11.2 \ --hash=sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b \ --hash=sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d # via - # feast (setup.py) + # feast (pyproject.toml) # sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ @@ -2817,14 +3357,18 @@ mypy-extensions==1.1.0 \ mypy-protobuf==3.3.0 \ --hash=sha256:15604f6943b16c05db646903261e3b3e775cf7f7990b7c37b03d043a907b650d \ --hash=sha256:24f3b0aecb06656e983f58e07c732a90577b9d7af3e1066fc2b663bbf0370248 - # via feast (setup.py) -nbclient==0.10.2 \ - --hash=sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d \ - --hash=sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193 + # via feast (pyproject.toml) +narwhals==2.24.0 \ + --hash=sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489 \ + --hash=sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d + # via scikit-learn +nbclient==0.11.0 \ + --hash=sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a \ + --hash=sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895 # via nbconvert -nbconvert==7.16.6 \ - --hash=sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b \ - --hash=sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582 +nbconvert==7.17.1 \ + --hash=sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2 \ + --hash=sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8 # via jupyter-server nbformat==5.10.4 \ --hash=sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a \ @@ -2834,13 +3378,13 @@ nbformat==5.10.4 \ # jupyter-server # nbclient # nbconvert -nest-asyncio==1.6.0 \ - --hash=sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe \ - --hash=sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c +nest-asyncio2==1.7.2 \ + --hash=sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8 \ + --hash=sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01 # via ipykernel -networkx==3.5 \ - --hash=sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec \ - --hash=sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037 +networkx==3.6.1 \ + --hash=sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509 \ + --hash=sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762 # via # scikit-image # torch @@ -2865,13 +3409,13 @@ ninja==1.13.0 \ --hash=sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa \ --hash=sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e # via easyocr -nodeenv==1.9.1 \ - --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ - --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 +nodeenv==1.10.0 \ + --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ + --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb # via pre-commit -notebook==7.4.7 \ - --hash=sha256:362b7c95527f7dd3c4c84d410b782872fd9c734fb2524c11dd92758527b6eda6 \ - --hash=sha256:3f0a04027dfcee8a876de48fba13ab77ec8c12f72f848a222ed7f5081b9e342a +notebook==7.6.1 \ + --hash=sha256:0b45fd1010668dd4808c40914d957706dbf044a677d28764dc881b74dfcede82 \ + --hash=sha256:6ea1e4c926f0dc490444ddcc335797a3dacda470a805d01031872fb119988ba2 # via great-expectations notebook-shim==0.2.4 \ --hash=sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef \ @@ -2879,85 +3423,56 @@ notebook-shim==0.2.4 \ # via # jupyterlab # notebook -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf - # via - # feast (setup.py) +numpy==2.5.1 \ + --hash=sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2 \ + --hash=sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d \ + --hash=sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1 \ + --hash=sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b \ + --hash=sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd \ + --hash=sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077 \ + --hash=sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a \ + --hash=sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e \ + --hash=sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277 \ + --hash=sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6 \ + --hash=sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75 \ + --hash=sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7 \ + --hash=sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1 \ + --hash=sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9 \ + --hash=sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21 \ + --hash=sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca \ + --hash=sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0 \ + --hash=sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb \ + --hash=sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d \ + --hash=sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75 \ + --hash=sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74 \ + --hash=sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf \ + --hash=sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0 \ + --hash=sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8 \ + --hash=sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af \ + --hash=sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a \ + --hash=sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4 \ + --hash=sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22 \ + --hash=sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3 \ + --hash=sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1 \ + --hash=sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b \ + --hash=sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1 \ + --hash=sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373 \ + --hash=sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95 \ + --hash=sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6 \ + --hash=sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09 \ + --hash=sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9 \ + --hash=sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438 \ + --hash=sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2 \ + --hash=sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7 \ + --hash=sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace \ + --hash=sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3 \ + --hash=sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2 \ + --hash=sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107 + # via + # feast (pyproject.toml) # accelerate # altair + # contourpy # dask # datasets # db-dtypes @@ -2967,26 +3482,28 @@ numpy==2.3.3 \ # great-expectations # ibis-framework # imageio + # matplotlib + # milvus-lite + # mlflow # opencv-python-headless # pandas # pandas-gbq - # pyarrow # qdrant-client # ray # safetensors # scikit-image # scikit-learn # scipy + # sentence-transformers # shapely + # skops # tifffile # torchvision # transformers oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 - # via - # kubernetes - # requests-oauthlib + # via requests-oauthlib opencensus==0.11.4 \ --hash=sha256:a18487ce68bc19900336e0ff4655c5a116daf10c1b3685ece8d971bddad6a864 \ --hash=sha256:cbef87d8b8773064ab60e5c2a1ced58bbaa38a6d052c41aec224958ce544eff2 @@ -2995,17 +3512,20 @@ opencensus-context==0.1.3 \ --hash=sha256:073bb0590007af276853009fac7e4bab1d523c3f03baf4cb4511ca38967c6039 \ --hash=sha256:a03108c3c10d8c80bb5ddf5c8a1f033161fa61972a9917f9b9b3a18517f0088c # via opencensus -opencv-python-headless==4.11.0.86 \ - --hash=sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b \ - --hash=sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca \ - --hash=sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca \ - --hash=sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb \ - --hash=sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798 \ - --hash=sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81 \ - --hash=sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b - # via - # docling-ibm-models - # easyocr +opencv-python-headless==5.0.0.93 \ + --hash=sha256:030ca5e0837a2963ab36ef896baa9767eb8d2b83353fb28af5a521e40dd8756f \ + --hash=sha256:09a872a157c1376ab922a69bbf22f9a95bcc7b658a9d8b436a60212b02b2eeb4 \ + --hash=sha256:10818d91510e05c04568ae12b5cd120779c70c01bf897b001a6221fe430df80f \ + --hash=sha256:1e55af3abfb462eeeabe5c775f12bdb36216d8a93a3583d69e6bd6e1d6ba7d00 \ + --hash=sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e \ + --hash=sha256:840bd717c21e5c11cadadc022a823315ea417f961213d06b4df010e019eb16f4 \ + --hash=sha256:b82f9831daab90b725c7c1ee1b36cb5732c367096ac76d119e64e14eb70d5f3c \ + --hash=sha256:c6bcd96b185975ea240d22cfdb15a1f6d080cc95264cfbe2621f21bb144d89b9 \ + --hash=sha256:ed709fdf9aa0bd1f2ed8549e71d19449b03a675bb581eb292285f6861953be37 + # via easyocr +openlineage-python==1.52.0 \ + --hash=sha256:e1bd73b3066021eecf69cc5c3571f6fd7e253768ea941f2c76f18969ea0ecb20 + # via feast (pyproject.toml) openpyxl==3.1.5 \ --hash=sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2 \ --hash=sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050 @@ -3014,119 +3534,169 @@ openshift-client==1.0.18 \ --hash=sha256:be3979440cfd96788146a3a1650dabe939d4d516eea0b39f87e66d2ab39495b1 \ --hash=sha256:d8a84080307ccd9556f6c62a3707a3e6507baedee36fa425754f67db9ded528b # via codeflare-sdk -opentelemetry-api==1.37.0 \ - --hash=sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7 \ - --hash=sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47 +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef # via + # mcp + # mlflow-skinny + # mlflow-tracing # opentelemetry-exporter-prometheus # opentelemetry-sdk # opentelemetry-semantic-conventions -opentelemetry-exporter-prometheus==0.58b0 \ - --hash=sha256:02005033a7a108ab9f3000ff3aa49e2d03a8893b5bf3431322ffa246affbf951 \ - --hash=sha256:70f2627b4bb82bac65a1fcf95f6e0dcce9e823dd47379ced854753a7e14dfc93 +opentelemetry-exporter-prometheus==0.65b0 \ + --hash=sha256:2777cbf41c403c119e10f418fce5d645c956b47f673a2ee120285d1d0c6df2d5 \ + --hash=sha256:3b3d24b586d0ad9712c7b52b7d19c8a9dfbb318b9b284121b5f95e90ed019367 # via ray -opentelemetry-proto==1.27.0 \ - --hash=sha256:33c9345d91dafd8a74fc3d7576c5a38f18b7fdf8d02983ac67485386132aedd6 \ - --hash=sha256:b133873de5581a50063e1e4b29cdcf0c5e253a8c2d8dc1229add20a4c3830ace - # via ray -opentelemetry-sdk==1.37.0 \ - --hash=sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c \ - --hash=sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5 +opentelemetry-proto==1.44.0 \ + --hash=sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56 \ + --hash=sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3 # via + # mlflow-skinny + # mlflow-tracing + # ray +opentelemetry-sdk==1.44.0 \ + --hash=sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b \ + --hash=sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad + # via + # mlflow-skinny + # mlflow-tracing # opentelemetry-exporter-prometheus # ray -opentelemetry-semantic-conventions==0.58b0 \ - --hash=sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28 \ - --hash=sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25 +opentelemetry-semantic-conventions==0.65b0 \ + --hash=sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb \ + --hash=sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60 # via opentelemetry-sdk -orjson==3.11.3 \ - --hash=sha256:00f1a271e56d511d1569937c0447d7dce5a99a33ea0dec76673706360a051904 \ - --hash=sha256:0c212cfdd90512fe722fa9bd620de4d46cda691415be86b2e02243242ae81873 \ - --hash=sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d \ - --hash=sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710 \ - --hash=sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f \ - --hash=sha256:124d5ba71fee9c9902c4a7baa9425e663f7f0aecf73d31d54fe3dd357d62c1a7 \ - --hash=sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce \ - --hash=sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a \ - --hash=sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077 \ - --hash=sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b \ - --hash=sha256:212e67806525d2561efbfe9e799633b17eb668b8964abed6b5319b2f1cfbae1f \ - --hash=sha256:215c595c792a87d4407cb72dd5e0f6ee8e694ceeb7f9102b533c5a9bf2a916bb \ - --hash=sha256:22724d80ee5a815a44fc76274bb7ba2e7464f5564aacb6ecddaa9970a83e3225 \ - --hash=sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae \ - --hash=sha256:29cb1f1b008d936803e2da3d7cba726fc47232c45df531b29edf0b232dd737e7 \ - --hash=sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451 \ - --hash=sha256:2d68bf97a771836687107abfca089743885fb664b90138d8761cce61d5625d55 \ - --hash=sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804 \ - --hash=sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca \ - --hash=sha256:3d721fee37380a44f9d9ce6c701b3960239f4fb3d5ceea7f31cbd43882edaa2f \ - --hash=sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f \ - --hash=sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667 \ - --hash=sha256:56afaf1e9b02302ba636151cfc49929c1bb66b98794291afd0e5f20fecaf757c \ - --hash=sha256:58533f9e8266cb0ac298e259ed7b4d42ed3fa0b78ce76860626164de49e0d467 \ - --hash=sha256:5ff835b5d3e67d9207343effb03760c00335f8b5285bfceefd4dc967b0e48f6a \ - --hash=sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27 \ - --hash=sha256:6890ace0809627b0dff19cfad92d69d0fa3f089d3e359a2a532507bb6ba34efb \ - --hash=sha256:6be2f1b5d3dc99a5ce5ce162fc741c22ba9f3443d3dd586e6a1211b7bc87bc7b \ - --hash=sha256:6e8e0c3b85575a32f2ffa59de455f85ce002b8bdc0662d6b9c2ed6d80ab5d204 \ - --hash=sha256:73b92a5b69f31b1a58c0c7e31080aeaec49c6e01b9522e71ff38d08f15aa56de \ - --hash=sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167 \ - --hash=sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1 \ - --hash=sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee \ - --hash=sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f \ - --hash=sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d \ - --hash=sha256:8ab962931015f170b97a3dd7bd933399c1bae8ed8ad0fb2a7151a5654b6941c7 \ - --hash=sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a \ - --hash=sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b \ - --hash=sha256:8e531abd745f51f8035e207e75e049553a86823d189a51809c078412cefb399a \ - --hash=sha256:90368277087d4af32d38bd55f9da2ff466d25325bf6167c8f382d8ee40cb2bbc \ - --hash=sha256:913f629adef31d2d350d41c051ce7e33cf0fd06a5d1cb28d49b1899b23b903aa \ - --hash=sha256:976c6f1975032cc327161c65d4194c549f2589d88b105a5e3499429a54479770 \ - --hash=sha256:97dceed87ed9139884a55db8722428e27bd8452817fbf1869c58b49fecab1120 \ - --hash=sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2 \ - --hash=sha256:9d2ae0cc6aeb669633e0124531f342a17d8e97ea999e42f12a5ad4adaa304c5f \ - --hash=sha256:9d8787bdfbb65a85ea76d0e96a3b1bed7bf0fbcb16d40408dc1172ad784a49d2 \ - --hash=sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc \ - --hash=sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43 \ - --hash=sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872 \ - --hash=sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e \ - --hash=sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be \ - --hash=sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810 \ - --hash=sha256:b67e71e47caa6680d1b6f075a396d04fa6ca8ca09aafb428731da9b3ea32a5a6 \ - --hash=sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2 \ - --hash=sha256:ba21dbb2493e9c653eaffdc38819b004b7b1b246fb77bfc93dc016fe664eac91 \ - --hash=sha256:bb93562146120bb51e6b154962d3dadc678ed0fce96513fa6bc06599bb6f6edc \ - --hash=sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424 \ - --hash=sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e \ - --hash=sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23 \ - --hash=sha256:bfc27516ec46f4520b18ef645864cee168d2a027dbf32c5537cb1f3e3c22dac1 \ - --hash=sha256:c5189a5dab8b0312eadaf9d58d3049b6a52c454256493a557405e77a3d67ab7f \ - --hash=sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d \ - --hash=sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4 \ - --hash=sha256:d2489b241c19582b3f1430cc5d732caefc1aaf378d97e7fb95b9e56bed11725f \ - --hash=sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229 \ - --hash=sha256:d7d012ebddffcce8c85734a6d9e5f08180cd3857c5f5a3ac70185b43775d043d \ - --hash=sha256:d7d18dd34ea2e860553a579df02041845dee0af8985dff7f8661306f95504ddf \ - --hash=sha256:d8b11701bc43be92ea42bd454910437b355dfb63696c06fe953ffb40b5f763b4 \ - --hash=sha256:dd759f75d6b8d1b62012b7f5ef9461d03c804f94d539a5515b454ba3a6588038 \ - --hash=sha256:e0a23b41f8f98b4e61150a03f83e4f0d566880fe53519d445a962929a4d21045 \ - --hash=sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633 \ - --hash=sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064 \ - --hash=sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc \ - --hash=sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049 \ - --hash=sha256:f5aa4682912a450c2db89cbd92d356fef47e115dffba07992555542f344d301b \ - --hash=sha256:f66b001332a017d7945e177e282a40b6997056394e3ed7ddb41fb1813b83e824 \ - --hash=sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c \ - --hash=sha256:f8d902867b699bcd09c176a280b1acdab57f924489033e53d0afe79817da37e6 \ - --hash=sha256:f9d4a5e041ae435b815e568537755773d05dac031fee6a57b4ba70897a44d9d2 \ - --hash=sha256:fafb1a99d740523d964b15c8db4eabbfc86ff29f84898262bf6e3e4c9e97e43e \ - --hash=sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1 \ - --hash=sha256:fd7ff459fb393358d3a155d25b275c60b07a2c83dcd7ea962b1923f5a1134569 \ - --hash=sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c - # via trino -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f +oracledb==4.0.2 \ + --hash=sha256:0101b88c15ae628af556506840ae876e12bdb75506ef342e4990ac1758bad1eb \ + --hash=sha256:04fd67692b3cdea2ac7135e6f87c82ce9035f3caa0891a26710a4c654f1e375e \ + --hash=sha256:087fdf5bc36b03dc3c55f7abc944a163ba8edc9c802bc8baf91698a52041ee13 \ + --hash=sha256:0a0be8111db80bb697ec66a34688a6985589e3f72789afe28e3b162b2c4cdd2b \ + --hash=sha256:0a380ab72853487ea2764c5df772f35026b4219868fc3eba68e193c9aea230ac \ + --hash=sha256:14124b5e8e4ba087f105ee389ee43e407840220a0ba2504de68eed903f51a711 \ + --hash=sha256:1b89c02670bafc9b1fcc0d15175f097bdc1968bc36ee4d66aa63aee09bfbbe30 \ + --hash=sha256:1d8933cfb94d9e947cb88dfc87bb2696120b388b89db14394133990585277fb3 \ + --hash=sha256:33bcb765bf97056764c38481dc1dd959e4b61f3a240baacc645d7f06038ce225 \ + --hash=sha256:36e72ffaaf589041dac72ddf589d694c0a9ac4576662a04628966d4ea089d6ea \ + --hash=sha256:3bde3d507d8f50faa9480e99222fe5fa04dc0db2cb8b57746092022ac69ae93d \ + --hash=sha256:41bb34ba8fe76019a958f6d0beba2baf9169ede0bf94e1a22f05f3ca7676f8d7 \ + --hash=sha256:543a7398ebd778d40a839b76a331afa93f506ac143691a6de59ee31ae2e1c4aa \ + --hash=sha256:567c093138d32f06512572de5b5d52459bea82b4e47098e9d668dae759965ccb \ + --hash=sha256:579f2c568433523a990cde5bea73c980d144754dc54d3ab2cd37efd670dc31d6 \ + --hash=sha256:58e71a3c1e294a99e39b086adf42074d48287548bf8edfa720c9507106186bbb \ + --hash=sha256:5c1f13ba535d9f0fc61e1987720988612cf9424667d491fc344ba681f3336a17 \ + --hash=sha256:5fe6e07ed29f84a6656e0580b4e662109d3bb90fc13d8f98ae3a56a67c75b80e \ + --hash=sha256:60cd8a6045674ab481ff33b0bed448f656e7d9abd7fc88c8f349abda5351aad7 \ + --hash=sha256:60f4a4847024f42e8b3be3c7d2597d682ef24f9342b5ebf1695a1bef2aa5be96 \ + --hash=sha256:6444be4991f33754cd98f624cecef3eb98db3e87f8ac14e9b65bd3591c9dd252 \ + --hash=sha256:6c8f3e34a077d7d2d39aa25c28b1aa53fd4fd6aa2437e4d48d9f18b2b28b95e4 \ + --hash=sha256:6d4effb098af3dbcc1fcf785b304523afee1b9a311a8bd9c62bd3b4669198970 \ + --hash=sha256:756db3529dc0f88251f9cbc349cc9c144af2424aae12c1ce24be2fbd9c1b1610 \ + --hash=sha256:78d4a1cc1482b5927a9962c7766aeeae616a85eedbe49af8b797148ac1c1ac32 \ + --hash=sha256:7d023f02f678937bcb6acce7c976569233d193e00c918d7e7b68580db1f68f45 \ + --hash=sha256:864ad6a838e8d41031f117052ee2fac85453b6ecd483d1da6e8fd87701807e5e \ + --hash=sha256:8a7a45fb1042f7258dfbd0b58044a6743c2e8112d0ef5ef9064e17a52968520f \ + --hash=sha256:93b4d2cd17a93224711fadd28f77e956e5c6575cd923f24bfe0b1a2a581beb79 \ + --hash=sha256:9d11723018b6aeae035f4e1feae4150b7cca1161023c2d68d957d7310fe0dd56 \ + --hash=sha256:a1f46b01a089e0e0dd44c4ac4c5c79903760976346c74a698955c1fb76d68bf5 \ + --hash=sha256:a45a13e33509db5bd3622a05cb2e4a6cb41d56d1bfd2a32caeacd26f4ce8b988 \ + --hash=sha256:a814307ca8a6bf0649d8bf0a650a72f23030d9af052a2d4a481b94b6ab50d5a4 \ + --hash=sha256:af4121112f0b68e8d61ce46a086c6aec8256fa2ca32d2e6467195a6c575c207b \ + --hash=sha256:b14d52c09b3c7a8273b3640ba06675acec5000dab0356ef288587c46c30bbbb4 \ + --hash=sha256:b4f2b51837248d4acfb323a64c48d89b62467b0f73d1211c99db9b80df0cdfbc \ + --hash=sha256:b5d203426f0d191842b4cfd87c223163ccc57f7e7875ec7ed073f163de2229af \ + --hash=sha256:b82d3d83bdc6246e53f8ad8b598d2508e9f5d983e352ce2e8a71a020200ba2d6 \ + --hash=sha256:c193d2636f9c2e8549d24cc53947a8381c7b9488e0dae9cea5cf5bce30dc7313 \ + --hash=sha256:cb70b231254ad82ff0639b8688e0e528100a984df65fcd7624bae88fb930e700 \ + --hash=sha256:cbe3a75b463a334da9b3d76026062bcfb24ec563b7df291f700c9f7eefcbeefe \ + --hash=sha256:d5a82a449d96fb89a93346d88128ad618526ffa290376d4b57fbcee1eedcd19f \ + --hash=sha256:d8a83c120d8177f344bedf7698bc1af5f6fd031851031d3e20ced632286e3e84 \ + --hash=sha256:db0d76199b6dffd721bda48dad4eeea6603942ce2e246f3b9301d85af6bda0ee \ + --hash=sha256:f5ed684ab419603d53981e0f32b51da673e21a42d7dce1bf3f215a9301c7c108 \ + --hash=sha256:f8ac435434193a1d88fcda6bfd9387c5562240d4f2e8de1f9ed20c64d5f0ff90 \ + --hash=sha256:ff8a239de950acb600ab4fbe1b349fb085a75383bc9a163e50cab362293fc936 + # via ibis-framework +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via + # pymilvus + # trino +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # accelerate # build @@ -3134,14 +3704,12 @@ packaging==24.2 \ # datasets # db-dtypes # deprecation - # dunamai # faiss-cpu # google-cloud-bigquery # great-expectations # gunicorn # huggingface-hub # ibis-framework - # ibis-substrait # ipykernel # jupyter-events # jupyter-server @@ -3149,14 +3717,20 @@ packaging==24.2 \ # jupyterlab-server # lazy-loader # marshmallow + # matplotlib + # mlflow-skinny + # mlflow-tracing # nbconvert + # openlineage-python # pandas-gbq # pytest # ray # scikit-image + # skops # snowflake-connector-python # sphinx # transformers + # wheel pandas==2.3.3 \ --hash=sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7 \ --hash=sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593 \ @@ -3214,7 +3788,7 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # altair # dask # datasets @@ -3224,29 +3798,30 @@ pandas==2.3.3 \ # google-cloud-bigquery # great-expectations # ibis-framework + # mlflow # pandas-gbq # pymilvus # ray # snowflake-connector-python -pandas-gbq==0.29.2 \ - --hash=sha256:99f6b642bd8340e96c2acdf7d717108aa0d31c703c2f284817ba7dc3b16110b2 \ - --hash=sha256:a3a28e7de4f8456fc4b8e9b7472b9bc470d2566f89c4cd997599e504f5a0a706 +pandas-gbq==0.30.0 \ + --hash=sha256:8fe811786e4ad2e0d4608e897534207d9fbe768ab3168f766a99f0cb4cd5ed20 \ + --hash=sha256:d9b4454b17aee3c23ef1dfcfd91df6e2b77f1e69e1e4b28467701cd75850664a # via google-cloud-bigquery pandocfilters==1.5.1 \ --hash=sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e \ --hash=sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc # via nbconvert -paramiko==4.0.0 \ - --hash=sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9 \ - --hash=sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f +paramiko==5.0.0 \ + --hash=sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79 \ + --hash=sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c # via openshift-client -parsimonious==0.10.0 \ - --hash=sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c \ - --hash=sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f +parsimonious==0.11.0 \ + --hash=sha256:32e3818abf9f05b3b9f3b6d87d128645e30177e91f614d2277d88a0aea98fae2 \ + --hash=sha256:e080377d98957beec053580d38ae54fcdf7c470fb78670ba4bf8b5f9d5cad2a9 # via singlestoredb -parso==0.8.5 \ - --hash=sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a \ - --hash=sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887 +parso==0.8.7 \ + --hash=sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c \ + --hash=sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1 # via jedi parsy==2.2 \ --hash=sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 \ @@ -3256,9 +3831,9 @@ partd==1.4.2 \ --hash=sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f \ --hash=sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c # via dask -pbr==7.0.1 \ - --hash=sha256:32df5156fbeccb6f8a858d1ebc4e465dcf47d6cc7a4895d5df9aa951c712fc35 \ - --hash=sha256:3ecbcb11d2b8551588ec816b3756b1eb4394186c3b689b17e04850dfc20f7e57 +pbr==7.0.3 \ + --hash=sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29 \ + --hash=sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b # via mock pexpect==4.9.0 \ --hash=sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 \ @@ -3372,24 +3947,25 @@ pillow==11.3.0 \ --hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \ --hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3 # via - # feast (setup.py) + # feast (pyproject.toml) # docling # docling-core # docling-ibm-models # docling-parse # easyocr # imageio + # matplotlib # python-pptx # scikit-image # torchvision -pip==25.2 \ - --hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \ - --hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717 +pip==26.1.2 \ + --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ + --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 # via pip-tools -pip-tools==7.5.1 \ - --hash=sha256:a051a94794ba52df9acad2d7c9b0b09ae001617db458a543f8287fea7b89c2cf \ - --hash=sha256:f5ff803823529edc0e6e40c86b1aa7da7266fb1078093c8beea4e5b77877036a - # via feast (setup.py) +pip-tools==7.6.0 \ + --hash=sha256:4bd99155b6d8de358a214b0865e1a2855a453570c1a83d40f7b564870b8657be \ + --hash=sha256:c1c59f7844df4866fa9542d3f50d1f44be537ac0027cb50b2563d6a992853981 + # via feast (pyproject.toml) platformdirs==3.11.0 \ --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e @@ -3408,14 +3984,6 @@ ply==3.11 \ --hash=sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3 \ --hash=sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce # via thriftpy2 -poetry-core==1.9.1 \ - --hash=sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0 \ - --hash=sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8 - # via feast (setup.py) -poetry-dynamic-versioning==1.9.1 \ - --hash=sha256:65a0c814e6d30d4807734a3c34edf261fd7cc3b340dbd23b6a33ee41f7d0b547 \ - --hash=sha256:d6e7b9df817aa2ca4946cd695c6c89e1379d2e6c640f008a9b6170d081a9da48 - # via feast (setup.py) portalocker==3.2.0 \ --hash=sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac \ --hash=sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968 @@ -3423,167 +3991,170 @@ portalocker==3.2.0 \ pre-commit==3.3.1 \ --hash=sha256:218e9e3f7f7f3271ebc355a15598a4d3893ad9fc7b57fe446db75644543323b9 \ --hash=sha256:733f78c9a056cdd169baa6cd4272d51ecfda95346ef8a89bf93712706021b907 - # via feast (setup.py) -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via - # feast (setup.py) + # via feast (pyproject.toml) +prettytable==3.18.0 \ + --hash=sha256:439217116152244369caf3d9f1caf2f9fe29b03bd79e88d2928c8e718c95d680 \ + --hash=sha256:b3346e0e6f79180833aebaac088ae926340586cf6d7d991b9eb125b65f72313a + # via skops +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via + # feast (pyproject.toml) # jupyter-server # opentelemetry-exporter-prometheus # ray -prompt-toolkit==3.0.52 \ - --hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \ - --hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 +prompt-toolkit==3.0.53 \ + --hash=sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2 \ + --hash=sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6 # via ipython -propcache==0.4.1 \ - --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ - --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ - --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ - --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ - --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ - --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ - --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ - --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ - --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ - --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274 \ + --hash=sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.26.1 \ - --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ - --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +proto-plus==1.28.2 \ + --hash=sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501 \ + --hash=sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52 # via # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==4.25.8 \ - --hash=sha256:077ff8badf2acf8bc474406706ad890466274191a48d0abd3bd6987107c9cde5 \ - --hash=sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59 \ - --hash=sha256:27d498ffd1f21fb81d987a041c32d07857d1d107909f5134ba3350e1ce80a4af \ - --hash=sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0 \ - --hash=sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd \ - --hash=sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0 \ - --hash=sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7 \ - --hash=sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9 \ - --hash=sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f \ - --hash=sha256:d552c53d0415449c8d17ced5c341caba0d89dbf433698e1436c8fa0aae7808a3 \ - --hash=sha256:f4510b93a3bec6eba8fd8f1093e9d7fb0d4a24d1a81377c10c0e5bbfe9e4ed24 - # via - # feast (setup.py) +protobuf==6.33.6 \ + --hash=sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326 \ + --hash=sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901 \ + --hash=sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3 \ + --hash=sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a \ + --hash=sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135 \ + --hash=sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e \ + --hash=sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3 \ + --hash=sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2 \ + --hash=sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593 \ + --hash=sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf + # via + # feast (pyproject.toml) + # databricks-sdk # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -3595,14 +4166,14 @@ protobuf==4.25.8 \ # grpcio-status # grpcio-testing # grpcio-tools - # ikvpy + # mlflow-skinny + # mlflow-tracing # mypy-protobuf # opentelemetry-proto # proto-plus # pymilvus # qdrant-client # ray - # substrait psutil==5.9.0 \ --hash=sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5 \ --hash=sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a \ @@ -3637,83 +4208,73 @@ psutil==5.9.0 \ --hash=sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c \ --hash=sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3 # via - # feast (setup.py) + # feast (pyproject.toml) # accelerate # ipykernel -psycopg[binary, pool]==3.2.5 \ - --hash=sha256:b782130983e5b3de30b4c529623d3687033b4dafa05bb661fc6bf45837ca5879 \ - --hash=sha256:f5f750611c67cb200e85b408882f29265c66d1de7f813add4f8125978bfd70e8 - # via feast (setup.py) -psycopg-binary==3.2.5 \ - --hash=sha256:02fb96091e2fb3ea1470b113fef08953baaedbca1d39a3f72d82cb615177846c \ - --hash=sha256:11e3ed8b94c750d54fc3e4502dd930fb0fd041629845b6a7ce089873ac9756b0 \ - --hash=sha256:1494827c43265820d5dcdc6f8086521bc7dd04b9da8831310978a788cdcd2e62 \ - --hash=sha256:21b839f9bfd77ed074f7f71464a43f453400c57d038a0ba0716329a28e335897 \ - --hash=sha256:23a1dc61abb8f7cc702472ab29554167a9421842f976c201ceb3b722c0299769 \ - --hash=sha256:274e852f9e61252bc8e80a0a43d300ba352d40219e856733054023a3bb960eb4 \ - --hash=sha256:28bd5cb2324567e5e70f07fe1d646398d6b0e210e28b49be0e69593590a59980 \ - --hash=sha256:2b053eae21dd3a6828b516a1171e1274d1af5f7c07d2d9a8f597f2e19c732168 \ - --hash=sha256:2cbb8649cfdacbd14e17f5ab78edc52d33350013888518c73e90c5d17d7bea55 \ - --hash=sha256:2cc86657c05e09c701e97f87132cd58e0d55381dd568520081ac1fe7580a9bbb \ - --hash=sha256:2d10ce4c39eb9631381a0c3792727946a4391e843625a7ee9579ac6bb11495a5 \ - --hash=sha256:2d22a15e45f43d36ed35aed4d5261f8ef6ab7d9b84ee075576ca56ae03b9e0aa \ - --hash=sha256:2dbaf32c18c0d11c4480016b89c9c5cadb7b64c55de7f181d222b189bd13a558 \ - --hash=sha256:32b5673736f04c36ccbf8012800fe5bc01b46dac22c5d59e41b043bebaad9d3d \ - --hash=sha256:375149006e21d58ed8aba640e0295d8e636043064c433af94eb58057f9b96877 \ - --hash=sha256:393ab353196d364858b47317d27804ecc58ab56dbde32217bd67f0f2f2980662 \ - --hash=sha256:39e2cd10bf15442d95c3f48376b25dc33360418ea6c3c05884d8bf42407768c0 \ - --hash=sha256:3d2e57a1d06f3968e49e948ba374f21a7d8dcf44f37d582a4aeddeb7c85ce239 \ - --hash=sha256:3eb71cfc35116e4a8e336b7e785f1fe06ca23b4516a48ea91facd577d1a1fdf6 \ - --hash=sha256:3f893c0ed3d5c7b83b76b1f8f7d3ca5a03e38bcd3cab5d65b5c25a0d1064aca4 \ - --hash=sha256:473f6827cf1faf3924eb77146d1e85126a1b5e48a88053b8d8b78dd29e971d78 \ - --hash=sha256:48f97936145cb7de18b95d85670b2d3e2c257277263272be05815b74fb0ef195 \ - --hash=sha256:48fcb12a0a72fdfe4102bdb1252a7366e8d73a2c89fe6ce5923be890de367c2f \ - --hash=sha256:4914dc60f2fddf0884464985e31d775aa865b665471fa156ec2f56fa72a1a097 \ - --hash=sha256:51a96d9fe51f718912b4a0089784f1f32d800217499fd0f0095b888506aba4c5 \ - --hash=sha256:5244bebaa9734a236b7157fb57c065b6c0f2344281916187bd73f951df1899e0 \ - --hash=sha256:5b81342e139ddccfa417832089cd213bd4beacd7a1462ca4019cafe71682d177 \ - --hash=sha256:5d2253189aa4cca0a425e2ca896d1a29760cd3a2b10ab12194e4e827a566505c \ - --hash=sha256:5fd017d7ed71c58f19b0f614e7bfb8f01ec862bacb67ae584f494d090956102e \ - --hash=sha256:605f70e267222d567fc40de7813ee3fb29f8145a1a20aa6fd3dc62baba9312f1 \ - --hash=sha256:60d0f36a42a822e43c4c7472df8a0c980c0f32e5d74ed871333c423a4e942f11 \ - --hash=sha256:62965045cc0fe3dc5dd55d39779620b225ef75962825c7b1b533033cb91810bd \ - --hash=sha256:65162a9cc3f86d70b1d895dbda506e3c079f80d082eb41c54d3f6d33a00b3965 \ - --hash=sha256:659f2c675d478b1bc01b95a8d3ded74fa939b370e71ffbecd496f617b215eb05 \ - --hash=sha256:6b581da13126b8715c0c0585cd37ce934c9864d44b2a4019f5487c0b943275e6 \ - --hash=sha256:71d82dbc7c6c7f5746468e7992e5483aa45b12250d78d220a2431ab88795825c \ - --hash=sha256:7376b13504396da9678b646f5338462347da01286b2a688a0d8493ec764683a2 \ - --hash=sha256:7623659d44a6aa032be4a066c658ba45009d768c2481526fbef7c609702af116 \ - --hash=sha256:7a94020821723a6a210206ddb458001f3ed27e1e6a0555b9422bebf7ead8ff37 \ - --hash=sha256:7d5f1bfc848a94e0d63fe693adee4f88bd9e5c415ecb4c9c17d2d44eba6795a6 \ - --hash=sha256:7efe6c732fd2d7e22d72dc4f7cf9b644020adacfff61b0a8a151343da8e661c0 \ - --hash=sha256:8a602d9fdb567cca090ca19ac3ebf10219065be2a4f8cf9eb8356cffb5a7ab1d \ - --hash=sha256:8cd9ebf335262e864d740f9dad3f672f61162cc0d4825a5eb5cf50df334a688f \ - --hash=sha256:8e6f2bef5aed021fbdf46323d3cd8847bf960efb56394698644a8ee2306f8892 \ - --hash=sha256:93221d5a759bd39b1face1d7d887d2b9ede3e55aefaff8eacf1b663ccdcd204b \ - --hash=sha256:9639289b72f9339721982e156527c296693236d6192ccc31412ab36fccd1683c \ - --hash=sha256:98efaedf2bf79f4d563ca039a57a025b72847bd80568f54709cc39fc1404772c \ - --hash=sha256:9abe093a303e25ac58774a11241150e2fe2947358d1ca12521ad03c90b131060 \ - --hash=sha256:a4321ee8180982d70458d3e8378e31448901bf0ee40fe0d410a87413578f4098 \ - --hash=sha256:a82211a43372cba9b1555a110e84e679deec2dc9463ae4c736977dad99dca5ed \ - --hash=sha256:a91b0e096fdfeb52d86bb8f5ee25dc22483d6960af9b968e6b381a8ec5bfbf82 \ - --hash=sha256:b5e0acbc991472188c9df40eb56d8a97ad3ad00d4de560b8b74bdc2d94041a8f \ - --hash=sha256:b6b5a4542aca4095ab35e184517cb0d18895ba4b6661c92865b431fa7b7974d8 \ - --hash=sha256:ba4a610882171bdaae0779f14e0ff45f3ee271fd2dbf16cdadfc81bd67323232 \ - --hash=sha256:bc5bd9bf5f5894923b78a41c5becd52d6bced1e1e43744855bd85cb341376ca6 \ - --hash=sha256:c37eb3be7a6be93f4925ccf52bbfa60244da6c63201770a709dd81a3d2d08534 \ - --hash=sha256:c3c5fa3d4fa0a651cefab391b783f89bc5e331afa0a4e93c9b16141993fa05c8 \ - --hash=sha256:ca5e36a3e7480a5c09aed99ecdb8e6554b21485c3b064297fe77f7b1b5806106 \ - --hash=sha256:d4e0c1b1aa5283f6d9a384ffc7a8400d25386bb98fdb9bddae446e4ef4da7366 \ - --hash=sha256:dc8bc40d82d1ee8dec136e10707c7f3147a6322fd8014e174a0f3446fb793649 \ - --hash=sha256:de576c49d7deab2b78088feb24e1f6ae3e16a0020e8496cdd3b8543f5e350e87 \ - --hash=sha256:e7d215a43343d91ba08301865f059d9518818d66a222a85fb425e4156716f5a6 \ - --hash=sha256:eb8293d66c6a4ddc72fceb7ad0e111cb196cc394954ae0f9b63c251d97f1b00e \ - --hash=sha256:ee6d8f489a9b116ea8dc797664a50671585a4ca20573359f067858e1231cc217 \ - --hash=sha256:efb878d08dd49d7d9d18512e791b418a1171d08f935475eec98305f0886b7c14 +psycopg[binary, pool]==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via feast (pyproject.toml) +psycopg-binary==3.3.4 \ + --hash=sha256:018fbed325936da502feb546642c982dcc4b9ffdea32dfef78dbf3b7f7ad4070 \ + --hash=sha256:0579252a1202cd73e4da137a1426e2dae993ae44e757605344282af3a082848c \ + --hash=sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc \ + --hash=sha256:13a7f380824c35896dcac7fe0f61440f7ca49d6dc73f3c13a9a4471e6a3b302e \ + --hash=sha256:17a21953a9e5ff3a16dab692625a3676e2f101db5e40072f39dbee2250194d68 \ + --hash=sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae \ + --hash=sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829 \ + --hash=sha256:22cdbf5f91ef7bb91fe0c5757e1962d3127a8010256eefd9c61fcaf441802097 \ + --hash=sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c \ + --hash=sha256:276904e3452d6a23d474ef9a21eee19f20eed3d53ddd2576af033827e0ba0992 \ + --hash=sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97 \ + --hash=sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6 \ + --hash=sha256:32a6fbf8481e3a370d0d72b860d35948a693cb01281da217f7b2f307636e591a \ + --hash=sha256:41f2ec0fea529832982bcb6c9415de3c86264ebe562b77a467c0fbcd7efbba8d \ + --hash=sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228 \ + --hash=sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e \ + --hash=sha256:494ca54901be8cf9eb7e02c25b731f2317c378efa44f43e8f9bd0e1184ae7be4 \ + --hash=sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41 \ + --hash=sha256:574ea21a9651958f1535c5a1c649c7409e9168bcbffa29a3f2f961f58b322949 \ + --hash=sha256:580ae30a5f95ccd90008ec697d3ed6a4a2047a516407ad904283fa42086936e9 \ + --hash=sha256:5ab28a2a7649df3b72e6b674b4c190e448e8e77cf496a65bd846472048de2089 \ + --hash=sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf \ + --hash=sha256:612a627d733f695b1de1f9b4bd511c15f999a5d8b915d444bbd7dd71cf3370da \ + --hash=sha256:6402a9d8146cf4b3974ded3fd28a971e83dc6a0333eb7822524a3aa20b546578 \ + --hash=sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014 \ + --hash=sha256:71e55ccbdfae79a2ed9c6369c3008a3025817ff9d7e27b32a2d84e2a4267e66e \ + --hash=sha256:7465bfe6087d2d5b42d4c53b9b11ca9f218e477317a4a162a10e3c19e984ba8e \ + --hash=sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31 \ + --hash=sha256:773d573e11f437ce0bdb95b7c18dc58390494f96d43f8b45b9760436114f7652 \ + --hash=sha256:77df19583501ea288eaf15ac0fe7ad01e6d8091a91d5c41df5c718f307d8e31b \ + --hash=sha256:7f7668f30b9dd5163197e5cbf4e0efd54e00f0a859cc566ce56cfc31f4054839 \ + --hash=sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277 \ + --hash=sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7 \ + --hash=sha256:ab8cca8ef8fb1ccf5b048ae5bd78ba55b9e4b5d472e3ce5ca39ff4d2a9c249e4 \ + --hash=sha256:ad3bc94054876155549fdaedf4a46d1ec69d39a5bcee377148afe498e84c4b8e \ + --hash=sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70 \ + --hash=sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf \ + --hash=sha256:b7bfff1ca23732b488cbca3076fc11bc98d520ee122514fdb17a8e20d3338f5a \ + --hash=sha256:bdef84570ebbce1d42b4e7ea952d21c414c5f118ad02fee00c5625f35e134429 \ + --hash=sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8 \ + --hash=sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3 \ + --hash=sha256:cf7f73a4a792bc5db58a4b385d8a1467e8d468f7548702fb0ed1e9b7501b1c13 \ + --hash=sha256:cffc3408d77a27973f33e5d909b624cce683db5fc25964b02fe0aae7886c1007 \ + --hash=sha256:d7b4d40c153fa352ab3cca530f3a0baedf7621b2ebcbd7f084009522c21788fc \ + --hash=sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38 \ + --hash=sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9 \ + --hash=sha256:e2631da29253a98bd496e6c4813b24e09a4fe3fb2a9e88513305d6f8747cce95 \ + --hash=sha256:e7510c37550f91a187e3660a8cc50d4b760f8c3b8b2f89ebc5698cd2c7f2c85d \ + --hash=sha256:eb05ee1c2b817d27c537333224c9e83c7afb86fe7296ba970990068baf819b16 \ + --hash=sha256:eb4eed2079c01a4850bf467deacfab56d356d4225040170af03dc9958321242d \ + --hash=sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260 \ + --hash=sha256:f80e3f2b5331dbbf0901bcb658056c03eeb2c1ef31d774afb0d61598b242e744 \ + --hash=sha256:f9b1c2533af01cd7648378599f82b0b8ae32f293296e6eec5753a625bc97ef28 \ + --hash=sha256:fa1cbc10768a796c96d3243656016bf4e337c81c71097270bb7b0ad6210d9765 \ + --hash=sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7 # via psycopg -psycopg-pool==3.2.6 \ - --hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \ - --hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7 +psycopg-pool==3.3.1 \ + --hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \ + --hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c # via psycopg ptyprocess==0.7.0 \ --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \ @@ -3728,70 +4289,86 @@ pure-eval==0.2.3 \ py==1.11.0 \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 - # via feast (setup.py) + # via feast (pyproject.toml) py-cpuinfo==9.0.0 \ --hash=sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690 \ --hash=sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5 # via pytest-benchmark -py-spy==0.4.1 \ - --hash=sha256:1fb8bf71ab8df95a95cc387deed6552934c50feef2cf6456bc06692a5508fd0c \ - --hash=sha256:4972c21890b6814017e39ac233c22572c4a61fd874524ebc5ccab0f2237aee0a \ - --hash=sha256:532d3525538254d1859b49de1fbe9744df6b8865657c9f0e444bf36ce3f19226 \ - --hash=sha256:6a80ec05eb8a6883863a367c6a4d4f2d57de68466f7956b6367d4edd5c61bb29 \ - --hash=sha256:809094208c6256c8f4ccadd31e9a513fe2429253f48e20066879239ba12cd8cc \ - --hash=sha256:d92e522bd40e9bf7d87c204033ce5bb5c828fca45fa28d970f58d71128069fdc \ - --hash=sha256:e53aa53daa2e47c2eef97dd2455b47bb3a7e7f962796a86cc3e7dbde8e6f4db4 \ - --hash=sha256:ee776b9d512a011d1ad3907ed53ae32ce2f3d9ff3e1782236554e22103b5c084 +py-spy==0.4.2 \ + --hash=sha256:142887e984a4e541071c99a4401ff8c3770f255d329dbd0f64e8c1dd51882cce \ + --hash=sha256:1ccf688393105111684435f035bc14ec3f22117dd2b85b2414612cf27a22755a \ + --hash=sha256:24720573f95230653b457671a1dcc3c5a381fcf4e92677761e328a430ad251b2 \ + --hash=sha256:7f1c6d9b0e2379ead5bf792df43f4cf36153aa79e6dda4fb8ac7740cf8017110 \ + --hash=sha256:8b06a353c177677e4e1701b288d8c58e2f8d4208ee81a8048d9f72ba800918f8 \ + --hash=sha256:90e600b27bb6bb40479637baca5a5b4bc2ba3395c93d889e672315d93042c4ae \ + --hash=sha256:a0e6f6810ccf0fc5e64e85e0182a5b626c4496eec01b14fb8755154b363a4831 \ + --hash=sha256:aeb0323409199c785f730645e9f4bb7a7b9ca2c481f2c331a55642b5d13fa52f # via ray py4j==0.10.9.9 \ --hash=sha256:c7c26e4158defb37b0bb124933163641a2ff6e3a3913f7811b0ddbe07ed61533 \ --hash=sha256:f694cad19efa5bd1dee4f3e5270eb406613c974394035e5bfc4ec1aba870b879 # via pyspark -pyarrow==17.0.0 \ - --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ - --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ - --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ - --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ - --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ - --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ - --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ - --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ - --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ - --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ - --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ - --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ - --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ - --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ - --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ - --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ - --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ - --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ - --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ - --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ - --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ - --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ - --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ - --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ - --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ - --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ - --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ - --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ - --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ - --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ - --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ - --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ - --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ - --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ - --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ - --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 - # via - # feast (setup.py) +pyarrow==24.0.0 \ + --hash=sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba \ + --hash=sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68 \ + --hash=sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868 \ + --hash=sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495 \ + --hash=sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e \ + --hash=sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66 \ + --hash=sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275 \ + --hash=sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3 \ + --hash=sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838 \ + --hash=sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491 \ + --hash=sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6 \ + --hash=sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826 \ + --hash=sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a \ + --hash=sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981 \ + --hash=sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e \ + --hash=sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c \ + --hash=sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e \ + --hash=sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05 \ + --hash=sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b \ + --hash=sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb \ + --hash=sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136 \ + --hash=sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810 \ + --hash=sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37 \ + --hash=sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147 \ + --hash=sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0 \ + --hash=sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b \ + --hash=sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb \ + --hash=sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f \ + --hash=sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83 \ + --hash=sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca \ + --hash=sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931 \ + --hash=sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d \ + --hash=sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2 \ + --hash=sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795 \ + --hash=sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26 \ + --hash=sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74 \ + --hash=sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c \ + --hash=sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57 \ + --hash=sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1 \ + --hash=sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591 \ + --hash=sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19 \ + --hash=sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42 \ + --hash=sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde \ + --hash=sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699 \ + --hash=sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041 \ + --hash=sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91 \ + --hash=sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76 \ + --hash=sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b \ + --hash=sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a \ + --hash=sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072 + # via + # feast (pyproject.toml) # dask # datasets # db-dtypes # deltalake # google-cloud-bigquery # ibis-framework + # milvus-lite + # mlflow # pandas-gbq # ray # snowflake-connector-python @@ -3799,12 +4376,10 @@ pyarrow-hotfix==0.7 \ --hash=sha256:3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 \ --hash=sha256:59399cd58bdd978b2e42816a4183a55c6472d4e33d183351b6069f11ed42661d # via ibis-framework -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 @@ -3812,62 +4387,48 @@ pyasn1-modules==0.4.2 \ pybindgen==0.22.0 \ --hash=sha256:21612f4806a2af25a175716d7e694563af7e10c704538a350cb595d187952f6f \ --hash=sha256:5b4837d3138ac56863d93fe462f1dac39fb87bd50898e0da4c57fefd645437ac - # via feast (setup.py) -pyclipper==1.3.0.post6 \ - --hash=sha256:04214d23cf79f4ddcde36e299dea9f23f07abb88fa47ef399bf0e819438bbefd \ - --hash=sha256:106b8622cd9fb07d80cbf9b1d752334c55839203bae962376a8c59087788af26 \ - --hash=sha256:16cc1705a915896d2aff52131c427df02265631279eac849ebda766432714cc0 \ - --hash=sha256:188fbfd1d30d02247f92c25ce856f5f3c75d841251f43367dbcf10935bc48f38 \ - --hash=sha256:1c03f1ae43b18ee07730c3c774cc3cf88a10c12a4b097239b33365ec24a0a14a \ - --hash=sha256:1fd56855ca92fa7eb0d8a71cf3a24b80b9724c8adcc89b385bbaa8924e620156 \ - --hash=sha256:2737df106b8487103916147fe30f887aff439d9f2bd2f67c9d9b5c13eac88ccf \ - --hash=sha256:28bb590ae79e6beb15794eaee12b6f1d769589572d33e494faf5aa3b1f31b9fa \ - --hash=sha256:2e257009030815853528ba4b2ef7fb7e172683a3f4255a63f00bde34cfab8b58 \ - --hash=sha256:32cd7fb9c1c893eb87f82a072dbb5e26224ea7cebbad9dc306d67e1ac62dd229 \ - --hash=sha256:33ab72260f144693e1f7735e93276c3031e1ed243a207eff1f8b98c7162ba22c \ - --hash=sha256:3404dfcb3415eee863564b5f49be28a8c7fb99ad5e31c986bcc33c8d47d97df7 \ - --hash=sha256:383f3433b968f2e4b0843f338c1f63b85392b6e1d936de722e8c5d4f577dbff5 \ - --hash=sha256:3d58202de8b8da4d1559afbda4e90a8c260a5373672b6d7bc5448c4614385144 \ - --hash=sha256:3e5e65176506da6335f6cbab497ae1a29772064467fa69f66de6bab4b6304d34 \ - --hash=sha256:42bff0102fa7a7f2abdd795a2594654d62b786d0c6cd67b72d469114fdeb608c \ - --hash=sha256:47a214f201ff930595a30649c2a063f78baa3a8f52e1f38da19f7930c90ed80c \ - --hash=sha256:48dd55fbd55f63902cad511432ec332368cbbbc1dd2110c0c6c1e9edd735713a \ - --hash=sha256:491ec1bfd2ee3013269c2b652dde14a85539480e0fb82f89bb12198fa59fff82 \ - --hash=sha256:58eae2ff92a8cae1331568df076c4c5775bf946afab0068b217f0cf8e188eb3c \ - --hash=sha256:5c9c80b5c46eef38ba3f12dd818dc87f5f2a0853ba914b6f91b133232315f526 \ - --hash=sha256:6363b9d79ba1b5d8f32d1623e797c1e9f994600943402e68d5266067bdde173e \ - --hash=sha256:640f20975727994d4abacd07396f564e9e5665ba5cb66ceb36b300c281f84fa4 \ - --hash=sha256:6893f9b701f3132d86018594d99b724200b937a3a3ddfe1be0432c4ff0284e6e \ - --hash=sha256:793b0aa54b914257aa7dc76b793dd4dcfb3c84011d48df7e41ba02b571616eaf \ - --hash=sha256:851b3e58106c62a5534a1201295fe20c21714dee2eda68081b37ddb0367e6caa \ - --hash=sha256:903176952a159c4195b8be55e597978e24804c838c7a9b12024c39704d341f72 \ - --hash=sha256:9699e98862dadefd0bea2360c31fa61ca553c660cbf6fb44993acde1b959f58f \ - --hash=sha256:9cbdc517e75e647aa9bf6e356b3a3d2e3af344f82af38e36031eb46ba0ab5425 \ - --hash=sha256:a01f182d8938c1dc515e8508ed2442f7eebd2c25c7d5cb29281f583c1a8008a4 \ - --hash=sha256:a63002f6bb0f1efa87c0b81634cbb571066f237067e23707dabf746306c92ba5 \ - --hash=sha256:aa0e7268f8ceba218964bc3a482a5e9d32e352e8c3538b03f69a6b3db979078d \ - --hash=sha256:aa604f8665ade434f9eafcd23f89435057d5d09427dfb4554c5e6d19f6d8aa1a \ - --hash=sha256:ace1f0753cf71c5c5f6488b8feef5dd0fa8b976ad86b24bb51f708f513df4aac \ - --hash=sha256:b15113ec4fc423b58e9ae80aa95cf5a0802f02d8f02a98a46af3d7d66ff0cc0e \ - --hash=sha256:c05ae2ea878fdfa31dd375326f6191b03de98a9602cc9c2b6d4ff960b20a974c \ - --hash=sha256:c4247e7c44b34c87acbf38f99d48fb1acaf5da4a2cf4dcd601a9b24d431be4ef \ - --hash=sha256:c92e41301a8f25f9adcd90954512038ed5f774a2b8c04a4a9db261b78ff75e3a \ - --hash=sha256:cf0a535cfa02b207435928e991c60389671fe1ea1dfae79170973f82f52335b2 \ - --hash=sha256:cf5ca2b9358d30a395ac6e14b3154a9fd1f9b557ad7153ea15cf697e88d07ce1 \ - --hash=sha256:d3f9da96f83b8892504923beb21a481cd4516c19be1d39eb57a92ef1c9a29548 \ - --hash=sha256:d6d129d0c2587f2f5904d201a4021f859afbb45fada4261c9fdedb2205b09d23 \ - --hash=sha256:dbc828641667142751b1127fd5c4291663490cf05689c85be4c5bcc89aaa236a \ - --hash=sha256:e2cd8600bd16d209d5d45a33b45c278e1cc8bedc169af1a1f2187b581c521395 \ - --hash=sha256:e3aab10e3c10ed8fa60c608fb87c040089b83325c937f98f06450cf9fcfdaf1d \ - --hash=sha256:e5ff68fa770ac654c7974fc78792978796f068bd274e95930c0691c31e192889 \ - --hash=sha256:ed6e50c6e87ed190141573615d54118869bd63e9cd91ca5660d2ca926bf25110 \ - --hash=sha256:f129284d2c7bcd213d11c0f35e1ae506a1144ce4954e9d1734d63b120b0a1b58 \ - --hash=sha256:fa0f5e78cfa8262277bb3d0225537b3c2a90ef68fd90a229d5d24cf49955dcf4 \ - --hash=sha256:fb1e52cf4ee0a9fa8b2254ed589cc51b0c989efc58fa8804289aca94a21253f7 + # via feast (pyproject.toml) +pyclipper==1.4.0 \ + --hash=sha256:0a4d2736fb3c42e8eb1d38bf27a720d1015526c11e476bded55138a977c17d9d \ + --hash=sha256:0b74a9dd44b22a7fd35d65fb1ceeba57f3817f34a97a28c3255556362e491447 \ + --hash=sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b \ + --hash=sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f \ + --hash=sha256:1b6c8d75ba20c6433c9ea8f1a0feb7e4d3ac06a09ad1fd6d571afc1ddf89b869 \ + --hash=sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140 \ + --hash=sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04 \ + --hash=sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a \ + --hash=sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4 \ + --hash=sha256:58e29d7443d7cc0e83ee9daf43927730386629786d00c63b04fe3b53ac01462c \ + --hash=sha256:6a97b961f182b92d899ca88c1bb3632faea2e00ce18d07c5f789666ebb021ca4 \ + --hash=sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826 \ + --hash=sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9 \ + --hash=sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c \ + --hash=sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e \ + --hash=sha256:8d42b07a2f6cfe2d9b87daf345443583f00a14e856927782fde52f3a255e305a \ + --hash=sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1 \ + --hash=sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87 \ + --hash=sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39 \ + --hash=sha256:a8d2b5fb75ebe57e21ce61e79a9131edec2622ff23cc665e4d1d1f201bc1a801 \ + --hash=sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9 \ + --hash=sha256:adcb7ca33c5bdc33cd775e8b3eadad54873c802a6d909067a57348bcb96e7a2d \ + --hash=sha256:b3b3630051b53ad2564cb079e088b112dd576e3d91038338ad1cc7915e0f14dc \ + --hash=sha256:bafad70d2679c187120e8c44e1f9a8b06150bad8c0aecf612ad7dfbfa9510f73 \ + --hash=sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286 \ + --hash=sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c \ + --hash=sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872 \ + --hash=sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037 \ + --hash=sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca \ + --hash=sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e \ + --hash=sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832 \ + --hash=sha256:e9b973467d9c5fa9bc30bb6ac95f9f4d7c3d9fc25f6cf2d1cc972088e5955c01 \ + --hash=sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303 \ + --hash=sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1 \ + --hash=sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6 \ + --hash=sha256:fd24849d2b94ec749ceac7c34c9f01010d23b6e9d9216cf2238b8481160e703d # via easyocr -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi pycryptodome==3.23.0 \ --hash=sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4 \ @@ -3912,12 +4473,13 @@ pycryptodome==3.23.0 \ --hash=sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be \ --hash=sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7 # via minio -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # codeflare-sdk + # dbt-artifacts-parser # docling # docling-core # docling-ibm-models @@ -3926,290 +4488,434 @@ pydantic==2.10.6 \ # fastapi-mcp # great-expectations # mcp + # mcp-types + # mlflow-skinny + # mlflow-tracing # pydantic-settings + # pyiceberg # qdrant-client # ray -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pydantic-settings==2.11.0 \ - --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ - --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f # via # docling + # docling-core # fastapi-mcp - # mcp pydata-google-auth==1.9.1 \ --hash=sha256:0a51ce41c601ca0bc69b8795bf58bedff74b4a6a007c9106c7cbcdec00eaced2 \ --hash=sha256:75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 # via pandas-gbq -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via - # feast (setup.py) + # feast (pyproject.toml) # ipython # ipython-pygments-lexers # mpire # nbconvert # rich # sphinx -pyjwt[crypto]==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb - # via - # feast (setup.py) +pyiceberg==0.11.1 \ + --hash=sha256:138983e96a5c473aae1e1cd6143aacf02ccc9bbbf6180f15fa2588f99531188b \ + --hash=sha256:1663d79fc8400903992c63f79b3908b9298c623138e8929bf36c559231e082d3 \ + --hash=sha256:1d6b6f0c1e7dd8357f1ba56524bfc870d04ad3c00979db291784a7145497ad3b \ + --hash=sha256:23ed91f8d2fa75a109708338f80a7addd2f37c93a8b0e6c9d220f1427939b81f \ + --hash=sha256:366fe0d5a74e3cf1d4e7cbf3c49e308da60e7835ea268667be9185388f05d7a5 \ + --hash=sha256:4d8790f420ebc484236017edba59182cf2a21bd3e4224a0bd0760a9c7268e96a \ + --hash=sha256:6400774e6820760eb6c322f6feace43fe7267deb9f8d508f10bf258887a9c4d5 \ + --hash=sha256:65a7ad892a570045b0de2db6af17119162880aebc05a0c125ce2db7dab36f17e \ + --hash=sha256:6cf94756fb6a822d20a5a64f44840e6633ebf8b1deb3ce01057bff1cc03b01c2 \ + --hash=sha256:7770e7bdee34549e8baf28b8626e6552de7894d869465a7c95fd9af33b4cbeb0 \ + --hash=sha256:856c7fca5ed780ed44f60bceb92d6b311ebc008a2249415b8f6045201d4f5530 \ + --hash=sha256:89c347170a691e3b8d072519f36790b9c19a2263ec0af0144c873994ecfe55eb \ + --hash=sha256:8bd52c1891ae74cee21a4ebe8325953310a2e0af5352d70f47f5461422fcce2d \ + --hash=sha256:8c62636a1e9d8a1fc74ffb70383939b9cd93f2c9ee8e12015a50dd75c98a989e \ + --hash=sha256:a0f958cbca18d05846e3081dfff8575e73d45595441d659847479656dc76f91d \ + --hash=sha256:ae27ba4d37925d5b2cff192acaa70c8bb114d632bbc527cc91fea0370702b866 \ + --hash=sha256:b347d3cc8510f8fbe191956fcda7da372ebb3302789acefca08e352345959003 \ + --hash=sha256:b4b91f1b1bac135c077326d30a1876f917e6c36b844a2e329b148fb9cb7aa660 \ + --hash=sha256:b7ec5db19feab98a31fcd5caccf4a9a4e83f96933d1ca393ba7aea665710c2bb \ + --hash=sha256:ba98d6a41ec0b7c81dd85d764f15653d6abbbbd69d92630677c43f92dd50d924 \ + --hash=sha256:bba3a35b4648694783aeae5b77c235a57191c8b1b375c8602b03ae56a6cf4fe7 \ + --hash=sha256:ccac408873f65b8954858ef08d4b098841301e232afc1b14e3eada489d303a7f \ + --hash=sha256:cd423b8ee2f75fc9db09158875abe5e2c952a26ae5e521c3265ab2f9d3511ddf \ + --hash=sha256:cec0616d2ba6e7dda6327089a2f34ec723aa9ac2c389857ef0b83f65fb135dd6 \ + --hash=sha256:db66a4e0fdfbf4090631d59c3f65e960d9a5561e9259f6f3993cbe91e396837e \ + --hash=sha256:ddb360da76c62c7c23ec3da40e1af48e6712a563905fea2d1a8911ff7a3b6c4d \ + --hash=sha256:e08c268e12d54ea629a1f123bf7cb9587bdd57fd7bcdac22394dc6272a18668f \ + --hash=sha256:e273242cdca56029af694d7ce18075d47a74d034326d663ff6dd2655a6f44825 \ + --hash=sha256:eb3a0a3e630ee89758eb96b39b456f4697732351fb0c080e9498ea578f9b71f9 + # via feast (pyproject.toml) +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + # via + # feast (pyproject.toml) + # kube-authkit + # mcp # msal # singlestoredb # snowflake-connector-python -pylatexenc==2.10 \ - --hash=sha256:3dd8fd84eb46dc30bee1e23eaab8d8fb5a7f507347b23e5f38ad9675c84f40d3 +pylatexenc==2.11 \ + --hash=sha256:305a072a99ce736246049c9da05841b9d718c0f7ea8888f5f596cf15cb621053 \ + --hash=sha256:e78e7391d6c104f1ed150e21cfaa58016cdb50aa54406a2eecb793649ffdfdd0 # via docling -pymilvus==2.4.9 \ - --hash=sha256:0937663700007c23a84cfc0656160b301f6ff9247aaec4c96d599a6b43572136 \ - --hash=sha256:45313607d2c164064bdc44e0f933cb6d6afa92e9efcc7f357c5240c57db58fbe - # via feast (setup.py) -pymssql==2.3.2 \ - --hash=sha256:06883bc9bdb297ae9132d9371b5b1a3a223c8f93dd6a87d1c112c6a688f26d53 \ - --hash=sha256:0768d90f96ae3267d7561d3bcfe94dd671d107489e870388b12570c3debbc552 \ - --hash=sha256:0831c5c95aab0b9aba5142dc97e28f59c4130e1c34ffc13ecbfdd4d2fe45b8a0 \ - --hash=sha256:08facd25a50a7279385d1ffcee9d6d83c4e361db1af38e14519a87d7b1cadb10 \ - --hash=sha256:0a20a17db870fb0e446a6d6bf7664aaf84af7be58ab1fad025cafa4e092507a1 \ - --hash=sha256:1037053e6c74d6fe14c428cc942968b4e4bf06854706a83fe8e822e475e3f107 \ - --hash=sha256:148b7714fff5a5b7ce038e92b02dd9bf68fe442c181a3aae32148e7b13f6db95 \ - --hash=sha256:18089641b687be1ebd0f64f0d1ff977478a397ffa1af372bdf10dbec29cf6d2e \ - --hash=sha256:1afda7b7022eff9451bd83e3f64c450a1a8cdff4ba8b8e399866dcd2cb861a1e \ - --hash=sha256:1bc33ed9af6d8ebea2d49144cd2317b7ae1105dd51dddfd46982c90c8f0cf6ab \ - --hash=sha256:1c99dba4bf5b1ce10657e9e2885f18ba9179190251b63d1498e7d6d72e64f1ce \ - --hash=sha256:1cdc2619e7b4192b8d6619fd52ba8a2eae18b38b376f8649fb8f0727c4e88ff9 \ - --hash=sha256:22b1ce3a48f28ee7d06ebc9ed94276d0bf1c99051ee1df3d2377b74721bd62ef \ - --hash=sha256:22fb0fdd3b889bc10abbe3aa2abe7a008b30a6367b9ba159412d185d7d8fda9d \ - --hash=sha256:235c230e56d8c8c5f289e665c538f31d967fec302d05ad269dcd64fa9d6eb3b7 \ - --hash=sha256:23f5e2e2bdba1cf7cecbac66dd07de7631a8efca5692efee18ff46ebc087b757 \ - --hash=sha256:2568944db3888996e161b40ad06c1b9e0fbb6cfcb38279a3abb98ece7a8e1c4a \ - --hash=sha256:26bdb7abd5f107b6be422635f03e2cecaa52a5f4c394a205014586abbff9e72a \ - --hash=sha256:2a44a0898dacba4e25cac8778d0ed112e297883fe862204e447081888da78dc4 \ - --hash=sha256:2f4093b95f1f3a1232687fc92f652aaf675eb423db8549c16d146b91ac2f0eba \ - --hash=sha256:33ad813092f8fb8f74578c5b5e37c818c4ae130fd4047cb28f0b256f2f107367 \ - --hash=sha256:3870085a49e5332bc67ecb24f217c586977d5352eb51598244fc7bc278eee3e1 \ - --hash=sha256:41d09e1b2534229b288c37b88c1de3d964317af2c7eec58bfb97e01d679eba27 \ - --hash=sha256:5904d78e61668ec89761d3ae01efd4b42b31d820f612929f449e93cd23ba3c54 \ - --hash=sha256:6019d2939963112662288704f608f31634038bffcfd5cad1bc79cb167edb3cc1 \ - --hash=sha256:72b6599963b6e066998c4b27b7bf207684c243b12b1e5dcc180b2af22802ae6c \ - --hash=sha256:73fac766b448613d7ae26e6b304b2cb8a7ffebccaa373633bad3b3cbcc829935 \ - --hash=sha256:793a93da1521fa66bf02b3b873065e22bf14bda5570e005ce3d5fae0776d7b92 \ - --hash=sha256:79cdc3ed1da3129ba56232127db86279728c4328595e2532ed4d0da6379a5c72 \ - --hash=sha256:82ed3dd560d3fb222d26ce3a7373f46dc3ad1d50b6e6417ef7399e87fa9aefe1 \ - --hash=sha256:8cd806380d362d4cef2d925a6baee6a4b2b151a92cac2cab5c4bfabed4be4849 \ - --hash=sha256:9361593a89c9162fc631baf648a87e2666373382d9d54aacfb19edab9ceb2007 \ - --hash=sha256:97fbd8491ad3ece0adcb321acec6db48b8fe37bc74af4c91bb657d4d9347d634 \ - --hash=sha256:9e3d6fada7fbe7a5f5fafc420673f777bab3f399c78fa44e29de6a8cbc36e515 \ - --hash=sha256:a3f9e7eb813dfeab6d01bf6474049bb76b0521235159d3e969ec82df384eac49 \ - --hash=sha256:aa08b6203b2b5ed5ce47f80d5c529459181300d7e0d0c1e84390a4d01d45e509 \ - --hash=sha256:ab48de09864fa6f49c575ef569f6773981d0cd905ff7288b5b185f8079a5a21f \ - --hash=sha256:ab912d1178d5977e421cf9c4d4071958b223cbe4a2b6dd64611d521aa6bb7187 \ - --hash=sha256:ae02cc1594f0addd748bf5ac1ccc7a73c03846ada9c553663c381b242b586606 \ - --hash=sha256:b0c2b11aca16617cacaf385fb94134e73ba0216a924f9b85778cc7e3d3713361 \ - --hash=sha256:b14cc65369d1425f2fb517609113465a0f55f19a49648160f2d10be4cb43ff4d \ - --hash=sha256:b156b15165f7a0bbb392a124d8e2d678145c93e5bfcfef3b637e4d87eadcc85b \ - --hash=sha256:b16d5880f7028442d6c49c94801ce9bff3af8af0fbda7c6039febb936714aed5 \ - --hash=sha256:b3eb201c402bcf4f5b9399df0bb20d522636d2e87d1c6957a0b6d772ee636c61 \ - --hash=sha256:bac6f355c454f94b0e15a04b7841236e5c5c4ef44d2d1beed00a3ad7b50ccc53 \ - --hash=sha256:c24ba6aedb9b5540b56f3e74bff92b687c6e90c00650823385729c7e55923cf5 \ - --hash=sha256:cbe9058b6520be74463476ff2cdb17bbab5ff60b60b3ed7bd8bd2d086bdfd9bd \ - --hash=sha256:cc13c2e0f1b8efc3a46941de9db768fa59937b5a54081ec0cb0ff0da17d1fff3 \ - --hash=sha256:dd5fe7552edc81628e4242b4671f7bad5ff1ec790bae5c7615d989375620edac \ - --hash=sha256:eb629b5fb0376fbf39d575cf1365e504b84877b19f9e8d53caa5228fed56894a \ - --hash=sha256:ee8ee2c7c227c413ad9b88ddba1cb6a25e28c217ae73ecac1c7a6b8c29003604 \ - --hash=sha256:ef0d29c705db552f9e75230f946b0ca9db0db903c5c9ee79ce8b88ad25ea9670 \ - --hash=sha256:f1791f4627c42fe2d2833c884d036b0c5c8cf628f2cdfa3536191c217acf729e \ - --hash=sha256:f282e701dca155b3e7f1644d7e3b60c201ca5f3be8045bce34042d3c737d63ee \ - --hash=sha256:f2b1da4e68d618c7972e583ae19f386ae620258acb61564e8067c203f27cd769 \ - --hash=sha256:f9737c06b13ca2012b9900185fa3af72a37941c532da2e6373dd7c9ab16abddf \ - --hash=sha256:fb8a7b197aaf466a7577ca6690aa9d747081b653ab212d052d71f3cc10587c3b \ - --hash=sha256:fdd774b26407babd0205ef85a098f90553e6b3da77a22322a1e7d2cb51f742c0 - # via feast (setup.py) -pymysql==1.1.2 \ - --hash=sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03 \ - --hash=sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 - # via feast (setup.py) -pynacl==1.6.0 \ - --hash=sha256:04f20784083014e265ad58c1b2dd562c3e35864b5394a14ab54f5d150ee9e53e \ - --hash=sha256:10d755cf2a455d8c0f8c767a43d68f24d163b8fe93ccfaabfa7bafd26be58d73 \ - --hash=sha256:140373378e34a1f6977e573033d1dd1de88d2a5d90ec6958c9485b2fd9f3eb90 \ - --hash=sha256:16c60daceee88d04f8d41d0a4004a7ed8d9a5126b997efd2933e08e93a3bd850 \ - --hash=sha256:16dd347cdc8ae0b0f6187a2608c0af1c8b7ecbbe6b4a06bff8253c192f696990 \ - --hash=sha256:25720bad35dfac34a2bcdd61d9e08d6bfc6041bebc7751d9c9f2446cf1e77d64 \ - --hash=sha256:2d6cd56ce4998cb66a6c112fda7b1fdce5266c9f05044fa72972613bef376d15 \ - --hash=sha256:347dcddce0b4d83ed3f32fd00379c83c425abee5a9d2cd0a2c84871334eaff64 \ - --hash=sha256:4853c154dc16ea12f8f3ee4b7e763331876316cc3a9f06aeedf39bcdca8f9995 \ - --hash=sha256:49c336dd80ea54780bcff6a03ee1a476be1612423010472e60af83452aa0f442 \ - --hash=sha256:4a25cfede801f01e54179b8ff9514bd7b5944da560b7040939732d1804d25419 \ - --hash=sha256:51fed9fe1bec9e7ff9af31cd0abba179d0e984a2960c77e8e5292c7e9b7f7b5d \ - --hash=sha256:536703b8f90e911294831a7fbcd0c062b837f3ccaa923d92a6254e11178aaf42 \ - --hash=sha256:5789f016e08e5606803161ba24de01b5a345d24590a80323379fc4408832d290 \ - --hash=sha256:6b08eab48c9669d515a344fb0ef27e2cbde847721e34bba94a343baa0f33f1f4 \ - --hash=sha256:6b393bc5e5a0eb86bb85b533deb2d2c815666665f840a09e0aa3362bb6088736 \ - --hash=sha256:84709cea8f888e618c21ed9a0efdb1a59cc63141c403db8bf56c469b71ad56f2 \ - --hash=sha256:8bfaa0a28a1ab718bad6239979a5a57a8d1506d0caf2fba17e524dbb409441cf \ - --hash=sha256:bbcc4452a1eb10cd5217318c822fde4be279c9de8567f78bad24c773c21254f8 \ - --hash=sha256:cb36deafe6e2bce3b286e5d1f3e1c246e0ccdb8808ddb4550bb2792f2df298f2 \ - --hash=sha256:cf831615cc16ba324240de79d925eacae8265b7691412ac6b24221db157f6bd1 \ - --hash=sha256:dcdeb41c22ff3c66eef5e63049abf7639e0db4edee57ba70531fc1b6b133185d \ - --hash=sha256:dea103a1afcbc333bc0e992e64233d360d393d1e63d0bc88554f572365664348 \ - --hash=sha256:ef214b90556bb46a485b7da8258e59204c244b1b5b576fb71848819b468c44a7 \ - --hash=sha256:f3482abf0f9815e7246d461fab597aa179b7524628a4bc36f86a7dc418d2608d \ - --hash=sha256:f46386c24a65383a9081d68e9c2de909b1834ec74ff3013271f1bca9c2d233eb \ - --hash=sha256:f4b3824920e206b4f52abd7de621ea7a44fd3cb5c8daceb7c3612345dfc54f2e +pymilvus==3.0.1 \ + --hash=sha256:c02389059088b18d6e598cd175541e445c772fab4926c5e527c4913be34887f1 \ + --hash=sha256:c5a8d5c1fa1de7b416e3529d383d8cc2e7da2170433ffa4a2d9087e14f70171a + # via feast (pyproject.toml) +pymongo==4.17.0 \ + --hash=sha256:0ff6bd2f735ab5356541e3e57d5b7dbfbc3f2ee1ccb10b6b0f82d58af69d1d8e \ + --hash=sha256:1175563375d682260f613a96fb7a53dce746ed752bfd924eab61de3bc5bfde34 \ + --hash=sha256:1195370a77baf003b59b10e91ecc4706297197f0dd9d29c840cc556dc08f7cee \ + --hash=sha256:12c4fded3a9f1d6a687e36ebd384ac6d00b9b00de1969aa74048e7051ec2a713 \ + --hash=sha256:15d3f3d732aecac1f8d481bde4029755615639bd3076f258a2147210aec8515a \ + --hash=sha256:20323b0b1c1d33770ad1fc68d429c757734ce9ad3594421c3d6618f10572b1b9 \ + --hash=sha256:2a0d5ac205728c86e0a02192f1aa5f865b0d7d51f8df6101c01a69a7fc620d72 \ + --hash=sha256:2db66aa8dd253a0fc1fad3b0d23d5b3993f7ebde02fbbd7727128debf2853675 \ + --hash=sha256:2e190827834fce70ecdf9d46796c6dbc0ce08ea87dc2ff5bc6f3f5579b605cb9 \ + --hash=sha256:320b34457b20bbcc79997801f95d25ce00472915ca5241167242b42c4359e027 \ + --hash=sha256:3689ea34f6b647c7d1e7bdc60fcfb214b2789ed1359a7fb96569c69f50e5f18f \ + --hash=sha256:37a8385c29881b43eab31f584100fa0eaddedd5607adf010147ba1810118be90 \ + --hash=sha256:3987e96e7c7be4083d42e8ac2cc6c0d5b78db9973c90fce42ae800b616ca6b20 \ + --hash=sha256:4141e6c6a339789b2974efa00ecd9409101672d77a0e3ee2cc3839eedf8ec4df \ + --hash=sha256:422fa50d7d7f5c22ea0953554396c9ef95684a2d775f860bd75a7b510538dfca \ + --hash=sha256:47b021363cd923ace5edc7a1d63c0ff8a6d9d43859b8a1ba23645f5afae63221 \ + --hash=sha256:485c8a8eaa4c739f00a331fc73757898ee7c092c214a79e63866ff76aaf282ff \ + --hash=sha256:48bbc576677b50af043df870d84ded67cc3a9b4aa7553201beef4da5dc050a0a \ + --hash=sha256:4ae22fafca69dd3c78261969e999782ac5fc23b76cf8cccfbc3707982a74cc3d \ + --hash=sha256:50e8f8e23c6df7c6d6929f5e734980b227706e73ee847517c9ba5af90f7fc466 \ + --hash=sha256:51e1915761f65f2aaabd0ba691a31d56551d3f19d1263c2d6bf261730603de5f \ + --hash=sha256:5376ad67bb30ae910d83affcf997f706d9dee37e8b5dad8b6fedb0626e262d85 \ + --hash=sha256:5960519b4d7168f1ecdd3ea10c81b2aedeb9423651aca953cfbc8e76705d3b38 \ + --hash=sha256:5a5de048e6da5c18e27cc2437e8c15b3b0cdc8385c15b41178b0caa3322a09c2 \ + --hash=sha256:5ab3b8ff79e0dfc49b68f3c925e8cc735ea95c60efaed84cfe75692dffcaac2a \ + --hash=sha256:64837adbbd72073301af51bb0fc80e3d7707fe5527cea1033ba0320f0b2f881b \ + --hash=sha256:6877214bff5f06f6884a9fc8d9016a4a7a5f51f537f5c51ac3a576f93e7dfb32 \ + --hash=sha256:68fca71e05ee5da23a8d73cee8379dfb3d26e609a377cae731d742771ed96946 \ + --hash=sha256:6c5f62862d0f87be481fa1fe8cb811994486773c94a2b61e509285e3f2890763 \ + --hash=sha256:6fe0de9d0f6791abce3471230b32b4817bf89d27b1182b6a550e1ec0fa72aa9a \ + --hash=sha256:70ffa08ba641468cc068cf46c06b34f01a8ce3489f6411309fcb5ceabe6b2fc0 \ + --hash=sha256:757f2a4c0c2c46cab87df0333681ce69e86c9d5b45bc5203ceba5410b3489e59 \ + --hash=sha256:75bc3aa5b94fdb7138d357ec6ca61cd97e0c79f4f7f0bd3efe9639b15cc50942 \ + --hash=sha256:77aa4bc164b4de60d5db193b322f0f5b6ead716e831031bfdef8e8bd92205556 \ + --hash=sha256:7db10678814cdf7ea39fd308c6f41395cfa7b29d904bcd7895288963d8f892ba \ + --hash=sha256:809ec74de3b9148ae43fa8df9faf53470f511c8d384f13b99d6f671f2a379f15 \ + --hash=sha256:8446ff4bfcb6ec2a2e50998c860986a1e992136f998b7f53e7a717fb8aa5a0b9 \ + --hash=sha256:8a1be016198a03fd7727cdd55998964bfa4e5a6fd9733c8e95830628cef34d29 \ + --hash=sha256:8e97e03fa13327c87e3fdc5656acd01e71817f0c1dc3221cd8f30de136bf4ec3 \ + --hash=sha256:93641192644fa1ee0f34030e774fd31022a27ad11ba22cb1716142231524f8bd \ + --hash=sha256:9543d8f84c2e5608565c08ac679774811e6730770d8a645439b073422a4276fb \ + --hash=sha256:9828485f72f63c7d802e0ec41f71906f633c2692621ab3af55ca990186b091b1 \ + --hash=sha256:9eb5d63a3c518cb0804ed678f5e2b875af032d89a7cf57a57360322cf6a4d222 \ + --hash=sha256:a431b737816bf4cddd4fa0fcef04e424ad36b7692734a64150f872fb8f3208be \ + --hash=sha256:a8f9c40a09bb7d4b9fc8b1da65ecf6efa79bda5cb2756f39d9b6940fac1d19ae \ + --hash=sha256:addd0498ebbdc6354227f6ed457ed9fce442d48a3bb30d5b5bad33e104996561 \ + --hash=sha256:b24598dc3c2feccbc83b43044be48145a0dc4f9bee49ef923e3d707d54a55d85 \ + --hash=sha256:b2dfcc795f5b9fedbe179a11fdf6051581479d196582a3fe819a92a00e9b9969 \ + --hash=sha256:b4384700cffc3f1dd98e088bc0072dedf6d7d68a230bb4b972665cf69c071c1e \ + --hash=sha256:b93b22eedc62598cf5ee9d8c8007a8e9121c50fd88137012d8985500e9dc3151 \ + --hash=sha256:ba2195d4f386f839a52a23ea1cfd60ffaaba78a3d7841db51b7e433001139918 \ + --hash=sha256:bb3ebc86782049f6928dcc583008287cb1c17d463501c94a620f035f5b4fd463 \ + --hash=sha256:bd835cdb37a1adec359dd072c24f8bb14809e2644fde86fab4ee2fc9719b9483 \ + --hash=sha256:c2292144505fb12156b981bd440f3dc994a883da06ac726c0c8692ccdbc1c510 \ + --hash=sha256:c4979e7e8887862bbb44d203f00cc8263a3f27237876fa691b6beba23e40e6d8 \ + --hash=sha256:c5c8e180cb2cabe37300e1e36c60aa4f2ff956cc579f0142135a5d2cba252243 \ + --hash=sha256:c797f8a80957134f6dd9690367a0f8f5906d672119af2c6aa55f0c527b656bed \ + --hash=sha256:c9786665926a09630c5d420c79762cfadbff35a9438bcbc4c81a9fb5ab9228b7 \ + --hash=sha256:cee36b3c0d0354f880fa7a7fdcdaf2bb5e542c2281e25c1bfadf8cfe21eba7d2 \ + --hash=sha256:d53ffa94b2340dbf6b055e09a0090618c60482c158ecfc9565642fc996bf0944 \ + --hash=sha256:df4a644af9ae132d4bfdb2e9516ea51a615fd881caddfbfbd071cf1354844479 \ + --hash=sha256:dff3de1294fbbc1db0ba6b511f77b8e540601d092538a31312e99c8a91a78b1e \ + --hash=sha256:e46767f28dea610e02edf6c5d956ce615c3c7790ea396660b9b1efd5c5ead2e0 \ + --hash=sha256:e4fab10f8403169ce92f3cea921609d9ee81107306caae06c08f592d4b8ad2b5 \ + --hash=sha256:e537e95514dae1aaa718f481ec03151a0f0394bcd05f1322896d8fc1330cb729 \ + --hash=sha256:e68c76b84e0c132d9dbf9307f12ff8185702328187a87b9aca8c941303873433 \ + --hash=sha256:e816db649ba5d7de0568cf3a9f287a9dc9aad21cf0ca667ab156a7ef47fca0b0 \ + --hash=sha256:f09645e0ce4e3825fa0baa8254064a716ed0be33f78feeedd4731016cb8aaa17 \ + --hash=sha256:f3ee3d241ed77a4fc99ce3cff3b289c3ebce37f61fdd7349d3592c23b82c8784 \ + --hash=sha256:faf03e4c2aafd6de626dbd30ba246d369ae33f47f10629d1bbe40f72115027a6 \ + --hash=sha256:ff5aa3f1c7e3f08eb0e7a016c91ba468b1850ccfd63d9b1f12f56350f4974cef + # via feast (pyproject.toml) +pymssql==2.3.13 \ + --hash=sha256:0a7e6431925572bc75fb47929ae8ca5b0aac26abfe8b98d4c08daf117b5657f1 \ + --hash=sha256:0fddd24efe9d18bbf174fab7c6745b0927773718387f5517cf8082241f721a68 \ + --hash=sha256:123c55ee41bc7a82c76db12e2eb189b50d0d7a11222b4f8789206d1cda3b33b9 \ + --hash=sha256:1493f63d213607f708a5722aa230776ada726ccdb94097fab090a1717a2534e0 \ + --hash=sha256:152be40c0d7f5e4b1323f7728b0a01f3ee0082190cfbadf84b2c2e930d57e00e \ + --hash=sha256:16c5957a3c9e51a03276bfd76a22431e2bc4c565e2e95f2cbb3559312edda230 \ + --hash=sha256:17942dc9474693ab2229a8a6013e5b9cb1312a5251207552141bb85fcce8c131 \ + --hash=sha256:1c6d0b2d7961f159a07e4f0d8cc81f70ceab83f5e7fd1e832a2d069e1d67ee4e \ + --hash=sha256:2137e904b1a65546be4ccb96730a391fcd5a85aab8a0632721feb5d7e39cfbce \ + --hash=sha256:2b056eb175955f7fb715b60dc1c0c624969f4d24dbdcf804b41ab1e640a2b131 \ + --hash=sha256:30918bb044242865c01838909777ef5e0f1b9ecd7f5882346aefa57f4414b29c \ + --hash=sha256:319810b89aa64b99d9c5c01518752c813938df230496fa2c4c6dda0603f04c4c \ + --hash=sha256:476f6f06b2ae5dfbfa0b169a6ecdd0d9ddfedb07f2d6dc97d2dd630ff2d6789a \ + --hash=sha256:48631c7b9fd14a1bd5675c521b6082590bf700b7961c65638d237817b3fde735 \ + --hash=sha256:4aa18944a121f996178e26cadc598abdbf73759f03dc3cd74263fdab1b28cd96 \ + --hash=sha256:4b834c34e7600369eee7bc877948b53eb0fe6f3689f0888d005ae47dd53c0a66 \ + --hash=sha256:4d87237500def5f743a52e415cd369d632907212154fcc7b4e13f264b4e30021 \ + --hash=sha256:51e42c5defc3667f0803c7ade85db0e6f24b9a1c5a18fcdfa2d09c36bff9b065 \ + --hash=sha256:5c045c0f1977a679cc30d5acd9da3f8aeb2dc6e744895b26444b4a2f20dad9a0 \ + --hash=sha256:5c2e55b6513f9c5a2f58543233ed40baaa7f91c79e64a5f961ea3fc57a700b80 \ + --hash=sha256:612ac062027d2118879f11a5986e9d9d82d07ca3545bb98c93200b68826ea687 \ + --hash=sha256:6a6c0783d97f57133573a03aad3017917dbdf7831a65e0d84ccf2a85e183ca66 \ + --hash=sha256:79c759db6e991eeae473b000c2e0a7fb8da799b2da469fe5a10d30916315e0b5 \ + --hash=sha256:7d7037d2b5b907acc7906d0479924db2935a70c720450c41339146a4ada2b93d \ + --hash=sha256:7f9b1d5aef2b5f47a7f9d9733caee4d66772681e8f798a0f5e4739a8bdab408c \ + --hash=sha256:8d66ce0a249d2e3b57369048d71e1f00d08dfb90a758d134da0250ae7bc739c1 \ + --hash=sha256:910404e0ec85c4cc7c633ec3df9b04a35f23bb74a844dd377a387026ae635e3a \ + --hash=sha256:a930adda87bdd8351a5637cf73d6491936f34e525a5e513068a6eac742f69cdb \ + --hash=sha256:aa5e07eff7e6e8bd4ba22c30e4cb8dd073e138cd272090603609a15cc5dbc75b \ + --hash=sha256:b0af51904764811da0bfe4b057b1d72dee11a399ce9ed5770875162772740c8a \ + --hash=sha256:c0ea72641cb0f8bce7ad8565dbdbda4a7437aa58bce045f2a3a788d71af2e4be \ + --hash=sha256:c690f1869dadbf4201b7f51317fceff6e5d8f5175cec6a4a813e06b0dca2d6ed \ + --hash=sha256:cf4f32b4a05b66f02cb7d55a0f3bcb0574a6f8cf0bee4bea6f7b104038364733 \ + --hash=sha256:d663c908414a6a032f04d17628138b1782af916afc0df9fefac4751fa394c3ac \ + --hash=sha256:d94da3a55545c5b6926cb4d1c6469396f0ae32ad5d6932c513f7a0bf569b4799 \ + --hash=sha256:db77da1a3fc9b5b5c5400639d79d7658ba7ad620957100c5b025be608b562193 \ + --hash=sha256:e053b443e842f9e1698fcb2b23a4bff1ff3d410894d880064e754ad823d541e5 \ + --hash=sha256:e7c31f192da9d30f0e03ad99e548120a8740a675302e2f04fa8c929f7cbee771 \ + --hash=sha256:eb3275985c23479e952d6462ae6c8b2b6993ab6b99a92805a9c17942cf3d5b3d \ + --hash=sha256:f1897c1b767cc143e77d285123ae5fd4fa7379a1bfec5c515d38826caf084eb6 \ + --hash=sha256:f5d995a80996235ed32102a93067ce6a7143cce3bfd4e5042bf600020fc08456 \ + --hash=sha256:fc5482969c813b0a45ce51c41844ae5bfa8044ad5ef8b4820ef6de7d4545b7f2 \ + --hash=sha256:ff5be7ab1d643dbce2ee3424d2ef9ae8e4146cf75bd20946bc7a6108e3ad1e47 + # via feast (pyproject.toml) +pymysql==1.2.0 \ + --hash=sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 \ + --hash=sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33 + # via feast (pyproject.toml) +pynacl==1.6.2 \ + --hash=sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c \ + --hash=sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574 \ + --hash=sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4 \ + --hash=sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130 \ + --hash=sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b \ + --hash=sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590 \ + --hash=sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444 \ + --hash=sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634 \ + --hash=sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87 \ + --hash=sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa \ + --hash=sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594 \ + --hash=sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0 \ + --hash=sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e \ + --hash=sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c \ + --hash=sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0 \ + --hash=sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c \ + --hash=sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577 \ + --hash=sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145 \ + --hash=sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88 \ + --hash=sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14 \ + --hash=sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6 \ + --hash=sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465 \ + --hash=sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0 \ + --hash=sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2 \ + --hash=sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9 # via paramiko -pyodbc==5.2.0 \ - --hash=sha256:057b8ede91b21d9f0ef58210d1ca1aad704e641ca68ac6b02f109d86b61d7402 \ - --hash=sha256:0dae0fb86078c87acf135dbe5afd3c7d15d52ab0db5965c44159e84058c3e2fb \ - --hash=sha256:0e4412f8e608db2a4be5bcc75f9581f386ed6a427dbcb5eac795049ba6fc205e \ - --hash=sha256:113f904b9852c12f10c7a3288f5a3563ecdbbefe3ccc829074a9eb8255edcd29 \ - --hash=sha256:207f16b7e9bf09c591616429ebf2b47127e879aad21167ac15158910dc9bbcda \ - --hash=sha256:26844d780045bbc3514d5c2f0d89e7fda7df7db0bd24292eb6902046f5730885 \ - --hash=sha256:26b7f8324fa01c09fe4843ad8adb0b131299ef263a1fb9e63830c9cd1d5c45e4 \ - --hash=sha256:26d2d8fd53b71204c755abc53b0379df4e23fd9a40faf211e1cb87e8a32470f0 \ - --hash=sha256:2b5323be83fedc79a6d1e1b96e67bdc368c1d3f1562b8f8184b735acdd749ae9 \ - --hash=sha256:4627779f0a608b51ce2d2fe6d1d395384e65ca36248bf9dbb6d7cf2c8fda1cab \ - --hash=sha256:4d997d3b6551273647825c734158ca8a6f682df269f6b3975f2499c01577ddec \ - --hash=sha256:4fde753fcea625bfaed36edae34c2fba15bf0b5d0ea27474ee038ef47b684d1d \ - --hash=sha256:5102007a8c78dd2fc1c1b6f6147de8cfc020f81013e4b46c33e66aaa7d1bf7b1 \ - --hash=sha256:5f0ecbc7067467df95c9b8bd38fb2682c4a13a3402d77dccaddf1e145cea8cc0 \ - --hash=sha256:600ef6f562f609f5612ffaa8a93827249150aa3030c867937c87b24a1608967e \ - --hash=sha256:6493b9c7506ca964b80ad638d0dc82869df7058255d71f04fdd1405e88bcb36b \ - --hash=sha256:74135cb10c1dcdbd99fe429c61539c232140e62939fa7c69b0a373cc552e4a08 \ - --hash=sha256:770e1ac2e7bdf31439bf1d57a1d34ae37d6151216367e8e3f6cdc275006c8bb0 \ - --hash=sha256:7e3cbc7075a46c411b531ada557c4aef13d034060a70077717124cabc1717e2d \ - --hash=sha256:96d3127f28c0dacf18da7ae009cd48eac532d3dcc718a334b86a3c65f6a5ef5c \ - --hash=sha256:97d086a8f7a302b74c9c2e77bedf954a603b19168af900d4d3a97322e773df63 \ - --hash=sha256:9e8f4ee2c523bbe85124540ffad62a3b62ae481f012e390ef93e0602b6302e5e \ - --hash=sha256:9f7badd0055221a744d76c11440c0856fd2846ed53b6555cf8f0a8893a3e4b03 \ - --hash=sha256:a27996b6d27e275dfb5fe8a34087ba1cacadfd1439e636874ef675faea5149d9 \ - --hash=sha256:ad633c52f4f4e7691daaa2278d6e6ebb2fe4ae7709e610e22c7dd1a1d620cf8b \ - --hash=sha256:b1f5686b142759c5b2bdbeaa0692622c2ebb1f10780eb3c174b85f5607fbcf55 \ - --hash=sha256:b77556349746fb90416a48bd114cd7323f7e2559a4b263dada935f9b406ba59b \ - --hash=sha256:be43d1ece4f2cf4d430996689d89a1a15aeb3a8da8262527e5ced5aee27e89c3 \ - --hash=sha256:d287121eeaa562b9ab3d4c52fa77c793dfedd127049273eb882a05d3d67a8ce8 \ - --hash=sha256:d57843b9792994f9e73b91667da6452a4f2d7caaa2499598783eb972c4b6eb93 \ - --hash=sha256:dc5342d1d09466f9e76e3979551f9205a01ff0ea78b02d2d889171e8c3c4fb9c \ - --hash=sha256:de1ee7ec2eb326b7be5e2c4ce20d472c5ef1a6eb838d126d1d26779ff5486e49 \ - --hash=sha256:de8be39809c8ddeeee26a4b876a6463529cd487a60d1393eb2a93e9bcd44a8f5 \ - --hash=sha256:e04de873607fb960e71953c164c83e8e5d9291ce0d69e688e54947b254b04902 \ - --hash=sha256:eaf42c4bd323b8fd01f1cd900cca2d09232155f9b8f0b9bcd0be66763588ce64 \ - --hash=sha256:eb0850e3e3782f57457feed297e220bb20c3e8fd7550d7a6b6bb96112bd9b6fe \ - --hash=sha256:f1f38adc47d36af392475cd4aaae0f35652fdc9e8364bf155810fe1be591336f - # via - # feast (setup.py) +pyodbc==5.3.0 \ + --hash=sha256:01166162149adf2b8a6dc21a212718f205cabbbdff4047dc0c415af3fd85867e \ + --hash=sha256:0263323fc47082c2bf02562f44149446bbbfe91450d271e44bffec0c3143bfb1 \ + --hash=sha256:08b2439500e212625471d32f8fde418075a5ddec556e095e5a4ba56d61df2dc6 \ + --hash=sha256:0df7ff47fab91ea05548095b00e5eb87ed88ddf4648c58c67b4db95ea4913e23 \ + --hash=sha256:101313a21d2654df856a60e4a13763e4d9f6c5d3fd974bcf3fc6b4e86d1bbe8e \ + --hash=sha256:13656184faa3f2d5c6f19b701b8f247342ed581484f58bf39af7315c054e69db \ + --hash=sha256:1629af4706e9228d79dabb4863c11cceb22a6dab90700db0ef449074f0150c0d \ + --hash=sha256:197bb6ddafe356a916b8ee1b8752009057fce58e216e887e2174b24c7ab99269 \ + --hash=sha256:2035c7dfb71677cd5be64d3a3eb0779560279f0a8dc6e33673499498caa88937 \ + --hash=sha256:25b6766e56748eb1fc1d567d863e06cbb7b7c749a41dfed85db0031e696fa39a \ + --hash=sha256:25c4cfb2c08e77bc6e82f666d7acd52f0e52a0401b1876e60f03c73c3b8aedc0 \ + --hash=sha256:2eb7151ed0a1959cae65b6ac0454f5c8bbcd2d8bafeae66483c09d58b0c7a7fc \ + --hash=sha256:2fe0e063d8fb66efd0ac6dc39236c4de1a45f17c33eaded0d553d21c199f4d05 \ + --hash=sha256:349a9abae62a968b98f6bbd23d2825151f8d9de50b3a8f5f3271b48958fdb672 \ + --hash=sha256:363311bd40320b4a61454bebf7c38b243cd67c762ed0f8a5219de3ec90c96353 \ + --hash=sha256:3cc472c8ae2feea5b4512e23b56e2b093d64f7cbc4b970af51da488429ff7818 \ + --hash=sha256:3f1bdb3ce6480a17afaaef4b5242b356d4997a872f39e96f015cabef00613797 \ + --hash=sha256:452e7911a35ee12a56b111ac5b596d6ed865b83fcde8427127913df53132759e \ + --hash=sha256:46185a1a7f409761716c71de7b95e7bbb004390c650d00b0b170193e3d6224bb \ + --hash=sha256:46869b9a6555ff003ed1d8ebad6708423adf2a5c88e1a578b9f029fb1435186e \ + --hash=sha256:58635a1cc859d5af3f878c85910e5d7228fe5c406d4571bffcdd281375a54b39 \ + --hash=sha256:5cbe4d753723c8a8f65020b7a259183ef5f14307587165ce37e8c7e251951852 \ + --hash=sha256:5ceaed87ba2ea848c11223f66f629ef121f6ebe621f605cde9cfdee4fd9f4b68 \ + --hash=sha256:5dd3d5e469f89a3112cf8b0658c43108a4712fad65e576071e4dd44d2bd763c7 \ + --hash=sha256:5ebf6b5d989395efe722b02b010cb9815698a4d681921bf5db1c0e1195ac1bde \ + --hash=sha256:6132554ffbd7910524d643f13ce17f4a72f3a6824b0adef4e9a7f66efac96350 \ + --hash=sha256:6682cdec78f1302d0c559422c8e00991668e039ed63dece8bf99ef62173376a5 \ + --hash=sha256:676031723aac7dcbbd2813bddda0e8abf171b20ec218ab8dfb21d64a193430ea \ + --hash=sha256:705903acf6f43c44fc64e764578d9a88649eb21bf7418d78677a9d2e337f56f2 \ + --hash=sha256:729c535341bb09c476f219d6f7ab194bcb683c4a0a368010f1cb821a35136f05 \ + --hash=sha256:74528fe148980d0c735c0ebb4a4dc74643ac4574337c43c1006ac4d09593f92d \ + --hash=sha256:754d052030d00c3ac38da09ceb9f3e240e8dd1c11da8906f482d5419c65b9ef5 \ + --hash=sha256:7713c740a10f33df3cb08f49a023b7e1e25de0c7c99650876bbe717bc95ee780 \ + --hash=sha256:7e9ab0b91de28a5ab838ac4db0253d7cc8ce2452efe4ad92ee6a57b922bf0c24 \ + --hash=sha256:8339d3094858893c1a68ee1af93efc4dff18b8b65de54d99104b99af6306320d \ + --hash=sha256:8aa396c6d6af52ccd51b8c8a5bffbb46fd44e52ce07ea4272c1d28e5e5b12722 \ + --hash=sha256:9b987a25a384f31e373903005554230f5a6d59af78bce62954386736a902a4b3 \ + --hash=sha256:9cd3f0a9796b3e1170a9fa168c7e7ca81879142f30e20f46663b882db139b7d2 \ + --hash=sha256:a48d731432abaee5256ed6a19a3e1528b8881f9cb25cb9cf72d8318146ea991b \ + --hash=sha256:ac23feb7ddaa729f6b840639e92f83ff0ccaa7072801d944f1332cd5f5b05f47 \ + --hash=sha256:af4d8c9842fc4a6360c31c35508d6594d5a3b39922f61b282c2b4c9d9da99514 \ + --hash=sha256:afe7c4ac555a8d10a36234788fc6cfc22a86ce37fc5ba88a1f75b3e6696665dc \ + --hash=sha256:b180bc5e49b74fd40a24ef5b0fe143d0c234ac1506febe810d7434bf47cb925b \ + --hash=sha256:b35b9983ad300e5aea82b8d1661fc9d3afe5868de527ee6bd252dd550e61ecd6 \ + --hash=sha256:bc834567c2990584b9726cba365834d039380c9dbbcef3030ddeb00c6541b943 \ + --hash=sha256:bfeb3e34795d53b7d37e66dd54891d4f9c13a3889a8f5fe9640e56a82d770955 \ + --hash=sha256:c25dc9c41f61573bdcf61a3408c34b65e4c0f821b8f861ca7531b1353b389804 \ + --hash=sha256:c2eb0b08e24fe5c40c7ebe9240c5d3bd2f18cd5617229acee4b0a0484dc226f2 \ + --hash=sha256:c5c30c5cd40b751f77bbc73edd32c4498630939bcd4e72ee7e6c9a4b982cc5ca \ + --hash=sha256:c67e7f2ce649155ea89beb54d3b42d83770488f025cf3b6f39ca82e9c598a02e \ + --hash=sha256:c68d9c225a97aedafb7fff1c0e1bfe293093f77da19eaf200d0e988fa2718d16 \ + --hash=sha256:c6ccb5315ec9e081f5cbd66f36acbc820ad172b8fa3736cf7f993cdf69bd8a96 \ + --hash=sha256:c79df54bbc25bce9f2d87094e7b39089c28428df5443d1902b0cc5f43fd2da6f \ + --hash=sha256:cf18797a12e70474e1b7f5027deeeccea816372497e3ff2d46b15bec2d18a0cc \ + --hash=sha256:d255f6b117d05cfc046a5201fdf39535264045352ea536c35777cf66d321fbb8 \ + --hash=sha256:d32c3259762bef440707098010035bbc83d1c73d81a434018ab8c688158bd3bb \ + --hash=sha256:d89a7f2e24227150c13be8164774b7e1f9678321a4248f1356a465b9cc17d31e \ + --hash=sha256:e3c39de3005fff3ae79246f952720d44affc6756b4b85398da4c5ea76bf8f506 \ + --hash=sha256:e981db84fee4cebec67f41bd266e1e7926665f1b99c3f8f4ea73cd7f7666e381 \ + --hash=sha256:ebc3be93f61ea0553db88589e683ace12bf975baa954af4834ab89f5ee7bf8ae \ + --hash=sha256:f1ad0e93612a6201621853fc661209d82ff2a35892b7d590106fe8f97d9f1f2a \ + --hash=sha256:f927b440c38ade1668f0da64047ffd20ec34e32d817f9a60d07553301324b364 \ + --hash=sha256:fc5ac4f2165f7088e74ecec5413b5c304247949f9702c8853b0e43023b4187e8 \ + --hash=sha256:fe77eb9dcca5fc1300c9121f81040cc9011d28cff383e2c35416e9ec06d4bc95 + # via + # feast (pyproject.toml) # ibis-framework -pyopenssl==25.1.0 \ - --hash=sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab \ - --hash=sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b +pyopenssl==26.2.0 \ + --hash=sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70 \ + --hash=sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387 # via snowflake-connector-python -pyparsing==3.2.5 \ - --hash=sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6 \ - --hash=sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e - # via great-expectations +pyparsing==3.3.2 \ + --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ + --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc + # via + # great-expectations + # matplotlib + # pyiceberg pypdfium2==4.30.0 \ --hash=sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e \ --hash=sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29 \ @@ -4231,14 +4937,101 @@ pyproject-hooks==1.2.0 \ # via # build # pip-tools -pyspark==4.0.1 \ - --hash=sha256:9d1f22d994f60369228397e3479003ffe2dd736ba79165003246ff7bd48e2c73 - # via feast (setup.py) +pyroaring==1.1.0 \ + --hash=sha256:01212da3752d6486adcca98c9d353f8fb8e36513e05062cdd0feebc4211dbe70 \ + --hash=sha256:0183b26c6e25a14e32b2aed7095a19491d6c835096032b33bc00b4ae995495bf \ + --hash=sha256:043dbbaa905f7288c515ac06a96b67a3763f35e9ae06f0c0278c0d9964d16760 \ + --hash=sha256:0dfb6cf50fd8898179e460e699a6b8326ca508c627d083f7bf62f769fe1717d5 \ + --hash=sha256:0f1d76ef29034017eb2cceebd5fa0504d6ced218ce6432f99da5adecbe038269 \ + --hash=sha256:100585f438b293112e2c52e45a442835837c8a0267dd1e513bafec35628f8ecb \ + --hash=sha256:13660386ea8905ee4d42c21a6275463e2dc7d31e0b5d65eec210aa7043ad96f4 \ + --hash=sha256:19c9bb8b32f17fe63e54e78f88a7a67fe2dca352bf79d7d3b43f0a901db5ea3b \ + --hash=sha256:19d0c81c865d63791fe20e5b38733b66f4f962e677ae7e8b3d3c4947ac6e752f \ + --hash=sha256:1fc112b9a9890f89cc645a16604783ed7fa25299f149b0ef7b45a5e2e3c1f31f \ + --hash=sha256:212f471da113eba24a0fedbd1ef078bfed34b792e8f48dba3f7301f1bca2678a \ + --hash=sha256:25d65257258f7a3df8b872ddfe18f3681f20b3bc2a3093f08fc80c66338051f4 \ + --hash=sha256:370d191b0d1b32bbd99452ef5f0485f22fcc4bf7404d33b821d0ce2459951152 \ + --hash=sha256:381eda673442c389993f8b0db2dbf5d02ea8ea9aac6ba736f64cc1ffb6c96885 \ + --hash=sha256:39eff7dd06c163c22d0a9f9fd72d27e671457bea8cdb71215382a10512539e1d \ + --hash=sha256:3beb40eb1220d1ce4fb3661bb019e9a21857e5bb294fe8c1c5016aeb6e82318c \ + --hash=sha256:3d04c3eb039cf2295e4d375ef81fd6e3cae7cefe3b5c457ccaf78d7d6f963a5d \ + --hash=sha256:44a9f9719ed18e86627286f90057f9b7e22f6ba1d952c9793f9600b5c14e8680 \ + --hash=sha256:462b0952277d3100a90ae890ae641d3fb3561b10cfea542e02468f0bef7700a5 \ + --hash=sha256:499f0a5d03d64e7b846ee749e87721fb8516da7233baafb24ab69aa63bcf080b \ + --hash=sha256:4a2c7ecd74e7b3ded54ad771a2b7c0aa08aa98f78e43500fa970d68d684bef0c \ + --hash=sha256:4ae81a0353af50d6885720b9119783b8be7bcd079e727ea1c4fca3f452fe95ba \ + --hash=sha256:4c443e9f942b6089efe8c9b264576e9d116f90be28a315679375bba2d8a915d6 \ + --hash=sha256:51dd2490a64ad4ed53c4fb58ef1ee3f84f6cbd97cdb47abd9065c9f714ab72ef \ + --hash=sha256:51ebe5e6f48e3dc9df91a4cb62137ef72e1469acd6f37479abd9991f6d945cc9 \ + --hash=sha256:532ae6bb1d3431d9956ef07589dd5c8dd918301a83d937c7dc6e511b1364d76a \ + --hash=sha256:532e53191d8dd29dedfc5202cbb45632f7df751b207a7f6d6860fb7067c7fe11 \ + --hash=sha256:53acecba8f898e96b84d4139356e30719c70358177e270055901d3ec1cb0e34c \ + --hash=sha256:5599b691e5e993f7fdd9880429d1c485eae03dc392ba5b2dbe2eef0434bea1a5 \ + --hash=sha256:55c9d0b708a65308eaa95c53d9de56a2b4425812395f6e57a4d9ad8efb081bf9 \ + --hash=sha256:562fa04bbfd41144d1276ed79505007557c161371450d68a1d71fc83dc01d083 \ + --hash=sha256:56a67794188275f8897a8f1fa64d6313c48241bebbdef38833063e7281b29ef8 \ + --hash=sha256:591e2ed4d60443dafd9075c1f72e9aaf359ccf5120e32a8c340c2b2ae3da45e7 \ + --hash=sha256:5e337f8c5b3c2e0c27da83fc2cb702684a47eee907a960cfee964fcb5344515b \ + --hash=sha256:5eb031237e9d39cbdfc9276facacdd88e27aefb58940bd8b56b878dfd38d6022 \ + --hash=sha256:61a8eabee99104ca197b6e7cce05dc4f27f503be52881800cd370eb5a5152d3f \ + --hash=sha256:650db21c10f42ff2b09ef02c10a779a3d59d0c7512552f3844738b30adbcb8a5 \ + --hash=sha256:66aa6a321fbc598f26d5e66050a7f145c2253f3fe5737b589841ff0cbe5cb177 \ + --hash=sha256:67650460c65bdd7b4f5078d9c955aa38f64627d02cb48f9cfb24eae84bca2aba \ + --hash=sha256:6a1d4c59d5b23c01d62f86d57ceefd0c0977de0425aafa7069f2d70563fed3b8 \ + --hash=sha256:71fec09bd42f8c33ac3b762cd00c5db842eb583ffd0e361739ce1c17ad078a6a \ + --hash=sha256:72f68a16b00b35481d9b3bfe897ecd8a1f7da69efd92ba5b17347ca11c21cb0d \ + --hash=sha256:731a7a9e050758986d5757eea10f9ccb08f9c3ef514ce0335f4a90e126f81131 \ + --hash=sha256:7caf95de39ce869ea0978068521cf6faa7350574fd1734ad6c63e5ed8cd06baa \ + --hash=sha256:81ebbc0c880c8a10f13118632e5c0d59159ceada8b651bba18f2e6dc70efdeda \ + --hash=sha256:8376c62e60a49e8fe51ffce569f74beed6e53fb58e5d6739f954c8c51ec0bec3 \ + --hash=sha256:83815b3a4cb9c3b17096c70833373610d5c93e89c134548cf43436c90326bfb3 \ + --hash=sha256:8a5fbcb86e44f1c0c9c052917eee67a04cbac9de7392fb4bc77c140ff4a7e471 \ + --hash=sha256:8cff06d18c9a30f8547a92757078aa345db1ba5b22e3082a05f64e50b384e27a \ + --hash=sha256:8f1f56004e8f1c1489bf279c25f1fa4764252cd9af5fb35675774268a4a615ba \ + --hash=sha256:8f5194c0544b08e56e108dad096c468c67f8ac60eec42763aed36ccbd3565346 \ + --hash=sha256:90ab2f00c09eed5bd986a80c8641e2dc10e7aca1a2d892d89a44b396e39c08ea \ + --hash=sha256:92643c9dd303de8960c3dbed93a28b8d87da5ed0a7776568979f379d7bc8a885 \ + --hash=sha256:974fb6fa4c4a682c633c4d7fa6087ccd717804726f9db1f771f1c2ea4f3990b7 \ + --hash=sha256:986efb3aec7655d69c14db2309a2072dbf181bdb906091fede83ad18e316cdaf \ + --hash=sha256:9882a204178cc8c915e0ce30abb4bdd1668e383c571b06649d5ed272d9625877 \ + --hash=sha256:99c42fe1449acfbf130da65e66b4d5b2726aba4497be359bae7672e38a15fc62 \ + --hash=sha256:9cf8608c9d6cb6bff9c624744f7a2ba8ab12276f097a07930490fe0e2219e9be \ + --hash=sha256:9d9f196007f0b15ea19c21732faacaea83cbf5946b6db4949b3b98cf871c93f0 \ + --hash=sha256:9dc61b7e38aca2ef384ac22b87a9c2b1e8769a1e15d916f93ea6a9a4551fc1d7 \ + --hash=sha256:a23cd023985b5f2ba23e84e1fadaeacde3c8a59e1d2adb3fe782e99db1e22387 \ + --hash=sha256:a31276323fdd355505883162f1dca4d7acc3d6ec2159c85ddef863f1fe9e8f05 \ + --hash=sha256:a6343249ce9401dd90c3b934fd9a4a618b6fc455f27a9aa11d198eebd4743137 \ + --hash=sha256:a98d1147fe1d3195053b67b474bccc0be5021506765d27f613a943c8c99f9e4c \ + --hash=sha256:abc0f0ce22464864fea208315d25e999e45cb5ee646ac1ca11d314a6a51dbe4a \ + --hash=sha256:b490f2d22df30affbfdcbe4f7896f321edb72a8dc0cbe5f38adec3de5b947c25 \ + --hash=sha256:b8ac1bc26223befbca986551521f37f4c1670dfe26fccb2f0fc2775e75be99c1 \ + --hash=sha256:c14fc6bd65e5624f76b90297b081222261476978f795f60d48745553617ddceb \ + --hash=sha256:c967ddb5e137c604ec94c7120d2f420ac9a9ce27b0e44987c7134b0eb5fe7a60 \ + --hash=sha256:c9f30ca28b991a920b446ed3ee19c7ecafcc49c46db592abf89cf239a7bb45f4 \ + --hash=sha256:cc2d113bf6236aa873dc710b38a856808c6054efd3a1a403c5acfceaf87138f1 \ + --hash=sha256:cc998e81748b72659802a7b434b39292308a5f4ab8dd4bf4739d65de1d4ed2bf \ + --hash=sha256:cdf2b8ab0effc335a9e61cf2a1e14c2295b999a656569b35f35ea3dc55a8931f \ + --hash=sha256:d2706a89242a347be20805147d58a38f4f4d8f6846228c4ee8dfd3587113719c \ + --hash=sha256:d9127feb5356ba3a92bdffa04c1bf6bcbc8d436369f78badf441018c3029dd63 \ + --hash=sha256:d92a0f4c7e6bb7deeafac68c79c92ef9340895fe825cf1a31078443753ab6756 \ + --hash=sha256:e8b3bfad0ae3ef0e67b40c193863dce8b7d79de545dadbe53c19acc3ace38f66 \ + --hash=sha256:e9864e19109e76111befc75d799d334e7365eb4189607aa734053c12e7840fa5 \ + --hash=sha256:e9c2b9aa8decdcf40ed8f4c887092c20a272f8c32215c3fee65e9db92ecf418e \ + --hash=sha256:e9caf67c0c05d27bab0d88cf193e73023f624c6714f1ff807c7d4b135b19d36a \ + --hash=sha256:eead129046822cb0fd47c78740b81bdaffd0515c0bb0306a2318acf0f0540b58 \ + --hash=sha256:f02e4021397ae02a139defdc6813b9942ab163de90affddd4ce4efbac299f619 \ + --hash=sha256:f742ebdd879063d2a92f4d57708886c720e854bc84ca240733f220936be0cb7e \ + --hash=sha256:fdf484d26016e0c016f23f2b635d2899daec034565fdcc062ed6b10f3b26a3f4 \ + --hash=sha256:fe0b9f0baf190663df37571cbb8a77370e7bd3a66068276d48f813c53ceba530 + # via pyiceberg +pyspark==4.2.0 \ + --hash=sha256:5ad689d53570ee1674193fd4f9bda065f0db3be9363a27d2a3406cc457b70b61 + # via feast (pyproject.toml) pytest==7.4.4 \ --hash=sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280 \ --hash=sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8 # via - # feast (setup.py) + # feast (pyproject.toml) # pytest-asyncio # pytest-benchmark # pytest-cov @@ -4251,203 +5044,222 @@ pytest==7.4.4 \ pytest-asyncio==0.23.8 \ --hash=sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2 \ --hash=sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-benchmark==3.4.1 \ --hash=sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809 \ --hash=sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47 - # via feast (setup.py) -pytest-cov==7.0.0 \ - --hash=sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1 \ - --hash=sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861 - # via feast (setup.py) + # via feast (pyproject.toml) +pytest-cov==7.1.0 \ + --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ + --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 + # via feast (pyproject.toml) pytest-env==1.1.3 \ --hash=sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc \ --hash=sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b - # via feast (setup.py) + # via feast (pyproject.toml) pytest-lazy-fixture==0.6.3 \ --hash=sha256:0e7d0c7f74ba33e6e80905e9bfd81f9d15ef9a790de97993e34213deb5ad10ac \ --hash=sha256:e0b379f38299ff27a653f03eaa69b08a6fd4484e46fd1c9907d984b9f9daeda6 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-mock==1.10.4 \ --hash=sha256:43ce4e9dd5074993e7c021bb1c22cbb5363e612a2b5a76bc6d956775b10758b7 \ --hash=sha256:5bf5771b1db93beac965a7347dc81c675ec4090cb841e49d9d34637a25c30568 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-ordering==0.6 \ --hash=sha256:27fba3fc265f5d0f8597e7557885662c1bdc1969497cd58aff6ed21c3b617de2 \ --hash=sha256:3f314a178dbeb6777509548727dc69edf22d6d9a2867bf2d310ab85c403380b6 \ --hash=sha256:561ad653626bb171da78e682f6d39ac33bb13b3e272d406cd555adb6b006bda6 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-timeout==1.4.2 \ --hash=sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76 \ --hash=sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063 - # via feast (setup.py) + # via feast (pyproject.toml) pytest-xdist==3.8.0 \ --hash=sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88 \ --hash=sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1 - # via feast (setup.py) -python-bidi==0.6.6 \ - --hash=sha256:00081439e969c9d9d2ede8eccef4e91397f601931c4f02864edccb760c8f1db5 \ - --hash=sha256:00622f54a80826a918b22a2d6d5481bb3f669147e17bac85c81136b6ffbe7c06 \ - --hash=sha256:02255a04e26520b19081f7d378881b39050f5893e2fb4d65da81b849f58f4f76 \ - --hash=sha256:0781c3c63b4bc3b37273de2076cb9b875436ae19be0ff04752914d02a4375790 \ - --hash=sha256:07c9f000671b187319bacebb9e98d8b75005ccd16aa41b9d4411e66813c467bb \ - --hash=sha256:07db4c7da502593bd6e39c07b3a38733704070de0cbf92a7b7277b7be8867dd9 \ - --hash=sha256:09d4da6b5851d0df01d7313a11d22f308fdfb0e12461f7262e0f55c521ccc0f1 \ - --hash=sha256:0c298868017614d6b7e0e31293775ebe6622e87009d95e1ecd0abdc1fa5228a2 \ - --hash=sha256:0eb12b724cc99853e0e0425b54c1c2219492486afaca106c827204b4189504db \ - --hash=sha256:125a815f2b20313a2f6d331aa84abdd07de7d270985b056e6729390a4cda90df \ - --hash=sha256:166060a31c10aa3ffadd52cf10a3c9c2b8d78d844e0f2c5801e2ed511d3ec316 \ - --hash=sha256:183fee39bd2de787f632376bd5ba0d5f1daf6a09d3ebfaa211df25d62223e531 \ - --hash=sha256:1d627f8cfeba70fe4e0ec27b35615c938a483cbef2d9eb7e1e42400d2196019e \ - --hash=sha256:207b0a7082ec38045910d37700a0dd73c10d4ffccb22a4fd0391d7e9ce241672 \ - --hash=sha256:2150ac84f7b15f00f8cd9e29fee7edb4639b7ed2cd9e3d23e2dfd83098f719b7 \ - --hash=sha256:25fa21b46dc80ac7099d2dee424b634eb1f76b2308d518e505a626c55cdbf7b1 \ - --hash=sha256:27cf629a0ef983a25cfd62c6238ee1e742e35552409d5c1b43f6d22945adc4c2 \ - --hash=sha256:2d139bab64962731b5288edb1b6db76060c5a5183187efa590499951cd230b02 \ - --hash=sha256:33bd0ba5eedf18315a1475ac0f215b5134e48011b7320aedc2fb97df31d4e5bf \ - --hash=sha256:3428331e7ce0d58c15b5a57e18a43a12e28f8733086066e6fd75b0ded80e1cae \ - --hash=sha256:3564e574db1a0b3826ed6e646dc7206602189c31194d8da412007477ce653174 \ - --hash=sha256:35adfb9fed3e72b9043a5c00b6ab69e4b33d53d2d8f8b9f60d4df700f77bc2c0 \ - --hash=sha256:39eed023add8c53684f1de96cb72b4309cc4d412745f59b5d0dab48e6b88317b \ - --hash=sha256:3e17441d31a8665a44f5f42dba7646bbcd3c51ae6657dd019f6a7bb12618b12f \ - --hash=sha256:4142467ec0caa063aca894ca8f1e8a4d9ca6834093c06b0ad5e7aa98dc801079 \ - --hash=sha256:41fde9b4bb45c0e1b3283599e7539c82624ef8a8d3115da76b06160d923aab09 \ - --hash=sha256:43a0409570c618d93706dc875b1d33b4adfe67144f6f2ebeb32d85d8bbdb85ed \ - --hash=sha256:471c1a5fcdbb3de47377d74a7f1017216d9464e5428ca4e66f863e49dca73393 \ - --hash=sha256:485f2ee109e7aa73efc165b90a6d90da52546801413540c08b7133fe729d5e0a \ - --hash=sha256:490f8fe09ed423bfe00531f215e3b87e6000b8170408a0ead6ea5626f644b1d1 \ - --hash=sha256:493a844891e23264411b01df58ba77d5dbb0045da3787f4195f50a56bfb847d9 \ - --hash=sha256:4bb186c8da4bdc953893504bba93f41d5b412fd767ba5661ff606f22950ec609 \ - --hash=sha256:4eb3f28ca5e2f7238eaf67126c7634ec35603cbfbbe9b9b340ffee4a3314455f \ - --hash=sha256:4ecfd1d0f6d2927eb2114b55a63b298766b85fc9f0c9aaacb4e8df3e0468538a \ - --hash=sha256:4ff1eba0ff87e04bd35d7e164203ad6e5ce19f0bac0bdf673134c0b78d919608 \ - --hash=sha256:53122c3492fe3df871eb682c17eb848e24aa702946622ab78141c7027775519f \ - --hash=sha256:534bc7c84159b6e4b777f5fb9122902d6e19223c4242f5b94417de1afcfe2fd9 \ - --hash=sha256:5351efb4e86281eb26c420066fade935cd670c0c0960edc323b80d0b94a0bc19 \ - --hash=sha256:53d7d3a550d176df99dd0bb0cc2da16b40634f11c8b9f5715777441d679c0a62 \ - --hash=sha256:5506ba56380140b3cb3504029de014d21eb8874c5e081d88495f8775f6ed90bc \ - --hash=sha256:57c0ca449a116c4f804422111b3345281c4e69c733c4556fa216644ec9907078 \ - --hash=sha256:589c5b24a8c4b5e07a1e97654020734bf16ed01a4353911ab663a37aaf1c281d \ - --hash=sha256:5c9f798dd49b24bb1a9d90f065ef25c7bffa94c04c554f1fc02d0aea0a9b10b0 \ - --hash=sha256:61cf12f6b7d0b9bb37838a5f045e6acbd91e838b57f0369c55319bb3969ffa4d \ - --hash=sha256:6255ad385bb90aa39f8340967eef35657e52f8ed011773d37113cafa0ed5eefd \ - --hash=sha256:63f7a9eaec31078e7611ab958b6e18e796c05b63ca50c1f7298311dc1e15ac3e \ - --hash=sha256:646e83862dadfee00b75c93a930015e9f1cb924b26c34319a75aef65fcb3ddfa \ - --hash=sha256:686642a52acdeffb1d9a593a284d07b175c63877c596fa3ccceeb2649ced1dd8 \ - --hash=sha256:691822fac1d6f3caf12e667dd8b41956485c78b211032747c5f97822ba208726 \ - --hash=sha256:69c02316a4f72a168ea6f66b90d845086e2f2d2de6b08eb32c576db36582177c \ - --hash=sha256:6a4f4c664b2594d2d6be6a31c9254e784d6d5c1b17edfdccb5f0fac317a1cd5e \ - --hash=sha256:6c84d901fad5fe3b58a329c0b4a5c9d93a2d5430d150ad41f0e1165fc75ff439 \ - --hash=sha256:6cc626d2f77cac470b3167a28d4975744f3d99f5eaf8f5c2048ac9c0b9cba9dc \ - --hash=sha256:6dfa55611022f95058bb7deb2ac20755ae8abbe1104f87515f561e4a56944ba1 \ - --hash=sha256:76a1cd320993ba3e91a567e97f057a03f2c6b493096b3fff8b5630f51a38e7eb \ - --hash=sha256:7906229befa0cea2fe0278a934a27f657b68ce07a2606b1244f814a38b4ab42a \ - --hash=sha256:7d395e537a34d59e776fcdf50a50786d1a82084849d55cf644f4969ef8156643 \ - --hash=sha256:804c74d070f4e85c6976e55cdbb3f4ead5ec5d7ea0cfad8f18f5464be5174ec9 \ - --hash=sha256:825d15e547a9a2da5501966db672d6c8a5a063c041b2741ba32cc9775694b0ff \ - --hash=sha256:82c7f6bb3dfc4f61aecb2290f1ea24bb2450a5cbc94ee8abe5d6278b67859e0b \ - --hash=sha256:82e0befbc1078a964c6b6f2f7a616ae8015b52fdcd2f03979abf0fb1f2f18b48 \ - --hash=sha256:8706addd827840c2c3b3a9963060d9b979b43801cc9be982efa9644facd3ed26 \ - --hash=sha256:87a5489189b0a852da0129df77f0cc8e874b7b1ab1f968a209d340477906f076 \ - --hash=sha256:8b5f648ee8e9f4ac0400f71e671934b39837d7031496e0edde867a303344d758 \ - --hash=sha256:91a8cb8feac5d0042e2897042fe7bbbeab5dea1ab785f4b7d0c0bbbf6bc7aefd \ - --hash=sha256:91c12d58cec15385817f8b2c7c56de8e37523f05926f2de0e59199d3e50e1516 \ - --hash=sha256:92eb89f9d8aa0c877cb49fc6356c7f5566e819ea29306992e26be59a5ce468d7 \ - --hash=sha256:965e6f2182e7b9352f2d79221f6c49502a307a9778d7d87d82dc36bb1ffecbab \ - --hash=sha256:994534e47260d712c3b3291a6ab55b46cdbfd78a879ef95d14b27bceebfd4049 \ - --hash=sha256:9a9de76229ac22cb6bd40b56a8f7f0c42cbdff985dbd14b65bac955acf070594 \ - --hash=sha256:a138a7607b459414431a5cdcf5834624d6f87911a8863b51dd363a1e2e5744ab \ - --hash=sha256:a2a49b506ed21f762ebf332de6de689bc4912e24dcc3b85f120b34e5f01e541a \ - --hash=sha256:a525bcb77b8edbfdcf8b199dbed24556e6d1436af8f5fa392f6cdc93ed79b4af \ - --hash=sha256:a6ac2a3ec5ccc3736e29bb201f27bd33707bfde774d3d222826aa181552590b2 \ - --hash=sha256:ada1aecd32773c61b16f7c9f74d9ec1b57ea433e2083e08ca387c5cd4b0ceaed \ - --hash=sha256:af828457e46b31542569b4391014e6645023f6144de1dabf9fce7e9683235c25 \ - --hash=sha256:b144a1b8766fa6a536cc0feb6fdd29d91af7a82a0c09d89db5fc0b79d5678d7d \ - --hash=sha256:b271cd05cb40f47eb4600de79a8e47f8579d81ce35f5650b39b7860d018c3ece \ - --hash=sha256:b31f5562839e7ecea881ba337f9d39716e2e0e6b3ba395e824620ee5060050ff \ - --hash=sha256:b53b8b061b67908b5b436abede8c450c8d2fa965cb713d541688f552b4cfa3d3 \ - --hash=sha256:b65b4105998436405a3e6bca60cbf9714f6a08099b16c0cf4752a4a3a70eb45b \ - --hash=sha256:b8a83f28c104ef3b86ad60219d885b31728eb40c644f414f505068a6ecba3575 \ - --hash=sha256:b9498ead7f09eee272ff9c45900a8dcdc50a9558e126420a71d15774cc98bb44 \ - --hash=sha256:bbbcb28474b71e3ad05d8bd483348efe41fb7dfef6bd3046f3072baa0954d746 \ - --hash=sha256:bd5b3aa43d5222f1deef9894356a42f2443486501405977cda3aad0f23e20f9d \ - --hash=sha256:c07e4d6d8c8f574aa135436207a37bba522443a8490b0ba720b54d343dfde1a7 \ - --hash=sha256:c0e715b500b09cefccaddb7087978dcd755443b9620aa1cc7b441824253cf2b8 \ - --hash=sha256:c48a755ca8ba3f2b242d6795d4a60e83ca580cc4fa270a3aaa8af05d93b7ba7f \ - --hash=sha256:c4c0255940e6ff98fb05f9d5de3ffcaab7b60d821d4ca072b50c4f871b036562 \ - --hash=sha256:c4e08753d32d633f5ecb5eb02624272eeffaa6d5c6f4f9ddf012637bcaabfc0a \ - --hash=sha256:d1dcd7a82ae00b86821fce627e310791f56da90924f15877cfda844e340679de \ - --hash=sha256:d941a6a8a7159982d904982cfe0feb0a794913c5592d8137ccae0d518b2575e4 \ - --hash=sha256:da4949496e563b51f53ff34aad5a9f4c3aaf06f4180cf3bcb42bec649486c8f1 \ - --hash=sha256:dc8b0566cef5277f127a80e7546b52393050e5a572f08a352ca220e3f94807cf \ - --hash=sha256:de020488c334c31916ee7526c1a867bf632516c1c2a0420d14d10b79f00761c7 \ - --hash=sha256:e2f227ee564e0241e57269043bdfa13025d08d0919b349f5c686e8cfc0540dbf \ - --hash=sha256:e4a6251e212f828bb10ea69e0aa6b92b54f00bf56526b490fe890ca5f4333ec1 \ - --hash=sha256:e7e36601edda15e67527560b1c00108b0d27831260b6b251cf7c6dd110645c03 \ - --hash=sha256:e7edb0d1baf45c70384e700e10d723a13aabe116e14453cbf099eea4dd763e28 \ - --hash=sha256:e8bf3e396f9ebe8f4f81e92fa4c98c50160d60c58964b89c8ff4ee0c482befaa \ - --hash=sha256:e99e9ae745ba283f0230ac50af3f91657dd0b763778f88e4f0cbbc53b3e45d6e \ - --hash=sha256:edae3dd8e595a40d3cdd6ff8b6d9f3860cd17f674792ea05bba5bf5f1b36e5ab \ - --hash=sha256:f1020fcd3c8f1b93091730e3e16810d3741cbf69c6bacaa9d6a95fb15032848f \ - --hash=sha256:f1d3e139ca3963201994ee7f45d51dce6015166462cffa025daf95508547e503 \ - --hash=sha256:f60afe457a37bd908fdc7b520c07620b1a7cc006e08b6e3e70474025b4f5e5c7 \ - --hash=sha256:fb750d3d5ac028e8afd62d000928a2110dbca012fee68b1a325a38caa03dc50b \ - --hash=sha256:fd9bf9736269ad5cb0d215308fd44e1e02fe591cb9fbb7927d83492358c7ed5f \ - --hash=sha256:fe31aa2d2be1c79300bda36b1a3daf8c2dda963539e0c6eedeb9882fc8c15491 \ - --hash=sha256:fefea733a1acaaf0c0daba8ccd5e161b9419efb62d8f6f4c679c51ef754ee750 + # via feast (pyproject.toml) +python-bidi==0.6.11 \ + --hash=sha256:034090c597af250d699299d7e7f1e83eb016f9e47b3b707bd89ab2bdec77bce0 \ + --hash=sha256:0608bddcc1c53dfa5293499de13ca9935b31aa46d1c722c404a88c703d1a4e47 \ + --hash=sha256:0d339c4ebf3c2501d7569971a22e762fb45d8af7e19a0573afe260d69b35fb0d \ + --hash=sha256:0d496f9fc21b7457e12395e54088ab99776966c663b4cd4a74770c7a6418ab59 \ + --hash=sha256:19bae96ff1ee76b3a7dc962598b69426538e3021460cf85a4017437548ab6947 \ + --hash=sha256:19d13c70b1f7bb5dc69866a11624f3b78123807b4ede18f4f99f656ed86babec \ + --hash=sha256:1a639a2c62f825e72eb42901fd1c69296d09c192ae003195e6cf773f92d6bb42 \ + --hash=sha256:1ad5f712a32be30d28eb96e119e85818747a43cea253de6c3160b464b62a5619 \ + --hash=sha256:1b41cc6bc9ad78a12da5f987da15e931c771f18ceca58f2fe8ed50f253490a97 \ + --hash=sha256:1ffd728f1f7866ff7399906bbc17ef5cf010b90ce56a1e94937b28a1a4cf5a7d \ + --hash=sha256:20ab47a4098577fc9a82816c330c89ff597c31ba69c98bc6a1b6a5737b03de05 \ + --hash=sha256:239a00b2adb5f897d11d7b7a491d759fb9883e71a71cfd90c4147a733b4df4d3 \ + --hash=sha256:2b208fc26ab2c7adfd2c4d84753a5045f664e0bec19ca462af2406132e62f92e \ + --hash=sha256:2d6970b09f5a3102c0aa192f5258c585742ab4ebd94f637a635ad3448ccba567 \ + --hash=sha256:2ff75d1befc335cbe85f834e81554a024f94d9b5d1dd75a5bd99af81a1cb783c \ + --hash=sha256:302f4ca7dabfe447e707d40e98520551181036c15750bdd1e73292ad8b3d8e75 \ + --hash=sha256:30d543b5baf9fca5ef5ec95647aa07c5e38fc7fa0f18be0c61d1d6c0a1032c6c \ + --hash=sha256:337ec315f8b286399e411f5bfc339fc2124db07757409b0595ceb46de087c4cd \ + --hash=sha256:3611d13b53d4c899c4f2a7cd8eb897064e8b5546c3c5d1037dd6209c82858a27 \ + --hash=sha256:36f23f12d9b1c56ed8d82e11f56c6cba7bc3f614ee73374bc7772bb2270e0966 \ + --hash=sha256:380b70d615647646dbe06f7d95dc30b8fdc9b596dbfa1cd3814feb9805c0c8f2 \ + --hash=sha256:397f7f289eba6ce25d99dbea99f873d699bf9aa030074e7fb746d8f93c2fb6f9 \ + --hash=sha256:3bdb64ee0a74951465cd4a761e1029e73806ff43b2fe5be98643e52da5cabb66 \ + --hash=sha256:3da9e536546f7c62c0da595c8f71a096e0b9a80e94cfd0f329b7b200ef81e7d5 \ + --hash=sha256:41c7d1914c1f33954aa2ab0e2c309be5d1d4afc75ce524762eaab4dd825c5ab8 \ + --hash=sha256:43f3e81bdd36f49171b7de6cf471086df503c29555f6f7f035ebe8f8ec1da779 \ + --hash=sha256:490118f4af5a3535923c76be07e937ef1cb4cf84c489b37471aa95455fe923b3 \ + --hash=sha256:493655043ebea6fec0edb76053bdad46f1f7352a759ca4deb733f13b3c09839c \ + --hash=sha256:495a76c881d78ab87b57c5270679c9bc3c1de36d8c6596d5e3a5a1b5f9c57471 \ + --hash=sha256:4ac819cac1abb15486c48af3399a5c726e89f0977a3aff205ac162533186e756 \ + --hash=sha256:4ccf1fe9ecb3b02a1a11b103cd2557e2653d82c141b6e2dccec8177e6af5c4bb \ + --hash=sha256:4d00757ef7bbbf14d8628f9bdb6b0e168d5e7b03fec20da3226624f11bffce89 \ + --hash=sha256:51915502898c45e9cb36636e974aca068fc5cdb9f06b794f49abea5b12f02016 \ + --hash=sha256:519eb90bedb04b5cbba0bfc8062984a5ba054a019d81ada34053dc67397d61cb \ + --hash=sha256:539d99efe02f4981171ed57bbc085094ef780405ca14663550a56c6c3e265c34 \ + --hash=sha256:555cdf9303c40bae1ab512ca427f1f0316a574bc0a48db22eec76ec0fd1213cf \ + --hash=sha256:55f27bd2cdeca96f46448a741d393575fbedfc70e38e09dbb030ed98dba8cf2a \ + --hash=sha256:55fa5b22cc8c220a6daf077570a8e29d3b607bd978bd3dbc04b445d01a17de57 \ + --hash=sha256:565d819fddb2bbc58c42ca5c97d73da7567f181115011e8f04c76b9d08378dcd \ + --hash=sha256:56807e0dde88d5ab96880c9d668bda7bca1df83cd30d20cbff5d6b6e6c1e9276 \ + --hash=sha256:56dfa3c1c13cb89da39a4e8f2f9442e4291de01877511a13f830125d36e9ce5b \ + --hash=sha256:56f27c1edfd15c12c9c348378ccd79166930d720cf316b1181a0a0ade2146253 \ + --hash=sha256:57aa56b1eca5f63ebdd25fe8fad02eab7797fc45ad1efc5eed2a830e2bd2038d \ + --hash=sha256:5dd49c11ae87ee6ced68594faa78b34a89a1a3c43647fa6157765f9726f9daf3 \ + --hash=sha256:5f3e1743b2d43377c4da5d687a03430a27f5769e158a9f75a50b05e7e82f4d21 \ + --hash=sha256:5f9ed312d445a691c0afec995ca4f726abe357d8da500e389f77fb874fa7ef76 \ + --hash=sha256:60150bffbe43bfc1e356c0c2900ba643a02ce09c37bc0c553c4d83e5c3de63ad \ + --hash=sha256:60d9bf6c60c022657637f64897e63dc3f5b1c07cb7f0e1ead6150aff5150c5ab \ + --hash=sha256:619ee3fe03daec8d3ce12239f0c22455676a063b2bcde361caecd788fae5b8d5 \ + --hash=sha256:62a6b700f3d7a2c4d52a5dccca765711a2414e734360b00365e155698a26e461 \ + --hash=sha256:63d9dcc714d549a5118ebc93b7a7c903a0c1feec8e57f5fcacf98d984b76325b \ + --hash=sha256:6623683fe39b9fbf508e3069f17e8e9cab26143f9d9f89c8a8f45424c052df4f \ + --hash=sha256:68c3e570f2908ac39db0b269f5648e257fab5204344b9bf6dae80852ae4cbe1c \ + --hash=sha256:69d1f4ee17644e8aeac93a7238ee2f28d79b0180815441eb511b77e6585aa971 \ + --hash=sha256:6c92d1cad16f9ec2f2a3ae439a0bc3a8e4189ec227987bed03d5b4056d5eb9c5 \ + --hash=sha256:6ca92a4e460f7e25e434a6d7982d94a4765ca242f527995da70abb5a32003b8a \ + --hash=sha256:6d9ef69c108b31f38e1f281a55fdedad7774bc1e952a45c8c14a18e891eee397 \ + --hash=sha256:6dc112e2239f69913273cbb0c050ca816b145b32037d4266c430159c5ddcdab2 \ + --hash=sha256:721697187f4da67dafc26f63488f463fa35ff2de8668d959fda773cccdbef0eb \ + --hash=sha256:73c38c604bfc01647c69ce38e5cf8b4206e2ede91ca3eb8e5d79b7409f17e0b3 \ + --hash=sha256:74457b43db34f984252e915828b5d1a4042a771f44e853a5643506d01562eccb \ + --hash=sha256:7738cfc7ee9fcdff3fef76b40007982ce7704010a51df307c4a51d378f5ff1ba \ + --hash=sha256:777fb44a6f1e91d6adde36e815024c210b0208f35d00762880257b8fb04dc849 \ + --hash=sha256:788c11c84b520ea973992cc95751d56b783ff5857df504c1347d99cacd2fcfe7 \ + --hash=sha256:79df1099a08e53edb678236d4d76d8de4e3901bafc84ce1788b71f9b96547325 \ + --hash=sha256:7c4dc0d1328321aeb08054be654e1d39f171d5aae8dca931c9332860f18f57d7 \ + --hash=sha256:7cb34dd8d22ca1847653c0e1cbba8fd96de09c24b8c83b7805eab270c81e863a \ + --hash=sha256:8013c1e6267a929be98d10c3a617171709c8226bd33c6180ec65e560c35b6df8 \ + --hash=sha256:80cfe97e2d65981be877ae5bd2338c6919a7cc1171fc308c8b8c7c306ba6ffd8 \ + --hash=sha256:83c780f7e4c3dd3f020db75dc425567981e65c6d5571b3c0372203d0df92c834 \ + --hash=sha256:83ee87feb5eafc0442e1db0014dad20d52a2a7a140b6cddc8f7bc65918f0a7b4 \ + --hash=sha256:879c3fba3e7511c7d01020449d970ca7d6a593f20cfc47c014d3d229a38930b2 \ + --hash=sha256:8b6b7fce8f47578be9aebf5a0b6b2d6c157b4e97af7586ecf13bfca5d128deea \ + --hash=sha256:8eb09af209cd660fa9689f6ce9e61e73c8afa4829ae61801deea7f6e32263800 \ + --hash=sha256:8fbb6d222b50324fb9d49b6ff0f8566fa97b907a68c00e6622fcf34463104f4a \ + --hash=sha256:9435acc52438c3c8f5142b9a17a622927618e80a32eed707343bc375cb51cebe \ + --hash=sha256:946b7dbec4e64017680f1a66b3a8534659d889018ac83bd2abf958278e6f62b0 \ + --hash=sha256:94f7f66bd7680d3f2085adaeea91c01b3281a79bb5041db1a51cbe8af36a9d88 \ + --hash=sha256:959335cd3814cb767fb832c5c71cbc838ccd9231a812ef2cb43092a216a91d5b \ + --hash=sha256:969ee7db3e169fcc0b2d2d094826e03cc5798dfd6b3571a340ea883672396cb1 \ + --hash=sha256:9af2b5c26a3eb960699dff040535a86dc2c0f708087b2d63bcfd6452fe9d0664 \ + --hash=sha256:9d18916c97855d68515ca2b3581948b6d864c304cd386210ab5aff5e0fb9be2c \ + --hash=sha256:a1115f3f02eb836b39e0c986a6b9e92c4377a1e5a680409650b02ec6b1a795e6 \ + --hash=sha256:a1b5ee069001bf7f4fff109598a9396caa96bb35e1b906b2c6d1bab9f9b2c4bd \ + --hash=sha256:a4f4a0cd24c09df748bfdc207b089c00e7af19f3151063f4cd74ac658290186b \ + --hash=sha256:a52f7ad9ef9091e81869e5d255e796755ccf542ade14dda17647cb7d7ffe1b9c \ + --hash=sha256:a873e6f8ab28b5a56201d5cdb98f4c7d2ff88e636a58659c5ac271e287001658 \ + --hash=sha256:a9aa2890661b238730e680ccd6eb06f4da625b2dbc1730052dae6f2d88957192 \ + --hash=sha256:ab8af1cc29c3f400a0f781d50f8ab52a8c63adf2fcef16f14641cd97e06a80e0 \ + --hash=sha256:acccb6d90e694684db2d314db2e2b0d3b8949bf1cfaec6d9808a8889f548add7 \ + --hash=sha256:ad5d9b8e8a6c330208eba413db506de58f21dbf88a1f1d5d75ef5f9e0e714adf \ + --hash=sha256:af7711cc6eeeadcb1aae877e0736a68e111c73a29562da6106c5e2fbb4dd83b2 \ + --hash=sha256:b2c759e13ebb81edaac3041697328bb1ca8433b55281723879f6b54e74881240 \ + --hash=sha256:b433840a924c8788f0abbda15d22c71dc636e2078d7d3ba39369cebe4bef74b8 \ + --hash=sha256:bde89739a979d9eb3ac48c4882c8a42dd528a7708b0faa99b90b551588bd5f8d \ + --hash=sha256:c17393753ddb77377f754ba2a88cacfe3056040a8a5dff8d43f9d5e51bbe1edc \ + --hash=sha256:c26cd9d81f820159026b1c99905ca12bf13892e3f6a9303359fef42fe6f39e50 \ + --hash=sha256:c6aca20472d7040bc1f0aadb510cb20d40b3d6caa4c8bf82d075ba2650a41652 \ + --hash=sha256:c6b3c853f99172ef22e5a16c8114cf243e351c8e70f72a894164088c2c99d9cb \ + --hash=sha256:caee7ee3662eab1411b44fef8571b87273cb5235061d7463ebd10e412ac07986 \ + --hash=sha256:cd564b8c583eba2d230d02d0467fd840045f08856b6524555cfff7f32af63c72 \ + --hash=sha256:ce2ef68dce82ae4067cf28910224d77b45eb2c2b3904db6eb670d1cddb8d0eb2 \ + --hash=sha256:cf4e88a6fec81b7155a487cbbea7753a3d9a76dc4d391b4f8958b37227ef2c12 \ + --hash=sha256:d14f2d400c75e07d1154299463a3d4d14aa5565b05088b2d9b314ccd9fd6dc3a \ + --hash=sha256:d52ec8ccdc2fd5c61749d876a9d1eb0ea6543c1676722e1e3fb9d7800852131c \ + --hash=sha256:d5695d87969fed5b3799b8a98848cb04fe2873fded46efe9f3f2f341efd1b829 \ + --hash=sha256:d7291e13496cb74fc1b71f7f1e3628586afefa531102bb4fa7af9c2d543efc3c \ + --hash=sha256:d73a821873c52635321196cfb8d3a231d7917ca284cf2bcd9422f6deb19db7ca \ + --hash=sha256:da51a3a2940478219f19249fcf7cd45e8ddc197982be22efa43c4763e9d2eb57 \ + --hash=sha256:dc69d594682fbedec7d0cc81cd4d5f14fb40b6b365587238fafb91509cfea020 \ + --hash=sha256:dfbb9ba8343a60daf4ced67c11d551dafe9a3c94892c326e4c216fa2e6eca802 \ + --hash=sha256:e142820537e08d567c2c017e26df05f88856a03f0f3948209cfcdc39617df363 \ + --hash=sha256:e4ebc24ac38e50676f65daf7ba6c568789660cb60d6dcf2606d4310dba826721 \ + --hash=sha256:e6c474618a7b6a50c10007f8a9edb50def6d98d297a07a34dc2fb82c344f2b8b \ + --hash=sha256:f0ee4e80dd3b2a46868dcc73e00aa59f99c0e1de54a2ba783faf0fddba2ccce2 \ + --hash=sha256:f237ebb570fd8bbe479b6967374d82b7f0b26f9452c276bdd5f793d83e7062bd \ + --hash=sha256:f400a30573774a1e90c0d50d43c35d9c004afcf53de801bbfd259f84ea80f31c \ + --hash=sha256:f563d20481f7d316adf605bb94d5b7182acecdbc4d431d60473e9b1d526d0210 \ + --hash=sha256:f7a8429d0d65232e314b4f494825c1205abcc3039f42bf7da80424a15b731709 \ + --hash=sha256:f8655ac559dcfce7179b150ebcc207982d4d60e67b3089b2032eed3a0a78c07a \ + --hash=sha256:f99162d6c6c9522c46eb213f1bc932829c2602131676c92f081cb865b8ef6784 \ + --hash=sha256:fa2848c3116d619870d114471fcd4a9f1aa43587a002111d7af1e6582f1b57e9 \ + --hash=sha256:fc3b0a6e2460f68de9ab98f71e3098bb21bb984563417ad104dec7ab08ebcadc \ + --hash=sha256:fccd1808bb427d6a6d34168461bef551faa93ce3dda489fcef9073bcd9b34a5e \ + --hash=sha256:ff675d036823bff05a2e0f8cdb13f5414274ff517d13b0d57c15527015eb6acb # via easyocr python-dateutil==2.9.0 \ --hash=sha256:78e73e19c63f5b20ffa567001531680d939dc042bf7850431877645523c66709 \ --hash=sha256:cbf2f1da5e6083ac2fbfd4da39a25f34312230110440f424a14c7558bb85d82e # via - # feast (setup.py) + # feast (pyproject.toml) # aiobotocore # arrow # botocore # elasticsearch # google-cloud-bigquery + # graphene # great-expectations # ibis-framework # jupyter-client # kubernetes + # matplotlib # moto + # openlineage-python # pandas + # strictyaml # trino python-docx==1.2.0 \ --hash=sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7 \ --hash=sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce # via docling -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via - # environs + # mlflow-skinny # pydantic-settings + # pymilvus # testcontainers # uvicorn -python-json-logger==4.0.0 \ - --hash=sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2 \ - --hash=sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f +python-json-logger==4.1.0 \ + --hash=sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2 \ + --hash=sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195 # via jupyter-events python-keycloak==4.2.2 \ --hash=sha256:1d43a1accd4a038ed39317fcb3eb78211df6c75bbcbc4c482c99ee76327136f2 \ --hash=sha256:5137fd87c69031a372a578df96bae96b9aead2c9dad976613bc978e9e0246a1e - # via feast (setup.py) -python-multipart==0.0.20 \ - --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \ - --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13 + # via feast (pyproject.toml) +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 # via mcp python-pptx==1.0.2 \ --hash=sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba \ --hash=sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095 # via docling -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via - # clickhouse-connect # great-expectations - # ibis-framework # pandas # snowflake-connector-python # trino @@ -4526,20 +5338,22 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # accelerate # dask # datasets # docling-core # easyocr # huggingface-hub - # ibis-substrait # jupyter-events # kubernetes + # mlflow-skinny + # openlineage-python # openshift-client # pre-commit # ray # responses + # scylla-driver # timm # transformers # uvicorn @@ -4640,40 +5454,34 @@ pyzmq==27.1.0 \ # ipykernel # jupyter-client # jupyter-server -qdrant-client==1.15.1 \ - --hash=sha256:2b975099b378382f6ca1cfb43f0d59e541be6e16a5892f282a4b8de7eff5cb63 \ - --hash=sha256:631f1f3caebfad0fd0c1fba98f41be81d9962b7bf3ca653bed3b727c0e0cbe0e - # via feast (setup.py) -ray[data, default]==2.47.1 \ - --hash=sha256:106817f80087d21d24e63f6e56ea5ab7c387a25105eb65e6b783551f569534ea \ - --hash=sha256:21f2689c1bbc688f9cd31a18bae2c9582027e91b508073849441167bb5077816 \ - --hash=sha256:252a471e8afb918b105cdbffb4cbebb0143baad75a06c8ffcde27ac317579ccb \ - --hash=sha256:322049c4546cf67e5efdad90c371c5508acbb193e5aaaf4038103c6c5ce1f578 \ - --hash=sha256:36a30930e8d265e708df96f37f6f1f5484f4b97090d505912f992e045a69d310 \ - --hash=sha256:3eaeaeec3bbe2ca6493e530c30473d84b8580a7ac3256bb9183d8c63def5a92f \ - --hash=sha256:44900a1a72cb3bfb331db160a8975737c25945a97f376c70e72ccf35adf3b744 \ - --hash=sha256:48961229614b2b56a535be510c8abc76e99a9aa7fa195b5c949bd0c6c69af40a \ - --hash=sha256:601f23ba89918b7b3ffebf967328f7bdb605deaf8c103aad7820dc2722fe450c \ - --hash=sha256:6c7b4abe112c4d698243e30023bcbffe2c2c9a68416b95a6a0d50f9ca5725545 \ - --hash=sha256:6fc7df8657b8df684b77c2d1b643137ad745aa1c12ade34743f06cca79003df0 \ - --hash=sha256:7c03a1e366d3a868a55f8c2f728f5ce35ac85ddf093ac81d0c1a35bf1c25c377 \ - --hash=sha256:84a96b4720175a0000521a48eb7aa915f3b419bb5cd6172d8dee005c3f23b813 \ - --hash=sha256:8cd625d469ce15391e5f1f44ddf8dd30b2380f917603fa0172661229acb0011f \ - --hash=sha256:a640d447e0e6cf63f85b9220c883ec02bb2b8e40a9c1d84efa012795c769ba68 \ - --hash=sha256:bd1cba64070db06bbf79c0e075cdc4529193e2d0b19564f4f057b4193b29e912 \ - --hash=sha256:c21720f283a3df360ddec002a592ddfbaf520faf4cb1b86562a7b7c196ad96a0 \ - --hash=sha256:cd4e7eb475487364b5209963b17cefedcb7fbd3a816fdb6def7ea533ebd72424 \ - --hash=sha256:d6ed6d182e25d6f77179dc77bc97a749c81765b13cb671a46db3203029389663 \ - --hash=sha256:db5ff652e9035f03c65e1742a706b76519f6e8a6744cc005396053ac8766fc46 \ - --hash=sha256:dee02ba9b8cd45c4eadc457183f6d80f1701b85f966d02cdacd5b11867cb7375 \ - --hash=sha256:e578929f58b3f0c59c7544a96d864e26278238b755d13cd19ae798070c848e57 \ - --hash=sha256:e6d9c78e53ac89cabbc4056aecfec53c506c692e3132af9dae941d6180ef462f \ - --hash=sha256:feeba1e715cfd8737d3adcd2018d0cdabb7c6084fa4b093e638e6c7d42f3c956 +qdrant-client==1.18.0 \ + --hash=sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd \ + --hash=sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4 + # via feast (pyproject.toml) +ray[data, default]==2.55.1 \ + --hash=sha256:0053fd5b400f7ac56263aa1bbd3d68fb79341b08b8dc697c88782d5aca7b3ed4 \ + --hash=sha256:0ea2f670a7725833ad2333a8c46ab69865ad06c8e5de9f65695e0f8f35331cec \ + --hash=sha256:137f9006eee28caab8260803cca314f37bbda3fc94fdfa31c770b5d019626ad8 \ + --hash=sha256:1380e043eb57cde69b7e9199c6f2558ceeb8f0fc41c97d1d5e50ea042115f302 \ + --hash=sha256:156ed3e72ad95b645d2006cd71a8dddbcc89b56bfc00027f6225adf78bd9cb74 \ + --hash=sha256:263705f6bab29e7622a94f82da25fd7f9cead76cdf89a07aab28f79cdf8f9d95 \ + --hash=sha256:26541f69bb55607ef8335baac75b2ed12ff2ce02d56313219b29eda003039221 \ + --hash=sha256:2d5786661e192148719accc959def6cdcabd7a24cd9008005bf3d0e3c8cfd529 \ + --hash=sha256:4e618d61e1b14b6fde9a586151f3fd9d435b0b85048b997bcaa7f4a533747b2b \ + --hash=sha256:5e56d2e8f304cafe990c198a2b894f5b813de018998cd7212869201f6dc17cff \ + --hash=sha256:86e618e9ad8c6a24331c788eb599cee9838a62d2e10dfca0227743be06cf551c \ + --hash=sha256:9ad56704c8bd7e92130162f9c58e4ef473609515637673d5a36e761f95335206 \ + --hash=sha256:b062045c64c2bce39a51661624f7292c7bbf30f2a9d878627aae31d46da5712d \ + --hash=sha256:b415d590e062f248907e0fe42994943f11726b7178fcf4b1cf5546721fb1a5f8 \ + --hash=sha256:baf2ec89df7838cabdef493ff9bdbec1e6a6452f8bc696ad0c1b8a6198721745 \ + --hash=sha256:bb49fbbe53a1d931e1f92d17f9271338f0b738885f8f70b7f531aa33f019d8af \ + --hash=sha256:d5382da181c03ee2f502ef46cf0ae4bbc30157b5bd9a67d7651f6a272528a85a \ + --hash=sha256:f9844a9272ef2e6eb5771025866072cf4234cf4c7cc1a31e235b7de7111864be # via codeflare-sdk -redis==4.6.0 \ - --hash=sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d \ - --hash=sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c - # via feast (setup.py) +redis==7.4.1 \ + --hash=sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e \ + --hash=sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + # via feast (pyproject.toml) referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 @@ -4681,132 +5489,132 @@ referencing==0.37.0 \ # jsonschema # jsonschema-specifications # jupyter-events -regex==2025.9.18 \ - --hash=sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5 \ - --hash=sha256:039a9d7195fd88c943d7c777d4941e8ef736731947becce773c31a1009cb3c35 \ - --hash=sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282 \ - --hash=sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef \ - --hash=sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41 \ - --hash=sha256:065b6956749379d41db2625f880b637d4acc14c0a4de0d25d609a62850e96d36 \ - --hash=sha256:0716e4d6e58853d83f6563f3cf25c281ff46cf7107e5f11879e32cb0b59797d9 \ - --hash=sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3 \ - --hash=sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788 \ - --hash=sha256:0cc3521060162d02bd36927e20690129200e5ac9d2c6d32b70368870b122db25 \ - --hash=sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac \ - --hash=sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56 \ - --hash=sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946 \ - --hash=sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203 \ - --hash=sha256:12296202480c201c98a84aecc4d210592b2f55e200a1d193235c4db92b9f6788 \ - --hash=sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12 \ - --hash=sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e \ - --hash=sha256:16bd2944e77522275e5ee36f867e19995bcaa533dcb516753a26726ac7285442 \ - --hash=sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d \ - --hash=sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af \ - --hash=sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3 \ - --hash=sha256:1e85f73ef7095f0380208269055ae20524bfde3f27c5384126ddccf20382a638 \ - --hash=sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23 \ - --hash=sha256:220381f1464a581f2ea988f2220cf2a67927adcef107d47d6897ba5a2f6d51a4 \ - --hash=sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494 \ - --hash=sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1 \ - --hash=sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2 \ - --hash=sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096 \ - --hash=sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5 \ - --hash=sha256:34d674cbba70c9398074c8a1fcc1a79739d65d1105de2a3c695e2b05ea728251 \ - --hash=sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d \ - --hash=sha256:385c9b769655cb65ea40b6eea6ff763cbb6d69b3ffef0b0db8208e1833d4e746 \ - --hash=sha256:3acc471d1dd7e5ff82e6cacb3b286750decd949ecd4ae258696d04f019817ef8 \ - --hash=sha256:3b524d010973f2e1929aeb635418d468d869a5f77b52084d9f74c272189c251d \ - --hash=sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77 \ - --hash=sha256:3dbcfcaa18e9480669030d07371713c10b4f1a41f791ffa5cb1a99f24e777f40 \ - --hash=sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e \ - --hash=sha256:431bd2a8726b000eb6f12429c9b438a24062a535d06783a93d2bcbad3698f8a8 \ - --hash=sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e \ - --hash=sha256:47acd811589301298c49db2c56bde4f9308d6396da92daf99cba781fa74aa450 \ - --hash=sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad \ - --hash=sha256:4a12a06c268a629cb67cc1d009b7bb0be43e289d00d5111f86a2efd3b1949444 \ - --hash=sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f \ - --hash=sha256:4baeb1b16735ac969a7eeecc216f1f8b7caf60431f38a2671ae601f716a32d25 \ - --hash=sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb \ - --hash=sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352 \ - --hash=sha256:50e8290707f2fb8e314ab3831e594da71e062f1d623b05266f8cfe4db4949afd \ - --hash=sha256:51076980cd08cd13c88eb7365427ae27f0d94e7cebe9ceb2bb9ffdae8fc4d82a \ - --hash=sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a \ - --hash=sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3 \ - --hash=sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425 \ - --hash=sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379 \ - --hash=sha256:5db95ff632dbabc8c38c4e82bf545ab78d902e81160e6e455598014f0abe66b9 \ - --hash=sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d \ - --hash=sha256:6479d5555122433728760e5f29edb4c2b79655a8deb681a141beb5c8a025baea \ - --hash=sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d \ - --hash=sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d \ - --hash=sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743 \ - --hash=sha256:6b498437c026a3d5d0be0020023ff76d70ae4d77118e92f6f26c9d0423452446 \ - --hash=sha256:726177ade8e481db669e76bf99de0b278783be8acd11cef71165327abd1f170a \ - --hash=sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742 \ - --hash=sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47 \ - --hash=sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164 \ - --hash=sha256:7e2b414deae99166e22c005e154a5513ac31493db178d8aec92b3269c9cce8c9 \ - --hash=sha256:828446870bd7dee4e0cbeed767f07961aa07f0ea3129f38b3ccecebc9742e0b8 \ - --hash=sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a \ - --hash=sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0 \ - --hash=sha256:87f681bfca84ebd265278b5daa1dcb57f4db315da3b5d044add7c30c10442e61 \ - --hash=sha256:8900b3208e022570ae34328712bef6696de0804c122933414014bae791437ab2 \ - --hash=sha256:895197241fccf18c0cea7550c80e75f185b8bd55b6924fcae269a1a92c614a07 \ - --hash=sha256:8e5f41ad24a1e0b5dfcf4c4e5d9f5bd54c895feb5708dd0c1d0d35693b24d478 \ - --hash=sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea \ - --hash=sha256:9098e29b3ea4ffffeade423f6779665e2a4f8db64e699c0ed737ef0db6ba7b12 \ - --hash=sha256:90b6b7a2d0f45b7ecaaee1aec6b362184d6596ba2092dd583ffba1b78dd0231c \ - --hash=sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783 \ - --hash=sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7 \ - --hash=sha256:a021217b01be2d51632ce056d7a837d3fa37c543ede36e39d14063176a26ae29 \ - --hash=sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68 \ - --hash=sha256:a295916890f4df0902e4286bc7223ee7f9e925daa6dcdec4192364255b70561a \ - --hash=sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e \ - --hash=sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b \ - --hash=sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368 \ - --hash=sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282 \ - --hash=sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306 \ - --hash=sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01 \ - --hash=sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95 \ - --hash=sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb \ - --hash=sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29 \ - --hash=sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a \ - --hash=sha256:c204e93bf32cd7a77151d44b05eb36f469d0898e3fba141c026a26b79d9914a0 \ - --hash=sha256:c28821d5637866479ec4cc23b8c990f5bc6dd24e5e4384ba4a11d38a526e1414 \ - --hash=sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4 \ - --hash=sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129 \ - --hash=sha256:c81b892af4a38286101502eae7aec69f7cd749a893d9987a92776954f3943408 \ - --hash=sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb \ - --hash=sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6 \ - --hash=sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f \ - --hash=sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773 \ - --hash=sha256:d4a691494439287c08ddb9b5793da605ee80299dd31e95fa3f323fac3c33d9d4 \ - --hash=sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730 \ - --hash=sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a \ - --hash=sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571 \ - --hash=sha256:dfbde38f38004703c35666a1e1c088b778e35d55348da2b7b278914491698d6a \ - --hash=sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459 \ - --hash=sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90 \ - --hash=sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab \ - --hash=sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f \ - --hash=sha256:ef8d10cc0989565bcbe45fb4439f044594d5c2b8919d3d229ea2c4238f1d55b0 \ - --hash=sha256:f04d2f20da4053d96c08f7fde6e1419b7ec9dbcee89c96e3d731fca77f411b95 \ - --hash=sha256:f2f422214a03fab16bfa495cfec72bee4aaa5731843b771860a471282f1bf74f \ - --hash=sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b \ - --hash=sha256:f5cca697da89b9f8ea44115ce3130f6c54c22f541943ac8e9900461edc2b8bd4 \ - --hash=sha256:fb137ec7c5c54f34a25ff9b31f6b7b0c2757be80176435bf367111e3f71d72df \ - --hash=sha256:fb967eb441b0f15ae610b7069bdb760b929f267efbf522e814bbbfffdf125ce2 \ - --hash=sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2 - # via - # feast (setup.py) +regex==2026.7.19 \ + --hash=sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0 \ + --hash=sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6 \ + --hash=sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62 \ + --hash=sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af \ + --hash=sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc \ + --hash=sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13 \ + --hash=sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd \ + --hash=sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951 \ + --hash=sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc \ + --hash=sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511 \ + --hash=sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12 \ + --hash=sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518 \ + --hash=sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db \ + --hash=sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae \ + --hash=sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009 \ + --hash=sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986 \ + --hash=sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1 \ + --hash=sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a \ + --hash=sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2 \ + --hash=sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0 \ + --hash=sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78 \ + --hash=sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d \ + --hash=sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4 \ + --hash=sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0 \ + --hash=sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11 \ + --hash=sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52 \ + --hash=sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e \ + --hash=sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902 \ + --hash=sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11 \ + --hash=sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6 \ + --hash=sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba \ + --hash=sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e \ + --hash=sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac \ + --hash=sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939 \ + --hash=sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb \ + --hash=sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc \ + --hash=sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095 \ + --hash=sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b \ + --hash=sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b \ + --hash=sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220 \ + --hash=sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c \ + --hash=sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae \ + --hash=sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3 \ + --hash=sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44 \ + --hash=sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665 \ + --hash=sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5 \ + --hash=sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97 \ + --hash=sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218 \ + --hash=sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864 \ + --hash=sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e \ + --hash=sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4 \ + --hash=sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda \ + --hash=sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459 \ + --hash=sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18 \ + --hash=sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3 \ + --hash=sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5 \ + --hash=sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a \ + --hash=sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035 \ + --hash=sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa \ + --hash=sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5 \ + --hash=sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78 \ + --hash=sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20 \ + --hash=sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a \ + --hash=sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a \ + --hash=sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a \ + --hash=sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965 \ + --hash=sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5 \ + --hash=sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797 \ + --hash=sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276 \ + --hash=sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c \ + --hash=sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547 \ + --hash=sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9 \ + --hash=sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d \ + --hash=sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1 \ + --hash=sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68 \ + --hash=sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a \ + --hash=sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd \ + --hash=sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c \ + --hash=sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6 \ + --hash=sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82 \ + --hash=sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7 \ + --hash=sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15 \ + --hash=sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e \ + --hash=sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38 \ + --hash=sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96 \ + --hash=sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2 \ + --hash=sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8 \ + --hash=sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732 \ + --hash=sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966 \ + --hash=sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053 \ + --hash=sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3 \ + --hash=sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0 \ + --hash=sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f \ + --hash=sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e \ + --hash=sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327 \ + --hash=sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac \ + --hash=sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6 \ + --hash=sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2 \ + --hash=sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a \ + --hash=sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435 \ + --hash=sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5 \ + --hash=sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d \ + --hash=sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312 \ + --hash=sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b \ + --hash=sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40 \ + --hash=sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974 \ + --hash=sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404 \ + --hash=sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff \ + --hash=sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf \ + --hash=sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175 \ + --hash=sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da \ + --hash=sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d \ + --hash=sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1 \ + --hash=sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2 + # via + # feast (pyproject.toml) # parsimonious # transformers -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # feast (setup.py) + # feast (pyproject.toml) # azure-core + # databricks-sdk # datasets # docker # docling @@ -4817,9 +5625,14 @@ requests==2.32.5 \ # great-expectations # huggingface-hub # jupyterlab-server + # kube-authkit # kubernetes + # mlflow-skinny # moto # msal + # openlineage-python + # pyiceberg + # pymilvus # python-keycloak # ray # requests-oauthlib @@ -4840,9 +5653,9 @@ requests-toolbelt==1.0.0 \ --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 # via python-keycloak -responses==0.25.8 \ - --hash=sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c \ - --hash=sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4 +responses==0.26.2 \ + --hash=sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364 \ + --hash=sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8 # via moto rfc3339-validator==0.1.4 \ --hash=sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b \ @@ -4860,177 +5673,135 @@ rfc3987-syntax==1.1.0 \ --hash=sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f \ --hash=sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d # via jsonschema -rich==13.9.4 \ - --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ - --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 +rich==14.3.4 \ + --hash=sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952 \ + --hash=sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9 # via # codeflare-sdk # fastapi-mcp # ibis-framework + # pyiceberg # typer -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==2026.6.3 \ + --hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \ + --hash=sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680 \ + --hash=sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9 \ + --hash=sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538 \ + --hash=sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804 \ + --hash=sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf \ + --hash=sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4 \ + --hash=sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97 \ + --hash=sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6 \ + --hash=sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96 \ + --hash=sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a \ + --hash=sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187 \ + --hash=sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975 \ + --hash=sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f \ + --hash=sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703 \ + --hash=sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9 \ + --hash=sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127 \ + --hash=sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f \ + --hash=sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa \ + --hash=sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05 \ + --hash=sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171 \ + --hash=sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba \ + --hash=sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c \ + --hash=sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223 \ + --hash=sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4 \ + --hash=sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885 \ + --hash=sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698 \ + --hash=sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f \ + --hash=sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7 \ + --hash=sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed \ + --hash=sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f \ + --hash=sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf \ + --hash=sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e \ + --hash=sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f \ + --hash=sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24 \ + --hash=sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a \ + --hash=sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41 \ + --hash=sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc \ + --hash=sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d \ + --hash=sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146 \ + --hash=sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e \ + --hash=sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e \ + --hash=sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4 \ + --hash=sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12 \ + --hash=sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7 \ + --hash=sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261 \ + --hash=sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6 \ + --hash=sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5 \ + --hash=sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93 \ + --hash=sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7 \ + --hash=sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda \ + --hash=sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8 \ + --hash=sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342 \ + --hash=sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c \ + --hash=sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb \ + --hash=sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0 \ + --hash=sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77 \ + --hash=sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3 \ + --hash=sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885 \ + --hash=sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826 \ + --hash=sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617 \ + --hash=sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb \ + --hash=sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577 \ + --hash=sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80 \ + --hash=sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e \ + --hash=sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945 \ + --hash=sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90 \ + --hash=sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7 \ + --hash=sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0 \ + --hash=sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140 \ + --hash=sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822 \ + --hash=sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba \ + --hash=sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9 \ + --hash=sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4 \ + --hash=sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a \ + --hash=sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8 \ + --hash=sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf \ + --hash=sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4 \ + --hash=sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324 \ + --hash=sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53 \ + --hash=sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b \ + --hash=sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41 \ + --hash=sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9 \ + --hash=sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca \ + --hash=sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1 \ + --hash=sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d \ + --hash=sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690 \ + --hash=sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107 \ + --hash=sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2 \ + --hash=sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76 \ + --hash=sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d \ + --hash=sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af \ + --hash=sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6 \ + --hash=sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db \ + --hash=sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369 \ + --hash=sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd \ + --hash=sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911 \ + --hash=sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504 \ + --hash=sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a \ + --hash=sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9 \ + --hash=sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13 \ + --hash=sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc \ + --hash=sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278 \ + --hash=sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868 \ + --hash=sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2 \ + --hash=sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd \ + --hash=sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4 \ + --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6 \ + --hash=sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9 \ + --hash=sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00 \ + --hash=sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f \ + --hash=sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e \ + --hash=sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442 \ + --hash=sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da \ + --hash=sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90 \ + --hash=sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef # via # jsonschema # referencing -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via google-auth rtree==1.4.1 \ --hash=sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c \ --hash=sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0 \ @@ -5048,197 +5819,251 @@ ruamel-yaml==0.17.17 \ --hash=sha256:9751de4cbb57d4bfbf8fc394e125ed4a2f170fbff3dc3d78abf50be85924f8be \ --hash=sha256:9af3ec5d7f8065582f3aa841305465025d0afd26c5fb54e15b964e11838fc74f # via great-expectations -ruff==0.14.0 \ - --hash=sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206 \ - --hash=sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2 \ - --hash=sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7 \ - --hash=sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f \ - --hash=sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3 \ - --hash=sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57 \ - --hash=sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02 \ - --hash=sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8 \ - --hash=sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543 \ - --hash=sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d \ - --hash=sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8 \ - --hash=sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296 \ - --hash=sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd \ - --hash=sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7 \ - --hash=sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2 \ - --hash=sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e \ - --hash=sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e \ - --hash=sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730 \ - --hash=sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c - # via feast (setup.py) -s3transfer==0.13.1 \ - --hash=sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724 \ - --hash=sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf +ruff==0.16.0 \ + --hash=sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17 \ + --hash=sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d \ + --hash=sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af \ + --hash=sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09 \ + --hash=sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522 \ + --hash=sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472 \ + --hash=sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0 \ + --hash=sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b \ + --hash=sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9 \ + --hash=sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213 \ + --hash=sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb \ + --hash=sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed \ + --hash=sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717 \ + --hash=sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a \ + --hash=sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81 \ + --hash=sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982 \ + --hash=sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e \ + --hash=sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4 + # via feast (pyproject.toml) +s3transfer==0.19.2 \ + --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \ + --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25 # via boto3 -safetensors[torch]==0.6.2 \ - --hash=sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd \ - --hash=sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9 \ - --hash=sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f \ - --hash=sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19 \ - --hash=sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1 \ - --hash=sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a \ - --hash=sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba \ - --hash=sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce \ - --hash=sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c \ - --hash=sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1 \ - --hash=sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b \ - --hash=sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5 \ - --hash=sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac \ - --hash=sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7 \ - --hash=sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda +safetensors[torch]==0.8.0 \ + --hash=sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358 \ + --hash=sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f \ + --hash=sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d \ + --hash=sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d \ + --hash=sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0 \ + --hash=sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc \ + --hash=sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235 \ + --hash=sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98 \ + --hash=sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4 \ + --hash=sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846 \ + --hash=sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca \ + --hash=sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0 \ + --hash=sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25 \ + --hash=sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452 \ + --hash=sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d \ + --hash=sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78 \ + --hash=sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774 # via # accelerate # docling-ibm-models # timm # transformers -scikit-image==0.25.2 \ - --hash=sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641 \ - --hash=sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341 \ - --hash=sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd \ - --hash=sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed \ - --hash=sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063 \ - --hash=sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054 \ - --hash=sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f \ - --hash=sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da \ - --hash=sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb \ - --hash=sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d \ - --hash=sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824 \ - --hash=sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09 \ - --hash=sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173 \ - --hash=sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b \ - --hash=sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147 \ - --hash=sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2 \ - --hash=sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99 \ - --hash=sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78 \ - --hash=sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0 \ - --hash=sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc \ - --hash=sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde \ - --hash=sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17 +scikit-image==0.26.0 \ + --hash=sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf \ + --hash=sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650 \ + --hash=sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb \ + --hash=sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581 \ + --hash=sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f \ + --hash=sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9 \ + --hash=sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d \ + --hash=sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e \ + --hash=sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2 \ + --hash=sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394 \ + --hash=sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37 \ + --hash=sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70 \ + --hash=sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44 \ + --hash=sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8 \ + --hash=sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc \ + --hash=sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab \ + --hash=sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c \ + --hash=sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3 \ + --hash=sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59 \ + --hash=sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af \ + --hash=sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd \ + --hash=sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466 \ + --hash=sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21 \ + --hash=sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37 \ + --hash=sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7 \ + --hash=sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5 \ + --hash=sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953 \ + --hash=sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49 \ + --hash=sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604 \ + --hash=sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219 \ + --hash=sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7 \ + --hash=sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0 \ + --hash=sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1 \ + --hash=sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd \ + --hash=sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0 \ + --hash=sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b \ + --hash=sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4 \ + --hash=sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f \ + --hash=sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505 \ + --hash=sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a \ + --hash=sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1 \ + --hash=sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd \ + --hash=sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932 \ + --hash=sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c \ + --hash=sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d \ + --hash=sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578 \ + --hash=sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa \ + --hash=sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331 \ + --hash=sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62 # via easyocr -scikit-learn==1.7.2 \ - --hash=sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1 \ - --hash=sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7 \ - --hash=sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c \ - --hash=sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda \ - --hash=sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a \ - --hash=sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c \ - --hash=sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18 \ - --hash=sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f \ - --hash=sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973 \ - --hash=sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290 \ - --hash=sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c \ - --hash=sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f \ - --hash=sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0 \ - --hash=sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8 \ - --hash=sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d \ - --hash=sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96 \ - --hash=sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1 \ - --hash=sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106 \ - --hash=sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61 \ - --hash=sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c \ - --hash=sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8 \ - --hash=sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1 \ - --hash=sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe \ - --hash=sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476 \ - --hash=sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44 \ - --hash=sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8 \ - --hash=sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e \ - --hash=sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5 \ - --hash=sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b \ - --hash=sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615 \ - --hash=sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33 - # via feast (setup.py) -scipy==1.16.2 \ - --hash=sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925 \ - --hash=sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232 \ - --hash=sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1 \ - --hash=sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88 \ - --hash=sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e \ - --hash=sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351 \ - --hash=sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70 \ - --hash=sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9 \ - --hash=sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851 \ - --hash=sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d \ - --hash=sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104 \ - --hash=sha256:4e409eac067dcee96a57fbcf424c13f428037827ec7ee3cb671ff525ca4fc34d \ - --hash=sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936 \ - --hash=sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1 \ - --hash=sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77 \ - --hash=sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9 \ - --hash=sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3 \ - --hash=sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef \ - --hash=sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8 \ - --hash=sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4 \ - --hash=sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7 \ - --hash=sha256:6ab88ea43a57da1af33292ebd04b417e8e2eaf9d5aa05700be8d6e1b6501cd92 \ - --hash=sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548 \ - --hash=sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4 \ - --hash=sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9 \ - --hash=sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374 \ - --hash=sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f \ - --hash=sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70 \ - --hash=sha256:87eb178db04ece7c698220d523c170125dbffebb7af0345e66c3554f6f60c173 \ - --hash=sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d \ - --hash=sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831 \ - --hash=sha256:912f46667d2d3834bc3d57361f854226475f695eb08c08a904aadb1c936b6a88 \ - --hash=sha256:91e9e8a37befa5a69e9cacbe0bcb79ae5afb4a0b130fd6db6ee6cc0d491695fa \ - --hash=sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb \ - --hash=sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d \ - --hash=sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a \ - --hash=sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8 \ - --hash=sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e \ - --hash=sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b \ - --hash=sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb \ - --hash=sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c \ - --hash=sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6 \ - --hash=sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7 \ - --hash=sha256:c95e96c7305c96ede73a7389f46ccd6c659c4da5ef1b2789466baeaed3622b6e \ - --hash=sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371 \ - --hash=sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7 \ - --hash=sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779 \ - --hash=sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f \ - --hash=sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5 \ - --hash=sha256:e574be127bb760f0dad24ff6e217c80213d153058372362ccb9555a10fc5e8d2 \ - --hash=sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1 \ - --hash=sha256:ec6e74c4e884104ae006d34110677bfe0098203a3fec2f3faf349f4cb05165e3 \ - --hash=sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac \ - --hash=sha256:f3bf75a6dcecab62afde4d1f973f1692be013110cad5338007927db8da73249c \ - --hash=sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff \ - --hash=sha256:f5db5ba6188d698ba7abab982ad6973265b74bb40a1efe1821b58c87f73892b9 \ - --hash=sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f \ - --hash=sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472 \ - --hash=sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0 \ - --hash=sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c \ - --hash=sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21 +scikit-learn==1.9.0 \ + --hash=sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa \ + --hash=sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8 \ + --hash=sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b \ + --hash=sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42 \ + --hash=sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb \ + --hash=sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2 \ + --hash=sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60 \ + --hash=sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac \ + --hash=sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28 \ + --hash=sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05 \ + --hash=sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283 \ + --hash=sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949 \ + --hash=sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913 \ + --hash=sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277 \ + --hash=sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713 \ + --hash=sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a \ + --hash=sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1 \ + --hash=sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759 \ + --hash=sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f \ + --hash=sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673 \ + --hash=sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666 \ + --hash=sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119 \ + --hash=sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557 \ + --hash=sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162 \ + --hash=sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b \ + --hash=sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e \ + --hash=sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714 \ + --hash=sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96 \ + --hash=sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c \ + --hash=sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8 \ + --hash=sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa + # via + # feast (pyproject.toml) + # mlflow + # sentence-transformers + # skops +scipy==1.18.0 \ + --hash=sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446 \ + --hash=sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468 \ + --hash=sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b \ + --hash=sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553 \ + --hash=sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0 \ + --hash=sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7 \ + --hash=sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2 \ + --hash=sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b \ + --hash=sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61 \ + --hash=sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8 \ + --hash=sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8 \ + --hash=sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6 \ + --hash=sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690 \ + --hash=sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de \ + --hash=sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578 \ + --hash=sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab \ + --hash=sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce \ + --hash=sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f \ + --hash=sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520 \ + --hash=sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378 \ + --hash=sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197 \ + --hash=sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709 \ + --hash=sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132 \ + --hash=sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867 \ + --hash=sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a \ + --hash=sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677 \ + --hash=sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4 \ + --hash=sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0 \ + --hash=sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58 \ + --hash=sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8 \ + --hash=sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76 \ + --hash=sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9 \ + --hash=sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f \ + --hash=sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11 \ + --hash=sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4 \ + --hash=sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b \ + --hash=sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b \ + --hash=sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76 \ + --hash=sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f \ + --hash=sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d \ + --hash=sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707 # via # docling # easyocr # great-expectations + # mlflow # scikit-image # scikit-learn -semchunk==2.2.2 \ - --hash=sha256:940e89896e64eeb01de97ba60f51c8c7b96c6a3951dfcf574f25ce2146752f52 \ - --hash=sha256:94ca19020c013c073abdfd06d79a7c13637b91738335f3b8cdb5655ee7cc94d2 + # sentence-transformers + # skops +scylla-driver==3.29.11 \ + --hash=sha256:0a93dd4af8995ac08335e581aca393a2bca64c3d4748ec359b9400797f2a7449 \ + --hash=sha256:17b19807a8690a4df2e52bb23327a746715e7076f5f28b45a6ce4556b2a00dd7 \ + --hash=sha256:1ea1fd011d8cf74b706c1b6ae982437597abf545e6d53f3a79905c97831db16a \ + --hash=sha256:241bffc347888dbeae48d842e58a7d20915ee518beb46467d9b7cd5a0ad501ed \ + --hash=sha256:2546a6646eead8225c8a0440c744c50ca85147fe61a712fb68e08be11ab21560 \ + --hash=sha256:2a4116f2da833f76db6e59b4c0534b72291cac478ca4b5944abbdd52b1635d94 \ + --hash=sha256:3094c90a49c172b1c09bbf44b46ea0c2c284e35cfd0222bc0062492de05131c0 \ + --hash=sha256:39f48ebe02d17e7cc6f3ba34f9748c971932eaa11d6557ab7d5fae1631987a55 \ + --hash=sha256:45050fd2028f07e236b7b3a555d4d17a1d88ea8ba8b5609ae1d14730a65b1e4f \ + --hash=sha256:57e8fffaf0803c0a406cc0e331a738c5d305fb28c9f5834d29c077fa134a8820 \ + --hash=sha256:617a8c459b30403002d38d7f99e3ed160da7671c7e62b0647e5fbd89539c6aff \ + --hash=sha256:677b5b0312275a958c80dbd3d1686e4a60c805ddae290a4b56551245c0a07a95 \ + --hash=sha256:6ee63f2503590f683e4b6c39c0070f567cc825fabd20f74cfe053d926a930ffc \ + --hash=sha256:70dda8384ae23472cfdf6c798d20e59639aa1e4796937417088151adb4acf4a9 \ + --hash=sha256:74668f6d4bca9f05f0826b8b464f363e7c1c7ac1cbbf373bae638bdc68780b8e \ + --hash=sha256:8072842efb2ff7c9761563485156fb0b6a11c68b028e32b6a807a70d226dbbbd \ + --hash=sha256:844e61a13edf0d12f95a64f6dd8a9c0fd0b0919570e10631e33c640acff59db7 \ + --hash=sha256:8ed1b2ec3634d3b25609a3a587de420a41b435f497e963434a8b5b42fa32c88e \ + --hash=sha256:9fb9bc8ae114434c3418ca8ba0bca00fb7afd4943a52694b5bde3f8681a0eea9 \ + --hash=sha256:a8f644d73713be556e99d6355cd2de41503ee50bff990865d71887e1f9f18b46 \ + --hash=sha256:abb5f98b4efbcb6257d1b97eea9c2bb95ea509e8983c2b8e021810ef460cb390 \ + --hash=sha256:acb38ed6acae2185bdef6834569bb95acd858ec91a8269c555e912127946e998 \ + --hash=sha256:ae36b731481c9ded343811132bcce4f68c9f2de8af8a51248fcea4d9c3b36d52 \ + --hash=sha256:b5a4851afb2a6d8410f55e7344b0589f71289338ff35c6901135c1a683381149 \ + --hash=sha256:bf33948e181c469f63108c01f319078051819c147f6446ea56b71975656efc1e \ + --hash=sha256:cd0d872882a4c098eacc83b832a042ea0b5165e247790d378e215c2a9f205d44 \ + --hash=sha256:cdd43ebb03db2441b920033d032a824612aa7f4daa58e88563e0a2036d6bd860 \ + --hash=sha256:ce1cc363d2b9c9e5a0def22b85c13c1bde6f26d1abca427b408bd18f0fd3828c \ + --hash=sha256:cf98016b6ce61dc30d80ac5bc491e0a84aa9da09e326fead5bba888d55f56126 \ + --hash=sha256:dda6b9f7fe48c6a0623e629a6bc0497dff132ca8a2a8897cd1f4281b5b12ecfe \ + --hash=sha256:e74576dba8992b4dac6ea3fe4c8cfd92f5318aa08b20142aff6423f20e56da7d \ + --hash=sha256:e7d899f1b5bc6d9441db01b05bd3c317b99be23525e6baae789ee59d759187fc \ + --hash=sha256:ee188f93fc931d9ce6231b550260988312445ecd2ea336445ec98b225ace47fb \ + --hash=sha256:f222606faeb11f7d41197f16b97cfc3c9578608bd5c2813d51d30c4dc42e675b \ + --hash=sha256:f3e2d06685fb1e6dad8905b0efa9423155852cf1b2a0d2b1aad3687cd488e4a4 + # via feast (pyproject.toml) +semchunk==3.2.5 \ + --hash=sha256:ee15e9a06a69a411937dd8fcf0a25d7ef389c5195863140436872a02c95b0218 \ + --hash=sha256:fd09cc5f380bd010b8ca773bd81893f7eaf11d37dd8362a83d46cedaf5dae076 # via docling-core -send2trash==1.8.3 \ - --hash=sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 \ - --hash=sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf +send2trash==2.1.0 \ + --hash=sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c \ + --hash=sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459 # via jupyter-server -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c - # via - # feast (setup.py) +sentence-transformers==5.6.1 \ + --hash=sha256:16af5d682ef66672b076d58599a23905800e850ec2bfb1865938306bf684ad72 \ + --hash=sha256:cefbb17b6325a982a4732c8c49fb013375392687049d1de3d435c4b04060680b + # via feast (pyproject.toml) +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 + # via + # feast (pyproject.toml) # grpcio-tools - # jupyterlab # pandas-gbq # pbr # pip-tools # pydata-google-auth - # pymilvus # singlestoredb # torch shapely==2.1.2 \ @@ -5312,13 +6137,11 @@ singlestoredb==1.7.2 \ --hash=sha256:92bc932df8b124a3c88b552210f9e0bb11cba4bdfbc9e7568c1582c00f0e8bcb \ --hash=sha256:c2a23b2b22f1e76cb0d53c99250de9a600bec9621766e25ae379c50914d6436a \ --hash=sha256:fba7f30f7fddb88e656e4309157d9e0016b6b1127d5adf348ba831bf77872d07 - # via feast (setup.py) + # via feast (pyproject.toml) six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via - # azure-core - # geomet # happybase # kubernetes # mock @@ -5327,60 +6150,69 @@ six==1.17.0 \ # python-dateutil # rfc3339-validator # thriftpy2 -smart-open==7.3.1 \ - --hash=sha256:b33fee8dffd206f189d5e704106a8723afb4210d2ff47e0e1f7fbe436187a990 \ - --hash=sha256:e243b2e7f69d6c0c96dd763d6fbbedbb4e0e4fc6d74aa007acc5b018d523858c +skops==0.14.0 \ + --hash=sha256:60a5db78a9db46ccee2139a0ba13ab5afb1c96f4749b382e75a371291bbe3e36 \ + --hash=sha256:6c8c0e047f691a3a582c3258943eecafcbfd79c8c7eef66260f3703e363254f0 + # via mlflow +smart-open==8.0.1 \ + --hash=sha256:18b1c4496003c6902be17c15f032b5c319f307c89c6ae9e6b028b508bed8b2cf \ + --hash=sha256:3e97f90e92a952cb57863dfe132082c400a52eeeb27c067692fb51dbcc5b0089 # via ray +smmap==5.0.3 \ + --hash=sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c \ + --hash=sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f + # via gitdb sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc # via - # anyio + # elastic-transport + # elasticsearch # httpx -snowballstemmer==3.0.1 \ - --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ - --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 +snowballstemmer==3.1.1 \ + --hash=sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 \ + --hash=sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260 # via sphinx -snowflake-connector-python[pandas]==3.18.0 \ - --hash=sha256:0af10b207af3d2de2b130e89018d49a60f2e5cfe841f3bf459e58f2e1c4c4506 \ - --hash=sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628 \ - --hash=sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4 \ - --hash=sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e \ - --hash=sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee \ - --hash=sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9 \ - --hash=sha256:32b1abfea32561d817b0a2f80b06d936cb32712af06bf7b848a428bfd857a10a \ - --hash=sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a \ - --hash=sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728 \ - --hash=sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800 \ - --hash=sha256:4ed2d593f1983939d5d8d88b212d86fd4f14f0ceefc1df9882b4a18534adbde9 \ - --hash=sha256:51eb789a09dc6c62119cfabd044fba1a6b8378206f05a1e83ddb2e9cb49acc0b \ - --hash=sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc \ - --hash=sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb \ - --hash=sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba \ - --hash=sha256:783a9ab206563d7b52fdcdd7a72af62de811d3381ca64132fd3445537b4d041b \ - --hash=sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f \ - --hash=sha256:8d3e96e1d09b07edca6c1f6ca675b6fdd05a4a7e428e4cdf6fb697d87b9f60fc \ - --hash=sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a \ - --hash=sha256:a8c570edff5a4888840dbe1e9e65c5e4d77d55c5c800cd359fe0903a769201e0 \ - --hash=sha256:aeeb181a156333480f60b5f8ddbb3d087e288b4509adbef7993236defe4d7570 \ - --hash=sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca \ - --hash=sha256:b99f261c82be92224ac20c8c12bdf26ce3ed5dfd8a3df8a97f15a1e11c46ad27 \ - --hash=sha256:bd1de3038b6d7059ca59f93e105aba2a673151c693cc4292f72f38bfaf147df2 \ - --hash=sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e \ - --hash=sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00 - # via feast (setup.py) +snowflake-connector-python[pandas]==4.7.1 \ + --hash=sha256:051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117 \ + --hash=sha256:052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7 \ + --hash=sha256:0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317 \ + --hash=sha256:167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf \ + --hash=sha256:171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a \ + --hash=sha256:1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6 \ + --hash=sha256:22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf \ + --hash=sha256:2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b \ + --hash=sha256:4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a \ + --hash=sha256:4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754 \ + --hash=sha256:5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9 \ + --hash=sha256:571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d \ + --hash=sha256:5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35 \ + --hash=sha256:7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0 \ + --hash=sha256:7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5 \ + --hash=sha256:81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57 \ + --hash=sha256:8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a \ + --hash=sha256:92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3 \ + --hash=sha256:b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45 \ + --hash=sha256:d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa \ + --hash=sha256:e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327 \ + --hash=sha256:f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c \ + --hash=sha256:f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7 \ + --hash=sha256:f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52 \ + --hash=sha256:fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1 \ + --hash=sha256:ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b + # via feast (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via snowflake-connector-python -soupsieve==2.8 \ - --hash=sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c \ - --hash=sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f +soupsieve==2.9.1 \ + --hash=sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c \ + --hash=sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba # via beautifulsoup4 sphinx==6.2.1 \ --hash=sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b \ --hash=sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912 - # via feast (setup.py) + # via feast (pyproject.toml) sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 @@ -5405,133 +6237,102 @@ sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via sphinx -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -sqlglot[rs]==25.20.2 \ - --hash=sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab \ - --hash=sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567 - # via - # feast (setup.py) +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via + # feast (pyproject.toml) + # alembic + # mlflow +sqlglot[rs]==30.14.0 \ + --hash=sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9 \ + --hash=sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636 + # via + # feast (pyproject.toml) # ibis-framework -sqlglotrs==0.2.12 \ - --hash=sha256:0338c7770a5cb5bb0ec1dcbe5206359fe9b83da0aba8dde53b9e7bd1afc89a22 \ - --hash=sha256:057a8db59a6c4bcdc42831e7ad01f41cf9e7f388ed5b139816adafbcacf2f591 \ - --hash=sha256:065835e7f2be50ba83895b64d044a39dab9d95098fff995427365e4bd8bc7bc6 \ - --hash=sha256:08e8be22da77c964be76ab4438da2c77096f5871088466ca950ee1b4712a97d4 \ - --hash=sha256:147cda8412f45af290ad190d9a98b5829a5f46a575ce768279ccebf9b7b53785 \ - --hash=sha256:155b0d59e34851b119c7ff0b2c7968c7b51667c1a1c2abefe1ac7244b3c1d78e \ - --hash=sha256:17b289ef0f25a7c034d183c588345e2b56622f7f64a85d1020633a75f8e3ac96 \ - --hash=sha256:1fc98b7649445e726a492841b8b8b39a4e5724ec2787cd1436404ebccf42519a \ - --hash=sha256:2554ead3126c83864a4b7e48e8e7e1bc23faf7160a6f28d3db967661cf529c9e \ - --hash=sha256:2824fc87fd7e41a785150ff042c7934e1fff97c6ccd59e4d96bebf6697a90762 \ - --hash=sha256:2db7e6cd41ef88c2ac647ad0258f87906de822955dec8f14e91829083047d784 \ - --hash=sha256:315f7f7bbfedf0c87d98068e62363454e986bdd05baa165b7fb448b5c6fe9f1a \ - --hash=sha256:327bfc2d71449f4dffba93d63f0565c4a1fa818143b1cfbc3f936fa8c9bcce10 \ - --hash=sha256:39a6ef72cf271db93ec6019847b7832defa9f4013c1e66851ca9c0a11c010c0c \ - --hash=sha256:4364116b7b0c72b841de6acd149a002bfc8fe360125989d4f39debd387c874d8 \ - --hash=sha256:4c07d3dba9c3ae8b56a0e45a9e47aa2a2c6ed95870c5bcc67dacaadb873843ff \ - --hash=sha256:4ceb28cf2ee3850cd745167cebe59a5fc3d506b32e9c81307938d8d272c1d670 \ - --hash=sha256:4ec38035523d54ba33de1e2b5562de4938254b61e1df48eb1db0e26ea189de28 \ - --hash=sha256:5026eada48f258ce9ad26fa41994b2ea5404bef2c3df9cb5cb2a159112a6269f \ - --hash=sha256:59499adc27a70a72170db9241404a18d4829cd3a83a076b9e112ad365c4b1452 \ - --hash=sha256:5be231acf95920bed473524dd1cac93e4cb320ed7e6ae937531b232c54cfc232 \ - --hash=sha256:67e288759d2be822db2175d0025c1f61283b019f2cc3e2577f31ad0ef3b5854d \ - --hash=sha256:6aacab6e20d92be3ca76f7358fa12346f29985e2d408660c764b7f1c75cc40ee \ - --hash=sha256:6ef3a827f2980aad17af4f8548297c93c4989d4cd3f64b9bcb7443952c542423 \ - --hash=sha256:732516bffffc70f172306ad8bc747dd9f16512cdbc09475abe6ad6f744479dee \ - --hash=sha256:76e4e1765c6be438329e234e56a6772537f6de16c4bb5ba7170e344664cccdf7 \ - --hash=sha256:7b553cdb9e8afcfea5466815e865f874f6f51aaace4fb4101670e150f7bbfe5a \ - --hash=sha256:7c79c43c5cde1f4017641032f11770ed8111c963dccc096cd15df906d4fb46a4 \ - --hash=sha256:8174aa227193d0a755f4515e6c3883be4681c9b669a65c2316f09be27b84be4d \ - --hash=sha256:8a18b3a09c32788d1ee2d0610ab35af862413c56b65f8ad8bc0131701f03103b \ - --hash=sha256:8f268aea3d2ebc05cb9148bb487f21e532f8af1b0a4aed6b7374703aadfb6a7c \ - --hash=sha256:91971032603d05428fd42a978084110afb2a4c0975e4343b075f69a23889e3da \ - --hash=sha256:9334f6c394a671a630c61339d52fb7da1a72eca057570f039b2a4035d2e39380 \ - --hash=sha256:954ccd912391ab5922adb23159ebcc0c5dccb468381e2a1ce92117cb4b0f0ed3 \ - --hash=sha256:9597865efc40e5c41af7719106c7620e1338aaa64646726652c63bae14225391 \ - --hash=sha256:97b2c74fcdd89f0d4458c0e2b5783989be99a1e0b2d627797688ab716ad9391b \ - --hash=sha256:989ccc5dc6b38da937481b6eb2dc1fc0b13676fe129697b874828e577984d7ef \ - --hash=sha256:9c4c6f6fe1c54fff614f9d0b2dd7a6bf948bda87ce51a245dcd3f447f20c8b74 \ - --hash=sha256:9d5b9a9d6259b72258f6764f88a89faa3c648438bd1b2c3a9598b725d42bf6f2 \ - --hash=sha256:a266c9047726d83c51a8ec3d5278ceb9caf131307c9c93c4ceefd99c0116e538 \ - --hash=sha256:a4a2cacb31f75e242c7b9ff4afae1d95f548df8441444114376d8007cc91b55b \ - --hash=sha256:aaf86275a3388da1ed2161645aa346bfca3ee6e1dc0e2115867db9e78f1caddd \ - --hash=sha256:ab676d2d7da28907a139ad5fc20dee0890054967bac0b18e653ac048837c9ea1 \ - --hash=sha256:acc25d651eb663332157c2e5d2736516cddf4cd0effe67a887723934de5051d1 \ - --hash=sha256:b10bf6b71961b31951bf4dff937d8d5d399ea1b3bd47fb5c5810386710fe7dfb \ - --hash=sha256:b40601e67f5abae5d09d23f92394dbd735539de469ce663b596eb42bf77d2c54 \ - --hash=sha256:b6020825e58af6e2795e6dcb69639f5500e45e1da78f1b1abd74c4d11083a249 \ - --hash=sha256:bc1807c6222e32fc9bf6f5c7e12b85c4b72f12227800d40c1693244c198b33bb \ - --hash=sha256:bd6c4e6a7670f761c8e69b45d6d302a4d37a3cddb1fdca2ad90e54b77858fe80 \ - --hash=sha256:bf3e2eab11f06f1df13c0f85b3e26fbab0b7e8a5d189e5edfed951bc85f6bd48 \ - --hash=sha256:c3d62905ce74a48714b7662ad95efe299fad62f193be4b482a327af060f98710 \ - --hash=sha256:c3e0edde0fdf598561e7404ac56fb4b12276394ee5155b5365e42434c6f287a3 \ - --hash=sha256:c64066d13bd2e5e788b845c933c765af9991faa93982e273b623019a1161fadc \ - --hash=sha256:c8bf7ae29c0fc66e9c998d7f8e6f6fc26309c6eb5a4728e1443cb628218bc307 \ - --hash=sha256:d2827c7bf7e57496f9b95658bcd2395cfb0c51adc3023cd3386988337dfaf6a5 \ - --hash=sha256:e7b2da43b2a6a85807df6c56b2627abe244aff28fdf9a4940d38d749cb4b8e3e \ - --hash=sha256:ebc162a599fac86e59f899631716752fbc7f89598e94729eadb707e54db371b2 \ - --hash=sha256:f0a2ddeab27a94447270b7a240770a31a3afed0a972d60085205baec990ad76a \ - --hash=sha256:f104a98182761d4613f920eda7ec5fc921afb3608f7db648206ce06dd10a6be5 \ - --hash=sha256:f83ad3fb4ea57218c0e65d3499e31c9bb3051bbb5dccbb11593eaf1640964b51 \ - --hash=sha256:fa1ae834fb78bd52bb76e3c8d02cb79f45717ab1f02f4ad8154bf33a5408a502 + # sqlglotc +sqlglotc==30.14.0 \ + --hash=sha256:0337b982675e17f6dd9a1ed7d944fdef4f81a2aaff6a4494d50492aa18fb0b4e \ + --hash=sha256:07ee5ed7f8b13cc8e918a31c328933c3f4fd155f5b4360bbbf305445b0f88e73 \ + --hash=sha256:13441a60a212faef4c658b61394cb22b922bf80c1bb544a5397bed67d5549aeb \ + --hash=sha256:13f24fa61b21bf21d6734fd9eeaffa8965dc70228b39994005488688bbc033bc \ + --hash=sha256:13ffa7852e59a6b12e3dbd96686c594847bd6bf4b1a41f1cd8975b0f90e2a522 \ + --hash=sha256:17951d2587cdfb39086e2b06e6d3bfb15628232a295580c6aecda881a4770824 \ + --hash=sha256:20c01558912dfc4a06b8484459337efa6fe98f3e45602e213d888fde56f0410e \ + --hash=sha256:331c600908846a4fee6ffbda0cd82e4b1550612503c761166010a3776334aaf0 \ + --hash=sha256:47f7ab74779572fd51f616522ca597b034158150a7a312a3dbe907d103f5d908 \ + --hash=sha256:49157e4dca20cdab9187dec83f51268d2e3c883ebc9b31afe5e5cf11954bd38e \ + --hash=sha256:5410ff65f26b57e6a9d60f57dd46e8d7aa96b70d1faca562b3931a2b03eb4d56 \ + --hash=sha256:68c259b9ef4ac630c6c04f4e6217f130f0856abe6146133226442e82e1b54ce4 \ + --hash=sha256:745bd364e8a5032c4ead9dd843bfa00c5b35a1d816801de7cfb1eeab08f24b8d \ + --hash=sha256:849520c4b9057408e9198f019791538d5bb77c648b3fc6410db1727acdac90ad \ + --hash=sha256:968f446a8304e782d6778389de561cad9f984b89c3efea0474d8ec89ae17f02c \ + --hash=sha256:9918233d827dcc2b1842a8fe3cbc8d915ab71d9ce0100b566e283edb53950867 \ + --hash=sha256:acb7f577a8e060fb1cb1c9df1b1de1187fc7c53d54539cb6cdc16bd558adf2a5 \ + --hash=sha256:b1f414c6a6a3c3b56b981e51f9fdd1b1cf95dee5de2592c4b4547803309a57d4 \ + --hash=sha256:bc3c4ff8f268e2558aed03638ee002d2187cdf4faa15bf34b6f40558abe29e6e \ + --hash=sha256:c9c9cf31aac1414ccde5e69849bf438b3c74ae763db00651bdf71955ebadc214 \ + --hash=sha256:dfe8296ee3e505c4f3ae22aee4b0556e1263410e02f274ead0bdb723cd1c4147 + # via sqlglot +sqlglotrs==0.13.0 \ + --hash=sha256:6b934a244b16f26fca50974328a2ebc7689583c59f06203cebb46e2e6e8d93a7 \ + --hash=sha256:ad1ad158234af0f8ba5054ca51bd17a7c1e3f81b4798c7970ebf7953fe08ddcb # via sqlglot sqlite-vec==0.1.6 \ --hash=sha256:77491bcaa6d496f2acb5cc0d0ff0b8964434f141523c121e313f9a7d8088dee3 \ @@ -5539,288 +6340,312 @@ sqlite-vec==0.1.6 \ --hash=sha256:823b0493add80d7fe82ab0fe25df7c0703f4752941aee1c7b2b02cec9656cb24 \ --hash=sha256:c65bcfd90fa2f41f9000052bcb8bb75d38240b2dae49225389eca6c3136d3f0c \ --hash=sha256:fdca35f7ee3243668a055255d4dee4dea7eed5a06da8cad409f89facf4595361 - # via feast (setup.py) + # via feast (pyproject.toml) sqlparams==6.2.0 \ --hash=sha256:3744a2ad16f71293db6505b21fd5229b4757489a9b09f3553656a1ae97ba7ca5 \ --hash=sha256:63b32ed9051bdc52e7e8b38bc4f78aed51796cdd9135e730f4c6a7db1048dedf # via singlestoredb -sse-starlette==3.0.2 \ - --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ - --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +sqlparse==0.5.5 \ + --hash=sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba \ + --hash=sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e + # via mlflow-skinny +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 \ + --hash=sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627 # via mcp stack-data==0.6.3 \ --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \ --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695 # via ipython -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via + # feast (pyproject.toml) # fastapi # mcp -substrait==0.24.2 \ - --hash=sha256:743cc352e96b0927b2cd37cd5a8fdac0a96a68df9600bd104fc36aebd222a836 \ - --hash=sha256:d1d475833566fa9d67eed3273456883c0568486ccced92b524b31709d2817e19 - # via ibis-substrait + # mlflow-skinny + # sse-starlette +strictyaml==1.7.3 \ + --hash=sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407 \ + --hash=sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7 + # via pyiceberg sympy==1.14.0 \ --hash=sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517 \ --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5 # via torch -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 # via - # feast (setup.py) + # feast (pyproject.toml) # docling-core # docling-parse tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via + # feast (pyproject.toml) + # pyiceberg terminado==0.18.1 \ --hash=sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 \ --hash=sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e # via # jupyter-server # jupyter-server-terminals -testcontainers==4.9.0 \ - --hash=sha256:2cd6af070109ff68c1ab5389dc89c86c2dc3ab30a21ca734b2cb8f0f80ad479e \ - --hash=sha256:c6fee929990972c40bf6b91b7072c94064ff3649b405a14fde0274c8b2479d32 - # via feast (setup.py) +testcontainers==4.15.0 \ + --hash=sha256:085cde086337632e19002719460b7b80bbab2bdd51bb3ea04f77d0de96504706 \ + --hash=sha256:8796c14e76604031ad39cf0ed3b8e9806283a1fbf5270965c2b1c594caa31b74 + # via feast (pyproject.toml) threadpoolctl==3.6.0 \ --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e # via scikit-learn -thriftpy2==0.5.3 \ - --hash=sha256:08d8699d318b6a8fe9e9fd4c2234ec7912462d90cc636c371b4f4f6500a44328 \ - --hash=sha256:0f36f80a038dbfc2b3b048151ca4732f310ebd0385cdf20e7864d781d5d6f582 \ - --hash=sha256:13c0316a1a9b6f7840d9c084a5a1fa2e419ae86645e45530593558704e792d7f \ - --hash=sha256:195beb93caa104879d808e87d92962fff8d59d40486590fd653b5aeb7774420e \ - --hash=sha256:1bb6c0482663887f2a9ab98453ab0ca20a3e1f2336a500b7da12af33614c0d75 \ - --hash=sha256:2023abcc504e4fc8825419964ecfab904244b0bc189d0082380d481ecba951d7 \ - --hash=sha256:236a7d4627b1aa692a901ca45d7dfa4e516bcd3f309efc18ac69671b31789e39 \ - --hash=sha256:29b09fa1fb77f1927ac4ce21d8f8f6663d8917f75780aa6bad57ca9473d0a3b3 \ - --hash=sha256:2d3c0403673a3b7fc38304cf89e07c792f44ac6aa3b15c12e6cc411a85d10af3 \ - --hash=sha256:2eb14e24febbeca84d603e88a24db9ccb4a1437f90e9a862dcee02dc0a2194c2 \ - --hash=sha256:2fb20edf082965487bddfba03b2c05bb50db38ceda3111540cb2353949fdb29e \ - --hash=sha256:387c04d02f23ae83415cb2de35a88ba79321619af25cf34a481cabc367ddf1aa \ - --hash=sha256:3c00c340114c3041961906a628e70e6b6b5805ee691e682c290cec3513e77efc \ - --hash=sha256:3da0e3c1a5c17f67a203d9814853dd1d8fe8b0ec69a26d30d6b634e4c0e2c87c \ - --hash=sha256:3e2fa8c3d2b1505d2a463c090d9e771b8fed0eda8b01b0365e7547ba106bf2db \ - --hash=sha256:455440132b01b3a895001dc59ecf6056e8fd041ad6e745ff22391cf3a1f8361e \ - --hash=sha256:523c480a4b3aad480e4738c32f97b3f356ed998e6549f5f55eb6f7852474cfff \ - --hash=sha256:524d69102843fed087e30c6edc5b99f7b42b768d88bf910787add60e37e2a2a5 \ - --hash=sha256:53e761ea5eab24bb3520f8adecdbb633e69dda7cf9678ca2eb6ad1952cc56540 \ - --hash=sha256:5e799f6c4caf79a5566bc14941e768b132c533bed66e0a5ef0a127a74f98acab \ - --hash=sha256:6196d7d5adb6214ed21633ec57a222c90a6a66498cdd9f8da7c85c7514c7c439 \ - --hash=sha256:6384a142514982b380431b7d8811f137c5ec9cf5cf3affa33884b7ce4a51c8c2 \ - --hash=sha256:67ec304d83510d0ec83338ef029ea3bae91fdbb3bf0504f7990dd446b483773e \ - --hash=sha256:6efccde429f93740943bc4c0e2664a49f0799ed21663cc26b16a823e8719040f \ - --hash=sha256:710b7f3d9fdbb5788b37b4b694d30fa37a3c08c0d52aeb1def57a5140fa2f4fd \ - --hash=sha256:716c36885e29a9480bdabe117235967b5fe36dc179aca80cd8ef9e12866e4cef \ - --hash=sha256:72a1cdfd3bcf16b667379f8277e83295a34ae74227e54711c657305bf4c9e63b \ - --hash=sha256:73832e79732bab15920bed700a5efbe2b6e9dbd35710e815e870feb873b15059 \ - --hash=sha256:74dc6ed8c9098b66a17b916dd898abc32801a8ef0b439a407bce4f11c1b3da34 \ - --hash=sha256:772c9c1015d05177e37b9a547bcd27b560fc888ecd1e179ebf7f114ec467914d \ - --hash=sha256:86489105abb39c6ff93c3d270cf1474f7fddd380847f2b6bc8d09e5d0f0a23ab \ - --hash=sha256:8a6652e823e0ae6fa9f73b62c1a2ed04d7c0e1ac402c7ca7c509f9f14fbcc80f \ - --hash=sha256:9460a8284881854e210907eeb1761f44afacb4d164d1b6ecfddc184ed1b03277 \ - --hash=sha256:94806a0c3436189a75efac4ab067cdab7298876fee40cc0006300cc1d7982055 \ - --hash=sha256:9cba3454b4e5e05102d2dbfadd3a9a66c19488c6aa18c147bbeff2097ae67f04 \ - --hash=sha256:9eda43701a94def9d063550b0d8261630c40ade312c35b1f6e4804859c783ed8 \ - --hash=sha256:a57c67a880c9da2d252a6244e9ccf7b08850388c7afa4f0e98cb60fdca904a09 \ - --hash=sha256:a7a00b772783847c0c48a43e098b64f5741ca5a2e52e6c66d9b753765cd93ed3 \ - --hash=sha256:a7f2913ea3beac18767784059f02a67751d99094d4a368b350911784f0e09709 \ - --hash=sha256:ab47f689b0003ec63a881e5ad4f46046f62632da5168b0665fd369a3561eaa2a \ - --hash=sha256:addcc3ba9c106758e9073ab205e4bcf9a9540acb335fafa0184a1fa8e76a78cb \ - --hash=sha256:ade0165ba060b97333bc7a927229e992441bfa17bb8e13ea05590c2ec3551b17 \ - --hash=sha256:b208f3c23f916ca0517285c11748ca1fcf43a2ac2224ea5eef8bcba464a20652 \ - --hash=sha256:b5670936016aeaeb7111c96661ced36541211c0e82eb357a9bee5d4176ebbcff \ - --hash=sha256:be23631c152323dd3d7d51368dadcec75e60e90e4662be4f2b8ada208c61fa34 \ - --hash=sha256:bf69d246c39d0ce4ed922b6e00e643ca514cdf40010b00b46f82b0f758a840b3 \ - --hash=sha256:c01e0da29120709d46cb4310944fc717f28ce097d8845c4c29e111ff98c9deff \ - --hash=sha256:c0cf4418810ecf984f6d7f538988175c459f6bd5c85d94b878ebb11dbdbfa62b \ - --hash=sha256:ca4d554f8fc79c8152119bbd576e5d6a5c11e907e0baf467fb4676b1d274558a \ - --hash=sha256:d5080c1c4bd13c4431613a2c0cd607c5e3a07a496a865a0d01f534401d3b09c7 \ - --hash=sha256:e6a77d3d190f1c2726cfb11d1115678fcfa4b0ff509bd8bb38e451c629d9383c \ - --hash=sha256:eb440b7d8e7460f6969016d77e25ebfdee2aa6d5fed95aecf2bd59310c2c5530 \ - --hash=sha256:ec49907ee15513ca2344540c4ad2bf1945c41a6c0236d589eebd32be8298faa9 \ - --hash=sha256:f2ccb893ae687ff946902d96a5615a93847a7868bc5d66f51caf5ccf46466314 \ - --hash=sha256:f4210c10b686fe4a32b121f618b407aaccc7a72021c6d64fa181a09df72c4d89 \ - --hash=sha256:f4d122a82cf7cd4743a2640199b066a994f6527802c2dd16f2e4fffc15efa2a3 \ - --hash=sha256:f768756c0c105e98a3760fc7b4f4df12e25f5334b204060bb6bdab3ce1599e11 \ - --hash=sha256:f965fff2f2f323ddb5d9cb7fabe33c4c9f008955dbb59728ffc3111557b87793 \ - --hash=sha256:fb86f4c0cfcb39949a53dcc689e3758594a18724753861ba9f59646b72417383 \ - --hash=sha256:fd4c6131ca6e919f03263cc83b713f1797bc20126a858da8518ed49b3e32c334 \ - --hash=sha256:fdc5676b52fa6a3009d205360eb9ba257b8b4813883ed52797a20838bcc45dde +thriftpy2==0.6.0 \ + --hash=sha256:033021acfc347f3e51cf107b189a5efafcd1e974a8217a4d066d93719b2bf353 \ + --hash=sha256:0345c8ba40f7b98c24c1ecabfc04ff512ed930ab86ff277572bd279b69a0a252 \ + --hash=sha256:0a0249bf9004d241cf6fd1ed1879209ab7641f7e09456323a839afb6c9213b58 \ + --hash=sha256:0aa86f5d83a49567fa6ac81c7f78ffe8e5cf68b57cf3f7f7c55dc1486f5e9bbb \ + --hash=sha256:0c2f823bb691dd71c9c81170026dc52ace5b750881799960ea9f992919eb9731 \ + --hash=sha256:1245c36b82f34aa26f049e6529f40ad34d9be8d12bd0131559847c8476b98ce0 \ + --hash=sha256:151d299e8e3694a6cc0f2f2cda01d5744080742649de958c5cdcbebb4306205f \ + --hash=sha256:16eecfd34bd541b75172ba0d69ea90b874611a7361d18906fb6d95955089cc30 \ + --hash=sha256:182f54a7248c8ecf1320cf26d1fc9115765df6b1f2589c1d2d0df7049a5b27d4 \ + --hash=sha256:210345281d41b3a76d263b555d3e3cc482103738441bdb92a73033a4e9a042e1 \ + --hash=sha256:265588b8cdb3fe1097db43bf35fb208edc69d9350a2bec3a737d6357d0a5650d \ + --hash=sha256:28fd55960a6d42207060536109928a4615fbbd6874c0ddd8a705b47075f1d2d0 \ + --hash=sha256:29ff125e40c8016b4d3bf48e6d727bd93d2892451b47bfe57ba896944ecbdb0c \ + --hash=sha256:2ae866adf9b112c7ab30c1a90d878a5d6f2d40244fbc46ec8477425d802f4ac5 \ + --hash=sha256:2bf891c2d441b1edddfc62f16ab3031ac7506cba5b77680654179dbe93d8b7ec \ + --hash=sha256:2c88b0d356ea18ce026a52aa7c2110693db77fd52d5ac7553616635a7f165bbd \ + --hash=sha256:3090d9cabc2c31c92ae943f36d539a20adfd82697f50e996ce94f0b279e9e1e4 \ + --hash=sha256:3359a7c4eb4c281bf54bd760dcc03c43299f0d529dd2a5018be2f1fbebf0cbbd \ + --hash=sha256:375cca06f36f54339838c7f8251b64a777d5ff1277f8b91999487fad1a8e2d73 \ + --hash=sha256:386d8c4a5677fd94fd16c1af2adf39f59698af1e4ef0c3c026083f278540e352 \ + --hash=sha256:44365bb5098d0a91c44382e7df0ad44d5b9a588d29d9071aca4a2867f704aaf1 \ + --hash=sha256:443e502b428d325c57aec0947991857e8dc0589d0464181f35bd48f870cd5d18 \ + --hash=sha256:4550cbb6629c9f6186ab22cb497f262408e1a90ae10b8653599f2717f518f0df \ + --hash=sha256:4b8714e6eb37d973bdd231a46ce1c639417fe8035d4d3224f832e4a6afd05d5f \ + --hash=sha256:4cdcd8ae0b1017c5d7cac3595f1ac43ab9c0471cf700156d62d246248c734a35 \ + --hash=sha256:5ad583efb91402c0aada9cb4df0b6255607ceb83048dd2f629c09858594977ff \ + --hash=sha256:621c263f99274d51a9a1deecf301845f1408d497bdafed682db6155132a99cf4 \ + --hash=sha256:64860404663009a41a529f1f13c012e1ccf4a814b123581729f98c2b103ef362 \ + --hash=sha256:690d53df7b154d817d5b1dc5d24ba95045b670862005b8321f307f011a0738b2 \ + --hash=sha256:6dbea13f82de14b3db06cbc66e2060af4bf1070442398ddf42fa71ab188db627 \ + --hash=sha256:6f78d1ceac9545b87857c0e9b4e28a42fb98f8c6991d6b0e4099a012c35d2e73 \ + --hash=sha256:7068cae451be320c41b1442d5e2ec06dae050f1d3883918096f9cc3fcc791e89 \ + --hash=sha256:7afbd9bbe89866dbd9221f4c7e7321f4d0519772245d1b216b5ff1d50b8c0af7 \ + --hash=sha256:851981ded8bb42da66213cf95d1dd5eaf06c5d6b3a95725a18eddd58ec992b6b \ + --hash=sha256:852e538b4866ed776126349161d9fdc37387b1e15ab46805f98dcdee25fee4b5 \ + --hash=sha256:8b19d19661fc9a71f19b7c432c413ab65025e5dd11fbd192bd9169afb13b9ce0 \ + --hash=sha256:8e62d9c36bcfe6b85bec7b754accb97e2fa7b6a7c9a0c37f824d85eba699e7b8 \ + --hash=sha256:8eb0566b4501c27ea51f2e593531122703946969564fe526a5ff1b18be0ad49a \ + --hash=sha256:8f393607d54091e8976e297f8fd7399606d12cd8c2b8852353f07ad5ddac4224 \ + --hash=sha256:91df1fa70a99ac08dc449d6fda24a14992a1836d571928f217c40c66fd63fcc8 \ + --hash=sha256:98128abaa1bac8c4f60d08af641b981ba56486269532c03e99a1d48c9d6f9aa9 \ + --hash=sha256:98a7911f5ca3d6f809377fa8eaad8295687a106dd7bdd15624b267270d0da2ab \ + --hash=sha256:a07d4c466ad1b8c146dd7b893d2c2e735c3e530abfcef0c741a464001e828155 \ + --hash=sha256:a7c4aba79ef5fa41017d814016037f5ba29ecae889cea3d37108a4677ecb3aac \ + --hash=sha256:aa57de5929ada67d2f753442ce04dcc35561899558a1566f39f6e0c893cbc54b \ + --hash=sha256:ac3f7143da2d1f6087128d47d348ba0d92fe7f59ff476919f8d0f78fa5720f7b \ + --hash=sha256:ad18b3082a56119e0fb19ad4d47556ee24ce076466fff42b0d0a75a20d69a2e0 \ + --hash=sha256:b23462a349d4e7c6c77e8f6e735fb24dccdde14dd445c5eca76a9aaca7111f08 \ + --hash=sha256:b361152c24fd5c791220de9966b00696578c9884a2bb67e4759d4dfe05fd7049 \ + --hash=sha256:b51b5259dc344482ab21b768dfc7f54d51d9133665e72890831725068f69f24a \ + --hash=sha256:b57f367d7b0e1bc9e5c9b0ff34febdb3fe440f1fe8d75903ae71301bc06072c0 \ + --hash=sha256:b8dc65d2e7951b7e81c17b92857db7c19d6b3dd442d2d8600b5bd5040aa43ce6 \ + --hash=sha256:bc320e960347e5f9d27e8b4a9fc7044b2b26bfe4522bb4957e741fc1d1ebd2f0 \ + --hash=sha256:bdf77ba7a8987a239eb57cf840d62669741f8b92b61a617e63898caed31da898 \ + --hash=sha256:bf96b64400da2f411b43c4c81c2e20b09e3300d86766a52f42393696c8962f11 \ + --hash=sha256:c37f5dbfe95579549485c33167854784358f559feda703ccc058719ca0efd8aa \ + --hash=sha256:c41312c6edad5e875613719236f1ca6bba9310df40b1adc9308248e1bdb7a1ea \ + --hash=sha256:cafa1d43bcc69129a4855fd3973ab7983bb2274c407e5ff572af5dc196e00313 \ + --hash=sha256:cb98556e919be3e6ba9bca629dbddccfaa33b95a0fe7503052849b124e4f88cd \ + --hash=sha256:cd2e2c4dcc30c373e317d39b044afa6d9a090bec11d186f25841f70bc520bbb5 \ + --hash=sha256:e6c4fb1e7f51f8858f348ed9c31bb408b61274942d18b549ec163bb480c987a0 \ + --hash=sha256:eccab0281667caab0c055882b3bbb8e346bb0181e55f37477e3e5e3f5b7a96dd \ + --hash=sha256:f59f74c3779aa47223ba0a9e23ef10d2af5a873ed3624c78303f62a679d1b63e \ + --hash=sha256:f6b86112cca7bd04151ce248d781763ea5f74cc18d148476c6d16cee32db81ac \ + --hash=sha256:f837ab85ae93b118766b8b28a1cec47a1daddee303e1f986a595c56379062a5c # via happybase -tifffile==2025.10.4 \ - --hash=sha256:2e437c16ab211be5bcdc79f71b4907359115f1f83b5d919e7c297c29725d3e38 \ - --hash=sha256:7687d691e49026053181470cec70fa9250e3a586b2041041297e38b10bbd34e1 +tifffile==2026.7.14 \ + --hash=sha256:4eb20372e76edf2c9fed922b1e3a0a0567be3560bd2008336115763bb1f3c034 \ + --hash=sha256:ce2703e5ef22c868f1528d5f5b4ef75eefb019cf628a1c9ec0d17e0afeca8ef5 # via scikit-image -timm==1.0.20 \ - --hash=sha256:7468d32a410c359181c1ef961f49c7e213286e0c342bfb898b99534a4221fc54 \ - --hash=sha256:f6e62f780358476691996c47aa49de87b95cc507edf923c3042f74a07e45b7fe - # via feast (setup.py) -tinycss2==1.4.0 \ - --hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \ - --hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 +timm==1.0.28 \ + --hash=sha256:3789d313fdd5541a327b60180d70dbb4bdec73db8ff0655e413db3c3d134a9a4 \ + --hash=sha256:e577b88da96b3a722ea5e2f042455ce6f715d398304d8e63b17d126ed7d89968 + # via feast (pyproject.toml) +tinycss2==1.5.1 \ + --hash=sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661 \ + --hash=sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957 # via bleach -tokenizers==0.22.1 \ - --hash=sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a \ - --hash=sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446 \ - --hash=sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7 \ - --hash=sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73 \ - --hash=sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a \ - --hash=sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9 \ - --hash=sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138 \ - --hash=sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc \ - --hash=sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390 \ - --hash=sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f \ - --hash=sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82 \ - --hash=sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879 \ - --hash=sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21 \ - --hash=sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4 \ - --hash=sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214 +tokenizers==0.22.2 \ + --hash=sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113 \ + --hash=sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37 \ + --hash=sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e \ + --hash=sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001 \ + --hash=sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee \ + --hash=sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7 \ + --hash=sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd \ + --hash=sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4 \ + --hash=sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012 \ + --hash=sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67 \ + --hash=sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a \ + --hash=sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5 \ + --hash=sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917 \ + --hash=sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c \ + --hash=sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195 \ + --hash=sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4 \ + --hash=sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a \ + --hash=sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc \ + --hash=sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92 \ + --hash=sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5 \ + --hash=sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48 \ + --hash=sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b \ + --hash=sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c \ + --hash=sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5 # via transformers toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via fastapi-mcp -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via - # poetry-dynamic-versioning - # snowflake-connector-python -toolz==0.12.1 \ - --hash=sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85 \ - --hash=sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 + # via snowflake-connector-python +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # altair # dask # ibis-framework # partd -torch==2.8.0 \ - --hash=sha256:06fcee8000e5c62a9f3e52a688b9c5abb7c6228d0e56e3452983416025c41381 \ - --hash=sha256:0be92c08b44009d4131d1ff7a8060d10bafdb7ddcb7359ef8d8c5169007ea905 \ - --hash=sha256:1a62a1ec4b0498930e2543535cf70b1bef8c777713de7ceb84cd79115f553767 \ - --hash=sha256:220a06fd7af8b653c35d359dfe1aaf32f65aa85befa342629f716acb134b9710 \ - --hash=sha256:2b2f96814e0345f5a5aed9bf9734efa913678ed19caf6dc2cddb7930672d6128 \ - --hash=sha256:2f4ac52f0130275d7517b03a33d2493bab3693c83dcfadf4f81688ea82147d2e \ - --hash=sha256:5128fe752a355d9308e56af1ad28b15266fe2da5948660fad44de9e3a9e36e8c \ - --hash=sha256:5ae0524688fb6707c57a530c2325e13bb0090b745ba7b4a2cd6a3ce262572916 \ - --hash=sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0 \ - --hash=sha256:65616ca8ec6f43245e1f5f296603e33923f4c30f93d65e103d9e50c25b35150b \ - --hash=sha256:659df54119ae03e83a800addc125856effda88b016dfc54d9f65215c3975be16 \ - --hash=sha256:7b677e17f5a3e69fdef7eb3b9da72622f8d322692930297e4ccb52fefc6c8211 \ - --hash=sha256:83c13411a26fac3d101fe8035a6b0476ae606deb8688e904e796a3534c197def \ - --hash=sha256:89aa9ee820bb39d4d72b794345cccef106b574508dd17dbec457949678c76011 \ - --hash=sha256:8c7ef765e27551b2fbfc0f41bcf270e1292d9bf79f8e0724848b1682be6e80aa \ - --hash=sha256:8f0a9d617a66509ded240add3754e462430a6c1fc5589f86c17b433dd808f97a \ - --hash=sha256:a3f16a58a9a800f589b26d47ee15aca3acf065546137fc2af039876135f4c760 \ - --hash=sha256:a7242b86f42be98ac674b88a4988643b9bc6145437ec8f048fea23f72feb5eca \ - --hash=sha256:b2aca0939fb7e4d842561febbd4ffda67a8e958ff725c1c27e244e85e982173c \ - --hash=sha256:c12fa219f51a933d5f80eeb3a7a5d0cbe9168c0a14bbb4055f1979431660879b \ - --hash=sha256:da6afa31c13b669d4ba49d8a2169f0db2c3ec6bec4af898aa714f401d4c38904 \ - --hash=sha256:e2fab4153768d433f8ed9279c8133a114a034a61e77a3a104dcdf54388838705 \ - --hash=sha256:e8e5bf982e87e2b59d932769938b698858c64cc53753894be25629bdf5cf2f46 \ - --hash=sha256:e9f071f5b52a9f6970dc8a919694b27a91ae9dc08898b2b988abbef5eddfd1ae - # via - # feast (setup.py) +torch==2.13.0 \ + --hash=sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d \ + --hash=sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c \ + --hash=sha256:0ab4b69f3ee03a62a002cfbf77b1ca5e88aceb4ea64cb4388bb28f638ddbb045 \ + --hash=sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005 \ + --hash=sha256:2bd30b6b730d987fa386ce3898933762c5cb8cc82eb0535211d787cc3ce2dfeb \ + --hash=sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027 \ + --hash=sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc \ + --hash=sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09 \ + --hash=sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6 \ + --hash=sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2 \ + --hash=sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd \ + --hash=sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4 \ + --hash=sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7 \ + --hash=sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b \ + --hash=sha256:94f0de129916f77b8dc2c7a8eff644cfeddfe59e39c9f55e9f6e17543410281d \ + --hash=sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330 \ + --hash=sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c \ + --hash=sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e \ + --hash=sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8 \ + --hash=sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1 \ + --hash=sha256:c78b7b4d04461855a764cf01bae9a462bb88bc93defcfa11235cbc8fdf3e12c4 \ + --hash=sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92 \ + --hash=sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c \ + --hash=sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8 + # via + # feast (pyproject.toml) # accelerate # docling-ibm-models # easyocr # safetensors + # sentence-transformers # timm # torchvision -torchvision==0.23.0 \ - --hash=sha256:01dc33ee24c79148aee7cdbcf34ae8a3c9da1674a591e781577b716d233b1fa6 \ - --hash=sha256:07d069cb29691ff566e3b7f11f20d91044f079e1dbdc9d72e0655899a9b06938 \ - --hash=sha256:09bfde260e7963a15b80c9e442faa9f021c7e7f877ac0a36ca6561b367185013 \ - --hash=sha256:1c37e325e09a184b730c3ef51424f383ec5745378dc0eca244520aca29722600 \ - --hash=sha256:2a3299d2b1d5a7aed2d3b6ffb69c672ca8830671967eb1cee1497bacd82fe47b \ - --hash=sha256:2df618e1143805a7673aaf82cb5720dd9112d4e771983156aaf2ffff692eebf9 \ - --hash=sha256:2f7fd6c15f3697e80627b77934f77705f3bc0e98278b989b2655de01f6903e1d \ - --hash=sha256:31c583ba27426a3a04eca8c05450524105c1564db41be6632f7536ef405a6de2 \ - --hash=sha256:35c27941831b653f5101edfe62c03d196c13f32139310519e8228f35eae0e96a \ - --hash=sha256:3932bf67256f2d095ce90a9f826f6033694c818856f4bb26794cf2ce64253e53 \ - --hash=sha256:49aa20e21f0c2bd458c71d7b449776cbd5f16693dd5807195a820612b8a229b7 \ - --hash=sha256:4e7d31c43bc7cbecbb1a5652ac0106b436aa66e26437585fc2c4b2cf04d6014c \ - --hash=sha256:6c74cbc1cbee26dd4f35f989cd80dccc40411f258dee476b29871dee4b483af0 \ - --hash=sha256:6dd7c4d329a0e03157803031bc856220c6155ef08c26d4f5bbac938acecf0948 \ - --hash=sha256:7266871daca00ad46d1c073e55d972179d12a58fa5c9adec9a3db9bbed71284a \ - --hash=sha256:76bc4c0b63d5114aa81281390f8472a12a6a35ce9906e67ea6044e5af4cab60c \ - --hash=sha256:83ee5bf827d61a8af14620c0a61d8608558638ac9c3bac8adb7b27138e2147d1 \ - --hash=sha256:a2e45272abe7b8bf0d06c405e78521b5757be1bd0ed7e5cd78120f7fdd4cbf35 \ - --hash=sha256:a76fafe113b2977be3a21bf78f115438c1f88631d7a87203acb3dd6ae55889e6 \ - --hash=sha256:a9e9d7552d34547b80843eaf64ab0737b19b2e8bec2514286b8cfd30861ca8b5 \ - --hash=sha256:b190db205f90206c230fc2f91cbdfd5733334babc0e0d19bddb90a40b8cf26c2 \ - --hash=sha256:b9e2dabf0da9c8aa9ea241afb63a8f3e98489e706b22ac3f30416a1be377153b \ - --hash=sha256:dc7ce5accbbb8c9df9a79f8cef6a6df042f28e2250a6ae0d2ca70b06473fa03b \ - --hash=sha256:e0e2c04a91403e8dd3af9756c6a024a1d9c0ed9c0d592a8314ded8f4fe30d440 - # via - # feast (setup.py) +torchvision==0.28.0 \ + --hash=sha256:028a3d481b37d785605620d7cdad897064c5a55bae2aa1f2658766333e291940 \ + --hash=sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49 \ + --hash=sha256:2a1ef4b6f4bf5828b48cfad97372c8982db906830884b2868ba5c3df937a7d81 \ + --hash=sha256:3557cc7b539f46dabcda2b6f2b14017ccbeef024de466d4fc5835fc3f287f769 \ + --hash=sha256:36beb0782976906069ca03d4c9aacaf4b6b838b06ed6c20960ea9c51cce7acdd \ + --hash=sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b \ + --hash=sha256:46f581979c010ad6da6bd85ee602aa707e1ff44312670223b7a0ee517ad06d47 \ + --hash=sha256:546fd85345cf8652f6cd099d4f9884b0ca5c2f3fae78689a21dd2f35ea6b622f \ + --hash=sha256:5a38bc6da3d72621be003400b66f66a2b4c6d644fde05f680c2cb7ca8cf8dd6c \ + --hash=sha256:5cf78ebc401ce64ae19b8c55de866bb836797d559a4de9c25ccbe74cfa642d3a \ + --hash=sha256:62c7d110f86a039245b587e4fae60278c649f3bd42ff79cfbc1178eca4e72542 \ + --hash=sha256:6dfb0f45e2b4ceb4e76f158c3fbb5f44387099f3c466e3423a09ab665a194aba \ + --hash=sha256:7e80f543b22503d9415e126db5f0ff3917036925e38560ee6b9ae38c571a4002 \ + --hash=sha256:7e9dd6f60d6e15f8dc27d4f877fdb6002fc70d70272412135f1c2ff9cfa08d3b \ + --hash=sha256:7fad44dc9582570c7d92c4487d36ac46998f40cc39b438e8b8f5111a935ce4e8 \ + --hash=sha256:83fe6c020866a85acd7d97deccc45ff11d66daf42916d04396a4309c66c0ccb8 \ + --hash=sha256:87dc16b2df427c1318ad335f1e2be2b3b15b2cf20f7934c83b0505a48425ee5d \ + --hash=sha256:89f90e29b0966352811b12589f3a3c61943bf2bb9487b9d7bbec10efb1096bb5 \ + --hash=sha256:904cf89af220f8c6b2ed0296bb5065b474ce43b77558e48b2bf9de8b0ba17204 \ + --hash=sha256:9a45ea67235d965ef52187130d20002a4de20c54ea3d927a24286961d268dc37 \ + --hash=sha256:ad7b3a439265cc3739a4ab5b4c998c0e38ea99c0ee7ca4dea35c5d0b099ec237 \ + --hash=sha256:bb6dd6918460ed89cc7644adcc2402991474d6933cf1ce92b390641cb233fddf \ + --hash=sha256:d483b4aa3f5237569053f749cd1a2b5bb548ca456e40461a5dd087f21149d123 \ + --hash=sha256:e9f54c30cd52e3ef7fd034cc69b7bb7e0964e1c8f8743e018ab92e95b40f9eee + # via + # feast (pyproject.toml) # docling-ibm-models # easyocr # timm -tornado==6.5.2 \ - --hash=sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c \ - --hash=sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6 \ - --hash=sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef \ - --hash=sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4 \ - --hash=sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0 \ - --hash=sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e \ - --hash=sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882 \ - --hash=sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04 \ - --hash=sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0 \ - --hash=sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af \ - --hash=sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f \ - --hash=sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108 +tornado==6.5.7 \ + --hash=sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163 \ + --hash=sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2 \ + --hash=sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92 \ + --hash=sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b \ + --hash=sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972 \ + --hash=sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100 \ + --hash=sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4 \ + --hash=sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5 \ + --hash=sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4 \ + --hash=sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796 # via # ipykernel # jupyter-client @@ -5828,27 +6653,28 @@ tornado==6.5.2 \ # jupyterlab # notebook # terminado -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 # via - # feast (setup.py) + # feast (pyproject.toml) # datasets # docling # docling-ibm-models # great-expectations # huggingface-hub - # milvus-lite # mpire # semchunk + # sentence-transformers # transformers -traitlets==5.14.3 \ - --hash=sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7 \ - --hash=sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f +traitlets==5.15.1 \ + --hash=sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92 \ + --hash=sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722 # via # ipykernel # ipython # ipywidgets + # jupyter-builder # jupyter-client # jupyter-core # jupyter-events @@ -5858,21 +6684,114 @@ traitlets==5.14.3 \ # nbclient # nbconvert # nbformat -transformers==4.57.0 \ - --hash=sha256:9d7c6d098c026e40d897e017ed1f481ab803cbac041021dbc6ae6100e4949b55 \ - --hash=sha256:d045753f3d93f9216e693cdb168698dfd2e9d3aad1bb72579a5d60ebf1545a8b +transformers==4.57.6 \ + --hash=sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550 \ + --hash=sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3 # via - # feast (setup.py) + # feast (pyproject.toml) # docling-core # docling-ibm-models -trino==0.336.0 \ - --hash=sha256:389150841446949119c3c2c13c1a51bb4be1a27818e40ae40dd3701f36c02550 \ - --hash=sha256:e82339e9fffe5c6c51de3bfdf28f083e3ae5945a4502739ab2094a0d08d68070 - # via feast (setup.py) -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) + # sentence-transformers +tree-sitter==0.26.0 \ + --hash=sha256:00289bfe7978f3e0dc0ce69813a20fa9f44ea4c100b3ec62043e5eb74ccfc3a2 \ + --hash=sha256:0f8793fd18ad7eec276ed4b51c097b4bf2002b357259b66b0d75db1f3f41c754 \ + --hash=sha256:10f0d4eb94aa7242dcb7f554bcd24dd7ba1c114f00d58759ba08c7a46c8ec51a \ + --hash=sha256:17a1c5cfd3a05d5c7c86bf4282b6ef8092c91dc0a98390499669c3fedb7d1814 \ + --hash=sha256:1d6fe0e8fb4df77b5ee816228e2c4475a63d8cc1d4d3a7ffd7097b2b87fc3e95 \ + --hash=sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90 \ + --hash=sha256:26c996c1edfee86e977bb3f5462e74fcec0d0b0db1e85a3c475875763caa03be \ + --hash=sha256:2f941cea06128c1f74f8937a8e2a90c7db49cf4be6647cd9e07d92a306d91517 \ + --hash=sha256:30a88be89ff1f2755297f81e8080d88b795dd98720c3f9fa2acf93873182cc95 \ + --hash=sha256:335294ce0504fcefde5245dff596778ffaf820205b98ae0b549c72e48855f1d8 \ + --hash=sha256:3f3c44339dd34fe8eb2b8d5aa7610660499a795f70376b130bbee7a437337280 \ + --hash=sha256:514a9bf8993e5210e7970736aaf6020d1759b670e195ef17b1c48f586aa30736 \ + --hash=sha256:526a165a2cb1d1f79e247d400f0e0acd8d49a817d6f312d543513af200b1f886 \ + --hash=sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa \ + --hash=sha256:5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4 \ + --hash=sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3 \ + --hash=sha256:6189c6c340c7384357711e3d92645e96bfb79f7a502f86de1ebdb23eb43f7dab \ + --hash=sha256:6cb2bd20efb2544c19ac54486ab7cb8ec7b36f913bbe1ce95df84acb96743d9c \ + --hash=sha256:7075ef857ef86f327dbb72d1e2574dda78db5754b3a1fca6506acd7fe5d561a7 \ + --hash=sha256:763627db05db34f12333081bd7422cc1c675893d373cc870b3e9249e200700e4 \ + --hash=sha256:7bcbadfa614326debef581957d5c780a9d7f66065c13deea61aa21d1dd36263f \ + --hash=sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52 \ + --hash=sha256:8ff2e0750b7daa722302838356d7b65e303829b7eb73c915df127ddba115e1d1 \ + --hash=sha256:918d89529786873f0982a0f59c2a303cd065fbfd1b903d71a8e4e1584f67b42e \ + --hash=sha256:93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f \ + --hash=sha256:94550e13b6ae576969da40246f4c4abb206380b5375ad43f26dd9151d55438e3 \ + --hash=sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c \ + --hash=sha256:b31a8195d2f224224c530ac814632d98c1dcc123d227442c07c736e86b70d564 \ + --hash=sha256:b40c219edccc4564530c96f8f1556f6202b37cda964d1cbd7bd2b7e68b40a245 \ + --hash=sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084 \ + --hash=sha256:bc6cb01d5ee75c85424aa1f1c72a82d8f07fd52539a0f3c4a6ed3e8721079b84 \ + --hash=sha256:c56581ad256c4195a21bfe449fed5d44a02fe83a4a7d6e70e6ec302c881191c7 \ + --hash=sha256:ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37 \ + --hash=sha256:dea4b4e27d49e9ec5b785d4f994da000e6726882fcc6ad05ec98478500c71aef \ + --hash=sha256:e9e46b664887d8c1014f1fb33e09454bbdd9ec1fe29b7fd02dde7b46bc1bb81a \ + --hash=sha256:ed0889dbed843ce45ede9f5169c0b2dea2222f12685844a03fadb81f12705867 \ + --hash=sha256:f289be0225ba2ace8e87d6c9639b2bc9ff2b5271afb7c5d39282a4a00e248682 \ + --hash=sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c \ + --hash=sha256:f9997ba61368c48ed54e715676afadf703947a1542464e39d047764fb3624b01 \ + --hash=sha256:ff527388df14cb5009f9274faf78cc69a7393ae6acf3b04784b8acca249519c5 \ + --hash=sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa + # via docling-core +tree-sitter-c==0.24.2 \ + --hash=sha256:1628584df0299b5a340aa63f8e67b6c97c91517f52fa7e7a4c557e40adb330a9 \ + --hash=sha256:4a2f4371cd816cc3153458f69062135ebb2ea5f275ddd90494e5c823d778204a \ + --hash=sha256:4d4579a8b54f0a442f903d88d3304cab77cd5c2031d4015baa4f2f8e15d6dcb7 \ + --hash=sha256:5041ef67eb68ce6bc8bb0b1f8ef3a5585ce523dae0c7eec109ab0627dd75aede \ + --hash=sha256:82842c5a5f2acd93f4de10038c33ac179c8979defc39376f990348d6289e933b \ + --hash=sha256:97bc80a224d48215d4e6e6376bf30d114f4c317b8145ff1b02afe785d4ba7bdd \ + --hash=sha256:abb549225091f7b25df2dd3a0143ece6e208f7055d8bcb4700b41ee79b9ef1e1 \ + --hash=sha256:c098bedcd5ac86ff93fa734d51d1dd86aed40fd5ed7d634c7af11380a0469969 \ + --hash=sha256:e2b42e8e22202c251f8629306f9321233542e07a6e01611b5fe83489272143eb + # via docling-core +tree-sitter-javascript==0.25.0 \ + --hash=sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b \ + --hash=sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54 \ + --hash=sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38 \ + --hash=sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b \ + --hash=sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1 \ + --hash=sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75 \ + --hash=sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc \ + --hash=sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc \ + --hash=sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c + # via docling-core +tree-sitter-python==0.25.0 \ + --hash=sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb \ + --hash=sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361 \ + --hash=sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762 \ + --hash=sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683 \ + --hash=sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5 \ + --hash=sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76 \ + --hash=sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac \ + --hash=sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b \ + --hash=sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d + # via docling-core +tree-sitter-typescript==0.23.2 \ + --hash=sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7 \ + --hash=sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478 \ + --hash=sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9 \ + --hash=sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31 \ + --hash=sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d \ + --hash=sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0 \ + --hash=sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8 \ + --hash=sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c + # via docling-core +trino==0.338.0 \ + --hash=sha256:093952f8a53b6f47ab357b77cbe92f1d66453290f7d8bcc4b74bd662e5ff2704 \ + --hash=sha256:1103d249cb96ba8f88f699588581965e2be088c16b9f8178e34cb64aa8a6c011 + # via feast (pyproject.toml) +truststore==0.10.4 \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 + # via + # httpcore2 + # httpx2 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) typer==0.12.5 \ --hash=sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b \ --hash=sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722 @@ -5880,65 +6799,65 @@ typer==0.12.5 \ # docling # docling-core # fastapi-mcp -types-cffi==1.17.0.20250915 \ - --hash=sha256:4362e20368f78dabd5c56bca8004752cc890e07a71605d9e0d9e069dbaac8c06 \ - --hash=sha256:cef4af1116c83359c11bb4269283c50f0688e9fc1d7f0eeb390f3661546da52c +types-cffi==2.0.0.20260518 \ + --hash=sha256:5b68a215a95d0eac4203b58e766ff7fe40c2e091b1fa1a9e54111f04cc560084 \ + --hash=sha256:f9707e66c13454789a58f8843d1ded4a66f1e9c8b10bd24d5eb5e0f25c0c5472 # via types-pyopenssl types-protobuf==3.19.22 \ --hash=sha256:d291388678af91bb045fafa864f142dc4ac22f5d4cdca097c7d8d8a32fa9b3ab \ --hash=sha256:d2b26861b0cb46a3c8669b0df507b7ef72e487da66d61f9f3576aa76ce028a83 # via - # feast (setup.py) + # feast (pyproject.toml) # mypy-protobuf -types-pymysql==1.1.0.20250916 \ - --hash=sha256:873eb9836bb5e3de4368cc7010ca72775f86e9692a5c7810f8c7f48da082e55b \ - --hash=sha256:98d75731795fcc06723a192786662bdfa760e1e00f22809c104fbb47bac5e29b - # via feast (setup.py) +types-pymysql==1.2.0.20260724 \ + --hash=sha256:71327a3fea0f680a280445f1b475c2307f68de05c2d3302a79df56af93c574cd \ + --hash=sha256:8493cf87e8df0a7b4e8c84305bc0aae69d3a8af9c6da7bfbb9a4c5c3f97d4a82 + # via feast (pyproject.toml) types-pyopenssl==24.1.0.20240722 \ --hash=sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39 \ --hash=sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54 # via types-redis -types-python-dateutil==2.9.0.20251008 \ - --hash=sha256:b9a5232c8921cf7661b29c163ccc56055c418ab2c6eabe8f917cbcc73a4c4157 \ - --hash=sha256:c3826289c170c93ebd8360c3485311187df740166dbab9dd3b792e69f2bc1f9c - # via - # feast (setup.py) - # arrow -types-pytz==2025.2.0.20250809 \ - --hash=sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5 \ - --hash=sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db - # via feast (setup.py) -types-pyyaml==6.0.12.20250915 \ - --hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \ - --hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6 - # via feast (setup.py) +types-python-dateutil==2.9.0.20260716 \ + --hash=sha256:1ae41d51a5c5f6bbeeb7f1f34df7086d55ff1605cf88d2ed71a7a276e1a7794e \ + --hash=sha256:1d55d1c3024bdb4861bb6a6622c9ec800c433d87bdc5b16fb84cdd0eed4ef2cb + # via feast (pyproject.toml) +types-pytz==2026.3.1.20260727 \ + --hash=sha256:42ac44e83645bfceb46597342c8fb8ec028a1407e2feae9c5c539986eab6b57a \ + --hash=sha256:4364075b6867dd15b210bb8c1d29727d609917129b45600defe1d4b3eda5ecb9 + # via feast (pyproject.toml) +types-pyyaml==6.0.12.20260724 \ + --hash=sha256:3c1ce1bb73cd5ec02e90390c2b1f00e810d241d8825fd73ff359696839271b6b \ + --hash=sha256:d57db930a4b2efbc57cf430ec8882765d246929432fa253092f383902329a453 + # via feast (pyproject.toml) types-redis==4.6.0.20241004 \ --hash=sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e \ --hash=sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed - # via feast (setup.py) + # via feast (pyproject.toml) types-requests==2.30.0.0 \ --hash=sha256:c6cf08e120ca9f0dc4fa4e32c3f953c3fba222bcc1db6b97695bce8da1ba9864 \ --hash=sha256:dec781054324a70ba64430ae9e62e7e9c8e4618c185a5cb3f87a6738251b5a31 - # via feast (setup.py) -types-setuptools==80.9.0.20250822 \ - --hash=sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965 \ - --hash=sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3 + # via feast (pyproject.toml) +types-setuptools==83.0.0.20260724 \ + --hash=sha256:eed1a91e1ed9d8ec9f3e71908e5bb17272e41e0852d37d1485ecd9cd33165e1a \ + --hash=sha256:fe2801176b667f1e8209f8571b0839ca6c34318d743825aeacf3f29970c8d46f # via - # feast (setup.py) + # feast (pyproject.toml) # types-cffi -types-tabulate==0.9.0.20241207 \ - --hash=sha256:ac1ac174750c0a385dfd248edc6279fa328aaf4ea317915ab879a2ec47833230 \ - --hash=sha256:b8dad1343c2a8ba5861c5441370c3e35908edd234ff036d4298708a1d4cf8a85 - # via feast (setup.py) +types-tabulate==0.10.0.20260508 \ + --hash=sha256:8e51f159e8b24976849706ae2ed1dc9adba8ebbd080b17e494ebb66a8cc92c74 \ + --hash=sha256:b1e1a2d0456fbd655a71690b09a7aaeffdf2978d32049184ea436492aa51d20a + # via feast (pyproject.toml) types-urllib3==1.26.25.14 \ --hash=sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f \ --hash=sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e # via types-requests -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiohttp # aiosignal + # alembic # anyio # azure-core # azure-identity @@ -5947,15 +6866,23 @@ typing-extensions==4.15.0 \ # docling-core # elasticsearch # fastapi + # graphene # great-expectations + # grpcio + # httpx2 # huggingface-hub # ibis-framework + # jupyter-client # jwcrypto + # mcp + # mcp-types # minio + # mlflow-skinny # mypy # opentelemetry-api # opentelemetry-sdk # opentelemetry-semantic-conventions + # oracledb # psycopg # psycopg-pool # pydantic @@ -5964,6 +6891,7 @@ typing-extensions==4.15.0 \ # python-docx # python-pptx # referencing + # sentence-transformers # snowflake-connector-python # sqlalchemy # starlette @@ -5975,296 +6903,235 @@ typing-extensions==4.15.0 \ typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via pydantic-settings -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via pandas -tzlocal==5.3.1 \ - --hash=sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd \ - --hash=sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via + # arrow + # ibis-framework + # pandas +tzlocal==5.4.4 \ + --hash=sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4 \ + --hash=sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15 # via # great-expectations # trino -ujson==5.11.0 \ - --hash=sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80 \ - --hash=sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef \ - --hash=sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a \ - --hash=sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840 \ - --hash=sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53 \ - --hash=sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076 \ - --hash=sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab \ - --hash=sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d \ - --hash=sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89 \ - --hash=sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c \ - --hash=sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb \ - --hash=sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c \ - --hash=sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823 \ - --hash=sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5 \ - --hash=sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac \ - --hash=sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d \ - --hash=sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723 \ - --hash=sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6 \ - --hash=sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0 \ - --hash=sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328 \ - --hash=sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0 \ - --hash=sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04 \ - --hash=sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25 \ - --hash=sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49 \ - --hash=sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec \ - --hash=sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3 \ - --hash=sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3 \ - --hash=sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc \ - --hash=sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a \ - --hash=sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9 \ - --hash=sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302 \ - --hash=sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694 \ - --hash=sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547 \ - --hash=sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01 \ - --hash=sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02 \ - --hash=sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6 \ - --hash=sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60 \ - --hash=sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915 \ - --hash=sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835 \ - --hash=sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701 \ - --hash=sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702 \ - --hash=sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50 \ - --hash=sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6 \ - --hash=sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc \ - --hash=sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a \ - --hash=sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c \ - --hash=sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c \ - --hash=sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03 \ - --hash=sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629 \ - --hash=sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105 \ - --hash=sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844 \ - --hash=sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241 \ - --hash=sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a \ - --hash=sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26 \ - --hash=sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac \ - --hash=sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596 \ - --hash=sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9 \ - --hash=sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752 \ - --hash=sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138 \ - --hash=sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b \ - --hash=sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba \ - --hash=sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5 \ - --hash=sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018 \ - --hash=sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362 \ - --hash=sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746 \ - --hash=sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f \ - --hash=sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88 \ - --hash=sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638 \ - --hash=sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b \ - --hash=sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9 \ - --hash=sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db \ - --hash=sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f \ - --hash=sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58 \ - --hash=sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b \ - --hash=sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433 \ - --hash=sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0 \ - --hash=sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764 \ - --hash=sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c \ - --hash=sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34 \ - --hash=sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052 \ - --hash=sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba \ - --hash=sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c \ - --hash=sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc \ - --hash=sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39 - # via pymilvus uri-template==1.3.0 \ --hash=sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7 \ --hash=sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363 # via jsonschema -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via - # feast (setup.py) + # feast (pyproject.toml) # botocore # clickhouse-connect + # databricks-sdk # docker # elastic-transport # great-expectations + # kube-authkit # kubernetes # minio # qdrant-client # requests # responses # testcontainers -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # mcp + # mlflow-skinny # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn virtualenv==20.23.0 \ --hash=sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e \ --hash=sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924 # via - # feast (setup.py) + # feast (pyproject.toml) # pre-commit # ray -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn -wcwidth==0.2.14 \ - --hash=sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605 \ - --hash=sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 - # via prompt-toolkit -webcolors==24.11.1 \ - --hash=sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9 \ - --hash=sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6 +wcwidth==0.8.2 \ + --hash=sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda \ + --hash=sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85 + # via + # prettytable + # prompt-toolkit +webcolors==25.10.0 \ + --hash=sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d \ + --hash=sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf # via jsonschema webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ @@ -6278,462 +7145,536 @@ websocket-client==1.9.0 \ # via # jupyter-server # kubernetes -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -werkzeug==3.1.3 \ - --hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \ - --hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746 - # via moto -wheel==0.45.1 \ - --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ - --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 +werkzeug==3.1.8 \ + --hash=sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50 \ + --hash=sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44 + # via + # flask + # flask-cors + # moto +wheel==0.47.0 \ + --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \ + --hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3 # via # pip-tools # singlestoredb -widgetsnbextension==4.0.14 \ - --hash=sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575 \ - --hash=sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af +widgetsnbextension==4.0.15 \ + --hash=sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366 \ + --hash=sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9 # via ipywidgets -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 # via # aiobotocore + # deprecated # smart-open # testcontainers xlsxwriter==3.2.9 \ --hash=sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c \ --hash=sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3 # via python-pptx -xmltodict==1.0.2 \ - --hash=sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649 \ - --hash=sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d +xmltodict==1.0.4 \ + --hash=sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61 \ + --hash=sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a # via moto -xxhash==3.6.0 \ - --hash=sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad \ - --hash=sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c \ - --hash=sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3 \ - --hash=sha256:01be0c5b500c5362871fc9cfdf58c69b3e5c4f531a82229ddb9eb1eb14138004 \ - --hash=sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b \ - --hash=sha256:02ea4cb627c76f48cd9fb37cf7ab22bd51e57e1b519807234b473faebe526796 \ - --hash=sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f \ - --hash=sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c \ - --hash=sha256:0d50101e57aad86f4344ca9b32d091a2135a9d0a4396f19133426c88025b09f1 \ - --hash=sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1 \ - --hash=sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0 \ - --hash=sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec \ - --hash=sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d \ - --hash=sha256:18b242455eccdfcd1fa4134c431a30737d2b4f045770f8fe84356b3469d4b919 \ - --hash=sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67 \ - --hash=sha256:1fc1ed882d1e8df932a66e2999429ba6cc4d5172914c904ab193381fba825360 \ - --hash=sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799 \ - --hash=sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679 \ - --hash=sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef \ - --hash=sha256:2762bfff264c4e73c0e507274b40634ff465e025f0eaf050897e88ec8367575d \ - --hash=sha256:277175a73900ad43a8caeb8b99b9604f21fe8d7c842f2f9061a364a7e220ddb7 \ - --hash=sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8 \ - --hash=sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa \ - --hash=sha256:2ab89a6b80f22214b43d98693c30da66af910c04f9858dd39c8e570749593d7e \ - --hash=sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa \ - --hash=sha256:2f171a900d59d51511209f7476933c34a0c2c711078d3c80e74e0fe4f38680ec \ - --hash=sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4 \ - --hash=sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad \ - --hash=sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7 \ - --hash=sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5 \ - --hash=sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11 \ - --hash=sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae \ - --hash=sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d \ - --hash=sha256:44e342e8cc11b4e79dae5c57f2fb6360c3c20cc57d32049af8f567f5b4bcb5f4 \ - --hash=sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6 \ - --hash=sha256:45aae0c9df92e7fa46fbb738737324a563c727990755ec1965a6a339ea10a1df \ - --hash=sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058 \ - --hash=sha256:4903530e866b7a9c1eadfd3fa2fbe1b97d3aed4739a80abf506eb9318561c850 \ - --hash=sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2 \ - --hash=sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d \ - --hash=sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89 \ - --hash=sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e \ - --hash=sha256:4da8168ae52c01ac64c511d6f4a709479da8b7a4a1d7621ed51652f93747dffa \ - --hash=sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6 \ - --hash=sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb \ - --hash=sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3 \ - --hash=sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b \ - --hash=sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4 \ - --hash=sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db \ - --hash=sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119 \ - --hash=sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec \ - --hash=sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518 \ - --hash=sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296 \ - --hash=sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033 \ - --hash=sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729 \ - --hash=sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca \ - --hash=sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063 \ - --hash=sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5 \ - --hash=sha256:6551880383f0e6971dc23e512c9ccc986147ce7bfa1cd2e4b520b876c53e9f3d \ - --hash=sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f \ - --hash=sha256:6965e0e90f1f0e6cb78da568c13d4a348eeb7f40acfd6d43690a666a459458b8 \ - --hash=sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42 \ - --hash=sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e \ - --hash=sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392 \ - --hash=sha256:780b90c313348f030b811efc37b0fa1431163cb8db8064cf88a7936b6ce5f222 \ - --hash=sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f \ - --hash=sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd \ - --hash=sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77 \ - --hash=sha256:7c35c4cdc65f2a29f34425c446f2f5cdcd0e3c34158931e1cc927ece925ab802 \ - --hash=sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d \ - --hash=sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1 \ - --hash=sha256:7dac94fad14a3d1c92affb661021e1d5cbcf3876be5f5b4d90730775ccb7ac41 \ - --hash=sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374 \ - --hash=sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263 \ - --hash=sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71 \ - --hash=sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13 \ - --hash=sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8 \ - --hash=sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc \ - --hash=sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62 \ - --hash=sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11 \ - --hash=sha256:9085e798c163ce310d91f8aa6b325dda3c2944c93c6ce1edb314030d4167cc65 \ - --hash=sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0 \ - --hash=sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b \ - --hash=sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2 \ - --hash=sha256:97460eec202017f719e839a0d3551fbc0b2fcc9c6c6ffaa5af85bbd5de432788 \ - --hash=sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6 \ - --hash=sha256:9e040d3e762f84500961791fa3709ffa4784d4dcd7690afc655c095e02fff05f \ - --hash=sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc \ - --hash=sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e \ - --hash=sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702 \ - --hash=sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405 \ - --hash=sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f \ - --hash=sha256:a75ffc1bd5def584129774c158e108e5d768e10b75813f2b32650bb041066ed6 \ - --hash=sha256:a87f271a33fad0e5bf3be282be55d78df3a45ae457950deb5241998790326f87 \ - --hash=sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3 \ - --hash=sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a \ - --hash=sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b \ - --hash=sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b \ - --hash=sha256:b0359391c3dad6de872fefb0cf5b69d55b0655c55ee78b1bb7a568979b2ce96b \ - --hash=sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8 \ - --hash=sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db \ - --hash=sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99 \ - --hash=sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a \ - --hash=sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2 \ - --hash=sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204 \ - --hash=sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b \ - --hash=sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546 \ - --hash=sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95 \ - --hash=sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9 \ - --hash=sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54 \ - --hash=sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06 \ - --hash=sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c \ - --hash=sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152 \ - --hash=sha256:c2f9ccd5c4be370939a2e17602fbc49995299203da72a3429db013d44d590e86 \ - --hash=sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4 \ - --hash=sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93 \ - --hash=sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd \ - --hash=sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd \ - --hash=sha256:cc604dc06027dbeb8281aeac5899c35fcfe7c77b25212833709f0bff4ce74d2a \ - --hash=sha256:cfbc5b91397c8c2972fdac13fb3e4ed2f7f8ccac85cd2c644887557780a9b6e2 \ - --hash=sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248 \ - --hash=sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd \ - --hash=sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6 \ - --hash=sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf \ - --hash=sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7 \ - --hash=sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490 \ - --hash=sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0 \ - --hash=sha256:e4ff728a2894e7f436b9e94c667b0f426b9c74b71f900cf37d5468c6b5da0536 \ - --hash=sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb \ - --hash=sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829 \ - --hash=sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746 \ - --hash=sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07 \ - --hash=sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292 \ - --hash=sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6 \ - --hash=sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd \ - --hash=sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7 \ - --hash=sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d \ - --hash=sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0 \ - --hash=sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee \ - --hash=sha256:ffc578717a347baf25be8397cb10d2528802d24f94cfc005c0e44fef44b5cdd6 +xxhash==3.8.1 \ + --hash=sha256:000435984a0469b0f822fe76f35bddea0f96a4d6521b3339a60a6428cdee1edc \ + --hash=sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3 \ + --hash=sha256:01cab782f8a0a05ecad2c63d7ef10f7ab475f660e0d6419d069418c14d88de7c \ + --hash=sha256:0204701e6d01f64254e0e5ff4255812b1febe027ddd7dda63372e27f98b5e91f \ + --hash=sha256:027dee4355f3fcc41481650d846cf6cfc895c85a1ab7acd063063821a0df5b4c \ + --hash=sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec \ + --hash=sha256:0418ec8b2331b9d4d575fc9284427e8e69449d7172e99e1a86fcdd1f51a0a937 \ + --hash=sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92 \ + --hash=sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a \ + --hash=sha256:09a204dd4bb0823daf938cdd0dc8057d5f1e14fe3cbde929424255f23f9de872 \ + --hash=sha256:0dfdf19b0d5433a75d61f19dc85737af0f0b95e445c1ad69c855115d05efed45 \ + --hash=sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3 \ + --hash=sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31 \ + --hash=sha256:1153265daa10750a9bf8e9b01753d7618024a300925591efaf16b1b7fa536699 \ + --hash=sha256:115772daeb71b2f3b9381177017f53e6cf3f3439c840737fdabd21aba6e54920 \ + --hash=sha256:12a3cf79dadbab9631230ebc4c51c7c60f1e9cdfb890c15fb733eaafe2e7713c \ + --hash=sha256:12eaeaa9ab8b9e6033a1fa5f6b338aaf55ff4df4bee11b59fd6ee03b19186ee4 \ + --hash=sha256:131324f719957b988861714de7d6ddf57b47abec3b0cc691302ffeaba0e05e10 \ + --hash=sha256:15790b686f8723b845fec6f612a343beb815a25c83117a7fa408d7c8ee5aa8fd \ + --hash=sha256:1731407102b9332cd3c9dadee07db498bc3d437b95d752b5b1a5f7eb730a3738 \ + --hash=sha256:1b86ae798a976ccbc1d02af6ccb98f5b4d24756b1f65e995f11d10fe071f486f \ + --hash=sha256:1c332dd48b8cb050da2bb2a3c96d72b1664168650a250ef9718e423df7989e05 \ + --hash=sha256:1da930bbcac3e8fbe2191850e2abb57977a99348c12c4b385e1058ac1b0a9ecc \ + --hash=sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329 \ + --hash=sha256:1ffcc98d8878e449e86dec008cea6f44cfd3a954d2ef24ae7d1cc9f725beec7d \ + --hash=sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215 \ + --hash=sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724 \ + --hash=sha256:23e710118a5778a45db740b431943a3f2a82a571a052c2768cce6544d9c8c62e \ + --hash=sha256:264710bd335016f303763ce1275c6486df30bb57c2245c91b224c983d7ac39b8 \ + --hash=sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62 \ + --hash=sha256:27a9e475157f7315826118e3f3127909a0fe25f1b43d3d3be9c584f9d265f937 \ + --hash=sha256:27cfc2f1ed76f956f36dfe0c56e5f5a3e94cd91eb78b893f63e2ef2ae404fcdf \ + --hash=sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d \ + --hash=sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75 \ + --hash=sha256:2f1c68394818e0595569c2ff3cbc1e6d5a36a434e796f5c526b987b80c8a8c62 \ + --hash=sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8 \ + --hash=sha256:314d05fbc55719ae2438eaaba77bf2508ca4f030b26fa4c9c8c380e81c48fa33 \ + --hash=sha256:32a94ad2763e0263d9102037d349002c3d3c401e42770542c3eeb4801f311661 \ + --hash=sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0 \ + --hash=sha256:345b07b78e2bf583d71682aa34ae5b5fab575f7a1cb31e10263ebbc6f89f8c42 \ + --hash=sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893 \ + --hash=sha256:358650d5bda9c635da699c53adf4e8134af492ecc79c960f917eebf088bb6799 \ + --hash=sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887 \ + --hash=sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629 \ + --hash=sha256:38c887aedb696ef8bca19983206d270848558cfae4a91afa6a2fb05dde58ffc5 \ + --hash=sha256:39c9d5b61508b0bb68f29e54546de0ed2a74943c6a18585535a7e37356f1dd12 \ + --hash=sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf \ + --hash=sha256:3bc0fa90830df1e1277f33cc6e55de9990b83c0319fd8c7412866cfde38b025e \ + --hash=sha256:3c0d84c5f2e086b120bae4e7f551cbda804c1deb10d958478bed4f89ba286dfe \ + --hash=sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913 \ + --hash=sha256:402db908ea70eaf9800d9182a66596fc86f36655df8f63fdecf7c11da741d86f \ + --hash=sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e \ + --hash=sha256:4482380b462ca9e59994d072a877ecadd1cf51102daeeab2db696f96ab763723 \ + --hash=sha256:44c89d915a75c11d2547eaee9098fcd80398987c4bff2974a0497a925bf92c07 \ + --hash=sha256:454d78e786602278a2a4383d08048482052f4f0c61fa677ca590af08914d9bca \ + --hash=sha256:460261045936975193bfd20549a0de1cd52a33b405cbb972f0d80940c42266cd \ + --hash=sha256:46b39976d008e2a845758650f0ff7136bca004f40da0c8798bd37ac37860154f \ + --hash=sha256:498017fbf2d13a768b3110d084bde39f2bd8664c1de0b8084f8ccc84425b7c88 \ + --hash=sha256:49aa8692507835dcc1e8ad8021f20c74c2dc13d83b5112e87877faa2a0035b20 \ + --hash=sha256:4b512261801b1e5fde7b6ebf2fef7977339c620cbbca88a0040ad9ad134f4d02 \ + --hash=sha256:4bec8b2c909bcfae9a0dc702346007e02a8c9ba5bbde83ffb224aa194f4f9efc \ + --hash=sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478 \ + --hash=sha256:4d6e88ddb3c741fbf29e1e7faf429880f8cd1d7aff4303247435a549726b4fb1 \ + --hash=sha256:4df57c0b161ec1b3ed0526a67b0db0914b557e86ee8aae51887aec941b261542 \ + --hash=sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa \ + --hash=sha256:4ef09bbc2519a93cd0f95f2ceb5f7b85919dffea643278e02362bf40e3c4bed1 \ + --hash=sha256:5013be3bea7612852c62a7437f3302c1cfb91ca7e703b194459db0b2b2e0d792 \ + --hash=sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed \ + --hash=sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647 \ + --hash=sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55 \ + --hash=sha256:53f3ed9118397074ff63a79b66b7fec1c84c782eecde35c5bc94e420a971c231 \ + --hash=sha256:559e3cabe522231909f9de98ef06929edbd53782046bd21aae0c72db6f2a0775 \ + --hash=sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398 \ + --hash=sha256:57f80a898544db78ec6b0be6183bd1bc008933193d4199f5cde36b0e6bd5e062 \ + --hash=sha256:58346024d47e84f7d8b3e7f5d6faa1d58acbbe49a8771497872059f58c1d8ea5 \ + --hash=sha256:594131ce1aad18db3689781f806db1b065cdaa04f4df36b4c038d2013aefd0bf \ + --hash=sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22 \ + --hash=sha256:5c566b123dce7e4867ca518434cdfb9f84e5023771235b2e3107a26c9a41cbd8 \ + --hash=sha256:5d3dfb1f0ff146da7952867a9414f0c7a29762f8825a84879592612fd6139342 \ + --hash=sha256:5da703225374e3a4c8d4fd90e26fe7213a52004ec77f88b42b42e9e86d8c6d57 \ + --hash=sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104 \ + --hash=sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb \ + --hash=sha256:602efcad4a42c184e81d43a2b7e6e4f524d619878f2b6ee2ba469011f47c8147 \ + --hash=sha256:614bca2c7cfa87ec95b703e691c3c5eb6c448b6dabbe9776ac53883152951729 \ + --hash=sha256:632a34590c090d1285ed5efa5a02be919f3f9a56a64bd25f693fe1e2d27a27fb \ + --hash=sha256:64af54dd1c3a45a27c04942f9a1a4683322bdd127f4745cca4e02549c1d2d2bb \ + --hash=sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170 \ + --hash=sha256:656256c9f9303e47f07d5cb8ae4468285370adfafd7ba48aea33a458e7697626 \ + --hash=sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65 \ + --hash=sha256:6c7574528bc922f8757f34dd78ed60ab52b1c7973b630f5eae7ba33ec133ce71 \ + --hash=sha256:6cee733fe4ccb1737e0997135283c82341e5cfa9cf214b165f9087fb663aaf4f \ + --hash=sha256:6cf633fe83b1d4e6519d7259b33afe40fbba5d3f438730156971dd0cf7730610 \ + --hash=sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113 \ + --hash=sha256:717b12fdc51819833704e85e6926d76981ffa3f780ef92e33ebb8b26d46bb230 \ + --hash=sha256:7258ee276e8772599bc19e14b36f6260306e21b637190cd7cb489a2449d48684 \ + --hash=sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629 \ + --hash=sha256:7345007c12780985de4fd740148776d1eee18c0d41407c6fa1e48c5450304fe5 \ + --hash=sha256:747476436f6891b9773374ce8d48edcc8b12cb5b61b67c6fb6289633747d088f \ + --hash=sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9 \ + --hash=sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276 \ + --hash=sha256:78c794b643d214f1522e7a288bcf5a2de120d26cd170516749a4009dc92722c9 \ + --hash=sha256:7b5f97ecfede10d5b2870383620e2d25c8561e217c7bf9081073802b54248d2b \ + --hash=sha256:7dc4bdf008f77c88d544849c48c1a40faf25a5eff6cc466de2e8edc37c191fce \ + --hash=sha256:81f4ed9ca9644bc95cd976bfe10f7a4cafab8ffdc3aed52877d4600e445be7ef \ + --hash=sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc \ + --hash=sha256:8304be0982130954b7fd3aad18e2c6f8ee40254bc3d2e635991c16d77c91e2bd \ + --hash=sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061 \ + --hash=sha256:836f11d4474d3228e9909d97216faa4f7505df41cfaf3927eb29809de785a78d \ + --hash=sha256:83b9130b80b216d56fdf9e87131946b353c9627930c061955a101ea82b09fed9 \ + --hash=sha256:83d879362ddd0fedd3f2ab8ce7cce3da2049a6d51d16da8af73011c6edf4752f \ + --hash=sha256:848182a391fffdc25605443e832f5b443f25498edeccf9a64343fd84421ca04b \ + --hash=sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc \ + --hash=sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56 \ + --hash=sha256:89b11a5cdd441aa463f6d34ca0241602bc09b001a76994b6059828494108c673 \ + --hash=sha256:89df64c10adfe340fb00330042537cdd6bf0d8d78bad73f29cfe5427eed7b084 \ + --hash=sha256:8ea8a141eeced4f6262ab6dd71c681ac546a558c30bb586abe087d814b5f85ea \ + --hash=sha256:8f454166c2ffed45636c8d501741e649851ba2f346c4eb73a64c07ac00428f20 \ + --hash=sha256:8f759eed402448c2bdbb492e4fba1f20668ffe29688605ea61f0f67f9e4e386d \ + --hash=sha256:9043877a917be88ccf230aa5667c1bd059bce80f4c2727e4defa1b29b7f48b08 \ + --hash=sha256:942bc86e9be6fdd6e1175048f5fe8f8fdaaf2309dd1323ef1e155a69cd346780 \ + --hash=sha256:947a585bcaa235702b7c59433b485489397f9a163b3f56058b9463a46fd9b74c \ + --hash=sha256:950ac754d16daea42038f38e7465eb84cda4d08d7343c1c915771b29470f065a \ + --hash=sha256:98d8ac1129b4dd39098cffed94d1284aceb61c3aa396757ccc736ac392e4cee5 \ + --hash=sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437 \ + --hash=sha256:9b2ce44bf8f4a1d01f418b3110ff8dff32fd3f3e836c0e06333c3725f243fa6c \ + --hash=sha256:9d45eee3a95a8b61e5b568580caac91f1502ddb731aaf8f4aa448a98660b2fb4 \ + --hash=sha256:9db455cb649dcfe4504d6d68a6d83a7315a99a3ca59871dc3ff840671f99adba \ + --hash=sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396 \ + --hash=sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0 \ + --hash=sha256:9f23083e1bd9d901f844af7a126727c486e7eada9a1a6791c8f7e73f94fac656 \ + --hash=sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907 \ + --hash=sha256:a5cd96f6dcdf4fa657b2d95668d71d58455248f98712ecffaa9c528edf40ccae \ + --hash=sha256:a5eed9d41995a83f3332b4e3396abb7f433cac584222bd7e305b606d8353861e \ + --hash=sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a \ + --hash=sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda \ + --hash=sha256:a98b2f95cab589e0f5e92c48431afb4d56238b8bf6668edcc66166180e9b509b \ + --hash=sha256:ad52a0e4bcc0ba956a953a169d1feec2734a64981d689e4fc8f490f7bf91af60 \ + --hash=sha256:af0c9fedc4a2c24e8664953882fe8185f3790b8338c9c700f76f5ad660817711 \ + --hash=sha256:afe6380a0e9653a87aa1e6e88fb47718113e5563c7a1cb2bcc23c1d8e17e3961 \ + --hash=sha256:affb37f152e55b5e4494bb9d0107f7bb08515c6704fbed82d9f61214d74adc17 \ + --hash=sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf \ + --hash=sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46 \ + --hash=sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2 \ + --hash=sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6 \ + --hash=sha256:b3e1107fe5ca030f946dfa59fdbb66b5df121c8432f14b0bdd282d17b297f4eb \ + --hash=sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a \ + --hash=sha256:b6fa3116e40e14e7782fb1a9f872f94b5997de21127c95545ce40196ac1351c5 \ + --hash=sha256:bb70573d2995d23932e2871120f78d798ebc3572e54c09e694a18ced95c5f8d9 \ + --hash=sha256:bbcdf9c92d21c65bc75426eecea724c8fa0d35a6e201fdf1630011d4cc3aa685 \ + --hash=sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315 \ + --hash=sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e \ + --hash=sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc \ + --hash=sha256:c240939e963653054fc7e4a17c382829cda4aa88a7daf0af841715dbded1b497 \ + --hash=sha256:c31a2649bcf1fe97cf11c79848d761df33ac46b3896942d31b640557b486ff6b \ + --hash=sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e \ + --hash=sha256:c73b6f652f0745425aa6378319c331293b5341756262e9408ed3d45f183375e6 \ + --hash=sha256:c85949d02c85adf6d786eb94858e124989a632a4e65739835b2fc5761827fac3 \ + --hash=sha256:c919f38cd3f0b5e8d30b81fd6cac688cf9221560340f0c35cbbb8b2bd77ad6ac \ + --hash=sha256:c959f88160b13b4e730b0d75b459b7929fc0d2225c284c9683ac95d6feeeac6a \ + --hash=sha256:cb3fe820c27593f170770d6c8d791936cf6275d9269405fbb7b30a55363c10c8 \ + --hash=sha256:cf399fac542a1c7a4734a435b93df2c55e858c7d31abf6c1bdf46f9ae67fbfd0 \ + --hash=sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068 \ + --hash=sha256:d247b34bf433c92b41689318fd25d246313cab2275a6a47e2efac178b80d6efe \ + --hash=sha256:d48acabb1e5cb0071009f80d71d7f01b6ba2c1d4b869b1352bb5df3f11bf7dfd \ + --hash=sha256:d5006c65ec507a333479e76e00e2c368781f16c24ededa764763956b32a0e93e \ + --hash=sha256:d58ce8b6cfa9c4d2f230557f69caf7c06369e318015d0b19485095bc2c5963ab \ + --hash=sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838 \ + --hash=sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297 \ + --hash=sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c \ + --hash=sha256:db77278a6eddadbf44ce5aae2fee5ebb4d061f026b1ce2130d058cd4d7a7b670 \ + --hash=sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975 \ + --hash=sha256:e14800b9b10bb39d7a60ad4a310e403164d7b8988a27ae933d4e40618a44088e \ + --hash=sha256:e2a845687219ba3214126f14a8a5861f97c9e065a7d0b8252adb6df13eea86fb \ + --hash=sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0 \ + --hash=sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1 \ + --hash=sha256:e605e0b8abca9457abd5bee737e086ab145a20c25083ef1113013612268872ff \ + --hash=sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc \ + --hash=sha256:e710ad822c493fb80a4fbc1e3d0a807b1422cb90adbe64378f98291b7fa48fef \ + --hash=sha256:ea6a3e734b0fd41b82784a400be946821900daebe610c050a5e0760838a34f99 \ + --hash=sha256:ec55d80e9b8a519d742669e0b49e8ce9e6747be42bf3c138158b6543a9c8e489 \ + --hash=sha256:ed8bcdab6692fd4ad0dd6241807a24a640a376764460023b8d462d745e6b7b27 \ + --hash=sha256:f377012b86c0a23a1df0cf5a1b05aa7187649e472f71c7892e5f2c2815bbe74f \ + --hash=sha256:f6114692261eff4266386cdec0f7d87eee24e317ab397c218b7ae6a76b4c6339 \ + --hash=sha256:f8044cf4c77f37968b8c4cbcbf7a0f355d8a437877ae18eba23e3aad953a6cc7 \ + --hash=sha256:f8ed8940435834141061da26d27c4dd0d18fb69777bf431f5c6cc46b43349113 \ + --hash=sha256:f93e408255ddce525189bf11feaa1be7ee35e55f486c299c97d9caa68d724a5b \ + --hash=sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1 # via datasets -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8 \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf \ - --hash=sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890 \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737 \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba \ - --hash=sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9 \ - --hash=sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0 \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147 \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e \ - --hash=sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2 \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6 \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750 \ + --hash=sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621 \ + --hash=sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13 \ + --hash=sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1 \ + --hash=sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2 \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata zstandard==0.25.0 \ --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \ @@ -6836,5 +7777,5 @@ zstandard==0.25.0 \ --hash=sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551 \ --hash=sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01 # via - # clickhouse-connect + # pyiceberg # trino diff --git a/sdk/python/requirements/py3.12-minimal-requirements.txt b/sdk/python/requirements/py3.12-minimal-requirements.txt index cba3a0d63a5..4affd220de9 100644 --- a/sdk/python/requirements/py3.12-minimal-requirements.txt +++ b/sdk/python/requirements/py3.12-minimal-requirements.txt @@ -1,152 +1,160 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.12 --no-strip-extras setup.py --extra minimal --generate-hashes --output-file sdk/python/requirements/py3.12-minimal-requirements.txt -aiobotocore==2.23.1 \ - --hash=sha256:a59f2a78629b97d52f10936b79c73de64e481a8c44a62c1871f088df6c1afc4f \ - --hash=sha256:d81c54d2eae2406ea9a473fea518fed580cf37bc4fc51ce43ba81546e5305114 - # via feast (setup.py) -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +# uv pip compile -p 3.12 --no-strip-extras pyproject.toml --extra minimal --generate-hashes --output-file sdk/python/requirements/py3.12-minimal-requirements.txt +aiobotocore==3.8.0 \ + --hash=sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670 \ + --hash=sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + # via feast (pyproject.toml) +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.0 \ - --hash=sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6 \ - --hash=sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a \ - --hash=sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e \ - --hash=sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971 \ - --hash=sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54 \ - --hash=sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2 \ - --hash=sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96 \ - --hash=sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469 \ - --hash=sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26 \ - --hash=sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0 \ - --hash=sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29 \ - --hash=sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885 \ - --hash=sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579 \ - --hash=sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a \ - --hash=sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1 \ - --hash=sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b \ - --hash=sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852 \ - --hash=sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865 \ - --hash=sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad \ - --hash=sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092 \ - --hash=sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7 \ - --hash=sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b \ - --hash=sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8 \ - --hash=sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820 \ - --hash=sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd \ - --hash=sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee \ - --hash=sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4 \ - --hash=sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67 \ - --hash=sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d \ - --hash=sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1 \ - --hash=sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d \ - --hash=sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114 \ - --hash=sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1 \ - --hash=sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755 \ - --hash=sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04 \ - --hash=sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca \ - --hash=sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1 \ - --hash=sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae \ - --hash=sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793 \ - --hash=sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e \ - --hash=sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a \ - --hash=sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8 \ - --hash=sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602 \ - --hash=sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778 \ - --hash=sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f \ - --hash=sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49 \ - --hash=sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77 \ - --hash=sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7 \ - --hash=sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f \ - --hash=sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0 \ - --hash=sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519 \ - --hash=sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d \ - --hash=sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370 \ - --hash=sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7 \ - --hash=sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247 \ - --hash=sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d \ - --hash=sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80 \ - --hash=sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9 \ - --hash=sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c \ - --hash=sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d \ - --hash=sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020 \ - --hash=sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504 \ - --hash=sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca \ - --hash=sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731 \ - --hash=sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd \ - --hash=sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6 \ - --hash=sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21 \ - --hash=sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1 \ - --hash=sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620 \ - --hash=sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64 \ - --hash=sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8 \ - --hash=sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3 \ - --hash=sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5 \ - --hash=sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985 \ - --hash=sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813 \ - --hash=sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed \ - --hash=sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20 \ - --hash=sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03 \ - --hash=sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702 \ - --hash=sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6 \ - --hash=sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed \ - --hash=sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353 \ - --hash=sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108 \ - --hash=sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f \ - --hash=sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409 \ - --hash=sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7 \ - --hash=sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3 \ - --hash=sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9 \ - --hash=sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba \ - --hash=sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe \ - --hash=sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c \ - --hash=sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861 \ - --hash=sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a \ - --hash=sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0 \ - --hash=sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390 \ - --hash=sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3 \ - --hash=sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90 \ - --hash=sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802 \ - --hash=sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163 \ - --hash=sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d \ - --hash=sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430 \ - --hash=sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a \ - --hash=sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b \ - --hash=sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6 \ - --hash=sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b \ - --hash=sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657 \ - --hash=sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c \ - --hash=sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93 \ - --hash=sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f \ - --hash=sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82 \ - --hash=sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31 \ - --hash=sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc \ - --hash=sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52 \ - --hash=sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c \ - --hash=sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a \ - --hash=sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52 \ - --hash=sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7 \ - --hash=sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b \ - --hash=sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147 \ - --hash=sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4 - # via aiobotocore -aioitertools==0.12.0 \ - --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \ - --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 + # via + # aiobotocore + # kubernetes +aioitertools==0.13.0 \ + --hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \ + --hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c # via aiobotocore aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 # via aiohttp -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via + # fastapi + # typer +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # httpx + # httpx2 # mcp # sse-starlette # starlette @@ -155,400 +163,434 @@ asn1crypto==1.5.1 \ --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 # via snowflake-connector-python -atpublic==5.1 \ - --hash=sha256:135783dbd887fbddb6ef032d104da70c124f2b44b9e2d79df07b9da5334825e3 \ - --hash=sha256:abc1f4b3dbdd841cc3539e4b5e4f3ad41d658359de704e30cb36da4d4e9d3022 +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy +atpublic==7.0.0 \ + --hash=sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e \ + --hash=sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b # via ibis-framework -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via + # feast (pyproject.toml) # aiohttp # jsonschema # referencing -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -boto3==1.38.27 \ - --hash=sha256:94bd7fdd92d5701b362d4df100d21e28f8307a67ff56b6a8b0398119cf22f859 \ - --hash=sha256:95f5fe688795303a8a15e8b7e7f255cadab35eae459d00cc281a4fd77252ea80 +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +boto3==1.43.46 \ + --hash=sha256:66c0d943b049a46a492ec4ec2ebe73c930b1842c7137bee83aad6d93e95d4d96 \ + --hash=sha256:69453e2c1bcb9fd9806527ab99950cacfc2826cb0dce9a3a0414d19270c06c3c # via - # feast (setup.py) + # feast (pyproject.toml) # snowflake-connector-python -botocore==1.38.46 \ - --hash=sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e \ - --hash=sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b +botocore==1.43.46 \ + --hash=sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533 \ + --hash=sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 # via # aiobotocore # boto3 # s3transfer # snowflake-connector-python -cachetools==6.2.1 \ - --hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \ - --hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201 - # via google-auth -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +cachetools==7.1.6 \ + --hash=sha256:2c12e255780330af28b91bb7fb96cce4c766f04e38396b9a24510190a5827096 \ + --hash=sha256:c7a79e7f30ba9943c1cefd08cc36f006aaae086e017af9166f1d59d6170c47e1 + # via pymilvus +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # httpcore # httpx # kubernetes # requests # snowflake-connector-python -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f # via - # feast (setup.py) + # feast (pyproject.toml) # cryptography - # snowflake-connector-python -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via # requests # snowflake-connector-python -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask - # typer # uvicorn -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a # via dask colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via feast (setup.py) -cryptography==46.0.0 \ - --hash=sha256:07a1be54f995ce14740bf8bbe1cc35f7a37760f992f73cf9f98a2a60b9b97419 \ - --hash=sha256:0f58183453032727a65e6605240e7a3824fd1d6a7e75d2b537e280286ab79a52 \ - --hash=sha256:16b5ac72a965ec9d1e34d9417dbce235d45fa04dac28634384e3ce40dfc66495 \ - --hash=sha256:1b4fba84166d906a22027f0d958e42f3a4dbbb19c28ea71f0fb7812380b04e3c \ - --hash=sha256:1d2073313324226fd846e6b5fc340ed02d43fd7478f584741bd6b791c33c9fee \ - --hash=sha256:249c41f2bbfa026615e7bdca47e4a66135baa81b08509ab240a2e666f6af5966 \ - --hash=sha256:274f8b2eb3616709f437326185eb563eb4e5813d01ebe2029b61bfe7d9995fbb \ - --hash=sha256:2fc30be952dd4334801d345d134c9ef0e9ccbaa8c3e1bc18925cbc4247b3e29c \ - --hash=sha256:32670ca085150ff36b438c17f2dfc54146fe4a074ebf0a76d72fb1b419a974bc \ - --hash=sha256:35aa1a44bd3e0efc3ef09cf924b3a0e2a57eda84074556f4506af2d294076685 \ - --hash=sha256:3738f50215211cee1974193a1809348d33893696ce119968932ea117bcbc9b1d \ - --hash=sha256:378eff89b040cbce6169528f130ee75dceeb97eef396a801daec03b696434f06 \ - --hash=sha256:399ef4c9be67f3902e5ca1d80e64b04498f8b56c19e1bc8d0825050ea5290410 \ - --hash=sha256:40ee4ce3c34acaa5bc347615ec452c74ae8ff7db973a98c97c62293120f668c6 \ - --hash=sha256:4bc257c2d5d865ed37d0bd7c500baa71f939a7952c424f28632298d80ccd5ec1 \ - --hash=sha256:4f70cbade61a16f5e238c4b0eb4e258d177a2fcb59aa0aae1236594f7b0ae338 \ - --hash=sha256:523153480d7575a169933f083eb47b1edd5fef45d87b026737de74ffeb300f69 \ - --hash=sha256:6460866a92143a24e3ed68eaeb6e98d0cedd85d7d9a8ab1fc293ec91850b1b38 \ - --hash=sha256:65e9117ebed5b16b28154ed36b164c20021f3a480e9cbb4b4a2a59b95e74c25d \ - --hash=sha256:6c39fd5cd9b7526afa69d64b5e5645a06e1b904f342584b3885254400b63f1b3 \ - --hash=sha256:6d8945bc120dcd90ae39aa841afddaeafc5f2e832809dc54fb906e3db829dfdc \ - --hash=sha256:75d2ddde8f1766ab2db48ed7f2aa3797aeb491ea8dfe9b4c074201aec00f5c16 \ - --hash=sha256:77e3bd53c9c189cea361bc18ceb173959f8b2dd8f8d984ae118e9ac641410252 \ - --hash=sha256:7f3f88df0c9b248dcc2e76124f9140621aca187ccc396b87bc363f890acf3a30 \ - --hash=sha256:80a548a5862d6912a45557a101092cd6c64ae1475b82cef50ee305d14a75f598 \ - --hash=sha256:834af45296083d892e23430e3b11df77e2ac5c042caede1da29c9bf59016f4d2 \ - --hash=sha256:83af84ebe7b6e9b6de05050c79f8cc0173c864ce747b53abce6a11e940efdc0d \ - --hash=sha256:88c09da8a94ac27798f6b62de6968ac78bb94805b5d272dbcfd5fdc8c566999f \ - --hash=sha256:8e8b222eb54e3e7d3743a7c2b1f7fa7df7a9add790307bb34327c88ec85fe087 \ - --hash=sha256:91585fc9e696abd7b3e48a463a20dda1a5c0eeeca4ba60fa4205a79527694390 \ - --hash=sha256:99f64a6d15f19f3afd78720ad2978f6d8d4c68cd4eb600fab82ab1a7c2071dca \ - --hash=sha256:9aa85222f03fdb30defabc7a9e1e3d4ec76eb74ea9fe1504b2800844f9c98440 \ - --hash=sha256:ab3a14cecc741c8c03ad0ad46dfbf18de25218551931a23bca2731d46c706d83 \ - --hash=sha256:b8e7db4ce0b7297e88f3d02e6ee9a39382e0efaf1e8974ad353120a2b5a57ef7 \ - --hash=sha256:bbaa5eef3c19c66613317dc61e211b48d5f550db009c45e1c28b59d5a9b7812a \ - --hash=sha256:be7479f9504bfb46628544ec7cb4637fe6af8b70445d4455fbb9c395ad9b7290 \ - --hash=sha256:bf1961037309ee0bdf874ccba9820b1c2f720c2016895c44d8eb2316226c1ad5 \ - --hash=sha256:c1f6ccd6f2eef3b2eb52837f0463e853501e45a916b3fc42e5d93cf244a4b97b \ - --hash=sha256:c3648d6a5878fd1c9a22b1d43fa75efc069d5f54de12df95c638ae7ba88701d0 \ - --hash=sha256:c39f0947d50f74b1b3523cec3931315072646286fb462995eb998f8136779319 \ - --hash=sha256:c3cd09b1490c1509bf3892bde9cef729795fae4a2fee0621f19be3321beca7e4 \ - --hash=sha256:c457ad3f151d5fb380be99425b286167b358f76d97ad18b188b68097193ed95a \ - --hash=sha256:c9c4121f9a41cc3d02164541d986f59be31548ad355a5c96ac50703003c50fb7 \ - --hash=sha256:d14eaf1569d6252280516bedaffdd65267428cdbc3a8c2d6de63753cf0863d5e \ - --hash=sha256:d1eccae15d5c28c74b2bea228775c63ac5b6c36eedb574e002440c0bc28750d3 \ - --hash=sha256:d349af4d76a93562f1dce4d983a4a34d01cb22b48635b0d2a0b8372cdb4a8136 \ - --hash=sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0 \ - --hash=sha256:da7f93551d39d462263b6b5c9056c49f780b9200bf9fc2656d7c88c7bdb9b363 \ - --hash=sha256:df932ac70388be034b2e046e34d636245d5eeb8140db24a6b4c2268cd2073270 \ - --hash=sha256:f09a3a108223e319168b7557810596631a8cb864657b0c16ed7a6017f0be9433 \ - --hash=sha256:f85e6a7d42ad60024fa1347b1d4ef82c4df517a4deb7f829d301f1a92ded038c \ - --hash=sha256:f9aaf2a91302e1490c068d2f3af7df4137ac2b36600f5bd26e53d9ec320412d3 \ - --hash=sha256:f9f85d9cf88e3ba2b2b6da3c2310d1cf75bdf04a5bc1a2e972603054f82c4dd5 \ - --hash=sha256:fe9ff1139b2b1f59a5a0b538bbd950f8660a39624bbe10cf3640d17574f973bb + # via feast (pyproject.toml) +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b # via + # google-auth + # pyjwt # pyopenssl # snowflake-connector-python -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) -db-dtypes==1.4.3 \ - --hash=sha256:a1c92b819af947fae1701d80a71f2a0eac08f825ca52cf0c68aeba80577ae966 \ - --hash=sha256:d3cb08c32939d24e05501f17694be8c1c54cfb4620d61fb56fb311e8c3d8885e +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) +db-dtypes==1.7.1 \ + --hash=sha256:450b94cd010cfa6d4d13cca2593009eba4fbe15516084c332cde1a47bbce0625 \ + --hash=sha256:75adf3bdf3174a6474ea4162d9a345ea0b3102ec4fd603e69ec26b6e8a99145a # via # google-cloud-bigquery # pandas-gbq dill==0.3.9 \ --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c - # via feast (setup.py) -duckdb==1.1.3 \ - --hash=sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586 \ - --hash=sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd \ - --hash=sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e \ - --hash=sha256:09c68522c30fc38fc972b8a75e9201616b96ae6da3444585f14cf0d116008c95 \ - --hash=sha256:0a55169d2d2e2e88077d91d4875104b58de45eff6a17a59c7dc41562c73df4be \ - --hash=sha256:0ba6baa0af33ded836b388b09433a69b8bec00263247f6bf0a05c65c897108d3 \ - --hash=sha256:183ac743f21c6a4d6adfd02b69013d5fd78e5e2cd2b4db023bc8a95457d4bc5d \ - --hash=sha256:1aa3abec8e8995a03ff1a904b0e66282d19919f562dd0a1de02f23169eeec461 \ - --hash=sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce \ - --hash=sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24 \ - --hash=sha256:2141c6b28162199999075d6031b5d63efeb97c1e68fb3d797279d31c65676269 \ - --hash=sha256:252d9b17d354beb9057098d4e5d5698e091a4f4a0d38157daeea5fc0ec161670 \ - --hash=sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9 \ - --hash=sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3 \ - --hash=sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289 \ - --hash=sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1 \ - --hash=sha256:4f0e2e5a6f5a53b79aee20856c027046fba1d73ada6178ed8467f53c3877d5e0 \ - --hash=sha256:51c6d79e05b4a0933672b1cacd6338f882158f45ef9903aef350c4427d9fc898 \ - --hash=sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8 \ - --hash=sha256:5ace6e4b1873afdd38bd6cc8fcf90310fb2d454f29c39a61d0c0cf1a24ad6c8d \ - --hash=sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff \ - --hash=sha256:6411e21a2128d478efbd023f2bdff12464d146f92bc3e9c49247240448ace5a6 \ - --hash=sha256:647f17bd126170d96a38a9a6f25fca47ebb0261e5e44881e3782989033c94686 \ - --hash=sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c \ - --hash=sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31 \ - --hash=sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7 \ - --hash=sha256:80158f4c7c7ada46245837d5b6869a336bbaa28436fbb0537663fa324a2750cd \ - --hash=sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898 \ - --hash=sha256:8ee97ec337794c162c0638dda3b4a30a483d0587deda22d45e1909036ff0b739 \ - --hash=sha256:911d58c22645bfca4a5a049ff53a0afd1537bc18fedb13bc440b2e5af3c46148 \ - --hash=sha256:9c619e4849837c8c83666f2cd5c6c031300cd2601e9564b47aa5de458ff6e69d \ - --hash=sha256:9d0767ada9f06faa5afcf63eb7ba1befaccfbcfdac5ff86f0168c673dd1f47aa \ - --hash=sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4 \ - --hash=sha256:a1f83c7217c188b7ab42e6a0963f42070d9aed114f6200e3c923c8899c090f16 \ - --hash=sha256:a1fa0c502f257fa9caca60b8b1478ec0f3295f34bb2efdc10776fc731b8a6c5f \ - --hash=sha256:a30dd599b8090ea6eafdfb5a9f1b872d78bac318b6914ada2d35c7974d643640 \ - --hash=sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38 \ - --hash=sha256:a4748635875fc3c19a7320a6ae7410f9295557450c0ebab6d6712de12640929a \ - --hash=sha256:b74e121ab65dbec5290f33ca92301e3a4e81797966c8d9feef6efdf05fc6dafd \ - --hash=sha256:c443d3d502335e69fc1e35295fcfd1108f72cb984af54c536adfd7875e79cee5 \ - --hash=sha256:c5336939d83837af52731e02b6a78a446794078590aa71fd400eb17f083dda3e \ - --hash=sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378 \ - --hash=sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9 \ - --hash=sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee \ - --hash=sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32 \ - --hash=sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537 \ - --hash=sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989 \ - --hash=sha256:ecb1dc9062c1cc4d2d88a5e5cd8cc72af7818ab5a3c0f796ef0ffd60cfd3efb4 \ - --hash=sha256:eeacb598120040e9591f5a4edecad7080853aa8ac27e62d280f151f8c862afa3 \ - --hash=sha256:f549af9f7416573ee48db1cf8c9d27aeed245cb015f4b4f975289418c6cf7320 \ - --hash=sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3 \ - --hash=sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b + # via feast (pyproject.toml) +duckdb==1.5.5 \ + --hash=sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053 \ + --hash=sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4 \ + --hash=sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c \ + --hash=sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a \ + --hash=sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528 \ + --hash=sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c \ + --hash=sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282 \ + --hash=sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f \ + --hash=sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7 \ + --hash=sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f \ + --hash=sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c \ + --hash=sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb \ + --hash=sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433 \ + --hash=sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8 \ + --hash=sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28 \ + --hash=sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238 \ + --hash=sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece \ + --hash=sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1 \ + --hash=sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151 \ + --hash=sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2 \ + --hash=sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353 \ + --hash=sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc \ + --hash=sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079 \ + --hash=sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a \ + --hash=sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6 \ + --hash=sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe \ + --hash=sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de \ + --hash=sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0 \ + --hash=sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e \ + --hash=sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f \ + --hash=sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf \ + --hash=sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae \ + --hash=sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6 \ + --hash=sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe \ + --hash=sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366 # via ibis-framework durationpy==0.10 \ --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 # via kubernetes -environs==9.5.0 \ - --hash=sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124 \ - --hash=sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9 - # via pymilvus -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 +faiss-cpu==1.14.3 \ + --hash=sha256:0a5eb27184123c7ac1060c6b862978eabf0e30c1369ccf8bdb1497d35c06ad3d \ + --hash=sha256:1d734cfa9ac90b6a5dfed3a27cb706d05f22824703dafc3969b4e2071877a31c \ + --hash=sha256:20414d5c98fb6ab9eedfb3df79841865911c2d67b6efe829dc46adfeab6f51b1 \ + --hash=sha256:54b0ea832be5bce75ef9abf1297fe4613d760bf7cfe7a963da040cb0314e8f58 \ + --hash=sha256:8780b526c06e57aad90a8c4655dfba2ff1b3195bd600ff4499752fc45159c9fc \ + --hash=sha256:a9369863290a3f0e033757e4c10577b6ef7431f1cede394dabd0a137e4e2ed45 \ + --hash=sha256:b28ba083e8c02f2c9be03783402537fd3f00d27e68799c44bf88931500ee12ec \ + --hash=sha256:cdcb90850cb4b7c27d270839b37bcc0dc8d1fb6a62d1e13053e51ac95061ca25 \ + --hash=sha256:ea2340f675db59af8db6da4535b541ce6948d68a008989c656c292c7b0c77127 \ + --hash=sha256:f278003d00c30c90cf118b98428bb96f73eba4b9dadac6993c788966d2e983bd \ + --hash=sha256:f9d0e84d909194f63f027bbd3c1e35e905e48c9345c2db6e6f24da09a6bc5906 + # via milvus-lite +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp fastapi-mcp==0.4.0 \ --hash=sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec \ --hash=sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d - # via feast (setup.py) -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 + # via feast (pyproject.toml) +filelock==3.32.0 \ + --hash=sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402 \ + --hash=sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3 # via snowflake-connector-python frozenlist==1.8.0 \ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ @@ -684,17 +726,17 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -fsspec==2024.9.0 \ - --hash=sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8 \ - --hash=sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b +fsspec==2026.7.0 \ + --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \ + --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88 # via - # feast (setup.py) + # feast (pyproject.toml) # dask -google-api-core[grpc]==2.26.0 \ - --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ - --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 +google-api-core[grpc]==2.33.0 \ + --hash=sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb \ + --hash=sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc # via - # feast (setup.py) + # feast (pyproject.toml) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -702,9 +744,9 @@ google-api-core[grpc]==2.26.0 \ # google-cloud-datastore # google-cloud-storage # pandas-gbq -google-auth==2.41.1 \ - --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ - --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 +google-auth[pyopenssl]==2.56.2 \ + --hash=sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6 \ + --hash=sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051 # via # google-api-core # google-auth-oauthlib @@ -714,391 +756,511 @@ google-auth==2.41.1 \ # google-cloud-core # google-cloud-datastore # google-cloud-storage - # kubernetes # pandas-gbq # pydata-google-auth -google-auth-oauthlib==1.2.2 \ - --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ - --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-auth-oauthlib==1.4.0 \ + --hash=sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2 \ + --hash=sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 # via # pandas-gbq # pydata-google-auth -google-cloud-bigquery[pandas]==3.38.0 \ - --hash=sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520 \ - --hash=sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6 +google-cloud-bigquery[pandas]==3.42.2 \ + --hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \ + --hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq -google-cloud-bigquery-storage==2.33.1 \ - --hash=sha256:24952aba0d69acc4d6bfbdc7a09dddbb728496b1780bd224f1056361a1b51044 \ - --hash=sha256:3fd25bef364ac5fb9bbd6560f0dd11b90b1845883df8e0a8c706ad53d00fc23b - # via feast (setup.py) -google-cloud-bigtable==2.33.0 \ - --hash=sha256:8d25c73dd41cea9436d14c9f03b91f94b05ff4afd47ce9a575bf18f8e6985747 \ - --hash=sha256:cd4f7178cde30113900318ec7e2fd793116752c97e0922c185c69b645a52656d - # via feast (setup.py) -google-cloud-core==2.4.3 \ - --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ - --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e +google-cloud-bigquery-storage==2.39.0 \ + --hash=sha256:8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 \ + --hash=sha256:d5afd90ad06cf24d9167316cca70ab5b344e880fc13031d7392aa78ee76b8bb6 + # via feast (pyproject.toml) +google-cloud-bigtable==2.41.0 \ + --hash=sha256:36da0fc730952bfe48ae91dee11015459c9046fa1499d0efb73a61bbb6edbe9b \ + --hash=sha256:d0c424bae6238eb16ffc0922be79da00d20d23e0750ea46742ffd4542c50fd26 + # via feast (pyproject.toml) +google-cloud-core==2.6.0 \ + --hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \ + --hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.21.0 \ - --hash=sha256:eee454dd4a55f5b327f9f344928ff1a09a6f77c23d5e3d908ad31a13cc2f4073 \ - --hash=sha256:f303f27cd1983383f20bd227019cd8a7897419e0ec6b878367c58c66245f9d9b - # via feast (setup.py) +google-cloud-datastore==2.26.0 \ + --hash=sha256:1ac4882f033fcb90bd75d1171b46400ecd5def23768598843d9c041e0c0b4a6a \ + --hash=sha256:1bde5694308468da5c932153a6b0331d5b54e1c21de366b6a75ddb0000fd5f4b + # via feast (pyproject.toml) google-cloud-storage==2.19.0 \ --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via feast (setup.py) -google-crc32c==1.7.1 \ - --hash=sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db \ - --hash=sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337 \ - --hash=sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c \ - --hash=sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242 \ - --hash=sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e \ - --hash=sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472 \ - --hash=sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194 \ - --hash=sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3 \ - --hash=sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582 \ - --hash=sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d \ - --hash=sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6 \ - --hash=sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82 \ - --hash=sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06 \ - --hash=sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349 \ - --hash=sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a \ - --hash=sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d \ - --hash=sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48 \ - --hash=sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb \ - --hash=sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315 \ - --hash=sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589 \ - --hash=sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76 \ - --hash=sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65 \ - --hash=sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6 \ - --hash=sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127 \ - --hash=sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53 \ - --hash=sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603 \ - --hash=sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35 \ - --hash=sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9 \ - --hash=sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638 \ - --hash=sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9 \ - --hash=sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77 \ - --hash=sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14 \ - --hash=sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b \ - --hash=sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb + # via feast (pyproject.toml) +google-crc32c==1.8.0 \ + --hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \ + --hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \ + --hash=sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff \ + --hash=sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 \ + --hash=sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411 \ + --hash=sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a \ + --hash=sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 \ + --hash=sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb \ + --hash=sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa \ + --hash=sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962 \ + --hash=sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215 \ + --hash=sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b \ + --hash=sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27 \ + --hash=sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113 \ + --hash=sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f \ + --hash=sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f \ + --hash=sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d \ + --hash=sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 \ + --hash=sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092 \ + --hash=sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7 \ + --hash=sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2 \ + --hash=sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93 \ + --hash=sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8 \ + --hash=sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21 \ + --hash=sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 \ + --hash=sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2 \ + --hash=sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc \ + --hash=sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454 \ + --hash=sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2 \ + --hash=sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733 \ + --hash=sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697 \ + --hash=sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651 \ + --hash=sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c # via # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 +google-resumable-media==2.10.0 \ + --hash=sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c \ + --hash=sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.70.0 \ - --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ - --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +googleapis-common-protos[grpc]==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # grpc-google-iam-v1 # grpcio-status -grpc-google-iam-v1==0.14.2 \ - --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ - --hash=sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20 +grpc-google-iam-v1==0.14.4 \ + --hash=sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038 \ + --hash=sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 # via google-cloud-bigtable -grpcio==1.62.3 \ - --hash=sha256:059444f0ed5dba73ab7dd0ee7e8e6b606df4130d2b0a9f010f84da4ab9f6c2d8 \ - --hash=sha256:114f2a865886ff33f85d70670e971fe0e3d252a1209656fefa5470286e3fcc76 \ - --hash=sha256:13571a5b868dcc308a55d36669a2d17d9dcd6ec8335213f6c49cc68da7305abe \ - --hash=sha256:1ac0944e9e3ee3e20825226d1e17985e9f88487055c475986cf0922a7d806d8a \ - --hash=sha256:1de3d04d9a4ec31ebe848ae1fe61e4cbc367fb9495cbf6c54368e60609a998d9 \ - --hash=sha256:216740723fc5971429550c374a0c039723b9d4dcaf7ba05227b7e0a500b06417 \ - --hash=sha256:25cd75dc73c5269932413e517be778640402f18cf9a81147e68645bd8af18ab0 \ - --hash=sha256:325c56ce94d738c31059cf91376f625d3effdff8f85c96660a5fd6395d5a707f \ - --hash=sha256:3737e5ef0aa0fcdfeaf3b4ecc1a6be78b494549b28aec4b7f61b5dc357f7d8be \ - --hash=sha256:377babc817e8b4186aed7ed56e832867c513e4e9b6c3503565c344ffdef440d4 \ - --hash=sha256:3fb7d966a976d762a31346353a19fce4afcffbeda3027dd563bc8cb521fcf799 \ - --hash=sha256:43670a25b752b7ed960fcec3db50ae5886dc0df897269b3f5119cde9b731745f \ - --hash=sha256:4439bbd759636e37b66841117a66444b454937e27f0125205d2d117d7827c643 \ - --hash=sha256:454a6aed4ebd56198d37e1f3be6f1c70838e33dd62d1e2cea12f2bcb08efecc5 \ - --hash=sha256:4c9c1502c76cadbf2e145061b63af077b08d5677afcef91970d6db87b30e2f8b \ - --hash=sha256:4dab8b64c438e19c763a6332b55e5efdbecfb7c55ae59a42c38c81ed27955fa5 \ - --hash=sha256:56757d3e4cf5d4b98a30f2c5456151607261c891fa2298a4554848dcbf83083d \ - --hash=sha256:57823dc7299c4f258ae9c32fd327d29f729d359c34d7612b36e48ed45b3ab8d0 \ - --hash=sha256:582bd03e9c3d1bd1162eb51fa0f1a35633d66e73f4f36702d3b8484a8b45eda7 \ - --hash=sha256:620165df24aae3d5b3e84cb8dd6b98f6ed49aed04126186bbf43061e301d6a21 \ - --hash=sha256:646c14e9f3356d3f34a65b58b0f8d08daa741ba1d4fcd4966b79407543332154 \ - --hash=sha256:668211f3699bbee4deaf1d6e6b8df59328bf63f077bf2dc9b8bfa4a17df4a279 \ - --hash=sha256:6be243f3954b0ca709f56f9cae926c84ac96e1cce19844711e647a1f1db88b99 \ - --hash=sha256:6da20a1ae010a988bc4ed47850f1122de0a88e18cd2f901fcf56007be1fc6c30 \ - --hash=sha256:7349cd7445ac65fbe1b744dcab9cc1ec02dae2256941a2e67895926cbf7422b4 \ - --hash=sha256:74f3fc9b93290e58264844f5bc46df4c58a94c4287a277dbcf75344fc6c37ca4 \ - --hash=sha256:75a4e9ac7ff185cad529f35934c5d711b88aca48b90c70e195f5657da50ce321 \ - --hash=sha256:7b33c1807d4ac564a3027d06f21a2220c116ceacaaef614deb96b3341ee58896 \ - --hash=sha256:807176971c504c598976f5a9ea62363cffbbbb6c7509d9808c2342b020880fa2 \ - --hash=sha256:80a82fdee14dc27e9299248b7aabd5a8739a1cf6b76c78aa2b848158b44a99d5 \ - --hash=sha256:81b7c121c4e52a0749bf0759185b8d5cfa48a786cd7d411cdab08269813e0aab \ - --hash=sha256:8257cc9e55fb0e2149a652d9dc14c023720f9e73c9145776e07c97e0a553922e \ - --hash=sha256:8a5f00b2508937952d23a1767739e95bbbe1120f8a66d10187d5e971d56bb55c \ - --hash=sha256:8ae2e7a390b2cdd2a95d3bf3b3385245eeb48a5e853943cb46139666462c2d1a \ - --hash=sha256:940459d81685549afdfe13a6de102c52ea4cdda093477baa53056884aadf7c48 \ - --hash=sha256:9c4aae4e683776c319169d87e7891b67b75e3f1c0beeb877902ea148b0585164 \ - --hash=sha256:9d5f8e0050a179b3bce9189b522dc91008d44f08c757a7c310e0fd06b4d3d147 \ - --hash=sha256:a1b85d35a7d9638c03321dfe466645b87e23c30df1266f9e04bbb5f44e7579a9 \ - --hash=sha256:a82410d7620c07cb32624e38f2a106980564dfef9dbe78f5b295cda9ef217c03 \ - --hash=sha256:abfe64811177e681edc81d9d9d1bd23edc5f599bd9846650864769264ace30cd \ - --hash=sha256:ac9783d5679c8da612465168c820fd0b916e70ec5496c840bddba0be7f2d124c \ - --hash=sha256:b033d50bd41e506e3b579775f54a30c16c222e0d88847ac8098d2eca2a7454cc \ - --hash=sha256:b097347441b86a8c3ad9579abaf5e5f7f82b1d74a898f47360433b2bca0e4536 \ - --hash=sha256:b708401ede2c4cb8943e8a713988fcfe6cbea105b07cd7fa7c8a9f137c22bddb \ - --hash=sha256:bd900e666bb68fff49703084be14407cd73b8a5752a7590cea98ec22de24fb5d \ - --hash=sha256:c118cfc80e2402a5595be36e9245ffd9b0e146f426cc40bdf60015bf183f8373 \ - --hash=sha256:c175b252d063af388523a397dbe8edbc4319761f5ee892a8a0f5890acc067362 \ - --hash=sha256:c8bb1a7aa82af6c7713cdf9dcb8f4ea1024ac7ce82bb0a0a82a49aea5237da34 \ - --hash=sha256:c95a0b76a44c548e6bd8c5f7dbecf89c77e2e16d3965be817b57769c4a30bea2 \ - --hash=sha256:e202e3f963480ca067a261179b1ac610c0f0272cb4a7942d11b7e2b3fc99c3aa \ - --hash=sha256:e9ffdb7bc9ccd56ec201aec3eab3432e1e820335b5a16ad2b37e094218dcd7a6 \ - --hash=sha256:ea7ca66a58421411c6486fa5015fe7704e2816ff0b4ec4fb779ad5e1cbbdabf3 \ - --hash=sha256:f2ff8ac447765e173842b554b31307b98b3bb1852710903ebb936e7efb7df6e5 \ - --hash=sha256:f5def814c5a4c90c8fe389c526ab881f4a28b7e239b23ed8e02dd02934dfaa1a +grpcio==1.83.0 \ + --hash=sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df \ + --hash=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 \ + --hash=sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5 \ + --hash=sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df \ + --hash=sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930 \ + --hash=sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf \ + --hash=sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9 \ + --hash=sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33 \ + --hash=sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da \ + --hash=sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03 \ + --hash=sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa \ + --hash=sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223 \ + --hash=sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9 \ + --hash=sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4 \ + --hash=sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf \ + --hash=sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881 \ + --hash=sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af \ + --hash=sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5 \ + --hash=sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0 \ + --hash=sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727 \ + --hash=sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb \ + --hash=sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a \ + --hash=sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735 \ + --hash=sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16 \ + --hash=sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49 \ + --hash=sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1 \ + --hash=sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24 \ + --hash=sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b \ + --hash=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f \ + --hash=sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57 \ + --hash=sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf \ + --hash=sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f \ + --hash=sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c \ + --hash=sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969 \ + --hash=sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd \ + --hash=sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c \ + --hash=sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b \ + --hash=sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61 \ + --hash=sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404 \ + --hash=sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617 \ + --hash=sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40 \ + --hash=sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45 \ + --hash=sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc \ + --hash=sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c \ + --hash=sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745 \ + --hash=sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45 \ + --hash=sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c \ + --hash=sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0 \ + --hash=sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d \ + --hash=sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9 \ + --hash=sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8 # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-datastore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking # grpcio-reflection # grpcio-status + # milvus-lite # pymilvus -grpcio-health-checking==1.62.3 \ - --hash=sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93 \ - --hash=sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d - # via feast (setup.py) -grpcio-reflection==1.62.3 \ - --hash=sha256:a48ef37df81a3bada78261fc92ef382f061112f989d1312398b945cc69838b9c \ - --hash=sha256:cb84682933c400bddf94dd94f928d1c6570f500b6dd255973d4bfb495b82585f - # via feast (setup.py) -grpcio-status==1.62.3 \ - --hash=sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485 \ - --hash=sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8 +grpcio-health-checking==1.83.0 \ + --hash=sha256:7d8b47a5bfbc699d4aee0fc7a27f5d0265eb23a6a64db5ac2d8b749a0f8a9911 \ + --hash=sha256:ad6bc4d5a1103ad704d25ccd82def300dfa27996b185cab3e4cceeef2c6867d4 + # via feast (pyproject.toml) +grpcio-reflection==1.83.0 \ + --hash=sha256:6a2a30a462ac2c3c6d149734a8fe655fa7617c17fa2cda9949006f3bf07f5b3e \ + --hash=sha256:9d4151f68f18a85aa5b0eff5d493ad13f9421f96c8d9c01c840b353dc979be93 + # via feast (pyproject.toml) +grpcio-status==1.83.0 \ + --hash=sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae \ + --hash=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 # via google-api-core -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore + # httpcore2 # uvicorn -hiredis==2.4.0 \ - --hash=sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027 \ - --hash=sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577 \ - --hash=sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603 \ - --hash=sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869 \ - --hash=sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228 \ - --hash=sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79 \ - --hash=sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb \ - --hash=sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33 \ - --hash=sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77 \ - --hash=sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9 \ - --hash=sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd \ - --hash=sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a \ - --hash=sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8 \ - --hash=sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9 \ - --hash=sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901 \ - --hash=sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7 \ - --hash=sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0 \ - --hash=sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d \ - --hash=sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213 \ - --hash=sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8 \ - --hash=sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389 \ - --hash=sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330 \ - --hash=sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1 \ - --hash=sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2 \ - --hash=sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288 \ - --hash=sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba \ - --hash=sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded \ - --hash=sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca \ - --hash=sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109 \ - --hash=sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8 \ - --hash=sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea \ - --hash=sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6 \ - --hash=sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8 \ - --hash=sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0 \ - --hash=sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5 \ - --hash=sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5 \ - --hash=sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182 \ - --hash=sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73 \ - --hash=sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af \ - --hash=sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac \ - --hash=sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204 \ - --hash=sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277 \ - --hash=sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212 \ - --hash=sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41 \ - --hash=sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e \ - --hash=sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c \ - --hash=sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2 \ - --hash=sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4 \ - --hash=sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348 \ - --hash=sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b \ - --hash=sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e \ - --hash=sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995 \ - --hash=sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371 \ - --hash=sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613 \ - --hash=sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b \ - --hash=sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d \ - --hash=sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708 \ - --hash=sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08 \ - --hash=sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca \ - --hash=sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12 \ - --hash=sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8 \ - --hash=sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829 \ - --hash=sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38 \ - --hash=sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825 \ - --hash=sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67 \ - --hash=sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056 \ - --hash=sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376 \ - --hash=sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214 \ - --hash=sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982 \ - --hash=sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034 \ - --hash=sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1 \ - --hash=sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f \ - --hash=sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9 \ - --hash=sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5 \ - --hash=sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390 \ - --hash=sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0 \ - --hash=sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d \ - --hash=sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57 \ - --hash=sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe \ - --hash=sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2 \ - --hash=sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415 \ - --hash=sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c \ - --hash=sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b \ - --hash=sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853 \ - --hash=sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36 \ - --hash=sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae \ - --hash=sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd \ - --hash=sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187 \ - --hash=sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c \ - --hash=sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542 \ - --hash=sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d \ - --hash=sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79 \ - --hash=sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7 \ - --hash=sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def - # via feast (setup.py) +hiredis==3.4.0 \ + --hash=sha256:02298551060cbfe17b1acb02e9d066eadd51e37357369736f8fc1f5533875255 \ + --hash=sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc \ + --hash=sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865 \ + --hash=sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c \ + --hash=sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec \ + --hash=sha256:06c163607f163a300cba9ef02fd8a234dd644a208bfafe972614f72e5287b9ba \ + --hash=sha256:0731dd9f2bf4d3417947a3cbbca4dd4d9fa6ad7cecfd4e16439c17a4562d6304 \ + --hash=sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419 \ + --hash=sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320 \ + --hash=sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b \ + --hash=sha256:0bcb630add6bc9ea136fce691ddff0c46aa91cb860df4ca789fe44127eb7e90d \ + --hash=sha256:12ea5facb5b08fa23e4c101ec2151f3a3de8ecec412fec58dbde0a6eebca02c7 \ + --hash=sha256:12eca9aea1450d1a85dc15574a985c227e52abbc2b6466f48ad2aa3b82124701 \ + --hash=sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f \ + --hash=sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017 \ + --hash=sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc \ + --hash=sha256:16a3ea8193c5f4ee6e8daafe0126207cef38eec5a923c97047e6985d9bb1b61f \ + --hash=sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4 \ + --hash=sha256:1bfb9ccfb13be63883e5f2e5ff7f6fc87bf256f8243af594257dfbed9dbc3cf0 \ + --hash=sha256:1c9e141b420dae63ea21303a7e8672282326cae67472768e9a3f6e7f0990bd28 \ + --hash=sha256:1d8c21fb85f2a3de9328cf5bace84b63da00028e771b92335ff4fdc00121dd5a \ + --hash=sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5 \ + --hash=sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016 \ + --hash=sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240 \ + --hash=sha256:282c4310af72afbe18b07d416459f4febeaeb805a067a7df790136e0e550fcb2 \ + --hash=sha256:2c432cce38190c3c13b6c28d6840ee85db50765ad510134007f1cfb3844dce73 \ + --hash=sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591 \ + --hash=sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea \ + --hash=sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980 \ + --hash=sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b \ + --hash=sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb \ + --hash=sha256:386b556f48fb0f9f09696b9d37f1cae554123fbd9f091d0ca23087f2aca02887 \ + --hash=sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b \ + --hash=sha256:4190bd07dd7879a8a7ddbb2a4f74d402721f3898276e35beb98851b85b5f539c \ + --hash=sha256:423570ac4d2665c0d55d8707b7859a331e9001da5e437b7b7a23e0acbce770c9 \ + --hash=sha256:43004b0b48abc628dda1ac3ac4871e1326c126f8cd9f11164d61934d827d7a3b \ + --hash=sha256:4404c557fd49bcfe24dff41f1209e4221c76d1607df2fb2dfd39474b5b086dcb \ + --hash=sha256:44660a91e0fbc803c29b337c1a9194c8d7b4cd3a3868d28f747cbec2df165483 \ + --hash=sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582 \ + --hash=sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2 \ + --hash=sha256:45c6c296056641b5df37cedafe7d1553f33bc247e2f81603a4d038b39261879b \ + --hash=sha256:46e041fc7a83eaa989ef5fe09526bc2353a6ba39b4ce5191684eabefc02e17b0 \ + --hash=sha256:4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e \ + --hash=sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d \ + --hash=sha256:4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d \ + --hash=sha256:4e5530d7a6c71c1a9c3b850b08bbc8178ffc9556e3aef7f9c808fe9ff91a6f11 \ + --hash=sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763 \ + --hash=sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584 \ + --hash=sha256:5359caad5b57da0bce11d2880f22617ba3710f0866121a924745447848448034 \ + --hash=sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79 \ + --hash=sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9 \ + --hash=sha256:64cf54c1dad35830bf1d86b343ad3c55aed443af51c5ccead8afd1b547aca196 \ + --hash=sha256:6774f1fe2723001ca0cd42bf5d8b1235301226273915c581c5c1260d4d114c43 \ + --hash=sha256:68c81c04e0d8778c01163dd2bc6f0e0236fc7f650c291526f10d4faccd99e5a0 \ + --hash=sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb \ + --hash=sha256:69d0326f20354ce278cbb86f5ae47cb390e22bb94a66877031038af907c42fa5 \ + --hash=sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22 \ + --hash=sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b \ + --hash=sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9 \ + --hash=sha256:7b159315df71a009375227384aa4219f98c2342ccd8eb33e3f4b58654f426376 \ + --hash=sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203 \ + --hash=sha256:7f7fc1535f6e1a190089eae46dee25f0c6b72bb221d377be07092803b8208733 \ + --hash=sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f \ + --hash=sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13 \ + --hash=sha256:85a9abf2d16b4cb112f6cc6c32236d763b34d21c69b00c2f81a4ff3016fcc1d6 \ + --hash=sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c \ + --hash=sha256:899d5cea72c9a6c2d9b6e7b223de62c9e9f541f48267cc7135764070548735ce \ + --hash=sha256:89e95f50ec4d29645ba2da9010e8717861585dc9f3df354a367f43807a6db3c7 \ + --hash=sha256:8aaaab18314fd25453b5cf59c8cdca4110e419455bcb4c0737d19d4151513e75 \ + --hash=sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1 \ + --hash=sha256:8f2c8569460aa456a294d5e9a3579a382b825dc2e283e3cc398323dcfb6a3dee \ + --hash=sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151 \ + --hash=sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a \ + --hash=sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d \ + --hash=sha256:98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c \ + --hash=sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f \ + --hash=sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d \ + --hash=sha256:a315009b441a0105a373a9a780ebb1c6f7d9ead88ac6ea5f2a15791353c6f590 \ + --hash=sha256:a3796094f616f72976ff51e4dc1a016e753c0f9af5393b2df96920b6bae1e19b \ + --hash=sha256:a427976ba339da624367613d7eba52a1516e6d5c7f8988dc8fb888fbcf52d8b6 \ + --hash=sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb \ + --hash=sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530 \ + --hash=sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4 \ + --hash=sha256:b0c29ac4f50fc61a904acfe76e4ce6bc37e16ab5a98089cb411476d466e1bb36 \ + --hash=sha256:b32afb576705fc5a02440d416ef476ddbcc827a20ff6c4a9d557882bc6b75135 \ + --hash=sha256:b6228afacadb19a858289319d72797023e2cb048f2f930b68b7fe57eaed5fb6b \ + --hash=sha256:b98f44bf873731d9ce99e6b3eeb6196bf7b27693ad9f6de0fba986b2b0845127 \ + --hash=sha256:bb44efa4fa3e3ed7779ad0ade3c08ed5d75ca7a6336893e9a4f2722093b4168a \ + --hash=sha256:bb8b2dd9c00d80ad2870ffbb51ce9af99fb2194c082d7b09f4125d90e21426f6 \ + --hash=sha256:bca175f02a2b0150ffe7f5dc8bf49c798f34d2c7024d17ace0ec97a7583560e3 \ + --hash=sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59 \ + --hash=sha256:c2245c46b4ced5f689469e6dcdfc8a0895bf873840a6600f5ea759cdf1b26a8b \ + --hash=sha256:c8c6337e7ad187e8039d06e4b33ee5148dcd73928819d94087e649eb37316a09 \ + --hash=sha256:ccc5c660e31d788ca534a20f2ccb7a80b946b960e18ed4e1db950fcac122b405 \ + --hash=sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643 \ + --hash=sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c \ + --hash=sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4 \ + --hash=sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d \ + --hash=sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352 \ + --hash=sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81 \ + --hash=sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5 \ + --hash=sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092 \ + --hash=sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4 \ + --hash=sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad \ + --hash=sha256:e29267ecdd08758926f1a9221af2671d90f475480c40aff409921b1f362f1bd5 \ + --hash=sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d \ + --hash=sha256:ed1dba2695f6de009c67d63b39ff978cb43b8a79362f697acedffb7743e50d21 \ + --hash=sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1 \ + --hash=sha256:f3c67f39b112dc35f68d5b59ee111db6121f037d1a60cf3840ecffbb2ec5686b \ + --hash=sha256:f7c7596fbb2b5202e943180353958e89014e763c7f25877a92f70bbde6cd7f19 \ + --hash=sha256:ff28eca77a57751488df90b0f385b472fd78e140a4d45097224648a5737acdf5 \ + --hash=sha256:ff6217da86a63a6a5ea954d9b9ed67916d9d94a566b44aa98e68ae34b40ebbbf + # via feast (pyproject.toml) httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httpcore2==2.9.1 \ + --hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \ + --hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26 + # via httpx2 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - # via - # fastapi-mcp - # mcp -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d + # via fastapi-mcp +httpx2==2.9.1 \ + --hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \ + --hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a # via mcp -ibis-framework[duckdb]==9.5.0 \ - --hash=sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0 \ - --hash=sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84 - # via feast (setup.py) -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +ibis-framework[duckdb]==12.0.0 \ + --hash=sha256:0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 \ + --hash=sha256:238624f2c14fdab8382ca2f4f667c3cdb81e29844cd5f8db8a325d0743767c61 + # via feast (pyproject.toml) +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx + # httpx2 # requests # snowflake-connector-python # yarl jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via feast (setup.py) -jmespath==1.0.1 \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe + # via feast (pyproject.toml) +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 # via # aiobotocore # boto3 # botocore -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 - # via - # feast (setup.py) - # mcp +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via mcp jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -kubernetes==33.1.0 \ - --hash=sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5 \ - --hash=sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993 - # via feast (setup.py) +kubernetes==36.0.3 \ + --hash=sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7 \ + --hash=sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f + # via feast (pyproject.toml) +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ --hash=sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 # via partd -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ @@ -1191,439 +1353,478 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -marshmallow==4.0.1 \ - --hash=sha256:72f14ef346f81269dbddee891bac547dda1501e9e08b6a809756ea3dbb7936a1 \ - --hash=sha256:e1d860bd262737cb2d34e1541b84cb52c32c72c9474e3fe6f30f137ef8b0d97f - # via environs -mcp==1.12.4 \ - --hash=sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5 \ - --hash=sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789 +mcp==2.0.0 \ + --hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \ + --hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6 # via fastapi-mcp +mcp-types==2.0.0 \ + --hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \ + --hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379 + # via mcp mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -milvus-lite==2.4.12 \ - --hash=sha256:20087663e7b4385050b7ad08f1f03404426d4c87b1ff91d5a8723eee7fd49e88 \ - --hash=sha256:334037ebbab60243b5d8b43d54ca2f835d81d48c3cda0c6a462605e588deb05d \ - --hash=sha256:a0f3a5ddbfd19f4a6b842b2fd3445693c796cde272b701a1646a94c1ac45d3d7 \ - --hash=sha256:e8d4f7cdd5f731efd6faeee3715d280fd91a5f9b4d89312664d56401f65b1473 - # via - # feast (setup.py) - # pymilvus -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) -multidict==6.7.0 \ - --hash=sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32 \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ - --hash=sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff \ - --hash=sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8 \ - --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ - --hash=sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 \ - --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ - --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6 \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ - --hash=sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912 \ - --hash=sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c \ - --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6 \ - --hash=sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84 \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ - --hash=sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd \ - --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0 \ - --hash=sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e \ - --hash=sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 \ - --hash=sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc \ - --hash=sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7 \ - --hash=sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4 \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111 \ - --hash=sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e \ - --hash=sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84 \ - --hash=sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851 \ - --hash=sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb \ - --hash=sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ - --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5 \ - --hash=sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb \ - --hash=sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349 \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ - --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ - --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34 \ - --hash=sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85 \ - --hash=sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ - --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ - --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ - --hash=sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91 \ - --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40 \ - --hash=sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73 \ - --hash=sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4 \ - --hash=sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687 \ - --hash=sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 +milvus-lite==3.1.1 \ + --hash=sha256:0cbf8386a2e99b6464a5eda14cf8e31934b0e287e36c831e293aee614d9efc2e \ + --hash=sha256:0f343c9bf1e291d6c2fa615a52a57090ebbaf96b84bfb71f7eb73cd95cd7fc1c + # via feast (pyproject.toml) +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via feast (pyproject.toml) +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via # aiobotocore # aiohttp # yarl -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mypy==2.3.0 \ + --hash=sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491 \ + --hash=sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762 \ + --hash=sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5 \ + --hash=sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654 \ + --hash=sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd \ + --hash=sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe \ + --hash=sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc \ + --hash=sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3 \ + --hash=sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5 \ + --hash=sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329 \ + --hash=sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b \ + --hash=sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3 \ + --hash=sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7 \ + --hash=sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805 \ + --hash=sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f \ + --hash=sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e \ + --hash=sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7 \ + --hash=sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494 \ + --hash=sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373 \ + --hash=sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88 \ + --hash=sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee \ + --hash=sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2 \ + --hash=sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757 \ + --hash=sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a \ + --hash=sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b \ + --hash=sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461 \ + --hash=sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97 \ + --hash=sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4 \ + --hash=sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117 \ + --hash=sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36 \ + --hash=sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac \ + --hash=sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5 \ + --hash=sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556 \ + --hash=sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c \ + --hash=sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88 \ + --hash=sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568 \ + --hash=sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595 \ + --hash=sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f \ + --hash=sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1 \ + --hash=sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef \ + --hash=sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6 \ + --hash=sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db \ + --hash=sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff \ + --hash=sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60 \ + --hash=sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c # via sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf +numpy==2.5.1 \ + --hash=sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2 \ + --hash=sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d \ + --hash=sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1 \ + --hash=sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b \ + --hash=sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd \ + --hash=sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077 \ + --hash=sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a \ + --hash=sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e \ + --hash=sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277 \ + --hash=sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6 \ + --hash=sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75 \ + --hash=sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7 \ + --hash=sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1 \ + --hash=sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9 \ + --hash=sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21 \ + --hash=sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca \ + --hash=sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0 \ + --hash=sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb \ + --hash=sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d \ + --hash=sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75 \ + --hash=sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74 \ + --hash=sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf \ + --hash=sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0 \ + --hash=sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8 \ + --hash=sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af \ + --hash=sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a \ + --hash=sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4 \ + --hash=sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22 \ + --hash=sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3 \ + --hash=sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1 \ + --hash=sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b \ + --hash=sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1 \ + --hash=sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373 \ + --hash=sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95 \ + --hash=sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6 \ + --hash=sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09 \ + --hash=sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9 \ + --hash=sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438 \ + --hash=sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2 \ + --hash=sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7 \ + --hash=sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace \ + --hash=sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3 \ + --hash=sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2 \ + --hash=sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes + # faiss-cpu # ibis-framework + # milvus-lite # pandas # pandas-gbq - # pyarrow oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 - # via - # kubernetes - # requests-oauthlib -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f + # via requests-oauthlib +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef + # via mcp +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via pymilvus +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # dask # db-dtypes + # faiss-cpu # google-cloud-bigquery # gunicorn # ibis-framework @@ -1686,7 +1887,7 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery @@ -1694,9 +1895,9 @@ pandas==2.3.3 \ # pandas-gbq # pymilvus # snowflake-connector-python -pandas-gbq==0.29.2 \ - --hash=sha256:99f6b642bd8340e96c2acdf7d717108aa0d31c703c2f284817ba7dc3b16110b2 \ - --hash=sha256:a3a28e7de4f8456fc4b8e9b7472b9bc470d2566f89c4cd997599e504f5a0a706 +pandas-gbq==0.35.0 \ + --hash=sha256:258de481019566611031919997bf9c1ece4ca30a4dd02d3fc3664b251d446182 \ + --hash=sha256:596c908487ef0649a161e86ef272c00c267e581b95dc5ee0dc3518545b33bcfc # via google-cloud-bigquery parsy==2.2 \ --hash=sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 \ @@ -1706,164 +1907,162 @@ partd==1.4.2 \ --hash=sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f \ --hash=sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c # via dask -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via mypy -platformdirs==4.5.0 \ - --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ - --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 # via snowflake-connector-python -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via feast (setup.py) -propcache==0.4.1 \ - --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ - --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ - --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ - --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ - --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ - --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ - --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ - --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ - --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ - --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via feast (pyproject.toml) +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274 \ + --hash=sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.26.1 \ - --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ - --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +proto-plus==1.28.2 \ + --hash=sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501 \ + --hash=sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52 # via # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==6.32.1 \ - --hash=sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346 \ - --hash=sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4 \ - --hash=sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1 \ - --hash=sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085 \ - --hash=sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1 \ - --hash=sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710 \ - --hash=sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122 \ - --hash=sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281 \ - --hash=sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d +protobuf==7.35.1 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -1875,235 +2074,275 @@ protobuf==6.32.1 \ # grpcio-status # proto-plus # pymilvus -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 - # via feast (setup.py) -psycopg[c, pool]==3.2.5 \ - --hash=sha256:b782130983e5b3de30b4c529623d3687033b4dafa05bb661fc6bf45837ca5879 \ - --hash=sha256:f5f750611c67cb200e85b408882f29265c66d1de7f813add4f8125978bfd70e8 - # via feast (setup.py) -psycopg-c==3.2.5 \ - --hash=sha256:57ad4cfd28de278c424aaceb1f2ad5c7910466e315dfe84e403f3c7a0a2ce81b +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # feast (pyproject.toml) + # pandas-gbq +psycopg[c, pool]==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via feast (pyproject.toml) +psycopg-c==3.3.4 \ + --hash=sha256:ed8106128b2d04359c185fc9641b4409abfce4d0b6fb1d1ff6800646e27f1a22 # via psycopg -psycopg-pool==3.2.6 \ - --hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \ - --hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7 +psycopg-pool==3.3.1 \ + --hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \ + --hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c # via psycopg -pyarrow==17.0.0 \ - --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ - --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ - --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ - --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ - --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ - --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ - --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ - --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ - --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ - --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ - --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ - --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ - --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ - --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ - --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ - --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ - --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ - --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ - --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ - --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ - --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ - --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ - --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ - --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ - --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ - --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ - --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ - --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ - --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ - --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ - --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ - --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ - --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ - --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ - --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ - --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 +pyarrow==25.0.0 \ + --hash=sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be \ + --hash=sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc \ + --hash=sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec \ + --hash=sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849 \ + --hash=sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e \ + --hash=sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8 \ + --hash=sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d \ + --hash=sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537 \ + --hash=sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062 \ + --hash=sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e \ + --hash=sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104 \ + --hash=sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517 \ + --hash=sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e \ + --hash=sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5 \ + --hash=sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887 \ + --hash=sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1 \ + --hash=sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b \ + --hash=sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be \ + --hash=sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778 \ + --hash=sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e \ + --hash=sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194 \ + --hash=sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2 \ + --hash=sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9 \ + --hash=sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b \ + --hash=sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18 \ + --hash=sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7 \ + --hash=sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc \ + --hash=sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62 \ + --hash=sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0 \ + --hash=sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f \ + --hash=sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e \ + --hash=sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862 \ + --hash=sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50 \ + --hash=sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe \ + --hash=sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3 \ + --hash=sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a \ + --hash=sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4 \ + --hash=sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af \ + --hash=sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b \ + --hash=sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0 \ + --hash=sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f \ + --hash=sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec \ + --hash=sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery # ibis-framework + # milvus-lite # pandas-gbq # snowflake-connector-python pyarrow-hotfix==0.7 \ --hash=sha256:3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 \ --hash=sha256:59399cd58bdd978b2e42816a4183a55c6472d4e33d183351b6069f11ed42661d # via ibis-framework -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 # via google-auth -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi # fastapi-mcp # mcp + # mcp-types # pydantic-settings -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pydantic-settings==2.11.0 \ - --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ - --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c - # via - # fastapi-mcp - # mcp +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f + # via fastapi-mcp pydata-google-auth==1.9.1 \ --hash=sha256:0a51ce41c601ca0bc69b8795bf58bedff74b4a6a007c9106c7cbcdec00eaced2 \ --hash=sha256:75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 # via pandas-gbq -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via - # feast (setup.py) + # feast (pyproject.toml) # rich -pyjwt==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via - # feast (setup.py) + # feast (pyproject.toml) + # mcp # snowflake-connector-python -pymilvus==2.4.9 \ - --hash=sha256:0937663700007c23a84cfc0656160b301f6ff9247aaec4c96d599a6b43572136 \ - --hash=sha256:45313607d2c164064bdc44e0f933cb6d6afa92e9efcc7f357c5240c57db58fbe - # via feast (setup.py) -pymysql==1.1.2 \ - --hash=sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03 \ - --hash=sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 - # via feast (setup.py) -pyopenssl==25.3.0 \ - --hash=sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6 \ - --hash=sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329 +pymilvus==3.0.1 \ + --hash=sha256:c02389059088b18d6e598cd175541e445c772fab4926c5e527c4913be34887f1 \ + --hash=sha256:c5a8d5c1fa1de7b416e3529d383d8cc2e7da2170433ffa4a2d9087e14f70171a + # via feast (pyproject.toml) +pymysql==1.2.0 \ + --hash=sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 \ + --hash=sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33 + # via feast (pyproject.toml) +pyopenssl==26.3.0 \ + --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ + --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 # via snowflake-connector-python python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ @@ -2115,22 +2354,21 @@ python-dateutil==2.9.0.post0 \ # ibis-framework # kubernetes # pandas -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via - # environs # pydantic-settings + # pymilvus # uvicorn -python-multipart==0.0.20 \ - --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \ - --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13 +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 # via mcp -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via - # ibis-framework # pandas # snowflake-connector-python pyyaml==6.0.3 \ @@ -2208,30 +2446,31 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # kubernetes # uvicorn -redis==4.6.0 \ - --hash=sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d \ - --hash=sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c - # via feast (setup.py) +redis==7.4.1 \ + --hash=sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e \ + --hash=sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + # via feast (pyproject.toml) referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 # via # jsonschema # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # google-api-core # google-cloud-bigquery # google-cloud-storage # kubernetes + # pymilvus # requests-oauthlib # snowflake-connector-python requests-oauthlib==2.0.0 \ @@ -2240,188 +2479,144 @@ requests-oauthlib==2.0.0 \ # via # google-auth-oauthlib # kubernetes -rich==13.9.4 \ - --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ - --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via # fastapi-mcp # ibis-framework # typer -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==2026.6.3 \ + --hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \ + --hash=sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680 \ + --hash=sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9 \ + --hash=sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538 \ + --hash=sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804 \ + --hash=sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf \ + --hash=sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4 \ + --hash=sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97 \ + --hash=sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6 \ + --hash=sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96 \ + --hash=sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a \ + --hash=sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187 \ + --hash=sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975 \ + --hash=sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f \ + --hash=sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703 \ + --hash=sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9 \ + --hash=sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127 \ + --hash=sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f \ + --hash=sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa \ + --hash=sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05 \ + --hash=sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171 \ + --hash=sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba \ + --hash=sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c \ + --hash=sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223 \ + --hash=sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4 \ + --hash=sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885 \ + --hash=sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698 \ + --hash=sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f \ + --hash=sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7 \ + --hash=sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed \ + --hash=sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f \ + --hash=sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf \ + --hash=sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e \ + --hash=sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f \ + --hash=sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24 \ + --hash=sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a \ + --hash=sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41 \ + --hash=sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc \ + --hash=sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d \ + --hash=sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146 \ + --hash=sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e \ + --hash=sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e \ + --hash=sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4 \ + --hash=sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12 \ + --hash=sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7 \ + --hash=sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261 \ + --hash=sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6 \ + --hash=sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5 \ + --hash=sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93 \ + --hash=sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7 \ + --hash=sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda \ + --hash=sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8 \ + --hash=sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342 \ + --hash=sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c \ + --hash=sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb \ + --hash=sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0 \ + --hash=sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77 \ + --hash=sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3 \ + --hash=sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885 \ + --hash=sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826 \ + --hash=sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617 \ + --hash=sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb \ + --hash=sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577 \ + --hash=sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80 \ + --hash=sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e \ + --hash=sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945 \ + --hash=sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90 \ + --hash=sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7 \ + --hash=sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0 \ + --hash=sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140 \ + --hash=sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822 \ + --hash=sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba \ + --hash=sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9 \ + --hash=sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4 \ + --hash=sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a \ + --hash=sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8 \ + --hash=sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf \ + --hash=sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4 \ + --hash=sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324 \ + --hash=sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53 \ + --hash=sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b \ + --hash=sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41 \ + --hash=sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9 \ + --hash=sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca \ + --hash=sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1 \ + --hash=sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d \ + --hash=sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690 \ + --hash=sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107 \ + --hash=sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2 \ + --hash=sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76 \ + --hash=sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d \ + --hash=sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af \ + --hash=sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6 \ + --hash=sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db \ + --hash=sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369 \ + --hash=sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd \ + --hash=sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911 \ + --hash=sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504 \ + --hash=sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a \ + --hash=sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9 \ + --hash=sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13 \ + --hash=sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc \ + --hash=sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278 \ + --hash=sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868 \ + --hash=sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2 \ + --hash=sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd \ + --hash=sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4 \ + --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6 \ + --hash=sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9 \ + --hash=sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00 \ + --hash=sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f \ + --hash=sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e \ + --hash=sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442 \ + --hash=sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da \ + --hash=sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90 \ + --hash=sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef # via # jsonschema # referencing -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via google-auth -s3transfer==0.13.1 \ - --hash=sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724 \ - --hash=sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf +s3transfer==0.19.2 \ + --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \ + --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25 # via boto3 -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq # pydata-google-auth - # pymilvus shellingham==1.5.4 \ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de @@ -2432,209 +2627,223 @@ six==1.17.0 \ # via # kubernetes # python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio -snowflake-connector-python[pandas]==3.18.0 \ - --hash=sha256:0af10b207af3d2de2b130e89018d49a60f2e5cfe841f3bf459e58f2e1c4c4506 \ - --hash=sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628 \ - --hash=sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4 \ - --hash=sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e \ - --hash=sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee \ - --hash=sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9 \ - --hash=sha256:32b1abfea32561d817b0a2f80b06d936cb32712af06bf7b848a428bfd857a10a \ - --hash=sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a \ - --hash=sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728 \ - --hash=sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800 \ - --hash=sha256:4ed2d593f1983939d5d8d88b212d86fd4f14f0ceefc1df9882b4a18534adbde9 \ - --hash=sha256:51eb789a09dc6c62119cfabd044fba1a6b8378206f05a1e83ddb2e9cb49acc0b \ - --hash=sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc \ - --hash=sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb \ - --hash=sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba \ - --hash=sha256:783a9ab206563d7b52fdcdd7a72af62de811d3381ca64132fd3445537b4d041b \ - --hash=sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f \ - --hash=sha256:8d3e96e1d09b07edca6c1f6ca675b6fdd05a4a7e428e4cdf6fb697d87b9f60fc \ - --hash=sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a \ - --hash=sha256:a8c570edff5a4888840dbe1e9e65c5e4d77d55c5c800cd359fe0903a769201e0 \ - --hash=sha256:aeeb181a156333480f60b5f8ddbb3d087e288b4509adbef7993236defe4d7570 \ - --hash=sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca \ - --hash=sha256:b99f261c82be92224ac20c8c12bdf26ce3ed5dfd8a3df8a97f15a1e11c46ad27 \ - --hash=sha256:bd1de3038b6d7059ca59f93e105aba2a673151c693cc4292f72f38bfaf147df2 \ - --hash=sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e \ - --hash=sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00 - # via feast (setup.py) +snowflake-connector-python[pandas]==4.7.1 \ + --hash=sha256:051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117 \ + --hash=sha256:052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7 \ + --hash=sha256:0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317 \ + --hash=sha256:167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf \ + --hash=sha256:171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a \ + --hash=sha256:1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6 \ + --hash=sha256:22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf \ + --hash=sha256:2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b \ + --hash=sha256:4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a \ + --hash=sha256:4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754 \ + --hash=sha256:5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9 \ + --hash=sha256:571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d \ + --hash=sha256:5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35 \ + --hash=sha256:7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0 \ + --hash=sha256:7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5 \ + --hash=sha256:81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57 \ + --hash=sha256:8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a \ + --hash=sha256:92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3 \ + --hash=sha256:b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45 \ + --hash=sha256:d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa \ + --hash=sha256:e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327 \ + --hash=sha256:f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c \ + --hash=sha256:f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7 \ + --hash=sha256:f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52 \ + --hash=sha256:fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1 \ + --hash=sha256:ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b + # via feast (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via snowflake-connector-python -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -sqlglot==25.20.2 \ - --hash=sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab \ - --hash=sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567 +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via feast (pyproject.toml) +sqlglot==30.14.0 \ + --hash=sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9 \ + --hash=sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636 # via ibis-framework -sse-starlette==3.0.2 \ - --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ - --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 \ + --hash=sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627 # via mcp -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via + # feast (pyproject.toml) # fastapi # mcp -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via feast (setup.py) + # sse-starlette +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via feast (pyproject.toml) tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via feast (pyproject.toml) toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via fastapi-mcp -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 # via snowflake-connector-python -toolz==0.12.1 \ - --hash=sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85 \ - --hash=sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # dask # ibis-framework # partd -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via feast (pyproject.toml) +truststore==0.10.4 \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 # via - # feast (setup.py) - # milvus-lite -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) -typer==0.19.2 \ - --hash=sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9 \ - --hash=sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca + # httpcore2 + # httpx2 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) +typer==0.27.0 \ + --hash=sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5 \ + --hash=sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1 # via fastapi-mcp -types-pymysql==1.1.0.20250916 \ - --hash=sha256:873eb9836bb5e3de4368cc7010ca72775f86e9692a5c7810f8c7f48da082e55b \ - --hash=sha256:98d75731795fcc06723a192786662bdfa760e1e00f22809c104fbb47bac5e29b - # via feast (setup.py) -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +types-pymysql==1.2.0.20260724 \ + --hash=sha256:71327a3fea0f680a280445f1b475c2307f68de05c2d3302a79df56af93c574cd \ + --hash=sha256:8493cf87e8df0a7b4e8c84305bc0aae69d3a8af9c6da7bfbb9a4c5c3f97d4a82 + # via feast (pyproject.toml) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiohttp # aiosignal # anyio # fastapi + # grpcio + # httpx2 # ibis-framework + # mcp + # mcp-types # mypy + # opentelemetry-api # psycopg # psycopg-pool # pydantic @@ -2645,555 +2854,510 @@ typing-extensions==4.15.0 \ # sqlalchemy # starlette # typeguard - # typer # typing-inspection typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via pydantic-settings -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via pandas -ujson==5.11.0 \ - --hash=sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80 \ - --hash=sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef \ - --hash=sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a \ - --hash=sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840 \ - --hash=sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53 \ - --hash=sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076 \ - --hash=sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab \ - --hash=sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d \ - --hash=sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89 \ - --hash=sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c \ - --hash=sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb \ - --hash=sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c \ - --hash=sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823 \ - --hash=sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5 \ - --hash=sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac \ - --hash=sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d \ - --hash=sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723 \ - --hash=sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6 \ - --hash=sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0 \ - --hash=sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328 \ - --hash=sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0 \ - --hash=sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04 \ - --hash=sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25 \ - --hash=sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49 \ - --hash=sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec \ - --hash=sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3 \ - --hash=sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3 \ - --hash=sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc \ - --hash=sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a \ - --hash=sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9 \ - --hash=sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302 \ - --hash=sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694 \ - --hash=sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547 \ - --hash=sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01 \ - --hash=sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02 \ - --hash=sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6 \ - --hash=sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60 \ - --hash=sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915 \ - --hash=sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835 \ - --hash=sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701 \ - --hash=sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702 \ - --hash=sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50 \ - --hash=sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6 \ - --hash=sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc \ - --hash=sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a \ - --hash=sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c \ - --hash=sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c \ - --hash=sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03 \ - --hash=sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629 \ - --hash=sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105 \ - --hash=sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844 \ - --hash=sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241 \ - --hash=sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a \ - --hash=sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26 \ - --hash=sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac \ - --hash=sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596 \ - --hash=sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9 \ - --hash=sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752 \ - --hash=sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138 \ - --hash=sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b \ - --hash=sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba \ - --hash=sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5 \ - --hash=sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018 \ - --hash=sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362 \ - --hash=sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746 \ - --hash=sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f \ - --hash=sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88 \ - --hash=sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638 \ - --hash=sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b \ - --hash=sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9 \ - --hash=sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db \ - --hash=sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f \ - --hash=sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58 \ - --hash=sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b \ - --hash=sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433 \ - --hash=sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0 \ - --hash=sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764 \ - --hash=sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c \ - --hash=sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34 \ - --hash=sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052 \ - --hash=sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba \ - --hash=sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c \ - --hash=sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc \ - --hash=sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39 - # via pymilvus -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via + # ibis-framework + # pandas +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via # botocore # kubernetes # requests -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # mcp # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn websocket-client==1.9.0 \ --hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \ --hash=sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef # via kubernetes -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 # via aiobotocore -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8 \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf \ - --hash=sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890 \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737 \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba \ - --hash=sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9 \ - --hash=sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0 \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147 \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e \ - --hash=sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2 \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6 \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750 \ + --hash=sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621 \ + --hash=sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13 \ + --hash=sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1 \ + --hash=sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2 \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp diff --git a/sdk/python/requirements/py3.12-minimal-sdist-requirements-build.txt b/sdk/python/requirements/py3.12-minimal-sdist-requirements-build.txt index 5ab11423e44..c08740883fb 100644 --- a/sdk/python/requirements/py3.12-minimal-sdist-requirements-build.txt +++ b/sdk/python/requirements/py3.12-minimal-sdist-requirements-build.txt @@ -4,95 +4,147 @@ # # pybuild-deps compile --generate-hashes --output-file=sdk/python/requirements/py3.12-minimal-sdist-requirements-build.txt sdk/python/requirements/py3.12-minimal-sdist-requirements.txt # +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy calver==2025.3.31 \ --hash=sha256:07511edf5e7fa75ae97445c8c5921240e0fe62937289a3ebe6963eddd3c691b6 \ --hash=sha256:255d1a70bba8f97dc1eee3af4240ed35980508da69257feef94c79e5c6545fc7 # via trove-classifiers -cffi==2.0.0 \ - --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ - --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ - --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ - --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ - --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ - --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ - --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ - --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ - --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ - --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ - --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ - --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ - --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ - --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ - --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ - --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ - --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ - --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ - --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ - --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ - --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ - --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ - --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ - --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ - --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ - --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ - --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ - --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ - --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ - --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ - --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ - --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ - --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ - --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ - --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ - --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ - --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ - --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ - --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ - --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ - --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ - --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ - --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ - --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ - --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ - --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ - --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ - --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ - --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ - --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ - --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ - --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ - --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ - --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ - --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ - --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ - --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ - --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ - --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ - --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ - --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ - --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ - --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ - --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ - --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ - --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ - --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ - --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ - --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ - --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ - --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ - --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ - --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ - --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ - --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f # via cryptography cython==3.0.12 \ --hash=sha256:0038c9bae46c459669390e53a1ec115f8096b2e4647ae007ff1bf4e6dee92806 \ @@ -162,17 +214,120 @@ cython==3.0.12 \ # via # numpy # pandas - # pyarrow # pyyaml # snowflake-connector-python # sqlalchemy - # uvloop -dunamai==1.25.0 \ - --hash=sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab \ - --hash=sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1 +cython==3.1.1 \ + --hash=sha256:011cdcbf7725f0cfc1abc55ec83d326e788050711272131daf3cc24a19c34bb2 \ + --hash=sha256:020089f9c9f10269181f17660a2cada7d4577bd8eea24b7d2b14e6b64b6996be \ + --hash=sha256:07621e044f332d18139df2ccfcc930151fd323c2f61a58c82f304cffc9eb5280 \ + --hash=sha256:0de7adff5b42d2556d073e9f321c2faa639a17fb195ec1de130327f60ec209d8 \ + --hash=sha256:0e3ccec55e2a534a712db14c6617b66f65ad149c014fad518fc3920f6edde770 \ + --hash=sha256:0f7954b0b4b3302655d3caa6924261de5907a4e129bc22ace52fe9ae0cd5a758 \ + --hash=sha256:10f0434916994fe213ea7749268b88d77e3ebcbd1b99542cf64bb7d180f45470 \ + --hash=sha256:12e00b88147b03c148a95365f89dc1c45a0fc52f9c35aa75ff770ef65b615839 \ + --hash=sha256:141ffd6279411c562f6b707adc56b63e965a4fd7f21db83f5d4fcbd8c50ac546 \ + --hash=sha256:16d9870654946375b28280371d370d541641d1071da123d0d64d2c7ebba0cc56 \ + --hash=sha256:23b886a6c8a50b1101ccef2f2f3dc9c699b77633ef5bb5007090226c2ad3f9c2 \ + --hash=sha256:24c640c0746d984789fe2787a098f06cda456ef2dd78b90164d17884b350839a \ + --hash=sha256:263cb0e497910fb5e0a361ad1393b6d728b092178afecc56e8a786f3739960c3 \ + --hash=sha256:268420b92307ae6c5a16e3cf0e2ba1ae3c861650e992893922a0ce08db07cfdb \ + --hash=sha256:28b174f41718a7041cfbe0f48913020875ff1aaa4793942b2451ac6d2baf3f07 \ + --hash=sha256:307f216ed319ea07644f2ef9974406c830f01bc8e677e2147e9bfcdf9e3ca8ad \ + --hash=sha256:3192a61c2a532d3faccdff508bc8427de9530b587888218bfc0226eb33a84e11 \ + --hash=sha256:3fa4bd840de63509c74867b4b092541720a01db1e07351206011c34e0777dc96 \ + --hash=sha256:402f86c00b08f875cd0990f0c4dc52eb3e0bc5d630066cdf3c798631976f1937 \ + --hash=sha256:40f50b07c479eaf33981d81cad274c68cf9fb81dbe79cbf991f59491c88a4705 \ + --hash=sha256:426d78565eb91d3366569b20e92b8f14bffef5f57b2acd05b60bbb9ce5c056a1 \ + --hash=sha256:505ccd413669d5132a53834d792c707974248088c4f60c497deb1b416e366397 \ + --hash=sha256:50ad80e2f438e9127a87c10927e6ac16a987df39c248b19ab2cd31330129be3c \ + --hash=sha256:54a8934cb3bf13b1f8f6cbdae8e382e25a26e67de08ea6ebfd0a467131b67227 \ + --hash=sha256:56c6768a6f601f93daab7c2487f9f110548a896a91e00a6e119445ada2575323 \ + --hash=sha256:64915259276482fa23417b284d1fdc7e3a618ee2f819bb6ea7f974c075633df6 \ + --hash=sha256:689c1aad373556bd2ab1aa1c2dad8939a2891465a1fbd2cbbdd42b488fb40ec8 \ + --hash=sha256:6ea77ad1e649cec38f8622ba28dcdfbe7bf519bc132abbcf5df759b3975b5a73 \ + --hash=sha256:7489559e6c5ecbba49d535c2e03cf77c2594a3190b6aca7da5b508ba1664a89a \ + --hash=sha256:755a991601b27dd3555310d0f95b19a05e622a80d7b4e7a91fa6f5f3ef3f3b80 \ + --hash=sha256:7da069ca769903c5dee56c5f7ab47b2b7b91030eee48912630db5f4f3ec5954a \ + --hash=sha256:7e5cad896af896482240979b996bf4136b0d18dc40c56c72c5641bf0ea085dfb \ + --hash=sha256:7fff6526bb6f4eea615663117b86de6ede0d17c477b600d3d8302be3502bd3c3 \ + --hash=sha256:83b2af5c327f7da4f08afc34fddfaf6d24fa0c000b6b70a527c8125e493b6080 \ + --hash=sha256:873aac4ac0b0fb197557c0ac15458b780b9221daa4a716881cbd1a9016c8459f \ + --hash=sha256:8aaa29e763adf3496ab9d371e3caed8da5d3ce5ff8fb57433e2a2f2b5036e5c8 \ + --hash=sha256:953046c190fa9ab9a09a546a909b847cdbb4c1fe34e9bfa4a15b6ee1585a86aa \ + --hash=sha256:9b61b99205308c96b1162de59bd67ecadcad3d166a4a1f03a3d9e826c39cd375 \ + --hash=sha256:9d7dc0e4d0cd491fac679a61e9ede348c64ca449f99a284f9a01851aa1dbc7f6 \ + --hash=sha256:a19188ecd385cdc649e3fec370f38d5fd7f1651aeed0b3fb403180f38fc88e8a \ + --hash=sha256:a585796939b09b3205b1980e4a55e745c0251e45a5c637afbcac3c6cc9ad6f90 \ + --hash=sha256:a92f6bd395eadea6eed722a8188d3bdd49db1c9fa3c38710456d6148ab71bad7 \ + --hash=sha256:ab644415458d782c16ba7252de9cec1e3125371641cafea2e53a8c1cf85dd58d \ + --hash=sha256:af8f62cc9339b75fe8434325083e6a7cae88c9c21efd74bbb6ba4e3623219469 \ + --hash=sha256:b181158b5761bdaf40f6854f016ab7ddff64d3db4fca55cb3ca0f73813dd76d6 \ + --hash=sha256:b194a65a0fd91f305d2d1e7010f44111774a28533e1e44dd2a76e7de81a219b9 \ + --hash=sha256:b68f1bc80387554eb43f2b62795c173bed9e37201f39dc5084ac437c90a79c9f \ + --hash=sha256:c360823e1063784efc2335617e0f28573d7a594c5a8a05d85e850a9621cccb1f \ + --hash=sha256:c5cb6c054daadaf01a88c8f49f3edd9e829c9b76a82cbb4269e3f9878254540b \ + --hash=sha256:c740a10cd0f50321d048c8ca318eefb4c42b8bffef982dcd89c946d374192702 \ + --hash=sha256:c8b8be01fd40b3e38a76c60a524f956548a3a7566e5530a833a48a695f3d6c12 \ + --hash=sha256:cb5661941707bd41ec7a9c273d698113ac50392444f785088e9d9706c6a5937b \ + --hash=sha256:cd748fab8e4426dbcb2e0fa2979558333934d24365e0de5672fbabfe337d880c \ + --hash=sha256:cdf53dc4b2a13bd072d6c2c18ac073dbf0f798555bc27ba4f7546a275eb16a0f \ + --hash=sha256:ce82070ccf92c3599d331b9eaaefd9d4562976fb86a8d6bccf05c4a0b8389f2a \ + --hash=sha256:d14186bd96783d13b8fd0e5b289f2e137a8a25479638b73a1c7e4a99a8d70753 \ + --hash=sha256:dee554f0a589377bdaea0eb70e212bf3f35dc6a51a2aa86c9351345e21fd2f07 \ + --hash=sha256:dfa500fd7ae95ca152a5f8062b870532fa3e27efcef6d00612e1f28b9f72615f \ + --hash=sha256:dff0e7dd53a0ca35b64cda843253d5cac944db26663dc097b3a1adf2c49514ad \ + --hash=sha256:e000f0533eedf3d6dfbe30bb3c58a054c58f0a7778390342fa577a0dc47adab3 \ + --hash=sha256:e851ab66a31794e40df1bc6f649cdc56c998c637f5a1b9410c97a90f6b6cb855 \ + --hash=sha256:fd689910002adfac8734f237cdea1573e38345f27ed7fd445482813b65a29457 + # via grpcio +cython==3.2.9 \ + --hash=sha256:114b2dee0fa1daa48a59574d848da0ff1b6bdb725a755e9b92fad14962e1ff8d \ + --hash=sha256:1c2974742433be5c36ae1df1e761ce0063753fc2702316d5a3062b0a4e94a1df \ + --hash=sha256:23e80bc885c599e72072e18d0746df82d394b73100c1e153cda7359e6e59fe09 \ + --hash=sha256:2751b7c0cb13135aadcc1cc8fe223be98d458cd1132611d31675e768a5e4a2e9 \ + --hash=sha256:2b1756ddc3bc0cd4341a515fc420c3e25e13c249f5537159b3fb0bff8d19e55c \ + --hash=sha256:41637c644d7224d2d3170ee312077c2173693f0d4a0da1c82a0cffa3680a42dc \ + --hash=sha256:44b7fc417933d65bf31bce00337ff318efa3ea59daed21d10fe842d41e657c08 \ + --hash=sha256:4b1fd5a9c03f72a18618668a8e90d569442ed742f910e3ad003dcc9348e9598b \ + --hash=sha256:4b871ad97dd7fb1cbf56f6238c54423febd310afc1d9d9bc70c69c89b7ce57fc \ + --hash=sha256:522e277c29ebee304e73f3a78307549d09908b0c63098421a12dffc8d779fd74 \ + --hash=sha256:56d95c0674c25f281c6ae8f1d17bd425d6c2818bb304ff781831bb5d00d04b0b \ + --hash=sha256:57a6a78d14f7dd7d6062d9bca694e2a8c1c14113b6ceceea076abcd1161fdc5a \ + --hash=sha256:61d4abbf84f77c8d19361d05d9f51d65d8d95e74f736eae55fa1aed8a1430469 \ + --hash=sha256:63a42e131112072f912b66734088abc946c6bc42cb7454461a6dfdd2d09d3bae \ + --hash=sha256:681980fcc82b346ac83bc7d59e68ca1eba74ed9576f5d8584761d0c6c68c45d0 \ + --hash=sha256:788ea800618d20158166d09f60835aab50922ae1cfe3108c93fa72e173b7002d \ + --hash=sha256:7c534d782757710e39f3c9582214d7a28a0d68c86e78ee3df1d729cbc270e133 \ + --hash=sha256:7d41baea51ea00f9237f75af498577827493bca5e9b45bbd4e351543727e589a \ + --hash=sha256:8c843dd85857cd0d0da055489f448d032866120cfb094ce16205a1ff54d06353 \ + --hash=sha256:92989da161a7d18a7ad4baebc49289b2b77556d5a94916f90140ba26aecf6892 \ + --hash=sha256:944dc8747f640b3527649c566a5fc75ee0c15e80642ea2fdae4fe6378e1a9d4a \ + --hash=sha256:9d3cba9c4770cf76370fead8d2ae71bc474f3ef67ae4119e455d025726bcdc1c \ + --hash=sha256:a2b0e87f6b80790c929308ca0831d686f7a180feab684fe8cd4a4380bd96aaca \ + --hash=sha256:a3fc783d12202d1b064b6f125772d85f00e36e62eee6b2e415f56d8fd2d2e7b2 \ + --hash=sha256:a5cd9c5f138cb052130b40ad3b6976d2180c35348410995812678f4636bd8f94 \ + --hash=sha256:c6d4a92a87d238231564b5cbf27a8d1e46b7e62040c66b5ff33778a16b22295e \ + --hash=sha256:cac792b0bde1c86d8f832e513cd061b7e682181cc5a6d843d487e6c8ae9d5fcb \ + --hash=sha256:ccb0bc6b8437cfcb04459a02bc5ecc58bf161ea11659438945ab5a1392ee39fe \ + --hash=sha256:d15c4d4ba8a3ad284124384f769465bfba45512aaa4c81caea8a165f79fc042a \ + --hash=sha256:d249c9022ab13286b17bd66f30609e800c5f95efeecb06168990c7a66cecde6c \ + --hash=sha256:ddb43637b7749b88644df4319e1c36f767e7fb71a92bb9942558c8de2f1cc5f4 \ + --hash=sha256:e75ec625d8f8781ced690b7a2f5c2d138067711cf24bb8fb68c872c30c2fefe5 \ + --hash=sha256:e8ec2d5a7b798c84d6779e7ca5318fd928b8fe9dd021106d714dc2e77cdc7555 \ + --hash=sha256:e9b6ebc6c74b4318eaa4e51e520dc8b95ebc7b262953c3ecb24131104681f14e \ + --hash=sha256:e9c39962f749bd40c6e8c5063d498bd3749947888306ffedb99a0cab3a69f995 \ + --hash=sha256:efd54fe07f808e7e82f6a04f370457ca02e770c948f0e2f83345ccc7ec8bb829 \ + --hash=sha256:f25b402a6f1eed34af2af57603a9aadcf594dacaced1655e925eb317fba3f337 \ + --hash=sha256:f2fe4bc1b8fa4130563b94a3614db9ea17e92a455bed00e6a5d03eaff434b33c \ + --hash=sha256:f815cd3387bd88ca9eeabc357ce4bdf884eef0ab2949db0fbd5e0b69fe5ee422 # via - # poetry-dynamic-versioning - # uv-dynamic-versioning + # pyarrow + # uvloop +dunamai==1.26.1 \ + --hash=sha256:2727d939c5b4257cb01ea404372803b477f5176e5a347c43beaf89cd5072e853 \ + --hash=sha256:3b46007bd65b00b4824ead0a1aee365fd22d0ec2b9c219497d4fd48f52860c8b + # via uv-dynamic-versioning expandvars==1.1.2 \ --hash=sha256:6c5822b7b756a99a356b915dd1267f52ab8a4efaa135963bd7f4bd5d368f71d7 \ --hash=sha256:d1652fe4e61914f5b88ada93aaedb396446f55ae4621de45c8cb9f66e5712526 @@ -192,13 +347,12 @@ flit-core==3.12.0 \ # idna # jinja2 # markdown-it-py - # marshmallow # mdurl # mypy-extensions # packaging # pathspec # pyproject-metadata - # roman-numerals-py + # roman-numerals # sphinx # sphinxcontrib-applehelp # sphinxcontrib-devhelp @@ -206,23 +360,18 @@ flit-core==3.12.0 \ # sphinxcontrib-qthelp # sphinxcontrib-serializinghtml # tomli + # truststore # typing-extensions # wheel -gitdb==4.0.12 \ - --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ - --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf - # via gitpython -gitpython==3.1.45 \ - --hash=sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c \ - --hash=sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77 - # via pymilvus hatch-fancy-pypi-readme==25.1.0 \ --hash=sha256:9c58ed3dff90d51f43414ce37009ad1d5b0f08ffc9fc216998a06380f01c0045 \ --hash=sha256:ce0134c40d63d874ac48f48ccc678b8f3b62b8e50e9318520d2bffc752eedaf3 # via # attrs # httpcore + # httpcore2 # httpx + # httpx2 # jsonschema # pydantic hatch-vcs==0.4.0 \ @@ -242,9 +391,9 @@ hatch-vcs==0.5.0 \ # via # filelock # platformdirs -hatchling==1.27.0 \ - --hash=sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6 \ - --hash=sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b +hatchling==1.31.0 \ + --hash=sha256:6b48ad4068a482ed7239b3a8215bc55b47aad3345d58dfc94e553c5d2d46211b \ + --hash=sha256:aac80bec8b6fe35e8480f1c335be8910fa210a0e6f735a139be205dadcacb544 # via # annotated-types # atpublic @@ -258,16 +407,22 @@ hatchling==1.27.0 \ # hatch-fancy-pypi-readme # hatch-vcs # httpcore + # httpcore2 # httpx + # httpx2 + # ibis-framework # jsonschema # jsonschema-specifications # mcp + # mcp-types + # opentelemetry-api # platformdirs # pyarrow-hotfix # pydantic # pydantic-settings # pygments # python-multipart + # redis # referencing # scikit-build-core # starlette @@ -279,9 +434,168 @@ hatchling==1.27.0 \ jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via - # poetry-dynamic-versioning - # uv-dynamic-versioning + # via uv-dynamic-versioning +libcst==1.8.6 \ + --hash=sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30 \ + --hash=sha256:06fc56335a45d61b7c1b856bfab4587b84cfe31e9d6368f60bb3c9129d900f58 \ + --hash=sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d \ + --hash=sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a \ + --hash=sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9 \ + --hash=sha256:0cbe17067055829607c5ba4afa46bfa4d0dd554c0b5a583546e690b7367a29b6 \ + --hash=sha256:16cfe0cfca5fd840e1fb2c30afb628b023d3085b30c3484a79b61eae9d6fe7ba \ + --hash=sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105 \ + --hash=sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330 \ + --hash=sha256:203ec2a83f259baf686b9526268cd23d048d38be5589594ef143aee50a4faf7e \ + --hash=sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1 \ + --hash=sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786 \ + --hash=sha256:25fc7a1303cad7639ad45ec38c06789b4540b7258e9a108924aaa2c132af4aca \ + --hash=sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012 \ + --hash=sha256:351ab879c2fd20d9cb2844ed1ea3e617ed72854d3d1e2b0880ede9c3eea43ba8 \ + --hash=sha256:36473e47cb199b7e6531d653ee6ffed057de1d179301e6c67f651f3af0b499d6 \ + --hash=sha256:3649a813660fbffd7bc24d3f810b1f75ac98bd40d9d6f56d1f0ee38579021073 \ + --hash=sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86 \ + --hash=sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474 \ + --hash=sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8 \ + --hash=sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa \ + --hash=sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42 \ + --hash=sha256:455f49a93aea4070132c30ebb6c07c2dea0ba6c1fde5ffde59fc45dbb9cfbe4b \ + --hash=sha256:4d7bbdd35f3abdfb5ac5d1a674923572dab892b126a58da81ff2726102d6ec2e \ + --hash=sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196 \ + --hash=sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93 \ + --hash=sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58 \ + --hash=sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1 \ + --hash=sha256:59a7e388c57d21d63722018978a8ddba7b176e3a99bd34b9b84a576ed53f2978 \ + --hash=sha256:5dcaaebc835dfe5755bc85f9b186fb7e2895dda78e805e577fef1011d51d5a5c \ + --hash=sha256:6366ab2107425bf934b0c83311177f2a371bfc757ee8c6ad4a602d7cbcc2f363 \ + --hash=sha256:6421a930b028c5ef4a943b32a5a78b7f1bf15138214525a2088f11acbb7d3d64 \ + --hash=sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d \ + --hash=sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996 \ + --hash=sha256:6aa11df6c58812f731172b593fcb485d7ba09ccc3b52fea6c7f26a43377dc748 \ + --hash=sha256:6b23d14a7fc0addd9795795763af26b185deb7c456b1e7cc4d5228e69dab5ce8 \ + --hash=sha256:6cad63e3a26556b020b634d25a8703b605c0e0b491426b3e6b9e12ed20f09100 \ + --hash=sha256:6d8b67874f2188399a71a71731e1ba2d1a2c3173b7565d1cc7ffb32e8fbaba5b \ + --hash=sha256:72cca15800ffc00ba25788e4626189fe0bc5fe2a0c1cb4294bce2e4df21cc073 \ + --hash=sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb \ + --hash=sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4 \ + --hash=sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde \ + --hash=sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f \ + --hash=sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f \ + --hash=sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661 \ + --hash=sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1 \ + --hash=sha256:98fa1ca321c81fb1f02e5c43f956ca543968cc1a30b264fd8e0a2e1b0b0bf106 \ + --hash=sha256:a20c5182af04332cc94d8520792befda06d73daf2865e6dddc5161c72ea92cb9 \ + --hash=sha256:b0d8c364c44ae343937f474b2e492c1040df96d94530377c2f9263fb77096e4f \ + --hash=sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c \ + --hash=sha256:b6c1248cc62952a3a005792b10cdef2a4e130847be9c74f33a7d617486f7e532 \ + --hash=sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4 \ + --hash=sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9 \ + --hash=sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82 \ + --hash=sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7 \ + --hash=sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28 \ + --hash=sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf \ + --hash=sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e \ + --hash=sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5 \ + --hash=sha256:cb2679ef532f9fa5be5c5a283b6357cb6e9888a8dd889c4bb2b01845a29d8c0b \ + --hash=sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d \ + --hash=sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47 \ + --hash=sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09 \ + --hash=sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b \ + --hash=sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4 + # via pyarrow +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ @@ -373,32 +687,36 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -maturin==1.9.6 \ - --hash=sha256:0246202377c49449315305209f45c8ecef6e2d6bd27a04b5b6f1ab3e4ea47238 \ - --hash=sha256:1b39a5d82572c240d20d9e8be024d722dfb311d330c5e28ddeb615211755941a \ - --hash=sha256:26e3ab1a42a7145824210e9d763f6958f2c46afb1245ddd0bab7d78b1f59bb3f \ - --hash=sha256:2c2ae37144811d365509889ed7220b0598487f1278c2441829c3abf56cc6324a \ - --hash=sha256:5263dda3f71feef2e4122baf5c4620e4b3710dbb7f2121f85a337182de214369 \ - --hash=sha256:5c2252b0956bb331460ac750c805ddf0d9b44442449fc1f16e3b66941689d0bc \ - --hash=sha256:7ab827c6e8c022eb2e1e7fb6deede54549c8460b20ccc2e9268cc6e8cde957a8 \ - --hash=sha256:7f506eb358386d94d6ec3208c003130cf4b69cab26034fc0cbbf8bf83afa4c2e \ - --hash=sha256:7f53d3b1d8396d3fea3e1ee5fd37558bca5719090f3d194ba1c02b0b56327ae3 \ - --hash=sha256:ac02a30083553d2a781c10cd6f5480119bf6692fd177e743267406cad2ad198c \ - --hash=sha256:f2c58d29ebdd4346fd004e6be213d071fdd94a77a16aa91474a21a4f9dbf6309 \ - --hash=sha256:f2d6984ab690af509f525dbd2b130714207c06ebb14a5814edbe1e42b17ae0de \ - --hash=sha256:f5bac167700fbb6f8c8ed1a97b494522554b4432d7578e11403b894b6a91d99f \ - --hash=sha256:fe78262c2800c92f67d1ce3c0f6463f958a692cc67bfb572e5dbf5b4b696a8ba +maturin==1.14.1 \ + --hash=sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894 \ + --hash=sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0 \ + --hash=sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd \ + --hash=sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8 \ + --hash=sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4 \ + --hash=sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666 \ + --hash=sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df \ + --hash=sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224 \ + --hash=sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69 \ + --hash=sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65 \ + --hash=sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d \ + --hash=sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e \ + --hash=sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c \ + --hash=sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed # via + # ast-serialize # cryptography + # orjson # pydantic-core # rpds-py # watchfiles meson-python==0.15.0 \ --hash=sha256:3ae38253ff02b2e947a05e362a2eaf5a9a09d133c5666b4123399ee5fbf2e591 \ --hash=sha256:fddb73eecd49e89c1c41c87937cd89c2d0b65a1c63ba28238681d4bd9484d26f - # via - # numpy - # pandas + # via pandas +meson-python==0.20.0 \ + --hash=sha256:6a744cf0c09e76ecbdc58cfa374b48c8902dac1b74479628238634efbf36aec9 \ + --hash=sha256:6d9726ae6cd37e22f210c74b364b30180a68c20442e97ff09f3c566a414af738 + # via numpy meson==1.7.1 \ --hash=sha256:155780a5be87f6dd7f427ad8bcbf0f2b2c5f62ee5fdacca7caa9de8439a24b89 \ --hash=sha256:6d9cbc9ce87a70243c75e4cc668ee3f206ab50b184beb0a08ece948112f19bd7 @@ -409,301 +727,353 @@ mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf +numpy==2.5.1 \ + --hash=sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2 \ + --hash=sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d \ + --hash=sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1 \ + --hash=sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b \ + --hash=sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd \ + --hash=sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077 \ + --hash=sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a \ + --hash=sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e \ + --hash=sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277 \ + --hash=sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6 \ + --hash=sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75 \ + --hash=sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7 \ + --hash=sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1 \ + --hash=sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9 \ + --hash=sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21 \ + --hash=sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca \ + --hash=sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0 \ + --hash=sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb \ + --hash=sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d \ + --hash=sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75 \ + --hash=sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74 \ + --hash=sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf \ + --hash=sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0 \ + --hash=sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8 \ + --hash=sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af \ + --hash=sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a \ + --hash=sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4 \ + --hash=sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22 \ + --hash=sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3 \ + --hash=sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1 \ + --hash=sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b \ + --hash=sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1 \ + --hash=sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373 \ + --hash=sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95 \ + --hash=sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6 \ + --hash=sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09 \ + --hash=sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9 \ + --hash=sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438 \ + --hash=sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2 \ + --hash=sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7 \ + --hash=sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace \ + --hash=sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3 \ + --hash=sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2 \ + --hash=sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107 # via # pandas # pyarrow -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via - # dunamai # hatchling + # meson-python # pyproject-metadata # scikit-build-core + # setuptools-git-versioning # setuptools-scm -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # vcs-versioning + # wheel +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via # hatchling # mypy # scikit-build-core -pdm-backend==2.4.5 \ - --hash=sha256:56c019c440308adad5d057c08cbb777e65f43b991a3b0920749781258972fe5b \ - --hash=sha256:7f6c780b529baaf88947e12203af46b3ed45cff719b04c870b186ba9cdc2ddab +pdm-backend==2.4.9 \ + --hash=sha256:8d1064751dadb0c40b1026b46826a448d85c8228a564985c62fb53d4aba538a1 \ + --hash=sha256:c41a852ccbf4b567b033db9b2ae78660d7a4ea49307719959ab88a01f0aabb2e # via + # annotated-doc # fastapi # typer -pkgconfig==1.5.5 \ - --hash=sha256:d20023bbeb42ee6d428a0fac6e0904631f545985a10cdd71a20aa58bc47a4209 \ - --hash=sha256:deb4163ef11f75b520d822d9505c1f462761b4309b1bb713d08689759ea8b899 +pkgconfig==1.6.0 \ + --hash=sha256:4a5a6631ce937fafac457104a40d558785a658bbdca5c49b6295bc3fd651907f \ + --hash=sha256:98e71754855e9563838d952a160eb577edabb57782e49853edb5381927e6bea1 # via aiohttp pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via hatchling -poetry-core==1.9.1 \ - --hash=sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0 \ - --hash=sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8 +poetry-core==2.4.1 \ + --hash=sha256:89dceb6c10e9c6d8650a16183400e3c9ff9ddee13b0a81023b5575334a2b3744 \ + --hash=sha256:acf06f9537cd2625bdaec926d95d90b557ba15353bc71d27a3a8a441042b5316 # via + # aiohappyeyeballs # dunamai - # ibis-framework - # ibis-substrait # pkgconfig - # poetry-dynamic-versioning # rich - # rsa # tomlkit -poetry-core==2.2.1 \ - --hash=sha256:97e50d8593c8729d3f49364b428583e044087ee3def1e010c6496db76bd65ac5 \ - --hash=sha256:bdfce710edc10bfcf9ab35041605c480829be4ab23f5bc01202cfe5db8f125ab - # via aiohappyeyeballs -poetry-dynamic-versioning==1.9.1 \ - --hash=sha256:65a0c814e6d30d4807734a3c34edf261fd7cc3b340dbd23b6a33ee41f7d0b547 \ - --hash=sha256:d6e7b9df817aa2ca4946cd695c6c89e1379d2e6c640f008a9b6170d081a9da48 - # via ibis-framework -pybind11==3.0.1 \ - --hash=sha256:9c0f40056a016da59bab516efb523089139fcc6f2ba7e4930854c61efb932051 \ - --hash=sha256:aa8f0aa6e0a94d3b64adfc38f560f33f15e589be2175e103c0a33c6bce55ee89 +pybind11-global==3.0.4 \ + --hash=sha256:95b693c3d646c6b7217a97156a36b6d40305505d4a5a728082da6fe75ada29f5 \ + --hash=sha256:a73e2ebd29f4ee5d7c754b495d555cd703f2cd26c84abe360ee56411dcd7834d + # via pybind11 +pybind11==3.0.4 \ + --hash=sha256:3286b59c8a774b9ee650169302dd5a4eedc30a8617905a0560dd8ee44775130c \ + --hash=sha256:961720ee652da51d531b7b2451a6bd2bc042b0106e6d9baa48ecb7d58034ce63 # via duckdb -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pyproject-metadata==0.9.1 \ - --hash=sha256:b8b2253dd1b7062b78cf949a115f02ba7fa4114aabe63fa10528e9e1a954a816 \ - --hash=sha256:ee5efde548c3ed9b75a354fc319d5afd25e9585fa918a34f62f904cc731973ad +pyproject-metadata==0.12.1 \ + --hash=sha256:8809a4df6fe08279b39a8890669506ed3158e0617855ac9aff098fcbe772ae4c \ + --hash=sha256:f7162d580a96386a8eb096da06215f981f547d1490f03055ef99e323bc2da427 # via meson-python -scikit-build-core==0.11.6 \ - --hash=sha256:5982ccd839735be99cfd3b92a8847c6c196692f476c215da84b79d2ad12f9f1b \ - --hash=sha256:ce6d8fe64e6b4c759ea0fb95d2f8a68f60d2df31c2989838633b8ec930736360 +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via libcst +scikit-build-core==1.0.3 \ + --hash=sha256:a4d7a05978ee37975c37743510c8991e2debce7ef83afb0a07c0c576fd4f16e8 \ + --hash=sha256:ae95427b7d3c14a6cf8bbfd4d901f6138ab64c99e20cbe8ea7d75cd26093f085 # via + # duckdb # patchelf + # pyarrow # pybind11 + # pybind11-global semantic-version==2.10.0 \ --hash=sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c \ --hash=sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177 # via setuptools-rust -setuptools-rust==1.12.0 \ - --hash=sha256:7e7db90547f224a835b45f5ad90c983340828a345554a9a660bdb2de8605dcdd \ - --hash=sha256:d94a93f0c97751c17014565f07bdc324bee45d396cd1bba83d8e7af92b945f0c - # via maturin -setuptools-scm==7.1.0 \ - --hash=sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27 \ - --hash=sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e - # via python-dateutil -setuptools-scm==8.3.1 \ - --hash=sha256:332ca0d43791b818b841213e76b1971b7711a960761c5bea5fc5cdb5196fbce3 \ - --hash=sha256:3d555e92b75dacd037d32bafdf94f97af51ea29ae8c7b234cf94b7a5bd242a63 +setuptools-git-versioning==3.1.0 \ + --hash=sha256:3a68f3fd58a2a5e86b0792435cfa9d8e569ab60ee5e4c29228c09da9b637bf18 \ + --hash=sha256:612dfcf184addac9e1c2216f4f229724b2390e5bf613fb925ae80b84f2529172 + # via toolz +setuptools-rust==1.13.0 \ + --hash=sha256:666439c4d90e968bb625bcf87ec0aa02b4f49e599d2f5dc255c633a04e8eca99 \ + --hash=sha256:f2afcf4baeee689910ce49cfa8aad4e08cce72f417449bcc32891b8664fdc726 # via - # hatch-vcs - # urllib3 -setuptools-scm==9.2.1 \ - --hash=sha256:2f5b21e45205e47ed3976e58bc29c38f49bb9b6f07c625c7fd2823871694b244 \ - --hash=sha256:84c9b3f39ffb77ba00dd92a06e025e4f66dc69ab8df1c2a225a0678e76311f8d + # libcst + # maturin +setuptools-scm==10.2.1 \ + --hash=sha256:4fa7dd82cf8c800df59c9a288c90299b1657ff1ecfc3f5cc00287c5dbf5e27a9 \ + --hash=sha256:b7c82f4102d389ee57dc66ccdb4f9b4bca3c40ba83b43f1f63d68ccd72db2580 # via # anyio + # cachetools + # dask # duckdb # hatch-vcs - # httpx-sse + # libcst # pluggy # pyarrow # pybindgen - # pymilvus # setuptools-rust - # sniffio # sqlglot - # substrait # tabulate # tenacity # tqdm # typeguard - # ujson -smmap==5.0.2 \ - --hash=sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5 \ - --hash=sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e - # via gitdb -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # urllib3 +setuptools-scm==7.1.0 \ + --hash=sha256:6c508345a771aad7d56ebff0e70628bf2b0ec7573762be9960214730de278f27 \ + --hash=sha256:73988b6d848709e2af142aa48c986ea29592bbcfca5375678064708205253d8e + # via python-dateutil +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via fastapi-mcp -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via - # poetry-dynamic-versioning - # uv-dynamic-versioning -trove-classifiers==2025.9.11.17 \ - --hash=sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd \ - --hash=sha256:931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 + # via uv-dynamic-versioning +trove-classifiers==2026.6.1.19 \ + --hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \ + --hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745 # via hatchling types-psutil==7.0.0.20250218 \ --hash=sha256:1447a30c282aafefcf8941ece854e1100eee7b0296a9d9be9977292f0269b121 \ --hash=sha256:1e642cdafe837b240295b23b1cbd4691d80b08a07d29932143cbbae30eb0db9c # via mypy -types-setuptools==80.9.0.20250822 \ - --hash=sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965 \ - --hash=sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3 +types-setuptools==83.0.0.20260724 \ + --hash=sha256:eed1a91e1ed9d8ec9f3e71908e5bb17272e41e0852d37d1485ecd9cd33165e1a \ + --hash=sha256:fe2801176b667f1e8209f8571b0839ca6c34318d743825aeacf3f29970c8d46f # via mypy -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via # mypy - # pydantic-core # setuptools-scm -uv-dynamic-versioning==0.11.2 \ - --hash=sha256:4c785ea02e00b93896d015fd3613c451735af1715a7f5f52e8e4e1d76e6ec3c5 \ - --hash=sha256:563ab4b7a93904eb8fb8fd9f2959dad853d62baa71f3cdb0233acf8a749d512f - # via mcp +uv-dynamic-versioning==0.14.0 \ + --hash=sha256:574fbc07e87ace45c01d55967ad3b864871257b98ff5b8ac87c261227ac8db5b \ + --hash=sha256:e087c346a786e98d41292ac2315180fb700cedfb30565fc973d64ce11a112387 + # via + # httpcore2 + # httpx2 + # mcp + # mcp-types +vcs-versioning==2.2.3 \ + --hash=sha256:4f7873af76f5cc24e701608354f376f5eb70b317d663787ace57a7edd7a27506 \ + --hash=sha256:c21e284aa98c8269996791a19d6173baf953cf9996ef42ec3ebcbdc151e18b9b + # via setuptools-scm versioneer==0.29 \ --hash=sha256:0f1a137bb5d6811e96a79bb0486798aeae9b9c6efc24b389659cebb0ee396cb9 \ --hash=sha256:5ab283b9857211d61b53318b7c792cf68e798e765ee17c27ade9f6c924235731 # via - # dask # pandas # partd -wheel==0.45.1 \ - --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ - --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 +wheel==0.47.0 \ + --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \ + --hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3 # via - # cachetools # google-crc32c - # httpx-sse + # grpcio + # grpcio-health-checking + # grpcio-reflection + # grpcio-status + # libcst # meson # mmh3 # pandas # psycopg # psycopg-c # psycopg-pool - # pymilvus + # pycparser # python-dateutil + # setuptools-git-versioning # shellingham # snowflake-connector-python - # tabulate - # tqdm # tzdata # uvloop + # wrapt # The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 # via # aiobotocore # aiohttp @@ -717,19 +1087,21 @@ setuptools==80.9.0 \ # cryptography # dask # dill - # duckdb # frozenlist - # gitpython # google-api-core # google-cloud-bigquery # google-crc32c # googleapis-common-protos # greenlet # grpc-google-iam-v1 + # grpcio + # grpcio-health-checking + # grpcio-reflection + # grpcio-status # gunicorn # httptools - # httpx-sse - # ibis-substrait + # libcst + # librt # markupsafe # maturin # meson @@ -741,39 +1113,49 @@ setuptools==80.9.0 \ # pathspec # pluggy # prometheus-client - # propcache # proto-plus # psutil # psycopg - # psycopg-c # psycopg-pool - # pyarrow # pyasn1 # pyasn1-modules + # pycparser # pyjwt - # pymilvus # pymysql - # python-dateutil + # python-dotenv # pyyaml + # requests + # setuptools-git-versioning # setuptools-rust # setuptools-scm # shellingham - # sniffio + # snowballstemmer # snowflake-connector-python # sqlalchemy + # sqlglot # sse-starlette - # substrait # tabulate # tenacity + # toolz # tqdm # trove-classifiers # typeguard - # types-pymysql - # types-setuptools # tzdata - # ujson # uvloop + # vcs-versioning # versioneer # websockets # wrapt # yarl +setuptools==80.3.1 \ + --hash=sha256:31e2c58dbb67c99c289f51c16d899afedae292b978f8051efaf6262d8212f927 \ + --hash=sha256:ea8e00d7992054c4c592aeb892f6ad51fe1b4d90cc6947cc45c45717c40ec537 + # via psycopg-c +setuptools==83.0.0 \ + --hash=sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef \ + --hash=sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3 + # via + # propcache + # python-dateutil + # types-pymysql + # types-setuptools diff --git a/sdk/python/requirements/py3.12-minimal-sdist-requirements.txt b/sdk/python/requirements/py3.12-minimal-sdist-requirements.txt index 1f0390892be..a11b0b0c0b3 100644 --- a/sdk/python/requirements/py3.12-minimal-sdist-requirements.txt +++ b/sdk/python/requirements/py3.12-minimal-sdist-requirements.txt @@ -1,138 +1,139 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.12 --no-strip-extras setup.py --extra minimal-sdist-build --no-emit-package milvus-lite --generate-hashes --output-file sdk/python/requirements/py3.12-minimal-sdist-requirements.txt -aiobotocore==2.23.1 \ - --hash=sha256:a59f2a78629b97d52f10936b79c73de64e481a8c44a62c1871f088df6c1afc4f \ - --hash=sha256:d81c54d2eae2406ea9a473fea518fed580cf37bc4fc51ce43ba81546e5305114 - # via feast (setup.py) -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +# uv pip compile -p 3.12 --no-strip-extras pyproject.toml --extra minimal-sdist-build --no-emit-package milvus-lite --no-emit-package pymilvus --no-emit-package faiss-cpu --generate-hashes --output-file sdk/python/requirements/py3.12-minimal-sdist-requirements.txt +aiobotocore==3.8.0 \ + --hash=sha256:80a1eb64ea915f3af3c1518669975bae74a17b2f37c14eb0fa2f83b915974670 \ + --hash=sha256:8bc605132cadfe844a3f334635a0a64fa5e360a4a206e915d99d53db5b6deeba + # via feast (pyproject.toml) +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.0 \ - --hash=sha256:00243e51f16f6ec0fb021659d4af92f675f3cf9f9b39efd142aa3ad641d8d1e6 \ - --hash=sha256:00fdfe370cffede3163ba9d3f190b32c0cfc8c774f6f67395683d7b0e48cdb8a \ - --hash=sha256:019dbef24fe28ce2301419dd63a2b97250d9760ca63ee2976c2da2e3f182f82e \ - --hash=sha256:052bcdd80c1c54b8a18a9ea0cd5e36f473dc8e38d51b804cea34841f677a9971 \ - --hash=sha256:059978d2fddc462e9211362cbc8446747ecd930537fa559d3d25c256f032ff54 \ - --hash=sha256:0a2be20eb23888df130214b91c262a90e2de1553d6fb7de9e9010cec994c0ff2 \ - --hash=sha256:0f1338b61ea66f4757a0544ed8a02ccbf60e38d9cfb3225888888dd4475ebb96 \ - --hash=sha256:0f19f7798996d4458c669bd770504f710014926e9970f4729cf55853ae200469 \ - --hash=sha256:0f735e680c323ee7e9ef8e2ea26425c7dbc2ede0086fa83ce9d7ccab8a089f26 \ - --hash=sha256:10b3f01d5aeb632adaaf39c5e93f040a550464a768d54c514050c635adcbb9d0 \ - --hash=sha256:12f5d820fadc5848d4559ea838aef733cf37ed2a1103bba148ac2f5547c14c29 \ - --hash=sha256:19ba8625fa69523627b67f7e9901b587a4952470f68814d79cdc5bc460e9b885 \ - --hash=sha256:19bb08e56f57c215e9572cd65cb6f8097804412c54081d933997ddde3e5ac579 \ - --hash=sha256:1c272a9a18a5ecc48a7101882230046b83023bb2a662050ecb9bfcb28d9ab53a \ - --hash=sha256:1ccedfe280e804d9a9d7fe8b8c4309d28e364b77f40309c86596baa754af50b1 \ - --hash=sha256:1f164699a060c0b3616459d13c1464a981fddf36f892f0a5027cbd45121fb14b \ - --hash=sha256:2117be9883501eaf95503bd313eb4c7a23d567edd44014ba15835a1e9ec6d852 \ - --hash=sha256:222c828243b4789d79a706a876910f656fad4381661691220ba57b2ab4547865 \ - --hash=sha256:23ca762140159417a6bbc959ca1927f6949711851e56f2181ddfe8d63512b5ad \ - --hash=sha256:240f99f88a9a6beb53ebadac79a2e3417247aa756202ed234b1dbae13d248092 \ - --hash=sha256:2c4aeaedd20771b7b4bcdf0ae791904445df6d856c02fc51d809d12d17cffdc7 \ - --hash=sha256:2d71ca30257ce756e37a6078b1dff2d9475fee13609ad831eac9a6531bea903b \ - --hash=sha256:2e66c57416352f36bf98f6641ddadd47c93740a22af7150d3e9a1ef6e983f9a8 \ - --hash=sha256:2f9d9ea547618d907f2ee6670c9a951f059c5994e4b6de8dcf7d9747b420c820 \ - --hash=sha256:2fd35177dc483ae702f07b86c782f4f4b100a8ce4e7c5778cea016979023d9fd \ - --hash=sha256:3194b8cab8dbc882f37c13ef1262e0a3d62064fa97533d3aa124771f7bf1ecee \ - --hash=sha256:34d8af6391c5f2e69749d7f037b614b8c5c42093c251f336bdbfa4b03c57d6c4 \ - --hash=sha256:378dbc57dd8cf341ce243f13fa1fa5394d68e2e02c15cd5f28eae35a70ec7f67 \ - --hash=sha256:3b1eb9871cbe43b6ca6fac3544682971539d8a1d229e6babe43446279679609d \ - --hash=sha256:3b3bdc89413117b40cc39baae08fd09cbdeb839d421c4e7dce6a34f6b54b3ac1 \ - --hash=sha256:3b64f22fbb6dcd5663de5ef2d847a5638646ef99112503e6f7704bdecb0d1c4d \ - --hash=sha256:3c11fa5dd2ef773a8a5a6daa40243d83b450915992eab021789498dc87acc114 \ - --hash=sha256:3e6a38366f7f0d0f6ed7a1198055150c52fda552b107dad4785c0852ad7685d1 \ - --hash=sha256:3e77a729df23be2116acc4e9de2767d8e92445fbca68886dd991dc912f473755 \ - --hash=sha256:469167d5372f5bb3aedff4fc53035d593884fff2617a75317740e885acd48b04 \ - --hash=sha256:4696665b2713021c6eba3e2b882a86013763b442577fe5d2056a42111e732eca \ - --hash=sha256:474cade59a447cb4019c0dce9f0434bf835fb558ea932f62c686fe07fe6db6a1 \ - --hash=sha256:475bd56492ce5f4cffe32b5533c6533ee0c406d1d0e6924879f83adcf51da0ae \ - --hash=sha256:48fcdd5bc771cbbab8ccc9588b8b6447f6a30f9fe00898b1a5107098e00d6793 \ - --hash=sha256:49ce7525853a981fc35d380aa2353536a01a9ec1b30979ea4e35966316cace7e \ - --hash=sha256:4aa995b9156ae499393d949a456a7ab0b994a8241a96db73a3b73c7a090eff6a \ - --hash=sha256:4af1f8877ca46ecdd0bc0d4a6b66d4b2bddc84a79e2e8366bc0d5308e76bceb8 \ - --hash=sha256:4b14bfae90598d331b5061fd15a7c290ea0c15b34aeb1cf620464bb5ec02a602 \ - --hash=sha256:4df1984c8804ed336089e88ac81a9417b1fd0db7c6f867c50a9264488797e778 \ - --hash=sha256:5004d727499ecb95f7c9147dd0bfc5b5670f71d355f0bd26d7af2d3af8e07d2f \ - --hash=sha256:55ca0e95a3905f62f00900255ed807c580775174252999286f283e646d675a49 \ - --hash=sha256:564b36512a7da3b386143c611867e3f7cfb249300a1bf60889bd9985da67ab77 \ - --hash=sha256:564be41e85318403fdb176e9e5b3e852d528392f42f2c1d1efcbeeed481126d7 \ - --hash=sha256:56f7d230ec66e799fbfd8350e9544f8a45a4353f1cf40c1fea74c1780f555b8f \ - --hash=sha256:582770f82513419512da096e8df21ca44f86a2e56e25dc93c5ab4df0fe065bf0 \ - --hash=sha256:5ae3a19949a27982c7425a7a5a963c1268fdbabf0be15ab59448cbcf0f992519 \ - --hash=sha256:5b8a5557d5af3f4e3add52a58c4cf2b8e6e59fc56b261768866f5337872d596d \ - --hash=sha256:5bf635c3476f4119b940cc8d94ad454cbe0c377e61b4527f0192aabeac1e9370 \ - --hash=sha256:62a3cddf8d9a2eae1f79585fa81d32e13d0c509bb9e7ad47d33c83b45a944df7 \ - --hash=sha256:62d8a0adcdaf62ee56bfb37737153251ac8e4b27845b3ca065862fb01d99e247 \ - --hash=sha256:62fd54f3e6f17976962ba67f911d62723c760a69d54f5d7b74c3ceb1a4e9ef8d \ - --hash=sha256:6475e42ef92717a678bfbf50885a682bb360a6f9c8819fb1a388d98198fdcb80 \ - --hash=sha256:682d2e434ff2f1108314ff7f056ce44e457f12dbed0249b24e106e385cf154b9 \ - --hash=sha256:6a51839f778b0e283b43cd82bb17f1835ee2cc1bf1101765e90ae886e53e751c \ - --hash=sha256:6b4174fcec98601f0cfdf308ee29a6ae53c55f14359e848dab4e94009112ee7d \ - --hash=sha256:6b9697d15231aeaed4786f090c9c8bc3ab5f0e0a6da1e76c135a310def271020 \ - --hash=sha256:6c333a2385d2a6298265f4b3e960590f787311b87f6b5e6e21bb8375914ef504 \ - --hash=sha256:703ad3f742fc81e543638a7bebddd35acadaa0004a5e00535e795f4b6f2c25ca \ - --hash=sha256:72714919ed9b90f030f761c20670e529c4af96c31bd000917dd0c9afd1afb731 \ - --hash=sha256:76484ba17b2832776581b7ab466d094e48eba74cb65a60aea20154dae485e8bd \ - --hash=sha256:77da5305a410910218b99f2a963092f4277d8a9c1f429c1ff1b026d1826bd0b6 \ - --hash=sha256:7897298b3eedc790257fef8a6ec582ca04e9dbe568ba4a9a890913b925b8ea21 \ - --hash=sha256:7972c82bed87d7bd8e374b60a6b6e816d75ba4f7c2627c2d14eed216e62738e1 \ - --hash=sha256:7c20db99da682f9180fa5195c90b80b159632fb611e8dbccdd99ba0be0970620 \ - --hash=sha256:7c2c4c9ce834801651f81d6760d0a51035b8b239f58f298de25162fcf6f8bb64 \ - --hash=sha256:7ddedba3d0043349edc79df3dc2da49c72b06d59a45a42c1c8d987e6b8d175b8 \ - --hash=sha256:84912962071087286333f70569362e10793f73f45c48854e6859df11001eb2d3 \ - --hash=sha256:90b570f1a146181c3d6ae8f755de66227ded49d30d050479b5ae07710f7894c5 \ - --hash=sha256:97891a23d7fd4e1afe9c2f4473e04595e4acb18e4733b910b6577b74e7e21985 \ - --hash=sha256:990a53b9d6a30b2878789e490758e568b12b4a7fb2527d0c89deb9650b0e5813 \ - --hash=sha256:99a303ad960747c33b65b1cb65d01a62ac73fa39b72f08a2e1efa832529b01ed \ - --hash=sha256:99eb94e97a42367fef5fc11e28cb2362809d3e70837f6e60557816c7106e2e20 \ - --hash=sha256:a1c20c26af48aea984f63f96e5d7af7567c32cb527e33b60a0ef0a6313cf8b03 \ - --hash=sha256:a4676b978a9711531e7cea499d4cdc0794c617a1c0579310ab46c9fdf5877702 \ - --hash=sha256:a4dc0b83e25267f42ef065ea57653de4365b56d7bc4e4cfc94fabe56998f8ee6 \ - --hash=sha256:a533873a7a4ec2270fb362ee5a0d3b98752e4e1dc9042b257cd54545a96bd8ed \ - --hash=sha256:a6d9487b9471ec36b0faedf52228cd732e89be0a2bbd649af890b5e2ce422353 \ - --hash=sha256:a921edbe971aade1bf45bcbb3494e30ba6863a5c78f28be992c42de980fd9108 \ - --hash=sha256:a9f3546b503975a69b547c9fd1582cad10ede1ce6f3e313a2f547c73a3d7814f \ - --hash=sha256:aab715b1a0c37f7f11f9f1f579c6fbaa51ef569e47e3c0a4644fba46077a9409 \ - --hash=sha256:ac90cfab65bc281d6752f22db5fa90419e33220af4b4fa53b51f5948f414c0e7 \ - --hash=sha256:b3a8e6a2058a0240cfde542b641d0e78b594311bc1a710cbcb2e1841417d5cb3 \ - --hash=sha256:bb34001fc1f05f6b323e02c278090c07a47645caae3aa77ed7ed8a3ce6abcce9 \ - --hash=sha256:bc95c49853cd29613e4fe4ff96d73068ff89b89d61e53988442e127e8da8e7ba \ - --hash=sha256:bfe824d6707a5dc3c5676685f624bc0c63c40d79dc0239a7fd6c034b98c25ebe \ - --hash=sha256:c32ada0abb4bc94c30be2b681c42f058ab104d048da6f0148280a51ce98add8c \ - --hash=sha256:c417f8c2e1137775569297c584a8a7144e5d1237789eae56af4faf1894a0b861 \ - --hash=sha256:c811612711e01b901e18964b3e5dec0d35525150f5f3f85d0aee2935f059910a \ - --hash=sha256:ca69ec38adf5cadcc21d0b25e2144f6a25b7db7bea7e730bac25075bc305eff0 \ - --hash=sha256:ca8313cb852af788c78d5afdea24c40172cbfff8b35e58b407467732fde20390 \ - --hash=sha256:cc6d5fc5edbfb8041d9607f6a417997fa4d02de78284d386bea7ab767b5ea4f3 \ - --hash=sha256:cd45eb70eca63f41bb156b7dffbe1a7760153b69892d923bdb79a74099e2ed90 \ - --hash=sha256:ce887c5e54411d607ee0959cac15bb31d506d86a9bcaddf0b7e9d63325a7a802 \ - --hash=sha256:cf2b60b65df05b6b2fa0d887f2189991a0dbf44a0dd18359001dc8fcdb7f1163 \ - --hash=sha256:cf7a4b976da219e726d0043fc94ae8169c0dba1d3a059b3c1e2c964bafc5a77d \ - --hash=sha256:cf8b0870047900eb1f17f453b4b3953b8ffbf203ef56c2f346780ff930a4d430 \ - --hash=sha256:cfe6285ef99e7ee51cef20609be2bc1dd0e8446462b71c9db8bb296ba632810a \ - --hash=sha256:d169c47e40c911f728439da853b6fd06da83761012e6e76f11cb62cddae7282b \ - --hash=sha256:d812838c109757a11354a161c95708ae4199c4fd4d82b90959b20914c1d097f6 \ - --hash=sha256:d871f6a30d43e32fc9252dc7b9febe1a042b3ff3908aa83868d7cf7c9579a59b \ - --hash=sha256:dea698b64235d053def7d2f08af9302a69fcd760d1c7bd9988fd5d3b6157e657 \ - --hash=sha256:df46d9a3d78ec19b495b1107bf26e4fcf97c900279901f4f4819ac5bb2a02a4c \ - --hash=sha256:e04ab827ec4f775817736b20cdc8350f40327f9b598dec4e18c9ffdcbea88a93 \ - --hash=sha256:e68c0076052dd911a81d3acc4ef2911cc4ef65bf7cadbfbc8ae762da24da858f \ - --hash=sha256:e88ab34826d6eeb6c67e6e92400b9ec653faf5092a35f07465f44c9f1c429f82 \ - --hash=sha256:ea01ffbe23df53ece0c8732d1585b3d6079bb8c9ee14f3745daf000051415a31 \ - --hash=sha256:ea6df292013c9f050cbf3f93eee9953d6e5acd9e64a0bf4ca16404bfd7aa9bcc \ - --hash=sha256:ee433e594d7948e760b5c2a78cc06ac219df33b0848793cf9513d486a9f90a52 \ - --hash=sha256:eeea0cdd2f687e210c8f605f322d7b0300ba55145014a5dbe98bd4be6fff1f6c \ - --hash=sha256:f27b7488144eb5dd9151cf839b195edd1569629d90ace4c5b6b18e4e75d1e63a \ - --hash=sha256:f84b53326abf8e56ebc28a35cebf4a0f396a13a76300f500ab11fe0573bf0b52 \ - --hash=sha256:f8d877aa60d80715b2afc565f0f1aea66565824c229a2d065b31670e09fed6d7 \ - --hash=sha256:f8e38d55ca36c15f36d814ea414ecb2401d860de177c49f84a327a25b3ee752b \ - --hash=sha256:f91e8f9053a07177868e813656ec57599cd2a63238844393cd01bd69c2e40147 \ - --hash=sha256:fcc425fb6fd2a00c6d91c85d084c6b75a61bc8bc12159d08e17c5711df6c5ba4 - # via aiobotocore -aioitertools==0.12.0 \ - --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \ - --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796 +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9 \ + --hash=sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2 \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43 \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f \ + --hash=sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb \ + --hash=sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48 \ + --hash=sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 \ + --hash=sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5 + # via + # aiobotocore + # kubernetes +aioitertools==0.13.0 \ + --hash=sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be \ + --hash=sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c # via aiobotocore aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ @@ -142,15 +143,22 @@ alabaster==1.0.0 \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b # via sphinx -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via + # fastapi + # typer +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # httpx + # httpx2 # mcp # sse-starlette # starlette @@ -159,317 +167,359 @@ asn1crypto==1.5.1 \ --hash=sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c \ --hash=sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67 # via snowflake-connector-python -atpublic==5.1 \ - --hash=sha256:135783dbd887fbddb6ef032d104da70c124f2b44b9e2d79df07b9da5334825e3 \ - --hash=sha256:abc1f4b3dbdd841cc3539e4b5e4f3ad41d658359de704e30cb36da4d4e9d3022 +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy +atpublic==7.0.0 \ + --hash=sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e \ + --hash=sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b # via ibis-framework -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via + # feast (pyproject.toml) # aiohttp # jsonschema # referencing -babel==2.17.0 \ - --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ - --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 +babel==2.18.0 \ + --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ + --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 # via sphinx -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -boto3==1.38.27 \ - --hash=sha256:94bd7fdd92d5701b362d4df100d21e28f8307a67ff56b6a8b0398119cf22f859 \ - --hash=sha256:95f5fe688795303a8a15e8b7e7f255cadab35eae459d00cc281a4fd77252ea80 - # via - # feast (setup.py) +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +boto3==1.43.46 \ + --hash=sha256:66c0d943b049a46a492ec4ec2ebe73c930b1842c7137bee83aad6d93e95d4d96 \ + --hash=sha256:69453e2c1bcb9fd9806527ab99950cacfc2826cb0dce9a3a0414d19270c06c3c + # via + # feast (pyproject.toml) # snowflake-connector-python -botocore==1.38.46 \ - --hash=sha256:8798e5a418c27cf93195b077153644aea44cb171fcd56edc1ecebaa1e49e226e \ - --hash=sha256:89ca782ffbf2e8769ca9c89234cfa5ca577f1987d07d913ee3c68c4776b1eb5b +botocore==1.43.46 \ + --hash=sha256:59f2e1ac3cdc66d191cae91c0804bc41847ce817dc8147cf43eaada8f76a5533 \ + --hash=sha256:cb673891e623ae6e6a1bf24d94ef169504f3eb02584adb5d5bee2f6aae819b60 # via # aiobotocore # boto3 # s3transfer # snowflake-connector-python -cachetools==6.2.1 \ - --hash=sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701 \ - --hash=sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201 - # via google-auth +cachetools==7.1.6 \ + --hash=sha256:2c12e255780330af28b91bb7fb96cce4c766f04e38396b9a24510190a5827096 \ + --hash=sha256:c7a79e7f30ba9943c1cefd08cc36f006aaae086e017af9166f1d59d6170c47e1 + # via pymilvus calver==2025.3.31 \ --hash=sha256:07511edf5e7fa75ae97445c8c5921240e0fe62937289a3ebe6963eddd3c691b6 \ --hash=sha256:255d1a70bba8f97dc1eee3af4240ed35980508da69257feef94c79e5c6545fc7 - # via feast (setup.py) -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 + # via feast (pyproject.toml) +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # httpcore # httpx # kubernetes # requests # snowflake-connector-python -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b - # via - # feast (setup.py) +cffi==2.1.0 \ + --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ + --hash=sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd \ + --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ + --hash=sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5 \ + --hash=sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f \ + --hash=sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6 \ + --hash=sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c \ + --hash=sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda \ + --hash=sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd \ + --hash=sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a \ + --hash=sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd \ + --hash=sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd \ + --hash=sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3 \ + --hash=sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb \ + --hash=sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66 \ + --hash=sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d \ + --hash=sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f \ + --hash=sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6 \ + --hash=sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0 \ + --hash=sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c \ + --hash=sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93 \ + --hash=sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d \ + --hash=sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d \ + --hash=sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8 \ + --hash=sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b \ + --hash=sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001 \ + --hash=sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d \ + --hash=sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43 \ + --hash=sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b \ + --hash=sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0 \ + --hash=sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0 \ + --hash=sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458 \ + --hash=sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8 \ + --hash=sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d \ + --hash=sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94 \ + --hash=sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022 \ + --hash=sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db \ + --hash=sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479 \ + --hash=sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376 \ + --hash=sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d \ + --hash=sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6 \ + --hash=sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3 \ + --hash=sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea \ + --hash=sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd \ + --hash=sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02 \ + --hash=sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde \ + --hash=sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224 \ + --hash=sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76 \ + --hash=sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804 \ + --hash=sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1 \ + --hash=sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913 \ + --hash=sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714 \ + --hash=sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc \ + --hash=sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2 \ + --hash=sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e \ + --hash=sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda \ + --hash=sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512 \ + --hash=sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28 \ + --hash=sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699 \ + --hash=sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3 \ + --hash=sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c \ + --hash=sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe \ + --hash=sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a \ + --hash=sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f \ + --hash=sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c \ + --hash=sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2 \ + --hash=sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f \ + --hash=sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b \ + --hash=sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565 \ + --hash=sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056 \ + --hash=sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629 \ + --hash=sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7 \ + --hash=sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0 \ + --hash=sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9 \ + --hash=sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853 \ + --hash=sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13 \ + --hash=sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a \ + --hash=sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4 \ + --hash=sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce \ + --hash=sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac \ + --hash=sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c \ + --hash=sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46 \ + --hash=sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384 \ + --hash=sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b \ + --hash=sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210 \ + --hash=sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc \ + --hash=sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a \ + --hash=sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5 \ + --hash=sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7 \ + --hash=sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2 \ + --hash=sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326 \ + --hash=sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f \ + --hash=sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca \ + --hash=sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98 \ + --hash=sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9 \ + --hash=sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5 \ + --hash=sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7 \ + --hash=sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc \ + --hash=sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da \ + --hash=sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f + # via + # feast (pyproject.toml) # cryptography - # snowflake-connector-python -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via # requests # snowflake-connector-python -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask - # typer # uvicorn -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a # via dask colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via feast (setup.py) -cryptography==46.0.0 \ - --hash=sha256:07a1be54f995ce14740bf8bbe1cc35f7a37760f992f73cf9f98a2a60b9b97419 \ - --hash=sha256:0f58183453032727a65e6605240e7a3824fd1d6a7e75d2b537e280286ab79a52 \ - --hash=sha256:16b5ac72a965ec9d1e34d9417dbce235d45fa04dac28634384e3ce40dfc66495 \ - --hash=sha256:1b4fba84166d906a22027f0d958e42f3a4dbbb19c28ea71f0fb7812380b04e3c \ - --hash=sha256:1d2073313324226fd846e6b5fc340ed02d43fd7478f584741bd6b791c33c9fee \ - --hash=sha256:249c41f2bbfa026615e7bdca47e4a66135baa81b08509ab240a2e666f6af5966 \ - --hash=sha256:274f8b2eb3616709f437326185eb563eb4e5813d01ebe2029b61bfe7d9995fbb \ - --hash=sha256:2fc30be952dd4334801d345d134c9ef0e9ccbaa8c3e1bc18925cbc4247b3e29c \ - --hash=sha256:32670ca085150ff36b438c17f2dfc54146fe4a074ebf0a76d72fb1b419a974bc \ - --hash=sha256:35aa1a44bd3e0efc3ef09cf924b3a0e2a57eda84074556f4506af2d294076685 \ - --hash=sha256:3738f50215211cee1974193a1809348d33893696ce119968932ea117bcbc9b1d \ - --hash=sha256:378eff89b040cbce6169528f130ee75dceeb97eef396a801daec03b696434f06 \ - --hash=sha256:399ef4c9be67f3902e5ca1d80e64b04498f8b56c19e1bc8d0825050ea5290410 \ - --hash=sha256:40ee4ce3c34acaa5bc347615ec452c74ae8ff7db973a98c97c62293120f668c6 \ - --hash=sha256:4bc257c2d5d865ed37d0bd7c500baa71f939a7952c424f28632298d80ccd5ec1 \ - --hash=sha256:4f70cbade61a16f5e238c4b0eb4e258d177a2fcb59aa0aae1236594f7b0ae338 \ - --hash=sha256:523153480d7575a169933f083eb47b1edd5fef45d87b026737de74ffeb300f69 \ - --hash=sha256:6460866a92143a24e3ed68eaeb6e98d0cedd85d7d9a8ab1fc293ec91850b1b38 \ - --hash=sha256:65e9117ebed5b16b28154ed36b164c20021f3a480e9cbb4b4a2a59b95e74c25d \ - --hash=sha256:6c39fd5cd9b7526afa69d64b5e5645a06e1b904f342584b3885254400b63f1b3 \ - --hash=sha256:6d8945bc120dcd90ae39aa841afddaeafc5f2e832809dc54fb906e3db829dfdc \ - --hash=sha256:75d2ddde8f1766ab2db48ed7f2aa3797aeb491ea8dfe9b4c074201aec00f5c16 \ - --hash=sha256:77e3bd53c9c189cea361bc18ceb173959f8b2dd8f8d984ae118e9ac641410252 \ - --hash=sha256:7f3f88df0c9b248dcc2e76124f9140621aca187ccc396b87bc363f890acf3a30 \ - --hash=sha256:80a548a5862d6912a45557a101092cd6c64ae1475b82cef50ee305d14a75f598 \ - --hash=sha256:834af45296083d892e23430e3b11df77e2ac5c042caede1da29c9bf59016f4d2 \ - --hash=sha256:83af84ebe7b6e9b6de05050c79f8cc0173c864ce747b53abce6a11e940efdc0d \ - --hash=sha256:88c09da8a94ac27798f6b62de6968ac78bb94805b5d272dbcfd5fdc8c566999f \ - --hash=sha256:8e8b222eb54e3e7d3743a7c2b1f7fa7df7a9add790307bb34327c88ec85fe087 \ - --hash=sha256:91585fc9e696abd7b3e48a463a20dda1a5c0eeeca4ba60fa4205a79527694390 \ - --hash=sha256:99f64a6d15f19f3afd78720ad2978f6d8d4c68cd4eb600fab82ab1a7c2071dca \ - --hash=sha256:9aa85222f03fdb30defabc7a9e1e3d4ec76eb74ea9fe1504b2800844f9c98440 \ - --hash=sha256:ab3a14cecc741c8c03ad0ad46dfbf18de25218551931a23bca2731d46c706d83 \ - --hash=sha256:b8e7db4ce0b7297e88f3d02e6ee9a39382e0efaf1e8974ad353120a2b5a57ef7 \ - --hash=sha256:bbaa5eef3c19c66613317dc61e211b48d5f550db009c45e1c28b59d5a9b7812a \ - --hash=sha256:be7479f9504bfb46628544ec7cb4637fe6af8b70445d4455fbb9c395ad9b7290 \ - --hash=sha256:bf1961037309ee0bdf874ccba9820b1c2f720c2016895c44d8eb2316226c1ad5 \ - --hash=sha256:c1f6ccd6f2eef3b2eb52837f0463e853501e45a916b3fc42e5d93cf244a4b97b \ - --hash=sha256:c3648d6a5878fd1c9a22b1d43fa75efc069d5f54de12df95c638ae7ba88701d0 \ - --hash=sha256:c39f0947d50f74b1b3523cec3931315072646286fb462995eb998f8136779319 \ - --hash=sha256:c3cd09b1490c1509bf3892bde9cef729795fae4a2fee0621f19be3321beca7e4 \ - --hash=sha256:c457ad3f151d5fb380be99425b286167b358f76d97ad18b188b68097193ed95a \ - --hash=sha256:c9c4121f9a41cc3d02164541d986f59be31548ad355a5c96ac50703003c50fb7 \ - --hash=sha256:d14eaf1569d6252280516bedaffdd65267428cdbc3a8c2d6de63753cf0863d5e \ - --hash=sha256:d1eccae15d5c28c74b2bea228775c63ac5b6c36eedb574e002440c0bc28750d3 \ - --hash=sha256:d349af4d76a93562f1dce4d983a4a34d01cb22b48635b0d2a0b8372cdb4a8136 \ - --hash=sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0 \ - --hash=sha256:da7f93551d39d462263b6b5c9056c49f780b9200bf9fc2656d7c88c7bdb9b363 \ - --hash=sha256:df932ac70388be034b2e046e34d636245d5eeb8140db24a6b4c2268cd2073270 \ - --hash=sha256:f09a3a108223e319168b7557810596631a8cb864657b0c16ed7a6017f0be9433 \ - --hash=sha256:f85e6a7d42ad60024fa1347b1d4ef82c4df517a4deb7f829d301f1a92ded038c \ - --hash=sha256:f9aaf2a91302e1490c068d2f3af7df4137ac2b36600f5bd26e53d9ec320412d3 \ - --hash=sha256:f9f85d9cf88e3ba2b2b6da3c2310d1cf75bdf04a5bc1a2e972603054f82c4dd5 \ - --hash=sha256:fe9ff1139b2b1f59a5a0b538bbd950f8660a39624bbe10cf3640d17574f973bb - # via + # via feast (pyproject.toml) +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b + # via + # google-auth + # pyjwt # pyopenssl # snowflake-connector-python cython==3.0.12 \ @@ -537,109 +587,84 @@ cython==3.0.12 \ --hash=sha256:fe030d4a00afb2844f5f70896b7f2a1a0d7da09bf3aa3d884cbe5f73fff5d310 \ --hash=sha256:feb86122a823937cc06e4c029d80ff69f082ebb0b959ab52a5af6cdd271c5dc3 \ --hash=sha256:ff5c0b6a65b08117d0534941d404833d516dac422eee88c6b4fd55feb409a5ed - # via feast (setup.py) -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) -db-dtypes==1.4.3 \ - --hash=sha256:a1c92b819af947fae1701d80a71f2a0eac08f825ca52cf0c68aeba80577ae966 \ - --hash=sha256:d3cb08c32939d24e05501f17694be8c1c54cfb4620d61fb56fb311e8c3d8885e + # via feast (pyproject.toml) +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) +db-dtypes==1.7.1 \ + --hash=sha256:450b94cd010cfa6d4d13cca2593009eba4fbe15516084c332cde1a47bbce0625 \ + --hash=sha256:75adf3bdf3174a6474ea4162d9a345ea0b3102ec4fd603e69ec26b6e8a99145a # via # google-cloud-bigquery # pandas-gbq dill==0.3.9 \ --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c - # via feast (setup.py) -docutils==0.21.2 \ - --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ - --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 + # via feast (pyproject.toml) +docutils==0.22.4 \ + --hash=sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968 \ + --hash=sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de # via sphinx -duckdb==1.1.3 \ - --hash=sha256:00cca22df96aa3473fe4584f84888e2cf1c516e8c2dd837210daec44eadba586 \ - --hash=sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd \ - --hash=sha256:0897f83c09356206ce462f62157ce064961a5348e31ccb2a557a7531d814e70e \ - --hash=sha256:09c68522c30fc38fc972b8a75e9201616b96ae6da3444585f14cf0d116008c95 \ - --hash=sha256:0a55169d2d2e2e88077d91d4875104b58de45eff6a17a59c7dc41562c73df4be \ - --hash=sha256:0ba6baa0af33ded836b388b09433a69b8bec00263247f6bf0a05c65c897108d3 \ - --hash=sha256:183ac743f21c6a4d6adfd02b69013d5fd78e5e2cd2b4db023bc8a95457d4bc5d \ - --hash=sha256:1aa3abec8e8995a03ff1a904b0e66282d19919f562dd0a1de02f23169eeec461 \ - --hash=sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce \ - --hash=sha256:1d9ab6143e73bcf17d62566e368c23f28aa544feddfd2d8eb50ef21034286f24 \ - --hash=sha256:2141c6b28162199999075d6031b5d63efeb97c1e68fb3d797279d31c65676269 \ - --hash=sha256:252d9b17d354beb9057098d4e5d5698e091a4f4a0d38157daeea5fc0ec161670 \ - --hash=sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9 \ - --hash=sha256:2f073d15d11a328f2e6d5964a704517e818e930800b7f3fa83adea47f23720d3 \ - --hash=sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289 \ - --hash=sha256:4ebf5f60ddbd65c13e77cddb85fe4af671d31b851f125a4d002a313696af43f1 \ - --hash=sha256:4f0e2e5a6f5a53b79aee20856c027046fba1d73ada6178ed8467f53c3877d5e0 \ - --hash=sha256:51c6d79e05b4a0933672b1cacd6338f882158f45ef9903aef350c4427d9fc898 \ - --hash=sha256:51e7dbd968b393343b226ab3f3a7b5a68dee6d3fe59be9d802383bf916775cb8 \ - --hash=sha256:5ace6e4b1873afdd38bd6cc8fcf90310fb2d454f29c39a61d0c0cf1a24ad6c8d \ - --hash=sha256:5d57776539211e79b11e94f2f6d63de77885f23f14982e0fac066f2885fcf3ff \ - --hash=sha256:6411e21a2128d478efbd023f2bdff12464d146f92bc3e9c49247240448ace5a6 \ - --hash=sha256:647f17bd126170d96a38a9a6f25fca47ebb0261e5e44881e3782989033c94686 \ - --hash=sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c \ - --hash=sha256:77f26884c7b807c7edd07f95cf0b00e6d47f0de4a534ac1706a58f8bc70d0d31 \ - --hash=sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7 \ - --hash=sha256:80158f4c7c7ada46245837d5b6869a336bbaa28436fbb0537663fa324a2750cd \ - --hash=sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898 \ - --hash=sha256:8ee97ec337794c162c0638dda3b4a30a483d0587deda22d45e1909036ff0b739 \ - --hash=sha256:911d58c22645bfca4a5a049ff53a0afd1537bc18fedb13bc440b2e5af3c46148 \ - --hash=sha256:9c619e4849837c8c83666f2cd5c6c031300cd2601e9564b47aa5de458ff6e69d \ - --hash=sha256:9d0767ada9f06faa5afcf63eb7ba1befaccfbcfdac5ff86f0168c673dd1f47aa \ - --hash=sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4 \ - --hash=sha256:a1f83c7217c188b7ab42e6a0963f42070d9aed114f6200e3c923c8899c090f16 \ - --hash=sha256:a1fa0c502f257fa9caca60b8b1478ec0f3295f34bb2efdc10776fc731b8a6c5f \ - --hash=sha256:a30dd599b8090ea6eafdfb5a9f1b872d78bac318b6914ada2d35c7974d643640 \ - --hash=sha256:a433ae9e72c5f397c44abdaa3c781d94f94f4065bcbf99ecd39433058c64cb38 \ - --hash=sha256:a4748635875fc3c19a7320a6ae7410f9295557450c0ebab6d6712de12640929a \ - --hash=sha256:b74e121ab65dbec5290f33ca92301e3a4e81797966c8d9feef6efdf05fc6dafd \ - --hash=sha256:c443d3d502335e69fc1e35295fcfd1108f72cb984af54c536adfd7875e79cee5 \ - --hash=sha256:c5336939d83837af52731e02b6a78a446794078590aa71fd400eb17f083dda3e \ - --hash=sha256:cddc6c1a3b91dcc5f32493231b3ba98f51e6d3a44fe02839556db2b928087378 \ - --hash=sha256:d08308e0a46c748d9c30f1d67ee1143e9c5ea3fbcccc27a47e115b19e7e78aa9 \ - --hash=sha256:d5724fd8a49e24d730be34846b814b98ba7c304ca904fbdc98b47fa95c0b0cee \ - --hash=sha256:e4ef7ba97a65bd39d66f2a7080e6fb60e7c3e41d4c1e19245f90f53b98e3ac32 \ - --hash=sha256:e59087dbbb63705f2483544e01cccf07d5b35afa58be8931b224f3221361d537 \ - --hash=sha256:e86006958e84c5c02f08f9b96f4bc26990514eab329b1b4f71049b3727ce5989 \ - --hash=sha256:ecb1dc9062c1cc4d2d88a5e5cd8cc72af7818ab5a3c0f796ef0ffd60cfd3efb4 \ - --hash=sha256:eeacb598120040e9591f5a4edecad7080853aa8ac27e62d280f151f8c862afa3 \ - --hash=sha256:f549af9f7416573ee48db1cf8c9d27aeed245cb015f4b4f975289418c6cf7320 \ - --hash=sha256:f58db1b65593ff796c8ea6e63e2e144c944dd3d51c8d8e40dffa7f41693d35d3 \ - --hash=sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b +duckdb==1.5.5 \ + --hash=sha256:078e6a60dd8eedde5832f45422ca5c4a6b8c837aeabd8a56ca0b7d933f588053 \ + --hash=sha256:0c42757cb34722144bd4dfb94b6f336339e7b2468f6813fa7fa9a319ba07bab4 \ + --hash=sha256:179633a3fc6296c75d57c69c1e239fa9e5cdcb670fd1dbff88a02663f932905c \ + --hash=sha256:1a925d06c2a4c3b64553d6cc1aced5028d376d4479bed689a7d47e9b1dccd80a \ + --hash=sha256:1b543841b0ae18a9c982345cfa3987e9c065d3a4b0f067daa473d92d1e65f528 \ + --hash=sha256:1bdc38922c365c37720149f90d90b1e9823eb82dad6830855b5f87537fa6fc0c \ + --hash=sha256:2725d2b9ace3a4e75d72fc5a239f6a44b502c580edadb8fb2676db772c5f9282 \ + --hash=sha256:2e72f9e1a4f90a5c8483ad4d540e495bf0834ba61c360b52499a573d7ed62a3f \ + --hash=sha256:33db46679b071f108d57139493dee2d37e1f5efcf5c5c039c2969eed11a6c8a7 \ + --hash=sha256:3b805507f88171b428b21c966c30e9a3d54e30b24528918a44ed0032542bc26f \ + --hash=sha256:49c963d9469373d7aba8d750d9ea565ab823e94166efed953f184dd9b169b98c \ + --hash=sha256:4acc72798ba1885a9c17d1242903d2cd502f13b1271c7677f7cab25d8578eceb \ + --hash=sha256:63e48d4b74b15aeacd688976432a7225163df8c226eddeb8536bba2d4d4ff433 \ + --hash=sha256:64078acfd16541132ac6e191eb81b2845554444a0305cc1aa581ba107e514aa8 \ + --hash=sha256:6826504277dba513c0c5d71d828456c94d729c9d2482f94b2e289f90a9167e28 \ + --hash=sha256:72f33ee57ca7595b23957671a2cc7f7fe2be0ecc2d68f63abedcfcaa3a5c1238 \ + --hash=sha256:77bbc1e6ba12e1e06f9020117bdf848627ecfdf36f907550e62e008e6109dece \ + --hash=sha256:7a6d2d11859d82a936ebdcb30ce3d8a1cbb3e990bff05c12abb9b54c44fa7bd1 \ + --hash=sha256:8c11775cc99a447618d5f1840126db17f2652f3eae05529df4f81f40e2df7151 \ + --hash=sha256:8e6413dd40facb7b8ab21bd844450cd8f549b29e138635be9cf090ef4d2049e2 \ + --hash=sha256:9dc826c4b50e64f6c4e4d07a3a9cb075ef70ba3899dc43ec5493dc3d7b04b353 \ + --hash=sha256:9f4287f97ccf0c1f3d471e7115be2b067cbf99627e2d34bffd462dd64703cddc \ + --hash=sha256:a17e6a922e42a5c06ed2353fe78c5dff2610f6632d603836f9606ad0bf754079 \ + --hash=sha256:a736217825461732b5442d05a220f3da2e23a0dae114efbf08c9bf171b53098a \ + --hash=sha256:b08e19cc856220d8a26fa62abc2264b349aff67255e9373c6a3f607addd56dc6 \ + --hash=sha256:b9b6f86ed85d4ef5e0211eaebf75d057bd8bb520bba438a95dd0f4e42234bbfe \ + --hash=sha256:baa9c5702002fabb559ded2a39008f9f421fcbc7237d388b8213eff1e08858de \ + --hash=sha256:cd98829b67788609017e65c761bd42a5dd0f9129441bed8bda4d6881ccf819f0 \ + --hash=sha256:d4dd65f8941a604b947e0b9b4b4f7165988e29a23ec0b69b4038520956d9933e \ + --hash=sha256:ddfbdb096c11d51ee22492397d342c90a82e62c5d09961477895934d0a25372f \ + --hash=sha256:e238060db5ca59879882a6e9b015e2c65d5c64ddf281ba1d7a9a2033764152cf \ + --hash=sha256:f0b88535a5d86fdd63dba6ea02ab68c003dfb9e4892b11256ef24c4da208baae \ + --hash=sha256:f316eae2323d9a851883fdf2dee91c1f9efe251ab33e14a2272f82a913422ed6 \ + --hash=sha256:fbf0f2d48b43c6c304d00463b463c27ead6c4b01c3c1816b750f728decf71afe \ + --hash=sha256:feead93c56679b79592d437c62975d39cb67adedffa7592c763baf8160ac7366 # via ibis-framework -dunamai==1.25.0 \ - --hash=sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab \ - --hash=sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1 - # via poetry-dynamic-versioning durationpy==0.10 \ --hash=sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba \ --hash=sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286 # via kubernetes -environs==9.5.0 \ - --hash=sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124 \ - --hash=sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9 - # via pymilvus -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp fastapi-mcp==0.4.0 \ --hash=sha256:d4a3fe7966af24d44e4b412720561c95eb12bed999a4443a88221834b3b15aec \ --hash=sha256:d4ca9410996f4c7b8ea0d7b20fdf79878dc359ebf89cbf3b222e0b675a55097d - # via feast (setup.py) -filelock==3.20.0 \ - --hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \ - --hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4 + # via feast (pyproject.toml) +filelock==3.32.0 \ + --hash=sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402 \ + --hash=sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3 # via snowflake-connector-python flit-core==3.12.0 \ --hash=sha256:18f63100d6f94385c6ed57a72073443e1a71a4acb4339491615d0f16d6ff01b2 \ --hash=sha256:e7a0304069ea895172e3c7bb703292e992c5d1555dd1233ab7b5621b5b69e62c - # via feast (setup.py) + # via feast (pyproject.toml) frozenlist==1.8.0 \ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ @@ -774,17 +799,17 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -fsspec==2024.9.0 \ - --hash=sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8 \ - --hash=sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b +fsspec==2026.7.0 \ + --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \ + --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88 # via - # feast (setup.py) + # feast (pyproject.toml) # dask -google-api-core[grpc]==2.26.0 \ - --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ - --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 +google-api-core[grpc]==2.33.0 \ + --hash=sha256:3a36bcc3e319783f4c97da41f6f45ea6ffcaa55848e341de16e09cb70243c2bb \ + --hash=sha256:a2e22a0c1d0f03eafff1858b38cf46f832d5902b0c052235bf0ab8402929fbdc # via - # feast (setup.py) + # feast (pyproject.toml) # google-cloud-bigquery # google-cloud-bigquery-storage # google-cloud-bigtable @@ -792,9 +817,9 @@ google-api-core[grpc]==2.26.0 \ # google-cloud-datastore # google-cloud-storage # pandas-gbq -google-auth==2.41.1 \ - --hash=sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d \ - --hash=sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2 +google-auth[pyopenssl]==2.56.2 \ + --hash=sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6 \ + --hash=sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051 # via # google-api-core # google-auth-oauthlib @@ -804,475 +829,613 @@ google-auth==2.41.1 \ # google-cloud-core # google-cloud-datastore # google-cloud-storage - # kubernetes # pandas-gbq # pydata-google-auth -google-auth-oauthlib==1.2.2 \ - --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ - --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-auth-oauthlib==1.4.0 \ + --hash=sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2 \ + --hash=sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070 # via # pandas-gbq # pydata-google-auth -google-cloud-bigquery[pandas]==3.38.0 \ - --hash=sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520 \ - --hash=sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6 +google-cloud-bigquery[pandas]==3.42.2 \ + --hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \ + --hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f # via - # feast (setup.py) + # feast (pyproject.toml) # pandas-gbq -google-cloud-bigquery-storage==2.33.1 \ - --hash=sha256:24952aba0d69acc4d6bfbdc7a09dddbb728496b1780bd224f1056361a1b51044 \ - --hash=sha256:3fd25bef364ac5fb9bbd6560f0dd11b90b1845883df8e0a8c706ad53d00fc23b - # via feast (setup.py) -google-cloud-bigtable==2.33.0 \ - --hash=sha256:8d25c73dd41cea9436d14c9f03b91f94b05ff4afd47ce9a575bf18f8e6985747 \ - --hash=sha256:cd4f7178cde30113900318ec7e2fd793116752c97e0922c185c69b645a52656d - # via feast (setup.py) -google-cloud-core==2.4.3 \ - --hash=sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53 \ - --hash=sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e +google-cloud-bigquery-storage==2.39.0 \ + --hash=sha256:8c192b6263804f7bdd6f57a17e763ba7f03fa4e53d7ecafca0187e0fd6467d48 \ + --hash=sha256:d5afd90ad06cf24d9167316cca70ab5b344e880fc13031d7392aa78ee76b8bb6 + # via feast (pyproject.toml) +google-cloud-bigtable==2.41.0 \ + --hash=sha256:36da0fc730952bfe48ae91dee11015459c9046fa1499d0efb73a61bbb6edbe9b \ + --hash=sha256:d0c424bae6238eb16ffc0922be79da00d20d23e0750ea46742ffd4542c50fd26 + # via feast (pyproject.toml) +google-cloud-core==2.6.0 \ + --hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \ + --hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83 # via # google-cloud-bigquery # google-cloud-bigtable # google-cloud-datastore # google-cloud-storage -google-cloud-datastore==2.21.0 \ - --hash=sha256:eee454dd4a55f5b327f9f344928ff1a09a6f77c23d5e3d908ad31a13cc2f4073 \ - --hash=sha256:f303f27cd1983383f20bd227019cd8a7897419e0ec6b878367c58c66245f9d9b - # via feast (setup.py) +google-cloud-datastore==2.26.0 \ + --hash=sha256:1ac4882f033fcb90bd75d1171b46400ecd5def23768598843d9c041e0c0b4a6a \ + --hash=sha256:1bde5694308468da5c932153a6b0331d5b54e1c21de366b6a75ddb0000fd5f4b + # via feast (pyproject.toml) google-cloud-storage==2.19.0 \ --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via feast (setup.py) -google-crc32c==1.7.1 \ - --hash=sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db \ - --hash=sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337 \ - --hash=sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c \ - --hash=sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242 \ - --hash=sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e \ - --hash=sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472 \ - --hash=sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194 \ - --hash=sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3 \ - --hash=sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582 \ - --hash=sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d \ - --hash=sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6 \ - --hash=sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82 \ - --hash=sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06 \ - --hash=sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349 \ - --hash=sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a \ - --hash=sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d \ - --hash=sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48 \ - --hash=sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb \ - --hash=sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315 \ - --hash=sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589 \ - --hash=sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76 \ - --hash=sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65 \ - --hash=sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6 \ - --hash=sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127 \ - --hash=sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53 \ - --hash=sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603 \ - --hash=sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35 \ - --hash=sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9 \ - --hash=sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638 \ - --hash=sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9 \ - --hash=sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77 \ - --hash=sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14 \ - --hash=sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b \ - --hash=sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb + # via feast (pyproject.toml) +google-crc32c==1.8.0 \ + --hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \ + --hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \ + --hash=sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff \ + --hash=sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288 \ + --hash=sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411 \ + --hash=sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a \ + --hash=sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15 \ + --hash=sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb \ + --hash=sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa \ + --hash=sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962 \ + --hash=sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215 \ + --hash=sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b \ + --hash=sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27 \ + --hash=sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113 \ + --hash=sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f \ + --hash=sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f \ + --hash=sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d \ + --hash=sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2 \ + --hash=sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092 \ + --hash=sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7 \ + --hash=sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2 \ + --hash=sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93 \ + --hash=sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8 \ + --hash=sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21 \ + --hash=sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79 \ + --hash=sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2 \ + --hash=sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc \ + --hash=sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454 \ + --hash=sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2 \ + --hash=sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733 \ + --hash=sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697 \ + --hash=sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651 \ + --hash=sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c # via # google-cloud-bigtable # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 +google-resumable-media==2.10.0 \ + --hash=sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c \ + --hash=sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee # via # google-cloud-bigquery # google-cloud-storage -googleapis-common-protos[grpc]==1.70.0 \ - --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ - --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +googleapis-common-protos[grpc]==1.75.0 \ + --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ + --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via - # feast (setup.py) + # feast (pyproject.toml) # google-api-core # grpc-google-iam-v1 # grpcio-status -greenlet==3.2.4 \ - --hash=sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b \ - --hash=sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735 \ - --hash=sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079 \ - --hash=sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d \ - --hash=sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433 \ - --hash=sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58 \ - --hash=sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52 \ - --hash=sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31 \ - --hash=sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246 \ - --hash=sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f \ - --hash=sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671 \ - --hash=sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8 \ - --hash=sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d \ - --hash=sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f \ - --hash=sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0 \ - --hash=sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd \ - --hash=sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337 \ - --hash=sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0 \ - --hash=sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633 \ - --hash=sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b \ - --hash=sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa \ - --hash=sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31 \ - --hash=sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9 \ - --hash=sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b \ - --hash=sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4 \ - --hash=sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc \ - --hash=sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c \ - --hash=sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98 \ - --hash=sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f \ - --hash=sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c \ - --hash=sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590 \ - --hash=sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3 \ - --hash=sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2 \ - --hash=sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9 \ - --hash=sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5 \ - --hash=sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02 \ - --hash=sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0 \ - --hash=sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1 \ - --hash=sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c \ - --hash=sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594 \ - --hash=sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5 \ - --hash=sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d \ - --hash=sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a \ - --hash=sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6 \ - --hash=sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b \ - --hash=sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df \ - --hash=sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945 \ - --hash=sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae \ - --hash=sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb \ - --hash=sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504 \ - --hash=sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb \ - --hash=sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01 \ - --hash=sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c \ - --hash=sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968 - # via feast (setup.py) -grpc-google-iam-v1==0.14.2 \ - --hash=sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351 \ - --hash=sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20 +greenlet==3.5.4 \ + --hash=sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20 \ + --hash=sha256:07bd44616608d873d06735b63ef1a88191d6ca57c8d291d6559c71bc14c0893c \ + --hash=sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994 \ + --hash=sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8 \ + --hash=sha256:123aa379c962ed5fe90a880327e0c3066124ac64ec99e12a238be9fd8eb3db3d \ + --hash=sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9 \ + --hash=sha256:13b980043cb1b3134e81ea469da1250ddcc6bfe6d245bbaa59168d9cdc8f228f \ + --hash=sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809 \ + --hash=sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c \ + --hash=sha256:188e4d142f243051d92a1f5c244a741da02dddc070a0620c842804d7b56d008c \ + --hash=sha256:1e1a4a684b16c45ba324e60b32a4386a87722bcb815d2a149d2182f9b401ca72 \ + --hash=sha256:1f17e362d78e37559e0506c5a7d066bdd45073c36a0127a543e8a0df27242ff3 \ + --hash=sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02 \ + --hash=sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c \ + --hash=sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c \ + --hash=sha256:2cdaadc3d31445a8f782bde3cd37e49a2c2a9c6da6daf76a3e34c683b271a3c7 \ + --hash=sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec \ + --hash=sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c \ + --hash=sha256:32802705c2c1ff25e8237b3bdacf2594fa02be80af8a66703eb7853ea7e68686 \ + --hash=sha256:3529a8a933582ad19e224792cac7372489526576b75b4c124e8e4f29948f4861 \ + --hash=sha256:39169a11d87a6a263afda3e9a27d1df16d0f919d40a4837cc73986c9884c0dd8 \ + --hash=sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0 \ + --hash=sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4 \ + --hash=sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9 \ + --hash=sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3 \ + --hash=sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9 \ + --hash=sha256:4ab9f0704bccf6d3b38e0d2130b7b33271cff11453690da074fa280c3aa8e8e7 \ + --hash=sha256:57aa201b351f7c7c75627c60d29e4d5b97a07d37efeb62b903466fca42c097d7 \ + --hash=sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd \ + --hash=sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3 \ + --hash=sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2 \ + --hash=sha256:69173331fbc5d64bfac0065d7e22c39cfcd089e9b18d125bdcd5079363b09616 \ + --hash=sha256:70bdfacdc183dac838b2a0aaff2dd6134a457c52fe68a9c6bbab435483d2b9df \ + --hash=sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf \ + --hash=sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0 \ + --hash=sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a \ + --hash=sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f \ + --hash=sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22 \ + --hash=sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356 \ + --hash=sha256:89f3738167bab8c1084b94e23023d41d247117ac149fa0fbcb5bd4cf6262b353 \ + --hash=sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e \ + --hash=sha256:9667862a2e38ad379f11b845daeda22c8989186def44f06962c9c4c05e556da7 \ + --hash=sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5 \ + --hash=sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8 \ + --hash=sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde \ + --hash=sha256:a2d614cb2372c7101a12ea8b96dd56f81c986d247c5a73db67063f3ed1ca4a52 \ + --hash=sha256:ac5bf81d79d2c8eeb2ef6359b2e1687a1e9ebf46c2b1f970da9a9255df51d190 \ + --hash=sha256:adf2244d7f69409925a8f22ed22cc5f93cdfe5c9dc87ff3476be2c2aaae61a05 \ + --hash=sha256:ae53534b5dec0f4c2ec26f898f538dc8ea1ca3ef2927d597a9439e40a09da937 \ + --hash=sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867 \ + --hash=sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d \ + --hash=sha256:b7c895310363f310361e0fe2072af85269d2a2a285cd04c0c59e79a5e3670dcf \ + --hash=sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f \ + --hash=sha256:bd3d1145f603b2db19feb9078c2e6855eb7c67e15580c010ed815cee519b86fd \ + --hash=sha256:c38c902a0986eba1f6e7ba1ab39ad5195926abde90f3fe080e08212db62176da \ + --hash=sha256:c3fe76c2cac86b4f7a1e92865ac0a54384deb05c92986287c1a7110d9bd53071 \ + --hash=sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88 \ + --hash=sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17 \ + --hash=sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c \ + --hash=sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66 \ + --hash=sha256:cd320d998cbaa032932830448e39abf3c6a12901295e386e8114db926e10cffb \ + --hash=sha256:cef589bc65fae02d10bca2ac341191c5b33acc2967892ebf4fcbd10eabb7a74c \ + --hash=sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25 \ + --hash=sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0 \ + --hash=sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927 \ + --hash=sha256:dc418cf4c873357964d6624445ed09472e50def990c65dd4e76fc3ba8cd9cef6 \ + --hash=sha256:dfc41ae893d9ceaf22c824f2153a88b30651b20e8758c2cd9ac143f23640563c \ + --hash=sha256:e849e6e139b9671adeac505f72fc05f4af7fd1921faef40295e214fc3b361b59 \ + --hash=sha256:e883de250e299654b1f1680f72a1a9f9ba62c9bd1bce84099c90657349a8dfbb \ + --hash=sha256:e9a5e3406e3ed8125ae1a3b37c12f3434e2b1f0fa053197c5557895b4fb09606 \ + --hash=sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef \ + --hash=sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3 \ + --hash=sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da \ + --hash=sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132 \ + --hash=sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7 \ + --hash=sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f \ + --hash=sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2 \ + --hash=sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f \ + --hash=sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667 + # via feast (pyproject.toml) +grpc-google-iam-v1==0.14.4 \ + --hash=sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038 \ + --hash=sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964 # via google-cloud-bigtable -grpcio==1.62.3 \ - --hash=sha256:059444f0ed5dba73ab7dd0ee7e8e6b606df4130d2b0a9f010f84da4ab9f6c2d8 \ - --hash=sha256:114f2a865886ff33f85d70670e971fe0e3d252a1209656fefa5470286e3fcc76 \ - --hash=sha256:13571a5b868dcc308a55d36669a2d17d9dcd6ec8335213f6c49cc68da7305abe \ - --hash=sha256:1ac0944e9e3ee3e20825226d1e17985e9f88487055c475986cf0922a7d806d8a \ - --hash=sha256:1de3d04d9a4ec31ebe848ae1fe61e4cbc367fb9495cbf6c54368e60609a998d9 \ - --hash=sha256:216740723fc5971429550c374a0c039723b9d4dcaf7ba05227b7e0a500b06417 \ - --hash=sha256:25cd75dc73c5269932413e517be778640402f18cf9a81147e68645bd8af18ab0 \ - --hash=sha256:325c56ce94d738c31059cf91376f625d3effdff8f85c96660a5fd6395d5a707f \ - --hash=sha256:3737e5ef0aa0fcdfeaf3b4ecc1a6be78b494549b28aec4b7f61b5dc357f7d8be \ - --hash=sha256:377babc817e8b4186aed7ed56e832867c513e4e9b6c3503565c344ffdef440d4 \ - --hash=sha256:3fb7d966a976d762a31346353a19fce4afcffbeda3027dd563bc8cb521fcf799 \ - --hash=sha256:43670a25b752b7ed960fcec3db50ae5886dc0df897269b3f5119cde9b731745f \ - --hash=sha256:4439bbd759636e37b66841117a66444b454937e27f0125205d2d117d7827c643 \ - --hash=sha256:454a6aed4ebd56198d37e1f3be6f1c70838e33dd62d1e2cea12f2bcb08efecc5 \ - --hash=sha256:4c9c1502c76cadbf2e145061b63af077b08d5677afcef91970d6db87b30e2f8b \ - --hash=sha256:4dab8b64c438e19c763a6332b55e5efdbecfb7c55ae59a42c38c81ed27955fa5 \ - --hash=sha256:56757d3e4cf5d4b98a30f2c5456151607261c891fa2298a4554848dcbf83083d \ - --hash=sha256:57823dc7299c4f258ae9c32fd327d29f729d359c34d7612b36e48ed45b3ab8d0 \ - --hash=sha256:582bd03e9c3d1bd1162eb51fa0f1a35633d66e73f4f36702d3b8484a8b45eda7 \ - --hash=sha256:620165df24aae3d5b3e84cb8dd6b98f6ed49aed04126186bbf43061e301d6a21 \ - --hash=sha256:646c14e9f3356d3f34a65b58b0f8d08daa741ba1d4fcd4966b79407543332154 \ - --hash=sha256:668211f3699bbee4deaf1d6e6b8df59328bf63f077bf2dc9b8bfa4a17df4a279 \ - --hash=sha256:6be243f3954b0ca709f56f9cae926c84ac96e1cce19844711e647a1f1db88b99 \ - --hash=sha256:6da20a1ae010a988bc4ed47850f1122de0a88e18cd2f901fcf56007be1fc6c30 \ - --hash=sha256:7349cd7445ac65fbe1b744dcab9cc1ec02dae2256941a2e67895926cbf7422b4 \ - --hash=sha256:74f3fc9b93290e58264844f5bc46df4c58a94c4287a277dbcf75344fc6c37ca4 \ - --hash=sha256:75a4e9ac7ff185cad529f35934c5d711b88aca48b90c70e195f5657da50ce321 \ - --hash=sha256:7b33c1807d4ac564a3027d06f21a2220c116ceacaaef614deb96b3341ee58896 \ - --hash=sha256:807176971c504c598976f5a9ea62363cffbbbb6c7509d9808c2342b020880fa2 \ - --hash=sha256:80a82fdee14dc27e9299248b7aabd5a8739a1cf6b76c78aa2b848158b44a99d5 \ - --hash=sha256:81b7c121c4e52a0749bf0759185b8d5cfa48a786cd7d411cdab08269813e0aab \ - --hash=sha256:8257cc9e55fb0e2149a652d9dc14c023720f9e73c9145776e07c97e0a553922e \ - --hash=sha256:8a5f00b2508937952d23a1767739e95bbbe1120f8a66d10187d5e971d56bb55c \ - --hash=sha256:8ae2e7a390b2cdd2a95d3bf3b3385245eeb48a5e853943cb46139666462c2d1a \ - --hash=sha256:940459d81685549afdfe13a6de102c52ea4cdda093477baa53056884aadf7c48 \ - --hash=sha256:9c4aae4e683776c319169d87e7891b67b75e3f1c0beeb877902ea148b0585164 \ - --hash=sha256:9d5f8e0050a179b3bce9189b522dc91008d44f08c757a7c310e0fd06b4d3d147 \ - --hash=sha256:a1b85d35a7d9638c03321dfe466645b87e23c30df1266f9e04bbb5f44e7579a9 \ - --hash=sha256:a82410d7620c07cb32624e38f2a106980564dfef9dbe78f5b295cda9ef217c03 \ - --hash=sha256:abfe64811177e681edc81d9d9d1bd23edc5f599bd9846650864769264ace30cd \ - --hash=sha256:ac9783d5679c8da612465168c820fd0b916e70ec5496c840bddba0be7f2d124c \ - --hash=sha256:b033d50bd41e506e3b579775f54a30c16c222e0d88847ac8098d2eca2a7454cc \ - --hash=sha256:b097347441b86a8c3ad9579abaf5e5f7f82b1d74a898f47360433b2bca0e4536 \ - --hash=sha256:b708401ede2c4cb8943e8a713988fcfe6cbea105b07cd7fa7c8a9f137c22bddb \ - --hash=sha256:bd900e666bb68fff49703084be14407cd73b8a5752a7590cea98ec22de24fb5d \ - --hash=sha256:c118cfc80e2402a5595be36e9245ffd9b0e146f426cc40bdf60015bf183f8373 \ - --hash=sha256:c175b252d063af388523a397dbe8edbc4319761f5ee892a8a0f5890acc067362 \ - --hash=sha256:c8bb1a7aa82af6c7713cdf9dcb8f4ea1024ac7ce82bb0a0a82a49aea5237da34 \ - --hash=sha256:c95a0b76a44c548e6bd8c5f7dbecf89c77e2e16d3965be817b57769c4a30bea2 \ - --hash=sha256:e202e3f963480ca067a261179b1ac610c0f0272cb4a7942d11b7e2b3fc99c3aa \ - --hash=sha256:e9ffdb7bc9ccd56ec201aec3eab3432e1e820335b5a16ad2b37e094218dcd7a6 \ - --hash=sha256:ea7ca66a58421411c6486fa5015fe7704e2816ff0b4ec4fb779ad5e1cbbdabf3 \ - --hash=sha256:f2ff8ac447765e173842b554b31307b98b3bb1852710903ebb936e7efb7df6e5 \ - --hash=sha256:f5def814c5a4c90c8fe389c526ab881f4a28b7e239b23ed8e02dd02934dfaa1a - # via - # feast (setup.py) +grpcio==1.83.0 \ + --hash=sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df \ + --hash=sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867 \ + --hash=sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5 \ + --hash=sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df \ + --hash=sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930 \ + --hash=sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf \ + --hash=sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9 \ + --hash=sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33 \ + --hash=sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da \ + --hash=sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03 \ + --hash=sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa \ + --hash=sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223 \ + --hash=sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9 \ + --hash=sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4 \ + --hash=sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf \ + --hash=sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881 \ + --hash=sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af \ + --hash=sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5 \ + --hash=sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0 \ + --hash=sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727 \ + --hash=sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb \ + --hash=sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a \ + --hash=sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735 \ + --hash=sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16 \ + --hash=sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49 \ + --hash=sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1 \ + --hash=sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24 \ + --hash=sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b \ + --hash=sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f \ + --hash=sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57 \ + --hash=sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf \ + --hash=sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f \ + --hash=sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c \ + --hash=sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969 \ + --hash=sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd \ + --hash=sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c \ + --hash=sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b \ + --hash=sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61 \ + --hash=sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404 \ + --hash=sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617 \ + --hash=sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40 \ + --hash=sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45 \ + --hash=sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc \ + --hash=sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c \ + --hash=sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745 \ + --hash=sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45 \ + --hash=sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c \ + --hash=sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0 \ + --hash=sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d \ + --hash=sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9 \ + --hash=sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8 + # via + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery + # google-cloud-bigquery-storage + # google-cloud-bigtable + # google-cloud-datastore # googleapis-common-protos # grpc-google-iam-v1 # grpcio-health-checking # grpcio-reflection # grpcio-status + # milvus-lite # pymilvus -grpcio-health-checking==1.62.3 \ - --hash=sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93 \ - --hash=sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d - # via feast (setup.py) -grpcio-reflection==1.62.3 \ - --hash=sha256:a48ef37df81a3bada78261fc92ef382f061112f989d1312398b945cc69838b9c \ - --hash=sha256:cb84682933c400bddf94dd94f928d1c6570f500b6dd255973d4bfb495b82585f - # via feast (setup.py) -grpcio-status==1.62.3 \ - --hash=sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485 \ - --hash=sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8 +grpcio-health-checking==1.83.0 \ + --hash=sha256:7d8b47a5bfbc699d4aee0fc7a27f5d0265eb23a6a64db5ac2d8b749a0f8a9911 \ + --hash=sha256:ad6bc4d5a1103ad704d25ccd82def300dfa27996b185cab3e4cceeef2c6867d4 + # via feast (pyproject.toml) +grpcio-reflection==1.83.0 \ + --hash=sha256:6a2a30a462ac2c3c6d149734a8fe655fa7617c17fa2cda9949006f3bf07f5b3e \ + --hash=sha256:9d4151f68f18a85aa5b0eff5d493ad13f9421f96c8d9c01c840b353dc979be93 + # via feast (pyproject.toml) +grpcio-status==1.83.0 \ + --hash=sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae \ + --hash=sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69 # via google-api-core -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via # httpcore + # httpcore2 # uvicorn hatch-fancy-pypi-readme==25.1.0 \ --hash=sha256:9c58ed3dff90d51f43414ce37009ad1d5b0f08ffc9fc216998a06380f01c0045 \ --hash=sha256:ce0134c40d63d874ac48f48ccc678b8f3b62b8e50e9318520d2bffc752eedaf3 - # via feast (setup.py) + # via feast (pyproject.toml) hatch-vcs==0.4.0 \ --hash=sha256:093810748fe01db0d451fabcf2c1ac2688caefd232d4ede967090b1c1b07d9f7 \ --hash=sha256:b8a2b6bee54cf6f9fc93762db73890017ae59c9081d1038a41f16235ceaf8b2c - # via feast (setup.py) -hatchling==1.27.0 \ - --hash=sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6 \ - --hash=sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b + # via feast (pyproject.toml) +hatchling==1.31.0 \ + --hash=sha256:6b48ad4068a482ed7239b3a8215bc55b47aad3345d58dfc94e553c5d2d46211b \ + --hash=sha256:aac80bec8b6fe35e8480f1c335be8910fa210a0e6f735a139be205dadcacb544 # via - # feast (setup.py) + # feast (pyproject.toml) # hatch-fancy-pypi-readme # hatch-vcs -hiredis==2.4.0 \ - --hash=sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027 \ - --hash=sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577 \ - --hash=sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603 \ - --hash=sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869 \ - --hash=sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228 \ - --hash=sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79 \ - --hash=sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb \ - --hash=sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33 \ - --hash=sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77 \ - --hash=sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9 \ - --hash=sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd \ - --hash=sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a \ - --hash=sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8 \ - --hash=sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9 \ - --hash=sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901 \ - --hash=sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7 \ - --hash=sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0 \ - --hash=sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d \ - --hash=sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213 \ - --hash=sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8 \ - --hash=sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389 \ - --hash=sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330 \ - --hash=sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1 \ - --hash=sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2 \ - --hash=sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288 \ - --hash=sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba \ - --hash=sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded \ - --hash=sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca \ - --hash=sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109 \ - --hash=sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8 \ - --hash=sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea \ - --hash=sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6 \ - --hash=sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8 \ - --hash=sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0 \ - --hash=sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5 \ - --hash=sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5 \ - --hash=sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182 \ - --hash=sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73 \ - --hash=sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af \ - --hash=sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac \ - --hash=sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204 \ - --hash=sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277 \ - --hash=sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212 \ - --hash=sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41 \ - --hash=sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e \ - --hash=sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c \ - --hash=sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2 \ - --hash=sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4 \ - --hash=sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348 \ - --hash=sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b \ - --hash=sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e \ - --hash=sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995 \ - --hash=sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371 \ - --hash=sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613 \ - --hash=sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b \ - --hash=sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d \ - --hash=sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708 \ - --hash=sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08 \ - --hash=sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca \ - --hash=sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12 \ - --hash=sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8 \ - --hash=sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829 \ - --hash=sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38 \ - --hash=sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825 \ - --hash=sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67 \ - --hash=sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056 \ - --hash=sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376 \ - --hash=sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214 \ - --hash=sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982 \ - --hash=sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034 \ - --hash=sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1 \ - --hash=sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f \ - --hash=sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9 \ - --hash=sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5 \ - --hash=sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390 \ - --hash=sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0 \ - --hash=sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d \ - --hash=sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57 \ - --hash=sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe \ - --hash=sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2 \ - --hash=sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415 \ - --hash=sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c \ - --hash=sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b \ - --hash=sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853 \ - --hash=sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36 \ - --hash=sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae \ - --hash=sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd \ - --hash=sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187 \ - --hash=sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c \ - --hash=sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542 \ - --hash=sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d \ - --hash=sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79 \ - --hash=sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7 \ - --hash=sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def - # via feast (setup.py) +hiredis==3.4.0 \ + --hash=sha256:02298551060cbfe17b1acb02e9d066eadd51e37357369736f8fc1f5533875255 \ + --hash=sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc \ + --hash=sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865 \ + --hash=sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c \ + --hash=sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec \ + --hash=sha256:06c163607f163a300cba9ef02fd8a234dd644a208bfafe972614f72e5287b9ba \ + --hash=sha256:0731dd9f2bf4d3417947a3cbbca4dd4d9fa6ad7cecfd4e16439c17a4562d6304 \ + --hash=sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419 \ + --hash=sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320 \ + --hash=sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b \ + --hash=sha256:0bcb630add6bc9ea136fce691ddff0c46aa91cb860df4ca789fe44127eb7e90d \ + --hash=sha256:12ea5facb5b08fa23e4c101ec2151f3a3de8ecec412fec58dbde0a6eebca02c7 \ + --hash=sha256:12eca9aea1450d1a85dc15574a985c227e52abbc2b6466f48ad2aa3b82124701 \ + --hash=sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f \ + --hash=sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017 \ + --hash=sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc \ + --hash=sha256:16a3ea8193c5f4ee6e8daafe0126207cef38eec5a923c97047e6985d9bb1b61f \ + --hash=sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4 \ + --hash=sha256:1bfb9ccfb13be63883e5f2e5ff7f6fc87bf256f8243af594257dfbed9dbc3cf0 \ + --hash=sha256:1c9e141b420dae63ea21303a7e8672282326cae67472768e9a3f6e7f0990bd28 \ + --hash=sha256:1d8c21fb85f2a3de9328cf5bace84b63da00028e771b92335ff4fdc00121dd5a \ + --hash=sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5 \ + --hash=sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016 \ + --hash=sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240 \ + --hash=sha256:282c4310af72afbe18b07d416459f4febeaeb805a067a7df790136e0e550fcb2 \ + --hash=sha256:2c432cce38190c3c13b6c28d6840ee85db50765ad510134007f1cfb3844dce73 \ + --hash=sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591 \ + --hash=sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea \ + --hash=sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980 \ + --hash=sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b \ + --hash=sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb \ + --hash=sha256:386b556f48fb0f9f09696b9d37f1cae554123fbd9f091d0ca23087f2aca02887 \ + --hash=sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b \ + --hash=sha256:4190bd07dd7879a8a7ddbb2a4f74d402721f3898276e35beb98851b85b5f539c \ + --hash=sha256:423570ac4d2665c0d55d8707b7859a331e9001da5e437b7b7a23e0acbce770c9 \ + --hash=sha256:43004b0b48abc628dda1ac3ac4871e1326c126f8cd9f11164d61934d827d7a3b \ + --hash=sha256:4404c557fd49bcfe24dff41f1209e4221c76d1607df2fb2dfd39474b5b086dcb \ + --hash=sha256:44660a91e0fbc803c29b337c1a9194c8d7b4cd3a3868d28f747cbec2df165483 \ + --hash=sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582 \ + --hash=sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2 \ + --hash=sha256:45c6c296056641b5df37cedafe7d1553f33bc247e2f81603a4d038b39261879b \ + --hash=sha256:46e041fc7a83eaa989ef5fe09526bc2353a6ba39b4ce5191684eabefc02e17b0 \ + --hash=sha256:4863b99b1bf739eaa60961798efc709f657864fbf5a142cb9b99d3e36a37208e \ + --hash=sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d \ + --hash=sha256:4de6869be2b33490569dae0712366bb794b7f5e7a8b674de3e092b3e95712d6d \ + --hash=sha256:4e5530d7a6c71c1a9c3b850b08bbc8178ffc9556e3aef7f9c808fe9ff91a6f11 \ + --hash=sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763 \ + --hash=sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584 \ + --hash=sha256:5359caad5b57da0bce11d2880f22617ba3710f0866121a924745447848448034 \ + --hash=sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79 \ + --hash=sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9 \ + --hash=sha256:64cf54c1dad35830bf1d86b343ad3c55aed443af51c5ccead8afd1b547aca196 \ + --hash=sha256:6774f1fe2723001ca0cd42bf5d8b1235301226273915c581c5c1260d4d114c43 \ + --hash=sha256:68c81c04e0d8778c01163dd2bc6f0e0236fc7f650c291526f10d4faccd99e5a0 \ + --hash=sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb \ + --hash=sha256:69d0326f20354ce278cbb86f5ae47cb390e22bb94a66877031038af907c42fa5 \ + --hash=sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22 \ + --hash=sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b \ + --hash=sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9 \ + --hash=sha256:7b159315df71a009375227384aa4219f98c2342ccd8eb33e3f4b58654f426376 \ + --hash=sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203 \ + --hash=sha256:7f7fc1535f6e1a190089eae46dee25f0c6b72bb221d377be07092803b8208733 \ + --hash=sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f \ + --hash=sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13 \ + --hash=sha256:85a9abf2d16b4cb112f6cc6c32236d763b34d21c69b00c2f81a4ff3016fcc1d6 \ + --hash=sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c \ + --hash=sha256:899d5cea72c9a6c2d9b6e7b223de62c9e9f541f48267cc7135764070548735ce \ + --hash=sha256:89e95f50ec4d29645ba2da9010e8717861585dc9f3df354a367f43807a6db3c7 \ + --hash=sha256:8aaaab18314fd25453b5cf59c8cdca4110e419455bcb4c0737d19d4151513e75 \ + --hash=sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1 \ + --hash=sha256:8f2c8569460aa456a294d5e9a3579a382b825dc2e283e3cc398323dcfb6a3dee \ + --hash=sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151 \ + --hash=sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a \ + --hash=sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d \ + --hash=sha256:98e28c10e43d076f50ce9fa9f4017303d5796c3058b1b651f507c2a7d6ef402c \ + --hash=sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f \ + --hash=sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d \ + --hash=sha256:a315009b441a0105a373a9a780ebb1c6f7d9ead88ac6ea5f2a15791353c6f590 \ + --hash=sha256:a3796094f616f72976ff51e4dc1a016e753c0f9af5393b2df96920b6bae1e19b \ + --hash=sha256:a427976ba339da624367613d7eba52a1516e6d5c7f8988dc8fb888fbcf52d8b6 \ + --hash=sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb \ + --hash=sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530 \ + --hash=sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4 \ + --hash=sha256:b0c29ac4f50fc61a904acfe76e4ce6bc37e16ab5a98089cb411476d466e1bb36 \ + --hash=sha256:b32afb576705fc5a02440d416ef476ddbcc827a20ff6c4a9d557882bc6b75135 \ + --hash=sha256:b6228afacadb19a858289319d72797023e2cb048f2f930b68b7fe57eaed5fb6b \ + --hash=sha256:b98f44bf873731d9ce99e6b3eeb6196bf7b27693ad9f6de0fba986b2b0845127 \ + --hash=sha256:bb44efa4fa3e3ed7779ad0ade3c08ed5d75ca7a6336893e9a4f2722093b4168a \ + --hash=sha256:bb8b2dd9c00d80ad2870ffbb51ce9af99fb2194c082d7b09f4125d90e21426f6 \ + --hash=sha256:bca175f02a2b0150ffe7f5dc8bf49c798f34d2c7024d17ace0ec97a7583560e3 \ + --hash=sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59 \ + --hash=sha256:c2245c46b4ced5f689469e6dcdfc8a0895bf873840a6600f5ea759cdf1b26a8b \ + --hash=sha256:c8c6337e7ad187e8039d06e4b33ee5148dcd73928819d94087e649eb37316a09 \ + --hash=sha256:ccc5c660e31d788ca534a20f2ccb7a80b946b960e18ed4e1db950fcac122b405 \ + --hash=sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643 \ + --hash=sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c \ + --hash=sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4 \ + --hash=sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d \ + --hash=sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352 \ + --hash=sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81 \ + --hash=sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5 \ + --hash=sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092 \ + --hash=sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4 \ + --hash=sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad \ + --hash=sha256:e29267ecdd08758926f1a9221af2671d90f475480c40aff409921b1f362f1bd5 \ + --hash=sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d \ + --hash=sha256:ed1dba2695f6de009c67d63b39ff978cb43b8a79362f697acedffb7743e50d21 \ + --hash=sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1 \ + --hash=sha256:f3c67f39b112dc35f68d5b59ee111db6121f037d1a60cf3840ecffbb2ec5686b \ + --hash=sha256:f7c7596fbb2b5202e943180353958e89014e763c7f25877a92f70bbde6cd7f19 \ + --hash=sha256:ff28eca77a57751488df90b0f385b472fd78e140a4d45097224648a5737acdf5 \ + --hash=sha256:ff6217da86a63a6a5ea954d9b9ed67916d9d94a566b44aa98e68ae34b40ebbbf + # via feast (pyproject.toml) httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httpcore2==2.9.1 \ + --hash=sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2 \ + --hash=sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26 + # via httpx2 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad - # via - # fastapi-mcp - # mcp -httpx-sse==0.4.3 \ - --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ - --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d + # via fastapi-mcp +httpx2==2.9.1 \ + --hash=sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a \ + --hash=sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a # via mcp -ibis-framework[duckdb]==9.5.0 \ - --hash=sha256:145fe30d94f111cff332580c275ce77725c5ff7086eede93af0b371649d009c0 \ - --hash=sha256:1c8a29277e63ee0dfc289bc8f550164b5e3bdaec1b76b62436c37d331bb4ef84 - # via - # feast (setup.py) - # ibis-substrait -ibis-substrait==4.0.1 \ - --hash=sha256:107ca49383a3cca2fdc88f67ea2f0172620c16fa8f39c9c52305af85dd6180b4 \ - --hash=sha256:614810a173d096fbc49d87a9b419e2162a3c25d8efda1a4d57a389ce56b9041f - # via feast (setup.py) -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +ibis-framework[duckdb]==12.0.0 \ + --hash=sha256:0bbd790f268da9cb87926d5eaad2b827a573927113c4ed3be5095efa89b9e512 \ + --hash=sha256:238624f2c14fdab8382ca2f4f667c3cdb81e29844cd5f8db8a325d0743767c61 + # via feast (pyproject.toml) +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx + # httpx2 # requests # snowflake-connector-python # yarl -imagesize==1.4.1 \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a +imagesize==2.0.0 \ + --hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \ + --hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3 # via sphinx jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 # via - # feast (setup.py) - # poetry-dynamic-versioning + # feast (pyproject.toml) # sphinx -jmespath==1.0.1 \ - --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \ - --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe +jmespath==1.1.0 \ + --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \ + --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64 # via # aiobotocore # boto3 # botocore -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 - # via - # feast (setup.py) - # mcp +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via mcp jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -kubernetes==33.1.0 \ - --hash=sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5 \ - --hash=sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993 - # via feast (setup.py) +kubernetes==36.0.3 \ + --hash=sha256:36993ed25ce59b789c9341473a228fcf268504a2fec7c2b2b1531d73072e5ce7 \ + --hash=sha256:8fde9241c4b298e6374a069dcf728359b4e72c2fb29489a975ba4e1c047cf10f + # via feast (pyproject.toml) +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ --hash=sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 # via partd -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich markupsafe==3.0.3 \ --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ @@ -1365,14 +1528,14 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -marshmallow==4.0.1 \ - --hash=sha256:72f14ef346f81269dbddee891bac547dda1501e9e08b6a809756ea3dbb7936a1 \ - --hash=sha256:e1d860bd262737cb2d34e1541b84cb52c32c72c9474e3fe6f30f137ef8b0d97f - # via environs -mcp==1.12.4 \ - --hash=sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5 \ - --hash=sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789 +mcp==2.0.0 \ + --hash=sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728 \ + --hash=sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6 # via fastapi-mcp +mcp-types==2.0.0 \ + --hash=sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0 \ + --hash=sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379 + # via mcp mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba @@ -1381,437 +1544,479 @@ meson==1.7.1 \ --hash=sha256:155780a5be87f6dd7f427ad8bcbf0f2b2c5f62ee5fdacca7caa9de8439a24b89 \ --hash=sha256:6d9cbc9ce87a70243c75e4cc668ee3f206ab50b184beb0a08ece948112f19bd7 # via - # feast (setup.py) + # feast (pyproject.toml) # meson-python meson-python==0.15.0 \ --hash=sha256:3ae38253ff02b2e947a05e362a2eaf5a9a09d133c5666b4123399ee5fbf2e591 \ --hash=sha256:fddb73eecd49e89c1c41c87937cd89c2d0b65a1c63ba28238681d4bd9484d26f - # via feast (setup.py) -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) -multidict==6.7.0 \ - --hash=sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32 \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ - --hash=sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff \ - --hash=sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8 \ - --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ - --hash=sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721 \ - --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ - --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6 \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ - --hash=sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912 \ - --hash=sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c \ - --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6 \ - --hash=sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84 \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ - --hash=sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd \ - --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0 \ - --hash=sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e \ - --hash=sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6 \ - --hash=sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc \ - --hash=sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7 \ - --hash=sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4 \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111 \ - --hash=sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e \ - --hash=sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84 \ - --hash=sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851 \ - --hash=sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb \ - --hash=sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ - --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5 \ - --hash=sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb \ - --hash=sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349 \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ - --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ - --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34 \ - --hash=sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85 \ - --hash=sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ - --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ - --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ - --hash=sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91 \ - --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40 \ - --hash=sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73 \ - --hash=sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4 \ - --hash=sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687 \ - --hash=sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 + # via feast (pyproject.toml) +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via feast (pyproject.toml) +multidict==6.7.1 \ + --hash=sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0 \ + --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ + --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ + --hash=sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2 \ + --hash=sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941 \ + --hash=sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3 \ + --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ + --hash=sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962 \ + --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ + --hash=sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f \ + --hash=sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c \ + --hash=sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8 \ + --hash=sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa \ + --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ + --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ + --hash=sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991 \ + --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ + --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ + --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ + --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ + --hash=sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5 \ + --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ + --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ + --hash=sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505 \ + --hash=sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0 \ + --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ + --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ + --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ + --hash=sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511 \ + --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ + --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ + --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ + --hash=sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e \ + --hash=sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582 \ + --hash=sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0 \ + --hash=sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2 \ + --hash=sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e \ + --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ + --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ + --hash=sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a \ + --hash=sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd \ + --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ + --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ + --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ + --hash=sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144 \ + --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ + --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ + --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ + --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ + --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ + --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ + --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ + --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ + --hash=sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568 \ + --hash=sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db \ + --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ + --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ + --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ + --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ + --hash=sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71 \ + --hash=sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f \ + --hash=sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0 \ + --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ + --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ + --hash=sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8 \ + --hash=sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49 \ + --hash=sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0 \ + --hash=sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9 \ + --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ + --hash=sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190 \ + --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ + --hash=sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d \ + --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ + --hash=sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e \ + --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ + --hash=sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40 \ + --hash=sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3 \ + --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ + --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ + --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ + --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ + --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ + --hash=sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a \ + --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ + --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ + --hash=sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8 \ + --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ + --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ + --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ + --hash=sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8 \ + --hash=sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92 \ + --hash=sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733 \ + --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ + --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ + --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ + --hash=sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6 \ + --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ + --hash=sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172 \ + --hash=sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981 \ + --hash=sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5 \ + --hash=sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de \ + --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ + --hash=sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7 \ + --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ + --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ + --hash=sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6 \ + --hash=sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf \ + --hash=sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f \ + --hash=sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b \ + --hash=sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961 \ + --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ + --hash=sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3 \ + --hash=sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b \ + --hash=sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358 \ + --hash=sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6 \ + --hash=sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e \ + --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ + --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ + --hash=sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5 \ + --hash=sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53 \ + --hash=sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872 \ + --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ + --hash=sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df \ + --hash=sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03 \ + --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ + --hash=sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a \ + --hash=sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122 \ + --hash=sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a \ + --hash=sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee \ + --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ + --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ + --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ + --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ + --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ + --hash=sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75 \ + --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ + --hash=sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a \ + --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ + --hash=sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855 \ + --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ + --hash=sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4 \ + --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ + --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ + --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 \ + --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ + --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via # aiobotocore # aiohttp # yarl -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mypy==2.3.0 \ + --hash=sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491 \ + --hash=sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762 \ + --hash=sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5 \ + --hash=sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654 \ + --hash=sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd \ + --hash=sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe \ + --hash=sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc \ + --hash=sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3 \ + --hash=sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5 \ + --hash=sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329 \ + --hash=sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b \ + --hash=sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3 \ + --hash=sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7 \ + --hash=sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805 \ + --hash=sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f \ + --hash=sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e \ + --hash=sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7 \ + --hash=sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494 \ + --hash=sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373 \ + --hash=sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88 \ + --hash=sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee \ + --hash=sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2 \ + --hash=sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757 \ + --hash=sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a \ + --hash=sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b \ + --hash=sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461 \ + --hash=sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97 \ + --hash=sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4 \ + --hash=sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117 \ + --hash=sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36 \ + --hash=sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac \ + --hash=sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5 \ + --hash=sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556 \ + --hash=sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c \ + --hash=sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88 \ + --hash=sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568 \ + --hash=sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595 \ + --hash=sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f \ + --hash=sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1 \ + --hash=sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef \ + --hash=sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6 \ + --hash=sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db \ + --hash=sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff \ + --hash=sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60 \ + --hash=sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c # via sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf - # via - # feast (setup.py) +numpy==2.5.1 \ + --hash=sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2 \ + --hash=sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d \ + --hash=sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1 \ + --hash=sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b \ + --hash=sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd \ + --hash=sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077 \ + --hash=sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a \ + --hash=sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e \ + --hash=sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277 \ + --hash=sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6 \ + --hash=sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75 \ + --hash=sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7 \ + --hash=sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1 \ + --hash=sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9 \ + --hash=sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21 \ + --hash=sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca \ + --hash=sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0 \ + --hash=sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb \ + --hash=sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d \ + --hash=sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75 \ + --hash=sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74 \ + --hash=sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf \ + --hash=sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0 \ + --hash=sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8 \ + --hash=sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af \ + --hash=sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a \ + --hash=sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4 \ + --hash=sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22 \ + --hash=sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3 \ + --hash=sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1 \ + --hash=sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b \ + --hash=sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1 \ + --hash=sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373 \ + --hash=sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95 \ + --hash=sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6 \ + --hash=sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09 \ + --hash=sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9 \ + --hash=sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438 \ + --hash=sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2 \ + --hash=sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7 \ + --hash=sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace \ + --hash=sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3 \ + --hash=sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2 \ + --hash=sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107 + # via + # feast (pyproject.toml) # dask # db-dtypes + # faiss-cpu # ibis-framework + # milvus-lite # pandas # pandas-gbq - # pyarrow oauthlib==3.3.1 \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 - # via - # kubernetes - # requests-oauthlib -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f + # via requests-oauthlib +opentelemetry-api==1.44.0 \ + --hash=sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a \ + --hash=sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef + # via mcp +orjson==3.11.9 \ + --hash=sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4 \ + --hash=sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62 \ + --hash=sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979 \ + --hash=sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0 \ + --hash=sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d \ + --hash=sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a \ + --hash=sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd \ + --hash=sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce \ + --hash=sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1 \ + --hash=sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180 \ + --hash=sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980 \ + --hash=sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697 \ + --hash=sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e \ + --hash=sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1 \ + --hash=sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09 \ + --hash=sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd \ + --hash=sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470 \ + --hash=sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b \ + --hash=sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877 \ + --hash=sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb \ + --hash=sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd \ + --hash=sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe \ + --hash=sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97 \ + --hash=sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c \ + --hash=sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5 \ + --hash=sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021 \ + --hash=sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362 \ + --hash=sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206 \ + --hash=sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4 \ + --hash=sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218 \ + --hash=sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9 \ + --hash=sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f \ + --hash=sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4 \ + --hash=sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02 \ + --hash=sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be \ + --hash=sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972 \ + --hash=sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586 \ + --hash=sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2 \ + --hash=sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124 \ + --hash=sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa \ + --hash=sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47 \ + --hash=sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c \ + --hash=sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244 \ + --hash=sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13 \ + --hash=sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10 \ + --hash=sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677 \ + --hash=sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48 \ + --hash=sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4 \ + --hash=sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624 \ + --hash=sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49 \ + --hash=sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0 \ + --hash=sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b \ + --hash=sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c \ + --hash=sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2 \ + --hash=sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db \ + --hash=sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92 \ + --hash=sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94 \ + --hash=sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e \ + --hash=sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61 \ + --hash=sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882 \ + --hash=sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff \ + --hash=sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254 \ + --hash=sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f \ + --hash=sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32 \ + --hash=sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff \ + --hash=sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673 \ + --hash=sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291 \ + --hash=sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0 \ + --hash=sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c \ + --hash=sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9 \ + --hash=sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f \ + --hash=sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e \ + --hash=sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7 \ + --hash=sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81 + # via pymilvus +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # dask # db-dtypes - # dunamai + # faiss-cpu # google-cloud-bigquery # gunicorn # hatchling # ibis-framework - # ibis-substrait # pandas-gbq # pyproject-metadata # scikit-build-core # setuptools-scm # snowflake-connector-python # sphinx + # vcs-versioning pandas==2.3.3 \ --hash=sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7 \ --hash=sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593 \ @@ -1869,7 +2074,7 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery @@ -1877,9 +2082,9 @@ pandas==2.3.3 \ # pandas-gbq # pymilvus # snowflake-connector-python -pandas-gbq==0.29.2 \ - --hash=sha256:99f6b642bd8340e96c2acdf7d717108aa0d31c703c2f284817ba7dc3b16110b2 \ - --hash=sha256:a3a28e7de4f8456fc4b8e9b7472b9bc470d2566f89c4cd997599e504f5a0a706 +pandas-gbq==0.35.0 \ + --hash=sha256:258de481019566611031919997bf9c1ece4ca30a4dd02d3fc3664b251d446182 \ + --hash=sha256:596c908487ef0649a161e86ef272c00c267e581b95dc5ee0dc3518545b33bcfc # via google-cloud-bigquery parsy==2.2 \ --hash=sha256:5e981613d9d2d8b68012d1dd0afe928967bea2e4eefdb76c2f545af0dd02a9e7 \ @@ -1899,180 +2104,170 @@ patchelf==0.17.2.4 \ --hash=sha256:ae44cb3c857d50f54b99e5697aa978726ada33a8a6129d4b8b7ffd28b996652d \ --hash=sha256:d842b51f0401460f3b1f3a3a67d2c266a8f515a5adfbfa6e7b656cb3ac2ed8bc \ --hash=sha256:d9b35ebfada70c02679ad036407d9724ffe1255122ba4ac5e4be5868618a5689 - # via feast (setup.py) -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 + # via feast (pyproject.toml) +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via # hatchling # mypy # scikit-build-core -platformdirs==4.5.0 \ - --hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \ - --hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 +platformdirs==4.11.0 \ + --hash=sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0 \ + --hash=sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74 # via snowflake-connector-python pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via hatchling -poetry-core==1.9.1 \ - --hash=sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0 \ - --hash=sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8 - # via feast (setup.py) -poetry-dynamic-versioning==1.9.1 \ - --hash=sha256:65a0c814e6d30d4807734a3c34edf261fd7cc3b340dbd23b6a33ee41f7d0b547 \ - --hash=sha256:d6e7b9df817aa2ca4946cd695c6c89e1379d2e6c640f008a9b6170d081a9da48 - # via feast (setup.py) -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via feast (setup.py) -propcache==0.4.1 \ - --hash=sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888 \ - --hash=sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37 \ - --hash=sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8 \ - --hash=sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5 \ - --hash=sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938 \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925 \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900 \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21 \ - --hash=sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb \ - --hash=sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19 \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00 \ - --hash=sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f \ - --hash=sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88 \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 \ - --hash=sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781 +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via feast (pyproject.toml) +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274 \ + --hash=sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.26.1 \ - --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ - --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +proto-plus==1.28.2 \ + --hash=sha256:26d843eb99c1e32fdf1d20ff0faae56607f7748fe774acf9ecd5cfe6c6472501 \ + --hash=sha256:b874236fcac2358f601e4330bcb76cb8b89c851303ccf4078408b3d4774d1c52 # via # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable # google-cloud-datastore -protobuf==6.32.1 \ - --hash=sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346 \ - --hash=sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4 \ - --hash=sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1 \ - --hash=sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085 \ - --hash=sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1 \ - --hash=sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710 \ - --hash=sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122 \ - --hash=sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281 \ - --hash=sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d - # via - # feast (setup.py) +protobuf==7.35.1 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a + # via + # feast (pyproject.toml) # google-api-core # google-cloud-bigquery-storage # google-cloud-bigtable @@ -2084,84 +2279,103 @@ protobuf==6.32.1 \ # grpcio-status # proto-plus # pymilvus - # substrait -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 - # via feast (setup.py) -psycopg[c, pool]==3.2.5 \ - --hash=sha256:b782130983e5b3de30b4c529623d3687033b4dafa05bb661fc6bf45837ca5879 \ - --hash=sha256:f5f750611c67cb200e85b408882f29265c66d1de7f813add4f8125978bfd70e8 - # via feast (setup.py) -psycopg-c==3.2.5 \ - --hash=sha256:57ad4cfd28de278c424aaceb1f2ad5c7910466e315dfe84e403f3c7a0a2ce81b +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via + # feast (pyproject.toml) + # pandas-gbq +psycopg[c, pool]==3.3.4 \ + --hash=sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a \ + --hash=sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc + # via feast (pyproject.toml) +psycopg-c==3.3.4 \ + --hash=sha256:ed8106128b2d04359c185fc9641b4409abfce4d0b6fb1d1ff6800646e27f1a22 # via psycopg -psycopg-pool==3.2.6 \ - --hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \ - --hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7 +psycopg-pool==3.3.1 \ + --hash=sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5 \ + --hash=sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c # via psycopg -pyarrow==17.0.0 \ - --hash=sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a \ - --hash=sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca \ - --hash=sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597 \ - --hash=sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c \ - --hash=sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb \ - --hash=sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977 \ - --hash=sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3 \ - --hash=sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687 \ - --hash=sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7 \ - --hash=sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204 \ - --hash=sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28 \ - --hash=sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087 \ - --hash=sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15 \ - --hash=sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc \ - --hash=sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2 \ - --hash=sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155 \ - --hash=sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df \ - --hash=sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22 \ - --hash=sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a \ - --hash=sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b \ - --hash=sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03 \ - --hash=sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda \ - --hash=sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07 \ - --hash=sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204 \ - --hash=sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b \ - --hash=sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c \ - --hash=sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545 \ - --hash=sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655 \ - --hash=sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420 \ - --hash=sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5 \ - --hash=sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4 \ - --hash=sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8 \ - --hash=sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053 \ - --hash=sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145 \ - --hash=sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047 \ - --hash=sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8 - # via - # feast (setup.py) +pyarrow==25.0.0 \ + --hash=sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be \ + --hash=sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc \ + --hash=sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec \ + --hash=sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849 \ + --hash=sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e \ + --hash=sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8 \ + --hash=sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d \ + --hash=sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537 \ + --hash=sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062 \ + --hash=sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e \ + --hash=sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104 \ + --hash=sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517 \ + --hash=sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e \ + --hash=sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5 \ + --hash=sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887 \ + --hash=sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1 \ + --hash=sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b \ + --hash=sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be \ + --hash=sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778 \ + --hash=sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e \ + --hash=sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194 \ + --hash=sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2 \ + --hash=sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9 \ + --hash=sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b \ + --hash=sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18 \ + --hash=sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7 \ + --hash=sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc \ + --hash=sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62 \ + --hash=sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0 \ + --hash=sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f \ + --hash=sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e \ + --hash=sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862 \ + --hash=sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50 \ + --hash=sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe \ + --hash=sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3 \ + --hash=sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a \ + --hash=sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4 \ + --hash=sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af \ + --hash=sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b \ + --hash=sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0 \ + --hash=sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f \ + --hash=sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec \ + --hash=sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f + # via + # feast (pyproject.toml) # dask # db-dtypes # google-cloud-bigquery # ibis-framework + # milvus-lite # pandas-gbq # snowflake-connector-python pyarrow-hotfix==0.7 \ --hash=sha256:3236f3b5f1260f0e2ac070a55c1a7b339c4bb7267839bd2015e283234e758100 \ --hash=sha256:59399cd58bdd978b2e42816a4183a55c6472d4e33d183351b6069f11ed42661d # via ibis-framework -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b + # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 @@ -2169,160 +2383,176 @@ pyasn1-modules==0.4.2 \ pybindgen==0.22.0 \ --hash=sha256:21612f4806a2af25a175716d7e694563af7e10c704538a350cb595d187952f6f \ --hash=sha256:5b4837d3138ac56863d93fe462f1dac39fb87bd50898e0da4c57fefd645437ac - # via feast (setup.py) -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 + # via feast (pyproject.toml) +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi # fastapi-mcp # mcp + # mcp-types # pydantic-settings -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pydantic-settings==2.11.0 \ - --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ - --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c - # via - # fastapi-mcp - # mcp +pydantic-settings==2.14.2 \ + --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ + --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f + # via fastapi-mcp pydata-google-auth==1.9.1 \ --hash=sha256:0a51ce41c601ca0bc69b8795bf58bedff74b4a6a007c9106c7cbcdec00eaced2 \ --hash=sha256:75ffce5d106e34b717b31844c1639ea505b7d9550dc23b96fb6c20d086b53fa3 # via pandas-gbq -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via - # feast (setup.py) + # feast (pyproject.toml) # rich # sphinx -pyjwt==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via - # feast (setup.py) + # feast (pyproject.toml) + # mcp # snowflake-connector-python -pymilvus==2.4.9 \ - --hash=sha256:0937663700007c23a84cfc0656160b301f6ff9247aaec4c96d599a6b43572136 \ - --hash=sha256:45313607d2c164064bdc44e0f933cb6d6afa92e9efcc7f357c5240c57db58fbe - # via feast (setup.py) -pymysql==1.1.2 \ - --hash=sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03 \ - --hash=sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 - # via feast (setup.py) -pyopenssl==25.3.0 \ - --hash=sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6 \ - --hash=sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329 +pymysql==1.2.0 \ + --hash=sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0 \ + --hash=sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33 + # via feast (pyproject.toml) +pyopenssl==26.3.0 \ + --hash=sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3 \ + --hash=sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341 # via snowflake-connector-python -pyproject-metadata==0.9.1 \ - --hash=sha256:b8b2253dd1b7062b78cf949a115f02ba7fa4114aabe63fa10528e9e1a954a816 \ - --hash=sha256:ee5efde548c3ed9b75a354fc319d5afd25e9585fa918a34f62f904cc731973ad +pyproject-metadata==0.12.1 \ + --hash=sha256:8809a4df6fe08279b39a8890669506ed3158e0617855ac9aff098fcbe772ae4c \ + --hash=sha256:f7162d580a96386a8eb096da06215f981f547d1490f03055ef99e323bc2da427 # via meson-python python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ @@ -2334,22 +2564,21 @@ python-dateutil==2.9.0.post0 \ # ibis-framework # kubernetes # pandas -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via - # environs # pydantic-settings + # pymilvus # uvicorn -python-multipart==0.0.20 \ - --hash=sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104 \ - --hash=sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13 +python-multipart==0.0.32 \ + --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ + --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 # via mcp -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via - # ibis-framework # pandas # snowflake-connector-python pyyaml==6.0.3 \ @@ -2427,31 +2656,31 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # dask - # ibis-substrait # kubernetes # uvicorn -redis==4.6.0 \ - --hash=sha256:585dc516b9eb042a619ef0a39c3d7d55fe81bdb4df09a52c9cdde0d07bf1aa7d \ - --hash=sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c - # via feast (setup.py) +redis==7.4.1 \ + --hash=sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e \ + --hash=sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0 + # via feast (pyproject.toml) referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 # via # jsonschema # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # google-api-core # google-cloud-bigquery # google-cloud-storage # kubernetes + # pymilvus # requests-oauthlib # snowflake-connector-python # sphinx @@ -2461,200 +2690,156 @@ requests-oauthlib==2.0.0 \ # via # google-auth-oauthlib # kubernetes -rich==13.9.4 \ - --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ - --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via # fastapi-mcp # ibis-framework # typer -roman-numerals-py==3.1.0 \ - --hash=sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c \ - --hash=sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d +roman-numerals==4.1.0 \ + --hash=sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2 \ + --hash=sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7 # via sphinx -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 +rpds-py==2026.6.3 \ + --hash=sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5 \ + --hash=sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680 \ + --hash=sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9 \ + --hash=sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538 \ + --hash=sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804 \ + --hash=sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf \ + --hash=sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4 \ + --hash=sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97 \ + --hash=sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6 \ + --hash=sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96 \ + --hash=sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a \ + --hash=sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187 \ + --hash=sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975 \ + --hash=sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f \ + --hash=sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703 \ + --hash=sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9 \ + --hash=sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127 \ + --hash=sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f \ + --hash=sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa \ + --hash=sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05 \ + --hash=sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171 \ + --hash=sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba \ + --hash=sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c \ + --hash=sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223 \ + --hash=sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4 \ + --hash=sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885 \ + --hash=sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698 \ + --hash=sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f \ + --hash=sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7 \ + --hash=sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed \ + --hash=sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f \ + --hash=sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf \ + --hash=sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e \ + --hash=sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f \ + --hash=sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24 \ + --hash=sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a \ + --hash=sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41 \ + --hash=sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc \ + --hash=sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d \ + --hash=sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146 \ + --hash=sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e \ + --hash=sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e \ + --hash=sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4 \ + --hash=sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12 \ + --hash=sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7 \ + --hash=sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261 \ + --hash=sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6 \ + --hash=sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5 \ + --hash=sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93 \ + --hash=sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7 \ + --hash=sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda \ + --hash=sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8 \ + --hash=sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342 \ + --hash=sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c \ + --hash=sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb \ + --hash=sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0 \ + --hash=sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77 \ + --hash=sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3 \ + --hash=sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885 \ + --hash=sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826 \ + --hash=sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617 \ + --hash=sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb \ + --hash=sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577 \ + --hash=sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80 \ + --hash=sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e \ + --hash=sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945 \ + --hash=sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90 \ + --hash=sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7 \ + --hash=sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0 \ + --hash=sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140 \ + --hash=sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822 \ + --hash=sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba \ + --hash=sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9 \ + --hash=sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4 \ + --hash=sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a \ + --hash=sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8 \ + --hash=sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf \ + --hash=sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4 \ + --hash=sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324 \ + --hash=sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53 \ + --hash=sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b \ + --hash=sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41 \ + --hash=sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9 \ + --hash=sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca \ + --hash=sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1 \ + --hash=sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d \ + --hash=sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690 \ + --hash=sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107 \ + --hash=sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2 \ + --hash=sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76 \ + --hash=sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d \ + --hash=sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af \ + --hash=sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6 \ + --hash=sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db \ + --hash=sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369 \ + --hash=sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd \ + --hash=sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911 \ + --hash=sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504 \ + --hash=sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a \ + --hash=sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9 \ + --hash=sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13 \ + --hash=sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc \ + --hash=sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278 \ + --hash=sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868 \ + --hash=sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2 \ + --hash=sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd \ + --hash=sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4 \ + --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6 \ + --hash=sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9 \ + --hash=sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00 \ + --hash=sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f \ + --hash=sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e \ + --hash=sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442 \ + --hash=sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da \ + --hash=sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90 \ + --hash=sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef # via # jsonschema # referencing -rsa==4.9.1 \ - --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \ - --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75 - # via google-auth -s3transfer==0.13.1 \ - --hash=sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724 \ - --hash=sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf +s3transfer==0.19.2 \ + --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \ + --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25 # via boto3 -scikit-build-core==0.11.6 \ - --hash=sha256:5982ccd839735be99cfd3b92a8847c6c196692f476c215da84b79d2ad12f9f1b \ - --hash=sha256:ce6d8fe64e6b4c759ea0fb95d2f8a68f60d2df31c2989838633b8ec930736360 - # via feast (setup.py) -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c - # via - # feast (setup.py) +scikit-build-core==1.0.3 \ + --hash=sha256:a4d7a05978ee37975c37743510c8991e2debce7ef83afb0a07c0c576fd4f16e8 \ + --hash=sha256:ae95427b7d3c14a6cf8bbfd4d901f6138ab64c99e20cbe8ea7d75cd26093f085 + # via feast (pyproject.toml) +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 + # via + # feast (pyproject.toml) # pandas-gbq # pydata-google-auth - # pymilvus # setuptools-scm -setuptools-scm==9.2.1 \ - --hash=sha256:2f5b21e45205e47ed3976e58bc29c38f49bb9b6f07c625c7fd2823871694b244 \ - --hash=sha256:84c9b3f39ffb77ba00dd92a06e025e4f66dc69ab8df1c2a225a0678e76311f8d +setuptools-scm==10.2.1 \ + --hash=sha256:4fa7dd82cf8c800df59c9a288c90299b1657ff1ecfc3f5cc00287c5dbf5e27a9 \ + --hash=sha256:b7c82f4102d389ee57dc66ccdb4f9b4bca3c40ba83b43f1f63d68ccd72db2580 # via hatch-vcs shellingham==1.5.4 \ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ @@ -2666,50 +2851,46 @@ six==1.17.0 \ # via # kubernetes # python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio -snowballstemmer==3.0.1 \ - --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ - --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 +snowballstemmer==3.1.1 \ + --hash=sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 \ + --hash=sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260 # via sphinx -snowflake-connector-python[pandas]==3.18.0 \ - --hash=sha256:0af10b207af3d2de2b130e89018d49a60f2e5cfe841f3bf459e58f2e1c4c4506 \ - --hash=sha256:1841b60dc376639493dfc520cf39ad4f4da1f30286bba57e878d57414263d628 \ - --hash=sha256:1afbd9e21180d2b4a76500ac2978b11865fdb3230609f2a9d80ba459fc27f2e4 \ - --hash=sha256:1fb9fc9d8c2c7d209ba89282d367a32e75b0688afd4a3f02409e24f153c1a32e \ - --hash=sha256:283366b35df88cd0c71caf0215ba80370ddef4dd37d2adf43b24208c747231ee \ - --hash=sha256:2e4c285cc6a7f6431cff98c8f235a0fe9da2262462dd3dfc2b97120574a95cf9 \ - --hash=sha256:32b1abfea32561d817b0a2f80b06d936cb32712af06bf7b848a428bfd857a10a \ - --hash=sha256:3fee7035f865088f948510b094101c8a0e5b22501891f2115f7fb1cb555de76a \ - --hash=sha256:41a46eb9824574c5f8068e3ed5c02a2dc0a733ed08ee81fa1fb3dd0ebe921728 \ - --hash=sha256:4c068c8d3cd0c9736cb0679a9f544d34327e64415303bbfe07ec8ce3c5dae800 \ - --hash=sha256:4ed2d593f1983939d5d8d88b212d86fd4f14f0ceefc1df9882b4a18534adbde9 \ - --hash=sha256:51eb789a09dc6c62119cfabd044fba1a6b8378206f05a1e83ddb2e9cb49acc0b \ - --hash=sha256:5d89f608fde2fb0597ca5e020c4ac602027dc67f11b61b4d1e5448163bae4edc \ - --hash=sha256:65d37263dd288abb649820b7e34af96dc6b2d2115bf5521a2526245f81ddb0cb \ - --hash=sha256:7116cfa410d517328fd25fabffb54845b88667586718578c4333ce034fead1ba \ - --hash=sha256:783a9ab206563d7b52fdcdd7a72af62de811d3381ca64132fd3445537b4d041b \ - --hash=sha256:7a5fcb9a25a9b77b6cd86dfc6a6324b9910e15a493a916983229011ce3509b5f \ - --hash=sha256:8d3e96e1d09b07edca6c1f6ca675b6fdd05a4a7e428e4cdf6fb697d87b9f60fc \ - --hash=sha256:94e041e347b5151b66d19d6cfc3b3172dac1f51e44bbf7cf58f3989427dd464a \ - --hash=sha256:a8c570edff5a4888840dbe1e9e65c5e4d77d55c5c800cd359fe0903a769201e0 \ - --hash=sha256:aeeb181a156333480f60b5f8ddbb3d087e288b4509adbef7993236defe4d7570 \ - --hash=sha256:b211b4240596a225b895261a4ced2633e0262e82e2e32f6fb8dfc7d4bfedf8ca \ - --hash=sha256:b99f261c82be92224ac20c8c12bdf26ce3ed5dfd8a3df8a97f15a1e11c46ad27 \ - --hash=sha256:bd1de3038b6d7059ca59f93e105aba2a673151c693cc4292f72f38bfaf147df2 \ - --hash=sha256:cfa6b234f53ec624149e21156d0a98e43408d194f2e65bcfaf30acefd35a581e \ - --hash=sha256:e17a9e806823d3a0e578cf9349f6a93810a582b3132903ea9e1683854d08da00 - # via feast (setup.py) +snowflake-connector-python[pandas]==4.7.1 \ + --hash=sha256:051e49157d955e5ba76577345169dd1869288ecef903dab17ec6180e1009b117 \ + --hash=sha256:052fd457fa79616d074f5b8f2e106f9f2ca36645526e4d0abddf7e3685d1bdb7 \ + --hash=sha256:0e23a5aaa1e9eaa9ba88b8867247d55c24e40f3be6367e3aa4f8633b37f56317 \ + --hash=sha256:167ba97d5b615b507fc8234266c8736ea97f91b7324db7a305a28ec3505c0edf \ + --hash=sha256:171ef3515c44e804dd86ab2b288179037fec40b3f45e1aa28fba095b764f9a6a \ + --hash=sha256:1ed85264edb186a39e641f1b744a1a1973e10d9d15a96a46a1e78ac67442c7b6 \ + --hash=sha256:22afd6de9fec8ef2cc23b231af9fc0f352351cd53da0cc11c2495272eff04ccf \ + --hash=sha256:2b3e53626c600b59c181ce18e2b253487fb7e2b0d30ac0d3cfbd3ddffd13743b \ + --hash=sha256:4b79e818d83306babff9d0803e697a008e8ada961deff55e3c5da0a9c3505d9a \ + --hash=sha256:4f2c6f40544739b43f2da262dd67a32d4ac7ca2b09cc6b7181f02b24c60d5754 \ + --hash=sha256:5011a5eb55dd80fed4198081743f75a8a56412b4623cb9cf61ce2813f600cbd9 \ + --hash=sha256:571eeeb5f3b034671c125186ecb02a2e54ebee1fad1b3af7f091773dc11f693d \ + --hash=sha256:5b7b27df86fc13afccabdb525973c0e9a730f8c25f07a4a55f95bc2ad88f8d35 \ + --hash=sha256:7703f33059daa6e30d824e5d88bd525a7feccd38412b888c6e35cf0847cc34b0 \ + --hash=sha256:7c67e735d38403109f6bc1a17022a4c950966e1c989f5e2376b9c28c5ece0fb5 \ + --hash=sha256:81a8f1ae86222e7b8561f41f688462687a435f5afc8aa34ae42f69c0c0f16a57 \ + --hash=sha256:8a5e2bd3701176521577eea8c5b384178b404bbcd73d634f86b888929c15fb8a \ + --hash=sha256:92b935a4f73651ea1306b8c3c58003e0e5a72fa3a86453087161296168c31ed3 \ + --hash=sha256:b03ef22742fee88d387f2ec3969dfb032417a2119bd366414b2c5ac9acb38a45 \ + --hash=sha256:d5821dc73d305b0804aa415a4fafee018e611cc8cf339de36a5c8c44fb57beaa \ + --hash=sha256:e582f6ac2c5fa53170ae28303d361ea6cc64e695ba528f1b6ec8529476324327 \ + --hash=sha256:f2482d1b059fcaa45b7edf8ea97b0f35179ab781fb1ce4716809d83d9f5f4d4c \ + --hash=sha256:f4e3b1222bc53b56ba4aa88224afe1ad894ee6f7389b2c0105b829d13d559aa7 \ + --hash=sha256:f5a066d2c1db940740c49bafe1c8983395bc574bc31465263898c9bd050c5c52 \ + --hash=sha256:fad8e1fb0c49eb1d93dad785ce738dac17473c6826f8cd36ee8d6f9675bceae1 \ + --hash=sha256:ff5bb51c1c21cbbb5c90b9785cc3df9d649c64db26553668b1a1ea8a461a0d5b + # via feast (pyproject.toml) sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 # via snowflake-connector-python -sphinx==8.2.3 \ - --hash=sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348 \ - --hash=sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3 - # via feast (setup.py) +sphinx==9.1.0 \ + --hash=sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb \ + --hash=sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978 + # via feast (pyproject.toml) sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 @@ -2734,187 +2915,199 @@ sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d # via sphinx -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -sqlglot==25.20.2 \ - --hash=sha256:169fe8308dd70d7bd40117b2221b62bdc7c4e2ea8eb07394b2a6146cdedf05ab \ - --hash=sha256:cdbfd7ce3f2f39f32bd7b4c23fd9e0fd261636a6b14285b914e8def25fd0a567 +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via feast (pyproject.toml) +sqlglot==30.14.0 \ + --hash=sha256:df2ef5d2b8ca814313781f4ff35bf63e58f821ef517eeddbd523c19a61fa9bb9 \ + --hash=sha256:fc768e24889d63a5e1237dea7ad305e5ffb4356a98b0bed828f89591ebcd3636 # via ibis-framework -sse-starlette==3.0.2 \ - --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ - --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +sse-starlette==3.4.6 \ + --hash=sha256:56217ab4c9a9f9c5db7b21e08732d3e7c2b807f45231ad23de0551a24c4a41f6 \ + --hash=sha256:725f8a1bd6d26ae1b2c9610c0ef5065dfdd496f3988d28adcf8c4b49dc25c627 # via mcp -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 # via + # feast (pyproject.toml) # fastapi # mcp -substrait==0.24.2 \ - --hash=sha256:743cc352e96b0927b2cd37cd5a8fdac0a96a68df9600bd104fc36aebd222a836 \ - --hash=sha256:d1d475833566fa9d67eed3273456883c0568486ccced92b524b31709d2817e19 - # via ibis-substrait -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via feast (setup.py) + # sse-starlette +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via feast (pyproject.toml) tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via feast (pyproject.toml) toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -tomli==2.3.0 \ - --hash=sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456 \ - --hash=sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845 \ - --hash=sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999 \ - --hash=sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0 \ - --hash=sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878 \ - --hash=sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf \ - --hash=sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3 \ - --hash=sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be \ - --hash=sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52 \ - --hash=sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b \ - --hash=sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67 \ - --hash=sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549 \ - --hash=sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba \ - --hash=sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22 \ - --hash=sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c \ - --hash=sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f \ - --hash=sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6 \ - --hash=sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba \ - --hash=sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45 \ - --hash=sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f \ - --hash=sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77 \ - --hash=sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606 \ - --hash=sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441 \ - --hash=sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0 \ - --hash=sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f \ - --hash=sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530 \ - --hash=sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05 \ - --hash=sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8 \ - --hash=sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005 \ - --hash=sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879 \ - --hash=sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae \ - --hash=sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc \ - --hash=sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b \ - --hash=sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b \ - --hash=sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e \ - --hash=sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf \ - --hash=sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac \ - --hash=sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8 \ - --hash=sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b \ - --hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \ - --hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \ - --hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876 + # via feast (pyproject.toml) +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 # via fastapi-mcp -tomlkit==0.13.3 \ - --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \ - --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 - # via - # poetry-dynamic-versioning - # snowflake-connector-python -toolz==0.12.1 \ - --hash=sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85 \ - --hash=sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d +tomlkit==0.15.1 \ + --hash=sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304 \ + --hash=sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97 + # via snowflake-connector-python +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # dask # ibis-framework # partd -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 - # via - # feast (setup.py) - # milvus-lite -trove-classifiers==2025.9.11.17 \ - --hash=sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd \ - --hash=sha256:931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via feast (pyproject.toml) +trove-classifiers==2026.6.1.19 \ + --hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \ + --hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745 # via hatchling -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) -typer==0.19.2 \ - --hash=sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9 \ - --hash=sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca +truststore==0.10.4 \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 + # via + # httpcore2 + # httpx2 +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) +typer==0.27.0 \ + --hash=sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5 \ + --hash=sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1 # via fastapi-mcp types-psutil==7.0.0.20250218 \ --hash=sha256:1447a30c282aafefcf8941ece854e1100eee7b0296a9d9be9977292f0269b121 \ --hash=sha256:1e642cdafe837b240295b23b1cbd4691d80b08a07d29932143cbbae30eb0db9c - # via feast (setup.py) -types-pymysql==1.1.0.20250916 \ - --hash=sha256:873eb9836bb5e3de4368cc7010ca72775f86e9692a5c7810f8c7f48da082e55b \ - --hash=sha256:98d75731795fcc06723a192786662bdfa760e1e00f22809c104fbb47bac5e29b - # via feast (setup.py) -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via feast (pyproject.toml) +types-pymysql==1.2.0.20260724 \ + --hash=sha256:71327a3fea0f680a280445f1b475c2307f68de05c2d3302a79df56af93c574cd \ + --hash=sha256:8493cf87e8df0a7b4e8c84305bc0aae69d3a8af9c6da7bfbb9a4c5c3f97d4a82 + # via feast (pyproject.toml) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiohttp # aiosignal # anyio # fastapi + # grpcio + # httpx2 # ibis-framework + # mcp + # mcp-types # mypy + # opentelemetry-api # psycopg # psycopg-pool # pydantic @@ -2925,558 +3118,519 @@ typing-extensions==4.15.0 \ # sqlalchemy # starlette # typeguard - # typer # typing-inspection typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 - # via pydantic-settings -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 - # via pandas -ujson==5.11.0 \ - --hash=sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80 \ - --hash=sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef \ - --hash=sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a \ - --hash=sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840 \ - --hash=sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53 \ - --hash=sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076 \ - --hash=sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab \ - --hash=sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d \ - --hash=sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89 \ - --hash=sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c \ - --hash=sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb \ - --hash=sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c \ - --hash=sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823 \ - --hash=sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5 \ - --hash=sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac \ - --hash=sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d \ - --hash=sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723 \ - --hash=sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6 \ - --hash=sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0 \ - --hash=sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328 \ - --hash=sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0 \ - --hash=sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04 \ - --hash=sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25 \ - --hash=sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49 \ - --hash=sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec \ - --hash=sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3 \ - --hash=sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3 \ - --hash=sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc \ - --hash=sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a \ - --hash=sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9 \ - --hash=sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302 \ - --hash=sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694 \ - --hash=sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547 \ - --hash=sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01 \ - --hash=sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02 \ - --hash=sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6 \ - --hash=sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60 \ - --hash=sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915 \ - --hash=sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835 \ - --hash=sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701 \ - --hash=sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702 \ - --hash=sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50 \ - --hash=sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6 \ - --hash=sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc \ - --hash=sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a \ - --hash=sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c \ - --hash=sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c \ - --hash=sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03 \ - --hash=sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629 \ - --hash=sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105 \ - --hash=sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844 \ - --hash=sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241 \ - --hash=sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a \ - --hash=sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26 \ - --hash=sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac \ - --hash=sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596 \ - --hash=sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9 \ - --hash=sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752 \ - --hash=sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138 \ - --hash=sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b \ - --hash=sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba \ - --hash=sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5 \ - --hash=sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018 \ - --hash=sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362 \ - --hash=sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746 \ - --hash=sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f \ - --hash=sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88 \ - --hash=sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638 \ - --hash=sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b \ - --hash=sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9 \ - --hash=sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db \ - --hash=sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f \ - --hash=sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58 \ - --hash=sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b \ - --hash=sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433 \ - --hash=sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0 \ - --hash=sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764 \ - --hash=sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c \ - --hash=sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34 \ - --hash=sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052 \ - --hash=sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba \ - --hash=sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c \ - --hash=sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc \ - --hash=sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39 - # via pymilvus -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + # via + # fastapi + # mcp + # pydantic + # pydantic-settings +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 + # via + # ibis-framework + # pandas +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via # botocore # kubernetes # requests -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi-mcp # mcp # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +vcs-versioning==2.2.3 \ + --hash=sha256:4f7873af76f5cc24e701608354f376f5eb70b317d663787ace57a7edd7a27506 \ + --hash=sha256:c21e284aa98c8269996791a19d6173baf953cf9996ef42ec3ebcbdc151e18b9b + # via setuptools-scm +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn websocket-client==1.9.0 \ --hash=sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98 \ --hash=sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef # via kubernetes -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn -wrapt==1.17.3 \ - --hash=sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56 \ - --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139 \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ - --hash=sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f \ - --hash=sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7 \ - --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc \ - --hash=sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05 \ - --hash=sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85 \ - --hash=sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c \ - --hash=sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ - --hash=sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a \ - --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5 \ - --hash=sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ - --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ - --hash=sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225 \ - --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ - --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ - --hash=sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00 \ - --hash=sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2 \ - --hash=sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a \ - --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04 \ - --hash=sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390 \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d \ - --hash=sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2 \ - --hash=sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18 \ - --hash=sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6 \ - --hash=sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311 \ - --hash=sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89 \ - --hash=sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ - --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ - --hash=sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a \ - --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2 \ - --hash=sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 \ - --hash=sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c +wrapt==2.3.0 \ + --hash=sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525 \ + --hash=sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7 \ + --hash=sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f \ + --hash=sha256:0db083387d6e75ec0be8173ecbf0e811cf60bae1cc75a815feb104167ea10d4d \ + --hash=sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f \ + --hash=sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760 \ + --hash=sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945 \ + --hash=sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3 \ + --hash=sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84 \ + --hash=sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3 \ + --hash=sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab \ + --hash=sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609 \ + --hash=sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07 \ + --hash=sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae \ + --hash=sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b \ + --hash=sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5 \ + --hash=sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a \ + --hash=sha256:3873c3c5ca9f4ef91f693602eca19d1f1e7c410338df82a4ff11d826b5896a8f \ + --hash=sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb \ + --hash=sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295 \ + --hash=sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6 \ + --hash=sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d \ + --hash=sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02 \ + --hash=sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5 \ + --hash=sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14 \ + --hash=sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23 \ + --hash=sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c \ + --hash=sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f \ + --hash=sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0 \ + --hash=sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe \ + --hash=sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd \ + --hash=sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8 \ + --hash=sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687 \ + --hash=sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109 \ + --hash=sha256:628f3ba8ec793a5b10a6cd8c6c6b7b55eb552abd1f3bd301336acb74c7a82dfe \ + --hash=sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501 \ + --hash=sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614 \ + --hash=sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab \ + --hash=sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107 \ + --hash=sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d \ + --hash=sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98 \ + --hash=sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df \ + --hash=sha256:73d0b10b64620a2cf4bc3d31775c4d9527e309a5549e4379e3bf71e8d2dc193e \ + --hash=sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3 \ + --hash=sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3 \ + --hash=sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb \ + --hash=sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2 \ + --hash=sha256:8f8a1c6472675956cece9a8f403f43c3594f1681319eed2dd56f60877397c636 \ + --hash=sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5 \ + --hash=sha256:932dced0a7b2950ed58a3325536a1dcb7b58e7330af54e8552d2e566b5328b99 \ + --hash=sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd \ + --hash=sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731 \ + --hash=sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360 \ + --hash=sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579 \ + --hash=sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84 \ + --hash=sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152 \ + --hash=sha256:abc71504669d126d91f89fc0e388c6295d8fbd2439be884f175133fda8aa403c \ + --hash=sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838 \ + --hash=sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43 \ + --hash=sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51 \ + --hash=sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0 \ + --hash=sha256:b767a9566f165dd14decf8f4194c6bb0ce3a8420cec213824e05a99400c9260a \ + --hash=sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d \ + --hash=sha256:c3b476ae63b4a3b4da681aafcb25ff3542d289fbda8b5da7caf76aaffafafdbb \ + --hash=sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98 \ + --hash=sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199 \ + --hash=sha256:c8858d8ff9822a081e3cc49ae1b3b22f0f789c14001cdac8f94564010d9c9d66 \ + --hash=sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7 \ + --hash=sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d \ + --hash=sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f \ + --hash=sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8 \ + --hash=sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f \ + --hash=sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c \ + --hash=sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4 \ + --hash=sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6 \ + --hash=sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2 \ + --hash=sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02 \ + --hash=sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35 \ + --hash=sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276 \ + --hash=sha256:e31734c5077f29f892b2565eee5106d610278151ad49fc6a9d69a647cd5730e2 \ + --hash=sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41 \ + --hash=sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8 \ + --hash=sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60 \ + --hash=sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc \ + --hash=sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570 \ + --hash=sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1 \ + --hash=sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8 \ + --hash=sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023 \ + --hash=sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944 \ + --hash=sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1 # via aiobotocore -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8 \ - --hash=sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf \ - --hash=sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890 \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ - --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ - --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748 \ - --hash=sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503 \ - --hash=sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737 \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ - --hash=sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d \ - --hash=sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b \ - --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6 \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba \ - --hash=sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9 \ - --hash=sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0 \ - --hash=sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6 \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967 \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028 \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147 \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ - --hash=sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60 \ - --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ - --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e \ - --hash=sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467 \ - --hash=sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca \ - --hash=sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054 \ - --hash=sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ - --hash=sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65 \ - --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ - --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ - --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ - --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ - --hash=sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2 \ - --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ - --hash=sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a \ - --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e \ - --hash=sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6 \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750 \ + --hash=sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621 \ + --hash=sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13 \ + --hash=sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1 \ + --hash=sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2 \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp # The following packages were excluded from the output: # milvus-lite +# pymilvus +# faiss-cpu diff --git a/sdk/python/requirements/py3.12-requirements.txt b/sdk/python/requirements/py3.12-requirements.txt index 37bb6d3ff9d..3ae800a8f74 100644 --- a/sdk/python/requirements/py3.12-requirements.txt +++ b/sdk/python/requirements/py3.12-requirements.txt @@ -1,248 +1,359 @@ # This file was autogenerated by uv via the following command: -# uv pip compile -p 3.12 --no-strip-extras setup.py --generate-hashes --output-file sdk/python/requirements/py3.12-requirements.txt -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +# uv pip compile -p 3.12 --no-strip-extras pyproject.toml --generate-hashes --output-file sdk/python/requirements/py3.12-requirements.txt +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via fastapi +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # starlette # watchfiles -attrs==25.4.0 \ - --hash=sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11 \ - --hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 - # via - # jsonschema - # referencing -bigtree==1.0.0 \ - --hash=sha256:7fbc4c069afd6c18e269a6d03e3a9f9db8edf057fc88ae69fe13de32cf1c120c \ - --hash=sha256:9eebf1acc3fe239e3fde9c97091f75f8b9e95f276164706d248c9b9195c290ce - # via feast (setup.py) -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +ast-serialize==0.6.0 \ + --hash=sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f \ + --hash=sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b \ + --hash=sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596 \ + --hash=sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e \ + --hash=sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089 \ + --hash=sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24 \ + --hash=sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a \ + --hash=sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516 \ + --hash=sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a \ + --hash=sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790 \ + --hash=sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e \ + --hash=sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a \ + --hash=sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66 \ + --hash=sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14 \ + --hash=sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c \ + --hash=sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1 \ + --hash=sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8 \ + --hash=sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b \ + --hash=sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a \ + --hash=sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc \ + --hash=sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f \ + --hash=sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264 \ + --hash=sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe \ + --hash=sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b \ + --hash=sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32 \ + --hash=sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec \ + --hash=sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081 \ + --hash=sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b \ + --hash=sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e \ + --hash=sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77 \ + --hash=sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b \ + --hash=sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7 \ + --hash=sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad \ + --hash=sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554 + # via mypy +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via feast (pyproject.toml) +bigtree==1.5.3 \ + --hash=sha256:47879c5d80f4e0d50f2c8818d3b80fc58af24b5372608223aac25e5580ac0ed8 \ + --hash=sha256:81169329ec92dfab7d8df9d61d85d8dea8b8512fbf073fbc95badc21eadcd14f + # via feast (pyproject.toml) +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via requests -charset-normalizer==3.4.4 \ - --hash=sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad \ - --hash=sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93 \ - --hash=sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394 \ - --hash=sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89 \ - --hash=sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc \ - --hash=sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86 \ - --hash=sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63 \ - --hash=sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d \ - --hash=sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f \ - --hash=sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8 \ - --hash=sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0 \ - --hash=sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505 \ - --hash=sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161 \ - --hash=sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af \ - --hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \ - --hash=sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318 \ - --hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \ - --hash=sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4 \ - --hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \ - --hash=sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3 \ - --hash=sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576 \ - --hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \ - --hash=sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1 \ - --hash=sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8 \ - --hash=sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1 \ - --hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \ - --hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \ - --hash=sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26 \ - --hash=sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88 \ - --hash=sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016 \ - --hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \ - --hash=sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf \ - --hash=sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a \ - --hash=sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc \ - --hash=sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0 \ - --hash=sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84 \ - --hash=sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db \ - --hash=sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1 \ - --hash=sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7 \ - --hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \ - --hash=sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8 \ - --hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \ - --hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \ - --hash=sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef \ - --hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \ - --hash=sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2 \ - --hash=sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0 \ - --hash=sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d \ - --hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \ - --hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \ - --hash=sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf \ - --hash=sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6 \ - --hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \ - --hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \ - --hash=sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa \ - --hash=sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381 \ - --hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \ - --hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \ - --hash=sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc \ - --hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \ - --hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \ - --hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \ - --hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \ - --hash=sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e \ - --hash=sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313 \ - --hash=sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569 \ - --hash=sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3 \ - --hash=sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d \ - --hash=sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525 \ - --hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \ - --hash=sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3 \ - --hash=sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9 \ - --hash=sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a \ - --hash=sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9 \ - --hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \ - --hash=sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25 \ - --hash=sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50 \ - --hash=sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf \ - --hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \ - --hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \ - --hash=sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac \ - --hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \ - --hash=sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815 \ - --hash=sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c \ - --hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \ - --hash=sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6 \ - --hash=sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e \ - --hash=sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4 \ - --hash=sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84 \ - --hash=sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69 \ - --hash=sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15 \ - --hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \ - --hash=sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0 \ - --hash=sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897 \ - --hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \ - --hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \ - --hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \ - --hash=sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d \ - --hash=sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074 \ - --hash=sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3 \ - --hash=sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224 \ - --hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \ - --hash=sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a \ - --hash=sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d \ - --hash=sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d \ - --hash=sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f \ - --hash=sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8 \ - --hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \ - --hash=sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966 \ - --hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9 \ - --hash=sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3 \ - --hash=sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e \ - --hash=sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via requests -click==8.3.0 \ - --hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \ - --hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4 +click==8.4.2 \ + --hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \ + --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # uvicorn -cloudpickle==3.1.1 \ - --hash=sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64 \ - --hash=sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a # via dask colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via feast (setup.py) -dask[dataframe]==2025.9.1 \ - --hash=sha256:2a8a7dc933caaea2f47745a65a6ec93d9e616e12aab53b4f03ee161d31939110 \ - --hash=sha256:718df73e1fd3d7e2b8546e0f04ce08e1ed7f9aa3da1eecd0c1f44c8b6d52f7e0 - # via feast (setup.py) + # via feast (pyproject.toml) +dask[dataframe]==2026.7.1 \ + --hash=sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7 \ + --hash=sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6 + # via feast (pyproject.toml) dill==0.3.9 \ --hash=sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a \ --hash=sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c - # via feast (setup.py) -fastapi==0.119.0 \ - --hash=sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7 \ - --hash=sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2 - # via feast (setup.py) -fsspec==2025.9.0 \ - --hash=sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19 \ - --hash=sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7 + # via feast (pyproject.toml) +fastapi==0.141.0 \ + --hash=sha256:5567705c791fa33202dabd1860df1d1650a7b1bf373378198d7b0675c9574618 \ + --hash=sha256:cae32222bdc8b2805c4d3deb4e024b381ea2a6f2de7febfe3a9a73a9c29c2e5e + # via feast (pyproject.toml) +fsspec==2026.7.0 \ + --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279 \ + --hash=sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88 # via dask -gunicorn==23.0.0 \ - --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ - --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec +gunicorn==26.0.0 \ + --hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \ + --hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via uvicorn -httptools==0.7.1 \ - --hash=sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c \ - --hash=sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad \ - --hash=sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1 \ - --hash=sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78 \ - --hash=sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb \ - --hash=sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03 \ - --hash=sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6 \ - --hash=sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df \ - --hash=sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5 \ - --hash=sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321 \ - --hash=sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346 \ - --hash=sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650 \ - --hash=sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657 \ - --hash=sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28 \ - --hash=sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023 \ - --hash=sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca \ - --hash=sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed \ - --hash=sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66 \ - --hash=sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3 \ - --hash=sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca \ - --hash=sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3 \ - --hash=sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2 \ - --hash=sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4 \ - --hash=sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70 \ - --hash=sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9 \ - --hash=sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4 \ - --hash=sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517 \ - --hash=sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a \ - --hash=sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270 \ - --hash=sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05 \ - --hash=sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e \ - --hash=sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568 \ - --hash=sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96 \ - --hash=sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf \ - --hash=sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b \ - --hash=sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a \ - --hash=sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b \ - --hash=sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c \ - --hash=sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274 \ - --hash=sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60 \ - --hash=sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5 \ - --hash=sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec \ - --hash=sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362 +httptools==0.8.0 \ + --hash=sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683 \ + --hash=sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb \ + --hash=sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b \ + --hash=sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527 \ + --hash=sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124 \ + --hash=sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca \ + --hash=sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081 \ + --hash=sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c \ + --hash=sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77 \ + --hash=sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09 \ + --hash=sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f \ + --hash=sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085 \ + --hash=sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376 \ + --hash=sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5 \ + --hash=sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62 \ + --hash=sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5 \ + --hash=sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8 \ + --hash=sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681 \ + --hash=sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999 \ + --hash=sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1 \ + --hash=sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005 \ + --hash=sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d \ + --hash=sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d \ + --hash=sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d \ + --hash=sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d \ + --hash=sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba \ + --hash=sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247 \ + --hash=sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745 \ + --hash=sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07 \ + --hash=sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b \ + --hash=sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4 \ + --hash=sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2 \ + --hash=sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557 \ + --hash=sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d \ + --hash=sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826 \ + --hash=sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b \ + --hash=sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813 \ + --hash=sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0 \ + --hash=sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150 \ + --hash=sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e \ + --hash=sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77 \ + --hash=sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568 \ + --hash=sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6 \ + --hash=sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8 \ + --hash=sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b \ + --hash=sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7 \ + --hash=sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168 \ + --hash=sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a \ + --hash=sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0 \ + --hash=sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72 # via uvicorn -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # requests jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 - # via feast (setup.py) -jsonschema==4.25.1 \ - --hash=sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63 \ - --hash=sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85 - # via feast (setup.py) -jsonschema-specifications==2025.9.1 \ - --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ - --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d - # via jsonschema + # via feast (pyproject.toml) +librt==0.13.0 \ + --hash=sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97 \ + --hash=sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b \ + --hash=sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd \ + --hash=sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a \ + --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ + --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ + --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ + --hash=sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1 \ + --hash=sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082 \ + --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ + --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ + --hash=sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c \ + --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ + --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ + --hash=sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac \ + --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ + --hash=sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2 \ + --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ + --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ + --hash=sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0 \ + --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ + --hash=sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14 \ + --hash=sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5 \ + --hash=sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005 \ + --hash=sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde \ + --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ + --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ + --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ + --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ + --hash=sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f \ + --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ + --hash=sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c \ + --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ + --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ + --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ + --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ + --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ + --hash=sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40 \ + --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ + --hash=sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348 \ + --hash=sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588 \ + --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ + --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ + --hash=sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd \ + --hash=sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94 \ + --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ + --hash=sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89 \ + --hash=sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3 \ + --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ + --hash=sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5 \ + --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ + --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ + --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ + --hash=sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176 \ + --hash=sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390 \ + --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ + --hash=sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7 \ + --hash=sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b \ + --hash=sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8 \ + --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ + --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ + --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ + --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ + --hash=sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4 \ + --hash=sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7 \ + --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ + --hash=sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f \ + --hash=sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03 \ + --hash=sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a \ + --hash=sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d \ + --hash=sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1 \ + --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ + --hash=sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9 \ + --hash=sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b \ + --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ + --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ + --hash=sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc \ + --hash=sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360 \ + --hash=sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1 \ + --hash=sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82 \ + --hash=sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547 \ + --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ + --hash=sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa \ + --hash=sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a \ + --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ + --hash=sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929 \ + --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ + --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 \ + --hash=sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79 \ + --hash=sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa \ + --hash=sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e \ + --hash=sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21 + # via mypy locket==1.0.0 \ --hash=sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632 \ --hash=sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3 @@ -338,255 +449,218 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via jinja2 -mmh3==5.2.0 \ - --hash=sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051 \ - --hash=sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762 \ - --hash=sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328 \ - --hash=sha256:10983c10f5c77683bd845751905ba535ec47409874acc759d5ce3ff7ef34398a \ - --hash=sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb \ - --hash=sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d \ - --hash=sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c \ - --hash=sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667 \ - --hash=sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646 \ - --hash=sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4 \ - --hash=sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12 \ - --hash=sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8 \ - --hash=sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765 \ - --hash=sha256:1fae471339ae1b9c641f19cf46dfe6ffd7f64b1fba7c4333b99fa3dd7f21ae0a \ - --hash=sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9 \ - --hash=sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49 \ - --hash=sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3 \ - --hash=sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd \ - --hash=sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed \ - --hash=sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115 \ - --hash=sha256:33576136c06b46a7046b6d83a3d75fbca7d25f84cec743f1ae156362608dc6d2 \ - --hash=sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647 \ - --hash=sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc \ - --hash=sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be \ - --hash=sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5 \ - --hash=sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770 \ - --hash=sha256:3c6041fd9d5fb5fcac57d5c80f521a36b74aea06b8566431c63e4ffc49aced51 \ - --hash=sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991 \ - --hash=sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0 \ - --hash=sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb \ - --hash=sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908 \ - --hash=sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89 \ - --hash=sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051 \ - --hash=sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8 \ - --hash=sha256:58477cf9ef16664d1ce2b038f87d2dc96d70fe50733a34a7f07da6c9a5e3538c \ - --hash=sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093 \ - --hash=sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513 \ - --hash=sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645 \ - --hash=sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363 \ - --hash=sha256:63830f846797187c5d3e2dae50f0848fdc86032f5bfdc58ae352f02f857e9025 \ - --hash=sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932 \ - --hash=sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7 \ - --hash=sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2 \ - --hash=sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3 \ - --hash=sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290 \ - --hash=sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3 \ - --hash=sha256:746a5ee71c6d1103d9b560fa147881b5e68fd35da56e54e03d5acefad0e7c055 \ - --hash=sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106 \ - --hash=sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1 \ - --hash=sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037 \ - --hash=sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7 \ - --hash=sha256:7bbb0df897944b5ec830f3ad883e32c5a7375370a521565f5fe24443bfb2c4f7 \ - --hash=sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b \ - --hash=sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54 \ - --hash=sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081 \ - --hash=sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc \ - --hash=sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b \ - --hash=sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96 \ - --hash=sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772 \ - --hash=sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee \ - --hash=sha256:931d47e08c9c8a67bf75d82f0ada8399eac18b03388818b62bfa42882d571d72 \ - --hash=sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65 \ - --hash=sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db \ - --hash=sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de \ - --hash=sha256:96f1e1ac44cbb42bcc406e509f70c9af42c594e72ccc7b1257f97554204445f0 \ - --hash=sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50 \ - --hash=sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504 \ - --hash=sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5 \ - --hash=sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279 \ - --hash=sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4 \ - --hash=sha256:aa6e5d31fdc5ed9e3e95f9873508615a778fe9b523d52c17fc770a3eb39ab6e4 \ - --hash=sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773 \ - --hash=sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a \ - --hash=sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e \ - --hash=sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814 \ - --hash=sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43 \ - --hash=sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9 \ - --hash=sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63 \ - --hash=sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05 \ - --hash=sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0 \ - --hash=sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28 \ - --hash=sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5 \ - --hash=sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d \ - --hash=sha256:be7d3dca9358e01dab1bad881fb2b4e8730cec58d36dd44482bc068bfcd3bc65 \ - --hash=sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094 \ - --hash=sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044 \ - --hash=sha256:c3f563e8901960e2eaa64c8e8821895818acabeb41c96f2efbb936f65dbe486c \ - --hash=sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e \ - --hash=sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c \ - --hash=sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07 \ - --hash=sha256:c677d78887244bf3095020b73c42b505b700f801c690f8eaa90ad12d3179612f \ - --hash=sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266 \ - --hash=sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22 \ - --hash=sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e \ - --hash=sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10 \ - --hash=sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8 \ - --hash=sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d \ - --hash=sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384 \ - --hash=sha256:dd966df3489ec13848d6c6303429bbace94a153f43d1ae2a55115fd36fd5ca5d \ - --hash=sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b \ - --hash=sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515 \ - --hash=sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501 \ - --hash=sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5 \ - --hash=sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078 \ - --hash=sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73 \ - --hash=sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9 \ - --hash=sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065 \ - --hash=sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d \ - --hash=sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2 \ - --hash=sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c \ - --hash=sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779 \ - --hash=sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935 \ - --hash=sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5 \ - --hash=sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b \ - --hash=sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5 \ - --hash=sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be \ - --hash=sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b \ - --hash=sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a \ - --hash=sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3 \ - --hash=sha256:fdfd3fb739f4e22746e13ad7ba0c6eedf5f454b18d11249724a388868e308ee4 \ - --hash=sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110 - # via feast (setup.py) -mypy==1.18.2 \ - --hash=sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914 \ - --hash=sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b \ - --hash=sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b \ - --hash=sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc \ - --hash=sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544 \ - --hash=sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86 \ - --hash=sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d \ - --hash=sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075 \ - --hash=sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e \ - --hash=sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac \ - --hash=sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b \ - --hash=sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34 \ - --hash=sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37 \ - --hash=sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b \ - --hash=sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428 \ - --hash=sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893 \ - --hash=sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce \ - --hash=sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8 \ - --hash=sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c \ - --hash=sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf \ - --hash=sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341 \ - --hash=sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e \ - --hash=sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba \ - --hash=sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed \ - --hash=sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f \ - --hash=sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d \ - --hash=sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8 \ - --hash=sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764 \ - --hash=sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d \ - --hash=sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0 \ - --hash=sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c \ - --hash=sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133 \ - --hash=sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986 \ - --hash=sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6 \ - --hash=sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074 \ - --hash=sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb \ - --hash=sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e \ - --hash=sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66 +mmh3==5.2.1 \ + --hash=sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d \ + --hash=sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082 \ + --hash=sha256:08043f7cb1fb9467c3fbbbaea7896986e7fbc81f4d3fd9289a73d9110ab6207a \ + --hash=sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00 \ + --hash=sha256:0bbc17250b10d3466875a40a52520a6bac3c02334ca709207648abd3c223ed5c \ + --hash=sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1 \ + --hash=sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b \ + --hash=sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc \ + --hash=sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104 \ + --hash=sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8 \ + --hash=sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9 \ + --hash=sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e \ + --hash=sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a \ + --hash=sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44 \ + --hash=sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825 \ + --hash=sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4 \ + --hash=sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb \ + --hash=sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82 \ + --hash=sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f \ + --hash=sha256:2d5d542bf2abd0fd0361e8017d03f7cb5786214ceb4a40eef1539d6585d93386 \ + --hash=sha256:30e4d2084df019880d55f6f7bea35328d9b464ebee090baa372c096dc77556fb \ + --hash=sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d \ + --hash=sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8 \ + --hash=sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593 \ + --hash=sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00 \ + --hash=sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a \ + --hash=sha256:3cb61db880ec11e984348227b333259994c2c85caa775eb7875decb3768db890 \ + --hash=sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5 \ + --hash=sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb \ + --hash=sha256:41105377f6282e8297f182e393a79cfffd521dde37ace52b106373bdcd9ca5cb \ + --hash=sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1 \ + --hash=sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b \ + --hash=sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74 \ + --hash=sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00 \ + --hash=sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d \ + --hash=sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b \ + --hash=sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba \ + --hash=sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b \ + --hash=sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4 \ + --hash=sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac \ + --hash=sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a \ + --hash=sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a \ + --hash=sha256:5d87a3584093e1a89987e3d36d82c98d9621b2cb944e22a420aa1401e096758f \ + --hash=sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18 \ + --hash=sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16 \ + --hash=sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf \ + --hash=sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000 \ + --hash=sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912 \ + --hash=sha256:6f01f044112d43a20be2f13a11683666d87151542ad627fe41a18b9791d2802f \ + --hash=sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5 \ + --hash=sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e \ + --hash=sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7 \ + --hash=sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025 \ + --hash=sha256:7501e9be34cb21e72fcfe672aafd0eee65c16ba2afa9dcb5500a587d3a0580f0 \ + --hash=sha256:76219cd1eefb9bf4af7856e3ae563d15158efa145c0aab01e9933051a1954045 \ + --hash=sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c \ + --hash=sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd \ + --hash=sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d \ + --hash=sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f \ + --hash=sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0 \ + --hash=sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15 \ + --hash=sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7 \ + --hash=sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006 \ + --hash=sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211 \ + --hash=sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc \ + --hash=sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503 \ + --hash=sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb \ + --hash=sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d \ + --hash=sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0 \ + --hash=sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38 \ + --hash=sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617 \ + --hash=sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f \ + --hash=sha256:add7ac388d1e0bf57259afbcf9ed05621a3bf11ce5ee337e7536f1e1aaf056b0 \ + --hash=sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b \ + --hash=sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166 \ + --hash=sha256:b4cce60d0223074803c9dbe0721ad3fa51dafe7d462fee4b656a1aa01ee07518 \ + --hash=sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728 \ + --hash=sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad \ + --hash=sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc \ + --hash=sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8 \ + --hash=sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03 \ + --hash=sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f \ + --hash=sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2 \ + --hash=sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229 \ + --hash=sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe \ + --hash=sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966 \ + --hash=sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4 \ + --hash=sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6 \ + --hash=sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1 \ + --hash=sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227 \ + --hash=sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450 \ + --hash=sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d \ + --hash=sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b \ + --hash=sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997 \ + --hash=sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6 \ + --hash=sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e \ + --hash=sha256:e8b5378de2b139c3a830f0209c1e91f7705919a4b3e563a10955104f5097a70a \ + --hash=sha256:e904f2417f0d6f6d514f3f8b836416c360f306ddaee1f84de8eef1e722d212e5 \ + --hash=sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7 \ + --hash=sha256:f1fbb0a99125b1287c6d9747f937dc66621426836d1a2d50d05aecfc81911b57 \ + --hash=sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105 \ + --hash=sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2 \ + --hash=sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312 \ + --hash=sha256:fb9d44c25244e11c8be3f12c938ca8ba8404620ef8092245d2093c6ab3df260f \ + --hash=sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2 \ + --hash=sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a \ + --hash=sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b + # via feast (pyproject.toml) +mypy==2.3.0 \ + --hash=sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491 \ + --hash=sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762 \ + --hash=sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5 \ + --hash=sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654 \ + --hash=sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd \ + --hash=sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe \ + --hash=sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc \ + --hash=sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3 \ + --hash=sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5 \ + --hash=sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329 \ + --hash=sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b \ + --hash=sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3 \ + --hash=sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7 \ + --hash=sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805 \ + --hash=sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f \ + --hash=sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e \ + --hash=sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7 \ + --hash=sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494 \ + --hash=sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373 \ + --hash=sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88 \ + --hash=sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee \ + --hash=sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2 \ + --hash=sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757 \ + --hash=sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a \ + --hash=sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b \ + --hash=sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461 \ + --hash=sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97 \ + --hash=sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4 \ + --hash=sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117 \ + --hash=sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36 \ + --hash=sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac \ + --hash=sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5 \ + --hash=sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556 \ + --hash=sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c \ + --hash=sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88 \ + --hash=sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568 \ + --hash=sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595 \ + --hash=sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f \ + --hash=sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1 \ + --hash=sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef \ + --hash=sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6 \ + --hash=sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db \ + --hash=sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff \ + --hash=sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60 \ + --hash=sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c # via sqlalchemy mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy -numpy==2.3.3 \ - --hash=sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b \ - --hash=sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54 \ - --hash=sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d \ - --hash=sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5 \ - --hash=sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970 \ - --hash=sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8 \ - --hash=sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e \ - --hash=sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3 \ - --hash=sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc \ - --hash=sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e \ - --hash=sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe \ - --hash=sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5 \ - --hash=sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b \ - --hash=sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652 \ - --hash=sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30 \ - --hash=sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d \ - --hash=sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb \ - --hash=sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7 \ - --hash=sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a \ - --hash=sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf \ - --hash=sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 \ - --hash=sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8 \ - --hash=sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19 \ - --hash=sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7 \ - --hash=sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1 \ - --hash=sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b \ - --hash=sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa \ - --hash=sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d \ - --hash=sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc \ - --hash=sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e \ - --hash=sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86 \ - --hash=sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097 \ - --hash=sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a \ - --hash=sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7 \ - --hash=sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30 \ - --hash=sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c \ - --hash=sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8 \ - --hash=sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3 \ - --hash=sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe \ - --hash=sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00 \ - --hash=sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6 \ - --hash=sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe \ - --hash=sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd \ - --hash=sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48 \ - --hash=sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae \ - --hash=sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f \ - --hash=sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a \ - --hash=sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6 \ - --hash=sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5 \ - --hash=sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0 \ - --hash=sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25 \ - --hash=sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593 \ - --hash=sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f \ - --hash=sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea \ - --hash=sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421 \ - --hash=sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf \ - --hash=sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7 \ - --hash=sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7 \ - --hash=sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf \ - --hash=sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20 \ - --hash=sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0 \ - --hash=sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e \ - --hash=sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150 \ - --hash=sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029 \ - --hash=sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db \ - --hash=sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021 \ - --hash=sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569 \ - --hash=sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57 \ - --hash=sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa \ - --hash=sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea \ - --hash=sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc \ - --hash=sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf \ - --hash=sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125 \ - --hash=sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf +numpy==2.5.1 \ + --hash=sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2 \ + --hash=sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d \ + --hash=sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1 \ + --hash=sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b \ + --hash=sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd \ + --hash=sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077 \ + --hash=sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a \ + --hash=sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e \ + --hash=sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277 \ + --hash=sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6 \ + --hash=sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75 \ + --hash=sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7 \ + --hash=sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1 \ + --hash=sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9 \ + --hash=sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21 \ + --hash=sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca \ + --hash=sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0 \ + --hash=sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb \ + --hash=sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d \ + --hash=sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75 \ + --hash=sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74 \ + --hash=sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf \ + --hash=sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0 \ + --hash=sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8 \ + --hash=sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af \ + --hash=sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a \ + --hash=sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4 \ + --hash=sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22 \ + --hash=sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3 \ + --hash=sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1 \ + --hash=sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b \ + --hash=sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1 \ + --hash=sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373 \ + --hash=sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95 \ + --hash=sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6 \ + --hash=sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09 \ + --hash=sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9 \ + --hash=sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438 \ + --hash=sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2 \ + --hash=sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7 \ + --hash=sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace \ + --hash=sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3 \ + --hash=sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2 \ + --hash=sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # pandas -packaging==25.0 \ - --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ - --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # dask # gunicorn @@ -647,216 +721,247 @@ pandas==2.3.3 \ --hash=sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c \ --hash=sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee # via - # feast (setup.py) + # feast (pyproject.toml) # dask partd==1.4.2 \ --hash=sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f \ --hash=sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c # via dask -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via mypy -prometheus-client==0.23.1 \ - --hash=sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce \ - --hash=sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99 - # via feast (setup.py) -protobuf==6.32.1 \ - --hash=sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346 \ - --hash=sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4 \ - --hash=sha256:68ff170bac18c8178f130d1ccb94700cf72852298e016a2443bdb9502279e5f1 \ - --hash=sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085 \ - --hash=sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1 \ - --hash=sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710 \ - --hash=sha256:d0975d0b2f3e6957111aa3935d08a0eb7e006b1505d825f862a1fffc8348e122 \ - --hash=sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281 \ - --hash=sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d - # via feast (setup.py) -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 - # via feast (setup.py) -pyarrow==21.0.0 \ - --hash=sha256:067c66ca29aaedae08218569a114e413b26e742171f526e828e1064fcdec13f4 \ - --hash=sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623 \ - --hash=sha256:0c4e75d13eb76295a49e0ea056eb18dbd87d81450bfeb8afa19a7e5a75ae2ad7 \ - --hash=sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636 \ - --hash=sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7 \ - --hash=sha256:203003786c9fd253ebcafa44b03c06983c9c8d06c3145e37f1b76a1f317aeae1 \ - --hash=sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10 \ - --hash=sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51 \ - --hash=sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd \ - --hash=sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8 \ - --hash=sha256:3b4d97e297741796fead24867a8dabf86c87e4584ccc03167e4a811f50fdf74d \ - --hash=sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569 \ - --hash=sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e \ - --hash=sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc \ - --hash=sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6 \ - --hash=sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c \ - --hash=sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82 \ - --hash=sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79 \ - --hash=sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6 \ - --hash=sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10 \ - --hash=sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61 \ - --hash=sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d \ - --hash=sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb \ - --hash=sha256:898afce396b80fdda05e3086b4256f8677c671f7b1d27a6976fa011d3fd0a86e \ - --hash=sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e \ - --hash=sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594 \ - --hash=sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634 \ - --hash=sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da \ - --hash=sha256:a7f6524e3747e35f80744537c78e7302cd41deee8baa668d56d55f77d9c464b3 \ - --hash=sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876 \ - --hash=sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e \ - --hash=sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a \ - --hash=sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b \ - --hash=sha256:cdc4c17afda4dab2a9c0b79148a43a7f4e1094916b3e18d8975bfd6d6d52241f \ - --hash=sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18 \ - --hash=sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe \ - --hash=sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99 \ - --hash=sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26 \ - --hash=sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d \ - --hash=sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a \ - --hash=sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd \ - --hash=sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503 \ - --hash=sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79 +prometheus-client==0.24.1 \ + --hash=sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055 \ + --hash=sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9 + # via feast (pyproject.toml) +protobuf==7.35.1 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a + # via feast (pyproject.toml) +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 + # via feast (pyproject.toml) +pyarrow==25.0.0 \ + --hash=sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be \ + --hash=sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc \ + --hash=sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec \ + --hash=sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849 \ + --hash=sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e \ + --hash=sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8 \ + --hash=sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d \ + --hash=sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537 \ + --hash=sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062 \ + --hash=sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e \ + --hash=sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104 \ + --hash=sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517 \ + --hash=sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e \ + --hash=sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5 \ + --hash=sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887 \ + --hash=sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1 \ + --hash=sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b \ + --hash=sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be \ + --hash=sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778 \ + --hash=sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e \ + --hash=sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194 \ + --hash=sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2 \ + --hash=sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9 \ + --hash=sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b \ + --hash=sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18 \ + --hash=sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7 \ + --hash=sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc \ + --hash=sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62 \ + --hash=sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0 \ + --hash=sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f \ + --hash=sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e \ + --hash=sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862 \ + --hash=sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50 \ + --hash=sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe \ + --hash=sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3 \ + --hash=sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a \ + --hash=sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4 \ + --hash=sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af \ + --hash=sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b \ + --hash=sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0 \ + --hash=sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f \ + --hash=sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec \ + --hash=sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f # via - # feast (setup.py) + # feast (pyproject.toml) # dask -pydantic==2.10.6 \ - --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \ - --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236 +pydantic==2.13.4 \ + --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ + --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via - # feast (setup.py) + # feast (pyproject.toml) # fastapi -pydantic-core==2.27.2 \ - --hash=sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278 \ - --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \ - --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \ - --hash=sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f \ - --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ - --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \ - --hash=sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54 \ - --hash=sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630 \ - --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \ - --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \ - --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ - --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \ - --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \ - --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \ - --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ - --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \ - --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ - --hash=sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd \ - --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \ - --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \ - --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \ - --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ - --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \ - --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \ - --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \ - --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \ - --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ - --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \ - --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \ - --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \ - --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \ - --hash=sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf \ - --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \ - --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \ - --hash=sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76 \ - --hash=sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362 \ - --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \ - --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ - --hash=sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320 \ - --hash=sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118 \ - --hash=sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96 \ - --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \ - --hash=sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046 \ - --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \ - --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ - --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \ - --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \ - --hash=sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67 \ - --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \ - --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \ - --hash=sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35 \ - --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \ - --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \ - --hash=sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b \ - --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \ - --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \ - --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ - --hash=sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145 \ - --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \ - --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \ - --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \ - --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \ - --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ - --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ - --hash=sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5 \ - --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ - --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \ - --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \ - --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \ - --hash=sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da \ - --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \ - --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \ - --hash=sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993 \ - --hash=sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656 \ - --hash=sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4 \ - --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \ - --hash=sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb \ - --hash=sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d \ - --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ - --hash=sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e \ - --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \ - --hash=sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc \ - --hash=sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a \ - --hash=sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9 \ - --hash=sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506 \ - --hash=sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b \ - --hash=sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1 \ - --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \ - --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \ - --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ - --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \ - --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \ - --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \ - --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \ - --hash=sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308 \ - --hash=sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2 \ - --hash=sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228 \ - --hash=sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b \ - --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9 \ - --hash=sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad +pydantic-core==2.46.4 \ + --hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \ + --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ + --hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \ + --hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \ + --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ + --hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \ + --hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \ + --hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \ + --hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \ + --hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \ + --hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \ + --hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \ + --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ + --hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \ + --hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \ + --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ + --hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \ + --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ + --hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \ + --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ + --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ + --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ + --hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \ + --hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \ + --hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \ + --hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \ + --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ + --hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \ + --hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \ + --hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \ + --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ + --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ + --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ + --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ + --hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \ + --hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \ + --hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \ + --hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \ + --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ + --hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \ + --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ + --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ + --hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \ + --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ + --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ + --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ + --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ + --hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \ + --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ + --hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \ + --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ + --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ + --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ + --hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \ + --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ + --hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \ + --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ + --hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \ + --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ + --hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \ + --hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \ + --hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \ + --hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \ + --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ + --hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \ + --hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \ + --hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \ + --hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \ + --hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \ + --hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \ + --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ + --hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \ + --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ + --hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \ + --hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \ + --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ + --hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \ + --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ + --hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \ + --hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \ + --hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \ + --hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \ + --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ + --hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \ + --hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \ + --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ + --hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \ + --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ + --hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \ + --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ + --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ + --hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \ + --hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \ + --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ + --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ + --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ + --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ + --hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \ + --hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \ + --hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \ + --hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \ + --hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \ + --hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \ + --hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \ + --hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \ + --hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \ + --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ + --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ + --hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \ + --hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \ + --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ + --hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \ + --hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \ + --hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \ + --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ + --hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \ + --hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \ + --hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \ + --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \ + --hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae # via pydantic -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b - # via feast (setup.py) -pyjwt==2.10.1 \ - --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \ - --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb - # via feast (setup.py) +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via feast (pyproject.toml) +pyjwt==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 + # via feast (pyproject.toml) python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via pandas -python-dotenv==1.1.1 \ - --hash=sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc \ - --hash=sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via uvicorn -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +pytz==2026.3.post1 \ + --hash=sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d \ + --hash=sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815 # via pandas pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ @@ -933,521 +1038,414 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via - # feast (setup.py) + # feast (pyproject.toml) # dask # uvicorn -referencing==0.37.0 \ - --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ - --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 - # via - # jsonschema - # jsonschema-specifications -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf - # via feast (setup.py) -rpds-py==0.27.1 \ - --hash=sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400 \ - --hash=sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1 \ - --hash=sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ - --hash=sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059 \ - --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ - --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ - --hash=sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52 \ - --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d \ - --hash=sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ - --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ - --hash=sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf \ - --hash=sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c \ - --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746 \ - --hash=sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39 \ - --hash=sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ - --hash=sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66 \ - --hash=sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b \ - --hash=sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc \ - --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013 \ - --hash=sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1 \ - --hash=sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ - --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c \ - --hash=sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195 \ - --hash=sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859 \ - --hash=sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a \ - --hash=sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125 \ - --hash=sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3 \ - --hash=sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948 \ - --hash=sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef \ - --hash=sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde \ - --hash=sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3 \ - --hash=sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab \ - --hash=sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be \ - --hash=sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6 \ - --hash=sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8 \ - --hash=sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad \ - --hash=sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4 \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8 \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab \ - --hash=sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808 \ - --hash=sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5 \ - --hash=sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3 \ - --hash=sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4 \ - --hash=sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ - --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ - --hash=sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52 \ - --hash=sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9 \ - --hash=sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1 \ - --hash=sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f \ - --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475 \ - --hash=sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e \ - --hash=sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657 \ - --hash=sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8 \ - --hash=sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e \ - --hash=sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819 \ - --hash=sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527 \ - --hash=sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed \ - --hash=sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8 \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ - --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8 \ - --hash=sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec \ - --hash=sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9 \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636 \ - --hash=sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec \ - --hash=sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21 - # via - # jsonschema - # referencing +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via feast (pyproject.toml) six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 # via python-dateutil -sniffio==1.3.1 \ - --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ - --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc - # via anyio -sqlalchemy[mypy]==2.0.44 \ - --hash=sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7 \ - --hash=sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22 \ - --hash=sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45 \ - --hash=sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd \ - --hash=sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1 \ - --hash=sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0 \ - --hash=sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d \ - --hash=sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749 \ - --hash=sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05 \ - --hash=sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e \ - --hash=sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40 \ - --hash=sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3 \ - --hash=sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3 \ - --hash=sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976 \ - --hash=sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726 \ - --hash=sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d \ - --hash=sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013 \ - --hash=sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667 \ - --hash=sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165 \ - --hash=sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74 \ - --hash=sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399 \ - --hash=sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183 \ - --hash=sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a \ - --hash=sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e \ - --hash=sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9 \ - --hash=sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632 \ - --hash=sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822 \ - --hash=sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f \ - --hash=sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985 \ - --hash=sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26 \ - --hash=sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29 \ - --hash=sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b \ - --hash=sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250 \ - --hash=sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7 \ - --hash=sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5 \ - --hash=sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce \ - --hash=sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e \ - --hash=sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4 \ - --hash=sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6 \ - --hash=sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5 \ - --hash=sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44 \ - --hash=sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100 \ - --hash=sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c \ - --hash=sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e \ - --hash=sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e \ - --hash=sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2 \ - --hash=sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2 \ - --hash=sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606 \ - --hash=sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9 \ - --hash=sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa \ - --hash=sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1 \ - --hash=sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e \ - --hash=sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73 \ - --hash=sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0 \ - --hash=sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4 \ - --hash=sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e \ - --hash=sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1 - # via feast (setup.py) -starlette==0.48.0 \ - --hash=sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659 \ - --hash=sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46 - # via fastapi -tabulate==0.9.0 \ - --hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \ - --hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f - # via feast (setup.py) +sqlalchemy[mypy]==2.0.51 \ + --hash=sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23 \ + --hash=sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1 \ + --hash=sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8 \ + --hash=sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72 \ + --hash=sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0 \ + --hash=sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5 \ + --hash=sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e \ + --hash=sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85 \ + --hash=sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d \ + --hash=sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2 \ + --hash=sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba \ + --hash=sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652 \ + --hash=sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f \ + --hash=sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9 \ + --hash=sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84 \ + --hash=sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46 \ + --hash=sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7 \ + --hash=sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080 \ + --hash=sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d \ + --hash=sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d \ + --hash=sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54 \ + --hash=sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd \ + --hash=sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195 \ + --hash=sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc \ + --hash=sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e \ + --hash=sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825 \ + --hash=sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8 \ + --hash=sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522 \ + --hash=sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491 \ + --hash=sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400 \ + --hash=sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a \ + --hash=sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07 \ + --hash=sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7 \ + --hash=sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a \ + --hash=sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9 \ + --hash=sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7 \ + --hash=sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499 \ + --hash=sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5 \ + --hash=sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0 \ + --hash=sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604 \ + --hash=sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265 \ + --hash=sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904 \ + --hash=sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a \ + --hash=sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64 \ + --hash=sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d \ + --hash=sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032 \ + --hash=sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b \ + --hash=sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5 \ + --hash=sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2 \ + --hash=sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d \ + --hash=sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389 \ + --hash=sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080 \ + --hash=sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37 \ + --hash=sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00 \ + --hash=sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86 \ + --hash=sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260 \ + --hash=sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de \ + --hash=sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1 + # via feast (pyproject.toml) +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 + # via + # feast (pyproject.toml) + # fastapi +tabulate==0.10.0 \ + --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ + --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 + # via feast (pyproject.toml) tenacity==8.5.0 \ --hash=sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78 \ --hash=sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687 - # via feast (setup.py) + # via feast (pyproject.toml) toml==0.10.2 \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f - # via feast (setup.py) -toolz==1.0.0 \ - --hash=sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236 \ - --hash=sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02 + # via feast (pyproject.toml) +toolz==1.1.0 \ + --hash=sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8 \ + --hash=sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b # via # dask # partd -tqdm==4.67.1 \ - --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ - --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 - # via feast (setup.py) -typeguard==4.4.4 \ - --hash=sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74 \ - --hash=sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e - # via feast (setup.py) -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +tqdm==4.70.0 \ + --hash=sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220 \ + --hash=sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953 + # via feast (pyproject.toml) +typeguard==4.6.0 \ + --hash=sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7 \ + --hash=sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21 + # via feast (pyproject.toml) +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via # anyio # fastapi # mypy # pydantic # pydantic-core - # referencing # sqlalchemy # starlette # typeguard -tzdata==2025.2 \ - --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \ - --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9 + # typing-inspection +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 + # via + # fastapi + # pydantic +tzdata==2026.3 \ + --hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \ + --hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931 # via pandas -urllib3==2.5.0 \ - --hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \ - --hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via requests -uvicorn[standard]==0.34.0 \ - --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ - --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 +uvicorn[standard]==0.52.0 \ + --hash=sha256:3d887809810b89ed33501bcf0a9aba469b06ecd608158efce04bd6b48d8c9b08 \ + --hash=sha256:ca8876ad6c1983f394157c168b39d52f6dd56dabf5602fa0982751cffc2293ae # via - # feast (setup.py) + # feast (pyproject.toml) # uvicorn-worker -uvicorn-worker==0.3.0 \ - --hash=sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b \ - --hash=sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52 - # via feast (setup.py) -uvloop==0.21.0 \ - --hash=sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0 \ - --hash=sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f \ - --hash=sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc \ - --hash=sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414 \ - --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ - --hash=sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d \ - --hash=sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd \ - --hash=sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff \ - --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ - --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ - --hash=sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d \ - --hash=sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a \ - --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ - --hash=sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2 \ - --hash=sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0 \ - --hash=sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6 \ - --hash=sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c \ - --hash=sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af \ - --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ - --hash=sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb \ - --hash=sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75 \ - --hash=sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb \ - --hash=sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553 \ - --hash=sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e \ - --hash=sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6 \ - --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ - --hash=sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206 \ - --hash=sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc \ - --hash=sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281 \ - --hash=sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b \ - --hash=sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8 \ - --hash=sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79 \ - --hash=sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f \ - --hash=sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe \ - --hash=sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26 \ - --hash=sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816 \ - --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 +uvicorn-worker==0.4.0 \ + --hash=sha256:8ee5306070d8f38dce124adce488c3c0b50f20cf0c0222b12c66188da7214493 \ + --hash=sha256:e2ed952cef976f5e9e429d7269640bbcafbd36c80aa80f1003c8c77a6797abde + # via feast (pyproject.toml) +uvloop==0.22.1 \ + --hash=sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772 \ + --hash=sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e \ + --hash=sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743 \ + --hash=sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54 \ + --hash=sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec \ + --hash=sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659 \ + --hash=sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8 \ + --hash=sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad \ + --hash=sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7 \ + --hash=sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35 \ + --hash=sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289 \ + --hash=sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142 \ + --hash=sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77 \ + --hash=sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733 \ + --hash=sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd \ + --hash=sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193 \ + --hash=sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74 \ + --hash=sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0 \ + --hash=sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6 \ + --hash=sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473 \ + --hash=sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21 \ + --hash=sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242 \ + --hash=sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705 \ + --hash=sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702 \ + --hash=sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6 \ + --hash=sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f \ + --hash=sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e \ + --hash=sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d \ + --hash=sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370 \ + --hash=sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4 \ + --hash=sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792 \ + --hash=sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa \ + --hash=sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079 \ + --hash=sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2 \ + --hash=sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86 \ + --hash=sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6 \ + --hash=sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4 \ + --hash=sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3 \ + --hash=sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21 \ + --hash=sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c \ + --hash=sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e \ + --hash=sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25 \ + --hash=sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820 \ + --hash=sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9 \ + --hash=sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88 \ + --hash=sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2 \ + --hash=sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c \ + --hash=sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c \ + --hash=sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42 # via uvicorn -watchfiles==1.1.0 \ - --hash=sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a \ - --hash=sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f \ - --hash=sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6 \ - --hash=sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3 \ - --hash=sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7 \ - --hash=sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a \ - --hash=sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259 \ - --hash=sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297 \ - --hash=sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1 \ - --hash=sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c \ - --hash=sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a \ - --hash=sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b \ - --hash=sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb \ - --hash=sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc \ - --hash=sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b \ - --hash=sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339 \ - --hash=sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9 \ - --hash=sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df \ - --hash=sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb \ - --hash=sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4 \ - --hash=sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5 \ - --hash=sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc \ - --hash=sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c \ - --hash=sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8 \ - --hash=sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433 \ - --hash=sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12 \ - --hash=sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30 \ - --hash=sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0 \ - --hash=sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86 \ - --hash=sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c \ - --hash=sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5 \ - --hash=sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866 \ - --hash=sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb \ - --hash=sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2 \ - --hash=sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e \ - --hash=sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575 \ - --hash=sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f \ - --hash=sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a \ - --hash=sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f \ - --hash=sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d \ - --hash=sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277 \ - --hash=sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9 \ - --hash=sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf \ - --hash=sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92 \ - --hash=sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72 \ - --hash=sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b \ - --hash=sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68 \ - --hash=sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa \ - --hash=sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc \ - --hash=sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b \ - --hash=sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd \ - --hash=sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4 \ - --hash=sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7 \ - --hash=sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792 \ - --hash=sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9 \ - --hash=sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0 \ - --hash=sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297 \ - --hash=sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef \ - --hash=sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179 \ - --hash=sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d \ - --hash=sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea \ - --hash=sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5 \ - --hash=sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee \ - --hash=sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82 \ - --hash=sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011 \ - --hash=sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e \ - --hash=sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4 \ - --hash=sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf \ - --hash=sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db \ - --hash=sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20 \ - --hash=sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4 \ - --hash=sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575 \ - --hash=sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa \ - --hash=sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c \ - --hash=sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f \ - --hash=sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f \ - --hash=sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267 \ - --hash=sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018 \ - --hash=sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2 \ - --hash=sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d \ - --hash=sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd \ - --hash=sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47 \ - --hash=sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb \ - --hash=sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29 \ - --hash=sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147 \ - --hash=sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8 \ - --hash=sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670 \ - --hash=sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587 \ - --hash=sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97 \ - --hash=sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c \ - --hash=sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5 \ - --hash=sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e \ - --hash=sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e \ - --hash=sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6 \ - --hash=sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc \ - --hash=sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e \ - --hash=sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8 \ - --hash=sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895 \ - --hash=sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7 \ - --hash=sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432 \ - --hash=sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc \ - --hash=sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633 \ - --hash=sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f \ - --hash=sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77 \ - --hash=sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12 \ - --hash=sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f +watchfiles==1.2.0 \ + --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \ + --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \ + --hash=sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551 \ + --hash=sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d \ + --hash=sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7 \ + --hash=sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db \ + --hash=sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69 \ + --hash=sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242 \ + --hash=sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925 \ + --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \ + --hash=sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5 \ + --hash=sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5 \ + --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \ + --hash=sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19 \ + --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \ + --hash=sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e \ + --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \ + --hash=sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba \ + --hash=sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df \ + --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \ + --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \ + --hash=sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65 \ + --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \ + --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \ + --hash=sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30 \ + --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \ + --hash=sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374 \ + --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \ + --hash=sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33 \ + --hash=sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831 \ + --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \ + --hash=sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2 \ + --hash=sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b \ + --hash=sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f \ + --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \ + --hash=sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579 \ + --hash=sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5 \ + --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \ + --hash=sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7 \ + --hash=sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666 \ + --hash=sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5 \ + --hash=sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201 \ + --hash=sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103 \ + --hash=sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6 \ + --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \ + --hash=sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1 \ + --hash=sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631 \ + --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \ + --hash=sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d \ + --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \ + --hash=sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2 \ + --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \ + --hash=sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a \ + --hash=sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1 \ + --hash=sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b \ + --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \ + --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \ + --hash=sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377 \ + --hash=sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8 \ + --hash=sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add \ + --hash=sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281 \ + --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \ + --hash=sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994 \ + --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \ + --hash=sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e \ + --hash=sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0 \ + --hash=sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28 \ + --hash=sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7 \ + --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \ + --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \ + --hash=sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07 \ + --hash=sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb \ + --hash=sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4 \ + --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \ + --hash=sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e \ + --hash=sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4 \ + --hash=sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9 \ + --hash=sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06 \ + --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \ + --hash=sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7 \ + --hash=sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4 \ + --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \ + --hash=sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3 \ + --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \ + --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \ + --hash=sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488 \ + --hash=sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717 \ + --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \ + --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \ + --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \ + --hash=sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b \ + --hash=sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2 \ + --hash=sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22 \ + --hash=sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6 \ + --hash=sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e \ + --hash=sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310 \ + --hash=sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165 \ + --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \ + --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \ + --hash=sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8 \ + --hash=sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7 \ + --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \ + --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \ + --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \ + --hash=sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4 \ + --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \ + --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4 # via uvicorn -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ - --hash=sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3 \ - --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e \ - --hash=sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1 \ - --hash=sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256 \ - --hash=sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85 \ - --hash=sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880 \ - --hash=sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ - --hash=sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065 \ - --hash=sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed \ - --hash=sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41 \ - --hash=sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411 \ - --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ - --hash=sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb \ - --hash=sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e \ - --hash=sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee \ - --hash=sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f \ - --hash=sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf \ - --hash=sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf \ - --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ - --hash=sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d \ - --hash=sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5 \ - --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792 \ - --hash=sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57 \ - --hash=sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9 \ - --hash=sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ - --hash=sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475 \ - --hash=sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940 \ - --hash=sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431 \ - --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413 \ - --hash=sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8 \ - --hash=sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b \ - --hash=sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a \ - --hash=sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054 \ - --hash=sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb \ - --hash=sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205 \ - --hash=sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04 \ - --hash=sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4 \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122 \ - --hash=sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b \ - --hash=sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905 \ - --hash=sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b \ - --hash=sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562 \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9 \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838 \ + --hash=sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b \ + --hash=sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d \ + --hash=sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 \ + --hash=sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a # via uvicorn diff --git a/sdk/python/tests/README.md b/sdk/python/tests/README.md index 5b930129026..418e2131928 100644 --- a/sdk/python/tests/README.md +++ b/sdk/python/tests/README.md @@ -48,19 +48,21 @@ $ tree │ ├── test_offline_write.py │ ├── test_push_features_to_offline_store.py │ ├── test_s3_custom_endpoint.py -│ └── test_universal_historical_retrieval.py +│ ├── test_universal_historical_retrieval.py +│ └── test_universal_types.py +├── cli +│ └── test_universal_cli.py ├── online_store │ ├── test_online_retrieval.py │ ├── test_push_features_to_online_store.py -│ └── test_universal_online.py +│ ├── test_universal_online.py +│ └── test_universal_online_types.py └── registration ├── test_feature_store.py ├── test_inference.py ├── test_registry.py ├── test_sql_registry.py - ├── test_universal_cli.py - ├── test_universal_odfv_feature_inference.py - └── test_universal_types.py + └── test_universal_odfv_feature_inference.py ``` diff --git a/sdk/python/tests/benchmarks/test_bigquery_partition_pruning_benchmark.py b/sdk/python/tests/benchmarks/test_bigquery_partition_pruning_benchmark.py new file mode 100644 index 00000000000..00123e79664 --- /dev/null +++ b/sdk/python/tests/benchmarks/test_bigquery_partition_pruning_benchmark.py @@ -0,0 +1,135 @@ +import os +from datetime import datetime, timedelta, timezone +from unittest.mock import Mock, patch + +import pytest + +from feast.infra.offline_stores.bigquery import ( + BigQueryOfflineStore, + BigQueryOfflineStoreConfig, +) +from feast.infra.offline_stores.bigquery_source import BigQuerySource +from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.repo_config import RepoConfig + +__doc__ = """ +Environment variables: + FEAST_BQ_BENCH_PROJECT: BigQuery project to run dry-run queries in + FEAST_BQ_BENCH_TABLE: BigQuery table in Feast format: project:dataset.table + FEAST_BQ_BENCH_TIMESTAMP_FIELD: event timestamp column name used by Feast + FEAST_BQ_BENCH_PARTITION_COLUMN: partition column to prune (e.g. _PARTITIONDATE) + FEAST_BQ_BENCH_LOCATION: optional BigQuery location + FEAST_BQ_BENCH_START: optional ISO datetime (e.g. 2026-01-01T00:00:00+00:00) + FEAST_BQ_BENCH_END: optional ISO datetime + FEAST_BQ_BENCH_REQUIRE_REDUCTION: if truthy, requires strict byte reduction +""" + + +def _required_env(name: str) -> str: + val = os.environ.get(name) + if not val: + pytest.skip(f"Missing env var {name}") + return val + + +def _optional_iso_datetime(name: str) -> datetime | None: + val = os.environ.get(name) + if not val: + return None + return datetime.fromisoformat(val.replace("Z", "+00:00")) + + +def _estimate_bytes_processed(project: str, location: str | None, sql: str) -> int: + try: + from google.cloud import bigquery + except Exception as e: + pytest.skip(str(e)) + client = bigquery.Client(project=project, location=location) + job_config = bigquery.QueryJobConfig(dry_run=True, use_query_cache=False) + job = client.query(sql, job_config=job_config) + return int(job.total_bytes_processed or 0) + + +@pytest.mark.benchmark(group="bigquery_partition_pruning") +@patch("feast.infra.offline_stores.bigquery._get_bigquery_client") +def test_bigquery_partition_pruning_bytes_processed( + mock_get_bigquery_client, benchmark +): + mock_get_bigquery_client.return_value = Mock() + + project = _required_env("FEAST_BQ_BENCH_PROJECT") + table = _required_env("FEAST_BQ_BENCH_TABLE") + timestamp_field = _required_env("FEAST_BQ_BENCH_TIMESTAMP_FIELD") + partition_column = _required_env("FEAST_BQ_BENCH_PARTITION_COLUMN") + location = os.environ.get("FEAST_BQ_BENCH_LOCATION") + + end = _optional_iso_datetime("FEAST_BQ_BENCH_END") + if end is None: + end = datetime.now(tz=timezone.utc).replace(microsecond=0) + start = _optional_iso_datetime("FEAST_BQ_BENCH_START") + if start is None: + start = end - timedelta(days=7) + + repo_config = RepoConfig( + registry="gs://ml-test/repo/registry.db", + project="bench", + provider="gcp", + online_store=SqliteOnlineStoreConfig(type="sqlite"), + offline_store=BigQueryOfflineStoreConfig(type="bigquery", dataset="feast"), + ) + + source_without_partition = BigQuerySource( + table=table, + timestamp_field=timestamp_field, + ) + source_with_partition = BigQuerySource( + table=table, + timestamp_field=timestamp_field, + date_partition_column=partition_column, + ) + + job_without = BigQueryOfflineStore.pull_all_from_table_or_query( + config=repo_config, + data_source=source_without_partition, + join_key_columns=[], + feature_name_columns=[], + timestamp_field=timestamp_field, + start_date=start, + end_date=end, + ) + job_with = BigQueryOfflineStore.pull_all_from_table_or_query( + config=repo_config, + data_source=source_with_partition, + join_key_columns=[], + feature_name_columns=[], + timestamp_field=timestamp_field, + start_date=start, + end_date=end, + ) + + sql_without = job_without.to_sql() + sql_with = job_with.to_sql() + + def measure(): + bytes_without = _estimate_bytes_processed(project, location, sql_without) + bytes_with = _estimate_bytes_processed(project, location, sql_with) + return bytes_without, bytes_with + + bytes_without, bytes_with = benchmark(measure) + benchmark.extra_info["total_bytes_processed_without_partition_filter"] = ( + bytes_without + ) + benchmark.extra_info["total_bytes_processed_with_partition_filter"] = bytes_with + if bytes_without > 0: + benchmark.extra_info["bytes_ratio_with_over_without"] = ( + bytes_with / bytes_without + ) + + if os.environ.get("FEAST_BQ_BENCH_REQUIRE_REDUCTION", "").lower() in ( + "1", + "true", + "yes", + ): + assert bytes_with < bytes_without + else: + assert bytes_with <= bytes_without diff --git a/sdk/python/tests/benchmarks/test_key_encoding_benchmarks.py b/sdk/python/tests/benchmarks/test_key_encoding_benchmarks.py new file mode 100644 index 00000000000..b34cabbdb37 --- /dev/null +++ b/sdk/python/tests/benchmarks/test_key_encoding_benchmarks.py @@ -0,0 +1,465 @@ +""" +Benchmarks for entity key serialization/deserialization performance. + +This module provides comprehensive performance tests for the key encoding utilities +to validate and track the performance improvements from optimization efforts. +""" + +import time + +import pytest + +from feast.infra.key_encoding_utils import ( + deserialize_entity_key, + serialize_entity_key, + serialize_entity_key_prefix, +) +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto + + +@pytest.fixture +def single_entity_key(): + """Single entity key (most common case - 90% of usage)""" + return EntityKeyProto( + join_keys=["user_id"], entity_values=[ValueProto(string_val="user123")] + ) + + +@pytest.fixture +def single_entity_key_int(): + """Single entity key with int64 value""" + return EntityKeyProto( + join_keys=["user_id"], entity_values=[ValueProto(int64_val=123456789)] + ) + + +@pytest.fixture +def multi_entity_key_small(): + """Small multi-entity key (2-3 entities)""" + return EntityKeyProto( + join_keys=["user_id", "session_id"], + entity_values=[ + ValueProto(string_val="user123"), + ValueProto(string_val="sess456"), + ], + ) + + +@pytest.fixture +def multi_entity_key_large(): + """Large multi-entity key (5+ entities)""" + return EntityKeyProto( + join_keys=["user_id", "session_id", "device_id", "app_version", "region"], + entity_values=[ + ValueProto(string_val="user123"), + ValueProto(string_val="sess456"), + ValueProto(string_val="dev789"), + ValueProto(string_val="v1.2.3"), + ValueProto(string_val="us-west-2"), + ], + ) + + +@pytest.fixture +def mixed_value_types_key(): + """Entity key with mixed value types""" + return EntityKeyProto( + join_keys=["user_id", "timestamp", "count", "score"], + entity_values=[ + ValueProto(string_val="user123"), + ValueProto(unix_timestamp_val=1758823656), + ValueProto(int64_val=42), + ValueProto(int32_val=95), + ], + ) + + +@pytest.fixture +def batch_entity_keys(single_entity_key, multi_entity_key_small): + """Batch of entity keys for bulk operation testing""" + keys = [] + # Generate 100 single entity keys (typical batch size) + for i in range(80): # 80% single entities + keys.append( + EntityKeyProto( + join_keys=["user_id"], entity_values=[ValueProto(string_val=f"user{i}")] + ) + ) + + # Add 20 multi-entity keys + for i in range(20): + keys.append( + EntityKeyProto( + join_keys=["user_id", "session_id"], + entity_values=[ + ValueProto(string_val=f"user{i}"), + ValueProto(string_val=f"sess{i}"), + ], + ) + ) + + return keys + + +# Serialization Benchmarks + + +@pytest.mark.benchmark(group="serialize_single") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_serialize_single_entity_string( + benchmark, single_entity_key, entity_key_serialization_version +): + """Benchmark single entity key serialization (string value) - most common case.""" + result = benchmark( + serialize_entity_key, single_entity_key, entity_key_serialization_version + ) + assert len(result) > 0 + + +@pytest.mark.benchmark(group="serialize_single") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_serialize_single_entity_int( + benchmark, single_entity_key_int, entity_key_serialization_version +): + """Benchmark single entity key serialization (int64 value).""" + result = benchmark( + serialize_entity_key, single_entity_key_int, entity_key_serialization_version + ) + assert len(result) > 0 + + +@pytest.mark.benchmark(group="serialize_multi") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_serialize_multi_entity_small( + benchmark, multi_entity_key_small, entity_key_serialization_version +): + """Benchmark small multi-entity key serialization.""" + result = benchmark( + serialize_entity_key, multi_entity_key_small, entity_key_serialization_version + ) + assert len(result) > 0 + + +@pytest.mark.benchmark(group="serialize_multi") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_serialize_multi_entity_large( + benchmark, multi_entity_key_large, entity_key_serialization_version +): + """Benchmark large multi-entity key serialization.""" + result = benchmark( + serialize_entity_key, multi_entity_key_large, entity_key_serialization_version + ) + assert len(result) > 0 + + +@pytest.mark.benchmark(group="serialize_mixed") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_serialize_mixed_value_types( + benchmark, mixed_value_types_key, entity_key_serialization_version +): + """Benchmark serialization with mixed value types.""" + result = benchmark( + serialize_entity_key, mixed_value_types_key, entity_key_serialization_version + ) + assert len(result) > 0 + + +# Deserialization Benchmarks + + +@pytest.mark.benchmark(group="deserialize_single") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_deserialize_single_entity_string( + benchmark, single_entity_key, entity_key_serialization_version +): + """Benchmark single entity key deserialization (string value).""" + serialized = serialize_entity_key( + single_entity_key, entity_key_serialization_version + ) + result = benchmark( + deserialize_entity_key, serialized, entity_key_serialization_version + ) + assert result == single_entity_key + + +@pytest.mark.benchmark(group="deserialize_single") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_deserialize_single_entity_int( + benchmark, single_entity_key_int, entity_key_serialization_version +): + """Benchmark single entity key deserialization (int64 value).""" + serialized = serialize_entity_key( + single_entity_key_int, entity_key_serialization_version + ) + result = benchmark( + deserialize_entity_key, serialized, entity_key_serialization_version + ) + assert result == single_entity_key_int + + +@pytest.mark.benchmark(group="deserialize_multi") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_deserialize_multi_entity_small( + benchmark, multi_entity_key_small, entity_key_serialization_version +): + """Benchmark small multi-entity key deserialization.""" + serialized = serialize_entity_key( + multi_entity_key_small, entity_key_serialization_version + ) + result = benchmark( + deserialize_entity_key, serialized, entity_key_serialization_version + ) + assert result == multi_entity_key_small + + +@pytest.mark.benchmark(group="deserialize_multi") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_deserialize_multi_entity_large( + benchmark, multi_entity_key_large, entity_key_serialization_version +): + """Benchmark large multi-entity key deserialization.""" + serialized = serialize_entity_key( + multi_entity_key_large, entity_key_serialization_version + ) + result = benchmark( + deserialize_entity_key, serialized, entity_key_serialization_version + ) + assert result == multi_entity_key_large + + +@pytest.mark.benchmark(group="deserialize_mixed") +@pytest.mark.parametrize("entity_key_serialization_version", [3]) +def test_deserialize_mixed_value_types( + benchmark, mixed_value_types_key, entity_key_serialization_version +): + """Benchmark deserialization with mixed value types.""" + serialized = serialize_entity_key( + mixed_value_types_key, entity_key_serialization_version + ) + result = benchmark( + deserialize_entity_key, serialized, entity_key_serialization_version + ) + assert result == mixed_value_types_key + + +# Round-trip Benchmarks + + +@pytest.mark.benchmark(group="roundtrip_single") +def test_roundtrip_single_entity(benchmark, single_entity_key): + """Benchmark complete serialize + deserialize round-trip for single entity.""" + + def roundtrip(): + serialized = serialize_entity_key(single_entity_key, 3) + return deserialize_entity_key(serialized, 3) + + result = benchmark(roundtrip) + assert result == single_entity_key + + +@pytest.mark.benchmark(group="roundtrip_multi") +def test_roundtrip_multi_entity(benchmark, multi_entity_key_small): + """Benchmark complete serialize + deserialize round-trip for multi-entity.""" + + def roundtrip(): + serialized = serialize_entity_key(multi_entity_key_small, 3) + return deserialize_entity_key(serialized, 3) + + result = benchmark(roundtrip) + assert result == multi_entity_key_small + + +# Prefix Serialization Benchmarks + + +@pytest.mark.benchmark(group="prefix") +def test_serialize_entity_key_prefix_single(benchmark): + """Benchmark entity key prefix serialization for single key.""" + result = benchmark(serialize_entity_key_prefix, ["user_id"], 3) + assert len(result) > 0 + + +@pytest.mark.benchmark(group="prefix") +def test_serialize_entity_key_prefix_multi(benchmark): + """Benchmark entity key prefix serialization for multiple keys.""" + keys = ["user_id", "session_id", "device_id"] + result = benchmark(serialize_entity_key_prefix, keys, 3) + assert len(result) > 0 + + +# Bulk Operations Benchmarks + + +@pytest.mark.benchmark(group="bulk_serialize") +def test_bulk_serialize_batch(benchmark, batch_entity_keys): + """Benchmark batch serialization of 100 mixed entity keys.""" + + def bulk_serialize(): + results = [] + for entity_key in batch_entity_keys: + serialized = serialize_entity_key(entity_key, 3) + results.append(serialized) + return results + + results = benchmark(bulk_serialize) + assert len(results) == 100 + + +@pytest.mark.benchmark(group="bulk_deserialize") +def test_bulk_deserialize_batch(benchmark, batch_entity_keys): + """Benchmark batch deserialization of 100 mixed entity keys.""" + # Pre-serialize all keys + serialized_keys = [serialize_entity_key(key, 3) for key in batch_entity_keys] + + def bulk_deserialize(): + results = [] + for serialized in serialized_keys: + deserialized = deserialize_entity_key(serialized, 3) + results.append(deserialized) + return results + + results = benchmark(bulk_deserialize) + assert len(results) == 100 + + +@pytest.mark.benchmark(group="bulk_roundtrip") +def test_bulk_roundtrip_batch(benchmark, batch_entity_keys): + """Benchmark bulk serialize + deserialize for realistic workload.""" + + def bulk_roundtrip(): + results = [] + for entity_key in batch_entity_keys: + serialized = serialize_entity_key(entity_key, 3) + deserialized = deserialize_entity_key(serialized, 3) + results.append(deserialized) + return results + + results = benchmark(bulk_roundtrip) + assert len(results) == 100 + + +# Memory Efficiency Tests + + +def test_memory_efficiency_serialization(single_entity_key): + """Test memory usage during serialization (not a benchmark, just validation).""" + import os + + import psutil + + process = psutil.Process(os.getpid()) + initial_memory = process.memory_info().rss + + # Perform many serializations + for i in range(10000): + entity_key = EntityKeyProto( + join_keys=["user_id"], entity_values=[ValueProto(string_val=f"user{i}")] + ) + serialize_entity_key(entity_key, 3) + + final_memory = process.memory_info().rss + memory_increase = final_memory - initial_memory + + # Memory increase should be minimal (< 10MB for 10k operations) + # This validates that we're not leaking memory in the optimized version + assert memory_increase < 10 * 1024 * 1024, ( + f"Memory usage increased by {memory_increase / 1024 / 1024:.2f} MB" + ) + + +# Performance Regression Tests + + +def test_performance_regression_single_entity(): + """Regression test: single entity serialization should be faster than baseline.""" + entity_key = EntityKeyProto( + join_keys=["user_id"], entity_values=[ValueProto(string_val="user123")] + ) + + # Warm up + for _ in range(100): + serialize_entity_key(entity_key, 3) + + # Time 1000 operations + start_time = time.perf_counter() + for _ in range(1000): + serialize_entity_key(entity_key, 3) + elapsed = time.perf_counter() - start_time + + # Should be able to do 1000 single entity serializations in < 100ms + # Using a generous threshold to avoid flaky failures on CI runners + assert elapsed < 0.1, ( + f"Single entity serialization too slow: {elapsed:.4f}s for 1000 operations" + ) + + +def test_performance_regression_deserialization(): + """Regression test: deserialization should be fast with memoryview optimization.""" + entity_key = EntityKeyProto( + join_keys=["user_id", "session_id"], + entity_values=[ + ValueProto(string_val="user123"), + ValueProto(string_val="sess456"), + ], + ) + + serialized = serialize_entity_key(entity_key, 3) + + # Warm up + for _ in range(100): + deserialize_entity_key(serialized, 3) + + # Time 1000 operations + start_time = time.perf_counter() + for _ in range(1000): + deserialize_entity_key(serialized, 3) + elapsed = time.perf_counter() - start_time + + # Should be able to do 1000 deserializations in < 200ms + # Using a generous threshold to avoid flaky failures on CI runners + assert elapsed < 0.2, ( + f"Deserialization too slow: {elapsed:.4f}s for 1000 operations" + ) + + +# Binary Compatibility Tests + + +def test_binary_format_consistency_single(): + """Ensure optimizations don't change binary format for single entities.""" + entity_key = EntityKeyProto( + join_keys=["user_id"], entity_values=[ValueProto(string_val="test")] + ) + + # Serialize multiple times - results should be identical + results = [] + for _ in range(10): + serialized = serialize_entity_key(entity_key, 3) + results.append(serialized) + + # All results should be identical + for result in results[1:]: + assert result == results[0], "Binary format inconsistency detected" + + +def test_binary_format_consistency_multi(): + """Ensure optimizations don't change binary format for multi-entity keys.""" + entity_key = EntityKeyProto( + join_keys=["user", "session", "device"], + entity_values=[ + ValueProto(string_val="u1"), + ValueProto(string_val="s1"), + ValueProto(string_val="d1"), + ], + ) + + # Serialize multiple times - results should be identical + results = [] + for _ in range(10): + serialized = serialize_entity_key(entity_key, 3) + results.append(serialized) + + # All results should be identical + for result in results[1:]: + assert result == results[0], "Binary format inconsistency detected" diff --git a/sdk/python/tests/component/.gitkeep b/sdk/python/tests/component/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/component/__init__.py b/sdk/python/tests/component/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/component/ray/.gitkeep b/sdk/python/tests/component/ray/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/component/ray/__init__.py b/sdk/python/tests/component/ray/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/integration/compute_engines/ray_compute/conftest.py b/sdk/python/tests/component/ray/conftest.py similarity index 87% rename from sdk/python/tests/integration/compute_engines/ray_compute/conftest.py rename to sdk/python/tests/component/ray/conftest.py index 885b1555ec7..d2d46b7c9a7 100644 --- a/sdk/python/tests/integration/compute_engines/ray_compute/conftest.py +++ b/sdk/python/tests/component/ray/conftest.py @@ -4,7 +4,7 @@ auto-discovered by pytest. """ -from tests.integration.compute_engines.ray_compute.ray_shared_utils import ( +from tests.component.ray.ray_shared_utils import ( entity_df, feature_dataset, ray_environment, diff --git a/sdk/python/tests/integration/compute_engines/ray_compute/ray_shared_utils.py b/sdk/python/tests/component/ray/ray_shared_utils.py similarity index 98% rename from sdk/python/tests/integration/compute_engines/ray_compute/ray_shared_utils.py rename to sdk/python/tests/component/ray/ray_shared_utils.py index 6b28949f401..ba79685d6cb 100644 --- a/sdk/python/tests/integration/compute_engines/ray_compute/ray_shared_utils.py +++ b/sdk/python/tests/component/ray/ray_shared_utils.py @@ -14,7 +14,7 @@ from feast.data_source import DataSource from feast.infra.ray_initializer import shutdown_ray from feast.utils import _utc_now -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_test_environment, ) diff --git a/sdk/python/tests/integration/compute_engines/ray_compute/repo_configuration.py b/sdk/python/tests/component/ray/repo_configuration.py similarity index 71% rename from sdk/python/tests/integration/compute_engines/ray_compute/repo_configuration.py rename to sdk/python/tests/component/ray/repo_configuration.py index 37d0d020ccd..f18185c35c9 100644 --- a/sdk/python/tests/integration/compute_engines/ray_compute/repo_configuration.py +++ b/sdk/python/tests/component/ray/repo_configuration.py @@ -3,19 +3,16 @@ from feast.infra.offline_stores.contrib.ray_repo_configuration import ( RayDataSourceCreator, ) -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.online_store.redis import ( - RedisOnlineStoreCreator, -) def get_ray_compute_engine_test_config() -> IntegrationTestRepoConfig: """Get test configuration for Ray compute engine.""" return IntegrationTestRepoConfig( provider="local", - online_store_creator=RedisOnlineStoreCreator, + online_store={"type": "sqlite"}, offline_store_creator=RayDataSourceCreator, batch_engine={ "type": "ray.engine", diff --git a/sdk/python/tests/component/ray/test_compute.py b/sdk/python/tests/component/ray/test_compute.py new file mode 100644 index 00000000000..84735e87bee --- /dev/null +++ b/sdk/python/tests/component/ray/test_compute.py @@ -0,0 +1,651 @@ +import tempfile +from datetime import timedelta +from typing import cast +from unittest.mock import MagicMock + +import pandas as pd +import pytest +from tqdm import tqdm + +from feast import BatchFeatureView, Field +from feast.aggregation import Aggregation +from feast.infra.common.materialization_job import ( + MaterializationJobStatus, + MaterializationTask, +) +from feast.infra.common.retrieval_task import HistoricalRetrievalTask +from feast.infra.compute_engines.ray.compute import RayComputeEngine +from feast.infra.compute_engines.ray.config import RayComputeEngineConfig +from feast.infra.compute_engines.ray.job import RayDAGRetrievalJob +from feast.infra.offline_stores.contrib.ray_offline_store.ray import ( + RayOfflineStore, +) +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import ( + RaySource, +) +from feast.transformation.ray_transformation import RayTransformation +from feast.types import Float32, Int32, Int64 +from tests.component.ray.ray_shared_utils import ( + driver, + now, + today, +) + + +@pytest.mark.integration +@pytest.mark.xdist_group(name="ray") +def test_ray_compute_engine_get_historical_features( + ray_environment, feature_dataset, entity_df +): + """Test Ray compute engine historical feature retrieval.""" + fs = ray_environment.feature_store + registry = fs.registry + + def transform_feature(df: pd.DataFrame) -> pd.DataFrame: + df["sum_conv_rate"] = df["sum_conv_rate"] * 2 + df["avg_acc_rate"] = df["avg_acc_rate"] * 2 + return df + + driver_stats_fv = BatchFeatureView( + name="driver_hourly_stats", + entities=[driver], + mode="pandas", + aggregations=[ + Aggregation(column="conv_rate", function="sum"), + Aggregation(column="acc_rate", function="avg"), + ], + udf=transform_feature, + udf_string="transform_feature", + ttl=timedelta(days=3), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + Field(name="driver_id", dtype=Int32), + ], + online=False, + offline=False, + source=feature_dataset, + ) + + fs.apply([driver, driver_stats_fv]) + + # Build retrieval task + task = HistoricalRetrievalTask( + project=ray_environment.project, + entity_df=entity_df, + feature_view=driver_stats_fv, + full_feature_name=False, + registry=registry, + ) + engine = RayComputeEngine( + repo_config=ray_environment.config, + offline_store=RayOfflineStore(), + online_store=MagicMock(), + ) + + ray_dag_retrieval_job = engine.get_historical_features(registry, task) + ray_dataset = cast(RayDAGRetrievalJob, ray_dag_retrieval_job).to_ray_dataset() + df_out = ray_dataset.to_pandas().sort_values("driver_id") + + assert df_out.driver_id.to_list() == [1001, 1002] + assert abs(df_out["sum_conv_rate"].to_list()[0] - 1.6) < 1e-6 + assert abs(df_out["sum_conv_rate"].to_list()[1] - 2.0) < 1e-6 + assert abs(df_out["avg_acc_rate"].to_list()[0] - 1.0) < 1e-6 + assert abs(df_out["avg_acc_rate"].to_list()[1] - 1.0) < 1e-6 + + +@pytest.mark.integration +@pytest.mark.xdist_group(name="ray") +def test_ray_compute_engine_materialize(ray_environment, feature_dataset): + """Test Ray compute engine materialization.""" + fs = ray_environment.feature_store + registry = fs.registry + + def transform_feature(df: pd.DataFrame) -> pd.DataFrame: + df["sum_conv_rate"] = df["sum_conv_rate"] * 2 + df["avg_acc_rate"] = df["avg_acc_rate"] * 2 + return df + + driver_stats_fv = BatchFeatureView( + name="driver_hourly_stats", + entities=[driver], + mode="pandas", + aggregations=[ + Aggregation(column="conv_rate", function="sum"), + Aggregation(column="acc_rate", function="avg"), + ], + udf=transform_feature, + udf_string="transform_feature", + ttl=timedelta(days=3), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + Field(name="driver_id", dtype=Int32), + ], + online=True, + offline=False, + source=feature_dataset, + ) + + def tqdm_builder(length): + return tqdm(length, ncols=100) + + fs.apply([driver, driver_stats_fv]) + + task = MaterializationTask( + project=ray_environment.project, + feature_view=driver_stats_fv, + start_time=now - timedelta(days=2), + end_time=now, + tqdm_builder=tqdm_builder, + ) + + engine = RayComputeEngine( + repo_config=ray_environment.config, + offline_store=RayOfflineStore(), + online_store=MagicMock(), + ) + + ray_materialize_jobs = engine.materialize(registry, task) + + assert len(ray_materialize_jobs) == 1 + assert ray_materialize_jobs[0].status() == MaterializationJobStatus.SUCCEEDED + + +@pytest.mark.integration +@pytest.mark.xdist_group(name="ray") +def test_ray_compute_engine_config(): + """Test Ray compute engine configuration.""" + config = RayComputeEngineConfig( + type="ray.engine", + ray_address="ray://localhost:10001", + broadcast_join_threshold_mb=200, + enable_distributed_joins=True, + max_parallelism_multiplier=4, + target_partition_size_mb=128, + window_size_for_joins="2H", + max_workers=4, + enable_optimization=True, + ) + + assert config.type == "ray.engine" + assert config.ray_address == "ray://localhost:10001" + assert config.broadcast_join_threshold_mb == 200 + assert config.window_size_timedelta == timedelta(hours=2) + + +@pytest.mark.integration +@pytest.mark.xdist_group(name="ray") +def test_ray_transformation_compute_engine(ray_environment, feature_dataset, entity_df): + """Test Ray compute engine with Ray transformation mode.""" + import ray.data + + fs = ray_environment.feature_store + registry = fs.registry + + def ray_transformation_udf(ds: ray.data.Dataset) -> ray.data.Dataset: + """Ray native transformation that processes data in parallel.""" + + def process_batch(batch: pd.DataFrame) -> pd.DataFrame: + # Simulate some computation (e.g., feature engineering) + if "conv_rate" in batch.columns: + batch["processed_conv_rate"] = batch["conv_rate"] * 2.0 + if "acc_rate" in batch.columns: + batch["processed_acc_rate"] = batch["acc_rate"] * 1.5 + return batch + + return ds.map_batches( + process_batch, + batch_format="pandas", + concurrency=2, # Use 2 parallel workers + ) + + # Create Ray transformation + ray_transform = RayTransformation( + udf=ray_transformation_udf, + udf_string="def ray_transformation_udf(ds): return ds.map_batches(...)", + ) + + driver_stats_fv = BatchFeatureView( + name="driver_hourly_stats_ray", + entities=[driver], + mode="ray", # Use Ray transformation mode + feature_transformation=ray_transform, + ttl=timedelta(days=3), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="processed_conv_rate", dtype=Float32), + Field(name="processed_acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + Field(name="driver_id", dtype=Int32), + ], + online=False, + offline=False, + source=feature_dataset, + ) + + fs.apply([driver, driver_stats_fv]) + + # Build retrieval task + task = HistoricalRetrievalTask( + project=ray_environment.project, + entity_df=entity_df, + feature_view=driver_stats_fv, + full_feature_name=False, + registry=registry, + ) + engine = RayComputeEngine( + repo_config=ray_environment.config, + offline_store=RayOfflineStore(), + online_store=MagicMock(), + ) + + ray_dag_retrieval_job = engine.get_historical_features(registry, task) + ray_dataset = cast(RayDAGRetrievalJob, ray_dag_retrieval_job).to_ray_dataset() + df_out = ray_dataset.to_pandas().sort_values("driver_id") + + # Verify the transformation was applied + assert df_out.driver_id.to_list() == [1001, 1002] + + # Check that original columns are present + assert "conv_rate" in df_out.columns + assert "acc_rate" in df_out.columns + + # Check that transformed columns are present + assert "processed_conv_rate" in df_out.columns + assert "processed_acc_rate" in df_out.columns + + # Verify the transformation logic was applied + for idx, row in df_out.iterrows(): + assert abs(row["processed_conv_rate"] - row["conv_rate"] * 2.0) < 1e-6 + assert abs(row["processed_acc_rate"] - row["acc_rate"] * 1.5) < 1e-6 + + +@pytest.mark.integration +@pytest.mark.xdist_group(name="ray") +def test_ray_transformation_materialization(ray_environment, feature_dataset): + """Test Ray transformation during materialization.""" + import ray.data + + fs = ray_environment.feature_store + registry = fs.registry + + def ray_embedding_udf(ds: ray.data.Dataset) -> ray.data.Dataset: + """Simulate embedding generation with Ray native processing.""" + + def generate_embeddings(batch: pd.DataFrame) -> pd.DataFrame: + # Simulate embedding generation + if "conv_rate" in batch.columns: + # Create a simple embedding based on conv_rate + batch["embedding"] = batch["conv_rate"].apply( + lambda x: [x * 0.1, x * 0.2, x * 0.3] + ) + return batch + + return ds.map_batches(generate_embeddings, batch_format="pandas", concurrency=2) + + # Create Ray transformation for embeddings + ray_embedding_transform = RayTransformation( + udf=ray_embedding_udf, + udf_string="def ray_embedding_udf(ds): return ds.map_batches(...)", + ) + + driver_embeddings_fv = BatchFeatureView( + name="driver_embeddings", + entities=[driver], + mode="ray", + feature_transformation=ray_embedding_transform, + ttl=timedelta(days=3), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field( + name="embedding", dtype=Float32 + ), # This would be Array(Float32) in real usage + Field(name="driver_id", dtype=Int32), + ], + online=True, + offline=False, + source=feature_dataset, + ) + + def tqdm_builder(length): + return tqdm(length, ncols=100) + + fs.apply([driver, driver_embeddings_fv]) + + task = MaterializationTask( + project=ray_environment.project, + feature_view=driver_embeddings_fv, + start_time=now - timedelta(days=2), + end_time=now, + tqdm_builder=tqdm_builder, + ) + + engine = RayComputeEngine( + repo_config=ray_environment.config, + offline_store=RayOfflineStore(), + online_store=MagicMock(), + ) + + ray_materialize_jobs = engine.materialize(registry, task) + + assert len(ray_materialize_jobs) == 1 + assert ray_materialize_jobs[0].status() == MaterializationJobStatus.SUCCEEDED + + +# --------------------------------------------------------------------------- +# RaySource integration tests — pull_latest and pull_all time-range filtering +# --------------------------------------------------------------------------- + + +def _write_ray_source_parquet(tmp_dir: str) -> str: + """Write a small driver-stats parquet with 4 rows (2 per entity) and return path.""" + yesterday = today - timedelta(days=1) + last_week = today - timedelta(days=7) + two_weeks_ago = today - timedelta(days=14) + + df = pd.DataFrame( + [ + # driver 1001 — two rows within the last 10 days + { + "driver_id": 1001, + "event_timestamp": yesterday, + "created": now, + "conv_rate": 0.8, + "acc_rate": 0.5, + "avg_daily_trips": 15, + }, + { + "driver_id": 1001, + "event_timestamp": last_week, + "created": now, + "conv_rate": 0.75, + "acc_rate": 0.9, + "avg_daily_trips": 14, + }, + # driver 1002 — recent row inside window, old row outside + { + "driver_id": 1002, + "event_timestamp": yesterday, + "created": now, + "conv_rate": 0.7, + "acc_rate": 0.4, + "avg_daily_trips": 12, + }, + { + "driver_id": 1002, + "event_timestamp": two_weeks_ago, + "created": now, + "conv_rate": 0.3, + "acc_rate": 0.6, + "avg_daily_trips": 11, + }, + ] + ) + path = f"{tmp_dir}/ray_source_features.parquet" + df.to_parquet(path, index=False) + return path + + +@pytest.mark.integration +@pytest.mark.xdist_group(name="ray") +def test_pull_latest_from_ray_source_filters_and_deduplicates(ray_environment): + """pull_latest_from_table_or_query with RaySource must: + + 1. Respect the start_date/end_date window — the row for driver 1002 at + two_weeks_ago falls outside the 10-day window and must be excluded. + 2. Deduplicate to the single latest row per entity — driver 1001 has two + rows inside the window; only the most recent (yesterday) must survive. + """ + with tempfile.TemporaryDirectory() as tmp_dir: + parquet_path = _write_ray_source_parquet(tmp_dir) + + source = RaySource( + name="driver_stats_ray", + reader_type="parquet", + path=parquet_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + start = now - timedelta(days=10) + end = now + + job = RayOfflineStore.pull_latest_from_table_or_query( + config=ray_environment.config, + data_source=source, + join_key_columns=["driver_id"], + feature_name_columns=["conv_rate", "acc_rate", "avg_daily_trips"], + timestamp_field="event_timestamp", + created_timestamp_column="created", + start_date=start, + end_date=end, + ) + + df = job.to_df().sort_values("driver_id").reset_index(drop=True) + + # Exactly one row per entity + assert df["driver_id"].to_list() == [1001, 1002], ( + f"Expected [1001, 1002], got {df['driver_id'].to_list()}" + ) + + # driver 1001: latest row is yesterday (conv_rate=0.8) + assert abs(df.loc[df["driver_id"] == 1001, "conv_rate"].iloc[0] - 0.8) < 1e-5 + + # driver 1002: the two_weeks_ago row is outside the window; only yesterday survives + assert abs(df.loc[df["driver_id"] == 1002, "conv_rate"].iloc[0] - 0.7) < 1e-5 + + +@pytest.mark.integration +@pytest.mark.xdist_group(name="ray") +def test_pull_all_from_ray_source_filters_by_time_range(ray_environment): + """pull_all_from_table_or_query with RaySource must return all rows within + the time window without deduplication. + + - driver 1001: both rows (yesterday, last_week) are inside the 10-day window → 2 rows + - driver 1002: yesterday is inside, two_weeks_ago is outside → 1 row + - Total: 3 rows + """ + with tempfile.TemporaryDirectory() as tmp_dir: + parquet_path = _write_ray_source_parquet(tmp_dir) + + source = RaySource( + name="driver_stats_ray_all", + reader_type="parquet", + path=parquet_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + start = now - timedelta(days=10) + end = now + + job = RayOfflineStore.pull_all_from_table_or_query( + config=ray_environment.config, + data_source=source, + join_key_columns=["driver_id"], + feature_name_columns=["conv_rate", "acc_rate", "avg_daily_trips"], + timestamp_field="event_timestamp", + created_timestamp_column="created", + start_date=start, + end_date=end, + ) + + df = ( + job.to_df() + .sort_values(["driver_id", "event_timestamp"]) + .reset_index(drop=True) + ) + + # 3 rows: both for 1001, one for 1002 (the 14-day-old row is outside window) + assert len(df) == 3, ( + f"Expected 3 rows, got {len(df)}: " + f"{df[['driver_id', 'event_timestamp']].to_dict('records')}" + ) + assert sorted(df["driver_id"].to_list()) == [1001, 1001, 1002] + + +@pytest.mark.integration +@pytest.mark.xdist_group(name="ray") +def test_get_historical_features_with_ray_source(ray_environment): + """engine.get_historical_features() with a BatchFeatureView backed by RaySource. + + This exercises the DAG execution path (RayFeatureBuilder → RayReadNode → + _resolve_source_dataset) rather than pull_latest_from_table_or_query, which + is a distinct code path from the two pull_* tests above. Asserting on + returned feature values confirms the full read pipeline is correct, not just + that execution succeeded. + + Expected results with entity_df timestamp = today and TTL = 10 days: + - driver 1001: latest row is yesterday → conv_rate ≈ 0.8 + - driver 1002: latest row is yesterday → conv_rate ≈ 0.7 + (the two_weeks_ago row is outside the TTL window and must be excluded) + """ + with tempfile.TemporaryDirectory() as tmp_dir: + parquet_path = _write_ray_source_parquet(tmp_dir) + + source = RaySource( + name="driver_stats_ray_hist", + reader_type="parquet", + path=parquet_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + fs = ray_environment.feature_store + registry = fs.registry + + driver_stats_fv = BatchFeatureView( + name="driver_hourly_stats_ray_source", + entities=[driver], + mode="pandas", + ttl=timedelta(days=10), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + Field(name="driver_id", dtype=Int32), + ], + online=False, + offline=False, + source=source, + ) + + fs.apply([driver, driver_stats_fv]) + + entity_df = pd.DataFrame( + [ + {"driver_id": 1001, "event_timestamp": today}, + {"driver_id": 1002, "event_timestamp": today}, + ] + ) + + task = HistoricalRetrievalTask( + project=ray_environment.project, + entity_df=entity_df, + feature_view=driver_stats_fv, + full_feature_name=False, + registry=registry, + ) + + engine = RayComputeEngine( + repo_config=ray_environment.config, + offline_store=RayOfflineStore(), + online_store=MagicMock(), + ) + + job = engine.get_historical_features(registry, task) + df_out = ( + cast(RayDAGRetrievalJob, job) + .to_ray_dataset() + .to_pandas() + .sort_values("driver_id") + .reset_index(drop=True) + ) + + assert df_out["driver_id"].to_list() == [1001, 1002] + assert ( + abs(df_out.loc[df_out["driver_id"] == 1001, "conv_rate"].iloc[0] - 0.8) + < 1e-5 + ) + assert ( + abs(df_out.loc[df_out["driver_id"] == 1002, "conv_rate"].iloc[0] - 0.7) + < 1e-5 + ) + + +@pytest.mark.integration +@pytest.mark.xdist_group(name="ray") +def test_pull_latest_from_ray_sql_source_filters_and_deduplicates(ray_environment): + """pull_latest_from_table_or_query with RaySource(reader_type="sql") using + a local SQLite database. + + SQLite requires no external server, making it CI-safe. This exercises the + non-file-backed RaySource path where data is loaded by read_sql and then + filtered/deduplicated by _load_and_filter_dataset_ray(pre_loaded_ds=...). + """ + pytest.importorskip("sqlalchemy", reason="sqlalchemy required for SQL reader") + + import sqlite3 + + with tempfile.TemporaryDirectory() as tmp_dir: + db_path = f"{tmp_dir}/driver_stats.db" + yesterday = today - timedelta(days=1) + last_week = today - timedelta(days=7) + two_weeks_ago = today - timedelta(days=14) + + conn = sqlite3.connect(db_path) + conn.execute( + "CREATE TABLE driver_stats " + "(driver_id INTEGER, event_timestamp TEXT, created TEXT, " + "conv_rate REAL, acc_rate REAL, avg_daily_trips INTEGER)" + ) + conn.executemany( + "INSERT INTO driver_stats VALUES (?,?,?,?,?,?)", + [ + (1001, yesterday.isoformat(), now.isoformat(), 0.8, 0.5, 15), + (1001, last_week.isoformat(), now.isoformat(), 0.75, 0.9, 14), + (1002, yesterday.isoformat(), now.isoformat(), 0.7, 0.4, 12), + (1002, two_weeks_ago.isoformat(), now.isoformat(), 0.3, 0.6, 11), + ], + ) + conn.commit() + conn.close() + + source = RaySource( + name="driver_stats_sql", + reader_type="sql", + timestamp_field="event_timestamp", + reader_options={ + "sql": "SELECT * FROM driver_stats", + "connection_url": f"sqlite:///{db_path}", + }, + ) + + start = now - timedelta(days=10) + end = now + + job = RayOfflineStore.pull_latest_from_table_or_query( + config=ray_environment.config, + data_source=source, + join_key_columns=["driver_id"], + feature_name_columns=["conv_rate", "acc_rate", "avg_daily_trips"], + timestamp_field="event_timestamp", + created_timestamp_column=None, + start_date=start, + end_date=end, + ) + + df = job.to_df().sort_values("driver_id").reset_index(drop=True) + + # One row per entity, within the window + assert df["driver_id"].to_list() == [1001, 1002], ( + f"Expected [1001, 1002], got {df['driver_id'].to_list()}" + ) + + # Latest for each: 1001→yesterday (0.8), 1002→yesterday (0.7) + assert abs(df.loc[df["driver_id"] == 1001, "conv_rate"].iloc[0] - 0.8) < 1e-5 + assert abs(df.loc[df["driver_id"] == 1002, "conv_rate"].iloc[0] - 0.7) < 1e-5 diff --git a/sdk/python/tests/unit/infra/compute_engines/ray_compute/test_nodes.py b/sdk/python/tests/component/ray/test_nodes.py similarity index 75% rename from sdk/python/tests/unit/infra/compute_engines/ray_compute/test_nodes.py rename to sdk/python/tests/component/ray/test_nodes.py index 0da4fcb956e..983b40a5ccc 100644 --- a/sdk/python/tests/unit/infra/compute_engines/ray_compute/test_nodes.py +++ b/sdk/python/tests/component/ray/test_nodes.py @@ -299,6 +299,146 @@ def test_ray_dedup_node( assert "driver_id" in result_df.columns +def test_ray_dedup_node_materialization_within_block( + ray_session, ray_config, mock_context, column_info +): + """Materialization path: within-block duplicates are removed and the row + with the latest event_timestamp is kept. + + is_materialization=True uses per-block map_batches (streaming-safe). + No ds.schema() call should be triggered. + """ + now = datetime.now() + older_ts = now - timedelta(hours=3) + newer_ts = now - timedelta(hours=1) + + block = pd.DataFrame( + [ + { + "driver_id": 1001, + "event_timestamp": older_ts, + "conv_rate": 0.5, + }, + { + "driver_id": 1001, + "event_timestamp": newer_ts, + "conv_rate": 0.8, + }, + { + "driver_id": 1002, + "event_timestamp": now - timedelta(hours=2), + "conv_rate": 0.7, + }, + ] + ) + + ray_dataset = ray.data.from_pandas(block) + input_value = DAGValue(data=ray_dataset, format=DAGFormat.RAY) + dummy_node = DummyInputNode("input_node", input_value) + node = RayDedupNode( + name="dedup", + column_info=column_info, + config=ray_config, + is_materialization=True, + ) + node.add_input(dummy_node) + mock_context.node_outputs = {"input_node": input_value} + + result = node.execute(mock_context) + result_df = result.data.to_pandas().sort_values("driver_id").reset_index(drop=True) + + assert len(result_df) == 2, "One row per entity should survive within the block" + driver_1001 = result_df[result_df["driver_id"] == 1001].iloc[0] + assert driver_1001["event_timestamp"] == newer_ts, ( + "Latest timestamp should be kept for driver 1001" + ) + + +def test_ray_dedup_node_materialization_cross_block_duplicates_survive( + ray_session, ray_config, mock_context, column_info +): + """Materialization path: the same entity in two *different* blocks both + survive — cross-block dedup is delegated to the online-store UPSERT. + + This validates the per-block (streaming-safe) semantics: a global shuffle + is intentionally avoided so that slow upstream actors (EasyOCR, CLIP, etc.) + do not need to finish all blocks before writes begin. + """ + now = datetime.now() + block_a = pd.DataFrame( + [ + { + "driver_id": 1001, + "event_timestamp": now - timedelta(hours=3), + "conv_rate": 0.5, + } + ] + ) + block_b = pd.DataFrame( + [ + { + "driver_id": 1001, + "event_timestamp": now - timedelta(hours=1), + "conv_rate": 0.8, + } + ] + ) + + # Force two separate Ray blocks by passing a list of DataFrames. + ray_dataset = ray.data.from_pandas([block_a, block_b]) + input_value = DAGValue(data=ray_dataset, format=DAGFormat.RAY) + dummy_node = DummyInputNode("input_node", input_value) + node = RayDedupNode( + name="dedup", + column_info=column_info, + config=ray_config, + is_materialization=True, + ) + node.add_input(dummy_node) + mock_context.node_outputs = {"input_node": input_value} + + result = node.execute(mock_context) + result_df = result.data.to_pandas() + + assert len(result_df) == 2, ( + "Both blocks should each contribute one row; " + "cross-block dedup is the online store's responsibility" + ) + + +def test_ray_dedup_node_materialization_no_join_keys( + ray_session, ray_config, mock_context, sample_data +): + """Materialization path: when no join keys are present all rows pass through + unchanged (there is nothing to deduplicate on). + """ + empty_column_info = ColumnInfo( + join_keys=[], + feature_cols=["conv_rate", "acc_rate", "avg_daily_trips"], + ts_col="event_timestamp", + created_ts_col="created", + field_mapping=None, + ) + ray_dataset = ray.data.from_pandas(sample_data) + input_value = DAGValue(data=ray_dataset, format=DAGFormat.RAY) + dummy_node = DummyInputNode("input_node", input_value) + node = RayDedupNode( + name="dedup", + column_info=empty_column_info, + config=ray_config, + is_materialization=True, + ) + node.add_input(dummy_node) + mock_context.node_outputs = {"input_node": input_value} + + result = node.execute(mock_context) + result_df = result.data.to_pandas() + + assert len(result_df) == len(sample_data), ( + "All rows should survive when there are no join keys to deduplicate on" + ) + + def test_ray_config_validation(): """Test Ray configuration validation.""" # Test valid configuration diff --git a/sdk/python/tests/component/ray/test_ray_source.py b/sdk/python/tests/component/ray/test_ray_source.py new file mode 100644 index 00000000000..1ded06e5f04 --- /dev/null +++ b/sdk/python/tests/component/ray/test_ray_source.py @@ -0,0 +1,407 @@ +"""Unit tests for RaySource, load_ray_dataset_from_source, and to_ray_dataset. + +These tests run without a live Ray cluster using mocks and lightweight +in-process data, so they are fast and suitable for CI. + +Coverage: + - RaySource construction, validation, and proto round-trip + - load_ray_dataset_from_source reader dispatch (mocked ray_wrapper) + - RetrievalJob.to_ray_dataset() base-class Arrow fallback + - pull_latest_from_table_or_query time-range filtering for RaySource +""" + +from datetime import datetime +from unittest.mock import MagicMock, patch + +import pandas as pd +import pyarrow as pa +import pytest + +from feast.infra.offline_stores.contrib.ray_offline_store.ray_source import ( + SUPPORTED_READER_TYPES, + RaySource, +) + +# --------------------------------------------------------------------------- +# RaySource — construction and attribute access +# --------------------------------------------------------------------------- + + +class TestRaySourceConstruction: + def test_huggingface_source(self): + src = RaySource( + name="hf", + reader_type="huggingface", + reader_options={"dataset_name": "my/dataset", "split": "train"}, + ) + assert src.reader_type == "huggingface" + assert src.reader_options["dataset_name"] == "my/dataset" + assert src.path == "" + + def test_parquet_source(self): + src = RaySource( + name="parq", + reader_type="parquet", + path="s3://bucket/data.parquet", + ) + assert src.reader_type == "parquet" + assert src.path == "s3://bucket/data.parquet" + assert src.reader_options == {} + + def test_all_supported_reader_types_are_accepted(self): + for rt in SUPPORTED_READER_TYPES: + src = RaySource(name=f"src_{rt}", reader_type=rt, path="/tmp/x") + assert src.reader_type == rt + + def test_unsupported_reader_type_raises(self): + with pytest.raises(ValueError, match="reader_type"): + RaySource(name="bad", reader_type="unsupported_format") + + def test_timestamp_field_default(self): + src = RaySource(name="s", reader_type="csv", path="/tmp/f.csv") + assert src.get_table_column_names_and_types(MagicMock()) == [] + + +# --------------------------------------------------------------------------- +# RaySource — proto round-trip +# --------------------------------------------------------------------------- + + +class TestRaySourceProto: + def test_round_trip_preserves_reader_type_and_path(self): + original = RaySource( + name="proto_test", + reader_type="json", + path="gs://bucket/data.jsonl", + reader_options={"lines": True}, + ) + proto = original.to_proto() + restored = RaySource.from_proto(proto) + assert restored.reader_type == original.reader_type + assert restored.path == original.path + assert restored.reader_options == original.reader_options + + def test_round_trip_huggingface(self): + original = RaySource( + name="hf_proto", + reader_type="huggingface", + reader_options={"dataset_name": "foo/bar", "split": "train[:10]"}, + ) + restored = RaySource.from_proto(original.to_proto()) + assert restored.reader_options["dataset_name"] == "foo/bar" + assert restored.reader_options["split"] == "train[:10]" + + +# --------------------------------------------------------------------------- +# load_ray_dataset_from_source — reader dispatch (mocked wrapper) +# --------------------------------------------------------------------------- + + +class TestLoadRayDatasetFromSource: + """Each reader type is dispatched to the right ray_wrapper method.""" + + def _mock_wrapper(self): + w = MagicMock() + w.read_parquet.return_value = "parquet_ds" + w.read_csv.return_value = "csv_ds" + w.read_json.return_value = "json_ds" + w.read_text.return_value = "text_ds" + w.read_images.return_value = "images_ds" + w.read_binary_files.return_value = "binary_ds" + w.read_tfrecords.return_value = "tfrecords_ds" + w.read_webdataset.return_value = "webdataset_ds" + w.from_huggingface.return_value = "hf_ds" + w.read_mongo.return_value = "mongo_ds" + w.read_sql.return_value = "sql_ds" + return w + + @pytest.mark.parametrize( + "reader_type,path,expected", + [ + ("parquet", "s3://b/data.parquet", "parquet_ds"), + ("csv", "/tmp/data.csv", "csv_ds"), + ("json", "/tmp/data.json", "json_ds"), + ("text", "/tmp/data.txt", "text_ds"), + ("images", "/tmp/imgs/", "images_ds"), + ("binary_files", "/tmp/bin/", "binary_ds"), + ("tfrecords", "/tmp/data.tfrecord", "tfrecords_ds"), + ("webdataset", "/tmp/data.tar", "webdataset_ds"), + ], + ) + def test_file_readers_dispatch(self, reader_type, path, expected): + from feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader import ( + load_ray_dataset_from_source, + ) + + src = RaySource(name="s", reader_type=reader_type, path=path) + mock_wrapper = self._mock_wrapper() + with patch( + "feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader.get_ray_wrapper", + return_value=mock_wrapper, + ): + result = load_ray_dataset_from_source(src) + assert result == expected + + def test_huggingface_dispatch(self): + import sys + + from feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader import ( + load_ray_dataset_from_source, + ) + + src = RaySource( + name="hf", + reader_type="huggingface", + reader_options={"dataset_name": "org/ds", "split": "train"}, + ) + mock_wrapper = self._mock_wrapper() + mock_hf_dataset = MagicMock() + + # Inject a fake `datasets` module so `from datasets import load_dataset` + # inside load_ray_dataset_from_source succeeds without the real package. + fake_datasets = MagicMock() + fake_datasets.load_dataset.return_value = mock_hf_dataset + + with ( + patch.dict(sys.modules, {"datasets": fake_datasets}), + patch( + "feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader.get_ray_wrapper", + return_value=mock_wrapper, + ), + ): + load_ray_dataset_from_source(src) + + mock_wrapper.from_huggingface.assert_called_once() + + def test_unknown_reader_type_raises(self): + from feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader import ( + load_ray_dataset_from_source, + ) + + # Bypass RaySource construction-time validation by patching the + # underlying RaySourceOptions directly (reader_type is a read-only + # property on RaySource that delegates to ray_source_options). + src = RaySource(name="s", reader_type="parquet", path="/x") + src.ray_source_options.reader_type = "not_a_thing" + mock_wrapper = self._mock_wrapper() + with patch( + "feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader.get_ray_wrapper", + return_value=mock_wrapper, + ): + with pytest.raises(ValueError, match="Unknown reader_type"): + load_ray_dataset_from_source(src) + + +# --------------------------------------------------------------------------- +# RetrievalJob.to_ray_dataset() — base-class Arrow fallback +# --------------------------------------------------------------------------- + + +class TestRetrievalJobToRayDataset: + """The base-class default converts via to_arrow() → ray.data.from_arrow().""" + + def _make_job(self, arrow_table: pa.Table): + """Create a minimal concrete RetrievalJob whose _to_arrow_internal returns arrow_table.""" + from feast.infra.offline_stores.offline_store import RetrievalJob + + class _ConcreteJob(RetrievalJob): + @property + def full_feature_names(self): + return False + + @property + def on_demand_feature_views(self): + return [] + + def _to_df_internal(self, timeout=None): + return arrow_table.to_pandas() + + def _to_arrow_internal(self, timeout=None): + return arrow_table + + return _ConcreteJob() + + def test_returns_ray_dataset_with_correct_rows(self): + pytest.importorskip("ray") + table = pa.table({"a": [1, 2, 3], "b": ["x", "y", "z"]}) + job = self._make_job(table) + ds = job.to_ray_dataset() + assert ds.count() == 3 + + def test_import_error_without_ray(self): + import sys + + table = pa.table({"a": [1]}) + job = self._make_job(table) + with patch.dict(sys.modules, {"ray": None, "ray.data": None}): + with pytest.raises(ImportError, match="Ray is required"): + job.to_ray_dataset() + + def test_ray_retrieval_job_overrides_base(self): + """RayRetrievalJob.to_ray_dataset() must not call the Arrow fallback.""" + from feast.infra.offline_stores.contrib.ray_offline_store.ray import ( + RayRetrievalJob, + ) + from feast.infra.offline_stores.offline_store import RetrievalJob + + base_impl = RetrievalJob.to_ray_dataset + ray_impl = RayRetrievalJob.__dict__.get("to_ray_dataset") + assert ray_impl is not None, ( + "RayRetrievalJob must define its own to_ray_dataset" + ) + assert ray_impl is not base_impl + + +# --------------------------------------------------------------------------- +# get_historical_features().to_ray_dataset() +# --------------------------------------------------------------------------- + + +class TestGetHistoricalFeaturesToRayDataset: + """Callers chain get_historical_features().to_ray_dataset() directly. + + FeatureStore has no separate to_ray_dataset() wrapper; to_ray_dataset() is + a first-class method on the RetrievalJob returned by get_historical_features(). + """ + + def test_chain_calls_to_ray_dataset_on_job(self): + mock_job = MagicMock() + sentinel = object() + mock_job.to_ray_dataset.return_value = sentinel + + store = MagicMock() + store.get_historical_features.return_value = mock_job + + entity_df = pd.DataFrame( + {"driver_id": [1], "event_timestamp": [datetime.now()]} + ) + result = store.get_historical_features( + features=["driver_stats:conv_rate"], + entity_df=entity_df, + ).to_ray_dataset() + + mock_job.to_ray_dataset.assert_called_once() + assert result is sentinel + + +# --------------------------------------------------------------------------- +# pull_latest_from_table_or_query — RaySource time-range filter and dedup +# --------------------------------------------------------------------------- + + +class TestPullLatestRaySourceFiltering: + """Verify that pull_latest_from_table_or_query applies time-range filtering + and deduplication for file-backed RaySource (e.g. reader_type="parquet"). + + RaySource without a timestamp_field (exotic sources such as HuggingFace + image datasets) must still be returned raw. + """ + + def _make_source(self, reader_type: str = "parquet") -> RaySource: + return RaySource( + name="test_src", + reader_type=reader_type, + path="s3://bucket/data.parquet", + timestamp_field="event_timestamp", + ) + + def test_load_and_filter_dataset_ray_pre_loaded(self): + """_load_and_filter_dataset_ray(pre_loaded_ds=...) must not raise and + must apply the shared field-mapping / normalise / batch pipeline.""" + from datetime import timezone + + from feast.infra.offline_stores.contrib.ray_offline_store.ray import ( + RayOfflineStore, + ) + + start = datetime(2024, 1, 2, tzinfo=timezone.utc) + end = datetime(2024, 1, 4, tzinfo=timezone.utc) + + rows = pa.table( + { + "driver_id": [1, 1, 1], + "event_timestamp": pa.array( + [ + datetime(2024, 1, 1, tzinfo=timezone.utc), # before window + datetime(2024, 1, 3, tzinfo=timezone.utc), # inside window + datetime(2024, 1, 5, tzinfo=timezone.utc), # after window + ], + type=pa.timestamp("us", tz="UTC"), + ), + "feature_a": [10, 20, 30], + } + ) + + src = self._make_source() + + with ( + patch( + "feast.infra.offline_stores.contrib.ray_offline_store.ray.normalize_timestamp_columns" + ) as mock_norm, + patch( + "feast.infra.offline_stores.contrib.ray_offline_store.ray.apply_field_mapping" + ) as mock_map, + ): + import ray.data as rd + + mock_ds = rd.from_arrow(rows) + mock_norm.return_value = mock_ds + mock_map.return_value = mock_ds + + # We call the static method directly; it should not raise. + # Full integration (filter + sort) is verified in component tests. + store = RayOfflineStore() + _ = store._load_and_filter_dataset_ray( + None, + src, + join_key_columns=["driver_id"], + feature_name_columns=["feature_a"], + timestamp_field="event_timestamp", + created_timestamp_column=None, + start_date=start, + end_date=end, + pre_loaded_ds=mock_ds, + ) + + def test_pull_latest_raw_for_source_without_timestamp(self): + """When timestamp_field is empty, RaySource data must be returned raw.""" + src = RaySource( + name="hf_src", + reader_type="huggingface", + reader_options={"dataset_name": "cheques_sample_data"}, + ) + + raw_sentinel = object() + + with ( + patch( + "feast.infra.offline_stores.contrib.ray_offline_store.ray.RayOfflineStore._init_ray" + ), + patch( + "feast.infra.offline_stores.contrib.ray_offline_store.ray_offline_store_reader.load_ray_dataset_from_source", + return_value=raw_sentinel, + ), + patch( + "feast.infra.offline_stores.contrib.ray_offline_store.ray.RayRetrievalJob" + ) as mock_job_cls, + ): + from feast.infra.offline_stores.contrib.ray_offline_store.ray import ( + RayOfflineStore, + ) + + mock_config = MagicMock() + mock_config.offline_store.storage_path = "/tmp/staging" + + RayOfflineStore.pull_latest_from_table_or_query( + config=mock_config, + data_source=src, + join_key_columns=[], + feature_name_columns=["feature_a"], + timestamp_field="", + created_timestamp_column=None, + start_date=datetime(2024, 1, 1), + end_date=datetime(2024, 1, 31), + ) + + # A RayRetrievalJob must still be created (just wrapping raw data). + mock_job_cls.assert_called_once() diff --git a/sdk/python/tests/component/ray/test_resource_scheduling.py b/sdk/python/tests/component/ray/test_resource_scheduling.py new file mode 100644 index 00000000000..7e26b72203a --- /dev/null +++ b/sdk/python/tests/component/ray/test_resource_scheduling.py @@ -0,0 +1,621 @@ +""" +Tests for GPU and worker resource scheduling support in the Ray compute engine +and offline store. + +Covers: +- RayComputeEngineConfig / RayOfflineStoreConfig: worker_task_options + num_gpus fields +- CodeFlareRayWrapper._get_task_options(): merge logic, num_gpus precedence +- RemoteDatasetProxy.map_batches: .options() applied, map_batches key filtering +- RayTransformationNode: worker_task_options threaded through map_batches +- RayResourceManager: GPU count read from cluster resources +- safe_batch_processor / _is_empty_batch: format-aware empty detection (regression + for AttributeError when gpu_batch_format is "numpy" or "pyarrow") +""" + +from datetime import datetime, timedelta +from unittest.mock import patch + +import numpy as np +import pandas as pd +import pyarrow as pa +import pytest +import ray + +from feast.infra.compute_engines.dag.model import DAGFormat +from feast.infra.compute_engines.dag.node import DAGNode +from feast.infra.compute_engines.dag.value import DAGValue +from feast.infra.compute_engines.ray.config import RayComputeEngineConfig +from feast.infra.compute_engines.ray.nodes import RayTransformationNode +from feast.infra.compute_engines.ray.utils import _is_empty_batch, safe_batch_processor +from feast.infra.offline_stores.contrib.ray_offline_store.ray import ( + RayOfflineStoreConfig, + RayResourceManager, +) +from feast.infra.ray_initializer import CodeFlareRayWrapper +from feast.infra.ray_shared_utils import RemoteDatasetProxy + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class DummyInputNode(DAGNode): + def __init__(self, name, output): + super().__init__(name) + self._output = output + + def execute(self, context): + return self._output + + +@pytest.fixture(scope="module") +def ray_session(): + if not ray.is_initialized(): + ray.init(num_cpus=2, ignore_reinit_error=True, include_dashboard=False) + yield ray + ray.shutdown() + + +@pytest.fixture +def sample_df(): + return pd.DataFrame( + { + "driver_id": [1, 2, 3], + "conv_rate": [0.8, 0.7, 0.6], + "event_timestamp": [datetime.now() - timedelta(hours=i) for i in range(3)], + } + ) + + +# --------------------------------------------------------------------------- +# Config field tests +# --------------------------------------------------------------------------- + + +class TestRayComputeEngineConfigTaskOptions: + def test_defaults(self): + config = RayComputeEngineConfig() + assert config.num_gpus is None + assert config.gpu_batch_format == "pandas" + assert config.worker_task_options is None + + def test_num_gpus_set(self): + config = RayComputeEngineConfig(num_gpus=1) + assert config.num_gpus == 1 + + def test_fractional_num_gpus(self): + config = RayComputeEngineConfig(num_gpus=0.5) + assert config.num_gpus == 0.5 + + def test_gpu_batch_format(self): + config = RayComputeEngineConfig(num_gpus=1, gpu_batch_format="numpy") + assert config.gpu_batch_format == "numpy" + + def test_worker_task_options_roundtrip(self): + opts = { + "num_cpus": 4, + "memory": 8 * 1024**3, + "accelerator_type": "A100", + "max_retries": 5, + } + config = RayComputeEngineConfig(worker_task_options=opts) + assert config.worker_task_options["accelerator_type"] == "A100" + assert config.worker_task_options["num_cpus"] == 4 + assert config.worker_task_options["max_retries"] == 5 + + def test_worker_task_options_with_runtime_env(self): + config = RayComputeEngineConfig( + worker_task_options={ + "runtime_env": { + "pip": ["cudf-cu12==24.10.0"], + "env_vars": {"CUDA_VISIBLE_DEVICES": "0"}, + } + } + ) + assert config.worker_task_options["runtime_env"]["pip"] == [ + "cudf-cu12==24.10.0" + ] + + +class TestRayOfflineStoreConfigTaskOptions: + def test_defaults(self): + config = RayOfflineStoreConfig() + assert config.num_gpus is None + assert config.gpu_batch_format == "pandas" + assert config.worker_task_options is None + + def test_worker_task_options_roundtrip(self): + config = RayOfflineStoreConfig( + worker_task_options={"num_cpus": 2, "accelerator_type": "T4"} + ) + assert config.worker_task_options["num_cpus"] == 2 + assert config.worker_task_options["accelerator_type"] == "T4" + + +# --------------------------------------------------------------------------- +# CodeFlareRayWrapper._get_task_options() tests +# --------------------------------------------------------------------------- + + +class TestCodeFlareRayWrapperGetTaskOptions: + """Unit tests for _get_task_options() merge logic — no cluster connection needed.""" + + def _make_wrapper(self, num_gpus=0, worker_task_options=None): + """Construct a wrapper instance without triggering __init__ side-effects.""" + wrapper = CodeFlareRayWrapper.__new__(CodeFlareRayWrapper) + wrapper.num_gpus = num_gpus + wrapper.worker_task_options = worker_task_options or {} + return wrapper + + def test_empty_when_no_resources(self): + wrapper = self._make_wrapper() + assert wrapper._get_task_options() == {} + + def test_io_tasks_exclude_num_gpus_by_default(self): + """include_gpu defaults to False so I/O methods never consume GPU slots.""" + wrapper = self._make_wrapper(num_gpus=1) + opts = wrapper._get_task_options() # default: include_gpu=False + assert "num_gpus" not in opts + + def test_io_tasks_strip_num_gpus_from_worker_task_options(self): + """ + Regression: num_gpus set inside worker_task_options must also be stripped + for I/O tasks. Previously _get_task_options only blocked the first-class + num_gpus field; a num_gpus key already present in the copied + worker_task_options dict would leak through to .options() on I/O tasks. + """ + wrapper = self._make_wrapper( + num_gpus=0, # first-class field not set + worker_task_options={"num_gpus": 1, "num_cpus": 4}, + ) + opts = wrapper._get_task_options() # default: include_gpu=False + assert "num_gpus" not in opts + assert opts.get("num_cpus") == 4 # other keys unaffected + + def test_num_gpus_added_when_include_gpu_true(self): + """include_gpu=True is used for compute tasks that actually need GPUs.""" + wrapper = self._make_wrapper(num_gpus=1) + assert wrapper._get_task_options(include_gpu=True) == {"num_gpus": 1} + + def test_worker_task_options_passthrough(self): + wrapper = self._make_wrapper( + worker_task_options={"num_cpus": 4, "accelerator_type": "A100"} + ) + opts = wrapper._get_task_options() + assert opts["num_cpus"] == 4 + assert opts["accelerator_type"] == "A100" + + def test_num_gpus_takes_precedence_over_worker_task_options(self): + """First-class num_gpus must override worker_task_options['num_gpus'].""" + wrapper = self._make_wrapper(num_gpus=2, worker_task_options={"num_gpus": 1}) + opts = wrapper._get_task_options(include_gpu=True) + assert opts["num_gpus"] == 2 + + def test_combined_num_gpus_and_worker_task_options(self): + wrapper = self._make_wrapper( + num_gpus=1, + worker_task_options={ + "num_cpus": 4, + "memory": 8 * 1024**3, + "max_retries": 5, + }, + ) + opts = wrapper._get_task_options(include_gpu=True) + assert opts["num_gpus"] == 1 + assert opts["num_cpus"] == 4 + assert opts["memory"] == 8 * 1024**3 + assert opts["max_retries"] == 5 + + def test_zero_num_gpus_not_added(self): + """num_gpus=0 (falsy) must not be added to options.""" + wrapper = self._make_wrapper(num_gpus=0, worker_task_options={"num_cpus": 2}) + opts = wrapper._get_task_options(include_gpu=True) + assert "num_gpus" not in opts + assert opts["num_cpus"] == 2 + + +# --------------------------------------------------------------------------- +# RemoteDatasetProxy.map_batches resource key filtering +# --------------------------------------------------------------------------- + + +class TestRemoteDatasetProxyMapBatches: + """ + Verifies that map_batches applies .options() correctly and only forwards + the scheduling-relevant subset of keys into the Ray Data map_batches call. + """ + + def test_no_options_when_empty(self, ray_session, sample_df): + dataset = ray.data.from_pandas(sample_df) + proxy = RemoteDatasetProxy(ray.put(dataset)) + + with patch.object( + ray.remote(lambda d, f, b, r: d.map_batches(f, **b, **r)), + "options", + wraps=lambda **kw: None, + ): + result = proxy.map_batches(lambda b: b, batch_format="pandas") + assert isinstance(result, RemoteDatasetProxy) + + def test_map_batches_resource_keys_filtered(self): + """ + The filtering set inside map_batches must pass only scheduling-relevant + keys (num_gpus, num_cpus, accelerator_type, resources) to Ray Data's + map_batches, and strip non-scheduling keys (max_retries, runtime_env, + memory, scheduling_strategy). + """ + _MAP_BATCHES_RESOURCE_KEYS = { + "num_gpus", + "num_cpus", + "accelerator_type", + "resources", + } + + all_task_options = { + "num_gpus": 1, + "num_cpus": 4, + "accelerator_type": "A100", + "resources": {"custom": 1}, + "max_retries": 5, + "runtime_env": {"pip": ["cudf"]}, + "memory": 8 * 1024**3, + "scheduling_strategy": "SPREAD", + } + + map_resource_kwargs = { + k: v for k, v in all_task_options.items() if k in _MAP_BATCHES_RESOURCE_KEYS + } + + # Should-be-present keys + assert "num_gpus" in map_resource_kwargs + assert "num_cpus" in map_resource_kwargs + assert "accelerator_type" in map_resource_kwargs + assert "resources" in map_resource_kwargs + + # Should-be-absent keys + assert "max_retries" not in map_resource_kwargs + assert "runtime_env" not in map_resource_kwargs + assert "memory" not in map_resource_kwargs + assert "scheduling_strategy" not in map_resource_kwargs + + def test_orchestration_task_excludes_compute_keys(self): + """ + The @ray.remote orchestration wrapper must never hold compute-scheduling + resources (num_gpus, num_cpus, accelerator_type, resources). Only + non-scheduling keys (runtime_env, max_retries, memory, etc.) should + reach the orchestration task via .options(), avoiding the deadlock + where the orchestrator holds a GPU slot while waiting for data workers + that also need GPUs. + """ + _MAP_BATCHES_RESOURCE_KEYS = { + "num_gpus", + "num_cpus", + "accelerator_type", + "resources", + } + + all_opts = { + "num_gpus": 1, + "num_cpus": 4, + "accelerator_type": "A100", + "resources": {"custom": 1}, + "max_retries": 5, + "runtime_env": {"pip": ["cudf"]}, + "memory": 8 * 1024**3, + "scheduling_strategy": "SPREAD", + } + + orchestration_opts = { + k: v for k, v in all_opts.items() if k not in _MAP_BATCHES_RESOURCE_KEYS + } + + # Compute-scheduling keys must be absent from the orchestration task + assert "num_gpus" not in orchestration_opts + assert "num_cpus" not in orchestration_opts + assert "accelerator_type" not in orchestration_opts + assert "resources" not in orchestration_opts + + # Non-scheduling keys must be present (orchestration task can use them) + assert orchestration_opts["max_retries"] == 5 + assert orchestration_opts["runtime_env"] == {"pip": ["cudf"]} + assert orchestration_opts["memory"] == 8 * 1024**3 + assert orchestration_opts["scheduling_strategy"] == "SPREAD" + + +# --------------------------------------------------------------------------- +# RayTransformationNode — task_options and num_gpus threaded through +# --------------------------------------------------------------------------- + + +class TestRayTransformationNodeResourceScheduling: + @pytest.fixture + def mock_context(self): + class DummyOfflineStore: + def offline_write_batch(self, *args, **kwargs): + pass + + class Ctx: + registry = None + store = None + project = "test_project" + entity_data = None + config = None + node_outputs = {} + offline_store = DummyOfflineStore() + + return Ctx() + + def test_transformation_with_worker_task_options_executes( + self, ray_session, mock_context, sample_df + ): + """Node with worker_task_options runs end-to-end and produces correct output.""" + config = RayComputeEngineConfig( + max_workers=2, + worker_task_options={"num_cpus": 1}, + ) + dataset = ray.data.from_pandas(sample_df) + input_value = DAGValue(data=dataset, format=DAGFormat.RAY) + input_node = DummyInputNode("inp", input_value) + + def double_conv(df: pd.DataFrame) -> pd.DataFrame: + df["conv_rate"] = df["conv_rate"] * 2 + return df + + node = RayTransformationNode( + name="t", transformation=double_conv, config=config + ) + node.add_input(input_node) + mock_context.node_outputs = {"inp": input_value} + + result = node.execute(mock_context) + result_df = result.data.to_pandas() + assert len(result_df) == 3 + assert ( + abs(result_df["conv_rate"].iloc[0] - sample_df["conv_rate"].iloc[0] * 2) + < 1e-6 + ) + + def test_transformation_gpu_batch_format_applied( + self, ray_session, mock_context, sample_df + ): + """ + When num_gpus is set, gpu_batch_format is passed as batch_format and + num_gpus is included in the map_batches kwargs. + + The UDF receives a numpy dict (Dict[str, np.ndarray]) when + batch_format="numpy". safe_batch_processor must handle this without + raising AttributeError on .empty — this test is the regression guard. + """ + config = RayComputeEngineConfig( + max_workers=2, + num_gpus=1, + gpu_batch_format="numpy", + ) + dataset = ray.data.from_pandas(sample_df) + input_value = DAGValue(data=dataset, format=DAGFormat.RAY) + input_node = DummyInputNode("inp", input_value) + + # UDF receives Dict[str, np.ndarray] when batch_format="numpy" + def numpy_identity(batch): + return batch + + node = RayTransformationNode( + name="t", transformation=numpy_identity, config=config + ) + node.add_input(input_node) + mock_context.node_outputs = {"inp": input_value} + + captured_kwargs = {} + original_map_batches = ray.data.Dataset.map_batches + + def capturing_map_batches(self_ds, fn, **kwargs): + captured_kwargs.update(kwargs) + # Strip GPU-scheduling keys so local Ray (no GPU nodes, no batch_size + # requirement) can execute without raising ValueError. The correctness + # of safe_batch_processor on numpy dicts is covered by + # TestSafeBatchProcessorFormats — this test verifies config flow only. + safe_kwargs = {k: v for k, v in kwargs.items() if k not in ("num_gpus",)} + safe_kwargs.setdefault("batch_format", "pandas") + return original_map_batches(self_ds, fn, **safe_kwargs) + + with patch.object(ray.data.Dataset, "map_batches", capturing_map_batches): + node.execute(mock_context) + + assert captured_kwargs.get("batch_format") == "numpy" + assert captured_kwargs.get("num_gpus") == 1 + + def test_transformation_no_resource_keys_when_no_options( + self, ray_session, mock_context, sample_df + ): + """With no GPU/worker_task_options, map_batches only gets batch_format + concurrency.""" + config = RayComputeEngineConfig(max_workers=2) + dataset = ray.data.from_pandas(sample_df) + input_value = DAGValue(data=dataset, format=DAGFormat.RAY) + input_node = DummyInputNode("inp", input_value) + node = RayTransformationNode( + name="t", transformation=lambda df: df, config=config + ) + node.add_input(input_node) + mock_context.node_outputs = {"inp": input_value} + + captured_kwargs = {} + original_map_batches = ray.data.Dataset.map_batches + + def spy(self_ds, fn, **kwargs): + captured_kwargs.update(kwargs) + return original_map_batches(self_ds, fn, **kwargs) + + with patch.object(ray.data.Dataset, "map_batches", spy): + node.execute(mock_context) + + assert "num_gpus" not in captured_kwargs + assert "num_cpus" not in captured_kwargs + assert "accelerator_type" not in captured_kwargs + assert captured_kwargs.get("batch_format") == "pandas" + + +# --------------------------------------------------------------------------- +# RayResourceManager — GPU count from cluster resources +# --------------------------------------------------------------------------- + + +class TestRayResourceManagerGPU: + def test_reads_gpu_from_cluster_resources(self): + with ( + patch("ray.is_initialized", return_value=True), + patch( + "ray.cluster_resources", + return_value={"CPU": 8, "GPU": 4, "memory": 32 * 1024**3}, + ), + patch("ray.nodes", return_value=[{}, {}, {}, {}]), + ): + mgr = RayResourceManager() + assert mgr.available_gpus == 4 + assert mgr.available_cpus == 8 + assert mgr.num_nodes == 4 + + def test_available_gpus_zero_when_no_gpus(self): + with ( + patch("ray.is_initialized", return_value=True), + patch( + "ray.cluster_resources", + return_value={"CPU": 4, "memory": 8 * 1024**3}, + ), + patch("ray.nodes", return_value=[{}]), + ): + mgr = RayResourceManager() + assert mgr.available_gpus == 0 + + def test_available_gpus_zero_when_ray_not_initialized(self): + with patch("ray.is_initialized", return_value=False): + mgr = RayResourceManager() + assert mgr.available_gpus == 0 + + +# --------------------------------------------------------------------------- +# safe_batch_processor / _is_empty_batch — format-aware empty detection +# Regression tests for AttributeError when gpu_batch_format is "numpy"/"pyarrow" +# --------------------------------------------------------------------------- + + +class TestIsEmptyBatch: + """Unit tests for _is_empty_batch() across all Ray Data batch formats.""" + + def test_pandas_non_empty(self, sample_df): + assert _is_empty_batch(sample_df) is False + + def test_pandas_empty(self): + assert _is_empty_batch(pd.DataFrame()) is True + + def test_numpy_non_empty(self): + batch = {"col_a": np.array([1, 2, 3]), "col_b": np.array([4, 5, 6])} + assert _is_empty_batch(batch) is False + + def test_numpy_empty_arrays(self): + batch = {"col_a": np.array([]), "col_b": np.array([])} + assert _is_empty_batch(batch) is True + + def test_numpy_empty_dict(self): + assert _is_empty_batch({}) is True + + def test_pyarrow_non_empty(self, sample_df): + table = pa.Table.from_pandas(sample_df) + assert _is_empty_batch(table) is False + + def test_pyarrow_empty(self): + table = pa.table({"col_a": pa.array([], type=pa.int64())}) + assert _is_empty_batch(table) is True + + +class TestSafeBatchProcessorFormats: + """ + Regression tests: safe_batch_processor must not raise AttributeError + when receiving non-pandas batches (numpy dict or pyarrow Table). + """ + + def test_pandas_batch_passes_through(self, sample_df): + @safe_batch_processor + def double_conv(batch: pd.DataFrame) -> pd.DataFrame: + batch = batch.copy() + batch["conv_rate"] = batch["conv_rate"] * 2 + return batch + + result = double_conv(sample_df) + assert isinstance(result, pd.DataFrame) + assert result["conv_rate"].iloc[0] == pytest.approx( + sample_df["conv_rate"].iloc[0] * 2 + ) + + def test_pandas_empty_batch_returned_early(self): + called = [] + + @safe_batch_processor + def should_not_run(batch): + called.append(True) + return batch + + result = should_not_run(pd.DataFrame()) + assert called == [] + assert isinstance(result, pd.DataFrame) + + def test_numpy_batch_no_attribute_error(self): + """Regression: was raising AttributeError on batch.empty.""" + + @safe_batch_processor + def numpy_passthrough(batch): + return batch + + batch = { + "driver_id": np.array([1, 2, 3]), + "conv_rate": np.array([0.8, 0.7, 0.6]), + } + # Must not raise AttributeError + result = numpy_passthrough(batch) + assert result is batch + + def test_numpy_empty_batch_returned_early(self): + called = [] + + @safe_batch_processor + def should_not_run(batch): + called.append(True) + return batch + + empty_batch = {"col": np.array([])} + result = should_not_run(empty_batch) + assert called == [] + assert result is empty_batch + + def test_pyarrow_batch_no_attribute_error(self, sample_df): + """Regression: was raising AttributeError on batch.empty.""" + + @safe_batch_processor + def pyarrow_passthrough(batch): + return batch + + table = pa.Table.from_pandas(sample_df) + result = pyarrow_passthrough(table) + assert result is table + + def test_pyarrow_empty_batch_returned_early(self): + called = [] + + @safe_batch_processor + def should_not_run(batch): + called.append(True) + return batch + + empty_table = pa.table({"col": pa.array([], type=pa.int64())}) + result = should_not_run(empty_table) + assert called == [] + assert result is empty_table + + def test_exception_in_func_returns_original_batch(self): + @safe_batch_processor + def always_raises(batch): + raise ValueError("simulated error") + + df = pd.DataFrame({"x": [1, 2]}) + result = always_raises(df) + # Should return the original batch, not propagate + assert result is df diff --git a/sdk/python/tests/integration/compute_engines/ray_compute/test_source_feature_views.py b/sdk/python/tests/component/ray/test_source_feature_views.py similarity index 99% rename from sdk/python/tests/integration/compute_engines/ray_compute/test_source_feature_views.py rename to sdk/python/tests/component/ray/test_source_feature_views.py index 7d8f23e1bf6..73f64c67dbe 100644 --- a/sdk/python/tests/integration/compute_engines/ray_compute/test_source_feature_views.py +++ b/sdk/python/tests/component/ray/test_source_feature_views.py @@ -10,7 +10,7 @@ MaterializationJobStatus, ) from feast.types import Float32, Int32, Int64 -from tests.integration.compute_engines.ray_compute.ray_shared_utils import ( +from tests.component.ray.ray_shared_utils import ( create_entity_df, create_feature_dataset, create_unique_sink_source, diff --git a/sdk/python/tests/component/spark/.gitkeep b/sdk/python/tests/component/spark/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/component/spark/__init__.py b/sdk/python/tests/component/spark/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/component/spark/conftest.py b/sdk/python/tests/component/spark/conftest.py new file mode 100644 index 00000000000..8bcd1772296 --- /dev/null +++ b/sdk/python/tests/component/spark/conftest.py @@ -0,0 +1,23 @@ +import pytest +from pyspark.sql import SparkSession + + +@pytest.fixture(scope="session") +def spark_session(): + spark = ( + SparkSession.builder.appName("FeastSparkTests") + .master("local[*]") + .config("spark.sql.shuffle.partitions", "1") + .config("spark.driver.host", "127.0.0.1") + .config("spark.driver.bindAddress", "127.0.0.1") + .getOrCreate() + ) + + yield spark + + spark.stop() + + +@pytest.fixture +def spark_fixture(spark_session): + yield spark_session diff --git a/sdk/python/tests/integration/compute_engines/spark/test_compute.py b/sdk/python/tests/component/spark/test_compute.py similarity index 99% rename from sdk/python/tests/integration/compute_engines/spark/test_compute.py rename to sdk/python/tests/component/spark/test_compute.py index e0855ae31f3..803cd505513 100644 --- a/sdk/python/tests/integration/compute_engines/spark/test_compute.py +++ b/sdk/python/tests/component/spark/test_compute.py @@ -19,7 +19,7 @@ SparkOfflineStore, ) from feast.types import Float32, Int32, Int64 -from tests.integration.compute_engines.spark.utils import ( +from tests.component.spark.utils import ( _check_offline_features, _check_online_features, create_entity_df, diff --git a/sdk/python/tests/integration/compute_engines/spark/test_compute_dag.py b/sdk/python/tests/component/spark/test_compute_dag.py similarity index 99% rename from sdk/python/tests/integration/compute_engines/spark/test_compute_dag.py rename to sdk/python/tests/component/spark/test_compute_dag.py index 24277d9a323..04b8a0b2edf 100644 --- a/sdk/python/tests/integration/compute_engines/spark/test_compute_dag.py +++ b/sdk/python/tests/component/spark/test_compute_dag.py @@ -19,7 +19,7 @@ SparkSource, ) from feast.types import Float32, Int32, Int64 -from tests.integration.compute_engines.spark.utils import ( +from tests.component.spark.utils import ( _check_offline_features, _check_online_features, create_entity_df, diff --git a/sdk/python/tests/unit/infra/compute_engines/spark/test_nodes.py b/sdk/python/tests/component/spark/test_nodes.py similarity index 93% rename from sdk/python/tests/unit/infra/compute_engines/spark/test_nodes.py rename to sdk/python/tests/component/spark/test_nodes.py index 0fb2bd4cb78..c8ed1157d86 100644 --- a/sdk/python/tests/unit/infra/compute_engines/spark/test_nodes.py +++ b/sdk/python/tests/component/spark/test_nodes.py @@ -1,9 +1,6 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock -import pytest -from pyspark.sql import SparkSession - from feast.aggregation import Aggregation from feast.infra.compute_engines.dag.context import ColumnInfo, ExecutionContext from feast.infra.compute_engines.dag.model import DAGFormat @@ -19,22 +16,6 @@ ) -@pytest.fixture(scope="session") -def spark_session(): - spark = ( - SparkSession.builder.appName("FeastSparkTests") - .master("local[*]") - .config("spark.sql.shuffle.partitions", "1") - .config("spark.driver.host", "127.0.0.1") - .config("spark.driver.bindAddress", "127.0.0.1") - .getOrCreate() - ) - - yield spark - - spark.stop() - - def test_spark_transformation_node_executes_udf(spark_session): # Sample Spark input df = spark_session.createDataFrame( @@ -113,6 +94,7 @@ def test_spark_aggregation_node_executes_correctly(spark_session): aggregations=agg_specs, group_by_keys=["user_id"], timestamp_col="", + spark_session=spark_session, ) node.add_input(MagicMock()) node.inputs[0].name = "source" diff --git a/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark.py b/sdk/python/tests/component/spark/test_spark.py similarity index 52% rename from sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark.py rename to sdk/python/tests/component/spark/test_spark.py index 938514a2ca0..fbbba055e56 100644 --- a/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark.py +++ b/sdk/python/tests/component/spark/test_spark.py @@ -1,13 +1,17 @@ +import os from datetime import datetime from unittest.mock import MagicMock, patch import pandas as pd +import pyarrow as pa from feast.entity import Entity from feast.feature_view import FeatureView, Field +from feast.infra.offline_stores.contrib.spark_offline_store import spark as spark_module from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( SparkOfflineStore, SparkOfflineStoreConfig, + SparkRetrievalJob, ) from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import ( SparkSource, @@ -339,3 +343,372 @@ def _mock_entity(): value_type=ValueType.INT64, ) ] + + +@patch( + "feast.infra.offline_stores.contrib.spark_offline_store.spark.get_spark_session_or_start_new_with_repoconfig" +) +def test_get_historical_features_non_entity_with_date_range(mock_get_spark_session): + mock_spark_session = MagicMock() + # Return a DataFrame for any sql call; last call is used by RetrievalJob + final_df = MagicMock() + expected_pdf = pd.DataFrame([{"feature1": 1.0, "feature2": 2.0}]) + final_df.toPandas.return_value = expected_pdf + mock_spark_session.sql.return_value = final_df + mock_get_spark_session.return_value = mock_spark_session + + test_repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=SparkOfflineStoreConfig(type="spark"), + ) + + test_data_source1 = SparkSource( + name="test_nested_batch_source1", + description="test_nested_batch_source", + table="offline_store_database_name.offline_store_table_name1", + timestamp_field="nested_timestamp", + field_mapping={ + "event_header.event_published_datetime_utc": "nested_timestamp", + }, + date_partition_column="effective_date", + date_partition_column_format="%Y%m%d", + ) + + test_data_source2 = SparkSource( + name="test_nested_batch_source2", + description="test_nested_batch_source", + table="offline_store_database_name.offline_store_table_name2", + timestamp_field="nested_timestamp", + field_mapping={ + "event_header.event_published_datetime_utc": "nested_timestamp", + }, + date_partition_column="effective_date", + ) + + test_feature_view1 = FeatureView( + name="test_feature_view1", + entities=_mock_entity(), + schema=[ + Field(name="feature1", dtype=Float32), + ], + source=test_data_source1, + ) + + test_feature_view2 = FeatureView( + name="test_feature_view2", + entities=_mock_entity(), + schema=[ + Field(name="feature2", dtype=Float32), + ], + source=test_data_source2, + ) + + mock_registry = MagicMock() + start_date = datetime(2021, 1, 1) + end_date = datetime(2021, 1, 2) + retrieval_job = SparkOfflineStore.get_historical_features( + config=test_repo_config, + feature_views=[test_feature_view2, test_feature_view1], + feature_refs=["test_feature_view2:feature2", "test_feature_view1:feature1"], + entity_df=None, + registry=mock_registry, + project="test_project", + start_date=start_date, + end_date=end_date, + ) + + # Verify query bounded by end_date correctly in both date formats from the two sources + query = retrieval_job.query + assert "effective_date <= '2021-01-02'" in query + assert "effective_date <= '20210102'" in query + + # Verify data: the mocked Spark DataFrame flows through to Pandas + pdf = retrieval_job._to_df_internal() + assert pdf.equals(expected_pdf) + + +@patch( + "feast.infra.offline_stores.contrib.spark_offline_store.spark.get_spark_session_or_start_new_with_repoconfig" +) +def test_get_historical_features_non_entity_with_only_end_date(mock_get_spark_session): + mock_spark_session = MagicMock() + final_df = MagicMock() + expected_pdf = pd.DataFrame([{"feature1": 10.0, "feature2": 20.0}]) + final_df.toPandas.return_value = expected_pdf + mock_spark_session.sql.return_value = final_df + mock_get_spark_session.return_value = mock_spark_session + + test_repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=SparkOfflineStoreConfig(type="spark"), + ) + + test_data_source1 = SparkSource( + name="test_nested_batch_source1", + description="test_nested_batch_source", + table="offline_store_database_name.offline_store_table_name1", + timestamp_field="nested_timestamp", + field_mapping={ + "event_header.event_published_datetime_utc": "nested_timestamp", + }, + date_partition_column="effective_date", + date_partition_column_format="%Y%m%d", + ) + + test_data_source2 = SparkSource( + name="test_nested_batch_source2", + description="test_nested_batch_source", + table="offline_store_database_name.offline_store_table_name2", + timestamp_field="nested_timestamp", + field_mapping={ + "event_header.event_published_datetime_utc": "nested_timestamp", + }, + date_partition_column="effective_date", + ) + + test_feature_view1 = FeatureView( + name="test_feature_view1", + entities=_mock_entity(), + schema=[ + Field(name="feature1", dtype=Float32), + ], + source=test_data_source1, + ) + + test_feature_view2 = FeatureView( + name="test_feature_view2", + entities=_mock_entity(), + schema=[ + Field(name="feature2", dtype=Float32), + ], + source=test_data_source2, + ) + + mock_registry = MagicMock() + end_date = datetime(2021, 1, 2) + retrieval_job = SparkOfflineStore.get_historical_features( + config=test_repo_config, + feature_views=[test_feature_view2, test_feature_view1], + feature_refs=["test_feature_view2:feature2", "test_feature_view1:feature1"], + entity_df=None, + registry=mock_registry, + project="test_project", + end_date=end_date, + ) + + # Verify query bounded by end_date correctly for both sources + query = retrieval_job.query + assert "effective_date <= '2021-01-02'" in query + assert "effective_date <= '20210102'" in query + + # Verify data: mocked DataFrame flows to Pandas + pdf = retrieval_job._to_df_internal() + assert pdf.equals(expected_pdf) + + +def test_to_arrow_uses_staging_when_enabled(monkeypatch, tmp_path): + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=SparkOfflineStoreConfig( + type="spark", + staging_location=str(tmp_path), + ), + ) + + job = SparkRetrievalJob( + spark_session=MagicMock(), + query="select 1", + full_feature_names=False, + config=repo_config, + ) + + expected_table = pa.table({"a": [1]}) + dataset_mock = MagicMock() + + monkeypatch.setattr( + job, "to_remote_storage", MagicMock(return_value=["file:///test.parquet"]) + ) + monkeypatch.setattr( + spark_module.ds, "dataset", MagicMock(return_value=dataset_mock) + ) + dataset_mock.to_table.return_value = expected_table + + result = job._to_arrow_internal() + + job.to_remote_storage.assert_called_once() + spark_module.ds.dataset.assert_called_once_with(["/test.parquet"], format="parquet") + assert result.equals(expected_table) + + +def test_to_arrow_normalizes_local_staging_paths(monkeypatch, tmp_path): + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=SparkOfflineStoreConfig( + type="spark", + staging_location=str(tmp_path / "local"), + ), + ) + + job = SparkRetrievalJob( + spark_session=MagicMock(), + query="select 1", + full_feature_names=False, + config=repo_config, + ) + + expected_table = pa.table({"a": [1]}) + dataset_mock = MagicMock() + + raw_path = os.path.join(str(tmp_path), "staged.parquet") + monkeypatch.setattr(job, "to_remote_storage", MagicMock(return_value=[raw_path])) + monkeypatch.setattr( + spark_module.ds, "dataset", MagicMock(return_value=dataset_mock) + ) + dataset_mock.to_table.return_value = expected_table + + result = job._to_arrow_internal() + + job.to_remote_storage.assert_called_once() + spark_module.ds.dataset.assert_called_once_with([raw_path], format="parquet") + assert result.equals(expected_table) + + +def test_to_arrow_falls_back_to_pandas_when_staging_disabled(monkeypatch): + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=SparkOfflineStoreConfig( + type="spark", + staging_location=None, + ), + ) + + job = SparkRetrievalJob( + spark_session=MagicMock(), + query="select 1", + full_feature_names=False, + config=repo_config, + ) + + pdf = pd.DataFrame({"a": [1]}) + monkeypatch.setattr(job, "_to_df_internal", MagicMock(return_value=pdf)) + monkeypatch.setattr( + job, "to_remote_storage", MagicMock(side_effect=AssertionError("not called")) + ) + + result = job._to_arrow_internal() + + assert result.equals(pa.Table.from_pandas(pdf)) + + +@patch("feast.infra.utils.aws_utils.list_s3_files") +def test_to_remote_storage_lists_with_s3_scheme(mock_list_s3_files): + spark_df = MagicMock() + spark_df.write.parquet = MagicMock() + spark_session = MagicMock() + spark_session.sql.return_value = spark_df + + mock_list_s3_files.return_value = ["s3://bucket/prefix/file.parquet"] + + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=SparkOfflineStoreConfig( + type="spark", + staging_location="s3://bucket/prefix", + region="us-west-2", + ), + ) + + job = SparkRetrievalJob( + spark_session=spark_session, + query="select 1", + full_feature_names=False, + config=repo_config, + ) + + result = job.to_remote_storage() + + assert spark_df.write.parquet.call_args[0][0].startswith("s3a://bucket/prefix") + assert mock_list_s3_files.call_args[0][1].startswith("s3://bucket/prefix") + assert result == mock_list_s3_files.return_value + + +@patch("feast.infra.offline_stores.contrib.spark_offline_store.spark._list_gcs_files") +def test_to_remote_storage_lists_with_gcs_scheme(mock_list_gcs_files): + spark_df = MagicMock() + spark_df.write.parquet = MagicMock() + spark_session = MagicMock() + spark_session.sql.return_value = spark_df + + mock_list_gcs_files.return_value = ["gs://bucket/prefix/file.parquet"] + + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=SparkOfflineStoreConfig( + type="spark", + staging_location="gs://bucket/prefix", + ), + ) + + job = SparkRetrievalJob( + spark_session=spark_session, + query="select 1", + full_feature_names=False, + config=repo_config, + ) + + result = job.to_remote_storage() + + assert spark_df.write.parquet.call_args[0][0].startswith("gs://bucket/prefix") + mock_list_gcs_files.assert_called_once() + assert result == mock_list_gcs_files.return_value + + +@patch("feast.infra.offline_stores.contrib.spark_offline_store.spark._list_azure_files") +def test_to_remote_storage_lists_with_azure_scheme(mock_list_azure_files): + spark_df = MagicMock() + spark_df.write.parquet = MagicMock() + spark_session = MagicMock() + spark_session.sql.return_value = spark_df + + mock_list_azure_files.return_value = [ + "wasbs://container@account.blob.core.windows.net/path/file.parquet" + ] + + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=SparkOfflineStoreConfig( + type="spark", + staging_location="wasbs://container@account.blob.core.windows.net/path", + ), + ) + + job = SparkRetrievalJob( + spark_session=spark_session, + query="select 1", + full_feature_names=False, + config=repo_config, + ) + + result = job.to_remote_storage() + + assert spark_df.write.parquet.call_args[0][0].startswith( + "wasbs://container@account.blob.core.windows.net/path" + ) + mock_list_azure_files.assert_called_once() + assert result == mock_list_azure_files.return_value diff --git a/sdk/python/tests/integration/materialization/contrib/spark/test_spark_materialization_engine.py b/sdk/python/tests/component/spark/test_spark_materialization_engine.py similarity index 90% rename from sdk/python/tests/integration/materialization/contrib/spark/test_spark_materialization_engine.py rename to sdk/python/tests/component/spark/test_spark_materialization_engine.py index 03f942c2f96..dbd4d842914 100644 --- a/sdk/python/tests/integration/materialization/contrib/spark/test_spark_materialization_engine.py +++ b/sdk/python/tests/component/spark/test_spark_materialization_engine.py @@ -10,13 +10,13 @@ ) from feast.types import Float32 from tests.data.data_creator import create_basic_driver_dataset -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_test_environment, ) -from tests.integration.feature_repos.universal.online_store.redis import ( +from tests.universal.feature_repos.universal.online_store.redis import ( RedisOnlineStoreCreator, ) from tests.utils.e2e_test_validation import validate_offline_online_store_consistency diff --git a/sdk/python/tests/component/spark/test_spark_table_format_integration.py b/sdk/python/tests/component/spark/test_spark_table_format_integration.py new file mode 100644 index 00000000000..639f478fb28 --- /dev/null +++ b/sdk/python/tests/component/spark/test_spark_table_format_integration.py @@ -0,0 +1,303 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import ( + SparkOptions, + SparkSource, +) +from feast.table_format import ( + DeltaFormat, + HudiFormat, + IcebergFormat, +) + + +class TestSparkSourceWithTableFormat: + """Test SparkSource integration with TableFormat.""" + + def test_spark_source_with_iceberg_table_format(self): + """Test SparkSource with IcebergFormat.""" + iceberg_format = IcebergFormat( + catalog="my_catalog", + namespace="my_db", + ) + iceberg_format.set_property("snapshot-id", "123456789") + + spark_source = SparkSource( + name="iceberg_features", + path="my_catalog.my_db.my_table", + table_format=iceberg_format, + ) + + assert spark_source.table_format == iceberg_format + assert spark_source.table_format.catalog == "my_catalog" + assert spark_source.table_format.get_property("snapshot-id") == "123456789" + assert spark_source.path == "my_catalog.my_db.my_table" + + def test_spark_source_with_delta_table_format(self): + """Test SparkSource with DeltaFormat.""" + delta_format = DeltaFormat() + delta_format.set_property("versionAsOf", "1") + + spark_source = SparkSource( + name="delta_features", + path="s3://bucket/delta-table", + table_format=delta_format, + ) + + assert spark_source.table_format == delta_format + assert spark_source.table_format.get_property("versionAsOf") == "1" + assert spark_source.path == "s3://bucket/delta-table" + + def test_spark_source_with_hudi_table_format(self): + """Test SparkSource with HudiFormat.""" + hudi_format = HudiFormat( + table_type="COPY_ON_WRITE", + record_key="id", + ) + hudi_format.set_property("hoodie.datasource.query.type", "snapshot") + + spark_source = SparkSource( + name="hudi_features", + path="s3://bucket/hudi-table", + table_format=hudi_format, + ) + + assert spark_source.table_format == hudi_format + assert spark_source.table_format.table_type == "COPY_ON_WRITE" + assert ( + spark_source.table_format.get_property("hoodie.datasource.query.type") + == "snapshot" + ) + + def test_spark_source_without_table_format(self): + """Test SparkSource without table format (traditional file reading).""" + spark_source = SparkSource( + name="parquet_features", + path="s3://bucket/data.parquet", + file_format="parquet", + ) + + assert spark_source.table_format is None + assert spark_source.file_format == "parquet" + assert spark_source.path == "s3://bucket/data.parquet" + + def test_spark_source_both_file_and_table_format(self): + """Test SparkSource with both file_format and table_format.""" + iceberg_format = IcebergFormat() + + spark_source = SparkSource( + name="mixed_features", + path="s3://bucket/iceberg-table", + file_format="parquet", # Underlying file format + table_format=iceberg_format, # Table metadata format + ) + + assert spark_source.table_format == iceberg_format + assert spark_source.file_format == "parquet" + + def test_spark_source_validation_with_table_format(self): + """Test SparkSource validation with table_format.""" + iceberg_format = IcebergFormat() + + # Should work: path with table_format, no file_format + spark_source = SparkSource( + name="iceberg_table", + path="catalog.db.table", + table_format=iceberg_format, + ) + assert spark_source.table_format == iceberg_format + + # Should work: path with both table_format and file_format + spark_source = SparkSource( + name="iceberg_table_with_file_format", + path="s3://bucket/data", + file_format="parquet", + table_format=iceberg_format, + ) + assert spark_source.table_format == iceberg_format + assert spark_source.file_format == "parquet" + + def test_spark_source_validation_without_table_format(self): + """Test SparkSource validation without table_format.""" + # Should work: path with file_format, no table_format + spark_source = SparkSource( + name="parquet_file", + path="s3://bucket/data.parquet", + file_format="parquet", + ) + assert spark_source.file_format == "parquet" + assert spark_source.table_format is None + + # Should fail: path without file_format or table_format + with pytest.raises( + ValueError, + match="If 'path' is specified without 'table_format', then 'file_format' is required", + ): + SparkSource( + name="invalid_source", + path="s3://bucket/data", + ) + + @patch( + "feast.infra.offline_stores.contrib.spark_offline_store.spark.get_spark_session_or_start_new_with_repoconfig" + ) + def test_load_dataframe_from_path_with_table_format(self, mock_get_spark_session): + """Test _load_dataframe_from_path with table formats.""" + mock_spark_session = MagicMock() + mock_get_spark_session.getActiveSession.return_value = mock_spark_session + + mock_reader = MagicMock() + mock_spark_session.read.format.return_value = mock_reader + mock_reader.option.return_value = mock_reader + mock_reader.load.return_value = MagicMock() + + # Test Iceberg with options + iceberg_format = IcebergFormat() + iceberg_format.set_property("snapshot-id", "123456789") + iceberg_format.set_property("read.split.target-size", "134217728") + + spark_source = SparkSource( + name="iceberg_test", + path="catalog.db.table", + table_format=iceberg_format, + ) + + spark_source._load_dataframe_from_path(mock_spark_session) + + # Verify format was set to iceberg + mock_spark_session.read.format.assert_called_with("iceberg") + + # Verify options were set + mock_reader.option.assert_any_call("snapshot-id", "123456789") + mock_reader.option.assert_any_call("read.split.target-size", "134217728") + + # Verify load was called with the path + mock_reader.load.assert_called_with("catalog.db.table") + + @patch( + "feast.infra.offline_stores.contrib.spark_offline_store.spark.get_spark_session_or_start_new_with_repoconfig" + ) + def test_load_dataframe_from_path_without_table_format( + self, mock_get_spark_session + ): + """Test _load_dataframe_from_path without table formats.""" + mock_spark_session = MagicMock() + mock_get_spark_session.getActiveSession.return_value = mock_spark_session + + mock_reader = MagicMock() + mock_spark_session.read.format.return_value = mock_reader + mock_reader.load.return_value = MagicMock() + + spark_source = SparkSource( + name="parquet_test", + path="s3://bucket/data.parquet", + file_format="parquet", + ) + + spark_source._load_dataframe_from_path(mock_spark_session) + + # Verify format was set to parquet (file format) + mock_spark_session.read.format.assert_called_with("parquet") + + # Verify load was called with the path + mock_reader.load.assert_called_with("s3://bucket/data.parquet") + + +class TestSparkOptionsWithTableFormat: + """Test SparkOptions serialization with TableFormat.""" + + def test_spark_options_protobuf_serialization_with_table_format(self): + """Test SparkOptions protobuf serialization/deserialization with table format.""" + iceberg_format = IcebergFormat( + catalog="test_catalog", + namespace="test_namespace", + ) + iceberg_format.set_property("snapshot-id", "123456789") + + spark_options = SparkOptions( + table=None, + query=None, + path="catalog.db.table", + file_format=None, + table_format=iceberg_format, + ) + + # Test serialization to proto + proto = spark_options.to_proto() + assert proto.path == "catalog.db.table" + assert proto.file_format == "" # Should be empty when not provided + + # Verify table_format is serialized as proto TableFormat + assert proto.HasField("table_format") + assert proto.table_format.HasField("iceberg_format") + assert proto.table_format.iceberg_format.catalog == "test_catalog" + assert proto.table_format.iceberg_format.namespace == "test_namespace" + assert ( + proto.table_format.iceberg_format.properties["snapshot-id"] == "123456789" + ) + + # Test deserialization from proto + restored_options = SparkOptions.from_proto(proto) + assert restored_options.path == "catalog.db.table" + assert restored_options.file_format == "" + assert isinstance(restored_options.table_format, IcebergFormat) + assert restored_options.table_format.catalog == "test_catalog" + assert restored_options.table_format.namespace == "test_namespace" + assert restored_options.table_format.get_property("snapshot-id") == "123456789" + + def test_spark_options_protobuf_serialization_without_table_format(self): + """Test SparkOptions protobuf serialization/deserialization without table format.""" + spark_options = SparkOptions( + table=None, + query=None, + path="s3://bucket/data.parquet", + file_format="parquet", + table_format=None, + ) + + # Test serialization to proto + proto = spark_options.to_proto() + assert proto.path == "s3://bucket/data.parquet" + assert proto.file_format == "parquet" + assert not proto.HasField("table_format") # Should not have table_format field + + # Test deserialization from proto + restored_options = SparkOptions.from_proto(proto) + assert restored_options.path == "s3://bucket/data.parquet" + assert restored_options.file_format == "parquet" + assert restored_options.table_format is None + + def test_spark_source_protobuf_roundtrip_with_table_format(self): + """Test complete SparkSource protobuf roundtrip with table format.""" + delta_format = DeltaFormat() + delta_format.set_property("versionAsOf", "1") + + original_source = SparkSource( + name="delta_test", + path="s3://bucket/delta-table", + table_format=delta_format, + timestamp_field="event_timestamp", + created_timestamp_column="created_at", + description="Test delta source", + ) + + # Serialize to proto + proto = original_source._to_proto_impl() + + # Deserialize from proto + restored_source = SparkSource.from_proto(proto) + + assert restored_source.name == original_source.name + assert restored_source.path == original_source.path + assert restored_source.timestamp_field == original_source.timestamp_field + assert ( + restored_source.created_timestamp_column + == original_source.created_timestamp_column + ) + assert restored_source.description == original_source.description + + # Verify table_format is properly restored + assert isinstance(restored_source.table_format, DeltaFormat) + assert restored_source.table_format.get_property("versionAsOf") == "1" diff --git a/sdk/python/tests/component/spark/test_spark_transformation.py b/sdk/python/tests/component/spark/test_spark_transformation.py new file mode 100644 index 00000000000..1345c77f0f4 --- /dev/null +++ b/sdk/python/tests/component/spark/test_spark_transformation.py @@ -0,0 +1,105 @@ +from unittest.mock import patch + +from pyspark.sql.functions import col, regexp_replace +from pyspark.testing.utils import assertDataFrameEqual + +from feast.transformation.base import Transformation +from feast.transformation.mode import TransformationMode +from feast.transformation.spark_transformation import SparkTransformation + + +def get_sample_df(spark): + sample_data = [ + {"name": "John D.", "age": 30}, + {"name": "Alice G.", "age": 25}, + {"name": "Bob T.", "age": 35}, + {"name": "Eve A.", "age": 28}, + ] + df = spark.createDataFrame(sample_data) + return df + + +def get_expected_df(spark): + expected_data = [ + {"name": "John D.", "age": 30}, + {"name": "Alice G.", "age": 25}, + {"name": "Bob T.", "age": 35}, + {"name": "Eve A.", "age": 28}, + ] + + expected_df = spark.createDataFrame(expected_data) + return expected_df + + +def remove_extra_spaces(df, column_name): + df_transformed = df.withColumn( + column_name, regexp_replace(col(column_name), "\\s+", " ") + ) + return df_transformed + + +def remove_extra_spaces_sql(df, column_name): + sql = f""" + SELECT + age, + regexp_replace({column_name}, '\\\\s+', ' ') as {column_name} + FROM {df} + """ + return sql + + +def test_spark_transformation(spark_fixture): + with patch( + "feast.infra.compute_engines.spark.utils.get_or_create_new_spark_session" + ) as m: + m.return_value = spark_fixture + spark = spark_fixture + df = get_sample_df(spark) + + spark_transformation = Transformation( + mode=TransformationMode.SPARK, + udf=remove_extra_spaces, + udf_string="remove extra spaces", + ) + + transformed_df = spark_transformation.transform(df, "name") + expected_df = get_expected_df(spark) + assertDataFrameEqual(transformed_df, expected_df) + + +def test_spark_transformation_init_transformation(spark_fixture): + with patch( + "feast.infra.compute_engines.spark.utils.get_or_create_new_spark_session" + ) as m: + m.return_value = spark_fixture + spark = spark_fixture + df = get_sample_df(spark) + + spark_transformation = SparkTransformation( + mode=TransformationMode.SPARK, + udf=remove_extra_spaces, + udf_string="remove extra spaces", + ) + + transformed_df = spark_transformation.transform(df, "name") + expected_df = get_expected_df(spark) + assertDataFrameEqual(transformed_df, expected_df) + + +def test_spark_transformation_sql(spark_fixture): + with patch( + "feast.infra.compute_engines.spark.utils.get_or_create_new_spark_session" + ) as m: + m.return_value = spark_fixture + spark = spark_fixture + df = get_sample_df(spark) + + spark_transformation = SparkTransformation( + mode=TransformationMode.SPARK_SQL, + udf=remove_extra_spaces_sql, + udf_string="remove extra spaces sql", + ) + + transformed_df = spark_transformation.transform(df, "name") + expected_df = get_expected_df(spark) + assertDataFrameEqual(transformed_df, expected_df) diff --git a/sdk/python/tests/component/spark/test_spark_utils.py b/sdk/python/tests/component/spark/test_spark_utils.py new file mode 100644 index 00000000000..84a680e7a25 --- /dev/null +++ b/sdk/python/tests/component/spark/test_spark_utils.py @@ -0,0 +1,274 @@ +from unittest.mock import MagicMock, patch + +from feast.infra.compute_engines.spark.utils import _ensure_s3a_event_log_dir + +BOTO3_PATH = "feast.infra.compute_engines.spark.utils.boto3" +BOTOCONFIG_PATH = "feast.infra.compute_engines.spark.utils.BotoConfig" + + +def _base_conf(event_log_dir: str) -> dict: + return { + "spark.eventLog.enabled": "true", + "spark.eventLog.dir": event_log_dir, + "spark.hadoop.fs.s3a.endpoint": "http://minio:9000", + } + + +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_creates_placeholder_when_empty(mock_boto3): + """S3A prefix doesn't exist -> placeholder object is written.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 0} + + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/spark-events/")) + + s3.list_objects_v2.assert_called_once_with( + Bucket="my-bucket", Prefix="spark-events/", MaxKeys=1 + ) + s3.put_object.assert_called_once_with( + Bucket="my-bucket", Key="spark-events/.keep", Body=b"" + ) + + +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_skips_when_prefix_exists(mock_boto3): + """S3A prefix already has objects -> no placeholder written.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 3} + + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/spark-events/")) + + s3.put_object.assert_not_called() + + +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_noop_when_event_log_disabled(mock_boto3): + """spark.eventLog.enabled != true -> boto3 never called.""" + _ensure_s3a_event_log_dir( + {"spark.eventLog.enabled": "false", "spark.eventLog.dir": "s3a://b/p/"} + ) + mock_boto3.client.assert_not_called() + + +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_noop_for_non_s3a_path(mock_boto3): + """Non-S3A paths (hdfs://, file://, etc.) are left untouched.""" + _ensure_s3a_event_log_dir( + {"spark.eventLog.enabled": "true", "spark.eventLog.dir": "hdfs:///spark-logs"} + ) + mock_boto3.client.assert_not_called() + + +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_non_fatal_on_s3_error(mock_boto3): + """boto3 errors are swallowed -> SparkContext will surface its own error.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.side_effect = Exception("connection refused") + + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/spark-events/")) + + +# --------------------------------------------------------------------------- +# Bucket-root edge cases (s3a://bucket, s3a://bucket/) +# --------------------------------------------------------------------------- + + +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_bucket_root_no_trailing_slash(mock_boto3): + """s3a://bucket (no path) -> .keep at bucket root, not /.keep.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 0} + + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket")) + + s3.list_objects_v2.assert_called_once_with(Bucket="my-bucket", Prefix="", MaxKeys=1) + s3.put_object.assert_called_once_with(Bucket="my-bucket", Key=".keep", Body=b"") + + +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_bucket_root_trailing_slash(mock_boto3): + """s3a://bucket/ (trailing slash, empty prefix) -> .keep at bucket root.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 0} + + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/")) + + s3.list_objects_v2.assert_called_once_with(Bucket="my-bucket", Prefix="", MaxKeys=1) + s3.put_object.assert_called_once_with(Bucket="my-bucket", Key=".keep", Body=b"") + + +# --------------------------------------------------------------------------- +# Credentials from spark config / env var fallback +# --------------------------------------------------------------------------- + + +@patch.dict( + "os.environ", + { + "AWS_ACCESS_KEY_ID": "env-ak", + "AWS_SECRET_ACCESS_KEY": "env-sk", # pragma: allowlist secret + "AWS_SESSION_TOKEN": "env-st", + }, +) +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_uses_spark_config_credentials(mock_boto3): + """Credentials in spark config take precedence over env vars.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 1} + + conf = { + **_base_conf("s3a://my-bucket/logs/"), + "spark.hadoop.fs.s3a.access.key": "spark-ak", + "spark.hadoop.fs.s3a.secret.key": "spark-sk", # pragma: allowlist secret + "spark.hadoop.fs.s3a.session.token": "spark-st", + } + _ensure_s3a_event_log_dir(conf) + + mock_boto3.client.assert_called_once() + kw = mock_boto3.client.call_args.kwargs + assert kw["aws_access_key_id"] == "spark-ak" + assert kw["aws_secret_access_key"] == "spark-sk" # pragma: allowlist secret + assert kw["aws_session_token"] == "spark-st" + + +@patch.dict( + "os.environ", + { + "AWS_ACCESS_KEY_ID": "env-ak", + "AWS_SECRET_ACCESS_KEY": "env-sk", # pragma: allowlist secret + "AWS_SESSION_TOKEN": "env-st", + }, +) +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_falls_back_to_env_credentials(mock_boto3): + """Without spark config keys, env vars are used.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 1} + + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/logs/")) + + mock_boto3.client.assert_called_once() + kw = mock_boto3.client.call_args.kwargs + assert kw["aws_access_key_id"] == "env-ak" + assert kw["aws_secret_access_key"] == "env-sk" # pragma: allowlist secret + assert kw["aws_session_token"] == "env-st" + + +@patch.dict("os.environ", {}, clear=True) +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_no_credentials_passes_none(mock_boto3): + """No credentials anywhere -> None passed to boto3 (anonymous / instance role).""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 1} + + conf = { + "spark.eventLog.enabled": "true", + "spark.eventLog.dir": "s3a://my-bucket/logs/", + } + _ensure_s3a_event_log_dir(conf) + + mock_boto3.client.assert_called_once() + kw = mock_boto3.client.call_args.kwargs + assert kw["aws_access_key_id"] is None + assert kw["aws_secret_access_key"] is None + assert kw["aws_session_token"] is None + + +# --------------------------------------------------------------------------- +# Path-style addressing (MinIO / S3-compatible) +# --------------------------------------------------------------------------- + + +@patch(BOTOCONFIG_PATH) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_path_style_when_enabled(mock_boto3, mock_config_cls): + """spark.hadoop.fs.s3a.path.style.access=true -> addressing_style='path'.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 1} + + conf = { + **_base_conf("s3a://my-bucket/logs/"), + "spark.hadoop.fs.s3a.path.style.access": "true", + } + _ensure_s3a_event_log_dir(conf) + + mock_config_cls.assert_called_once() + config_kwargs = mock_config_cls.call_args + assert config_kwargs.kwargs["s3"] == {"addressing_style": "path"} + + +@patch(BOTOCONFIG_PATH) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_virtual_hosted_style_by_default( + mock_boto3, mock_config_cls +): + """No path.style.access config -> addressing_style='auto'.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 1} + + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/logs/")) + + mock_config_cls.assert_called_once() + config_kwargs = mock_config_cls.call_args + assert config_kwargs.kwargs["s3"] == {"addressing_style": "auto"} + + +# --------------------------------------------------------------------------- +# Endpoint env var fallback (AWS_ENDPOINT_URL) +# --------------------------------------------------------------------------- + + +@patch.dict("os.environ", {"AWS_ENDPOINT_URL": "http://localhost:9000"}, clear=True) +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_endpoint_from_env(mock_boto3): + """AWS_ENDPOINT_URL env var is used when spark config has no endpoint.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 1} + + conf = { + "spark.eventLog.enabled": "true", + "spark.eventLog.dir": "s3a://my-bucket/logs/", + } + _ensure_s3a_event_log_dir(conf) + + mock_boto3.client.assert_called_once() + kw = mock_boto3.client.call_args.kwargs + assert kw["endpoint_url"] == "http://localhost:9000" + + +@patch.dict("os.environ", {"AWS_ENDPOINT_URL": "http://env-endpoint:9000"}, clear=True) +@patch(BOTOCONFIG_PATH, MagicMock()) +@patch(BOTO3_PATH) +def test_ensure_s3a_event_log_dir_spark_endpoint_over_env(mock_boto3): + """spark.hadoop.fs.s3a.endpoint takes precedence over AWS_ENDPOINT_URL.""" + s3 = MagicMock() + mock_boto3.client.return_value = s3 + s3.list_objects_v2.return_value = {"KeyCount": 1} + + _ensure_s3a_event_log_dir(_base_conf("s3a://my-bucket/logs/")) + + mock_boto3.client.assert_called_once() + kw = mock_boto3.client.call_args.kwargs + assert kw["endpoint_url"] == "http://minio:9000" diff --git a/sdk/python/tests/integration/compute_engines/spark/utils.py b/sdk/python/tests/component/spark/utils.py similarity index 94% rename from sdk/python/tests/integration/compute_engines/spark/utils.py rename to sdk/python/tests/component/spark/utils.py index 20ffba4eff1..f12eef58948 100644 --- a/sdk/python/tests/integration/compute_engines/spark/utils.py +++ b/sdk/python/tests/component/spark/utils.py @@ -7,13 +7,13 @@ from feast.infra.offline_stores.contrib.spark_offline_store.tests.data_source import ( SparkDataSourceCreator, ) -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_test_environment, ) -from tests.integration.feature_repos.universal.online_store.redis import ( +from tests.universal.feature_repos.universal.online_store.redis import ( RedisOnlineStoreCreator, ) diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py index a57846c7e2e..83c3a2778e6 100644 --- a/sdk/python/tests/conftest.py +++ b/sdk/python/tests/conftest.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import importlib import logging import multiprocessing import os @@ -20,7 +21,7 @@ from multiprocessing import Process from sys import platform from textwrap import dedent -from typing import Any, Dict, List, Tuple, no_type_check +from typing import Any, Dict, List, Optional, Tuple, no_type_check from unittest import mock import pandas as pd @@ -36,34 +37,90 @@ create_document_dataset, create_image_dataset, ) -from tests.integration.feature_repos.integration_test_repo_config import ( # noqa: E402 - IntegrationTestRepoConfig, -) -from tests.integration.feature_repos.repo_configuration import ( # noqa: E402 - AVAILABLE_OFFLINE_STORES, - AVAILABLE_ONLINE_STORES, - OFFLINE_STORE_TO_PROVIDER_CONFIG, - Environment, - TestData, - construct_test_environment, - construct_universal_feature_views, - construct_universal_test_data, -) -from tests.integration.feature_repos.universal.data_sources.file import ( # noqa: E402 - FileDataSourceCreator, -) -from tests.integration.feature_repos.universal.entities import ( # noqa: E402 - customer, - driver, - location, -) -from tests.utils.auth_permissions_util import default_store from tests.utils.http_server import check_port_open, free_port # noqa: E402 -from tests.utils.ssl_certifcates_util import ( - combine_trust_stores, - create_ca_trust_store, - generate_self_signed_cert, -) + +IntegrationTestRepoConfig: Any = None +Environment = Any +TestData = Any +AVAILABLE_OFFLINE_STORES: Any = None +AVAILABLE_ONLINE_STORES: Any = None +OFFLINE_STORE_TO_PROVIDER_CONFIG: Any = None +construct_test_environment: Any = None +construct_universal_feature_views: Any = None +construct_universal_test_data: Any = None +FileDataSourceCreator: Any = None +customer: Any = None +driver: Any = None +location: Any = None +_universal_deps_missing_reason: Optional[str] = None + + +def _load_universal_feature_repo_deps() -> bool: + global IntegrationTestRepoConfig + global Environment + global TestData + global AVAILABLE_OFFLINE_STORES + global AVAILABLE_ONLINE_STORES + global OFFLINE_STORE_TO_PROVIDER_CONFIG + global construct_test_environment + global construct_universal_feature_views + global construct_universal_test_data + global FileDataSourceCreator + global customer + global driver + global location + global _universal_deps_missing_reason + + if IntegrationTestRepoConfig is not None: + return True + + try: + integration_config = importlib.import_module( + "tests.universal.feature_repos.integration_test_repo_config" + ) + repo_configuration = importlib.import_module( + "tests.universal.feature_repos.repo_configuration" + ) + file_data_sources = importlib.import_module( + "tests.universal.feature_repos.universal.data_sources.file" + ) + entities = importlib.import_module( + "tests.universal.feature_repos.universal.entities" + ) + except ModuleNotFoundError as e: + _universal_deps_missing_reason = ( + f"Optional integration test dependency is not installed: {e.name}" + ) + return False + + IntegrationTestRepoConfig = integration_config.IntegrationTestRepoConfig + Environment = repo_configuration.Environment + TestData = repo_configuration.TestData + AVAILABLE_OFFLINE_STORES = repo_configuration.AVAILABLE_OFFLINE_STORES + AVAILABLE_ONLINE_STORES = repo_configuration.AVAILABLE_ONLINE_STORES + OFFLINE_STORE_TO_PROVIDER_CONFIG = ( + repo_configuration.OFFLINE_STORE_TO_PROVIDER_CONFIG + ) + construct_test_environment = repo_configuration.construct_test_environment + construct_universal_feature_views = ( + repo_configuration.construct_universal_feature_views + ) + construct_universal_test_data = repo_configuration.construct_universal_test_data + FileDataSourceCreator = file_data_sources.FileDataSourceCreator + customer = entities.customer + driver = entities.driver + location = entities.location + _universal_deps_missing_reason = None + return True + + +def _skip_missing_universal_feature_repo_deps() -> None: + if not _load_universal_feature_repo_deps(): + pytest.skip( + _universal_deps_missing_reason + or "Optional integration test dependencies are not installed" + ) + logger = logging.getLogger(__name__) @@ -85,7 +142,7 @@ def pytest_configure(config): - if platform in ["darwin", "windows"]: + if platform == "darwin" or platform.startswith("win"): multiprocessing.set_start_method("spawn", force=True) else: multiprocessing.set_start_method("fork") @@ -101,6 +158,10 @@ def pytest_configure(config): "markers", "universal_offline_stores: mark tests that can be run against different offline stores", ) + config.addinivalue_line( + "markers", + "ray_offline_stores_only: mark tests that currently only work with Ray offline store", + ) def pytest_addoption(parser): @@ -188,6 +249,7 @@ def start_test_local_server(repo_path: str, port: int): @pytest.fixture def environment(request, worker_id): + _skip_missing_universal_feature_repo_deps() e = construct_test_environment( request.param, worker_id=worker_id, @@ -207,6 +269,7 @@ def environment(request, worker_id): @pytest.fixture def vectordb_environment(request, worker_id): + _skip_missing_universal_feature_repo_deps() e = construct_test_environment( request.param, worker_id=worker_id, @@ -247,6 +310,23 @@ def pytest_generate_tests(metafunc: pytest.Metafunc): parameter should point to the same Python object (hence, we use _config_cache dict to store those objects). """ if "environment" in metafunc.fixturenames: + if not _load_universal_feature_repo_deps(): + metafunc.parametrize( + "environment", + [ + pytest.param( + None, + marks=pytest.mark.skip( + reason=_universal_deps_missing_reason + or "Optional integration test dependencies are not installed" + ), + ) + ], + indirect=True, + ids=["missing_optional_integration_deps"], + ) + return + markers = {m.name: m for m in metafunc.definition.own_markers} offline_stores = None if "universal_offline_stores" in markers: @@ -367,6 +447,7 @@ def feature_server_endpoint(environment): @pytest.fixture def universal_data_sources(environment) -> TestData: + _skip_missing_universal_feature_repo_deps() return construct_universal_test_data(environment) @@ -390,6 +471,7 @@ def feature_store_for_online_retrieval( Returns a feature store that is ready for online retrieval, along with entity rows and feature refs that can be used to query for online features. """ + _skip_missing_universal_feature_repo_deps() fs = environment.feature_store entities, datasets, data_sources = universal_data_sources feature_views = construct_universal_feature_views(data_sources) @@ -431,6 +513,9 @@ def fake_ingest_data(): "conv_rate": [0.5], "acc_rate": [0.6], "avg_daily_trips": [4], + "driver_metadata": [None], + "driver_config": [None], + "driver_profile": [None], "event_timestamp": [pd.Timestamp(_utc_now()).round("ms")], "created": [pd.Timestamp(_utc_now()).round("ms")], } @@ -471,6 +556,11 @@ def server_port(): @pytest.fixture def feature_store(temp_dir, auth_config, applied_permissions): + try: + from tests.utils.auth_permissions_util import default_store + except ModuleNotFoundError as e: + pytest.skip(f"Optional auth test dependency is not installed: {e.name}") + print(f"Creating store at {temp_dir}") return default_store(str(temp_dir), auth_config, applied_permissions) @@ -535,6 +625,15 @@ def auth_config(request, is_integration_test): @pytest.fixture(scope="module") def tls_mode(request): + try: + from tests.utils.ssl_certifcates_util import ( + combine_trust_stores, + create_ca_trust_store, + generate_self_signed_cert, + ) + except ModuleNotFoundError as e: + pytest.skip(f"Optional TLS test dependency is not installed: {e.name}") + is_tls_mode = request.param[0] output_combined_truststore_path = "" diff --git a/sdk/python/tests/example_repos/example_feature_repo_1.py b/sdk/python/tests/example_repos/example_feature_repo_1.py index 492b8c5555d..4cdcb9622a4 100644 --- a/sdk/python/tests/example_repos/example_feature_repo_1.py +++ b/sdk/python/tests/example_repos/example_feature_repo_1.py @@ -5,7 +5,7 @@ from feast import Entity, FeatureService, FeatureView, Field, FileSource, PushSource from feast.on_demand_feature_view import on_demand_feature_view from feast.types import Array, Float32, Int64, String -from tests.integration.feature_repos.universal.feature_views import TAGS +from tests.universal.feature_repos.universal.feature_views import TAGS # Note that file source paths are not validated, so there doesn't actually need to be any data # at the paths for these file sources. Since these paths are effectively fake, this example diff --git a/sdk/python/tests/example_repos/example_feature_repo_with_project_1.py b/sdk/python/tests/example_repos/example_feature_repo_with_project_1.py index ad04d7ae664..a6db2d63a33 100644 --- a/sdk/python/tests/example_repos/example_feature_repo_with_project_1.py +++ b/sdk/python/tests/example_repos/example_feature_repo_with_project_1.py @@ -6,7 +6,7 @@ from feast.on_demand_feature_view import on_demand_feature_view from feast.project import Project from feast.types import Array, Float32, Int64, String -from tests.integration.feature_repos.universal.feature_views import TAGS +from tests.universal.feature_repos.universal.feature_views import TAGS # Note that file source paths are not validated, so there doesn't actually need to be any data # at the paths for these file sources. Since these paths are effectively fake, this example diff --git a/sdk/python/tests/foo_provider.py b/sdk/python/tests/foo_provider.py index a04ff3cc456..2586db80353 100644 --- a/sdk/python/tests/foo_provider.py +++ b/sdk/python/tests/foo_provider.py @@ -19,6 +19,7 @@ from feast import Entity, FeatureService, FeatureView, RepoConfig from feast.data_source import DataSource +from feast.filter_models import ComparisonFilter, CompoundFilter from feast.infra.offline_stores.offline_store import RetrievalJob from feast.infra.provider import Provider from feast.infra.registry.base_registry import BaseRegistry @@ -155,6 +156,7 @@ def retrieve_online_documents( query: List[float], top_k: int, distance_metric: Optional[str] = None, + include_feature_view_version_metadata: bool = False, ) -> List[ Tuple[ Optional[datetime], @@ -170,10 +172,12 @@ def retrieve_online_documents_v2( config: RepoConfig, table: FeatureView, requested_features: List[str], - query: Optional[List[float]], + embedding: Optional[List[float]], top_k: int, distance_metric: Optional[str] = None, query_string: Optional[str] = None, + filters: Optional[Union[ComparisonFilter, CompoundFilter]] = None, + include_feature_view_version_metadata: bool = False, ) -> List[ Tuple[ Optional[datetime], @@ -206,6 +210,7 @@ def get_online_features( registry: BaseRegistry, project: str, full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: pass @@ -220,6 +225,7 @@ async def get_online_features_async( registry: BaseRegistry, project: str, full_feature_names: bool = False, + include_feature_view_version_metadata: bool = False, ) -> OnlineResponse: pass diff --git a/sdk/python/tests/integration/cli/__init__.py b/sdk/python/tests/integration/cli/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/unit/cli/test_cli_apply_duplicates.py b/sdk/python/tests/integration/cli/test_cli_apply_duplicates.py similarity index 95% rename from sdk/python/tests/unit/cli/test_cli_apply_duplicates.py rename to sdk/python/tests/integration/cli/test_cli_apply_duplicates.py index b3e350fe73c..f29d07a67c1 100644 --- a/sdk/python/tests/unit/cli/test_cli_apply_duplicates.py +++ b/sdk/python/tests/integration/cli/test_cli_apply_duplicates.py @@ -2,13 +2,17 @@ from pathlib import Path from textwrap import dedent +import pytest + from tests.utils.cli_repo_creator import CliRunner, get_example_repo +pytestmark = pytest.mark.integration + def test_cli_apply_duplicated_featureview_names() -> None: run_simple_apply_test( example_repo_file_name="example_feature_repo_with_duplicated_featureview_names.py", - expected_error=b"Please ensure that all feature view names are case-insensitively unique", + expected_error=b"Feature view names must be case-insensitively unique", ) @@ -152,9 +156,7 @@ def test_cli_apply_imported_featureview_with_duplication() -> None: rc, output = runner.run_with_output(["apply"], cwd=repo_path) assert rc != 0 - assert ( - b"More than one feature view with name driver_hourly_stats found." in output - ) + assert b"Multiple FeatureViews with name 'driver_hourly_stats' found." in output def test_cli_apply_duplicated_featureview_names_multiple_py_files() -> None: @@ -195,6 +197,5 @@ def test_cli_apply_duplicated_featureview_names_multiple_py_files() -> None: assert ( rc != 0 - and b"Please ensure that all feature view names are case-insensitively unique" - in output + and b"Feature view names must be case-insensitively unique" in output ) diff --git a/sdk/python/tests/integration/registration/test_universal_cli.py b/sdk/python/tests/integration/cli/test_universal_cli.py similarity index 99% rename from sdk/python/tests/integration/registration/test_universal_cli.py rename to sdk/python/tests/integration/cli/test_universal_cli.py index 735f71407f6..ed9aea7ff91 100644 --- a/sdk/python/tests/integration/registration/test_universal_cli.py +++ b/sdk/python/tests/integration/cli/test_universal_cli.py @@ -7,7 +7,7 @@ from assertpy import assertpy from feast.feature_store import FeatureStore -from tests.integration.feature_repos.universal.data_sources.file import ( +from tests.universal.feature_repos.universal.data_sources.file import ( FileDataSourceCreator, ) from tests.utils.basic_read_write_test import basic_rw_test diff --git a/sdk/python/tests/integration/compute_engines/ray_compute/__init__.py b/sdk/python/tests/integration/compute_engines/ray_compute/__init__.py deleted file mode 100644 index 7938db59420..00000000000 --- a/sdk/python/tests/integration/compute_engines/ray_compute/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Ray compute engine integration tests.""" diff --git a/sdk/python/tests/integration/compute_engines/ray_compute/test_compute.py b/sdk/python/tests/integration/compute_engines/ray_compute/test_compute.py deleted file mode 100644 index ef4bfa131da..00000000000 --- a/sdk/python/tests/integration/compute_engines/ray_compute/test_compute.py +++ /dev/null @@ -1,331 +0,0 @@ -from datetime import timedelta -from typing import cast -from unittest.mock import MagicMock - -import pandas as pd -import pytest -from tqdm import tqdm - -from feast import BatchFeatureView, Field -from feast.aggregation import Aggregation -from feast.infra.common.materialization_job import ( - MaterializationJobStatus, - MaterializationTask, -) -from feast.infra.common.retrieval_task import HistoricalRetrievalTask -from feast.infra.compute_engines.ray.compute import RayComputeEngine -from feast.infra.compute_engines.ray.config import RayComputeEngineConfig -from feast.infra.compute_engines.ray.job import RayDAGRetrievalJob -from feast.infra.offline_stores.contrib.ray_offline_store.ray import ( - RayOfflineStore, -) -from feast.transformation.ray_transformation import RayTransformation -from feast.types import Float32, Int32, Int64 -from tests.integration.compute_engines.ray_compute.ray_shared_utils import ( - driver, - now, -) - - -@pytest.mark.integration -@pytest.mark.xdist_group(name="ray") -def test_ray_compute_engine_get_historical_features( - ray_environment, feature_dataset, entity_df -): - """Test Ray compute engine historical feature retrieval.""" - fs = ray_environment.feature_store - registry = fs.registry - - def transform_feature(df: pd.DataFrame) -> pd.DataFrame: - df["sum_conv_rate"] = df["sum_conv_rate"] * 2 - df["avg_acc_rate"] = df["avg_acc_rate"] * 2 - return df - - driver_stats_fv = BatchFeatureView( - name="driver_hourly_stats", - entities=[driver], - mode="pandas", - aggregations=[ - Aggregation(column="conv_rate", function="sum"), - Aggregation(column="acc_rate", function="avg"), - ], - udf=transform_feature, - udf_string="transform_feature", - ttl=timedelta(days=3), - schema=[ - Field(name="conv_rate", dtype=Float32), - Field(name="acc_rate", dtype=Float32), - Field(name="avg_daily_trips", dtype=Int64), - Field(name="driver_id", dtype=Int32), - ], - online=False, - offline=False, - source=feature_dataset, - ) - - fs.apply([driver, driver_stats_fv]) - - # Build retrieval task - task = HistoricalRetrievalTask( - project=ray_environment.project, - entity_df=entity_df, - feature_view=driver_stats_fv, - full_feature_name=False, - registry=registry, - ) - engine = RayComputeEngine( - repo_config=ray_environment.config, - offline_store=RayOfflineStore(), - online_store=MagicMock(), - ) - - ray_dag_retrieval_job = engine.get_historical_features(registry, task) - ray_dataset = cast(RayDAGRetrievalJob, ray_dag_retrieval_job).to_ray_dataset() - df_out = ray_dataset.to_pandas().sort_values("driver_id") - - assert df_out.driver_id.to_list() == [1001, 1002] - assert abs(df_out["sum_conv_rate"].to_list()[0] - 1.6) < 1e-6 - assert abs(df_out["sum_conv_rate"].to_list()[1] - 2.0) < 1e-6 - assert abs(df_out["avg_acc_rate"].to_list()[0] - 1.0) < 1e-6 - assert abs(df_out["avg_acc_rate"].to_list()[1] - 1.0) < 1e-6 - - -@pytest.mark.integration -@pytest.mark.xdist_group(name="ray") -def test_ray_compute_engine_materialize(ray_environment, feature_dataset): - """Test Ray compute engine materialization.""" - fs = ray_environment.feature_store - registry = fs.registry - - def transform_feature(df: pd.DataFrame) -> pd.DataFrame: - df["sum_conv_rate"] = df["sum_conv_rate"] * 2 - df["avg_acc_rate"] = df["avg_acc_rate"] * 2 - return df - - driver_stats_fv = BatchFeatureView( - name="driver_hourly_stats", - entities=[driver], - mode="pandas", - aggregations=[ - Aggregation(column="conv_rate", function="sum"), - Aggregation(column="acc_rate", function="avg"), - ], - udf=transform_feature, - udf_string="transform_feature", - ttl=timedelta(days=3), - schema=[ - Field(name="conv_rate", dtype=Float32), - Field(name="acc_rate", dtype=Float32), - Field(name="avg_daily_trips", dtype=Int64), - Field(name="driver_id", dtype=Int32), - ], - online=True, - offline=False, - source=feature_dataset, - ) - - def tqdm_builder(length): - return tqdm(length, ncols=100) - - fs.apply([driver, driver_stats_fv]) - - task = MaterializationTask( - project=ray_environment.project, - feature_view=driver_stats_fv, - start_time=now - timedelta(days=2), - end_time=now, - tqdm_builder=tqdm_builder, - ) - - engine = RayComputeEngine( - repo_config=ray_environment.config, - offline_store=RayOfflineStore(), - online_store=MagicMock(), - ) - - ray_materialize_jobs = engine.materialize(registry, task) - - assert len(ray_materialize_jobs) == 1 - assert ray_materialize_jobs[0].status() == MaterializationJobStatus.SUCCEEDED - - -@pytest.mark.integration -@pytest.mark.xdist_group(name="ray") -def test_ray_compute_engine_config(): - """Test Ray compute engine configuration.""" - config = RayComputeEngineConfig( - type="ray.engine", - ray_address="ray://localhost:10001", - broadcast_join_threshold_mb=200, - enable_distributed_joins=True, - max_parallelism_multiplier=4, - target_partition_size_mb=128, - window_size_for_joins="2H", - max_workers=4, - enable_optimization=True, - ) - - assert config.type == "ray.engine" - assert config.ray_address == "ray://localhost:10001" - assert config.broadcast_join_threshold_mb == 200 - assert config.window_size_timedelta == timedelta(hours=2) - - -@pytest.mark.integration -@pytest.mark.xdist_group(name="ray") -def test_ray_transformation_compute_engine(ray_environment, feature_dataset, entity_df): - """Test Ray compute engine with Ray transformation mode.""" - import ray.data - - fs = ray_environment.feature_store - registry = fs.registry - - def ray_transformation_udf(ds: ray.data.Dataset) -> ray.data.Dataset: - """Ray native transformation that processes data in parallel.""" - - def process_batch(batch: pd.DataFrame) -> pd.DataFrame: - # Simulate some computation (e.g., feature engineering) - if "conv_rate" in batch.columns: - batch["processed_conv_rate"] = batch["conv_rate"] * 2.0 - if "acc_rate" in batch.columns: - batch["processed_acc_rate"] = batch["acc_rate"] * 1.5 - return batch - - return ds.map_batches( - process_batch, - batch_format="pandas", - concurrency=2, # Use 2 parallel workers - ) - - # Create Ray transformation - ray_transform = RayTransformation( - udf=ray_transformation_udf, - udf_string="def ray_transformation_udf(ds): return ds.map_batches(...)", - ) - - driver_stats_fv = BatchFeatureView( - name="driver_hourly_stats_ray", - entities=[driver], - mode="ray", # Use Ray transformation mode - feature_transformation=ray_transform, - ttl=timedelta(days=3), - schema=[ - Field(name="conv_rate", dtype=Float32), - Field(name="acc_rate", dtype=Float32), - Field(name="processed_conv_rate", dtype=Float32), - Field(name="processed_acc_rate", dtype=Float32), - Field(name="avg_daily_trips", dtype=Int64), - Field(name="driver_id", dtype=Int32), - ], - online=False, - offline=False, - source=feature_dataset, - ) - - fs.apply([driver, driver_stats_fv]) - - # Build retrieval task - task = HistoricalRetrievalTask( - project=ray_environment.project, - entity_df=entity_df, - feature_view=driver_stats_fv, - full_feature_name=False, - registry=registry, - ) - engine = RayComputeEngine( - repo_config=ray_environment.config, - offline_store=RayOfflineStore(), - online_store=MagicMock(), - ) - - ray_dag_retrieval_job = engine.get_historical_features(registry, task) - ray_dataset = cast(RayDAGRetrievalJob, ray_dag_retrieval_job).to_ray_dataset() - df_out = ray_dataset.to_pandas().sort_values("driver_id") - - # Verify the transformation was applied - assert df_out.driver_id.to_list() == [1001, 1002] - - # Check that original columns are present - assert "conv_rate" in df_out.columns - assert "acc_rate" in df_out.columns - - # Check that transformed columns are present - assert "processed_conv_rate" in df_out.columns - assert "processed_acc_rate" in df_out.columns - - # Verify the transformation logic was applied - for idx, row in df_out.iterrows(): - assert abs(row["processed_conv_rate"] - row["conv_rate"] * 2.0) < 1e-6 - assert abs(row["processed_acc_rate"] - row["acc_rate"] * 1.5) < 1e-6 - - -@pytest.mark.integration -@pytest.mark.xdist_group(name="ray") -def test_ray_transformation_materialization(ray_environment, feature_dataset): - """Test Ray transformation during materialization.""" - import ray.data - - fs = ray_environment.feature_store - registry = fs.registry - - def ray_embedding_udf(ds: ray.data.Dataset) -> ray.data.Dataset: - """Simulate embedding generation with Ray native processing.""" - - def generate_embeddings(batch: pd.DataFrame) -> pd.DataFrame: - # Simulate embedding generation - if "conv_rate" in batch.columns: - # Create a simple embedding based on conv_rate - batch["embedding"] = batch["conv_rate"].apply( - lambda x: [x * 0.1, x * 0.2, x * 0.3] - ) - return batch - - return ds.map_batches(generate_embeddings, batch_format="pandas", concurrency=2) - - # Create Ray transformation for embeddings - ray_embedding_transform = RayTransformation( - udf=ray_embedding_udf, - udf_string="def ray_embedding_udf(ds): return ds.map_batches(...)", - ) - - driver_embeddings_fv = BatchFeatureView( - name="driver_embeddings", - entities=[driver], - mode="ray", - feature_transformation=ray_embedding_transform, - ttl=timedelta(days=3), - schema=[ - Field(name="conv_rate", dtype=Float32), - Field( - name="embedding", dtype=Float32 - ), # This would be Array(Float32) in real usage - Field(name="driver_id", dtype=Int32), - ], - online=True, - offline=False, - source=feature_dataset, - ) - - def tqdm_builder(length): - return tqdm(length, ncols=100) - - fs.apply([driver, driver_embeddings_fv]) - - task = MaterializationTask( - project=ray_environment.project, - feature_view=driver_embeddings_fv, - start_time=now - timedelta(days=2), - end_time=now, - tqdm_builder=tqdm_builder, - ) - - engine = RayComputeEngine( - repo_config=ray_environment.config, - offline_store=RayOfflineStore(), - online_store=MagicMock(), - ) - - ray_materialize_jobs = engine.materialize(registry, task) - - assert len(ray_materialize_jobs) == 1 - assert ray_materialize_jobs[0].status() == MaterializationJobStatus.SUCCEEDED diff --git a/sdk/python/tests/integration/conftest.py b/sdk/python/tests/integration/conftest.py index 5784ad30292..f4f70d31ec3 100644 --- a/sdk/python/tests/integration/conftest.py +++ b/sdk/python/tests/integration/conftest.py @@ -134,7 +134,7 @@ def start_keycloak_server(): @pytest.fixture(scope="session") def mysql_server(): - container = MySqlContainer("mysql:latest") + container = MySqlContainer("mysql:latest", dialect="pymysql") container.start() yield container diff --git a/sdk/python/tests/integration/dbt/__init__.py b/sdk/python/tests/integration/dbt/__init__.py new file mode 100644 index 00000000000..d0e9fe0a015 --- /dev/null +++ b/sdk/python/tests/integration/dbt/__init__.py @@ -0,0 +1 @@ +# Integration tests for dbt import functionality diff --git a/sdk/python/tests/integration/dbt/conftest.py b/sdk/python/tests/integration/dbt/conftest.py new file mode 100644 index 00000000000..00ee01e7027 --- /dev/null +++ b/sdk/python/tests/integration/dbt/conftest.py @@ -0,0 +1,35 @@ +""" +Conftest for dbt integration tests. + +This is a standalone conftest that doesn't depend on the main Feast test infrastructure. +""" + +from pathlib import Path + +import pytest + +# This conftest is minimal and doesn't import the main feast conftest +# to avoid complex dependency chains for dbt-specific tests + +# Path to the test dbt project manifest +TEST_DBT_PROJECT_DIR = Path(__file__).parent / "test_dbt_project" +TEST_MANIFEST_PATH = TEST_DBT_PROJECT_DIR / "target" / "manifest.json" + + +def pytest_collection_modifyitems(config, items): # noqa: ARG001 + """ + Skip dbt integration tests if manifest.json doesn't exist. + + These tests require running 'dbt build' first to generate the manifest. + The dbt-integration-test workflow handles this, but regular unit test + runs don't, so we skip them to avoid failures. + """ + if not TEST_MANIFEST_PATH.exists(): + skip_marker = pytest.mark.skip( + reason="dbt manifest.json not found - run 'dbt build' first or use dbt-integration-test workflow" + ) + for item in items: + if str(TEST_DBT_PROJECT_DIR) in str(item.fspath) or "/dbt/" in str( + item.fspath + ): + item.add_marker(skip_marker) diff --git a/sdk/python/tests/integration/dbt/pytest.ini b/sdk/python/tests/integration/dbt/pytest.ini new file mode 100644 index 00000000000..8233ec3a4dc --- /dev/null +++ b/sdk/python/tests/integration/dbt/pytest.ini @@ -0,0 +1,9 @@ +[pytest] +# Prevent loading parent conftest.py files which may import heavy dependencies +# like ray that are not needed for dbt integration tests +norecursedirs = .. +asyncio_mode = auto + +# Test markers +markers = + dbt: dbt integration tests diff --git a/sdk/python/tests/integration/dbt/test_dbt_integration.py b/sdk/python/tests/integration/dbt/test_dbt_integration.py new file mode 100644 index 00000000000..09a3cb5e123 --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_integration.py @@ -0,0 +1,1420 @@ +""" +Integration tests for dbt import functionality. + +Tests the full dbt integration workflow including: +- Parsing dbt manifest.json files +- Creating Feast objects from dbt models +- Tag filtering and model selection +- Different data source types (bigquery, snowflake, file) +""" + +import ast +import os +import tempfile +from pathlib import Path + +import pytest + +# Skip all tests if dbt-artifacts-parser is not installed +pytest.importorskip("dbt_artifacts_parser", reason="dbt-artifacts-parser not installed") + +from feast.dbt.codegen import ( # noqa: E402 + DbtCodeGenerator, + _escape_description, + _make_var_name, + generate_feast_code, +) +from feast.dbt.mapper import DbtToFeastMapper # noqa: E402 +from feast.dbt.parser import DbtColumn, DbtManifestParser, DbtModel # noqa: E402 +from feast.entity import Entity # noqa: E402 +from feast.feature_view import FeatureView # noqa: E402 +from feast.infra.offline_stores.bigquery_source import BigQuerySource # noqa: E402 +from feast.infra.offline_stores.file_source import FileSource # noqa: E402 +from feast.infra.offline_stores.snowflake_source import SnowflakeSource # noqa: E402 +from feast.types import Float32, Float64, Int32, Int64, String # noqa: E402 +from feast.value_type import ValueType # noqa: E402 + +# Get the path to the test dbt project +TEST_DBT_PROJECT_DIR = Path(__file__).parent / "test_dbt_project" +TEST_MANIFEST_PATH = TEST_DBT_PROJECT_DIR / "target" / "manifest.json" + + +@pytest.fixture +def manifest_path(): + """Path to the test dbt manifest.json file.""" + return str(TEST_MANIFEST_PATH) + + +@pytest.fixture +def parser(manifest_path): + """Create a DbtManifestParser with the test manifest.""" + parser = DbtManifestParser(manifest_path) + parser.parse() + return parser + + +class TestDbtManifestParsing: + """Test dbt manifest parsing functionality.""" + + def test_parse_manifest_metadata(self, parser): + """Test that manifest metadata is correctly parsed.""" + # dbt_version will vary based on installed version - just verify it's present + assert parser.dbt_version is not None + assert len(parser.dbt_version) > 0 + assert parser.project_name == "feast_integration_test" + + def test_get_all_models(self, parser): + """Test retrieving all models from manifest.""" + models = parser.get_models() + + assert len(models) == 3 + model_names = {m.name for m in models} + assert model_names == { + "driver_features", + "customer_features", + "product_features", + } + + def test_get_models_with_tag_filter(self, parser): + """Test filtering models by dbt tag.""" + # Filter by 'ml' tag (driver_features and customer_features have it) + ml_models = parser.get_models(tag_filter="ml") + assert len(ml_models) == 2 + ml_names = {m.name for m in ml_models} + assert ml_names == {"driver_features", "customer_features"} + + # Filter by 'recommendations' tag (only product_features has it) + rec_models = parser.get_models(tag_filter="recommendations") + assert len(rec_models) == 1 + assert rec_models[0].name == "product_features" + + # Filter by 'feast' tag (all models have it) + feast_models = parser.get_models(tag_filter="feast") + assert len(feast_models) == 3 + + def test_get_models_by_name(self, parser): + """Test filtering models by specific names.""" + models = parser.get_models(model_names=["driver_features", "customer_features"]) + + assert len(models) == 2 + model_names = {m.name for m in models} + assert model_names == {"driver_features", "customer_features"} + + def test_model_properties(self, parser): + """Test that model properties are correctly extracted.""" + model = parser.get_model_by_name("driver_features") + + assert model is not None + assert model.name == "driver_features" + # Database and schema vary by adapter (e.g., DuckDB uses "memory"/"main") + assert model.database is not None + assert model.schema is not None + assert model.alias == "driver_features" + assert model.description == "Driver hourly features for ML models" + # Verify full_table_name is built correctly from components + assert model.full_table_name == f"{model.database}.{model.schema}.{model.alias}" + assert "feast" in model.tags + assert "ml" in model.tags + assert len(model.columns) == 5 + + def test_model_columns(self, parser): + """Test that column metadata is correctly extracted.""" + model = parser.get_model_by_name("driver_features") + + column_dict = {col.name: col for col in model.columns} + + # Check entity column + assert "driver_id" in column_dict + driver_id_col = column_dict["driver_id"] + assert driver_id_col.data_type == "int64" + assert driver_id_col.description == "Unique driver identifier" + + # Check timestamp column + assert "event_timestamp" in column_dict + ts_col = column_dict["event_timestamp"] + assert ts_col.data_type == "timestamp" + + # Check feature columns + assert "conv_rate" in column_dict + assert column_dict["conv_rate"].data_type == "float64" + + assert "avg_daily_trips" in column_dict + assert column_dict["avg_daily_trips"].data_type == "int32" + + +class TestDbtToFeastMapping: + """Test mapping dbt models to Feast objects.""" + + def test_create_bigquery_data_source(self, parser): + """Test creating BigQuery data source from dbt model.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + model = parser.get_model_by_name("driver_features") + + data_source = mapper.create_data_source(model) + + assert isinstance(data_source, BigQuerySource) + assert data_source.name == "driver_features_source" + # Table name is built from model's full_table_name (database.schema.alias) + assert data_source.table == model.full_table_name + assert data_source.timestamp_field == "event_timestamp" + assert data_source.description == model.description + assert "dbt.model" in data_source.tags + assert data_source.tags["dbt.model"] == "driver_features" + + def test_create_snowflake_data_source(self, parser): + """Test creating Snowflake data source from dbt model.""" + mapper = DbtToFeastMapper(data_source_type="snowflake") + model = parser.get_model_by_name("customer_features") + + data_source = mapper.create_data_source(model) + + assert isinstance(data_source, SnowflakeSource) + assert data_source.name == "customer_features_source" + # Database and schema come from the model (varies by adapter) + assert data_source.database == model.database + assert data_source.schema == model.schema + assert data_source.table == "customer_features" + assert data_source.timestamp_field == "event_timestamp" + + def test_create_file_data_source(self, parser): + """Test creating File data source from dbt model.""" + mapper = DbtToFeastMapper(data_source_type="file") + model = parser.get_model_by_name("product_features") + + data_source = mapper.create_data_source(model) + + assert isinstance(data_source, FileSource) + assert data_source.name == "product_features_source" + assert data_source.path == "/data/product_features.parquet" + assert data_source.timestamp_field == "event_timestamp" + + def test_create_entity(self): + """Test creating Feast Entity.""" + mapper = DbtToFeastMapper() + + entity = mapper.create_entity( + name="driver_id", + description="Driver entity", + ) + + assert isinstance(entity, Entity) + assert entity.name == "driver_id" + assert entity.join_key == "driver_id" + assert entity.description == "Driver entity" + + def test_create_feature_view(self, parser): + """Test creating Feast FeatureView from dbt model.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + model = parser.get_model_by_name("driver_features") + + data_source = mapper.create_data_source(model) + entity = mapper.create_entity( + "driver_id", description="Driver entity", value_type=ValueType.INT64 + ) + + feature_view = mapper.create_feature_view( + model=model, + source=data_source, + entity_columns="driver_id", + entities=entity, + timestamp_field="event_timestamp", + ttl_days=1, + ) + + assert isinstance(feature_view, FeatureView) + assert feature_view.name == "driver_features" + assert feature_view.source == data_source + assert len(feature_view.entities) == 1 + assert feature_view.entities[0] == entity.name # entities is a list of names + assert feature_view.description == model.description + + # Check that schema excludes timestamp but includes entity columns + # (FeatureView.__init__ expects entity columns in schema and extracts them) + feature_names = {f.name for f in feature_view.schema} + assert "event_timestamp" not in feature_names # Timestamp column excluded + assert "driver_id" in feature_names # Entity column kept in schema + assert "conv_rate" in feature_names + assert "acc_rate" in feature_names + assert "avg_daily_trips" in feature_names + + # Check feature types + feature_dict = {f.name: f for f in feature_view.schema} + assert feature_dict["conv_rate"].dtype == Float64 + assert feature_dict["acc_rate"].dtype == Float64 + assert feature_dict["avg_daily_trips"].dtype == Int32 + + def test_create_feature_view_with_exclude_columns(self, parser): + """Test creating FeatureView with excluded columns.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + model = parser.get_model_by_name("driver_features") + + data_source = mapper.create_data_source(model) + entity = mapper.create_entity("driver_id", value_type=ValueType.INT64) + + feature_view = mapper.create_feature_view( + model=model, + source=data_source, + entity_columns="driver_id", + entities=entity, + exclude_columns=["acc_rate"], # Exclude specific feature + ) + + feature_names = {f.name for f in feature_view.schema} + assert "acc_rate" not in feature_names + assert "conv_rate" in feature_names + assert "avg_daily_trips" in feature_names + + def test_create_all_from_model(self, parser): + """Test creating all Feast objects at once from dbt model.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + model = parser.get_model_by_name("customer_features") + + objects = mapper.create_all_from_model( + model=model, + entity_columns="customer_id", + ttl_days=2, + ) + + assert "entities" in objects + assert "data_source" in objects + assert "feature_view" in objects + + assert isinstance(objects["entities"], list) + assert len(objects["entities"]) == 1 + assert isinstance(objects["entities"][0], Entity) + assert objects["entities"][0].name == "customer_id" + + assert isinstance(objects["data_source"], BigQuerySource) + assert isinstance(objects["feature_view"], FeatureView) + + # Verify the feature view uses the created entity and data source + assert objects["feature_view"].source == objects["data_source"] + assert objects["entities"][0].name in objects["feature_view"].entities + + +class TestDbtDataSourceTypes: + """Test different data source type configurations.""" + + @pytest.mark.parametrize( + "data_source_type,expected_source_class", + [ + ("bigquery", BigQuerySource), + ("snowflake", SnowflakeSource), + ("file", FileSource), + ], + ) + def test_all_data_source_types( + self, parser, data_source_type, expected_source_class + ): + """Test creating data sources for all supported types.""" + mapper = DbtToFeastMapper(data_source_type=data_source_type) + model = parser.get_model_by_name("driver_features") + + data_source = mapper.create_data_source(model) + + assert isinstance(data_source, expected_source_class) + assert data_source.timestamp_field == "event_timestamp" + + def test_unsupported_data_source_type(self, parser): + """Test that unsupported data source types raise an error.""" + mapper = DbtToFeastMapper(data_source_type="unsupported") + model = parser.get_model_by_name("driver_features") + + with pytest.raises(ValueError, match="Unsupported data_source_type"): + mapper.create_data_source(model) + + +class TestDbtCodeGeneration: + """Test code generation functionality.""" + + def test_generate_feast_code(self, parser): + """Test generating Python code from dbt models.""" + models = parser.get_models(tag_filter="feast") + + code = generate_feast_code( + models=models, + entity_columns="driver_id", + data_source_type="bigquery", + timestamp_field="event_timestamp", + ttl_days=1, + manifest_path=str(TEST_MANIFEST_PATH), + project_name="feast_integration_test", + online=True, + ) + + # Verify generated code contains expected imports + assert "from feast import Entity, FeatureView, Field" in code + assert ( + "from feast.infra.offline_stores.bigquery_source import BigQuerySource" + in code + ) + + # Verify entity definitions + assert "Entity(" in code + assert 'name="driver_id"' in code + + # Verify data source definitions + assert "BigQuerySource(" in code + assert "timestamp_field=" in code + + # Verify feature view definitions + assert "FeatureView(" in code + assert "schema=[" in code + + def test_generate_code_for_snowflake(self, parser): + """Test generating code for Snowflake data sources.""" + models = parser.get_models(model_names=["customer_features"]) + + code = generate_feast_code( + models=models, + entity_columns="customer_id", + data_source_type="snowflake", + timestamp_field="event_timestamp", + ttl_days=1, + manifest_path=str(TEST_MANIFEST_PATH), + project_name="feast_integration_test", + ) + + assert ( + "from feast.infra.offline_stores.snowflake_source import SnowflakeSource" + in code + ) + assert "SnowflakeSource(" in code + + def test_generate_code_for_file_source(self, parser): + """Test generating code for File data sources.""" + models = parser.get_models(model_names=["product_features"]) + + code = generate_feast_code( + models=models, + entity_columns="product_id", + data_source_type="file", + timestamp_field="event_timestamp", + ttl_days=1, + manifest_path=str(TEST_MANIFEST_PATH), + project_name="feast_integration_test", + ) + + assert "from feast.infra.offline_stores.file_source import FileSource" in code + assert "FileSource(" in code + + +class TestDbtTypeMapping: + """Test type mapping from dbt to Feast types.""" + + def test_string_type_mapping(self, parser): + """Test that string columns are mapped correctly.""" + model = parser.get_model_by_name("customer_features") + mapper = DbtToFeastMapper() + + data_source = mapper.create_data_source(model) + # Test that feature view creation works with string entity column + fv = mapper.create_feature_view( + model=model, + source=data_source, + entity_columns="customer_id", + ) + + # Verify feature view was created successfully + # customer_id is a string type entity column + assert fv is not None + assert fv.name == "customer_features" + + def test_integer_type_mapping(self, parser): + """Test that integer columns are mapped correctly.""" + model = parser.get_model_by_name("driver_features") + mapper = DbtToFeastMapper() + + data_source = mapper.create_data_source(model) + feature_view = mapper.create_feature_view( + model=model, + source=data_source, + entity_columns="driver_id", + ) + + feature_dict = {f.name: f for f in feature_view.schema} + + # avg_daily_trips is INT32 + assert feature_dict["avg_daily_trips"].dtype == Int32 + + def test_float_type_mapping(self, parser): + """Test that float columns are mapped correctly.""" + model = parser.get_model_by_name("product_features") + mapper = DbtToFeastMapper() + + data_source = mapper.create_data_source(model) + feature_view = mapper.create_feature_view( + model=model, + source=data_source, + entity_columns="product_id", + ) + + feature_dict = {f.name: f for f in feature_view.schema} + + # rating_avg is FLOAT32 + assert feature_dict["rating_avg"].dtype == Float32 + + # view_count and purchase_count are INT64 + assert feature_dict["view_count"].dtype == Int64 + assert feature_dict["purchase_count"].dtype == Int64 + + +class TestDbtIntegrationWorkflow: + """Test the complete end-to-end dbt integration workflow.""" + + def test_full_workflow_bigquery(self, parser): + """Test complete workflow with BigQuery.""" + # Step 1: Parse manifest and filter models + models = parser.get_models(tag_filter="feast") + assert len(models) == 3 + + # Step 2: Create mapper + mapper = DbtToFeastMapper( + data_source_type="bigquery", + timestamp_field="event_timestamp", + ttl_days=1, + ) + + # Step 3: Create Feast objects for each model + all_objects = [] + entities = {} + + # Map entity columns to their value types based on the dbt schema + entity_value_types = { + "driver_id": ValueType.INT64, + "customer_id": ValueType.STRING, + "product_id": ValueType.STRING, + } + + for model in models: + # Determine entity column based on model + if "driver" in model.name: + entity_col = "driver_id" + elif "customer" in model.name: + entity_col = "customer_id" + elif "product" in model.name: + entity_col = "product_id" + else: + continue + + # Create or reuse entity + if entity_col not in entities: + entity = mapper.create_entity( + entity_col, value_type=entity_value_types[entity_col] + ) + entities[entity_col] = entity + all_objects.append(entity) + else: + entity = entities[entity_col] + + # Create data source + data_source = mapper.create_data_source(model) + all_objects.append(data_source) + + # Create feature view + feature_view = mapper.create_feature_view( + model=model, + source=data_source, + entity_columns=entity_col, + entities=entity, + ) + all_objects.append(feature_view) + + # Verify we created the right objects + assert len(entities) == 3 # 3 unique entities + assert len(all_objects) == 9 # 3 entities + 3 data sources + 3 feature views + + def test_code_generation_workflow(self, parser): + """Test workflow that generates Python code.""" + models = parser.get_models(model_names=["driver_features"]) + + # Generate code + code = generate_feast_code( + models=models, + entity_columns="driver_id", + data_source_type="bigquery", + timestamp_field="event_timestamp", + ttl_days=1, + manifest_path=str(TEST_MANIFEST_PATH), + project_name="feast_integration_test", + ) + + # Verify code is valid Python (basic check) + assert code.startswith('"""') + assert "from feast import" in code + assert "Entity(" in code + assert "FeatureView(" in code + + # Write to temp file and verify it can be read + with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f: + f.write(code) + temp_path = f.name + + try: + # Verify file was written + assert os.path.exists(temp_path) + with open(temp_path, "r") as f: + read_code = f.read() + assert read_code == code + finally: + os.unlink(temp_path) + + +class TestDbtErrorHandling: + """Test error handling for edge cases.""" + + def test_manifest_not_found(self): + """Test that FileNotFoundError is raised for missing manifest.""" + parser = DbtManifestParser("/nonexistent/path/manifest.json") + + with pytest.raises(FileNotFoundError, match="dbt manifest not found"): + parser.parse() + + def test_invalid_json_manifest(self, tmp_path): + """Test that ValueError is raised for invalid JSON.""" + invalid_manifest = tmp_path / "manifest.json" + invalid_manifest.write_text("{ invalid json }") + + parser = DbtManifestParser(str(invalid_manifest)) + + with pytest.raises(ValueError, match="Invalid JSON"): + parser.parse() + + +class TestDbtTypeMappingEdgeCases: + """Test type mapping edge cases.""" + + def test_unknown_type_falls_back_to_string(self): + """Test that unknown types fall back to String.""" + from feast.dbt.mapper import map_dbt_type_to_feast_type + + result = map_dbt_type_to_feast_type("UNKNOWN_TYPE") + assert result == String + + def test_empty_type_falls_back_to_string(self): + """Test that empty/null types fall back to String.""" + from feast.dbt.mapper import map_dbt_type_to_feast_type + + assert map_dbt_type_to_feast_type("") == String + # Test None handling (type: ignore for static analysis) + assert map_dbt_type_to_feast_type(None) == String # type: ignore[arg-type] + + def test_bool_type_mapping(self): + """Test boolean type mapping.""" + from feast.dbt.mapper import map_dbt_type_to_feast_type + from feast.types import Bool + + assert map_dbt_type_to_feast_type("BOOL") == Bool + assert map_dbt_type_to_feast_type("BOOLEAN") == Bool + assert map_dbt_type_to_feast_type("bool") == Bool + + def test_parameterized_varchar_mapping(self): + """Test that VARCHAR(N) maps to String.""" + from feast.dbt.mapper import map_dbt_type_to_feast_type + + assert map_dbt_type_to_feast_type("VARCHAR(255)") == String + assert map_dbt_type_to_feast_type("VARCHAR(100)") == String + assert map_dbt_type_to_feast_type("CHAR(10)") == String + + def test_snowflake_number_with_precision(self): + """Test Snowflake NUMBER with precision/scale mapping.""" + from feast.dbt.mapper import map_dbt_type_to_feast_type + + # No decimal places -> Int + assert map_dbt_type_to_feast_type("NUMBER(10,0)") == Int64 + assert map_dbt_type_to_feast_type("NUMBER(5,0)") == Int32 + + # With decimal places -> Float64 + assert map_dbt_type_to_feast_type("NUMBER(10,2)") == Float64 + assert map_dbt_type_to_feast_type("NUMBER(18,4)") == Float64 + + def test_array_type_mapping(self): + """Test ARRAY type mapping.""" + from feast.dbt.mapper import map_dbt_type_to_feast_type + from feast.types import Array + + result = map_dbt_type_to_feast_type("ARRAY") + assert isinstance(result, Array) + + result = map_dbt_type_to_feast_type("ARRAY") + assert isinstance(result, Array) + + +class TestDbtDataSourceAdvanced: + """Test advanced data source features.""" + + def test_created_timestamp_column(self, parser): + """Test that created_timestamp_column is passed correctly.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + model = parser.get_model_by_name("driver_features") + + data_source = mapper.create_data_source( + model, + created_timestamp_column="created_at", + ) + + assert data_source.created_timestamp_column == "created_at" + + def test_custom_timestamp_field(self, parser): + """Test custom timestamp field override.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + model = parser.get_model_by_name("driver_features") + + data_source = mapper.create_data_source( + model, + timestamp_field="custom_ts", + ) + + assert data_source.timestamp_field == "custom_ts" + + +class TestDbtCodeGenerationAdvanced: + """Test advanced code generation scenarios.""" + + def test_generated_code_is_valid_python(self, parser): + """Test that generated code can be parsed as valid Python.""" + import ast + + models = parser.get_models(model_names=["driver_features"]) + + code = generate_feast_code( + models=models, + entity_columns="driver_id", + data_source_type="bigquery", + timestamp_field="event_timestamp", + ttl_days=1, + manifest_path=str(TEST_MANIFEST_PATH), + project_name="test", + ) + + # This will raise SyntaxError if code is invalid + ast.parse(code) + + def test_generated_code_has_correct_imports(self, parser): + """Test that generated code has all necessary type imports.""" + models = parser.get_models(model_names=["driver_features"]) + + code = generate_feast_code( + models=models, + entity_columns="driver_id", + data_source_type="bigquery", + timestamp_field="event_timestamp", + ttl_days=1, + manifest_path=str(TEST_MANIFEST_PATH), + project_name="test", + ) + + # Check that all used types are imported + assert "Float64" in code + assert "Int32" in code + + +# --------------------------------------------------------------------------- +# Helper to build synthetic DbtModel objects for targeted tests +# --------------------------------------------------------------------------- +def _make_model( + name="test_model", + columns=None, + tags=None, + description="", + database="testdb", + schema="public", +): + """Build a DbtModel without needing a manifest.""" + if columns is None: + columns = [ + DbtColumn(name="entity_id", data_type="string"), + DbtColumn(name="event_timestamp", data_type="timestamp"), + DbtColumn(name="feature_a", data_type="float64"), + ] + return DbtModel( + name=name, + unique_id=f"model.test.{name}", + database=database, + schema=schema, + alias=name, + description=description, + columns=columns, + tags=tags or ["feast"], + ) + + +def _run_generated_code(code): + """Write generated code to a temp module and import it, returning its namespace.""" + import importlib.util + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, prefix="feast_gen_" + ) as f: + f.write(code) + mod_path = f.name + + try: + spec = importlib.util.spec_from_file_location("_feast_gen", mod_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return vars(mod) + finally: + os.unlink(mod_path) + + +# ============================================================================ +# Gap #1 – Generated code is never executed +# ============================================================================ +class TestCodegenExecution: + """Verify generated code actually produces valid Feast objects when run.""" + + def test_run_generated_bigquery_code(self, parser): + """Import generated BigQuery code and inspect the resulting objects.""" + models = parser.get_models(model_names=["customer_features"]) + code = generate_feast_code( + models=models, + entity_columns="customer_id", + data_source_type="bigquery", + timestamp_field="event_timestamp", + ttl_days=7, + manifest_path=str(TEST_MANIFEST_PATH), + project_name="run_test", + ) + + ns = _run_generated_code(code) + + # Entity + assert "customer_id" in ns + entity = ns["customer_id"] + assert isinstance(entity, Entity) + assert entity.name == "customer_id" + + # DataSource + assert "customer_features_source" in ns + src = ns["customer_features_source"] + assert isinstance(src, BigQuerySource) + + # FeatureView + assert "customer_features_fv" in ns + fv = ns["customer_features_fv"] + assert isinstance(fv, FeatureView) + assert fv.name == "customer_features" + assert fv.online is True + + def test_run_generated_file_source_code(self, parser): + """Import generated File source code.""" + models = parser.get_models(model_names=["product_features"]) + code = generate_feast_code( + models=models, + entity_columns="product_id", + data_source_type="file", + timestamp_field="event_timestamp", + ttl_days=1, + manifest_path=str(TEST_MANIFEST_PATH), + project_name="run_test", + ) + ns = _run_generated_code(code) + + assert isinstance(ns["product_features_fv"], FeatureView) + assert isinstance(ns["product_features_source"], FileSource) + + def test_run_generated_snowflake_code(self, parser): + """Import generated Snowflake code.""" + models = parser.get_models(model_names=["customer_features"]) + code = generate_feast_code( + models=models, + entity_columns="customer_id", + data_source_type="snowflake", + timestamp_field="event_timestamp", + ttl_days=1, + manifest_path=str(TEST_MANIFEST_PATH), + project_name="run_test", + ) + ns = _run_generated_code(code) + + assert isinstance(ns["customer_features_fv"], FeatureView) + assert isinstance(ns["customer_features_source"], SnowflakeSource) + + +# ============================================================================ +# Gap #2 – No multi-entity column tests +# ============================================================================ +class TestMultiEntityColumns: + """Test mapper and codegen with multiple entity columns.""" + + @pytest.fixture + def multi_entity_model(self): + return _make_model( + name="user_merchant_features", + columns=[ + DbtColumn(name="user_id", data_type="string"), + DbtColumn(name="merchant_id", data_type="string"), + DbtColumn(name="event_timestamp", data_type="timestamp"), + DbtColumn(name="txn_count", data_type="int64"), + DbtColumn(name="total_amount", data_type="float64"), + ], + tags=["feast"], + description="User-merchant interaction features", + ) + + def test_create_feature_view_multi_entity(self, multi_entity_model): + mapper = DbtToFeastMapper(data_source_type="bigquery") + src = mapper.create_data_source(multi_entity_model) + + fv = mapper.create_feature_view( + model=multi_entity_model, + source=src, + entity_columns=["user_id", "merchant_id"], + ) + + assert len(fv.entities) == 2 + entity_names = set(fv.entities) + assert "user_id" in entity_names + assert "merchant_id" in entity_names + + def test_create_all_from_model_multi_entity(self, multi_entity_model): + mapper = DbtToFeastMapper(data_source_type="bigquery") + + result = mapper.create_all_from_model( + model=multi_entity_model, + entity_columns=["user_id", "merchant_id"], + ) + + assert len(result["entities"]) == 2 + assert result["entities"][0].name == "user_id" + assert result["entities"][1].name == "merchant_id" + + def test_codegen_multi_entity(self, multi_entity_model): + code = generate_feast_code( + models=[multi_entity_model], + entity_columns=["user_id", "merchant_id"], + data_source_type="bigquery", + timestamp_field="event_timestamp", + ttl_days=1, + manifest_path="test", + project_name="test", + ) + + # Verify both entities are in the generated code + assert 'name="user_id"' in code + assert 'name="merchant_id"' in code + + # Verify it's executable and produces correct objects + ns = _run_generated_code(code) + fv = ns["user_merchant_features_fv"] + assert isinstance(fv, FeatureView) + assert len(fv.entities) == 2 + + def test_entity_columns_count_mismatch_raises(self, multi_entity_model): + """Providing mismatched entity_columns and entities should raise.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + src = mapper.create_data_source(multi_entity_model) + single_entity = mapper.create_entity("user_id") + + with pytest.raises(ValueError, match="must match"): + mapper.create_feature_view( + model=multi_entity_model, + source=src, + entity_columns=["user_id", "merchant_id"], + entities=single_entity, # 1 entity, 2 columns + ) + + +# ============================================================================ +# Gap #3 – Codegen model-skipping is untested +# ============================================================================ +class TestCodegenModelSkipping: + """Verify codegen silently skips models missing required columns.""" + + def test_skips_model_missing_timestamp(self): + model = _make_model( + name="no_timestamp", + columns=[ + DbtColumn(name="entity_id", data_type="string"), + DbtColumn(name="feature_a", data_type="float64"), + ], + ) + code = generate_feast_code( + models=[model], + entity_columns="entity_id", + data_source_type="bigquery", + timestamp_field="event_timestamp", + ) + # Model should be skipped — no FeatureView generated + assert "FeatureView(" not in code + + def test_skips_model_missing_entity_column(self): + model = _make_model( + name="no_entity", + columns=[ + DbtColumn(name="event_timestamp", data_type="timestamp"), + DbtColumn(name="feature_a", data_type="float64"), + ], + ) + code = generate_feast_code( + models=[model], + entity_columns="missing_col", + data_source_type="bigquery", + timestamp_field="event_timestamp", + ) + assert "FeatureView(" not in code + + def test_partial_skip_with_multiple_models(self, parser): + """When some models have required columns and some don't, only valid ones appear.""" + valid_model = parser.get_model_by_name("driver_features") + invalid_model = _make_model( + name="bad_model", + columns=[DbtColumn(name="feature_a", data_type="float64")], + ) + + code = generate_feast_code( + models=[valid_model, invalid_model], + entity_columns="driver_id", + data_source_type="bigquery", + timestamp_field="event_timestamp", + ) + + assert "driver_features_fv" in code + assert "bad_model" not in code + + +# ============================================================================ +# Gap #4 – online=False never tested +# ============================================================================ +class TestOnlineFalse: + """Verify online=False propagates through mapper and codegen.""" + + def test_feature_view_online_false(self, parser): + mapper = DbtToFeastMapper(data_source_type="bigquery") + model = parser.get_model_by_name("customer_features") + src = mapper.create_data_source(model) + + fv = mapper.create_feature_view( + model=model, + source=src, + entity_columns="customer_id", + online=False, + ) + assert fv.online is False + + def test_create_all_from_model_online_false(self, parser): + mapper = DbtToFeastMapper(data_source_type="bigquery") + model = parser.get_model_by_name("customer_features") + + result = mapper.create_all_from_model( + model=model, + entity_columns="customer_id", + online=False, + ) + assert result["feature_view"].online is False + + def test_codegen_online_false(self, parser): + models = parser.get_models(model_names=["customer_features"]) + code = generate_feast_code( + models=models, + entity_columns="customer_id", + data_source_type="bigquery", + timestamp_field="event_timestamp", + online=False, + ) + assert "online=False" in code + + ns = _run_generated_code(code) + assert ns["customer_features_fv"].online is False + + +# ============================================================================ +# Gap #5 – No CLI test coverage +# ============================================================================ +class TestDbtCli: + """Test the `feast dbt import` and `feast dbt list` CLI commands.""" + + @staticmethod + def _get_cli_output(result) -> str: + """Return combined stdout/stderr across Click versions.""" + stderr = getattr(result, "stderr", "") + if not stderr: + stderr_bytes = getattr(result, "stderr_bytes", b"") + if stderr_bytes: + stderr = stderr_bytes.decode("utf-8", errors="replace") + return result.output + stderr + + def test_dbt_list_command(self, manifest_path): + from click.testing import CliRunner + + from feast.cli.dbt_import import list_command + + runner = CliRunner() + result = runner.invoke(list_command, ["-m", manifest_path]) + assert result.exit_code == 0 + assert "driver_features" in result.output + assert "customer_features" in result.output + assert "product_features" in result.output + + def test_dbt_list_with_tag_filter(self, manifest_path): + from click.testing import CliRunner + + from feast.cli.dbt_import import list_command + + runner = CliRunner() + result = runner.invoke(list_command, ["-m", manifest_path, "--tag", "ml"]) + assert result.exit_code == 0 + assert "driver_features" in result.output + assert "customer_features" in result.output + # product_features doesn't have the 'ml' tag + assert "product_features" not in result.output + + def test_dbt_list_show_columns(self, manifest_path): + from click.testing import CliRunner + + from feast.cli.dbt_import import list_command + + runner = CliRunner() + result = runner.invoke(list_command, ["-m", manifest_path, "--show-columns"]) + assert result.exit_code == 0 + assert "driver_id" in result.output + assert "conv_rate" in result.output + + def test_dbt_list_import_error(self, manifest_path, monkeypatch): + from click.testing import CliRunner + + from feast.cli.dbt_import import list_command + from feast.dbt.parser import DbtManifestParser + + def _raise_import_error(self): # pragma: no cover - behavior tested via CLI + raise ImportError( + "dbt-artifacts-parser is required for dbt integration.\n" + "Install with: pip install 'feast[dbt]' or pip install dbt-artifacts-parser" + ) + + monkeypatch.setattr(DbtManifestParser, "parse", _raise_import_error) + + runner = CliRunner() + result = runner.invoke(list_command, ["-m", manifest_path]) + output = self._get_cli_output(result) + assert result.exit_code == 1 + assert "dbt-artifacts-parser is required for dbt integration" in output + assert "feast[dbt]" in output + + def test_dbt_import_dry_run(self, manifest_path): + from click.testing import CliRunner + + from feast.cli.dbt_import import import_command + + runner = CliRunner() + result = runner.invoke( + import_command, + [ + "-m", + manifest_path, + "-e", + "driver_id", + "-d", + "bigquery", + "--tag", + "ml", + "--dry-run", + ], + obj={"CHDIR": ".", "FS_YAML_FILE": "feature_store.yaml"}, + catch_exceptions=False, + ) + assert result.exit_code == 0 + assert "Dry run" in result.output + + def test_dbt_import_import_error(self, manifest_path, monkeypatch): + from click.testing import CliRunner + + from feast.cli.dbt_import import import_command + from feast.dbt.parser import DbtManifestParser + + def _raise_import_error(self): # pragma: no cover - behavior tested via CLI + raise ImportError( + "dbt-artifacts-parser is required for dbt integration.\n" + "Install with: pip install 'feast[dbt]' or pip install dbt-artifacts-parser" + ) + + monkeypatch.setattr(DbtManifestParser, "parse", _raise_import_error) + + runner = CliRunner() + result = runner.invoke( + import_command, + [ + "-m", + manifest_path, + "-e", + "driver_id", + "--dry-run", + ], + obj={"CHDIR": ".", "FS_YAML_FILE": "feature_store.yaml"}, + catch_exceptions=False, + ) + output = self._get_cli_output(result) + assert result.exit_code == 1 + assert "dbt-artifacts-parser is required for dbt integration" in output + assert "feast[dbt]" in output + + def test_dbt_import_output_codegen(self, manifest_path, tmp_path): + from click.testing import CliRunner + + from feast.cli.dbt_import import import_command + + output_file = str(tmp_path / "features.py") + runner = CliRunner() + result = runner.invoke( + import_command, + [ + "-m", + manifest_path, + "-e", + "customer_id", + "-d", + "bigquery", + "--tag", + "ml", + "--output", + output_file, + ], + obj={"CHDIR": ".", "FS_YAML_FILE": "feature_store.yaml"}, + catch_exceptions=False, + ) + assert result.exit_code == 0 + assert os.path.exists(output_file) + code = open(output_file).read() + assert "FeatureView(" in code + # Verify the generated file is valid python + ast.parse(code) + + def test_dbt_import_duplicate_entity_columns(self, manifest_path): + from click.testing import CliRunner + + from feast.cli.dbt_import import import_command + + runner = CliRunner() + result = runner.invoke( + import_command, + [ + "-m", + manifest_path, + "-e", + "driver_id", + "-e", + "driver_id", + "-d", + "bigquery", + "--dry-run", + ], + obj={"CHDIR": ".", "FS_YAML_FILE": "feature_store.yaml"}, + ) + assert result.exit_code != 0 + assert "Duplicate" in result.output or "duplicate" in result.output.lower() + + def test_dbt_import_no_matching_models(self, manifest_path): + from click.testing import CliRunner + + from feast.cli.dbt_import import import_command + + runner = CliRunner() + result = runner.invoke( + import_command, + [ + "-m", + manifest_path, + "-e", + "driver_id", + "--tag", + "nonexistent_tag", + "--dry-run", + ], + obj={"CHDIR": ".", "FS_YAML_FILE": "feature_store.yaml"}, + ) + assert result.exit_code == 0 + assert "No models found" in result.output + + +# ============================================================================ +# Gap #6 – Special characters in descriptions not tested through codegen +# ============================================================================ +class TestCodegenSpecialCharacters: + """Test that descriptions with special characters produce valid code.""" + + @pytest.mark.parametrize( + "desc", + [ + 'Model with "quoted" text', + "Line1\nLine2", + "Path\\to\\file", + 'Mixed "quotes" and\nnewlines\\here', + ], + ) + def test_description_escaping_produces_valid_code(self, desc): + model = _make_model(name="special_desc_model", description=desc) + code = generate_feast_code( + models=[model], + entity_columns="entity_id", + data_source_type="bigquery", + timestamp_field="event_timestamp", + ) + # Must be valid Python + ast.parse(code) + + # Must be executable without errors + ns = _run_generated_code(code) + assert isinstance(ns["special_desc_model_fv"], FeatureView) + + +# ============================================================================ +# Gap #7 – _make_var_name() with hyphens, spaces, digit-prefixed names +# ============================================================================ +class TestMakeVarName: + """Test variable name sanitization.""" + + def test_hyphens_replaced(self): + assert _make_var_name("my-model") == "my_model" + + def test_spaces_replaced(self): + assert _make_var_name("my model") == "my_model" + + def test_digit_prefix_gets_underscore(self): + assert _make_var_name("3phase") == "_3phase" + + def test_combined(self): + assert _make_var_name("3-phase model") == "_3_phase_model" + + def test_already_valid(self): + assert _make_var_name("good_name") == "good_name" + + +# ============================================================================ +# Gap #8 – Validation error paths: empty entity_columns, entity count mismatch +# ============================================================================ +class TestValidationErrors: + """Test that invalid inputs produce clear errors.""" + + def test_empty_entity_columns_in_feature_view(self, parser): + mapper = DbtToFeastMapper(data_source_type="bigquery") + model = parser.get_model_by_name("driver_features") + src = mapper.create_data_source(model) + + with pytest.raises(ValueError, match="At least one entity column"): + mapper.create_feature_view(model=model, source=src, entity_columns=[]) + + def test_empty_entity_columns_in_codegen(self): + model = _make_model() + with pytest.raises(ValueError, match="At least one entity column"): + generate_feast_code( + models=[model], + entity_columns=[], + data_source_type="bigquery", + timestamp_field="event_timestamp", + ) + + +# ============================================================================ +# Gap #9 – Combined filters (tag_filter + model_names) untested +# ============================================================================ +class TestCombinedFilters: + """Test parser with both tag_filter and model_names applied.""" + + def test_tag_and_model_name_combined(self, parser): + models = parser.get_models( + tag_filter="ml", + model_names=["driver_features"], + ) + assert len(models) == 1 + assert models[0].name == "driver_features" + + def test_tag_and_model_name_no_overlap(self, parser): + # product_features has 'recommendations' tag but not 'ml' + models = parser.get_models( + tag_filter="ml", + model_names=["product_features"], + ) + assert len(models) == 0 + + def test_nonexistent_tag(self, parser): + models = parser.get_models(tag_filter="nonexistent") + assert len(models) == 0 + + def test_nonexistent_model_name(self, parser): + models = parser.get_models(model_names=["does_not_exist"]) + assert len(models) == 0 + + +# ============================================================================ +# Gap #10 – Lazy parse behavior untested +# ============================================================================ +class TestLazyParse: + """Verify get_models() auto-calls parse() if not called explicitly.""" + + def test_get_models_without_explicit_parse(self, manifest_path): + parser = DbtManifestParser(manifest_path) + # Don't call parser.parse() — get_models should do it lazily + models = parser.get_models() + assert len(models) == 3 + + def test_get_model_by_name_without_explicit_parse(self, manifest_path): + parser = DbtManifestParser(manifest_path) + model = parser.get_model_by_name("driver_features") + assert model is not None + assert model.name == "driver_features" + + +# ============================================================================ +# Gap #11 – DbtCodeGenerator class never tested directly +# ============================================================================ +class TestDbtCodeGeneratorClass: + """Test the DbtCodeGenerator class directly (not the convenience function).""" + + def test_basic_generation(self, parser): + gen = DbtCodeGenerator( + data_source_type="bigquery", + timestamp_field="event_timestamp", + ttl_days=3, + ) + models = parser.get_models(model_names=["driver_features"]) + code = gen.generate( + models=models, + entity_columns="driver_id", + manifest_path="test/manifest.json", + project_name="my_project", + ) + ast.parse(code) + assert "FeatureView(" in code + assert "timedelta(days=3)" in code + + def test_exclude_columns(self, parser): + gen = DbtCodeGenerator(data_source_type="bigquery") + models = parser.get_models(model_names=["driver_features"]) + code = gen.generate( + models=models, + entity_columns="driver_id", + exclude_columns=["acc_rate"], + ) + assert "acc_rate" not in code + assert "conv_rate" in code + + def test_case_insensitive_data_source_type(self, parser): + gen = DbtCodeGenerator(data_source_type="BigQuery") + models = parser.get_models(model_names=["driver_features"]) + code = gen.generate(models=models, entity_columns="driver_id") + assert "BigQuerySource(" in code + + +# ============================================================================ +# Additional: _escape_description edge cases +# ============================================================================ +class TestEscapeDescription: + """Test the _escape_description helper.""" + + def test_none_returns_empty(self): + assert _escape_description(None) == "" + + def test_empty_returns_empty(self): + assert _escape_description("") == "" + + def test_quotes_escaped(self): + assert '\\"' in _escape_description('has "quotes"') + + def test_newlines_replaced(self): + result = _escape_description("line1\nline2") + assert "\n" not in result + + def test_backslashes_escaped(self): + result = _escape_description("path\\to\\file") + assert "\\\\" in result diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/.gitignore b/sdk/python/tests/integration/dbt/test_dbt_project/.gitignore new file mode 100644 index 00000000000..7a8eff8b13e --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/.gitignore @@ -0,0 +1,3 @@ +# Ignore dbt artifacts +*.pyc +__pycache__/ diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/dbt_project.yml b/sdk/python/tests/integration/dbt/test_dbt_project/dbt_project.yml new file mode 100644 index 00000000000..7c373eb8acd --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/dbt_project.yml @@ -0,0 +1,25 @@ +name: 'feast_integration_test' +version: '1.0.0' +config-version: 2 + +# Project profile (for testing we don't need real connections) +profile: 'test' + +# Model directory +model-paths: ["models"] + +# Seed directory +seed-paths: ["seeds"] + +# Target directory where manifest.json will be generated +target-path: "target" + +# Configure models +models: + feast_integration_test: + +materialized: table + +# Configure seeds +seeds: + feast_integration_test: + +schema: raw diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/models/customer_features.sql b/sdk/python/tests/integration/dbt/test_dbt_project/models/customer_features.sql new file mode 100644 index 00000000000..eea5635e380 --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/models/customer_features.sql @@ -0,0 +1,10 @@ +-- Customer statistics feature model +-- Features for customer behavior + +SELECT + customer_id, + event_timestamp, + total_orders, + total_spent, + avg_order_value +FROM {{ ref('customer_stats') }} diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/models/driver_features.sql b/sdk/python/tests/integration/dbt/test_dbt_project/models/driver_features.sql new file mode 100644 index 00000000000..1bed6ad184e --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/models/driver_features.sql @@ -0,0 +1,10 @@ +-- Driver statistics feature model +-- This model aggregates driver-level features for ML + +SELECT + driver_id, + event_timestamp, + conv_rate, + acc_rate, + avg_daily_trips +FROM {{ ref('driver_hourly_stats') }} diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/models/product_features.sql b/sdk/python/tests/integration/dbt/test_dbt_project/models/product_features.sql new file mode 100644 index 00000000000..2c09781da51 --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/models/product_features.sql @@ -0,0 +1,10 @@ +-- Product recommendation features +-- Tagged with 'feast' for filtering tests + +SELECT + product_id, + event_timestamp, + view_count, + purchase_count, + rating_avg +FROM {{ ref('product_stats') }} diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/models/schema.yml b/sdk/python/tests/integration/dbt/test_dbt_project/models/schema.yml new file mode 100644 index 00000000000..7aef1a51fa8 --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/models/schema.yml @@ -0,0 +1,86 @@ +version: 2 + +# Seeds will be loaded as tables +seeds: + - name: driver_hourly_stats + description: "Raw driver hourly statistics" + - name: customer_stats + description: "Raw customer statistics" + - name: product_stats + description: "Raw product statistics" + +models: + - name: driver_features + description: "Driver hourly features for ML models" + config: + tags: ["feast", "ml", "driver"] + columns: + - name: driver_id + description: "Unique driver identifier" + data_type: int64 + tests: + - not_null + - name: event_timestamp + description: "Event timestamp" + data_type: timestamp + tests: + - not_null + - name: conv_rate + description: "Conversion rate" + data_type: float64 + - name: acc_rate + description: "Acceptance rate" + data_type: float64 + - name: avg_daily_trips + description: "Average daily trips" + data_type: int32 + + - name: customer_features + description: "Customer behavior features" + config: + tags: ["feast", "ml", "customer"] + columns: + - name: customer_id + description: "Unique customer identifier" + data_type: string + tests: + - not_null + - name: event_timestamp + description: "Event timestamp" + data_type: timestamp + tests: + - not_null + - name: total_orders + description: "Total number of orders" + data_type: int64 + - name: total_spent + description: "Total amount spent" + data_type: float64 + - name: avg_order_value + description: "Average order value" + data_type: float64 + + - name: product_features + description: "Product recommendation features" + config: + tags: ["feast", "recommendations"] + columns: + - name: product_id + description: "Unique product identifier" + data_type: string + tests: + - not_null + - name: event_timestamp + description: "Event timestamp" + data_type: timestamp + tests: + - not_null + - name: view_count + description: "Number of views" + data_type: int64 + - name: purchase_count + description: "Number of purchases" + data_type: int64 + - name: rating_avg + description: "Average rating" + data_type: float32 diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/profiles.yml b/sdk/python/tests/integration/dbt/test_dbt_project/profiles.yml new file mode 100644 index 00000000000..3a832125075 --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/profiles.yml @@ -0,0 +1,10 @@ +# Profiles for testing dbt compilation +# We use DuckDB for local testing as it doesn't require external services + +test: + target: dev + outputs: + dev: + type: duckdb + path: ':memory:' + threads: 1 diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/seeds/customer_stats.csv b/sdk/python/tests/integration/dbt/test_dbt_project/seeds/customer_stats.csv new file mode 100644 index 00000000000..0f991b45298 --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/seeds/customer_stats.csv @@ -0,0 +1,4 @@ +customer_id,event_timestamp,total_orders,total_spent,avg_order_value +cust_001,2024-01-01 00:00:00,5,250.50,50.10 +cust_002,2024-01-01 00:00:00,3,180.75,60.25 +cust_003,2024-01-01 00:00:00,7,420.00,60.00 diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/seeds/driver_hourly_stats.csv b/sdk/python/tests/integration/dbt/test_dbt_project/seeds/driver_hourly_stats.csv new file mode 100644 index 00000000000..375d289ab7e --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/seeds/driver_hourly_stats.csv @@ -0,0 +1,4 @@ +driver_id,event_timestamp,conv_rate,acc_rate,avg_daily_trips +1001,2024-01-01 00:00:00,0.85,0.92,12 +1002,2024-01-01 00:00:00,0.78,0.88,15 +1003,2024-01-01 00:00:00,0.91,0.95,10 diff --git a/sdk/python/tests/integration/dbt/test_dbt_project/seeds/product_stats.csv b/sdk/python/tests/integration/dbt/test_dbt_project/seeds/product_stats.csv new file mode 100644 index 00000000000..527555811a8 --- /dev/null +++ b/sdk/python/tests/integration/dbt/test_dbt_project/seeds/product_stats.csv @@ -0,0 +1,4 @@ +product_id,event_timestamp,view_count,purchase_count,rating_avg +prod_001,2024-01-01 00:00:00,150,25,4.5 +prod_002,2024-01-01 00:00:00,200,30,4.2 +prod_003,2024-01-01 00:00:00,100,15,4.8 diff --git a/sdk/python/tests/integration/doctest/__init__.py b/sdk/python/tests/integration/doctest/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/doctest/test_all.py b/sdk/python/tests/integration/doctest/test_all.py similarity index 75% rename from sdk/python/tests/doctest/test_all.py rename to sdk/python/tests/integration/doctest/test_all.py index 8a85a72ab45..9620aabf55b 100644 --- a/sdk/python/tests/doctest/test_all.py +++ b/sdk/python/tests/integration/doctest/test_all.py @@ -5,9 +5,13 @@ import traceback import unittest +import pytest + import feast from feast.utils import _utc_now +pytestmark = pytest.mark.integration + FILES_TO_IGNORE = {"app"} @@ -72,20 +76,41 @@ def test_docstrings(): for package in current_packages: try: - for _, name, is_pkg in pkgutil.walk_packages(package.__path__): + for _, name, is_pkg in pkgutil.walk_packages( + package.__path__, onerror=lambda _: None + ): if name in FILES_TO_IGNORE: continue full_name = package.__name__ + "." + name + temp_module = None try: # https://github.com/feast-dev/feast/issues/5088 - if "ikv" not in full_name and "milvus" not in full_name: + # Skip ray_transformation doctests - they hang on macOS due to + # Ray worker spawning issues with uv-managed environments + if ( + "milvus" not in full_name + and "openlineage" not in full_name + and "ray_transformation" not in full_name + and "qdrant" not in full_name + ): temp_module = importlib.import_module(full_name) if is_pkg: next_packages.append(temp_module) - except ModuleNotFoundError: + except Exception: # noqa: BLE001 + # Gracefully skip modules that fail to import due to: + # - ModuleNotFoundError: optional system dependency missing + # - FeastExtrasDependencyImportError: optional Python extra + # missing (e.g. pymongo, couchbase) + # - TypeError or other errors: third-party libraries with + # internal incompatibilities at import time (e.g. + # qdrant_client raises TypeError when a grpc EnumTypeWrapper + # is used with the | union operator on Python < 3.12) pass + if temp_module is None: + continue + # Retrieve the setup and teardown functions defined in this file. relative_path_from_feast = full_name.split(".", 1)[1] function_suffix = relative_path_from_feast.replace(".", "_") diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/mysql.py b/sdk/python/tests/integration/feature_repos/universal/online_store/mysql.py deleted file mode 100644 index 093295c86ba..00000000000 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/mysql.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Dict - -from testcontainers.mysql import MySqlContainer - -from tests.integration.feature_repos.universal.online_store_creator import ( - OnlineStoreCreator, -) - - -class MySQLOnlineStoreCreator(OnlineStoreCreator): - def __init__(self, project_name: str, **kwargs): - super().__init__(project_name) - self.container = ( - MySqlContainer("mysql:latest", platform="linux/amd64") - .with_exposed_ports(3306) - .with_env("MYSQL_USER", "root") - .with_env("MYSQL_PASSWORD", "test") - .with_env("MYSQL_DATABASE", "test") - ) - - def create_online_store(self) -> Dict[str, str]: - self.container.start() - exposed_port = self.container.get_exposed_port(3306) - return { - "type": "mysql", - "user": "root", - "password": "test", - "database": "test", - "port": exposed_port, - } - - def teardown(self): - self.container.stop() diff --git a/sdk/python/tests/integration/local_feast_tests/__init__.py b/sdk/python/tests/integration/local_feast_tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/unit/local_feast_tests/test_e2e_local.py b/sdk/python/tests/integration/local_feast_tests/test_e2e_local.py similarity index 86% rename from sdk/python/tests/unit/local_feast_tests/test_e2e_local.py rename to sdk/python/tests/integration/local_feast_tests/test_e2e_local.py index fa272c4847f..878b6336aa0 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_e2e_local.py +++ b/sdk/python/tests/integration/local_feast_tests/test_e2e_local.py @@ -1,9 +1,11 @@ import os +import platform import tempfile from datetime import datetime, timedelta from pathlib import Path import pandas as pd +import pytest from feast import Entity, FeatureView, Field, FileSource from feast.driver_test_data import ( @@ -16,7 +18,14 @@ from tests.utils.cli_repo_creator import CliRunner, get_example_repo from tests.utils.feature_records import validate_online_features +pytestmark = pytest.mark.integration + +@pytest.mark.skipif( + platform.system() == "Darwin" and os.environ.get("CI") == "true", + reason="Skip on macOS CI due to Ray/uv subprocess compatibility issues", +) +@pytest.mark.timeout(600) def test_e2e_local() -> None: """ Tests the end-to-end workflow of apply, materialize, and online retrieval. @@ -36,15 +45,17 @@ def test_e2e_local() -> None: driver_df = create_driver_hourly_stats_df(driver_entities, start_date, end_date) driver_stats_path = os.path.join(data_dir, "driver_stats.parquet") driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) + driver_stats_path_posix = Path(driver_stats_path).as_posix() global_df = create_global_daily_stats_df(start_date, end_date) global_stats_path = os.path.join(data_dir, "global_stats.parquet") global_df.to_parquet(path=global_stats_path, allow_truncated_timestamps=True) + global_stats_path_posix = Path(global_stats_path).as_posix() with runner.local_repo( get_example_repo("example_feature_repo_2.py") - .replace("%PARQUET_PATH%", driver_stats_path) - .replace("%PARQUET_PATH_GLOBAL%", global_stats_path), + .replace("%PARQUET_PATH%", driver_stats_path_posix) + .replace("%PARQUET_PATH_GLOBAL%", global_stats_path_posix), "file", ) as store: _test_materialize_and_online_retrieval( @@ -53,8 +64,8 @@ def test_e2e_local() -> None: with runner.local_repo( get_example_repo("example_feature_repo_with_bfvs.py") - .replace("%PARQUET_PATH%", driver_stats_path) - .replace("%PARQUET_PATH_GLOBAL%", global_stats_path), + .replace("%PARQUET_PATH%", driver_stats_path_posix) + .replace("%PARQUET_PATH_GLOBAL%", global_stats_path_posix), "file", ) as store: _test_materialize_and_online_retrieval( @@ -63,8 +74,8 @@ def test_e2e_local() -> None: with runner.local_repo( get_example_repo("example_feature_repo_with_ttl_0.py") - .replace("%PARQUET_PATH%", driver_stats_path) - .replace("%PARQUET_PATH_GLOBAL%", global_stats_path), + .replace("%PARQUET_PATH%", driver_stats_path_posix) + .replace("%PARQUET_PATH_GLOBAL%", global_stats_path_posix), "file", ) as store: _test_materialize_and_online_retrieval( @@ -74,7 +85,7 @@ def test_e2e_local() -> None: # Test a failure case when the parquet file doesn't include a join key with runner.local_repo( get_example_repo("example_feature_repo_with_entity_join_key.py").replace( - "%PARQUET_PATH%", driver_stats_path + "%PARQUET_PATH%", driver_stats_path_posix ), "file", ) as store: diff --git a/sdk/python/tests/integration/materialization/kubernetes/test_k8s.py b/sdk/python/tests/integration/materialization/kubernetes/test_k8s.py index a944ae3e943..bb8d17da5bd 100644 --- a/sdk/python/tests/integration/materialization/kubernetes/test_k8s.py +++ b/sdk/python/tests/integration/materialization/kubernetes/test_k8s.py @@ -4,14 +4,14 @@ from feast import Entity, Feature, FeatureView, ValueType from tests.data.data_creator import create_basic_driver_dataset -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, RegistryLocation, ) -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_test_environment, ) -from tests.integration.feature_repos.universal.data_sources.redshift import ( +from tests.universal.feature_repos.universal.data_sources.redshift import ( RedshiftDataSourceCreator, ) from tests.utils.e2e_test_validation import validate_offline_online_store_consistency diff --git a/sdk/python/tests/integration/materialization/test_lambda.py b/sdk/python/tests/integration/materialization/test_lambda.py index f0c1e108694..8e317d7059f 100644 --- a/sdk/python/tests/integration/materialization/test_lambda.py +++ b/sdk/python/tests/integration/materialization/test_lambda.py @@ -7,14 +7,14 @@ from feast.field import Field from feast.types import Float32 from tests.data.data_creator import create_basic_driver_dataset -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, RegistryLocation, ) -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_test_environment, ) -from tests.integration.feature_repos.universal.data_sources.redshift import ( +from tests.universal.feature_repos.universal.data_sources.redshift import ( RedshiftDataSourceCreator, ) from tests.utils.e2e_test_validation import validate_offline_online_store_consistency diff --git a/sdk/python/tests/integration/materialization/test_snowflake.py b/sdk/python/tests/integration/materialization/test_snowflake.py index a783eac0380..d79be84d33f 100644 --- a/sdk/python/tests/integration/materialization/test_snowflake.py +++ b/sdk/python/tests/integration/materialization/test_snowflake.py @@ -9,17 +9,22 @@ from feast.types import Array, Bool, Bytes, Float64, Int32, Int64, String, UnixTimestamp from feast.utils import _utc_now from tests.data.data_creator import create_basic_driver_dataset -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_test_environment, ) -from tests.integration.feature_repos.universal.data_sources.snowflake import ( +from tests.universal.feature_repos.universal.data_sources.snowflake import ( SnowflakeDataSourceCreator, ) from tests.utils.e2e_test_validation import validate_offline_online_store_consistency +pytestmark = pytest.mark.skipif( + not os.getenv("SNOWFLAKE_CI_DEPLOYMENT"), + reason="Snowflake account not configured in CI (SNOWFLAKE_CI_DEPLOYMENT not set)", +) + SNOWFLAKE_ENGINE_CONFIG = { "type": "snowflake.engine", "account": os.getenv("SNOWFLAKE_CI_DEPLOYMENT", ""), diff --git a/sdk/python/tests/integration/materialization/test_universal_materialization.py b/sdk/python/tests/integration/materialization/test_universal_materialization.py index 860e9a5fc6c..3bf1eac51c2 100644 --- a/sdk/python/tests/integration/materialization/test_universal_materialization.py +++ b/sdk/python/tests/integration/materialization/test_universal_materialization.py @@ -10,8 +10,6 @@ ) from feast.on_demand_feature_view import on_demand_feature_view from feast.types import Float32, Float64 -from tests.data.data_creator import create_basic_driver_dataset -from tests.utils.e2e_test_validation import validate_offline_online_store_consistency def _create_test_entities(): @@ -217,32 +215,6 @@ def odfv_multi(df: pd.DataFrame) -> pd.DataFrame: assert resp["price_plus_revenue"][0] == 105.0 -@pytest.mark.integration -@pytest.mark.universal_offline_stores -def test_universal_materialization_consistency(environment): - fs = environment.feature_store - df = create_basic_driver_dataset() - ds = environment.data_source_creator.create_data_source( - df, - fs.project, - field_mapping={"ts_1": "ts"}, - ) - driver = Entity( - name="driver_id", - join_keys=["driver_id"], - ) - driver_stats_fv = FeatureView( - name="driver_hourly_stats", - entities=[driver], - ttl=timedelta(weeks=52), - schema=[Field(name="value", dtype=Float32)], - source=ds, - ) - fs.apply([driver, driver_stats_fv]) - split_dt = df["ts_1"][4].to_pydatetime() - timedelta(seconds=1) - validate_offline_online_store_consistency(fs, driver_stats_fv, split_dt) - - @pytest.mark.integration def test_odfv_write_methods(environment): """ diff --git a/sdk/python/tests/integration/monitoring/__init__.py b/sdk/python/tests/integration/monitoring/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/sdk/python/tests/integration/monitoring/__init__.py @@ -0,0 +1 @@ + diff --git a/sdk/python/tests/integration/monitoring/test_monitoring_integration.py b/sdk/python/tests/integration/monitoring/test_monitoring_integration.py new file mode 100644 index 00000000000..59e045bf0b5 --- /dev/null +++ b/sdk/python/tests/integration/monitoring/test_monitoring_integration.py @@ -0,0 +1,1041 @@ +"""Integration tests for the monitoring feature. + +Tests cover: +- Auto-compute (all granularities from source timestamps) +- Compute baseline (idempotent) +- Transient compute +- DQM job lifecycle +- CLI commands +- REST API endpoints +- RBAC enforcement +- Compute engine dispatch (SQL push-down vs Python fallback) +- Log source monitoring (feature serving logs) +""" + +from datetime import date, datetime, timezone +from unittest.mock import MagicMock, patch + +import pyarrow as pa +import pytest +from click.testing import CliRunner + +from feast.monitoring.monitoring_service import VALID_GRANULARITIES, MonitoringService +from feast.types import PrimitiveFeastType + +# ------------------------------------------------------------------ # +# Shared helpers +# ------------------------------------------------------------------ # + + +def _make_feature_field(name, dtype): + field = MagicMock() + field.name = name + field.dtype = dtype + return field + + +def _make_feature_view(name, features, entities=None, batch_source=None): + fv = MagicMock() + fv.name = name + fv.features = features + fv.entities = entities or [] + if batch_source is None: + batch_source = MagicMock() + batch_source.timestamp_field = "event_timestamp" + batch_source.created_timestamp_column = "" + fv.batch_source = batch_source + return fv + + +def _make_feature_service(name, fv_names, logging_config=None, feature_map=None): + """Create a mock FeatureService. + + Args: + feature_map: optional dict mapping view_name -> list of feature names. + Used to build realistic projections with features and name_to_use(). + """ + fs = MagicMock() + fs.name = name + fs.feature_view_projections = [MagicMock(name=n) for n in fv_names] + for proj, n in zip(fs.feature_view_projections, fv_names): + proj.name = n + proj.name_to_use.return_value = n + if feature_map and n in feature_map: + feats = [] + for fname in feature_map[n]: + f = MagicMock() + f.name = fname + feats.append(f) + proj.features = feats + else: + proj.features = [] + fs.logging_config = logging_config + return fs + + +def _make_logging_config_with_source(log_table_schema): + """Create a mock LoggingConfig whose destination.to_data_source() returns a DataSource.""" + logging_config = MagicMock() + mock_data_source = MagicMock() + mock_data_source.timestamp_field = "__log_timestamp" + mock_data_source.created_timestamp_column = "" + logging_config.destination.to_data_source.return_value = mock_data_source + return logging_config, mock_data_source + + +def _make_mock_store(feature_views, feature_services=None): + """Create a mock FeatureStore with offline store that uses Python fallback.""" + store = MagicMock() + store.project = "test_project" + store.config.project = "test_project" + store.config.offline_store = MagicMock() + + store.registry.list_feature_views.return_value = feature_views + store.registry.list_entities.return_value = [] + store.registry.list_feature_services.return_value = feature_services or [] + + if feature_views: + store.registry.get_feature_view.return_value = feature_views[0] + + if feature_services: + store.registry.get_feature_service.return_value = feature_services[0] + + arrow_table = pa.table( + { + "conv_rate": [0.1, 0.5, 0.9, 0.3, 0.7], + "acc_rate": [0.8, 0.6, 0.4, 0.9, 0.2], + "city": ["NYC", "LA", "NYC", "SF", "LA"], + "event_timestamp": [ + datetime(2025, 3, 25, tzinfo=timezone.utc), + datetime(2025, 3, 26, tzinfo=timezone.utc), + datetime(2025, 3, 26, tzinfo=timezone.utc), + datetime(2025, 3, 27, tzinfo=timezone.utc), + datetime(2025, 3, 27, tzinfo=timezone.utc), + ], + } + ) + + mock_retrieval = MagicMock() + mock_retrieval.to_arrow.return_value = arrow_table + + provider = MagicMock() + provider.offline_store.pull_all_from_table_or_query.return_value = mock_retrieval + provider.offline_store.compute_monitoring_metrics.side_effect = NotImplementedError + provider.offline_store.get_monitoring_max_timestamp.side_effect = ( + NotImplementedError + ) + + job_store = {} + + def _mock_save(config, metric_type, metrics): + if metric_type == "job": + for m in metrics: + job_store[m["job_id"]] = dict(m) + + def _mock_query(config, project, metric_type, filters=None, **kwargs): + if metric_type == "job": + results = list(job_store.values()) + if filters: + for key, value in filters.items(): + if value is not None: + results = [r for r in results if r.get(key) == value] + return results + return [] + + provider.offline_store.ensure_monitoring_tables.return_value = None + provider.offline_store.save_monitoring_metrics.side_effect = _mock_save + provider.offline_store.query_monitoring_metrics.side_effect = _mock_query + provider.offline_store.clear_monitoring_baseline.return_value = None + + store._get_provider.return_value = provider + + return store + + +# ------------------------------------------------------------------ # +# Test: Auto-compute +# ------------------------------------------------------------------ # + + +class TestAutoCompute: + def test_auto_compute_all_granularities(self): + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + store = _make_mock_store([fv]) + svc = MonitoringService(store) + + result = svc.auto_compute(project="test_project") + + assert result["status"] == "completed" + assert result["computed_feature_views"] == 1 + assert len(result["granularities"]) == len(VALID_GRANULARITIES) + for g in VALID_GRANULARITIES: + assert g in result["granularities"] + + provider = store._get_provider.return_value + provider.offline_store.save_monitoring_metrics.assert_called() + + def test_auto_compute_specific_view(self): + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + store = _make_mock_store([fv]) + svc = MonitoringService(store) + + result = svc.auto_compute( + project="test_project", + feature_view_name="driver_stats", + ) + + assert result["status"] == "completed" + assert result["computed_feature_views"] == 1 + + +# ------------------------------------------------------------------ # +# Test: Compute baseline +# ------------------------------------------------------------------ # + + +class TestComputeBaseline: + def test_compute_baseline_for_new_features(self): + fv = _make_feature_view( + "driver_stats", + [ + _make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64), + _make_feature_field("city", PrimitiveFeastType.STRING), + ], + ) + store = _make_mock_store([fv]) + svc = MonitoringService(store) + + result = svc.compute_baseline(project="test_project") + + assert result["status"] == "completed" + assert result["is_baseline"] is True + assert result["computed_features"] == 2 + + provider = store._get_provider.return_value + provider.offline_store.clear_monitoring_baseline.assert_called() + provider.offline_store.save_monitoring_metrics.assert_called() + + def test_baseline_idempotent_skips_existing(self): + fv = _make_feature_view( + "driver_stats", + [ + _make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64), + _make_feature_field("acc_rate", PrimitiveFeastType.FLOAT64), + ], + ) + store = _make_mock_store([fv]) + + # Simulate conv_rate already has baseline via query_monitoring_metrics + provider = store._get_provider.return_value + existing_baseline = { + "project_id": "test_project", + "feature_view_name": "driver_stats", + "feature_name": "conv_rate", + "metric_date": "2025-01-01", + "granularity": "daily", + "data_source_type": "batch", + "computed_at": datetime.now(timezone.utc).isoformat(), + "is_baseline": True, + "feature_type": "numeric", + "row_count": 100, + "null_count": 0, + "null_rate": 0.0, + "mean": 5.0, + "stddev": 1.0, + "min_val": 0.0, + "max_val": 10.0, + "p50": 5.0, + "p75": 7.5, + "p90": 9.0, + "p95": 9.5, + "p99": 9.9, + "histogram": None, + } + + original_side_effect = ( + provider.offline_store.query_monitoring_metrics.side_effect + ) + + def _query_with_baseline(config, project, metric_type, filters=None, **kwargs): + if metric_type == "feature" and filters and filters.get("is_baseline"): + return [existing_baseline] + if original_side_effect: + return original_side_effect( + config, project, metric_type, filters=filters, **kwargs + ) + return [] + + provider.offline_store.query_monitoring_metrics.side_effect = ( + _query_with_baseline + ) + + svc = MonitoringService(store) + result = svc.compute_baseline(project="test_project") + + # Only acc_rate should be computed (conv_rate already has baseline) + assert result["computed_features"] == 1 + + +# ------------------------------------------------------------------ # +# Test: Transient compute +# ------------------------------------------------------------------ # + + +class TestTransientCompute: + def test_transient_returns_metrics_without_saving(self): + fv = _make_feature_view( + "driver_stats", + [ + _make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64), + _make_feature_field("city", PrimitiveFeastType.STRING), + ], + ) + store = _make_mock_store([fv]) + svc = MonitoringService(store) + + result = svc.compute_transient( + project="test_project", + feature_view_name="driver_stats", + start_date=date(2025, 1, 1), + end_date=date(2025, 1, 15), + ) + + assert result["status"] == "completed" + assert result["start_date"] == "2025-01-01" + assert result["end_date"] == "2025-01-15" + assert len(result["metrics"]) == 2 + + # Transient should NOT call save + provider = store._get_provider.return_value + provider.offline_store.save_monitoring_metrics.assert_not_called() + + def test_transient_empty_features(self): + fv = _make_feature_view( + "fv", + [_make_feature_field("ts", PrimitiveFeastType.UNIX_TIMESTAMP)], + ) + store = _make_mock_store([fv]) + svc = MonitoringService(store) + + result = svc.compute_transient( + project="test_project", + feature_view_name="fv", + ) + assert result["metrics"] == [] + + +# ------------------------------------------------------------------ # +# Test: DQM Job Manager +# ------------------------------------------------------------------ # + + +class TestDQMJobManager: + def _make_manager(self): + from feast.monitoring.dqm_job_manager import DQMJobManager + + stored = {} + + def mock_save(config, metric_type, metrics): + for m in metrics: + stored[m["job_id"]] = dict(m) + + def mock_query(config, project, metric_type, filters=None, **kwargs): + results = list(stored.values()) + if filters: + for key, value in filters.items(): + if value is not None: + results = [r for r in results if r.get(key) == value] + return results + + offline_store = MagicMock() + offline_store.save_monitoring_metrics.side_effect = mock_save + offline_store.query_monitoring_metrics.side_effect = mock_query + return DQMJobManager(offline_store, MagicMock()) + + def test_submit_and_get_job(self): + mgr = self._make_manager() + job_id = mgr.submit( + project="test_project", + job_type="auto_compute", + feature_view_name="driver_stats", + ) + + assert job_id is not None + assert len(job_id) == 36 # UUID format + + job = mgr.get_job(job_id) + assert job is not None + assert job["project_id"] == "test_project" + assert job["job_type"] == "auto_compute" + assert job["status"] == "pending" + + def test_update_status(self): + from feast.monitoring.dqm_job_manager import JOB_STATUS_RUNNING + + mgr = self._make_manager() + job_id = mgr.submit( + project="test_project", + job_type="compute", + ) + mgr.update_status(job_id, JOB_STATUS_RUNNING) + + job = mgr.get_job(job_id) + assert job["status"] == JOB_STATUS_RUNNING + assert job["started_at"] is not None + + +# ------------------------------------------------------------------ # +# Test: CLI +# ------------------------------------------------------------------ # + + +class TestComputeMetricsCLI: + def test_help(self): + from feast.cli.monitor import monitor_cmd + + runner = CliRunner() + result = runner.invoke(monitor_cmd, ["run", "--help"]) + assert result.exit_code == 0 + assert "--granularity" in result.output + assert "--set-baseline" in result.output + assert "--feature-view" in result.output + + @patch("feast.cli.monitor.create_feature_store") + @patch("feast.monitoring.monitoring_service.MonitoringService.auto_compute") + def test_run_auto_mode(self, mock_auto, mock_create_store): + from feast.cli.monitor import monitor_cmd + + mock_store = MagicMock() + mock_store.project = "proj" + mock_create_store.return_value = mock_store + + mock_auto.return_value = { + "status": "completed", + "computed_feature_views": 2, + "computed_features": 5, + "granularities": list(VALID_GRANULARITIES), + "duration_ms": 1200, + } + + runner = CliRunner() + result = runner.invoke(monitor_cmd, ["run"]) + + assert result.exit_code == 0 + assert "Auto-computing" in result.output + assert "Features computed: 5" in result.output + mock_auto.assert_called_once() + + @patch("feast.cli.monitor.create_feature_store") + @patch("feast.monitoring.monitoring_service.MonitoringService.compute_metrics") + def test_run_explicit_granularity(self, mock_compute, mock_create_store): + from feast.cli.monitor import monitor_cmd + + mock_store = MagicMock() + mock_store.project = "proj" + mock_create_store.return_value = mock_store + + mock_compute.return_value = { + "status": "completed", + "granularity": "weekly", + "computed_features": 3, + "computed_feature_views": 1, + "computed_feature_services": 1, + "metric_dates": ["2025-01-01"], + "duration_ms": 500, + } + + runner = CliRunner() + result = runner.invoke( + monitor_cmd, + [ + "run", + "--granularity", + "weekly", + "--start-date", + "2025-01-01", + "--end-date", + "2025-01-07", + ], + ) + + assert result.exit_code == 0 + assert "Granularity: weekly" in result.output + + +# ------------------------------------------------------------------ # +# Test: REST API +# ------------------------------------------------------------------ # + + +class TestRESTEndpoints: + @pytest.fixture + def app(self): + from fastapi import FastAPI + from fastapi.testclient import TestClient + + from feast.api.registry.rest.monitoring import get_monitoring_router + + mock_handler = MagicMock() + + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + mock_store = _make_mock_store([fv]) + + app = FastAPI() + app.include_router(get_monitoring_router(mock_handler, store=mock_store)) + + mock_server = MagicMock() + mock_server.store = mock_store + return TestClient(app), mock_server + + @patch("feast.api.registry.rest.monitoring.assert_permissions") + def test_auto_compute_endpoint(self, mock_perms, app): + client, _ = app + + response = client.post( + "/monitoring/auto_compute", + json={"project": "test_project"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "completed" + assert "job_id" in data + + @patch("feast.api.registry.rest.monitoring.assert_permissions") + def test_transient_compute_endpoint(self, mock_perms, app): + client, _ = app + + response = client.post( + "/monitoring/compute/transient", + json={ + "project": "test_project", + "feature_view_name": "driver_stats", + "start_date": "2025-01-05", + "end_date": "2025-01-20", + }, + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "completed" + assert len(data["metrics"]) >= 1 + + @patch("feast.api.registry.rest.monitoring.assert_permissions") + def test_get_metrics_with_granularity(self, mock_perms, app): + client, _ = app + + response = client.get( + "/monitoring/metrics/features", + params={"project": "test_project", "granularity": "weekly"}, + ) + + assert response.status_code == 200 + + @patch("feast.api.registry.rest.monitoring.assert_permissions") + def test_get_timeseries(self, mock_perms, app): + client, _ = app + + response = client.get( + "/monitoring/metrics/timeseries", + params={ + "project": "test_project", + "feature_view_name": "driver_stats", + "granularity": "daily", + }, + ) + + assert response.status_code == 200 + + +# ------------------------------------------------------------------ # +# Test: RBAC enforcement +# ------------------------------------------------------------------ # + + +class TestRBACEnforcement: + @pytest.fixture + def app(self): + from fastapi import FastAPI + from fastapi.testclient import TestClient + + from feast.api.registry.rest.monitoring import get_monitoring_router + + mock_handler = MagicMock() + + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + mock_store = _make_mock_store([fv]) + + app = FastAPI() + app.include_router(get_monitoring_router(mock_handler, store=mock_store)) + + mock_server = MagicMock() + mock_server.store = mock_store + return TestClient(app), mock_server + + @patch("feast.api.registry.rest.monitoring.assert_permissions") + def test_compute_requires_update(self, mock_perms, app): + client, _ = app + + from feast.permissions.action import AuthzedAction + + client.post( + "/monitoring/compute", + json={ + "project": "test_project", + "feature_view_name": "driver_stats", + }, + ) + + mock_perms.assert_called() + call_args = mock_perms.call_args + assert AuthzedAction.UPDATE in call_args.kwargs.get( + "actions", call_args[1].get("actions", []) + ) + + @patch("feast.api.registry.rest.monitoring.assert_permissions") + def test_transient_requires_describe(self, mock_perms, app): + client, _ = app + + from feast.permissions.action import AuthzedAction + + client.post( + "/monitoring/compute/transient", + json={ + "project": "test_project", + "feature_view_name": "driver_stats", + }, + ) + + mock_perms.assert_called() + call_args = mock_perms.call_args + assert AuthzedAction.DESCRIBE in call_args.kwargs.get( + "actions", call_args[1].get("actions", []) + ) + + @patch("feast.api.registry.rest.monitoring.assert_permissions") + def test_read_requires_describe(self, mock_perms, app): + client, _ = app + + from feast.permissions.action import AuthzedAction + + client.get( + "/monitoring/metrics/features", + params={"project": "test_project", "feature_view_name": "driver_stats"}, + ) + + mock_perms.assert_called() + call_args = mock_perms.call_args + assert AuthzedAction.DESCRIBE in call_args.kwargs.get( + "actions", call_args[1].get("actions", []) + ) + + +# ------------------------------------------------------------------ # +# Test: SQL push-down dispatch +# ------------------------------------------------------------------ # + + +class TestComputeEngineDispatch: + """Verify that MonitoringService prefers SQL push-down and falls back + to Python-based computation when the offline store doesn't support it.""" + + def _make_store_with_pushdown(self, pushdown_result): + """Create a mock store where the offline store supports push-down.""" + fv = _make_feature_view( + "driver_stats", + [ + _make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64), + _make_feature_field("city", PrimitiveFeastType.STRING), + ], + ) + store = _make_mock_store([fv]) + provider = store._get_provider.return_value + provider.offline_store.compute_monitoring_metrics.side_effect = None + provider.offline_store.compute_monitoring_metrics.return_value = pushdown_result + provider.offline_store.get_monitoring_max_timestamp.side_effect = None + provider.offline_store.get_monitoring_max_timestamp.return_value = datetime( + 2025, 3, 27, tzinfo=timezone.utc + ) + return store, fv + + def test_uses_sql_pushdown_when_available(self): + """When the offline store supports compute_monitoring_metrics, + pull_all_from_table_or_query should NOT be called.""" + sql_result = [ + { + "feature_name": "conv_rate", + "feature_type": "numeric", + "row_count": 100, + "null_count": 2, + "null_rate": 0.02, + "mean": 0.5, + "stddev": 0.2, + "min_val": 0.0, + "max_val": 1.0, + "p50": 0.5, + "p75": 0.75, + "p90": 0.9, + "p95": 0.95, + "p99": 0.99, + "histogram": { + "bins": [0.0, 0.5, 1.0], + "counts": [50, 50], + "bin_width": 0.5, + }, + }, + ] + store, _ = self._make_store_with_pushdown(sql_result) + svc = MonitoringService(store) + + result = svc.compute_transient( + project="test_project", + feature_view_name="driver_stats", + feature_names=["conv_rate"], + start_date=date(2025, 1, 1), + end_date=date(2025, 1, 15), + ) + + assert result["status"] == "completed" + assert len(result["metrics"]) == 1 + assert result["metrics"][0]["mean"] == 0.5 + + provider = store._get_provider.return_value + provider.offline_store.compute_monitoring_metrics.assert_called_once() + provider.offline_store.pull_all_from_table_or_query.assert_not_called() + + def test_falls_back_to_python_when_not_supported(self): + """When compute_monitoring_metrics raises NotImplementedError, + the service falls back to pulling data + Python compute.""" + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + store = _make_mock_store([fv]) + + svc = MonitoringService(store) + result = svc.compute_transient( + project="test_project", + feature_view_name="driver_stats", + start_date=date(2025, 1, 1), + end_date=date(2025, 1, 15), + ) + + assert result["status"] == "completed" + assert len(result["metrics"]) == 1 + assert result["metrics"][0]["feature_name"] == "conv_rate" + + provider = store._get_provider.return_value + provider.offline_store.pull_all_from_table_or_query.assert_called() + + def test_auto_compute_uses_pushdown_for_max_timestamp(self): + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + store = _make_mock_store([fv]) + provider = store._get_provider.return_value + + provider.offline_store.get_monitoring_max_timestamp.side_effect = None + provider.offline_store.get_monitoring_max_timestamp.return_value = datetime( + 2025, 3, 27, tzinfo=timezone.utc + ) + provider.offline_store.compute_monitoring_metrics.side_effect = None + provider.offline_store.compute_monitoring_metrics.return_value = [ + { + "feature_name": "conv_rate", + "feature_type": "numeric", + "row_count": 5, + "null_count": 0, + "null_rate": 0.0, + "mean": 0.5, + "stddev": 0.2, + "min_val": 0.1, + "max_val": 0.9, + "p50": 0.5, + "p75": 0.7, + "p90": 0.9, + "p95": 0.9, + "p99": 0.9, + "histogram": None, + }, + ] + + svc = MonitoringService(store) + result = svc.auto_compute(project="test_project") + + assert result["status"] == "completed" + provider.offline_store.get_monitoring_max_timestamp.assert_called() + provider.offline_store.compute_monitoring_metrics.assert_called() + provider.offline_store.pull_all_from_table_or_query.assert_not_called() + + +# ------------------------------------------------------------------ # +# Test: Native storage dispatch +# ------------------------------------------------------------------ # + + +class TestNativeStorageDispatch: + """Verify that MonitoringService uses OfflineStore for all storage + operations (save, query, clear_baseline, ensure_tables).""" + + def test_save_goes_through_offline_store(self): + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + store = _make_mock_store([fv]) + svc = MonitoringService(store) + + svc.compute_metrics( + project="test_project", + granularity="daily", + ) + + provider = store._get_provider.return_value + provider.offline_store.ensure_monitoring_tables.assert_called() + provider.offline_store.save_monitoring_metrics.assert_called() + + save_calls = provider.offline_store.save_monitoring_metrics.call_args_list + metric_types_saved = {c[0][1] for c in save_calls} + assert "feature" in metric_types_saved + assert "feature_view" in metric_types_saved + + def test_query_goes_through_offline_store(self): + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + store = _make_mock_store([fv]) + svc = MonitoringService(store) + + svc.get_feature_metrics(project="test_project", granularity="daily") + + provider = store._get_provider.return_value + provider.offline_store.query_monitoring_metrics.assert_called() + call_args = provider.offline_store.query_monitoring_metrics.call_args + assert call_args[1]["metric_type"] == "feature" + + def test_baseline_clear_goes_through_offline_store(self): + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + store = _make_mock_store([fv]) + svc = MonitoringService(store) + + svc.compute_baseline(project="test_project") + + provider = store._get_provider.return_value + provider.offline_store.clear_monitoring_baseline.assert_called() + + def test_transient_does_not_save(self): + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + store = _make_mock_store([fv]) + svc = MonitoringService(store) + + svc.compute_transient( + project="test_project", + feature_view_name="driver_stats", + ) + + provider = store._get_provider.return_value + provider.offline_store.save_monitoring_metrics.assert_not_called() + + +# ------------------------------------------------------------------ # +# Test: Log source monitoring +# ------------------------------------------------------------------ # + + +class TestLogSourceMonitoring: + """Verify that monitoring can compute metrics from feature serving logs.""" + + # Realistic log column names follow the {view}__{feature} convention + # produced by FeatureServiceLoggingSource.get_schema(). + _LOG_SCHEMA = pa.schema( + [ + ("driver_id", pa.int64()), + ("driver_stats__conv_rate", pa.float64()), + ("driver_stats__conv_rate__timestamp", pa.timestamp("us", tz="UTC")), + ("driver_stats__conv_rate__status", pa.int32()), + ("driver_stats__city", pa.utf8()), + ("driver_stats__city__timestamp", pa.timestamp("us", tz="UTC")), + ("driver_stats__city__status", pa.int32()), + ("__log_timestamp", pa.timestamp("us", tz="UTC")), + ("__log_date", pa.date32()), + ("__request_id", pa.utf8()), + ] + ) + + def _make_log_store(self): + """Create a mock store with a feature service that has logging configured.""" + fv = _make_feature_view( + "driver_stats", + [ + _make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64), + _make_feature_field("city", PrimitiveFeastType.STRING), + ], + ) + + logging_config, log_data_source = _make_logging_config_with_source( + self._LOG_SCHEMA + ) + + fs = _make_feature_service( + "driver_service", + ["driver_stats"], + logging_config=logging_config, + feature_map={"driver_stats": ["conv_rate", "city"]}, + ) + store = _make_mock_store([fv], feature_services=[fs]) + + log_arrow_table = pa.table( + { + "driver_stats__conv_rate": [0.1, 0.5, 0.9, 0.3, 0.7], + "driver_stats__city": ["NYC", "LA", "NYC", "SF", "LA"], + "__log_timestamp": [ + datetime(2025, 3, 25, tzinfo=timezone.utc), + datetime(2025, 3, 26, tzinfo=timezone.utc), + datetime(2025, 3, 26, tzinfo=timezone.utc), + datetime(2025, 3, 27, tzinfo=timezone.utc), + datetime(2025, 3, 27, tzinfo=timezone.utc), + ], + } + ) + + mock_log_retrieval = MagicMock() + mock_log_retrieval.to_arrow.return_value = log_arrow_table + + provider = store._get_provider.return_value + provider.offline_store.pull_all_from_table_or_query.return_value = ( + mock_log_retrieval + ) + + entity_col = MagicMock() + entity_col.name = "driver_id" + fv.entity_columns = [entity_col] + + return store, fs + + def test_compute_log_metrics(self): + store, fs = self._make_log_store() + svc = MonitoringService(store) + + with patch( + "feast.monitoring.monitoring_service.FeatureServiceLoggingSource" + ) as mock_cls: + mock_instance = MagicMock() + mock_instance.get_schema.return_value = self._LOG_SCHEMA + mock_cls.return_value = mock_instance + + result = svc.compute_log_metrics( + project="test_project", + feature_service_name="driver_service", + start_date=date(2025, 3, 25), + end_date=date(2025, 3, 27), + granularity="daily", + ) + + assert result["status"] == "completed" + assert result["data_source_type"] == "log" + assert result["computed_features"] == 2 + + provider = store._get_provider.return_value + provider.offline_store.save_monitoring_metrics.assert_called() + + save_calls = provider.offline_store.save_monitoring_metrics.call_args_list + feature_calls = [c for c in save_calls if c[0][1] == "feature"] + assert len(feature_calls) >= 1 + saved_metrics = feature_calls[0][0][2] + assert all(m["data_source_type"] == "log" for m in saved_metrics) + # Feature names normalized: driver_stats__conv_rate -> conv_rate + saved_names = {m["feature_name"] for m in saved_metrics} + assert saved_names == {"conv_rate", "city"} + # Feature view name is the actual view, not the service + assert all(m["feature_view_name"] == "driver_stats" for m in saved_metrics) + + # Feature service aggregate saved to the service table + svc_calls = [c for c in save_calls if c[0][1] == "feature_service"] + assert len(svc_calls) >= 1 + svc_metric = svc_calls[0][0][2][0] + assert svc_metric["feature_service_name"] == "driver_service" + assert svc_metric["data_source_type"] == "log" + assert svc_metric["total_features"] == 2 + + def test_compute_log_metrics_no_logging_config(self): + fv = _make_feature_view( + "driver_stats", + [_make_feature_field("conv_rate", PrimitiveFeastType.FLOAT64)], + ) + fs = _make_feature_service("no_log_service", ["driver_stats"]) + fs.logging_config = None + store = _make_mock_store([fv], feature_services=[fs]) + svc = MonitoringService(store) + + result = svc.compute_log_metrics( + project="test_project", + feature_service_name="no_log_service", + ) + + assert result["status"] == "skipped" + assert "no logging configured" in result["reason"] + + def test_auto_compute_log_metrics(self): + store, fs = self._make_log_store() + svc = MonitoringService(store) + + with patch( + "feast.monitoring.monitoring_service.FeatureServiceLoggingSource" + ) as mock_cls: + mock_instance = MagicMock() + mock_instance.get_schema.return_value = self._LOG_SCHEMA + mock_cls.return_value = mock_instance + + result = svc.auto_compute_log_metrics(project="test_project") + + assert result["status"] == "completed" + assert result["data_source_type"] == "log" + assert result["computed_feature_services"] == 1 + assert len(result["granularities"]) == len(VALID_GRANULARITIES) + + def test_log_metrics_tagged_differently_from_batch(self): + """Log metrics should have data_source_type='log', batch should have 'batch'.""" + store, fs = self._make_log_store() + svc = MonitoringService(store) + + with patch( + "feast.monitoring.monitoring_service.FeatureServiceLoggingSource" + ) as mock_cls: + mock_instance = MagicMock() + mock_instance.get_schema.return_value = self._LOG_SCHEMA + mock_cls.return_value = mock_instance + + svc.compute_log_metrics( + project="test_project", + feature_service_name="driver_service", + granularity="daily", + ) + + provider = store._get_provider.return_value + save_calls = provider.offline_store.save_monitoring_metrics.call_args_list + feature_calls = [c for c in save_calls if c[0][1] == "feature"] + for call in feature_calls: + for m in call[0][2]: + assert m["data_source_type"] == "log" + assert m["feature_view_name"] == "driver_stats" + assert m["feature_name"] in ("conv_rate", "city") diff --git a/sdk/python/tests/integration/offline_server/__init__.py b/sdk/python/tests/integration/offline_server/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/integration/offline_server/test_offline_server.py b/sdk/python/tests/integration/offline_server/test_offline_server.py new file mode 100644 index 00000000000..8069326bb43 --- /dev/null +++ b/sdk/python/tests/integration/offline_server/test_offline_server.py @@ -0,0 +1,375 @@ +import os +import tempfile +from datetime import datetime, timedelta +from unittest.mock import patch + +import assertpy +import pandas as pd +import pyarrow as pa +import pyarrow.flight as flight +import pytest + +from feast import FeatureStore, FeatureView, FileSource +from feast.errors import FeatureViewNotFoundException +from feast.feature_logging import FeatureServiceLoggingSource +from feast.infra.offline_stores.remote import ( + RemoteOfflineStore, + RemoteOfflineStoreConfig, +) +from feast.offline_server import OfflineServer, _init_auth_manager +from feast.repo_config import RepoConfig +from feast.torch_wrapper import get_torch +from tests.utils.cli_repo_creator import CliRunner + +pytestmark = pytest.mark.integration + +PROJECT_NAME = "test_remote_offline" + + +@pytest.fixture +def empty_offline_server(environment): + store = environment.feature_store + + location = "grpc+tcp://localhost:0" + _init_auth_manager(store=store) + return OfflineServer(store=store, location=location) + + +@pytest.fixture +def arrow_client(empty_offline_server): + return flight.FlightClient(f"grpc://localhost:{empty_offline_server.port}") + + +def test_offline_server_is_alive(environment, empty_offline_server, arrow_client): + server = empty_offline_server + client = arrow_client + + assertpy.assert_that(server).is_not_none() + assertpy.assert_that(server.port).is_not_equal_to(0) + + actions = list(client.list_actions()) + flights = list(client.list_flights()) + + assertpy.assert_that(actions).is_equal_to( + [ + ( + "offline_write_batch", + "Writes the specified arrow table to the data source underlying the specified feature view.", + ), + ( + "write_logged_features", + "Writes logged features to a specified destination in the offline store.", + ), + ( + "persist", + "Synchronously executes the underlying query and persists the result in the same offline store at the " + "specified destination.", + ), + ] + ) + assertpy.assert_that(flights).is_empty() + + +def default_store(temp_dir): + runner = CliRunner() + result = runner.run(["init", PROJECT_NAME], cwd=temp_dir) + repo_path = os.path.join(temp_dir, PROJECT_NAME, "feature_repo") + assert result.returncode == 0 + + result = runner.run(["--chdir", repo_path, "apply"], cwd=temp_dir) + assert result.returncode == 0 + + fs = FeatureStore(repo_path=repo_path) + return fs + + +def remote_feature_store(offline_server): + offline_config = RemoteOfflineStoreConfig( + type="remote", host="0.0.0.0", port=offline_server.port + ) + + registry_path = os.path.join( + str(offline_server.store.repo_path), + offline_server.store.config.registry.path, + ) + store = FeatureStore( + config=RepoConfig( + project=PROJECT_NAME, + registry=registry_path, + provider="local", + offline_store=offline_config, + entity_key_serialization_version=3, + # repo_config = + ) + ) + return store + + +def test_remote_offline_store_apis(): + with tempfile.TemporaryDirectory() as temp_dir: + store = default_store(str(temp_dir)) + location = "grpc+tcp://localhost:0" + + _init_auth_manager(store=store) + server = OfflineServer(store=store, location=location) + + assertpy.assert_that(server).is_not_none() + assertpy.assert_that(server.port).is_not_equal_to(0) + + fs = remote_feature_store(server) + + _test_get_historical_features_returns_data(fs) + _test_get_historical_features_to_tensor(fs) + _test_get_historical_features_returns_nan(fs) + _test_get_historical_features_to_tensor_with_nan(fs) + _test_offline_write_batch(str(temp_dir), fs) + _test_write_logged_features(str(temp_dir), fs) + _test_pull_latest_from_table_or_query(str(temp_dir), fs) + _test_pull_all_from_table_or_query(str(temp_dir), fs) + + +def test_remote_offline_store_exception_handling(): + with tempfile.TemporaryDirectory() as temp_dir: + store = default_store(str(temp_dir)) + location = "grpc+tcp://localhost:0" + + _init_auth_manager(store=store) + server = OfflineServer(store=store, location=location) + + assertpy.assert_that(server).is_not_none() + assertpy.assert_that(server.port).is_not_equal_to(0) + + fs = remote_feature_store(server) + data_file = os.path.join( + temp_dir, fs.project, "feature_repo/data/driver_stats.parquet" + ) + data_df = pd.read_parquet(data_file) + + with pytest.raises( + FeatureViewNotFoundException, + match="Feature view test does not exist in project test_remote_offline", + ): + RemoteOfflineStore.offline_write_batch( + fs.config, + FeatureView(name="test", source=FileSource(path="test")), + pa.Table.from_pandas(data_df), + progress=None, + ) + + +def _test_get_historical_features_returns_data(fs: FeatureStore): + entity_df = pd.DataFrame.from_dict( + { + "driver_id": [1001, 1002, 1003], + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + ], + "label_driver_reported_satisfaction": [1, 5, 3], + "val_to_add": [1, 2, 3], + "val_to_add_2": [10, 20, 30], + } + ) + + features = [ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ] + + training_df = fs.get_historical_features(entity_df, features).to_df() + + assertpy.assert_that(training_df).is_not_none() + assertpy.assert_that(len(training_df)).is_equal_to(3) + + for index, driver_id in enumerate(entity_df["driver_id"]): + assertpy.assert_that(training_df["driver_id"][index]).is_equal_to(driver_id) + for feature in features: + column_id = feature.split(":")[1] + value = training_df[column_id][index] + assertpy.assert_that(value).is_not_nan() + + +def _test_get_historical_features_to_tensor(fs: FeatureStore): + entity_df = pd.DataFrame.from_dict( + { + "driver_id": [1001, 1002, 1003], + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + ], + "label_driver_reported_satisfaction": [1, 5, 3], + "val_to_add": [1, 2, 3], + "val_to_add_2": [10, 20, 30], + } + ) + + features = [ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ] + + job = fs.get_historical_features(entity_df, features) + tensor_data = job.to_tensor() + + assertpy.assert_that(tensor_data).is_not_none() + assertpy.assert_that(tensor_data["driver_id"].shape[0]).is_equal_to(3) + torch = get_torch() + for key, values in tensor_data.items(): + if isinstance(values, torch.Tensor): + assertpy.assert_that(values.shape[0]).is_equal_to(3) + for val in values: + val_float = val.item() + assertpy.assert_that(val_float).is_instance_of((float, int)) + assertpy.assert_that(val_float).is_not_nan() + + +def _test_get_historical_features_returns_nan(fs: FeatureStore): + entity_df = pd.DataFrame.from_dict( + { + "driver_id": [1, 2, 3], + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + ], + "label_driver_reported_satisfaction": [1, 5, 3], + "val_to_add": [1, 2, 3], + "val_to_add_2": [10, 20, 30], + } + ) + + features = [ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ] + + training_df = fs.get_historical_features(entity_df, features).to_df() + + assertpy.assert_that(training_df).is_not_none() + assertpy.assert_that(len(training_df)).is_equal_to(3) + + for index, driver_id in enumerate(entity_df["driver_id"]): + assertpy.assert_that(training_df["driver_id"][index]).is_equal_to(driver_id) + for feature in features: + column_id = feature.split(":")[1] + value = training_df[column_id][index] + assertpy.assert_that(value).is_nan() + + +def _test_get_historical_features_to_tensor_with_nan(fs: FeatureStore): + entity_df = pd.DataFrame.from_dict( + { + "driver_id": [9991, 9992], # IDs with no matching features + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 10, 59, 42), + ], + } + ) + features = ["driver_hourly_stats:conv_rate"] + job = fs.get_historical_features(entity_df, features) + tensor_data = job.to_tensor() + assert "conv_rate" in tensor_data + values = tensor_data["conv_rate"] + # conv_rate is a float feature, missing values should be NaN + torch = get_torch() + for val in values: + assert isinstance(val, torch.Tensor) or torch.is_tensor(val) + assertpy.assert_that(torch.isnan(val).item()).is_true() + + +def _test_offline_write_batch(temp_dir, fs: FeatureStore): + data_file = os.path.join( + temp_dir, fs.project, "feature_repo/data/driver_stats.parquet" + ) + data_df = pd.read_parquet(data_file) + feature_view = fs.get_feature_view("driver_hourly_stats") + + RemoteOfflineStore.offline_write_batch( + fs.config, feature_view, pa.Table.from_pandas(data_df), progress=None + ) + + +def _test_write_logged_features(temp_dir, fs: FeatureStore): + data_file = os.path.join( + temp_dir, fs.project, "feature_repo/data/driver_stats.parquet" + ) + data_df = pd.read_parquet(data_file) + feature_service = fs.get_feature_service("driver_activity_v1") + + RemoteOfflineStore.write_logged_features( + config=fs.config, + data=pa.Table.from_pandas(data_df), + source=FeatureServiceLoggingSource(feature_service, fs.config.project), + logging_config=feature_service.logging_config, + registry=fs.registry, + ) + + +def _test_pull_latest_from_table_or_query(temp_dir, fs: FeatureStore): + data_source = fs.get_data_source("driver_hourly_stats_source") + + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + start_date = end_date - timedelta(days=15) + RemoteOfflineStore.pull_latest_from_table_or_query( + config=fs.config, + data_source=data_source, + join_key_columns=[], + feature_name_columns=[], + timestamp_field="event_timestamp", + created_timestamp_column="created", + start_date=start_date, + end_date=end_date, + ).to_df() + + +def _test_pull_all_from_table_or_query(temp_dir, fs: FeatureStore): + data_source = fs.get_data_source("driver_hourly_stats_source") + + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + start_date = end_date - timedelta(days=15) + RemoteOfflineStore.pull_all_from_table_or_query( + config=fs.config, + data_source=data_source, + join_key_columns=[], + feature_name_columns=[], + timestamp_field="event_timestamp", + start_date=start_date, + end_date=end_date, + ).to_df() + + +def test_get_feature_view_by_name_propagates_transient_errors(): + """Transient registry errors must not be swallowed and misreported as + FeatureViewNotFoundException.""" + with tempfile.TemporaryDirectory() as temp_dir: + store = default_store(str(temp_dir)) + location = "grpc+tcp://localhost:0" + + _init_auth_manager(store=store) + server = OfflineServer(store=store, location=location) + + transient_error = ConnectionError("registry temporarily unavailable") + + with patch.object( + server.store.registry, + "get_feature_view", + side_effect=transient_error, + ): + with pytest.raises(ConnectionError, match="registry temporarily"): + server.get_feature_view_by_name( + fv_name="driver_hourly_stats", + name_alias=None, + project=PROJECT_NAME, + ) diff --git a/sdk/python/tests/integration/offline_store/test_dqm_validation.py b/sdk/python/tests/integration/offline_store/test_dqm_validation.py index 52d83ab8d8f..3fce9f07597 100644 --- a/sdk/python/tests/integration/offline_store/test_dqm_validation.py +++ b/sdk/python/tests/integration/offline_store/test_dqm_validation.py @@ -1,419 +1,419 @@ -import datetime -import shutil - -import pandas as pd -import pyarrow as pa -import pytest -from great_expectations.core import ExpectationSuite -from great_expectations.dataset import PandasDataset - -from feast import FeatureService -from feast.dqm.errors import ValidationFailed -from feast.dqm.profilers.ge_profiler import ge_profiler -from feast.feature_logging import ( - LOG_TIMESTAMP_FIELD, - FeatureServiceLoggingSource, - LoggingConfig, -) -from feast.protos.feast.serving.ServingService_pb2 import FieldStatus -from feast.utils import _utc_now, make_tzaware -from feast.wait import wait_retry_backoff -from tests.integration.feature_repos.repo_configuration import ( - construct_universal_feature_views, -) -from tests.integration.feature_repos.universal.entities import ( - customer, - driver, - location, -) -from tests.utils.cli_repo_creator import CliRunner -from tests.utils.test_log_creator import prepare_logs - -_features = [ - "customer_profile:current_balance", - "customer_profile:avg_passenger_count", - "customer_profile:lifetime_trip_count", - "order:order_is_success", - "global_stats:num_rides", - "global_stats:avg_ride_length", -] - - -@pytest.mark.integration -@pytest.mark.universal_offline_stores -def test_historical_retrieval_with_validation(environment, universal_data_sources): - store = environment.feature_store - (entities, datasets, data_sources) = universal_data_sources - feature_views = construct_universal_feature_views(data_sources) - storage = environment.data_source_creator.create_saved_dataset_destination() - - store.apply([driver(), customer(), location(), *feature_views.values()]) - - # Added to handle the case that the offline store is remote - store.registry.apply_data_source(storage.to_data_source(), store.config.project) - - # Create two identical retrieval jobs - entity_df = datasets.entity_df.drop( - columns=["order_id", "origin_id", "destination_id"] - ) - reference_job = store.get_historical_features( - entity_df=entity_df, - features=_features, - ) - job = store.get_historical_features( - entity_df=entity_df, - features=_features, - ) - - # Save dataset using reference job and retrieve it - store.create_saved_dataset( - from_=reference_job, - name="my_training_dataset", - storage=storage, - allow_overwrite=True, - ) - saved_dataset = store.get_saved_dataset("my_training_dataset") - - # If validation pass there will be no exceptions on this point - reference = saved_dataset.as_reference(name="ref", profiler=configurable_profiler) - job.to_df(validation_reference=reference) - - -@pytest.mark.integration -def test_historical_retrieval_fails_on_validation(environment, universal_data_sources): - store = environment.feature_store - - (entities, datasets, data_sources) = universal_data_sources - feature_views = construct_universal_feature_views(data_sources) - storage = environment.data_source_creator.create_saved_dataset_destination() - - store.apply([driver(), customer(), location(), *feature_views.values()]) - - # Added to handle the case that the offline store is remote - store.registry.apply_data_source(storage.to_data_source(), store.config.project) - - entity_df = datasets.entity_df.drop( - columns=["order_id", "origin_id", "destination_id"] - ) - - reference_job = store.get_historical_features( - entity_df=entity_df, - features=_features, - ) - - store.create_saved_dataset( - from_=reference_job, - name="my_other_dataset", - storage=storage, - allow_overwrite=True, - ) - - job = store.get_historical_features( - entity_df=entity_df, - features=_features, - ) - - ds = store.get_saved_dataset("my_other_dataset") - profiler_expectation_suite = ds.get_profile( - profiler=profiler_with_unrealistic_expectations - ) - - assert len(profiler_expectation_suite.expectation_suite["expectations"]) == 3 - - with pytest.raises(ValidationFailed) as exc_info: - job.to_df( - validation_reference=store.get_saved_dataset( - "my_other_dataset" - ).as_reference(name="ref", profiler=profiler_with_unrealistic_expectations) - ) - - failed_expectations = exc_info.value.report.errors - assert len(failed_expectations) == 2 - - assert failed_expectations[0].check_name == "expect_column_max_to_be_between" - assert failed_expectations[0].column_name == "current_balance" - - assert failed_expectations[1].check_name == "expect_column_values_to_be_in_set" - assert failed_expectations[1].column_name == "avg_passenger_count" - - -@pytest.mark.integration -@pytest.mark.universal_offline_stores -def test_logged_features_validation(environment, universal_data_sources): - store = environment.feature_store - - (_, datasets, data_sources) = universal_data_sources - feature_views = construct_universal_feature_views(data_sources) - feature_service = FeatureService( - name="test_service", - features=[ - feature_views.customer[ - ["current_balance", "avg_passenger_count", "lifetime_trip_count"] - ], - feature_views.order[["order_is_success"]], - feature_views.global_fv[["num_rides", "avg_ride_length"]], - ], - logging_config=LoggingConfig( - destination=environment.data_source_creator.create_logged_features_destination() - ), - ) - - storage = environment.data_source_creator.create_saved_dataset_destination() - - store.apply( - [driver(), customer(), location(), feature_service, *feature_views.values()] - ) - - # Added to handle the case that the offline store is remote - store.registry.apply_data_source( - feature_service.logging_config.destination.to_data_source(), - store.config.project, - ) - store.registry.apply_data_source(storage.to_data_source(), store.config.project) - - entity_df = datasets.entity_df.drop( - columns=["order_id", "origin_id", "destination_id"] - ) - - # add some non-existing entities to check NotFound feature handling - for i in range(5): - entity_df = pd.concat( - [ - entity_df, - pd.DataFrame.from_records( - [ - { - "customer_id": 2000 + i, - "driver_id": 6000 + i, - "event_timestamp": make_tzaware(datetime.datetime.now()), - } - ] - ), - ] - ) - - store_fs = store.get_feature_service(feature_service.name) - reference_dataset = store.create_saved_dataset( - from_=store.get_historical_features( - entity_df=entity_df, features=store_fs, full_feature_names=True - ), - name="reference_for_validating_logged_features", - storage=storage, - allow_overwrite=True, - ) - - log_source_df = store.get_historical_features( - entity_df=entity_df, features=store_fs, full_feature_names=False - ).to_df() - logs_df = prepare_logs(log_source_df, feature_service, store) - - schema = FeatureServiceLoggingSource( - feature_service=feature_service, project=store.project - ).get_schema(store._registry) - store.write_logged_features( - pa.Table.from_pandas(logs_df, schema=schema), source=feature_service - ) - - def validate(): - """ - Return Tuple[succeed, completed] - Succeed will be True if no ValidateFailed exception was raised - """ - try: - store.validate_logged_features( - feature_service, - start=logs_df[LOG_TIMESTAMP_FIELD].min(), - end=logs_df[LOG_TIMESTAMP_FIELD].max() + datetime.timedelta(seconds=1), - reference=reference_dataset.as_reference( - name="ref", profiler=profiler_with_feature_metadata - ), - ) - except ValidationFailed: - return False, True - except Exception: - # log table is still being created - return False, False - - return True, True - - success = wait_retry_backoff(validate, timeout_secs=30) - assert success, "Validation failed (unexpectedly)" - - -@pytest.mark.integration -def test_e2e_validation_via_cli(environment, universal_data_sources): - runner = CliRunner() - store = environment.feature_store - - (_, datasets, data_sources) = universal_data_sources - feature_views = construct_universal_feature_views(data_sources) - feature_service = FeatureService( - name="test_service", - features=[ - feature_views.customer[ - ["current_balance", "avg_passenger_count", "lifetime_trip_count"] - ], - ], - logging_config=LoggingConfig( - destination=environment.data_source_creator.create_logged_features_destination() - ), - ) - store.apply([customer(), feature_service, feature_views.customer]) - - entity_df = datasets.entity_df.drop( - columns=["order_id", "origin_id", "destination_id", "driver_id"] - ) - retrieval_job = store.get_historical_features( - entity_df=entity_df, - features=store.get_feature_service(feature_service.name), - full_feature_names=True, - ) - logs_df = prepare_logs(retrieval_job.to_df(), feature_service, store) - saved_dataset = store.create_saved_dataset( - from_=retrieval_job, - name="reference_for_validating_logged_features", - storage=environment.data_source_creator.create_saved_dataset_destination(), - allow_overwrite=True, - ) - reference = saved_dataset.as_reference( - name="test_reference", profiler=configurable_profiler - ) - - schema = FeatureServiceLoggingSource( - feature_service=feature_service, project=store.project - ).get_schema(store._registry) - store.write_logged_features( - pa.Table.from_pandas(logs_df, schema=schema), source=feature_service - ) - - with runner.local_repo(example_repo_py="", offline_store="file") as local_repo: - local_repo.apply( - [customer(), feature_views.customer, feature_service, reference] - ) - local_repo._registry.apply_saved_dataset(saved_dataset, local_repo.project) - validate_args = [ - "validate", - "--feature-service", - feature_service.name, - "--reference", - reference.name, - (datetime.datetime.now() - datetime.timedelta(days=7)).isoformat(), - datetime.datetime.now().isoformat(), - ] - p = runner.run(validate_args, cwd=local_repo.repo_path) - - assert p.returncode == 0, p.stderr.decode() - assert "Validation successful" in p.stdout.decode(), p.stderr.decode() - - p = runner.run( - ["saved-datasets", "describe", saved_dataset.name], cwd=local_repo.repo_path - ) - assert p.returncode == 0, p.stderr.decode() - - p = runner.run( - ["validation-references", "describe", reference.name], - cwd=local_repo.repo_path, - ) - assert p.returncode == 0, p.stderr.decode() - - p = runner.run( - ["feature-services", "describe", feature_service.name], - cwd=local_repo.repo_path, - ) - assert p.returncode == 0, p.stderr.decode() - - # make sure second validation will use cached profile - shutil.rmtree(saved_dataset.storage.file_options.uri) - - # Add some invalid data that would lead to failed validation - invalid_data = pd.DataFrame( - data={ - "customer_id": [0], - "current_balance": [0], - "avg_passenger_count": [0], - "lifetime_trip_count": [0], - "event_timestamp": [ - make_tzaware(_utc_now()) - datetime.timedelta(hours=1) - ], - } - ) - invalid_logs = prepare_logs(invalid_data, feature_service, store) - store.write_logged_features( - pa.Table.from_pandas(invalid_logs, schema=schema), source=feature_service - ) - - p = runner.run(validate_args, cwd=local_repo.repo_path) - assert p.returncode == 1, p.stdout.decode() - assert "Validation failed" in p.stdout.decode(), p.stderr.decode() - - -# Great expectations profilers created for testing - - -@ge_profiler -def configurable_profiler(dataset: PandasDataset) -> ExpectationSuite: - from great_expectations.profile.user_configurable_profiler import ( - UserConfigurableProfiler, - ) - - return UserConfigurableProfiler( - profile_dataset=dataset, - ignored_columns=["event_timestamp"], - excluded_expectations=[ - "expect_table_columns_to_match_ordered_list", - "expect_table_row_count_to_be_between", - ], - value_set_threshold="few", - ).build_suite() - - -@ge_profiler(with_feature_metadata=True) -def profiler_with_feature_metadata(dataset: PandasDataset) -> ExpectationSuite: - from great_expectations.profile.user_configurable_profiler import ( - UserConfigurableProfiler, - ) - - # always present - dataset.expect_column_values_to_be_in_set( - "global_stats__avg_ride_length__status", {FieldStatus.PRESENT} - ) - - # present at least in 70% of rows - dataset.expect_column_values_to_be_in_set( - "customer_profile__current_balance__status", {FieldStatus.PRESENT}, mostly=0.7 - ) - - return UserConfigurableProfiler( - profile_dataset=dataset, - ignored_columns=["event_timestamp"] - + [ - c - for c in dataset.columns - if c.endswith("__timestamp") or c.endswith("__status") - ], - excluded_expectations=[ - "expect_table_columns_to_match_ordered_list", - "expect_table_row_count_to_be_between", - ], - value_set_threshold="few", - ).build_suite() - - -@ge_profiler -def profiler_with_unrealistic_expectations(dataset: PandasDataset) -> ExpectationSuite: - # note: there are 4 expectations here and only 3 are returned from the profiler - # need to create dataframe with corrupted data first - df = pd.DataFrame() - df["current_balance"] = [-100] - df["avg_passenger_count"] = [0] - - other_ds = PandasDataset(df) - other_ds.expect_column_max_to_be_between("current_balance", -1000, -100) - other_ds.expect_column_values_to_be_in_set("avg_passenger_count", value_set={0}) - - # this should pass - other_ds.expect_column_min_to_be_between("avg_passenger_count", 0, 1000) - # this should fail - other_ds.expect_column_to_exist("missing random column") - - return other_ds.get_expectation_suite() +import datetime +import shutil + +import pandas as pd +import pyarrow as pa +import pytest +from great_expectations.core import ExpectationSuite +from great_expectations.dataset import PandasDataset + +from feast import FeatureService +from feast.dqm.errors import ValidationFailed +from feast.dqm.profilers.ge_profiler import ge_profiler +from feast.feature_logging import ( + LOG_TIMESTAMP_FIELD, + FeatureServiceLoggingSource, + LoggingConfig, +) +from feast.protos.feast.serving.ServingService_pb2 import FieldStatus +from feast.utils import _utc_now, make_tzaware +from feast.wait import wait_retry_backoff +from tests.universal.feature_repos.repo_configuration import ( + construct_universal_feature_views, +) +from tests.universal.feature_repos.universal.entities import ( + customer, + driver, + location, +) +from tests.utils.cli_repo_creator import CliRunner +from tests.utils.test_log_creator import prepare_logs + +_features = [ + "customer_profile:current_balance", + "customer_profile:avg_passenger_count", + "customer_profile:lifetime_trip_count", + "order:order_is_success", + "global_stats:num_rides", + "global_stats:avg_ride_length", +] + + +@pytest.mark.integration +@pytest.mark.universal_offline_stores +def test_historical_retrieval_with_validation(environment, universal_data_sources): + store = environment.feature_store + (entities, datasets, data_sources) = universal_data_sources + feature_views = construct_universal_feature_views(data_sources) + storage = environment.data_source_creator.create_saved_dataset_destination() + + store.apply([driver(), customer(), location(), *feature_views.values()]) + + # Added to handle the case that the offline store is remote + store.registry.apply_data_source(storage.to_data_source(), store.config.project) + + # Create two identical retrieval jobs + entity_df = datasets.entity_df.drop( + columns=["order_id", "origin_id", "destination_id"] + ) + reference_job = store.get_historical_features( + entity_df=entity_df, + features=_features, + ) + job = store.get_historical_features( + entity_df=entity_df, + features=_features, + ) + + # Save dataset using reference job and retrieve it + store.create_saved_dataset( + from_=reference_job, + name="my_training_dataset", + storage=storage, + allow_overwrite=True, + ) + saved_dataset = store.get_saved_dataset("my_training_dataset") + + # If validation pass there will be no exceptions on this point + reference = saved_dataset.as_reference(name="ref", profiler=configurable_profiler) + job.to_df(validation_reference=reference) + + +@pytest.mark.integration +def test_historical_retrieval_fails_on_validation(environment, universal_data_sources): + store = environment.feature_store + + (entities, datasets, data_sources) = universal_data_sources + feature_views = construct_universal_feature_views(data_sources) + storage = environment.data_source_creator.create_saved_dataset_destination() + + store.apply([driver(), customer(), location(), *feature_views.values()]) + + # Added to handle the case that the offline store is remote + store.registry.apply_data_source(storage.to_data_source(), store.config.project) + + entity_df = datasets.entity_df.drop( + columns=["order_id", "origin_id", "destination_id"] + ) + + reference_job = store.get_historical_features( + entity_df=entity_df, + features=_features, + ) + + store.create_saved_dataset( + from_=reference_job, + name="my_other_dataset", + storage=storage, + allow_overwrite=True, + ) + + job = store.get_historical_features( + entity_df=entity_df, + features=_features, + ) + + ds = store.get_saved_dataset("my_other_dataset") + profiler_expectation_suite = ds.get_profile( + profiler=profiler_with_unrealistic_expectations + ) + + assert len(profiler_expectation_suite.expectation_suite["expectations"]) == 3 + + with pytest.raises(ValidationFailed) as exc_info: + job.to_df( + validation_reference=store.get_saved_dataset( + "my_other_dataset" + ).as_reference(name="ref", profiler=profiler_with_unrealistic_expectations) + ) + + failed_expectations = exc_info.value.report.errors + assert len(failed_expectations) == 2 + + assert failed_expectations[0].check_name == "expect_column_max_to_be_between" + assert failed_expectations[0].column_name == "current_balance" + + assert failed_expectations[1].check_name == "expect_column_values_to_be_in_set" + assert failed_expectations[1].column_name == "avg_passenger_count" + + +@pytest.mark.integration +@pytest.mark.universal_offline_stores +def test_logged_features_validation(environment, universal_data_sources): + store = environment.feature_store + + (_, datasets, data_sources) = universal_data_sources + feature_views = construct_universal_feature_views(data_sources) + feature_service = FeatureService( + name="test_service", + features=[ + feature_views.customer[ + ["current_balance", "avg_passenger_count", "lifetime_trip_count"] + ], + feature_views.order[["order_is_success"]], + feature_views.global_fv[["num_rides", "avg_ride_length"]], + ], + logging_config=LoggingConfig( + destination=environment.data_source_creator.create_logged_features_destination() + ), + ) + + storage = environment.data_source_creator.create_saved_dataset_destination() + + store.apply( + [driver(), customer(), location(), feature_service, *feature_views.values()] + ) + + # Added to handle the case that the offline store is remote + store.registry.apply_data_source( + feature_service.logging_config.destination.to_data_source(), + store.config.project, + ) + store.registry.apply_data_source(storage.to_data_source(), store.config.project) + + entity_df = datasets.entity_df.drop( + columns=["order_id", "origin_id", "destination_id"] + ) + + # add some non-existing entities to check NotFound feature handling + for i in range(5): + entity_df = pd.concat( + [ + entity_df, + pd.DataFrame.from_records( + [ + { + "customer_id": 2000 + i, + "driver_id": 6000 + i, + "event_timestamp": make_tzaware(datetime.datetime.now()), + } + ] + ), + ] + ) + + store_fs = store.get_feature_service(feature_service.name) + reference_dataset = store.create_saved_dataset( + from_=store.get_historical_features( + entity_df=entity_df, features=store_fs, full_feature_names=True + ), + name="reference_for_validating_logged_features", + storage=storage, + allow_overwrite=True, + ) + + log_source_df = store.get_historical_features( + entity_df=entity_df, features=store_fs, full_feature_names=False + ).to_df() + logs_df = prepare_logs(log_source_df, feature_service, store) + + schema = FeatureServiceLoggingSource( + feature_service=feature_service, project=store.project + ).get_schema(store._registry) + store.write_logged_features( + pa.Table.from_pandas(logs_df, schema=schema), source=feature_service + ) + + def validate(): + """ + Return Tuple[succeed, completed] + Succeed will be True if no ValidateFailed exception was raised + """ + try: + store.validate_logged_features( + feature_service, + start=logs_df[LOG_TIMESTAMP_FIELD].min(), + end=logs_df[LOG_TIMESTAMP_FIELD].max() + datetime.timedelta(seconds=1), + reference=reference_dataset.as_reference( + name="ref", profiler=profiler_with_feature_metadata + ), + ) + except ValidationFailed: + return False, True + except Exception: + # log table is still being created + return False, False + + return True, True + + success = wait_retry_backoff(validate, timeout_secs=30) + assert success, "Validation failed (unexpectedly)" + + +@pytest.mark.integration +def test_e2e_validation_via_cli(environment, universal_data_sources): + runner = CliRunner() + store = environment.feature_store + + (_, datasets, data_sources) = universal_data_sources + feature_views = construct_universal_feature_views(data_sources) + feature_service = FeatureService( + name="test_service", + features=[ + feature_views.customer[ + ["current_balance", "avg_passenger_count", "lifetime_trip_count"] + ], + ], + logging_config=LoggingConfig( + destination=environment.data_source_creator.create_logged_features_destination() + ), + ) + store.apply([customer(), feature_service, feature_views.customer]) + + entity_df = datasets.entity_df.drop( + columns=["order_id", "origin_id", "destination_id", "driver_id"] + ) + retrieval_job = store.get_historical_features( + entity_df=entity_df, + features=store.get_feature_service(feature_service.name), + full_feature_names=True, + ) + logs_df = prepare_logs(retrieval_job.to_df(), feature_service, store) + saved_dataset = store.create_saved_dataset( + from_=retrieval_job, + name="reference_for_validating_logged_features", + storage=environment.data_source_creator.create_saved_dataset_destination(), + allow_overwrite=True, + ) + reference = saved_dataset.as_reference( + name="test_reference", profiler=configurable_profiler + ) + + schema = FeatureServiceLoggingSource( + feature_service=feature_service, project=store.project + ).get_schema(store._registry) + store.write_logged_features( + pa.Table.from_pandas(logs_df, schema=schema), source=feature_service + ) + + with runner.local_repo(example_repo_py="", offline_store="file") as local_repo: + local_repo.apply( + [customer(), feature_views.customer, feature_service, reference] + ) + local_repo.registry.apply_saved_dataset(saved_dataset, local_repo.project) + validate_args = [ + "validate", + "--feature-service", + feature_service.name, + "--reference", + reference.name, + (datetime.datetime.now() - datetime.timedelta(days=7)).isoformat(), + datetime.datetime.now().isoformat(), + ] + p = runner.run(validate_args, cwd=local_repo.repo_path) + + assert p.returncode == 0, p.stderr.decode() + assert "Validation successful" in p.stdout.decode(), p.stderr.decode() + + p = runner.run( + ["saved-datasets", "describe", saved_dataset.name], cwd=local_repo.repo_path + ) + assert p.returncode == 0, p.stderr.decode() + + p = runner.run( + ["validation-references", "describe", reference.name], + cwd=local_repo.repo_path, + ) + assert p.returncode == 0, p.stderr.decode() + + p = runner.run( + ["feature-services", "describe", feature_service.name], + cwd=local_repo.repo_path, + ) + assert p.returncode == 0, p.stderr.decode() + + # make sure second validation will use cached profile + shutil.rmtree(saved_dataset.storage.file_options.uri) + + # Add some invalid data that would lead to failed validation + invalid_data = pd.DataFrame( + data={ + "customer_id": [0], + "current_balance": [0], + "avg_passenger_count": [0], + "lifetime_trip_count": [0], + "event_timestamp": [ + make_tzaware(_utc_now()) - datetime.timedelta(hours=1) + ], + } + ) + invalid_logs = prepare_logs(invalid_data, feature_service, store) + store.write_logged_features( + pa.Table.from_pandas(invalid_logs, schema=schema), source=feature_service + ) + + p = runner.run(validate_args, cwd=local_repo.repo_path) + assert p.returncode == 1, p.stdout.decode() + assert "Validation failed" in p.stdout.decode(), p.stderr.decode() + + +# Great expectations profilers created for testing + + +@ge_profiler +def configurable_profiler(dataset: PandasDataset) -> ExpectationSuite: + from great_expectations.profile.user_configurable_profiler import ( + UserConfigurableProfiler, + ) + + return UserConfigurableProfiler( + profile_dataset=dataset, + ignored_columns=["event_timestamp"], + excluded_expectations=[ + "expect_table_columns_to_match_ordered_list", + "expect_table_row_count_to_be_between", + ], + value_set_threshold="few", + ).build_suite() + + +@ge_profiler(with_feature_metadata=True) +def profiler_with_feature_metadata(dataset: PandasDataset) -> ExpectationSuite: + from great_expectations.profile.user_configurable_profiler import ( + UserConfigurableProfiler, + ) + + # always present + dataset.expect_column_values_to_be_in_set( + "global_stats__avg_ride_length__status", {FieldStatus.PRESENT} + ) + + # present at least in 70% of rows + dataset.expect_column_values_to_be_in_set( + "customer_profile__current_balance__status", {FieldStatus.PRESENT}, mostly=0.7 + ) + + return UserConfigurableProfiler( + profile_dataset=dataset, + ignored_columns=["event_timestamp"] + + [ + c + for c in dataset.columns + if c.endswith("__timestamp") or c.endswith("__status") + ], + excluded_expectations=[ + "expect_table_columns_to_match_ordered_list", + "expect_table_row_count_to_be_between", + ], + value_set_threshold="few", + ).build_suite() + + +@ge_profiler +def profiler_with_unrealistic_expectations(dataset: PandasDataset) -> ExpectationSuite: + # note: there are 4 expectations here and only 3 are returned from the profiler + # need to create dataframe with corrupted data first + df = pd.DataFrame() + df["current_balance"] = [-100] + df["avg_passenger_count"] = [0] + + other_ds = PandasDataset(df) + other_ds.expect_column_max_to_be_between("current_balance", -1000, -100) + other_ds.expect_column_values_to_be_in_set("avg_passenger_count", value_set={0}) + + # this should pass + other_ds.expect_column_min_to_be_between("avg_passenger_count", 0, 1000) + # this should fail + other_ds.expect_column_to_exist("missing random column") + + return other_ds.get_expectation_suite() diff --git a/sdk/python/tests/integration/offline_store/test_feature_logging.py b/sdk/python/tests/integration/offline_store/test_feature_logging.py index 53147d242ef..dc98ba69d2c 100644 --- a/sdk/python/tests/integration/offline_store/test_feature_logging.py +++ b/sdk/python/tests/integration/offline_store/test_feature_logging.py @@ -13,15 +13,15 @@ ) from feast.feature_service import FeatureService from feast.wait import wait_retry_backoff -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_universal_feature_views, ) -from tests.integration.feature_repos.universal.entities import ( +from tests.universal.feature_repos.universal.entities import ( customer, driver, location, ) -from tests.integration.feature_repos.universal.feature_views import conv_rate_plus_100 +from tests.universal.feature_repos.universal.feature_views import conv_rate_plus_100 from tests.utils.test_log_creator import prepare_logs, to_logs_dataset diff --git a/sdk/python/tests/integration/offline_store/test_non_entity_mode.py b/sdk/python/tests/integration/offline_store/test_non_entity_mode.py new file mode 100644 index 00000000000..f17352fb356 --- /dev/null +++ b/sdk/python/tests/integration/offline_store/test_non_entity_mode.py @@ -0,0 +1,138 @@ +from datetime import timedelta + +import pandas as pd +import pytest + +from feast.utils import _utc_now +from tests.universal.feature_repos.repo_configuration import ( + construct_universal_feature_views, +) +from tests.universal.feature_repos.universal.entities import driver + + +@pytest.mark.integration +@pytest.mark.universal_offline_stores +@pytest.mark.ray_offline_stores_only +def test_non_entity_mode_basic(environment, universal_data_sources): + """Test historical features retrieval without entity_df (non-entity mode). + + This tests the basic functionality where entity_df=None and start_date/end_date + are provided to retrieve all features within the time range. + """ + store = environment.feature_store + + (entities, datasets, data_sources) = universal_data_sources + feature_views = construct_universal_feature_views(data_sources) + + store.apply( + [ + driver(), + feature_views.driver, + ] + ) + + # Use the environment's start and end dates for the query + start_date = environment.start_date + end_date = environment.end_date + + # Non-entity mode: entity_df=None with start_date and end_date + result_df = store.get_historical_features( + entity_df=None, + features=[ + "driver_stats:conv_rate", + "driver_stats:acc_rate", + "driver_stats:avg_daily_trips", + ], + full_feature_names=False, + start_date=start_date, + end_date=end_date, + ).to_df() + + # Verify data was retrieved + assert len(result_df) > 0, "Non-entity mode should return data" + assert "conv_rate" in result_df.columns + assert "acc_rate" in result_df.columns + assert "avg_daily_trips" in result_df.columns + assert "event_timestamp" in result_df.columns + assert "driver_id" in result_df.columns + + # Verify timestamps are within the requested range + result_df["event_timestamp"] = pd.to_datetime( + result_df["event_timestamp"], utc=True + ) + assert (result_df["event_timestamp"] >= start_date).all() + assert (result_df["event_timestamp"] <= end_date).all() + + +@pytest.mark.integration +@pytest.mark.universal_offline_stores +@pytest.mark.ray_offline_stores_only +def test_non_entity_mode_preserves_multiple_timestamps( + environment, universal_data_sources +): + """Test that non-entity mode preserves multiple transactions per entity ID. + + This is a regression test for the fix that ensures distinct (entity_key, event_timestamp) + combinations are preserved, not just distinct entity keys. This is critical for + proper point-in-time joins when an entity has multiple transactions. + """ + store = environment.feature_store + + (entities, datasets, data_sources) = universal_data_sources + feature_views = construct_universal_feature_views(data_sources) + + store.apply( + [ + driver(), + feature_views.driver, + ] + ) + + now = _utc_now() + ts1 = pd.Timestamp(now - timedelta(hours=2)).round("ms") + ts2 = pd.Timestamp(now - timedelta(hours=1)).round("ms") + ts3 = pd.Timestamp(now).round("ms") + + # Write data with multiple timestamps for the same entity (driver_id=9001) + df_to_write = pd.DataFrame.from_dict( + { + "event_timestamp": [ts1, ts2, ts3], + "driver_id": [9001, 9001, 9001], # Same entity, different timestamps + "conv_rate": [0.1, 0.2, 0.3], + "acc_rate": [0.9, 0.8, 0.7], + "avg_daily_trips": [10, 20, 30], + "driver_metadata": [None, None, None], + "driver_config": [None, None, None], + "driver_profile": [None, None, None], + "created": [ts1, ts2, ts3], + }, + ) + + store.write_to_offline_store( + feature_views.driver.name, df_to_write, allow_registry_cache=False + ) + + # Query without entity_df - should get all 3 rows for driver_id=9001 + result_df = store.get_historical_features( + entity_df=None, + features=[ + "driver_stats:conv_rate", + "driver_stats:acc_rate", + ], + full_feature_names=False, + start_date=ts1 - timedelta(minutes=1), + end_date=ts3 + timedelta(minutes=1), + ).to_df() + + # Filter to just our test entity + result_df = result_df[result_df["driver_id"] == 9001] + + # Verify we got all 3 rows with different timestamps (not just 1 row) + assert len(result_df) == 3, ( + f"Expected 3 rows for driver_id=9001 (one per timestamp), got {len(result_df)}" + ) + + # Verify the feature values are correct for each timestamp + result_df = result_df.sort_values("event_timestamp").reset_index(drop=True) + assert list(result_df["conv_rate"]) == [0.1, 0.2, 0.3] + assert list(result_df["acc_rate"]) == [0.9, 0.8, 0.7] diff --git a/sdk/python/tests/integration/offline_store/test_offline_write.py b/sdk/python/tests/integration/offline_store/test_offline_write.py index 63bdc4755ac..6ae8b68147a 100644 --- a/sdk/python/tests/integration/offline_store/test_offline_write.py +++ b/sdk/python/tests/integration/offline_store/test_offline_write.py @@ -1,3 +1,4 @@ +import json import random from datetime import timedelta @@ -6,12 +7,12 @@ import pytest from feast import FeatureView, Field -from feast.types import Float32, Int32 +from feast.types import Float32, Int32, Json, Map, String, Struct from feast.utils import _utc_now -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_universal_feature_views, ) -from tests.integration.feature_repos.universal.entities import driver +from tests.universal.feature_repos.universal.entities import driver @pytest.mark.integration @@ -36,6 +37,18 @@ def test_reorder_columns(environment, universal_data_sources): "event_timestamp": [ts, ts], "acc_rate": [random.random(), random.random()], "driver_id": [1001, 1001], + "driver_metadata": [ + {"vehicle_type": "sedan", "rating": "4.5"}, + {"vehicle_type": "suv", "rating": "3.8"}, + ], + "driver_config": [ + json.dumps({"max_distance_km": 100, "preferred_zones": ["north"]}), + json.dumps({"max_distance_km": 50, "preferred_zones": ["south"]}), + ], + "driver_profile": [ + {"name": "driver_1001", "age": "30"}, + {"name": "driver_1001", "age": "30"}, + ], }, ) @@ -66,11 +79,24 @@ def test_writing_incorrect_schema_fails(environment, universal_data_sources): "created": [ts, ts], }, ) - with pytest.raises(ValueError): + expected_missing = [ + "acc_rate", + "avg_daily_trips", + "driver_config", + "driver_metadata", + "driver_profile", + ] + expected_extra = ["incorrect_schema"] + + with pytest.raises(ValueError, match="missing_expected_columns") as excinfo: store.write_to_offline_store( driver_fv.name, expected_df, allow_registry_cache=False ) + error_message = str(excinfo.value) + assert f"missing_expected_columns: {expected_missing}" in error_message + assert f"extra_unexpected_columns: {expected_extra}" in error_message + @pytest.mark.integration @pytest.mark.universal_offline_stores @@ -85,6 +111,12 @@ def test_writing_consecutively_to_offline_store(environment, universal_data_sour Field(name="avg_daily_trips", dtype=Int32), Field(name="conv_rate", dtype=Float32), Field(name="acc_rate", dtype=Float32), + Field(name="driver_metadata", dtype=Map), + Field(name="driver_config", dtype=Json), + Field( + name="driver_profile", + dtype=Struct({"name": String, "age": String}), + ), ], source=data_sources.driver, ttl=timedelta( @@ -103,6 +135,8 @@ def test_writing_consecutively_to_offline_store(environment, universal_data_sour ) store.apply([driver_entity, driver_stats]) + # Refresh registry after apply to ensure subsequent reads see the new feature view + store.refresh_registry() df = store.get_historical_features( entity_df=entity_df, features=[ @@ -125,6 +159,18 @@ def test_writing_consecutively_to_offline_store(environment, universal_data_sour "acc_rate": [random.random(), random.random()], "avg_daily_trips": [random.randint(0, 10), random.randint(0, 10)], "created": [ts, ts], + "driver_metadata": [ + {"vehicle_type": "sedan", "rating": "4.5"}, + {"vehicle_type": "suv", "rating": "3.8"}, + ], + "driver_config": [ + json.dumps({"max_distance_km": 100, "preferred_zones": ["north"]}), + json.dumps({"max_distance_km": 50, "preferred_zones": ["south"]}), + ], + "driver_profile": [ + {"name": "driver_1001", "age": "30"}, + {"name": "driver_1001", "age": "35"}, + ], }, ) first_df = first_df.astype({"conv_rate": "float32", "acc_rate": "float32"}) @@ -169,6 +215,18 @@ def test_writing_consecutively_to_offline_store(environment, universal_data_sour "acc_rate": [random.random(), random.random()], "avg_daily_trips": [random.randint(0, 10), random.randint(0, 10)], "created": [ts, ts], + "driver_metadata": [ + {"vehicle_type": "truck", "rating": "4.0"}, + {"vehicle_type": "sedan", "rating": "4.2"}, + ], + "driver_config": [ + json.dumps({"max_distance_km": 150, "preferred_zones": ["east"]}), + json.dumps({"max_distance_km": 200, "preferred_zones": ["west"]}), + ], + "driver_profile": [ + {"name": "driver_1001", "age": "31"}, + {"name": "driver_1001", "age": "36"}, + ], }, ) second_df = second_df.astype({"conv_rate": "float32", "acc_rate": "float32"}) diff --git a/sdk/python/tests/integration/offline_store/test_persist.py b/sdk/python/tests/integration/offline_store/test_persist.py index 8e6f1829174..80b024310b1 100644 --- a/sdk/python/tests/integration/offline_store/test_persist.py +++ b/sdk/python/tests/integration/offline_store/test_persist.py @@ -2,10 +2,10 @@ from feast.errors import SavedDatasetLocationAlreadyExists from feast.saved_dataset import SavedDatasetStorage -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_universal_feature_views, ) -from tests.integration.feature_repos.universal.entities import ( +from tests.universal.feature_repos.universal.entities import ( customer, driver, location, diff --git a/sdk/python/tests/integration/offline_store/test_push_features_to_offline_store.py b/sdk/python/tests/integration/offline_store/test_push_features_to_offline_store.py index 5e3d72e671b..7d038063ee5 100644 --- a/sdk/python/tests/integration/offline_store/test_push_features_to_offline_store.py +++ b/sdk/python/tests/integration/offline_store/test_push_features_to_offline_store.py @@ -4,10 +4,10 @@ from feast.data_source import PushMode from feast.utils import _utc_now -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_universal_feature_views, ) -from tests.integration.feature_repos.universal.entities import location +from tests.universal.feature_repos.universal.entities import location @pytest.mark.integration diff --git a/sdk/python/tests/integration/offline_store/test_s3_custom_endpoint.py b/sdk/python/tests/integration/offline_store/test_s3_custom_endpoint.py index 645e0f7331f..45426c63b8d 100644 --- a/sdk/python/tests/integration/offline_store/test_s3_custom_endpoint.py +++ b/sdk/python/tests/integration/offline_store/test_s3_custom_endpoint.py @@ -3,12 +3,12 @@ import pytest from feast.feature_store import FeastObject -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( IntegrationTestRepoConfig, construct_test_environment, construct_universal_feature_views, ) -from tests.integration.feature_repos.universal.entities import customer, driver +from tests.universal.feature_repos.universal.entities import customer, driver # TODO: Allow integration tests to run using different credentials. @@ -21,7 +21,7 @@ def test_registration_and_retrieval_from_custom_s3_endpoint( universal_data_sources, ): config = IntegrationTestRepoConfig( - offline_store_creator="tests.integration.feature_repos.universal.data_sources.file.S3FileDataSourceCreator" + offline_store_creator="tests.universal.feature_repos.universal.data_sources.file.S3FileDataSourceCreator" ) import os diff --git a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py index 37df649386c..6a17fc47bbb 100644 --- a/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py +++ b/sdk/python/tests/integration/offline_store/test_universal_historical_retrieval.py @@ -16,19 +16,16 @@ ) from feast.types import Float32, Int32, String from feast.utils import _utc_now -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_universal_feature_views, table_name_from_data_source, ) -from tests.integration.feature_repos.universal.data_sources.file import ( +from tests.universal.feature_repos.universal.data_sources.file import ( RemoteOfflineOidcAuthStoreDataSourceCreator, RemoteOfflineStoreDataSourceCreator, RemoteOfflineTlsStoreDataSourceCreator, ) -from tests.integration.feature_repos.universal.data_sources.snowflake import ( - SnowflakeDataSourceCreator, -) -from tests.integration.feature_repos.universal.entities import ( +from tests.universal.feature_repos.universal.entities import ( customer, driver, location, @@ -47,19 +44,14 @@ @pytest.mark.integration @pytest.mark.universal_offline_stores @pytest.mark.parametrize("full_feature_names", [True, False], ids=lambda v: f"full:{v}") -@pytest.mark.parametrize( - "use_substrait_odfv", [True, False], ids=lambda v: f"substrait:{v}" -) def test_historical_features_main( - environment, universal_data_sources, full_feature_names, use_substrait_odfv + environment, universal_data_sources, full_feature_names ): store = environment.feature_store (entities, datasets, data_sources) = universal_data_sources - feature_views = construct_universal_feature_views( - data_sources, use_substrait_odfv=use_substrait_odfv - ) + feature_views = construct_universal_feature_views(data_sources) entity_df_with_request_data = datasets.entity_df.copy(deep=True) entity_df_with_request_data["val_to_add"] = [ @@ -284,7 +276,7 @@ def test_historical_features_with_entities_from_query( raise pytest.skip("Offline source is not sql-based") data_source_creator = environment.data_source_creator - if isinstance(data_source_creator, SnowflakeDataSourceCreator): + if type(data_source_creator).__name__ == "SnowflakeDataSourceCreator": entity_df_query = f""" SELECT "customer_id", "driver_id", "order_id", "origin_id", "destination_id", "event_timestamp" FROM "{orders_table}" @@ -736,3 +728,311 @@ def test_historical_features_field_mapping( actual_df, sort_by=["driver_id"], ) + + +@pytest.mark.integration +@pytest.mark.universal_offline_stores(only=["file"]) +def test_historical_features_non_entity_retrieval(environment): + """Test get_historical_features with entity_df=None using start_date/end_date. + + This exercises the non-entity retrieval path where a synthetic entity_df is + generated internally. Regression test for the bug where start_date was used + instead of end_date for min_event_timestamp in the synthetic entity_df. + """ + store = environment.feature_store + + now = datetime.now().replace(microsecond=0, second=0, minute=0) + two_days_ago = now - timedelta(days=2) + one_day_ago = now - timedelta(days=1) + + driver_stats_df = pd.DataFrame( + data=[ + { + "driver_id": 1001, + "avg_daily_trips": 10, + "event_timestamp": two_days_ago, + "created": two_days_ago, + }, + { + "driver_id": 1001, + "avg_daily_trips": 20, + "event_timestamp": one_day_ago, + "created": one_day_ago, + }, + { + "driver_id": 1001, + "avg_daily_trips": 30, + "event_timestamp": now, + "created": now, + }, + { + "driver_id": 1002, + "avg_daily_trips": 100, + "event_timestamp": two_days_ago, + "created": two_days_ago, + }, + { + "driver_id": 1002, + "avg_daily_trips": 200, + "event_timestamp": one_day_ago, + "created": one_day_ago, + }, + { + "driver_id": 1002, + "avg_daily_trips": 300, + "event_timestamp": now, + "created": now, + }, + ] + ) + + start_date = now - timedelta(days=3) + end_date = now + timedelta(hours=1) + + driver_stats_data_source = environment.data_source_creator.create_data_source( + df=driver_stats_df, + destination_name=f"test_driver_stats_{int(time.time_ns())}_{random.randint(1000, 9999)}", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + driver_entity = Entity(name="driver", join_keys=["driver_id"]) + driver_fv = FeatureView( + name="driver_stats", + entities=[driver_entity], + schema=[Field(name="avg_daily_trips", dtype=Int32)], + source=driver_stats_data_source, + ) + + store.apply([driver_entity, driver_fv]) + + offline_job = store.get_historical_features( + entity_df=None, + features=["driver_stats:avg_daily_trips"], + full_feature_names=False, + start_date=start_date, + end_date=end_date, + ) + + actual_df = offline_job.to_df() + + assert not actual_df.empty, "Result should not be empty" + assert "avg_daily_trips" in actual_df.columns + + actual_driver_ids = set(actual_df["driver_id"].tolist()) + assert 1001 in actual_driver_ids, "driver 1001 should be in results" + assert 1002 in actual_driver_ids, "driver 1002 should be in results" + + # Verify timestamps fall within the requested range. + # Strip tz info to avoid tz-naive vs tz-aware comparison issues. + ts_start = pd.Timestamp(start_date).tz_localize(None) + ts_end = pd.Timestamp(end_date).tz_localize(None) + for ts in actual_df["event_timestamp"]: + ts_val = pd.Timestamp(ts).tz_localize(None) + assert ts_val >= ts_start, f"Timestamp {ts_val} before start_date" + assert ts_val <= ts_end, f"Timestamp {ts_val} after end_date" + + # The latest features must be present -- this is the critical regression check. + # With the old bug (using start_date instead of end_date), the synthetic entity_df + # had wrong max_event_timestamp causing the latest rows to be missed. + actual_trips = set(actual_df["avg_daily_trips"].tolist()) + assert 30 in actual_trips, "Latest trip value 30 for driver 1001 should be present" + assert 300 in actual_trips, ( + "Latest trip value 300 for driver 1002 should be present" + ) + + +@pytest.mark.integration +@pytest.mark.universal_offline_stores +@pytest.mark.parametrize("full_feature_names", [True, False], ids=lambda v: f"full:{v}") +def test_odfv_projection(environment, universal_data_sources, full_feature_names): + """ + Test that requesting a subset of ODFV features only returns those features. + + Regression test for issue #6099: OnDemandFeatureViews should honor output + projection in offline retrieval, matching the behavior of online retrieval. + + Before the fix, offline retrieval would return ALL ODFV output features even + when only a subset was requested, while online retrieval correctly returned + only the requested features. + """ + store = environment.feature_store + + (entities, datasets, data_sources) = universal_data_sources + + feature_views = construct_universal_feature_views(data_sources) + + # Add request data needed for ODFV + entity_df_with_request_data = datasets.entity_df.copy(deep=True) + entity_df_with_request_data["val_to_add"] = [ + i for i in range(len(entity_df_with_request_data)) + ] + + store.apply([driver(), *feature_views.values()]) + + # The conv_rate_plus_100 ODFV has 3 output features: + # - conv_rate_plus_100 + # - conv_rate_plus_val_to_add + # - conv_rate_plus_100_rounded + + # Test 1: Request only ONE ODFV feature + job = store.get_historical_features( + entity_df=entity_df_with_request_data, + features=[ + "conv_rate_plus_100:conv_rate_plus_100", # Request only this one + ], + full_feature_names=full_feature_names, + ) + + actual_df = job.to_df() + + # Determine expected column names based on full_feature_names setting + expected_feature = ( + "conv_rate_plus_100__conv_rate_plus_100" + if full_feature_names + else "conv_rate_plus_100" + ) + unrequested_feature_1 = ( + "conv_rate_plus_100__conv_rate_plus_val_to_add" + if full_feature_names + else "conv_rate_plus_val_to_add" + ) + unrequested_feature_2 = ( + "conv_rate_plus_100__conv_rate_plus_100_rounded" + if full_feature_names + else "conv_rate_plus_100_rounded" + ) + + # Verify the requested feature is present + assert expected_feature in actual_df.columns, ( + f"Requested feature '{expected_feature}' should be in the result" + ) + + # Verify unrequested ODFV features are NOT present (this is the key fix) + assert unrequested_feature_1 not in actual_df.columns, ( + f"Unrequested ODFV feature '{unrequested_feature_1}' should NOT be in the result. " + f"This indicates the bug from issue #6099 still exists." + ) + assert unrequested_feature_2 not in actual_df.columns, ( + f"Unrequested ODFV feature '{unrequested_feature_2}' should NOT be in the result. " + f"This indicates the bug from issue #6099 still exists." + ) + + # Test 2: Request TWO out of THREE ODFV features + job2 = store.get_historical_features( + entity_df=entity_df_with_request_data, + features=[ + "conv_rate_plus_100:conv_rate_plus_100", + "conv_rate_plus_100:conv_rate_plus_val_to_add", + # Deliberately NOT requesting conv_rate_plus_100_rounded + ], + full_feature_names=full_feature_names, + ) + + actual_df2 = job2.to_df() + + # Verify the two requested features are present + assert expected_feature in actual_df2.columns + assert unrequested_feature_1 in actual_df2.columns + + # Verify the unrequested feature is NOT present + assert unrequested_feature_2 not in actual_df2.columns, ( + f"Unrequested ODFV feature '{unrequested_feature_2}' should NOT be in the result" + ) + + +@pytest.mark.integration +@pytest.mark.universal_offline_stores +def test_historical_features_filter_by_created_timestamp(environment): + store = environment.feature_store + + now = datetime.now().replace(microsecond=0, second=0, minute=0) + tomorrow = now + timedelta(days=1) + day_after_tomorrow = now + timedelta(days=2) + + entity_df = pd.DataFrame( + data=[ + {"driver_id": 1001, "event_timestamp": tomorrow}, + {"driver_id": 1002, "event_timestamp": tomorrow}, + ] + ) + + driver_stats_df = pd.DataFrame( + data=[ + # Values that were already created at the entity timestamp + { + "driver_id": 1001, + "avg_daily_trips": 10, + "event_timestamp": now, + "created": now, + }, + { + "driver_id": 1002, + "avg_daily_trips": 30, + "event_timestamp": now, + "created": now, + }, + # Backfilled values for the same event timestamps, created after the entity timestamp + { + "driver_id": 1001, + "avg_daily_trips": 20, + "event_timestamp": now, + "created": day_after_tomorrow, + }, + { + "driver_id": 1002, + "avg_daily_trips": 40, + "event_timestamp": now, + "created": day_after_tomorrow, + }, + ] + ) + + driver_stats_data_source = environment.data_source_creator.create_data_source( + df=driver_stats_df, + destination_name=f"test_driver_stats_{int(time.time_ns())}_{random.randint(1000, 9999)}", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + driver = Entity(name="driver", join_keys=["driver_id"]) + driver_fv = FeatureView( + name="driver_stats", + entities=[driver], + schema=[Field(name="avg_daily_trips", dtype=Int32)], + source=driver_stats_data_source, + ) + + store.apply([driver, driver_fv]) + + # Default: the backfilled values win the dedup + actual_df = store.get_historical_features( + entity_df=entity_df, + features=["driver_stats:avg_daily_trips"], + full_feature_names=False, + ).to_df() + expected_df = pd.DataFrame( + data=[ + {"driver_id": 1001, "event_timestamp": tomorrow, "avg_daily_trips": 20}, + {"driver_id": 1002, "event_timestamp": tomorrow, "avg_daily_trips": 40}, + ] + ) + validate_dataframes(expected_df, actual_df, sort_by=["driver_id"]) + + try: + job = store.get_historical_features( + entity_df=entity_df, + features=["driver_stats:avg_daily_trips"], + full_feature_names=False, + filter_by_created_timestamp=True, + ) + except NotImplementedError: + pytest.skip("The offline store does not support filter_by_created_timestamp") + actual_df = job.to_df() + expected_df = pd.DataFrame( + data=[ + {"driver_id": 1001, "event_timestamp": tomorrow, "avg_daily_trips": 10}, + {"driver_id": 1002, "event_timestamp": tomorrow, "avg_daily_trips": 30}, + ] + ) + validate_dataframes(expected_df, actual_df, sort_by=["driver_id"]) diff --git a/sdk/python/tests/integration/offline_store/test_universal_materialization.py b/sdk/python/tests/integration/offline_store/test_universal_materialization.py new file mode 100644 index 00000000000..bbb41bf5d36 --- /dev/null +++ b/sdk/python/tests/integration/offline_store/test_universal_materialization.py @@ -0,0 +1,39 @@ +from datetime import timedelta + +import pytest + +from feast import Entity, FeatureView, Field +from feast.types import Float32 +from tests.data.data_creator import create_basic_driver_dataset +from tests.utils.e2e_test_validation import validate_offline_online_store_consistency + + +@pytest.mark.integration +@pytest.mark.universal_offline_stores +@pytest.mark.parametrize("materialization_pull_latest", [True, False]) +def test_universal_materialization_consistency( + environment, materialization_pull_latest +): + environment.materialization.pull_latest_features = materialization_pull_latest + + fs = environment.feature_store + df = create_basic_driver_dataset() + ds = environment.data_source_creator.create_data_source( + df, + fs.project, + field_mapping={"ts_1": "ts"}, + ) + driver = Entity( + name="driver_id", + join_keys=["driver_id"], + ) + driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(weeks=52), + schema=[Field(name="value", dtype=Float32)], + source=ds, + ) + fs.apply([driver, driver_stats_fv]) + split_dt = df["ts_1"][4].to_pydatetime() - timedelta(seconds=1) + validate_offline_online_store_consistency(fs, driver_stats_fv, split_dt) diff --git a/sdk/python/tests/integration/registration/test_universal_types.py b/sdk/python/tests/integration/offline_store/test_universal_types.py similarity index 58% rename from sdk/python/tests/integration/registration/test_universal_types.py rename to sdk/python/tests/integration/offline_store/test_universal_types.py index b464cf2f766..011508dd634 100644 --- a/sdk/python/tests/integration/registration/test_universal_types.py +++ b/sdk/python/tests/integration/offline_store/test_universal_types.py @@ -1,7 +1,6 @@ import logging -from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Tuple, Union import numpy as np import pandas as pd @@ -10,24 +9,48 @@ from feast.infra.offline_stores.offline_store import RetrievalJob from feast.types import ( - Array, - Bool, - FeastType, Float32, Float64, Int32, Int64, String, - UnixTimestamp, ) from feast.utils import _utc_now from tests.data.data_creator import create_basic_driver_dataset -from tests.integration.feature_repos.universal.entities import driver -from tests.integration.feature_repos.universal.feature_views import driver_feature_view +from tests.universal.feature_repos.universal.entities import driver +from tests.universal.feature_repos.universal.feature_views import driver_feature_view +from tests.utils.type_test_utils import ( + TypeTestConfig, + get_feast_type, + get_type_test_fixtures, + populate_test_configs, +) logger = logging.getLogger(__name__) +OFFLINE_TYPE_TEST_CONFIGS: List[TypeTestConfig] = populate_test_configs() + + +@pytest.fixture( + params=OFFLINE_TYPE_TEST_CONFIGS, + ids=[str(c) for c in OFFLINE_TYPE_TEST_CONFIGS], +) +def offline_types_test_fixtures(request, environment): + config: TypeTestConfig = request.param + if environment.provider == "aws" and config.feature_is_list is True: + pytest.skip("Redshift doesn't support list features") + if ( + environment.data_source_creator.__class__.__name__ + == "ClickhouseDataSourceCreator" + and config.feature_is_list + and not config.has_empty_list + ): + pytest.skip("Clickhouse doesn't support Nullable(Array) type features") + + return get_type_test_fixtures(request, environment) + + @pytest.mark.integration @pytest.mark.universal_offline_stores @pytest.mark.parametrize("entity_type", [Int32, Int64, String]) @@ -48,7 +71,7 @@ def test_entity_inference_types_match(environment, entity_type): data_source=data_source, name=f"fv_entity_type_{entity_type.name.lower()}", infer_entities=True, # Forces entity inference by not including a field for the entity. - dtype=_get_feast_type("int32", False), + dtype=get_feast_type("int32", False), entity_type=entity_type, ) @@ -88,7 +111,7 @@ def test_feature_get_historical_features_types_match( fv = driver_feature_view( data_source=data_source, name="get_historical_features_types_match", - dtype=_get_feast_type(config.feature_dtype, config.feature_is_list), + dtype=get_feast_type(config.feature_dtype, config.feature_is_list), ) fs.apply([fv, entity]) @@ -127,88 +150,6 @@ def test_feature_get_historical_features_types_match( ) -@pytest.mark.integration -@pytest.mark.universal_online_stores(only=["sqlite"]) -def test_feature_get_online_features_types_match( - online_types_test_fixtures, environment -): - config, data_source, fv = online_types_test_fixtures - entity = driver() - fv = driver_feature_view( - data_source=data_source, - name="get_online_features_types_match", - dtype=_get_feast_type(config.feature_dtype, config.feature_is_list), - ) - fs = environment.feature_store - features = [fv.name + ":value"] - fs.apply([fv, entity]) - fs.materialize( - environment.start_date, - environment.end_date - - timedelta(hours=1), # throwing out last record to make sure - # we can successfully infer type even from all empty values - ) - - online_features = fs.get_online_features( - features=features, - entity_rows=[{"driver_id": 1}], - ).to_dict() - - feature_list_dtype_to_expected_online_response_value_type = { - "int32": int, - "int64": int, - "float": float, - "string": str, - "bool": bool, - "datetime": datetime, - } - expected_dtype = feature_list_dtype_to_expected_online_response_value_type[ - config.feature_dtype - ] - - assert len(online_features["value"]) == 1 - - if config.feature_is_list: - for feature in online_features["value"]: - assert isinstance(feature, list), "Feature value should be a list" - assert config.has_empty_list or len(feature) > 0, ( - "List of values should not be empty" - ) - for element in feature: - assert isinstance(element, expected_dtype) - else: - for feature in online_features["value"]: - assert isinstance(feature, expected_dtype) - - -def _get_feast_type(feature_dtype: str, feature_is_list: bool) -> FeastType: - dtype: Optional[FeastType] = None - if feature_is_list is True: - if feature_dtype == "int32": - dtype = Array(Int32) - elif feature_dtype == "int64": - dtype = Array(Int64) - elif feature_dtype == "float": - dtype = Array(Float32) - elif feature_dtype == "bool": - dtype = Array(Bool) - elif feature_dtype == "datetime": - dtype = Array(UnixTimestamp) - else: - if feature_dtype == "int32": - dtype = Int32 - elif feature_dtype == "int64": - dtype = Int64 - elif feature_dtype == "float": - dtype = Float32 - elif feature_dtype == "bool": - dtype = Bool - elif feature_dtype == "datetime": - dtype = UnixTimestamp - assert dtype - return dtype - - def assert_expected_historical_feature_types( feature_dtype: str, historical_features_df: pd.DataFrame ): @@ -293,96 +234,3 @@ def assert_expected_arrow_types( assert arrow_type_checker(pa_type.value_type) else: assert arrow_type_checker(pa_type) - - -def populate_test_configs(offline: bool): - feature_dtypes = [ - "int32", - "int64", - "float", - "bool", - "datetime", - ] - configs: List[TypeTestConfig] = [] - for feature_dtype in feature_dtypes: - for feature_is_list in [True, False]: - for has_empty_list in [True, False]: - # For non list features `has_empty_list` does nothing - if feature_is_list is False and has_empty_list is True: - continue - - configs.append( - TypeTestConfig( - feature_dtype=feature_dtype, - feature_is_list=feature_is_list, - has_empty_list=has_empty_list, - ) - ) - return configs - - -@dataclass(frozen=True, repr=True) -class TypeTestConfig: - feature_dtype: str - feature_is_list: bool - has_empty_list: bool - - -OFFLINE_TYPE_TEST_CONFIGS: List[TypeTestConfig] = populate_test_configs(offline=True) -ONLINE_TYPE_TEST_CONFIGS: List[TypeTestConfig] = populate_test_configs(offline=False) - - -@pytest.fixture( - params=OFFLINE_TYPE_TEST_CONFIGS, - ids=[str(c) for c in OFFLINE_TYPE_TEST_CONFIGS], -) -def offline_types_test_fixtures(request, environment): - config: TypeTestConfig = request.param - if environment.provider == "aws" and config.feature_is_list is True: - pytest.skip("Redshift doesn't support list features") - if ( - environment.data_source_creator.__class__.__name__ - == "ClickhouseDataSourceCreator" - and config.feature_is_list - and not config.has_empty_list - ): - pytest.skip("Clickhouse doesn't support Nullable(Array) type features") - - return get_fixtures(request, environment) - - -@pytest.fixture( - params=ONLINE_TYPE_TEST_CONFIGS, - ids=[str(c) for c in ONLINE_TYPE_TEST_CONFIGS], -) -def online_types_test_fixtures(request, environment): - return get_fixtures(request, environment) - - -def get_fixtures(request, environment): - config: TypeTestConfig = request.param - # Lower case needed because Redshift lower-cases all table names - destination_name = ( - f"feature_type_{config.feature_dtype}{config.feature_is_list}".replace( - ".", "" - ).lower() - ) - config = request.param - df = create_basic_driver_dataset( - Int64, - config.feature_dtype, - config.feature_is_list, - config.has_empty_list, - ) - data_source = environment.data_source_creator.create_data_source( - df, - destination_name=destination_name, - field_mapping={"ts_1": "ts"}, - ) - fv = driver_feature_view( - data_source=data_source, - name=destination_name, - dtype=_get_feast_type(config.feature_dtype, config.feature_is_list), - ) - - return config, data_source, fv diff --git a/sdk/python/tests/integration/online_store/__init__.py b/sdk/python/tests/integration/online_store/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/integration/online_store/conftest.py b/sdk/python/tests/integration/online_store/conftest.py new file mode 100644 index 00000000000..42b507231dc --- /dev/null +++ b/sdk/python/tests/integration/online_store/conftest.py @@ -0,0 +1,173 @@ +""" +Fixtures for online-store integration tests. +""" + +from typing import Dict + +import pytest +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_for_logs + +from tests.universal.feature_repos.universal.online_store_creator import ( + OnlineStoreCreator, +) + + +class _SharedDbDynamoDBOnlineStoreCreator(OnlineStoreCreator): + """DynamoDB Local container started with ``-sharedDb -inMemory``. + + Why ``-sharedDb`` + ----------------- + DynamoDB Local 2.x namespaces tables by the **access key ID** in the + request signature. In CI, the sync ``boto3`` client and the async + ``aiobotocore`` client can resolve credentials from *different* sources + (env vars, credential file, ``credential_process``, container IAM role, + etc.) even after ``monkeypatch.setenv`` has set fake keys—because the + credential chain is evaluated lazily and various caches may hold stale + values. + + When the two clients end up using *different* access keys, the sync + client creates tables in namespace A while the async client queries + namespace B, which is empty → ``ResourceNotFoundException``. + + ``-sharedDb`` collapses all namespaces into a single in-memory database, + making table visibility completely independent of which credentials each + client uses. This is the correct setting for integration tests that want + to verify async read/write behaviour without caring about credential + isolation. + """ + + def __init__(self, project_name: str, **kwargs): + super().__init__(project_name) + self.container = ( + DockerContainer("amazon/dynamodb-local:latest") + .with_exposed_ports("8000") + .with_command("-jar DynamoDBLocal.jar -sharedDb -inMemory") + ) + + def create_online_store(self) -> Dict[str, str]: + self.container.start() + wait_for_logs( + container=self.container, + predicate="Initializing DynamoDB Local with the following configuration:", + timeout=10, + ) + exposed_port = self.container.get_exposed_port("8000") + return { + "type": "dynamodb", + "endpoint_url": f"http://localhost:{exposed_port}", + "region": "us-west-2", + } + + def teardown(self): + self.container.stop() + + +@pytest.fixture +async def dynamodb_local_environment(monkeypatch, worker_id): + """Isolated, self-contained Environment for DynamoDB async tests. + + Root cause of the async credential failures + ------------------------------------------- + DynamoDB Local 2.x isolates tables **per access key ID**. In CI, + ``boto3`` (sync, used to provision tables via ``store.apply()``) and + ``aiobotocore`` (async, used for reads/writes in the test body) may + resolve credentials from *different* sources even when ``monkeypatch`` + has set fake static keys—the credential chain is evaluated lazily and + caches may hold stale values from a real AWS session configured in the + runner environment. + + When the two clients end up using different access key IDs they land in + different DynamoDB Local namespaces: + + * sync client → namespace ``KEY_A`` → tables exist ✓ + * async client → namespace ``KEY_B`` → tables not found → ``ResourceNotFoundException`` + + Fix: ``_SharedDbDynamoDBOnlineStoreCreator`` + -------------------------------------------- + The isolated container is started with ``-sharedDb -inMemory``. In + shared-DB mode DynamoDB Local stores *all* tables in a single namespace + regardless of the access key, so sync and async clients always see the + same tables. + + Why async + ``await fs.initialize()`` before yielding + ----------------------------------------------------- + Calling ``await fs.initialize()`` eagerly creates the ``aiobotocore`` + client inside this fixture's event loop (the *same* loop the test will + run in). This pre-caches: + + 1. ``FeatureStore._provider`` so the identical ``DynamoDBOnlineStore`` + instance is reused for the entire test. + 2. The aiobotocore client, which is now unambiguously pointed at our + isolated container's ``endpoint_url``. + + Yields + ------ + tuple[Environment, TestData] + ``(environment, (entities, datasets, data_sources))`` + """ + from feast.infra.online_stores.dynamodb import DynamoDBOnlineStore + from tests.universal.feature_repos.integration_test_repo_config import ( + IntegrationTestRepoConfig, + ) + from tests.universal.feature_repos.repo_configuration import ( + construct_test_environment, + construct_universal_test_data, + ) + from tests.universal.feature_repos.universal.data_sources.file import ( + FileDataSourceCreator, + ) + + # Set fake static credentials before any boto client is created. + # These are accepted by DynamoDB Local regardless of validity. + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "fakeaccesskey000000") + monkeypatch.setenv( + "AWS_SECRET_ACCESS_KEY", "fakesecretkey0000000000000000000000000000" + ) + monkeypatch.delenv("AWS_SESSION_TOKEN", raising=False) + monkeypatch.delenv("AWS_SECURITY_TOKEN", raising=False) + # Prevent IMDS from injecting real session tokens on EC2-backed runners. + monkeypatch.setenv("AWS_EC2_METADATA_DISABLED", "true") + # Disable the container credentials provider (ECS/EKS IAM roles). + monkeypatch.delenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", raising=False) + monkeypatch.delenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", raising=False) + # Ensure no profile redirects boto to a different credential source. + monkeypatch.delenv("AWS_PROFILE", raising=False) + monkeypatch.delenv("AWS_DEFAULT_PROFILE", raising=False) + + # Reset class-level boto3 client caches so that no stale client from a + # previous test in this worker bleeds into our isolated environment. + DynamoDBOnlineStore._dynamodb_client = None + DynamoDBOnlineStore._dynamodb_resource = None + + config = IntegrationTestRepoConfig( + provider="local", + offline_store_creator=FileDataSourceCreator, + online_store_creator=_SharedDbDynamoDBOnlineStoreCreator, + online_store=None, + ) + + environment = construct_test_environment( + config, + fixture_request=None, + worker_id=worker_id, + ) + environment.setup() + + # FileDataSourceCreator writes only local Parquet files — no AWS calls. + universal_test_data = construct_universal_test_data(environment) + + # Eagerly initialise the aiobotocore client in *this* event loop so it + # is guaranteed to point at our container and is reused throughout the + # test body without lazy-init surprises. + await environment.feature_store.initialize() + + yield environment, universal_test_data + + # Cleanly shut down the async client before the container disappears. + await environment.feature_store.close() + environment.teardown() + + # Flush class-level caches so the next test starts completely fresh. + DynamoDBOnlineStore._dynamodb_client = None + DynamoDBOnlineStore._dynamodb_resource = None diff --git a/sdk/python/tests/integration/online_store/test_dynamodb_versioning.py b/sdk/python/tests/integration/online_store/test_dynamodb_versioning.py new file mode 100644 index 00000000000..2575e1dbb19 --- /dev/null +++ b/sdk/python/tests/integration/online_store/test_dynamodb_versioning.py @@ -0,0 +1,194 @@ +"""Integration tests for DynamoDB online store feature view versioning. + +Run with: pytest --integration sdk/python/tests/integration/online_store/test_dynamodb_versioning.py + +Uses moto to mock the DynamoDB service (no Docker required). +""" + +import os +from datetime import datetime, timedelta, timezone + +import pytest + +from feast import Entity, FeatureView +from feast.field import Field +from feast.infra.online_stores.dynamodb import DynamoDBOnlineStore +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import RegistryConfig, RepoConfig +from feast.types import Float32, Int64 +from feast.value_type import ValueType + + +def _make_feature_view(name="driver_stats", version="latest"): + entity = Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + return FeatureView( + name=name, + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + ], + version=version, + ) + + +def _make_entity_key(driver_id: int) -> EntityKeyProto: + entity_key = EntityKeyProto() + entity_key.join_keys.append("driver_id") + val = ValueProto() + val.int64_val = driver_id + entity_key.entity_values.append(val) + return entity_key + + +def _write_and_read(store, config, fv, driver_id=1001, trips=42): + entity_key = _make_entity_key(driver_id) + val = ValueProto() + val.int64_val = trips + now = datetime.now(tz=timezone.utc) + store.online_write_batch( + config, fv, [(entity_key, {"trips_today": val}, now, now)], None + ) + return store.online_read(config, fv, [entity_key], ["trips_today"]) + + +def _make_config(enable_versioning=False): + from feast.infra.online_stores.dynamodb import DynamoDBOnlineStoreConfig + + return RepoConfig( + project="test_project", + provider="local", + online_store=DynamoDBOnlineStoreConfig( + type="dynamodb", + region="us-east-1", + ), + registry=RegistryConfig( + path="/tmp/test_dynamodb_registry.pb", + enable_online_feature_view_versioning=enable_versioning, + ), + entity_key_serialization_version=3, + ) + + +@pytest.mark.integration +class TestDynamoDBVersioningIntegration: + """Integration tests for DynamoDB versioning using moto mock.""" + + @pytest.fixture(autouse=True) + def setup_dynamodb(self): + try: + from moto import mock_dynamodb + except ImportError: + pytest.skip("moto not installed") + + # Set dummy AWS credentials for moto + os.environ["AWS_ACCESS_KEY_ID"] = "testing" + os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" # noqa: S105 # pragma: allowlist secret + os.environ["AWS_SECURITY_TOKEN"] = "testing" # noqa: S105 # pragma: allowlist secret + os.environ["AWS_SESSION_TOKEN"] = "testing" # noqa: S105 # pragma: allowlist secret + os.environ["AWS_DEFAULT_REGION"] = "us-east-1" + + with mock_dynamodb(): + yield + + def test_write_read_without_versioning(self): + config = _make_config(enable_versioning=False) + store = DynamoDBOnlineStore() + fv = _make_feature_view() + store.update(config, [], [fv], [], [], False) + + result = _write_and_read(store, config, fv) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 42 + + def test_write_read_with_versioning_v1(self): + config = _make_config(enable_versioning=True) + store = DynamoDBOnlineStore() + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + + result = _write_and_read(store, config, fv) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 42 + + def test_version_isolation(self): + """Data written to v1 is not visible from v2.""" + config = _make_config(enable_versioning=True) + store = DynamoDBOnlineStore() + + fv_v1 = _make_feature_view() + fv_v1.current_version_number = 1 + store.update(config, [], [fv_v1], [], [], False) + _write_and_read(store, config, fv_v1, driver_id=1001, trips=10) + + fv_v2 = _make_feature_view() + fv_v2.current_version_number = 2 + store.update(config, [], [fv_v2], [], [], False) + + entity_key = _make_entity_key(1001) + result = store.online_read(config, fv_v2, [entity_key], ["trips_today"]) + assert result[0] == (None, None) + + result = store.online_read(config, fv_v1, [entity_key], ["trips_today"]) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 10 + + def test_projection_version_tag_routes_to_correct_table(self): + """projection.version_tag routes reads to the correct versioned DynamoDB table.""" + config = _make_config(enable_versioning=True) + store = DynamoDBOnlineStore() + + fv_v1 = _make_feature_view() + fv_v1.current_version_number = 1 + store.update(config, [], [fv_v1], [], [], False) + _write_and_read(store, config, fv_v1, driver_id=1001, trips=100) + + fv_v2 = _make_feature_view() + fv_v2.current_version_number = 2 + store.update(config, [], [fv_v2], [], [], False) + _write_and_read(store, config, fv_v2, driver_id=1001, trips=200) + + fv_read = _make_feature_view() + fv_read.projection.version_tag = 1 + entity_key = _make_entity_key(1001) + result = store.online_read(config, fv_read, [entity_key], ["trips_today"]) + assert result[0][1]["trips_today"].int64_val == 100 + + fv_read2 = _make_feature_view() + fv_read2.projection.version_tag = 2 + result = store.online_read(config, fv_read2, [entity_key], ["trips_today"]) + assert result[0][1]["trips_today"].int64_val == 200 + + def test_teardown_versioned_table(self): + """teardown() drops the versioned DynamoDB table without error.""" + config = _make_config(enable_versioning=True) + store = DynamoDBOnlineStore() + + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + _write_and_read(store, config, fv) + + # Should not raise + store.teardown(config, [fv], []) + + def test_update_deletes_versioned_table(self): + """update() with tables_to_delete correctly drops versioned DynamoDB tables.""" + config = _make_config(enable_versioning=True) + store = DynamoDBOnlineStore() + + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + _write_and_read(store, config, fv, driver_id=1001, trips=50) + + # Delete the versioned table + store.update(config, [fv], [], [], [], False) diff --git a/sdk/python/tests/integration/online_store/test_hybrid_online_store.py b/sdk/python/tests/integration/online_store/test_hybrid_online_store.py index 4b9dad05ff8..26f07c54b14 100644 --- a/sdk/python/tests/integration/online_store/test_hybrid_online_store.py +++ b/sdk/python/tests/integration/online_store/test_hybrid_online_store.py @@ -12,6 +12,8 @@ from feast.protos.feast.types.Value_pb2 import Value from feast.types import PrimitiveFeastType +pytestmark = pytest.mark.integration + @pytest.fixture def sample_entity(): diff --git a/sdk/python/tests/integration/online_store/test_mongodb_online_retrieval.py b/sdk/python/tests/integration/online_store/test_mongodb_online_retrieval.py new file mode 100644 index 00000000000..9c198d9a617 --- /dev/null +++ b/sdk/python/tests/integration/online_store/test_mongodb_online_retrieval.py @@ -0,0 +1,165 @@ +"""MongoDB online store integration tests.""" + +import pytest + +from feast.protos.feast.types.EntityKey_pb2 import ( + EntityKey as EntityKeyProto, +) +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.utils import _utc_now +from tests.universal.feature_repos.universal.feature_views import TAGS +from tests.utils.cli_repo_creator import CliRunner, get_example_repo + +pytestmark = pytest.mark.integration +pytest.importorskip("pymongo") + +docker_available = False +try: + import docker + from testcontainers.mongodb import MongoDbContainer + + try: + client = docker.from_env() + client.ping() + docker_available = True + except Exception: + pass +except ImportError: + pass + +_requires_docker = pytest.mark.skipif( + not docker_available, + reason="Docker is not available or not running. Start Docker daemon to run these tests.", +) + + +@pytest.fixture(scope="module") +def mongodb_container(): + container = MongoDbContainer( + "mongo:latest", + username="test", + password="test", # pragma: allowlist secret + ).with_exposed_ports(27017) + container.start() + yield container + container.stop() + + +@pytest.fixture +def mongodb_connection_string(mongodb_container): + exposed_port = mongodb_container.get_exposed_port(27017) + return f"mongodb://test:test@localhost:{exposed_port}" # pragma: allowlist secret + + +@_requires_docker +def test_mongodb_online_features(mongodb_connection_string): + runner = CliRunner() + with runner.local_repo( + get_example_repo("example_feature_repo_1.py"), + offline_store="file", + online_store="mongodb", + teardown=False, + ) as store: + store.config.online_store.connection_string = mongodb_connection_string + + driver_locations_fv = store.get_feature_view(name="driver_locations") + customer_profile_fv = store.get_feature_view(name="customer_profile") + customer_driver_combined_fv = store.get_feature_view( + name="customer_driver_combined" + ) + + provider = store._get_provider() + + driver_key = EntityKeyProto( + join_keys=["driver_id"], entity_values=[ValueProto(int64_val=1)] + ) + provider.online_write_batch( + config=store.config, + table=driver_locations_fv, + data=[ + ( + driver_key, + { + "lat": ValueProto(double_val=0.1), + "lon": ValueProto(string_val="1.0"), + }, + _utc_now(), + _utc_now(), + ) + ], + progress=None, + ) + + customer_key = EntityKeyProto( + join_keys=["customer_id"], entity_values=[ValueProto(string_val="5")] + ) + provider.online_write_batch( + config=store.config, + table=customer_profile_fv, + data=[ + ( + customer_key, + { + "avg_orders_day": ValueProto(float_val=1.0), + "name": ValueProto(string_val="John"), + "age": ValueProto(int64_val=3), + }, + _utc_now(), + _utc_now(), + ) + ], + progress=None, + ) + + customer_key = EntityKeyProto( + join_keys=["customer_id", "driver_id"], + entity_values=[ValueProto(string_val="5"), ValueProto(int64_val=1)], + ) + provider.online_write_batch( + config=store.config, + table=customer_driver_combined_fv, + data=[ + ( + customer_key, + {"trips": ValueProto(int64_val=7)}, + _utc_now(), + _utc_now(), + ) + ], + progress=None, + ) + + assert len(store.list_entities()) == 3 + assert len(store.list_entities(tags=TAGS)) == 2 + + result = store.get_online_features( + features=[ + "driver_locations:lon", + "customer_profile:avg_orders_day", + "customer_profile:name", + "customer_driver_combined:trips", + ], + entity_rows=[ + {"driver_id": 1, "customer_id": "5"}, + {"driver_id": 1, "customer_id": 5}, + ], + full_feature_names=False, + ).to_dict() + + assert "lon" in result + assert "avg_orders_day" in result + assert "name" in result + assert result["driver_id"] == [1, 1] + assert result["customer_id"] == ["5", "5"] + assert result["lon"] == ["1.0", "1.0"] + assert result["avg_orders_day"] == [1.0, 1.0] + assert result["name"] == ["John", "John"] + assert result["trips"] == [7, 7] + + result = store.get_online_features( + features=["customer_driver_combined:trips"], + entity_rows=[{"driver_id": 0, "customer_id": 0}], + full_feature_names=False, + ).to_dict() + + assert result["trips"] == [None] diff --git a/sdk/python/tests/integration/online_store/test_mongodb_vector_search.py b/sdk/python/tests/integration/online_store/test_mongodb_vector_search.py new file mode 100644 index 00000000000..1a0892aed99 --- /dev/null +++ b/sdk/python/tests/integration/online_store/test_mongodb_vector_search.py @@ -0,0 +1,280 @@ +"""Integration tests for MongoDB Atlas Vector Search in MongoDBOnlineStore. + +These tests require Docker and the ``mongodb/mongodb-atlas-local:8.0.4`` image. +They exercise: + - Vector index creation during ``update()`` + - Write + retrieve round-trip with known embeddings + - ``top_k`` limiting + - Index cleanup on teardown +""" + +from __future__ import annotations + +import time +from datetime import datetime, timedelta +from typing import Any, Dict, List + +import numpy as np +import pytest + +from feast import Entity, FeatureView, RepoConfig +from feast.field import Field +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.online_stores.mongodb_online_store.mongodb import ( + MongoDBOnlineStore, +) +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.types import Array, Float32, Int64, String +from feast.value_type import ValueType +from tests.universal.feature_repos.universal.online_store.mongodb import ( + MongoDBAtlasOnlineStoreCreator, +) + +VECTOR_DIM = 3 +NUM_ROWS = 5 +INDEX_WAIT = 5 # seconds to wait for Atlas Search index eventual consistency + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _build_write_batch( + write_data: List[Dict[str, Any]], feature_view: FeatureView +) -> list: + """Build a list of (entity_key, features, event_ts, created_ts) tuples.""" + batch = [] + now = datetime.utcnow() + for row in write_data: + entity_key = EntityKeyProto( + join_keys=["item_id"], + entity_values=[ValueProto(int64_val=row["item_id"])], + ) + features: Dict[str, ValueProto] = { + "title": ValueProto(string_val=row["title"]), + } + emb_proto = ValueProto() + emb_proto.float_list_val.val.extend(row["embedding"]) + features["embedding"] = emb_proto + batch.append((entity_key, features, now, now)) + return batch + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def atlas_creator(): + """Start an Atlas-local container for the whole module.""" + creator = MongoDBAtlasOnlineStoreCreator(project_name="test_vs") + online_store_dict = creator.create_online_store() + yield online_store_dict, creator + creator.teardown() + + +@pytest.fixture(scope="module") +def repo_config(atlas_creator) -> RepoConfig: + online_store_dict, _ = atlas_creator + return RepoConfig( + project="test_vs", + provider="local", + online_store=online_store_dict, + registry="memory://", + entity_key_serialization_version=3, + ) + + +@pytest.fixture(scope="module") +def item_entity() -> Entity: + return Entity(name="item_id", join_keys=["item_id"], value_type=ValueType.INT64) + + +@pytest.fixture(scope="module") +def feature_view() -> FeatureView: + data_source = FileSource( + path="dummy_path.parquet", timestamp_field="event_timestamp" + ) + return FeatureView( + name="item_embeddings", + entities=[ + Entity( + name="item_id", + join_keys=["item_id"], + value_type=ValueType.INT64, + ) + ], + schema=[ + Field( + name="embedding", + dtype=Array(Float32), + vector_index=True, + vector_length=VECTOR_DIM, + vector_search_metric="cosine", + ), + Field(name="title", dtype=String), + Field(name="item_id", dtype=Int64), + ], + source=data_source, + ttl=timedelta(hours=24), + online=True, + ) + + +@pytest.fixture(scope="module") +def write_data() -> List[Dict[str, Any]]: + """Deterministic embeddings so we know the expected ordering.""" + np.random.seed(42) + rows = [] + for i in range(1, NUM_ROWS + 1): + rows.append( + { + "item_id": i, + "embedding": list(np.random.rand(VECTOR_DIM).astype(np.float32)), + "title": f"Document {i}", + } + ) + return rows + + +@pytest.fixture(scope="module") +def store() -> MongoDBOnlineStore: + """A single store instance shared across the module.""" + return MongoDBOnlineStore() + + +@pytest.fixture(scope="module") +def _setup_index_and_data(store, repo_config, feature_view, item_entity, write_data): + """One-time setup: create index, write data. Runs once for the module.""" + store.update( + config=repo_config, + tables_to_delete=[], + tables_to_keep=[feature_view], + entities_to_delete=[], + entities_to_keep=[item_entity], + partial=False, + ) + batch = _build_write_batch(write_data, feature_view) + store.online_write_batch( + config=repo_config, + table=feature_view, + data=batch, + progress=None, + ) + # Atlas Search indexes are eventually consistent — give the index time + # to pick up the newly written documents before running queries. + time.sleep(INDEX_WAIT) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +class TestMongoDBVectorSearch: + """Tests for MongoDB Atlas Vector Search integration.""" + + def test_vector_index_created_on_update( + self, store, repo_config, feature_view, item_entity, _setup_index_and_data + ): + """Verify that update() creates an Atlas vector search index.""" + clxn = store._get_collection(repo_config) + indexes = list(clxn.list_search_indexes()) + vs_index_names = [idx["name"] for idx in indexes] + expected = "item_embeddings__embedding__vs_index" + assert expected in vs_index_names, ( + f"Expected index '{expected}' not found. Got: {vs_index_names}" + ) + + def test_write_and_retrieve_round_trip( + self, store, repo_config, feature_view, write_data, _setup_index_and_data + ): + """Write embeddings then retrieve via vector search.""" + query_embedding = write_data[0]["embedding"] + results = store.retrieve_online_documents_v2( + config=repo_config, + table=feature_view, + requested_features=["embedding", "title"], + embedding=query_embedding, + top_k=3, + ) + assert len(results) == 3 + # Each result is (event_ts, entity_key_proto, feature_dict) + for ts, ek, fdict in results: + assert fdict is not None + assert "distance" in fdict + assert "title" in fdict + assert fdict["distance"].float_val >= 0 + + # The closest match should be the document itself (highest score) + _, _, best = results[0] + assert best["title"].string_val == "Document 1" + + def test_top_k_limiting( + self, store, repo_config, feature_view, write_data, _setup_index_and_data + ): + """Verify that top_k correctly limits the number of results.""" + query_embedding = write_data[0]["embedding"] + + results_2 = store.retrieve_online_documents_v2( + config=repo_config, + table=feature_view, + requested_features=["embedding", "title"], + embedding=query_embedding, + top_k=2, + ) + assert len(results_2) == 2 + + results_all = store.retrieve_online_documents_v2( + config=repo_config, + table=feature_view, + requested_features=["embedding", "title"], + embedding=query_embedding, + top_k=100, + ) + assert len(results_all) == NUM_ROWS + + def test_update_idempotent( + self, store, repo_config, feature_view, item_entity, _setup_index_and_data + ): + """Calling update() a second time should not error (index already exists).""" + # _setup_index_and_data already called update() once. + # A second call must be a no-op for the existing index. + store.update( + config=repo_config, + tables_to_delete=[], + tables_to_keep=[feature_view], + entities_to_delete=[], + entities_to_keep=[item_entity], + partial=False, + ) + clxn = store._get_collection(repo_config) + indexes = list(clxn.list_search_indexes()) + vs_index_names = [idx["name"] for idx in indexes] + expected = "item_embeddings__embedding__vs_index" + assert vs_index_names.count(expected) == 1, ( + f"Expected exactly one '{expected}' index, got: {vs_index_names}" + ) + + def test_index_cleanup_on_teardown( + self, store, repo_config, feature_view, item_entity, _setup_index_and_data + ): + """Verify teardown drops the collection (and thus all indexes). + + This test must run last as it destroys the collection. + """ + store.teardown( + config=repo_config, + tables=[feature_view], + entities=[item_entity], + ) + # After teardown the collection should be dropped + client = store._get_client(repo_config) + online_config = repo_config.online_store + db = client[online_config.database_name] + clxn_name = f"{repo_config.project}_{online_config.collection_suffix}" + assert clxn_name not in db.list_collection_names() diff --git a/sdk/python/tests/integration/online_store/test_mysql_versioning.py b/sdk/python/tests/integration/online_store/test_mysql_versioning.py new file mode 100644 index 00000000000..d7353cd4a08 --- /dev/null +++ b/sdk/python/tests/integration/online_store/test_mysql_versioning.py @@ -0,0 +1,188 @@ +"""Integration tests for MySQL online store feature view versioning. + +Run with: pytest --integration sdk/python/tests/integration/online_store/test_mysql_versioning.py +""" + +import shutil +from datetime import datetime, timedelta, timezone + +import pytest + +from feast import Entity, FeatureView +from feast.field import Field +from feast.infra.online_stores.mysql_online_store.mysql import MySQLOnlineStore +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import RegistryConfig, RepoConfig +from feast.types import Float32, Int64 +from feast.value_type import ValueType + + +def _make_feature_view(name="driver_stats", version="latest"): + entity = Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + return FeatureView( + name=name, + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + ], + version=version, + ) + + +def _make_entity_key(driver_id: int) -> EntityKeyProto: + entity_key = EntityKeyProto() + entity_key.join_keys.append("driver_id") + val = ValueProto() + val.int64_val = driver_id + entity_key.entity_values.append(val) + return entity_key + + +def _write_and_read(store, config, fv, driver_id=1001, trips=42): + entity_key = _make_entity_key(driver_id) + val = ValueProto() + val.int64_val = trips + now = datetime.now(tz=timezone.utc) + store.online_write_batch( + config, fv, [(entity_key, {"trips_today": val}, now, now)], None + ) + return store.online_read(config, fv, [entity_key], ["trips_today"]) + + +@pytest.mark.integration +@pytest.mark.skipif( + not shutil.which("docker"), + reason="Docker not available", +) +class TestMySQLVersioningIntegration: + """Integration tests for MySQL versioning with a real database.""" + + @pytest.fixture(autouse=True) + def setup_mysql(self): + try: + from testcontainers.mysql import MySqlContainer + except ImportError: + pytest.skip("testcontainers[mysql] not installed") + + self.container = MySqlContainer( + "mysql:8.0", + username="root", + password="testpass", # pragma: allowlist secret + dbname="feast", + dialect="pymysql", + ).with_exposed_ports(3306) + self.container.start() + self.port = self.container.get_exposed_port(3306) + yield + self.container.stop() + + def _make_config(self, enable_versioning=False): + from feast.infra.online_stores.mysql_online_store.mysql import ( + MySQLOnlineStoreConfig, + ) + + return RepoConfig( + project="test_project", + provider="local", + online_store=MySQLOnlineStoreConfig( + type="mysql", + host="localhost", + port=int(self.port), + user="root", + password="testpass", # pragma: allowlist secret + database="feast", + ), + registry=RegistryConfig( + path="/tmp/test_mysql_registry.pb", + enable_online_feature_view_versioning=enable_versioning, + ), + entity_key_serialization_version=3, + ) + + def test_write_read_without_versioning(self): + config = self._make_config(enable_versioning=False) + store = MySQLOnlineStore() + fv = _make_feature_view() + store.update(config, [], [fv], [], [], False) + + result = _write_and_read(store, config, fv) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 42 + + def test_write_read_with_versioning_v1(self): + config = self._make_config(enable_versioning=True) + store = MySQLOnlineStore() + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + + result = _write_and_read(store, config, fv) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 42 + + def test_version_isolation(self): + """Data written to v1 is not visible from v2.""" + config = self._make_config(enable_versioning=True) + store = MySQLOnlineStore() + + fv_v1 = _make_feature_view() + fv_v1.current_version_number = 1 + store.update(config, [], [fv_v1], [], [], False) + _write_and_read(store, config, fv_v1, driver_id=1001, trips=10) + + fv_v2 = _make_feature_view() + fv_v2.current_version_number = 2 + store.update(config, [], [fv_v2], [], [], False) + + entity_key = _make_entity_key(1001) + result = store.online_read(config, fv_v2, [entity_key], ["trips_today"]) + assert result[0] == (None, None) + + result = store.online_read(config, fv_v1, [entity_key], ["trips_today"]) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 10 + + def test_projection_version_tag_routes_to_correct_table(self): + """projection.version_tag routes reads to the correct versioned table.""" + config = self._make_config(enable_versioning=True) + store = MySQLOnlineStore() + + fv_v1 = _make_feature_view() + fv_v1.current_version_number = 1 + store.update(config, [], [fv_v1], [], [], False) + _write_and_read(store, config, fv_v1, driver_id=1001, trips=100) + + fv_v2 = _make_feature_view() + fv_v2.current_version_number = 2 + store.update(config, [], [fv_v2], [], [], False) + _write_and_read(store, config, fv_v2, driver_id=1001, trips=200) + + fv_read = _make_feature_view() + fv_read.projection.version_tag = 1 + entity_key = _make_entity_key(1001) + result = store.online_read(config, fv_read, [entity_key], ["trips_today"]) + assert result[0][1]["trips_today"].int64_val == 100 + + fv_read2 = _make_feature_view() + fv_read2.projection.version_tag = 2 + result = store.online_read(config, fv_read2, [entity_key], ["trips_today"]) + assert result[0][1]["trips_today"].int64_val == 200 + + def test_teardown_versioned_table(self): + config = self._make_config(enable_versioning=True) + store = MySQLOnlineStore() + + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + _write_and_read(store, config, fv) + + store.teardown(config, [fv], []) diff --git a/sdk/python/tests/integration/online_store/test_postgres_versioning.py b/sdk/python/tests/integration/online_store/test_postgres_versioning.py new file mode 100644 index 00000000000..9816c2a43c2 --- /dev/null +++ b/sdk/python/tests/integration/online_store/test_postgres_versioning.py @@ -0,0 +1,205 @@ +"""Integration tests for PostgreSQL online store feature view versioning. + +Run with: pytest --integration sdk/python/tests/integration/online_store/test_postgres_versioning.py +""" + +import shutil +from datetime import datetime, timedelta, timezone + +import pytest + +from feast import Entity, FeatureView +from feast.field import Field +from feast.infra.online_stores.postgres_online_store.postgres import ( + PostgreSQLOnlineStore, +) +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import RegistryConfig, RepoConfig +from feast.types import Float32, Int64 +from feast.value_type import ValueType + + +def _make_feature_view(name="driver_stats", version="latest"): + entity = Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + return FeatureView( + name=name, + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + ], + version=version, + ) + + +def _make_entity_key(driver_id: int) -> EntityKeyProto: + entity_key = EntityKeyProto() + entity_key.join_keys.append("driver_id") + val = ValueProto() + val.int64_val = driver_id + entity_key.entity_values.append(val) + return entity_key + + +def _write_and_read(store, config, fv, driver_id=1001, trips=42): + entity_key = _make_entity_key(driver_id) + val = ValueProto() + val.int64_val = trips + now = datetime.now(tz=timezone.utc) + store.online_write_batch( + config, fv, [(entity_key, {"trips_today": val}, now, now)], None + ) + return store.online_read(config, fv, [entity_key], ["trips_today"]) + + +@pytest.mark.integration +@pytest.mark.skipif( + not shutil.which("docker"), + reason="Docker not available", +) +class TestPostgresVersioningIntegration: + """Integration tests for PostgreSQL versioning with a real database.""" + + @pytest.fixture(autouse=True) + def setup_postgres(self): + try: + from testcontainers.postgres import PostgresContainer + except ImportError: + pytest.skip("testcontainers[postgres] not installed") + + self.container = PostgresContainer( + "postgres:16", + username="root", + password="testpass", # pragma: allowlist secret + dbname="test", + ).with_exposed_ports(5432) + self.container.start() + self.port = self.container.get_exposed_port(5432) + yield + self.container.stop() + + def _make_config(self, enable_versioning=False): + from feast.infra.online_stores.postgres_online_store.postgres import ( + PostgreSQLOnlineStoreConfig, + ) + + return RepoConfig( + project="test_project", + provider="local", + online_store=PostgreSQLOnlineStoreConfig( + type="postgres", + host="localhost", + port=int(self.port), + user="root", + password="testpass", # pragma: allowlist secret + database="test", + sslmode="disable", + ), + registry=RegistryConfig( + path="/tmp/test_pg_registry.pb", + enable_online_feature_view_versioning=enable_versioning, + ), + entity_key_serialization_version=3, + ) + + def test_write_read_without_versioning(self): + config = self._make_config(enable_versioning=False) + store = PostgreSQLOnlineStore() + fv = _make_feature_view() + store.update(config, [], [fv], [], [], False) + + result = _write_and_read(store, config, fv) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 42 + + def test_write_read_with_versioning_v1(self): + config = self._make_config(enable_versioning=True) + store = PostgreSQLOnlineStore() + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + + result = _write_and_read(store, config, fv) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 42 + + def test_version_isolation(self): + """Data written to v1 is not visible from v2.""" + config = self._make_config(enable_versioning=True) + store = PostgreSQLOnlineStore() + + fv_v1 = _make_feature_view() + fv_v1.current_version_number = 1 + store.update(config, [], [fv_v1], [], [], False) + _write_and_read(store, config, fv_v1, driver_id=1001, trips=10) + + fv_v2 = _make_feature_view() + fv_v2.current_version_number = 2 + store.update(config, [], [fv_v2], [], [], False) + + entity_key = _make_entity_key(1001) + result = store.online_read(config, fv_v2, [entity_key], ["trips_today"]) + assert result[0] == (None, None) + + result = store.online_read(config, fv_v1, [entity_key], ["trips_today"]) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 10 + + def test_projection_version_tag_routes_to_correct_table(self): + """projection.version_tag routes reads to the correct versioned table.""" + config = self._make_config(enable_versioning=True) + store = PostgreSQLOnlineStore() + + fv_v1 = _make_feature_view() + fv_v1.current_version_number = 1 + store.update(config, [], [fv_v1], [], [], False) + _write_and_read(store, config, fv_v1, driver_id=1001, trips=100) + + fv_v2 = _make_feature_view() + fv_v2.current_version_number = 2 + store.update(config, [], [fv_v2], [], [], False) + _write_and_read(store, config, fv_v2, driver_id=1001, trips=200) + + fv_read = _make_feature_view() + fv_read.projection.version_tag = 1 + entity_key = _make_entity_key(1001) + result = store.online_read(config, fv_read, [entity_key], ["trips_today"]) + assert result[0][1]["trips_today"].int64_val == 100 + + fv_read2 = _make_feature_view() + fv_read2.projection.version_tag = 2 + result = store.online_read(config, fv_read2, [entity_key], ["trips_today"]) + assert result[0][1]["trips_today"].int64_val == 200 + + def test_teardown_versioned_table(self): + """teardown() drops the versioned table without error.""" + config = self._make_config(enable_versioning=True) + store = PostgreSQLOnlineStore() + + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + _write_and_read(store, config, fv) + + # Should not raise + store.teardown(config, [fv], []) + + def test_update_deletes_versioned_table(self): + """update() with tables_to_delete correctly drops versioned tables.""" + config = self._make_config(enable_versioning=True) + store = PostgreSQLOnlineStore() + + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + _write_and_read(store, config, fv, driver_id=1001, trips=50) + + # Delete the versioned table + store.update(config, [fv], [], [], [], False) diff --git a/sdk/python/tests/integration/online_store/test_push_features_to_online_store.py b/sdk/python/tests/integration/online_store/test_push_features_to_online_store.py index 8986e21c57d..55a4eb12adc 100644 --- a/sdk/python/tests/integration/online_store/test_push_features_to_online_store.py +++ b/sdk/python/tests/integration/online_store/test_push_features_to_online_store.py @@ -2,10 +2,10 @@ import pytest from feast.utils import _utc_now -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( construct_universal_feature_views, ) -from tests.integration.feature_repos.universal.entities import location +from tests.universal.feature_repos.universal.entities import location @pytest.fixture @@ -47,7 +47,7 @@ def test_push_features_and_read(store): @pytest.mark.integration -@pytest.mark.universal_online_stores(only=["dynamodb"]) +@pytest.mark.universal_online_stores(only=["mongodb"]) async def test_push_features_and_read_async(store): await store.push_async("location_stats_push_source", _ingest_df()) @@ -56,3 +56,36 @@ async def test_push_features_and_read_async(store): entity_rows=[{"location_id": 1}], ) assert_response(online_resp) + + +@pytest.mark.asyncio +@pytest.mark.integration +@pytest.mark.universal_online_stores +async def test_push_features_and_read_async_dynamodb(dynamodb_local_environment): + """Async push + async read for DynamoDB with a credential-isolated environment. + + DynamoDB Local 2.x rejects requests that carry an expired AWS session + token. In CI, real (possibly expired) STS credentials exist in the + environment. The shared ``environment`` fixture resolves credentials + before the async client is created, so those bad credentials bleed in. + + This test uses ``dynamodb_local_environment``, which sets dummy + credentials *before* any boto client is instantiated, guaranteeing that + both the sync boto3 table-provisioning client and the async aiobotocore + client start with clean, token-free credentials. + """ + environment, universal_test_data = dynamodb_local_environment + store = environment.feature_store + _, _, data_sources = universal_test_data + + feature_views = construct_universal_feature_views(data_sources) + location_fv = feature_views.pushed_locations + store.apply([location(), location_fv]) + + await store.push_async("location_stats_push_source", _ingest_df()) + + online_resp = await store.get_online_features_async( + features=["pushable_location_stats:temperature"], + entity_rows=[{"location_id": 1}], + ) + assert_response(online_resp) diff --git a/sdk/python/tests/integration/online_store/test_redis_versioning.py b/sdk/python/tests/integration/online_store/test_redis_versioning.py new file mode 100644 index 00000000000..50ebe36b106 --- /dev/null +++ b/sdk/python/tests/integration/online_store/test_redis_versioning.py @@ -0,0 +1,207 @@ +"""Integration tests for Redis online store feature view versioning. + +Run with: pytest --integration sdk/python/tests/integration/online_store/test_redis_versioning.py +""" + +import shutil +from datetime import datetime, timedelta, timezone + +import pytest + +from feast import Entity, FeatureView +from feast.field import Field +from feast.infra.online_stores.redis import RedisOnlineStore +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import RegistryConfig, RepoConfig +from feast.types import Float32, Int64 +from feast.value_type import ValueType + + +def _make_feature_view(name="driver_stats", version="latest"): + entity = Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + return FeatureView( + name=name, + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + ], + version=version, + ) + + +def _make_entity_key(driver_id: int) -> EntityKeyProto: + entity_key = EntityKeyProto() + entity_key.join_keys.append("driver_id") + val = ValueProto() + val.int64_val = driver_id + entity_key.entity_values.append(val) + return entity_key + + +def _write_and_read(store, config, fv, driver_id=1001, trips=42): + entity_key = _make_entity_key(driver_id) + val = ValueProto() + val.int64_val = trips + now = datetime.now(tz=timezone.utc) + store.online_write_batch( + config, fv, [(entity_key, {"trips_today": val}, now, now)], None + ) + return store.online_read(config, fv, [entity_key], ["trips_today"]) + + +@pytest.mark.integration +@pytest.mark.skipif( + not shutil.which("docker"), + reason="Docker not available", +) +class TestRedisVersioningIntegration: + """Integration tests for Redis versioning with a real Redis instance.""" + + @pytest.fixture(autouse=True) + def setup_redis(self): + try: + from testcontainers.redis import RedisContainer + except ImportError: + pytest.skip("testcontainers[redis] not installed") + + self.container = RedisContainer("redis:7").with_exposed_ports(6379) + self.container.start() + self.port = self.container.get_exposed_port(6379) + yield + self.container.stop() + + def _make_config(self, enable_versioning=False): + from feast.infra.online_stores.redis import RedisOnlineStoreConfig + + return RepoConfig( + project="test_project", + provider="local", + online_store=RedisOnlineStoreConfig( + type="redis", + connection_string=f"localhost:{self.port}", + ), + registry=RegistryConfig( + path="/tmp/test_redis_registry.pb", + enable_online_feature_view_versioning=enable_versioning, + ), + entity_key_serialization_version=3, + ) + + def test_write_read_without_versioning(self): + config = self._make_config(enable_versioning=False) + store = RedisOnlineStore() + fv = _make_feature_view() + store.update(config, [], [fv], [], [], False) + + result = _write_and_read(store, config, fv) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 42 + + def test_write_read_with_versioning_v1(self): + config = self._make_config(enable_versioning=True) + store = RedisOnlineStore() + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + + result = _write_and_read(store, config, fv) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 42 + + def test_version_isolation(self): + """Data written to v1 is not visible from v2.""" + config = self._make_config(enable_versioning=True) + store = RedisOnlineStore() + + fv_v1 = _make_feature_view() + fv_v1.current_version_number = 1 + store.update(config, [], [fv_v1], [], [], False) + _write_and_read(store, config, fv_v1, driver_id=1001, trips=10) + + fv_v2 = _make_feature_view() + fv_v2.current_version_number = 2 + store.update(config, [], [fv_v2], [], [], False) + + entity_key = _make_entity_key(1001) + result = store.online_read(config, fv_v2, [entity_key], ["trips_today"]) + # In Redis, all versions share the same entity hash key. When v2 hash + # fields don't exist, hmget returns None values which become empty + # ValueProtos. The key assertion is that v1's actual data (10) does NOT + # leak through to v2. + ts_v2, feats_v2 = result[0] + assert feats_v2 is None or feats_v2["trips_today"].int64_val != 10 + + result = store.online_read(config, fv_v1, [entity_key], ["trips_today"]) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 10 + + def test_projection_version_tag_routes_to_correct_table(self): + """projection.version_tag routes reads to the correct versioned hash fields.""" + config = self._make_config(enable_versioning=True) + store = RedisOnlineStore() + + fv_v1 = _make_feature_view() + fv_v1.current_version_number = 1 + store.update(config, [], [fv_v1], [], [], False) + _write_and_read(store, config, fv_v1, driver_id=1001, trips=100) + + fv_v2 = _make_feature_view() + fv_v2.current_version_number = 2 + store.update(config, [], [fv_v2], [], [], False) + _write_and_read(store, config, fv_v2, driver_id=1001, trips=200) + + fv_read = _make_feature_view() + fv_read.projection.version_tag = 1 + entity_key = _make_entity_key(1001) + result = store.online_read(config, fv_read, [entity_key], ["trips_today"]) + assert result[0][1]["trips_today"].int64_val == 100 + + fv_read2 = _make_feature_view() + fv_read2.projection.version_tag = 2 + result = store.online_read(config, fv_read2, [entity_key], ["trips_today"]) + assert result[0][1]["trips_today"].int64_val == 200 + + def test_teardown_cleans_up(self): + """teardown() removes entity keys without error.""" + config = self._make_config(enable_versioning=True) + store = RedisOnlineStore() + + fv = _make_feature_view() + fv.current_version_number = 1 + store.update(config, [], [fv], [], [], False) + _write_and_read(store, config, fv) + + # Should not raise + store.teardown(config, [fv], []) + + def test_delete_table_versioned(self): + """delete_table() removes only the versioned hash fields.""" + config = self._make_config(enable_versioning=True) + store = RedisOnlineStore() + + fv_v1 = _make_feature_view() + fv_v1.current_version_number = 1 + store.update(config, [], [fv_v1], [], [], False) + _write_and_read(store, config, fv_v1, driver_id=1001, trips=10) + + fv_v2 = _make_feature_view() + fv_v2.current_version_number = 2 + store.update(config, [], [fv_v2], [], [], False) + _write_and_read(store, config, fv_v2, driver_id=1001, trips=20) + + # Delete v1 via update + store.update(config, [fv_v1], [fv_v2], [], [], False) + + entity_key = _make_entity_key(1001) + # v2 should still be readable + result = store.online_read(config, fv_v2, [entity_key], ["trips_today"]) + assert result[0][1] is not None + assert result[0][1]["trips_today"].int64_val == 20 diff --git a/sdk/python/tests/integration/online_store/test_remote_online_store.py b/sdk/python/tests/integration/online_store/test_remote_online_store.py index 80166abf431..3ee3d161d14 100644 --- a/sdk/python/tests/integration/online_store/test_remote_online_store.py +++ b/sdk/python/tests/integration/online_store/test_remote_online_store.py @@ -1,3 +1,4 @@ +import json import logging import os import tempfile @@ -383,6 +384,18 @@ def test_remote_online_store_read_write(auth_config, tls_mode): "avg_daily_trips": [50, 45], "event_timestamp": [pd.Timestamp(_utc_now()).round("ms")] * 2, "created": [pd.Timestamp(_utc_now()).round("ms")] * 2, + "driver_metadata": [ + {"vehicle_type": "sedan", "rating": "4.5"}, + {"vehicle_type": "suv", "rating": "3.8"}, + ], + "driver_config": [ + json.dumps({"max_distance_km": 100, "preferred_zones": ["north"]}), + json.dumps({"max_distance_km": 50, "preferred_zones": ["south"]}), + ], + "driver_profile": [ + {"name": "driver_1000", "age": "30"}, + {"name": "driver_1001", "age": "35"}, + ], } ) diff --git a/sdk/python/tests/integration/online_store/test_scylladb_online_store.py b/sdk/python/tests/integration/online_store/test_scylladb_online_store.py new file mode 100644 index 00000000000..4c2385d1e50 --- /dev/null +++ b/sdk/python/tests/integration/online_store/test_scylladb_online_store.py @@ -0,0 +1,489 @@ +"""Integration tests for ScyllaDBOnlineStore, including vector search. + +All tests (regular read/write and vector search) run against a local +ScyllaDB + Vector Store stack started via ``ScyllaDBOnlineStoreCreator``. +No external cloud cluster is required. + +To run against a ScyllaDB Cloud cluster instead, set: + + SCYLLA_HOSTS="host1,host2" contact points + SCYLLA_KEYSPACE="feast_test" keyspace (default: feast_test) + SCYLLA_USERNAME="scylla" username (optional) + SCYLLA_PASSWORD="..." password (optional) + SCYLLA_LOCAL_DC="..." DC name, e.g. AWS_US_EAST_1 (required) +""" + +import os +import time +from datetime import datetime, timedelta, timezone +from typing import List + +import pytest + +from feast import Entity, FeatureView, RepoConfig +from feast.field import Field +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.online_stores.scylladb_online_store.scylladb import ( + ScyllaDBOnlineStore, + ScyllaDBOnlineStoreConfig, +) +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import FloatList +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.types import Array, Float32, Int64 +from feast.utils import _utc_now +from feast.value_type import ValueType +from tests.universal.feature_repos.universal.online_store.scylladb import ( + ScyllaDBOnlineStoreCreator, +) + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def docker_config() -> RepoConfig: + """Start a local ScyllaDB container and return a RepoConfig for it.""" + creator = ScyllaDBOnlineStoreCreator(project_name="integ_test") + store_dict = creator.create_online_store() + cfg = RepoConfig( + project="integ_test", + provider="local", + online_store=ScyllaDBOnlineStoreConfig(**store_dict), + registry="memory://", + entity_key_serialization_version=3, + ) + yield cfg + creator.teardown() + + +@pytest.fixture(scope="module") +def cloud_config(): + """RepoConfig pointing at a ScyllaDB Cloud cluster (skipped if env vars absent).""" + if not os.environ.get("SCYLLA_HOSTS") or not os.environ.get("SCYLLA_LOCAL_DC"): + pytest.skip("Set SCYLLA_HOSTS and SCYLLA_LOCAL_DC to run vector search tests") + hosts = os.environ["SCYLLA_HOSTS"].split(",") + cfg = RepoConfig( + project="integ_test", + provider="local", + online_store=ScyllaDBOnlineStoreConfig( + hosts=hosts, + keyspace=os.environ.get("SCYLLA_KEYSPACE", "feast_test"), + username=os.environ.get("SCYLLA_USERNAME"), + password=os.environ.get("SCYLLA_PASSWORD"), + local_dc=os.environ["SCYLLA_LOCAL_DC"], + ), + registry="memory://", + entity_key_serialization_version=3, + ) + return cfg + + +def _make_entity_key(val: str) -> EntityKeyProto: + return EntityKeyProto( + join_keys=["item_id"], + entity_values=[ValueProto(string_val=val)], + ) + + +def _make_feature_view( + name: str, with_vector: bool = False, ttl: timedelta = None +) -> FeatureView: + source = FileSource(path="dummy.parquet", timestamp_field="event_timestamp") + schema: List[Field] = [Field(name="score", dtype=Array(Float32))] + if with_vector: + schema.append( + Field( + name="embedding", + dtype=Array(Float32), + tags={ + "vector_index": "true", + "dimensions": "4", + "similarity_function": "COSINE", + }, + ) + ) + return FeatureView( + name=name, + entities=[ + Entity(name="item_id", join_keys=["item_id"], value_type=ValueType.STRING) + ], + schema=schema, + online=True, + source=source, + ttl=ttl, + ) + + +# --------------------------------------------------------------------------- +# Tests — regular online store (uses Docker via docker_config fixture) +# --------------------------------------------------------------------------- + + +def test_write_and_read(docker_config): + store = ScyllaDBOnlineStore() + cfg = docker_config + fv = _make_feature_view("test_write_read") + + store.update(cfg, [], [fv], [], [], partial=False) + try: + ek = _make_entity_key("item_1") + store.online_write_batch( + cfg, + fv, + [ + ( + ek, + {"score": ValueProto(float_list_val=FloatList(val=[0.9]))}, + _utc_now(), + None, + ) + ], + None, + ) + results = store.online_read(cfg, fv, [ek]) + assert len(results) == 1 + ts, feats = results[0] + assert feats is not None + assert "score" in feats + assert list(feats["score"].float_list_val.val) == pytest.approx([0.9]) + finally: + store.teardown(cfg, [fv], []) + + +def test_missing_key_returns_none(docker_config): + store = ScyllaDBOnlineStore() + cfg = docker_config + fv = _make_feature_view("test_missing_key") + + store.update(cfg, [], [fv], [], [], partial=False) + try: + existing = _make_entity_key("present") + missing = _make_entity_key("does_not_exist") + store.online_write_batch( + cfg, + fv, + [ + ( + existing, + {"score": ValueProto(float_list_val=FloatList(val=[0.5]))}, + _utc_now(), + None, + ) + ], + None, + ) + results = store.online_read(cfg, fv, [existing, missing]) + assert len(results) == 2 + assert results[0][1] is not None + assert results[1][1] is None + finally: + store.teardown(cfg, [fv], []) + + +def test_multiple_features_roundtrip(docker_config): + """Multiple features of different types all round-trip with the correct value.""" + store = ScyllaDBOnlineStore() + cfg = docker_config + source = FileSource(path="dummy.parquet", timestamp_field="event_timestamp") + fv = FeatureView( + name="test_multi_features", + entities=[ + Entity(name="item_id", join_keys=["item_id"], value_type=ValueType.STRING) + ], + schema=[ + Field(name="score", dtype=Array(Float32)), + Field(name="priority", dtype=Int64), + ], + online=True, + source=source, + ) + + store.update(cfg, [], [fv], [], [], partial=False) + try: + ek = _make_entity_key("item_mf") + store.online_write_batch( + cfg, + fv, + [ + ( + ek, + { + "score": ValueProto(float_list_val=FloatList(val=[0.85, 0.15])), + "priority": ValueProto(int64_val=42), + }, + _utc_now(), + None, + ) + ], + None, + ) + results = store.online_read(cfg, fv, [ek]) + assert len(results) == 1 + ts, feats = results[0] + assert feats is not None + assert list(feats["score"].float_list_val.val) == pytest.approx([0.85, 0.15]) + assert feats["priority"].int64_val == 42 + finally: + store.teardown(cfg, [fv], []) + + +def test_multiple_entities(docker_config): + """Multiple entity keys can be read in a single online_read call with correct values. + + Mirrors the multi-entity behaviour verified by the universal online store + suite (``test_online_retrieval_with_event_timestamps``) which Cassandra runs. + """ + store = ScyllaDBOnlineStore() + cfg = docker_config + fv = _make_feature_view("test_multi_entities") + + store.update(cfg, [], [fv], [], [], partial=False) + try: + keys = [_make_entity_key(f"entity_{i}") for i in range(3)] + batch = [ + ( + ek, + { + "score": ValueProto( + float_list_val=FloatList(val=[float(i) * 0.1 + 0.1]) + ) + }, + _utc_now(), + None, + ) + for i, ek in enumerate(keys) + ] + store.online_write_batch(cfg, fv, batch, None) + results = store.online_read(cfg, fv, keys) + assert len(results) == 3 + for i, (ts, feats) in enumerate(results): + assert feats is not None, f"Entity {i} returned None features" + assert list(feats["score"].float_list_val.val) == pytest.approx( + [float(i) * 0.1 + 0.1] + ) + finally: + store.teardown(cfg, [fv], []) + + +def test_overwrite_uses_latest_value(docker_config): + """Writing the same entity key twice keeps the most-recently-written value.""" + store = ScyllaDBOnlineStore() + cfg = docker_config + fv = _make_feature_view("test_overwrite") + + store.update(cfg, [], [fv], [], [], partial=False) + try: + ek = _make_entity_key("overwrite_item") + store.online_write_batch( + cfg, + fv, + [ + ( + ek, + {"score": ValueProto(float_list_val=FloatList(val=[0.1]))}, + _utc_now(), + None, + ) + ], + None, + ) + store.online_write_batch( + cfg, + fv, + [ + ( + ek, + {"score": ValueProto(float_list_val=FloatList(val=[0.9]))}, + _utc_now(), + None, + ) + ], + None, + ) + results = store.online_read(cfg, fv, [ek]) + assert len(results) == 1 + ts, feats = results[0] + assert feats is not None + assert list(feats["score"].float_list_val.val) == pytest.approx([0.9]) + finally: + store.teardown(cfg, [fv], []) + + +def test_event_timestamp_returned(docker_config): + """The event timestamp written with a row is returned correctly by online_read. + + Mirrors ``test_online_retrieval_with_event_timestamps`` from the universal + suite which verifies per-entity timestamps for all online store types. + """ + store = ScyllaDBOnlineStore() + cfg = docker_config + fv = _make_feature_view("test_event_ts") + + store.update(cfg, [], [fv], [], [], partial=False) + try: + ek = _make_entity_key("ts_item") + # Use a second-boundary timestamp — CQL ``timestamp`` has ms precision. + write_ts = datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc) + store.online_write_batch( + cfg, + fv, + [ + ( + ek, + {"score": ValueProto(float_list_val=FloatList(val=[0.5]))}, + write_ts, + None, + ) + ], + None, + ) + results = store.online_read(cfg, fv, [ek]) + assert len(results) == 1 + ts, feats = results[0] + assert ts is not None + assert isinstance(ts, datetime) + # Normalise both to UTC before comparing. + ts_utc = ts if ts.tzinfo is not None else ts.replace(tzinfo=timezone.utc) + assert ts_utc == write_ts + finally: + store.teardown(cfg, [fv], []) + + +# --------------------------------------------------------------------------- +# Tests — vector search (local Docker stack via docker_config) +# --------------------------------------------------------------------------- + + +def test_vector_search(docker_config): + store = ScyllaDBOnlineStore() + cfg = docker_config + fv = _make_feature_view("test_vector_search", with_vector=True) + + store.update(cfg, [], [fv], [], [], partial=False) + try: + rows = [ + ("vec_a", [1.0, 0.0, 0.0, 0.0]), + ("vec_b", [0.0, 1.0, 0.0, 0.0]), + ("vec_c", [1.0, 0.1, 0.0, 0.0]), # close to vec_a + ] + batch = [] + for item_id, vec in rows: + ek = _make_entity_key(item_id) + batch.append( + ( + ek, + { + "score": ValueProto(float_list_val=FloatList(val=[0.5])), + "embedding": ValueProto(float_list_val=FloatList(val=vec)), + }, + _utc_now(), + None, + ) + ) + store.online_write_batch(cfg, fv, batch, None) + + # Wait for the vector index to finish building (up to 60 s). + deadline = time.time() + 60 + results = None + while time.time() < deadline: + try: + results = store.retrieve_online_documents_v2( + cfg, + fv, + requested_features=["score", "embedding"], + embedding=[1.0, 0.0, 0.0, 0.0], + top_k=2, + ) + break # index is ready + except Exception as exc: + if "still being constructed" in str(exc) or "missing index" in str(exc): + time.sleep(2) + else: + raise + else: + raise TimeoutError("Vector index was not ready within 60 s") + + assert results is not None + assert len(results) == 2 + + # Extract entity IDs from the returned entity key protos. + # Also verify the shape and types of every field in each result tuple. + # The expected embeddings keyed by entity ID for value verification. + expected_embeddings = { + "vec_a": [1.0, 0.0, 0.0, 0.0], + "vec_b": [0.0, 1.0, 0.0, 0.0], + "vec_c": [1.0, 0.1, 0.0, 0.0], + } + returned_ids = [] + for ts, ek_proto, feats in results: + assert isinstance(ts, datetime), f"Expected datetime, got {type(ts)}" + assert feats is not None + assert "score" in feats + assert "embedding" in feats + # score field: single-element float list written as 0.5 + assert list(feats["score"].float_list_val.val) == pytest.approx([0.5]) + # embedding field: values must match what was written + entity_id = ek_proto.entity_values[0].string_val + assert list(feats["embedding"].float_list_val.val) == pytest.approx( + expected_embeddings[entity_id] + ) + assert ek_proto is not None + returned_ids.append(entity_id) + + # Query is [1,0,0,0]; vec_a=[1,0,0,0] (exact match) and + # vec_c=[1,0.1,0,0] are the two nearest neighbours by cosine similarity. + # vec_b=[0,1,0,0] is orthogonal and must NOT appear in top-2. + assert set(returned_ids) == {"vec_a", "vec_c"}, ( + f"Expected top-2 neighbours to be vec_a and vec_c, got {returned_ids}" + ) + # Exact match must be ranked first. + assert returned_ids[0] == "vec_a", ( + f"Expected vec_a (exact match) to be ranked first, got {returned_ids[0]}" + ) + finally: + store.teardown(cfg, [fv], []) + + +def test_ttl_expiry(docker_config): + """Rows written with a TTL should be gone after ScyllaDB expires them.""" + store = ScyllaDBOnlineStore() + cfg = docker_config + # TTL of 2 seconds, short enough for a test, long enough to write first. + fv = _make_feature_view("test_ttl_expiry", ttl=timedelta(seconds=5)) + + store.update(cfg, [], [fv], [], [], partial=False) + try: + ek = _make_entity_key("ttl_item") + store.online_write_batch( + cfg, + fv, + [ + ( + ek, + {"score": ValueProto(float_list_val=FloatList(val=[0.7]))}, + _utc_now(), + None, + ) + ], + None, + ) + + # Confirm the row is readable immediately after writing. + results = store.online_read(cfg, fv, [ek]) + assert len(results) == 1 + ts, feats = results[0] + assert feats is not None, "Row should be present right after write" + + # Wait for ScyllaDB to expire the row (TTL = 5s, wait 8s to be safe). + time.sleep(8) + + results = store.online_read(cfg, fv, [ek]) + assert len(results) == 1 + ts_after, feats_after = results[0] + assert feats_after is None, ( + f"Row should have expired and return None, but got features: {feats_after}" + ) + finally: + store.teardown(cfg, [fv], []) diff --git a/sdk/python/tests/integration/materialization/test_universal_e2e.py b/sdk/python/tests/integration/online_store/test_universal_e2e.py similarity index 84% rename from sdk/python/tests/integration/materialization/test_universal_e2e.py rename to sdk/python/tests/integration/online_store/test_universal_e2e.py index 202ae859aed..cfacbca59af 100644 --- a/sdk/python/tests/integration/materialization/test_universal_e2e.py +++ b/sdk/python/tests/integration/online_store/test_universal_e2e.py @@ -2,8 +2,8 @@ import pytest -from tests.integration.feature_repos.universal.entities import driver -from tests.integration.feature_repos.universal.feature_views import driver_feature_view +from tests.universal.feature_repos.universal.entities import driver +from tests.universal.feature_repos.universal.feature_views import driver_feature_view from tests.utils.e2e_test_validation import validate_offline_online_store_consistency diff --git a/sdk/python/tests/integration/online_store/test_universal_online.py b/sdk/python/tests/integration/online_store/test_universal_online.py index 51296f31607..41b0e6a635f 100644 --- a/sdk/python/tests/integration/online_store/test_universal_online.py +++ b/sdk/python/tests/integration/online_store/test_universal_online.py @@ -1,3 +1,4 @@ +import asyncio import os import random import time @@ -14,10 +15,11 @@ from feast import FeatureStore from feast.entity import Entity -from feast.errors import FeatureNameCollisionError +from feast.errors import FeatureNameCollisionError, FeatureViewNotFoundException from feast.feature_service import FeatureService from feast.feature_view import FeatureView from feast.field import Field +from feast.filter_models import ComparisonFilter, CompoundFilter from feast.infra.offline_stores.file_source import FileSource from feast.infra.utils.postgres.postgres_config import ConnectionType from feast.online_response import TIMESTAMP_POSTFIX @@ -31,13 +33,14 @@ ValueType, ) from feast.utils import _utc_now +from feast.vector_store_utils import feature_view_to_vs_id from feast.wait import wait_retry_backoff -from tests.integration.feature_repos.repo_configuration import ( +from tests.universal.feature_repos.repo_configuration import ( Environment, construct_universal_feature_views, ) -from tests.integration.feature_repos.universal.entities import driver, item -from tests.integration.feature_repos.universal.feature_views import ( +from tests.universal.feature_repos.universal.entities import driver, item +from tests.universal.feature_repos.universal.feature_views import ( TAGS, create_driver_hourly_stats_feature_view, create_item_embeddings_feature_view, @@ -258,6 +261,9 @@ def test_write_to_online_store(environment, universal_data_sources): "conv_rate": [0.85], "acc_rate": [0.91], "avg_daily_trips": [14], + "driver_metadata": [None], + "driver_config": [None], + "driver_profile": [None], "event_timestamp": [pd.Timestamp(_utc_now()).round("ms")], "created": [pd.Timestamp(_utc_now()).round("ms")], } @@ -435,6 +441,9 @@ def setup_feature_store_universal_feature_views( "conv_rate": [0.5, 0.3], "acc_rate": [0.6, 0.4], "avg_daily_trips": [4, 5], + "driver_metadata": [None, None], + "driver_config": [None, None], + "driver_profile": [None, None], "event_timestamp": [ pd.to_datetime(1646263500, utc=True, unit="s"), pd.to_datetime(1646263600, utc=True, unit="s"), @@ -517,7 +526,7 @@ async def _do_async_retrieval_test(environment, universal_data_sources): @pytest.mark.asyncio @pytest.mark.integration -@pytest.mark.universal_online_stores(only=["redis", "postgres"]) +@pytest.mark.universal_online_stores(only=["redis", "postgres", "mongodb"]) async def test_async_online_retrieval_with_event_timestamps( environment, universal_data_sources ): @@ -526,10 +535,19 @@ async def test_async_online_retrieval_with_event_timestamps( @pytest.mark.asyncio @pytest.mark.integration -@pytest.mark.universal_online_stores(only=["dynamodb"]) +@pytest.mark.universal_online_stores async def test_async_online_retrieval_with_event_timestamps_dynamo( - environment, universal_data_sources + dynamodb_local_environment, ): + """Async online retrieval for DynamoDB with a credential-isolated environment. + + Uses ``dynamodb_local_environment`` (its own DynamoDB Local container + + FileDataSourceCreator) so that dummy credentials are set before any boto + client is created. This avoids the expired-STS-token problem that + occurs when aiobotocore lazily resolves credentials from the shared + environment in CI. + """ + environment, universal_data_sources = dynamodb_local_environment await _do_async_retrieval_test(environment, universal_data_sources) @@ -915,13 +933,27 @@ def test_retrieve_online_milvus_documents(environment, fake_document_data): df, data_source = fake_document_data item_embeddings_feature_view = create_item_embeddings_feature_view(data_source) fs.apply([item_embeddings_feature_view, item()]) + + features = [ + "item_embeddings:embedding_float", + "item_embeddings:item_id", + "item_embeddings:string_feature", + ] + + # Empty-store query: collection exists but has no rows yet. + empty = fs.retrieve_online_documents_v2( + features=features, + query=[1.0, 2.0], + top_k=2, + distance_metric="L2", + ).to_dict() + assert len(empty["embedding_float"]) == 0 + assert len(empty["item_id"]) == 0 + fs.write_to_online_store("item_embeddings", df) + documents = fs.retrieve_online_documents_v2( - features=[ - "item_embeddings:embedding_float", - "item_embeddings:item_id", - "item_embeddings:string_feature", - ], + features=features, query=[1.0, 2.0], top_k=2, distance_metric="L2", @@ -942,6 +974,50 @@ def test_retrieve_online_milvus_documents(environment, fake_document_data): f"Integration test: embedding {i} has {len(embedding)} dimensions, expected {query_dim}" ) + # Oversized top_k: dataset has 3 rows, request 5 -> expect 3 back. + all_docs = fs.retrieve_online_documents_v2( + features=features, + query=[1.0, 2.0], + top_k=5, + distance_metric="L2", + ).to_dict() + assert len(all_docs["embedding_float"]) == 3 + assert sorted(all_docs["item_id"]) == [1, 2, 3] + + # Cosine-metric variant: separate FV so the Milvus collection is created + # with COSINE as its index metric. + cosine_fv = FeatureView( + name="item_embeddings_cosine", + entities=[item()], + schema=[ + Field( + name="embedding_float", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="string_feature", dtype=String), + Field(name="float_feature", dtype=Float32), + ], + source=data_source, + ttl=timedelta(hours=2), + ) + fs.apply([cosine_fv]) + fs.write_to_online_store("item_embeddings_cosine", df) + + cosine_docs = fs.retrieve_online_documents_v2( + features=[ + "item_embeddings_cosine:embedding_float", + "item_embeddings_cosine:item_id", + "item_embeddings_cosine:string_feature", + ], + query=[1.0, 2.0], + top_k=2, + distance_metric="COSINE", + ).to_dict() + assert len(cosine_docs["embedding_float"]) == 2 + assert len(cosine_docs["item_id"]) == 2 + @pytest.mark.integration @pytest.mark.universal_online_stores(only=["milvus"]) @@ -1153,6 +1229,12 @@ def test_retrieve_online_documents_v2(environment, fake_document_data): assert len(vector_results["text_field"]) == 5 assert len(vector_results["category"]) == 5 + assert all(isinstance(v, list) for v in vector_results["embedding"]) + assert all(isinstance(v, float) for v in vector_results["distance"]) + assert all(isinstance(v, str) for v in vector_results["text_field"]) + assert all(isinstance(v, str) for v in vector_results["category"]) + assert all(isinstance(v, int) for v in vector_results["item_id"]) + # Test 2: Vector similarity search with Cosine distance vector_results = fs.retrieve_online_documents_v2( features=[ @@ -1171,6 +1253,12 @@ def test_retrieve_online_documents_v2(environment, fake_document_data): assert len(vector_results["text_field"]) == 5 assert len(vector_results["category"]) == 5 + assert all(isinstance(v, list) for v in vector_results["embedding"]) + assert all(isinstance(v, float) for v in vector_results["distance"]) + assert all(isinstance(v, str) for v in vector_results["text_field"]) + assert all(isinstance(v, str) for v in vector_results["category"]) + assert all(isinstance(v, int) for v in vector_results["item_id"]) + # Test 3: Full text search text_results = fs.retrieve_online_documents_v2( features=[ @@ -1189,6 +1277,11 @@ def test_retrieve_online_documents_v2(environment, fake_document_data): assert len(text_results["category"]) == 5 assert len(text_results["item_id"]) == 5 + assert all(isinstance(v, str) for v in text_results["text_field"]) + assert all(isinstance(v, float) for v in text_results["text_rank"]) + assert all(isinstance(v, str) for v in text_results["category"]) + assert all(isinstance(v, int) for v in text_results["item_id"]) + # Verify text rank values are between 0 and 1 assert all(0 <= rank <= 1 for rank in text_results["text_rank"]) @@ -1218,37 +1311,329 @@ def test_retrieve_online_documents_v2(environment, fake_document_data): assert len(hybrid_results["category"]) == 5 assert len(hybrid_results["item_id"]) == 5 - # Test 5: Hybrid search with different text query - hybrid_results = fs.retrieve_online_documents_v2( + assert all(isinstance(v, list) for v in hybrid_results["embedding"]) + assert all(isinstance(v, float) for v in hybrid_results["distance"]) + assert all(isinstance(v, str) for v in hybrid_results["text_field"]) + assert all(isinstance(v, float) for v in hybrid_results["text_rank"]) + assert all(isinstance(v, str) for v in hybrid_results["category"]) + assert all(isinstance(v, int) for v in hybrid_results["item_id"]) + + # Test 6: Full text search with no matches + no_match_results = fs.retrieve_online_documents_v2( features=[ "item_embeddings:embedding", "item_embeddings:text_field", "item_embeddings:category", "item_embeddings:item_id", ], - query=query_embedding, - query_string="Category-1", + query_string="nonexistent keyword", top_k=5, + ).to_dict() + + # Verify no results are returned for non-matching query + assert "text_field" in no_match_results + assert len(no_match_results["text_field"]) == 0 + assert "text_rank" in no_match_results + assert len(no_match_results["text_rank"]) == 0 + + +def _setup_documents_with_categories(fs): + """Shared helper that creates and populates a feature view with embeddings, + text, and category fields. Returns (feature_view, entity, dataframe).""" + n_rows = 20 + vector_dim = 2 + random.seed(42) + + df = pd.DataFrame( + { + "item_id": list(range(n_rows)), + "chunk_id": list(range(n_rows)), + "embedding": [list(np.random.random(vector_dim)) for _ in range(n_rows)], + "text_field": [ + f"Document text content {i} with searchable keywords" + for i in range(n_rows) + ], + "category": [f"Category-{i % 5}" for i in range(n_rows)], + "event_timestamp": [datetime.now() for _ in range(n_rows)], + } + ) + + data_source = FileSource( + path="dummy_path.parquet", timestamp_field="event_timestamp" + ) + + item_entity = Entity( + name="item_id", + join_keys=["item_id"], + value_type=ValueType.INT64, + ) + + item_embeddings_fv = FeatureView( + name="item_embeddings", + entities=[item_entity], + schema=[ + Field(name="embedding", dtype=Array(Float32), vector_index=True), + Field(name="text_field", dtype=String), + Field(name="category", dtype=String), + Field(name="item_id", dtype=Int64), + Field(name="chunk_id", dtype=Int64), + ], + source=data_source, + ) + + fs.apply([item_embeddings_fv, item_entity]) + fs.write_to_online_store("item_embeddings", df) + return item_embeddings_fv, item_entity, df + + +@pytest.mark.integration +@pytest.mark.universal_online_stores(only=["pgvector", "elasticsearch"]) +def test_retrieve_online_documents_v2_with_filters(environment, fake_document_data): + """Test that metadata filters narrow down vector/text search results.""" + fs = environment.feature_store + fs.config.online_store.vector_enabled = True + + _, _, df = _setup_documents_with_categories(fs) + vector_dim = 2 + query_embedding = list(np.random.random(vector_dim)) + + # --- eq filter: only Category-0 rows --- + eq_filter = ComparisonFilter(type="eq", key="category", value="Category-0") + results = fs.retrieve_online_documents_v2( + features=[ + "item_embeddings:embedding", + "item_embeddings:text_field", + "item_embeddings:category", + "item_embeddings:item_id", + ], + query=query_embedding, + top_k=10, distance_metric="L2", + filters=eq_filter, ).to_dict() - # Verify results contain only documents from Category-1 - assert all(cat == "Category-1" for cat in hybrid_results["category"]) + assert len(results["category"]) > 0 + assert len(results["category"]) <= 4 # 20 rows / 5 categories + assert all(c == "Category-0" for c in results["category"]) - # Test 6: Full text search with no matches - no_match_results = fs.retrieve_online_documents_v2( + # --- ne filter: exclude Category-0 --- + ne_filter = ComparisonFilter(type="ne", key="category", value="Category-0") + results = fs.retrieve_online_documents_v2( features=[ "item_embeddings:embedding", "item_embeddings:text_field", "item_embeddings:category", "item_embeddings:item_id", ], - query_string="nonexistent keyword", - top_k=5, + query=query_embedding, + top_k=10, + distance_metric="L2", + filters=ne_filter, ).to_dict() - # Verify no results are returned for non-matching query - assert "text_field" in no_match_results - assert len(no_match_results["text_field"]) == 0 - assert "text_rank" in no_match_results - assert len(no_match_results["text_rank"]) == 0 + assert len(results["category"]) > 0 + assert all(c != "Category-0" for c in results["category"]) + + # --- in filter: Category-0 or Category-1 --- + in_filter = ComparisonFilter( + type="in", key="category", value=["Category-0", "Category-1"] + ) + results = fs.retrieve_online_documents_v2( + features=[ + "item_embeddings:embedding", + "item_embeddings:text_field", + "item_embeddings:category", + "item_embeddings:item_id", + ], + query=query_embedding, + top_k=10, + distance_metric="L2", + filters=in_filter, + ).to_dict() + + assert len(results["category"]) > 0 + assert all(c in ("Category-0", "Category-1") for c in results["category"]) + + # --- compound AND filter: category == Category-0 AND chunk_id >= 5 --- + and_filter = CompoundFilter( + type="and", + filters=[ + ComparisonFilter(type="eq", key="category", value="Category-0"), + ComparisonFilter(type="gte", key="chunk_id", value=5), + ], + ) + results = fs.retrieve_online_documents_v2( + features=[ + "item_embeddings:embedding", + "item_embeddings:text_field", + "item_embeddings:category", + "item_embeddings:chunk_id", + ], + query=query_embedding, + top_k=10, + distance_metric="L2", + filters=and_filter, + ).to_dict() + + assert len(results["category"]) > 0 + assert all(c == "Category-0" for c in results["category"]) + assert all(i >= 5 for i in results["chunk_id"]) + + # --- text search + filter --- + text_filter = ComparisonFilter(type="eq", key="category", value="Category-2") + text_results = fs.retrieve_online_documents_v2( + features=[ + "item_embeddings:embedding", + "item_embeddings:text_field", + "item_embeddings:category", + "item_embeddings:item_id", + ], + query_string="searchable keywords", + top_k=10, + filters=text_filter, + ).to_dict() + + assert len(text_results["category"]) > 0 + assert all(c == "Category-2" for c in text_results["category"]) + + # --- filter with no matches --- + empty_filter = ComparisonFilter( + type="eq", key="category", value="NonexistentCategory" + ) + empty_results = fs.retrieve_online_documents_v2( + features=[ + "item_embeddings:embedding", + "item_embeddings:text_field", + "item_embeddings:category", + "item_embeddings:item_id", + ], + query=query_embedding, + top_k=10, + distance_metric="L2", + filters=empty_filter, + ).to_dict() + + assert len(empty_results.get("category", [])) == 0 + + +@pytest.mark.integration +@pytest.mark.universal_online_stores(only=["pgvector", "elasticsearch"]) +def test_openai_search(environment, fake_document_data): + """Test OpenAI-compatible vector store search returns the correct response shape.""" + fs = environment.feature_store + fs.config.online_store.vector_enabled = True + + fv, _, df = _setup_documents_with_categories(fs) + vector_dim = 2 + vs_id = feature_view_to_vs_id(fs.project, "item_embeddings") + + fake_embedding = list(np.random.random(vector_dim)) + + mock_provider = unittest.mock.MagicMock() + mock_provider.aembed = unittest.mock.AsyncMock(return_value=[fake_embedding]) + fs.embedding_provider = mock_provider + + result = asyncio.run( + fs.openai_search( + vector_store_id="item_embeddings", + query="test query", + max_num_results=5, + ) + ) + + # Validate top-level OpenAI response shape + assert result["object"] == "vector_store.search_results.page" + assert isinstance(result["search_query"], list) + assert result["search_query"] == ["test query"] + assert result["has_more"] is False + assert result["next_page"] is None + + assert isinstance(result["data"], list) + assert len(result["data"]) > 0 + assert len(result["data"]) <= 5 + + seen_file_ids = set() + for item_result in result["data"]: + assert "file_id" in item_result + fid = item_result["file_id"] + assert fid.startswith(f"{vs_id}_") + seen_file_ids.add(fid) + assert "filename" in item_result + assert item_result["filename"] == vs_id + assert "score" in item_result + assert isinstance(item_result["score"], float) + assert "attributes" in item_result + assert isinstance(item_result["attributes"], dict) + assert "item_id" not in item_result["attributes"] + assert "content" in item_result + assert isinstance(item_result["content"], list) + for part in item_result["content"]: + assert "type" in part + assert part["type"] == "text" + assert "text" in part + assert len(seen_file_ids) == len(result["data"]) + + # --- Test with features_to_retrieve --- + result_subset = asyncio.run( + fs.openai_search( + vector_store_id="item_embeddings", + query="test query", + max_num_results=5, + features_to_retrieve=["text_field", "category"], + ) + ) + + assert len(result_subset["data"]) > 0 + for item_result in result_subset["data"]: + attr_keys = set(item_result["attributes"].keys()) + assert "embedding" not in attr_keys + + # --- Test with list query --- + result_list = asyncio.run( + fs.openai_search( + vector_store_id="item_embeddings", + query=["term1", "term2"], + max_num_results=5, + ) + ) + + assert result_list["search_query"] == ["term1", "term2"] + + +@pytest.mark.integration +@pytest.mark.universal_online_stores(only=["pgvector", "elasticsearch"]) +def test_openai_search_no_embedding_config(environment, fake_document_data): + """Test that openai_search raises ValueError + when no embedding provider is set and embedding_model is not configured.""" + fs = environment.feature_store + fs.config.online_store.vector_enabled = True + _setup_documents_with_categories(fs) + fs.config.embedding_model = None + fs._embedding_provider = None + + with pytest.raises(ValueError, match="No embedding provider set"): + asyncio.run( + fs.openai_search( + vector_store_id="item_embeddings", + query="test query", + ) + ) + + +@pytest.mark.integration +@pytest.mark.universal_online_stores(only=["pgvector", "elasticsearch"]) +def test_openai_search_not_found(environment, fake_document_data): + """Test that openai_search raises FeatureViewNotFoundException + for a non-existent feature view.""" + fs = environment.feature_store + mock_provider = unittest.mock.MagicMock() + mock_provider.aembed = unittest.mock.AsyncMock(return_value=[[0.0, 0.0]]) + fs.embedding_provider = mock_provider + + with pytest.raises(FeatureViewNotFoundException): + asyncio.run( + fs.openai_search( + vector_store_id="nonexistent_feature_view", + query="test query", + ) + ) diff --git a/sdk/python/tests/integration/online_store/test_universal_online_types.py b/sdk/python/tests/integration/online_store/test_universal_online_types.py new file mode 100644 index 00000000000..7340d00f274 --- /dev/null +++ b/sdk/python/tests/integration/online_store/test_universal_online_types.py @@ -0,0 +1,77 @@ +from datetime import datetime, timedelta +from typing import List + +import pytest + +from tests.universal.feature_repos.universal.entities import driver +from tests.universal.feature_repos.universal.feature_views import driver_feature_view +from tests.utils.type_test_utils import ( + TypeTestConfig, + get_feast_type, + get_type_test_fixtures, + populate_test_configs, +) + +ONLINE_TYPE_TEST_CONFIGS: List[TypeTestConfig] = populate_test_configs() + + +@pytest.fixture( + params=ONLINE_TYPE_TEST_CONFIGS, + ids=[str(c) for c in ONLINE_TYPE_TEST_CONFIGS], +) +def online_types_test_fixtures(request, environment): + return get_type_test_fixtures(request, environment) + + +@pytest.mark.integration +@pytest.mark.universal_online_stores(only=["sqlite"]) +def test_feature_get_online_features_types_match( + online_types_test_fixtures, environment +): + config, data_source, fv = online_types_test_fixtures + entity = driver() + fv = driver_feature_view( + data_source=data_source, + name="get_online_features_types_match", + dtype=get_feast_type(config.feature_dtype, config.feature_is_list), + ) + fs = environment.feature_store + features = [fv.name + ":value"] + fs.apply([fv, entity]) + fs.materialize( + environment.start_date, + environment.end_date + - timedelta(hours=1), # throwing out last record to make sure + # we can successfully infer type even from all empty values + ) + + online_features = fs.get_online_features( + features=features, + entity_rows=[{"driver_id": 1}], + ).to_dict() + + feature_list_dtype_to_expected_online_response_value_type = { + "int32": int, + "int64": int, + "float": float, + "string": str, + "bool": bool, + "datetime": datetime, + } + expected_dtype = feature_list_dtype_to_expected_online_response_value_type[ + config.feature_dtype + ] + + assert len(online_features["value"]) == 1 + + if config.feature_is_list: + for feature in online_features["value"]: + assert isinstance(feature, list), "Feature value should be a list" + assert config.has_empty_list or len(feature) > 0, ( + "List of values should not be empty" + ) + for element in feature: + assert isinstance(element, expected_dtype) + else: + for feature in online_features["value"]: + assert isinstance(feature, expected_dtype) diff --git a/sdk/python/tests/integration/permissions/__init__.py b/sdk/python/tests/integration/permissions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/integration/permissions/auth/__init__.py b/sdk/python/tests/integration/permissions/auth/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/integration/permissions/auth/conftest.py b/sdk/python/tests/integration/permissions/auth/conftest.py new file mode 100644 index 00000000000..81b4faa7af0 --- /dev/null +++ b/sdk/python/tests/integration/permissions/auth/conftest.py @@ -0,0 +1,30 @@ +import pytest + +from tests.unit.permissions.auth.server.test_utils import ( + invalid_list_entities_perm, + read_entities_perm, + read_fv_perm, + read_odfv_perm, + read_permissions_perm, + read_projects_perm, + read_sfv_perm, +) + + +@pytest.fixture( + scope="module", + params=[ + [], + [invalid_list_entities_perm], + [ + read_entities_perm, + read_permissions_perm, + read_fv_perm, + read_odfv_perm, + read_sfv_perm, + read_projects_perm, + ], + ], +) +def applied_permissions(request): + return request.param diff --git a/sdk/python/tests/integration/permissions/auth/server/__init__.py b/sdk/python/tests/integration/permissions/auth/server/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py b/sdk/python/tests/integration/permissions/auth/server/test_auth_registry_server.py similarity index 87% rename from sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py rename to sdk/python/tests/integration/permissions/auth/server/test_auth_registry_server.py index 71a883e1933..40b506cd4db 100644 --- a/sdk/python/tests/unit/permissions/auth/server/test_auth_registry_server.py +++ b/sdk/python/tests/integration/permissions/auth/server/test_auth_registry_server.py @@ -11,7 +11,9 @@ FeastPermissionError, FeatureViewNotFoundException, ) +from feast.permissions.auth.auth_manager import AllowAll, set_auth_manager from feast.permissions.permission import Permission +from feast.permissions.security_manager import no_security_manager from feast.registry_server import start_server from feast.wait import wait_retry_backoff # noqa: E402 from tests.unit.permissions.auth.server import mock_utils @@ -27,6 +29,8 @@ from tests.utils.auth_permissions_util import get_remote_registry_store from tests.utils.http_server import check_port_open # noqa: E402 +pytestmark = [pytest.mark.integration, pytest.mark.rbac_remote_integration_test] + @pytest.fixture def start_registry_server( @@ -71,7 +75,11 @@ def start_registry_server( yield server print("Stopping server") - server.stop(grace=None) # Teardown server + try: + server.stop(grace=None) # Teardown server + finally: + no_security_manager() + set_auth_manager(AllowAll()) @pytest.mark.parametrize( @@ -142,6 +150,11 @@ def _test_get_historical_features(client_fs: FeatureStore): def _test_get_entity(client_fs: FeatureStore, permissions: list[Permission]): + if _is_auth_enabled(client_fs) and len(permissions) == 0: + with pytest.raises(FeastPermissionError): + client_fs.get_entity("driver") + return + if not _is_auth_enabled(client_fs) or _is_permission_enabled( client_fs, permissions, read_entities_perm ): @@ -156,6 +169,18 @@ def _test_get_entity(client_fs: FeatureStore, permissions: list[Permission]): def _test_list_entities(client_fs: FeatureStore, permissions: list[Permission]): + if _is_auth_enabled(client_fs) and len(permissions) == 0: + with pytest.raises(FeastPermissionError): + client_fs.list_entities() + return + + if _is_auth_enabled(client_fs) and _permissions_exist_in_permission_list( + [invalid_list_entities_perm], permissions + ): + with pytest.raises(FeastPermissionError): + client_fs.list_entities() + return + entities = client_fs.list_entities() if not _is_auth_enabled(client_fs) or _is_permission_enabled( @@ -183,6 +208,10 @@ def _test_list_permissions( with pytest.raises(Exception): client_fs.list_permissions() return [] + elif _is_auth_enabled(client_fs) and len(applied_permissions) == 0: + with pytest.raises(FeastPermissionError): + client_fs.list_permissions() + return [] else: permissions = client_fs.list_permissions() @@ -229,6 +258,11 @@ def _is_auth_enabled(client_fs: FeatureStore) -> bool: def _test_get_fv(client_fs: FeatureStore, permissions: list[Permission]): + if _is_auth_enabled(client_fs) and len(permissions) == 0: + with pytest.raises(FeastPermissionError): + client_fs.get_feature_view("driver_hourly_stats") + return + if not _is_auth_enabled(client_fs) or _is_permission_enabled( client_fs, permissions, read_fv_perm ): @@ -249,6 +283,10 @@ def _test_list_fvs(client_fs: FeatureStore, permissions: list[Permission]): with pytest.raises(Exception): client_fs.list_feature_views() return [] + elif _is_auth_enabled(client_fs) and len(permissions) == 0: + with pytest.raises(FeastPermissionError): + client_fs.list_feature_views() + return [] else: fvs = client_fs.list_feature_views() for fv in fvs: diff --git a/sdk/python/tests/integration/registration/test_feature_store.py b/sdk/python/tests/integration/registration/test_feature_store.py index b59af900190..93334fbda19 100644 --- a/sdk/python/tests/integration/registration/test_feature_store.py +++ b/sdk/python/tests/integration/registration/test_feature_store.py @@ -14,15 +14,23 @@ from datetime import timedelta from tempfile import mkstemp +import pandas as pd import pytest from pytest_lazyfixture import lazy_fixture +from feast import FileSource +from feast.data_format import AvroFormat +from feast.data_source import KafkaSource from feast.entity import Entity -from feast.feature_store import FeatureStore +from feast.errors import ConflictingFeatureViewNames +from feast.feature_store import FeatureStore, _validate_feature_views from feast.feature_view import FeatureView +from feast.field import Field from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.on_demand_feature_view import on_demand_feature_view from feast.repo_config import RepoConfig -from feast.types import Float64, Int64, String +from feast.stream_feature_view import StreamFeatureView +from feast.types import Float32, Float64, Int64, String from tests.utils.data_source_test_creator import prep_file_source @@ -75,3 +83,146 @@ def feature_store_with_local_registry(): entity_key_serialization_version=3, ) ) + + +@pytest.mark.integration +def test_validate_feature_views_cross_type_conflict(): + """ + Test that _validate_feature_views() catches cross-type name conflicts. + + This is a unit test for the validation that happens during feast plan/apply. + The validation must catch conflicts across FeatureView, StreamFeatureView, + and OnDemandFeatureView to prevent silent data correctness bugs in + get_online_features (which uses fixed-order lookup). + + See: https://github.com/feast-dev/feast/issues/5995 + """ + # Create a simple entity + entity = Entity(name="driver_entity", join_keys=["test_key"]) + + # Create a regular FeatureView + file_source = FileSource(name="my_file_source", path="test.parquet") + feature_view = FeatureView( + name="my_feature_view", + entities=[entity], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + ) + + # Create a StreamFeatureView with the SAME name + stream_source = KafkaSource( + name="kafka", + timestamp_field="event_timestamp", + kafka_bootstrap_servers="", + message_format=AvroFormat(""), + topic="topic", + batch_source=file_source, + watermark_delay_threshold=timedelta(days=1), + ) + stream_feature_view = StreamFeatureView( + name="my_feature_view", # Same name as FeatureView! + entities=[entity], + ttl=timedelta(days=30), + schema=[Field(name="feature1", dtype=Float32)], + source=stream_source, + ) + + # Validate should raise ConflictingFeatureViewNames + with pytest.raises(ConflictingFeatureViewNames) as exc_info: + _validate_feature_views([feature_view, stream_feature_view]) + + # Verify error message contains type information + error_message = str(exc_info.value) + assert "my_feature_view" in error_message + assert "FeatureView" in error_message + assert "StreamFeatureView" in error_message + + +def test_validate_feature_views_same_type_conflict(): + """ + Test that _validate_feature_views() also catches same-type name conflicts + with a proper error message indicating duplicate FeatureViews. + """ + # Create a simple entity + entity = Entity(name="driver_entity", join_keys=["test_key"]) + + # Create two FeatureViews with the same name + file_source = FileSource(name="my_file_source", path="test.parquet") + fv1 = FeatureView( + name="duplicate_fv", + entities=[entity], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + ) + fv2 = FeatureView( + name="duplicate_fv", # Same name! + entities=[entity], + schema=[Field(name="feature2", dtype=Float32)], + source=file_source, + ) + + # Validate should raise ConflictingFeatureViewNames + with pytest.raises(ConflictingFeatureViewNames) as exc_info: + _validate_feature_views([fv1, fv2]) + + # Verify error message indicates same-type duplicate + error_message = str(exc_info.value) + assert "duplicate_fv" in error_message + assert "Multiple FeatureViews" in error_message + assert "case-insensitively unique" in error_message + + +def test_validate_feature_views_case_insensitive(): + """ + Test that _validate_feature_views() catches case-insensitive conflicts. + """ + entity = Entity(name="driver_entity", join_keys=["test_key"]) + file_source = FileSource(name="my_file_source", path="test.parquet") + + fv1 = FeatureView( + name="MyFeatureView", + entities=[entity], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + ) + fv2 = FeatureView( + name="myfeatureview", # Same name, different case! + entities=[entity], + schema=[Field(name="feature2", dtype=Float32)], + source=file_source, + ) + + # Validate should raise ConflictingFeatureViewNames (case-insensitive) + with pytest.raises(ConflictingFeatureViewNames): + _validate_feature_views([fv1, fv2]) + + +def test_validate_feature_views_odfv_conflict(): + """ + Test that _validate_feature_views() catches OnDemandFeatureView name conflicts. + """ + entity = Entity(name="driver_entity", join_keys=["test_key"]) + file_source = FileSource(name="my_file_source", path="test.parquet") + + fv = FeatureView( + name="shared_name", + entities=[entity], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + ) + + @on_demand_feature_view( + sources=[fv], + schema=[Field(name="output", dtype=Float32)], + ) + def shared_name(inputs: pd.DataFrame) -> pd.DataFrame: + return pd.DataFrame({"output": inputs["feature1"] * 2}) + + # Validate should raise ConflictingFeatureViewNames + with pytest.raises(ConflictingFeatureViewNames) as exc_info: + _validate_feature_views([fv, shared_name]) + + error_message = str(exc_info.value) + assert "shared_name" in error_message + assert "FeatureView" in error_message + assert "OnDemandFeatureView" in error_message diff --git a/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py b/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py index 151f629289f..e1b936a3042 100644 --- a/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py +++ b/sdk/python/tests/integration/registration/test_universal_odfv_feature_inference.py @@ -6,8 +6,8 @@ from feast.infra.offline_stores.file_source import FileSource from feast.types import Float64 from feast.utils import _utc_now -from tests.integration.feature_repos.universal.entities import customer, driver, item -from tests.integration.feature_repos.universal.feature_views import ( +from tests.universal.feature_repos.universal.entities import customer, driver, item +from tests.universal.feature_repos.universal.feature_views import ( conv_rate_plus_100_feature_view, create_conv_rate_request_source, create_driver_hourly_stats_batch_feature_view, @@ -18,7 +18,6 @@ @pytest.mark.integration -@pytest.mark.universal_offline_stores @pytest.mark.parametrize("infer_features", [True, False], ids=lambda v: str(v)) def test_infer_odfv_features(environment, universal_data_sources, infer_features): store = environment.feature_store diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index 29b31ef1b75..ead74ca8a2b 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -16,7 +16,7 @@ import random import string import time -from datetime import timedelta, timezone +from datetime import datetime, timedelta, timezone from tempfile import mkstemp from unittest import mock @@ -35,7 +35,7 @@ from feast.data_format import AvroFormat, ParquetFormat from feast.data_source import KafkaSource from feast.entity import Entity -from feast.errors import FeatureViewNotFoundException +from feast.errors import ConflictingFeatureViewNames, FeatureViewNotFoundException from feast.feature_view import FeatureView from feast.field import Field from feast.infra.infra_object import Infra @@ -43,6 +43,7 @@ from feast.infra.registry.base_registry import BaseRegistry from feast.infra.registry.registry import Registry from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig +from feast.infra.registry.snowflake import SnowflakeRegistry, SnowflakeRegistryConfig from feast.infra.registry.sql import SqlRegistry, SqlRegistryConfig from feast.on_demand_feature_view import on_demand_feature_view from feast.permissions.action import AuthzedAction @@ -56,7 +57,7 @@ from feast.types import Array, Bytes, Float32, Int32, Int64, String from feast.utils import _utc_now from feast.value_type import ValueType -from tests.integration.feature_repos.universal.entities import driver +from tests.universal.feature_repos.universal.entities import driver @pytest.fixture @@ -272,16 +273,51 @@ def mysql_registry_async(mysql_server): yield SqlRegistry(registry_config, "project", None) -@pytest.fixture(scope="session") -def sqlite_registry(): +@pytest.fixture(scope="function") +def sqlite_registry(tmp_path): registry_config = SqlRegistryConfig( registry_type="sql", - path="sqlite://", + path=f"sqlite:///{tmp_path / 'registry.db'}", cache_ttl_seconds=2, cache_mode="sync", ) - yield SqlRegistry(registry_config, "project", None) + registry = SqlRegistry(registry_config, "project", None) + yield registry + registry.teardown() + + +@pytest.fixture(scope="function") +def snowflake_registry(): + account = os.getenv("SNOWFLAKE_CI_DEPLOYMENT", "") + if not account: + pytest.skip("SNOWFLAKE_CI_DEPLOYMENT not set") + + config_kwargs = dict( + registry_type="snowflake.registry", + account=account, + user=os.getenv("SNOWFLAKE_CI_USER", ""), + role=os.getenv("SNOWFLAKE_CI_ROLE", ""), + warehouse=os.getenv("SNOWFLAKE_CI_WAREHOUSE", ""), + database=os.getenv("SNOWFLAKE_CI_DATABASE", "FEAST"), + schema=os.getenv("SNOWFLAKE_CI_SCHEMA", "REGISTRY_TEST"), + cache_ttl_seconds=2, + purge_feast_metadata=False, + ) + + private_key = os.getenv("SNOWFLAKE_CI_PRIVATE_KEY_PATH", "") + if private_key: + config_kwargs["private_key"] = private_key + passphrase = os.getenv("SNOWFLAKE_CI_PRIVATE_KEY_PASSPHRASE", "") + if passphrase: + config_kwargs["private_key_passphrase"] = passphrase + else: + config_kwargs["password"] = os.getenv("SNOWFLAKE_CI_PASSWORD", "") + + registry_config = SnowflakeRegistryConfig(**config_kwargs) + registry = SnowflakeRegistry(registry_config, "project", None) + yield registry + registry.teardown() @pytest.fixture(scope="function") @@ -386,33 +422,45 @@ def mock_remote_registry(): yield registry -if os.getenv("FEAST_IS_LOCAL_TEST", "False") == "False": - all_fixtures = [lazy_fixture("s3_registry"), lazy_fixture("gcs_registry")] +all_fixtures = [ + lazy_fixture("local_registry"), + pytest.param( + lazy_fixture("pg_registry"), + marks=pytest.mark.xdist_group(name="pg_registry"), + ), + pytest.param( + lazy_fixture("mysql_registry"), + marks=pytest.mark.xdist_group(name="mysql_registry"), + ), + lazy_fixture("sqlite_registry"), + pytest.param( + lazy_fixture("mock_remote_registry"), + marks=pytest.mark.rbac_remote_integration_test, + ), +] + +if os.getenv("FEAST_IS_LOCAL_TEST", "False") != "True": + all_fixtures.extend( + [ + lazy_fixture("s3_registry"), + lazy_fixture("gcs_registry"), + pytest.param( + lazy_fixture("hdfs_registry"), + marks=pytest.mark.xdist_group(name="hdfs_registry"), + ), + pytest.param( + lazy_fixture("snowflake_registry"), + marks=pytest.mark.xdist_group(name="snowflake_registry"), + ), + ] + ) else: - all_fixtures = [ - lazy_fixture("local_registry"), + all_fixtures.append( pytest.param( lazy_fixture("minio_registry"), marks=pytest.mark.xdist_group(name="minio_registry"), - ), - pytest.param( - lazy_fixture("pg_registry"), - marks=pytest.mark.xdist_group(name="pg_registry"), - ), - pytest.param( - lazy_fixture("mysql_registry"), - marks=pytest.mark.xdist_group(name="mysql_registry"), - ), - lazy_fixture("sqlite_registry"), - pytest.param( - lazy_fixture("mock_remote_registry"), - marks=pytest.mark.rbac_remote_integration_test, - ), - pytest.param( - lazy_fixture("hdfs_registry"), - marks=pytest.mark.xdist_group(name="hdfs_registry"), - ), - ] + ) + ) sql_fixtures = [ pytest.param( @@ -611,6 +659,188 @@ def test_apply_feature_view_success(test_registry: BaseRegistry): test_registry.teardown() +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + [ + pytest.param( + lazy_fixture("mysql_registry"), + marks=pytest.mark.xdist_group(name="mysql_registry"), + ), + pytest.param( + lazy_fixture("mysql_registry_async"), + marks=pytest.mark.xdist_group(name="mysql_registry"), + ), + ], +) +def test_apply_feature_view_large_proto_roundtrip_mysql(test_registry: BaseRegistry): + """Regression for the MySQL BLOB 64 KB truncation bug. + + The SQL registry stores each object as a serialized proto in a binary + column. With those columns typed BLOB (64 KB cap) on MySQL/MariaDB, a + FeatureView proto larger than 64 KB is silently truncated on write and + later fails to deserialize. The LONGBLOB fix must let a >64 KB proto + round-trip through a live MySQL connection intact. + + Reads use allow_cache=False (the default) so the assertion exercises the + DB column, not the in-memory cache populated at apply time. + """ + batch_source = FileSource( + file_format=ParquetFormat(), + path="file://feast/*", + timestamp_field="ts_col", + created_timestamp_column="timestamp", + ) + entity = Entity(name="large_proto_entity", join_keys=["test"]) + # A ~200 KB tag value pushes the serialized FeatureView proto well past the + # 64 KB BLOB cap, so a truncating column would corrupt it. + large_value = "x" * (200 * 1024) + fv = FeatureView( + name="large_proto_feature_view", + schema=[Field(name="test", dtype=Int64)], + entities=[entity], + tags={"big": large_value}, + source=batch_source, + ttl=timedelta(minutes=5), + ) + project = "project" + + test_registry.apply_feature_view(fv, project) + + # Pass allow_cache=False explicitly so this keeps reading the DB column even + # if the CachingRegistry default ever changes. + read_back = test_registry.get_feature_view( + "large_proto_feature_view", project, allow_cache=False + ) + # Equality proves the >64 KB proto round-tripped without truncation. + assert read_back.tags["big"] == large_value + + test_registry.delete_feature_view("large_proto_feature_view", project) + test_registry.teardown() + + +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + [ + pytest.param( + lazy_fixture("mysql_registry"), + marks=pytest.mark.xdist_group(name="mysql_registry"), + ), + ], +) +def test_warn_if_narrow_blob_columns_detects_real_blob(test_registry, caplog): + """Exercise the startup BLOB diagnostic against a real MySQL schema. + + The unit tests only mock the engine, so the live information_schema query + (DATA_TYPE = 'blob' filter + table-name scoping) is otherwise unverified. + Narrow a real proto column back to BLOB and assert the diagnostic fires; + then widen it to LONGBLOB and assert it stays silent (confirming the filter + does not also match LONGBLOB, i.e. no false positive on a migrated registry). + """ + from sqlalchemy import text + + engine = test_registry.write_engine + + try: + # Narrow a real proto column back to BLOB NOT NULL to mirror a pre-fix + # (legacy) registry schema. + with engine.begin() as conn: + conn.execute(text("ALTER TABLE entities MODIFY entity_proto BLOB NOT NULL")) + with caplog.at_level(logging.ERROR): + SqlRegistry._warn_if_narrow_blob_columns(engine) + assert "still typed BLOB" in caplog.text + assert "entities.entity_proto" in caplog.text + + # Widen back to LONGBLOB and confirm the DATA_TYPE='blob' filter does not + # also match LONGBLOB (no false positive on a migrated registry). + caplog.clear() + with engine.begin() as conn: + conn.execute( + text("ALTER TABLE entities MODIFY entity_proto LONGBLOB NOT NULL") + ) + with caplog.at_level(logging.ERROR): + SqlRegistry._warn_if_narrow_blob_columns(engine) + assert "still typed BLOB" not in caplog.text + finally: + test_registry.teardown() + + +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + all_fixtures, +) +def test_apply_feature_view_without_source_success(test_registry: BaseRegistry): + """Test that a FeatureView with no source can be applied, retrieved, updated, and deleted.""" + entity = Entity(name="fs1_my_entity_1", join_keys=["test"]) + + fv1 = FeatureView( + name="my_feature_view_no_source", + schema=[ + Field(name="test", dtype=Int64), + Field(name="fs1_my_feature_1", dtype=Int64), + Field(name="fs1_my_feature_2", dtype=String), + Field(name="fs1_my_feature_3", dtype=Array(String)), + ], + entities=[entity], + tags={"team": "matchmaking"}, + source=None, + ttl=timedelta(minutes=5), + ) + + project = "project" + + # Register Feature View + test_registry.apply_feature_view(fv1, project) + + feature_views = test_registry.list_feature_views(project, tags=fv1.tags) + + assert len(feature_views) == 1 + assert feature_views[0].name == "my_feature_view_no_source" + assert feature_views[0].batch_source is None + assert feature_views[0].stream_source is None + assert feature_views[0].features[0].name == "fs1_my_feature_1" + assert feature_views[0].features[0].dtype == Int64 + assert feature_views[0].features[1].name == "fs1_my_feature_2" + assert feature_views[0].features[1].dtype == String + assert feature_views[0].features[2].name == "fs1_my_feature_3" + assert feature_views[0].features[2].dtype == Array(String) + + feature_view = test_registry.get_feature_view("my_feature_view_no_source", project) + any_feature_view = test_registry.get_any_feature_view( + "my_feature_view_no_source", project + ) + + assert feature_view.name == "my_feature_view_no_source" + assert feature_view.batch_source is None + assert feature_view.stream_source is None + assert feature_view.ttl == timedelta(minutes=5) + assert feature_view == any_feature_view + + # After the first apply, created_timestamp should equal last_updated_timestamp. + assert feature_view.created_timestamp == feature_view.last_updated_timestamp + + # Update the feature view and verify created_timestamp is preserved. + fv1.ttl = timedelta(minutes=10) + test_registry.apply_feature_view(fv1, project) + feature_views = test_registry.list_feature_views(project) + assert len(feature_views) == 1 + updated_feature_view = test_registry.get_feature_view( + "my_feature_view_no_source", project + ) + assert updated_feature_view.ttl == timedelta(minutes=10) + assert updated_feature_view.batch_source is None + assert updated_feature_view.created_timestamp == feature_view.created_timestamp + + # Delete the feature view. + test_registry.delete_feature_view("my_feature_view_no_source", project) + feature_views = test_registry.list_feature_views(project) + assert len(feature_views) == 0 + + test_registry.teardown() + + @pytest.mark.integration @pytest.mark.parametrize( "test_registry", @@ -702,6 +932,65 @@ def location_features_from_push(inputs: pd.DataFrame) -> pd.DataFrame: test_registry.teardown() +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + [ + lazy_fixture("local_registry"), + lazy_fixture("sqlite_registry"), + pytest.param( + lazy_fixture("mock_remote_registry"), + marks=pytest.mark.rbac_remote_integration_test, + ), + ], +) +def test_list_all_feature_views_updated_since(test_registry: BaseRegistry): + """Test that list_all_feature_views correctly filters by updated_since.""" + batch_source = FileSource( + file_format=ParquetFormat(), + path="file://feast/*", + timestamp_field="ts_col", + created_timestamp_column="timestamp", + ) + entity = Entity(name="fs1_driver_1", join_keys=["test"]) + fv1 = FeatureView( + name="test_fv_updated_since_1", + schema=[Field(name="test", dtype=Int64)], + entities=[entity], + source=batch_source, + ttl=timedelta(minutes=5), + ) + fv2 = FeatureView( + name="test_fv_updated_since_2", + schema=[Field(name="test", dtype=Int64)], + entities=[entity], + source=batch_source, + ttl=timedelta(minutes=5), + ) + project = "project" + test_registry.apply_entity(entity, project) + test_registry.apply_feature_view(fv1, project) + test_registry.apply_feature_view(fv2, project) + + # A cutoff in the past should return all feature views + past = datetime(2000, 1, 1, tzinfo=timezone.utc) + result = test_registry.list_all_feature_views(project, updated_since=past) + assert len(result) == 2 + + # A cutoff in the future should return nothing + future = datetime(2999, 1, 1, tzinfo=timezone.utc) + result = test_registry.list_all_feature_views(project, updated_since=future) + assert len(result) == 0 + + # No filter returns all feature views + result = test_registry.list_all_feature_views(project) + assert len(result) == 2 + + test_registry.delete_feature_view("test_fv_updated_since_1", project) + test_registry.delete_feature_view("test_fv_updated_since_2", project) + test_registry.teardown() + + @pytest.mark.integration @pytest.mark.parametrize( "test_registry", @@ -816,6 +1105,111 @@ def test_apply_data_source_with_timestamps(test_registry): test_registry.teardown() +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + [lazy_fixture("local_registry")], +) +def test_apply_data_source_cross_project_isolation(test_registry): + """Test that apply_data_source uses project-scoped filtering. + + Regression test for https://github.com/feast-dev/feast/issues/6206: + applying a data source to one project must not overwrite the data source + with the same name in a different project. + + See: feast-dev/feast#6298 + """ + project_a = "project_a" + project_b = "project_b" + + source_a = FileSource( + name="shared_source_name", + file_format=ParquetFormat(), + path="file://feast/project_a.parquet", + timestamp_field="ts_col", + ) + source_b = FileSource( + name="shared_source_name", + file_format=ParquetFormat(), + path="file://feast/project_b.parquet", + timestamp_field="ts_col", + ) + + test_registry.apply_data_source(source_a, project_a, commit=True) + test_registry.apply_data_source(source_b, project_b, commit=True) + + # Each project should have exactly its own source + sources_a = test_registry.list_data_sources(project_a) + sources_b = test_registry.list_data_sources(project_b) + assert len(sources_a) == 1 + assert len(sources_b) == 1 + + # Paths must be project-specific — not overwritten cross-project + assert sources_a[0].path == "file://feast/project_a.parquet" + assert sources_b[0].path == "file://feast/project_b.parquet" + + # Re-apply source_b with updated path: must not bleed into project_a + source_b_updated = FileSource( + name="shared_source_name", + file_format=ParquetFormat(), + path="file://feast/project_b_v2.parquet", + timestamp_field="ts_col", + ) + test_registry.apply_data_source(source_b_updated, project_b, commit=True) + + sources_a_after = test_registry.list_data_sources(project_a) + assert len(sources_a_after) == 1 + assert sources_a_after[0].path == "file://feast/project_a.parquet", ( + "apply_data_source for project_b must not overwrite project_a's source" + ) + + test_registry.teardown() + + +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + [lazy_fixture("local_registry")], +) +def test_delete_data_source_project_scoped(test_registry): + """Test that delete_data_source only removes the source from the given project. + + Regression test for https://github.com/feast-dev/feast/issues/6206: + deleting a data source from one project must not delete the data source + with the same name from another project. + """ + project_a = "project_a" + project_b = "project_b" + + source_a = FileSource( + name="shared_source_name", + file_format=ParquetFormat(), + path="file://feast/project_a.parquet", + timestamp_field="ts_col", + ) + source_b = FileSource( + name="shared_source_name", + file_format=ParquetFormat(), + path="file://feast/project_b.parquet", + timestamp_field="ts_col", + ) + + test_registry.apply_data_source(source_a, project_a, commit=True) + test_registry.apply_data_source(source_b, project_b, commit=True) + + # Delete the source from project_a only + test_registry.delete_data_source("shared_source_name", project_a, commit=True) + + # project_a should have no sources; project_b should be unaffected + sources_a = test_registry.list_data_sources(project_a) + sources_b = test_registry.list_data_sources(project_b) + assert len(sources_a) == 0, "Source should be deleted from project_a" + assert len(sources_b) == 1, "Source in project_b must not be deleted" + assert sources_b[0].path == "file://feast/project_b.parquet" + + test_registry.teardown() + + @pytest.mark.integration @pytest.mark.parametrize( "test_registry", @@ -1993,3 +2387,119 @@ def test_commit_for_read_only_user(): assert len(entities) == 1 write_registry.teardown() + + +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + # mock_remote_registry excluded: the mock gRPC channel does not propagate + # server-side errors, so ConflictingFeatureViewNames is not raised client-side. + [f for f in all_fixtures if "mock_remote" not in str(f)], +) +def test_cross_type_feature_view_name_conflict(test_registry: BaseRegistry): + """ + Test that feature view names must be unique across all feature view types. + + This validates the fix for feast-dev/feast#5995: If a FeatureView and + StreamFeatureView share the same name, get_online_features would silently + return the wrong one (fixed order lookup). This test ensures such conflicts + are caught during registration. + """ + project = "project" + + # Create a simple entity + entity = Entity(name="driver_entity", join_keys=["test_key"]) + + # Create a regular FeatureView + file_source = FileSource(name="my_file_source", path="test.parquet") + feature_view = FeatureView( + name="shared_feature_view_name", + entities=[entity], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + ) + + # Create a StreamFeatureView with the SAME name + stream_source = KafkaSource( + name="kafka", + timestamp_field="event_timestamp", + kafka_bootstrap_servers="", + message_format=AvroFormat(""), + topic="topic", + batch_source=FileSource(path="some path"), + watermark_delay_threshold=timedelta(days=1), + ) + + def simple_udf(x: int): + return x + 3 + + stream_feature_view = StreamFeatureView( + name="shared_feature_view_name", # Same name as FeatureView! + entities=[entity], + ttl=timedelta(days=30), + schema=[Field(name="feature1", dtype=Float32)], + source=stream_source, + udf=simple_udf, + ) + + # Register the regular FeatureView first - should succeed + test_registry.apply_feature_view(feature_view, project) + + # Attempt to register StreamFeatureView with same name - should fail + with pytest.raises(ConflictingFeatureViewNames) as exc_info: + test_registry.apply_feature_view(stream_feature_view, project) + + # Verify error message contains the conflicting types + error_message = str(exc_info.value) + assert "shared_feature_view_name" in error_message + + # Cleanup + test_registry.delete_feature_view("shared_feature_view_name", project) + test_registry.teardown() + + +@pytest.mark.integration +@pytest.mark.parametrize( + "test_registry", + [f for f in all_fixtures if "mock_remote" not in str(f)], +) +def test_cross_type_feature_view_odfv_conflict(test_registry: BaseRegistry): + """ + Test that OnDemandFeatureView names must be unique across all feature view types. + """ + project = "project" + + # Create a simple entity + entity = Entity(name="driver_entity", join_keys=["test_key"]) + + # Create a regular FeatureView + file_source = FileSource(name="my_file_source", path="test.parquet") + feature_view = FeatureView( + name="shared_odfv_name", + entities=[entity], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + ) + + # Create an OnDemandFeatureView with the SAME name + @on_demand_feature_view( + sources=[feature_view], + schema=[Field(name="output", dtype=Float32)], + ) + def shared_odfv_name(inputs: pd.DataFrame) -> pd.DataFrame: + return pd.DataFrame({"output": inputs["feature1"] * 2}) + + # Register the regular FeatureView first - should succeed + test_registry.apply_feature_view(feature_view, project) + + # Attempt to register OnDemandFeatureView with same name - should fail + with pytest.raises(ConflictingFeatureViewNames) as exc_info: + test_registry.apply_feature_view(shared_odfv_name, project) + + # Verify error message contains the conflicting types + error_message = str(exc_info.value) + assert "shared_odfv_name" in error_message + + # Cleanup + test_registry.delete_feature_view("shared_odfv_name", project) + test_registry.teardown() diff --git a/sdk/python/tests/integration/registration/test_versioning.py b/sdk/python/tests/integration/registration/test_versioning.py new file mode 100644 index 00000000000..72289e72a4f --- /dev/null +++ b/sdk/python/tests/integration/registration/test_versioning.py @@ -0,0 +1,1418 @@ +"""Integration tests for feature view versioning.""" + +import os +import tempfile +from datetime import timedelta +from pathlib import Path + +import pytest + +from feast import FeatureStore +from feast.entity import Entity +from feast.errors import FeatureViewPinConflict, FeatureViewVersionNotFound +from feast.feature_service import FeatureService +from feast.feature_view import FeatureView +from feast.field import Field +from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.infra.registry.registry import Registry +from feast.repo_config import RegistryConfig, RepoConfig +from feast.stream_feature_view import StreamFeatureView +from feast.types import Float32, Int64 +from feast.value_type import ValueType + + +@pytest.fixture +def registry(): + """Create a file-based Registry for testing. Version history is always-on.""" + with tempfile.TemporaryDirectory() as tmpdir: + registry_path = Path(tmpdir) / "registry.pb" + config = RegistryConfig(path=str(registry_path)) + reg = Registry("test_project", config, None) + yield reg + + +@pytest.fixture +def registry_no_online_versioning(): + """Create a file-based Registry without online versioning (default).""" + with tempfile.TemporaryDirectory() as tmpdir: + registry_path = Path(tmpdir) / "registry.pb" + config = RegistryConfig(path=str(registry_path)) + reg = Registry("test_project", config, None) + yield reg + + +@pytest.fixture +def entity(): + return Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + + +@pytest.fixture +def make_fv(entity): + def _make(description="test feature view", version="latest", **kwargs): + return FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + ], + description=description, + version=version, + **kwargs, + ) + + return _make + + +@pytest.fixture +def make_sfv(entity): + def _make(description="test stream feature view", udf=None, **kwargs): + from feast.data_source import PushSource + from feast.infra.offline_stores.file_source import FileSource + + def default_udf(df): + return df + + # Create batch source + batch_source = FileSource(name="test_batch_source", path="test.parquet") + + # Create a simple push source for testing + source = PushSource(name="test_push_source", batch_source=batch_source) + + return StreamFeatureView( + name="driver_stats_stream", + entities=[entity], + ttl=timedelta(hours=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + ], + description=description, + udf=udf or default_udf, + source=source, + **kwargs, + ) + + return _make + + +# OnDemandFeatureView tests removed due to transformation comparison issues + + +class TestFileRegistryVersioning: + def test_first_apply_creates_v0(self, registry, make_fv): + fv = make_fv() + registry.apply_feature_view(fv, "test_project", commit=True) + + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 1 + assert versions[0]["version"] == "v0" + assert versions[0]["version_number"] == 0 + + def test_modify_and_reapply_creates_new_version(self, registry, make_fv, entity): + fv1 = make_fv(description="version one") + registry.apply_feature_view(fv1, "test_project", commit=True) + + # Create a schema change by adding a new feature + fv2 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_feature", dtype=Float32), # Schema change + ], + description="version two", + ) + registry.apply_feature_view(fv2, "test_project", commit=True) + + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 2 + assert versions[0]["version"] == "v0" + assert versions[1]["version"] == "v1" + + def test_idempotent_apply_no_new_version(self, registry, make_fv): + fv = make_fv(description="same definition") + registry.apply_feature_view(fv, "test_project", commit=True) + + # Apply identical FV again + fv_same = make_fv(description="same definition") + registry.apply_feature_view(fv_same, "test_project", commit=True) + + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 1 # No new version created + + def test_pin_to_v0(self, registry, make_fv, entity): + # Create v0 + fv1 = make_fv(description="original") + registry.apply_feature_view(fv1, "test_project", commit=True) + + # Create v1 with schema change + fv2 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_field", dtype=Float32), # Schema change + ], + description="updated", + ) + registry.apply_feature_view(fv2, "test_project", commit=True) + + # Pin to v0 (definition must match active FV, only version changes) + fv_pin = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_field", dtype=Float32), # Keep current schema + ], + description="updated", + version="v0", + ) + registry.apply_feature_view(fv_pin, "test_project", commit=True) + + # Verify active entry has v0's content + active_fv = registry.get_feature_view("driver_stats", "test_project") + assert active_fv.description == "original" + assert active_fv.version == "v0" + + def test_explicit_version_creates_when_not_exists(self, registry, make_fv): + """Explicit version on a new FV creates that version (forward declaration).""" + fv = make_fv(description="forward declared v1", version="v1") + registry.apply_feature_view(fv, "test_project", commit=True) + + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 1 + assert versions[0]["version_number"] == 1 + + active_fv = registry.get_feature_view("driver_stats", "test_project") + assert active_fv.current_version_number == 1 + + def test_explicit_version_reverts_when_exists(self, registry, make_fv, entity): + """Explicit version on an existing FV reverts to that snapshot (pin/revert).""" + # Create v0 + fv1 = make_fv(description="original") + registry.apply_feature_view(fv1, "test_project", commit=True) + + # Create v1 with schema change + fv2 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_field", dtype=Float32), + ], + description="updated", + ) + registry.apply_feature_view(fv2, "test_project", commit=True) + + # Pin to v0 (definition must match active FV, only version changes) + fv_pin = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_field", dtype=Float32), + ], + description="updated", + version="v0", + ) + registry.apply_feature_view(fv_pin, "test_project", commit=True) + + active_fv = registry.get_feature_view("driver_stats", "test_project") + assert active_fv.description == "original" + assert active_fv.version == "v0" + + def test_forward_declare_nonexistent_version(self, registry, make_fv): + """Forward-declaring a version that doesn't exist creates it.""" + fv = make_fv() + registry.apply_feature_view(fv, "test_project", commit=True) + + # Forward-declare v5 — should create it, not raise + fv_v5 = make_fv(description="forward declared v5", version="v5") + registry.apply_feature_view(fv_v5, "test_project", commit=True) + + versions = registry.list_feature_view_versions("driver_stats", "test_project") + version_numbers = [v["version_number"] for v in versions] + assert 5 in version_numbers + + # The active FV should now be the forward-declared v5 + active_fv = registry.get_feature_view("driver_stats", "test_project") + assert active_fv.current_version_number == 5 + assert active_fv.description == "forward declared v5" + + def test_apply_after_pin_creates_new_version(self, registry, make_fv, entity): + # Create v0 + fv1 = make_fv(description="v0 desc") + registry.apply_feature_view(fv1, "test_project", commit=True) + + # Create v1 with schema change + fv2 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="v1_field", dtype=Float32), # Schema change + ], + description="v1 desc", + ) + registry.apply_feature_view(fv2, "test_project", commit=True) + + # Pin to v0 (definition must match active FV, only version changes) + fv_pin = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="v1_field", dtype=Float32), # Keep current schema + ], + description="v1 desc", + version="v0", + ) + registry.apply_feature_view(fv_pin, "test_project", commit=True) + + # Apply new content with another schema change (should create new version) + fv3 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="v1_field", dtype=Float32), + Field(name="v2_field", dtype=Float32), # Another schema change + ], + description="v2 desc after pin", + ) + registry.apply_feature_view(fv3, "test_project", commit=True) + + versions = registry.list_feature_view_versions("driver_stats", "test_project") + # Should have v0, v1, and potentially more versions + assert len(versions) >= 2 + + def test_pin_with_modified_definition_raises(self, registry, make_fv, entity): + # Create v0 + fv1 = make_fv(description="original") + registry.apply_feature_view(fv1, "test_project", commit=True) + + # Create v1 with schema change + fv2 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_field", dtype=Float32), # Schema change + ], + description="updated", + ) + registry.apply_feature_view(fv2, "test_project", commit=True) + + # Attempt to pin to v0 while also changing schema (sneaky change) + fv_pin = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_field", dtype=Float32), + Field(name="sneaky_field", dtype=Float32), # Sneaky schema change + ], + description="updated", + version="v0", + ) + with pytest.raises(FeatureViewPinConflict): + registry.apply_feature_view(fv_pin, "test_project", commit=True) + + def test_pin_without_modification_succeeds(self, registry, make_fv, entity): + # Create v0 + fv1 = make_fv(description="original") + registry.apply_feature_view(fv1, "test_project", commit=True) + + # Create v1 with schema change + fv2 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_field", dtype=Float32), # Schema change + ], + description="updated", + ) + registry.apply_feature_view(fv2, "test_project", commit=True) + + # Pin to v0 with same definition as active (only version changes) + fv_pin = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_field", dtype=Float32), # Keep current schema + ], + description="updated", + version="v0", + ) + registry.apply_feature_view(fv_pin, "test_project", commit=True) + + # Verify active entry has v0's content + active_fv = registry.get_feature_view("driver_stats", "test_project") + assert active_fv.description == "original" + assert active_fv.version == "v0" + + def test_get_feature_view_by_version(self, registry, make_fv, entity): + # Create v0 + fv1 = make_fv(description="version zero") + registry.apply_feature_view(fv1, "test_project", commit=True) + + # Create v1 with schema change and different description + fv2 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="v1_field", dtype=Float32), # Schema change + ], + description="version one", + ) + registry.apply_feature_view(fv2, "test_project", commit=True) + + # Retrieve v0 snapshot + fv_v0 = registry.get_feature_view_by_version("driver_stats", "test_project", 0) + assert fv_v0.description == "version zero" + assert fv_v0.current_version_number == 0 + + # Retrieve v1 snapshot + fv_v1 = registry.get_feature_view_by_version("driver_stats", "test_project", 1) + assert fv_v1.description == "version one" + assert fv_v1.current_version_number == 1 + + def test_get_feature_view_by_version_not_found(self, registry, make_fv): + fv = make_fv() + registry.apply_feature_view(fv, "test_project", commit=True) + + with pytest.raises(FeatureViewVersionNotFound): + registry.get_feature_view_by_version("driver_stats", "test_project", 99) + + def test_version_in_proto_roundtrip(self, registry, make_fv): + fv = make_fv(version="v3") + # Manually set version number for testing + fv.current_version_number = 3 + + proto = fv.to_proto() + assert proto.spec.version == "v3" + assert proto.meta.current_version_number == 3 + + fv2 = FeatureView.from_proto(proto) + assert fv2.version == "v3" + assert fv2.current_version_number == 3 + + +class TestRefinedVersioningBehavior: + """Test that only schema and UDF changes create new versions.""" + + def test_metadata_changes_no_new_version(self, registry, make_fv): + """Verify metadata changes don't create new versions.""" + fv = make_fv(description="original description", tags={"team": "ml"}) + registry.apply_feature_view(fv, "test_project", commit=True) + + # Modify only metadata + fv_updated = make_fv( + description="updated description", + tags={"team": "ml", "env": "prod"}, + owner="new_owner@company.com", + ) + registry.apply_feature_view(fv_updated, "test_project", commit=True) + + # Should not create new version + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 1 # Still just v0 + assert versions[0]["version_number"] == 0 + + def test_schema_changes_create_new_version(self, registry, make_fv, entity): + """Verify schema changes create new versions.""" + fv = make_fv() + registry.apply_feature_view(fv, "test_project", commit=True) + + # Add a new feature (schema change) + fv_with_new_feature = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_feature", dtype=Float32), # New field + ], + description="same description", # Keep same metadata + ) + registry.apply_feature_view(fv_with_new_feature, "test_project", commit=True) + + # Should create new version + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 2 # v0 and v1 + assert versions[1]["version_number"] == 1 + + def test_configuration_changes_no_new_version(self, registry, make_fv): + """Verify configuration changes don't create new versions.""" + fv = make_fv(online=True, offline=True) + registry.apply_feature_view(fv, "test_project", commit=True) + + # Change configuration fields + fv_config_updated = make_fv( + online=False, # Configuration change + offline=False, # Configuration change + description="same description", # Keep same metadata + ) + registry.apply_feature_view(fv_config_updated, "test_project", commit=True) + + # Should not create new version + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 1 # Still just v0 + assert versions[0]["version_number"] == 0 + + def test_entity_changes_create_new_version(self, registry, make_fv): + """Verify entity changes create new versions.""" + fv = make_fv() + registry.apply_feature_view(fv, "test_project", commit=True) + + # Create new entity and add it (schema change) + new_entity = Entity( + name="vehicle_id", + join_keys=["vehicle_id"], + value_type=ValueType.INT64, # Match the field type + ) + + fv_with_new_entity = FeatureView( + name="driver_stats", + entities=[ + Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ), + new_entity, + ], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="vehicle_id", dtype=Int64), # New entity field + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + ], + description="same description", + ) + registry.apply_feature_view(fv_with_new_entity, "test_project", commit=True) + + # Should create new version due to entity change + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 2 # v0 and v1 + assert versions[1]["version_number"] == 1 + + def test_stream_feature_view_udf_changes_create_new_version( + self, registry, make_sfv + ): + """Verify UDF changes create new versions (StreamFeatureView).""" + + def original_transform(df): + return df + + def updated_transform(df): + df["new_col"] = df["trips_today"] * 2 + return df + + sfv = make_sfv(udf=original_transform) + registry.apply_feature_view(sfv, "test_project", commit=True) + + sfv_updated = make_sfv( + udf=updated_transform, + description="same description", # Keep same metadata + ) + registry.apply_feature_view(sfv_updated, "test_project", commit=True) + + # Should create new version due to UDF change + versions = registry.list_feature_view_versions( + "driver_stats_stream", "test_project" + ) + assert len(versions) == 2 # v0 and v1 + + # TODO: Add tests for OnDemandFeatureView once transformation comparison issues are resolved + # The current issue is that PythonTransformation.__eq__ has strict type checking + # that prevents proper comparison between objects created at different times + + +class TestVersionMetadataIntegration: + """Integration tests for version metadata functionality in responses.""" + + def test_version_metadata_disabled_by_default(self, registry, make_fv): + """Test that version metadata is not included by default.""" + from feast.protos.feast.serving.ServingService_pb2 import ( + GetOnlineFeaturesResponse, + ) + from feast.utils import _populate_response_from_feature_data + + # Create and register a versioned feature view + fv = make_fv(description="test version metadata") + registry.apply_feature_view(fv, "test_project", commit=True) + + # Get the feature view with version info + active_fv = registry.get_feature_view("driver_stats", "test_project") + assert hasattr(active_fv, "current_version_number") + + # Mock response generation without version metadata + response = GetOnlineFeaturesResponse() + _populate_response_from_feature_data( + requested_features=["trips_today"], + read_rows=[], + indexes=(), + online_features_response=response, + full_feature_names=True, + table=active_fv, + output_len=0, + include_feature_view_version_metadata=False, # Default behavior + ) + + # Verify no version metadata is included + assert len(response.metadata.feature_view_metadata) == 0 + # Feature names should still be populated + assert len(response.metadata.feature_names.val) == 1 + + def test_version_metadata_included_when_requested(self, registry, make_fv, entity): + """Test that version metadata is included when requested.""" + from feast.protos.feast.serving.ServingService_pb2 import ( + GetOnlineFeaturesResponse, + ) + from feast.utils import _populate_response_from_feature_data + + # Create v0 + fv1 = make_fv(description="first version") + registry.apply_feature_view(fv1, "test_project", commit=True) + + # Create v1 by modifying schema + fv2 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="total_earnings", dtype=Float32), # New field + ], + description="second version", + ) + registry.apply_feature_view(fv2, "test_project", commit=True) + + # Get v1 (latest version) + active_fv = registry.get_feature_view("driver_stats", "test_project") + assert active_fv.current_version_number == 1 + + # Mock response generation with version metadata + response = GetOnlineFeaturesResponse() + _populate_response_from_feature_data( + requested_features=["trips_today", "total_earnings"], + read_rows=[], + indexes=(), + online_features_response=response, + full_feature_names=False, # Test without prefixes + table=active_fv, + output_len=0, + include_feature_view_version_metadata=True, # Enable metadata + ) + + # Verify version metadata is included + assert len(response.metadata.feature_view_metadata) == 1 + fv_metadata = response.metadata.feature_view_metadata[0] + assert fv_metadata.name == "driver_stats" + assert fv_metadata.version == 1 + + # Verify feature names are clean (no @v1 suffix) + feature_names = list(response.metadata.feature_names.val) + assert feature_names == ["trips_today", "total_earnings"] + assert all("@" not in name for name in feature_names) + + def test_version_metadata_clean_names_with_prefixes(self, registry, make_fv): + """Test that feature names are clean even with full_feature_names=True.""" + from feast.protos.feast.serving.ServingService_pb2 import ( + GetOnlineFeaturesResponse, + ) + from feast.utils import _populate_response_from_feature_data + + # Create versioned feature view + fv = make_fv(description="test clean names") + registry.apply_feature_view(fv, "test_project", commit=True) + active_fv = registry.get_feature_view("driver_stats", "test_project") + + # Test with full feature names (prefixed) + response = GetOnlineFeaturesResponse() + _populate_response_from_feature_data( + requested_features=["trips_today"], + read_rows=[], + indexes=(), + online_features_response=response, + full_feature_names=True, # Enable prefixes + table=active_fv, + output_len=0, + include_feature_view_version_metadata=True, + ) + + # Feature names should be prefixed but clean (no @v0) + feature_names = list(response.metadata.feature_names.val) + assert len(feature_names) == 1 + assert feature_names[0] == "driver_stats__trips_today" # Clean prefix + assert "@" not in feature_names[0] # No version in name + + # Version metadata should be separate + assert len(response.metadata.feature_view_metadata) == 1 + assert response.metadata.feature_view_metadata[0].name == "driver_stats" + assert response.metadata.feature_view_metadata[0].version == 0 + + def test_version_metadata_multiple_feature_views(self, registry, entity): + """Test version metadata with multiple feature views.""" + from feast.protos.feast.serving.ServingService_pb2 import ( + GetOnlineFeaturesResponse, + ) + from feast.utils import _populate_response_from_feature_data + + # Create first feature view + fv1 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + ], + description="driver features", + ) + registry.apply_feature_view(fv1, "test_project", commit=True) + + # Create second feature view + fv2 = FeatureView( + name="user_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="total_bookings", dtype=Int64), + ], + description="user features", + ) + registry.apply_feature_view(fv2, "test_project", commit=True) + + # Modify user_stats to create v1 + fv2_v1 = FeatureView( + name="user_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="total_bookings", dtype=Int64), + Field(name="cancellation_rate", dtype=Float32), # New field + ], + description="user features v1", + ) + registry.apply_feature_view(fv2_v1, "test_project", commit=True) + + # Get feature views + driver_fv = registry.get_feature_view("driver_stats", "test_project") # v0 + user_fv = registry.get_feature_view("user_stats", "test_project") # v1 + + # Simulate processing multiple feature views in response + response = GetOnlineFeaturesResponse() + + # Process first feature view + _populate_response_from_feature_data( + requested_features=["trips_today"], + read_rows=[], + indexes=(), + online_features_response=response, + full_feature_names=False, + table=driver_fv, + output_len=0, + include_feature_view_version_metadata=True, + ) + + # Process second feature view + _populate_response_from_feature_data( + requested_features=["total_bookings", "cancellation_rate"], + read_rows=[], + indexes=(), + online_features_response=response, + full_feature_names=False, + table=user_fv, + output_len=0, + include_feature_view_version_metadata=True, + ) + + # Verify both feature views are in metadata + assert len(response.metadata.feature_view_metadata) == 2 + + fv_metadata_by_name = { + fvm.name: fvm for fvm in response.metadata.feature_view_metadata + } + assert "driver_stats" in fv_metadata_by_name + assert "user_stats" in fv_metadata_by_name + + # Check versions + assert fv_metadata_by_name["driver_stats"].version == 0 + assert fv_metadata_by_name["user_stats"].version == 1 + + # Verify feature names are clean + feature_names = list(response.metadata.feature_names.val) + assert feature_names == ["trips_today", "total_bookings", "cancellation_rate"] + assert all("@" not in name for name in feature_names) + + def test_version_metadata_prevents_duplicates(self, registry, make_fv): + """Test that duplicate feature view metadata is not added.""" + from feast.protos.feast.serving.ServingService_pb2 import ( + GetOnlineFeaturesResponse, + ) + from feast.utils import _populate_response_from_feature_data + + fv = make_fv(description="test duplicates") + registry.apply_feature_view(fv, "test_project", commit=True) + active_fv = registry.get_feature_view("driver_stats", "test_project") + + response = GetOnlineFeaturesResponse() + + # Process same feature view twice (simulating multiple features from same view) + _populate_response_from_feature_data( + requested_features=["trips_today"], + read_rows=[], + indexes=(), + online_features_response=response, + full_feature_names=False, + table=active_fv, + output_len=0, + include_feature_view_version_metadata=True, + ) + + _populate_response_from_feature_data( + requested_features=["avg_rating"], + read_rows=[], + indexes=(), + online_features_response=response, + full_feature_names=False, + table=active_fv, + output_len=0, + include_feature_view_version_metadata=True, + ) + + # Should only have one metadata entry despite processing twice + assert len(response.metadata.feature_view_metadata) == 1 + assert response.metadata.feature_view_metadata[0].name == "driver_stats" + + # But should have both feature names + feature_names = list(response.metadata.feature_names.val) + assert len(feature_names) == 2 + assert "trips_today" in feature_names + assert "avg_rating" in feature_names + + def test_version_metadata_backward_compatibility(self, registry, make_fv): + """Test that existing code without version metadata still works.""" + from feast.protos.feast.serving.ServingService_pb2 import ( + GetOnlineFeaturesResponse, + ) + from feast.utils import _populate_response_from_feature_data + + fv = make_fv(description="backward compatibility test") + registry.apply_feature_view(fv, "test_project", commit=True) + active_fv = registry.get_feature_view("driver_stats", "test_project") + + # Test calling without the new parameter (should default to False) + response = GetOnlineFeaturesResponse() + _populate_response_from_feature_data( + requested_features=["trips_today"], + read_rows=[], + indexes=(), + online_features_response=response, + full_feature_names=True, + table=active_fv, + output_len=0, + # Note: include_feature_view_version_metadata parameter omitted + ) + + # Should work and not include version metadata + assert len(response.metadata.feature_view_metadata) == 0 + assert len(response.metadata.feature_names.val) == 1 + + +class TestOnlineVersioningDisabled: + """Tests that online versioning guard works for version-qualified refs.""" + + def test_version_qualified_ref_raises_when_online_versioning_disabled( + self, registry_no_online_versioning, make_fv, entity + ): + """Using @v2 refs raises ValueError when online versioning is disabled.""" + from feast.utils import _get_feature_views_to_use + + fv = make_fv(description="test fv") + registry_no_online_versioning.apply_feature_view( + fv, "test_project", commit=True + ) + + # Create v1 with schema change + fv2 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_field", dtype=Float32), + ], + description="version one", + ) + registry_no_online_versioning.apply_feature_view( + fv2, "test_project", commit=True + ) + + # Version-qualified ref should raise + with pytest.raises(ValueError, match="online versioning is disabled"): + _get_feature_views_to_use( + registry=registry_no_online_versioning, + project="test_project", + features=["driver_stats@v1:trips_today"], + allow_cache=False, + hide_dummy_entity=False, + ) + + +class TestFeatureServiceVersioningGates: + """Tests that feature services are gated when referencing versioned feature views.""" + + @pytest.fixture + def versioned_fv_and_entity(self): + """Create a versioned feature view (v1) and its entity.""" + entity = Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + # v0 definition + fv_v0 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + ], + description="v0", + ) + # v1 definition (schema change) + fv_v1 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + ], + description="v1", + ) + return entity, fv_v0, fv_v1 + + @pytest.fixture + def unversioned_fv_and_entity(self): + """Create an unversioned feature view (v0 only) and its entity.""" + entity = Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + fv = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + ], + description="only version", + ) + return entity, fv + + def _make_store(self, tmpdir, enable_versioning=False): + """Create a FeatureStore with optional online versioning.""" + registry_path = os.path.join(tmpdir, "registry.db") + online_path = os.path.join(tmpdir, "online.db") + return FeatureStore( + config=RepoConfig( + registry=RegistryConfig( + path=registry_path, + enable_online_feature_view_versioning=enable_versioning, + ), + project="test_project", + provider="local", + online_store=SqliteOnlineStoreConfig(path=online_path), + entity_key_serialization_version=3, + ) + ) + + def test_feature_service_apply_succeeds_with_versioned_fv_when_flag_off( + self, versioned_fv_and_entity + ): + """Apply a feature service referencing a versioned FV with flag off -> succeeds.""" + entity, fv_v0, fv_v1 = versioned_fv_and_entity + + with tempfile.TemporaryDirectory() as tmpdir: + store = self._make_store(tmpdir, enable_versioning=False) + + # Apply v0 first, then v1 to create version history + store.apply([entity, fv_v0]) + store.apply([entity, fv_v1]) + + # Now create a feature service referencing the versioned FV + fs = FeatureService( + name="driver_service", + features=[fv_v1], + ) + store.apply([fs]) # Should not raise + + def test_feature_service_apply_succeeds_with_versioned_fv_when_flag_on( + self, versioned_fv_and_entity + ): + """Apply a feature service referencing a versioned FV with flag on -> succeeds.""" + entity, fv_v0, fv_v1 = versioned_fv_and_entity + + with tempfile.TemporaryDirectory() as tmpdir: + store = self._make_store(tmpdir, enable_versioning=True) + + # Apply v0 first, then v1 to create version history + store.apply([entity, fv_v0]) + store.apply([entity, fv_v1]) + + # Feature service referencing versioned FV should succeed + fs = FeatureService( + name="driver_service", + features=[fv_v1], + ) + store.apply([fs]) # Should not raise + + def test_feature_service_retrieval_succeeds_with_versioned_fv_when_flag_off( + self, versioned_fv_and_entity + ): + """get_online_features with a feature service referencing a versioned FV, flag off -> succeeds.""" + entity, fv_v0, fv_v1 = versioned_fv_and_entity + from feast.utils import _get_feature_views_to_use + + with tempfile.TemporaryDirectory() as tmpdir: + # First apply with flag on so the feature service can be registered + store_on = self._make_store(tmpdir, enable_versioning=True) + store_on.apply([entity, fv_v0]) + store_on.apply([entity, fv_v1]) + fs = FeatureService( + name="driver_service", + features=[fv_v1], + ) + store_on.apply([fs]) + + # Now create a store with the flag off to test retrieval + store_off = self._make_store(tmpdir, enable_versioning=False) + registered_fs = store_off.registry.get_feature_service( + "driver_service", "test_project" + ) + + fvs, _ = _get_feature_views_to_use( + registry=store_off.registry, + project="test_project", + features=registered_fs, + allow_cache=False, + hide_dummy_entity=False, + ) + + assert len(fvs) == 1 + + def test_feature_service_with_unversioned_fv_succeeds( + self, unversioned_fv_and_entity + ): + """Feature service with v0 FV works fine regardless of flag.""" + entity, fv = unversioned_fv_and_entity + + with tempfile.TemporaryDirectory() as tmpdir: + store = self._make_store(tmpdir, enable_versioning=False) + + # Apply unversioned FV and feature service + fs = FeatureService( + name="driver_service", + features=[fv], + ) + store.apply([entity, fv, fs]) # Should not raise + + def test_feature_service_serves_versioned_fv_when_flag_on( + self, versioned_fv_and_entity + ): + """With online versioning on, FeatureService projections do not carry version_tag; + the FV in the registry carries current_version_number.""" + from feast.utils import _get_feature_views_to_use + + entity, fv_v0, fv_v1 = versioned_fv_and_entity + + with tempfile.TemporaryDirectory() as tmpdir: + store = self._make_store(tmpdir, enable_versioning=True) + + # Apply v0 then v1 to create version history + store.apply([entity, fv_v0]) + store.apply([entity, fv_v1]) + + # Create and apply a feature service referencing the versioned FV + fs = FeatureService( + name="driver_service", + features=[fv_v1], + ) + store.apply([fs]) + + # Retrieve the registered feature service + registered_fs = store.registry.get_feature_service( + "driver_service", "test_project" + ) + + fvs, _ = _get_feature_views_to_use( + registry=store.registry, + project="test_project", + features=registered_fs, + allow_cache=False, + hide_dummy_entity=False, + ) + + assert len(fvs) == 1 + assert fvs[0].projection.version_tag is None + assert fvs[0].projection.name_to_use() == "driver_stats" + + # Verify the FV in the registry has the correct version + fv_from_registry = store.registry.get_feature_view( + "driver_stats", "test_project" + ) + assert fv_from_registry.current_version_number == 1 + + def test_feature_service_feature_refs_are_plain_when_flag_on( + self, versioned_fv_and_entity + ): + """With online versioning on, _get_features() produces plain (non-versioned) refs for FeatureService.""" + from feast.utils import _get_features + + entity, fv_v0, fv_v1 = versioned_fv_and_entity + + with tempfile.TemporaryDirectory() as tmpdir: + store = self._make_store(tmpdir, enable_versioning=True) + + # Apply v0 then v1 to create version history + store.apply([entity, fv_v0]) + store.apply([entity, fv_v1]) + + # Create and apply a feature service referencing the versioned FV + fs = FeatureService( + name="driver_service", + features=[fv_v1], + ) + store.apply([fs]) + + # Retrieve the registered feature service + registered_fs = store.registry.get_feature_service( + "driver_service", "test_project" + ) + + refs = _get_features( + registry=store.registry, + project="test_project", + features=registered_fs, + allow_cache=False, + ) + + # Refs should be plain (no version qualifier) + for ref in refs: + assert "@v" not in ref, f"Expected plain ref, got: {ref}" + + # Check specific ref format + assert "driver_stats:trips_today" in refs + + def test_unpin_from_versioned_to_latest(self, versioned_fv_and_entity): + """Pin a FV to v1, then apply with version='latest' (no schema change) -> unpinned.""" + entity, fv_v0, fv_v1 = versioned_fv_and_entity + + with tempfile.TemporaryDirectory() as tmpdir: + store = self._make_store(tmpdir, enable_versioning=True) + + # Apply v0 then v1 to create version history (v1 has schema change) + store.apply([entity, fv_v0]) + store.apply([entity, fv_v1]) + + # Verify it's pinned to v1 + reloaded = store.registry.get_feature_view("driver_stats", "test_project") + assert reloaded.current_version_number == 1 + + # Now re-apply the same schema with version="latest" to unpin + fv_latest = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + ], + version="latest", + description="v1", + ) + store.apply([entity, fv_latest]) + + # Reload and verify unpinned + reloaded = store.registry.get_feature_view("driver_stats", "test_project") + assert reloaded.current_version_number is None + assert reloaded.version == "latest" + + +class TestNoPromote: + """Tests for the no_promote flag on apply_feature_view.""" + + def test_no_promote_saves_version_without_updating_active( + self, registry, make_fv, entity + ): + """Apply v0, then schema change with no_promote=True. + Version record for v1 should exist, but active FV keeps v0's schema.""" + fv0 = make_fv(description="original v0") + registry.apply_feature_view(fv0, "test_project", commit=True) + + # Schema change with no_promote + fv1 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="new_feature", dtype=Float32), # Schema change + ], + description="staged v1", + ) + registry.apply_feature_view(fv1, "test_project", commit=True, no_promote=True) + + # Version v1 should exist in history + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 2 + assert versions[0]["version_number"] == 0 + assert versions[1]["version_number"] == 1 + + # Active FV should still be v0 (initial apply with version="latest" + # has current_version_number=None since proto 0 maps to None for latest) + active = registry.get_feature_view("driver_stats", "test_project") + assert active.current_version_number is None + assert active.description == "original v0" + # v0 schema has 3 fields (driver_id, trips_today, avg_rating) + feature_names = {f.name for f in active.schema} + assert "new_feature" not in feature_names + + def test_no_promote_then_regular_apply_promotes(self, registry, make_fv, entity): + """Apply with no_promote, then re-apply the same schema change without + no_promote. The new version should now be promoted to active.""" + fv0 = make_fv(description="original") + registry.apply_feature_view(fv0, "test_project", commit=True) + + # Schema change with no_promote + fv1_schema = [ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="extra", dtype=Float32), + ] + fv1 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=fv1_schema, + description="staged v1", + ) + registry.apply_feature_view(fv1, "test_project", commit=True, no_promote=True) + + # Now apply same schema change without no_promote + fv1_promote = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=fv1_schema, + description="promoted v1", + ) + registry.apply_feature_view( + fv1_promote, "test_project", commit=True, no_promote=False + ) + + # Active FV should now have the new schema + active = registry.get_feature_view("driver_stats", "test_project") + feature_names = {f.name for f in active.schema} + assert "extra" in feature_names + + def test_no_promote_then_explicit_pin_promotes(self, registry, make_fv, entity): + """Apply with no_promote, then pin to v1. Active should now be v1.""" + fv0 = make_fv(description="original") + registry.apply_feature_view(fv0, "test_project", commit=True) + + # Schema change with no_promote + fv1 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="pinned_feature", dtype=Float32), + ], + description="staged v1", + ) + registry.apply_feature_view(fv1, "test_project", commit=True, no_promote=True) + + # Active is still v0 (initial apply with version="latest" + # has current_version_number=None since proto 0 maps to None for latest) + active = registry.get_feature_view("driver_stats", "test_project") + assert active.current_version_number is None + + # Pin to v1 (user's definition must match current active, only version changes) + fv_pin = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + ], + description="original", + version="v1", + ) + registry.apply_feature_view(fv_pin, "test_project", commit=True) + + # Active should now be v1's snapshot + active = registry.get_feature_view("driver_stats", "test_project") + assert active.current_version_number == 1 + feature_names = {f.name for f in active.schema} + assert "pinned_feature" in feature_names + + def test_no_promote_noop_without_schema_change(self, registry, make_fv): + """Apply with no_promote but no schema change — metadata-only update, + no new version should be created.""" + fv0 = make_fv(description="original") + registry.apply_feature_view(fv0, "test_project", commit=True) + + # Same schema, different description (metadata-only) + fv_same = make_fv(description="updated description only") + registry.apply_feature_view( + fv_same, "test_project", commit=True, no_promote=True + ) + + # Still only v0 + versions = registry.list_feature_view_versions("driver_stats", "test_project") + assert len(versions) == 1 + assert versions[0]["version_number"] == 0 + + def test_no_promote_version_accessible_by_explicit_ref( + self, registry, make_fv, entity + ): + """After no_promote apply, the new version should be accessible via + get_feature_view_by_version().""" + fv0 = make_fv(description="original") + registry.apply_feature_view(fv0, "test_project", commit=True) + + # Schema change with no_promote + fv1 = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + Field(name="avg_rating", dtype=Float32), + Field(name="explicit_feature", dtype=Float32), + ], + description="staged v1", + ) + registry.apply_feature_view(fv1, "test_project", commit=True, no_promote=True) + + # Should be accessible by explicit version ref + v1_fv = registry.get_feature_view_by_version("driver_stats", "test_project", 1) + assert v1_fv.current_version_number == 1 + feature_names = {f.name for f in v1_fv.schema} + assert "explicit_feature" in feature_names + + # v0 should also still be accessible + v0_fv = registry.get_feature_view_by_version("driver_stats", "test_project", 0) + assert v0_fv.current_version_number == 0 + feature_names_v0 = {f.name for f in v0_fv.schema} + assert "explicit_feature" not in feature_names_v0 + + +class TestFeatureViewNameValidation: + """Tests that feature view names with reserved characters are rejected on apply.""" + + def test_apply_feature_view_with_at_sign_raises(self, registry, entity): + """Applying a feature view with '@' in its name should raise ValueError.""" + fv = FeatureView( + name="my_weirdly_@_named_fv", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + ], + ) + with pytest.raises(ValueError, match="must not contain '@'"): + registry.apply_feature_view(fv, "test_project", commit=True) + + def test_apply_feature_view_with_colon_raises(self, registry, entity): + """Applying a feature view with ':' in its name should raise ValueError.""" + fv = FeatureView( + name="my:weird:fv", + entities=[entity], + ttl=timedelta(days=1), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="trips_today", dtype=Int64), + ], + ) + with pytest.raises(ValueError, match="must not contain ':'"): + registry.apply_feature_view(fv, "test_project", commit=True) diff --git a/sdk/python/tests/integration/rest_api/conftest.py b/sdk/python/tests/integration/rest_api/conftest.py new file mode 100644 index 00000000000..d7aa0b4a3c5 --- /dev/null +++ b/sdk/python/tests/integration/rest_api/conftest.py @@ -0,0 +1,182 @@ +import os +import time +from pathlib import Path + +import pytest +import requests +from kubernetes import client, config + +from tests.integration.rest_api.support import ( + applyFeastProject, + create_feast_project, + create_namespace, + create_route, + delete_namespace, + deploy_and_validate_pod, + execPodCommand, + get_pod_name_by_prefix, + run_kubectl_command, + validate_feature_store_cr_status, +) + + +class FeastRestClient: + def __init__(self, base_url): + self.base_url = base_url.rstrip("/") + self.api_prefix = "/api/v1" + + def _build_url(self, endpoint): + if not endpoint.startswith("/"): + endpoint = "/" + endpoint + return f"{self.base_url}{self.api_prefix}{endpoint}" + + def get(self, endpoint, params=None): + params = params or {} + params.setdefault("allow_cache", "false") + url = self._build_url(endpoint) + return requests.get(url, params=params, verify=False) + + +def _wait_for_http_ready( + route_url: str, + timeout: int = 300, + interval: int = 5, + initial_delay: int = 30, +) -> None: + """ + Poll the HTTP endpoint until it returns a non-502 response. + + After Pod/CR readiness is confirmed, the backend behind the ingress may + still be initializing. This helper avoids the race condition where tests + start before the Feast server is ready, causing all requests to return 502. + """ + health_url = f"{route_url}/api/v1/projects" + last_status = None + + if initial_delay > 0: + print( + f"\n Waiting {initial_delay}s for backend to start after apply/dataset creation..." + ) + time.sleep(initial_delay) + + deadline = time.time() + timeout + print( + f"\n Waiting for HTTP endpoint to become ready (timeout={timeout}s): {health_url}" + ) + + while time.time() < deadline: + try: + resp = requests.get(health_url, timeout=10, verify=False) + last_status = resp.status_code + if resp.status_code != 502: + print(f" HTTP endpoint is ready (status={resp.status_code})") + return + print( + f" HTTP endpoint returned {resp.status_code}, retrying in {interval}s..." + ) + except requests.exceptions.RequestException as exc: + last_status = str(exc) + print(f" HTTP request failed ({exc}), retrying in {interval}s...") + + time.sleep(interval) + + raise RuntimeError( + f"HTTP endpoint {health_url} did not become ready within {timeout}s " + f"(last status: {last_status})" + ) + + +@pytest.fixture(scope="session") +def feast_rest_client(): + # Load kubeconfig and initialize Kubernetes client + config.load_kube_config() + api_instance = client.CoreV1Api() + + # Get the directory containing this conftest.py file + test_dir = Path(__file__).parent + resource_dir = test_dir / "resource" + + # Constants and environment values + namespace = "test-ns-feast-rest" + credit_scoring = "credit-scoring" + driver_ranking = "driver-ranking" + # Registry REST service name created by the operator for credit-scoring (kind and OpenShift) + registry_rest_service = "feast-credit-scoring-registry-rest" + run_on_openshift = os.getenv("RUN_ON_OPENSHIFT_CI", "false").lower() == "true" + + # Create test namespace + create_namespace(api_instance, namespace) + + try: + # Deploy dependencies (same for kind and OpenShift) + deploy_and_validate_pod( + namespace, str(resource_dir / "redis.yaml"), "app=redis" + ) + deploy_and_validate_pod( + namespace, str(resource_dir / "postgres.yaml"), "app=postgres" + ) + + # Create and validate FeatureStore CRs (SQL registry, same as kind) + create_feast_project( + str(resource_dir / "feast_config_credit_scoring.yaml"), + namespace, + credit_scoring, + ) + validate_feature_store_cr_status(namespace, credit_scoring) + + create_feast_project( + str(resource_dir / "feast_config_driver_ranking.yaml"), + namespace, + driver_ranking, + ) + validate_feature_store_cr_status(namespace, driver_ranking) + + if run_on_openshift: + # OpenShift: expose registry REST via route (no nginx ingress) + route_url = create_route(namespace, credit_scoring, registry_rest_service) + else: + # Kind: deploy nginx ingress and get route URL + run_kubectl_command( + [ + "apply", + "-f", + str(resource_dir / "feast-registry-nginx.yaml"), + "-n", + namespace, + ] + ) + ingress_host = run_kubectl_command( + [ + "get", + "ingress", + "feast-registry-ingress", + "-n", + namespace, + "-o", + "jsonpath={.spec.rules[0].host}", + ] + ) + route_url = f"http://{ingress_host}" + + # Apply feast projects + applyFeastProject(namespace, credit_scoring) + applyFeastProject(namespace, driver_ranking) + + # Create Saved Datasets and Permissions + pod_name = get_pod_name_by_prefix(namespace, credit_scoring) + execPodCommand(namespace, pod_name, ["python", "create_ui_visible_datasets.py"]) + execPodCommand(namespace, pod_name, ["python", "permissions_apply.py"]) + if not route_url: + raise RuntimeError("Route URL could not be fetched.") + + # Wait for the HTTP endpoint to become ready before running tests. + # Pod/CR readiness does not guarantee the backend is serving traffic; + # the ingress may return 502 while the Feast server is still starting. + _wait_for_http_ready(route_url) + + print(f"\n Connected to Feast REST at: {route_url}") + yield FeastRestClient(route_url) + + finally: + print(f"\n Deleting namespace: {namespace}") + delete_namespace(api_instance, namespace) diff --git a/sdk/python/tests/registry_rest_api_tests/resource/feast-registry-nginx.yaml b/sdk/python/tests/integration/rest_api/resource/feast-registry-nginx.yaml similarity index 100% rename from sdk/python/tests/registry_rest_api_tests/resource/feast-registry-nginx.yaml rename to sdk/python/tests/integration/rest_api/resource/feast-registry-nginx.yaml diff --git a/sdk/python/tests/registry_rest_api_tests/resource/feast_config_credit_scoring.yaml b/sdk/python/tests/integration/rest_api/resource/feast_config_credit_scoring.yaml similarity index 100% rename from sdk/python/tests/registry_rest_api_tests/resource/feast_config_credit_scoring.yaml rename to sdk/python/tests/integration/rest_api/resource/feast_config_credit_scoring.yaml diff --git a/sdk/python/tests/registry_rest_api_tests/resource/feast_config_driver_ranking.yaml b/sdk/python/tests/integration/rest_api/resource/feast_config_driver_ranking.yaml similarity index 100% rename from sdk/python/tests/registry_rest_api_tests/resource/feast_config_driver_ranking.yaml rename to sdk/python/tests/integration/rest_api/resource/feast_config_driver_ranking.yaml diff --git a/sdk/python/tests/registry_rest_api_tests/resource/feast_config_rhoai.yaml b/sdk/python/tests/integration/rest_api/resource/feast_config_rhoai.yaml similarity index 100% rename from sdk/python/tests/registry_rest_api_tests/resource/feast_config_rhoai.yaml rename to sdk/python/tests/integration/rest_api/resource/feast_config_rhoai.yaml diff --git a/sdk/python/tests/registry_rest_api_tests/resource/postgres.yaml b/sdk/python/tests/integration/rest_api/resource/postgres.yaml similarity index 92% rename from sdk/python/tests/registry_rest_api_tests/resource/postgres.yaml rename to sdk/python/tests/integration/rest_api/resource/postgres.yaml index ca0de81d15c..34ac2eb620b 100644 --- a/sdk/python/tests/registry_rest_api_tests/resource/postgres.yaml +++ b/sdk/python/tests/integration/rest_api/resource/postgres.yaml @@ -25,7 +25,7 @@ spec: spec: containers: - name: postgres - image: 'postgres:16-alpine' + image: 'quay.io/feastdev-ci/feast-test-images:postgres-17-alpine' ports: - containerPort: 5432 envFrom: diff --git a/sdk/python/tests/registry_rest_api_tests/resource/redis.yaml b/sdk/python/tests/integration/rest_api/resource/redis.yaml similarity index 83% rename from sdk/python/tests/registry_rest_api_tests/resource/redis.yaml rename to sdk/python/tests/integration/rest_api/resource/redis.yaml index 297205b8a79..df8187daeec 100644 --- a/sdk/python/tests/registry_rest_api_tests/resource/redis.yaml +++ b/sdk/python/tests/integration/rest_api/resource/redis.yaml @@ -15,7 +15,8 @@ spec: spec: containers: - name: redis - image: 'quay.io/sclorg/redis-7-c9s' + image: 'quay.io/feastdev-ci/feast-test-images:redis-7-alpine' + command: ["redis-server", "--save", ""] ports: - containerPort: 6379 env: diff --git a/sdk/python/tests/registry_rest_api_tests/support.py b/sdk/python/tests/integration/rest_api/support.py similarity index 100% rename from sdk/python/tests/registry_rest_api_tests/support.py rename to sdk/python/tests/integration/rest_api/support.py diff --git a/sdk/python/tests/registry_rest_api_tests/test_feast_registry.py b/sdk/python/tests/integration/rest_api/test_registry_rest_api.py similarity index 84% rename from sdk/python/tests/registry_rest_api_tests/test_feast_registry.py rename to sdk/python/tests/integration/rest_api/test_registry_rest_api.py index 04e86618cf7..5d37d700a64 100644 --- a/sdk/python/tests/registry_rest_api_tests/test_feast_registry.py +++ b/sdk/python/tests/integration/rest_api/test_registry_rest_api.py @@ -13,7 +13,7 @@ # Test Configuration Constants @dataclass(frozen=True) -class TestConfig: +class RegistryTestConfig: """Configuration constants for registry REST API tests.""" CREDIT_SCORING_PROJECT = "credit_scoring_local" @@ -103,7 +103,7 @@ def validate_entity_structure(entity: Dict[str, Any]) -> None: assert "lastUpdatedTimestamp" in meta assert isinstance(entity["project"], str) - assert entity["project"] in TestConfig.PROJECT_NAMES + assert entity["project"] in RegistryTestConfig.PROJECT_NAMES @staticmethod def validate_feature_structure(feature: Dict[str, Any]) -> None: @@ -132,6 +132,7 @@ def validate_batch_source(batch_source: Dict[str, Any]) -> None: assert batch_source.get("type") == "BATCH_FILE" +@pytest.mark.integration @pytest.mark.skipif( not os.path.exists(os.path.expanduser("~/.kube/config")), reason="Kube config not available in this environment", @@ -143,7 +144,7 @@ class TestRegistryServerRest: def test_list_entities(self, feast_rest_client): """Test listing entities for a specific project.""" response = feast_rest_client.get( - f"/entities/?project={TestConfig.CREDIT_SCORING_PROJECT}" + f"/entities/?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) @@ -151,21 +152,21 @@ def test_list_entities(self, feast_rest_client): assert "entities" in data entities = data["entities"] assert isinstance(entities, list) - assert len(entities) == TestConfig.CREDIT_SCORING_ENTITIES_COUNT + assert len(entities) == RegistryTestConfig.CREDIT_SCORING_ENTITIES_COUNT # Validate entity names actual_entity_names = {entity["spec"]["name"] for entity in entities} - assert actual_entity_names == TestConfig.ENTITY_NAMES + assert actual_entity_names == RegistryTestConfig.ENTITY_NAMES # Validate pagination APITestHelpers.validate_pagination( - data, TestConfig.CREDIT_SCORING_ENTITIES_COUNT + data, RegistryTestConfig.CREDIT_SCORING_ENTITIES_COUNT ) def test_get_entity(self, feast_rest_client): """Test getting a specific entity with detailed validation.""" response = feast_rest_client.get( - f"/entities/zipcode/?project={TestConfig.CREDIT_SCORING_PROJECT}" + f"/entities/zipcode/?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) @@ -178,7 +179,7 @@ def test_get_entity(self, feast_rest_client): spec["description"] == "ZIP code identifier for geographic location-based features" ) - assert spec["tags"] == TestConfig.ZIPCODE_SPEC_TAGS + assert spec["tags"] == RegistryTestConfig.ZIPCODE_SPEC_TAGS # Validate meta meta = data["meta"] @@ -215,22 +216,22 @@ def test_entities_all(self, feast_rest_client): def test_list_data_sources(self, feast_rest_client): """Test listing data sources for a specific project.""" response = feast_rest_client.get( - f"/data_sources/?project={TestConfig.CREDIT_SCORING_PROJECT}" + f"/data_sources/?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) assert "dataSources" in data data_sources = data["dataSources"] - assert len(data_sources) == TestConfig.CREDIT_SCORING_DATA_SOURCES_COUNT + assert len(data_sources) == RegistryTestConfig.CREDIT_SCORING_DATA_SOURCES_COUNT APITestHelpers.validate_pagination( - data, TestConfig.CREDIT_SCORING_DATA_SOURCES_COUNT + data, RegistryTestConfig.CREDIT_SCORING_DATA_SOURCES_COUNT ) def test_get_data_sources(self, feast_rest_client): """Test getting a specific data source.""" response = feast_rest_client.get( - f"/data_sources/Zipcode source/?project={TestConfig.CREDIT_SCORING_PROJECT}" + f"/data_sources/Zipcode source/?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) @@ -254,7 +255,7 @@ def test_data_sources_all(self, feast_rest_client): # Validate project associations for relevant data source types for ds in data_sources: if ds["type"] in ("BATCH_FILE", "REQUEST_SOURCE"): - assert ds["project"] in TestConfig.PROJECT_NAMES + assert ds["project"] in RegistryTestConfig.PROJECT_NAMES pagination = data.get("pagination", {}) assert pagination.get("page") == 1 @@ -266,12 +267,15 @@ def test_data_sources_all(self, feast_rest_client): def test_list_feature_services(self, feast_rest_client): """Test listing feature services for a specific project.""" response = feast_rest_client.get( - f"/feature_services/?project={TestConfig.DRIVER_RANKING_PROJECT}" + f"/feature_services/?project={RegistryTestConfig.DRIVER_RANKING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) feature_services = data.get("featureServices", []) - assert len(feature_services) == TestConfig.DRIVER_RANKING_FEATURE_SERVICES_COUNT + assert ( + len(feature_services) + == RegistryTestConfig.DRIVER_RANKING_FEATURE_SERVICES_COUNT + ) # Validate batch sources in features for fs in feature_services: @@ -288,7 +292,7 @@ def test_feature_services_all(self, feast_rest_client): assert len(feature_services) >= 1 for fs in feature_services: - assert fs.get("project") in TestConfig.PROJECT_NAMES + assert fs.get("project") in RegistryTestConfig.PROJECT_NAMES # Validate features structure spec = fs.get("spec", {}) @@ -299,7 +303,7 @@ def test_feature_services_all(self, feast_rest_client): def test_get_feature_services(self, feast_rest_client): """Test getting a specific feature service.""" response = feast_rest_client.get( - f"/feature_services/driver_activity_v2/?project={TestConfig.DRIVER_RANKING_PROJECT}" + f"/feature_services/driver_activity_v2/?project={RegistryTestConfig.DRIVER_RANKING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) @@ -313,21 +317,22 @@ def test_get_feature_services(self, feast_rest_client): def test_list_feature_views(self, feast_rest_client): """Test listing feature views for a specific project.""" response = feast_rest_client.get( - f"/feature_views/?project={TestConfig.CREDIT_SCORING_PROJECT}" + f"/feature_views/?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) assert ( - len(data["featureViews"]) == TestConfig.CREDIT_SCORING_FEATURE_VIEWS_COUNT + len(data["featureViews"]) + == RegistryTestConfig.CREDIT_SCORING_FEATURE_VIEWS_COUNT ) APITestHelpers.validate_pagination( - data, TestConfig.CREDIT_SCORING_FEATURE_VIEWS_COUNT + data, RegistryTestConfig.CREDIT_SCORING_FEATURE_VIEWS_COUNT ) def test_get_feature_view(self, feast_rest_client): """Test getting a specific feature view.""" response = feast_rest_client.get( - f"/feature_views/credit_history/?project={TestConfig.CREDIT_SCORING_PROJECT}" + f"/feature_views/credit_history/?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) @@ -351,26 +356,26 @@ def test_feature_views_all(self, feast_rest_client): def test_list_features(self, feast_rest_client): """Test listing features for a specific project.""" response = feast_rest_client.get( - f"/features/?project={TestConfig.CREDIT_SCORING_PROJECT}&include_relationships=true" + f"/features/?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}&include_relationships=true" ) data = APITestHelpers.validate_response_success(response) features = data.get("features") assert isinstance(features, list) - assert len(features) == TestConfig.CREDIT_SCORING_FEATURES_COUNT + assert len(features) == RegistryTestConfig.CREDIT_SCORING_FEATURES_COUNT # Validate each feature structure for feature in features: APITestHelpers.validate_feature_structure(feature) APITestHelpers.validate_pagination( - data, TestConfig.CREDIT_SCORING_FEATURES_COUNT + data, RegistryTestConfig.CREDIT_SCORING_FEATURES_COUNT ) def test_get_feature(self, feast_rest_client): """Test getting a specific feature.""" response = feast_rest_client.get( - f"/features/zipcode_features/city/?project={TestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" + f"/features/zipcode_features/city/?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" ) data = APITestHelpers.validate_response_success(response) @@ -396,14 +401,17 @@ def test_features_all(self, feast_rest_client): # Validate expected projects are present actual_projects = set(f["project"] for f in features) - assert TestConfig.PROJECT_NAMES.issubset(actual_projects) + assert RegistryTestConfig.PROJECT_NAMES.issubset(actual_projects) APITestHelpers.validate_pagination_all_endpoint(data, "features") # Project Tests @pytest.mark.parametrize( "project_name", - [TestConfig.CREDIT_SCORING_PROJECT, TestConfig.DRIVER_RANKING_PROJECT], + [ + RegistryTestConfig.CREDIT_SCORING_PROJECT, + RegistryTestConfig.DRIVER_RANKING_PROJECT, + ], ) def test_get_project_by_name(self, feast_rest_client, project_name): """Test getting a project by name.""" @@ -420,13 +428,13 @@ def test_get_projects_list(self, feast_rest_client): assert len(projects) == 2 actual_project_names = [project["spec"]["name"] for project in projects] - assert set(actual_project_names) == TestConfig.PROJECT_NAMES + assert set(actual_project_names) == RegistryTestConfig.PROJECT_NAMES # Lineage Tests def test_get_registry_lineage(self, feast_rest_client): """Test getting registry lineage for a specific project.""" response = feast_rest_client.get( - f"/lineage/registry?project={TestConfig.CREDIT_SCORING_PROJECT}" + f"/lineage/registry?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) @@ -448,11 +456,11 @@ def test_get_registry_lineage(self, feast_rest_client): def test_get_lineage_complete(self, feast_rest_client): """Test getting complete lineage for a specific project.""" response = feast_rest_client.get( - f"/lineage/complete?project={TestConfig.CREDIT_SCORING_PROJECT}" + f"/lineage/complete?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) - assert data.get("project") == TestConfig.CREDIT_SCORING_PROJECT + assert data.get("project") == RegistryTestConfig.CREDIT_SCORING_PROJECT assert "objects" in data objects = data["objects"] @@ -511,12 +519,12 @@ def test_get_registry_complete_all(self, feast_rest_client): assert len(data["projects"]) > 0 project_names = [project["project"] for project in data.get("projects", [])] - assert TestConfig.CREDIT_SCORING_PROJECT in project_names + assert RegistryTestConfig.CREDIT_SCORING_PROJECT in project_names def test_get_lineage_object_path(self, feast_rest_client): """Test getting lineage for a specific object.""" response = feast_rest_client.get( - f"/lineage/objects/entity/dob_ssn?project={TestConfig.CREDIT_SCORING_PROJECT}" + f"/lineage/objects/entity/dob_ssn?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}" ) data = APITestHelpers.validate_response_success(response) @@ -541,7 +549,7 @@ def test_get_lineage_object_path(self, feast_rest_client): def test_saved_datasets_endpoints(self, feast_rest_client, endpoint, key): """Test saved datasets endpoints with parameterization.""" if endpoint == "/saved_datasets": - url = f"{endpoint}?project={TestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" + url = f"{endpoint}?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" else: url = f"{endpoint}?allow_cache=true&page=1&limit=50&sort_order=asc&include_relationships=false" @@ -555,11 +563,13 @@ def test_saved_datasets_endpoints(self, feast_rest_client, endpoint, key): # Extract and validate names actual_names = [ds["spec"]["name"] for ds in saved_datasets] APITestHelpers.validate_names_match( - actual_names, TestConfig.SAVED_DATASET_NAMES + actual_names, RegistryTestConfig.SAVED_DATASET_NAMES ) # Validate pagination - APITestHelpers.validate_pagination(data, TestConfig.SAVED_DATASETS_COUNT) + APITestHelpers.validate_pagination( + data, RegistryTestConfig.SAVED_DATASETS_COUNT + ) if endpoint == "/saved_datasets/all": assert data["pagination"]["page"] == 1 assert data["pagination"]["limit"] == 50 @@ -568,7 +578,7 @@ def test_get_saved_datasets_by_name(self, feast_rest_client): """Test getting a specific saved dataset by name.""" dataset_name = "comprehensive_credit_dataset_v1" response = feast_rest_client.get( - f"/saved_datasets/{dataset_name}?project={TestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" + f"/saved_datasets/{dataset_name}?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" ) data = APITestHelpers.validate_response_success(response) @@ -580,14 +590,14 @@ def test_get_saved_datasets_by_name(self, feast_rest_client): def test_get_permission_by_name(self, feast_rest_client): """Test getting a specific permission by name.""" response = feast_rest_client.get( - f"/permissions/feast_admin_permission?project={TestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" + f"/permissions/feast_admin_permission?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" ) APITestHelpers.validate_response_success(response) def test_list_permissions(self, feast_rest_client): """Test listing permissions for a specific project.""" response = feast_rest_client.get( - f"/permissions?project={TestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" + f"/permissions?project={RegistryTestConfig.CREDIT_SCORING_PROJECT}&include_relationships=false" ) data = APITestHelpers.validate_response_success(response) @@ -595,9 +605,9 @@ def test_list_permissions(self, feast_rest_client): # Extract and validate names actual_names = [ds["spec"]["name"] for ds in data["permissions"]] - assert len(actual_names) == len(TestConfig.PERMISSION_NAMES) + assert len(actual_names) == len(RegistryTestConfig.PERMISSION_NAMES) - for name in TestConfig.PERMISSION_NAMES: + for name in RegistryTestConfig.PERMISSION_NAMES: assert name in actual_names - APITestHelpers.validate_pagination(data, TestConfig.PERMISSIONS_COUNT) + APITestHelpers.validate_pagination(data, RegistryTestConfig.PERMISSIONS_COUNT) diff --git a/sdk/python/tests/integration/test_mcp_feature_server.py b/sdk/python/tests/integration/test_mcp_feature_server.py index 6920c2e3f2a..61f45e37314 100644 --- a/sdk/python/tests/integration/test_mcp_feature_server.py +++ b/sdk/python/tests/integration/test_mcp_feature_server.py @@ -4,10 +4,13 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient +from pydantic import ValidationError from feast.feature_store import FeatureStore from feast.infra.mcp_servers.mcp_config import McpFeatureServerConfig +pytestmark = pytest.mark.integration + class TestMCPFeatureServerIntegration(unittest.TestCase): """Integration tests for MCP feature server functionality.""" @@ -49,7 +52,7 @@ def test_mcp_server_functionality_with_mock_store(self): mcp_server_version="1.0.0", ) - mock_mcp_instance = Mock() + mock_mcp_instance = Mock(spec_set=["mount_sse", "mount_http", "mount"]) mock_fast_api_mcp.return_value = mock_mcp_instance result = add_mcp_support_to_app(mock_app, mock_store, config) @@ -58,7 +61,7 @@ def test_mcp_server_functionality_with_mock_store(self): self.assertIsNotNone(result) self.assertEqual(result, mock_mcp_instance) mock_fast_api_mcp.assert_called_once() - mock_mcp_instance.mount.assert_called_once() + mock_mcp_instance.mount_sse.assert_called_once() @patch("feast.infra.mcp_servers.mcp_server.MCP_AVAILABLE", True) @patch("feast.infra.mcp_servers.mcp_server.FastApiMCP") @@ -77,7 +80,7 @@ def test_complete_mcp_setup_flow(self, mock_fast_api_mcp): transformation_service_endpoint="localhost:6566", ) - mock_mcp_instance = Mock() + mock_mcp_instance = Mock(spec_set=["mount_sse", "mount_http", "mount"]) mock_fast_api_mcp.return_value = mock_mcp_instance # Execute the flow @@ -90,7 +93,7 @@ def test_complete_mcp_setup_flow(self, mock_fast_api_mcp): name="e2e-test-server", description="Feast Feature Store MCP Server - Access feature store data and operations through MCP", ) - mock_mcp_instance.mount.assert_called_once() + mock_mcp_instance.mount_sse.assert_called_once() self.assertEqual(result, mock_mcp_instance) @pytest.mark.skipif( @@ -160,36 +163,29 @@ def test_feature_server_with_mcp_config(self): def test_mcp_server_configuration_validation(self): """Test comprehensive MCP server configuration validation.""" # Test various configuration combinations - test_configs = [ - { - "enabled": True, - "mcp_enabled": True, - "mcp_server_name": "test-server-1", - "mcp_server_version": "1.0.0", - "mcp_transport": "sse", - }, - { - "enabled": True, - "mcp_enabled": True, - "mcp_server_name": "test-server-2", - "mcp_server_version": "2.0.0", - "mcp_transport": "websocket", - }, - { - "enabled": False, - "mcp_enabled": False, - "mcp_server_name": "disabled-server", - "mcp_server_version": "1.0.0", - "mcp_transport": None, - }, - ] - - for config_dict in test_configs: - config = McpFeatureServerConfig(**config_dict) - self.assertEqual(config.enabled, config_dict["enabled"]) - self.assertEqual(config.mcp_enabled, config_dict["mcp_enabled"]) - self.assertEqual(config.mcp_server_name, config_dict["mcp_server_name"]) - self.assertEqual( - config.mcp_server_version, config_dict["mcp_server_version"] + for transport in ["sse", "http"]: + config = McpFeatureServerConfig( + enabled=True, + mcp_enabled=True, + mcp_server_name="test-server", + mcp_server_version="1.0.0", + mcp_transport=transport, + ) + self.assertEqual(config.mcp_transport, transport) + + config_default = McpFeatureServerConfig( + enabled=True, + mcp_enabled=True, + mcp_server_name="test-server-default", + mcp_server_version="1.0.0", + ) + self.assertEqual(config_default.mcp_transport, "sse") + + with self.assertRaises(ValidationError): + McpFeatureServerConfig( + enabled=True, + mcp_enabled=True, + mcp_server_name="bad-transport", + mcp_server_version="1.0.0", + mcp_transport="websocket", ) - self.assertEqual(config.mcp_transport, config_dict["mcp_transport"]) diff --git a/sdk/python/tests/integration/test_mlflow_integration.py b/sdk/python/tests/integration/test_mlflow_integration.py new file mode 100644 index 00000000000..c09e7d444f2 --- /dev/null +++ b/sdk/python/tests/integration/test_mlflow_integration.py @@ -0,0 +1,1116 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from datetime import datetime, timedelta + +import numpy as np +import pandas as pd +import pytest + +from feast import Entity, FeatureService, FeatureStore, FeatureView, Field, FileSource +from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.repo_config import RepoConfig +from feast.types import Float32, Int64 + +mlflow = pytest.importorskip("mlflow", reason="mlflow is not installed") +from mlflow.tracking import MlflowClient # noqa: E402 + +import feast.mlflow # noqa: E402 +from feast.mlflow_integration import ( # noqa: E402 + FeastMlflowEntityDfError, + FeastMlflowModelResolutionError, + MlflowConfig, +) +from feast.mlflow_integration.config import ( # noqa: E402 + MLFLOW_TAG_TRUNCATION_LIMIT, + resolve_tracking_uri, +) + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def _isolate_mlflow_globals(): + """Reset module-level mlflow caching between tests.""" + feast.mlflow._client = None + feast.mlflow._registered_store = None + yield + + +@pytest.fixture() +def tracking_uri(tmp_path): + uri = f"sqlite:///{tmp_path}/mlflow.db" + mlflow.set_tracking_uri(uri) + mlflow.set_experiment("test_mlflow") + yield uri + mlflow.set_tracking_uri("") + + +@pytest.fixture() +def driver_parquet(tmp_path): + data_dir = tmp_path / "data" + data_dir.mkdir() + + end = datetime.now().replace(microsecond=0, second=0, minute=0) + start = end - timedelta(days=7) + timestamps = pd.date_range(start, end, freq="h") + driver_ids = [1001, 1002, 1003] + + np.random.seed(42) + rows = [ + { + "driver_id": did, + "event_timestamp": ts, + "created": ts, + "conv_rate": float(np.random.uniform(0, 1)), + "acc_rate": float(np.random.uniform(0, 1)), + "avg_daily_trips": int(np.random.randint(1, 100)), + } + for ts in timestamps + for did in driver_ids + ] + df = pd.DataFrame(rows) + path = str(data_dir / "driver_stats.parquet") + df.to_parquet(path) + return tmp_path, path + + +@pytest.fixture() +def feast_objects(driver_parquet): + _, parquet_path = driver_parquet + + driver = Entity(name="driver", join_keys=["driver_id"]) + source = FileSource( + name="driver_stats_source", + path=parquet_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=7), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=source, + ) + fs = FeatureService(name="driver_activity_v1", features=[fv]) + return driver, source, fv, fs + + +def _make_store( + tmp_path, + tracking_uri, + *, + enabled=True, + auto_log=True, + auto_log_entity_df=True, + entity_df_max_rows=100_000, +): + data_dir = tmp_path / "data" + data_dir.mkdir(exist_ok=True) + + config = RepoConfig( + project="test_mlflow", + provider="local", + registry=str(data_dir / "registry.db"), + online_store=SqliteOnlineStoreConfig(path=str(data_dir / "online.db")), + entity_key_serialization_version=3, + mlflow=MlflowConfig( + enabled=enabled, + tracking_uri=tracking_uri, + auto_log=auto_log, + auto_log_entity_df=auto_log_entity_df, + entity_df_max_rows=entity_df_max_rows, + ), + ) + return FeatureStore(config=config) + + +@pytest.fixture() +def store_enabled(driver_parquet, tracking_uri, feast_objects): + tmp_path, _ = driver_parquet + store = _make_store(tmp_path, tracking_uri) + store.apply(list(feast_objects)) + store.materialize( + start_date=datetime.now() - timedelta(days=7), + end_date=datetime.now(), + ) + return store + + +@pytest.fixture() +def entity_df(): + np.random.seed(42) + n = 50 + return pd.DataFrame( + { + "driver_id": np.random.choice([1001, 1002, 1003], n), + "event_timestamp": [ + datetime.now() - timedelta(hours=i % 48) for i in range(n) + ], + "label": np.random.randint(0, 2, n), + } + ) + + +class TestMlflowConfig: + @pytest.mark.integration + def test_defaults(self): + cfg = MlflowConfig() + assert cfg.enabled is False + assert cfg.auto_log is True + assert cfg.auto_log_entity_df is False + assert cfg.entity_df_max_rows == 100_000 + assert cfg.tracking_uri is None + + @pytest.mark.integration + def test_get_tracking_uri_explicit(self): + cfg = MlflowConfig(tracking_uri="http://example.com:5000") + assert cfg.get_tracking_uri() == "http://example.com:5000" + + @pytest.mark.integration + def test_get_tracking_uri_env_fallback(self, monkeypatch): + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://env-uri:5000") + cfg = MlflowConfig() + assert cfg.get_tracking_uri() == "http://env-uri:5000" + + @pytest.mark.integration + def test_get_tracking_uri_none_when_unset(self, monkeypatch): + monkeypatch.delenv("MLFLOW_TRACKING_URI", raising=False) + cfg = MlflowConfig() + assert cfg.get_tracking_uri() is None + + @pytest.mark.integration + def test_resolve_tracking_uri_priority(self, monkeypatch): + monkeypatch.setenv("MLFLOW_TRACKING_URI", "http://env:5000") + assert resolve_tracking_uri("http://explicit:5000") == "http://explicit:5000" + assert resolve_tracking_uri(None) == "http://env:5000" + monkeypatch.delenv("MLFLOW_TRACKING_URI") + assert resolve_tracking_uri(None) is None + + +class TestLogFeatureRetrieval: + @pytest.mark.integration + def test_logs_all_tags_and_metric(self, store_enabled, tracking_uri): + client = MlflowClient(tracking_uri=tracking_uri) + refs = [ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "transformed:conv_rate_plus_val1", + ] + + with mlflow.start_run(run_name="test_tags") as run: + result = store_enabled.mlflow.log_feature_retrieval( + feature_refs=refs, + entity_count=200, + duration_seconds=0.1234, + retrieval_type="historical", + feature_service_name="driver_activity_v1", + ) + + assert result is True + data = client.get_run(run.info.run_id) + tags = data.data.tags + + assert tags["feast.project"] == "test_mlflow" + assert tags["feast.retrieval_type"] == "historical" + assert tags["feast.feature_service"] == "driver_activity_v1" + assert tags["feast.entity_count"] == "200" + assert tags["feast.feature_count"] == "3" + assert "driver_hourly_stats" in tags["feast.feature_views"] + assert "transformed" in tags["feast.feature_views"] + assert "driver_hourly_stats:conv_rate" in tags["feast.feature_refs"] + assert data.data.metrics["feast.job_submission_sec"] == 0.1234 + + @pytest.mark.integration + def test_noop_without_active_run(self, store_enabled, tracking_uri): + result = store_enabled.mlflow.log_feature_retrieval( + feature_refs=["fv:feat"], + entity_count=1, + duration_seconds=0.01, + ) + assert result is False + + @pytest.mark.integration + def test_feature_views_sorted_and_deduped(self, store_enabled, tracking_uri): + client = MlflowClient(tracking_uri=tracking_uri) + refs = ["z_view:f1", "a_view:f2", "z_view:f3", "a_view:f4"] + + with mlflow.start_run() as run: + store_enabled.mlflow.log_feature_retrieval( + feature_refs=refs, + entity_count=1, + duration_seconds=0.01, + ) + + tags = client.get_run(run.info.run_id).data.tags + assert tags["feast.feature_views"] == "a_view,z_view" + + @pytest.mark.integration + def test_truncation_for_long_refs(self, store_enabled, tracking_uri): + client = MlflowClient(tracking_uri=tracking_uri) + refs = [f"fv:feature_{i:04d}" for i in range(500)] + + with mlflow.start_run() as run: + store_enabled.mlflow.log_feature_retrieval( + feature_refs=refs, + entity_count=1, + duration_seconds=0.01, + ) + + tags = client.get_run(run.info.run_id).data.tags + assert len(tags["feast.feature_refs"]) <= MLFLOW_TAG_TRUNCATION_LIMIT + assert tags["feast.feature_refs"].endswith("...") + + @pytest.mark.integration + def test_no_feature_service_tag_when_none(self, store_enabled, tracking_uri): + client = MlflowClient(tracking_uri=tracking_uri) + + with mlflow.start_run() as run: + store_enabled.mlflow.log_feature_retrieval( + feature_refs=["fv:f1"], + entity_count=1, + duration_seconds=0.01, + feature_service=None, + feature_service_name=None, + ) + + tags = client.get_run(run.info.run_id).data.tags + assert "feast.feature_service" not in tags + + +class TestLogTrainingDataset: + @pytest.mark.integration + def test_logs_dataset_input(self, store_enabled, tracking_uri): + client = MlflowClient(tracking_uri=tracking_uri) + df = pd.DataFrame({"a": [1, 2], "b": [3, 4]}) + + with mlflow.start_run() as run: + result = store_enabled.mlflow.log_training_dataset( + df, dataset_name="test_ds" + ) + + assert result is True + run_data = client.get_run(run.info.run_id) + assert len(run_data.inputs.dataset_inputs) > 0 + assert run_data.inputs.dataset_inputs[0].dataset.name == "test_ds" + + @pytest.mark.integration + def test_noop_without_active_run(self, store_enabled, tracking_uri): + df = pd.DataFrame({"a": [1]}) + assert store_enabled.mlflow.log_training_dataset(df) is False + + +class TestHistoricalAutoLog: + @pytest.mark.integration + def test_tags_logged_via_feature_service( + self, store_enabled, entity_df, tracking_uri + ): + client = MlflowClient(tracking_uri=tracking_uri) + + with mlflow.start_run(run_name="hist_fs") as run: + store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + data = client.get_run(run.info.run_id) + tags = data.data.tags + + assert tags["feast.project"] == "test_mlflow" + assert tags["feast.retrieval_type"] == "historical" + assert tags["feast.feature_service"] == "driver_activity_v1" + assert "driver_hourly_stats" in tags["feast.feature_views"] + assert tags["feast.entity_count"] == str(len(entity_df)) + assert int(tags["feast.feature_count"]) >= 3 + assert data.data.metrics["feast.job_submission_sec"] >= 0 + + @pytest.mark.integration + def test_tags_logged_via_feature_refs(self, store_enabled, entity_df, tracking_uri): + client = MlflowClient(tracking_uri=tracking_uri) + + with mlflow.start_run(run_name="hist_refs") as run: + store_enabled.get_historical_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + ], + entity_df=entity_df, + ).to_df() + + tags = client.get_run(run.info.run_id).data.tags + assert tags["feast.retrieval_type"] == "historical" + assert tags["feast.feature_count"] == "2" + assert "driver_hourly_stats:conv_rate" in tags["feast.feature_refs"] + + @pytest.mark.integration + def test_entity_df_artifact_uploaded(self, store_enabled, entity_df, tracking_uri): + client = MlflowClient(tracking_uri=tracking_uri) + + with mlflow.start_run(run_name="hist_artifact") as run: + store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + artifacts = [a.path for a in client.list_artifacts(run.info.run_id)] + assert "entity_df.parquet" in artifacts + + tags = client.get_run(run.info.run_id).data.tags + assert tags["feast.entity_df_rows"] == str(len(entity_df)) + assert "driver_id" in tags["feast.entity_df_columns"] + assert "event_timestamp" in tags["feast.entity_df_columns"] + assert tags["feast.entity_df_type"] == "dataframe" + + @pytest.mark.integration + def test_entity_df_skipped_when_exceeds_max_rows( + self, driver_parquet, tracking_uri, feast_objects + ): + tmp_path, _ = driver_parquet + store = _make_store(tmp_path, tracking_uri, entity_df_max_rows=5) + store.apply(list(feast_objects)) + + client = MlflowClient(tracking_uri=tracking_uri) + entity_df = pd.DataFrame( + { + "driver_id": [1001] * 10, + "event_timestamp": [ + datetime.now() - timedelta(hours=i) for i in range(10) + ], + } + ) + + with mlflow.start_run(run_name="hist_skip") as run: + store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + artifacts = [a.path for a in client.list_artifacts(run.info.run_id)] + assert "entity_df.parquet" not in artifacts + tags = client.get_run(run.info.run_id).data.tags + assert tags["feast.entity_df_rows"] == "10" + + @pytest.mark.integration + def test_no_tags_without_active_run(self, store_enabled, entity_df): + result = store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + assert len(result) == len(entity_df) + + +class TestOnlineAutoLog: + @pytest.mark.integration + def test_tags_logged_for_online_retrieval(self, store_enabled, tracking_uri): + client = MlflowClient(tracking_uri=tracking_uri) + + with mlflow.start_run(run_name="online") as run: + store_enabled.get_online_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_rows=[{"driver_id": 1001}, {"driver_id": 1002}], + ) + + data = client.get_run(run.info.run_id) + tags = data.data.tags + + assert tags["feast.retrieval_type"] == "online" + assert tags["feast.feature_service"] == "driver_activity_v1" + assert tags["feast.entity_count"] == "2" + assert int(tags["feast.feature_count"]) >= 3 + assert data.data.metrics["feast.job_submission_sec"] >= 0 + + @pytest.mark.integration + def test_entity_count_for_list_input(self, store_enabled, tracking_uri): + client = MlflowClient(tracking_uri=tracking_uri) + + with mlflow.start_run(run_name="online_list") as run: + store_enabled.get_online_features( + features=["driver_hourly_stats:conv_rate"], + entity_rows=[ + {"driver_id": 1001}, + {"driver_id": 1002}, + {"driver_id": 1003}, + ], + ) + + tags = client.get_run(run.info.run_id).data.tags + assert tags["feast.entity_count"] == "3" + + +class TestDisabledIntegration: + @pytest.mark.integration + def test_disabled_does_not_log( + self, driver_parquet, tracking_uri, feast_objects, entity_df + ): + tmp_path, _ = driver_parquet + store = _make_store(tmp_path, tracking_uri, enabled=False) + store.apply(list(feast_objects)) + + client = MlflowClient(tracking_uri=tracking_uri) + with mlflow.start_run(run_name="disabled") as run: + store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + tags = client.get_run(run.info.run_id).data.tags + assert "feast.project" not in tags + assert "feast.feature_service" not in tags + + @pytest.mark.integration + def test_auto_log_false_does_not_log( + self, driver_parquet, tracking_uri, feast_objects, entity_df + ): + tmp_path, _ = driver_parquet + store = _make_store(tmp_path, tracking_uri, enabled=True, auto_log=False) + store.apply(list(feast_objects)) + + client = MlflowClient(tracking_uri=tracking_uri) + with mlflow.start_run(run_name="no_auto_log") as run: + store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + tags = client.get_run(run.info.run_id).data.tags + assert "feast.project" not in tags + + @pytest.mark.integration + def test_auto_log_entity_df_false_skips_artifact( + self, driver_parquet, tracking_uri, feast_objects, entity_df + ): + tmp_path, _ = driver_parquet + store = _make_store(tmp_path, tracking_uri, auto_log_entity_df=False) + store.apply(list(feast_objects)) + + client = MlflowClient(tracking_uri=tracking_uri) + with mlflow.start_run(run_name="no_entity_df") as run: + store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + run_data = client.get_run(run.info.run_id).data + artifacts = [a.path for a in client.list_artifacts(run.info.run_id)] + assert "entity_df.parquet" not in artifacts + + assert "feast.entity_df_rows" in run_data.tags + assert run_data.tags["feast.entity_df_type"] == "dataframe" + assert run_data.tags["feast.feature_service"] == "driver_activity_v1" + + +class TestEntityDfBuilder: + @pytest.mark.integration + def test_roundtrip_parquet(self, store_enabled, entity_df, tracking_uri): + with mlflow.start_run(run_name="roundtrip") as run: + store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + recovered = store_enabled.mlflow.get_training_entity_df( + run_id=run.info.run_id, + ) + + assert recovered.shape == entity_df.shape + assert set(recovered.columns) == set(entity_df.columns) + assert "event_timestamp" in recovered.columns + + @pytest.mark.integration + def test_max_rows_limits_output(self, store_enabled, entity_df, tracking_uri): + with mlflow.start_run(run_name="max_rows") as run: + store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + recovered = store_enabled.mlflow.get_training_entity_df( + run_id=run.info.run_id, + max_rows=5, + ) + assert len(recovered) == 5 + + @pytest.mark.integration + def test_missing_artifact_raises(self, store_enabled, tracking_uri): + with mlflow.start_run(run_name="empty") as run: + mlflow.log_param("dummy", "value") + + with pytest.raises(FeastMlflowEntityDfError, match="No entity data found"): + store_enabled.mlflow.get_training_entity_df( + run_id=run.info.run_id, + ) + + @pytest.mark.integration + def test_nonexistent_run_raises(self, store_enabled, tracking_uri): + with pytest.raises(FeastMlflowEntityDfError, match="not found"): + store_enabled.mlflow.get_training_entity_df( + run_id="0000000000000000deadbeef00000000", + ) + + @pytest.mark.integration + def test_missing_timestamp_column_raises(self, store_enabled, tracking_uri): + df = pd.DataFrame({"driver_id": [1001], "value": [0.5]}) + with mlflow.start_run(run_name="bad_cols") as run: + import tempfile + + with tempfile.TemporaryDirectory() as tmp_dir: + path = os.path.join(tmp_dir, "entity_df.parquet") + df.to_parquet(path, index=False) + mlflow.log_artifact(path) + + with pytest.raises( + FeastMlflowEntityDfError, match="missing required timestamp" + ): + store_enabled.mlflow.get_training_entity_df( + run_id=run.info.run_id, + ) + + @pytest.mark.integration + def test_custom_timestamp_column(self, store_enabled, tracking_uri): + df = pd.DataFrame( + { + "driver_id": [1001], + "ts": [datetime.now()], + } + ) + with mlflow.start_run(run_name="custom_ts") as run: + import tempfile + + with tempfile.TemporaryDirectory() as tmp_dir: + path = os.path.join(tmp_dir, "entity_df.parquet") + df.to_parquet(path, index=False) + mlflow.log_artifact(path) + + recovered = store_enabled.mlflow.get_training_entity_df( + run_id=run.info.run_id, + timestamp_column="ts", + ) + assert len(recovered) == 1 + assert "ts" in recovered.columns + + +class TestModelResolver: + def _train_and_register(self, store, entity_df, tracking_uri, model_name): + """Train inside an mlflow run, log a model, register it.""" + from sklearn.linear_model import LogisticRegression + + with mlflow.start_run(run_name=f"train_{model_name}") as run: + store.get_historical_features( + features=store.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + model = LogisticRegression() + model.fit([[0, 0], [1, 1]], [0, 1]) + mlflow.sklearn.log_model(model, "model") + + mlflow.register_model(f"runs:/{run.info.run_id}/model", model_name) + return run.info.run_id + + @pytest.mark.integration + def test_resolve_from_run_tags(self, store_enabled, entity_df, tracking_uri): + self._train_and_register( + store_enabled, entity_df, tracking_uri, "test_resolve_run_tags" + ) + + fs_name = store_enabled.mlflow.resolve_features( + model_uri="models:/test_resolve_run_tags/1", + ) + assert fs_name == "driver_activity_v1" + + @pytest.mark.integration + def test_resolve_from_model_version_tag( + self, store_enabled, entity_df, tracking_uri, feast_objects + ): + self._train_and_register( + store_enabled, entity_df, tracking_uri, "test_resolve_mv_tag" + ) + + _, _, fv, _ = feast_objects + override_fs = FeatureService(name="overridden_service", features=[fv]) + store_enabled.apply([override_fs]) + + client = MlflowClient(tracking_uri=tracking_uri) + client.set_model_version_tag( + "test_resolve_mv_tag", "1", "feast.feature_service", "overridden_service" + ) + + fs_name = store_enabled.mlflow.resolve_features( + model_uri="models:/test_resolve_mv_tag/1", + ) + assert fs_name == "overridden_service" + + @pytest.mark.integration + def test_model_version_tag_takes_priority_over_run_tag( + self, store_enabled, entity_df, tracking_uri, feast_objects + ): + self._train_and_register( + store_enabled, entity_df, tracking_uri, "test_priority" + ) + + _, _, fv, _ = feast_objects + override_fs = FeatureService(name="explicit_override", features=[fv]) + store_enabled.apply([override_fs]) + + client = MlflowClient(tracking_uri=tracking_uri) + client.set_model_version_tag( + "test_priority", "1", "feast.feature_service", "explicit_override" + ) + + fs_name = store_enabled.mlflow.resolve_features( + model_uri="models:/test_priority/1", + ) + assert fs_name == "explicit_override" + + @pytest.mark.integration + def test_validates_feature_service_exists( + self, store_enabled, entity_df, tracking_uri + ): + self._train_and_register( + store_enabled, entity_df, tracking_uri, "test_validate_exists" + ) + + client = MlflowClient(tracking_uri=tracking_uri) + client.set_model_version_tag( + "test_validate_exists", + "1", + "feast.feature_service", + "nonexistent_service", + ) + + with pytest.raises( + FeastMlflowModelResolutionError, match="not found in the Feast registry" + ): + store_enabled.mlflow.resolve_features( + model_uri="models:/test_validate_exists/1", + ) + + @pytest.mark.integration + def test_invalid_uri_raises(self, store_enabled, tracking_uri): + with pytest.raises(FeastMlflowModelResolutionError, match="Invalid model_uri"): + store_enabled.mlflow.resolve_features( + model_uri="not-a-valid-uri", + ) + + @pytest.mark.integration + def test_nonexistent_model_raises(self, store_enabled, tracking_uri): + with pytest.raises(FeastMlflowModelResolutionError, match="Could not resolve"): + store_enabled.mlflow.resolve_features( + model_uri="models:/does_not_exist/1", + ) + + @pytest.mark.integration + def test_no_feast_tag_anywhere_raises(self, store_enabled, tracking_uri): + from sklearn.linear_model import LogisticRegression + + mlflow.set_experiment("test_mlflow") + with mlflow.start_run(run_name="no_feast_tags") as run: + model = LogisticRegression() + model.fit([[0], [1]], [0, 1]) + mlflow.sklearn.log_model(model, "model") + + mlflow.register_model(f"runs:/{run.info.run_id}/model", "test_no_feast_tag") + + with pytest.raises( + FeastMlflowModelResolutionError, + match="Could not determine feature service", + ): + store_enabled.mlflow.resolve_features( + model_uri="models:/test_no_feast_tag/1", + ) + + @pytest.mark.integration + def test_feature_mismatch_with_required_features_artifact( + self, store_enabled, entity_df, tracking_uri + ): + from sklearn.linear_model import LogisticRegression + + with mlflow.start_run(run_name="mismatch") as run: + store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + model = LogisticRegression() + model.fit([[0], [1]], [0, 1]) + mlflow.sklearn.log_model(model, "model") + + import tempfile + + with tempfile.TemporaryDirectory() as tmp_dir: + path = os.path.join(tmp_dir, "feast_features.json") + with open(path, "w") as f: + json.dump(["driver_hourly_stats:nonexistent_feature"], f) + mlflow.log_artifact(path) + + mlflow.register_model(f"runs:/{run.info.run_id}/model", "test_mismatch") + + with pytest.raises(FeastMlflowModelResolutionError, match="Feature mismatch"): + store_enabled.mlflow.resolve_features( + model_uri="models:/test_mismatch/1", + ) + + +class TestEndToEnd: + @pytest.mark.integration + def test_full_lifecycle(self, store_enabled, entity_df, tracking_uri): + from sklearn.linear_model import LogisticRegression + + with store_enabled.mlflow.start_run(run_name="e2e_train") as train_run: + training_df = store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + X = training_df[["conv_rate", "acc_rate", "avg_daily_trips"]].fillna(0) + y = entity_df["label"].values[: len(X)] + model = LogisticRegression().fit(X, y) + store_enabled.mlflow.log_model(model, "model") + + store_enabled.mlflow.register_model( + f"runs:/{train_run.info.run_id}/model", "e2e_model" + ) + + fs_name = store_enabled.mlflow.resolve_features( + model_uri="models:/e2e_model/1", + ) + assert fs_name == "driver_activity_v1" + + with store_enabled.mlflow.start_run(run_name="e2e_serve") as serve_run: + store_enabled.get_online_features( + features=store_enabled.get_feature_service(fs_name), + entity_rows=[{"driver_id": 1001}], + ) + + serve_tags = ( + MlflowClient(tracking_uri=tracking_uri) + .get_run(serve_run.info.run_id) + .data.tags + ) + assert serve_tags["feast.retrieval_type"] == "online" + assert serve_tags["feast.feature_service"] == "driver_activity_v1" + + recovered_df = store_enabled.mlflow.get_training_entity_df( + run_id=train_run.info.run_id, + ) + assert recovered_df.shape == entity_df.shape + + with store_enabled.mlflow.start_run(run_name="e2e_reproduce") as repro_run: + store_enabled.get_historical_features( + features=store_enabled.get_feature_service(fs_name), + entity_df=recovered_df, + ).to_df() + + repro_tags = ( + MlflowClient(tracking_uri=tracking_uri) + .get_run(repro_run.info.run_id) + .data.tags + ) + assert repro_tags["feast.feature_service"] == "driver_activity_v1" + assert repro_tags["feast.entity_count"] == str(len(entity_df)) + + +class TestLogApplyTransitionTypes: + """Tests for per-object transition type tags via store.mlflow.""" + + @pytest.mark.integration + def test_transition_tags_created(self, store_enabled, tracking_uri, feast_objects): + driver, source, fv, fs = feast_objects + transition_types = { + fv.name: "CREATE", + fs.name: "CREATE", + driver.name: "CREATE", + } + store_enabled.mlflow.log_apply( + changed_objects=[fv, fs, driver], + transition_types=transition_types, + ) + + client = MlflowClient(tracking_uri=tracking_uri) + exp = client.get_experiment_by_name("test_mlflow-feast-ops") + assert exp is not None + runs = client.search_runs(experiment_ids=[exp.experiment_id]) + assert len(runs) == 1 + + tags = runs[0].data.tags + assert tags["feast.operation"] == "apply" + assert tags["feast.feature_views_created"] == fv.name + assert tags["feast.feature_services_created"] == fs.name + assert tags["feast.entities_created"] == driver.name + assert "feast.feature_views_updated" not in tags + assert "feast.feature_views_deleted" not in tags + + @pytest.mark.integration + def test_transition_tags_mixed(self, store_enabled, tracking_uri, feast_objects): + driver, source, fv, fs = feast_objects + + fv2 = FeatureView( + name="driver_daily_stats", + entities=[driver], + ttl=timedelta(days=1), + schema=[Field(name="conv_rate", dtype=Float32)], + online=True, + source=source, + ) + + transition_types = { + fv.name: "UPDATE", + fv2.name: "CREATE", + fs.name: "DELETE", + } + store_enabled.mlflow.log_apply( + changed_objects=[fv, fv2, fs], + transition_types=transition_types, + ) + + client = MlflowClient(tracking_uri=tracking_uri) + exp = client.get_experiment_by_name("test_mlflow-feast-ops") + runs = client.search_runs(experiment_ids=[exp.experiment_id]) + assert len(runs) == 1 + + tags = runs[0].data.tags + assert fv.name in tags["feast.feature_views_updated"] + assert fv2.name in tags["feast.feature_views_created"] + assert fs.name in tags["feast.feature_services_deleted"] + + @pytest.mark.integration + def test_no_transition_tags_when_none( + self, store_enabled, tracking_uri, feast_objects + ): + _, _, fv, fs = feast_objects + store_enabled.mlflow.log_apply( + changed_objects=[fv, fs], + transition_types=None, + ) + + client = MlflowClient(tracking_uri=tracking_uri) + exp = client.get_experiment_by_name("test_mlflow-feast-ops") + runs = client.search_runs(experiment_ids=[exp.experiment_id]) + assert len(runs) == 1 + + tags = runs[0].data.tags + assert tags["feast.feature_views_changed"] == fv.name + assert "feast.feature_views_created" not in tags + assert "feast.feature_views_updated" not in tags + + @pytest.mark.integration + def test_backward_compatible_changed_tags_still_present( + self, store_enabled, tracking_uri, feast_objects + ): + """Transition tags are additive — the aggregate *_changed tags still appear.""" + driver, _, fv, fs = feast_objects + transition_types = { + fv.name: "CREATE", + fs.name: "UPDATE", + driver.name: "CREATE", + } + store_enabled.mlflow.log_apply( + changed_objects=[fv, fs, driver], + transition_types=transition_types, + ) + + client = MlflowClient(tracking_uri=tracking_uri) + exp = client.get_experiment_by_name("test_mlflow-feast-ops") + runs = client.search_runs(experiment_ids=[exp.experiment_id]) + tags = runs[0].data.tags + + assert tags["feast.feature_views_changed"] == fv.name + assert tags["feast.feature_services_changed"] == fs.name + assert tags["feast.entities_changed"] == driver.name + assert tags["feast.feature_views_created"] == fv.name + assert tags["feast.feature_services_updated"] == fs.name + assert tags["feast.entities_created"] == driver.name + + +class TestFeastMlflowModuleAPI: + """Tests for the ``store.mlflow`` and ``feast.mlflow`` module-level API.""" + + @pytest.mark.integration + def test_store_mlflow_property(self, store_enabled, tracking_uri): + """store.mlflow returns a FeastMlflowClient instance.""" + assert store_enabled.mlflow is not None + assert hasattr(store_enabled.mlflow, "start_run") + assert hasattr(store_enabled.mlflow, "log_model") + assert hasattr(store_enabled.mlflow, "resolve_features") + + @pytest.mark.integration + def test_store_mlflow_none_when_disabled(self, driver_parquet, tracking_uri): + tmp_path, _ = driver_parquet + store = _make_store(tmp_path, tracking_uri, enabled=False) + assert store.mlflow is None + + @pytest.mark.integration + def test_auto_registration_via_feature_store(self, store_enabled, tracking_uri): + """FeatureStore auto-registers with feast.mlflow — no init() needed.""" + client = MlflowClient(tracking_uri=tracking_uri) + + with store_enabled.mlflow.start_run(run_name="auto_reg") as run: + feast.mlflow.log_params({"lr": "0.01"}) + feast.mlflow.log_metrics({"acc": 0.95}) + feast.mlflow.log_metric("loss", 0.05) + assert store_enabled.mlflow.active_run_id == run.info.run_id + + data = client.get_run(run.info.run_id) + assert data.data.params["lr"] == "0.01" + assert data.data.metrics["acc"] == 0.95 + assert data.data.metrics["loss"] == 0.05 + assert data.data.tags.get("feast.project") == "test_mlflow" + + @pytest.mark.integration + def test_explicit_init_overrides_auto_registration( + self, store_enabled, tracking_uri + ): + feast.mlflow.init(store_enabled) + with store_enabled.mlflow.start_run(run_name="explicit_init") as run: + feast.mlflow.log_metric("x", 1.0) + + client = MlflowClient(tracking_uri=tracking_uri) + data = client.get_run(run.info.run_id) + assert data.data.tags.get("feast.project") == "test_mlflow" + + @pytest.mark.integration + def test_getattr_delegates_all_methods(self, store_enabled, tracking_uri): + """All FeastMlflowClient public methods are accessible via store.mlflow.""" + for method_name in [ + "start_run", + "log_model", + "register_model", + "load_model", + "resolve_features", + "get_training_entity_df", + "log_feature_retrieval", + "log_training_dataset", + "log_apply", + "log_materialize", + ]: + attr = getattr(store_enabled.mlflow, method_name) + assert callable(attr), f"{method_name} should be callable" + + @pytest.mark.integration + def test_active_run_id_property(self, store_enabled): + assert store_enabled.mlflow.active_run_id is None + with store_enabled.mlflow.start_run(run_name="prop_test"): + assert store_enabled.mlflow.active_run_id is not None + + @pytest.mark.integration + def test_mlflow_escape_hatch(self, store_enabled): + raw = store_enabled.mlflow.mlflow + assert hasattr(raw, "start_run") + + @pytest.mark.integration + def test_unknown_attr_raises(self, store_enabled): + with pytest.raises(AttributeError): + feast.mlflow.nonexistent_method + + @pytest.mark.integration + def test_log_model_and_register(self, store_enabled, entity_df, tracking_uri): + from sklearn.linear_model import LogisticRegression + + with store_enabled.mlflow.start_run(run_name="mod_train"): + store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + + model = LogisticRegression().fit([[0, 0], [1, 1]], [0, 1]) + store_enabled.mlflow.log_model(model, "model") + train_run_id = store_enabled.mlflow.active_run_id + + mv = store_enabled.mlflow.register_model( + f"runs:/{train_run_id}/model", "mod_test_model" + ) + assert mv.version is not None + + client = MlflowClient(tracking_uri=tracking_uri) + mv_info = client.get_model_version("mod_test_model", mv.version) + assert mv_info.tags.get("feast.feature_service") == "driver_activity_v1" + + @pytest.mark.integration + def test_load_model_tags_prediction_run( + self, store_enabled, entity_df, tracking_uri + ): + from sklearn.linear_model import LogisticRegression + + with store_enabled.mlflow.start_run(run_name="mod_train_load"): + store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + model = LogisticRegression().fit([[0, 0], [1, 1]], [0, 1]) + store_enabled.mlflow.log_model(model, "model") + train_run_id = store_enabled.mlflow.active_run_id + + store_enabled.mlflow.register_model( + f"runs:/{train_run_id}/model", "mod_load_test" + ) + + with store_enabled.mlflow.start_run(run_name="mod_predict") as pred_run: + store_enabled.mlflow.load_model("models:/mod_load_test/1") + + client = MlflowClient(tracking_uri=tracking_uri) + tags = client.get_run(pred_run.info.run_id).data.tags + assert tags.get("feast.training_run_id") == train_run_id + assert tags.get("feast.model_name") == "mod_load_test" + + @pytest.mark.integration + def test_resolve_features_and_get_training_entity_df( + self, store_enabled, entity_df, tracking_uri + ): + from sklearn.linear_model import LogisticRegression + + with store_enabled.mlflow.start_run(run_name="mod_resolve"): + store_enabled.get_historical_features( + features=store_enabled.get_feature_service("driver_activity_v1"), + entity_df=entity_df, + ).to_df() + model = LogisticRegression().fit([[0, 0], [1, 1]], [0, 1]) + store_enabled.mlflow.log_model(model, "model") + train_run_id = store_enabled.mlflow.active_run_id + + store_enabled.mlflow.register_model( + f"runs:/{train_run_id}/model", "mod_resolve_model" + ) + + fs_name = store_enabled.mlflow.resolve_features("models:/mod_resolve_model/1") + assert fs_name == "driver_activity_v1" + + recovered = store_enabled.mlflow.get_training_entity_df(train_run_id) + assert recovered.shape == entity_df.shape + + @pytest.mark.integration + def test_no_active_run_returns_none(self, store_enabled): + assert store_enabled.mlflow.active_run_id is None + + @pytest.mark.integration + def test_error_when_mlflow_disabled(self, driver_parquet, tracking_uri): + """feast.mlflow raises RuntimeError when mlflow is not enabled.""" + tmp_path, _ = driver_parquet + feast.mlflow._client = None + feast.mlflow._registered_store = None + + disabled_store = _make_store(tmp_path, tracking_uri, enabled=False) + feast.mlflow.init(disabled_store) + + with pytest.raises(RuntimeError, match="not enabled"): + feast.mlflow.start_run(run_name="should_fail") diff --git a/sdk/python/tests/registry_rest_api_tests/conftest.py b/sdk/python/tests/registry_rest_api_tests/conftest.py deleted file mode 100644 index 2a128785fd8..00000000000 --- a/sdk/python/tests/registry_rest_api_tests/conftest.py +++ /dev/null @@ -1,130 +0,0 @@ -import os - -import pytest -import requests -from kubernetes import client, config -from support import ( - applyFeastProject, - create_feast_project, - create_namespace, - create_route, - delete_namespace, - deploy_and_validate_pod, - execPodCommand, - get_pod_name_by_prefix, - run_kubectl_apply_with_sed, - run_kubectl_command, - validate_feature_store_cr_status, -) - - -class FeastRestClient: - def __init__(self, base_url): - self.base_url = base_url.rstrip("/") - self.api_prefix = "/api/v1" - - def _build_url(self, endpoint): - if not endpoint.startswith("/"): - endpoint = "/" + endpoint - return f"{self.base_url}{self.api_prefix}{endpoint}" - - def get(self, endpoint, params=None): - params = params or {} - params.setdefault("allow_cache", "false") - url = self._build_url(endpoint) - return requests.get(url, params=params, verify=False) - - -@pytest.fixture(scope="session") -def feast_rest_client(): - # Load kubeconfig and initialize Kubernetes client - config.load_kube_config() - api_instance = client.CoreV1Api() - - # Constants and environment values - namespace = "test-ns-feast-rest" - credit_scoring = "credit-scoring" - driver_ranking = "driver-ranking" - service_name = "feast-test-s3-registry-rest" - run_on_openshift = os.getenv("RUN_ON_OPENSHIFT_CI", "false").lower() == "true" - - # Create test namespace - create_namespace(api_instance, namespace) - - try: - if not run_on_openshift: - # Deploy dependencies - deploy_and_validate_pod(namespace, "resource/redis.yaml", "app=redis") - deploy_and_validate_pod(namespace, "resource/postgres.yaml", "app=postgres") - - # Create and validate FeatureStore CRs - create_feast_project( - "resource/feast_config_credit_scoring.yaml", namespace, credit_scoring - ) - validate_feature_store_cr_status(namespace, credit_scoring) - - create_feast_project( - "resource/feast_config_driver_ranking.yaml", namespace, driver_ranking - ) - validate_feature_store_cr_status(namespace, driver_ranking) - - # Deploy ingress and get route URL - run_kubectl_command( - ["apply", "-f", "resource/feast-registry-nginx.yaml", "-n", namespace] - ) - ingress_host = run_kubectl_command( - [ - "get", - "ingress", - "feast-registry-ingress", - "-n", - namespace, - "-o", - "jsonpath={.spec.rules[0].host}", - ] - ) - route_url = f"http://{ingress_host}" - - # Apply feast projects - - applyFeastProject(namespace, credit_scoring) - - applyFeastProject(namespace, driver_ranking) - - # Create Saved Datasets and Permissions - pod_name = get_pod_name_by_prefix(namespace, credit_scoring) - - # Apply datasets - execPodCommand( - namespace, pod_name, ["python", "create_ui_visible_datasets.py"] - ) - - # Apply permissions - execPodCommand(namespace, pod_name, ["python", "permissions_apply.py"]) - - else: - # OpenShift cluster setup using S3-based registry - aws_access_key = os.getenv("AWS_ACCESS_KEY") - aws_secret_key = os.getenv("AWS_SECRET_KEY") - aws_bucket = os.getenv("AWS_BUCKET_NAME") - registry_path = os.getenv("AWS_REGISTRY_FILE_PATH") - - run_kubectl_apply_with_sed( - aws_access_key, - aws_secret_key, - aws_bucket, - registry_path, - "resource/feast_config_rhoai.yaml", - namespace, - ) - validate_feature_store_cr_status(namespace, "test-s3") - route_url = create_route(namespace, credit_scoring, service_name) - if not route_url: - raise RuntimeError("Route URL could not be fetched.") - - print(f"\n Connected to Feast REST at: {route_url}") - yield FeastRestClient(route_url) - - finally: - print(f"\n Deleting namespace: {namespace}") - delete_namespace(api_instance, namespace) diff --git a/sdk/python/tests/unit/api/test_api_rest_registry.py b/sdk/python/tests/unit/api/test_api_rest_registry.py index 12e22737f93..fe67ff34ebe 100644 --- a/sdk/python/tests/unit/api/test_api_rest_registry.py +++ b/sdk/python/tests/unit/api/test_api_rest_registry.py @@ -139,7 +139,7 @@ def test_on_demand_feature_view(features_df: pd.DataFrame) -> pd.DataFrame: test_on_demand_feature_view, ] ) - store._registry.apply_saved_dataset(test_saved_dataset, "demo_project") + store.registry.apply_saved_dataset(test_saved_dataset, "demo_project") # Build REST app with registered routes rest_server = RestRegistryServer(store) @@ -337,6 +337,34 @@ def test_feature_views_comprehensive_filtering_via_rest(fastapi_test_app): assert len(data["featureViews"]) == 0 +def test_feature_views_updated_since_via_rest(fastapi_test_app): + """Test that feature views can be filtered by updated_since timestamp.""" + # A timestamp in the past should return all feature views + response = fastapi_test_app.get( + "/feature_views?project=demo_project&updated_since=2000-01-01T00:00:00Z" + ) + assert response.status_code == 200 + data = response.json() + assert "featureViews" in data + all_count = len(data["featureViews"]) + assert all_count > 0 + + # A timestamp far in the future should return no feature views + response = fastapi_test_app.get( + "/feature_views?project=demo_project&updated_since=2999-01-01T00:00:00Z" + ) + assert response.status_code == 200 + data = response.json() + assert "featureViews" in data + assert len(data["featureViews"]) == 0 + + # Without updated_since returns the same count as the past-timestamp query + response = fastapi_test_app.get("/feature_views?project=demo_project") + assert response.status_code == 200 + data = response.json() + assert len(data["featureViews"]) == all_count + + def test_feature_services_via_rest(fastapi_test_app): response = fastapi_test_app.get("/feature_services?project=demo_project") assert response.status_code == 200 @@ -773,7 +801,7 @@ def fastapi_test_app_with_multiple_objects(): store.apply(entities + data_sources + feature_views + feature_services) for dataset in saved_datasets: - store._registry.apply_saved_dataset(dataset, "demo_project") + store.registry.apply_saved_dataset(dataset, "demo_project") rest_server = RestRegistryServer(store) client = TestClient(rest_server.app) @@ -1503,11 +1531,50 @@ def test_metrics_resource_counts_via_rest(fastapi_test_app): assert "featureViews" in counts assert "featureServices" in counts - # Verify counts are integers for key, value in counts.items(): assert isinstance(value, int) assert value >= 0 + # Verify feature services summaries + assert "featureServices" in data + assert isinstance(data["featureServices"], list) + assert len(data["featureServices"]) == counts["featureServices"] + for fs in data["featureServices"]: + assert "name" in fs + assert "project" in fs + assert fs["project"] == "demo_project" + service_names = [fs["name"] for fs in data["featureServices"]] + assert "user_service" in service_names + + # Verify feature views summaries with detail + assert "featureViews" in data + assert isinstance(data["featureViews"], list) + assert len(data["featureViews"]) == counts["featureViews"] + for fv in data["featureViews"]: + assert "name" in fv + assert "project" in fv + assert "type" in fv + assert "featureCount" in fv + assert isinstance(fv["featureCount"], int) + assert fv["featureCount"] >= 0 + assert fv["project"] == "demo_project" + fv_names = [fv["name"] for fv in data["featureViews"]] + assert "user_profile" in fv_names + user_profile_fv = next( + fv for fv in data["featureViews"] if fv["name"] == "user_profile" + ) + assert user_profile_fv["featureCount"] == 2 + + # Verify projects list + assert "projects" in data + assert isinstance(data["projects"], list) + assert len(data["projects"]) == 1 + assert data["projects"][0]["name"] == "demo_project" + + # Verify registry last updated + assert "registryLastUpdated" in data + assert data["registryLastUpdated"] is not None + # Test without project parameter (should return all projects) response = fastapi_test_app.get("/metrics/resource_counts") assert response.status_code == 200 @@ -1527,6 +1594,116 @@ def test_metrics_resource_counts_via_rest(fastapi_test_app): assert "demo_project" in per_project assert isinstance(per_project["demo_project"], dict) + # Verify metadata in all-projects mode + assert "featureServices" in data + assert isinstance(data["featureServices"], list) + + assert "featureViews" in data + assert isinstance(data["featureViews"], list) + for fv in data["featureViews"]: + assert "name" in fv + assert "project" in fv + assert "type" in fv + assert "featureCount" in fv + + assert "projects" in data + assert isinstance(data["projects"], list) + assert len(data["projects"]) >= 1 + for proj in data["projects"]: + assert "name" in proj + assert "description" in proj + + assert "registryLastUpdated" in data + assert data["registryLastUpdated"] is not None + + +def test_metrics_resource_counts_with_permission_errors(fastapi_test_app): + """ + Test that /metrics/resource_counts returns 200 with zero counts for + resource types that raise FeastPermissionError, instead of failing + the entire request. This simulates the scenario where access is + restricted for some resource types via GroupBasedPolicy, + NamespaceBasedPolicy, or CombinedGroupNamespacePolicy. + """ + from unittest.mock import patch + + from feast.errors import FeastPermissionError + + original_get = fastapi_test_app.get + + # First, get the baseline counts to know which resources have data + baseline = original_get("/metrics/resource_counts?project=demo_project").json() + baseline_counts = baseline["counts"] + + # Patch grpc_call to raise FeastPermissionError for entities and data sources + # while allowing other resource types to succeed + original_grpc_call = None + + def grpc_call_entities_denied(handler_fn, request): + handler_name = getattr(handler_fn, "__name__", "") + if handler_name in ("ListEntities", "ListDataSources"): + raise FeastPermissionError(f"Permission denied for {handler_name}") + return original_grpc_call(handler_fn, request) + + with patch("feast.api.registry.rest.metrics.grpc_call") as mock_grpc_call: + import feast.api.registry.rest.metrics as metrics_module + + original_grpc_call = metrics_module.__dict__.get("grpc_call") + # Restore actual import since patch replaces it + from feast.api.registry.rest.rest_utils import grpc_call as real_grpc_call + + original_grpc_call = real_grpc_call + mock_grpc_call.side_effect = grpc_call_entities_denied + + response = fastapi_test_app.get("/metrics/resource_counts?project=demo_project") + + assert response.status_code == 200, ( + f"Expected 200 but got {response.status_code}: {response.text}" + ) + data = response.json() + assert "counts" in data + counts = data["counts"] + + # Denied resource types should have 0 counts + assert counts["entities"] == 0 + assert counts["dataSources"] == 0 + + # Permitted resource types should still have their original counts + assert counts["featureViews"] == baseline_counts["featureViews"] + assert counts["featureServices"] == baseline_counts["featureServices"] + assert counts["features"] == baseline_counts["features"] + assert counts["savedDatasets"] == baseline_counts["savedDatasets"] + + # Permitted metadata summaries should still be populated + assert len(data["featureViews"]) == baseline_counts["featureViews"] + assert len(data["featureServices"]) == baseline_counts["featureServices"] + + # Now test with ALL resource types denied + def grpc_call_all_denied(handler_fn, request): + handler_name = getattr(handler_fn, "__name__", "") + if handler_name.startswith("List") and handler_name != "ListProjects": + raise FeastPermissionError(f"Permission denied for {handler_name}") + return real_grpc_call(handler_fn, request) + + with patch("feast.api.registry.rest.metrics.grpc_call") as mock_grpc_call: + mock_grpc_call.side_effect = grpc_call_all_denied + + response = fastapi_test_app.get("/metrics/resource_counts?project=demo_project") + + assert response.status_code == 200 + data = response.json() + counts = data["counts"] + + # All counts should be 0 when all resource types are denied + for resource_type, count in counts.items(): + assert count == 0, ( + f"Expected 0 for {resource_type} when permission denied, got {count}" + ) + + # Metadata summaries should be empty when all permissions are denied + assert data["featureViews"] == [] + assert data["featureServices"] == [] + def test_feature_views_all_types_and_resource_counts_match(fastapi_test_app): """ @@ -1851,3 +2028,303 @@ def test_all_endpoints_return_404_for_invalid_objects(fastapi_test_app): data = response.json() assert data["status_code"] == 404 assert data["error_type"] == "FeastObjectNotFoundException" + + +def test_apply_and_delete_entity_via_rest(fastapi_test_app): + """Test POST /entities and DELETE /entities/{name} endpoints.""" + # Apply a new entity + response = fastapi_test_app.post( + "/entities", + json={ + "name": "driver_id", + "project": "demo_project", + "join_key": "driver_id", + "value_type": 2, + "description": "Driver entity", + "owner": "ml-team", + }, + ) + assert response.status_code == 201 + data = response.json() + assert data["name"] == "driver_id" + assert data["status"] == "applied" + + # Verify it exists + response = fastapi_test_app.get("/entities/driver_id?project=demo_project") + assert response.status_code == 200 + assert response.json()["spec"]["name"] == "driver_id" + + # Delete it + response = fastapi_test_app.delete("/entities/driver_id?project=demo_project") + assert response.status_code == 200 + data = response.json() + assert data["name"] == "driver_id" + assert data["status"] == "deleted" + + # Verify it's gone + response = fastapi_test_app.get("/entities/driver_id?project=demo_project") + assert response.status_code == 404 + + +def test_apply_and_delete_data_source_via_rest(fastapi_test_app): + """Test POST /data_sources and DELETE /data_sources/{name} endpoints.""" + # Apply a new file data source + response = fastapi_test_app.post( + "/data_sources", + json={ + "name": "test_file_source", + "project": "demo_project", + "type": 1, + "timestamp_field": "event_timestamp", + "description": "Test file source", + "file_options": {"uri": "s3://bucket/path/data.parquet"}, + }, + ) + assert response.status_code == 201 + data = response.json() + assert data["name"] == "test_file_source" + assert data["status"] == "applied" + + # Verify it exists + response = fastapi_test_app.get( + "/data_sources/test_file_source?project=demo_project" + ) + assert response.status_code == 200 + assert response.json()["name"] == "test_file_source" + + # Delete it + response = fastapi_test_app.delete( + "/data_sources/test_file_source?project=demo_project" + ) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "test_file_source" + assert data["status"] == "deleted" + + # Verify it's gone + response = fastapi_test_app.get( + "/data_sources/test_file_source?project=demo_project" + ) + assert response.status_code == 404 + + +def test_apply_and_delete_feature_view_via_rest(fastapi_test_app): + """Test POST /feature_views and DELETE /feature_views/{name} endpoints.""" + # Apply a new feature view (no batch_source: a bare DataSourceProto with only a + # name but no type is rejected by the registry's source-type validation) + response = fastapi_test_app.post( + "/feature_views", + json={ + "name": "driver_stats", + "project": "demo_project", + "entities": ["user_id"], + "features": [ + { + "name": "trip_count", + "value_type": 2, + "description": "Number of completed trips", + }, + { + "name": "avg_rating", + "value_type": 4, + "description": "Average driver rating", + }, + ], + "ttl_seconds": 86400, + "online": True, + "description": "Driver statistics feature view", + }, + ) + assert response.status_code == 201 + data = response.json() + assert data["name"] == "driver_stats" + assert data["status"] == "applied" + + # Verify it exists + response = fastapi_test_app.get("/feature_views/driver_stats?project=demo_project") + assert response.status_code == 200 + spec = response.json()["spec"] + assert spec["name"] == "driver_stats" + assert spec["features"][0]["description"] == "Number of completed trips" + assert spec["features"][1]["description"] == "Average driver rating" + + # Delete it + response = fastapi_test_app.delete( + "/feature_views/driver_stats?project=demo_project" + ) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "driver_stats" + assert data["status"] == "deleted" + + # Verify it's gone + response = fastapi_test_app.get("/feature_views/driver_stats?project=demo_project") + assert response.status_code == 404 + + +def test_apply_and_delete_feature_service_via_rest(fastapi_test_app): + """Test POST /feature_services and DELETE /feature_services/{name} endpoints.""" + response = fastapi_test_app.post( + "/feature_views", + json={ + "name": "driver_stats_for_service", + "project": "demo_project", + "entities": ["user_id"], + "features": [ + { + "name": "trip_count", + "value_type": 2, + "description": "Number of completed trips", + }, + ], + "ttl_seconds": 86400, + "online": True, + "description": "Driver statistics feature view", + }, + ) + assert response.status_code == 201 + + response = fastapi_test_app.post( + "/feature_services", + json={ + "name": "driver_activity_v1", + "project": "demo_project", + "features": [ + { + "feature_view_name": "driver_stats_for_service", + "feature_names": ["trip_count"], + } + ], + "description": "Driver activity feature service", + "owner": "ml-team", + }, + ) + assert response.status_code == 201 + data = response.json() + assert data["name"] == "driver_activity_v1" + assert data["status"] == "applied" + + response = fastapi_test_app.get( + "/feature_services/driver_activity_v1?project=demo_project" + ) + assert response.status_code == 200 + data = response.json() + assert data["spec"]["name"] == "driver_activity_v1" + projections = data["spec"]["features"] + assert len(projections) == 1 + assert projections[0]["featureViewName"] == "driver_stats_for_service" + assert len(projections[0]["featureColumns"]) == 1 + assert projections[0]["featureColumns"][0]["name"] == "trip_count" + + response = fastapi_test_app.get("/feature_services?project=demo_project") + assert response.status_code == 200 + listed = next( + fs + for fs in response.json()["featureServices"] + if fs["spec"]["name"] == "driver_activity_v1" + ) + listed_projection = listed["spec"]["features"][0] + assert listed_projection["featureColumns"][0]["name"] == "trip_count" + + response = fastapi_test_app.post( + "/feature_services", + json={ + "name": "driver_activity_all", + "project": "demo_project", + "features": [ + {"feature_view_name": "driver_stats_for_service"}, + ], + "description": "All features from the feature view", + }, + ) + assert response.status_code == 201 + + response = fastapi_test_app.get( + "/feature_services/driver_activity_all?project=demo_project" + ) + assert response.status_code == 200 + all_features_projection = response.json()["spec"]["features"][0] + assert all_features_projection["featureViewName"] == "driver_stats_for_service" + assert len(all_features_projection["featureColumns"]) == 1 + assert all_features_projection["featureColumns"][0]["name"] == "trip_count" + + response = fastapi_test_app.delete( + "/feature_services/driver_activity_all?project=demo_project" + ) + assert response.status_code == 200 + + response = fastapi_test_app.delete( + "/feature_services/driver_activity_v1?project=demo_project" + ) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "driver_activity_v1" + assert data["status"] == "deleted" + + response = fastapi_test_app.get( + "/feature_services/driver_activity_v1?project=demo_project" + ) + assert response.status_code == 404 + + response = fastapi_test_app.delete( + "/feature_views/driver_stats_for_service?project=demo_project" + ) + assert response.status_code == 200 + + +def test_metrics_resource_counts_nonexistent_project(fastapi_test_app): + """Test /metrics/resource_counts with a non-existent project returns empty data.""" + response = fastapi_test_app.get( + "/metrics/resource_counts?project=nonexistent_project" + ) + assert response.status_code == 200 + data = response.json() + + counts = data["counts"] + for value in counts.values(): + assert value == 0 + assert data["featureServices"] == [] + assert data["featureViews"] == [] + assert "registryLastUpdated" in data + + +def test_registry_refresh_via_rest(fastapi_test_app): + """POST /registry/refresh invalidates the registry cache and returns 200.""" + response = fastapi_test_app.post("/registry/refresh") + assert response.status_code == 200 + + +def test_registry_refresh_via_rest_error(): + """POST /registry/refresh returns 500 when refresh fails.""" + from unittest.mock import patch + + from feast.api.registry.rest import register_all_routes + from feast.registry_server import RegistryServer + + tmp_dir = tempfile.TemporaryDirectory() + registry_path = os.path.join(tmp_dir.name, "registry.db") + + config = { + "registry": registry_path, + "project": "demo_project", + "provider": "local", + "offline_store": {"type": "file"}, + "online_store": {"type": "sqlite", "path": ":memory:"}, + } + from fastapi import FastAPI + + from feast.repo_config import RepoConfig + + store = FeatureStore(config=RepoConfig.model_validate(config)) + store.apply([]) + + app = FastAPI() + grpc_handler = RegistryServer(store.registry, store=store) + register_all_routes(app, grpc_handler, store=store) + + with patch.object(store, "refresh_registry", side_effect=Exception("db error")): + client = TestClient(app, raise_server_exceptions=False) + response = client.post("/registry/refresh") + assert response.status_code == 500 + + tmp_dir.cleanup() diff --git a/sdk/python/tests/unit/api/test_api_rest_registry_server.py b/sdk/python/tests/unit/api/test_api_rest_registry_server.py index 2abfa3ac462..c2fe987ab07 100644 --- a/sdk/python/tests/unit/api/test_api_rest_registry_server.py +++ b/sdk/python/tests/unit/api/test_api_rest_registry_server.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest @@ -11,23 +11,36 @@ def mock_store_and_registry(): mock_registry = MagicMock() mock_store = MagicMock(spec=FeatureStore) mock_store.registry = mock_registry - mock_store.config = MagicMock() + mock_store.project = "test_project" + mock_config = MagicMock() + mock_store.config = mock_config + mock_config.auth_config.type = "no_auth" + # MagicMock makes nested attrs truthy; keep MCP off unless a test opts in. + mock_config.registry.mcp = None return mock_store, mock_registry -@patch("feast.api.registry.rest.rest_registry_server.RegistryServer") -@patch("feast.api.registry.rest.rest_registry_server.register_all_routes") -@patch("feast.api.registry.rest.rest_registry_server.init_security_manager") -@patch("feast.api.registry.rest.rest_registry_server.init_auth_manager") -@patch("feast.api.registry.rest.rest_registry_server.get_auth_manager") +@pytest.mark.xdist_group(name="rest_registry_server") def test_rest_registry_server_initializes_correctly( - mock_get_auth_manager, - mock_init_auth_manager, - mock_init_security_manager, - mock_register_all_routes, - mock_registry_server_cls, mock_store_and_registry, + mocker, ): + mock_get_auth_manager = mocker.patch( + "feast.api.registry.rest.rest_registry_server.get_auth_manager" + ) + mock_init_auth_manager = mocker.patch( + "feast.api.registry.rest.rest_registry_server.init_auth_manager" + ) + mock_init_security_manager = mocker.patch( + "feast.api.registry.rest.rest_registry_server.init_security_manager" + ) + mock_register_all_routes = mocker.patch( + "feast.api.registry.rest.rest_registry_server.register_all_routes" + ) + mock_registry_server_cls = mocker.patch( + "feast.api.registry.rest.rest_registry_server.RegistryServer" + ) + store, registry = mock_store_and_registry mock_grpc_handler = MagicMock() mock_registry_server_cls.return_value = mock_grpc_handler @@ -54,33 +67,12 @@ def test_rest_registry_server_initializes_correctly( assert {"BearerAuth": []} in openapi_schema["security"] -def test_routes_registered_in_app(mock_store_and_registry): - from fastapi.routing import APIRoute +def test_routes_registered_in_app(): + from feast.api.registry.rest import register_all_routes - store, _ = mock_store_and_registry + app = MagicMock() + grpc_handler = MagicMock() + server = MagicMock() + register_all_routes(app, grpc_handler, server) - server = RestRegistryServer(store) - route_paths = [ - route.path for route in server.app.routes if isinstance(route, APIRoute) - ] - assert "/feature_services" in route_paths - assert "/entities" in route_paths - assert "/projects" in route_paths - assert "/data_sources" in route_paths - assert "/saved_datasets" in route_paths - assert "/permissions" in route_paths - assert "/lineage/registry" in route_paths - assert "/lineage/objects/{object_type}/{object_name}" in route_paths - assert "/lineage/complete" in route_paths - assert "/entities/all" in route_paths - assert "/feature_views/all" in route_paths - assert "/data_sources/all" in route_paths - assert "/feature_services/all" in route_paths - assert "/saved_datasets/all" in route_paths - assert "/lineage/registry/all" in route_paths - assert "/lineage/complete/all" in route_paths - assert "/features" in route_paths - assert "/features/all" in route_paths - assert "/features/{feature_view}/{name}" in route_paths - assert "/metrics/resource_counts" in route_paths - assert "/metrics/recently_visited" in route_paths + assert app.include_router.call_count == 15 diff --git a/sdk/python/tests/unit/api/test_search_api.py b/sdk/python/tests/unit/api/test_search_api.py index 48e422c5fe1..bce7a30e9fb 100644 --- a/sdk/python/tests/unit/api/test_search_api.py +++ b/sdk/python/tests/unit/api/test_search_api.py @@ -235,7 +235,7 @@ def user_on_demand_features(inputs: dict): user_on_demand_features, ] ) - store._registry.apply_saved_dataset(user_dataset, "test_project") + store.registry.apply_saved_dataset(user_dataset, "test_project") global global_store global_store = store @@ -431,7 +431,7 @@ def multi_project_search_test_app(): description=project_data["description"], tags={"domain": project_data["domain"]}, ) - master_store._registry.apply_project(project_obj) + master_store.registry.apply_project(project_obj) # Create resources for each project and apply them to the shared registry for project_name, project_data in projects_data.items(): @@ -565,19 +565,19 @@ def multi_project_search_test_app(): # Apply all objects for this project directly to the registry for entity in entities: - master_store._registry.apply_entity(entity, project_name) + master_store.registry.apply_entity(entity, project_name) for data_source in data_sources: - master_store._registry.apply_data_source(data_source, project_name) + master_store.registry.apply_data_source(data_source, project_name) for feature_view in feature_views: - master_store._registry.apply_feature_view(feature_view, project_name) + master_store.registry.apply_feature_view(feature_view, project_name) for feature_service in feature_services: - master_store._registry.apply_feature_service(feature_service, project_name) + master_store.registry.apply_feature_service(feature_service, project_name) # Ensure registry is committed - master_store._registry.commit() + master_store.registry.commit() # Build REST app using the master store's registry (contains all projects) rest_server = RestRegistryServer(master_store) @@ -734,6 +734,143 @@ def test_search_by_tags(self, shared_search_responses): f"Expected to find some of {expected_resources} but found none in {found_resources}" ) + def test_search_matched_tags_exact_match(self, search_test_app): + """Test that matched_tags field is present when a tag matches exactly""" + # Search for "data" which should match tag key "team" with value "data" + response = search_test_app.get("/search?query=data") + assert response.status_code == 200 + + data = response.json() + results = data["results"] + + # Find results that matched via tags (match_score = 60) + tag_matched_results = [ + r for r in results if r.get("match_score") == 60 and "matched_tags" in r + ] + + assert len(tag_matched_results) > 0, ( + "Expected to find at least one result with matched_tags from tag matching" + ) + + # Verify matched_tags is present and has a valid dictionary value + for result in tag_matched_results: + matched_tags = result.get("matched_tags") + assert matched_tags is not None, ( + f"matched_tags should not be None for result {result['name']}" + ) + assert isinstance(matched_tags, dict), ( + f"matched_tags should be a dictionary, got {type(matched_tags)}" + ) + # matched_tags should be a non-empty dict for tag-matched results + assert len(matched_tags) > 0, ( + "matched_tags should not be empty for tag matches" + ) + + logger.debug( + f"Found {len(tag_matched_results)} results with matched_tags: {[r['name'] + ' -> ' + str(r.get('matched_tags', 'N/A')) for r in tag_matched_results]}" + ) + + def test_search_matched_tags_multiple_tags(self, search_test_app): + """Test that multiple matching tags are returned in matched_tags""" + # Search for "a" which should match: + # - Names containing "a" (e.g., user_training_dataset, data sources) + # - Tags where key/value contains "a": "team" (key), "data" (value), "training" (value) + response = search_test_app.get("/search?query=a") + logger.info(response.json()) + assert response.status_code == 200 + + data = response.json() + results = data["results"] + + # Find user_training_dataset which has tags: {"environment": "test", "purpose": "training", "team": "data"} + # "team" contains "a", "data" contains "a", "training" contains "a" + # So matched_tags should have at least 2 entries: "purpose" and "team" + dataset_results = [ + r for r in results if r.get("name") == "user_training_dataset" + ] + + assert len(dataset_results) > 0, ( + "Expected to find user_training_dataset in results" + ) + + dataset_result = dataset_results[0] + matched_tags = dataset_result.get("matched_tags", {}) + + assert isinstance(matched_tags, dict), ( + f"matched_tags should be a dictionary, got {type(matched_tags)}" + ) + + # Should have multiple matching tags: "purpose" and "team" + assert len(matched_tags) >= 2, ( + f"Expected at least 2 matching tags for 'a' query, got {len(matched_tags)}: {matched_tags}" + ) + + # Verify the expected tags are present + assert "team" in matched_tags and "purpose" in matched_tags, ( + f"Expected 'team' and 'purpose' in matched_tags, got: {matched_tags}" + ) + + logger.debug(f"user_training_dataset matched_tags: {matched_tags}") + + def test_search_matched_tags_fuzzy_match(self, search_test_app): + """Test that matched_tags field is present when a tag matches via fuzzy matching""" + # Search for "te" which should fuzzy match tag key "team" + # "te" vs "team": overlap={'t','e'}/union={'t','e','a','m'} = 2/4 = 50% (below threshold) + # Try "tea" which should fuzzy match "team" better + # "tea" vs "team": overlap={'t','e','a'}/union={'t','e','a','m'} = 3/4 = 75% (above threshold) + response = search_test_app.get("/search?query=tea") + assert response.status_code == 200 + + data = response.json() + results = data["results"] + + # Find results that matched via fuzzy tag matching (match_score < 60 but >= 40) + fuzzy_tag_matched_results = [ + r + for r in results + if r.get("match_score", 0) >= 40 + and r.get("match_score", 0) < 60 + and "matched_tags" in r + ] + + # If we don't find fuzzy matches, try a different query that's more likely to match + if len(fuzzy_tag_matched_results) == 0: + # Try "dat" which should fuzzy match tag value "data" + # "dat" vs "data": overlap={'d','a','t'}/union={'d','a','t','a'} = 3/4 = 75% (above threshold) + response = search_test_app.get("/search?query=dat") + assert response.status_code == 200 + data = response.json() + results = data["results"] + fuzzy_tag_matched_results = [ + r + for r in results + if r.get("match_score", 0) >= 40 + and r.get("match_score", 0) < 60 + and "matched_tags" in r + ] + + if len(fuzzy_tag_matched_results) > 0: + # Verify matched_tags is present for fuzzy matches + for result in fuzzy_tag_matched_results: + matched_tags = result.get("matched_tags") + assert matched_tags is not None, ( + f"matched_tags should not be None for fuzzy-matched result {result['name']}" + ) + assert isinstance(matched_tags, dict), ( + f"matched_tags should be a dictionary, got {type(matched_tags)}" + ) + assert len(matched_tags) > 0, ( + "matched_tags should not be empty for fuzzy tag matches" + ) + # Verify the match_score is in the fuzzy range + assert 40 <= result.get("match_score", 0) < 60, ( + f"Fuzzy tag match should have score in [40, 60), got {result.get('match_score')}" + ) + + logger.debug( + f"Found {len(fuzzy_tag_matched_results)} results with fuzzy matched_tags: {[r['name'] + ' -> ' + str(r.get('matched_tags', 'N/A')) + ' (score: ' + str(r.get('match_score', 'N/A')) + ')' for r in fuzzy_tag_matched_results]}" + ) + def test_search_sorting_functionality(self, shared_search_responses): """Test search results sorting using pre-computed responses""" # Test match_score descending sort @@ -1076,7 +1213,7 @@ def test_search_on_demand_feature_view(self, search_test_app): """Test searching for on-demand feature views""" # Search by name global global_store - global_store._registry.refresh() + global_store.registry.refresh() response = search_test_app.get("/search?query=user_on_demand_features") assert response.status_code == 200 diff --git a/sdk/python/tests/unit/cli/test_cli.py b/sdk/python/tests/unit/cli/test_cli.py index 46f4d24956b..9569b877fc6 100644 --- a/sdk/python/tests/unit/cli/test_cli.py +++ b/sdk/python/tests/unit/cli/test_cli.py @@ -1,14 +1,23 @@ import os +import platform import tempfile from contextlib import contextmanager from pathlib import Path from textwrap import dedent from unittest import mock +import pytest from assertpy import assertpy from tests.utils.cli_repo_creator import CliRunner +# Skip all tests in this module on macOS CI due to Ray/uv subprocess compatibility issues +# The CliRunner spawns subprocesses that hang when Ray tries to spawn workers +pytestmark = pytest.mark.skipif( + platform.system() == "Darwin" and os.environ.get("CI") == "true", + reason="Skip CLI tests on macOS CI due to Ray/uv subprocess compatibility issues", +) + def test_3rd_party_providers() -> None: """ diff --git a/sdk/python/tests/unit/cli/test_cli_chdir.py b/sdk/python/tests/unit/cli/test_cli_chdir.py index dd592db0743..ff0f9722b53 100644 --- a/sdk/python/tests/unit/cli/test_cli_chdir.py +++ b/sdk/python/tests/unit/cli/test_cli_chdir.py @@ -1,62 +1,73 @@ -import tempfile -from datetime import timedelta -from pathlib import Path - -from feast.utils import _utc_now -from tests.utils.cli_repo_creator import CliRunner - - -def test_cli_chdir() -> None: - """ - This test simply makes sure that you can run 'feast --chdir COMMAND' - to switch to a feature repository before running a COMMAND. - """ - runner = CliRunner() - with tempfile.TemporaryDirectory() as temp_dir: - # Make sure the path is absolute by resolving any symlinks - temp_path = Path(temp_dir).resolve() - result = runner.run(["init", "my_project"], cwd=temp_path) - repo_path = str(temp_path / "my_project" / "feature_repo") - assert result.returncode == 0 - - result = runner.run(["--chdir", repo_path, "apply"], cwd=temp_path) - assert result.returncode == 0 - - result = runner.run(["--chdir", repo_path, "entities", "list"], cwd=temp_path) - assert result.returncode == 0 - - result = runner.run( - ["--chdir", repo_path, "feature-views", "list"], cwd=temp_path - ) - assert result.returncode == 0 - - end_date = _utc_now() - start_date = end_date - timedelta(days=100) - result = runner.run( - [ - "--chdir", - repo_path, - "materialize", - start_date.isoformat(), - end_date.isoformat(), - ], - cwd=temp_path, - ) - assert result.returncode == 0 - - result = runner.run( - [ - "--chdir", - repo_path, - "materialize-incremental", - end_date.isoformat(), - ], - cwd=temp_path, - ) - assert result.returncode == 0 - - result = runner.run(["--chdir", repo_path, "registry-dump"], cwd=temp_path) - assert result.returncode == 0 - - result = runner.run(["--chdir", repo_path, "teardown"], cwd=temp_path) - assert result.returncode == 0 +import os +import platform +import tempfile +from datetime import timedelta +from pathlib import Path + +import pytest + +from feast.utils import _utc_now +from tests.utils.cli_repo_creator import CliRunner + +# Skip all tests in this module on macOS CI due to subprocess timeout issues +# The CliRunner spawns subprocesses that can hang on macOS (e.g., registry-dump) +pytestmark = pytest.mark.skipif( + platform.system() == "Darwin" and os.environ.get("CI") == "true", + reason="Skip CLI tests on macOS CI due to subprocess timeout issues", +) + + +def test_cli_chdir() -> None: + """ + This test simply makes sure that you can run 'feast --chdir COMMAND' + to switch to a feature repository before running a COMMAND. + """ + runner = CliRunner() + with tempfile.TemporaryDirectory() as temp_dir: + # Make sure the path is absolute by resolving any symlinks + temp_path = Path(temp_dir).resolve() + result = runner.run(["init", "my_project"], cwd=temp_path) + repo_path = str(temp_path / "my_project" / "feature_repo") + assert result.returncode == 0 + + result = runner.run(["--chdir", repo_path, "apply"], cwd=temp_path) + assert result.returncode == 0 + + result = runner.run(["--chdir", repo_path, "entities", "list"], cwd=temp_path) + assert result.returncode == 0 + + result = runner.run( + ["--chdir", repo_path, "feature-views", "list"], cwd=temp_path + ) + assert result.returncode == 0 + + end_date = _utc_now() + start_date = end_date - timedelta(days=100) + result = runner.run( + [ + "--chdir", + repo_path, + "materialize", + start_date.isoformat(), + end_date.isoformat(), + ], + cwd=temp_path, + ) + assert result.returncode == 0 + + result = runner.run( + [ + "--chdir", + repo_path, + "materialize-incremental", + end_date.isoformat(), + ], + cwd=temp_path, + ) + assert result.returncode == 0 + + result = runner.run(["--chdir", repo_path, "registry-dump"], cwd=temp_path) + assert result.returncode == 0 + + result = runner.run(["--chdir", repo_path, "teardown"], cwd=temp_path) + assert result.returncode == 0 diff --git a/sdk/python/tests/unit/dbt/__init__.py b/sdk/python/tests/unit/dbt/__init__.py new file mode 100644 index 00000000000..8a225265d2c --- /dev/null +++ b/sdk/python/tests/unit/dbt/__init__.py @@ -0,0 +1 @@ +# dbt integration tests diff --git a/sdk/python/tests/unit/dbt/sample_manifest.json b/sdk/python/tests/unit/dbt/sample_manifest.json new file mode 100644 index 00000000000..6a44b9db749 --- /dev/null +++ b/sdk/python/tests/unit/dbt/sample_manifest.json @@ -0,0 +1,170 @@ +{ + "metadata": { + "dbt_version": "1.5.0", + "project_name": "sample_dbt_project", + "generated_at": "2024-01-10T00:00:00Z", + "invocation_id": "12345678-1234-1234-1234-123456789012" + }, + "nodes": { + "model.sample_dbt_project.driver_stats": { + "name": "driver_stats", + "unique_id": "model.sample_dbt_project.driver_stats", + "resource_type": "model", + "database": "feast_demo", + "schema": "public", + "alias": "driver_stats", + "description": "Driver statistics aggregated hourly for ML features", + "columns": { + "driver_id": { + "name": "driver_id", + "description": "Unique driver identifier", + "data_type": "INT64", + "tags": ["entity", "primary_key"], + "meta": {} + }, + "event_timestamp": { + "name": "event_timestamp", + "description": "Timestamp of the event", + "data_type": "TIMESTAMP", + "tags": ["timestamp"], + "meta": {} + }, + "trip_count": { + "name": "trip_count", + "description": "Total number of trips completed", + "data_type": "INT64", + "tags": ["feature"], + "meta": {} + }, + "avg_rating": { + "name": "avg_rating", + "description": "Average driver rating (1-5 scale)", + "data_type": "FLOAT64", + "tags": ["feature"], + "meta": {} + }, + "total_earnings": { + "name": "total_earnings", + "description": "Total earnings in dollars", + "data_type": "FLOAT64", + "tags": ["feature"], + "meta": {} + }, + "is_active": { + "name": "is_active", + "description": "Whether driver is currently active", + "data_type": "BOOLEAN", + "tags": ["feature"], + "meta": {} + } + }, + "tags": ["feast", "ml", "driver"], + "meta": { + "owner": "ml-team@example.com", + "team": "driver-experience" + }, + "depends_on": { + "nodes": ["source.sample_dbt_project.raw_trips"] + } + }, + "model.sample_dbt_project.customer_stats": { + "name": "customer_stats", + "unique_id": "model.sample_dbt_project.customer_stats", + "resource_type": "model", + "database": "feast_demo", + "schema": "public", + "alias": "customer_stats", + "description": "Customer statistics for personalization features", + "columns": { + "customer_id": { + "name": "customer_id", + "description": "Unique customer identifier", + "data_type": "STRING", + "tags": ["entity"], + "meta": {} + }, + "event_timestamp": { + "name": "event_timestamp", + "description": "Event timestamp", + "data_type": "TIMESTAMP", + "tags": [], + "meta": {} + }, + "order_count": { + "name": "order_count", + "description": "Total number of orders placed", + "data_type": "INT64", + "tags": ["feature"], + "meta": {} + }, + "avg_order_value": { + "name": "avg_order_value", + "description": "Average order value in dollars", + "data_type": "FLOAT64", + "tags": ["feature"], + "meta": {} + }, + "preferred_payment": { + "name": "preferred_payment", + "description": "Preferred payment method", + "data_type": "STRING", + "tags": ["feature"], + "meta": {} + } + }, + "tags": ["feast", "ml", "customer"], + "meta": { + "owner": "ml-team@example.com" + }, + "depends_on": { + "nodes": [] + } + }, + "model.sample_dbt_project.location_stats": { + "name": "location_stats", + "unique_id": "model.sample_dbt_project.location_stats", + "resource_type": "model", + "database": "feast_demo", + "schema": "public", + "alias": "location_stats", + "description": "Location-based statistics (no feast tag)", + "columns": { + "location_id": { + "name": "location_id", + "description": "Location identifier", + "data_type": "STRING", + "tags": [], + "meta": {} + }, + "event_timestamp": { + "name": "event_timestamp", + "description": "Event timestamp", + "data_type": "TIMESTAMP", + "tags": [], + "meta": {} + }, + "demand_score": { + "name": "demand_score", + "description": "Demand score for the location", + "data_type": "FLOAT64", + "tags": [], + "meta": {} + } + }, + "tags": ["analytics"], + "meta": {}, + "depends_on": { + "nodes": [] + } + } + }, + "sources": { + "source.sample_dbt_project.raw_trips": { + "name": "raw_trips", + "unique_id": "source.sample_dbt_project.raw_trips", + "source_name": "raw_data", + "schema": "raw", + "identifier": "trips" + } + } +} diff --git a/sdk/python/tests/unit/dbt/test_mapper.py b/sdk/python/tests/unit/dbt/test_mapper.py new file mode 100644 index 00000000000..4f06ba47fc7 --- /dev/null +++ b/sdk/python/tests/unit/dbt/test_mapper.py @@ -0,0 +1,313 @@ +""" +Unit tests for dbt to Feast mapper. +""" + +from datetime import timedelta + +import pytest + +# Skip all tests in this module if dbt-artifacts-parser is not installed +pytest.importorskip("dbt_artifacts_parser", reason="dbt-artifacts-parser not installed") + +from feast.dbt.mapper import ( + DbtToFeastMapper, + map_dbt_type_to_feast_type, +) +from feast.dbt.parser import DbtColumn, DbtModel +from feast.types import ( + Array, + Bool, + Bytes, + Float32, + Float64, + Int32, + Int64, + String, + UnixTimestamp, +) + + +class TestTypeMapping: + """Tests for dbt to Feast type mapping.""" + + def test_string_types(self): + """Test string type mappings.""" + assert map_dbt_type_to_feast_type("STRING") == String + assert map_dbt_type_to_feast_type("TEXT") == String + assert map_dbt_type_to_feast_type("VARCHAR") == String + assert map_dbt_type_to_feast_type("VARCHAR(255)") == String + assert map_dbt_type_to_feast_type("CHAR") == String + assert map_dbt_type_to_feast_type("NVARCHAR") == String + + def test_integer_types(self): + """Test integer type mappings.""" + assert map_dbt_type_to_feast_type("INT") == Int64 + assert map_dbt_type_to_feast_type("INT64") == Int64 + assert map_dbt_type_to_feast_type("INTEGER") == Int64 + assert map_dbt_type_to_feast_type("BIGINT") == Int64 + assert map_dbt_type_to_feast_type("INT32") == Int32 + assert map_dbt_type_to_feast_type("SMALLINT") == Int32 + assert map_dbt_type_to_feast_type("TINYINT") == Int32 + + def test_float_types(self): + """Test float type mappings.""" + assert map_dbt_type_to_feast_type("FLOAT") == Float32 + assert map_dbt_type_to_feast_type("FLOAT32") == Float32 + assert map_dbt_type_to_feast_type("FLOAT64") == Float64 + assert map_dbt_type_to_feast_type("DOUBLE") == Float64 + assert map_dbt_type_to_feast_type("DOUBLE PRECISION") == Float64 + assert map_dbt_type_to_feast_type("REAL") == Float32 + + def test_boolean_types(self): + """Test boolean type mappings.""" + assert map_dbt_type_to_feast_type("BOOL") == Bool + assert map_dbt_type_to_feast_type("BOOLEAN") == Bool + + def test_timestamp_types(self): + """Test timestamp type mappings.""" + assert map_dbt_type_to_feast_type("TIMESTAMP") == UnixTimestamp + assert map_dbt_type_to_feast_type("TIMESTAMP_NTZ") == UnixTimestamp + assert map_dbt_type_to_feast_type("TIMESTAMP_LTZ") == UnixTimestamp + assert map_dbt_type_to_feast_type("DATETIME") == UnixTimestamp + assert map_dbt_type_to_feast_type("DATE") == UnixTimestamp + + def test_binary_types(self): + """Test binary type mappings.""" + assert map_dbt_type_to_feast_type("BYTES") == Bytes + assert map_dbt_type_to_feast_type("BINARY") == Bytes + assert map_dbt_type_to_feast_type("VARBINARY") == Bytes + assert map_dbt_type_to_feast_type("BLOB") == Bytes + + def test_case_insensitivity(self): + """Test type mapping is case insensitive.""" + assert map_dbt_type_to_feast_type("string") == String + assert map_dbt_type_to_feast_type("String") == String + assert map_dbt_type_to_feast_type("STRING") == String + assert map_dbt_type_to_feast_type("int64") == Int64 + assert map_dbt_type_to_feast_type("INT64") == Int64 + + def test_parameterized_types(self): + """Test parameterized types are handled correctly.""" + assert map_dbt_type_to_feast_type("VARCHAR(255)") == String + assert map_dbt_type_to_feast_type("CHAR(10)") == String + assert map_dbt_type_to_feast_type("DECIMAL(10,2)") == Int64 + + def test_snowflake_number_precision(self): + """Test Snowflake NUMBER type with precision.""" + # Small precision -> Int32 + assert map_dbt_type_to_feast_type("NUMBER(5,0)") == Int32 + assert map_dbt_type_to_feast_type("NUMBER(9,0)") == Int32 + + # Medium precision -> Int64 + assert map_dbt_type_to_feast_type("NUMBER(10,0)") == Int64 + assert map_dbt_type_to_feast_type("NUMBER(18,0)") == Int64 + + # Large precision -> Float64 + assert map_dbt_type_to_feast_type("NUMBER(20,0)") == Float64 + + # With decimal places -> Float64 + assert map_dbt_type_to_feast_type("NUMBER(10,2)") == Float64 + assert map_dbt_type_to_feast_type("NUMBER(5,3)") == Float64 + + def test_array_types(self): + """Test ARRAY type mappings.""" + result = map_dbt_type_to_feast_type("ARRAY") + assert isinstance(result, Array) + + result = map_dbt_type_to_feast_type("ARRAY") + assert isinstance(result, Array) + + result = map_dbt_type_to_feast_type("ARRAY") + assert isinstance(result, Array) + + def test_unknown_type_defaults_to_string(self): + """Test unknown types default to String.""" + assert map_dbt_type_to_feast_type("UNKNOWN_TYPE") == String + assert map_dbt_type_to_feast_type("CUSTOM") == String + + def test_empty_type_defaults_to_string(self): + """Test empty type defaults to String.""" + assert map_dbt_type_to_feast_type("") == String + assert map_dbt_type_to_feast_type(None) == String + + +@pytest.fixture +def sample_model(): + """Create a sample DbtModel for testing.""" + return DbtModel( + name="driver_stats", + unique_id="model.test_project.driver_stats", + database="my_database", + schema="analytics", + alias="driver_stats", + description="Driver statistics", + columns=[ + DbtColumn(name="driver_id", data_type="INT64", description="Driver ID"), + DbtColumn( + name="event_timestamp", + data_type="TIMESTAMP", + description="Event time", + ), + DbtColumn( + name="trip_count", data_type="INT64", description="Number of trips" + ), + DbtColumn( + name="avg_rating", data_type="FLOAT64", description="Average rating" + ), + DbtColumn( + name="is_active", data_type="BOOLEAN", description="Is driver active" + ), + ], + tags=["feast", "ml"], + meta={"owner": "ml-team"}, + ) + + +class TestDbtToFeastMapper: + """Tests for DbtToFeastMapper class.""" + + def test_create_bigquery_data_source(self, sample_model): + """Test creating a BigQuery data source.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + source = mapper.create_data_source(sample_model) + + assert source.name == "driver_stats_source" + assert source.table == "my_database.analytics.driver_stats" + assert source.timestamp_field == "event_timestamp" + assert "dbt.model" in source.tags + assert source.tags["dbt.model"] == "driver_stats" + + def test_create_snowflake_data_source(self, sample_model): + """Test creating a Snowflake data source.""" + mapper = DbtToFeastMapper(data_source_type="snowflake") + source = mapper.create_data_source(sample_model) + + assert source.name == "driver_stats_source" + assert source.database == "my_database" + assert source.schema == "analytics" + assert source.table == "driver_stats" + assert source.timestamp_field == "event_timestamp" + + def test_create_file_data_source(self, sample_model): + """Test creating a file data source.""" + mapper = DbtToFeastMapper(data_source_type="file") + source = mapper.create_data_source(sample_model) + + assert source.name == "driver_stats_source" + assert "driver_stats.parquet" in source.path + + def test_unsupported_data_source_type(self, sample_model): + """Test error for unsupported data source type.""" + mapper = DbtToFeastMapper(data_source_type="unsupported") + + with pytest.raises(ValueError) as exc_info: + mapper.create_data_source(sample_model) + + assert "Unsupported data_source_type" in str(exc_info.value) + + def test_custom_timestamp_field(self, sample_model): + """Test custom timestamp field.""" + mapper = DbtToFeastMapper( + data_source_type="bigquery", timestamp_field="created_at" + ) + source = mapper.create_data_source(sample_model) + + assert source.timestamp_field == "created_at" + + def test_create_entity(self): + """Test creating an entity.""" + mapper = DbtToFeastMapper() + entity = mapper.create_entity( + name="driver_id", + description="Driver entity", + tags={"source": "dbt"}, + ) + + assert entity.name == "driver_id" + assert entity.join_key == "driver_id" + assert entity.description == "Driver entity" + assert entity.tags == {"source": "dbt"} + + def test_create_feature_view(self, sample_model): + """Test creating a feature view.""" + mapper = DbtToFeastMapper(data_source_type="bigquery", ttl_days=7) + source = mapper.create_data_source(sample_model) + fv = mapper.create_feature_view( + model=sample_model, + source=source, + entity_columns="driver_id", + ) + + assert fv.name == "driver_stats" + assert fv.ttl == timedelta(days=7) + assert fv.description == "Driver statistics" + + # Check features (should exclude entity and timestamp) + feature_names = [f.name for f in fv.features] + assert "trip_count" in feature_names + assert "avg_rating" in feature_names + assert "is_active" in feature_names + assert "driver_id" not in feature_names + assert "event_timestamp" not in feature_names + + # Check tags + assert "dbt.model" in fv.tags + assert fv.tags["dbt.model"] == "driver_stats" + assert "dbt.tag.feast" in fv.tags + + def test_create_feature_view_with_exclude(self, sample_model): + """Test excluding columns from feature view.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + source = mapper.create_data_source(sample_model) + fv = mapper.create_feature_view( + model=sample_model, + source=source, + entity_columns="driver_id", + exclude_columns=["is_active"], + ) + + feature_names = [f.name for f in fv.features] + assert "trip_count" in feature_names + assert "avg_rating" in feature_names + assert "is_active" not in feature_names + + def test_create_all_from_model(self, sample_model): + """Test creating all Feast objects from a model.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + result = mapper.create_all_from_model( + model=sample_model, + entity_columns="driver_id", + ) + + assert "entities" in result + assert "data_source" in result + assert "feature_view" in result + + assert len(result["entities"]) == 1 + assert result["entities"][0].name == "driver_id" + assert result["data_source"].name == "driver_stats_source" + assert result["feature_view"].name == "driver_stats" + + def test_feature_type_mapping(self, sample_model): + """Test that feature types are correctly mapped.""" + mapper = DbtToFeastMapper(data_source_type="bigquery") + source = mapper.create_data_source(sample_model) + fv = mapper.create_feature_view( + model=sample_model, + source=source, + entity_columns="driver_id", + ) + + # Find specific features and check types + trip_count = next((f for f in fv.features if f.name == "trip_count"), None) + avg_rating = next((f for f in fv.features if f.name == "avg_rating"), None) + is_active = next((f for f in fv.features if f.name == "is_active"), None) + + assert trip_count is not None + assert trip_count.dtype == Int64 + + assert avg_rating is not None + assert avg_rating.dtype == Float64 + + assert is_active is not None + assert is_active.dtype == Bool diff --git a/sdk/python/tests/unit/dbt/test_parser.py b/sdk/python/tests/unit/dbt/test_parser.py new file mode 100644 index 00000000000..af4a5c3c366 --- /dev/null +++ b/sdk/python/tests/unit/dbt/test_parser.py @@ -0,0 +1,361 @@ +""" +Unit tests for dbt manifest parser. +""" + +import json + +import pytest + +# Skip all tests in this module if dbt-artifacts-parser is not installed +pytest.importorskip("dbt_artifacts_parser", reason="dbt-artifacts-parser not installed") + +from feast.dbt.parser import DbtColumn, DbtManifestParser, DbtModel + + +def _create_model_node( + name: str, + unique_id: str, + database: str = "my_database", + schema: str = "analytics", + description: str = "", + columns: dict = None, + tags: list = None, + meta: dict = None, + depends_on_nodes: list = None, +): + """Helper to create a complete model node for dbt-artifacts-parser.""" + return { + "name": name, + "unique_id": unique_id, + "resource_type": "model", + "package_name": "test_project", + "path": f"models/{name}.sql", + "original_file_path": f"models/{name}.sql", + "fqn": ["test_project", name], + "alias": name, + "checksum": {"name": "sha256", "checksum": "abc123"}, + "database": database, + "schema": schema, + "description": description or "", + "columns": columns or {}, + "tags": tags or [], + "meta": meta or {}, + "config": { + "enabled": True, + "materialized": "table", + "tags": tags or [], + "meta": meta or {}, + }, + "depends_on": {"nodes": depends_on_nodes or [], "macros": []}, + "refs": [], + "sources": [], + "metrics": [], + "compiled_path": f"target/compiled/test_project/models/{name}.sql", + } + + +def _create_column( + name: str, + data_type: str = "STRING", + description: str = "", + tags: list = None, + meta: dict = None, +): + """Helper to create a column definition.""" + return { + "name": name, + "description": description or "", + "data_type": data_type, + "tags": tags or [], + "meta": meta or {}, + } + + +@pytest.fixture +def sample_manifest(tmp_path): + """Create a sample dbt manifest.json for testing.""" + manifest = { + "metadata": { + "dbt_schema_version": "https://schemas.getdbt.com/dbt/manifest/v9.json", + "dbt_version": "1.5.0", + "generated_at": "2024-01-10T00:00:00Z", + "invocation_id": "test-invocation-id", + "env": {}, + "adapter_type": "bigquery", + }, + "nodes": { + "model.test_project.driver_stats": _create_model_node( + name="driver_stats", + unique_id="model.test_project.driver_stats", + description="Driver statistics aggregated hourly", + columns={ + "driver_id": _create_column( + "driver_id", "INT64", "Unique driver identifier", ["entity"] + ), + "event_timestamp": _create_column( + "event_timestamp", "TIMESTAMP", "Event timestamp" + ), + "trip_count": _create_column( + "trip_count", "INT64", "Number of trips", ["feature"] + ), + "avg_rating": _create_column( + "avg_rating", "FLOAT64", "Average driver rating", ["feature"] + ), + }, + tags=["feast", "ml"], + meta={"owner": "data-team"}, + depends_on_nodes=["source.test_project.raw_trips"], + ), + "model.test_project.customer_stats": _create_model_node( + name="customer_stats", + unique_id="model.test_project.customer_stats", + description="Customer statistics", + columns={ + "customer_id": _create_column( + "customer_id", "STRING", "Unique customer ID" + ), + "event_timestamp": _create_column( + "event_timestamp", "TIMESTAMP", "Event timestamp" + ), + "order_count": _create_column( + "order_count", "INT64", "Total orders" + ), + }, + tags=["ml"], + ), + }, + "sources": {}, + "macros": {}, + "docs": {}, + "exposures": {}, + "metrics": {}, + "groups": {}, + "selectors": {}, + "disabled": {}, + "parent_map": {}, + "child_map": {}, + } + + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps(manifest)) + return manifest_path + + +class TestDbtManifestParser: + """Tests for DbtManifestParser class.""" + + def test_parse_manifest(self, sample_manifest): + """Test parsing a valid manifest file.""" + parser = DbtManifestParser(str(sample_manifest)) + parser.parse() + + assert parser.dbt_version == "1.5.0" + + def test_parse_manifest_not_found(self, tmp_path): + """Test error when manifest file doesn't exist.""" + parser = DbtManifestParser(str(tmp_path / "nonexistent.json")) + + with pytest.raises(FileNotFoundError) as exc_info: + parser.parse() + + assert "dbt manifest not found" in str(exc_info.value) + + def test_parse_manifest_invalid_json(self, tmp_path): + """Test error when manifest is invalid JSON.""" + invalid_path = tmp_path / "invalid.json" + invalid_path.write_text("not valid json {{{") + + parser = DbtManifestParser(str(invalid_path)) + + with pytest.raises(ValueError) as exc_info: + parser.parse() + + assert "Invalid JSON" in str(exc_info.value) + + def test_parse_retries_with_sanitized_supported_languages( + self, tmp_path, monkeypatch + ): + """Test parser retries after removing unsupported macro languages.""" + manifest = { + "metadata": { + "dbt_schema_version": "https://schemas.getdbt.com/dbt/manifest/v12.json", + "dbt_version": "1.11.11", + }, + "nodes": {}, + "macros": { + "macro.dbt.materialization_function_default": { + "name": "materialization_function_default", + "supported_languages": ["python", "sql", "javascript"], + } + }, + } + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps(manifest)) + + class _ParsedManifest: + nodes = {} + metadata = None + + call_manifests = [] + + def _fake_parse_manifest(*, manifest): + call_manifests.append(manifest) + if len(call_manifests) == 1: + raise Exception("supported_languages Input should be 'python' or 'sql'") + return _ParsedManifest() + + monkeypatch.setattr( + "dbt_artifacts_parser.parser.parse_manifest", _fake_parse_manifest + ) + + parser = DbtManifestParser(str(manifest_path)) + parser.parse() + + assert len(call_manifests) == 2 + assert call_manifests[0]["macros"][ + "macro.dbt.materialization_function_default" + ]["supported_languages"] == ["python", "sql", "javascript"] + assert call_manifests[1]["macros"][ + "macro.dbt.materialization_function_default" + ]["supported_languages"] == ["python", "sql"] + + def test_get_all_models(self, sample_manifest): + """Test getting all models from manifest.""" + parser = DbtManifestParser(str(sample_manifest)) + models = parser.get_models() + + assert len(models) == 2 + model_names = [m.name for m in models] + assert "driver_stats" in model_names + assert "customer_stats" in model_names + + def test_get_models_by_name(self, sample_manifest): + """Test filtering models by name.""" + parser = DbtManifestParser(str(sample_manifest)) + models = parser.get_models(model_names=["driver_stats"]) + + assert len(models) == 1 + assert models[0].name == "driver_stats" + + def test_get_models_by_tag(self, sample_manifest): + """Test filtering models by tag.""" + parser = DbtManifestParser(str(sample_manifest)) + + # Filter by 'feast' tag - only driver_stats has it + feast_models = parser.get_models(tag_filter="feast") + assert len(feast_models) == 1 + assert feast_models[0].name == "driver_stats" + + # Filter by 'ml' tag - both models have it + ml_models = parser.get_models(tag_filter="ml") + assert len(ml_models) == 2 + + def test_model_properties(self, sample_manifest): + """Test DbtModel properties.""" + parser = DbtManifestParser(str(sample_manifest)) + model = parser.get_model_by_name("driver_stats") + + assert model is not None + assert model.name == "driver_stats" + assert model.unique_id == "model.test_project.driver_stats" + assert model.database == "my_database" + assert model.schema == "analytics" + assert model.alias == "driver_stats" + assert model.description == "Driver statistics aggregated hourly" + assert model.full_table_name == "my_database.analytics.driver_stats" + assert "feast" in model.tags + assert "ml" in model.tags + assert len(model.columns) == 4 + + def test_column_properties(self, sample_manifest): + """Test DbtColumn properties.""" + parser = DbtManifestParser(str(sample_manifest)) + model = parser.get_model_by_name("driver_stats") + + # Find the trip_count column + trip_count_col = next( + (c for c in model.columns if c.name == "trip_count"), None + ) + + assert trip_count_col is not None + assert trip_count_col.name == "trip_count" + assert trip_count_col.data_type == "INT64" + assert trip_count_col.description == "Number of trips" + assert "feature" in trip_count_col.tags + + def test_get_model_by_name_not_found(self, sample_manifest): + """Test getting a model that doesn't exist.""" + parser = DbtManifestParser(str(sample_manifest)) + model = parser.get_model_by_name("nonexistent_model") + + assert model is None + + def test_depends_on(self, sample_manifest): + """Test model dependencies are captured.""" + parser = DbtManifestParser(str(sample_manifest)) + model = parser.get_model_by_name("driver_stats") + + assert len(model.depends_on) == 1 + assert "source.test_project.raw_trips" in model.depends_on + + +class TestDbtColumn: + """Tests for DbtColumn dataclass.""" + + def test_column_defaults(self): + """Test DbtColumn default values.""" + col = DbtColumn(name="test_col") + + assert col.name == "test_col" + assert col.description == "" + assert col.data_type == "STRING" + assert col.tags == [] + assert col.meta == {} + + def test_column_with_all_fields(self): + """Test DbtColumn with all fields specified.""" + col = DbtColumn( + name="feature_col", + description="A feature column", + data_type="FLOAT64", + tags=["feature", "numeric"], + meta={"owner": "ml-team"}, + ) + + assert col.name == "feature_col" + assert col.description == "A feature column" + assert col.data_type == "FLOAT64" + assert col.tags == ["feature", "numeric"] + assert col.meta == {"owner": "ml-team"} + + +class TestDbtModel: + """Tests for DbtModel dataclass.""" + + def test_model_full_table_name(self): + """Test full_table_name property.""" + model = DbtModel( + name="test_model", + unique_id="model.proj.test_model", + database="prod_db", + schema="public", + alias="test_model_v2", + ) + + assert model.full_table_name == "prod_db.public.test_model_v2" + + def test_model_defaults(self): + """Test DbtModel default values.""" + model = DbtModel( + name="test", + unique_id="model.proj.test", + database="db", + schema="schema", + alias="test", + ) + + assert model.description == "" + assert model.columns == [] + assert model.tags == [] + assert model.meta == {} + assert model.depends_on == [] diff --git a/sdk/python/tests/unit/infra/compute_engines/flink/__init__.py b/sdk/python/tests/unit/infra/compute_engines/flink/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/flink/__init__.py @@ -0,0 +1 @@ + diff --git a/sdk/python/tests/unit/infra/compute_engines/flink/test_flink_compute_engine.py b/sdk/python/tests/unit/infra/compute_engines/flink/test_flink_compute_engine.py new file mode 100644 index 00000000000..95dea15759d --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/flink/test_flink_compute_engine.py @@ -0,0 +1,1198 @@ +from __future__ import annotations + +import re +import sys +import types +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any, List, Optional +from unittest.mock import MagicMock + +import pandas as pd +import pyarrow as pa +import pytest + +from feast import BatchFeatureView, Entity, Field, FileSource +from feast.aggregation import Aggregation +from feast.infra.common.materialization_job import ( + MaterializationJobStatus, + MaterializationTask, +) +from feast.infra.common.retrieval_task import HistoricalRetrievalTask +from feast.infra.compute_engines.dag.context import ColumnInfo, ExecutionContext +from feast.infra.compute_engines.dag.model import DAGFormat +from feast.infra.compute_engines.dag.node import DAGNode +from feast.infra.compute_engines.dag.value import DAGValue +from feast.infra.compute_engines.flink.compute import ( + FlinkComputeEngine, + FlinkComputeEngineConfig, +) +from feast.infra.compute_engines.flink.nodes import ( + ENTITY_ROW_ID, + ENTITY_TS_ALIAS, + FlinkAggregationNode, + FlinkDedupNode, + FlinkFilterNode, + FlinkJoinNode, + FlinkOutputNode, + FlinkSourceReadNode, + FlinkTransformationNode, + FlinkValidationNode, + _subtract_flink_intervals, +) +from feast.infra.compute_engines.flink.utils import pandas_to_flink_table +from feast.infra.offline_stores.offline_store import RetrievalJob, RetrievalMetadata +from feast.on_demand_feature_view import OnDemandFeatureView +from feast.repo_config import RepoConfig +from feast.saved_dataset import SavedDatasetStorage +from feast.types import Float32 +from feast.value_type import ValueType + + +class FakeFlinkTable: + def __init__(self, df: pd.DataFrame, fail_on_to_pandas: bool = False) -> None: + self._df = df.copy() + self.fail_on_to_pandas = fail_on_to_pandas + + def to_pandas(self) -> pd.DataFrame: + if self.fail_on_to_pandas: + raise AssertionError("FlinkOutputNode should not collect via to_pandas()") + return self._df.copy() + + def get_schema(self) -> FakeFlinkSchema: + return FakeFlinkSchema(list(self._df.columns)) + + def execute(self) -> FakeTableResult: + return FakeTableResult(self._df) + + +class FakeTableResult: + def __init__(self, df: pd.DataFrame) -> None: + self._df = df.copy() + + def collect(self) -> FakeCloseableIterator: + return FakeCloseableIterator(self._df) + + +class FakeCloseableIterator: + def __init__(self, df: pd.DataFrame) -> None: + self._rows = iter(df.itertuples(index=False, name=None)) + self.closed = False + + def __iter__(self) -> FakeCloseableIterator: + return self + + def __next__(self) -> tuple[Any, ...]: + return next(self._rows) + + def close(self) -> None: + self.closed = True + + +class FakeTableEnvironment: + def __init__(self) -> None: + self.created_tables: List[pd.DataFrame] = [] + self.schemas: List[object] = [] + self.split_nums: List[int] = [] + self.views: dict[str, object] = {} + self.dropped_views: List[str] = [] + self.queries: List[str] = [] + + def from_pandas( + self, + df: pd.DataFrame, + schema: object = None, + splits_num: int = 1, + split_num: Optional[int] = None, + ) -> FakeFlinkTable: + self.created_tables.append(df.copy()) + self.schemas.append(schema) + self.split_nums.append(split_num if split_num is not None else splits_num) + return FakeFlinkTable(df) + + def create_temporary_view( + self, view_path: str, table_or_data_stream: object, *args: object + ) -> None: + self.views[view_path] = table_or_data_stream + + def drop_temporary_view(self, view_path: str) -> None: + self.dropped_views.append(view_path) + self.views.pop(view_path, None) + + def sql_query(self, query: str) -> Any: + self.queries.append(query) + return FakeFlinkTable(self._evaluate_sql(query)) + + def _view_df(self, view_name: str) -> pd.DataFrame: + table = self.views[view_name] + if isinstance(table, FakeFlinkTable): + return table.to_pandas() + if isinstance(table, FakeNativeFlinkTable): + return pd.DataFrame(columns=table.get_schema().get_field_names()) + raise TypeError(f"Unsupported fake Flink table type: {type(table)}") + + def _evaluate_sql(self, query: str) -> pd.DataFrame: + if "ROW_NUMBER() OVER" in query: + return self._evaluate_row_number_query(query) + if " GROUP BY " in query: + return self._evaluate_group_by_query(query) + if " JOIN " in query: + return self._evaluate_join_query(query) + if " WHERE " in query: + return self._evaluate_where_query(query) + return self._evaluate_select_query(query) + + def _extract_views(self, query: str) -> List[str]: + return re.findall(r"(?:FROM|JOIN)\s+`([^`]+)`", query) + + def _evaluate_select_query(self, query: str) -> pd.DataFrame: + views = self._extract_views(query) + if not views: + if "FROM entities" in query: + return self._view_df("entities")[["driver_id", "event_timestamp"]] + raise ValueError(f"Could not infer source view from query: {query}") + source_df = self._view_df(views[-1]) + select_clause = query.split(" FROM ", 1)[0].removeprefix("SELECT ") + if select_clause == "*": + return source_df + result = pd.DataFrame() + for column_expr in select_clause.split(", "): + parts = re.findall(r"`([^`]+)`", column_expr) + if not parts: + continue + source_column = parts[0] + output_column = parts[-1] + result[output_column] = source_df[source_column] + return result + + def _evaluate_where_query(self, query: str) -> pd.DataFrame: + view_name = self._extract_views(query)[0] + df = self._view_df(view_name) + if "`event_timestamp` <= `__entity_event_timestamp`" in query: + df = df[df["event_timestamp"] <= df[ENTITY_TS_ALIAS]] + if "conv_rate > 0.15" in query: + df = df[df["conv_rate"] > 0.15] + return df.reset_index(drop=True) + + def _evaluate_group_by_query(self, query: str) -> pd.DataFrame: + view_name = self._extract_views(query)[0] + df = self._view_df(view_name) + return ( + df.groupby("driver_id") + .agg(sum_conv_rate=pd.NamedAgg(column="conv_rate", aggfunc="sum")) + .reset_index() + ) + + def _evaluate_row_number_query(self, query: str) -> pd.DataFrame: + view_name = self._extract_views(query)[-1] + df = self._view_df(view_name) + if " - 1 AS " in query: + df = df.copy() + df[ENTITY_ROW_ID] = range(len(df)) + if " AS `__entity_event_timestamp`" in query: + df = df.rename(columns={"event_timestamp": ENTITY_TS_ALIAS}) + return df.reset_index(drop=True) + + dedup_keys = [ENTITY_ROW_ID] if ENTITY_ROW_ID in df.columns else ["driver_id"] + sort_keys = [ + column for column in ["event_timestamp", "created"] if column in df + ] + return ( + df.sort_values(by=sort_keys, ascending=False) + .drop_duplicates(subset=dedup_keys) + .reset_index(drop=True) + ) + + def _evaluate_join_query(self, query: str) -> pd.DataFrame: + views = self._extract_views(query) + if " AS e LEFT JOIN " in query: + entity_df = self._view_df(views[-2]) + feature_df = self._view_df(views[-1]) + feature_columns = [ + column + for column in feature_df.columns + if column not in entity_df.columns and column != "driver_id" + ] + return entity_df.merge( + feature_df[["driver_id", *feature_columns]], + on="driver_id", + how="left", + ) + + joined_df = self._view_df(views[0]) + for view_name in views[1:]: + joined_df = joined_df.merge( + self._view_df(view_name), on="driver_id", how="left" + ) + return joined_df + + +class FakeFlinkSchema: + def __init__(self, columns: List[str]) -> None: + self._columns = columns + + def get_field_names(self) -> List[str]: + return list(self._columns) + + +class FakeNativeFlinkTable: + def __init__(self, columns: List[str]) -> None: + self._columns = columns + + def get_schema(self) -> FakeFlinkSchema: + return FakeFlinkSchema(self._columns) + + +class RecordingTableEnvironment(FakeTableEnvironment): + def __init__(self) -> None: + super().__init__() + + def create_temporary_view( + self, view_path: str, table_or_data_stream: object, *args: object + ) -> None: + self.views[view_path] = table_or_data_stream + + def sql_query(self, query: str) -> FakeNativeFlinkTable: + self.queries.append(query) + return FakeNativeFlinkTable([]) + + +class InputNode(DAGNode): + def execute(self, context: ExecutionContext) -> DAGValue: + return context.node_outputs[self.name] + + +class FakeRetrievalJob(RetrievalJob): + def __init__(self, table: pa.Table) -> None: + self._table = table + + def _to_df_internal(self, timeout: Optional[int] = None) -> pd.DataFrame: + return self._table.to_pandas() + + def _to_arrow_internal(self, timeout: Optional[int] = None) -> pa.Table: + return self._table + + @property + def full_feature_names(self) -> bool: + return False + + @property + def on_demand_feature_views(self) -> List[OnDemandFeatureView]: + return [] + + @property + def metadata(self) -> Optional[RetrievalMetadata]: + return None + + def persist( + self, + storage: SavedDatasetStorage, + allow_overwrite: bool = False, + timeout: Optional[int] = None, + ) -> None: + raise NotImplementedError + + def to_remote_storage(self) -> List[str]: + raise NotImplementedError + + def to_sql(self) -> str: + raise NotImplementedError + + +class FakeFlinkRetrievalJob: + def __init__(self, df: pd.DataFrame) -> None: + self._table = FakeFlinkTable(df) + + def to_flink_table(self, table_env: object) -> FakeFlinkTable: + return self._table + + +def _repo_config(tmp_path: Path, batch_engine: dict[str, object]) -> RepoConfig: + return RepoConfig( + project="test_project", + registry=str(tmp_path / "registry.db"), + provider="local", + offline_store={"type": "file"}, + online_store={"type": "sqlite", "path": str(tmp_path / "online.db")}, + batch_engine=batch_engine, + ) + + +def _driver() -> Entity: + return Entity(name="driver_id", value_type=ValueType.INT64) + + +def _source() -> FileSource: + return FileSource( + name="driver_stats_source", + path="unused.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + +def _feature_view(source: FileSource, **kwargs: Any) -> BatchFeatureView: + return BatchFeatureView( + name="driver_stats", + entities=[_driver()], + ttl=timedelta(days=2), + schema=[Field(name="conv_rate", dtype=Float32)], + source=source, + **kwargs, + ) + + +def _feature_data() -> pd.DataFrame: + return pd.DataFrame( + { + "driver_id": [1, 1, 2], + "event_timestamp": [ + datetime(2024, 1, 1, 9, 0, 0), + datetime(2024, 1, 1, 10, 0, 0), + datetime(2024, 1, 1, 10, 0, 0), + ], + "created": [ + datetime(2024, 1, 1, 9, 1, 0), + datetime(2024, 1, 1, 10, 1, 0), + datetime(2024, 1, 1, 10, 1, 0), + ], + "conv_rate": [0.1, 0.2, 0.3], + } + ) + + +def _offline_store(df: pd.DataFrame) -> MagicMock: + store = MagicMock() + store.pull_all_from_table_or_query.return_value = FakeFlinkRetrievalJob(df) + store.pull_latest_from_table_or_query.return_value = FakeFlinkRetrievalJob(df) + return store + + +def _registry(entity: Entity) -> MagicMock: + registry = MagicMock() + registry.get_entity.return_value = entity + return registry + + +def _column_info() -> ColumnInfo: + return ColumnInfo( + join_keys=["driver_id"], + feature_cols=["conv_rate"], + ts_col="event_timestamp", + created_ts_col="created", + ) + + +def _execution_context( + tmp_path: Path, node_outputs: dict[str, DAGValue] +) -> ExecutionContext: + return ExecutionContext( + project="test_project", + repo_config=_repo_config(tmp_path, {"type": "flink.engine"}), + offline_store=MagicMock(), + online_store=MagicMock(), + entity_defs=[_driver()], + node_outputs=node_outputs, + ) + + +def _flink_value(df: pd.DataFrame) -> DAGValue: + return DAGValue( + data=FakeFlinkTable(df), + format=DAGFormat.FLINK, + metadata={"columns": list(df.columns)}, + ) + + +def test_pandas_to_flink_table_builds_typed_schema( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class FakeSchemaBuilder: + def __init__(self) -> None: + self.columns: List[tuple[str, str]] = [] + + def column(self, name: str, dtype: str) -> FakeSchemaBuilder: + self.columns.append((name, dtype)) + return self + + def build(self) -> List[tuple[str, str]]: + return self.columns + + class FakeSchema: + @staticmethod + def new_builder() -> FakeSchemaBuilder: + return FakeSchemaBuilder() + + class FakeDataTypes: + @staticmethod + def BOOLEAN() -> str: + return "BOOLEAN" + + @staticmethod + def TINYINT() -> str: + return "TINYINT" + + @staticmethod + def SMALLINT() -> str: + return "SMALLINT" + + @staticmethod + def INT() -> str: + return "INT" + + @staticmethod + def BIGINT() -> str: + return "BIGINT" + + @staticmethod + def FLOAT() -> str: + return "FLOAT" + + @staticmethod + def DOUBLE() -> str: + return "DOUBLE" + + @staticmethod + def TIMESTAMP(precision: int) -> str: + return f"TIMESTAMP({precision})" + + @staticmethod + def TIMESTAMP_LTZ(precision: int) -> str: + return f"TIMESTAMP_LTZ({precision})" + + @staticmethod + def BYTES() -> str: + return "BYTES" + + @staticmethod + def DATE() -> str: + return "DATE" + + @staticmethod + def DECIMAL(precision: int, scale: int) -> str: + return f"DECIMAL({precision},{scale})" + + @staticmethod + def STRING() -> str: + return "STRING" + + pyflink_module = types.ModuleType("pyflink") + table_module = types.ModuleType("pyflink.table") + setattr(table_module, "Schema", FakeSchema) + setattr(table_module, "DataTypes", FakeDataTypes) + monkeypatch.setitem(sys.modules, "pyflink", pyflink_module) + monkeypatch.setitem(sys.modules, "pyflink.table", table_module) + + table_env = FakeTableEnvironment() + df = pd.DataFrame( + { + "driver_id": pd.Series([1, 2], dtype="int64"), + "conv_rate": pd.Series([0.1, 0.2], dtype="float64"), + "event_timestamp": pd.to_datetime( + ["2024-01-01 00:00:00", "2024-01-02 00:00:00"] + ), + "active": pd.Series([True, False], dtype="bool"), + } + ) + + pandas_to_flink_table(table_env, df, split_num=4) + + assert table_env.schemas[-1] == [ + ("driver_id", "BIGINT"), + ("conv_rate", "DOUBLE"), + ("event_timestamp", "TIMESTAMP(3)"), + ("active", "BOOLEAN"), + ] + assert table_env.schemas[-1] != list(df.columns) + assert table_env.split_nums == [4] + + +def _native_flink_value(columns: List[str]) -> DAGValue: + return DAGValue( + data=FakeNativeFlinkTable(columns), + format=DAGFormat.FLINK, + metadata={"columns": columns}, + ) + + +def test_repo_config_loads_flink_batch_engine_config(tmp_path: Path) -> None: + config = _repo_config( + tmp_path, + { + "type": "flink.engine", + "execution_mode": "streaming", + "parallelism": 3, + "table_config": {"pipeline.name": "feast-flink-test"}, + "pandas_split_num": 2, + }, + ) + + assert isinstance(config.batch_engine, FlinkComputeEngineConfig) + assert config.batch_engine.execution_mode == "streaming" + assert config.batch_engine.parallelism == 3 + assert config.batch_engine.table_config == {"pipeline.name": "feast-flink-test"} + assert config.batch_engine.pandas_split_num == 2 + + +def test_flink_source_read_node_converts_arrow_retrieval_jobs( + tmp_path: Path, +) -> None: + offline_store = MagicMock() + offline_store.pull_all_from_table_or_query.return_value = FakeRetrievalJob( + pa.Table.from_pandas(_feature_data()) + ) + context = _execution_context(tmp_path, {}) + context.offline_store = offline_store + node = FlinkSourceReadNode( + "source", + _source(), + _column_info(), + FakeTableEnvironment(), + split_num=1, + ) + + result = node.execute(context) + + assert result.format == DAGFormat.FLINK + assert result.metadata["columns"] == list(_feature_data().columns) + assert result.data.to_pandas().equals(_feature_data()) + + +def test_flink_historical_retrieval_executes_dag_with_transformation( + tmp_path: Path, +) -> None: + entity = _driver() + source = _source() + + def double_conv_rate(table: FakeFlinkTable) -> FakeFlinkTable: + df = table.to_pandas() + df["conv_rate"] = df["conv_rate"] * 2 + return FakeFlinkTable(df) + + feature_view = _feature_view( + source, + mode="flink", + udf=double_conv_rate, + udf_string="double_conv_rate", + online=False, + offline=False, + ) + config = _repo_config( + tmp_path, + {"type": "flink.engine", "pandas_split_num": 4}, + ) + table_env = FakeTableEnvironment() + engine = FlinkComputeEngine( + repo_config=config, + offline_store=_offline_store(_feature_data()), + online_store=MagicMock(), + table_environment=table_env, + ) + task = HistoricalRetrievalTask( + project=config.project, + entity_df=None, + feature_view=feature_view, + full_feature_name=False, + registry=_registry(entity), + ) + + job = engine.get_historical_features(_registry(entity), task) + result = job.to_df().sort_values("driver_id").reset_index(drop=True) + + assert job.error() is None + assert result["driver_id"].tolist() == [1, 2] + assert result["conv_rate"].tolist() == [0.4, 0.6] + assert table_env.dropped_views + assert table_env.views == {} + + +def test_flink_historical_retrieval_with_empty_entity_df_returns_empty_result( + tmp_path: Path, +) -> None: + entity = _driver() + source = _source() + feature_view = _feature_view(source, online=False, offline=False) + config = _repo_config(tmp_path, {"type": "flink.engine", "pandas_split_num": 4}) + table_env = FakeTableEnvironment() + engine = FlinkComputeEngine( + repo_config=config, + offline_store=_offline_store(_feature_data()), + online_store=MagicMock(), + table_environment=table_env, + ) + task = HistoricalRetrievalTask( + project=config.project, + entity_df=pd.DataFrame( + { + "driver_id": pd.Series(dtype="int64"), + "event_timestamp": pd.Series(dtype="datetime64[ns]"), + } + ), + feature_view=feature_view, + full_feature_name=False, + registry=_registry(entity), + ) + + job = engine.get_historical_features(_registry(entity), task) + result = job.to_df() + + assert job.error() is None + assert result.empty + assert "conv_rate" in result.columns + assert table_env.created_tables[-1].empty + + +def test_flink_historical_retrieval_is_read_only_and_dedupes_per_entity_row( + tmp_path: Path, +) -> None: + entity = _driver() + source = _source() + feature_view = _feature_view(source, online=True, offline=True) + config = _repo_config(tmp_path, {"type": "flink.engine", "pandas_split_num": 4}) + feature_data = pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": [ + datetime(2024, 1, 1, 9, 0, 0), + datetime(2024, 1, 1, 10, 0, 0), + ], + "created": [ + datetime(2024, 1, 1, 9, 1, 0), + datetime(2024, 1, 1, 10, 1, 0), + ], + "conv_rate": [0.1, 0.2], + } + ) + offline_store = _offline_store(feature_data) + online_store = MagicMock() + table_env = FakeTableEnvironment() + engine = FlinkComputeEngine( + repo_config=config, + offline_store=offline_store, + online_store=online_store, + table_environment=table_env, + ) + task = HistoricalRetrievalTask( + project=config.project, + entity_df=pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": [ + datetime(2024, 1, 1, 9, 30, 0), + datetime(2024, 1, 1, 10, 30, 0), + ], + } + ), + feature_view=feature_view, + full_feature_name=False, + registry=_registry(entity), + ) + + result = engine.get_historical_features(_registry(entity), task).to_df() + result = result.sort_values(ENTITY_TS_ALIAS).reset_index(drop=True) + + assert result["conv_rate"].tolist() == [0.1, 0.2] + assert table_env.split_nums == [4] + assert ENTITY_ROW_ID not in result.columns + online_store.online_write_batch.assert_not_called() + offline_store.offline_write_batch.assert_not_called() + + +def test_flink_historical_retrieval_supports_sql_entity_df(tmp_path: Path) -> None: + entity = _driver() + source = _source() + feature_view = _feature_view(source, online=False, offline=False) + config = _repo_config(tmp_path, {"type": "flink.engine"}) + table_env = FakeTableEnvironment() + table_env.create_temporary_view( + "entities", + FakeFlinkTable( + pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": [ + datetime(2024, 1, 1, 9, 30, 0), + datetime(2024, 1, 1, 10, 30, 0), + ], + } + ) + ), + ) + engine = FlinkComputeEngine( + repo_config=config, + offline_store=_offline_store(_feature_data()), + online_store=MagicMock(), + table_environment=table_env, + ) + task = HistoricalRetrievalTask( + project=config.project, + entity_df="SELECT driver_id, event_timestamp FROM entities", + feature_view=feature_view, + full_feature_name=False, + registry=_registry(entity), + ) + + job = engine.get_historical_features(_registry(entity), task) + result = job.to_df().sort_values(ENTITY_TS_ALIAS).reset_index(drop=True) + + assert job.error() is None + assert result["conv_rate"].tolist() == [0.1, 0.2] + assert any( + "SELECT driver_id, event_timestamp FROM entities" in query + for query in table_env.queries + ) + assert set(table_env.views) == {"entities"} + + +def test_flink_materialize_writes_online_and_offline(tmp_path: Path) -> None: + entity = _driver() + source = _source() + feature_view = _feature_view(source, online=True, offline=True) + config = _repo_config(tmp_path, {"type": "flink.engine"}) + offline_store = _offline_store(_feature_data().head(1)) + online_store = MagicMock() + table_env = FakeTableEnvironment() + engine = FlinkComputeEngine( + repo_config=config, + offline_store=offline_store, + online_store=online_store, + table_environment=table_env, + ) + task = MaterializationTask( + project=config.project, + feature_view=feature_view, + start_time=datetime(2024, 1, 1), + end_time=datetime(2024, 1, 2), + ) + + jobs = engine.materialize(_registry(entity), task) + + assert len(jobs) == 1 + assert jobs[0].status() == MaterializationJobStatus.SUCCEEDED + assert jobs[0].error() is None + online_store.online_write_batch.assert_called_once() + offline_store.offline_write_batch.assert_called_once() + assert table_env.dropped_views + assert table_env.views == {} + + +def test_flink_output_node_streams_batches_without_full_pandas_collect( + tmp_path: Path, +) -> None: + source = _source() + feature_view = _feature_view(source, online=True, offline=True) + input_node = InputNode("input") + node = FlinkOutputNode( + "output", + feature_view, + FakeTableEnvironment(), + split_num=1, + write_output=True, + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + { + "input": DAGValue( + data=FakeFlinkTable(_feature_data().head(2), fail_on_to_pandas=True), + format=DAGFormat.FLINK, + metadata={"columns": list(_feature_data().columns)}, + ) + }, + ) + context.repo_config.materialization_config.online_write_batch_size = 1 + context.online_store = MagicMock() + context.offline_store = MagicMock() + + node.execute(context) + + assert context.online_store.online_write_batch.call_count == 2 + assert context.offline_store.offline_write_batch.call_count == 2 + + +def test_flink_engine_reports_materialization_errors(tmp_path: Path) -> None: + entity = _driver() + source = _source() + feature_view = _feature_view(source, online=False, offline=False) + offline_store = MagicMock() + offline_store.pull_all_from_table_or_query.side_effect = RuntimeError("boom") + config = _repo_config(tmp_path, {"type": "flink.engine"}) + engine = FlinkComputeEngine( + repo_config=config, + offline_store=offline_store, + online_store=MagicMock(), + table_environment=FakeTableEnvironment(), + ) + task = MaterializationTask( + project=config.project, + feature_view=feature_view, + start_time=datetime(2024, 1, 1), + end_time=datetime(2024, 1, 2), + ) + + jobs = engine.materialize(_registry(entity), task) + + assert jobs[0].status() == MaterializationJobStatus.ERROR + assert isinstance(jobs[0].error(), RuntimeError) + + +def test_flink_join_node_merges_input_tables(tmp_path: Path) -> None: + left = InputNode("left") + right = InputNode("right") + node = FlinkJoinNode( + "join", + _column_info(), + FakeTableEnvironment(), + split_num=1, + inputs=[left, right], + ) + context = _execution_context( + tmp_path, + { + "left": _flink_value( + pd.DataFrame({"driver_id": [1, 2], "conv_rate": [0.1, 0.2]}) + ), + "right": _flink_value( + pd.DataFrame({"driver_id": [1, 2], "acc_rate": [0.3, 0.4]}) + ), + }, + ) + + result = node.execute(context).data.to_pandas().sort_values("driver_id") + + assert result["conv_rate"].tolist() == [0.1, 0.2] + assert result["acc_rate"].tolist() == [0.3, 0.4] + + +def test_flink_join_node_uses_native_sql_when_available(tmp_path: Path) -> None: + left = InputNode("left") + right = InputNode("right") + table_env = RecordingTableEnvironment() + node = FlinkJoinNode( + "join", + _column_info(), + table_env, + split_num=1, + inputs=[left, right], + ) + context = _execution_context( + tmp_path, + { + "left": _native_flink_value(["driver_id", "conv_rate"]), + "right": _native_flink_value(["driver_id", "acc_rate"]), + }, + ) + + result = node.execute(context) + + assert result.format == DAGFormat.FLINK + assert result.metadata["columns"] == ["driver_id", "conv_rate", "acc_rate"] + assert any("JOIN" in query for query in table_env.queries) + + +def test_flink_filter_node_applies_filter_expression(tmp_path: Path) -> None: + input_node = InputNode("input") + node = FlinkFilterNode( + "filter", + _column_info(), + FakeTableEnvironment(), + split_num=1, + filter_expr="conv_rate > 0.15", + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + { + "input": _flink_value( + pd.DataFrame({"driver_id": [1, 2], "conv_rate": [0.1, 0.2]}) + ) + }, + ) + + result = node.execute(context).data.to_pandas() + + assert result["driver_id"].tolist() == [2] + + +def test_flink_filter_node_uses_native_sql_when_available(tmp_path: Path) -> None: + input_node = InputNode("input") + table_env = RecordingTableEnvironment() + node = FlinkFilterNode( + "filter", + _column_info(), + table_env, + split_num=1, + filter_expr="conv_rate > 0.15", + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + {"input": _native_flink_value(["driver_id", "conv_rate"])}, + ) + + result = node.execute(context) + + assert result.format == DAGFormat.FLINK + assert any( + "WHERE" in query and "conv_rate > 0.15" in query for query in table_env.queries + ) + + +def test_flink_filter_node_renders_ttl_as_valid_flink_interval( + tmp_path: Path, +) -> None: + input_node = InputNode("input") + table_env = RecordingTableEnvironment() + node = FlinkFilterNode( + "filter", + _column_info(), + table_env, + split_num=1, + ttl=timedelta(days=2, hours=3, minutes=4, seconds=5), + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + { + "input": _native_flink_value( + ["driver_id", "conv_rate", "event_timestamp", ENTITY_TS_ALIAS] + ) + }, + ) + + node.execute(context) + + assert _subtract_flink_intervals( + "`__entity_event_timestamp`", timedelta(days=2, hours=3, minutes=4, seconds=5) + ) == ( + "`__entity_event_timestamp` - INTERVAL '2' DAY - INTERVAL '3' HOUR " + "- INTERVAL '4' MINUTE - INTERVAL '5' SECOND" + ) + assert any( + "`event_timestamp` >= `__entity_event_timestamp` - INTERVAL '2' DAY " + "- INTERVAL '3' HOUR - INTERVAL '4' MINUTE - INTERVAL '5' SECOND" in query + for query in table_env.queries + ) + assert all("+ INTERVAL" not in query for query in table_env.queries) + + +def test_flink_aggregation_node_groups_features(tmp_path: Path) -> None: + input_node = InputNode("input") + node = FlinkAggregationNode( + "agg", + ["driver_id"], + aggregations=[Aggregation(column="conv_rate", function="sum")], + table_env=FakeTableEnvironment(), + split_num=1, + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + { + "input": _flink_value( + pd.DataFrame({"driver_id": [1, 1, 2], "conv_rate": [0.1, 0.2, 0.3]}) + ) + }, + ) + + result = node.execute(context).data.to_pandas().sort_values("driver_id") + + assert result["sum_conv_rate"].tolist() == pytest.approx([0.3, 0.3]) + + +def test_flink_aggregation_node_uses_native_sql_when_available(tmp_path: Path) -> None: + input_node = InputNode("input") + table_env = RecordingTableEnvironment() + node = FlinkAggregationNode( + "agg", + ["driver_id"], + aggregations=[Aggregation(column="conv_rate", function="sum")], + table_env=table_env, + split_num=1, + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + {"input": _native_flink_value(["driver_id", "conv_rate"])}, + ) + + result = node.execute(context) + + assert result.format == DAGFormat.FLINK + assert result.metadata["columns"] == ["driver_id", "sum_conv_rate"] + assert any("GROUP BY" in query and "SUM" in query for query in table_env.queries) + + +def test_flink_dedup_node_uses_entity_row_id_for_historical_retrieval( + tmp_path: Path, +) -> None: + input_node = InputNode("input") + node = FlinkDedupNode( + "dedup", + _column_info(), + FakeTableEnvironment(), + split_num=1, + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + { + "input": _flink_value( + pd.DataFrame( + { + ENTITY_ROW_ID: [0, 0, 1], + "driver_id": [1, 1, 1], + "event_timestamp": [ + datetime(2024, 1, 1, 9, 0, 0), + datetime(2024, 1, 1, 10, 0, 0), + datetime(2024, 1, 1, 10, 0, 0), + ], + "created": [ + datetime(2024, 1, 1, 9, 1, 0), + datetime(2024, 1, 1, 10, 1, 0), + datetime(2024, 1, 1, 10, 1, 0), + ], + "conv_rate": [0.1, 0.2, 0.3], + } + ) + ) + }, + ) + + result = node.execute(context).data.to_pandas().sort_values(ENTITY_ROW_ID) + + assert result["conv_rate"].tolist() == [0.2, 0.3] + + +def test_flink_dedup_node_uses_native_row_number_when_available( + tmp_path: Path, +) -> None: + input_node = InputNode("input") + table_env = RecordingTableEnvironment() + node = FlinkDedupNode( + "dedup", + _column_info(), + table_env, + split_num=1, + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + { + "input": _native_flink_value( + [ENTITY_ROW_ID, "driver_id", "event_timestamp", "created", "conv_rate"] + ) + }, + ) + + result = node.execute(context) + + assert result.format == DAGFormat.FLINK + assert any("ROW_NUMBER() OVER" in query for query in table_env.queries) + assert ENTITY_ROW_ID in result.metadata["columns"] + + +def test_flink_transformation_node_keeps_native_flink_table(tmp_path: Path) -> None: + input_node = InputNode("input") + native_result = FakeNativeFlinkTable(["driver_id", "conv_rate"]) + + def native_udf(table: object) -> FakeNativeFlinkTable: + return native_result + + node = FlinkTransformationNode( + "transform", + native_udf, + RecordingTableEnvironment(), + split_num=1, + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + {"input": _native_flink_value(["driver_id", "conv_rate"])}, + ) + + result = node.execute(context) + + assert result.data is native_result + assert result.metadata["columns"] == ["driver_id", "conv_rate"] + + +def test_flink_validation_node_raises_for_missing_columns(tmp_path: Path) -> None: + input_node = InputNode("input") + node = FlinkValidationNode( + "validate", + expected_columns={"missing_feature": pa.float32()}, + json_columns=set(), + table_env=FakeTableEnvironment(), + split_num=1, + inputs=[input_node], + ) + context = _execution_context( + tmp_path, + {"input": _flink_value(pd.DataFrame({"driver_id": [1]}))}, + ) + + with pytest.raises(ValueError, match="Missing expected columns"): + node.execute(context) + + +@pytest.mark.integration +@pytest.mark.slow +def test_flink_compute_engine_executes_with_real_pyflink_when_installed( + tmp_path: Path, +) -> None: + pyflink_table = pytest.importorskip( + "pyflink.table", reason="PyFlink is required for this runtime smoke test" + ) + entity = _driver() + source = _source() + feature_view = _feature_view(source, online=True, offline=True) + config = _repo_config(tmp_path, {"type": "flink.engine"}) + offline_store = _offline_store(_feature_data()) + online_store = MagicMock() + table_env = pyflink_table.TableEnvironment.create( + pyflink_table.EnvironmentSettings.new_instance().in_batch_mode().build() + ) + engine = FlinkComputeEngine( + repo_config=config, + offline_store=offline_store, + online_store=online_store, + table_environment=table_env, + ) + task = HistoricalRetrievalTask( + project=config.project, + entity_df=pd.DataFrame( + { + "driver_id": [1, 1, 2], + "event_timestamp": [ + datetime(2024, 1, 1, 9, 30, 0), + datetime(2024, 1, 1, 10, 30, 0), + datetime(2024, 1, 1, 10, 30, 0), + ], + } + ), + feature_view=feature_view, + full_feature_name=False, + registry=_registry(entity), + ) + + result = engine.get_historical_features(_registry(entity), task).to_df() + result = result.sort_values(["driver_id", ENTITY_TS_ALIAS]).reset_index(drop=True) + + assert result["conv_rate"].tolist() == [0.1, 0.2, 0.3] + assert ENTITY_ROW_ID not in result.columns + online_store.online_write_batch.assert_not_called() + + materialization_task = MaterializationTask( + project=config.project, + feature_view=feature_view, + start_time=datetime(2024, 1, 1), + end_time=datetime(2024, 1, 2), + ) + + jobs = engine.materialize(_registry(entity), materialization_task) + + assert jobs[0].status() == MaterializationJobStatus.SUCCEEDED + assert jobs[0].error() is None + online_store.online_write_batch.assert_called_once() + offline_store.offline_write_batch.assert_called_once() diff --git a/sdk/python/tests/unit/infra/compute_engines/flink/test_flink_dependencies.py b/sdk/python/tests/unit/infra/compute_engines/flink/test_flink_dependencies.py new file mode 100644 index 00000000000..c10c654590f --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/flink/test_flink_dependencies.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from pathlib import Path + +import toml # type: ignore[import-untyped] + + +def test_flink_extra_constrains_shared_pyarrow_dependency() -> None: + pyproject_path = Path(__file__).resolve().parents[7] / "pyproject.toml" + pyproject = toml.loads(pyproject_path.read_text()) + + dependencies = pyproject["project"]["dependencies"] + flink_dependencies = pyproject["project"]["optional-dependencies"]["flink"] + + assert "pyarrow>=16.1.0" in dependencies + assert not any( + dependency.startswith("pyarrow") and "extra" in dependency + for dependency in dependencies + ) + assert "pyarrow<21.0.0" in flink_dependencies + assert "apache-flink>=2.2.1,<3" in flink_dependencies diff --git a/sdk/python/tests/unit/infra/compute_engines/local/test_nodes.py b/sdk/python/tests/unit/infra/compute_engines/local/test_nodes.py index 905ea65ae42..872dd978c9e 100644 --- a/sdk/python/tests/unit/infra/compute_engines/local/test_nodes.py +++ b/sdk/python/tests/unit/infra/compute_engines/local/test_nodes.py @@ -15,6 +15,7 @@ LocalOutputNode, LocalTransformationNode, ) +from feast.repo_config import MaterializationConfig backend = PandasBackend() now = pd.Timestamp.utcnow() @@ -37,9 +38,11 @@ def create_context(node_outputs): # Setup execution context + repo_config = MagicMock() + repo_config.materialization_config = MaterializationConfig() return ExecutionContext( project="test_proj", - repo_config=MagicMock(), + repo_config=repo_config, offline_store=MagicMock(), online_store=MagicMock(), entity_defs=MagicMock(), @@ -183,6 +186,122 @@ def test_local_dedup_node(): assert set(df_result["entity_id"]) == {1, 2} +def test_local_dedup_node_with_field_mapping_on_join_key(): + """Regression test for materialization failure when a join key has a field mapping. + + The source-read node renames columns via field_mapping (e.g. ``USERID`` -> ``user_id``) + before passing the table to downstream nodes. Without mapping ``column_info.join_keys`` + the dedup node would look up the pre-mapping name and raise ``KeyError(['USERID'])``. + + See https://github.com/feast-dev/feast/issues/5942. + """ + # Simulate a source-read node output: columns already renamed to the mapped names. + df = pd.DataFrame( + { + "user_id": [1, 1, 2], + "value": [100, 200, 300], + "event_timestamp": [ + now - timedelta(seconds=1), + now, + now, + ], + } + ) + + context = create_context( + node_outputs={"source": ArrowTableValue(pa.Table.from_pandas(df))} + ) + + node = LocalDedupNode( + name="dedup", + backend=backend, + column_info=ColumnInfo( + # The raw join key matches the source column name; field_mapping maps + # it to the user-facing name that the source-read node has already + # renamed the column to. + join_keys=["USERID"], + feature_cols=["value"], + ts_col="EVENT_TIMESTAMP", + created_ts_col=None, + field_mapping={"USERID": "user_id", "EVENT_TIMESTAMP": "event_timestamp"}, + ), + ) + node.add_input(MagicMock()) + node.inputs[0].name = "source" + + result = node.execute(context) + + df_result = result.data.to_pandas() + assert df_result.shape[0] == 2 + assert set(df_result["user_id"]) == {1, 2} + + +def test_local_join_node_with_field_mapping_on_join_key(): + """Regression test for materialization failure when a join key has a field mapping. + + The source-read node renames columns via field_mapping (e.g. ``USERID`` -> ``user_id``) + before passing the table to downstream nodes. Without mapping ``column_info.join_keys`` + the join node would call ``backend.join(..., on=["USERID"], ...)`` and raise + ``KeyError(['USERID'])`` because the columns have already been renamed. + + See https://github.com/feast-dev/feast/issues/5942. + """ + # Simulate two source-read node outputs: columns already renamed to the mapped names. + left_df = pd.DataFrame( + { + "user_id": [1, 2], + "value": [10, 20], + "event_timestamp": [now, now], + } + ) + right_df = pd.DataFrame( + { + "user_id": [1, 2], + "other_value": [100, 200], + "event_timestamp": [now, now], + } + ) + + context = create_context( + node_outputs={ + "left": ArrowTableValue(pa.Table.from_pandas(left_df)), + "right": ArrowTableValue(pa.Table.from_pandas(right_df)), + } + ) + # Bypass the trailing entity_df join — this test exercises the input-table + # join path that consumed the raw (unmapped) join keys before the fix. + context.entity_df = None + + join_node = LocalJoinNode( + name="join", + backend=backend, + column_info=ColumnInfo( + # Raw join key matches the source column name; field_mapping maps it + # to the user-facing name that the source-read node has already + # renamed the column to. + join_keys=["USERID"], + feature_cols=["value", "other_value"], + ts_col="EVENT_TIMESTAMP", + created_ts_col=None, + field_mapping={"USERID": "user_id", "EVENT_TIMESTAMP": "event_timestamp"}, + ), + ) + left_input = MagicMock() + left_input.name = "left" + right_input = MagicMock() + right_input.name = "right" + join_node.add_input(left_input) + join_node.add_input(right_input) + + result = join_node.execute(context) + + df_result = result.data.to_pandas() + assert df_result.shape[0] == 2 + assert set(df_result["user_id"]) == {1, 2} + assert "value" in df_result.columns + assert "other_value" in df_result.columns + + def test_local_transformation_node(): context = create_context( node_outputs={"source": ArrowTableValue(pa.Table.from_pandas(sample_df))} @@ -213,4 +332,54 @@ def test_local_output_node(): node.add_input(MagicMock()) node.inputs[0].name = "source" result = node.execute(context) - assert result.num_rows == 4 + assert isinstance(result, ArrowTableValue) + assert result.data.num_rows == 4 + + +def test_local_output_node_online_write_default_batch(): + """Test that online_write_batch is called once when batch_size is None (default).""" + # Create a feature view with online=True + feature_view = MagicMock() + feature_view.online = True + feature_view.offline = False + feature_view.entity_columns = [] + + # Create context with default materialization config (batch_size=None) + context = create_context( + node_outputs={"source": ArrowTableValue(pa.Table.from_pandas(sample_df))} + ) + + node = LocalOutputNode("output", feature_view) + node.add_input(MagicMock()) + node.inputs[0].name = "source" + + node.execute(context) + + # Verify online_write_batch was called exactly once (all rows in single batch) + assert context.online_store.online_write_batch.call_count == 1 + + +def test_local_output_node_online_write_batched(): + """Test that online_write_batch is called multiple times when batch_size is configured.""" + # Create a feature view with online=True + feature_view = MagicMock() + feature_view.online = True + feature_view.offline = False + feature_view.entity_columns = [] + + # Create context with batch_size=2 (sample_df has 4 rows, so expect 2 batches) + context = create_context( + node_outputs={"source": ArrowTableValue(pa.Table.from_pandas(sample_df))} + ) + context.repo_config.materialization_config = MaterializationConfig( + online_write_batch_size=2 + ) + + node = LocalOutputNode("output", feature_view) + node.add_input(MagicMock()) + node.inputs[0].name = "source" + + node.execute(context) + + # Verify online_write_batch was called twice (4 rows / batch_size 2 = 2 batches) + assert context.online_store.online_write_batch.call_count == 2 diff --git a/sdk/python/tests/unit/infra/compute_engines/ray_compute/__init__.py b/sdk/python/tests/unit/infra/compute_engines/ray_compute/__init__.py deleted file mode 100644 index 2734c36c704..00000000000 --- a/sdk/python/tests/unit/infra/compute_engines/ray_compute/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Ray compute engine unit tests.""" diff --git a/sdk/python/tests/unit/infra/compute_engines/snowflake/__init__.py b/sdk/python/tests/unit/infra/compute_engines/snowflake/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/unit/infra/compute_engines/snowflake/test_snowflake_compute_engine.py b/sdk/python/tests/unit/infra/compute_engines/snowflake/test_snowflake_compute_engine.py new file mode 100644 index 00000000000..f450c124bed --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/snowflake/test_snowflake_compute_engine.py @@ -0,0 +1,117 @@ +"""Regression tests for SnowflakeComputeEngine.update(). + +See https://github.com/feast-dev/feast/pull/6608#discussion_r3602461959: a +maintainer pointed out that once materialization UDFs were deployed for a +project, a `SHOW USER FUNCTIONS` check made `update()` return early on every +subsequent call, and the SQL template used `CREATE FUNCTION IF NOT EXISTS`. +Together, this meant a user changing `python_udf_runtime_version` (e.g. after +Snowflake decommissions a runtime) would never actually get the new runtime +version deployed -- the stale, already-deployed UDFs were left untouched. + +The fix removes the early return and switches the SQL template to +`CREATE OR REPLACE FUNCTION`, so `update()` always (re)deploys the UDFs using +the currently configured runtime version. +""" + +import shutil +import tempfile +from unittest.mock import MagicMock, patch + +from feast.infra.compute_engines.snowflake.snowflake_engine import ( + SnowflakeComputeEngine, + SnowflakeComputeEngineConfig, +) + +MODULE = "feast.infra.compute_engines.snowflake.snowflake_engine" + + +def _make_engine(python_udf_runtime_version: str = "3.10") -> SnowflakeComputeEngine: + repo_config = MagicMock() + repo_config.offline_store.type = "snowflake.offline" + repo_config.batch_engine = SnowflakeComputeEngineConfig( + database="FEAST", + schema="PUBLIC", + python_udf_runtime_version=python_udf_runtime_version, + ) + + return SnowflakeComputeEngine( + repo_config=repo_config, + offline_store=MagicMock(), + online_store=MagicMock(), + ) + + +def _run_update(engine: SnowflakeComputeEngine, mock_execute: MagicMock) -> None: + mock_conn_cm = MagicMock() + mock_conn_cm.__enter__ = MagicMock(return_value=MagicMock()) + mock_conn_cm.__exit__ = MagicMock(return_value=False) + + def _fake_package_snowpark_zip(project_name): + copy_path = tempfile.mkdtemp() + return copy_path, f"{copy_path}/feast.zip" + + with ( + patch(f"{MODULE}.GetSnowflakeConnection", return_value=mock_conn_cm), + patch(f"{MODULE}.execute_snowflake_statement", mock_execute), + patch(f"{MODULE}.package_snowpark_zip", side_effect=_fake_package_snowpark_zip), + patch(f"{MODULE}.shutil.rmtree", side_effect=shutil.rmtree), + ): + engine.update( + project="myproject", + views_to_delete=[], + views_to_keep=[], + entities_to_delete=[], + entities_to_keep=[], + ) + + +def test_update_never_short_circuits_on_existing_functions(): + """update() must not query `SHOW USER FUNCTIONS` and skip deployment -- + that early-return path is what caused stale runtime versions to stick + around after a config change. + """ + engine = _make_engine() + mock_execute = MagicMock() + + _run_update(engine, mock_execute) + + executed_queries = [call.args[1] for call in mock_execute.call_args_list] + assert not any("SHOW USER FUNCTIONS" in q for q in executed_queries), ( + "update() should no longer perform a SHOW USER FUNCTIONS check that " + "causes it to skip (re)deploying the UDFs" + ) + assert any("CREATE OR REPLACE FUNCTION" in q for q in executed_queries), ( + "update() should deploy UDFs using CREATE OR REPLACE FUNCTION so " + "redeploys pick up a changed python_udf_runtime_version" + ) + + +def test_update_redeploys_with_current_runtime_version_on_every_call(): + """Calling update() twice (simulating two `feast apply` runs, e.g. before + and after a user bumps `python_udf_runtime_version`) must deploy the UDFs + both times, each time using whatever runtime version is currently + configured. + """ + engine = _make_engine(python_udf_runtime_version="3.11") + mock_execute = MagicMock() + + _run_update(engine, mock_execute) + first_call_count = mock_execute.call_count + assert first_call_count > 0 + + _run_update(engine, mock_execute) + second_call_count = mock_execute.call_count + assert second_call_count == 2 * first_call_count, ( + "the second update() call should re-run the full deployment (no " + "early return), just like the first" + ) + + executed_queries = [call.args[1] for call in mock_execute.call_args_list] + create_function_queries = [ + q for q in executed_queries if "CREATE OR REPLACE FUNCTION" in q + ] + assert create_function_queries, "expected CREATE OR REPLACE FUNCTION queries" + assert all("RUNTIME_VERSION = '3.11'" in q for q in create_function_queries), ( + "every deployed UDF should use the currently configured " + "python_udf_runtime_version ('3.11'), including on redeploy" + ) diff --git a/sdk/python/tests/unit/infra/compute_engines/test_backend_factory.py b/sdk/python/tests/unit/infra/compute_engines/test_backend_factory.py new file mode 100644 index 00000000000..ee0ebf1e8ec --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/test_backend_factory.py @@ -0,0 +1,55 @@ +from unittest.mock import MagicMock, patch + +import pandas as pd +import pyarrow +import pytest + +from feast.infra.compute_engines.backends.factory import BackendFactory +from feast.infra.compute_engines.backends.pandas_backend import PandasBackend + + +class TestBackendFactoryFromName: + def test_pandas_backend(self): + backend = BackendFactory.from_name("pandas") + assert isinstance(backend, PandasBackend) + + @patch( + "feast.infra.compute_engines.backends.factory.BackendFactory._get_polars_backend" + ) + def test_polars_backend(self, mock_get_polars): + mock_backend = MagicMock() + mock_get_polars.return_value = mock_backend + + result = BackendFactory.from_name("polars") + assert result is mock_backend + + def test_unsupported_name_raises(self): + with pytest.raises(ValueError, match="Unsupported backend name"): + BackendFactory.from_name("dask") + + +class TestBackendFactoryInferFromEntityDf: + def test_pandas_dataframe_returns_pandas_backend(self): + """A non-empty pandas DataFrame is detected via isinstance check.""" + df = pd.DataFrame({"a": [1, 2]}) + backend = BackendFactory.infer_from_entity_df(df) + assert isinstance(backend, PandasBackend) + + def test_empty_pandas_dataframe_returns_pandas_backend(self): + """An empty pandas DataFrame returns PandasBackend.""" + df = pd.DataFrame() + backend = BackendFactory.infer_from_entity_df(df) + assert isinstance(backend, PandasBackend) + + def test_pyarrow_table(self): + table = pyarrow.table({"a": [1, 2]}) + backend = BackendFactory.infer_from_entity_df(table) + assert isinstance(backend, PandasBackend) + + def test_none_input(self): + backend = BackendFactory.infer_from_entity_df(None) + assert isinstance(backend, PandasBackend) + + def test_empty_string_input(self): + backend = BackendFactory.infer_from_entity_df("") + assert isinstance(backend, PandasBackend) diff --git a/sdk/python/tests/unit/infra/compute_engines/test_feature_builder.py b/sdk/python/tests/unit/infra/compute_engines/test_feature_builder.py index b78ef15299c..8ad19d02290 100644 --- a/sdk/python/tests/unit/infra/compute_engines/test_feature_builder.py +++ b/sdk/python/tests/unit/infra/compute_engines/test_feature_builder.py @@ -1,4 +1,6 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch + +import pytest from feast.data_source import DataSource from feast.infra.compute_engines.dag.context import ExecutionContext @@ -7,6 +9,7 @@ from feast.infra.compute_engines.dag.plan import ExecutionPlan from feast.infra.compute_engines.dag.value import DAGValue from feast.infra.compute_engines.feature_builder import FeatureBuilder +from feast.transformation.mode import TransformationMode # --------------------------- # Minimal Mock DAGNode for testing @@ -143,3 +146,144 @@ def test_recursive_featureview_build(): - Source(hourly_driver_stats)""" assert execution_plan.to_dag() == expected_output + + +# --------------------------------------------------------------------------- +# Helpers for get_column_info tests +# --------------------------------------------------------------------------- + +# Stable return value for _get_column_names: (join_keys, feature_cols, ts_col, created_ts_col) +_MOCK_COLUMN_NAMES = ( + ["user_id"], + ["user_avg_rating", "user_review_count"], + "event_timestamp", + None, +) + + +def _make_transformation(mode): + """Return a minimal transformation stub with the given mode.""" + t = MagicMock() + t.mode = mode + return t + + +def _make_builder_for_column_info(transformation): + """ + Build a MockFeatureBuilder whose task.feature_view carries the given + transformation. registry.get_entity is stubbed out per entity name. + """ + view = MagicMock() + view.entities = ["user"] + view.feature_transformation = transformation + view.batch_source = MagicMock() + view.batch_source.field_mapping = {} + view.stream_source = None + + task = MagicMock() + task.project = "test_project" + task.feature_view = view + task.only_latest = False + + registry = MagicMock() + registry.get_entity.return_value = MagicMock(join_key="user_id") + + builder = MockFeatureBuilder.__new__(MockFeatureBuilder) + builder.registry = registry + builder.task = task + builder.nodes = [] + return builder, view + + +# --------------------------------------------------------------------------- +# Bug fix: TransformationMode.PYTHON must set feature_cols=[] +# +# Previously only "ray" and "pandas" were handled. "python" (the default mode +# for @batch_feature_view) was missing, causing get_column_info to forward +# the BFV *output* feature names (e.g. user_avg_rating) to the offline store +# read step — columns that don't exist in raw source data — resulting in +# UNRESOLVED_COLUMN errors at Spark analysis time. +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "mode", + [ + TransformationMode.PYTHON, + TransformationMode.PANDAS, + TransformationMode.RAY, + # String forms (getattr(mode, "value", None) path) + "python", + "pandas", + "ray", + ], +) +def test_get_column_info_clears_feature_cols_for_udf_modes(mode): + """ + For transformation modes that compute output features from raw input + (python, pandas, ray), get_column_info must set feature_cols=[] so the + offline store read step issues SELECT * instead of projecting the output + feature names that don't exist in the raw source schema. + """ + builder, view = _make_builder_for_column_info(_make_transformation(mode)) + + with patch( + "feast.infra.compute_engines.feature_builder._get_column_names", + return_value=_MOCK_COLUMN_NAMES, + ): + col_info = builder.get_column_info(view) + + assert col_info.feature_cols == [], ( + f"Expected feature_cols=[] for TransformationMode {mode!r}, " + f"got {col_info.feature_cols!r}. " + "The offline store read step must not project output feature names " + "that don't exist in the raw source schema." + ) + assert col_info.join_keys == ["user_id"] + assert col_info.ts_col == "event_timestamp" + + +@pytest.mark.parametrize( + "mode", + [ + TransformationMode.SPARK_SQL, + TransformationMode.SQL, + TransformationMode.SPARK, + "spark_sql", + "sql", + ], +) +def test_get_column_info_preserves_feature_cols_for_non_udf_modes(mode): + """ + SQL/Spark-SQL transformations operate on already-projected columns and + should NOT get feature_cols cleared — the source read must still select + the named feature columns explicitly. + """ + builder, view = _make_builder_for_column_info(_make_transformation(mode)) + + with patch( + "feast.infra.compute_engines.feature_builder._get_column_names", + return_value=_MOCK_COLUMN_NAMES, + ): + col_info = builder.get_column_info(view) + + assert col_info.feature_cols == ["user_avg_rating", "user_review_count"], ( + f"Expected feature_cols to be preserved for mode {mode!r}, " + f"got {col_info.feature_cols!r}." + ) + + +def test_get_column_info_preserves_feature_cols_with_no_transformation(): + """ + A plain FeatureView (no transformation) must retain its feature column + names so the offline store read step selects only the required columns. + """ + builder, view = _make_builder_for_column_info(None) + + with patch( + "feast.infra.compute_engines.feature_builder._get_column_names", + return_value=_MOCK_COLUMN_NAMES, + ): + col_info = builder.get_column_info(view) + + assert col_info.feature_cols == ["user_avg_rating", "user_review_count"] diff --git a/sdk/python/tests/unit/infra/compute_engines/test_local_compute_engine.py b/sdk/python/tests/unit/infra/compute_engines/test_local_compute_engine.py new file mode 100644 index 00000000000..3396a45c995 --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/test_local_compute_engine.py @@ -0,0 +1,67 @@ +from unittest.mock import MagicMock + +from feast.infra.compute_engines.base import ComputeEngine +from feast.infra.compute_engines.local.compute import ( + LocalComputeEngine, + LocalComputeEngineConfig, +) + + +class TestLocalComputeEngineConfig: + def test_default_type(self): + config = LocalComputeEngineConfig() + assert config.type == "local" + + def test_default_backend_none(self): + config = LocalComputeEngineConfig() + assert config.backend is None + + def test_custom_backend(self): + config = LocalComputeEngineConfig(backend="polars") + assert config.backend == "polars" + + +class TestLocalComputeEngine: + def _make_engine(self): + return LocalComputeEngine( + repo_config=MagicMock(), + offline_store=MagicMock(), + online_store=MagicMock(), + ) + + def test_is_compute_engine(self): + engine = self._make_engine() + assert isinstance(engine, ComputeEngine) + + def test_update_is_noop(self): + engine = self._make_engine() + # Should not raise + engine.update( + project="test", + views_to_delete=[], + views_to_keep=[], + entities_to_delete=[], + entities_to_keep=[], + ) + + def test_teardown_is_noop(self): + engine = self._make_engine() + # Should not raise + engine.teardown_infra( + project="test", + fvs=[], + entities=[], + ) + + def test_stores_config(self): + repo_config = MagicMock() + offline = MagicMock() + online = MagicMock() + engine = LocalComputeEngine( + repo_config=repo_config, + offline_store=offline, + online_store=online, + ) + assert engine.repo_config is repo_config + assert engine.offline_store is offline + assert engine.online_store is online diff --git a/sdk/python/tests/unit/infra/compute_engines/test_local_job.py b/sdk/python/tests/unit/infra/compute_engines/test_local_job.py new file mode 100644 index 00000000000..2f4a1ac48fd --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/test_local_job.py @@ -0,0 +1,100 @@ +from unittest.mock import MagicMock + +import pytest + +from feast.infra.common.materialization_job import MaterializationJobStatus +from feast.infra.compute_engines.local.job import ( + LocalMaterializationJob, + LocalRetrievalJob, +) + + +class TestLocalMaterializationJob: + def test_status(self): + job = LocalMaterializationJob( + job_id="test-job-1", + status=MaterializationJobStatus.SUCCEEDED, + ) + assert job.status() == MaterializationJobStatus.SUCCEEDED + + def test_job_id(self): + job = LocalMaterializationJob( + job_id="test-job-1", + status=MaterializationJobStatus.RUNNING, + ) + assert job.job_id() == "test-job-1" + + def test_error_none_by_default(self): + job = LocalMaterializationJob( + job_id="test-job-1", + status=MaterializationJobStatus.SUCCEEDED, + ) + assert job.error() is None + + def test_error_stored(self): + err = RuntimeError("something failed") + job = LocalMaterializationJob( + job_id="test-job-1", + status=MaterializationJobStatus.ERROR, + error=err, + ) + assert job.error() is err + + def test_should_not_be_retried(self): + job = LocalMaterializationJob( + job_id="test-job-1", + status=MaterializationJobStatus.ERROR, + ) + assert job.should_be_retried() is False + + def test_url_is_none(self): + job = LocalMaterializationJob( + job_id="test-job-1", + status=MaterializationJobStatus.SUCCEEDED, + ) + assert job.url() is None + + +class TestLocalRetrievalJob: + def test_full_feature_names(self): + job = LocalRetrievalJob( + plan=None, + context=MagicMock(), + full_feature_names=True, + ) + assert job.full_feature_names is True + + def test_full_feature_names_false(self): + job = LocalRetrievalJob( + plan=None, + context=MagicMock(), + full_feature_names=False, + ) + assert job.full_feature_names is False + + def test_error_none_by_default(self): + job = LocalRetrievalJob(plan=None, context=MagicMock()) + assert job.error() is None + + def test_error_stored(self): + err = ValueError("bad data") + job = LocalRetrievalJob(plan=None, context=MagicMock(), error=err) + assert job.error() is err + + def test_on_demand_feature_views_default_empty(self): + job = LocalRetrievalJob(plan=None, context=MagicMock()) + assert job.on_demand_feature_views == [] + + def test_metadata_default_none(self): + job = LocalRetrievalJob(plan=None, context=MagicMock()) + assert job.metadata is None + + def test_to_remote_storage_raises(self): + job = LocalRetrievalJob(plan=None, context=MagicMock()) + with pytest.raises(NotImplementedError, match="Remote storage"): + job.to_remote_storage() + + def test_to_sql_raises(self): + job = LocalRetrievalJob(plan=None, context=MagicMock()) + with pytest.raises(NotImplementedError, match="SQL generation"): + job.to_sql() diff --git a/sdk/python/tests/unit/infra/compute_engines/test_materialization_job.py b/sdk/python/tests/unit/infra/compute_engines/test_materialization_job.py new file mode 100644 index 00000000000..8d6ab4f2288 --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/test_materialization_job.py @@ -0,0 +1,59 @@ +from datetime import datetime +from unittest.mock import MagicMock + +from feast.infra.common.materialization_job import ( + MaterializationJobStatus, + MaterializationTask, +) + + +class TestMaterializationJobStatus: + def test_all_statuses_defined(self): + expected = { + "WAITING", + "RUNNING", + "AVAILABLE", + "ERROR", + "CANCELLING", + "CANCELLED", + "SUCCEEDED", + "PAUSED", + "RETRYING", + } + actual = {s.name for s in MaterializationJobStatus} + assert actual == expected + + +class TestMaterializationTask: + def test_creation(self): + mock_fv = MagicMock() + task = MaterializationTask( + project="my_project", + feature_view=mock_fv, + start_time=datetime(2024, 1, 1), + end_time=datetime(2024, 1, 2), + ) + assert task.project == "my_project" + assert task.feature_view is mock_fv + assert task.start_time == datetime(2024, 1, 1) + assert task.end_time == datetime(2024, 1, 2) + + def test_default_only_latest(self): + mock_fv = MagicMock() + task = MaterializationTask( + project="p", + feature_view=mock_fv, + start_time=datetime(2024, 1, 1), + end_time=datetime(2024, 1, 2), + ) + assert task.only_latest is True + + def test_default_disable_event_timestamp(self): + mock_fv = MagicMock() + task = MaterializationTask( + project="p", + feature_view=mock_fv, + start_time=datetime(2024, 1, 1), + end_time=datetime(2024, 1, 2), + ) + assert task.disable_event_timestamp is False diff --git a/sdk/python/tests/unit/infra/compute_engines/test_spark_application.py b/sdk/python/tests/unit/infra/compute_engines/test_spark_application.py new file mode 100644 index 00000000000..3b080bd8993 --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/test_spark_application.py @@ -0,0 +1,440 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from feast.feature_view import FeatureViewState +from feast.infra.common.materialization_job import ( + MaterializationJobStatus, +) +from feast.infra.compute_engines.spark_application.config import ( + SparkApplicationComputeEngineConfig, +) +from feast.infra.compute_engines.spark_application.job import ( + _STATE_MAP, + CompletedMaterializationJob, + SparkApplicationMaterializationJob, +) + + +def _make_repo_config( + online_store_type="redis", + registry_type="sql", + registry_path="postgresql://user:pass@host:5432/feast", # pragma: allowlist secret + offline_store_type="spark", + spark_conf=None, +): + """Build a mock RepoConfig for testing.""" + config = MagicMock() + config.online_store = MagicMock() + config.online_store.type = online_store_type + config.offline_store = MagicMock() + config.offline_store.type = offline_store_type + config.registry = MagicMock() + config.registry.path = registry_path + config.registry.registry_type = registry_type + config.batch_engine = SparkApplicationComputeEngineConfig( + image="quay.io/test/feast-spark:latest", + spark_conf=spark_conf, + ) + config.model_dump = MagicMock( + return_value={ + "project": "test", + "provider": "local", + "batch_engine": {"type": "spark_application"}, + "offline_store": { + "type": offline_store_type, + "spark_conf": {"spark.existing": "value"}, + }, + "online_store": {"type": online_store_type}, + "registry": {"registry_type": registry_type, "path": registry_path}, + } + ) + return config + + +@patch("feast.infra.compute_engines.spark_application.compute.k8s_config") +@patch("feast.infra.compute_engines.spark_application.compute.client") +def _make_engine(mock_client, mock_k8s_config, **kwargs): + """Create engine with mocked K8s client. + + Pre-seed API clients so later property access does not call real + load_config() after this helper's patches have exited. + """ + from feast.infra.compute_engines.spark_application.compute import ( + SparkApplicationComputeEngine, + ) + + repo_config = _make_repo_config(**kwargs) + engine = SparkApplicationComputeEngine( + repo_config=repo_config, offline_store=None, online_store=None + ) + engine._core_v1 = MagicMock() + engine._custom_api = MagicMock() + return engine + + +# ── Test 1: Config defaults + required field ── + + +def test_config_defaults_and_required_image(): + c = SparkApplicationComputeEngineConfig(image="quay.io/test:v1") + assert c.type == "spark_application" + assert c.namespace == "default" + assert c.executor_instances == 1 + assert c.restart_policy == "Never" + assert c.max_retries == 3 + + with pytest.raises(Exception): + SparkApplicationComputeEngineConfig() # image is required + + +# ── Test 2: EC-3 rejects file-based online stores ── + + +def test_rejects_sqlite_online_store(): + with pytest.raises(ValueError, match="sqlite"): + _make_engine(online_store_type="sqlite") + + +def test_rejects_faiss_online_store(): + with pytest.raises(ValueError, match="faiss"): + _make_engine(online_store_type="faiss") + + +# ── Test 2b: rejects file-based offline stores ── + + +@pytest.mark.parametrize("store_type", ["dask", "file", "duckdb"]) +def test_rejects_file_based_offline_store(store_type): + with pytest.raises(ValueError, match=store_type): + _make_engine(offline_store_type=store_type) + + +# ── Test 3: EC-2 rejects file-based registries ── + + +def test_rejects_file_registry(): + with pytest.raises(ValueError, match="file.*registry"): + _make_engine(registry_type="file") + + +# ── Test 4: accepts network-accessible registries ── + + +def test_accepts_sql_registry(): + engine = _make_engine(registry_type="sql") + assert engine is not None + + +def test_accepts_snowflake_registry(): + engine = _make_engine(registry_type="snowflake.registry") + assert engine is not None + + +@patch("feast.infra.compute_engines.spark_application.compute.k8s_config") +@patch("feast.infra.compute_engines.spark_application.compute.client") +def test_init_does_not_load_kubeconfig(mock_client, mock_k8s_config): + """feast apply constructs the engine but never materializes — no kubeconfig needed.""" + from feast.infra.compute_engines.spark_application.compute import ( + SparkApplicationComputeEngine, + ) + + engine = SparkApplicationComputeEngine( + repo_config=_make_repo_config(), offline_store=None, online_store=None + ) + mock_k8s_config.load_config.assert_not_called() + + _ = engine.core_v1 + mock_k8s_config.load_config.assert_called_once() + assert engine.custom_api is not None + # Second access must not reload + _ = engine.custom_api + mock_k8s_config.load_config.assert_called_once() + + +# ── Test 5: _build_driver_repo_config — one rewrite (batch_engine only) ── + + +def test_build_driver_repo_config_rewrites(): + engine = _make_engine(offline_store_type="spark") + d = engine._build_driver_repo_config() + assert d["batch_engine"]["type"] == "spark.engine" + assert d["offline_store"]["type"] == "spark" # NOT rewritten — respects user intent + assert ( + d["registry"]["registry_type"] == "sql" + ) # NOT rewritten — pod uses SQL directly + + +# ── Test 6: _build_driver_repo_config — batch_engine is just type (no spark_conf copy) ── + + +def test_build_driver_repo_config_batch_engine_minimal(): + engine = _make_engine(spark_conf={"spark.new": "from_engine"}) + d = engine._build_driver_repo_config() + assert d["batch_engine"] == {"type": "spark.engine"} + assert d["offline_store"]["spark_conf"]["spark.existing"] == "value" + + +# ── Test 7: CR structure ── + + +def test_cr_structure(): + engine = _make_engine() + cr = engine._build_spark_application_cr("abcd1234") + assert cr["apiVersion"] == "sparkoperator.k8s.io/v1beta2" + assert cr["kind"] == "SparkApplication" + assert cr["spec"]["type"] == "Python" + assert cr["spec"]["mode"] == "cluster" + assert cr["spec"]["mainApplicationFile"] == "local:///opt/feast/main.py" + assert "driver" in cr["spec"] + assert "executor" in cr["spec"] + assert cr["metadata"]["name"] == "feast-sa-abcd1234" + + +# ── Test 8: CR sparkConf includes driver env passthrough ── + + +def test_cr_driver_env_passthrough(): + engine = _make_engine() + cr = engine._build_spark_application_cr("abcd1234") + spark_conf = cr["spec"]["sparkConf"] + assert ( + spark_conf["spark.kubernetes.driverEnv.FEAST_CONFIGMAP_NAME"] + == "feast-sa-abcd1234" + ) + assert ( + spark_conf["spark.kubernetes.driverEnv.FEAST_CONFIGMAP_NAMESPACE"] == "default" + ) + + +# ── Test 9: Status mapping covers all 14 states ── + + +def test_state_map_coverage(): + assert len(_STATE_MAP) == 14 + assert _STATE_MAP["COMPLETED"] == MaterializationJobStatus.SUCCEEDED + assert _STATE_MAP["FAILED"] == MaterializationJobStatus.ERROR + assert _STATE_MAP["SUBMISSION_FAILED"] == MaterializationJobStatus.ERROR + assert _STATE_MAP["RUNNING"] == MaterializationJobStatus.RUNNING + assert _STATE_MAP[""] == MaterializationJobStatus.WAITING + assert _STATE_MAP["UNKNOWN"] == MaterializationJobStatus.WAITING + + +# ── Test 10: Cleanup swallows 404 ── + + +@patch("feast.infra.compute_engines.spark_application.compute.k8s_config") +@patch("feast.infra.compute_engines.spark_application.compute.client") +def test_cleanup_swallows_404(mock_client, mock_k8s_config): + from kubernetes.client.exceptions import ApiException + + from feast.infra.compute_engines.spark_application.compute import ( + SparkApplicationComputeEngine, + ) + + repo_config = _make_repo_config() + engine = SparkApplicationComputeEngine( + repo_config=repo_config, offline_store=None, online_store=None + ) + + engine.custom_api.delete_namespaced_custom_object.side_effect = ApiException( + status=404 + ) + engine.core_v1.delete_namespaced_config_map.side_effect = ApiException(status=404) + + engine._cleanup("test-id") + + +# ── Test 11: Timeout sets error on job (does not raise) ── + + +@patch("feast.infra.compute_engines.spark_application.compute.k8s_config") +@patch("feast.infra.compute_engines.spark_application.compute.client") +@patch("feast.infra.compute_engines.spark_application.compute.time") +def test_timeout_sets_error(mock_time, mock_client, mock_k8s_config): + from feast.infra.compute_engines.spark_application.compute import ( + SparkApplicationComputeEngine, + ) + + repo_config = _make_repo_config() + repo_config.batch_engine = SparkApplicationComputeEngineConfig( + image="test", + job_timeout_seconds=1, + poll_interval_seconds=1, + ) + engine = SparkApplicationComputeEngine( + repo_config=repo_config, offline_store=None, online_store=None + ) + + # Calls: start(0), while-check(0), elapsed(0), sleep, while-check(2 > deadline=1) → exit + mock_time.monotonic.side_effect = [0, 0, 0, 2] + mock_time.sleep = MagicMock() + + mock_job = MagicMock() + mock_job.status.return_value = MaterializationJobStatus.RUNNING + mock_job._job_id = "test123" + mock_job._error = None + mock_job.job_id.return_value = "feast-sa-test123" + + engine._wait_for_completion(mock_job) + assert mock_job._error is not None + assert "did not complete" in str(mock_job._error) + + +# ── Test 12: Job naming < 63 chars ── + + +def test_job_naming_under_63_chars(): + mock_api = MagicMock() + job = SparkApplicationMaterializationJob("abcdef12", "default", mock_api) + assert len(job.job_id()) <= 63 + assert job.job_id() == "feast-sa-abcdef12" + + +# ── Test 13: _build_per_fv_jobs — all succeeded ── + + +def test_build_per_fv_jobs_all_succeeded(): + engine = _make_engine() + mock_registry = MagicMock() + + fv1 = MagicMock() + fv1.name = "fv_1" + fv1.state = FeatureViewState.AVAILABLE_ONLINE + fv2 = MagicMock() + fv2.name = "fv_2" + fv2.state = FeatureViewState.AVAILABLE_ONLINE + mock_registry.get_feature_view.side_effect = [fv1, fv2] + + task1 = MagicMock() + task1.feature_view.name = "fv_1" + task1.project = "test" + task2 = MagicMock() + task2.feature_view.name = "fv_2" + task2.project = "test" + + parent_job = SparkApplicationMaterializationJob("job1", "default", MagicMock()) + jobs = engine._build_per_fv_jobs(mock_registry, [task1, task2], "job1", parent_job) + + assert len(jobs) == 2 + assert all(isinstance(j, CompletedMaterializationJob) for j in jobs) + assert all(j.status() == MaterializationJobStatus.SUCCEEDED for j in jobs) + + +# ── Test 14: _build_per_fv_jobs — partial failure (independent jobs) ── + + +def test_build_per_fv_jobs_partial_failure(): + engine = _make_engine() + mock_registry = MagicMock() + + fv_ok = MagicMock() + fv_ok.name = "fv_ok" + fv_ok.state = FeatureViewState.AVAILABLE_ONLINE + fv_fail = MagicMock() + fv_fail.name = "fv_fail" + fv_fail.state = FeatureViewState.MATERIALIZING + mock_registry.get_feature_view.side_effect = [fv_ok, fv_fail] + + task_ok = MagicMock() + task_ok.feature_view.name = "fv_ok" + task_ok.project = "test" + task_fail = MagicMock() + task_fail.feature_view.name = "fv_fail" + task_fail.project = "test" + + parent_job = SparkApplicationMaterializationJob("job1", "default", MagicMock()) + jobs = engine._build_per_fv_jobs( + mock_registry, [task_ok, task_fail], "job1", parent_job + ) + + assert len(jobs) == 2 + assert isinstance(jobs[0], CompletedMaterializationJob) + assert jobs[0].status() == MaterializationJobStatus.SUCCEEDED + assert jobs[1].status() == MaterializationJobStatus.ERROR + assert "fv_fail" in str(jobs[1].error()) + + +# ── Test 15: _build_per_fv_jobs — single task returns parent job directly ── + + +def test_build_per_fv_jobs_single_task(): + engine = _make_engine() + mock_registry = MagicMock() + task = MagicMock() + task.feature_view.name = "fv_1" + task.project = "test" + + parent_job = SparkApplicationMaterializationJob("job1", "default", MagicMock()) + jobs = engine._build_per_fv_jobs(mock_registry, [task], "job1", parent_job) + + assert len(jobs) == 1 + assert jobs[0] is parent_job + mock_registry.get_feature_view.assert_not_called() + + +# ── Test 16: CompletedMaterializationJob is always SUCCEEDED ── + + +def test_completed_job_status(): + job = CompletedMaterializationJob("abc123") + assert job.status() == MaterializationJobStatus.SUCCEEDED + assert job.error() is None + assert job.job_id() == "feast-sa-abc123" + assert job.should_be_retried() is False + + +# ── Test 17: Env validation — requires value or valueFrom ── + + +def test_env_requires_value_or_valuefrom(): + with pytest.raises(ValueError, match="'value' or 'valueFrom'"): + SparkApplicationComputeEngineConfig( + image="test:v1", + env=[{"name": "FOO"}], + ) + + +def test_env_accepts_value(): + c = SparkApplicationComputeEngineConfig( + image="test:v1", + env=[{"name": "FOO", "value": "bar"}], + ) + assert len(c.env) == 1 + + +def test_env_accepts_valuefrom(): + c = SparkApplicationComputeEngineConfig( + image="test:v1", + env=[ + {"name": "SECRET", "valueFrom": {"secretKeyRef": {"name": "s", "key": "k"}}} + ], + ) + assert len(c.env) == 1 + + +def test_env_rejects_non_dict(): + with pytest.raises(Exception, match="dict"): + SparkApplicationComputeEngineConfig( + image="test:v1", + env=["not_a_dict"], + ) + + +# ── Test 18: Retry — 403 fails fast with RBAC hint ── + + +def test_poll_403_fails_fast_with_hint(): + from kubernetes.client.exceptions import ApiException + + mock_api = MagicMock() + mock_api.get_namespaced_custom_object.side_effect = ApiException( + status=403, reason="Forbidden" + ) + + job = SparkApplicationMaterializationJob("test1", "default", mock_api) + status = job.status() + + assert status == MaterializationJobStatus.ERROR + assert "Role/RoleBinding" in str(job.error()) + mock_api.get_namespaced_custom_object.assert_called_once() diff --git a/sdk/python/tests/unit/infra/compute_engines/test_topo_sort.py b/sdk/python/tests/unit/infra/compute_engines/test_topo_sort.py new file mode 100644 index 00000000000..5bec3f63734 --- /dev/null +++ b/sdk/python/tests/unit/infra/compute_engines/test_topo_sort.py @@ -0,0 +1,90 @@ +from unittest.mock import MagicMock + +from feast.infra.compute_engines.algorithms.topo import ( + topological_sort, + topological_sort_multiple, +) +from feast.infra.compute_engines.dag.node import DAGNode + + +def _make_node(name, inputs=None): + """Create a mock DAGNode.""" + node = MagicMock(spec=DAGNode) + node.name = name + node.inputs = inputs or [] + return node + + +class TestTopologicalSort: + def test_single_node(self): + root = _make_node("root") + result = topological_sort(root) + assert len(result) == 1 + assert result[0] is root + + def test_linear_chain(self): + """A -> B -> C should produce [A, B, C].""" + a = _make_node("A") + b = _make_node("B", inputs=[a]) + c = _make_node("C", inputs=[b]) + + result = topological_sort(c) + assert len(result) == 3 + # Dependencies must come before dependents + assert result.index(a) < result.index(b) + assert result.index(b) < result.index(c) + + def test_diamond_dependency(self): + """ + A → B + A → C + B,C → D + Should visit A before B and C, and B/C before D. + """ + a = _make_node("A") + b = _make_node("B", inputs=[a]) + c = _make_node("C", inputs=[a]) + d = _make_node("D", inputs=[b, c]) + + result = topological_sort(d) + assert len(result) == 4 + assert result.index(a) < result.index(b) + assert result.index(a) < result.index(c) + assert result.index(b) < result.index(d) + assert result.index(c) < result.index(d) + + def test_no_duplicates(self): + """Shared dependencies should appear only once.""" + shared = _make_node("shared") + b = _make_node("B", inputs=[shared]) + c = _make_node("C", inputs=[shared]) + root = _make_node("root", inputs=[b, c]) + + result = topological_sort(root) + assert result.count(shared) == 1 + + +class TestTopologicalSortMultiple: + def test_multiple_roots_no_overlap(self): + r1 = _make_node("root1") + r2 = _make_node("root2") + + result = topological_sort_multiple([r1, r2]) + assert len(result) == 2 + assert r1 in result + assert r2 in result + + def test_multiple_roots_with_shared_dep(self): + shared = _make_node("shared") + r1 = _make_node("root1", inputs=[shared]) + r2 = _make_node("root2", inputs=[shared]) + + result = topological_sort_multiple([r1, r2]) + assert len(result) == 3 + assert result.count(shared) == 1 + assert result.index(shared) < result.index(r1) + assert result.index(shared) < result.index(r2) + + def test_empty_roots(self): + result = topological_sort_multiple([]) + assert result == [] diff --git a/sdk/python/tests/unit/infra/feature_servers/test_mcp_server.py b/sdk/python/tests/unit/infra/feature_servers/test_mcp_server.py index f5748be1acc..e8809f84577 100644 --- a/sdk/python/tests/unit/infra/feature_servers/test_mcp_server.py +++ b/sdk/python/tests/unit/infra/feature_servers/test_mcp_server.py @@ -1,8 +1,141 @@ import unittest +from types import SimpleNamespace from unittest.mock import Mock, patch +from pydantic import ValidationError + from feast.feature_store import FeatureStore from feast.infra.mcp_servers.mcp_config import McpFeatureServerConfig +from feast.infra.mcp_servers.mcp_server import _resolve_schema_references_safe + + +class TestResolveSchemaReferencesSafe(unittest.TestCase): + """Tests for the circular-ref-safe OpenAPI schema resolver.""" + + def test_simple_ref_resolution(self): + reference_schema = { + "components": { + "schemas": { + "Pet": { + "type": "object", + "properties": {"name": {"type": "string"}}, + } + } + } + } + schema_part = {"$ref": "#/components/schemas/Pet"} + result = _resolve_schema_references_safe(schema_part, reference_schema) + self.assertEqual(result["type"], "object") + self.assertNotIn("$ref", result) + self.assertIn("name", result["properties"]) + + def test_circular_ref_breaks_cycle(self): + """Value -> Struct -> Value must not recurse infinitely.""" + reference_schema = { + "components": { + "schemas": { + "Value": { + "type": "object", + "properties": { + "struct_value": {"$ref": "#/components/schemas/Struct"}, + }, + }, + "Struct": { + "type": "object", + "properties": { + "fields": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Value" + }, + } + }, + }, + } + } + } + schema_part = {"$ref": "#/components/schemas/Value"} + result = _resolve_schema_references_safe(schema_part, reference_schema) + self.assertEqual(result["type"], "object") + self.assertNotIn("$ref", result) + # The circular Value ref should be replaced with {"type": "object"} + additional = result["properties"]["struct_value"]["properties"]["fields"][ + "additionalProperties" + ] + self.assertEqual(additional.get("type"), "object") + self.assertNotIn("$ref", additional) + + def test_self_referential_schema(self): + """A schema referencing itself (e.g. a tree node).""" + reference_schema = { + "components": { + "schemas": { + "TreeNode": { + "type": "object", + "properties": { + "children": { + "type": "array", + "items": {"$ref": "#/components/schemas/TreeNode"}, + } + }, + } + } + } + } + schema_part = {"$ref": "#/components/schemas/TreeNode"} + result = _resolve_schema_references_safe(schema_part, reference_schema) + self.assertEqual(result["type"], "object") + child_items = result["properties"]["children"]["items"] + self.assertEqual(child_items.get("type"), "object") + self.assertNotIn("$ref", child_items) + + def test_no_ref_passthrough(self): + schema_part = {"type": "string", "description": "a name"} + result = _resolve_schema_references_safe(schema_part, {}) + self.assertEqual(result, schema_part) + + def test_unknown_ref_preserved(self): + schema_part = {"$ref": "#/components/schemas/Missing"} + reference_schema = {"components": {"schemas": {}}} + result = _resolve_schema_references_safe(schema_part, reference_schema) + self.assertIn("$ref", result) + + def test_does_not_mutate_input(self): + reference_schema = { + "components": { + "schemas": { + "Foo": {"type": "object"}, + } + } + } + schema_part = {"$ref": "#/components/schemas/Foo"} + original_copy = schema_part.copy() + _resolve_schema_references_safe(schema_part, reference_schema) + self.assertEqual(schema_part, original_copy) + + +class TestPatchFastapiMcpSchemaResolver(unittest.TestCase): + """Tests for the monkey-patch helper.""" + + @patch("feast.infra.mcp_servers.mcp_server.MCP_AVAILABLE", True) + def test_patch_replaces_function(self): + from feast.infra.mcp_servers.mcp_server import ( + _patch_fastapi_mcp_schema_resolver, + _resolve_schema_references_safe, + ) + + try: + import fastapi_mcp.openapi.utils as mcp_utils + + original = mcp_utils.resolve_schema_references + _patch_fastapi_mcp_schema_resolver() + self.assertIs( + mcp_utils.resolve_schema_references, + _resolve_schema_references_safe, + ) + mcp_utils.resolve_schema_references = original + except ImportError: + self.skipTest("fastapi_mcp not installed") class TestMcpFeatureServerConfig(unittest.TestCase): @@ -17,7 +150,7 @@ def test_default_config(self): self.assertFalse(config.mcp_enabled) self.assertEqual(config.mcp_server_name, "feast-mcp-server") self.assertEqual(config.mcp_server_version, "1.0.0") - self.assertIsNone(config.mcp_transport) + self.assertEqual(config.mcp_transport, "sse") self.assertEqual(config.transformation_service_endpoint, "localhost:6566") def test_custom_config(self): @@ -41,11 +174,11 @@ def test_custom_config(self): def test_config_validation(self): """Test configuration validation.""" - # Test valid transport options - valid_transports = ["sse", "websocket", None] - for transport in valid_transports: + for transport in ["sse", "http"]: config = McpFeatureServerConfig(mcp_transport=transport) self.assertEqual(config.mcp_transport, transport) + with self.assertRaises(ValidationError): + McpFeatureServerConfig(mcp_transport="websocket") def test_config_inheritance(self): """Test that McpFeatureServerConfig properly inherits from BaseFeatureServerConfig.""" @@ -66,12 +199,13 @@ def test_add_mcp_support_success(self, mock_fast_api_mcp): mock_app = Mock() mock_store = Mock(spec=FeatureStore) - mock_config = Mock() - mock_config.mcp_server_name = "test-server" - mock_config.mcp_server_version = "1.0.0" + mock_config = SimpleNamespace( + mcp_server_name="test-server", + mcp_server_version="1.0.0", + mcp_transport="sse", + ) - # Mock the FastApiMCP instance - mock_mcp_instance = Mock() + mock_mcp_instance = Mock(spec_set=["mount_sse", "mount", "mount_http"]) mock_fast_api_mcp.return_value = mock_mcp_instance result = add_mcp_support_to_app(mock_app, mock_store, mock_config) @@ -83,8 +217,7 @@ def test_add_mcp_support_success(self, mock_fast_api_mcp): description="Feast Feature Store MCP Server - Access feature store data and operations through MCP", ) - # Verify mount was called - mock_mcp_instance.mount.assert_called_once() + mock_mcp_instance.mount_sse.assert_called_once() # Verify the result self.assertEqual(result, mock_mcp_instance) @@ -96,11 +229,9 @@ def test_add_mcp_support_with_defaults(self, mock_fast_api_mcp): mock_app = Mock() mock_store = Mock(spec=FeatureStore) - mock_config = Mock() - # Don't set mcp_server_name to test default - del mock_config.mcp_server_name + mock_config = SimpleNamespace(mcp_transport="sse") - mock_mcp_instance = Mock() + mock_mcp_instance = Mock(spec_set=["mount_sse", "mount", "mount_http"]) mock_fast_api_mcp.return_value = mock_mcp_instance result = add_mcp_support_to_app(mock_app, mock_store, mock_config) @@ -114,6 +245,40 @@ def test_add_mcp_support_with_defaults(self, mock_fast_api_mcp): self.assertEqual(result, mock_mcp_instance) + @patch("feast.infra.mcp_servers.mcp_server.FastApiMCP") + def test_add_mcp_support_http_transport(self, mock_fast_api_mcp): + from feast.infra.mcp_servers.mcp_server import add_mcp_support_to_app + + mock_app = Mock() + mock_store = Mock(spec=FeatureStore) + mock_config = SimpleNamespace( + mcp_server_name="test-server", mcp_transport="http" + ) + + mock_mcp_instance = Mock(spec_set=["mount_http"]) + mock_fast_api_mcp.return_value = mock_mcp_instance + + result = add_mcp_support_to_app(mock_app, mock_store, mock_config) + mock_mcp_instance.mount_http.assert_called_once() + self.assertEqual(result, mock_mcp_instance) + + @patch("feast.infra.mcp_servers.mcp_server.FastApiMCP") + def test_add_mcp_support_http_missing_mount_http_fails(self, mock_fast_api_mcp): + from feast.infra.mcp_servers.mcp_server import ( + McpTransportNotSupportedError, + add_mcp_support_to_app, + ) + + mock_app = Mock() + mock_store = Mock(spec=FeatureStore) + mock_config = SimpleNamespace(mcp_transport="http") + + mock_mcp_instance = Mock(spec_set=["mount"]) + mock_fast_api_mcp.return_value = mock_mcp_instance + + with self.assertRaises(McpTransportNotSupportedError): + add_mcp_support_to_app(mock_app, mock_store, mock_config) + @patch("feast.infra.mcp_servers.mcp_server.FastApiMCP") @patch("feast.infra.mcp_servers.mcp_server.logger") def test_add_mcp_support_with_exception(self, mock_logger, mock_fast_api_mcp): @@ -122,8 +287,9 @@ def test_add_mcp_support_with_exception(self, mock_logger, mock_fast_api_mcp): mock_app = Mock() mock_store = Mock(spec=FeatureStore) - mock_config = Mock() - mock_config.mcp_server_name = "test-server" + mock_config = SimpleNamespace( + mcp_server_name="test-server", mcp_transport="sse" + ) # Mock FastApiMCP to raise an exception mock_fast_api_mcp.side_effect = Exception("MCP initialization failed") @@ -135,7 +301,8 @@ def test_add_mcp_support_with_exception(self, mock_logger, mock_fast_api_mcp): # Verify error was logged mock_logger.error.assert_called_once_with( - "Failed to initialize MCP integration: MCP initialization failed" + "Failed to initialize MCP integration: MCP initialization failed", + exc_info=True, ) @patch("feast.infra.mcp_servers.mcp_server.FastApiMCP") @@ -145,10 +312,11 @@ def test_add_mcp_support_mount_exception(self, mock_fast_api_mcp): mock_app = Mock() mock_store = Mock(spec=FeatureStore) - mock_config = Mock() - mock_config.mcp_server_name = "test-server" + mock_config = SimpleNamespace( + mcp_server_name="test-server", mcp_transport="sse" + ) - mock_mcp_instance = Mock() + mock_mcp_instance = Mock(spec_set=["mount"]) mock_mcp_instance.mount.side_effect = Exception("Mount failed") mock_fast_api_mcp.return_value = mock_mcp_instance @@ -203,3 +371,85 @@ def test_add_mcp_support_if_enabled_exception(self, mock_logger): mock_logger.error.assert_called_with( "Error checking/adding MCP support: Test error" ) + + @patch("feast.infra.mcp_servers.mcp_server.add_mcp_support_to_app") + def test_add_mcp_support_if_enabled_transport_not_supported_fails(self, mock_add): + from feast.feature_server import _add_mcp_support_if_enabled + from feast.infra.mcp_servers.mcp_server import McpTransportNotSupportedError + + mock_app = Mock() + mock_store = Mock() + mock_store.config.feature_server = Mock() + mock_store.config.feature_server.type = "mcp" + mock_store.config.feature_server.mcp_enabled = True + mock_store.config.feature_server.mcp_transport = "http" + + mock_add.side_effect = McpTransportNotSupportedError("bad") + + with self.assertRaises(McpTransportNotSupportedError): + _add_mcp_support_if_enabled(mock_app, mock_store) + + +class TestRestRegistryServerMCP(unittest.TestCase): + """Test MCP integration in RestRegistryServer.""" + + @patch("fastapi_mcp.FastApiMCP") + @patch("feast.api.registry.rest.rest_registry_server.RestRegistryServer._init_auth") + @patch( + "feast.api.registry.rest.rest_registry_server.RestRegistryServer._register_routes" + ) + def test_mcp_mounted_when_enabled( + self, mock_register, mock_auth, mock_fast_api_mcp + ): + """Test that MCP is mounted on RestRegistryServer when registry.mcp.enabled is True.""" + from feast.api.registry.rest.rest_registry_server import RestRegistryServer + + mock_store = Mock() + mock_store.config.registry.mcp = SimpleNamespace(enabled=True) + mock_store.config.auth_config.type = "no_auth" + mock_store.registry = Mock() + mock_store.project = "test_project" + + mock_mcp_instance = Mock(spec_set=["mount_sse", "mount", "mount_http"]) + mock_fast_api_mcp.return_value = mock_mcp_instance + + server = RestRegistryServer(mock_store) + + mock_fast_api_mcp.assert_called_once_with(server.app, name="feast-registry-mcp") + mock_mcp_instance.mount_sse.assert_called_once() + + @patch("feast.api.registry.rest.rest_registry_server.RestRegistryServer._init_auth") + @patch( + "feast.api.registry.rest.rest_registry_server.RestRegistryServer._register_routes" + ) + def test_mcp_not_mounted_when_disabled(self, mock_register, mock_auth): + """Test that MCP is not mounted when registry.mcp.enabled is False.""" + from feast.api.registry.rest.rest_registry_server import RestRegistryServer + + mock_store = Mock() + mock_store.config.registry.mcp = SimpleNamespace(enabled=False) + mock_store.config.auth_config.type = "no_auth" + mock_store.registry = Mock() + mock_store.project = "test_project" + + with patch("fastapi_mcp.FastApiMCP") as mock_fast_api_mcp: + RestRegistryServer(mock_store) + mock_fast_api_mcp.assert_not_called() + + @patch("feast.api.registry.rest.rest_registry_server.RestRegistryServer._init_auth") + @patch( + "feast.api.registry.rest.rest_registry_server.RestRegistryServer._register_routes" + ) + def test_mcp_not_mounted_when_mcp_config_absent(self, mock_register, mock_auth): + """Test that MCP is not mounted when registry.mcp is None.""" + from feast.api.registry.rest.rest_registry_server import RestRegistryServer + + mock_store = Mock() + mock_store.config.registry.mcp = None + mock_store.config.auth_config.type = "no_auth" + mock_store.registry = Mock() + mock_store.project = "test_project" + + with patch("fastapi_mcp.FastApiMCP") as mock_fast_api_mcp: + RestRegistryServer(mock_store) + mock_fast_api_mcp.assert_not_called() diff --git a/sdk/python/tests/unit/infra/offline_stores/contrib/mongodb_offline_store/__init__.py b/sdk/python/tests/unit/infra/offline_stores/contrib/mongodb_offline_store/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/unit/infra/offline_stores/contrib/mongodb_offline_store/test_mongodb.py b/sdk/python/tests/unit/infra/offline_stores/contrib/mongodb_offline_store/test_mongodb.py new file mode 100644 index 00000000000..8fabcf7b28a --- /dev/null +++ b/sdk/python/tests/unit/infra/offline_stores/contrib/mongodb_offline_store/test_mongodb.py @@ -0,0 +1,1719 @@ +""" +Unit tests for MongoDB offline store (mongodb.py). + +Tests the single-collection schema with grouped aggregation. All feature +views share one collection (``feature_history``) discriminated by the +``feature_view`` field. + +Schema: + { + "entity_id": bytes, # serialized entity key + "feature_view": str, # discriminator + "features": {"feat1": val, ...}, + "event_timestamp": datetime, + "created_at": datetime + } + +Docker-dependent tests are marked with ``@_requires_docker`` and are skipped +when Docker is unavailable. +""" + +import os +import tempfile +from datetime import datetime, timedelta +from typing import Dict, Generator, Optional +from unittest.mock import MagicMock, patch + +import pandas as pd +import pyarrow +import pytest +import pytz + +pytest.importorskip("pymongo") + +from pymongo import MongoClient +from testcontainers.mongodb import MongoDbContainer + +from feast import Entity, FeatureView, Field +from feast.errors import SavedDatasetLocationAlreadyExists +from feast.infra.key_encoding_utils import serialize_entity_key +from feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb import ( + MongoDBOfflineStore, + MongoDBOfflineStoreConfig, + MongoDBSource, +) +from feast.infra.offline_stores.file_source import SavedDatasetFileStorage +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import RepoConfig +from feast.types import Float64, Int32, Int64, String +from feast.value_type import ValueType + +# --------------------------------------------------------------------------- +# Docker availability check +# --------------------------------------------------------------------------- + +docker_available = False +try: + import docker + + try: + _docker_client = docker.from_env() + _docker_client.ping() + docker_available = True + except Exception: + pass +except ImportError: + pass + +_requires_docker = pytest.mark.skipif( + not docker_available, + reason="Docker is not available or not running.", +) + +ENTITY_KEY_VERSION = 3 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_entity_id( + join_keys: dict, + value_types: Optional[Dict[str, ValueType]] = None, +) -> bytes: + """Serialize an entity key dict to bytes (matches offline_write_batch serialization).""" + entity_key = EntityKeyProto() + for key in sorted(join_keys.keys()): + entity_key.join_keys.append(key) + val = ValueProto() + value = join_keys[key] + declared_type = value_types.get(key) if value_types else None + if declared_type == ValueType.INT32: + val.int32_val = int(value) + elif declared_type == ValueType.INT64: + val.int64_val = int(value) + elif declared_type == ValueType.STRING: + val.string_val = str(value) + else: + if isinstance(value, bool): + val.bool_val = value + elif isinstance(value, int): + val.int64_val = value + elif isinstance(value, str): + val.string_val = value + else: + val.string_val = str(value) + entity_key.entity_values.append(val) + return serialize_entity_key(entity_key, ENTITY_KEY_VERSION) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def mongodb_container() -> Generator[MongoDbContainer, None, None]: + container = MongoDbContainer( + "mongo:latest", + username="test", + password="test", # pragma: allowlist secret + ).with_exposed_ports(27017) + container.start() + yield container + container.stop() + + +@pytest.fixture +def mongodb_connection_string(mongodb_container: MongoDbContainer) -> str: + exposed_port = mongodb_container.get_exposed_port(27017) + return f"mongodb://test:test@localhost:{exposed_port}" # pragma: allowlist secret + + +@pytest.fixture +def repo_config(mongodb_connection_string: str) -> RepoConfig: + return RepoConfig( + project="test_project", + registry="memory://", + provider="local", + offline_store=MongoDBOfflineStoreConfig( + connection_string=mongodb_connection_string, + database="feast_test", + collection="feature_history", + ), + online_store={"type": "sqlite"}, + entity_key_serialization_version=ENTITY_KEY_VERSION, + ) + + +@pytest.fixture +def sample_data(mongodb_connection_string: str) -> datetime: + """Insert sample data using the feature_history schema. + + 4 documents: driver 1 at 3 timestamps, driver 2 at 1 timestamp. + """ + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + collection.drop() + + now = datetime.now(tz=pytz.UTC) + + docs = [ + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats", + "features": {"conv_rate": 0.5, "acc_rate": 0.9}, + "event_timestamp": now - timedelta(hours=2), + "created_at": now - timedelta(hours=2), + }, + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats", + "features": {"conv_rate": 0.6, "acc_rate": 0.85}, + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + }, + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats", + "features": {"conv_rate": 0.7, "acc_rate": 0.8}, + "event_timestamp": now, + "created_at": now, + }, + { + "entity_id": _make_entity_id({"driver_id": 2}), + "feature_view": "driver_stats", + "features": {"conv_rate": 0.3, "acc_rate": 0.95}, + "event_timestamp": now - timedelta(hours=2), + "created_at": now - timedelta(hours=2), + }, + ] + collection.insert_many(docs) + client.close() + return now + + +@pytest.fixture +def driver_source() -> MongoDBSource: + return MongoDBSource( + name="driver_stats", + timestamp_field="event_timestamp", + created_timestamp_column="created_at", + ) + + +@pytest.fixture +def driver_fv(driver_source: MongoDBSource) -> FeatureView: + driver_entity = Entity( + name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + return FeatureView( + name="driver_stats", + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float64), + Field(name="acc_rate", dtype=Float64), + ], + source=driver_source, + ttl=timedelta(days=1), + ) + + +# --------------------------------------------------------------------------- +# Tests: pull_latest_from_table_or_query +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_pull_latest_from_table_or_query( + repo_config: RepoConfig, sample_data: datetime, driver_source: MongoDBSource +) -> None: + """pull_latest returns the most recent row per entity within the time window.""" + now = sample_data + job = MongoDBOfflineStore.pull_latest_from_table_or_query( + config=repo_config, + data_source=driver_source, + join_key_columns=["driver_id"], + feature_name_columns=["conv_rate", "acc_rate"], + timestamp_field="event_timestamp", + created_timestamp_column="created_at", + start_date=now - timedelta(days=1), + end_date=now + timedelta(hours=1), + ) + + df = job.to_df() + + assert isinstance(df, pd.DataFrame) + assert len(df) == 2 # Two unique entities + + # Join key columns must be present in the output. + assert "driver_id" in df.columns, ( + f"Expected 'driver_id' in output columns, got {list(df.columns)}" + ) + + conv_rates = sorted(df["conv_rate"].tolist()) + assert conv_rates[0] == pytest.approx(0.3) # Driver 2's only value + assert conv_rates[1] == pytest.approx(0.7) # Driver 1's latest + + +# --------------------------------------------------------------------------- +# Tests: get_historical_features (training path — repeated entity IDs) +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_get_historical_features_training_path( + repo_config: RepoConfig, sample_data: datetime, driver_fv: FeatureView +) -> None: + """Training path: repeated entity IDs at different timestamps → merge_asof PIT join.""" + now = sample_data + + entity_df = pd.DataFrame( + { + "driver_id": [1, 1, 2], + "event_timestamp": [ + now - timedelta(hours=1, minutes=30), # → conv_rate=0.5 (doc 2h ago) + now - timedelta(minutes=30), # → conv_rate=0.6 (doc 1h ago) + now - timedelta(hours=1), # → conv_rate=0.3 (only doc) + ], + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[driver_fv], + feature_refs=["driver_stats:conv_rate", "driver_stats:acc_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + result_df = job.to_df() + assert isinstance(result_df, pd.DataFrame) + assert len(result_df) == 3 + + result_df = result_df.sort_values(["driver_id", "event_timestamp"]).reset_index( + drop=True + ) + + # Driver 1, 1.5 hours ago → feature row from 2 hours ago + assert result_df.loc[0, "conv_rate"] == pytest.approx(0.5) + # Driver 1, 30 min ago → feature row from 1 hour ago + assert result_df.loc[1, "conv_rate"] == pytest.approx(0.6) + # Driver 2, 1 hour ago → only feature row (2 hours ago) + assert result_df.loc[2, "conv_rate"] == pytest.approx(0.3) + + +# --------------------------------------------------------------------------- +# Tests: training path created_at tiebreaker +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_training_path_created_at_tiebreaker( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """Training path must pick the highest created_at when event_timestamps tie. + + Two docs for the same entity with identical event_timestamp but different + created_at values. Without sorting fv_df on created_at, merge_asof would + pick whichever doc MongoDB returned first (undefined order). + """ + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + collection.drop() + + now = datetime.now(tz=pytz.UTC) + t_feature = now - timedelta(hours=1) + created_early = now - timedelta(minutes=30) + created_late = now - timedelta(minutes=10) + + driver_entity = Entity( + name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + source = MongoDBSource(name="driver_stats_tiebreak") + fv = FeatureView( + name="driver_stats_tiebreak", + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float64), + ], + source=source, + ttl=timedelta(days=1), + ) + + docs = [ + # Same entity, same event_timestamp, earlier created_at → stale + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats_tiebreak", + "features": {"conv_rate": 0.1}, + "event_timestamp": t_feature, + "created_at": created_early, + }, + # Same entity, same event_timestamp, later created_at → should win + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats_tiebreak", + "features": {"conv_rate": 0.9}, + "event_timestamp": t_feature, + "created_at": created_late, + }, + ] + collection.insert_many(docs) + client.close() + + # Repeated entity → forces training path (merge_asof) + entity_df = pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": [now, now], + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["driver_stats_tiebreak:conv_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + result_df = job.to_df() + assert len(result_df) == 2 + + # Both rows should get conv_rate=0.9 (the doc with later created_at) + for idx in range(len(result_df)): + assert result_df.loc[idx, "conv_rate"] == pytest.approx(0.9), ( + f"Row {idx}: expected 0.9 (latest created_at) but got " + f"{result_df.loc[idx, 'conv_rate']!r}. " + f"Training path may not be sorting by created_at." + ) + + +# --------------------------------------------------------------------------- +# Tests: get_historical_features (scoring path — unique entity IDs) +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_get_historical_features_scoring_path( + repo_config: RepoConfig, sample_data: datetime, driver_fv: FeatureView +) -> None: + """Scoring path: unique entity IDs → server-side $group dedup, vectorized join.""" + now = sample_data + + # Unique entity IDs → scoring path uses $group + entity_df = pd.DataFrame( + { + "driver_id": [1, 2], + "event_timestamp": [now, now], + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[driver_fv], + feature_refs=["driver_stats:conv_rate", "driver_stats:acc_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + result_df = job.to_df().sort_values("driver_id").reset_index(drop=True) + assert len(result_df) == 2 + + # Driver 1: latest value at or before now + assert result_df.loc[0, "conv_rate"] == pytest.approx(0.7) + assert result_df.loc[0, "acc_rate"] == pytest.approx(0.8) + + # Driver 2: only value (2 hours ago) + assert result_df.loc[1, "conv_rate"] == pytest.approx(0.3) + assert result_df.loc[1, "acc_rate"] == pytest.approx(0.95) + + +# --------------------------------------------------------------------------- +# Tests: pull_all_from_table_or_query +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_pull_all_from_table_or_query( + repo_config: RepoConfig, sample_data: datetime, driver_source: MongoDBSource +) -> None: + """pull_all returns all rows in the time window without deduplication.""" + now = sample_data + job = MongoDBOfflineStore.pull_all_from_table_or_query( + config=repo_config, + data_source=driver_source, + join_key_columns=["driver_id"], + feature_name_columns=["conv_rate", "acc_rate"], + timestamp_field="event_timestamp", + created_timestamp_column="created_at", + start_date=now - timedelta(hours=1, minutes=30), + end_date=now + timedelta(hours=1), + ) + + df = job.to_df() + assert isinstance(df, pd.DataFrame) + # 2 rows in window: driver 1 at 1h ago and driver 1 at now. + # Excludes: driver 1 at 2h ago (before start_date) and driver 2 at 2h ago. + assert len(df) == 2 + + # Join key columns must be present in the output. + assert "driver_id" in df.columns, ( + f"Expected 'driver_id' in output columns, got {list(df.columns)}" + ) + + +# --------------------------------------------------------------------------- +# Tests: TTL +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_ttl_excludes_stale_features( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """TTL causes stale feature values to be returned as NULL.""" + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + + now = datetime.now(tz=pytz.UTC) + + ttl_docs = [ + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats_ttl", + "features": {"conv_rate": 0.9}, + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + }, + { + "entity_id": _make_entity_id({"driver_id": 2}), + "feature_view": "driver_stats_ttl", + "features": {"conv_rate": 0.5}, + "event_timestamp": now - timedelta(days=2), # Stale + "created_at": now - timedelta(days=2), + }, + ] + collection.insert_many(ttl_docs) + client.close() + + ttl_source = MongoDBSource( + name="driver_stats_ttl", + timestamp_field="event_timestamp", + ) + driver_entity = Entity( + name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + ttl_fv = FeatureView( + name="driver_stats_ttl", + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float64), + ], + source=ttl_source, + ttl=timedelta(days=1), + ) + + entity_df = pd.DataFrame({"driver_id": [1, 2], "event_timestamp": [now, now]}) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[ttl_fv], + feature_refs=["driver_stats_ttl:conv_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + result_df = job.to_df().sort_values("driver_id").reset_index(drop=True) + + # Driver 1: fresh → has value + assert result_df.loc[0, "conv_rate"] == pytest.approx(0.9) + + # Driver 2: stale (2 days ago, TTL=1 day) → NULL + assert pd.isna(result_df.loc[1, "conv_rate"]) + + +# --------------------------------------------------------------------------- +# Tests: K-collapse (multiple FVs, same join key signature) +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_k_collapse_multiple_feature_views( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """K=2 FVs sharing the same join key are resolved in a single aggregation group.""" + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + + now = datetime.now(tz=pytz.UTC) + + docs = [ + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats_k", + "features": {"rating": 4.8}, + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + }, + { + "entity_id": _make_entity_id({"driver_id": 2}), + "feature_view": "driver_stats_k", + "features": {"rating": 4.5}, + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + }, + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "vehicle_stats_k", + "features": {"vehicle_age": 2, "mileage": 50000}, + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + }, + { + "entity_id": _make_entity_id({"driver_id": 2}), + "feature_view": "vehicle_stats_k", + "features": {"vehicle_age": 5, "mileage": 120000}, + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + }, + ] + collection.insert_many(docs) + client.close() + + driver_source_k = MongoDBSource(name="driver_stats_k") + vehicle_source_k = MongoDBSource(name="vehicle_stats_k") + + driver_entity = Entity( + name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + + driver_fv_k = FeatureView( + name="driver_stats_k", + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="rating", dtype=Float64), + ], + source=driver_source_k, + ttl=timedelta(days=1), + ) + + vehicle_fv_k = FeatureView( + name="vehicle_stats_k", + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="vehicle_age", dtype=Int64), + Field(name="mileage", dtype=Int64), + ], + source=vehicle_source_k, + ttl=timedelta(days=1), + ) + + entity_df = pd.DataFrame({"driver_id": [1, 2], "event_timestamp": [now, now]}) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[driver_fv_k, vehicle_fv_k], + feature_refs=[ + "driver_stats_k:rating", + "vehicle_stats_k:vehicle_age", + "vehicle_stats_k:mileage", + ], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + result_df = job.to_df().sort_values("driver_id").reset_index(drop=True) + + assert len(result_df) == 2 + assert set(result_df.columns) >= {"driver_id", "rating", "vehicle_age", "mileage"} + + assert result_df.loc[0, "rating"] == pytest.approx(4.8) + assert result_df.loc[0, "vehicle_age"] == 2 + assert result_df.loc[0, "mileage"] == 50000 + + assert result_df.loc[1, "rating"] == pytest.approx(4.5) + assert result_df.loc[1, "vehicle_age"] == 5 + assert result_df.loc[1, "mileage"] == 120000 + + +# --------------------------------------------------------------------------- +# Tests: mixed join key cardinality (scoring_path per-FV) +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_mixed_join_key_cardinality( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """FVs with different join key sets must not lose data via $group. + + FV_A uses (user_id), FV_B uses (user_id, device_id). + entity_df has two rows: (user=1, device=X, t=09:00) and (user=1, device=Y, t=10:00). + Unique on (user_id, device_id), but user_id=1 appears twice. + + For FV_A, both rows map to the same entity_id. The old code would use + the scoring path ($group) globally, discarding older docs for user_id=1. + The fix detects that FV_A's entity_id is non-unique and falls back to + merge_asof for FV_A while FV_B can still use the scoring path. + """ + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + + now = datetime.now(tz=pytz.UTC) + t_0800 = now - timedelta(hours=2) # 08:00 + t_0900 = now - timedelta(hours=1) # 09:00 + t_0930 = now - timedelta(minutes=30) # 09:30 + + user_entity = Entity( + name="user_id", join_keys=["user_id"], value_type=ValueType.INT64 + ) + device_entity = Entity( + name="device_id", join_keys=["device_id"], value_type=ValueType.STRING + ) + + # FV_A: keyed on user_id only + source_a = MongoDBSource(name="user_prefs") + fv_a = FeatureView( + name="user_prefs", + entities=[user_entity], + schema=[ + Field(name="user_id", dtype=Int64), + Field(name="theme", dtype=String), + ], + source=source_a, + ttl=timedelta(days=1), + ) + + # FV_B: keyed on (user_id, device_id) + source_b = MongoDBSource(name="device_stats") + fv_b = FeatureView( + name="device_stats", + entities=[user_entity, device_entity], + schema=[ + Field(name="user_id", dtype=Int64), + Field(name="device_id", dtype=String), + Field(name="battery", dtype=Float64), + ], + source=source_b, + ttl=timedelta(days=1), + ) + + # Insert docs for FV_A (user_id only key) + # Two docs for user 1: one at 08:00 ("dark") and one at 09:30 ("light") + docs = [ + { + "entity_id": _make_entity_id({"user_id": 1}), + "feature_view": "user_prefs", + "features": {"theme": "dark"}, + "event_timestamp": t_0800, + "created_at": t_0800, + }, + { + "entity_id": _make_entity_id({"user_id": 1}), + "feature_view": "user_prefs", + "features": {"theme": "light"}, + "event_timestamp": t_0930, + "created_at": t_0930, + }, + # FV_B: keyed on (user_id, device_id) + { + "entity_id": _make_entity_id({"device_id": "X", "user_id": 1}), + "feature_view": "device_stats", + "features": {"battery": 0.8}, + "event_timestamp": t_0800, + "created_at": t_0800, + }, + { + "entity_id": _make_entity_id({"device_id": "Y", "user_id": 1}), + "feature_view": "device_stats", + "features": {"battery": 0.6}, + "event_timestamp": t_0800, + "created_at": t_0800, + }, + ] + collection.insert_many(docs) + client.close() + + # entity_df: two rows, unique on (user_id, device_id) but NOT on user_id + entity_df = pd.DataFrame( + { + "user_id": [1, 1], + "device_id": ["X", "Y"], + "event_timestamp": [t_0900, now], # 09:00 and 10:00 + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv_a, fv_b], + feature_refs=["user_prefs:theme", "device_stats:battery"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + strict_pit=True, + ) + + result_df = job.to_df().sort_values("device_id").reset_index(drop=True) + + assert len(result_df) == 2 + + # Row 0: device=X, request at 09:00 + # FV_A (user_id=1): 09:30 doc is AFTER 09:00 (strict_pit), 08:00 doc is valid → "dark" + # FV_B (user=1, device=X): 08:00 doc is before 09:00 → battery=0.8 + assert result_df.loc[0, "theme"] == "dark", ( + f"Expected 'dark' for row at 09:00 but got {result_df.loc[0, 'theme']!r}. " + "The scoring path $group may have discarded the 08:00 doc." + ) + assert result_df.loc[0, "battery"] == pytest.approx(0.8) + + # Row 1: device=Y, request at 10:00 + # FV_A (user_id=1): 09:30 doc is before 10:00 → "light" + # FV_B (user=1, device=Y): 08:00 doc is before 10:00 → battery=0.6 + assert result_df.loc[1, "theme"] == "light" + assert result_df.loc[1, "battery"] == pytest.approx(0.6) + + +# --------------------------------------------------------------------------- +# Tests: heterogeneous timestamps prevent scoring path +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_heterogeneous_timestamps_fall_back_to_training_path( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """Unique entity IDs but different request timestamps must use training path. + + Entity A requests at 09:00, Entity B at 10:00. + Doc for A: 08:00 (valid) and 09:30 (future for A's request). + Doc for B: 09:00 (valid). + + If the scoring path were used, $group with $lte max_ts=10:00 would pick + the 09:30 doc for A. future_mask would null it, but the valid 08:00 doc + was already discarded. The training path (merge_asof) correctly returns + the 08:00 doc for A. + """ + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + + now = datetime.now(tz=pytz.UTC) + t_0800 = now - timedelta(hours=2) + t_0900 = now - timedelta(hours=1) + t_0930 = now - timedelta(minutes=30) + + driver_entity = Entity( + name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + source = MongoDBSource(name="driver_stats_het") + fv = FeatureView( + name="driver_stats_het", + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float64), + ], + source=source, + ttl=timedelta(days=1), + ) + + docs = [ + # Driver 1: valid doc at 08:00 + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats_het", + "features": {"conv_rate": 0.5}, + "event_timestamp": t_0800, + "created_at": t_0800, + }, + # Driver 1: doc at 09:30 (future relative to driver 1's request at 09:00) + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats_het", + "features": {"conv_rate": 0.9}, + "event_timestamp": t_0930, + "created_at": t_0930, + }, + # Driver 2: valid doc at 09:00 + { + "entity_id": _make_entity_id({"driver_id": 2}), + "feature_view": "driver_stats_het", + "features": {"conv_rate": 0.7}, + "event_timestamp": t_0900, + "created_at": t_0900, + }, + ] + collection.insert_many(docs) + client.close() + + # Different request timestamps: driver 1 at 09:00, driver 2 at 10:00 + entity_df = pd.DataFrame( + { + "driver_id": [1, 2], + "event_timestamp": [t_0900, now], + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["driver_stats_het:conv_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + strict_pit=True, + ) + + result_df = job.to_df().sort_values("driver_id").reset_index(drop=True) + assert len(result_df) == 2 + + # Driver 1: request at 09:00, 09:30 doc is future → must return 08:00 doc (0.5) + assert result_df.loc[0, "conv_rate"] == pytest.approx(0.5), ( + f"Expected 0.5 for driver 1 (08:00 doc) but got " + f"{result_df.loc[0, 'conv_rate']!r}. The scoring path may have " + f"discarded the valid older doc via $group." + ) + + # Driver 2: request at 10:00, 09:00 doc is valid → 0.7 + assert result_df.loc[1, "conv_rate"] == pytest.approx(0.7) + + +# --------------------------------------------------------------------------- +# Tests: overlapping feature names +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_overlapping_feature_names_full_feature_names( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """full_feature_names=True prevents collision when multiple FVs share a feature name.""" + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + + now = datetime.now(tz=pytz.UTC) + + docs = [] + for fv_name, score_val in [ + ("fv_overlap_a", 1.0), + ("fv_overlap_b", 2.0), + ("fv_overlap_c", 3.0), + ]: + for driver_id in [1, 2]: + docs.append( + { + "entity_id": _make_entity_id({"driver_id": driver_id}), + "feature_view": fv_name, + "features": {"score": score_val + driver_id * 0.1}, + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + } + ) + collection.insert_many(docs) + client.close() + + driver_entity = Entity( + name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + fvs = [] + feature_refs = [] + for fv_name in ["fv_overlap_a", "fv_overlap_b", "fv_overlap_c"]: + source = MongoDBSource(name=fv_name) + fv = FeatureView( + name=fv_name, + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="score", dtype=Float64), + ], + source=source, + ttl=timedelta(days=1), + ) + fvs.append(fv) + feature_refs.append(f"{fv_name}:score") + + entity_df = pd.DataFrame({"driver_id": [1, 2], "event_timestamp": [now, now]}) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=fvs, + feature_refs=feature_refs, + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=True, + ) + result_df = job.to_df().sort_values("driver_id").reset_index(drop=True) + + assert len(result_df) == 2 + assert "fv_overlap_a__score" in result_df.columns + assert "fv_overlap_b__score" in result_df.columns + assert "fv_overlap_c__score" in result_df.columns + + assert result_df.loc[0, "fv_overlap_a__score"] == pytest.approx(1.1) + assert result_df.loc[0, "fv_overlap_b__score"] == pytest.approx(2.1) + assert result_df.loc[0, "fv_overlap_c__score"] == pytest.approx(3.1) + assert result_df.loc[1, "fv_overlap_a__score"] == pytest.approx(1.2) + assert result_df.loc[1, "fv_overlap_b__score"] == pytest.approx(2.2) + assert result_df.loc[1, "fv_overlap_c__score"] == pytest.approx(3.2) + + +# --------------------------------------------------------------------------- +# Tests: compound join keys +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_compound_join_keys( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """Compound join keys (user_id + device_id) are serialized and matched correctly.""" + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + + now = datetime.now(tz=pytz.UTC) + + docs = [ + { + "entity_id": _make_entity_id({"user_id": 1, "device_id": "mobile"}), + "feature_view": "user_device_features", + "features": {"app_opens": 50}, + "event_timestamp": now - timedelta(hours=2), + "created_at": now - timedelta(hours=2), + }, + { + "entity_id": _make_entity_id({"user_id": 1, "device_id": "mobile"}), + "feature_view": "user_device_features", + "features": {"app_opens": 55}, # Latest + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + }, + { + "entity_id": _make_entity_id({"user_id": 1, "device_id": "desktop"}), + "feature_view": "user_device_features", + "features": {"app_opens": 10}, + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + }, + { + "entity_id": _make_entity_id({"user_id": 2, "device_id": "tablet"}), + "feature_view": "user_device_features", + "features": {"app_opens": 25}, + "event_timestamp": now - timedelta(hours=1), + "created_at": now - timedelta(hours=1), + }, + ] + collection.insert_many(docs) + client.close() + + source = MongoDBSource(name="user_device_features") + user_entity = Entity( + name="user_id", join_keys=["user_id"], value_type=ValueType.INT64 + ) + device_entity = Entity( + name="device_id", join_keys=["device_id"], value_type=ValueType.STRING + ) + + fv = FeatureView( + name="user_device_features", + entities=[user_entity, device_entity], + schema=[ + Field(name="user_id", dtype=Int64), + Field(name="device_id", dtype=String), + Field(name="app_opens", dtype=Int64), + ], + source=source, + ttl=timedelta(days=1), + ) + + # pull_latest: one row per unique (user_id, device_id) + job = MongoDBOfflineStore.pull_latest_from_table_or_query( + config=repo_config, + data_source=source, + join_key_columns=["user_id", "device_id"], + feature_name_columns=["app_opens"], + timestamp_field="event_timestamp", + created_timestamp_column="created_at", + start_date=now - timedelta(days=1), + end_date=now + timedelta(hours=1), + ) + + df = job.to_df() + assert len(df) == 3 + + app_opens_values = sorted(df["app_opens"].tolist()) + assert 10 in app_opens_values + assert 25 in app_opens_values + assert 55 in app_opens_values + + # get_historical_features with compound keys + entity_df = pd.DataFrame( + { + "user_id": [1, 1, 2], + "device_id": ["mobile", "desktop", "tablet"], + "event_timestamp": [now, now, now], + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["user_device_features:app_opens"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + result_df = job.to_df() + assert len(result_df) == 3 + + result_df = result_df.sort_values(["user_id", "device_id"]).reset_index(drop=True) + + assert result_df.loc[0, "app_opens"] == 10 # user 1, desktop + assert result_df.loc[1, "app_opens"] == 55 # user 1, mobile (latest) + assert result_df.loc[2, "app_opens"] == 25 # user 2, tablet + + +# --------------------------------------------------------------------------- +# Tests: extra columns in entity_df +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_entity_df_with_extra_columns( + repo_config: RepoConfig, sample_data: datetime, driver_fv: FeatureView +) -> None: + """Label columns in entity_df must not corrupt entity key serialization.""" + now = sample_data + + entity_df = pd.DataFrame( + { + "driver_id": [1, 1, 2], + "event_timestamp": [ + now - timedelta(hours=1, minutes=30), + now - timedelta(minutes=30), + now - timedelta(hours=1), + ], + "trip_success": [1, 0, 1], # label — must not enter entity key + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[driver_fv], + feature_refs=["driver_stats:conv_rate", "driver_stats:acc_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + result_df = job.to_df() + assert isinstance(result_df, pd.DataFrame) + assert len(result_df) == 3 + assert "trip_success" in result_df.columns + assert sorted(result_df["trip_success"].tolist()) == [0, 1, 1] + + assert result_df["conv_rate"].notna().all(), ( + "conv_rate is null — label column was likely included in entity key serialization." + ) + + result_df = result_df.sort_values(["driver_id", "event_timestamp"]).reset_index( + drop=True + ) + assert result_df.loc[0, "conv_rate"] == pytest.approx(0.5) + assert result_df.loc[1, "conv_rate"] == pytest.approx(0.6) + assert result_df.loc[2, "conv_rate"] == pytest.approx(0.3) + + +# --------------------------------------------------------------------------- +# Tests: INT32 entity key +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_int32_entity_key( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """INT32 entity keys use int32_val in the proto, not int64_val. + + offline_write_batch must serialize INT32 entities with int32_val so that + the bytes match what get_historical_features produces. + """ + driver_entity = Entity( + name="order_id", join_keys=["order_id"], value_type=ValueType.INT32 + ) + source = MongoDBSource(name="order_features") + fv = FeatureView( + name="order_features", + entities=[driver_entity], + schema=[ + Field(name="order_id", dtype=Int32), + Field(name="amount", dtype=Float64), + ], + source=source, + ttl=timedelta(days=1), + ) + + client: MongoClient = MongoClient(mongodb_connection_string) + client["feast_test"]["feature_history"].drop() + client.close() + + now = datetime.now(tz=pytz.UTC) + + # Write via offline_write_batch (uses mongodb.py's serializer) + df = pd.DataFrame( + { + "order_id": [1, 2], + "amount": [100.0, 200.0], + "event_timestamp": [now - timedelta(hours=1), now - timedelta(hours=1)], + "created_at": [now, now], + } + ) + table = pyarrow.Table.from_pandas(df) + + MongoDBOfflineStore.offline_write_batch( + config=repo_config, + feature_view=fv, + table=table, + progress=None, + ) + + entity_df = pd.DataFrame( + { + "order_id": [1, 2], + "event_timestamp": [now, now], + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["order_features:amount"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + result_df = job.to_df().sort_values("order_id").reset_index(drop=True) + assert len(result_df) == 2 + assert result_df["amount"].notna().all(), ( + "amount is null — INT32 entity key serialization mismatch between " + "offline_write_batch and get_historical_features." + ) + assert result_df.loc[0, "amount"] == pytest.approx(100.0) + assert result_df.loc[1, "amount"] == pytest.approx(200.0) + + +# --------------------------------------------------------------------------- +# Tests: persist +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_persist_writes_parquet( + repo_config: RepoConfig, + sample_data: datetime, + driver_fv: FeatureView, +) -> None: + """persist() writes the joined result to a parquet file.""" + now = sample_data + + entity_df = pd.DataFrame( + { + "driver_id": [1, 2], + "event_timestamp": [now, now - timedelta(hours=2)], + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[driver_fv], + feature_refs=["driver_stats:conv_rate", "driver_stats:acc_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as f: + path = f.name + + try: + os.unlink(path) # persist creates the file; it must not already exist + job.persist(SavedDatasetFileStorage(path=path)) + + assert os.path.exists(path) + result_df = pd.read_parquet(path) + assert len(result_df) == 2 + assert {"driver_id", "conv_rate", "acc_rate", "event_timestamp"}.issubset( + result_df.columns + ) + finally: + if os.path.exists(path): + os.unlink(path) + + +@_requires_docker +def test_persist_raises_if_file_exists( + repo_config: RepoConfig, + sample_data: datetime, + driver_fv: FeatureView, +) -> None: + """persist() raises SavedDatasetLocationAlreadyExists when the file already exists.""" + now = sample_data + + entity_df = pd.DataFrame({"driver_id": [1], "event_timestamp": [now]}) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[driver_fv], + feature_refs=["driver_stats:conv_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as f: + path = f.name + + try: + with pytest.raises(SavedDatasetLocationAlreadyExists): + job.persist(SavedDatasetFileStorage(path=path), allow_overwrite=False) + finally: + if os.path.exists(path): + os.unlink(path) + + +@_requires_docker +def test_persist_allow_overwrite( + repo_config: RepoConfig, + sample_data: datetime, + driver_fv: FeatureView, +) -> None: + """persist(allow_overwrite=True) replaces any existing file.""" + now = sample_data + + entity_df = pd.DataFrame({"driver_id": [1], "event_timestamp": [now]}) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[driver_fv], + feature_refs=["driver_stats:conv_rate", "driver_stats:acc_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as f: + path = f.name + f.write(b"stale content") + + try: + job.persist(SavedDatasetFileStorage(path=path), allow_overwrite=True) + + result_df = pd.read_parquet(path) + assert len(result_df) == 1 + assert "driver_id" in result_df.columns + finally: + if os.path.exists(path): + os.unlink(path) + + +# --------------------------------------------------------------------------- +# Tests: offline_write_batch round-trip +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_offline_write_batch_round_trip( + repo_config: RepoConfig, + mongodb_connection_string: str, + driver_fv: FeatureView, +) -> None: + """offline_write_batch writes documents that get_historical_features reads back.""" + client: MongoClient = MongoClient(mongodb_connection_string) + client["feast_test"]["feature_history"].drop() + client.close() + + now = datetime.now(tz=pytz.UTC) + + df = pd.DataFrame( + { + "driver_id": [1, 2], + "conv_rate": [0.8, 0.6], + "acc_rate": [0.9, 0.7], + "event_timestamp": [now - timedelta(hours=1), now - timedelta(hours=1)], + "created_at": [now, now], + } + ) + table = pyarrow.Table.from_pandas(df) + + MongoDBOfflineStore.offline_write_batch( + config=repo_config, + feature_view=driver_fv, + table=table, + progress=None, + ) + + # Verify document structure + client = MongoClient(mongodb_connection_string) + docs = list( + client["feast_test"]["feature_history"].find( + {"feature_view": "driver_stats"}, {"_id": 0} + ) + ) + client.close() + + assert len(docs) == 2 + doc = docs[0] + assert isinstance(doc["entity_id"], bytes), "entity_id must be serialized bytes" + assert doc["feature_view"] == "driver_stats" + assert set(doc["features"].keys()) == {"conv_rate", "acc_rate"} + assert "event_timestamp" in doc + assert "created_at" in doc + + # Round-trip: read back via get_historical_features + entity_df = pd.DataFrame({"driver_id": [1, 2], "event_timestamp": [now, now]}) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[driver_fv], + feature_refs=["driver_stats:conv_rate", "driver_stats:acc_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + ) + + result_df = job.to_df().sort_values("driver_id").reset_index(drop=True) + assert len(result_df) == 2 + assert result_df["conv_rate"].notna().all(), ( + "conv_rate is null — offline_write_batch and get_historical_features " + "produced different entity_id bytes." + ) + assert result_df.loc[0, "conv_rate"] == pytest.approx(0.8) + assert result_df.loc[0, "acc_rate"] == pytest.approx(0.9) + assert result_df.loc[1, "conv_rate"] == pytest.approx(0.6) + assert result_df.loc[1, "acc_rate"] == pytest.approx(0.7) + + +# --------------------------------------------------------------------------- +# Tests: max_ts overshoot (strict_pit semantics) +# --------------------------------------------------------------------------- + + +@_requires_docker +def test_scoring_path_nulls_future_doc( + repo_config: RepoConfig, mongodb_connection_string: str, driver_fv: FeatureView +) -> None: + """strict_pit=True: a doc whose timestamp is after the entity request time → NULL. + + Scenario: + Entity A requests features at 09:00. + The only available document is at 10:30 (after the request time). + With strict_pit=True the scoring path must return NULL for entity A. + + This exercises the ``future_mask`` in the scoring path. The aggregation + uses ``max_ts`` as the server-side upper bound, so the 10:30 doc IS + fetched from MongoDB. The Python-side ``future_mask`` is what nulls it. + """ + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + collection.drop() + + base = datetime.now(tz=pytz.UTC).replace(microsecond=0) + request_a = base.replace(hour=9, minute=0, second=0) + request_b = base.replace(hour=11, minute=0, second=0) + doc_ts_a = base.replace(hour=10, minute=30, second=0) # after A's request time + doc_ts_b = base.replace(hour=10, minute=0, second=0) # before B's request time + + # Replace with fixed dates to avoid day-boundary issues + from datetime import timezone + + now = datetime.now(tz=timezone.utc) + request_a = now - timedelta(hours=3) # 3 hours ago + request_b = now # now + doc_ts_a = now - timedelta( + hours=1 + ) # 1 hour ago — AFTER request_a, BEFORE request_b + doc_ts_b = now - timedelta(hours=2) # 2 hours ago — before both requests + + docs = [ + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats_pit", + "features": {"conv_rate": 0.9}, + "event_timestamp": doc_ts_a, + "created_at": doc_ts_a, + }, + { + "entity_id": _make_entity_id({"driver_id": 2}), + "feature_view": "driver_stats_pit", + "features": {"conv_rate": 0.5}, + "event_timestamp": doc_ts_b, + "created_at": doc_ts_b, + }, + ] + collection.insert_many(docs) + client.close() + + pit_source = MongoDBSource( + name="driver_stats_pit", timestamp_field="event_timestamp" + ) + driver_entity = Entity( + name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + pit_fv = FeatureView( + name="driver_stats_pit", + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float64), + ], + source=pit_source, + ttl=timedelta(days=7), + ) + + # Unique entity IDs → scoring path + entity_df = pd.DataFrame( + { + "driver_id": [1, 2], + "event_timestamp": [request_a, request_b], + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[pit_fv], + feature_refs=["driver_stats_pit:conv_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + strict_pit=True, + ) + + result_df = job.to_df().sort_values("driver_id").reset_index(drop=True) + assert len(result_df) == 2 + + # Driver 1: only doc is at doc_ts_a which is AFTER request_a → NULL + assert pd.isna(result_df.loc[0, "conv_rate"]), ( + f"Expected NULL for driver 1 (doc at {doc_ts_a} is after request at " + f"{request_a}), got {result_df.loc[0, 'conv_rate']!r}" + ) + + # Driver 2: doc is at doc_ts_b which is BEFORE request_b → value returned + assert result_df.loc[1, "conv_rate"] == pytest.approx(0.5) + + +@_requires_docker +def test_scoring_path_nulls_future_doc_chunk_size_1( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """Same as test_scoring_path_nulls_future_doc but with CHUNK_SIZE=1. + + With CHUNK_SIZE=1 each entity is processed in its own chunk. Per-chunk + ``max_ts`` equals that entity's own request timestamp, so the 10:30 doc + for entity A is never even fetched from MongoDB (server-side filter). + Either way, the result for entity A must be NULL and entity B must have + a value. + """ + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + collection.drop() + + from datetime import timezone + + now = datetime.now(tz=timezone.utc) + request_a = now - timedelta(hours=3) + request_b = now + doc_ts_a = now - timedelta(hours=1) # after request_a + doc_ts_b = now - timedelta(hours=2) # before both + + docs = [ + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats_pit2", + "features": {"conv_rate": 0.9}, + "event_timestamp": doc_ts_a, + "created_at": doc_ts_a, + }, + { + "entity_id": _make_entity_id({"driver_id": 2}), + "feature_view": "driver_stats_pit2", + "features": {"conv_rate": 0.5}, + "event_timestamp": doc_ts_b, + "created_at": doc_ts_b, + }, + ] + collection.insert_many(docs) + client.close() + + pit_source = MongoDBSource( + name="driver_stats_pit2", timestamp_field="event_timestamp" + ) + driver_entity = Entity( + name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + pit_fv = FeatureView( + name="driver_stats_pit2", + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float64), + ], + source=pit_source, + ttl=timedelta(days=7), + ) + + entity_df = pd.DataFrame( + { + "driver_id": [1, 2], + "event_timestamp": [request_a, request_b], + } + ) + + mongodb_module = "feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb" + + # Force CHUNK_SIZE=1 so each entity is in its own chunk + with patch(f"{mongodb_module}._CHUNK_SIZE", 1): + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[pit_fv], + feature_refs=["driver_stats_pit2:conv_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + strict_pit=True, + ) + result_df = job.to_df().sort_values("driver_id").reset_index(drop=True) + + assert len(result_df) == 2 + + # Driver 1: doc at doc_ts_a (after request_a) → NULL regardless of chunk boundary + assert pd.isna(result_df.loc[0, "conv_rate"]), ( + f"Expected NULL for driver 1 with CHUNK_SIZE=1, got " + f"{result_df.loc[0, 'conv_rate']!r}" + ) + + # Driver 2: doc at doc_ts_b (before request_b) → value returned + assert result_df.loc[1, "conv_rate"] == pytest.approx(0.5) + + +@_requires_docker +def test_strict_pit_false_returns_future_doc( + repo_config: RepoConfig, mongodb_connection_string: str +) -> None: + """strict_pit=False: future doc IS returned (real-time inference mode). + + Same scenario as test_scoring_path_nulls_future_doc: + Entity A requests at 3 hours ago; only doc is at 1 hour ago (future). + + With strict_pit=False the future_mask is suppressed and the most recent + document is returned regardless of whether it post-dates the request time. + This is correct for real-time scoring where you always want the latest + observation. + """ + client: MongoClient = MongoClient(mongodb_connection_string) + db = client["feast_test"] + collection = db["feature_history"] + collection.drop() + + from datetime import timezone + + now = datetime.now(tz=timezone.utc) + request_a = now - timedelta(hours=3) + doc_ts_a = now - timedelta(hours=1) # AFTER request_a + + docs = [ + { + "entity_id": _make_entity_id({"driver_id": 1}), + "feature_view": "driver_stats_pit3", + "features": {"conv_rate": 0.9}, + "event_timestamp": doc_ts_a, + "created_at": doc_ts_a, + }, + ] + collection.insert_many(docs) + client.close() + + pit_source = MongoDBSource( + name="driver_stats_pit3", timestamp_field="event_timestamp" + ) + driver_entity = Entity( + name="driver_id", join_keys=["driver_id"], value_type=ValueType.INT64 + ) + pit_fv = FeatureView( + name="driver_stats_pit3", + entities=[driver_entity], + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float64), + ], + source=pit_source, + ttl=timedelta(days=7), + ) + + entity_df = pd.DataFrame( + { + "driver_id": [1], + "event_timestamp": [request_a], + } + ) + + job = MongoDBOfflineStore.get_historical_features( + config=repo_config, + feature_views=[pit_fv], + feature_refs=["driver_stats_pit3:conv_rate"], + entity_df=entity_df, + registry=MagicMock(), + project=repo_config.project, + full_feature_names=False, + strict_pit=False, # Accept future doc + ) + + result_df = job.to_df().reset_index(drop=True) + assert len(result_df) == 1 + + # With strict_pit=False the doc at doc_ts_a (after request_a) IS returned + assert result_df.loc[0, "conv_rate"] == pytest.approx(0.9), ( + f"Expected conv_rate=0.9 with strict_pit=False but got " + f"{result_df.loc[0, 'conv_rate']!r}. The future_mask may not be " + f"correctly suppressed." + ) diff --git a/sdk/python/tests/unit/infra/offline_stores/contrib/postgres_offline_store/test_postgres.py b/sdk/python/tests/unit/infra/offline_stores/contrib/postgres_offline_store/test_postgres.py index ce98315eef4..3eb01a0e6d0 100644 --- a/sdk/python/tests/unit/infra/offline_stores/contrib/postgres_offline_store/test_postgres.py +++ b/sdk/python/tests/unit/infra/offline_stores/contrib/postgres_offline_store/test_postgres.py @@ -615,6 +615,71 @@ def test_non_entity_mode_with_both_dates(self): assert "start_date" not in str(e) assert "end_date" not in str(e) + def test_non_entity_entity_df_uses_end_date(self): + """Test that the synthetic entity_df uses end_date, not start_date. + + Regression test: the old code used pd.date_range(start=start_date, ...)[:1] + which put start_date in the entity_df. Since PIT joins use + MAX(entity_timestamp) as the upper bound, start_date made end_date + unreachable. The fix uses [end_date] directly. + """ + test_repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=_mock_offline_store_config(), + ) + + feature_view = _mock_feature_view("test_fv", ttl=timedelta(days=1)) + start_date = datetime(2023, 1, 1, tzinfo=timezone.utc) + end_date = datetime(2023, 1, 7, tzinfo=timezone.utc) + + mock_get_entity_schema = MagicMock( + return_value={"event_timestamp": "timestamp"} + ) + + with ( + patch.multiple( + "feast.infra.offline_stores.contrib.postgres_offline_store.postgres", + _get_conn=MagicMock(), + _upload_entity_df=MagicMock(), + _get_entity_schema=mock_get_entity_schema, + _get_entity_df_event_timestamp_range=MagicMock( + return_value=(start_date, end_date) + ), + ), + patch( + "feast.infra.offline_stores.contrib.postgres_offline_store.postgres.offline_utils.get_expected_join_keys", + return_value=[], + ), + patch( + "feast.infra.offline_stores.contrib.postgres_offline_store.postgres.offline_utils.assert_expected_columns_in_entity_df", + ), + patch( + "feast.infra.offline_stores.contrib.postgres_offline_store.postgres.offline_utils.get_feature_view_query_context", + return_value=[], + ), + ): + PostgreSQLOfflineStore.get_historical_features( + config=test_repo_config, + feature_views=[feature_view], + feature_refs=["test_fv:feature1"], + entity_df=None, + registry=MagicMock(), + project="test_project", + start_date=start_date, + end_date=end_date, + ) + + # _get_entity_schema is called with the synthetic entity_df + df = mock_get_entity_schema.call_args[0][0] + assert len(df) == 1 + ts = df["event_timestamp"].iloc[0] + # The entity_df must use end_date, not start_date + assert ts == end_date, ( + f"entity_df timestamp should be end_date ({end_date}), got {ts}" + ) + def test_non_entity_mode_with_end_date_only(self): """Test non-entity retrieval calculates start_date from TTL""" test_repo_config = RepoConfig( @@ -888,7 +953,10 @@ def test_cli_date_combinations(self): from pathlib import Path from textwrap import dedent - from tests.utils.cli_repo_creator import CliRunner, get_example_repo + from click.testing import CliRunner + + from feast.cli.cli import cli + from feast.feature_store import FeatureStore runner = CliRunner() @@ -910,43 +978,208 @@ def test_cli_date_combinations(self): """) ) - repo_example = repo_path / "example.py" - repo_example.write_text(get_example_repo("example_feature_repo_1.py")) - - result = runner.run(["apply"], cwd=repo_path) - assert result.returncode == 0 - - # Test 1: Both dates provided - should parse correctly - result = runner.run( - [ - "get-historical-features", - "--features", - "driver_hourly_stats:conv_rate", - "--start-date", - "2023-01-01 00:00:00", - "--end-date", - "2023-01-07 00:00:00", - ], - cwd=repo_path, - ) + retrieval_job = MagicMock() + retrieval_job.to_df.return_value = pd.DataFrame() + + with patch.object( + FeatureStore, "get_historical_features", return_value=retrieval_job + ) as mock_get_historical_features: + # Test 1: Both dates provided - should parse correctly + result = runner.invoke( + cli, + [ + "--chdir", + str(repo_path), + "get-historical-features", + "--features", + "driver_hourly_stats:conv_rate", + "--start-date", + "2023-01-01 00:00:00", + "--end-date", + "2023-01-07 00:00:00", + ], + ) - # Should not fail on date parsing - stderr_output = result.stderr.decode() - assert "Error parsing" not in stderr_output - assert "time data" not in stderr_output # datetime parsing errors - - # Test 2: Only end date provided - should work (start_date calculated from TTL) - result = runner.run( - [ - "get-historical-features", - "--features", - "driver_hourly_stats:conv_rate", - "--end-date", - "2023-01-07 00:00:00", - ], - cwd=repo_path, - ) + assert result.exit_code == 0, result.output + assert mock_get_historical_features.call_args.kwargs[ + "start_date" + ] == datetime(2023, 1, 1) + assert mock_get_historical_features.call_args.kwargs[ + "end_date" + ] == datetime(2023, 1, 7) + + # Test 2: Only end date provided - should work (start_date calculated from TTL) + result = runner.invoke( + cli, + [ + "--chdir", + str(repo_path), + "get-historical-features", + "--features", + "driver_hourly_stats:conv_rate", + "--end-date", + "2023-01-07 00:00:00", + ], + ) + + assert result.exit_code == 0, result.output + assert ( + mock_get_historical_features.call_args.kwargs["start_date"] is None + ) + assert mock_get_historical_features.call_args.kwargs[ + "end_date" + ] == datetime(2023, 1, 7) + + # Test 3: No date or dataframe provided - should fail validation before retrieval. + result = runner.invoke( + cli, + [ + "--chdir", + str(repo_path), + "get-historical-features", + "--features", + "driver_hourly_stats:conv_rate", + ], + ) + + assert result.exit_code == 0, result.output + assert ( + "Either --dataframe or --start-date and/or --end-date must be provided." + in result.output + ) + assert mock_get_historical_features.call_count == 2 + + +class TestPostgreSQLSourceQueryStringAlias: + """Test suite for get_table_query_string_with_alias() method. + + This addresses GitHub issue #5605: PostgreSQL requires all subqueries + in FROM clauses to have aliases. + """ + + def test_table_source_get_table_query_string(self): + """Test get_table_query_string() with table-based source""" + source = PostgreSQLSource( + name="test_source", + table="my_schema.my_table", + timestamp_field="event_timestamp", + ) + result = source.get_table_query_string() + assert result == "my_schema.my_table" + + def test_query_source_get_table_query_string(self): + """Test get_table_query_string() with query-based source""" + source = PostgreSQLSource( + name="test_source", + query="SELECT * FROM my_table WHERE active = true", + timestamp_field="event_timestamp", + ) + result = source.get_table_query_string() + assert result == "(SELECT * FROM my_table WHERE active = true)" + + def test_table_source_with_alias(self): + """Test get_table_query_string_with_alias() with table-based source returns table without alias""" + source = PostgreSQLSource( + name="test_source", + table="my_schema.my_table", + timestamp_field="event_timestamp", + ) + result = source.get_table_query_string_with_alias() + # Table sources don't need aliases + assert result == "my_schema.my_table" + + def test_query_source_with_default_alias(self): + """Test get_table_query_string_with_alias() with query-based source uses default alias""" + source = PostgreSQLSource( + name="test_source", + query="SELECT * FROM my_table WHERE active = true", + timestamp_field="event_timestamp", + ) + result = source.get_table_query_string_with_alias() + assert result == "(SELECT * FROM my_table WHERE active = true) AS subquery" + + def test_query_source_with_custom_alias(self): + """Test get_table_query_string_with_alias() with custom alias""" + source = PostgreSQLSource( + name="test_source", + query="SELECT id, name FROM users", + timestamp_field="event_timestamp", + ) + result = source.get_table_query_string_with_alias(alias="user_data") + assert result == "(SELECT id, name FROM users) AS user_data" + + def test_table_source_with_custom_alias_ignored(self): + """Test get_table_query_string_with_alias() ignores alias for table-based sources""" + source = PostgreSQLSource( + name="test_source", + table="events", + timestamp_field="event_timestamp", + ) + result = source.get_table_query_string_with_alias(alias="ignored_alias") + # Alias should be ignored for table sources + assert result == "events" + + def test_sql_query_with_alias_is_valid(self): + """Test that SQL using get_table_query_string_with_alias() is syntactically valid""" + source = PostgreSQLSource( + name="test_source", + query="SELECT id, ts FROM raw_data", + timestamp_field="ts", + ) + + # Construct a SQL query using the new method + entity_sql = f"SELECT id, ts FROM {source.get_table_query_string_with_alias()}" + + # Verify SQL is valid using sqlglot + parsed = sqlglot.parse(entity_sql, dialect="postgres") + assert len(parsed) == 1 + assert parsed[0] is not None + + def test_sql_query_without_alias_fails_in_postgres(self): + """Test that SQL using get_table_query_string() for query source produces invalid PostgreSQL + + This demonstrates the issue that get_table_query_string_with_alias() fixes: + PostgreSQL requires all subqueries in FROM clauses to have aliases. + """ + source = PostgreSQLSource( + name="test_source", + query="SELECT id, ts FROM raw_data", + timestamp_field="ts", + ) + + # Using the old method (without alias) for query-based source + entity_sql_without_alias = ( + f"SELECT id, ts FROM {source.get_table_query_string()}" + ) + + # This produces: SELECT id, ts FROM (SELECT id, ts FROM raw_data) + # which is invalid in PostgreSQL (subquery needs alias) + # sqlglot is lenient and may parse it, but PostgreSQL would reject it + assert "AS" not in entity_sql_without_alias, ( + "get_table_query_string() should not add alias" + ) + + # Using the new method (with alias) produces valid SQL + entity_sql_with_alias = ( + f"SELECT id, ts FROM {source.get_table_query_string_with_alias()}" + ) + assert "AS subquery" in entity_sql_with_alias + + def test_complex_query_with_alias(self): + """Test get_table_query_string_with_alias() with complex nested query""" + complex_query = """ + SELECT u.id, u.name, o.total + FROM users u + JOIN orders o ON u.id = o.user_id + WHERE o.created_at > '2023-01-01' + """ + source = PostgreSQLSource( + name="test_source", + query=complex_query, + timestamp_field="created_at", + ) - # Should not fail on parameter validation - stderr_output = result.stderr.decode() - assert "must be provided" not in stderr_output + result = source.get_table_query_string_with_alias(alias="user_orders") + assert result.startswith("(") + assert result.endswith(") AS user_orders") + assert "SELECT u.id" in result diff --git a/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_bfv_compute_on_read.py b/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_bfv_compute_on_read.py new file mode 100644 index 00000000000..130583a13b1 --- /dev/null +++ b/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_bfv_compute_on_read.py @@ -0,0 +1,223 @@ +""" +Unit tests for BFV compute-on-read in SparkOfflineStore.get_historical_features(). + +Verifies that BatchFeatureViews with a UDF have their transformation applied +during get_historical_features(), with the transformed DataFrame registered as +a temp view that replaces the raw table_subquery in the PIT join. +""" + +from dataclasses import replace +from unittest.mock import MagicMock + +import pytest + +from feast.batch_feature_view import BatchFeatureView +from feast.feature_view import FeatureView +from feast.infra.offline_stores import offline_utils +from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( + _apply_bfv_transformations, +) +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import ( + SparkSource, +) +from feast.transformation.base import Transformation + + +@pytest.fixture() +def spark_session(): + mock = MagicMock() + mock.sql.return_value = MagicMock(name="source_df") + return mock + + +@pytest.fixture() +def spark_source(): + source = MagicMock(spec=SparkSource) + source.get_table_query_string.return_value = "`raw_events`" + return source + + +@pytest.fixture() +def base_query_context(): + return offline_utils.FeatureViewQueryContext( + name="my_bfv", + ttl=3600, + entities=["user_id"], + features=["avg_rating"], + field_mapping={}, + timestamp_field="event_timestamp", + created_timestamp_column=None, + table_subquery="`raw_events`", + entity_selections=["user_id AS user_id"], + min_event_timestamp="2023-01-01T00:00:00", + max_event_timestamp="2024-01-01T00:00:00", + date_partition_column=None, + timestamp_field_type=None, + ) + + +def _make_bfv(name: str, spark_source, has_udf: bool = True): + """Create a mock BatchFeatureView with optional UDF.""" + fv = MagicMock(spec=BatchFeatureView) + fv.name = name + fv.projection = MagicMock() + fv.projection.name_to_use.return_value = name + fv.batch_source = spark_source + fv.source_views = [] + + if has_udf: + transformation = MagicMock(spec=Transformation) + transformed_df = MagicMock(name="transformed_df") + transformation.udf = MagicMock(return_value=transformed_df) + fv.feature_transformation = transformation + fv.udf = transformation.udf + else: + fv.feature_transformation = None + fv.udf = None + + return fv + + +def _make_plain_fv(name: str, spark_source): + """Create a mock plain FeatureView (not a BatchFeatureView).""" + fv = MagicMock(spec=FeatureView) + fv.name = name + fv.projection = MagicMock() + fv.projection.name_to_use.return_value = name + fv.batch_source = spark_source + fv.feature_transformation = None + fv.udf = None + return fv + + +class TestApplyBfvTransformations: + def test_bfv_with_udf_replaces_table_subquery( + self, spark_session, spark_source, base_query_context + ): + """BFV with a UDF should have its table_subquery replaced with a temp view.""" + bfv = _make_bfv("my_bfv", spark_source) + contexts = [base_query_context] + + result = _apply_bfv_transformations(spark_session, [bfv], contexts) + + assert len(result) == 1 + assert result[0].table_subquery != "`raw_events`" + assert result[0].table_subquery.startswith("feast_bfv_") + + def test_bfv_udf_is_invoked_with_source_df( + self, spark_session, spark_source, base_query_context + ): + """The UDF should be called with the DataFrame read from the raw source.""" + bfv = _make_bfv("my_bfv", spark_source) + contexts = [base_query_context] + + _apply_bfv_transformations(spark_session, [bfv], contexts) + + sql_arg = spark_session.sql.call_args[0][0] + assert "SELECT * FROM `raw_events`" in sql_arg + assert "WHERE" in sql_arg + source_df = spark_session.sql.return_value + bfv.feature_transformation.udf.assert_called_once_with(source_df) + + def test_transformed_df_registered_as_temp_view( + self, spark_session, spark_source, base_query_context + ): + """The transformed DataFrame should be registered as a temp view.""" + bfv = _make_bfv("my_bfv", spark_source) + transformed_df = bfv.feature_transformation.udf.return_value + contexts = [base_query_context] + + result = _apply_bfv_transformations(spark_session, [bfv], contexts) + + transformed_df.createOrReplaceTempView.assert_called_once() + view_name = transformed_df.createOrReplaceTempView.call_args[0][0] + assert view_name == result[0].table_subquery + + def test_plain_feature_view_unchanged( + self, spark_session, spark_source, base_query_context + ): + """Plain FeatureViews (not BFV) should pass through without modification.""" + fv = _make_plain_fv("my_bfv", spark_source) + contexts = [base_query_context] + + result = _apply_bfv_transformations(spark_session, [fv], contexts) + + assert result[0].table_subquery == "`raw_events`" + spark_session.sql.assert_not_called() + + def test_bfv_without_udf_unchanged( + self, spark_session, spark_source, base_query_context + ): + """BFV without a UDF should pass through without modification.""" + bfv = _make_bfv("my_bfv", spark_source, has_udf=False) + contexts = [base_query_context] + + result = _apply_bfv_transformations(spark_session, [bfv], contexts) + + assert result[0].table_subquery == "`raw_events`" + spark_session.sql.assert_not_called() + + def test_mixed_views_only_transforms_bfvs( + self, spark_session, spark_source, base_query_context + ): + """With mixed BFV and plain FVs, only BFVs with UDFs get transformed.""" + bfv = _make_bfv("my_bfv", spark_source) + plain_fv = _make_plain_fv("plain_fv", spark_source) + + ctx_bfv = base_query_context + ctx_plain = replace( + base_query_context, + name="plain_fv", + features=["some_feature"], + ) + + result = _apply_bfv_transformations( + spark_session, [bfv, plain_fv], [ctx_bfv, ctx_plain] + ) + + assert result[0].table_subquery.startswith("feast_bfv_") + assert result[1].table_subquery == "`raw_events`" + + def test_time_range_filter_applied( + self, spark_session, spark_source, base_query_context + ): + """Source query should include time bounds from the context.""" + bfv = _make_bfv("my_bfv", spark_source) + contexts = [base_query_context] + + _apply_bfv_transformations(spark_session, [bfv], contexts) + + sql_arg = spark_session.sql.call_args[0][0] + assert "2023-01-01" in sql_arg + assert "2024-01-01" in sql_arg + assert "event_timestamp" in sql_arg + + def test_time_range_filter_with_none_min_timestamp( + self, spark_session, spark_source, base_query_context + ): + """When min_event_timestamp is None (no TTL), query should still work.""" + bfv = _make_bfv("my_bfv", spark_source) + ctx = replace(base_query_context, min_event_timestamp=None) + + result = _apply_bfv_transformations(spark_session, [bfv], [ctx]) + + assert result[0].table_subquery.startswith("feast_bfv_") + sql_arg = spark_session.sql.call_args[0][0] + assert "2024-01-01" in sql_arg + + def test_other_context_fields_preserved( + self, spark_session, spark_source, base_query_context + ): + """All fields besides table_subquery should remain unchanged.""" + bfv = _make_bfv("my_bfv", spark_source) + contexts = [base_query_context] + + result = _apply_bfv_transformations(spark_session, [bfv], contexts) + + assert result[0].name == base_query_context.name + assert result[0].ttl == base_query_context.ttl + assert result[0].entities == base_query_context.entities + assert result[0].features == base_query_context.features + assert result[0].timestamp_field == base_query_context.timestamp_field + assert result[0].min_event_timestamp == base_query_context.min_event_timestamp + assert result[0].max_event_timestamp == base_query_context.max_event_timestamp diff --git a/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_offline_store_pull_all.py b/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_offline_store_pull_all.py new file mode 100644 index 00000000000..c29b89db001 --- /dev/null +++ b/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_offline_store_pull_all.py @@ -0,0 +1,172 @@ +""" +Unit tests for SparkOfflineStore.pull_all_from_table_or_query SQL generation. + +Covers the bug where feature_name_columns=[] (signalling "read all source +columns" for BatchFeatureView UDF transformations) caused a bare + SELECT user_id, event_timestamp FROM source +instead of SELECT *, silently dropping all columns the UDF needs. +""" + +from datetime import datetime, timezone +from unittest.mock import MagicMock, patch + +import pytest + +from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( # noqa: E402 + SparkOfflineStore, + SparkOfflineStoreConfig, +) +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import ( # noqa: E402 + SparkSource, +) +from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig # noqa: E402 +from feast.repo_config import RepoConfig # noqa: E402 + +# --------------------------------------------------------------------------- +# Shared fixtures +# --------------------------------------------------------------------------- + +START = datetime(2023, 1, 1, tzinfo=timezone.utc) +END = datetime(2024, 1, 1, tzinfo=timezone.utc) + +# Fixed table name returned by the mocked get_table_query_string +_TABLE_EXPR = "`raw_reviews`" + + +@pytest.fixture() +def repo_config(): + return RepoConfig( + registry="file:///tmp/registry.db", + project="test", + provider="local", + online_store=SqliteOnlineStoreConfig(type="sqlite"), + offline_store=SparkOfflineStoreConfig(type="spark"), + ) + + +@pytest.fixture() +def spark_source(): + return SparkSource( + name="raw_reviews", + path="s3a://bucket/processed/reviews/", + file_format="parquet", + timestamp_field="event_timestamp", + ) + + +def _run_pull_all(repo_config, spark_source, feature_name_columns): + """ + Call pull_all_from_table_or_query with a mocked SparkSession and mocked + data-source table resolution, then return the SQL query string. + + Two things are patched so no real Spark/S3 access occurs: + 1. get_spark_session_or_start_new_with_repoconfig → MagicMock session + 2. spark_source.get_table_query_string → fixed table expression + (avoids SparkSource.validate / _load_dataframe_from_path hitting S3) + """ + mock_spark = MagicMock() + + with ( + patch( + "feast.infra.offline_stores.contrib.spark_offline_store.spark" + ".get_spark_session_or_start_new_with_repoconfig", + return_value=mock_spark, + ), + patch.object( + spark_source, + "get_table_query_string", + return_value=_TABLE_EXPR, + ), + ): + job = SparkOfflineStore.pull_all_from_table_or_query( + config=repo_config, + data_source=spark_source, + join_key_columns=["user_id"], + feature_name_columns=feature_name_columns, + timestamp_field="event_timestamp", + created_timestamp_column=None, + start_date=START, + end_date=END, + ) + + return job.query.strip() + + +def test_pull_all_with_empty_feature_cols_generates_select_star( + repo_config, spark_source +): + """ + feature_name_columns=[] must produce SELECT * so UDF-based + BatchFeatureViews receive all raw source columns for aggregation. + """ + sql = _run_pull_all(repo_config, spark_source, feature_name_columns=[]) + + assert sql.startswith("SELECT *"), ( + "Expected 'SELECT *' when feature_name_columns=[], " + f"got: {sql[:120]!r}\n\n" + "BatchFeatureView UDFs need all raw source columns to compute " + "aggregations — projecting only join key + timestamp silently " + "drops rating, text, helpful_vote, etc." + ) + assert "user_id" not in sql.split("FROM")[0], ( + "SELECT * must not also explicitly list join key columns" + ) + + +def test_pull_all_with_feature_cols_generates_explicit_projection( + repo_config, spark_source +): + """ + When feature_name_columns is non-empty (normal FeatureView path), + the query must project only the requested columns — not SELECT *. + """ + sql = _run_pull_all( + repo_config, + spark_source, + feature_name_columns=["avg_rating", "review_count"], + ) + + assert "SELECT *" not in sql, ( + "Non-empty feature_name_columns must produce explicit SELECT projection, not SELECT *" + ) + assert "avg_rating" in sql + assert "review_count" in sql + assert "user_id" in sql + assert "event_timestamp" in sql + + +def test_pull_all_empty_feature_cols_upstream_regression(repo_config, spark_source): + """ + Regression guard: the upstream (unfixed) behaviour with feature_name_columns=[] + produced a query that only selected join key + timestamp, dropping all columns + the UDF needs. Verify the fixed code does NOT produce that broken query. + + Broken upstream SQL looked like: + SELECT user_id, event_timestamp FROM ... WHERE ... + """ + sql = _run_pull_all(repo_config, spark_source, feature_name_columns=[]) + + projection = sql.split("FROM")[0] + assert "user_id" not in projection, ( + "Upstream bug: query projected only 'user_id, event_timestamp', " + "silently dropping all columns needed by the BFV UDF. " + "Fixed query should use SELECT *." + ) + + +@pytest.mark.parametrize( + "feature_cols,expect_star", + [ + ([], True), + (["f1"], False), + (["f1", "f2", "f3"], False), + ], +) +def test_pull_all_select_star_only_when_feature_cols_empty( + repo_config, spark_source, feature_cols, expect_star +): + sql = _run_pull_all(repo_config, spark_source, feature_name_columns=feature_cols) + has_star = sql.strip().upper().startswith("SELECT *") + assert has_star == expect_star, ( + f"feature_cols={feature_cols!r}: expected SELECT *={expect_star}, got SQL: {sql[:100]!r}" + ) diff --git a/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_persist.py b/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_persist.py new file mode 100644 index 00000000000..adcb54a1cf7 --- /dev/null +++ b/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_persist.py @@ -0,0 +1,260 @@ +""" +Unit tests for SparkRetrievalJob.persist() and SavedDatasetSparkStorage.from_data_source(). + +Covers the fix for https://github.com/feast-dev/feast/issues/6261 where: +1. SavedDatasetStorage.from_data_source() did not support SparkSource +2. SavedDatasetSparkStorage lacked a from_data_source() method +3. SparkRetrievalJob.persist() only supported table-based storage, not path-based +""" + +from unittest.mock import MagicMock + +import pytest + +from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( + SparkOfflineStoreConfig, + SparkRetrievalJob, +) +from feast.infra.offline_stores.contrib.spark_offline_store.spark_source import ( + SavedDatasetSparkStorage, + SparkSource, +) +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.repo_config import RepoConfig +from feast.saved_dataset import SavedDatasetStorage +from feast.table_format import IcebergFormat + +# --------------------------------------------------------------------------- +# Shared fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture() +def repo_config(): + return RepoConfig( + registry="file:///tmp/registry.db", + project="test", + provider="local", + online_store=SqliteOnlineStoreConfig(type="sqlite"), + offline_store=SparkOfflineStoreConfig(type="spark"), + ) + + +@pytest.fixture() +def table_spark_source(): + return SparkSource( + name="my_table", + table="db.my_table", + timestamp_field="event_timestamp", + ) + + +@pytest.fixture() +def path_spark_source(): + return SparkSource( + name="my_path_source", + path="s3a://bucket/data/features/", + file_format="parquet", + timestamp_field="event_timestamp", + ) + + +def _make_spark_retrieval_job(repo_config, remote_warehouse=True): + """Build a SparkRetrievalJob with a mocked SparkSession.""" + mock_spark = MagicMock() + + if remote_warehouse: + mock_spark.conf.get.side_effect = lambda key: { + "hive.metastore.uris": "thrift://metastore:9083", + }.get(key, None) + else: + + def _local_conf_get(key): + if key == "hive.metastore.uris": + raise Exception("not set") + if key == "spark.sql.warehouse.dir": + return "file:///tmp/spark-warehouse" + return None + + mock_spark.conf.get.side_effect = _local_conf_get + + return SparkRetrievalJob( + spark_session=mock_spark, + query="SELECT 1", + full_feature_names=False, + config=repo_config, + ) + + +# --------------------------------------------------------------------------- +# Group 1: SavedDatasetSparkStorage.from_data_source() +# --------------------------------------------------------------------------- + + +class TestSavedDatasetSparkStorageFromDataSource: + def test_from_data_source_with_table_source(self, table_spark_source): + storage = SavedDatasetSparkStorage.from_data_source(table_spark_source) + + assert isinstance(storage, SavedDatasetSparkStorage) + assert storage.spark_options.table == "db.my_table" + assert storage.spark_options.query is None + assert storage.spark_options.path is None + + def test_from_data_source_with_path_source(self, path_spark_source): + storage = SavedDatasetSparkStorage.from_data_source(path_spark_source) + + assert isinstance(storage, SavedDatasetSparkStorage) + assert storage.spark_options.path == "s3a://bucket/data/features/" + assert storage.spark_options.file_format == "parquet" + assert storage.spark_options.table is None + assert storage.spark_options.query is None + + def test_from_data_source_rejects_non_spark_source(self): + file_source = FileSource( + path="/tmp/data.parquet", + timestamp_field="event_timestamp", + ) + with pytest.raises(AssertionError): + SavedDatasetSparkStorage.from_data_source(file_source) + + +# --------------------------------------------------------------------------- +# Group 2: SavedDatasetStorage.from_data_source() dispatch +# --------------------------------------------------------------------------- + + +class TestSavedDatasetStorageDispatch: + def test_from_data_source_resolves_spark(self, table_spark_source): + storage = SavedDatasetStorage.from_data_source(table_spark_source) + + assert isinstance(storage, SavedDatasetSparkStorage) + assert storage.spark_options.table == "db.my_table" + + def test_from_data_source_resolves_path_spark(self, path_spark_source): + storage = SavedDatasetStorage.from_data_source(path_spark_source) + + assert isinstance(storage, SavedDatasetSparkStorage) + assert storage.spark_options.path == "s3a://bucket/data/features/" + assert storage.spark_options.file_format == "parquet" + + def test_roundtrip_table_source(self, table_spark_source): + storage = SavedDatasetStorage.from_data_source(table_spark_source) + roundtripped = storage.to_data_source() + + assert isinstance(roundtripped, SparkSource) + assert roundtripped.table == table_spark_source.table + assert roundtripped.query == table_spark_source.query + assert roundtripped.path == table_spark_source.path + + def test_roundtrip_path_source(self): + source = SparkSource( + name="my_path_source", + table="fallback_name", + timestamp_field="event_timestamp", + ) + storage = SavedDatasetStorage.from_data_source(source) + roundtripped = storage.to_data_source() + + assert isinstance(roundtripped, SparkSource) + assert roundtripped.table == source.table + + +# --------------------------------------------------------------------------- +# Group 3: SparkRetrievalJob.persist() +# --------------------------------------------------------------------------- + + +class TestSparkRetrievalJobPersist: + def test_persist_with_table_saves_as_table(self, repo_config): + job = _make_spark_retrieval_job(repo_config, remote_warehouse=True) + storage = SavedDatasetSparkStorage(table="output_table") + + job.persist(storage) + + mock_df = job.spark_session.sql.return_value + mock_df.write.saveAsTable.assert_called_once_with("output_table") + + def test_persist_with_table_and_format(self, repo_config): + job = _make_spark_retrieval_job(repo_config, remote_warehouse=True) + storage = SavedDatasetSparkStorage(table="output_table", file_format="parquet") + + job.persist(storage) + + mock_df = job.spark_session.sql.return_value + mock_df.write.format.assert_called_once_with("parquet") + mock_df.write.format.return_value.saveAsTable.assert_called_once_with( + "output_table" + ) + + def test_persist_with_path_writes_to_path(self, repo_config): + job = _make_spark_retrieval_job(repo_config, remote_warehouse=True) + storage = SavedDatasetSparkStorage( + path="s3a://bucket/output/", file_format="parquet" + ) + + job.persist(storage) + + mock_df = job.spark_session.sql.return_value + mock_df.write.format.assert_called_once_with("parquet") + mock_df.write.format.return_value.mode.assert_called_once_with("error") + mock_df.write.format.return_value.mode.return_value.save.assert_called_once_with( + "s3a://bucket/output/" + ) + + def test_persist_with_path_defaults_to_parquet(self, repo_config): + """When path is set with table_format but no file_format, persist defaults to parquet.""" + job = _make_spark_retrieval_job(repo_config, remote_warehouse=True) + storage = SavedDatasetSparkStorage( + path="s3a://bucket/output/", + file_format=None, + table_format=IcebergFormat(catalog="test_catalog"), + ) + + job.persist(storage) + + mock_df = job.spark_session.sql.return_value + mock_df.write.format.assert_called_once_with("parquet") + + def test_persist_with_path_allow_overwrite(self, repo_config): + job = _make_spark_retrieval_job(repo_config, remote_warehouse=True) + storage = SavedDatasetSparkStorage( + path="s3a://bucket/output/", file_format="parquet" + ) + + job.persist(storage, allow_overwrite=True) + + mock_df = job.spark_session.sql.return_value + mock_df.write.format.return_value.mode.assert_called_once_with("overwrite") + + def test_persist_with_path_custom_format(self, repo_config): + job = _make_spark_retrieval_job(repo_config, remote_warehouse=True) + storage = SavedDatasetSparkStorage( + path="s3a://bucket/output/", file_format="avro" + ) + + job.persist(storage) + + mock_df = job.spark_session.sql.return_value + mock_df.write.format.assert_called_once_with("avro") + mock_df.write.format.return_value.mode.return_value.save.assert_called_once_with( + "s3a://bucket/output/" + ) + + def test_persist_raises_without_table_or_path(self, repo_config): + job = _make_spark_retrieval_job(repo_config, remote_warehouse=True) + storage = SavedDatasetSparkStorage(query="SELECT * FROM t") + + with pytest.raises( + ValueError, match="either 'table' or 'path' must be specified" + ): + job.persist(storage) + + def test_persist_local_warehouse_creates_temp_view(self, repo_config): + job = _make_spark_retrieval_job(repo_config, remote_warehouse=False) + storage = SavedDatasetSparkStorage(table="output_table") + + job.persist(storage) + + mock_df = job.spark_session.sql.return_value + mock_df.createOrReplaceTempView.assert_called_once_with("output_table") diff --git a/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_staging_filesystem.py b/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_staging_filesystem.py new file mode 100644 index 00000000000..5b91646ef7c --- /dev/null +++ b/sdk/python/tests/unit/infra/offline_stores/contrib/spark_offline_store/test_spark_staging_filesystem.py @@ -0,0 +1,134 @@ +""" +Unit tests for SparkRetrievalJob._resolve_staging_filesystem. + +Verifies that the correct PyArrow filesystem and prefix-stripped paths +are returned for S3, S3A, GCS, file://, and plain local paths. +""" + +from unittest.mock import MagicMock, patch + +import pytest + +from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( + SparkRetrievalJob, +) + + +@pytest.fixture() +def retrieval_job(): + """Minimal SparkRetrievalJob with a mock config that has no offline_store region.""" + job = object.__new__(SparkRetrievalJob) + config = MagicMock() + config.offline_store.region = None + job._config = config + return job + + +class TestResolveS3Filesystem: + def test_s3_scheme_returns_s3_filesystem(self, retrieval_job): + with patch("pyarrow.fs.S3FileSystem") as mock_s3: + mock_s3.return_value = MagicMock(name="s3fs") + fs, paths = retrieval_job._resolve_staging_filesystem( + ["s3://my-bucket/path/a.parquet", "s3://my-bucket/path/b.parquet"] + ) + mock_s3.assert_called_once() + assert fs is mock_s3.return_value + assert paths == ["my-bucket/path/a.parquet", "my-bucket/path/b.parquet"] + + def test_s3a_scheme_strips_prefix(self, retrieval_job): + with patch("pyarrow.fs.S3FileSystem") as mock_s3: + mock_s3.return_value = MagicMock(name="s3fs") + fs, paths = retrieval_job._resolve_staging_filesystem( + ["s3a://bucket/dir/file.parquet"] + ) + assert paths == ["bucket/dir/file.parquet"] + + def test_s3_with_minio_endpoint(self, retrieval_job, monkeypatch): + monkeypatch.setenv("AWS_ENDPOINT_URL_S3", "http://minio.local:9000") + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") + with patch("pyarrow.fs.S3FileSystem") as mock_s3: + mock_s3.return_value = MagicMock(name="s3fs") + retrieval_job._resolve_staging_filesystem(["s3://bucket/file.parquet"]) + call_kwargs = mock_s3.call_args[1] + assert call_kwargs["endpoint_override"] == "minio.local:9000" + assert call_kwargs["scheme"] == "http" + + def test_s3_with_https_endpoint(self, retrieval_job, monkeypatch): + monkeypatch.setenv("AWS_ENDPOINT_URL_S3", "https://s3.custom.corp") + with patch("pyarrow.fs.S3FileSystem") as mock_s3: + mock_s3.return_value = MagicMock(name="s3fs") + retrieval_job._resolve_staging_filesystem(["s3://bucket/file.parquet"]) + call_kwargs = mock_s3.call_args[1] + assert call_kwargs["endpoint_override"] == "s3.custom.corp" + assert call_kwargs["scheme"] == "https" + + def test_s3_falls_back_to_aws_s3_endpoint_env(self, retrieval_job, monkeypatch): + monkeypatch.delenv("AWS_ENDPOINT_URL_S3", raising=False) + monkeypatch.setenv("AWS_S3_ENDPOINT", "http://legacy-minio:9000") + with patch("pyarrow.fs.S3FileSystem") as mock_s3: + mock_s3.return_value = MagicMock(name="s3fs") + retrieval_job._resolve_staging_filesystem(["s3://bucket/file.parquet"]) + call_kwargs = mock_s3.call_args[1] + assert "endpoint_override" in call_kwargs + + def test_s3_no_endpoint_no_override(self, retrieval_job, monkeypatch): + monkeypatch.delenv("AWS_ENDPOINT_URL_S3", raising=False) + monkeypatch.delenv("AWS_S3_ENDPOINT", raising=False) + with patch("pyarrow.fs.S3FileSystem") as mock_s3: + mock_s3.return_value = MagicMock(name="s3fs") + retrieval_job._resolve_staging_filesystem(["s3://bucket/file.parquet"]) + call_kwargs = mock_s3.call_args[1] + assert "endpoint_override" not in call_kwargs + assert "scheme" not in call_kwargs + + def test_s3_region_from_offline_store_config(self, retrieval_job): + retrieval_job._config.offline_store.region = "eu-west-1" + with patch("pyarrow.fs.S3FileSystem") as mock_s3: + mock_s3.return_value = MagicMock(name="s3fs") + retrieval_job._resolve_staging_filesystem(["s3://bucket/file.parquet"]) + call_kwargs = mock_s3.call_args[1] + assert call_kwargs["region"] == "eu-west-1" + + def test_s3_region_fallback_to_env(self, retrieval_job, monkeypatch): + retrieval_job._config.offline_store.region = None + monkeypatch.setenv("AWS_DEFAULT_REGION", "ap-southeast-1") + with patch("pyarrow.fs.S3FileSystem") as mock_s3: + mock_s3.return_value = MagicMock(name="s3fs") + retrieval_job._resolve_staging_filesystem(["s3://bucket/file.parquet"]) + call_kwargs = mock_s3.call_args[1] + assert call_kwargs["region"] == "ap-southeast-1" + + +class TestResolveGCSFilesystem: + def test_gs_scheme_returns_gcs_filesystem(self, retrieval_job): + with patch("pyarrow.fs.GcsFileSystem") as mock_gcs: + mock_gcs.return_value = MagicMock(name="gcsfs") + fs, paths = retrieval_job._resolve_staging_filesystem( + ["gs://my-bucket/path/a.parquet", "gs://my-bucket/path/b.parquet"] + ) + mock_gcs.assert_called_once() + assert fs is mock_gcs.return_value + assert paths == ["my-bucket/path/a.parquet", "my-bucket/path/b.parquet"] + + +class TestResolveLocalFilesystem: + def test_file_scheme_stripped(self, retrieval_job): + fs, paths = retrieval_job._resolve_staging_filesystem( + ["file:///tmp/staging/a.parquet"] + ) + assert fs is None + assert paths == ["/tmp/staging/a.parquet"] + + def test_plain_local_path_unchanged(self, retrieval_job): + fs, paths = retrieval_job._resolve_staging_filesystem( + ["/tmp/staging/a.parquet", "/tmp/staging/b.parquet"] + ) + assert fs is None + assert paths == ["/tmp/staging/a.parquet", "/tmp/staging/b.parquet"] + + def test_mixed_file_and_plain_paths(self, retrieval_job): + fs, paths = retrieval_job._resolve_staging_filesystem( + ["file:///tmp/a.parquet", "/tmp/b.parquet"] + ) + assert fs is None + assert paths == ["/tmp/a.parquet", "/tmp/b.parquet"] diff --git a/sdk/python/tests/unit/infra/offline_stores/test_bigquery.py b/sdk/python/tests/unit/infra/offline_stores/test_bigquery.py index 662be20b316..132339e1034 100644 --- a/sdk/python/tests/unit/infra/offline_stores/test_bigquery.py +++ b/sdk/python/tests/unit/infra/offline_stores/test_bigquery.py @@ -1,3 +1,4 @@ +from datetime import datetime, timezone from unittest.mock import Mock, patch import pandas as pd @@ -5,9 +6,11 @@ import pytest from feast.infra.offline_stores.bigquery import ( + BigQueryOfflineStore, BigQueryOfflineStoreConfig, BigQueryRetrievalJob, ) +from feast.infra.offline_stores.bigquery_source import BigQuerySource from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig from feast.repo_config import RepoConfig @@ -82,3 +85,290 @@ def test_to_arrow_timeout(self, big_query_result): self.retrieval_job._execute_query.assert_called_once_with( query=self.query, timeout=30 ) + + +@patch("feast.infra.offline_stores.bigquery._get_bigquery_client") +def test_pull_latest_from_table_or_query_partition_pruning(mock_get_bigquery_client): + mock_get_bigquery_client.return_value = Mock() + test_repo_config = RepoConfig( + registry="gs://ml-test/repo/registry.db", + project="test", + provider="gcp", + online_store=SqliteOnlineStoreConfig(type="sqlite"), + offline_store=BigQueryOfflineStoreConfig(type="bigquery", dataset="feast"), + ) + test_data_source = BigQuerySource( + table="project:dataset.table", + timestamp_field="event_timestamp", + date_partition_column="partition_date", + ) + retrieval_job = BigQueryOfflineStore.pull_latest_from_table_or_query( + config=test_repo_config, + data_source=test_data_source, + join_key_columns=["driver_id"], + feature_name_columns=["feature1"], + timestamp_field="event_timestamp", + created_timestamp_column=None, + start_date=datetime(2021, 1, 1, tzinfo=timezone.utc), + end_date=datetime(2021, 1, 2, tzinfo=timezone.utc), + ) + actual_query = retrieval_job.to_sql() + assert ( + "event_timestamp BETWEEN TIMESTAMP('2021-01-01T00:00:00+00:00') AND TIMESTAMP('2021-01-02T00:00:00+00:00')" + in actual_query + ) + assert "partition_date >= '2021-01-01'" in actual_query + assert "partition_date <= '2021-01-02'" in actual_query + + +@patch("feast.infra.offline_stores.bigquery._get_bigquery_client") +def test_pull_all_from_table_or_query_partition_pruning(mock_get_bigquery_client): + mock_get_bigquery_client.return_value = Mock() + test_repo_config = RepoConfig( + registry="gs://ml-test/repo/registry.db", + project="test", + provider="gcp", + online_store=SqliteOnlineStoreConfig(type="sqlite"), + offline_store=BigQueryOfflineStoreConfig(type="bigquery", dataset="feast"), + ) + test_data_source = BigQuerySource( + table="project:dataset.table", + timestamp_field="event_timestamp", + date_partition_column="partition_date", + ) + retrieval_job = BigQueryOfflineStore.pull_all_from_table_or_query( + config=test_repo_config, + data_source=test_data_source, + join_key_columns=["driver_id"], + feature_name_columns=["feature1"], + timestamp_field="event_timestamp", + start_date=datetime(2021, 1, 1, tzinfo=timezone.utc), + end_date=datetime(2021, 1, 2, tzinfo=timezone.utc), + ) + actual_query = retrieval_job.to_sql() + assert ( + "event_timestamp BETWEEN TIMESTAMP('2021-01-01T00:00:00+00:00') AND TIMESTAMP('2021-01-02T00:00:00+00:00')" + in actual_query + ) + assert "partition_date >= '2021-01-01'" in actual_query + assert "partition_date <= '2021-01-02'" in actual_query + + +@patch("feast.infra.offline_stores.bigquery._get_bigquery_client") +def test_pull_latest_date_type_timestamp_field(mock_get_bigquery_client): + mock_get_bigquery_client.return_value = Mock() + test_repo_config = RepoConfig( + registry="gs://ml-test/repo/registry.db", + project="test", + provider="gcp", + online_store=SqliteOnlineStoreConfig(type="sqlite"), + offline_store=BigQueryOfflineStoreConfig(type="bigquery", dataset="feast"), + ) + test_data_source = BigQuerySource( + table="project:dataset.table", + timestamp_field="event_date", + timestamp_field_type="DATE", + ) + retrieval_job = BigQueryOfflineStore.pull_latest_from_table_or_query( + config=test_repo_config, + data_source=test_data_source, + join_key_columns=["driver_id"], + feature_name_columns=["feature1"], + timestamp_field="event_date", + created_timestamp_column=None, + start_date=datetime(2021, 1, 1, tzinfo=timezone.utc), + end_date=datetime(2021, 1, 2, tzinfo=timezone.utc), + ) + actual_query = retrieval_job.to_sql() + assert ( + "event_date BETWEEN DATE('2021-01-01') AND DATE('2021-01-02')" in actual_query + ) + assert "TIMESTAMP(" not in actual_query + + +@patch("feast.infra.offline_stores.bigquery._get_bigquery_client") +def test_pull_all_date_type_timestamp_field(mock_get_bigquery_client): + mock_get_bigquery_client.return_value = Mock() + test_repo_config = RepoConfig( + registry="gs://ml-test/repo/registry.db", + project="test", + provider="gcp", + online_store=SqliteOnlineStoreConfig(type="sqlite"), + offline_store=BigQueryOfflineStoreConfig(type="bigquery", dataset="feast"), + ) + test_data_source = BigQuerySource( + table="project:dataset.table", + timestamp_field="event_date", + timestamp_field_type="DATE", + ) + retrieval_job = BigQueryOfflineStore.pull_all_from_table_or_query( + config=test_repo_config, + data_source=test_data_source, + join_key_columns=["driver_id"], + feature_name_columns=["feature1"], + timestamp_field="event_date", + start_date=datetime(2021, 1, 1, tzinfo=timezone.utc), + end_date=datetime(2021, 1, 2, tzinfo=timezone.utc), + ) + actual_query = retrieval_job.to_sql() + assert ( + "event_date BETWEEN DATE('2021-01-01') AND DATE('2021-01-02')" in actual_query + ) + assert "TIMESTAMP(" not in actual_query + + +@patch("feast.infra.offline_stores.bigquery._get_bigquery_client") +def test_pull_latest_date_type_with_partition_column(mock_get_bigquery_client): + mock_get_bigquery_client.return_value = Mock() + test_repo_config = RepoConfig( + registry="gs://ml-test/repo/registry.db", + project="test", + provider="gcp", + online_store=SqliteOnlineStoreConfig(type="sqlite"), + offline_store=BigQueryOfflineStoreConfig(type="bigquery", dataset="feast"), + ) + test_data_source = BigQuerySource( + table="project:dataset.table", + timestamp_field="event_date", + timestamp_field_type="DATE", + date_partition_column="_PARTITIONDATE", + ) + retrieval_job = BigQueryOfflineStore.pull_latest_from_table_or_query( + config=test_repo_config, + data_source=test_data_source, + join_key_columns=["driver_id"], + feature_name_columns=["feature1"], + timestamp_field="event_date", + created_timestamp_column=None, + start_date=datetime(2021, 1, 1, tzinfo=timezone.utc), + end_date=datetime(2021, 1, 2, tzinfo=timezone.utc), + ) + actual_query = retrieval_job.to_sql() + assert "DATE('2021-01-01')" in actual_query + assert "DATE('2021-01-02')" in actual_query + assert "_PARTITIONDATE >= '2021-01-01'" in actual_query + assert "_PARTITIONDATE <= '2021-01-02'" in actual_query + + +def test_bigquery_source_date_type_proto_roundtrip(): + source = BigQuerySource( + table="project:dataset.table", + timestamp_field="event_date", + timestamp_field_type="DATE", + date_partition_column="_PARTITIONDATE", + ) + proto = source.to_proto() + restored = BigQuerySource.from_proto(proto) + assert restored.timestamp_field_type == "DATE" + assert restored.date_partition_column == "_PARTITIONDATE" + assert restored.timestamp_field == "event_date" + assert source == restored + + +class TestBigQuerySourceGetTableQueryString: + def test_table_only(self): + source = BigQuerySource( + name="test", + table="project.dataset.table", + timestamp_field="ts", + ) + assert source.get_table_query_string() == "`project.dataset.table`" + + def test_query_only(self): + source = BigQuerySource( + name="test", + query="SELECT * FROM `project.dataset.table` WHERE active = TRUE", + timestamp_field="ts", + ) + assert ( + source.get_table_query_string() + == "(SELECT * FROM `project.dataset.table` WHERE active = TRUE)" + ) + + def test_both_table_and_query_prefers_query(self): + """When both table and query are set, query takes priority for reads.""" + query = ( + "SELECT * FROM `project.dataset.table`" + " QUALIFY ROW_NUMBER() OVER (PARTITION BY entity_id, event_time) = 1" + ) + source = BigQuerySource( + name="test", + table="project.dataset.table", + query=query, + timestamp_field="ts", + ) + result = source.get_table_query_string() + assert result.startswith("(") + assert "QUALIFY" in result + assert result != "`project.dataset.table`" + + def test_table_property_unaffected_by_query_priority(self): + """The .table property is still accessible for write paths.""" + source = BigQuerySource( + name="test", + table="project.dataset.write_target", + query="SELECT * FROM `project.dataset.write_target` WHERE deduped", + timestamp_field="ts", + ) + assert source.table == "project.dataset.write_target" + + +class TestOfflineWriteBatch: + @patch("feast.infra.offline_stores.bigquery._get_bigquery_client") + def test_offline_write_batch_enables_list_inference(self, mock_get_client): + """LoadJobConfig must set parquet_options.enable_list_inference = True + so that BigQuery correctly interprets PyArrow list columns from parquet. + """ + from unittest.mock import MagicMock + + source = BigQuerySource( + name="test", + table="project.dataset.table", + timestamp_field="ts", + ) + fv = MagicMock() + fv.batch_source = source + + pa_schema = pyarrow.schema( + [ + pyarrow.field("entity_id", pyarrow.string()), + pyarrow.field("tags", pyarrow.list_(pyarrow.string())), + pyarrow.field("ts", pyarrow.timestamp("us", tz="UTC")), + ] + ) + pa_table = pyarrow.table( + { + "entity_id": ["e1"], + "tags": [["a", "b"]], + "ts": [datetime(2024, 1, 1, tzinfo=timezone.utc)], + }, + schema=pa_schema, + ) + + mock_client = MagicMock() + mock_get_client.return_value = mock_client + mock_client.load_table_from_file.return_value = MagicMock() + + config = RepoConfig( + registry="gs://test/registry.db", + project="test", + provider="gcp", + offline_store=BigQueryOfflineStoreConfig(project_id="test-project"), + online_store=SqliteOnlineStoreConfig(), + ) + + with patch( + "feast.infra.offline_stores.offline_utils.get_pyarrow_schema_from_batch_source", + return_value=(pa_schema, pa_table.column_names), + ): + BigQueryOfflineStore.offline_write_batch( + config=config, + feature_view=fv, + table=pa_table, + progress=None, + ) + + call_kwargs = mock_client.load_table_from_file.call_args + job_config = call_kwargs[1]["job_config"] + assert job_config.parquet_options is not None + assert job_config.parquet_options.enable_list_inference is True diff --git a/sdk/python/tests/unit/infra/offline_stores/test_bigquery_non_entity_mode.py b/sdk/python/tests/unit/infra/offline_stores/test_bigquery_non_entity_mode.py new file mode 100644 index 00000000000..6bad310041b --- /dev/null +++ b/sdk/python/tests/unit/infra/offline_stores/test_bigquery_non_entity_mode.py @@ -0,0 +1,758 @@ +""" +Unit tests for BigQuery offline store non-entity mode (entity_df=None). + +Covers: +- _gather_all_entities helper +- _bq_create_entity_union_table SQL generation +- get_historical_features non-entity mode flow +- Regression: normal entity_df mode still works +""" + +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock, patch + +import pytest + +from feast.infra.offline_stores.bigquery import ( + BigQueryOfflineStore, + BigQueryOfflineStoreConfig, + BigQueryRetrievalJob, + _bq_create_entity_union_table, +) +from feast.infra.offline_stores.bigquery_source import BigQuerySource +from feast.infra.offline_stores.offline_utils import ( + FeatureViewQueryContext, + gather_all_entities, +) +from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.repo_config import RepoConfig + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +START = datetime(2023, 1, 1, tzinfo=timezone.utc) +END = datetime(2024, 1, 1, tzinfo=timezone.utc) +TABLE_NAME = "project.dataset.feast_tmp_abc123" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_repo_config(project_id: str = "my-project") -> RepoConfig: + return RepoConfig( + registry="gs://test/registry.db", + project="feast_test", + provider="gcp", + online_store=SqliteOnlineStoreConfig(type="sqlite"), + offline_store=BigQueryOfflineStoreConfig( + type="bigquery", + project_id=project_id, + dataset="feast", + ), + ) + + +def _make_fv_context( + name: str, + entities: list, + timestamp_field: str = "event_timestamp", + table_subquery: str = "`project.dataset.table`", +) -> FeatureViewQueryContext: + return FeatureViewQueryContext( + name=name, + ttl=86400, + entities=entities, + features=["feature_a"], + field_mapping={}, + timestamp_field=timestamp_field, + created_timestamp_column=None, + table_subquery=table_subquery, + entity_selections=[f"{e} AS {e}" for e in entities], + min_event_timestamp="2023-01-01T00:00:00", + max_event_timestamp="2024-01-01T00:00:00", + date_partition_column=None, + timestamp_field_type=None, + ) + + +def _make_bq_source(table: str = "project.dataset.table") -> BigQuerySource: + return BigQuerySource( + name="test_source", + table=table, + timestamp_field="event_timestamp", + ) + + +def _make_feature_view_mock( + name: str, entities: list, table: str = "project.dataset.table" +) -> MagicMock: + fv = MagicMock() + fv.name = name + fv.batch_source = _make_bq_source(table) + fv.entities = entities + return fv + + +# --------------------------------------------------------------------------- +# Tests: _gather_all_entities +# --------------------------------------------------------------------------- + + +class TestGatherAllEntities: + def test_single_feature_view(self): + ctx = _make_fv_context("fv1", ["customer_id", "item_id"]) + assert gather_all_entities([ctx]) == ["customer_id", "item_id"] + + def test_multiple_views_overlapping_entities(self): + ctx1 = _make_fv_context("fv1", ["customer_id", "item_id"]) + ctx2 = _make_fv_context("fv2", ["customer_id", "store_id"]) + result = gather_all_entities([ctx1, ctx2]) + # customer_id should appear only once; order is first-seen + assert result == ["customer_id", "item_id", "store_id"] + + def test_multiple_views_disjoint_entities(self): + ctx1 = _make_fv_context("fv1", ["driver_id"]) + ctx2 = _make_fv_context("fv2", ["customer_id"]) + result = gather_all_entities([ctx1, ctx2]) + assert result == ["driver_id", "customer_id"] + + def test_entityless_feature_view(self): + ctx = _make_fv_context("fv1", []) + assert gather_all_entities([ctx]) == [] + + def test_empty_list(self): + assert gather_all_entities([]) == [] + + def test_preserves_insertion_order(self): + ctx1 = _make_fv_context("fv1", ["z_entity", "a_entity"]) + ctx2 = _make_fv_context("fv2", ["a_entity", "m_entity"]) + result = gather_all_entities([ctx1, ctx2]) + assert result == ["z_entity", "a_entity", "m_entity"] + + +# --------------------------------------------------------------------------- +# Tests: _bq_create_entity_union_table — SQL generation +# --------------------------------------------------------------------------- + + +class TestBqCreateEntityUnionTable: + def _make_client(self) -> MagicMock: + client = MagicMock() + client.query.return_value = MagicMock() + client.get_table.return_value = MagicMock() + return client + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + def test_single_view_creates_table_with_correct_sql(self, mock_utc_now, mock_block): + mock_utc_now.return_value = END + client = self._make_client() + fv = _make_feature_view_mock("fv1", ["customer_id"]) + ctx = _make_fv_context( + "fv1", ["customer_id"], table_subquery="`project.dataset.orders`" + ) + + _bq_create_entity_union_table( + client=client, + table_name=TABLE_NAME, + feature_views=[fv], + fv_query_contexts=[ctx], + start_date=START, + end_date=END, + all_entities=["customer_id"], + event_timestamp_col="entity_ts", + ) + + sql = client.query.call_args[0][0] + assert f"CREATE TABLE `{TABLE_NAME}` AS" in sql + assert "SELECT DISTINCT `customer_id`" in sql + assert "UNION DISTINCT" not in sql # single view → no union + assert "entity_ts" in sql + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + def test_entity_column_not_cast_to_string(self, mock_utc_now, mock_block): + """INT64 entity columns must NOT be cast to STRING — that breaks the PIT join.""" + mock_utc_now.return_value = END + client = self._make_client() + fv = _make_feature_view_mock("fv1", ["user_id"]) + ctx = _make_fv_context("fv1", ["user_id"]) + + _bq_create_entity_union_table( + client=client, + table_name=TABLE_NAME, + feature_views=[fv], + fv_query_contexts=[ctx], + start_date=START, + end_date=END, + all_entities=["user_id"], + event_timestamp_col="entity_ts", + ) + + sql = client.query.call_args[0][0] + assert "CAST(`user_id` AS STRING)" not in sql + assert "`user_id`" in sql + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + def test_missing_entity_filled_with_null_not_cast_string( + self, mock_utc_now, mock_block + ): + """When a feature view doesn't have an entity column, fill it with NULL.""" + mock_utc_now.return_value = END + client = self._make_client() + fv1 = _make_feature_view_mock("fv1", ["customer_id", "item_id"]) + fv2 = _make_feature_view_mock("fv2", ["customer_id"]) + ctx1 = _make_fv_context("fv1", ["customer_id", "item_id"]) + ctx2 = _make_fv_context("fv2", ["customer_id"]) + + _bq_create_entity_union_table( + client=client, + table_name=TABLE_NAME, + feature_views=[fv1, fv2], + fv_query_contexts=[ctx1, ctx2], + start_date=START, + end_date=END, + all_entities=["customer_id", "item_id"], + event_timestamp_col="entity_ts", + ) + + sql = client.query.call_args[0][0] + # fv2 does not have item_id → must appear as NULL for item_id + assert "`item_id`" in sql + assert "NULL" in sql + # fv2's item_id fill must not appear as a regular column reference + assert "SELECT DISTINCT `customer_id`" in sql + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + def test_multiple_views_produce_union_distinct(self, mock_utc_now, mock_block): + mock_utc_now.return_value = END + client = self._make_client() + fv1 = _make_feature_view_mock("fv1", ["driver_id"]) + fv2 = _make_feature_view_mock("fv2", ["driver_id"]) + ctx1 = _make_fv_context("fv1", ["driver_id"]) + ctx2 = _make_fv_context("fv2", ["driver_id"]) + + _bq_create_entity_union_table( + client=client, + table_name=TABLE_NAME, + feature_views=[fv1, fv2], + fv_query_contexts=[ctx1, ctx2], + start_date=START, + end_date=END, + all_entities=["driver_id"], + event_timestamp_col="entity_ts", + ) + + sql = client.query.call_args[0][0] + assert "UNION DISTINCT" in sql + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + def test_timestamp_filter_uses_start_and_end(self, mock_utc_now, mock_block): + mock_utc_now.return_value = END + client = self._make_client() + fv = _make_feature_view_mock("fv1", ["entity_id"]) + ctx = _make_fv_context("fv1", ["entity_id"]) + + _bq_create_entity_union_table( + client=client, + table_name=TABLE_NAME, + feature_views=[fv], + fv_query_contexts=[ctx], + start_date=START, + end_date=END, + all_entities=["entity_id"], + event_timestamp_col="entity_ts", + ) + + sql = client.query.call_args[0][0] + assert "2023-01-01T00:00:00" in sql # start_date + assert ( + "2024-01-01T00:00:00" in sql + ) # end_date (appears twice: WHERE + entity_ts value) + assert "BETWEEN TIMESTAMP(" in sql + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + def test_entity_ts_column_set_to_end_date(self, mock_utc_now, mock_block): + """The as-of timestamp in the synthetic left table must equal end_date.""" + mock_utc_now.return_value = END + client = self._make_client() + fv = _make_feature_view_mock("fv1", ["entity_id"]) + ctx = _make_fv_context("fv1", ["entity_id"]) + + _bq_create_entity_union_table( + client=client, + table_name=TABLE_NAME, + feature_views=[fv], + fv_query_contexts=[ctx], + start_date=START, + end_date=END, + all_entities=["entity_id"], + event_timestamp_col="entity_ts", + ) + + sql = client.query.call_args[0][0] + assert "TIMESTAMP('2024-01-01T00:00:00') AS `entity_ts`" in sql + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + def test_sets_table_expiry(self, mock_utc_now, mock_block): + mock_utc_now.return_value = datetime(2024, 1, 1, tzinfo=timezone.utc) + client = self._make_client() + fv = _make_feature_view_mock("fv1", ["entity_id"]) + ctx = _make_fv_context("fv1", ["entity_id"]) + + _bq_create_entity_union_table( + client=client, + table_name=TABLE_NAME, + feature_views=[fv], + fv_query_contexts=[ctx], + start_date=START, + end_date=END, + all_entities=["entity_id"], + event_timestamp_col="entity_ts", + ) + + client.update_table.assert_called_once() + updated_table = client.update_table.call_args[0][0] + # expiry should be 30 minutes after _utc_now() + assert updated_table.expires == datetime( + 2024, 1, 1, tzinfo=timezone.utc + ) + timedelta(minutes=30) + + +# --------------------------------------------------------------------------- +# Tests: get_historical_features — non-entity mode flow +# --------------------------------------------------------------------------- + + +@pytest.fixture +def repo_config(): + return _make_repo_config() + + +@pytest.fixture +def mock_bq_client(): + client = MagicMock() + client.project = "my-project" + client.query.return_value = MagicMock( + state="DONE", exception=lambda timeout=None: None + ) + client.get_table.return_value = MagicMock() + return client + + +class TestGetHistoricalFeaturesNonEntityMode: + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + @patch("feast.infra.offline_stores.bigquery._bq_create_entity_union_table") + @patch("feast.infra.offline_stores.bigquery._upload_entity_df") + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.get_feature_view_query_context" + ) + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.build_point_in_time_query" + ) + @patch("feast.infra.offline_stores.bigquery._get_table_reference_for_new_entity") + @patch("feast.infra.offline_stores.bigquery._get_bigquery_client") + def test_non_entity_mode_calls_union_table_not_upload( + self, + mock_get_client, + mock_get_table_ref, + mock_build_pit, + mock_get_ctx, + mock_upload, + mock_union_table, + mock_utc_now, + mock_block, + repo_config, + mock_bq_client, + ): + mock_get_client.return_value = mock_bq_client + mock_get_table_ref.return_value = TABLE_NAME + mock_build_pit.return_value = "SELECT 1" + mock_utc_now.return_value = END + + fv = MagicMock() + fv.batch_source = _make_bq_source() + fv.ttl = timedelta(days=30) + + ctx = _make_fv_context("fv1", ["customer_id"]) + mock_get_ctx.return_value = [ctx] + + registry = MagicMock() + registry.list_entities.return_value = [] + + job = BigQueryOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["fv1:feature_a"], + entity_df=None, + registry=registry, + project="feast_test", + full_feature_names=False, + start_date=START, + end_date=END, + ) + + # Trigger query_generator + with job._query_generator(): + pass + + mock_union_table.assert_called_once() + mock_upload.assert_not_called() + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + @patch("feast.infra.offline_stores.bigquery._bq_create_entity_union_table") + @patch("feast.infra.offline_stores.bigquery._upload_entity_df") + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.get_feature_view_query_context" + ) + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.build_point_in_time_query" + ) + @patch("feast.infra.offline_stores.bigquery._get_table_reference_for_new_entity") + @patch("feast.infra.offline_stores.bigquery._get_bigquery_client") + def test_non_entity_mode_uses_entity_ts_as_timestamp_col( + self, + mock_get_client, + mock_get_table_ref, + mock_build_pit, + mock_get_ctx, + mock_upload, + mock_union_table, + mock_utc_now, + mock_block, + repo_config, + mock_bq_client, + ): + mock_get_client.return_value = mock_bq_client + mock_get_table_ref.return_value = TABLE_NAME + mock_build_pit.return_value = "SELECT 1" + mock_utc_now.return_value = END + + fv = MagicMock() + fv.batch_source = _make_bq_source() + fv.ttl = timedelta(days=30) + + ctx = _make_fv_context("fv1", ["customer_id"]) + mock_get_ctx.return_value = [ctx] + + registry = MagicMock() + + job = BigQueryOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["fv1:feature_a"], + entity_df=None, + registry=registry, + project="feast_test", + start_date=START, + end_date=END, + ) + + with job._query_generator(): + pass + + build_call_kwargs = mock_build_pit.call_args[1] + assert build_call_kwargs["entity_df_event_timestamp_col"] == "entity_ts" + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + @patch("feast.infra.offline_stores.bigquery._bq_create_entity_union_table") + @patch("feast.infra.offline_stores.bigquery._upload_entity_df") + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.get_feature_view_query_context" + ) + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.build_point_in_time_query" + ) + @patch("feast.infra.offline_stores.bigquery._get_table_reference_for_new_entity") + @patch("feast.infra.offline_stores.bigquery._get_bigquery_client") + def test_non_entity_mode_passes_start_and_end_to_union_table( + self, + mock_get_client, + mock_get_table_ref, + mock_build_pit, + mock_get_ctx, + mock_upload, + mock_union_table, + mock_utc_now, + mock_block, + repo_config, + mock_bq_client, + ): + mock_get_client.return_value = mock_bq_client + mock_get_table_ref.return_value = TABLE_NAME + mock_build_pit.return_value = "SELECT 1" + mock_utc_now.return_value = END + + fv = MagicMock() + fv.batch_source = _make_bq_source() + fv.ttl = timedelta(days=30) + + ctx = _make_fv_context("fv1", ["customer_id"]) + mock_get_ctx.return_value = [ctx] + + registry = MagicMock() + + job = BigQueryOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["fv1:feature_a"], + entity_df=None, + registry=registry, + project="feast_test", + start_date=START, + end_date=END, + ) + + with job._query_generator(): + pass + + call_kwargs = mock_union_table.call_args[1] + assert call_kwargs["start_date"].replace(tzinfo=timezone.utc) == START + assert call_kwargs["end_date"].replace(tzinfo=timezone.utc) == END + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + @patch("feast.infra.offline_stores.bigquery._bq_create_entity_union_table") + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.get_feature_view_query_context" + ) + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.build_point_in_time_query" + ) + @patch("feast.infra.offline_stores.bigquery._get_table_reference_for_new_entity") + @patch("feast.infra.offline_stores.bigquery._get_bigquery_client") + def test_non_entity_mode_returns_retrieval_job( + self, + mock_get_client, + mock_get_table_ref, + mock_build_pit, + mock_get_ctx, + mock_union_table, + mock_utc_now, + mock_block, + repo_config, + mock_bq_client, + ): + mock_get_client.return_value = mock_bq_client + mock_get_table_ref.return_value = TABLE_NAME + mock_build_pit.return_value = "SELECT 1" + mock_utc_now.return_value = END + + fv = MagicMock() + fv.batch_source = _make_bq_source() + fv.ttl = timedelta(days=30) + + ctx = _make_fv_context("fv1", ["customer_id"]) + mock_get_ctx.return_value = [ctx] + + registry = MagicMock() + + job = BigQueryOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["fv1:feature_a"], + entity_df=None, + registry=registry, + project="feast_test", + start_date=START, + end_date=END, + ) + + assert isinstance(job, BigQueryRetrievalJob) + + @patch("feast.infra.offline_stores.bigquery.block_until_done") + @patch("feast.infra.offline_stores.bigquery._utc_now") + @patch("feast.infra.offline_stores.bigquery._bq_create_entity_union_table") + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.get_feature_view_query_context" + ) + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.build_point_in_time_query" + ) + @patch("feast.infra.offline_stores.bigquery._get_table_reference_for_new_entity") + @patch("feast.infra.offline_stores.bigquery._get_bigquery_client") + def test_non_entity_mode_metadata_excludes_timestamp_col_from_keys( + self, + mock_get_client, + mock_get_table_ref, + mock_build_pit, + mock_get_ctx, + mock_union_table, + mock_utc_now, + mock_block, + repo_config, + mock_bq_client, + ): + mock_get_client.return_value = mock_bq_client + mock_get_table_ref.return_value = TABLE_NAME + mock_build_pit.return_value = "SELECT 1" + mock_utc_now.return_value = END + + fv = MagicMock() + fv.batch_source = _make_bq_source() + fv.ttl = timedelta(days=30) + + ctx = _make_fv_context("fv1", ["customer_id"]) + mock_get_ctx.return_value = [ctx] + + registry = MagicMock() + + job = BigQueryOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["fv1:feature_a"], + entity_df=None, + registry=registry, + project="feast_test", + start_date=START, + end_date=END, + ) + + # entity_ts (the as-of timestamp) must NOT appear in the returned keys + assert "entity_ts" not in job.metadata.keys + assert "customer_id" in job.metadata.keys + + +# --------------------------------------------------------------------------- +# Tests: get_historical_features — entity_df mode regression +# --------------------------------------------------------------------------- + + +class TestGetHistoricalFeaturesEntityDfMode: + @patch("feast.infra.offline_stores.bigquery._bq_create_entity_union_table") + @patch("feast.infra.offline_stores.bigquery._upload_entity_df") + @patch("feast.infra.offline_stores.bigquery._get_entity_df_event_timestamp_range") + @patch("feast.infra.offline_stores.bigquery._get_entity_schema") + @patch("feast.infra.offline_stores.bigquery.offline_utils.get_expected_join_keys") + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.assert_expected_columns_in_entity_df" + ) + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.get_feature_view_query_context" + ) + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.build_point_in_time_query" + ) + @patch("feast.infra.offline_stores.bigquery._get_table_reference_for_new_entity") + @patch("feast.infra.offline_stores.bigquery._get_bigquery_client") + def test_entity_df_mode_calls_upload_not_union_table( + self, + mock_get_client, + mock_get_table_ref, + mock_build_pit, + mock_get_ctx, + mock_assert_cols, + mock_join_keys, + mock_entity_schema, + mock_ts_range, + mock_upload, + mock_union_table, + ): + import pandas as pd + + mock_bq_client = MagicMock() + mock_bq_client.project = "my-project" + mock_get_client.return_value = mock_bq_client + mock_get_table_ref.return_value = TABLE_NAME + mock_build_pit.return_value = "SELECT 1" + mock_entity_schema.return_value = { + "customer_id": "int64", + "event_timestamp": "datetime64[ns, UTC]", + } + mock_ts_range.return_value = (START, END) + mock_get_ctx.return_value = [_make_fv_context("fv1", ["customer_id"])] + mock_join_keys.return_value = ["customer_id"] + + entity_df = pd.DataFrame( + {"customer_id": [1, 2], "event_timestamp": [START, END]} + ) + + repo_config = _make_repo_config() + fv = MagicMock() + fv.batch_source = _make_bq_source() + + job = BigQueryOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["fv1:feature_a"], + entity_df=entity_df, + registry=MagicMock(), + project="feast_test", + ) + + with job._query_generator(): + pass + + mock_upload.assert_called_once() + mock_union_table.assert_not_called() + + @patch("feast.infra.offline_stores.bigquery._bq_create_entity_union_table") + @patch("feast.infra.offline_stores.bigquery._upload_entity_df") + @patch("feast.infra.offline_stores.bigquery._get_entity_df_event_timestamp_range") + @patch("feast.infra.offline_stores.bigquery._get_entity_schema") + @patch("feast.infra.offline_stores.bigquery.offline_utils.get_expected_join_keys") + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.assert_expected_columns_in_entity_df" + ) + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.get_feature_view_query_context" + ) + @patch( + "feast.infra.offline_stores.bigquery.offline_utils.build_point_in_time_query" + ) + @patch("feast.infra.offline_stores.bigquery._get_table_reference_for_new_entity") + @patch("feast.infra.offline_stores.bigquery._get_bigquery_client") + def test_entity_df_sql_string_mode_works( + self, + mock_get_client, + mock_get_table_ref, + mock_build_pit, + mock_get_ctx, + mock_assert_cols, + mock_join_keys, + mock_entity_schema, + mock_ts_range, + mock_upload, + mock_union_table, + ): + mock_bq_client = MagicMock() + mock_bq_client.project = "my-project" + mock_get_client.return_value = mock_bq_client + mock_get_table_ref.return_value = TABLE_NAME + mock_build_pit.return_value = "SELECT 1" + mock_entity_schema.return_value = { + "customer_id": "int64", + "event_timestamp": "datetime64[ns, UTC]", + } + mock_ts_range.return_value = (START, END) + mock_get_ctx.return_value = [_make_fv_context("fv1", ["customer_id"])] + mock_join_keys.return_value = ["customer_id"] + + repo_config = _make_repo_config() + fv = MagicMock() + fv.batch_source = _make_bq_source() + + job = BigQueryOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["fv1:feature_a"], + entity_df="SELECT customer_id, event_timestamp FROM `project.dataset.entities`", + registry=MagicMock(), + project="feast_test", + ) + + with job._query_generator(): + pass + + mock_upload.assert_called_once() + mock_union_table.assert_not_called() diff --git a/sdk/python/tests/unit/infra/offline_stores/test_clickhouse.py b/sdk/python/tests/unit/infra/offline_stores/test_clickhouse.py new file mode 100644 index 00000000000..7789cde72b3 --- /dev/null +++ b/sdk/python/tests/unit/infra/offline_stores/test_clickhouse.py @@ -0,0 +1,242 @@ +import logging +import threading +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock, patch + +import pytest + +from feast.infra.utils.clickhouse.clickhouse_config import ClickhouseConfig +from feast.infra.utils.clickhouse.connection_utils import get_client, thread_local + +logger = logging.getLogger(__name__) + + +@pytest.fixture +def clickhouse_config(): + """Create a test ClickHouse configuration.""" + return ClickhouseConfig( + host="localhost", + port=9000, + user="default", + password="password", + database="test_db", + ) + + +@pytest.fixture(autouse=True) +def cleanup_thread_local(): + """Clean up thread_local storage after each test.""" + yield + if hasattr(thread_local, "clickhouse_client"): + delattr(thread_local, "clickhouse_client") + + +@patch("feast.infra.utils.clickhouse.connection_utils.clickhouse_connect.get_client") +def test_get_client_returns_different_objects_for_separate_threads( + mock_get_client, clickhouse_config +): + """ + Clickhouse client is thread-unsafe and crashes if shared between threads. + This test ensures that get_client returns different client instances for different threads, while + reusing the same instance within the same thread. + """ + + def create_mock_client(*args, **kwargs): + """Create a unique mock client for each call.""" + return MagicMock() + + mock_get_client.side_effect = create_mock_client + + results = {} + + def thread_1_work(): + """Thread 1 makes 2 calls to get_client.""" + client_1a = get_client(clickhouse_config) + client_1b = get_client(clickhouse_config) + results["thread_1"] = (client_1a, client_1b) + + def thread_2_work(): + """Thread 2 makes 1 call to get_client.""" + client_2 = get_client(clickhouse_config) + results["thread_2"] = client_2 + + thread_1 = threading.Thread(target=thread_1_work) + thread_2 = threading.Thread(target=thread_2_work) + + thread_1.start() + thread_2.start() + + thread_1.join() + thread_2.join() + + # Thread 1's two calls should return the same client (thread-local reuse) + client_1a, client_1b = results["thread_1"] + assert client_1a is client_1b, ( + "Same thread should get same client instance (cached)" + ) + + # Thread 2's client should be different from thread 1's client + client_2 = results["thread_2"] + assert client_1a is not client_2, ( + "Different threads should get different client instances (not cached)" + ) + + +def test_clickhouse_config_parses_additional_client_args(): + """ + Test that ClickhouseConfig correctly parses additional_client_args from a dict, + simulating how it would be parsed from YAML by Pydantic. + """ + # This simulates the dict that would come from yaml.safe_load() + raw_config = { + "host": "localhost", + "port": 8123, + "database": "default", + "user": "default", + "password": "password", + "additional_client_args": { + "send_receive_timeout": 60, + "compress": True, + "client_name": "feast_test", + }, + } + + # Pydantic should parse this dict into a ClickhouseConfig object + config = ClickhouseConfig(**raw_config) + + # Verify all fields are correctly parsed + assert config.host == "localhost" + assert config.port == 8123 + assert config.database == "default" + assert config.user == "default" + assert config.password == "password" + + # Verify additional_client_args is correctly parsed as a dict + assert config.additional_client_args is not None + assert isinstance(config.additional_client_args, dict) + assert config.additional_client_args["send_receive_timeout"] == 60 + assert config.additional_client_args["compress"] is True + assert config.additional_client_args["client_name"] == "feast_test" + + +def test_clickhouse_config_handles_none_additional_client_args(): + """ + Test that ClickhouseConfig correctly handles when additional_client_args is not provided. + """ + raw_config = { + "host": "localhost", + "port": 8123, + "database": "default", + "user": "default", + "password": "password", + } + + config = ClickhouseConfig(**raw_config) + + assert config.additional_client_args is None + + +class TestNonEntityRetrieval: + """Test the non-entity retrieval logic (entity_df=None) for ClickHouse.""" + + _MODULE = "feast.infra.offline_stores.contrib.clickhouse_offline_store.clickhouse" + + def _call_get_historical_features(self, feature_views, **kwargs): + """Call get_historical_features with entity_df=None, mocking the pipeline.""" + from feast.infra.offline_stores.contrib.clickhouse_offline_store.clickhouse import ( + ClickhouseOfflineStore, + ClickhouseOfflineStoreConfig, + ) + from feast.repo_config import RepoConfig + + config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=ClickhouseOfflineStoreConfig( + type="clickhouse", + host="localhost", + port=9000, + database="test_db", + user="default", + password="password", + ), + ) + + end = kwargs.get("end_date", datetime(2023, 1, 7, tzinfo=timezone.utc)) + + with ( + patch.multiple( + self._MODULE, + _upload_entity_df=MagicMock(), + _get_entity_schema=MagicMock( + return_value={"event_timestamp": "timestamp"} + ), + _get_entity_df_event_timestamp_range=MagicMock( + return_value=(end - timedelta(days=1), end) + ), + ), + patch( + f"{self._MODULE}.offline_utils.get_expected_join_keys", + return_value=[], + ), + patch( + f"{self._MODULE}.offline_utils.assert_expected_columns_in_entity_df", + ), + patch( + f"{self._MODULE}.offline_utils.get_feature_view_query_context", + return_value=[], + ), + ): + refs = [f"{fv.name}:feature1" for fv in feature_views] + return ClickhouseOfflineStore.get_historical_features( + config=config, + feature_views=feature_views, + feature_refs=refs, + entity_df=None, + registry=MagicMock(), + project="test_project", + **kwargs, + ) + + @staticmethod + def _make_feature_view(name, ttl=None): + from feast.entity import Entity + from feast.feature_view import FeatureView, Field + from feast.infra.offline_stores.contrib.clickhouse_offline_store.clickhouse_source import ( + ClickhouseSource, + ) + from feast.types import Float32 + + return FeatureView( + name=name, + entities=[Entity(name="driver_id", join_keys=["driver_id"])], + ttl=ttl, + source=ClickhouseSource( + name=f"{name}_source", + table=f"{name}_table", + timestamp_field="event_timestamp", + ), + schema=[ + Field(name="feature1", dtype=Float32), + ], + ) + + def test_non_entity_mode_with_end_date(self): + """entity_df=None with explicit end_date produces a valid RetrievalJob.""" + from feast.infra.offline_stores.offline_store import RetrievalJob + + fv = self._make_feature_view("test_fv") + job = self._call_get_historical_features( + [fv], + end_date=datetime(2023, 1, 7, tzinfo=timezone.utc), + ) + assert isinstance(job, RetrievalJob) + + def test_non_entity_mode_defaults_end_date(self): + """entity_df=None without end_date defaults to now.""" + from feast.infra.offline_stores.offline_store import RetrievalJob + + fv = self._make_feature_view("test_fv") + job = self._call_get_historical_features([fv]) + assert isinstance(job, RetrievalJob) diff --git a/sdk/python/tests/unit/infra/offline_stores/test_dask_non_entity.py b/sdk/python/tests/unit/infra/offline_stores/test_dask_non_entity.py new file mode 100644 index 00000000000..4f8ad322eee --- /dev/null +++ b/sdk/python/tests/unit/infra/offline_stores/test_dask_non_entity.py @@ -0,0 +1,555 @@ +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock, patch + +import dask.dataframe as dd +import pandas as pd + +from feast.entity import Entity +from feast.feature_view import FeatureView, Field +from feast.infra.offline_stores import dask as dask_mod +from feast.infra.offline_stores.dask import ( + DaskOfflineStore, + DaskOfflineStoreConfig, +) +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.offline_stores.offline_store import RetrievalJob +from feast.repo_config import RepoConfig +from feast.types import Float32, Int64, ValueType + + +def _mock_dask_offline_store_config(): + return DaskOfflineStoreConfig(type="dask") + + +def _mock_entity(): + return [ + Entity( + name="driver_id", + join_keys=["driver_id"], + description="Driver ID", + value_type=ValueType.INT64, + ) + ] + + +def _mock_feature_view(name: str = "driver_stats", ttl: timedelta = None): + """Helper to create mock feature views with configurable TTL""" + return FeatureView( + name=name, + entities=_mock_entity(), + schema=[ + Field(name="conv_rate", dtype=Float32), + ], + source=FileSource( + path="dummy.parquet", # not read in this test + timestamp_field="event_timestamp", + ), + ttl=ttl, + ) + + +def test_dask_non_entity_historical_retrieval_accepts_dates(): + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=_mock_dask_offline_store_config(), + ) + + fv = _mock_feature_view() + + retrieval_job = DaskOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["driver_stats:conv_rate"], + entity_df=None, # start/end-only mode + registry=MagicMock(), + project="test_project", + full_feature_names=False, + start_date=datetime(2023, 1, 1, tzinfo=timezone.utc), + end_date=datetime(2023, 1, 2, tzinfo=timezone.utc), + ) + + # Should return a RetrievalJob + + assert isinstance(retrieval_job, RetrievalJob) + + +class TestNonEntityRetrieval: + """ + Test suite for non-entity retrieval functionality (entity_df=None) + + This test suite comprehensively covers the new non-entity retrieval mode + for Dask offline store, which enables retrieving features for specified + time ranges without requiring an entity DataFrame. + + These tests use monkeypatching to inject real data and validate actual + retrieval behavior, not just API signatures. + """ + + def test_dask_non_entity_snapshot_ttl_and_dedup(self, monkeypatch): + # Controlled feature data with dedup (same event ts, newer created ts wins) and TTL filtering + src = pd.DataFrame( + { + "driver_id": [1, 1, 1, 2, 2], + "event_timestamp": pd.to_datetime( + [ + "2025-01-01T10:00:00Z", + "2025-01-01T10:00:00Z", # duplicate event ts, newer created_ts + "2024-12-30T10:00:00Z", # outside TTL (1 day) relative to end_date + "2025-01-01T12:00:00Z", # within TTL window + "2025-01-02T11:00:00Z", # after end_date + ] + ), + "created_ts": pd.to_datetime( + [ + "2025-01-01T10:00:01Z", + "2025-01-01T10:00:02Z", # should be selected for (1, 2025-01-01 10:00) + "2024-12-30T10:00:00Z", + "2025-01-01T12:00:00Z", + "2025-01-02T11:00:00Z", + ] + ), + "conv_rate": [0.1, 0.2, 0.05, 0.3, 0.4], + } + ) + ddf = dd.from_pandas(src, npartitions=1) + + # Monkeypatch the datasource reader used by DaskOfflineStore to return our in-memory data + monkeypatch.setattr(dask_mod, "_read_datasource", lambda ds, repo_path: ddf) + + fv = FeatureView( + name="driver_stats", + entities=_mock_entity(), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float32), + ], + source=FileSource( + path="unused", + timestamp_field="event_timestamp", + created_timestamp_column="created_ts", + ), + ttl=timedelta(days=1), + ) + + repo_config = RepoConfig( + project="proj", + registry="unused", + provider="local", + offline_store=_mock_dask_offline_store_config(), + ) + + end = datetime(2025, 1, 2, 10, 0, tzinfo=timezone.utc) + start = end - timedelta(days=7) + + job = DaskOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["driver_stats:conv_rate"], + entity_df=None, + registry=MagicMock(), + project="proj", + full_feature_names=False, + start_date=start, + end_date=end, + ) + + df = job.to_df() + + # Two entities are present + assert set(df["driver_id"]) == {1, 2} + out = df.set_index("driver_id")["conv_rate"].to_dict() + # Dedup by created_ts for entity 1 at same event_timestamp + assert out[1] == 0.2 + # TTL-enforced latest for entity 2 within the window + assert out[2] == 0.3 + + # Event timestamps are tz-aware UTC + assert "UTC" in str(df["event_timestamp"].dtype) + + # Metadata reflects the requested window in non-entity mode + assert job.metadata.min_event_timestamp == start + assert job.metadata.max_event_timestamp == end + + def test_non_entity_mode_with_both_dates_retrieves_data(self, monkeypatch): + """Test non-entity retrieval with both dates actually retrieves and filters data correctly""" + + # Create test data spanning the date range + src = pd.DataFrame( + { + "driver_id": [1, 1, 1, 2], + "event_timestamp": pd.to_datetime( + [ + "2023-01-01T08:00:00Z", # Before start_date + "2023-01-03T10:00:00Z", # Within range + "2023-01-05T12:00:00Z", # Within range + "2023-01-08T14:00:00Z", # After end_date + ] + ), + "conv_rate": [0.1, 0.2, 0.3, 0.4], + } + ) + ddf = dd.from_pandas(src, npartitions=1) + monkeypatch.setattr(dask_mod, "_read_datasource", lambda ds, repo_path: ddf) + + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=_mock_dask_offline_store_config(), + ) + + fv = FeatureView( + name="test_fv", + entities=_mock_entity(), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float32), + ], + source=FileSource(path="unused", timestamp_field="event_timestamp"), + ttl=None, # No TTL - should include all rows within date range + ) + + start_date = datetime(2023, 1, 2, tzinfo=timezone.utc) + end_date = datetime(2023, 1, 7, tzinfo=timezone.utc) + + job = DaskOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["test_fv:conv_rate"], + entity_df=None, + registry=MagicMock(), + project="test_project", + start_date=start_date, + end_date=end_date, + ) + + df = job.to_df() + + # Verify metadata + assert job.metadata.min_event_timestamp == start_date + assert job.metadata.max_event_timestamp == end_date + + # Verify data: should only include rows within the date range + # Row at 2023-01-01 (before start) and 2023-01-08 (after end) should be filtered out + assert len(df) >= 1 + # Should have data for entities that have rows within the range + assert set(df["driver_id"]) == {1} # Only driver 1 has data in range + # Verify the latest value for driver 1 within the range + driver1_data = df[df["driver_id"] == 1] + assert len(driver1_data) >= 1 + # Latest value should be 0.3 (from 2023-01-05) + assert 0.3 in driver1_data["conv_rate"].values + + def test_non_entity_mode_with_end_date_only_calculates_start_from_ttl( + self, monkeypatch + ): + """Test that start_date is calculated from max TTL when only end_date provided""" + + # Create data with rows at different times + src = pd.DataFrame( + { + "driver_id": [1, 1, 1], + "event_timestamp": pd.to_datetime( + [ + "2023-01-05T08:00:00Z", # Outside 1-day TTL from end_date + "2023-01-06T10:00:00Z", # Within 1-day TTL + "2023-01-07T12:00:00Z", # At end_date + ] + ), + "conv_rate": [0.1, 0.2, 0.3], + } + ) + ddf = dd.from_pandas(src, npartitions=1) + monkeypatch.setattr(dask_mod, "_read_datasource", lambda ds, repo_path: ddf) + + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=_mock_dask_offline_store_config(), + ) + + # Feature view with 1-day TTL + fv = FeatureView( + name="test_fv", + entities=_mock_entity(), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float32), + ], + source=FileSource(path="unused", timestamp_field="event_timestamp"), + ttl=timedelta(days=1), + ) + + end_date = datetime(2023, 1, 7, 12, 0, tzinfo=timezone.utc) + + job = DaskOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["test_fv:conv_rate"], + entity_df=None, + registry=MagicMock(), + project="test_project", + end_date=end_date, + # start_date not provided - should be calculated from TTL + ) + + df = job.to_df() + + # Verify start_date was calculated from TTL (1 day before end_date) + expected_start = end_date - timedelta(days=1) + assert job.metadata.min_event_timestamp == expected_start + assert job.metadata.max_event_timestamp == end_date + + # Verify TTL filtering: row at 2023-01-05 (outside TTL) should be filtered out + # Only rows within 1 day of end_date should be present + driver1_data = df[df["driver_id"] == 1] + # Should have latest value from within TTL window + assert len(driver1_data) >= 1 + # Latest value should be 0.3 (from 2023-01-07) + assert 0.3 in driver1_data["conv_rate"].values + + @patch("feast.utils.datetime") + def test_no_dates_provided_defaults_to_current_time_and_filters_data( + self, mock_datetime, monkeypatch + ): + """Test that default date calculation works and filters data correctly""" + + fixed_now = datetime(2023, 1, 7, 12, 0, 0, tzinfo=timezone.utc) + mock_datetime.now.return_value = fixed_now + + # Create data with rows at different times relative to "now" + src = pd.DataFrame( + { + "driver_id": [1, 1, 1], + "event_timestamp": pd.to_datetime( + [ + "2023-01-05T12:00:00Z", # 2 days before "now" - outside 1-day TTL + "2023-01-06T18:00:00Z", # ~18 hours before "now" - within 1-day TTL + "2023-01-07T11:00:00Z", # 1 hour before "now" - within 1-day TTL + ] + ), + "conv_rate": [0.1, 0.2, 0.3], + } + ) + ddf = dd.from_pandas(src, npartitions=1) + monkeypatch.setattr(dask_mod, "_read_datasource", lambda ds, repo_path: ddf) + + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=_mock_dask_offline_store_config(), + ) + + fv = FeatureView( + name="test_fv", + entities=_mock_entity(), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float32), + ], + source=FileSource(path="unused", timestamp_field="event_timestamp"), + ttl=timedelta(days=1), + ) + + job = DaskOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["test_fv:conv_rate"], + entity_df=None, + registry=MagicMock(), + project="test_project", + # No start_date or end_date provided + ) + + df = job.to_df() + + # Verify end_date defaulted to current time + assert job.metadata.max_event_timestamp == fixed_now + # Verify start_date was calculated from TTL + expected_start = fixed_now - timedelta(days=1) + assert job.metadata.min_event_timestamp == expected_start + + # Verify data filtering: row at 2023-01-05 (outside TTL) should be filtered out + driver1_data = df[df["driver_id"] == 1] + assert len(driver1_data) >= 1 + # Latest value should be 0.3 (from 2023-01-07) + assert 0.3 in driver1_data["conv_rate"].values + + def test_ttl_filtering_excludes_old_rows(self, monkeypatch): + """Test that TTL filtering correctly excludes rows outside the TTL window""" + + # Create data with rows inside and outside TTL window + src = pd.DataFrame( + { + "driver_id": [1, 1, 1], + "event_timestamp": pd.to_datetime( + [ + "2025-01-01T08:00:00Z", # Outside 1-hour TTL (end_date is 10:00) + "2025-01-01T09:30:00Z", # Within 1-hour TTL + "2025-01-01T10:00:00Z", # At end_date + ] + ), + "conv_rate": [0.1, 0.2, 0.3], + } + ) + ddf = dd.from_pandas(src, npartitions=1) + monkeypatch.setattr(dask_mod, "_read_datasource", lambda ds, repo_path: ddf) + + fv = FeatureView( + name="driver_stats", + entities=_mock_entity(), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="conv_rate", dtype=Float32), + ], + source=FileSource(path="unused", timestamp_field="event_timestamp"), + ttl=timedelta(hours=1), + ) + + repo_config = RepoConfig( + project="proj", + registry="unused", + provider="local", + offline_store=_mock_dask_offline_store_config(), + ) + + end = datetime(2025, 1, 1, 10, 0, tzinfo=timezone.utc) + start = end - timedelta(days=1) + + job = DaskOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["driver_stats:conv_rate"], + entity_df=None, + registry=MagicMock(), + project="proj", + full_feature_names=False, + start_date=start, + end_date=end, + ) + + df = job.to_df() + + # Only rows within TTL window should be present + # Row at 08:00 should be filtered out (outside 1-hour TTL) + driver1_data = df[df["driver_id"] == 1] + assert len(driver1_data) >= 1 + # Latest value should be 0.3 (from 10:00) or 0.2 (from 09:30) + assert ( + 0.2 in driver1_data["conv_rate"].values + or 0.3 in driver1_data["conv_rate"].values + ) + # Value 0.1 (from 08:00, outside TTL) should NOT be present + assert 0.1 not in driver1_data["conv_rate"].values + + def test_multiple_feature_views_with_different_ttls(self, monkeypatch): + """Test retrieval with multiple feature views having different TTLs""" + # Create data for two feature views + src1 = pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": pd.to_datetime( + [ + "2025-01-01T08:00:00Z", # Outside 1-hour TTL + "2025-01-01T09:30:00Z", # Within 1-hour TTL + ] + ), + "age": [25, 26], + } + ) + + src2 = pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": pd.to_datetime( + [ + "2025-01-01T08:00:00Z", # Within 1-day TTL + "2025-01-01T09:30:00Z", # Within 1-day TTL + ] + ), + "total_trips": [100, 101], + } + ) + + ddf1 = dd.from_pandas(src1, npartitions=1) + ddf2 = dd.from_pandas(src2, npartitions=1) + + call_count = [0] + + def mock_read_datasource(ds, repo_path): + call_count[0] += 1 + if call_count[0] == 1: + return ddf1 + return ddf2 + + monkeypatch.setattr(dask_mod, "_read_datasource", mock_read_datasource) + + fv1 = FeatureView( + name="user_fv", + entities=_mock_entity(), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="age", dtype=Float32), + ], + source=FileSource(path="unused1", timestamp_field="event_timestamp"), + ttl=timedelta(hours=1), + ) + + fv2 = FeatureView( + name="transaction_fv", + entities=_mock_entity(), + schema=[ + Field(name="driver_id", dtype=Int64), + Field(name="total_trips", dtype=Float32), + ], + source=FileSource(path="unused2", timestamp_field="event_timestamp"), + ttl=timedelta(days=1), + ) + + repo_config = RepoConfig( + project="proj", + registry="unused", + provider="local", + offline_store=_mock_dask_offline_store_config(), + ) + + end = datetime(2025, 1, 1, 10, 0, tzinfo=timezone.utc) + start = end - timedelta(days=1) + + job = DaskOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv1, fv2], + feature_refs=["user_fv:age", "transaction_fv:total_trips"], + entity_df=None, + registry=MagicMock(), + project="proj", + full_feature_names=False, + start_date=start, + end_date=end, + ) + + df = job.to_df() + + # Both feature views should be processed + assert "age" in df.columns or "user_fv__age" in df.columns + assert ( + "total_trips" in df.columns or "transaction_fv__total_trips" in df.columns + ) + + # Verify TTL filtering worked correctly + # For user_fv (1-hour TTL): only row at 09:30 should be present + # For transaction_fv (1-day TTL): both rows should be present, latest is 101 + if "age" in df.columns: + age_values = df["age"].values + # Should have 26 (from 09:30, within 1-hour TTL), not 25 (from 08:00) + assert 26 in age_values + assert 25 not in age_values + + if "total_trips" in df.columns: + trips_values = df["total_trips"].values + # Should have 101 (latest within 1-day TTL) + assert 101 in trips_values diff --git a/sdk/python/tests/unit/infra/offline_stores/test_filter_by_created_timestamp.py b/sdk/python/tests/unit/infra/offline_stores/test_filter_by_created_timestamp.py new file mode 100644 index 00000000000..e5eff80a412 --- /dev/null +++ b/sdk/python/tests/unit/infra/offline_stores/test_filter_by_created_timestamp.py @@ -0,0 +1,320 @@ +from datetime import timedelta +from unittest.mock import MagicMock + +import dask.dataframe as dd +import ibis +import pandas as pd +import pytest + +from feast.entity import Entity +from feast.feature_view import FeatureView, Field +from feast.infra.offline_stores import dask as dask_mod +from feast.infra.offline_stores import offline_utils +from feast.infra.offline_stores.bigquery import ( + MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, +) +from feast.infra.offline_stores.dask import ( + DaskOfflineStore, + DaskOfflineStoreConfig, +) +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.offline_stores.ibis import point_in_time_join +from feast.infra.offline_stores.offline_utils import FeatureViewQueryContext +from feast.repo_config import RepoConfig +from feast.types import Float32, Int64, ValueType + +CREATED_TIMESTAMP_PREDICATE = ( + "subquery.created_timestamp <= entity_dataframe.entity_timestamp" +) + + +def _query_context(created_timestamp_column): + return FeatureViewQueryContext( + name="driver_stats", + ttl=86400, + entities=["driver_id"], + features=["conv_rate"], + field_mapping={}, + timestamp_field="event_timestamp", + created_timestamp_column=created_timestamp_column, + table_subquery="`project`.`dataset`.`table`", + entity_selections=["driver_id AS driver_id"], + min_event_timestamp="2025-01-01T00:00:00", + max_event_timestamp="2025-01-02T00:00:00", + date_partition_column=None, + timestamp_field_type=None, + ) + + +def _render( + created_timestamp_column, + query_template=MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN, + **kwargs, +): + return offline_utils.build_point_in_time_query( + [_query_context(created_timestamp_column)], + left_table_query_string="entity_df_table", + entity_df_event_timestamp_col="event_timestamp", + entity_df_columns={"driver_id": None, "event_timestamp": None}.keys(), + query_template=query_template, + full_feature_names=False, + **kwargs, + ) + + +def test_filter_by_created_timestamp_adds_created_timestamp_cutoff_to_query(): + query = _render("created_ts", filter_by_created_timestamp=True) + assert CREATED_TIMESTAMP_PREDICATE in query + + +def test_filter_by_created_timestamp_defaults_to_false_and_leaves_query_unchanged(): + assert CREATED_TIMESTAMP_PREDICATE not in _render("created_ts") + assert _render("created_ts") == _render( + "created_ts", filter_by_created_timestamp=False + ) + + +def test_filter_by_created_timestamp_has_no_effect_without_created_timestamp_column(): + query = _render(None, filter_by_created_timestamp=True) + assert CREATED_TIMESTAMP_PREDICATE not in query + + +SQL_TEMPLATE_STORE_MODULES = [ + "feast.infra.offline_stores.redshift", + "feast.infra.offline_stores.contrib.spark_offline_store.spark", + "feast.infra.offline_stores.contrib.trino_offline_store.trino", + "feast.infra.offline_stores.contrib.athena_offline_store.athena", + "feast.infra.offline_stores.contrib.postgres_offline_store.postgres", + "feast.infra.offline_stores.contrib.clickhouse_offline_store.clickhouse", + "feast.infra.offline_stores.contrib.couchbase_offline_store.couchbase", +] + + +@pytest.mark.parametrize("module_name", SQL_TEMPLATE_STORE_MODULES) +def test_all_sql_templates_gate_the_created_timestamp_cutoff(module_name): + module = pytest.importorskip(module_name) + template = module.MULTIPLE_FEATURE_VIEW_POINT_IN_TIME_JOIN + + with_cutoff = _render( + "created_ts", query_template=template, filter_by_created_timestamp=True + ) + assert CREATED_TIMESTAMP_PREDICATE in with_cutoff + + assert CREATED_TIMESTAMP_PREDICATE not in _render( + "created_ts", query_template=template + ) + assert CREATED_TIMESTAMP_PREDICATE not in _render( + None, query_template=template, filter_by_created_timestamp=True + ) + + +def test_stores_declare_filter_by_created_timestamp_support(): + from feast.infra.offline_stores.offline_store import OfflineStore + + class _UnsupportedStore(OfflineStore): + pass + + with pytest.raises(NotImplementedError, match="filter_by_created_timestamp"): + _UnsupportedStore().ensure_filter_by_created_timestamp_supported() + + assert DaskOfflineStore.supports_filter_by_created_timestamp + DaskOfflineStore().ensure_filter_by_created_timestamp_supported() + + +class TestDaskFilterByCreatedTimestamp: + def _run(self, filter_by_created_timestamp, monkeypatch, src=None, entity_df=None): + if src is None: + src = pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": pd.to_datetime( + [ + "2025-01-01T10:00:00Z", + "2025-01-01T10:00:00Z", # same event ts, created after the entity ts + ] + ), + "created_ts": pd.to_datetime( + [ + "2025-01-01T12:00:00Z", # created before the entity ts + "2025-01-03T00:00:00Z", # created after the entity ts + ] + ), + "conv_rate": [0.4, 0.6], + } + ) + ddf = dd.from_pandas(src, npartitions=1) + monkeypatch.setattr(dask_mod, "_read_datasource", lambda ds, repo_path: ddf) + + repo_config = RepoConfig( + project="test_project", + registry="test_registry", + provider="local", + offline_store=DaskOfflineStoreConfig(type="dask"), + ) + fv = FeatureView( + name="driver_stats", + entities=[ + Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + ], + schema=[Field(name="conv_rate", dtype=Float32)], + source=FileSource( + path="dummy.parquet", # not read in this test + timestamp_field="event_timestamp", + created_timestamp_column="created_ts", + ), + ttl=timedelta(days=7), + ) + # Without this the derived join keys are empty and _merge cross joins, which + # hides per-entity bugs. apply()/infer_features() sets it in real usage. + fv.entity_columns = [Field(name="driver_id", dtype=Int64)] + registry = MagicMock() + registry.list_on_demand_feature_views.return_value = [] + + if entity_df is None: + entity_df = pd.DataFrame( + { + "driver_id": [1], + "event_timestamp": pd.to_datetime(["2025-01-02T00:00:00Z"]), + } + ) + + job = DaskOfflineStore.get_historical_features( + config=repo_config, + feature_views=[fv], + feature_refs=["driver_stats:conv_rate"], + entity_df=entity_df, + registry=registry, + project="test_project", + full_feature_names=False, + filter_by_created_timestamp=filter_by_created_timestamp, + ) + return job.to_df() + + def test_default_serves_latest_created_value(self, monkeypatch): + df = self._run(False, monkeypatch) + assert df["conv_rate"].tolist() == [0.6] + + def test_filter_by_created_timestamp_excludes_values_created_after_entity_timestamp( + self, monkeypatch + ): + df = self._run(True, monkeypatch) + assert df["conv_rate"].tolist() == [0.4] + + def test_filter_by_created_timestamp_excludes_rows_with_null_created_timestamp( + self, monkeypatch + ): + src = pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": pd.to_datetime( + ["2025-01-01T10:00:00Z", "2025-01-01T10:00:00Z"] + ), + "created_ts": pd.to_datetime(["2025-01-01T12:00:00Z", pd.NaT]), + "conv_rate": [0.4, 0.6], + } + ) + df = self._run(True, monkeypatch, src=src) + assert df["conv_rate"].tolist() == [0.4] + + def test_entity_row_is_kept_when_every_candidate_is_future_created( + self, monkeypatch + ): + src = pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": pd.to_datetime( + ["2025-01-01T10:00:00Z", "2025-01-01T11:00:00Z"] + ), + "created_ts": pd.to_datetime( + ["2025-01-03T00:00:00Z", "2025-01-04T00:00:00Z"] + ), + "conv_rate": [0.4, 0.6], + } + ) + df = self._run(True, monkeypatch, src=src) + assert len(df) == 1 + assert df["driver_id"].tolist() == [1] + assert df["conv_rate"].isna().all() + + def test_entity_rows_without_a_valid_version_do_not_borrow_from_others( + self, monkeypatch + ): + src = pd.DataFrame( + { + "driver_id": [1, 2], + "event_timestamp": pd.to_datetime( + ["2025-01-01T10:00:00Z", "2025-01-01T10:00:00Z"] + ), + "created_ts": pd.to_datetime( + ["2025-01-01T12:00:00Z", "2025-01-03T00:00:00Z"] + ), + "conv_rate": [0.4, 0.6], + } + ) + entity_df = pd.DataFrame( + { + "driver_id": [1, 2], + "event_timestamp": pd.to_datetime( + ["2025-01-02T00:00:00Z", "2025-01-02T00:00:00Z"] + ), + } + ) + df = self._run(True, monkeypatch, src=src, entity_df=entity_df) + by_driver = df.set_index("driver_id")["conv_rate"] + assert len(df) == 2 + assert by_driver.loc[1] == pytest.approx(0.4) + assert pd.isna(by_driver.loc[2]) + + +class TestIbisFilterByCreatedTimestamp: + def _run(self, filter_by_created_timestamp): + entity_table = ibis.memtable( + pd.DataFrame( + { + "driver_id": [1], + "event_timestamp": pd.to_datetime(["2025-01-02T00:00:00Z"]), + } + ) + ) + feature_table = ibis.memtable( + pd.DataFrame( + { + "driver_id": [1, 1], + "event_timestamp": pd.to_datetime( + ["2025-01-01T10:00:00Z", "2025-01-01T10:00:00Z"] + ), + "created_ts": pd.to_datetime( + ["2025-01-01T12:00:00Z", "2025-01-03T00:00:00Z"] + ), + "conv_rate": [0.4, 0.6], + } + ) + ) + res = point_in_time_join( + entity_table=entity_table, + feature_tables=[ + ( + feature_table, + "event_timestamp", + "created_ts", + {"driver_id": "driver_id"}, + ["conv_rate"], + None, + ) + ], + event_timestamp_col="event_timestamp", + filter_by_created_timestamp=filter_by_created_timestamp, + ).execute() + return res + + def test_default_serves_latest_created_value(self): + assert self._run(False)["conv_rate"].tolist() == [0.6] + + def test_filter_by_created_timestamp_excludes_values_created_after_entity_timestamp( + self, + ): + assert self._run(True)["conv_rate"].tolist() == [0.4] diff --git a/sdk/python/tests/unit/infra/online_store/test_cassandra_online_store.py b/sdk/python/tests/unit/infra/online_store/test_cassandra_online_store.py new file mode 100644 index 00000000000..c37f8592c2a --- /dev/null +++ b/sdk/python/tests/unit/infra/online_store/test_cassandra_online_store.py @@ -0,0 +1,518 @@ +from unittest.mock import MagicMock, patch + +import pytest + +# Skip the entire module when the optional cassandra-driver is not installed. +pytest.importorskip("cassandra", reason="cassandra-driver not installed") + +from cassandra.cluster import EXEC_PROFILE_DEFAULT # noqa: E402 + +from feast.infra.online_stores.cassandra_online_store import ( # noqa: E402 + cassandra_online_store as _cos, +) +from feast.repo_config import RepoConfig # noqa: E402 + +CassandraInvalidConfig = _cos.CassandraInvalidConfig +CassandraOnlineStore = _cos.CassandraOnlineStore +CassandraOnlineStoreConfig = _cos.CassandraOnlineStoreConfig +E_CASSANDRA_DC_CONFIG_CONFLICT = _cos.E_CASSANDRA_DC_CONFIG_CONFLICT +E_CASSANDRA_DC_CONFIG_EMPTY = _cos.E_CASSANDRA_DC_CONFIG_EMPTY +E_CASSANDRA_DC_DEFAULT_NOT_FOUND = _cos.E_CASSANDRA_DC_DEFAULT_NOT_FOUND +E_CASSANDRA_DC_ROUTING_INVALID = _cos.E_CASSANDRA_DC_ROUTING_INVALID + + +_DCConfig = CassandraOnlineStoreConfig.CassandraDatacenterConfig +_LBConfig = CassandraOnlineStoreConfig.CassandraLoadBalancingPolicy +_RoutingConfig = CassandraOnlineStoreConfig.CassandraRoutingConfig + +# Path to patch so tests never open a real Cassandra connection +_CLUSTER_PATH = ( + "feast.infra.online_stores.cassandra_online_store.cassandra_online_store.Cluster" +) + + +# helpers + + +def _dc(name: str, hosts=None, rf=None, rs=None) -> _DCConfig: + return _DCConfig( + name=name, + hosts=hosts or ["192.168.0.1"], + replication_factor=rf, + replication_strategy=rs, + ) + + +def _lb( + local_dc: str = "dc1", + policy: str = "DCAwareRoundRobinPolicy", +) -> _LBConfig: + return _LBConfig(local_dc=local_dc, load_balancing_policy=policy) + + +def _routing(read_dc=None, write_dc=None) -> _RoutingConfig: + return _RoutingConfig(read_dc=read_dc, write_dc=write_dc) + + +def _repo_config(online_store_cfg) -> RepoConfig: + return RepoConfig( + registry="dummy_registry.db", + project="test_project", + provider="local", + entity_key_serialization_version=3, + online_store=online_store_cfg, + ) + + +# Fixture: mock out the cassandra cluster conn + + +@pytest.fixture +def mock_cluster(): + """Yields (MockClusterClass, mock_session) with Cluster fully patched.""" + with patch(_CLUSTER_PATH) as mock_cls: + mock_session = MagicMock() + mock_cls.return_value.connect.return_value = mock_session + yield mock_cls, mock_session + + +# Section 1 — Config-parsing tests (no DB connection, no mocking needed) + + +class TestCassandraConfigParsing: + """Validate that the Pydantic config models parse correctly.""" + + def test_single_dc_hosts_config_parses(self): + cfg = CassandraOnlineStoreConfig(hosts=["192.168.1.1"], keyspace="ks") + assert cfg.hosts == ["192.168.1.1"] + assert cfg.keyspace == "ks" + assert cfg.datacenters is None + assert cfg.routing is None + + def test_datacenters_field_parses_correctly(self): + cfg = CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + ) + assert len(cfg.datacenters) == 2 + assert cfg.datacenters[0].name == "dc1" + assert cfg.datacenters[0].hosts == ["192.168.1.1"] + assert cfg.datacenters[1].name == "dc2" + assert cfg.datacenters[1].hosts == ["10.0.0.1"] + + def test_datacenter_replication_fields_parse(self): + cfg = CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc( + "dc1", + ["192.168.1.1"], + rf=3, + rs="NetworkTopologyStrategy", + ), + ], + ) + assert cfg.datacenters[0].replication_factor == 3 + assert cfg.datacenters[0].replication_strategy == "NetworkTopologyStrategy" + + def test_datacenter_replication_fields_default_to_none(self): + cfg = CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[_dc("dc1", ["192.168.1.1"])], + ) + assert cfg.datacenters[0].replication_factor is None + assert cfg.datacenters[0].replication_strategy is None + + def test_routing_block_parses(self): + cfg = CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + routing=_routing(read_dc="dc2", write_dc="dc1"), + ) + assert cfg.routing.read_dc == "dc2" + assert cfg.routing.write_dc == "dc1" + + def test_routing_fields_default_to_none(self): + cfg = CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[_dc("dc1", ["192.168.1.1"])], + routing=_routing(), + ) + assert cfg.routing.read_dc is None + assert cfg.routing.write_dc is None + + def test_astra_db_config_parses(self): + cfg = CassandraOnlineStoreConfig( + secure_bundle_path="/path/to/bundle.zip", + keyspace="ks", + username="client_id", + password="pw", # pragma: allowlist secret + ) + assert cfg.secure_bundle_path == "/path/to/bundle.zip" + assert cfg.datacenters is None + + +# Section 2 — _get_session: validation errors (Branch B early exits) +# These should be raised BEFORE any driver connection is made. + + +class TestCassandraMultiDCValidation: + """ + Validation errors that _get_session raises before touching the driver. + """ + + def test_datacenters_and_hosts_are_mutually_exclusive(self, mock_cluster): + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + hosts=["192.168.1.1"], + datacenters=[_dc("dc1", ["10.0.0.1"])], + ) + ) + with pytest.raises( + CassandraInvalidConfig, match=E_CASSANDRA_DC_CONFIG_CONFLICT + ): + store._get_session(cfg) + + def test_datacenters_and_secure_bundle_are_mutually_exclusive(self, mock_cluster): + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + secure_bundle_path="/path/bundle.zip", + datacenters=[_dc("dc1", ["10.0.0.1"])], + ) + ) + with pytest.raises( + CassandraInvalidConfig, match=E_CASSANDRA_DC_CONFIG_CONFLICT + ): + store._get_session(cfg) + + def test_empty_datacenters_list_raises(self, mock_cluster): + store = CassandraOnlineStore() + cfg = _repo_config(CassandraOnlineStoreConfig(keyspace="ks", datacenters=[])) + with pytest.raises(CassandraInvalidConfig, match=E_CASSANDRA_DC_CONFIG_EMPTY): + store._get_session(cfg) + + def test_load_balancing_local_dc_not_in_datacenters_raises(self, mock_cluster): + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc99"), # dc99 doesn't exist + ) + ) + with pytest.raises( + CassandraInvalidConfig, + match=E_CASSANDRA_DC_DEFAULT_NOT_FOUND, + ): + store._get_session(cfg) + + def test_routing_read_dc_not_in_datacenters_raises(self, mock_cluster): + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc1"), + routing=_routing(read_dc="dc99"), # dc99 doesn't exist + ) + ) + with pytest.raises( + CassandraInvalidConfig, match=E_CASSANDRA_DC_ROUTING_INVALID + ): + store._get_session(cfg) + + def test_routing_write_dc_not_in_datacenters_raises(self, mock_cluster): + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc1"), + routing=_routing(write_dc="dc99"), # dc99 doesn't exist + ) + ) + with pytest.raises( + CassandraInvalidConfig, match=E_CASSANDRA_DC_ROUTING_INVALID + ): + store._get_session(cfg) + + +# Section 3 — _get_session: successful multi-DC session creation (Branch B) + + +class TestCassandraMultiDCSession: + """ + Happy-path tests for Branch B — one execution profile per datacenter. + """ + + def test_execution_profiles_created_for_each_dc(self, mock_cluster): + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc1"), + ) + ) + store._get_session(cfg) + + profiles = mock_cls.call_args.kwargs["execution_profiles"] + assert "dc1" in profiles + assert "dc2" in profiles + assert EXEC_PROFILE_DEFAULT in profiles + assert store._dc_execution_profiles == ["dc1", "dc2"] + + def test_default_dc_from_load_balancing_local_dc(self, mock_cluster): + """ + EXEC_PROFILE_DEFAULT must point to the DC named by + load_balancing.local_dc. + """ + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc2"), # dc2 is default + ) + ) + store._get_session(cfg) + + profiles = mock_cls.call_args.kwargs["execution_profiles"] + assert profiles[EXEC_PROFILE_DEFAULT] is profiles["dc2"] + + def test_default_dc_fallback_to_first_entry_when_no_load_balancing( + self, mock_cluster + ): + """ + When load_balancing is absent, the first datacenter entry is + the default. + """ + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + # no load_balancing block + ) + ) + store._get_session(cfg) + + profiles = mock_cls.call_args.kwargs["execution_profiles"] + assert profiles[EXEC_PROFILE_DEFAULT] is profiles["dc1"] + + def test_all_dc_hosts_merged_into_cluster_contact_points(self, mock_cluster): + """ + Cluster() must receive the union of all DC hosts as contact points. + """ + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1", "192.168.1.2"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc1"), + ) + ) + store._get_session(cfg) + + contact_points = mock_cls.call_args.args[0] + assert set(contact_points) == { + "192.168.1.1", + "192.168.1.2", + "10.0.0.1", + } + + def test_routing_read_dc_sets_read_execution_profile(self, mock_cluster): + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc1"), + routing=_routing(read_dc="dc2"), + ) + ) + store._get_session(cfg) + assert store._read_execution_profile == "dc2" + + def test_routing_write_dc_sets_write_execution_profile(self, mock_cluster): + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc1"), + routing=_routing(write_dc="dc1"), + ) + ) + store._get_session(cfg) + assert store._write_execution_profile == "dc1" + + def test_no_routing_block_uses_default_dc_for_both_ops(self, mock_cluster): + """Without a routing block, both read and write use the default DC.""" + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc1"), + ) + ) + store._get_session(cfg) + assert store._read_execution_profile == "dc1" + assert store._write_execution_profile == "dc1" + + def test_partial_routing_missing_read_dc_falls_back_to_default(self, mock_cluster): + """routing.read_dc omitted → reads use default DC.""" + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[ + _dc("dc1", ["192.168.1.1"]), + _dc("dc2", ["10.0.0.1"]), + ], + load_balancing=_lb(local_dc="dc1"), + routing=_routing(write_dc="dc2"), # only write_dc given + ) + ) + store._get_session(cfg) + # read_dc not given → falls back to default_dc "dc1" + assert store._read_execution_profile == "dc1" + assert store._write_execution_profile == "dc2" + + def test_session_is_cached_and_cluster_created_once(self, mock_cluster): + """Calling _get_session twice returns the same session object.""" + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[_dc("dc1", ["192.168.1.1"])], + load_balancing=_lb(local_dc="dc1"), + ) + ) + s1 = store._get_session(cfg) + s2 = store._get_session(cfg) + + assert s1 is s2 + assert mock_cls.call_count == 1 # Cluster() instantiated exactly once + + def test_single_dc_in_datacenters_list_works(self, mock_cluster): + """A single-entry datacenters list is valid (not an error).""" + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + keyspace="ks", + datacenters=[_dc("dc1", ["192.168.1.1"])], + load_balancing=_lb(local_dc="dc1"), + ) + ) + session = store._get_session(cfg) + assert session is mock_session + + +# Section 4 — Backward compatibility: Branch A (single-DC / Astra DB) + + +class TestCassandraSingleDCBackwardCompat: + """ + The original Branch A path must be unaffected by the multi-DC changes. + In particular, _read_execution_profile and _write_execution_profile must + stay at their class-level EXEC_PROFILE_DEFAULT sentinel in Branch A. + """ + + def test_branch_a_hosts_creates_session(self, mock_cluster): + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig(hosts=["127.0.0.1"], port=9042, keyspace="ks") + ) + session = store._get_session(cfg) + + assert session is mock_session + # Branch A leaves the sentinel defaults untouched + assert store._read_execution_profile is EXEC_PROFILE_DEFAULT + assert store._write_execution_profile is EXEC_PROFILE_DEFAULT + + def test_branch_a_astra_db_creates_session(self, mock_cluster): + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + secure_bundle_path="/path/bundle.zip", + keyspace="ks", + username="client_id", + password="pw", # pragma: allowlist secret + ) + ) + session = store._get_session(cfg) + + assert session is mock_session + assert store._read_execution_profile is EXEC_PROFILE_DEFAULT + assert store._write_execution_profile is EXEC_PROFILE_DEFAULT + + def test_branch_a_with_load_balancing_creates_session(self, mock_cluster): + mock_cls, mock_session = mock_cluster + store = CassandraOnlineStore() + cfg = _repo_config( + CassandraOnlineStoreConfig( + hosts=["127.0.0.1"], + keyspace="ks", + load_balancing=_lb(local_dc="datacenter1"), + ) + ) + session = store._get_session(cfg) + + assert session is mock_session + # Branch A does not touch execution profile attributes + assert store._read_execution_profile is EXEC_PROFILE_DEFAULT + assert store._write_execution_profile is EXEC_PROFILE_DEFAULT diff --git a/sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py b/sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py index 4127f699810..3b84b1a3547 100644 --- a/sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py +++ b/sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py @@ -64,10 +64,17 @@ def test_dynamodb_online_store_config_default(): aws_region = "us-west-2" dynamodb_store_config = DynamoDBOnlineStoreConfig(region=aws_region) assert dynamodb_store_config.type == "dynamodb" - assert dynamodb_store_config.batch_size == 40 + assert dynamodb_store_config.batch_size == 100 assert dynamodb_store_config.endpoint_url is None assert dynamodb_store_config.region == aws_region assert dynamodb_store_config.table_name_template == "{project}.{table_name}" + # Verify other optimized defaults + assert dynamodb_store_config.max_pool_connections == 50 + assert dynamodb_store_config.keepalive_timeout == 30.0 + assert dynamodb_store_config.connect_timeout == 5 + assert dynamodb_store_config.read_timeout == 10 + assert dynamodb_store_config.total_max_retry_attempts == 3 + assert dynamodb_store_config.retry_mode == "adaptive" def test_dynamodb_online_store_config_custom_params(): @@ -476,3 +483,807 @@ def to_ek_proto(val): actual = list(_latest_data_to_write(data)) expected = [data[2], data[1], data[4]] assert expected == actual + + +def _create_entity_key(entity_id: str) -> EntityKeyProto: + """Helper function to create EntityKeyProto for testing.""" + return EntityKeyProto( + join_keys=["customer"], entity_values=[ValueProto(string_val=entity_id)] + ) + + +def _create_string_list_value(items: list[str]) -> ValueProto: + """Helper function to create ValueProto with string list.""" + from feast.protos.feast.types.Value_pb2 import StringList + + return ValueProto(string_list_val=StringList(val=items)) + + +def _create_int32_list_value(items: list[int]) -> ValueProto: + """Helper function to create ValueProto with int32 list.""" + from feast.protos.feast.types.Value_pb2 import Int32List + + return ValueProto(int32_list_val=Int32List(val=items)) + + +def _extract_string_list(value_proto: ValueProto) -> list[str]: + """Helper function to extract string list from ValueProto.""" + return list(value_proto.string_list_val.val) + + +def _extract_int32_list(value_proto: ValueProto) -> list[int]: + """Helper function to extract int32 list from ValueProto.""" + return list(value_proto.int32_list_val.val) + + +@mock_dynamodb +def test_dynamodb_update_online_store_list_append(repo_config, dynamodb_online_store): + """Test DynamoDB update_online_store with list_append operation.""" + + table_name = f"{TABLE_NAME}_update_list_append" + create_test_table(PROJECT, table_name, REGION) + + # Create initial data with existing transactions + initial_data = [ + ( + _create_entity_key("entity1"), + {"transactions": _create_string_list_value(["tx1", "tx2"])}, + datetime.utcnow(), + None, + ) + ] + + # Write initial data using standard method + dynamodb_online_store.online_write_batch( + repo_config, MockFeatureView(name=table_name), initial_data, None + ) + + # Update with list_append - should append new transaction + update_data = [ + ( + _create_entity_key("entity1"), + {"transactions": _create_string_list_value(["tx3"])}, + datetime.utcnow(), + None, + ) + ] + + update_expressions = {"transactions": "list_append(transactions, :new_val)"} + + dynamodb_online_store.update_online_store( + repo_config, + MockFeatureView(name=table_name), + update_data, + update_expressions, + None, + ) + + # Verify result - should have all three transactions + result = dynamodb_online_store.online_read( + repo_config, MockFeatureView(name=table_name), [_create_entity_key("entity1")] + ) + + assert len(result) == 1 + assert result[0][0] is not None # timestamp should exist + assert result[0][1] is not None # features should exist + transactions = result[0][1]["transactions"] + assert _extract_string_list(transactions) == ["tx1", "tx2", "tx3"] + + +@mock_dynamodb +def test_dynamodb_update_online_store_list_prepend(repo_config, dynamodb_online_store): + """Test DynamoDB update_online_store with list prepend operation.""" + + table_name = f"{TABLE_NAME}_update_list_prepend" + create_test_table(PROJECT, table_name, REGION) + + # Create initial data + initial_data = [ + ( + _create_entity_key("entity1"), + {"recent_items": _create_string_list_value(["item2", "item3"])}, + datetime.utcnow(), + None, + ) + ] + + dynamodb_online_store.online_write_batch( + repo_config, MockFeatureView(name=table_name), initial_data, None + ) + + # Update with list prepend - should add new item at the beginning + update_data = [ + ( + _create_entity_key("entity1"), + {"recent_items": _create_string_list_value(["item1"])}, + datetime.utcnow(), + None, + ) + ] + + update_expressions = {"recent_items": "list_append(:new_val, recent_items)"} + + dynamodb_online_store.update_online_store( + repo_config, + MockFeatureView(name=table_name), + update_data, + update_expressions, + None, + ) + + # Verify result - new item should be first + result = dynamodb_online_store.online_read( + repo_config, MockFeatureView(name=table_name), [_create_entity_key("entity1")] + ) + + assert len(result) == 1 + recent_items = result[0][1]["recent_items"] + assert _extract_string_list(recent_items) == ["item1", "item2", "item3"] + + +@mock_dynamodb +def test_dynamodb_update_online_store_new_entity(repo_config, dynamodb_online_store): + """Test DynamoDB update_online_store with new entity (no existing data).""" + + table_name = f"{TABLE_NAME}_update_new_entity" + create_test_table(PROJECT, table_name, REGION) + + # Update entity that doesn't exist yet - should create new item + update_data = [ + ( + _create_entity_key("new_entity"), + {"transactions": _create_string_list_value(["tx1"])}, + datetime.utcnow(), + None, + ) + ] + + update_expressions = {"transactions": "list_append(transactions, :new_val)"} + + dynamodb_online_store.update_online_store( + repo_config, + MockFeatureView(name=table_name), + update_data, + update_expressions, + None, + ) + + # Verify result - should create new item with the transaction + result = dynamodb_online_store.online_read( + repo_config, + MockFeatureView(name=table_name), + [_create_entity_key("new_entity")], + ) + + assert len(result) == 1 + assert result[0][0] is not None # timestamp should exist + assert result[0][1] is not None # features should exist + transactions = result[0][1]["transactions"] + assert _extract_string_list(transactions) == ["tx1"] + + +@mock_dynamodb +def test_dynamodb_update_online_store_mixed_operations( + repo_config, dynamodb_online_store +): + """Test DynamoDB update_online_store with mixed update and replace operations.""" + + table_name = f"{TABLE_NAME}_update_mixed" + create_test_table(PROJECT, table_name, REGION) + + # Create initial data + initial_data = [ + ( + _create_entity_key("entity1"), + { + "transactions": _create_string_list_value(["tx1"]), + "user_score": ValueProto(int32_val=100), + }, + datetime.utcnow(), + None, + ) + ] + + dynamodb_online_store.online_write_batch( + repo_config, MockFeatureView(name=table_name), initial_data, None + ) + + # Update with mixed operations - append to list and replace scalar + update_data = [ + ( + _create_entity_key("entity1"), + { + "transactions": _create_string_list_value(["tx2"]), + "user_score": ValueProto(int32_val=150), + }, + datetime.utcnow(), + None, + ) + ] + + update_expressions = { + "transactions": "list_append(transactions, :new_val)", + # user_score will use standard replacement (no expression) + } + + dynamodb_online_store.update_online_store( + repo_config, + MockFeatureView(name=table_name), + update_data, + update_expressions, + None, + ) + + # Verify result + result = dynamodb_online_store.online_read( + repo_config, MockFeatureView(name=table_name), [_create_entity_key("entity1")] + ) + + assert len(result) == 1 + features = result[0][1] + + # Transactions should be appended + transactions = features["transactions"] + assert _extract_string_list(transactions) == ["tx1", "tx2"] + + # User score should be replaced + user_score = features["user_score"] + assert user_score.int32_val == 150 + + +@mock_dynamodb +def test_dynamodb_update_online_store_int_list(repo_config, dynamodb_online_store): + """Test DynamoDB update_online_store with integer list.""" + + table_name = f"{TABLE_NAME}_update_int_list" + create_test_table(PROJECT, table_name, REGION) + + # Create initial data with integer list + initial_data = [ + ( + _create_entity_key("entity1"), + {"scores": _create_int32_list_value([10, 20])}, + datetime.utcnow(), + None, + ) + ] + + dynamodb_online_store.online_write_batch( + repo_config, MockFeatureView(name=table_name), initial_data, None + ) + + # Update with list_append for integer list + update_data = [ + ( + _create_entity_key("entity1"), + {"scores": _create_int32_list_value([30])}, + datetime.utcnow(), + None, + ) + ] + + update_expressions = {"scores": "list_append(scores, :new_val)"} + + dynamodb_online_store.update_online_store( + repo_config, + MockFeatureView(name=table_name), + update_data, + update_expressions, + None, + ) + + # Verify result + result = dynamodb_online_store.online_read( + repo_config, MockFeatureView(name=table_name), [_create_entity_key("entity1")] + ) + + assert len(result) == 1 + scores = result[0][1]["scores"] + assert _extract_int32_list(scores) == [10, 20, 30] + + +@mock_dynamodb +def test_dynamodb_online_store_online_read_empty_entities( + repo_config, dynamodb_online_store +): + """Test DynamoDBOnlineStore online_read with empty entity list.""" + db_table_name = f"{TABLE_NAME}_empty_entities" + create_test_table(PROJECT, db_table_name, REGION) + + returned_items = dynamodb_online_store.online_read( + config=repo_config, + table=MockFeatureView(name=db_table_name), + entity_keys=[], + ) + assert returned_items == [] + + +@mock_dynamodb +def test_dynamodb_online_store_online_read_parallel_batches( + repo_config, dynamodb_online_store +): + """Test DynamoDBOnlineStore online_read with multiple batches (parallel execution). + + With batch_size=100 (default), 250 entities should create 3 batches + that are executed in parallel via ThreadPoolExecutor. + """ + n_samples = 250 + db_table_name = f"{TABLE_NAME}_parallel_batches" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + returned_items = dynamodb_online_store.online_read( + config=repo_config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + ) + + # Verify all items returned + assert len(returned_items) == n_samples + # Verify order is preserved + assert [item[1] for item in returned_items] == list(features) + + +@mock_dynamodb +def test_dynamodb_online_store_online_read_single_batch_no_parallel( + repo_config, dynamodb_online_store +): + """Test DynamoDBOnlineStore online_read with single batch (no parallelization). + + With batch_size=100, 50 entities should use single batch path + without ThreadPoolExecutor overhead. + """ + n_samples = 50 + db_table_name = f"{TABLE_NAME}_single_batch" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + returned_items = dynamodb_online_store.online_read( + config=repo_config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + ) + + assert len(returned_items) == n_samples + assert [item[1] for item in returned_items] == list(features) + + +@mock_dynamodb +def test_dynamodb_online_store_online_read_order_preservation_across_batches( + repo_config, dynamodb_online_store +): + """Test that entity order is preserved across parallel batch reads. + + This is critical: parallel execution must not change the order of results. + """ + n_samples = 150 # 2 batches with batch_size=100 + db_table_name = f"{TABLE_NAME}_order_preservation" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + + # Read multiple times to verify consistent ordering + for _ in range(3): + returned_items = dynamodb_online_store.online_read( + config=repo_config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + ) + assert len(returned_items) == n_samples + # Verify exact order matches + for i, (returned, expected) in enumerate(zip(returned_items, features)): + assert returned[1] == expected, f"Mismatch at index {i}" + + +@mock_dynamodb +def test_dynamodb_online_store_online_read_small_batch_size(dynamodb_online_store): + """Test parallel reads with small batch_size. + + Verifies correctness with small batch sizes that create multiple batches. + """ + small_batch_config = RepoConfig( + registry=REGISTRY, + project=PROJECT, + provider=PROVIDER, + online_store=DynamoDBOnlineStoreConfig(region=REGION, batch_size=5), + offline_store=DaskOfflineStoreConfig(), + entity_key_serialization_version=3, + ) + + n_samples = 25 # 5 batches with batch_size=5 + db_table_name = f"{TABLE_NAME}_small_batch" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + returned_items = dynamodb_online_store.online_read( + config=small_batch_config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + ) + + assert len(returned_items) == n_samples + assert [item[1] for item in returned_items] == list(features) + + +@mock_dynamodb +def test_dynamodb_online_store_online_read_many_batches(dynamodb_online_store): + """Test parallel reads with many batches (>10). + + Verifies correctness when number of batches exceeds max_workers cap. + """ + many_batch_config = RepoConfig( + registry=REGISTRY, + project=PROJECT, + provider=PROVIDER, + online_store=DynamoDBOnlineStoreConfig(region=REGION, batch_size=10), + offline_store=DaskOfflineStoreConfig(), + entity_key_serialization_version=3, + ) + + n_samples = 150 # 15 batches with batch_size=10 + db_table_name = f"{TABLE_NAME}_many_batches" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + returned_items = dynamodb_online_store.online_read( + config=many_batch_config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + ) + + assert len(returned_items) == n_samples + assert [item[1] for item in returned_items] == list(features) + + +@mock_dynamodb +def test_dynamodb_online_store_max_workers_capped_at_config(dynamodb_online_store): + """Verify ThreadPoolExecutor max_workers uses max_read_workers config. + + Bug: Old code used min(len(batches), batch_size) which fails with small batch_size. + Fix: New code uses min(len(batches), max_read_workers) for proper parallelization. + + This test uses batch_size=5 with 15 batches to expose the bug: + - OLD (buggy): max_workers = min(15, 5) = 5 (insufficient parallelism) + - NEW (fixed): max_workers = min(15, 10) = 10 (uses max_read_workers default) + """ + # Use small batch_size to expose the bug + small_batch_config = RepoConfig( + registry=REGISTRY, + project=PROJECT, + provider=PROVIDER, + online_store=DynamoDBOnlineStoreConfig(region=REGION, batch_size=5), + offline_store=DaskOfflineStoreConfig(), + entity_key_serialization_version=3, + ) + + n_samples = 75 # 15 batches with batch_size=5 + db_table_name = f"{TABLE_NAME}_max_workers_cap" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + + with patch( + "feast.infra.online_stores.dynamodb.ThreadPoolExecutor" + ) as mock_executor: + # Configure mock to work like real ThreadPoolExecutor + mock_executor.return_value.__enter__.return_value.map.return_value = iter( + [{"Responses": {}} for _ in range(15)] + ) + + dynamodb_online_store.online_read( + config=small_batch_config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + ) + + # Verify ThreadPoolExecutor was called with max_workers=10 (capped at 10, NOT batch_size=5) + mock_executor.assert_called_once() + call_kwargs = mock_executor.call_args + assert call_kwargs[1]["max_workers"] == 10, ( + f"Expected max_workers=10 (capped), got {call_kwargs[1]['max_workers']}. " + f"If got 5, the bug is using batch_size instead of 10 as cap." + ) + + +@mock_dynamodb +def test_dynamodb_online_store_thread_safety_uses_shared_client( + dynamodb_online_store, +): + """Verify multi-batch reads use a shared thread-safe boto3 client. + + boto3 clients ARE thread-safe, so we share a single client across threads + for better performance (avoids creating new sessions per thread). + https://docs.aws.amazon.com/boto3/latest/guide/clients.html#multithreading-or-multiprocessing-with-clients + """ + config = RepoConfig( + registry=REGISTRY, + project=PROJECT, + provider=PROVIDER, + online_store=DynamoDBOnlineStoreConfig(region=REGION, batch_size=50), + offline_store=DaskOfflineStoreConfig(), + entity_key_serialization_version=3, + ) + + n_samples = 150 # 3 batches + db_table_name = f"{TABLE_NAME}_thread_safety" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + + # Track clients created to verify thread-safety via shared client + clients_created = [] + original_client = boto3.client + + def tracking_client(*args, **kwargs): + client = original_client(*args, **kwargs) + clients_created.append(id(client)) + return client + + with patch.object(boto3, "client", side_effect=tracking_client): + returned_items = dynamodb_online_store.online_read( + config=config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + ) + + # Verify results are correct (functional correctness) + assert len(returned_items) == n_samples + + # Verify only one client was created (shared across threads) + # The client is cached and reused for all batch requests + dynamodb_clients = [c for c in clients_created] + assert len(set(dynamodb_clients)) == 1, ( + f"Expected 1 shared client for thread-safety, " + f"got {len(set(dynamodb_clients))} unique clients" + ) + + +@mock_dynamodb +def test_dynamodb_online_store_online_read_with_requested_features( + repo_config, dynamodb_online_store +): + """Test that requested_features filters returned features.""" + n_samples = 5 + db_table_name = f"{TABLE_NAME}_requested_features" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + returned_items = dynamodb_online_store.online_read( + config=repo_config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + requested_features=["name", "age"], + ) + assert len(returned_items) == n_samples + for _, feat_dict in returned_items: + assert feat_dict is not None + assert "name" in feat_dict + assert "age" in feat_dict + assert "avg_orders_day" not in feat_dict + + +@mock_dynamodb +def test_dynamodb_online_store_online_read_without_requested_features( + repo_config, dynamodb_online_store +): + """Test that omitting requested_features returns all features.""" + n_samples = 5 + db_table_name = f"{TABLE_NAME}_all_features" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + returned_items = dynamodb_online_store.online_read( + config=repo_config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + requested_features=None, + ) + assert len(returned_items) == n_samples + for _, feat_dict in returned_items: + assert feat_dict is not None + assert set(feat_dict.keys()) == {"avg_orders_day", "name", "age"} + + +def test_build_projection_expression(): + """Test that _build_projection_expression generates correct DynamoDB expressions.""" + result = DynamoDBOnlineStore._build_projection_expression(["feat_a", "feat_b"]) + assert result is not None + assert "#entity_id" in result["ProjectionExpression"] + assert "#event_ts" in result["ProjectionExpression"] + assert "#vals.#feat0" in result["ProjectionExpression"] + assert "#vals.#feat1" in result["ProjectionExpression"] + attr_names = result["ExpressionAttributeNames"] + assert attr_names["#vals"] == "values" + assert attr_names["#feat0"] == "feat_a" + assert attr_names["#feat1"] == "feat_b" + + +def test_build_projection_expression_none(): + """Test that _build_projection_expression returns None for empty input.""" + assert DynamoDBOnlineStore._build_projection_expression(None) is None + assert DynamoDBOnlineStore._build_projection_expression([]) is None + + +@mock_dynamodb +def test_dynamodb_online_store_online_read_requested_features_parallel( + dynamodb_online_store, +): + """Test that requested_features works across parallel batches.""" + small_batch_config = RepoConfig( + registry=REGISTRY, + project=PROJECT, + provider=PROVIDER, + online_store=DynamoDBOnlineStoreConfig(region=REGION, batch_size=5), + offline_store=DaskOfflineStoreConfig(), + entity_key_serialization_version=3, + ) + n_samples = 15 + db_table_name = f"{TABLE_NAME}_requested_parallel" + create_test_table(PROJECT, db_table_name, REGION) + data = create_n_customer_test_samples(n=n_samples) + insert_data_test_table(data, PROJECT, db_table_name, REGION) + + entity_keys, features, *rest = zip(*data) + returned_items = dynamodb_online_store.online_read( + config=small_batch_config, + table=MockFeatureView(name=db_table_name), + entity_keys=entity_keys, + requested_features=["age"], + ) + assert len(returned_items) == n_samples + for _, feat_dict in returned_items: + assert feat_dict is not None + assert "age" in feat_dict + assert "name" not in feat_dict + assert "avg_orders_day" not in feat_dict + + +# --------------------------------------------------------------------------- +# Tests for the diff-based _update_tags (fix for GitHub issue #6418) +# +# DynamoDB's TagResource / UntagResource are eventually consistent, so the +# old approach of "remove all → re-add all" could leave tables tag-less due +# to the async propagation gap. The new approach only touches tags that +# actually need to change. +# --------------------------------------------------------------------------- + + +def _make_mock_dynamodb_client( + current_tags: list[dict[str, str]], + table_arn: str = "arn:aws:dynamodb:us-west-2:123456789012:table/test_table", +): + """Helper: create a pure MagicMock DynamoDB client for tag tests. + This avoids Moto's authentication issues with DynamoDB tag APIs. + """ + from unittest.mock import MagicMock + + client = MagicMock() + # Mock describe_table + client.describe_table.return_value = {"Table": {"TableArn": table_arn}} + # Mock list_tags_of_resource + client.list_tags_of_resource.return_value = {"Tags": current_tags} + return client + + +def test_update_tags_no_change_makes_no_api_calls(): + """Idempotency: calling _update_tags with identical tags must not call + untag_resource or tag_resource (avoids unnecessary eventual-consistency exposure). + """ + table_name = f"{TABLE_NAME}_no_change" + initial_tags = [{"Key": "env", "Value": "prod"}, {"Key": "team", "Value": "ml"}] + client = _make_mock_dynamodb_client(current_tags=initial_tags) + + DynamoDBOnlineStore._update_tags(client, table_name, initial_tags) + + client.untag_resource.assert_not_called() + client.tag_resource.assert_not_called() + + +def test_update_tags_adds_new_tags_without_untag(): + """Adding a new tag key must call tag_resource but NOT untag_resource.""" + table_name = f"{TABLE_NAME}_add_only" + existing = [{"Key": "env", "Value": "prod"}] + client = _make_mock_dynamodb_client(current_tags=existing) + + new_tags = [{"Key": "env", "Value": "prod"}, {"Key": "team", "Value": "ml"}] + DynamoDBOnlineStore._update_tags(client, table_name, new_tags) + + client.untag_resource.assert_not_called() + client.tag_resource.assert_called_once() + added = client.tag_resource.call_args[1]["Tags"] + assert added == [{"Key": "team", "Value": "ml"}] + + +def test_update_tags_removes_obsolete_tags_only(): + """Only truly obsolete tag keys (not in new_tags) must be passed to untag_resource.""" + table_name = f"{TABLE_NAME}_remove_only" + existing = [{"Key": "env", "Value": "prod"}, {"Key": "old-key", "Value": "old"}] + client = _make_mock_dynamodb_client(current_tags=existing) + + new_tags = [{"Key": "env", "Value": "prod"}] + DynamoDBOnlineStore._update_tags(client, table_name, new_tags) + + client.untag_resource.assert_called_once() + removed_keys = client.untag_resource.call_args[1]["TagKeys"] + assert removed_keys == ["old-key"] + + client.tag_resource.assert_not_called() + + +def test_update_tags_updates_changed_value(): + """A tag whose value changed must be passed to tag_resource (not untag_resource).""" + table_name = f"{TABLE_NAME}_change_value" + existing = [{"Key": "env", "Value": "staging"}] + client = _make_mock_dynamodb_client(current_tags=existing) + + new_tags = [{"Key": "env", "Value": "prod"}] + + DynamoDBOnlineStore._update_tags(client, table_name, new_tags) + + client.untag_resource.assert_not_called() + client.tag_resource.assert_called_once() + updated = client.tag_resource.call_args[1]["Tags"] + assert updated == [{"Key": "env", "Value": "prod"}] + + +def test_update_tags_full_replace(): + """Replacing an entire tag set removes old keys and adds new ones correctly.""" + table_name = f"{TABLE_NAME}_full_replace" + existing = [{"Key": "old1", "Value": "v1"}, {"Key": "old2", "Value": "v2"}] + client = _make_mock_dynamodb_client(current_tags=existing) + + new_tags = [{"Key": "new1", "Value": "n1"}, {"Key": "new2", "Value": "n2"}] + + DynamoDBOnlineStore._update_tags(client, table_name, new_tags) + + client.untag_resource.assert_called_once() + removed = set(client.untag_resource.call_args[1]["TagKeys"]) + assert removed == {"old1", "old2"} + + client.tag_resource.assert_called_once() + added = {t["Key"] for t in client.tag_resource.call_args[1]["Tags"]} + assert added == {"new1", "new2"} + + +def test_update_tags_clear_all_tags(): + """Passing an empty new_tags list removes all existing tags without calling tag_resource.""" + table_name = f"{TABLE_NAME}_clear_all" + existing = [{"Key": "env", "Value": "prod"}] + client = _make_mock_dynamodb_client(current_tags=existing) + + DynamoDBOnlineStore._update_tags(client, table_name, []) + + client.untag_resource.assert_called_once() + removed = client.untag_resource.call_args[1]["TagKeys"] + assert removed == ["env"] + + client.tag_resource.assert_not_called() + + +def test_update_tags_no_existing_tags_no_new_tags(): + """Both old and new tag sets are empty: no API calls at all.""" + table_name = f"{TABLE_NAME}_empty_both" + client = _make_mock_dynamodb_client(current_tags=[]) + + DynamoDBOnlineStore._update_tags(client, table_name, []) + + client.untag_resource.assert_not_called() + client.tag_resource.assert_not_called() diff --git a/sdk/python/tests/unit/infra/online_store/test_dynamodb_plan.py b/sdk/python/tests/unit/infra/online_store/test_dynamodb_plan.py new file mode 100644 index 00000000000..bb65a2adc5c --- /dev/null +++ b/sdk/python/tests/unit/infra/online_store/test_dynamodb_plan.py @@ -0,0 +1,153 @@ +from datetime import timedelta + +from feast.data_format import AvroFormat +from feast.data_source import KafkaSource +from feast.feature_view import FeatureView +from feast.field import Field +from feast.infra.infra_object import Infra +from feast.infra.offline_stores.dask import DaskOfflineStoreConfig +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.online_stores.dynamodb import ( + DynamoDBOnlineStore, + DynamoDBOnlineStoreConfig, + DynamoDBTable, +) +from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto +from feast.repo_config import RepoConfig +from feast.stream_feature_view import StreamFeatureView +from feast.types import String + +REGISTRY = "s3://test_registry/registry.db" +PROJECT = "test_aws" +PROVIDER = "aws" +REGION = "us-west-2" + + +def _repo_config() -> RepoConfig: + return RepoConfig( + registry=REGISTRY, + project=PROJECT, + provider=PROVIDER, + online_store=DynamoDBOnlineStoreConfig(region=REGION), + offline_store=DaskOfflineStoreConfig(), + entity_key_serialization_version=3, + ) + + +def _feature_view(name: str) -> FeatureView: + return FeatureView( + name=name, + entities=[], + schema=[Field(name="value", dtype=String)], + ttl=timedelta(days=1), + online=True, + source=FileSource(path="dummy.parquet", timestamp_field="event_timestamp"), + ) + + +def _stream_feature_view(name: str) -> StreamFeatureView: + return StreamFeatureView( + name=name, + entities=[], + schema=[Field(name="value", dtype=String)], + source=KafkaSource( + name="dummy_kafka", + timestamp_field="event_timestamp", + message_format=AvroFormat(""), + kafka_bootstrap_servers="localhost:9092", + topic="dummy_topic", + batch_source=FileSource( + path="dummy.parquet", timestamp_field="event_timestamp" + ), + ), + ) + + +class TestDynamoDBOnlineStorePlan: + """DynamoDBOnlineStore previously had no plan() override at all, so + `feast plan` reported no DynamoDB infrastructure changes. This adds one, + following the InfraObject.CustomInfra extension point (see + protos/feast/core/InfraObject.proto) used by no other in-tree store yet.""" + + def test_plan_returns_one_table_per_feature_view(self): + config = _repo_config() + registry_proto = RegistryProto() + registry_proto.feature_views.append(_feature_view("view_a").to_proto()) + registry_proto.feature_views.append(_feature_view("view_b").to_proto()) + + infra_objects = DynamoDBOnlineStore().plan(config, registry_proto) + + assert sorted(o.name for o in infra_objects) == [ + f"{PROJECT}.view_a", + f"{PROJECT}.view_b", + ] + assert all(isinstance(o, DynamoDBTable) for o in infra_objects) + assert all(o.region == REGION for o in infra_objects) + + def test_plan_includes_stream_feature_views(self): + """Regression guard: FeatureView.from_proto() is @typechecked and + only accepts a FeatureViewProto, so it can't be applied to + stream_feature_views (StreamFeatureViewProto) too -- each list needs + its matching class (see the related fix in sqlite.py's plan()).""" + config = _repo_config() + registry_proto = RegistryProto() + registry_proto.stream_feature_views.append( + _stream_feature_view("driver_dropoffs_stream").to_proto() + ) + + infra_objects = DynamoDBOnlineStore().plan(config, registry_proto) + + assert [o.name for o in infra_objects] == [f"{PROJECT}.driver_dropoffs_stream"] + + def test_plan_includes_batch_and_stream_feature_views_together(self): + config = _repo_config() + registry_proto = RegistryProto() + registry_proto.feature_views.append(_feature_view("batch_view").to_proto()) + registry_proto.stream_feature_views.append( + _stream_feature_view("driver_dropoffs_stream").to_proto() + ) + + infra_objects = DynamoDBOnlineStore().plan(config, registry_proto) + + assert sorted(o.name for o in infra_objects) == [ + f"{PROJECT}.batch_view", + f"{PROJECT}.driver_dropoffs_stream", + ] + + def test_plan_empty_registry_produces_empty_plan(self): + config = _repo_config() + registry_proto = RegistryProto() + + assert DynamoDBOnlineStore().plan(config, registry_proto) == [] + + +class TestDynamoDBTableProtoRoundTrip: + """This is the mechanism `feast plan` relies on: Infra gets serialized to + proto and deserialized back via the dotted class_type path, not a + hardcoded per-store branch.""" + + def test_infra_object_survives_proto_round_trip(self): + original = DynamoDBTable( + name=f"{PROJECT}.view_a", region=REGION, endpoint_url=None + ) + infra = Infra(infra_objects=[original]) + + restored = Infra.from_proto(infra.to_proto()) + + assert len(restored.infra_objects) == 1 + restored_table = restored.infra_objects[0] + assert isinstance(restored_table, DynamoDBTable) + assert restored_table.name == f"{PROJECT}.view_a" + assert restored_table.region == REGION + + def test_round_trip_preserves_endpoint_url(self): + original = DynamoDBTable( + name=f"{PROJECT}.view_a", + region=REGION, + endpoint_url="http://localhost:8000", + ) + infra = Infra(infra_objects=[original]) + + restored = Infra.from_proto(infra.to_proto()) + + assert restored.infra_objects[0].endpoint_url == "http://localhost:8000" diff --git a/sdk/python/tests/unit/infra/online_store/test_dynamodb_versioning.py b/sdk/python/tests/unit/infra/online_store/test_dynamodb_versioning.py new file mode 100644 index 00000000000..492d68494d8 --- /dev/null +++ b/sdk/python/tests/unit/infra/online_store/test_dynamodb_versioning.py @@ -0,0 +1,128 @@ +"""Unit tests for DynamoDB online store feature view versioning.""" + +from datetime import timedelta +from unittest.mock import MagicMock + +from feast import Entity, FeatureView +from feast.field import Field +from feast.types import Float32 +from feast.value_type import ValueType + + +def _make_feature_view(name="driver_stats", version_number=None, version_tag=None): + entity = Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + fv = FeatureView( + name=name, + entities=[entity], + ttl=timedelta(days=1), + schema=[Field(name="trips_today", dtype=Float32)], + ) + if version_number is not None: + fv.current_version_number = version_number + if version_tag is not None: + fv.projection.version_tag = version_tag + return fv + + +def _make_config(project="test_project", versioning=False): + config = MagicMock() + config.project = project + config.entity_key_serialization_version = 2 + config.registry.enable_online_feature_view_versioning = versioning + return config + + +def _make_online_config(template="{project}.{table_name}"): + online_config = MagicMock() + online_config.table_name_template = template + return online_config + + +class TestGetTableName: + """Test _get_table_name with versioning enabled/disabled.""" + + def test_no_versioning(self): + from feast.infra.online_stores.dynamodb import _get_table_name + + fv = _make_feature_view() + config = _make_config(versioning=False) + online_config = _make_online_config() + assert _get_table_name(online_config, config, fv) == "test_project.driver_stats" + + def test_versioning_enabled_with_version(self): + from feast.infra.online_stores.dynamodb import _get_table_name + + fv = _make_feature_view(version_number=2) + config = _make_config(versioning=True) + online_config = _make_online_config() + assert ( + _get_table_name(online_config, config, fv) == "test_project.driver_stats_v2" + ) + + def test_projection_version_tag_takes_priority(self): + from feast.infra.online_stores.dynamodb import _get_table_name + + fv = _make_feature_view(version_number=1, version_tag=3) + config = _make_config(versioning=True) + online_config = _make_online_config() + assert ( + _get_table_name(online_config, config, fv) == "test_project.driver_stats_v3" + ) + + def test_version_zero_no_suffix(self): + from feast.infra.online_stores.dynamodb import _get_table_name + + fv = _make_feature_view(version_number=0) + config = _make_config(versioning=True) + online_config = _make_online_config() + assert _get_table_name(online_config, config, fv) == "test_project.driver_stats" + + def test_versioning_enabled_no_version_set(self): + from feast.infra.online_stores.dynamodb import _get_table_name + + fv = _make_feature_view() + config = _make_config(versioning=True) + online_config = _make_online_config() + assert _get_table_name(online_config, config, fv) == "test_project.driver_stats" + + def test_custom_template_with_versioning(self): + from feast.infra.online_stores.dynamodb import _get_table_name + + fv = _make_feature_view(version_number=2) + config = _make_config(project="prod", versioning=True) + online_config = _make_online_config(template="feast-{project}-{table_name}") + assert ( + _get_table_name(online_config, config, fv) == "feast-prod-driver_stats_v2" + ) + + def test_versioning_disabled_ignores_version(self): + from feast.infra.online_stores.dynamodb import _get_table_name + + fv = _make_feature_view(version_number=5) + config = _make_config(versioning=False) + online_config = _make_online_config() + assert _get_table_name(online_config, config, fv) == "test_project.driver_stats" + + +class TestDynamoDBVersionedReadSupport: + """Test that DynamoDBOnlineStore passes _check_versioned_read_support.""" + + def test_allowed_with_version_tag(self): + from feast.infra.online_stores.dynamodb import DynamoDBOnlineStore + + store = DynamoDBOnlineStore() + fv = _make_feature_view() + fv.projection.version_tag = 2 + # Should not raise + store._check_versioned_read_support([(fv, ["trips_today"])]) + + def test_allowed_without_version_tag(self): + from feast.infra.online_stores.dynamodb import DynamoDBOnlineStore + + store = DynamoDBOnlineStore() + fv = _make_feature_view() + store._check_versioned_read_support([(fv, ["trips_today"])]) diff --git a/sdk/python/tests/unit/infra/online_store/test_faiss_versioning.py b/sdk/python/tests/unit/infra/online_store/test_faiss_versioning.py new file mode 100644 index 00000000000..84b0aa24e99 --- /dev/null +++ b/sdk/python/tests/unit/infra/online_store/test_faiss_versioning.py @@ -0,0 +1,249 @@ +"""Unit tests for FAISS online store feature view versioning.""" + +import sys +from datetime import timedelta +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest + +from feast import Entity, FeatureView +from feast.field import Field +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.types import Float32 +from feast.value_type import ValueType + + +def _make_feature_view(name="driver_stats", version_number=None, version_tag=None): + entity = Entity( + name="driver_id", + join_keys=["driver_id"], + value_type=ValueType.INT64, + ) + fv = FeatureView( + name=name, + entities=[entity], + ttl=timedelta(days=1), + schema=[Field(name="feature_a", dtype=Float32)], + ) + if version_number is not None: + fv.current_version_number = version_number + if version_tag is not None: + fv.projection.version_tag = version_tag + return fv + + +@pytest.fixture(autouse=True) +def _mock_faiss(): + """Inject a minimal faiss mock so faiss_online_store can be imported.""" + faiss_mock = MagicMock() + with patch.dict(sys.modules, {"faiss": faiss_mock}): + sys.modules.pop("feast.infra.online_stores.faiss_online_store", None) + yield faiss_mock + sys.modules.pop("feast.infra.online_stores.faiss_online_store", None) + + +class TestFaissTableId: + """Test _table_id generates correct versioned table names.""" + + def test_default_no_versioning(self): + from feast.infra.online_stores.faiss_online_store import _table_id + + fv = _make_feature_view() + assert _table_id("proj", fv) == "proj_driver_stats" + + def test_versioning_explicitly_disabled(self): + from feast.infra.online_stores.faiss_online_store import _table_id + + fv = _make_feature_view(version_number=3) + assert _table_id("proj", fv, enable_versioning=False) == "proj_driver_stats" + + def test_versioning_enabled_no_version_set(self): + from feast.infra.online_stores.faiss_online_store import _table_id + + fv = _make_feature_view() + assert _table_id("proj", fv, enable_versioning=True) == "proj_driver_stats" + + def test_versioning_enabled_with_current_version_number(self): + from feast.infra.online_stores.faiss_online_store import _table_id + + fv = _make_feature_view(version_number=2) + assert _table_id("proj", fv, enable_versioning=True) == "proj_driver_stats_v2" + + def test_version_zero_no_suffix(self): + from feast.infra.online_stores.faiss_online_store import _table_id + + fv = _make_feature_view(version_number=0) + assert _table_id("proj", fv, enable_versioning=True) == "proj_driver_stats" + + def test_projection_version_tag_takes_priority(self): + from feast.infra.online_stores.faiss_online_store import _table_id + + fv = _make_feature_view(version_number=1, version_tag=3) + assert _table_id("proj", fv, enable_versioning=True) == "proj_driver_stats_v3" + + def test_projection_version_tag_zero_no_suffix(self): + from feast.infra.online_stores.faiss_online_store import _table_id + + fv = _make_feature_view(version_tag=0, version_number=3) + assert _table_id("proj", fv, enable_versioning=True) == "proj_driver_stats" + + def test_different_project_names(self): + from feast.infra.online_stores.faiss_online_store import _table_id + + fv = _make_feature_view(version_number=1) + assert _table_id("prod", fv, enable_versioning=True) == "prod_driver_stats_v1" + assert ( + _table_id("staging", fv, enable_versioning=True) + == "staging_driver_stats_v1" + ) + + def test_different_feature_view_names(self): + from feast.infra.online_stores.faiss_online_store import _table_id + + fv = _make_feature_view(name="user_stats", version_number=2) + assert _table_id("proj", fv, enable_versioning=True) == "proj_user_stats_v2" + + +class TestFaissVersionedReadSupport: + """Test that FaissOnlineStore passes _check_versioned_read_support.""" + + def test_allowed_with_version_tag(self): + from feast.infra.online_stores.faiss_online_store import FaissOnlineStore + + store = FaissOnlineStore() + fv = _make_feature_view() + fv.projection.version_tag = 2 + store._check_versioned_read_support([(fv, ["feature_a"])]) + + def test_allowed_without_version_tag(self): + from feast.infra.online_stores.faiss_online_store import FaissOnlineStore + + store = FaissOnlineStore() + fv = _make_feature_view() + store._check_versioned_read_support([(fv, ["feature_a"])]) + + +def _make_config(project="test_project", versioning=False): + """Build a minimal RepoConfig-like mock.""" + config = MagicMock() + config.project = project + config.entity_key_serialization_version = 2 + config.online_store.dict.return_value = { + "dimension": 1, + "index_path": "/tmp/test.index", + "index_type": "IVFFlat", + "nlist": 10, + } + config.registry.enable_online_feature_view_versioning = versioning + return config + + +def _make_entity_key(driver_id=1): + return EntityKeyProto( + join_keys=["driver_id"], + entity_values=[ValueProto(int64_val=driver_id)], + ) + + +class TestFaissOnlineStoreVersionedReadWrite: + def _make_store(self, faiss_mock, nlist=10): + """Create a FaissOnlineStore with a real-enough faiss mock.""" + index_mock = MagicMock() + index_mock.ntotal = 0 + + def add_side_effect(vectors): + index_mock.ntotal += len(vectors) + + index_mock.add.side_effect = add_side_effect + + def reconstruct_side_effect(idx): + return np.array([float(idx)], dtype=np.float32) + + index_mock.reconstruct.side_effect = reconstruct_side_effect + + faiss_mock.IndexFlatL2.return_value = MagicMock() + faiss_mock.IndexIVFFlat.return_value = index_mock + + from feast.infra.online_stores.faiss_online_store import FaissOnlineStore + + store = FaissOnlineStore() + return store, index_mock + + def test_write_and_read_without_versioning(self, _mock_faiss): + store, _ = self._make_store(_mock_faiss) + config = _make_config(versioning=False) + fv = _make_feature_view() + + store.update(config, [], [fv], [], [], partial=False) + + entity_key = _make_entity_key(driver_id=42) + data = [(entity_key, {"feature_a": ValueProto(double_val=1.5)}, None, None)] + store.online_write_batch(config, fv, data, None) + + results = store.online_read(config, fv, [entity_key]) + assert len(results) == 1 + _, feature_dict = results[0] + assert feature_dict is not None + assert "feature_a" in feature_dict + + def test_write_and_read_with_versioning(self, _mock_faiss): + store, _ = self._make_store(_mock_faiss) + config = _make_config(versioning=True) + fv_v2 = _make_feature_view(version_number=2) + + store.update(config, [], [fv_v2], [], [], partial=False) + + entity_key = _make_entity_key(driver_id=7) + data = [(entity_key, {"feature_a": ValueProto(double_val=2.0)}, None, None)] + store.online_write_batch(config, fv_v2, data, None) + + results = store.online_read(config, fv_v2, [entity_key]) + assert len(results) == 1 + _, feature_dict = results[0] + assert feature_dict is not None + + def test_versioned_namespaces_are_isolated(self, _mock_faiss): + """Data written under v1 must not be visible when reading under v2.""" + store, _ = self._make_store(_mock_faiss) + config = _make_config(versioning=True) + + fv_v1 = _make_feature_view(version_number=1) + fv_v2 = _make_feature_view(version_number=2) + + store.update(config, [], [fv_v1, fv_v2], [], [], partial=False) + + entity_key = _make_entity_key(driver_id=99) + data = [(entity_key, {"feature_a": ValueProto(double_val=9.9)}, None, None)] + store.online_write_batch(config, fv_v1, data, None) + + results_v2 = store.online_read(config, fv_v2, [entity_key]) + assert results_v2 == [(None, None)] + + results_v1 = store.online_read(config, fv_v1, [entity_key]) + assert results_v1[0][1] is not None + + def test_missing_index_returns_none(self, _mock_faiss): + store, _ = self._make_store(_mock_faiss) + config = _make_config(versioning=True) + fv = _make_feature_view(version_number=5) + entity_key = _make_entity_key(driver_id=1) + results = store.online_read(config, fv, [entity_key]) + assert results == [(None, None)] + + def test_teardown_removes_versioned_index(self, _mock_faiss): + store, _ = self._make_store(_mock_faiss) + config = _make_config(versioning=True) + fv = _make_feature_view(version_number=3) + + store.update(config, [], [fv], [], [], partial=False) + + entity_key = _make_entity_key(driver_id=1) + data = [(entity_key, {"feature_a": ValueProto(double_val=3.0)}, None, None)] + store.online_write_batch(config, fv, data, None) + + store.teardown(config, [fv], []) + + results = store.online_read(config, fv, [entity_key]) + assert results == [(None, None)] diff --git a/sdk/python/tests/unit/infra/online_store/test_helpers.py b/sdk/python/tests/unit/infra/online_store/test_helpers.py new file mode 100644 index 00000000000..2d65dff5203 --- /dev/null +++ b/sdk/python/tests/unit/infra/online_store/test_helpers.py @@ -0,0 +1,150 @@ +import struct +from datetime import datetime, timezone +from unittest.mock import MagicMock, patch + +import mmh3 + +from feast.infra.online_stores.helpers import ( + _mmh3, + _redis_key, + _redis_key_prefix, + _to_naive_utc, + compute_entity_id, + get_online_store_from_config, +) +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto + + +def _make_entity_key(join_keys, values): + """Helper to build an EntityKeyProto.""" + entity_key = EntityKeyProto() + for k in join_keys: + entity_key.join_keys.append(k) + for v in values: + entity_key.entity_values.append(v) + return entity_key + + +class TestRedisKey: + def test_redis_key_produces_bytes(self): + entity_key = _make_entity_key(["user_id"], [ValueProto(string_val="user_1")]) + result = _redis_key("my_project", entity_key) + assert isinstance(result, bytes) + + def test_redis_key_contains_project(self): + entity_key = _make_entity_key(["user_id"], [ValueProto(string_val="user_1")]) + result = _redis_key("my_project", entity_key) + assert b"my_project" in result + + def test_redis_key_different_projects_produce_different_keys(self): + entity_key = _make_entity_key(["user_id"], [ValueProto(string_val="user_1")]) + key_a = _redis_key("project_a", entity_key) + key_b = _redis_key("project_b", entity_key) + assert key_a != key_b + + def test_redis_key_different_entities_produce_different_keys(self): + ek1 = _make_entity_key(["user_id"], [ValueProto(string_val="user_1")]) + ek2 = _make_entity_key(["user_id"], [ValueProto(string_val="user_2")]) + assert _redis_key("proj", ek1) != _redis_key("proj", ek2) + + def test_redis_key_serialization_version_changes_key(self): + entity_key = _make_entity_key(["user_id"], [ValueProto(string_val="user_1")]) + key_v2 = _redis_key("proj", entity_key, entity_key_serialization_version=2) + key_v3 = _redis_key("proj", entity_key, entity_key_serialization_version=3) + assert key_v2 != key_v3 + + +class TestRedisKeyPrefix: + def test_returns_bytes(self): + result = _redis_key_prefix(["user_id"]) + assert isinstance(result, bytes) + + def test_different_keys_produce_different_prefixes(self): + prefix_a = _redis_key_prefix(["user_id"]) + prefix_b = _redis_key_prefix(["merchant_id"]) + assert prefix_a != prefix_b + + +class TestMmh3: + def test_returns_bytes(self): + result = _mmh3("test_key") + assert isinstance(result, bytes) + + def test_consistent_hash(self): + assert _mmh3("hello") == _mmh3("hello") + + def test_different_keys_different_hashes(self): + assert _mmh3("key_a") != _mmh3("key_b") + + def test_hash_length(self): + result = _mmh3("test") + assert len(result) == 4 # 32-bit hash = 4 bytes + + def test_little_endian_format(self): + """Verify the hash matches the expected little-endian encoding.""" + key = "test_key" + key_hash = mmh3.hash(key, signed=False) + expected = bytes.fromhex(struct.pack(" RepoConfig: + return RepoConfig( + registry="/tmp/unused_registry.db", + project="test_project", + provider="local", + online_store=SqliteOnlineStoreConfig(), + offline_store=DaskOfflineStoreConfig(), + entity_key_serialization_version=3, + ) + + +def _feature_view(name: str) -> FeatureView: + return FeatureView( + name=name, + entities=[], + schema=[Field(name="value", dtype=String)], + ttl=timedelta(days=1), + online=True, + source=FileSource(path="dummy.parquet", timestamp_field="event_timestamp"), + ) + + +def _stream_feature_view(name: str) -> StreamFeatureView: + return StreamFeatureView( + name=name, + entities=[], + schema=[Field(name="value", dtype=String)], + source=KafkaSource( + name="dummy_kafka", + timestamp_field="event_timestamp", + message_format=AvroFormat(""), + kafka_bootstrap_servers="localhost:9092", + topic="dummy_topic", + batch_source=FileSource( + path="dummy.parquet", timestamp_field="event_timestamp" + ), + ), + ) + + +class TestSqliteOnlineStorePlanWithStreamFeatureViews: + """Regression test for a typeguard.TypeCheckError previously raised by + plan() when the registry contains a StreamFeatureView: FeatureView.from_proto() + was applied uniformly to both feature_views and stream_feature_views, but + FeatureView is @typechecked and a StreamFeatureView proto is not a + FeatureView proto.""" + + def test_plan_succeeds_with_only_stream_feature_views(self): + config = _repo_config() + registry_proto = RegistryProto() + registry_proto.stream_feature_views.append( + _stream_feature_view("driver_dropoffs_stream").to_proto() + ) + + infra_objects = SqliteOnlineStore().plan(config, registry_proto) + + assert len(infra_objects) == 1 + assert infra_objects[0].name == "test_project_driver_dropoffs_stream" + + def test_plan_succeeds_with_batch_and_stream_feature_views_together(self): + config = _repo_config() + registry_proto = RegistryProto() + registry_proto.feature_views.append(_feature_view("batch_view").to_proto()) + registry_proto.stream_feature_views.append( + _stream_feature_view("driver_dropoffs_stream").to_proto() + ) + + infra_objects = SqliteOnlineStore().plan(config, registry_proto) + + assert sorted(o.name for o in infra_objects) == [ + "test_project_batch_view", + "test_project_driver_dropoffs_stream", + ] diff --git a/sdk/python/tests/unit/infra/registry/test_file_registry.py b/sdk/python/tests/unit/infra/registry/test_file_registry.py new file mode 100644 index 00000000000..07b443e7eaa --- /dev/null +++ b/sdk/python/tests/unit/infra/registry/test_file_registry.py @@ -0,0 +1,95 @@ +import tempfile +from datetime import timedelta + +import pytest + +from feast import Field +from feast.data_source import PushSource +from feast.entity import Entity +from feast.errors import ConflictingFeatureViewNames +from feast.feature_view import FeatureView +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.registry.registry import Registry +from feast.repo_config import RegistryConfig +from feast.stream_feature_view import StreamFeatureView +from feast.types import Float32 +from feast.value_type import ValueType + + +@pytest.fixture +def file_registry(): + fd, registry_path = tempfile.mkstemp() + config = RegistryConfig(path=registry_path) + registry = Registry("test_project", config, None) + yield registry + registry.teardown() + + +def _make_sources(): + file_source = FileSource( + path="driver_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + push_source = PushSource(name="driver_push", batch_source=file_source) + return file_source, push_source + + +def test_same_project_name_conflict_batch_vs_stream(file_registry): + """A FeatureView and StreamFeatureView with the same name in the same project must raise ConflictingFeatureViewNames.""" + entity = Entity(name="driver", value_type=ValueType.STRING, join_keys=["driver_id"]) + file_registry.apply_entity(entity, "test_project") + + file_source, push_source = _make_sources() + + batch_view = FeatureView( + name="driver_activity", + entities=[entity], + ttl=timedelta(days=1), + schema=[Field(name="conv_rate", dtype=Float32)], + source=file_source, + ) + file_registry.apply_feature_view(batch_view, "test_project") + + stream_view = StreamFeatureView( + name="driver_activity", + source=push_source, + entities=[entity], + schema=[Field(name="conv_rate", dtype=Float32)], + timestamp_field="event_timestamp", + ) + with pytest.raises(ConflictingFeatureViewNames): + file_registry.apply_feature_view(stream_view, "test_project") + + +def test_cross_project_name_does_not_conflict_batch_vs_stream(file_registry): + """A FeatureView in project_a and a StreamFeatureView with the same name in project_b + must not raise ConflictingFeatureViewNames. + + Before the fix, _existing_feature_view_names_to_fvs scanned all projects, + so the type mismatch between the two projects triggered a spurious error. + """ + entity = Entity(name="driver", value_type=ValueType.STRING, join_keys=["driver_id"]) + file_registry.apply_entity(entity, "project_a") + file_registry.apply_entity(entity, "project_b") + + file_source, push_source = _make_sources() + + batch_view = FeatureView( + name="driver_activity", + entities=[entity], + ttl=timedelta(days=1), + schema=[Field(name="conv_rate", dtype=Float32)], + source=file_source, + ) + file_registry.apply_feature_view(batch_view, "project_a") + + stream_view = StreamFeatureView( + name="driver_activity", + source=push_source, + entities=[entity], + schema=[Field(name="conv_rate", dtype=Float32)], + timestamp_field="event_timestamp", + ) + # Must not raise — same name, different project, different type. + file_registry.apply_feature_view(stream_view, "project_b") diff --git a/sdk/python/tests/unit/infra/registry/test_registry.py b/sdk/python/tests/unit/infra/registry/test_registry.py index 51bb7d28e32..219d480d2d0 100644 --- a/sdk/python/tests/unit/infra/registry/test_registry.py +++ b/sdk/python/tests/unit/infra/registry/test_registry.py @@ -53,6 +53,9 @@ def _get_saved_dataset(self, *args, **kwargs): def _get_stream_feature_view(self, *args, **kwargs): pass + def _get_label_view(self, *args, **kwargs): + pass + def _get_validation_reference(self, *args, **kwargs): pass @@ -89,6 +92,9 @@ def _list_saved_datasets(self, *args, **kwargs): def _list_stream_feature_views(self, *args, **kwargs): pass + def _list_label_views(self, *args, **kwargs): + pass + def _list_validation_references(self, *args, **kwargs): pass @@ -137,6 +143,9 @@ def delete_feature_service(self, *args, **kwargs): def delete_feature_view(self, *args, **kwargs): pass + def delete_label_view(self, *args, **kwargs): + pass + def delete_permission(self, *args, **kwargs): pass diff --git a/sdk/python/tests/unit/infra/registry/test_registry_server_proto.py b/sdk/python/tests/unit/infra/registry/test_registry_server_proto.py new file mode 100644 index 00000000000..03b9902ff55 --- /dev/null +++ b/sdk/python/tests/unit/infra/registry/test_registry_server_proto.py @@ -0,0 +1,132 @@ +"""Unit tests for the ``RegistryServer.Proto`` RPC (issue #6558). + +The RPC must build the ``RegistryProto`` from individually RBAC-filtered list calls +rather than returning ``proxied_registry.proto()`` directly (which would bypass +permissions). Under ``NoAuthConfig`` filtering is a no-op, so the full registry is +returned; with auth enabled only ``DESCRIBE``-permitted objects are included. +""" + +from datetime import datetime, timezone +from unittest.mock import patch + +from google.protobuf.empty_pb2 import Empty + +from feast.data_source import DataSource +from feast.entity import Entity +from feast.feast_object import FeastObject +from feast.project import Project +from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto +from feast.registry_server import RegistryServer +from feast.value_type import ValueType + +# The registry's authentic metadata, returned by _FakeRegistry.proto(). Proto() must carry these +# through rather than stamping "now" (issue #6558 review feedback). +_REGISTRY_LAST_UPDATED = datetime(2024, 1, 2, 3, 4, 5, tzinfo=timezone.utc) +_REGISTRY_VERSION_ID = "test-version-id" + + +class _FakeRegistry: + """Minimal BaseRegistry stand-in exposing only the list_* calls Proto uses.""" + + def __init__(self, projects, entities_by_project, data_sources_by_project): + self._projects = projects + self._entities = entities_by_project + self._data_sources = data_sources_by_project + + def proto(self) -> RegistryProto: + # Source of the authentic last_updated / version_id metadata. Proto() reads only these + # scalar fields from here (no objects), so RBAC filtering is unaffected. + proto = RegistryProto() + proto.version_id = _REGISTRY_VERSION_ID + proto.last_updated.FromDatetime(_REGISTRY_LAST_UPDATED) + return proto + + def list_projects(self, allow_cache: bool = False, tags=None): + return self._projects + + def list_entities(self, project: str, allow_cache: bool = False, tags=None): + return self._entities.get(project, []) + + def list_data_sources(self, project: str, allow_cache: bool = False, tags=None): + return self._data_sources.get(project, []) + + # Every other object type is empty for this fixture. + def _empty(self, *args, **kwargs): + return [] + + list_feature_views = _empty + list_stream_feature_views = _empty + list_on_demand_feature_views = _empty + list_label_views = _empty + list_feature_services = _empty + list_saved_datasets = _empty + list_validation_references = _empty + list_permissions = _empty + + +def _entity(name: str) -> Entity: + return Entity(name=name, value_type=ValueType.STRING) + + +def _data_source(name: str) -> DataSource: + from feast.infra.offline_stores.file_source import FileSource + + return FileSource(name=name, path=f"/tmp/{name}.parquet", timestamp_field="ts") + + +def _build_server() -> tuple[RegistryServer, _FakeRegistry]: + registry = _FakeRegistry( + projects=[Project(name="proj_a"), Project(name="proj_b")], + entities_by_project={ + "proj_a": [_entity("driver"), _entity("customer")], + "proj_b": [_entity("merchant")], + }, + data_sources_by_project={"proj_a": [_data_source("src_a")]}, + ) + return RegistryServer(registry), registry # type: ignore[arg-type] + + +def test_proto_returns_full_registry_when_no_auth(): + """NoAuthConfig (no security manager) -> every object across all projects.""" + server, _ = _build_server() + + result = server.Proto(Empty(), context=None) + + assert {p.spec.name for p in result.projects} == {"proj_a", "proj_b"} + assert {e.spec.name for e in result.entities} == {"driver", "customer", "merchant"} + assert {d.name for d in result.data_sources} == {"src_a"} + # last_updated / version_id are carried from the registry's real proto (not stamped "now"), + # so cache consumers see the registry's authentic freshness metadata. + assert result.version_id == _REGISTRY_VERSION_ID + assert result.last_updated.ToDatetime(tzinfo=timezone.utc) == _REGISTRY_LAST_UPDATED + + +def test_proto_filters_by_describe_permission(): + """With RBAC, only DESCRIBE-permitted objects are included.""" + server, _ = _build_server() + + # Simulate a security manager that permits everything except the "customer" + # entity, regardless of object type (filters by DESCRIBE). + def fake_permitted(resources: list[FeastObject], actions): + return [r for r in resources if getattr(r, "name", None) != "customer"] + + with patch( + "feast.registry_server.permitted_resources", side_effect=fake_permitted + ) as mocked: + result = server.Proto(Empty(), context=None) + + assert mocked.called + # "customer" is filtered out; everything else survives. + assert {e.spec.name for e in result.entities} == {"driver", "merchant"} + assert {p.spec.name for p in result.projects} == {"proj_a", "proj_b"} + assert {d.name for d in result.data_sources} == {"src_a"} + + +def test_proto_empty_registry(): + """No projects -> empty (but valid) RegistryProto, not an error.""" + server = RegistryServer(_FakeRegistry([], {}, {})) # type: ignore[arg-type] + + result = server.Proto(Empty(), context=None) + + assert len(result.projects) == 0 + assert len(result.entities) == 0 diff --git a/sdk/python/tests/unit/infra/registry/test_remote_registry.py b/sdk/python/tests/unit/infra/registry/test_remote_registry.py new file mode 100644 index 00000000000..d6b9bbc7fa0 --- /dev/null +++ b/sdk/python/tests/unit/infra/registry/test_remote_registry.py @@ -0,0 +1,85 @@ +# Copyright 2024 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock, patch + +import pytest +from google.protobuf.timestamp_pb2 import Timestamp + +from feast.infra.registry.remote import RemoteRegistry +from feast.protos.feast.registry import RegistryServer_pb2 + + +@pytest.fixture +def remote_registry(): + with patch.object(RemoteRegistry, "__init__", return_value=None): + registry = RemoteRegistry.__new__(RemoteRegistry) + registry.stub = MagicMock() + registry.stub.ListAllFeatureViews.return_value = ( + RegistryServer_pb2.ListAllFeatureViewsResponse(feature_views=[]) + ) + yield registry + + +def _captured_updated_since(remote_registry) -> Timestamp: + """Return the updated_since Timestamp from the last ListAllFeatureViews call.""" + call_args = remote_registry.stub.ListAllFeatureViews.call_args + request: RegistryServer_pb2.ListAllFeatureViewsRequest = call_args[0][0] + return request.updated_since + + +def test_updated_since_utc_aware(remote_registry): + """A UTC-aware datetime is encoded to the correct UTC epoch seconds.""" + dt = datetime(2024, 6, 1, 17, 0, 0, tzinfo=timezone.utc) + remote_registry.list_all_feature_views("project", updated_since=dt) + + ts = _captured_updated_since(remote_registry) + assert ts.seconds == int(dt.timestamp()) + + +def test_updated_since_non_utc_aware(remote_registry): + """A non-UTC tz-aware datetime is converted to the correct UTC epoch, not treated as UTC.""" + est = timezone(timedelta(hours=-5)) + # 2024-06-01 12:00 EST == 2024-06-01 17:00 UTC + dt_est = datetime(2024, 6, 1, 12, 0, 0, tzinfo=est) + dt_utc = datetime(2024, 6, 1, 17, 0, 0, tzinfo=timezone.utc) + + remote_registry.list_all_feature_views("project", updated_since=dt_est) + + ts = _captured_updated_since(remote_registry) + assert ts.seconds == int(dt_utc.timestamp()), ( + "Non-UTC datetime must be converted to UTC before encoding, " + "not have its tzinfo stripped (which would misinterpret 12:00 EST as 12:00 UTC)" + ) + + +def test_updated_since_naive_datetime(remote_registry): + """A naive datetime is treated as UTC by protobuf's FromDatetime.""" + dt_naive = datetime(2024, 6, 1, 17, 0, 0) + dt_utc = datetime(2024, 6, 1, 17, 0, 0, tzinfo=timezone.utc) + remote_registry.list_all_feature_views("project", updated_since=dt_naive) + + ts = _captured_updated_since(remote_registry) + assert ts.seconds == int(dt_utc.timestamp()) + + +def test_updated_since_none(remote_registry): + """When updated_since is None, the field is not set in the request.""" + remote_registry.list_all_feature_views("project", updated_since=None) + + request: RegistryServer_pb2.ListAllFeatureViewsRequest = ( + remote_registry.stub.ListAllFeatureViews.call_args[0][0] + ) + assert not request.HasField("updated_since") diff --git a/sdk/python/tests/unit/infra/registry/test_remote_registry_mtls.py b/sdk/python/tests/unit/infra/registry/test_remote_registry_mtls.py new file mode 100644 index 00000000000..e63b3d637df --- /dev/null +++ b/sdk/python/tests/unit/infra/registry/test_remote_registry_mtls.py @@ -0,0 +1,344 @@ +""" +Tests for mTLS (mutual TLS) support on RemoteRegistry. + +Covers two deployment patterns: + +1. **Direct connection** — client connects straight to the service. The + server cert's SAN includes the connection target so ``authority`` is + optional (gRPC defaults it to the target host). + +2. **IAP-tunnel / proxy** — ``gcloud compute start-iap-tunnel`` forwards + traffic through ``localhost``. The server cert's SAN is the real service + hostname (not ``localhost``), so the client *must* set ``authority`` to + match — otherwise TLS hostname verification fails. +""" + +import tempfile +from collections.abc import Generator +from concurrent import futures + +import grpc +import pytest +from google.protobuf.empty_pb2 import Empty + +from feast.protos.feast.core import Project_pb2, Registry_pb2 +from feast.protos.feast.registry import RegistryServer_pb2, RegistryServer_pb2_grpc +from tests.utils.ssl_certifcates_util import generate_mtls_certs + +# The hostname the server cert is issued for. +# Deliberately *not* localhost — the whole point of the authority override. +SERVICE_HOSTNAME = "feature-registry.example.com" + + +# --------------------------------------------------------------------------- +# Minimal gRPC servicer — just enough to prove the channel works +# --------------------------------------------------------------------------- + + +class _StubRegistryServicer(RegistryServer_pb2_grpc.RegistryServerServicer): + """Returns a single dummy project so list_projects() has something to assert on.""" + + def ListProjects( + self, + request: RegistryServer_pb2.ListProjectsRequest, + context: grpc.ServicerContext, + ) -> RegistryServer_pb2.ListProjectsResponse: + spec = Project_pb2.ProjectSpec( + name="test_project", + description="created by mTLS test", + ) + project = Project_pb2.Project(spec=spec) + return RegistryServer_pb2.ListProjectsResponse(projects=[project]) + + def Proto( + self, + request: Empty, + context: grpc.ServicerContext, + ) -> Registry_pb2.Registry: + return Registry_pb2.Registry() + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +def _generate_certs_and_start_server( + tmpdir: str, server_san: str +) -> tuple[dict[str, str], grpc.Server, int]: + """Helper: generate mTLS certs and start a gRPC server requiring client auth.""" + certs = generate_mtls_certs(tmpdir, server_san=server_san) + + with open(certs["ca_cert"], "rb") as f: + ca_cert = f.read() + with open(certs["server_key"], "rb") as f: + server_key = f.read() + with open(certs["server_cert"], "rb") as f: + server_cert = f.read() + + server_creds = grpc.ssl_server_credentials( + [(server_key, server_cert)], + root_certificates=ca_cert, + require_client_auth=True, + ) + + server = grpc.server(futures.ThreadPoolExecutor(max_workers=2)) + RegistryServer_pb2_grpc.add_RegistryServerServicer_to_server( + _StubRegistryServicer(), server + ) + port = server.add_secure_port("localhost:0", server_creds) + server.start() + return certs, server, port + + +# -- Fixtures for IAP-tunnel tests (SAN ≠ localhost) -- + + +@pytest.fixture(scope="module") +def mtls_certs() -> Generator[tuple[dict[str, str], int], None, None]: + """Certs where the server SAN is SERVICE_HOSTNAME (not localhost).""" + with tempfile.TemporaryDirectory() as tmpdir: + certs, server, port = _generate_certs_and_start_server(tmpdir, SERVICE_HOSTNAME) + yield certs, port + server.stop(grace=0) + + +@pytest.fixture(scope="module") +def mtls_server(mtls_certs: tuple[dict[str, str], int]) -> int: + return mtls_certs[1] + + +@pytest.fixture(scope="module") +def mtls_certs_only(mtls_certs: tuple[dict[str, str], int]) -> dict[str, str]: + return mtls_certs[0] + + +# -- Fixtures for direct-connection tests (SAN = localhost) -- + + +@pytest.fixture(scope="module") +def direct_mtls() -> Generator[tuple[dict[str, str], int], None, None]: + """Certs where the server SAN includes localhost (direct connection).""" + with tempfile.TemporaryDirectory() as tmpdir: + certs, server, port = _generate_certs_and_start_server(tmpdir, "localhost") + yield certs, port + server.stop(grace=0) + + +@pytest.fixture(scope="module") +def direct_server(direct_mtls: tuple[dict[str, str], int]) -> int: + return direct_mtls[1] + + +@pytest.fixture(scope="module") +def direct_certs(direct_mtls: tuple[dict[str, str], int]) -> dict[str, str]: + return direct_mtls[0] + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +class TestMtlsDirectConnection: + """ + Direct connection to the registry — no proxy or tunnel. + + The server cert's SAN includes ``localhost`` so gRPC's default authority + (derived from the connection target) already matches. The ``authority`` + field should be optional. + + Config equivalent: + registry: + registry_type: remote + path: feature-registry.example.com:443 + cert: /etc/tls/internal-client/ca.crt + client_cert: /etc/tls/internal-client/tls.crt + client_key: /etc/tls/internal-client/tls.key + """ + + def test_mtls_without_authority( + self, direct_server: int, direct_certs: dict[str, str] + ) -> None: + """authority is not set — gRPC defaults it to 'localhost', which matches the SAN.""" + from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig + + config = RemoteRegistryConfig( + registry_type="remote", + path=f"localhost:{direct_server}", + cert=direct_certs["ca_cert"], + client_cert=direct_certs["client_cert"], + client_key=direct_certs["client_key"], + ) + registry = RemoteRegistry(config, project="test", repo_path=None) + try: + projects = registry.list_projects() + assert len(projects) == 1 + assert projects[0].name == "test_project" + finally: + registry.close() + + def test_mtls_with_explicit_authority_matching_san( + self, direct_server: int, direct_certs: dict[str, str] + ) -> None: + """authority is explicitly set to the same host — should also work.""" + from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig + + config = RemoteRegistryConfig( + registry_type="remote", + path=f"localhost:{direct_server}", + cert=direct_certs["ca_cert"], + client_cert=direct_certs["client_cert"], + client_key=direct_certs["client_key"], + authority="localhost", + ) + registry = RemoteRegistry(config, project="test", repo_path=None) + try: + projects = registry.list_projects() + assert len(projects) == 1 + assert projects[0].name == "test_project" + finally: + registry.close() + + +class TestMtlsIapTunnel: + """ + Simulates ``gcloud compute start-iap-tunnel`` to a remote registry. + + The server cert's SAN is ``feature-registry.example.com`` — it does NOT + include ``localhost``. The ``authority`` field is mandatory. + + Config equivalent: + registry: + registry_type: remote + path: localhost:8443 + cert: /etc/tls/internal-client/ca.crt + client_cert: /etc/tls/internal-client/tls.crt + client_key: /etc/tls/internal-client/tls.key + authority: feature-registry.example.com + """ + + def test_list_projects_via_mtls_with_authority( + self, mtls_server: int, mtls_certs_only: dict[str, str] + ) -> None: + """The primary IAP-tunnel scenario: all four fields set.""" + from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig + + config = RemoteRegistryConfig( + registry_type="remote", + path=f"localhost:{mtls_server}", + cert=mtls_certs_only["ca_cert"], + client_cert=mtls_certs_only["client_cert"], + client_key=mtls_certs_only["client_key"], + authority=SERVICE_HOSTNAME, + ) + registry = RemoteRegistry(config, project="test", repo_path=None) + try: + projects = registry.list_projects() + assert len(projects) == 1 + assert projects[0].name == "test_project" + assert projects[0].description == "created by mTLS test" + finally: + registry.close() + + def test_proto_via_mtls_with_authority( + self, mtls_server: int, mtls_certs_only: dict[str, str] + ) -> None: + """Proto() is the simplest RPC — verify the channel works for it too.""" + from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig + + config = RemoteRegistryConfig( + registry_type="remote", + path=f"localhost:{mtls_server}", + cert=mtls_certs_only["ca_cert"], + client_cert=mtls_certs_only["client_cert"], + client_key=mtls_certs_only["client_key"], + authority=SERVICE_HOSTNAME, + ) + registry = RemoteRegistry(config, project="test", repo_path=None) + try: + proto = registry.proto() + assert proto is not None + finally: + registry.close() + + +class TestMtlsRejections: + """Verify mTLS enforcement — connections fail without proper credentials.""" + + def test_rejected_without_client_cert( + self, mtls_server: int, mtls_certs_only: dict[str, str] + ) -> None: + """Server requires mTLS; connecting with only server-TLS must fail.""" + from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig + + config = RemoteRegistryConfig( + registry_type="remote", + path=f"localhost:{mtls_server}", + cert=mtls_certs_only["ca_cert"], + is_tls=True, + authority=SERVICE_HOSTNAME, + ) + registry = RemoteRegistry(config, project="test", repo_path=None) + try: + with pytest.raises(grpc.RpcError): + registry.list_projects() + finally: + registry.close() + + def test_fails_without_authority_when_san_mismatch( + self, mtls_server: int, mtls_certs_only: dict[str, str] + ) -> None: + """ + Without authority override, the gRPC client checks the server cert SAN + against 'localhost' — which doesn't match — so the handshake fails. + This proves the authority field is necessary for the IAP-tunnel case. + """ + from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig + + config = RemoteRegistryConfig( + registry_type="remote", + path=f"localhost:{mtls_server}", + cert=mtls_certs_only["ca_cert"], + client_cert=mtls_certs_only["client_cert"], + client_key=mtls_certs_only["client_key"], + # No authority — should fail because SAN is SERVICE_HOSTNAME, not localhost + ) + registry = RemoteRegistry(config, project="test", repo_path=None) + try: + with pytest.raises(grpc.RpcError): + registry.list_projects() + finally: + registry.close() + + +class TestMtlsConfigValidation: + """Config validation catches mismatched cert/key before any I/O.""" + + def test_client_cert_without_client_key_raises(self) -> None: + from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig + + config = RemoteRegistryConfig( + registry_type="remote", + path="localhost:0", + cert="/dev/null", + client_cert="/dev/null", + ) + with pytest.raises( + ValueError, match="Both client_cert and client_key must be provided" + ): + RemoteRegistry(config, project="test", repo_path=None) + + def test_client_key_without_client_cert_raises(self) -> None: + from feast.infra.registry.remote import RemoteRegistry, RemoteRegistryConfig + + config = RemoteRegistryConfig( + registry_type="remote", + path="localhost:0", + cert="/dev/null", + client_key="/dev/null", + ) + with pytest.raises( + ValueError, match="Both client_cert and client_key must be provided" + ): + RemoteRegistry(config, project="test", repo_path=None) diff --git a/sdk/python/tests/unit/infra/registry/test_snowflake_registry.py b/sdk/python/tests/unit/infra/registry/test_snowflake_registry.py new file mode 100644 index 00000000000..59ab2b87c03 --- /dev/null +++ b/sdk/python/tests/unit/infra/registry/test_snowflake_registry.py @@ -0,0 +1,458 @@ +# Copyright 2024 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock, patch + +import pandas as pd +import pytest + +from feast import FeatureView, Field +from feast.entity import Entity +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.registry.snowflake import SnowflakeRegistry, SnowflakeRegistryConfig +from feast.infra.utils.snowflake.snowflake_utils import GetSnowflakeConnection +from feast.types import Float32 + + +@pytest.fixture +def registry(): + config = SnowflakeRegistryConfig( + database="TEST_DB", + schema="PUBLIC", + account="test_account", + user="test_user", + password="test_password", # pragma: allowlist secret + ) + mock_conn = MagicMock() + mock_conn.__enter__ = MagicMock(return_value=MagicMock()) + mock_conn.__exit__ = MagicMock(return_value=False) + + # execute_snowflake_statement during __init__ creates tables; return empty cursor + mock_cursor = MagicMock() + mock_cursor.fetch_pandas_all.return_value = pd.DataFrame() + + with ( + patch( + "feast.infra.registry.snowflake.GetSnowflakeConnection", + return_value=mock_conn, + ), + patch( + "feast.infra.registry.snowflake.execute_snowflake_statement", + return_value=mock_cursor, + ), + ): + reg = SnowflakeRegistry(config, "test_project", None) + return reg + + +def test_apply_object_update_includes_project_id_in_where_clause(registry): + """ + Regression test for feast-dev/feast#6208. + + _apply_object UPDATE path was missing project_id in the WHERE clause, + allowing a same-named object in one project to silently overwrite the + same-named object in a different project when a Snowflake registry is shared. + + The SELECT path correctly scopes by project_id; the UPDATE must do the same. + """ + entity = Entity(name="driver", join_keys=["driver_id"]) + project = "project_b" + + # Non-empty DataFrame tells _apply_object the row already exists → UPDATE path + existing_row = pd.DataFrame({"project_id": [project]}) + + captured_queries = [] + + def capture_and_respond(conn, query): + normalised = " ".join(query.split()) + captured_queries.append(normalised) + cursor = MagicMock() + cursor.fetch_pandas_all.return_value = ( + existing_row if "SELECT" in normalised else pd.DataFrame() + ) + return cursor + + mock_conn = MagicMock() + mock_conn.__enter__ = MagicMock(return_value=MagicMock()) + mock_conn.__exit__ = MagicMock(return_value=False) + + with ( + patch( + "feast.infra.registry.snowflake.GetSnowflakeConnection", + return_value=mock_conn, + ), + patch( + "feast.infra.registry.snowflake.execute_snowflake_statement", + side_effect=capture_and_respond, + ), + patch.object(registry, "_maybe_init_project_metadata"), + patch.object(registry, "_initialize_project_if_not_exists"), + patch.object(registry, "get_project", return_value=MagicMock()), + patch.object(registry, "apply_project"), + patch.object(registry, "_set_last_updated_metadata"), + ): + registry._apply_object( + "ENTITIES", + project, + "ENTITY_NAME", + entity, + "ENTITY_PROTO", + ) + + update_queries = [q for q in captured_queries if q.startswith("UPDATE")] + assert len(update_queries) == 1, ( + f"Expected exactly 1 UPDATE query, got {len(update_queries)}: {update_queries}" + ) + + update_query = update_queries[0] + assert f"project_id = '{project}'" in update_query, ( + f"feast#6208: UPDATE WHERE clause is missing project_id filter — " + f"cross-project overwrites are possible in shared Snowflake registries.\n" + f"Query was: {update_query}" + ) + + +def test_apply_object_does_not_overwrite_sibling_project(registry): + """ + Cross-project isolation: applying an object in project_b must not overwrite + the same-named object in project_a when sharing a Snowflake registry. + + Regression coverage for feast-dev/feast#6208: the UPDATE path must scope by + project_id so writes in one project cannot bleed into a sibling project. + """ + entity = Entity(name="driver", join_keys=["driver_id"]) + project_a, project_b = "project_a", "project_b" + + # Simulate both projects having a pre-existing "driver" entity row + row_b = pd.DataFrame({"project_id": [project_b]}) + + update_queries = [] + + def simulated_snowflake(conn, query): + normalised = " ".join(query.split()) + cursor = MagicMock() + if "SELECT" in normalised: + # Scope the response to the project in the query; no row for project_a + if f"project_id = '{project_b}'" in normalised: + cursor.fetch_pandas_all.return_value = row_b + else: + cursor.fetch_pandas_all.return_value = pd.DataFrame() + elif "UPDATE" in normalised: + update_queries.append(normalised) + cursor.fetch_pandas_all.return_value = pd.DataFrame() + else: + cursor.fetch_pandas_all.return_value = pd.DataFrame() + return cursor + + mock_conn = MagicMock() + mock_conn.__enter__ = MagicMock(return_value=MagicMock()) + mock_conn.__exit__ = MagicMock(return_value=False) + + with ( + patch( + "feast.infra.registry.snowflake.GetSnowflakeConnection", + return_value=mock_conn, + ), + patch( + "feast.infra.registry.snowflake.execute_snowflake_statement", + side_effect=simulated_snowflake, + ), + patch.object(registry, "_maybe_init_project_metadata"), + patch.object(registry, "_initialize_project_if_not_exists"), + patch.object(registry, "get_project", return_value=MagicMock()), + patch.object(registry, "apply_project"), + patch.object(registry, "_set_last_updated_metadata"), + ): + registry._apply_object( + "ENTITIES", project_b, "ENTITY_NAME", entity, "ENTITY_PROTO" + ) + + assert len(update_queries) == 1, ( + f"Expected exactly 1 UPDATE, got {len(update_queries)}: {update_queries}" + ) + update_query = update_queries[0] + assert f"project_id = '{project_b}'" in update_query, ( + f"feast#6208: UPDATE is not scoped to {project_b!r} — cross-project overwrite possible.\n" + f"Query: {update_query}" + ) + assert f"project_id = '{project_a}'" not in update_query, ( + f"feast#6208: UPDATE WHERE clause references {project_a!r} — unintended cross-project write.\n" + f"Query: {update_query}" + ) + + +class TestSyncFeastMetadataToProjectsTable: + def _make_registry(self): + """Create a SnowflakeRegistry with mocked __init__.""" + with patch.object(SnowflakeRegistry, "__init__", lambda self: None): + registry = SnowflakeRegistry() + registry.registry_config = MagicMock() + registry.registry_path = "test_db.test_schema" + registry.purge_feast_metadata = False + return registry + + @patch( + "feast.infra.registry.snowflake.GetSnowflakeConnection", + ) + @patch("feast.infra.registry.snowflake.execute_snowflake_statement") + def test_sync_with_feast_metadata_projects(self, mock_execute, mock_get_conn): + registry = self._make_registry() + + metadata_df = pd.DataFrame({"PROJECT_ID": ["project_a", "project_b"]}) + projects_df = pd.DataFrame({"PROJECT_ID": ["project_a"]}) + + mock_cursor = MagicMock() + mock_cursor.fetch_pandas_all.side_effect = [metadata_df, projects_df] + mock_execute.return_value = mock_cursor + + mock_conn = MagicMock() + mock_get_conn.return_value.__enter__ = MagicMock(return_value=mock_conn) + mock_get_conn.return_value.__exit__ = MagicMock(return_value=False) + + with patch.object(registry, "apply_project") as mock_apply: + registry._sync_feast_metadata_to_projects_table() + + mock_apply.assert_called_once() + applied_project = mock_apply.call_args[0][0] + assert applied_project.name == "project_b" + + @patch( + "feast.infra.registry.snowflake.GetSnowflakeConnection", + ) + @patch("feast.infra.registry.snowflake.execute_snowflake_statement") + def test_sync_with_no_feast_metadata(self, mock_execute, mock_get_conn): + registry = self._make_registry() + + empty_df = pd.DataFrame({"PROJECT_ID": []}) + mock_cursor = MagicMock() + mock_cursor.fetch_pandas_all.return_value = empty_df + mock_execute.return_value = mock_cursor + + mock_conn = MagicMock() + mock_get_conn.return_value.__enter__ = MagicMock(return_value=mock_conn) + mock_get_conn.return_value.__exit__ = MagicMock(return_value=False) + + with patch.object(registry, "apply_project") as mock_apply: + registry._sync_feast_metadata_to_projects_table() + + mock_apply.assert_not_called() + + @patch( + "feast.infra.registry.snowflake.GetSnowflakeConnection", + ) + @patch("feast.infra.registry.snowflake.execute_snowflake_statement") + def test_sync_deduplicates_project_ids(self, mock_execute, mock_get_conn): + """Sets should deduplicate project IDs; lists would not.""" + registry = self._make_registry() + + metadata_df = pd.DataFrame( + {"PROJECT_ID": ["project_a", "project_a", "project_b"]} + ) + projects_df = pd.DataFrame({"PROJECT_ID": []}) + + mock_cursor = MagicMock() + mock_cursor.fetch_pandas_all.side_effect = [metadata_df, projects_df] + mock_execute.return_value = mock_cursor + + mock_conn = MagicMock() + mock_get_conn.return_value.__enter__ = MagicMock(return_value=mock_conn) + mock_get_conn.return_value.__exit__ = MagicMock(return_value=False) + + with patch.object(registry, "apply_project") as mock_apply: + registry._sync_feast_metadata_to_projects_table() + + assert mock_apply.call_count == 2 + applied_names = {call[0][0].name for call in mock_apply.call_args_list} + assert applied_names == {"project_a", "project_b"} + + +class _DictableConfig: + """A config object that supports dict() conversion and attribute access.""" + + def __init__(self, data): + self._data = data + for k, v in data.items(): + setattr(self, k, v) + + def __iter__(self): + return iter(self._data) + + def keys(self): + return self._data.keys() + + def __getitem__(self, key): + return self._data[key] + + +class TestGetSnowflakeConnection: + @patch("feast.infra.utils.snowflake.snowflake_utils.parse_private_key_path") + @patch("feast.infra.utils.snowflake.snowflake_utils.snowflake.connector") + @patch("feast.infra.utils.snowflake.snowflake_utils._cache", {}) + def test_private_key_kwargs_not_leaked_to_connect( + self, mock_connector, mock_parse_key + ): + """private_key_passphrase and private_key_content must not be passed to connect().""" + mock_parse_key.return_value = b"parsed_key_bytes" + mock_conn = MagicMock() + mock_connector.connect.return_value = mock_conn + + config = _DictableConfig( + { + "type": "snowflake.registry", + "account": "test_account", + "user": "test_user", + "password": None, + "role": "test_role", + "warehouse": "test_wh", + "database": "test_db", + "schema_": "test_schema", + "config_path": "", + "private_key": "/path/to/key.p8", + "private_key_passphrase": "my_secret", # pragma: allowlist secret + "private_key_content": None, + } + ) + + with GetSnowflakeConnection(config): + pass + + connect_kwargs = mock_connector.connect.call_args[1] + assert "private_key_passphrase" not in connect_kwargs + assert "private_key_content" not in connect_kwargs + assert connect_kwargs["private_key"] == b"parsed_key_bytes" + + +def _make_feature_view(name: str, entity: Entity, source: FileSource) -> FeatureView: + return FeatureView( + name=name, + entities=[entity], + ttl=timedelta(days=1), + schema=[Field(name="conv_rate", dtype=Float32)], + source=source, + ) + + +@pytest.fixture +def mock_snowflake_registry(): + """Return a SnowflakeRegistry whose Snowflake connection is fully mocked.""" + config = MagicMock() + config.path = "snowflake://account/db/schema" + config.registry_type = "snowflake" + config.cache_ttl_seconds = 0 + config.cache_mode = "sync" + + with patch( + "feast.infra.registry.snowflake.SnowflakeRegistry.__init__", + return_value=None, + ): + registry = SnowflakeRegistry.__new__(SnowflakeRegistry) + registry.registry_config = config + registry.registry_path = "db.schema" + registry.cache_mode = "sync" + # list_all_feature_views also aggregates label views; default to none so the + # updated_since tests only need to stub the feature-view list methods they exercise. + registry.list_label_views = MagicMock(return_value=[]) + yield registry + + +def _make_mock_fv(name: str, updated_at: datetime) -> MagicMock: + fv = MagicMock() + fv.name = name + # Snowflake stores timestamps as offset-naive UTC after round-tripping through proto + fv.last_updated_timestamp = updated_at.replace(tzinfo=None) + fv.tags = {} + return fv + + +def test_list_all_feature_views_updated_since_no_cache(mock_snowflake_registry): + """updated_since filters Python-side when allow_cache=False.""" + old_ts = datetime(2020, 1, 1, tzinfo=timezone.utc) + new_ts = datetime(2024, 6, 1, tzinfo=timezone.utc) + + old_fv = _make_mock_fv("old_view", old_ts) + new_fv = _make_mock_fv("new_view", new_ts) + + mock_snowflake_registry.list_feature_views = MagicMock( + return_value=[old_fv, new_fv] + ) + mock_snowflake_registry.list_stream_feature_views = MagicMock(return_value=[]) + mock_snowflake_registry.list_on_demand_feature_views = MagicMock(return_value=[]) + + cutoff = datetime(2023, 1, 1, tzinfo=timezone.utc) + result = mock_snowflake_registry.list_all_feature_views( + "project", allow_cache=False, updated_since=cutoff + ) + assert [fv.name for fv in result] == ["new_view"] + + +def test_list_all_feature_views_updated_since_no_filter(mock_snowflake_registry): + """Without updated_since, all feature views are returned.""" + ts = datetime(2020, 1, 1, tzinfo=timezone.utc) + fv1 = _make_mock_fv("view_a", ts) + fv2 = _make_mock_fv("view_b", ts) + + mock_snowflake_registry.list_feature_views = MagicMock(return_value=[fv1, fv2]) + mock_snowflake_registry.list_stream_feature_views = MagicMock(return_value=[]) + mock_snowflake_registry.list_on_demand_feature_views = MagicMock(return_value=[]) + + result = mock_snowflake_registry.list_all_feature_views( + "project", allow_cache=False + ) + assert len(result) == 2 + + +def test_list_all_feature_views_updated_since_future_returns_empty( + mock_snowflake_registry, +): + """A future cutoff returns no feature views.""" + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + fv = _make_mock_fv("any_view", ts) + + mock_snowflake_registry.list_feature_views = MagicMock(return_value=[fv]) + mock_snowflake_registry.list_stream_feature_views = MagicMock(return_value=[]) + mock_snowflake_registry.list_on_demand_feature_views = MagicMock(return_value=[]) + + future = datetime(2999, 1, 1, tzinfo=timezone.utc) + result = mock_snowflake_registry.list_all_feature_views( + "project", allow_cache=False, updated_since=future + ) + assert result == [] + + +def test_list_all_feature_views_updated_since_non_utc_tz(mock_snowflake_registry): + """A non-UTC tz-aware cutoff is converted to UTC before comparing against naive-UTC timestamps.""" + est = timezone(timedelta(hours=-5)) + # 2023-01-01 00:00 EST == 2023-01-01 05:00 UTC + cutoff_est = datetime(2023, 1, 1, 0, 0, 0, tzinfo=est) + + # Feature view updated at 2023-01-01 03:00 UTC — after midnight EST but before 05:00 UTC + ts_between = datetime(2023, 1, 1, 3, 0, 0, tzinfo=timezone.utc) + # Feature view updated at 2023-01-01 06:00 UTC — after both midnight EST and 05:00 UTC + ts_after = datetime(2023, 1, 1, 6, 0, 0, tzinfo=timezone.utc) + + fv_between = _make_mock_fv("view_between", ts_between) + fv_after = _make_mock_fv("view_after", ts_after) + + mock_snowflake_registry.list_feature_views = MagicMock( + return_value=[fv_between, fv_after] + ) + mock_snowflake_registry.list_stream_feature_views = MagicMock(return_value=[]) + mock_snowflake_registry.list_on_demand_feature_views = MagicMock(return_value=[]) + + result = mock_snowflake_registry.list_all_feature_views( + "project", allow_cache=False, updated_since=cutoff_est + ) + # Only view_after is at or after 05:00 UTC; view_between (03:00 UTC) should be excluded + assert [fv.name for fv in result] == ["view_after"] diff --git a/sdk/python/tests/unit/infra/registry/test_sql_registry.py b/sdk/python/tests/unit/infra/registry/test_sql_registry.py index 8e5154da47b..77a71d66857 100644 --- a/sdk/python/tests/unit/infra/registry/test_sql_registry.py +++ b/sdk/python/tests/unit/infra/registry/test_sql_registry.py @@ -12,12 +12,39 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging +import sys import tempfile +import types +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock +import dill import pytest +from feast import Field +from feast.data_source import PushSource from feast.entity import Entity -from feast.infra.registry.sql import SqlRegistry, SqlRegistryConfig +from feast.errors import ConflictingFeatureViewNames +from feast.feature_view import FeatureView +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.registry.sql import ( + FeastRegistrySchemaError, + ProtoBytes, + SqlRegistry, + SqlRegistryConfig, + feature_views, +) +from feast.infra.registry.sql import ( + metadata as registry_metadata, +) +from feast.protos.feast.core.Transformation_pb2 import ( + FeatureTransformationV2, + UserDefinedFunctionV2, +) +from feast.stream_feature_view import StreamFeatureView +from feast.types import Float32 +from feast.value_type import ValueType @pytest.fixture @@ -35,6 +62,268 @@ def sqlite_registry(): registry.teardown() +@pytest.fixture +def shared_sqlite_db_path(): + """Return a shared SQLite DB path for cross-project tests.""" + fd, path = tempfile.mkstemp() + yield path + + +def test_read_path_normalizes_bare_postgresql_scheme(caplog): + """A bare `postgresql://` read_path is rewritten to the psycopg3 driver, the + same way `path` is, and logs a migration warning. Without this, read_path + silently falls back to psycopg2 while path uses psycopg3.""" + with caplog.at_level(logging.WARNING): + config = SqlRegistryConfig( + registry_type="sql", + path="postgresql://localhost:5432/db", + read_path="postgresql://localhost:5432/replica", + ) + assert config.path == "postgresql+psycopg://localhost:5432/db" + assert config.read_path == "postgresql+psycopg://localhost:5432/replica" + # The migration warning names the read_path field specifically. + assert "`read_path` of the `RegistryConfig`" in caplog.text + + +def test_read_path_normalized_when_registry_type_defaulted(): + """read_path normalization must still fire when registry_type is left to its + default ('sql') rather than passed explicitly — the common real-world config.""" + config = SqlRegistryConfig( + path="postgresql://localhost/db", + read_path="postgresql://localhost/replica", + ) + assert config.registry_type == "sql" + assert config.read_path == "postgresql+psycopg://localhost/replica" + + +@pytest.mark.parametrize( + "read_path", + [ + "postgresql+psycopg2://localhost/replica", # explicit psycopg2 preserved + "postgresql+psycopg://localhost/replica", # already psycopg3 + "mysql://localhost/replica", # non-postgres left untouched + ], +) +def test_read_path_leaves_explicit_scheme_untouched(read_path): + config = SqlRegistryConfig( + registry_type="sql", + path="sqlite:///unused.db", + read_path=read_path, + ) + assert config.read_path == read_path + + +def test_read_path_none_stays_none(): + config = SqlRegistryConfig(registry_type="sql", path="sqlite:///unused.db") + assert config.read_path is None + + +@pytest.mark.parametrize("field_name", ["path", "read_path"]) +def test_postgres_normalization_only_rewrites_leading_scheme(field_name): + raw_url = "postgresql://localhost/db?target=postgresql://replica" + config_values = { + "registry_type": "sql", + "path": "sqlite:///unused.db", + } + config_values[field_name] = raw_url + + config = SqlRegistryConfig(**config_values) + + assert ( + getattr(config, field_name) + == "postgresql+psycopg://localhost/db?target=postgresql://replica" + ) + + +def test_proto_columns_use_longblob_on_mysql(): + """On MySQL and MariaDB, serialized-proto columns must compile to LONGBLOB. + + MySQL maps SQLAlchemy's LargeBinary to BLOB, which caps at 64 KB and + silently truncates larger protos (e.g. a FeatureView proto), later + surfacing as a protobuf DecodeError. The dialect-aware ProtoBytes type + must emit LONGBLOB so large protos round-trip intact. MariaDB is checked + separately because SQLAlchemy 2.x reports dialect.name == "mariadb", which + would miss a "mysql"-only variant. + + NOTE: this asserts only the compiled DDL type. The live >64 KB proto + round-trip through a real MySQL/MariaDB connection is covered by + test_apply_feature_view_large_proto_roundtrip_mysql in + tests/integration/registration/test_universal_registry.py. + """ + from sqlalchemy.dialects import mysql, postgresql, sqlite + from sqlalchemy.schema import CreateTable + + # The MariaDB dialect lives at a SQLAlchemy-version-specific path; import it + # defensively so the rest of this test still runs on SQLAlchemy 1.4.x (a + # version the production code and dependency floor, >=1.4.19, support). The + # always-present mysql dialect keeps the LONGBLOB assertion unconditional. + try: + from sqlalchemy.dialects.mysql.mariadb import MariaDBDialect + + mariadb_dialect = MariaDBDialect() + except ImportError: # pragma: no cover - only on SQLAlchemy builds w/o MariaDB + mariadb_dialect = None + + # ProtoBytes columns share the same MetaData as every registry table, so + # reach it through an already-imported table instead of a second import. + registry_metadata = feature_views.metadata + + # Every column whose name ends in these suffixes stores a (potentially + # large) serialized proto or its companion metadata blob. + binary_suffixes = ("_proto", "_intervals", "user_metadata") + + checked = 0 + for table in registry_metadata.tables.values(): + binary_columns = [ + c.name for c in table.columns if c.name.endswith(binary_suffixes) + ] + # Guard against drift between this name-suffix heuristic and the + # production type predicate (`column.type is ProtoBytes`) used by the + # startup diagnostic: every suffix-matched column must be typed + # ProtoBytes, and no ProtoBytes column may be missed by the suffix list. + # Catches a proto column accidentally typed as plain LargeBinary. + proto_typed_columns = [c.name for c in table.columns if c.type is ProtoBytes] + assert set(binary_columns) == set(proto_typed_columns), ( + f"{table.name}: name-suffix vs ProtoBytes-typed columns disagree — " + f"suffix={sorted(binary_columns)} typed={sorted(proto_typed_columns)}" + ) + if not binary_columns: + continue + + mysql_ddl = str(CreateTable(table).compile(dialect=mysql.dialect())) + sqlite_ddl = str(CreateTable(table).compile(dialect=sqlite.dialect())) + postgres_ddl = str(CreateTable(table).compile(dialect=postgresql.dialect())) + mariadb_ddl = ( + str(CreateTable(table).compile(dialect=mariadb_dialect)) + if mariadb_dialect is not None + else None + ) + + for col_name in binary_columns: + # MySQL and MariaDB must use LONGBLOB for the column (not bare BLOB). + assert f"{col_name} LONGBLOB" in mysql_ddl, ( + f"{table.name}.{col_name} should compile to LONGBLOB on MySQL, " + f"got:\n{mysql_ddl}" + ) + if mariadb_ddl is not None: + assert f"{col_name} LONGBLOB" in mariadb_ddl, ( + f"{table.name}.{col_name} should compile to LONGBLOB on " + f"MariaDB, got:\n{mariadb_ddl}" + ) + # Other dialects keep LargeBinary's default mapping: BLOB on SQLite, + # BYTEA on PostgreSQL. + assert f"{col_name} BLOB" in sqlite_ddl, ( + f"{table.name}.{col_name} should compile to BLOB on SQLite, " + f"got:\n{sqlite_ddl}" + ) + assert f"{col_name} BYTEA" in postgres_ddl, ( + f"{table.name}.{col_name} should compile to BYTEA on PostgreSQL, " + f"got:\n{postgres_ddl}" + ) + checked += 1 + + assert checked > 0, "expected at least one serialized-proto column to check" + + +def _mock_mysql_engine(dialect_name, stale_rows): + """Build a MagicMock Engine that returns ``stale_rows`` from the BLOB query.""" + engine = MagicMock() + engine.dialect.name = dialect_name + conn = MagicMock() + engine.connect.return_value.__enter__.return_value = conn + conn.execute.return_value.fetchall.return_value = stale_rows + return engine + + +@pytest.mark.parametrize("dialect_name", ["mysql", "mariadb"]) +def test_warn_if_narrow_blob_columns_errors_on_stale(caplog, dialect_name): + """On MySQL/MariaDB, stale BLOB registry columns log an error at startup.""" + engine = _mock_mysql_engine( + dialect_name, + [("feature_views", "feature_view_proto"), ("entities", "entity_proto")], + ) + + with caplog.at_level(logging.ERROR): + SqlRegistry._warn_if_narrow_blob_columns(engine) + + assert "still typed BLOB" in caplog.text + assert "feature_views.feature_view_proto" in caplog.text + assert "entities.entity_proto" in caplog.text + # Reported at ERROR so monitoring that filters below ERROR still catches it. + assert any(r.levelno == logging.ERROR for r in caplog.records) + + +@pytest.mark.parametrize("dialect_name", ["mysql", "mariadb"]) +def test_warn_if_narrow_blob_columns_silent_when_migrated(caplog, dialect_name): + """A fully-migrated MySQL/MariaDB registry (no BLOB columns) stays silent.""" + engine = _mock_mysql_engine(dialect_name, []) + + with caplog.at_level(logging.ERROR): + SqlRegistry._warn_if_narrow_blob_columns(engine) + + assert "still typed BLOB" not in caplog.text + + +def test_warn_if_narrow_blob_columns_does_not_refuse_to_start(caplog): + """Stale columns are reported but must never raise — an upgrade can't break + a registry whose protos all fit in 64 KB.""" + engine = _mock_mysql_engine("mysql", [("feature_views", "feature_view_proto")]) + + # Must not raise. + SqlRegistry._warn_if_narrow_blob_columns(engine) + + +def test_warn_if_narrow_blob_columns_skips_non_mysql(): + """Non-MySQL/MariaDB dialects skip the information_schema query entirely.""" + engine = MagicMock() + engine.dialect.name = "sqlite" + + SqlRegistry._warn_if_narrow_blob_columns(engine) + + engine.connect.assert_not_called() + + +def test_init_runs_blob_check_on_read_and_write_engines(tmp_path, monkeypatch): + """When read_path differs from path, the BLOB check runs on both engines.""" + checked_engines = [] + monkeypatch.setattr( + SqlRegistry, + "_warn_if_narrow_blob_columns", + staticmethod(lambda engine, *args, **kwargs: checked_engines.append(engine)), + ) + # Same sqlite file for both engines so create_all (write) and the cache + # refresh (read) both see the registry tables, while remaining two distinct + # Engine objects. + db_url = f"sqlite:///{tmp_path / 'registry.db'}" + config = SqlRegistryConfig( + registry_type="sql", + path=db_url, + read_path=db_url, + purge_feast_metadata=False, + ) + + registry = SqlRegistry(config, "test_project", None) + try: + assert registry.read_engine is not registry.write_engine + assert checked_engines == [registry.write_engine, registry.read_engine] + finally: + registry.teardown() + + +def test_warn_if_narrow_blob_columns_swallows_query_errors(caplog): + """A failing diagnostic query must never propagate out of registry init.""" + engine = MagicMock() + engine.dialect.name = "mysql" + engine.connect.return_value.__enter__.side_effect = Exception("connection refused") + + with caplog.at_level(logging.ERROR): + # Must not raise even though the query path blows up. + SqlRegistry._warn_if_narrow_blob_columns(engine) + + # The failure is demoted to a debug note, never an error/exception. + assert "still typed BLOB" not in caplog.text + + def test_sql_registry(sqlite_registry): """ Test the SQL registry @@ -56,3 +345,332 @@ def test_sql_registry(sqlite_registry): sqlite_registry.delete_entity("test_entity", "test_project") with pytest.raises(Exception): sqlite_registry.get_entity("test_entity", "test_project") + + +def _build_feature_view(name: str, entity: Entity, source: FileSource) -> FeatureView: + return FeatureView( + name=name, + entities=[entity], + ttl=timedelta(days=1), + schema=[Field(name="conv_rate", dtype=Float32)], + source=source, + ) + + +def test_feature_view_name_conflict_between_stream_and_batch(sqlite_registry): + entity = Entity( + name="driver", + value_type=ValueType.STRING, + join_keys=["driver_id"], + ) + sqlite_registry.apply_entity(entity, "test_project") + + file_source = FileSource( + path="driver_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + batch_view = _build_feature_view("driver_activity", entity, file_source) + sqlite_registry.apply_feature_view(batch_view, "test_project") + + push_source = PushSource(name="driver_push", batch_source=file_source) + stream_view = StreamFeatureView( + name="driver_activity", + source=push_source, + entities=[entity], + schema=[Field(name="conv_rate", dtype=Float32)], + timestamp_field="event_timestamp", + ) + + with pytest.raises(ConflictingFeatureViewNames): + sqlite_registry.apply_feature_view(stream_view, "test_project") + + +def _serialize_udf_referencing_module(module_name: str) -> bytes: + """Create a dill-serialized UDF that references a fake module. + + The function is defined inside a temporary module so that dill records + a dependency on that module. After serialization, the module is removed + from sys.modules so that deserializing the bytes will raise + ModuleNotFoundError. + """ + mod = types.ModuleType(module_name) + mod.__package__ = module_name + sys.modules[module_name] = mod + exec("def _udf(x): return x", mod.__dict__) + udf_bytes = dill.dumps(mod._udf) + del sys.modules[module_name] + return udf_bytes + + +def test_shared_registry_cross_project_udf_does_not_crash(shared_sqlite_db_path): + """Initializing a SqlRegistry must not crash when another project in the + same database has a feature view whose UDF references a module that is + not installed in the current environment. + + Before the fix, proto() called from_proto() on every feature view across + all projects, triggering dill.loads() which raised ModuleNotFoundError. + """ + db_url = f"sqlite:///{shared_sqlite_db_path}" + config = SqlRegistryConfig( + registry_type="sql", path=db_url, purge_feast_metadata=False + ) + + registry_a = SqlRegistry(config, "project_a", None) + + entity = Entity(name="driver", join_keys=["driver_id"]) + registry_a.apply_entity(entity, "project_a") + + file_source = FileSource( + path="driver_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + fv = FeatureView( + name="driver_features", + entities=[entity], + ttl=timedelta(days=1), + schema=[Field(name="conv_rate", dtype=Float32)], + source=file_source, + ) + registry_a.apply_feature_view(fv, "project_a") + + # Inject a UDF body that references a non-existent module directly into + # the DB, simulating a feature view from another project that uses a + # module not available in this environment. + fake_udf_bytes = _serialize_udf_referencing_module("nonexistent_project_module") + + fv_proto = fv.to_proto() + fv_proto.spec.project = "project_a" + fv_proto.spec.feature_transformation.CopyFrom( + FeatureTransformationV2( + user_defined_function=UserDefinedFunctionV2( + name="fake_udf", + body=fake_udf_bytes, + body_text="def _udf(x): return x", + mode="python", + ) + ) + ) + with registry_a.write_engine.begin() as conn: + from sqlalchemy import update + + stmt = ( + update(feature_views) + .where( + feature_views.c.feature_view_name == "driver_features", + feature_views.c.project_id == "project_a", + ) + .values(feature_view_proto=fv_proto.SerializeToString()) + ) + conn.execute(stmt) + + # Creating a new SqlRegistry for project_b against the same DB should + # NOT crash even though project_a has a UDF referencing an unavailable + # module. proto() should read raw protos without deserializing UDFs. + registry_b = SqlRegistry(config, "project_b", None) + + entity_b = Entity(name="customer", join_keys=["customer_id"]) + registry_b.apply_entity(entity_b, "project_b") + retrieved = registry_b.get_entity("customer", "project_b") + assert retrieved.name == "customer" + + # Verify project_a's data is still accessible in the cached proto + proto = registry_b.proto() + project_names = [p.spec.name for p in proto.projects] + assert "project_a" in project_names + assert "project_b" in project_names + + fv_names = [fv.spec.name for fv in proto.feature_views] + assert "driver_features" in fv_names + + registry_a.teardown() + + +def test_list_all_feature_views_updated_since(sqlite_registry): + """Test that _list_all_feature_views filters by updated_since at the SQL level.""" + entity = Entity( + name="driver", + value_type=ValueType.STRING, + join_keys=["driver_id"], + ) + sqlite_registry.apply_entity(entity, "test_project") + + file_source = FileSource( + path="driver_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + fv1 = _build_feature_view("driver_activity_1", entity, file_source) + fv2 = _build_feature_view("driver_activity_2", entity, file_source) + sqlite_registry.apply_feature_view(fv1, "test_project") + sqlite_registry.apply_feature_view(fv2, "test_project") + + # Filtering with a past timestamp returns all feature views + past = datetime(2000, 1, 1, tzinfo=timezone.utc) + result = sqlite_registry.list_all_feature_views("test_project", updated_since=past) + assert len(result) == 2 + + # Filtering with a future timestamp returns nothing + future = datetime(2999, 1, 1, tzinfo=timezone.utc) + result = sqlite_registry.list_all_feature_views( + "test_project", updated_since=future + ) + assert len(result) == 0 + + # No filter returns all feature views + result = sqlite_registry.list_all_feature_views("test_project") + assert len(result) == 2 + + +def test_list_feature_views_updated_since(sqlite_registry): + """Test that _list_feature_views respects updated_since via SQL WHERE clause.""" + entity = Entity( + name="rider", + value_type=ValueType.STRING, + join_keys=["rider_id"], + ) + sqlite_registry.apply_entity(entity, "test_project") + + file_source = FileSource( + path="rider_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + fv = _build_feature_view("rider_activity", entity, file_source) + sqlite_registry.apply_feature_view(fv, "test_project") + + # A cutoff just before the feature view was applied returns it + before = datetime.now(tz=timezone.utc) - timedelta(seconds=60) + result = sqlite_registry._list_feature_views( + "test_project", tags=None, updated_since=before + ) + assert any(fv.name == "rider_activity" for fv in result) + + # A cutoff in the future returns nothing + future = datetime(2999, 1, 1, tzinfo=timezone.utc) + result = sqlite_registry._list_feature_views( + "test_project", tags=None, updated_since=future + ) + assert len(result) == 0 + + +def test_list_feature_views_updated_since_naive_treated_as_utc(sqlite_registry): + """A naive updated_since is treated as UTC, not local time, in the SQL filter.""" + entity = Entity( + name="courier", + value_type=ValueType.STRING, + join_keys=["courier_id"], + ) + sqlite_registry.apply_entity(entity, "test_project") + + file_source = FileSource( + path="courier_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + fv = _build_feature_view("courier_activity", entity, file_source) + sqlite_registry.apply_feature_view(fv, "test_project") + + # A naive past cutoff (interpreted as UTC) should return the feature view + past_naive = datetime(2000, 1, 1) + result = sqlite_registry._list_feature_views( + "test_project", tags=None, updated_since=past_naive + ) + assert any(fv.name == "courier_activity" for fv in result) + + # The equivalent UTC-aware cutoff must produce the same result + past_aware = datetime(2000, 1, 1, tzinfo=timezone.utc) + result_aware = sqlite_registry._list_feature_views( + "test_project", tags=None, updated_since=past_aware + ) + assert len(result) == len(result_aware) + + +class TestSchemaMode: + def test_schema_mode_auto_creates_tables(self, tmp_path): + """Default schema_mode='auto' creates tables on init (existing behavior).""" + db_file = tmp_path / "auto.db" + config = SqlRegistryConfig( + registry_type="sql", + path=f"sqlite:///{db_file}", + schema_mode="auto", + ) + registry = SqlRegistry(config, "test_project", None) + from sqlalchemy import create_engine, inspect + + engine = create_engine(f"sqlite:///{db_file}") + tables = set(inspect(engine).get_table_names()) + expected = set(registry_metadata.tables.keys()) + assert expected.issubset(tables) + engine.dispose() + registry.teardown() + + def test_schema_mode_verify_raises_when_tables_missing(self, tmp_path): + """schema_mode='verify' raises FeastRegistrySchemaError on empty database.""" + db_file = tmp_path / "verify_empty.db" + config = SqlRegistryConfig( + registry_type="sql", + path=f"sqlite:///{db_file}", + schema_mode="verify", + ) + with pytest.raises(FeastRegistrySchemaError, match="missing tables"): + SqlRegistry(config, "test_project", None) + + def test_schema_mode_verify_passes_when_tables_exist(self, tmp_path): + """schema_mode='verify' succeeds when schema was pre-created.""" + db_file = tmp_path / "verify_ok.db" + db_url = f"sqlite:///{db_file}" + from sqlalchemy import create_engine + + engine = create_engine(db_url) + registry_metadata.create_all(engine) + engine.dispose() + + config = SqlRegistryConfig( + registry_type="sql", + path=db_url, + schema_mode="verify", + ) + registry = SqlRegistry(config, "test_project", None) + registry.teardown() + + def test_schema_mode_skip_does_not_run_ddl(self, tmp_path): + """schema_mode='skip' calls neither create_all nor _verify_schema.""" + from unittest.mock import patch + + db_file = tmp_path / "skip.db" + db_url = f"sqlite:///{db_file}" + from sqlalchemy import create_engine + + engine = create_engine(db_url) + registry_metadata.create_all(engine) + engine.dispose() + + with ( + patch.object(registry_metadata, "create_all") as mock_create, + patch.object(SqlRegistry, "_verify_schema") as mock_verify, + ): + config = SqlRegistryConfig( + registry_type="sql", + path=db_url, + schema_mode="skip", + ) + SqlRegistry(config, "test_project", None) + mock_create.assert_not_called() + mock_verify.assert_not_called() + + def test_schema_mode_invalid_value_rejected(self): + """schema_mode only accepts 'auto', 'verify', 'skip'.""" + from pydantic import ValidationError + + with pytest.raises(ValidationError): + SqlRegistryConfig( + registry_type="sql", + path="sqlite:///dummy.db", + schema_mode="bogus", + ) diff --git a/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py b/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py index 2f274d539ff..9034f0cbcb5 100644 --- a/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py +++ b/sdk/python/tests/unit/infra/scaffolding/test_repo_operations.py @@ -28,15 +28,27 @@ def feature_repo(feastignore_contents: Optional[str]): (repo_root / "bar").mkdir() (repo_root / "bar/subdir1").mkdir() (repo_root / "bar/subdir1/subdir2").mkdir() + (repo_root / "foo/__pycache__").mkdir() + (repo_root / "foo/.pytest_cache").mkdir() + (repo_root / "foo/.ipynb_checkpoints").mkdir() + (repo_root / "bar/subdir1/.ipynb_checkpoints").mkdir() + (repo_root / "bar/subdir1/subdir2/.ipynb_checkpoints").mkdir() (repo_root / "a.py").touch() (repo_root / ".ipynb_checkpoints/test-checkpoint.py").touch() (repo_root / "foo/b.py").touch() + (repo_root / "foo/__pycache__/b.cpython.pyc").touch() + (repo_root / "foo/.pytest_cache/test-README.md").touch() + (repo_root / "foo/.ipynb_checkpoints/foo-checkpoint.py").touch() (repo_root / "foo1/c.py").touch() (repo_root / "foo1/bar/d.py").touch() (repo_root / "bar/e.py").touch() (repo_root / "bar/subdir1/f.py").touch() + (repo_root / "bar/subdir1/.ipynb_checkpoints/subdir1-checkpoint.py").touch() (repo_root / "bar/subdir1/subdir2/g.py").touch() + ( + repo_root / "bar/subdir1/subdir2/.ipynb_checkpoints/nested-checkpoint.py" + ).touch() if feastignore_contents: with open(repo_root / ".feastignore", "w") as f: @@ -80,6 +92,7 @@ def test_feastignore_no_stars(): { (repo_root / "foo/b.py").resolve(), (repo_root / "bar/subdir1/f.py").resolve(), + (repo_root / "foo/.ipynb_checkpoints/foo-checkpoint.py").resolve(), } ) assertpy.assert_that(get_repo_files(repo_root)).is_equal_to( @@ -110,6 +123,13 @@ def test_feastignore_with_stars(): { (repo_root / "foo/b.py").resolve(), (repo_root / "bar/subdir1/f.py").resolve(), + ( + repo_root + / "bar/subdir1/subdir2/.ipynb_checkpoints/nested-checkpoint.py" + ).resolve(), + ( + repo_root / "bar/subdir1/.ipynb_checkpoints/subdir1-checkpoint.py" + ).resolve(), (repo_root / "bar/e.py").resolve(), (repo_root / "bar/subdir1/f.py").resolve(), (repo_root / "bar/subdir1/subdir2/g.py").resolve(), @@ -135,6 +155,13 @@ def test_feastignore_with_stars2(): assertpy.assert_that(get_ignore_files(repo_root, ignore_paths)).is_equal_to( { (repo_root / "bar/subdir1/f.py").resolve(), + ( + repo_root + / "bar/subdir1/subdir2/.ipynb_checkpoints/nested-checkpoint.py" + ).resolve(), + ( + repo_root / "bar/subdir1/.ipynb_checkpoints/subdir1-checkpoint.py" + ).resolve(), (repo_root / "bar/e.py").resolve(), (repo_root / "bar/subdir1/f.py").resolve(), (repo_root / "bar/subdir1/subdir2/g.py").resolve(), @@ -162,12 +189,13 @@ def test_parse_repo(): repo_contents = parse_repo(repo_path) - assert len(repo_contents.data_sources) == 3 + assert len(repo_contents.data_sources) == 5 assert len(repo_contents.feature_views) == 2 assert len(repo_contents.on_demand_feature_views) == 2 assert len(repo_contents.stream_feature_views) == 0 assert len(repo_contents.entities) == 2 assert len(repo_contents.feature_services) == 3 + assert len(repo_contents.label_views) == 1 def test_parse_repo_with_future_annotations(): @@ -180,17 +208,18 @@ def test_parse_repo_with_future_annotations(): repo_path = Path(temp_path / "my_project" / "feature_repo") assert result.returncode == 0 - with open(repo_path / "example_repo.py", "r") as f: + with open(repo_path / "feature_definitions.py", "r") as f: existing_content = f.read() - with open(repo_path / "example_repo.py", "w") as f: + with open(repo_path / "feature_definitions.py", "w") as f: f.write("from __future__ import annotations" + "\n" + existing_content) repo_contents = parse_repo(repo_path) - assert len(repo_contents.data_sources) == 3 + assert len(repo_contents.data_sources) == 5 assert len(repo_contents.feature_views) == 2 assert len(repo_contents.on_demand_feature_views) == 2 assert len(repo_contents.stream_feature_views) == 0 assert len(repo_contents.entities) == 2 assert len(repo_contents.feature_services) == 3 + assert len(repo_contents.label_views) == 1 diff --git a/sdk/python/tests/unit/infra/test_key_encoding_utils.py b/sdk/python/tests/unit/infra/test_key_encoding_utils.py index 14433a41e65..ba481adf234 100644 --- a/sdk/python/tests/unit/infra/test_key_encoding_utils.py +++ b/sdk/python/tests/unit/infra/test_key_encoding_utils.py @@ -4,6 +4,7 @@ deserialize_entity_key, reserialize_entity_v2_key_to_v3, serialize_entity_key, + serialize_entity_key_prefix, ) from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto from feast.protos.feast.types.Value_pb2 import Value as ValueProto @@ -151,3 +152,186 @@ def test_reserialize_entity_v2_key_to_v3(): join_keys=["user"], entity_values=[ValueProto(int64_val=int(2**15))], ) + + +def test_single_entity_fast_path(): + """Test that single entity optimization works correctly.""" + entity_key_proto = EntityKeyProto( + join_keys=["user_id"], + entity_values=[ValueProto(string_val="test_user")], + ) + + serialized_key = serialize_entity_key( + entity_key_proto, entity_key_serialization_version=3 + ) + deserialized_key = deserialize_entity_key( + serialized_key, entity_key_serialization_version=3 + ) + + assert deserialized_key == entity_key_proto + + +def test_empty_entity_key(): + """Test handling of empty entity keys.""" + entity_key_proto = EntityKeyProto(join_keys=[], entity_values=[]) + + serialized_key = serialize_entity_key( + entity_key_proto, entity_key_serialization_version=3 + ) + deserialized_key = deserialize_entity_key( + serialized_key, entity_key_serialization_version=3 + ) + + assert deserialized_key == entity_key_proto + + +def test_binary_format_deterministic(): + """Test that serialization is deterministic (same input produces same output).""" + entity_key_proto = EntityKeyProto( + join_keys=["customer", "user", "session"], + entity_values=[ + ValueProto(string_val="cust1"), + ValueProto(string_val="user1"), + ValueProto(string_val="sess1"), + ], + ) + + # Serialize the same entity multiple times + serializations = [] + for _ in range(5): + serialized = serialize_entity_key( + entity_key_proto, entity_key_serialization_version=3 + ) + serializations.append(serialized) + + # All serializations should be identical + for s in serializations[1:]: + assert s == serializations[0], "Serialization is not deterministic" + + +def test_optimization_preserves_sorting(): + """Test that optimizations preserve the sorting behavior for multi-entity keys.""" + # Create entity key with unsorted keys + entity_key_proto = EntityKeyProto( + join_keys=["zebra", "alpha", "beta"], + entity_values=[ + ValueProto(string_val="z_val"), + ValueProto(string_val="a_val"), + ValueProto(string_val="b_val"), + ], + ) + + serialized = serialize_entity_key( + entity_key_proto, entity_key_serialization_version=3 + ) + deserialized = deserialize_entity_key( + serialized, entity_key_serialization_version=3 + ) + + # Keys should be sorted in the result + expected_sorted_keys = ["alpha", "beta", "zebra"] + expected_sorted_values = ["a_val", "b_val", "z_val"] + + assert deserialized.join_keys == expected_sorted_keys + assert [v.string_val for v in deserialized.entity_values] == expected_sorted_values + + +def test_performance_bounds_single_entity(): + """Regression test to ensure single entity performance meets minimum bounds.""" + import time + + entity_key = EntityKeyProto( + join_keys=["user_id"], entity_values=[ValueProto(string_val="user123")] + ) + + # Measure serialization time for 1000 operations + start = time.perf_counter() + for _ in range(1000): + serialize_entity_key(entity_key, entity_key_serialization_version=3) + serialize_time = time.perf_counter() - start + + # Measure deserialization time + serialized = serialize_entity_key(entity_key, entity_key_serialization_version=3) + start = time.perf_counter() + for _ in range(1000): + deserialize_entity_key(serialized, entity_key_serialization_version=3) + deserialize_time = time.perf_counter() - start + + # Performance bounds with generous thresholds to avoid flaky failures on CI runners + assert serialize_time < 0.2, f"Serialization too slow: {serialize_time:.4f}s" + assert deserialize_time < 0.2, f"Deserialization too slow: {deserialize_time:.4f}s" + + +def test_non_ascii_prefix_compatibility(): + """Critical test: ensure prefix serialization matches full entity key serialization for non-ASCII keys.""" + # Test with non-ASCII characters that have different byte vs character lengths + non_ascii_keys = ["用户ID", "사용자ID", "идентификатор", "مُعرِّف"] + + for key in non_ascii_keys: + # Test single key prefix + prefix_result = serialize_entity_key_prefix( + [key], entity_key_serialization_version=3 + ) + + # Create full entity key and serialize it + entity_key = EntityKeyProto( + join_keys=[key], entity_values=[ValueProto(string_val="test_value")] + ) + full_result = serialize_entity_key( + entity_key, entity_key_serialization_version=3 + ) + + # The prefix should match the beginning of the full serialization + # Extract just the key portion (skip entity count, but include key metadata) + prefix_len = len(prefix_result) + assert full_result[:prefix_len] == prefix_result, ( + f"Prefix mismatch for non-ASCII key '{key}': " + f"Character length: {len(key)}, " + f"UTF-8 byte length: {len(key.encode('utf8'))}" + ) + + +def test_ascii_prefix_compatibility(): + """Verify prefix compatibility still works for ASCII keys.""" + ascii_keys = ["user_id", "session_id", "device_id"] + + for key in ascii_keys: + prefix_result = serialize_entity_key_prefix( + [key], entity_key_serialization_version=3 + ) + + entity_key = EntityKeyProto( + join_keys=[key], entity_values=[ValueProto(string_val="test_value")] + ) + full_result = serialize_entity_key( + entity_key, entity_key_serialization_version=3 + ) + + prefix_len = len(prefix_result) + assert full_result[:prefix_len] == prefix_result, ( + f"Prefix mismatch for ASCII key '{key}'" + ) + + +def test_multi_key_non_ascii_prefix_compatibility(): + """Test multi-key prefix compatibility with non-ASCII characters.""" + mixed_keys = ["user_id", "用户会话", "session_id"] # Mix ASCII and non-ASCII + + prefix_result = serialize_entity_key_prefix( + mixed_keys, entity_key_serialization_version=3 + ) + + entity_key = EntityKeyProto( + join_keys=mixed_keys, + entity_values=[ + ValueProto(string_val="test1"), + ValueProto(string_val="test2"), + ValueProto(string_val="test3"), + ], + ) + full_result = serialize_entity_key(entity_key, entity_key_serialization_version=3) + + prefix_len = len(prefix_result) + assert full_result[:prefix_len] == prefix_result, ( + "Multi-key prefix mismatch with non-ASCII" + ) diff --git a/sdk/python/tests/unit/infra/utils/snowflake/test_snowflake_udfs.py b/sdk/python/tests/unit/infra/utils/snowflake/test_snowflake_udfs.py new file mode 100644 index 00000000000..5e8046bd83d --- /dev/null +++ b/sdk/python/tests/unit/infra/utils/snowflake/test_snowflake_udfs.py @@ -0,0 +1,182 @@ +import os +import re + +import pytest + +DECOMMISSIONED_RUNTIME_VERSION = "3.9" + +# Snowflake requires the Python UDF RUNTIME_VERSION to match a Python runtime +# that Snowflake still supports, and Feast should never request a runtime +# below the minimum Python version Feast itself supports +# (see `requires-python` in pyproject.toml). +MIN_SUPPORTED_RUNTIME_VERSION = "3.10" + +# Placeholder substituted into snowflake_python_udfs_creation.sql at deploy time by +# SnowflakeComputeEngine.update(), using +# SnowflakeComputeEngineConfig.python_udf_runtime_version. See +# https://github.com/feast-dev/feast/issues/6606 and the follow-up request to make +# the runtime version user-configurable on PR #6608. +RUNTIME_VERSION_PLACEHOLDER = "RUNTIME_VERSION_HOLDER" + + +def _snowpark_dir() -> str: + import feast + + return os.path.join( + feast.__path__[0], + "infra", + "utils", + "snowflake", + "snowpark", + ) + + +def _render_udf_creation_sql(runtime_version: str) -> str: + """Reproduce the substitution SnowflakeComputeEngine.update() performs on + snowflake_python_udfs_creation.sql, using a given runtime version, without + needing a live Snowflake connection. + """ + sql_path = os.path.join(_snowpark_dir(), "snowflake_python_udfs_creation.sql") + with open(sql_path) as f: + sql = f.read() + + sql = sql.replace("STAGE_HOLDER", "feast_stage") + sql = sql.replace("PROJECT_NAME", "myproject") + sql = sql.replace(RUNTIME_VERSION_PLACEHOLDER, runtime_version) + return sql + + +def test_udf_creation_sql_template_uses_runtime_version_placeholder(): + """The CREATE FUNCTION statements Feast executes against Snowflake must not + hardcode a specific (and eventually decommissioned) Python runtime version. + Instead, the SQL template should carry a placeholder that + SnowflakeComputeEngine.update() fills in from the user-configurable + `python_udf_runtime_version` setting (see + https://github.com/feast-dev/feast/issues/6606). + """ + sql_path = os.path.join(_snowpark_dir(), "snowflake_python_udfs_creation.sql") + with open(sql_path) as f: + sql = f.read() + + versions = re.findall(r"RUNTIME_VERSION\s*=\s*'([^']+)'", sql) + assert versions, ( + "expected to find RUNTIME_VERSION declarations in the UDF creation SQL" + ) + assert DECOMMISSIONED_RUNTIME_VERSION not in versions, ( + f"snowflake_python_udfs_creation.sql still requests the decommissioned " + f"Snowflake Python runtime {DECOMMISSIONED_RUNTIME_VERSION!r}" + ) + assert all(v == RUNTIME_VERSION_PLACEHOLDER for v in versions), ( + "expected all RUNTIME_VERSION declarations to use the " + f"{RUNTIME_VERSION_PLACEHOLDER!r} placeholder (filled in at deploy time " + f"from the configurable python_udf_runtime_version), got " + f"{sorted(set(versions))}" + ) + + +def test_udf_creation_sql_renders_default_runtime_version(): + """When rendered with the default `python_udf_runtime_version` + ("3.10"), the UDF creation SQL should request Feast's default runtime and + never the decommissioned 3.9 runtime. + """ + from feast.infra.compute_engines.snowflake.snowflake_engine import ( + SnowflakeComputeEngineConfig, + ) + + default_runtime_version = SnowflakeComputeEngineConfig( + database="FEAST" + ).python_udf_runtime_version + assert default_runtime_version == MIN_SUPPORTED_RUNTIME_VERSION + + rendered_sql = _render_udf_creation_sql(default_runtime_version) + versions = re.findall(r"RUNTIME_VERSION\s*=\s*'([^']+)'", rendered_sql) + assert versions, "expected to find rendered RUNTIME_VERSION declarations" + assert DECOMMISSIONED_RUNTIME_VERSION not in versions + assert all(v == MIN_SUPPORTED_RUNTIME_VERSION for v in versions) + + +def test_udf_creation_sql_renders_custom_runtime_version(): + """A custom `python_udf_runtime_version` should flow through to every + rendered RUNTIME_VERSION declaration, so users aren't blocked on a Feast + release the next time Snowflake decommissions a runtime. + """ + custom_runtime_version = "3.12" + rendered_sql = _render_udf_creation_sql(custom_runtime_version) + + versions = re.findall(r"RUNTIME_VERSION\s*=\s*'([^']+)'", rendered_sql) + assert versions, "expected to find rendered RUNTIME_VERSION declarations" + assert RUNTIME_VERSION_PLACEHOLDER not in versions + assert all(v == custom_runtime_version for v in versions), ( + f"expected all RUNTIME_VERSION declarations to be " + f"{custom_runtime_version!r}, got {sorted(set(versions))}" + ) + + +def test_udf_module_docstrings_do_not_reference_decommissioned_runtime_version(): + """The reference `CREATE OR REPLACE FUNCTION` docstrings embedded in + snowflake_udfs.py document the same UDFs and should stay consistent with + the SQL template that is actually executed. + + Note: we read the file as text rather than importing the module, since + `snowflake_udfs.py` imports `_snowflake`, which is only available inside + Snowflake's own Python UDF runtime. + """ + udfs_path = os.path.join(_snowpark_dir(), "snowflake_udfs.py") + with open(udfs_path) as f: + source = f.read() + + versions = re.findall(r"RUNTIME_VERSION\s*=\s*'([^']+)'", source) + assert versions, ( + "expected to find RUNTIME_VERSION declarations in snowflake_udfs.py" + ) + assert DECOMMISSIONED_RUNTIME_VERSION not in versions, ( + f"snowflake_udfs.py docstrings still reference the decommissioned " + f"Snowflake Python runtime {DECOMMISSIONED_RUNTIME_VERSION!r}" + ) + + +def test_python_udf_runtime_version_defaults_to_3_10(): + """The Snowflake compute engine config should default + `python_udf_runtime_version` to "3.10", matching Feast's own minimum + supported Python version, without requiring users to set it explicitly. + """ + from feast.infra.compute_engines.snowflake.snowflake_engine import ( + SnowflakeComputeEngineConfig, + ) + + config = SnowflakeComputeEngineConfig(database="FEAST") + assert config.python_udf_runtime_version == MIN_SUPPORTED_RUNTIME_VERSION + + +def test_python_udf_runtime_version_accepts_custom_value(): + """Per maintainer feedback on + https://github.com/feast-dev/feast/pull/6608 (@ntkathole: "is it possible + to make it user configurable?"), users must be able to override the + runtime version Feast requests, e.g. once Snowflake decommissions 3.10. + """ + from feast.infra.compute_engines.snowflake.snowflake_engine import ( + SnowflakeComputeEngineConfig, + ) + + config = SnowflakeComputeEngineConfig( + database="FEAST", python_udf_runtime_version="3.11" + ) + assert config.python_udf_runtime_version == "3.11" + + +@pytest.mark.parametrize("bad_version", ["", "three-ten", "3", "3.x", "3.10.", "v3.10"]) +def test_python_udf_runtime_version_rejects_invalid_values(bad_version): + """python_udf_runtime_version should validate that it looks like a Python + version string (e.g. "3.10" or "3.11.2"), so misconfiguration is caught + early rather than surfacing as an opaque Snowflake SQL compilation error. + """ + from pydantic import ValidationError + + from feast.infra.compute_engines.snowflake.snowflake_engine import ( + SnowflakeComputeEngineConfig, + ) + + with pytest.raises(ValidationError): + SnowflakeComputeEngineConfig( + database="FEAST", python_udf_runtime_version=bad_version + ) diff --git a/sdk/python/tests/unit/infra/utils/snowflake/test_snowflake_utils.py b/sdk/python/tests/unit/infra/utils/snowflake/test_snowflake_utils.py index 8ae6ec63ba5..4bb02076354 100644 --- a/sdk/python/tests/unit/infra/utils/snowflake/test_snowflake_utils.py +++ b/sdk/python/tests/unit/infra/utils/snowflake/test_snowflake_utils.py @@ -1,11 +1,16 @@ import tempfile from typing import Optional +from unittest.mock import MagicMock, patch import pytest from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa -from feast.infra.utils.snowflake.snowflake_utils import parse_private_key_path +from feast.infra.utils.snowflake.snowflake_utils import ( + GetSnowflakeConnection, + execute_snowflake_statement, + parse_private_key_path, +) PRIVATE_KEY_PASSPHRASE = "test" @@ -69,3 +74,96 @@ def test_parse_private_key_path_key_path_encrypted(encrypted_private_key): f.name, None, ) + + +class _AttrDict(dict): + __getattr__ = dict.__getitem__ + + +def _make_config(**overrides): + defaults = { + "type": "snowflake.offline", + "account": "test_account", + "user": "test_user", + "password": "test_password", # pragma: allowlist secret + "role": "test_role", + "warehouse": "test_wh", + "database": "test_db", + "schema_": "test_schema", + "config_path": "", + } + defaults.update(overrides) + return _AttrDict(defaults) + + +@patch("feast.infra.utils.snowflake.snowflake_utils.snowflake.connector") +class TestGetSnowflakeConnectionIdentifierQuoting: + @pytest.fixture(autouse=True) + def _clear_cache(self): + with patch("feast.infra.utils.snowflake.snowflake_utils._cache", {}): + yield + + @pytest.mark.parametrize( + "config_key,connect_key,value", + [ + ("warehouse", "warehouse", "MY_WH"), + ("role", "role", "ANALYST"), + ("database", "database", "PROD_DB"), + ("schema_", "schema", "PUBLIC"), + ], + ) + def test_identifier_passed_without_quoting( + self, mock_connector, config_key, connect_key, value + ): + mock_connector.connect.return_value = MagicMock() + + with GetSnowflakeConnection(_make_config(**{config_key: value})): + pass + + kwargs = mock_connector.connect.call_args[1] + assert kwargs[connect_key] == value + + def test_schema_key_renamed_from_schema_underscore(self, mock_connector): + mock_connector.connect.return_value = MagicMock() + + with GetSnowflakeConnection(_make_config(schema_="analytics")): + pass + + kwargs = mock_connector.connect.call_args[1] + assert "schema" in kwargs + assert "schema_" not in kwargs + + +class TestExecuteSnowflakeStatement: + def test_empty_query_is_passed_through_to_execute(self): + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_executed_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.execute.return_value = mock_executed_cursor + + result = execute_snowflake_statement(mock_conn, "") + + assert result is mock_executed_cursor + mock_cursor.execute.assert_called_once_with("") + + def test_valid_query_executes_and_returns_cursor(self): + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_executed_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.execute.return_value = mock_executed_cursor + + result = execute_snowflake_statement(mock_conn, "SELECT 1") + + assert result is mock_executed_cursor + mock_cursor.execute.assert_called_once_with("SELECT 1") + + def test_valid_query_raises_on_none_cursor(self): + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_cursor.execute.return_value = None + + with pytest.raises(Exception, match="Snowflake query failed"): + execute_snowflake_statement(mock_conn, "SELECT 1") diff --git a/sdk/python/tests/unit/local_feast_tests/test_feature_service.py b/sdk/python/tests/unit/local_feast_tests/test_feature_service.py index 75ceb463085..400d7ae86b2 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_feature_service.py +++ b/sdk/python/tests/unit/local_feast_tests/test_feature_service.py @@ -6,7 +6,7 @@ create_driver_hourly_stats_df, create_global_daily_stats_df, ) -from tests.integration.feature_repos.universal.feature_views import TAGS +from tests.universal.feature_repos.universal.feature_views import TAGS from tests.utils.basic_read_write_test import basic_rw_test from tests.utils.cli_repo_creator import CliRunner, get_example_repo @@ -56,22 +56,24 @@ def test_apply_with_fv_inference() -> None: driver_df = create_driver_hourly_stats_df(driver_entities, start_date, end_date) driver_stats_path = os.path.join(data_dir, "driver_stats.parquet") driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) + driver_stats_path_posix = driver_stats_path.replace("\\", "/") global_df = create_global_daily_stats_df(start_date, end_date) global_stats_path = os.path.join(data_dir, "global_stats.parquet") global_df.to_parquet(path=global_stats_path, allow_truncated_timestamps=True) + global_stats_path_posix = global_stats_path.replace("\\", "/") with runner.local_repo( get_example_repo("example_feature_repo_with_feature_service_3.py") - .replace("%PARQUET_PATH%", driver_stats_path) - .replace("%PARQUET_PATH_GLOBAL%", global_stats_path), + .replace("%PARQUET_PATH%", driver_stats_path_posix) + .replace("%PARQUET_PATH_GLOBAL%", global_stats_path_posix), "file", ) as store: assert len(store.list_feature_services()) == 2 fs = store.get_feature_service("all_stats") assert len(fs.feature_view_projections) == 2 - assert len(fs.feature_view_projections[0].features) == 3 + assert len(fs.feature_view_projections[0].features) == 6 assert len(fs.feature_view_projections[0].desired_features) == 0 assert len(fs.feature_view_projections[1].features) == 2 assert len(fs.feature_view_projections[1].desired_features) == 0 diff --git a/sdk/python/tests/unit/local_feast_tests/test_init.py b/sdk/python/tests/unit/local_feast_tests/test_init.py index 4543a239796..1f3c0993845 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_init.py +++ b/sdk/python/tests/unit/local_feast_tests/test_init.py @@ -67,3 +67,17 @@ def test_repo_init_with_underscore_in_project_name() -> None: ) result = runner.run(["apply"], cwd=repo_dir) assert result.returncode != 0 + + +def test_postgres_template_registry_path_is_parameterized() -> None: + template_fs_yaml = ( + Path(__file__).resolve().parents[3] + / "feast" + / "templates" + / "postgres" + / "feature_repo" + / "feature_store.yaml" + ) + contents = template_fs_yaml.read_text(encoding="utf-8") + expected = "path: postgresql://DB_USERNAME:DB_PASSWORD@DB_HOST:DB_PORT/DB_NAME" + assert expected in contents diff --git a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py index cbb9d3d334a..ec2513f0726 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py +++ b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py @@ -1,7 +1,8 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from tempfile import mkstemp -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch +import pandas as pd import pytest from pytest_lazyfixture import lazy_fixture @@ -10,19 +11,23 @@ from feast.data_format import AvroFormat, ParquetFormat from feast.data_source import KafkaSource from feast.entity import Entity +from feast.errors import ConflictingFeatureViewNames from feast.feast_object import ALL_RESOURCE_TYPES from feast.feature_store import FeatureStore from feast.feature_view import DUMMY_ENTITY_ID, DUMMY_ENTITY_NAME, FeatureView from feast.field import Field from feast.infra.offline_stores.file_source import FileSource +from feast.infra.online_stores.dynamodb import DynamoDBOnlineStore from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.on_demand_feature_view import OnDemandFeatureView from feast.permissions.action import AuthzedAction from feast.permissions.permission import Permission from feast.permissions.policy import RoleBasedPolicy from feast.repo_config import RegistryConfig, RepoConfig from feast.stream_feature_view import stream_feature_view +from feast.transformation.pandas_transformation import PandasTransformation from feast.types import Array, Bytes, Float32, Int64, String, ValueType, from_value_type -from tests.integration.feature_repos.universal.feature_views import TAGS +from tests.universal.feature_repos.universal.feature_views import TAGS from tests.utils.cli_repo_creator import CliRunner, get_example_repo from tests.utils.data_source_test_creator import prep_file_source @@ -508,6 +513,69 @@ def test_reapply_feature_view(test_feature_store, dataframe_source): test_feature_store.teardown() +@pytest.mark.parametrize( + "test_feature_store", + [lazy_fixture("feature_store_with_local_registry")], +) +@pytest.mark.parametrize("dataframe_source", [lazy_fixture("simple_dataset_1")]) +def test_materialize_incremental_odfv_entity_name_differs_from_join_key( + test_feature_store, dataframe_source +): + """Regression test for https://github.com/feast-dev/feast/issues/5965. + + _materialize_odfv must resolve source feature views' join key *columns* + (FeatureView.join_keys), not their entity *names* (FeatureView.entities), + when building the entity_df and querying the offline store. Before the + fix, this failed whenever an Entity's name differed from its join_keys. + """ + with prep_file_source(df=dataframe_source, timestamp_field="ts_1") as file_source: + # Entity name ("id") intentionally differs from its join key + # ("id_join_key"), mirroring the exact repro from the issue. + e = Entity(name="id", join_keys=["id_join_key"]) + + source_fv = FeatureView( + name="my_feature_view_1", + schema=[Field(name="float_col", dtype=Float32)], + entities=[e], + source=file_source, + ttl=timedelta(days=3650), + ) + + def transform(features_df: pd.DataFrame) -> pd.DataFrame: + out = pd.DataFrame() + out["label"] = features_df["float_col"].apply( + lambda v: "high" if v >= 1 else "low" + ) + return out + + odfv = OnDemandFeatureView( + name="my_odfv", + entities=[e], + sources=[source_fv], + schema=[Field(name="label", dtype=String)], + feature_transformation=PandasTransformation( + udf=transform, udf_string="transform" + ), + write_to_online_store=True, + ) + + test_feature_store.apply([e, source_fv, odfv]) + + # Should not raise. Before the fix, this raised + # FeastJoinKeysDuringMaterialization because _materialize_odfv + # queried the offline store for a column named "id" (the entity + # name) instead of "id_join_key" (the actual join key column). + test_feature_store.materialize_incremental(end_date=datetime.now(timezone.utc)) + + response = test_feature_store.get_online_features( + features=["my_odfv:label"], + entity_rows=[{"id_join_key": 1}], + ).to_dict() + assert response["label"][0] is not None + + test_feature_store.teardown() + + def test_apply_conflicting_feature_view_names(feature_store_with_local_registry): """Test applying feature views with non-case-insensitively unique names""" driver = Entity(name="driver", join_keys=["driver_id"]) @@ -532,16 +600,10 @@ def test_apply_conflicting_feature_view_names(feature_store_with_local_registry) source=FileSource(path="customer_stats.parquet"), tags={}, ) - try: + with pytest.raises(ConflictingFeatureViewNames) as exc_info: feature_store_with_local_registry.apply([driver_stats, customer_stats]) - error = None - except ValueError as e: - error = e - assert ( - isinstance(error, ValueError) - and "Please ensure that all feature view names are case-insensitively unique" - in error.args[0] - ) + + assert "Feature view names must be case-insensitively unique" in str(exc_info.value) feature_store_with_local_registry.teardown() @@ -862,3 +924,73 @@ def test_registry_config_cache_mode_can_be_set(): config = RegistryConfig(cache_mode="sync") assert config.cache_mode == "sync" + + +# Tests for update_online_store functionality + + +@pytest.mark.asyncio +async def test_update_online_store_not_supported(): + """Test that update raises NotImplementedError for non-DynamoDB stores.""" + + fd, registry_path = mkstemp() + fd, online_store_path = mkstemp() + store = FeatureStore( + config=RepoConfig( + registry=registry_path, + project="default", + provider="local", + online_store=SqliteOnlineStoreConfig(path=online_store_path), + entity_key_serialization_version=3, + ) + ) + + df = pd.DataFrame({"entity_id": ["1", "2"], "transactions": [["tx1"], ["tx2"]]}) + + update_expressions = {"transactions": "list_append(transactions, :new_val)"} + + with pytest.raises(NotImplementedError) as exc_info: + await store.update_online_store( + feature_view_name="test_fv", df=df, update_expressions=update_expressions + ) + + assert "does not support async update expressions" in str(exc_info.value) + assert "DynamoDB online store" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_update_online_store_success(): + """Test successful update_online_store call.""" + + with ( + patch( + "feast.feature_store.FeatureStore._get_feature_view_and_df_for_online_write" + ) as mock_get_fv_df, + patch( + "feast.infra.passthrough_provider.PassthroughProvider._prep_rows_to_write_for_ingestion" + ) as mock_prep, + patch("feast.feature_store.load_repo_config") as mock_load_config, + patch("feast.feature_store.Registry"), + patch("feast.feature_store.get_provider") as mock_get_provider, + ): + mock_online_store = Mock(spec=DynamoDBOnlineStore) + mock_online_store.update_online_store_async = AsyncMock() + mock_provider = Mock() + mock_provider.online_store = mock_online_store + mock_get_provider.return_value = mock_provider + mock_prep.return_value = [] + mock_load_config.return_value = Mock() + + df = pd.DataFrame({"entity_id": ["1", "2"], "transactions": [["tx1"], ["tx2"]]}) + mock_feature_view = Mock() + mock_feature_view.features = [Field(name="transactions", dtype=Array(String))] + mock_get_fv_df.return_value = (mock_feature_view, df) + + store = FeatureStore() + update_expressions = {"transactions": "list_append(transactions, :new_val)"} + + await store.update_online_store( + feature_view_name="test_fv", df=df, update_expressions=update_expressions + ) + + mock_online_store.update_online_store_async.assert_called_once() diff --git a/sdk/python/tests/unit/monitoring/__init__.py b/sdk/python/tests/unit/monitoring/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/sdk/python/tests/unit/monitoring/__init__.py @@ -0,0 +1 @@ + diff --git a/sdk/python/tests/unit/monitoring/test_compute_correctness.py b/sdk/python/tests/unit/monitoring/test_compute_correctness.py new file mode 100644 index 00000000000..9f9005380df --- /dev/null +++ b/sdk/python/tests/unit/monitoring/test_compute_correctness.py @@ -0,0 +1,1831 @@ +""" +Compute correctness tests for monitoring metric calculations. + +Verifies that each offline store backend's SQL/compute helpers produce +mathematically correct results for a known golden dataset. + +- DuckDB and Dask tests run fully in-memory with zero external dependencies. +- PostgreSQL tests require a live Postgres instance (skipped if unavailable). +- Snowflake, BigQuery, Redshift, Spark, Oracle tests require their respective + backends (skipped if unavailable). +""" + +import statistics +from datetime import datetime, timezone +from typing import Any, Dict + +import pyarrow as pa +import pytest + +# --------------------------------------------------------------------------- +# Golden dataset: known values with hand-computable statistics +# --------------------------------------------------------------------------- + +NUMERIC_VALUES = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] +NUMERIC_WITH_NULLS = [1.0, None, 3.0, None, 5.0, None, 7.0, None, 9.0, None] +CATEGORICAL_VALUES = ["a", "b", "a", "c", "a", "b", "d", "a", "b", "c"] + +ROW_COUNT = len(NUMERIC_VALUES) +NON_NULL_VALUES = [v for v in NUMERIC_WITH_NULLS if v is not None] + + +def _expected_numeric_stats(): + """Hand-computed expected values for NUMERIC_VALUES = [1..10].""" + vals = NUMERIC_VALUES + return { + "row_count": 10, + "null_count": 0, + "null_rate": 0.0, + "mean": 5.5, + "stddev": statistics.stdev(vals), # sample stddev ≈ 3.0277 + "min_val": 1.0, + "max_val": 10.0, + "p50": 5.5, + "p75": 7.75, + "p90": 9.1, + "p95": 9.55, + "p99": 9.91, + } + + +def _expected_numeric_with_nulls_stats(): + """Hand-computed expected values for NUMERIC_WITH_NULLS.""" + vals = NON_NULL_VALUES # [1, 3, 5, 7, 9] + return { + "row_count": 10, + "null_count": 5, + "null_rate": 0.5, + "mean": 5.0, + "stddev": statistics.stdev(vals), # sample stddev ≈ 3.1623 + "min_val": 1.0, + "max_val": 9.0, + } + + +def _expected_categorical_stats(): + """Expected values for CATEGORICAL_VALUES.""" + return { + "row_count": 10, + "null_count": 0, + "null_rate": 0.0, + "unique_count": 4, + "top_value": "a", + "top_count": 4, + } + + +# --------------------------------------------------------------------------- +# Shared assertions: validate any backend's numeric/categorical result +# --------------------------------------------------------------------------- + + +def assert_numeric_correctness( + result: Dict[str, Any], expected: Dict, label: str, approx_percentiles: bool = False +): + """Assert that a numeric result from any backend matches expected values.""" + assert result["feature_type"] == "numeric", f"{label}: wrong feature_type" + assert result["row_count"] == expected["row_count"], f"{label}: wrong row_count" + assert result["null_count"] == expected["null_count"], f"{label}: wrong null_count" + assert result["null_rate"] == pytest.approx(expected["null_rate"], abs=1e-6), ( + f"{label}: wrong null_rate" + ) + assert result["mean"] == pytest.approx(expected["mean"], abs=1e-4), ( + f"{label}: wrong mean" + ) + assert result["stddev"] == pytest.approx(expected["stddev"], abs=0.05), ( + f"{label}: wrong stddev" + ) + assert result["min_val"] == pytest.approx(expected["min_val"], abs=1e-6), ( + f"{label}: wrong min_val" + ) + assert result["max_val"] == pytest.approx(expected["max_val"], abs=1e-6), ( + f"{label}: wrong max_val" + ) + + if "p50" in expected and not approx_percentiles: + assert result["p50"] == pytest.approx(expected["p50"], abs=0.5), ( + f"{label}: wrong p50" + ) + assert result["p75"] == pytest.approx(expected["p75"], abs=0.5), ( + f"{label}: wrong p75" + ) + assert result["p90"] == pytest.approx(expected["p90"], abs=0.5), ( + f"{label}: wrong p90" + ) + + # Percentile ordering is always enforced + assert result["p50"] <= result["p75"], f"{label}: p50 > p75" + assert result["p75"] <= result["p90"], f"{label}: p75 > p90" + assert result["p90"] <= result["p95"], f"{label}: p90 > p95" + assert result["p95"] <= result["p99"], f"{label}: p95 > p99" + assert result["p50"] >= result["min_val"], f"{label}: p50 < min" + assert result["p99"] <= result["max_val"], f"{label}: p99 > max" + + +def assert_histogram_correctness( + result: Dict[str, Any], label: str, expected_bins: int = 5 +): + """Assert that a numeric histogram has consistent structure and totals.""" + hist = result.get("histogram") + assert hist is not None, f"{label}: histogram is None" + assert len(hist["counts"]) == expected_bins, f"{label}: wrong number of bins" + assert len(hist["bins"]) == expected_bins + 1, f"{label}: wrong number of bin edges" + total = sum(hist["counts"]) + non_null = result["row_count"] - result["null_count"] + assert total == non_null, f"{label}: histogram total {total} != non_null {non_null}" + assert hist["bins"][0] <= result["min_val"], f"{label}: first bin edge > min_val" + assert hist["bins"][-1] >= result["max_val"], f"{label}: last bin edge < max_val" + + +def assert_categorical_correctness(result: Dict[str, Any], expected: Dict, label: str): + """Assert that a categorical result from any backend matches expected values.""" + assert result["feature_type"] == "categorical", f"{label}: wrong feature_type" + assert result["row_count"] == expected["row_count"], f"{label}: wrong row_count" + assert result["null_count"] == expected["null_count"], f"{label}: wrong null_count" + assert result["null_rate"] == pytest.approx(expected["null_rate"], abs=1e-6), ( + f"{label}: wrong null_rate" + ) + + hist = result["histogram"] + assert hist is not None, f"{label}: histogram is None" + assert hist["unique_count"] == expected["unique_count"], ( + f"{label}: wrong unique_count" + ) + + top_entry = hist["values"][0] + assert top_entry["value"] == expected["top_value"], f"{label}: wrong top value" + assert top_entry["count"] == expected["top_count"], f"{label}: wrong top count" + + total = sum(e["count"] for e in hist["values"]) + hist["other_count"] + expected_total = expected["row_count"] - expected["null_count"] + assert total == expected_total, ( + f"{label}: categorical total {total} != expected {expected_total}" + ) + + +# =================================================================== +# DuckDB compute correctness tests (fully in-memory, no external deps) +# =================================================================== + + +class TestDuckDBComputeCorrectness: + """Test DuckDB SQL helper functions produce correct metric values.""" + + @pytest.fixture(autouse=True) + def setup_duckdb(self): + duckdb = pytest.importorskip("duckdb") + self.conn = duckdb.connect() + + self.conn.execute(""" + CREATE TABLE test_data ( + event_timestamp TIMESTAMP, + numeric_col DOUBLE, + numeric_with_nulls DOUBLE, + categorical_col VARCHAR + ) + """) + + ts = datetime(2025, 1, 15, 12, 0, 0) + for i in range(ROW_COUNT): + n_val = NUMERIC_VALUES[i] + n_null = NUMERIC_WITH_NULLS[i] + c_val = CATEGORICAL_VALUES[i] + n_null_sql = f"{n_null}" if n_null is not None else "NULL" + self.conn.execute( + f"INSERT INTO test_data VALUES " + f"(TIMESTAMP '{ts.strftime('%Y-%m-%d %H:%M:%S')}', " + f"{n_val}, {n_null_sql}, '{c_val}')" + ) + yield + self.conn.close() + + def test_numeric_stats_basic(self): + from feast.infra.offline_stores.duckdb import _duckdb_numeric_stats + + results = _duckdb_numeric_stats( + self.conn, + "test_data", + ["numeric_col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + result = results[0] + expected = _expected_numeric_stats() + assert_numeric_correctness(result, expected, "duckdb_numeric") + assert_histogram_correctness(result, "duckdb_numeric", expected_bins=5) + + def test_numeric_stats_with_nulls(self): + from feast.infra.offline_stores.duckdb import _duckdb_numeric_stats + + results = _duckdb_numeric_stats( + self.conn, + "test_data", + ["numeric_with_nulls"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + result = results[0] + expected = _expected_numeric_with_nulls_stats() + assert_numeric_correctness(result, expected, "duckdb_numeric_nulls") + assert_histogram_correctness(result, "duckdb_numeric_nulls", expected_bins=5) + + def test_numeric_multiple_features(self): + from feast.infra.offline_stores.duckdb import _duckdb_numeric_stats + + results = _duckdb_numeric_stats( + self.conn, + "test_data", + ["numeric_col", "numeric_with_nulls"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 2 + assert results[0]["feature_name"] == "numeric_col" + assert results[1]["feature_name"] == "numeric_with_nulls" + assert results[0]["mean"] == pytest.approx(5.5, abs=1e-4) + assert results[1]["mean"] == pytest.approx(5.0, abs=1e-4) + assert results[0]["null_count"] == 0 + assert results[1]["null_count"] == 5 + + def test_categorical_stats(self): + from feast.infra.offline_stores.duckdb import _duckdb_categorical_stats + + result = _duckdb_categorical_stats( + self.conn, + "test_data", + "categorical_col", + "1=1", + top_n=10, + ) + + expected = _expected_categorical_stats() + assert_categorical_correctness(result, expected, "duckdb_categorical") + + def test_categorical_top_n_truncation(self): + from feast.infra.offline_stores.duckdb import _duckdb_categorical_stats + + result = _duckdb_categorical_stats( + self.conn, + "test_data", + "categorical_col", + "1=1", + top_n=2, + ) + + assert len(result["histogram"]["values"]) == 2 + assert result["histogram"]["other_count"] > 0 + total = ( + sum(e["count"] for e in result["histogram"]["values"]) + + result["histogram"]["other_count"] + ) + assert total == 10 + + def test_histogram_bin_edges_cover_range(self): + from feast.infra.offline_stores.duckdb import _duckdb_numeric_histogram + + hist = _duckdb_numeric_histogram( + self.conn, + "test_data", + "numeric_col", + "1=1", + bins=5, + min_val=1.0, + max_val=10.0, + ) + + assert hist["bins"][0] == pytest.approx(1.0, abs=1e-6) + assert hist["bins"][-1] == pytest.approx(10.0, abs=0.1) + assert sum(hist["counts"]) == 10 + assert hist["bin_width"] == pytest.approx(1.8, abs=0.01) + + def test_histogram_single_value(self): + from feast.infra.offline_stores.duckdb import _duckdb_numeric_histogram + + self.conn.execute(""" + CREATE TABLE single_val (event_timestamp TIMESTAMP, v DOUBLE) + """) + self.conn.execute( + "INSERT INTO single_val VALUES (TIMESTAMP '2025-01-15 12:00:00', 42.0)" + ) + + hist = _duckdb_numeric_histogram( + self.conn, + "single_val", + "v", + "1=1", + bins=5, + min_val=42.0, + max_val=42.0, + ) + + assert hist["counts"] == [1] + assert hist["bin_width"] == 0.0 + + def test_empty_table(self): + from feast.infra.offline_stores.duckdb import _duckdb_numeric_stats + + self.conn.execute(""" + CREATE TABLE empty_tbl (event_timestamp TIMESTAMP, v DOUBLE) + """) + results = _duckdb_numeric_stats( + self.conn, + "empty_tbl", + ["v"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + assert results[0]["row_count"] == 0 + assert results[0]["mean"] is None + assert results[0]["histogram"] is None + + def test_stddev_with_single_row(self): + from feast.infra.offline_stores.duckdb import _duckdb_numeric_stats + + self.conn.execute(""" + CREATE TABLE one_row (event_timestamp TIMESTAMP, v DOUBLE) + """) + self.conn.execute( + "INSERT INTO one_row VALUES (TIMESTAMP '2025-01-15 12:00:00', 7.0)" + ) + results = _duckdb_numeric_stats( + self.conn, + "one_row", + ["v"], + "1=1", + histogram_bins=5, + ) + + assert results[0]["mean"] == pytest.approx(7.0) + assert results[0]["min_val"] == 7.0 + assert results[0]["max_val"] == 7.0 + # STDDEV_SAMP of a single value is NULL + assert results[0]["stddev"] is None + + def test_large_dataset_percentiles(self): + from feast.infra.offline_stores.duckdb import _duckdb_numeric_stats + + self.conn.execute(""" + CREATE TABLE large_tbl (event_timestamp TIMESTAMP, v DOUBLE) + """) + for i in range(1, 1001): + self.conn.execute( + f"INSERT INTO large_tbl VALUES " + f"(TIMESTAMP '2025-01-15 12:00:00', {float(i)})" + ) + + results = _duckdb_numeric_stats( + self.conn, + "large_tbl", + ["v"], + "1=1", + histogram_bins=10, + ) + + r = results[0] + assert r["mean"] == pytest.approx(500.5, abs=0.1) + assert r["min_val"] == 1.0 + assert r["max_val"] == 1000.0 + assert r["p50"] == pytest.approx(500.5, abs=5.0) + assert r["p90"] == pytest.approx(900.0, abs=10.0) + assert r["p99"] == pytest.approx(990.0, abs=10.0) + assert_histogram_correctness(r, "duckdb_large", expected_bins=10) + + +# =================================================================== +# Dask (PyArrow) compute correctness tests (no external deps) +# =================================================================== + + +class TestDaskComputeCorrectness: + """Test Dask/PyArrow compute helpers produce correct metric values.""" + + def test_numeric_stats_basic(self): + from feast.infra.offline_stores.dask import _dask_compute_numeric_metrics + + col = pa.chunked_array([pa.array(NUMERIC_VALUES, type=pa.float64())]) + result = _dask_compute_numeric_metrics(col, histogram_bins=5) + + expected = _expected_numeric_stats() + result["feature_name"] = "test" + assert_numeric_correctness(result, expected, "dask_numeric") + assert_histogram_correctness(result, "dask_numeric", expected_bins=5) + + def test_numeric_stats_with_nulls(self): + from feast.infra.offline_stores.dask import _dask_compute_numeric_metrics + + col = pa.chunked_array([pa.array(NUMERIC_WITH_NULLS, type=pa.float64())]) + result = _dask_compute_numeric_metrics(col, histogram_bins=5) + result["feature_name"] = "test" + + expected = _expected_numeric_with_nulls_stats() + assert_numeric_correctness(result, expected, "dask_numeric_nulls") + assert_histogram_correctness(result, "dask_numeric_nulls", expected_bins=5) + + def test_numeric_all_nulls(self): + from feast.infra.offline_stores.dask import _dask_compute_numeric_metrics + + col = pa.chunked_array([pa.array([None, None, None], type=pa.float64())]) + result = _dask_compute_numeric_metrics(col, histogram_bins=5) + + assert result["row_count"] == 3 + assert result["null_count"] == 3 + assert result["mean"] is None + assert result["histogram"] is None + + def test_numeric_empty(self): + from feast.infra.offline_stores.dask import _dask_compute_numeric_metrics + + col = pa.chunked_array([pa.array([], type=pa.float64())]) + result = _dask_compute_numeric_metrics(col, histogram_bins=5) + + assert result["row_count"] == 0 + assert result["mean"] is None + + def test_numeric_single_value(self): + from feast.infra.offline_stores.dask import _dask_compute_numeric_metrics + + col = pa.chunked_array([pa.array([42.0], type=pa.float64())]) + result = _dask_compute_numeric_metrics(col, histogram_bins=5) + + assert result["mean"] == pytest.approx(42.0) + assert result["min_val"] == 42.0 + assert result["max_val"] == 42.0 + assert result["stddev"] is None # STDDEV_SAMP of single value + + def test_numeric_large_dataset_percentiles(self): + from feast.infra.offline_stores.dask import _dask_compute_numeric_metrics + + vals = list(range(1, 1001)) + col = pa.chunked_array([pa.array(vals, type=pa.float64())]) + result = _dask_compute_numeric_metrics(col, histogram_bins=10) + + assert result["mean"] == pytest.approx(500.5, abs=0.1) + assert result["p50"] == pytest.approx(500.5, abs=5.0) + assert result["p90"] == pytest.approx(900.0, abs=10.0) + assert result["p99"] == pytest.approx(990.0, abs=10.0) + result["feature_name"] = "test" + assert_histogram_correctness(result, "dask_large", expected_bins=10) + + def test_categorical_stats_basic(self): + from feast.infra.offline_stores.dask import _dask_compute_categorical_metrics + + col = pa.chunked_array([pa.array(CATEGORICAL_VALUES, type=pa.string())]) + result = _dask_compute_categorical_metrics(col, top_n=10) + result["feature_name"] = "test" + + expected = _expected_categorical_stats() + assert_categorical_correctness(result, expected, "dask_categorical") + + def test_categorical_with_nulls(self): + from feast.infra.offline_stores.dask import _dask_compute_categorical_metrics + + vals = ["a", None, "b", None, "a", "c"] + col = pa.chunked_array([pa.array(vals, type=pa.string())]) + result = _dask_compute_categorical_metrics(col, top_n=10) + + assert result["row_count"] == 6 + assert result["null_count"] == 2 + assert result["null_rate"] == pytest.approx(1 / 3, abs=1e-4) + assert result["histogram"]["unique_count"] == 3 + + def test_categorical_top_n_truncation(self): + from feast.infra.offline_stores.dask import _dask_compute_categorical_metrics + + col = pa.chunked_array([pa.array(CATEGORICAL_VALUES, type=pa.string())]) + result = _dask_compute_categorical_metrics(col, top_n=2) + + assert len(result["histogram"]["values"]) == 2 + assert result["histogram"]["other_count"] > 0 + total = ( + sum(e["count"] for e in result["histogram"]["values"]) + + result["histogram"]["other_count"] + ) + assert total == 10 + + def test_categorical_all_nulls(self): + from feast.infra.offline_stores.dask import _dask_compute_categorical_metrics + + col = pa.chunked_array([pa.array([None, None], type=pa.string())]) + result = _dask_compute_categorical_metrics(col, top_n=10) + + assert result["null_count"] == 2 + assert result["histogram"] is None + + +# =================================================================== +# PostgreSQL compute correctness tests (requires live Postgres) +# =================================================================== + + +def _pg_available(): + try: + import psycopg # noqa: F401 + + return True + except ImportError: + return False + + +@pytest.mark.skipif(not _pg_available(), reason="psycopg not installed") +class TestPostgresComputeCorrectness: + """Test PostgreSQL SQL helpers produce correct metric values. + + Requires env vars: FEAST_PG_HOST, FEAST_PG_PORT, FEAST_PG_DB, + FEAST_PG_USER, FEAST_PG_PASS (or a local Postgres at localhost:5432). + """ + + @pytest.fixture(autouse=True) + def setup_pg(self): + import os + + import psycopg + + host = os.environ.get("FEAST_PG_HOST", "localhost") + port = os.environ.get("FEAST_PG_PORT", "5432") + db = os.environ.get("FEAST_PG_DB", "feast") + user = os.environ.get("FEAST_PG_USER", "feast") + password = os.environ.get("FEAST_PG_PASS", "feast") + + try: + self.conn = psycopg.connect( + f"host={host} port={port} dbname={db} user={user} password={password}", + autocommit=True, + ) + except psycopg.OperationalError: + pytest.skip("PostgreSQL not reachable") + + self.conn.execute("DROP TABLE IF EXISTS feast_test_monitoring_correctness") + self.conn.execute(""" + CREATE TABLE feast_test_monitoring_correctness ( + event_timestamp TIMESTAMPTZ, + numeric_col DOUBLE PRECISION, + numeric_with_nulls DOUBLE PRECISION, + categorical_col TEXT + ) + """) + + ts = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc) + for i in range(ROW_COUNT): + n_val = NUMERIC_VALUES[i] + n_null = NUMERIC_WITH_NULLS[i] + c_val = CATEGORICAL_VALUES[i] + self.conn.execute( + "INSERT INTO feast_test_monitoring_correctness VALUES (%s, %s, %s, %s)", + (ts, n_val, n_null, c_val), + ) + yield + self.conn.execute("DROP TABLE IF EXISTS feast_test_monitoring_correctness") + self.conn.close() + + def test_numeric_stats(self): + from feast.infra.offline_stores.contrib.postgres_offline_store.postgres import ( + _sql_numeric_stats, + ) + + results = _sql_numeric_stats( + self.conn, + "feast_test_monitoring_correctness", + ["numeric_col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + expected = _expected_numeric_stats() + assert_numeric_correctness(results[0], expected, "pg_numeric") + assert_histogram_correctness(results[0], "pg_numeric", expected_bins=5) + + def test_numeric_stats_with_nulls(self): + from feast.infra.offline_stores.contrib.postgres_offline_store.postgres import ( + _sql_numeric_stats, + ) + + results = _sql_numeric_stats( + self.conn, + "feast_test_monitoring_correctness", + ["numeric_with_nulls"], + "1=1", + histogram_bins=5, + ) + + expected = _expected_numeric_with_nulls_stats() + assert_numeric_correctness(results[0], expected, "pg_numeric_nulls") + + def test_numeric_multiple_features(self): + from feast.infra.offline_stores.contrib.postgres_offline_store.postgres import ( + _sql_numeric_stats, + ) + + results = _sql_numeric_stats( + self.conn, + "feast_test_monitoring_correctness", + ["numeric_col", "numeric_with_nulls"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 2 + assert results[0]["mean"] == pytest.approx(5.5, abs=1e-4) + assert results[1]["mean"] == pytest.approx(5.0, abs=1e-4) + + def test_categorical_stats(self): + from feast.infra.offline_stores.contrib.postgres_offline_store.postgres import ( + _sql_categorical_stats, + ) + + result = _sql_categorical_stats( + self.conn, + "feast_test_monitoring_correctness", + "categorical_col", + "1=1", + top_n=10, + ) + + expected = _expected_categorical_stats() + assert_categorical_correctness(result, expected, "pg_categorical") + + +# =================================================================== +# Snowflake compute correctness (mocked connection, real parsing) +# =================================================================== + + +def _snowflake_importable(): + try: + from feast.infra.offline_stores.snowflake import ( + _snowflake_sql_numeric_stats, # noqa: F401 + ) + + return True + except (ImportError, Exception): + return False + + +@pytest.mark.skipif(not _snowflake_importable(), reason="Snowflake deps not installed") +class TestSnowflakeComputeCorrectness: + """Tests Snowflake result parsing with mocked cursor. + + The cursor returns exactly the row format Snowflake would produce. + This validates column indexing, opt_float, null_count math, and + histogram assembly without needing a live Snowflake account. + """ + + def _make_mock_cursor(self, fetchone_val=None, fetchall_val=None): + from unittest.mock import MagicMock + + cursor = MagicMock() + cursor.fetchone.return_value = fetchone_val + cursor.fetchall.return_value = fetchall_val or [] + return cursor + + def test_numeric_stats(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.snowflake import ( + _snowflake_sql_numeric_stats, + ) + + vals = NUMERIC_VALUES + mean_v = statistics.mean(vals) + stddev_v = statistics.stdev(vals) + # row: COUNT(*), then per-feature: nn, avg, stddev, min, max, p50..p99 + stats_row = ( + 10, + 10, + mean_v, + stddev_v, + 1.0, + 10.0, + 5.5, + 7.75, + 9.1, + 9.55, + 9.91, + ) + stats_cursor = self._make_mock_cursor(fetchone_val=stats_row) + hist_row_data = [(1, 2), (2, 2), (3, 2), (4, 2), (5, 2)] + hist_cursor = self._make_mock_cursor(fetchall_val=hist_row_data) + + call_count = [0] + + def mock_execute(conn, query): + call_count[0] += 1 + return stats_cursor if call_count[0] == 1 else hist_cursor + + with patch( + "feast.infra.offline_stores.snowflake.execute_snowflake_statement", + side_effect=mock_execute, + ): + results = _snowflake_sql_numeric_stats( + MagicMock(), + "test_table", + ["numeric_col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + r = results[0] + expected = _expected_numeric_stats() + assert_numeric_correctness(r, expected, "snowflake_numeric") + assert r["histogram"] is not None + assert sum(r["histogram"]["counts"]) == 10 + + def test_numeric_stats_with_nulls(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.snowflake import ( + _snowflake_sql_numeric_stats, + ) + + vals = NON_NULL_VALUES + stats_row = ( + 10, + 5, + statistics.mean(vals), + statistics.stdev(vals), + 1.0, + 9.0, + 5.0, + 7.0, + 8.6, + 8.8, + 8.96, + ) + stats_cursor = self._make_mock_cursor(fetchone_val=stats_row) + hist_cursor = self._make_mock_cursor( + fetchall_val=[(1, 1), (2, 1), (3, 1), (4, 1), (5, 1)] + ) + + call_count = [0] + + def mock_execute(conn, query): + call_count[0] += 1 + return stats_cursor if call_count[0] == 1 else hist_cursor + + with patch( + "feast.infra.offline_stores.snowflake.execute_snowflake_statement", + side_effect=mock_execute, + ): + results = _snowflake_sql_numeric_stats( + MagicMock(), + "t", + ["col"], + "1=1", + histogram_bins=5, + ) + + r = results[0] + assert r["null_count"] == 5 + assert r["null_rate"] == pytest.approx(0.5) + assert r["mean"] == pytest.approx(5.0, abs=1e-4) + + def test_categorical_stats(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.snowflake import ( + _snowflake_sql_categorical_stats, + ) + + rows = [ + (10, 0, 4, "a", 4), + (10, 0, 4, "b", 3), + (10, 0, 4, "c", 2), + (10, 0, 4, "d", 1), + ] + cursor = self._make_mock_cursor(fetchall_val=rows) + + with patch( + "feast.infra.offline_stores.snowflake.execute_snowflake_statement", + return_value=cursor, + ): + result = _snowflake_sql_categorical_stats( + MagicMock(), + "t", + "cat_col", + "1=1", + top_n=10, + ) + + expected = _expected_categorical_stats() + assert_categorical_correctness(result, expected, "snowflake_categorical") + + def test_empty_result(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.snowflake import ( + _snowflake_sql_numeric_stats, + ) + + cursor = self._make_mock_cursor(fetchone_val=None) + with patch( + "feast.infra.offline_stores.snowflake.execute_snowflake_statement", + return_value=cursor, + ): + results = _snowflake_sql_numeric_stats( + MagicMock(), + "t", + ["col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + assert results[0]["mean"] is None + assert results[0]["row_count"] == 0 + + +# =================================================================== +# BigQuery compute correctness (mocked client, real parsing) +# =================================================================== + + +class TestBigQueryComputeCorrectness: + """Tests BigQuery result parsing with mocked client. + + BigQuery results use dict-like row access (row["column_name"]). + """ + + def _make_mock_bq_row(self, data: dict): + """Create an object supporting both dict-key and index access.""" + + class BQRow: + def __init__(self, d): + self._data = d + self._keys = list(d.keys()) + + def __getitem__(self, key): + if isinstance(key, int): + return self._data[self._keys[key]] + return self._data[key] + + return BQRow(data) + + def _make_mock_job(self, rows): + from unittest.mock import MagicMock + + job = MagicMock() + job.result.return_value = None + job.__iter__ = lambda self_: iter(rows) + return job + + def test_numeric_stats(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.bigquery import _bq_numeric_stats + + vals = NUMERIC_VALUES + row_data = { + "_row_count": 10, + "c0_nn": 10, + "c0_avg": statistics.mean(vals), + "c0_stddev": statistics.stdev(vals), + "c0_min": 1.0, + "c0_max": 10.0, + "c0_p50": 5.5, + "c0_p75": 7.75, + "c0_p90": 9.1, + "c0_p95": 9.55, + "c0_p99": 9.91, + } + stats_row = self._make_mock_bq_row(row_data) + stats_job = self._make_mock_job([stats_row]) + + hist_rows = [ + self._make_mock_bq_row({"bucket": i + 1, "cnt": 2}) for i in range(5) + ] + hist_job = self._make_mock_job(hist_rows) + + call_count = [0] + + def mock_query(sql, *args, **kwargs): + call_count[0] += 1 + return stats_job if call_count[0] == 1 else hist_job + + mock_config = MagicMock() + mock_config.offline_store.billing_project_id = "proj" + mock_config.offline_store.project_id = "proj" + mock_config.offline_store.location = "US" + + with patch( + "feast.infra.offline_stores.bigquery._get_bigquery_client" + ) as mock_client: + mock_client.return_value.query = mock_query + results = _bq_numeric_stats( + mock_config, + "test_table", + ["numeric_col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + r = results[0] + expected = _expected_numeric_stats() + assert_numeric_correctness(r, expected, "bq_numeric") + assert r["histogram"] is not None + assert sum(r["histogram"]["counts"]) == 10 + + def test_numeric_stats_with_nulls(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.bigquery import _bq_numeric_stats + + vals = NON_NULL_VALUES + row_data = { + "_row_count": 10, + "c0_nn": 5, + "c0_avg": statistics.mean(vals), + "c0_stddev": statistics.stdev(vals), + "c0_min": 1.0, + "c0_max": 9.0, + "c0_p50": 5.0, + "c0_p75": 7.0, + "c0_p90": 8.6, + "c0_p95": 8.8, + "c0_p99": 8.96, + } + stats_row = self._make_mock_bq_row(row_data) + stats_job = self._make_mock_job([stats_row]) + hist_job = self._make_mock_job( + [self._make_mock_bq_row({"bucket": i + 1, "cnt": 1}) for i in range(5)] + ) + + call_count = [0] + + def mock_query(sql, *args, **kwargs): + call_count[0] += 1 + return stats_job if call_count[0] == 1 else hist_job + + mock_config = MagicMock() + mock_config.offline_store.billing_project_id = "proj" + mock_config.offline_store.project_id = "proj" + mock_config.offline_store.location = "US" + + with patch( + "feast.infra.offline_stores.bigquery._get_bigquery_client" + ) as mock_client: + mock_client.return_value.query = mock_query + results = _bq_numeric_stats( + mock_config, + "t", + ["col"], + "1=1", + histogram_bins=5, + ) + + r = results[0] + assert r["null_count"] == 5 + assert r["null_rate"] == pytest.approx(0.5) + assert r["mean"] == pytest.approx(5.0, abs=1e-4) + + def test_categorical_stats(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.bigquery import _bq_categorical_stats + + rows = [ + self._make_mock_bq_row( + { + "row_count": 10, + "null_count": 0, + "unique_count": 4, + "value": "a", + "cnt": 4, + } + ), + self._make_mock_bq_row( + { + "row_count": 10, + "null_count": 0, + "unique_count": 4, + "value": "b", + "cnt": 3, + } + ), + self._make_mock_bq_row( + { + "row_count": 10, + "null_count": 0, + "unique_count": 4, + "value": "c", + "cnt": 2, + } + ), + self._make_mock_bq_row( + { + "row_count": 10, + "null_count": 0, + "unique_count": 4, + "value": "d", + "cnt": 1, + } + ), + ] + job = self._make_mock_job(rows) + + mock_config = MagicMock() + mock_config.offline_store.billing_project_id = "proj" + mock_config.offline_store.project_id = "proj" + mock_config.offline_store.location = "US" + + with patch( + "feast.infra.offline_stores.bigquery._get_bigquery_client" + ) as mock_client: + mock_client.return_value.query.return_value = job + result = _bq_categorical_stats( + mock_config, + "t", + "cat_col", + "1=1", + top_n=10, + ) + + expected = _expected_categorical_stats() + assert_categorical_correctness(result, expected, "bq_categorical") + + def test_multiple_features(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.bigquery import _bq_numeric_stats + + row_data = { + "_row_count": 10, + "c0_nn": 10, + "c0_avg": 5.5, + "c0_stddev": 3.03, + "c0_min": 1.0, + "c0_max": 10.0, + "c0_p50": 5.5, + "c0_p75": 7.75, + "c0_p90": 9.1, + "c0_p95": 9.55, + "c0_p99": 9.91, + "c1_nn": 5, + "c1_avg": 5.0, + "c1_stddev": 3.16, + "c1_min": 1.0, + "c1_max": 9.0, + "c1_p50": 5.0, + "c1_p75": 7.0, + "c1_p90": 8.6, + "c1_p95": 8.8, + "c1_p99": 8.96, + } + stats_job = self._make_mock_job([self._make_mock_bq_row(row_data)]) + hist_job = self._make_mock_job( + [self._make_mock_bq_row({"bucket": i + 1, "cnt": 2}) for i in range(5)] + ) + + call_count = [0] + + def mock_query(sql, *args, **kwargs): + call_count[0] += 1 + return stats_job if call_count[0] == 1 else hist_job + + mock_config = MagicMock() + mock_config.offline_store.billing_project_id = "p" + mock_config.offline_store.project_id = "p" + mock_config.offline_store.location = "US" + + with patch( + "feast.infra.offline_stores.bigquery._get_bigquery_client" + ) as mock_client: + mock_client.return_value.query = mock_query + results = _bq_numeric_stats( + mock_config, + "t", + ["col_a", "col_b"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 2 + assert results[0]["mean"] == pytest.approx(5.5, abs=1e-2) + assert results[1]["mean"] == pytest.approx(5.0, abs=1e-2) + assert results[0]["null_count"] == 0 + assert results[1]["null_count"] == 5 + + +# =================================================================== +# Redshift compute correctness (mocked Data API, real parsing) +# =================================================================== + + +class TestRedshiftComputeCorrectness: + """Tests Redshift result parsing with mocked _redshift_execute_fetch_rows. + + Redshift Data API returns rows as lists of field dicts, e.g. + [{"longValue": 10}, {"doubleValue": 5.5}, ...]. + """ + + def _long(self, v): + return {"longValue": v} + + def _double(self, v): + return {"doubleValue": v} + + def _string(self, v): + return {"stringValue": v} + + def _null(self): + return {"isNull": True} + + def test_numeric_stats(self): + from unittest.mock import patch + + from feast.infra.offline_stores.redshift import ( + _redshift_sql_numeric_stats, + ) + + vals = NUMERIC_VALUES + row = [ + self._long(10), # COUNT(*) + self._long(10), # COUNT(col) + self._double(statistics.mean(vals)), # AVG + self._double(statistics.stdev(vals)), # STDDEV_SAMP + self._double(1.0), # MIN + self._double(10.0), # MAX + self._double(5.5), # p50 + self._double(7.75), # p75 + self._double(9.1), # p90 + self._double(9.55), # p95 + self._double(9.91), # p99 + ] + hist_rows = [[self._long(i + 1), self._long(2)] for i in range(5)] + + call_count = [0] + + def mock_fetch(config, sql): + call_count[0] += 1 + return [row] if call_count[0] == 1 else hist_rows + + with patch( + "feast.infra.offline_stores.redshift._redshift_execute_fetch_rows", + side_effect=mock_fetch, + ): + from unittest.mock import MagicMock + + results = _redshift_sql_numeric_stats( + MagicMock(), + "test_table", + ["numeric_col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + r = results[0] + expected = _expected_numeric_stats() + assert_numeric_correctness(r, expected, "redshift_numeric") + assert r["histogram"] is not None + assert sum(r["histogram"]["counts"]) == 10 + + def test_numeric_stats_with_nulls(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.redshift import ( + _redshift_sql_numeric_stats, + ) + + vals = NON_NULL_VALUES + row = [ + self._long(10), + self._long(5), + self._double(statistics.mean(vals)), + self._double(statistics.stdev(vals)), + self._double(1.0), + self._double(9.0), + self._double(5.0), + self._double(7.0), + self._double(8.6), + self._double(8.8), + self._double(8.96), + ] + hist_rows = [[self._long(i + 1), self._long(1)] for i in range(5)] + + call_count = [0] + + def mock_fetch(config, sql): + call_count[0] += 1 + return [row] if call_count[0] == 1 else hist_rows + + with patch( + "feast.infra.offline_stores.redshift._redshift_execute_fetch_rows", + side_effect=mock_fetch, + ): + results = _redshift_sql_numeric_stats( + MagicMock(), + "t", + ["col"], + "1=1", + histogram_bins=5, + ) + + r = results[0] + assert r["null_count"] == 5 + assert r["null_rate"] == pytest.approx(0.5) + assert r["mean"] == pytest.approx(5.0, abs=1e-4) + + def test_categorical_stats(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.redshift import ( + _redshift_sql_categorical_stats, + ) + + rows = [ + [ + self._long(10), + self._long(0), + self._long(4), + self._string("a"), + self._long(4), + ], + [ + self._long(10), + self._long(0), + self._long(4), + self._string("b"), + self._long(3), + ], + [ + self._long(10), + self._long(0), + self._long(4), + self._string("c"), + self._long(2), + ], + [ + self._long(10), + self._long(0), + self._long(4), + self._string("d"), + self._long(1), + ], + ] + + with patch( + "feast.infra.offline_stores.redshift._redshift_execute_fetch_rows", + return_value=rows, + ): + result = _redshift_sql_categorical_stats( + MagicMock(), + "t", + "cat_col", + "1=1", + top_n=10, + ) + + expected = _expected_categorical_stats() + assert_categorical_correctness(result, expected, "redshift_categorical") + + def test_empty_result(self): + from unittest.mock import MagicMock, patch + + from feast.infra.offline_stores.redshift import ( + _redshift_sql_numeric_stats, + ) + + with patch( + "feast.infra.offline_stores.redshift._redshift_execute_fetch_rows", + return_value=[], + ): + results = _redshift_sql_numeric_stats( + MagicMock(), + "t", + ["col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + assert results[0]["mean"] is None + assert results[0]["row_count"] == 0 + + +# =================================================================== +# Spark compute correctness tests (requires SparkSession) +# =================================================================== + + +def _spark_available(): + try: + from pyspark.sql import SparkSession # noqa: F401 + + return True + except ImportError: + return False + + +@pytest.mark.skipif(not _spark_available(), reason="PySpark not installed") +class TestSparkComputeCorrectness: + """Test Spark SQL helpers produce correct metric values. + + Uses a local SparkSession — no external cluster required. + """ + + @pytest.fixture(autouse=True) + def setup_spark(self): + from pyspark.sql import SparkSession + from pyspark.sql.types import ( + DoubleType, + StringType, + StructField, + StructType, + TimestampType, + ) + + try: + self.spark = ( + SparkSession.builder.master("local[1]") + .appName("feast_monitoring_test") + .config("spark.ui.enabled", "false") + .config("spark.driver.bindAddress", "127.0.0.1") + .getOrCreate() + ) + except Exception as e: + pytest.skip(f"SparkSession unavailable: {e}") + + schema = StructType( + [ + StructField("event_timestamp", TimestampType(), False), + StructField("numeric_col", DoubleType(), True), + StructField("numeric_with_nulls", DoubleType(), True), + StructField("categorical_col", StringType(), True), + ] + ) + + ts = datetime(2025, 1, 15, 12, 0, 0) + rows = [ + (ts, NUMERIC_VALUES[i], NUMERIC_WITH_NULLS[i], CATEGORICAL_VALUES[i]) + for i in range(ROW_COUNT) + ] + df = self.spark.createDataFrame(rows, schema) + df.createOrReplaceTempView("feast_test_monitoring") + + yield + self.spark.sql("DROP VIEW IF EXISTS feast_test_monitoring") + + def test_numeric_stats(self): + from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( + _spark_sql_numeric_stats, + ) + + results = _spark_sql_numeric_stats( + self.spark, + "feast_test_monitoring", + ["numeric_col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + expected = _expected_numeric_stats() + assert_numeric_correctness( + results[0], + expected, + "spark_numeric", + approx_percentiles=True, + ) + assert results[0]["histogram"] is not None + assert sum(results[0]["histogram"]["counts"]) == 10 + + def test_numeric_stats_with_nulls(self): + from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( + _spark_sql_numeric_stats, + ) + + results = _spark_sql_numeric_stats( + self.spark, + "feast_test_monitoring", + ["numeric_with_nulls"], + "1=1", + histogram_bins=5, + ) + + expected = _expected_numeric_with_nulls_stats() + assert_numeric_correctness( + results[0], + expected, + "spark_numeric_nulls", + approx_percentiles=True, + ) + + def test_categorical_stats(self): + from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( + _spark_sql_categorical_stats, + ) + + result = _spark_sql_categorical_stats( + self.spark, + "feast_test_monitoring", + "categorical_col", + "1=1", + top_n=10, + ) + + expected = _expected_categorical_stats() + assert_categorical_correctness(result, expected, "spark_categorical") + + def test_numeric_multiple_features(self): + from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( + _spark_sql_numeric_stats, + ) + + results = _spark_sql_numeric_stats( + self.spark, + "feast_test_monitoring", + ["numeric_col", "numeric_with_nulls"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 2 + assert results[0]["mean"] == pytest.approx(5.5, abs=1e-4) + assert results[1]["mean"] == pytest.approx(5.0, abs=1e-4) + + def test_categorical_top_n_truncation(self): + from feast.infra.offline_stores.contrib.spark_offline_store.spark import ( + _spark_sql_categorical_stats, + ) + + result = _spark_sql_categorical_stats( + self.spark, + "feast_test_monitoring", + "categorical_col", + "1=1", + top_n=2, + ) + + assert len(result["histogram"]["values"]) == 2 + assert result["histogram"]["other_count"] > 0 + total = ( + sum(e["count"] for e in result["histogram"]["values"]) + + result["histogram"]["other_count"] + ) + assert total == 10 + + +# =================================================================== +# Oracle compute correctness (mocked Ibis connection, real parsing) +# =================================================================== + + +def _oracle_importable(): + try: + from feast.infra.offline_stores.contrib.oracle_offline_store.oracle import ( + _oracle_numeric_stats, # noqa: F401 + ) + + return True + except ImportError: + return False + + +@pytest.mark.skipif(not _oracle_importable(), reason="Oracle deps not installed") +class TestOracleComputeCorrectness: + """Tests Oracle result parsing with mocked Ibis connection. + + _oracle_fetchall returns list of tuples (positional indexing). + """ + + def test_numeric_stats(self): + from unittest.mock import patch + + from feast.infra.offline_stores.contrib.oracle_offline_store.oracle import ( + _oracle_numeric_stats, + ) + + vals = NUMERIC_VALUES + row = ( + 10, + 10, + statistics.mean(vals), + statistics.stdev(vals), + 1.0, + 10.0, + 5.5, + 7.75, + 9.1, + 9.55, + 9.91, + ) + hist_rows = [(i + 1, 2) for i in range(5)] + + call_count = [0] + + def mock_fetchall(con, sql): + call_count[0] += 1 + return [row] if call_count[0] == 1 else hist_rows + + with patch( + "feast.infra.offline_stores.contrib.oracle_offline_store.oracle._oracle_fetchall", + side_effect=mock_fetchall, + ): + results = _oracle_numeric_stats( + None, + "test_table", + ["numeric_col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + r = results[0] + expected = _expected_numeric_stats() + assert_numeric_correctness(r, expected, "oracle_numeric") + assert r["histogram"] is not None + assert sum(r["histogram"]["counts"]) == 10 + + def test_numeric_stats_with_nulls(self): + from unittest.mock import patch + + from feast.infra.offline_stores.contrib.oracle_offline_store.oracle import ( + _oracle_numeric_stats, + ) + + vals = NON_NULL_VALUES + row = ( + 10, + 5, + statistics.mean(vals), + statistics.stdev(vals), + 1.0, + 9.0, + 5.0, + 7.0, + 8.6, + 8.8, + 8.96, + ) + hist_rows = [(i + 1, 1) for i in range(5)] + + call_count = [0] + + def mock_fetchall(con, sql): + call_count[0] += 1 + return [row] if call_count[0] == 1 else hist_rows + + with patch( + "feast.infra.offline_stores.contrib.oracle_offline_store.oracle._oracle_fetchall", + side_effect=mock_fetchall, + ): + results = _oracle_numeric_stats( + None, + "t", + ["col"], + "1=1", + histogram_bins=5, + ) + + r = results[0] + assert r["null_count"] == 5 + assert r["null_rate"] == pytest.approx(0.5) + assert r["mean"] == pytest.approx(5.0, abs=1e-4) + + def test_categorical_stats(self): + from unittest.mock import patch + + from feast.infra.offline_stores.contrib.oracle_offline_store.oracle import ( + _oracle_categorical_stats, + ) + + rows = [ + (10, 0, 4, "a", 4), + (10, 0, 4, "b", 3), + (10, 0, 4, "c", 2), + (10, 0, 4, "d", 1), + ] + + with patch( + "feast.infra.offline_stores.contrib.oracle_offline_store.oracle._oracle_fetchall", + return_value=rows, + ): + result = _oracle_categorical_stats( + None, + "t", + "cat_col", + "1=1", + top_n=10, + ) + + expected = _expected_categorical_stats() + assert_categorical_correctness(result, expected, "oracle_categorical") + + def test_empty_result(self): + from unittest.mock import patch + + from feast.infra.offline_stores.contrib.oracle_offline_store.oracle import ( + _oracle_numeric_stats, + ) + + with patch( + "feast.infra.offline_stores.contrib.oracle_offline_store.oracle._oracle_fetchall", + return_value=[None], + ): + results = _oracle_numeric_stats( + None, + "t", + ["col"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 1 + assert results[0]["mean"] is None + assert results[0]["row_count"] == 0 + + def test_multiple_features(self): + from unittest.mock import patch + + from feast.infra.offline_stores.contrib.oracle_offline_store.oracle import ( + _oracle_numeric_stats, + ) + + row = ( + 10, + # Feature 0: numeric_col + 10, + 5.5, + 3.03, + 1.0, + 10.0, + 5.5, + 7.75, + 9.1, + 9.55, + 9.91, + # Feature 1: numeric_with_nulls + 5, + 5.0, + 3.16, + 1.0, + 9.0, + 5.0, + 7.0, + 8.6, + 8.8, + 8.96, + ) + hist_rows = [(i + 1, 2) for i in range(5)] + + call_count = [0] + + def mock_fetchall(con, sql): + call_count[0] += 1 + return [row] if call_count[0] == 1 else hist_rows + + with patch( + "feast.infra.offline_stores.contrib.oracle_offline_store.oracle._oracle_fetchall", + side_effect=mock_fetchall, + ): + results = _oracle_numeric_stats( + None, + "t", + ["col_a", "col_b"], + "1=1", + histogram_bins=5, + ) + + assert len(results) == 2 + assert results[0]["mean"] == pytest.approx(5.5, abs=1e-2) + assert results[1]["mean"] == pytest.approx(5.0, abs=1e-2) + assert results[0]["null_count"] == 0 + assert results[1]["null_count"] == 5 + + +# =================================================================== +# Cross-backend consistency: MetricsCalculator vs DuckDB vs Dask +# =================================================================== + + +class TestCrossBackendConsistency: + """Verify that DuckDB, Dask, and MetricsCalculator produce + consistent results for the same dataset.""" + + def test_numeric_mean_matches_across_backends(self): + duckdb = pytest.importorskip("duckdb") + from feast.infra.offline_stores.dask import _dask_compute_numeric_metrics + from feast.infra.offline_stores.duckdb import _duckdb_numeric_stats + from feast.monitoring.metrics_calculator import MetricsCalculator + + calc = MetricsCalculator(histogram_bins=5, top_n=10) + arr = pa.array(NUMERIC_VALUES, type=pa.float64()) + pyarrow_result = calc.compute_numeric(arr) + + col = pa.chunked_array([arr]) + dask_result = _dask_compute_numeric_metrics(col, histogram_bins=5) + + conn = duckdb.connect() + conn.execute("CREATE TABLE consistency_test (v DOUBLE)") + for v in NUMERIC_VALUES: + conn.execute(f"INSERT INTO consistency_test VALUES ({v})") + + duckdb_results = _duckdb_numeric_stats( + conn, + "consistency_test", + ["v"], + "1=1", + histogram_bins=5, + ) + conn.close() + + duckdb_result = duckdb_results[0] + + assert pyarrow_result["mean"] == pytest.approx(dask_result["mean"], abs=1e-6) + assert pyarrow_result["mean"] == pytest.approx(duckdb_result["mean"], abs=1e-6) + assert dask_result["mean"] == pytest.approx(duckdb_result["mean"], abs=1e-6) + + assert pyarrow_result["stddev"] == pytest.approx( + dask_result["stddev"], abs=0.01 + ) + assert pyarrow_result["stddev"] == pytest.approx( + duckdb_result["stddev"], abs=0.01 + ) + + assert pyarrow_result["min_val"] == dask_result["min_val"] + assert pyarrow_result["min_val"] == duckdb_result["min_val"] + assert pyarrow_result["max_val"] == dask_result["max_val"] + assert pyarrow_result["max_val"] == duckdb_result["max_val"] + + def test_categorical_unique_count_matches(self): + duckdb = pytest.importorskip("duckdb") + from feast.infra.offline_stores.dask import ( + _dask_compute_categorical_metrics, + ) + from feast.infra.offline_stores.duckdb import _duckdb_categorical_stats + from feast.monitoring.metrics_calculator import MetricsCalculator + + calc = MetricsCalculator(histogram_bins=5, top_n=10) + arr = pa.array(CATEGORICAL_VALUES, type=pa.string()) + pyarrow_result = calc.compute_categorical(arr) + + col = pa.chunked_array([arr]) + dask_result = _dask_compute_categorical_metrics(col, top_n=10) + + conn = duckdb.connect() + conn.execute("CREATE TABLE cat_consistency (v VARCHAR)") + for v in CATEGORICAL_VALUES: + conn.execute(f"INSERT INTO cat_consistency VALUES ('{v}')") + + duckdb_result = _duckdb_categorical_stats( + conn, + "cat_consistency", + "v", + "1=1", + top_n=10, + ) + conn.close() + + assert ( + pyarrow_result["histogram"]["unique_count"] + == dask_result["histogram"]["unique_count"] + == duckdb_result["histogram"]["unique_count"] + == 4 + ) + + pyarrow_top = pyarrow_result["histogram"]["values"][0] + dask_top = dask_result["histogram"]["values"][0] + duckdb_top = duckdb_result["histogram"]["values"][0] + assert pyarrow_top["value"] == dask_top["value"] == duckdb_top["value"] == "a" + assert pyarrow_top["count"] == dask_top["count"] == duckdb_top["count"] == 4 + + def test_null_rate_matches_across_backends(self): + duckdb = pytest.importorskip("duckdb") + from feast.infra.offline_stores.dask import _dask_compute_numeric_metrics + from feast.infra.offline_stores.duckdb import _duckdb_numeric_stats + from feast.monitoring.metrics_calculator import MetricsCalculator + + calc = MetricsCalculator(histogram_bins=5, top_n=10) + arr = pa.array(NUMERIC_WITH_NULLS, type=pa.float64()) + pyarrow_result = calc.compute_numeric(arr) + + col = pa.chunked_array([arr]) + dask_result = _dask_compute_numeric_metrics(col, histogram_bins=5) + + conn = duckdb.connect() + conn.execute("CREATE TABLE null_consistency (v DOUBLE)") + for v in NUMERIC_WITH_NULLS: + val = f"{v}" if v is not None else "NULL" + conn.execute(f"INSERT INTO null_consistency VALUES ({val})") + + duckdb_results = _duckdb_numeric_stats( + conn, + "null_consistency", + ["v"], + "1=1", + histogram_bins=5, + ) + conn.close() + + assert pyarrow_result["null_rate"] == pytest.approx(0.5, abs=1e-6) + assert dask_result["null_rate"] == pytest.approx(0.5, abs=1e-6) + assert duckdb_results[0]["null_rate"] == pytest.approx(0.5, abs=1e-6) diff --git a/sdk/python/tests/unit/monitoring/test_metrics_calculator.py b/sdk/python/tests/unit/monitoring/test_metrics_calculator.py new file mode 100644 index 00000000000..8124531d765 --- /dev/null +++ b/sdk/python/tests/unit/monitoring/test_metrics_calculator.py @@ -0,0 +1,289 @@ +import json +import math + +import pyarrow as pa +import pytest + +from feast.monitoring.metrics_calculator import MetricsCalculator +from feast.monitoring.monitoring_utils import opt_float +from feast.types import PrimitiveFeastType + + +def _make_calc(bins=20, top_n=10): + return MetricsCalculator(histogram_bins=bins, top_n=top_n) + + +class TestClassifyFeature: + @pytest.mark.parametrize( + "dtype, expected", + [ + (PrimitiveFeastType.INT32, "numeric"), + (PrimitiveFeastType.INT64, "numeric"), + (PrimitiveFeastType.FLOAT32, "numeric"), + (PrimitiveFeastType.FLOAT64, "numeric"), + (PrimitiveFeastType.STRING, "categorical"), + (PrimitiveFeastType.BOOL, "categorical"), + (PrimitiveFeastType.BYTES, None), + (PrimitiveFeastType.UNIX_TIMESTAMP, None), + ], + ) + def test_classification(self, dtype, expected): + assert MetricsCalculator.classify_feature(dtype) == expected + + +class TestComputeNumeric: + def test_basic_stats(self): + calc = _make_calc() + arr = pa.array([1.0, 2.0, 3.0, 4.0, 5.0]) + result = calc.compute_numeric(arr) + + assert result["feature_type"] == "numeric" + assert result["row_count"] == 5 + assert result["null_count"] == 0 + assert result["null_rate"] == 0.0 + assert result["mean"] == pytest.approx(3.0) + assert result["min_val"] == 1.0 + assert result["max_val"] == 5.0 + assert result["p50"] is not None + assert result["histogram"] is not None + assert "bins" in result["histogram"] + assert "counts" in result["histogram"] + + def test_with_nulls(self): + calc = _make_calc() + arr = pa.array([1.0, None, 3.0, None, 5.0]) + result = calc.compute_numeric(arr) + + assert result["row_count"] == 5 + assert result["null_count"] == 2 + assert result["null_rate"] == pytest.approx(0.4) + assert result["mean"] == pytest.approx(3.0) + + def test_all_nulls(self): + calc = _make_calc() + arr = pa.array([None, None, None], type=pa.float64()) + result = calc.compute_numeric(arr) + + assert result["null_count"] == 3 + assert result["mean"] is None + assert result["histogram"] is None + + def test_empty_array(self): + calc = _make_calc() + arr = pa.array([], type=pa.float64()) + result = calc.compute_numeric(arr) + + assert result["row_count"] == 0 + assert result["null_rate"] == 0.0 + + def test_single_value(self): + calc = _make_calc() + arr = pa.array([42.0]) + result = calc.compute_numeric(arr) + + assert result["mean"] == 42.0 + assert result["min_val"] == 42.0 + assert result["max_val"] == 42.0 + assert result["stddev"] is None # STDDEV_SAMP of 1 value is NaN → None + + def test_histogram_bin_count(self): + calc = _make_calc(bins=5) + arr = pa.array(list(range(100)), type=pa.float64()) + result = calc.compute_numeric(arr) + + assert len(result["histogram"]["counts"]) == 5 + assert len(result["histogram"]["bins"]) == 6 + + def test_percentiles_order(self): + calc = _make_calc() + arr = pa.array(list(range(1000)), type=pa.float64()) + result = calc.compute_numeric(arr) + + assert result["p50"] <= result["p75"] + assert result["p75"] <= result["p90"] + assert result["p90"] <= result["p95"] + assert result["p95"] <= result["p99"] + + +class TestComputeCategorical: + def test_basic(self): + calc = _make_calc() + arr = pa.array(["a", "b", "a", "c", "a", "b"]) + result = calc.compute_categorical(arr) + + assert result["feature_type"] == "categorical" + assert result["row_count"] == 6 + assert result["null_count"] == 0 + assert result["histogram"] is not None + assert result["histogram"]["unique_count"] == 3 + + top_values = {v["value"] for v in result["histogram"]["values"]} + assert "a" in top_values + + def test_with_nulls(self): + calc = _make_calc() + arr = pa.array(["a", None, "b", None]) + result = calc.compute_categorical(arr) + + assert result["null_count"] == 2 + assert result["null_rate"] == 0.5 + + def test_high_cardinality(self): + calc = _make_calc(top_n=3) + arr = pa.array([f"val_{i}" for i in range(100)]) + result = calc.compute_categorical(arr) + + assert len(result["histogram"]["values"]) == 3 + assert result["histogram"]["unique_count"] == 100 + assert result["histogram"]["other_count"] == 97 + + def test_all_nulls(self): + calc = _make_calc() + arr = pa.array([None, None], type=pa.string()) + result = calc.compute_categorical(arr) + + assert result["null_count"] == 2 + assert result["histogram"] is None + + +class TestComputeAll: + def test_mixed_features(self): + calc = _make_calc() + table = pa.table( + { + "age": [25, 30, 35, 40], + "city": ["NYC", "LA", "NYC", "SF"], + } + ) + fields = [("age", "numeric"), ("city", "categorical")] + results = calc.compute_all(table, fields) + + assert len(results) == 2 + assert results[0]["feature_name"] == "age" + assert results[0]["feature_type"] == "numeric" + assert results[1]["feature_name"] == "city" + assert results[1]["feature_type"] == "categorical" + + def test_missing_column_skipped(self): + calc = _make_calc() + table = pa.table({"age": [25, 30]}) + fields = [("age", "numeric"), ("missing_col", "numeric")] + results = calc.compute_all(table, fields) + + assert len(results) == 1 + assert results[0]["feature_name"] == "age" + + +class TestNaNSanitization: + """Verify that NaN/Inf values never leak into metric results.""" + + def test_opt_float_none(self): + assert opt_float(None) is None + + def test_opt_float_normal(self): + assert opt_float(3.14) == pytest.approx(3.14) + + def test_opt_float_nan(self): + assert opt_float(float("nan")) is None + + def test_opt_float_inf(self): + assert opt_float(float("inf")) is None + + def test_opt_float_neg_inf(self): + assert opt_float(float("-inf")) is None + + def test_opt_float_zero(self): + assert opt_float(0) == 0.0 + + def test_opt_float_integer(self): + assert opt_float(42) == 42.0 + + def test_single_value_stddev_is_none_not_nan(self): + """pc.stddev(ddof=1) on a single value returns NaN; we must convert to None.""" + calc = _make_calc() + arr = pa.array([7.0]) + result = calc.compute_numeric(arr) + + assert result["stddev"] is None + assert result["mean"] == pytest.approx(7.0) + + def test_two_values_stddev_is_valid(self): + calc = _make_calc() + arr = pa.array([4.0, 6.0]) + result = calc.compute_numeric(arr) + + assert result["stddev"] is not None + assert result["stddev"] == pytest.approx(math.sqrt(2.0)) + + def test_all_numeric_results_json_serializable(self): + """Every field in a numeric result must be JSON-serializable (no NaN/Inf).""" + calc = _make_calc(bins=5) + for test_data in [ + [42.0], # single value + [1.0, 2.0], # two values + [1.0, None, 3.0], # with nulls + list(range(100)), # many values + ]: + arr = pa.array(test_data, type=pa.float64()) + result = calc.compute_numeric(arr) + json.dumps(result) # raises ValueError if NaN/Inf present + + def test_all_categorical_results_json_serializable(self): + calc = _make_calc() + for test_data in [ + ["a", "b", "a"], + ["x", None, "y"], + [None, None], + ]: + arr = pa.array(test_data, type=pa.string()) + result = calc.compute_categorical(arr) + json.dumps(result) + + def test_sanitize_floats_cleans_nan(self): + from feast.monitoring.monitoring_service import _sanitize_floats + + row = { + "feature_name": "test", + "mean": float("nan"), + "stddev": float("inf"), + "null_rate": float("-inf"), + "min_val": 1.0, + "max_val": 10.0, + "p50": 5.0, + "p75": None, + "row_count": 100, + } + result = _sanitize_floats(row) + + assert result["mean"] is None + assert result["stddev"] is None + assert result["null_rate"] is None + assert result["min_val"] == 1.0 + assert result["max_val"] == 10.0 + assert result["p50"] == 5.0 + assert result["p75"] is None + assert result["row_count"] == 100 # non-float fields untouched + assert result["feature_name"] == "test" + json.dumps(result) + + def test_sanitize_floats_preserves_valid_values(self): + from feast.monitoring.monitoring_service import _sanitize_floats + + row = { + "mean": 5.5, + "stddev": 2.3, + "null_rate": 0.0, + "min_val": 0.0, + "max_val": 10.0, + "p50": 5.0, + "p75": 7.5, + "p90": 9.0, + "p95": 9.5, + "p99": 9.9, + "avg_null_rate": 0.05, + "max_null_rate": 0.1, + } + result = _sanitize_floats(row) + + for key, val in row.items(): + assert result[key] == val diff --git a/sdk/python/tests/unit/online_store/test_aerospike_online_retrieval.py b/sdk/python/tests/unit/online_store/test_aerospike_online_retrieval.py new file mode 100644 index 00000000000..f44d27bb0d7 --- /dev/null +++ b/sdk/python/tests/unit/online_store/test_aerospike_online_retrieval.py @@ -0,0 +1,1684 @@ +""" +Unit tests for the Aerospike online store. + +Most of the tests here are pure Python and run in any environment (they cover +the timestamp/TTL helpers, the column-oriented proto reshape, and the +write/read/admin dispatch with a mocked Aerospike client). One end-to-end test +is marked with ``@_requires_docker`` and is skipped when Docker is unavailable. +""" + +import threading +import time +from datetime import datetime, timedelta, timezone +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +pytest.importorskip("aerospike") + +import aerospike # noqa: E402 + +from feast import FeatureView, Field, FileSource # noqa: E402 +from feast.infra.online_stores.aerospike_online_store.aerospike import ( # noqa: E402 + AerospikeOnlineStore, + _datetime_to_epoch_ms, + _epoch_ms_to_datetime, + _resolve_ttl, +) +from feast.protos.feast.types.EntityKey_pb2 import ( + EntityKey as EntityKeyProto, # noqa: E402 +) +from feast.protos.feast.types.Value_pb2 import Value as ValueProto # noqa: E402 +from feast.repo_config import RepoConfig # noqa: E402 +from feast.types import Float64, Int64 # noqa: E402 +from feast.utils import _utc_now # noqa: E402 +from tests.universal.feature_repos.universal.online_store.aerospike import ( # noqa: E402 + AEROSPIKE_CE_IMAGE, +) +from tests.utils.cli_repo_creator import CliRunner, get_example_repo # noqa: E402 + +docker_available = False +try: + import docker + from testcontainers.core.container import DockerContainer + from testcontainers.core.waiting_utils import wait_for_logs + + try: + _docker = docker.from_env() + _docker.ping() + docker_available = True + except Exception: + pass +except ImportError: + pass + +_requires_docker = pytest.mark.skipif( + not docker_available, + reason="Docker is not available or not running. Start Docker daemon to run these tests.", +) + + +# --------------------------------------------------------------------------- +# Shared fixtures / helpers +# --------------------------------------------------------------------------- + + +def _make_fv(*field_names: str, dtype=Int64) -> FeatureView: + """Build a minimal FeatureView for conversion tests.""" + return FeatureView( + name="test_fv", + entities=[], + schema=[Field(name=n, dtype=dtype) for n in field_names], + source=FileSource(path="fake.parquet", timestamp_field="event_timestamp"), + ttl=timedelta(days=1), + ) + + +def _aerospike_repo_config(**online_store_overrides) -> RepoConfig: + base = {"type": "aerospike", "namespace": "feast"} + base.update(online_store_overrides) + return RepoConfig( + project="demo", + provider="local", + registry="/tmp/reg.db", + online_store=base, + entity_key_serialization_version=3, + ) + + +def _fake_batch_record(key: tuple, bins): + """Mimic aerospike_helpers.batch.records.BatchRecord for a successful read.""" + return SimpleNamespace( + key=key, + result=0, + record=(key, {"ttl": 0, "gen": 1}, bins) if bins is not None else None, + in_doubt=False, + ) + + +# --------------------------------------------------------------------------- +# Helpers: timestamp and TTL conversions +# --------------------------------------------------------------------------- + + +def test_datetime_helpers_round_trip_utc(): + dt = datetime(2026, 4, 20, 12, 30, 45, 123000, tzinfo=timezone.utc) + ms = _datetime_to_epoch_ms(dt) + assert _epoch_ms_to_datetime(ms) == dt + + +def test_datetime_helpers_treat_naive_as_utc(): + dt_naive = datetime(2026, 4, 20, 12, 30, 45, 123000) + dt_utc = dt_naive.replace(tzinfo=timezone.utc) + assert _datetime_to_epoch_ms(dt_naive) == _datetime_to_epoch_ms(dt_utc) + + +def test_epoch_ms_to_datetime_none_passthrough(): + assert _epoch_ms_to_datetime(None) is None + + +def test_resolve_ttl_sentinels(): + assert _resolve_ttl(None) == aerospike.TTL_NAMESPACE_DEFAULT + assert _resolve_ttl(0) == aerospike.TTL_NEVER_EXPIRE + assert _resolve_ttl(3600) == 3600 + + +def test_socket_timeout_ms_propagates_to_read_write_and_batch_policies(monkeypatch): + """``socket_timeout_ms`` must apply uniformly to read, write and batch + policies — otherwise ``max_retries`` can't fire within the total budget + (the first attempt alone consumes the whole deadline).""" + captured: dict = {} + + def fake_client(cfg): + captured["cfg"] = cfg + fake = MagicMock() + fake.connect.return_value = fake + return fake + + monkeypatch.setattr(aerospike, "client", fake_client) + + config = _aerospike_repo_config( + batch_total_timeout_ms=1500, + socket_timeout_ms=40, + read_timeout_ms=100, + write_timeout_ms=200, + ) + store = AerospikeOnlineStore() + store._get_client(config) + + policies = captured["cfg"]["policies"] + assert policies["read"]["total_timeout"] == 100 + assert policies["read"]["socket_timeout"] == 40 + assert policies["write"]["total_timeout"] == 200 + assert policies["write"]["socket_timeout"] == 40 + assert policies["batch"]["total_timeout"] == 1500 + assert policies["batch"]["socket_timeout"] == 40 + + +def test_socket_timeout_ms_omitted_when_not_set(monkeypatch): + """Unset ``socket_timeout_ms`` must leave the client's default in place, + not inject ``None`` into the policy dicts.""" + captured: dict = {} + + def fake_client(cfg): + captured["cfg"] = cfg + fake = MagicMock() + fake.connect.return_value = fake + return fake + + monkeypatch.setattr(aerospike, "client", fake_client) + + store = AerospikeOnlineStore() + store._get_client(_aerospike_repo_config()) + + policies = captured["cfg"]["policies"] + for scope in ("read", "write", "batch"): + assert "socket_timeout" not in policies[scope], ( + f"socket_timeout leaked into {scope} policy with no config override" + ) + + +# --------------------------------------------------------------------------- +# _convert_raw_docs_to_proto — same contract as MongoDB's helper +# --------------------------------------------------------------------------- + + +def test_convert_raw_docs_missing_entity(): + """Entity key absent from docs -> (None, None).""" + fv = _make_fv("score") + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + ids = [b"present", b"missing"] + docs = { + b"present": { + "features": {"test_fv": {"score": 42}}, + "event_timestamps": {"test_fv": ts}, + } + } + + results = AerospikeOnlineStore._convert_raw_docs_to_proto(ids, docs, fv) + + assert len(results) == 2 + ts_out, feats_out = results[0] + assert ts_out == ts + assert feats_out["score"].int64_val == 42 + assert results[1] == (None, None) + + +def test_convert_raw_docs_partial_doc(): + """Entity exists but one feature key is absent -> empty ValueProto for that feature.""" + fv = _make_fv("present_feat", "missing_feat") + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + ids = [b"entity1"] + docs = { + b"entity1": { + "features": {"test_fv": {"present_feat": 99}}, + "event_timestamps": {"test_fv": ts}, + } + } + + results = AerospikeOnlineStore._convert_raw_docs_to_proto(ids, docs, fv) + + assert len(results) == 1 + ts_out, feats_out = results[0] + assert ts_out == ts + assert feats_out["present_feat"].int64_val == 99 + assert feats_out["missing_feat"] == ValueProto() + + +def test_convert_raw_docs_entity_exists_but_fv_not_written(): + """Entity doc exists (written by another FV) but this FV was never written -> (None, None).""" + pricing_fv = _make_fv("price") + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + ids = [b"driver_1"] + docs = { + b"driver_1": { + "features": {"driver_stats": {"acc_rate": 0.9}}, + "event_timestamps": {"driver_stats": ts}, + } + } + + results = AerospikeOnlineStore._convert_raw_docs_to_proto(ids, docs, pricing_fv) + + assert len(results) == 1 + assert results[0] == (None, None) + + +def test_convert_raw_docs_ordering(): + """Result order matches the ids list regardless of dict insertion order in docs.""" + fv = _make_fv("score") + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + + ids = [b"entity_z", b"entity_a", b"entity_m"] + docs = { + b"entity_a": { + "features": {"test_fv": {"score": 2}}, + "event_timestamps": {"test_fv": ts}, + }, + b"entity_m": { + "features": {"test_fv": {"score": 3}}, + "event_timestamps": {"test_fv": ts}, + }, + b"entity_z": { + "features": {"test_fv": {"score": 1}}, + "event_timestamps": {"test_fv": ts}, + }, + } + + results = AerospikeOnlineStore._convert_raw_docs_to_proto(ids, docs, fv) + + assert [row[1]["score"].int64_val for row in results] == [1, 2, 3] + + +# --------------------------------------------------------------------------- +# Write path: _build_batch_writes + online_write_batch dispatch +# --------------------------------------------------------------------------- + + +def _entity_key(join_key: str, value: int) -> EntityKeyProto: + return EntityKeyProto( + join_keys=[join_key], entity_values=[ValueProto(int64_val=value)] + ) + + +def test_build_batch_writes_produces_three_ops_with_created_ts(): + config = _aerospike_repo_config(ttl_seconds=3600) + fv = SimpleNamespace(name="driver_stats") + ts = datetime(2026, 4, 20, 12, 30, 45, tzinfo=timezone.utc) + row = ( + _entity_key("driver_id", 1), + { + "rating": ValueProto(double_val=4.91), + "trips_last_7d": ValueProto(int64_val=132), + }, + ts, + ts, + ) + + batch = AerospikeOnlineStore._build_batch_writes( + config, fv, [row], namespace="feast", set_name="demo_latest" + ) + + assert len(batch.batch_records) == 1 + bw = batch.batch_records[0] + assert bw.key[:2] == ("feast", "demo_latest") + # Must be bytearray, not bytes: the Aerospike Python C client rejects + # bytes user keys ("Key is invalid") and silently hashes only the first + # byte inside batch_operate/batch_read — causing digest collisions. + assert isinstance(bw.key[2], bytearray) + assert bw.meta == {"ttl": 3600} + # No explicit per-record policy: writes rely on the client-level default + # (POLICY_KEY_DIGEST), which doesn't persist the serialized entity key + # server-side — batch_operate preserves request order so the stored key + # has no functional use on the read path. + assert bw.policy is None + assert len(bw.ops) == 3 + bin_names = [op["bin"] for op in bw.ops] + assert bin_names == ["features", "event_ts", "created_ts"] + # Map CDTs must be created with MAP_KEY_ORDERED so key lookups on reads + # and the update() background scan stay O(log N). + for op in bw.ops: + if op["bin"] in ("features", "event_ts"): + assert op["map_policy"] == {"map_order": aerospike.MAP_KEY_ORDERED} + + +def test_build_batch_writes_omits_created_ts_when_none(): + config = _aerospike_repo_config() + fv = SimpleNamespace(name="driver_stats") + row = ( + _entity_key("driver_id", 1), + {"rating": ValueProto(double_val=4.91)}, + datetime(2026, 4, 20, tzinfo=timezone.utc), + None, + ) + + batch = AerospikeOnlineStore._build_batch_writes( + config, fv, [row], namespace="feast", set_name="demo_latest" + ) + + ops = batch.batch_records[0].ops + assert len(ops) == 2 + assert {op["bin"] for op in ops} == {"features", "event_ts"} + + +def test_build_batch_writes_ttl_sentinels(): + config = _aerospike_repo_config(ttl_seconds=0) + fv = SimpleNamespace(name="fv") + row = ( + _entity_key("id", 1), + {"x": ValueProto(int64_val=1)}, + datetime(2026, 1, 1, tzinfo=timezone.utc), + None, + ) + + batch = AerospikeOnlineStore._build_batch_writes( + config, fv, [row], namespace="feast", set_name="set" + ) + assert batch.batch_records[0].meta == {"ttl": aerospike.TTL_NEVER_EXPIRE} + + +def test_online_write_batch_dispatches_to_client(): + config = _aerospike_repo_config() + fv = SimpleNamespace(name="fv") + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + row = ( + _entity_key("id", 1), + {"x": ValueProto(int64_val=1)}, + datetime(2026, 1, 1, tzinfo=timezone.utc), + None, + ) + progress_calls: list[int] = [] + store.online_write_batch(config, fv, [row], progress=progress_calls.append) + + assert fake_client.batch_write.called + assert progress_calls == [1] + + +def test_online_write_batch_empty_short_circuits(): + config = _aerospike_repo_config() + fv = SimpleNamespace(name="fv") + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + progress_calls: list[int] = [] + store.online_write_batch(config, fv, [], progress=progress_calls.append) + + assert not fake_client.batch_write.called + assert progress_calls == [0] + + +def test_chunked_splits_without_partial_tail(): + chunks = list(AerospikeOnlineStore._chunked([1, 2, 3, 4, 5], 2)) + assert chunks == [[1, 2], [3, 4], [5]] + + +def test_get_client_thread_safe_single_connect(monkeypatch): + """Concurrent first callers must share one client, not leak connections.""" + connect_count = 0 + connect_lock = threading.Lock() + + def fake_client(cfg): + fake = MagicMock() + + def connect(): + nonlocal connect_count + with connect_lock: + connect_count += 1 + time.sleep(0.05) + return fake + + fake.connect = connect + return fake + + monkeypatch.setattr(aerospike, "client", fake_client) + store = AerospikeOnlineStore() + config = _aerospike_repo_config() + barrier = threading.Barrier(8) + + def worker(): + barrier.wait() + store._get_client(config) + + threads = [threading.Thread(target=worker) for _ in range(8)] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + + assert connect_count == 1 + + +def test_online_write_batch_chunks_at_batch_max_records(): + config = _aerospike_repo_config(batch_max_records=100) + fv = SimpleNamespace(name="fv") + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + ts = datetime(2026, 1, 1, tzinfo=timezone.utc) + rows = [ + ( + _entity_key("id", i), + {"x": ValueProto(int64_val=i)}, + ts, + None, + ) + for i in range(250) + ] + progress_calls: list[int] = [] + store.online_write_batch(config, fv, rows, progress=progress_calls.append) + + assert fake_client.batch_write.call_count == 3 + sizes = [ + len(call.args[0].batch_records) + for call in fake_client.batch_write.call_args_list + ] + assert sizes == [100, 100, 50] + assert progress_calls == [100, 200, 250] + + +# --------------------------------------------------------------------------- +# Read path: online_read dispatches and converts via batch_operate +# --------------------------------------------------------------------------- + + +def _read_feature_view() -> SimpleNamespace: + """Minimal FV object exposing .name and .features with dtype mappings.""" + return SimpleNamespace( + name="driver_stats", + features=[ + SimpleNamespace(name="rating", dtype=Float64), + SimpleNamespace(name="trips_last_7d", dtype=Int64), + ], + ) + + +def test_online_read_happy_path_with_projection_and_ordering(): + config = _aerospike_repo_config() + fv = _read_feature_view() + store = AerospikeOnlineStore() + ts = datetime(2026, 4, 20, 12, 30, 45, 123000, tzinfo=timezone.utc) + + ek1 = _entity_key("driver_id", 1) + ek2 = _entity_key("driver_id", 2) + ek3 = _entity_key("driver_id", 3) + key1 = store._aerospike_key(config, ek1) + key2 = store._aerospike_key(config, ek2) + key3 = store._aerospike_key(config, ek3) + + def fake_batch_operate(keys, ops): + assert keys == [key1, key2, key3] + assert len(ops) == 2 + assert ops[0]["bin"] == "features" + assert ops[1]["bin"] == "event_ts" + # Aerospike's batch_operate preserves input order; the middle record + # is simulated as missing to verify we still emit (None, None) in + # the correct slot. + br1 = _fake_batch_record( + key1, + { + "features": {"rating": 4.91, "trips_last_7d": 132}, + "event_ts": _datetime_to_epoch_ms(ts), + }, + ) + br2 = _fake_batch_record(key2, None) # missing record + br3 = _fake_batch_record( + key3, + { + "features": {"rating": 3.75, "trips_last_7d": 42}, + "event_ts": _datetime_to_epoch_ms(ts), + }, + ) + return SimpleNamespace(batch_records=[br1, br2, br3]) + + fake_client = MagicMock() + fake_client.batch_operate.side_effect = fake_batch_operate + store._client = fake_client + + results = store.online_read(config, fv, [ek1, ek2, ek3]) + + assert len(results) == 3 + ts0, feats0 = results[0] + assert ts0 == ts + assert abs(feats0["rating"].double_val - 4.91) < 1e-9 + assert feats0["trips_last_7d"].int64_val == 132 + assert results[1] == (None, None) + ts2, feats2 = results[2] + assert ts2 == ts + assert abs(feats2["rating"].double_val - 3.75) < 1e-9 + + +def test_online_read_empty_keys_returns_empty(): + store = AerospikeOnlineStore() + store._client = MagicMock() + fv = _read_feature_view() + assert store.online_read(_aerospike_repo_config(), fv, []) == [] + assert not store._client.batch_operate.called + + +def test_online_read_chunks_at_batch_max_records(): + config = _aerospike_repo_config(batch_max_records=100) + fv = _read_feature_view() + store = AerospikeOnlineStore() + entity_keys = [_entity_key("driver_id", i) for i in range(250)] + ts = datetime(2026, 1, 1, tzinfo=timezone.utc) + + def fake_batch_operate(keys, ops): + return SimpleNamespace( + batch_records=[ + _fake_batch_record( + key, + { + "features": {"rating": 4.0, "trips_last_7d": 10}, + "event_ts": _datetime_to_epoch_ms(ts), + }, + ) + for key in keys + ] + ) + + fake_client = MagicMock() + fake_client.batch_operate.side_effect = fake_batch_operate + store._client = fake_client + + results = store.online_read(config, fv, entity_keys) + + assert fake_client.batch_operate.call_count == 3 + sizes = [len(call.args[0]) for call in fake_client.batch_operate.call_args_list] + assert sizes == [100, 100, 50] + assert len(results) == 250 + assert all(feats is not None for _, feats in results) + + +def test_online_read_record_exists_but_fv_not_present_returns_none(): + config = _aerospike_repo_config() + fv = _read_feature_view() + store = AerospikeOnlineStore() + ek = _entity_key("driver_id", 1) + key = store._aerospike_key(config, ek) + + def fake_batch_operate(keys, ops): + return SimpleNamespace( + batch_records=[ + _fake_batch_record(key, {"features": None, "event_ts": None}) + ] + ) + + fake_client = MagicMock() + fake_client.batch_operate.side_effect = fake_batch_operate + store._client = fake_client + + results = store.online_read(config, fv, [ek]) + assert results == [(None, None)] + + +def _fake_error_record(key: tuple, result_code: int): + """Batch response for an error case — no ``record`` but a non-OK result.""" + return SimpleNamespace(key=key, result=result_code, record=None, in_doubt=False) + + +def test_online_read_record_not_found_returns_none(): + """``br.result == 2`` (RECORD_NOT_FOUND) is a genuine miss, not an error.""" + config = _aerospike_repo_config() + fv = _read_feature_view() + store = AerospikeOnlineStore() + ek = _entity_key("driver_id", 1) + key = store._aerospike_key(config, ek) + + fake_client = MagicMock() + fake_client.batch_operate.return_value = SimpleNamespace( + batch_records=[_fake_error_record(key, result_code=2)] + ) + store._client = fake_client + + assert store.online_read(config, fv, [ek]) == [(None, None)] + + +def test_online_read_op_not_applicable_returns_none(): + """``br.result == 26`` (OP_NOT_APPLICABLE) happens when the nested FV + submap is absent — also a miss under the OnlineStore contract, not an + error.""" + config = _aerospike_repo_config() + fv = _read_feature_view() + store = AerospikeOnlineStore() + ek = _entity_key("driver_id", 1) + key = store._aerospike_key(config, ek) + + fake_client = MagicMock() + fake_client.batch_operate.return_value = SimpleNamespace( + batch_records=[_fake_error_record(key, result_code=26)] + ) + store._client = fake_client + + assert store.online_read(config, fv, [ek]) == [(None, None)] + + +def test_online_read_raises_on_transient_error(): + """Any other non-OK result (e.g. 9 = TIMEOUT) must be surfaced as an error. + + A silent null-return on a transient timeout is the root-cause class of + bug that shows up weeks later as a model-quality regression; pinning + this with a test keeps the read path loud on failure. + """ + config = _aerospike_repo_config() + fv = _read_feature_view() + store = AerospikeOnlineStore() + ek = _entity_key("driver_id", 1) + key = store._aerospike_key(config, ek) + + fake_client = MagicMock() + fake_client.batch_operate.return_value = SimpleNamespace( + batch_records=[_fake_error_record(key, result_code=9)] # TIMEOUT + ) + store._client = fake_client + + with pytest.raises(RuntimeError, match="non-OK status"): + store.online_read(config, fv, [ek]) + + +def test_online_read_projects_requested_features_server_side(): + """``requested_features`` must drive a ``map_get_by_key_list`` projection + nested into the feature-view submap via ``cdt_ctx``; the unordered + ``MAP_RETURN_KEY_VALUE`` response is a flat ``[k1,v1,k2,v2]`` list.""" + config = _aerospike_repo_config() + fv = _read_feature_view() + store = AerospikeOnlineStore() + ek = _entity_key("driver_id", 1) + key = store._aerospike_key(config, ek) + ts = datetime(2026, 4, 20, 12, 30, 45, tzinfo=timezone.utc) + + captured_ops: list = [] + + def fake_batch_operate(keys, ops): + captured_ops.extend(ops) + # Projected features come back as a flat [k,v,k,v] list rather than + # a dict — the store must normalize this before feeding the row + # reshape helper. + return SimpleNamespace( + batch_records=[ + _fake_batch_record( + key, + { + "features": ["rating", 4.91], + "event_ts": _datetime_to_epoch_ms(ts), + }, + ) + ] + ) + + fake_client = MagicMock() + fake_client.batch_operate.side_effect = fake_batch_operate + store._client = fake_client + + results = store.online_read(config, fv, [ek], requested_features=["rating"]) + + assert len(results) == 1 + ts_out, feats = results[0] + assert ts_out == ts + assert abs(feats["rating"].double_val - 4.91) < 1e-9 + assert feats["trips_last_7d"] == ValueProto() + features_op = captured_ops[0] + assert features_op["op"] == aerospike.OP_MAP_GET_BY_KEY_LIST + assert features_op["bin"] == "features" + assert features_op["val"] == ["rating"] + assert features_op["return_type"] == aerospike.MAP_RETURN_KEY_VALUE + # The ctx must nest the projection inside the FV's submap so the server + # only ships the requested columns over the wire. + assert features_op.get("ctx"), "projection op is missing ctx" + + +def test_normalize_projected_features_handles_all_payload_shapes(): + """The shape of the ``features`` payload depends on which op produced it; + the helper must accept all of them.""" + assert AerospikeOnlineStore._normalize_projected_features(None) is None + assert AerospikeOnlineStore._normalize_projected_features([]) == {} + assert AerospikeOnlineStore._normalize_projected_features(["a", 1, "b", 2]) == { + "a": 1, + "b": 2, + } + assert AerospikeOnlineStore._normalize_projected_features({"a": 1, "b": 2}) == { + "a": 1, + "b": 2, + } + + +def test_online_write_batch_raises_on_per_record_error(): + """A partial batch failure (one record's result != 0) must raise rather + than silently succeed — otherwise downstream sees "model saw stale + features" weeks after the fact.""" + config = _aerospike_repo_config() + fv = SimpleNamespace(name="fv") + store = AerospikeOnlineStore() + fake_client = MagicMock() + + def fake_batch_write(batch): + # Simulate a one-partition timeout on the single record. + batch.batch_records[0].result = 9 # TIMEOUT + + fake_client.batch_write.side_effect = fake_batch_write + store._client = fake_client + + row = ( + _entity_key("id", 1), + {"x": ValueProto(int64_val=1)}, + datetime(2026, 1, 1, tzinfo=timezone.utc), + None, + ) + with pytest.raises(RuntimeError, match="non-OK"): + store.online_write_batch(config, fv, [row], progress=None) + + +# --------------------------------------------------------------------------- +# Admin paths: update / teardown +# --------------------------------------------------------------------------- + + +def test_update_no_op_when_nothing_to_delete(): + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + store.update(_aerospike_repo_config(), [], [], [], [], partial=False) + assert not fake_client.scan.called + + +def test_update_single_fv_issues_single_background_scan(): + store = AerospikeOnlineStore() + fake_client = MagicMock() + fake_scan = MagicMock() + fake_client.scan.return_value = fake_scan + store._client = fake_client + + store.update( + _aerospike_repo_config(), + [SimpleNamespace(name="old_fv")], + [], + [], + [], + partial=False, + ) + + assert fake_client.scan.call_args[0] == ("feast", "demo_latest") + ops = fake_scan.add_ops.call_args[0][0] + assert len(ops) == 2 + assert {op["bin"] for op in ops} == {"features", "event_ts"} + for op in ops: + assert op["key"] == "old_fv" + assert op["return_type"] == aerospike.MAP_RETURN_NONE + fake_scan.execute_background.assert_called_once() + + +def test_update_multi_fv_coalesces_into_one_scan(): + store = AerospikeOnlineStore() + fake_client = MagicMock() + fake_scan = MagicMock() + fake_client.scan.return_value = fake_scan + store._client = fake_client + + tables = [SimpleNamespace(name=n) for n in ("a", "b", "c")] + store.update(_aerospike_repo_config(), tables, [], [], [], partial=False) + + fake_client.scan.assert_called_once() + ops = fake_scan.add_ops.call_args[0][0] + assert len(ops) == 6 + assert {op["bin"] for op in ops} == {"features", "event_ts"} + assert {op["key"] for op in ops} == {"a", "b", "c"} + fake_scan.execute_background.assert_called_once() + + +def test_update_rejects_non_aerospike_config(): + wrong_config = RepoConfig( + project="demo", + provider="local", + registry="/tmp/reg.db", + online_store={"type": "sqlite", "path": "/tmp/online.db"}, + entity_key_serialization_version=3, + ) + store = AerospikeOnlineStore() + with pytest.raises(RuntimeError): + store.update( + wrong_config, + [SimpleNamespace(name="x")], + [], + [], + [], + partial=False, + ) + + +def test_teardown_truncates_and_closes_client(): + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + store.teardown(_aerospike_repo_config(), [], []) + assert fake_client.truncate.call_args[0] == ("feast", "demo_latest", 0) + fake_client.close.assert_called_once() + assert store._client is None + + +def test_teardown_rejects_non_aerospike_config(): + wrong_config = RepoConfig( + project="demo", + provider="local", + registry="/tmp/reg.db", + online_store={"type": "sqlite", "path": "/tmp/online.db"}, + entity_key_serialization_version=3, + ) + store = AerospikeOnlineStore() + with pytest.raises(RuntimeError): + store.teardown(wrong_config, [], []) + + +# --------------------------------------------------------------------------- +# Per-feature-view namespace + set overrides +# --------------------------------------------------------------------------- +# These let a deployment pin individual feature views to RAM-only / SSD-backed +# namespaces or isolate one view in its own set without splitting projects. +# Coverage: +# * the ``_namespace_for_fv`` / ``_set_name`` resolvers +# * read + write paths actually use the resolved ns/set on the wire +# * ``update()`` issues one scan per (ns, set) group +# * ``teardown()`` truncates every unique (ns, set) pair + + +def test_set_name_default_when_no_overrides(): + config = _aerospike_repo_config() + store = AerospikeOnlineStore() + assert store._set_name(config) == "demo_latest" + assert store._set_name(config, "any_fv") == "demo_latest" + + +def test_set_name_resolves_per_fv_override(): + config = _aerospike_repo_config( + set_overrides={"hot_fv": "demo_hot", "cold_fv": "demo_cold"} + ) + store = AerospikeOnlineStore() + assert store._set_name(config, "hot_fv") == "demo_hot" + assert store._set_name(config, "cold_fv") == "demo_cold" + # Unmapped FV still falls back to the project-level default. + assert store._set_name(config, "other_fv") == "demo_latest" + # Calling without an FV name explicitly asks for the project default. + assert store._set_name(config) == "demo_latest" + + +def test_namespace_for_fv_default_when_no_overrides(): + config = _aerospike_repo_config() + store = AerospikeOnlineStore() + assert store._namespace_for_fv(config) == "feast" + assert store._namespace_for_fv(config, "any_fv") == "feast" + + +def test_namespace_for_fv_resolves_per_fv_override(): + config = _aerospike_repo_config( + namespace_overrides={"hot_fv": "feast_ram", "cold_fv": "feast_ssd"} + ) + store = AerospikeOnlineStore() + assert store._namespace_for_fv(config, "hot_fv") == "feast_ram" + assert store._namespace_for_fv(config, "cold_fv") == "feast_ssd" + assert store._namespace_for_fv(config, "other_fv") == "feast" + + +def test_aerospike_key_honours_overrides_when_fv_passed(): + """``_aerospike_key`` is the choke point for everything that builds an + Aerospike (ns, set, user_key) tuple — it must respect the overrides + when an FV name is provided, and stay backwards-compatible without.""" + config = _aerospike_repo_config( + namespace_overrides={"hot_fv": "feast_ram"}, + set_overrides={"hot_fv": "demo_hot"}, + ) + store = AerospikeOnlineStore() + ek = _entity_key("driver_id", 1) + + default_ns, default_set, _ = store._aerospike_key(config, ek) + assert (default_ns, default_set) == ("feast", "demo_latest") + + hot_ns, hot_set, _ = store._aerospike_key(config, ek, fv_name="hot_fv") + assert (hot_ns, hot_set) == ("feast_ram", "demo_hot") + + +def test_online_write_batch_writes_to_per_fv_ns_and_set(): + """The wire keys produced by online_write_batch must use the FV's + resolved ns + set, not the store-level defaults.""" + config = _aerospike_repo_config( + namespace_overrides={"driver_stats": "feast_ram"}, + set_overrides={"driver_stats": "demo_hot"}, + ) + fv = SimpleNamespace(name="driver_stats") + store = AerospikeOnlineStore() + fake_client = MagicMock() + captured: dict = {} + + def fake_batch_write(batch): + captured["batch"] = batch + + fake_client.batch_write.side_effect = fake_batch_write + store._client = fake_client + + row = ( + _entity_key("id", 1), + {"x": ValueProto(int64_val=1)}, + datetime(2026, 1, 1, tzinfo=timezone.utc), + None, + ) + store.online_write_batch(config, fv, [row], progress=None) + + bw = captured["batch"].batch_records[0] + assert bw.key[:2] == ("feast_ram", "demo_hot") + + +def test_online_read_uses_per_fv_ns_and_set(): + """The ``batch_operate`` keys must use the FV's resolved ns + set.""" + config = _aerospike_repo_config( + namespace_overrides={"driver_stats": "feast_ram"}, + set_overrides={"driver_stats": "demo_hot"}, + ) + fv = _read_feature_view() # name="driver_stats" + store = AerospikeOnlineStore() + captured: dict = {} + + def fake_batch_operate(keys, ops): + captured["keys"] = keys + return SimpleNamespace(batch_records=[_fake_batch_record(keys[0], None)]) + + fake_client = MagicMock() + fake_client.batch_operate.side_effect = fake_batch_operate + store._client = fake_client + + store.online_read(config, fv, [_entity_key("driver_id", 1)]) + assert len(captured["keys"]) == 1 + assert captured["keys"][0][:2] == ("feast_ram", "demo_hot") + + +def test_update_groups_dropped_fvs_by_resolved_ns_and_set(): + """Dropped feature views in different (ns, set) buckets must produce + one background scan per bucket — a single combined scan would either + miss records or pointlessly scan unrelated namespaces.""" + config = _aerospike_repo_config( + namespace_overrides={"a": "ns_x"}, # b, c land on default ns + set_overrides={"b": "set_y"}, # a, c land on default set + ) + store = AerospikeOnlineStore() + fake_client = MagicMock() + + fake_scans: list = [] + + def make_scan(ns, set_name): + s = MagicMock(name=f"scan_{ns}_{set_name}") + s._ns = ns + s._set = set_name + fake_scans.append(s) + return s + + fake_client.scan.side_effect = make_scan + store._client = fake_client + + tables = [ + SimpleNamespace(name="a"), # (ns_x, demo_latest) + SimpleNamespace(name="b"), # (feast, set_y) + SimpleNamespace(name="c"), # (feast, demo_latest) + ] + store.update(config, tables, [], [], [], partial=False) + + targets = {(s._ns, s._set) for s in fake_scans} + assert targets == { + ("ns_x", "demo_latest"), + ("feast", "set_y"), + ("feast", "demo_latest"), + } + # Every group's scan must execute and ship the matching FVs' remove ops. + by_target = {(s._ns, s._set): s for s in fake_scans} + for tgt, fv_name in [ + (("ns_x", "demo_latest"), "a"), + (("feast", "set_y"), "b"), + (("feast", "demo_latest"), "c"), + ]: + scan = by_target[tgt] + ops = scan.add_ops.call_args[0][0] + assert {op["bin"] for op in ops} == {"features", "event_ts"} + assert all(op["key"] == fv_name for op in ops) + scan.execute_background.assert_called_once() + + +def test_update_coalesces_fvs_sharing_a_resolved_target(): + """Two dropped FVs that resolve to the same (ns, set) must share one + scan — the per-FV grouping is "by target", not "by FV name".""" + config = _aerospike_repo_config( + namespace_overrides={"a": "ns_x", "b": "ns_x"}, + set_overrides={"a": "set_y", "b": "set_y"}, + ) + store = AerospikeOnlineStore() + fake_client = MagicMock() + fake_scan = MagicMock() + fake_client.scan.return_value = fake_scan + store._client = fake_client + + store.update( + config, + [SimpleNamespace(name="a"), SimpleNamespace(name="b")], + [], + [], + [], + partial=False, + ) + + fake_client.scan.assert_called_once_with("ns_x", "set_y") + ops = fake_scan.add_ops.call_args[0][0] + # 4 ops total: features+event_ts for each of the 2 FVs. + assert len(ops) == 4 + assert {op["key"] for op in ops} == {"a", "b"} + + +def test_teardown_truncates_default_plus_every_overridden_pair(): + config = _aerospike_repo_config( + namespace_overrides={"hot": "feast_ram"}, + set_overrides={"isolated": "demo_iso"}, + ) + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + tables = [ + SimpleNamespace(name="hot"), # (feast_ram, demo_latest) + SimpleNamespace(name="isolated"), # (feast, demo_iso) + SimpleNamespace(name="default"), # (feast, demo_latest) + ] + store.teardown(config, tables, []) + + truncated = {tuple(call.args[:2]) for call in fake_client.truncate.call_args_list} + assert truncated == { + ("feast", "demo_latest"), + ("feast_ram", "demo_latest"), + ("feast", "demo_iso"), + } + # cutoff is always 0 ("drop everything regardless of last-update time") + for call in fake_client.truncate.call_args_list: + assert call.args[2] == 0 + fake_client.close.assert_called_once() + + +def test_teardown_with_empty_tables_still_clears_default_pair(): + """A teardown call with an empty tables list (e.g. a bare ``feast + teardown`` on a repo whose registry is empty) must still clear the + store-default location, otherwise stale data lingers.""" + config = _aerospike_repo_config( + namespace_overrides={"unused": "feast_ram"}, + ) + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + store.teardown(config, [], []) + + truncated = {tuple(call.args[:2]) for call in fake_client.truncate.call_args_list} + assert truncated == {("feast", "demo_latest")} + + +# --------------------------------------------------------------------------- +# Prewriting hook +# --------------------------------------------------------------------------- +# Hook-target functions must be module-level so the import-string resolver +# can find them via importlib + getattr. They're referenced by f"{__name__}.X". + + +def _hook_uppercase_string_features(config, table, data): # noqa: ARG001 + """Sample hook: uppercase every string ValueProto.string_val on every row.""" + new_data = [] + for entity_key, values, ts, created in data: + new_values = {} + for k, v in values.items(): + if v.HasField("string_val"): + new_values[k] = ValueProto(string_val=v.string_val.upper()) + else: + new_values[k] = v + new_data.append((entity_key, new_values, ts, created)) + return new_data + + +def _hook_drop_all_rows(config, table, data): # noqa: ARG001 + """Sample hook that filters every row — exercises the post-hook empty-batch path.""" + return [] + + +def _hook_raises(config, table, data): # noqa: ARG001 + raise ValueError("hook intentionally failed") + + +_NOT_A_FUNCTION = 42 + + +def test_prewriting_hook_unset_returns_none(): + config = _aerospike_repo_config() # no prewriting_hook + store = AerospikeOnlineStore() + assert store._resolve_prewriting_hook(config) is None + # Must clear any previously-cached value so a config swap takes effect. + store._prewriting_hook = lambda *a, **kw: None # type: ignore[assignment] + store._prewriting_hook_spec = "stale.spec" + store._resolve_prewriting_hook(config) + assert store._prewriting_hook is None + assert store._prewriting_hook_spec is None + + +def test_prewriting_hook_resolves_and_caches(): + spec = f"{__name__}._hook_uppercase_string_features" + config = _aerospike_repo_config(prewriting_hook=spec) + store = AerospikeOnlineStore() + + hook = store._resolve_prewriting_hook(config) + assert hook is _hook_uppercase_string_features + + # Second call must hit the cache (no re-import). + with patch( + "feast.infra.online_stores.aerospike_online_store.aerospike.importlib" + ) as m: + again = store._resolve_prewriting_hook(config) + assert again is hook + assert not m.import_module.called + + +def test_prewriting_hook_recompiles_after_spec_change(): + """If the config is rebound to a new hook string, the resolver must + re-resolve instead of returning the cached old callable.""" + store = AerospikeOnlineStore() + + cfg1 = _aerospike_repo_config( + prewriting_hook=f"{__name__}._hook_uppercase_string_features" + ) + assert store._resolve_prewriting_hook(cfg1) is _hook_uppercase_string_features + + cfg2 = _aerospike_repo_config(prewriting_hook=f"{__name__}._hook_drop_all_rows") + assert store._resolve_prewriting_hook(cfg2) is _hook_drop_all_rows + + +def test_prewriting_hook_bad_format_raises(): + config = _aerospike_repo_config(prewriting_hook="no_dot_here") + store = AerospikeOnlineStore() + with pytest.raises(ValueError, match="fully qualified import path"): + store._resolve_prewriting_hook(config) + + +def test_prewriting_hook_missing_module_raises(): + config = _aerospike_repo_config(prewriting_hook="definitely_not_a_real.module.fn") + store = AerospikeOnlineStore() + with pytest.raises(ValueError, match="could not import module"): + store._resolve_prewriting_hook(config) + + +def test_prewriting_hook_missing_attribute_raises(): + config = _aerospike_repo_config(prewriting_hook=f"{__name__}._does_not_exist") + store = AerospikeOnlineStore() + with pytest.raises(ValueError, match="has no attribute"): + store._resolve_prewriting_hook(config) + + +def test_prewriting_hook_non_callable_raises(): + config = _aerospike_repo_config(prewriting_hook=f"{__name__}._NOT_A_FUNCTION") + store = AerospikeOnlineStore() + with pytest.raises(TypeError, match="non-callable"): + store._resolve_prewriting_hook(config) + + +def test_prewriting_hook_transforms_data_before_write(): + """End-to-end: the hook's output is what lands on the wire, not the + caller-supplied data.""" + config = _aerospike_repo_config( + prewriting_hook=f"{__name__}._hook_uppercase_string_features" + ) + fv = SimpleNamespace(name="fv") + store = AerospikeOnlineStore() + fake_client = MagicMock() + captured: dict = {} + + def fake_batch_write(batch): + captured["batch"] = batch + + fake_client.batch_write.side_effect = fake_batch_write + store._client = fake_client + + row = ( + _entity_key("id", 1), + {"name": ValueProto(string_val="alice")}, + datetime(2026, 1, 1, tzinfo=timezone.utc), + None, + ) + store.online_write_batch(config, fv, [row], progress=None) + + bw = captured["batch"].batch_records[0] + map_put_op = next(op for op in bw.ops if op["bin"] == "features") + # The hook upper-cases string features. ``map_put_items`` stores its + # payload under the ``val`` key (mapping FV name -> feature submap). + fv_map = map_put_op["val"]["fv"] + assert fv_map == {"name": "ALICE"} + + +def test_prewriting_hook_returning_empty_short_circuits(): + """A hook that filters every row must skip the wire call and report + progress=0 — same shape as the empty-input fast path.""" + config = _aerospike_repo_config(prewriting_hook=f"{__name__}._hook_drop_all_rows") + fv = SimpleNamespace(name="fv") + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + progress_calls: list[int] = [] + row = ( + _entity_key("id", 1), + {"x": ValueProto(int64_val=1)}, + datetime(2026, 1, 1, tzinfo=timezone.utc), + None, + ) + store.online_write_batch(config, fv, [row], progress=progress_calls.append) + + assert not fake_client.batch_write.called + assert progress_calls == [0] + + +def test_prewriting_hook_propagates_exceptions(): + """A raising hook must fail the whole batch — there's no per-row + fallback. ``batch_write`` must never be called.""" + config = _aerospike_repo_config(prewriting_hook=f"{__name__}._hook_raises") + fv = SimpleNamespace(name="fv") + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + row = ( + _entity_key("id", 1), + {"x": ValueProto(int64_val=1)}, + datetime(2026, 1, 1, tzinfo=timezone.utc), + None, + ) + with pytest.raises(ValueError, match="hook intentionally failed"): + store.online_write_batch(config, fv, [row], progress=None) + assert not fake_client.batch_write.called + + +def test_prewriting_hook_skipped_for_empty_input(): + """The hook resolver must not even be invoked when ``data`` is empty — + avoids paying the import cost for no-op writes.""" + config = _aerospike_repo_config( + prewriting_hook="should.never.resolve" # would raise if resolved + ) + fv = SimpleNamespace(name="fv") + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + progress_calls: list[int] = [] + store.online_write_batch(config, fv, [], progress=progress_calls.append) + assert not fake_client.batch_write.called + assert progress_calls == [0] + + +# --------------------------------------------------------------------------- +# Async wrappers (run_in_executor) +# --------------------------------------------------------------------------- + + +async def test_online_write_batch_async_delegates_to_sync(): + """The async write path must produce identical side-effects to the sync one.""" + config = _aerospike_repo_config() + fv = SimpleNamespace(name="fv") + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + row = ( + _entity_key("id", 1), + {"x": ValueProto(int64_val=1)}, + datetime(2026, 1, 1, tzinfo=timezone.utc), + None, + ) + progress_calls: list[int] = [] + await store.online_write_batch_async( + config, fv, [row], progress=progress_calls.append + ) + + assert fake_client.batch_write.called + assert progress_calls == [1] + + +async def test_online_read_async_returns_same_shape_as_sync(): + config = _aerospike_repo_config() + fv = _read_feature_view() + store = AerospikeOnlineStore() + ts = datetime(2026, 4, 20, tzinfo=timezone.utc) + ek = _entity_key("driver_id", 1) + key = store._aerospike_key(config, ek) + + def fake_batch_operate(keys, ops): + return SimpleNamespace( + batch_records=[ + _fake_batch_record( + key, + { + "features": {"rating": 4.91, "trips_last_7d": 132}, + "event_ts": _datetime_to_epoch_ms(ts), + }, + ) + ] + ) + + fake_client = MagicMock() + fake_client.batch_operate.side_effect = fake_batch_operate + store._client = fake_client + + results = await store.online_read_async(config, fv, [ek]) + assert len(results) == 1 + ts_out, feats = results[0] + assert ts_out == ts + assert feats["trips_last_7d"].int64_val == 132 + + +async def test_initialize_pre_warms_client(): + """initialize() must cause the client to connect without needing a read/write.""" + config = _aerospike_repo_config() + store = AerospikeOnlineStore() + assert store._client is None + + sentinel = MagicMock(name="warm_client") + store._get_client = MagicMock(return_value=sentinel) # type: ignore[assignment] + + await store.initialize(config) + store._get_client.assert_called_once_with(config) + + +async def test_close_is_noop_without_client(): + store = AerospikeOnlineStore() + assert store._client is None + await store.close() # must not raise + + +async def test_close_releases_client(): + store = AerospikeOnlineStore() + fake_client = MagicMock() + store._client = fake_client + + await store.close() + + fake_client.close.assert_called_once() + assert store._client is None + + +# --------------------------------------------------------------------------- +# End-to-end integration test — requires Docker +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def aerospike_container(): + """Start a real Aerospike CE container for end-to-end testing.""" + container = DockerContainer(AEROSPIKE_CE_IMAGE).with_exposed_ports("3000") + container.start() + wait_for_logs(container=container, predicate="migrations: complete", timeout=60) + yield container + container.stop() + + +@pytest.fixture +def aerospike_online_store_config(aerospike_container): + port = int(aerospike_container.get_exposed_port("3000")) + return { + "type": "aerospike", + "hosts": [("127.0.0.1", port)], + "namespace": "test", # default namespace shipped in the CE image + } + + +@_requires_docker +def test_aerospike_online_features(aerospike_online_store_config): + """Full round-trip: write via the provider, read via the feature store API.""" + runner = CliRunner() + with runner.local_repo( + get_example_repo("example_feature_repo_1.py"), + offline_store="file", + online_store="aerospike", + teardown=False, # container torn down by fixture + ) as store: + # Patch in the live container's port. + store.config.online_store.hosts = aerospike_online_store_config["hosts"] + store.config.online_store.namespace = aerospike_online_store_config["namespace"] + + driver_locations_fv = store.get_feature_view(name="driver_locations") + customer_profile_fv = store.get_feature_view(name="customer_profile") + customer_driver_combined_fv = store.get_feature_view( + name="customer_driver_combined" + ) + + provider = store._get_provider() + + driver_key = EntityKeyProto( + join_keys=["driver_id"], entity_values=[ValueProto(int64_val=1)] + ) + provider.online_write_batch( + config=store.config, + table=driver_locations_fv, + data=[ + ( + driver_key, + { + "lat": ValueProto(double_val=0.1), + "lon": ValueProto(string_val="1.0"), + }, + _utc_now(), + _utc_now(), + ) + ], + progress=None, + ) + + customer_key = EntityKeyProto( + join_keys=["customer_id"], entity_values=[ValueProto(string_val="5")] + ) + provider.online_write_batch( + config=store.config, + table=customer_profile_fv, + data=[ + ( + customer_key, + { + "avg_orders_day": ValueProto(float_val=1.0), + "name": ValueProto(string_val="John"), + "age": ValueProto(int64_val=3), + }, + _utc_now(), + _utc_now(), + ) + ], + progress=None, + ) + + combined_key = EntityKeyProto( + join_keys=["customer_id", "driver_id"], + entity_values=[ValueProto(string_val="5"), ValueProto(int64_val=1)], + ) + provider.online_write_batch( + config=store.config, + table=customer_driver_combined_fv, + data=[ + ( + combined_key, + {"trips": ValueProto(int64_val=7)}, + _utc_now(), + _utc_now(), + ) + ], + progress=None, + ) + + result = store.get_online_features( + features=[ + "driver_locations:lon", + "customer_profile:avg_orders_day", + "customer_profile:name", + "customer_driver_combined:trips", + ], + entity_rows=[ + {"driver_id": 1, "customer_id": "5"}, + {"driver_id": 1, "customer_id": 5}, + ], + full_feature_names=False, + ).to_dict() + + assert result["driver_id"] == [1, 1] + assert result["customer_id"] == ["5", "5"] + assert result["lon"] == ["1.0", "1.0"] + assert result["avg_orders_day"] == [1.0, 1.0] + assert result["name"] == ["John", "John"] + assert result["trips"] == [7, 7] + + missing = store.get_online_features( + features=["customer_driver_combined:trips"], + entity_rows=[{"driver_id": 0, "customer_id": 0}], + full_feature_names=False, + ).to_dict() + assert missing["trips"] == [None] + + +# --------------------------------------------------------------------------- +# Integration tests that exercise the store directly (no CliRunner/apply). +# --------------------------------------------------------------------------- + + +def _integration_repo_config( + aerospike_online_store_config: dict, collection_suffix: str +) -> RepoConfig: + """Build a RepoConfig targeting the live container with an isolated set name. + + Each test passes a unique ``collection_suffix`` so the module-scoped + Aerospike container can host multiple tests without cross-contamination. + """ + return RepoConfig( + project="itest", + provider="local", + registry="/tmp/reg.db", + online_store={ + **aerospike_online_store_config, + "collection_suffix": collection_suffix, + }, + entity_key_serialization_version=3, + ) + + +def _multi_fv_feature_view(name: str) -> SimpleNamespace: + """Minimal FV duck-typed for the store: exposes .name and .features.""" + return SimpleNamespace( + name=name, + features=[ + SimpleNamespace(name="value", dtype=Int64), + ], + ) + + +@_requires_docker +def test_aerospike_cross_fv_map_cdt_upsert(aerospike_online_store_config): + """Writing two feature views for the same entity must not clobber each other. + + The store uses Aerospike Map CDT ops (``map_put_items``) rather than a + full-record ``put``, so two feature views sharing an entity key live in + separate slots of the ``features`` / ``event_ts`` maps. This test + exercises that guarantee end to end against a real server. + """ + config = _integration_repo_config(aerospike_online_store_config, "cross_fv") + store = AerospikeOnlineStore() + + fv_a = _multi_fv_feature_view("driver_stats") + fv_b = _multi_fv_feature_view("driver_geo") + ek = _entity_key("driver_id", 101) + ts = _utc_now() + + store.online_write_batch( + config, + fv_a, + [(ek, {"value": ValueProto(int64_val=42)}, ts, ts)], + progress=None, + ) + # Second write targets the SAME entity key under a different feature + # view — it must add a new map entry rather than overwrite fv_a. + store.online_write_batch( + config, + fv_b, + [(ek, {"value": ValueProto(int64_val=7)}, ts, ts)], + progress=None, + ) + + results_a = store.online_read(config, fv_a, [ek]) + results_b = store.online_read(config, fv_b, [ek]) + + assert len(results_a) == 1 and len(results_b) == 1 + _, feats_a = results_a[0] + _, feats_b = results_b[0] + assert feats_a is not None, "driver_stats was clobbered by the driver_geo write" + assert feats_b is not None, "driver_geo write did not produce a readable slot" + assert feats_a["value"].int64_val == 42 + assert feats_b["value"].int64_val == 7 + + # Sanity-check the raw record too: both feature views should coexist in + # the ``features`` Map CDT under their own names. + client = store._get_client(config) + _, _, bins = client.get(store._aerospike_key(config, ek)) + assert set(bins["features"].keys()) == {"driver_stats", "driver_geo"} + assert set(bins["event_ts"].keys()) == {"driver_stats", "driver_geo"} + + store.teardown(config, [], []) + + +@_requires_docker +def test_aerospike_update_strips_dropped_feature_view(aerospike_online_store_config): + """``update(tables_to_delete=[...])`` removes a FV's slot from every record. + + The store issues a single background scan that applies + ``map_remove_by_key`` to the ``features`` and ``event_ts`` bins for each + dropped feature view. We verify: + + * The dropped FV's slot disappears from the record (read returns + ``(None, None)``). + * The kept FV is untouched (still readable with its original values). + * The underlying record itself still exists (background scan strips map + entries, not whole records). + """ + config = _integration_repo_config(aerospike_online_store_config, "update") + store = AerospikeOnlineStore() + + fv_keep = _multi_fv_feature_view("keep_fv") + fv_drop = _multi_fv_feature_view("drop_fv") + ek = _entity_key("driver_id", 202) + ts = _utc_now() + + store.online_write_batch( + config, + fv_keep, + [(ek, {"value": ValueProto(int64_val=1)}, ts, ts)], + progress=None, + ) + store.online_write_batch( + config, + fv_drop, + [(ek, {"value": ValueProto(int64_val=999)}, ts, ts)], + progress=None, + ) + + # Pre-condition: both feature views readable. + assert store.online_read(config, fv_keep, [ek])[0][1] is not None + assert store.online_read(config, fv_drop, [ek])[0][1] is not None + + store.update( + config=config, + tables_to_delete=[fv_drop], + tables_to_keep=[fv_keep], + entities_to_delete=[], + entities_to_keep=[], + partial=False, + ) + + # The background scan is asynchronous server-side. Poll up to ~10s for + # the drop_fv slot to disappear before failing — this matches how a real + # Feast caller would experience the post-apply propagation delay. + deadline = time.monotonic() + 10.0 + drop_result = store.online_read(config, fv_drop, [ek])[0] + while drop_result != (None, None) and time.monotonic() < deadline: + time.sleep(0.1) + drop_result = store.online_read(config, fv_drop, [ek])[0] + assert drop_result == (None, None), ( + f"drop_fv slot was not cleared by background scan within 10s; " + f"last result: {drop_result!r}" + ) + + # keep_fv must still be present with its original value. + keep_ts, keep_feats = store.online_read(config, fv_keep, [ek])[0] + assert keep_feats is not None, "keep_fv was removed by the scan" + assert keep_feats["value"].int64_val == 1 + assert keep_ts is not None + + # The record itself still exists — only the dropped FV's map entries + # were removed. Verify directly via a raw get so we notice if a future + # change accidentally escalates to a full-record delete. + client = store._get_client(config) + _, _, bins = client.get(store._aerospike_key(config, ek)) + assert "drop_fv" not in bins["features"] + assert "drop_fv" not in bins["event_ts"] + assert "keep_fv" in bins["features"] + + store.teardown(config, [], []) diff --git a/sdk/python/tests/unit/online_store/test_mongodb_online_retrieval.py b/sdk/python/tests/unit/online_store/test_mongodb_online_retrieval.py new file mode 100644 index 00000000000..485ab2dce08 --- /dev/null +++ b/sdk/python/tests/unit/online_store/test_mongodb_online_retrieval.py @@ -0,0 +1,243 @@ +"""Pure unit tests for MongoDB online store helpers.""" + +# ruff: noqa: E402 + +from datetime import datetime, timedelta, timezone + +import pytest + +pytest.importorskip("pymongo") + +from feast import Entity, FeatureView, Field, FileSource, RepoConfig # noqa: E402 +from feast.infra.online_stores.mongodb_online_store.mongodb import ( # noqa: E402 + MongoDBOnlineStore, +) +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.types import Array, Float32, Int64, String + + +def _make_fv(*field_names: str) -> FeatureView: + """Build a minimal FeatureView with Int64 features for use in unit tests.""" + return FeatureView( + name="test_fv", + entities=[], + schema=[Field(name=n, dtype=Int64) for n in field_names], + source=FileSource(path="fake.parquet", timestamp_field="event_timestamp"), + ttl=timedelta(days=1), + ) + + +def test_convert_raw_docs_missing_entity(): + """Entity key absent from docs → result tuple is (None, None) for that position.""" + fv = _make_fv("score") + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + ids = [b"present", b"missing"] + docs = { + b"present": { + "features": {"test_fv": {"score": 42}}, + "event_timestamps": {"test_fv": ts}, + } + } + + results = MongoDBOnlineStore._convert_raw_docs_to_proto(ids, docs, fv) + + assert len(results) == 2 + ts_out, feats_out = results[0] + assert ts_out == ts + assert feats_out["score"].int64_val == 42 + assert results[1] == (None, None) + + +def test_convert_raw_docs_partial_doc(): + """Entity exists but one feature key is absent → empty ValueProto for that feature.""" + fv = _make_fv("present_feat", "missing_feat") + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + ids = [b"entity1"] + docs = { + b"entity1": { + # missing_feat intentionally omitted (e.g. schema migration scenario) + "features": {"test_fv": {"present_feat": 99}}, + "event_timestamps": {"test_fv": ts}, + } + } + + results = MongoDBOnlineStore._convert_raw_docs_to_proto(ids, docs, fv) + + assert len(results) == 1 + ts_out, feats_out = results[0] + assert ts_out == ts + assert feats_out["present_feat"].int64_val == 99 + assert feats_out["missing_feat"] == ValueProto() # null / not-set + + +def test_convert_raw_docs_entity_exists_but_fv_not_written(): + """Entity doc exists (written by another FV) but this FV was never written → (None, None). + + MongoDB stores all feature views for the same entity in one document. + If FV "driver_stats" was written, an entity doc exists for driver_1. + A subsequent read for FV "pricing" (never written) must return (None, None), + not a truthy dict of empty ValueProtos. + """ + pricing_fv = _make_fv("price") + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + ids = [b"driver_1"] + # doc was created by driver_stats, pricing key is absent entirely + docs = { + b"driver_1": { + "features": {"driver_stats": {"acc_rate": 0.9}}, + "event_timestamps": {"driver_stats": ts}, + } + } + + results = MongoDBOnlineStore._convert_raw_docs_to_proto(ids, docs, pricing_fv) + + assert len(results) == 1 + assert results[0] == (None, None) + + +def test_convert_raw_docs_ordering(): + """Result order matches the ids list regardless of dict insertion order in docs.""" + fv = _make_fv("score") + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + + # Request entity keys in z → a → m order + ids = [b"entity_z", b"entity_a", b"entity_m"] + + # docs is in a different order (simulating arbitrary MongoDB cursor return order) + docs = { + b"entity_a": { + "features": {"test_fv": {"score": 2}}, + "event_timestamps": {"test_fv": ts}, + }, + b"entity_m": { + "features": {"test_fv": {"score": 3}}, + "event_timestamps": {"test_fv": ts}, + }, + b"entity_z": { + "features": {"test_fv": {"score": 1}}, + "event_timestamps": {"test_fv": ts}, + }, + } + + results = MongoDBOnlineStore._convert_raw_docs_to_proto(ids, docs, fv) + + assert len(results) == 3 + # Results must follow the ids order: z=1, a=2, m=3 + assert results[0][1]["score"].int64_val == 1 # entity_z + assert results[1][1]["score"].int64_val == 2 # entity_a + assert results[2][1]["score"].int64_val == 3 # entity_m + + +# --------------------------------------------------------------------------- +# Vector search validation — pure Python, no Docker required +# --------------------------------------------------------------------------- + + +def _make_vector_fv( + *, vector_index: bool = True, vector_length: int = 3 +) -> FeatureView: + """Build a FeatureView with an embedding field for vector search tests.""" + embedding_kwargs: dict = {"name": "embedding", "dtype": Array(Float32)} + if vector_index: + embedding_kwargs.update( + vector_index=True, + vector_length=vector_length, + vector_search_metric="cosine", + ) + return FeatureView( + name="item_embeddings", + entities=[Entity(name="item_id", join_keys=["item_id"])], + schema=[ + Field(**embedding_kwargs), + Field(name="title", dtype=String), + Field(name="item_id", dtype=Int64), + ], + source=FileSource(path="fake.parquet", timestamp_field="event_timestamp"), + ttl=timedelta(hours=24), + ) + + +def _make_repo_config(*, vector_enabled: bool = True) -> RepoConfig: + """Build a RepoConfig with MongoDB online store for vector search tests.""" + return RepoConfig( + project="test_vs", + provider="local", + online_store={ + "type": "mongodb", + "connection_string": "mongodb://localhost:27017", + "vector_enabled": vector_enabled, + }, + registry="memory://", + entity_key_serialization_version=3, + ) + + +def test_retrieve_raises_when_vector_not_enabled(): + """retrieve_online_documents_v2 raises ValueError when vector_enabled=False.""" + store = MongoDBOnlineStore() + config = _make_repo_config(vector_enabled=False) + fv = _make_vector_fv() + + with pytest.raises(ValueError, match="Vector search is not enabled"): + store.retrieve_online_documents_v2( + config=config, + table=fv, + requested_features=["embedding"], + embedding=[1.0, 0.0, 0.0], + top_k=3, + ) + + +def test_retrieve_raises_when_embedding_is_none(): + """retrieve_online_documents_v2 raises ValueError when embedding is None.""" + store = MongoDBOnlineStore() + config = _make_repo_config(vector_enabled=True) + fv = _make_vector_fv() + + with pytest.raises(ValueError, match="embedding vector must be provided"): + store.retrieve_online_documents_v2( + config=config, + table=fv, + requested_features=["embedding"], + embedding=None, + top_k=3, + ) + + +def test_retrieve_raises_when_no_vector_fields(): + """retrieve_online_documents_v2 raises ValueError when no fields have vector_index=True.""" + store = MongoDBOnlineStore() + config = _make_repo_config(vector_enabled=True) + fv = _make_vector_fv(vector_index=False) + + with pytest.raises(ValueError, match="has no fields with vector_index=True"): + store.retrieve_online_documents_v2( + config=config, + table=fv, + requested_features=["embedding"], + embedding=[1.0, 0.0, 0.0], + top_k=3, + ) + + +def test_ensure_vector_indexes_raises_on_missing_vector_length(): + """_ensure_vector_indexes raises ValueError when vector_length is not set.""" + from unittest.mock import MagicMock + + store = MongoDBOnlineStore() + config = _make_repo_config(vector_enabled=True) + online_config = config.online_store + + # Create a feature view where vector_index=True but vector_length=0/None + fv = _make_vector_fv(vector_index=True, vector_length=0) + + # Mock collection so we don't need a real MongoDB connection + mock_collection = MagicMock() + mock_collection.database.list_collection_names.return_value = ["test_vs_latest"] + mock_collection.name = "test_vs_latest" + mock_collection.list_search_indexes.return_value = [] + + with pytest.raises( + ValueError, match="vector_index=True but vector_length is not set" + ): + store._ensure_vector_indexes(mock_collection, [fv], online_config) diff --git a/sdk/python/tests/unit/online_store/test_online_retrieval.py b/sdk/python/tests/unit/online_store/test_online_retrieval.py index 26d9430d1b6..96853618c59 100644 --- a/sdk/python/tests/unit/online_store/test_online_retrieval.py +++ b/sdk/python/tests/unit/online_store/test_online_retrieval.py @@ -21,7 +21,7 @@ from feast.torch_wrapper import get_torch from feast.types import ValueType from feast.utils import _utc_now -from tests.integration.feature_repos.universal.feature_views import TAGS +from tests.universal.feature_repos.universal.feature_views import TAGS from tests.utils.cli_repo_creator import CliRunner, get_example_repo @@ -1208,7 +1208,7 @@ def test_milvus_lite_retrieve_online_documents_v2() -> None: ) documents_df = pd.DataFrame( { - "item_id": [str(i) for i in range(n)], + "item_id": [i for i in range(n)], "author_id": [f"author_{i}" for i in range(n)], "vector": [ np.random.random( @@ -1303,7 +1303,6 @@ def test_milvus_stored_writes_with_explode() -> None: ) random.seed(42) - vector_length = 10 runner = CliRunner() with runner.local_repo( example_repo_py=get_example_repo("example_rag_feature_repo.py"), @@ -1357,10 +1356,10 @@ def milvus_explode_feature_view(inputs: dict[str, Any]): "Document chunking example.", ], "vector": [ - [0.1] * vector_length, - [0.2] * vector_length, - [0.3] * vector_length, - [0.4] * vector_length, + [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], ], } return output @@ -1428,7 +1427,8 @@ def milvus_explode_feature_view(inputs: dict[str, Any]): ) # Test vector search using Milvus - query_embedding = [0.1] * vector_length + # Query vector closest to doc_2/chunk-1 [0,0,1,0,...] then doc_1/chunk-2 [0,1,0,0,...] + query_embedding = [0.0, 0.3, 0.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # First get Milvus client and search directly client = store._provider._online_store.client @@ -1714,3 +1714,181 @@ def test_milvus_keyword_search() -> None: assert len(result_hybrid["content"]) > 0 assert any("Feast" in content for content in result_hybrid["content"]) assert len(result_hybrid["vector"]) > 0 + + +def test_milvus_update_preserves_collection_cache() -> None: + """ + Regression test: update() used to overwrite self._collections with the + describe_collection() dict of the last processed table, replacing the + dict-of-dicts cache with a single flat dict. After the fix, each call + to _get_or_create_collection() updates the keyed entry in-place and the + cache remains a proper mapping from collection name to collection info. + """ + from datetime import timedelta + + from feast import Entity, FeatureView, Field, FileSource + from feast.types import Array, Float32, Int64, String + + runner = CliRunner() + with runner.local_repo( + example_repo_py=get_example_repo("example_rag_feature_repo.py"), + offline_store="file", + online_store="milvus", + apply=False, + teardown=False, + ) as store: + source = FileSource( + path="data/dummy.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created_timestamp", + ) + entity_a = Entity(name="id_a", join_keys=["id_a"], value_type=ValueType.INT64) + entity_b = Entity(name="id_b", join_keys=["id_b"], value_type=ValueType.INT64) + + fv_a = FeatureView( + name="fv_a", + entities=[entity_a], + schema=[ + Field(name="id_a", dtype=Int64), + Field( + name="vec_a", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="text_a", dtype=String), + ], + source=source, + ttl=timedelta(hours=1), + ) + fv_b = FeatureView( + name="fv_b", + entities=[entity_b], + schema=[ + Field(name="id_b", dtype=Int64), + Field( + name="vec_b", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="text_b", dtype=String), + ], + source=source, + ttl=timedelta(hours=1), + ) + + store.apply([source, entity_a, entity_b, fv_a, fv_b]) + + online_store = store._provider._online_store + # After applying two feature views, the cache must be a proper dict + # mapping collection names to collection-info dicts, not a flat dict. + assert isinstance(online_store._collections, dict), ( + "_collections should be a dict" + ) + collection_name_a = f"{store.config.project}_fv_a" + collection_name_b = f"{store.config.project}_fv_b" + assert collection_name_a in online_store._collections, ( + f"Cache missing entry for {collection_name_a}" + ) + assert collection_name_b in online_store._collections, ( + f"Cache missing entry for {collection_name_b} — " + "update() likely overwrote _collections with a single collection dict" + ) + # Each cached value must be a collection-info dict (has a 'fields' key), + # not itself keyed by collection name. + for name in [collection_name_a, collection_name_b]: + assert "fields" in online_store._collections[name], ( + f"Cache entry for {name} looks like a corrupted flat dict" + ) + + +def test_milvus_plan_returns_empty_list() -> None: + """ + Regression test: plan() used to raise NotImplementedError, causing + `feast plan` to crash for any project using the Milvus online store. + It should return [] matching the OnlineStore base class default. + """ + from feast.infra.online_stores.milvus_online_store.milvus import MilvusOnlineStore + + store = MilvusOnlineStore() + result = store.plan(config=None, desired_registry_proto=None) # type: ignore[arg-type] + assert result == [], f"plan() should return [] but returned {result!r}" + + +def test_milvus_retrieve_online_documents_v2_missing_entity_key() -> None: + """ + Regression test: retrieve_online_documents_v2() passed the raw + hit.get("entity", {}).get(composite_key_name, None) directly to + bytes.fromhex(), raising TypeError when the key was absent. + After the fix, a missing composite key produces a None entity_key_proto + instead of crashing. + """ + from datetime import timedelta + from unittest.mock import patch + + from feast import Entity, FeatureView, Field, FileSource + from feast.types import Array, Float32, Int64, String + + runner = CliRunner() + with runner.local_repo( + example_repo_py=get_example_repo("example_rag_feature_repo.py"), + offline_store="file", + online_store="milvus", + apply=False, + teardown=False, + ) as store: + source = FileSource( + path="data/dummy.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created_timestamp", + ) + entity = Entity(name="doc_id", join_keys=["doc_id"], value_type=ValueType.INT64) + fv = FeatureView( + name="docs", + entities=[entity], + schema=[ + Field(name="doc_id", dtype=Int64), + Field( + name="vec", + dtype=Array(Float32), + vector_index=True, + vector_search_metric="COSINE", + ), + Field(name="text", dtype=String), + ], + source=source, + ttl=timedelta(hours=1), + ) + store.apply([source, entity, fv]) + + online_store = store._provider._online_store + fv_obj = store.get_feature_view("docs") + # Simulate a search hit that is missing the composite primary key. + fake_hit = { + "entity": { + "event_ts": int(_utc_now().timestamp() * 1e6), + "created_ts": int(_utc_now().timestamp() * 1e6), + "text": "hello", + }, + "distance": 0.9, + } + + mock_results = [[fake_hit]] + with patch.object(online_store.client, "search", return_value=mock_results): + with patch.object( + online_store.client, "load_collection", return_value=None + ): + # Before the fix this raised TypeError: fromhex argument must be str, not None + result = online_store.retrieve_online_documents_v2( + config=store.config, + table=fv_obj, + requested_features=["text"], + embedding=[0.1] * 10, + top_k=1, + ) + assert len(result) == 1 + _ts, entity_key_proto, _features = result[0] + assert entity_key_proto is None, ( + "entity_key_proto should be None when the composite key is absent from the hit" + ) diff --git a/sdk/python/tests/unit/openlineage/__init__.py b/sdk/python/tests/unit/openlineage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/unit/openlineage/test_client.py b/sdk/python/tests/unit/openlineage/test_client.py new file mode 100644 index 00000000000..a98420144af --- /dev/null +++ b/sdk/python/tests/unit/openlineage/test_client.py @@ -0,0 +1,147 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import pytest + +# --------------------------------------------------------------------------- +# Guard: skip entire module if openlineage-python is not installed +# --------------------------------------------------------------------------- +ol = pytest.importorskip( + "openlineage.client", reason="openlineage-python not installed" +) + +from openlineage.client.transport.console import ConsoleTransport # noqa: E402 +from openlineage.client.transport.transform import TransformTransport # noqa: E402 + +from feast.openlineage.client import FeastOpenLineageClient # noqa: E402 +from feast.openlineage.config import OpenLineageConfig # noqa: E402 + +_TRANSFORM_YML = """\ +transport: + type: transform + transformer_class: openlineage.client.transport.transform.JobNamespaceReplaceTransformer + transformer_properties: + new_job_namespace: new_value + transport: + type: console +""" + + +def _write_openlineage_yml(tmp_path: Path, content: str = _TRANSFORM_YML) -> str: + """Write an openlineage.yml file and return its path.""" + yml = tmp_path / "openlineage.yml" + yml.write_text(content) + return str(yml) + + +class TestDefaultConsoleTransport: + """When transport_type is None and there is no openlineage.yml, + the OpenLineage SDK should fall back to ConsoleTransport.""" + + def test_default_config_uses_console_transport( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.delenv("OPENLINEAGE_CONFIG", raising=False) + monkeypatch.delenv("OPENLINEAGE_URL", raising=False) + monkeypatch.delenv("OPENLINEAGE_DISABLED", raising=False) + + config = OpenLineageConfig(enabled=True) + client = FeastOpenLineageClient(config=config) + + assert client.is_enabled + assert isinstance(client._client.transport, ConsoleTransport) + + def test_default_from_dict_uses_console_transport( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.delenv("OPENLINEAGE_CONFIG", raising=False) + monkeypatch.delenv("OPENLINEAGE_URL", raising=False) + monkeypatch.delenv("OPENLINEAGE_DISABLED", raising=False) + + config = OpenLineageConfig.from_dict({"enabled": True}) + client = FeastOpenLineageClient(config=config) + + assert client.is_enabled + assert isinstance(client._client.transport, ConsoleTransport) + + +class TestTransformTransportFromYml: + def test_transform_yml_is_respected( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + yml_path = _write_openlineage_yml(tmp_path) + monkeypatch.setenv("OPENLINEAGE_CONFIG", yml_path) + monkeypatch.delenv("OPENLINEAGE_URL", raising=False) + monkeypatch.delenv("OPENLINEAGE_DISABLED", raising=False) + + config = OpenLineageConfig(enabled=True) + client = FeastOpenLineageClient(config=config) + + assert client.is_enabled + assert isinstance(client._client.transport, TransformTransport) + assert isinstance(client._client.transport.transport, ConsoleTransport) + + def test_transform_yml_in_cwd_is_respected( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv("OPENLINEAGE_CONFIG", raising=False) + monkeypatch.delenv("OPENLINEAGE_URL", raising=False) + monkeypatch.delenv("OPENLINEAGE_DISABLED", raising=False) + + _write_openlineage_yml(tmp_path) + monkeypatch.chdir(tmp_path) + + config = OpenLineageConfig(enabled=True) + client = FeastOpenLineageClient(config=config) + + assert client.is_enabled + assert isinstance(client._client.transport, TransformTransport) + + +class TestExplicitConfigOverridesYml: + def test_explicit_console_ignores_yml_env( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + yml_path = _write_openlineage_yml(tmp_path) + monkeypatch.setenv("OPENLINEAGE_CONFIG", yml_path) + monkeypatch.delenv("OPENLINEAGE_URL", raising=False) + monkeypatch.delenv("OPENLINEAGE_DISABLED", raising=False) + + config = OpenLineageConfig(enabled=True, transport_type="console") + client = FeastOpenLineageClient(config=config) + + assert client.is_enabled + assert isinstance(client._client.transport, ConsoleTransport) + assert not isinstance(client._client.transport, TransformTransport) + + def test_explicit_console_ignores_yml_in_cwd( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv("OPENLINEAGE_CONFIG", raising=False) + monkeypatch.delenv("OPENLINEAGE_URL", raising=False) + monkeypatch.delenv("OPENLINEAGE_DISABLED", raising=False) + + _write_openlineage_yml(tmp_path) + monkeypatch.chdir(tmp_path) + + config = OpenLineageConfig(enabled=True, transport_type="console") + client = FeastOpenLineageClient(config=config) + + assert client.is_enabled + assert isinstance(client._client.transport, ConsoleTransport) + assert not isinstance(client._client.transport, TransformTransport) diff --git a/sdk/python/tests/unit/openlineage/test_consumer.py b/sdk/python/tests/unit/openlineage/test_consumer.py new file mode 100644 index 00000000000..1666063dcbf --- /dev/null +++ b/sdk/python/tests/unit/openlineage/test_consumer.py @@ -0,0 +1,439 @@ +"""Comprehensive tests for the OpenLineage consumer API endpoints.""" + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.pool import StaticPool + +from feast.openlineage.config import OpenLineageConfig, OpenLineageConsumerConfig +from feast.openlineage.consumer import get_consumer_router +from feast.openlineage.processor import OpenLineageProcessor +from feast.openlineage.store import OpenLineageStore + + +def _make_run_event( + job_ns="test-ns", + job_name="test-job", + run_id="run-001", + event_type="COMPLETE", + producer="https://github.com/test/producer", + inputs=None, + outputs=None, +): + event = { + "eventType": event_type, + "eventTime": "2026-01-15T10:00:00Z", + "producer": producer, + "job": {"namespace": job_ns, "name": job_name, "facets": {}}, + "run": {"runId": run_id, "facets": {}}, + "inputs": inputs or [], + "outputs": outputs or [], + } + return event + + +@pytest.fixture +def app_no_key(): + """App with consumer that has no API key requirement.""" + engine = create_engine( + "sqlite://", + echo=False, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + store = OpenLineageStore(engine=engine) + store.initialize() + processor = OpenLineageProcessor(store=store) + config = OpenLineageConfig( + enabled=True, + consumer=OpenLineageConsumerConfig(enabled=True, api_key=None), + ) + app = FastAPI() + router = get_consumer_router(config, store, processor) + app.include_router(router, prefix="/api/v1") + return app, store + + +@pytest.fixture +def app_with_key(): + """App with consumer that requires API key.""" + engine = create_engine( + "sqlite://", + echo=False, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + store = OpenLineageStore(engine=engine) + store.initialize() + processor = OpenLineageProcessor(store=store) + config = OpenLineageConfig( + enabled=True, + consumer=OpenLineageConsumerConfig(enabled=True, api_key="secret-key"), + ) + app = FastAPI() + router = get_consumer_router(config, store, processor) + app.include_router(router, prefix="/api/v1") + return app, store + + +@pytest.fixture +def app_with_rbac(): + """App with namespace RBAC filtering.""" + engine = create_engine( + "sqlite://", + echo=False, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + store = OpenLineageStore(engine=engine) + store.initialize() + processor = OpenLineageProcessor(store=store) + config = OpenLineageConfig( + enabled=True, + consumer=OpenLineageConsumerConfig(enabled=True), + ) + app = FastAPI() + router = get_consumer_router( + config, + store, + processor, + get_allowed_namespaces=lambda: ["allowed-ns"], + ) + app.include_router(router, prefix="/api/v1") + return app, store + + +# ── Event Ingestion ── + + +class TestEventIngestion: + def test_single_event_returns_201(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + resp = client.post("/api/v1/v1/lineage", json=_make_run_event()) + assert resp.status_code == 201 + assert "event_id" in resp.json() + + def test_array_of_events_returns_200(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + events = [_make_run_event(run_id=f"r{i}") for i in range(3)] + resp = client.post("/api/v1/v1/lineage", json=events) + assert resp.status_code == 200 + assert resp.json()["summary"]["received"] == 3 + assert resp.json()["summary"]["successful"] == 3 + + def test_batch_endpoint(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + events = [_make_run_event(run_id=f"r{i}") for i in range(2)] + resp = client.post("/api/v1/v1/lineage/batch", json=events) + assert resp.status_code == 204 + + def test_batch_rejects_non_array(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + resp = client.post("/api/v1/v1/lineage/batch", json={"not": "array"}) + assert resp.status_code == 400 + + def test_api_key_required(self, app_with_key): + app, _ = app_with_key + client = TestClient(app) + resp = client.post("/api/v1/v1/lineage", json=_make_run_event()) + assert resp.status_code == 401 + + def test_api_key_via_header(self, app_with_key): + app, _ = app_with_key + client = TestClient(app) + resp = client.post( + "/api/v1/v1/lineage", + json=_make_run_event(), + headers={"X-API-Key": "secret-key"}, + ) + assert resp.status_code == 201 + + def test_api_key_via_bearer(self, app_with_key): + app, _ = app_with_key + client = TestClient(app) + resp = client.post( + "/api/v1/v1/lineage", + json=_make_run_event(), + headers={"Authorization": "Bearer secret-key"}, + ) + assert resp.status_code == 201 + + def test_wrong_api_key_rejected(self, app_with_key): + app, _ = app_with_key + client = TestClient(app) + resp = client.post( + "/api/v1/v1/lineage", + json=_make_run_event(), + headers={"X-API-Key": "wrong-key"}, + ) + assert resp.status_code == 401 + + +# ── Query Endpoints ── + + +class TestQueryEndpoints: + def _ingest(self, client, events): + for evt in events: + client.post("/api/v1/v1/lineage", json=evt) + + def test_get_events(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + self._ingest(client, [_make_run_event()]) + resp = client.get("/api/v1/lineage/openlineage/events") + assert resp.status_code == 200 + assert resp.json()["total"] >= 1 + + def test_get_events_filtered(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + self._ingest( + client, + [ + _make_run_event(job_ns="ns-a", job_name="j1", run_id="r1"), + _make_run_event(job_ns="ns-b", job_name="j2", run_id="r2"), + ], + ) + resp = client.get("/api/v1/lineage/openlineage/events?namespace=ns-a") + assert resp.status_code == 200 + for evt in resp.json()["events"]: + assert evt["job_namespace"] == "ns-a" + + def test_get_jobs(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + self._ingest(client, [_make_run_event()]) + resp = client.get("/api/v1/lineage/openlineage/jobs") + assert resp.status_code == 200 + assert len(resp.json()["jobs"]) >= 1 + + def test_get_datasets(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + self._ingest( + client, + [ + _make_run_event( + inputs=[ + {"namespace": "s3://bucket", "name": "input-ds", "facets": {}} + ], + outputs=[ + {"namespace": "s3://bucket", "name": "output-ds", "facets": {}} + ], + ), + ], + ) + resp = client.get("/api/v1/lineage/openlineage/datasets") + assert resp.status_code == 200 + assert len(resp.json()["datasets"]) >= 1 + + def test_get_full_graph(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + self._ingest( + client, + [ + _make_run_event( + inputs=[ + {"namespace": "s3://bucket", "name": "input-ds", "facets": {}} + ], + outputs=[ + {"namespace": "s3://bucket", "name": "output-ds", "facets": {}} + ], + ), + ], + ) + resp = client.get("/api/v1/lineage/openlineage/graph") + assert resp.status_code == 200 + data = resp.json() + assert "nodes" in data + assert "edges" in data + + def test_get_graph_by_node(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + self._ingest( + client, + [ + _make_run_event( + inputs=[ + {"namespace": "s3://bucket", "name": "input-ds", "facets": {}} + ], + ), + ], + ) + resp = client.get("/api/v1/lineage/openlineage/graph/job/test-ns/test-job") + assert resp.status_code == 200 + + def test_get_graph_invalid_node_type(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + resp = client.get("/api/v1/lineage/openlineage/graph/invalid/ns/name") + assert resp.status_code == 400 + + +# ── Reset Endpoint ── + + +class TestResetEndpoint: + def test_reset_requires_api_key(self, app_with_key): + app, _ = app_with_key + client = TestClient(app) + resp = client.delete("/api/v1/lineage/openlineage/reset") + assert resp.status_code == 401 + + def test_reset_full_purge(self, app_with_key): + app, store = app_with_key + client = TestClient(app) + client.post( + "/api/v1/v1/lineage", + json=_make_run_event(), + headers={"X-API-Key": "secret-key"}, + ) + resp = client.delete( + "/api/v1/lineage/openlineage/reset", + headers={"X-API-Key": "secret-key"}, + ) + assert resp.status_code == 200 + assert resp.json()["status"] == "success" + assert len(store.get_events()) == 0 + + def test_reset_by_namespace(self, app_with_key): + app, store = app_with_key + client = TestClient(app) + client.post( + "/api/v1/v1/lineage", + json=_make_run_event(job_ns="ns-keep", run_id="r1"), + headers={"X-API-Key": "secret-key"}, + ) + client.post( + "/api/v1/v1/lineage", + json=_make_run_event(job_ns="ns-delete", run_id="r2"), + headers={"X-API-Key": "secret-key"}, + ) + resp = client.delete( + "/api/v1/lineage/openlineage/reset?namespace=ns-delete", + headers={"X-API-Key": "secret-key"}, + ) + assert resp.status_code == 200 + assert len(store.get_events(namespace="ns-delete")) == 0 + assert len(store.get_events(namespace="ns-keep")) >= 1 + + def test_reset_no_key_when_not_required(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + resp = client.delete("/api/v1/lineage/openlineage/reset") + assert resp.status_code == 200 + + +# ── Runs Endpoints ── + + +class TestRunsEndpoints: + def _seed(self, client): + client.post( + "/api/v1/v1/lineage", + json=_make_run_event( + job_ns="ns-a", + job_name="j1", + run_id="r1", + inputs=[{"namespace": "s3://b", "name": "in1", "facets": {}}], + outputs=[{"namespace": "s3://b", "name": "out1", "facets": {}}], + ), + ) + client.post( + "/api/v1/v1/lineage", + json=_make_run_event( + job_ns="ns-b", + job_name="j2", + run_id="r2", + ), + ) + + def test_list_runs_unfiltered(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + self._seed(client) + resp = client.get("/api/v1/lineage/openlineage/runs") + assert resp.status_code == 200 + assert resp.json()["total"] >= 2 + + def test_list_runs_filtered_by_job(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + self._seed(client) + resp = client.get( + "/api/v1/lineage/openlineage/runs?job_namespace=ns-a&job_name=j1" + ) + assert resp.status_code == 200 + runs = resp.json()["runs"] + assert all(r["job_namespace"] == "ns-a" for r in runs) + + def test_run_detail_found(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + self._seed(client) + resp = client.get("/api/v1/lineage/openlineage/runs/r1") + assert resp.status_code == 200 + detail = resp.json() + assert detail["run_id"] == "r1" + assert len(detail["inputs"]) >= 1 + assert len(detail["outputs"]) >= 1 + + def test_run_detail_not_found(self, app_no_key): + app, _ = app_no_key + client = TestClient(app) + resp = client.get("/api/v1/lineage/openlineage/runs/nonexistent") + assert resp.status_code == 404 + + +# ── Namespace RBAC ── + + +class TestNamespaceRBAC: + def test_events_filtered_by_allowed_namespaces(self, app_with_rbac): + app, store = app_with_rbac + client = TestClient(app) + store.store_event( + "e1", + { + "eventType": "COMPLETE", + "eventTime": "2026-01-01T00:00:00Z", + "producer": "test", + "job": {"namespace": "allowed-ns", "name": "j1"}, + "run": {"runId": "r1"}, + }, + ) + store.store_event( + "e2", + { + "eventType": "COMPLETE", + "eventTime": "2026-01-01T00:00:00Z", + "producer": "test", + "job": {"namespace": "blocked-ns", "name": "j2"}, + "run": {"runId": "r2"}, + }, + ) + resp = client.get("/api/v1/lineage/openlineage/events") + assert resp.status_code == 200 + events = resp.json()["events"] + namespaces = {e["job_namespace"] for e in events} + assert "blocked-ns" not in namespaces + + def test_jobs_filtered_by_allowed_namespaces(self, app_with_rbac): + app, store = app_with_rbac + client = TestClient(app) + store.upsert_job("allowed-ns", "j1", {"facets": {}}) + store.upsert_job("blocked-ns", "j2", {"facets": {}}) + resp = client.get("/api/v1/lineage/openlineage/jobs") + assert resp.status_code == 200 + jobs = resp.json()["jobs"] + namespaces = {j["job_namespace"] for j in jobs} + assert "allowed-ns" in namespaces + assert "blocked-ns" not in namespaces diff --git a/sdk/python/tests/unit/openlineage/test_processor.py b/sdk/python/tests/unit/openlineage/test_processor.py new file mode 100644 index 00000000000..e5ed9388132 --- /dev/null +++ b/sdk/python/tests/unit/openlineage/test_processor.py @@ -0,0 +1,655 @@ +"""Comprehensive tests for OpenLineageProcessor.""" + +import pytest +from sqlalchemy import create_engine + +from feast.openlineage.processor import OpenLineageProcessor +from feast.openlineage.store import OpenLineageStore + + +@pytest.fixture +def store(): + engine = create_engine("sqlite://", echo=False) + s = OpenLineageStore(engine=engine) + s.initialize() + return s + + +@pytest.fixture +def processor(store): + return OpenLineageProcessor(store=store) + + +@pytest.fixture +def processor_with_mapping(store): + return OpenLineageProcessor( + store=store, + namespace_mapping={"spark-ns": "feast-project"}, + ) + + +def _run_event( + job_ns="test-ns", + job_name="etl-job", + run_id="run-001", + event_type="COMPLETE", + producer="https://spark.apache.org", + inputs=None, + outputs=None, + job_facets=None, + run_facets=None, +): + return { + "eventType": event_type, + "eventTime": "2026-06-01T12:00:00Z", + "producer": producer, + "job": {"namespace": job_ns, "name": job_name, "facets": job_facets or {}}, + "run": {"runId": run_id, "facets": run_facets or {}}, + "inputs": inputs or [], + "outputs": outputs or [], + } + + +def _dataset_event( + ds_ns="data-ns", + ds_name="my-table", + producer="https://dbt.com", + ds_facets=None, +): + return { + "eventTime": "2026-06-01T12:00:00Z", + "producer": producer, + "dataset": { + "namespace": ds_ns, + "name": ds_name, + "facets": ds_facets or {}, + }, + } + + +def _job_event( + job_ns="pipeline-ns", + job_name="my-pipeline", + producer="https://airflow.apache.org", + inputs=None, + outputs=None, + job_facets=None, +): + return { + "eventTime": "2026-06-01T12:00:00Z", + "producer": producer, + "job": { + "namespace": job_ns, + "name": job_name, + "facets": job_facets or {}, + }, + "inputs": inputs or [], + "outputs": outputs or [], + } + + +# ── Event type routing ── + + +class TestEventRouting: + def test_run_event_detected(self, processor, store): + eid = processor.process_event(_run_event()) + assert eid + assert len(store.get_events()) == 1 + assert len(store.get_jobs()) == 1 + assert len(store.get_runs()) == 1 + + def test_dataset_event_detected(self, processor, store): + eid = processor.process_event(_dataset_event()) + assert eid + assert len(store.get_events()) == 1 + assert len(store.get_datasets()) == 1 + assert len(store.get_jobs()) == 0 + + def test_job_event_detected(self, processor, store): + eid = processor.process_event(_job_event()) + assert eid + assert len(store.get_events()) == 1 + assert len(store.get_jobs()) == 1 + assert len(store.get_runs()) == 0 + + def test_unknown_event_stored_as_raw(self, processor, store): + eid = processor.process_event( + {"eventTime": "2026-01-01T00:00:00Z", "producer": "x"} + ) + assert eid + assert len(store.get_events()) == 1 + + def test_returns_unique_event_ids(self, processor): + id1 = processor.process_event(_run_event(run_id="r1")) + id2 = processor.process_event(_run_event(run_id="r2")) + assert id1 != id2 + + +# ── RunEvent processing ── + + +class TestProcessRunEvent: + def test_job_created(self, processor, store): + processor.process_event(_run_event(job_ns="ns", job_name="j1")) + jobs = store.get_jobs() + assert len(jobs) == 1 + assert jobs[0]["job_namespace"] == "ns" + assert jobs[0]["job_name"] == "j1" + + def test_run_created(self, processor, store): + processor.process_event(_run_event(run_id="r1", event_type="START")) + runs = store.get_runs() + assert len(runs) == 1 + assert runs[0]["run_id"] == "r1" + assert runs[0]["state"] == "START" + + def test_producer_propagated_to_job(self, processor, store): + processor.process_event(_run_event(producer="https://spark.apache.org")) + jobs = store.get_jobs() + assert jobs[0]["producer"] == "https://spark.apache.org" + + def test_inputs_create_datasets_and_edges(self, processor, store): + processor.process_event( + _run_event( + inputs=[{"namespace": "s3://bucket", "name": "raw-data", "facets": {}}], + ) + ) + datasets = store.get_datasets() + assert any(d["dataset_name"] == "raw-data" for d in datasets) + + edges = store.get_all_lineage_edges() + input_edges = [e for e in edges if e["edge_type"] == "input"] + assert len(input_edges) == 1 + assert input_edges[0]["source_name"] == "raw-data" + assert input_edges[0]["target_name"] == "etl-job" + + def test_outputs_create_datasets_and_edges(self, processor, store): + processor.process_event( + _run_event( + outputs=[ + {"namespace": "s3://bucket", "name": "processed", "facets": {}} + ], + ) + ) + datasets = store.get_datasets() + assert any(d["dataset_name"] == "processed" for d in datasets) + + edges = store.get_all_lineage_edges() + output_edges = [e for e in edges if e["edge_type"] == "output"] + assert len(output_edges) == 1 + assert output_edges[0]["source_name"] == "etl-job" + assert output_edges[0]["target_name"] == "processed" + + def test_run_io_stored(self, processor, store): + processor.process_event( + _run_event( + run_id="r1", + inputs=[{"namespace": "ns", "name": "in1", "facets": {}}], + outputs=[{"namespace": "ns", "name": "out1", "facets": {}}], + ) + ) + detail = store.get_run_detail("r1") + assert len(detail["inputs"]) == 1 + assert len(detail["outputs"]) == 1 + + def test_run_facets_stored(self, processor, store): + processor.process_event( + _run_event( + run_id="r1", + run_facets={"spark.logicalPlan": {"plan": "..."}}, + ) + ) + runs = store.get_runs() + import json + + facets = json.loads(runs[0]["facets_json"]) + assert "spark.logicalPlan" in facets + + def test_input_defaults_namespace_to_job_namespace(self, processor, store): + processor.process_event( + _run_event( + job_ns="my-ns", + inputs=[{"name": "table1", "facets": {}}], + ) + ) + datasets = store.get_datasets() + assert datasets[0]["dataset_namespace"] == "my-ns" + + +# ── Dataset-to-dataset transitive edges ── + + +class TestDatasetToDatasetEdges: + def test_derived_edges_created(self, processor, store): + processor.process_event( + _run_event( + inputs=[{"namespace": "ns", "name": "raw", "facets": {}}], + outputs=[{"namespace": "ns", "name": "clean", "facets": {}}], + ) + ) + edges = store.get_all_lineage_edges() + derived = [e for e in edges if e["edge_type"] == "derived"] + assert len(derived) == 1 + assert derived[0]["source_name"] == "raw" + assert derived[0]["target_name"] == "clean" + + def test_multiple_inputs_outputs_create_cross_product(self, processor, store): + processor.process_event( + _run_event( + inputs=[ + {"namespace": "ns", "name": "in1", "facets": {}}, + {"namespace": "ns", "name": "in2", "facets": {}}, + ], + outputs=[ + {"namespace": "ns", "name": "out1", "facets": {}}, + {"namespace": "ns", "name": "out2", "facets": {}}, + ], + ) + ) + edges = store.get_all_lineage_edges() + derived = [e for e in edges if e["edge_type"] == "derived"] + assert len(derived) == 4 + + def test_no_derived_edge_for_empty_name(self, processor, store): + processor.process_event( + _run_event( + inputs=[{"namespace": "ns", "name": "", "facets": {}}], + outputs=[{"namespace": "ns", "name": "out", "facets": {}}], + ) + ) + edges = store.get_all_lineage_edges() + derived = [e for e in edges if e["edge_type"] == "derived"] + assert len(derived) == 0 + + +# ── DatasetEvent processing ── + + +class TestProcessDatasetEvent: + def test_dataset_created(self, processor, store): + processor.process_event(_dataset_event(ds_ns="ns", ds_name="tbl")) + datasets = store.get_datasets() + assert len(datasets) == 1 + assert datasets[0]["dataset_namespace"] == "ns" + assert datasets[0]["dataset_name"] == "tbl" + + def test_producer_set_on_dataset(self, processor, store): + processor.process_event(_dataset_event(producer="https://dbt.com")) + datasets = store.get_datasets() + assert datasets[0]["producer"] == "https://dbt.com" + + def test_facets_extracted(self, processor, store): + processor.process_event( + _dataset_event( + ds_facets={ + "documentation": {"description": "Customer table"}, + "schema": {"fields": [{"name": "id", "type": "INT"}]}, + }, + ) + ) + datasets = store.get_datasets() + assert datasets[0]["description"] == "Customer table" + + +# ── JobEvent processing ── + + +class TestProcessJobEvent: + def test_job_created(self, processor, store): + processor.process_event(_job_event(job_ns="ns", job_name="pipeline")) + jobs = store.get_jobs() + assert len(jobs) == 1 + assert jobs[0]["job_name"] == "pipeline" + + def test_job_inputs_create_edges(self, processor, store): + processor.process_event( + _job_event( + inputs=[{"namespace": "ns", "name": "src-table", "facets": {}}], + ) + ) + edges = store.get_all_lineage_edges() + assert any( + e["edge_type"] == "input" and e["source_name"] == "src-table" for e in edges + ) + + def test_job_outputs_create_edges(self, processor, store): + processor.process_event( + _job_event( + outputs=[{"namespace": "ns", "name": "dest-table", "facets": {}}], + ) + ) + edges = store.get_all_lineage_edges() + assert any( + e["edge_type"] == "output" and e["target_name"] == "dest-table" + for e in edges + ) + + +# ── Symlink processing ── + + +class TestSymlinkProcessing: + def test_symlinks_facet_creates_symlinks(self, processor, store): + processor.process_event( + _run_event( + inputs=[ + { + "namespace": "spark-ns", + "name": "spark_table", + "facets": { + "symlinks": { + "identifiers": [ + { + "namespace": "hive-ns", + "name": "hive_table", + "type": "TABLE", + }, + ] + } + }, + } + ], + ) + ) + aliases = store.get_dataset_aliases("spark-ns", "spark_table") + assert len(aliases) == 1 + assert aliases[0]["namespace"] == "hive-ns" + assert aliases[0]["name"] == "hive_table" + + def test_symlinks_create_bidirectional_edges(self, processor, store): + processor.process_event( + _run_event( + inputs=[ + { + "namespace": "ns-a", + "name": "ds-a", + "facets": { + "symlinks": { + "identifiers": [ + { + "namespace": "ns-b", + "name": "ds-b", + "type": "TABLE", + }, + ] + } + }, + } + ], + ) + ) + edges = store.get_all_lineage_edges() + symlink_edges = [e for e in edges if e["edge_type"] == "symlink"] + assert len(symlink_edges) == 2 + directions = {(e["source_name"], e["target_name"]) for e in symlink_edges} + assert ("ds-a", "ds-b") in directions + assert ("ds-b", "ds-a") in directions + + def test_symlink_to_self_ignored(self, processor, store): + processor.process_event( + _run_event( + inputs=[ + { + "namespace": "ns", + "name": "ds", + "facets": { + "symlinks": { + "identifiers": [ + {"namespace": "ns", "name": "ds", "type": "TABLE"}, + ] + } + }, + } + ], + ) + ) + aliases = store.get_dataset_aliases("ns", "ds") + assert len(aliases) == 0 + + def test_symlink_linked_dataset_created(self, processor, store): + processor.process_event( + _run_event( + inputs=[ + { + "namespace": "ns-a", + "name": "ds-a", + "facets": { + "symlinks": { + "identifiers": [ + { + "namespace": "ns-b", + "name": "ds-b", + "type": "TABLE", + }, + ] + } + }, + } + ], + ) + ) + datasets = store.get_datasets() + names = {d["dataset_name"] for d in datasets} + assert "ds-b" in names + + def test_datasource_uri_links_datasets(self, processor, store): + store.upsert_dataset( + "existing-ns", + "existing-ds", + facets={"dataSource": {"uri": "postgres://host/db/table"}}, + ) + processor.process_event( + _run_event( + inputs=[ + { + "namespace": "new-ns", + "name": "new-ds", + "facets": { + "dataSource": {"uri": "postgres://host/db/table"}, + }, + } + ], + ) + ) + aliases = store.get_dataset_aliases("new-ns", "new-ds") + assert any( + a["namespace"] == "existing-ns" and a["name"] == "existing-ds" + for a in aliases + ) + + def test_datasource_uri_no_match(self, processor, store): + processor.process_event( + _run_event( + inputs=[ + { + "namespace": "ns", + "name": "ds", + "facets": {"dataSource": {"uri": "s3://unique/path"}}, + } + ], + ) + ) + aliases = store.get_dataset_aliases("ns", "ds") + assert len(aliases) == 0 + + def test_symlinks_in_dataset_event(self, processor, store): + processor.process_event( + _dataset_event( + ds_ns="ns-a", + ds_name="ds-a", + ds_facets={ + "symlinks": { + "identifiers": [ + {"namespace": "ns-b", "name": "ds-b", "type": "TABLE"}, + ] + } + }, + ) + ) + aliases = store.get_dataset_aliases("ns-a", "ds-a") + assert len(aliases) == 1 + + +# ── Feast mapping ── + + +class TestFeastMapping: + def test_online_store_prefix_mapped(self, processor, store): + processor.process_event( + _run_event( + inputs=[ + { + "namespace": "test-ns", + "name": "online_store_driver_fv", + "facets": {}, + } + ], + ) + ) + datasets = store.get_datasets() + ds = [d for d in datasets if d["dataset_name"] == "online_store_driver_fv"][0] + assert ds["feast_object_type"] == "featureView" + assert ds["feast_object_name"] == "driver_fv" + + def test_request_source_prefix_mapped(self, processor, store): + processor.process_event( + _run_event( + inputs=[ + { + "namespace": "test-ns", + "name": "request_source_input", + "facets": {}, + } + ], + ) + ) + datasets = store.get_datasets() + ds = [d for d in datasets if d["dataset_name"] == "request_source_input"][0] + assert ds["feast_object_type"] == "dataSource" + + def test_default_mapping(self, processor, store): + processor.process_event( + _run_event( + inputs=[ + {"namespace": "test-ns", "name": "regular_dataset", "facets": {}} + ], + ) + ) + datasets = store.get_datasets() + ds = [d for d in datasets if d["dataset_name"] == "regular_dataset"][0] + assert ds["feast_object_type"] == "unknown" + + def test_namespace_mapping_applied(self, processor_with_mapping, store): + processor_with_mapping.process_event( + _run_event( + job_ns="spark-ns", + inputs=[ + {"namespace": "spark-ns", "name": "online_store_fv1", "facets": {}} + ], + ) + ) + datasets = store.get_datasets() + ds = [d for d in datasets if d["dataset_name"] == "online_store_fv1"][0] + assert ds["feast_project"] == "feast-project" + + +# ── Batch processing ── + + +class TestBatchProcessing: + def test_batch_all_succeed(self, processor, store): + events = [_run_event(run_id=f"r{i}") for i in range(3)] + result = processor.process_batch(events) + assert result["received"] == 3 + assert result["successful"] == 3 + assert result["failed"] == 0 + assert len(result["event_ids"]) == 3 + + def test_batch_with_failures(self, processor, store): + events = [ + _run_event(run_id="r1"), + "not a dict", + _run_event(run_id="r2"), + ] + result = processor.process_batch(events) + assert result["received"] == 3 + assert result["successful"] == 2 + assert result["failed"] == 1 + + def test_batch_empty(self, processor, store): + result = processor.process_batch([]) + assert result["received"] == 0 + assert result["successful"] == 0 + assert result["failed"] == 0 + + +# ── End-to-end lineage chain ── + + +class TestEndToEndLineage: + def test_full_pipeline_lineage(self, processor, store): + """Simulate: raw_data -> spark_etl -> clean_data -> dbt_model -> analytics_table""" + processor.process_event( + _run_event( + job_ns="spark", + job_name="spark_etl", + run_id="r1", + producer="https://spark.apache.org", + inputs=[{"namespace": "s3", "name": "raw_data", "facets": {}}], + outputs=[ + {"namespace": "warehouse", "name": "clean_data", "facets": {}} + ], + ) + ) + processor.process_event( + _run_event( + job_ns="dbt", + job_name="dbt_model", + run_id="r2", + producer="https://getdbt.com", + inputs=[{"namespace": "warehouse", "name": "clean_data", "facets": {}}], + outputs=[ + {"namespace": "warehouse", "name": "analytics_table", "facets": {}} + ], + ) + ) + + graph = store.get_lineage_graph( + "dataset", "s3", "raw_data", direction="downstream", depth=10 + ) + names = {n["name"] for n in graph["nodes"]} + assert "raw_data" in names + assert "spark_etl" in names + assert "clean_data" in names + assert "dbt_model" in names + assert "analytics_table" in names + + def test_upstream_from_output(self, processor, store): + """Verify upstream traversal from the final output.""" + processor.process_event( + _run_event( + job_ns="ns", + job_name="j1", + run_id="r1", + inputs=[{"namespace": "ns", "name": "a", "facets": {}}], + outputs=[{"namespace": "ns", "name": "b", "facets": {}}], + ) + ) + processor.process_event( + _run_event( + job_ns="ns", + job_name="j2", + run_id="r2", + inputs=[{"namespace": "ns", "name": "b", "facets": {}}], + outputs=[{"namespace": "ns", "name": "c", "facets": {}}], + ) + ) + + graph = store.get_lineage_graph( + "dataset", "ns", "c", direction="upstream", depth=10 + ) + names = {n["name"] for n in graph["nodes"]} + assert "a" in names + assert "j1" in names + assert "c" in names diff --git a/sdk/python/tests/unit/openlineage/test_store.py b/sdk/python/tests/unit/openlineage/test_store.py new file mode 100644 index 00000000000..bf45765358d --- /dev/null +++ b/sdk/python/tests/unit/openlineage/test_store.py @@ -0,0 +1,563 @@ +"""Comprehensive tests for OpenLineageStore.""" + +import json +import time + +import pytest +from sqlalchemy import create_engine, select + +from feast.openlineage.models import OL_TABLES +from feast.openlineage.store import OpenLineageStore + + +@pytest.fixture +def store(): + engine = create_engine("sqlite://", echo=False) + s = OpenLineageStore(engine=engine) + s.initialize() + return s + + +def _make_event( + event_id="evt-1", + event_type="COMPLETE", + job_ns="ns-a", + job_name="job-1", + run_id="run-1", + producer="test-producer", +): + return { + "eventType": event_type, + "eventTime": "2026-01-01T00:00:00Z", + "producer": producer, + "job": {"namespace": job_ns, "name": job_name}, + "run": {"runId": run_id}, + } + + +# ── Initialization ── + + +class TestStoreInitialize: + def test_tables_created(self, store): + with store.engine.connect() as conn: + for tbl in OL_TABLES.values(): + conn.execute(select(tbl)).fetchall() + + def test_idempotent_reinit(self, store): + store.initialize() + with store.engine.connect() as conn: + conn.execute(select(OL_TABLES["events"])).fetchall() + + def test_requires_engine_or_connection_string(self): + with pytest.raises(ValueError): + OpenLineageStore() + + def test_connection_string_init(self, tmp_path): + db = tmp_path / "test.db" + s = OpenLineageStore(connection_string=f"sqlite:///{db}") + s.initialize() + assert s.engine is not None + + +# ── store_event / get_events ── + + +class TestStoreEvent: + def test_insert_and_retrieve(self, store): + store.store_event("e1", _make_event(event_id="e1")) + events = store.get_events() + assert len(events) == 1 + assert events[0]["event_id"] == "e1" + assert events[0]["event_type"] == "COMPLETE" + + def test_event_json_round_trip(self, store): + data = _make_event(event_id="e2") + store.store_event("e2", data) + events = store.get_events() + parsed = json.loads(events[0]["event_json"]) + assert parsed["producer"] == "test-producer" + + +class TestGetEvents: + def test_filter_by_namespace(self, store): + store.store_event("e1", _make_event(event_id="e1", job_ns="ns-a")) + store.store_event("e2", _make_event(event_id="e2", job_ns="ns-b")) + events = store.get_events(namespace="ns-a") + assert len(events) == 1 + assert events[0]["job_namespace"] == "ns-a" + + def test_filter_by_job_name(self, store): + store.store_event("e1", _make_event(event_id="e1", job_name="j1")) + store.store_event("e2", _make_event(event_id="e2", job_name="j2")) + events = store.get_events(job_name="j1") + assert len(events) == 1 + + def test_filter_by_namespaces_list(self, store): + store.store_event("e1", _make_event(event_id="e1", job_ns="ns-a")) + store.store_event("e2", _make_event(event_id="e2", job_ns="ns-b")) + store.store_event("e3", _make_event(event_id="e3", job_ns="ns-c")) + events = store.get_events(namespaces=["ns-a", "ns-c"]) + assert len(events) == 2 + + def test_limit_and_offset(self, store): + for i in range(5): + store.store_event(f"e{i}", _make_event(event_id=f"e{i}")) + events = store.get_events(limit=2) + assert len(events) == 2 + events = store.get_events(limit=2, offset=3) + assert len(events) == 2 + + +# ── upsert_job / get_jobs ── + + +class TestUpsertJob: + def test_insert(self, store): + store.upsert_job("ns", "j1", {"facets": {}}, producer="spark") + jobs = store.get_jobs() + assert len(jobs) == 1 + assert jobs[0]["job_name"] == "j1" + assert jobs[0]["producer"] == "spark" + + def test_update_preserves_fields(self, store): + store.upsert_job( + "ns", + "j1", + {"facets": {"documentation": {"description": "desc1"}}}, + producer="spark", + ) + store.upsert_job("ns", "j1", {"facets": {}}, producer="spark-v2") + jobs = store.get_jobs() + assert len(jobs) == 1 + assert jobs[0]["description"] == "desc1" + assert jobs[0]["producer"] == "spark-v2" + + def test_job_type_extraction(self, store): + store.upsert_job( + "ns", "j1", {"facets": {"jobType": {"processingType": "BATCH"}}} + ) + jobs = store.get_jobs() + assert jobs[0]["job_type"] == "BATCH" + + def test_filter_by_namespaces(self, store): + store.upsert_job("ns-a", "j1", {"facets": {}}) + store.upsert_job("ns-b", "j2", {"facets": {}}) + jobs = store.get_jobs(namespaces=["ns-a"]) + assert len(jobs) == 1 + assert jobs[0]["job_namespace"] == "ns-a" + + +# ── upsert_dataset / get_datasets ── + + +class TestUpsertDataset: + def test_insert(self, store): + store.upsert_dataset("ns", "ds1", producer="dbt") + datasets = store.get_datasets() + assert len(datasets) == 1 + assert datasets[0]["dataset_name"] == "ds1" + assert datasets[0]["producer"] == "dbt" + + def test_schema_and_description(self, store): + facets = { + "schema": {"fields": [{"name": "id", "type": "INT"}]}, + "documentation": {"description": "my dataset"}, + } + store.upsert_dataset("ns", "ds1", facets=facets) + datasets = store.get_datasets() + assert datasets[0]["description"] == "my dataset" + assert json.loads(datasets[0]["schema_json"])["fields"][0]["name"] == "id" + + def test_feast_mapping(self, store): + store.upsert_dataset( + "ns", + "ds1", + feast_mapping={"type": "FeatureView", "name": "fv1", "project": "proj"}, + ) + datasets = store.get_datasets() + assert datasets[0]["feast_object_type"] == "FeatureView" + assert datasets[0]["feast_object_name"] == "fv1" + assert datasets[0]["feast_project"] == "proj" + + def test_update(self, store): + store.upsert_dataset( + "ns", "ds1", facets={"documentation": {"description": "v1"}} + ) + store.upsert_dataset( + "ns", "ds1", facets={"documentation": {"description": "v2"}} + ) + datasets = store.get_datasets() + assert len(datasets) == 1 + assert datasets[0]["description"] == "v2" + + def test_filter_by_namespaces(self, store): + store.upsert_dataset("ns-a", "ds1") + store.upsert_dataset("ns-b", "ds2") + datasets = store.get_datasets(namespaces=["ns-b"]) + assert len(datasets) == 1 + assert datasets[0]["dataset_namespace"] == "ns-b" + + +# ── upsert_run / get_runs ── + + +class TestUpsertRun: + def test_insert_start(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_run("r1", "ns", "j1", "START") + runs = store.get_runs() + assert len(runs) == 1 + assert runs[0]["state"] == "START" + assert runs[0]["started_at"] is not None + assert runs[0]["ended_at"] is None + + def test_update_to_complete(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_run("r1", "ns", "j1", "START") + store.upsert_run("r1", "ns", "j1", "COMPLETE") + runs = store.get_runs() + assert runs[0]["state"] == "COMPLETE" + assert runs[0]["ended_at"] is not None + + def test_update_to_fail(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_run("r1", "ns", "j1", "START") + store.upsert_run("r1", "ns", "j1", "FAIL") + runs = store.get_runs() + assert runs[0]["state"] == "FAIL" + assert runs[0]["ended_at"] is not None + + def test_latest_run_id_updated_on_job(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_run("r1", "ns", "j1", "COMPLETE") + jobs = store.get_jobs() + assert jobs[0]["latest_run_id"] == "r1" + + def test_facets_stored(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_run("r1", "ns", "j1", "COMPLETE", facets={"key": "val"}) + runs = store.get_runs() + assert json.loads(runs[0]["facets_json"])["key"] == "val" + + +class TestGetRuns: + def test_filter_by_job_namespace(self, store): + store.upsert_job("ns-a", "j1", {"facets": {}}) + store.upsert_job("ns-b", "j2", {"facets": {}}) + store.upsert_run("r1", "ns-a", "j1", "COMPLETE") + store.upsert_run("r2", "ns-b", "j2", "COMPLETE") + runs = store.get_runs(job_namespace="ns-a") + assert len(runs) == 1 + assert runs[0]["job_namespace"] == "ns-a" + + def test_filter_by_job_name(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_job("ns", "j2", {"facets": {}}) + store.upsert_run("r1", "ns", "j1", "COMPLETE") + store.upsert_run("r2", "ns", "j2", "COMPLETE") + runs = store.get_runs(job_name="j2") + assert len(runs) == 1 + assert runs[0]["job_name"] == "j2" + + def test_limit_and_offset(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + for i in range(5): + store.upsert_run(f"r{i}", "ns", "j1", "COMPLETE") + runs = store.get_runs(limit=2) + assert len(runs) == 2 + runs = store.get_runs(limit=10, offset=3) + assert len(runs) == 2 + + def test_ordering(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_run("r1", "ns", "j1", "START") + time.sleep(0.01) + store.upsert_run("r2", "ns", "j1", "COMPLETE") + runs = store.get_runs() + assert runs[0]["run_id"] == "r2" + + +# ── get_run_detail ── + + +class TestGetRunDetail: + def test_returns_run_with_io(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_run("r1", "ns", "j1", "COMPLETE") + store.store_run_io("r1", "ns-ds", "input-ds", "INPUT", {"key": "v"}) + store.store_run_io("r1", "ns-ds", "output-ds", "OUTPUT") + + detail = store.get_run_detail("r1") + assert detail is not None + assert detail["run_id"] == "r1" + assert len(detail["inputs"]) == 1 + assert detail["inputs"][0]["name"] == "input-ds" + assert detail["inputs"][0]["facets"]["key"] == "v" + assert len(detail["outputs"]) == 1 + assert detail["outputs"][0]["name"] == "output-ds" + + def test_missing_run_returns_none(self, store): + assert store.get_run_detail("nonexistent") is None + + def test_run_with_no_io(self, store): + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_run("r1", "ns", "j1", "COMPLETE") + detail = store.get_run_detail("r1") + assert detail is not None + assert detail["inputs"] == [] + assert detail["outputs"] == [] + + +# ── store_run_io ── + + +class TestStoreRunIO: + def test_insert(self, store): + store.store_run_io("r1", "ns", "ds1", "INPUT") + tbl = OL_TABLES["run_io"] + with store.engine.connect() as conn: + rows = conn.execute(select(tbl)).fetchall() + assert len(rows) == 1 + + def test_dedup(self, store): + store.store_run_io("r1", "ns", "ds1", "INPUT") + store.store_run_io("r1", "ns", "ds1", "INPUT") + tbl = OL_TABLES["run_io"] + with store.engine.connect() as conn: + rows = conn.execute(select(tbl)).fetchall() + assert len(rows) == 1 + + def test_input_and_output_separate(self, store): + store.store_run_io("r1", "ns", "ds1", "INPUT") + store.store_run_io("r1", "ns", "ds1", "OUTPUT") + tbl = OL_TABLES["run_io"] + with store.engine.connect() as conn: + rows = conn.execute(select(tbl)).fetchall() + assert len(rows) == 2 + + +# ── upsert_lineage_edge / get_all_lineage_edges ── + + +class TestUpsertLineageEdge: + def test_insert(self, store): + store.upsert_lineage_edge("dataset", "ns", "ds1", "job", "ns", "j1", "consumes") + edges = store.get_all_lineage_edges() + assert len(edges) == 1 + assert edges[0]["edge_type"] == "consumes" + + def test_update(self, store): + store.upsert_lineage_edge("dataset", "ns", "ds1", "job", "ns", "j1", "consumes") + store.upsert_lineage_edge("dataset", "ns", "ds1", "job", "ns", "j1", "produces") + edges = store.get_all_lineage_edges() + assert len(edges) == 1 + assert edges[0]["edge_type"] == "produces" + + def test_dedup(self, store): + store.upsert_lineage_edge("dataset", "ns", "ds1", "job", "ns", "j1") + store.upsert_lineage_edge("dataset", "ns", "ds1", "job", "ns", "j1") + edges = store.get_all_lineage_edges() + assert len(edges) == 1 + + +class TestGetAllLineageEdges: + def test_unfiltered(self, store): + store.upsert_lineage_edge("dataset", "ns-a", "ds1", "job", "ns-a", "j1") + store.upsert_lineage_edge("dataset", "ns-b", "ds2", "job", "ns-b", "j2") + edges = store.get_all_lineage_edges() + assert len(edges) == 2 + + def test_namespace_filtered(self, store): + store.upsert_lineage_edge("dataset", "ns-a", "ds1", "job", "ns-a", "j1") + store.upsert_lineage_edge("dataset", "ns-b", "ds2", "job", "ns-b", "j2") + edges = store.get_all_lineage_edges(namespaces=["ns-a"]) + assert len(edges) == 1 + assert edges[0]["source_namespace"] == "ns-a" + + +# ── Lineage graph traversal ── + + +class TestLineageGraph: + def _setup_chain(self, store): + """ds1 -> j1 -> ds2 -> j2 -> ds3""" + store.upsert_lineage_edge("dataset", "ns", "ds1", "job", "ns", "j1") + store.upsert_lineage_edge("job", "ns", "j1", "dataset", "ns", "ds2") + store.upsert_lineage_edge("dataset", "ns", "ds2", "job", "ns", "j2") + store.upsert_lineage_edge("job", "ns", "j2", "dataset", "ns", "ds3") + + def test_downstream(self, store): + self._setup_chain(store) + graph = store.get_lineage_graph("dataset", "ns", "ds1", direction="downstream") + names = {n["name"] for n in graph["nodes"]} + assert "ds1" in names + assert "j1" in names + assert "ds2" in names + + def test_upstream(self, store): + self._setup_chain(store) + graph = store.get_lineage_graph("dataset", "ns", "ds3", direction="upstream") + names = {n["name"] for n in graph["nodes"]} + assert "ds3" in names + assert "j2" in names + assert "ds2" in names + + def test_both(self, store): + self._setup_chain(store) + graph = store.get_lineage_graph("dataset", "ns", "ds2", direction="both") + names = {n["name"] for n in graph["nodes"]} + assert "ds1" in names + assert "ds3" in names + + def test_depth_limit(self, store): + self._setup_chain(store) + graph = store.get_lineage_graph( + "dataset", "ns", "ds1", depth=1, direction="downstream" + ) + names = {n["name"] for n in graph["nodes"]} + assert "ds1" in names + assert "j1" in names + assert "ds3" not in names + + def test_root_included_when_no_edges(self, store): + graph = store.get_lineage_graph("dataset", "ns", "orphan", direction="both") + assert len(graph["nodes"]) == 1 + assert graph["nodes"][0]["name"] == "orphan" + + def test_namespace_rbac_filtering(self, store): + store.upsert_lineage_edge("dataset", "ns-a", "ds1", "job", "ns-b", "j1") + graph = store.get_lineage_graph( + "dataset", + "ns-a", + "ds1", + direction="downstream", + allowed_namespaces=["ns-a"], + ) + assert len(graph["edges"]) == 0 + + +# ── Symlinks ── + + +class TestSymlinks: + def test_upsert_and_retrieve(self, store): + store.upsert_dataset_symlink("ns-a", "ds-a", "ns-b", "ds-b", "symlink") + aliases = store.get_dataset_aliases("ns-a", "ds-a") + assert len(aliases) == 1 + assert aliases[0]["namespace"] == "ns-b" + assert aliases[0]["name"] == "ds-b" + + def test_bidirectional_lookup(self, store): + store.upsert_dataset_symlink("ns-a", "ds-a", "ns-b", "ds-b") + reverse = store.get_dataset_aliases("ns-b", "ds-b") + assert len(reverse) == 1 + assert reverse[0]["namespace"] == "ns-a" + + def test_get_all_symlinks(self, store): + store.upsert_dataset_symlink("ns-a", "ds-a", "ns-b", "ds-b") + store.upsert_dataset_symlink("ns-c", "ds-c", "ns-d", "ds-d") + all_links = store.get_all_symlinks() + assert len(all_links) == 2 + + def test_upsert_updates_existing(self, store): + store.upsert_dataset_symlink("ns-a", "ds-a", "ns-b", "ds-b", "symlink") + store.upsert_dataset_symlink("ns-a", "ds-a", "ns-b", "ds-b", "alias") + aliases = store.get_dataset_aliases("ns-a", "ds-a") + assert len(aliases) == 1 + assert aliases[0]["link_type"] == "alias" + + +# ── find_datasets_by_uri ── + + +class TestFindDatasetsByUri: + def test_match(self, store): + store.upsert_dataset( + "ns", "ds1", facets={"dataSource": {"uri": "s3://bucket/path"}} + ) + store.upsert_dataset( + "ns", "ds2", facets={"dataSource": {"uri": "other://path"}} + ) + results = store.find_datasets_by_uri("s3://bucket/path") + assert len(results) == 1 + assert results[0]["name"] == "ds1" + + def test_no_match(self, store): + store.upsert_dataset( + "ns", "ds1", facets={"dataSource": {"uri": "s3://bucket/path"}} + ) + results = store.find_datasets_by_uri("s3://bucket/other") + assert len(results) == 0 + + def test_no_facets(self, store): + store.upsert_dataset("ns", "ds1") + results = store.find_datasets_by_uri("s3://bucket/path") + assert len(results) == 0 + + +# ── purge_all ── + + +class TestPurgeAll: + def _populate(self, store): + store.store_event("e1", _make_event()) + store.upsert_job("ns", "j1", {"facets": {}}) + store.upsert_dataset("ns", "ds1") + store.upsert_run("r1", "ns", "j1", "COMPLETE") + store.store_run_io("r1", "ns", "ds1", "INPUT") + store.upsert_lineage_edge("dataset", "ns", "ds1", "job", "ns", "j1") + store.upsert_dataset_symlink("ns", "ds1", "ns2", "ds2") + + def test_purge_all_empties_tables(self, store): + self._populate(store) + store.purge_all() + assert len(store.get_events()) == 0 + assert len(store.get_jobs()) == 0 + assert len(store.get_datasets()) == 0 + assert len(store.get_runs()) == 0 + assert len(store.get_all_lineage_edges()) == 0 + assert len(store.get_all_symlinks()) == 0 + + def test_purge_all_idempotent(self, store): + store.purge_all() + store.purge_all() + + +# ── purge_namespace ── + + +class TestPurgeNamespace: + def _populate_two_ns(self, store): + store.store_event("e1", _make_event(event_id="e1", job_ns="ns-a")) + store.store_event("e2", _make_event(event_id="e2", job_ns="ns-b")) + store.upsert_job("ns-a", "j1", {"facets": {}}) + store.upsert_job("ns-b", "j2", {"facets": {}}) + store.upsert_dataset("ns-a", "ds1") + store.upsert_dataset("ns-b", "ds2") + store.upsert_run("r1", "ns-a", "j1", "COMPLETE") + store.upsert_run("r2", "ns-b", "j2", "COMPLETE") + store.store_run_io("r1", "ns-a", "ds1", "INPUT") + store.store_run_io("r2", "ns-b", "ds2", "INPUT") + store.upsert_lineage_edge("dataset", "ns-a", "ds1", "job", "ns-a", "j1") + store.upsert_lineage_edge("dataset", "ns-b", "ds2", "job", "ns-b", "j2") + store.upsert_dataset_symlink("ns-a", "ds1", "ns-a", "ds1-alias") + store.upsert_dataset_symlink("ns-b", "ds2", "ns-b", "ds2-alias") + + def test_purge_removes_only_target_namespace(self, store): + self._populate_two_ns(store) + store.purge_namespace("ns-a") + + assert len(store.get_events(namespace="ns-a")) == 0 + assert len(store.get_events(namespace="ns-b")) == 1 + assert len(store.get_jobs(namespaces=["ns-a"])) == 0 + assert len(store.get_jobs(namespaces=["ns-b"])) == 1 + assert len(store.get_datasets(namespaces=["ns-a"])) == 0 + assert len(store.get_datasets(namespaces=["ns-b"])) == 1 + assert len(store.get_runs(job_namespace="ns-a")) == 0 + assert len(store.get_runs(job_namespace="ns-b")) == 1 + + def test_purge_nonexistent_namespace(self, store): + self._populate_two_ns(store) + store.purge_namespace("ns-nonexistent") + assert len(store.get_events()) == 2 diff --git a/sdk/python/tests/unit/openlineage/test_teardown.py b/sdk/python/tests/unit/openlineage/test_teardown.py new file mode 100644 index 00000000000..2f754c230b5 --- /dev/null +++ b/sdk/python/tests/unit/openlineage/test_teardown.py @@ -0,0 +1,84 @@ +"""Tests for FeatureStore._teardown_openlineage().""" + +from contextvars import ContextVar +from unittest.mock import MagicMock, patch + +import pytest + + +class TestTeardownOpenlineage: + def _make_feature_store( + self, ol_enabled=True, consumer_enabled=True, conn_str="sqlite:///test.db" + ): + """Create a mock FeatureStore with configurable OL settings.""" + from feast.feature_store import FeatureStore + + fs = object.__new__(FeatureStore) + fs._current_project = ContextVar("current_project", default=None) + + mock_ol_config = MagicMock() + mock_ol_config.enabled = ol_enabled + + mock_consumer = MagicMock() + mock_consumer.enabled = consumer_enabled + mock_consumer.connection_string = conn_str + + mock_full_config = MagicMock() + mock_full_config.consumer = mock_consumer + + mock_ol_config.to_openlineage_config.return_value = mock_full_config + + mock_config = MagicMock() + mock_config.project = "test_project" + mock_config.openlineage = mock_ol_config + + fs.config = mock_config + + return fs + + def test_calls_purge_namespace(self): + """When OL consumer is configured, teardown should purge the project namespace.""" + fs = self._make_feature_store() + + with patch("feast.openlineage.store.OpenLineageStore") as mock_store_cls: + mock_instance = mock_store_cls.return_value + fs._teardown_openlineage() + mock_store_cls.assert_called_once_with( + connection_string="sqlite:///test.db" + ) + mock_instance.purge_namespace.assert_called_once_with( + "test_project/test_project" + ) + + def test_no_crash_when_ol_not_configured(self): + """When OL is not configured, teardown should silently succeed.""" + from feast.feature_store import FeatureStore + + fs = object.__new__(FeatureStore) + fs._current_project = ContextVar("current_project", default=None) + mock_config = MagicMock() + mock_config.openlineage = None + fs.config = mock_config + + fs._teardown_openlineage() + + def test_no_crash_when_consumer_disabled(self): + """When consumer is disabled, teardown should not attempt purge.""" + fs = self._make_feature_store(consumer_enabled=False) + fs._teardown_openlineage() + + def test_no_crash_when_ol_disabled(self): + """When OL is disabled entirely, teardown should not attempt purge.""" + fs = self._make_feature_store(ol_enabled=False) + fs._teardown_openlineage() + + def test_exception_is_warning_not_error(self): + """If purge fails, it should warn, not raise.""" + fs = self._make_feature_store() + + with patch( + "feast.openlineage.store.OpenLineageStore", + side_effect=Exception("DB error"), + ): + with pytest.warns(match="Failed to clean up OpenLineage"): + fs._teardown_openlineage() diff --git a/sdk/python/tests/unit/permissions/auth/client/test_http_session_manager.py b/sdk/python/tests/unit/permissions/auth/client/test_http_session_manager.py new file mode 100644 index 00000000000..ab2efe53cdd --- /dev/null +++ b/sdk/python/tests/unit/permissions/auth/client/test_http_session_manager.py @@ -0,0 +1,182 @@ +import pytest + +from feast.permissions.auth_model import NoAuthConfig +from feast.permissions.client.http_auth_requests_wrapper import ( + HttpSessionManager, + get_http_auth_requests_session, +) + + +class TestHttpSessionManager: + """Test suite for HTTP session manager with connection pooling.""" + + @pytest.fixture(autouse=True) + def reset_session(self): + """Reset the session before and after each test.""" + HttpSessionManager.close_session() + yield + HttpSessionManager.close_session() + + def test_session_creation(self): + """Test that session is created with correct configuration.""" + auth_config = NoAuthConfig() + session = get_http_auth_requests_session(auth_config) + + # Session should be created + assert session is not None + + # Session should have HTTP and HTTPS adapters + assert "http://" in session.adapters + assert "https://" in session.adapters + + # Session should have keep-alive header + assert session.headers.get("Connection") == "keep-alive" + + def test_session_reuse(self): + """Test that session is reused across multiple calls.""" + auth_config = NoAuthConfig() + + # First call creates session + session1 = get_http_auth_requests_session(auth_config) + + # Second call should return the same session + session2 = get_http_auth_requests_session(auth_config) + + # Should be the exact same object + assert session1 is session2 + + def test_session_close(self): + """Test that session can be closed and recreated.""" + auth_config = NoAuthConfig() + + # Create session + session1 = get_http_auth_requests_session(auth_config) + assert HttpSessionManager._session is not None + + # Close session + HttpSessionManager.close_session() + assert HttpSessionManager._session is None + assert HttpSessionManager._session_auth_type is None + + # New session should be created + session2 = get_http_auth_requests_session(auth_config) + assert session2 is not None + assert session2 is not session1 + + def test_thread_safety(self): + """Test that session management is thread-safe.""" + import threading + + auth_config = NoAuthConfig() + sessions = [] + errors = [] + + def get_session(): + try: + session = get_http_auth_requests_session(auth_config) + sessions.append(session) + except Exception as e: + errors.append(e) + + # Create multiple threads + threads = [threading.Thread(target=get_session) for _ in range(10)] + + # Start all threads + for t in threads: + t.start() + + # Wait for all threads to complete + for t in threads: + t.join() + + # No errors should have occurred + assert len(errors) == 0 + + # All threads should have gotten the same session (after initial creation) + assert len(sessions) == 10 + # All sessions should be the same object (due to caching) + assert all(s is sessions[0] for s in sessions) + + def test_connection_pool_configuration(self): + """Test that connection pool default configuration is correct.""" + assert HttpSessionManager.DEFAULT_POOL_CONNECTIONS == 10 + assert HttpSessionManager.DEFAULT_POOL_MAXSIZE == 50 + assert HttpSessionManager.DEFAULT_MAX_RETRIES == 3 + assert HttpSessionManager.DEFAULT_BACKOFF_FACTOR == 0.5 + assert HttpSessionManager.DEFAULT_MAX_IDLE_SECONDS == 300 # 5 minutes + + def test_custom_configuration(self): + """Test that custom configuration is applied.""" + auth_config = NoAuthConfig() + + # Create session with custom config + _session = get_http_auth_requests_session( # noqa: F841 + auth_config, + pool_maxsize=100, + max_idle_seconds=600, + max_retries=5, + ) + + # Verify custom config was applied + assert HttpSessionManager._pool_maxsize == 100 + assert HttpSessionManager._max_idle_seconds == 600 + assert HttpSessionManager._max_retries == 5 + + def test_disable_idle_timeout(self): + """Test that idle timeout can be disabled by setting to 0.""" + auth_config = NoAuthConfig() + + # Create session with idle timeout disabled + _session = get_http_auth_requests_session( # noqa: F841 + auth_config, + max_idle_seconds=0, + ) + + assert HttpSessionManager._max_idle_seconds == 0 + + def test_idle_timeout(self): + """Test that session is closed after idle timeout.""" + import time + + auth_config = NoAuthConfig() + + # Create session with very short timeout for testing + session1 = get_http_auth_requests_session( + auth_config, + max_idle_seconds=1, # 1 second + ) + + # Wait longer than timeout + time.sleep(1.5) + + # Get session again - should create new one due to idle timeout + session2 = get_http_auth_requests_session( + auth_config, + max_idle_seconds=1, + ) + + # Should be different sessions + assert session1 is not session2 + + def test_session_last_used_tracking(self): + """Test that last used time is tracked correctly.""" + import time + + auth_config = NoAuthConfig() + + # Create session + _session = get_http_auth_requests_session(auth_config) # noqa: F841 + + # Last used should be set + assert HttpSessionManager._session_last_used is not None + + first_used = HttpSessionManager._session_last_used + + # Small delay + time.sleep(0.1) + + # Get session again + _session2 = get_http_auth_requests_session(auth_config) # noqa: F841 + + # Last used should be updated + assert HttpSessionManager._session_last_used >= first_used diff --git a/sdk/python/tests/unit/permissions/auth/conftest.py b/sdk/python/tests/unit/permissions/auth/conftest.py index 1ad5a30fe7d..cf25fc970a0 100644 --- a/sdk/python/tests/unit/permissions/auth/conftest.py +++ b/sdk/python/tests/unit/permissions/auth/conftest.py @@ -1,3 +1,6 @@ +from typing import Dict +from unittest.mock import MagicMock + import pytest from kubernetes import client @@ -62,6 +65,22 @@ def oidc_config() -> OidcAuthConfig: ) +@pytest.fixture +def discovery_data() -> Dict[str, str]: + return { + "authorization_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/auth", + "token_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/token", + "jwks_uri": "https://localhost:8080/realms/master/protocol/openid-connect/certs", + } + + +@pytest.fixture +def signing_key() -> MagicMock: + key = MagicMock() + key.key = "a-key" + return key + + @pytest.fixture( scope="module", params=[ diff --git a/sdk/python/tests/unit/permissions/auth/server/mock_utils.py b/sdk/python/tests/unit/permissions/auth/server/mock_utils.py index 5bde4d4ecbc..4d62bbf4348 100644 --- a/sdk/python/tests/unit/permissions/auth/server/mock_utils.py +++ b/sdk/python/tests/unit/permissions/auth/server/mock_utils.py @@ -32,14 +32,14 @@ async def mock_oath2(self, request): } monkeypatch.setattr( "feast.permissions.client.oidc_authentication_client_manager.requests.get", - lambda url: discovery_response, + lambda url, verify=True: discovery_response, ) token_response = Mock(spec=Response) token_response.status_code = 200 token_response.json.return_value = {"access_token": "my-token"} monkeypatch.setattr( "feast.permissions.client.oidc_authentication_client_manager.requests.post", - lambda url, data, headers: token_response, + lambda url, data, headers, verify=True: token_response, ) monkeypatch.setattr( diff --git a/sdk/python/tests/unit/permissions/auth/test_token_parser.py b/sdk/python/tests/unit/permissions/auth/test_token_parser.py index 8ac0f2b6d5e..a5056393b60 100644 --- a/sdk/python/tests/unit/permissions/auth/test_token_parser.py +++ b/sdk/python/tests/unit/permissions/auth/test_token_parser.py @@ -1,16 +1,21 @@ import asyncio import os +import ssl +import time from unittest import mock from unittest.mock import MagicMock, patch import assertpy +import jwt import pytest +from pydantic import ValidationError from starlette.authentication import ( AuthenticationError, ) from feast.permissions.auth.kubernetes_token_parser import KubernetesTokenParser from feast.permissions.auth.oidc_token_parser import OidcTokenParser +from feast.permissions.auth_model import OidcAuthConfig from feast.permissions.user import User _CLIENT_ID = "test" @@ -23,17 +28,16 @@ @patch("feast.permissions.auth.oidc_token_parser.jwt.decode") @patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") def test_oidc_token_validation_success( - mock_discovery_data, mock_jwt, mock_signing_key, mock_oauth2, oidc_config + mock_discovery_data, + mock_jwt, + mock_signing_key, + mock_oauth2, + oidc_config, + discovery_data, + signing_key, ): - signing_key = MagicMock() - signing_key.key = "a-key" mock_signing_key.return_value = signing_key - - mock_discovery_data.return_value = { - "authorization_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/auth", - "token_endpoint": "https://localhost:8080/realms/master/protocol/openid-connect/token", - "jwks_uri": "https://localhost:8080/realms/master/protocol/openid-connect/certs", - } + mock_discovery_data.return_value = discovery_data user_data = { "preferred_username": "my-name", @@ -50,10 +54,341 @@ def test_oidc_token_validation_success( assertpy.assert_that(user).is_type_of(User) if isinstance(user, User): assertpy.assert_that(user.username).is_equal_to("my-name") - assertpy.assert_that(user.roles.sort()).is_equal_to(["reader", "writer"].sort()) + assertpy.assert_that(sorted(user.roles)).is_equal_to( + sorted(["reader", "writer"]) + ) assertpy.assert_that(user.has_matching_role(["reader"])).is_true() assertpy.assert_that(user.has_matching_role(["writer"])).is_true() assertpy.assert_that(user.has_matching_role(["updater"])).is_false() + assertpy.assert_that(user.groups).is_equal_to([]) + assertpy.assert_that(user.namespaces).is_equal_to([]) + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_token_missing_roles_key_returns_empty( + mock_discovery_data, + mock_jwt, + mock_signing_key, + mock_oauth2, + oidc_config, + discovery_data, + signing_key, +): + mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = discovery_data + + user_data = { + "preferred_username": "my-name", + "resource_access": {_CLIENT_ID: {}}, + } + mock_jwt.return_value = user_data + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to("my-name") + assertpy.assert_that(user.roles).is_equal_to([]) + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_token_extracts_groups( + mock_discovery_data, + mock_jwt, + mock_signing_key, + mock_oauth2, + oidc_config, + discovery_data, + signing_key, +): + mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = discovery_data + + user_data = { + "preferred_username": "my-name", + "resource_access": {_CLIENT_ID: {"roles": ["reader"]}}, + "groups": ["banking-admin", "data-engineers"], + } + mock_jwt.return_value = user_data + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.groups).is_equal_to( + ["banking-admin", "data-engineers"] + ) + assertpy.assert_that(user.has_matching_group(["banking-admin"])).is_true() + assertpy.assert_that(user.has_matching_group(["unknown-group"])).is_false() + assertpy.assert_that(user.namespaces).is_equal_to([]) + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_token_extracts_groups_and_roles( + mock_discovery_data, + mock_jwt, + mock_signing_key, + mock_oauth2, + oidc_config, + discovery_data, + signing_key, +): + mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = discovery_data + + user_data = { + "preferred_username": "my-name", + "resource_access": {_CLIENT_ID: {"roles": ["reader", "writer"]}}, + "groups": ["banking-admin", "data-engineers"], + } + mock_jwt.return_value = user_data + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to("my-name") + assertpy.assert_that(sorted(user.roles)).is_equal_to( + sorted(["reader", "writer"]) + ) + assertpy.assert_that(user.groups).is_equal_to( + ["banking-admin", "data-engineers"] + ) + assertpy.assert_that(user.has_matching_role(["reader"])).is_true() + assertpy.assert_that(user.has_matching_group(["banking-admin"])).is_true() + + +@pytest.mark.parametrize( + "token_claims, expected_username", + [ + # Human identity claims keep precedence over the application claims. + ( + { + "preferred_username": "my-name", + "upn": "my-name@example.com", + "azp": "my-client-app", + }, + "my-name", + ), + ( + {"upn": "my-name@example.com", "azp": "my-client-app"}, + "my-name@example.com", + ), + # A human claim still wins when accompanied only by application claims. + ( + { + "preferred_username": "my-name", + "appid": "my-v1-client-app", + "sub": "my-subject", + }, + "my-name", + ), + ({"upn": "my-name@example.com", "sub": "my-subject"}, "my-name@example.com"), + # Entra ID client-credentials (app-only) tokens carry no human claim. + ( + {"azp": "my-client-app", "appid": "my-v1-client-app", "sub": "my-subject"}, + "my-client-app", + ), + ({"appid": "my-v1-client-app", "sub": "my-subject"}, "my-v1-client-app"), + ({"sub": "my-subject"}, "my-subject"), + ], +) +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_token_username_claim_precedence( + mock_discovery_data, + mock_jwt, + mock_signing_key, + mock_oauth2, + token_claims, + expected_username, + oidc_config, + discovery_data, + signing_key, +): + mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = discovery_data + + mock_jwt.return_value = token_claims + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to(expected_username) + + +@pytest.mark.parametrize( + "token_claims", + [ + # No identity claim at all. + {"resource_access": {_CLIENT_ID: {"roles": ["reader"]}}}, + # Identity claims present but null are skipped, not accepted. + {"preferred_username": None, "upn": None, "azp": None, "sub": None}, + # A non-string identity claim is skipped, not accepted. + {"sub": 12345}, + ], +) +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_token_without_usable_username_claim_fails( + mock_discovery_data, + mock_jwt, + mock_signing_key, + mock_oauth2, + token_claims, + oidc_config, + discovery_data, + signing_key, +): + mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = discovery_data + + mock_jwt.return_value = token_claims + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + with pytest.raises(AuthenticationError): + asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + +@pytest.mark.parametrize( + "token_claims, expected_roles", + [ + # Entra ID app roles arrive in the top-level `roles` claim. + ({"roles": ["reader", "writer"]}, ["reader", "writer"]), + # Duplicates within the top-level claim are collapsed. + ({"roles": ["reader", "reader", "writer"]}, ["reader", "writer"]), + # Keycloak's nested claim keeps working on its own. + ({"resource_access": {_CLIENT_ID: {"roles": ["reader"]}}}, ["reader"]), + # Both shapes present: merged, in order, without duplicates. + ( + { + "resource_access": {_CLIENT_ID: {"roles": ["reader", "writer"]}}, + "roles": ["writer", "admin"], + }, + ["reader", "writer", "admin"], + ), + ], +) +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_token_merges_top_level_and_resource_access_roles( + mock_discovery_data, + mock_jwt, + mock_signing_key, + mock_oauth2, + token_claims, + expected_roles, + oidc_config, + discovery_data, + signing_key, +): + mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = discovery_data + + mock_jwt.return_value = {"preferred_username": "my-name", **token_claims} + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.roles).is_equal_to(expected_roles) + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_identity_and_roles_come_from_verified_decode( + mock_discovery_data, + mock_jwt, + mock_signing_key, + mock_oauth2, + oidc_config, + discovery_data, + signing_key, +): + """Identity and roles must come from the signature-verified decode, not the + unverified decode used only for routing.""" + mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = discovery_data + + def decode(token, key=None, *args, **kwargs): + if kwargs.get("options", {}).get("verify_signature") is False: + # Unverified decode, used only for routing; must not drive identity. + return { + "preferred_username": "spoofed-user", + "resource_access": {_CLIENT_ID: {"roles": ["spoofed-role"]}}, + } + return { + "preferred_username": "verified-user", + "resource_access": {_CLIENT_ID: {"roles": ["reader"]}}, + } + + mock_jwt.side_effect = decode + + access_token = "aaa-bbb-ccc" + token_parser = OidcTokenParser(auth_config=oidc_config) + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=access_token) + ) + + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to("verified-user") + assertpy.assert_that(user.roles).is_equal_to(["reader"]) @patch( @@ -131,14 +466,384 @@ async def mock_oath2(self, request): assertpy.assert_that(user).is_type_of(User) if isinstance(user, User): assertpy.assert_that(user.username).is_equal_to("my-name") - assertpy.assert_that(user.roles.sort()).is_equal_to( - ["reader", "writer"].sort() + assertpy.assert_that(sorted(user.roles)).is_equal_to( + sorted(["reader", "writer"]) ) assertpy.assert_that(user.has_matching_role(["reader"])).is_true() assertpy.assert_that(user.has_matching_role(["writer"])).is_true() assertpy.assert_that(user.has_matching_role(["updater"])).is_false() +# --------------------------------------------------------------------------- +# JWKS client lifecycle (one lazy client per parser) +# --------------------------------------------------------------------------- + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_jwks_client_is_lazy_and_reused_per_parser( + mock_discovery_data, + mock_jwks_client_cls, + mock_jwt, + mock_oauth2, + oidc_config, + discovery_data, +): + """One JWKS client per parser: built on the first request (not at + construction, which would move a blocking discovery fetch into server + startup), reused across requests, and scoped to the parser instance.""" + mock_discovery_data.return_value = discovery_data + mock_jwt.return_value = {"preferred_username": "my-name"} + + token_parser = OidcTokenParser(auth_config=oidc_config) + assertpy.assert_that(mock_jwks_client_cls.call_count).is_equal_to(0) + + for _ in range(3): + asyncio.run( + token_parser.user_details_from_access_token(access_token="aaa-bbb-ccc") + ) + assertpy.assert_that(mock_jwks_client_cls.call_count).is_equal_to(1) + + # A second parser must not see the first parser's client: each parser + # verifies against the JWKS of its own configured provider. + other_parser = OidcTokenParser(auth_config=oidc_config) + asyncio.run(other_parser.user_details_from_access_token(access_token="aaa-bbb-ccc")) + assertpy.assert_that(mock_jwks_client_cls.call_count).is_equal_to(2) + + +@pytest.mark.parametrize("verify_ssl", [True, False]) +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_jwks_client_ssl_context_follows_config( + mock_discovery_data, + mock_jwks_client_cls, + mock_jwt, + mock_oauth2, + verify_ssl, + discovery_data, +): + """The client is built with the discovery JWKS URL and an SSL context + matching verify_ssl: default configs must keep certificate verification + on, and verify_ssl=False must be the only way to turn it off.""" + mock_discovery_data.return_value = discovery_data + mock_jwt.return_value = {"preferred_username": "my-name"} + + token_parser = OidcTokenParser(auth_config=_oidc_config_with(verify_ssl=verify_ssl)) + asyncio.run(token_parser.user_details_from_access_token(access_token="aaa-bbb-ccc")) + + call = mock_jwks_client_cls.call_args + assertpy.assert_that(call.args[0]).is_equal_to(discovery_data["jwks_uri"]) + ssl_ctx = call.kwargs["ssl_context"] + if verify_ssl: + assertpy.assert_that(ssl_ctx.verify_mode).is_equal_to(ssl.CERT_REQUIRED) + assertpy.assert_that(ssl_ctx.check_hostname).is_true() + else: + assertpy.assert_that(ssl_ctx.verify_mode).is_equal_to(ssl.CERT_NONE) + assertpy.assert_that(ssl_ctx.check_hostname).is_false() + + +@pytest.mark.parametrize( + "overrides,expected_lifespan,expected_timeout", + [ + ({}, 300, 10), + ( + { + "jwks_cache_lifespan_seconds": 60, + "jwks_request_timeout_seconds": 2.5, + }, + 60, + 2.5, + ), + ], +) +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_jwks_client_cache_and_timeout_follow_config( + mock_discovery_data, + mock_jwks_client_cls, + mock_jwt, + mock_oauth2, + overrides, + expected_lifespan, + expected_timeout, + discovery_data, +): + """The JWK-set cache lifespan and the fetch timeout are operator-tunable: + the lifespan bounds how long a revoked key keeps validating, and the + timeout bounds how long an unresponsive IdP blocks the serving path.""" + mock_discovery_data.return_value = discovery_data + mock_jwt.return_value = {"preferred_username": "my-name"} + + token_parser = OidcTokenParser(auth_config=_oidc_config_with(**overrides)) + asyncio.run(token_parser.user_details_from_access_token(access_token="aaa-bbb-ccc")) + + kwargs = mock_jwks_client_cls.call_args.kwargs + assertpy.assert_that(kwargs["lifespan"]).is_equal_to(expected_lifespan) + assertpy.assert_that(kwargs["timeout"]).is_equal_to(expected_timeout) + + +@pytest.mark.parametrize( + "overrides", + [ + {"jwks_cache_lifespan_seconds": 0}, + {"jwks_cache_lifespan_seconds": -1}, + {"jwks_request_timeout_seconds": 0}, + {"jwks_request_timeout_seconds": -1}, + ], +) +def test_oidc_jwks_tunables_reject_non_positive_values(overrides): + """A non-positive lifespan would expire the cache immediately, silently + restoring a JWKS fetch per request; a non-positive timeout is equally + meaningless. Reject both at config load rather than at serving time.""" + with pytest.raises(ValidationError): + _oidc_config_with(**overrides) + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_jwks_client_construction_failure_is_retried( + mock_discovery_data, + mock_jwks_client_cls, + mock_jwt, + mock_oauth2, + oidc_config, + discovery_data, +): + """A failed first construction must leave the parser able to retry on + the next request: the parser is a process singleton, so caching a failed + or half-built client would wedge authentication until restart.""" + mock_discovery_data.return_value = discovery_data + mock_jwt.return_value = {"preferred_username": "my-name"} + mock_jwks_client_cls.side_effect = [RuntimeError("IdP unreachable"), MagicMock()] + + token_parser = OidcTokenParser(auth_config=oidc_config) + with pytest.raises(RuntimeError): + asyncio.run( + token_parser.user_details_from_access_token(access_token="aaa-bbb-ccc") + ) + + user = asyncio.run( + token_parser.user_details_from_access_token(access_token="aaa-bbb-ccc") + ) + assertpy.assert_that(user.username).is_equal_to("my-name") + assertpy.assert_that(mock_jwks_client_cls.call_count).is_equal_to(2) + + +# --------------------------------------------------------------------------- +# Optional audience / issuer verification (opt-in via OidcAuthConfig) +# --------------------------------------------------------------------------- + + +def _oidc_config_with(**overrides) -> OidcAuthConfig: + return OidcAuthConfig( + auth_discovery_url="https://localhost:8080/realms/master/.well-known/openid-configuration", + client_id=_CLIENT_ID, + type="oidc", + **overrides, + ) + + +@pytest.fixture(scope="module") +def rsa_keypair() -> tuple: + """A real RSA keypair, so the aud/iss tests exercise the real ``jwt.decode``.""" + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + private_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + public_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + return private_pem, public_pem + + +def _make_token(private_pem: bytes, claims: dict) -> str: + now = int(time.time()) + return jwt.encode( + {"iat": now, "exp": now + 300, **claims}, private_pem, algorithm="RS256" + ) + + +@pytest.mark.parametrize( + "audience,issuer", + [ + (None, None), + ("api://feast-server", None), + (None, "https://idp.example.com/realm"), + ("api://feast-server", "https://idp.example.com/realm"), + ], +) +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_decode_verification_options_follow_config( + mock_discovery_data, + mock_jwt, + mock_signing_key, + mock_oauth2, + audience, + issuer, + discovery_data, + signing_key, +): + """The verified decode enables aud/iss checks exactly when the config + provides expected values, and stays permissive otherwise.""" + mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = discovery_data + mock_jwt.return_value = {"preferred_username": "my-name"} + + token_parser = OidcTokenParser( + auth_config=_oidc_config_with(audience=audience, issuer=issuer) + ) + asyncio.run(token_parser.user_details_from_access_token(access_token="aaa-bbb-ccc")) + + verified_calls = [ + c + for c in mock_jwt.call_args_list + if c.kwargs.get("options", {}).get("verify_signature") is not False + ] + assertpy.assert_that(verified_calls).is_length(1) + kwargs = verified_calls[0].kwargs + assertpy.assert_that(kwargs["options"]["verify_aud"]).is_equal_to( + audience is not None + ) + assertpy.assert_that(kwargs["options"]["verify_iss"]).is_equal_to( + issuer is not None + ) + assertpy.assert_that(kwargs["audience"]).is_equal_to( + audience if audience is not None else "account" + ) + assertpy.assert_that(kwargs["issuer"]).is_equal_to(issuer) + + +@pytest.mark.parametrize( + "config_kwargs,claims,should_authenticate", + [ + # Opt-in audience: match accepted, mismatch and missing rejected. + ({"audience": "api://feast-server"}, {"aud": "api://feast-server"}, True), + ({"audience": "api://feast-server"}, {"aud": "api://another-app"}, False), + ({"audience": "api://feast-server"}, {}, False), + # Opt-in issuer: match accepted, mismatch rejected. + ( + {"issuer": "https://idp.example.com/expected"}, + {"iss": "https://idp.example.com/expected"}, + True, + ), + ( + {"issuer": "https://idp.example.com/expected"}, + {"iss": "https://idp.example.com/other"}, + False, + ), + # Default config: neither claim is verified, so a token minted for a + # different resource still authenticates (pre-existing behavior). + ({}, {"aud": "api://another-app", "iss": "https://idp.example.com/any"}, True), + ], +) +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_audience_issuer_verification_end_to_end( + mock_discovery_data, + mock_signing_key, + mock_oauth2, + config_kwargs, + claims, + should_authenticate, + discovery_data, + rsa_keypair, +): + """Real RS256-signed tokens through the real ``jwt.decode``: opt-in checks + reject mismatched aud/iss and the default stays permissive.""" + private_pem, public_pem = rsa_keypair + mock_discovery_data.return_value = discovery_data + key = MagicMock() + key.key = public_pem + mock_signing_key.return_value = key + + token = _make_token(private_pem, {"preferred_username": "my-name", **claims}) + token_parser = OidcTokenParser(auth_config=_oidc_config_with(**config_kwargs)) + + if should_authenticate: + user = asyncio.run( + token_parser.user_details_from_access_token(access_token=token) + ) + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to("my-name") + else: + with pytest.raises(AuthenticationError): + asyncio.run(token_parser.user_details_from_access_token(access_token=token)) + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_default_supports_v1_tokens_against_v2_discovery( + mock_discovery_data, + mock_signing_key, + mock_oauth2, + discovery_data, + rsa_keypair, +): + """Pins the Entra ID v1-token-against-v2-discovery setup: with no expected + audience or issuer configured, a v1.0-shaped app-only token (issuer under + ``sts.windows.net``, ``api://`` audience, ``appid`` identity) validates + against a v2.0-style discovery document, because discovery is used only to + source the JWKS signing keys. A future strict-by-default change would + break real deployments and must fail here first.""" + private_pem, public_pem = rsa_keypair + mock_discovery_data.return_value = discovery_data + key = MagicMock() + key.key = public_pem + mock_signing_key.return_value = key + + token = _make_token( + private_pem, + { + "iss": "https://sts.windows.net/11111111-2222-3333-4444-555555555555/", + "aud": "api://66666666-7777-8888-9999-000000000000", + "appid": "client-app-id", + "roles": ["reader"], + }, + ) + token_parser = OidcTokenParser(auth_config=_oidc_config_with()) + + user = asyncio.run(token_parser.user_details_from_access_token(access_token=token)) + + assertpy.assert_that(user).is_type_of(User) + if isinstance(user, User): + assertpy.assert_that(user.username).is_equal_to("client-app-id") + assertpy.assert_that(user.roles).is_equal_to(["reader"]) + + # TODO RBAC: Move role bindings to a reusable fixture @patch("feast.permissions.auth.kubernetes_token_parser.config.load_incluster_config") @patch("feast.permissions.auth.kubernetes_token_parser.jwt.decode") @@ -175,7 +880,7 @@ def test_k8s_token_validation_success( assertpy.assert_that(user).is_type_of(User) if isinstance(user, User): assertpy.assert_that(user.username).is_equal_to(f"{sa_namespace}:{sa_name}") - assertpy.assert_that(user.roles.sort()).is_equal_to(roles.sort()) + assertpy.assert_that(sorted(user.roles)).is_equal_to(sorted(roles)) for r in roles: assertpy.assert_that(user.has_matching_role([r])).is_true() assertpy.assert_that(user.has_matching_role(["foo"])).is_false() @@ -259,7 +964,86 @@ def test_k8s_inter_server_comm( assertpy.assert_that(user).is_type_of(User) if isinstance(user, User): assertpy.assert_that(user.username).is_equal_to(f"{sa_namespace}:{sa_name}") - assertpy.assert_that(user.roles.sort()).is_equal_to(roles.sort()) + assertpy.assert_that(sorted(user.roles)).is_equal_to(sorted(roles)) for r in roles: assertpy.assert_that(user.has_matching_role([r])).is_true() assertpy.assert_that(user.has_matching_role(["foo"])).is_false() + + +# --------------------------------------------------------------------------- +# OidcTokenParser — SA token routing +# --------------------------------------------------------------------------- + + +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_parser_handles_sa_token_via_token_review( + mock_discovery_data, mock_jwt_decode, oidc_config, discovery_data +): + """When a token contains kubernetes.io claim, _handle_sa_token is called (not the OIDC JWKS path).""" + mock_discovery_data.return_value = discovery_data + + mock_jwt_decode.return_value = { + "kubernetes.io": {"namespace": "feast"}, + "sub": "system:serviceaccount:feast:feast", + } + + sa_user = User( + username="system:serviceaccount:feast:feast", + roles=[], + groups=[], + namespaces=["feast"], + ) + + token_parser = OidcTokenParser(auth_config=oidc_config) + + with patch.object( + token_parser, + "_validate_k8s_sa_token_and_extract_namespace", + return_value=sa_user, + ) as mock_handle: + user = asyncio.run( + token_parser.user_details_from_access_token(access_token="sa-token") + ) + mock_handle.assert_called_once_with("sa-token") + + assertpy.assert_that(user.username).is_equal_to("system:serviceaccount:feast:feast") + assertpy.assert_that(user.namespaces).is_equal_to(["feast"]) + assertpy.assert_that(user.roles).is_equal_to([]) + assertpy.assert_that(user.groups).is_equal_to([]) + + +@patch( + "feast.permissions.auth.oidc_token_parser.OAuth2AuthorizationCodeBearer.__call__" +) +@patch("feast.permissions.auth.oidc_token_parser.PyJWKClient.get_signing_key_from_jwt") +@patch("feast.permissions.auth.oidc_token_parser.jwt.decode") +@patch("feast.permissions.oidc_service.OIDCDiscoveryService._fetch_discovery_data") +def test_oidc_parser_routes_keycloak_token_normally( + mock_discovery_data, + mock_jwt_decode, + mock_signing_key, + mock_oauth2, + oidc_config, + discovery_data, + signing_key, +): + """When a token does NOT contain kubernetes.io claim, it should follow the OIDC path.""" + mock_signing_key.return_value = signing_key + mock_discovery_data.return_value = discovery_data + + keycloak_payload = { + "preferred_username": "testuser", + "resource_access": {_CLIENT_ID: {"roles": ["reader"]}}, + "groups": ["data-team"], + } + mock_jwt_decode.return_value = keycloak_payload + + token_parser = OidcTokenParser(auth_config=oidc_config) + user = asyncio.run( + token_parser.user_details_from_access_token(access_token="keycloak-jwt") + ) + + assertpy.assert_that(user.username).is_equal_to("testuser") + assertpy.assert_that(user.roles).is_equal_to(["reader"]) + assertpy.assert_that(user.groups).is_equal_to(["data-team"]) diff --git a/sdk/python/tests/unit/permissions/conftest.py b/sdk/python/tests/unit/permissions/conftest.py index fceb9f0b197..363d9f0bdbe 100644 --- a/sdk/python/tests/unit/permissions/conftest.py +++ b/sdk/python/tests/unit/permissions/conftest.py @@ -1,3 +1,4 @@ +from typing import Iterator from unittest.mock import MagicMock, Mock import pytest @@ -11,6 +12,7 @@ from feast.permissions.policy import RoleBasedPolicy from feast.permissions.security_manager import ( SecurityManager, + no_security_manager, set_security_manager, ) from feast.permissions.user import User @@ -63,7 +65,7 @@ def users() -> list[User]: @pytest.fixture -def security_manager() -> SecurityManager: +def security_manager() -> Iterator[SecurityManager]: permissions = [] permissions.append( Permission( @@ -111,4 +113,9 @@ def security_manager() -> SecurityManager: registry.list_permissions = Mock(return_value=permissions) sm = SecurityManager(project="any", registry=registry) set_security_manager(sm) - return sm + yield sm + # `set_security_manager` populates a module-level singleton that outlives + # this fixture. Reset it, or every later test in the same process that + # serves a request (e.g. the feature server tests) runs against this mock + # security manager and is rejected with 401 Unauthorized. + no_security_manager() diff --git a/sdk/python/tests/permissions/test_groups_namespaces_auth.py b/sdk/python/tests/unit/permissions/test_groups_namespaces_auth.py similarity index 100% rename from sdk/python/tests/permissions/test_groups_namespaces_auth.py rename to sdk/python/tests/unit/permissions/test_groups_namespaces_auth.py diff --git a/sdk/python/tests/unit/permissions/test_oidc_token_passthrough.py b/sdk/python/tests/unit/permissions/test_oidc_token_passthrough.py new file mode 100644 index 00000000000..e0766ad6b0e --- /dev/null +++ b/sdk/python/tests/unit/permissions/test_oidc_token_passthrough.py @@ -0,0 +1,431 @@ +""" +Tests for OIDC client-side token passthrough feature. + +Covers: + - Config validation (OidcClientAuthConfig) + - Token manager (OidcAuthClientManager.get_token) + - Routing (RepoConfig.auth_config property) +""" + +import os +from unittest.mock import MagicMock, patch + +import pytest + +from feast.permissions.auth_model import OidcClientAuthConfig +from feast.permissions.client.oidc_authentication_client_manager import ( + OidcAuthClientManager, +) +from feast.repo_config import RepoConfig + +# --------------------------------------------------------------------------- +# Config validation +# --------------------------------------------------------------------------- + + +class TestOidcClientAuthConfigValidation: + def test_bare_oidc_valid(self): + cfg = OidcClientAuthConfig(type="oidc") + assert cfg.token_env_var is None + assert cfg.auth_discovery_url is None + assert cfg.client_id is None + + def test_token_alone_valid(self): + cfg = OidcClientAuthConfig(type="oidc", token="eyJhbGciOiJSUzI1NiJ9.test") + assert cfg.token == "eyJhbGciOiJSUzI1NiJ9.test" + + def test_token_env_var_alone_valid(self): + cfg = OidcClientAuthConfig(type="oidc", token_env_var="MY_VAR") + assert cfg.token_env_var == "MY_VAR" + + def test_token_plus_custom_env_var_invalid(self): + with pytest.raises(ValueError, match="Only one of"): + OidcClientAuthConfig( + type="oidc", + token="eyJtoken", + token_env_var="MY_VAR", + ) + + def test_client_secret_without_discovery_url_invalid(self): + with pytest.raises( + ValueError, match="Incomplete configuration for 'client_credentials'" + ): + OidcClientAuthConfig( + type="oidc", + client_secret="my-secret", # pragma: allowlist secret + ) + + def test_full_client_credentials_valid(self): + cfg = OidcClientAuthConfig( + type="oidc", + client_secret="my-secret", # pragma: allowlist secret + auth_discovery_url="https://idp.example.com/.well-known/openid-configuration", + client_id="feast-client", + ) + assert cfg.client_secret == "my-secret" # pragma: allowlist secret + + def test_full_ropg_valid(self): + cfg = OidcClientAuthConfig( + type="oidc", + username="user1", + password="pass1", # pragma: allowlist secret + client_secret="my-secret", # pragma: allowlist secret + auth_discovery_url="https://idp.example.com/.well-known/openid-configuration", + client_id="feast-client", + ) + assert cfg.username == "user1" + + def test_ropg_without_discovery_url_invalid(self): + with pytest.raises( + ValueError, match="Incomplete configuration for 'client_credentials'" + ): + OidcClientAuthConfig( + type="oidc", + username="user1", + password="pass1", # pragma: allowlist secret + client_secret="my-secret", # pragma: allowlist secret + ) + + def test_username_without_client_secret_invalid(self): + with pytest.raises( + ValueError, match="Incomplete configuration for 'client_credentials'" + ): + OidcClientAuthConfig( + type="oidc", + username="user1", + password="pass1", # pragma: allowlist secret + ) + + def test_token_plus_client_secret_invalid(self): + with pytest.raises(ValueError, match="Only one of"): + OidcClientAuthConfig( + type="oidc", + token="jwt", + client_secret="secret", # pragma: allowlist secret + auth_discovery_url="https://idp/.well-known/openid-configuration", + client_id="feast-client", + ) + + def test_token_env_var_with_discovery_url_invalid(self): + with pytest.raises( + ValueError, match="Incomplete configuration for 'client_credentials'" + ): + OidcClientAuthConfig( + type="oidc", + token_env_var="MY_VAR", + auth_discovery_url="https://idp/.well-known/openid-configuration", + client_id="feast-client", + ) + + def test_token_with_discovery_url_invalid(self): + with pytest.raises( + ValueError, match="Incomplete configuration for 'client_credentials'" + ): + OidcClientAuthConfig( + type="oidc", + token="eyJ.test", + auth_discovery_url="https://idp/.well-known/openid-configuration", + client_id="feast-client", + ) + + +# --------------------------------------------------------------------------- +# Token manager +# --------------------------------------------------------------------------- + + +class TestOidcAuthClientManagerGetToken: + def _make_manager(self, **kwargs) -> OidcAuthClientManager: + cfg = OidcClientAuthConfig(type="oidc", **kwargs) + return OidcAuthClientManager(cfg) + + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_token_returned_directly(self, mock_discovery_cls): + mgr = self._make_manager(token="my-static-jwt") + assert mgr.get_token() == "my-static-jwt" + mock_discovery_cls.assert_not_called() + + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OidcAuthClientManager._read_sa_token", + return_value=None, + ) + def test_no_token_source_raises(self, _mock_sa): + mgr = self._make_manager() + with pytest.raises(PermissionError, match="No OIDC token source configured"): + mgr.get_token() + + @patch.dict(os.environ, {"FEAST_OIDC_TOKEN": "env-jwt-value"}) + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_explicit_feast_env_var(self, mock_discovery_cls): + mgr = self._make_manager(token_env_var="FEAST_OIDC_TOKEN") + assert mgr.get_token() == "env-jwt-value" + mock_discovery_cls.assert_not_called() + + @patch.dict(os.environ, {"FEAST_OIDC_TOKEN": "fallback-jwt"}) + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_bare_config_falls_back_to_well_known_env(self, mock_discovery_cls): + mgr = self._make_manager() + assert mgr.get_token() == "fallback-jwt" + mock_discovery_cls.assert_not_called() + + @patch.dict(os.environ, {"FEAST_OIDC_TOKEN": "should-not-win"}, clear=False) + @patch("feast.permissions.client.oidc_authentication_client_manager.requests") + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_network_config_not_overridden_by_well_known_env( + self, mock_discovery_cls, mock_requests + ): + mock_discovery_instance = MagicMock() + mock_discovery_instance.get_token_url.return_value = "https://idp/token" + mock_discovery_cls.return_value = mock_discovery_instance + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"access_token": "idp-token"} + mock_requests.post.return_value = mock_response + + mgr = self._make_manager( + client_secret="secret", # pragma: allowlist secret + auth_discovery_url="https://idp/.well-known/openid-configuration", + client_id="feast-client", + ) + assert mgr.get_token() == "idp-token" + + @patch.dict(os.environ, {"CUSTOM_TOKEN_VAR": "custom-env-jwt"}) + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_custom_env_var_read(self, mock_discovery_cls): + mgr = self._make_manager(token_env_var="CUSTOM_TOKEN_VAR") + assert mgr.get_token() == "custom-env-jwt" + mock_discovery_cls.assert_not_called() + + @patch.dict(os.environ, {}, clear=False) + @patch("feast.permissions.client.oidc_authentication_client_manager.requests") + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_fallthrough_to_client_credentials(self, mock_discovery_cls, mock_requests): + os.environ.pop("FEAST_OIDC_TOKEN", None) + + mock_discovery_instance = MagicMock() + mock_discovery_instance.get_token_url.return_value = "https://idp/token" + mock_discovery_cls.return_value = mock_discovery_instance + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"access_token": "network-token"} + mock_requests.post.return_value = mock_response + + mgr = self._make_manager( + client_secret="secret", # pragma: allowlist secret + auth_discovery_url="https://idp/.well-known/openid-configuration", + client_id="feast-client", + ) + assert mgr.get_token() == "network-token" + mock_discovery_cls.assert_called_once() + + @patch.dict(os.environ, {}, clear=False) + @patch("feast.permissions.client.oidc_authentication_client_manager.requests") + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_ropg_flow(self, mock_discovery_cls, mock_requests): + os.environ.pop("FEAST_OIDC_TOKEN", None) + + mock_discovery_instance = MagicMock() + mock_discovery_instance.get_token_url.return_value = "https://idp/token" + mock_discovery_cls.return_value = mock_discovery_instance + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"access_token": "ropg-token"} + mock_requests.post.return_value = mock_response + + mgr = self._make_manager( + username="user1", + password="pass1", # pragma: allowlist secret + client_secret="secret", # pragma: allowlist secret + auth_discovery_url="https://idp/.well-known/openid-configuration", + client_id="feast-client", + ) + assert mgr.get_token() == "ropg-token" + + call_args = mock_requests.post.call_args + assert call_args[1]["data"]["grant_type"] == "password" + assert call_args[1]["data"]["username"] == "user1" + + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_token_takes_priority_over_env_var(self, mock_discovery_cls): + with patch.dict(os.environ, {"FEAST_OIDC_TOKEN": "env-token"}): + mgr = self._make_manager(token="config-token") + assert mgr.get_token() == "config-token" + mock_discovery_cls.assert_not_called() + + @patch.dict(os.environ, {}, clear=False) + def test_configured_env_var_missing_raises(self): + os.environ.pop("MY_CUSTOM_VAR", None) + mgr = self._make_manager(token_env_var="MY_CUSTOM_VAR") + with pytest.raises(PermissionError, match="token_env_var='MY_CUSTOM_VAR'"): + mgr.get_token() + + @patch.dict(os.environ, {"FEAST_OIDC_TOKEN": "stale-token"}, clear=False) + def test_configured_env_var_missing_does_not_fall_through(self): + os.environ.pop("MY_CUSTOM_VAR", None) + mgr = self._make_manager(token_env_var="MY_CUSTOM_VAR") + with pytest.raises(PermissionError, match="token_env_var='MY_CUSTOM_VAR'"): + mgr.get_token() + + # --- SA token file fallback tests --- + + @patch.dict(os.environ, {}, clear=False) + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OidcAuthClientManager._read_sa_token", + return_value="sa-jwt-from-file", + ) + def test_sa_token_file_read(self, _mock_sa): + os.environ.pop("FEAST_OIDC_TOKEN", None) + mgr = self._make_manager() + assert mgr.get_token() == "sa-jwt-from-file" + + @patch.dict(os.environ, {}, clear=False) + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OidcAuthClientManager._read_sa_token", + return_value=None, + ) + def test_sa_token_file_missing_raises(self, _mock_sa): + os.environ.pop("FEAST_OIDC_TOKEN", None) + mgr = self._make_manager() + with pytest.raises(PermissionError, match="No OIDC token source configured"): + mgr.get_token() + + @patch.dict(os.environ, {"FEAST_OIDC_TOKEN": "env-token"}, clear=False) + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OidcAuthClientManager._read_sa_token", + return_value="sa-jwt-from-file", + ) + def test_feast_env_takes_priority_over_sa_token(self, _mock_sa): + mgr = self._make_manager() + assert mgr.get_token() == "env-token" + _mock_sa.assert_not_called() + + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OidcAuthClientManager._read_sa_token", + return_value="sa-jwt-from-file", + ) + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_explicit_token_skips_sa_file(self, mock_discovery_cls, _mock_sa): + mgr = self._make_manager(token="my-explicit-token") + assert mgr.get_token() == "my-explicit-token" + _mock_sa.assert_not_called() + + @patch.dict(os.environ, {}, clear=False) + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OidcAuthClientManager._read_sa_token", + return_value="sa-jwt-from-file", + ) + @patch("feast.permissions.client.oidc_authentication_client_manager.requests") + @patch( + "feast.permissions.client.oidc_authentication_client_manager.OIDCDiscoveryService" + ) + def test_client_secret_skips_sa_file( + self, mock_discovery_cls, mock_requests, _mock_sa + ): + os.environ.pop("FEAST_OIDC_TOKEN", None) + + mock_discovery_instance = MagicMock() + mock_discovery_instance.get_token_url.return_value = "https://idp/token" + mock_discovery_cls.return_value = mock_discovery_instance + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"access_token": "network-token"} + mock_requests.post.return_value = mock_response + + mgr = self._make_manager( + client_secret="secret", # pragma: allowlist secret + auth_discovery_url="https://idp/.well-known/openid-configuration", + client_id="feast-client", + ) + assert mgr.get_token() == "network-token" + _mock_sa.assert_not_called() + + +# --------------------------------------------------------------------------- +# Routing (RepoConfig.auth_config property) +# --------------------------------------------------------------------------- + + +class TestOidcClientRouting: + def _make_repo_config(self, auth_dict: dict) -> RepoConfig: + return RepoConfig( + project="test_project", + registry="data/registry.db", + provider="local", + auth=auth_dict, + ) + + def test_bare_oidc_routes_to_client(self): + rc = self._make_repo_config({"type": "oidc"}) + assert isinstance(rc.auth_config, OidcClientAuthConfig) + + def test_token_routes_to_client(self): + rc = self._make_repo_config({"type": "oidc", "token": "x"}) + assert isinstance(rc.auth_config, OidcClientAuthConfig) + assert rc.auth_config.token == "x" + + def test_token_env_var_routes_to_client(self): + rc = self._make_repo_config({"type": "oidc", "token_env_var": "MY_VAR"}) + assert isinstance(rc.auth_config, OidcClientAuthConfig) + assert rc.auth_config.token_env_var == "MY_VAR" + + def test_server_config_routes_to_oidc_auth_config(self): + from feast.permissions.auth_model import OidcAuthConfig + + rc = self._make_repo_config( + { + "type": "oidc", + "auth_discovery_url": "https://idp/.well-known/openid-configuration", + "client_id": "feast-server", + } + ) + assert isinstance(rc.auth_config, OidcAuthConfig) + assert type(rc.auth_config) is OidcAuthConfig + + def test_ropg_routes_to_client(self): + rc = self._make_repo_config( + { + "type": "oidc", + "auth_discovery_url": "https://idp/.well-known/openid-configuration", + "client_id": "feast-client", + "client_secret": "secret", # pragma: allowlist secret + "username": "user1", + "password": "pass1", # pragma: allowlist secret + } + ) + assert isinstance(rc.auth_config, OidcClientAuthConfig) + + def test_incomplete_ropg_routes_to_client_with_actionable_error(self): + with pytest.raises( + ValueError, match="Incomplete configuration for 'client_credentials'" + ): + self._make_repo_config( + { + "type": "oidc", + "auth_discovery_url": "https://idp/.well-known/openid-configuration", + "client_id": "feast-client", + "username": "user1", + "password": "pass1", # pragma: allowlist secret + } + ).auth_config diff --git a/sdk/python/tests/unit/permissions/test_policy.py b/sdk/python/tests/unit/permissions/test_policy.py index 4e78282d4f8..beccaa1d577 100644 --- a/sdk/python/tests/unit/permissions/test_policy.py +++ b/sdk/python/tests/unit/permissions/test_policy.py @@ -42,3 +42,10 @@ def test_role_based_policy(users, required_roles, username, result): assertpy.assert_that(explain).is_equal_to("") else: assertpy.assert_that(len(explain)).is_greater_than(0) + + +def test_policy_eq_cross_type_returns_false(): + """A policy compared to a different type returns False, not TypeError.""" + policy = RoleBasedPolicy(roles=["reader"]) + assert (policy == "not a policy") is False + assert (policy == 42) is False diff --git a/sdk/python/tests/unit/permissions/test_security_manager.py b/sdk/python/tests/unit/permissions/test_security_manager.py index ee0ec9e079a..34d8e4962e9 100644 --- a/sdk/python/tests/unit/permissions/test_security_manager.py +++ b/sdk/python/tests/unit/permissions/test_security_manager.py @@ -15,7 +15,7 @@ @pytest.mark.parametrize( "username, requested_actions, allowed, allowed_single, raise_error_in_assert, raise_error_in_permit, intra_communication_flag", [ - (None, [], False, [False, False], [True, True], False, False), + (None, [], False, [False, False], [True, True], True, False), (None, [], True, [True, True], [False, False], False, True), ( "r", @@ -42,7 +42,7 @@ False, [False, False], [True, True], - False, + True, False, ), ("r", [AuthzedAction.UPDATE], True, [True, True], [False, False], False, True), @@ -52,7 +52,7 @@ False, [False, False], [True, True], - False, + True, False, ), ( @@ -116,7 +116,7 @@ False, [False, False], [True, True], - True, + False, False, ), ( @@ -134,7 +134,7 @@ False, [False, True], [True, False], - True, + False, False, ), ( @@ -152,7 +152,7 @@ False, [False, False], [True, True], - True, + False, False, ), ( diff --git a/sdk/python/tests/unit/test_aggregation_ops.py b/sdk/python/tests/unit/test_aggregation_ops.py new file mode 100644 index 00000000000..fd4f147f5d8 --- /dev/null +++ b/sdk/python/tests/unit/test_aggregation_ops.py @@ -0,0 +1,137 @@ +from datetime import timedelta + +import pandas as pd +import pytest + +from feast.aggregation import Aggregation, aggregation_specs_to_agg_ops +from feast.aggregation.tiling.base import get_ir_metadata_for_aggregation + + +class DummyAggregation: + def __init__(self, *, function: str, column: str, time_window=None, name: str = ""): + self.function = function + self.column = column + self.time_window = time_window + self.name = name + + +def test_aggregation_specs_to_agg_ops_success(): + agg_specs = [ + DummyAggregation(function="sum", column="trips"), + DummyAggregation(function="mean", column="fare"), + ] + + agg_ops = aggregation_specs_to_agg_ops( + agg_specs, + time_window_unsupported_error_message="Time window aggregation is not supported.", + ) + + assert agg_ops == { + "sum_trips": ("sum", "trips"), + "mean_fare": ("mean", "fare"), + } + + +@pytest.mark.parametrize( + "error_message", + [ + "Time window aggregation is not supported in online serving.", + "Time window aggregation is not supported in the local compute engine.", + ], +) +def test_aggregation_specs_to_agg_ops_time_window_unsupported(error_message: str): + agg_specs = [DummyAggregation(function="sum", column="trips", time_window=1)] + + with pytest.raises(ValueError, match=error_message): + aggregation_specs_to_agg_ops( + agg_specs, + time_window_unsupported_error_message=error_message, + ) + + +def test_aggregation_specs_to_agg_ops_custom_name(): + agg_specs = [ + DummyAggregation( + function="sum", + column="seconds_watched", + name="sum_seconds_watched_per_ad_1d", + ), + ] + + agg_ops = aggregation_specs_to_agg_ops( + agg_specs, + time_window_unsupported_error_message="Time window aggregation is not supported.", + ) + + assert agg_ops == { + "sum_seconds_watched_per_ad_1d": ("sum", "seconds_watched"), + } + + +def test_aggregation_specs_to_agg_ops_mixed_names(): + agg_specs = [ + DummyAggregation(function="sum", column="trips", name="total_trips"), + DummyAggregation(function="mean", column="fare"), + ] + + agg_ops = aggregation_specs_to_agg_ops( + agg_specs, + time_window_unsupported_error_message="Time window aggregation is not supported.", + ) + + assert agg_ops == { + "total_trips": ("sum", "trips"), + "mean_fare": ("mean", "fare"), + } + + +def test_aggregation_round_trip_with_name(): + agg = Aggregation( + column="seconds_watched", + function="sum", + time_window=timedelta(days=1), + name="sum_seconds_watched_per_ad_1d", + ) + proto = agg.to_proto() + assert proto.name == "sum_seconds_watched_per_ad_1d" + + restored = Aggregation.from_proto(proto) + assert restored.name == "sum_seconds_watched_per_ad_1d" + assert restored == agg + + +def test_count_distinct_agg_ops(): + """aggregation_specs_to_agg_ops maps count_distinct to the nunique pandas function.""" + agg_specs = [DummyAggregation(function="count_distinct", column="item_id")] + + agg_ops = aggregation_specs_to_agg_ops( + agg_specs, + time_window_unsupported_error_message="no windows", + ) + + assert agg_ops == {"count_distinct_item_id": ("nunique", "item_id")} + + +def test_count_distinct_result(): + """count_distinct via nunique returns the number of unique values per group.""" + from feast.infra.compute_engines.backends.pandas_backend import PandasBackend + + agg_specs = [DummyAggregation(function="count_distinct", column="item_id")] + agg_ops = aggregation_specs_to_agg_ops( + agg_specs, + time_window_unsupported_error_message="no windows", + ) + + df = pd.DataFrame({"user": ["A", "A", "B"], "item_id": [1, 2, 1]}) + result = PandasBackend().groupby_agg(df, ["user"], agg_ops) + result = result.set_index("user") + + assert result.loc["A", "count_distinct_item_id"] == 2 + assert result.loc["B", "count_distinct_item_id"] == 1 + + +def test_count_distinct_tiling_raises(): + """get_ir_metadata_for_aggregation raises ValueError for count_distinct.""" + agg = Aggregation(column="item_id", function="count_distinct") + with pytest.raises(ValueError, match="count_distinct does not support tiling"): + get_ir_metadata_for_aggregation(agg, "count_distinct_item_id") diff --git a/sdk/python/tests/unit/test_arrow_error_decorator.py b/sdk/python/tests/unit/test_arrow_error_decorator.py index fc350d34c0a..af753ad8f00 100644 --- a/sdk/python/tests/unit/test_arrow_error_decorator.py +++ b/sdk/python/tests/unit/test_arrow_error_decorator.py @@ -1,8 +1,15 @@ +from unittest.mock import MagicMock, patch + import pyarrow.flight as fl import pytest +from pydantic import ValidationError -from feast.arrow_error_handler import arrow_client_error_handling_decorator -from feast.errors import PermissionNotFoundException +from feast.arrow_error_handler import ( + _get_exception_data, + arrow_client_error_handling_decorator, +) +from feast.errors import FeatureViewNotFoundException, PermissionNotFoundException +from feast.infra.offline_stores.remote import RemoteOfflineStoreConfig permissionError = PermissionNotFoundException("dummy_name", "dummy_project") @@ -31,3 +38,194 @@ def test_rest_error_handling_with_feast_exception(error, expected_raised_error): match=str(expected_raised_error), ): decorated_method(error) + + +class TestArrowClientRetry: + @patch("feast.arrow_error_handler.time.sleep") + def test_retries_on_flight_unavailable_error(self, mock_sleep): + client = MagicMock() + client._connection_retries = 3 + call_count = 0 + + @arrow_client_error_handling_decorator + def flaky_method(self_arg): + nonlocal call_count + call_count += 1 + if call_count < 3: + raise fl.FlightUnavailableError("Connection refused") + return "success" + + result = flaky_method(client) + assert result == "success" + assert call_count == 3 + assert mock_sleep.call_count == 2 + + @patch("feast.arrow_error_handler.time.sleep") + def test_raises_after_max_retries_exhausted(self, mock_sleep): + client = MagicMock() + client._connection_retries = 3 + + @arrow_client_error_handling_decorator + def always_unavailable(self_arg): + raise fl.FlightUnavailableError("Connection refused") + + with pytest.raises(fl.FlightUnavailableError, match="Connection refused"): + always_unavailable(client) + assert mock_sleep.call_count == 3 + + @patch("feast.arrow_error_handler.time.sleep") + def test_respects_connection_retries_from_client(self, mock_sleep): + client = MagicMock() + client._connection_retries = 1 + call_count = 0 + + @arrow_client_error_handling_decorator + def method_on_client(self_arg): + nonlocal call_count + call_count += 1 + raise fl.FlightUnavailableError("Connection refused") + + with pytest.raises(fl.FlightUnavailableError): + method_on_client(client) + + assert call_count == 2 # 1 initial + 1 retry + assert mock_sleep.call_count == 1 + + @patch("feast.arrow_error_handler.time.sleep") + def test_no_retry_on_non_transient_errors(self, mock_sleep): + client = MagicMock() + client._connection_retries = 3 + call_count = 0 + + @arrow_client_error_handling_decorator + def method_with_error(self_arg): + nonlocal call_count + call_count += 1 + raise fl.FlightError("Permanent error") + + with pytest.raises(fl.FlightError, match="Permanent error"): + method_with_error(client) + + assert call_count == 1 + mock_sleep.assert_not_called() + + @patch("feast.arrow_error_handler.time.sleep") + def test_exponential_backoff_timing(self, mock_sleep): + client = MagicMock() + client._connection_retries = 3 + + @arrow_client_error_handling_decorator + def always_unavailable(self_arg): + raise fl.FlightUnavailableError("Connection refused") + + with pytest.raises(fl.FlightUnavailableError): + always_unavailable(client) + + wait_times = [call.args[0] for call in mock_sleep.call_args_list] + assert wait_times == [0.5, 1.0, 2.0] + + @patch("feast.arrow_error_handler.time.sleep") + def test_zero_retries_disables_retry(self, mock_sleep): + client = MagicMock() + client._connection_retries = 0 + call_count = 0 + + @arrow_client_error_handling_decorator + def method_on_client(self_arg): + nonlocal call_count + call_count += 1 + raise fl.FlightUnavailableError("Connection refused") + + with pytest.raises(fl.FlightUnavailableError): + method_on_client(client) + + assert call_count == 1 + mock_sleep.assert_not_called() + + @patch("feast.arrow_error_handler.time.sleep") + def test_no_retry_for_standalone_stream_functions(self, mock_sleep): + """Standalone functions (write_table, read_all) where args[0] is a + writer/reader should not retry since broken streams can't be reused.""" + writer = MagicMock(spec=[]) # no _connection_retries attribute + call_count = 0 + + @arrow_client_error_handling_decorator + def write_table(w): + nonlocal call_count + call_count += 1 + raise fl.FlightUnavailableError("stream broken") + + with pytest.raises(fl.FlightUnavailableError, match="stream broken"): + write_table(writer) + + assert call_count == 1 + mock_sleep.assert_not_called() + + @patch("feast.arrow_error_handler.time.sleep") + def test_negative_connection_retries_treated_as_zero(self, mock_sleep): + """Negative _connection_retries must not skip function execution.""" + client = MagicMock() + client._connection_retries = -1 + call_count = 0 + + @arrow_client_error_handling_decorator + def method_on_client(self_arg): + nonlocal call_count + call_count += 1 + return "ok" + + result = method_on_client(client) + assert result == "ok" + assert call_count == 1 + mock_sleep.assert_not_called() + + def test_config_rejects_negative_connection_retries(self): + with pytest.raises(ValidationError): + RemoteOfflineStoreConfig(host="localhost", connection_retries=-1) + + +class TestGetExceptionData: + def test_non_string_input_returns_empty(self): + assert _get_exception_data(12345) == "" + assert _get_exception_data(None) == "" + assert _get_exception_data(b"bytes") == "" + + def test_no_flight_error_prefix_returns_empty(self): + assert _get_exception_data("some random error") == "" + + def test_flight_error_prefix_without_json_returns_empty(self): + assert _get_exception_data("Flight error: no json here") == "" + + def test_extracts_json_from_flight_error(self): + fv_error = FeatureViewNotFoundException("my_view", "my_project") + error_str = f"Flight error: {fv_error.to_error_detail()}" + result = _get_exception_data(error_str) + assert '"class": "FeatureViewNotFoundException"' in result + assert '"module": "feast.errors"' in result + + def test_extracts_json_with_trailing_text(self): + fv_error = FeatureViewNotFoundException("my_view", "my_project") + error_str = ( + f"Flight error: {fv_error.to_error_detail()}. " + "gRPC client debug context: some extra info" + ) + result = _get_exception_data(error_str) + assert '"class": "FeatureViewNotFoundException"' in result + assert '"module": "feast.errors"' in result + + def test_extracts_json_with_grpc_debug_context_containing_braces(self): + fv_error = FeatureViewNotFoundException("my_view", "my_project") + error_str = ( + f"Flight error: {fv_error.to_error_detail()}. " + "gRPC client debug context: UNKNOWN:Error received from peer " + 'ipv4:127.0.0.1:59930 {grpc_message:"Flight error: ...", ' + 'grpc_status:2, created_time:"2026-03-17T17:32:07"}' + ) + result = _get_exception_data(error_str) + assert '"class": "FeatureViewNotFoundException"' in result + assert '"module": "feast.errors"' in result + from feast.errors import FeastError + + reconstructed = FeastError.from_error_detail(result) + assert reconstructed is not None + assert "my_view" in str(reconstructed) diff --git a/sdk/python/tests/unit/test_data_sources.py b/sdk/python/tests/unit/test_data_sources.py index e6375fc3a00..e2a928f7bb9 100644 --- a/sdk/python/tests/unit/test_data_sources.py +++ b/sdk/python/tests/unit/test_data_sources.py @@ -306,3 +306,30 @@ def test_redshift_fully_qualified_table_name(source_kwargs, expected_name): ) assert redshift_source.redshift_options.fully_qualified_table_name == expected_name + + +def test_data_source_eq_cross_type_returns_false(): + """A DataSource compared to a different type returns ``False``, never ``TypeError``. + + Regression: ``__eq__`` used to ``raise TypeError("Comparisons should only involve + class objects.")`` on a cross-type comparison, so changing a feature view's + source type and re-``apply()``-ing over an existing registry crashed. Comparing + objects of different types must return ``False``, matching ``PushSource.__eq__``. + """ + file_source = FileSource(name="src", path="/tmp/x.parquet", timestamp_field="ts") + snowflake_source = SnowflakeSource( + name="src", database="D", schema="S", table="T", timestamp_field="ts" + ) + + # Cross-type comparison is False in both directions, not a raise. + assert (file_source == snowflake_source) is False + assert (snowflake_source == file_source) is False + + # Comparison against a non-DataSource operand is also False, not a raise. + assert (file_source == "not a data source") is False + assert (file_source == 42) is False + + # Same-type equality still holds for two independently-built equal instances. + assert file_source == FileSource( + name="src", path="/tmp/x.parquet", timestamp_field="ts" + ) diff --git a/sdk/python/tests/unit/test_demos.py b/sdk/python/tests/unit/test_demos.py new file mode 100644 index 00000000000..52d61b353a2 --- /dev/null +++ b/sdk/python/tests/unit/test_demos.py @@ -0,0 +1,360 @@ +"""Unit tests for feast.demos — demo notebook generation.""" + +import json +import pathlib +import textwrap + +import pytest + +from feast.demos import ( + _extract_store_info, + _find_feature_store_yamls, + _is_operator_client, + copy_demo_notebooks, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_LOCAL_YAML = textwrap.dedent("""\ + project: local_proj + provider: local + registry: + path: data/registry.db + registry_type: file + offline_store: + type: file + online_store: + type: sqlite + path: data/online_store.db + entity_key_serialization_version: 3 +""") + +_OPERATOR_YAML = textwrap.dedent("""\ + project: remote_proj + provider: local + offline_store: + host: feast-offline.svc.cluster.local + port: 80 + type: remote + online_store: + path: http://feast-online.svc.cluster.local:80 + type: remote + registry: + path: feast-registry.svc.cluster.local:80 + registry_type: remote + auth: + type: oidc + entity_key_serialization_version: 3 +""") + +_VECTOR_YAML = textwrap.dedent("""\ + project: vec_proj + provider: local + registry: + path: data/registry.db + registry_type: file + offline_store: + type: file + online_store: + type: pgvector + vector_enabled: true + embedding_dim: 512 + entity_key_serialization_version: 3 +""") + + +def _write(tmp_path: pathlib.Path, rel: str, content: str) -> pathlib.Path: + p = tmp_path / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(content) + return p + + +def _sections(nb: dict) -> list[str]: + """Return the first line of every markdown cell that starts with #.""" + return [ + "".join(cell["source"]).splitlines()[0] + for cell in nb["cells"] + if cell["cell_type"] == "markdown" and "".join(cell["source"]).startswith("#") + ] + + +# --------------------------------------------------------------------------- +# _extract_store_info +# --------------------------------------------------------------------------- + + +class TestExtractStoreInfo: + def test_local_defaults(self): + info = _extract_store_info({}) + assert info["project"] == "my_feast_project" + assert info["provider"] == "local" + assert info["online_store_type"] == "sqlite" + assert info["offline_store_type"] == "file" + assert info["registry_type"] == "file" + assert info["auth_type"] == "no_auth" + assert info["vector_enabled"] is False + assert info["embedding_dim"] is None + + def test_operator_client_yaml(self): + config = { + "project": "sample", + "provider": "local", + "offline_store": {"type": "remote", "host": "h", "port": 80}, + "online_store": {"type": "remote", "path": "http://h:80"}, + "registry": {"registry_type": "remote", "path": "h:80"}, + "auth": {"type": "oidc"}, + } + info = _extract_store_info(config) + assert info["registry_type"] == "remote" + assert info["online_store_type"] == "remote" + assert info["offline_store_type"] == "remote" + assert info["auth_type"] == "oidc" + + def test_registry_type_key_takes_priority_over_type(self): + config = {"registry": {"registry_type": "remote", "type": "file"}} + info = _extract_store_info(config) + assert info["registry_type"] == "remote" + + def test_registry_type_fallback_to_type(self): + config = {"registry": {"type": "snowflake"}} + info = _extract_store_info(config) + assert info["registry_type"] == "snowflake" + + def test_string_registry_path_stays_file(self): + info = _extract_store_info({"registry": "data/registry.db"}) + assert info["registry_type"] == "file" + + def test_vector_enabled(self): + config = { + "online_store": { + "type": "pgvector", + "vector_enabled": True, + "embedding_dim": 512, + } + } + info = _extract_store_info(config) + assert info["vector_enabled"] is True + assert info["embedding_dim"] == 512 + + def test_online_store_as_string(self): + info = _extract_store_info({"online_store": "Redis"}) + assert info["online_store_type"] == "redis" + + def test_offline_store_as_string(self): + info = _extract_store_info({"offline_store": "BigQuery"}) + assert info["offline_store_type"] == "bigquery" + + +# --------------------------------------------------------------------------- +# _is_operator_client +# --------------------------------------------------------------------------- + + +class TestIsOperatorClient: + def _info(self, registry="remote", online="remote", offline="remote"): + return { + "registry_type": registry, + "online_store_type": online, + "offline_store_type": offline, + } + + def test_all_remote_is_operator(self): + assert _is_operator_client(self._info()) is True + + def test_local_registry_not_operator(self): + assert _is_operator_client(self._info(registry="file")) is False + + def test_local_online_not_operator(self): + assert _is_operator_client(self._info(online="sqlite")) is False + + def test_local_offline_not_operator(self): + assert _is_operator_client(self._info(offline="file")) is False + + +# --------------------------------------------------------------------------- +# _find_feature_store_yamls +# --------------------------------------------------------------------------- + + +class TestFindFeatureStoreYamls: + def test_direct(self, tmp_path): + _write(tmp_path, "feature_store.yaml", "project: p") + found = _find_feature_store_yamls(tmp_path) + assert len(found) == 1 + assert found[0].name == "feature_store.yaml" + + def test_feast_config_root(self, tmp_path): + _write(tmp_path, "feast-config/feature_store.yaml", "project: p") + found = _find_feature_store_yamls(tmp_path) + assert len(found) == 1 + + def test_feast_config_multiple_files(self, tmp_path): + _write(tmp_path, "feast-config/rag.yaml", "project: rag") + _write(tmp_path, "feast-config/rec.yml", "project: rec") + found = _find_feature_store_yamls(tmp_path) + assert len(found) == 2 + + def test_feast_config_any_extension(self, tmp_path): + _write(tmp_path, "feast-config/project_a.yaml", "project: a") + _write(tmp_path, "feast-config/project_b", "project: b") + found = _find_feature_store_yamls(tmp_path) + assert len(found) == 2 + + def test_feast_config_ignores_directories(self, tmp_path): + _write(tmp_path, "feast-config/valid.yaml", "project: p") + (tmp_path / "feast-config" / "subdir").mkdir() + found = _find_feature_store_yamls(tmp_path) + assert len(found) == 1 + + def test_multiple_sources(self, tmp_path): + _write(tmp_path, "feature_store.yaml", "project: root") + _write(tmp_path, "feast-config/a.yaml", "project: a") + _write(tmp_path, "feast-config/b", "project: b") + found = _find_feature_store_yamls(tmp_path) + assert len(found) == 3 + + def test_no_yaml_returns_empty(self, tmp_path): + assert _find_feature_store_yamls(tmp_path) == [] + + +# --------------------------------------------------------------------------- +# copy_demo_notebooks — file generation +# --------------------------------------------------------------------------- + + +class TestCopyDemoNotebooks: + def test_generates_notebooks(self, tmp_path): + _write(tmp_path, "feature_store.yaml", _LOCAL_YAML) + out = tmp_path / "out" + copy_demo_notebooks(output_dir=str(out), repo_path=str(tmp_path)) + + assert (out / "local_proj" / "01_feature_store_overview.ipynb").exists() + assert (out / "local_proj" / "02_historical_features_training.ipynb").exists() + assert (out / "local_proj" / "03_online_features_serving.ipynb").exists() + + def test_valid_notebook_json(self, tmp_path): + _write(tmp_path, "feature_store.yaml", _LOCAL_YAML) + out = tmp_path / "out" + copy_demo_notebooks(output_dir=str(out), repo_path=str(tmp_path)) + + nb = json.loads( + (out / "local_proj" / "01_feature_store_overview.ipynb").read_text() + ) + assert nb["nbformat"] == 4 + assert isinstance(nb["cells"], list) + + def test_raises_if_output_exists(self, tmp_path): + _write(tmp_path, "feature_store.yaml", _LOCAL_YAML) + out = tmp_path / "out" + copy_demo_notebooks(output_dir=str(out), repo_path=str(tmp_path)) + + with pytest.raises(FileExistsError): + copy_demo_notebooks(output_dir=str(out), repo_path=str(tmp_path)) + + def test_overwrite_flag(self, tmp_path): + _write(tmp_path, "feature_store.yaml", _LOCAL_YAML) + out = tmp_path / "out" + copy_demo_notebooks(output_dir=str(out), repo_path=str(tmp_path)) + copy_demo_notebooks( + output_dir=str(out), repo_path=str(tmp_path), overwrite=True + ) + + def test_no_yaml_returns_without_creating_output(self, tmp_path): + out = tmp_path / "out" + copy_demo_notebooks(output_dir=str(out), repo_path=str(tmp_path)) + assert not out.exists() + + def test_multiple_projects(self, tmp_path): + _write( + tmp_path, + "feast-config/proj_a.yaml", + "project: proj_a\nprovider: local\n", + ) + _write( + tmp_path, + "feast-config/proj_b.yaml", + "project: proj_b\nprovider: local\n", + ) + out = tmp_path / "out" + copy_demo_notebooks(output_dir=str(out), repo_path=str(tmp_path)) + assert (out / "proj_a").is_dir() + assert (out / "proj_b").is_dir() + + +# --------------------------------------------------------------------------- +# Notebook content — section headings +# --------------------------------------------------------------------------- + + +class TestNotebookContent: + def _notebooks(self, tmp_path, yaml_content): + _write(tmp_path, "feature_store.yaml", yaml_content) + out = tmp_path / "out" + copy_demo_notebooks(output_dir=str(out), repo_path=str(tmp_path)) + project = _extract_store_info(__import__("yaml").safe_load(yaml_content))[ + "project" + ] + return { + name: json.loads((out / project / name).read_text()) + for name in [ + "01_feature_store_overview.ipynb", + "02_historical_features_training.ipynb", + "03_online_features_serving.ipynb", + ] + } + + def test_local_overview_has_apply_section(self, tmp_path): + nbs = self._notebooks(tmp_path, _LOCAL_YAML) + sections = _sections(nbs["01_feature_store_overview.ipynb"]) + assert any("Apply Feature Definitions" in s for s in sections) + + def test_remote_overview_has_registry_sync(self, tmp_path): + nbs = self._notebooks(tmp_path, _OPERATOR_YAML) + sections = _sections(nbs["01_feature_store_overview.ipynb"]) + assert any("Registry Sync" in s for s in sections) + + def test_historical_no_apply_section(self, tmp_path): + nbs = self._notebooks(tmp_path, _LOCAL_YAML) + sections = _sections(nbs["02_historical_features_training.ipynb"]) + assert not any("Apply" in s for s in sections) + + def test_online_no_apply_section(self, tmp_path): + nbs = self._notebooks(tmp_path, _LOCAL_YAML) + sections = _sections(nbs["03_online_features_serving.ipynb"]) + assert not any("Apply" in s for s in sections) + + def test_vector_notebook_has_vector_section(self, tmp_path): + nbs = self._notebooks(tmp_path, _VECTOR_YAML) + sections = _sections(nbs["03_online_features_serving.ipynb"]) + assert any("Vector" in s for s in sections) + + def test_non_vector_notebook_no_vector_section(self, tmp_path): + nbs = self._notebooks(tmp_path, _LOCAL_YAML) + sections = _sections(nbs["03_online_features_serving.ipynb"]) + assert not any("Vector" in s for s in sections) + + def test_auth_section_present_for_oidc(self, tmp_path): + nbs = self._notebooks(tmp_path, _OPERATOR_YAML) + sections = _sections(nbs["03_online_features_serving.ipynb"]) + assert any("Authentication" in s for s in sections) + + def test_auth_section_absent_for_no_auth(self, tmp_path): + nbs = self._notebooks(tmp_path, _LOCAL_YAML) + sections = _sections(nbs["03_online_features_serving.ipynb"]) + assert not any("Authentication" in s for s in sections) + + def test_path_setup_cell_contains_yaml_path(self, tmp_path): + _write(tmp_path, "feature_store.yaml", _LOCAL_YAML) + out = tmp_path / "out" + copy_demo_notebooks(output_dir=str(out), repo_path=str(tmp_path)) + nb = json.loads( + (out / "local_proj" / "01_feature_store_overview.ipynb").read_text() + ) + code_sources = [ + "".join(c["source"]) for c in nb["cells"] if c["cell_type"] == "code" + ] + yaml_path = str((tmp_path / "feature_store.yaml").resolve()) + assert any(yaml_path in src for src in code_sources) diff --git a/sdk/python/tests/unit/test_doc_embedder.py b/sdk/python/tests/unit/test_doc_embedder.py new file mode 100644 index 00000000000..10f72c82d02 --- /dev/null +++ b/sdk/python/tests/unit/test_doc_embedder.py @@ -0,0 +1,657 @@ +import os +from unittest.mock import MagicMock, Mock, patch + +import numpy as np +import pandas as pd +import pytest + +from feast.chunker import BaseChunker, ChunkingConfig, TextChunker +from feast.doc_embedder import ( + DocEmbedder, + SchemaTransformFn, + default_schema_transform_fn, +) +from feast.embedder import BaseEmbedder, EmbeddingConfig, MultiModalEmbedder + + +@pytest.fixture +def sample_documents_df(): + """Small DataFrame with 3 documents of varying lengths.""" + return pd.DataFrame( + { + "id": ["doc1", "doc2", "doc3"], + "text": [ + " ".join([f"word{i}" for i in range(150)]), + " ".join([f"term{i}" for i in range(200)]), + " ".join([f"item{i}" for i in range(50)]), + ], + "type": ["article", "paper", "note"], + } + ) + + +class TestTextChunker: + def test_basic_chunking(self): + """Given ~200 words, returns chunks with correct keys and count.""" + chunker = TextChunker() + text = " ".join([f"word{i}" for i in range(200)]) + + chunks = chunker.load_parse_and_chunk( + source=text, source_id="doc1", source_column="text" + ) + + assert len(chunks) > 0 + for chunk in chunks: + assert "chunk_id" in chunk + assert "original_id" in chunk + assert "text" in chunk + assert "chunk_index" in chunk + assert chunk["original_id"] == "doc1" + + assert len(chunks) == 3 + assert chunks[0]["chunk_id"] == "doc1_0" + assert chunks[1]["chunk_id"] == "doc1_1" + assert chunks[2]["chunk_id"] == "doc1_2" + assert chunks[0]["chunk_index"] == 0 + assert chunks[2]["chunk_index"] == 2 + + def test_overlap(self): + """Consecutive chunks share exactly chunk_overlap words.""" + config = ChunkingConfig( + chunk_size=10, chunk_overlap=3, min_chunk_size=3, max_chunk_chars=None + ) + chunker = TextChunker(config=config) + text = " ".join([f"w{i}" for i in range(20)]) + + chunks = chunker.load_parse_and_chunk( + source=text, source_id="doc1", source_column="text" + ) + assert len(chunks) >= 2 + + words_0 = chunks[0]["text"].split() + words_1 = chunks[1]["text"].split() + + assert words_0[-3:] == words_1[:3] + + def test_min_chunk_size_filters_small_trailing(self): + """Trailing chunk smaller than min_chunk_size is dropped.""" + config = ChunkingConfig( + chunk_size=10, chunk_overlap=0, min_chunk_size=5, max_chunk_chars=None + ) + chunker = TextChunker(config=config) + text = " ".join([f"w{i}" for i in range(13)]) + + chunks = chunker.load_parse_and_chunk( + source=text, source_id="doc1", source_column="text" + ) + + assert len(chunks) == 1 + + def test_max_chunk_chars_truncation(self): + """Chunks exceeding max_chunk_chars are truncated.""" + config = ChunkingConfig( + chunk_size=100, chunk_overlap=0, min_chunk_size=1, max_chunk_chars=50 + ) + chunker = TextChunker(config=config) + text = " ".join([f"longword{i}" for i in range(200)]) + + chunks = chunker.load_parse_and_chunk( + source=text, source_id="doc1", source_column="text" + ) + + assert len(chunks) > 0 + for chunk in chunks: + assert len(chunk["text"]) <= 50 + + def test_empty_text_returns_no_chunks(self): + """Empty string produces zero chunks.""" + chunker = TextChunker() + + chunks = chunker.load_parse_and_chunk( + source="", source_id="doc1", source_column="text" + ) + + assert chunks == [] + + def test_chunk_dataframe(self, sample_documents_df): + """Chunking a multi-row DataFrame returns a flattened DataFrame.""" + chunker = TextChunker() + + result = chunker.chunk_dataframe( + df=sample_documents_df, + id_column="id", + source_column="text", + type_column="type", + ) + + assert isinstance(result, pd.DataFrame) + assert "chunk_id" in result.columns + assert "original_id" in result.columns + assert "text" in result.columns + assert "chunk_index" in result.columns + assert len(result) >= len(sample_documents_df) + assert set(result["original_id"].unique()) == {"doc1", "doc2", "doc3"} + + +def test_base_chunker_cannot_instantiate(): + """BaseChunker is abstract and cannot be instantiated directly.""" + with pytest.raises(TypeError): + BaseChunker() + + +def test_base_embedder_cannot_instantiate(): + """BaseEmbedder is abstract and cannot be instantiated directly.""" + with pytest.raises(TypeError): + BaseEmbedder() + + +class TestMultiModalEmbedder: + def test_supported_modalities(self): + """After init, supported_modalities returns text and image.""" + embedder = MultiModalEmbedder() + modalities = embedder.supported_modalities + + assert "text" in modalities + assert "image" in modalities + + def test_unsupported_modality_raises(self): + """Calling embed with unsupported modality raises ValueError.""" + embedder = MultiModalEmbedder() + + with pytest.raises(ValueError, match="Unsupported modality: 'video'"): + embedder.embed(["test"], "video") + + def test_text_embed_mocked(self): + """Text embedding calls encode with correct batch_size and show_progress.""" + embedder = MultiModalEmbedder() + + mock_model = MagicMock() + mock_model.encode.return_value = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]) + embedder._text_model = mock_model + + result = embedder.embed(["hello", "world"], "text") + + mock_model.encode.assert_called_once_with( + ["hello", "world"], + batch_size=64, + show_progress_bar=True, + ) + assert result.shape == (2, 3) + + def test_text_embed_custom_config(self): + """Text embedding respects custom EmbeddingConfig values.""" + config = EmbeddingConfig(batch_size=16, show_progress=False) + embedder = MultiModalEmbedder(config=config) + + mock_model = MagicMock() + mock_model.encode.return_value = np.array([[0.1, 0.2]]) + embedder._text_model = mock_model + + embedder.embed(["hello"], "text") + + mock_model.encode.assert_called_once_with( + ["hello"], + batch_size=16, + show_progress_bar=False, + ) + + def test_lazy_loading(self): + """Models are None until first accessed.""" + embedder = MultiModalEmbedder() + + assert embedder._text_model is None + assert embedder._image_model is None + assert embedder._image_processor is None + + def test_get_embedding_dim_text(self): + """get_embedding_dim for text delegates to text_model.""" + embedder = MultiModalEmbedder() + mock_model = MagicMock() + mock_model.get_sentence_embedding_dimension.return_value = 384 + embedder._text_model = mock_model + + assert embedder.get_embedding_dim("text") == 384 + mock_model.get_sentence_embedding_dimension.assert_called_once() + + def test_get_embedding_dim_unknown_modality(self): + """get_embedding_dim returns None for unrecognized modalities.""" + embedder = MultiModalEmbedder() + + assert embedder.get_embedding_dim("audio") is None + + def test_embed_dataframe(self): + """embed_dataframe adds embedding columns via column_mapping.""" + embedder = MultiModalEmbedder() + embedder.embed = Mock(return_value=np.array([[0.1, 0.2], [0.3, 0.4]])) + + df = pd.DataFrame({"text": ["hello", "world"], "id": [1, 2]}) + result = embedder.embed_dataframe( + df, column_mapping={"text": ("text", "text_embedding")} + ) + + assert "text_embedding" in result.columns + assert len(result) == 2 + embedder.embed.assert_called_once_with(["hello", "world"], "text") + + +def test_schema_transform_fn_protocol_check(): + """A matching function is recognized as SchemaTransformFn.""" + + def my_fn(df: pd.DataFrame) -> pd.DataFrame: + return df + + assert isinstance(my_fn, SchemaTransformFn) + + +def test_default_schema_transform_fn_output(): + """default_schema_transform_fn transforms columns correctly.""" + input_df = pd.DataFrame( + { + "chunk_id": ["c1", "c2"], + "text": ["hello", "world"], + "text_embedding": [[0.1, 0.2], [0.3, 0.4]], + "original_id": ["doc1", "doc1"], + } + ) + + result = default_schema_transform_fn(input_df) + + assert list(result.columns) == [ + "passage_id", + "text", + "embedding", + "event_timestamp", + "source_id", + ] + assert result["passage_id"].tolist() == ["c1", "c2"] + assert result["text"].tolist() == ["hello", "world"] + assert result["embedding"].tolist() == [[0.1, 0.2], [0.3, 0.4]] + assert result["source_id"].tolist() == ["doc1", "doc1"] + assert len(result["event_timestamp"]) == 2 + + +class TestDocEmbedder: + @patch("feast.repo_operations.apply_total") + @patch("feast.repo_config.load_repo_config") + def test_init_no_feature_view(self, mock_load_config, mock_apply_total, tmp_path): + """With create_feature_view=False, generate_repo_file is not called.""" + mock_chunker = MagicMock(spec=BaseChunker) + mock_embedder = MagicMock(spec=BaseEmbedder) + + with patch("feast.doc_embedder.generate_repo_file") as mock_gen: + doc_embedder = DocEmbedder( + repo_path=str(tmp_path), + feature_view_name="test_view", + chunker=mock_chunker, + embedder=mock_embedder, + create_feature_view=False, + ) + + mock_gen.assert_not_called() + assert doc_embedder.repo_path == str(tmp_path) + assert doc_embedder.feature_view_name == "test_view" + assert doc_embedder.chunker is mock_chunker + assert doc_embedder.embedder is mock_embedder + assert doc_embedder.schema_transform_fn is default_schema_transform_fn + + @patch("feast.repo_operations.apply_total") + @patch("feast.repo_config.load_repo_config") + def test_init_creates_feature_view( + self, mock_load_config, mock_apply_total, tmp_path + ): + """With create_feature_view=True, generate_repo_file and apply_repo are called.""" + mock_chunker = MagicMock(spec=BaseChunker) + mock_embedder = MagicMock(spec=BaseEmbedder) + mock_embedder.get_embedding_dim.return_value = None + + with patch("feast.doc_embedder.generate_repo_file") as mock_gen: + DocEmbedder( + repo_path=str(tmp_path), + feature_view_name="test_view", + chunker=mock_chunker, + embedder=mock_embedder, + create_feature_view=True, + ) + + mock_gen.assert_called_once_with( + repo_path=str(tmp_path), + feature_view_name="test_view", + vector_length=384, + ) + mock_load_config.assert_called_once() + mock_apply_total.assert_called_once() + + @patch("feast.repo_operations.apply_total") + @patch("feast.repo_config.load_repo_config") + def test_init_creates_feature_view_with_explicit_vector_length( + self, mock_load_config, mock_apply_total, tmp_path + ): + """Explicit vector_length overrides auto-detection.""" + mock_chunker = MagicMock(spec=BaseChunker) + mock_embedder = MagicMock(spec=BaseEmbedder) + + with patch("feast.doc_embedder.generate_repo_file") as mock_gen: + DocEmbedder( + repo_path=str(tmp_path), + feature_view_name="test_view", + chunker=mock_chunker, + embedder=mock_embedder, + create_feature_view=True, + vector_length=256, + ) + + mock_gen.assert_called_once_with( + repo_path=str(tmp_path), + feature_view_name="test_view", + vector_length=256, + ) + + @patch("feast.repo_operations.apply_total") + @patch("feast.repo_config.load_repo_config") + def test_init_creates_feature_view_auto_detects_vector_length( + self, mock_load_config, mock_apply_total, tmp_path + ): + """Auto-detected vector_length from embedder is used when not explicitly set.""" + mock_chunker = MagicMock(spec=BaseChunker) + mock_embedder = MagicMock(spec=BaseEmbedder) + mock_embedder.get_embedding_dim.return_value = 512 + + with patch("feast.doc_embedder.generate_repo_file") as mock_gen: + DocEmbedder( + repo_path=str(tmp_path), + feature_view_name="test_view", + chunker=mock_chunker, + embedder=mock_embedder, + create_feature_view=True, + ) + + mock_gen.assert_called_once_with( + repo_path=str(tmp_path), + feature_view_name="test_view", + vector_length=512, + ) + mock_embedder.get_embedding_dim.assert_called_once_with("text") + + @patch("feast.repo_operations.apply_total") + @patch("feast.repo_config.load_repo_config") + @patch("feast.feature_store.FeatureStore") + def test_embed_documents_full_chain( + self, mock_fs_cls, mock_load_config, mock_apply_total, tmp_path + ): + """embed_documents wires chunk -> embed -> schema_transform -> save correctly.""" + mock_chunker = MagicMock(spec=BaseChunker) + chunked_df = pd.DataFrame( + { + "chunk_id": ["doc1_0", "doc1_1"], + "original_id": ["doc1", "doc1"], + "text": ["chunk text 1", "chunk text 2"], + "chunk_index": [0, 1], + } + ) + mock_chunker.chunk_dataframe.return_value = chunked_df + + mock_embedder = MagicMock(spec=BaseEmbedder) + embedded_df = chunked_df.copy() + embedded_df["text_embedding"] = [[0.1, 0.2], [0.3, 0.4]] + mock_embedder.embed_dataframe.return_value = embedded_df + + transformed_df = pd.DataFrame( + { + "passage_id": ["doc1_0", "doc1_1"], + "text": ["chunk text 1", "chunk text 2"], + "embedding": [[0.1, 0.2], [0.3, 0.4]], + "source_id": ["doc1", "doc1"], + } + ) + logical_fn_tracker = MagicMock() + + def mock_logical_fn(df: pd.DataFrame) -> pd.DataFrame: + logical_fn_tracker(df) + return transformed_df + + doc_embedder = DocEmbedder( + repo_path=str(tmp_path), + chunker=mock_chunker, + embedder=mock_embedder, + schema_transform_fn=mock_logical_fn, + create_feature_view=False, + ) + + input_df = pd.DataFrame( + { + "id": ["doc1"], + "text": ["some long document text"], + "type": ["article"], + } + ) + + result = doc_embedder.embed_documents( + documents=input_df, + id_column="id", + source_column="text", + type_column="type", + ) + + mock_chunker.chunk_dataframe.assert_called_once() + chunk_kwargs = mock_chunker.chunk_dataframe.call_args[1] + assert chunk_kwargs["id_column"] == "id" + assert chunk_kwargs["source_column"] == "text" + assert chunk_kwargs["type_column"] == "type" + + mock_embedder.embed_dataframe.assert_called_once() + embed_args = mock_embedder.embed_dataframe.call_args + pd.testing.assert_frame_equal(embed_args[0][0], chunked_df) + assert embed_args[1]["column_mapping"] == {"text": ("text", "text_embedding")} + + logical_fn_tracker.assert_called_once() + pd.testing.assert_frame_equal(logical_fn_tracker.call_args[0][0], embedded_df) + + mock_fs_cls.return_value.write_to_online_store.assert_called_once() + save_kwargs = mock_fs_cls.return_value.write_to_online_store.call_args[1] + assert save_kwargs["feature_view_name"] == "text_feature_view" + + pd.testing.assert_frame_equal(result, transformed_df) + + @patch("feast.repo_operations.apply_total") + @patch("feast.repo_config.load_repo_config") + @patch("feast.feature_store.FeatureStore") + def test_embed_documents_default_column_mapping( + self, mock_fs_cls, mock_load_config, mock_apply_total, tmp_path + ): + """When column_mapping=None, default mapping is used.""" + mock_chunker = MagicMock(spec=BaseChunker) + mock_chunker.chunk_dataframe.return_value = pd.DataFrame( + { + "chunk_id": ["c1"], + "original_id": ["d1"], + "text": ["t"], + "chunk_index": [0], + } + ) + + mock_embedder = MagicMock(spec=BaseEmbedder) + mock_embedder.embed_dataframe.return_value = pd.DataFrame( + { + "chunk_id": ["c1"], + "original_id": ["d1"], + "text": ["t"], + "chunk_index": [0], + "text_embedding": [[0.1]], + } + ) + + doc_embedder = DocEmbedder( + repo_path=str(tmp_path), + chunker=mock_chunker, + embedder=mock_embedder, + create_feature_view=False, + ) + + doc_embedder.embed_documents( + documents=pd.DataFrame({"id": ["d1"], "text": ["t"]}), + id_column="id", + source_column="text", + column_mapping=None, + ) + + embed_call = mock_embedder.embed_dataframe.call_args + assert embed_call[1]["column_mapping"] == {"text": ("text", "text_embedding")} + + @patch("feast.repo_operations.apply_total") + @patch("feast.repo_config.load_repo_config") + @patch("feast.feature_store.FeatureStore") + def test_embed_documents_custom_column_mapping( + self, mock_fs_cls, mock_load_config, mock_apply_total, tmp_path + ): + """Custom column_mapping is forwarded to the embedder.""" + mock_chunker = MagicMock(spec=BaseChunker) + mock_chunker.chunk_dataframe.return_value = pd.DataFrame( + { + "chunk_id": ["c1"], + "original_id": ["d1"], + "content": ["t"], + "chunk_index": [0], + } + ) + + mock_embedder = MagicMock(spec=BaseEmbedder) + mock_embedder.embed_dataframe.return_value = pd.DataFrame( + { + "chunk_id": ["c1"], + "original_id": ["d1"], + "content": ["t"], + "chunk_index": [0], + "content_embedding": [[0.1]], + } + ) + + def mock_logical_fn(df: pd.DataFrame) -> pd.DataFrame: + return pd.DataFrame({"passage_id": ["c1"], "embedding": [[0.1]]}) + + doc_embedder = DocEmbedder( + repo_path=str(tmp_path), + chunker=mock_chunker, + embedder=mock_embedder, + schema_transform_fn=mock_logical_fn, + create_feature_view=False, + ) + + custom_mapping = ("text", "content_embedding") + doc_embedder.embed_documents( + documents=pd.DataFrame({"id": ["d1"], "content": ["t"]}), + id_column="id", + source_column="content", + column_mapping=custom_mapping, + ) + + embed_call = mock_embedder.embed_dataframe.call_args + assert embed_call[1]["column_mapping"] == { + "content": ("text", "content_embedding") + } + + @patch("feast.repo_operations.apply_total") + @patch("feast.repo_config.load_repo_config") + def test_apply_repo_restores_cwd( + self, mock_load_config, mock_apply_total, tmp_path + ): + """apply_repo restores os.getcwd() even if apply_total changes it.""" + mock_chunker = MagicMock(spec=BaseChunker) + mock_embedder = MagicMock(spec=BaseEmbedder) + + doc_embedder = DocEmbedder( + repo_path=str(tmp_path), + chunker=mock_chunker, + embedder=mock_embedder, + create_feature_view=False, + ) + + cwd_before = os.getcwd() + mock_apply_total.side_effect = lambda **kwargs: os.chdir(str(tmp_path)) + + doc_embedder.apply_repo() + + assert os.getcwd() == cwd_before + + @patch("feast.repo_operations.apply_total") + @patch("feast.repo_config.load_repo_config") + @patch("feast.feature_store.FeatureStore") + def test_save_to_online_store( + self, mock_fs_cls, mock_load_config, mock_apply_total, tmp_path + ): + """save_to_online_store creates FeatureStore and writes data.""" + mock_chunker = MagicMock(spec=BaseChunker) + mock_embedder = MagicMock(spec=BaseEmbedder) + + doc_embedder = DocEmbedder( + repo_path=str(tmp_path), + chunker=mock_chunker, + embedder=mock_embedder, + create_feature_view=False, + ) + + test_df = pd.DataFrame({"col": [1, 2]}) + doc_embedder.save_to_online_store(df=test_df, feature_view_name="my_view") + + mock_fs_cls.assert_called_with(repo_path=str(tmp_path)) + mock_fs_cls.return_value.write_to_online_store.assert_called_once_with( + feature_view_name="my_view", + df=test_df, + ) + + +@patch("feast.repo_operations.apply_total") +@patch("feast.repo_config.load_repo_config") +@patch("feast.feature_store.FeatureStore") +def test_end_to_end_pipeline(mock_fs_cls, mock_load_config, mock_apply_total, tmp_path): + """Full pipeline: real TextChunker + mocked embedder + default schema transform.""" + chunker = TextChunker( + config=ChunkingConfig( + chunk_size=10, + chunk_overlap=2, + min_chunk_size=3, + max_chunk_chars=None, + ) + ) + + mock_embedder = MagicMock(spec=BaseEmbedder) + + def fake_embed_dataframe(df, column_mapping): + df = df.copy() + for _src_col, (_modality, out_col) in column_mapping.items(): + df[out_col] = [[0.1, 0.2, 0.3]] * len(df) + return df + + mock_embedder.embed_dataframe.side_effect = fake_embed_dataframe + + doc_embedder = DocEmbedder( + repo_path=str(tmp_path), + chunker=chunker, + embedder=mock_embedder, + schema_transform_fn=default_schema_transform_fn, + create_feature_view=False, + ) + + documents = pd.DataFrame( + { + "id": ["doc1", "doc2"], + "text": [ + " ".join([f"word{i}" for i in range(30)]), + " ".join([f"term{i}" for i in range(25)]), + ], + } + ) + + result = doc_embedder.embed_documents( + documents=documents, + id_column="id", + source_column="text", + ) + + assert "passage_id" in result.columns + assert "text" in result.columns + assert "embedding" in result.columns + assert "event_timestamp" in result.columns + assert "source_id" in result.columns + assert len(result) > 2 + assert set(result["source_id"].unique()) == {"doc1", "doc2"} + mock_fs_cls.return_value.write_to_online_store.assert_called_once() diff --git a/sdk/python/tests/unit/test_entity.py b/sdk/python/tests/unit/test_entity.py index b36f363a6ff..f9192309891 100644 --- a/sdk/python/tests/unit/test_entity.py +++ b/sdk/python/tests/unit/test_entity.py @@ -88,3 +88,10 @@ def test_entity_with_value_type_no_warning(): warnings.simplefilter("error") entity = Entity(name="my-entity", value_type=ValueType.STRING) assert entity.value_type == ValueType.STRING + + +def test_entity_eq_cross_type_returns_false(): + """Entity compared to a different type returns False, not TypeError.""" + entity = Entity(name="my-entity", value_type=ValueType.STRING) + assert (entity == "not an entity") is False + assert (entity == 42) is False diff --git a/sdk/python/tests/unit/test_eq_cross_type.py b/sdk/python/tests/unit/test_eq_cross_type.py new file mode 100644 index 00000000000..7a87dd68f10 --- /dev/null +++ b/sdk/python/tests/unit/test_eq_cross_type.py @@ -0,0 +1,122 @@ +"""Cross-type ``__eq__`` regression tests (see #6636). + +Comparing two Feast registry objects of different types must return ``False`` +instead of raising ``TypeError``. This exercises the shared +``if not isinstance(other, X): return False`` guard across the importable core +object model in one place; the per-type tests for ``DataSource``, ``Entity``, +``LabelView``, and ``RoleBasedPolicy`` live in their own modules. + +Most contrib offline sources (athena, couchbase, mssql, oracle, postgres, ray, +trino) and the optional-dependency transformations are intentionally omitted: +the unit environment does not install their drivers, so they cannot be imported +here. Their ``__eq__`` follows the identical, mechanical pattern. SparkSource +is the exception — its module imports without pyspark, and its ``__eq__`` +accesses spark-only attributes after the shared ``DataSource`` base check, so +it gets a dedicated cross-subclass test below. +""" + +from datetime import timedelta + +import pytest + +from feast import Entity, FeatureService, FeatureView, Project +from feast.aggregation import Aggregation +from feast.data_format import ParquetFormat, ProtoFormat +from feast.data_source import KafkaSource, KinesisSource, RequestSource +from feast.feature import Feature +from feast.field import Field +from feast.infra.offline_stores.bigquery_source import BigQuerySource +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.offline_stores.redshift_source import RedshiftSource +from feast.infra.offline_stores.snowflake_source import SnowflakeSource +from feast.permissions.permission import Permission +from feast.permissions.policy import ( + CombinedGroupNamespacePolicy, + GroupBasedPolicy, + NamespaceBasedPolicy, + RoleBasedPolicy, +) +from feast.types import Int64 +from feast.value_type import ValueType + + +def _instances(): + """One instance of each importable type touched by the __eq__ sweep.""" + return { + "Entity": Entity(name="e"), + "Feature": Feature(name="f", dtype=ValueType.INT64), + "ParquetFormat": ParquetFormat(), + "ProtoFormat": ProtoFormat("com.example.Msg"), + "Project": Project(name="proj"), + "Aggregation": Aggregation(column="c", function="sum"), + "Permission": Permission(name="perm"), + "RoleBasedPolicy": RoleBasedPolicy(roles=["reader"]), + "GroupBasedPolicy": GroupBasedPolicy(groups=["g"]), + "NamespaceBasedPolicy": NamespaceBasedPolicy(namespaces=["n"]), + "CombinedGroupNamespacePolicy": CombinedGroupNamespacePolicy( + groups=["g"], namespaces=["n"] + ), + "FileSource": FileSource( + name="fs", path="/tmp/x.parquet", timestamp_field="ts" + ), + "BigQuerySource": BigQuerySource( + name="bq", table="p.d.t", timestamp_field="ts" + ), + "RedshiftSource": RedshiftSource(name="rs", table="t", timestamp_field="ts"), + "SnowflakeSource": SnowflakeSource( + name="sf", database="D", schema="S", table="T", timestamp_field="ts" + ), + "KafkaSource": KafkaSource( + name="ks", + kafka_bootstrap_servers="s", + message_format=ProtoFormat("cp"), + topic="t", + timestamp_field="ts", + ), + "KinesisSource": KinesisSource( + name="kn", + region="r", + record_format=ProtoFormat("cp"), + stream_name="s", + timestamp_field="ts", + ), + "RequestSource": RequestSource( + name="rq", schema=[Field(name="f", dtype=Int64)] + ), + "FeatureView": FeatureView(name="fv", ttl=timedelta(days=1)), + "FeatureService": FeatureService(name="svc", features=[]), + } + + +_CASES = list(_instances().items()) + + +@pytest.mark.parametrize("name,obj", _CASES, ids=[n for n, _ in _CASES]) +def test_eq_cross_type_returns_false(name, obj): + # A different-typed operand must compare False, never raise (#6636). + assert (obj == object()) is False + assert (obj == "not a feast object") is False + # __ne__ derives from __eq__, so it must be the inverse. + assert (obj != object()) is True + # The isinstance guard must not break same-object equality. + assert (obj == obj) is True + + +def test_spark_source_vs_file_source_eq(): + # Reported on #6636: swapping a FeatureView's source from FileSource to + # SparkSource. The two sources share the base DataSource fields, so + # SparkSource.__eq__ used to pass the base check and then raise + # AttributeError on FileSource's missing `table`. Both directions must + # simply compare False. + spark_source = pytest.importorskip( + "feast.infra.offline_stores.contrib.spark_offline_store.spark_source" + ) + SparkSource = spark_source.SparkSource + + spark = SparkSource(name="src", table="t", timestamp_field="ts") + file = FileSource(name="src", path="/tmp/x.parquet", timestamp_field="ts") + + assert (spark == file) is False + assert (file == spark) is False + assert (spark == object()) is False + assert (spark == SparkSource(name="src", table="t", timestamp_field="ts")) is True diff --git a/sdk/python/tests/unit/test_feature_resolution_cache.py b/sdk/python/tests/unit/test_feature_resolution_cache.py new file mode 100644 index 00000000000..f77a4c1e0ac --- /dev/null +++ b/sdk/python/tests/unit/test_feature_resolution_cache.py @@ -0,0 +1,171 @@ +"""Tests for the feature resolution cache in feast.utils._get_cached_request_context.""" + +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock, patch + +import pytest + +import feast.utils as utils + + +@pytest.fixture(autouse=True) +def _clear_cache(): + """Reset the module-level cache before each test.""" + with utils._feature_resolution_cache_lock: + utils._feature_resolution_cache.clear() + utils._feature_resolution_registry_ts = None + yield + with utils._feature_resolution_cache_lock: + utils._feature_resolution_cache.clear() + utils._feature_resolution_registry_ts = None + + +def _make_registry(ts: datetime, ttl_seconds: int = 600, cache_valid: bool = True): + reg = MagicMock() + reg.cached_registry_proto_created = ts + reg.cached_registry_proto_ttl = timedelta(seconds=ttl_seconds) + reg.is_cache_valid.return_value = cache_valid + return reg + + +def _make_context(): + """Return a plausible fake context tuple matching _get_online_request_context output.""" + return ( + ["fv:feat1"], # feature_refs + [], # requested_on_demand_feature_views + {}, # entity_name_to_join_key_map + {}, # entity_type_map + {"user_id"}, # join_keys_set + [("fv_table", ["feat1"])], # grouped_refs + {"feat1"}, # requested_result_row_names + set(), # needed_request_data + False, # entityless_case + ) + + +class TestFeatureResolutionCache: + @patch("feast.utils._get_online_request_context") + def test_second_call_uses_cache(self, mock_ctx): + """Identical feature requests within the same registry generation + should only call _get_online_request_context once.""" + mock_ctx.return_value = _make_context() + now = datetime.now(tz=timezone.utc) + registry = _make_registry(now, ttl_seconds=600) + features = ["fv:feat1"] + + utils._get_cached_request_context(registry, "proj", features, False) + utils._get_cached_request_context(registry, "proj", features, False) + + mock_ctx.assert_called_once() + + @patch("feast.utils._get_online_request_context") + def test_different_features_are_separate_cache_entries(self, mock_ctx): + """Different feature lists should be cached independently.""" + mock_ctx.return_value = _make_context() + now = datetime.now(tz=timezone.utc) + registry = _make_registry(now, ttl_seconds=600) + + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + utils._get_cached_request_context(registry, "proj", ["fv:feat2"], False) + + assert mock_ctx.call_count == 2 + + @patch("feast.utils._get_online_request_context") + def test_cache_cleared_on_registry_refresh(self, mock_ctx): + """When registry.cached_registry_proto_created changes, the cache + must be cleared and _get_online_request_context called again.""" + mock_ctx.return_value = _make_context() + t1 = datetime.now(tz=timezone.utc) + t2 = t1 + timedelta(seconds=30) + + registry = _make_registry(t1, ttl_seconds=600) + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + assert mock_ctx.call_count == 1 + + registry.cached_registry_proto_created = t2 + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + assert mock_ctx.call_count == 2 + + @patch("feast.utils._get_online_request_context") + def test_cache_bypassed_when_registry_ttl_expired(self, mock_ctx): + """If the registry's own cache has expired (TTL), the feature + resolution cache must NOT be used even if the timestamp matches.""" + mock_ctx.return_value = _make_context() + past = datetime.now(tz=timezone.utc) - timedelta(seconds=120) + registry = _make_registry(past, ttl_seconds=60, cache_valid=False) + + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + + assert mock_ctx.call_count == 2 + + @patch("feast.utils._get_online_request_context") + def test_cache_works_with_infinite_ttl(self, mock_ctx): + """TTL of 0 means infinite cache — should cache normally.""" + mock_ctx.return_value = _make_context() + past = datetime.now(tz=timezone.utc) - timedelta(hours=24) + registry = _make_registry(past, ttl_seconds=0) + + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + + mock_ctx.assert_called_once() + + @patch("feast.utils._get_online_request_context") + def test_no_cache_without_registry_timestamp(self, mock_ctx): + """Registries without cached_registry_proto_created should bypass + caching entirely.""" + mock_ctx.return_value = _make_context() + registry = MagicMock(spec=[]) + + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + + assert mock_ctx.call_count == 2 + + @patch("feast.utils._get_online_request_context") + def test_returned_result_row_names_is_mutable_copy(self, mock_ctx): + """requested_result_row_names is stored as frozenset in cache; + each call to _prepare_entities_to_read_from_online_store should + get a fresh mutable copy so mutations don't leak between requests.""" + mock_ctx.return_value = _make_context() + now = datetime.now(tz=timezone.utc) + registry = _make_registry(now, ttl_seconds=600) + + r1 = utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + r2 = utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + + result_row_names_1 = r1[6] + result_row_names_2 = r2[6] + assert isinstance(result_row_names_1, frozenset) + assert result_row_names_1 == result_row_names_2 + + @patch("feast.utils._get_online_request_context") + def test_feature_service_cached_separately(self, mock_ctx): + """FeatureService objects should be cached by name.""" + mock_ctx.return_value = _make_context() + now = datetime.now(tz=timezone.utc) + registry = _make_registry(now, ttl_seconds=600) + + fs = MagicMock() + fs.name = "credit_scoring_v1" + + utils._get_cached_request_context(registry, "proj", fs, False) + utils._get_cached_request_context(registry, "proj", fs, False) + + mock_ctx.assert_called_once() + + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + assert mock_ctx.call_count == 2 + + @patch("feast.utils._get_online_request_context") + def test_full_feature_names_flag_is_cache_key(self, mock_ctx): + """full_feature_names=True vs False should be different cache entries.""" + mock_ctx.return_value = _make_context() + now = datetime.now(tz=timezone.utc) + registry = _make_registry(now, ttl_seconds=600) + + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], False) + utils._get_cached_request_context(registry, "proj", ["fv:feat1"], True) + + assert mock_ctx.call_count == 2 diff --git a/sdk/python/tests/unit/test_feature_server.py b/sdk/python/tests/unit/test_feature_server.py index 21c01d61765..b9f2afd4a61 100644 --- a/sdk/python/tests/unit/test_feature_server.py +++ b/sdk/python/tests/unit/test_feature_server.py @@ -1,5 +1,22 @@ +# Copyright 2025 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import json -from unittest.mock import AsyncMock, MagicMock +import sys +import time +from collections import Counter +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi.testclient import TestClient @@ -8,7 +25,13 @@ from feast.errors import PushSourceNotFoundException from feast.feature_server import get_app from feast.online_response import OnlineResponse -from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse +from feast.protos.feast.serving.ServingService_pb2 import ( + FeatureList, + FieldStatus, + GetOnlineFeaturesResponse, + GetOnlineFeaturesResponseMetadata, +) +from feast.protos.feast.types.Value_pb2 import Value from feast.utils import _utc_now from tests.foo_provider import FooProvider from tests.utils.cli_repo_creator import CliRunner, get_example_repo @@ -207,3 +230,537 @@ def test_materialize_request_model(): assert req2.disable_event_timestamp is False assert req2.start_ts == "2021-01-01T00:00:00" assert req2.end_ts == "2021-01-02T00:00:00" + + +def _enable_offline_batching_config( + fs, enabled: bool = True, batch_size: int = 1, batch_interval_seconds: int = 60 +): + """ + Attach a minimal feature_server.offline_push_batching config + to a mocked FeatureStore. + """ + if not hasattr(fs, "config") or fs.config is None: + fs.config = SimpleNamespace() + + if not hasattr(fs.config, "feature_server") or fs.config.feature_server is None: + fs.config.feature_server = SimpleNamespace() + + fs.config.feature_server.offline_push_batching_enabled = enabled + fs.config.feature_server.offline_push_batching_batch_size = batch_size + fs.config.feature_server.offline_push_batching_batch_interval_seconds = ( + batch_interval_seconds + ) + + +def push_body_many(push_mode=PushMode.ONLINE, count: int = 2, id_start: int = 100): + """Build a push body with multiple entities.""" + driver_ids = list(range(id_start, id_start + count)) + lats = [float(i) for i in driver_ids] + longs = [str(lat) for lat in lats] + event_ts = [str(_utc_now()) for _ in range(count)] + created_ts = [str(_utc_now()) for _ in range(count)] + + return { + "push_source_name": "driver_locations_push", + "df": { + "driver_lat": lats, + "driver_long": longs, + "driver_id": driver_ids, + "event_timestamp": event_ts, + "created_timestamp": created_ts, + }, + "to": push_mode.name.lower(), + } + + +@pytest.mark.parametrize("online_write", [True, False]) +@pytest.mark.parametrize("batching_enabled", [True, False]) +@pytest.mark.parametrize( + "push_mode", + [PushMode.ONLINE, PushMode.OFFLINE, PushMode.ONLINE_AND_OFFLINE], +) +def test_push_batched_matrix( + online_write, batching_enabled, push_mode, mock_fs_factory +): + """ + Matrix over: + - online_write ∈ {True, False} + - batching_enabled ∈ {True, False} + - push_mode ∈ {ONLINE, OFFLINE, ONLINE_AND_OFFLINE} + + Asserts: + - which of fs.push / fs.push_async are called + - how many times + - with which `to` values + + For batching_enabled=True, batch_size=1 ensures immediate flush of offline part. + """ + fs = mock_fs_factory(online_write=online_write) + + _enable_offline_batching_config( + fs, + enabled=batching_enabled, + batch_size=1, # flush immediately on a single offline request + batch_interval_seconds=60, + ) + + client = TestClient(get_app(fs)) + + # use a multi-row payload to ensure we test non-trivial dfs + resp = client.post("/push", json=push_body_many(push_mode, count=2, id_start=100)) + needs_offline = push_mode in (PushMode.OFFLINE, PushMode.ONLINE_AND_OFFLINE) + expected_status = 202 if batching_enabled and needs_offline else 200 + assert resp.status_code == expected_status + + # Collect calls + sync_calls = fs.push.call_args_list + async_calls = fs.push_async.await_args_list + sync_tos = [c.kwargs.get("to") for c in sync_calls] + async_tos = [c.kwargs.get("to") for c in async_calls] + + # ------------------------------- + # Build expectations + # ------------------------------- + expected_sync_calls = 0 + expected_async_calls = 0 + expected_sync_tos = [] + expected_async_tos = [] + + if push_mode == PushMode.ONLINE: + # Only online path, batching irrelevant + if online_write: + expected_async_calls = 1 + expected_async_tos = [PushMode.ONLINE] + else: + expected_sync_calls = 1 + expected_sync_tos = [PushMode.ONLINE] + + elif push_mode == PushMode.OFFLINE: + # Only offline path, never async + if batching_enabled: + # via batcher, but externally still one push(to=OFFLINE) + expected_sync_calls = 1 + expected_sync_tos = [PushMode.OFFLINE] + else: + # direct push(to=OFFLINE) + expected_sync_calls = 1 + expected_sync_tos = [PushMode.OFFLINE] + + elif push_mode == PushMode.ONLINE_AND_OFFLINE: + if not batching_enabled: + # Old behaviour: single call with to=ONLINE_AND_OFFLINE + if online_write: + expected_async_calls = 1 + expected_async_tos = [PushMode.ONLINE_AND_OFFLINE] + else: + expected_sync_calls = 1 + expected_sync_tos = [PushMode.ONLINE_AND_OFFLINE] + else: + # Batching enabled: ONLINE part and OFFLINE part are split + if online_write: + # async ONLINE + sync OFFLINE (via batcher) + expected_async_calls = 1 + expected_async_tos = [PushMode.ONLINE] + expected_sync_calls = 1 + expected_sync_tos = [PushMode.OFFLINE] + else: + # both ONLINE and OFFLINE via sync push + expected_sync_calls = 2 + expected_sync_tos = [PushMode.ONLINE, PushMode.OFFLINE] + + # ------------------------------- + # Assert counts + # ------------------------------- + assert fs.push.call_count == expected_sync_calls + assert fs.push_async.await_count == expected_async_calls + + # Allow ordering differences by comparing as multisets + assert Counter(sync_tos) == Counter(expected_sync_tos) + assert Counter(async_tos) == Counter(expected_async_tos) + + +def test_offline_batches_are_separated_by_flags(mock_fs_factory): + """ + Offline batches must be separated by (allow_registry_cache, transform_on_write). + + If we send three offline pushes with the same push_source_name but different + combinations of allow_registry_cache / transform_on_write, they must result + in three separate fs.push(...) calls, not one merged batch. + """ + fs = mock_fs_factory(online_write=True) + # Large batch_size so we rely on interval-based flush, not size-based. + _enable_offline_batching_config( + fs, enabled=True, batch_size=100, batch_interval_seconds=1 + ) + + client = TestClient(get_app(fs)) + + # Base body: allow_registry_cache=True, transform_on_write=True (default) + body_base = push_body_many(PushMode.OFFLINE, count=2, id_start=100) + + # 1) Default flags: allow_registry_cache=True, transform_on_write=True + resp1 = client.post("/push", json=body_base) + assert resp1.status_code == 202 + + # 2) Different allow_registry_cache + body_allow_false = dict(body_base) + body_allow_false["allow_registry_cache"] = False + resp2 = client.post("/push", json=body_allow_false) + assert resp2.status_code == 202 + + # 3) Different transform_on_write + body_transform_false = dict(body_base) + body_transform_false["transform_on_write"] = False + resp3 = client.post("/push", json=body_transform_false) + assert resp3.status_code == 202 + + # Immediately after: no flush expected yet (interval-based) + assert fs.push.call_count == 0 + + # Wait up to ~3 seconds for interval-based flush + deadline = time.time() + 3.0 + while time.time() < deadline and fs.push.call_count < 3: + time.sleep(0.1) + + # We expect exactly 3 separate pushes, each with 2 rows and to=OFFLINE + assert fs.push.call_count == 3 + + lengths = [c.kwargs["df"].shape[0] for c in fs.push.call_args_list] + tos = [c.kwargs["to"] for c in fs.push.call_args_list] + allow_flags = [c.kwargs["allow_registry_cache"] for c in fs.push.call_args_list] + transform_flags = [c.kwargs["transform_on_write"] for c in fs.push.call_args_list] + + assert all(t == PushMode.OFFLINE for t in tos) + assert lengths == [2, 2, 2] + + # Ensure we really saw 3 distinct (allow_registry_cache, transform_on_write) combos + assert len({(a, t) for a, t in zip(allow_flags, transform_flags)}) == 3 + + +def test_offline_batcher_interval_flush(mock_fs_factory): + """ + With batching enabled and a large batch_size, ensure that the time-based + flush still triggers even when the size threshold is never reached. + """ + fs = mock_fs_factory(online_write=True) + _enable_offline_batching_config( + fs, + enabled=True, + batch_size=100, # won't be hit by this test + batch_interval_seconds=1, # small interval + ) + + client = TestClient(get_app(fs)) + + # Send a single OFFLINE push (2 rows), below size threshold + resp = client.post( + "/push", json=push_body_many(PushMode.OFFLINE, count=2, id_start=500) + ) + assert resp.status_code == 202 + + # Immediately after: no sync push yet (buffer only) + assert fs.push.call_count == 0 + + # Wait up to ~3 seconds for interval-based flush + deadline = time.time() + 3.0 + while time.time() < deadline and fs.push.call_count < 1: + time.sleep(0.1) + + assert fs.push.call_count == 1 + kwargs = fs.push.call_args.kwargs + assert kwargs["to"] == PushMode.OFFLINE + assert len(kwargs["df"]) == 2 + + +# Static Artifacts Tests +@pytest.fixture +def mock_store_with_static_artifacts(tmp_path): + """Create a mock store with static_artifacts.py file for testing.""" + # Create static_artifacts.py file + static_artifacts_content = ''' +from fastapi import FastAPI +from fastapi.logger import logger + +def load_test_model(): + """Mock model loading for testing.""" + logger.info("Loading test model...") + return "test_model_loaded" + +def load_test_lookup_tables(): + """Mock lookup tables for testing.""" + return {"test_label": "test_value"} + +def load_artifacts(app: FastAPI): + """Load test static artifacts.""" + app.state.test_model = load_test_model() + app.state.test_lookup_tables = load_test_lookup_tables() + logger.info("✅ Test static artifacts loaded") +''' + + # Write static_artifacts.py to temp directory + artifacts_file = tmp_path / "static_artifacts.py" + artifacts_file.write_text(static_artifacts_content) + + # Create mock store + mock_store = MagicMock() + mock_store.repo_path = str(tmp_path) + return mock_store + + +def test_load_static_artifacts_success(mock_store_with_static_artifacts): + """Test successful loading of static artifacts during server startup.""" + import asyncio + + from fastapi import FastAPI + + from feast.feature_server import load_static_artifacts + + app = FastAPI() + + # Load static artifacts + asyncio.run(load_static_artifacts(app, mock_store_with_static_artifacts)) + + # Verify artifacts were loaded into app.state + assert hasattr(app.state, "test_model") + assert hasattr(app.state, "test_lookup_tables") + assert app.state.test_model == "test_model_loaded" + assert app.state.test_lookup_tables == {"test_label": "test_value"} + + +def test_load_static_artifacts_no_file(tmp_path): + """Test graceful handling when static_artifacts.py doesn't exist.""" + import asyncio + + from fastapi import FastAPI + + from feast.feature_server import load_static_artifacts + + app = FastAPI() + mock_store = MagicMock() + mock_store.repo_path = str(tmp_path) # Empty directory + + # Should not raise an exception + asyncio.run(load_static_artifacts(app, mock_store)) + + # Should not have added test artifacts + assert not hasattr(app.state, "test_model") + assert not hasattr(app.state, "test_lookup_tables") + + +def test_load_static_artifacts_invalid_file(tmp_path): + """Test graceful handling when static_artifacts.py has errors.""" + import asyncio + + from fastapi import FastAPI + + from feast.feature_server import load_static_artifacts + + # Create invalid static_artifacts.py + artifacts_file = tmp_path / "static_artifacts.py" + artifacts_file.write_text("raise ValueError('Test error')") + + app = FastAPI() + mock_store = MagicMock() + mock_store.repo_path = str(tmp_path) + + # Should handle the error gracefully + asyncio.run(load_static_artifacts(app, mock_store)) + + # Should not have artifacts due to error + assert not hasattr(app.state, "test_model") + + +def test_load_static_artifacts_no_load_function(tmp_path): + """Test handling when static_artifacts.py has no load_artifacts function.""" + import asyncio + + from fastapi import FastAPI + + from feast.feature_server import load_static_artifacts + + # Create static_artifacts.py without load_artifacts function + artifacts_file = tmp_path / "static_artifacts.py" + artifacts_file.write_text("TEST_CONSTANT = 'test'") + + app = FastAPI() + mock_store = MagicMock() + mock_store.repo_path = str(tmp_path) + + # Should handle gracefully + asyncio.run(load_static_artifacts(app, mock_store)) + + # Should not have artifacts since no load_artifacts function + assert not hasattr(app.state, "test_model") + + +def test_static_artifacts_persist_across_requests(mock_store_with_static_artifacts): + """Test that static artifacts persist across multiple requests.""" + from feast.feature_server import get_app + + # Create app with static artifacts + app = get_app(mock_store_with_static_artifacts) + + # Simulate artifacts being loaded (normally done in lifespan) + app.state.test_model = "persistent_model" + app.state.test_lookup_tables = {"persistent": "data"} + + # Artifacts should be available and persistent + assert app.state.test_model == "persistent_model" + assert app.state.test_lookup_tables["persistent"] == "data" + + # After simulated requests, artifacts should still be there + assert app.state.test_model == "persistent_model" + assert app.state.test_lookup_tables["persistent"] == "data" + + +def test_pytorch_nlp_template_artifacts_pattern(tmp_path): + """Test the specific PyTorch NLP template static artifacts pattern.""" + import asyncio + + from fastapi import FastAPI + + from feast.feature_server import load_static_artifacts + + # Create PyTorch NLP template-style static_artifacts.py + pytorch_artifacts_content = ''' +from fastapi import FastAPI +from fastapi.logger import logger + +def load_sentiment_model(): + """Mock sentiment analysis model loading.""" + logger.info("Loading sentiment analysis model...") + return "mock_roberta_sentiment_model" + +def load_lookup_tables(): + """Load lookup tables for sentiment mapping.""" + return { + "sentiment_labels": {"LABEL_0": "negative", "LABEL_1": "neutral", "LABEL_2": "positive"}, + "emoji_sentiment": {"😊": "positive", "😞": "negative", "😐": "neutral"}, + } + +def load_artifacts(app: FastAPI): + """Load all static artifacts for PyTorch NLP template.""" + app.state.sentiment_model = load_sentiment_model() + app.state.lookup_tables = load_lookup_tables() + + # Update global references (simulating example_repo.py pattern) + # In real template, this would be: import example_repo; example_repo._sentiment_model = ... + logger.info("✅ PyTorch NLP static artifacts loaded successfully") +''' + + artifacts_file = tmp_path / "static_artifacts.py" + artifacts_file.write_text(pytorch_artifacts_content) + + # Test loading + app = FastAPI() + mock_store = MagicMock() + mock_store.repo_path = str(tmp_path) + + asyncio.run(load_static_artifacts(app, mock_store)) + + # Verify PyTorch NLP template artifacts + assert hasattr(app.state, "sentiment_model") + assert hasattr(app.state, "lookup_tables") + assert app.state.sentiment_model == "mock_roberta_sentiment_model" + + # Verify lookup tables structure matches template + lookup_tables = app.state.lookup_tables + assert "sentiment_labels" in lookup_tables + assert "emoji_sentiment" in lookup_tables + assert lookup_tables["sentiment_labels"]["LABEL_0"] == "negative" + assert lookup_tables["sentiment_labels"]["LABEL_1"] == "neutral" + assert lookup_tables["sentiment_labels"]["LABEL_2"] == "positive" + assert lookup_tables["emoji_sentiment"]["😊"] == "positive" + + +def _build_online_response_with_features(): + """Build a GetOnlineFeaturesResponse with real feature data.""" + feature_names = ["driver_id", "driver_lat", "driver_long"] + proto = GetOnlineFeaturesResponse( + metadata=GetOnlineFeaturesResponseMetadata( + feature_names=FeatureList(val=feature_names), + ), + results=[ + GetOnlineFeaturesResponse.FeatureVector( + values=[Value(int64_val=123)], + statuses=[FieldStatus.PRESENT], + ), + GetOnlineFeaturesResponse.FeatureVector( + values=[Value(double_val=42.0)], + statuses=[FieldStatus.PRESENT], + ), + GetOnlineFeaturesResponse.FeatureVector( + values=[Value(double_val=-73.5)], + statuses=[FieldStatus.PRESENT], + ), + ], + status=True, + ) + return OnlineResponse(proto) + + +@pytest.mark.parametrize("async_online_read", [True, False]) +def test_get_online_features_non_empty_response(async_online_read, mock_fs_factory): + """Non-empty responses must pass FastAPI response_model validation (no 500).""" + fs = mock_fs_factory(online_read=async_online_read) + response_obj = _build_online_response_with_features() + fs.get_online_features = MagicMock(return_value=response_obj) + fs.get_online_features_async = AsyncMock(return_value=response_obj) + + client = TestClient(get_app(fs)) + resp = client.post("/get-online-features", json=get_online_features_body()) + assert resp.status_code == 200 + + body = resp.json() + assert body["metadata"]["feature_names"] == [ + "driver_id", + "driver_lat", + "driver_long", + ] + assert len(body["results"]) == 3 + assert body["results"][0]["values"] == [123] + assert body["results"][0]["statuses"] == ["PRESENT"] + + +def test_metadata_model_accepts_raw_proto_dict(): + """OnlineFeaturesMetadataResponse must accept the un-patched MessageToDict + format where feature_names is ``{"val": [...]}}`` instead of a flat list.""" + from feast.feature_server import ( + OnlineFeaturesMetadataResponse, + OnlineFeaturesResponse, + ) + + meta = OnlineFeaturesMetadataResponse.model_validate( + {"feature_names": {"val": ["feat_a", "feat_b"]}} + ) + assert meta.feature_names == ["feat_a", "feat_b"] + + meta_flat = OnlineFeaturesMetadataResponse.model_validate( + {"feature_names": ["feat_a", "feat_b"]} + ) + assert meta_flat.feature_names == ["feat_a", "feat_b"] + + full = OnlineFeaturesResponse.model_validate( + { + "metadata": {"feature_names": {"val": ["x", "y"]}}, + "results": [ + {"values": [1], "statuses": ["PRESENT"], "event_timestamps": []} + ], + } + ) + assert full.metadata is not None + assert full.metadata.feature_names == ["x", "y"] + + +@pytest.mark.skipif(sys.platform == "win32", reason="Gunicorn is not used on Windows") +def test_gunicorn_post_worker_init_starts_resource_and_freshness_monitoring(): + from feast.feature_server import _gunicorn_post_worker_init + + mock_store = MagicMock() + with ( + patch("feast.feature_server.feast_metrics") as mock_fm, + ): + _gunicorn_post_worker_init(mock_store, worker=MagicMock()) + + mock_fm.init_worker_monitoring.assert_called_once() + mock_fm.init_worker_freshness_monitoring.assert_called_once_with(mock_store) diff --git a/sdk/python/tests/unit/test_feature_server_utils.py b/sdk/python/tests/unit/test_feature_server_utils.py new file mode 100644 index 00000000000..85dc38db2ce --- /dev/null +++ b/sdk/python/tests/unit/test_feature_server_utils.py @@ -0,0 +1,694 @@ +# Copyright 2025 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for feature_server_utils.py + +Tests the optimized convert_response_to_dict function to ensure it matches +the output format of MessageToDict with proto_json.patch() applied. + +Related issue: https://github.com/feast-dev/feast/issues/6013 +""" + +import base64 +import json +import time + +import pytest +from google.protobuf.json_format import MessageToDict +from google.protobuf.timestamp_pb2 import Timestamp + +import feast.proto_json as proto_json +from feast.feature_server_utils import ( + _STATUS_NAMES, + _metadata_to_dict, + _timestamp_to_str, + _value_to_native, + convert_response_to_dict, +) +from feast.protos.feast.serving.ServingService_pb2 import ( + FieldStatus, + GetOnlineFeaturesResponse, + GetOnlineFeaturesResponseMetadata, +) +from feast.protos.feast.types.Value_pb2 import ( + BoolList, + BytesList, + DoubleList, + FloatList, + Int32List, + Int64List, + Int64Set, + Map, + RepeatedValue, + StringList, + StringSet, + Value, + ZonedTimestamp, +) + + +class TestValueToNative: + """Tests for _value_to_native function (matches proto_json.patch() format).""" + + def test_null_value(self): + v = Value() + result = _value_to_native(v) + assert result is None + + def test_explicit_null_val(self): + v = Value(null_val=0) + result = _value_to_native(v) + assert result is None + + def test_double_val(self): + v = Value(double_val=3.14159) + result = _value_to_native(v) + assert result == 3.14159 + + def test_float_val(self): + v = Value(float_val=2.5) + result = _value_to_native(v) + assert result == 2.5 + + def test_int64_val(self): + v = Value(int64_val=9223372036854775807) + result = _value_to_native(v) + assert result == 9223372036854775807 + + def test_int32_val(self): + v = Value(int32_val=42) + result = _value_to_native(v) + assert result == 42 + + def test_string_val(self): + v = Value(string_val="hello feast") + result = _value_to_native(v) + assert result == "hello feast" + + def test_bool_val(self): + v = Value(bool_val=True) + result = _value_to_native(v) + assert result is True + + def test_bytes_val(self): + data = b"\x00\x01\x02\x03" + v = Value(bytes_val=data) + result = _value_to_native(v) + assert result == base64.b64encode(data).decode("ascii") + + def test_double_list_val(self): + v = Value(double_list_val=DoubleList(val=[1.1, 2.2, 3.3])) + result = _value_to_native(v) + assert result == [1.1, 2.2, 3.3] + + def test_float_list_val(self): + v = Value(float_list_val=FloatList(val=[1.5, 2.5])) + result = _value_to_native(v) + assert result == [1.5, 2.5] + + def test_int64_list_val(self): + v = Value(int64_list_val=Int64List(val=[100, 200, 300])) + result = _value_to_native(v) + assert result == [100, 200, 300] + + def test_int32_list_val(self): + v = Value(int32_list_val=Int32List(val=[1, 2, 3])) + result = _value_to_native(v) + assert result == [1, 2, 3] + + def test_string_list_val(self): + v = Value(string_list_val=StringList(val=["a", "b", "c"])) + result = _value_to_native(v) + assert result == ["a", "b", "c"] + + def test_bool_list_val(self): + v = Value(bool_list_val=BoolList(val=[True, False, True])) + result = _value_to_native(v) + assert result == [True, False, True] + + def test_unix_timestamp_val(self): + v = Value(unix_timestamp_val=1609459200) + result = _value_to_native(v) + assert result == 1609459200 + + def test_bytes_list_val(self): + v = Value(bytes_list_val=BytesList(val=[b"\x00\x01", b"\x02\x03"])) + result = _value_to_native(v) + assert result == [ + base64.b64encode(b"\x00\x01").decode("ascii"), + base64.b64encode(b"\x02\x03").decode("ascii"), + ] + + def test_unix_timestamp_list_val(self): + v = Value(unix_timestamp_list_val=Int64List(val=[1609459200, 1609545600])) + result = _value_to_native(v) + assert result == [1609459200, 1609545600] + + def test_string_set_val(self): + v = Value(string_set_val=StringSet(val=["x", "y", "z"])) + result = _value_to_native(v) + assert set(result) == {"x", "y", "z"} + + def test_int64_set_val(self): + v = Value(int64_set_val=Int64Set(val=[10, 20, 30])) + result = _value_to_native(v) + assert set(result) == {10, 20, 30} + + def test_list_val_nested_values(self): + inner = RepeatedValue() + inner.val.append(Value(int64_val=1)) + inner.val.append(Value(string_val="a")) + inner.val.append(Value()) + v = Value(list_val=inner) + result = _value_to_native(v) + assert result == [1, "a", None] + + def test_set_val_nested_values(self): + inner = RepeatedValue() + inner.val.append(Value(bool_val=True)) + inner.val.append(Value(double_val=3.14)) + v = Value(set_val=inner) + result = _value_to_native(v) + assert result == [True, 3.14] + + def test_map_val(self): + m = Map() + m.val["key1"].CopyFrom(Value(int64_val=42)) + m.val["key2"].CopyFrom(Value(string_val="hello")) + v = Value(map_val=m) + result = _value_to_native(v) + assert result == {"key1": 42, "key2": "hello"} + + def test_map_val_nested_null(self): + m = Map() + m.val["present"].CopyFrom(Value(int32_val=7)) + m.val["missing"].CopyFrom(Value()) + v = Value(map_val=m) + result = _value_to_native(v) + assert result["present"] == 7 + assert result["missing"] is None + + def test_json_val(self): + v = Value(json_val='{"foo": 1}') + result = _value_to_native(v) + assert result == '{"foo": 1}' + + def test_zoned_timestamp_val(self): + # 2026-06-09T16:00:00Z viewed in America/Los_Angeles (= 09:00 -07:00). + v = Value( + zoned_timestamp_val=ZonedTimestamp( + unix_timestamp=1781020800, zone="America/Los_Angeles" + ) + ) + result = _value_to_native(v) + # Rendered as an ISO 8601 string in the stored zone, and JSON-serializable. + assert isinstance(result, str) + assert result == "2026-06-09T09:00:00-07:00" + json.dumps(result) + + def test_zoned_timestamp_val_utc(self): + v = Value( + zoned_timestamp_val=ZonedTimestamp(unix_timestamp=1781020800, zone="") + ) + result = _value_to_native(v) + assert result == "2026-06-09T16:00:00+00:00" + json.dumps(result) + + +class TestTimestampToStr: + """Tests for _timestamp_to_str function.""" + + def test_zero_timestamp(self): + ts = Timestamp(seconds=0, nanos=0) + result = _timestamp_to_str(ts) + assert result == "1970-01-01T00:00:00Z" + + def test_valid_timestamp(self): + ts = Timestamp(seconds=1609459200, nanos=0) + result = _timestamp_to_str(ts) + assert result == "2021-01-01T00:00:00Z" + + def test_timestamp_with_millis(self): + ts = Timestamp(seconds=1609459200, nanos=500000000) + result = _timestamp_to_str(ts) + assert result == "2021-01-01T00:00:00.500Z" + + def test_timestamp_with_micros(self): + ts = Timestamp(seconds=1609459200, nanos=123456000) + result = _timestamp_to_str(ts) + assert result == "2021-01-01T00:00:00.123456Z" + + def test_timestamp_with_nanos(self): + ts = Timestamp(seconds=1609459200, nanos=123456789) + result = _timestamp_to_str(ts) + assert result == "2021-01-01T00:00:00.123456789Z" + + def test_timestamp_high_nanos_no_float_rounding(self): + ts = Timestamp(seconds=1609459200, nanos=999999999) + result = _timestamp_to_str(ts) + assert result == "2021-01-01T00:00:00.999999999Z" + + +class TestMetadataToDict: + """Tests for _metadata_to_dict function (matches proto_json.patch() format).""" + + def test_empty_metadata(self): + metadata = GetOnlineFeaturesResponseMetadata() + result = _metadata_to_dict(metadata) + assert result == {} + + def test_metadata_with_feature_names(self): + metadata = GetOnlineFeaturesResponseMetadata() + metadata.feature_names.val.extend(["feature1", "feature2", "feature3"]) + result = _metadata_to_dict(metadata) + assert result == {"feature_names": ["feature1", "feature2", "feature3"]} + + +class TestConvertResponseToDict: + """Tests for the main convert_response_to_dict function.""" + + def test_empty_response(self): + response = GetOnlineFeaturesResponse() + result = convert_response_to_dict(response) + assert result == {"results": []} + + def test_single_feature_vector(self): + response = GetOnlineFeaturesResponse() + fv = response.results.add() + fv.values.append(Value(string_val="test")) + fv.statuses.append(FieldStatus.PRESENT) + + result = convert_response_to_dict(response) + + assert len(result["results"]) == 1 + assert result["results"][0]["values"] == ["test"] + assert result["results"][0]["statuses"] == ["PRESENT"] + + def test_multiple_feature_vectors(self): + response = GetOnlineFeaturesResponse() + + fv1 = response.results.add() + fv1.values.append(Value(int64_val=100)) + fv1.statuses.append(FieldStatus.PRESENT) + + fv2 = response.results.add() + fv2.values.append(Value(double_val=3.14)) + fv2.statuses.append(FieldStatus.NOT_FOUND) + + result = convert_response_to_dict(response) + + assert len(result["results"]) == 2 + assert result["results"][0]["values"] == [100] + assert result["results"][0]["statuses"] == ["PRESENT"] + assert result["results"][1]["values"] == [3.14] + assert result["results"][1]["statuses"] == ["NOT_FOUND"] + + def test_response_with_metadata(self): + response = GetOnlineFeaturesResponse() + response.metadata.feature_names.val.extend(["driver_id", "driver_rating"]) + + fv = response.results.add() + fv.values.append(Value(int64_val=123)) + fv.statuses.append(FieldStatus.PRESENT) + + result = convert_response_to_dict(response) + + assert "metadata" in result + assert result["metadata"]["feature_names"] == ["driver_id", "driver_rating"] + + def test_response_with_timestamps(self): + response = GetOnlineFeaturesResponse() + fv = response.results.add() + fv.values.append(Value(string_val="test")) + fv.statuses.append(FieldStatus.PRESENT) + ts = fv.event_timestamps.add() + ts.seconds = 1609459200 + + result = convert_response_to_dict(response) + + assert len(result["results"][0]["event_timestamps"]) == 1 + assert "2021-01-01" in result["results"][0]["event_timestamps"][0] + + def test_all_status_types(self): + response = GetOnlineFeaturesResponse() + fv = response.results.add() + + for status in [ + FieldStatus.INVALID, + FieldStatus.PRESENT, + FieldStatus.NULL_VALUE, + FieldStatus.NOT_FOUND, + FieldStatus.OUTSIDE_MAX_AGE, + ]: + fv.values.append(Value(int32_val=1)) + fv.statuses.append(status) + + result = convert_response_to_dict(response) + + expected_statuses = [ + "INVALID", + "PRESENT", + "NULL_VALUE", + "NOT_FOUND", + "OUTSIDE_MAX_AGE", + ] + assert result["results"][0]["statuses"] == expected_statuses + + def test_status_field_included_when_true(self): + response = GetOnlineFeaturesResponse() + response.status = True + result = convert_response_to_dict(response) + assert result.get("status") is True + + def test_status_field_omitted_when_false(self): + response = GetOnlineFeaturesResponse() + result = convert_response_to_dict(response) + assert "status" not in result + + def test_null_values_become_none(self): + response = GetOnlineFeaturesResponse() + fv = response.results.add() + fv.values.append(Value()) + fv.values.append(Value(null_val=0)) + fv.statuses.extend([FieldStatus.NULL_VALUE, FieldStatus.NULL_VALUE]) + + result = convert_response_to_dict(response) + + assert result["results"][0]["values"] == [None, None] + + def test_list_values_are_native_lists(self): + response = GetOnlineFeaturesResponse() + fv = response.results.add() + fv.values.append(Value(int64_list_val=Int64List(val=[1, 2, 3]))) + fv.values.append(Value(string_list_val=StringList(val=["a", "b"]))) + fv.statuses.extend([FieldStatus.PRESENT, FieldStatus.PRESENT]) + + result = convert_response_to_dict(response) + + assert result["results"][0]["values"] == [[1, 2, 3], ["a", "b"]] + + +class TestConvertResponseToDictConsistency: + """Tests ensuring convert_response_to_dict matches MessageToDict with patch.""" + + @pytest.fixture(autouse=True) + def setup_proto_json_patch(self): + proto_json.patch() + + def _build_complex_response( + self, num_features: int = 10 + ) -> GetOnlineFeaturesResponse: + response = GetOnlineFeaturesResponse() + feature_names = [f"feature_{i}" for i in range(num_features)] + response.metadata.feature_names.val.extend(feature_names) + + for i in range(num_features): + fv = response.results.add() + if i % 4 == 0: + fv.values.append(Value(int64_val=i * 100)) + elif i % 4 == 1: + fv.values.append(Value(double_val=i * 0.1)) + elif i % 4 == 2: + fv.values.append(Value(string_val=f"value_{i}")) + else: + fv.values.append(Value()) + fv.statuses.append(FieldStatus.PRESENT) + + return response + + def test_values_match_patched_message_to_dict(self): + """Ensure value serialization matches proto_json.patch() format.""" + response = self._build_complex_response(8) + + fast_result = convert_response_to_dict(response) + standard_result = MessageToDict(response, preserving_proto_field_name=True) + + assert set(fast_result.keys()) == set(standard_result.keys()) + assert len(fast_result["results"]) == len(standard_result["results"]) + + for i in range(len(fast_result["results"])): + fast_values = fast_result["results"][i]["values"] + standard_values = standard_result["results"][i]["values"] + assert fast_values == standard_values, f"Mismatch at result {i}" + + def test_metadata_matches_patched_format(self): + """Ensure metadata format matches proto_json.patch() format.""" + response = self._build_complex_response(5) + + fast_result = convert_response_to_dict(response) + standard_result = MessageToDict(response, preserving_proto_field_name=True) + + if "metadata" in standard_result: + assert "metadata" in fast_result + assert ( + fast_result["metadata"]["feature_names"] + == standard_result["metadata"]["feature_names"] + ) + + def test_float_val_precision_matches_message_to_dict(self): + """float32 storage causes truncation; both paths must return identical values.""" + response = GetOnlineFeaturesResponse() + fv = response.results.add() + # 3.14 is not exactly representable as float32; both implementations + # should return the same truncated float64 representation + fv.values.append(Value(float_val=3.14)) + fv.statuses.append(FieldStatus.PRESENT) + + fast_result = convert_response_to_dict(response) + standard_result = MessageToDict(response, preserving_proto_field_name=True) + + assert ( + fast_result["results"][0]["values"] + == standard_result["results"][0]["values"] + ) + + def test_double_val_precision(self): + """double_val is returned as a Python float (shortest round-trip form). + + The upstream code passed float_precision=18 to MessageToDict, which forced 18 + significant digits for doubles. Our implementation returns native Python floats + serialized by json.dumps using Python 3.1+ shortest-round-trip representation + (~15–17 sig digits). The value is identical when round-tripped through float64; + the only difference is how many trailing digits appear in the JSON string. + This is an intentional trade-off for speed and is safe for all ML feature values. + """ + import struct + + # Use a value with many significant digits + pi = 3.141592653589793 + response = GetOnlineFeaturesResponse() + fv = response.results.add() + fv.values.append(Value(double_val=pi)) + fv.statuses.append(FieldStatus.PRESENT) + + result = convert_response_to_dict(response) + value = result["results"][0]["values"][0] + + # Value must round-trip correctly through json.dumps + assert value == pi + round_tripped = json.loads(json.dumps(value)) + assert round_tripped == pi + # Verify it encodes as the same float64 bit pattern + assert struct.pack("d", value) == struct.pack("d", pi) + + def test_set_types_return_flat_list(self): + """set types (string_set_val, int64_set_val, etc.) return flat lists. + + Note: proto_json.patch() does not explicitly handle _set_ types — they fall + through to the else branch and return the raw proto object, which MessageToDict + then serializes as {"val": [...]}. Our implementation normalizes these to flat + lists, which is more useful for API consumers and consistent with list types. + """ + response = GetOnlineFeaturesResponse() + fv = response.results.add() + fv.values.append(Value(string_set_val=StringSet(val=["a", "b", "c"]))) + fv.statuses.append(FieldStatus.PRESENT) + + result = convert_response_to_dict(response) + values = result["results"][0]["values"] + assert isinstance(values[0], list), "set type should be a flat list" + assert set(values[0]) == {"a", "b", "c"} + + def test_bytes_val_is_base64_encoded(self): + """bytes_val is base64-encoded so JSONResponse can serialize it. + + This intentionally differs from proto_json.patch() which returns raw bytes. + Raw bytes are not JSON-serializable; base64 is the standard protobuf JSON + encoding for bytes fields and is safe for all HTTP clients. + """ + response = GetOnlineFeaturesResponse() + fv = response.results.add() + data = b"\x00\x01\x02\xff" + fv.values.append(Value(bytes_val=data)) + fv.statuses.append(FieldStatus.PRESENT) + + result = convert_response_to_dict(response) + encoded = result["results"][0]["values"][0] + + assert encoded == base64.b64encode(data).decode("ascii") + # Must be JSON-serializable + assert json.dumps(encoded) + + +class TestJsonSerializability: + """Ensure convert_response_to_dict output is always JSON-serializable.""" + + def test_complex_types_are_json_serializable(self): + """map_val, list_val, set_val must not leave proto objects in the output.""" + response = GetOnlineFeaturesResponse() + fv = response.results.add() + + # map_val + m = Map() + m.val["k"].CopyFrom(Value(int64_val=1)) + fv.values.append(Value(map_val=m)) + + # list_val (RepeatedValue) + inner = RepeatedValue() + inner.val.append(Value(string_val="a")) + fv.values.append(Value(list_val=inner)) + + # int64_set_val + fv.values.append(Value(int64_set_val=Int64Set(val=[10, 20]))) + + fv.statuses.extend( + [FieldStatus.PRESENT, FieldStatus.PRESENT, FieldStatus.PRESENT] + ) + + result = convert_response_to_dict(response) + # must not raise + serialized = json.dumps(result) + assert serialized # non-empty + + +class TestProtobufCompatibility: + """Regression tests for protobuf >= 7.34.0 compatibility (issue #6435). + + protobuf 7.34.0 removed the float_precision kwarg from MessageToDict. + convert_response_to_dict must never call MessageToDict, so it is immune + to this breaking change regardless of the installed protobuf version. + """ + + def test_no_message_to_dict_dependency(self): + """convert_response_to_dict must not call MessageToDict internally.""" + import unittest.mock as mock + + response = GetOnlineFeaturesResponse() + fv = response.results.add() + fv.values.append(Value(double_val=3.141592653589793)) + fv.values.append(Value(int64_val=42)) + fv.values.append(Value(string_val="hello")) + fv.statuses.extend( + [FieldStatus.PRESENT, FieldStatus.PRESENT, FieldStatus.PRESENT] + ) + response.metadata.feature_names.val.extend(["f1", "f2", "f3"]) + + with mock.patch( + "google.protobuf.json_format.MessageToDict", + side_effect=AssertionError("MessageToDict must not be called"), + ): + result = convert_response_to_dict(response) + + assert len(result["results"]) == 1 + assert result["results"][0]["values"] == [3.141592653589793, 42, "hello"] + assert result["metadata"]["feature_names"] == ["f1", "f2", "f3"] + + def test_double_precision_without_float_precision_kwarg(self): + """Doubles must round-trip without the removed float_precision kwarg.""" + values = [ + 1e-300, + 1.7976931348623157e308, + 2.2250738585072014e-308, + 0.1 + 0.2, + ] + response = GetOnlineFeaturesResponse() + fv = response.results.add() + for v in values: + fv.values.append(Value(double_val=v)) + fv.statuses.append(FieldStatus.PRESENT) + + result = convert_response_to_dict(response) + for i, expected in enumerate(values): + actual = result["results"][0]["values"][i] + assert actual == expected, f"Mismatch at index {i}: {actual} != {expected}" + round_tripped = json.loads(json.dumps(actual)) + assert round_tripped == expected + + +class TestPerformance: + """Performance tests to validate the optimization claim.""" + + def _build_large_response( + self, num_entities: int = 50, num_features: int = 100 + ) -> GetOnlineFeaturesResponse: + response = GetOnlineFeaturesResponse() + feature_names = [f"feature_{i}" for i in range(num_features)] + response.metadata.feature_names.val.extend(feature_names) + + for i in range(num_features): + fv = response.results.add() + for j in range(num_entities): + if i % 4 == 0: + fv.values.append(Value(int64_val=j * 100 + i)) + elif i % 4 == 1: + fv.values.append(Value(double_val=j * 0.1 + i)) + elif i % 4 == 2: + fv.values.append(Value(string_val=f"entity_{j}_feature_{i}")) + else: + fv.values.append(Value(bool_val=j % 2 == 0)) + fv.statuses.append(FieldStatus.PRESENT) + + return response + + @pytest.mark.slow + def test_faster_than_message_to_dict(self): + """Verify convert_response_to_dict is faster than MessageToDict.""" + proto_json.patch() + response = self._build_large_response(num_entities=50, num_features=100) + iterations = 100 + + for _ in range(10): + convert_response_to_dict(response) + MessageToDict(response, preserving_proto_field_name=True) + + start = time.process_time() + for _ in range(iterations): + convert_response_to_dict(response) + fast_time = time.process_time() - start + + start = time.process_time() + for _ in range(iterations): + MessageToDict(response, preserving_proto_field_name=True) + standard_time = time.process_time() - start + + speedup = standard_time / fast_time + print(f"\nPerformance: fast={fast_time:.3f}s, standard={standard_time:.3f}s") + print(f"Speedup: {speedup:.2f}x") + + assert speedup >= 1.2, f"Expected at least 1.2x speedup, got {speedup:.2f}x" + + +class TestStatusNames: + """Tests for the status name mapping.""" + + def test_all_status_codes_mapped(self): + assert 0 in _STATUS_NAMES # INVALID + assert 1 in _STATUS_NAMES # PRESENT + assert 2 in _STATUS_NAMES # NULL_VALUE + assert 3 in _STATUS_NAMES # NOT_FOUND + assert 4 in _STATUS_NAMES # OUTSIDE_MAX_AGE + + def test_unknown_status_returns_invalid(self): + assert _STATUS_NAMES.get(999, "INVALID") == "INVALID" diff --git a/sdk/python/tests/unit/test_feature_service.py b/sdk/python/tests/unit/test_feature_service.py index 4448d2e8ea2..33f7290c47a 100644 --- a/sdk/python/tests/unit/test_feature_service.py +++ b/sdk/python/tests/unit/test_feature_service.py @@ -1,11 +1,152 @@ +from feast.errors import FeastObjectNotFoundException from feast.feature_service import FeatureService from feast.feature_view import FeatureView +from feast.feature_view_projection import FeatureViewProjection from feast.field import Field from feast.infra.offline_stores.file_source import FileSource -from feast.types import Float32 +from feast.protos.feast.core.Feature_pb2 import FeatureSpecV2 +from feast.protos.feast.core.FeatureViewProjection_pb2 import ( + FeatureViewProjection as FeatureViewProjectionProto, +) +from feast.types import Float32, String from tests.utils.test_wrappers import no_warnings +class _MockRegistry: + def __init__(self, feature_view: FeatureView): + self._feature_view = feature_view + + def get_any_feature_view(self, name, project, allow_cache=False): + if name != self._feature_view.name: + raise FeastObjectNotFoundException(name) + return self._feature_view + + +def _build_feature_view(): + file_source = FileSource(name="my-file-source", path="test.parquet") + return FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=String), + ], + source=file_source, + ) + + +def test_prepare_for_apply_whole_view(): + feature_view = _build_feature_view() + feature_service = FeatureService.from_proto( + FeatureService( + name="my-feature-service", + features=[], + ).to_proto() + ) + feature_service.feature_view_projections = [ + FeatureViewProjection( + name=feature_view.name, + name_alias=None, + desired_features=[], + features=[], + ) + ] + + feature_service.prepare_for_apply(_MockRegistry(feature_view), "test_project") + + assert len(feature_service.feature_view_projections) == 1 + resolved = feature_service.feature_view_projections[0] + assert [feature.name for feature in resolved.features] == [ + "feature1", + "feature2", + ] + assert resolved.features[0].dtype == Float32 + assert resolved.features[1].dtype == String + + +def test_prepare_for_apply_feature_subset(): + feature_view = _build_feature_view() + feature_service = FeatureService( + name="my-feature-service", + features=[], + ) + feature_service.feature_view_projections = [ + FeatureViewProjection( + name=feature_view.name, + name_alias=None, + desired_features=["feature2"], + features=[], + ) + ] + + feature_service.prepare_for_apply(_MockRegistry(feature_view), "test_project") + + resolved = feature_service.feature_view_projections[0] + assert len(resolved.features) == 1 + assert resolved.features[0].name == "feature2" + assert resolved.features[0].dtype == String + + +def test_prepare_for_apply_name_only_columns(): + feature_view = _build_feature_view() + projection_proto = FeatureViewProjectionProto( + feature_view_name=feature_view.name, + ) + projection_proto.feature_columns.append(FeatureSpecV2(name="feature2")) + feature_service = FeatureService.from_proto( + FeatureService(name="my-feature-service", features=[]).to_proto() + ) + feature_service.feature_view_projections = [ + FeatureViewProjection.from_proto(projection_proto) + ] + + feature_service.prepare_for_apply(_MockRegistry(feature_view), "test_project") + + resolved = feature_service.feature_view_projections[0] + assert len(resolved.features) == 1 + assert resolved.features[0].name == "feature2" + assert resolved.features[0].dtype == String + + +def test_prepare_for_apply_keeps_complete_projection(): + feature_view = _build_feature_view() + feature_service = FeatureService( + name="my-feature-service", + features=[feature_view[["feature1"]]], + ) + original_projection = feature_service.feature_view_projections[0] + + feature_service.prepare_for_apply(_MockRegistry(feature_view), "test_project") + + assert feature_service.feature_view_projections[0] == original_projection + + +def test_build_apply_request(): + request = FeatureService.build_apply_request( + name="my-feature-service", + project="test_project", + feature_view_refs=[ + ("feature_view_a", None), + ("feature_view_b", ["feature1"]), + ], + description="test service", + tags={"team": "ml"}, + owner="owner@example.com", + ) + + spec = request.feature_service.spec + assert spec.name == "my-feature-service" + assert spec.description == "test service" + assert spec.owner == "owner@example.com" + assert dict(spec.tags) == {"team": "ml"} + assert request.project == "test_project" + assert len(spec.features) == 2 + assert spec.features[0].feature_view_name == "feature_view_a" + assert len(spec.features[0].feature_columns) == 0 + assert spec.features[1].feature_view_name == "feature_view_b" + assert [column.name for column in spec.features[1].feature_columns] == ["feature1"] + + def test_feature_service_with_description(): feature_service = FeatureService( name="my-feature-service", features=[], description="a clear description" diff --git a/sdk/python/tests/unit/test_feature_view_state.py b/sdk/python/tests/unit/test_feature_view_state.py new file mode 100644 index 00000000000..3af91b3b469 --- /dev/null +++ b/sdk/python/tests/unit/test_feature_view_state.py @@ -0,0 +1,433 @@ +import copy +from datetime import datetime, timedelta +from tempfile import mkstemp + +import pytest + +from feast.data_format import AvroFormat, ParquetFormat +from feast.data_source import KafkaSource +from feast.entity import Entity +from feast.feature_store import FeatureStore +from feast.feature_view import FeatureView, FeatureViewState +from feast.field import Field +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig +from feast.on_demand_feature_view import OnDemandFeatureView +from feast.protos.feast.core.FeatureView_pb2 import FeatureView as FeatureViewProto +from feast.protos.feast.core.FeatureView_pb2 import ( + FeatureViewMeta as FeatureViewMetaProto, +) +from feast.protos.feast.core.FeatureView_pb2 import ( + FeatureViewSpec as FeatureViewSpecProto, +) +from feast.repo_config import RepoConfig +from feast.stream_feature_view import StreamFeatureView +from feast.types import Float32, Int64 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _kafka_source(): + return KafkaSource( + name="kafka", + timestamp_field="event_timestamp", + kafka_bootstrap_servers="localhost:9092", + message_format=AvroFormat(""), + topic="topic", + batch_source=_batch_source(), + ) + + +def _batch_source(): + return FileSource( + file_format=ParquetFormat(), + path="file://feast/*", + timestamp_field="ts_col", + created_timestamp_column="timestamp", + ) + + +def _simple_feature_view(name="test_fv", enabled=True): + return FeatureView( + name=name, + entities=[], + schema=[Field(name="f1", dtype=Float32)], + source=_batch_source(), + ttl=timedelta(days=1), + enabled=enabled, + ) + + +@pytest.fixture +def local_feature_store(): + _, registry_path = mkstemp() + _, online_store_path = mkstemp() + return FeatureStore( + config=RepoConfig( + registry=registry_path, + project="default", + provider="local", + online_store=SqliteOnlineStoreConfig(path=online_store_path), + entity_key_serialization_version=3, + ) + ) + + +# --------------------------------------------------------------------------- +# FeatureViewState enum +# --------------------------------------------------------------------------- + + +class TestFeatureViewState: + def test_state_values(self): + assert FeatureViewState.STATE_UNSPECIFIED == 0 + assert FeatureViewState.CREATED == 1 + assert FeatureViewState.GENERATED == 2 + assert FeatureViewState.MATERIALIZING == 3 + assert FeatureViewState.AVAILABLE_ONLINE == 4 + + def test_from_proto_valid(self): + assert FeatureViewState.from_proto(0) == FeatureViewState.STATE_UNSPECIFIED + assert FeatureViewState.from_proto(4) == FeatureViewState.AVAILABLE_ONLINE + + def test_from_proto_invalid_falls_back(self): + assert FeatureViewState.from_proto(999) == FeatureViewState.STATE_UNSPECIFIED + + def test_to_proto_round_trip(self): + for state in FeatureViewState: + assert FeatureViewState.from_proto(state.to_proto()) == state + + def test_valid_transitions(self): + """Verify that valid transitions are accepted.""" + assert FeatureViewState.STATE_UNSPECIFIED.can_transition_to( + FeatureViewState.CREATED + ) + assert FeatureViewState.CREATED.can_transition_to(FeatureViewState.GENERATED) + assert FeatureViewState.GENERATED.can_transition_to( + FeatureViewState.MATERIALIZING + ) + assert FeatureViewState.MATERIALIZING.can_transition_to( + FeatureViewState.AVAILABLE_ONLINE + ) + assert FeatureViewState.MATERIALIZING.can_transition_to( + FeatureViewState.GENERATED + ) + assert FeatureViewState.AVAILABLE_ONLINE.can_transition_to( + FeatureViewState.MATERIALIZING + ) + + def test_invalid_transitions(self): + """Verify that invalid transitions are rejected.""" + assert not FeatureViewState.STATE_UNSPECIFIED.can_transition_to( + FeatureViewState.AVAILABLE_ONLINE + ) + assert not FeatureViewState.CREATED.can_transition_to( + FeatureViewState.MATERIALIZING + ) + assert not FeatureViewState.CREATED.can_transition_to( + FeatureViewState.AVAILABLE_ONLINE + ) + assert not FeatureViewState.GENERATED.can_transition_to( + FeatureViewState.AVAILABLE_ONLINE + ) + assert not FeatureViewState.AVAILABLE_ONLINE.can_transition_to( + FeatureViewState.CREATED + ) + + +# --------------------------------------------------------------------------- +# FeatureView enabled / state defaults +# --------------------------------------------------------------------------- + + +class TestFeatureViewDefaults: + def test_default_enabled_is_true(self): + fv = _simple_feature_view() + assert fv.enabled is True + + def test_default_state_is_unspecified(self): + fv = _simple_feature_view() + assert fv.state == FeatureViewState.STATE_UNSPECIFIED + + def test_enabled_false(self): + fv = _simple_feature_view(enabled=False) + assert fv.enabled is False + + +# --------------------------------------------------------------------------- +# Proto serialization round-trips +# --------------------------------------------------------------------------- + + +class TestFeatureViewProtoRoundTrip: + def test_enabled_true_round_trip(self): + fv = _simple_feature_view(enabled=True) + proto = fv.to_proto() + assert proto.spec.disabled is False + restored = FeatureView.from_proto(proto) + assert restored.enabled is True + + def test_enabled_false_round_trip(self): + fv = _simple_feature_view(enabled=False) + proto = fv.to_proto() + assert proto.spec.disabled is True + restored = FeatureView.from_proto(proto) + assert restored.enabled is False + + def test_state_round_trip(self): + fv = _simple_feature_view() + fv.state = FeatureViewState.AVAILABLE_ONLINE + proto = fv.to_proto() + assert proto.meta.state == FeatureViewState.AVAILABLE_ONLINE.value + restored = FeatureView.from_proto(proto) + assert restored.state == FeatureViewState.AVAILABLE_ONLINE + + def test_state_unspecified_not_written_to_proto(self): + fv = _simple_feature_view() + assert fv.state == FeatureViewState.STATE_UNSPECIFIED + proto = fv.to_proto() + assert proto.meta.state == 0 + + def test_backward_compat_old_proto_without_disabled_field(self): + """Old protos without `disabled` field default to False -> enabled=True.""" + spec = FeatureViewSpecProto() + spec.name = "legacy_fv" + proto = FeatureViewProto(spec=spec, meta=FeatureViewMetaProto()) + fv = FeatureView.from_proto(proto) + assert fv.enabled is True + + def test_backward_compat_old_proto_without_state_field(self): + """Old protos without `state` field default to 0 -> STATE_UNSPECIFIED.""" + spec = FeatureViewSpecProto() + spec.name = "legacy_fv" + proto = FeatureViewProto(spec=spec, meta=FeatureViewMetaProto()) + fv = FeatureView.from_proto(proto) + assert fv.state == FeatureViewState.STATE_UNSPECIFIED + + def test_all_states_round_trip(self): + for state in FeatureViewState: + fv = _simple_feature_view() + fv.state = state + restored = FeatureView.from_proto(fv.to_proto()) + assert restored.state == state + + +# --------------------------------------------------------------------------- +# copy.copy preserves enabled/state +# --------------------------------------------------------------------------- + + +class TestCopyPreservesState: + def test_feature_view_copy(self): + fv = _simple_feature_view(enabled=False) + fv.state = FeatureViewState.GENERATED + copied = copy.copy(fv) + assert copied.enabled is False + assert copied.state == FeatureViewState.GENERATED + + def test_on_demand_feature_view_copy(self): + source_fv = _simple_feature_view() + odfv = OnDemandFeatureView( + name="test_odfv", + sources=[source_fv], + schema=[Field(name="out", dtype=Float32)], + mode="python", + udf=lambda features: {"out": [1.0]}, + enabled=False, + ) + odfv.state = FeatureViewState.GENERATED + copied = copy.copy(odfv) + assert copied.enabled is False + assert copied.state == FeatureViewState.GENERATED + + def test_stream_feature_view_copy(self): + sfv = StreamFeatureView( + name="test_sfv", + entities=[], + schema=[Field(name="f1", dtype=Float32)], + source=_kafka_source(), + ttl=timedelta(days=1), + ) + sfv.enabled = False + sfv.state = FeatureViewState.AVAILABLE_ONLINE + copied = copy.copy(sfv) + assert copied.enabled is False + assert copied.state == FeatureViewState.AVAILABLE_ONLINE + + +# --------------------------------------------------------------------------- +# OnDemandFeatureView enabled / state +# --------------------------------------------------------------------------- + + +class TestOnDemandFeatureViewState: + def test_default_enabled(self): + source_fv = _simple_feature_view() + odfv = OnDemandFeatureView( + name="test_odfv", + sources=[source_fv], + schema=[Field(name="out", dtype=Float32)], + mode="python", + udf=lambda features: {"out": [1.0]}, + ) + assert odfv.enabled is True + assert odfv.state == FeatureViewState.STATE_UNSPECIFIED + + def test_disabled(self): + source_fv = _simple_feature_view() + odfv = OnDemandFeatureView( + name="test_odfv", + sources=[source_fv], + schema=[Field(name="out", dtype=Float32)], + mode="python", + udf=lambda features: {"out": [1.0]}, + enabled=False, + ) + assert odfv.enabled is False + + def test_proto_disabled_field(self): + """Verify the proto disabled field is set correctly without full round-trip.""" + source_fv = _simple_feature_view() + odfv = OnDemandFeatureView( + name="test_odfv", + sources=[source_fv], + schema=[Field(name="out", dtype=Float32)], + mode="python", + udf=lambda features: {"out": [1.0]}, + enabled=False, + ) + odfv.state = FeatureViewState.AVAILABLE_ONLINE + proto = odfv.to_proto() + assert proto.spec.disabled is True + assert proto.meta.state == FeatureViewState.AVAILABLE_ONLINE.value + + +# --------------------------------------------------------------------------- +# StreamFeatureView enabled / state +# --------------------------------------------------------------------------- + + +class TestStreamFeatureViewState: + def test_proto_round_trip(self): + sfv = StreamFeatureView( + name="test_sfv", + entities=[], + schema=[Field(name="f1", dtype=Float32)], + source=_kafka_source(), + ttl=timedelta(days=1), + ) + sfv.enabled = False + sfv.state = FeatureViewState.MATERIALIZING + proto = sfv.to_proto() + assert proto.spec.disabled is True + restored = StreamFeatureView.from_proto(proto) + assert restored.enabled is False + assert restored.state == FeatureViewState.MATERIALIZING + + +# --------------------------------------------------------------------------- +# Registry apply preserves enabled/state +# --------------------------------------------------------------------------- + + +class TestRegistryEnabledState: + def test_apply_and_retrieve_enabled(self, local_feature_store): + store = local_feature_store + fv = _simple_feature_view(enabled=True) + store.apply([fv]) + retrieved = store.get_feature_view("test_fv") + assert retrieved.enabled is True + store.teardown() + + def test_apply_and_retrieve_disabled(self, local_feature_store): + store = local_feature_store + fv = _simple_feature_view(enabled=False) + store.apply([fv]) + retrieved = store.get_feature_view("test_fv") + assert retrieved.enabled is False + store.teardown() + + def test_toggle_enabled_via_registry(self, local_feature_store): + store = local_feature_store + fv = _simple_feature_view(enabled=True) + store.apply([fv]) + + # Disable it + fv.enabled = False + store.registry.apply_feature_view(fv, store.project) + retrieved = store.get_feature_view("test_fv") + assert retrieved.enabled is False + + # Re-enable it + fv.enabled = True + store.registry.apply_feature_view(fv, store.project) + retrieved = store.get_feature_view("test_fv") + assert retrieved.enabled is True + store.teardown() + + def test_state_persists_through_registry(self, local_feature_store): + store = local_feature_store + fv = _simple_feature_view() + fv.state = FeatureViewState.GENERATED + store.apply([fv]) + + # State should be updated via registry apply + fv.state = FeatureViewState.AVAILABLE_ONLINE + store.registry.apply_feature_view(fv, store.project) + retrieved = store.get_feature_view("test_fv") + assert retrieved.state == FeatureViewState.AVAILABLE_ONLINE + store.teardown() + + def test_reapply_does_not_reset_state(self, local_feature_store): + """feast apply with a default-state FV must not reset an existing state.""" + store = local_feature_store + fv = _simple_feature_view() + store.apply([fv]) + + # Simulate materialization having moved state to AVAILABLE_ONLINE + fv.state = FeatureViewState.AVAILABLE_ONLINE + store.registry.apply_feature_view(fv, store.project) + retrieved = store.get_feature_view("test_fv") + assert retrieved.state == FeatureViewState.AVAILABLE_ONLINE + + # Re-apply with a fresh FV (default STATE_UNSPECIFIED) — should preserve state + fresh_fv = _simple_feature_view() + assert fresh_fv.state == FeatureViewState.STATE_UNSPECIFIED + store.apply([fresh_fv]) + retrieved = store.get_feature_view("test_fv") + assert retrieved.state == FeatureViewState.AVAILABLE_ONLINE + store.teardown() + + +# --------------------------------------------------------------------------- +# Materialization blocks disabled feature views +# --------------------------------------------------------------------------- + + +class TestMaterializationDisabledBlocking: + def test_materialize_disabled_fv_by_name_raises(self, local_feature_store): + store = local_feature_store + entity = Entity(name="entity_1", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + schema=[ + Field(name="f1", dtype=Float32), + Field(name="entity_id", dtype=Int64), + ], + source=_batch_source(), + ttl=timedelta(days=1), + online=True, + enabled=False, + ) + store.apply([entity, fv]) + + with pytest.raises(ValueError, match="disabled"): + store.materialize( + feature_views=["test_fv"], + start_date=datetime.utcnow() - timedelta(hours=1), + end_date=datetime.utcnow(), + ) + store.teardown() diff --git a/sdk/python/tests/unit/test_feature_view_versioning.py b/sdk/python/tests/unit/test_feature_view_versioning.py new file mode 100644 index 00000000000..80c2bc808ea --- /dev/null +++ b/sdk/python/tests/unit/test_feature_view_versioning.py @@ -0,0 +1,558 @@ +import pytest + +from feast.utils import _parse_feature_ref, _strip_version_from_ref +from feast.version_utils import ( + generate_version_id, + normalize_version_string, + parse_version, + version_tag, +) + + +class TestParseVersion: + def test_latest_string(self): + is_latest, num = parse_version("latest") + assert is_latest is True + assert num == 0 + + def test_empty_string(self): + is_latest, num = parse_version("") + assert is_latest is True + assert num == 0 + + def test_latest_case_insensitive(self): + is_latest, num = parse_version("Latest") + assert is_latest is True + + def test_v_format(self): + is_latest, num = parse_version("v2") + assert is_latest is False + assert num == 2 + + def test_version_format(self): + is_latest, num = parse_version("version3") + assert is_latest is False + assert num == 3 + + def test_case_insensitive_v(self): + is_latest, num = parse_version("V5") + assert is_latest is False + assert num == 5 + + def test_case_insensitive_version(self): + is_latest, num = parse_version("Version10") + assert is_latest is False + assert num == 10 + + def test_v0(self): + is_latest, num = parse_version("v0") + assert is_latest is False + assert num == 0 + + def test_invalid_format(self): + with pytest.raises(ValueError, match="Invalid version string"): + parse_version("abc") + + def test_invalid_format_no_number(self): + with pytest.raises(ValueError, match="Invalid version string"): + parse_version("v") + + def test_invalid_format_negative(self): + with pytest.raises(ValueError, match="Invalid version string"): + parse_version("v-1") + + +class TestNormalizeVersionString: + def test_empty_to_latest(self): + assert normalize_version_string("") == "latest" + + def test_latest_unchanged(self): + assert normalize_version_string("latest") == "latest" + + def test_v2_canonical(self): + assert normalize_version_string("v2") == "v2" + + def test_version2_to_v2(self): + assert normalize_version_string("version2") == "v2" + + def test_V3_to_v3(self): + assert normalize_version_string("V3") == "v3" + + +class TestVersionTag: + def test_simple(self): + assert version_tag(0) == "v0" + assert version_tag(5) == "v5" + assert version_tag(100) == "v100" + + +class TestGenerateVersionId: + def test_is_uuid(self): + vid = generate_version_id() + assert len(vid) == 36 + assert vid.count("-") == 4 + + def test_unique(self): + v1 = generate_version_id() + v2 = generate_version_id() + assert v1 != v2 + + +class TestFeatureViewVersionField: + def test_feature_view_default_version(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + ) + assert fv.version == "latest" + assert fv.current_version_number is None + + def test_feature_view_explicit_version(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + version="v2", + ) + assert fv.version == "v2" + + def test_feature_view_proto_roundtrip(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + version="v3", + ) + fv.current_version_number = 3 + + proto = fv.to_proto() + assert proto.spec.version == "v3" + assert proto.meta.current_version_number == 3 + + fv2 = FeatureView.from_proto(proto) + assert fv2.version == "v3" + assert fv2.current_version_number == 3 + + def test_feature_view_proto_roundtrip_v0(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + version="v0", + ) + fv.current_version_number = 0 + + proto = fv.to_proto() + assert proto.spec.version == "v0" + assert proto.meta.current_version_number == 0 + + fv2 = FeatureView.from_proto(proto) + assert fv2.version == "v0" + assert fv2.current_version_number == 0 + + def test_feature_view_proto_roundtrip_latest_zero(self): + """version='latest' with current_version_number=None is preserved as + None through proto roundtrip (proto default 0 without spec.version + is treated as None for backward compatibility).""" + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + # default version="latest", current_version_number=None + ) + assert fv.current_version_number is None + + proto = fv.to_proto() + fv2 = FeatureView.from_proto(proto) + assert fv2.version == "latest" + # Proto default 0 without spec.version is treated as None + assert fv2.current_version_number is None + + def test_feature_view_equality_with_version(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv1 = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + version="v2", + ) + fv2 = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + version="version2", + ) + # v2 and version2 should be equivalent + assert fv1 == fv2 + + def test_feature_view_inequality_different_version(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv1 = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + version="latest", + ) + fv2 = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + version="v2", + ) + assert fv1 != fv2 + + def test_feature_view_empty_version_equals_latest(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv1 = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + ) + fv2 = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + version="latest", + ) + assert fv1 == fv2 + + def test_feature_view_copy_preserves_version(self): + import copy + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + version="v5", + ) + fv_copy = copy.copy(fv) + assert fv_copy.version == "v5" + + +class TestOnDemandFeatureViewVersionField: + def test_odfv_default_version(self): + from feast.data_source import RequestSource + from feast.field import Field + from feast.on_demand_feature_view import OnDemandFeatureView + from feast.types import Float32 + + request_source = RequestSource( + name="vals_to_add", + schema=[ + Field(name="val_to_add", dtype=Float32), + Field(name="val_to_add_2", dtype=Float32), + ], + ) + odfv = OnDemandFeatureView( + name="test_odfv", + sources=[request_source], + schema=[Field(name="output", dtype=Float32)], + feature_transformation=_dummy_transformation(), + mode="python", + ) + assert odfv.version == "latest" + + def test_odfv_proto_roundtrip(self): + from feast.data_source import RequestSource + from feast.field import Field + from feast.on_demand_feature_view import OnDemandFeatureView + from feast.types import Float32 + + request_source = RequestSource( + name="vals_to_add", + schema=[ + Field(name="val_to_add", dtype=Float32), + Field(name="val_to_add_2", dtype=Float32), + ], + ) + odfv = OnDemandFeatureView( + name="test_odfv", + sources=[request_source], + schema=[Field(name="output", dtype=Float32)], + feature_transformation=_dummy_transformation(), + mode="python", + version="v1", + ) + odfv.current_version_number = 1 + + proto = odfv.to_proto() + assert proto.spec.version == "v1" + assert proto.meta.current_version_number == 1 + + odfv2 = OnDemandFeatureView.from_proto(proto) + assert odfv2.version == "v1" + assert odfv2.current_version_number == 1 + + +class TestParseFeatureRef: + def test_bare_ref(self): + fv, version, feat = _parse_feature_ref("driver_stats:trips") + assert fv == "driver_stats" + assert version is None + assert feat == "trips" + + def test_versioned_ref(self): + fv, version, feat = _parse_feature_ref("driver_stats@v2:trips") + assert fv == "driver_stats" + assert version == 2 + assert feat == "trips" + + def test_latest_ref(self): + fv, version, feat = _parse_feature_ref("driver_stats@latest:trips") + assert fv == "driver_stats" + assert version is None + assert feat == "trips" + + def test_v0_ref(self): + fv, version, feat = _parse_feature_ref("driver_stats@v0:trips") + assert fv == "driver_stats" + assert version == 0 + assert feat == "trips" + + def test_uppercase_v(self): + fv, version, feat = _parse_feature_ref("driver_stats@V3:trips") + assert fv == "driver_stats" + assert version == 3 + assert feat == "trips" + + def test_invalid_no_colon(self): + with pytest.raises(ValueError, match="Invalid feature reference"): + _parse_feature_ref("driver_stats_trips") + + def test_unrecognized_version_falls_back(self): + """Unrecognized version format falls back to treating full fv_part as the name.""" + fv, version, feat = _parse_feature_ref("driver_stats@abc:trips") + assert fv == "driver_stats@abc" + assert version is None + assert feat == "trips" + + def test_empty_version(self): + fv, version, feat = _parse_feature_ref("driver_stats@:trips") + assert fv == "driver_stats" + assert version is None + assert feat == "trips" + + def test_at_sign_in_fv_name_falls_back_gracefully(self): + """Legacy FV name with @ falls back to treating whole pre-colon string as name.""" + fv, version, feat = _parse_feature_ref("my@weird:feature") + assert fv == "my@weird" + assert version is None + assert feat == "feature" + + def test_at_sign_with_valid_version_still_parses(self): + """A valid @v suffix still parses as a version.""" + fv, version, feat = _parse_feature_ref("stats@v2:trips") + assert fv == "stats" + assert version == 2 + assert feat == "trips" + + +class TestEnsureValidRejectsReservedChars: + def test_at_sign_in_name_raises(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="my@weird_fv", + entities=[entity], + ttl=timedelta(days=1), + ) + with pytest.raises(ValueError, match="must not contain '@'"): + fv.ensure_valid() + + def test_colon_in_name_raises(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="my:weird_fv", + entities=[entity], + ttl=timedelta(days=1), + ) + with pytest.raises(ValueError, match="must not contain ':'"): + fv.ensure_valid() + + def test_valid_name_passes(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="driver_stats", + entities=[entity], + ttl=timedelta(days=1), + ) + fv.ensure_valid() # Should not raise + + +class TestStripVersionFromRef: + def test_bare_ref(self): + assert _strip_version_from_ref("driver_stats:trips") == "driver_stats:trips" + + def test_versioned_ref(self): + assert _strip_version_from_ref("driver_stats@v2:trips") == "driver_stats:trips" + + def test_latest_ref(self): + assert ( + _strip_version_from_ref("driver_stats@latest:trips") == "driver_stats:trips" + ) + + +class TestTableId: + def test_no_version(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + from feast.infra.online_stores.sqlite import _table_id + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + ) + assert _table_id("my_project", fv) == "my_project_test_fv" + + def test_v0_no_suffix(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + from feast.infra.online_stores.sqlite import _table_id + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + ) + fv.current_version_number = 0 + assert _table_id("my_project", fv) == "my_project_test_fv" + + def test_v1_with_suffix(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + from feast.infra.online_stores.sqlite import _table_id + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + ) + fv.current_version_number = 1 + assert ( + _table_id("my_project", fv, enable_versioning=True) + == "my_project_test_fv_v1" + ) + + def test_v5_with_suffix(self): + from datetime import timedelta + + from feast.entity import Entity + from feast.feature_view import FeatureView + from feast.infra.online_stores.sqlite import _table_id + + entity = Entity(name="entity_id", join_keys=["entity_id"]) + fv = FeatureView( + name="test_fv", + entities=[entity], + ttl=timedelta(days=1), + ) + fv.current_version_number = 5 + assert ( + _table_id("my_project", fv, enable_versioning=True) + == "my_project_test_fv_v5" + ) + + +class TestValidateFeatureRefsVersioned: + def test_versioned_refs_no_collision_with_full_names(self): + from feast.utils import _validate_feature_refs + + # Different versions of the same feature should not collide with full names + refs = ["driver_stats@v1:trips", "driver_stats@v2:trips"] + _validate_feature_refs(refs, full_feature_names=True) # Should not raise + + def test_versioned_refs_collision_without_full_names(self): + from feast.errors import FeatureNameCollisionError + from feast.utils import _validate_feature_refs + + # Same feature name from different versions collides without full names + refs = ["driver_stats@v1:trips", "driver_stats@v2:trips"] + with pytest.raises(FeatureNameCollisionError): + _validate_feature_refs(refs, full_feature_names=False) + + +def _dummy_transformation(): + from feast.transformation.python_transformation import PythonTransformation + + def identity(features_df): + return features_df + + return PythonTransformation( + udf=identity, + udf_string="def identity(features_df):\n return features_df\n", + ) diff --git a/sdk/python/tests/unit/test_feature_views.py b/sdk/python/tests/unit/test_feature_views.py index 9030e6e0c69..1f3fd043485 100644 --- a/sdk/python/tests/unit/test_feature_views.py +++ b/sdk/python/tests/unit/test_feature_views.py @@ -23,6 +23,18 @@ from feast.utils import _utc_now, make_tzaware +def test_create_feature_view_without_source(): + fv = FeatureView(name="test_no_source", ttl=timedelta(days=1)) + assert fv.batch_source is None + assert fv.stream_source is None + + proto = fv.to_proto() + assert not proto.spec.HasField("batch_source") + + fv_roundtrip = FeatureView.from_proto(proto) + assert fv_roundtrip.batch_source is None + + def test_create_feature_view_with_conflicting_entities(): user1 = Entity(name="user1", join_keys=["user_id"]) user2 = Entity(name="user2", join_keys=["user_id"]) @@ -48,7 +60,7 @@ def test_create_batch_feature_view(): udf=lambda x: x, ) - with pytest.raises(TypeError): + with pytest.raises(ValueError): BatchFeatureView( name="test batch feature view", entities=[], ttl=timedelta(days=30) ) @@ -530,3 +542,58 @@ def simple_udf(df: pd.DataFrame) -> pd.DataFrame: assert deserialized.feature_transformation is not None, ( "Expected transformation to be present" ) + + +def test_feature_view_org_field(): + """Test that the optional `org` field is stored, serialized, and round-trips correctly.""" + file_source = FileSource(name="my-file-source", path="test.parquet") + + # org is optional — defaults to empty string + fv_no_org = FeatureView( + name="fv-no-org", + entities=[], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + ) + assert fv_no_org.org == "" + + # org can be set explicitly + fv_with_org = FeatureView( + name="fv-with-org", + entities=[], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + org="ads", + ) + assert fv_with_org.org == "ads" + + # org is serialized to proto + proto = fv_with_org.to_proto() + assert proto.spec.org == "ads" + + # org survives a proto round-trip + roundtripped = FeatureView.from_proto(proto) + assert roundtripped.org == "ads" + + # a view without org round-trips to empty string + proto_no_org = fv_no_org.to_proto() + assert proto_no_org.spec.org == "" + roundtripped_no_org = FeatureView.from_proto(proto_no_org) + assert roundtripped_no_org.org == "" + + # equality respects org + fv_org_a = FeatureView( + name="fv-eq", + entities=[], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + org="ads", + ) + fv_org_b = FeatureView( + name="fv-eq", + entities=[], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + org="search", + ) + assert fv_org_a != fv_org_b diff --git a/sdk/python/tests/unit/test_filter_models.py b/sdk/python/tests/unit/test_filter_models.py new file mode 100644 index 00000000000..d5db20a1330 --- /dev/null +++ b/sdk/python/tests/unit/test_filter_models.py @@ -0,0 +1,160 @@ +import pytest +from pydantic import ValidationError + +from feast.filter_models import ( + ComparisonFilter, + CompoundFilter, + convert_dict_to_filter, +) + + +class TestComparisonFilter: + @pytest.mark.parametrize("op", ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"]) + def test_valid_operators(self, op): + f = ComparisonFilter(type=op, key="field", value="x") + assert f.type == op + assert f.key == "field" + + def test_rejects_invalid_operator(self): + with pytest.raises(ValidationError): + ComparisonFilter(type="like", key="field", value="x") + + def test_accepts_string_value(self): + f = ComparisonFilter(type="eq", key="city", value="LA") + assert f.value == "LA" + + def test_accepts_int_value(self): + f = ComparisonFilter(type="gt", key="age", value=25) + assert f.value == 25 + + def test_accepts_float_value(self): + f = ComparisonFilter(type="lte", key="score", value=0.95) + assert f.value == 0.95 + + def test_accepts_bool_value(self): + f = ComparisonFilter(type="eq", key="active", value=True) + assert f.value is True + + def test_accepts_list_value(self): + f = ComparisonFilter(type="in", key="status", value=["a", "b"]) + assert f.value == ["a", "b"] + + +class TestCompoundFilter: + def test_and_filter(self): + f = CompoundFilter( + type="and", + filters=[ + ComparisonFilter(type="eq", key="a", value=1), + ComparisonFilter(type="gt", key="b", value=2), + ], + ) + assert f.type == "and" + assert len(f.filters) == 2 + + def test_or_filter(self): + f = CompoundFilter( + type="or", + filters=[ + ComparisonFilter(type="eq", key="a", value=1), + ComparisonFilter(type="eq", key="b", value=2), + ], + ) + assert f.type == "or" + assert len(f.filters) == 2 + + def test_rejects_invalid_type(self): + with pytest.raises(ValidationError): + CompoundFilter( + type="xor", + filters=[ComparisonFilter(type="eq", key="a", value=1)], + ) + + def test_nested_compound(self): + f = CompoundFilter( + type="and", + filters=[ + ComparisonFilter(type="eq", key="x", value=1), + CompoundFilter( + type="or", + filters=[ + ComparisonFilter(type="gt", key="y", value=5), + ComparisonFilter(type="lt", key="z", value=10), + ], + ), + ], + ) + assert f.type == "and" + assert len(f.filters) == 2 + inner = f.filters[1] + assert isinstance(inner, CompoundFilter) + assert inner.type == "or" + assert len(inner.filters) == 2 + + +class TestConvertDictToFilter: + def test_comparison_dict(self): + result = convert_dict_to_filter({"type": "eq", "key": "city", "value": "LA"}) + assert isinstance(result, ComparisonFilter) + assert result.type == "eq" + assert result.key == "city" + assert result.value == "LA" + + def test_compound_dict(self): + result = convert_dict_to_filter( + { + "type": "and", + "filters": [ + {"type": "eq", "key": "a", "value": 1}, + {"type": "gt", "key": "b", "value": 2}, + ], + } + ) + assert isinstance(result, CompoundFilter) + assert result.type == "and" + assert len(result.filters) == 2 + assert all(isinstance(f, ComparisonFilter) for f in result.filters) + + def test_nested_compound_dict(self): + result = convert_dict_to_filter( + { + "type": "or", + "filters": [ + {"type": "eq", "key": "x", "value": "a"}, + { + "type": "and", + "filters": [ + {"type": "gt", "key": "y", "value": 5}, + {"type": "lt", "key": "z", "value": 10}, + ], + }, + ], + } + ) + assert isinstance(result, CompoundFilter) + assert result.type == "or" + inner = result.filters[1] + assert isinstance(inner, CompoundFilter) + assert inner.type == "and" + assert len(inner.filters) == 2 + + def test_or_compound_dict(self): + result = convert_dict_to_filter( + { + "type": "or", + "filters": [ + {"type": "eq", "key": "a", "value": 1}, + {"type": "eq", "key": "b", "value": 2}, + ], + } + ) + assert isinstance(result, CompoundFilter) + assert result.type == "or" + + def test_in_operator_dict(self): + result = convert_dict_to_filter( + {"type": "in", "key": "status", "value": ["active", "pending"]} + ) + assert isinstance(result, ComparisonFilter) + assert result.type == "in" + assert result.value == ["active", "pending"] diff --git a/sdk/python/tests/unit/test_grpc_server_write_protos.py b/sdk/python/tests/unit/test_grpc_server_write_protos.py new file mode 100644 index 00000000000..31c3e5c84c8 --- /dev/null +++ b/sdk/python/tests/unit/test_grpc_server_write_protos.py @@ -0,0 +1,159 @@ +import pytest + +from feast.infra.contrib.grpc_server import parse_typed +from feast.protos.feast.serving.GrpcServer_pb2 import ( + PushRequest, + WriteToOnlineStoreRequest, +) +from feast.protos.feast.types.Value_pb2 import ( + Int64List, + Map, + Null, + StringSet, + Value, +) + + +def test_push_request_string_features(): + request = PushRequest( + features={"driver_id": "1001", "conv_rate": "0.5"}, + stream_feature_view="driver_stats", + to="online", + ) + assert request.features["driver_id"] == "1001" + assert request.features["conv_rate"] == "0.5" + assert len(request.typed_features) == 0 + + +def test_push_request_typed_features(): + request = PushRequest( + typed_features={ + "driver_id": Value(int64_val=1001), + "conv_rate": Value(float_val=0.5), + "active": Value(bool_val=True), + "label": Value(string_val="fast"), + }, + stream_feature_view="driver_stats", + to="online", + ) + assert request.typed_features["driver_id"].int64_val == 1001 + assert request.typed_features["conv_rate"].float_val == pytest.approx(0.5) + assert request.typed_features["active"].bool_val is True + assert request.typed_features["label"].string_val == "fast" + assert len(request.features) == 0 + + +def test_push_request_typed_features_val_case(): + """WhichOneof('val') returns the correct field name for each value type.""" + cases = [ + (Value(int32_val=1), "int32_val"), + (Value(int64_val=2), "int64_val"), + (Value(float_val=1.0), "float_val"), + (Value(double_val=2.0), "double_val"), + (Value(bool_val=True), "bool_val"), + (Value(string_val="x"), "string_val"), + ] + for value, expected_case in cases: + assert value.WhichOneof("val") == expected_case + + +def test_write_to_online_store_request_string_features(): + request = WriteToOnlineStoreRequest( + features={"driver_id": "1001", "avg_daily_trips": "10"}, + feature_view_name="driver_hourly_stats", + ) + assert request.features["driver_id"] == "1001" + assert request.features["avg_daily_trips"] == "10" + assert len(request.typed_features) == 0 + + +def test_write_to_online_store_request_typed_features(): + request = WriteToOnlineStoreRequest( + typed_features={ + "driver_id": Value(int64_val=1001), + "avg_daily_trips": Value(int32_val=10), + "conv_rate": Value(float_val=0.42), + }, + feature_view_name="driver_hourly_stats", + ) + assert request.typed_features["driver_id"].int64_val == 1001 + assert request.typed_features["avg_daily_trips"].int32_val == 10 + assert request.typed_features["conv_rate"].float_val == pytest.approx(0.42) + assert len(request.features) == 0 + + +def test_push_request_string_and_typed_features_are_independent(): + """Setting features does not affect typed_features and vice versa.""" + r1 = PushRequest( + features={"driver_id": "1001"}, stream_feature_view="s", to="online" + ) + r2 = PushRequest( + typed_features={"driver_id": Value(int64_val=1001)}, + stream_feature_view="s", + to="online", + ) + assert len(r1.typed_features) == 0 + assert len(r2.features) == 0 + + +def test_write_to_online_store_string_and_typed_features_are_independent(): + r1 = WriteToOnlineStoreRequest( + features={"driver_id": "1001"}, feature_view_name="fv" + ) + r2 = WriteToOnlineStoreRequest( + typed_features={"driver_id": Value(int64_val=1001)}, feature_view_name="fv" + ) + assert len(r1.typed_features) == 0 + assert len(r2.features) == 0 + + +def test_parse_typed_null_val_becomes_none(): + """Value(null_val=NULL) must produce None in the DataFrame, not the integer 0.""" + df = parse_typed( + { + "present": Value(int64_val=42), + "missing": Value(null_val=Null.NULL), + } + ) + assert df["present"].iloc[0] == 42 + assert df["missing"].iloc[0] is None + + +def test_parse_typed_unset_val_becomes_none(): + """A Value with no oneof field set (WhichOneof returns None) must also produce None.""" + df = parse_typed({"empty": Value()}) + assert df["empty"].iloc[0] is None + + +def test_parse_typed_list_val_unwrapped_to_python_list(): + """Compound list values are unwrapped from their protobuf wrapper to a plain list.""" + df = parse_typed( + { + "ids": Value(int64_list_val=Int64List(val=[1, 2, 3])), + } + ) + assert df["ids"].iloc[0] == [1, 2, 3] + + +def test_parse_typed_set_val_unwrapped_to_python_list(): + """Compound set values are unwrapped from their protobuf wrapper to a plain list.""" + df = parse_typed( + { + "tags": Value(string_set_val=StringSet(val=["a", "b"])), + } + ) + assert sorted(df["tags"].iloc[0]) == ["a", "b"] + + +def test_parse_typed_map_val_unwrapped_to_python_dict(): + """Map values are unwrapped from their protobuf Map wrapper to a plain dict.""" + df = parse_typed( + { + "scores": Value( + map_val=Map(val={"x": Value(float_val=1.0), "y": Value(float_val=2.0)}) + ), + } + ) + result = df["scores"].iloc[0] + assert isinstance(result, dict) + assert set(result.keys()) == {"x", "y"} diff --git a/sdk/python/tests/unit/test_image_utils.py b/sdk/python/tests/unit/test_image_utils.py index e635b477ce6..f54077f08cf 100644 --- a/sdk/python/tests/unit/test_image_utils.py +++ b/sdk/python/tests/unit/test_image_utils.py @@ -17,10 +17,7 @@ import pytest from PIL import Image -pytest.importorskip("torch") -pytest.importorskip("timm") -pytest.importorskip("sklearn") - +import feast.image_utils as image_utils from feast.image_utils import ( ImageFeatureExtractor, combine_embeddings, @@ -28,6 +25,36 @@ validate_image_format, ) +torch = pytest.importorskip("torch") +pytest.importorskip("timm") +pytest.importorskip("sklearn") + + +@pytest.fixture(autouse=True) +def mock_timm_model(monkeypatch): + class DummyModel: + def eval(self): + return self + + def __call__(self, input_tensor): + batch_size = input_tensor.shape[0] + return torch.arange(1, 5, dtype=torch.float32).repeat(batch_size, 1) + + def create_model(*_args, **_kwargs): + return DummyModel() + + def create_transform(**_config): + def transform(_image): + return torch.ones((3, 224, 224), dtype=torch.float32) + + return transform + + monkeypatch.setattr(image_utils.timm, "create_model", create_model) + monkeypatch.setattr( + image_utils, "resolve_data_config", lambda *_args, **_kwargs: {} + ) + monkeypatch.setattr(image_utils, "create_transform", create_transform) + class TestImageFeatureExtractor: """Test ImageFeatureExtractor functionality.""" diff --git a/sdk/python/tests/unit/test_label_view.py b/sdk/python/tests/unit/test_label_view.py new file mode 100644 index 00000000000..f08aac72dbb --- /dev/null +++ b/sdk/python/tests/unit/test_label_view.py @@ -0,0 +1,420 @@ +from datetime import timedelta + +import pytest + +from feast.data_source import PushSource +from feast.entity import Entity +from feast.feature_service import FeatureService +from feast.field import Field +from feast.infra.offline_stores.file_source import FileSource +from feast.labeling import ConflictPolicy, LabelView +from feast.protos.feast.core.LabelView_pb2 import ( + ConflictResolutionPolicy as ConflictResolutionPolicyProto, +) +from feast.protos.feast.core.LabelView_pb2 import LabelView as LabelViewProto +from feast.types import Float32, String +from feast.value_type import ValueType + + +def _sample_label_view() -> LabelView: + interaction = Entity( + name="interaction", + join_keys=["interaction_id"], + value_type=ValueType.STRING, + ) + label_push = PushSource( + name="label_push", + batch_source=FileSource(path="labels.parquet", timestamp_field="ts"), + ) + return LabelView( + name="interaction_labels", + source=label_push, + entities=[interaction], + schema=[ + Field(name="interaction_id", dtype=String), + Field(name="reward_label", dtype=String), + Field(name="safety_score", dtype=Float32), + Field(name="labeler", dtype=String), + ], + labeler_field="labeler", + conflict_policy=ConflictPolicy.LAST_WRITE_WINS, + reference_feature_view="interaction_history", + ttl=timedelta(days=90), + online=True, + description="Mutable reward labels", + tags={"team": "safety"}, + owner="safety@example.com", + ) + + +class TestLabelViewCreation: + def test_basic_creation(self): + lv = _sample_label_view() + assert lv.name == "interaction_labels" + assert lv.entities == ["interaction"] + assert len(lv.features) == 3 + assert len(lv.entity_columns) == 1 + assert lv.labeler_field == "labeler" + assert lv.conflict_policy == ConflictPolicy.LAST_WRITE_WINS + assert lv.reference_feature_view == "interaction_history" + assert lv.ttl == timedelta(days=90) + assert lv.online is True + assert lv.description == "Mutable reward labels" + assert lv.tags == {"team": "safety"} + assert lv.owner == "safety@example.com" + + def test_default_values(self): + entity = Entity(name="item", join_keys=["item_id"], value_type=ValueType.STRING) + lv = LabelView( + name="minimal_labels", + entities=[entity], + schema=[ + Field(name="item_id", dtype=String), + Field(name="quality_label", dtype=String), + ], + ) + assert lv.labeler_field == "labeler" + assert lv.conflict_policy == ConflictPolicy.LAST_WRITE_WINS + assert lv.reference_feature_view == "" + assert lv.ttl == timedelta(days=0) + assert lv.online is True + + def test_conflicting_entity_join_keys_raises(self): + e1 = Entity(name="e1", join_keys=["shared_key"]) + e2 = Entity(name="e2", join_keys=["shared_key"]) + with pytest.raises(ValueError, match="share a join key"): + LabelView( + name="bad", + entities=[e1, e2], + schema=[Field(name="shared_key", dtype=String)], + ) + + def test_feature_columns_exclude_entity_columns(self): + lv = _sample_label_view() + feature_names = {f.name for f in lv.features} + entity_names = {f.name for f in lv.entity_columns} + assert feature_names == {"reward_label", "safety_score", "labeler"} + assert entity_names == {"interaction_id"} + assert feature_names.isdisjoint(entity_names) + + +class TestLabelViewProtoRoundtrip: + def test_to_proto(self): + lv = _sample_label_view() + proto = lv.to_proto() + assert isinstance(proto, LabelViewProto) + assert proto.spec.name == "interaction_labels" + assert list(proto.spec.entities) == ["interaction"] + assert len(proto.spec.features) == 3 + assert len(proto.spec.entity_columns) == 1 + assert proto.spec.labeler_field == "labeler" + assert ( + proto.spec.conflict_policy == ConflictResolutionPolicyProto.LAST_WRITE_WINS + ) + assert proto.spec.reference_feature_view == "interaction_history" + assert proto.spec.online is True + assert proto.spec.description == "Mutable reward labels" + assert dict(proto.spec.tags) == {"team": "safety"} + assert proto.spec.owner == "safety@example.com" + + def test_from_proto_roundtrip(self): + lv = _sample_label_view() + proto = lv.to_proto() + lv2 = LabelView.from_proto(proto) + + assert lv2.name == lv.name + assert lv2.entities == lv.entities + assert sorted(f.name for f in lv2.features) == sorted( + f.name for f in lv.features + ) + assert sorted(f.name for f in lv2.entity_columns) == sorted( + f.name for f in lv.entity_columns + ) + assert lv2.labeler_field == lv.labeler_field + assert lv2.conflict_policy == lv.conflict_policy + assert lv2.reference_feature_view == lv.reference_feature_view + assert lv2.ttl == lv.ttl + assert lv2.online == lv.online + assert lv2.description == lv.description + assert lv2.tags == lv.tags + assert lv2.owner == lv.owner + + def test_all_conflict_policies_roundtrip(self): + entity = Entity(name="item", join_keys=["item_id"], value_type=ValueType.STRING) + for policy in ConflictPolicy: + lv = LabelView( + name=f"lv_{policy.name}", + entities=[entity], + schema=[ + Field(name="item_id", dtype=String), + Field(name="label", dtype=String), + ], + conflict_policy=policy, + ) + proto = lv.to_proto() + lv2 = LabelView.from_proto(proto) + assert lv2.conflict_policy == policy + + +class TestLabelViewCopyAndEquality: + def test_copy(self): + lv = _sample_label_view() + lv_copy = lv.__copy__() + assert lv_copy == lv + assert lv_copy is not lv + assert lv_copy.features is not lv.features + + def test_equality_detects_differences(self): + lv1 = _sample_label_view() + lv2 = _sample_label_view() + assert lv1 == lv2 + + lv2_mod = lv2.__copy__() + lv2_mod.labeler_field = "annotator" + assert lv1 != lv2_mod + + def test_equality_type_check(self): + lv = _sample_label_view() + # Cross-type comparison returns False, not TypeError. + assert (lv == "not a label view") is False + assert (lv == 42) is False + + def test_hash_by_name(self): + lv1 = _sample_label_view() + lv2 = _sample_label_view() + assert hash(lv1) == hash(lv2) + + +class TestLabelViewValidation: + def test_ensure_valid_raises_on_empty_name(self): + entity = Entity(name="e", join_keys=["eid"], value_type=ValueType.STRING) + lv = LabelView( + name="", + entities=[entity], + schema=[Field(name="eid", dtype=String)], + ) + with pytest.raises(ValueError, match="needs a name"): + lv.ensure_valid() + + def test_ensure_valid_raises_on_no_entities(self): + lv = LabelView.__new__(LabelView) + lv.name = "test" + lv.entities = [] + lv.features = [] + lv.entity_columns = [] + lv.tags = {} + with pytest.raises(ValueError, match="no entities"): + lv.ensure_valid() + + +class TestLabelViewProtoClass: + def test_proto_class_property(self): + lv = _sample_label_view() + assert lv.proto_class == LabelViewProto + + +class TestLabelViewInFeatureService: + def test_label_view_in_feature_service(self): + lv = _sample_label_view() + fs = FeatureService( + name="training_service", + features=[lv], + ) + assert len(fs.feature_view_projections) == 1 + assert fs.feature_view_projections[0].name == "interaction_labels" + + +class TestLabelViewRegistryRoundtrip: + """Tests that LabelView can be applied/retrieved from the file-based registry proto.""" + + def test_registry_proto_roundtrip(self): + from feast.infra.registry.proto_registry_utils import ( + get_label_view, + list_label_views, + ) + from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto + + lv = _sample_label_view() + proto = lv.to_proto() + proto.spec.project = "test_project" + + registry = RegistryProto() + registry.label_views.append(proto) + + # list + views = list_label_views(registry, "test_project", None) + assert len(views) == 1 + assert views[0].name == "interaction_labels" + assert views[0].labeler_field == "labeler" + assert views[0].conflict_policy == ConflictPolicy.LAST_WRITE_WINS + + # get + lv_got = get_label_view(registry, "interaction_labels", "test_project") + assert lv_got.name == "interaction_labels" + + def test_registry_proto_get_not_found(self): + from feast.errors import FeatureViewNotFoundException + from feast.infra.registry.proto_registry_utils import get_label_view + from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto + + registry = RegistryProto() + with pytest.raises(FeatureViewNotFoundException): + get_label_view(registry, "nonexistent", "test_project") + + def test_list_all_feature_views_includes_label_views(self): + from feast.infra.registry.proto_registry_utils import list_all_feature_views + from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto + + lv = _sample_label_view() + proto = lv.to_proto() + proto.spec.project = "test_project" + + registry = RegistryProto() + registry.label_views.append(proto) + + all_views = list_all_feature_views(registry, "test_project", None) + assert any(isinstance(v, LabelView) for v in all_views) + + def test_get_any_feature_view_finds_label_view(self): + from feast.infra.registry.proto_registry_utils import get_any_feature_view + from feast.protos.feast.core.Registry_pb2 import Registry as RegistryProto + + lv = _sample_label_view() + proto = lv.to_proto() + proto.spec.project = "test_project" + + registry = RegistryProto() + registry.label_views.append(proto) + + found = get_any_feature_view(registry, "interaction_labels", "test_project") + assert isinstance(found, LabelView) + assert found.name == "interaction_labels" + + def test_repo_contents_includes_label_views(self): + from feast.repo_contents import RepoContents + + lv = _sample_label_view() + rc = RepoContents( + projects=[], + data_sources=[], + feature_views=[], + on_demand_feature_views=[], + stream_feature_views=[], + label_views=[lv], + entities=[], + feature_services=[], + permissions=[], + ) + reg_proto = rc.to_registry_proto() + assert len(reg_proto.label_views) == 1 + assert reg_proto.label_views[0].spec.name == "interaction_labels" + + def test_feast_object_type_label_view(self): + from feast.infra.registry.registry import FeastObjectType + + assert FeastObjectType.LABEL_VIEW.value == "label view" + + +class TestLabelViewBatchSourceProperty: + """Tests for the batch_source / stream_source compatibility properties.""" + + def test_batch_source_from_push_source(self): + lv = _sample_label_view() + assert lv.batch_source is not None + assert isinstance(lv.batch_source, FileSource) + + def test_stream_source_from_push_source(self): + lv = _sample_label_view() + assert lv.stream_source is not None + assert isinstance(lv.stream_source, PushSource) + + def test_batch_source_from_file_source(self): + entity = Entity(name="item", join_keys=["item_id"], value_type=ValueType.STRING) + fs = FileSource(path="data.parquet", timestamp_field="ts") + lv = LabelView( + name="file_labels", + entities=[entity], + schema=[ + Field(name="item_id", dtype=String), + Field(name="label", dtype=String), + ], + source=fs, + ) + assert lv.batch_source is fs + assert lv.stream_source is None + + def test_batch_source_none_when_no_source(self): + entity = Entity(name="item", join_keys=["item_id"], value_type=ValueType.STRING) + lv = LabelView( + name="no_source_labels", + entities=[entity], + schema=[ + Field(name="item_id", dtype=String), + Field(name="label", dtype=String), + ], + ) + assert lv.batch_source is None + assert lv.stream_source is None + + +class TestLabelViewFeatureStoreIntegration: + """Tests for FeatureStore integration without needing a real store.""" + + def test_label_view_in_feature_views_list_includes_type(self): + from feast.feature_view import FeatureView as FV + from feast.on_demand_feature_view import OnDemandFeatureView as ODFV + + lv = _sample_label_view() + assert not isinstance(lv, FV) + assert not isinstance(lv, ODFV) + + def test_validate_feature_views_catches_name_conflict(self): + from feast.feature_store import _validate_feature_views + + entity = Entity(name="item", join_keys=["item_id"], value_type=ValueType.STRING) + lv1 = LabelView( + name="my_view", + entities=[entity], + schema=[ + Field(name="item_id", dtype=String), + Field(name="label", dtype=String), + ], + ) + lv2 = LabelView( + name="MY_VIEW", + entities=[entity], + schema=[ + Field(name="item_id", dtype=String), + Field(name="label", dtype=String), + ], + ) + from feast.errors import ConflictingFeatureViewNames + + with pytest.raises(ConflictingFeatureViewNames): + _validate_feature_views([lv1, lv2]) + + def test_materialization_task_accepts_label_view(self): + from datetime import datetime + + from feast.infra.common.materialization_job import MaterializationTask + + lv = _sample_label_view() + task = MaterializationTask( + project="test", + feature_view=lv, + start_time=datetime(2025, 1, 1), + end_time=datetime(2025, 1, 2), + ) + assert task.feature_view.name == "interaction_labels" + + +class TestConflictPolicy: + def test_proto_roundtrip(self): + for policy in ConflictPolicy: + proto_val = policy.to_proto() + assert ConflictPolicy.from_proto(proto_val) == policy + + def test_enum_values(self): + assert ConflictPolicy.LAST_WRITE_WINS.value == "last_write_wins" + assert ConflictPolicy.LABELER_PRIORITY.value == "labeler_priority" + assert ConflictPolicy.MAJORITY_VOTE.value == "majority_vote" diff --git a/sdk/python/tests/unit/test_materialize_version_forwarding.py b/sdk/python/tests/unit/test_materialize_version_forwarding.py new file mode 100644 index 00000000000..cd7f6b40d61 --- /dev/null +++ b/sdk/python/tests/unit/test_materialize_version_forwarding.py @@ -0,0 +1,199 @@ +# Copyright 2026 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Server tests: version is forwarded on materialize sync and async paths.""" + +from concurrent.futures import Future, ThreadPoolExecutor +from inspect import signature +from unittest.mock import AsyncMock, MagicMock, patch + +from fastapi.testclient import TestClient + +from feast.feature_server import _authorize_materialize_views, get_app +from feast.feature_store import FeatureStore +from feast.feature_view import FeatureViewState + + +def _mock_store_for_materialize(): + fs = MagicMock() + fs.project = "test_project" + fs.initialize = AsyncMock() + fs.close = AsyncMock() + fs._validate_materialize_version.return_value = "v2" + fv = MagicMock() + fv.name = "driver_hourly_stats" + fv.state = FeatureViewState.GENERATED + fs._get_feature_views_to_materialize.return_value = [fv] + fs.registry.get_feature_view.return_value = fv + fs.materialize = MagicMock() + fs.materialize_incremental = MagicMock() + return fs + + +def _run_executor_inline(loop, executor, func, *args): + """Make asyncio run_in_executor execute synchronously for tests.""" + fut: Future = Future() + try: + fut.set_result(func(*args) if args else func()) + except Exception as exc: + fut.set_exception(exc) + return fut + + +def test_run_async_defaults_to_false(): + for method_name in ( + "materialize", + "materialize_incremental", + "_delegate_remote_materialize", + ): + param = signature(getattr(FeatureStore, method_name)).parameters["run_async"] + assert param.default is False, ( + f"{method_name}.run_async default should be False" + ) + + +@patch("feast.feature_server.assert_permissions") +def test_authorize_materialize_views_forwards_version(_mock_perms): + fs = _mock_store_for_materialize() + names = _authorize_materialize_views(fs, ["driver_hourly_stats"], version="v2") + assert names == ["driver_hourly_stats"] + fs._validate_materialize_version.assert_called_once_with( + "v2", ["driver_hourly_stats"] + ) + fs._get_feature_views_to_materialize.assert_called_once_with( + ["driver_hourly_stats"], version="v2" + ) + + +@patch("feast.feature_server.assert_permissions") +def test_materialize_sync_forwards_version(_mock_perms): + fs = _mock_store_for_materialize() + client = TestClient(get_app(fs)) + + response = client.post( + "/materialize", + json={ + "start_ts": "2021-01-01T00:00:00", + "end_ts": "2021-01-02T00:00:00", + "feature_views": ["driver_hourly_stats"], + "version": "v2", + }, + ) + + assert response.status_code == 200 + fs._validate_materialize_version.assert_called_with("v2", ["driver_hourly_stats"]) + fs.materialize.assert_called_once() + _, kwargs = fs.materialize.call_args + assert kwargs.get("version") == "v2" + assert fs.materialize.call_args.args[2] == ["driver_hourly_stats"] + + +@patch("feast.feature_server.assert_permissions") +def test_materialize_incremental_sync_forwards_version(_mock_perms): + fs = _mock_store_for_materialize() + client = TestClient(get_app(fs)) + + response = client.post( + "/materialize-incremental", + json={ + "end_ts": "2021-01-02T00:00:00", + "feature_views": ["driver_hourly_stats"], + "version": "v2", + }, + ) + + assert response.status_code == 200 + fs._validate_materialize_version.assert_called_with("v2", ["driver_hourly_stats"]) + fs.materialize_incremental.assert_called_once() + _, kwargs = fs.materialize_incremental.call_args + assert kwargs.get("version") == "v2" + assert fs.materialize_incremental.call_args.args[1] == ["driver_hourly_stats"] + + +@patch("feast.feature_server.assert_permissions") +def test_materialize_async_uses_dedicated_executor(_mock_perms): + """Async materialize must not use the shared default pool (executor=None).""" + fs = _mock_store_for_materialize() + seen: dict = {} + + def capture_executor(loop, executor, func, *args): + seen["executor"] = executor + return _run_executor_inline(loop, executor, func, *args) + + with patch( + "asyncio.BaseEventLoop.run_in_executor", + new=capture_executor, + ): + client = TestClient(get_app(fs)) + response = client.post( + "/materialize?async=true", + json={ + "start_ts": "2021-01-01T00:00:00", + "end_ts": "2021-01-02T00:00:00", + "feature_views": ["driver_hourly_stats"], + }, + ) + + assert response.status_code == 202 + assert seen.get("executor") is not None + assert isinstance(seen["executor"], ThreadPoolExecutor) + assert "feast-materialize" in seen["executor"]._thread_name_prefix + + +@patch( + "asyncio.BaseEventLoop.run_in_executor", + new=_run_executor_inline, +) +@patch("feast.feature_server.assert_permissions") +def test_materialize_async_forwards_version(_mock_perms): + fs = _mock_store_for_materialize() + client = TestClient(get_app(fs)) + + response = client.post( + "/materialize?async=true", + json={ + "start_ts": "2021-01-01T00:00:00", + "end_ts": "2021-01-02T00:00:00", + "feature_views": ["driver_hourly_stats"], + "version": "v2", + }, + ) + + assert response.status_code == 202 + fs.materialize.assert_called_once() + _, kwargs = fs.materialize.call_args + assert kwargs.get("version") == "v2" + + +@patch( + "asyncio.BaseEventLoop.run_in_executor", + new=_run_executor_inline, +) +@patch("feast.feature_server.assert_permissions") +def test_materialize_incremental_async_forwards_version(_mock_perms): + fs = _mock_store_for_materialize() + client = TestClient(get_app(fs)) + + response = client.post( + "/materialize-incremental?async=true", + json={ + "end_ts": "2021-01-02T00:00:00", + "feature_views": ["driver_hourly_stats"], + "version": "v2", + }, + ) + + assert response.status_code == 202 + fs.materialize_incremental.assert_called_once() + _, kwargs = fs.materialize_incremental.call_args + assert kwargs.get("version") == "v2" diff --git a/sdk/python/tests/unit/test_metrics.py b/sdk/python/tests/unit/test_metrics.py new file mode 100644 index 00000000000..a3a65ebe782 --- /dev/null +++ b/sdk/python/tests/unit/test_metrics.py @@ -0,0 +1,1807 @@ +# Copyright 2025 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock, patch + +import pytest + +from feast.metrics import ( + emit_offline_audit_log, + emit_online_audit_log, + feature_freshness_seconds, + materialization_duration_seconds, + materialization_result_total, + offline_store_request_latency_seconds, + offline_store_request_total, + offline_store_row_count, + online_features_entity_count, + online_features_request_count, + online_features_status_total, + online_store_read_duration_seconds, + push_request_count, + request_count, + request_latency, + track_feature_statuses, + track_materialization, + track_online_features_entities, + track_online_store_read, + track_push, + track_request_latency, + track_transformation, + track_write_transformation, + transformation_duration_seconds, + update_feature_freshness, + write_transformation_duration_seconds, +) + + +def _all_enabled_flags(): + """Return a _MetricsFlags with every category enabled.""" + import feast.metrics as m + + return m._MetricsFlags( + enabled=True, + resource=True, + request=True, + online_features=True, + push=True, + materialization=True, + freshness=True, + offline_features=True, + audit_logging=True, + ) + + +@pytest.fixture(autouse=True) +def _enable_metrics(): + """Enable all metric categories for each test, then restore.""" + import feast.metrics as m + + original = m._config + m._config = _all_enabled_flags() + yield + m._config = original + + +class TestTrackRequestLatency: + def test_success_increments_counter_and_records_latency(self): + before_count = request_count.labels( + endpoint="/test", status="success" + )._value.get() + + with track_request_latency("/test"): + pass + + after_count = request_count.labels( + endpoint="/test", status="success" + )._value.get() + assert after_count == before_count + 1 + + def test_error_increments_error_counter(self): + before_count = request_count.labels( + endpoint="/test-err", status="error" + )._value.get() + + with pytest.raises(ValueError): + with track_request_latency("/test-err"): + raise ValueError("boom") + + after_count = request_count.labels( + endpoint="/test-err", status="error" + )._value.get() + assert after_count == before_count + 1 + + def test_latency_is_recorded(self): + before_sum = request_latency.labels( + endpoint="/test-latency", feature_count="", feature_view_count="" + )._sum.get() + + with track_request_latency("/test-latency"): + import time + + time.sleep(0.01) + + after_sum = request_latency.labels( + endpoint="/test-latency", feature_count="", feature_view_count="" + )._sum.get() + assert after_sum > before_sum + + def test_feature_count_and_feature_view_count_labels(self): + """Latency histogram carries feature_count and feature_view_count labels.""" + label_set = dict( + endpoint="/get-online-features", + feature_count="5", + feature_view_count="2", + ) + before_sum = request_latency.labels(**label_set)._sum.get() + + with track_request_latency( + "/get-online-features", feature_count="5", feature_view_count="2" + ): + pass + + after_sum = request_latency.labels(**label_set)._sum.get() + assert after_sum > before_sum + + def test_default_labels_are_empty_string(self): + """Non-online-features endpoints get empty-string labels by default.""" + label_set = dict( + endpoint="/materialize", feature_count="", feature_view_count="" + ) + before_sum = request_latency.labels(**label_set)._sum.get() + + with track_request_latency("/materialize"): + pass + + after_sum = request_latency.labels(**label_set)._sum.get() + assert after_sum > before_sum + + def test_labels_updated_via_yielded_context(self): + """Labels set on the yielded context are used in the final metrics.""" + label_set = dict( + endpoint="/ctx-update", feature_count="3", feature_view_count="1" + ) + before_sum = request_latency.labels(**label_set)._sum.get() + + with track_request_latency("/ctx-update") as ctx: + ctx.feature_count = "3" + ctx.feature_view_count = "1" + + after_sum = request_latency.labels(**label_set)._sum.get() + assert after_sum > before_sum + + def test_error_before_labels_set_still_records(self): + """Errors before labels are updated still record with default labels.""" + before_count = request_count.labels( + endpoint="/early-fail", status="error" + )._value.get() + + with pytest.raises(RuntimeError): + with track_request_latency("/early-fail") as _ctx: + raise RuntimeError("auth failed") + + after_count = request_count.labels( + endpoint="/early-fail", status="error" + )._value.get() + assert after_count == before_count + 1 + + recorded_sum = request_latency.labels( + endpoint="/early-fail", feature_count="", feature_view_count="" + )._sum.get() + assert recorded_sum > 0 + + +class TestMetricsOptIn: + """Verify that when a category is disabled, its helpers are true no-ops.""" + + @staticmethod + def _all_off(): + import feast.metrics as m + + m._config = m._MetricsFlags() # everything False + + def test_track_request_latency_noop_when_disabled(self): + self._all_off() + label_set = dict( + endpoint="/disabled-test", feature_count="", feature_view_count="" + ) + before_sum = request_latency.labels(**label_set)._sum.get() + + with track_request_latency("/disabled-test"): + pass + + assert request_latency.labels(**label_set)._sum.get() == before_sum + + def test_track_online_features_entities_noop_when_disabled(self): + self._all_off() + before = online_features_request_count._value.get() + track_online_features_entities(100) + assert online_features_request_count._value.get() == before + + def test_track_push_noop_when_disabled(self): + self._all_off() + before = push_request_count.labels( + push_source="src", mode="online" + )._value.get() + track_push("src", "online") + assert ( + push_request_count.labels(push_source="src", mode="online")._value.get() + == before + ) + + def test_track_materialization_noop_when_disabled(self): + self._all_off() + before = materialization_result_total.labels( + feature_view="fv_disabled", status="success" + )._value.get() + track_materialization("fv_disabled", success=True, duration_seconds=1.0) + assert ( + materialization_result_total.labels( + feature_view="fv_disabled", status="success" + )._value.get() + == before + ) + + +class TestGranularCategoryControl: + """Verify individual category toggles work independently.""" + + def test_request_disabled_but_push_enabled(self): + import feast.metrics as m + + m._config = m._MetricsFlags( + enabled=True, + request=False, + push=True, + resource=True, + online_features=True, + materialization=True, + freshness=True, + ) + + # request should be no-op + label_set = dict( + endpoint="/granular-req", feature_count="", feature_view_count="" + ) + before_req = request_latency.labels(**label_set)._sum.get() + with track_request_latency("/granular-req"): + pass + assert request_latency.labels(**label_set)._sum.get() == before_req + + # push should still record + before_push = push_request_count.labels( + push_source="s", mode="online" + )._value.get() + track_push("s", "online") + assert ( + push_request_count.labels(push_source="s", mode="online")._value.get() + == before_push + 1 + ) + + def test_online_features_disabled_but_materialization_enabled(self): + import feast.metrics as m + + m._config = m._MetricsFlags( + enabled=True, + online_features=False, + materialization=True, + resource=True, + request=True, + push=True, + freshness=True, + ) + + # online_features should be no-op + before_of = online_features_request_count._value.get() + track_online_features_entities(50) + assert online_features_request_count._value.get() == before_of + + # materialization should still record + before_mat = materialization_result_total.labels( + feature_view="fv_gran", status="success" + )._value.get() + track_materialization("fv_gran", success=True, duration_seconds=1.0) + assert ( + materialization_result_total.labels( + feature_view="fv_gran", status="success" + )._value.get() + == before_mat + 1 + ) + + def test_only_resource_enabled(self): + """When only resource is on, all request-path helpers are no-ops.""" + import feast.metrics as m + + m._config = m._MetricsFlags( + enabled=True, + resource=True, + request=False, + online_features=False, + push=False, + materialization=False, + freshness=False, + ) + + label_set = dict(endpoint="/res-only", feature_count="", feature_view_count="") + before_req = request_latency.labels(**label_set)._sum.get() + before_of = online_features_request_count._value.get() + before_push = push_request_count.labels( + push_source="x", mode="offline" + )._value.get() + before_mat = materialization_result_total.labels( + feature_view="fv_res", status="success" + )._value.get() + + with track_request_latency("/res-only"): + pass + track_online_features_entities(10) + track_push("x", "offline") + track_materialization("fv_res", success=True, duration_seconds=1.0) + + assert request_latency.labels(**label_set)._sum.get() == before_req + assert online_features_request_count._value.get() == before_of + assert ( + push_request_count.labels(push_source="x", mode="offline")._value.get() + == before_push + ) + assert ( + materialization_result_total.labels( + feature_view="fv_res", status="success" + )._value.get() + == before_mat + ) + + +class TestInitWorkerFreshnessMonitoring: + """init_worker_freshness_monitoring mirrors init_worker_monitoring's gating.""" + + def test_starts_daemon_thread_when_freshness_enabled(self): + import feast.metrics as m + + m._config = m._MetricsFlags( + enabled=True, + resource=False, + request=False, + online_features=False, + push=False, + materialization=False, + freshness=True, + ) + mock_store = MagicMock() + + with patch("feast.metrics.threading.Thread") as mock_thread_cls: + m.init_worker_freshness_monitoring(mock_store) + + mock_thread_cls.assert_called_once_with( + target=m.monitor_freshness, args=(mock_store, 30), daemon=True + ) + mock_thread_cls.return_value.start.assert_called_once() + + def test_does_not_start_thread_when_freshness_disabled(self): + import feast.metrics as m + + m._config = m._MetricsFlags( + enabled=True, + resource=False, + request=False, + online_features=False, + push=False, + materialization=False, + freshness=False, + ) + mock_store = MagicMock() + + with patch("feast.metrics.threading.Thread") as mock_thread_cls: + m.init_worker_freshness_monitoring(mock_store) + + mock_thread_cls.assert_not_called() + + +class TestMetricsYamlConfig: + """Verify metrics config in feature_store.yaml is respected. + + We mock out everything past the metrics-gate check in ``start_server`` + so these tests never actually launch a real HTTP server. + """ + + @staticmethod + def _call_start_server(mock_store, cli_metrics: bool): + """Call start_server with enough mocking to avoid side-effects.""" + from feast.feature_server import start_server + + with ( + patch("feast.feature_server.feast_metrics") as mock_fm, + patch("feast.feature_server.str_to_auth_manager_type"), + patch("feast.feature_server.init_security_manager"), + patch("feast.feature_server.init_auth_manager"), + patch( + "feast.feature_server.FeastServeApplication", + side_effect=RuntimeError("stop"), + ) + if hasattr(__import__("sys"), "platform") + and __import__("sys").platform != "win32" + else patch("uvicorn.run", side_effect=RuntimeError("stop")), + ): + try: + start_server( + store=mock_store, + host="127.0.0.1", + port=6566, + no_access_log=True, + workers=1, + worker_connections=1000, + max_requests=1000, + max_requests_jitter=50, + keep_alive_timeout=30, + registry_ttl_sec=60, + tls_key_path="", + tls_cert_path="", + metrics=cli_metrics, + ) + except (RuntimeError, Exception): + pass + return mock_fm + + def test_metrics_enabled_from_yaml_config(self): + """start_server enables metrics when config has metrics.enabled=True, + even though the CLI flag is False.""" + from types import SimpleNamespace + + metrics_cfg = SimpleNamespace(enabled=True) + fs_cfg = SimpleNamespace(metrics=metrics_cfg) + mock_store = MagicMock() + mock_store.config = SimpleNamespace(feature_server=fs_cfg) + + mock_fm = self._call_start_server(mock_store, cli_metrics=False) + mock_fm.build_metrics_flags.assert_called_once_with(metrics_cfg) + mock_fm.start_metrics_server.assert_called_once() + + def test_cli_flag_enables_metrics_without_yaml_config(self): + """start_server enables metrics when --metrics is passed even without + any feature_server config section.""" + from types import SimpleNamespace + + mock_store = MagicMock() + mock_store.config = SimpleNamespace(feature_server=None) + + mock_fm = self._call_start_server(mock_store, cli_metrics=True) + mock_fm.build_metrics_flags.assert_called_once_with(None) + mock_fm.start_metrics_server.assert_called_once() + + def test_metrics_not_started_when_both_disabled(self): + """start_server does NOT start metrics when neither CLI nor config enables it.""" + from types import SimpleNamespace + + mock_store = MagicMock() + mock_store.config = SimpleNamespace( + feature_server=SimpleNamespace(metrics=SimpleNamespace(enabled=False)), + ) + + mock_fm = self._call_start_server(mock_store, cli_metrics=False) + mock_fm.start_metrics_server.assert_not_called() + + def test_metrics_not_started_when_config_is_none(self): + """start_server does NOT start metrics when feature_server config is None + and CLI flag is also False.""" + from types import SimpleNamespace + + mock_store = MagicMock() + mock_store.config = SimpleNamespace(feature_server=None) + + mock_fm = self._call_start_server(mock_store, cli_metrics=False) + mock_fm.start_metrics_server.assert_not_called() + + def test_freshness_monitoring_deferred_same_as_resource_monitoring(self): + """start_freshness_monitoring must mirror start_resource_monitoring exactly.""" + from types import SimpleNamespace + + mock_store = MagicMock() + mock_store.config = SimpleNamespace( + feature_server=SimpleNamespace(metrics=SimpleNamespace(enabled=True)), + ) + + mock_fm = self._call_start_server(mock_store, cli_metrics=False) + _, kwargs = mock_fm.start_metrics_server.call_args + assert ( + kwargs["start_resource_monitoring"] == kwargs["start_freshness_monitoring"] + ) + + +class TestTrackOnlineFeaturesEntities: + def test_increments_request_count(self): + before = online_features_request_count._value.get() + track_online_features_entities(10) + assert online_features_request_count._value.get() == before + 1 + + def test_records_entity_count(self): + before_count = online_features_entity_count._sum.get() + track_online_features_entities(42) + assert online_features_entity_count._sum.get() >= before_count + 42 + + +class TestTrackFeatureStatuses: + def test_increments_present_and_not_found(self): + before_present = online_features_status_total.labels( + feature_view="fv_status", status="present" + )._value.get() + before_not_found = online_features_status_total.labels( + feature_view="fv_status", status="not_found" + )._value.get() + + track_feature_statuses("fv_status", present_count=3, not_found_count=2) + + assert ( + online_features_status_total.labels( + feature_view="fv_status", status="present" + )._value.get() + == before_present + 3 + ) + assert ( + online_features_status_total.labels( + feature_view="fv_status", status="not_found" + )._value.get() + == before_not_found + 2 + ) + + @patch("feast.metrics.track_feature_statuses") + def test_populate_response_passes_correct_counts(self, mock_track): + """Integration: _populate_response_from_feature_data computes and + forwards the right present/not_found counts. + + 3 entities, 2 features each: + Entity 0 — both present (2 PRESENT) + Entity 1 — missing (2 NOT_FOUND) + Entity 2 — partial (1 PRESENT, 1 NOT_FOUND) + """ + from feast.protos.feast.serving.ServingService_pb2 import ( + GetOnlineFeaturesResponse, + ) + from feast.protos.feast.types.Value_pb2 import Value as ValueProto + from feast.utils import _populate_response_from_feature_data + + now = datetime.now(tz=timezone.utc) + val = ValueProto(int64_val=1) + table = MagicMock() + table.name = "driver_fv" + table.projection.name_to_use.return_value = "driver_fv" + table.projection.name_alias = None + table.projection.name = "driver_fv" + + _populate_response_from_feature_data( + requested_features=["feat_a", "feat_b"], + read_rows=[ + (now, {"feat_a": val, "feat_b": val}), + (None, None), + (now, {"feat_a": val}), + ], + indexes=([0], [1], [2]), + online_features_response=GetOnlineFeaturesResponse(), + full_feature_names=False, + table=table, + output_len=3, + ) + + mock_track.assert_called_once_with("driver_fv", 3, 3) + + +class TestTrackPush: + def test_increments_push_counter(self): + before = push_request_count.labels( + push_source="my_source", mode="online" + )._value.get() + track_push("my_source", "online") + assert ( + push_request_count.labels( + push_source="my_source", mode="online" + )._value.get() + == before + 1 + ) + + +class TestTrackMaterialization: + def test_success_counter(self): + before = materialization_result_total.labels( + feature_view="fv1", status="success" + )._value.get() + track_materialization("fv1", success=True, duration_seconds=1.5) + assert ( + materialization_result_total.labels( + feature_view="fv1", status="success" + )._value.get() + == before + 1 + ) + + def test_failure_counter(self): + before = materialization_result_total.labels( + feature_view="fv2", status="failure" + )._value.get() + track_materialization("fv2", success=False, duration_seconds=0.5) + assert ( + materialization_result_total.labels( + feature_view="fv2", status="failure" + )._value.get() + == before + 1 + ) + + def test_duration_histogram(self): + before_sum = materialization_duration_seconds.labels( + feature_view="fv3" + )._sum.get() + track_materialization("fv3", success=True, duration_seconds=3.7) + after_sum = materialization_duration_seconds.labels( + feature_view="fv3" + )._sum.get() + assert pytest.approx(after_sum - before_sum, abs=0.01) == 3.7 + + +class TestUpdateFeatureFreshness: + def test_sets_freshness_for_materialized_views(self): + mock_fv = MagicMock() + mock_fv.name = "test_fv" + mock_fv.most_recent_end_time = datetime.now(tz=timezone.utc) - timedelta( + minutes=5 + ) + + mock_sfv = MagicMock() + mock_sfv.name = "test_sfv" + mock_sfv.most_recent_end_time = datetime.now(tz=timezone.utc) - timedelta( + minutes=5 + ) + + mock_store = MagicMock() + mock_store.project = "test_project" + mock_store.list_feature_views.return_value = [mock_fv] + mock_store.list_stream_feature_views.return_value = [mock_sfv] + + update_feature_freshness(mock_store) + + staleness = feature_freshness_seconds.labels( + feature_view="test_fv", project="test_project" + )._value.get() + assert 280 < staleness < 320 + + sfv_staleness = feature_freshness_seconds.labels( + feature_view="test_sfv", project="test_project" + )._value.get() + assert 280 < sfv_staleness < 320 + + def test_skips_unmaterialized_views(self): + mock_fv = MagicMock() + mock_fv.name = "unmaterialized_fv" + mock_fv.most_recent_end_time = None + + mock_store = MagicMock() + mock_store.project = "test_project" + mock_store.list_feature_views.return_value = [mock_fv] + mock_store.list_stream_feature_views.return_value = [] + + update_feature_freshness(mock_store) + + def test_handles_naive_datetime(self): + mock_fv = MagicMock() + mock_fv.name = "naive_fv" + # Simulate a naive UTC datetime (no tzinfo), as Feast typically stores + naive_utc_now = datetime.now(tz=timezone.utc).replace(tzinfo=None) + mock_fv.most_recent_end_time = naive_utc_now - timedelta(hours=1) + + mock_store = MagicMock() + mock_store.project = "test_project" + mock_store.list_feature_views.return_value = [mock_fv] + mock_store.list_stream_feature_views.return_value = [] + + update_feature_freshness(mock_store) + + staleness = feature_freshness_seconds.labels( + feature_view="naive_fv", project="test_project" + )._value.get() + assert 3500 < staleness < 3700 + + def test_handles_registry_errors_gracefully(self): + mock_store = MagicMock() + mock_store.list_feature_views.side_effect = Exception("registry down") + + update_feature_freshness(mock_store) + + +class TestResolveFeatureCounts: + """Verify _resolve_feature_counts for both feature-ref lists and FeatureService.""" + + def test_feature_ref_list(self): + from feast.feature_server import _resolve_feature_counts + + refs = ["driver_fv:conv_rate", "driver_fv:acc_rate", "vehicle_fv:mileage"] + feat_count, fv_count = _resolve_feature_counts(refs) + assert feat_count == "3" + assert fv_count == "2" + + def test_single_feature_view(self): + from feast.feature_server import _resolve_feature_counts + + refs = ["fv1:a", "fv1:b", "fv1:c"] + feat_count, fv_count = _resolve_feature_counts(refs) + assert feat_count == "3" + assert fv_count == "1" + + def test_empty_list(self): + from feast.feature_server import _resolve_feature_counts + + feat_count, fv_count = _resolve_feature_counts([]) + assert feat_count == "0" + assert fv_count == "0" + + def test_feature_service(self): + from feast.feature_server import _resolve_feature_counts + + proj1 = MagicMock() + proj1.features = [MagicMock(), MagicMock()] + proj2 = MagicMock() + proj2.features = [MagicMock()] + + fs_svc = MagicMock() + fs_svc.feature_view_projections = [proj1, proj2] + + from feast.feature_service import FeatureService + + fs_svc.__class__ = FeatureService + + feat_count, fv_count = _resolve_feature_counts(fs_svc) + assert feat_count == "3" + assert fv_count == "2" + + +class TestFeatureServerMetricsIntegration: + """Test that feature server endpoints record metrics.""" + + @pytest.fixture + def mock_fs_factory(self): + from tests.foo_provider import FooProvider + + def builder(**async_support): + provider = FooProvider.with_async_support(**async_support) + fs = MagicMock() + fs._get_provider.return_value = provider + from feast.online_response import OnlineResponse + from feast.protos.feast.serving.ServingService_pb2 import ( + GetOnlineFeaturesResponse, + ) + + empty_response = OnlineResponse(GetOnlineFeaturesResponse(results=[])) + fs.get_online_features = MagicMock(return_value=empty_response) + fs.push = MagicMock() + fs.get_online_features_async = MagicMock(return_value=empty_response) + fs.push_async = MagicMock() + return fs + + return builder + + def test_get_online_features_records_metrics(self, mock_fs_factory): + from fastapi.testclient import TestClient + + from feast.feature_server import get_app + + fs = mock_fs_factory(online_read=False) + client = TestClient(get_app(fs)) + + before_req = request_count.labels( + endpoint="/get-online-features", status="success" + )._value.get() + before_entity = online_features_request_count._value.get() + + client.post( + "/get-online-features", + json={ + "features": ["fv:feat1"], + "entities": {"id": [1, 2, 3]}, + }, + ) + + assert ( + request_count.labels( + endpoint="/get-online-features", status="success" + )._value.get() + == before_req + 1 + ) + assert online_features_request_count._value.get() == before_entity + 1 + + @pytest.mark.parametrize( + "features,expected_feat_count,expected_fv_count", + [ + (["fv1:a"], "1", "1"), + (["fv1:a", "fv1:b", "fv2:c"], "3", "2"), + ( + ["fv1:a", "fv1:b", "fv2:c", "fv2:d", "fv3:e"], + "5", + "3", + ), + ], + ids=["1_feat_1_fv", "3_feats_2_fvs", "5_feats_3_fvs"], + ) + def test_latency_labels_with_varying_request_sizes( + self, mock_fs_factory, features, expected_feat_count, expected_fv_count + ): + """Verify feature_count and feature_view_count labels change with request size.""" + from fastapi.testclient import TestClient + + from feast.feature_server import get_app + + fs = mock_fs_factory(online_read=False) + client = TestClient(get_app(fs)) + + label_set = dict( + endpoint="/get-online-features", + feature_count=expected_feat_count, + feature_view_count=expected_fv_count, + ) + before_sum = request_latency.labels(**label_set)._sum.get() + + client.post( + "/get-online-features", + json={ + "features": features, + "entities": {"id": [1]}, + }, + ) + + after_sum = request_latency.labels(**label_set)._sum.get() + assert after_sum > before_sum + + def test_push_records_metrics(self, mock_fs_factory): + from fastapi.testclient import TestClient + + from feast.feature_server import get_app + from feast.utils import _utc_now + + fs = mock_fs_factory(online_write=False) + client = TestClient(get_app(fs)) + + before = push_request_count.labels( + push_source="driver_locations_push", mode="online" + )._value.get() + + client.post( + "/push", + json={ + "push_source_name": "driver_locations_push", + "df": { + "driver_lat": [42.0], + "driver_long": ["42.0"], + "driver_id": [123], + "event_timestamp": [str(_utc_now())], + "created_timestamp": [str(_utc_now())], + }, + "to": "online", + }, + ) + + assert ( + push_request_count.labels( + push_source="driver_locations_push", mode="online" + )._value.get() + == before + 1 + ) + + +class TestBuildMetricsFlags: + """Verify build_metrics_flags correctly maps MetricsConfig to _MetricsFlags.""" + + def test_no_config_enables_all(self): + from feast.metrics import build_metrics_flags + + flags = build_metrics_flags(None) + assert flags.enabled is True + assert flags.resource is True + assert flags.request is True + assert flags.online_features is True + assert flags.push is True + assert flags.materialization is True + assert flags.freshness is True + + def test_selective_disable(self): + from types import SimpleNamespace + + from feast.metrics import build_metrics_flags + + mc = SimpleNamespace( + enabled=True, + resource=True, + request=False, + online_features=True, + push=False, + materialization=True, + freshness=False, + ) + flags = build_metrics_flags(mc) + assert flags.enabled is True + assert flags.resource is True + assert flags.request is False + assert flags.online_features is True + assert flags.push is False + assert flags.materialization is True + assert flags.freshness is False + + def test_all_categories_disabled(self): + from types import SimpleNamespace + + from feast.metrics import build_metrics_flags + + mc = SimpleNamespace( + enabled=True, + resource=False, + request=False, + online_features=False, + push=False, + materialization=False, + freshness=False, + ) + flags = build_metrics_flags(mc) + assert flags.enabled is True + assert flags.resource is False + assert flags.request is False + + +class TestCleanupMultiprocessDir: + """Verify the atexit handler only deletes the temp dir in the owner process.""" + + def test_cleanup_skipped_in_forked_child(self, tmp_path): + """Simulate a forked worker: _owns_mp_dir=True but _owner_pid != current PID.""" + import feast.metrics as m + + original_dir = m._prometheus_mp_dir + original_owns = m._owns_mp_dir + original_pid = m._owner_pid + + fake_dir = tmp_path / "feast_metrics_test" + fake_dir.mkdir() + + m._prometheus_mp_dir = str(fake_dir) + m._owns_mp_dir = True + m._owner_pid = -1 # Different from os.getpid() + + try: + m._cleanup_multiprocess_dir() + assert fake_dir.exists(), ( + "Directory should NOT be deleted when _owner_pid != os.getpid()" + ) + finally: + m._prometheus_mp_dir = original_dir + m._owns_mp_dir = original_owns + m._owner_pid = original_pid + + def test_cleanup_runs_in_owner_process(self, tmp_path): + """The owner process (matching PID) should delete the directory.""" + import os + + import feast.metrics as m + + original_dir = m._prometheus_mp_dir + original_owns = m._owns_mp_dir + original_pid = m._owner_pid + + fake_dir = tmp_path / "feast_metrics_test" + fake_dir.mkdir() + + m._prometheus_mp_dir = str(fake_dir) + m._owns_mp_dir = True + m._owner_pid = os.getpid() + + try: + m._cleanup_multiprocess_dir() + assert not fake_dir.exists(), ( + "Directory SHOULD be deleted when _owner_pid == os.getpid()" + ) + finally: + m._prometheus_mp_dir = original_dir + m._owns_mp_dir = original_owns + m._owner_pid = original_pid + + +class TestTrackOnlineStoreRead: + """Tests for the online store read duration metric.""" + + def test_records_duration(self): + before_sum = online_store_read_duration_seconds._sum.get() + + track_online_store_read(0.123) + + assert online_store_read_duration_seconds._sum.get() >= before_sum + 0.123 + + def test_noop_when_online_features_disabled(self): + import feast.metrics as m + + m._config = m._MetricsFlags(enabled=True, online_features=False) + + before_sum = online_store_read_duration_seconds._sum.get() + + track_online_store_read(0.5) + + assert online_store_read_duration_seconds._sum.get() == before_sum + + m._config = m._MetricsFlags( + enabled=True, + resource=True, + request=True, + online_features=True, + push=True, + materialization=True, + freshness=True, + ) + + +class TestTrackTransformation: + """Tests for the ODFV transformation duration metric.""" + + def test_records_python_mode(self): + labels = ("my_odfv", "python") + before = transformation_duration_seconds._metrics.get(labels, None) + before_sum = before._sum.get() if before else 0.0 + + track_transformation("my_odfv", "python", 0.042) + + sample = transformation_duration_seconds._metrics[labels] + assert sample._sum.get() >= before_sum + 0.042 + + def test_records_pandas_mode(self): + labels = ("my_odfv", "pandas") + before = transformation_duration_seconds._metrics.get(labels, None) + before_sum = before._sum.get() if before else 0.0 + + track_transformation("my_odfv", "pandas", 0.15) + + sample = transformation_duration_seconds._metrics[labels] + assert sample._sum.get() >= before_sum + 0.15 + + def test_noop_when_online_features_disabled(self): + import feast.metrics as m + + m._config = m._MetricsFlags(enabled=True, online_features=False) + + labels = ("disabled_odfv", "python") + before = transformation_duration_seconds._metrics.get(labels, None) + before_sum = before._sum.get() if before else 0.0 + + track_transformation("disabled_odfv", "python", 1.0) + + sample = transformation_duration_seconds._metrics.get(labels, None) + after_sum = sample._sum.get() if sample else 0.0 + assert after_sum == before_sum + + m._config = m._MetricsFlags( + enabled=True, + resource=True, + request=True, + online_features=True, + push=True, + materialization=True, + freshness=True, + ) + + def test_multiple_odfvs_tracked_independently(self): + labels_a = ("odfv_a", "python") + labels_b = ("odfv_b", "pandas") + before_a = transformation_duration_seconds._metrics.get(labels_a, None) + before_a_sum = before_a._sum.get() if before_a else 0.0 + before_b = transformation_duration_seconds._metrics.get(labels_b, None) + before_b_sum = before_b._sum.get() if before_b else 0.0 + + track_transformation("odfv_a", "python", 0.01) + track_transformation("odfv_b", "pandas", 0.05) + + sample_a = transformation_duration_seconds._metrics[labels_a] + sample_b = transformation_duration_seconds._metrics[labels_b] + assert sample_a._sum.get() >= before_a_sum + 0.01 + assert sample_b._sum.get() >= before_b_sum + 0.05 + + +class TestTrackWriteTransformation: + """Tests for the write-path ODFV transformation duration metric.""" + + def test_records_python_mode(self): + labels = ("write_odfv", "python") + before = write_transformation_duration_seconds._metrics.get(labels, None) + before_sum = before._sum.get() if before else 0.0 + + track_write_transformation("write_odfv", "python", 0.033) + + sample = write_transformation_duration_seconds._metrics[labels] + assert sample._sum.get() >= before_sum + 0.033 + + def test_records_pandas_mode(self): + labels = ("write_odfv", "pandas") + before = write_transformation_duration_seconds._metrics.get(labels, None) + before_sum = before._sum.get() if before else 0.0 + + track_write_transformation("write_odfv", "pandas", 0.12) + + sample = write_transformation_duration_seconds._metrics[labels] + assert sample._sum.get() >= before_sum + 0.12 + + def test_noop_when_online_features_disabled(self): + import feast.metrics as m + + m._config = m._MetricsFlags(enabled=True, online_features=False) + + labels = ("disabled_write_odfv", "python") + before = write_transformation_duration_seconds._metrics.get(labels, None) + before_sum = before._sum.get() if before else 0.0 + + track_write_transformation("disabled_write_odfv", "python", 1.0) + + sample = write_transformation_duration_seconds._metrics.get(labels, None) + after_sum = sample._sum.get() if sample else 0.0 + assert after_sum == before_sum + + m._config = m._MetricsFlags( + enabled=True, + resource=True, + request=True, + online_features=True, + push=True, + materialization=True, + freshness=True, + ) + + def test_separate_from_read_transform_metric(self): + """Write and read transform metrics are independent histograms.""" + read_labels = ("shared_odfv", "python") + write_labels = ("shared_odfv", "python") + + read_before = transformation_duration_seconds._metrics.get(read_labels, None) + read_before_sum = read_before._sum.get() if read_before else 0.0 + write_before = write_transformation_duration_seconds._metrics.get( + write_labels, None + ) + write_before_sum = write_before._sum.get() if write_before else 0.0 + + track_transformation("shared_odfv", "python", 0.01) + track_write_transformation("shared_odfv", "python", 0.05) + + read_after = transformation_duration_seconds._metrics[read_labels] + write_after = write_transformation_duration_seconds._metrics[write_labels] + + read_delta = read_after._sum.get() - read_before_sum + write_delta = write_after._sum.get() - write_before_sum + + assert abs(read_delta - 0.01) < 0.001 + assert abs(write_delta - 0.05) < 0.001 + + +class TestOfflineStoreMetrics: + """Tests for the offline store Prometheus metrics (RED pattern).""" + + def test_request_total_increments_on_success(self): + before = offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + + offline_store_request_total.labels(method="to_arrow", status="success").inc() + + assert ( + offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + == before + 1 + ) + + def test_request_total_increments_on_error(self): + before = offline_store_request_total.labels( + method="to_arrow", status="error" + )._value.get() + + offline_store_request_total.labels(method="to_arrow", status="error").inc() + + assert ( + offline_store_request_total.labels( + method="to_arrow", status="error" + )._value.get() + == before + 1 + ) + + def test_latency_histogram_records(self): + before_sum = offline_store_request_latency_seconds.labels( + method="to_arrow" + )._sum.get() + + offline_store_request_latency_seconds.labels(method="to_arrow").observe(2.5) + + after_sum = offline_store_request_latency_seconds.labels( + method="to_arrow" + )._sum.get() + assert pytest.approx(after_sum - before_sum, abs=0.01) == 2.5 + + def test_row_count_histogram_records(self): + before_sum = offline_store_row_count.labels(method="to_arrow")._sum.get() + + offline_store_row_count.labels(method="to_arrow").observe(1000) + + after_sum = offline_store_row_count.labels(method="to_arrow")._sum.get() + assert pytest.approx(after_sum - before_sum, abs=1) == 1000 + + def test_different_methods_tracked_independently(self): + before_a = offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + before_b = offline_store_request_total.labels( + method="other", status="success" + )._value.get() + + offline_store_request_total.labels(method="to_arrow", status="success").inc() + + assert ( + offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + == before_a + 1 + ) + assert ( + offline_store_request_total.labels( + method="other", status="success" + )._value.get() + == before_b + ) + + +class TestEmitAuditLogs: + """Tests for structured JSON audit log emission.""" + + def test_emit_online_audit_log_writes_json(self): + import json + import logging + + _audit_logger = logging.getLogger("feast.audit") + with patch.object(_audit_logger, "info") as mock_info: + emit_online_audit_log( + requestor_id="user@example.com", + entity_keys=["driver_id", "customer_id"], + entity_count=10, + feature_views=["driver_fv", "order_fv"], + feature_count=5, + status="success", + latency_ms=42.0, + ) + + mock_info.assert_called_once() + logged_json = mock_info.call_args[0][0] + record = json.loads(logged_json) + + assert record["event"] == "online_feature_request" + assert record["requestor_id"] == "user@example.com" + assert record["entity_keys"] == ["driver_id", "customer_id"] + assert record["entity_count"] == 10 + assert record["feature_views"] == ["driver_fv", "order_fv"] + assert record["feature_count"] == 5 + assert record["status"] == "success" + assert record["latency_ms"] == pytest.approx(42.0) + assert "timestamp" in record + + def test_emit_online_audit_log_noop_when_disabled(self): + import logging + + import feast.metrics as m + + m._config = m._MetricsFlags(enabled=True, audit_logging=False) + _audit_logger = logging.getLogger("feast.audit") + with patch.object(_audit_logger, "info") as mock_info: + emit_online_audit_log( + requestor_id="user@example.com", + entity_keys=["driver_id"], + entity_count=1, + feature_views=["driver_fv"], + feature_count=1, + status="success", + latency_ms=10.0, + ) + mock_info.assert_not_called() + + def test_emit_offline_audit_log_writes_json(self): + import json + import logging + + _audit_logger = logging.getLogger("feast.audit") + with patch.object(_audit_logger, "info") as mock_info: + emit_offline_audit_log( + method="to_arrow", + feature_views=["driver_fv"], + feature_count=3, + row_count=500, + status="success", + start_time="2026-04-27T12:00:00+00:00", + end_time="2026-04-27T12:00:01+00:00", + duration_ms=1230.0, + ) + + mock_info.assert_called_once() + logged_json = mock_info.call_args[0][0] + record = json.loads(logged_json) + + assert record["event"] == "offline_feature_retrieval" + assert "timestamp" in record + assert record["method"] == "to_arrow" + assert record["feature_views"] == ["driver_fv"] + assert record["feature_count"] == 3 + assert record["row_count"] == 500 + assert record["status"] == "success" + assert record["duration_ms"] == pytest.approx(1230.0) + assert record["start_time"] == "2026-04-27T12:00:00+00:00" + assert record["end_time"] == "2026-04-27T12:00:01+00:00" + + def test_emit_offline_audit_log_noop_when_disabled(self): + import logging + + import feast.metrics as m + + m._config = m._MetricsFlags(enabled=True, audit_logging=False) + _audit_logger = logging.getLogger("feast.audit") + with patch.object(_audit_logger, "info") as mock_info: + emit_offline_audit_log( + method="to_arrow", + feature_views=["fv"], + feature_count=1, + row_count=10, + status="success", + start_time="t0", + end_time="t1", + duration_ms=500.0, + ) + mock_info.assert_not_called() + + def test_emit_online_audit_log_with_error_status(self): + import json + import logging + + _audit_logger = logging.getLogger("feast.audit") + with patch.object(_audit_logger, "info") as mock_info: + emit_online_audit_log( + requestor_id="unknown", + entity_keys=[], + entity_count=0, + feature_views=[], + feature_count=0, + status="error", + latency_ms=1.0, + ) + + record = json.loads(mock_info.call_args[0][0]) + assert record["status"] == "error" + + +class TestBuildMetricsFlagsOfflineAndAudit: + """Tests for the new offline_features and audit_logging flags.""" + + def test_no_config_defaults_for_new_flags(self): + from feast.metrics import build_metrics_flags + + flags = build_metrics_flags(None) + assert flags.offline_features is True + assert flags.audit_logging is False + + def test_explicit_enable(self): + from types import SimpleNamespace + + from feast.metrics import build_metrics_flags + + mc = SimpleNamespace( + enabled=True, + resource=True, + request=True, + online_features=True, + push=True, + materialization=True, + freshness=True, + offline_features=True, + audit_logging=True, + ) + flags = build_metrics_flags(mc) + assert flags.offline_features is True + assert flags.audit_logging is True + + def test_explicit_disable(self): + from types import SimpleNamespace + + from feast.metrics import build_metrics_flags + + mc = SimpleNamespace( + enabled=True, + resource=True, + request=True, + online_features=True, + push=True, + materialization=True, + freshness=True, + offline_features=False, + audit_logging=False, + ) + flags = build_metrics_flags(mc) + assert flags.offline_features is False + assert flags.audit_logging is False + + def test_missing_new_attrs_fall_back_to_defaults(self): + from types import SimpleNamespace + + from feast.metrics import build_metrics_flags + + mc = SimpleNamespace( + enabled=True, + resource=True, + request=True, + online_features=True, + push=True, + materialization=True, + freshness=True, + ) + flags = build_metrics_flags(mc) + assert flags.offline_features is True + assert flags.audit_logging is False + + +class TestExtractRetrievalMetadata: + """Tests for _extract_retrieval_metadata helper.""" + + def test_extracts_feature_views_and_count(self): + from feast.infra.offline_stores.offline_store import ( + RetrievalMetadata, + _extract_retrieval_metadata, + ) + + job = MagicMock() + job.metadata = RetrievalMetadata( + features=[ + "driver_fv:conv_rate", + "driver_fv:acc_rate", + "vehicle_fv:mileage", + ], + keys=["driver_id"], + ) + + fv_names, feat_count = _extract_retrieval_metadata(job) + assert feat_count == 3 + assert set(fv_names) == {"driver_fv", "vehicle_fv"} + + def test_returns_empty_when_no_metadata(self): + from feast.infra.offline_stores.offline_store import ( + _extract_retrieval_metadata, + ) + + job = MagicMock() + job.metadata = None + + fv_names, feat_count = _extract_retrieval_metadata(job) + assert fv_names == [] + assert feat_count == 0 + + def test_handles_not_implemented_metadata(self): + from feast.infra.offline_stores.offline_store import ( + _extract_retrieval_metadata, + ) + + job = MagicMock() + type(job).metadata = property( + lambda self: (_ for _ in ()).throw(NotImplementedError()) + ) + + fv_names, feat_count = _extract_retrieval_metadata(job) + assert fv_names == [] + assert feat_count == 0 + + +class TestRetrievalJobToArrowInstrumentation: + """Tests for the metrics/audit instrumentation in RetrievalJob.to_arrow().""" + + def _make_job( + self, table, on_demand_fvs=None, metadata=None, raise_on_internal=None + ): + """Create a concrete RetrievalJob subclass for testing.""" + from feast.infra.offline_stores.offline_store import RetrievalJob + + class _TestJob(RetrievalJob): + def __init__(self): + self._table = table + self._odfvs = on_demand_fvs or [] + self._metadata = metadata + self._raise = raise_on_internal + + def _to_arrow_internal(self, timeout=None): + if self._raise: + raise self._raise + return self._table + + @property + def full_feature_names(self): + return False + + @property + def on_demand_feature_views(self): + return self._odfvs + + @property + def metadata(self): + return self._metadata + + return _TestJob() + + def test_success_increments_counter_and_records_latency(self): + import pyarrow as pa + + table = pa.table({"col": [1, 2, 3]}) + job = self._make_job(table) + + before_count = offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + before_latency = offline_store_request_latency_seconds.labels( + method="to_arrow" + )._sum.get() + + result = job.to_arrow() + + assert result.num_rows == 3 + assert ( + offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + == before_count + 1 + ) + assert ( + offline_store_request_latency_seconds.labels(method="to_arrow")._sum.get() + > before_latency + ) + + def test_error_increments_error_counter(self): + job = self._make_job(None, raise_on_internal=RuntimeError("query failed")) + + before_error = offline_store_request_total.labels( + method="to_arrow", status="error" + )._value.get() + + with pytest.raises(RuntimeError, match="query failed"): + job.to_arrow() + + assert ( + offline_store_request_total.labels( + method="to_arrow", status="error" + )._value.get() + == before_error + 1 + ) + + def test_row_count_recorded_on_success(self): + import pyarrow as pa + + table = pa.table({"a": list(range(500))}) + job = self._make_job(table) + + before_sum = offline_store_row_count.labels(method="to_arrow")._sum.get() + + job.to_arrow() + + assert ( + offline_store_row_count.labels(method="to_arrow")._sum.get() + >= before_sum + 500 + ) + + def test_row_count_recorded_when_zero(self): + """Zero-row result must still record an observation so operators can + distinguish 'metric not emitted' from 'query returned 0 rows'.""" + import pyarrow as pa + + table = pa.table({"a": pa.array([], type=pa.int64())}) + job = self._make_job(table) + + hist = offline_store_row_count.labels(method="to_arrow") + before_bucket = hist._buckets[0].get() + before_sum = hist._sum.get() + before_count = offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + + job.to_arrow() + + assert hist._buckets[0].get() == before_bucket + 1 + assert hist._sum.get() == before_sum # 0 rows adds 0 to sum + assert ( + offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + == before_count + 1 + ) + + def test_metrics_skipped_when_offline_features_disabled(self): + import pyarrow as pa + + import feast.metrics as m + + m._config = m._MetricsFlags( + enabled=True, offline_features=False, audit_logging=False + ) + + table = pa.table({"col": [1, 2]}) + job = self._make_job(table) + + before_count = offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + + job.to_arrow() + + assert ( + offline_store_request_total.labels( + method="to_arrow", status="success" + )._value.get() + == before_count + ) + + def test_audit_log_emitted_on_success(self): + import pyarrow as pa + + from feast.infra.offline_stores.offline_store import RetrievalMetadata + + meta = RetrievalMetadata( + features=["driver_fv:conv_rate", "driver_fv:acc_rate"], + keys=["driver_id"], + ) + table = pa.table({"col": [1, 2, 3]}) + job = self._make_job(table, metadata=meta) + + with patch("feast.metrics.emit_offline_audit_log") as mock_audit: + job.to_arrow() + + mock_audit.assert_called_once() + call_kwargs = mock_audit.call_args[1] + assert call_kwargs["method"] == "to_arrow" + assert call_kwargs["status"] == "success" + assert call_kwargs["row_count"] == 3 + assert call_kwargs["feature_count"] == 2 + assert set(call_kwargs["feature_views"]) == {"driver_fv"} + + def test_audit_log_skipped_when_disabled(self): + import pyarrow as pa + + import feast.metrics as m + + m._config = m._MetricsFlags( + enabled=True, offline_features=True, audit_logging=False + ) + + table = pa.table({"col": [1]}) + job = self._make_job(table) + + with patch("feast.metrics.emit_offline_audit_log") as mock_audit: + job.to_arrow() + mock_audit.assert_not_called() + + def test_instrumentation_failure_does_not_mask_query_error(self): + """If metrics code itself throws, the original query error still propagates.""" + import pyarrow as pa + + table = pa.table({"col": [1]}) + job = self._make_job(table) + + with patch( + "feast.metrics._config", + new_callable=lambda: property( + lambda self: (_ for _ in ()).throw(RuntimeError("metrics broken")) + ), + ): + result = job.to_arrow() + assert result.num_rows == 1 + + +class TestParseFeatureInfo: + """Tests for _parse_feature_info in feature_server.""" + + def test_feature_ref_list(self): + from feast.feature_server import _parse_feature_info + + refs = ["driver_fv:conv_rate", "driver_fv:acc_rate", "vehicle_fv:mileage"] + fv_names, feat_count = _parse_feature_info(refs) + assert feat_count == 3 + assert set(fv_names) == {"driver_fv", "vehicle_fv"} + + def test_empty_list(self): + from feast.feature_server import _parse_feature_info + + fv_names, feat_count = _parse_feature_info([]) + assert fv_names == [] + assert feat_count == 0 + + def test_feature_service(self): + from feast.feature_server import _parse_feature_info + + proj1 = MagicMock() + proj1.name = "driver_fv" + proj1.features = [MagicMock(), MagicMock()] + proj2 = MagicMock() + proj2.name = "order_fv" + proj2.features = [MagicMock()] + + fs_svc = MagicMock() + fs_svc.feature_view_projections = [proj1, proj2] + + from feast.feature_service import FeatureService + + fs_svc.__class__ = FeatureService + + fv_names, feat_count = _parse_feature_info(fs_svc) + assert feat_count == 3 + assert fv_names == ["driver_fv", "order_fv"] + + def test_strips_version_suffix(self): + from feast.feature_server import _parse_feature_info + + refs = ["driver_fv@v2:conv_rate"] + fv_names, feat_count = _parse_feature_info(refs) + assert feat_count == 1 + assert fv_names == ["driver_fv"] + + +class TestEmitOnlineAudit: + """Tests for the _emit_online_audit helper in feature_server.""" + + def test_emits_audit_log_with_anonymous_user(self): + from feast.feature_server import GetOnlineFeaturesRequest, _emit_online_audit + + request = GetOnlineFeaturesRequest( + entities={"driver_id": [1, 2]}, + features=["driver_fv:conv_rate"], + ) + + with ( + patch("feast.feature_server.feast_metrics") as mock_metrics, + patch( + "feast.permissions.security_manager.get_security_manager", + return_value=None, + ), + ): + _emit_online_audit( + request=request, + features=request.features, + entity_count=2, + status="success", + latency_ms=15.0, + ) + + mock_metrics.emit_online_audit_log.assert_called_once() + kwargs = mock_metrics.emit_online_audit_log.call_args[1] + assert kwargs["requestor_id"] == "anonymous" + assert kwargs["entity_keys"] == ["driver_id"] + assert kwargs["entity_count"] == 2 + assert kwargs["status"] == "success" + + def test_emits_audit_log_with_authenticated_user(self): + from feast.feature_server import GetOnlineFeaturesRequest, _emit_online_audit + + request = GetOnlineFeaturesRequest( + entities={"driver_id": [1]}, + features=["driver_fv:conv_rate"], + ) + + mock_sm = MagicMock() + mock_sm.current_user.username = "jdoe" + + with ( + patch("feast.feature_server.feast_metrics") as mock_metrics, + patch( + "feast.permissions.security_manager.get_security_manager", + return_value=mock_sm, + ), + ): + _emit_online_audit( + request=request, + features=request.features, + entity_count=1, + status="success", + latency_ms=10.0, + ) + + kwargs = mock_metrics.emit_online_audit_log.call_args[1] + assert kwargs["requestor_id"] == "jdoe" + + def test_does_not_raise_on_failure(self): + from feast.feature_server import GetOnlineFeaturesRequest, _emit_online_audit + + request = GetOnlineFeaturesRequest( + entities={"driver_id": [1]}, + features=["driver_fv:conv_rate"], + ) + + with patch( + "feast.permissions.security_manager.get_security_manager", + side_effect=RuntimeError("auth broken"), + ): + _emit_online_audit( + request=request, + features=request.features, + entity_count=1, + status="error", + latency_ms=5.0, + ) diff --git a/sdk/python/tests/unit/test_offline_server.py b/sdk/python/tests/unit/test_offline_server.py index c5f309eb323..fa1bf85e97e 100644 --- a/sdk/python/tests/unit/test_offline_server.py +++ b/sdk/python/tests/unit/test_offline_server.py @@ -1,347 +1,194 @@ import os -import tempfile -from datetime import datetime, timedelta +import subprocess +import sys +import textwrap +from unittest.mock import MagicMock, mock_open, patch import assertpy -import pandas as pd -import pyarrow as pa -import pyarrow.flight as flight -import pytest - -from feast import FeatureStore, FeatureView, FileSource -from feast.errors import FeatureViewNotFoundException -from feast.feature_logging import FeatureServiceLoggingSource + from feast.infra.offline_stores.remote import ( RemoteOfflineStore, RemoteOfflineStoreConfig, + _create_retrieval_metadata, +) +from feast.offline_server import ( + OfflineServer, + _configure_grpc_fips, + _is_fips_enabled, ) -from feast.offline_server import OfflineServer, _init_auth_manager -from feast.repo_config import RepoConfig -from feast.torch_wrapper import get_torch -from tests.utils.cli_repo_creator import CliRunner - -PROJECT_NAME = "test_remote_offline" - - -@pytest.fixture -def empty_offline_server(environment): - store = environment.feature_store - - location = "grpc+tcp://localhost:0" - _init_auth_manager(store=store) - return OfflineServer(store=store, location=location) - - -@pytest.fixture -def arrow_client(empty_offline_server): - return flight.FlightClient(f"grpc://localhost:{empty_offline_server.port}") - - -def test_offline_server_is_alive(environment, empty_offline_server, arrow_client): - server = empty_offline_server - client = arrow_client - - assertpy.assert_that(server).is_not_none - assertpy.assert_that(server.port).is_not_equal_to(0) - - actions = list(client.list_actions()) - flights = list(client.list_flights()) - - assertpy.assert_that(actions).is_equal_to( - [ - ( - "offline_write_batch", - "Writes the specified arrow table to the data source underlying the specified feature view.", - ), - ( - "write_logged_features", - "Writes logged features to a specified destination in the offline store.", - ), - ( - "persist", - "Synchronously executes the underlying query and persists the result in the same offline store at the " - "specified destination.", - ), - ] - ) - assertpy.assert_that(flights).is_empty() - - -def default_store(temp_dir): - runner = CliRunner() - result = runner.run(["init", PROJECT_NAME], cwd=temp_dir) - repo_path = os.path.join(temp_dir, PROJECT_NAME, "feature_repo") - assert result.returncode == 0 - - result = runner.run(["--chdir", repo_path, "apply"], cwd=temp_dir) - assert result.returncode == 0 - - fs = FeatureStore(repo_path=repo_path) - return fs - -def remote_feature_store(offline_server): - offline_config = RemoteOfflineStoreConfig( - type="remote", host="0.0.0.0", port=offline_server.port - ) - registry_path = os.path.join( - str(offline_server.store.repo_path), - offline_server.store.config.registry.path, +def test_create_retrieval_metadata_with_sql_string(): + """SQL string entity_df should produce a stub with empty keys and no timestamps.""" + sql = "SELECT driver_id, event_timestamp FROM driver_stats" + metadata = _create_retrieval_metadata( + feature_refs=["driver_hourly_stats:conv_rate"], entity_df=sql ) - store = FeatureStore( - config=RepoConfig( - project=PROJECT_NAME, - registry=registry_path, - provider="local", - offline_store=offline_config, - entity_key_serialization_version=3, - # repo_config = - ) + assertpy.assert_that(metadata.features).is_equal_to( + ["driver_hourly_stats:conv_rate"] ) - return store - - -def test_remote_offline_store_apis(): - with tempfile.TemporaryDirectory() as temp_dir: - store = default_store(str(temp_dir)) - location = "grpc+tcp://localhost:0" - - _init_auth_manager(store=store) - server = OfflineServer(store=store, location=location) - - assertpy.assert_that(server).is_not_none - assertpy.assert_that(server.port).is_not_equal_to(0) - - fs = remote_feature_store(server) - - _test_get_historical_features_returns_data(fs) - _test_get_historical_features_to_tensor(fs) - _test_get_historical_features_returns_nan(fs) - _test_get_historical_features_to_tensor_with_nan(fs) - _test_offline_write_batch(str(temp_dir), fs) - _test_write_logged_features(str(temp_dir), fs) - _test_pull_latest_from_table_or_query(str(temp_dir), fs) - _test_pull_all_from_table_or_query(str(temp_dir), fs) - - -def test_remote_offline_store_exception_handling(): - with tempfile.TemporaryDirectory() as temp_dir: - store = default_store(str(temp_dir)) - location = "grpc+tcp://localhost:0" - - _init_auth_manager(store=store) - server = OfflineServer(store=store, location=location) - - assertpy.assert_that(server).is_not_none - assertpy.assert_that(server.port).is_not_equal_to(0) - - fs = remote_feature_store(server) - data_file = os.path.join( - temp_dir, fs.project, "feature_repo/data/driver_stats.parquet" + assertpy.assert_that(list(metadata.keys)).is_empty() + assertpy.assert_that(metadata.min_event_timestamp).is_none() + assertpy.assert_that(metadata.max_event_timestamp).is_none() + + +def test_remote_offline_store_sql_entity_df_routing(): + """RemoteOfflineStore.get_historical_features moves SQL into api_parameters.""" + sql = "SELECT driver_id, event_timestamp FROM driver_stats" + + mock_client = MagicMock() + with patch( + "feast.infra.offline_stores.remote.build_arrow_flight_client", + return_value=mock_client, + ): + job = RemoteOfflineStore.get_historical_features( + config=MagicMock( + offline_store=RemoteOfflineStoreConfig( + type="remote", host="localhost", port=8815 + ), + auth_config=MagicMock(type="no_auth"), + ), + feature_views=[], + feature_refs=["driver_hourly_stats:conv_rate"], + entity_df=sql, + registry=MagicMock(), + project="test", + full_feature_names=False, ) - data_df = pd.read_parquet(data_file) - - with pytest.raises( - FeatureViewNotFoundException, - match="Feature view test does not exist in project test_remote_offline", - ): - RemoteOfflineStore.offline_write_batch( - fs.config, - FeatureView(name="test", source=FileSource(path="test")), - pa.Table.from_pandas(data_df), - progress=None, - ) - - -def _test_get_historical_features_returns_data(fs: FeatureStore): - entity_df = pd.DataFrame.from_dict( - { - "driver_id": [1001, 1002, 1003], - "event_timestamp": [ - datetime(2021, 4, 12, 10, 59, 42), - datetime(2021, 4, 12, 8, 12, 10), - datetime(2021, 4, 12, 16, 40, 26), - ], - "label_driver_reported_satisfaction": [1, 5, 3], - "val_to_add": [1, 2, 3], - "val_to_add_2": [10, 20, 30], - } - ) - features = [ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "transformed_conv_rate:conv_rate_plus_val1", - "transformed_conv_rate:conv_rate_plus_val2", - ] - - training_df = fs.get_historical_features(entity_df, features).to_df() - - assertpy.assert_that(training_df).is_not_none() - assertpy.assert_that(len(training_df)).is_equal_to(3) - - for index, driver_id in enumerate(entity_df["driver_id"]): - assertpy.assert_that(training_df["driver_id"][index]).is_equal_to(driver_id) - for feature in features: - column_id = feature.split(":")[1] - value = training_df[column_id][index] - assertpy.assert_that(value).is_not_nan() - - -def _test_get_historical_features_to_tensor(fs: FeatureStore): - entity_df = pd.DataFrame.from_dict( - { - "driver_id": [1001, 1002, 1003], - "event_timestamp": [ - datetime(2021, 4, 12, 10, 59, 42), - datetime(2021, 4, 12, 8, 12, 10), - datetime(2021, 4, 12, 16, 40, 26), - ], - "label_driver_reported_satisfaction": [1, 5, 3], - "val_to_add": [1, 2, 3], - "val_to_add_2": [10, 20, 30], - } - ) + assertpy.assert_that(job.entity_df).is_none() + assertpy.assert_that(job.api_parameters).contains_key("entity_df_sql") + assertpy.assert_that(job.api_parameters["entity_df_sql"]).is_equal_to(sql) - features = [ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "transformed_conv_rate:conv_rate_plus_val1", - "transformed_conv_rate:conv_rate_plus_val2", - ] - - job = fs.get_historical_features(entity_df, features) - tensor_data = job.to_tensor() - - assertpy.assert_that(tensor_data).is_not_none() - assertpy.assert_that(tensor_data["driver_id"].shape[0]).is_equal_to(3) - torch = get_torch() - for key, values in tensor_data.items(): - if isinstance(values, torch.Tensor): - assertpy.assert_that(values.shape[0]).is_equal_to(3) - for val in values: - val_float = val.item() - assertpy.assert_that(val_float).is_instance_of((float, int)) - assertpy.assert_that(val_float).is_not_nan() - - -def _test_get_historical_features_returns_nan(fs: FeatureStore): - entity_df = pd.DataFrame.from_dict( - { - "driver_id": [1, 2, 3], - "event_timestamp": [ - datetime(2021, 4, 12, 10, 59, 42), - datetime(2021, 4, 12, 8, 12, 10), - datetime(2021, 4, 12, 16, 40, 26), - ], - "label_driver_reported_satisfaction": [1, 5, 3], - "val_to_add": [1, 2, 3], - "val_to_add_2": [10, 20, 30], - } - ) - features = [ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "transformed_conv_rate:conv_rate_plus_val1", - "transformed_conv_rate:conv_rate_plus_val2", - ] - - training_df = fs.get_historical_features(entity_df, features).to_df() - - assertpy.assert_that(training_df).is_not_none() - assertpy.assert_that(len(training_df)).is_equal_to(3) - - for index, driver_id in enumerate(entity_df["driver_id"]): - assertpy.assert_that(training_df["driver_id"][index]).is_equal_to(driver_id) - for feature in features: - column_id = feature.split(":")[1] - value = training_df[column_id][index] - assertpy.assert_that(value).is_nan() - - -def _test_get_historical_features_to_tensor_with_nan(fs: FeatureStore): - entity_df = pd.DataFrame.from_dict( - { - "driver_id": [9991, 9992], # IDs with no matching features - "event_timestamp": [ - datetime(2021, 4, 12, 10, 59, 42), - datetime(2021, 4, 12, 10, 59, 42), - ], - } - ) - features = ["driver_hourly_stats:conv_rate"] - job = fs.get_historical_features(entity_df, features) - tensor_data = job.to_tensor() - assert "conv_rate" in tensor_data - values = tensor_data["conv_rate"] - # conv_rate is a float feature, missing values should be NaN - torch = get_torch() - for val in values: - assert isinstance(val, torch.Tensor) or torch.is_tensor(val) - assertpy.assert_that(torch.isnan(val).item()).is_true() - - -def _test_offline_write_batch(temp_dir, fs: FeatureStore): - data_file = os.path.join( - temp_dir, fs.project, "feature_repo/data/driver_stats.parquet" - ) - data_df = pd.read_parquet(data_file) - feature_view = fs.get_feature_view("driver_hourly_stats") +def test_offline_server_get_historical_features_passes_sql_to_store(): + """OfflineServer forwards entity_df_sql to the backing offline store.""" + sql = "SELECT driver_id, event_timestamp FROM driver_stats" - RemoteOfflineStore.offline_write_batch( - fs.config, feature_view, pa.Table.from_pandas(data_df), progress=None + mock_job = MagicMock() + mock_offline_store = MagicMock() + mock_offline_store.get_historical_features.return_value = mock_job + + mock_store = MagicMock() + mock_store.config.project = "test" + + server = MagicMock(spec=OfflineServer) + server.offline_store = mock_offline_store + server.store = mock_store + server.flights = {} + server.list_feature_views_by_name.return_value = [] + + command = { + "api": "get_historical_features", + "command_id": "abc", + "feature_view_names": [], + "name_aliases": [], + "feature_refs": ["driver_hourly_stats:conv_rate"], + "project": "test", + "full_feature_names": False, + "entity_df_sql": sql, + } + + result = OfflineServer.get_historical_features(server, command, key=None) + + assertpy.assert_that(result).is_equal_to(mock_job) + _, kwargs = mock_offline_store.get_historical_features.call_args + assertpy.assert_that(kwargs["entity_df"]).is_equal_to(sql) + + +def test_is_fips_enabled_returns_true(): + with patch("builtins.open", mock_open(read_data="1\n")): + assert _is_fips_enabled() is True + + +def test_is_fips_enabled_returns_false(): + with patch("builtins.open", mock_open(read_data="0\n")): + assert _is_fips_enabled() is False + + +def test_is_fips_enabled_missing_file(): + with patch("builtins.open", side_effect=FileNotFoundError): + assert _is_fips_enabled() is False + + +def test_configure_grpc_fips_sets_cipher_suites(): + with ( + patch("feast.offline_server._is_fips_enabled", return_value=True), + patch.dict(os.environ, {}, clear=False), + ): + os.environ.pop("GRPC_SSL_CIPHER_SUITES", None) + _configure_grpc_fips() + assert "GRPC_SSL_CIPHER_SUITES" in os.environ + assert "AES128-GCM-SHA256" in os.environ["GRPC_SSL_CIPHER_SUITES"] + del os.environ["GRPC_SSL_CIPHER_SUITES"] + + +def test_configure_grpc_fips_respects_existing_env(): + with ( + patch("feast.offline_server._is_fips_enabled", return_value=True), + patch.dict(os.environ, {"GRPC_SSL_CIPHER_SUITES": "custom"}, clear=False), + ): + _configure_grpc_fips() + assert os.environ["GRPC_SSL_CIPHER_SUITES"] == "custom" + + +def test_configure_grpc_fips_noop_without_fips(): + with ( + patch("feast.offline_server._is_fips_enabled", return_value=False), + patch.dict(os.environ, {}, clear=False), + ): + os.environ.pop("GRPC_SSL_CIPHER_SUITES", None) + _configure_grpc_fips() + assert "GRPC_SSL_CIPHER_SUITES" not in os.environ + + +def test_module_level_fips_sets_env_before_pyarrow_import(): + """GRPC_SSL_CIPHER_SUITES must be set at module load time, + before pyarrow.flight (which bundles gRPC) is imported. + + Uses a subprocess so pyarrow.flight is not already cached in + sys.modules, which lets us verify the true import ordering. + """ + script = textwrap.dedent("""\ + import io, os, sys + + # Intercept only /proc/sys/crypto/fips_enabled to simulate FIPS + _real_open = open + def _fips_open(file, *args, **kwargs): + if str(file) == "/proc/sys/crypto/fips_enabled": + return io.StringIO("1\\n") + return _real_open(file, *args, **kwargs) + + import builtins + builtins.open = _fips_open + + # Track import order to verify env var is set before pyarrow.flight + original_import = builtins.__import__ + def tracking_import(name, *args, **kwargs): + if name == "pyarrow.flight": + assert "GRPC_SSL_CIPHER_SUITES" in os.environ, ( + "GRPC_SSL_CIPHER_SUITES not set before pyarrow.flight import" + ) + return original_import(name, *args, **kwargs) + + builtins.__import__ = tracking_import + try: + import feast.offline_server + assert "GRPC_SSL_CIPHER_SUITES" in os.environ + assert "AES128-GCM-SHA256" in os.environ["GRPC_SSL_CIPHER_SUITES"] + finally: + builtins.__import__ = original_import + builtins.open = _real_open + """) + env = os.environ.copy() + env.pop("GRPC_SSL_CIPHER_SUITES", None) + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + env=env, + timeout=60, ) - - -def _test_write_logged_features(temp_dir, fs: FeatureStore): - data_file = os.path.join( - temp_dir, fs.project, "feature_repo/data/driver_stats.parquet" - ) - data_df = pd.read_parquet(data_file) - feature_service = fs.get_feature_service("driver_activity_v1") - - RemoteOfflineStore.write_logged_features( - config=fs.config, - data=pa.Table.from_pandas(data_df), - source=FeatureServiceLoggingSource(feature_service, fs.config.project), - logging_config=feature_service.logging_config, - registry=fs.registry, + assert result.returncode == 0, ( + f"Subprocess failed:\nstdout: {result.stdout}\nstderr: {result.stderr}" ) - - -def _test_pull_latest_from_table_or_query(temp_dir, fs: FeatureStore): - data_source = fs.get_data_source("driver_hourly_stats_source") - - end_date = datetime.now().replace(microsecond=0, second=0, minute=0) - start_date = end_date - timedelta(days=15) - RemoteOfflineStore.pull_latest_from_table_or_query( - config=fs.config, - data_source=data_source, - join_key_columns=[], - feature_name_columns=[], - timestamp_field="event_timestamp", - created_timestamp_column="created", - start_date=start_date, - end_date=end_date, - ).to_df() - - -def _test_pull_all_from_table_or_query(temp_dir, fs: FeatureStore): - data_source = fs.get_data_source("driver_hourly_stats_source") - - end_date = datetime.now().replace(microsecond=0, second=0, minute=0) - start_date = end_date - timedelta(days=15) - RemoteOfflineStore.pull_all_from_table_or_query( - config=fs.config, - data_source=data_source, - join_key_columns=[], - feature_name_columns=[], - timestamp_field="event_timestamp", - start_date=start_date, - end_date=end_date, - ).to_df() diff --git a/sdk/python/tests/unit/test_on_demand_feature_view.py b/sdk/python/tests/unit/test_on_demand_feature_view.py index 505146aa612..c63aaa0f435 100644 --- a/sdk/python/tests/unit/test_on_demand_feature_view.py +++ b/sdk/python/tests/unit/test_on_demand_feature_view.py @@ -17,6 +17,7 @@ import pandas as pd import pytest +from feast.aggregation import Aggregation from feast.feature_view import FeatureView from feast.field import Field from feast.infra.offline_stores.file_source import FileSource @@ -418,3 +419,332 @@ def another_transform(features_df: pd.DataFrame) -> pd.DataFrame: deserialized = OnDemandFeatureView.from_proto(proto) assert deserialized.name == CUSTOM_FUNCTION_NAME + + +def test_track_metrics_defaults_to_false(): + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=Float32), + ], + source=file_source, + ) + odfv = OnDemandFeatureView( + name="metrics-default-odfv", + sources=[feature_view], + schema=[Field(name="output1", dtype=Float32)], + feature_transformation=PandasTransformation( + udf=udf1, udf_string="udf1 source code" + ), + ) + assert odfv.track_metrics is False + + +def test_track_metrics_true_persists_via_proto(): + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=Float32), + ], + source=file_source, + ) + odfv = OnDemandFeatureView( + name="tracked-metrics-odfv", + sources=[feature_view], + schema=[Field(name="output1", dtype=Float32)], + feature_transformation=PandasTransformation( + udf=udf1, udf_string="udf1 source code" + ), + track_metrics=True, + ) + assert odfv.track_metrics is True + + proto = odfv.to_proto() + assert proto.spec.tags.get("feast:track_metrics") == "true" + + restored = OnDemandFeatureView.from_proto(proto) + assert restored.track_metrics is True + assert "feast:track_metrics" not in restored.tags, ( + "Internal feast:track_metrics tag leaked into user-facing self.tags " + "after proto round-trip" + ) + + +def test_track_metrics_proto_roundtrip_preserves_user_tags(): + """User tags must survive a proto round-trip without internal tag pollution.""" + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=Float32), + ], + source=file_source, + ) + user_tags = {"team": "ml-platform", "priority": "high"} + odfv = OnDemandFeatureView( + name="tagged-odfv", + sources=[feature_view], + schema=[Field(name="output1", dtype=Float32)], + feature_transformation=PandasTransformation( + udf=udf1, udf_string="udf1 source code" + ), + tags=user_tags, + track_metrics=True, + ) + assert odfv.tags == user_tags + + proto = odfv.to_proto() + restored = OnDemandFeatureView.from_proto(proto) + + assert restored.tags == user_tags + assert restored.track_metrics is True + + +def test_track_metrics_false_not_stored_in_tags(): + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=Float32), + ], + source=file_source, + ) + odfv = OnDemandFeatureView( + name="no-metrics-odfv", + sources=[feature_view], + schema=[Field(name="output1", dtype=Float32)], + feature_transformation=PandasTransformation( + udf=udf1, udf_string="udf1 source code" + ), + track_metrics=False, + ) + proto = odfv.to_proto() + assert "feast:track_metrics" not in proto.spec.tags + + restored = OnDemandFeatureView.from_proto(proto) + assert restored.track_metrics is False + + +def test_copy_preserves_track_metrics(): + """__copy__ must carry track_metrics so FeatureService projections keep timing enabled.""" + import copy + + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=Float32), + ], + source=file_source, + ) + odfv = OnDemandFeatureView( + name="tracked-odfv", + sources=[feature_view], + schema=[Field(name="output1", dtype=Float32)], + feature_transformation=PandasTransformation( + udf=udf1, udf_string="udf1 source code" + ), + track_metrics=True, + ) + assert odfv.track_metrics is True + + copied = copy.copy(odfv) + assert copied.track_metrics is True, ( + "__copy__ lost track_metrics; ODFV timing metrics will be silently disabled " + "when using FeatureService projections" + ) + + +def test_eq_considers_track_metrics(): + """__eq__ must distinguish ODFVs that differ only in track_metrics.""" + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[ + Field(name="feature1", dtype=Float32), + Field(name="feature2", dtype=Float32), + ], + source=file_source, + ) + common = dict( + name="eq-odfv", + sources=[feature_view], + schema=[Field(name="output1", dtype=Float32)], + feature_transformation=PandasTransformation( + udf=udf1, udf_string="udf1 source code" + ), + ) + odfv_tracked = OnDemandFeatureView(**common, track_metrics=True) + odfv_untracked = OnDemandFeatureView(**common, track_metrics=False) + + assert odfv_tracked != odfv_untracked + + +def test_aggregations_valid_without_udf(): + """An ODFV with aggregations must pass ensure_valid() without a udf or feature_transformation.""" + from datetime import timedelta + + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[Field(name="purchase_count", dtype=Float32)], + source=file_source, + ) + odfv = OnDemandFeatureView( + name="agg-odfv", + sources=[feature_view], + schema=[Field(name="purchase_sum_30d", dtype=Float32)], + aggregations=[ + Aggregation( + column="purchase_count", + function="sum", + time_window=timedelta(days=30), + ) + ], + ) + # Must not raise + odfv.ensure_valid() + + +def test_aggregations_only_odfv_proto_roundtrip(): + """Aggregation-only ODFV must survive a proto round-trip without crashing on dill.loads(b'').""" + from datetime import timedelta + + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[Field(name="purchase_count", dtype=Float32)], + source=file_source, + ) + odfv = OnDemandFeatureView( + name="agg-odfv", + sources=[feature_view], + schema=[Field(name="purchase_sum_30d", dtype=Float32)], + aggregations=[ + Aggregation( + column="purchase_count", + function="sum", + time_window=timedelta(days=30), + ) + ], + ) + proto = odfv.to_proto() + restored = OnDemandFeatureView.from_proto(proto) + assert restored.feature_transformation is None + assert len(restored.aggregations) == 1 + assert restored.aggregations[0].column == "purchase_count" + + +def test_aggregations_only_odfv_infer_features(): + """infer_features must not crash for aggregation-only ODFVs with explicit schema.""" + from datetime import timedelta + + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[Field(name="purchase_count", dtype=Float32)], + source=file_source, + ) + odfv = OnDemandFeatureView( + name="agg-odfv", + sources=[feature_view], + schema=[Field(name="purchase_sum_30d", dtype=Float32)], + aggregations=[ + Aggregation( + column="purchase_count", + function="sum", + time_window=timedelta(days=30), + ) + ], + ) + # Must not raise; features are taken from schema, no transformation execution needed + odfv.infer_features() + + +def test_input_schema_aggregation_no_udf(): + """ODFV with input_schema, no sources, aggregations, and no udf must validate and round-trip.""" + from datetime import timedelta + + odfv = OnDemandFeatureView( + name="input-schema-agg-odfv", + sources=None, + input_schema=[Field(name="purchase_count", dtype=Float32)], + schema=[Field(name="purchase_sum_30d", dtype=Float32)], + aggregations=[ + Aggregation( + column="purchase_count", + function="sum", + time_window=timedelta(days=30), + ) + ], + ) + + odfv.ensure_valid() + odfv.infer_features() + + proto = odfv.to_proto() + restored = OnDemandFeatureView.from_proto(proto) + assert restored.feature_transformation is None + assert len(restored.aggregations) == 1 + assert restored.input_schema == [Field(name="purchase_count", dtype=Float32)] + + +def test_on_demand_feature_view_org_field(): + """Test that the optional `org` field is stored, serialized, and round-trips correctly.""" + file_source = FileSource(name="my-file-source", path="test.parquet") + feature_view = FeatureView( + name="my-feature-view", + entities=[], + schema=[Field(name="feature1", dtype=Float32)], + source=file_source, + ) + common = dict( + sources=[feature_view], + schema=[Field(name="output1", dtype=Float32)], + feature_transformation=PandasTransformation( + udf=udf1, udf_string="udf1 source code" + ), + ) + + # org defaults to empty string + odfv_no_org = OnDemandFeatureView(name="odfv-no-org", **common) + assert odfv_no_org.org == "" + + # org can be set explicitly + odfv_with_org = OnDemandFeatureView(name="odfv-with-org", org="ads", **common) + assert odfv_with_org.org == "ads" + + # org is serialized to proto + proto = odfv_with_org.to_proto() + assert proto.spec.org == "ads" + + # org survives a proto round-trip + roundtripped = OnDemandFeatureView.from_proto(proto) + assert roundtripped.org == "ads" + + # a view without org round-trips to empty string + proto_no_org = odfv_no_org.to_proto() + assert proto_no_org.spec.org == "" + roundtripped_no_org = OnDemandFeatureView.from_proto(proto_no_org) + assert roundtripped_no_org.org == "" + + # equality respects org + odfv_org_a = OnDemandFeatureView(name="odfv-eq", org="ads", **common) + odfv_org_b = OnDemandFeatureView(name="odfv-eq", org="search", **common) + assert odfv_org_a != odfv_org_b diff --git a/sdk/python/tests/unit/test_on_demand_feature_view_input_schema.py b/sdk/python/tests/unit/test_on_demand_feature_view_input_schema.py new file mode 100644 index 00000000000..fd69762ef08 --- /dev/null +++ b/sdk/python/tests/unit/test_on_demand_feature_view_input_schema.py @@ -0,0 +1,167 @@ +# Copyright 2025 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for OnDemandFeatureView input_schema support.""" + +import copy +from datetime import timedelta + +import pandas as pd +import pytest + +from feast import Entity, Field +from feast.aggregation import Aggregation +from feast.on_demand_feature_view import OnDemandFeatureView, on_demand_feature_view +from feast.types import Float64, Int64 +from feast.value_type import ValueType + +user = Entity(name="user", join_keys=["user_id"], value_type=ValueType.INT64) + + +def test_decorator_with_input_schema(): + """The @on_demand_feature_view decorator supports input_schema without sources.""" + + @on_demand_feature_view( + input_schema=[ + Field(name="txn_amount", dtype=Float64), + ], + schema=[ + Field(name="txn_count", dtype=Int64), + Field(name="total_txn_amount", dtype=Float64), + Field(name="avg_txn_amount", dtype=Float64), + ], + aggregations=[ + Aggregation( + column="txn_amount", + function="count", + name="txn_count", + time_window=timedelta(days=30), + ), + Aggregation( + column="txn_amount", + function="sum", + name="total_txn_amount", + time_window=timedelta(days=30), + ), + Aggregation( + column="txn_amount", + function="mean", + name="avg_txn_amount", + time_window=timedelta(days=30), + ), + ], + entities=[user], + ) + def compute_txn_stats(df: pd.DataFrame) -> pd.DataFrame: + return df + + assert isinstance(compute_txn_stats, OnDemandFeatureView) + assert compute_txn_stats.name == "compute_txn_stats" + assert compute_txn_stats.input_schema == [Field(name="txn_amount", dtype=Float64)] + assert len(compute_txn_stats.aggregations) == 3 + assert len(compute_txn_stats.features) == 3 + + # The internal sentinel RequestSource should be present + sentinel_name = ( + f"{OnDemandFeatureView._INPUT_SCHEMA_SOURCE_PREFIX}compute_txn_stats" + ) + assert sentinel_name in compute_txn_stats.source_request_sources + + # sources (user-visible) should be empty + assert compute_txn_stats.sources == [] + + +def test_aggregation_aliases(): + """Aggregation name and time_window params work correctly.""" + agg = Aggregation( + column="txn_amount", + function="sum", + name="total_txn_amount", + time_window=timedelta(days=30), + ) + assert agg.name == "total_txn_amount" + assert agg.time_window == timedelta(days=30) + + +def test_input_schema_proto_roundtrip(): + """An ODFV with input_schema survives a to_proto / from_proto round-trip.""" + + @on_demand_feature_view( + input_schema=[ + Field(name="txn_amount", dtype=Float64), + ], + schema=[ + Field(name="total_txn_amount", dtype=Float64), + ], + aggregations=[ + Aggregation( + column="txn_amount", + function="sum", + name="total_txn_amount", + time_window=timedelta(days=30), + ), + ], + entities=[user], + ) + def txn_view(df: pd.DataFrame) -> pd.DataFrame: + return df + + proto = txn_view.to_proto() + restored = OnDemandFeatureView.from_proto(proto) + + assert restored.input_schema == txn_view.input_schema + assert restored.aggregations == txn_view.aggregations + sentinel_name = f"{OnDemandFeatureView._INPUT_SCHEMA_SOURCE_PREFIX}txn_view" + assert sentinel_name in restored.source_request_sources + + +def test_input_schema_copy(): + """__copy__ preserves input_schema and aggregations.""" + + @on_demand_feature_view( + input_schema=[ + Field(name="txn_amount", dtype=Float64), + ], + schema=[ + Field(name="total_txn_amount", dtype=Float64), + ], + aggregations=[ + Aggregation(column="txn_amount", function="sum", name="total_txn_amount"), + ], + entities=[user], + ) + def copy_view(df: pd.DataFrame) -> pd.DataFrame: + return df + + cloned = copy.copy(copy_view) + assert cloned.input_schema == copy_view.input_schema + assert cloned.aggregations == copy_view.aggregations + sentinel_name = f"{OnDemandFeatureView._INPUT_SCHEMA_SOURCE_PREFIX}copy_view" + assert sentinel_name in cloned.source_request_sources + + +def test_sources_required_without_input_schema(): + """Constructor raises if neither sources nor input_schema is provided.""" + with pytest.raises( + (ValueError), + ): + + def dummy(df): + return df + + OnDemandFeatureView( + name="bad_view", + schema=[Field(name="out", dtype=Float64)], + udf=dummy, + ) diff --git a/sdk/python/tests/unit/test_on_demand_pandas_transformation.py b/sdk/python/tests/unit/test_on_demand_pandas_transformation.py index c5ab657becc..d11fed82dfd 100644 --- a/sdk/python/tests/unit/test_on_demand_pandas_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_pandas_transformation.py @@ -341,3 +341,259 @@ def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: ), ): store.apply([request_source, pandas_view]) + + +def test_odfv_udf_does_not_receive_undeclared_source_columns(): + """#6158: a pandas ODFV's UDF should only see its own source columns, not + features from another feature view that was requested too.""" + with tempfile.TemporaryDirectory() as data_dir: + store = FeatureStore( + config=RepoConfig( + project="test_odfv_source_isolation", + registry=os.path.join(data_dir, "registry.db"), + provider="local", + entity_key_serialization_version=3, + online_store=SqliteOnlineStoreConfig( + path=os.path.join(data_dir, "online.db") + ), + ) + ) + + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + start_date = end_date - timedelta(days=15) + driver_df = create_driver_hourly_stats_df([1001], start_date, end_date) + driver_stats_path = os.path.join(data_dir, "driver_stats.parquet") + driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) + + driver = Entity(name="driver", join_keys=["driver_id"]) + source = FileSource( + name="driver_hourly_stats_source", + path=driver_stats_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + # Two feature views from the same source. The ODFV declares only fv_declared. + fv_declared = FeatureView( + name="fv_declared", + entities=[driver], + ttl=timedelta(days=0), + schema=[Field(name="conv_rate", dtype=Float32)], + online=True, + source=source, + ) + fv_undeclared = FeatureView( + name="fv_undeclared", + entities=[driver], + ttl=timedelta(days=0), + schema=[Field(name="avg_daily_trips", dtype=Int64)], + online=True, + source=source, + ) + + @on_demand_feature_view( + sources=[fv_declared], + schema=[ + Field(name="saw_undeclared", dtype=Bool), + Field(name="saw_declared", dtype=Bool), + Field(name="saw_join_key", dtype=Bool), + ], + mode="pandas", + ) + def guard_view(inputs: pd.DataFrame) -> pd.DataFrame: + out = pd.DataFrame() + n = range(len(inputs)) + out["saw_undeclared"] = ["avg_daily_trips" in inputs.columns for _ in n] + out["saw_declared"] = ["conv_rate" in inputs.columns for _ in n] + out["saw_join_key"] = ["driver_id" in inputs.columns for _ in n] + return out + + store.apply([driver, source, fv_declared, fv_undeclared, guard_view]) + store.write_to_online_store(feature_view_name="fv_declared", df=driver_df) + store.write_to_online_store(feature_view_name="fv_undeclared", df=driver_df) + + response = store.get_online_features( + entity_rows=[{"driver_id": 1001}], + features=[ + "fv_declared:conv_rate", + "fv_undeclared:avg_daily_trips", + "guard_view:saw_undeclared", + "guard_view:saw_declared", + "guard_view:saw_join_key", + ], + ).to_dict() + + # the other FV's column should be hidden; ours and the join key should not + assert response["saw_undeclared"] == [False] + assert response["saw_declared"] == [True] + assert response["saw_join_key"] == [True] + + +def _two_fv_store(data_dir): + store = FeatureStore( + config=RepoConfig( + project="test_odfv_source_isolation", + registry=os.path.join(data_dir, "registry.db"), + provider="local", + entity_key_serialization_version=3, + online_store=SqliteOnlineStoreConfig( + path=os.path.join(data_dir, "online.db") + ), + ) + ) + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + start_date = end_date - timedelta(days=15) + driver_df = create_driver_hourly_stats_df([1001], start_date, end_date) + path = os.path.join(data_dir, "driver_stats.parquet") + driver_df.to_parquet(path=path, allow_truncated_timestamps=True) + driver = Entity(name="driver", join_keys=["driver_id"]) + src = FileSource( + name="driver_hourly_stats_source", + path=path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + fv1 = FeatureView( + name="fv1", + entities=[driver], + ttl=timedelta(days=0), + schema=[Field(name="conv_rate", dtype=Float32)], + online=True, + source=src, + ) + fv2 = FeatureView( + name="fv2", + entities=[driver], + ttl=timedelta(days=0), + schema=[Field(name="avg_daily_trips", dtype=Int64)], + online=True, + source=src, + ) + return store, driver, src, fv1, fv2, driver_df + + +def test_odfv_python_mode_does_not_receive_undeclared_source_columns(): + """Same as above but for python mode, where the UDF gets a dict (#6158).""" + with tempfile.TemporaryDirectory() as data_dir: + store, driver, src, fv1, fv2, driver_df = _two_fv_store(data_dir) + + @on_demand_feature_view( + sources=[fv1], + schema=[ + Field(name="saw_undeclared", dtype=Bool), + Field(name="saw_declared", dtype=Bool), + ], + mode="python", + ) + def guard_py(inputs: dict) -> dict: + return { + "saw_undeclared": ["avg_daily_trips" in inputs], + "saw_declared": ["conv_rate" in inputs], + } + + store.apply([driver, src, fv1, fv2, guard_py]) + store.write_to_online_store(feature_view_name="fv1", df=driver_df) + store.write_to_online_store(feature_view_name="fv2", df=driver_df) + + response = store.get_online_features( + entity_rows=[{"driver_id": 1001}], + features=[ + "fv1:conv_rate", + "fv2:avg_daily_trips", + "guard_py:saw_undeclared", + "guard_py:saw_declared", + ], + ).to_dict() + + assert response["saw_undeclared"] == [False] + assert response["saw_declared"] == [True] + + +def test_odfv_does_not_see_other_requested_odfv_source_columns(): + """#6158: one ODFV's UDF should not see another ODFV's source column, even + when that feature view is only in the response as the other ODFV's input.""" + with tempfile.TemporaryDirectory() as data_dir: + store, driver, src, fv1, fv2, driver_df = _two_fv_store(data_dir) + + @on_demand_feature_view( + sources=[fv1], + schema=[Field(name="a_saw_fv2", dtype=Bool)], + mode="pandas", + ) + def odfv_a(inputs: pd.DataFrame) -> pd.DataFrame: + out = pd.DataFrame() + out["a_saw_fv2"] = [ + "avg_daily_trips" in inputs.columns for _ in range(len(inputs)) + ] + return out + + @on_demand_feature_view( + sources=[fv2], + schema=[Field(name="b_out", dtype=Int64)], + mode="pandas", + ) + def odfv_b(inputs: pd.DataFrame) -> pd.DataFrame: + out = pd.DataFrame() + out["b_out"] = list(inputs["avg_daily_trips"]) + return out + + store.apply([driver, src, fv1, fv2, odfv_a, odfv_b]) + store.write_to_online_store(feature_view_name="fv1", df=driver_df) + store.write_to_online_store(feature_view_name="fv2", df=driver_df) + + # ask for just the two ODFVs - fv2 is only here as odfv_b's input + response = store.get_online_features( + entity_rows=[{"driver_id": 1001}], + features=["odfv_a:a_saw_fv2", "odfv_b:b_out"], + ).to_dict() + + # odfv_a shouldn't see fv2's column; odfv_b still uses it fine + assert response["a_saw_fv2"] == [False] + assert response["b_out"] is not None + + +def test_odfv_udf_receives_aliased_declared_source_columns(): + """A declared source referenced under an alias (``with_name``) must still reach + the UDF. The isolation filter keys columns by ``projection.name``, which is what + the retrieval path emits regardless of the alias, so the aliased source's feature + is retained (not dropped) while an unrelated feature view stays hidden (#6158).""" + with tempfile.TemporaryDirectory() as data_dir: + store, driver, src, fv1, fv2, driver_df = _two_fv_store(data_dir) + + # ODFV declares fv1 *under an alias*; fv2 is requested but undeclared. + aliased_source = fv1.with_name("aliased_fv1") + assert aliased_source.projection.name == "fv1" + assert aliased_source.projection.name_to_use() == "aliased_fv1" + + @on_demand_feature_view( + sources=[aliased_source], + schema=[ + Field(name="saw_declared", dtype=Bool), + Field(name="saw_undeclared", dtype=Bool), + ], + mode="pandas", + ) + def aliased_guard(inputs: pd.DataFrame) -> pd.DataFrame: + out = pd.DataFrame() + n = range(len(inputs)) + out["saw_declared"] = ["conv_rate" in inputs.columns for _ in n] + out["saw_undeclared"] = ["avg_daily_trips" in inputs.columns for _ in n] + return out + + store.apply([driver, src, fv1, fv2, aliased_guard]) + store.write_to_online_store(feature_view_name="fv1", df=driver_df) + store.write_to_online_store(feature_view_name="fv2", df=driver_df) + + response = store.get_online_features( + entity_rows=[{"driver_id": 1001}], + features=[ + "fv1:conv_rate", + "fv2:avg_daily_trips", + "aliased_guard:saw_declared", + "aliased_guard:saw_undeclared", + ], + ).to_dict() + + # the aliased-but-declared source must survive; the unrelated FV stays hidden + assert response["saw_declared"] == [True] + assert response["saw_undeclared"] == [False] diff --git a/sdk/python/tests/unit/test_on_demand_python_transformation.py b/sdk/python/tests/unit/test_on_demand_python_transformation.py index 9a09037d422..3ce02255c7c 100644 --- a/sdk/python/tests/unit/test_on_demand_python_transformation.py +++ b/sdk/python/tests/unit/test_on_demand_python_transformation.py @@ -45,202 +45,201 @@ class TestOnDemandPythonTransformation(unittest.TestCase): def setUp(self): - with tempfile.TemporaryDirectory() as data_dir: - self.store = FeatureStore( - config=RepoConfig( - project="test_on_demand_python_transformation", - registry=os.path.join(data_dir, "registry.db"), - provider="local", - entity_key_serialization_version=3, - online_store=SqliteOnlineStoreConfig( - path=os.path.join(data_dir, "online.db") - ), - ) + self.data_dir = tempfile.mkdtemp() + data_dir = self.data_dir + self.store = FeatureStore( + config=RepoConfig( + project="test_on_demand_python_transformation", + registry=os.path.join(data_dir, "registry.db"), + provider="local", + entity_key_serialization_version=3, + online_store=SqliteOnlineStoreConfig( + path=os.path.join(data_dir, "online.db") + ), ) + ) - # Generate test data. - end_date = datetime.now().replace(microsecond=0, second=0, minute=0) - start_date = end_date - timedelta(days=15) + # Generate test data. + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + start_date = end_date - timedelta(days=15) - driver_entities = [1001, 1002, 1003, 1004, 1005] - driver_df = create_driver_hourly_stats_df( - driver_entities, start_date, end_date - ) - driver_stats_path = os.path.join(data_dir, "driver_stats.parquet") - driver_df.to_parquet( - path=driver_stats_path, allow_truncated_timestamps=True - ) + driver_entities = [1001, 1002, 1003, 1004, 1005] + driver_df = create_driver_hourly_stats_df(driver_entities, start_date, end_date) + driver_stats_path = os.path.join(data_dir, "driver_stats.parquet") + driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) - driver = Entity( - name="driver", join_keys=["driver_id"], value_type=ValueType.INT64 - ) + driver = Entity( + name="driver", join_keys=["driver_id"], value_type=ValueType.INT64 + ) - driver_stats_source = FileSource( - name="driver_hourly_stats_source", - path=driver_stats_path, - timestamp_field="event_timestamp", - created_timestamp_column="created", - ) - input_request_source = RequestSource( - name="counter_source", - schema=[ - Field(name="counter", dtype=Int64), - Field(name="input_datetime", dtype=UnixTimestamp), - ], - ) + driver_stats_source = FileSource( + name="driver_hourly_stats_source", + path=driver_stats_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + input_request_source = RequestSource( + name="counter_source", + schema=[ + Field(name="counter", dtype=Int64), + Field(name="input_datetime", dtype=UnixTimestamp), + ], + ) - driver_stats_fv = FeatureView( - name="driver_hourly_stats", - entities=[driver], - ttl=timedelta(days=0), - schema=[ - Field(name="conv_rate", dtype=Float32), - Field(name="acc_rate", dtype=Float32), - Field(name="avg_daily_trips", dtype=Int64), - ], - online=True, - source=driver_stats_source, - ) + driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=0), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, + ) - driver_stats_entity_less_fv = FeatureView( - name="driver_hourly_stats_no_entity", - entities=[], - ttl=timedelta(days=0), - schema=[ - Field(name="conv_rate", dtype=Float32), - Field(name="acc_rate", dtype=Float32), - Field(name="avg_daily_trips", dtype=Int64), - ], - online=True, - source=driver_stats_source, - ) + driver_stats_entity_less_fv = FeatureView( + name="driver_hourly_stats_no_entity", + entities=[], + ttl=timedelta(days=0), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, + ) - @on_demand_feature_view( - sources=[driver_stats_fv], - schema=[Field(name="conv_rate_plus_acc_pandas", dtype=Float64)], - mode="pandas", - ) - def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: - df = pd.DataFrame() - df["conv_rate_plus_acc_pandas"] = ( - inputs["conv_rate"] + inputs["acc_rate"] - ) - return df + @on_demand_feature_view( + sources=[driver_stats_fv], + schema=[Field(name="conv_rate_plus_acc_pandas", dtype=Float64)], + mode="pandas", + ) + def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_acc_pandas"] = inputs["conv_rate"] + inputs["acc_rate"] + return df - @on_demand_feature_view( - sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], - schema=[Field(name="conv_rate_plus_acc_python", dtype=Float64)], - mode="python", - ) - def python_view(inputs: dict[str, Any]) -> dict[str, Any]: - output: dict[str, Any] = { - "conv_rate_plus_acc_python": conv_rate + acc_rate + @on_demand_feature_view( + sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], + schema=[Field(name="conv_rate_plus_acc_python", dtype=Float64)], + mode="python", + ) + def python_view(inputs: dict[str, Any]) -> dict[str, Any]: + output: dict[str, Any] = { + "conv_rate_plus_acc_python": conv_rate + acc_rate + for conv_rate, acc_rate in zip(inputs["conv_rate"], inputs["acc_rate"]) + } + return output + + @on_demand_feature_view( + sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], + schema=[ + Field(name="conv_rate_plus_val1_python", dtype=Float64), + Field(name="conv_rate_plus_val2_python", dtype=Float64), + ], + mode="python", + ) + def python_demo_view(inputs: dict[str, Any]) -> dict[str, Any]: + output: dict[str, Any] = { + "conv_rate_plus_val1_python": [ + conv_rate + acc_rate for conv_rate, acc_rate in zip( inputs["conv_rate"], inputs["acc_rate"] ) - } - return output - - @on_demand_feature_view( - sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], - schema=[ - Field(name="conv_rate_plus_val1_python", dtype=Float64), - Field(name="conv_rate_plus_val2_python", dtype=Float64), ], - mode="python", - ) - def python_demo_view(inputs: dict[str, Any]) -> dict[str, Any]: - output: dict[str, Any] = { - "conv_rate_plus_val1_python": [ - conv_rate + acc_rate - for conv_rate, acc_rate in zip( - inputs["conv_rate"], inputs["acc_rate"] - ) - ], - "conv_rate_plus_val2_python": [ - conv_rate + acc_rate - for conv_rate, acc_rate in zip( - inputs["conv_rate"], inputs["acc_rate"] - ) - ], - } - return output - - @on_demand_feature_view( - sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], - schema=[ - Field(name="conv_rate_plus_acc_python_singleton", dtype=Float64), - Field( - name="conv_rate_plus_acc_python_singleton_array", - dtype=Array(Float64), - ), + "conv_rate_plus_val2_python": [ + conv_rate + acc_rate + for conv_rate, acc_rate in zip( + inputs["conv_rate"], inputs["acc_rate"] + ) ], - mode="python", - singleton=True, + } + return output + + @on_demand_feature_view( + sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], + schema=[ + Field(name="conv_rate_plus_acc_python_singleton", dtype=Float64), + Field( + name="conv_rate_plus_acc_python_singleton_array", + dtype=Array(Float64), + ), + ], + mode="python", + singleton=True, + ) + def python_singleton_view(inputs: dict[str, Any]) -> dict[str, Any]: + output: dict[str, Any] = dict(conv_rate_plus_acc_python=float("-inf")) + output["conv_rate_plus_acc_python_singleton"] = ( + inputs["conv_rate"] + inputs["acc_rate"] ) - def python_singleton_view(inputs: dict[str, Any]) -> dict[str, Any]: - output: dict[str, Any] = dict(conv_rate_plus_acc_python=float("-inf")) - output["conv_rate_plus_acc_python_singleton"] = ( - inputs["conv_rate"] + inputs["acc_rate"] - ) - output["conv_rate_plus_acc_python_singleton_array"] = [0.1, 0.2, 0.3] - return output + output["conv_rate_plus_acc_python_singleton_array"] = [0.1, 0.2, 0.3] + return output - @on_demand_feature_view( - sources=[ - driver_stats_fv[["conv_rate", "acc_rate"]], - input_request_source, - ], - schema=[ - Field(name="conv_rate_plus_acc", dtype=Float64), - Field(name="current_datetime", dtype=UnixTimestamp), - Field(name="counter", dtype=Int64), - Field(name="input_datetime", dtype=UnixTimestamp), + @on_demand_feature_view( + sources=[ + driver_stats_fv[["conv_rate", "acc_rate"]], + input_request_source, + ], + schema=[ + Field(name="conv_rate_plus_acc", dtype=Float64), + Field(name="current_datetime", dtype=UnixTimestamp), + Field(name="counter", dtype=Int64), + Field(name="input_datetime", dtype=UnixTimestamp), + ], + mode="python", + write_to_online_store=True, + ) + def python_stored_writes_feature_view( + inputs: dict[str, Any], + ) -> dict[str, Any]: + output: dict[str, Any] = { + "conv_rate_plus_acc": [ + conv_rate + acc_rate + for conv_rate, acc_rate in zip( + inputs["conv_rate"], inputs["acc_rate"] + ) ], - mode="python", - write_to_online_store=True, - ) - def python_stored_writes_feature_view( - inputs: dict[str, Any], - ) -> dict[str, Any]: - output: dict[str, Any] = { - "conv_rate_plus_acc": [ - conv_rate + acc_rate - for conv_rate, acc_rate in zip( - inputs["conv_rate"], inputs["acc_rate"] - ) - ], - "current_datetime": [datetime.now() for _ in inputs["conv_rate"]], - "counter": [c + 1 for c in inputs["counter"]], - "input_datetime": [d for d in inputs["input_datetime"]], - } - return output + "current_datetime": [datetime.now() for _ in inputs["conv_rate"]], + "counter": [c + 1 for c in inputs["counter"]], + "input_datetime": [d for d in inputs["input_datetime"]], + } + return output - self.store.apply( - [ - driver, - driver_stats_source, - driver_stats_fv, - pandas_view, - python_view, - python_singleton_view, - python_demo_view, - driver_stats_entity_less_fv, - python_stored_writes_feature_view, - ] - ) - self.store.write_to_online_store( - feature_view_name="driver_hourly_stats", df=driver_df - ) - assert driver_stats_fv.entity_columns == [ - Field(name=driver.join_key, dtype=from_value_type(driver.value_type)) + self.store.apply( + [ + driver, + driver_stats_source, + driver_stats_fv, + pandas_view, + python_view, + python_singleton_view, + python_demo_view, + driver_stats_entity_less_fv, + python_stored_writes_feature_view, ] - assert driver_stats_entity_less_fv.entity_columns == [DUMMY_ENTITY_FIELD] + ) + self.store.write_to_online_store( + feature_view_name="driver_hourly_stats", df=driver_df + ) + assert driver_stats_fv.entity_columns == [ + Field(name=driver.join_key, dtype=from_value_type(driver.value_type)) + ] + assert driver_stats_entity_less_fv.entity_columns == [DUMMY_ENTITY_FIELD] - assert len(self.store.list_all_feature_views()) == 7 - assert len(self.store.list_feature_views()) == 2 - assert len(self.store.list_on_demand_feature_views()) == 5 - assert len(self.store.list_stream_feature_views()) == 0 + assert len(self.store.list_all_feature_views()) == 7 + assert len(self.store.list_feature_views()) == 2 + assert len(self.store.list_on_demand_feature_views()) == 5 + assert len(self.store.list_stream_feature_views()) == 0 + + def tearDown(self): + import shutil + + if hasattr(self, "data_dir"): + shutil.rmtree(self.data_dir, ignore_errors=True) def test_setup(self): pass @@ -401,291 +400,292 @@ def test_stored_writes(self): class TestOnDemandPythonTransformationAllDataTypes(unittest.TestCase): def setUp(self): - with tempfile.TemporaryDirectory() as data_dir: - self.store = FeatureStore( - config=RepoConfig( - project="test_on_demand_python_transformation", - registry=os.path.join(data_dir, "registry.db"), - provider="local", - entity_key_serialization_version=3, - online_store=SqliteOnlineStoreConfig( - path=os.path.join(data_dir, "online.db") - ), - ) + self.data_dir = tempfile.mkdtemp() + data_dir = self.data_dir + self.store = FeatureStore( + config=RepoConfig( + project="test_on_demand_python_transformation", + registry=os.path.join(data_dir, "registry.db"), + provider="local", + entity_key_serialization_version=3, + online_store=SqliteOnlineStoreConfig( + path=os.path.join(data_dir, "online.db") + ), ) + ) - # Generate test data. - end_date = datetime.now().replace(microsecond=0, second=0, minute=0) - start_date = end_date - timedelta(days=15) + # Generate test data. + end_date = datetime.now().replace(microsecond=0, second=0, minute=0) + start_date = end_date - timedelta(days=15) - driver_entities = [1001, 1002, 1003, 1004, 1005] - driver_df = create_driver_hourly_stats_df( - driver_entities, start_date, end_date - ) - driver_stats_path = os.path.join(data_dir, "driver_stats.parquet") - driver_df.to_parquet( - path=driver_stats_path, allow_truncated_timestamps=True - ) + driver_entities = [1001, 1002, 1003, 1004, 1005] + driver_df = create_driver_hourly_stats_df(driver_entities, start_date, end_date) + driver_stats_path = os.path.join(data_dir, "driver_stats.parquet") + driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) - driver = Entity(name="driver", join_keys=["driver_id"]) + driver = Entity(name="driver", join_keys=["driver_id"]) - driver_stats_source = FileSource( - name="driver_hourly_stats_source", - path=driver_stats_path, - timestamp_field="event_timestamp", - created_timestamp_column="created", - ) + driver_stats_source = FileSource( + name="driver_hourly_stats_source", + path=driver_stats_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) - driver_stats_fv = FeatureView( - name="driver_hourly_stats", - entities=[driver], - ttl=timedelta(days=0), - schema=[ - Field(name="conv_rate", dtype=Float32), - Field(name="acc_rate", dtype=Float32), - Field(name="avg_daily_trips", dtype=Int64), - ], - online=True, - source=driver_stats_source, - ) - assert driver_stats_fv.entities == [driver.name] - assert driver_stats_fv.entity_columns == [] + driver_stats_fv = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=0), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, + ) + assert driver_stats_fv.entities == [driver.name] + assert driver_stats_fv.entity_columns == [] - request_source = RequestSource( - name="request_source", - schema=[ - Field(name="avg_daily_trip_rank_thresholds", dtype=Array(Int64)), - Field(name="avg_daily_trip_rank_names", dtype=Array(String)), - ], - ) - input_request = RequestSource( - name="vals_to_add", - schema=[ - Field(name="val_to_add", dtype=Int64), - Field(name="val_to_add_2", dtype=Int64), - ], - ) + request_source = RequestSource( + name="request_source", + schema=[ + Field(name="avg_daily_trip_rank_thresholds", dtype=Array(Int64)), + Field(name="avg_daily_trip_rank_names", dtype=Array(String)), + ], + ) + input_request = RequestSource( + name="vals_to_add", + schema=[ + Field(name="val_to_add", dtype=Int64), + Field(name="val_to_add_2", dtype=Int64), + ], + ) - @on_demand_feature_view( - sources=[request_source, driver_stats_fv], - schema=[ - Field(name="highest_achieved_rank", dtype=String), - Field(name="avg_daily_trips_plus_one", dtype=Int64), - Field(name="conv_rate_plus_acc", dtype=Float64), - Field(name="is_highest_rank", dtype=Bool), - Field(name="achieved_ranks", dtype=Array(String)), - Field(name="trips_until_next_rank_int", dtype=Array(Int64)), - Field(name="trips_until_next_rank_float", dtype=Array(Float64)), - Field(name="achieved_ranks_mask", dtype=Array(Bool)), - ], - mode="python", - ) - def python_view(inputs: dict[str, Any]) -> dict[str, Any]: - output = {} - trips_until_next_rank = [ - [max(threshold - row[1], 0) for threshold in row[0]] - for row in zip( - inputs["avg_daily_trip_rank_thresholds"], - inputs["avg_daily_trips"], - ) - ] - mask = [[value <= 0 for value in row] for row in trips_until_next_rank] - ranks = [ - [rank if mask else "Locked" for mask, rank in zip(*row)] - for row in zip(mask, inputs["avg_daily_trip_rank_names"]) - ] - highest_rank = [ - ([rank for rank in row if rank != "Locked"][-1:] or ["None"])[0] - for row in ranks - ] + @on_demand_feature_view( + sources=[request_source, driver_stats_fv], + schema=[ + Field(name="highest_achieved_rank", dtype=String), + Field(name="avg_daily_trips_plus_one", dtype=Int64), + Field(name="conv_rate_plus_acc", dtype=Float64), + Field(name="is_highest_rank", dtype=Bool), + Field(name="achieved_ranks", dtype=Array(String)), + Field(name="trips_until_next_rank_int", dtype=Array(Int64)), + Field(name="trips_until_next_rank_float", dtype=Array(Float64)), + Field(name="achieved_ranks_mask", dtype=Array(Bool)), + ], + mode="python", + ) + def python_view(inputs: dict[str, Any]) -> dict[str, Any]: + output = {} + trips_until_next_rank = [ + [max(threshold - row[1], 0) for threshold in row[0]] + for row in zip( + inputs["avg_daily_trip_rank_thresholds"], + inputs["avg_daily_trips"], + ) + ] + mask = [[value <= 0 for value in row] for row in trips_until_next_rank] + ranks = [ + [rank if mask else "Locked" for mask, rank in zip(*row)] + for row in zip(mask, inputs["avg_daily_trip_rank_names"]) + ] + highest_rank = [ + ([rank for rank in row if rank != "Locked"][-1:] or ["None"])[0] + for row in ranks + ] - output["conv_rate_plus_acc"] = [ - sum(row) for row in zip(inputs["conv_rate"], inputs["acc_rate"]) - ] - output["avg_daily_trips_plus_one"] = [ - row + 1 for row in inputs["avg_daily_trips"] - ] - output["highest_achieved_rank"] = highest_rank - output["is_highest_rank"] = [row[-1] != "Locked" for row in ranks] + output["conv_rate_plus_acc"] = [ + sum(row) for row in zip(inputs["conv_rate"], inputs["acc_rate"]) + ] + output["avg_daily_trips_plus_one"] = [ + row + 1 for row in inputs["avg_daily_trips"] + ] + output["highest_achieved_rank"] = highest_rank + output["is_highest_rank"] = [row[-1] != "Locked" for row in ranks] - output["trips_until_next_rank_int"] = trips_until_next_rank - output["trips_until_next_rank_float"] = [ - [float(value) for value in row] for row in trips_until_next_rank - ] - output["achieved_ranks_mask"] = mask - output["achieved_ranks"] = ranks - return output + output["trips_until_next_rank_int"] = trips_until_next_rank + output["trips_until_next_rank_float"] = [ + [float(value) for value in row] for row in trips_until_next_rank + ] + output["achieved_ranks_mask"] = mask + output["achieved_ranks"] = ranks + return output - @on_demand_feature_view( - sources=[ - driver_stats_fv, - input_request, - ], - schema=[ - Field(name="conv_rate_plus_val1", dtype=Float64), - Field(name="conv_rate_plus_val2", dtype=Float64), - ], - mode="pandas", + @on_demand_feature_view( + sources=[ + driver_stats_fv, + input_request, + ], + schema=[ + Field(name="conv_rate_plus_val1", dtype=Float64), + Field(name="conv_rate_plus_val2", dtype=Float64), + ], + mode="pandas", + ) + def pandas_view(features_df: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val1"] = ( + features_df["conv_rate"] + features_df["val_to_add"] ) - def pandas_view(features_df: pd.DataFrame) -> pd.DataFrame: - df = pd.DataFrame() - df["conv_rate_plus_val1"] = ( - features_df["conv_rate"] + features_df["val_to_add"] - ) - df["conv_rate_plus_val2"] = ( - features_df["conv_rate"] + features_df["val_to_add_2"] - ) - return df - - self.store.apply( - [ - driver, - driver_stats_source, - driver_stats_fv, - python_view, - pandas_view, - input_request, - request_source, - ] + df["conv_rate_plus_val2"] = ( + features_df["conv_rate"] + features_df["val_to_add_2"] ) - fv_applied = self.store.get_feature_view("driver_hourly_stats") - assert fv_applied.entities == [driver.name] - # Note here that after apply() is called, the entity_columns are populated with the join_key - assert fv_applied.entity_columns[0].name == driver.join_key + return df - self.store.write_to_online_store( - feature_view_name="driver_hourly_stats", df=driver_df - ) + self.store.apply( + [ + driver, + driver_stats_source, + driver_stats_fv, + python_view, + pandas_view, + input_request, + request_source, + ] + ) + fv_applied = self.store.get_feature_view("driver_hourly_stats") + assert fv_applied.entities == [driver.name] + # Note here that after apply() is called, the entity_columns are populated with the join_key + assert fv_applied.entity_columns[0].name == driver.join_key - batch_sample = pd.DataFrame(driver_entities, columns=["driver_id"]) - batch_sample["val_to_add"] = 0 - batch_sample["val_to_add_2"] = 1 - batch_sample["event_timestamp"] = start_date - batch_sample["created"] = start_date - fv_only_cols = ["driver_id", "event_timestamp", "created"] + self.store.write_to_online_store( + feature_view_name="driver_hourly_stats", df=driver_df + ) - resp_base_fv = self.store.get_historical_features( - entity_df=batch_sample[fv_only_cols], - features=[ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - ], - ).to_df() - assert resp_base_fv is not None - assert sorted(resp_base_fv.columns) == [ - "acc_rate", - "avg_daily_trips", - "conv_rate", - "created__", - "driver_id", - "event_timestamp", - ] - resp = self.store.get_historical_features( - entity_df=batch_sample, - features=[ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "pandas_view:conv_rate_plus_val1", - "pandas_view:conv_rate_plus_val2", - ], - ).to_df() - assert resp is not None - assert resp["conv_rate_plus_val1"].isnull().sum() == 0 - - batch_sample["avg_daily_trip_rank_thresholds"] = [ - [100, 250, 500, 1000] - ] * batch_sample.shape[0] - batch_sample["avg_daily_trip_rank_names"] = [ - ["Bronze", "Silver", "Gold", "Platinum"] - ] * batch_sample.shape[0] - resp_python = self.store.get_historical_features( - entity_df=batch_sample, - features=[ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "python_view:conv_rate_plus_acc", - ], - ).to_df() - assert resp_python is not None - assert resp_python["conv_rate_plus_acc"].isnull().sum() == 0 - - # Now testing feature retrieval for driver ids not in the dataset - missing_batch_sample = pd.DataFrame([1234567890], columns=["driver_id"]) - missing_batch_sample["val_to_add"] = 0 - missing_batch_sample["val_to_add_2"] = 1 - missing_batch_sample["event_timestamp"] = start_date - missing_batch_sample["created"] = start_date - resp_offline = self.store.get_historical_features( - entity_df=missing_batch_sample, - features=[ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "pandas_view:conv_rate_plus_val1", - "pandas_view:conv_rate_plus_val2", - ], - ).to_df() - assert resp_offline is not None - assert resp_offline["conv_rate_plus_val1"].isnull().sum() == 1 - assert sorted(resp_offline.columns) == [ - "acc_rate", - "avg_daily_trips", - "conv_rate", - "conv_rate_plus_val1", - "conv_rate_plus_val2", - "created__", - "driver_id", - "event_timestamp", - "val_to_add", - "val_to_add_2", - ] - resp_online_missing_entity = self.store.get_online_features( - entity_rows=[ - {"driver_id": 1234567890, "val_to_add": 0, "val_to_add_2": 1} - ], - features=[ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "pandas_view:conv_rate_plus_val1", - "pandas_view:conv_rate_plus_val2", - ], - ) - assert resp_online_missing_entity is not None - resp_online = self.store.get_online_features( - entity_rows=[{"driver_id": 1001, "val_to_add": 0, "val_to_add_2": 1}], - features=[ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "pandas_view:conv_rate_plus_val1", - "pandas_view:conv_rate_plus_val2", - ], - ).to_df() - assert resp_online is not None - assert sorted(resp_online.columns) == [ - "acc_rate", - "avg_daily_trips", - "conv_rate", - "conv_rate_plus_val1", - "conv_rate_plus_val2", - "driver_id", - # It does not have the items below - # "created__", - # "event_timestamp", - # "val_to_add", - # "val_to_add_2", - ] - # Note online and offline columns will not match because: - # you want to be space efficient online when considering the impact of network latency so you want to send - # and receive the minimally required set of data, which means after transformation you only need to send the - # output in the response. - # Offline, you will probably prioritize reproducibility and being able to iterate, which means you will want - # the underlying inputs into your transformation, so the extra data is tolerable. - assert sorted(resp_online.columns) != sorted(resp_offline.columns) + batch_sample = pd.DataFrame(driver_entities, columns=["driver_id"]) + batch_sample["val_to_add"] = 0 + batch_sample["val_to_add_2"] = 1 + batch_sample["event_timestamp"] = start_date + batch_sample["created"] = start_date + fv_only_cols = ["driver_id", "event_timestamp", "created"] + + resp_base_fv = self.store.get_historical_features( + entity_df=batch_sample[fv_only_cols], + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + ).to_df() + assert resp_base_fv is not None + assert sorted(resp_base_fv.columns) == [ + "acc_rate", + "avg_daily_trips", + "conv_rate", + "created__", + "driver_id", + "event_timestamp", + ] + resp = self.store.get_historical_features( + entity_df=batch_sample, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_val1", + "pandas_view:conv_rate_plus_val2", + ], + ).to_df() + assert resp is not None + assert resp["conv_rate_plus_val1"].isnull().sum() == 0 + + batch_sample["avg_daily_trip_rank_thresholds"] = [ + [100, 250, 500, 1000] + ] * batch_sample.shape[0] + batch_sample["avg_daily_trip_rank_names"] = [ + ["Bronze", "Silver", "Gold", "Platinum"] + ] * batch_sample.shape[0] + resp_python = self.store.get_historical_features( + entity_df=batch_sample, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "python_view:conv_rate_plus_acc", + ], + ).to_df() + assert resp_python is not None + assert resp_python["conv_rate_plus_acc"].isnull().sum() == 0 + + # Now testing feature retrieval for driver ids not in the dataset + missing_batch_sample = pd.DataFrame([1234567890], columns=["driver_id"]) + missing_batch_sample["val_to_add"] = 0 + missing_batch_sample["val_to_add_2"] = 1 + missing_batch_sample["event_timestamp"] = start_date + missing_batch_sample["created"] = start_date + resp_offline = self.store.get_historical_features( + entity_df=missing_batch_sample, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_val1", + "pandas_view:conv_rate_plus_val2", + ], + ).to_df() + assert resp_offline is not None + assert resp_offline["conv_rate_plus_val1"].isnull().sum() == 1 + assert sorted(resp_offline.columns) == [ + "acc_rate", + "avg_daily_trips", + "conv_rate", + "conv_rate_plus_val1", + "conv_rate_plus_val2", + "created__", + "driver_id", + "event_timestamp", + "val_to_add", + "val_to_add_2", + ] + resp_online_missing_entity = self.store.get_online_features( + entity_rows=[{"driver_id": 1234567890, "val_to_add": 0, "val_to_add_2": 1}], + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_val1", + "pandas_view:conv_rate_plus_val2", + ], + ) + assert resp_online_missing_entity is not None + resp_online = self.store.get_online_features( + entity_rows=[{"driver_id": 1001, "val_to_add": 0, "val_to_add_2": 1}], + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "pandas_view:conv_rate_plus_val1", + "pandas_view:conv_rate_plus_val2", + ], + ).to_df() + assert resp_online is not None + assert sorted(resp_online.columns) == [ + "acc_rate", + "avg_daily_trips", + "conv_rate", + "conv_rate_plus_val1", + "conv_rate_plus_val2", + "driver_id", + # It does not have the items below + # "created__", + # "event_timestamp", + # "val_to_add", + # "val_to_add_2", + ] + # Note online and offline columns will not match because: + # you want to be space efficient online when considering the impact of network latency so you want to send + # and receive the minimally required set of data, which means after transformation you only need to send the + # output in the response. + # Offline, you will probably prioritize reproducibility and being able to iterate, which means you will want + # the underlying inputs into your transformation, so the extra data is tolerable. + assert sorted(resp_online.columns) != sorted(resp_offline.columns) + + def tearDown(self): + import shutil + + if hasattr(self, "data_dir"): + shutil.rmtree(self.data_dir, ignore_errors=True) def test_setup(self): pass diff --git a/sdk/python/tests/unit/test_precomputed_feature_vectors.py b/sdk/python/tests/unit/test_precomputed_feature_vectors.py new file mode 100644 index 00000000000..80b0b83f53f --- /dev/null +++ b/sdk/python/tests/unit/test_precomputed_feature_vectors.py @@ -0,0 +1,933 @@ +"""Tests for pre-computed feature vectors (issue #6185).""" + +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock + +import pytest +from google.protobuf.timestamp_pb2 import Timestamp + +from feast.feature_service import FeatureService +from feast.feature_view import FeatureView +from feast.field import Field +from feast.infra.offline_stores.file_source import FileSource +from feast.on_demand_feature_view import OnDemandFeatureView +from feast.protos.feast.core.PrecomputedFeatureVector_pb2 import ( + FeatureViewTimestamp, + PrecomputedFeatureVector, +) +from feast.protos.feast.serving.ServingService_pb2 import ( + FieldStatus, + GetOnlineFeaturesResponse, +) +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.types import Float32, Int64 + + +def _make_file_source(name="src"): + return FileSource(name=name, path="test.parquet") + + +def _make_fv(name, features, source=None, ttl=None): + return FeatureView( + name=name, + entities=[], + schema=features, + source=source or _make_file_source(f"{name}_src"), + ttl=ttl or timedelta(0), + ) + + +def _make_timestamp(dt=None): + ts = Timestamp() + ts.FromDatetime(dt or datetime.now(tz=timezone.utc)) + return ts + + +def _make_vector(feature_names, values, fv_timestamps=None, precomputed_at=None): + return PrecomputedFeatureVector( + feature_names=feature_names, + values=values, + fv_timestamps=fv_timestamps or [], + precomputed_at=precomputed_at or _make_timestamp(), + ) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# Proto round-trip +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestPrecomputedFeatureVectorProto: + def test_serialize_deserialize(self): + now = _make_timestamp() + fv_ts = FeatureViewTimestamp(feature_view_name="driver_fv", event_timestamp=now) + vector = _make_vector( + feature_names=["driver_fv__trips", "driver_fv__rating"], + values=[ValueProto(int64_val=100), ValueProto(float_val=4.5)], + fv_timestamps=[fv_ts], + precomputed_at=now, + ) + + blob = vector.SerializeToString() + parsed = PrecomputedFeatureVector() + parsed.ParseFromString(blob) + + assert list(parsed.feature_names) == ["driver_fv__trips", "driver_fv__rating"] + assert parsed.values[0].int64_val == 100 + assert parsed.values[1].float_val == pytest.approx(4.5) + assert parsed.fv_timestamps[0].feature_view_name == "driver_fv" + + def test_empty_vector(self): + vector = PrecomputedFeatureVector() + blob = vector.SerializeToString() + parsed = PrecomputedFeatureVector() + parsed.ParseFromString(blob) + assert list(parsed.feature_names) == [] + assert list(parsed.values) == [] + + def test_multiple_fv_timestamps(self): + ts1 = _make_timestamp(datetime(2025, 1, 1, tzinfo=timezone.utc)) + ts2 = _make_timestamp(datetime(2025, 6, 1, tzinfo=timezone.utc)) + + vector = _make_vector( + feature_names=["fv1__a", "fv2__b"], + values=[ValueProto(float_val=1.0), ValueProto(float_val=2.0)], + fv_timestamps=[ + FeatureViewTimestamp(feature_view_name="fv1", event_timestamp=ts1), + FeatureViewTimestamp(feature_view_name="fv2", event_timestamp=ts2), + ], + ) + blob = vector.SerializeToString() + parsed = PrecomputedFeatureVector() + parsed.ParseFromString(blob) + + assert len(parsed.fv_timestamps) == 2 + assert parsed.fv_timestamps[0].feature_view_name == "fv1" + assert parsed.fv_timestamps[1].feature_view_name == "fv2" + assert parsed.fv_timestamps[0].event_timestamp.seconds == ts1.seconds + assert parsed.fv_timestamps[1].event_timestamp.seconds == ts2.seconds + + def test_null_values_preserved(self): + vector = _make_vector( + feature_names=["fv1__a", "fv1__b"], + values=[ValueProto(float_val=1.0), ValueProto()], + ) + blob = vector.SerializeToString() + parsed = PrecomputedFeatureVector() + parsed.ParseFromString(blob) + + assert parsed.values[0].float_val == pytest.approx(1.0) + assert not parsed.values[1].HasField("val") + + def test_large_vector(self): + n = 100 + names = [f"fv__feat_{i}" for i in range(n)] + vals = [ValueProto(float_val=float(i)) for i in range(n)] + vector = _make_vector(feature_names=names, values=vals) + blob = vector.SerializeToString() + parsed = PrecomputedFeatureVector() + parsed.ParseFromString(blob) + + assert len(parsed.feature_names) == n + assert len(parsed.values) == n + assert parsed.values[99].float_val == pytest.approx(99.0) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# FeatureService precompute_online flag +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestFeatureServicePrecomputeOnline: + def test_default_is_false(self): + fs = FeatureService(name="svc", features=[]) + assert fs.precompute_online is False + + def test_set_true(self): + fs = FeatureService(name="svc", features=[], precompute_online=True) + assert fs.precompute_online is True + + def test_to_proto_roundtrip_true(self): + fv = _make_fv("fv1", [Field(name="f1", dtype=Float32)]) + fs = FeatureService(name="svc", features=[fv], precompute_online=True) + proto = fs.to_proto() + assert proto.spec.precompute_online is True + restored = FeatureService.from_proto(proto) + assert restored.precompute_online is True + + def test_to_proto_roundtrip_false(self): + fs = FeatureService(name="svc", features=[], precompute_online=False) + proto = fs.to_proto() + assert proto.spec.precompute_online is False + restored = FeatureService.from_proto(proto) + assert restored.precompute_online is False + + def test_equality_includes_precompute(self): + fv = _make_fv("fv1", [Field(name="f1", dtype=Float32)]) + fs1 = FeatureService(name="svc", features=[fv], precompute_online=True) + fs2 = FeatureService(name="svc", features=[fv], precompute_online=False) + assert fs1 != fs2 + + def test_equality_same_precompute(self): + fv = _make_fv("fv1", [Field(name="f1", dtype=Float32)]) + fs1 = FeatureService(name="svc", features=[fv], precompute_online=True) + fs2 = FeatureService(name="svc", features=[fv], precompute_online=True) + assert fs1 == fs2 + + def test_proto_preserves_other_fields(self): + fv = _make_fv("fv1", [Field(name="f1", dtype=Float32)]) + fs = FeatureService( + name="svc", + features=[fv], + precompute_online=True, + description="test desc", + owner="owner@test.com", + tags={"env": "prod"}, + ) + proto = fs.to_proto() + restored = FeatureService.from_proto(proto) + + assert restored.precompute_online is True + assert restored.description == "test desc" + assert restored.owner == "owner@test.com" + assert restored.tags == {"env": "prod"} + + +# ═══════════════════════════════════════════════════════════════════════════════ +# Validation +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestFeatureServiceValidation: + def test_validate_passes_without_precompute(self): + fs = FeatureService(name="svc", features=[], precompute_online=False) + fs.validate() + + def test_validate_passes_precompute_regular_fv(self): + fv = _make_fv("fv1", [Field(name="f1", dtype=Float32)]) + fs = FeatureService(name="svc", features=[fv], precompute_online=True) + fs.validate() + + def test_validate_rejects_odfv_without_write_to_online(self): + fv = _make_fv("fv1", [Field(name="f1", dtype=Float32)]) + + odfv = MagicMock(spec=OnDemandFeatureView) + odfv.name = "odfv1" + odfv.write_to_online_store = False + + fs = FeatureService(name="svc", features=[fv], precompute_online=True) + fs._features = [fv, odfv] + + with pytest.raises(ValueError, match="write_to_online_store=False"): + fs.validate() + + def test_validate_passes_odfv_with_write_to_online(self): + fv = _make_fv("fv1", [Field(name="f1", dtype=Float32)]) + + odfv = MagicMock(spec=OnDemandFeatureView) + odfv.name = "odfv1" + odfv.write_to_online_store = True + + fs = FeatureService(name="svc", features=[fv], precompute_online=True) + fs._features = [fv, odfv] + + fs.validate() + + def test_validate_rejects_multiple_odfvs_without_write(self): + odfv1 = MagicMock(spec=OnDemandFeatureView) + odfv1.name = "odfv1" + odfv1.write_to_online_store = False + + odfv2 = MagicMock(spec=OnDemandFeatureView) + odfv2.name = "odfv2" + odfv2.write_to_online_store = False + + fs = FeatureService(name="svc", features=[], precompute_online=True) + fs._features = [odfv1, odfv2] + + with pytest.raises(ValueError, match="odfv1"): + fs.validate() + + def test_validate_error_message_includes_service_and_odfv_name(self): + odfv = MagicMock(spec=OnDemandFeatureView) + odfv.name = "my_transform" + odfv.write_to_online_store = False + + fs = FeatureService(name="my_service", features=[], precompute_online=True) + fs._features = [odfv] + + with pytest.raises(ValueError) as exc_info: + fs.validate() + + assert "my_service" in str(exc_info.value) + assert "my_transform" in str(exc_info.value) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# Schema mismatch detection (fast path fallback) +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestSchemaMismatchFallback: + def _fast_path(self, blobs, expected_names, grouped_refs=None, num_rows=None): + from feast.infra.online_stores.online_store import OnlineStore + + response = GetOnlineFeaturesResponse(results=[]) + result = OnlineStore._try_precomputed_fast_path( + blobs=blobs, + expected_feature_names=expected_names, + online_features_response=response, + full_feature_names=True, + num_rows=num_rows or len(blobs), + grouped_refs=grouped_refs or [], + registry=MagicMock(), + project="test", + ) + return result, response + + def test_mismatch_returns_false(self): + vector = _make_vector( + feature_names=["fv1__f1", "fv1__f2"], + values=[ValueProto(float_val=1.0), ValueProto(float_val=2.0)], + ) + ok, _ = self._fast_path([vector.SerializeToString()], ["fv1__f1", "fv1__f3"]) + assert ok is False + + def test_none_blob_returns_false(self): + ok, _ = self._fast_path([None], ["fv1__f1"]) + assert ok is False + + def test_matching_schema_returns_true(self): + vector = _make_vector( + feature_names=["fv1__f1"], + values=[ValueProto(float_val=42.0)], + ) + ok, response = self._fast_path([vector.SerializeToString()], ["fv1__f1"]) + assert ok is True + assert len(response.results) == 1 + assert response.results[0].values[0].float_val == pytest.approx(42.0) + + def test_extra_feature_in_expected_returns_false(self): + vector = _make_vector( + feature_names=["fv1__f1"], + values=[ValueProto(float_val=1.0)], + ) + ok, _ = self._fast_path([vector.SerializeToString()], ["fv1__f1", "fv1__f2"]) + assert ok is False + + def test_fewer_features_in_expected_returns_false(self): + vector = _make_vector( + feature_names=["fv1__f1", "fv1__f2"], + values=[ValueProto(float_val=1.0), ValueProto(float_val=2.0)], + ) + ok, _ = self._fast_path([vector.SerializeToString()], ["fv1__f1"]) + assert ok is False + + def test_reordered_features_succeeds_with_correct_values(self): + vector = _make_vector( + feature_names=["fv1__f1", "fv1__f2"], + values=[ValueProto(float_val=1.0), ValueProto(float_val=2.0)], + ) + ok, response = self._fast_path( + [vector.SerializeToString()], ["fv1__f2", "fv1__f1"] + ) + assert ok is True + assert response.results[0].values[0].float_val == pytest.approx(2.0) + assert response.results[1].values[0].float_val == pytest.approx(1.0) + + def test_all_none_blobs_returns_false(self): + ok, _ = self._fast_path([None, None, None], ["fv1__f1"], num_rows=3) + assert ok is False + + def test_mixed_none_and_valid_returns_false(self): + vector = _make_vector( + feature_names=["fv1__f1"], + values=[ValueProto(float_val=1.0)], + ) + ok, _ = self._fast_path( + [vector.SerializeToString(), None], ["fv1__f1"], num_rows=2 + ) + assert ok is False + + def test_schema_mismatch_in_second_entity_returns_false(self): + vec1 = _make_vector( + feature_names=["fv1__f1"], values=[ValueProto(float_val=1.0)] + ) + vec2 = _make_vector( + feature_names=["fv1__f_WRONG"], values=[ValueProto(float_val=2.0)] + ) + ok, _ = self._fast_path( + [vec1.SerializeToString(), vec2.SerializeToString()], + ["fv1__f1"], + num_rows=2, + ) + assert ok is False + + +# ═══════════════════════════════════════════════════════════════════════════════ +# Fast path: multi-entity response building +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestFastPathMultiEntity: + def _fast_path(self, blobs, expected_names, grouped_refs=None, num_rows=None): + from feast.infra.online_stores.online_store import OnlineStore + + response = GetOnlineFeaturesResponse(results=[]) + result = OnlineStore._try_precomputed_fast_path( + blobs=blobs, + expected_feature_names=expected_names, + online_features_response=response, + full_feature_names=True, + num_rows=num_rows or len(blobs), + grouped_refs=grouped_refs or [], + registry=MagicMock(), + project="test", + ) + return result, response + + def test_multiple_entities_all_present(self): + vec1 = _make_vector( + feature_names=["fv1__a", "fv1__b"], + values=[ValueProto(float_val=1.0), ValueProto(float_val=10.0)], + ) + vec2 = _make_vector( + feature_names=["fv1__a", "fv1__b"], + values=[ValueProto(float_val=2.0), ValueProto(float_val=20.0)], + ) + vec3 = _make_vector( + feature_names=["fv1__a", "fv1__b"], + values=[ValueProto(float_val=3.0), ValueProto(float_val=30.0)], + ) + + ok, response = self._fast_path( + [ + vec1.SerializeToString(), + vec2.SerializeToString(), + vec3.SerializeToString(), + ], + ["fv1__a", "fv1__b"], + num_rows=3, + ) + assert ok is True + assert len(response.results) == 2 + + a_values = [v.float_val for v in response.results[0].values] + b_values = [v.float_val for v in response.results[1].values] + + assert a_values == pytest.approx([1.0, 2.0, 3.0]) + assert b_values == pytest.approx([10.0, 20.0, 30.0]) + + def test_statuses_are_present(self): + vec = _make_vector( + feature_names=["fv1__a"], + values=[ValueProto(float_val=1.0)], + ) + ok, response = self._fast_path( + [vec.SerializeToString()], ["fv1__a"], num_rows=1 + ) + assert ok is True + assert response.results[0].statuses[0] == FieldStatus.PRESENT + + def test_feature_names_in_metadata(self): + vec = _make_vector( + feature_names=["fv1__x", "fv2__y"], + values=[ValueProto(float_val=1.0), ValueProto(float_val=2.0)], + ) + ok, response = self._fast_path([vec.SerializeToString()], ["fv1__x", "fv2__y"]) + assert ok is True + assert list(response.metadata.feature_names.val) == ["fv1__x", "fv2__y"] + + +# ═══════════════════════════════════════════════════════════════════════════════ +# TTL enforcement in fast path +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestFastPathTTLEnforcement: + def _fast_path_with_ttl(self, fv_event_dt, fv_ttl_seconds, fv_name="fv1"): + from feast.infra.online_stores.online_store import OnlineStore + + event_ts = _make_timestamp(fv_event_dt) + fv_ts = FeatureViewTimestamp( + feature_view_name=fv_name, event_timestamp=event_ts + ) + vector = _make_vector( + feature_names=[f"{fv_name}__f1"], + values=[ValueProto(float_val=1.0)], + fv_timestamps=[fv_ts], + ) + + fv = _make_fv( + fv_name, + [Field(name="f1", dtype=Float32)], + ttl=timedelta(seconds=fv_ttl_seconds), + ) + grouped_refs = [(fv, ["f1"])] + + response = GetOnlineFeaturesResponse(results=[]) + ok = OnlineStore._try_precomputed_fast_path( + blobs=[vector.SerializeToString()], + expected_feature_names=[f"{fv_name}__f1"], + online_features_response=response, + full_feature_names=True, + num_rows=1, + grouped_refs=grouped_refs, + registry=MagicMock(), + project="test", + ) + return ok, response + + def test_fresh_data_is_present(self): + now = datetime.now(tz=timezone.utc) + ok, response = self._fast_path_with_ttl(now, fv_ttl_seconds=3600) + assert ok is True + assert response.results[0].statuses[0] == FieldStatus.PRESENT + + def test_expired_data_is_outside_max_age(self): + old = datetime.now(tz=timezone.utc) - timedelta(hours=2) + ok, response = self._fast_path_with_ttl(old, fv_ttl_seconds=3600) + assert ok is True + assert response.results[0].statuses[0] == FieldStatus.OUTSIDE_MAX_AGE + + def test_zero_ttl_means_no_expiry(self): + old = datetime.now(tz=timezone.utc) - timedelta(days=365) + fv = _make_fv( + "fv1", + [Field(name="f1", dtype=Float32)], + ttl=timedelta(0), + ) + + event_ts = _make_timestamp(old) + fv_ts = FeatureViewTimestamp(feature_view_name="fv1", event_timestamp=event_ts) + vector = _make_vector( + feature_names=["fv1__f1"], + values=[ValueProto(float_val=1.0)], + fv_timestamps=[fv_ts], + ) + + from feast.infra.online_stores.online_store import OnlineStore + + response = GetOnlineFeaturesResponse(results=[]) + ok = OnlineStore._try_precomputed_fast_path( + blobs=[vector.SerializeToString()], + expected_feature_names=["fv1__f1"], + online_features_response=response, + full_feature_names=True, + num_rows=1, + grouped_refs=[(fv, ["f1"])], + registry=MagicMock(), + project="test", + ) + assert ok is True + assert response.results[0].statuses[0] == FieldStatus.PRESENT + + def test_mixed_ttl_some_expired_some_fresh(self): + from feast.infra.online_stores.online_store import OnlineStore + + now = datetime.now(tz=timezone.utc) + fresh_ts = _make_timestamp(now) + stale_ts = _make_timestamp(now - timedelta(hours=5)) + + vector = _make_vector( + feature_names=["fv1__f1", "fv2__f2"], + values=[ValueProto(float_val=1.0), ValueProto(float_val=2.0)], + fv_timestamps=[ + FeatureViewTimestamp(feature_view_name="fv1", event_timestamp=fresh_ts), + FeatureViewTimestamp(feature_view_name="fv2", event_timestamp=stale_ts), + ], + ) + + fv1 = _make_fv("fv1", [Field(name="f1", dtype=Float32)], ttl=timedelta(hours=1)) + fv2 = _make_fv("fv2", [Field(name="f2", dtype=Float32)], ttl=timedelta(hours=1)) + + response = GetOnlineFeaturesResponse(results=[]) + ok = OnlineStore._try_precomputed_fast_path( + blobs=[vector.SerializeToString()], + expected_feature_names=["fv1__f1", "fv2__f2"], + online_features_response=response, + full_feature_names=True, + num_rows=1, + grouped_refs=[(fv1, ["f1"]), (fv2, ["f2"])], + registry=MagicMock(), + project="test", + ) + assert ok is True + assert response.results[0].statuses[0] == FieldStatus.PRESENT + assert response.results[1].statuses[0] == FieldStatus.OUTSIDE_MAX_AGE + + +# ═══════════════════════════════════════════════════════════════════════════════ +# Expected feature names computation +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestComputeExpectedFeatureNames: + def _compute(self, grouped_refs, full_feature_names): + from feast.infra.online_stores.online_store import OnlineStore + + return OnlineStore._compute_expected_feature_names( + grouped_refs, full_feature_names + ) + + def test_full_feature_names(self): + fv = _make_fv("driver_fv", [Field(name="trips", dtype=Int64)]) + assert self._compute([(fv, ["trips"])], True) == ["driver_fv__trips"] + + def test_short_feature_names(self): + fv = _make_fv("driver_fv", [Field(name="trips", dtype=Int64)]) + assert self._compute([(fv, ["trips"])], False) == ["trips"] + + def test_skips_ts_keys(self): + fv = _make_fv("driver_fv", [Field(name="trips", dtype=Int64)]) + result = self._compute([(fv, ["trips", "_ts:driver_fv"])], True) + assert result == ["driver_fv__trips"] + + def test_multiple_fvs(self): + fv1 = _make_fv("fv1", [Field(name="a", dtype=Float32)]) + fv2 = _make_fv("fv2", [Field(name="b", dtype=Float32)]) + result = self._compute([(fv1, ["a"]), (fv2, ["b"])], True) + assert result == ["fv1__a", "fv2__b"] + + def test_multiple_features_per_fv(self): + fv = _make_fv( + "fv1", + [ + Field(name="a", dtype=Float32), + Field(name="b", dtype=Float32), + Field(name="c", dtype=Float32), + ], + ) + result = self._compute([(fv, ["a", "b", "c"])], True) + assert result == ["fv1__a", "fv1__b", "fv1__c"] + + def test_empty_grouped_refs(self): + assert self._compute([], True) == [] + + def test_only_ts_keys(self): + fv = _make_fv("fv1", [Field(name="a", dtype=Float32)]) + result = self._compute([(fv, ["_ts:fv1"])], True) + assert result == [] + + +# ═══════════════════════════════════════════════════════════════════════════════ +# OnlineStore base class default implementations +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestOnlineStoreBaseDefaults: + def _make_dummy_store(self, online_read_fn=None, online_write_batch_fn=None): + from feast.infra.online_stores.online_store import OnlineStore + + class DummyStore(OnlineStore): + def __init__(self): + self.written = [] + + def online_write_batch(self, config, table, data, progress): + self.written.append((table.name, data)) + if online_write_batch_fn: + online_write_batch_fn(config, table, data, progress) + + def online_read(self, config, table, entity_keys, requested_features=None): + if online_read_fn: + return online_read_fn( + config, table, entity_keys, requested_features + ) + raise RuntimeError("not implemented") + + def update(self, *args, **kwargs): + pass + + def teardown(self, *args, **kwargs): + pass + + return DummyStore() + + def test_read_precomputed_returns_none_on_exception(self): + store = self._make_dummy_store() + config = MagicMock() + entity_keys = [MagicMock(), MagicMock()] + + result = store.read_precomputed_vectors( + config, "my_service", "project", entity_keys + ) + assert result == [None, None] + + def test_read_precomputed_returns_none_for_missing_entities(self): + def online_read_fn(config, table, entity_keys, requested_features): + return [(None, None) for _ in entity_keys] + + store = self._make_dummy_store(online_read_fn=online_read_fn) + result = store.read_precomputed_vectors( + MagicMock(), "svc", "proj", [MagicMock()] + ) + assert result == [None] + + def test_read_precomputed_returns_bytes_for_present_entities(self): + test_bytes = b"test_vector_data" + + def online_read_fn(config, table, entity_keys, requested_features): + return [ + ( + datetime.now(tz=timezone.utc), + {"vector": ValueProto(bytes_val=test_bytes)}, + ) + for _ in entity_keys + ] + + store = self._make_dummy_store(online_read_fn=online_read_fn) + result = store.read_precomputed_vectors( + MagicMock(), "svc", "proj", [MagicMock()] + ) + assert result == [test_bytes] + + def test_write_precomputed_delegates_to_online_write_batch(self): + store = self._make_dummy_store() + entity_key = MagicMock() + vector_bytes = b"blob_data" + + store.write_precomputed_vector( + MagicMock(), "my_svc", "proj", entity_key, vector_bytes + ) + assert len(store.written) == 1 + table_name, data = store.written[0] + assert table_name == "__precomputed__my_svc" + assert len(data) == 1 + assert data[0][1]["vector"].bytes_val == vector_bytes + + def test_write_then_read_roundtrip(self): + stored = {} + + def online_write_fn(config, table, data, progress): + for ek, vals, ts, _ in data: + stored[id(ek)] = (ts, vals) + + def online_read_fn(config, table, entity_keys, requested_features): + results = [] + for ek in entity_keys: + if id(ek) in stored: + ts, vals = stored[id(ek)] + results.append((ts, vals)) + else: + results.append((None, None)) + return results + + store = self._make_dummy_store( + online_read_fn=online_read_fn, + online_write_batch_fn=online_write_fn, + ) + + entity_key = MagicMock() + vector_bytes = b"test_roundtrip" + + store.write_precomputed_vector( + MagicMock(), "svc", "proj", entity_key, vector_bytes + ) + result = store.read_precomputed_vectors( + MagicMock(), "svc", "proj", [entity_key] + ) + assert result == [vector_bytes] + + def test_read_returns_none_for_non_bytes_val(self): + def online_read_fn(config, table, entity_keys, requested_features): + return [ + ( + datetime.now(tz=timezone.utc), + {"vector": ValueProto(string_val="not bytes")}, + ) + for _ in entity_keys + ] + + store = self._make_dummy_store(online_read_fn=online_read_fn) + result = store.read_precomputed_vectors( + MagicMock(), "svc", "proj", [MagicMock()] + ) + assert result == [None] + + def test_read_returns_none_when_no_vector_key(self): + def online_read_fn(config, table, entity_keys, requested_features): + return [ + ( + datetime.now(tz=timezone.utc), + {"other_field": ValueProto(float_val=1.0)}, + ) + for _ in entity_keys + ] + + store = self._make_dummy_store(online_read_fn=online_read_fn) + result = store.read_precomputed_vectors( + MagicMock(), "svc", "proj", [MagicMock()] + ) + assert result == [None] + + +# ═══════════════════════════════════════════════════════════════════════════════ +# Materialization hook: _precompute_affected_services +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestPrecomputeAffectedServices: + def _make_mock_store(self, services): + store = MagicMock() + store.registry.list_feature_services.return_value = services + store.project = "test_project" + store._precompute_affected_services = lambda fv_names: type( + store + )._precompute_affected_services(store, fv_names) + + from feast.feature_store import FeatureStore + + store._precompute_affected_services = ( + FeatureStore._precompute_affected_services.__get__(store) + ) + return store + + def test_triggers_precompute_for_matching_service(self): + proj = MagicMock() + proj.name = "fv1" + svc = MagicMock() + svc.precompute_online = True + svc.feature_view_projections = [proj] + svc.name = "my_service" + + store = self._make_mock_store([svc]) + store._precompute_affected_services(["fv1"]) + store.precompute_feature_service.assert_called_once_with("my_service") + + def test_skips_non_precompute_services(self): + svc = MagicMock() + svc.precompute_online = False + svc.name = "svc" + + store = self._make_mock_store([svc]) + store._precompute_affected_services(["fv1"]) + store.precompute_feature_service.assert_not_called() + + def test_skips_when_no_fvs_match(self): + proj = MagicMock() + proj.name = "fv_other" + svc = MagicMock() + svc.precompute_online = True + svc.feature_view_projections = [proj] + svc.name = "svc" + + store = self._make_mock_store([svc]) + store._precompute_affected_services(["fv1"]) + store.precompute_feature_service.assert_not_called() + + def test_handles_registry_exception_gracefully(self): + store = MagicMock() + store.registry.list_feature_services.side_effect = RuntimeError("oops") + store.project = "test" + + from feast.feature_store import FeatureStore + + FeatureStore._precompute_affected_services(store, ["fv1"]) + + def test_handles_precompute_exception_gracefully(self): + proj = MagicMock() + proj.name = "fv1" + svc = MagicMock() + svc.precompute_online = True + svc.feature_view_projections = [proj] + svc.name = "svc" + + store = self._make_mock_store([svc]) + store.precompute_feature_service.side_effect = RuntimeError("fail") + + store._precompute_affected_services(["fv1"]) + + def test_multiple_services_all_triggered(self): + proj1 = MagicMock() + proj1.name = "fv1" + svc1 = MagicMock() + svc1.precompute_online = True + svc1.feature_view_projections = [proj1] + svc1.name = "svc1" + + proj2 = MagicMock() + proj2.name = "fv1" + svc2 = MagicMock() + svc2.precompute_online = True + svc2.feature_view_projections = [proj2] + svc2.name = "svc2" + + store = self._make_mock_store([svc1, svc2]) + store._precompute_affected_services(["fv1"]) + + assert store.precompute_feature_service.call_count == 2 + + +# ═══════════════════════════════════════════════════════════════════════════════ +# Push hook: _precompute_for_push +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestPrecomputeForPush: + def _make_mock_store(self, services): + store = MagicMock() + store.registry.list_feature_services.return_value = services + store.project = "test_project" + + from feast.feature_store import FeatureStore + + store._precompute_for_push = FeatureStore._precompute_for_push.__get__(store) + return store + + def test_triggers_for_matching_fv(self): + proj = MagicMock() + proj.name = "pushed_fv" + svc = MagicMock() + svc.precompute_online = True + svc.feature_view_projections = [proj] + svc.name = "svc" + + store = self._make_mock_store([svc]) + store._precompute_for_push("pushed_fv", MagicMock()) + store.precompute_feature_service.assert_called_once_with("svc") + + def test_skips_non_matching_fv(self): + proj = MagicMock() + proj.name = "other_fv" + svc = MagicMock() + svc.precompute_online = True + svc.feature_view_projections = [proj] + svc.name = "svc" + + store = self._make_mock_store([svc]) + store._precompute_for_push("pushed_fv", MagicMock()) + store.precompute_feature_service.assert_not_called() + + def test_skips_non_precompute_service(self): + proj = MagicMock() + proj.name = "pushed_fv" + svc = MagicMock() + svc.precompute_online = False + svc.feature_view_projections = [proj] + svc.name = "svc" + + store = self._make_mock_store([svc]) + store._precompute_for_push("pushed_fv", MagicMock()) + store.precompute_feature_service.assert_not_called() + + def test_handles_exception_gracefully(self): + proj = MagicMock() + proj.name = "fv" + svc = MagicMock() + svc.precompute_online = True + svc.feature_view_projections = [proj] + svc.name = "svc" + + store = self._make_mock_store([svc]) + store.precompute_feature_service.side_effect = RuntimeError("fail") + + store._precompute_for_push("fv", MagicMock()) + + def test_handles_registry_exception_gracefully(self): + store = MagicMock() + store.registry.list_feature_services.side_effect = RuntimeError("oops") + store.project = "test" + + from feast.feature_store import FeatureStore + + FeatureStore._precompute_for_push(store, "fv", MagicMock()) diff --git a/sdk/python/tests/unit/test_proto_json.py b/sdk/python/tests/unit/test_proto_json.py index b5e01744e44..6b3c7181ad7 100644 --- a/sdk/python/tests/unit/test_proto_json.py +++ b/sdk/python/tests/unit/test_proto_json.py @@ -103,6 +103,33 @@ def test_feature_list(proto_json_patch): ) +def test_nested_collection_json_roundtrip(proto_json_patch): + """Nested collection values (list of lists) should survive JSON roundtrip.""" + from feast.protos.feast.types.Value_pb2 import Value + + # Build a Value with list_val containing [[1,2],[3,4,5]] + value_proto = Value() + inner1 = value_proto.list_val.val.add() + inner1.int64_list_val.val.extend([1, 2]) + inner2 = value_proto.list_val.val.add() + inner2.int64_list_val.val.extend([3, 4, 5]) + + # Serialize to JSON + value_json = MessageToDict(value_proto) + assert isinstance(value_json, list) + assert len(value_json) == 2 + assert value_json[0] == [1, 2] + assert value_json[1] == [3, 4, 5] + + # Deserialize back from JSON + feature_vector_str = '{"values": [[[1, 2], [3, 4, 5]]]}' + feature_vector_proto = FeatureVector() + Parse(feature_vector_str, feature_vector_proto) + assert len(feature_vector_proto.values) == 1 + assert feature_vector_proto.values[0].WhichOneof("val") == "list_val" + assert len(feature_vector_proto.values[0].list_val.val) == 2 + + @pytest.fixture(scope="module") def proto_json_patch(): proto_json.patch() diff --git a/sdk/python/tests/unit/test_registry_server_commit_refresh.py b/sdk/python/tests/unit/test_registry_server_commit_refresh.py new file mode 100644 index 00000000000..3c591a2b9de --- /dev/null +++ b/sdk/python/tests/unit/test_registry_server_commit_refresh.py @@ -0,0 +1,276 @@ +from unittest.mock import Mock + +import pytest +from google.protobuf.empty_pb2 import Empty + +from feast.errors import FeastPermissionError, ProjectObjectNotFoundException +from feast.infra.infra_object import Infra +from feast.permissions.permission import AuthzedAction, Permission +from feast.permissions.policy import RoleBasedPolicy +from feast.permissions.security_manager import ( + SecurityManager, + no_security_manager, + set_security_manager, +) +from feast.permissions.user import User +from feast.project import Project +from feast.protos.feast.registry import RegistryServer_pb2 +from feast.registry_server import RegistryServer + + +@pytest.fixture +def project_permissions(): + return [ + Permission( + name="project_updater", + types=Project, + name_patterns="team-a.*", + policy=RoleBasedPolicy(roles=["team-a"]), + actions=[AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], + ), + Permission( + name="project_creator", + types=Project, + name_patterns="team-a.*", + policy=RoleBasedPolicy(roles=["team-a"]), + actions=[AuthzedAction.CREATE], + ), + Permission( + name="project_a_create_only", + types=Project, + name_patterns="team-a.*", + policy=RoleBasedPolicy(roles=["team-a-creator"]), + actions=[AuthzedAction.CREATE], + ), + Permission( + name="project_b_updater", + types=Project, + name_patterns="team-b.*", + policy=RoleBasedPolicy(roles=["team-b"]), + actions=[AuthzedAction.DESCRIBE, AuthzedAction.UPDATE], + ), + ] + + +@pytest.fixture +def auth_security_manager(project_permissions): + registry = Mock() + registry.list_permissions = Mock(return_value=project_permissions) + sm = SecurityManager(project="any", registry=registry) + set_security_manager(sm) + yield sm + no_security_manager() + + +def _server_with_projects(*project_names: str) -> tuple[RegistryServer, Mock]: + registry = Mock() + projects = [Project(name=name) for name in project_names] + registry.list_projects = Mock(return_value=projects) + + def get_project(name: str, allow_cache: bool = False): + for project in projects: + if project.name == name: + return project + raise ProjectObjectNotFoundException(name=name) + + registry.get_project = Mock(side_effect=get_project) + registry.refresh = Mock() + registry.commit = Mock() + registry.get_infra = Mock(return_value=Infra()) + registry.update_infra = Mock() + return RegistryServer(registry=registry), registry + + +def test_refresh_allows_missing_project_without_auth(): + no_security_manager() + server, registry = _server_with_projects("existing") + + response = server.Refresh( + RegistryServer_pb2.RefreshRequest(project="brand-new"), None + ) + + assert response == Empty() + registry.refresh.assert_called_once_with("brand-new") + + +def test_refresh_allows_missing_project_with_create_permission(auth_security_manager): + auth_security_manager.set_current_user(User("alice", ["team-a"])) + server, registry = _server_with_projects("team-b-project") + + response = server.Refresh( + RegistryServer_pb2.RefreshRequest(project="team-a-new"), None + ) + + assert response == Empty() + registry.refresh.assert_called_once_with("team-a-new") + + +def test_refresh_denies_missing_project_without_create_permission( + auth_security_manager, +): + auth_security_manager.set_current_user(User("bob", ["team-b"])) + server, _ = _server_with_projects("team-b-project") + + with pytest.raises(FeastPermissionError): + server.Refresh(RegistryServer_pb2.RefreshRequest(project="team-a-new"), None) + + +def test_refresh_requires_update_for_existing_project(auth_security_manager): + auth_security_manager.set_current_user(User("alice", ["team-a"])) + server, registry = _server_with_projects("team-a-project", "team-b-project") + + response = server.Refresh( + RegistryServer_pb2.RefreshRequest(project="team-a-project"), None + ) + assert response == Empty() + registry.refresh.assert_called_once_with("team-a-project") + + with pytest.raises(FeastPermissionError): + server.Refresh( + RegistryServer_pb2.RefreshRequest(project="team-b-project"), None + ) + + +def test_commit_allows_when_caller_can_update_one_shared_project(auth_security_manager): + auth_security_manager.set_current_user(User("alice", ["team-a"])) + server, registry = _server_with_projects("team-a-project", "team-b-project") + + response = server.Commit(Empty(), None) + + assert response == Empty() + registry.commit.assert_called_once() + + +def test_commit_denies_when_caller_cannot_create_or_update_any_project( + auth_security_manager, +): + auth_security_manager.set_current_user(User("nobody", ["other"])) + server, _ = _server_with_projects("team-a-project", "team-b-project") + + with pytest.raises(FeastPermissionError): + server.Commit(Empty(), None) + + +def test_commit_allows_without_auth(): + no_security_manager() + server, registry = _server_with_projects("a", "b") + + response = server.Commit(Empty(), None) + + assert response == Empty() + registry.commit.assert_called_once() + + +def test_commit_allows_empty_registry_with_auth(auth_security_manager): + # No projects yet — same as old behavior (empty permission loop). + auth_security_manager.set_current_user(User("nobody", ["other"])) + server, registry = _server_with_projects() + + response = server.Commit(Empty(), None) + + assert response == Empty() + registry.commit.assert_called_once() + + +def test_refresh_existing_project_without_auth(): + no_security_manager() + server, registry = _server_with_projects("existing") + + response = server.Refresh( + RegistryServer_pb2.RefreshRequest(project="existing"), None + ) + + assert response == Empty() + registry.refresh.assert_called_once_with("existing") + + +def test_commit_allows_with_only_create_permission_on_new_project( + auth_security_manager, +): + # After ApplyProject(commit=False), project exists in cache but caller may + # only have CREATE (not UPDATE) for that first apply transaction. + auth_security_manager.set_current_user(User("creator", ["team-a-creator"])) + server, registry = _server_with_projects("team-a-new") + + response = server.Commit(Empty(), None) + + assert response == Empty() + registry.commit.assert_called_once() + + +def test_get_infra_allows_missing_project_without_auth(): + no_security_manager() + server, registry = _server_with_projects("existing") + + response = server.GetInfra( + RegistryServer_pb2.GetInfraRequest(project="brand-new", allow_cache=True), + None, + ) + + assert response == Infra().to_proto() + registry.get_infra.assert_called_once_with(project="brand-new", allow_cache=True) + + +def test_get_infra_allows_missing_project_with_create_permission(auth_security_manager): + auth_security_manager.set_current_user(User("creator", ["team-a-creator"])) + server, registry = _server_with_projects("team-b-project") + + response = server.GetInfra( + RegistryServer_pb2.GetInfraRequest(project="team-a-new", allow_cache=True), + None, + ) + + assert response == Infra().to_proto() + registry.get_infra.assert_called_once_with(project="team-a-new", allow_cache=True) + + +def test_get_infra_denies_missing_project_without_create_permission( + auth_security_manager, +): + auth_security_manager.set_current_user(User("bob", ["team-b"])) + server, _ = _server_with_projects("team-b-project") + + with pytest.raises(FeastPermissionError): + server.GetInfra( + RegistryServer_pb2.GetInfraRequest(project="team-a-new", allow_cache=True), + None, + ) + + +def test_get_infra_requires_describe_for_existing_project(auth_security_manager): + auth_security_manager.set_current_user(User("alice", ["team-a"])) + server, registry = _server_with_projects("team-a-project", "team-b-project") + + response = server.GetInfra( + RegistryServer_pb2.GetInfraRequest(project="team-a-project", allow_cache=True), + None, + ) + assert response == Infra().to_proto() + registry.get_infra.assert_called_once_with( + project="team-a-project", allow_cache=True + ) + + with pytest.raises(FeastPermissionError): + server.GetInfra( + RegistryServer_pb2.GetInfraRequest( + project="team-b-project", allow_cache=True + ), + None, + ) + + +def test_update_infra_allows_missing_project_with_create_permission( + auth_security_manager, +): + auth_security_manager.set_current_user(User("creator", ["team-a-creator"])) + server, registry = _server_with_projects("team-b-project") + + response = server.UpdateInfra( + RegistryServer_pb2.UpdateInfraRequest( + infra=Infra().to_proto(), project="team-a-new", commit=True + ), + None, + ) + + assert response == Empty() + registry.update_infra.assert_called_once() diff --git a/sdk/python/tests/unit/test_registry_string_config.py b/sdk/python/tests/unit/test_registry_string_config.py new file mode 100644 index 00000000000..ca337454e52 --- /dev/null +++ b/sdk/python/tests/unit/test_registry_string_config.py @@ -0,0 +1,137 @@ +"""Tests for string-based registry configuration in RepoConfig. + +Verifies that passing registry as a string (e.g. "gs://bucket/registry.pb") +correctly allows auto-detection of the registry store class from the URI +scheme, rather than hardcoding FileRegistryStore. + +Regression test for: RepoConfig.registry property hardcodes +get_registry_config_from_type("file") when registry_config is a string, +ignoring URI scheme and breaking gs:// and s3:// paths. +""" + +from pathlib import Path + +import pytest + +from feast.infra.registry.registry import ( + REGISTRY_STORE_CLASS_FOR_SCHEME, + get_registry_store_class_from_scheme, +) +from feast.repo_config import RegistryConfig, RepoConfig + + +def _make_repo_config(registry): + """Helper to create a minimal RepoConfig with the given registry value.""" + return RepoConfig( + project="test", + provider="gcp", + registry=registry, + online_store={"type": "redis", "connection_string": "localhost:6379"}, + entity_key_serialization_version=3, + ) + + +class TestStringRegistryAutoDetection: + """When registry is passed as a string, RepoConfig must produce a + RegistryConfig that allows Registry.__init__ to auto-detect the correct + store class from the URI scheme.""" + + def test_gcs_string_registry_produces_correct_config(self): + """gs:// string -> RegistryConfig with registry_store_type=None + so Registry.__init__ auto-detects GCSRegistryStore.""" + config = _make_repo_config("gs://my-bucket/feast/registry.pb") + reg = config.registry + + assert isinstance(reg, RegistryConfig) + assert reg.path == "gs://my-bucket/feast/registry.pb" + assert reg.registry_store_type is None + + def test_s3_string_registry_produces_correct_config(self): + """s3:// string -> RegistryConfig with registry_store_type=None + so Registry.__init__ auto-detects S3RegistryStore.""" + config = _make_repo_config("s3://my-bucket/feast/registry.pb") + reg = config.registry + + assert isinstance(reg, RegistryConfig) + assert reg.path == "s3://my-bucket/feast/registry.pb" + assert reg.registry_store_type is None + + def test_local_string_registry_still_works(self): + """A local file path string must still produce a valid RegistryConfig + (no regression for the common case).""" + config = _make_repo_config("/tmp/feast/registry.db") + reg = config.registry + + assert isinstance(reg, RegistryConfig) + assert reg.path == "/tmp/feast/registry.db" + # registry_type defaults to "file", which is correct for local paths + assert reg.registry_type == "file" + + def test_dict_registry_still_works(self): + """Dict-based registry config must continue to work as before.""" + config = _make_repo_config({"path": "gs://my-bucket/feast/registry.pb"}) + reg = config.registry + + assert isinstance(reg, RegistryConfig) + assert reg.path == "gs://my-bucket/feast/registry.pb" + assert reg.registry_store_type is None + + def test_dict_registry_with_explicit_registry_type(self): + """Dict with explicit registry_type must route through + get_registry_config_from_type (no change in behavior).""" + config = _make_repo_config( + {"registry_type": "file", "path": "/tmp/registry.db"} + ) + reg = config.registry + + assert isinstance(reg, RegistryConfig) + assert reg.path == "/tmp/registry.db" + assert reg.registry_type == "file" + + +class TestRegistryStoreSchemeDetection: + """Verify that REGISTRY_STORE_CLASS_FOR_SCHEME and + get_registry_store_class_from_scheme correctly map URI schemes + to their store classes.""" + + def test_gcs_scheme_selects_gcs_registry_store(self): + assert "gs" in REGISTRY_STORE_CLASS_FOR_SCHEME + cls = get_registry_store_class_from_scheme("gs://bucket/registry.pb") + assert cls.__name__ == "GCSRegistryStore" + + def test_s3_scheme_selects_s3_registry_store(self): + assert "s3" in REGISTRY_STORE_CLASS_FOR_SCHEME + cls = get_registry_store_class_from_scheme("s3://bucket/registry.pb") + assert cls.__name__ == "S3RegistryStore" + + def test_file_scheme_selects_file_registry_store(self): + assert "file" in REGISTRY_STORE_CLASS_FOR_SCHEME + cls = get_registry_store_class_from_scheme("file:///tmp/registry.db") + assert cls.__name__ == "FileRegistryStore" + + def test_unknown_scheme_raises(self): + with pytest.raises(Exception, match="unsupported scheme"): + get_registry_store_class_from_scheme("ftp://host/registry.pb") + + +class TestFileRegistryStorePathHandling: + """Demonstrate that FileRegistryStore cannot handle cloud URIs — + this is the root cause of the IsADirectoryError in production when + the bug is present.""" + + def test_pathlib_does_not_treat_gcs_as_absolute(self): + """pathlib.Path('gs://...') is NOT absolute, so FileRegistryStore + joins it with repo_path producing nonsense like /app/gs://...""" + gcs_path = Path("gs://my-bucket/feast/registry.pb") + assert not gcs_path.is_absolute() + + joined = Path("/app").joinpath(gcs_path) + assert str(joined).startswith("/app/gs:") + + def test_pathlib_does_not_treat_s3_as_absolute(self): + """Same issue for s3:// paths.""" + s3_path = Path("s3://my-bucket/feast/registry.pb") + assert not s3_path.is_absolute() + + joined = Path("/app").joinpath(s3_path) + assert str(joined).startswith("/app/s3:") diff --git a/sdk/python/tests/unit/test_rest_error_decorator.py b/sdk/python/tests/unit/test_rest_error_decorator.py index 147ae767bdb..cdde3d208e6 100644 --- a/sdk/python/tests/unit/test_rest_error_decorator.py +++ b/sdk/python/tests/unit/test_rest_error_decorator.py @@ -10,6 +10,7 @@ RemoteOnlineStoreConfig, get_remote_online_features, ) +from feast.permissions.client.http_auth_requests_wrapper import HttpSessionManager @pytest.fixture @@ -26,6 +27,9 @@ def none_feast_exception() -> RuntimeError: def test_rest_error_handling_with_feast_exception( mock_post, environment, feast_exception ): + # Close any cached session to ensure mock is applied to fresh session + HttpSessionManager.close_session() + # Create a mock response object mock_response = Mock() mock_response.status_code = feast_exception.http_status_code() @@ -54,6 +58,9 @@ def test_rest_error_handling_with_feast_exception( def test_rest_error_handling_with_none_feast_exception( mock_post, environment, none_feast_exception ): + # Close any cached session to ensure mock is applied to fresh session + HttpSessionManager.close_session() + # Create a mock response object mock_response = Mock() mock_response.status_code = 500 diff --git a/sdk/python/tests/unit/test_skip_validation.py b/sdk/python/tests/unit/test_skip_validation.py new file mode 100644 index 00000000000..8fb916e3776 --- /dev/null +++ b/sdk/python/tests/unit/test_skip_validation.py @@ -0,0 +1,71 @@ +""" +Tests for skip_feature_view_validation parameter in FeatureStore.apply() and FeatureStore.plan() + +This feature allows users to skip Feature View validation when the validation system +is being overly strict. This is particularly important for: +- Feature transformations that go through validation (e.g., _construct_random_input in ODFVs) +- Cases where the type/validation system is being too restrictive + +Users should be encouraged to report issues on GitHub when they need to use this flag. +""" + +import inspect + +from feast.feature_store import FeatureStore + + +def test_apply_has_skip_feature_view_validation_parameter(): + """Test that FeatureStore.apply() method has skip_feature_view_validation parameter""" + # Get the signature of the apply method + sig = inspect.signature(FeatureStore.apply) + + # Check that skip_feature_view_validation parameter exists + assert "skip_feature_view_validation" in sig.parameters + + # Check that it has a default value of False + param = sig.parameters["skip_feature_view_validation"] + assert param.default is False + + # Check that it's a boolean type hint (if type hints are present) + if param.annotation != inspect.Parameter.empty: + assert param.annotation == bool + + +def test_plan_has_skip_feature_view_validation_parameter(): + """Test that FeatureStore.plan() method has skip_feature_view_validation parameter""" + # Get the signature of the plan method + sig = inspect.signature(FeatureStore.plan) + + # Check that skip_feature_view_validation parameter exists + assert "skip_feature_view_validation" in sig.parameters + + # Check that it has a default value of False + param = sig.parameters["skip_feature_view_validation"] + assert param.default is False + + # Check that it's a boolean type hint (if type hints are present) + if param.annotation != inspect.Parameter.empty: + assert param.annotation == bool + + +def test_skip_feature_view_validation_use_case_documentation(): + """ + Documentation test: This test documents the key use case for skip_feature_view_validation. + + The skip_feature_view_validation flag is particularly important for On-Demand Feature Views (ODFVs) + that use feature transformations. During the apply() process, ODFVs call infer_features() + which internally uses _construct_random_input() to validate the transformation. + + Sometimes this validation can be overly strict or fail for complex transformations. + In such cases, users can use skip_feature_view_validation=True to bypass this check. + + Example use case from the issue: + - User has an ODFV with a complex transformation + - The _construct_random_input validation fails or is too restrictive + - User can now call: fs.apply([odfv], skip_feature_view_validation=True) + - The ODFV is registered without going through the validation + + Note: Users should be encouraged to report such cases on GitHub so the Feast team + can improve the validation system. + """ + pass # This is a documentation test diff --git a/sdk/python/tests/unit/test_stream_feature_view.py b/sdk/python/tests/unit/test_stream_feature_view.py index 96e62d9d9e2..e0670b084bd 100644 --- a/sdk/python/tests/unit/test_stream_feature_view.py +++ b/sdk/python/tests/unit/test_stream_feature_view.py @@ -303,3 +303,36 @@ def test_update_materialization_intervals(): updated_stream_feature_view.materialization_intervals[0][1] == stored_stream_feature_view.materialization_intervals[0][1] ) + + +def test_stream_feature_view_org_field(): + """Test that the optional `org` field is stored, serialized, and round-trips correctly.""" + stream_source = KafkaSource( + name="kafka", + timestamp_field="event_timestamp", + kafka_bootstrap_servers="", + message_format=AvroFormat(""), + topic="topic", + batch_source=FileSource(path="some path"), + ) + common = dict( + entities=[], + ttl=timedelta(days=30), + schema=[Field(name="dummy_field", dtype=Float32)], + source=stream_source, + ) + + sfv_no_org = StreamFeatureView(name="sfv-no-org", **common) + assert sfv_no_org.org == "" + + sfv_with_org = StreamFeatureView(name="sfv-with-org", org="ads", **common) + assert sfv_with_org.org == "ads" + + proto = sfv_with_org.to_proto() + assert proto.spec.org == "ads" + + roundtripped = StreamFeatureView.from_proto(proto) + assert roundtripped.org == "ads" + + sfv_other_org = StreamFeatureView(name="sfv-with-org", org="search", **common) + assert sfv_with_org != sfv_other_org diff --git a/sdk/python/tests/unit/test_substrait_transformation.py b/sdk/python/tests/unit/test_substrait_transformation.py deleted file mode 100644 index 1e5cd0889ae..00000000000 --- a/sdk/python/tests/unit/test_substrait_transformation.py +++ /dev/null @@ -1,132 +0,0 @@ -import os -import tempfile -from datetime import datetime, timedelta - -import pandas as pd - -from feast import Entity, FeatureStore, FeatureView, FileSource, RepoConfig -from feast.driver_test_data import create_driver_hourly_stats_df -from feast.field import Field -from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig -from feast.on_demand_feature_view import on_demand_feature_view -from feast.types import Float32, Float64, Int64 - - -def test_ibis_pandas_parity(): - with tempfile.TemporaryDirectory() as data_dir: - store = FeatureStore( - config=RepoConfig( - project="test_on_demand_substrait_transformation", - registry=os.path.join(data_dir, "registry.db"), - provider="local", - entity_key_serialization_version=3, - online_store=SqliteOnlineStoreConfig( - path=os.path.join(data_dir, "online.db") - ), - ) - ) - - # Generate test data. - end_date = datetime.now().replace(microsecond=0, second=0, minute=0) - start_date = end_date - timedelta(days=15) - - driver_entities = [1001, 1002, 1003, 1004, 1005] - driver_df = create_driver_hourly_stats_df(driver_entities, start_date, end_date) - driver_stats_path = os.path.join(data_dir, "driver_stats.parquet") - driver_df.to_parquet(path=driver_stats_path, allow_truncated_timestamps=True) - - driver = Entity(name="driver", join_keys=["driver_id"]) - - driver_stats_source = FileSource( - name="driver_hourly_stats_source", - path=driver_stats_path, - timestamp_field="event_timestamp", - created_timestamp_column="created", - ) - - driver_stats_fv = FeatureView( - name="driver_hourly_stats", - entities=[driver], - ttl=timedelta(days=1), - schema=[ - Field(name="conv_rate", dtype=Float32), - Field(name="acc_rate", dtype=Float32), - Field(name="avg_daily_trips", dtype=Int64), - ], - online=True, - source=driver_stats_source, - ) - - @on_demand_feature_view( - sources=[driver_stats_fv], - schema=[Field(name="conv_rate_plus_acc", dtype=Float64)], - mode="pandas", - ) - def pandas_view(inputs: pd.DataFrame) -> pd.DataFrame: - df = pd.DataFrame() - df["conv_rate_plus_acc"] = inputs["conv_rate"] + inputs["acc_rate"] - return df - - from ibis.expr.types import Table - - @on_demand_feature_view( - sources=[driver_stats_fv[["conv_rate", "acc_rate"]]], - schema=[Field(name="conv_rate_plus_acc_substrait", dtype=Float64)], - mode="substrait", - ) - def substrait_view(inputs: Table) -> Table: - return inputs.mutate( - conv_rate_plus_acc_substrait=inputs["conv_rate"] + inputs["acc_rate"] - ) - - store.apply( - [driver, driver_stats_source, driver_stats_fv, substrait_view, pandas_view] - ) - - store.materialize( - start_date=start_date, - end_date=end_date, - ) - - entity_df = pd.DataFrame.from_dict( - { - # entity's join key -> entity values - "driver_id": [1001, 1002, 1003], - # "event_timestamp" (reserved key) -> timestamps - "event_timestamp": [ - start_date + timedelta(days=4), - start_date + timedelta(days=5), - start_date + timedelta(days=6), - ], - } - ) - - requested_features = [ - "driver_hourly_stats:conv_rate", - "driver_hourly_stats:acc_rate", - "driver_hourly_stats:avg_daily_trips", - "substrait_view:conv_rate_plus_acc_substrait", - "pandas_view:conv_rate_plus_acc", - ] - - training_df = store.get_historical_features( - entity_df=entity_df, features=requested_features - ) - - assert training_df.to_df()["conv_rate_plus_acc"].equals( - training_df.to_df()["conv_rate_plus_acc_substrait"] - ) - - assert training_df.to_arrow()["conv_rate_plus_acc"].equals( - training_df.to_arrow()["conv_rate_plus_acc_substrait"] - ) - - online_response = store.get_online_features( - features=requested_features, - entity_rows=[{"driver_id": 1001}, {"driver_id": 1002}, {"driver_id": 1003}], - ) - - assert ( - online_response.to_dict()["conv_rate_plus_acc"] - == online_response.to_dict()["conv_rate_plus_acc_substrait"] - ) diff --git a/sdk/python/tests/unit/test_table_format.py b/sdk/python/tests/unit/test_table_format.py new file mode 100644 index 00000000000..908d491e940 --- /dev/null +++ b/sdk/python/tests/unit/test_table_format.py @@ -0,0 +1,323 @@ +import json + +import pytest + +from feast.table_format import ( + DeltaFormat, + HudiFormat, + IcebergFormat, + TableFormatType, + create_table_format, + table_format_from_dict, + table_format_from_json, +) + + +class TestTableFormat: + """Test core TableFormat classes and functionality.""" + + def test_iceberg_table_format_creation(self): + """Test IcebergFormat creation and properties.""" + iceberg_format = IcebergFormat( + catalog="my_catalog", + namespace="my_namespace", + properties={"catalog.uri": "s3://bucket/warehouse", "format-version": "2"}, + ) + + assert iceberg_format.format_type == TableFormatType.ICEBERG + assert iceberg_format.catalog == "my_catalog" + assert iceberg_format.namespace == "my_namespace" + assert iceberg_format.get_property("iceberg.catalog") == "my_catalog" + assert iceberg_format.get_property("iceberg.namespace") == "my_namespace" + assert iceberg_format.get_property("catalog.uri") == "s3://bucket/warehouse" + assert iceberg_format.get_property("format-version") == "2" + + def test_iceberg_table_format_minimal(self): + """Test IcebergFormat with minimal config.""" + iceberg_format = IcebergFormat() + + assert iceberg_format.format_type == TableFormatType.ICEBERG + assert iceberg_format.catalog is None + assert iceberg_format.namespace is None + assert len(iceberg_format.properties) == 0 + + def test_delta_table_format_creation(self): + """Test DeltaFormat creation and properties.""" + delta_format = DeltaFormat( + checkpoint_location="s3://bucket/checkpoints", + properties={"delta.autoOptimize.optimizeWrite": "true"}, + ) + + assert delta_format.format_type == TableFormatType.DELTA + assert delta_format.checkpoint_location == "s3://bucket/checkpoints" + assert ( + delta_format.get_property("delta.checkpointLocation") + == "s3://bucket/checkpoints" + ) + assert delta_format.get_property("delta.autoOptimize.optimizeWrite") == "true" + + def test_hudi_table_format_creation(self): + """Test HudiFormat creation and properties.""" + hudi_format = HudiFormat( + table_type="COPY_ON_WRITE", + record_key="id", + precombine_field="timestamp", + properties={ + "hoodie.compaction.strategy": "org.apache.hudi.table.action.compact.strategy.LogFileSizeBasedCompactionStrategy" + }, + ) + + assert hudi_format.format_type == TableFormatType.HUDI + assert hudi_format.table_type == "COPY_ON_WRITE" + assert hudi_format.record_key == "id" + assert hudi_format.precombine_field == "timestamp" + assert ( + hudi_format.get_property("hoodie.datasource.write.table.type") + == "COPY_ON_WRITE" + ) + assert ( + hudi_format.get_property("hoodie.datasource.write.recordkey.field") == "id" + ) + assert ( + hudi_format.get_property("hoodie.datasource.write.precombine.field") + == "timestamp" + ) + + def test_table_format_property_methods(self): + """Test property getter/setter methods.""" + iceberg_format = IcebergFormat() + + # Test setting and getting properties + iceberg_format.set_property("snapshot-id", "123456789") + assert iceberg_format.get_property("snapshot-id") == "123456789" + + # Test default value + assert iceberg_format.get_property("non-existent-key", "default") == "default" + assert iceberg_format.get_property("non-existent-key") is None + + def test_table_format_serialization(self): + """Test table format serialization to/from dict.""" + # Test Iceberg + iceberg_format = IcebergFormat( + catalog="test_catalog", + namespace="test_namespace", + properties={"key1": "value1", "key2": "value2"}, + ) + + iceberg_dict = iceberg_format.to_dict() + iceberg_restored = IcebergFormat.from_dict(iceberg_dict) + + assert iceberg_restored.format_type == iceberg_format.format_type + assert iceberg_restored.catalog == iceberg_format.catalog + assert iceberg_restored.namespace == iceberg_format.namespace + assert iceberg_restored.properties == iceberg_format.properties + + # Test Delta + delta_format = DeltaFormat( + checkpoint_location="s3://bucket/checkpoints", + properties={"key": "value"}, + ) + + delta_dict = delta_format.to_dict() + delta_restored = DeltaFormat.from_dict(delta_dict) + + assert delta_restored.format_type == delta_format.format_type + assert delta_restored.properties == delta_format.properties + assert delta_restored.checkpoint_location == delta_format.checkpoint_location + + # Test Hudi + hudi_format = HudiFormat( + table_type="MERGE_ON_READ", + record_key="uuid", + precombine_field="ts", + ) + + hudi_dict = hudi_format.to_dict() + hudi_restored = HudiFormat.from_dict(hudi_dict) + + assert hudi_restored.format_type == hudi_format.format_type + assert hudi_restored.table_type == hudi_format.table_type + assert hudi_restored.record_key == hudi_format.record_key + assert hudi_restored.precombine_field == hudi_format.precombine_field + + def test_factory_function(self): + """Test create_table_format factory function.""" + # Test Iceberg + iceberg_format = create_table_format( + TableFormatType.ICEBERG, + catalog="test_catalog", + namespace="test_ns", + ) + assert isinstance(iceberg_format, IcebergFormat) + assert iceberg_format.catalog == "test_catalog" + assert iceberg_format.namespace == "test_ns" + + # Test Delta + delta_format = create_table_format( + TableFormatType.DELTA, + checkpoint_location="s3://test", + ) + assert isinstance(delta_format, DeltaFormat) + assert delta_format.checkpoint_location == "s3://test" + + # Test Hudi + hudi_format = create_table_format( + TableFormatType.HUDI, + table_type="COPY_ON_WRITE", + ) + assert isinstance(hudi_format, HudiFormat) + assert hudi_format.table_type == "COPY_ON_WRITE" + + # Test invalid format type + with pytest.raises(ValueError, match="Unknown table format type"): + create_table_format("invalid_format") + + def test_table_format_from_dict(self): + """Test table_format_from_dict function.""" + # Test Iceberg + iceberg_dict = { + "format_type": "iceberg", + "catalog": "test_catalog", + "namespace": "test_namespace", + "properties": {"key1": "value1", "key2": "value2"}, + } + iceberg_format = table_format_from_dict(iceberg_dict) + assert isinstance(iceberg_format, IcebergFormat) + assert iceberg_format.catalog == "test_catalog" + + # Test Delta + delta_dict = { + "format_type": "delta", + "properties": {"key": "value"}, + "checkpoint_location": "s3://bucket/checkpoints", + } + delta_format = table_format_from_dict(delta_dict) + assert isinstance(delta_format, DeltaFormat) + assert delta_format.checkpoint_location == "s3://bucket/checkpoints" + + # Test Hudi + hudi_dict = { + "format_type": "hudi", + "table_type": "MERGE_ON_READ", + "record_key": "id", + "precombine_field": "ts", + "properties": {}, + } + hudi_format = table_format_from_dict(hudi_dict) + assert isinstance(hudi_format, HudiFormat) + assert hudi_format.table_type == "MERGE_ON_READ" + + # Test invalid format type + with pytest.raises(ValueError, match="Unknown table format type"): + table_format_from_dict({"format_type": "invalid"}) + + def test_table_format_from_json(self): + """Test table_format_from_json function.""" + iceberg_dict = { + "format_type": "iceberg", + "catalog": "test_catalog", + "namespace": "test_namespace", + "properties": {}, + } + json_str = json.dumps(iceberg_dict) + iceberg_format = table_format_from_json(json_str) + + assert isinstance(iceberg_format, IcebergFormat) + assert iceberg_format.catalog == "test_catalog" + assert iceberg_format.namespace == "test_namespace" + + def test_table_format_error_handling(self): + """Test error handling in table format operations.""" + + # Test invalid format type - create mock enum value + class MockFormat: + value = "invalid_format" + + with pytest.raises(ValueError, match="Unknown table format type"): + create_table_format(MockFormat()) + + # Test invalid format type in from_dict + with pytest.raises(ValueError, match="Unknown table format type"): + table_format_from_dict({"format_type": "invalid"}) + + # Test missing format_type + with pytest.raises(KeyError): + table_format_from_dict({}) + + # Test invalid JSON + with pytest.raises(json.JSONDecodeError): + table_format_from_json("invalid json") + + def test_table_format_property_edge_cases(self): + """Test edge cases for table format properties.""" + iceberg_format = IcebergFormat() + + # Test property overwriting + iceberg_format.set_property("snapshot-id", "123") + assert iceberg_format.get_property("snapshot-id") == "123" + iceberg_format.set_property("snapshot-id", "456") + assert iceberg_format.get_property("snapshot-id") == "456" + + # Test empty properties + delta_format = DeltaFormat(properties=None) + assert len(delta_format.properties) == 0 + + # Test None values in constructors + hudi_format = HudiFormat( + table_type=None, + record_key=None, + precombine_field=None, + properties=None, + ) + assert hudi_format.table_type is None + assert hudi_format.record_key is None + assert hudi_format.precombine_field is None + + def test_hudi_format_comprehensive(self): + """Test comprehensive Hudi format functionality.""" + # Test with all properties + hudi_format = HudiFormat( + table_type="COPY_ON_WRITE", + record_key="id,uuid", + precombine_field="ts", + properties={"custom.prop": "value"}, + ) + + assert ( + hudi_format.get_property("hoodie.datasource.write.table.type") + == "COPY_ON_WRITE" + ) + assert ( + hudi_format.get_property("hoodie.datasource.write.recordkey.field") + == "id,uuid" + ) + assert ( + hudi_format.get_property("hoodie.datasource.write.precombine.field") == "ts" + ) + assert hudi_format.get_property("custom.prop") == "value" + + # Test serialization roundtrip with complex data + serialized = hudi_format.to_dict() + restored = HudiFormat.from_dict(serialized) + assert restored.table_type == hudi_format.table_type + assert restored.record_key == hudi_format.record_key + assert restored.precombine_field == hudi_format.precombine_field + + def test_table_format_with_special_characters(self): + """Test table formats with special characters and edge values.""" + # Test with unicode and special characters + iceberg_format = IcebergFormat( + catalog="测试目录", # Chinese + namespace="тест_ns", # Cyrillic + properties={"special.key": "value with spaces & symbols!@#$%^&*()"}, + ) + + # Serialization roundtrip should preserve special characters + serialized = iceberg_format.to_dict() + restored = IcebergFormat.from_dict(serialized) + assert restored.catalog == "测试目录" + assert restored.namespace == "тест_ns" + assert ( + restored.properties["special.key"] + == "value with spaces & symbols!@#$%^&*()" + ) diff --git a/sdk/python/tests/unit/test_type_map.py b/sdk/python/tests/unit/test_type_map.py index be8a25c1639..4fd62dc680e 100644 --- a/sdk/python/tests/unit/test_type_map.py +++ b/sdk/python/tests/unit/test_type_map.py @@ -1,11 +1,30 @@ +import uuid + import numpy as np import pandas as pd +import pyarrow import pytest +from feast.protos.feast.types.Value_pb2 import Map, MapList +from feast.protos.feast.types.Value_pb2 import Value as ProtoValue from feast.type_map import ( + _convert_value_type_str_to_value_type, + _python_dict_to_map_proto, + _python_list_to_map_list_proto, + arrow_to_pg_type, + feast_value_type_to_pa, feast_value_type_to_python_type, + pa_to_feast_value_type, + pa_to_redshift_value_type, + pg_type_to_feast_value_type, + python_type_to_feast_value_type, python_values_to_proto_values, + redshift_to_feast_value_type, + snowflake_type_to_feast_value_type, + spark_to_feast_value_type, ) +from feast.types import Array, from_feast_to_pyarrow_type +from feast.types import Map as FeastMap from feast.value_type import ValueType @@ -68,6 +87,9 @@ def test_python_values_to_proto_values_bool(values): (np.array([None]), ValueType.BYTES_LIST, None), (np.array([None]), ValueType.STRING_LIST, None), (np.array([None]), ValueType.UNIX_TIMESTAMP_LIST, None), + ([np.array([], dtype=np.int32)], ValueType.INT32_LIST, []), + ([np.array([], dtype=np.float32)], ValueType.FLOAT_LIST, []), + ([np.array([], dtype=np.bool_)], ValueType.BOOL_LIST, []), ([b"[1,2,3]"], ValueType.INT64_LIST, [1, 2, 3]), ([b"[1,2,3]"], ValueType.INT32_LIST, [1, 2, 3]), ([b"[1.5,2.5,3.5]"], ValueType.FLOAT_LIST, [1.5, 2.5, 3.5]), @@ -94,3 +116,2207 @@ def test_python_values_to_proto_values_int_list_with_null_not_supported(): arr = df["column"].to_numpy() with pytest.raises(TypeError): _ = python_values_to_proto_values(arr, ValueType.INT32_LIST) + + +class TestMapTypes: + """Test cases for MAP and MAP_LIST value types.""" + + def test_simple_map_conversion(self): + """Test basic MAP type conversion from Python dict to proto and back.""" + test_dict = {"key1": "value1", "key2": "value2", "key3": 123} + + protos = python_values_to_proto_values([test_dict], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, dict) + assert converted["key1"] == "value1" + assert converted["key2"] == "value2" + assert converted["key3"] == 123 + + def test_nested_map_conversion(self): + """Test nested MAP type conversion.""" + test_dict = { + "level1": { + "level2": {"key": "deep_value", "number": 42}, + "simple": "value", + }, + "top_level": "top_value", + } + + protos = python_values_to_proto_values([test_dict], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, dict) + assert converted["level1"]["level2"]["key"] == "deep_value" + assert converted["level1"]["level2"]["number"] == 42 + assert converted["level1"]["simple"] == "value" + assert converted["top_level"] == "top_value" + + def test_map_with_different_value_types(self): + """Test MAP with various value types.""" + test_dict = { + "string_val": "hello", + "int_val": 42, + "float_val": 3.14, + "bool_val": True, + "list_val": [1, 2, 3], + "string_list_val": ["a", "b", "c"], + } + + protos = python_values_to_proto_values([test_dict], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + + assert converted["string_val"] == "hello" + assert converted["int_val"] == 42 + assert converted["float_val"] == 3.14 + assert converted["bool_val"] is True + assert converted["list_val"] == [1, 2, 3] + assert converted["string_list_val"] == ["a", "b", "c"] + + def test_map_with_none_values(self): + """Test MAP with None values.""" + test_dict = {"key1": "value1", "key2": None, "key3": "value3"} + + protos = python_values_to_proto_values([test_dict], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + + assert converted["key1"] == "value1" + assert converted["key2"] is None + assert converted["key3"] == "value3" + + def test_empty_map(self): + """Test empty MAP conversion.""" + test_dict = {} + + protos = python_values_to_proto_values([test_dict], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, dict) + assert len(converted) == 0 + + def test_null_map(self): + """Test None MAP conversion.""" + protos = python_values_to_proto_values([None], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + + assert converted is None + + def test_map_list_conversion(self): + """Test basic MAP_LIST type conversion.""" + test_list = [ + {"name": "John", "age": 30}, + {"name": "Jane", "age": 25}, + {"name": "Bob", "score": 85.5}, + ] + + protos = python_values_to_proto_values([test_list], ValueType.MAP_LIST) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, list) + assert len(converted) == 3 + assert converted[0]["name"] == "John" + assert converted[0]["age"] == 30 + assert converted[1]["name"] == "Jane" + assert converted[1]["age"] == 25 + assert converted[2]["name"] == "Bob" + assert converted[2]["score"] == 85.5 + + def test_map_list_with_nested_maps(self): + """Test MAP_LIST with nested maps.""" + test_list = [ + {"user": {"name": "John", "details": {"city": "NYC"}}, "score": 100}, + {"user": {"name": "Jane", "details": {"city": "SF"}}, "score": 95}, + ] + + protos = python_values_to_proto_values([test_list], ValueType.MAP_LIST) + converted = feast_value_type_to_python_type(protos[0]) + + assert len(converted) == 2 + assert converted[0]["user"]["name"] == "John" + assert converted[0]["user"]["details"]["city"] == "NYC" + assert converted[1]["user"]["name"] == "Jane" + assert converted[1]["user"]["details"]["city"] == "SF" + + def test_map_list_with_lists_in_maps(self): + """Test MAP_LIST where maps contain lists.""" + test_list = [ + {"name": "John", "hobbies": ["reading", "swimming"]}, + {"name": "Jane", "scores": [95, 87, 92]}, + ] + + protos = python_values_to_proto_values([test_list], ValueType.MAP_LIST) + converted = feast_value_type_to_python_type(protos[0]) + + assert converted[0]["name"] == "John" + assert converted[0]["hobbies"] == ["reading", "swimming"] + assert converted[1]["name"] == "Jane" + assert converted[1]["scores"] == [95, 87, 92] + + def test_empty_map_list(self): + """Test empty MAP_LIST conversion.""" + test_list = [] + + protos = python_values_to_proto_values([test_list], ValueType.MAP_LIST) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, list) + assert len(converted) == 0 + + def test_null_map_list(self): + """Test None MAP_LIST conversion.""" + protos = python_values_to_proto_values([None], ValueType.MAP_LIST) + converted = feast_value_type_to_python_type(protos[0]) + + assert converted is None + + def test_map_list_with_empty_maps(self): + """Test MAP_LIST containing empty maps.""" + test_list = [{}, {"key": "value"}, {}] + + protos = python_values_to_proto_values([test_list], ValueType.MAP_LIST) + converted = feast_value_type_to_python_type(protos[0]) + + assert len(converted) == 3 + assert len(converted[0]) == 0 + assert converted[1]["key"] == "value" + assert len(converted[2]) == 0 + + def test_python_type_inference_for_map(self): + """Test that dictionaries are correctly inferred as MAP type.""" + test_dict = {"key1": "value1", "key2": 123} + + inferred_type = python_type_to_feast_value_type("test_field", test_dict) + + assert inferred_type == ValueType.MAP + + def test_python_type_inference_for_map_list(self): + """Test that list of dictionaries is correctly inferred as MAP_LIST type.""" + test_list = [{"key1": "value1"}, {"key2": "value2"}] + + inferred_type = python_type_to_feast_value_type("test_field", test_list) + + assert inferred_type == ValueType.MAP_LIST + + def test_multiple_map_values(self): + """Test conversion of multiple MAP values.""" + test_dicts = [ + {"name": "Alice", "age": 30}, + {"name": "Bob", "age": 25}, + {"name": "Charlie", "city": "NYC"}, + ] + + protos = python_values_to_proto_values(test_dicts, ValueType.MAP) + converted_values = [feast_value_type_to_python_type(proto) for proto in protos] + + assert len(converted_values) == 3 + assert converted_values[0]["name"] == "Alice" + assert converted_values[1]["name"] == "Bob" + assert converted_values[2]["city"] == "NYC" + + def test_multiple_map_list_values(self): + """Test conversion of multiple MAP_LIST values.""" + test_lists = [[{"id": 1}, {"id": 2}], [{"id": 3}, {"id": 4}], []] + + protos = python_values_to_proto_values(test_lists, ValueType.MAP_LIST) + converted_values = [feast_value_type_to_python_type(proto) for proto in protos] + + assert len(converted_values) == 3 + assert len(converted_values[0]) == 2 + assert converted_values[0][0]["id"] == 1 + assert len(converted_values[2]) == 0 + + def test_map_with_map_list_value(self): + """Test MAP containing MAP_LIST as a value.""" + test_dict = { + "metadata": {"version": "1.0"}, + "items": [{"name": "item1", "count": 5}, {"name": "item2", "count": 3}], + } + + protos = python_values_to_proto_values([test_dict], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + + assert converted["metadata"]["version"] == "1.0" + assert len(converted["items"]) == 2 + assert converted["items"][0]["name"] == "item1" + assert converted["items"][1]["count"] == 3 + + @pytest.mark.parametrize( + "invalid_value", + [ + [{"key": "value"}, "not_a_dict", {"another": "dict"}], + ["string1", "string2"], + [1, 2, 3], + ], + ) + def test_map_list_with_invalid_items(self, invalid_value): + """Test that MAP_LIST with non-dict items raises appropriate errors.""" + with pytest.raises((ValueError, TypeError)): + python_values_to_proto_values([invalid_value], ValueType.MAP_LIST) + + def test_direct_proto_construction(self): + """Test direct construction of Map and MapList proto messages.""" + # Test Map proto construction + test_dict = {"key1": "value1", "key2": 42} + map_proto = _python_dict_to_map_proto(test_dict) + + assert isinstance(map_proto, Map) + assert len(map_proto.val) == 2 + + # Test MapList proto construction + test_list = [{"a": 1}, {"b": 2}] + map_list_proto = _python_list_to_map_list_proto(test_list) + + assert isinstance(map_list_proto, MapList) + assert len(map_list_proto.val) == 2 + + def test_roundtrip_conversion_consistency(self): + """Test that roundtrip conversion maintains data integrity.""" + original_map = { + "string": "hello", + "integer": 42, + "float": 3.14159, + "boolean": True, + "nested": {"inner_string": "world", "inner_list": [1, 2, 3]}, + "list_of_maps": [{"item": "first"}, {"item": "second"}], + } + + # Convert to proto and back + protos = python_values_to_proto_values([original_map], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + + # Verify all data is preserved + assert converted["string"] == original_map["string"] + assert converted["integer"] == original_map["integer"] + assert converted["float"] == original_map["float"] + assert converted["boolean"] == original_map["boolean"] + assert ( + converted["nested"]["inner_string"] + == original_map["nested"]["inner_string"] + ) + assert converted["nested"]["inner_list"] == original_map["nested"]["inner_list"] + assert len(converted["list_of_maps"]) == len(original_map["list_of_maps"]) + assert converted["list_of_maps"][0]["item"] == "first" + assert converted["list_of_maps"][1]["item"] == "second" + + +class TestSetTypes: + """Test cases for SET value types.""" + + def test_simple_set_conversion(self): + """Test basic SET type conversion from Python set to proto and back.""" + test_set = {1, 2, 3, 4, 5} + + protos = python_values_to_proto_values([test_set], ValueType.INT32_SET) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, set) + assert converted == test_set + + def test_string_set_conversion(self): + """Test STRING_SET type conversion.""" + test_set = {"apple", "banana", "cherry"} + + protos = python_values_to_proto_values([test_set], ValueType.STRING_SET) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, set) + assert converted == test_set + + def test_float_set_conversion(self): + """Test FLOAT_SET type conversion.""" + test_set = {1.5, 2.5, 3.5} + + protos = python_values_to_proto_values([test_set], ValueType.FLOAT_SET) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, set) + assert converted == test_set + + def test_bool_set_conversion(self): + """Test BOOL_SET type conversion.""" + test_set = {True, False} + + protos = python_values_to_proto_values([test_set], ValueType.BOOL_SET) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, set) + assert converted == test_set + + def test_set_from_list_with_duplicates(self): + """Test that duplicate values in lists are removed when converted to sets.""" + test_list = [1, 2, 2, 3, 3, 3, 4, 5, 5] + + protos = python_values_to_proto_values([test_list], ValueType.INT32_SET) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, set) + assert converted == {1, 2, 3, 4, 5} + + def test_empty_set(self): + """Test empty SET conversion.""" + test_set = set() + + protos = python_values_to_proto_values([test_set], ValueType.STRING_SET) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, set) + assert len(converted) == 0 + + def test_null_set(self): + """Test None SET conversion.""" + protos = python_values_to_proto_values([None], ValueType.INT32_SET) + converted = feast_value_type_to_python_type(protos[0]) + + assert converted is None + + def test_multiple_set_values(self): + """Test conversion of multiple set values.""" + test_sets = [{1, 2, 3}, {4, 5}, {6}] + + protos = python_values_to_proto_values(test_sets, ValueType.INT32_SET) + + assert len(protos) == 3 + assert feast_value_type_to_python_type(protos[0]) == {1, 2, 3} + assert feast_value_type_to_python_type(protos[1]) == {4, 5} + assert feast_value_type_to_python_type(protos[2]) == {6} + + +class TestMapArrowTypeSupport: + """Test cases for MAP and MAP_LIST Arrow type conversions.""" + + def test_feast_value_type_to_pa_map(self): + """Test that ValueType.MAP converts to a PyArrow map type.""" + pa_type = feast_value_type_to_pa(ValueType.MAP) + assert isinstance(pa_type, pyarrow.MapType) + assert pa_type.key_type == pyarrow.string() + + def test_feast_value_type_to_pa_map_list(self): + """Test that ValueType.MAP_LIST converts to a PyArrow list of maps.""" + pa_type = feast_value_type_to_pa(ValueType.MAP_LIST) + assert isinstance(pa_type, pyarrow.ListType) + assert isinstance(pa_type.value_type, pyarrow.MapType) + + def test_pa_to_feast_value_type_map(self): + """Test that PyArrow map type string converts to ValueType.MAP.""" + result = pa_to_feast_value_type("map") + assert result == ValueType.MAP + + def test_pa_to_feast_value_type_map_various_value_types(self): + """Test that various PyArrow map type strings all convert to MAP.""" + assert pa_to_feast_value_type("map") == ValueType.MAP + assert pa_to_feast_value_type("map") == ValueType.MAP + assert pa_to_feast_value_type("map") == ValueType.MAP + + def test_from_feast_to_pyarrow_type_map(self): + """Test that Feast Map type converts to PyArrow map type.""" + pa_type = from_feast_to_pyarrow_type(FeastMap) + assert isinstance(pa_type, pyarrow.MapType) + + def test_from_feast_to_pyarrow_type_array_map(self): + """Test that Feast Array(Map) converts to PyArrow list of maps.""" + pa_type = from_feast_to_pyarrow_type(Array(FeastMap)) + assert isinstance(pa_type, pyarrow.ListType) + assert isinstance(pa_type.value_type, pyarrow.MapType) + + def test_convert_value_type_str_map(self): + """Test that 'MAP' string converts to ValueType.MAP.""" + assert _convert_value_type_str_to_value_type("MAP") == ValueType.MAP + + def test_convert_value_type_str_map_list(self): + """Test that 'MAP_LIST' string converts to ValueType.MAP_LIST.""" + assert _convert_value_type_str_to_value_type("MAP_LIST") == ValueType.MAP_LIST + + def test_arrow_to_pg_type_map(self): + """Test that Arrow map type converts to Postgres jsonb.""" + assert arrow_to_pg_type("map") == "jsonb" + assert arrow_to_pg_type("map") == "jsonb" + + def test_pg_type_to_feast_value_type_json(self): + """Test that Postgres json/jsonb types convert to ValueType.MAP.""" + assert pg_type_to_feast_value_type("json") == ValueType.MAP + assert pg_type_to_feast_value_type("jsonb") == ValueType.MAP + + def test_pg_type_to_feast_value_type_json_array(self): + """Test that Postgres json[]/jsonb[] types convert to ValueType.MAP_LIST.""" + assert pg_type_to_feast_value_type("json[]") == ValueType.MAP_LIST + assert pg_type_to_feast_value_type("jsonb[]") == ValueType.MAP_LIST + + def test_pg_type_to_feast_value_type_real(self): + """Postgres real is single-precision (float4), so it maps to FLOAT, not DOUBLE.""" + assert pg_type_to_feast_value_type("real") == ValueType.FLOAT + assert pg_type_to_feast_value_type("real[]") == ValueType.FLOAT_LIST + + def test_snowflake_variant_to_map(self): + """Test that Snowflake VARIANT/OBJECT types convert to ValueType.MAP.""" + assert snowflake_type_to_feast_value_type("VARIANT") == ValueType.MAP + assert snowflake_type_to_feast_value_type("OBJECT") == ValueType.MAP + + def test_redshift_super_to_map(self): + """Test that Redshift super type converts to ValueType.MAP.""" + assert redshift_to_feast_value_type("super") == ValueType.MAP + + def test_map_roundtrip_proto_to_arrow_type(self): + """Test that MAP type survives a full conversion roundtrip.""" + pa_type = feast_value_type_to_pa(ValueType.MAP) + pa_type_str = str(pa_type) + roundtrip = pa_to_feast_value_type(pa_type_str) + assert roundtrip == ValueType.MAP + + def test_spark_map_to_feast(self): + """Test that Spark map types convert to ValueType.MAP.""" + assert spark_to_feast_value_type("map") == ValueType.MAP + assert spark_to_feast_value_type("map") == ValueType.MAP + assert spark_to_feast_value_type("MAP") == ValueType.MAP + + def test_spark_array_map_to_feast(self): + """Test that Spark array> types convert to ValueType.MAP_LIST.""" + assert ( + spark_to_feast_value_type("array>") == ValueType.MAP_LIST + ) + + def test_spark_unknown_still_returns_null(self): + """Test that unrecognized Spark types still return NULL.""" + assert spark_to_feast_value_type("interval") == ValueType.NULL + + def test_spark_struct_to_feast_struct(self): + """Test that Spark struct types now convert to ValueType.STRUCT.""" + assert spark_to_feast_value_type("struct") == ValueType.STRUCT + + +class TestEnableValidationOnFeatureView: + """Test that enable_validation is a real parameter on FeatureView.""" + + def test_feature_view_has_enable_validation_default_false(self): + """Test that FeatureView has enable_validation defaulting to False.""" + import inspect + + from feast.feature_view import FeatureView + + sig = inspect.signature(FeatureView.__init__) + assert "enable_validation" in sig.parameters + assert sig.parameters["enable_validation"].default is False + + def test_batch_feature_view_has_enable_validation(self): + """Test that BatchFeatureView has enable_validation parameter.""" + import inspect + + from feast.batch_feature_view import BatchFeatureView + + sig = inspect.signature(BatchFeatureView.__init__) + assert "enable_validation" in sig.parameters + assert sig.parameters["enable_validation"].default is False + + def test_stream_feature_view_has_enable_validation(self): + """Test that StreamFeatureView has enable_validation parameter.""" + import inspect + + from feast.stream_feature_view import StreamFeatureView + + sig = inspect.signature(StreamFeatureView.__init__) + assert "enable_validation" in sig.parameters + assert sig.parameters["enable_validation"].default is False + + +class TestRedshiftDynamoDBMapSupport: + """Test cases for DynamoDB + Redshift map type round-trips.""" + + def test_pa_to_redshift_value_type_map(self): + """Test that Arrow map type maps to Redshift 'super' type.""" + pa_type = feast_value_type_to_pa(ValueType.MAP) + assert pa_to_redshift_value_type(pa_type) == "super" + + def test_pa_to_redshift_value_type_map_list(self): + """Test that Arrow list-of-map type maps to Redshift 'super' type.""" + pa_type = feast_value_type_to_pa(ValueType.MAP_LIST) + assert pa_to_redshift_value_type(pa_type) == "super" + + def test_json_string_to_map_proto(self): + """Test that JSON strings are parsed to MAP protos during materialization.""" + json_str = '{"key1": "value1", "key2": "value2"}' + protos = python_values_to_proto_values([json_str], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + assert isinstance(converted, dict) + assert converted["key1"] == "value1" + assert converted["key2"] == "value2" + + def test_json_string_to_map_list_proto(self): + """Test that JSON strings are parsed to MAP_LIST protos during materialization.""" + json_str = '[{"a": "1"}, {"b": "2"}]' + protos = python_values_to_proto_values([json_str], ValueType.MAP_LIST) + converted = feast_value_type_to_python_type(protos[0]) + assert isinstance(converted, list) + assert len(converted) == 2 + assert converted[0]["a"] == "1" + + def test_dict_still_works_for_map(self): + """Test that regular Python dicts still work for MAP (no regression).""" + test_dict = {"x": "y", "a": 1} + protos = python_values_to_proto_values([test_dict], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + assert isinstance(converted, dict) + assert converted["x"] == "y" + + def test_none_map_still_works(self): + """Test that None MAP values still produce empty proto (no regression).""" + protos = python_values_to_proto_values([None], ValueType.MAP) + converted = feast_value_type_to_python_type(protos[0]) + assert converted is None + + def test_redshift_super_roundtrip(self): + """Test full type conversion roundtrip: Redshift super → Feast MAP → Arrow → Redshift super.""" + feast_type = redshift_to_feast_value_type("super") + assert feast_type == ValueType.MAP + pa_type = feast_value_type_to_pa(feast_type) + redshift_type = pa_to_redshift_value_type(pa_type) + assert redshift_type == "super" + + +class TestJsonTypeSupport: + """Test cases for JSON value type.""" + + def test_simple_json_conversion(self): + """Test basic JSON type conversion: Python dict -> proto (json_val) -> Python.""" + test_data = {"name": "Alice", "age": 30, "active": True} + protos = python_values_to_proto_values([test_data], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, dict) + assert converted["name"] == "Alice" + assert converted["age"] == 30 + assert converted["active"] is True + + def test_json_string_passthrough(self): + """Test that a raw JSON string is stored and returned correctly.""" + json_str = '{"key": "value", "count": 42}' + protos = python_values_to_proto_values([json_str], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, dict) + assert converted["key"] == "value" + assert converted["count"] == 42 + + def test_json_array_value(self): + """Test JSON type with an array as the top-level value.""" + test_data = [1, 2, 3, "four"] + protos = python_values_to_proto_values([test_data], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, list) + assert converted == [1, 2, 3, "four"] + + def test_json_nested(self): + """Test deeply nested JSON structures.""" + test_data = { + "level1": {"level2": {"level3": {"value": "deep"}}}, + "array": [{"a": 1}, {"b": 2}], + } + protos = python_values_to_proto_values([test_data], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + + assert converted["level1"]["level2"]["level3"]["value"] == "deep" + assert converted["array"][0]["a"] == 1 + + def test_null_json(self): + """Test None JSON conversion.""" + protos = python_values_to_proto_values([None], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + assert converted is None + + def test_json_list_conversion(self): + """Test JSON_LIST type conversion.""" + test_data = [ + {"name": "Alice"}, + '{"name": "Bob"}', + {"count": 42}, + ] + protos = python_values_to_proto_values([test_data], ValueType.JSON_LIST) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, list) + assert len(converted) == 3 + assert converted[0] == {"name": "Alice"} + assert converted[1] == {"name": "Bob"} + assert converted[2] == {"count": 42} + + def test_null_json_list(self): + """Test None JSON_LIST conversion.""" + protos = python_values_to_proto_values([None], ValueType.JSON_LIST) + converted = feast_value_type_to_python_type(protos[0]) + assert converted is None + + def test_multiple_json_values(self): + """Test conversion of multiple JSON values.""" + test_values = [ + {"x": 1}, + {"y": 2}, + None, + {"z": 3}, + ] + protos = python_values_to_proto_values(test_values, ValueType.JSON) + converted = [feast_value_type_to_python_type(p) for p in protos] + + assert converted[0] == {"x": 1} + assert converted[1] == {"y": 2} + assert converted[2] is None + assert converted[3] == {"z": 3} + + def test_feast_value_type_to_pa_json(self): + """Test that ValueType.JSON converts to PyArrow large_string.""" + pa_type = feast_value_type_to_pa(ValueType.JSON) + assert pa_type == pyarrow.large_string() + + def test_feast_value_type_to_pa_json_list(self): + """Test that ValueType.JSON_LIST converts to PyArrow list of large_string.""" + pa_type = feast_value_type_to_pa(ValueType.JSON_LIST) + assert isinstance(pa_type, pyarrow.ListType) + assert pa_type.value_type == pyarrow.large_string() + + def test_convert_value_type_str_json(self): + """Test that 'JSON' string converts to ValueType.JSON.""" + assert _convert_value_type_str_to_value_type("JSON") == ValueType.JSON + assert _convert_value_type_str_to_value_type("JSON_LIST") == ValueType.JSON_LIST + + def test_arrow_to_pg_type_json(self): + """Test that Arrow large_string converts to Postgres jsonb.""" + assert arrow_to_pg_type("large_string") == "jsonb" + + def test_bq_json_to_feast(self): + """Test that BigQuery JSON type converts to ValueType.JSON.""" + from feast.type_map import bq_to_feast_value_type + + assert bq_to_feast_value_type("JSON") == ValueType.JSON + + def test_spark_struct_not_json(self): + """Test that Spark struct types map to STRUCT not JSON.""" + assert spark_to_feast_value_type("struct") == ValueType.STRUCT + + def test_snowflake_json_to_feast(self): + """Test that Snowflake JSON type converts to ValueType.JSON.""" + assert snowflake_type_to_feast_value_type("JSON") == ValueType.JSON + + def test_json_feast_type_aliases(self): + """Test Json FeastType alias and conversions.""" + from feast.types import Json, from_feast_to_pyarrow_type + + pa_type = from_feast_to_pyarrow_type(Json) + assert pa_type == pyarrow.large_string() + + def test_json_value_types_mapping(self): + """Test JSON types in VALUE_TYPES_TO_FEAST_TYPES.""" + from feast.types import VALUE_TYPES_TO_FEAST_TYPES, Json + + assert VALUE_TYPES_TO_FEAST_TYPES[ValueType.JSON] == Json + + def test_pa_to_feast_value_type_large_string(self): + """Test that large_string arrow type converts to ValueType.JSON.""" + result = pa_to_feast_value_type("large_string") + assert result == ValueType.JSON + + +class TestStructTypeSupport: + """Test cases for STRUCT value type.""" + + def test_simple_struct_conversion(self): + """Test basic STRUCT type conversion: Python dict -> proto (struct_val) -> Python dict.""" + test_data = {"name": "Alice", "age": 30} + protos = python_values_to_proto_values([test_data], ValueType.STRUCT) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, dict) + assert converted["name"] == "Alice" + assert converted["age"] == 30 + + def test_nested_struct_conversion(self): + """Test nested STRUCT type conversion.""" + test_data = { + "address": {"street": "123 Main St", "city": "NYC"}, + "name": "Alice", + } + protos = python_values_to_proto_values([test_data], ValueType.STRUCT) + converted = feast_value_type_to_python_type(protos[0]) + + assert converted["address"]["street"] == "123 Main St" + assert converted["address"]["city"] == "NYC" + assert converted["name"] == "Alice" + + def test_null_struct(self): + """Test None STRUCT conversion.""" + protos = python_values_to_proto_values([None], ValueType.STRUCT) + converted = feast_value_type_to_python_type(protos[0]) + assert converted is None + + def test_struct_list_conversion(self): + """Test STRUCT_LIST type conversion.""" + test_data = [ + {"name": "Alice", "age": 30}, + {"name": "Bob", "age": 25}, + ] + protos = python_values_to_proto_values([test_data], ValueType.STRUCT_LIST) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, list) + assert len(converted) == 2 + assert converted[0]["name"] == "Alice" + assert converted[1]["age"] == 25 + + def test_null_struct_list(self): + """Test None STRUCT_LIST conversion.""" + protos = python_values_to_proto_values([None], ValueType.STRUCT_LIST) + converted = feast_value_type_to_python_type(protos[0]) + assert converted is None + + def test_multiple_struct_values(self): + """Test conversion of multiple STRUCT values.""" + test_values = [ + {"x": 1}, + None, + {"y": 2, "z": 3}, + ] + protos = python_values_to_proto_values(test_values, ValueType.STRUCT) + converted = [feast_value_type_to_python_type(p) for p in protos] + + assert converted[0] == {"x": 1} + assert converted[1] is None + assert converted[2] == {"y": 2, "z": 3} + + def test_struct_class_creation(self): + """Test Struct FeastType creation and validation.""" + from feast.types import Int32, String, Struct + + struct_type = Struct({"name": String, "age": Int32}) + assert struct_type.to_value_type() == ValueType.STRUCT + assert "name" in struct_type.fields + assert struct_type.fields["name"] == String + assert struct_type.fields["age"] == Int32 + + def test_struct_empty_raises(self): + """Test that empty Struct raises ValueError.""" + from feast.types import Struct + + with pytest.raises(ValueError, match="at least one field"): + Struct({}) + + def test_struct_to_pyarrow(self): + """Test Struct type converts to PyArrow struct.""" + from feast.types import Int32, String, Struct + + struct_type = Struct({"name": String, "age": Int32}) + pa_type = struct_type.to_pyarrow_type() + + assert pyarrow.types.is_struct(pa_type) + assert pa_type.get_field_index("name") >= 0 + assert pa_type.get_field_index("age") >= 0 + + def test_struct_from_feast_to_pyarrow(self): + """Test from_feast_to_pyarrow_type handles Struct.""" + from feast.types import Int32, String, Struct + + struct_type = Struct({"name": String, "age": Int32}) + pa_type = from_feast_to_pyarrow_type(struct_type) + + assert pyarrow.types.is_struct(pa_type) + + def test_array_of_struct(self): + """Test Array(Struct(...)) works.""" + from feast.types import Array, Int32, String, Struct + + struct_type = Struct({"name": String, "value": Int32}) + array_type = Array(struct_type) + + assert array_type.to_value_type() == ValueType.STRUCT_LIST + pa_type = from_feast_to_pyarrow_type(array_type) + assert isinstance(pa_type, pyarrow.ListType) + assert pyarrow.types.is_struct(pa_type.value_type) + + def test_feast_value_type_to_pa_struct(self): + """Test that ValueType.STRUCT converts to PyArrow struct (empty default).""" + pa_type = feast_value_type_to_pa(ValueType.STRUCT) + assert pyarrow.types.is_struct(pa_type) + + def test_feast_value_type_to_pa_struct_list(self): + """Test that ValueType.STRUCT_LIST converts to PyArrow list of struct.""" + pa_type = feast_value_type_to_pa(ValueType.STRUCT_LIST) + assert isinstance(pa_type, pyarrow.ListType) + assert pyarrow.types.is_struct(pa_type.value_type) + + def test_convert_value_type_str_struct(self): + """Test that 'STRUCT' string converts to ValueType.STRUCT.""" + assert _convert_value_type_str_to_value_type("STRUCT") == ValueType.STRUCT + assert ( + _convert_value_type_str_to_value_type("STRUCT_LIST") + == ValueType.STRUCT_LIST + ) + + def test_spark_struct_to_feast(self): + """Test that Spark struct types convert to ValueType.STRUCT.""" + assert spark_to_feast_value_type("struct") == ValueType.STRUCT + assert spark_to_feast_value_type("STRUCT") == ValueType.STRUCT + + def test_spark_array_struct_to_feast(self): + """Test that Spark array> types convert to STRUCT_LIST.""" + assert ( + spark_to_feast_value_type("array>") == ValueType.STRUCT_LIST + ) + + def test_bq_struct_to_feast(self): + """Test that BigQuery STRUCT/RECORD types convert to ValueType.STRUCT.""" + from feast.type_map import bq_to_feast_value_type + + assert bq_to_feast_value_type("STRUCT") == ValueType.STRUCT + assert bq_to_feast_value_type("RECORD") == ValueType.STRUCT + + def test_pa_to_feast_value_type_struct(self): + """Test that struct arrow type string converts to ValueType.STRUCT.""" + result = pa_to_feast_value_type("struct") + assert result == ValueType.STRUCT + + def test_struct_schema_persistence(self): + """Test that Struct schema is preserved through Field serialization/deserialization.""" + from feast.field import Field + from feast.types import Int32, String, Struct + + struct_type = Struct({"street": String, "zip": Int32}) + field = Field(name="address", dtype=struct_type) + + proto = field.to_proto() + restored = Field.from_proto(proto) + + assert isinstance(restored.dtype, Struct) + assert "street" in restored.dtype.fields + assert "zip" in restored.dtype.fields + assert restored.dtype.fields["street"] == String + assert restored.dtype.fields["zip"] == Int32 + + def test_struct_json_string_parsing(self): + """Test that JSON string input is parsed for STRUCT type.""" + json_str = '{"name": "Alice", "score": 95}' + protos = python_values_to_proto_values([json_str], ValueType.STRUCT) + converted = feast_value_type_to_python_type(protos[0]) + + assert isinstance(converted, dict) + assert converted["name"] == "Alice" + assert converted["score"] == 95 + + def test_struct_equality(self): + """Test Struct type equality.""" + from feast.types import Int32, String, Struct + + s1 = Struct({"name": String, "age": Int32}) + s2 = Struct({"name": String, "age": Int32}) + s3 = Struct({"name": String}) + + assert s1 == s2 + assert s1 != s3 + + def test_from_feast_type_struct(self): + """Test from_feast_type works for Struct.""" + from feast.types import Int32, String, Struct, from_feast_type + + struct_type = Struct({"name": String, "age": Int32}) + value_type = from_feast_type(struct_type) + assert value_type == ValueType.STRUCT + + def test_from_value_type_struct(self): + """Test from_value_type works for STRUCT (returns placeholder).""" + from feast.types import Struct, from_value_type + + feast_type = from_value_type(ValueType.STRUCT) + assert isinstance(feast_type, Struct) + + def test_from_value_type_struct_list(self): + """Test from_value_type works for STRUCT_LIST (returns placeholder Array(Struct)).""" + from feast.types import Array, Struct, from_value_type + + feast_type = from_value_type(ValueType.STRUCT_LIST) + assert isinstance(feast_type, Array) + assert isinstance(feast_type.base_type, Struct) + + +class TestJsonValidation: + """Test JSON well-formedness validation.""" + + def test_proto_conversion_valid_json_string(self): + """Valid JSON strings should convert without error.""" + valid_json = '{"key": "value", "num": 42}' + protos = python_values_to_proto_values([valid_json], ValueType.JSON) + assert protos[0].json_val == valid_json + + def test_proto_conversion_invalid_json_string_raises(self): + """Invalid JSON strings should raise ValueError during proto conversion.""" + import pytest + + invalid_json = "this is not json {{" + with pytest.raises(ValueError, match="Invalid JSON string for JSON type"): + python_values_to_proto_values([invalid_json], ValueType.JSON) + + def test_proto_conversion_dict_no_validation_needed(self): + """Python dicts are valid by definition and should not raise.""" + data = {"name": "Alice", "items": [1, 2, 3]} + protos = python_values_to_proto_values([data], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + assert converted == data + + def test_proto_conversion_list_no_validation_needed(self): + """Python lists are valid by definition and should not raise.""" + data = [1, "two", {"three": 3}] + protos = python_values_to_proto_values([data], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + assert converted == data + + def test_proto_conversion_none_passes(self): + """None values should pass through without validation.""" + protos = python_values_to_proto_values([None], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + assert converted is None + + def test_proto_conversion_json_list_invalid_string_raises(self): + """Invalid JSON strings in JSON_LIST should raise ValueError.""" + import pytest + + data = ['{"valid": true}', "not-json"] + with pytest.raises(ValueError, match="Invalid JSON string in JSON_LIST"): + python_values_to_proto_values([data], ValueType.JSON_LIST) + + def test_proto_conversion_json_list_valid_mixed(self): + """JSON_LIST with valid strings and dicts should succeed.""" + data = ['{"a": 1}', {"b": 2}] + protos = python_values_to_proto_values([data], ValueType.JSON_LIST) + converted = feast_value_type_to_python_type(protos[0]) + assert len(converted) == 2 + assert converted[0] == {"a": 1} + assert converted[1] == {"b": 2} + + def test_proto_conversion_json_scalar_string(self): + """JSON scalar values like numbers-as-strings should validate.""" + protos = python_values_to_proto_values(["42"], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + assert converted == 42 + + def test_proto_conversion_json_null_string(self): + """The JSON string 'null' is valid JSON.""" + protos = python_values_to_proto_values(["null"], ValueType.JSON) + converted = feast_value_type_to_python_type(protos[0]) + assert converted is None + + def test_proto_conversion_json_empty_string_raises(self): + """An empty string is not valid JSON.""" + import pytest + + with pytest.raises(ValueError, match="Invalid JSON string for JSON type"): + python_values_to_proto_values([""], ValueType.JSON) + + def test_local_validation_node_valid_json(self): + """LocalValidationNode should accept valid JSON strings.""" + from feast.infra.compute_engines.local.nodes import LocalValidationNode + + table = pyarrow.table( + {"config": ['{"a": 1}', '{"b": 2}', "null"]}, + schema=pyarrow.schema([pyarrow.field("config", pyarrow.string())]), + ) + + node = LocalValidationNode( + name="test_validate", + validation_config={ + "columns": {"config": pyarrow.large_string()}, + "json_columns": {"config"}, + }, + backend=None, + ) + # Should not raise + node._validate_schema(table) + + def test_local_validation_node_invalid_json(self): + """LocalValidationNode should reject invalid JSON strings.""" + import pytest + + from feast.infra.compute_engines.local.nodes import LocalValidationNode + + table = pyarrow.table( + {"config": ['{"valid": true}', "not-json-at-all", '{"ok": 1}']}, + schema=pyarrow.schema([pyarrow.field("config", pyarrow.string())]), + ) + + node = LocalValidationNode( + name="test_validate", + validation_config={ + "columns": {"config": pyarrow.large_string()}, + "json_columns": {"config"}, + }, + backend=None, + ) + with pytest.raises(ValueError, match="invalid JSON value"): + node._validate_schema(table) + + def test_local_validation_node_skips_nulls(self): + """LocalValidationNode should skip null values in JSON columns.""" + from feast.infra.compute_engines.local.nodes import LocalValidationNode + + table = pyarrow.table( + {"config": ['{"a": 1}', None, '{"b": 2}']}, + schema=pyarrow.schema([pyarrow.field("config", pyarrow.string())]), + ) + + node = LocalValidationNode( + name="test_validate", + validation_config={ + "columns": {"config": pyarrow.large_string()}, + "json_columns": {"config"}, + }, + backend=None, + ) + # Should not raise + node._validate_schema(table) + + def test_local_validation_node_no_json_columns(self): + """LocalValidationNode should skip JSON validation if no json_columns.""" + from feast.infra.compute_engines.local.nodes import LocalValidationNode + + table = pyarrow.table( + {"data": ["not-json"]}, + schema=pyarrow.schema([pyarrow.field("data", pyarrow.string())]), + ) + + node = LocalValidationNode( + name="test_validate", + validation_config={ + "columns": {"data": pyarrow.string()}, + }, + backend=None, + ) + # Should not raise — no json_columns configured + node._validate_schema(table) + + def test_local_validation_node_error_message_shows_row_and_detail(self): + """Error message should include the row number and parse error.""" + import pytest + + from feast.infra.compute_engines.local.nodes import LocalValidationNode + + table = pyarrow.table( + {"config": ['{"ok": 1}', '{"ok": 2}', "{bad}"]}, + schema=pyarrow.schema([pyarrow.field("config", pyarrow.string())]), + ) + + node = LocalValidationNode( + name="test_validate", + validation_config={ + "columns": {"config": pyarrow.large_string()}, + "json_columns": {"config"}, + }, + backend=None, + ) + with pytest.raises(ValueError, match="row 2"): + node._validate_schema(table) + + +class TestSparkNativeTypeValidation: + """Test Spark-native type mapping and compatibility checking.""" + + def test_feast_string_to_spark_string(self): + from pyspark.sql.types import StringType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import String + + assert from_feast_to_spark_type(String) == StringType() + + def test_feast_int32_to_spark_integer(self): + from pyspark.sql.types import IntegerType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Int32 + + assert from_feast_to_spark_type(Int32) == IntegerType() + + def test_feast_int64_to_spark_long(self): + from pyspark.sql.types import LongType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Int64 + + assert from_feast_to_spark_type(Int64) == LongType() + + def test_feast_float32_to_spark_float(self): + from pyspark.sql.types import FloatType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Float32 + + assert from_feast_to_spark_type(Float32) == FloatType() + + def test_feast_float64_to_spark_double(self): + from pyspark.sql.types import DoubleType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Float64 + + assert from_feast_to_spark_type(Float64) == DoubleType() + + def test_feast_bool_to_spark_boolean(self): + from pyspark.sql.types import BooleanType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Bool + + assert from_feast_to_spark_type(Bool) == BooleanType() + + def test_feast_bytes_to_spark_binary(self): + from pyspark.sql.types import BinaryType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Bytes + + assert from_feast_to_spark_type(Bytes) == BinaryType() + + def test_feast_timestamp_to_spark_timestamp(self): + from pyspark.sql.types import TimestampType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import UnixTimestamp + + assert from_feast_to_spark_type(UnixTimestamp) == TimestampType() + + def test_feast_map_to_spark_map(self): + from pyspark.sql.types import MapType, StringType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Map + + assert from_feast_to_spark_type(Map) == MapType(StringType(), StringType()) + + def test_feast_json_to_spark_string(self): + from pyspark.sql.types import StringType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Json + + assert from_feast_to_spark_type(Json) == StringType() + + def test_feast_array_int_to_spark_array(self): + from pyspark.sql.types import ArrayType, IntegerType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Array, Int32 + + assert from_feast_to_spark_type(Array(Int32)) == ArrayType(IntegerType()) + + def test_feast_array_map_to_spark_array(self): + from pyspark.sql.types import ArrayType, MapType, StringType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Array, Map + + assert from_feast_to_spark_type(Array(Map)) == ArrayType( + MapType(StringType(), StringType()) + ) + + def test_feast_struct_to_spark_struct(self): + from pyspark.sql.types import IntegerType, StringType, StructField, StructType + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Int32, String, Struct + + struct = Struct({"name": String, "age": Int32}) + expected = StructType( + [ + StructField("name", StringType(), True), + StructField("age", IntegerType(), True), + ] + ) + assert from_feast_to_spark_type(struct) == expected + + def test_feast_array_struct_to_spark_array_struct(self): + from pyspark.sql.types import ( + ArrayType, + IntegerType, + StringType, + StructField, + StructType, + ) + + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Array, Int32, String, Struct + + struct = Struct({"name": String, "age": Int32}) + expected = ArrayType( + StructType( + [ + StructField("name", StringType(), True), + StructField("age", IntegerType(), True), + ] + ) + ) + assert from_feast_to_spark_type(Array(struct)) == expected + + def test_unsupported_type_returns_none(self): + from feast.infra.compute_engines.spark.nodes import from_feast_to_spark_type + from feast.types import Invalid + + assert from_feast_to_spark_type(Invalid) is None + + # Compatibility tests + + def test_exact_match_compatible(self): + from pyspark.sql.types import StringType + + from feast.infra.compute_engines.spark.nodes import _spark_types_compatible + + assert _spark_types_compatible(StringType(), StringType()) + + def test_map_struct_compatible(self): + from pyspark.sql.types import MapType, StringType, StructType + + from feast.infra.compute_engines.spark.nodes import _spark_types_compatible + + assert _spark_types_compatible( + MapType(StringType(), StringType()), StructType([]) + ) + + def test_struct_map_compatible(self): + from pyspark.sql.types import MapType, StringType, StructType + + from feast.infra.compute_engines.spark.nodes import _spark_types_compatible + + assert _spark_types_compatible( + StructType([]), MapType(StringType(), StringType()) + ) + + def test_integer_long_widening_compatible(self): + from pyspark.sql.types import IntegerType, LongType + + from feast.infra.compute_engines.spark.nodes import _spark_types_compatible + + assert _spark_types_compatible(IntegerType(), LongType()) + assert _spark_types_compatible(LongType(), IntegerType()) + + def test_float_double_widening_compatible(self): + from pyspark.sql.types import DoubleType, FloatType + + from feast.infra.compute_engines.spark.nodes import _spark_types_compatible + + assert _spark_types_compatible(FloatType(), DoubleType()) + assert _spark_types_compatible(DoubleType(), FloatType()) + + def test_string_vs_integer_incompatible(self): + from pyspark.sql.types import IntegerType, StringType + + from feast.infra.compute_engines.spark.nodes import _spark_types_compatible + + assert not _spark_types_compatible(StringType(), IntegerType()) + + def test_bool_vs_double_incompatible(self): + from pyspark.sql.types import BooleanType, DoubleType + + from feast.infra.compute_engines.spark.nodes import _spark_types_compatible + + assert not _spark_types_compatible(BooleanType(), DoubleType()) + + def test_array_element_compatibility(self): + from pyspark.sql.types import ArrayType, IntegerType, LongType + + from feast.infra.compute_engines.spark.nodes import _spark_types_compatible + + assert _spark_types_compatible(ArrayType(IntegerType()), ArrayType(LongType())) + + def test_array_element_incompatibility(self): + from pyspark.sql.types import ArrayType, IntegerType, StringType + + from feast.infra.compute_engines.spark.nodes import _spark_types_compatible + + assert not _spark_types_compatible( + ArrayType(StringType()), ArrayType(IntegerType()) + ) + + +class TestUuidTypes: + """Test cases for UUID and TIME_UUID value types.""" + + def test_uuid_string_roundtrip(self): + """UUID string -> proto -> uuid.UUID object roundtrip.""" + test_uuid = uuid.uuid4() + protos = python_values_to_proto_values([str(test_uuid)], ValueType.UUID) + assert protos[0].uuid_val == str(test_uuid) + + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, uuid.UUID) + assert result == test_uuid + + def test_uuid_object_serialization(self): + """uuid.UUID object -> proto serialization (str conversion automatic).""" + test_uuid = uuid.uuid4() + protos = python_values_to_proto_values([test_uuid], ValueType.UUID) + assert protos[0].uuid_val == str(test_uuid) + + def test_time_uuid_roundtrip(self): + """TIME_UUID string -> proto -> uuid.UUID object roundtrip.""" + test_uuid = uuid.uuid1() + protos = python_values_to_proto_values([str(test_uuid)], ValueType.TIME_UUID) + assert protos[0].time_uuid_val == str(test_uuid) + + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, uuid.UUID) + assert result == test_uuid + + def test_uuid_without_feature_type_returns_uuid(self): + """With dedicated uuid_val proto field, UUID is identified without feature_type hint.""" + test_uuid = uuid.uuid4() + protos = python_values_to_proto_values([str(test_uuid)], ValueType.UUID) + + # No feature_type hint needed — uuid_val field identifies the type + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, uuid.UUID) + assert result == test_uuid + + def test_uuid_backward_compat_string_val(self): + """UUIDs stored as string_val (old format) still work with feature_type hint.""" + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + test_uuid = uuid.uuid4() + # Simulate old-format proto with string_val + proto = ProtoValue(string_val=str(test_uuid)) + + # Without feature_type, returns plain string + result = feast_value_type_to_python_type(proto) + assert isinstance(result, str) + + # With feature_type hint, returns uuid.UUID + result = feast_value_type_to_python_type(proto, ValueType.UUID) + assert isinstance(result, uuid.UUID) + assert result == test_uuid + + def test_uuid_list_roundtrip(self): + """UUID list -> proto -> list of uuid.UUID objects roundtrip.""" + test_uuids = [uuid.uuid4(), uuid.uuid4()] + test_uuid_strs = [str(u) for u in test_uuids] + protos = python_values_to_proto_values([test_uuid_strs], ValueType.UUID_LIST) + result = feast_value_type_to_python_type(protos[0]) + assert all(isinstance(r, uuid.UUID) for r in result) + assert result == test_uuids + + def test_uuid_object_list_roundtrip(self): + """uuid.UUID objects in list -> proto -> list of uuid.UUID roundtrip.""" + test_uuids = [uuid.uuid4(), uuid.uuid4(), uuid.uuid4()] + protos = python_values_to_proto_values([test_uuids], ValueType.UUID_LIST) + result = feast_value_type_to_python_type(protos[0]) + assert all(isinstance(r, uuid.UUID) for r in result) + assert result == test_uuids + + def test_time_uuid_object_list_roundtrip(self): + """uuid.UUID objects in TIME_UUID list -> proto -> roundtrip.""" + test_uuids = [uuid.uuid1(), uuid.uuid1()] + protos = python_values_to_proto_values([test_uuids], ValueType.TIME_UUID_LIST) + result = feast_value_type_to_python_type(protos[0]) + assert all(isinstance(r, uuid.UUID) for r in result) + assert result == test_uuids + + def test_uuid_set_roundtrip(self): + """UUID set -> proto -> set of uuid.UUID objects roundtrip.""" + test_uuids = {uuid.uuid4(), uuid.uuid4(), uuid.uuid4()} + protos = python_values_to_proto_values([test_uuids], ValueType.UUID_SET) + + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, set) + assert all(isinstance(r, uuid.UUID) for r in result) + assert result == test_uuids + + def test_time_uuid_set_roundtrip(self): + """TIME_UUID set -> proto -> set of uuid.UUID objects roundtrip.""" + test_uuids = {uuid.uuid1(), uuid.uuid1()} + protos = python_values_to_proto_values([test_uuids], ValueType.TIME_UUID_SET) + + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, set) + assert all(isinstance(r, uuid.UUID) for r in result) + assert result == test_uuids + + def test_uuid_object_set_roundtrip(self): + """uuid.UUID objects in set -> proto -> set of uuid.UUID roundtrip.""" + test_uuids = {uuid.uuid4(), uuid.uuid4()} + protos = python_values_to_proto_values([test_uuids], ValueType.UUID_SET) + + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, set) + assert result == test_uuids + + def test_uuid_set_backward_compat_string_set_val(self): + """UUIDs stored as string_set_val (old format) still work with feature_type hint.""" + from feast.protos.feast.types.Value_pb2 import StringSet + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + test_uuids = {uuid.uuid4(), uuid.uuid4()} + # Simulate old-format proto with string_set_val + proto = ProtoValue(string_set_val=StringSet(val=[str(u) for u in test_uuids])) + + # Without feature_type, returns set of strings + result = feast_value_type_to_python_type(proto) + assert isinstance(result, set) + assert all(isinstance(r, str) for r in result) + + # With feature_type hint, returns set of uuid.UUID + result = feast_value_type_to_python_type(proto, ValueType.UUID_SET) + assert isinstance(result, set) + assert all(isinstance(r, uuid.UUID) for r in result) + assert result == test_uuids + + def test_pg_uuid_type_mapping(self): + """PostgreSQL uuid type maps to ValueType.UUID.""" + assert pg_type_to_feast_value_type("uuid") == ValueType.UUID + assert pg_type_to_feast_value_type("uuid[]") == ValueType.UUID_LIST + + +class TestDecimalTypes: + """Test cases for DECIMAL, DECIMAL_LIST, and DECIMAL_SET value types.""" + + def test_decimal_string_roundtrip(self): + """Decimal string -> proto -> decimal.Decimal object roundtrip.""" + import decimal + + val = decimal.Decimal("3.14159265358979323846") + protos = python_values_to_proto_values([str(val)], ValueType.DECIMAL) + assert protos[0].decimal_val == str(val) + + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, decimal.Decimal) + assert result == val + + def test_decimal_object_serialization(self): + """decimal.Decimal object -> proto serialization (str conversion automatic).""" + import decimal + + val = decimal.Decimal("99.99") + protos = python_values_to_proto_values([val], ValueType.DECIMAL) + assert protos[0].decimal_val == "99.99" + + def test_decimal_preserves_precision(self): + """High-precision decimal values survive the proto round-trip without loss.""" + import decimal + + high_prec = decimal.Decimal("1.23456789012345678901234567890") + protos = python_values_to_proto_values([high_prec], ValueType.DECIMAL) + result = feast_value_type_to_python_type(protos[0]) + assert result == high_prec + + def test_decimal_negative_value(self): + """Negative decimal values round-trip correctly.""" + import decimal + + val = decimal.Decimal("-9876.543210") + protos = python_values_to_proto_values([val], ValueType.DECIMAL) + result = feast_value_type_to_python_type(protos[0]) + assert result == val + + def test_decimal_zero(self): + """Zero decimal value round-trips correctly.""" + import decimal + + val = decimal.Decimal("0") + protos = python_values_to_proto_values([val], ValueType.DECIMAL) + result = feast_value_type_to_python_type(protos[0]) + assert result == val + + def test_decimal_list_roundtrip(self): + """DECIMAL_LIST string list -> proto -> list of decimal.Decimal roundtrip.""" + import decimal + + vals = [decimal.Decimal("1.1"), decimal.Decimal("2.2"), decimal.Decimal("3.3")] + val_strs = [str(v) for v in vals] + protos = python_values_to_proto_values([val_strs], ValueType.DECIMAL_LIST) + result = feast_value_type_to_python_type(protos[0]) + assert all(isinstance(r, decimal.Decimal) for r in result) + assert result == vals + + def test_decimal_object_list_roundtrip(self): + """List of decimal.Decimal objects -> proto -> list of decimal.Decimal roundtrip.""" + import decimal + + vals = [ + decimal.Decimal("10.5"), + decimal.Decimal("20.75"), + decimal.Decimal("30.125"), + ] + protos = python_values_to_proto_values([vals], ValueType.DECIMAL_LIST) + result = feast_value_type_to_python_type(protos[0]) + assert all(isinstance(r, decimal.Decimal) for r in result) + assert result == vals + + def test_decimal_set_roundtrip(self): + """DECIMAL_SET -> proto -> set of decimal.Decimal roundtrip.""" + import decimal + + vals = {decimal.Decimal("1.1"), decimal.Decimal("2.2"), decimal.Decimal("3.3")} + protos = python_values_to_proto_values([vals], ValueType.DECIMAL_SET) + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, set) + assert all(isinstance(r, decimal.Decimal) for r in result) + assert result == vals + + def test_decimal_set_deduplication(self): + """Duplicate decimal values in a set are deduplicated.""" + import decimal + + vals = {decimal.Decimal("5.0"), decimal.Decimal("5.0"), decimal.Decimal("6.0")} + protos = python_values_to_proto_values([vals], ValueType.DECIMAL_SET) + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, set) + assert len(result) == 2 + + def test_decimal_type_inference(self): + """python_type_to_feast_value_type infers DECIMAL from decimal.Decimal values.""" + import decimal + + from feast.type_map import python_type_to_feast_value_type + + assert ( + python_type_to_feast_value_type("price", decimal.Decimal("1.5")) + == ValueType.DECIMAL + ) + + def test_decimal_value_type_string_conversion(self): + """_convert_value_type_str_to_value_type handles DECIMAL strings.""" + from feast.type_map import _convert_value_type_str_to_value_type + + assert _convert_value_type_str_to_value_type("DECIMAL") == ValueType.DECIMAL + assert ( + _convert_value_type_str_to_value_type("DECIMAL_LIST") + == ValueType.DECIMAL_LIST + ) + assert ( + _convert_value_type_str_to_value_type("DECIMAL_SET") + == ValueType.DECIMAL_SET + ) + + def test_decimal_proto_field_name_mapping(self): + """PROTO_VALUE_TO_VALUE_TYPE_MAP and VALUE_TYPE_TO_PROTO_VALUE_MAP include DECIMAL.""" + from feast.type_map import ( + PROTO_VALUE_TO_VALUE_TYPE_MAP, + VALUE_TYPE_TO_PROTO_VALUE_MAP, + ) + + assert PROTO_VALUE_TO_VALUE_TYPE_MAP["decimal_val"] == ValueType.DECIMAL + assert ( + PROTO_VALUE_TO_VALUE_TYPE_MAP["decimal_list_val"] == ValueType.DECIMAL_LIST + ) + assert PROTO_VALUE_TO_VALUE_TYPE_MAP["decimal_set_val"] == ValueType.DECIMAL_SET + assert VALUE_TYPE_TO_PROTO_VALUE_MAP[ValueType.DECIMAL] == "decimal_val" + assert ( + VALUE_TYPE_TO_PROTO_VALUE_MAP[ValueType.DECIMAL_LIST] == "decimal_list_val" + ) + assert VALUE_TYPE_TO_PROTO_VALUE_MAP[ValueType.DECIMAL_SET] == "decimal_set_val" + + def test_decimal_pandas_type(self): + """feast_value_type_to_pandas_type returns 'object' for DECIMAL.""" + from feast.type_map import feast_value_type_to_pandas_type + + assert feast_value_type_to_pandas_type(ValueType.DECIMAL) == "object" + + def test_decimal_pyarrow_type(self): + """feast_value_type_to_pa returns pyarrow.string() for DECIMAL scalar/list/set.""" + import pyarrow + + from feast.type_map import feast_value_type_to_pa + + assert feast_value_type_to_pa(ValueType.DECIMAL) == pyarrow.string() + assert feast_value_type_to_pa(ValueType.DECIMAL_LIST) == pyarrow.list_( + pyarrow.string() + ) + assert feast_value_type_to_pa(ValueType.DECIMAL_SET) == pyarrow.list_( + pyarrow.string() + ) + + def test_decimal_snowflake_udf(self): + """_convert_value_name_to_snowflake_udf maps DECIMAL types to varchar UDFs.""" + from feast.type_map import _convert_value_name_to_snowflake_udf + + project = "myproject" + assert _convert_value_name_to_snowflake_udf("DECIMAL", project) == ( + f"FEAST_{project.upper()}_SNOWFLAKE_VARCHAR_TO_STRING_PROTO" + ) + assert _convert_value_name_to_snowflake_udf("DECIMAL_LIST", project) == ( + f"FEAST_{project.upper()}_SNOWFLAKE_ARRAY_VARCHAR_TO_LIST_STRING_PROTO" + ) + assert _convert_value_name_to_snowflake_udf("DECIMAL_SET", project) == ( + f"FEAST_{project.upper()}_SNOWFLAKE_ARRAY_VARCHAR_TO_LIST_STRING_PROTO" + ) + + +class TestNestedCollectionTypes: + """Tests for nested collection type proto conversion (VALUE_LIST, VALUE_SET).""" + + def test_value_list_proto_roundtrip(self): + """Test python_values_to_proto_values and feast_value_type_to_python_type for VALUE_LIST.""" + values = [[[1, 2, 3], [4, 5]]] + protos = python_values_to_proto_values(values, ValueType.VALUE_LIST) + assert len(protos) == 1 + assert protos[0].WhichOneof("val") == "list_val" + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, list) + assert len(result) == 2 + + def test_value_set_proto_roundtrip(self): + """Test VALUE_SET proto conversion.""" + values = [[["a", "b"], ["c"]]] + protos = python_values_to_proto_values(values, ValueType.VALUE_SET) + assert len(protos) == 1 + assert protos[0].WhichOneof("val") == "set_val" + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, list) + assert len(result) == 2 + + def test_nested_collection_null_handling(self): + """Test that None values are handled correctly.""" + values = [None] + protos = python_values_to_proto_values(values, ValueType.VALUE_LIST) + assert len(protos) == 1 + assert protos[0].WhichOneof("val") is None + + def test_convert_value_type_str_nested(self): + """Test _convert_value_type_str_to_value_type for nested types.""" + assert ( + _convert_value_type_str_to_value_type("VALUE_LIST") == ValueType.VALUE_LIST + ) + assert _convert_value_type_str_to_value_type("VALUE_SET") == ValueType.VALUE_SET + + def test_nested_collection_empty_inner_list(self): + """Test that empty inner collections are handled gracefully.""" + values = [[[], [1, 2]]] + protos = python_values_to_proto_values(values, ValueType.VALUE_LIST) + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, list) + assert len(result) == 2 + # Empty inner list should round-trip as None (empty ProtoValue) + assert result[0] is None + assert result[1] == [1, 2] + + def test_nested_collection_inner_none(self): + """Test that None inner elements are handled.""" + values = [[[1, 2], None, [3]]] + protos = python_values_to_proto_values(values, ValueType.VALUE_LIST) + result = feast_value_type_to_python_type(protos[0]) + assert len(result) == 3 + assert result[0] == [1, 2] + assert result[1] is None + assert result[2] == [3] + + def test_value_list_no_dedup(self): + """Test that VALUE_LIST does NOT deduplicate (Array semantics).""" + values = [[[1, 1, 2], [3, 3]]] + protos = python_values_to_proto_values(values, ValueType.VALUE_LIST) + result = feast_value_type_to_python_type(protos[0]) + assert result[0] == [1, 1, 2] + assert result[1] == [3, 3] + + def test_value_list_proto_roundtrip_values(self): + """Test that VALUE_LIST roundtrip preserves actual inner values.""" + values = [[[1, 2, 3], [4, 5]]] + protos = python_values_to_proto_values(values, ValueType.VALUE_LIST) + result = feast_value_type_to_python_type(protos[0]) + assert result[0] == [1, 2, 3] + assert result[1] == [4, 5] + + def test_value_set_proto_roundtrip_values(self): + """Test that VALUE_SET roundtrip preserves actual inner values.""" + values = [[["a", "b"], ["c"]]] + protos = python_values_to_proto_values(values, ValueType.VALUE_SET) + result = feast_value_type_to_python_type(protos[0]) + assert result[0] == ["a", "b"] + assert result[1] == ["c"] + + def test_multi_value_batch_nested(self): + """Test multiple nested collection values in a single batch.""" + values = [[[1, 2], [3]], [[4], [5, 6]]] + protos = python_values_to_proto_values(values, ValueType.VALUE_LIST) + assert len(protos) == 2 + r0 = feast_value_type_to_python_type(protos[0]) + r1 = feast_value_type_to_python_type(protos[1]) + assert r0 == [[1, 2], [3]] + assert r1 == [[4], [5, 6]] + + def test_feast_value_type_to_pa_nested(self): + """Test feast_value_type_to_pa for nested collection types.""" + for vt in ( + ValueType.VALUE_LIST, + ValueType.VALUE_SET, + ): + pa_type = feast_value_type_to_pa(vt) + assert pa_type == pyarrow.list_(pyarrow.list_(pyarrow.string())) + + def test_pa_to_feast_value_type_nested(self): + """Test pa_to_feast_value_type recognizes nested list PyArrow types.""" + assert ( + pa_to_feast_value_type("list>") + == ValueType.VALUE_LIST + ) + assert ( + pa_to_feast_value_type("list>") + == ValueType.VALUE_LIST + ) + assert ( + pa_to_feast_value_type("list>") + == ValueType.VALUE_LIST + ) + + +class TestEmptyArrayAsNull: + """Regression tests for https://github.com/feast-dev/feast/issues/6255 + Ensure that an empty numpy array in a scalar feature column is treated as + null rather than raising ``ValueError: The truth value of an empty array is + ambiguous``. + """ + + def test_empty_numpy_array_treated_as_null_double(self): + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + result = python_values_to_proto_values( + [np.array([]), 1.0, None], ValueType.DOUBLE + ) + assert result[0] == ProtoValue(), ( + "empty array should produce an empty ProtoValue" + ) + assert result[1].double_val == 1.0 + assert result[2] == ProtoValue(), ( + "None should still produce an empty ProtoValue" + ) + + def test_empty_numpy_array_treated_as_null_int64(self): + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + result = python_values_to_proto_values( + [np.array([]), 42, None], ValueType.INT64 + ) + assert result[0] == ProtoValue(), ( + "empty array should produce an empty ProtoValue" + ) + assert result[1].int64_val == 42 + assert result[2] == ProtoValue() + + def test_empty_numpy_array_treated_as_null_bool(self): + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + result = python_values_to_proto_values( + [np.array([]), True, None], ValueType.BOOL + ) + assert result[0] == ProtoValue(), ( + "empty array should produce an empty ProtoValue" + ) + assert result[1].bool_val is True + assert result[2] == ProtoValue() + + def test_array_with_null_element_treated_as_null(self): + """A non-empty array containing any null element in a scalar column is treated as null.""" + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + result = python_values_to_proto_values( + [np.array([np.nan, 1.0]), 3.0], ValueType.DOUBLE + ) + assert result[0] == ProtoValue(), ( + "array with null element should produce an empty ProtoValue" + ) + assert result[1].double_val == 3.0 + + def test_non_empty_array_without_nulls_is_treated_as_null(self): + """A non-empty numpy array in a scalar column is always treated as null. + + A scalar feature column cannot hold an ndarray value (protobuf would + reject it), so any array-like value – empty or not – is mapped to an + empty ProtoValue() rather than crashing with ValueError. + """ + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + result = python_values_to_proto_values( + [np.array([1.0, 2.0]), 3.0, None], ValueType.DOUBLE + ) + # array-like value in a scalar column → null, not a crash + assert result[0] == ProtoValue(), ( + "non-empty array in scalar column should be null" + ) + assert result[1].double_val == 3.0 + assert result[2] == ProtoValue() + + def test_empty_numpy_array_treated_as_null_unix_timestamp(self): + """Array-like values in a scalar UNIX_TIMESTAMP column must not crash.""" + from datetime import datetime, timezone + + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + ts = datetime(2024, 1, 1, tzinfo=timezone.utc) + result = python_values_to_proto_values( + [np.array([]), ts, None], ValueType.UNIX_TIMESTAMP + ) + assert result[0] == ProtoValue(), ( + "empty array in UNIX_TIMESTAMP scalar column should produce null" + ) + assert result[1].unix_timestamp_val == int(ts.timestamp()) + assert result[2] == ProtoValue() + + def test_non_empty_array_treated_as_null_unix_timestamp(self): + """Non-empty array in a UNIX_TIMESTAMP scalar column should produce null, not crash.""" + from datetime import datetime, timezone + + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + ts = datetime(2024, 6, 15, tzinfo=timezone.utc) + result = python_values_to_proto_values( + [np.array([1, 2, 3]), ts], ValueType.UNIX_TIMESTAMP + ) + assert result[0] == ProtoValue(), ( + "non-empty array in UNIX_TIMESTAMP scalar column should produce null" + ) + assert result[1].unix_timestamp_val == int(ts.timestamp()) + + +class TestValueMapTypes: + """Tests for SCALAR_MAP: maps with non-string keys encoded via ScalarMap proto.""" + + def test_int_key_roundtrip(self): + """Int keys are encoded as int64_key and round-trip back as int.""" + data = {1: "one", 2: "two", 3: "three"} + + protos = python_values_to_proto_values([data], ValueType.SCALAR_MAP) + assert protos[0].WhichOneof("val") == "scalar_map_val" + result = feast_value_type_to_python_type(protos[0]) + + assert result == {1: "one", 2: "two", 3: "three"} + + def test_long_key_roundtrip(self): + """Large int keys (simulating int64/Long) round-trip correctly.""" + data = {1513185957000: {"svacct_id": 123, "amount": 99.5}} + + protos = python_values_to_proto_values([data], ValueType.SCALAR_MAP) + result = feast_value_type_to_python_type(protos[0]) + + assert result[1513185957000]["svacct_id"] == 123 + assert result[1513185957000]["amount"] == 99.5 + + def test_uuid_key_roundtrip(self): + """UUID keys are encoded as uuid_key strings and decoded back to uuid.UUID.""" + key1 = uuid.UUID("12345678-1234-5678-1234-567812345678") + key2 = uuid.UUID("87654321-4321-8765-4321-876543218765") + data = {key1: "first", key2: "second"} + + protos = python_values_to_proto_values([data], ValueType.SCALAR_MAP) + result = feast_value_type_to_python_type(protos[0]) + + assert result[key1] == "first" + assert result[key2] == "second" + + def test_type_inference_non_string_keys_returns_scalar_map(self): + """python_type_to_feast_value_type infers SCALAR_MAP for non-string-keyed dicts.""" + assert python_type_to_feast_value_type("f", {1: "a"}) == ValueType.SCALAR_MAP + assert ( + python_type_to_feast_value_type("f", {uuid.uuid4(): "x"}) + == ValueType.SCALAR_MAP + ) + + def test_type_inference_string_keys_returns_map(self): + """python_type_to_feast_value_type still infers MAP for string-keyed dicts.""" + assert python_type_to_feast_value_type("f", {"k": "v"}) == ValueType.MAP + + def test_type_inference_empty_dict_returns_map(self): + """Empty dict infers MAP (no key to inspect).""" + assert python_type_to_feast_value_type("f", {}) == ValueType.MAP + + def test_none_value_roundtrip(self): + """None values in SCALAR_MAP are preserved as None.""" + data = {10: "present", 20: None} + + protos = python_values_to_proto_values([data], ValueType.SCALAR_MAP) + result = feast_value_type_to_python_type(protos[0]) + + assert result[10] == "present" + assert result[20] is None + + def test_null_value_map(self): + """None SCALAR_MAP encodes to empty ProtoValue and decodes to None.""" + protos = python_values_to_proto_values([None], ValueType.SCALAR_MAP) + assert protos[0] == ProtoValue() + assert feast_value_type_to_python_type(protos[0]) is None + + def test_empty_value_map(self): + """Empty dict encodes and decodes as empty SCALAR_MAP.""" + protos = python_values_to_proto_values([{}], ValueType.SCALAR_MAP) + # empty dict has no non-string key to trigger SCALAR_MAP via inference, + # but explicit type forces the path + result = feast_value_type_to_python_type(protos[0]) + assert isinstance(result, dict) + + def test_multiple_value_maps_in_batch(self): + """Batch of SCALAR_MAP values all encode correctly.""" + batch = [{1: "a", 2: "b"}, {10: "x"}, None] + + protos = python_values_to_proto_values(batch, ValueType.SCALAR_MAP) + assert feast_value_type_to_python_type(protos[0]) == {1: "a", 2: "b"} + assert feast_value_type_to_python_type(protos[1]) == {10: "x"} + assert feast_value_type_to_python_type(protos[2]) is None + + def test_nested_map_value_in_value_map(self): + """SCALAR_MAP can hold nested dicts as values (encoded as MAP vals).""" + inner = {"name": "alice", "score": 42} + data = {100: inner} + + protos = python_values_to_proto_values([data], ValueType.SCALAR_MAP) + result = feast_value_type_to_python_type(protos[0]) + + assert result[100]["name"] == "alice" + assert result[100]["score"] == 42 + + def test_invalid_key_type_raises(self): + """Passing an unsupported key type raises TypeError.""" + + class Unsupported: + pass + + with pytest.raises(TypeError, match="Unsupported key type for SCALAR_MAP"): + python_values_to_proto_values([{Unsupported(): "v"}], ValueType.SCALAR_MAP) + + def test_proto_field_name_in_map(self): + """scalar_map_val maps to ValueType.SCALAR_MAP in PROTO_VALUE_TO_VALUE_TYPE_MAP.""" + from feast.type_map import PROTO_VALUE_TO_VALUE_TYPE_MAP + + assert PROTO_VALUE_TO_VALUE_TYPE_MAP["scalar_map_val"] == ValueType.SCALAR_MAP + + +class TestArrowArrayStringListMaterialization: + """Regression tests for Array(String) columns from Arrow/Athena materialization. + + Arrow/Athena deserializes Array(String) feature columns as numpy.ndarray with + object dtype. Two bugs were triggered: + + 1. ValueError: "The truth value of an empty array is ambiguous" + — when an empty ndarray reached the scalar null-check `elif not pd.isnull(value)`. + + 2. TypeError: "bad argument type for built-in operation" + — when proto_type(val=) was called; protobuf rejects ndarrays. + + Both are fixed by _sanitize_list_value, which converts ndarrays to plain Python + lists and replaces None elements with a type-appropriate zero/empty default + (see _LIST_NONE_DEFAULTS). + """ + + def test_sanitize_list_value_ndarray(self): + """ndarray is converted to a plain Python list.""" + from feast.type_map import _sanitize_list_value + + arr = np.array(["foo", "bar"], dtype=object) + result = _sanitize_list_value(arr, ValueType.STRING_LIST) + assert result == ["foo", "bar"] + assert isinstance(result, list) + + def test_sanitize_list_value_empty_ndarray(self): + """Empty ndarray is converted to an empty Python list.""" + from feast.type_map import _sanitize_list_value + + arr = np.array([], dtype=object) + result = _sanitize_list_value(arr, ValueType.STRING_LIST) + assert result == [] + + def test_sanitize_list_value_ndarray_with_none(self): + """None elements inside a STRING_LIST ndarray are replaced with empty string.""" + from feast.type_map import _sanitize_list_value + + arr = np.array(["foo", None, "baz"], dtype=object) + result = _sanitize_list_value(arr, ValueType.STRING_LIST) + assert result == ["foo", "", "baz"] + + def test_sanitize_list_value_plain_list(self): + """Plain Python lists without None pass through unchanged.""" + from feast.type_map import _sanitize_list_value + + lst = ["foo", "bar"] + result = _sanitize_list_value(lst, ValueType.STRING_LIST) + assert result == ["foo", "bar"] + + def test_sanitize_list_value_plain_list_with_none(self): + """None elements in a STRING_LIST plain list are replaced with empty string.""" + from feast.type_map import _sanitize_list_value + + lst = ["foo", None] + result = _sanitize_list_value(lst, ValueType.STRING_LIST) + assert result == ["foo", ""] + + def test_sanitize_list_value_numeric_none_replaced(self): + """None elements in numeric lists are replaced with a type-appropriate default.""" + from feast.type_map import _sanitize_list_value + + assert _sanitize_list_value([1, None, 2], ValueType.INT32_LIST) == [1, 0, 2] + assert _sanitize_list_value([1, None, 2], ValueType.INT64_LIST) == [1, 0, 2] + assert _sanitize_list_value([1.0, None, 2.0], ValueType.FLOAT_LIST) == [ + 1.0, + 0.0, + 2.0, + ] + assert _sanitize_list_value([1.0, None, 2.0], ValueType.DOUBLE_LIST) == [ + 1.0, + 0.0, + 2.0, + ] + assert _sanitize_list_value([True, None, False], ValueType.BOOL_LIST) == [ + True, + False, + False, + ] + + def test_sanitize_list_value_bytes_none_replaced(self): + """None elements in BYTES_LIST are replaced with b''.""" + from feast.type_map import _sanitize_list_value + + result = _sanitize_list_value([b"x", None], ValueType.BYTES_LIST) + assert result == [b"x", b""] + + def test_sanitize_list_value_scalar_passthrough(self): + """Non-list, non-ndarray values are returned unchanged.""" + from feast.type_map import _sanitize_list_value + + assert _sanitize_list_value("hello", ValueType.STRING_LIST) == "hello" + assert _sanitize_list_value(42, ValueType.INT32_LIST) == 42 + + def test_string_list_from_ndarray(self): + """STRING_LIST column with ndarray values materializes without TypeError.""" + values = [ + np.array(["foo", "bar"], dtype=object), + np.array(["baz"], dtype=object), + ] + protos = python_values_to_proto_values(values, ValueType.STRING_LIST) + assert len(protos) == 2 + assert list(protos[0].string_list_val.val) == ["foo", "bar"] + assert list(protos[1].string_list_val.val) == ["baz"] + + def test_string_list_from_empty_ndarray(self): + """Empty ndarray in a STRING_LIST column must not raise ValueError.""" + values = [ + np.array([], dtype=object), + np.array(["foo"], dtype=object), + ] + protos = python_values_to_proto_values(values, ValueType.STRING_LIST) + assert list(protos[0].string_list_val.val) == [] + assert list(protos[1].string_list_val.val) == ["foo"] + + def test_string_list_from_ndarray_with_none_elements(self): + """None elements inside an ndarray must not cause TypeError in protobuf.""" + values = [ + np.array(["foo", None, "baz"], dtype=object), + ] + protos = python_values_to_proto_values(values, ValueType.STRING_LIST) + # None is replaced with empty string + assert list(protos[0].string_list_val.val) == ["foo", "", "baz"] + + def test_string_list_null_row_produces_empty_proto(self): + """A None row (missing user) produces an empty ProtoValue.""" + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + values = [ + None, + np.array(["foo"], dtype=object), + ] + protos = python_values_to_proto_values(values, ValueType.STRING_LIST) + assert protos[0] == ProtoValue() + assert list(protos[1].string_list_val.val) == ["foo"] + + def test_mixed_batch_simulating_athena_chunk(self): + """Simulate a real Athena chunk: mix of ndarray, empty ndarray, and None rows. + + This is the exact scenario that triggered the TypeError during + string_list_features materialization. + """ + from feast.protos.feast.types.Value_pb2 import Value as ProtoValue + + # tags / labels column from Athena + values = [ + np.array(["foo", "bar"], dtype=object), # normal entity + np.array([], dtype=object), # entity with no values set + None, # missing entity (NULL row) + np.array(["baz"], dtype=object), # normal entity + np.array(["qux", None], dtype=object), # entity with partial null + ] + protos = python_values_to_proto_values(values, ValueType.STRING_LIST) + + assert list(protos[0].string_list_val.val) == ["foo", "bar"] + assert list(protos[1].string_list_val.val) == [] + assert protos[2] == ProtoValue() + assert list(protos[3].string_list_val.val) == ["baz"] + assert list(protos[4].string_list_val.val) == ["qux", ""] + + +class TestZonedTimestamp: + def test_round_trip_preserves_zone(self): + from datetime import datetime + from zoneinfo import ZoneInfo + + dt = datetime(2026, 6, 17, 9, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles")) + protos = python_values_to_proto_values([dt], ValueType.ZONED_TIMESTAMP) + assert protos[0].WhichOneof("val") == "zoned_timestamp_val" + assert protos[0].zoned_timestamp_val.zone == "America/Los_Angeles" + + converted = feast_value_type_to_python_type(protos[0]) + # Same instant AND same wall-clock zone (unlike UNIX_TIMESTAMP, which is UTC). + assert converted == dt + assert str(converted.tzinfo) == "America/Los_Angeles" + assert converted.hour == 9 + + def test_distinct_zones_same_instant_are_not_collapsed(self): + from datetime import datetime, timezone + from zoneinfo import ZoneInfo + + la = datetime(2026, 6, 17, 9, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles")) + utc = datetime(2026, 6, 17, 16, 0, 0, tzinfo=timezone.utc) # same instant + protos = python_values_to_proto_values([la, utc], ValueType.ZONED_TIMESTAMP) + # Same epoch, different zone preserved. + assert ( + protos[0].zoned_timestamp_val.unix_timestamp + == protos[1].zoned_timestamp_val.unix_timestamp + ) + assert protos[0].zoned_timestamp_val.zone == "America/Los_Angeles" + assert protos[1].zoned_timestamp_val.zone == "UTC" + + def test_naive_datetime_treated_as_utc(self): + from datetime import datetime, timezone + + dt = datetime(2026, 6, 17, 12, 0, 0) # naive + protos = python_values_to_proto_values([dt], ValueType.ZONED_TIMESTAMP) + assert protos[0].zoned_timestamp_val.zone == "" + converted = feast_value_type_to_python_type(protos[0]) + assert converted == dt.replace(tzinfo=timezone.utc) + + def test_null_values(self): + protos = python_values_to_proto_values([None], ValueType.ZONED_TIMESTAMP) + assert protos[0] == ProtoValue() + assert feast_value_type_to_python_type(protos[0]) is None + + def test_value_type_round_trips_to_feast_type(self): + from feast.types import ZonedTimestamp, from_value_type + + assert from_value_type(ValueType.ZONED_TIMESTAMP) == ZonedTimestamp + assert ZonedTimestamp.to_value_type() == ValueType.ZONED_TIMESTAMP + + def test_fixed_offset_zone_round_trips(self): + from datetime import datetime, timedelta, timezone + + # A fixed-offset tzinfo has no IANA key, so _zone_name stores it as an + # offset string. Decode must preserve the offset rather than dropping to UTC. + offset = timezone(timedelta(hours=-7)) + dt = datetime(2026, 6, 17, 9, 0, 0, tzinfo=offset) + protos = python_values_to_proto_values([dt], ValueType.ZONED_TIMESTAMP) + + converted = feast_value_type_to_python_type(protos[0]) + # Same instant AND same wall-clock offset preserved. + assert converted == dt + assert converted.utcoffset() == timedelta(hours=-7) + assert converted.hour == 9 + + def test_str_to_value_type_resolves_zoned_timestamp(self): + from feast.type_map import _convert_value_type_str_to_value_type + + # Must not silently fall back to STRING. + assert ( + _convert_value_type_str_to_value_type("ZONED_TIMESTAMP") + == ValueType.ZONED_TIMESTAMP + ) + + def test_not_supported_as_collection_base_type(self): + # ZonedTimestamp has no ZONED_TIMESTAMP_LIST/_SET ValueType, so it must be + # rejected as an Array/Set base type rather than KeyError-ing later. + from feast.types import Array, Set, ZonedTimestamp + + with pytest.raises(ValueError): + Array(ZonedTimestamp) + with pytest.raises(ValueError): + Set(ZonedTimestamp) diff --git a/sdk/python/tests/unit/test_types.py b/sdk/python/tests/unit/test_types.py index af490b4f3a9..aa89a1b4c6c 100644 --- a/sdk/python/tests/unit/test_types.py +++ b/sdk/python/tests/unit/test_types.py @@ -1,6 +1,23 @@ +import pyarrow import pytest -from feast.types import Array, Float32, String, from_value_type +from feast.field import Field +from feast.types import ( + Array, + Bool, + Decimal, + Float32, + Float64, + Int32, + Int64, + Set, + String, + TimeUuid, + Uuid, + from_feast_to_pyarrow_type, + from_feast_type, + from_value_type, +) from feast.value_type import ValueType @@ -23,8 +40,214 @@ def test_array_feast_type(): with pytest.raises(ValueError): _ = Array(Array) + +def test_set_feast_type(): + set_string = Set(String) + assert set_string.to_value_type() == ValueType.STRING_SET + assert from_value_type(set_string.to_value_type()) == set_string + + set_float_32 = Set(Float32) + assert set_float_32.to_value_type() == ValueType.FLOAT_SET + assert from_value_type(set_float_32.to_value_type()) == set_float_32 + with pytest.raises(ValueError): - _ = Array(Array(String)) + _ = Set(Set) + + +def test_nested_array_array(): + """Array(Array(T)) should produce VALUE_LIST.""" + t = Array(Array(String)) + assert t.to_value_type() == ValueType.VALUE_LIST + assert from_feast_type(t) == ValueType.VALUE_LIST + + t2 = Array(Array(Int32)) + assert t2.to_value_type() == ValueType.VALUE_LIST + + +def test_nested_array_set(): + """Array(Set(T)) should produce VALUE_LIST.""" + t = Array(Set(String)) + assert t.to_value_type() == ValueType.VALUE_LIST + assert from_feast_type(t) == ValueType.VALUE_LIST + + +def test_nested_set_array(): + """Set(Array(T)) should produce VALUE_SET.""" + t = Set(Array(String)) + assert t.to_value_type() == ValueType.VALUE_SET + assert from_feast_type(t) == ValueType.VALUE_SET + + +def test_nested_set_set(): + """Set(Set(T)) should produce VALUE_SET.""" + t = Set(Set(String)) + assert t.to_value_type() == ValueType.VALUE_SET + assert from_feast_type(t) == ValueType.VALUE_SET + + +def test_nested_unbounded_depth(): + """Nesting depth should be unbounded.""" + # 3-level + t3 = Array(Array(Array(String))) + assert t3.to_value_type() == ValueType.VALUE_LIST + + t3_mixed = Array(Set(Array(String))) + assert t3_mixed.to_value_type() == ValueType.VALUE_LIST + + t3_set = Set(Array(Array(String))) + assert t3_set.to_value_type() == ValueType.VALUE_SET + + t3_set2 = Set(Set(Set(String))) + assert t3_set2.to_value_type() == ValueType.VALUE_SET + + # 4-level + t4 = Array(Array(Array(Array(Int32)))) + assert t4.to_value_type() == ValueType.VALUE_LIST + + +def test_nested_from_value_type_roundtrip(): + """from_value_type should return a placeholder for nested types.""" + for vt in ( + ValueType.VALUE_LIST, + ValueType.VALUE_SET, + ): + ft = from_value_type(vt) + assert ft.to_value_type() == vt + + +def test_nested_pyarrow_conversion(): + """Nested collection types should convert to pyarrow list(list(...)).""" + # Array(Array(String)) -> list(list(string)) + pa_type = from_feast_to_pyarrow_type(Array(Array(String))) + assert pa_type == pyarrow.list_(pyarrow.list_(pyarrow.string())) + + # Array(Set(Int64)) -> list(list(int64)) + pa_type = from_feast_to_pyarrow_type(Array(Set(Int64))) + assert pa_type == pyarrow.list_(pyarrow.list_(pyarrow.int64())) + + # Set(Array(Float64)) -> list(list(float64)) + pa_type = from_feast_to_pyarrow_type(Set(Array(Float64))) + assert pa_type == pyarrow.list_(pyarrow.list_(pyarrow.float64())) + + # Set(Set(Bool)) -> list(list(bool)) + pa_type = from_feast_to_pyarrow_type(Set(Set(Bool))) + assert pa_type == pyarrow.list_(pyarrow.list_(pyarrow.bool_())) + + # 3-level: Array(Array(Array(Int32))) -> list(list(list(int32))) + pa_type = from_feast_to_pyarrow_type(Array(Array(Array(Int32)))) + assert pa_type == pyarrow.list_(pyarrow.list_(pyarrow.list_(pyarrow.int32()))) + + +def test_nested_field_roundtrip(): + """Field with nested collection type should survive to_proto -> from_proto.""" + test_cases = [ + ("aa", Array(Array(String))), + ("as_field", Array(Set(Int32))), + ("sa", Set(Array(Float64))), + ("ss", Set(Set(Bool))), + # 3-level nesting + ("aaa", Array(Array(Array(Int32)))), + ("asa", Array(Set(Array(String)))), + # 4-level nesting + ("aaaa", Array(Array(Array(Array(Float64))))), + ] + for name, dtype in test_cases: + field = Field(name=name, dtype=dtype, tags={"user_tag": "value"}) + proto = field.to_proto() + restored = Field.from_proto(proto) + assert restored.name == name, f"Name mismatch for {dtype}" + assert restored.dtype.to_value_type() == dtype.to_value_type(), ( + f"dtype mismatch for {name}: {restored.dtype} vs {dtype}" + ) + # Verify inner type is preserved (not just ValueType equality) + assert str(restored.dtype) == str(dtype), ( + f"Inner type lost for {name}: got {restored.dtype}, expected {dtype}" + ) + assert restored.tags == {"user_tag": "value"}, ( + f"Tags should not contain internal tags for {name}" + ) + + +def test_uuid_feast_type(): + assert Uuid.to_value_type() == ValueType.UUID + assert from_value_type(ValueType.UUID) == Uuid + assert TimeUuid.to_value_type() == ValueType.TIME_UUID + assert from_value_type(ValueType.TIME_UUID) == TimeUuid + + +def test_uuid_array_feast_type(): + array_uuid = Array(Uuid) + assert array_uuid.to_value_type() == ValueType.UUID_LIST + assert from_value_type(array_uuid.to_value_type()) == array_uuid + + array_time_uuid = Array(TimeUuid) + assert array_time_uuid.to_value_type() == ValueType.TIME_UUID_LIST + assert from_value_type(array_time_uuid.to_value_type()) == array_time_uuid + + +def test_uuid_set_feast_type(): + set_uuid = Set(Uuid) + assert set_uuid.to_value_type() == ValueType.UUID_SET + assert from_value_type(set_uuid.to_value_type()) == set_uuid + + set_time_uuid = Set(TimeUuid) + assert set_time_uuid.to_value_type() == ValueType.TIME_UUID_SET + assert from_value_type(set_time_uuid.to_value_type()) == set_time_uuid + + +def test_decimal_feast_type(): + assert Decimal.to_value_type() == ValueType.DECIMAL + assert from_value_type(ValueType.DECIMAL) == Decimal + + +def test_decimal_array_feast_type(): + array_decimal = Array(Decimal) + assert array_decimal.to_value_type() == ValueType.DECIMAL_LIST + assert from_value_type(array_decimal.to_value_type()) == array_decimal + + +def test_decimal_set_feast_type(): + set_decimal = Set(Decimal) + assert set_decimal.to_value_type() == ValueType.DECIMAL_SET + assert from_value_type(set_decimal.to_value_type()) == set_decimal + + +def test_feast_type_str_roundtrip(): + """_feast_type_to_str and _str_to_feast_type should roundtrip for nested types.""" + from feast.field import _feast_type_to_str, _str_to_feast_type + + test_cases = [ + Array(Array(String)), + Array(Array(Int32)), + Array(Array(Float64)), + Array(Set(Int64)), + Array(Set(Bool)), + Set(Array(String)), + Set(Array(Float32)), + Set(Set(Int32)), + Set(Set(Float64)), + # 3+ level nesting + Array(Array(Array(String))), + Array(Set(Array(Int32))), + Set(Set(Set(Float64))), + ] + for dtype in test_cases: + s = _feast_type_to_str(dtype) + restored = _str_to_feast_type(s) + assert str(restored) == str(dtype), ( + f"Roundtrip failed: {dtype} -> '{s}' -> {restored}" + ) + + +def test_str_to_feast_type_invalid(): + """_str_to_feast_type should raise ValueError on unrecognized type names.""" + from feast.field import _str_to_feast_type + + with pytest.raises(ValueError, match="Unknown FeastType"): + _str_to_feast_type("INVALID_TYPE") + + with pytest.raises(ValueError, match="Unknown FeastType"): + _str_to_feast_type("Strig") def test_all_value_types(): diff --git a/sdk/python/tests/unit/test_ui_server.py b/sdk/python/tests/unit/test_ui_server.py index c5a85a85382..8bf01ac5f24 100644 --- a/sdk/python/tests/unit/test_ui_server.py +++ b/sdk/python/tests/unit/test_ui_server.py @@ -15,7 +15,6 @@ EXPECTED_SUCCESS_STATUS = 200 EXPECTED_ERROR_STATUS = 503 TEST_PROJECT_NAME = "test_project" -REGISTRY_TTL_SECS = 60 def _create_mock_ui_files(temp_dir): @@ -23,12 +22,10 @@ def _create_mock_ui_files(temp_dir): ui_dir = os.path.join(temp_dir, "ui", "build") os.makedirs(ui_dir, exist_ok=True) - # Create projects-list.json file projects_file = os.path.join(ui_dir, "projects-list.json") with open(projects_file, "w") as f: json.dump({"projects": []}, f) - # Create index.html file index_file = os.path.join(ui_dir, "index.html") with open(index_file, "w") as f: f.write("Test UI") @@ -36,20 +33,13 @@ def _create_mock_ui_files(temp_dir): @contextlib.contextmanager def _setup_importlib_mocks(temp_dir): - """Helper function to setup importlib resource mocks. - - This function mocks the importlib_resources functionality used by the UI server - to serve static files. It creates a proper context manager that returns the - temporary directory path when used with importlib_resources.as_file(). - """ + """Helper function to setup importlib resource mocks.""" mock_path = Path(temp_dir) - # Create a proper context manager mock mock_context_manager = MagicMock() mock_context_manager.__enter__.return_value = mock_path mock_context_manager.__exit__.return_value = None - # Mock the files() method to return a mock that supports division mock_file_ref = MagicMock() mock_file_ref.__truediv__.return_value = MagicMock() @@ -63,235 +53,263 @@ def _setup_importlib_mocks(temp_dir): yield mock_files, mock_as_file +def _make_project_mock(name, description=""): + """Create a mock project object with the given name and description.""" + proj = MagicMock() + proj.name = name + proj.description = description + return proj + + @pytest.fixture def mock_feature_store(): """Fixture for creating a mock feature store""" mock_store = MagicMock() - mock_store.refresh_registry = MagicMock() return mock_store @pytest.fixture def ui_app_with_registry(mock_feature_store): - """Fixture for UI app with valid registry data. - - Creates a UI app instance with a properly configured feature store - that has valid registry data available for testing endpoints that - require registry access. - """ + """Fixture for UI app with valid registry data.""" mock_registry = MagicMock() - mock_proto = MagicMock() - mock_proto.SerializeToString.return_value = b"mock_proto_data" - mock_registry.proto.return_value = mock_proto + mock_registry.list_projects.return_value = [] mock_feature_store.registry = mock_registry with tempfile.TemporaryDirectory() as temp_dir: _create_mock_ui_files(temp_dir) with _setup_importlib_mocks(temp_dir): - app = get_app(mock_feature_store, TEST_PROJECT_NAME, REGISTRY_TTL_SECS) + app = get_app(mock_feature_store, TEST_PROJECT_NAME) yield app @pytest.fixture def ui_app_without_registry(mock_feature_store): - """Fixture for UI app with None registry data. - - Creates a UI app instance with a feature store that has no registry - data available, used for testing error conditions and service - unavailable responses. - """ + """Fixture for UI app where registry raises on list_projects.""" mock_registry = MagicMock() - mock_registry.proto.return_value = None + mock_registry.list_projects.side_effect = Exception("registry unavailable") mock_feature_store.registry = mock_registry with tempfile.TemporaryDirectory() as temp_dir: _create_mock_ui_files(temp_dir) with _setup_importlib_mocks(temp_dir): - app = get_app(mock_feature_store, TEST_PROJECT_NAME, REGISTRY_TTL_SECS) + app = get_app(mock_feature_store, TEST_PROJECT_NAME) yield app def test_ui_server_health_endpoint(ui_app_with_registry): - """Test the UI server health endpoint returns 200 when registry is available. - - This test verifies that the /health endpoint correctly returns HTTP 200 - when the feature store registry is properly initialized and contains data. - """ + """Health endpoint returns 200 when registry is available.""" client = TestClient(ui_app_with_registry) response = client.get("/health") assertpy.assert_that(response.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS) -def test_ui_server_health_endpoint_with_none_registry(ui_app_without_registry): - """Test the UI server health endpoint returns 503 when registry is None. - - This test verifies that the /health endpoint correctly returns HTTP 503 - (Service Unavailable) when the feature store registry is not available - or contains no data. - """ +def test_ui_server_health_endpoint_with_unavailable_registry(ui_app_without_registry): + """Health endpoint returns 503 when registry is unavailable.""" client = TestClient(ui_app_without_registry) response = client.get("/health") assertpy.assert_that(response.status_code).is_equal_to(EXPECTED_ERROR_STATUS) -def test_registry_endpoint_with_valid_data(ui_app_with_registry): - """Test the registry endpoint returns valid data with correct content type. +@pytest.mark.parametrize( + "registry_available,expected_status", + [(True, EXPECTED_SUCCESS_STATUS), (False, EXPECTED_ERROR_STATUS)], +) +def test_health_endpoint_status( + registry_available, expected_status, mock_feature_store +): + """Health endpoint returns correct status based on registry availability.""" + mock_registry = MagicMock() + if registry_available: + mock_registry.list_projects.return_value = [] + else: + mock_registry.list_projects.side_effect = Exception("unavailable") + mock_feature_store.registry = mock_registry + + with tempfile.TemporaryDirectory() as temp_dir: + _create_mock_ui_files(temp_dir) + + with _setup_importlib_mocks(temp_dir): + app = get_app(mock_feature_store, TEST_PROJECT_NAME) + client = TestClient(app) + response = client.get("/health") + assertpy.assert_that(response.status_code).is_equal_to(expected_status) + - This test verifies that the /registry endpoint correctly returns HTTP 200 - with the proper content-type header when registry data is available. - """ +def test_catch_all_route(ui_app_with_registry): + """Test the catch-all route for React router paths.""" client = TestClient(ui_app_with_registry) - response = client.get("/registry") - assertpy.assert_that(response.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS) - assertpy.assert_that(response.headers["content-type"]).is_equal_to( - "application/octet-stream" - ) + with pytest.raises(Exception): + client.get("/p/some/react/path") -def test_registry_endpoint_with_none_data(ui_app_without_registry): - """Test the registry endpoint returns 503 when registry data is None. - This test verifies that the /registry endpoint correctly returns HTTP 503 - (Service Unavailable) when no registry data is available. - """ - client = TestClient(ui_app_without_registry) - response = client.get("/registry") - assertpy.assert_that(response.status_code).is_equal_to(EXPECTED_ERROR_STATUS) +# ---------- projects-list.json tests ---------- -def test_save_document_endpoint_success(ui_app_with_registry): - """Test the save document endpoint successfully saves data to a labels file. +def _read_projects_list(app): + """Fetch the projects-list.json from the dynamic endpoint.""" + client = TestClient(app) + resp = client.get("/projects-list.json") + assertpy.assert_that(resp.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS) + return resp.json() - This test verifies that the /save-document endpoint correctly processes - a valid request, creates a labels file, and returns success confirmation. - """ - client = TestClient(ui_app_with_registry) - # Create a temporary file in the current working directory for testing - with tempfile.NamedTemporaryFile( - mode="w", suffix=".py", delete=False, dir=os.getcwd() - ) as f: - test_file_path = f.name - f.write("# Test file content") - - try: - request_data = { - "file_path": test_file_path, - "data": {"test": "data", "key": "value"}, - } - - response = client.post("/save-document", json=request_data) - assertpy.assert_that(response.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS) - - response_data = response.json() - assertpy.assert_that(response_data["success"]).is_true() - assertpy.assert_that(response_data).contains_key("saved_to") - - # Verify the file was created - labels_file = response_data["saved_to"] - assertpy.assert_that(os.path.exists(labels_file)).is_true() - - with open(labels_file, "r") as f: - saved_data = json.load(f) - assertpy.assert_that(saved_data).is_equal_to(request_data["data"]) - - finally: - # Cleanup - if os.path.exists(test_file_path): - os.unlink(test_file_path) - labels_file = test_file_path.replace(".py", "-labels.json") - if os.path.exists(labels_file): - os.unlink(labels_file) - - -def test_save_document_endpoint_invalid_path(ui_app_with_registry): - """Test the save document endpoint returns error for invalid file path. - - This test verifies that the /save-document endpoint correctly rejects - file paths that are outside the current working directory for security. - """ - client = TestClient(ui_app_with_registry) +def test_projects_list_registry_path(mock_feature_store): + """projects-list.json uses /api/v1 as registryPath.""" + mock_registry = MagicMock() + mock_registry.list_projects.return_value = [] + mock_feature_store.registry = mock_registry - request_data = { - "file_path": "/invalid/absolute/path/outside/workspace.py", - "data": {"test": "data"}, - } + with tempfile.TemporaryDirectory() as temp_dir: + _create_mock_ui_files(temp_dir) - response = client.post("/save-document", json=request_data) - assertpy.assert_that(response.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS) + with _setup_importlib_mocks(temp_dir): + app = get_app(mock_feature_store, TEST_PROJECT_NAME) - response_data = response.json() - assertpy.assert_that(response_data).contains_key("error") - assertpy.assert_that(response_data["error"]).contains("Invalid file path") + data = _read_projects_list(app) + assertpy.assert_that(data["projects"][0]["registryPath"]).is_equal_to("/api/v1") -def test_save_document_endpoint_exception_handling(ui_app_with_registry): - """Test the save document endpoint handles exceptions gracefully. +def test_projects_list_with_root_path(mock_feature_store): + """root_path prefix is included in registryPath.""" + mock_registry = MagicMock() + mock_registry.list_projects.return_value = [] + mock_feature_store.registry = mock_registry - This test verifies that the /save-document endpoint properly catches - and returns error responses when exceptions occur during processing. - """ - client = TestClient(ui_app_with_registry) + with tempfile.TemporaryDirectory() as temp_dir: + _create_mock_ui_files(temp_dir) - # Test with a file path outside the current working directory (will cause an exception) - request_data = { - "file_path": "/invalid/absolute/path/outside/workspace.py", - "data": {"test": "data"}, - } + with _setup_importlib_mocks(temp_dir): + app = get_app( + mock_feature_store, + TEST_PROJECT_NAME, + root_path="/feast", + ) - response = client.post("/save-document", json=request_data) - assertpy.assert_that(response.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS) + data = _read_projects_list(app) + assertpy.assert_that(data["projects"][0]["registryPath"]).is_equal_to( + "/feast/api/v1" + ) - response_data = response.json() - assertpy.assert_that(response_data).contains_key("error") - assertpy.assert_that(response_data["error"]).contains("Invalid file path") +def test_projects_list_multiple_projects(mock_feature_store): + """Multiple projects get an 'All Projects' entry prepended.""" + mock_registry = MagicMock() + mock_registry.list_projects.return_value = [ + _make_project_mock("project_alpha", "Alpha project"), + _make_project_mock("project_beta", "Beta project"), + ] + mock_feature_store.registry = mock_registry -@pytest.mark.parametrize( - "registry_available,expected_status", - [(True, EXPECTED_SUCCESS_STATUS), (False, EXPECTED_ERROR_STATUS)], -) -def test_health_endpoint_status( - registry_available, expected_status, mock_feature_store -): - """Test the health endpoint returns correct status based on registry availability. + with tempfile.TemporaryDirectory() as temp_dir: + _create_mock_ui_files(temp_dir) - This parametrized test verifies that the /health endpoint returns the - appropriate HTTP status code based on whether registry data is available. - """ - if registry_available: - mock_registry = MagicMock() - mock_proto = MagicMock() - mock_proto.SerializeToString.return_value = b"mock_proto_data" - mock_registry.proto.return_value = mock_proto - mock_feature_store.registry = mock_registry - else: - mock_registry = MagicMock() - mock_registry.proto.return_value = None - mock_feature_store.registry = mock_registry + with _setup_importlib_mocks(temp_dir): + app = get_app(mock_feature_store, TEST_PROJECT_NAME) + + data = _read_projects_list(app) + assertpy.assert_that(len(data["projects"])).is_equal_to(3) + assertpy.assert_that(data["projects"][0]["id"]).is_equal_to("all") + assertpy.assert_that(data["projects"][1]["id"]).is_equal_to("project_alpha") + assertpy.assert_that(data["projects"][2]["id"]).is_equal_to("project_beta") + + +def test_projects_list_fallback_on_empty(mock_feature_store): + """When list_projects returns empty, fallback project is used.""" + mock_registry = MagicMock() + mock_registry.list_projects.return_value = [] + mock_feature_store.registry = mock_registry with tempfile.TemporaryDirectory() as temp_dir: _create_mock_ui_files(temp_dir) with _setup_importlib_mocks(temp_dir): - app = get_app(mock_feature_store, TEST_PROJECT_NAME, REGISTRY_TTL_SECS) - client = TestClient(app) - response = client.get("/health") - assertpy.assert_that(response.status_code).is_equal_to(expected_status) + app = get_app(mock_feature_store, TEST_PROJECT_NAME) + data = _read_projects_list(app) + assertpy.assert_that(len(data["projects"])).is_equal_to(1) + assertpy.assert_that(data["projects"][0]["id"]).is_equal_to(TEST_PROJECT_NAME) -def test_catch_all_route(ui_app_with_registry): - """Test the catch-all route for React router paths. - This test reveals a bug in the original UI server code where ui_dir - is not in scope for the catch_all function. The ui_dir variable is defined - inside the importlib_resources context manager but used outside of it. - This causes a NameError when the route is accessed. - """ - client = TestClient(ui_app_with_registry) +def test_projects_list_fallback_on_exception(mock_feature_store): + """When list_projects raises, fallback project is used.""" + mock_registry = MagicMock() + mock_registry.list_projects.side_effect = Exception("not implemented") + mock_feature_store.registry = mock_registry - # The route will fail due to the scope issue with ui_dir - with pytest.raises(Exception): # Expecting NameError or FileNotFoundError - client.get("/p/some/react/path") + with tempfile.TemporaryDirectory() as temp_dir: + _create_mock_ui_files(temp_dir) + + with _setup_importlib_mocks(temp_dir): + app = get_app(mock_feature_store, TEST_PROJECT_NAME) + + data = _read_projects_list(app) + assertpy.assert_that(len(data["projects"])).is_equal_to(1) + assertpy.assert_that(data["projects"][0]["id"]).is_equal_to(TEST_PROJECT_NAME) + + +def test_projects_list_dynamic_refresh(mock_feature_store): + """New projects appear without restarting the server.""" + mock_registry = MagicMock() + mock_registry.list_projects.return_value = [ + _make_project_mock("picked_elk"), + ] + mock_feature_store.registry = mock_registry + + with tempfile.TemporaryDirectory() as temp_dir: + _create_mock_ui_files(temp_dir) + + with _setup_importlib_mocks(temp_dir): + app = get_app(mock_feature_store, TEST_PROJECT_NAME) + + client = TestClient(app) + + data = client.get("/projects-list.json").json() + assertpy.assert_that(len(data["projects"])).is_equal_to(1) + assertpy.assert_that(data["projects"][0]["id"]).is_equal_to("picked_elk") + + mock_registry.list_projects.return_value = [ + _make_project_mock("picked_elk"), + _make_project_mock("picked_elk2"), + ] + + data = client.get("/projects-list.json").json() + assertpy.assert_that(len(data["projects"])).is_equal_to(3) + assertpy.assert_that(data["projects"][0]["id"]).is_equal_to("all") + assertpy.assert_that(data["projects"][1]["id"]).is_equal_to("picked_elk") + assertpy.assert_that(data["projects"][2]["id"]).is_equal_to("picked_elk2") + + +def test_registry_refresh_endpoint(mock_feature_store): + """POST /api/v1/registry/refresh calls store.refresh_registry().""" + mock_feature_store.refresh_registry = MagicMock() + + with tempfile.TemporaryDirectory() as temp_dir: + _create_mock_ui_files(temp_dir) + + with _setup_importlib_mocks(temp_dir): + app = get_app(mock_feature_store, TEST_PROJECT_NAME) + + client = TestClient(app) + resp = client.post("/api/v1/registry/refresh") + assertpy.assert_that(resp.status_code).is_equal_to(EXPECTED_SUCCESS_STATUS) + mock_feature_store.refresh_registry.assert_called_once() + + +def test_registry_refresh_endpoint_error(mock_feature_store): + """POST /api/v1/registry/refresh returns 500 when refresh_registry raises.""" + mock_feature_store.refresh_registry = MagicMock( + side_effect=Exception("registry unreachable") + ) + + with tempfile.TemporaryDirectory() as temp_dir: + _create_mock_ui_files(temp_dir) + + with _setup_importlib_mocks(temp_dir): + app = get_app(mock_feature_store, TEST_PROJECT_NAME) + + client = TestClient(app, raise_server_exceptions=False) + resp = client.post("/api/v1/registry/refresh") + assertpy.assert_that(resp.status_code).is_equal_to(500) diff --git a/sdk/python/tests/unit/test_utils.py b/sdk/python/tests/unit/test_utils.py new file mode 100644 index 00000000000..7c83eb6ec8a --- /dev/null +++ b/sdk/python/tests/unit/test_utils.py @@ -0,0 +1,442 @@ +""" +Tests for feast.utils module. + +These unit tests cover the _populate_response_from_feature_data function +which converts raw online_read rows into protobuf FeatureVectors and +populates the GetOnlineFeaturesResponse. +""" + +from datetime import datetime, timezone +from unittest.mock import MagicMock + +from feast.protos.feast.serving.ServingService_pb2 import ( + FieldStatus, + GetOnlineFeaturesResponse, +) +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.utils import _populate_response_from_feature_data + + +def _make_table(name="test_fv"): + """Create a minimal mock FeatureView for testing.""" + table = MagicMock() + table.projection.name_to_use.return_value = name + table.projection.name_alias = None + table.projection.name = name + return table + + +class TestPopulateResponseFromFeatureData: + """Tests for _populate_response_from_feature_data function.""" + + def test_basic_single_feature(self): + """Test basic conversion with single feature and single entity.""" + timestamp = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + value = ValueProto(float_val=1.5) + + read_rows = [(timestamp, {"feature_1": value})] + indexes = ([0],) + response = GetOnlineFeaturesResponse(results=[]) + + _populate_response_from_feature_data( + requested_features=["feature_1"], + read_rows=read_rows, + indexes=indexes, + online_features_response=response, + full_feature_names=False, + table=_make_table(), + output_len=1, + ) + + assert len(response.results) == 1 + assert response.results[0].values[0] == value + assert response.results[0].statuses[0] == FieldStatus.PRESENT + assert response.results[0].event_timestamps[0].seconds == int( + timestamp.timestamp() + ) + assert list(response.metadata.feature_names.val) == ["feature_1"] + + def test_multiple_features_same_entity(self): + """Test multiple features from the same row.""" + timestamp = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + v1 = ValueProto(float_val=1.0) + v2 = ValueProto(float_val=2.0) + + read_rows = [(timestamp, {"feature_1": v1, "feature_2": v2})] + indexes = ([0],) + response = GetOnlineFeaturesResponse(results=[]) + + _populate_response_from_feature_data( + requested_features=["feature_1", "feature_2"], + read_rows=read_rows, + indexes=indexes, + online_features_response=response, + full_feature_names=False, + table=_make_table(), + output_len=1, + ) + + assert len(response.results) == 2 + assert response.results[0].values[0] == v1 + assert response.results[1].values[0] == v2 + ts1 = response.results[0].event_timestamps[0].seconds + ts2 = response.results[1].event_timestamps[0].seconds + assert ts1 == ts2 == int(timestamp.timestamp()) + + def test_multiple_entities_deduplication(self): + """Test that duplicate entity rows are correctly mapped via indexes.""" + ts = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + val = ValueProto(float_val=42.0) + + read_rows = [(ts, {"feature_1": val})] + indexes = ([0, 1, 2],) # One unique row maps to 3 output positions + response = GetOnlineFeaturesResponse(results=[]) + + _populate_response_from_feature_data( + requested_features=["feature_1"], + read_rows=read_rows, + indexes=indexes, + online_features_response=response, + full_feature_names=False, + table=_make_table(), + output_len=3, + ) + + assert len(response.results[0].values) == 3 + for i in range(3): + assert response.results[0].values[i] == val + assert response.results[0].statuses[i] == FieldStatus.PRESENT + + def test_null_timestamp_handling(self): + """Test that null timestamps produce empty Timestamp proto.""" + read_rows = [ + (None, {"feature_1": ValueProto(float_val=1.0)}), + ( + datetime(2024, 1, 1, tzinfo=timezone.utc), + {"feature_1": ValueProto(float_val=2.0)}, + ), + ] + indexes = ([0],), ([1],) + indexes = ([0], [1]) + response = GetOnlineFeaturesResponse(results=[]) + + _populate_response_from_feature_data( + requested_features=["feature_1"], + read_rows=read_rows, + indexes=indexes, + online_features_response=response, + full_feature_names=False, + table=_make_table(), + output_len=2, + ) + + ts_list = response.results[0].event_timestamps + assert ts_list[0].seconds == 0 # Null timestamp -> empty proto + assert ts_list[1].seconds != 0 # Valid timestamp + + def test_missing_feature_data(self): + """Test handling of missing feature data (None row).""" + ts = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + + read_rows = [ + (ts, {"feature_1": ValueProto(float_val=1.0)}), + (ts, None), + ] + indexes = ([0], [1]) + response = GetOnlineFeaturesResponse(results=[]) + + _populate_response_from_feature_data( + requested_features=["feature_1"], + read_rows=read_rows, + indexes=indexes, + online_features_response=response, + full_feature_names=False, + table=_make_table(), + output_len=2, + ) + + assert response.results[0].statuses[0] == FieldStatus.PRESENT + assert response.results[0].statuses[1] == FieldStatus.NOT_FOUND + + def test_feature_not_in_row(self): + """Test handling when requested feature is not in the row's data.""" + ts = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + + read_rows = [(ts, {"feature_1": ValueProto(float_val=1.0)})] + indexes = ([0],) + response = GetOnlineFeaturesResponse(results=[]) + + _populate_response_from_feature_data( + requested_features=["feature_1", "feature_2"], + read_rows=read_rows, + indexes=indexes, + online_features_response=response, + full_feature_names=False, + table=_make_table(), + output_len=1, + ) + + assert len(response.results) == 2 + assert response.results[0].statuses[0] == FieldStatus.PRESENT + assert response.results[1].statuses[0] == FieldStatus.NOT_FOUND + + def test_empty_inputs(self): + """Test handling of empty inputs.""" + response = GetOnlineFeaturesResponse(results=[]) + _populate_response_from_feature_data( + requested_features=["feature_1"], + read_rows=[], + indexes=(), + online_features_response=response, + full_feature_names=False, + table=_make_table(), + output_len=0, + ) + assert len(response.results) == 1 + assert len(response.results[0].values) == 0 + + response2 = GetOnlineFeaturesResponse(results=[]) + ts = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + _populate_response_from_feature_data( + requested_features=[], + read_rows=[(ts, {"f": ValueProto()})], + indexes=([0],), + online_features_response=response2, + full_feature_names=False, + table=_make_table(), + output_len=1, + ) + assert len(response2.results) == 0 + + def test_full_feature_names(self): + """Test that full_feature_names prefixes feature names with table name.""" + ts = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + read_rows = [(ts, {"feature_1": ValueProto(float_val=1.0)})] + response = GetOnlineFeaturesResponse(results=[]) + + _populate_response_from_feature_data( + requested_features=["feature_1"], + read_rows=read_rows, + indexes=([0],), + online_features_response=response, + full_feature_names=True, + table=_make_table("my_fv"), + output_len=1, + ) + + assert list(response.metadata.feature_names.val) == ["my_fv__feature_1"] + + def test_large_scale_correctness(self): + """Test correctness with large number of features and entities. + + This test verifies that the fused implementation produces correct + results at scale (50 features x 500 entities = 25,000 data points). + """ + timestamp = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + num_entities = 500 + num_features = 50 + + feature_data = { + f"feature_{i}": ValueProto(float_val=float(i)) for i in range(num_features) + } + read_rows = [(timestamp, feature_data.copy()) for _ in range(num_entities)] + requested_features = [f"feature_{i}" for i in range(num_features)] + indexes = tuple([i] for i in range(num_entities)) + response = GetOnlineFeaturesResponse(results=[]) + + _populate_response_from_feature_data( + requested_features=requested_features, + read_rows=read_rows, + indexes=indexes, + online_features_response=response, + full_feature_names=False, + table=_make_table(), + output_len=num_entities, + ) + + assert len(response.results) == num_features + expected_ts = int(timestamp.timestamp()) + for feature_idx in range(num_features): + fv = response.results[feature_idx] + assert len(fv.values) == num_entities + assert len(fv.statuses) == num_entities + assert len(fv.event_timestamps) == num_entities + + for ts in fv.event_timestamps: + assert ts.seconds == expected_ts + for status in fv.statuses: + assert status == FieldStatus.PRESENT + + +class TestGetFeatureViewsToUseSharedSource: + def _build_store(self, data_dir): + import os + from datetime import timedelta + + import pandas as pd + + from feast import Entity, FeatureStore, FeatureView, Field, FileSource + from feast.infra.online_stores.sqlite import SqliteOnlineStoreConfig + from feast.on_demand_feature_view import on_demand_feature_view + from feast.repo_config import RepoConfig + from feast.types import Float64 + + driver = Entity(name="driver", join_keys=["driver_id"]) + + df = pd.DataFrame( + { + "driver_id": [1001, 1002], + "event_timestamp": [ + datetime(2024, 1, 1, 10, 0, 0, tzinfo=timezone.utc), + datetime(2024, 1, 1, 11, 0, 0, tzinfo=timezone.utc), + ], + "created": [ + datetime(2024, 1, 1, 10, 0, 0, tzinfo=timezone.utc), + datetime(2024, 1, 1, 11, 0, 0, tzinfo=timezone.utc), + ], + "a": [1.0, 2.0], + "b": [10.0, 20.0], + } + ) + src_path = os.path.join(data_dir, "src.parquet") + df.to_parquet(path=src_path, allow_truncated_timestamps=True) + + src = FileSource( + name="src_source", + path=src_path, + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + + src_fv = FeatureView( + name="src_fv", + entities=[driver], + ttl=timedelta(days=0), + schema=[ + Field(name="a", dtype=Float64), + Field(name="b", dtype=Float64), + ], + online=True, + source=src, + ) + + @on_demand_feature_view( + sources=[src_fv[["a"]]], + schema=[Field(name="a_out", dtype=Float64)], + mode="pandas", + ) + def odfv_a(inputs): + return pd.DataFrame({"a_out": inputs["a"] + 1}) + + @on_demand_feature_view( + sources=[src_fv[["b"]]], + schema=[Field(name="b_out", dtype=Float64)], + mode="pandas", + ) + def odfv_b(inputs): + return pd.DataFrame({"b_out": inputs["b"] + 100}) + + store = FeatureStore( + config=RepoConfig( + project="test_shared_source", + registry=os.path.join(data_dir, "registry.db"), + provider="local", + entity_key_serialization_version=3, + online_store=SqliteOnlineStoreConfig( + path=os.path.join(data_dir, "online.db") + ), + ) + ) + store.apply([driver, src, src_fv, odfv_a, odfv_b]) + return store + + def test_shared_source_projections_are_merged(self): + import tempfile + + from feast.utils import _get_feature_views_to_use + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as data_dir: + store = self._build_store(data_dir) + + fvs, odfvs = _get_feature_views_to_use( + store.registry, + store.project, + ["odfv_a:a_out", "odfv_b:b_out"], + ) + + src_entries = [fv for fv in fvs if fv.projection.name_to_use() == "src_fv"] + assert len(src_entries) == 1 + projected = sorted( + feature.name for feature in src_entries[0].projection.features + ) + assert projected == ["a", "b"] + assert {odfv.name for odfv in odfvs} == {"odfv_a", "odfv_b"} + + def test_regular_fv_and_odfv_source_are_merged(self): + # Regression: a regular FeatureView requested alongside an ODFV that + # sources it must resolve to a single, merged src_fv entry. Previously + # the regular FV was appended but not indexed in fvs_by_projection_key, + # so the ODFV source appended a second partial src_fv projection and a + # later lookup raised "KeyError: Feature a not found in projection + # src_fv". + import tempfile + + from feast.utils import _get_feature_views_to_use + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as data_dir: + store = self._build_store(data_dir) + + fvs, odfvs = _get_feature_views_to_use( + store.registry, + store.project, + ["src_fv:a", "odfv_b:b_out"], + ) + + src_entries = [fv for fv in fvs if fv.projection.name_to_use() == "src_fv"] + assert len(src_entries) == 1 + projected = sorted( + feature.name for feature in src_entries[0].projection.features + ) + assert projected == ["a", "b"] + assert {odfv.name for odfv in odfvs} == {"odfv_b"} + + def test_regular_fv_and_odfv_source_merge_order_independent(self): + import tempfile + + from feast.utils import _get_feature_views_to_use + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as data_dir: + store = self._build_store(data_dir) + + fvs, _ = _get_feature_views_to_use( + store.registry, + store.project, + ["odfv_b:b_out", "src_fv:a"], + ) + + src_entries = [fv for fv in fvs if fv.projection.name_to_use() == "src_fv"] + assert len(src_entries) == 1 + projected = sorted( + feature.name for feature in src_entries[0].projection.features + ) + assert projected == ["a", "b"] + + def test_shared_source_ref_order_independent(self): + import tempfile + + from feast.utils import _get_feature_views_to_use + + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as data_dir: + store = self._build_store(data_dir) + + fvs, _ = _get_feature_views_to_use( + store.registry, + store.project, + ["odfv_b:b_out", "odfv_a:a_out"], + ) + src_entries = [fv for fv in fvs if fv.projection.name_to_use() == "src_fv"] + assert len(src_entries) == 1 + projected = sorted( + feature.name for feature in src_entries[0].projection.features + ) + assert projected == ["a", "b"] diff --git a/sdk/python/tests/unit/test_utils_entity_maps.py b/sdk/python/tests/unit/test_utils_entity_maps.py new file mode 100644 index 00000000000..cf331b4ad30 --- /dev/null +++ b/sdk/python/tests/unit/test_utils_entity_maps.py @@ -0,0 +1,280 @@ +# Copyright 2025 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for _get_entity_maps function in feast/utils.py + +These tests verify that the fix for issue #6012 correctly eliminates +redundant registry.get_entity() calls by using a local lookup dict. + +Related issue: https://github.com/feast-dev/feast/issues/6012 +""" + +from unittest.mock import MagicMock + +from feast.entity import Entity +from feast.utils import _get_entity_maps + + +class MockFeatureViewProjection: + """Mock FeatureViewProjection for testing.""" + + def __init__(self, join_key_map=None): + self.join_key_map = join_key_map or {} + + +class MockEntityColumn: + """Mock entity column for testing.""" + + def __init__(self, name: str, dtype): + self.name = name + self.dtype = dtype + + +class MockDtype: + """Mock dtype with to_value_type method.""" + + def __init__(self, value_type): + self._value_type = value_type + + def to_value_type(self): + return self._value_type + + +class MockFeatureView: + """Mock FeatureView for testing.""" + + def __init__(self, entities=None, entity_columns=None, join_key_map=None): + self.entities = entities or [] + self.entity_columns = entity_columns or [] + self.projection = MockFeatureViewProjection(join_key_map) + + +def create_mock_entity(name: str, join_key: str) -> Entity: + """Create a mock Entity with the specified name and join_key.""" + entity = MagicMock(spec=Entity) + entity.name = name + entity.join_key = join_key + return entity + + +class TestGetEntityMaps: + """Tests for _get_entity_maps function.""" + + def test_no_redundant_get_entity_calls(self): + """ + Verify that get_entity is NOT called after list_entities fetches all entities. + This is the core fix for issue #6012. + """ + # Create mock entities + entity1 = create_mock_entity("driver", "driver_id") + entity2 = create_mock_entity("customer", "customer_id") + + # Create mock registry + registry = MagicMock() + registry.list_entities.return_value = [entity1, entity2] + + # Create feature views that reference the entities + fv1 = MockFeatureView(entities=["driver"]) + fv2 = MockFeatureView(entities=["customer"]) + fv3 = MockFeatureView(entities=["driver", "customer"]) + + # Call the function under test + _get_entity_maps(registry, "test_project", [fv1, fv2, fv3]) + + # Verify list_entities was called once + registry.list_entities.assert_called_once_with("test_project", allow_cache=True) + + # Verify get_entity was NEVER called (this is the fix) + registry.get_entity.assert_not_called() + + def test_entity_name_to_join_key_mapping(self): + """Test that entity names are correctly mapped to join keys.""" + entity1 = create_mock_entity("driver", "driver_id") + entity2 = create_mock_entity("customer", "customer_id") + + registry = MagicMock() + registry.list_entities.return_value = [entity1, entity2] + + fv = MockFeatureView(entities=["driver", "customer"]) + + entity_name_to_join_key, _, _ = _get_entity_maps(registry, "test_project", [fv]) + + assert "driver" in entity_name_to_join_key + assert entity_name_to_join_key["driver"] == "driver_id" + assert "customer" in entity_name_to_join_key + assert entity_name_to_join_key["customer"] == "customer_id" + + def test_join_keys_set(self): + """Test that the join keys set is correctly returned.""" + entity1 = create_mock_entity("driver", "driver_id") + entity2 = create_mock_entity("customer", "customer_id") + + registry = MagicMock() + registry.list_entities.return_value = [entity1, entity2] + + fv = MockFeatureView(entities=["driver", "customer"]) + + _, _, join_keys = _get_entity_maps(registry, "test_project", [fv]) + + assert "driver_id" in join_keys + assert "customer_id" in join_keys + assert len(join_keys) == 2 + + def test_missing_entity_raises_exception(self): + """ + Test that missing entities (not in registry) raise EntityNotFoundException. + This maintains the original error behavior for misconfigured registries. + """ + import pytest + + from feast.errors import EntityNotFoundException + + entity1 = create_mock_entity("driver", "driver_id") + + registry = MagicMock() + registry.list_entities.return_value = [entity1] + + # Feature view references entity that doesn't exist in registry + fv = MockFeatureView(entities=["driver", "nonexistent_entity"]) + + # Should raise EntityNotFoundException for the missing entity + with pytest.raises(EntityNotFoundException) as exc_info: + _get_entity_maps(registry, "test_project", [fv]) + + assert "nonexistent_entity" in str(exc_info.value) + + def test_join_key_remapping(self): + """Test that join_key_map correctly remaps entity names and join keys.""" + entity = create_mock_entity("driver", "driver_id") + + registry = MagicMock() + registry.list_entities.return_value = [entity] + + # Feature view with join key mapping + fv = MockFeatureView( + entities=["driver"], + join_key_map={"driver_id": "remapped_driver_id"}, + ) + + entity_name_to_join_key, _, join_keys = _get_entity_maps( + registry, "test_project", [fv] + ) + + # The remapped join key should be in the mapping + assert "remapped_driver_id" in join_keys + + def test_empty_feature_views(self): + """Test with no feature views.""" + entity1 = create_mock_entity("driver", "driver_id") + + registry = MagicMock() + registry.list_entities.return_value = [entity1] + + entity_name_to_join_key, entity_type_map, join_keys = _get_entity_maps( + registry, "test_project", [] + ) + + # Should still have the base entity mapping from list_entities + assert "driver" in entity_name_to_join_key + assert entity_name_to_join_key["driver"] == "driver_id" + + def test_empty_registry_and_feature_views(self): + """Test with no entities and no feature views returns empty maps.""" + registry = MagicMock() + registry.list_entities.return_value = [] + + entity_name_to_join_key, entity_type_map, join_keys = _get_entity_maps( + registry, "test_project", [] + ) + + assert len(entity_name_to_join_key) == 0 + assert len(join_keys) == 0 + + def test_entity_type_map_from_entity_columns(self): + """Test that entity_type_map is populated from entity_columns.""" + from feast.value_type import ValueType + + entity = create_mock_entity("driver", "driver_id") + + registry = MagicMock() + registry.list_entities.return_value = [entity] + + # Create entity columns with dtype + driver_col = MockEntityColumn("driver_id", MockDtype(ValueType.INT64)) + rating_col = MockEntityColumn("rating", MockDtype(ValueType.FLOAT)) + + fv = MockFeatureView( + entities=["driver"], + entity_columns=[driver_col, rating_col], + ) + + _, entity_type_map, _ = _get_entity_maps(registry, "test_project", [fv]) + + assert "driver_id" in entity_type_map + assert entity_type_map["driver_id"] == ValueType.INT64 + assert "rating" in entity_type_map + assert entity_type_map["rating"] == ValueType.FLOAT + + +class TestGetEntityMapsPerformance: + """Performance-related tests for _get_entity_maps.""" + + def test_linear_scaling_with_feature_views(self): + """ + Verify that increasing feature views doesn't increase registry calls. + With N feature views referencing M entities, we should have: + - 1 list_entities call (not N*M get_entity calls) + """ + # Create many entities + entities = [create_mock_entity(f"entity_{i}", f"key_{i}") for i in range(10)] + + registry = MagicMock() + registry.list_entities.return_value = entities + + # Create many feature views, each referencing multiple entities + feature_views = [ + MockFeatureView(entities=[f"entity_{j}" for j in range(i % 10 + 1)]) + for i in range(50) + ] + + _get_entity_maps(registry, "test_project", feature_views) + + # Regardless of 50 feature views with varying entity counts: + # - list_entities should be called exactly once + # - get_entity should NEVER be called + registry.list_entities.assert_called_once() + registry.get_entity.assert_not_called() + + def test_duplicate_entity_references(self): + """ + Test that duplicate entity references across feature views + don't cause any issues or duplicate lookups. + """ + entity = create_mock_entity("driver", "driver_id") + + registry = MagicMock() + registry.list_entities.return_value = [entity] + + # Multiple feature views all referencing the same entity + feature_views = [MockFeatureView(entities=["driver"]) for _ in range(20)] + + entity_name_to_join_key, _, join_keys = _get_entity_maps( + registry, "test_project", feature_views + ) + + # Should work correctly with just one entity in the result + assert entity_name_to_join_key["driver"] == "driver_id" + assert "driver_id" in join_keys + registry.get_entity.assert_not_called() diff --git a/sdk/python/tests/unit/test_vector_store_utils.py b/sdk/python/tests/unit/test_vector_store_utils.py new file mode 100644 index 00000000000..135519ba8f1 --- /dev/null +++ b/sdk/python/tests/unit/test_vector_store_utils.py @@ -0,0 +1,123 @@ +import unittest.mock +from datetime import datetime, timezone + +import pytest + +from feast.errors import FeatureViewNotFoundException +from feast.feature_view import FeatureView +from feast.field import Field +from feast.infra.offline_stores.file_source import FileSource +from feast.types import Array, Float32, String +from feast.vector_store_utils import ( + VectorStoreRegistry, + build_vector_store_object, + feature_view_to_vs_id, +) + +_DUMMY_SOURCE = FileSource(path="dummy.parquet", timestamp_field="ts") + + +def _make_fv(name: str, vector_index: bool = True) -> FeatureView: + schema = [ + Field(name="embedding", dtype=Array(Float32), vector_index=vector_index), + Field(name="text", dtype=String), + ] + fv = FeatureView(name=name, schema=schema, source=_DUMMY_SOURCE) + fv.created_timestamp = datetime(2025, 1, 1, tzinfo=timezone.utc) + return fv + + +class TestFeatureViewToVsId: + def test_deterministic(self): + id1 = feature_view_to_vs_id("proj", "fv_name") + id2 = feature_view_to_vs_id("proj", "fv_name") + assert id1 == id2 + + def test_format(self): + vs_id = feature_view_to_vs_id("proj", "fv_name") + assert vs_id.startswith("vs_") + assert len(vs_id) == 27 # "vs_" + 24 hex chars + + def test_different_projects_produce_different_ids(self): + id_a = feature_view_to_vs_id("project_a", "embeddings") + id_b = feature_view_to_vs_id("project_b", "embeddings") + assert id_a != id_b + + def test_different_fv_names_produce_different_ids(self): + id_a = feature_view_to_vs_id("proj", "fv_one") + id_b = feature_view_to_vs_id("proj", "fv_two") + assert id_a != id_b + + +class TestBuildVectorStoreObject: + def test_shape(self): + fv = _make_fv("my_embeddings") + obj = build_vector_store_object("proj", fv) + assert obj["object"] == "vector_store" + assert obj["name"] == "my_embeddings" + assert obj["status"] == "completed" + assert obj["id"] == feature_view_to_vs_id("proj", "my_embeddings") + assert isinstance(obj["created_at"], int) + assert obj["created_at"] > 0 + + def test_no_created_timestamp(self): + fv = _make_fv("fv") + fv.created_timestamp = None + obj = build_vector_store_object("proj", fv) + assert obj["created_at"] == 0 + + +class TestVectorStoreRegistry: + def _mock_store(self, feature_views: list, project: str = "test_project"): + store = unittest.mock.MagicMock() + store.project = project + store.list_feature_views.return_value = feature_views + return store + + def test_resolve_found(self): + fv = _make_fv("products") + store = self._mock_store([fv]) + registry = VectorStoreRegistry(store) + vs_id = feature_view_to_vs_id("test_project", "products") + assert registry.resolve(vs_id).name == "products" + + def test_resolve_not_found(self): + store = self._mock_store([_make_fv("products")]) + registry = VectorStoreRegistry(store) + with pytest.raises(FeatureViewNotFoundException): + registry.resolve("vs_does_not_exist_000000") + + def test_list_filters_non_vector_fvs(self): + vec_fv = _make_fv("vec_fv", vector_index=True) + plain_fv = _make_fv("plain_fv", vector_index=False) + store = self._mock_store([vec_fv, plain_fv]) + registry = VectorStoreRegistry(store) + stores = registry.list_vector_stores() + names = [s["name"] for s in stores] + assert "vec_fv" in names + assert "plain_fv" not in names + + def test_get_vector_store_found(self): + fv = _make_fv("embeddings") + store = self._mock_store([fv]) + registry = VectorStoreRegistry(store) + vs_id = feature_view_to_vs_id("test_project", "embeddings") + obj = registry.get_vector_store(vs_id) + assert obj is not None + assert obj["name"] == "embeddings" + + def test_get_vector_store_not_found(self): + store = self._mock_store([_make_fv("embeddings")]) + registry = VectorStoreRegistry(store) + assert registry.get_vector_store("vs_bogus_id_000000000000") is None + + def test_refresh_picks_up_new_fvs(self): + fv1 = _make_fv("fv1") + store = self._mock_store([fv1]) + registry = VectorStoreRegistry(store) + assert len(registry.list_vector_stores()) == 1 + + fv2 = _make_fv("fv2") + store.list_feature_views.return_value = [fv1, fv2] + registry.refresh() + assert len(registry.list_vector_stores()) == 2 diff --git a/sdk/python/tests/unit/transformation/__init__.py b/sdk/python/tests/unit/transformation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/unit/transformation/test_factory.py b/sdk/python/tests/unit/transformation/test_factory.py new file mode 100644 index 00000000000..64c0e282239 --- /dev/null +++ b/sdk/python/tests/unit/transformation/test_factory.py @@ -0,0 +1,102 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from feast.transformation.factory import ( + TRANSFORMATION_CLASS_FOR_TYPE, + get_transformation_class_from_type, +) + + +class TestTransformationClassForType: + def test_all_expected_types_registered(self): + expected_types = { + "python", + "pandas", + "substrait", + "sql", + "spark_sql", + "spark", + "flink", + "ray", + } + assert set(TRANSFORMATION_CLASS_FOR_TYPE.keys()) == expected_types + + def test_spark_and_spark_sql_resolve_to_same_class(self): + assert ( + TRANSFORMATION_CLASS_FOR_TYPE["spark"] + == TRANSFORMATION_CLASS_FOR_TYPE["spark_sql"] + ) + + +class TestGetTransformationClassFromType: + @patch("feast.transformation.factory.import_class") + def test_known_type_resolves(self, mock_import): + mock_cls = MagicMock() + mock_import.return_value = mock_cls + + result = get_transformation_class_from_type("python") + + mock_import.assert_called_once_with( + "feast.transformation.python_transformation", + "PythonTransformation", + "PythonTransformation", + ) + assert result == mock_cls + + @patch("feast.transformation.factory.import_class") + def test_pandas_type_resolves(self, mock_import): + mock_cls = MagicMock() + mock_import.return_value = mock_cls + + get_transformation_class_from_type("pandas") + + mock_import.assert_called_once_with( + "feast.transformation.pandas_transformation", + "PandasTransformation", + "PandasTransformation", + ) + + @patch("feast.transformation.factory.import_class") + def test_sql_type_resolves(self, mock_import): + mock_cls = MagicMock() + mock_import.return_value = mock_cls + + get_transformation_class_from_type("sql") + + mock_import.assert_called_once_with( + "feast.transformation.sql_transformation", + "SQLTransformation", + "SQLTransformation", + ) + + @patch("feast.transformation.factory.import_class") + def test_flink_type_resolves(self, mock_import): + mock_cls = MagicMock() + mock_import.return_value = mock_cls + + get_transformation_class_from_type("flink") + + mock_import.assert_called_once_with( + "feast.transformation.flink_transformation", + "FlinkTransformation", + "FlinkTransformation", + ) + + def test_invalid_type_raises_value_error(self): + with pytest.raises(ValueError, match="Invalid transformation type"): + get_transformation_class_from_type("nonexistent") + + @patch("feast.transformation.factory.import_class") + def test_fully_qualified_class_name_accepted(self, mock_import): + """A string ending in 'Transformation' is treated as a fully qualified class path.""" + mock_cls = MagicMock() + mock_import.return_value = mock_cls + + get_transformation_class_from_type("my.custom.module.CustomTransformation") + + mock_import.assert_called_once_with( + "my.custom.module", + "CustomTransformation", + "CustomTransformation", + ) diff --git a/sdk/python/tests/unit/transformation/test_mode.py b/sdk/python/tests/unit/transformation/test_mode.py new file mode 100644 index 00000000000..3b35edcba0f --- /dev/null +++ b/sdk/python/tests/unit/transformation/test_mode.py @@ -0,0 +1,31 @@ +from feast.transformation.mode import TransformationMode + + +class TestTransformationMode: + def test_all_modes_defined(self): + expected = { + "PYTHON", + "PANDAS", + "SPARK_SQL", + "SPARK", + "FLINK", + "RAY", + "SQL", + "SUBSTRAIT", + } + actual = {m.name for m in TransformationMode} + assert actual == expected + + def test_mode_values(self): + assert TransformationMode.PYTHON.value == "python" + assert TransformationMode.PANDAS.value == "pandas" + assert TransformationMode.SPARK_SQL.value == "spark_sql" + assert TransformationMode.SPARK.value == "spark" + assert TransformationMode.FLINK.value == "flink" + assert TransformationMode.RAY.value == "ray" + assert TransformationMode.SQL.value == "sql" + assert TransformationMode.SUBSTRAIT.value == "substrait" + + def test_mode_from_value(self): + assert TransformationMode("python") == TransformationMode.PYTHON + assert TransformationMode("pandas") == TransformationMode.PANDAS diff --git a/sdk/python/tests/unit/transformation/test_spark_transformation.py b/sdk/python/tests/unit/transformation/test_spark_transformation.py deleted file mode 100644 index 63d9b520ce9..00000000000 --- a/sdk/python/tests/unit/transformation/test_spark_transformation.py +++ /dev/null @@ -1,127 +0,0 @@ -from unittest.mock import patch - -import pytest -from pyspark.sql import SparkSession -from pyspark.sql.functions import col, regexp_replace -from pyspark.testing.utils import assertDataFrameEqual - -from feast.transformation.base import Transformation -from feast.transformation.mode import TransformationMode -from feast.transformation.spark_transformation import SparkTransformation - - -def get_sample_df(spark): - sample_data = [ - {"name": "John D.", "age": 30}, - {"name": "Alice G.", "age": 25}, - {"name": "Bob T.", "age": 35}, - {"name": "Eve A.", "age": 28}, - ] - df = spark.createDataFrame(sample_data) - return df - - -def get_expected_df(spark): - expected_data = [ - {"name": "John D.", "age": 30}, - {"name": "Alice G.", "age": 25}, - {"name": "Bob T.", "age": 35}, - {"name": "Eve A.", "age": 28}, - ] - - expected_df = spark.createDataFrame(expected_data) - return expected_df - - -def remove_extra_spaces(df, column_name): - df_transformed = df.withColumn( - column_name, regexp_replace(col(column_name), "\\s+", " ") - ) - return df_transformed - - -def remove_extra_spaces_sql(df, column_name): - sql = f""" - SELECT - age, - regexp_replace({column_name}, '\\\\s+', ' ') as {column_name} - FROM {df} - """ - return sql - - -@pytest.fixture -def spark_fixture(): - spark = ( - SparkSession.builder.appName("Testing PySpark Example") - .config("spark.driver.host", "127.0.0.1") - .config("spark.driver.bindAddress", "127.0.0.1") - .getOrCreate() - ) - try: - yield spark - finally: - spark.stop() - - -@patch("feast.infra.compute_engines.spark.utils.get_or_create_new_spark_session") -def test_spark_transformation(spark_fixture): - spark = ( - SparkSession.builder.appName("Testing PySpark Example") - .config("spark.driver.host", "127.0.0.1") - .config("spark.driver.bindAddress", "127.0.0.1") - .getOrCreate() - ) - df = get_sample_df(spark) - - spark_transformation = Transformation( - mode=TransformationMode.SPARK, - udf=remove_extra_spaces, - udf_string="remove extra spaces", - ) - - transformed_df = spark_transformation.transform(df, "name") - expected_df = get_expected_df(spark) - assertDataFrameEqual(transformed_df, expected_df) - - -@patch("feast.infra.compute_engines.spark.utils.get_or_create_new_spark_session") -def test_spark_transformation_init_transformation(spark_fixture): - spark = ( - SparkSession.builder.appName("Testing PySpark Example") - .config("spark.driver.host", "127.0.0.1") - .config("spark.driver.bindAddress", "127.0.0.1") - .getOrCreate() - ) - df = get_sample_df(spark) - - spark_transformation = SparkTransformation( - mode=TransformationMode.SPARK, - udf=remove_extra_spaces, - udf_string="remove extra spaces", - ) - - transformed_df = spark_transformation.transform(df, "name") - expected_df = get_expected_df(spark) - assertDataFrameEqual(transformed_df, expected_df) - - -@patch("feast.infra.compute_engines.spark.utils.get_or_create_new_spark_session") -def test_spark_transformation_sql(spark_fixture): - spark = ( - SparkSession.builder.appName("Testing PySpark Example") - .config("spark.driver.host", "127.0.0.1") - .config("spark.driver.bindAddress", "127.0.0.1") - .getOrCreate() - ) - df = get_sample_df(spark) - - spark_transformation = SparkTransformation( - mode=TransformationMode.SPARK_SQL, - udf=remove_extra_spaces_sql, - udf_string="remove extra spaces sql", - ) - - transformed_df = spark_transformation.transform(df, "name") - expected_df = get_expected_df(spark) - assertDataFrameEqual(transformed_df, expected_df) diff --git a/sdk/python/tests/unit/transformation/test_sql_transformation.py b/sdk/python/tests/unit/transformation/test_sql_transformation.py new file mode 100644 index 00000000000..8d24eaac3ee --- /dev/null +++ b/sdk/python/tests/unit/transformation/test_sql_transformation.py @@ -0,0 +1,16 @@ +from feast.transformation.sql_transformation import SQLTransformation + + +def sql_udf(inputs): + return f"SELECT * FROM source WHERE id = {inputs['id']}" + + +def test_sql_transformation_transform(): + """SQLTransformation.transform delegates to the udf.""" + transformation = SQLTransformation( + mode="sql", + udf=sql_udf, + udf_string="sql_udf", + ) + result = transformation.transform({"id": 42}) + assert result == "SELECT * FROM source WHERE id = 42" diff --git a/sdk/python/tests/universal/__init__.py b/sdk/python/tests/universal/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/universal/feature_repos/duckdb_repo_configuration.py b/sdk/python/tests/universal/feature_repos/duckdb_repo_configuration.py new file mode 100644 index 00000000000..5e03065cc3b --- /dev/null +++ b/sdk/python/tests/universal/feature_repos/duckdb_repo_configuration.py @@ -0,0 +1,35 @@ +from feast.infra.offline_stores.duckdb import DuckDBOfflineStoreConfig +from tests.universal.feature_repos.universal.data_sources.file import ( + DeltaFileSourceCreator, + DeltaS3FileSourceCreator, + FileDataSourceCreator, +) + + +class DuckDBDataSourceCreator(FileDataSourceCreator): + def create_offline_store_config(self): + self.duckdb_offline_store_config = DuckDBOfflineStoreConfig() + return self.duckdb_offline_store_config + + +class DuckDBDeltaDataSourceCreator(DeltaFileSourceCreator): + def create_offline_store_config(self): + self.duckdb_offline_store_config = DuckDBOfflineStoreConfig() + return self.duckdb_offline_store_config + + +class DuckDBDeltaS3DataSourceCreator(DeltaS3FileSourceCreator): + def create_offline_store_config(self): + self.duckdb_offline_store_config = DuckDBOfflineStoreConfig( + staging_location="s3://test/staging", + staging_location_endpoint_override=self.endpoint_url, + ) + return self.duckdb_offline_store_config + + +AVAILABLE_OFFLINE_STORES = [ + ("local", DuckDBDataSourceCreator), + ("local", DuckDBDeltaDataSourceCreator), +] + +AVAILABLE_ONLINE_STORES = {"sqlite": ({"type": "sqlite"}, None)} diff --git a/sdk/python/tests/integration/feature_repos/integration_test_repo_config.py b/sdk/python/tests/universal/feature_repos/integration_test_repo_config.py similarity index 91% rename from sdk/python/tests/integration/feature_repos/integration_test_repo_config.py rename to sdk/python/tests/universal/feature_repos/integration_test_repo_config.py index 309f92005a3..f635d2e9c17 100644 --- a/sdk/python/tests/integration/feature_repos/integration_test_repo_config.py +++ b/sdk/python/tests/universal/feature_repos/integration_test_repo_config.py @@ -3,13 +3,13 @@ from enum import Enum from typing import Dict, Optional, Type, Union -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) -from tests.integration.feature_repos.universal.data_sources.file import ( +from tests.universal.feature_repos.universal.data_sources.file import ( FileDataSourceCreator, ) -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/universal/feature_repos/ray_repo_configuration.py b/sdk/python/tests/universal/feature_repos/ray_repo_configuration.py new file mode 100644 index 00000000000..39e43ae7b12 --- /dev/null +++ b/sdk/python/tests/universal/feature_repos/ray_repo_configuration.py @@ -0,0 +1,6 @@ +from feast.infra.offline_stores.contrib.ray_repo_configuration import ( + RayDataSourceCreator, +) + +AVAILABLE_OFFLINE_STORES = [("local", RayDataSourceCreator)] +AVAILABLE_ONLINE_STORES = {"sqlite": ({"type": "sqlite"}, None)} diff --git a/sdk/python/tests/integration/feature_repos/repo_configuration.py b/sdk/python/tests/universal/feature_repos/repo_configuration.py similarity index 88% rename from sdk/python/tests/integration/feature_repos/repo_configuration.py rename to sdk/python/tests/universal/feature_repos/repo_configuration.py index 89a13df69ed..7da8d54f433 100644 --- a/sdk/python/tests/integration/feature_repos/repo_configuration.py +++ b/sdk/python/tests/universal/feature_repos/repo_configuration.py @@ -27,40 +27,26 @@ FeatureLoggingConfig, ) from feast.infra.feature_servers.local_process.config import LocalFeatureServerConfig -from feast.infra.offline_stores.contrib.ray_repo_configuration import ( - RayDataSourceCreator, -) from feast.permissions.action import AuthzedAction from feast.permissions.auth_model import OidcClientAuthConfig from feast.permissions.permission import Permission from feast.permissions.policy import RoleBasedPolicy -from feast.repo_config import RegistryConfig, RepoConfig +from feast.repo_config import MaterializationConfig, RegistryConfig, RepoConfig from feast.utils import _utc_now -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, RegistryLocation, ) -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) -from tests.integration.feature_repos.universal.data_sources.bigquery import ( - BigQueryDataSourceCreator, -) -from tests.integration.feature_repos.universal.data_sources.file import ( - DuckDBDataSourceCreator, - DuckDBDeltaDataSourceCreator, +from tests.universal.feature_repos.universal.data_sources.file import ( FileDataSourceCreator, RemoteOfflineOidcAuthStoreDataSourceCreator, RemoteOfflineStoreDataSourceCreator, RemoteOfflineTlsStoreDataSourceCreator, ) -from tests.integration.feature_repos.universal.data_sources.redshift import ( - RedshiftDataSourceCreator, -) -from tests.integration.feature_repos.universal.data_sources.snowflake import ( - SnowflakeDataSourceCreator, -) -from tests.integration.feature_repos.universal.feature_views import ( +from tests.universal.feature_repos.universal.feature_views import ( conv_rate_plus_100_feature_view, create_conv_rate_request_source, create_customer_daily_profile_feature_view, @@ -72,22 +58,7 @@ create_order_feature_view, create_pushable_feature_view, ) -from tests.integration.feature_repos.universal.online_store.bigtable import ( - BigtableOnlineStoreCreator, -) -from tests.integration.feature_repos.universal.online_store.datastore import ( - DatastoreOnlineStoreCreator, -) -from tests.integration.feature_repos.universal.online_store.dynamodb import ( - DynamoDBOnlineStoreCreator, -) -from tests.integration.feature_repos.universal.online_store.milvus import ( - MilvusOnlineStoreCreator, -) -from tests.integration.feature_repos.universal.online_store.redis import ( - RedisOnlineStoreCreator, -) -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) @@ -118,29 +89,15 @@ "instance": os.getenv("BIGTABLE_INSTANCE_ID", "feast-integration-tests"), } -IKV_CONFIG = { - "type": "ikv", - "account_id": os.getenv("IKV_ACCOUNT_ID", ""), - "account_passkey": os.getenv("IKV_ACCOUNT_PASSKEY", ""), - "store_name": os.getenv("IKV_STORE_NAME", ""), - "mount_directory": os.getenv("IKV_MOUNT_DIR", ""), -} - OFFLINE_STORE_TO_PROVIDER_CONFIG: Dict[str, Tuple[str, Type[DataSourceCreator]]] = { "file": ("local", FileDataSourceCreator), - "bigquery": ("gcp", BigQueryDataSourceCreator), - "redshift": ("aws", RedshiftDataSourceCreator), - "snowflake": ("aws", SnowflakeDataSourceCreator), } AVAILABLE_OFFLINE_STORES: List[Tuple[str, Type[DataSourceCreator]]] = [ ("local", FileDataSourceCreator), - ("local", DuckDBDataSourceCreator), - ("local", DuckDBDeltaDataSourceCreator), ("local", RemoteOfflineStoreDataSourceCreator), ("local", RemoteOfflineOidcAuthStoreDataSourceCreator), ("local", RemoteOfflineTlsStoreDataSourceCreator), - ("local", RayDataSourceCreator), ] if os.getenv("FEAST_IS_LOCAL_TEST", "False") == "True": @@ -157,25 +114,42 @@ # Only configure Cloud DWH if running full integration tests if os.getenv("FEAST_IS_LOCAL_TEST", "False") != "True": + from tests.universal.feature_repos.universal.data_sources.bigquery import ( + BigQueryDataSourceCreator, + ) + from tests.universal.feature_repos.universal.data_sources.redshift import ( + RedshiftDataSourceCreator, + ) + AVAILABLE_OFFLINE_STORES.extend( [ ("gcp", BigQueryDataSourceCreator), ("aws", RedshiftDataSourceCreator), - ("aws", SnowflakeDataSourceCreator), ] ) + OFFLINE_STORE_TO_PROVIDER_CONFIG["bigquery"] = ("gcp", BigQueryDataSourceCreator) + OFFLINE_STORE_TO_PROVIDER_CONFIG["redshift"] = ("aws", RedshiftDataSourceCreator) + AVAILABLE_ONLINE_STORES["redis"] = (REDIS_CONFIG, None) AVAILABLE_ONLINE_STORES["dynamodb"] = (DYNAMO_CONFIG, None) AVAILABLE_ONLINE_STORES["datastore"] = ("datastore", None) - AVAILABLE_ONLINE_STORES["snowflake"] = (SNOWFLAKE_CONFIG, None) AVAILABLE_ONLINE_STORES["bigtable"] = (BIGTABLE_CONFIG, None) AVAILABLE_ONLINE_STORES["milvus"] = (MILVUS_CONFIG, None) - # Uncomment to test using private IKV account. Currently not enabled as - # there is no dedicated IKV instance for CI testing and there is no - # containerized version of IKV. - # AVAILABLE_ONLINE_STORES["ikv"] = (IKV_CONFIG, None) + # Snowflake is disabled temporarily + if os.getenv("SNOWFLAKE_CI_DEPLOYMENT"): + from tests.universal.feature_repos.universal.data_sources.snowflake import ( + SnowflakeDataSourceCreator, + ) + + AVAILABLE_OFFLINE_STORES.extend([("aws", SnowflakeDataSourceCreator)]) + OFFLINE_STORE_TO_PROVIDER_CONFIG["snowflake"] = ( + "aws", + SnowflakeDataSourceCreator, + ) + AVAILABLE_ONLINE_STORES["snowflake"] = (SNOWFLAKE_CONFIG, None) + full_repo_configs_module = os.environ.get(FULL_REPO_CONFIGS_MODULE_ENV_NAME) if full_repo_configs_module is not None: @@ -214,8 +188,27 @@ # Replace online stores with emulated online stores if we're running local integration tests if os.getenv("FEAST_LOCAL_ONLINE_CONTAINER", "False").lower() == "true": + from tests.universal.feature_repos.universal.online_store.bigtable import ( + BigtableOnlineStoreCreator, + ) + from tests.universal.feature_repos.universal.online_store.datastore import ( + DatastoreOnlineStoreCreator, + ) + from tests.universal.feature_repos.universal.online_store.dynamodb import ( + DynamoDBOnlineStoreCreator, + ) + from tests.universal.feature_repos.universal.online_store.milvus import ( + MilvusOnlineStoreCreator, + ) + from tests.universal.feature_repos.universal.online_store.postgres import ( + PGVectorOnlineStoreCreator, + ) + from tests.universal.feature_repos.universal.online_store.redis import ( + RedisOnlineStoreCreator, + ) + replacements: Dict[ - str, Tuple[Union[str, Dict[str, str]], Optional[Type[OnlineStoreCreator]]] + str, Tuple[Union[str, Dict[str, Any]], Optional[Type[OnlineStoreCreator]]] ] = { "redis": (REDIS_CONFIG, RedisOnlineStoreCreator), "milvus": (MILVUS_CONFIG, MilvusOnlineStoreCreator), @@ -223,6 +216,10 @@ "datastore": ("datastore", DatastoreOnlineStoreCreator), "bigtable": ("bigtable", BigtableOnlineStoreCreator), } + AVAILABLE_ONLINE_STORES["pgvector"] = ( + {"type": "postgres", "vector_enabled": True, "sslmode": "disable"}, + PGVectorOnlineStoreCreator, + ) for key, replacement in replacements.items(): if key in AVAILABLE_ONLINE_STORES: @@ -380,7 +377,6 @@ def values(self): def construct_universal_feature_views( data_sources: UniversalDataSources, with_odfv: bool = True, - use_substrait_odfv: bool = False, ) -> UniversalFeatureViews: driver_hourly_stats = create_driver_hourly_stats_feature_view(data_sources.driver) driver_hourly_stats_base_feature_view = ( @@ -396,7 +392,6 @@ def construct_universal_feature_views( driver_hourly_stats_base_feature_view[["conv_rate"]], create_conv_rate_request_source(), ], - use_substrait_odfv=use_substrait_odfv, ) if with_odfv else None, @@ -423,6 +418,9 @@ class Environment: entity_key_serialization_version: int repo_dir_name: str fixture_request: Optional[pytest.FixtureRequest] = None + materialization: MaterializationConfig = dataclasses.field( + default_factory=lambda: MaterializationConfig() + ) def __post_init__(self): self.end_date = _utc_now().replace(microsecond=0, second=0, minute=0) @@ -443,6 +441,7 @@ def setup(self): repo_path=self.repo_dir_name, feature_server=self.feature_server, entity_key_serialization_version=self.entity_key_serialization_version, + materialization_config=self.materialization, ) self.feature_store = FeatureStore(config=self.config) diff --git a/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py b/sdk/python/tests/universal/feature_repos/universal/data_source_creator.py similarity index 73% rename from sdk/python/tests/integration/feature_repos/universal/data_source_creator.py rename to sdk/python/tests/universal/feature_repos/universal/data_source_creator.py index 467db4dddce..6c0bc39b353 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_source_creator.py +++ b/sdk/python/tests/universal/feature_repos/universal/data_source_creator.py @@ -1,3 +1,4 @@ +import json from abc import ABC, abstractmethod from typing import Dict, Optional @@ -14,6 +15,29 @@ class DataSourceCreator(ABC): def __init__(self, project_name: str, *args, **kwargs): self.project_name = project_name + @staticmethod + def serialize_complex_columns(df: pd.DataFrame) -> pd.DataFrame: + """Serialize dict columns (Map/Struct types) to JSON strings. + + Backends like Snowflake, BigQuery, and Redshift cannot natively + ingest Python dicts via their bulk-load paths (VARIANT, STRUCT, + super types cause issues). Converting them to JSON strings lets + the data be stored as VARCHAR/STRING instead. + + List columns with primitive values (int, float, str, bool) are + left untouched since backends handle those as native ARRAY types. + """ + df = df.copy() + for col in df.columns: + if df[col].dropna().empty: + continue + sample = df[col].dropna().iloc[0] + if isinstance(sample, dict): + df[col] = df[col].apply( + lambda v: json.dumps(v) if v is not None else None + ) + return df + @abstractmethod def create_data_source( self, diff --git a/sdk/python/tests/universal/feature_repos/universal/data_sources/__init__.py b/sdk/python/tests/universal/feature_repos/universal/data_sources/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/bigquery.py b/sdk/python/tests/universal/feature_repos/universal/data_sources/bigquery.py similarity index 96% rename from sdk/python/tests/integration/feature_repos/universal/data_sources/bigquery.py rename to sdk/python/tests/universal/feature_repos/universal/data_sources/bigquery.py index 4fcd9533e8e..39595acf536 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/bigquery.py +++ b/sdk/python/tests/universal/feature_repos/universal/data_sources/bigquery.py @@ -15,7 +15,7 @@ SavedDatasetBigQueryStorage, ) from feast.utils import make_df_tzaware -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) @@ -80,6 +80,7 @@ def create_data_source( # `BigQueryOfflineStore.offline_write_batch`, but since we're bypassing that API here, we should follow the same # rule. The schema of this initial dataframe determines the schema in the newly created BigQuery table. df = make_df_tzaware(df) + df = self.serialize_complex_columns(df) job = self.client.load_table_from_dataframe(df, destination_name) job.result() diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py b/sdk/python/tests/universal/feature_repos/universal/data_sources/file.py similarity index 95% rename from sdk/python/tests/integration/feature_repos/universal/data_sources/file.py rename to sdk/python/tests/universal/feature_repos/universal/data_sources/file.py index a592dfc54a5..1084685e361 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/file.py +++ b/sdk/python/tests/universal/feature_repos/universal/data_sources/file.py @@ -24,7 +24,6 @@ from feast.data_source import DataSource from feast.feature_logging import LoggingDestination from feast.infra.offline_stores.dask import DaskOfflineStoreConfig -from feast.infra.offline_stores.duckdb import DuckDBOfflineStoreConfig from feast.infra.offline_stores.file_source import ( FileLoggingDestination, SavedDatasetFileStorage, @@ -32,7 +31,7 @@ from feast.infra.offline_stores.remote import RemoteOfflineStoreConfig from feast.repo_config import FeastConfigBaseModel, RegistryConfig from feast.wait import wait_retry_backoff # noqa: E402 -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) from tests.utils.auth_permissions_util import include_auth_config @@ -346,28 +345,6 @@ def teardown(self): self.f.close() -# TODO split up DataSourceCreator and OfflineStoreCreator -class DuckDBDataSourceCreator(FileDataSourceCreator): - def create_offline_store_config(self): - self.duckdb_offline_store_config = DuckDBOfflineStoreConfig() - return self.duckdb_offline_store_config - - -class DuckDBDeltaDataSourceCreator(DeltaFileSourceCreator): - def create_offline_store_config(self): - self.duckdb_offline_store_config = DuckDBOfflineStoreConfig() - return self.duckdb_offline_store_config - - -class DuckDBDeltaS3DataSourceCreator(DeltaS3FileSourceCreator): - def create_offline_store_config(self): - self.duckdb_offline_store_config = DuckDBOfflineStoreConfig( - staging_location="s3://test/staging", - staging_location_endpoint_override=self.endpoint_url, - ) - return self.duckdb_offline_store_config - - class RemoteOfflineStoreDataSourceCreator(FileDataSourceCreator): def __init__(self, project_name: str, *args, **kwargs): super().__init__(project_name) diff --git a/sdk/python/tests/universal/feature_repos/universal/data_sources/mongodb.py b/sdk/python/tests/universal/feature_repos/universal/data_sources/mongodb.py new file mode 100644 index 00000000000..829dcda9766 --- /dev/null +++ b/sdk/python/tests/universal/feature_repos/universal/data_sources/mongodb.py @@ -0,0 +1,195 @@ +""" +MongoDB DataSourceCreator implementation for universal Feast tests. +""" + +import os +import tempfile +from typing import Any, Dict, Optional + +import pandas as pd +import pytest +from testcontainers.mongodb import MongoDbContainer + +from feast.data_source import DataSource +from feast.feature_logging import LoggingDestination +from feast.infra.key_encoding_utils import serialize_entity_key +from feast.infra.offline_stores.contrib.mongodb_offline_store.mongodb import ( + MongoDBOfflineStoreConfig, + MongoDBSource, +) +from feast.infra.offline_stores.file_source import ( + FileLoggingDestination, + SavedDatasetFileStorage, +) +from feast.protos.feast.types.EntityKey_pb2 import EntityKey as EntityKeyProto +from feast.protos.feast.types.Value_pb2 import Value as ValueProto +from feast.repo_config import FeastConfigBaseModel +from tests.universal.feature_repos.universal.data_source_creator import ( + DataSourceCreator, +) + +try: + from pymongo import MongoClient +except ImportError: + MongoClient = None # type: ignore + + +class MongoDBDataSourceCreator(DataSourceCreator): + """DataSourceCreator for the MongoDB offline store (single shared collection).""" + + ENTITY_KEY_VERSION = 3 + + def __init__(self, project_name: str, *args, **kwargs): + super().__init__(project_name) + self.container = MongoDbContainer( + "mongo:7.0", + username="test", + password="test", # pragma: allowlist secret + ).with_exposed_ports(27017) + self.container.start() + self.port = self.container.get_exposed_port(27017) + self.connection_string = ( + f"mongodb://test:test@localhost:{self.port}" # pragma: allowlist secret + ) + self.database = f"feast_test_{project_name}" + self.collection = "feature_history" + self._entity_key_columns: Dict[str, list] = {} + + def _serialize_entity_key(self, row: pd.Series, join_keys: list[str]) -> bytes: + """Serialize entity key columns to bytes. + + Join keys are sorted before serialization to match the order used by + _serialize_entity_key_from_row at query time. Without sorting, compound + join keys written in a different order would produce different bytes and + cause all features to be returned as NULL. + """ + entity_key = EntityKeyProto() + for key in sorted(join_keys): + entity_key.join_keys.append(key) + val = ValueProto() + value = row[key] + # bool must be checked before int: bool is a subclass of int + if isinstance(value, bool): + val.bool_val = value + elif isinstance(value, int): + val.int64_val = value + elif isinstance(value, str): + val.string_val = value + elif isinstance(value, float): + val.double_val = value + else: + val.string_val = str(value) + entity_key.entity_values.append(val) + return serialize_entity_key(entity_key, self.ENTITY_KEY_VERSION) + + def create_data_source( + self, + df: pd.DataFrame, + destination_name: str, + created_timestamp_column: str = "created_ts", + field_mapping: Optional[Dict[str, str]] = None, + timestamp_field: Optional[str] = "ts", + ) -> DataSource: + """Create a MongoDB data source by inserting df into the shared collection. + + The data is transformed into the One schema: + - entity_id: serialized entity key + - feature_view: destination_name + - features: nested dict of feature values + - event_timestamp: from timestamp_field + - created_at: from created_timestamp_column + """ + # Determine which columns are join keys vs features + # Join keys must be integer or string types (serializable as entity keys) + timestamp_cols = {timestamp_field, created_timestamp_column} + all_cols = set(df.columns) - timestamp_cols - {None} + + # Heuristic: identify join keys (columns ending with _id or known key names) + join_keys = [] + for c in all_cols: + if c.endswith("_id") or c in {"driver", "customer", "entity"}: + dtype = df[c].dtype + if dtype in ("int64", "int32", "object") or str(dtype).startswith( + "int" + ): + join_keys.append(c) + + if not join_keys: + for c in all_cols: + if df[c].dtype in ("int64", "int32") or str(df[c].dtype).startswith( + "int" + ): + join_keys = [c] + break + + feature_cols = [c for c in all_cols if c not in join_keys] + self._entity_key_columns[destination_name] = join_keys + + docs = [] + for _, row in df.iterrows(): + entity_id = self._serialize_entity_key(row, join_keys) + features = {col: row[col] for col in feature_cols if pd.notna(row.get(col))} + doc: Dict[str, Any] = { + "entity_id": entity_id, + "feature_view": destination_name, + "features": features, + } + if timestamp_field and timestamp_field in row: + doc["event_timestamp"] = row[timestamp_field] + if created_timestamp_column and created_timestamp_column in row: + doc["created_at"] = row[created_timestamp_column] + docs.append(doc) + + # Insert into MongoDB + client: Any = MongoClient(self.connection_string, tz_aware=True) + try: + coll = client[self.database][self.collection] + if docs: + coll.insert_many(docs) + finally: + client.close() + + return MongoDBSource( + name=destination_name, + timestamp_field="event_timestamp", + created_timestamp_column="created_at" if created_timestamp_column else None, + field_mapping=field_mapping, + ) + + def get_prefixed_table_name(self, suffix: str) -> str: + return f"{self.project_name}_{suffix}" + + def create_offline_store_config(self) -> FeastConfigBaseModel: + return MongoDBOfflineStoreConfig( + connection_string=self.connection_string, + database=self.database, + collection=self.collection, + ) + + def create_saved_dataset_destination(self) -> SavedDatasetFileStorage: + # Saved datasets are written as parquet via SavedDatasetFileStorage. + # A fresh temp file is created per call; teardown() does not clean it up + # because the test framework owns the lifetime of saved datasets. + path = os.path.join( + tempfile.mkdtemp(), f"{self.project_name}_saved_dataset.parquet" + ) + return SavedDatasetFileStorage(path=path) + + def create_logged_features_destination(self) -> LoggingDestination: + d = tempfile.mkdtemp(prefix=self.project_name) + return FileLoggingDestination(path=d) + + def teardown(self): + try: + client: Any = MongoClient(self.connection_string, tz_aware=True) + try: + client[self.database][self.collection].drop() + finally: + client.close() + except Exception: + pass + self.container.stop() + + @staticmethod + def test_markers() -> list: + return [pytest.mark.mongodb] diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/redshift.py b/sdk/python/tests/universal/feature_repos/universal/data_sources/redshift.py similarity index 97% rename from sdk/python/tests/integration/feature_repos/universal/data_sources/redshift.py rename to sdk/python/tests/universal/feature_repos/universal/data_sources/redshift.py index 91d1a74f071..000bc226694 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/redshift.py +++ b/sdk/python/tests/universal/feature_repos/universal/data_sources/redshift.py @@ -14,7 +14,7 @@ ) from feast.infra.utils import aws_utils from feast.repo_config import FeastConfigBaseModel -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) @@ -55,6 +55,7 @@ def create_data_source( ) -> DataSource: destination_name = self.get_prefixed_table_name(destination_name) + df = self.serialize_complex_columns(df) aws_utils.upload_df_to_redshift( self.client, self.offline_store_config.cluster_id, diff --git a/sdk/python/tests/integration/feature_repos/universal/data_sources/snowflake.py b/sdk/python/tests/universal/feature_repos/universal/data_sources/snowflake.py similarity index 96% rename from sdk/python/tests/integration/feature_repos/universal/data_sources/snowflake.py rename to sdk/python/tests/universal/feature_repos/universal/data_sources/snowflake.py index e9c4ad21a31..8e456168786 100644 --- a/sdk/python/tests/integration/feature_repos/universal/data_sources/snowflake.py +++ b/sdk/python/tests/universal/feature_repos/universal/data_sources/snowflake.py @@ -18,7 +18,7 @@ write_pandas, ) from feast.repo_config import FeastConfigBaseModel -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) @@ -53,6 +53,7 @@ def create_data_source( ) -> DataSource: destination_name = self.get_prefixed_table_name(destination_name) + df = self.serialize_complex_columns(df) with GetSnowflakeConnection(self.offline_store_config) as conn: write_pandas(conn, df, destination_name, auto_create_table=True) diff --git a/sdk/python/tests/integration/feature_repos/universal/entities.py b/sdk/python/tests/universal/feature_repos/universal/entities.py similarity index 100% rename from sdk/python/tests/integration/feature_repos/universal/entities.py rename to sdk/python/tests/universal/feature_repos/universal/entities.py diff --git a/sdk/python/tests/integration/feature_repos/universal/feature_views.py b/sdk/python/tests/universal/feature_repos/universal/feature_views.py similarity index 91% rename from sdk/python/tests/integration/feature_repos/universal/feature_views.py rename to sdk/python/tests/universal/feature_repos/universal/feature_views.py index cd02c3478c7..2d1e62ea617 100644 --- a/sdk/python/tests/integration/feature_repos/universal/feature_views.py +++ b/sdk/python/tests/universal/feature_repos/universal/feature_views.py @@ -3,7 +3,6 @@ import numpy as np import pandas as pd -from ibis.expr.types.relations import Table from feast import ( BatchFeatureView, @@ -16,9 +15,20 @@ ) from feast.data_source import DataSource, RequestSource from feast.feature_view_projection import FeatureViewProjection -from feast.on_demand_feature_view import PandasTransformation, SubstraitTransformation -from feast.types import Array, FeastType, Float32, Float64, Int32, Int64, String -from tests.integration.feature_repos.universal.entities import ( +from feast.on_demand_feature_view import PandasTransformation +from feast.types import ( + Array, + FeastType, + Float32, + Float64, + Int32, + Int64, + Json, + Map, + String, + Struct, +) +from tests.universal.feature_repos.universal.entities import ( customer, driver, item, @@ -59,22 +69,10 @@ def conv_rate_plus_100(features_df: pd.DataFrame) -> pd.DataFrame: return df -def conv_rate_plus_100_ibis(features_table: Table) -> Table: - return features_table.mutate( - conv_rate_plus_100=features_table["conv_rate"] + 100, - conv_rate_plus_val_to_add=features_table["conv_rate"] - + features_table["val_to_add"], - conv_rate_plus_100_rounded=(features_table["conv_rate"] + 100) - .round(digits=0) - .cast("int32"), - ) - - def conv_rate_plus_100_feature_view( sources: List[Union[FeatureView, RequestSource, FeatureViewProjection]], infer_features: bool = False, features: Optional[List[Field]] = None, - use_substrait_odfv: bool = False, ) -> OnDemandFeatureView: # Test that positional arguments and Features still work for ODFVs. _features = features or [ @@ -89,10 +87,8 @@ def conv_rate_plus_100_feature_view( feature_transformation=PandasTransformation( udf=conv_rate_plus_100, udf_string="raw udf source", # type: ignore - ) - if not use_substrait_odfv - else SubstraitTransformation.from_ibis(conv_rate_plus_100_ibis, sources), - mode="pandas" if not use_substrait_odfv else "substrait", + ), + mode="pandas", ) @@ -208,6 +204,12 @@ def create_driver_hourly_stats_feature_view(source, infer_features: bool = False Field(name="acc_rate", dtype=Float32), Field(name="avg_daily_trips", dtype=Int32), Field(name=d.join_key, dtype=Int64), + Field(name="driver_metadata", dtype=Map), + Field(name="driver_config", dtype=Json), + Field( + name="driver_profile", + dtype=Struct({"name": String, "age": String}), + ), ], source=source, ttl=timedelta(hours=2), @@ -228,6 +230,12 @@ def create_driver_hourly_stats_batch_feature_view( Field(name="conv_rate", dtype=Float32), Field(name="acc_rate", dtype=Float32), Field(name="avg_daily_trips", dtype=Int32), + Field(name="driver_metadata", dtype=Map), + Field(name="driver_config", dtype=Json), + Field( + name="driver_profile", + dtype=Struct({"name": String, "age": String}), + ), ], source=source, ttl=timedelta(hours=2), diff --git a/sdk/python/tests/universal/feature_repos/universal/online_store/__init__.py b/sdk/python/tests/universal/feature_repos/universal/online_store/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/universal/feature_repos/universal/online_store/aerospike.py b/sdk/python/tests/universal/feature_repos/universal/online_store/aerospike.py new file mode 100644 index 00000000000..de28ad15163 --- /dev/null +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/aerospike.py @@ -0,0 +1,58 @@ +# +# Copyright 2019 The Feast Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from typing import Any, Dict + +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_for_logs + +from tests.universal.feature_repos.universal.online_store_creator import ( + OnlineStoreCreator, +) + +# Aerospike Community Edition — pin a known-good server image so CI stays +# deterministic; bump when Feast's Aerospike server baseline moves. +AEROSPIKE_CE_IMAGE = "aerospike/aerospike-server:8.0.0.10_1" + +# The Aerospike server prints this line once the default "test" namespace has +# finished starting up and the service is accepting client connections. +_READY_LOG_MARKER = "migrations: complete" + + +class AerospikeOnlineStoreCreator(OnlineStoreCreator): + """Spin up a single-node Aerospike CE cluster for universal online-store tests. + + The CE image ships a pre-configured namespace called ``test``, which we + reuse as the Feast namespace (avoiding the need to mount a custom + ``aerospike.conf``). + """ + + def __init__(self, project_name: str, **kwargs): + super().__init__(project_name) + self.container = DockerContainer(AEROSPIKE_CE_IMAGE).with_exposed_ports("3000") + + def create_online_store(self) -> Dict[str, Any]: + self.container.start() + wait_for_logs(container=self.container, predicate=_READY_LOG_MARKER, timeout=60) + exposed_port = int(self.container.get_exposed_port("3000")) + return { + "type": "aerospike", + "hosts": [("127.0.0.1", exposed_port)], + "namespace": "test", + } + + def teardown(self): + self.container.stop() diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/bigtable.py b/sdk/python/tests/universal/feature_repos/universal/online_store/bigtable.py similarity index 95% rename from sdk/python/tests/integration/feature_repos/universal/online_store/bigtable.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/bigtable.py index c06143e245b..a6f6ceb12da 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/bigtable.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/bigtable.py @@ -5,7 +5,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/cassandra.py b/sdk/python/tests/universal/feature_repos/universal/online_store/cassandra.py similarity index 96% rename from sdk/python/tests/integration/feature_repos/universal/online_store/cassandra.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/cassandra.py index 190d94a8305..c7ca91e18b2 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/cassandra.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/cassandra.py @@ -20,7 +20,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/couchbase.py b/sdk/python/tests/universal/feature_repos/universal/online_store/couchbase.py similarity index 98% rename from sdk/python/tests/integration/feature_repos/universal/online_store/couchbase.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/couchbase.py index 2723ff13a30..cb030b708c6 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/couchbase.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/couchbase.py @@ -5,7 +5,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/datastore.py b/sdk/python/tests/universal/feature_repos/universal/online_store/datastore.py similarity index 94% rename from sdk/python/tests/integration/feature_repos/universal/online_store/datastore.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/datastore.py index b5bbb94f7c1..8eaa5139e43 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/datastore.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/datastore.py @@ -5,7 +5,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/dynamodb.py b/sdk/python/tests/universal/feature_repos/universal/online_store/dynamodb.py similarity index 93% rename from sdk/python/tests/integration/feature_repos/universal/online_store/dynamodb.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/dynamodb.py index 1aefdffb24b..04a1ffac355 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/dynamodb.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/dynamodb.py @@ -3,7 +3,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/elasticsearch.py b/sdk/python/tests/universal/feature_repos/universal/online_store/elasticsearch.py similarity index 90% rename from sdk/python/tests/integration/feature_repos/universal/online_store/elasticsearch.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/elasticsearch.py index 1e8088a997e..8ef467ae796 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/elasticsearch.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/elasticsearch.py @@ -2,7 +2,7 @@ from testcontainers.elasticsearch import ElasticSearchContainer -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/hazelcast.py b/sdk/python/tests/universal/feature_repos/universal/online_store/hazelcast.py similarity index 95% rename from sdk/python/tests/integration/feature_repos/universal/online_store/hazelcast.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/hazelcast.py index d50f2b75a3d..22ea5dcc09d 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/hazelcast.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/hazelcast.py @@ -6,7 +6,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/hbase.py b/sdk/python/tests/universal/feature_repos/universal/online_store/hbase.py similarity index 92% rename from sdk/python/tests/integration/feature_repos/universal/online_store/hbase.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/hbase.py index dba611b30bc..d350828d08a 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/hbase.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/hbase.py @@ -3,7 +3,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/hybrid_online_store.py b/sdk/python/tests/universal/feature_repos/universal/online_store/hybrid_online_store.py similarity index 90% rename from sdk/python/tests/integration/feature_repos/universal/online_store/hybrid_online_store.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/hybrid_online_store.py index f0efbd11044..619702330e6 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/hybrid_online_store.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/hybrid_online_store.py @@ -1,4 +1,4 @@ -from sdk.python.tests.integration.feature_repos.universal.online_store_creator import ( +from sdk.python.tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/init.sql b/sdk/python/tests/universal/feature_repos/universal/online_store/init.sql similarity index 100% rename from sdk/python/tests/integration/feature_repos/universal/online_store/init.sql rename to sdk/python/tests/universal/feature_repos/universal/online_store/init.sql diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/milvus.py b/sdk/python/tests/universal/feature_repos/universal/online_store/milvus.py similarity index 81% rename from sdk/python/tests/integration/feature_repos/universal/online_store/milvus.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/milvus.py index 7231e5d9ded..cfe6aec3677 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/milvus.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/milvus.py @@ -1,6 +1,6 @@ -from typing import Any, Dict +from typing import Any -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) @@ -10,7 +10,7 @@ def __init__(self, project_name: str, **kwargs): super().__init__(project_name) self.db_path = "online_store.db" - def create_online_store(self) -> Dict[str, Any]: + def create_online_store(self) -> dict[str, Any]: return { "type": "milvus", "path": self.db_path, diff --git a/sdk/python/tests/universal/feature_repos/universal/online_store/mongodb.py b/sdk/python/tests/universal/feature_repos/universal/online_store/mongodb.py new file mode 100644 index 00000000000..a3791446232 --- /dev/null +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/mongodb.py @@ -0,0 +1,57 @@ +from typing import Any, Dict + +from testcontainers.mongodb import MongoDBAtlasLocalContainer, MongoDbContainer + +from tests.universal.feature_repos.universal.online_store_creator import ( + OnlineStoreCreator, +) + + +class MongoDBOnlineStoreCreator(OnlineStoreCreator): + def __init__(self, project_name: str, **kwargs): + super().__init__(project_name) + # MongoDbContainer from testcontainers sets up authentication by default + # with username and password from the constructor + self.container = MongoDbContainer( + "mongo:latest", + username="test", + password="test", # pragma: allowlist secret + ).with_exposed_ports(27017) + + def create_online_store(self) -> Dict[str, Any]: + self.container.start() + exposed_port = self.container.get_exposed_port(27017) + # Include authentication in the connection string + return { + "type": "mongodb", + "connection_string": f"mongodb://test:test@localhost:{exposed_port}", # pragma: allowlist secret + } + + def teardown(self): + self.container.stop() + + +class MongoDBAtlasOnlineStoreCreator(OnlineStoreCreator): + """OnlineStoreCreator backed by ``MongoDBAtlasLocalContainer``. + + This uses the ``mongodb/mongodb-atlas-local`` Docker image which provides + a local Atlas deployment with support for Atlas Search and Vector Search. + """ + + def __init__(self, project_name: str, **kwargs): + super().__init__(project_name) + self.container = MongoDBAtlasLocalContainer( + "mongodb/mongodb-atlas-local:8.0.4", + ) + + def create_online_store(self) -> Dict[str, Any]: + self.container.start() + connection_string = self.container.get_connection_url() + return { + "type": "mongodb", + "connection_string": connection_string, + "vector_enabled": True, + } + + def teardown(self): + self.container.stop() diff --git a/sdk/python/tests/universal/feature_repos/universal/online_store/mysql.py b/sdk/python/tests/universal/feature_repos/universal/online_store/mysql.py new file mode 100644 index 00000000000..fa6978b259a --- /dev/null +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/mysql.py @@ -0,0 +1,61 @@ +from typing import Dict + +from testcontainers.mysql import MySqlContainer + +from tests.universal.feature_repos.universal.online_store_creator import ( + OnlineStoreCreator, +) + + +class MySQLOnlineStoreCreator(OnlineStoreCreator): + def __init__(self, project_name: str, **kwargs): + super().__init__(project_name) + self.container = ( + MySqlContainer("mysql:latest", platform="linux/amd64", dialect="pymysql") + .with_exposed_ports(3306) + .with_env("MYSQL_USER", "root") + .with_env("MYSQL_PASSWORD", "test") + .with_env("MYSQL_DATABASE", "test") + ) + + def create_online_store(self) -> Dict[str, str]: + self.container.start() + exposed_port = self.container.get_exposed_port(3306) + return { + "type": "mysql", + "user": "root", + "password": "test", + "database": "test", + "port": exposed_port, + } + + def teardown(self): + self.container.stop() + + +class BatchWriteMySQLOnlineStoreCreator(OnlineStoreCreator): + def __init__(self, project_name: str, **kwargs): + super().__init__(project_name) + self.container = ( + MySqlContainer("mysql:latest", platform="linux/amd64", dialect="pymysql") + .with_exposed_ports(3306) + .with_env("MYSQL_USER", "root") + .with_env("MYSQL_PASSWORD", "test") + .with_env("MYSQL_DATABASE", "test") + ) + + def create_online_store(self) -> Dict[str, str]: + self.container.start() + exposed_port = self.container.get_exposed_port(3306) + return { + "type": "mysql", + "user": "root", + "password": "test", + "database": "test", + "port": exposed_port, + "batch_write": "True", + "bacth_size": "1000", + } + + def teardown(self): + self.container.stop() diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py b/sdk/python/tests/universal/feature_repos/universal/online_store/postgres.py similarity index 80% rename from sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/postgres.py index d11f563cb5c..1a2289cef84 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/postgres.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/postgres.py @@ -1,11 +1,12 @@ import os +import time from typing import Any, Dict from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs from testcontainers.postgres import PostgresContainer -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) @@ -53,14 +54,18 @@ def __init__(self, project_name: str, **kwargs): def create_online_store(self) -> Dict[str, Any]: self.container.start() - log_string_to_wait_for = "database system is ready to accept connections" wait_for_logs( - container=self.container, predicate=log_string_to_wait_for, timeout=10 + container=self.container, + predicate="database system is ready to accept connections", + timeout=30, ) - init_log_string_to_wait_for = "PostgreSQL init process complete" wait_for_logs( - container=self.container, predicate=init_log_string_to_wait_for, timeout=10 + container=self.container, + predicate="PostgreSQL init process complete", + timeout=30, ) + # PG restarts after init completes; allow the restart to finish + time.sleep(3) return { "host": "localhost", "type": "postgres", @@ -68,7 +73,9 @@ def create_online_store(self) -> Dict[str, Any]: "password": "test!@#$%", "database": "test", "vector_enabled": True, + "enable_openai_compatible_store": True, "port": self.container.get_exposed_port(5432), + "sslmode": "disable", } def teardown(self): diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/qdrant.py b/sdk/python/tests/universal/feature_repos/universal/online_store/qdrant.py similarity index 90% rename from sdk/python/tests/integration/feature_repos/universal/online_store/qdrant.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/qdrant.py index 82a027b416d..a9036252711 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/qdrant.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/qdrant.py @@ -2,7 +2,7 @@ from testcontainers.qdrant import QdrantContainer -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/redis.py b/sdk/python/tests/universal/feature_repos/universal/online_store/redis.py similarity index 92% rename from sdk/python/tests/integration/feature_repos/universal/online_store/redis.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/redis.py index 8e18f7fb172..715755471df 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/redis.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/redis.py @@ -3,7 +3,7 @@ from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/universal/feature_repos/universal/online_store/scylladb.py b/sdk/python/tests/universal/feature_repos/universal/online_store/scylladb.py new file mode 100644 index 00000000000..59f6ae97549 --- /dev/null +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/scylladb.py @@ -0,0 +1,83 @@ +import time +from typing import Dict + +from testcontainers.core.container import DockerContainer +from testcontainers.core.network import Network +from testcontainers.core.waiting_utils import wait_for_logs + +from tests.universal.feature_repos.universal.online_store_creator import ( + OnlineStoreCreator, +) + +_SCYLLA_IMAGE = "scylladb/scylla:2026.1.4" +_VECTOR_STORE_IMAGE = "scylladb/vector-store:1.7.0" + + +class ScyllaDBOnlineStoreCreator(OnlineStoreCreator): + """ + Starts a ScyllaDB + Vector Store stack for integration tests. + + Two containers share a Docker network: + - scylladb/scylla:2026.1.4 — CQL data node (exposes port 9042) + - scylladb/vector-store:1.7.0 — ANN indexing service (port 6080) + + Both regular read/write and vector search tests run against this stack + with no external cloud dependency. + """ + + def __init__(self, project_name: str, **kwargs): + super().__init__(project_name) + self.network = Network() + self.scylla = ( + DockerContainer(_SCYLLA_IMAGE) + .with_network(self.network) + .with_network_aliases("scylla") + .with_command( + "--smp 1 --memory 1G --overprovisioned 1 " + "--vector-store-primary-uri http://vector-store:6080 " + "--broadcast-rpc-address 127.0.0.1" + ) + .with_exposed_ports("9042") + ) + self.vector_store = ( + DockerContainer(_VECTOR_STORE_IMAGE) + .with_network(self.network) + .with_network_aliases("vector-store") + .with_env("VECTOR_STORE_URI", "0.0.0.0:6080") + .with_env("VECTOR_STORE_SCYLLADB_URI", "scylla:9042") + ) + + def create_online_store(self) -> Dict[str, object]: + self.network.create() + self.scylla.start() + wait_for_logs( + container=self.scylla, + predicate="Starting listening for CQL clients", + timeout=120, + ) + time.sleep(5) # allow CQL port to be fully ready + self.vector_store.start() + wait_for_logs( + container=self.vector_store, + predicate="6080", + timeout=60, + ) + keyspace = "feast_keyspace" + self.scylla.exec( + f'cqlsh -e "CREATE KEYSPACE IF NOT EXISTS \\"{keyspace}\\"' + " WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter1': 1}" + " AND tablets = {'enabled': true};\"" + ) + exposed_port = int(self.scylla.get_exposed_port("9042")) + return { + "type": "scylladb", + "hosts": ["127.0.0.1"], + "port": exposed_port, + "keyspace": keyspace, + "local_dc": "datacenter1", + } + + def teardown(self): + self.vector_store.stop() + self.scylla.stop() + self.network.remove() diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store/singlestore.py b/sdk/python/tests/universal/feature_repos/universal/online_store/singlestore.py similarity index 95% rename from sdk/python/tests/integration/feature_repos/universal/online_store/singlestore.py rename to sdk/python/tests/universal/feature_repos/universal/online_store/singlestore.py index d3a02421d0a..3c9a835dfe2 100644 --- a/sdk/python/tests/integration/feature_repos/universal/online_store/singlestore.py +++ b/sdk/python/tests/universal/feature_repos/universal/online_store/singlestore.py @@ -4,7 +4,7 @@ from testcontainers.core.container import DockerContainer -from tests.integration.feature_repos.universal.online_store_creator import ( +from tests.universal.feature_repos.universal.online_store_creator import ( OnlineStoreCreator, ) diff --git a/sdk/python/tests/integration/feature_repos/universal/online_store_creator.py b/sdk/python/tests/universal/feature_repos/universal/online_store_creator.py similarity index 100% rename from sdk/python/tests/integration/feature_repos/universal/online_store_creator.py rename to sdk/python/tests/universal/feature_repos/universal/online_store_creator.py diff --git a/sdk/python/tests/universal/offline_store/.gitkeep b/sdk/python/tests/universal/offline_store/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/universal/online_store/.gitkeep b/sdk/python/tests/universal/online_store/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/tests/utils/cli_repo_creator.py b/sdk/python/tests/utils/cli_repo_creator.py index ea1d7fcf10b..e39ab992c73 100644 --- a/sdk/python/tests/utils/cli_repo_creator.py +++ b/sdk/python/tests/utils/cli_repo_creator.py @@ -1,3 +1,25 @@ +""" +CLI test utilities for Feast testing. + +Note: This module contains a workaround for a known subprocess hang issue: + +Dask atexit handler: when a Feast materialization fails mid-execution, Dask's +global ThreadPoolExecutor registers an atexit.register(default_pool.shutdown) +handler. If the pool has active or recently-used threads, shutdown(wait=True) +can block for an extended period, preventing the subprocess from exiting. This +causes the parent's subprocess.check_output / communicate() to block forever. + +The fix is to use subprocess.Popen with communicate(timeout=...) so we can kill +the subprocess if it hangs and still recover any partial output (which contains +the error message printed before the hang). + +Teardown is intentionally performed in-process (store.teardown()) rather than +via a 'feast teardown' subprocess. This eliminates per-repo subprocess startup +overhead and avoids atexit-handler (Dask thread pool, PySpark JVM) hang risks +that can push the cumulative test time past the pytest global timeout budget. +""" + +import os import random import string import subprocess @@ -32,23 +54,87 @@ class CliRunner: modules from the feature repo, and it is hard to clean up that state otherwise. """ - def run(self, args: List[str], cwd: Path) -> subprocess.CompletedProcess: - return subprocess.run( - [sys.executable, cli.__file__] + args, cwd=cwd, capture_output=True - ) + def _subprocess_env(self) -> dict[str, str]: + env = os.environ.copy() + parent_paths = [path for path in sys.path if path] + existing_pythonpath = env.get("PYTHONPATH") + if existing_pythonpath: + parent_paths.append(existing_pythonpath) + env["PYTHONPATH"] = os.pathsep.join(parent_paths) + return env + + def run( + self, args: List[str], cwd: Path, attempts: int = 2 + ) -> subprocess.CompletedProcess: + # Apply a conservative timeout to prevent CI hangs from Dask atexit-handler + # stalls or other subprocess blockages. + timeout = 120 + + # Commands routed through here (e.g. `apply`) do not materialize, so they + # cannot trigger the Dask atexit hang described in the module docstring. A + # timeout is therefore almost always transient slowness — a cold child + # interpreter importing Feast (pandas/pyarrow/...) on a loaded CI runner, + # which has been observed to exceed the timeout on the slower macOS + # runners. Retry once so a single slow cold start does not flake the test: + # the retry runs with warm OS/page caches and typically finishes quickly, + # while a genuine repeated stall still fails once attempts are exhausted. + full_args = [sys.executable, cli.__file__] + args + result = None + for attempt in range(1, attempts + 1): + try: + return subprocess.run( + full_args, + cwd=cwd, + capture_output=True, + timeout=timeout, + env=self._subprocess_env(), + ) + except subprocess.TimeoutExpired: + result = subprocess.CompletedProcess( + args=full_args, + returncode=-1, + stdout=b"", + stderr=( + f"Command timed out after {timeout}s " + f"(attempt {attempt}/{attempts}): {args}" + ).encode(), + ) + return result def run_with_output(self, args: List[str], cwd: Path) -> Tuple[int, bytes]: + is_teardown = "teardown" in args + # Use subprocess.Popen + communicate(timeout=...) so that on a hang we can + # kill the process and still recover any output already buffered in the pipe. + # This matters when feast prints an error and then hangs in the Dask atexit + # handler — the error text is already in the pipe buffer and can be read after + # the process is killed. + timeout = 120 if is_teardown else 60 + + proc = subprocess.Popen( + [sys.executable, cli.__file__] + args, + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=self._subprocess_env(), + ) try: - return ( - 0, - subprocess.check_output( - [sys.executable, cli.__file__] + args, - cwd=cwd, - stderr=subprocess.STDOUT, - ), - ) - except subprocess.CalledProcessError as e: - return e.returncode, e.output + stdout, _ = proc.communicate(timeout=timeout) + returncode = proc.returncode + if returncode != 0: + return returncode, stdout + return 0, stdout + except subprocess.TimeoutExpired: + proc.kill() + stdout, _ = proc.communicate() + if is_teardown: + return ( + -1, + b"Teardown timed out (known PySpark JVM cleanup issue on macOS)", + ) + else: + # Return partial output (likely contains the error printed before hang) + # with a non-zero returncode so callers can inspect it. + return -1, stdout or b"" @contextmanager def local_repo( @@ -73,15 +159,18 @@ def local_repo( repo_path = Path(repo_dir_name) data_path = Path(data_dir_name) + registry_path_yaml = str(data_path / "registry.db") + online_store_path_yaml = str(data_path / "online_store.db") + repo_config = repo_path / "feature_store.yaml" if online_store == "sqlite": yaml_config = dedent( f""" project: {project_id} - registry: {data_path / "registry.db"} + registry: {registry_path_yaml} provider: local online_store: - path: {data_path / "online_store.db"} + path: {online_store_path_yaml} offline_store: type: {offline_store} entity_key_serialization_version: 3 @@ -91,10 +180,10 @@ def local_repo( yaml_config = dedent( f""" project: {project_id} - registry: {data_path / "registry.db"} + registry: {registry_path_yaml} provider: local online_store: - path: {data_path / "online_store.db"} + path: {online_store_path_yaml} type: milvus vector_enabled: true embedding_dim: 10 @@ -103,6 +192,19 @@ def local_repo( entity_key_serialization_version: 3 """ ) + elif online_store: # Added for mongodb, but very general + yaml_config = dedent( + f""" + project: {project_id} + registry: {registry_path_yaml} + provider: local + online_store: + type: {online_store} + offline_store: + type: {offline_store} + entity_key_serialization_version: 3 + """ + ) else: pass @@ -121,14 +223,13 @@ def local_repo( f"stdout: {result.stdout}\nstderr: {result.stderr}" ) - yield FeatureStore(repo_path=str(repo_path), config=None) + store_instance = FeatureStore(repo_path=str(repo_path), config=None) + yield store_instance if teardown: - result = self.run(["teardown"], cwd=repo_path) - stdout = result.stdout.decode("utf-8") - stderr = result.stderr.decode("utf-8") - print(f"Apply stdout:\n{stdout}") - print(f"Apply stderr:\n{stderr}") - assert result.returncode == 0, ( - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) + # Use in-process teardown instead of a 'feast teardown' subprocess. + # Subprocess teardown adds per-repo startup overhead and risks + # blocking indefinitely in Dask/PySpark atexit handlers, which + # can push the cumulative test time past the pytest timeout budget. + # store.teardown() performs the same SQLite/registry cleanup directly. + store_instance.teardown() diff --git a/sdk/python/tests/utils/e2e_test_validation.py b/sdk/python/tests/utils/e2e_test_validation.py index 6d887b8bb07..c89d61b9d5a 100644 --- a/sdk/python/tests/utils/e2e_test_validation.py +++ b/sdk/python/tests/utils/e2e_test_validation.py @@ -10,22 +10,16 @@ from feast import FeatureStore, FeatureView, RepoConfig from feast.utils import _utc_now -from tests.integration.feature_repos.integration_test_repo_config import ( +from tests.universal.feature_repos.integration_test_repo_config import ( IntegrationTestRepoConfig, ) -from tests.integration.feature_repos.universal.data_source_creator import ( +from tests.universal.feature_repos.universal.data_source_creator import ( DataSourceCreator, ) -from tests.integration.feature_repos.universal.data_sources.bigquery import ( - BigQueryDataSourceCreator, -) -from tests.integration.feature_repos.universal.data_sources.file import ( +from tests.universal.feature_repos.universal.data_sources.file import ( FileDataSourceCreator, FileParquetDatasetSourceCreator, ) -from tests.integration.feature_repos.universal.data_sources.redshift import ( - RedshiftDataSourceCreator, -) def validate_offline_online_store_consistency( @@ -225,6 +219,13 @@ def make_feature_store_yaml( # Only test if this is NOT a local test if os.getenv("FEAST_IS_LOCAL_TEST", "False") != "True": + from tests.universal.feature_repos.universal.data_sources.bigquery import ( + BigQueryDataSourceCreator, + ) + from tests.universal.feature_repos.universal.data_sources.redshift import ( + RedshiftDataSourceCreator, + ) + NULLABLE_ONLINE_STORE_CONFIGS.extend( [ IntegrationTestRepoConfig( diff --git a/sdk/python/tests/utils/rag_test_utils.py b/sdk/python/tests/utils/rag_test_utils.py index e3aac2546bd..675fa28b4a5 100644 --- a/sdk/python/tests/utils/rag_test_utils.py +++ b/sdk/python/tests/utils/rag_test_utils.py @@ -92,7 +92,9 @@ def run_side_effect(cmd, *args, **kwargs): with runner.local_repo( get_example_repo("example_feature_repo_1.py"), offline_store="file", - online_store="milvus", + # The vector store tests below use MockVectorStore and only need the + # feature view registered; avoid Milvus Lite index setup flakiness. + online_store="sqlite", apply=False, teardown=True, ) as store: diff --git a/sdk/python/tests/utils/ssl_certifcates_util.py b/sdk/python/tests/utils/ssl_certifcates_util.py index 53b9df3973c..4525e0249c9 100644 --- a/sdk/python/tests/utils/ssl_certifcates_util.py +++ b/sdk/python/tests/utils/ssl_certifcates_util.py @@ -15,6 +15,97 @@ logger = logging.getLogger(__name__) +def generate_mtls_certs( + output_dir: str, + server_san: str = "feature-registry.example.com", +) -> dict[str, str]: + """ + Generate a CA, server, and client certificate chain for mTLS testing. + + The server cert's SAN is set to ``server_san`` (no ``localhost``), so a + gRPC client connecting to ``localhost`` must set the ``authority`` channel + option to ``server_san`` — exactly the IAP-tunnel pattern. + + Returns a dict with keys: ca_cert, server_key, server_cert, + client_key, client_cert. + """ + os.makedirs(output_dir, exist_ok=True) + paths = { + "ca_cert": os.path.join(output_dir, "ca.crt"), + "server_key": os.path.join(output_dir, "server.key"), + "server_cert": os.path.join(output_dir, "server.crt"), + "client_key": os.path.join(output_dir, "client.key"), + "client_cert": os.path.join(output_dir, "client.crt"), + } + + # --- CA (self-signed) --- + ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "TestCA")]) + ca_cert = ( + x509.CertificateBuilder() + .subject_name(ca_name) + .issuer_name(ca_name) + .public_key(ca_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.utcnow()) + .not_valid_after(datetime.utcnow() + timedelta(days=1)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .sign(ca_key, hashes.SHA256()) + ) + with open(paths["ca_cert"], "wb") as f: + f.write(ca_cert.public_bytes(serialization.Encoding.PEM)) + + def _issue_cert( + cn: str, + key_path: str, + cert_path: str, + san: x509.SubjectAlternativeName | None = None, + ): + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) + builder = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(ca_cert.subject) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.utcnow()) + .not_valid_after(datetime.utcnow() + timedelta(days=1)) + ) + if san is not None: + builder = builder.add_extension(san, critical=False) + cert = builder.sign(ca_key, hashes.SHA256()) + + with open(key_path, "wb") as f: + f.write( + key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.NoEncryption(), + ) + ) + with open(cert_path, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + + # --- Server cert --- + _issue_cert( + cn=server_san, + key_path=paths["server_key"], + cert_path=paths["server_cert"], + san=x509.SubjectAlternativeName([x509.DNSName(server_san)]), + ) + + # --- Client cert (no SAN needed) --- + _issue_cert( + cn="TestClient", + key_path=paths["client_key"], + cert_path=paths["client_cert"], + ) + + logger.info(f"mTLS certificates generated in {output_dir}") + return paths + + def generate_self_signed_cert( cert_path="cert.pem", key_path="key.pem", common_name="localhost" ): diff --git a/sdk/python/tests/utils/type_test_utils.py b/sdk/python/tests/utils/type_test_utils.py new file mode 100644 index 00000000000..5abf069b928 --- /dev/null +++ b/sdk/python/tests/utils/type_test_utils.py @@ -0,0 +1,103 @@ +from dataclasses import dataclass +from typing import List, Optional + +from feast.types import ( + Array, + Bool, + FeastType, + Float32, + Int32, + Int64, + UnixTimestamp, +) +from tests.data.data_creator import create_basic_driver_dataset +from tests.universal.feature_repos.universal.feature_views import driver_feature_view + + +@dataclass(frozen=True, repr=True) +class TypeTestConfig: + feature_dtype: str + feature_is_list: bool + has_empty_list: bool + + +def get_feast_type(feature_dtype: str, feature_is_list: bool) -> FeastType: + dtype: Optional[FeastType] = None + if feature_is_list is True: + if feature_dtype == "int32": + dtype = Array(Int32) + elif feature_dtype == "int64": + dtype = Array(Int64) + elif feature_dtype == "float": + dtype = Array(Float32) + elif feature_dtype == "bool": + dtype = Array(Bool) + elif feature_dtype == "datetime": + dtype = Array(UnixTimestamp) + else: + if feature_dtype == "int32": + dtype = Int32 + elif feature_dtype == "int64": + dtype = Int64 + elif feature_dtype == "float": + dtype = Float32 + elif feature_dtype == "bool": + dtype = Bool + elif feature_dtype == "datetime": + dtype = UnixTimestamp + assert dtype + return dtype + + +def populate_test_configs(): + feature_dtypes = [ + "int32", + "int64", + "float", + "bool", + "datetime", + ] + configs: List[TypeTestConfig] = [] + for feature_dtype in feature_dtypes: + for feature_is_list in [True, False]: + for has_empty_list in [True, False]: + # For non list features `has_empty_list` does nothing + if feature_is_list is False and has_empty_list is True: + continue + + configs.append( + TypeTestConfig( + feature_dtype=feature_dtype, + feature_is_list=feature_is_list, + has_empty_list=has_empty_list, + ) + ) + return configs + + +def get_type_test_fixtures(request, environment): + config: TypeTestConfig = request.param + # Lower case needed because Redshift lower-cases all table names + destination_name = ( + f"feature_type_{config.feature_dtype}{config.feature_is_list}".replace( + ".", "" + ).lower() + ) + df = create_basic_driver_dataset( + Int64, + config.feature_dtype, + config.feature_is_list, + config.has_empty_list, + ) + data_source = environment.data_source_creator.create_data_source( + df, + destination_name=destination_name, + field_mapping={"ts_1": "ts"}, + ) + fv = driver_feature_view( + data_source=data_source, + name=destination_name, + dtype=get_feast_type(config.feature_dtype, config.feature_is_list), + ) + + return config, data_source, fv diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf13daa..00000000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index ac3beaa21bd..00000000000 --- a/setup.py +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright 2019 The Feast Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import glob -import os -import pathlib -import re -import shutil -import subprocess -from subprocess import CalledProcessError -import sys -from pathlib import Path - -from setuptools import find_packages, setup, Command - -NAME = "feast" -DESCRIPTION = "Python SDK for Feast" -URL = "https://github.com/feast-dev/feast" -AUTHOR = "Feast" -REQUIRES_PYTHON = ">=3.10.0" - -REQUIRED = [ - "click>=7.0.0,<9.0.0", - "colorama>=0.3.9,<1", - "dill~=0.3.0", - "protobuf>=4.24.0", - "Jinja2>=2,<4", - "jsonschema", - "mmh3", - "numpy>=2.0.0,<3", - "pandas>=1.4.3,<3", - "pyarrow<=21.0.0", - "pydantic>=2.0.0", - "pygments>=2.12.0,<3", - "PyYAML>=5.4.0,<7", - "requests", - "SQLAlchemy[mypy]>1", - "tabulate>=0.8.0,<1", - "tenacity>=7,<9", - "toml>=0.10.0,<1", - "tqdm>=4,<5", - "typeguard>=4.0.0", - "fastapi>=0.68.0", - "uvicorn[standard]>=0.30.6,<=0.34.0", - "uvicorn-worker", - "gunicorn; platform_system != 'Windows'", - "dask[dataframe]>=2024.2.1", - "prometheus_client", - "psutil", - "bigtree>=0.19.2", - "pyjwt", -] - -GCP_REQUIRED = [ - "google-api-core>=1.23.0,<3", - "googleapis-common-protos>=1.52.0,<2", - "google-cloud-bigquery[pandas]>=2,<4", - "google-cloud-bigquery-storage >= 2.0.0,<3", - "google-cloud-datastore>=2.16.0,<3", - "google-cloud-storage>=1.34.0,<3", - "google-cloud-bigtable>=2.11.0,<3", - "fsspec<=2024.9.0", -] - -REDIS_REQUIRED = [ - "redis>=4.2.2,<5", - "hiredis>=2.0.0,<3", -] - -AWS_REQUIRED = ["boto3==1.38.27", "fsspec<=2024.9.0", "aiobotocore>2,<3"] - -KUBERNETES_REQUIRED = ["kubernetes"] - -SNOWFLAKE_REQUIRED = [ - "snowflake-connector-python[pandas]>=3.7,<4", -] - -SPARK_REQUIRED = [ - "pyspark>=4.0.0", -] - -SQLITE_VEC_REQUIRED = [ - "sqlite-vec==v0.1.6", -] -TRINO_REQUIRED = ["trino>=0.305.0,<0.400.0", "regex"] - -POSTGRES_REQUIRED = [ - "psycopg[binary,pool]>=3.0.0,<4", -] -POSTGRES_C_REQUIRED = [ - "psycopg[c,pool]>=3.0.0,<4", -] - -OPENTELEMETRY = ["prometheus_client", "psutil"] - -MYSQL_REQUIRED = ["pymysql", "types-PyMySQL"] - -HBASE_REQUIRED = [ - "happybase>=1.2.0,<3", -] - -CASSANDRA_REQUIRED = [ - "cassandra-driver>=3.24.0,<4", -] - -GE_REQUIRED = ["great_expectations>=0.15.41,<1"] - -AZURE_REQUIRED = [ - "azure-storage-blob>=0.37.0", - "azure-identity>=1.6.1", - "SQLAlchemy>=1.4.19", - "pyodbc>=4.0.30", - "pymssql", -] - -IKV_REQUIRED = [ - "ikvpy>=0.0.36", -] - -HAZELCAST_REQUIRED = [ - "hazelcast-python-client>=5.1", -] - -IBIS_REQUIRED = [ - "ibis-framework>=9.0.0,<10", - "ibis-substrait>=4.0.0", -] - -GRPCIO_REQUIRED = [ - "grpcio>=1.56.2,<=1.62.3", - "grpcio-reflection>=1.56.2,<=1.62.3", - "grpcio-health-checking>=1.56.2,<=1.62.3", -] - -DUCKDB_REQUIRED = ["ibis-framework[duckdb]>=9.0.0,<10"] - -DELTA_REQUIRED = ["deltalake<1.0.0"] - -DOCLING_REQUIRED = ["docling>=2.23.0"] - -ELASTICSEARCH_REQUIRED = ["elasticsearch>=8.13.0"] - -SINGLESTORE_REQUIRED = ["singlestoredb<1.8.0"] - -COUCHBASE_REQUIRED = [ - "couchbase==4.3.2", - "couchbase-columnar==1.0.0" -] - -MSSQL_REQUIRED = ["ibis-framework[mssql]>=9.0.0,<10"] - -FAISS_REQUIRED = ["faiss-cpu>=1.7.0,<=1.10.0"] -QDRANT_REQUIRED = ["qdrant-client>=1.12.0"] - -GO_REQUIRED = ["cffi>=1.15.0"] - -MILVUS_REQUIRED = ["pymilvus"] - -TORCH_REQUIRED = [ - "torch>=2.7.0", - "torchvision>=0.22.1", -] - -CLICKHOUSE_REQUIRED = ["clickhouse-connect>=0.7.19"] - -MCP_REQUIRED = ["fastapi_mcp"] - -RAG_REQUIRED = [ - "transformers>=4.36.0", - "datasets>=3.6.0", -] - -IMAGE_REQUIRED = [ - "timm>=0.6.0", - "Pillow>=8.0.0", - "scikit-learn>=1.0.0", -] + TORCH_REQUIRED - -RAY_REQUIRED = [ - "ray>=2.47.0; python_version == '3.10'", - 'codeflare-sdk>=0.31.1; python_version != "3.10"', - ] - -CI_REQUIRED = ( - [ - "build", - "virtualenv==20.23.0", - "cryptography>=43.0,<44", - "ruff>=0.8.0", - "mypy-protobuf>=3.1", - "grpcio-tools>=1.56.2,<=1.62.3", - "grpcio-testing>=1.56.2,<=1.62.3", - # FastAPI does not correctly pull starlette dependency on httpx see thread(https://github.com/tiangolo/fastapi/issues/5656). - "httpx==0.27.2", - "minio==7.2.11", - "mock==2.0.0", - "moto==4.2.14", - "mypy>=1.4.1,<1.11.3", - "urllib3>=1.25.4,<3", - "psutil==5.9.0", - "py>=1.11.0", # https://github.com/pytest-dev/pytest/issues/10420 - "pytest>=6.0.0,<8", - "pytest-asyncio<=0.24.0", - "pytest-cov", - "pytest-xdist", - "pytest-benchmark>=3.4.1,<4", - "pytest-lazy-fixture==0.6.3", - "pytest-timeout==1.4.2", - "pytest-ordering~=0.6.0", - "pytest-mock==1.10.4", - "pytest-env", - "Sphinx>4.0.0,<7", - "testcontainers==4.9.0", - "python-keycloak==4.2.2", - "pre-commit<3.3.2", - "assertpy==1.1", - "pip-tools", - "pybindgen", - "types-protobuf~=3.19.22", - "types-python-dateutil", - "types-pytz", - "types-PyYAML", - "types-redis", - "types-requests<2.31.0", - "types-setuptools", - "types-tabulate", - "virtualenv<20.24.2", - ] - + GCP_REQUIRED - + REDIS_REQUIRED - + AWS_REQUIRED - + KUBERNETES_REQUIRED - + SNOWFLAKE_REQUIRED - + SPARK_REQUIRED - + POSTGRES_REQUIRED - + MYSQL_REQUIRED - + TRINO_REQUIRED - + GE_REQUIRED - + HBASE_REQUIRED - + CASSANDRA_REQUIRED - + AZURE_REQUIRED - + HAZELCAST_REQUIRED - + IBIS_REQUIRED - + GRPCIO_REQUIRED - + DUCKDB_REQUIRED - + DELTA_REQUIRED - + ELASTICSEARCH_REQUIRED - + SQLITE_VEC_REQUIRED - + SINGLESTORE_REQUIRED - + COUCHBASE_REQUIRED - + OPENTELEMETRY - + FAISS_REQUIRED - + QDRANT_REQUIRED - + MILVUS_REQUIRED - + DOCLING_REQUIRED - + TORCH_REQUIRED - + CLICKHOUSE_REQUIRED - + MCP_REQUIRED - + RAG_REQUIRED - + IMAGE_REQUIRED - + RAY_REQUIRED -) -MINIMAL_REQUIRED = ( - GCP_REQUIRED - + AWS_REQUIRED - + REDIS_REQUIRED - + KUBERNETES_REQUIRED - + SNOWFLAKE_REQUIRED - + POSTGRES_C_REQUIRED - + MYSQL_REQUIRED - + GO_REQUIRED - + GRPCIO_REQUIRED - + DUCKDB_REQUIRED - + OPENTELEMETRY - + MILVUS_REQUIRED -) -NLP_REQUIRED = ( - DOCLING_REQUIRED - + MILVUS_REQUIRED - + TORCH_REQUIRED - + RAG_REQUIRED - + IMAGE_REQUIRED -) -DOCS_REQUIRED = CI_REQUIRED -DEV_REQUIRED = CI_REQUIRED - -# Get git repo root directory -repo_root = str(pathlib.Path(__file__).resolve().parent) - -# README file from Feast repo root directory -README_FILE = os.path.join(repo_root, "README.md") -with open(README_FILE, "r", encoding="utf8") as f: - LONG_DESCRIPTION = f.read() - -# Add Support for parsing tags that have a prefix containing '/' (ie 'sdk/go') to setuptools_scm. -# Regex modified from default tag regex in: -# https://github.com/pypa/setuptools_scm/blob/2a1b46d38fb2b8aeac09853e660bcd0d7c1bc7be/src/setuptools_scm/config.py#L9 -TAG_REGEX = re.compile( - r"^(?:[\/\w-]+)?(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$" -) - -# Only set use_scm_version if git executable exists (setting this variable causes pip to use git under the hood) -if shutil.which("git"): - use_scm_version = {"root": ".", "relative_to": __file__, "tag_regex": TAG_REGEX} -else: - use_scm_version = None - -PYTHON_CODE_PREFIX = "sdk/python" - - -setup( - name=NAME, - author=AUTHOR, - description=DESCRIPTION, - long_description=LONG_DESCRIPTION, - long_description_content_type="text/markdown", - python_requires=REQUIRES_PYTHON, - url=URL, - packages=find_packages( - where=PYTHON_CODE_PREFIX, exclude=("java", "infra", "sdk/python/tests", "ui") - ), - package_dir={"": PYTHON_CODE_PREFIX}, - install_requires=REQUIRED, - extras_require={ - "dev": DEV_REQUIRED, - "ci": CI_REQUIRED, - "minimal": MINIMAL_REQUIRED, - "gcp": GCP_REQUIRED, - "aws": AWS_REQUIRED, - "k8s": KUBERNETES_REQUIRED, - "redis": REDIS_REQUIRED, - "snowflake": SNOWFLAKE_REQUIRED, - "spark": SPARK_REQUIRED, - "trino": TRINO_REQUIRED, - "postgres": POSTGRES_REQUIRED, - "postgres-c": POSTGRES_C_REQUIRED, - "azure": AZURE_REQUIRED, - "mysql": MYSQL_REQUIRED, - "mssql": MSSQL_REQUIRED, - "ge": GE_REQUIRED, - "hbase": HBASE_REQUIRED, - "docs": DOCS_REQUIRED, - "cassandra": CASSANDRA_REQUIRED, - "hazelcast": HAZELCAST_REQUIRED, - "grpcio": GRPCIO_REQUIRED, - "ibis": IBIS_REQUIRED, - "duckdb": DUCKDB_REQUIRED, - "ikv": IKV_REQUIRED, - "delta": DELTA_REQUIRED, - "elasticsearch": ELASTICSEARCH_REQUIRED, - "sqlite_vec": SQLITE_VEC_REQUIRED, - "singlestore": SINGLESTORE_REQUIRED, - "couchbase": COUCHBASE_REQUIRED, - "opentelemetry": OPENTELEMETRY, - "faiss": FAISS_REQUIRED, - "qdrant": QDRANT_REQUIRED, - "go": GO_REQUIRED, - "milvus": MILVUS_REQUIRED, - "docling": DOCLING_REQUIRED, - "pytorch": TORCH_REQUIRED, - "nlp": NLP_REQUIRED, - "clickhouse": CLICKHOUSE_REQUIRED, - "mcp": MCP_REQUIRED, - "rag": RAG_REQUIRED, - "image": IMAGE_REQUIRED, - "ray": RAY_REQUIRED, - }, - include_package_data=True, - license="Apache", - classifiers=[ - # Trove classifiers - # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - ], - entry_points={"console_scripts": ["feast=feast.cli.cli:cli"]}, - use_scm_version=use_scm_version, - setup_requires=[ - "pybindgen==0.22.0", # TODO do we need this? - "setuptools_scm>=6.2", # TODO do we need this? - ], -) diff --git a/skills/SKILL.md b/skills/SKILL.md new file mode 100644 index 00000000000..8be173257ac --- /dev/null +++ b/skills/SKILL.md @@ -0,0 +1,253 @@ +--- +name: feast-user-guide +description: Guide for working with Feast (Feature Store) — defining features, configuring feature_store.yaml, retrieving features online/offline, using the CLI, and building RAG retrieval pipelines. Use when the user asks about creating entities, feature views, on-demand feature views, stream feature views, feature services, data sources, feature_store.yaml configuration, feast apply/materialize commands, online or historical feature retrieval, or vector-based document retrieval with Feast. +license: Apache-2.0 +compatibility: Works with Claude Code, OpenAI Codex, and any Agent Skills compatible tool. +metadata: + author: feast-dev + version: "1.0" +--- + +# Feast User Guide + +## Quick Start + +A Feast project requires: +1. A `feature_store.yaml` config file +2. Python files defining entities, data sources, feature views, and feature services +3. Running `feast apply` to register definitions + +```bash +feast init my_project +cd my_project +feast apply +``` + +## Core Concepts + +### Entity +An entity is a collection of semantically related features (e.g., a customer, a driver). Entities have join keys used to look up features. + +```python +from feast import Entity +from feast.value_type import ValueType + +driver = Entity( + name="driver_id", + description="Driver identifier", + value_type=ValueType.INT64, +) +``` + +### Data Sources +Data sources describe where raw feature data lives. + +```python +from feast import FileSource, BigQuerySource, KafkaSource, PushSource, RequestSource +from feast.data_format import ParquetFormat + +# Batch source (file) +driver_stats_source = FileSource( + name="driver_stats_source", + path="data/driver_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +# Request source (for on-demand features) +input_request = RequestSource( + name="vals_to_add", + schema=[Field(name="val_to_add", dtype=Float64)], +) +``` + +### FeatureView +Maps features from a data source to entities with a schema, TTL, and online/offline settings. + +```python +from feast import FeatureView, Field +from feast.types import Float32, Int64, String +from datetime import timedelta + +driver_hourly_stats = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=365), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, +) +``` + +### OnDemandFeatureView +Computes features at request time from other feature views and/or request data. + +```python +from feast import on_demand_feature_view +import pandas as pd + +@on_demand_feature_view( + sources=[driver_hourly_stats, input_request], + schema=[Field(name="conv_rate_plus_val", dtype=Float64)], + mode="pandas", +) +def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val"] = inputs["conv_rate"] + inputs["val_to_add"] + return df +``` + +### FeatureService +Groups features from multiple views for retrieval. + +```python +from feast import FeatureService + +driver_fs = FeatureService( + name="driver_ranking", + features=[driver_hourly_stats, transformed_conv_rate], +) +``` + +## Feature Retrieval + +### Online (low-latency) +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +features = store.get_online_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + ], + entity_rows=[{"driver_id": 1001}, {"driver_id": 1002}], +).to_dict() +``` + +### Historical (training data with point-in-time joins) +```python +entity_df = pd.DataFrame({ + "driver_id": [1001, 1002], + "event_timestamp": [datetime(2023, 1, 1), datetime(2023, 1, 2)], +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], +).to_df() +``` + +Or use a FeatureService: +```python +training_df = store.get_historical_features( + entity_df=entity_df, + features=driver_fs, +).to_df() +``` + +## Materialization + +Load features from offline store into online store: + +```bash +# Full materialization over a time range +feast materialize 2023-01-01T00:00:00 2023-12-31T23:59:59 + +# Incremental (from last materialized timestamp) +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +Python API: +```python +from datetime import datetime +store.materialize(start_date=datetime(2023, 1, 1), end_date=datetime(2023, 12, 31)) +store.materialize_incremental(end_date=datetime.utcnow()) +``` + +## CLI Commands + +| Command | Purpose | +|---------|---------| +| `feast init [DIR]` | Create new feature repository | +| `feast apply` | Register/update feature definitions | +| `feast plan` | Preview changes without applying | +| `feast materialize START END` | Materialize features to online store | +| `feast materialize-incremental END` | Incremental materialization | +| `feast entities list` | List registered entities | +| `feast feature-views list` | List feature views | +| `feast feature-services list` | List feature services | +| `feast on-demand-feature-views list` | List on-demand feature views | +| `feast teardown` | Remove infrastructure resources | +| `feast version` | Show SDK version | + +Options: `--chdir` / `-c` (run in different directory), `--feature-store-yaml` / `-f` (override config path). + +## Vector Search / RAG + +Define a feature view with vector fields for similarity search: + +```python +from feast.types import Array, Float32 + +wiki_passages = FeatureView( + name="wiki_passages", + entities=[passage_entity], + schema=[ + Field(name="passage_text", dtype=String), + Field( + name="embedding", + dtype=Array(Float32), + vector_index=True, + vector_length=384, + vector_search_metric="COSINE", + ), + ], + source=passages_source, + online=True, +) +``` + +Retrieve similar documents: +```python +results = store.retrieve_online_documents( + feature="wiki_passages:embedding", + query=query_embedding, + top_k=5, +) +``` + +## feature_store.yaml Minimal Config + +```yaml +project: my_project +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db +``` + +## Common Imports + +```python +from feast import ( + Entity, FeatureView, OnDemandFeatureView, FeatureService, + Field, FileSource, RequestSource, FeatureStore, +) +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Float32, Float64, Int64, String, Bool, Array +from feast.value_type import ValueType +from datetime import timedelta +``` + +## Detailed References + +- **Feature definitions** (all types, parameters, patterns): See [references/feature-definitions.md](references/feature-definitions.md) +- **Configuration** (feature_store.yaml, all store types, auth): See [references/configuration.md](references/configuration.md) +- **Retrieval & RAG** (online/offline retrieval, vector search, RAG retriever): See [references/retrieval-and-rag.md](references/retrieval-and-rag.md) diff --git a/skills/feast-architecture/SKILL.md b/skills/feast-architecture/SKILL.md new file mode 100644 index 00000000000..5a6049d237b --- /dev/null +++ b/skills/feast-architecture/SKILL.md @@ -0,0 +1,383 @@ +--- +name: feast-architecture +description: Internals of the Feast codebase — how each component works, where the key abstractions live, and the data flow through the system. Use when asked how feast apply works, how the registry stores data, how materialization moves data, how get_online_features retrieves features, how the feature server works, how the Kubernetes operator manages deployments, or when navigating the codebase to understand where to make a change. +license: Apache-2.0 +compatibility: Works with Claude Code, OpenAI Codex, and any Agent Skills compatible tool. +metadata: + author: feast-dev + version: "1.0" +--- + +# Feast Architecture Internals + +## Full Component Map + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Feast Deployment Modes │ +│ │ +│ Local / Python SDK Kubernetes (feast-operator) │ +│ ───────────────── ───────────────────────────────── │ +│ feature_store.yaml ←── FeatureStore CR (CRD) │ +│ │ │ │ +│ ▼ ▼ │ +│ FeatureStore (Python) Operator deploys services: │ +│ ├── Registry - feature-server (Go or Python) │ +│ ├── Provider - offline-store-server │ +│ │ ├── OnlineStore - registry-server │ +│ │ └── OfflineStore + manages feature_store.yaml config │ +│ └── FeatureServer │ +│ (Python FastAPI or │ +│ Go gRPC/HTTP) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Python SDK Core + +### FeatureStore — the orchestrator + +**File**: `sdk/python/feast/feature_store.py` + +`FeatureStore` is the single entry point for all operations. It never reads/writes data +directly — it delegates to the registry (for metadata) and the provider (for infrastructure +and data movement). + +```python +FeatureStore(repo_path=".") # loads feature_store.yaml +store.apply(objects) # register feature definitions +store.materialize(...) # offline → online +store.get_online_features(...) # serve +store.get_historical_features(...) # training data +``` + +**File**: `sdk/python/feast/repo_config.py` — parses `feature_store.yaml` into typed `RepoConfig`. +All component classes (online store, offline store, registry) are loaded dynamically from the +`type:` string via `repo_config.ONLINE_STORE_TYPE_MAP` / `OFFLINE_STORE_TYPE_MAP`. + +--- + +### Registry + +**Purpose**: Metadata store — persists definitions of entities, feature views, data sources, +feature services, permissions. + +**Backends and their files:** + +| Backend | File | Notes | +|---|---|---| +| File/GCS/S3 (default) | `infra/registry/registry.py` | Single proto blob, cached in memory | +| SQL | `infra/registry/sql.py` | Per-object tables via SQLAlchemy | +| Snowflake | `infra/registry/snowflake.py` | Snowflake tables | +| Remote | `infra/registry/remote.py` | Delegates to a remote registry server over gRPC | + +**How the proto/file backend works:** +1. All metadata is serialized into one `Registry` protobuf (`protos/feast/core/Registry.proto`) +2. The proto blob is stored at the configured `registry:` path +3. In-memory `cached_registry_proto` is refreshed on a TTL (default 10s) +4. Writes re-serialize and overwrite the full blob — no partial updates + +**Key pattern — apply:** +```python +# Python object → proto → stored in registry blob +registry.apply_feature_view(feature_view, project) +# → feature_view.to_proto() +# → upserts into cached_registry_proto.feature_views +# → registry_store.update_registry_proto(proto) +``` + +**How the SQL backend works:** +- Per-object tables, each storing that object's serialized proto in a binary column. +- **Binary proto columns must use `ProtoBytes`, not `LargeBinary` directly** (defined at the top of `infra/registry/sql.py`). `ProtoBytes` emits `LONGBLOB` on MySQL and MariaDB and falls back to `LargeBinary`'s default on every other dialect (`BLOB` on SQLite, `BYTEA` on PostgreSQL). Plain `LargeBinary` maps to MySQL `BLOB` (64 KB cap), which silently truncates large protos (e.g. a `FeatureView`) and later fails to deserialize. Any new serialized-proto/blob-metadata column reintroduces that bug if it uses `LargeBinary`. +- All tables are declared on the module-level `metadata` object in `sql.py` (the source of truth). `metadata.create_all` only creates missing tables — it never widens existing columns, so schema changes to existing registries require a manual migration (see `docs/reference/registries/sql.md`). On MySQL/MariaDB, `SqlRegistry._warn_if_narrow_blob_columns` logs an error at startup for any registry proto column still typed as the narrow `BLOB` — a new column is covered automatically as long as it's typed `ProtoBytes` (the diagnostic selects columns by `column.type is ProtoBytes`); a column typed as plain `LargeBinary` would be silently missed. + +**Supporting files:** +- `infra/registry/base_registry.py` — abstract interface +- `infra/registry/proto_registry_utils.py` — proto serialization helpers +- `infra/registry/caching_registry.py` — adds TTL caching on top of any backend + +--- + +### Provider + +**Purpose**: Infrastructure lifecycle — creates/updates/tears down online store tables. +Also dispatches `online_write_batch` and `get_historical_features`. + +**File**: `sdk/python/feast/infra/provider.py` + +Built-in providers (set via `provider:` in `feature_store.yaml`): +- `local` — SQLite online store, file offline (dev default) +- `gcp` — Datastore/Bigtable online, BigQuery offline +- `aws` — DynamoDB online, Redshift offline + +Custom providers extend `Provider` and override `update_infra` / `teardown_infra`. + +--- + +### Online Store + +**Purpose**: Low-latency feature serving. Stores the latest feature values per entity key. + +**Interface**: `sdk/python/feast/infra/online_stores/online_store.py` +**Implementations**: `sdk/python/feast/infra/online_stores/` (redis, dynamodb, sqlite, bigtable, postgres, snowflake, …) + +Key methods: +- `online_write_batch` — write entity→feature values +- `online_read` — read by entity keys +- `update` — provision/deprovision tables on `feast apply` +- `teardown` — clean up on `feast teardown` + +--- + +### Offline Store + +**Purpose**: Historical feature retrieval and training data generation (point-in-time joins). + +**Interface**: `sdk/python/feast/infra/offline_stores/offline_store.py` +**Implementations**: `sdk/python/feast/infra/offline_stores/` (bigquery, snowflake, redshift, duckdb, file, …) + +Returns a `RetrievalJob` (lazy) — no data moves until `.to_df()` or `.to_arrow()` is called. + +PIT join logic (shared): `sdk/python/feast/infra/offline_stores/offline_utils.py` + +**Key methods to implement for a new backend:** +```python +class MyOfflineStore(OfflineStore): + def get_historical_features(self, config, feature_views, feature_refs, + entity_df, registry, project, ...) -> RetrievalJob: ... + def pull_latest_from_table_or_query(self, config, data_source, + join_key_columns, feature_name_columns, + timestamp_field, created_timestamp_column, + start_date, end_date) -> RetrievalJob: ... + def pull_all_from_table_or_query(self, config, data_source, join_key_columns, + feature_name_columns, timestamp_field, + start_date, end_date) -> RetrievalJob: ... + def write_logged_features(self, config, data, source, logging_config, + registry) -> None: ... # optional +``` + +**Config class**: subclass `FeastConfigBaseModel` with a `type` Literal (short alias + full dotted path). Register in `OFFLINE_STORE_TYPE_MAP` in `sdk/python/feast/repo_config.py`. + +**Data source**: each offline store backend pairs with a `DataSource` subclass (e.g. `BigQuerySource`, `FileSource`). Add it to `sdk/python/feast/data_sources/` and register in `DATA_SOURCE_CLASS_FOR_TYPE`. + +--- + +## Data Flows + +### `feast apply` +``` +feast apply (CLI → repo_operations.py) + ├── Parse Python files → collect FeastObjects + ├── store.apply(objects) + │ ├── diff against registry (diff/registry_diff.py) + │ ├── update registry metadata for changed objects + │ └── provider.update_infra(tables_to_keep, tables_to_delete) + │ └── online_store.update(...) ← create/drop tables + └── Write updated registry to storage +``` + +### `feast materialize` +``` +store.materialize(start_date, end_date) + ├── Load feature views from registry + ├── For each feature view: + │ ├── offline_store.pull_latest_from_table_or_query(...) + │ │ └── Returns RetrievalJob (lazy) + │ ├── job.to_arrow() ← executes query, fetches Arrow table + │ └── provider.online_write_batch(...) + │ └── online_store.online_write_batch(config, table, data, progress) + └── Update last_updated_timestamp in registry +``` + +### `get_online_features` +``` +store.get_online_features(features, entity_rows) + ├── Resolve feature refs → FeatureViews from registry + ├── online_store.online_read(config, table, entity_rows, requested_features) + │ └── Deserialize ValueProto → Python dict + ├── Apply OnDemandFeatureView transformations (if any) + └── Return OnlineFeaturesResponse +``` + +### `get_historical_features` +``` +store.get_historical_features(entity_df, features) + ├── Resolve feature refs → FeatureViews from registry + ├── offline_store.get_historical_features(config, feature_views, entity_df) + │ └── Point-in-time join: + │ for each entity row, find latest values where + │ event_timestamp ≤ entity_df.event_timestamp + │ (prevents data leakage in training) + └── Returns RetrievalJob → .to_df() / .to_arrow() +``` + +--- + +## Feature Servers + +### Python Feature Server (FastAPI) + +**File**: `sdk/python/feast/feature_server.py` + +A FastAPI app that wraps `FeatureStore`. Started with `feast serve`. + +Endpoints: +- `POST /get-online-features` — online feature retrieval +- `POST /push` — push features to online/offline store +- `POST /materialize` — trigger materialization +- `GET /health` — health check + +The app loads `feature_store.yaml` at startup, creates a `FeatureStore`, and periodically +refreshes the registry in the background (async timer). + +### Go Feature Server + +**Directory**: `go/` +**Entry point**: `go/main.go` + +A high-performance alternative to the Python feature server, written in Go. +Supports HTTP, HTTPS, and gRPC transports: + +```bash +go run go/main.go -type http -port 6566 +go run go/main.go -type grpc -port 6566 +``` + +Key packages: +- `go/internal/feast/` — Go port of FeatureStore (reads feature_store.yaml, calls online store) +- `go/internal/feast/server/` — HTTP and gRPC server implementations +- `go/internal/feast/server/logging/` — feature logging to offline store + +The Go server reads the registry directly (proto file or remote) and calls the online store. +It does not support `feast apply` or materialization — those remain Python-only. + +--- + +## Feast Operator (Kubernetes) + +**Directory**: `infra/feast-operator/` +**Language**: Go (controller-runtime / kubebuilder) + +The operator manages the full lifecycle of a Feast deployment on Kubernetes via a +`FeatureStore` Custom Resource Definition (CRD). + +### CRD: FeatureStore + +**API version**: `feast.dev/v1` +**File**: `infra/feast-operator/api/v1/featurestore_types.go` + +```yaml +apiVersion: feast.dev/v1 +kind: FeatureStore +metadata: + name: my-feast +spec: + feastProjectName: my_project + services: + offlineStore: + persistence: + file: + type: dask + onlineStore: + persistence: + store: + type: redis + secretRef: + name: redis-credentials + registry: + local: + persistence: + file: + path: /data/registry.db +``` + +### What the operator manages + +| Service | What it deploys | +|---|---| +| **Online Store server** | Deployment + Service for the feature server (Go or Python) | +| **Offline Store server** | Deployment + Service for the offline feature server | +| **Registry server** | Deployment + Service for the registry gRPC server | +| **feature_store.yaml** | ConfigMap auto-generated from the CR spec | +| **Materialization jobs** | CronJob (`spec.services.onlineStore.cronJob`) | +| **TLS** | Certificate management via `spec.services.*.tls` | +| **Auth** | OIDC / Kubernetes RBAC via `spec.authz` | + +### Reconcile loop + +**File**: `infra/feast-operator/internal/controller/featurestore_controller.go` + +The `FeatureStoreReconciler.Reconcile` method runs on every CR change: +1. Fetches the `FeatureStore` CR +2. Calls `deployFeast()` → creates/updates Deployments, Services, ConfigMaps +3. Updates CR status conditions (`OfflineStore`, `OnlineStore`, `Registry` ready conditions) +4. Watches owned resources; re-reconciles on any change + +Supporting services logic: `infra/feast-operator/internal/controller/services/` + +--- + +## Serialization Layer + +All persistent metadata and the feature server wire format use **Protocol Buffers**. + +``` +Python object (FeatureView, Entity, ...) + ├── .to_proto() → Protobuf message → stored in registry or sent over gRPC + └── .from_proto() ← Protobuf message + +Proto definitions: + protos/feast/core/ # registry objects (FeatureView, Entity, DataSource, …) + protos/feast/serving/ # serving API (GetOnlineFeaturesRequest/Response) + protos/feast/types/ # Value, EntityKey, Field +``` + +When adding a new field to a Feast object: +1. Update the `.proto` file +2. Run `make compile-protos-python` (and `make compile-protos-go` if applicable) +3. Update `.to_proto()` and `.from_proto()` in the Python class + +--- + +## Key Files Quick Reference + +| Concern | Key file(s) | +|---|---| +| User-facing Python API | `sdk/python/feast/feature_store.py` | +| Config parsing | `sdk/python/feast/repo_config.py` | +| `feast apply` CLI logic | `sdk/python/feast/repo_operations.py` | +| Registry diff | `sdk/python/feast/diff/registry_diff.py` | +| Registry (proto/file) | `sdk/python/feast/infra/registry/registry.py` | +| Registry (SQL) | `sdk/python/feast/infra/registry/sql.py` | +| PIT join | `sdk/python/feast/infra/offline_stores/offline_utils.py` | +| Online store interface | `sdk/python/feast/infra/online_stores/online_store.py` | +| Entity key serialization | `sdk/python/feast/infra/online_stores/helpers.py` | +| Python feature server | `sdk/python/feast/feature_server.py` | +| Go feature server | `go/main.go`, `go/internal/feast/server/` | +| Operator CRD types | `infra/feast-operator/api/v1/featurestore_types.go` | +| Operator controller | `infra/feast-operator/internal/controller/featurestore_controller.go` | +| Operator services | `infra/feast-operator/internal/controller/services/` | +| Proto definitions | `protos/feast/` | +| Web UI | `ui/` (React) | + +--- + +## Official Architecture Documentation + +The `docs/` directory contains user-facing architecture documentation that complements this skill: + +| Topic | Doc | +|---|---| +| Architecture overview | `docs/getting-started/architecture/overview.md` | +| Push vs pull model | `docs/getting-started/architecture/push-vs-pull-model.md` | +| Write patterns | `docs/getting-started/architecture/write-patterns.md` | +| Feature transformation | `docs/getting-started/architecture/feature-transformation.md` | +| RBAC / authorization | `docs/getting-started/architecture/rbac.md` | +| Online store component | `docs/getting-started/components/online-store.md` | +| Offline store component | `docs/getting-started/components/offline-store.md` | +| Registry component | `docs/getting-started/components/registry.md` | +| Feature server component | `docs/getting-started/components/feature-server.md` | +| Provider component | `docs/getting-started/components/provider.md` | +| Compute engine | `docs/getting-started/components/compute-engine.md` | +| ADRs (design decisions) | `docs/adr/` | diff --git a/skills/feast-dev/SKILL.md b/skills/feast-dev/SKILL.md new file mode 100644 index 00000000000..b3e0c27c5a2 --- /dev/null +++ b/skills/feast-dev/SKILL.md @@ -0,0 +1,142 @@ +--- +name: feast-dev +description: Development guide for contributing to the Feast codebase. Covers environment setup, testing, linting, project structure, and PR workflow for feast-dev/feast. +license: Apache-2.0 +compatibility: Works with Claude Code, OpenAI Codex, and any Agent Skills compatible tool. Requires Python 3.10+, uv, and git. +metadata: + author: feast-dev + version: "1.0" +--- + +# Feast Development Guide + +## Environment Setup + +```bash +# Install development dependencies (uses uv pip sync with pinned requirements) +make install-python-dependencies-dev + +# Install minimal dependencies +make install-python-dependencies-minimal + +# Install pre-commit hooks (runs formatters and linters on commit) +make install-precommit +``` + +## Running Tests + +### Unit Tests +```bash +# Run all unit tests +make test-python-unit + +# Run a specific test file +python -m pytest sdk/python/tests/unit/test_unit_feature_store.py -v + +# Run a specific test by name +python -m pytest sdk/python/tests/unit/test_unit_feature_store.py -k "test_apply" -v + +# Run fast unit tests only (no external dependencies) +make test-python-unit-fast +``` + +### Integration Tests (local) +```bash +# Run integration tests in local dev mode +make test-python-integration-local +``` + +## Linting and Formatting + +```bash +# Format Python code +make format-python + +# Lint Python code +make lint-python + +# Run all precommit checks (format + lint) +make precommit-check + +# Type checking (entire codebase) +uv run bash -c "cd sdk/python && mypy feast" + +# Type-check a single file +uv run bash -c "cd sdk/python && mypy feast/path/to/file.py" +``` + +## Code Style + +- Use type hints on all function signatures +- Use `from __future__ import annotations` at the top of new files +- Follow existing patterns in the module you are modifying +- PR titles must follow semantic conventions: `feat:`, `fix:`, `ci:`, `chore:`, `docs:` +- Add a GitHub label to PRs (e.g. `kind/bug`, `kind/feature`, `kind/housekeeping`) +- Sign off commits with `git commit -s` (DCO requirement) + +## Docker Images + +```bash +# Build all Docker images +make build-docker + +# Build feature server Docker image +make build-feature-server-docker +``` + +## Documentation + +```bash +# Build Sphinx documentation +make build-sphinx + +# Build templates +make build-templates + +# Build Helm docs +make build-helm-docs +``` + +## Documentation and Blog Posts + +- **Blog posts must be placed in `/infra/website/docs/blog/`** — do NOT place them under `docs/blog/` or elsewhere. +- Blog post files must include YAML frontmatter with `title`, `description`, `date`, and `authors` fields matching the format of existing posts in that directory. +- All other docs go under `docs/` and **must be added to `docs/SUMMARY.md`** (GitBook navigation) or they won't appear on the website. + +| Change type | Doc location | +|---|---| +| New online store | `docs/reference/online-stores/.md` + update `README.md` and `SUMMARY.md` | +| New offline store | `docs/reference/offline-stores/.md` + update `README.md`, `overview.md`, `SUMMARY.md` | +| New registry backend | `docs/reference/registries/.md` + update `SUMMARY.md` | +| Config option | `docs/reference/feature-store-yaml.md` | +| CLI flag/command | `docs/reference/feast-cli-commands.md` | +| How-to / integration | `docs/how-to-guides/customizing-feast/` or `docs/how-to-guides/` + update `SUMMARY.md` | +| Architecture / concept | `docs/getting-started/architecture/` or `docs/getting-started/components/` | + +## Project Structure + +``` +sdk/python/feast/ # Main Python SDK + cli/cli.py # CLI entry point (feast apply, feast materialize, etc.) + feature_store.py # FeatureStore class - core orchestration + repo_config.py # feature_store.yaml configuration parsing + repo_operations.py # feast apply / feast teardown logic + infra/ # Online/offline store implementations + online_stores/ # Redis, DynamoDB, SQLite, etc. + offline_stores/ # BigQuery, Snowflake, File, etc. + transformation/ # On-demand and streaming transformations +protos/feast/ # Protobuf definitions +sdk/python/tests/ # Test suite + unit/ # Fast, no external deps + integration/ # Requires infrastructure +``` + +## Key Abstractions + +- **FeatureStore** (`feature_store.py`): Entry point for all operations +- **FeatureView**: Defines a set of features from a data source +- **OnDemandFeatureView**: Computed features using request-time transformations +- **Entity**: Join key definition (e.g. driver_id, customer_id) +- **DataSource**: Where raw data lives (BigQuery, files, Snowflake, etc.) +- **OnlineStore**: Low-latency feature serving (Redis, DynamoDB, SQLite) +- **OfflineStore**: Historical feature retrieval (BigQuery, Snowflake, file) diff --git a/skills/feast-testing/SKILL.md b/skills/feast-testing/SKILL.md new file mode 100644 index 00000000000..2704ff3feb2 --- /dev/null +++ b/skills/feast-testing/SKILL.md @@ -0,0 +1,268 @@ +--- +name: feast-testing +description: How to test and debug Feast — running targeted tests, writing unit tests for new components, debugging registry and online store issues, and inspecting live feature store state. Use when writing tests for a new feature, debugging a failing test, investigating a runtime error, or verifying that a change works correctly end-to-end. +license: Apache-2.0 +compatibility: Works with Claude Code, OpenAI Codex, and any Agent Skills compatible tool. +metadata: + author: feast-dev + version: "1.0" +--- + +# Testing and Debugging Feast + +## Test Organization + +``` +sdk/python/tests/ +├── unit/ # Fast tests, no external deps, run locally +│ ├── infra/ +│ │ ├── online_store/ # Per-store unit tests (redis, dynamodb, etc.) +│ │ ├── offline_stores/ # Offline store unit tests +│ │ └── registry/ # Registry unit tests +│ ├── test_unit_feature_store.py # Core FeatureStore behavior +│ ├── test_feature_views.py # Feature view validation +│ └── test_on_demand_*.py # On-demand transformation tests +└── integration/ # Requires real infrastructure (run in CI) + ├── feature_repos/ # Sample repos used as test fixtures + └── test_universal_*.py # Cross-store compatibility tests +``` + +**Rule of thumb**: if a test needs mocks for an external service, it belongs in `unit/`. +If it spins up real infra (Redis, DynamoDB, BigQuery), it belongs in `integration/`. + +--- + +## Running Tests + +### Targeted unit tests + +```bash +# Run a single test file +python -m pytest sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py -v + +# Run a single test by name +python -m pytest sdk/python/tests/unit/test_unit_feature_store.py -k "test_apply" -v + +# Run all unit tests for one subsystem +python -m pytest sdk/python/tests/unit/infra/online_store/ -v + +# Run all unit tests (fast, no infra needed) +make test-python-unit +``` + +### Integration tests (local) + +```bash +# Run local integration tests (SQLite online, file offline) +make test-python-integration-local + +# Run specific integration test with verbose output +python -m pytest sdk/python/tests/integration/ -k "test_online_retrieval" -v -s +``` + +### Running a single test file fast (skip slow markers) + +```bash +python -m pytest sdk/python/tests/unit/infra/online_store/test_redis.py -v --no-header -q +``` + +--- + +## Writing Unit Tests for a New Component + +### Pattern: testing an online store + +Follow `sdk/python/tests/unit/infra/online_store/test_dynamodb_online_store.py` or `test_redis.py`. + +```python +import asyncio +from unittest.mock import AsyncMock, MagicMock, patch +from feast.infra.online_stores.mystore import MyStore, MyStoreConfig +from feast.repo_config import RepoConfig + +def make_config(store_config: dict) -> RepoConfig: + return RepoConfig( + project="test_project", + registry="data/registry.db", + provider="local", + online_store=store_config, + ) + +@patch("feast.infra.online_stores.mystore.MyClient") +def test_online_write_batch(mock_client_cls): + mock_client = MagicMock() + mock_client_cls.return_value = mock_client + + config = make_config({"type": "mystore", "host": "localhost"}) + store = MyStore() + feature_view = MagicMock() + feature_view.name = "driver_stats" + + store.online_write_batch(config, feature_view, data=[...], progress=None) + + mock_client.set.assert_called_once() + +# For async methods: +def test_online_write_batch_async(): + async def _run(): + store = MyStore() + await store.online_write_batch_async(config, feature_view, data, progress=None) + asyncio.run(_run()) +``` + +### Pattern: testing registry operations + +Follow `sdk/python/tests/unit/infra/registry/test_registry.py`. + +```python +from feast.infra.registry.registry import Registry +from feast.repo_config import RegistryConfig + +def test_apply_feature_view(tmp_path): + config = RegistryConfig(path=str(tmp_path / "registry.db")) + registry = Registry("test_project", config, repo_path=None) + + entity = Entity(name="driver_id", value_type=ValueType.INT64) + registry.apply_entity(entity, project="test_project") + + stored = registry.get_entity("driver_id", project="test_project") + assert stored.name == "driver_id" +``` + +### Pattern: SQL registry — dialect DDL and startup-check tests + +See `sdk/python/tests/unit/infra/registry/test_sql_registry.py`. + +- **Assert dialect-specific DDL without a live DB**: compile a table for a target dialect and check the emitted column type. Useful for column-type rules like `ProtoBytes` → `LONGBLOB` on MySQL/MariaDB. + ```python + from sqlalchemy.dialects import mysql + from sqlalchemy.schema import CreateTable + ddl = str(CreateTable(feature_views).compile(dialect=mysql.dialect())) + assert "feature_view_proto LONGBLOB" in ddl + ``` +- **Unit-test a startup diagnostic with a mock engine**: stub `engine.dialect.name` and the `engine.connect()` context manager so no DB is needed. + ```python + engine = MagicMock() + engine.dialect.name = "mysql" + engine.connect.return_value.__enter__.return_value.execute.return_value.fetchall.return_value = [ + ("feature_views", "feature_view_proto"), + ] + SqlRegistry._warn_if_narrow_blob_columns(engine) + ``` +- **Live round-trip behavior** (e.g. a >64 KB proto surviving the column) belongs in an integration test against the `mysql_registry` fixture in `tests/integration/registration/test_universal_registry.py` (DDL-compile tests can't catch runtime truncation). + +### Pattern: testing FeatureStore end-to-end (unit level) + +```python +from feast import FeatureStore +from feast.repo_config import RepoConfig + +def test_get_online_features(tmp_path): + config = RepoConfig( + project="test", + registry=str(tmp_path / "registry.db"), + provider="local", + online_store={"type": "sqlite", "path": str(tmp_path / "online.db")}, + offline_store={"type": "file"}, + ) + store = FeatureStore(config=config) + # apply objects, write data, then assert retrieval +``` + +--- + +## Debugging Common Issues + +### Registry out of sync / stale metadata + +```bash +# Inspect the live registry +feast registry-dump # prints full proto as JSON + +# Or from Python: +from feast import FeatureStore +store = FeatureStore(repo_path=".") +store.registry.list_feature_views(project=store.project) +store.registry.list_entities(project=store.project) +``` + +**Symptom**: `FeatureView not found` or stale feature definitions. +**Fix**: Re-run `feast apply`. If using a cached registry (TTL > 0), wait for cache expiry or force refresh by restarting the process. + +### Online store returns None for all features + +```bash +# Check if materialization has run +feast feature-views list # shows last_updated_timestamp + +# Check entity key format +# Entity keys are serialized — if the serialization_version doesn't match, lookups fail. +# Check entity_key_serialization_version in feature_store.yaml +``` + +**Symptom**: `get_online_features` returns all `None` values despite materialization running. +**Cause**: Entity key serialization version mismatch, wrong project name, or wrong TTL causing data to expire. + +### Type errors from mypy + +```bash +# Type-check only the file you changed +uv run bash -c "cd sdk/python && mypy feast/infra/online_stores/mystore.py" + +# Common fix: add Optional[] around return types, use cast() for dynamic lookups +``` + +### Test imports fail / module not found + +```bash +# Ensure you're running from repo root with dev dependencies installed +make install-python-dependencies-dev + +# Run with uv to use the pinned venv +uv run python -m pytest sdk/python/tests/unit/... -v +``` + +### Protobuf deserialization errors + +**Symptom**: `DecodeError` or unexpected field values when reading registry. +**Cause**: Proto schema changed without recompiling. +**Fix**: +```bash +make compile-protos-python +``` + +### Feature server returns 500 + +```bash +# Start the feature server locally and check logs +feast serve --host 0.0.0.0 --port 6566 + +# Test directly +curl -X POST http://localhost:6566/get-online-features \ + -H "Content-Type: application/json" \ + -d '{"features": ["driver_stats:conv_rate"], "entities": {"driver_id": [1001]}}' +``` + +--- + +## Useful CLI Commands for Inspection + +```bash +feast entities list # list all registered entities +feast feature-views list # list feature views + last materialization time +feast feature-services list # list feature services +feast on-demand-feature-views list # list ODFVs +feast registry-dump # dump full registry as JSON (debug) +feast plan # preview what feast apply would change (dry run) +``` + +--- + +## Test Utilities + +| Utility | Location | Purpose | +|---|---|---| +| `get_feature_view()` helpers | `tests/unit/infra/online_store/test_*.py` | Build minimal FeatureView mocks | +| `LocalRegistry` fixture | `tests/unit/infra/registry/test_registry.py` | In-memory registry for unit tests | +| `integration/feature_repos/` | `tests/integration/` | Sample repos for integration test scenarios | +| `conftest.py` fixtures | Various `tests/` subdirs | Shared pytest fixtures per subsystem | diff --git a/skills/feast-user-guide/SKILL.md b/skills/feast-user-guide/SKILL.md new file mode 100644 index 00000000000..0210ee00b42 --- /dev/null +++ b/skills/feast-user-guide/SKILL.md @@ -0,0 +1,253 @@ +--- +name: feast-user-guide +description: Guide for working with Feast (Feature Store) — defining features, configuring feature_store.yaml, retrieving features online/offline, using the CLI, and building RAG retrieval pipelines. Use when the user asks about creating entities, feature views, on-demand feature views, stream feature views, feature services, data sources, feature_store.yaml configuration, feast apply/materialize commands, online or historical feature retrieval, or vector-based document retrieval with Feast. +license: Apache-2.0 +compatibility: Works with Claude Code, OpenAI Codex, and any Agent Skills compatible tool. +metadata: + author: feast-dev + version: "1.0" +--- + +# Feast User Guide + +## Quick Start + +A Feast project requires: +1. A `feature_store.yaml` config file +2. Python files defining entities, data sources, feature views, and feature services +3. Running `feast apply` to register definitions + +```bash +feast init my_project +cd my_project +feast apply +``` + +## Core Concepts + +### Entity +An entity is a collection of semantically related features (e.g., a customer, a driver). Entities have join keys used to look up features. + +```python +from feast import Entity +from feast.value_type import ValueType + +driver = Entity( + name="driver_id", + description="Driver identifier", + value_type=ValueType.INT64, +) +``` + +### Data Sources +Data sources describe where raw feature data lives. + +```python +from feast import FileSource, BigQuerySource, KafkaSource, PushSource, RequestSource +from feast.data_format import ParquetFormat + +# Batch source (file) +driver_stats_source = FileSource( + name="driver_stats_source", + path="data/driver_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) + +# Request source (for on-demand features) +input_request = RequestSource( + name="vals_to_add", + schema=[Field(name="val_to_add", dtype=Float64)], +) +``` + +### FeatureView +Maps features from a data source to entities with a schema, TTL, and online/offline settings. + +```python +from feast import FeatureView, Field +from feast.types import Float32, Int64, String +from datetime import timedelta + +driver_hourly_stats = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=365), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, +) +``` + +### OnDemandFeatureView +Computes features at request time from other feature views and/or request data. + +```python +from feast import on_demand_feature_view +import pandas as pd + +@on_demand_feature_view( + sources=[driver_hourly_stats, input_request], + schema=[Field(name="conv_rate_plus_val", dtype=Float64)], + mode="pandas", +) +def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val"] = inputs["conv_rate"] + inputs["val_to_add"] + return df +``` + +### FeatureService +Groups features from multiple views for retrieval. + +```python +from feast import FeatureService + +driver_fs = FeatureService( + name="driver_ranking", + features=[driver_hourly_stats, transformed_conv_rate], +) +``` + +## Feature Retrieval + +### Online (low-latency) +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +features = store.get_online_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + ], + entity_rows=[{"driver_id": 1001}, {"driver_id": 1002}], +).to_dict() +``` + +### Historical (training data with point-in-time joins) +```python +entity_df = pd.DataFrame({ + "driver_id": [1001, 1002], + "event_timestamp": [datetime(2023, 1, 1), datetime(2023, 1, 2)], +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=["driver_hourly_stats:conv_rate", "driver_hourly_stats:acc_rate"], +).to_df() +``` + +Or use a FeatureService: +```python +training_df = store.get_historical_features( + entity_df=entity_df, + features=driver_fs, +).to_df() +``` + +## Materialization + +Load features from offline store into online store: + +```bash +# Full materialization over a time range +feast materialize 2023-01-01T00:00:00 2023-12-31T23:59:59 + +# Incremental (from last materialized timestamp) +feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") +``` + +Python API: +```python +from datetime import datetime +store.materialize(start_date=datetime(2023, 1, 1), end_date=datetime(2023, 12, 31)) +store.materialize_incremental(end_date=datetime.utcnow()) +``` + +## CLI Commands + +| Command | Purpose | +|---------|---------| +| `feast init [DIR]` | Create new feature repository | +| `feast apply` | Register/update feature definitions | +| `feast plan` | Preview changes without applying | +| `feast materialize START END` | Materialize features to online store | +| `feast materialize-incremental END` | Incremental materialization | +| `feast entities list` | List registered entities | +| `feast feature-views list` | List feature views | +| `feast feature-services list` | List feature services | +| `feast on-demand-feature-views list` | List on-demand feature views | +| `feast teardown` | Remove infrastructure resources | +| `feast version` | Show SDK version | + +Options: `--chdir` / `-c` (run in different directory), `--feature-store-yaml` / `-f` (override config path). + +## Vector Search / RAG + +Define a feature view with vector fields for similarity search: + +```python +from feast.types import Array, Float32 + +wiki_passages = FeatureView( + name="wiki_passages", + entities=[passage_entity], + schema=[ + Field(name="passage_text", dtype=String), + Field( + name="embedding", + dtype=Array(Float32), + vector_index=True, + vector_length=384, + vector_search_metric="COSINE", + ), + ], + source=passages_source, + online=True, +) +``` + +Retrieve similar documents: +```python +results = store.retrieve_online_documents( + feature="wiki_passages:embedding", + query=query_embedding, + top_k=5, +) +``` + +## feature_store.yaml Minimal Config + +```yaml +project: my_project +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db +``` + +## Common Imports + +```python +from feast import ( + Entity, FeatureView, OnDemandFeatureView, FeatureService, + Field, FileSource, RequestSource, FeatureStore, +) +from feast.on_demand_feature_view import on_demand_feature_view +from feast.types import Float32, Float64, Int64, String, Bool, Array +from feast.value_type import ValueType +from datetime import timedelta +``` + +## Detailed References + +- **Feature definitions** (all types, parameters, patterns): See [references/feature-definitions.md](../references/feature-definitions.md) +- **Configuration** (feature_store.yaml, all store types, auth): See [references/configuration.md](../references/configuration.md) +- **Retrieval & RAG** (online/offline retrieval, vector search, RAG retriever): See [references/retrieval-and-rag.md](../references/retrieval-and-rag.md) diff --git a/skills/references/configuration.md b/skills/references/configuration.md new file mode 100644 index 00000000000..7a63b8429bd --- /dev/null +++ b/skills/references/configuration.md @@ -0,0 +1,284 @@ +# Configuration Reference + +## Table of Contents +- [feature_store.yaml](#feature_storeyaml) +- [RepoConfig Fields](#repoconfig-fields) +- [Registry Configuration](#registry-configuration) +- [Online Store Types](#online-store-types) +- [Offline Store Types](#offline-store-types) +- [Batch Engine Types](#batch-engine-types) +- [Authentication](#authentication) +- [Feature Server](#feature-server) +- [Materialization Config](#materialization-config) +- [OpenLineage Config](#openlineage-config) +- [Feature Repository Layout](#feature-repository-layout) + +## feature_store.yaml + +Minimal local config: +```yaml +project: my_project +registry: data/registry.db +provider: local +online_store: + type: sqlite + path: data/online_store.db +``` + +GCP config: +```yaml +project: my_project +registry: gs://my-bucket/registry.pb +provider: gcp +online_store: + type: datastore +offline_store: + type: bigquery +``` + +AWS config: +```yaml +project: my_project +registry: s3://my-bucket/registry.pb +provider: aws +online_store: + type: dynamodb + region: us-east-1 +offline_store: + type: redshift + cluster_id: my-cluster + region: us-east-1 + database: feast + user: admin + s3_staging_location: s3://my-bucket/feast-staging +``` + +## RepoConfig Fields + +| Field | Alias | Type | Default | Description | +|-------|-------|------|---------|-------------| +| `project` | - | str | required | Project namespace (alphanumeric + underscores) | +| `project_description` | - | str | None | Project description | +| `provider` | - | str | `"local"` | `"local"`, `"gcp"`, or `"aws"` | +| `registry` | `registry_config` | str/dict | required | Registry path or config object | +| `online_store` | `online_config` | str/dict | `"sqlite"` | Online store type or config | +| `offline_store` | `offline_config` | str/dict | `"dask"` | Offline store type or config | +| `batch_engine` | `batch_engine_config` | str/dict | `"local"` | Batch materialization engine | +| `auth` | - | dict | no_auth | Authentication config | +| `feature_server` | - | dict | None | Feature server config | +| `entity_key_serialization_version` | - | int | 3 | Entity key serialization version | +| `coerce_tz_aware` | - | bool | True | Coerce timestamps to timezone-aware | +| `materialization` | `materialization_config` | dict | default | Materialization options | +| `openlineage` | `openlineage_config` | dict | None | OpenLineage config | + +## Registry Configuration + +| Field | Default | Description | +|-------|---------|-------------| +| `registry_type` | `"file"` | `"file"`, `"sql"`, `"snowflake.registry"`, `"remote"` | +| `path` | `""` | Local path, GCS/S3 URI (file), or DB connection URL (sql) | +| `cache_ttl_seconds` | 600 | Registry cache TTL (0 = no expiry) | +| `cache_mode` | `"sync"` | `"sync"` or `"thread"` | +| `s3_additional_kwargs` | None | Extra boto3 kwargs for S3 | + +### File registry +```yaml +registry: data/registry.db +``` +or +```yaml +registry: + registry_type: file + path: data/registry.db + cache_ttl_seconds: 60 +``` + +### SQL registry +```yaml +registry: + registry_type: sql + path: postgresql://user:pass@host:5432/feast # pragma: allowlist secret + cache_ttl_seconds: 60 +``` + +### Remote registry +```yaml +registry: + registry_type: remote + path: grpc://feast-registry-server:6570 +``` + +## Online Store Types + +| Type | Config Key | Use Case | +|------|-----------|----------| +| `sqlite` | `path` | Local development | +| `redis` | `connection_string` | Production, low-latency | +| `dynamodb` | `region` | AWS-native | +| `datastore` | `project_id` | GCP-native | +| `bigtable` | `project_id`, `instance` | GCP, high-throughput | +| `postgres` | `host`, `port`, `database`, `user`, `password` | Self-managed | +| `snowflake.online` | `account`, `database`, `schema` | Snowflake ecosystem | +| `milvus` | `host`, `port` | Vector search | +| `qdrant` | `host`, `port` | Vector search | +| `remote` | `path` | Remote feature server | + +### Examples + +```yaml +# SQLite (local dev) +online_store: + type: sqlite + path: data/online_store.db + +# Redis +online_store: + type: redis + connection_string: redis://localhost:6379 + +# PostgreSQL +online_store: + type: postgres + host: localhost + port: 5432 + database: feast + db_schema: public + user: postgres + password: secret + +# Milvus (vector search) +online_store: + type: milvus + host: localhost + port: 19530 +``` + +## Offline Store Types + +| Type | Use Case | +|------|----------| +| `dask` | Local development (default) | +| `duckdb` | Local, fast analytics | +| `bigquery` | GCP | +| `snowflake.offline` | Snowflake | +| `redshift` | AWS | +| `spark` | Large-scale processing | +| `postgres` | Self-managed | +| `trino` | Federated queries | +| `athena` | AWS serverless | +| `clickhouse` | Analytics | +| `remote` | Remote offline server | + +### Examples + +```yaml +# DuckDB +offline_store: + type: duckdb + +# BigQuery +offline_store: + type: bigquery + project_id: my-gcp-project + dataset: feast_dataset + +# Snowflake +offline_store: + type: snowflake.offline + account: my_account + user: user + password: pass + database: FEAST + schema: PUBLIC + warehouse: COMPUTE_WH + +# Spark +offline_store: + type: spark + spark_conf: + spark.master: "local[*]" +``` + +## Batch Engine Types + +| Type | Description | +|------|-------------| +| `local` | Local Python process (default) | +| `snowflake.engine` | Snowflake-based materialization | +| `spark.engine` | Spark-based materialization | +| `lambda` | AWS Lambda-based | +| `k8s` | Kubernetes job-based | +| `ray.engine` | Ray-based | + +```yaml +batch_engine: + type: local +``` + +## Authentication + +| Type | Description | +|------|-------------| +| `no_auth` | No authentication (default) | +| `kubernetes` | Kubernetes service account | +| `oidc` | OpenID Connect (server-side) | +| `oidc_client` | OpenID Connect (client-side) | + +```yaml +# OIDC example +auth: + type: oidc + client_id: feast-client + auth_server_url: https://auth.example.com + auth_discovery_url: https://auth.example.com/.well-known/openid-configuration +``` + +## Feature Server + +```yaml +feature_server: + type: local +``` + +MCP-based feature server: +```yaml +feature_server: + type: mcp +``` + +## Materialization Config + +```yaml +materialization: + pull_latest_features: false # Only pull latest feature values per entity +``` + +## OpenLineage Config + +```yaml +openlineage: + enabled: true + transport_type: http # http, console, file, kafka (omit to use OpenLineage SDK defaults) + transport_url: http://marquez:5000 + transport_endpoint: api/v1/lineage + namespace: feast + emit_on_apply: true + emit_on_materialize: true +``` + +## Feature Repository Layout + +``` +my_feature_repo/ +├── feature_store.yaml # Required config +├── .feastignore # Optional gitignore-style file +├── driver_features.py # Feature definitions +├── customer_features.py # More definitions +└── data/ + ├── driver_stats.parquet # Data files (for FileSource) + └── registry.db # Auto-generated registry +``` + +- Feast recursively scans all `.py` files for feature definitions +- Use `.feastignore` to exclude files/directories from scanning +- `feast apply` registers all discovered definitions into the registry diff --git a/skills/references/feature-definitions.md b/skills/references/feature-definitions.md new file mode 100644 index 00000000000..d9040e089c0 --- /dev/null +++ b/skills/references/feature-definitions.md @@ -0,0 +1,350 @@ +# Feature Definitions Reference + +## Table of Contents +- [Entity](#entity) +- [Field](#field) +- [Data Sources](#data-sources) +- [FeatureView](#featureview) +- [OnDemandFeatureView](#ondemandfeatureview) +- [StreamFeatureView](#streamfeatureview) +- [FeatureService](#featureservice) +- [Aggregation](#aggregation) + +## Entity + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | str | required | Unique entity name | +| `join_keys` | List[str] | `[name]` | Join keys for lookup (only one supported) | +| `value_type` | ValueType | - | Deprecated; use `join_keys` instead | +| `description` | str | `""` | Human-readable description | +| `tags` | Dict[str,str] | `{}` | Metadata tags | +| `owner` | str | `""` | Owner/maintainer | + +```python +from feast import Entity +from feast.value_type import ValueType + +driver = Entity(name="driver_id", description="Driver identifier") +customer = Entity(name="customer_id", join_keys=["customer_id"]) +``` + +## Field + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | str | required | Field name | +| `dtype` | FeastType | required | Data type | +| `description` | str | `""` | Description | +| `vector_index` | bool | False | Enable vector similarity search | +| `vector_length` | int | - | Vector dimension (required if `vector_index=True`) | +| `vector_search_metric` | str | - | `"COSINE"`, `"L2"`, `"INNER_PRODUCT"` | + +### Type System + +**Scalar types** (from `feast.types`): `Float32`, `Float64`, `Int32`, `Int64`, `String`, `Bool`, `Bytes`, `UnixTimestamp`, `ZonedTimestamp` + +**Collection types**: `Array(T)` where T is a scalar type (e.g., `Array(Float32)` for embeddings) + +**ValueType enum** (legacy, from `feast.value_type`): `STRING`, `INT32`, `INT64`, `FLOAT`, `DOUBLE`, `BOOL`, `BYTES`, `UNIX_TIMESTAMP`; plus `_LIST` and `_SET` variants. + +**Python → Feast mapping**: `int` → INT64, `str` → STRING, `float` → DOUBLE, `bytes` → BYTES, `bool` → BOOL, `datetime` → UNIX_TIMESTAMP + +### Vector field example + +```python +Field( + name="embedding", + dtype=Array(Float32), + vector_index=True, + vector_length=384, + vector_search_metric="COSINE", +) +``` + +## Data Sources + +### Batch Sources + +**FileSource**: +```python +from feast import FileSource + +source = FileSource( + name="driver_stats", + path="data/driver_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", +) +``` + +**BigQuerySource**: +```python +from feast.infra.offline_stores.contrib.bigquery_offline_store.bigquery_source import BigQuerySource + +source = BigQuerySource( + name="driver_stats_bq", + table="project.dataset.driver_stats", + timestamp_field="event_timestamp", +) +``` + +Other batch sources: `SnowflakeSource`, `RedshiftSource`, `PostgreSQLSource`, `SparkSource`, `TrinoSource`, `AthenaSource`, `ClickhouseSource` + +### Stream Sources + +**KafkaSource**: +```python +from feast.data_source import KafkaSource + +source = KafkaSource( + name="driver_trips_stream", + kafka_bootstrap_servers="broker:9092", + topic="driver_trips", + timestamp_field="event_timestamp", + batch_source=file_source, # for backfill + message_format=AvroFormat(schema_json=schema), +) +``` + +**KinesisSource**: `region`, `stream_name`, `record_format`, `batch_source` + +**PushSource** (for manual push via SDK): +```python +from feast.data_source import PushSource + +push_source = PushSource(name="driver_push", batch_source=file_source) +``` + +### RequestSource (for OnDemandFeatureView) + +```python +from feast import RequestSource, Field +from feast.types import Float64 + +input_request = RequestSource( + name="vals_to_add", + schema=[Field(name="val_to_add", dtype=Float64)], +) +``` + +## FeatureView + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | str | required | Unique name | +| `source` | DataSource | required | Batch or stream data source | +| `entities` | List[Entity] | `[]` | Associated entities | +| `schema` | List[Field] | `[]` | Feature schema (can be inferred from source) | +| `ttl` | timedelta | `timedelta(0)` | Time-to-live for features | +| `online` | bool | `True` | Available for online retrieval | +| `offline` | bool | `False` | Available for offline retrieval | +| `description` | str | `""` | Description | +| `tags` | Dict[str,str] | `{}` | Metadata | +| `owner` | str | `""` | Owner | +| `mode` | str | - | Transformation mode: `"python"`, `"pandas"`, `"sql"`, `"spark"`, `"ray"`, `"substrait"` | + +```python +from feast import FeatureView, Field +from feast.types import Float32, Int64 +from datetime import timedelta + +driver_hourly_stats = FeatureView( + name="driver_hourly_stats", + entities=[driver], + ttl=timedelta(days=365), + schema=[ + Field(name="conv_rate", dtype=Float32), + Field(name="acc_rate", dtype=Float32), + Field(name="avg_daily_trips", dtype=Int64), + ], + online=True, + source=driver_stats_source, +) +``` + +## OnDemandFeatureView + +Features computed at request time from other feature views and/or request data. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | str | required | Unique name | +| `sources` | List | required | Input FeatureViews and/or RequestSources | +| `schema` | List[Field] | required | Output schema | +| `mode` | str | `"pandas"` | `"pandas"` or `"python"` | +| `singleton` | bool | `False` | Single-row dict input (mode="python" only) | +| `write_to_online_store` | bool | `False` | Precompute on write instead of read | +| `aggregations` | List[Aggregation] | `[]` | Pre-transformation aggregations | + +### Pandas mode (default) + +```python +@on_demand_feature_view( + sources=[driver_hourly_stats, input_request], + schema=[Field(name="conv_rate_plus_val", dtype=Float64)], + mode="pandas", +) +def transformed_conv_rate(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["conv_rate_plus_val"] = inputs["conv_rate"] + inputs["val_to_add"] + return df +``` + +### Python mode + +```python +@on_demand_feature_view( + sources=[driver_hourly_stats], + schema=[Field(name="conv_rate_category", dtype=String)], + mode="python", +) +def categorize_conv_rate(inputs: dict) -> dict: + output = {"conv_rate_category": []} + for rate in inputs["conv_rate"]: + output["conv_rate_category"].append("high" if rate > 0.5 else "low") + return output +``` + +### Python singleton mode + +```python +@on_demand_feature_view( + sources=[driver_hourly_stats], + schema=[Field(name="conv_rate_category", dtype=String)], + mode="python", + singleton=True, +) +def categorize_conv_rate(inputs: dict) -> dict: + rate = inputs["conv_rate"] + return {"conv_rate_category": "high" if rate > 0.5 else "low"} +``` + +### Write-to-online-store mode + +```python +@on_demand_feature_view( + sources=[push_source], + schema=[Field(name="trips_today_category", dtype=String)], + write_to_online_store=True, +) +def categorize_trips(inputs: pd.DataFrame) -> pd.DataFrame: + df = pd.DataFrame() + df["trips_today_category"] = inputs["trips_today"].apply( + lambda x: "high" if x > 10 else "low" + ) + return df +``` + +### Aggregation-based ODFV + +```python +from feast.aggregation import Aggregation + +@on_demand_feature_view( + sources=[driver_hourly_stats], + schema=[Field(name="sum_trips", dtype=Int64)], + aggregations=[Aggregation(column="avg_daily_trips", function="sum")], +) +def agg_view(inputs: pd.DataFrame) -> pd.DataFrame: + return inputs +``` + +### Validation note + +Use `feast apply --skip-feature-view-validation` if ODFV validation fails with complex logic (validation uses random inputs). + +## StreamFeatureView + +Extends FeatureView for stream sources (Kafka, Kinesis, PushSource). + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | str | required | Unique name | +| `source` | DataSource | required | KafkaSource, KinesisSource, or PushSource | +| `entities` | List[Entity] | `[]` | Entities | +| `schema` | List[Field] | `[]` | Schema | +| `ttl` | timedelta | `timedelta(0)` | TTL | +| `aggregations` | List[Aggregation] | `[]` | Windowed aggregations | +| `timestamp_field` | str | - | Required if using aggregations | +| `udf` | function | - | Transformation function | +| `mode` | str | - | `"python"`, `"pandas"`, `"spark"`, `"spark_sql"` | + +```python +from feast import StreamFeatureView, Field +from feast.types import Int64 +from feast.aggregation import Aggregation +from datetime import timedelta + +driver_stream = StreamFeatureView( + name="driver_trips_stream", + entities=[driver], + source=kafka_source, + schema=[Field(name="trips", dtype=Int64)], + ttl=timedelta(hours=2), + aggregations=[ + Aggregation(column="trips", function="count", time_window=timedelta(hours=1)), + ], + timestamp_field="event_timestamp", +) +``` + +## FeatureService + +Groups features from one or more feature views for retrieval. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | str | required | Unique name | +| `features` | List | required | Feature views or projections | +| `description` | str | `""` | Description | +| `tags` | Dict[str,str] | `{}` | Metadata | +| `owner` | str | `""` | Owner | +| `logging_config` | LoggingConfig | - | Logging configuration | + +```python +from feast import FeatureService + +driver_activity_service = FeatureService( + name="driver_activity", + features=[ + driver_hourly_stats, + transformed_conv_rate, + ], + description="Features for driver activity model", +) +``` + +### Feature projections (select specific features) + +```python +driver_fs = FeatureService( + name="driver_ranking", + features=[ + driver_hourly_stats[["conv_rate", "acc_rate"]], + ], +) +``` + +## Aggregation + +For StreamFeatureView windowed aggregations. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `column` | str | Source column name | +| `function` | str | `"sum"`, `"max"`, `"min"`, `"count"`, `"mean"` | +| `time_window` | timedelta | Aggregation window | +| `slide_interval` | timedelta | Slide interval (for sliding windows) | + +```python +from feast.aggregation import Aggregation +from datetime import timedelta + +agg = Aggregation( + column="trips", + function="count", + time_window=timedelta(hours=1), + slide_interval=timedelta(minutes=5), +) +``` diff --git a/skills/references/retrieval-and-rag.md b/skills/references/retrieval-and-rag.md new file mode 100644 index 00000000000..8198134e5bc --- /dev/null +++ b/skills/references/retrieval-and-rag.md @@ -0,0 +1,287 @@ +# Retrieval & RAG Reference + +## Table of Contents +- [FeatureStore Construction](#featurestore-construction) +- [Online Feature Retrieval](#online-feature-retrieval) +- [Historical Feature Retrieval](#historical-feature-retrieval) +- [Push and Write Operations](#push-and-write-operations) +- [Vector Similarity Search](#vector-similarity-search) +- [RAG Retriever](#rag-retriever) +- [FeatureStore API Quick Reference](#featurestore-api-quick-reference) + +## FeatureStore Construction + +```python +from feast import FeatureStore + +# From repo path (looks for feature_store.yaml) +store = FeatureStore(repo_path="path/to/feature_repo") + +# From config object +from feast.repo_config import RepoConfig +store = FeatureStore(config=RepoConfig( + project="my_project", + registry="data/registry.db", + provider="local", + online_store={"type": "sqlite", "path": "data/online.db"}, +)) + +# From explicit YAML path +from pathlib import Path +store = FeatureStore(fs_yaml_file=Path("custom/feature_store.yaml")) +``` + +## Online Feature Retrieval + +Low-latency lookup from the online store. Features must be materialized first. + +### By feature references +```python +result = store.get_online_features( + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + ], + entity_rows=[ + {"driver_id": 1001}, + {"driver_id": 1002}, + ], +) + +feature_dict = result.to_dict() +feature_df = result.to_df() +``` + +### By FeatureService +```python +result = store.get_online_features( + features=driver_ranking_service, + entity_rows=[{"driver_id": 1001}], +) +``` + +### Feature reference format +`"feature_view_name:feature_name"` — e.g., `"driver_hourly_stats:conv_rate"` + +## Historical Feature Retrieval + +Point-in-time correct joins for training data. Prevents data leakage by joining features based on event timestamps. + +### Basic usage +```python +import pandas as pd +from datetime import datetime + +entity_df = pd.DataFrame({ + "driver_id": [1001, 1002, 1003], + "event_timestamp": [ + datetime(2023, 6, 1), + datetime(2023, 6, 15), + datetime(2023, 7, 1), + ], +}) + +training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + ], +).to_df() +``` + +### With FeatureService +```python +training_df = store.get_historical_features( + entity_df=entity_df, + features=driver_ranking_service, +).to_df() +``` + +### Output +Returns a `RetrievalJob` with methods: +- `.to_df()` — pandas DataFrame +- `.to_arrow()` — PyArrow Table +- `.to_sql_string()` — SQL query (for SQL-based offline stores) + +## Push and Write Operations + +### Push (for PushSource/StreamFeatureView) +```python +store.push( + push_source_name="driver_push", + df=pd.DataFrame({ + "driver_id": [1001], + "trips_today": [15], + "event_timestamp": [datetime.utcnow()], + }), +) +``` + +### Write to online store +```python +store.write_to_online_store( + feature_view_name="driver_hourly_stats", + df=features_df, +) +``` + +### Write to offline store +```python +store.write_to_offline_store( + feature_view_name="driver_hourly_stats", + df=features_df, +) +``` + +## Vector Similarity Search + +Requires a FeatureView with a `vector_index=True` field and an online store that supports vector search (e.g., Milvus, Qdrant, PostgreSQL with pgvector). + +### Define vector feature view +```python +from feast import Entity, FeatureView, Field, FileSource +from feast.types import Array, Float32, String + +passage_entity = Entity(name="passage_id", join_keys=["passage_id"]) + +wiki_passages = FeatureView( + name="wiki_passages", + entities=[passage_entity], + schema=[ + Field(name="passage_text", dtype=String), + Field( + name="embedding", + dtype=Array(Float32), + vector_index=True, + vector_length=384, + vector_search_metric="COSINE", + ), + ], + source=passages_source, + online=True, +) +``` + +### Retrieve similar documents +```python +# v1 API +results = store.retrieve_online_documents( + feature="wiki_passages:embedding", + query=query_embedding_vector, + top_k=5, +) + +# v2 API (supports text, vector, and image queries) +results = store.retrieve_online_documents_v2( + feature_view_name="wiki_passages", + query_string="What is machine learning?", + top_k=5, +) +``` + +### Search metrics +- `"COSINE"` — Cosine similarity (default, best for normalized embeddings) +- `"L2"` — Euclidean distance +- `"INNER_PRODUCT"` — Dot product + +## RAG Retriever + +`FeastRAGRetriever` integrates Feast with HuggingFace for retrieval-augmented generation. + +### Prerequisites +- A FeatureView with a `vector_index=True` embedding field +- Features materialized to the online store +- HuggingFace `transformers` installed + +### Setup +```python +from feast.rag_retriever import FeastRAGRetriever +from transformers import AutoTokenizer, AutoModel, AutoModelForSeq2SeqLM + +question_tokenizer = AutoTokenizer.from_pretrained("facebook/dpr-question_encoder-single-nq-base") +question_encoder = AutoModel.from_pretrained("facebook/dpr-question_encoder-single-nq-base") +generator_tokenizer = AutoTokenizer.from_pretrained("facebook/bart-large") +generator_model = AutoModelForSeq2SeqLM.from_pretrained("facebook/bart-large") + +retriever = FeastRAGRetriever( + question_encoder_tokenizer=question_tokenizer, + question_encoder=question_encoder, + generator_tokenizer=generator_tokenizer, + generator_model=generator_model, + feast_repo_path="path/to/feature_repo", + feature_view="wiki_passages", + features=["passage_text", "embedding"], + search_type="vector", # "text", "vector", or "hybrid" + id_field="passage_id", + text_field="passage_text", +) +``` + +### Retrieve documents +```python +doc_embeddings, doc_ids, doc_dicts = retriever.retrieve( + question_input_ids=question_tokenizer("What is ML?", return_tensors="pt")["input_ids"], + n_docs=5, +) +``` + +### End-to-end answer generation +```python +answer = retriever.generate_answer( + query="What is machine learning?", + top_k=5, + max_new_tokens=200, +) +print(answer) +``` + +### FeastVectorStore (lower-level) + +```python +from feast.vector_store import FeastVectorStore + +vector_store = FeastVectorStore(feast_repo_path="path/to/feature_repo") + +results = vector_store.query( + query_vector=embedding_list, + top_k=10, +) +``` + +Supports `query_vector`, `query_string`, and `query_image_bytes` for different search modalities. + +## FeatureStore API Quick Reference + +| Method | Purpose | +|--------|---------| +| `apply(objects)` | Register entities, FVs, ODFVs, SFVs, services, sources | +| `plan(desired_registry)` | Preview apply changes | +| `get_online_features(features, entity_rows)` | Low-latency online lookup | +| `get_historical_features(entity_df, features)` | Point-in-time training data | +| `materialize(start_date, end_date)` | Load offline → online store | +| `materialize_incremental(end_date)` | Incremental materialization | +| `push(push_source_name, df)` | Push data to online/offline store | +| `write_to_online_store(fv_name, df)` | Direct write to online store | +| `write_to_offline_store(fv_name, df)` | Direct write to offline store | +| `retrieve_online_documents(feature, query, top_k)` | Vector similarity search | +| `retrieve_online_documents_v2(...)` | Vector search v2 (text/vector/image) | +| `list_entities()` | List all entities | +| `list_feature_views()` | List all feature views | +| `list_on_demand_feature_views()` | List on-demand feature views | +| `list_stream_feature_views()` | List stream feature views | +| `list_feature_services()` | List feature services | +| `list_data_sources()` | List data sources | +| `get_entity(name)` | Get entity by name | +| `get_feature_view(name)` | Get feature view by name | +| `get_feature_service(name)` | Get feature service by name | +| `delete_feature_view(name)` | Delete a feature view | +| `delete_feature_service(name)` | Delete a feature service | +| `create_saved_dataset(...)` | Save a dataset for reuse | +| `refresh_registry()` | Force refresh registry cache | +| `teardown()` | Remove all infrastructure resources | +| `serve(port)` | Start feature server | +| `serve_ui(port)` | Start Feast UI | +| `serve_registry(port)` | Start registry server | +| `serve_offline(port)` | Start offline server | diff --git a/ui/.prettierignore b/ui/.prettierignore index d2fa6a8b18e..14a40bc5836 100644 --- a/ui/.prettierignore +++ b/ui/.prettierignore @@ -1,3 +1,4 @@ *.css *.md dist/ +build/ diff --git a/ui/feature_repo/apply_permissions.py b/ui/feature_repo/apply_permissions.py index b7d39733634..4a62b8de9c3 100644 --- a/ui/feature_repo/apply_permissions.py +++ b/ui/feature_repo/apply_permissions.py @@ -11,15 +11,17 @@ store = FeatureStore(repo_path=".") -store.apply([ - zipcode_features_permission, - zipcode_source_permission, - model_v1_permission, - risky_features_permission, - document_embeddings_permission, - document_metadata_permission, - rag_model_permission, -]) +store.apply( + [ + zipcode_features_permission, + zipcode_source_permission, + model_v1_permission, + risky_features_permission, + document_embeddings_permission, + document_metadata_permission, + rag_model_permission, + ] +) print("Permissions applied successfully!") print("Current permissions:", store.list_permissions()) diff --git a/ui/feature_repo/apply_rag_data.py b/ui/feature_repo/apply_rag_data.py index 67d000f9d6c..be20dab38ab 100644 --- a/ui/feature_repo/apply_rag_data.py +++ b/ui/feature_repo/apply_rag_data.py @@ -5,28 +5,32 @@ now = datetime.now() embeddings = [] for i in range(10): - embeddings.append({ - 'document_id': f'doc_{i}', - 'embedding': np.random.rand(768).astype(np.float32), - 'event_timestamp': now - timedelta(days=i), - 'created_timestamp': now - timedelta(days=i, hours=1) - }) + embeddings.append( + { + "document_id": f"doc_{i}", + "embedding": np.random.rand(768).astype(np.float32), + "event_timestamp": now - timedelta(days=i), + "created_timestamp": now - timedelta(days=i, hours=1), + } + ) df_embeddings = pd.DataFrame(embeddings) -df_embeddings.to_parquet('data/document_embeddings.parquet', index=False) +df_embeddings.to_parquet("data/document_embeddings.parquet", index=False) metadata = [] for i in range(10): - metadata.append({ - 'document_id': f'doc_{i}', - 'title': f'Document {i}', - 'content': f'This is the content of document {i}', - 'source': 'web', - 'author': f'author_{i}', - 'publish_date': (now - timedelta(days=i*30)).strftime('%Y-%m-%d'), - 'event_timestamp': now - timedelta(days=i), - 'created_timestamp': now - timedelta(days=i, hours=1) - }) + metadata.append( + { + "document_id": f"doc_{i}", + "title": f"Document {i}", + "content": f"This is the content of document {i}", + "source": "web", + "author": f"author_{i}", + "publish_date": (now - timedelta(days=i * 30)).strftime("%Y-%m-%d"), + "event_timestamp": now - timedelta(days=i), + "created_timestamp": now - timedelta(days=i, hours=1), + } + ) df_metadata = pd.DataFrame(metadata) -df_metadata.to_parquet('data/document_metadata.parquet', index=False) +df_metadata.to_parquet("data/document_metadata.parquet", index=False) -print('Created RAG data files successfully!') +print("Created RAG data files successfully!") diff --git a/ui/feature_repo/features.py b/ui/feature_repo/features.py index 102dec74c7b..1c6854e257a 100644 --- a/ui/feature_repo/features.py +++ b/ui/feature_repo/features.py @@ -1,7 +1,6 @@ from datetime import timedelta import pandas as pd -import numpy as np from feast import Entity, FeatureService, FeatureView, Field, FileSource from feast.data_source import RequestSource @@ -139,6 +138,7 @@ ], ) + # Define an on demand feature view which can generate new features based on # existing feature views and RequestSource features @on_demand_feature_view( @@ -306,6 +306,7 @@ def transaction_gt_last_credit_card_due(inputs: pd.DataFrame) -> pd.DataFrame: ], ) + # Define an on-demand feature view for similarity calculation @on_demand_feature_view( sources=[document_embeddings_view, query_request], @@ -319,6 +320,7 @@ def document_similarity(inputs: pd.DataFrame) -> pd.DataFrame: df["similarity_score"] = 0.95 # Placeholder value return df + rag_model = FeatureService( name="rag_retriever", features=[ diff --git a/ui/jest.config.js b/ui/jest.config.js index 6bf85f961b6..d8ef66218b3 100644 --- a/ui/jest.config.js +++ b/ui/jest.config.js @@ -1,4 +1,11 @@ -const transformNodeModules = ["@elastic/eui", "uuid"]; +const transformNodeModules = [ + "@elastic/eui", + "uuid", + "msw", + "until-async", + "chroma-js", + "keycloak-js", +]; module.exports = { roots: ["/src"], @@ -30,7 +37,7 @@ module.exports = { moduleNameMapper: { "^react-native$": "react-native-web", "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy", - "chroma-js": "/node_modules/chroma-js/dist/chroma.min.cjs", + "^chroma-js$": "/node_modules/chroma-js/dist/chroma.min.cjs", "^reactflow/dist/style\\.css$": "identity-obj-proxy", }, moduleFileExtensions: [ diff --git a/ui/package-lock.json b/ui/package-lock.json index 049ada8b001..c6ba3dd2299 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "@feast-dev/feast-ui", - "version": "0.52.0", + "version": "0.64.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@feast-dev/feast-ui", - "version": "0.52.0", + "version": "0.64.0", "license": "Apache-2.0", "dependencies": { "@elastic/datemath": "^5.0.3", @@ -18,6 +18,7 @@ "@types/styled-components": "^5.1.34", "dagre": "^0.8.5", "inter-ui": "^3.19.3", + "keycloak-js": "^26.2.4", "long": "^5.2.3", "moment": "^2.29.1", "protobufjs": "^7.1.1", @@ -133,20 +134,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -162,9 +149,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -172,22 +159,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -213,9 +200,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz", - "integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.5.tgz", + "integrity": "sha512-fcdRcWahONYo+JRnJg1/AekOacGvKx12Gu0qXJXFi2WBqQA1i7+O5PaxRB7kxE/Op94dExnCiiar6T09pvdHpA==", "dev": true, "license": "MIT", "dependencies": { @@ -252,13 +239,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -308,18 +295,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", + "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "engines": { @@ -340,14 +327,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -394,14 +381,14 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -521,9 +508,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -555,26 +542,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -584,14 +571,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1169,9 +1156,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", + "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", "dev": true, "license": "MIT", "dependencies": { @@ -1219,9 +1206,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", - "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "dev": true, "license": "MIT", "dependencies": { @@ -1230,7 +1217,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1257,14 +1244,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1357,9 +1344,9 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", "dev": true, "license": "MIT", "dependencies": { @@ -1473,9 +1460,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", "dev": true, "license": "MIT", "dependencies": { @@ -1539,16 +1526,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-module-transforms": "^7.28.3", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1640,9 +1627,9 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", "dev": true, "license": "MIT", "dependencies": { @@ -1650,7 +1637,7 @@ "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1693,9 +1680,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1862,9 +1849,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", - "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", "dev": true, "license": "MIT", "dependencies": { @@ -1911,9 +1898,9 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", - "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", + "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2023,14 +2010,14 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", - "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", + "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" @@ -2110,17 +2097,17 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.0", + "@babel/compat-data": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", @@ -2133,42 +2120,42 @@ "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-block-scoping": "^7.28.5", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-destructuring": "^7.28.5", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", + "@babel/plugin-transform-regenerator": "^7.28.4", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -2233,15 +2220,15 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", - "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" @@ -2254,9 +2241,9 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", - "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", "dev": true, "license": "MIT", "dependencies": { @@ -2264,7 +2251,7 @@ "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.27.1" + "@babel/plugin-transform-typescript": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2274,9 +2261,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2297,17 +2284,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -2315,13 +2302,13 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2340,37 +2327,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@bundled-es-modules/cookie": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", - "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cookie": "^0.7.2" - } - }, - "node_modules/@bundled-es-modules/statuses": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", - "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", - "dev": true, - "license": "ISC", - "dependencies": { - "statuses": "^2.0.1" - } - }, - "node_modules/@bundled-es-modules/tough-cookie": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", - "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@types/tough-cookie": "^4.0.5", - "tough-cookie": "^4.1.4" - } - }, "node_modules/@csstools/normalize.css": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz", @@ -2901,9 +2857,9 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -2920,9 +2876,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -2961,9 +2917,9 @@ "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3084,15 +3040,25 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/confirm": { - "version": "5.1.14", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", - "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" }, "engines": { "node": ">=18" @@ -3107,20 +3073,20 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", - "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.13", - "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" @@ -3148,9 +3114,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", - "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "dev": true, "license": "MIT", "engines": { @@ -3158,9 +3124,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", - "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "dev": true, "license": "MIT", "engines": { @@ -3865,9 +3831,9 @@ } }, "node_modules/@jest/transform/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz", + "integrity": "sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g==", "dev": true, "license": "MIT", "dependencies": { @@ -3937,6 +3903,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -3964,9 +3941,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -4030,9 +4007,9 @@ } }, "node_modules/@mswjs/interceptors": { - "version": "0.39.6", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.6.tgz", - "integrity": "sha512-bndDP83naYYkfayr/qhBHMhk0YGwS1iv6vaEGcr0SQbO0IZtbOPqjKjds/WcG+bJA+1T5vCx6kprKOzn5Bg+Vw==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", + "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4370,9 +4347,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -4529,9 +4506,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", + "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", "dev": true, "license": "MIT" }, @@ -4867,9 +4844,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz", - "integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, "license": "MIT", "dependencies": { @@ -5049,13 +5026,6 @@ "@types/node": "*" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -5095,9 +5065,9 @@ } }, "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, "node_modules/@types/d3-axis": { @@ -5345,22 +5315,22 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", "dev": true, "license": "MIT", "dependencies": { @@ -5371,9 +5341,9 @@ } }, "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", "dev": true, "license": "MIT", "dependencies": { @@ -5456,9 +5426,9 @@ "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", "dev": true, "license": "MIT", "dependencies": { @@ -5588,18 +5558,18 @@ } }, "node_modules/@types/node": { - "version": "22.17.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz", - "integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==", + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/node-forge": { - "version": "1.3.13", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.13.tgz", - "integrity": "sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==", + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", "dev": true, "license": "MIT", "dependencies": { @@ -5651,13 +5621,13 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", - "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", "dependencies": { "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { @@ -5705,20 +5675,19 @@ "license": "MIT" }, "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", "dev": true, "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -5733,15 +5702,26 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", - "@types/send": "*" + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/@types/sockjs": { @@ -5769,14 +5749,14 @@ "license": "MIT" }, "node_modules/@types/styled-components": { - "version": "5.1.34", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz", - "integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==", + "version": "5.1.36", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.36.tgz", + "integrity": "sha512-pGMRNY5G2rNDKEv2DOiFYa7Ft1r0jrhmgBwHhOMzPTgCjO76bCot0/4uEfqj7K0Jf1KdQmDtAuaDk9EAs9foSw==", "license": "MIT", "dependencies": { "@types/hoist-non-react-statics": "*", "@types/react": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/stylis": { @@ -5822,9 +5802,9 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, "license": "MIT", "dependencies": { @@ -6819,9 +6799,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", "dev": true, "funding": [ { @@ -6839,9 +6819,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -6873,9 +6853,9 @@ } }, "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", "dev": true, "license": "MPL-2.0", "engines": { @@ -6933,9 +6913,9 @@ } }, "node_modules/babel-jest/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz", + "integrity": "sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g==", "dev": true, "license": "MIT", "dependencies": { @@ -7200,6 +7180,16 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", + "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -7376,9 +7366,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", - "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "dev": true, "funding": [ { @@ -7396,10 +7386,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001733", - "electron-to-chromium": "^1.5.199", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -7554,9 +7545,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001735", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", - "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "version": "1.0.30001756", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", "dev": true, "funding": [ { @@ -7858,9 +7849,9 @@ } }, "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, "license": "MIT" }, @@ -8051,13 +8042,13 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/cookie-signature": { @@ -8068,9 +8059,9 @@ "license": "MIT" }, "node_modules/core-js": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz", - "integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==", + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -8079,13 +8070,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz", - "integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==", + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.25.1" + "browserslist": "^4.28.0" }, "funding": { "type": "opencollective", @@ -8093,9 +8084,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.0.tgz", - "integrity": "sha512-OtwjqcDpY2X/eIIg1ol/n0y/X8A9foliaNt1dSK0gV3J2/zw+89FcNG3mPK+N8YWts4ZFUPxnrAzsxs/lf8yDA==", + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz", + "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8146,9 +8137,9 @@ "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -8610,9 +8601,9 @@ "license": "MIT" }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/d3-color": { @@ -8807,9 +8798,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -8840,9 +8831,9 @@ } }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -9251,9 +9242,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.203", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.203.tgz", - "integrity": "sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g==", + "version": "1.5.258", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.258.tgz", + "integrity": "sha512-rHUggNV5jKQ0sSdWwlaRDkFc3/rRJIVnOSe9yR4zrR07m3ZxhP4N27Hlg8VeJGGYgFTxK5NqDmWI4DSH72vIJg==", "dev": true, "license": "ISC" }, @@ -9335,9 +9326,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -10140,9 +10131,9 @@ } }, "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -10585,9 +10576,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, "funding": [ { @@ -11048,9 +11039,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -11083,16 +11074,16 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -11189,6 +11180,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11490,9 +11491,9 @@ } }, "node_modules/graphql": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", - "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "dev": true, "license": "MIT", "engines": { @@ -11968,9 +11969,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", - "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz", + "integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==", "dev": true, "license": "MIT", "dependencies": { @@ -12645,14 +12646,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -13134,9 +13136,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13805,9 +13807,9 @@ } }, "node_modules/jest-haste-map/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz", + "integrity": "sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g==", "dev": true, "license": "MIT", "dependencies": { @@ -14743,9 +14745,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -14756,9 +14758,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/chalk": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", - "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { @@ -14819,9 +14821,9 @@ } }, "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -14898,9 +14900,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -14922,9 +14924,9 @@ } }, "node_modules/jsdoc": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz", - "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", + "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -15137,6 +15139,15 @@ "node": ">=4.0" } }, + "node_modules/keycloak-js": { + "version": "26.2.4", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.4.tgz", + "integrity": "sha512-PnXpR3ubETGOt0B/Qt2lxmPbkZr5bc3vlQsOqDoTPPQsZRp7JjhTKxlJ187uWh8qJhvBab6Gsjb06a8ayOPfuw==", + "license": "Apache-2.0", + "workspaces": [ + "test" + ] + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -15208,9 +15219,9 @@ } }, "node_modules/launch-editor": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", - "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", + "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -15269,13 +15280,17 @@ } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -15758,11 +15773,11 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -15812,30 +15827,30 @@ "license": "MIT" }, "node_modules/msw": { - "version": "2.10.5", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.10.5.tgz", - "integrity": "sha512-0EsQCrCI1HbhpBWd89DvmxY6plmvrM96b0sCIztnvcNHQbXn5vqwm1KlXslo6u4wN9LFGLC1WFjjgljcQhe40A==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.2.tgz", + "integrity": "sha512-Fsr8AR5Yu6C0thoWa1Z8qGBFQLDvLsWlAn/v3CNLiUizoRqBYArK3Ex3thXpMWRr1Li5/MKLOEZ5mLygUmWi1A==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@bundled-es-modules/cookie": "^2.0.1", - "@bundled-es-modules/statuses": "^1.0.1", - "@bundled-es-modules/tough-cookie": "^0.1.6", "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.39.1", + "@mswjs/interceptors": "^0.40.0", "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/until": "^2.1.0", - "@types/cookie": "^0.6.0", "@types/statuses": "^2.0.4", + "cookie": "^1.0.2", "graphql": "^16.8.1", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", + "rettime": "^0.7.0", + "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", "type-fest": "^4.26.1", + "until-async": "^3.0.2", "yargs": "^17.7.2" }, "bin": { @@ -15856,6 +15871,19 @@ } } }, + "node_modules/msw/node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/msw/node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -15989,9 +16017,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -16064,9 +16092,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "dev": true, "license": "MIT" }, @@ -18575,6 +18603,12 @@ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", "license": "MIT" }, + "node_modules/react-code-blocks/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, "node_modules/react-code-blocks/node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -18980,12 +19014,12 @@ } }, "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.1" }, "engines": { "node": ">=14.0.0" @@ -18995,13 +19029,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" }, "engines": { "node": ">=14.0.0" @@ -19034,16 +19068,16 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", - "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "version": "15.6.6", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", + "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", - "prismjs": "^1.27.0", + "prismjs": "^1.30.0", "refractor": "^3.6.0" }, "peerDependencies": { @@ -19233,9 +19267,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "dev": true, "license": "MIT", "dependencies": { @@ -19280,18 +19314,18 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -19305,31 +19339,18 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/rehype-raw": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-5.1.0.tgz", @@ -19510,12 +19531,12 @@ } }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -19606,6 +19627,13 @@ "node": ">= 4" } }, + "node_modules/rettime": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", + "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", + "dev": true, + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -19748,9 +19776,9 @@ } }, "node_modules/rollup-plugin-import-css/node_modules/@rollup/pluginutils": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", - "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -20135,9 +20163,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "dependencies": { @@ -20213,9 +20241,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -21415,13 +21443,17 @@ "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/temp-dir": { @@ -21467,14 +21499,14 @@ } }, "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -21602,6 +21634,26 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tldts": { + "version": "7.0.18", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.18.tgz", + "integrity": "sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.18" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.18", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.18.tgz", + "integrity": "sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -22015,9 +22067,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "dev": true, "license": "MIT", "engines": { @@ -22025,9 +22077,9 @@ } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "dev": true, "license": "MIT", "engines": { @@ -22190,6 +22242,16 @@ "node": ">= 0.8" } }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -22202,9 +22264,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -22328,9 +22390,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -22502,9 +22564,9 @@ } }, "node_modules/webpack": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.2.tgz", - "integrity": "sha512-4JLXU0tD6OZNVqlwzm3HGEhAHufSiyv+skb7q0d2367VDMzrU1Q/ZeepvkcHH0rZie6uqEtTQQe0OEOOluH3Mg==", + "version": "5.103.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", "dependencies": { @@ -22516,7 +22578,7 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", + "browserslist": "^4.26.3", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", @@ -22525,13 +22587,13 @@ "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", + "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, "bin": { @@ -23443,9 +23505,9 @@ } }, "node_modules/yoctocolors-cjs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", "dev": true, "license": "MIT", "engines": { diff --git a/ui/package.json b/ui/package.json index f087d6cad14..f024cb22548 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "@feast-dev/feast-ui", - "version": "0.56.0", + "version": "0.65.0", "private": false, "files": [ "dist" @@ -32,6 +32,7 @@ "@types/styled-components": "^5.1.34", "dagre": "^0.8.5", "inter-ui": "^3.19.3", + "keycloak-js": "^26.2.4", "long": "^5.2.3", "moment": "^2.29.1", "protobufjs": "^7.1.1", diff --git a/ui/public/projects-list.json b/ui/public/projects-list.json index 238df4b5b41..4868adf6d88 100644 --- a/ui/public/projects-list.json +++ b/ui/public/projects-list.json @@ -3,7 +3,7 @@ { "name": "Credit Score Project", "description": "Project for credit scoring team and associated models.", - "id": "credit_score_project", + "id": "credit_scoring_aws", "registryPath": "/registry.db" }, { diff --git a/ui/public/registry.db b/ui/public/registry.db index ae9a05a4a97..0c16e405ccb 100644 Binary files a/ui/public/registry.db and b/ui/public/registry.db differ diff --git a/ui/src/FeastUISansProviders.test.tsx b/ui/src/FeastUISansProviders.test.tsx index cf4a01621de..af4eca68eca 100644 --- a/ui/src/FeastUISansProviders.test.tsx +++ b/ui/src/FeastUISansProviders.test.tsx @@ -10,33 +10,17 @@ import { import userEvent from "@testing-library/user-event"; import FeastUISansProviders from "./FeastUISansProviders"; -import { - projectsListWithDefaultProject, - creditHistoryRegistry, - creditHistoryRegistryDB, -} from "./mocks/handlers"; +import { allRestHandlers } from "./mocks/handlers"; import { readFileSync } from "fs"; import { feast } from "./protos"; import path from "path"; // declare which API requests to mock -const server = setupServer( - projectsListWithDefaultProject, - creditHistoryRegistry, - creditHistoryRegistryDB, -); +const server = setupServer(...allRestHandlers); const registry = readFileSync(path.resolve(__dirname, "../public/registry.db")); const parsedRegistry = feast.core.Registry.decode(registry); -console.log("Registry Feature Views:", parsedRegistry.featureViews?.length); -if (parsedRegistry.featureViews && parsedRegistry.featureViews.length > 0) { - console.log( - "First Feature View Name:", - parsedRegistry.featureViews[0].spec?.name, - ); -} - // establish API mocking before all tests beforeAll(() => server.listen()); // reset any request handlers that are declared as a part of our tests @@ -64,14 +48,12 @@ test("full app rendering", async () => { // Explore Panel Should Appear expect(screen.getByText(/Explore this Project/i)).toBeInTheDocument(); - const projectNameRegExp = new RegExp( - parsedRegistry.projects[0].spec?.name!, - "i", - ); - // It should load the default project, which is credit_scoring_aws + // The heading shows the display name from projects-list.json await waitFor(() => { - expect(screen.getByText(projectNameRegExp)).toBeInTheDocument(); + expect( + screen.getByRole("heading", { name: /Credit Score Project/i }), + ).toBeInTheDocument(); }); }); @@ -83,27 +65,27 @@ test("routes are reachable", async () => { // Wait for content to load await screen.findByText(/Explore this Project/i); - const mainRoutesNames = [ - "Data Sources", - "Entities", - "Feature Views", - "Feature Services", - "Datasets", + const mainRoutes = [ + { link: "Data Sources", heading: "Data Sources" }, + { link: "Entities", heading: "Entities" }, + { link: "Feature Views", heading: "Feature Views" }, + { link: "Feature Services", heading: "Feature Services" }, + { link: "Datasets", heading: "Data Catalog" }, ]; - for (const routeName of mainRoutesNames) { + for (const { link, heading } of mainRoutes) { // Main heading shouldn't start with the route name expect( - screen.queryByRole("heading", { name: routeName, level: 1 }), + screen.queryByRole("heading", { name: heading, level: 1 }), ).toBeNull(); - const routeRegExp = new RegExp(routeName, "i"); + const routeRegExp = new RegExp(link, "i"); await user.click(screen.getByRole("link", { name: routeRegExp })); // Should land on a page with the heading screen.getByRole("heading", { - name: routeName, + name: heading, level: 1, }); } diff --git a/ui/src/FeastUISansProviders.tsx b/ui/src/FeastUISansProviders.tsx index 9a2207e22dd..60a02aa1d56 100644 --- a/ui/src/FeastUISansProviders.tsx +++ b/ui/src/FeastUISansProviders.tsx @@ -22,14 +22,21 @@ import FeatureServiceInstance from "./pages/feature-services/FeatureServiceInsta import DataSourceInstance from "./pages/data-sources/DataSourceInstance"; import RootProjectSelectionPage from "./pages/RootProjectSelectionPage"; import DatasetInstance from "./pages/saved-data-sets/DatasetInstance"; -import DocumentLabelingPage from "./pages/document-labeling/DocumentLabelingPage"; +import LabelViewIndex from "./pages/label-views/Index"; +import LabelViewInstance from "./pages/label-views/LabelViewInstance"; import PermissionsIndex from "./pages/permissions/Index"; import LineageIndex from "./pages/lineage/Index"; import NoProjectGuard from "./components/NoProjectGuard"; +import MonitoringIndex from "./pages/monitoring/Index"; +import FeatureMetricsDetail from "./pages/monitoring/FeatureMetricsDetail"; +import ComputeEngineIndex from "./pages/compute-engines/Index"; import TabsRegistryContext, { FeastTabsRegistryInterface, } from "./custom-tabs/TabsRegistryContext"; +import MonitoringContext, { + MonitoringConfig, +} from "./contexts/MonitoringContext"; import CurlGeneratorTab from "./pages/feature-views/CurlGeneratorTab"; import FeatureFlagsContext, { FeatureFlags, @@ -38,11 +45,16 @@ import { ProjectListContext, ProjectsListContextInterface, } from "./contexts/ProjectListContext"; +import DataModeContext from "./contexts/DataModeContext"; +import type { DataModeConfig, FetchOptions } from "./contexts/DataModeContext"; +import { AuthProvider, useAuth } from "./contexts/AuthContext"; interface FeastUIConfigs { tabsRegistry?: FeastTabsRegistryInterface; featureFlags?: FeatureFlags; projectListPromise?: Promise; + fetchOptions?: FetchOptions; + monitoringConfig?: MonitoringConfig; } const defaultProjectListPromise = (basename: string) => { @@ -67,19 +79,23 @@ const FeastUISansProviders = ({ ? { projectsListPromise: feastUIConfigs?.projectListPromise, isCustom: true, + basename, } : { projectsListPromise: defaultProjectListPromise(basename), isCustom: false, + basename, }; return ( - + + + ); }; @@ -94,96 +110,160 @@ const FeastUISansProvidersInner = ({ feastUIConfigs?: FeastUIConfigs; }) => { const { colorMode } = useTheme(); + const { isInitializing } = useAuth(); + + const dataModeConfig: DataModeConfig = { + fetchOptions: feastUIConfigs?.fetchOptions, + }; + + const monitoringConfig: MonitoringConfig = + feastUIConfigs?.monitoringConfig || { + apiBaseUrl: "/api/v1", + enabled: true, + }; + + if (isInitializing) { + return ( + +
+
+

Connecting to identity provider...

+
+ + ); + } return ( - - + - - - }> - } /> - }> - } /> - } /> - } - /> - } /> - } - /> - } - > - } - /> - } - /> - } - /> - } /> - } - /> - - } /> - } - /> - } - /> - } /> - } /> - - - } /> - - - - + + + + + }> + } /> + } + > + } /> + } + /> + } + /> + } /> + } + /> + } + > + } + /> + } + /> + } + /> + } /> + } + /> + } + /> + } + /> + } + /> + } /> + } + /> + } + /> + } /> + } + /> + } + /> + } + /> + + + } /> + + + + + + ); diff --git a/ui/src/components/DataSourceFormModal.tsx b/ui/src/components/DataSourceFormModal.tsx new file mode 100644 index 00000000000..e3c838a349b --- /dev/null +++ b/ui/src/components/DataSourceFormModal.tsx @@ -0,0 +1,1745 @@ +import React, { useState } from "react"; +import { + EuiFormRow, + EuiFieldText, + EuiSelect, + EuiSpacer, + EuiHorizontalRule, + EuiText, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTextArea, + EuiTitle, +} from "@elastic/eui"; +import { feast } from "../protos"; +import FormModal from "./forms/FormModal"; +import TagsEditor, { TagEntry } from "./forms/TagsEditor"; +import { DATA_SOURCE_TYPES } from "../pages/data-sources/DataSourceCatalog"; + +const SOURCE_TYPE_OPTIONS = [ + { + value: String(feast.core.DataSource.SourceType.BATCH_FILE), + text: "File (Parquet / CSV)", + }, + { + value: String(feast.core.DataSource.SourceType.BATCH_BIGQUERY), + text: "BigQuery", + }, + { + value: String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE), + text: "Snowflake", + }, + { + value: String(feast.core.DataSource.SourceType.BATCH_REDSHIFT), + text: "Redshift", + }, + { + value: String(feast.core.DataSource.SourceType.BATCH_SPARK), + text: "Spark", + }, + { + value: String(feast.core.DataSource.SourceType.BATCH_TRINO), + text: "Trino", + }, + { + value: String(feast.core.DataSource.SourceType.BATCH_ATHENA), + text: "AWS Athena", + }, + { + value: String(feast.core.DataSource.SourceType.BATCH_ICEBERG), + text: "Iceberg / Unity Catalog", + }, + { + value: String(feast.core.DataSource.SourceType.STREAM_KAFKA), + text: "Kafka", + }, + { + value: String(feast.core.DataSource.SourceType.STREAM_KINESIS), + text: "AWS Kinesis", + }, + { + value: String(feast.core.DataSource.SourceType.REQUEST_SOURCE), + text: "Request Source", + }, + { + value: String(feast.core.DataSource.SourceType.PUSH_SOURCE), + text: "Push Source", + }, + { + value: String(feast.core.DataSource.SourceType.CUSTOM_SOURCE), + text: "Custom Source", + }, + { value: "RAY_SOURCE", text: "Ray" }, + { value: "POSTGRES_SOURCE", text: "PostgreSQL" }, + { value: "MONGODB_SOURCE", text: "MongoDB" }, + { value: "CLICKHOUSE_SOURCE", text: "ClickHouse" }, + { value: "MSSQL_SOURCE", text: "SQL Server" }, + { value: "ORACLE_SOURCE", text: "Oracle" }, + { value: "COUCHBASE_SOURCE", text: "Couchbase" }, +]; + +interface DataSourceFormData { + name: string; + description: string; + owner: string; + sourceType: string; + timestampField: string; + createdTimestampColumn: string; + tags: TagEntry[]; + // File source + fileUri: string; + fileFormat: string; + fileS3EndpointOverride: string; + // BigQuery + bigqueryTable: string; + bigqueryQuery: string; + bigqueryDatePartitionColumn: string; + // Snowflake + snowflakeTable: string; + snowflakeDatabase: string; + snowflakeSchema: string; + snowflakeQuery: string; + snowflakeWarehouse: string; + // Redshift + redshiftTable: string; + redshiftDatabase: string; + redshiftSchema: string; + redshiftQuery: string; + // Kafka + kafkaBootstrapServers: string; + kafkaTopic: string; + kafkaMessageFormat: string; + kafkaWatermarkDelay: string; + // Spark + sparkTable: string; + sparkPath: string; + sparkQuery: string; + sparkFileFormat: string; + sparkTableFormat: string; + sparkTableFormatCatalog: string; + sparkTableFormatNamespace: string; + sparkTableFormatProperties: string; + sparkDatePartitionColumn: string; + sparkDatePartitionFormat: string; + // Kinesis + kinesisRegion: string; + kinesisStreamName: string; + kinesisRecordFormat: string; + // Trino + trinoTable: string; + trinoQuery: string; + // Athena + athenaTable: string; + athenaQuery: string; + athenaDatabase: string; + athenaDataSource: string; + athenaDatePartitionColumn: string; + // Custom + customSourceClassName: string; + customSourceConfig: string; + // Iceberg / Unity Catalog + icebergCatalogType: string; + icebergEndpoint: string; + icebergWarehouse: string; + icebergNamespace: string; + icebergTable: string; + icebergTokenEnvVar: string; + icebergCredentialVending: string; + icebergCatalogProperties: string; + // Ray + rayReaderType: string; + rayPath: string; + rayReaderOptions: string; + // Postgres + postgresTable: string; + postgresQuery: string; + // MongoDB + mongodbCollection: string; + // ClickHouse + clickhouseTable: string; + clickhouseQuery: string; + // MSSQL + mssqlTable: string; + mssqlConnectionStr: string; + mssqlDatePartitionColumn: string; + // Oracle + oracleTable: string; + oracleConnectionStr: string; + oracleDatePartitionColumn: string; + // Couchbase + couchbaseDatabase: string; + couchbaseScope: string; + couchbaseCollection: string; + couchbaseQuery: string; +} + +interface DataSourceFormModalProps { + onClose: () => void; + onSubmit: (data: DataSourceFormData) => void; + initialData?: DataSourceFormData; + isEdit?: boolean; + isSubmitting?: boolean; + submitError?: string | null; +} + +const EMPTY_FORM: DataSourceFormData = { + name: "", + description: "", + owner: "", + sourceType: String(feast.core.DataSource.SourceType.BATCH_FILE), + timestampField: "", + createdTimestampColumn: "", + tags: [], + fileUri: "", + fileFormat: "parquet", + fileS3EndpointOverride: "", + bigqueryTable: "", + bigqueryQuery: "", + bigqueryDatePartitionColumn: "", + snowflakeTable: "", + snowflakeDatabase: "", + snowflakeSchema: "", + snowflakeQuery: "", + snowflakeWarehouse: "", + redshiftTable: "", + redshiftDatabase: "", + redshiftSchema: "", + redshiftQuery: "", + kafkaBootstrapServers: "", + kafkaTopic: "", + kafkaMessageFormat: "json", + kafkaWatermarkDelay: "", + sparkTable: "", + sparkPath: "", + sparkQuery: "", + sparkFileFormat: "parquet", + sparkTableFormat: "", + sparkTableFormatCatalog: "", + sparkTableFormatNamespace: "", + sparkTableFormatProperties: "", + sparkDatePartitionColumn: "", + sparkDatePartitionFormat: "%Y-%m-%d", + kinesisRegion: "", + kinesisStreamName: "", + kinesisRecordFormat: "json", + trinoTable: "", + trinoQuery: "", + athenaTable: "", + athenaQuery: "", + athenaDatabase: "", + athenaDataSource: "", + athenaDatePartitionColumn: "", + customSourceClassName: "", + customSourceConfig: "", + icebergCatalogType: "rest", + icebergEndpoint: "", + icebergWarehouse: "", + icebergNamespace: "", + icebergTable: "", + icebergTokenEnvVar: "", + icebergCredentialVending: "true", + icebergCatalogProperties: "", + rayReaderType: "parquet", + rayPath: "", + rayReaderOptions: "", + postgresTable: "", + postgresQuery: "", + mongodbCollection: "", + clickhouseTable: "", + clickhouseQuery: "", + mssqlTable: "", + mssqlConnectionStr: "", + mssqlDatePartitionColumn: "", + oracleTable: "", + oracleConnectionStr: "", + oracleDatePartitionColumn: "", + couchbaseDatabase: "", + couchbaseScope: "", + couchbaseCollection: "", + couchbaseQuery: "", +}; + +const BATCH_SOURCE_TYPES = new Set([ + String(feast.core.DataSource.SourceType.BATCH_FILE), + String(feast.core.DataSource.SourceType.BATCH_BIGQUERY), + String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE), + String(feast.core.DataSource.SourceType.BATCH_REDSHIFT), + String(feast.core.DataSource.SourceType.BATCH_SPARK), + String(feast.core.DataSource.SourceType.BATCH_TRINO), + String(feast.core.DataSource.SourceType.BATCH_ATHENA), + String(feast.core.DataSource.SourceType.BATCH_ICEBERG), + "RAY_SOURCE", + "POSTGRES_SOURCE", + "MONGODB_SOURCE", + "CLICKHOUSE_SOURCE", + "MSSQL_SOURCE", + "ORACLE_SOURCE", + "COUCHBASE_SOURCE", +]); + +const RAY_READER_OPTIONS = [ + { value: "parquet", text: "Parquet" }, + { value: "csv", text: "CSV" }, + { value: "json", text: "JSON" }, + { value: "text", text: "Text" }, + { value: "images", text: "Images" }, + { value: "binary_files", text: "Binary Files" }, + { value: "tfrecords", text: "TFRecords" }, + { value: "webdataset", text: "WebDataset" }, + { value: "huggingface", text: "HuggingFace" }, + { value: "mongo", text: "MongoDB (via Ray)" }, + { value: "sql", text: "SQL (via Ray)" }, +]; + +const DataSourceFormModal: React.FC = ({ + onClose, + onSubmit, + initialData, + isEdit = false, + isSubmitting = false, + submitError, +}) => { + const [formData, setFormData] = useState( + initialData || EMPTY_FORM, + ); + const [errors, setErrors] = useState>({}); + const [submitted, setSubmitted] = useState(false); + + const isBatchSource = BATCH_SOURCE_TYPES.has(formData.sourceType); + const isPreselected = !!initialData?.sourceType; + + const catalogEntry = DATA_SOURCE_TYPES.find( + (ds) => ds.sourceType === formData.sourceType, + ); + + const validate = (): boolean => { + const newErrors: Record = {}; + const st = formData.sourceType; + + if (!formData.name.trim()) { + newErrors.name = "Data source name is required."; + } else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.name)) { + newErrors.name = + "Must start with a letter or underscore, and contain only letters, numbers, and underscores."; + } + + if (st === String(feast.core.DataSource.SourceType.BATCH_FILE)) { + if (!formData.fileUri.trim()) { + newErrors.fileUri = "File URI is required."; + } else if ( + !/^(s3|gs|gcs|hdfs|abfs|file):\/\/\S+$/.test(formData.fileUri.trim()) + ) { + newErrors.fileUri = + "Must be a valid URI (e.g. s3://bucket/path, gs://bucket/path, file:///local/path)."; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_BIGQUERY)) { + if (!formData.bigqueryTable.trim() && !formData.bigqueryQuery.trim()) { + newErrors.bigqueryTable = + "Either a table reference or a query is required."; + } + } else if ( + st === String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE) + ) { + if (!formData.snowflakeTable.trim() && !formData.snowflakeQuery.trim()) { + newErrors.snowflakeTable = + "Either a table or a query is required for Snowflake."; + } + if (!formData.snowflakeDatabase.trim()) { + newErrors.snowflakeDatabase = "Database is required for Snowflake."; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_REDSHIFT)) { + if (!formData.redshiftTable.trim() && !formData.redshiftQuery.trim()) { + newErrors.redshiftTable = + "Either a table or a query is required for Redshift."; + } + if (!formData.redshiftDatabase.trim()) { + newErrors.redshiftDatabase = "Database is required for Redshift."; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SPARK)) { + if ( + !formData.sparkTable.trim() && + !formData.sparkPath.trim() && + !formData.sparkQuery.trim() + ) { + newErrors.sparkTable = + "Either a table, path, or query is required for Spark."; + } else if ( + formData.sparkPath.trim() && + !formData.sparkTableFormat && + !formData.sparkFileFormat + ) { + newErrors.sparkFileFormat = + "File format is required when using a path without table format."; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_TRINO)) { + if (!formData.trinoTable.trim() && !formData.trinoQuery.trim()) { + newErrors.trinoTable = + "Either a table reference or a query is required for Trino."; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ATHENA)) { + if (!formData.athenaTable.trim() && !formData.athenaQuery.trim()) { + newErrors.athenaTable = + "Either a table reference or a query is required for Athena."; + } + if (!formData.athenaDatabase.trim()) { + newErrors.athenaDatabase = "Database is required for Athena."; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ICEBERG)) { + if (!formData.icebergWarehouse.trim()) { + newErrors.icebergWarehouse = "Warehouse is required."; + } + if (!formData.icebergNamespace.trim()) { + newErrors.icebergNamespace = "Namespace is required."; + } + if (!formData.icebergTable.trim()) { + newErrors.icebergTable = "Table name is required."; + } + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KAFKA)) { + if (!formData.kafkaBootstrapServers.trim()) { + newErrors.kafkaBootstrapServers = "Bootstrap servers are required."; + } else if ( + !/^[\w.-]+:\d+(,[\w.-]+:\d+)*$/.test( + formData.kafkaBootstrapServers.trim(), + ) + ) { + newErrors.kafkaBootstrapServers = + "Must be in host:port format (e.g. localhost:9092)."; + } + if (!formData.kafkaTopic.trim()) { + newErrors.kafkaTopic = "Topic is required."; + } + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KINESIS)) { + if (!formData.kinesisRegion.trim()) { + newErrors.kinesisRegion = "AWS region is required."; + } + if (!formData.kinesisStreamName.trim()) { + newErrors.kinesisStreamName = "Stream name is required."; + } + } else if (st === String(feast.core.DataSource.SourceType.CUSTOM_SOURCE)) { + if (!formData.customSourceClassName.trim()) { + newErrors.customSourceClassName = "Class name is required."; + } + } else if (st === "RAY_SOURCE") { + if ( + !formData.rayPath.trim() && + !["huggingface", "mongo", "sql"].includes(formData.rayReaderType) + ) { + newErrors.rayPath = "Path is required for this reader type."; + } + } else if (st === "POSTGRES_SOURCE") { + if (!formData.postgresTable.trim() && !formData.postgresQuery.trim()) { + newErrors.postgresTable = "Either a table or query is required."; + } + } else if (st === "CLICKHOUSE_SOURCE") { + if ( + !formData.clickhouseTable.trim() && + !formData.clickhouseQuery.trim() + ) { + newErrors.clickhouseTable = "Either a table or query is required."; + } + } else if (st === "MSSQL_SOURCE") { + if (!formData.mssqlTable.trim()) { + newErrors.mssqlTable = "Table reference is required."; + } + } else if (st === "ORACLE_SOURCE") { + if (!formData.oracleTable.trim()) { + newErrors.oracleTable = "Table reference is required."; + } + } else if (st === "COUCHBASE_SOURCE") { + if ( + !formData.couchbaseCollection.trim() && + !formData.couchbaseQuery.trim() + ) { + newErrors.couchbaseCollection = + "Either a collection or query is required."; + } + } + + if (isBatchSource && !formData.timestampField.trim()) { + newErrors.timestampField = + "Timestamp field is required for batch sources."; + } else if ( + formData.timestampField.trim() && + !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.timestampField.trim()) + ) { + newErrors.timestampField = + "Must be a valid column name (letters, numbers, underscores)."; + } + + const tagKeys = formData.tags.map((t) => t.key).filter((k) => k.trim()); + if (new Set(tagKeys).size !== tagKeys.length) { + newErrors.tags = "Tag keys must be unique."; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = () => { + setSubmitted(true); + if (validate()) { + const cleanedData = { + ...formData, + tags: formData.tags.filter((t) => t.key.trim()), + }; + onSubmit(cleanedData); + } + }; + + const updateField = ( + field: K, + value: DataSourceFormData[K], + ) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (submitted) { + setErrors((prev) => { + const next = { ...prev }; + delete next[field]; + return next; + }); + } + }; + + const renderFileSourceFields = () => ( + <> + + updateField("fileUri", e.target.value)} + isInvalid={!!errors.fileUri} + placeholder="s3://bucket/path/to/data.parquet" + /> + + + + + updateField("fileFormat", e.target.value)} + /> + + + + + + updateField("fileS3EndpointOverride", e.target.value) + } + placeholder="http://minio:9000" + /> + + + + + ); + + const renderSourceTypeHeader = () => { + if (!isPreselected || !catalogEntry) return null; + + const IconComponent = catalogEntry.icon; + return ( + + + +
+ +
+
+ + + {catalogEntry.name} + + + {catalogEntry.description} + + +
+
+ ); + }; + + const renderSourceTypeFields = () => { + const st = formData.sourceType; + + if (st === String(feast.core.DataSource.SourceType.BATCH_FILE)) { + return renderFileSourceFields(); + } + + if (st === String(feast.core.DataSource.SourceType.BATCH_BIGQUERY)) { + return ( + <> + + updateField("bigqueryTable", e.target.value)} + isInvalid={!!errors.bigqueryTable} + placeholder="project:dataset.table" + /> + + + updateField("bigqueryQuery", e.target.value)} + placeholder="SELECT * FROM `project.dataset.table` WHERE ..." + rows={3} + /> + + + + updateField("bigqueryDatePartitionColumn", e.target.value) + } + placeholder="date_partition" + /> + + + ); + } + + if (st === String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE)) { + return ( + <> + + + + + updateField("snowflakeDatabase", e.target.value) + } + isInvalid={!!errors.snowflakeDatabase} + placeholder="MY_DATABASE" + /> + + + + + + updateField("snowflakeSchema", e.target.value) + } + placeholder="PUBLIC" + /> + + + + + + + + updateField("snowflakeTable", e.target.value) + } + isInvalid={!!errors.snowflakeTable} + placeholder="MY_TABLE" + /> + + + + + + updateField("snowflakeWarehouse", e.target.value) + } + placeholder="COMPUTE_WH" + /> + + + + + updateField("snowflakeQuery", e.target.value)} + placeholder="SELECT * FROM MY_TABLE WHERE ..." + rows={3} + /> + + + ); + } + + if (st === String(feast.core.DataSource.SourceType.BATCH_REDSHIFT)) { + return ( + <> + + + + + updateField("redshiftDatabase", e.target.value) + } + isInvalid={!!errors.redshiftDatabase} + placeholder="my_database" + /> + + + + + + updateField("redshiftSchema", e.target.value) + } + placeholder="public" + /> + + + + + updateField("redshiftTable", e.target.value)} + isInvalid={!!errors.redshiftTable} + placeholder="my_table" + /> + + + updateField("redshiftQuery", e.target.value)} + placeholder="SELECT * FROM my_table WHERE ..." + rows={3} + /> + + + ); + } + + if (st === String(feast.core.DataSource.SourceType.STREAM_KAFKA)) { + return ( + <> + + + updateField("kafkaBootstrapServers", e.target.value) + } + isInvalid={!!errors.kafkaBootstrapServers} + placeholder="broker1:9092,broker2:9092" + /> + + + + + updateField("kafkaTopic", e.target.value)} + isInvalid={!!errors.kafkaTopic} + placeholder="my-feature-topic" + /> + + + + + + updateField("kafkaMessageFormat", e.target.value) + } + /> + + + + + + updateField("kafkaWatermarkDelay", e.target.value) + } + placeholder="30 seconds" + /> + + + ); + } + + if (st === String(feast.core.DataSource.SourceType.BATCH_SPARK)) { + return ( + <> + + updateField("sparkTable", e.target.value)} + isInvalid={!!errors.sparkTable} + placeholder="catalog.database.table" + /> + + + updateField("sparkPath", e.target.value)} + placeholder="s3://bucket/path/ or abfss://container@account/path/" + /> + + + updateField("sparkQuery", e.target.value)} + placeholder="SELECT * FROM catalog.db.table WHERE ..." + rows={3} + /> + + + + + + updateField("sparkFileFormat", e.target.value) + } + /> + + + + + + updateField("sparkTableFormat", e.target.value) + } + /> + + + + {formData.sparkTableFormat && ( + <> + + + + + updateField("sparkTableFormatCatalog", e.target.value) + } + placeholder="my_catalog" + /> + + + + + + updateField("sparkTableFormatNamespace", e.target.value) + } + placeholder="my_db" + /> + + + + + + updateField("sparkTableFormatProperties", e.target.value) + } + placeholder='{"warehouse": "s3://bucket/warehouse"}' + rows={2} + /> + + + )} + + + + + updateField("sparkDatePartitionColumn", e.target.value) + } + placeholder="date_partition" + /> + + + + + + updateField("sparkDatePartitionFormat", e.target.value) + } + placeholder="%Y-%m-%d" + /> + + + + + ); + } + + if (st === String(feast.core.DataSource.SourceType.BATCH_TRINO)) { + return ( + <> + + updateField("trinoTable", e.target.value)} + isInvalid={!!errors.trinoTable} + placeholder="catalog.schema.table" + /> + + + updateField("trinoQuery", e.target.value)} + placeholder="SELECT * FROM catalog.schema.table" + rows={3} + /> + + + ); + } + + if (st === String(feast.core.DataSource.SourceType.BATCH_ATHENA)) { + return ( + <> + + + + + updateField("athenaDatabase", e.target.value) + } + isInvalid={!!errors.athenaDatabase} + placeholder="my_database" + /> + + + + + + updateField("athenaDataSource", e.target.value) + } + placeholder="AwsDataCatalog" + /> + + + + + updateField("athenaTable", e.target.value)} + isInvalid={!!errors.athenaTable} + placeholder="my_table" + /> + + + updateField("athenaQuery", e.target.value)} + placeholder="SELECT * FROM my_table" + rows={3} + /> + + + + updateField("athenaDatePartitionColumn", e.target.value) + } + placeholder="date_partition" + /> + + + ); + } + + if (st === String(feast.core.DataSource.SourceType.BATCH_ICEBERG)) { + return ( + <> + + + updateField("icebergCatalogType", e.target.value) + } + /> + + + updateField("icebergEndpoint", e.target.value)} + placeholder="http://localhost:8080/api/2.1/unity-catalog/iceberg" + /> + + + + + + updateField("icebergWarehouse", e.target.value) + } + isInvalid={!!errors.icebergWarehouse} + placeholder="unity" + /> + + + + + + updateField("icebergNamespace", e.target.value) + } + isInvalid={!!errors.icebergNamespace} + placeholder="default" + /> + + + + + updateField("icebergTable", e.target.value)} + isInvalid={!!errors.icebergTable} + placeholder="driver_stats" + /> + + + + updateField("icebergTokenEnvVar", e.target.value) + } + placeholder="DATABRICKS_TOKEN" + /> + + + + updateField("icebergCatalogProperties", e.target.value) + } + placeholder='{"s3.region": "us-east-1"}' + rows={3} + /> + + + ); + } + + if (st === String(feast.core.DataSource.SourceType.STREAM_KINESIS)) { + return ( + <> + + + + updateField("kinesisRegion", e.target.value)} + isInvalid={!!errors.kinesisRegion} + placeholder="us-east-1" + /> + + + + + + updateField("kinesisRecordFormat", e.target.value) + } + /> + + + + + updateField("kinesisStreamName", e.target.value)} + isInvalid={!!errors.kinesisStreamName} + placeholder="my-feature-stream" + /> + + + ); + } + + if (st === String(feast.core.DataSource.SourceType.CUSTOM_SOURCE)) { + return ( + <> + + + updateField("customSourceClassName", e.target.value) + } + isInvalid={!!errors.customSourceClassName} + placeholder="mymodule.MyCustomDataSource" + /> + + + + updateField("customSourceConfig", e.target.value) + } + placeholder='{"key": "value"}' + rows={3} + /> + + + ); + } + + if (st === "RAY_SOURCE") { + return ( + <> + + updateField("rayReaderType", e.target.value)} + /> + + + updateField("rayPath", e.target.value)} + isInvalid={!!errors.rayPath} + placeholder="s3://bucket/images/" + /> + + + updateField("rayReaderOptions", e.target.value)} + placeholder='{"dataset_name": "org/name", "split": "train"}' + rows={3} + /> + + + ); + } + + if (st === "POSTGRES_SOURCE") { + return ( + <> + + updateField("postgresTable", e.target.value)} + isInvalid={!!errors.postgresTable} + placeholder="public.my_features" + /> + + + updateField("postgresQuery", e.target.value)} + placeholder="SELECT * FROM my_features WHERE ..." + rows={3} + /> + + + ); + } + + if (st === "MONGODB_SOURCE") { + return ( + + updateField("mongodbCollection", e.target.value)} + isInvalid={!!errors.mongodbCollection} + placeholder="features_collection" + /> + + ); + } + + if (st === "CLICKHOUSE_SOURCE") { + return ( + <> + + updateField("clickhouseTable", e.target.value)} + isInvalid={!!errors.clickhouseTable} + placeholder="default.my_features" + /> + + + updateField("clickhouseQuery", e.target.value)} + placeholder="SELECT * FROM default.my_features" + rows={3} + /> + + + ); + } + + if (st === "MSSQL_SOURCE") { + return ( + <> + + updateField("mssqlTable", e.target.value)} + isInvalid={!!errors.mssqlTable} + placeholder="dbo.my_features" + /> + + + + updateField("mssqlConnectionStr", e.target.value) + } + placeholder="mssql+pyodbc://user:pass@host/db" // pragma: allowlist secret + /> + + + + updateField("mssqlDatePartitionColumn", e.target.value) + } + placeholder="date_partition" + /> + + + ); + } + + if (st === "ORACLE_SOURCE") { + return ( + <> + + updateField("oracleTable", e.target.value)} + isInvalid={!!errors.oracleTable} + placeholder="SCHEMA.MY_FEATURES" + /> + + + + updateField("oracleConnectionStr", e.target.value) + } + placeholder="oracle+cx_oracle://user:pass@host:1521/service" // pragma: allowlist secret + /> + + + + updateField("oracleDatePartitionColumn", e.target.value) + } + placeholder="date_partition" + /> + + + ); + } + + if (st === "COUCHBASE_SOURCE") { + return ( + <> + + + + + updateField("couchbaseDatabase", e.target.value) + } + placeholder="Default" + /> + + + + + + updateField("couchbaseScope", e.target.value) + } + placeholder="Default" + /> + + + + + + updateField("couchbaseCollection", e.target.value) + } + isInvalid={!!errors.couchbaseCollection} + placeholder="my_collection" + /> + + + updateField("couchbaseQuery", e.target.value)} + placeholder="SELECT * FROM `collection`" + rows={3} + /> + + + ); + } + + if ( + st === String(feast.core.DataSource.SourceType.REQUEST_SOURCE) || + st === String(feast.core.DataSource.SourceType.PUSH_SOURCE) + ) { + return ( + + + No connection configuration needed. This source type receives data + at request time or via push ingestion. + + + ); + } + + return null; + }; + + const sourceTypeName = + SOURCE_TYPE_OPTIONS.find((o) => o.value === formData.sourceType)?.text || + "Data Source"; + + return ( + + {submitError && ( + <> + +

{submitError}

+
+ + + )} + + {isPreselected && renderSourceTypeHeader()} + {isPreselected && } + + {/* Section: Identity */} + +

Identity

+
+ + + + + + updateField("name", e.target.value)} + isInvalid={!!errors.name} + disabled={isEdit} + placeholder="e.g. customer_transactions" + /> + + + + + updateField("owner", e.target.value)} + placeholder="team@company.com" + /> + + + + + + updateField("description", e.target.value)} + placeholder="Brief description of this data source..." + /> + + + {!isPreselected && ( + <> + + + { + updateField("sourceType", e.target.value); + setErrors({}); + }} + disabled={isEdit} + /> + + + )} + + + + + {/* Section: Connection */} + +

Connection Details

+
+ + + {renderSourceTypeFields()} + + {/* Section: Timestamp (for batch sources) */} + {isBatchSource && ( + <> + + + +

Time Configuration

+
+ + + + + + + updateField("timestampField", e.target.value) + } + isInvalid={!!errors.timestampField} + placeholder="event_timestamp" + /> + + + + + + updateField("createdTimestampColumn", e.target.value) + } + placeholder="created_at" + /> + + + + + )} + + {/* Section: Tags */} + + + +

Tags (optional)

+
+ + + updateField("tags", tags)} + error={errors.tags} + /> +
+ ); +}; + +export default DataSourceFormModal; +export type { DataSourceFormData }; diff --git a/ui/src/components/EntityFormModal.tsx b/ui/src/components/EntityFormModal.tsx new file mode 100644 index 00000000000..0b64c71d668 --- /dev/null +++ b/ui/src/components/EntityFormModal.tsx @@ -0,0 +1,260 @@ +import React, { useState, useEffect } from "react"; +import { + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiButtonEmpty, + EuiButtonIcon, + EuiText, + EuiHorizontalRule, + EuiCallOut, +} from "@elastic/eui"; +import { feast } from "../protos"; +import FormModal from "./forms/FormModal"; +import TagsEditor, { TagEntry } from "./forms/TagsEditor"; +import NameDescriptionOwnerFields from "./forms/NameDescriptionOwnerFields"; +import ValueTypeSelect from "./forms/ValueTypeSelect"; + +interface EntityFormData { + name: string; + description: string; + joinKeys: string[]; + valueType: string; + tags: TagEntry[]; +} + +interface EntityFormModalProps { + onClose: () => void; + onSubmit: (data: EntityFormData) => void; + initialData?: EntityFormData; + isEdit?: boolean; + isSubmitting?: boolean; + submitError?: string | null; +} + +const EMPTY_FORM: EntityFormData = { + name: "", + description: "", + joinKeys: [""], + valueType: String(feast.types.ValueType.Enum.STRING), + tags: [], +}; + +const EntityFormModal: React.FC = ({ + onClose, + onSubmit, + initialData, + isEdit = false, + isSubmitting = false, + submitError, +}) => { + const [formData, setFormData] = useState( + initialData || EMPTY_FORM, + ); + const [errors, setErrors] = useState>({}); + const [submitted, setSubmitted] = useState(false); + + useEffect(() => { + if (initialData) { + setFormData(initialData); + } + }, [initialData]); + + const validate = (): boolean => { + const newErrors: Record = {}; + + if (!formData.name.trim()) { + newErrors.name = "Entity name is required."; + } else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.name)) { + newErrors.name = + "Must start with a letter or underscore, and contain only letters, numbers, and underscores."; + } + + const nonEmptyKeys = formData.joinKeys.filter((k) => k.trim()); + if (nonEmptyKeys.length === 0) { + newErrors.joinKeys = "At least one join key is required."; + } else { + if (new Set(nonEmptyKeys).size !== nonEmptyKeys.length) { + newErrors.joinKeys = "Join keys must be unique."; + } + const invalidKey = nonEmptyKeys.find( + (k) => !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(k), + ); + if (invalidKey) { + newErrors.joinKeys = `Invalid join key "${invalidKey}". Use only letters, numbers, and underscores.`; + } + } + + const tagKeys = formData.tags.map((t) => t.key).filter((k) => k.trim()); + if (new Set(tagKeys).size !== tagKeys.length) { + newErrors.tags = "Tag keys must be unique."; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = () => { + setSubmitted(true); + if (validate()) { + const cleanedData = { + ...formData, + joinKeys: formData.joinKeys.filter((k) => k.trim()), + tags: formData.tags.filter((t) => t.key.trim()), + }; + onSubmit(cleanedData); + } + }; + + const updateField = ( + field: K, + value: EntityFormData[K], + ) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (submitted) { + setErrors((prev) => { + const next = { ...prev }; + delete next[field]; + return next; + }); + } + }; + + const addJoinKey = () => { + updateField("joinKeys", [...formData.joinKeys, ""]); + }; + + const removeJoinKey = (index: number) => { + if (formData.joinKeys.length <= 1) return; + updateField( + "joinKeys", + formData.joinKeys.filter((_, i) => i !== index), + ); + }; + + const updateJoinKey = (index: number, value: string) => { + const updated = [...formData.joinKeys]; + updated[index] = value; + updateField("joinKeys", updated); + }; + + return ( + + {submitError && ( + <> + +

{submitError}

+
+ + + )} + + updateField("name", v)} + onChangeDescription={(v) => updateField("description", v)} + nameDisabled={isEdit} + nameError={errors.name} + nameHelpText="A unique identifier for this entity (e.g. customer_id)." + namePlaceholder="e.g. customer_id" + descriptionPlaceholder="Describe what this entity represents..." + /> + + + + + + + +

Join Keys

+
+
+ + + Add join key + + +
+ + + Column name(s) used to join this entity in data sources. + + + {errors.joinKeys && ( + <> + + + + )} + + {formData.joinKeys.map((key, index) => ( + + + updateJoinKey(index, e.target.value)} + placeholder={ + index === 0 ? "e.g. customer_id" : "e.g. timestamp_field" + } + compressed + isInvalid={!!errors.joinKeys && !key.trim()} + /> + + + removeJoinKey(index)} + disabled={formData.joinKeys.length <= 1} + /> + + + ))} + + + + updateField("valueType", v)} + helpText="Data type of the join key." + /> + + + + updateField("tags", tags)} + error={errors.tags} + /> +
+ ); +}; + +export default EntityFormModal; +export type { EntityFormData, TagEntry }; diff --git a/ui/src/components/ExplorePanel.tsx b/ui/src/components/ExplorePanel.tsx index 3527c245b16..f66e1093f58 100644 --- a/ui/src/components/ExplorePanel.tsx +++ b/ui/src/components/ExplorePanel.tsx @@ -15,10 +15,13 @@ import { useNavigate } from "react-router-dom"; import useFCOExploreSuggestions from "../hooks/useFCOExploreSuggestions"; const ExplorePanel = () => { - const { isLoading, isSuccess, data } = useFCOExploreSuggestions(); + const { isLoading, isSuccess, isPermissionDenied, data } = + useFCOExploreSuggestions(); const navigate = useNavigate(); + if (isPermissionDenied) return null; + return ( diff --git a/ui/src/components/FeatureServiceFormModal.tsx b/ui/src/components/FeatureServiceFormModal.tsx new file mode 100644 index 00000000000..1f210bdb402 --- /dev/null +++ b/ui/src/components/FeatureServiceFormModal.tsx @@ -0,0 +1,380 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiHorizontalRule, + EuiCallOut, + EuiButtonEmpty, + EuiButtonIcon, + EuiFormRow, + EuiComboBox, + EuiComboBoxOptionOption, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import FormModal from "./forms/FormModal"; +import TagsEditor, { TagEntry } from "./forms/TagsEditor"; +import NameDescriptionOwnerFields from "./forms/NameDescriptionOwnerFields"; +import useResourceQuery, { + featureViewListPath, + restFeatureViewsToMergedList, +} from "../queries/useResourceQuery"; + +interface FeatureViewProjectionEntry { + featureViewName: string; + featureNames: string[]; +} + +interface FeatureServiceFormData { + name: string; + description: string; + owner: string; + projections: FeatureViewProjectionEntry[]; + tags: TagEntry[]; +} + +interface FeatureServiceFormModalProps { + onClose: () => void; + onSubmit: (data: FeatureServiceFormData) => void; + initialData?: FeatureServiceFormData; + isEdit?: boolean; + isSubmitting?: boolean; + submitError?: string | null; +} + +const EMPTY_PROJECTION: FeatureViewProjectionEntry = { + featureViewName: "", + featureNames: [], +}; + +const EMPTY_FORM: FeatureServiceFormData = { + name: "", + description: "", + owner: "", + projections: [{ ...EMPTY_PROJECTION }], + tags: [], +}; + +const FeatureServiceFormModal: React.FC = ({ + onClose, + onSubmit, + initialData, + isEdit = false, + isSubmitting = false, + submitError, +}) => { + const [formData, setFormData] = useState( + initialData || EMPTY_FORM, + ); + const [errors, setErrors] = useState>({}); + const [submitted, setSubmitted] = useState(false); + + const { projectName } = useParams(); + + const featureViewsQuery = useResourceQuery({ + resourceType: "feature-views-list-fs-form", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: restFeatureViewsToMergedList, + }); + + const featureViews = useMemo( + () => featureViewsQuery.data || [], + [featureViewsQuery.data], + ); + + const featureViewOptions: EuiComboBoxOptionOption[] = useMemo( + () => + featureViews.map((fv) => ({ + label: fv.name, + value: fv.name, + })), + [featureViews], + ); + + const featureNamesByView = useMemo(() => { + const map: Record = {}; + featureViews.forEach((fv) => { + const features = fv.object?.spec?.features || []; + map[fv.name] = features + .map((feature: any) => feature?.name || "") + .filter(Boolean); + }); + return map; + }, [featureViews]); + + useEffect(() => { + if (initialData) { + setFormData(initialData); + } + }, [initialData]); + + const validate = (): boolean => { + const newErrors: Record = {}; + + if (!formData.name.trim()) { + newErrors.name = "Feature service name is required."; + } else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.name)) { + newErrors.name = + "Must start with a letter or underscore, and contain only letters, numbers, and underscores."; + } + + const validProjections = formData.projections.filter((projection) => + projection.featureViewName.trim(), + ); + if (validProjections.length === 0) { + newErrors.projections = "Select at least one feature view."; + } + + const viewNames = validProjections.map( + (projection) => projection.featureViewName, + ); + if (new Set(viewNames).size !== viewNames.length) { + newErrors.projections = "Each feature view can only be selected once."; + } + + const tagKeys = formData.tags + .map((tag) => tag.key) + .filter((key) => key.trim()); + if (new Set(tagKeys).size !== tagKeys.length) { + newErrors.tags = "Tag keys must be unique."; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = () => { + setSubmitted(true); + if (validate()) { + onSubmit({ + ...formData, + projections: formData.projections.filter((projection) => + projection.featureViewName.trim(), + ), + tags: formData.tags.filter((tag) => tag.key.trim()), + }); + } + }; + + const updateField = ( + field: K, + value: FeatureServiceFormData[K], + ) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (submitted) { + setErrors((prev) => { + const next = { ...prev }; + delete next[field]; + return next; + }); + } + }; + + const addProjection = () => { + updateField("projections", [ + ...formData.projections, + { ...EMPTY_PROJECTION }, + ]); + }; + + const removeProjection = (index: number) => { + if (formData.projections.length <= 1) { + return; + } + updateField( + "projections", + formData.projections.filter( + (_, projectionIndex) => projectionIndex !== index, + ), + ); + }; + + const updateProjection = ( + index: number, + field: keyof FeatureViewProjectionEntry, + value: string | string[], + ) => { + const updated = [...formData.projections]; + updated[index] = { + ...updated[index], + [field]: value, + ...(field === "featureViewName" ? { featureNames: [] } : {}), + }; + updateField("projections", updated); + }; + + return ( + + {submitError && ( + <> + +

{submitError}

+
+ + + )} + + updateField("name", value)} + onChangeDescription={(value) => updateField("description", value)} + onChangeOwner={(value) => updateField("owner", value)} + nameDisabled={isEdit} + nameError={errors.name} + nameHelpText="A unique identifier for this feature service." + namePlaceholder="e.g. customer_activity_v1" + descriptionPlaceholder="Describe what this feature service is used for..." + /> + + + + + + + +

Feature views

+
+
+ + + Add feature view + + +
+ + + Select one or more feature views to include in this service. Leave + feature selection empty to include all features from a view. + + + {errors.projections && ( + <> + + + + )} + + {formData.projections.map((projection, index) => { + const availableFeatures = + featureNamesByView[projection.featureViewName] || []; + const selectedFeatureOptions = projection.featureNames.map( + (featureName) => ({ + label: featureName, + value: featureName, + }), + ); + const featureOptions = availableFeatures.map((featureName) => ({ + label: featureName, + value: featureName, + })); + + return ( + + + + + + + updateProjection( + index, + "featureViewName", + selected.length > 0 ? selected[0].value || "" : "", + ) + } + placeholder="Select a feature view" + isLoading={featureViewsQuery.isLoading} + compressed + /> + + + + + + updateProjection( + index, + "featureNames", + selected.map((option) => option.value || option.label), + ) + } + placeholder={ + projection.featureViewName + ? "All features" + : "Select a feature view first" + } + isDisabled={!projection.featureViewName} + compressed + /> + + + + + removeProjection(index)} + disabled={formData.projections.length <= 1} + /> + + + + + ); + })} + + + + updateField("tags", tags)} + error={errors.tags} + /> +
+ ); +}; + +export default FeatureServiceFormModal; +export type { FeatureServiceFormData, FeatureViewProjectionEntry }; diff --git a/ui/src/components/FeatureViewFormModal.tsx b/ui/src/components/FeatureViewFormModal.tsx new file mode 100644 index 00000000000..b39f09e192e --- /dev/null +++ b/ui/src/components/FeatureViewFormModal.tsx @@ -0,0 +1,572 @@ +import React, { useState, useEffect } from "react"; +import { + EuiFormRow, + EuiFieldText, + EuiFieldNumber, + EuiSelect, + EuiSwitch, + EuiComboBox, + EuiComboBoxOptionOption, + EuiSpacer, + EuiCallOut, + EuiButtonEmpty, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import FormModal from "./forms/FormModal"; +import TagsEditor, { TagEntry } from "./forms/TagsEditor"; +import NameDescriptionOwnerFields from "./forms/NameDescriptionOwnerFields"; +import FeatureFieldEditor, { + FeatureFieldEntry, +} from "./forms/FeatureFieldEditor"; +import EntityFormModal, { EntityFormData } from "./EntityFormModal"; +import DataSourceFormModal, { DataSourceFormData } from "./DataSourceFormModal"; +import { useApplyEntity } from "../queries/mutations/useEntityMutations"; +import { useApplyDataSource } from "../queries/mutations/useDataSourceMutations"; +import { feast } from "../protos"; +import useResourceQuery, { + entityListPath, + dataSourceListPath, +} from "../queries/useResourceQuery"; + +const TTL_UNIT_OPTIONS = [ + { value: "seconds", text: "Seconds" }, + { value: "minutes", text: "Minutes" }, + { value: "hours", text: "Hours" }, + { value: "days", text: "Days" }, +]; + +const TTL_UNITS: Record = { + seconds: 1, + minutes: 60, + hours: 3600, + days: 86400, +}; + +interface FeatureViewFormData { + name: string; + description: string; + owner: string; + entities: string[]; + features: FeatureFieldEntry[]; + batchSource: string; + ttlValue: number; + ttlUnit: string; + online: boolean; + tags: TagEntry[]; +} + +interface FeatureViewFormModalProps { + onClose: () => void; + onSubmit: (data: FeatureViewFormData) => void; + initialData?: FeatureViewFormData; + isEdit?: boolean; + isSubmitting?: boolean; + submitError?: string | null; +} + +const EMPTY_FORM: FeatureViewFormData = { + name: "", + description: "", + owner: "", + entities: [], + features: [], + batchSource: "", + ttlValue: 0, + ttlUnit: "seconds", + online: true, + tags: [], +}; + +const FeatureViewFormModal: React.FC = ({ + onClose, + onSubmit, + initialData, + isEdit = false, + isSubmitting = false, + submitError, +}) => { + const [formData, setFormData] = useState( + initialData || EMPTY_FORM, + ); + const [errors, setErrors] = useState>({}); + const [submitted, setSubmitted] = useState(false); + const [showEntityForm, setShowEntityForm] = useState(false); + const [showDataSourceForm, setShowDataSourceForm] = useState(false); + + const { projectName } = useParams(); + + const entitiesQuery = useResourceQuery({ + resourceType: "entities-list", + project: projectName, + restPath: entityListPath(projectName), + restSelect: (d) => d.entities, + }); + + const dataSourcesQuery = useResourceQuery({ + resourceType: "data-sources-list", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources, + }); + + const applyEntity = useApplyEntity(); + const applyDataSource = useApplyDataSource(); + + // REST API returns objects with spec.name; fall back to top-level name + const entities: any[] = entitiesQuery.data || []; + const dataSources: any[] = dataSourcesQuery.data || []; + + const entityOptions: EuiComboBoxOptionOption[] = entities + .map((e: any) => e?.spec?.name || e?.name || "") + .filter(Boolean) + .map((name: string) => ({ label: name })); + + const dataSourceOptions = dataSources + .map((ds: any) => ds?.spec?.name || ds?.name || "") + .filter(Boolean) + .map((name: string) => ({ value: name, text: name })); + + const hasNoEntities = entitiesQuery.isSuccess && entities.length === 0; + const hasNoDataSources = + dataSourcesQuery.isSuccess && dataSources.length === 0; + + useEffect(() => { + if (initialData) { + setFormData(initialData); + } + }, [initialData]); + + const validate = (): boolean => { + const newErrors: Record = {}; + + if (!formData.name.trim()) { + newErrors.name = "Feature view name is required."; + } else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(formData.name)) { + newErrors.name = + "Must start with a letter or underscore, and contain only letters, numbers, and underscores."; + } + + if (formData.features.length === 0) { + newErrors.features = "At least one feature is required."; + } else { + const hasEmptyName = formData.features.some((f) => !f.name.trim()); + if (hasEmptyName) { + newErrors.features = "All features must have a name."; + } else { + const featureNames = formData.features.map((f) => f.name.trim()); + if (new Set(featureNames).size !== featureNames.length) { + newErrors.features = "Feature names must be unique."; + } + const invalidName = featureNames.find( + (n) => !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(n), + ); + if (invalidName) { + newErrors.features = `Invalid feature name "${invalidName}". Use only letters, numbers, and underscores.`; + } + } + } + + if (!formData.batchSource.trim()) { + newErrors.batchSource = "A batch source is required."; + } + + if (formData.ttlValue < 0) { + newErrors.ttlValue = "TTL must be 0 (never expire) or a positive number."; + } + + const tagKeys = formData.tags.map((t) => t.key).filter((k) => k.trim()); + if (new Set(tagKeys).size !== tagKeys.length) { + newErrors.tags = "Tag keys must be unique."; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = () => { + setSubmitted(true); + if (validate()) { + const cleanedData = { + ...formData, + tags: formData.tags.filter((t) => t.key.trim()), + }; + onSubmit(cleanedData); + } + }; + + const updateField = ( + field: K, + value: FeatureViewFormData[K], + ) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (submitted) { + setErrors((prev) => { + const next = { ...prev }; + delete next[field]; + return next; + }); + } + }; + + const handleInlineEntityCreate = (entityData: EntityFormData) => { + const payload = { + name: entityData.name, + project: projectName || "", + join_key: entityData.joinKeys[0] || entityData.name, + value_type: parseInt(entityData.valueType, 10), + description: entityData.description, + tags: Object.fromEntries( + entityData.tags + .filter((t) => t.key.trim()) + .map((t) => [t.key, t.value]), + ), + }; + applyEntity.mutate(payload, { + onSuccess: () => { + setShowEntityForm(false); + updateField("entities", [...formData.entities, entityData.name]); + }, + }); + }; + + const handleInlineDataSourceCreate = (dsData: DataSourceFormData) => { + const payload: Record = { + name: dsData.name, + project: projectName || "", + type: parseInt(dsData.sourceType, 10), + timestamp_field: dsData.timestampField, + created_timestamp_column: dsData.createdTimestampColumn, + description: dsData.description, + owner: dsData.owner, + tags: Object.fromEntries( + dsData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + }; + + const st = dsData.sourceType; + if (st === String(feast.core.DataSource.SourceType.BATCH_FILE)) { + payload.file_options = { + uri: dsData.fileUri, + file_format: dsData.fileFormat || "parquet", + s3_endpoint_override: dsData.fileS3EndpointOverride || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_BIGQUERY)) { + payload.bigquery_options = { + table: dsData.bigqueryTable, + query: dsData.bigqueryQuery, + }; + if (dsData.bigqueryDatePartitionColumn) { + payload.date_partition_column = dsData.bigqueryDatePartitionColumn; + } + } else if ( + st === String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE) + ) { + payload.snowflake_options = { + table: dsData.snowflakeTable, + database: dsData.snowflakeDatabase, + schema_: dsData.snowflakeSchema, + query: dsData.snowflakeQuery || "", + warehouse: dsData.snowflakeWarehouse || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_REDSHIFT)) { + payload.redshift_options = { + table: dsData.redshiftTable, + database: dsData.redshiftDatabase, + schema_: dsData.redshiftSchema, + query: dsData.redshiftQuery || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KAFKA)) { + payload.kafka_options = { + kafka_bootstrap_servers: dsData.kafkaBootstrapServers, + topic: dsData.kafkaTopic, + message_format: dsData.kafkaMessageFormat || "json", + watermark_delay_threshold: dsData.kafkaWatermarkDelay || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SPARK)) { + payload.spark_options = { + table: dsData.sparkTable, + path: dsData.sparkPath, + query: dsData.sparkQuery || "", + file_format: dsData.sparkFileFormat || "", + table_format: dsData.sparkTableFormat || "", + table_format_catalog: dsData.sparkTableFormatCatalog || "", + table_format_namespace: dsData.sparkTableFormatNamespace || "", + table_format_properties: dsData.sparkTableFormatProperties || "", + date_partition_column: dsData.sparkDatePartitionColumn || "", + date_partition_column_format: dsData.sparkDatePartitionFormat || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_TRINO)) { + payload.trino_options = { + table: dsData.trinoTable, + query: dsData.trinoQuery, + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ATHENA)) { + payload.athena_options = { + table: dsData.athenaTable, + query: dsData.athenaQuery, + database: dsData.athenaDatabase, + data_source: dsData.athenaDataSource, + }; + if (dsData.athenaDatePartitionColumn) { + payload.date_partition_column = dsData.athenaDatePartitionColumn; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ICEBERG)) { + const catalogProps = dsData.icebergCatalogProperties?.trim() + ? JSON.parse(dsData.icebergCatalogProperties) + : {}; + payload.custom_options = { + configuration: JSON.stringify({ + catalog_type: dsData.icebergCatalogType || "rest", + endpoint: dsData.icebergEndpoint, + warehouse: dsData.icebergWarehouse, + namespace: dsData.icebergNamespace, + table: dsData.icebergTable, + token_env_var: dsData.icebergTokenEnvVar || null, + credential_vending: dsData.icebergCredentialVending !== "false", + catalog_properties: catalogProps, + }), + }; + payload.data_source_class_type = + "feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source.IcebergSource"; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KINESIS)) { + payload.kinesis_options = { + region: dsData.kinesisRegion, + stream_name: dsData.kinesisStreamName, + record_format: dsData.kinesisRecordFormat || "json", + }; + } else if (st === String(feast.core.DataSource.SourceType.CUSTOM_SOURCE)) { + payload.custom_options = { + class_name: dsData.customSourceClassName, + config: dsData.customSourceConfig, + }; + } + + applyDataSource.mutate(payload as any, { + onSuccess: () => { + setShowDataSourceForm(false); + updateField("batchSource", dsData.name); + }, + }); + }; + + const selectedEntityOptions = formData.entities.map((e) => ({ label: e })); + + return ( + <> + + {submitError && ( + <> + +

{submitError}

+
+ + + )} + + updateField("name", v)} + onChangeDescription={(v) => updateField("description", v)} + onChangeOwner={(v) => updateField("owner", v)} + nameDisabled={isEdit} + nameError={errors.name} + nameHelpText="A unique name for this feature view." + namePlaceholder="e.g. customer_features" + descriptionPlaceholder="Describe what this feature view provides..." + /> + + {hasNoEntities && ( + <> + + +

+ Feature views typically reference entities. You can create one + now. +

+ setShowEntityForm(true)} + > + Create Entity + +
+ + + )} + + + + updateField( + "entities", + selected.map((s) => s.label), + ) + } + isClearable + isLoading={entitiesQuery.isLoading} + /> + + + {hasNoDataSources && ( + <> + + +

+ A batch source is required. You can create a data source now. +

+ setShowDataSourceForm(true)} + > + Create Data Source + +
+ + + )} + + + {dataSourceOptions.length > 0 ? ( + updateField("batchSource", e.target.value)} + isInvalid={!!errors.batchSource} + isLoading={dataSourcesQuery.isLoading} + /> + ) : ( + updateField("batchSource", e.target.value)} + isInvalid={!!errors.batchSource} + placeholder={ + dataSourcesQuery.isLoading + ? "Loading data sources..." + : "Enter data source name" + } + /> + )} + + + + + updateField("features", features)} + error={errors.features} + /> + + + + +
+ + updateField("ttlValue", parseInt(e.target.value) || 0) + } + min={0} + isInvalid={!!errors.ttlValue} + style={{ width: 120 }} + /> + updateField("ttlUnit", e.target.value)} + style={{ width: 140 }} + /> +
+
+ + + updateField("online", e.target.checked)} + /> + + + + + updateField("tags", tags)} + error={errors.tags} + /> +
+ + {showEntityForm && ( + setShowEntityForm(false)} + onSubmit={handleInlineEntityCreate} + /> + )} + + {showDataSourceForm && ( + setShowDataSourceForm(false)} + onSubmit={handleInlineDataSourceCreate} + /> + )} + + ); +}; + +export default FeatureViewFormModal; +export type { FeatureViewFormData }; +export { TTL_UNITS }; diff --git a/ui/src/components/FeaturesInServiceDisplay.tsx b/ui/src/components/FeaturesInServiceDisplay.tsx index 14731caa0b8..6d772b78f34 100644 --- a/ui/src/components/FeaturesInServiceDisplay.tsx +++ b/ui/src/components/FeaturesInServiceDisplay.tsx @@ -12,39 +12,53 @@ interface IFeatureColumnInService { featureViewName: string; name: string; valueType: feast.types.ValueType.Enum; + viewType?: string; } const FeaturesInServiceList = ({ featureViews }: FeatureViewsListInterace) => { const { projectName } = useParams(); - const items: IFeatureColumnInService[] = featureViews.flatMap((featureView) => - featureView.featureColumns!.map((featureColumn) => ({ - featureViewName: featureView.featureViewName!, - name: featureColumn.name!, - valueType: featureColumn.valueType!, - })), + + const items: IFeatureColumnInService[] = featureViews.flatMap( + (featureView: any) => + (featureView.featureColumns || []).map((featureColumn: any) => ({ + featureViewName: featureView.featureViewName!, + name: featureColumn.name!, + valueType: featureColumn.valueType!, + viewType: featureView.viewType || "featureView", + })), ); + const isLabelMode = items.length > 0 && items[0].viewType === "labelView"; + const columns = [ { - name: "Feature View", + name: isLabelMode ? "Label View" : "Feature View", field: "featureViewName", - render: (name: string) => { - return ( - - {name} - - ); + render: (name: string, item: IFeatureColumnInService) => { + const isLabelView = item.viewType === "labelView"; + const path = isLabelView + ? `/p/${projectName}/label-view/${name}` + : `/p/${projectName}/feature-view/${name}`; + return {name}; }, }, { - name: "Feature Column", + name: isLabelMode ? "Label" : "Feature", field: "name", + render: (name: string, item: IFeatureColumnInService) => { + const isLabelView = item.viewType === "labelView"; + const path = isLabelView + ? `/p/${projectName}/label-view/${item.featureViewName}/label/${name}` + : `/p/${projectName}/feature-view/${item.featureViewName}/feature/${name}`; + return {name}; + }, }, { name: "Value Type", field: "valueType", - render: (valueType: feast.types.ValueType.Enum) => { - return feast.types.ValueType.Enum[valueType]; + render: (valueType: feast.types.ValueType.Enum | string) => { + if (typeof valueType === "string") return valueType; + return feast.types.ValueType.Enum[valueType] || String(valueType); }, }, ]; diff --git a/ui/src/components/FeaturesListDisplay.tsx b/ui/src/components/FeaturesListDisplay.tsx index 61f57b478d6..51bc62e8e2b 100644 --- a/ui/src/components/FeaturesListDisplay.tsx +++ b/ui/src/components/FeaturesListDisplay.tsx @@ -30,8 +30,9 @@ const FeaturesList = ({ { name: "Value Type", field: "valueType", - render: (valueType: feast.types.ValueType.Enum) => { - return feast.types.ValueType.Enum[valueType]; + render: (valueType: feast.types.ValueType.Enum | string) => { + if (typeof valueType === "string") return valueType; + return feast.types.ValueType.Enum[valueType] || String(valueType || ""); }, }, ]; diff --git a/ui/src/components/LineageEventsList.tsx b/ui/src/components/LineageEventsList.tsx new file mode 100644 index 00000000000..1c029b9894d --- /dev/null +++ b/ui/src/components/LineageEventsList.tsx @@ -0,0 +1,221 @@ +import React, { useState } from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiBasicTable, + EuiLoadingSpinner, + EuiEmptyPrompt, + EuiBadge, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiCodeBlock, + EuiButtonEmpty, +} from "@elastic/eui"; +import type { OpenLineageEvent } from "../queries/useLoadOpenLineageGraph"; +import { useLoadOpenLineageEvents } from "../queries/useLoadOpenLineageGraph"; + +const eventTypeColor = (eventType: string) => { + switch (eventType) { + case "START": + return "primary"; + case "COMPLETE": + return "success"; + case "FAIL": + return "danger"; + case "ABORT": + return "warning"; + default: + return "default"; + } +}; + +const formatTimestamp = (ts: number) => { + if (!ts) return "-"; + const date = new Date(ts); + return date.toLocaleString(); +}; + +const LineageEventsList: React.FC = () => { + const [namespace, setNamespace] = useState(""); + const [jobFilter, setJobFilter] = useState(""); + const [selectedEvent, setSelectedEvent] = useState( + null, + ); + + const { data, isLoading, isError } = useLoadOpenLineageEvents( + namespace || undefined, + jobFilter || undefined, + 100, + ); + + const columns = [ + { + field: "event_type", + name: "Type", + width: "100px", + render: (val: string) => ( + {val} + ), + }, + { + field: "event_time", + name: "Event Time", + width: "200px", + render: (val: number) => formatTimestamp(val), + }, + { + field: "job_namespace", + name: "Namespace", + width: "200px", + }, + { + field: "job_name", + name: "Job", + width: "250px", + }, + { + field: "run_id", + name: "Run ID", + width: "150px", + render: (val: string) => (val ? val.substring(0, 8) + "..." : "-"), + }, + { + field: "producer", + name: "Producer", + width: "150px", + }, + { + name: "Actions", + width: "80px", + render: (event: OpenLineageEvent) => ( + setSelectedEvent(event)} + > + View + + ), + }, + ]; + + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + if (isError) { + return ( + + Events Unavailable} + body={ +

+ Could not load OpenLineage events. The consumer may not be + enabled. +

+ } + /> +
+ ); + } + + const events = data?.events || []; + + return ( + + +

OpenLineage Events ({events.length})

+
+ + + + + setNamespace(e.target.value)} + aria-label="Filter by namespace" + /> + + + setJobFilter(e.target.value)} + aria-label="Filter by job name" + /> + + + + + + {events.length === 0 ? ( + No Events} + body={

No OpenLineage events match the current filters.

} + /> + ) : ( + + )} + + {selectedEvent && ( + setSelectedEvent(null)} + size="m" + aria-labelledby="eventDetailTitle" + > + + +

+ Event: {selectedEvent.event_type} - {selectedEvent.job_name} +

+
+
+ + + +

Event Details

+
+ +
+ Event ID: {selectedEvent.event_id} +
+
+ Time: {formatTimestamp(selectedEvent.event_time)} +
+
+ Run ID: {selectedEvent.run_id || "-"} +
+
+ Producer: {selectedEvent.producer || "-"} +
+ + +

Raw Event JSON

+
+ + + {JSON.stringify(JSON.parse(selectedEvent.event_json), null, 2)} + +
+
+ )} +
+ ); +}; + +export default LineageEventsList; diff --git a/ui/src/components/ObjectsCountStats.tsx b/ui/src/components/ObjectsCountStats.tsx index bf1dd2dc9dd..e2aed99b5fd 100644 --- a/ui/src/components/ObjectsCountStats.tsx +++ b/ui/src/components/ObjectsCountStats.tsx @@ -1,94 +1,134 @@ -import React, { useContext } from "react"; +import React from "react"; import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiHorizontalRule, - EuiTitle, EuiSpacer, } from "@elastic/eui"; -import useLoadRegistry from "../queries/useLoadRegistry"; import { useNavigate, useParams } from "react-router-dom"; -import RegistryPathContext from "../contexts/RegistryPathContext"; +import useResourceQuery, { + entityListPath, + featureViewListPath, + featureServiceListPath, + dataSourceListPath, + labelViewListPath, + featuresListPath, + restFeatureViewsToMergedList, + restLabelViewsFromResponse, +} from "../queries/useResourceQuery"; +import type { genericFVType } from "../parsers/mergedFVTypes"; -const useLoadObjectStats = () => { - const registryUrl = useContext(RegistryPathContext); - const query = useLoadRegistry(registryUrl); +const statStyle = { cursor: "pointer" }; + +const ObjectsCountStats = () => { + const { projectName } = useParams(); + const navigate = useNavigate(); + + const fsQuery = useResourceQuery({ + resourceType: "stats-fs", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices, + }); + + const fvQuery = useResourceQuery({ + resourceType: "stats-fvs", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: restFeatureViewsToMergedList, + }); + + const entQuery = useResourceQuery({ + resourceType: "stats-ent", + project: projectName, + restPath: entityListPath(projectName), + restSelect: (d) => d.entities, + }); - const data = - query.isSuccess && query.data - ? { - featureServices: query.data.objects.featureServices?.length || 0, - featureViews: query.data.mergedFVList.length, - entities: query.data.objects.entities?.length || 0, - dataSources: query.data.objects.dataSources?.length || 0, - } - : undefined; + const dsQuery = useResourceQuery({ + resourceType: "stats-ds", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources, + }); - return { - ...query, - data, + const featQuery = useResourceQuery({ + resourceType: "stats-feat", + project: projectName, + restPath: featuresListPath(projectName), + restSelect: (d) => d.features || [], + }); + + const lvQuery = useResourceQuery({ + resourceType: "stats-lv", + project: projectName, + restPath: labelViewListPath(projectName), + restSelect: restLabelViewsFromResponse, + }); + + const settled = (q: { isSuccess: boolean; isError: boolean }) => + q.isSuccess || q.isError; + const allSettled = + settled(fsQuery) && + settled(fvQuery) && + settled(entQuery) && + settled(dsQuery) && + settled(featQuery) && + settled(lvQuery); + + const stat = ( + q: { data?: any[]; isPermissionDenied: boolean }, + label: string, + path: string, + ) => { + if (q.isPermissionDenied) return null; + return ( + + navigate(`/p/${projectName}/${path}`)} + description={`${label}→`} + title={q.data?.length || 0} + reverse + /> + + ); }; -}; -const statStyle = { cursor: "pointer" }; + const hasLabelViews = + !lvQuery.isPermissionDenied && (lvQuery.data?.length || 0) > 0; -const ObjectsCountStats = () => { - const { isLoading, isSuccess, isError, data } = useLoadObjectStats(); - const { projectName } = useParams(); + const queries = [fsQuery, fvQuery, featQuery, entQuery, dsQuery, lvQuery]; + const hasAnyAccessible = queries.some((q) => !q.isPermissionDenied); - const navigate = useNavigate(); + if (allSettled && !hasAnyAccessible) return null; return ( - {isLoading &&

Loading

} - {isError &&

There was an error in loading registry information.

} - {isSuccess && data && ( + {!allSettled &&

Loading

} + {allSettled && ( - -

Registered in this Feast project are …

-
- - - navigate(`/p/${projectName}/feature-service`)} - description="Feature Services→" - title={data.featureServices} - reverse - /> - - - navigate(`/p/${projectName}/feature-view`)} - title={data.featureViews} - reverse - /> - - - navigate(`/p/${projectName}/entity`)} - title={data.entities} - reverse - /> - - - navigate(`/p/${projectName}/data-source`)} - title={data.dataSources} - reverse - /> - + {stat(fsQuery, "Feature Services", "feature-service")} + {stat(fvQuery, "Feature Views", "feature-view")} + {stat(featQuery, "Features", "features")} + {stat(entQuery, "Entities", "entity")} + {stat(dsQuery, "Data Sources", "data-source")} + {hasLabelViews && ( + + + + {stat(lvQuery, "Label Views", "label-view")} + + + + + + )}
)}
diff --git a/ui/src/components/OpenLineageGraph.tsx b/ui/src/components/OpenLineageGraph.tsx new file mode 100644 index 00000000000..a95c6c20b08 --- /dev/null +++ b/ui/src/components/OpenLineageGraph.tsx @@ -0,0 +1,1150 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + ReactFlow, + Node, + Edge, + Controls, + Background, + useNodesState, + useEdgesState, + ConnectionLineType, + MarkerType, + Handle, + Position, +} from "reactflow"; + +import "reactflow/dist/style.css"; +import dagre from "dagre"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiLoadingSpinner, + EuiToolTip, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSelect, + EuiBadge, +} from "@elastic/eui"; +import { useTheme } from "../contexts/ThemeContext"; +import type { + OpenLineageGraphData, + OpenLineageNode, +} from "../queries/useLoadOpenLineageGraph"; +import { useRunHistory, useRunDetail } from "../queries/useLoadRunHistory"; +import type { RunSummary } from "../queries/useLoadRunHistory"; + +const nodeWidth = 280; +const nodeHeight = 65; + +// ── Producer-based colors (generated dynamically) ── + +const normalizeProducer = (producer?: string | null): string => { + if (!producer) return "unknown"; + const p = producer.toLowerCase().trim(); + + // Extract the last meaningful path segment from URLs like + // "https://github.com/OpenLineage/OpenLineage/tree/1.0.0/integration/airflow" + try { + const url = new URL(p); + const segments = url.pathname.split("/").filter(Boolean); + if (segments.length > 0) { + return segments[segments.length - 1].replace(/-/g, "_"); + } + } catch { + // not a URL + } + + // For plain names like "feast" or "my-custom-producer", just clean up + return p.replace(/-/g, "_").replace(/\s+/g, "_"); +}; + +const hashString = (str: string): number => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return Math.abs(hash); +}; + +const generateProducerColor = ( + producer: string, +): { main: string; light: string } => { + if (producer === "unknown") return { main: "#888888", light: "#f0f0f0" }; + const hue = hashString(producer) % 360; + const main = `hsl(${hue}, 70%, 45%)`; + const light = `hsl(${hue}, 60%, 94%)`; + return { main, light }; +}; + +const producerColorCache: Record = {}; + +const getProducerColors = (producer?: string | null) => { + const key = normalizeProducer(producer); + if (!producerColorCache[key]) { + producerColorCache[key] = generateProducerColor(key); + } + return producerColorCache[key]; +}; + +const getNodeIcon = (type: string) => { + return type === "job" ? "\u2699" : "\u2B21"; +}; + +// ── Custom Node ── + +interface LineageNodeData { + label: string; + type: string; + producer?: string; + namespace?: string; + nodeRef?: OpenLineageNode; + onNodeClick?: (node: OpenLineageNode) => void; +} + +const LineageCustomNode = ({ data }: { data: LineageNodeData }) => { + const [isHovered, setIsHovered] = useState(false); + const colors = getProducerColors(data.producer); + const icon = getNodeIcon(data.type); + const producerLabel = normalizeProducer(data.producer); + + const handleClick = () => { + if (data.onNodeClick && data.nodeRef) { + data.onNodeClick(data.nodeRef); + } + }; + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={handleClick} + > +
+ {producerLabel} +
+ + {data.namespace && isHovered && ( + +
+ {data.namespace} +
+
+ )} + + +
+
{icon}
+
+
+ {data.label} +
+ +
+ ); +}; + +const lineageNodeTypes = { lineageCustom: LineageCustomNode }; + +// ── Dagre layout ── + +const layoutGraph = (nodes: Node[], edges: Edge[], direction = "LR") => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ + rankdir: direction, + nodesep: 80, + ranksep: 120, + marginx: 50, + marginy: 50, + }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); + }); + + edges.forEach((edge) => { + if (dagreGraph.hasNode(edge.source) && dagreGraph.hasNode(edge.target)) { + dagreGraph.setEdge(edge.source, edge.target); + } + }); + + dagre.layout(dagreGraph); + + return { + nodes: nodes.map((node) => { + const pos = dagreGraph.node(node.id); + return { + ...node, + position: { + x: (pos?.x ?? 0) - nodeWidth / 2, + y: (pos?.y ?? 0) - nodeHeight / 2, + }, + sourcePosition: direction === "TB" ? Position.Bottom : Position.Right, + targetPosition: direction === "TB" ? Position.Top : Position.Left, + }; + }), + edges, + }; +}; + +// ── Legend ── + +const ProducerLegend: React.FC<{ producers: string[] }> = ({ producers }) => { + const { colorMode } = useTheme(); + const isDarkMode = colorMode === "dark"; + const bg = isDarkMode ? "#1D1E24" : "white"; + const border = isDarkMode ? "#343741" : "#ddd"; + const text = isDarkMode ? "#DFE5EF" : "#333"; + + const items = producers.map((p) => ({ + key: p, + label: p.charAt(0).toUpperCase() + p.slice(1), + color: getProducerColors(p).main, + })); + + return ( +
+
+ Producers +
+ {items.map((item) => ( +
+
+
{item.label}
+
+ ))} +
+ ); +}; + +// ── Run History helpers ── + +const formatTimestamp = (ts: number | null): string => { + if (!ts) return "—"; + return new Date(ts).toLocaleString(); +}; + +const formatDuration = (start: number | null, end: number | null): string => { + if (!start || !end) return "—"; + const ms = end - start; + if (ms < 1000) return `${ms}ms`; + const s = Math.floor(ms / 1000); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + return `${m}m ${s % 60}s`; +}; + +const stateColor = (state: string): string => { + switch (state.toUpperCase()) { + case "COMPLETE": + return "#00BFB3"; + case "FAIL": + return "#FF6666"; + case "RUNNING": + case "START": + return "#6092C0"; + case "ABORT": + return "#D36086"; + default: + return "#98A2B3"; + } +}; + +const RunHistorySection: React.FC<{ + jobNamespace: string; + jobName: string; + isDarkMode: boolean; +}> = ({ jobNamespace, jobName, isDarkMode }) => { + const { data, isLoading } = useRunHistory(jobNamespace, jobName); + const [selectedRunId, setSelectedRunId] = useState(); + const { data: runDetail, isLoading: detailLoading } = + useRunDetail(selectedRunId); + + const runs = data?.runs || []; + + if (isLoading) { + return ( +
+
Run History
+ +
+ ); + } + + if (runs.length === 0) return null; + + return ( +
+
+ Run History ({runs.length}) +
+ + + + + + + + + + + {runs.map((run: RunSummary) => ( + + setSelectedRunId( + selectedRunId === run.run_id ? undefined : run.run_id, + ) + } + > + + + + + + ))} + +
RunStatusStartedDuration
+ {run.run_id.substring(0, 8)}… + + {run.state} + + {formatTimestamp(run.started_at)} + + {formatDuration(run.started_at, run.ended_at)} +
+ + {selectedRunId && ( +
+ {detailLoading ? ( + + ) : runDetail ? ( + <> +
+ Run {runDetail.run_id.substring(0, 8)}… I/O +
+ {runDetail.inputs.length > 0 && ( +
+
Inputs:
+ {runDetail.inputs.map((io) => ( +
+ {io.name} +
+ ))} +
+ )} + {runDetail.outputs.length > 0 && ( +
+
Outputs:
+ {runDetail.outputs.map((io) => ( +
+ {io.name} +
+ ))} +
+ )} + {runDetail.inputs.length === 0 && + runDetail.outputs.length === 0 && ( +
+ No I/O recorded for this run +
+ )} + + ) : ( +
Run details not available
+ )} +
+ )} +
+ ); +}; + +// ── Node Detail Panel ── + +const NodeDetailPanel: React.FC<{ + node: OpenLineageNode; + onClose: () => void; +}> = ({ node, onClose }) => { + const { colorMode } = useTheme(); + const isDarkMode = colorMode === "dark"; + const colors = getProducerColors(node.producer); + + const schema = node.schema; + const fields = schema?.fields || []; + const facets = node.facets || {}; + + const tags: string[] = []; + if (facets.feast_featureView?.tags) { + Object.entries(facets.feast_featureView.tags).forEach(([k, v]) => + tags.push(`${k}:${v}`), + ); + } + if (facets.feast_entity?.tags) { + Object.entries(facets.feast_entity.tags).forEach(([k, v]) => + tags.push(`${k}:${v}`), + ); + } + + const features: string[] = facets.feast_featureView?.features || []; + const entities: string[] = facets.feast_featureView?.entities || []; + const fvDescription = + node.description || + facets.documentation?.description || + facets.feast_featureView?.description || + facets.feast_featureService?.description || + facets.feast_entity?.description; + + const featureViews: string[] = + facets.feast_featureService?.feature_views || []; + const dqMetrics = facets.dataQualityMetrics; + const sqlFacet = facets.sql; + const dataSource = facets.dataSource; + const ownership = facets.ownership; + + const knownFacetKeys = new Set([ + "schema", + "documentation", + "feast_featureView", + "feast_featureService", + "feast_entity", + "dataQualityMetrics", + "sql", + "dataSource", + "ownership", + "symlinks", + "jobType", + ]); + const otherFacets = Object.keys(facets).filter((k) => !knownFacetKeys.has(k)); + + return ( +
+
+
+
+ {node.type} +
+
+ {node.name} +
+
+ +
+ + + {normalizeProducer(node.producer)} + + {node.job_type && ( + + {node.job_type} + + )} + {node.source_type && ( + + {node.source_type} + + )} + +
+ {node.namespace} +
+ + {fvDescription && ( +
+
Description
+
+ {fvDescription} +
+
+ )} + + {tags.length > 0 && ( +
+
Tags
+
+ {tags.map((t) => ( + + {t} + + ))} +
+
+ )} + + {entities.length > 0 && ( +
+
Entities
+ {entities.map((e) => ( +
+ {e} +
+ ))} +
+ )} + + {features.length > 0 && ( +
+
Features
+ {features.map((f) => ( +
+ {f} +
+ ))} +
+ )} + + {featureViews.length > 0 && ( +
+
Feature Views
+ {featureViews.map((fv) => ( +
+ {fv} +
+ ))} +
+ )} + + {fields.length > 0 && ( +
+
+ Schema ({fields.length} fields) +
+ + + + + + + + + {fields.map((f: any) => ( + + + + + ))} + +
FieldType
+ {f.name} + + {f.type || "—"} +
+
+ )} + + {node.feast_object_type && node.feast_object_type !== "unknown" && ( +
+
Feast Mapping
+
+ Type:{" "} + {node.feast_object_type} +
+ {node.feast_object_name && ( +
+ Name:{" "} + {node.feast_object_name} +
+ )} + {node.feast_project && ( +
+ Project:{" "} + {node.feast_project} +
+ )} +
+ )} + + {dataSource && ( +
+
Data Source
+ {dataSource.name && ( +
+ Name: {dataSource.name} +
+ )} + {dataSource.uri && ( +
+ {dataSource.uri} +
+ )} +
+ )} + + {ownership && ownership.owners && ( +
+
Owners
+ {ownership.owners.map((o: any, i: number) => ( +
+ {o.name || o.owner} + {o.type ? ` (${o.type})` : ""} +
+ ))} +
+ )} + + {sqlFacet && sqlFacet.query && ( +
+
SQL
+
+            {sqlFacet.query}
+          
+
+ )} + + {dqMetrics && ( +
+
Data Quality
+ {dqMetrics.rowCount != null && ( +
+ Row count:{" "} + {dqMetrics.rowCount.toLocaleString()} +
+ )} + {dqMetrics.columnMetrics && + Object.entries(dqMetrics.columnMetrics).map( + ([col, metrics]: [string, any]) => ( +
+ {col} + + {metrics.nullCount != null && `nulls: ${metrics.nullCount}`} + {metrics.distinctCount != null && + ` distinct: ${metrics.distinctCount}`} + {metrics.min != null && ` min: ${metrics.min}`} + {metrics.max != null && ` max: ${metrics.max}`} + +
+ ), + )} +
+ )} + + {otherFacets.length > 0 && ( +
+
Other Facets
+ {otherFacets.map((key) => ( +
+ {key} +
+ ))} +
+ )} + + {node.type === "job" && ( + + )} +
+ ); +}; + +// ── Main LineageGraph component ── + +interface LineageGraphProps { + olData?: OpenLineageGraphData; + olLoading: boolean; + olError: boolean; + feastOnlyCheckbox?: React.ReactNode; +} + +const LineageGraph: React.FC = ({ + olData, + olLoading, + olError, + feastOnlyCheckbox, +}) => { + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + const [filterType, setFilterType] = useState(""); + const [filterProducer, setFilterProducer] = useState(""); + const [filterObject, setFilterObject] = useState(""); + const [selectedNode, setSelectedNode] = useState( + null, + ); + + const producers = useMemo(() => { + if (!olData) return []; + const set = new Set(); + for (const n of olData.nodes) { + set.add(normalizeProducer(n.producer)); + } + return Array.from(set).sort(); + }, [olData]); + + const objectOptions = useMemo(() => { + if (!olData) return []; + return olData.nodes + .filter((n) => { + if (filterType && n.type !== filterType) return false; + if (filterProducer && normalizeProducer(n.producer) !== filterProducer) + return false; + return true; + }) + .map((n) => n.name) + .filter((v, i, a) => a.indexOf(v) === i) + .sort(); + }, [olData, filterType, filterProducer]); + + useEffect(() => { + setFilterObject(""); + }, [filterType, filterProducer]); + + useEffect(() => { + if (!olData) return; + + let filteredNodes = olData.nodes; + if (filterType) { + filteredNodes = filteredNodes.filter((n) => n.type === filterType); + } + if (filterProducer) { + filteredNodes = filteredNodes.filter( + (n) => normalizeProducer(n.producer) === filterProducer, + ); + } + + const makeId = (type: string, ns: string, name: string) => + `${type}:${ns}:${name}`; + + if (filterObject) { + const focusIds = new Set( + filteredNodes + .filter((n) => n.name === filterObject) + .map((n) => makeId(n.type, n.namespace, n.name)), + ); + + const connectedIds = new Set(); + for (const e of olData.edges) { + const srcId = makeId(e.source_type, e.source_namespace, e.source_name); + const tgtId = makeId(e.target_type, e.target_namespace, e.target_name); + if (focusIds.has(srcId)) connectedIds.add(tgtId); + if (focusIds.has(tgtId)) connectedIds.add(srcId); + } + + const visibleIds = new Set( + Array.from(focusIds).concat(Array.from(connectedIds)), + ); + filteredNodes = olData.nodes.filter((n) => + visibleIds.has(makeId(n.type, n.namespace, n.name)), + ); + } + + const filteredNodeIds = new Set( + filteredNodes.map((n) => makeId(n.type, n.namespace, n.name)), + ); + + const flowNodes: Node[] = filteredNodes.map((n) => ({ + id: makeId(n.type, n.namespace, n.name), + type: "lineageCustom", + data: { + label: n.name, + type: n.type, + producer: n.producer, + namespace: n.namespace, + nodeRef: n, + onNodeClick: setSelectedNode, + } as LineageNodeData, + position: { x: 0, y: 0 }, + })); + + const flowEdges: Edge[] = olData.edges + .filter((e) => { + const srcId = makeId(e.source_type, e.source_namespace, e.source_name); + const tgtId = makeId(e.target_type, e.target_namespace, e.target_name); + return filteredNodeIds.has(srcId) && filteredNodeIds.has(tgtId); + }) + .map((e, i) => { + const isSymlink = e.edge_type === "symlink"; + const color = isSymlink + ? "#999999" + : e.edge_type === "derived" + ? "#3366cc" + : "#e67300"; + return { + id: `ol-edge-${i}`, + source: makeId(e.source_type, e.source_namespace, e.source_name), + sourceHandle: "source", + target: makeId(e.target_type, e.target_namespace, e.target_name), + targetHandle: "target", + animated: !isSymlink, + style: { + strokeWidth: isSymlink ? 1 : 2, + stroke: color, + strokeDasharray: isSymlink + ? "3 3" + : e.edge_type === "derived" + ? "5 3" + : "none", + }, + type: "smoothstep", + markerEnd: { + type: MarkerType.ArrowClosed, + width: 16, + height: 16, + color, + }, + }; + }); + + const { nodes: ln, edges: le } = layoutGraph(flowNodes, flowEdges); + setNodes(ln); + setEdges(le); + }, [olData, filterType, filterProducer, filterObject, setNodes, setEdges]); + + if (olLoading) { + return ( + +
+ +
+
+ ); + } + + if (olError || !olData) { + return ( + + OpenLineage Data Unavailable} + body={ +

+ The OpenLineage consumer is not enabled or no events have been + received yet. Enable the consumer in your{" "} + feature_store.yaml configuration. +

+ } + /> +
+ ); + } + + if (olData.nodes.length === 0) { + return ( + + No Lineage Events} + body={ +

+ No OpenLineage events have been received yet. Configure your data + pipeline producers (Airflow, Spark, dbt, Feast) to send events to + this instance. +

+ } + /> +
+ ); + } + + return ( + +
+ +

OpenLineage Graph

+
+ {feastOnlyCheckbox && ( +
+ {feastOnlyCheckbox} +
+ )} +
+ + + + + setFilterType(e.target.value)} + aria-label="Filter by type" + /> + + + + + ({ + value: p, + text: p.charAt(0).toUpperCase() + p.slice(1), + })), + ]} + value={filterProducer} + onChange={(e) => setFilterProducer(e.target.value)} + aria-label="Filter by producer" + /> + + + + + ({ value: name, text: name })), + ]} + value={filterObject} + onChange={(e) => setFilterObject(e.target.value)} + aria-label="Select object" + /> + + + +
+
+ setSelectedNode(null)} + > + + + + +
+ {selectedNode && ( + setSelectedNode(null)} + /> + )} +
+
+ ); +}; + +export { LineageGraph }; +export default LineageGraph; diff --git a/ui/src/components/PermissionFormModal.tsx b/ui/src/components/PermissionFormModal.tsx new file mode 100644 index 00000000000..4498cd3f08b --- /dev/null +++ b/ui/src/components/PermissionFormModal.tsx @@ -0,0 +1,581 @@ +import React, { useState } from "react"; +import { + EuiFormRow, + EuiFieldText, + EuiSpacer, + EuiCallOut, + EuiCheckboxGroup, + EuiRadioGroup, + EuiTitle, + EuiHorizontalRule, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiFieldText as EuiField, + EuiButtonIcon, + EuiToolTip, + EuiPanel, +} from "@elastic/eui"; +import FormModal from "./forms/FormModal"; +import TagsEditor, { TagEntry } from "./forms/TagsEditor"; + +const RESOURCE_TYPES = [ + { id: "FEATURE_VIEW", label: "Feature View" }, + { id: "ON_DEMAND_FEATURE_VIEW", label: "On-Demand Feature View" }, + { id: "BATCH_FEATURE_VIEW", label: "Batch Feature View" }, + { id: "STREAM_FEATURE_VIEW", label: "Stream Feature View" }, + { id: "ENTITY", label: "Entity" }, + { id: "FEATURE_SERVICE", label: "Feature Service" }, + { id: "DATA_SOURCE", label: "Data Source" }, + { id: "VALIDATION_REFERENCE", label: "Validation Reference" }, + { id: "SAVED_DATASET", label: "Saved Dataset" }, + { id: "PERMISSION", label: "Permission" }, + { id: "PROJECT", label: "Project" }, + { id: "LABEL_VIEW", label: "Label View" }, +]; + +const ACTIONS = [ + { id: "CREATE", label: "Create" }, + { id: "DESCRIBE", label: "Describe" }, + { id: "UPDATE", label: "Update" }, + { id: "DELETE", label: "Delete" }, + { id: "READ_ONLINE", label: "Read Online" }, + { id: "READ_OFFLINE", label: "Read Offline" }, + { id: "WRITE_ONLINE", label: "Write Online" }, + { id: "WRITE_OFFLINE", label: "Write Offline" }, +]; + +const ACTION_PRESETS = [ + { + id: "all", + label: "All Actions", + actions: ACTIONS.map((a) => a.id), + }, + { + id: "crud", + label: "CRUD Only", + actions: ["CREATE", "DESCRIBE", "UPDATE", "DELETE"], + }, + { + id: "read", + label: "Read Only", + actions: ["DESCRIBE", "READ_ONLINE", "READ_OFFLINE"], + }, + { + id: "write", + label: "Write Only", + actions: ["WRITE_ONLINE", "WRITE_OFFLINE"], + }, +]; + +type PolicyType = "role_based" | "group_based" | "namespace_based" | "combined"; + +const POLICY_TYPE_OPTIONS = [ + { id: "role_based", label: "Role-based" }, + { id: "group_based", label: "Group-based" }, + { id: "namespace_based", label: "Namespace-based" }, + { id: "combined", label: "Combined (Groups + Namespaces)" }, +]; + +interface PermissionFormData { + name: string; + types: string[]; + namePatterns: string[]; + actions: string[]; + policyType: PolicyType; + roles: string[]; + groups: string[]; + namespaces: string[]; + tags: TagEntry[]; + requiredTags: TagEntry[]; +} + +interface PermissionFormModalProps { + onClose: () => void; + onSubmit: (data: PermissionFormData) => void; + initialData?: PermissionFormData; + isEdit?: boolean; + isSubmitting?: boolean; + submitError?: string | null; +} + +const EMPTY_FORM: PermissionFormData = { + name: "", + types: [], + namePatterns: [], + actions: [], + policyType: "role_based", + roles: [], + groups: [], + namespaces: [], + tags: [], + requiredTags: [], +}; + +const StringListEditor: React.FC<{ + items: string[]; + onChange: (items: string[]) => void; + placeholder: string; + addLabel: string; +}> = ({ items, onChange, placeholder, addLabel }) => { + return ( + <> + {items.map((item, index) => ( + + + { + const updated = [...items]; + updated[index] = e.target.value; + onChange(updated); + }} + compressed + /> + + + onChange(items.filter((_, i) => i !== index))} + /> + + + ))} + + onChange([...items, ""])} + flush="left" + > + {addLabel} + + + ); +}; + +const PermissionFormModal: React.FC = ({ + onClose, + onSubmit, + initialData, + isEdit = false, + isSubmitting = false, + submitError, +}) => { + const [formData, setFormData] = useState( + initialData || EMPTY_FORM, + ); + const [errors, setErrors] = useState>({}); + + const validate = (): boolean => { + const newErrors: Record = {}; + + if (!formData.name.trim()) { + newErrors.name = "Permission name is required."; + } else if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(formData.name)) { + newErrors.name = + "Must start with a letter or underscore, and contain only letters, numbers, underscores, and hyphens."; + } + + if (formData.types.length === 0) { + newErrors.types = "Select at least one resource type."; + } + + if (formData.actions.length === 0) { + newErrors.actions = "Select at least one action."; + } + + if (formData.policyType === "role_based") { + const nonEmpty = formData.roles.filter((r) => r.trim()); + if (nonEmpty.length === 0) { + newErrors.policy = "Add at least one role."; + } + } else if (formData.policyType === "group_based") { + const nonEmpty = formData.groups.filter((g) => g.trim()); + if (nonEmpty.length === 0) { + newErrors.policy = "Add at least one group."; + } + } else if (formData.policyType === "namespace_based") { + const nonEmpty = formData.namespaces.filter((n) => n.trim()); + if (nonEmpty.length === 0) { + newErrors.policy = "Add at least one namespace."; + } + } else if (formData.policyType === "combined") { + const hasGroups = formData.groups.filter((g) => g.trim()).length > 0; + const hasNamespaces = + formData.namespaces.filter((n) => n.trim()).length > 0; + if (!hasGroups && !hasNamespaces) { + newErrors.policy = "Add at least one group or namespace."; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = () => { + if (validate()) { + onSubmit(formData); + } + }; + + const selectedTypesMap: Record = {}; + formData.types.forEach((t) => { + selectedTypesMap[t] = true; + }); + + const selectedActionsMap: Record = {}; + formData.actions.forEach((a) => { + selectedActionsMap[a] = true; + }); + + const handleTypeToggle = (id: string) => { + setFormData((prev) => ({ + ...prev, + types: prev.types.includes(id) + ? prev.types.filter((t) => t !== id) + : [...prev.types, id], + })); + }; + + const handleActionToggle = (id: string) => { + setFormData((prev) => ({ + ...prev, + actions: prev.actions.includes(id) + ? prev.actions.filter((a) => a !== id) + : [...prev.actions, id], + })); + }; + + const applyActionPreset = (presetActions: string[]) => { + setFormData((prev) => ({ ...prev, actions: [...presetActions] })); + }; + + const selectAllTypes = () => { + setFormData((prev) => ({ + ...prev, + types: RESOURCE_TYPES.map((t) => t.id), + })); + }; + + const clearAllTypes = () => { + setFormData((prev) => ({ ...prev, types: [] })); + }; + + return ( + + {submitError && ( + <> + + + + )} + + {/* Section 1: Identity */} + +

Identity

+
+ + + + + setFormData((prev) => ({ ...prev, name: e.target.value })) + } + isInvalid={!!errors.name} + disabled={isEdit} + placeholder="e.g. data-team-read-access" + /> + + + + + {/* Section 2: Resource Scope */} + +

Resource Scope

+
+ +

Define which resources this permission applies to.

+
+ + + + <> + + + + Select All + + + + + Clear All + + + + + + + + + + + + + + Name Patterns{" "} + + (optional) + + + + } + > + + setFormData((prev) => ({ ...prev, namePatterns })) + } + placeholder="e.g. driver_.*" + addLabel="Add name pattern" + /> + + + + + + Required Tags{" "} + + (optional) + + + + } + > + + setFormData((prev) => ({ ...prev, requiredTags })) + } + /> + + + + + {/* Section 3: Actions */} + +

Allowed Actions

+
+ +

Which operations are permitted on matching resources.

+
+ + + + <> + + {ACTION_PRESETS.map((preset) => ( + + applyActionPreset(preset.actions)} + color={ + JSON.stringify([...formData.actions].sort()) === + JSON.stringify([...preset.actions].sort()) + ? "primary" + : "text" + } + > + {preset.label} + + + ))} + + + + + + + + + + {/* Section 4: Policy */} + +

Access Policy

+
+ +

+ Define who is authorized. A user must match at least one entry in the + policy to be granted access. +

+
+ + + + + setFormData((prev) => ({ + ...prev, + policyType: id as PolicyType, + })) + } + compressed + /> + + + + + {errors.policy && ( + <> + + + + )} + + {(formData.policyType === "role_based" || + formData.policyType === "combined") && ( + + <> + {formData.policyType === "role_based" && ( + +

User must have at least one of these roles.

+
+ )} + {formData.policyType === "role_based" && ( + + setFormData((prev) => ({ ...prev, roles })) + } + placeholder="e.g. admin, data-engineer" + addLabel="Add role" + /> + )} + +
+ )} + + {(formData.policyType === "group_based" || + formData.policyType === "combined") && ( + + <> + {formData.policyType === "group_based" && ( + +

User must belong to at least one of these groups.

+
+ )} + + setFormData((prev) => ({ ...prev, groups })) + } + placeholder="e.g. ml-team, platform-team" + addLabel="Add group" + /> + +
+ )} + + {(formData.policyType === "namespace_based" || + formData.policyType === "combined") && ( + + <> + {formData.policyType === "namespace_based" && ( + +

User must be in at least one of these namespaces.

+
+ )} + + setFormData((prev) => ({ ...prev, namespaces })) + } + placeholder="e.g. production, staging" + addLabel="Add namespace" + /> + +
+ )} + + {formData.policyType === "combined" && ( + <> + + +

+ User must match at least one group or one + namespace. +

+
+ + )} + + + + {/* Section 5: Tags */} + +

+ Metadata Tags{" "} + + (optional) + +

+
+ + setFormData((prev) => ({ ...prev, tags }))} + /> +
+ ); +}; + +export default PermissionFormModal; +export type { PermissionFormData }; diff --git a/ui/src/components/PermissionsDisplay.tsx b/ui/src/components/PermissionsDisplay.tsx index 47af120a1cd..dd9fe58c336 100644 --- a/ui/src/components/PermissionsDisplay.tsx +++ b/ui/src/components/PermissionsDisplay.tsx @@ -3,18 +3,66 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, - EuiPanel, EuiText, - EuiTitle, - EuiHorizontalRule, EuiToolTip, + EuiPanel, + EuiSpacer, + EuiHorizontalRule, } from "@elastic/eui"; -import { formatPermissions } from "../utils/permissionUtils"; interface PermissionsDisplayProps { permissions: any[] | undefined; } +const ACTION_NAMES = [ + "CREATE", + "DESCRIBE", + "UPDATE", + "DELETE", + "READ_ONLINE", + "READ_OFFLINE", + "WRITE_ONLINE", + "WRITE_OFFLINE", +]; + +const getActionColor = (action: string) => { + if (action.startsWith("READ")) return "success"; + if (action.startsWith("WRITE")) return "warning"; + if (action === "CREATE") return "primary"; + if (action === "UPDATE") return "accent"; + if (action === "DELETE") return "danger"; + if (action === "DESCRIBE") return "hollow"; + return "default"; +}; + +const getPolicyLabel = (policy: any): string => { + if (!policy) return "Allow All"; + if (policy.roleBasedPolicy?.roles) { + return `Roles: ${policy.roleBasedPolicy.roles.join(", ")}`; + } + if (policy.groupBasedPolicy?.groups) { + return `Groups: ${policy.groupBasedPolicy.groups.join(", ")}`; + } + if (policy.namespaceBasedPolicy?.namespaces) { + return `Namespaces: ${policy.namespaceBasedPolicy.namespaces.join(", ")}`; + } + if (policy.combinedGroupNamespacePolicy) { + const parts = []; + if (policy.combinedGroupNamespacePolicy.groups?.length) { + parts.push( + `Groups: ${policy.combinedGroupNamespacePolicy.groups.join(", ")}`, + ); + } + if (policy.combinedGroupNamespacePolicy.namespaces?.length) { + parts.push( + `Namespaces: ${policy.combinedGroupNamespacePolicy.namespaces.join(", ")}`, + ); + } + return parts.join(" | ") || "Allow All"; + } + return "Allow All"; +}; + const PermissionsDisplay: React.FC = ({ permissions, }) => { @@ -26,78 +74,65 @@ const PermissionsDisplay: React.FC = ({ ); } - const getActionColor = (action: string) => { - if (action.startsWith("READ")) return "success"; - if (action.startsWith("WRITE")) return "warning"; - if (action === "CREATE") return "primary"; - if (action === "UPDATE") return "accent"; - if (action === "DELETE") return "danger"; - return "default"; - }; - return ( {permissions.map((permission, index) => { - const actions = permission.spec?.actions?.map((a: number) => { - const actionNames = [ - "CREATE", - "DESCRIBE", - "UPDATE", - "DELETE", - "READ_ONLINE", - "READ_OFFLINE", - "WRITE_ONLINE", - "WRITE_OFFLINE", - ]; - return actionNames[a] || `Unknown (${a})`; - }); + const actions = (permission.spec?.actions || []).map( + (a: number) => ACTION_NAMES[a] || `Unknown (${a})`, + ); + const policy = permission.spec?.policy; + const policyLabel = getPolicyLabel(policy); + const namePatterns = + permission.spec?.namePatterns || permission.spec?.name_patterns || []; + const requiredTags = + permission.spec?.requiredTags || permission.spec?.required_tags || {}; return ( -
- -

- Name: {permission.spec?.name} -

-

- Policy:{" "} - {permission.spec?.policy?.roles - ? `Roles: ${permission.spec.policy.roles.join(", ")}` - : "No policy defined"} -

- {permission.spec?.name_patterns && ( -

- Name Patterns:{" "} - {Array.isArray(permission.spec.name_patterns) - ? permission.spec.name_patterns.join(", ") - : permission.spec.name_patterns} -

- )} - {permission.spec?.required_tags && ( -

- Required Tags:{" "} - {Object.entries(permission.spec.required_tags) - .map(([key, value]) => `${key}: ${value}`) - .join(", ")} -

- )} -
- } - > + + {index > 0 && } +

{permission.spec?.name}

- - - {actions.map((action: string, actionIndex: number) => ( - - {action} - - ))} - -
+ + + {actions.map((action: string, actionIndex: number) => ( + + {action} + + ))} + + + +

+ Policy: {policyLabel} +

+ {namePatterns.length > 0 && ( +

+ Name Patterns:{" "} + {namePatterns.join(", ")} +

+ )} + {Object.keys(requiredTags).length > 0 && ( +

+ Required Tags:{" "} + {Object.entries(requiredTags) + .map(([key, value]) => `${key}: ${value}`) + .join(", ")} +

+ )} +
+ } + > + + {policyLabel} + + + + ); })} diff --git a/ui/src/components/ProjectSelector.test.tsx b/ui/src/components/ProjectSelector.test.tsx index 40d89cde93c..d311e7ef980 100644 --- a/ui/src/components/ProjectSelector.test.tsx +++ b/ui/src/components/ProjectSelector.test.tsx @@ -5,18 +5,10 @@ import userEvent from "@testing-library/user-event"; import FeastUISansProviders from "../FeastUISansProviders"; -import { - projectsListWithDefaultProject, - creditHistoryRegistry, - creditHistoryRegistryDB, -} from "../mocks/handlers"; +import { allRestHandlers } from "../mocks/handlers"; // declare which API requests to mock -const server = setupServer( - projectsListWithDefaultProject, - creditHistoryRegistry, - creditHistoryRegistryDB, -); +const server = setupServer(...allRestHandlers); // establish API mocking before all tests beforeAll(() => server.listen()); @@ -48,12 +40,12 @@ test("in a full App render, it shows the right initial project", async () => { // Wait for Project Data from Registry to Load await screen.findAllByRole("heading", { - name: /Project: credit_scoring_aws/i, + name: /Project: Credit Score Project/i, }); // Before User Event: Heading is the credit scoring project screen.getByRole("heading", { - name: /credit_scoring_aws/i, + name: /Credit Score Project/i, }); // Do the select option user event @@ -78,6 +70,6 @@ test("in a full App render, it shows the right initial project", async () => { // ... and the new heading should appear // meaning we successfully navigated await screen.findByRole("heading", { - name: /Project: credit_scoring_aws/i, + name: /Project: Credit Score Project/i, }); }); diff --git a/ui/src/components/RegistrySearch.tsx b/ui/src/components/RegistrySearch.tsx index 46d72966713..ed3b987240e 100644 --- a/ui/src/components/RegistrySearch.tsx +++ b/ui/src/components/RegistrySearch.tsx @@ -3,6 +3,7 @@ import React, { useRef, forwardRef, useImperativeHandle, + useCallback, } from "react"; import { EuiText, @@ -17,8 +18,6 @@ import { } from "@elastic/eui"; import EuiCustomLink from "./EuiCustomLink"; -import { css } from "@emotion/react"; - const searchResultsStyles = { searchResults: { marginTop: "8px", @@ -68,11 +67,11 @@ const RegistrySearch = forwardRef( const [searchText, setSearchText] = useState(""); const inputRef = useRef(null); - const focusSearchInput = () => { + const focusSearchInput = useCallback(() => { if (inputRef.current) { inputRef.current.focus(); } - }; + }, []); useImperativeHandle( ref, diff --git a/ui/src/components/RegistryVisualization.test.tsx b/ui/src/components/RegistryVisualization.test.tsx new file mode 100644 index 00000000000..01da8f415a3 --- /dev/null +++ b/ui/src/components/RegistryVisualization.test.tsx @@ -0,0 +1,372 @@ +import React from "react"; +import { render, screen, within } from "../test-utils"; +import { waitFor } from "@testing-library/react"; +import RegistryVisualization from "./RegistryVisualization"; +import { feast } from "../protos"; +import { FEAST_FCO_TYPES } from "../parsers/types"; +import { EntityRelation } from "../parsers/parseEntityRelationships"; +import { ThemeProvider } from "../contexts/ThemeContext"; + +// ReactFlow requires ResizeObserver +class ResizeObserverMock { + observe() {} + unobserve() {} + disconnect() {} +} +(global as any).ResizeObserver = ResizeObserverMock; + +// Helper to build minimal registry data +const makeRegistry = ( + overrides: Partial = {}, +): feast.core.Registry => { + return feast.core.Registry.create({ + featureViews: [], + onDemandFeatureViews: [], + streamFeatureViews: [], + featureServices: [], + entities: [], + dataSources: [], + ...overrides, + }); +}; + +const makeRelationship = ( + sourceName: string, + sourceType: FEAST_FCO_TYPES, + targetName: string, + targetType: FEAST_FCO_TYPES, +): EntityRelation => ({ + source: { name: sourceName, type: sourceType }, + target: { name: targetName, type: targetType }, +}); + +const renderVisualization = ( + registryData: feast.core.Registry, + relationships: EntityRelation[] = [], + indirectRelationships: EntityRelation[] = [], +) => { + return render( + + + , + ); +}; + +describe("RegistryVisualization version indicators", () => { + test("renders version badge on feature view with currentVersionNumber > 1", async () => { + const registry = makeRegistry({ + featureViews: [ + feast.core.FeatureView.create({ + spec: { name: "my_feature_view" }, + meta: { currentVersionNumber: 3 }, + }), + ], + entities: [ + feast.core.Entity.create({ + spec: { name: "my_entity" }, + }), + ], + }); + + const relationships = [ + makeRelationship( + "my_feature_view", + FEAST_FCO_TYPES.featureView, + "my_entity", + FEAST_FCO_TYPES.entity, + ), + ]; + + renderVisualization(registry, relationships); + + await waitFor(() => { + expect(screen.getByText("v3")).toBeInTheDocument(); + }); + }); + + test("does not render version badge when currentVersionNumber is 0", async () => { + const registry = makeRegistry({ + featureViews: [ + feast.core.FeatureView.create({ + spec: { name: "unversioned_fv" }, + meta: { currentVersionNumber: 0 }, + }), + ], + entities: [ + feast.core.Entity.create({ + spec: { name: "my_entity" }, + }), + ], + }); + + const relationships = [ + makeRelationship( + "unversioned_fv", + FEAST_FCO_TYPES.featureView, + "my_entity", + FEAST_FCO_TYPES.entity, + ), + ]; + + renderVisualization(registry, relationships); + + await waitFor(() => { + expect(screen.getByText("unversioned_fv")).toBeInTheDocument(); + }); + + expect(screen.queryByText("v0")).not.toBeInTheDocument(); + }); + + test("does not render version badge when currentVersionNumber is 1 (initial version)", async () => { + const registry = makeRegistry({ + featureViews: [ + feast.core.FeatureView.create({ + spec: { name: "initial_fv" }, + meta: { currentVersionNumber: 1 }, + }), + ], + entities: [ + feast.core.Entity.create({ + spec: { name: "my_entity" }, + }), + ], + }); + + const relationships = [ + makeRelationship( + "initial_fv", + FEAST_FCO_TYPES.featureView, + "my_entity", + FEAST_FCO_TYPES.entity, + ), + ]; + + renderVisualization(registry, relationships); + + await waitFor(() => { + expect(screen.getByText("initial_fv")).toBeInTheDocument(); + }); + + expect(screen.queryByText("v1")).not.toBeInTheDocument(); + }); + + test("does not render version badge when meta has no version", async () => { + const registry = makeRegistry({ + featureViews: [ + feast.core.FeatureView.create({ + spec: { name: "no_version_fv" }, + meta: {}, + }), + ], + entities: [ + feast.core.Entity.create({ + spec: { name: "my_entity" }, + }), + ], + }); + + const relationships = [ + makeRelationship( + "no_version_fv", + FEAST_FCO_TYPES.featureView, + "my_entity", + FEAST_FCO_TYPES.entity, + ), + ]; + + renderVisualization(registry, relationships); + + await waitFor(() => { + expect(screen.getByText("no_version_fv")).toBeInTheDocument(); + }); + + // No version badges should exist at all + const badges = screen.queryAllByText(/^v\d+$/); + expect(badges).toHaveLength(0); + }); + + test("renders version badge on on-demand feature view", async () => { + const registry = makeRegistry({ + featureViews: [ + feast.core.FeatureView.create({ + spec: { name: "regular_fv" }, + }), + ], + onDemandFeatureViews: [ + feast.core.OnDemandFeatureView.create({ + spec: { name: "odfv_versioned" }, + meta: { currentVersionNumber: 2 }, + }), + ], + entities: [ + feast.core.Entity.create({ + spec: { name: "my_entity" }, + }), + ], + }); + + // Connect regular FV to entity so nodes render, and ODFV to regular FV + const relationships = [ + makeRelationship( + "regular_fv", + FEAST_FCO_TYPES.featureView, + "my_entity", + FEAST_FCO_TYPES.entity, + ), + ]; + + // Use the full component and toggle isolated nodes to show ODFV + render( + + + , + ); + + // Enable isolated nodes to show the ODFV + const isolatedCheckbox = screen.getByLabelText( + /Show Objects Without Relationships/i, + ); + isolatedCheckbox.click(); + + await waitFor(() => { + expect(screen.getByText("v2")).toBeInTheDocument(); + }); + }); + + test("renders version badge on stream feature view", async () => { + const registry = makeRegistry({ + featureViews: [ + feast.core.FeatureView.create({ + spec: { name: "regular_fv" }, + }), + ], + streamFeatureViews: [ + feast.core.StreamFeatureView.create({ + spec: { name: "sfv_versioned" }, + meta: { currentVersionNumber: 5 }, + }), + ], + entities: [ + feast.core.Entity.create({ + spec: { name: "my_entity" }, + }), + ], + }); + + const relationships = [ + makeRelationship( + "regular_fv", + FEAST_FCO_TYPES.featureView, + "my_entity", + FEAST_FCO_TYPES.entity, + ), + ]; + + render( + + + , + ); + + // Enable isolated nodes to show the SFV + const isolatedCheckbox = screen.getByLabelText( + /Show Objects Without Relationships/i, + ); + isolatedCheckbox.click(); + + await waitFor(() => { + expect(screen.getByText("v5")).toBeInTheDocument(); + }); + }); + + test("renders version history info from featureViewVersionHistory", async () => { + const registry = makeRegistry({ + featureViews: [ + feast.core.FeatureView.create({ + spec: { name: "versioned_fv" }, + meta: { currentVersionNumber: 2 }, + }), + ], + entities: [ + feast.core.Entity.create({ + spec: { name: "my_entity" }, + }), + ], + featureViewVersionHistory: feast.core.FeatureViewVersionHistory.create({ + records: [ + feast.core.FeatureViewVersionRecord.create({ + featureViewName: "versioned_fv", + versionNumber: 1, + description: "Initial version", + }), + feast.core.FeatureViewVersionRecord.create({ + featureViewName: "versioned_fv", + versionNumber: 2, + description: "Added new features", + }), + ], + }), + }); + + const relationships = [ + makeRelationship( + "versioned_fv", + FEAST_FCO_TYPES.featureView, + "my_entity", + FEAST_FCO_TYPES.entity, + ), + ]; + + renderVisualization(registry, relationships); + + await waitFor(() => { + expect(screen.getByText("v2")).toBeInTheDocument(); + }); + }); +}); + +describe("RegistryVisualization legend", () => { + test("renders Version Changed entry in legend", async () => { + const registry = makeRegistry({ + featureViews: [ + feast.core.FeatureView.create({ + spec: { name: "some_fv" }, + }), + ], + entities: [ + feast.core.Entity.create({ + spec: { name: "some_entity" }, + }), + ], + }); + + const relationships = [ + makeRelationship( + "some_fv", + FEAST_FCO_TYPES.featureView, + "some_entity", + FEAST_FCO_TYPES.entity, + ), + ]; + + renderVisualization(registry, relationships); + + await waitFor(() => { + expect(screen.getByText("Version Changed")).toBeInTheDocument(); + }); + + expect(screen.getByText("vN")).toBeInTheDocument(); + }); +}); diff --git a/ui/src/components/RegistryVisualization.tsx b/ui/src/components/RegistryVisualization.tsx index 64dc9f5f9b2..b8730e59e0b 100644 --- a/ui/src/components/RegistryVisualization.tsx +++ b/ui/src/components/RegistryVisualization.tsx @@ -21,9 +21,11 @@ import { EuiSpacer, EuiLoadingSpinner, EuiToolTip, + EuiBadge, } from "@elastic/eui"; import { FEAST_FCO_TYPES } from "../parsers/types"; import { EntityRelation } from "../parsers/parseEntityRelationships"; +import { MlflowRunData } from "../queries/useLoadMlflowRuns"; import { feast } from "../protos"; import { useTheme } from "../contexts/ThemeContext"; import { @@ -64,6 +66,8 @@ interface NodeData { type: FEAST_FCO_TYPES; metadata: any; permissions?: any[]; // Add permissions field + versionNumber?: number; + versionInfo?: { totalVersions: number; latestDescription?: string }; } const getNodeColor = (type: FEAST_FCO_TYPES) => { @@ -76,6 +80,16 @@ const getNodeColor = (type: FEAST_FCO_TYPES) => { return "#ff8000"; // Orange case FEAST_FCO_TYPES.dataSource: return "#cc0000"; // Red + case FEAST_FCO_TYPES.labelView: + return "#e6570e"; // Deep orange for label views + case FEAST_FCO_TYPES.mlflowRun: + return "#0194e2"; // MLflow brand blue + case FEAST_FCO_TYPES.mlflowModel: + return "#7b2d8e"; // Purple + case FEAST_FCO_TYPES.openlineageJob: + return "#e67300"; // Deep orange for OL jobs + case FEAST_FCO_TYPES.openlineageDataset: + return "#3366cc"; // Steel blue for OL datasets default: return "#666666"; // Gray } @@ -91,6 +105,16 @@ const getLightNodeColor = (type: FEAST_FCO_TYPES) => { return "#fff2e6"; // Light orange case FEAST_FCO_TYPES.dataSource: return "#ffe6e6"; // Light red + case FEAST_FCO_TYPES.labelView: + return "#fde8dc"; // Light deep orange + case FEAST_FCO_TYPES.mlflowRun: + return "#e6f6fd"; // Light MLflow blue + case FEAST_FCO_TYPES.mlflowModel: + return "#f3e6f9"; // Light purple + case FEAST_FCO_TYPES.openlineageJob: + return "#fff0e0"; // Light deep orange + case FEAST_FCO_TYPES.openlineageDataset: + return "#e0ecff"; // Light steel blue default: return "#f0f0f0"; // Light gray } @@ -106,6 +130,16 @@ const getNodeIcon = (type: FEAST_FCO_TYPES) => { return "▲"; // Triangle for entity case FEAST_FCO_TYPES.dataSource: return "◆"; // Diamond for data source + case FEAST_FCO_TYPES.labelView: + return "◉"; // Bullseye for label view + case FEAST_FCO_TYPES.mlflowRun: + return "⬡"; // Hexagon for MLflow run + case FEAST_FCO_TYPES.mlflowModel: + return "⬢"; // Filled hexagon for registered model + case FEAST_FCO_TYPES.openlineageJob: + return "⚙"; // Gear for OL job + case FEAST_FCO_TYPES.openlineageDataset: + return "⬡"; // Hexagon for OL dataset default: return "●"; // Default circle } @@ -119,8 +153,17 @@ const CustomNode = ({ data }: { data: NodeData }) => { const icon = getNodeIcon(data.type); const [isHovered, setIsHovered] = useState(false); const hasPermissions = data.permissions && data.permissions.length > 0; + const hasVersion = data.versionNumber != null && data.versionNumber > 1; const handleClick = () => { + if ( + (data.type === FEAST_FCO_TYPES.mlflowRun || + data.type === FEAST_FCO_TYPES.mlflowModel) && + data.metadata?.mlflow_url + ) { + window.open(data.metadata.mlflow_url, "_blank", "noopener,noreferrer"); + return; + } let path; switch (data.type) { case FEAST_FCO_TYPES.dataSource: @@ -135,6 +178,9 @@ const CustomNode = ({ data }: { data: NodeData }) => { case FEAST_FCO_TYPES.featureService: path = `/p/${projectName}/feature-service/${data.label}`; break; + case FEAST_FCO_TYPES.labelView: + path = `/p/${projectName}/label-view/${data.label}`; + break; default: return; } @@ -179,7 +225,10 @@ const CustomNode = ({ data }: { data: NodeData }) => { zIndex: 5, }} > - View Details + {data.type === FEAST_FCO_TYPES.mlflowRun || + data.type === FEAST_FCO_TYPES.mlflowModel + ? "Open in MLflow ↗" + : "View Details"} )} @@ -207,6 +256,40 @@ const CustomNode = ({ data }: { data: NodeData }) => { )} + {/* Version indicator */} + {hasVersion && ( + +
Version: {data.versionNumber}
+ {data.versionInfo && ( + <> +
Total versions: {data.versionInfo.totalVersions}
+ {data.versionInfo.latestDescription && ( +
Latest: {data.versionInfo.latestDescription}
+ )} + + )} +
+ Click node for full history +
+ + } + > +
+ v{data.versionNumber} +
+
+ )} + { @@ -414,8 +502,11 @@ const Legend = () => { const types = [ { type: FEAST_FCO_TYPES.featureService, label: "Feature Service" }, { type: FEAST_FCO_TYPES.featureView, label: "Feature View" }, + { type: FEAST_FCO_TYPES.labelView, label: "Label View" }, { type: FEAST_FCO_TYPES.entity, label: "Entity" }, { type: FEAST_FCO_TYPES.dataSource, label: "Data Source" }, + { type: FEAST_FCO_TYPES.mlflowRun, label: "MLflow Run" }, + { type: FEAST_FCO_TYPES.mlflowModel, label: "Registered Model" }, ]; const isDarkMode = colorMode === "dark"; @@ -474,6 +565,20 @@ const Legend = () => {
{item.label}
))} +
+ + vN + +
Version Changed
+
); }; @@ -482,10 +587,43 @@ const registryToFlow = ( objects: feast.core.Registry, relationships: EntityRelation[], permissions?: any[], + versionHistory?: feast.core.IFeatureViewVersionRecord[], + mlflowRuns?: MlflowRunData[], ) => { const nodes: Node[] = []; const edges: Edge[] = []; + // Build a lookup of version info by feature view name + const versionInfoMap = new Map< + string, + { totalVersions: number; latestDescription?: string } + >(); + if (versionHistory) { + const grouped: Record = {}; + for (let i = 0; i < versionHistory.length; i++) { + const record = versionHistory[i]; + const name = record.featureViewName; + if (!name) continue; + if (!grouped[name]) grouped[name] = []; + grouped[name].push(record); + } + const groupedNames = Object.keys(grouped); + for (let i = 0; i < groupedNames.length; i++) { + const name = groupedNames[i]; + const records = grouped[name]; + records.sort( + ( + a: feast.core.IFeatureViewVersionRecord, + b: feast.core.IFeatureViewVersionRecord, + ) => (b.versionNumber ?? 0) - (a.versionNumber ?? 0), + ); + versionInfoMap.set(name, { + totalVersions: records.length, + latestDescription: records[0]?.description ?? undefined, + }); + } + } + objects.featureServices?.forEach((fs) => { nodes.push({ id: `fs-${fs.spec?.name}`, @@ -507,60 +645,69 @@ const registryToFlow = ( }); objects.featureViews?.forEach((fv) => { + const fvName = fv.spec?.name; nodes.push({ - id: `fv-${fv.spec?.name}`, + id: `fv-${fvName}`, type: "custom", data: { - label: fv.spec?.name, + label: fvName, type: FEAST_FCO_TYPES.featureView, metadata: fv, permissions: permissions ? getEntityPermissions( permissions, FEAST_FCO_TYPES.featureView, - fv.spec?.name, + fvName, ) : [], + versionNumber: fv.meta?.currentVersionNumber ?? undefined, + versionInfo: fvName ? versionInfoMap.get(fvName) : undefined, }, position: { x: 0, y: 0 }, }); }); objects.onDemandFeatureViews?.forEach((odfv) => { + const odfvName = odfv.spec?.name; nodes.push({ - id: `odfv-${odfv.spec?.name}`, + id: `odfv-${odfvName}`, type: "custom", data: { - label: odfv.spec?.name, + label: odfvName, type: FEAST_FCO_TYPES.featureView, metadata: odfv, permissions: permissions ? getEntityPermissions( permissions, FEAST_FCO_TYPES.featureView, - odfv.spec?.name, + odfvName, ) : [], + versionNumber: odfv.meta?.currentVersionNumber ?? undefined, + versionInfo: odfvName ? versionInfoMap.get(odfvName) : undefined, }, position: { x: 0, y: 0 }, }); }); objects.streamFeatureViews?.forEach((sfv) => { + const sfvName = sfv.spec?.name; nodes.push({ - id: `sfv-${sfv.spec?.name}`, + id: `sfv-${sfvName}`, type: "custom", data: { - label: sfv.spec?.name, + label: sfvName, type: FEAST_FCO_TYPES.featureView, metadata: sfv, permissions: permissions ? getEntityPermissions( permissions, FEAST_FCO_TYPES.featureView, - sfv.spec?.name, + sfvName, ) : [], + versionNumber: sfv.meta?.currentVersionNumber ?? undefined, + versionInfo: sfvName ? versionInfoMap.get(sfvName) : undefined, }, position: { x: 0, y: 0 }, }); @@ -586,6 +733,25 @@ const registryToFlow = ( }); }); + objects.labelViews?.forEach((lv: any) => { + const lvName = lv.spec?.name; + nodes.push({ + id: `lv-${lvName}`, + type: "custom", + data: { + label: lvName, + type: FEAST_FCO_TYPES.labelView, + metadata: lv, + permissions: permissions + ? getEntityPermissions(permissions, FEAST_FCO_TYPES.labelView, lvName) + : [], + versionNumber: lv.meta?.currentVersionNumber ?? undefined, + versionInfo: lvName ? versionInfoMap.get(lvName) : undefined, + }, + position: { x: 0, y: 0 }, + }); + }); + const dataSources = new Set(); objects.featureViews?.forEach((fv) => { @@ -603,6 +769,18 @@ const registryToFlow = ( } }); + (objects as any).labelViews?.forEach((lv: any) => { + if (lv.spec?.source?.name) { + dataSources.add(lv.spec.source.name); + } + if (lv.spec?.source?.batchSource?.name) { + dataSources.add(lv.spec.source.batchSource.name); + } + if (lv.spec?.batchSource?.name) { + dataSources.add(lv.spec.batchSource.name); + } + }); + Array.from(dataSources).forEach((dsName) => { nodes.push({ id: `ds-${dsName}`, @@ -650,6 +828,101 @@ const registryToFlow = ( }); }); + if (mlflowRuns && mlflowRuns.length > 0) { + mlflowRuns.forEach((run) => { + const runLabel = run.run_name || run.run_id.substring(0, 8); + nodes.push({ + id: `mlflow-${run.run_id}`, + type: "custom", + data: { + label: runLabel, + type: FEAST_FCO_TYPES.mlflowRun, + metadata: { + mlflow_url: run.mlflow_url, + retrieval_type: run.retrieval_type, + status: run.status, + run_id: run.run_id, + }, + }, + position: { x: 0, y: 0 }, + }); + + if (run.feature_service) { + const fsNodeId = `fs-${run.feature_service}`; + const fsNodeExists = nodes.some((n) => n.id === fsNodeId); + if (fsNodeExists) { + edges.push({ + id: `edge-mlflow-${run.run_id}`, + source: fsNodeId, + sourceHandle: "source", + target: `mlflow-${run.run_id}`, + targetHandle: "target", + animated: true, + style: { + strokeWidth: 3, + stroke: "#0194e2", + strokeDasharray: "10 5", + animation: "dataflow 2s linear infinite", + }, + type: "smoothstep", + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20, + color: "#0194e2", + }, + }); + } + } + + if (run.registered_models && run.registered_models.length > 0) { + run.registered_models.forEach((model) => { + const modelNodeId = `model-${model.model_name}-v${model.version}`; + const modelExists = nodes.some((n) => n.id === modelNodeId); + if (!modelExists) { + nodes.push({ + id: modelNodeId, + type: "custom", + data: { + label: `${model.model_name} v${model.version}`, + type: FEAST_FCO_TYPES.mlflowModel, + metadata: { + mlflow_url: model.mlflow_url, + model_name: model.model_name, + version: model.version, + stage: model.stage, + }, + }, + position: { x: 0, y: 0 }, + }); + } + + edges.push({ + id: `edge-model-${run.run_id}-${model.model_name}-v${model.version}`, + source: `mlflow-${run.run_id}`, + sourceHandle: "source", + target: modelNodeId, + targetHandle: "target", + animated: true, + style: { + strokeWidth: 3, + stroke: "#7b2d8e", + strokeDasharray: "10 5", + animation: "dataflow 2s linear infinite", + }, + type: "smoothstep", + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20, + color: "#7b2d8e", + }, + }); + }); + } + }); + } + return { nodes, edges }; }; @@ -663,6 +936,16 @@ const getNodePrefix = (type: FEAST_FCO_TYPES) => { return "entity"; case FEAST_FCO_TYPES.dataSource: return "ds"; + case FEAST_FCO_TYPES.labelView: + return "lv"; + case FEAST_FCO_TYPES.mlflowRun: + return "mlflow"; + case FEAST_FCO_TYPES.mlflowModel: + return "model"; + case FEAST_FCO_TYPES.openlineageJob: + return "ol-job"; + case FEAST_FCO_TYPES.openlineageDataset: + return "ol-ds"; default: return "unknown"; } @@ -673,7 +956,10 @@ interface RegistryVisualizationProps { relationships: EntityRelation[]; indirectRelationships: EntityRelation[]; filterNode?: { type: FEAST_FCO_TYPES; name: string }; - permissions?: any[]; // Add permissions field + permissions?: any[]; + mlflowRuns?: MlflowRunData[]; + extraCheckboxes?: React.ReactNode; + filterControls?: React.ReactNode; } const RegistryVisualization: React.FC = ({ @@ -682,6 +968,9 @@ const RegistryVisualization: React.FC = ({ indirectRelationships, filterNode, permissions, + mlflowRuns, + extraCheckboxes, + filterControls, }) => { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -750,10 +1039,15 @@ const RegistryVisualization: React.FC = ({ return rel.source && rel.target && rel.source.name && rel.target.name; }); + const versionRecords = + registryData.featureViewVersionHistory?.records ?? undefined; + const { nodes: initialNodes, edges: initialEdges } = registryToFlow( registryData, validRelationships, permissions, + versionRecords as feast.core.IFeatureViewVersionRecord[] | undefined, + mlflowRuns, ); const { nodes: layoutedNodes, edges: layoutedEdges } = @@ -775,6 +1069,8 @@ const RegistryVisualization: React.FC = ({ showIndirectRelationships, showIsolatedNodes, filterNode, + permissions, + mlflowRuns, setNodes, setEdges, ]); @@ -792,7 +1088,15 @@ const RegistryVisualization: React.FC = ({

Lineage

-
+
+ {extraCheckboxes}
+ {filterControls} {loading ? (
diff --git a/ui/src/components/RegistryVisualizationTab.tsx b/ui/src/components/RegistryVisualizationTab.tsx index ebc77604322..4fed3c4f856 100644 --- a/ui/src/components/RegistryVisualizationTab.tsx +++ b/ui/src/components/RegistryVisualizationTab.tsx @@ -10,18 +10,26 @@ import { EuiFlexItem, } from "@elastic/eui"; import useLoadRegistry from "../queries/useLoadRegistry"; +import useLoadMlflowRuns from "../queries/useLoadMlflowRuns"; import RegistryPathContext from "../contexts/RegistryPathContext"; import RegistryVisualization from "./RegistryVisualization"; import { FEAST_FCO_TYPES } from "../parsers/types"; import { filterPermissionsByAction } from "../utils/permissionUtils"; -const RegistryVisualizationTab = () => { +interface RegistryVisualizationTabProps { + feastOnlyCheckbox?: React.ReactNode; +} + +const RegistryVisualizationTab: React.FC = ({ + feastOnlyCheckbox, +}) => { const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); const { isLoading, isSuccess, isError, data } = useLoadRegistry( registryUrl, projectName, ); + const { data: mlflowData } = useLoadMlflowRuns(); const [selectedObjectType, setSelectedObjectType] = useState(""); const [selectedObjectName, setSelectedObjectName] = useState(""); const [selectedPermissionAction, setSelectedPermissionAction] = useState(""); @@ -40,6 +48,13 @@ const RegistryVisualizationTab = () => { if (sfv.spec?.streamSource?.name) dataSources.add(sfv.spec.streamSource.name); }); + objects.labelViews?.forEach((lv: any) => { + if (lv.spec?.source?.name) dataSources.add(lv.spec.source.name); + if (lv.spec?.source?.batchSource?.name) + dataSources.add(lv.spec.source.batchSource.name); + if (lv.spec?.batchSource?.name) + dataSources.add(lv.spec.batchSource.name); + }); return Array.from(dataSources); case "entity": return objects.entities?.map((entity: any) => entity.spec?.name) || []; @@ -52,6 +67,8 @@ const RegistryVisualizationTab = () => { ...(objects.streamFeatureViews?.map((sfv: any) => sfv.spec?.name) || []), ]; + case "labelView": + return objects.labelViews?.map((lv: any) => lv.spec?.name) || []; case "featureService": return objects.featureServices?.map((fs: any) => fs.spec?.name) || []; default: @@ -82,66 +99,6 @@ const RegistryVisualizationTab = () => { {isSuccess && data && ( <> - - - - { - setSelectedObjectType(e.target.value); - setSelectedObjectName(""); // Reset name when type changes - }} - aria-label="Select object type" - /> - - - - - ({ - value: name, - text: name, - }), - ), - ]} - value={selectedObjectName} - onChange={(e) => setSelectedObjectName(e.target.value)} - aria-label="Select object" - disabled={selectedObjectType === ""} - /> - - - - - setSelectedPermissionAction(e.target.value)} - aria-label="Filter by permissions" - /> - - - { } : undefined } + mlflowRuns={mlflowData?.runs?.length ? mlflowData.runs : undefined} + extraCheckboxes={feastOnlyCheckbox} + filterControls={ + + + + { + setSelectedObjectType(e.target.value); + setSelectedObjectName(""); + }} + aria-label="Select object type" + /> + + + + + ({ + value: name, + text: name, + })), + ]} + value={selectedObjectName} + onChange={(e) => setSelectedObjectName(e.target.value)} + aria-label="Select object" + disabled={selectedObjectType === ""} + /> + + + + + + setSelectedPermissionAction(e.target.value) + } + aria-label="Filter by permissions" + /> + + + + } /> )} diff --git a/ui/src/components/forms/FeatureFieldEditor.tsx b/ui/src/components/forms/FeatureFieldEditor.tsx new file mode 100644 index 00000000000..be8b4f9f210 --- /dev/null +++ b/ui/src/components/forms/FeatureFieldEditor.tsx @@ -0,0 +1,163 @@ +import React from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiSelect, + EuiButtonEmpty, + EuiButtonIcon, + EuiText, + EuiHorizontalRule, + EuiSpacer, + EuiCallOut, +} from "@elastic/eui"; +import { VALUE_TYPE_OPTIONS } from "./ValueTypeSelect"; +import { feast } from "../../protos"; + +interface FeatureFieldEntry { + name: string; + valueType: string; + description: string; +} + +interface FeatureFieldEditorProps { + features: FeatureFieldEntry[]; + onChange: (features: FeatureFieldEntry[]) => void; + error?: string; +} + +const EMPTY_FEATURE: FeatureFieldEntry = { + name: "", + valueType: String(feast.types.ValueType.Enum.INT64), + description: "", +}; + +const FeatureFieldEditor: React.FC = ({ + features, + onChange, + error, +}) => { + const addFeature = () => { + onChange([...features, { ...EMPTY_FEATURE }]); + }; + + const removeFeature = (index: number) => { + onChange(features.filter((_, i) => i !== index)); + }; + + const updateFeature = ( + index: number, + field: keyof FeatureFieldEntry, + val: string, + ) => { + const updated = [...features]; + updated[index] = { ...updated[index], [field]: val }; + onChange(updated); + }; + + return ( + <> + + + + +

Features

+
+
+ + + Add feature + + +
+ + {error && ( + <> + + + + )} + + {features.length > 0 && ( + + + + Name + + + + + Type + + + + + Description + + + + + )} + + {features.map((feature, index) => ( + + + updateFeature(index, "name", e.target.value)} + compressed + /> + + + + updateFeature(index, "valueType", e.target.value) + } + compressed + /> + + + + updateFeature(index, "description", e.target.value) + } + compressed + /> + + + removeFeature(index)} + /> + + + ))} + + {features.length === 0 && ( + + No features added yet. Click "Add feature" above. + + )} + + ); +}; + +export default FeatureFieldEditor; +export type { FeatureFieldEntry }; diff --git a/ui/src/components/forms/FormModal.tsx b/ui/src/components/forms/FormModal.tsx new file mode 100644 index 00000000000..b205f9f35a2 --- /dev/null +++ b/ui/src/components/forms/FormModal.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, + EuiForm, +} from "@elastic/eui"; + +interface FormModalProps { + title: string; + submitLabel: string; + onClose: () => void; + onSubmit: () => void; + children: React.ReactNode; + width?: number; + isSubmitting?: boolean; +} + +const FormModal: React.FC = ({ + title, + submitLabel, + onClose, + onSubmit, + children, + width = 600, + isSubmitting = false, +}) => { + return ( + + + {title} + + + + {children} + + + + + Cancel + + + {submitLabel} + + + + ); +}; + +export default FormModal; diff --git a/ui/src/components/forms/NameDescriptionOwnerFields.tsx b/ui/src/components/forms/NameDescriptionOwnerFields.tsx new file mode 100644 index 00000000000..46d21c369ba --- /dev/null +++ b/ui/src/components/forms/NameDescriptionOwnerFields.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import { EuiFormRow, EuiFieldText, EuiTextArea } from "@elastic/eui"; + +interface NameDescriptionOwnerFieldsProps { + name: string; + description: string; + owner?: string; + onChangeName: (value: string) => void; + onChangeDescription: (value: string) => void; + onChangeOwner?: (value: string) => void; + nameDisabled?: boolean; + nameError?: string; + nameHelpText?: string; + namePlaceholder?: string; + descriptionPlaceholder?: string; +} + +const NameDescriptionOwnerFields: React.FC = ({ + name, + description, + owner, + onChangeName, + onChangeDescription, + onChangeOwner, + nameDisabled = false, + nameError, + nameHelpText, + namePlaceholder = "e.g. my_resource", + descriptionPlaceholder = "Describe this resource...", +}) => { + return ( + <> + + onChangeName(e.target.value)} + isInvalid={!!nameError} + disabled={nameDisabled} + placeholder={namePlaceholder} + /> + + + + onChangeDescription(e.target.value)} + placeholder={descriptionPlaceholder} + rows={2} + /> + + + {onChangeOwner !== undefined && ( + + onChangeOwner(e.target.value)} + placeholder="e.g. team-ml-platform" + /> + + )} + + ); +}; + +export default NameDescriptionOwnerFields; diff --git a/ui/src/components/forms/TagsEditor.tsx b/ui/src/components/forms/TagsEditor.tsx new file mode 100644 index 00000000000..73ea5df9c82 --- /dev/null +++ b/ui/src/components/forms/TagsEditor.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiButtonEmpty, + EuiButtonIcon, + EuiText, + EuiHorizontalRule, + EuiSpacer, + EuiCallOut, +} from "@elastic/eui"; + +interface TagEntry { + key: string; + value: string; +} + +interface TagsEditorProps { + tags: TagEntry[]; + onChange: (tags: TagEntry[]) => void; + error?: string; +} + +const TagsEditor: React.FC = ({ tags, onChange, error }) => { + const addTag = () => { + onChange([...tags, { key: "", value: "" }]); + }; + + const removeTag = (index: number) => { + onChange(tags.filter((_, i) => i !== index)); + }; + + const updateTag = (index: number, field: "key" | "value", val: string) => { + const updated = [...tags]; + updated[index] = { ...updated[index], [field]: val }; + onChange(updated); + }; + + return ( + <> + + + + +

Labels

+
+
+ + + Add label + + +
+ + {error && ( + <> + + + + )} + + {tags.map((tag, index) => ( + + + updateTag(index, "key", e.target.value)} + compressed + /> + + + updateTag(index, "value", e.target.value)} + compressed + /> + + + removeTag(index)} + /> + + + ))} + + {tags.length === 0 && ( + + No labels added yet. + + )} + + ); +}; + +export default TagsEditor; +export type { TagEntry }; diff --git a/ui/src/components/forms/ValueTypeSelect.tsx b/ui/src/components/forms/ValueTypeSelect.tsx new file mode 100644 index 00000000000..2718b7c027e --- /dev/null +++ b/ui/src/components/forms/ValueTypeSelect.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { EuiFormRow, EuiSelect } from "@elastic/eui"; +import { feast } from "../../protos"; + +const VALUE_TYPE_OPTIONS = [ + { value: String(feast.types.ValueType.Enum.STRING), text: "STRING" }, + { value: String(feast.types.ValueType.Enum.INT32), text: "INT32" }, + { value: String(feast.types.ValueType.Enum.INT64), text: "INT64" }, + { value: String(feast.types.ValueType.Enum.FLOAT), text: "FLOAT" }, + { value: String(feast.types.ValueType.Enum.DOUBLE), text: "DOUBLE" }, + { value: String(feast.types.ValueType.Enum.BOOL), text: "BOOL" }, + { value: String(feast.types.ValueType.Enum.BYTES), text: "BYTES" }, + { + value: String(feast.types.ValueType.Enum.UNIX_TIMESTAMP), + text: "UNIX_TIMESTAMP", + }, +]; + +interface ValueTypeSelectProps { + value: string; + onChange: (value: string) => void; + label?: string; + helpText?: string; + compressed?: boolean; +} + +const ValueTypeSelect: React.FC = ({ + value, + onChange, + label = "Value Type", + helpText, + compressed = false, +}) => { + return ( + + onChange(e.target.value)} + compressed={compressed} + /> + + ); +}; + +export default ValueTypeSelect; +export { VALUE_TYPE_OPTIONS }; diff --git a/ui/src/contexts/AuthContext.tsx b/ui/src/contexts/AuthContext.tsx new file mode 100644 index 00000000000..6003ff761d0 --- /dev/null +++ b/ui/src/contexts/AuthContext.tsx @@ -0,0 +1,245 @@ +import React, { + createContext, + useContext, + useState, + useCallback, + useEffect, + useRef, +} from "react"; +// @ts-ignore -- keycloak-js types use "exports" field; bundler resolves the JS fine +import Keycloak from "keycloak-js"; + +interface AuthUser { + username: string; + roles: string[]; + groups: string[]; + email?: string; +} + +interface AuthContextValue { + user: AuthUser | null; + isAuthenticated: boolean; + isAuthEnabled: boolean; + isInitializing: boolean; + logout: () => void; + keycloak: Keycloak | null; +} + +const AuthContext = createContext({ + user: null, + isAuthenticated: false, + isAuthEnabled: false, + isInitializing: true, + logout: () => {}, + keycloak: null, +}); + +interface ServerAuthConfig { + auth_type: string; + url?: string; + realm?: string; + client_id?: string; + auth_discovery_url?: string; +} + +function readServerAuthConfig(): ServerAuthConfig | null { + // Injected by the Feast UI server (feast ui) into index.html from feature_store.yaml + const el = document.getElementById("feast-auth-config"); + if (el) { + try { + return JSON.parse(el.textContent || "{}"); + } catch { + /* fall through */ + } + } + + // Dev mode fallback: fetch from /api/auth-config (served by setupProxy or REST server) + return null; +} + +function extractUser(kc: Keycloak): AuthUser { + const parsed = kc.tokenParsed as any; + const clientRoles: string[] = + parsed?.resource_access?.[kc.clientId!]?.roles || []; + const realmRoles: string[] = parsed?.realm_access?.roles || []; + const combined = [...clientRoles, ...realmRoles]; + + return { + username: parsed?.preferred_username || "unknown", + roles: combined.filter((v, i) => combined.indexOf(v) === i), + groups: parsed?.groups || [], + email: parsed?.email, + }; +} + +const TOKEN_REFRESH_INTERVAL = 30_000; +const MIN_VALIDITY_SECS = 60; + +const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [kc, setKc] = useState(null); + const [user, setUser] = useState(null); + const [isInitializing, setIsInitializing] = useState(true); + const [isAuthEnabled, setIsAuthEnabled] = useState(false); + const initStarted = useRef(false); + + const syncUser = useCallback((keycloak: Keycloak) => { + if (keycloak.authenticated) { + setUser(extractUser(keycloak)); + } + }, []); + + const doRefresh = useCallback( + (keycloak: Keycloak) => { + keycloak + .updateToken(MIN_VALIDITY_SECS) + .then((refreshed: boolean) => { + if (refreshed) { + syncUser(keycloak); + } + }) + .catch(() => { + console.warn("Token refresh failed — redirecting to login"); + keycloak.login(); + }); + }, + [syncUser], + ); + + useEffect(() => { + if (initStarted.current) return; + initStarted.current = true; + + const initAuth = async () => { + const config = readServerAuthConfig(); + + if (!config || config.auth_type !== "oidc") { + // Auth is disabled — skip Keycloak, render the app immediately + setIsAuthEnabled(false); + setIsInitializing(false); + return; + } + + setIsAuthEnabled(true); + + const keycloak = new Keycloak({ + url: config.url || "http://localhost:8080", + realm: config.realm || "feast", + clientId: config.client_id || "feast-ui", + }); + + keycloak.onTokenExpired = () => { + console.info("Access token expired — attempting refresh"); + doRefresh(keycloak); + }; + + keycloak.onAuthRefreshSuccess = () => { + syncUser(keycloak); + }; + + keycloak.onAuthRefreshError = () => { + console.warn("Auth refresh error — redirecting to login"); + keycloak.login(); + }; + + try { + const authenticated = await keycloak.init({ + onLoad: "login-required", + checkLoginIframe: false, + pkceMethod: "S256", + }); + + setKc(keycloak); + if (authenticated) { + syncUser(keycloak); + } + } catch (err) { + console.error("Keycloak init failed:", err); + } + + setIsInitializing(false); + }; + + initAuth(); + }, [doRefresh, syncUser]); + + // Proactive token refresh + useEffect(() => { + if (!kc?.authenticated) return; + const id = setInterval(() => doRefresh(kc), TOKEN_REFRESH_INTERVAL); + return () => clearInterval(id); + }, [kc, doRefresh]); + + // Global fetch interceptor: inject auth header on /api/ calls, handle 401 + useEffect(() => { + if (!kc) return; + + const originalFetch = window.fetch; + window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { + const url = + typeof input === "string" + ? input + : input instanceof Request + ? input.url + : input.toString(); + + const isApiCall = url.includes("/api/"); + + if (isApiCall && kc.authenticated && kc.token) { + const mergedHeaders = new Headers(init?.headers); + if (!mergedHeaders.has("Authorization")) { + mergedHeaders.set("Authorization", `Bearer ${kc.token}`); + } + init = { ...init, headers: mergedHeaders }; + } + + let response = await originalFetch(input, init); + + if (response.status === 401 && isApiCall) { + console.warn("API returned 401 — attempting token refresh and retry"); + try { + await kc.updateToken(5); + syncUser(kc); + const retryHeaders = new Headers(init?.headers); + retryHeaders.set("Authorization", `Bearer ${kc.token}`); + response = await originalFetch(input, { + ...init, + headers: retryHeaders, + }); + } catch { + kc.login(); + } + } + + return response; + }; + + return () => { + window.fetch = originalFetch; + }; + }, [kc, syncUser]); + + const logout = useCallback(() => { + if (kc) { + kc.logout({ redirectUri: window.location.origin }); + } + }, [kc]); + + const value: AuthContextValue = { + user, + isAuthenticated: !!kc?.authenticated, + isAuthEnabled, + isInitializing, + logout, + keycloak: kc, + }; + + return {children}; +}; + +const useAuth = () => useContext(AuthContext); + +export default AuthContext; +export { AuthProvider, useAuth }; +export type { AuthUser }; diff --git a/ui/src/contexts/DataModeContext.tsx b/ui/src/contexts/DataModeContext.tsx new file mode 100644 index 00000000000..c8ef4ea0bab --- /dev/null +++ b/ui/src/contexts/DataModeContext.tsx @@ -0,0 +1,20 @@ +import React, { useContext } from "react"; + +interface FetchOptions { + headers?: Record; + credentials?: RequestCredentials; +} + +interface DataModeConfig { + fetchOptions?: FetchOptions; +} + +const defaultConfig: DataModeConfig = {}; + +const DataModeContext = React.createContext(defaultConfig); + +const useDataMode = () => useContext(DataModeContext); + +export default DataModeContext; +export { useDataMode }; +export type { DataModeConfig, FetchOptions }; diff --git a/ui/src/contexts/MonitoringContext.ts b/ui/src/contexts/MonitoringContext.ts new file mode 100644 index 00000000000..f701cbcd5bf --- /dev/null +++ b/ui/src/contexts/MonitoringContext.ts @@ -0,0 +1,14 @@ +import React from "react"; + +interface MonitoringConfig { + apiBaseUrl: string; + enabled: boolean; +} + +const MonitoringContext = React.createContext({ + apiBaseUrl: "/api/v1", + enabled: false, +}); + +export default MonitoringContext; +export type { MonitoringConfig }; diff --git a/ui/src/contexts/ProjectListContext.ts b/ui/src/contexts/ProjectListContext.ts index c42b22f6611..c0c24840efb 100644 --- a/ui/src/contexts/ProjectListContext.ts +++ b/ui/src/contexts/ProjectListContext.ts @@ -13,12 +13,14 @@ const ProjectEntrySchema = z.object({ const ProjectsListSchema = z.object({ default: z.string().optional(), projects: z.array(ProjectEntrySchema), + mode: z.string().optional(), }); type ProjectsListType = z.infer; interface ProjectsListContextInterface { projectsListPromise: Promise; isCustom: boolean; + basename?: string; } const ProjectListContext = React.createContext< diff --git a/ui/src/contexts/RegistryRefreshContext.ts b/ui/src/contexts/RegistryRefreshContext.ts new file mode 100644 index 00000000000..12be5977d88 --- /dev/null +++ b/ui/src/contexts/RegistryRefreshContext.ts @@ -0,0 +1,22 @@ +import React, { useContext } from "react"; + +interface RegistryRefreshContextInterface { + refreshing: boolean; + handleRefresh: () => Promise; +} + +const RegistryRefreshContext = React.createContext< + RegistryRefreshContextInterface | undefined +>(undefined); + +const useRegistryRefreshContext = () => { + const ctx = useContext(RegistryRefreshContext); + if (!ctx) { + throw new Error( + "useRegistryRefreshContext must be used within RegistryRefreshContext.Provider", + ); + } + return ctx; +}; + +export { RegistryRefreshContext, useRegistryRefreshContext }; diff --git a/ui/src/custom-tabs/TabsRegistryContext.tsx b/ui/src/custom-tabs/TabsRegistryContext.tsx index 38c9ccea486..4152edd832d 100644 --- a/ui/src/custom-tabs/TabsRegistryContext.tsx +++ b/ui/src/custom-tabs/TabsRegistryContext.tsx @@ -16,7 +16,6 @@ import FeatureCustomTabLoadingWrapper from "../utils/custom-tabs/FeatureCustomTa import DataSourceCustomTabLoadingWrapper from "../utils/custom-tabs/DataSourceCustomTabLoadingWrapper"; import EntityCustomTabLoadingWrapper from "../utils/custom-tabs/EntityCustomTabLoadingWrapper"; import DatasetCustomTabLoadingWrapper from "../utils/custom-tabs/DatasetCustomTabLoadingWrapper"; -import CurlGeneratorTab from "../pages/feature-views/CurlGeneratorTab"; import { RegularFeatureViewCustomTabRegistrationInterface, diff --git a/ui/src/graphics/ComputeEngineIcon.tsx b/ui/src/graphics/ComputeEngineIcon.tsx new file mode 100644 index 00000000000..82873776ccf --- /dev/null +++ b/ui/src/graphics/ComputeEngineIcon.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +const ComputeEngineIcon = (props: React.SVGProps) => { + return ( + + + + + + + + + + + ); +}; + +export { ComputeEngineIcon }; diff --git a/ui/src/graphics/JobsIcon.tsx b/ui/src/graphics/JobsIcon.tsx new file mode 100644 index 00000000000..35f4c80af7c --- /dev/null +++ b/ui/src/graphics/JobsIcon.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +const JobsIcon = (props: React.SVGProps) => { + return ( + + + + + ); +}; + +export { JobsIcon }; diff --git a/ui/src/graphics/LabelViewIcon.tsx b/ui/src/graphics/LabelViewIcon.tsx new file mode 100644 index 00000000000..d154e8731a1 --- /dev/null +++ b/ui/src/graphics/LabelViewIcon.tsx @@ -0,0 +1,28 @@ +import React from "react"; + +const LabelViewIcon = (props: React.SVGProps) => { + return ( + + + + + + ); +}; + +export { LabelViewIcon }; diff --git a/ui/src/graphics/data-source-icons.tsx b/ui/src/graphics/data-source-icons.tsx new file mode 100644 index 00000000000..813c714a560 --- /dev/null +++ b/ui/src/graphics/data-source-icons.tsx @@ -0,0 +1,377 @@ +import React from "react"; + +export const BigQueryIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const SnowflakeIcon = (props: React.SVGProps) => ( + + + + +); + +export const RedshiftIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const KafkaIcon = (props: React.SVGProps) => ( + + + + + + + + + + + + + + + +); + +export const SparkIcon = (props: React.SVGProps) => ( + + + + + +); + +export const FileIcon = (props: React.SVGProps) => ( + + + + + + + + +); + +export const RequestSourceIcon = (props: React.SVGProps) => ( + + + + + + + +); + +export const PushSourceIcon = (props: React.SVGProps) => ( + + + + + +); + +export const KinesisIcon = (props: React.SVGProps) => ( + + + + + +); + +export const TrinoIcon = (props: React.SVGProps) => ( + + + + + + + +); + +export const AthenaIcon = (props: React.SVGProps) => ( + + + + +); + +export const CustomSourceIcon = (props: React.SVGProps) => ( + + + + +); + +export const RayIcon = (props: React.SVGProps) => ( + + + + + +); + +export const PostgresIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const MongoDBIcon = (props: React.SVGProps) => ( + + + + + +); + +export const SqlServerIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const OracleIcon = (props: React.SVGProps) => ( + + + + + ORA + + +); + +export const CouchbaseIcon = (props: React.SVGProps) => ( + + + + + + +); + +export const ClickHouseIcon = (props: React.SVGProps) => ( + + + + + + + +); diff --git a/ui/src/hooks/useFCOExploreSuggestions.ts b/ui/src/hooks/useFCOExploreSuggestions.ts index 43a0e1bea3f..e9ab456f72b 100644 --- a/ui/src/hooks/useFCOExploreSuggestions.ts +++ b/ui/src/hooks/useFCOExploreSuggestions.ts @@ -22,6 +22,11 @@ const FCO_TO_URL_NAME_MAP: Record = { entity: "/entity", featureView: "/feature-view", featureService: "/feature-service", + labelView: "/label-view", + mlflowRun: "/mlflow-run", + mlflowModel: "/mlflow-model", + openlineageJob: "/lineage", + openlineageDataset: "/lineage", }; const createSearchLink = ( diff --git a/ui/src/hooks/useRegistryRefresh.ts b/ui/src/hooks/useRegistryRefresh.ts new file mode 100644 index 00000000000..cb684cf5636 --- /dev/null +++ b/ui/src/hooks/useRegistryRefresh.ts @@ -0,0 +1,80 @@ +import { useCallback, useContext, useState } from "react"; +import { useQueryClient } from "react-query"; +import { + ProjectListContext, + ProjectsListSchema, +} from "../contexts/ProjectListContext"; +import { useDataMode } from "../contexts/DataModeContext"; + +interface Toast { + id: string; + title: string; + color: "success" | "danger"; + iconType: string; +} + +const useRegistryRefresh = () => { + const [refreshing, setRefreshing] = useState(false); + const [toasts, setToasts] = useState([]); + const queryClient = useQueryClient(); + const projectListCtx = useContext(ProjectListContext); + const basename = projectListCtx?.basename || ""; + const { fetchOptions } = useDataMode(); + + const removeToast = useCallback((removedToast: { id: string }) => { + setToasts((prev) => prev.filter((t) => t.id !== removedToast.id)); + }, []); + + const handleRefresh = useCallback(async () => { + setRefreshing(true); + try { + const refreshRes = await fetch(`${basename}/api/v1/registry/refresh`, { + method: "POST", + headers: { ...fetchOptions?.headers }, + credentials: fetchOptions?.credentials, + }); + if (!refreshRes.ok) { + throw new Error(`Registry refresh failed (${refreshRes.status})`); + } + const res = await fetch(`${basename}/projects-list.json`, { + headers: { + "Content-Type": "application/json", + ...fetchOptions?.headers, + }, + credentials: fetchOptions?.credentials, + }); + if (!res.ok) { + throw new Error(`Failed to fetch project list (${res.status})`); + } + const json = await res.json(); + const parsed = ProjectsListSchema.parse(json); + queryClient.setQueryData("feast-projects-list", parsed); + await queryClient.invalidateQueries("registry-rest-bulk"); + setToasts((prev) => [ + ...prev, + { + id: String(Date.now()), + title: "Refresh successful", + color: "success" as const, + iconType: "check", + }, + ]); + } catch { + setToasts((prev) => [ + ...prev, + { + id: String(Date.now()), + title: "Refresh failed", + color: "danger" as const, + iconType: "alert", + }, + ]); + } finally { + setRefreshing(false); + } + }, [basename, queryClient, fetchOptions]); + + return { refreshing, toasts, handleRefresh, removeToast }; +}; + +export default useRegistryRefresh; diff --git a/ui/src/hooks/useTagsAggregation.ts b/ui/src/hooks/useTagsAggregation.ts index 5d36fd54285..9ad0d78d6f5 100644 --- a/ui/src/hooks/useTagsAggregation.ts +++ b/ui/src/hooks/useTagsAggregation.ts @@ -1,13 +1,13 @@ -import { useContext, useMemo } from "react"; -import RegistryPathContext from "../contexts/RegistryPathContext"; -import useLoadRegistry from "../queries/useLoadRegistry"; -import { feast } from "../protos"; +import { useMemo } from "react"; +import { useParams } from "react-router-dom"; +import useResourceQuery, { + featureViewListPath, + featureServiceListPath, +} from "../queries/useResourceQuery"; -// Usage of generic type parameter T -// https://stackoverflow.com/questions/53203409/how-to-tell-typescript-that-im-returning-an-array-of-arrays-of-the-input-type const buildTagCollection = ( array: T[], - recordExtractor: (unknownFCO: T) => Record | undefined, // Assumes that tags are always a Record + recordExtractor: (unknownFCO: T) => Record | undefined, ): Record> => { const tagCollection = array.reduce( (memo: Record>, fco: T) => { @@ -38,17 +38,17 @@ const buildTagCollection = ( }; const useFeatureViewTagsAggregation = () => { - const registryUrl = useContext(RegistryPathContext); - const query = useLoadRegistry(registryUrl); + const { projectName } = useParams(); + const query = useResourceQuery({ + resourceType: "tags-fvs", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: (d) => d.featureViews, + }); const data = useMemo(() => { - return query.data && query.data.objects && query.data.objects.featureViews - ? buildTagCollection( - query.data.objects.featureViews!, - (fv) => { - return fv.spec?.tags!; - }, - ) + return query.data + ? buildTagCollection(query.data, (fv) => fv.spec?.tags) : undefined; }, [query.data]); @@ -59,19 +59,17 @@ const useFeatureViewTagsAggregation = () => { }; const useFeatureServiceTagsAggregation = () => { - const registryUrl = useContext(RegistryPathContext); - const query = useLoadRegistry(registryUrl); + const { projectName } = useParams(); + const query = useResourceQuery({ + resourceType: "tags-fss", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices, + }); const data = useMemo(() => { - return query.data && - query.data.objects && - query.data.objects.featureServices - ? buildTagCollection( - query.data.objects.featureServices, - (fs) => { - return fs.spec?.tags!; - }, - ) + return query.data + ? buildTagCollection(query.data, (fs) => fs.spec?.tags) : undefined; }, [query.data]); diff --git a/ui/src/mocks/handlers.ts b/ui/src/mocks/handlers.ts index 1c32bb2cf87..d9fb8a031b7 100644 --- a/ui/src/mocks/handlers.ts +++ b/ui/src/mocks/handlers.ts @@ -1,10 +1,48 @@ import { http, HttpResponse } from "msw"; import { readFileSync } from "fs"; import path from "path"; +import { feast } from "../protos"; -const registry = readFileSync( +const registryBuf = readFileSync( path.resolve(__dirname, "../../public/registry.db"), ); +const parsedRegistry = feast.core.Registry.decode(registryBuf); + +const toJSON = (obj: any) => (obj && obj.toJSON ? obj.toJSON() : obj); + +const entitiesJSON = (parsedRegistry.entities || []).map(toJSON); +const featureViewsJSON = (parsedRegistry.featureViews || []).map((fv) => ({ + ...toJSON(fv), + type: "featureView", +})); +const onDemandFVsJSON = (parsedRegistry.onDemandFeatureViews || []).map( + (fv) => ({ + ...toJSON(fv), + type: "onDemandFeatureView", + }), +); +const streamFVsJSON = (parsedRegistry.streamFeatureViews || []).map((fv) => ({ + ...toJSON(fv), + type: "streamFeatureView", +})); +const allFeatureViewsJSON = [ + ...featureViewsJSON, + ...onDemandFVsJSON, + ...streamFVsJSON, +]; +const featureServicesJSON = (parsedRegistry.featureServices || []).map(toJSON); +const dataSourcesJSON = (parsedRegistry.dataSources || []).map(toJSON); +const savedDatasetsJSON = (parsedRegistry.savedDatasets || []).map(toJSON); +const projectsJSON = (parsedRegistry.projects || []).map(toJSON); + +const allFeatures = featureViewsJSON.flatMap((fv: any) => + (fv?.spec?.features || []).map((f: any) => ({ + name: f.name, + featureViewName: fv.spec?.name, + valueType: f.valueType, + project: fv.spec?.project, + })), +); const projectsListWithDefaultProject = http.get("/projects-list.json", () => HttpResponse.json({ @@ -14,22 +52,266 @@ const projectsListWithDefaultProject = http.get("/projects-list.json", () => name: "Credit Score Project", description: "Project for credit scoring team and associated models.", id: "credit_scoring_aws", - registryPath: "/registry.db", // Changed to match what the test expects + registryPath: "/api/v1", }, ], }), ); -const creditHistoryRegistryPB = http.get("/registry.pb", () => { - return HttpResponse.arrayBuffer(registry.buffer); -}); +// REST API list endpoints +const restEntities = http.get("/api/v1/entities", () => + HttpResponse.json({ + entities: entitiesJSON, + pagination: {}, + relationships: {}, + }), +); + +const restFeatureViews = http.get("/api/v1/feature_views", () => + HttpResponse.json({ + featureViews: allFeatureViewsJSON, + pagination: {}, + relationships: {}, + }), +); + +const restFeatureServices = http.get("/api/v1/feature_services", () => + HttpResponse.json({ + featureServices: featureServicesJSON, + pagination: {}, + relationships: {}, + }), +); + +const restDataSources = http.get("/api/v1/data_sources", () => + HttpResponse.json({ + dataSources: dataSourcesJSON, + pagination: {}, + relationships: {}, + }), +); + +const restSavedDatasets = http.get("/api/v1/saved_datasets", () => + HttpResponse.json({ + savedDatasets: savedDatasetsJSON, + pagination: {}, + }), +); + +const restProjects = http.get("/api/v1/projects", () => + HttpResponse.json({ + projects: projectsJSON, + pagination: {}, + }), +); + +const restFeatures = http.get("/api/v1/features", () => + HttpResponse.json({ + features: allFeatures, + pagination: {}, + }), +); -const creditHistoryRegistryDB = http.get("/registry.db", () => { - return HttpResponse.arrayBuffer(registry.buffer); +const restPermissions = http.get("/api/v1/permissions", () => + HttpResponse.json({ + permissions: [], + pagination: {}, + }), +); + +// Detail endpoints +const restFeatureViewDetail = http.get( + "/api/v1/feature_views/:name", + ({ params }) => { + const name = params.name as string; + const fv = allFeatureViewsJSON.find((f: any) => f.spec?.name === name); + if (!fv) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(fv); + }, +); + +const restEntityDetail = http.get("/api/v1/entities/:name", ({ params }) => { + const name = params.name as string; + const entity = entitiesJSON.find((e: any) => e.spec?.name === name); + if (!entity) + return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(entity); }); -export { +const restFeatureServiceDetail = http.get( + "/api/v1/feature_services/:name", + ({ params }) => { + const name = params.name as string; + const fs = featureServicesJSON.find((f: any) => f.spec?.name === name); + if (!fs) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(fs); + }, +); + +const restDataSourceDetail = http.get( + "/api/v1/data_sources/:name", + ({ params }) => { + const name = params.name as string; + const ds = dataSourcesJSON.find((d: any) => d.name === name); + if (!ds) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(ds); + }, +); + +const restFeatureDetail = http.get( + "/api/v1/features/:fvName/:featureName", + ({ params }) => { + const fvName = params.fvName as string; + const featureName = params.featureName as string; + const fv = allFeatureViewsJSON.find((f: any) => f.spec?.name === fvName); + if (!fv) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + const feature = (fv as any).spec?.features?.find( + (f: any) => f.name === featureName, + ); + if (!feature) + return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json({ + featureViewName: fvName, + featureName, + feature, + featureView: fv, + }); + }, +); + +// "all" endpoints (for global search / all-projects view) +const restEntitiesAll = http.get("/api/v1/entities/all", () => + HttpResponse.json({ + entities: entitiesJSON.map((e: any) => ({ + ...e, + project: e.spec?.project, + })), + pagination: {}, + relationships: {}, + }), +); + +const restFeatureViewsAll = http.get("/api/v1/feature_views/all", () => + HttpResponse.json({ + featureViews: allFeatureViewsJSON.map((fv: any) => ({ + ...fv, + project: fv.spec?.project, + })), + pagination: {}, + relationships: {}, + }), +); + +const restFeatureServicesAll = http.get("/api/v1/feature_services/all", () => + HttpResponse.json({ + featureServices: featureServicesJSON.map((fs: any) => ({ + ...fs, + project: fs.spec?.project, + })), + pagination: {}, + relationships: {}, + }), +); + +const restDataSourcesAll = http.get("/api/v1/data_sources/all", () => + HttpResponse.json({ + dataSources: dataSourcesJSON.map((ds: any) => ({ + ...ds, + project: ds.project, + })), + pagination: {}, + relationships: {}, + }), +); + +const restSavedDatasetsAll = http.get("/api/v1/saved_datasets/all", () => + HttpResponse.json({ + savedDatasets: savedDatasetsJSON, + pagination: {}, + }), +); + +const restFeaturesAll = http.get("/api/v1/features/all", () => + HttpResponse.json({ + features: allFeatures, + pagination: {}, + }), +); + +const restSavedDatasetDetail = http.get( + "/api/v1/saved_datasets/:name", + ({ params }) => { + const name = params.name as string; + const sd = savedDatasetsJSON.find((d: any) => d.spec?.name === name); + if (!sd) return HttpResponse.json({ detail: "Not found" }, { status: 404 }); + return HttpResponse.json(sd); + }, +); + +const restLabelViews = http.get("/api/v1/label_views", () => + HttpResponse.json({ + featureViews: [], + pagination: {}, + relationships: {}, + }), +); + +const restLabelViewsAll = http.get("/api/v1/label_views/all", () => + HttpResponse.json({ + featureViews: [], + pagination: {}, + relationships: {}, + }), +); + +const restLabels = http.get("/api/v1/labels", () => + HttpResponse.json({ + labels: [], + pagination: {}, + }), +); + +const restLabelsAll = http.get("/api/v1/labels/all", () => + HttpResponse.json({ + labels: [], + pagination: {}, + }), +); + +const restMetrics = http.get("/api/v1/metrics/:type", () => + HttpResponse.json({}), +); + +const allRestHandlers = [ projectsListWithDefaultProject, - creditHistoryRegistryPB as creditHistoryRegistry, - creditHistoryRegistryDB, -}; + // "all" endpoints must come before parameterized detail routes + restEntitiesAll, + restFeatureViewsAll, + restFeatureServicesAll, + restDataSourcesAll, + restSavedDatasetsAll, + restFeaturesAll, + restLabelViewsAll, + restLabelsAll, + // List endpoints + restEntities, + restFeatureViews, + restFeatureServices, + restDataSources, + restSavedDatasets, + restProjects, + restFeatures, + restLabelViews, + restLabels, + restPermissions, + // Detail endpoints + restFeatureViewDetail, + restEntityDetail, + restFeatureServiceDetail, + restDataSourceDetail, + restSavedDatasetDetail, + restFeatureDetail, + restMetrics, +]; + +export { projectsListWithDefaultProject, allRestHandlers }; diff --git a/ui/src/pages/Layout.tsx b/ui/src/pages/Layout.tsx index 0e3341b8820..a951b9a2649 100644 --- a/ui/src/pages/Layout.tsx +++ b/ui/src/pages/Layout.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect } from "react"; import { + EuiGlobalToastList, EuiPage, EuiPageSidebar, EuiPageBody, @@ -9,6 +10,13 @@ import { EuiSpacer, EuiFlexGroup, EuiFlexItem, + EuiAvatar, + EuiText, + EuiBadge, + EuiToolTip, + EuiPopover, + EuiButtonEmpty, + EuiIcon, } from "@elastic/eui"; import { Outlet } from "react-router-dom"; @@ -26,14 +34,18 @@ import RegistrySearch, { } from "../components/RegistrySearch"; import GlobalSearchShortcut from "../components/GlobalSearchShortcut"; import CommandPalette from "../components/CommandPalette"; +import { useAuth } from "../contexts/AuthContext"; +import { RegistryRefreshContext } from "../contexts/RegistryRefreshContext"; +import useRegistryRefresh from "../hooks/useRegistryRefresh"; const Layout = () => { - // Registry Path Context has to be inside Layout - // because it has to be under routes - // in order to use useParams let { projectName } = useParams(); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); const searchRef = useRef(null); + const { user, logout, isAuthEnabled } = useAuth(); + const { refreshing, toasts, handleRefresh, removeToast } = + useRegistryRefresh(); const { data: projectsData } = useLoadProjectsList(); @@ -54,47 +66,6 @@ const Layout = () => { // Load unfiltered data for global search (across all projects) const { data: globalData } = useLoadRegistry(globalRegistryPath); - // Categories for page-level search (filtered to current project) - const categories = data - ? [ - { - name: "Data Sources", - data: data.objects.dataSources || [], - getLink: (item: any) => `/p/${projectName}/data-source/${item.name}`, - }, - { - name: "Entities", - data: data.objects.entities || [], - getLink: (item: any) => `/p/${projectName}/entity/${item.name}`, - }, - { - name: "Features", - data: data.allFeatures || [], - getLink: (item: any) => { - const featureView = item?.featureView; - return featureView - ? `/p/${projectName}/feature-view/${featureView}/feature/${item.name}` - : "#"; - }, - }, - { - name: "Feature Views", - data: data.mergedFVList || [], - getLink: (item: any) => `/p/${projectName}/feature-view/${item.name}`, - }, - { - name: "Feature Services", - data: data.objects.featureServices || [], - getLink: (item: any) => { - const serviceName = item?.name || item?.spec?.name; - return serviceName - ? `/p/${projectName}/feature-service/${serviceName}` - : "#"; - }, - }, - ] - : []; - // Helper function to extract project ID from an item const getProjectId = (item: any): string => { // Try different possible locations for the project field @@ -151,6 +122,18 @@ const Layout = () => { return `/p/${project}/feature-view/${item.name}`; }, }, + { + name: "Label Views", + data: (globalData.objects.labelViews || []).map((item: any) => ({ + ...item, + projectId: getProjectId(item), + })), + getLink: (item: any) => { + const lvName = item?.name || item?.spec?.name; + const project = item?.projectId || getProjectId(item); + return `/p/${project}/label-view/${lvName}`; + }, + }, { name: "Feature Services", data: (globalData.objects.featureServices || []).map((item: any) => ({ @@ -188,52 +171,52 @@ const Layout = () => { }, []); return ( - - - setIsCommandPaletteOpen(false)} - categories={globalCategories} - /> - - - - - - {registryPath && ( - - - - - + + + + setIsCommandPaletteOpen(false)} + categories={globalCategories} + /> + + + + + + {registryPath && ( + + + + + +
+ +
+
+ )} +
+ + +
- -
-
- )} -
- - - -
- {data && (
{ backgroundColor: "var(--euiPageBackgroundColor)", borderBottom: "1px solid #D3DAE6", boxShadow: "0px 1px 5px rgba(0, 0, 0, 0.05)", - padding: "16px", + padding: "12px 16px", width: "100%", }} > - - - - + + {data && ( + +
+ +
+
+ )} + {!data && } + + {projectName && ( + + + Refresh + + + )} + + {isAuthEnabled && user && ( + + setIsUserMenuOpen((v) => !v)} + style={{ + display: "flex", + alignItems: "center", + gap: 8, + background: "none", + border: "none", + cursor: "pointer", + padding: "4px 8px", + borderRadius: 6, + }} + aria-label="User menu" + > + + + {user.username} + + + + } + isOpen={isUserMenuOpen} + closePopover={() => setIsUserMenuOpen(false)} + anchorPosition="downRight" + panelPaddingSize="m" + > +
+ + + + + + + {user.username} + + {user.email && ( + + {user.email} + + )} + + + + {user.roles.length > 0 && ( + <> + + + Roles + + +
+ {user.roles + .filter( + (r) => + ![ + "default-roles-feast", + "offline_access", + "uma_authorization", + ].includes(r), + ) + .map((role) => ( + + + {role} + + + ))} +
+ + )} + + {user.groups.length > 0 && ( + <> + + + Groups + + +
+ {user.groups.map((group) => ( + + {group} + + ))} +
+ + )} + + + + Sign out + +
+
+
+ )}
- )} -
- +
+ +
-
-
-
-
-
+ + + + + + ); }; diff --git a/ui/src/pages/ProjectOverviewPage.tsx b/ui/src/pages/ProjectOverviewPage.tsx index 839fbcc5d89..017c32d56e7 100644 --- a/ui/src/pages/ProjectOverviewPage.tsx +++ b/ui/src/pages/ProjectOverviewPage.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React from "react"; import { EuiPageTemplate, EuiText, @@ -7,8 +7,6 @@ import { EuiTitle, EuiSpacer, EuiSkeletonText, - EuiEmptyPrompt, - EuiFieldSearch, EuiPanel, EuiStat, EuiCard, @@ -17,54 +15,107 @@ import { import { useDocumentTitle } from "../hooks/useDocumentTitle"; import ObjectsCountStats from "../components/ObjectsCountStats"; import ExplorePanel from "../components/ExplorePanel"; -import useLoadRegistry from "../queries/useLoadRegistry"; -import RegistryPathContext from "../contexts/RegistryPathContext"; -import RegistryVisualizationTab from "../components/RegistryVisualizationTab"; -import RegistrySearch from "../components/RegistrySearch"; +import useResourceQuery, { + restFeatureViewsToMergedList, + restLabelViewsFromResponse, +} from "../queries/useResourceQuery"; import { useParams, useNavigate } from "react-router-dom"; import { useLoadProjectsList } from "../contexts/ProjectListContext"; +import type { genericFVType } from "../parsers/mergedFVTypes"; + +const getItemProject = (item: any): string => + item?.project || item?.spec?.project || ""; // Component for "All Projects" view const AllProjectsDashboard = () => { - const registryUrl = useContext(RegistryPathContext); const navigate = useNavigate(); const { data: projectsData } = useLoadProjectsList(); - const { data: registryData } = useLoadRegistry(registryUrl); - if (!registryData) { + const fvQuery = useResourceQuery({ + resourceType: "all-proj-fvs", + restPath: "/feature_views/all?limit=100&include_relationships=true", + restSelect: restFeatureViewsToMergedList, + }); + + const entQuery = useResourceQuery({ + resourceType: "all-proj-entities", + restPath: "/entities/all?limit=100", + restSelect: (d) => d.entities, + }); + + const dsQuery = useResourceQuery({ + resourceType: "all-proj-ds", + restPath: "/data_sources/all?limit=100", + restSelect: (d) => d.dataSources, + }); + + const fsQuery = useResourceQuery({ + resourceType: "all-proj-fs", + restPath: "/feature_services/all?limit=100", + restSelect: (d) => d.featureServices, + }); + + const featQuery = useResourceQuery({ + resourceType: "all-proj-features", + restPath: "/features/all?limit=100", + restSelect: (d) => d.features, + }); + + const lvQuery = useResourceQuery({ + resourceType: "all-proj-lvs", + restPath: "/label_views/all?limit=100&include_relationships=true", + restSelect: restLabelViewsFromResponse, + }); + + const settled = (q: { isSuccess: boolean; isError: boolean }) => + q.isSuccess || q.isError; + const allSettled = + settled(fvQuery) && + settled(entQuery) && + settled(dsQuery) && + settled(fsQuery) && + settled(featQuery) && + settled(lvQuery); + + if (!allSettled) { return ; } - // Calculate total counts across all projects + const allFVs = fvQuery.data || []; + const allEntities = entQuery.data || []; + const allDS = dsQuery.data || []; + const allFS = fsQuery.data || []; + const allFeatures = featQuery.data || []; + const allLabelViews = lvQuery.data || []; + const totalCounts = { - featureViews: registryData.objects.featureViews?.length || 0, - entities: registryData.objects.entities?.length || 0, - dataSources: registryData.objects.dataSources?.length || 0, - featureServices: registryData.objects.featureServices?.length || 0, - features: registryData.allFeatures?.length || 0, + featureViews: fvQuery.isPermissionDenied ? null : allFVs.length, + entities: entQuery.isPermissionDenied ? null : allEntities.length, + dataSources: dsQuery.isPermissionDenied ? null : allDS.length, + featureServices: fsQuery.isPermissionDenied ? null : allFS.length, + features: featQuery.isPermissionDenied ? null : allFeatures.length, + labelViews: lvQuery.isPermissionDenied ? null : allLabelViews.length, }; - // Get projects from registry and count their objects const projects = projectsData?.projects.filter((p) => p.id !== "all") || []; const projectStats = projects.map((project) => { - const projectFVs = - registryData.objects.featureViews?.filter( - (fv: any) => fv?.spec?.project === project.id, - ) || []; - const projectEntities = - registryData.objects.entities?.filter( - (e: any) => e?.spec?.project === project.id, - ) || []; - const projectFeatures = - registryData.allFeatures?.filter((f: any) => f?.project === project.id) || - []; + const matchesProject = (item: any) => getItemProject(item) === project.id; return { ...project, counts: { - featureViews: projectFVs.length, - entities: projectEntities.length, - features: projectFeatures.length, + featureViews: fvQuery.isPermissionDenied + ? null + : allFVs.filter((fv: any) => matchesProject(fv.object || fv)).length, + entities: entQuery.isPermissionDenied + ? null + : allEntities.filter(matchesProject).length, + features: featQuery.isPermissionDenied + ? null + : allFeatures.filter(matchesProject).length, + labelViews: lvQuery.isPermissionDenied + ? null + : allLabelViews.filter(matchesProject).length, }, }; }); @@ -92,47 +143,76 @@ const AllProjectsDashboard = () => { - - - - - - - - - - - - - - - + {totalCounts.featureViews != null && ( + + + + )} + {totalCounts.entities != null && ( + + + + )} + {totalCounts.features != null && ( + + + + )} + {totalCounts.featureServices != null && ( + + + + )} + {totalCounts.dataSources != null && ( + + + + )} + {totalCounts.labelViews != null && totalCounts.labelViews > 0 && ( + + + + + + + + + + + + + )} @@ -156,33 +236,53 @@ const AllProjectsDashboard = () => { > - - - {project.counts.featureViews} -
- - Feature Views - -
-
- - - {project.counts.entities} -
- - Entities - -
-
- - - {project.counts.features} -
- - Features - -
-
+ {project.counts.featureViews != null && ( + + + {project.counts.featureViews} +
+ + Feature Views + +
+
+ )} + {project.counts.entities != null && ( + + + {project.counts.entities} +
+ + Entities + +
+
+ )} + {project.counts.features != null && ( + + + {project.counts.features} +
+ + Features + +
+
+ )} + {project.counts.labelViews != null && + project.counts.labelViews > 0 && ( + + + {project.counts.labelViews} +
+ + Label Views + +
+
+ )}
@@ -195,112 +295,59 @@ const AllProjectsDashboard = () => { const ProjectOverviewPage = () => { useDocumentTitle("Feast Home"); - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams<{ projectName: string }>(); - const { isLoading, isSuccess, isError, data } = useLoadRegistry( - registryUrl, - projectName, - ); + const { data: projectsData } = useLoadProjectsList(); // Show aggregated dashboard for "All Projects" view if (projectName === "all") { return ; } - const categories = [ - { - name: "Data Sources", - data: data?.objects.dataSources || [], - getLink: (item: any) => `/p/${projectName}/data-source/${item.name}`, - }, - { - name: "Entities", - data: data?.objects.entities || [], - getLink: (item: any) => `/p/${projectName}/entity/${item.name}`, - }, - { - name: "Features", - data: data?.allFeatures || [], - getLink: (item: any) => { - const featureView = item?.featureView; - return featureView - ? `/p/${projectName}/feature-view/${featureView}/feature/${item.name}` - : "#"; - }, - }, - { - name: "Feature Views", - data: data?.mergedFVList || [], - getLink: (item: any) => `/p/${projectName}/feature-view/${item.name}`, - }, - { - name: "Feature Services", - data: data?.objects.featureServices || [], - getLink: (item: any) => { - const serviceName = item?.name || item?.spec?.name; - return serviceName - ? `/p/${projectName}/feature-service/${serviceName}` - : "#"; - }, - }, - ]; + const currentProject = projectsData?.projects.find( + (p) => p.id === projectName, + ); return (

- {isLoading && } - {isSuccess && data?.project && `Project: ${data.project}`} + {currentProject + ? `Project: ${currentProject.name}` + : projectName + ? `Project: ${projectName}` + : ""}

- {isLoading && } - {isError && ( - Error Loading Project Configs} - body={ -

- There was an error loading the Project Configurations. - Please check that feature_store.yaml file is - available and well-formed. -

- } - /> + {currentProject?.description ? ( + +
{currentProject.description}
+
+ ) : ( + +

+ Welcome to your new Feast project. In this UI, you can see + Data Sources, Entities, Features, Feature Views, and Feature + Services registered in Feast. +

+

+ It looks like this project already has some objects + registered. If you are new to this project, we suggest + starting by exploring the Feature Services, as they represent + the collection of Feature Views serving a particular model. +

+

+ Note: We encourage you to replace this + welcome message with more suitable content for your team. You + can do so by specifying a project_description in + your feature_store.yaml file. +

+
)} - {isSuccess && - (data?.description ? ( - -
{data.description}
-
- ) : ( - -

- Welcome to your new Feast project. In this UI, you can see - Data Sources, Entities, Features, Feature Views, and Feature - Services registered in Feast. -

-

- It looks like this project already has some objects - registered. If you are new to this project, we suggest - starting by exploring the Feature Services, as they - represent the collection of Feature Views serving a - particular model. -

-

- Note: We encourage you to replace this - welcome message with more suitable content for your team. - You can do so by specifying a{" "} - project_description in your{" "} - feature_store.yaml file. -

-
- ))}
diff --git a/ui/src/pages/RootProjectSelectionPage.tsx b/ui/src/pages/RootProjectSelectionPage.tsx index fb488e714bc..6740e266f2a 100644 --- a/ui/src/pages/RootProjectSelectionPage.tsx +++ b/ui/src/pages/RootProjectSelectionPage.tsx @@ -1,7 +1,9 @@ import React, { useEffect } from "react"; import { + EuiButtonEmpty, EuiCard, EuiFlexGrid, + EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSkeletonText, @@ -13,10 +15,12 @@ import { import { useLoadProjectsList } from "../contexts/ProjectListContext"; import { useNavigate } from "react-router-dom"; import FeastIconBlue from "../graphics/FeastIconBlue"; +import { useRegistryRefreshContext } from "../contexts/RegistryRefreshContext"; const RootProjectSelectionPage = () => { const { isLoading, isSuccess, data } = useLoadProjectsList(); const navigate = useNavigate(); + const { refreshing, handleRefresh } = useRegistryRefreshContext(); useEffect(() => { if (data && data.default) { @@ -48,12 +52,27 @@ const RootProjectSelectionPage = () => { return ( - -

Welcome to Feast

-
- -

Select one of the projects.

-
+ + + +

Welcome to Feast

+
+ +

Select one of the projects.

+
+
+ + + Refresh + + +
{isLoading && } {isSuccess && data?.projects && ( diff --git a/ui/src/pages/Sidebar.tsx b/ui/src/pages/Sidebar.tsx index 55c8ec805c9..1054b3d8e3c 100644 --- a/ui/src/pages/Sidebar.tsx +++ b/ui/src/pages/Sidebar.tsx @@ -1,10 +1,19 @@ -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; import { EuiIcon, EuiSideNav, htmlIdGenerator } from "@elastic/eui"; import { Link, useParams } from "react-router-dom"; import { useMatchSubpath } from "../hooks/useMatchSubpath"; -import useLoadRegistry from "../queries/useLoadRegistry"; -import RegistryPathContext from "../contexts/RegistryPathContext"; +import useResourceQuery, { + entityListPath, + featureViewListPath, + featureServiceListPath, + dataSourceListPath, + savedDatasetListPath, + featuresListPath, + labelViewListPath, + restFeatureViewsToMergedList, + restLabelViewsFromResponse, +} from "../queries/useResourceQuery"; import { DataSourceIcon } from "../graphics/DataSourceIcon"; import { EntityIcon } from "../graphics/EntityIcon"; @@ -14,11 +23,67 @@ import { DatasetIcon } from "../graphics/DatasetIcon"; import { FeatureIcon } from "../graphics/FeatureIcon"; import { HomeIcon } from "../graphics/HomeIcon"; import { PermissionsIcon } from "../graphics/PermissionsIcon"; +import { LabelViewIcon } from "../graphics/LabelViewIcon"; +import { ComputeEngineIcon } from "../graphics/ComputeEngineIcon"; +import type { genericFVType } from "../parsers/mergedFVTypes"; const SideNav = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const { isSuccess, data } = useLoadRegistry(registryUrl, projectName); + + const { isSuccess: dsSuccess, data: dataSources } = useResourceQuery({ + resourceType: "sidebar-ds", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources, + }); + + const { isSuccess: entSuccess, data: entities } = useResourceQuery({ + resourceType: "sidebar-entities", + project: projectName, + restPath: entityListPath(projectName), + restSelect: (d) => d.entities, + }); + + const { isSuccess: fvSuccess, data: featureViews } = useResourceQuery< + genericFVType[] + >({ + resourceType: "sidebar-fvs", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: restFeatureViewsToMergedList, + }); + + const { isSuccess: featSuccess, data: features } = useResourceQuery({ + resourceType: "sidebar-features", + project: projectName, + restPath: featuresListPath(projectName), + restSelect: (d) => d.features, + }); + + const { isSuccess: fsSuccess, data: featureServices } = useResourceQuery< + any[] + >({ + resourceType: "sidebar-fs", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices, + }); + + const { isSuccess: sdSuccess, data: savedDatasets } = useResourceQuery( + { + resourceType: "sidebar-sd", + project: projectName, + restPath: savedDatasetListPath(projectName), + restSelect: (d) => d.savedDatasets, + }, + ); + + const { isSuccess: lvSuccess, data: labelViews } = useResourceQuery({ + resourceType: "sidebar-lvs", + project: projectName, + restPath: labelViewListPath(projectName), + restSelect: restLabelViewsFromResponse, + }); const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); @@ -26,43 +91,16 @@ const SideNav = () => { setisSideNavOpenOnMobile(!isSideNavOpenOnMobile); }; - const dataSourcesLabel = `Data Sources ${ - isSuccess && data?.objects.dataSources - ? `(${data?.objects.dataSources?.length})` - : "" - }`; - - const entitiesLabel = `Entities ${ - isSuccess && data?.objects.entities - ? `(${data?.objects.entities?.length})` - : "" - }`; - - const featureViewsLabel = `Feature Views ${ - isSuccess && data?.mergedFVList && data?.mergedFVList.length > 0 - ? `(${data?.mergedFVList.length})` - : "" - }`; - - const featureListLabel = `Features ${ - isSuccess && data?.allFeatures && data?.allFeatures.length > 0 - ? `(${data?.allFeatures.length})` - : "" - }`; - - const featureServicesLabel = `Feature Services ${ - isSuccess && data?.objects.featureServices - ? `(${data?.objects.featureServices?.length})` - : "" - }`; - - const savedDatasetsLabel = `Datasets ${ - isSuccess && data?.objects.savedDatasets - ? `(${data?.objects.savedDatasets?.length})` - : "" - }`; + const dataSourcesLabel = `Data Sources ${dsSuccess && dataSources ? `(${dataSources.length})` : ""}`; + const entitiesLabel = `Entities ${entSuccess && entities ? `(${entities.length})` : ""}`; + const featureViewsLabel = `Feature Views ${fvSuccess && featureViews && featureViews.length > 0 ? `(${featureViews.length})` : ""}`; + const featureListLabel = `Features ${featSuccess && features && features.length > 0 ? `(${features.length})` : ""}`; + const featureServicesLabel = `Feature Services ${fsSuccess && featureServices ? `(${featureServices.length})` : ""}`; + const savedDatasetsLabel = `Datasets ${sdSuccess && savedDatasets ? `(${savedDatasets.length})` : ""}`; + const labelViewsLabel = `Label Views ${lvSuccess && labelViews && labelViews.length > 0 ? `(${labelViews.length})` : ""}`; const baseUrl = `/p/${projectName}`; + const monitoringSelected = useMatchSubpath(`${baseUrl}/monitoring`); const sideNav: React.ComponentProps["items"] = [ { @@ -124,6 +162,15 @@ const SideNav = () => { ), isSelected: useMatchSubpath(`${baseUrl}/feature-service`), }, + { + name: labelViewsLabel, + id: htmlIdGenerator("labelViews")(), + icon: , + renderItem: (props) => ( + + ), + isSelected: useMatchSubpath(`${baseUrl}/label-view`), + }, { name: savedDatasetsLabel, id: htmlIdGenerator("savedDatasets")(), @@ -131,15 +178,6 @@ const SideNav = () => { renderItem: (props) => , isSelected: useMatchSubpath(`${baseUrl}/data-set`), }, - { - name: "Data Labeling", - id: htmlIdGenerator("dataLabeling")(), - icon: , - renderItem: (props) => ( - - ), - isSelected: useMatchSubpath(`${baseUrl}/data-labeling`), - }, { name: "Permissions", id: htmlIdGenerator("permissions")(), @@ -149,6 +187,24 @@ const SideNav = () => { ), isSelected: useMatchSubpath(`${baseUrl}/permissions`), }, + { + name: "Monitoring", + id: htmlIdGenerator("monitoring")(), + icon: , + renderItem: (props: any) => ( + + ), + isSelected: monitoringSelected, + }, + { + name: "Compute & Jobs", + id: htmlIdGenerator("computeEngine")(), + icon: , + renderItem: (props: any) => ( + + ), + isSelected: useMatchSubpath(`${baseUrl}/compute-engine`), + }, ], }, ]; diff --git a/ui/src/pages/compute-engines/Index.tsx b/ui/src/pages/compute-engines/Index.tsx new file mode 100644 index 00000000000..f03d4e2269f --- /dev/null +++ b/ui/src/pages/compute-engines/Index.tsx @@ -0,0 +1,548 @@ +import React, { useMemo, useState } from "react"; +import { Route, Routes, useNavigate, useParams } from "react-router-dom"; + +import { + EuiPageTemplate, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiSpacer, + EuiPanel, + EuiHorizontalRule, + EuiTitle, + EuiText, + EuiBadge, + EuiBasicTable, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiHealth, + EuiFilterGroup, + EuiFilterButton, + EuiCallOut, +} from "@elastic/eui"; + +import { ComputeEngineIcon } from "../../graphics/ComputeEngineIcon"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import { + useLoadComputeEngine, + FeatureViewEngineInfo, +} from "../../queries/useLoadComputeEngine"; +import EuiCustomLink from "../../components/EuiCustomLink"; + +interface MaterializationJobRow { + id: string; + featureView: string; + status: "SUCCEEDED" | "RUNNING" | "ERROR" | "WAITING"; + rangeStart: string; + rangeEnd: string; + sortKey: number; +} + +function formatRange(iso: string | undefined): string { + if (!iso) return "—"; + try { + return new Date(iso).toLocaleString(); + } catch { + return iso; + } +} + +function buildJobsFromFeatureViews( + featureViewInfos: FeatureViewEngineInfo[], +): MaterializationJobRow[] { + const jobs: MaterializationJobRow[] = []; + + featureViewInfos.forEach((fv) => { + if (fv.materializationIntervals && fv.materializationIntervals.length > 0) { + fv.materializationIntervals.forEach((interval, idx) => { + const startTime = + (interval as any).startTime || (interval as any).start_time; + const endTime = (interval as any).endTime || (interval as any).end_time; + + const sortMs = endTime + ? new Date(endTime).getTime() + : startTime + ? new Date(startTime).getTime() + : 0; + + jobs.push({ + id: `${fv.name}-${idx}`, + featureView: fv.name, + status: "SUCCEEDED", + rangeStart: formatRange(startTime), + rangeEnd: formatRange(endTime), + sortKey: sortMs, + }); + }); + } + }); + + return jobs.sort((a, b) => { + try { + return b.sortKey - a.sortKey; + } catch { + return 0; + } + }); +} + +const statusColorMap: Record = { + SUCCEEDED: "success", + RUNNING: "primary", + ERROR: "danger", + WAITING: "subdued", +}; + +const OverviewTab = ({ + engineInfo, + featureViewInfos, + projectName, +}: { + engineInfo: any; + featureViewInfos: FeatureViewEngineInfo[]; + projectName: string | undefined; +}) => { + const materializedCount = featureViewInfos.filter( + (fv) => fv.lastMaterialized, + ).length; + const overrideCount = featureViewInfos.filter((fv) => fv.hasOverride).length; + + const fvColumns = [ + { + name: "Feature View", + field: "name", + sortable: true, + render: (name: string) => ( + + {name} + + ), + }, + { + name: "Type", + field: "type", + sortable: true, + render: (type: string) => {type}, + }, + { + name: "Online", + field: "online", + render: (online: boolean) => ( + + {online ? "Yes" : "No"} + + ), + }, + { + name: "Last Materialized", + field: "lastMaterialized", + render: (val: string | undefined) => { + if (!val) + return ( + + Never + + ); + try { + return new Date(val).toLocaleString(); + } catch { + return val; + } + }, + }, + { + name: "Engine Override", + field: "hasOverride", + render: (hasOverride: boolean, item: any) => { + if (!hasOverride) { + return ( + + None + + ); + } + const keys = item.overrides ? Object.keys(item.overrides) : []; + return ( + + {keys.length > 0 ? keys.join(", ") : "Custom"} + + ); + }, + }, + ]; + + return ( + + + + + + + + + + + + + + + + + + + + +

Engine Configuration

+
+ + + Type + + + {engineInfo?.engineType || "local"} + + + + Class + + {engineInfo?.engineClass || "LocalComputeEngine"} + + + + {engineInfo?.config && + Object.entries(engineInfo.config).filter( + ([key, value]) => key !== "type" && value != null && value !== "", + ).length > 0 && ( + <> + + +

Parameters

+
+ + + {Object.entries(engineInfo.config) + .filter( + ([key, value]) => + key !== "type" && value != null && value !== "", + ) + .map(([key, value]) => ( + + {key} + + {typeof value === "object" + ? JSON.stringify(value, null, 2) + : String(value)} + + + ))} + + + )} +
+ + + + +

Feature Views Using This Engine

+
+ + {featureViewInfos.length > 0 ? ( + ({ + "data-test-subj": `row-${item.name}`, + })} + /> + ) : ( + + No feature views found in this project. + + )} +
+ ); +}; + +const JobsTab = ({ + engineInfo, + featureViewInfos, + projectName, +}: { + engineInfo: any; + featureViewInfos: FeatureViewEngineInfo[]; + projectName: string | undefined; +}) => { + const [statusFilter, setStatusFilter] = useState(null); + + const jobs = useMemo( + () => buildJobsFromFeatureViews(featureViewInfos), + [featureViewInfos], + ); + + const filteredJobs = statusFilter + ? jobs.filter((j) => j.status === statusFilter) + : jobs; + + const succeededCount = jobs.filter((j) => j.status === "SUCCEEDED").length; + const failedCount = jobs.filter((j) => j.status === "ERROR").length; + const runningCount = jobs.filter((j) => j.status === "RUNNING").length; + + const columns = [ + { + name: "Job ID", + field: "id", + sortable: true, + render: (id: string) => ( + + {id} + + ), + }, + { + name: "Feature View", + field: "featureView", + sortable: true, + render: (name: string) => ( + + {name} + + ), + }, + { + name: "Engine", + field: "id", + render: () => ( + {engineInfo?.engineType || "local"} + ), + }, + { + name: "Status", + field: "status", + sortable: true, + render: (status: string) => ( + + {status} + + ), + }, + { + name: "Range Start", + field: "rangeStart", + sortable: true, + }, + { + name: "Range End", + field: "rangeEnd", + }, + ]; + + return ( + + + + + + + + + + + + + + + + + + + + + +

Filter by Status

+
+
+ + + setStatusFilter(null)} + > + All + + + setStatusFilter( + statusFilter === "SUCCEEDED" ? null : "SUCCEEDED", + ) + } + numFilters={succeededCount} + > + Succeeded + + + setStatusFilter(statusFilter === "RUNNING" ? null : "RUNNING") + } + numFilters={runningCount} + > + Running + + + setStatusFilter(statusFilter === "ERROR" ? null : "ERROR") + } + numFilters={failedCount} + > + Failed + + + +
+ + + + {filteredJobs.length > 0 ? ( + ({ + "data-test-subj": `row-${item.id}`, + })} + /> + ) : ( + + {jobs.length === 0 + ? "No materialization jobs found. Run 'feast materialize' to create jobs." + : "No jobs match the selected filter."} + + )} +
+ ); +}; + +const Index = () => { + const { projectName } = useParams(); + const navigate = useNavigate(); + + const { + isLoading, + isSuccess, + isError, + isPermissionDenied, + engineInfo, + featureViewInfos, + } = useLoadComputeEngine(projectName); + + useDocumentTitle(`Compute & Jobs | Feast`); + + return ( + + navigate(""), + }, + { + label: "Jobs", + isSelected: useMatchSubpath("jobs"), + onClick: () => navigate("jobs"), + }, + ]} + /> + + {isLoading && ( +

+ Loading +

+ )} + {isPermissionDenied && ( + +

You do not have permission to view compute engines.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} + {isSuccess && ( + + + } + /> + + } + /> + + )} +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/data-sources/DataSourceCatalog.tsx b/ui/src/pages/data-sources/DataSourceCatalog.tsx new file mode 100644 index 00000000000..61653b6cc0e --- /dev/null +++ b/ui/src/pages/data-sources/DataSourceCatalog.tsx @@ -0,0 +1,379 @@ +import React, { useState } from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiText, + EuiTitle, + EuiSpacer, + EuiButton, + EuiBadge, +} from "@elastic/eui"; +import { feast } from "../../protos"; +import { + BigQueryIcon, + SnowflakeIcon, + RedshiftIcon, + KafkaIcon, + SparkIcon, + FileIcon, + RequestSourceIcon, + PushSourceIcon, + KinesisIcon, + TrinoIcon, + AthenaIcon, + CustomSourceIcon, + RayIcon, + PostgresIcon, + MongoDBIcon, + SqlServerIcon, + OracleIcon, + CouchbaseIcon, + ClickHouseIcon, +} from "../../graphics/data-source-icons"; + +interface DataSourceTypeInfo { + id: string; + sourceType: string; + name: string; + description: string; + icon: React.FC>; + category: "batch" | "stream" | "on-demand"; + color: string; + contrib?: boolean; +} + +const DATA_SOURCE_TYPES: DataSourceTypeInfo[] = [ + { + id: "bigquery", + sourceType: String(feast.core.DataSource.SourceType.BATCH_BIGQUERY), + name: "BigQuery", + description: + "Google Cloud's serverless data warehouse. Ideal for large-scale analytics and ML feature computation.", + icon: BigQueryIcon, + category: "batch", + color: "#4285F4", + }, + { + id: "snowflake", + sourceType: String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE), + name: "Snowflake", + description: + "Cloud-native data platform with elastic scaling. Connect to your Snowflake tables for feature engineering.", + icon: SnowflakeIcon, + category: "batch", + color: "#29B5E8", + }, + { + id: "redshift", + sourceType: String(feast.core.DataSource.SourceType.BATCH_REDSHIFT), + name: "Redshift", + description: + "AWS fully managed data warehouse. Pull features from your Redshift clusters with fast parallel queries.", + icon: RedshiftIcon, + category: "batch", + color: "#205B97", + }, + { + id: "spark", + sourceType: String(feast.core.DataSource.SourceType.BATCH_SPARK), + name: "Spark", + description: + "Apache Spark data source for distributed processing. Access tables via Spark catalog or direct file paths.", + icon: SparkIcon, + category: "batch", + color: "#E25A1C", + }, + { + id: "file", + sourceType: String(feast.core.DataSource.SourceType.BATCH_FILE), + name: "File (Parquet / CSV)", + description: + "Read features from Parquet or CSV files stored in S3, GCS, HDFS, or local filesystem.", + icon: FileIcon, + category: "batch", + color: "#4CAF50", + }, + { + id: "trino", + sourceType: String(feast.core.DataSource.SourceType.BATCH_TRINO), + name: "Trino", + description: + "Distributed SQL query engine for big data analytics. Query data across heterogeneous sources via Trino catalog.", + icon: TrinoIcon, + category: "batch", + color: "#DD00A1", + }, + { + id: "athena", + sourceType: String(feast.core.DataSource.SourceType.BATCH_ATHENA), + name: "AWS Athena", + description: + "Serverless interactive query service on AWS. Run SQL queries directly against data in S3 without infrastructure.", + icon: AthenaIcon, + category: "batch", + color: "#8C4FFF", + }, + { + id: "iceberg", + sourceType: String(feast.core.DataSource.SourceType.BATCH_ICEBERG), + name: "Iceberg / Unity Catalog", + description: + "Apache Iceberg REST Catalog source. Connect to Unity Catalog or any Iceberg REST-compatible catalog for governed feature data.", + icon: CustomSourceIcon, + category: "batch", + color: "#3B82F6", + }, + { + id: "kafka", + sourceType: String(feast.core.DataSource.SourceType.STREAM_KAFKA), + name: "Kafka", + description: + "Real-time event streaming platform. Ingest features from Kafka topics for low-latency serving.", + icon: KafkaIcon, + category: "stream", + color: "#231F20", + }, + { + id: "kinesis", + sourceType: String(feast.core.DataSource.SourceType.STREAM_KINESIS), + name: "AWS Kinesis", + description: + "Managed real-time data streaming on AWS. Capture and process streaming data at scale for real-time features.", + icon: KinesisIcon, + category: "stream", + color: "#FF9900", + }, + { + id: "request-source", + sourceType: String(feast.core.DataSource.SourceType.REQUEST_SOURCE), + name: "Request Source", + description: + "Features provided at request time by the caller. No external storage needed — values come from the client.", + icon: RequestSourceIcon, + category: "on-demand", + color: "#7B61FF", + }, + { + id: "push-source", + sourceType: String(feast.core.DataSource.SourceType.PUSH_SOURCE), + name: "Push Source", + description: + "Push-based ingestion source. Clients push feature values directly to the online/offline store.", + icon: PushSourceIcon, + category: "on-demand", + color: "#FF6B35", + }, + { + id: "ray", + sourceType: "RAY_SOURCE", + name: "Ray", + description: + "Multi-format data source powered by Ray. Read images, HuggingFace datasets, Parquet, CSV, MongoDB, and more via Ray Data.", + icon: RayIcon, + category: "batch", + color: "#00A2E8", + contrib: true, + }, + { + id: "postgres", + sourceType: "POSTGRES_SOURCE", + name: "PostgreSQL", + description: + "Open-source relational database. Query feature data from PostgreSQL tables with full SQL support.", + icon: PostgresIcon, + category: "batch", + color: "#336791", + contrib: true, + }, + { + id: "mongodb", + sourceType: "MONGODB_SOURCE", + name: "MongoDB", + description: + "Document-oriented NoSQL database. Access feature data stored in MongoDB collections.", + icon: MongoDBIcon, + category: "batch", + color: "#00684A", + contrib: true, + }, + { + id: "clickhouse", + sourceType: "CLICKHOUSE_SOURCE", + name: "ClickHouse", + description: + "Column-oriented OLAP database for real-time analytics. High-performance queries for feature retrieval.", + icon: ClickHouseIcon, + category: "batch", + color: "#FFCC00", + contrib: true, + }, + { + id: "mssql", + sourceType: "MSSQL_SOURCE", + name: "SQL Server", + description: + "Microsoft SQL Server data source. Connect to MSSQL databases for enterprise feature data.", + icon: SqlServerIcon, + category: "batch", + color: "#CC2927", + contrib: true, + }, + { + id: "oracle", + sourceType: "ORACLE_SOURCE", + name: "Oracle", + description: + "Oracle Database data source. Pull features from Oracle tables and views for enterprise workloads.", + icon: OracleIcon, + category: "batch", + color: "#F80000", + contrib: true, + }, + { + id: "couchbase", + sourceType: "COUCHBASE_SOURCE", + name: "Couchbase", + description: + "Couchbase Columnar analytics source. Run SQL++ queries across distributed data in Couchbase.", + icon: CouchbaseIcon, + category: "batch", + color: "#EA2328", + contrib: true, + }, + { + id: "custom-source", + sourceType: String(feast.core.DataSource.SourceType.CUSTOM_SOURCE), + name: "Custom Source", + description: + "Plugin-based data source for custom integrations. Extend Feast with your own data source implementation.", + icon: CustomSourceIcon, + category: "batch", + color: "#607D8B", + }, +]; + +const CATEGORY_LABELS: Record = { + batch: { label: "Batch", color: "primary" }, + stream: { label: "Streaming", color: "accent" }, + "on-demand": { label: "On-Demand", color: "warning" }, +}; + +interface DataSourceCatalogProps { + onSelectType: (sourceType: string) => void; +} + +const SourceCard: React.FC<{ + dsType: DataSourceTypeInfo; + isHovered: boolean; + onHover: (id: string | null) => void; + onSelect: (sourceType: string) => void; +}> = ({ dsType, isHovered, onHover, onSelect }) => { + const categoryInfo = CATEGORY_LABELS[dsType.category]; + + return ( + + onHover(dsType.id)} + onMouseLeave={() => onHover(null)} + style={{ + height: "100%", + display: "flex", + flexDirection: "column", + transition: "all 0.2s ease", + transform: isHovered ? "translateY(-2px)" : "none", + borderTop: `3px solid ${dsType.color}`, + cursor: "pointer", + }} + onClick={() => onSelect(dsType.sourceType)} + > + + +
+ +
+
+ + +

{dsType.name}

+
+
+ + + {categoryInfo.label} + + +
+ + + + +

{dsType.description}

+
+ + + + { + e.stopPropagation(); + onSelect(dsType.sourceType); + }} + iconType="plusInCircle" + size="s" + > + Create Connection + +
+
+ ); +}; + +const DataSourceCatalog: React.FC = ({ + onSelectType, +}) => { + const [hoveredId, setHoveredId] = useState(null); + + return ( +
+ +

+ Choose a data source type to create a new connection. Each type has + its own configuration tailored to the underlying storage system. +

+
+ + + + {DATA_SOURCE_TYPES.map((dsType) => ( + + ))} + +
+ ); +}; + +export default DataSourceCatalog; +export { DATA_SOURCE_TYPES }; +export type { DataSourceTypeInfo }; diff --git a/ui/src/pages/data-sources/DataSourceInstance.tsx b/ui/src/pages/data-sources/DataSourceInstance.tsx index 1ed2cfa5eab..574da82114e 100644 --- a/ui/src/pages/data-sources/DataSourceInstance.tsx +++ b/ui/src/pages/data-sources/DataSourceInstance.tsx @@ -1,21 +1,343 @@ -import React from "react"; +import React, { useState } from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, +} from "@elastic/eui"; import { DataSourceIcon } from "../../graphics/DataSourceIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import DataSourceRawData from "./DataSourceRawData"; import DataSourceOverviewTab from "./DataSourceOverviewTab"; +import DataSourceFormModal, { + DataSourceFormData, +} from "../../components/DataSourceFormModal"; +import { + useApplyDataSource, + useDeleteDataSource, +} from "../../queries/mutations/useDataSourceMutations"; +import useLoadDataSource from "./useLoadDataSource"; +import { feast } from "../../protos"; import { useDataSourceCustomTabs, useDataSourceCustomTabRoutes, } from "../../custom-tabs/TabsRegistryContext"; +const buildEditFormData = (ds: any): DataSourceFormData => { + const spec = ds.spec || ds; + const tags = spec.tags + ? Object.entries(spec.tags).map(([key, value]) => ({ + key, + value: value as string, + })) + : []; + + return { + name: spec.name || ds.name || "", + description: spec.description || ds.description || "", + owner: spec.owner || ds.owner || "", + sourceType: String(spec.type ?? ds.type ?? 0), + timestampField: spec.timestampField || ds.timestampField || "", + createdTimestampColumn: + spec.createdTimestampColumn || ds.createdTimestampColumn || "", + tags, + // File + fileUri: spec.fileOptions?.uri || ds.fileOptions?.uri || "", + fileFormat: + spec.fileOptions?.fileFormat || ds.fileOptions?.fileFormat || "parquet", + fileS3EndpointOverride: + spec.fileOptions?.s3EndpointOverride || + ds.fileOptions?.s3EndpointOverride || + "", + // BigQuery + bigqueryTable: + spec.bigqueryOptions?.table || ds.bigqueryOptions?.table || "", + bigqueryQuery: + spec.bigqueryOptions?.query || ds.bigqueryOptions?.query || "", + bigqueryDatePartitionColumn: + spec.datePartitionColumn || ds.datePartitionColumn || "", + // Snowflake + snowflakeTable: + spec.snowflakeOptions?.table || ds.snowflakeOptions?.table || "", + snowflakeDatabase: + spec.snowflakeOptions?.database || ds.snowflakeOptions?.database || "", + snowflakeSchema: + spec.snowflakeOptions?.schema || ds.snowflakeOptions?.schema || "", + snowflakeQuery: + spec.snowflakeOptions?.query || ds.snowflakeOptions?.query || "", + snowflakeWarehouse: + spec.snowflakeOptions?.warehouse || ds.snowflakeOptions?.warehouse || "", + // Redshift + redshiftTable: + spec.redshiftOptions?.table || ds.redshiftOptions?.table || "", + redshiftDatabase: + spec.redshiftOptions?.database || ds.redshiftOptions?.database || "", + redshiftSchema: + spec.redshiftOptions?.schema || ds.redshiftOptions?.schema || "", + redshiftQuery: + spec.redshiftOptions?.query || ds.redshiftOptions?.query || "", + // Kafka + kafkaBootstrapServers: + spec.kafkaOptions?.kafkaBootstrapServers || + ds.kafkaOptions?.kafkaBootstrapServers || + "", + kafkaTopic: spec.kafkaOptions?.topic || ds.kafkaOptions?.topic || "", + kafkaMessageFormat: + spec.kafkaOptions?.messageFormat || + ds.kafkaOptions?.messageFormat || + "json", + kafkaWatermarkDelay: + spec.kafkaOptions?.watermarkDelayThreshold || + ds.kafkaOptions?.watermarkDelayThreshold || + "", + // Spark + sparkTable: spec.sparkOptions?.table || ds.sparkOptions?.table || "", + sparkPath: spec.sparkOptions?.path || ds.sparkOptions?.path || "", + sparkQuery: spec.sparkOptions?.query || ds.sparkOptions?.query || "", + sparkFileFormat: + spec.sparkOptions?.fileFormat || ds.sparkOptions?.fileFormat || "", + sparkTableFormat: + spec.sparkOptions?.tableFormat?.formatType || + ds.sparkOptions?.tableFormat?.formatType || + "", + sparkTableFormatCatalog: + spec.sparkOptions?.tableFormat?.catalog || + ds.sparkOptions?.tableFormat?.catalog || + "", + sparkTableFormatNamespace: + spec.sparkOptions?.tableFormat?.namespace || + ds.sparkOptions?.tableFormat?.namespace || + "", + sparkTableFormatProperties: (() => { + const props = + spec.sparkOptions?.tableFormat?.properties || + ds.sparkOptions?.tableFormat?.properties; + return props ? JSON.stringify(props) : ""; + })(), + sparkDatePartitionColumn: + spec.sparkOptions?.datePartitionColumn || + ds.sparkOptions?.datePartitionColumn || + spec.datePartitionColumn || + ds.datePartitionColumn || + "", + sparkDatePartitionFormat: + spec.sparkOptions?.datePartitionColumnFormat || + ds.sparkOptions?.datePartitionColumnFormat || + "%Y-%m-%d", + // Kinesis + kinesisRegion: + spec.kinesisOptions?.region || ds.kinesisOptions?.region || "", + kinesisStreamName: + spec.kinesisOptions?.streamName || ds.kinesisOptions?.streamName || "", + kinesisRecordFormat: + spec.kinesisOptions?.recordFormat || + ds.kinesisOptions?.recordFormat || + "json", + // Trino + trinoTable: spec.trinoOptions?.table || ds.trinoOptions?.table || "", + trinoQuery: spec.trinoOptions?.query || ds.trinoOptions?.query || "", + // Athena + athenaTable: spec.athenaOptions?.table || ds.athenaOptions?.table || "", + athenaQuery: spec.athenaOptions?.query || ds.athenaOptions?.query || "", + athenaDatabase: + spec.athenaOptions?.database || ds.athenaOptions?.database || "", + athenaDataSource: + spec.athenaOptions?.dataSource || ds.athenaOptions?.dataSource || "", + athenaDatePartitionColumn: + spec.datePartitionColumn || ds.datePartitionColumn || "", + // Custom + customSourceClassName: + spec.customOptions?.className || ds.customOptions?.className || "", + customSourceConfig: + spec.customOptions?.config || ds.customOptions?.config || "", + // Iceberg + ...(() => { + const configStr = + spec.customOptions?.configuration || + ds.customOptions?.configuration || + ""; + if ( + String(spec.type ?? ds.type ?? 0) === + String(feast.core.DataSource.SourceType.BATCH_ICEBERG) && + configStr + ) { + try { + const cfg = JSON.parse(configStr); + return { + icebergCatalogType: cfg.catalog_type || "rest", + icebergEndpoint: cfg.endpoint || "", + icebergWarehouse: cfg.warehouse || "", + icebergNamespace: cfg.namespace || "", + icebergTable: cfg.table || "", + icebergTokenEnvVar: cfg.token_env_var || "", + icebergCredentialVending: String(cfg.credential_vending ?? true), + icebergCatalogProperties: cfg.catalog_properties + ? JSON.stringify(cfg.catalog_properties) + : "", + }; + } catch { + /* ignore parse errors */ + } + } + return { + icebergCatalogType: "rest", + icebergEndpoint: "", + icebergWarehouse: "", + icebergNamespace: "", + icebergTable: "", + icebergTokenEnvVar: "", + icebergCredentialVending: "true", + icebergCatalogProperties: "", + }; + })(), + // Ray + rayReaderType: "", + rayPath: "", + rayReaderOptions: "", + // Postgres + postgresTable: "", + postgresQuery: "", + // MongoDB + mongodbCollection: "", + // ClickHouse + clickhouseTable: "", + clickhouseQuery: "", + // MSSQL + mssqlTable: "", + mssqlConnectionStr: "", + mssqlDatePartitionColumn: "", + // Oracle + oracleTable: "", + oracleConnectionStr: "", + oracleDatePartitionColumn: "", + // Couchbase + couchbaseDatabase: "", + couchbaseScope: "", + couchbaseCollection: "", + couchbaseQuery: "", + }; +}; + +const formDataToPayload = (formData: DataSourceFormData, project: string) => { + const payload: Record = { + name: formData.name, + project, + type: parseInt(formData.sourceType, 10), + timestamp_field: formData.timestampField, + created_timestamp_column: formData.createdTimestampColumn, + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + }; + + const st = formData.sourceType; + if (st === String(feast.core.DataSource.SourceType.BATCH_FILE)) { + payload.file_options = { + uri: formData.fileUri, + file_format: formData.fileFormat || "parquet", + s3_endpoint_override: formData.fileS3EndpointOverride || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_BIGQUERY)) { + payload.bigquery_options = { + table: formData.bigqueryTable, + query: formData.bigqueryQuery, + }; + if (formData.bigqueryDatePartitionColumn) { + payload.date_partition_column = formData.bigqueryDatePartitionColumn; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE)) { + payload.snowflake_options = { + table: formData.snowflakeTable, + database: formData.snowflakeDatabase, + schema_: formData.snowflakeSchema, + query: formData.snowflakeQuery || "", + warehouse: formData.snowflakeWarehouse || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_REDSHIFT)) { + payload.redshift_options = { + table: formData.redshiftTable, + database: formData.redshiftDatabase, + schema_: formData.redshiftSchema, + query: formData.redshiftQuery || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KAFKA)) { + payload.kafka_options = { + kafka_bootstrap_servers: formData.kafkaBootstrapServers, + topic: formData.kafkaTopic, + message_format: formData.kafkaMessageFormat || "json", + watermark_delay_threshold: formData.kafkaWatermarkDelay || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SPARK)) { + payload.spark_options = { + table: formData.sparkTable, + path: formData.sparkPath, + query: formData.sparkQuery || "", + file_format: formData.sparkFileFormat || "", + table_format: formData.sparkTableFormat || "", + table_format_catalog: formData.sparkTableFormatCatalog || "", + table_format_namespace: formData.sparkTableFormatNamespace || "", + table_format_properties: formData.sparkTableFormatProperties || "", + date_partition_column: formData.sparkDatePartitionColumn || "", + date_partition_column_format: formData.sparkDatePartitionFormat || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_TRINO)) { + payload.trino_options = { + table: formData.trinoTable, + query: formData.trinoQuery, + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ATHENA)) { + payload.athena_options = { + table: formData.athenaTable, + query: formData.athenaQuery, + database: formData.athenaDatabase, + data_source: formData.athenaDataSource, + }; + if (formData.athenaDatePartitionColumn) { + payload.date_partition_column = formData.athenaDatePartitionColumn; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ICEBERG)) { + const catalogProps = formData.icebergCatalogProperties.trim() + ? JSON.parse(formData.icebergCatalogProperties) + : {}; + payload.custom_options = { + configuration: JSON.stringify({ + catalog_type: formData.icebergCatalogType || "rest", + endpoint: formData.icebergEndpoint, + warehouse: formData.icebergWarehouse, + namespace: formData.icebergNamespace, + table: formData.icebergTable, + token_env_var: formData.icebergTokenEnvVar || null, + credential_vending: formData.icebergCredentialVending !== "false", + catalog_properties: catalogProps, + }), + }; + payload.data_source_class_type = + "feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source.IcebergSource"; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KINESIS)) { + payload.kinesis_options = { + region: formData.kinesisRegion, + stream_name: formData.kinesisStreamName, + record_format: formData.kinesisRecordFormat || "json", + }; + } else if (st === String(feast.core.DataSource.SourceType.CUSTOM_SOURCE)) { + payload.custom_options = { + class_name: formData.customSourceClassName, + config: formData.customSourceConfig, + }; + } + + return payload; +}; + const DataSourceInstance = () => { const navigate = useNavigate(); - let { dataSourceName } = useParams(); + let { dataSourceName, projectName } = useParams(); useDocumentTitle(`${dataSourceName} | Data Source | Feast`); @@ -34,12 +356,66 @@ const DataSourceInstance = () => { const CustomTabRoutes = useDataSourceCustomTabRoutes(); + const { data } = useLoadDataSource(dataSourceName || ""); + const applyDataSource = useApplyDataSource(); + const deleteDataSource = useDeleteDataSource(); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editError, setEditError] = useState(null); + + const handleDelete = () => { + deleteDataSource.mutate( + { name: dataSourceName || "", project: projectName || "" }, + { + onSuccess: () => { + navigate(`/p/${projectName}/data-source`); + }, + }, + ); + }; + + const handleEditSubmit = (formData: DataSourceFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyDataSource.mutate(payload as any, { + onSuccess: () => { + setIsEditModalOpen(false); + setEditError(null); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setEditError(message); + }, + }); + }; + return ( { + setEditError(null); + setIsEditModalOpen(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={tabs} /> @@ -49,6 +425,37 @@ const DataSourceInstance = () => { {CustomTabRoutes} + + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteDataSource.isLoading} + > +

+ This will permanently remove the data source. This action cannot be + undone. +

+
+ )} + + {isEditModalOpen && data && ( + { + setIsEditModalOpen(false); + setEditError(null); + }} + onSubmit={handleEditSubmit} + initialData={buildEditFormData(data)} + isEdit + isSubmitting={applyDataSource.isLoading} + submitError={editError} + /> + )}
); }; diff --git a/ui/src/pages/data-sources/DataSourceOverviewTab.tsx b/ui/src/pages/data-sources/DataSourceOverviewTab.tsx index d702034a558..831e90b91d1 100644 --- a/ui/src/pages/data-sources/DataSourceOverviewTab.tsx +++ b/ui/src/pages/data-sources/DataSourceOverviewTab.tsx @@ -13,29 +13,34 @@ import { EuiDescriptionListDescription, EuiSpacer, } from "@elastic/eui"; -import React, { useContext } from "react"; +import React from "react"; import { useParams } from "react-router-dom"; -import PermissionsDisplay from "../../components/PermissionsDisplay"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import { FEAST_FCO_TYPES } from "../../parsers/types"; -import { feast } from "../../protos"; -import useLoadRegistry from "../../queries/useLoadRegistry"; -import { getEntityPermissions } from "../../utils/permissionUtils"; import BatchSourcePropertiesView from "./BatchSourcePropertiesView"; import FeatureViewEdgesList from "../entities/FeatureViewEdgesList"; import RequestDataSourceSchemaTable from "./RequestDataSourceSchemaTable"; import useLoadDataSource from "./useLoadDataSource"; +import { feast } from "../../protos"; const DataSourceOverviewTab = () => { - let { dataSourceName, projectName } = useParams(); - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl, projectName); + const { dataSourceName } = useParams(); const dsName = dataSourceName === undefined ? "" : dataSourceName; const { isLoading, isSuccess, isError, data, consumingFeatureViews } = useLoadDataSource(dsName); const isEmpty = data === undefined; + const viewTypesForDs: Record | undefined = + consumingFeatureViews && consumingFeatureViews.length > 0 + ? consumingFeatureViews.reduce((acc: Record, f: any) => { + acc[f.target.name] = + f.target.type === "labelView" ? "labelView" : "featureView"; + return acc; + }, {}) + : undefined; + + const spec = data?.spec || data; + const sourceType = spec?.type; + return ( {isLoading && ( @@ -56,16 +61,82 @@ const DataSourceOverviewTab = () => {

Properties

- {data.fileOptions || data.bigqueryOptions ? ( - - ) : data.type ? ( + {spec?.fileOptions || spec?.bigqueryOptions ? ( + + ) : String(sourceType) === + String(feast.core.DataSource.SourceType.BATCH_ICEBERG) ? ( + (() => { + let cfg: any = {}; + try { + cfg = JSON.parse( + spec?.customOptions?.configuration || "{}", + ); + } catch { + /* ignore */ + } + return ( + + + Source Type + + + Iceberg / Unity Catalog + + + Catalog Type + + + {cfg.catalog_type || "rest"} + + {cfg.endpoint && ( + <> + + Endpoint + + + {cfg.endpoint} + + + )} + + Warehouse + + + {cfg.warehouse || "—"} + + + Namespace + + + {cfg.namespace || "—"} + + + Table + + + {cfg.table || "—"} + + {cfg.token_env_var && ( + <> + + Token Env Variable + + + {cfg.token_env_var} + + + )} + + ); + })() + ) : sourceType ? ( Source Type - {feast.core.DataSource.SourceType[data.type]} + {sourceType} @@ -78,7 +149,7 @@ const DataSourceOverviewTab = () => { - {data.requestDataOptions ? ( + {spec?.requestDataOptions ? (

Request Source Schema

@@ -86,7 +157,7 @@ const DataSourceOverviewTab = () => { { + data?.requestDataOptions?.schema!.map((obj: any) => { return { fieldName: obj.name!, valueType: obj.valueType!, @@ -104,37 +175,18 @@ const DataSourceOverviewTab = () => { -

Consuming Feature Views

+

Consuming Views

{consumingFeatureViews && consumingFeatureViews.length > 0 ? ( { + fvNames={consumingFeatureViews.map((f: any) => { return f.target.name; })} + viewTypes={viewTypesForDs} /> ) : ( - No consuming feature views - )} -
- - - -

Permissions

-
- - {registryQuery.data?.permissions ? ( - - ) : ( - - No permissions defined for this data source. - + No consuming views )}
diff --git a/ui/src/pages/data-sources/DataSourcesListingTable.tsx b/ui/src/pages/data-sources/DataSourcesListingTable.tsx index c314a4dfb94..c08060dd0e4 100644 --- a/ui/src/pages/data-sources/DataSourcesListingTable.tsx +++ b/ui/src/pages/data-sources/DataSourcesListingTable.tsx @@ -32,8 +32,11 @@ const DatasourcesListingTable = ({ name: "Type", field: "type", sortable: true, - render: (valueType: feast.core.DataSource.SourceType) => { - return feast.core.DataSource.SourceType[valueType]; + render: (valueType: feast.core.DataSource.SourceType | string) => { + if (typeof valueType === "string") { + return valueType; + } + return feast.core.DataSource.SourceType[valueType] || String(valueType); }, }, ]; diff --git a/ui/src/pages/data-sources/Index.tsx b/ui/src/pages/data-sources/Index.tsx index 96aef712aec..c8422d3943a 100644 --- a/ui/src/pages/data-sources/Index.tsx +++ b/ui/src/pages/data-sources/Index.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { @@ -9,52 +9,175 @@ import { EuiTitle, EuiFieldSearch, EuiSpacer, + EuiButton, + EuiCallOut, } from "@elastic/eui"; -import useLoadRegistry from "../../queries/useLoadRegistry"; import DatasourcesListingTable from "./DataSourcesListingTable"; +import DataSourceCatalog from "./DataSourceCatalog"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import DataSourceIndexEmptyState from "./DataSourceIndexEmptyState"; import { DataSourceIcon } from "../../graphics/DataSourceIcon"; import { useSearchQuery } from "../../hooks/useSearchInputWithTags"; import { feast } from "../../protos"; import ExportButton from "../../components/ExportButton"; +import DataSourceFormModal, { + DataSourceFormData, +} from "../../components/DataSourceFormModal"; +import { useApplyDataSource } from "../../queries/mutations/useDataSourceMutations"; +import useResourceQuery, { + dataSourceListPath, +} from "../../queries/useResourceQuery"; const useLoadDatasources = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.dataSources; - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: "data-sources-list", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources, + }); }; -const filterFn = (data: feast.core.IDataSource[], searchTokens: string[]) => { - let filteredByTags = data; - +const filterFn = (data: any[], searchTokens: string[]) => { if (searchTokens.length) { - return filteredByTags.filter((entry) => { + return data.filter((entry) => { + const name = entry.name || entry.spec?.name || ""; return searchTokens.find((token) => { - return ( - token.length >= 3 && entry.name && entry.name.indexOf(token) >= 0 - ); + return token.length >= 3 && name.indexOf(token) >= 0; }); }); } - return filteredByTags; + return data; +}; + +const formDataToPayload = (formData: DataSourceFormData, project: string) => { + const payload: Record = { + name: formData.name, + project, + type: parseInt(formData.sourceType, 10), + timestamp_field: formData.timestampField, + created_timestamp_column: formData.createdTimestampColumn, + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + }; + + const st = formData.sourceType; + if (st === String(feast.core.DataSource.SourceType.BATCH_FILE)) { + payload.file_options = { + uri: formData.fileUri, + file_format: formData.fileFormat || "parquet", + s3_endpoint_override: formData.fileS3EndpointOverride || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_BIGQUERY)) { + payload.bigquery_options = { + table: formData.bigqueryTable, + query: formData.bigqueryQuery, + }; + if (formData.bigqueryDatePartitionColumn) { + payload.date_partition_column = formData.bigqueryDatePartitionColumn; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE)) { + payload.snowflake_options = { + table: formData.snowflakeTable, + database: formData.snowflakeDatabase, + schema_: formData.snowflakeSchema, + query: formData.snowflakeQuery || "", + warehouse: formData.snowflakeWarehouse || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_REDSHIFT)) { + payload.redshift_options = { + table: formData.redshiftTable, + database: formData.redshiftDatabase, + schema_: formData.redshiftSchema, + query: formData.redshiftQuery || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KAFKA)) { + payload.kafka_options = { + kafka_bootstrap_servers: formData.kafkaBootstrapServers, + topic: formData.kafkaTopic, + message_format: formData.kafkaMessageFormat || "json", + watermark_delay_threshold: formData.kafkaWatermarkDelay || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_SPARK)) { + payload.spark_options = { + table: formData.sparkTable, + path: formData.sparkPath, + query: formData.sparkQuery || "", + file_format: formData.sparkFileFormat || "", + table_format: formData.sparkTableFormat || "", + table_format_catalog: formData.sparkTableFormatCatalog || "", + table_format_namespace: formData.sparkTableFormatNamespace || "", + table_format_properties: formData.sparkTableFormatProperties || "", + date_partition_column: formData.sparkDatePartitionColumn || "", + date_partition_column_format: formData.sparkDatePartitionFormat || "", + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_TRINO)) { + payload.trino_options = { + table: formData.trinoTable, + query: formData.trinoQuery, + }; + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ATHENA)) { + payload.athena_options = { + table: formData.athenaTable, + query: formData.athenaQuery, + database: formData.athenaDatabase, + data_source: formData.athenaDataSource, + }; + if (formData.athenaDatePartitionColumn) { + payload.date_partition_column = formData.athenaDatePartitionColumn; + } + } else if (st === String(feast.core.DataSource.SourceType.BATCH_ICEBERG)) { + const catalogProps = formData.icebergCatalogProperties.trim() + ? JSON.parse(formData.icebergCatalogProperties) + : {}; + payload.custom_options = { + configuration: JSON.stringify({ + catalog_type: formData.icebergCatalogType || "rest", + endpoint: formData.icebergEndpoint, + warehouse: formData.icebergWarehouse, + namespace: formData.icebergNamespace, + table: formData.icebergTable, + token_env_var: formData.icebergTokenEnvVar || null, + credential_vending: formData.icebergCredentialVending !== "false", + catalog_properties: catalogProps, + }), + }; + payload.data_source_class_type = + "feast.infra.data_sources.contrib.iceberg_catalog.iceberg_source.IcebergSource"; + } else if (st === String(feast.core.DataSource.SourceType.STREAM_KINESIS)) { + payload.kinesis_options = { + region: formData.kinesisRegion, + stream_name: formData.kinesisStreamName, + record_format: formData.kinesisRecordFormat || "json", + }; + } else if (st === String(feast.core.DataSource.SourceType.CUSTOM_SOURCE)) { + payload.custom_options = { + class_name: formData.customSourceClassName, + config: formData.customSourceConfig, + }; + } + + return payload; }; const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadDatasources(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadDatasources(); + const isAllProjects = projectName === "all"; + + const [showCatalog, setShowCatalog] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [preselectedSourceType, setPreselectedSourceType] = useState< + string | null + >(null); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const applyDataSource = useApplyDataSource(); useDocumentTitle(`Data Sources | Feast`); @@ -62,6 +185,115 @@ const Index = () => { const filterResult = data ? filterFn(data, searchTokens) : data; + const hasExistingSources = isSuccess && data && data.length > 0; + const isEmpty = isSuccess && (!data || data.length === 0); + + const modalInitialData = useMemo(() => { + if (!preselectedSourceType) return undefined; + return { + name: "", + description: "", + owner: "", + sourceType: preselectedSourceType, + timestampField: "", + createdTimestampColumn: "", + tags: [] as { key: string; value: string }[], + fileUri: "", + fileFormat: "parquet", + fileS3EndpointOverride: "", + bigqueryTable: "", + bigqueryQuery: "", + bigqueryDatePartitionColumn: "", + snowflakeTable: "", + snowflakeDatabase: "", + snowflakeSchema: "", + snowflakeQuery: "", + snowflakeWarehouse: "", + redshiftTable: "", + redshiftDatabase: "", + redshiftSchema: "", + redshiftQuery: "", + kafkaBootstrapServers: "", + kafkaTopic: "", + kafkaMessageFormat: "json", + kafkaWatermarkDelay: "", + sparkTable: "", + sparkPath: "", + sparkQuery: "", + sparkFileFormat: "parquet", + sparkTableFormat: "", + sparkTableFormatCatalog: "", + sparkTableFormatNamespace: "", + sparkTableFormatProperties: "", + sparkDatePartitionColumn: "", + sparkDatePartitionFormat: "%Y-%m-%d", + kinesisRegion: "", + kinesisStreamName: "", + kinesisRecordFormat: "json", + trinoTable: "", + trinoQuery: "", + athenaTable: "", + athenaQuery: "", + athenaDatabase: "", + athenaDataSource: "", + athenaDatePartitionColumn: "", + customSourceClassName: "", + customSourceConfig: "", + icebergCatalogType: "rest", + icebergEndpoint: "", + icebergWarehouse: "", + icebergNamespace: "", + icebergTable: "", + icebergTokenEnvVar: "", + icebergCredentialVending: "true", + icebergCatalogProperties: "", + rayReaderType: "parquet", + rayPath: "", + rayReaderOptions: "", + postgresTable: "", + postgresQuery: "", + mongodbCollection: "", + clickhouseTable: "", + clickhouseQuery: "", + mssqlTable: "", + mssqlConnectionStr: "", + mssqlDatePartitionColumn: "", + oracleTable: "", + oracleConnectionStr: "", + oracleDatePartitionColumn: "", + couchbaseDatabase: "", + couchbaseScope: "", + couchbaseCollection: "", + couchbaseQuery: "", + }; + }, [preselectedSourceType]); + + const handleSelectType = (sourceType: string) => { + setPreselectedSourceType(sourceType); + setIsModalOpen(true); + }; + + const handleCreateSubmit = (formData: DataSourceFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyDataSource.mutate(payload as any, { + onSuccess: () => { + setIsModalOpen(false); + setPreselectedSourceType(null); + setShowCatalog(false); + setErrorMessage(null); + setSuccessMessage( + `Data source "${formData.name}" created successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setErrorMessage(message); + }, + }); + }; + return ( { iconType={DataSourceIcon} pageTitle="Data Sources" rightSideItems={[ + ...(isAllProjects || showCatalog + ? [] + : [ + setShowCatalog(true)} + key="create" + > + Create Data Source + , + ]), , ]} /> - {isLoading && ( -

- Loading -

+ {successMessage && ( + <> + + + )} - {isError &&

We encountered an error while loading.

} - {isSuccess && !data && } - {isSuccess && data && data.length > 0 && filterResult && ( - - - - -

Search

+ {errorMessage && !isModalOpen && ( + <> + + + + )} + + {showCatalog && !isAllProjects && ( + <> + + + +

Select a Data Source Type

- { - setSearchString(e.target.value); - }} - />
+ {hasExistingSources && ( + + setShowCatalog(false)} + iconType="arrowLeft" + size="s" + > + Back to Data Sources + + + )}
- -
+ + + )} + + {!showCatalog && ( + <> + {isLoading && ( +

+ Loading +

+ )} + {isPermissionDenied && ( + +

You do not have permission to view data sources.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} + {isEmpty && !isAllProjects && ( + <> + +

No data sources yet — create your first connection

+
+ + + + )} + {isEmpty && isAllProjects && ( +

No data sources found across projects.

+ )} + {hasExistingSources && filterResult && ( + + + + +

Search

+
+ { + setSearchString(e.target.value); + }} + /> +
+
+ + +
+ )} + )}
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setPreselectedSourceType(null); + setErrorMessage(null); + }} + onSubmit={handleCreateSubmit} + isSubmitting={applyDataSource.isLoading} + submitError={errorMessage} + initialData={modalInitialData} + /> + )}
); }; diff --git a/ui/src/pages/data-sources/RequestDataSourceSchemaTable.tsx b/ui/src/pages/data-sources/RequestDataSourceSchemaTable.tsx index a55aeac0280..f671042dd85 100644 --- a/ui/src/pages/data-sources/RequestDataSourceSchemaTable.tsx +++ b/ui/src/pages/data-sources/RequestDataSourceSchemaTable.tsx @@ -20,8 +20,9 @@ const RequestDataSourceSchemaTable = ({ fields }: RequestDataSourceSchema) => { { name: "Value Type", field: "valueType", - render: (valueType: feast.types.ValueType.Enum) => { - return feast.types.ValueType.Enum[valueType]; + render: (valueType: feast.types.ValueType.Enum | string) => { + if (typeof valueType === "string") return valueType; + return feast.types.ValueType.Enum[valueType] || String(valueType || ""); }, }, ]; diff --git a/ui/src/pages/data-sources/useLoadDataSource.ts b/ui/src/pages/data-sources/useLoadDataSource.ts index 43f697fca03..c099e78f917 100644 --- a/ui/src/pages/data-sources/useLoadDataSource.ts +++ b/ui/src/pages/data-sources/useLoadDataSource.ts @@ -1,35 +1,35 @@ -import { useContext } from "react"; import { useParams } from "react-router-dom"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import { FEAST_FCO_TYPES } from "../../parsers/types"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import useResourceQuery, { + dataSourceDetailPath, +} from "../../queries/useResourceQuery"; const useLoadDataSource = (dataSourceName: string) => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.dataSources?.find( - (ds) => ds.name === dataSourceName, - ); + const dsQuery = useResourceQuery({ + resourceType: `data-source:${dataSourceName}`, + project: projectName, + restPath: dataSourceDetailPath(dataSourceName, projectName || ""), + restSelect: (d) => ({ + dataSource: d, + relationships: d?.relationships || [], + }), + enabled: !!dataSourceName, + }); - const consumingFeatureViews = - registryQuery.data === undefined - ? undefined - : registryQuery.data.relationships.filter((relationship) => { - return ( - relationship.source.type === FEAST_FCO_TYPES.dataSource && - relationship.source.name === data?.name && - relationship.target.type === FEAST_FCO_TYPES.featureView - ); - }); + const dataSource = dsQuery.data?.dataSource; + const relationships = dsQuery.data?.relationships || []; + + const consumingFeatureViews = relationships.filter( + (rel: any) => + rel?.source?.type === "dataSource" && + (rel?.target?.type === "featureView" || + rel?.target?.type === "labelView"), + ); return { - ...registryQuery, - data, + ...dsQuery, + data: dataSource, consumingFeatureViews, }; }; diff --git a/ui/src/pages/document-labeling/ClassificationTab.tsx b/ui/src/pages/document-labeling/ClassificationTab.tsx deleted file mode 100644 index 302b03cd9fa..00000000000 --- a/ui/src/pages/document-labeling/ClassificationTab.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import React, { useState } from "react"; -import { - EuiPageSection, - EuiCallOut, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldText, - EuiButton, - EuiPanel, - EuiTitle, - EuiText, - EuiTable, - EuiTableHeader, - EuiTableHeaderCell, - EuiTableBody, - EuiTableRow, - EuiTableRowCell, - EuiSelect, - EuiLoadingSpinner, -} from "@elastic/eui"; - -interface ClassificationData { - id: number; - text: string; - currentClass: string; - originalClass?: string; -} - -const ClassificationTab = () => { - const [csvPath, setCsvPath] = useState("./src/sample-data.csv"); - const [isLoading, setIsLoading] = useState(false); - const [data, setData] = useState([]); - const [error, setError] = useState(null); - const [availableClasses] = useState(["positive", "negative", "neutral"]); - - const loadCsvData = async () => { - if (!csvPath) return; - - setIsLoading(true); - setError(null); - - try { - if (csvPath === "./src/sample-data.csv") { - const sampleData: ClassificationData[] = [ - { - id: 1, - text: "This product is amazing! I love the quality and design.", - currentClass: "positive", - originalClass: "positive", - }, - { - id: 2, - text: "The service was terrible and the food was cold.", - currentClass: "negative", - originalClass: "negative", - }, - { - id: 3, - text: "It's an okay product, nothing special but does the job.", - currentClass: "neutral", - originalClass: "neutral", - }, - { - id: 4, - text: "Excellent customer support and fast delivery!", - currentClass: "positive", - originalClass: "positive", - }, - { - id: 5, - text: "I'm not sure how I feel about this purchase.", - currentClass: "neutral", - originalClass: "positive", - }, - ]; - - setData(sampleData); - } else { - throw new Error( - "CSV file not found. Please use the sample data path: ./src/sample-data.csv", - ); - } - } catch (err) { - setError( - err instanceof Error - ? err.message - : "An error occurred while loading the CSV data", - ); - } finally { - setIsLoading(false); - } - }; - - const handleClassChange = (id: number, newClass: string) => { - setData( - data.map((item) => - item.id === id ? { ...item, currentClass: newClass } : item, - ), - ); - }; - - const getChangedItems = () => { - return data.filter((item) => item.currentClass !== item.originalClass); - }; - - const resetChanges = () => { - setData( - data.map((item) => ({ ...item, currentClass: item.originalClass || "" })), - ); - }; - - const saveChanges = () => { - const changedItems = getChangedItems(); - console.log("Saving classification changes:", changedItems); - alert(`Saved ${changedItems.length} classification changes!`); - }; - - const columns = [ - { - field: "id", - name: "ID", - width: "60px", - }, - { - field: "text", - name: "Text", - width: "60%", - }, - { - field: "originalClass", - name: "Original Class", - width: "15%", - }, - { - field: "currentClass", - name: "Current Class", - width: "20%", - }, - ]; - - return ( - - -

- Load a CSV file containing text samples and edit their classification - labels. This helps improve your classification models by providing - corrected training data. -

-
- - - - - - - setCsvPath(e.target.value)} - /> - - - - - - Load CSV Data - - - - - - - - {isLoading && ( - - - - - - Loading CSV data... - - - )} - - {error && ( - -

{error}

-
- )} - - {data.length > 0 && ( - <> - - - -

Classification Data ({data.length} samples)

-
-
- - - - - Reset Changes - - - - - Save Changes ({getChangedItems().length}) - - - - -
- - - - - - - {columns.map((column, index) => ( - - {column.name} - - ))} - - - {data.map((item) => ( - - {item.id} - - {item.text} - - - - {item.originalClass} - - - - ({ - value: cls, - text: cls, - }))} - value={item.currentClass} - onChange={(e) => - handleClassChange(item.id, e.target.value) - } - compressed - /> - - - ))} - - - - - {getChangedItems().length > 0 && ( - <> - - -

- You have unsaved changes. Click "Save Changes" to persist your - modifications. -

-
- - )} - - )} -
- ); -}; - -export default ClassificationTab; diff --git a/ui/src/pages/document-labeling/DocumentLabelingPage.tsx b/ui/src/pages/document-labeling/DocumentLabelingPage.tsx deleted file mode 100644 index 5563d6328c1..00000000000 --- a/ui/src/pages/document-labeling/DocumentLabelingPage.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useState } from "react"; -import { - EuiPage, - EuiPageBody, - EuiPageSection, - EuiPageHeader, - EuiTitle, - EuiSpacer, - EuiTabs, - EuiTab, -} from "@elastic/eui"; -import RagTab from "./RagTab"; -import ClassificationTab from "./ClassificationTab"; - -const DocumentLabelingPage = () => { - const [selectedTab, setSelectedTab] = useState("rag"); - - const tabs = [ - { - id: "rag", - name: "RAG", - content: , - }, - { - id: "classification", - name: "Classification", - content: , - }, - ]; - - const selectedTabContent = tabs.find( - (tab) => tab.id === selectedTab, - )?.content; - - return ( - - - - -

Data Labeling

-
-
- - - - {tabs.map((tab) => ( - setSelectedTab(tab.id)} - isSelected={tab.id === selectedTab} - > - {tab.name} - - ))} - - - - - {selectedTabContent} - -
-
- ); -}; - -export default DocumentLabelingPage; diff --git a/ui/src/pages/document-labeling/RagTab.tsx b/ui/src/pages/document-labeling/RagTab.tsx deleted file mode 100644 index ae5fd22aea7..00000000000 --- a/ui/src/pages/document-labeling/RagTab.tsx +++ /dev/null @@ -1,615 +0,0 @@ -import React, { useState } from "react"; -import { - EuiPageSection, - EuiCallOut, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldText, - EuiButton, - EuiPanel, - EuiTitle, - EuiText, - EuiLoadingSpinner, - EuiButtonGroup, - EuiCode, - EuiTextArea, -} from "@elastic/eui"; -import { useTheme } from "../../contexts/ThemeContext"; - -interface DocumentContent { - content: string; - file_path: string; -} - -interface TextSelection { - text: string; - start: number; - end: number; -} - -interface DocumentLabel { - text: string; - start: number; - end: number; - label: string; - timestamp: number; - groundTruthLabel: string; -} - -const RagTab = () => { - const { colorMode } = useTheme(); - const [filePath, setFilePath] = useState("./src/test-document.txt"); - const [selectedText, setSelectedText] = useState(null); - const [labelingMode, setLabelingMode] = useState("relevant"); - const [labels, setLabels] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [documentContent, setDocumentContent] = - useState(null); - const [error, setError] = useState(null); - const [prompt, setPrompt] = useState(""); - const [query, setQuery] = useState(""); - const [groundTruthLabel, setGroundTruthLabel] = useState(""); - const [isSaving, setIsSaving] = useState(false); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - - const loadDocument = async () => { - if (!filePath) return; - - setIsLoading(true); - setError(null); - - try { - if (filePath === "./src/test-document.txt") { - const testContent = `This is a sample document for testing the data labeling functionality in Feast UI. - -The document contains multiple paragraphs and sections that can be used to test the text highlighting and labeling features. - -This paragraph discusses machine learning and artificial intelligence concepts. It covers topics like neural networks, deep learning, and natural language processing. Users should be able to select and label relevant portions of this text for RAG retrieval systems. - -Another section focuses on data engineering and ETL pipelines. This content explains how to process large datasets and build scalable data infrastructure. The labeling system should allow users to mark this as relevant or irrelevant for their specific use cases. - -The final paragraph contains information about feature stores and real-time machine learning systems. This text can be used to test the highlighting functionality and ensure that labels are properly stored and displayed in the user interface.`; - - setDocumentContent({ - content: testContent, - file_path: filePath, - }); - - loadSavedLabels(); - } else { - throw new Error( - "Document not found. Please use the test document path: ./src/test-document.txt", - ); - } - } catch (err) { - setError( - err instanceof Error - ? err.message - : "An error occurred while loading the document", - ); - } finally { - setIsLoading(false); - } - }; - - const handleTextSelection = () => { - const selection = window.getSelection(); - if (selection && selection.toString().trim() && documentContent) { - const selectedTextContent = selection.toString().trim(); - const range = selection.getRangeAt(0); - - const textContent = documentContent.content; - - let startIndex = -1; - let endIndex = -1; - - const rangeText = range.toString(); - if (rangeText) { - startIndex = textContent.indexOf(rangeText); - if (startIndex !== -1) { - endIndex = startIndex + rangeText.length; - } - } - - if (startIndex !== -1 && endIndex !== -1) { - setSelectedText({ - text: selectedTextContent, - start: startIndex, - end: endIndex, - }); - } - } - }; - - const handleLabelSelection = () => { - if (selectedText) { - const newLabel: DocumentLabel = { - text: selectedText.text, - start: selectedText.start, - end: selectedText.end, - label: labelingMode, - timestamp: Date.now(), - groundTruthLabel: groundTruthLabel, - }; - - setLabels([...labels, newLabel]); - setSelectedText(null); - setHasUnsavedChanges(true); - - const selection = window.getSelection(); - if (selection) { - selection.removeAllRanges(); - } - } - }; - - const handleRemoveLabel = (index: number) => { - setLabels(labels.filter((_: DocumentLabel, i: number) => i !== index)); - setHasUnsavedChanges(true); - }; - - const saveLabels = () => { - setIsSaving(true); - - setTimeout(() => { - try { - const saveData = { - filePath: filePath, - prompt: prompt, - query: query, - groundTruthLabel: groundTruthLabel, - labels: labels, - timestamp: new Date().toISOString(), - }; - - const pathParts = filePath.split("/"); - const filename = pathParts[pathParts.length - 1]; - const nameWithoutExt = filename.replace(/\.[^/.]+$/, ""); - const downloadFilename = `${nameWithoutExt}-labels.json`; - - const jsonString = JSON.stringify(saveData, null, 2); - const blob = new Blob([jsonString], { type: "application/json" }); - const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = downloadFilename; - link.style.display = "none"; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - setHasUnsavedChanges(false); - alert( - `Successfully saved ${labels.length} labels. File downloaded as ${downloadFilename}`, - ); - } catch (error) { - console.error("Error saving labels:", error); - alert("Error saving labels. Please try again."); - } finally { - setIsSaving(false); - } - }, 100); - }; - - const loadSavedLabels = () => { - try { - const savedData = JSON.parse(localStorage.getItem("ragLabels") || "[]"); - const fileData = savedData.find( - (item: any) => item.filePath === filePath, - ); - - if (fileData) { - setPrompt(fileData.prompt || ""); - setQuery(fileData.query || ""); - setGroundTruthLabel(fileData.groundTruthLabel || ""); - setLabels(fileData.labels || []); - setHasUnsavedChanges(false); - } - } catch (error) { - console.error("Error loading saved labels:", error); - } - }; - - const renderDocumentWithHighlights = ( - content: string, - ): (string | React.ReactElement)[] => { - const allHighlights = [...labels]; - - if (selectedText) { - allHighlights.push({ - text: selectedText.text, - start: selectedText.start, - end: selectedText.end, - label: "temp-selection", - timestamp: 0, - groundTruthLabel: "", - }); - } - - if (allHighlights.length === 0) { - return [content]; - } - - const sortedHighlights = [...allHighlights].sort( - (a, b) => a.start - b.start, - ); - const result: (string | React.ReactElement)[] = []; - let lastIndex = 0; - - sortedHighlights.forEach((highlight, index) => { - result.push(content.slice(lastIndex, highlight.start)); - - let highlightColor, borderColor; - - if (highlight.label === "temp-selection") { - if (colorMode === "dark") { - highlightColor = "#1a4d66"; - borderColor = "#2d6b8a"; - } else { - highlightColor = "#add8e6"; - borderColor = "#87ceeb"; - } - } else if (highlight.label === "irrelevant") { - if (colorMode === "dark") { - highlightColor = "#4d1a1a"; - borderColor = "#6b2d2d"; - } else { - highlightColor = "#f8d7da"; - borderColor = "#f5c6cb"; - } - } else { - if (colorMode === "dark") { - highlightColor = "#1a4d1a"; - borderColor = "#2d6b2d"; - } else { - highlightColor = "#d4edda"; - borderColor = "#c3e6cb"; - } - } - - result.push( - - {highlight.text} - , - ); - - lastIndex = highlight.end; - }); - - result.push(content.slice(lastIndex)); - return result; - }; - - const labelingOptions = [ - { - id: "relevant", - label: "Relevant", - }, - { - id: "irrelevant", - label: "Irrelevant", - }, - ]; - - return ( - - -

- Load a document and highlight text chunks to label them for chunk - extraction/retrieval. Add prompt and query context, then provide - ground truth labels for generation evaluation. -

-
- - - - - - - setFilePath(e.target.value)} - /> - - - - - - Load Document - - - - - - - - {isLoading && ( - - - - - - Loading document... - - - )} - - {error && ( - -

{error}

-
- )} - - {documentContent && ( - <> - - -

RAG Context

-
- - - - - { - setPrompt(e.target.value); - setHasUnsavedChanges(true); - }} - rows={3} - /> - - - - - { - setQuery(e.target.value); - setHasUnsavedChanges(true); - }} - rows={3} - /> - - - -
- - - - -

Step 1: Label for Chunk Extraction

-
- - - - - - setLabelingMode(id)} - buttonSize="s" - /> - - - - - - Label Selected Text - - - - - - - - {selectedText && ( - - {selectedText.text} - - )} - - - - - -

Document Content

-
- - -
- {renderDocumentWithHighlights(documentContent.content)} -
-
-
- - - - -

Step 2: Label for Generation

-
- - - - { - setGroundTruthLabel(e.target.value); - setHasUnsavedChanges(true); - }} - rows={3} - /> - - - - - - - - Save Labels - - - - - - - {(labels.length > 0 || groundTruthLabel || prompt || query) && ( - <> - -

- Click "Save Labels" to download your labeled data as a JSON - file. -

-
- - - )} - - - - {hasUnsavedChanges && ( - <> - -

- You have unsaved changes. Click "Save Labels" to persist your - work. -

-
- - - )} - - {labels.length > 0 && ( - <> - - - -

Extracted Chunk Labels ({labels.length})

-
- - {labels.map((label, index) => ( - - - - Chunk: {label.label} - - - {label.groundTruthLabel && ( - - - GT: {label.groundTruthLabel} - - - )} - - - "{label.text.substring(0, 80)} - {label.text.length > 80 ? "..." : ""}" - - - - handleRemoveLabel(index)} - > - Remove - - - - ))} -
- - )} - - )} -
- ); -}; - -export default RagTab; diff --git a/ui/src/pages/document-labeling/index.ts b/ui/src/pages/document-labeling/index.ts deleted file mode 100644 index f3f4012b362..00000000000 --- a/ui/src/pages/document-labeling/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./DocumentLabelingPage"; diff --git a/ui/src/pages/entities/EntitiesListingTable.tsx b/ui/src/pages/entities/EntitiesListingTable.tsx index 51ffb7c8609..625ec7d5fbb 100644 --- a/ui/src/pages/entities/EntitiesListingTable.tsx +++ b/ui/src/pages/entities/EntitiesListingTable.tsx @@ -20,7 +20,8 @@ const EntitiesListingTable = ({ entities }: EntitiesListingTableProps) => { sortable: true, render: (name: string, item: feast.core.IEntity) => { // For "All Projects" view, link to the specific project - const itemProject = item?.spec?.project || projectName; + const itemProject = + item?.spec?.project || (item as any)?.project || projectName; return ( {name} @@ -28,12 +29,19 @@ const EntitiesListingTable = ({ entities }: EntitiesListingTableProps) => { ); }, }, + { + name: "Join Key", + field: "spec.joinKey", + sortable: true, + }, { name: "Type", field: "spec.valueType", sortable: true, - render: (valueType: feast.types.ValueType.Enum) => { - return feast.types.ValueType.Enum[valueType]; + render: (valueType: feast.types.ValueType.Enum | string | undefined) => { + if (!valueType) return "—"; + if (typeof valueType === "string") return valueType; + return feast.types.ValueType.Enum[valueType] || String(valueType); }, }, { @@ -52,7 +60,7 @@ const EntitiesListingTable = ({ entities }: EntitiesListingTableProps) => { if (projectName === "all") { columns.splice(1, 0, { name: "Project", - field: "spec.project", + field: "project", sortable: true, render: (project: string) => { return {project || "Unknown"}; diff --git a/ui/src/pages/entities/EntityInstance.tsx b/ui/src/pages/entities/EntityInstance.tsx index e3be0ef167f..4807568ce2c 100644 --- a/ui/src/pages/entities/EntityInstance.tsx +++ b/ui/src/pages/entities/EntityInstance.tsx @@ -1,31 +1,128 @@ -import React from "react"; +import React, { useState } from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, +} from "@elastic/eui"; import { EntityIcon } from "../../graphics/EntityIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import EntityOverviewTab from "./EntityOverviewTab"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import EntityFormModal, { + EntityFormData, +} from "../../components/EntityFormModal"; +import { + useApplyEntity, + useDeleteEntity, +} from "../../queries/mutations/useEntityMutations"; +import useLoadEntity from "./useLoadEntity"; import { useEntityCustomTabs, useEntityCustomTabRoutes, } from "../../custom-tabs/TabsRegistryContext"; +import { feast } from "../../protos"; + +const buildEditFormData = (entity: feast.core.IEntity): EntityFormData => { + const tags = entity.spec?.tags + ? Object.entries(entity.spec.tags).map(([key, value]) => ({ + key, + value: String(value), + })) + : []; + + const joinKeys = entity.spec?.joinKey ? [entity.spec.joinKey] : [""]; + + return { + name: entity.spec?.name || "", + description: entity.spec?.description || "", + joinKeys, + valueType: String(entity.spec?.valueType ?? 0), + tags, + }; +}; const EntityInstance = () => { const navigate = useNavigate(); - let { entityName } = useParams(); + let { entityName, projectName } = useParams(); const { customNavigationTabs } = useEntityCustomTabs(navigate); const CustomTabRoutes = useEntityCustomTabRoutes(); useDocumentTitle(`${entityName} | Entity | Feast`); + const { data } = useLoadEntity(entityName || ""); + const applyEntity = useApplyEntity(); + const deleteEntity = useDeleteEntity(); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editError, setEditError] = useState(null); + + const handleDelete = () => { + deleteEntity.mutate( + { name: entityName || "", project: projectName || "" }, + { + onSuccess: () => { + navigate(`/p/${projectName}/entity`); + }, + }, + ); + }; + + const handleEditSubmit = (formData: EntityFormData) => { + const payload = { + name: formData.name, + project: projectName || "", + join_key: formData.joinKeys[0] || formData.name, + value_type: parseInt(formData.valueType, 10), + description: formData.description, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + owner: "", + }; + applyEntity.mutate(payload, { + onSuccess: () => { + setIsEditModalOpen(false); + setEditError(null); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setEditError(message); + }, + }); + }; + return ( { + setEditError(null); + setIsEditModalOpen(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={[ { label: "Overview", @@ -43,6 +140,37 @@ const EntityInstance = () => { {CustomTabRoutes}
+ + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteEntity.isLoading} + > +

+ This will permanently remove the entity. This action cannot be + undone. +

+
+ )} + + {isEditModalOpen && data && ( + { + setIsEditModalOpen(false); + setEditError(null); + }} + onSubmit={handleEditSubmit} + initialData={buildEditFormData(data)} + isEdit + isSubmitting={applyEntity.isLoading} + submitError={editError} + /> + )}
); }; diff --git a/ui/src/pages/entities/EntityOverviewTab.tsx b/ui/src/pages/entities/EntityOverviewTab.tsx index 8a20688d140..c590eeb3b8e 100644 --- a/ui/src/pages/entities/EntityOverviewTab.tsx +++ b/ui/src/pages/entities/EntityOverviewTab.tsx @@ -19,7 +19,7 @@ import PermissionsDisplay from "../../components/PermissionsDisplay"; import TagsDisplay from "../../components/TagsDisplay"; import RegistryPathContext from "../../contexts/RegistryPathContext"; import { FEAST_FCO_TYPES } from "../../parsers/types"; -import { feast } from "../../protos"; + import useLoadRegistry from "../../queries/useLoadRegistry"; import { getEntityPermissions } from "../../utils/permissionUtils"; import { toDate } from "../../utils/timestamp"; @@ -40,6 +40,15 @@ const EntityOverviewTab = () => { const fvEdgesSuccess = fvEdges.isSuccess; const fvEdgesData = fvEdges.data; + const viewTypesForEntity: Record | undefined = + fvEdgesSuccess && fvEdgesData && fvEdgesData[eName] + ? fvEdgesData[eName].reduce((acc: Record, r) => { + acc[r.target.name] = + r.target.type === "labelView" ? "labelView" : "featureView"; + return acc; + }, {}) + : undefined; + return ( {isLoading && ( @@ -64,14 +73,22 @@ const EntityOverviewTab = () => { {data?.spec?.joinKey} - Description - - {data?.spec?.description} - + {data?.spec?.valueType && ( + <> + + Value Type + + + {typeof data.spec.valueType === "string" + ? data.spec.valueType + : String(data.spec.valueType)} + + + )} - Value Type + Description - {feast.types.ValueType.Enum[data?.spec?.valueType!]} + {data?.spec?.description || "—"} @@ -109,7 +126,7 @@ const EntityOverviewTab = () => { -

Feature Views

+

Consuming Views

{fvEdgesSuccess && fvEdgesData ? ( @@ -118,13 +135,14 @@ const EntityOverviewTab = () => { fvNames={fvEdgesData[eName].map((r) => { return r.target.name; })} + viewTypes={viewTypesForEntity} /> ) : ( - No feature views have this entity + No views consume this entity ) ) : ( - Error loading feature views that have this entity. + Error loading views that consume this entity. )}
diff --git a/ui/src/pages/entities/FeatureViewEdgesList.tsx b/ui/src/pages/entities/FeatureViewEdgesList.tsx index eca599c852a..79f8eb89251 100644 --- a/ui/src/pages/entities/FeatureViewEdgesList.tsx +++ b/ui/src/pages/entities/FeatureViewEdgesList.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { EuiBasicTable, EuiLoadingSpinner } from "@elastic/eui"; +import { EuiBasicTable, EuiBadge, EuiLoadingSpinner } from "@elastic/eui"; import EuiCustomLink from "../../components/EuiCustomLink"; import { useParams } from "react-router-dom"; import useLoadRelationshipData from "../../queries/useLoadRelationshipsData"; @@ -8,6 +8,7 @@ import { FEAST_FCO_TYPES } from "../../parsers/types"; interface FeatureViewEdgesListInterace { fvNames: string[]; + viewTypes?: Record; } const whereFSconsumesThisFv = (fvName: string) => { @@ -42,7 +43,10 @@ const useGetFSConsumersOfFV = (fvList: string[]) => { }; }; -const FeatureViewEdgesList = ({ fvNames }: FeatureViewEdgesListInterace) => { +const FeatureViewEdgesList = ({ + fvNames, + viewTypes, +}: FeatureViewEdgesListInterace) => { const { projectName } = useParams(); const { isLoading, data } = useGetFSConsumersOfFV(fvNames); @@ -52,10 +56,20 @@ const FeatureViewEdgesList = ({ fvNames }: FeatureViewEdgesListInterace) => { name: "Name", field: "", render: ({ name }: { name: string }) => { + const isLabelView = viewTypes?.[name] === "labelView"; + const path = isLabelView + ? `/p/${projectName}/label-view/${name}` + : `/p/${projectName}/feature-view/${name}`; return ( - - {name} - + + {name} + {isLabelView && ( + <> + {" "} + label view + + )} + ); }, }, diff --git a/ui/src/pages/entities/Index.tsx b/ui/src/pages/entities/Index.tsx index 070c53d38fa..57a2584b071 100644 --- a/ui/src/pages/entities/Index.tsx +++ b/ui/src/pages/entities/Index.tsx @@ -1,38 +1,83 @@ -import React, { useContext } from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; -import { EuiPageTemplate, EuiLoadingSpinner } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiLoadingSpinner, + EuiButton, + EuiCallOut, + EuiSpacer, +} from "@elastic/eui"; import { EntityIcon } from "../../graphics/EntityIcon"; -import useLoadRegistry from "../../queries/useLoadRegistry"; import EntitiesListingTable from "./EntitiesListingTable"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; import EntityIndexEmptyState from "./EntityIndexEmptyState"; import ExportButton from "../../components/ExportButton"; +import EntityFormModal, { + EntityFormData, +} from "../../components/EntityFormModal"; +import { useApplyEntity } from "../../queries/mutations/useEntityMutations"; +import useResourceQuery, { + entityListPath, +} from "../../queries/useResourceQuery"; const useLoadEntities = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.entities; - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: "entities-list", + project: projectName, + restPath: entityListPath(projectName), + restSelect: (d) => d.entities, + }); }; +const formDataToPayload = (formData: EntityFormData, project: string) => ({ + name: formData.name, + project, + join_key: formData.joinKeys[0] || formData.name, + value_type: parseInt(formData.valueType, 10), + description: formData.description, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + owner: "", +}); + const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadEntities(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadEntities(); + const isAllProjects = projectName === "all"; + + const [isModalOpen, setIsModalOpen] = useState(false); + const [successMessage, setSuccessMessage] = useState(null); + const [submitErrorMessage, setSubmitErrorMessage] = useState( + null, + ); + const applyEntity = useApplyEntity(); useDocumentTitle(`Entities | Feast`); + const handleCreateSubmit = (formData: EntityFormData) => { + setSubmitErrorMessage(null); + const payload = formDataToPayload(formData, projectName || ""); + applyEntity.mutate(payload, { + onSuccess: () => { + setIsModalOpen(false); + setSubmitErrorMessage(null); + setSuccessMessage(`Entity "${formData.name}" created successfully.`); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setSubmitErrorMessage(message); + }, + }); + }; + return ( { iconType={EntityIcon} pageTitle="Entities" rightSideItems={[ + ...(isAllProjects + ? [] + : [ + setIsModalOpen(true)} + key="create" + > + Create Entity + , + ]), , ]} /> + {successMessage && ( + <> + + + + )} {isLoading && (

Loading

)} - {isError &&

We encountered an error while loading.

} + {isPermissionDenied && ( + +

You do not have permission to view entities.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} {isSuccess && !data && } {isSuccess && data && }
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setSubmitErrorMessage(null); + }} + onSubmit={handleCreateSubmit} + isSubmitting={applyEntity.isLoading} + submitError={submitErrorMessage} + /> + )}
); }; diff --git a/ui/src/pages/entities/useLoadEntity.ts b/ui/src/pages/entities/useLoadEntity.ts index fdb4a7968f1..cf20c33bd8f 100644 --- a/ui/src/pages/entities/useLoadEntity.ts +++ b/ui/src/pages/entities/useLoadEntity.ts @@ -1,24 +1,18 @@ -import { useContext } from "react"; import { useParams } from "react-router-dom"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import useResourceQuery, { + entityDetailPath, +} from "../../queries/useResourceQuery"; const useLoadEntity = (entityName: string) => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.entities?.find( - (fv) => fv?.spec?.name === entityName, - ); - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: `entity:${entityName}`, + project: projectName, + restPath: entityDetailPath(entityName, projectName || ""), + restSelect: (d) => d, + enabled: !!entityName, + }); }; export default useLoadEntity; diff --git a/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx b/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx index a8080d0a68b..e2905f1fd02 100644 --- a/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx +++ b/ui/src/pages/feature-services/FeatureServiceIndexEmptyState.tsx @@ -2,28 +2,41 @@ import React from "react"; import { EuiEmptyPrompt, EuiTitle, EuiLink, EuiButton } from "@elastic/eui"; import FeastIconBlue from "../../graphics/FeastIconBlue"; -const FeatureServiceIndexEmptyState = () => { +interface FeatureServiceIndexEmptyStateProps { + onCreate?: () => void; +} + +const FeatureServiceIndexEmptyState: React.FC< + FeatureServiceIndexEmptyStateProps +> = ({ onCreate }) => { return ( There are no feature services} body={

- This project does not have any Feature Services. Learn more about - creating Feature Services in Feast Docs. + Feature services group related features from one or more feature views + for training or online serving. Create your first feature service to + get started.

} actions={ - { - window.open( - "https://docs.feast.dev/getting-started/concepts/feature-retrieval#feature-services", - "_blank", - ); - }} - > - Open Feature Services Docs - + onCreate ? ( + + Create Feature Service + + ) : ( + { + window.open( + "https://docs.feast.dev/getting-started/concepts/feature-retrieval#feature-services", + "_blank", + ); + }} + > + Open Feature Services Docs + + ) } footer={ <> diff --git a/ui/src/pages/feature-services/FeatureServiceInstance.tsx b/ui/src/pages/feature-services/FeatureServiceInstance.tsx index b88d2f4bdbf..4c16b383e94 100644 --- a/ui/src/pages/feature-services/FeatureServiceInstance.tsx +++ b/ui/src/pages/feature-services/FeatureServiceInstance.tsx @@ -1,11 +1,24 @@ -import React from "react"; +import React, { useState } from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, +} from "@elastic/eui"; import { FeatureServiceIcon } from "../../graphics/FeatureServiceIcon"; import { useMatchExact } from "../../hooks/useMatchSubpath"; import FeatureServiceOverviewTab from "./FeatureServiceOverviewTab"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import FeatureServiceFormModal, { + FeatureServiceFormData, +} from "../../components/FeatureServiceFormModal"; +import { + useApplyFeatureService, + useDeleteFeatureService, +} from "../../queries/mutations/useFeatureServiceMutations"; +import useLoadFeatureService from "./useLoadFeatureService"; import { useFeatureServiceCustomTabs, @@ -14,19 +27,107 @@ import { const FeatureServiceInstance = () => { const navigate = useNavigate(); - let { featureServiceName } = useParams(); + let { featureServiceName, projectName } = useParams(); useDocumentTitle(`${featureServiceName} | Feature Service | Feast`); const { customNavigationTabs } = useFeatureServiceCustomTabs(navigate); const CustomTabRoutes = useFeatureServiceCustomTabRoutes(); + const { data } = useLoadFeatureService(featureServiceName || ""); + const deleteFeatureService = useDeleteFeatureService(); + const applyFeatureService = useApplyFeatureService(); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editError, setEditError] = useState(null); + + const handleDelete = () => { + deleteFeatureService.mutate( + { name: featureServiceName || "", project: projectName || "" }, + { + onSuccess: () => { + navigate(`/p/${projectName}/feature-service`); + }, + }, + ); + }; + + const buildInitialEditData = (): FeatureServiceFormData | undefined => { + if (!data?.spec) return undefined; + const spec = data.spec; + return { + name: spec.name || featureServiceName || "", + description: spec.description || "", + owner: spec.owner || "", + projections: (spec.features || []).map((proj: any) => ({ + featureViewName: proj.featureViewName || "", + featureNames: (proj.featureColumns || []) + .map((col: any) => col.name) + .filter(Boolean), + })), + tags: Object.entries(spec.tags || {}).map(([key, value]) => ({ + key, + value: value as string, + })), + }; + }; + + const handleEditSubmit = (formData: FeatureServiceFormData) => { + const payload = { + name: formData.name, + project: projectName || "", + features: formData.projections.map((projection) => ({ + feature_view_name: projection.featureViewName, + feature_names: projection.featureNames, + })), + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags + .filter((tag) => tag.key.trim()) + .map((tag) => [tag.key, tag.value]), + ), + }; + applyFeatureService.mutate(payload, { + onSuccess: () => { + setIsEditModalOpen(false); + setEditError(null); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setEditError(message); + }, + }); + }; + return ( { + setEditError(null); + setIsEditModalOpen(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={[ { label: "Overview", @@ -44,6 +145,37 @@ const FeatureServiceInstance = () => { {CustomTabRoutes}
+ + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteFeatureService.isLoading} + > +

+ This will permanently remove the feature service. This action cannot + be undone. +

+
+ )} + + {isEditModalOpen && ( + { + setIsEditModalOpen(false); + setEditError(null); + }} + onSubmit={handleEditSubmit} + initialData={buildInitialEditData()} + isEdit={true} + isSubmitting={applyFeatureService.isLoading} + submitError={editError} + /> + )}
); }; diff --git a/ui/src/pages/feature-services/FeatureServiceListingTable.tsx b/ui/src/pages/feature-services/FeatureServiceListingTable.tsx index acc68b6e619..c26b976e197 100644 --- a/ui/src/pages/feature-services/FeatureServiceListingTable.tsx +++ b/ui/src/pages/feature-services/FeatureServiceListingTable.tsx @@ -29,8 +29,8 @@ const FeatureServiceListingTable = ({ name: "Name", field: "spec.name", render: (name: string, item: feast.core.IFeatureService) => { - // For "All Projects" view, link to the specific project - const itemProject = item?.spec?.project || projectName; + const itemProject = + item?.spec?.project || (item as any)?.project || projectName; return ( {name} @@ -41,10 +41,12 @@ const FeatureServiceListingTable = ({ { name: "# of Features", field: "spec.features", - render: (featureViews: feast.core.IFeatureViewProjection[]) => { - var numFeatures = 0; - featureViews.forEach((featureView) => { - numFeatures += featureView.featureColumns!.length; + render: ( + featureViews: feast.core.IFeatureViewProjection[] | undefined, + ) => { + let numFeatures = 0; + (featureViews || []).forEach((featureView) => { + numFeatures += (featureView.featureColumns || []).length; }); return numFeatures; }, @@ -58,11 +60,10 @@ const FeatureServiceListingTable = ({ }, ]; - // Add Project column when viewing all projects if (projectName === "all") { columns.splice(1, 0, { name: "Project", - field: "spec.project", + field: "project", sortable: true, render: (project: string) => { return project || "Unknown"; diff --git a/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx b/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx index be922e41261..f1ac3c5e349 100644 --- a/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx +++ b/ui/src/pages/feature-services/FeatureServiceOverviewTab.tsx @@ -34,11 +34,19 @@ const FeatureServiceOverviewTab = () => { const isEmpty = data === undefined; let numFeatures = 0; - let numFeatureViews = 0; + let numLabels = 0; + const featureProjections: any[] = []; + const labelProjections: any[] = []; if (data) { - data?.spec?.features?.forEach((featureView) => { - numFeatureViews += 1; - numFeatures += featureView?.featureColumns!.length; + data?.spec?.features?.forEach((featureView: any) => { + const columnCount = (featureView?.featureColumns || []).length; + if (featureView.viewType === "labelView") { + numLabels += columnCount; + labelProjections.push(featureView); + } else { + numFeatures += columnCount; + featureProjections.push(featureView); + } }); } @@ -57,7 +65,7 @@ const FeatureServiceOverviewTab = () => { - + @@ -66,7 +74,7 @@ const FeatureServiceOverviewTab = () => { @@ -90,14 +98,43 @@ const FeatureServiceOverviewTab = () => {

Features

- {data?.spec?.features ? ( - + {featureProjections.length > 0 ? ( + ) : ( No features specified for this feature service. )} + {labelProjections.length > 0 && ( + + + + + + + + +

from

+
+
+ + + +
+ + + +

Labels

+
+ + +
+
+ )} @@ -153,16 +190,28 @@ const FeatureServiceOverviewTab = () => { -

All Feature Views

+

All Views

{data?.spec?.features?.length! > 0 ? ( { + data?.spec?.features?.map((f: any) => { return f.featureViewName!; })! } + viewTypes={ + data?.spec?.features?.reduce( + (acc: Record, f: any) => { + if (f.featureViewName) { + acc[f.featureViewName] = + f.viewType || "featureView"; + } + return acc; + }, + {}, + ) || {} + } /> ) : ( No feature views in this feature service diff --git a/ui/src/pages/feature-services/Index.tsx b/ui/src/pages/feature-services/Index.tsx index 260a9b821dc..b50b6066a24 100644 --- a/ui/src/pages/feature-services/Index.tsx +++ b/ui/src/pages/feature-services/Index.tsx @@ -1,19 +1,20 @@ -import React, { useContext } from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; import { EuiPageTemplate, EuiLoadingSpinner, - EuiTitle, EuiSpacer, + EuiTitle, + EuiFieldSearch, EuiFlexGroup, EuiFlexItem, - EuiFieldSearch, + EuiButton, + EuiCallOut, } from "@elastic/eui"; import { FeatureServiceIcon } from "../../graphics/FeatureServiceIcon"; -import useLoadRegistry from "../../queries/useLoadRegistry"; import FeatureServiceListingTable from "./FeatureServiceListingTable"; import { useSearchQuery, @@ -22,27 +23,29 @@ import { tagTokenGroupsType, } from "../../hooks/useSearchInputWithTags"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; import FeatureServiceIndexEmptyState from "./FeatureServiceIndexEmptyState"; import TagSearch from "../../components/TagSearch"; import ExportButton from "../../components/ExportButton"; +import FeatureServiceFormModal, { + FeatureServiceFormData, +} from "../../components/FeatureServiceFormModal"; +import { useApplyFeatureService } from "../../queries/mutations/useFeatureServiceMutations"; import { useFeatureServiceTagsAggregation } from "../../hooks/useTagsAggregation"; import { feast } from "../../protos"; +import useResourceQuery, { + featureServiceListPath, + featureViewListPath, + restFeatureViewsToMergedList, +} from "../../queries/useResourceQuery"; const useLoadFeatureServices = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.featureServices; - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: "feature-services-list", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices, + }); }; const shouldIncludeFSsGivenTokenGroups = ( @@ -54,7 +57,7 @@ const shouldIncludeFSsGivenTokenGroups = ( if (entryTagValue) { return values.every((value) => { - return value.length > 0 ? entryTagValue.indexOf(value) >= 0 : true; // Don't filter if the string is empty + return value.length > 0 ? entryTagValue.indexOf(value) >= 0 : true; }); } else { return false; @@ -88,10 +91,46 @@ const filterFn = ( return filteredByTags; }; +const formDataToPayload = ( + formData: FeatureServiceFormData, + project: string, +) => ({ + name: formData.name, + project, + features: formData.projections.map((projection) => ({ + feature_view_name: projection.featureViewName, + feature_names: projection.featureNames, + })), + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags + .filter((tag) => tag.key.trim()) + .map((tag) => [tag.key, tag.value]), + ), +}); + const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadFeatureServices(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadFeatureServices(); + const isAllProjects = projectName === "all"; const tagAggregationQuery = useFeatureServiceTagsAggregation(); + const featureViewsQuery = useResourceQuery({ + resourceType: "feature-views-list-fs-prereq", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: restFeatureViewsToMergedList, + enabled: !isAllProjects, + }); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [prereqWarning, setPrereqWarning] = useState(null); + const applyFeatureService = useApplyFeatureService(); + useDocumentTitle(`Feature Services | Feast`); const { searchString, searchTokens, setSearchString } = useSearchQuery(); @@ -112,6 +151,40 @@ const Index = () => { ? filterFn(data, { tagTokenGroups, searchTokens }) : data; + const handleCreateClick = () => { + const featureViews = featureViewsQuery.data || []; + if (featureViews.length === 0) { + setPrereqWarning( + "Feature services require at least one feature view. Create a feature view first, or proceed and add views later.", + ); + } else { + setPrereqWarning(null); + } + setIsModalOpen(true); + }; + + const handleCreateSubmit = (formData: FeatureServiceFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyFeatureService.mutate(payload, { + onSuccess: () => { + setIsModalOpen(false); + setErrorMessage(null); + setPrereqWarning(null); + setSuccessMessage( + `Feature service "${formData.name}" created successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setErrorMessage(message); + }, + }); + }; + + const showEmptyState = isSuccess && (!data || data.length === 0); + return ( { iconType={FeatureServiceIcon} pageTitle="Feature Services" rightSideItems={[ + ...(isAllProjects + ? [] + : [ + + Create Feature Service + , + ]), , ]} /> + {successMessage && ( + <> + + + + )} + {prereqWarning && !isModalOpen && ( + <> + + + + )} {isLoading && (

Loading

)} - {isError &&

We encountered an error while loading.

} - {isSuccess && !data && } - {isSuccess && filterResult && ( + {isPermissionDenied && ( + +

You do not have permission to view feature services.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} + {showEmptyState && ( + + )} + {isSuccess && filterResult && filterResult.length > 0 && ( @@ -169,6 +288,18 @@ const Index = () => { )}
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setErrorMessage(null); + }} + onSubmit={handleCreateSubmit} + isSubmitting={applyFeatureService.isLoading} + submitError={errorMessage} + /> + )}
); }; diff --git a/ui/src/pages/feature-services/useLoadFeatureService.ts b/ui/src/pages/feature-services/useLoadFeatureService.ts index 004ab35b927..81fff2e931d 100644 --- a/ui/src/pages/feature-services/useLoadFeatureService.ts +++ b/ui/src/pages/feature-services/useLoadFeatureService.ts @@ -1,53 +1,50 @@ -import { FEAST_FCO_TYPES } from "../../parsers/types"; -import { useContext } from "react"; import { useParams } from "react-router-dom"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; - -import useLoadRegistry from "../../queries/useLoadRegistry"; import { EntityReference } from "../../parsers/parseEntityRelationships"; +import useResourceQuery, { + featureServiceDetailPath, +} from "../../queries/useResourceQuery"; const useLoadFeatureService = (featureServiceName: string) => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.featureServices?.find( - (fs) => fs?.spec?.name === featureServiceName, - ); + const fsQuery = useResourceQuery({ + resourceType: `feature-service:${featureServiceName}`, + project: projectName, + restPath: featureServiceDetailPath(featureServiceName, projectName || ""), + restSelect: (d) => ({ + featureService: d, + indirectRelationships: d?.relationships || [], + permissions: d?.permissions || [], + }), + enabled: !!featureServiceName, + }); + + const featureService = fsQuery.data?.featureService; + const indirectRelationships = fsQuery.data?.indirectRelationships || []; + const permissions = fsQuery.data?.permissions || []; - let entities = - data === undefined + let entities: EntityReference[] | undefined = + featureService === undefined ? undefined - : registryQuery.data?.indirectRelationships - .filter((relationship) => { - return ( - relationship.target.type === FEAST_FCO_TYPES.featureService && - relationship.target.name === data?.spec?.name && - relationship.source.type === FEAST_FCO_TYPES.entity - ); - }) - .map((relationship) => { - return relationship.source; - }); - // Deduplicate on name of entity + : indirectRelationships + .filter( + (rel: any) => + rel?.target?.type === "featureService" && + rel?.source?.type === "entity", + ) + .map((rel: any) => rel.source); + if (entities) { - let entityToName: { [key: string]: EntityReference } = {}; - for (let entity of entities) { + const entityToName: { [key: string]: EntityReference } = {}; + for (const entity of entities) { entityToName[entity.name] = entity; } entities = Object.values(entityToName); } + return { - ...registryQuery, - data: data - ? { - ...data, - permissions: registryQuery.data?.permissions, - } - : undefined, + ...fsQuery, + data: featureService ? { ...featureService, permissions } : undefined, entities, }; }; diff --git a/ui/src/pages/feature-views/CurlGeneratorTab.tsx b/ui/src/pages/feature-views/CurlGeneratorTab.tsx index 147921458ee..5c83440a2a0 100644 --- a/ui/src/pages/feature-views/CurlGeneratorTab.tsx +++ b/ui/src/pages/feature-views/CurlGeneratorTab.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { EuiPanel, EuiTitle, @@ -16,23 +16,22 @@ import { import { CodeBlock, github } from "react-code-blocks"; import { RegularFeatureViewCustomTabProps } from "../../custom-tabs/types"; +const defaultServerUrl = + process.env.REACT_APP_FEAST_FEATURE_SERVER_URL || "http://localhost:6566"; + const CurlGeneratorTab = ({ feastObjectQuery, }: RegularFeatureViewCustomTabProps) => { const data = feastObjectQuery.data as any; const [serverUrl, setServerUrl] = useState(() => { const savedUrl = localStorage.getItem("feast-feature-server-url"); - return savedUrl || "http://localhost:6566"; + return savedUrl || defaultServerUrl; }); const [entityValues, setEntityValues] = useState>({}); const [selectedFeatures, setSelectedFeatures] = useState< Record >({}); - useEffect(() => { - localStorage.setItem("feast-feature-server-url", serverUrl); - }, [serverUrl]); - if (feastObjectQuery.isLoading) { return Loading...; } @@ -106,8 +105,12 @@ const CurlGeneratorTab = ({ setServerUrl(e.target.value)} - placeholder="http://localhost:6566" + onChange={(e) => { + const nextValue = e.target.value; + setServerUrl(nextValue); + localStorage.setItem("feast-feature-server-url", nextValue); + }} + placeholder={defaultServerUrl} /> diff --git a/ui/src/pages/feature-views/FeatureViewListingTable.tsx b/ui/src/pages/feature-views/FeatureViewListingTable.tsx index e865abe6e74..9fd6f8f8fd7 100644 --- a/ui/src/pages/feature-views/FeatureViewListingTable.tsx +++ b/ui/src/pages/feature-views/FeatureViewListingTable.tsx @@ -31,7 +31,10 @@ const FeatureViewListingTable = ({ sortable: true, render: (name: string, item: genericFVType) => { // For "All Projects" view, link to the specific project - const itemProject = item.object?.spec?.project || projectName; + const itemProject = + item.object?.spec?.project || + (item.object as any)?.project || + projectName; return ( {name}{" "} @@ -49,6 +52,13 @@ const FeatureViewListingTable = ({ return features.length; }, }, + { + name: "Version", + render: (item: genericFVType) => { + const ver = (item.object as any)?.meta?.currentVersionNumber; + return ver != null && ver > 0 ? `v${ver}` : "—"; + }, + }, ]; // Add Project column when viewing all projects @@ -56,7 +66,13 @@ const FeatureViewListingTable = ({ columns.splice(1, 0, { name: "Project", render: (item: genericFVType) => { - return {item.object?.spec?.project || "Unknown"}; + return ( + + {item.object?.spec?.project || + (item.object as any)?.project || + "Unknown"} + + ); }, }); } diff --git a/ui/src/pages/feature-views/FeatureViewUsagePanel.tsx b/ui/src/pages/feature-views/FeatureViewUsagePanel.tsx new file mode 100644 index 00000000000..71eaebedd02 --- /dev/null +++ b/ui/src/pages/feature-views/FeatureViewUsagePanel.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import { + EuiBadge, + EuiBasicTable, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiPanel, + EuiText, + EuiTitle, + EuiToolTip, +} from "@elastic/eui"; +import useLoadFeatureUsage, { + FeatureUsageEntry, +} from "../../queries/useLoadFeatureUsage"; + +interface FeatureViewUsagePanelProps { + featureViewName: string; +} + +const formatTimestamp = (ts: number | null): string => { + if (ts == null) return "Never"; + const date = new Date(ts); + return date.toLocaleString(); +}; + +const formatRelativeTime = (ts: number | null): string => { + if (ts == null) return ""; + const now = Date.now(); + const diffMs = now - ts; + const diffMin = Math.floor(diffMs / 60000); + if (diffMin < 1) return "just now"; + if (diffMin < 60) return `${diffMin}m ago`; + const diffHr = Math.floor(diffMin / 60); + if (diffHr < 24) return `${diffHr}h ago`; + const diffDays = Math.floor(diffHr / 24); + return `${diffDays}d ago`; +}; + +const FeatureViewUsagePanel = ({ + featureViewName, +}: FeatureViewUsagePanelProps) => { + const { data, isLoading, isError } = useLoadFeatureUsage(); + + if (isLoading) { + return ( + + +

MLflow Usage

+
+ + + + + + +
+ ); + } + + if (isError || !data || !data.mlflow_enabled) { + return null; + } + + const usage: FeatureUsageEntry | undefined = + data.feature_usage?.[featureViewName]; + + if (!usage || usage.run_count === 0) { + return ( + + +

MLflow Usage

+
+ + + No MLflow training runs have used this feature view. + +
+ ); + } + + const modelItems = usage.models.map((name) => ({ name })); + + const modelColumns = [ + { + name: "Registered Model", + field: "name", + render: (name: string) => {name}, + }, + ]; + + return ( + + +

MLflow Usage

+
+ + + + + Training runs: {usage.run_count} + + + + + + Last used: {formatRelativeTime(usage.last_used)} + + + + + {modelItems.length > 0 && ( + <> + + + Registered models using this feature view: + + + + )} +
+ ); +}; + +export default FeatureViewUsagePanel; diff --git a/ui/src/pages/feature-views/FeatureViewVersionsTab.tsx b/ui/src/pages/feature-views/FeatureViewVersionsTab.tsx new file mode 100644 index 00000000000..f388d247da4 --- /dev/null +++ b/ui/src/pages/feature-views/FeatureViewVersionsTab.tsx @@ -0,0 +1,268 @@ +import React, { useContext, useState, useMemo } from "react"; +import { + EuiBasicTable, + EuiText, + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import { feast } from "../../protos"; +import { toDate } from "../../utils/timestamp"; +import { useParams } from "react-router-dom"; + +interface FeatureViewVersionsTabProps { + featureViewName: string; +} + +interface DecodedVersion { + record: feast.core.IFeatureViewVersionRecord; + features: feast.core.IFeatureSpecV2[]; + entities: string[]; + description: string; + udfBody: string | null; +} + +const decodeVersionProto = ( + record: feast.core.IFeatureViewVersionRecord, +): DecodedVersion => { + const result: DecodedVersion = { + record, + features: [], + entities: [], + description: "", + udfBody: null, + }; + + if (!record.featureViewProto || record.featureViewProto.length === 0) { + return result; + } + + try { + const bytes = + record.featureViewProto instanceof Uint8Array + ? record.featureViewProto + : new Uint8Array(record.featureViewProto); + + if (record.featureViewType === "on_demand_feature_view") { + const odfv = feast.core.OnDemandFeatureView.decode(bytes); + result.features = odfv.spec?.features || []; + result.description = odfv.spec?.description || ""; + result.udfBody = + odfv.spec?.featureTransformation?.userDefinedFunction?.bodyText || + odfv.spec?.userDefinedFunction?.bodyText || + null; + } else if (record.featureViewType === "stream_feature_view") { + const sfv = feast.core.StreamFeatureView.decode(bytes); + result.features = sfv.spec?.features || []; + result.entities = sfv.spec?.entities || []; + result.description = sfv.spec?.description || ""; + } else if (record.featureViewType === "label_view") { + const lv = feast.core.LabelView.decode(bytes); + result.features = lv.spec?.features || []; + result.entities = lv.spec?.entities || []; + result.description = lv.spec?.description || ""; + } else { + const fv = feast.core.FeatureView.decode(bytes); + result.features = fv.spec?.features || []; + result.entities = fv.spec?.entities || []; + result.description = fv.spec?.description || ""; + } + } catch (e) { + console.error("Failed to decode version proto:", e); + } + + return result; +}; + +const VersionDetail = ({ decoded }: { decoded: DecodedVersion }) => { + return ( + + {decoded.description && ( + + + {decoded.description} + + + )} + {decoded.udfBody && ( + + + +

Transformation

+
+ + + {decoded.udfBody} + +
+
+ )} + + + + +

Features ({decoded.features.length})

+
+ + {decoded.features.length > 0 ? ( + + typeof vt === "string" + ? vt + : feast.types.ValueType.Enum[vt] || String(vt || ""), + }, + ]} + /> + ) : ( + No features in this version. + )} +
+
+ {decoded.entities.length > 0 && ( + + + +

Entities

+
+ + + {decoded.entities.map((entity) => ( + + {entity} + + ))} + +
+
+ )} +
+
+ ); +}; + +const FeatureViewVersionsTab = ({ + featureViewName, +}: FeatureViewVersionsTabProps) => { + const registryUrl = useContext(RegistryPathContext); + const { projectName } = useParams(); + const registryQuery = useLoadRegistry(registryUrl, projectName); + const [expandedRows, setExpandedRows] = useState>({}); + + const records = useMemo( + () => + registryQuery.data?.objects?.featureViewVersionHistory?.records?.filter( + (r: feast.core.IFeatureViewVersionRecord) => + r.featureViewName === featureViewName, + ) || [], + [ + registryQuery.data?.objects?.featureViewVersionHistory?.records, + featureViewName, + ], + ); + + const decodedVersions: DecodedVersion[] = useMemo( + () => records.map(decodeVersionProto), + [records], + ); + + if (records.length === 0) { + return No version history available.; + } + + const toggleRow = (versionNumber: number) => { + setExpandedRows((prev) => ({ + ...prev, + [versionNumber]: !prev[versionNumber], + })); + }; + + const columns = [ + { + field: "record.versionNumber", + name: "Version", + render: (_: unknown, item: DecodedVersion) => + `v${item.record.versionNumber}`, + sortable: true, + width: "80px", + }, + { + name: "Features", + render: (item: DecodedVersion) => `${item.features.length}`, + width: "80px", + }, + { + field: "record.featureViewType", + name: "Type", + render: (_: unknown, item: DecodedVersion) => + item.record.featureViewType || "—", + }, + { + field: "record.createdTimestamp", + name: "Created", + render: (_: unknown, item: DecodedVersion) => + item.record.createdTimestamp + ? toDate(item.record.createdTimestamp).toLocaleString() + : "—", + }, + { + field: "record.versionId", + name: "Version ID", + render: (_: unknown, item: DecodedVersion) => + item.record.versionId || "—", + }, + ]; + + const itemIdToExpandedRowMap: Record = {}; + decodedVersions.forEach((decoded) => { + const vn = decoded.record.versionNumber!; + if (expandedRows[vn]) { + itemIdToExpandedRowMap[String(vn)] = ; + } + }); + + return ( + String(item.record.versionNumber)} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + columns={[ + { + isExpander: true, + width: "40px", + render: (item: DecodedVersion) => { + const vn = item.record.versionNumber!; + return ( + + ); + }, + }, + ...columns, + ]} + /> + ); +}; + +export default FeatureViewVersionsTab; diff --git a/ui/src/pages/feature-views/Index.tsx b/ui/src/pages/feature-views/Index.tsx index b1c28895370..879aa717574 100644 --- a/ui/src/pages/feature-views/Index.tsx +++ b/ui/src/pages/feature-views/Index.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; import { @@ -9,11 +9,12 @@ import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, + EuiButton, + EuiCallOut, } from "@elastic/eui"; import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; -import useLoadRegistry from "../../queries/useLoadRegistry"; import FeatureViewListingTable from "./FeatureViewListingTable"; import { filterInputInterface, @@ -22,26 +23,29 @@ import { } from "../../hooks/useSearchInputWithTags"; import { genericFVType, regularFVInterface } from "../../parsers/mergedFVTypes"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; import FeatureViewIndexEmptyState from "./FeatureViewIndexEmptyState"; import { useFeatureViewTagsAggregation } from "../../hooks/useTagsAggregation"; import TagSearch from "../../components/TagSearch"; import ExportButton from "../../components/ExportButton"; +import FeatureViewFormModal, { + FeatureViewFormData, +} from "../../components/FeatureViewFormModal"; +import { useApplyFeatureView } from "../../queries/mutations/useFeatureViewMutations"; +import useResourceQuery, { + featureViewListPath, + restFeatureViewsToMergedList, + entityListPath, + dataSourceListPath, +} from "../../queries/useResourceQuery"; const useLoadFeatureViews = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const registryQuery = useLoadRegistry(registryUrl, projectName); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.mergedFVList; - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: "feature-views-list", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: restFeatureViewsToMergedList, + }); }; const shouldIncludeFVsGivenTokenGroups = ( @@ -49,13 +53,12 @@ const shouldIncludeFVsGivenTokenGroups = ( tagTokenGroups: Record, ) => { return Object.entries(tagTokenGroups).every(([key, values]) => { - const entryTagValue = entry?.object?.spec!.tags - ? entry.object.spec.tags[key] - : undefined; + const tags = entry?.object?.spec?.tags; + const entryTagValue = tags ? (tags as any)[key] : undefined; if (entryTagValue) { return values.every((value) => { - return value.length > 0 ? entryTagValue.indexOf(value) >= 0 : true; // Don't filter if the string is empty + return value.length > 0 ? entryTagValue.indexOf(value) >= 0 : true; }); } else { return false; @@ -74,7 +77,7 @@ const filterFn = (data: genericFVType[], filterInput: filterInputInterface) => { filterInput.tagTokenGroups, ); } else { - return false; // ODFVs don't have tags yet + return false; } }); } @@ -90,9 +93,73 @@ const filterFn = (data: genericFVType[], filterInput: filterInputInterface) => { return filteredByTags; }; +const TTL_UNITS: Record = { + days: 86400, + hours: 3600, + minutes: 60, + seconds: 1, +}; + +const formDataToPayload = (formData: FeatureViewFormData, project: string) => ({ + name: formData.name, + project, + entities: formData.entities, + features: formData.features.map((f) => ({ + name: f.name, + value_type: parseInt(f.valueType, 10), + description: f.description, + })), + batch_source: formData.batchSource, + ttl_seconds: formData.ttlValue * (TTL_UNITS[formData.ttlUnit] || 1), + online: formData.online, + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), +}); + const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadFeatureViews(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadFeatureViews(); + const isAllProjects = projectName === "all"; + + const entitiesQuery = useResourceQuery({ + resourceType: "entities-list-fv-prereq", + project: projectName, + restPath: entityListPath(projectName), + restSelect: (d) => d.entities, + }); + const dataSourcesQuery = useResourceQuery({ + resourceType: "data-sources-list-fv-prereq", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources, + }); + const tagAggregationQuery = useFeatureViewTagsAggregation(); + const [isModalOpen, setIsModalOpen] = useState(false); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [prereqWarning, setPrereqWarning] = useState(null); + const applyFeatureView = useApplyFeatureView(); + + const handleCreateClick = () => { + const missingDeps: string[] = []; + const entities = entitiesQuery.data || []; + const dataSources = dataSourcesQuery.data || []; + + if (entities.length === 0) missingDeps.push("entities"); + if (dataSources.length === 0) missingDeps.push("data sources"); + + if (missingDeps.length > 0) { + setPrereqWarning( + `Feature views require at least one entity and one data source. Missing: ${missingDeps.join(" and ")}. You can still proceed — the form will let you create them inline.`, + ); + } + setIsModalOpen(true); + }; useDocumentTitle(`Feature Views | Feast`); @@ -114,6 +181,27 @@ const Index = () => { ? filterFn(data, { tagTokenGroups, searchTokens }) : data; + const handleCreateSubmit = (formData: FeatureViewFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyFeatureView.mutate(payload, { + onSuccess: () => { + setIsModalOpen(false); + setErrorMessage(null); + setPrereqWarning(null); + setSuccessMessage( + `Feature view "${formData.name}" created successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + // Error shown inside the modal via submitError prop + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setErrorMessage(message); + }, + }); + }; + return ( { iconType={FeatureViewIcon} pageTitle="Feature Views" rightSideItems={[ + ...(isAllProjects + ? [] + : [ + + Create Feature View + , + ]), , ]} /> + {prereqWarning && ( + <> + +

{prereqWarning}

+
+ + + )} + {successMessage && ( + <> + + + + )} + {errorMessage && ( + <> + + + + )} {isLoading && (

Loading

)} - {isError &&

We encountered an error while loading.

} + {isPermissionDenied && ( + +

You do not have permission to view feature views.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} {isSuccess && data?.length === 0 && } {isSuccess && data && data.length > 0 && filterResult && ( @@ -171,6 +314,19 @@ const Index = () => { )}
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setPrereqWarning(null); + setErrorMessage(null); + }} + onSubmit={handleCreateSubmit} + isSubmitting={applyFeatureView.isLoading} + submitError={errorMessage} + /> + )}
); }; diff --git a/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx b/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx index 5a4b48f6d6d..70824219aaa 100644 --- a/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/OnDemandFeatureViewInstance.tsx @@ -1,11 +1,12 @@ import React from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { EuiBadge, EuiPageTemplate } from "@elastic/eui"; import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; -import { useMatchExact } from "../../hooks/useMatchSubpath"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import OnDemandFeatureViewOverviewTab from "./OnDemandFeatureViewOverviewTab"; +import FeatureViewVersionsTab from "./FeatureViewVersionsTab"; import { useOnDemandFeatureViewCustomTabs, @@ -29,7 +30,17 @@ const OnDemandFeatureInstance = ({ data }: OnDemandFeatureInstanceProps) => { + {featureViewName} + {data?.meta?.currentVersionNumber != null && + data.meta.currentVersionNumber > 0 && ( + + v{data.meta.currentVersionNumber} + + )} + + } tabs={[ { label: "Overview", @@ -38,6 +49,13 @@ const OnDemandFeatureInstance = ({ data }: OnDemandFeatureInstanceProps) => { navigate(""); }, }, + { + label: "Versions", + isSelected: useMatchSubpath("versions"), + onClick: () => { + navigate("versions"); + }, + }, ...customNavigationTabs, ]} /> @@ -47,6 +65,12 @@ const OnDemandFeatureInstance = ({ data }: OnDemandFeatureInstanceProps) => { path="/" element={} /> + + } + /> {CustomTabRoutes} diff --git a/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx b/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx index 48d61e45f8f..b800a861481 100644 --- a/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx @@ -1,12 +1,26 @@ -import React, { useContext } from "react"; -import { Route, Routes, useNavigate } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import React, { useContext, useState } from "react"; +import { Route, Routes, useNavigate, useParams } from "react-router-dom"; +import { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, + EuiPageTemplate, +} from "@elastic/eui"; import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import RegularFeatureViewOverviewTab from "./RegularFeatureViewOverviewTab"; import FeatureViewLineageTab from "./FeatureViewLineageTab"; +import FeatureViewVersionsTab from "./FeatureViewVersionsTab"; +import FeatureViewFormModal, { + FeatureViewFormData, +} from "../../components/FeatureViewFormModal"; +import { + useApplyFeatureView, + useDeleteFeatureView, +} from "../../queries/mutations/useFeatureViewMutations"; import { useRegularFeatureViewCustomTabs, @@ -20,12 +34,69 @@ interface RegularFeatureInstanceProps { permissions?: any[]; } +const buildEditFormData = ( + fv: feast.core.IFeatureView, +): FeatureViewFormData => { + const tags = fv.spec?.tags + ? Object.entries(fv.spec.tags).map(([key, value]) => ({ key, value })) + : []; + + const features = (fv.spec?.features || []).map((f) => ({ + name: f.name || "", + valueType: String(f.valueType ?? 0), + description: f.description || "", + })); + + let ttlValue = 0; + let ttlUnit = "seconds"; + if (fv.spec?.ttl?.seconds) { + const secs = + typeof fv.spec.ttl.seconds === "number" + ? fv.spec.ttl.seconds + : ((fv.spec.ttl.seconds as any).toNumber?.() ?? 0); + if (secs > 0 && secs % 86400 === 0) { + ttlValue = secs / 86400; + ttlUnit = "days"; + } else if (secs > 0 && secs % 3600 === 0) { + ttlValue = secs / 3600; + ttlUnit = "hours"; + } else if (secs > 0 && secs % 60 === 0) { + ttlValue = secs / 60; + ttlUnit = "minutes"; + } else { + ttlValue = secs; + ttlUnit = "seconds"; + } + } + + return { + name: fv.spec?.name || "", + description: fv.spec?.description || "", + owner: fv.spec?.owner || "", + entities: fv.spec?.entities || [], + features, + batchSource: fv.spec?.batchSource?.name || "", + ttlValue, + ttlUnit, + online: fv.spec?.online ?? true, + tags, + }; +}; + +const TTL_UNITS: Record = { + days: 86400, + hours: 3600, + minutes: 60, + seconds: 1, +}; + const RegularFeatureInstance = ({ data, permissions, }: RegularFeatureInstanceProps) => { const { enabledFeatureStatistics } = useContext(FeatureFlagsContext); const navigate = useNavigate(); + const { projectName } = useParams(); const { customNavigationTabs } = useRegularFeatureViewCustomTabs(navigate); let tabs = [ @@ -57,16 +128,104 @@ const RegularFeatureInstance = ({ }); } + tabs.push({ + label: "Versions", + isSelected: useMatchSubpath("versions"), + onClick: () => { + navigate("versions"); + }, + }); + tabs.push(...customNavigationTabs); const TabRoutes = useRegularFeatureViewCustomTabRoutes(); + const applyFeatureView = useApplyFeatureView(); + const deleteFeatureView = useDeleteFeatureView(); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editError, setEditError] = useState(null); + + const handleDelete = () => { + deleteFeatureView.mutate( + { name: data?.spec?.name || "", project: projectName || "" }, + { + onSuccess: () => { + navigate(`/p/${projectName}/feature-view`); + }, + }, + ); + }; + + const handleEditSubmit = (formData: FeatureViewFormData) => { + const payload = { + name: formData.name, + project: projectName || "", + entities: formData.entities, + features: formData.features.map((f) => ({ + name: f.name, + value_type: parseInt(f.valueType, 10), + description: f.description, + })), + batch_source: formData.batchSource, + ttl_seconds: formData.ttlValue * (TTL_UNITS[formData.ttlUnit] || 1), + online: formData.online, + description: formData.description, + owner: formData.owner, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + }; + applyFeatureView.mutate(payload, { + onSuccess: () => { + setIsEditModalOpen(false); + setEditError(null); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setEditError(message); + }, + }); + }; + return ( + {data?.spec?.name} + {data?.meta?.currentVersionNumber != null && + data.meta.currentVersionNumber > 0 && ( + + v{data.meta.currentVersionNumber} + + )} + + } + rightSideItems={[ + { + setEditError(null); + setIsEditModalOpen(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={tabs} /> @@ -84,9 +243,46 @@ const RegularFeatureInstance = ({ path="/lineage" element={} /> + + } + /> {TabRoutes} + + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteFeatureView.isLoading} + > +

+ This will permanently remove the feature view. This action cannot be + undone. +

+
+ )} + + {isEditModalOpen && data && ( + { + setIsEditModalOpen(false); + setEditError(null); + }} + onSubmit={handleEditSubmit} + initialData={buildEditFormData(data)} + isEdit + isSubmitting={applyFeatureView.isLoading} + submitError={editError} + /> + )}
); }; diff --git a/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx b/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx index e766e4fd0ab..e58e690c04e 100644 --- a/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx +++ b/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx @@ -8,6 +8,7 @@ import { EuiStat, EuiText, EuiTitle, + EuiToolTip, } from "@elastic/eui"; import React from "react"; @@ -19,9 +20,11 @@ import { encodeSearchQueryString } from "../../hooks/encodeSearchQueryString"; import { EntityRelation } from "../../parsers/parseEntityRelationships"; import { FEAST_FCO_TYPES } from "../../parsers/types"; import useLoadRelationshipData from "../../queries/useLoadRelationshipsData"; +import useLoadFeatureUsage from "../../queries/useLoadFeatureUsage"; import { getEntityPermissions } from "../../utils/permissionUtils"; import BatchSourcePropertiesView from "../data-sources/BatchSourcePropertiesView"; import ConsumingFeatureServicesList from "./ConsumingFeatureServicesList"; +import FeatureViewUsagePanel from "./FeatureViewUsagePanel"; import { feast } from "../../protos"; import { toDate } from "../../utils/timestamp"; @@ -51,6 +54,7 @@ const RegularFeatureViewOverviewTab = ({ const fvName = featureViewName === undefined ? "" : featureViewName; const relationshipQuery = useLoadRelationshipData(); + const { data: usageData } = useLoadFeatureUsage(); const fsNames = relationshipQuery.data ? relationshipQuery.data.filter(whereFSconsumesThisFv(fvName)).map((fs) => { @@ -59,12 +63,43 @@ const RegularFeatureViewOverviewTab = ({ : []; const numOfFs = fsNames.length; + const fvUsage = usageData?.feature_usage?.[fvName]; + const runCount = fvUsage?.run_count ?? 0; + const lastUsed = fvUsage?.last_used ?? null; + const lastUsedLabel = + lastUsed != null ? new Date(lastUsed).toLocaleDateString() : "N/A"; + return ( + {usageData?.mlflow_enabled && ( + <> + + + + + + + + + + )} @@ -128,6 +163,10 @@ const RegularFeatureViewOverviewTab = ({ )}
+ {usageData?.mlflow_enabled && ( + + )} +

Tags

diff --git a/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx b/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx index 0e22a6c2e5d..c0b9627bca5 100644 --- a/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/StreamFeatureViewInstance.tsx @@ -1,11 +1,12 @@ import React from "react"; import { Route, Routes, useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { EuiBadge, EuiPageTemplate } from "@elastic/eui"; import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; -import { useMatchExact } from "../../hooks/useMatchSubpath"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import StreamFeatureViewOverviewTab from "./StreamFeatureViewOverviewTab"; +import FeatureViewVersionsTab from "./FeatureViewVersionsTab"; import { useStreamFeatureViewCustomTabs, @@ -30,7 +31,17 @@ const StreamFeatureInstance = ({ data }: StreamFeatureInstanceProps) => { restrictWidth paddingSize="l" iconType={FeatureViewIcon} - pageTitle={`${featureViewName}`} + pageTitle={ + <> + {featureViewName} + {data?.meta?.currentVersionNumber != null && + data.meta.currentVersionNumber > 0 && ( + + v{data.meta.currentVersionNumber} + + )} + + } tabs={[ { label: "Overview", @@ -39,6 +50,13 @@ const StreamFeatureInstance = ({ data }: StreamFeatureInstanceProps) => { navigate(""); }, }, + { + label: "Versions", + isSelected: useMatchSubpath("versions"), + onClick: () => { + navigate("versions"); + }, + }, ...customNavigationTabs, ]} /> @@ -48,6 +66,12 @@ const StreamFeatureInstance = ({ data }: StreamFeatureInstanceProps) => { path="/" element={} /> + + } + /> {CustomTabRoutes} diff --git a/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx b/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx index 1e31df47d69..4abb5d02f75 100644 --- a/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx +++ b/ui/src/pages/feature-views/components/FeatureViewProjectionDisplayPanel.tsx @@ -1,9 +1,9 @@ import React from "react"; import { + EuiBadge, EuiBasicTable, EuiPanel, EuiSpacer, - EuiText, EuiTitle, } from "@elastic/eui"; import { useParams } from "react-router-dom"; @@ -17,6 +17,8 @@ const FeatureViewProjectionDisplayPanel = ( featureViewProjection: RequestDataDisplayPanelProps, ) => { const { projectName } = useParams(); + const isLabelView = (featureViewProjection as any).viewType === "labelView"; + const viewPath = isLabelView ? "label-view" : "feature-view"; const columns = [ { @@ -27,20 +29,21 @@ const FeatureViewProjectionDisplayPanel = ( name: "Type", field: "valueType", render: (valueType: any) => { - return feast.types.ValueType.Enum[valueType]; + if (typeof valueType === "string") return valueType; + return feast.types.ValueType.Enum[valueType] || String(valueType || ""); }, }, ]; return ( - - Feature View - + + {isLabelView ? "label view" : "feature view"} + {featureViewProjection?.featureViewName} @@ -48,7 +51,7 @@ const FeatureViewProjectionDisplayPanel = ( ); diff --git a/ui/src/pages/feature-views/useLoadFeatureView.ts b/ui/src/pages/feature-views/useLoadFeatureView.ts index 08e8646f60f..5f88aab0a7d 100644 --- a/ui/src/pages/feature-views/useLoadFeatureView.ts +++ b/ui/src/pages/feature-views/useLoadFeatureView.ts @@ -1,71 +1,56 @@ -import { useContext } from "react"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import { useParams } from "react-router-dom"; +import useResourceQuery, { + featureViewDetailPath, + restFeatureViewDetailToGeneric, +} from "../../queries/useResourceQuery"; +import type { genericFVType } from "../../parsers/mergedFVTypes"; const useLoadFeatureView = (featureViewName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.mergedFVMap[featureViewName]; - - return { - ...registryQuery, - data, - }; + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `feature-view:${featureViewName}`, + project: projectName, + restPath: featureViewDetailPath(featureViewName, projectName || ""), + restSelect: restFeatureViewDetailToGeneric, + enabled: !!featureViewName, + }); }; const useLoadRegularFeatureView = (featureViewName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.featureViews?.find((fv) => { - return fv?.spec?.name === featureViewName; - }); - - return { - ...registryQuery, - data, - }; + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `regular-fv:${featureViewName}`, + project: projectName, + restPath: featureViewDetailPath(featureViewName, projectName || ""), + restSelect: (d) => (d?.type === "featureView" ? d : undefined), + enabled: !!featureViewName, + }); }; const useLoadOnDemandFeatureView = (featureViewName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.onDemandFeatureViews?.find((fv) => { - return fv?.spec?.name === featureViewName; - }); - - return { - ...registryQuery, - data, - }; + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `odfv:${featureViewName}`, + project: projectName, + restPath: featureViewDetailPath(featureViewName, projectName || ""), + restSelect: (d) => (d?.type === "onDemandFeatureView" ? d : undefined), + enabled: !!featureViewName, + }); }; const useLoadStreamFeatureView = (featureViewName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); - - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.streamFeatureViews?.find((fv) => { - return fv.spec?.name === featureViewName; - }); - - return { - ...registryQuery, - data, - }; + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `sfv:${featureViewName}`, + project: projectName, + restPath: featureViewDetailPath(featureViewName, projectName || ""), + restSelect: (d) => (d?.type === "streamFeatureView" ? d : undefined), + enabled: !!featureViewName, + }); }; export default useLoadFeatureView; diff --git a/ui/src/pages/features/FeatureInstance.tsx b/ui/src/pages/features/FeatureInstance.tsx index fe81c6e619f..aa73db7c8c1 100644 --- a/ui/src/pages/features/FeatureInstance.tsx +++ b/ui/src/pages/features/FeatureInstance.tsx @@ -3,8 +3,9 @@ import { Route, Routes, useNavigate, useParams } from "react-router-dom"; import { EuiPageTemplate } from "@elastic/eui"; import { FeatureIcon } from "../../graphics/FeatureIcon"; -import { useMatchExact } from "../../hooks/useMatchSubpath"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import FeatureOverviewTab from "./FeatureOverviewTab"; +import FeatureMonitoringTab from "./FeatureMonitoringTab"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import { useFeatureCustomTabs, @@ -34,12 +35,20 @@ const FeatureInstance = () => { navigate(""); }, }, + { + label: "Monitoring", + isSelected: useMatchSubpath("monitoring"), + onClick: () => { + navigate("monitoring"); + }, + }, ...customNavigationTabs, ]} /> } /> + } /> {CustomTabRoutes} diff --git a/ui/src/pages/features/FeatureListPage.tsx b/ui/src/pages/features/FeatureListPage.tsx index 36087f98bc0..d03ed0ec508 100644 --- a/ui/src/pages/features/FeatureListPage.tsx +++ b/ui/src/pages/features/FeatureListPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useContext } from "react"; +import React, { useState } from "react"; import { EuiBasicTable, EuiTableFieldDataColumnType, @@ -15,13 +15,19 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiBadge, + EuiCallOut, } from "@elastic/eui"; import EuiCustomLink from "../../components/EuiCustomLink"; import ExportButton from "../../components/ExportButton"; import { useParams } from "react-router-dom"; -import useLoadRegistry from "../../queries/useLoadRegistry"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadFeatureModels, { + FeatureModelInfo, +} from "../../queries/useLoadFeatureModels"; import { FeatureIcon } from "../../graphics/FeatureIcon"; +import useResourceQuery, { + featuresListPath, +} from "../../queries/useResourceQuery"; import { FEAST_FCO_TYPES } from "../../parsers/types"; import { getEntityPermissions, @@ -35,6 +41,7 @@ interface Feature { type: string; project?: string; permissions?: any[]; + models?: FeatureModelInfo[]; } type FeatureColumn = @@ -43,11 +50,24 @@ type FeatureColumn = const FeatureListPage = () => { const { projectName } = useParams(); - const registryUrl = useContext(RegistryPathContext); - const { data, isLoading, isError } = useLoadRegistry( - registryUrl, - projectName, - ); + const { + data: features, + isLoading, + isError, + isPermissionDenied, + } = useResourceQuery({ + resourceType: "features-list", + project: projectName, + restPath: featuresListPath(projectName), + restSelect: (d) => d.features, + }); + const { data: permissions } = useResourceQuery({ + resourceType: "permissions", + project: projectName, + restPath: `/permissions?project=${encodeURIComponent(projectName || "")}`, + restSelect: (d) => d.permissions, + }); + const { data: featureModelsData } = useLoadFeatureModels(); const [searchText, setSearchText] = useState(""); const [selectedPermissionAction, setSelectedPermissionAction] = useState(""); @@ -57,27 +77,24 @@ const FeatureListPage = () => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(100); - const featuresWithPermissions: Feature[] = (data?.allFeatures || []).map( - (feature) => { - return { - ...feature, - permissions: getEntityPermissions( - selectedPermissionAction - ? filterPermissionsByAction( - data?.permissions, - selectedPermissionAction, - ) - : data?.permissions, - FEAST_FCO_TYPES.featureView, - feature.featureView, - ), - }; - }, - ); + const featuresWithPermissions: Feature[] = (features || []).map((feature) => { + const featureRef = `${feature.featureView}:${feature.name}`; + return { + ...feature, + models: featureModelsData?.feature_models?.[featureRef] || [], + permissions: getEntityPermissions( + selectedPermissionAction + ? filterPermissionsByAction(permissions, selectedPermissionAction) + : permissions, + FEAST_FCO_TYPES.featureView, + feature.featureView, + ), + }; + }); - const features: Feature[] = featuresWithPermissions; + const enrichedFeatures: Feature[] = featuresWithPermissions; - const filteredFeatures = features.filter((feature) => + const filteredFeatures = enrichedFeatures.filter((feature) => feature.name.toLowerCase().includes(searchText.toLowerCase()), ); @@ -100,7 +117,6 @@ const FeatureListPage = () => { field: "name", sortable: true, render: (name: string, feature: Feature) => { - // For "All Projects" view, link to the specific project const itemProject = feature.project || projectName; return ( { field: "featureView", sortable: true, render: (featureView: string, feature: Feature) => { - // For "All Projects" view, link to the specific project const itemProject = feature.project || projectName; return ( @@ -126,6 +141,47 @@ const FeatureListPage = () => { }, }, { name: "Type", field: "type", sortable: true }, + { + name: "Models", + field: "models", + sortable: false, + render: (models: FeatureModelInfo[]) => { + if (!models || models.length === 0) { + return ( + + -- + + ); + } + if (models.length === 1) { + return ( + + {models[0].model_name} v{models[0].version} + + ); + } + return ( + + {models.map((m) => ( +
+ {m.model_name} v{m.version} +
+ ))} +
+ } + > + {models.length} models + + ); + }, + }, { name: "Permissions", field: "permissions", @@ -203,6 +259,10 @@ const FeatureListPage = () => { {isLoading ? (

Loading...

+ ) : isPermissionDenied ? ( + +

You do not have permission to view features.

+
) : isError ? (

We encountered an error while loading.

) : ( diff --git a/ui/src/pages/features/FeatureMonitoringTab.tsx b/ui/src/pages/features/FeatureMonitoringTab.tsx new file mode 100644 index 00000000000..bc8e2c2cf9f --- /dev/null +++ b/ui/src/pages/features/FeatureMonitoringTab.tsx @@ -0,0 +1,113 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSkeletonText, + EuiEmptyPrompt, + EuiButton, +} from "@elastic/eui"; +import { + useFeatureMetrics, + useBaselineMetrics, +} from "../../queries/useMonitoringApi"; +import type { + NumericHistogram, + CategoricalHistogram, +} from "../../queries/useMonitoringApi"; +import { + NumericHistogramChart, + CategoricalHistogramChart, +} from "../monitoring/components/HistogramChart"; +import StatsPanel from "../monitoring/components/StatsPanel"; + +const FeatureMonitoringTab = () => { + const { projectName, FeatureViewName, FeatureName } = useParams(); + + const { + data: metrics, + isLoading, + isError, + } = useFeatureMetrics({ + project: projectName || "", + feature_view_name: FeatureViewName, + feature_name: FeatureName, + }); + + const { data: baselineMetrics } = useBaselineMetrics( + projectName || "", + FeatureViewName, + FeatureName, + ); + + if (isLoading) { + return ; + } + + const latestMetric = (() => { + if (!metrics || metrics.length === 0) return null; + const withData = metrics.filter((m) => m.row_count > 0); + const candidates = withData.length > 0 ? withData : metrics; + return candidates.reduce((a, b) => (a.metric_date > b.metric_date ? a : b)); + })(); + + const baselineMetric = + baselineMetrics && baselineMetrics.length > 0 ? baselineMetrics[0] : null; + + if (isError || !latestMetric) { + return ( + No Monitoring Data} + body={ +

+ No monitoring metrics available for this feature. Run a monitoring + compute job to generate data quality metrics. +

+ } + actions={ + + Go to Monitoring + + } + /> + ); + } + + const isNumeric = latestMetric.feature_type === "numeric"; + + return ( + <> + + + {isNumeric && latestMetric.histogram && ( + + )} + {!isNumeric && latestMetric.histogram && ( + + )} + {!latestMetric.histogram && ( + No Histogram} + body={

Histogram data is not available.

} + /> + )} +
+ + + +
+ + + ); +}; + +export default FeatureMonitoringTab; diff --git a/ui/src/pages/features/FeatureOverviewTab.tsx b/ui/src/pages/features/FeatureOverviewTab.tsx index 6b613abc589..d895f52c585 100644 --- a/ui/src/pages/features/FeatureOverviewTab.tsx +++ b/ui/src/pages/features/FeatureOverviewTab.tsx @@ -1,4 +1,5 @@ import { + EuiBadge, EuiFlexGroup, EuiHorizontalRule, EuiLoadingSpinner, @@ -63,7 +64,9 @@ const FeatureOverviewTab = () => { Value Type - {feast.types.ValueType.Enum[featureData?.valueType!]} + {featureData?.valueType + ? feast.types.ValueType.Enum[featureData.valueType] + : featureData?.type || data?.type || "—"} Description @@ -71,13 +74,21 @@ const FeatureOverviewTab = () => { {featureData?.description} - FeatureView + + {data?.kind === "label" ? "Label View" : "Feature View"} + {FeatureViewName} + {data?.kind === "label" && ( + <> + {" "} + label view + + )} diff --git a/ui/src/pages/features/useLoadFeature.ts b/ui/src/pages/features/useLoadFeature.ts index 54bf31e996f..be322a9c8aa 100644 --- a/ui/src/pages/features/useLoadFeature.ts +++ b/ui/src/pages/features/useLoadFeature.ts @@ -1,27 +1,32 @@ -import { useContext } from "react"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import { useParams } from "react-router-dom"; +import useResourceQuery, { + featureDetailPath, +} from "../../queries/useResourceQuery"; const useLoadFeature = (featureViewName: string, featureName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); + const { projectName } = useParams(); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.featureViews?.find((fv) => { - return fv?.spec?.name === featureViewName; - }); + const fvQuery = useResourceQuery({ + resourceType: `feature:${featureViewName}:${featureName}`, + project: projectName, + restPath: featureDetailPath( + featureViewName, + featureName, + projectName || "", + ), + restSelect: (d) => d, + enabled: !!featureViewName && !!featureName, + }); const featureData = - data === undefined + fvQuery.data === undefined ? undefined - : data?.spec?.features?.find((f) => { - return f.name === featureName; - }); + : fvQuery.data?.spec?.features?.find( + (f: any) => f.name === featureName, + ) || fvQuery.data; return { - ...registryQuery, + ...fvQuery, featureData, }; }; diff --git a/ui/src/pages/label-views/ActiveLearningTab.tsx b/ui/src/pages/label-views/ActiveLearningTab.tsx new file mode 100644 index 00000000000..e880c2aaa55 --- /dev/null +++ b/ui/src/pages/label-views/ActiveLearningTab.tsx @@ -0,0 +1,739 @@ +import React, { useContext, useState, useMemo, useCallback } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiButton, + EuiSpacer, + EuiCallOut, + EuiText, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiBadge, + EuiBasicTable, + EuiBasicTableColumn, + EuiCodeBlock, + EuiIcon, + EuiEmptyPrompt, + EuiFieldNumber, + EuiSuperSelect, + EuiFieldSearch, + EuiTablePagination, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiOverlayMask, + EuiGlobalToastList, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import useAnnotationConfig from "./useAnnotationConfig"; + +interface CandidateData { + unlabeled_entities: Record[]; + total_labeled: number; + total_unlabeled: number; + entity_names: string[]; + feature_names: string[]; + label_view: string; + reference_feature_view: string | null; +} + +const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; + +const ActiveLearningTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, data } = useLoadLabelView(name); + const { data: registryData } = useLoadRegistry(registryUrl); + const { data: annotationConfig } = useAnnotationConfig(name); + + const [refFV, setRefFV] = useState(""); + const [limit, setLimit] = useState(50); + const [candidates, setCandidates] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [selectedItems, setSelectedItems] = useState[]>([]); + const [labelValues, setLabelValues] = useState>({}); + const [submitting, setSubmitting] = useState(false); + const [submitSuccess, setSubmitSuccess] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + const [toasts, setToasts] = useState< + Array<{ + id: string; + title: string; + color: "success" | "danger"; + iconType: string; + }> + >([]); + + const removeToast = useCallback((removedToast: { id: string }) => { + setToasts((prev) => prev.filter((t) => t.id !== removedToast.id)); + }, []); + + const spec = data?.object?.spec || data?.spec || {}; + const labelFields: { name: string; valueType?: string }[] = + spec.features || []; + const labelerField: string | null = + spec.labelerField || spec.labeler_field || null; + const configLabelValues = annotationConfig?.label_values || {}; + const configLabelWidgets = annotationConfig?.label_widgets || {}; + + const featureViewOptions = (registryData?.objects?.featureViews || []) + .filter((fv: any) => { + const fvName = fv.spec?.name || fv.name || ""; + return fvName !== name; + }) + .map((fv: any) => ({ + value: fv.spec?.name || fv.name || "", + inputDisplay: fv.spec?.name || fv.name || "Unknown", + dropdownDisplay: ( + + {fv.spec?.name || fv.name} + {fv.spec?.description && ( + +

{fv.spec.description}

+
+ )} +
+ ), + })); + + const fetchCandidates = async () => { + setLoading(true); + setError(null); + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch(`${baseUrl}/active-learning/candidates`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + feature_view: name, + reference_feature_view: refFV || undefined, + limit: limit, + }), + }); + const result = await response.json(); + if (!response.ok) { + const detail = result.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : "Failed to fetch candidates", + ); + } else { + setCandidates(result); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setLoading(false); + } + }; + + const entityColumns: EuiBasicTableColumn>[] = candidates + ? Object.keys(candidates.unlabeled_entities[0] || {}).map((key) => ({ + field: key, + name: key, + sortable: true, + })) + : []; + + const filteredCandidates = useMemo(() => { + if (!candidates) return []; + if (!searchQuery.trim()) return candidates.unlabeled_entities; + const query = searchQuery.toLowerCase(); + return candidates.unlabeled_entities.filter((row) => + Object.values(row).some( + (val) => val != null && String(val).toLowerCase().includes(query), + ), + ); + }, [candidates, searchQuery]); + + const paginatedCandidates = useMemo(() => { + const start = pageIndex * pageSize; + return filteredCandidates.slice(start, start + pageSize); + }, [filteredCandidates, pageIndex, pageSize]); + + const handleSubmitLabels = async () => { + if (selectedItems.length === 0) return; + setSubmitting(true); + setSubmitSuccess(null); + setError(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const pushSourceName = + spec.source?.pushSourceName || + spec.source?.name || + `${name}_push_source`; + + const rows = selectedItems.map((entity) => { + const row: Record = { ...entity, ...labelValues }; + row["event_timestamp"] = new Date().toISOString(); + return row; + }); + + const columnar: Record = {}; + if (rows.length > 0) { + for (const key of Object.keys(rows[0])) { + columnar[key] = rows.map((r) => r[key]); + } + } + + const response = await fetch(`${baseUrl}/push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + df: columnar, + to: "online_and_offline", + }), + }); + + if (response.ok) { + const msg = `Successfully labeled ${selectedItems.length} record${selectedItems.length !== 1 ? "s" : ""}`; + setSubmitSuccess(msg); + setToasts((prev) => [ + ...prev, + { + id: String(Date.now()), + title: msg, + color: "success", + iconType: "check", + }, + ]); + setSelectedItems([]); + setLabelValues({}); + fetchCandidates(); + } else { + const errData = await response.json().catch(() => null); + const detail = errData?.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : `Label submission failed (${response.status})`, + ); + } + } catch (e: any) { + setError(e.message || "Network error during label submission"); + } finally { + setSubmitting(false); + } + }; + + if (isLoading) { + return ( +

+ Loading... +

+ ); + } + + const exportJSON = () => { + if (!candidates) return; + const blob = new Blob( + [JSON.stringify(candidates.unlabeled_entities, null, 2)], + { type: "application/json" }, + ); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${name}_unlabeled_candidates.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const argillaPushCode = candidates + ? `import argilla as rg +import requests + +# Fetch unlabeled candidates from Feast +response = requests.post( + "${window.location.origin}/api/v1/active-learning/candidates", + json={ + "feature_view": "${name}", + ${refFV ? `"reference_feature_view": "${refFV}",` : ""} + "limit": ${limit}, + }, +) +candidates = response.json()["unlabeled_entities"] + +# Create Argilla records for annotation +records = [] +for entity in candidates: + records.append( + rg.Record( + fields={"text": f"Review entity: {entity}"}, + metadata=entity, + suggestions=[], # Add model predictions here for pre-labeling + ) + ) + +# Push to Argilla dataset for human annotation +dataset = rg.Dataset(name="${name}_annotation", settings=your_settings) +dataset.records.log(records) +print(f"Pushed {len(records)} candidates for annotation")` + : ""; + + return ( + + + + Find records that exist in your feature views but have NOT been + labeled yet. Label them directly here in the Feast UI, or export + candidates to your annotation tool (Argilla, Label Studio) for + targeted human review. + + + + + + + +

Find Unlabeled Records

+
+ + + + + {featureViewOptions.length > 0 ? ( + setRefFV(value)} + placeholder="Select a feature view..." + hasDividers + /> + ) : ( + setRefFV(e.target.value)} + /> + )} + + + + setLimit(parseInt(e.target.value) || 50)} + /> + + + + + + Find Unlabeled Records + + +
+ + {error && !isModalOpen && ( + + + + {error} + + + )} + + {candidates && ( + + + + + + + + + + + + + + + + + 0 + ? `${( + (candidates.total_labeled / + (candidates.total_labeled + + candidates.total_unlabeled)) * + 100 + ).toFixed(1)}%` + : "N/A" + } + description="Label Coverage" + titleColor="primary" + /> + + + + + + + {candidates.unlabeled_entities.length > 0 ? ( + + + + + +

+ Unlabeled Records{" "} + + {candidates.unlabeled_entities.length} + + {selectedItems.length > 0 && ( + <> + {" "} + + {selectedItems.length} selected + + + )} +

+
+
+ + + {selectedItems.length > 0 && ( + + setIsModalOpen(true)} + iconType="tag" + > + Label Selected ({selectedItems.length}) + + + )} + + + Export JSON + + + + +
+ + + Select records below, then click "Label Selected" to + assign labels. + + + { + setSearchQuery(e.target.value); + setPageIndex(0); + }} + isClearable + fullWidth + /> + + ) => + Object.values(item).join("_") + } + selection={{ + onSelectionChange: (items: Record[]) => + setSelectedItems(items), + selectable: () => true, + selectableMessage: () => "Select to label", + }} + /> + {filteredCandidates.length > pageSize && ( + <> + + setPageIndex(page)} + itemsPerPage={pageSize} + onChangeItemsPerPage={(size) => { + setPageSize(size); + setPageIndex(0); + }} + itemsPerPageOptions={PAGE_SIZE_OPTIONS} + /> + + )} +
+ + {submitSuccess && ( + + + + + )} + + {isModalOpen && ( + + setIsModalOpen(false)} + maxWidth={500} + > + + + Label {selectedItems.length}{" "} + Record + {selectedItems.length !== 1 ? "s" : ""} + + + + + Assign label values to all {selectedItems.length}{" "} + selected records. Labels will be pushed via{" "} + FeatureStore.push(). + + + + {labelFields + .filter((f) => f.name !== labelerField) + .map((field) => { + const widget = configLabelWidgets[field.name]; + const values = configLabelValues[field.name]; + let input; + + if ( + widget === "binary" && + values && + values.length === 2 + ) { + input = ( + ({ + value: v, + inputDisplay: + v === "1" + ? "Yes (1)" + : v === "0" + ? "No (0)" + : v, + })), + ]} + valueOfSelected={ + labelValues[field.name] || "" + } + onChange={(val) => + setLabelValues((prev) => ({ + ...prev, + [field.name]: val, + })) + } + /> + ); + } else if (values && values.length > 0) { + input = ( + ({ + value: v, + inputDisplay: v, + })), + ]} + valueOfSelected={ + labelValues[field.name] || "" + } + onChange={(val) => + setLabelValues((prev) => ({ + ...prev, + [field.name]: val, + })) + } + /> + ); + } else if (widget === "number") { + input = ( + + setLabelValues((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + /> + ); + } else { + input = ( + + setLabelValues((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + /> + ); + } + + return ( + + {input} + + ); + })} + {labelerField && ( + + + setLabelValues((prev) => ({ + ...prev, + [labelerField]: e.target.value, + })) + } + /> + + )} + + {error && ( + + + + {error} + + + )} + + + { + setIsModalOpen(false); + setLabelValues({}); + }} + > + Cancel + + { + await handleSubmitLabels(); + if (!error) { + setIsModalOpen(false); + } + }} + isLoading={submitting} + iconType="check" + > + Submit Labels + + + + + )} +
+ ) : ( + All records are labeled!} + body="No unlabeled records found in the reference feature view." + /> + )} + + + + + +

Push Candidates to Argilla

+
+ + + Use this script to push unlabeled candidates to Argilla for + annotation: + + + + {argillaPushCode} + +
+
+ )} + +
+ +
+
+ ); +}; + +export default ActiveLearningTab; diff --git a/ui/src/pages/label-views/AnnotateTab.tsx b/ui/src/pages/label-views/AnnotateTab.tsx new file mode 100644 index 00000000000..f378e2dfcb7 --- /dev/null +++ b/ui/src/pages/label-views/AnnotateTab.tsx @@ -0,0 +1,149 @@ +import React, { useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiButtonGroup, + EuiSpacer, + EuiPanel, + EuiText, + EuiLoadingSpinner, + EuiCallOut, + EuiBadge, + EuiFlexGroup, + EuiFlexItem, +} from "@elastic/eui"; +import ActiveLearningTab from "./ActiveLearningTab"; +import RagLabelingMethod from "./RagLabelingMethod"; +import ClassificationMethod from "./ClassificationMethod"; +import EntityFormMethod from "./EntityFormMethod"; +import useAnnotationConfig from "./useAnnotationConfig"; + +const PROFILE_DESCRIPTIONS: Record = { + "document-span": + "Load documents, highlight text spans, and label them for RAG retrieval or citation evaluation.", + "review-edit": + "Review and edit existing label records in a table view. Supports inline editing and batch push.", + "entity-form": + "Fill label fields per entity using a structured form. One record at a time.", + "active-learning": + "Surface unlabeled entities from a reference feature view and label them.", +}; + +const AnnotateTab = () => { + const { labelViewName } = useParams(); + const { + data: config, + isLoading, + isError, + } = useAnnotationConfig(labelViewName || ""); + + const detectedProfile = config?.profile || "table"; + + const availableMethods = React.useMemo(() => { + const methods: { id: string; label: string }[] = []; + + if (detectedProfile === "document-span") { + methods.push({ id: "document-span", label: "Document Span" }); + methods.push({ id: "review-edit", label: "Review & Edit" }); + return methods; + } + + if (detectedProfile === "entity-form") { + methods.push({ id: "entity-form", label: "Entity Form" }); + methods.push({ id: "review-edit", label: "Review & Edit" }); + methods.push({ id: "active-learning", label: "Active Learning" }); + return methods; + } + + if (detectedProfile === "active-learning") { + methods.push({ id: "active-learning", label: "Active Learning" }); + methods.push({ id: "entity-form", label: "Entity Form" }); + methods.push({ id: "review-edit", label: "Review & Edit" }); + return methods; + } + + methods.push({ id: "review-edit", label: "Review & Edit" }); + methods.push({ id: "active-learning", label: "Active Learning" }); + methods.push({ id: "entity-form", label: "Entity Form" }); + return methods; + }, [detectedProfile]); + + const [selectedMethod, setSelectedMethod] = useState(null); + const activeMethod = + selectedMethod || availableMethods[0]?.id || "review-edit"; + + if (isLoading) { + return ( + + + + + + Loading labeling configuration... + + + ); + } + + if (isError || !config) { + return ( + +

+ Falling back to Review & Edit view. Define{" "} + feast.io/labeling-method in your LabelView tags to + configure the labeling experience. +

+
+ ); + } + + return ( + + + + + + Labeling Method + + + + profile: {detectedProfile} + + + + setSelectedMethod(id)} + buttonSize="m" + isFullWidth={false} + /> + {PROFILE_DESCRIPTIONS[activeMethod] && ( + <> + + + {PROFILE_DESCRIPTIONS[activeMethod]} + + + )} + + + + + {activeMethod === "active-learning" && } + {activeMethod === "document-span" && ( + + )} + {activeMethod === "review-edit" && } + {activeMethod === "entity-form" && ( + + )} + + ); +}; + +export default AnnotateTab; diff --git a/ui/src/pages/label-views/BatchUploadTab.tsx b/ui/src/pages/label-views/BatchUploadTab.tsx new file mode 100644 index 00000000000..6f3930c5d92 --- /dev/null +++ b/ui/src/pages/label-views/BatchUploadTab.tsx @@ -0,0 +1,332 @@ +import React, { useContext, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiForm, + EuiFormRow, + EuiButton, + EuiSpacer, + EuiCallOut, + EuiText, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiSelect, + EuiFilePicker, + EuiBasicTable, + EuiBasicTableColumn, + EuiCodeBlock, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; + +const BatchUploadTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, data } = useLoadLabelView(name); + + const [fileData, setFileData] = useState(null); + const [fileName, setFileName] = useState(""); + const [pushTarget, setPushTarget] = useState("online"); + const [uploading, setUploading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [parseError, setParseError] = useState(null); + + if (isLoading) { + return ( +

+ Loading... +

+ ); + } + + const spec = data?.object?.spec || data?.spec || {}; + const pushSourceName = + spec.streamSource?.pushOptions?.pushSourceName || + spec.batchSource?.name?.replace("_batch", "_push_source") || + `${name}_push_source`; + + const features = data?.features || spec.features || []; + const entities = spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || []; + + const handleFileChange = (files: FileList | null) => { + if (!files || files.length === 0) { + setFileData(null); + setFileName(""); + setParseError(null); + return; + } + + const file = files[0]; + setFileName(file.name); + setParseError(null); + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const text = e.target?.result as string; + if (file.name.endsWith(".json")) { + const parsed = JSON.parse(text); + const rows = Array.isArray(parsed) + ? parsed + : parsed.data || parsed.records || [parsed]; + setFileData(rows); + } else if (file.name.endsWith(".csv")) { + const lines = text.trim().split("\n"); + if (lines.length < 2) { + setParseError( + "CSV must have at least a header row and one data row", + ); + return; + } + const headers = lines[0] + .split(",") + .map((h) => h.trim().replace(/"/g, "")); + const rows = []; + for (let i = 1; i < lines.length; i++) { + const values = lines[i] + .split(",") + .map((v) => v.trim().replace(/"/g, "")); + const row: Record = {}; + headers.forEach((h, idx) => { + const val = values[idx] || ""; + const numVal = Number(val); + row[h] = val === "" ? null : isNaN(numVal) ? val : numVal; + }); + rows.push(row); + } + setFileData(rows); + } else { + setParseError("Unsupported file format. Use .csv or .json"); + } + } catch (e: any) { + setParseError(`Failed to parse file: ${e.message}`); + setFileData(null); + } + }; + reader.readAsText(file); + }; + + const handleUpload = async () => { + if (!fileData || fileData.length === 0) return; + + setUploading(true); + setError(null); + setResult(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + + const dataWithTimestamp = fileData.map((row) => ({ + ...row, + event_timestamp: row.event_timestamp || new Date().toISOString(), + })); + + const response = await fetch(`${baseUrl}/batch-push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + data: dataWithTimestamp, + to: pushTarget, + }), + }); + + const res = await response.json(); + if (!response.ok) { + const detail = res.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : "Upload failed", + ); + } else { + setResult(res); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setUploading(false); + } + }; + + const previewColumns: EuiBasicTableColumn[] = fileData + ? Object.keys(fileData[0] || {}).map((col) => ({ + field: col, + name: col, + truncateText: true, + width: "120px", + })) + : []; + + const csvTemplate = [ + entities.join(",") + + "," + + features.map((f: any) => f.name || f).join(",") + + ",event_timestamp", + entities.map(() => "").join(",") + + "," + + features.map(() => "").join(",") + + ",2026-01-01T00:00:00Z", + ].join("\n"); + + return ( + + + + Upload a CSV or JSON file to push labels in bulk. Useful for + correcting labels, importing from external systems, or backfilling + historical labels. + + + + + + + +

Upload File

+
+ + + + + + + + {parseError && ( + + + + {parseError} + + + )} + + + setPushTarget(e.target.value)} + /> + + + + + + Push {fileData ? `${fileData.length} rows` : "Labels"} + + +
+ + {/* Preview */} + {fileData && fileData.length > 0 && ( + + + + + + +

+ Preview{" "} + {fileData.length} rows{" "} + {fileName} +

+
+
+
+ + + {fileData.length > 10 && ( + + Showing first 10 of {fileData.length} rows + + )} +
+
+ )} + + {error && ( + + + + {error} + + + )} + + {result && ( + + + + + Pushed {result.rows_pushed} rows to{" "} + {pushTarget} store. + + + + )} + + + + {/* Template */} + + +

CSV Template

+
+ + + Expected columns for this LabelView: + + + + {csvTemplate} + +
+
+ ); +}; + +export default BatchUploadTab; diff --git a/ui/src/pages/label-views/ClassificationMethod.tsx b/ui/src/pages/label-views/ClassificationMethod.tsx new file mode 100644 index 00000000000..9d4366f2e00 --- /dev/null +++ b/ui/src/pages/label-views/ClassificationMethod.tsx @@ -0,0 +1,465 @@ +import React, { + useState, + useContext, + useEffect, + useCallback, + useMemo, +} from "react"; +import { useParams } from "react-router-dom"; +import { + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiPanel, + EuiText, + EuiLoadingSpinner, + EuiBasicTable, + EuiBasicTableColumn, + EuiSelect, + EuiBadge, + EuiFieldSearch, + EuiTablePagination, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; +import useAnnotationConfig from "./useAnnotationConfig"; + +interface LabelRow { + _id: string; + [key: string]: any; +} + +const PAGE_SIZE_OPTIONS = [10, 25, 50]; + +const ClassificationMethod = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { data } = useLoadLabelView(labelViewName || ""); + const { data: annotationConfig } = useAnnotationConfig(labelViewName || ""); + + const [rows, setRows] = useState([]); + const [originalRows, setOriginalRows] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [pushSuccess, setPushSuccess] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + + const spec = data?.object?.spec || data?.spec || {}; + const labelFields: { name: string; valueType?: string }[] = useMemo( + () => spec.features || [], + [spec.features], + ); + const entities: string[] = useMemo( + () => + spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || [], + [spec.entityColumns, spec.entities], + ); + + const configuredValues = annotationConfig?.label_values || {}; + const fieldRoles = annotationConfig?.field_roles || {}; + const labelWidgets = annotationConfig?.label_widgets || {}; + + const fetchLabels = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch(`${baseUrl}/list-labels`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + feature_view: labelViewName || "", + limit: 200, + }), + }); + if (!response.ok) { + throw new Error(`Failed to fetch labels (${response.status})`); + } + const result = await response.json(); + const labelData: Record[] = result.labels || []; + const mapped = labelData.map((row, idx) => ({ + ...row, + _id: `row_${idx}_${Object.values(row).join("_")}`, + })); + setRows(mapped); + setOriginalRows(JSON.parse(JSON.stringify(mapped))); + } catch (e: any) { + setError(e.message || "Failed to load labels"); + } finally { + setIsLoading(false); + } + }, [labelViewName, registryUrl]); + + useEffect(() => { + if (labelViewName) { + fetchLabels(); + } + }, [labelViewName, fetchLabels]); + + const handleFieldChange = (rowId: string, field: string, value: string) => { + setRows((prev) => + prev.map((row) => (row._id === rowId ? { ...row, [field]: value } : row)), + ); + }; + + const getChangedRows = () => { + return rows.filter((row) => { + const original = originalRows.find((o) => o._id === row._id); + if (!original) return false; + return labelFields.some((f) => row[f.name] !== original[f.name]); + }); + }; + + const resetChanges = () => { + setRows(JSON.parse(JSON.stringify(originalRows))); + }; + + const saveToLabelView = async () => { + const changed = getChangedRows(); + if (changed.length === 0) return; + + setIsSaving(true); + setError(null); + setPushSuccess(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const pushSourceName = + spec.source?.pushSourceName || + spec.source?.name || + `${labelViewName}_push_source`; + + const pushRows = changed.map((row) => { + const pushRow: Record = {}; + entities.forEach((e) => { + pushRow[e] = row[e]; + }); + labelFields.forEach((f) => { + pushRow[f.name] = row[f.name]; + }); + pushRow["event_timestamp"] = new Date().toISOString(); + return pushRow; + }); + + const columnar: Record = {}; + if (pushRows.length > 0) { + for (const key of Object.keys(pushRows[0])) { + columnar[key] = pushRows.map((r) => r[key]); + } + } + + const response = await fetch(`${baseUrl}/push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + df: columnar, + to: "online_and_offline", + }), + }); + + if (response.ok) { + setPushSuccess( + `Successfully pushed ${changed.length} updated labels to ${labelViewName}`, + ); + setOriginalRows(JSON.parse(JSON.stringify(rows))); + } else { + const errData = await response.json().catch(() => null); + setError(errData?.detail || `Push failed (${response.status})`); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setIsSaving(false); + } + }; + + const exportJSON = () => { + const exportData = rows.map((row) => { + const { _id, ...rest } = row; + return rest; + }); + const blob = new Blob([JSON.stringify(exportData, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${labelViewName}_labels.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const filteredRows = useMemo(() => { + if (!searchQuery.trim()) return rows; + const q = searchQuery.toLowerCase(); + return rows.filter((row) => + Object.entries(row) + .filter(([key]) => key !== "_id") + .some( + ([, val]) => val != null && String(val).toLowerCase().includes(q), + ), + ); + }, [rows, searchQuery]); + + const paginatedRows = useMemo(() => { + const start = pageIndex * pageSize; + return filteredRows.slice(start, start + pageSize); + }, [filteredRows, pageIndex, pageSize]); + + const uniqueValuesForField = useMemo(() => { + const result: Record = {}; + labelFields.forEach((field) => { + const values = new Set(); + rows.forEach((row) => { + if (row[field.name] != null && String(row[field.name]).trim() !== "") { + values.add(String(row[field.name])); + } + }); + result[field.name] = Array.from(values).sort(); + }); + return result; + }, [rows, labelFields]); + + const metadataFields = ["event_timestamp", "labeler"]; + + const entityColumns: EuiBasicTableColumn[] = entities.map( + (ent) => ({ + field: ent, + name: ent, + sortable: true, + truncateText: true, + }), + ); + + const labelColumns: EuiBasicTableColumn[] = labelFields + .filter((field) => !metadataFields.includes(field.name)) + .map((field) => { + const configVals = configuredValues[field.name]; + const role = fieldRoles[field.name]; + const widget = labelWidgets[field.name]; + + return { + field: field.name, + name: field.name, + render: (value: any, row: LabelRow) => { + if (widget === "binary" && configVals && configVals.length === 2) { + return ( + ({ + value: o, + text: o === "1" ? "Yes (1)" : o === "0" ? "No (0)" : o, + })), + ]} + value={value != null ? String(value) : ""} + onChange={(e) => + handleFieldChange(row._id, field.name, e.target.value) + } + /> + ); + } + const dropdownOptions = + configVals || uniqueValuesForField[field.name] || []; + if ( + (role === "label" || dropdownOptions.length > 0) && + dropdownOptions.length <= 30 + ) { + return ( + ({ + value: o, + text: o, + })), + ]} + value={value != null ? String(value) : ""} + onChange={(e) => + handleFieldChange(row._id, field.name, e.target.value) + } + /> + ); + } + return {value != null ? String(value) : ""}; + }, + }; + }); + + const metadataColumns: EuiBasicTableColumn[] = labelFields + .filter((field) => metadataFields.includes(field.name)) + .map((field) => ({ + field: field.name, + name: field.name, + sortable: true, + truncateText: true, + })); + + const columns = [...entityColumns, ...labelColumns, ...metadataColumns]; + + const changedCount = getChangedRows().length; + + if (isLoading) { + return ( + + + + + + Loading labels from {labelViewName}... + + + ); + } + + return ( + + +

+ Review and correct existing labels in the table below. Changes are + pushed to {labelViewName} via its PushSource and + governed by the configured conflict policy. +

+
+ + {error && ( + <> + + +

{error}

+
+ + )} + + {pushSuccess && ( + <> + + + + )} + + + + {rows.length === 0 ? ( + + +

+ No labels found. Use Entity Form or Active Learning to add labels + first, then review them here. +

+
+
+ ) : ( + <> + + + { + setSearchQuery(e.target.value); + setPageIndex(0); + }} + isClearable + /> + + + + {changedCount > 0 && ( + + {changedCount} changed + + )} + + + Export JSON + + + + + Reset + + + + + Save ({changedCount}) + + + + + + + + + + { + const original = originalRows.find((o) => o._id === row._id); + const isChanged = + original && + labelFields.some((f) => row[f.name] !== original[f.name]); + return isChanged + ? { style: { backgroundColor: "rgba(255, 200, 0, 0.05)" } } + : {}; + }} + /> + + + {filteredRows.length > pageSize && ( + <> + + setPageIndex(page)} + itemsPerPage={pageSize} + onChangeItemsPerPage={(size) => { + setPageSize(size); + setPageIndex(0); + }} + itemsPerPageOptions={PAGE_SIZE_OPTIONS} + /> + + )} + + )} +
+ ); +}; + +export default ClassificationMethod; diff --git a/ui/src/pages/label-views/EntityFormMethod.tsx b/ui/src/pages/label-views/EntityFormMethod.tsx new file mode 100644 index 00000000000..025f08b277c --- /dev/null +++ b/ui/src/pages/label-views/EntityFormMethod.tsx @@ -0,0 +1,484 @@ +import React, { useState, useContext, useEffect, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiPanel, + EuiTitle, + EuiText, + EuiFormRow, + EuiFieldText, + EuiFieldNumber, + EuiSelect, + EuiTextArea, + EuiButtonGroup, + EuiBadge, + EuiIcon, + EuiEmptyPrompt, + EuiForm, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; +import { AnnotationConfig } from "./useAnnotationConfig"; + +interface EntityFormMethodProps { + annotationConfig: AnnotationConfig; +} + +const EntityFormMethod = ({ annotationConfig }: EntityFormMethodProps) => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { data } = useLoadLabelView(labelViewName || ""); + + const spec = data?.object?.spec || data?.spec || {}; + const entities: string[] = useMemo( + () => + spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || [], + [spec.entityColumns, spec.entities], + ); + const labelFields: { name: string; valueType?: string }[] = useMemo( + () => spec.features || [], + [spec.features], + ); + + const fieldRoles = annotationConfig.field_roles; + const labelValues = annotationConfig.label_values; + const labelWidgets = annotationConfig.label_widgets; + const labelerField = annotationConfig.labeler_field || "labeler"; + + const [entityValues, setEntityValues] = useState>({}); + const [fieldInputs, setFieldInputs] = useState>({}); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const [pushSuccess, setPushSuccess] = useState(null); + const [labelCount, setLabelCount] = useState(0); + + const editableFields = useMemo( + () => + labelFields.filter( + (f) => f.name !== labelerField && f.name !== "event_timestamp", + ), + [labelFields, labelerField], + ); + + useEffect(() => { + const defaults: Record = {}; + editableFields.forEach((f) => { + const vals = labelValues[f.name]; + if (vals && vals.length > 0) { + defaults[f.name] = ""; + } else { + defaults[f.name] = ""; + } + }); + setFieldInputs(defaults); + }, [editableFields, labelValues]); + + const resetForm = () => { + const defaults: Record = {}; + editableFields.forEach((f) => { + defaults[f.name] = ""; + }); + setFieldInputs(defaults); + setError(null); + setPushSuccess(null); + }; + + const isFormValid = useMemo(() => { + const hasEntity = entities.every( + (e) => entityValues[e] && entityValues[e].trim() !== "", + ); + const hasAtLeastOneLabel = editableFields.some( + (f) => + fieldRoles[f.name] === "label" && + fieldInputs[f.name] && + fieldInputs[f.name].trim() !== "", + ); + return hasEntity && hasAtLeastOneLabel; + }, [entities, entityValues, editableFields, fieldInputs, fieldRoles]); + + const submitLabel = async () => { + if (!isFormValid) return; + + setIsSaving(true); + setError(null); + setPushSuccess(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const pushSourceName = + annotationConfig.push_source_name || + spec.source?.pushSourceName || + spec.source?.name || + `${labelViewName}_push_source`; + + const pushRow: Record = {}; + entities.forEach((e) => { + pushRow[e] = entityValues[e]; + }); + editableFields.forEach((f) => { + if (fieldInputs[f.name] && fieldInputs[f.name].trim() !== "") { + const widget = labelWidgets[f.name]; + if (widget === "number" || widget === "binary") { + pushRow[f.name] = Number(fieldInputs[f.name]); + } else { + pushRow[f.name] = fieldInputs[f.name]; + } + } + }); + pushRow[labelerField] = fieldInputs[labelerField] || "human_reviewer"; + pushRow["event_timestamp"] = new Date().toISOString(); + + const columnar: Record = {}; + for (const key of Object.keys(pushRow)) { + columnar[key] = [pushRow[key]]; + } + + const response = await fetch(`${baseUrl}/push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + df: columnar, + to: "online_and_offline", + }), + }); + + if (response.ok) { + setLabelCount((c) => c + 1); + setPushSuccess( + `Label pushed for ${entities.map((e) => `${e}=${entityValues[e]}`).join(", ")}`, + ); + resetForm(); + } else { + const errData = await response.json().catch(() => null); + setError(errData?.detail || `Push failed (${response.status})`); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setIsSaving(false); + } + }; + + const renderFieldInput = (field: { name: string; valueType?: string }) => { + const widget = labelWidgets[field.name]; + const values = labelValues[field.name]; + const role = fieldRoles[field.name]; + const currentValue = fieldInputs[field.name] || ""; + + if (widget === "binary" && values && values.length === 2) { + const options = values.map((v) => ({ + id: v, + label: v === "1" ? "Yes" : v === "0" ? "No" : v, + })); + return ( + + setFieldInputs((prev) => ({ ...prev, [field.name]: id })) + } + buttonSize="m" + /> + ); + } + + if ( + widget === "enum" || + (values && values.length > 0 && values.length <= 30) + ) { + return ( + ({ value: v, text: v })), + ]} + value={currentValue} + onChange={(e) => + setFieldInputs((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + /> + ); + } + + if (widget === "number") { + return ( + + setFieldInputs((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + placeholder={`Enter ${field.name}`} + /> + ); + } + + if (widget === "text" || role === "metadata") { + return ( + + setFieldInputs((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + placeholder={`Enter ${field.name}`} + rows={2} + compressed + /> + ); + } + + return ( + + setFieldInputs((prev) => ({ + ...prev, + [field.name]: e.target.value, + })) + } + placeholder={`Enter ${field.name}`} + /> + ); + }; + + if (!labelViewName) { + return ( + No label view selected} + /> + ); + } + + return ( + + +

+ Fill in the entity identifier and label values below. Each submission + pushes one label record to {labelViewName}. +

+
+ + {error && ( + <> + + +

{error}

+
+ + )} + + {pushSuccess && ( + <> + + + + )} + + + + + + + + +

+ Entity +

+
+ + + {entities.map((entity) => ( + + + setEntityValues((prev) => ({ + ...prev, + [entity]: e.target.value, + })) + } + placeholder={`Enter ${entity} value`} + /> + + ))} + + + +

+ Labels +

+
+ + + {editableFields + .filter((f) => fieldRoles[f.name] === "label") + .map((field) => ( + + {renderFieldInput(field)} + + ))} + + {editableFields.filter( + (f) => + fieldRoles[f.name] !== "label" && + fieldRoles[f.name] !== undefined, + ).length > 0 && ( + <> + + +

+ Additional Fields +

+
+ + {editableFields + .filter( + (f) => + fieldRoles[f.name] !== "label" && + fieldRoles[f.name] !== undefined, + ) + .map((field) => ( + + {renderFieldInput(field)} + + ))} + + )} + + {editableFields.filter((f) => !fieldRoles[f.name]).length > 0 && ( + <> + + {editableFields + .filter((f) => !fieldRoles[f.name]) + .map((field) => ( + + {renderFieldInput(field)} + + ))} + + )} + + + + + + setFieldInputs((prev) => ({ + ...prev, + [labelerField]: e.target.value, + })) + } + placeholder="your_name or reviewer_id" + /> + + + + + + + + Submit Label + + + + + Clear + + + +
+
+
+ + + + +

Session

+
+ + +

+ Labels submitted:{" "} + {labelCount} +

+

+ Conflict policy:{" "} + + {spec.conflictPolicy || "LAST_WRITE_WINS"} + +

+

+ Labeler field: {labelerField} +

+
+ + +

Schema

+
+ + + {entities.map((e) => ( +

+ entity {e} +

+ ))} + {editableFields.map((f) => ( +

+ + {fieldRoles[f.name] || "field"} + {" "} + {f.name} +

+ ))} +
+
+
+
+
+ ); +}; + +export default EntityFormMethod; diff --git a/ui/src/pages/label-views/Index.tsx b/ui/src/pages/label-views/Index.tsx new file mode 100644 index 00000000000..ae8fe324f7d --- /dev/null +++ b/ui/src/pages/label-views/Index.tsx @@ -0,0 +1,208 @@ +import React from "react"; +import { useParams } from "react-router-dom"; + +import { + EuiPageTemplate, + EuiLoadingSpinner, + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiEmptyPrompt, + EuiTitle, + EuiLink, + EuiCallOut, +} from "@elastic/eui"; + +import { LabelViewIcon } from "../../graphics/LabelViewIcon"; +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import useResourceQuery, { + labelViewListPath, + restLabelViewsFromResponse, +} from "../../queries/useResourceQuery"; + +const useLoadLabelViews = () => { + const { projectName } = useParams(); + return useResourceQuery({ + resourceType: "label-views-list", + project: projectName, + restPath: labelViewListPath(projectName), + restSelect: restLabelViewsFromResponse, + }); +}; + +interface LabelViewRow { + name: string; + entities: string[]; + conflictPolicy: string; + annotationProfile: string; + labelerField: string; + online: boolean; + description: string; +} + +const LabelViewsListingTable = ({ labelViews }: { labelViews: any[] }) => { + const { projectName } = useParams(); + + const rows: LabelViewRow[] = labelViews.map((lv: any) => { + const spec = lv.spec || {}; + const tags = spec.tags || {}; + return { + name: spec.name || "Unknown", + entities: spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || [], + conflictPolicy: spec.conflictPolicy || "LAST_WRITE_WINS", + annotationProfile: tags["feast.io/labeling-method"] || "table", + labelerField: spec.labelerField || "labeler", + online: spec.online !== false, + description: spec.description || "", + }; + }); + + const columns: EuiBasicTableColumn[] = [ + { + field: "name", + name: "Name", + sortable: true, + render: (name: string) => ( + {name} + ), + }, + { + field: "entities", + name: "Entities", + render: (entities: string[]) => + entities.length > 0 + ? entities.map((e, i) => ( + + {i > 0 && ", "} + {e} + + )) + : "-", + }, + { + field: "conflictPolicy", + name: "Conflict Policy", + render: (policy: string) => { + const color = + policy === "LAST_WRITE_WINS" + ? "default" + : policy === "MAJORITY_VOTE" + ? "primary" + : "accent"; + return {policy}; + }, + }, + { + field: "annotationProfile", + name: "Labeling Method", + render: (profile: string) => { + const color = + profile === "document-span" + ? "warning" + : profile === "entity-form" + ? "success" + : profile === "active-learning" + ? "accent" + : "hollow"; + return {profile}; + }, + }, + { + field: "labelerField", + name: "Labeler Field", + }, + { + field: "online", + name: "Online", + render: (online: boolean) => ( + + {online ? "Yes" : "No"} + + ), + }, + { + field: "description", + name: "Description", + render: (desc: string) => ( + + {desc || "-"} + + ), + }, + ]; + + return ( + + items={rows} + columns={columns} + tableLayout="auto" + /> + ); +}; + +const LabelViewIndexEmptyState = () => ( + +

No Label Views

+ + } + body={ +

+ Label views manage mutable labels and annotations for agent + interactions, safety monitoring, and RLHF pipelines. Define a LabelView + in your feature repository to get started. +

+ } + /> +); + +const Index = () => { + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadLabelViews(); + + useDocumentTitle(`Label Views | Feast`); + + return ( + + + + {isLoading && ( +

+ Loading +

+ )} + {isPermissionDenied && ( + +

You do not have permission to view label views.

+
+ )} + {isError && !isPermissionDenied && ( +

We encountered an error while loading.

+ )} + {isSuccess && (!data || data.length === 0) && ( + + )} + {isSuccess && data && data.length > 0 && ( + + )} +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/label-views/IntegrationsTab.tsx b/ui/src/pages/label-views/IntegrationsTab.tsx new file mode 100644 index 00000000000..feca543bd10 --- /dev/null +++ b/ui/src/pages/label-views/IntegrationsTab.tsx @@ -0,0 +1,288 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiText, + EuiLoadingSpinner, + EuiCallOut, + EuiCodeBlock, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiIcon, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; + +const IntegrationsTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, isSuccess, data } = useLoadLabelView(name); + + const [webhookConfig, setWebhookConfig] = useState(null); + const [configLoading, setConfigLoading] = useState(true); + + useEffect(() => { + if (isSuccess && data) { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + fetch(`${baseUrl}/webhook/config/${name}`) + .then((r) => r.json()) + .then((d) => { + setWebhookConfig(d); + setConfigLoading(false); + }) + .catch(() => setConfigLoading(false)); + } + }, [isSuccess, data, name, registryUrl]); + + if (isLoading || configLoading) { + return ( +

+ Loading integration config... +

+ ); + } + + if (!webhookConfig) { + return ( + + Could not fetch webhook configuration. + + ); + } + + const webhookPayload = JSON.stringify(webhookConfig.payload_example, null, 2); + const baseUrl = window.location.origin; + const webhookFullUrl = `${baseUrl}${webhookConfig.webhook_url}`; + const batchFullUrl = `${baseUrl}${webhookConfig.batch_url}`; + + const argillaPython = `import argilla as rg +import requests +from datetime import datetime, timezone + +# After annotation is complete, export and push to Feast LabelView +dataset = rg.load("your_dataset_name") +submitted = dataset.records(status="submitted").to_list(flatten=True) + +records = [] +for record in submitted: + records.append({ + ${webhookConfig.entity_fields.map((e: string) => `"${e}": record.metadata["${e}"],`).join("\n ")} + ${webhookConfig.label_fields.map((f: string) => `"${f}": record.responses["${f}"],`).join("\n ")} + ${webhookConfig.labeler_field ? `"${webhookConfig.labeler_field}": record.user_id,` : ""} + "event_timestamp": datetime.now(timezone.utc).isoformat(), + }) + +response = requests.post( + "${webhookFullUrl}", + json={ + "push_source_name": "${webhookConfig.push_source_name}", + "records": records, + }, +) +print(f"Pushed {len(records)} labels: {response.json()}")`; + + const labelStudioPython = `import requests +from datetime import datetime, timezone +from label_studio_sdk import Client + +ls = Client(url="http://localhost:8080", api_key="YOUR_KEY") +project = ls.get_project(PROJECT_ID) + +# Export completed annotations +tasks = project.get_labeled_tasks() + +records = [] +for task in tasks: + annotation = task["annotations"][0]["result"][0] + records.append({ + ${webhookConfig.entity_fields.map((e: string) => `"${e}": task["data"]["${e}"],`).join("\n ")} + ${webhookConfig.label_fields.map((f: string) => `"${f}": annotation["value"].get("${f}", ""),`).join("\n ")} + ${webhookConfig.labeler_field ? `"${webhookConfig.labeler_field}": str(task["annotations"][0]["completed_by"]),` : ""} + "event_timestamp": datetime.now(timezone.utc).isoformat(), + }) + +response = requests.post( + "${webhookFullUrl}", + json={ + "push_source_name": "${webhookConfig.push_source_name}", + "records": records, + }, +) +print(f"Pushed {len(records)} labels: {response.json()}")`; + + const curlExample = `curl -X POST "${webhookFullUrl}" \\ + -H "Content-Type: application/json" \\ + -d '${webhookPayload}'`; + + return ( + + + + Connect Argilla, Label Studio, or any annotation tool to push labels + into this LabelView via webhook or batch API. + + + + + + {/* Webhook Configuration */} + + + + + + + +

Webhook Endpoint

+
+
+ + POST + +
+ + + Real-time label ingestion from annotation tools. Automatically adds + timestamps if not provided. + + + + + URL: + + {webhookFullUrl} + + + + + + + Required fields: + + {webhookConfig.entity_fields.map((f: string) => ( + + {f} (entity) + + ))} + {webhookConfig.label_fields.map((f: string) => ( + + {f} (label) + + ))} + {webhookConfig.labeler_field && ( + + + {webhookConfig.labeler_field} (labeler) + + + )} + + + +

Example payload:

+
+ + {webhookPayload} + +
+ + + + {/* Batch Push */} + + + + + + + +

Batch Push Endpoint

+
+
+ + POST + +
+ + + Upload bulk labels from CSV/parquet exports. Same schema as webhook. + + + + {batchFullUrl} + +
+ + + + {/* cURL Example */} + + +

cURL Example

+
+ + + {curlExample} + +
+ + + + {/* Argilla Integration */} + + + + + + + +

Argilla Integration

+
+
+
+ + + Export annotated records from Argilla and push to this LabelView. + + + + {argillaPython} + +
+ + + + {/* Label Studio Integration */} + + + + + + + +

Label Studio Integration

+
+
+
+ + + Export completed annotations from Label Studio and ingest via webhook. + + + + {labelStudioPython} + +
+
+ ); +}; + +export default IntegrationsTab; diff --git a/ui/src/pages/label-views/LabelBrowseTab.tsx b/ui/src/pages/label-views/LabelBrowseTab.tsx new file mode 100644 index 00000000000..7ed1b51a8b1 --- /dev/null +++ b/ui/src/pages/label-views/LabelBrowseTab.tsx @@ -0,0 +1,385 @@ +import React, { useContext, useState, useEffect, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiFieldSearch, + EuiButton, + EuiSpacer, + EuiCallOut, + EuiText, + EuiLoadingSpinner, + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiTablePagination, + EuiEmptyPrompt, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; + +interface LabelRow { + [key: string]: any; +} + +const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; + +const LabelBrowseTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, isSuccess, data } = useLoadLabelView(name); + + const [allLabels, setAllLabels] = useState(null); + const [allEntityNames, setAllEntityNames] = useState([]); + const [totalEntities, setTotalEntities] = useState(0); + const [loadingAll, setLoadingAll] = useState(false); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + const initialLoadDone = React.useRef(false); + + useEffect(() => { + if (isSuccess && data && !initialLoadDone.current) { + initialLoadDone.current = true; + loadLabels(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess, data, name, registryUrl]); + + const loadLabels = async () => { + setLoadingAll(true); + setError(null); + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch(`${baseUrl}/list-labels`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ feature_view: name, limit: 1000 }), + }); + if (response.ok) { + const respData = await response.json(); + setAllLabels(respData.labels || []); + setAllEntityNames(respData.entity_names || []); + setTotalEntities(respData.total_entities || 0); + } else { + const errData = await response.json().catch(() => null); + setError( + errData?.detail || `Failed to load labels (${response.status})`, + ); + } + } catch (err: any) { + setError(err.message || "Network error."); + } finally { + setLoadingAll(false); + } + }; + + const spec = data?.spec || data?.object?.spec || {}; + const entities: string[] = spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || []; + const features: any[] = useMemo(() => spec.features || [], [spec.features]); + const conflictPolicy = + spec.conflictPolicy || spec.conflict_policy || "LAST_WRITE_WINS"; + const policyLabel = + typeof conflictPolicy === "number" + ? ["LAST_WRITE_WINS", "LABELER_PRIORITY", "MAJORITY_VOTE"][ + conflictPolicy + ] || "LAST_WRITE_WINS" + : String(conflictPolicy).replace("CONFLICT_POLICY_", ""); + const labelerField: string = + spec.labelerField || spec.labeler_field || "labeler"; + + const entityCols = allEntityNames.length > 0 ? allEntityNames : entities; + + const filteredLabels = useMemo(() => { + if (!allLabels) return []; + if (!searchQuery.trim()) return allLabels; + + const query = searchQuery.toLowerCase(); + return allLabels.filter((row) => + Object.values(row).some( + (val) => val != null && String(val).toLowerCase().includes(query), + ), + ); + }, [allLabels, searchQuery]); + + const paginatedLabels = useMemo(() => { + const start = pageIndex * pageSize; + return filteredLabels.slice(start, start + pageSize); + }, [filteredLabels, pageIndex, pageSize]); + + const columns: EuiBasicTableColumn[] = useMemo(() => { + const cols: EuiBasicTableColumn[] = []; + + for (const entity of entityCols) { + cols.push({ + field: entity, + name: entity, + sortable: true, + render: (value: any) => ( + + {value != null ? String(value) : "\u2014"} + + ), + }); + } + + for (const feature of features) { + cols.push({ + field: feature.name, + name: feature.name, + sortable: true, + render: (value: any) => { + if (value === null || value === undefined) { + return ( + + + + ); + } + return {String(value)}; + }, + }); + } + + cols.push({ + field: "_event_ts", + name: "Last Updated", + sortable: true, + render: (value: any) => + value ? new Date(value * 1000).toLocaleString() : "\u2014", + }); + + return cols; + }, [entityCols, features]); + + if (isLoading) { + return ( +

+ Loading schema... +

+ ); + } + + if (!isSuccess || !data) { + return

Unable to load label view schema.

; + } + + return ( + + + + + +

Schema

+
+ + ({ + name: e, + type: "ENTITY", + role: "entity", + })), + ...features.map((f: any) => ({ + name: f.name, + type: f.valueType || "STRING", + role: f.name === labelerField ? "labeler" : "label", + })), + ]} + columns={[ + { field: "name", name: "Field", width: "40%" }, + { field: "type", name: "Type", width: "30%" }, + { + field: "role", + name: "Role", + width: "30%", + render: (role: string) => ( + + {role} + + ), + }, + ]} + tableLayout="fixed" + compressed + /> +
+ + +

Properties

+
+ + + + + Conflict Policy + +
+ + {policyLabel} + +
+
+ + + Labeler Field + + + {labelerField} + + +
+
+
+
+ + + + + + + +

+ Label Records{" "} + {allLabels && ( + {totalEntities} total + )} + {searchQuery && + filteredLabels.length !== (allLabels || []).length && ( + <> + {" "} + + {filteredLabels.length} matching + + + )} +

+
+
+ + + Refresh + + +
+ + + + +

+ All label records in the online store, resolved by conflict policy. + Use the search bar to filter by any field value. +

+
+ + + + { + setSearchQuery(e.target.value); + setPageIndex(0); + }} + isClearable + fullWidth + /> + + + + {allLabels === null && loadingAll && ( + + + + + Loading label records... + + )} + + {allLabels !== null && allLabels.length === 0 && ( + No labels submitted yet} + body="No labels have been pushed to the online store for this label view." + /> + )} + + {allLabels !== null && + allLabels.length > 0 && + filteredLabels.length === 0 && ( + No matching records} + body={ +

+ No records match "{searchQuery}". + Try a different search term. +

+ } + /> + )} + + {filteredLabels.length > 0 && ( + <> + + items={paginatedLabels} + columns={columns} + tableLayout="auto" + /> + + setPageIndex(page)} + itemsPerPage={pageSize} + onChangeItemsPerPage={(size) => { + setPageSize(size); + setPageIndex(0); + }} + itemsPerPageOptions={PAGE_SIZE_OPTIONS} + /> + + )} +
+ + {error && ( + <> + + + {error} + + + )} +
+ ); +}; + +export default LabelBrowseTab; diff --git a/ui/src/pages/label-views/LabelViewInstance.tsx b/ui/src/pages/label-views/LabelViewInstance.tsx new file mode 100644 index 00000000000..8ef720ed278 --- /dev/null +++ b/ui/src/pages/label-views/LabelViewInstance.tsx @@ -0,0 +1,152 @@ +import React, { useState } from "react"; +import { Route, Routes, useNavigate, useParams } from "react-router-dom"; +import { + EuiPageTemplate, + EuiPopover, + EuiContextMenu, + EuiButton, +} from "@elastic/eui"; + +import { LabelViewIcon } from "../../graphics/LabelViewIcon"; +import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; +import LabelBrowseTab from "./LabelBrowseTab"; +import QualityDashboardTab from "./QualityDashboardTab"; +import AnnotateTab from "./AnnotateTab"; +import TrainingExportTab from "./TrainingExportTab"; +import IntegrationsTab from "./IntegrationsTab"; +import BatchUploadTab from "./BatchUploadTab"; +import LabelViewLineageTab from "./LabelViewLineageTab"; +import FeatureViewVersionsTab from "../feature-views/FeatureViewVersionsTab"; +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; + +const LabelViewInstance = () => { + const navigate = useNavigate(); + const { labelViewName } = useParams(); + const [isMoreOpen, setIsMoreOpen] = useState(false); + + useDocumentTitle(`${labelViewName} | Label View | Feast`); + + const moreMenuItems = [ + { + id: "more-panel", + items: [ + { + name: "Export", + icon: "exportAction", + onClick: () => { + setIsMoreOpen(false); + navigate("export"); + }, + }, + { + name: "Upload", + icon: "importAction", + onClick: () => { + setIsMoreOpen(false); + navigate("upload"); + }, + }, + { + name: "Versions", + icon: "copyClipboard", + onClick: () => { + setIsMoreOpen(false); + navigate("versions"); + }, + }, + { + name: "Lineage", + icon: "graphApp", + onClick: () => { + setIsMoreOpen(false); + navigate("lineage"); + }, + }, + { + name: "Integrations", + icon: "gear", + onClick: () => { + setIsMoreOpen(false); + navigate("integrations"); + }, + }, + ], + }, + ]; + + return ( + + setIsMoreOpen(!isMoreOpen)} + > + More + + } + isOpen={isMoreOpen} + closePopover={() => setIsMoreOpen(false)} + panelPaddingSize="none" + anchorPosition="downRight" + > + + , + ]} + tabs={[ + { + label: "Labels", + isSelected: useMatchExact(""), + onClick: () => { + navigate(""); + }, + }, + { + label: "Quality", + isSelected: useMatchSubpath("quality"), + onClick: () => { + navigate("quality"); + }, + }, + { + label: "Label Data", + isSelected: useMatchSubpath("annotate"), + onClick: () => { + navigate("annotate"); + }, + }, + ]} + /> + + + } /> + } /> + } /> + } /> + } /> + + } + /> + } /> + } /> + + + + ); +}; + +export default LabelViewInstance; diff --git a/ui/src/pages/label-views/LabelViewLineageTab.tsx b/ui/src/pages/label-views/LabelViewLineageTab.tsx new file mode 100644 index 00000000000..0da3d92dab8 --- /dev/null +++ b/ui/src/pages/label-views/LabelViewLineageTab.tsx @@ -0,0 +1,93 @@ +import React, { useContext, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiEmptyPrompt, + EuiLoadingSpinner, + EuiSpacer, + EuiSelect, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, +} from "@elastic/eui"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import RegistryVisualization from "../../components/RegistryVisualization"; +import { FEAST_FCO_TYPES } from "../../parsers/types"; +import { filterPermissionsByAction } from "../../utils/permissionUtils"; + +const LabelViewLineageTab = () => { + const registryUrl = useContext(RegistryPathContext); + const { labelViewName, projectName } = useParams(); + const { + isLoading, + isSuccess, + isError, + data: registryData, + } = useLoadRegistry(registryUrl, projectName); + const [selectedPermissionAction, setSelectedPermissionAction] = useState(""); + + const filterNode = { + type: FEAST_FCO_TYPES.labelView, + name: labelViewName || "", + }; + + return ( + <> + {isLoading && ( +
+ +
+ )} + {isError && ( + Error loading lineage} + body={

Could not load lineage data for this label view.

} + /> + )} + {isSuccess && registryData && ( + <> + + + + setSelectedPermissionAction(e.target.value)} + aria-label="Filter by permissions" + /> + + + + + + + )} + + ); +}; + +export default LabelViewLineageTab; diff --git a/ui/src/pages/label-views/LabelViewOverviewTab.tsx b/ui/src/pages/label-views/LabelViewOverviewTab.tsx new file mode 100644 index 00000000000..bbe65071c39 --- /dev/null +++ b/ui/src/pages/label-views/LabelViewOverviewTab.tsx @@ -0,0 +1,340 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiText, + EuiSpacer, + EuiBadge, + EuiLoadingSpinner, + EuiBasicTable, + EuiBasicTableColumn, + EuiCallOut, + EuiLink, + EuiCode, +} from "@elastic/eui"; + +import useLoadLabelView from "./useLoadLabelView"; +import useAnnotationConfig from "./useAnnotationConfig"; + +const CONFLICT_POLICY_MAP: Record = { + "0": "LAST_WRITE_WINS", + "1": "LABELER_PRIORITY", + "2": "MAJORITY_VOTE", + LAST_WRITE_WINS: "LAST_WRITE_WINS", + LABELER_PRIORITY: "LABELER_PRIORITY", + MAJORITY_VOTE: "MAJORITY_VOTE", +}; + +interface SchemaField { + name: string; + valueType: string; +} + +const PROFILE_COLORS: Record = { + "document-span": "primary", + table: "default", + "entity-form": "accent", + "active-learning": "success", +}; + +interface FieldRoleRow { + field: string; + role: string; + values?: string; + widget?: string; +} + +const LabelViewOverviewTab = () => { + const { labelViewName, projectName } = useParams(); + const name = labelViewName || ""; + const { isLoading, isSuccess, isError, data } = useLoadLabelView(name); + const { data: annotationConfig } = useAnnotationConfig(name); + + if (isLoading) { + return ( +

+ Loading +

+ ); + } + if (isError) { + return

Error loading label view: {name}

; + } + if (!isSuccess || !data) { + return

No label view found with name: {name}

; + } + + const spec = data.spec || {}; + const meta = data.meta || {}; + const conflictPolicy = + CONFLICT_POLICY_MAP[spec.conflictPolicy] || + spec.conflictPolicy || + "LAST_WRITE_WINS"; + const labelerField = spec.labelerField || "labeler"; + const entities: string[] = spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || []; + const features: any[] = spec.features || []; + + const schemaColumns: EuiBasicTableColumn[] = [ + { field: "name", name: "Field Name", sortable: true }, + { field: "valueType", name: "Value Type" }, + ]; + + const schemaRows: SchemaField[] = features.map((f: any) => ({ + name: f.name || "Unknown", + valueType: f.valueType != null ? String(f.valueType) : "Unknown", + })); + + return ( + + +

+ conflict_policy is enforced for offline store reads + (training data, Browse, Quality). The offline store always retains + full write history. The online store uses last-write-wins for serving. +

+
+ + + + + +

Properties

+
+ + + Conflict Policy + + + {conflictPolicy} + + + + Labeler Field + + {labelerField} + + + Entities + + {entities.length > 0 + ? entities.map((ent: string, i: number) => ( + + {i > 0 && ", "} + + {ent} + + + )) + : "-"} + + + {spec.description && ( + <> + Description + + {spec.description} + + + )} + +
+ + + +

Metadata

+
+ + + Created + + {meta.createdTimestamp + ? new Date( + typeof meta.createdTimestamp === "string" + ? meta.createdTimestamp + : Number(meta.createdTimestamp.seconds) * 1000, + ).toLocaleDateString("en-CA") + : "N/A"} + + + Last Updated + + {meta.lastUpdatedTimestamp + ? new Date( + typeof meta.lastUpdatedTimestamp === "string" + ? meta.lastUpdatedTimestamp + : Number(meta.lastUpdatedTimestamp.seconds) * 1000, + ).toLocaleDateString("en-CA") + : "N/A"} + + +
+ {annotationConfig && ( + <> + + + +

Labeling Method

+
+ + + Profile + + + {annotationConfig.profile} + + + + Push Source + + + {annotationConfig.push_source_name || "N/A"} + + + + + {Object.keys(annotationConfig.field_roles).length > 0 && ( + <> + + + Field Roles + + + + items={Object.entries(annotationConfig.field_roles).map( + ([field, role]) => ({ + field, + role, + values: + annotationConfig.label_values[field]?.join(", "), + widget: annotationConfig.label_widgets[field], + }), + )} + columns={[ + { field: "field", name: "Field", width: "30%" }, + { + field: "role", + name: "Role", + width: "25%", + render: (role: string) => ( + + {role} + + ), + }, + { + field: "values", + name: "Values", + render: (v: string) => v || "\u2014", + }, + { + field: "widget", + name: "Widget", + render: (w: string) => w || "\u2014", + }, + ]} + tableLayout="auto" + compressed + /> + + )} +
+ + )} +
+ + + +

Labels

+
+ + {schemaRows.length > 0 ? ( + + items={schemaRows} + columns={[ + { + field: "name", + name: "Label Name", + sortable: true, + render: (labelName: string) => ( + + {labelName} + + ), + }, + { field: "valueType", name: "Value Type" }, + ]} + tableLayout="auto" + /> + ) : ( + No labels defined. + )} +
+ + {spec.source && ( + + +

Data Source

+
+ + + Source Type + + {spec.source.type || "PushSource"} + + {spec.source.name && ( + <> + + Source Name + + + + {spec.source.name} + + + + )} + +
+ )} +
+
+
+ ); +}; + +export default LabelViewOverviewTab; diff --git a/ui/src/pages/label-views/QualityDashboardTab.tsx b/ui/src/pages/label-views/QualityDashboardTab.tsx new file mode 100644 index 00000000000..7d0a8a0f3f4 --- /dev/null +++ b/ui/src/pages/label-views/QualityDashboardTab.tsx @@ -0,0 +1,406 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiText, + EuiLoadingSpinner, + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiBadge, + EuiBasicTable, + EuiBasicTableColumn, + EuiProgress, + EuiButton, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; + +interface QualityData { + total_entities: number; + feature_names: string[]; + distributions: Record>; + coverage_pct: Record; + null_counts: Record; + labeler_stats: Record; + staleness_seconds: number | null; + oldest_label_ts: string | null; + newest_label_ts: string | null; + labeler_field: string | null; +} + +const formatStaleness = (seconds: number | null): string => { + if (seconds === null) return "N/A"; + if (seconds < 60) return `${Math.round(seconds)}s ago`; + if (seconds < 3600) return `${Math.round(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.round(seconds / 3600)}h ago`; + return `${Math.round(seconds / 86400)}d ago`; +}; + +const getStalenessColor = (seconds: number | null): string => { + if (seconds === null) return "subdued"; + if (seconds < 3600) return "success"; + if (seconds < 86400) return "warning"; + return "danger"; +}; + +const DistributionBar = ({ + distribution, + label, +}: { + distribution: Record; + label: string; +}) => { + const entries = Object.entries(distribution).sort((a, b) => b[1] - a[1]); + const total = entries.reduce((acc, [, count]) => acc + count, 0); + const colors = [ + "#0569EA", + "#00BFB3", + "#F5A623", + "#BD271E", + "#6092C0", + "#D36086", + "#9170B8", + "#CA8EAE", + ]; + + if (entries.length === 0) { + return ( + + No data + + ); + } + + return ( +
+ + {label} ({total} values, {entries.length} unique) + + +
+ {entries.slice(0, 8).map(([val, count], idx) => ( +
+ ))} +
+ + + {entries.slice(0, 6).map(([val, count], idx) => ( + + + {val.length > 12 ? val.slice(0, 12) + "\u2026" : val}: {count} + + + ))} + {entries.length > 6 && ( + + +{entries.length - 6} more + + )} + +
+ ); +}; + +const QualityDashboardTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading: lvLoading, isSuccess } = useLoadLabelView(name); + + const [quality, setQuality] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchQuality = async () => { + setLoading(true); + setError(null); + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch(`${baseUrl}/label-quality`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ feature_view: name, limit: 500 }), + }); + const result = await response.json(); + if (!response.ok) { + const detail = result.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : "Failed to load quality metrics", + ); + } else { + setQuality(result); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (isSuccess) { + fetchQuality(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSuccess, name]); + + if (lvLoading) { + return ( +

+ Loading... +

+ ); + } + + if (loading) { + return ( + + + + + + Computing label quality metrics... + + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + if (!quality) return null; + + const labelerColumns: EuiBasicTableColumn<{ name: string; count: number }>[] = + [ + { field: "name", name: "Labeler", sortable: true }, + { + field: "count", + name: "Labels Submitted", + sortable: true, + render: (count: number) => {count}, + }, + { + field: "count", + name: "Share", + render: (count: number) => { + const total = Object.values(quality.labeler_stats).reduce( + (a, b) => a + b, + 0, + ); + return `${((count / total) * 100).toFixed(1)}%`; + }, + }, + ]; + + const labelerData = Object.entries(quality.labeler_stats) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count); + + return ( + + {/* Time Range + Refresh at top */} + + + + + + + Oldest label:{" "} + {quality.oldest_label_ts + ? new Date(quality.oldest_label_ts).toLocaleString() + : "N/A"} + + + + + Newest label:{" "} + {quality.newest_label_ts + ? new Date(quality.newest_label_ts).toLocaleString() + : "N/A"} + + + + + + + Refresh Metrics + + + + + + + + {/* Summary Stats */} + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Coverage */} + + +

Field Coverage

+
+ + Percentage of records with non-null values for each label field + + + {quality.feature_names.map((fn) => ( + + + + + {fn} + + + + 80 + ? "success" + : (quality.coverage_pct[fn] || 0) > 50 + ? "warning" + : "danger" + } + label={`${(quality.coverage_pct[fn] || 0).toFixed(1)}%`} + /> + + + + {quality.null_counts[fn] || 0} nulls + + + + + + ))} +
+ + + + {/* Distributions */} + + +

Value Distributions

+
+ + Distribution of label values across all records + + + {quality.feature_names.map((fn) => ( + + + + + + ))} +
+ + + + {/* Per-Labeler Stats */} + {labelerData.length > 0 && ( + + + + +

Per-Labeler Statistics

+
+
+ {quality.labeler_field && ( + + + Tracked via: {quality.labeler_field} + + + )} +
+ + +
+ )} +
+ ); +}; + +export default QualityDashboardTab; diff --git a/ui/src/pages/label-views/RagLabelingMethod.tsx b/ui/src/pages/label-views/RagLabelingMethod.tsx new file mode 100644 index 00000000000..84ad696a149 --- /dev/null +++ b/ui/src/pages/label-views/RagLabelingMethod.tsx @@ -0,0 +1,899 @@ +import React, { useState, useContext, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiButton, + EuiPanel, + EuiTitle, + EuiText, + EuiLoadingSpinner, + EuiButtonGroup, + EuiCode, + EuiTextArea, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiOverlayMask, + EuiForm, + EuiIcon, + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from "@elastic/eui"; +import { useTheme } from "../../contexts/ThemeContext"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import type { AnnotationConfig } from "./useAnnotationConfig"; + +interface TextSelection { + text: string; + start: number; + end: number; +} + +interface DocumentLabel { + text: string; + start: number; + end: number; + label: string; + timestamp: number; +} + +interface RagLabelingMethodProps { + annotationConfig: AnnotationConfig; +} + +const RagLabelingMethod = ({ annotationConfig }: RagLabelingMethodProps) => { + const { labelViewName } = useParams(); + const { colorMode } = useTheme(); + const registryUrl = useContext(RegistryPathContext); + + const fieldRoles = annotationConfig.field_roles; + const labelValues = annotationConfig.label_values; + const labelWidgets = annotationConfig.label_widgets; + + const contentRefField = useMemo( + () => + Object.entries(fieldRoles).find( + ([, role]) => role === "content_ref", + )?.[0] || null, + [fieldRoles], + ); + const contentField = useMemo( + () => + Object.entries(fieldRoles).find(([, role]) => role === "content")?.[0] || + null, + [fieldRoles], + ); + const spanStartField = useMemo( + () => + Object.entries(fieldRoles).find( + ([, role]) => role === "span_start", + )?.[0] || null, + [fieldRoles], + ); + const spanEndField = useMemo( + () => + Object.entries(fieldRoles).find(([, role]) => role === "span_end")?.[0] || + null, + [fieldRoles], + ); + + const labelFieldEntries = useMemo( + () => Object.entries(fieldRoles).filter(([, role]) => role === "label"), + [fieldRoles], + ); + const primaryLabelField = labelFieldEntries[0]?.[0] || null; + const secondaryLabelFields = labelFieldEntries.slice(1).map(([name]) => name); + + const primaryLabelOptions = useMemo(() => { + if (!primaryLabelField) + return [ + { id: "relevant", label: "Relevant" }, + { id: "irrelevant", label: "Irrelevant" }, + ]; + const values = labelValues[primaryLabelField]; + if (values && values.length > 0) { + return values.map((v) => ({ + id: v, + label: v.charAt(0).toUpperCase() + v.slice(1), + })); + } + return [ + { id: "relevant", label: "Relevant" }, + { id: "irrelevant", label: "Irrelevant" }, + ]; + }, [primaryLabelField, labelValues]); + + const [filePath, setFilePath] = useState(""); + const [selectedText, setSelectedText] = useState(null); + const [labelingMode, setLabelingMode] = useState( + primaryLabelOptions[0]?.id || "relevant", + ); + const [labels, setLabels] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [documentContent, setDocumentContent] = useState(null); + const [error, setError] = useState(null); + const [groundTruthLabel, setGroundTruthLabel] = useState(""); + const [isSaving, setIsSaving] = useState(false); + const [pushSuccess, setPushSuccess] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [extraFieldValues, setExtraFieldValues] = useState< + Record + >({}); + + const loadDocument = async () => { + if (!filePath) return; + setIsLoading(true); + setError(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const response = await fetch( + `${baseUrl}/document-content?path=${encodeURIComponent(filePath)}`, + ); + if (response.ok) { + const result = await response.json(); + setDocumentContent(result.content); + } else { + setDocumentContent( + `This is a sample document for testing RAG labeling in Feast UI. + +The document contains multiple paragraphs that can be used to test text highlighting and labeling. + +This paragraph discusses machine learning and artificial intelligence concepts. It covers topics like neural networks, deep learning, and natural language processing. Users should be able to select and label relevant portions of this text for RAG retrieval systems. + +Another section focuses on data engineering and ETL pipelines. This content explains how to process large datasets and build scalable data infrastructure. + +The final paragraph contains information about feature stores and real-time machine learning systems.`, + ); + } + } catch { + setDocumentContent( + `This is a sample document for testing RAG labeling in Feast UI. + +The document contains multiple paragraphs that can be used to test text highlighting and labeling. + +This paragraph discusses machine learning and artificial intelligence concepts. It covers topics like neural networks, deep learning, and natural language processing. + +Another section focuses on data engineering and ETL pipelines. This content explains how to process large datasets and build scalable data infrastructure. + +The final paragraph contains information about feature stores and real-time machine learning systems.`, + ); + } finally { + setIsLoading(false); + } + }; + + const handleTextSelection = () => { + const selection = window.getSelection(); + if (selection && selection.toString().trim() && documentContent) { + const selectedTextContent = selection.toString().trim(); + const range = selection.getRangeAt(0); + const rangeText = range.toString(); + if (rangeText) { + const startIndex = documentContent.indexOf(rangeText); + if (startIndex !== -1) { + setSelectedText({ + text: selectedTextContent, + start: startIndex, + end: startIndex + rangeText.length, + }); + } + } + } + }; + + const handleLabelSelection = () => { + if (selectedText) { + const newLabel: DocumentLabel = { + text: selectedText.text, + start: selectedText.start, + end: selectedText.end, + label: labelingMode, + timestamp: Date.now(), + }; + setLabels([...labels, newLabel]); + setSelectedText(null); + const selection = window.getSelection(); + if (selection) selection.removeAllRanges(); + } + }; + + const handleRemoveLabel = (index: number) => { + setLabels(labels.filter((_: DocumentLabel, i: number) => i !== index)); + }; + + const generateChunkId = (docName: string, start: number, end: number) => { + const raw = `${docName}:${start}:${end}`; + let hash = 0; + for (let i = 0; i < raw.length; i++) { + const char = raw.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash |= 0; + } + return `chunk_${Math.abs(hash).toString(36)}`; + }; + + const openSubmitModal = () => { + const defaults: Record = {}; + if (annotationConfig.labeler_field) { + defaults[annotationConfig.labeler_field] = "rag_labeling_ui"; + } + setExtraFieldValues(defaults); + setIsModalOpen(true); + }; + + const buildPushRows = () => { + const docName = filePath || "document"; + const entityField = annotationConfig.entities[0] || "entity_id"; + const labelerField = annotationConfig.labeler_field; + + return labels.map((label) => { + const row: Record = {}; + + row[entityField] = generateChunkId(docName, label.start, label.end); + + if (annotationConfig.entities.length > 1) { + for (let i = 1; i < annotationConfig.entities.length; i++) { + const ent = annotationConfig.entities[i]; + if (extraFieldValues[ent]) { + row[ent] = extraFieldValues[ent]; + } + } + } + + if (contentRefField) row[contentRefField] = filePath; + if (contentField) row[contentField] = label.text; + if (spanStartField) row[spanStartField] = label.start; + if (spanEndField) row[spanEndField] = label.end; + if (primaryLabelField) row[primaryLabelField] = label.label; + + for (const secField of secondaryLabelFields) { + const widget = labelWidgets[secField]; + if (widget === "text" && groundTruthLabel) { + row[secField] = groundTruthLabel; + } else if (extraFieldValues[secField]) { + row[secField] = extraFieldValues[secField]; + } + } + + if (labelerField) { + row[labelerField] = extraFieldValues[labelerField] || "rag_labeling_ui"; + } + + row["event_timestamp"] = new Date().toISOString(); + return row; + }); + }; + + const saveToLabelView = async () => { + if (labels.length === 0) return; + setIsSaving(true); + setError(null); + setPushSuccess(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const pushSourceName = + annotationConfig.push_source_name || `${labelViewName}_push_source`; + const rows = buildPushRows(); + + const columnar: Record = {}; + if (rows.length > 0) { + for (const key of Object.keys(rows[0])) { + columnar[key] = rows.map((r) => r[key]); + } + } + + const response = await fetch(`${baseUrl}/push`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + push_source_name: pushSourceName, + df: columnar, + to: "online_and_offline", + }), + }); + + if (response.ok) { + setPushSuccess( + `Pushed ${labels.length} span labels to ${labelViewName}`, + ); + setLabels([]); + setIsModalOpen(false); + } else { + const errData = await response.json().catch(() => null); + setError( + typeof errData?.detail === "string" + ? errData.detail + : `Push failed (${response.status})`, + ); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setIsSaving(false); + } + }; + + const exportJSON = () => { + const rows = buildPushRows(); + const saveData = { + labelView: labelViewName, + profile: annotationConfig.profile, + filePath, + groundTruthLabel, + rows, + timestamp: new Date().toISOString(), + }; + const blob = new Blob([JSON.stringify(saveData, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${labelViewName}_rag_labels.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const renderDocumentWithHighlights = ( + content: string, + ): (string | React.ReactElement)[] => { + const allHighlights = [...labels]; + if (selectedText) { + allHighlights.push({ + text: selectedText.text, + start: selectedText.start, + end: selectedText.end, + label: "temp-selection", + timestamp: 0, + }); + } + if (allHighlights.length === 0) return [content]; + + const sortedHighlights = [...allHighlights].sort( + (a, b) => a.start - b.start, + ); + const result: (string | React.ReactElement)[] = []; + let lastIndex = 0; + + const positiveValues = new Set( + primaryLabelOptions.length > 0 + ? [primaryLabelOptions[0].id] + : ["relevant"], + ); + + sortedHighlights.forEach((highlight, index) => { + result.push(content.slice(lastIndex, highlight.start)); + let highlightColor: string; + let borderColor: string; + + if (highlight.label === "temp-selection") { + highlightColor = colorMode === "dark" ? "#1a4d66" : "#add8e6"; + borderColor = colorMode === "dark" ? "#2d6b8a" : "#87ceeb"; + } else if (!positiveValues.has(highlight.label)) { + highlightColor = colorMode === "dark" ? "#4d1a1a" : "#f8d7da"; + borderColor = colorMode === "dark" ? "#6b2d2d" : "#f5c6cb"; + } else { + highlightColor = colorMode === "dark" ? "#1a4d1a" : "#d4edda"; + borderColor = colorMode === "dark" ? "#2d6b2d" : "#c3e6cb"; + } + + result.push( + + {highlight.text} + , + ); + lastIndex = highlight.end; + }); + + result.push(content.slice(lastIndex)); + return result; + }; + + const chunkColumns: EuiBasicTableColumn[] = [ + { + field: "label", + name: primaryLabelField || "Label", + width: "120px", + render: (value: string) => { + const isPositive = + primaryLabelOptions.length > 0 && value === primaryLabelOptions[0].id; + return ( + {value} + ); + }, + }, + { + field: "text", + name: "Span Text", + truncateText: true, + render: (value: string) => + value.substring(0, 100) + (value.length > 100 ? "..." : ""), + }, + { + name: "Offsets", + width: "100px", + render: (item: DocumentLabel) => ( + + {item.start}–{item.end} + + ), + }, + ]; + + const unmappedFeatures = annotationConfig.features.filter((f) => { + if (f === annotationConfig.labeler_field) return false; + if (fieldRoles[f]) return false; + return true; + }); + + return ( + + +

+ Load a document, highlight text spans, and label them. Span positions + and labels are auto-mapped to your schema fields and pushed to{" "} + {labelViewName} via its PushSource. +

+
+ + + + + +

Field Mapping

+
+ + + {contentRefField && ( + <> + Document path + + {contentRefField} + + + )} + {contentField && ( + <> + Span text + + {contentField} + + + )} + {spanStartField && spanEndField && ( + <> + Offsets + + {spanStartField},{" "} + {spanEndField} + + + )} + {primaryLabelField && ( + <> + Primary label + + {primaryLabelField} + {" → "} + {primaryLabelOptions.map((o) => o.id).join(", ")} + + + )} + {secondaryLabelFields.map((f) => ( + + {f} + + {labelWidgets[f] || "text"} + + + ))} + +
+ + + + + + + setFilePath(e.target.value)} + /> + + + + + + Load Document + + + + + + {isLoading && ( + <> + + + + + + + Loading document... + + + + )} + + {error && ( + <> + + +

{error}

+
+ + )} + + {pushSuccess && ( + <> + + + + )} + + {documentContent && ( + <> + + + + + + setLabelingMode(id)} + buttonSize="s" + /> + + + + + + Label Selected Text + + + + + + {selectedText && ( + <> + + + {selectedText.text.substring(0, 120)} + + + )} + + + + + +

Document Content

+
+ + +
+ {renderDocumentWithHighlights(documentContent)} +
+
+
+ + {secondaryLabelFields.length > 0 && ( + <> + + {secondaryLabelFields.map((f) => { + const widget = labelWidgets[f] || "text"; + if (widget === "text") { + return ( + + { + if (f === secondaryLabelFields[0]) { + setGroundTruthLabel(e.target.value); + } else { + setExtraFieldValues((prev) => ({ + ...prev, + [f]: e.target.value, + })); + } + }} + rows={3} + /> + + ); + } + return null; + })} + + )} + + + + + + + Export JSON + + + + + Save to LabelView ({labels.length}) + + + + + {labels.length > 0 && ( + <> + + + +

Labeled Spans ({labels.length})

+
+ + {labels.map((label, index) => ( + + + + {label.label} + + + + + {label.start}–{label.end} + + + + + "{label.text.substring(0, 80)} + {label.text.length > 80 ? "..." : ""}" + + + + handleRemoveLabel(index)} + > + Remove + + + + ))} +
+ + )} + + )} + + {isModalOpen && ( + + setIsModalOpen(false)} maxWidth={600}> + + + Push {labels.length} Span + {labels.length !== 1 ? "s" : ""} to {labelViewName} + + + + +

+ Each span will be pushed as a row. Entity key ( + {annotationConfig.entities[0] || "entity_id"} + ) is auto-generated from document path + span offsets. Fields + are mapped from the annotation profile. +

+
+ + + + +

Spans to push:

+
+ + +
+ + + +

Additional Fields

+
+ + + + + setExtraFieldValues((prev) => ({ + ...prev, + [annotationConfig.labeler_field]: e.target.value, + })) + } + /> + + + {annotationConfig.entities.length > 1 && + annotationConfig.entities.slice(1).map((ent) => ( + + + setExtraFieldValues((prev) => ({ + ...prev, + [ent]: e.target.value, + })) + } + /> + + ))} + + {unmappedFeatures.map((f) => ( + + + setExtraFieldValues((prev) => ({ + ...prev, + [f]: e.target.value, + })) + } + /> + + ))} + + + {error && ( + <> + + + {error} + + + )} +
+ + { + setIsModalOpen(false); + setError(null); + }} + > + Cancel + + + Push Labels + + +
+
+ )} +
+ ); +}; + +export default RagLabelingMethod; diff --git a/ui/src/pages/label-views/TrainingExportTab.tsx b/ui/src/pages/label-views/TrainingExportTab.tsx new file mode 100644 index 00000000000..7a19741c182 --- /dev/null +++ b/ui/src/pages/label-views/TrainingExportTab.tsx @@ -0,0 +1,421 @@ +import React, { useContext, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPanel, + EuiTitle, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiButton, + EuiSpacer, + EuiCallOut, + EuiText, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiDatePicker, + EuiSuperSelect, +} from "@elastic/eui"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadLabelView from "./useLoadLabelView"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import moment from "moment"; + +const TrainingExportTab = () => { + const { labelViewName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const name = labelViewName || ""; + const { isLoading, data } = useLoadLabelView(name); + const { data: registryData } = useLoadRegistry(registryUrl); + + const [featureService, setFeatureService] = useState(""); + const [entityColumn, setEntityColumn] = useState(""); + const [entityValues, setEntityValues] = useState(""); + const [startDate, setStartDate] = useState( + moment().subtract(30, "days"), + ); + const [endDate, setEndDate] = useState(moment()); + const [exporting, setExporting] = useState(false); + const [exportResult, setExportResult] = useState(null); + const [error, setError] = useState(null); + + if (isLoading) { + return ( +

+ Loading... +

+ ); + } + + const spec = data?.object?.spec || data?.spec || {}; + const entities: string[] = spec.entityColumns?.length + ? spec.entityColumns.map((ec: { name: string }) => ec.name) + : spec.entities || []; + + const allFeatureServices = registryData?.objects?.featureServices || []; + const relevantFeatureServices = allFeatureServices.filter((fs: any) => { + const projections = [ + ...(fs.spec?.features || []), + ...(fs.spec?.featureViewProjections || []), + ]; + return projections.some( + (proj: any) => + proj.featureViewName === name || + proj.name === name || + proj.featureViewProjection?.featureViewName === name, + ); + }); + const servicesToShow = + relevantFeatureServices.length > 0 + ? relevantFeatureServices + : allFeatureServices; + + const featureServiceOptions = servicesToShow.map((fs: any) => ({ + value: fs.spec?.name || fs.name || "", + inputDisplay: fs.spec?.name || fs.name || "Unknown", + dropdownDisplay: ( + + {fs.spec?.name || fs.name} + {fs.spec?.description && ( + +

{fs.spec.description}

+
+ )} +
+ ), + })); + + const entityColumnOptions = entities.map((e: string) => ({ + value: e, + inputDisplay: e, + })); + + const handleExport = async () => { + setExporting(true); + setError(null); + setExportResult(null); + + try { + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + const entityKey = + entityColumn || (entities.length > 0 ? entities[0] : "entity_id"); + const values = entityValues + .split(",") + .map((v: string) => v.trim()) + .filter((v: string) => v.length > 0); + + if (values.length === 0) { + setError("Please provide at least one entity value"); + setExporting(false); + return; + } + + const serviceName = featureService || `${name}_service`; + const entityDf: Record = { + [entityKey]: values, + }; + + const response = await fetch(`${baseUrl}/training-dataset/export`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + feature_service: serviceName, + entity_df: entityDf, + start_date: startDate?.toISOString() || null, + end_date: endDate?.toISOString() || null, + }), + }); + + const result = await response.json(); + if (!response.ok) { + const detail = result.detail; + setError( + typeof detail === "string" + ? detail + : Array.isArray(detail) + ? detail.map((d: any) => d.msg || JSON.stringify(d)).join("; ") + : "Export failed", + ); + } else { + setExportResult(result); + } + } catch (e: any) { + setError(e.message || "Network error"); + } finally { + setExporting(false); + } + }; + + const downloadCSV = () => { + if (!exportResult?.data) return; + const cols = exportResult.columns; + const csvRows = [cols.join(",")]; + for (const row of exportResult.data) { + csvRows.push( + cols + .map((c: string) => { + const val = row[c]; + if (val === null || val === undefined) return ""; + const str = String(val); + return str.includes(",") ? `"${str}"` : str; + }) + .join(","), + ); + } + const blob = new Blob([csvRows.join("\n")], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${exportResult.feature_service}_training_data.csv`; + a.click(); + URL.revokeObjectURL(url); + }; + + const downloadJSON = () => { + if (!exportResult?.data) return; + const blob = new Blob([JSON.stringify(exportResult.data, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${exportResult.feature_service}_training_data.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const columns: EuiBasicTableColumn[] = exportResult + ? exportResult.columns.map((col: string) => ({ + field: col, + name: col, + truncateText: true, + render: (val: any) => + val === null || val === undefined ? ( + null + ) : ( + String(val) + ), + })) + : []; + + return ( + + + + Create a point-in-time correct training dataset by joining features + with labels via get_historical_features. Export as CSV or + JSON for model training. + + + + + + + +

Export Configuration

+
+ + + + + setFeatureService(value)} + placeholder="Select a feature service..." + hasDividers + /> + + + + {entityColumnOptions.length > 0 ? ( + setEntityColumn(value)} + /> + ) : ( + setEntityColumn(e.target.value)} + /> + )} + + + + setEntityValues(e.target.value)} + /> + + + + + + + + + + + + + + + + + + + Generate Training Dataset + + +
+ + {error && ( + + + + {error} + + + )} + + {exportResult && ( + + + + + + +

+ Training Dataset{" "} + + {exportResult.row_count} rows + +

+
+
+ + + + + Download CSV + + + + + Download JSON + + + + +
+ + + Feature service: {exportResult.feature_service} | + Columns: {exportResult.columns.length} | Point-in-time correct + + + + {exportResult.data.length > 50 && ( + + Showing first 50 of {exportResult.data.length} rows. Download + for full dataset. + + )} +
+
+ )} + + + + + +

SDK Equivalent

+
+ + + This UI action is equivalent to the following Python SDK call: + + +
+          {`from feast import FeatureStore
+import pandas as pd
+
+store = FeatureStore(".")
+entity_df = pd.DataFrame({
+    "${entityColumn || entities[0] || "entity_id"}": [${
+      entityValues
+        ? entityValues
+            .split(",")
+            .map((v) => `"${v.trim()}"`)
+            .join(", ")
+        : '"user_1", "user_2"'
+    }],
+    "event_timestamp": pd.Timestamp("${endDate?.toISOString() || "now"}"),
+})
+
+training_df = store.get_historical_features(
+    entity_df=entity_df,
+    features=store.get_feature_service("${featureService || "your_service"}"),
+).to_df()
+
+training_df.to_parquet("training_data.parquet")`}
+        
+
+
+ ); +}; + +export default TrainingExportTab; diff --git a/ui/src/pages/label-views/useAnnotationConfig.ts b/ui/src/pages/label-views/useAnnotationConfig.ts new file mode 100644 index 00000000000..558fd3067e8 --- /dev/null +++ b/ui/src/pages/label-views/useAnnotationConfig.ts @@ -0,0 +1,41 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; + +export interface AnnotationConfig { + label_view: string; + profile: string; + field_roles: Record; + label_values: Record; + label_widgets: Record; + entities: string[]; + features: string[]; + labeler_field: string; + push_source_name: string | null; +} + +const useAnnotationConfig = (labelViewName: string) => { + const registryUrl = useContext(RegistryPathContext); + const baseUrl = registryUrl?.replace(/\/$/, "") || "/api/v1"; + + return useQuery( + ["annotation-config", labelViewName, registryUrl], + async () => { + const response = await fetch( + `${baseUrl}/annotation-config/${encodeURIComponent(labelViewName)}`, + ); + if (!response.ok) { + throw new Error( + `Failed to load annotation config (${response.status})`, + ); + } + return response.json(); + }, + { + enabled: !!labelViewName && !!registryUrl, + staleTime: 60_000, + }, + ); +}; + +export default useAnnotationConfig; diff --git a/ui/src/pages/label-views/useLoadLabelView.ts b/ui/src/pages/label-views/useLoadLabelView.ts new file mode 100644 index 00000000000..8ddb44cd22c --- /dev/null +++ b/ui/src/pages/label-views/useLoadLabelView.ts @@ -0,0 +1,18 @@ +import { useParams } from "react-router-dom"; +import useResourceQuery, { + labelViewDetailPath, +} from "../../queries/useResourceQuery"; + +const useLoadLabelView = (labelViewName: string) => { + const { projectName } = useParams(); + + return useResourceQuery({ + resourceType: `label-view:${labelViewName}`, + project: projectName, + restPath: labelViewDetailPath(labelViewName, projectName || ""), + restSelect: (d) => d, + enabled: !!labelViewName, + }); +}; + +export default useLoadLabelView; diff --git a/ui/src/pages/lineage/Index.tsx b/ui/src/pages/lineage/Index.tsx index a3a9ca19296..40bf7f83afc 100644 --- a/ui/src/pages/lineage/Index.tsx +++ b/ui/src/pages/lineage/Index.tsx @@ -1,18 +1,29 @@ -import React, { useContext } from "react"; +import React, { useContext, useState } from "react"; import { EuiPageTemplate, EuiTitle, EuiSpacer, EuiSkeletonText, EuiEmptyPrompt, + EuiButtonGroup, } from "@elastic/eui"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import useLoadRegistry from "../../queries/useLoadRegistry"; import RegistryPathContext from "../../contexts/RegistryPathContext"; import RegistryVisualizationTab from "../../components/RegistryVisualizationTab"; +import { LineageGraph } from "../../components/OpenLineageGraph"; +import LineageEventsList from "../../components/LineageEventsList"; +import { useLoadOpenLineageGraph } from "../../queries/useLoadOpenLineageGraph"; import { useParams } from "react-router-dom"; +type ActiveTab = "lineage" | "events"; + +const tabButtons = [ + { id: "lineage", label: "Lineage" }, + { id: "events", label: "Events" }, +]; + const LineagePage = () => { useDocumentTitle("Feast Lineage"); const registryUrl = useContext(RegistryPathContext); @@ -22,7 +33,14 @@ const LineagePage = () => { projectName, ); - // Show message for "All Projects" view + const [activeTab, setActiveTab] = useState("lineage"); + const [registryOnly, setRegistryOnly] = useState(false); + + const olGraphQuery = useLoadOpenLineageGraph(); + + const olConsumerAvailable = + !olGraphQuery.isError && olGraphQuery.data !== undefined; + if (projectName === "all") { return ( @@ -81,7 +99,66 @@ const LineagePage = () => { /> )} - {isSuccess && } + {isSuccess && ( + <> + {olConsumerAvailable ? ( + <> + setActiveTab(id as ActiveTab)} + buttonSize="m" + isFullWidth={false} + /> + + + {activeTab === "lineage" && ( + <> + {registryOnly ? ( + + + setRegistryOnly(e.target.checked) + } + /> + {" Feast Only Lineage"} + + } + /> + ) : ( + + + setRegistryOnly(e.target.checked) + } + /> + {" Feast Only Lineage"} + + } + /> + )} + + )} + + {activeTab === "events" && } + + ) : ( + + )} + + )} ); diff --git a/ui/src/pages/monitoring/FeatureMetricsDetail.tsx b/ui/src/pages/monitoring/FeatureMetricsDetail.tsx new file mode 100644 index 00000000000..51f690ffa55 --- /dev/null +++ b/ui/src/pages/monitoring/FeatureMetricsDetail.tsx @@ -0,0 +1,259 @@ +import React, { useState, useMemo } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + EuiPageTemplate, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSkeletonText, + EuiEmptyPrompt, + EuiButton, + EuiBreadcrumbs, + EuiSuperSelect, + EuiFormRow, +} from "@elastic/eui"; +import { FeatureIcon } from "../../graphics/FeatureIcon"; +import { + useFeatureMetrics, + useBaselineMetrics, +} from "../../queries/useMonitoringApi"; +import type { + NumericHistogram, + CategoricalHistogram, +} from "../../queries/useMonitoringApi"; +import { + NumericHistogramChart, + CategoricalHistogramChart, +} from "./components/HistogramChart"; +import StatsPanel from "./components/StatsPanel"; +import TimeSeriesAnalysis from "./components/TimeSeriesAnalysis"; +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; + +const BASELINE_KEY = "__baseline__"; + +const GRANULARITY_LABELS: Record = { + daily: "Daily", + weekly: "Weekly", + biweekly: "Biweekly", + monthly: "Monthly", + quarterly: "Quarterly", + [BASELINE_KEY]: "Baseline", +}; + +const FeatureMetricsDetail = () => { + const { projectName, featureViewName, featureName } = useParams(); + const navigate = useNavigate(); + const [selectedGranularity, setSelectedGranularity] = useState(""); + + useDocumentTitle(`${featureName} Monitoring | ${featureViewName} | Feast`); + + const { + data: metrics, + isLoading, + isError, + } = useFeatureMetrics({ + project: projectName || "", + feature_view_name: featureViewName, + feature_name: featureName, + }); + + const { data: baselineMetrics } = useBaselineMetrics( + projectName || "", + featureViewName, + featureName, + ); + + const baselineMetric = + baselineMetrics && baselineMetrics.length > 0 ? baselineMetrics[0] : null; + + const availableGranularities = useMemo(() => { + const granularities = new Set(); + if (metrics) { + for (const m of metrics) { + if (m.row_count > 0) granularities.add(m.granularity); + } + } + return Array.from(granularities).sort(); + }, [metrics]); + + const granularityOptions = useMemo(() => { + const options = availableGranularities.map((g) => ({ + value: g, + inputDisplay: GRANULARITY_LABELS[g] || g, + dropdownDisplay: GRANULARITY_LABELS[g] || g, + })); + if (baselineMetric) { + options.push({ + value: BASELINE_KEY, + inputDisplay: "Baseline", + dropdownDisplay: "Baseline (all data)", + }); + } + return options; + }, [availableGranularities, baselineMetric]); + + const effectiveGranularity = + selectedGranularity || availableGranularities[0] || ""; + + const activeMetric = useMemo(() => { + if (effectiveGranularity === BASELINE_KEY && baselineMetric) { + return baselineMetric; + } + if (!metrics || metrics.length === 0) return null; + const matching = metrics.filter( + (m) => m.granularity === effectiveGranularity && m.row_count > 0, + ); + if (matching.length === 0) { + const withData = metrics.filter((m) => m.row_count > 0); + const candidates = withData.length > 0 ? withData : metrics; + return candidates.reduce((a, b) => + a.metric_date > b.metric_date ? a : b, + ); + } + return matching.reduce((a, b) => (a.metric_date > b.metric_date ? a : b)); + }, [metrics, effectiveGranularity, baselineMetric]); + + const breadcrumbs = [ + { + text: "Monitoring", + onClick: () => navigate(`/p/${projectName}/monitoring`), + }, + { + text: featureViewName || "", + }, + { + text: featureName || "", + }, + ]; + + if (isLoading) { + return ( + + + + + + ); + } + + if (isError || !activeMetric) { + return ( + + + + + No Metrics Available} + body={ +

+ No monitoring metrics found for feature{" "} + {featureName} in feature view{" "} + {featureViewName}. Run a monitoring compute job + first. +

+ } + actions={ + navigate(`/p/${projectName}/monitoring`)} + > + Back to Monitoring + + } + /> +
+
+ ); + } + + const isNumeric = activeMetric.feature_type === "numeric"; + + return ( + + navigate(`/p/${projectName}/monitoring`)} + > + Back to Monitoring + , + ]} + /> + + + + + {granularityOptions.length > 0 && ( + <> + + + + setSelectedGranularity(val)} + compressed + /> + + + + + + )} + + + + {isNumeric && activeMetric.histogram && ( + + )} + {!isNumeric && activeMetric.histogram && ( + + )} + {!activeMetric.histogram && ( + No Histogram Data} + body={

Histogram data is not available for this metric.

} + /> + )} +
+ + + + +
+ + {metrics && metrics.length > 1 && ( + <> + + + + )} +
+
+ ); +}; + +export default FeatureMetricsDetail; diff --git a/ui/src/pages/monitoring/FeatureMetricsTable.tsx b/ui/src/pages/monitoring/FeatureMetricsTable.tsx new file mode 100644 index 00000000000..5b998c98aee --- /dev/null +++ b/ui/src/pages/monitoring/FeatureMetricsTable.tsx @@ -0,0 +1,426 @@ +import React, { useState, useMemo, useEffect } from "react"; +import { + EuiBasicTable, + EuiBasicTableColumn, + EuiBadge, + EuiButtonIcon, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiLink, + EuiPopover, + EuiProgress, + EuiTitle, + EuiToolTip, + Criteria, +} from "@elastic/eui"; +import type { + FeatureMetric, + NumericHistogram, + CategoricalHistogram, +} from "../../queries/useMonitoringApi"; + +const healthColor = (nullRate: number): string => { + if (nullRate >= 0.5) return "danger"; + if (nullRate >= 0.1) return "warning"; + return "success"; +}; + +const healthLabel = (nullRate: number): string => { + if (nullRate >= 0.5) return "High null rate"; + if (nullRate >= 0.1) return "Moderate null rate"; + return "Healthy"; +}; + +const formatNum = (val: number | null, decimals = 2): string => { + if (val === null || val === undefined) return "—"; + if (Number.isInteger(val)) return val.toLocaleString(); + return val.toFixed(decimals); +}; + +const formatFreshness = (computedAt: string | null): string => { + if (!computedAt) return "—"; + const diff = Date.now() - new Date(computedAt).getTime(); + const mins = Math.floor(diff / 60_000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + const days = Math.floor(hrs / 24); + if (days < 30) return `${days}d ago`; + return `${Math.floor(days / 30)}mo ago`; +}; + +const freshnessColor = (computedAt: string | null): string => { + if (!computedAt) return "subdued"; + const hrs = (Date.now() - new Date(computedAt).getTime()) / 3_600_000; + if (hrs < 24) return "success"; + if (hrs < 72) return "warning"; + return "danger"; +}; + +const MiniHistogram = ({ metric }: { metric: FeatureMetric }) => { + if (!metric.histogram) return ; + + const width = 120; + const height = 28; + + if (metric.feature_type === "numeric") { + const hist = metric.histogram as NumericHistogram; + const maxCount = Math.max(...hist.counts, 1); + const barW = Math.max(Math.floor(width / hist.counts.length) - 1, 2); + + return ( + + + {hist.counts.map((count, i) => { + const h = (count / maxCount) * (height - 2); + return ( + + ); + })} + + + ); + } + + const hist = metric.histogram as CategoricalHistogram; + const maxCount = Math.max(...hist.values.map((v) => v.count), 1); + const barW = Math.max( + Math.floor(width / Math.min(hist.values.length, 10)) - 1, + 6, + ); + + return ( + + + {hist.values.slice(0, 10).map((v, i) => { + const h = (v.count / maxCount) * (height - 2); + return ( + + ); + })} + + + ); +}; + +interface FeatureMetricsTableProps { + metrics: FeatureMetric[]; + isLoading: boolean; + onFeatureClick: (fvName: string, featureName: string) => void; +} + +const PAGE_SIZE_OPTIONS = [10, 20, 50]; + +const FeatureMetricsTable = ({ + metrics, + isLoading, + onFeatureClick, +}: FeatureMetricsTableProps) => { + const [sortField, setSortField] = + useState("feature_view_name"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(20); + + useEffect(() => { + setPageIndex(0); + }, [metrics]); + + const latestMetrics = useMemo(() => { + const byKey = new Map(); + for (const m of metrics) { + const key = `${m.feature_view_name}::${m.feature_name}`; + const existing = byKey.get(key); + if (!existing) { + byKey.set(key, m); + } else { + const preferNew = + m.row_count > 0 && existing.row_count === 0 + ? true + : existing.row_count > 0 && m.row_count === 0 + ? false + : m.metric_date > existing.metric_date; + if (preferNew) byKey.set(key, m); + } + } + return Array.from(byKey.values()); + }, [metrics]); + + const sortedItems = useMemo(() => { + return [...latestMetrics].sort((a, b) => { + const aVal = a[sortField]; + const bVal = b[sortField]; + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + if (aVal < bVal) return sortDirection === "asc" ? -1 : 1; + if (aVal > bVal) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + }, [latestMetrics, sortField, sortDirection]); + + const pageOfItems = useMemo(() => { + const start = pageIndex * pageSize; + return sortedItems.slice(start, start + pageSize); + }, [sortedItems, pageIndex, pageSize]); + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount: sortedItems.length, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }), + [pageIndex, pageSize, sortedItems.length], + ); + + const onTableChange = ({ sort, page }: Criteria) => { + if (sort) { + setSortField(sort.field as keyof FeatureMetric); + setSortDirection(sort.direction); + } + if (page) { + setPageIndex(page.index); + setPageSize(page.size); + } + }; + + const [isLegendOpen, setIsLegendOpen] = useState(false); + + const columnLegend = [ + { + title: "Feature", + description: + "Name of the individual feature. Click to view full distribution and detailed statistics.", + }, + { + title: "Feature View", + description: + "The feature view this feature belongs to — a logical grouping of related features sharing the same data source.", + }, + { + title: "Type", + description: + "Data type: numeric (continuous/discrete numbers) or categorical (strings/labels).", + }, + { + title: "Distribution", + description: + "Compact histogram showing the value distribution. Blue bars = numeric, orange bars = categorical.", + }, + { + title: "Rows", + description: + "Total number of rows (data points) observed for this feature in the computed time window.", + }, + { + title: "Null Rate", + description: + "Percentage of rows with missing (null) values. Shown as a progress bar colored by severity.", + }, + { + title: "Health", + description: + "Data quality indicator based on null rate: Healthy (< 10%), Moderate (10–49%), High (>= 50%).", + }, + { + title: "Mean", + description: + "Arithmetic mean of the feature values. Only shown for numeric features.", + }, + { + title: "Std Dev", + description: + "Standard deviation — measures how spread out the values are from the mean. Only for numeric features.", + }, + { + title: "Freshness", + description: + "Recency of the underlying data. Green (< 24h old), Yellow (24–72h), Red (> 72h). Hover for the data date.", + }, + { + title: "Source", + description: + "Data source type used for metric computation (e.g. batch, stream).", + }, + ]; + + const columns: EuiBasicTableColumn[] = [ + { + field: "feature_name", + name: "Feature", + sortable: true, + render: (name: string, item: FeatureMetric) => ( + onFeatureClick(item.feature_view_name, name)}> + {name} + + ), + }, + { + field: "feature_view_name", + name: "Feature View", + sortable: true, + }, + { + field: "feature_type", + name: "Type", + sortable: true, + width: "100px", + render: (type: string) => ( + + {type} + + ), + }, + { + name: "Distribution", + width: "140px", + render: (item: FeatureMetric) => , + }, + { + field: "row_count", + name: "Rows", + sortable: true, + width: "90px", + render: (val: number) => formatNum(val, 0), + }, + { + field: "null_rate", + name: "Null Rate", + sortable: true, + width: "150px", + render: (val: number) => ( +
+ + {(val * 100).toFixed(1)}% +
+ ), + }, + { + field: "null_rate", + name: "Health", + width: "130px", + render: (val: number) => ( + {healthLabel(val)} + ), + }, + { + field: "mean", + name: "Mean", + sortable: true, + width: "100px", + render: (val: number | null) => formatNum(val), + }, + { + field: "stddev", + name: "Std Dev", + sortable: true, + width: "100px", + render: (val: number | null) => formatNum(val), + }, + { + field: "metric_date", + name: "Freshness", + sortable: true, + width: "110px", + render: (val: string) => ( + + + {formatFreshness(val)} + + + ), + }, + { + field: "data_source_type", + name: "Source", + width: "80px", + render: (val: string) => {val}, + }, + ]; + + return ( + <> + + + setIsLegendOpen(!isLegendOpen)} + /> + } + isOpen={isLegendOpen} + closePopover={() => setIsLegendOpen(false)} + anchorPosition="downRight" + panelPaddingSize="m" + panelStyle={{ maxWidth: 420 }} + > + +

Column Legend

+
+ +
+
+
+ ({ + "data-test-subj": `row-${item.feature_name}`, + })} + noItemsMessage={ + isLoading + ? "Loading metrics..." + : "No metrics found. Run a monitoring compute job to generate metrics." + } + /> + + ); +}; + +export default FeatureMetricsTable; diff --git a/ui/src/pages/monitoring/FeatureServiceMetricsPanel.tsx b/ui/src/pages/monitoring/FeatureServiceMetricsPanel.tsx new file mode 100644 index 00000000000..536c5d56c6c --- /dev/null +++ b/ui/src/pages/monitoring/FeatureServiceMetricsPanel.tsx @@ -0,0 +1,225 @@ +import React, { useState, useMemo } from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiBasicTable, + EuiBasicTableColumn, + EuiProgress, + EuiBadge, + EuiSkeletonText, + Criteria, +} from "@elastic/eui"; +import type { FeatureServiceMetric } from "../../queries/useMonitoringApi"; + +const healthColor = (nullRate: number): string => { + if (nullRate >= 0.5) return "danger"; + if (nullRate >= 0.1) return "warning"; + return "success"; +}; + +interface FeatureServiceMetricsPanelProps { + metrics: FeatureServiceMetric[]; + isLoading: boolean; +} + +const FeatureServiceMetricsPanel = ({ + metrics, + isLoading, +}: FeatureServiceMetricsPanelProps) => { + if (isLoading) { + return ( + + +

Feature Service Metrics

+
+ + +
+ ); + } + + const latestByFS = new Map(); + for (const m of metrics) { + const existing = latestByFS.get(m.feature_service_name); + if (!existing || m.metric_date > existing.metric_date) { + latestByFS.set(m.feature_service_name, m); + } + } + const latestMetrics = Array.from(latestByFS.values()); + + const totalViews = latestMetrics.reduce( + (sum, m) => sum + (m.total_feature_views || 0), + 0, + ); + const totalFeatures = latestMetrics.reduce( + (sum, m) => sum + (m.total_features || 0), + 0, + ); + const avgNullRate = + latestMetrics.length > 0 + ? latestMetrics.reduce((sum, m) => sum + (m.avg_null_rate || 0), 0) / + latestMetrics.length + : 0; + + const columns: EuiBasicTableColumn[] = [ + { + field: "feature_service_name", + name: "Feature Service", + sortable: true, + }, + { + field: "total_feature_views", + name: "Feature Views", + sortable: true, + width: "110px", + }, + { + field: "total_features", + name: "Features", + sortable: true, + width: "80px", + }, + { + field: "avg_null_rate", + name: "Avg Null Rate", + sortable: true, + render: (val: number) => ( +
+ + {((val || 0) * 100).toFixed(1)}% +
+ ), + }, + { + field: "max_null_rate", + name: "Max Null Rate", + sortable: true, + width: "110px", + render: (val: number) => `${((val || 0) * 100).toFixed(1)}%`, + }, + { + field: "metric_date", + name: "Date", + sortable: true, + width: "110px", + }, + { + field: "data_source_type", + name: "Source", + width: "80px", + render: (val: string) => {val}, + }, + ]; + + return ( + + +

Feature Service Metrics

+
+

+ Aggregated data quality metrics across feature services. +

+ + + + + + + + + + + + + + + + + + + + {latestMetrics.length > 0 && ( + + )} +
+ ); +}; + +const SortableFSTable = ({ + items, + columns, +}: { + items: FeatureServiceMetric[]; + columns: EuiBasicTableColumn[]; +}) => { + const [sortField, setSortField] = useState("feature_service_name"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + + const sortedItems = useMemo(() => { + return [...items].sort((a, b) => { + const aVal = (a as any)[sortField]; + const bVal = (b as any)[sortField]; + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + if (aVal < bVal) return sortDirection === "asc" ? -1 : 1; + if (aVal > bVal) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + }, [items, sortField, sortDirection]); + + const onTableChange = ({ sort }: Criteria) => { + if (sort) { + setSortField(sort.field as string); + setSortDirection(sort.direction); + } + }; + + return ( + + ); +}; + +export default FeatureServiceMetricsPanel; diff --git a/ui/src/pages/monitoring/FeatureViewMetricsPanel.tsx b/ui/src/pages/monitoring/FeatureViewMetricsPanel.tsx new file mode 100644 index 00000000000..a0dcc78a8ab --- /dev/null +++ b/ui/src/pages/monitoring/FeatureViewMetricsPanel.tsx @@ -0,0 +1,241 @@ +import React, { useState, useMemo } from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiBasicTable, + EuiBasicTableColumn, + EuiProgress, + EuiBadge, + EuiSkeletonText, + Criteria, +} from "@elastic/eui"; +import type { FeatureViewMetric } from "../../queries/useMonitoringApi"; + +const healthColor = (nullRate: number): string => { + if (nullRate >= 0.5) return "danger"; + if (nullRate >= 0.1) return "warning"; + return "success"; +}; + +interface FeatureViewMetricsPanelProps { + metrics: FeatureViewMetric[]; + isLoading: boolean; + title: string; + description?: string; +} + +const FeatureViewMetricsPanel = ({ + metrics, + isLoading, + title, + description, +}: FeatureViewMetricsPanelProps) => { + if (isLoading) { + return ( + + +

{title}

+
+ + +
+ ); + } + + const latestByFV = new Map(); + for (const m of metrics) { + const existing = latestByFV.get(m.feature_view_name); + if (!existing || m.metric_date > existing.metric_date) { + latestByFV.set(m.feature_view_name, m); + } + } + const latestMetrics = Array.from(latestByFV.values()); + + const totalRows = latestMetrics.reduce( + (sum, m) => sum + (m.total_row_count || 0), + 0, + ); + const totalFeatures = latestMetrics.reduce( + (sum, m) => sum + (m.total_features || 0), + 0, + ); + const avgNullRate = + latestMetrics.length > 0 + ? latestMetrics.reduce((sum, m) => sum + (m.avg_null_rate || 0), 0) / + latestMetrics.length + : 0; + const healthyViews = latestMetrics.filter( + (m) => m.avg_null_rate < 0.1, + ).length; + + const columns: EuiBasicTableColumn[] = [ + { + field: "feature_view_name", + name: "Feature View", + sortable: true, + }, + { + field: "total_row_count", + name: "Total Rows", + sortable: true, + render: (val: number) => (val || 0).toLocaleString(), + }, + { + field: "total_features", + name: "Features", + sortable: true, + width: "80px", + }, + { + field: "features_with_nulls", + name: "With Nulls", + sortable: true, + width: "90px", + }, + { + field: "avg_null_rate", + name: "Avg Null Rate", + sortable: true, + render: (val: number) => ( +
+ + {((val || 0) * 100).toFixed(1)}% +
+ ), + }, + { + field: "max_null_rate", + name: "Max Null Rate", + sortable: true, + width: "110px", + render: (val: number) => `${((val || 0) * 100).toFixed(1)}%`, + }, + { + field: "metric_date", + name: "Date", + sortable: true, + width: "110px", + }, + { + field: "data_source_type", + name: "Source", + width: "80px", + render: (val: string) => {val}, + }, + ]; + + return ( + + +

{title}

+
+ {description && ( +

+ {description} +

+ )} + + + + + + + + + + + + + + + + + + + + {latestMetrics.length > 0 && ( + + )} +
+ ); +}; + +const SortableTable = ({ + items, + columns, +}: { + items: FeatureViewMetric[]; + columns: EuiBasicTableColumn[]; +}) => { + const [sortField, setSortField] = useState("feature_view_name"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + + const sortedItems = useMemo(() => { + return [...items].sort((a, b) => { + const aVal = (a as any)[sortField]; + const bVal = (b as any)[sortField]; + if (aVal == null && bVal == null) return 0; + if (aVal == null) return 1; + if (bVal == null) return -1; + if (aVal < bVal) return sortDirection === "asc" ? -1 : 1; + if (aVal > bVal) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + }, [items, sortField, sortDirection]); + + const onTableChange = ({ sort }: Criteria) => { + if (sort) { + setSortField(sort.field as string); + setSortDirection(sort.direction); + } + }; + + return ( + + ); +}; + +export default FeatureViewMetricsPanel; diff --git a/ui/src/pages/monitoring/Index.tsx b/ui/src/pages/monitoring/Index.tsx new file mode 100644 index 00000000000..c3576d07c3d --- /dev/null +++ b/ui/src/pages/monitoring/Index.tsx @@ -0,0 +1,327 @@ +import React, { useState, useContext, useMemo } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { + EuiPageTemplate, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, + EuiEmptyPrompt, + EuiButton, + EuiCallOut, +} from "@elastic/eui"; + +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { + isServiceUnavailable, + useFeatureMetrics, + useFeatureViewMetrics, + useFeatureServiceMetrics, + useComputeMetrics, +} from "../../queries/useMonitoringApi"; +import FeatureMetricsTable from "./FeatureMetricsTable"; +import FeatureViewMetricsPanel from "./FeatureViewMetricsPanel"; +import FeatureServiceMetricsPanel from "./FeatureServiceMetricsPanel"; +import MetricsFilters from "./components/MetricsFilters"; + +const MonitoringIndex = () => { + useDocumentTitle("Monitoring | Feast"); + + const { projectName } = useParams(); + const navigate = useNavigate(); + const registryUrl = useContext(RegistryPathContext); + const { data: registryData } = useLoadRegistry(registryUrl, projectName); + + const [selectedFV, setSelectedFV] = useState(""); + const [granularity, setGranularity] = useState("daily"); + const [dataSourceType, setDataSourceType] = useState(""); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + + const isBaseline = granularity === "baseline"; + + const handleGranularityChange = (g: string) => { + setGranularity(g); + if (g === "baseline") { + setStartDate(""); + setEndDate(""); + } + }; + + const filters = useMemo( + () => ({ + project: projectName || "", + feature_view_name: selectedFV || undefined, + granularity: isBaseline ? undefined : granularity || undefined, + data_source_type: dataSourceType || undefined, + start_date: isBaseline ? undefined : startDate || undefined, + end_date: isBaseline ? undefined : endDate || undefined, + is_baseline: isBaseline || undefined, + }), + [ + projectName, + selectedFV, + granularity, + isBaseline, + dataSourceType, + startDate, + endDate, + ], + ); + + const featureQuery = useFeatureMetrics(filters); + const fvQuery = useFeatureViewMetrics(filters); + const fsQuery = useFeatureServiceMetrics({ + project: projectName || "", + granularity: isBaseline ? undefined : granularity || undefined, + data_source_type: dataSourceType || undefined, + start_date: startDate || undefined, + end_date: endDate || undefined, + is_baseline: isBaseline || undefined, + }); + const computeMutation = useComputeMetrics(); + + const featureViews = useMemo(() => { + if (!registryData?.mergedFVList) return []; + return registryData.mergedFVList.map((fv: any) => fv.name as string); + }, [registryData]); + + const handleFeatureClick = (fvName: string, featureName: string) => { + navigate(`/p/${projectName}/monitoring/feature/${fvName}/${featureName}`); + }; + + const uniqueFeatureCount = useMemo(() => { + if (!featureQuery.data) return 0; + const seen = new Set(); + for (const m of featureQuery.data) { + seen.add(`${m.feature_view_name}::${m.feature_name}`); + } + return seen.size; + }, [featureQuery.data]); + + const handleRefresh = () => { + featureQuery.refetch(); + fvQuery.refetch(); + fsQuery.refetch(); + }; + + const handleCompute = () => { + computeMutation.mutate({ + project: projectName || "", + feature_view_name: selectedFV || undefined, + }); + }; + + const allFailed = featureQuery.isError && fvQuery.isError && fsQuery.isError; + const monitoringNotEnabled = + allFailed && + isServiceUnavailable(featureQuery.error) && + isServiceUnavailable(fvQuery.error) && + isServiceUnavailable(fsQuery.error); + const hasError = allFailed && !monitoringNotEnabled; + const hasData = + (featureQuery.data && featureQuery.data.length > 0) || + (fvQuery.data && fvQuery.data.length > 0); + + const tabs: EuiTabbedContentTab[] = [ + { + id: "features", + name: `Features${uniqueFeatureCount > 0 ? ` (${uniqueFeatureCount})` : ""}`, + content: ( + <> + + + + ), + }, + { + id: "feature-views", + name: "Feature Views", + content: ( + <> + + + + ), + }, + { + id: "feature-services", + name: "Feature Services", + content: ( + <> + + + + ), + }, + ]; + + if (monitoringNotEnabled) { + return ( + + + + Monitoring Is Not Enabled} + body={ + <> +

+ Data quality monitoring is not configured for this Feast + deployment. +

+

+ To enable monitoring, add the following to your{" "} + feature_store.yaml: +

+
+                  {`data_quality_monitoring:
+  auto_baseline: true`}
+                
+

Then restart the Feast registry server.

+ + } + /> +
+
+ ); + } + + return ( + + + Compute Metrics + , + ]} + /> + + {hasError && ( + <> + +

+ Could not connect to the monitoring API. Make sure the Feast + registry server is running with monitoring enabled. +

+
+ + + )} + + + + + + {!hasData && !featureQuery.isLoading && !hasError && ( + No Metrics Yet} + body={ +

+ No monitoring data has been computed for this project. Click + "Compute Metrics" to run data quality analysis on your + feature views, or use the CLI:{" "} + feast monitor run --data-source batch +

+ } + actions={ + + Compute Metrics + + } + /> + )} + + {(hasData || featureQuery.isLoading) && ( + + )} + + {computeMutation.isSuccess && ( + <> + + +

+ Data quality metrics have been computed. The table above has + been refreshed. +

+
+ + )} + + {computeMutation.isError && ( + <> + + +

{(computeMutation.error as Error)?.message}

+
+ + )} +
+
+ ); +}; + +export default MonitoringIndex; diff --git a/ui/src/pages/monitoring/components/HistogramChart.tsx b/ui/src/pages/monitoring/components/HistogramChart.tsx new file mode 100644 index 00000000000..a716b025021 --- /dev/null +++ b/ui/src/pages/monitoring/components/HistogramChart.tsx @@ -0,0 +1,435 @@ +import React, { useState } from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiText, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, +} from "@elastic/eui"; +import type { + NumericHistogram, + CategoricalHistogram, +} from "../../../queries/useMonitoringApi"; + +const BAR_COLOR = "#006BB4"; +const BAR_COLOR_BASELINE = "#BD271E55"; + +interface ChartDimensions { + chartHeight: number; + axisHeight: number; + leftPad: number; + barGap: number; + minBarWidth: number; + targetBarsWidth: number; + fontSize: number; + xTickCount: number; +} + +const COMPACT: ChartDimensions = { + chartHeight: 160, + axisHeight: 28, + leftPad: 54, + barGap: 2, + minBarWidth: 6, + targetBarsWidth: 460, + fontSize: 10, + xTickCount: 2, +}; + +const EXPANDED: ChartDimensions = { + chartHeight: 400, + axisHeight: 48, + leftPad: 72, + barGap: 3, + minBarWidth: 12, + targetBarsWidth: 800, + fontSize: 12, + xTickCount: 6, +}; + +const formatNumber = (val: number, compact: boolean): string => { + if (val === 0) return "0"; + const abs = Math.abs(val); + if (compact && abs >= 1_000_000) return (val / 1_000_000).toFixed(1) + "M"; + if (compact && abs >= 1_000) return (val / 1_000).toFixed(1) + "K"; + if (abs >= 1) + return val.toLocaleString(undefined, { maximumFractionDigits: 1 }); + if (abs >= 0.01) return val.toFixed(2); + return val.toExponential(1); +}; + +const renderNumericSvg = ( + histogram: NumericHistogram, + baseline: NumericHistogram | null | undefined, + dim: ChartDimensions, +) => { + const maxCount = Math.max( + ...histogram.counts, + ...(baseline ? baseline.counts : []), + 1, + ); + const numBars = histogram.counts.length; + const barWidth = Math.max( + Math.floor(dim.targetBarsWidth / numBars) - dim.barGap, + dim.minBarWidth, + ); + const barsWidth = (barWidth + dim.barGap) * numBars; + const svgWidth = dim.leftPad + barsWidth + 24; + const isCompact = dim === COMPACT; + + const yTickFractions = [0, 0.25, 0.5, 0.75, 1]; + const yTicks = yTickFractions.map((f) => ({ + label: formatNumber(Math.round(maxCount * f), isCompact), + y: dim.chartHeight - f * dim.chartHeight, + })); + + const xTickStep = Math.max(1, Math.floor(numBars / dim.xTickCount)); + const xTicks: { label: string; x: number }[] = []; + for (let i = 0; i < numBars; i += xTickStep) { + xTicks.push({ + label: formatNumber(histogram.bins[i], isCompact), + x: dim.leftPad + i * (barWidth + dim.barGap) + barWidth / 2, + }); + } + if (numBars > 0) { + const lastBin = histogram.bins[histogram.bins.length - 1]; + xTicks.push({ + label: formatNumber(lastBin, isCompact), + x: dim.leftPad + (numBars - 1) * (barWidth + dim.barGap) + barWidth / 2, + }); + } + + return ( + + {yTicks.map((t, i) => ( + + + + {t.label} + + + ))} + {histogram.counts.map((count, i) => { + const height = (count / maxCount) * dim.chartHeight; + const x = dim.leftPad + i * (barWidth + dim.barGap); + const binStart = histogram.bins[i]; + const binEnd = + i < histogram.bins.length - 1 + ? histogram.bins[i + 1] + : binStart + histogram.bin_width; + const baselineHeight = + baseline && baseline.counts[i] + ? (baseline.counts[i] / maxCount) * dim.chartHeight + : 0; + + return ( + + {baselineHeight > 0 && ( + + )} + + {`${formatNumber(binStart, false)} – ${formatNumber(binEnd, false)}: ${count.toLocaleString()}`} + + + ); + })} + + {xTicks.map((t, i) => ( + + {t.label} + + ))} + + ); +}; + +const NumericHistogramChart = ({ + histogram, + baseline, + title, +}: { + histogram: NumericHistogram; + baseline?: NumericHistogram | null; + title?: string; +}) => { + const [expanded, setExpanded] = useState(false); + + return ( + <> + + + + {title && ( + +

{title}

+
+ )} +
+ + + setExpanded(true)} + /> + + +
+ {title && } +
+ {renderNumericSvg(histogram, baseline, COMPACT)} +
+ {baseline && ( + + + Baseline + + )} +
+ + {expanded && ( + setExpanded(false)} maxWidth={960}> + + {title || "Histogram"} + + +
+ {renderNumericSvg(histogram, baseline, EXPANDED)} +
+ {baseline && ( + <> + + + + Baseline + + + )} +
+
+ )} + + ); +}; + +const LABEL_WIDTH = 60; +const BAR_MAX_WIDTH = 320; +const COUNT_PAD = 80; + +const LABEL_WIDTH_EXP = 120; +const BAR_MAX_WIDTH_EXP = 560; +const COUNT_PAD_EXP = 100; + +const renderCategoricalSvg = ( + histogram: CategoricalHistogram, + isExpanded: boolean, +) => { + const labelW = isExpanded ? LABEL_WIDTH_EXP : LABEL_WIDTH; + const barMax = isExpanded ? BAR_MAX_WIDTH_EXP : BAR_MAX_WIDTH; + const countPad = isExpanded ? COUNT_PAD_EXP : COUNT_PAD; + const totalW = labelW + barMax + countPad; + const truncLen = isExpanded ? 20 : 8; + const fontSize = isExpanded ? 13 : 12; + const barHeight = isExpanded ? 30 : 24; + const rowHeight = barHeight + 6; + + const maxCount = Math.max(...histogram.values.map((v) => v.count), 1); + const chartHeight = histogram.values.length * rowHeight; + + return ( + + {histogram.values.map((v, i) => { + const width = (v.count / maxCount) * barMax; + const y = i * rowHeight; + return ( + + + {v.value.length > truncLen + ? v.value.slice(0, truncLen) + "…" + : v.value} + + + {`${v.value}: ${v.count.toLocaleString()}`} + + + {v.count.toLocaleString()} + + + ); + })} + + ); +}; + +const CategoricalHistogramChart = ({ + histogram, + title, +}: { + histogram: CategoricalHistogram; + title?: string; +}) => { + const [expanded, setExpanded] = useState(false); + + return ( + <> + + + + {title && ( + +

{title}

+
+ )} +
+ + + setExpanded(true)} + /> + + +
+ {title && } +
+ {renderCategoricalSvg(histogram, false)} +
+ + {histogram.unique_count} unique values + {histogram.other_count > 0 && + ` (${histogram.other_count.toLocaleString()} in other categories)`} + +
+ + {expanded && ( + setExpanded(false)} maxWidth={960}> + + + {title || "Category Distribution"} + + + +
+ {renderCategoricalSvg(histogram, true)} +
+ + + {histogram.unique_count} unique values + {histogram.other_count > 0 && + ` (${histogram.other_count.toLocaleString()} in other categories)`} + +
+
+ )} + + ); +}; + +export { NumericHistogramChart, CategoricalHistogramChart }; diff --git a/ui/src/pages/monitoring/components/MetricsFilters.tsx b/ui/src/pages/monitoring/components/MetricsFilters.tsx new file mode 100644 index 00000000000..977044d495a --- /dev/null +++ b/ui/src/pages/monitoring/components/MetricsFilters.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSelect, + EuiFieldText, + EuiFormRow, + EuiButton, +} from "@elastic/eui"; + +interface MetricsFiltersProps { + featureViews: string[]; + selectedFeatureView: string; + onFeatureViewChange: (fv: string) => void; + granularity: string; + onGranularityChange: (g: string) => void; + dataSourceType: string; + onDataSourceTypeChange: (ds: string) => void; + startDate: string; + onStartDateChange: (d: string) => void; + endDate: string; + onEndDateChange: (d: string) => void; + onRefresh: () => void; + isLoading?: boolean; + datesDisabled?: boolean; +} + +const GRANULARITY_OPTIONS = [ + { value: "baseline", text: "Baseline" }, + { value: "daily", text: "Daily" }, + { value: "weekly", text: "Weekly" }, + { value: "biweekly", text: "Biweekly" }, + { value: "monthly", text: "Monthly" }, + { value: "quarterly", text: "Quarterly" }, +]; + +const DATA_SOURCE_OPTIONS = [ + { value: "", text: "All Sources" }, + { value: "batch", text: "Batch" }, + { value: "log", text: "Log" }, +]; + +const MetricsFilters = ({ + featureViews, + selectedFeatureView, + onFeatureViewChange, + granularity, + onGranularityChange, + dataSourceType, + onDataSourceTypeChange, + startDate, + onStartDateChange, + endDate, + onEndDateChange, + onRefresh, + isLoading, + datesDisabled, +}: MetricsFiltersProps) => { + const fvOptions = [ + { value: "", text: "All Feature Views" }, + ...featureViews.map((fv) => ({ value: fv, text: fv })), + ]; + + return ( + + + + onFeatureViewChange(e.target.value)} + compressed + /> + + + + + onGranularityChange(e.target.value)} + compressed + /> + + + + + onDataSourceTypeChange(e.target.value)} + compressed + /> + + + + + onStartDateChange(e.target.value)} + compressed + disabled={datesDisabled} + /> + + + + + onEndDateChange(e.target.value)} + compressed + disabled={datesDisabled} + /> + + + + + Refresh + + + + ); +}; + +export default MetricsFilters; diff --git a/ui/src/pages/monitoring/components/StatsPanel.tsx b/ui/src/pages/monitoring/components/StatsPanel.tsx new file mode 100644 index 00000000000..070b99373e7 --- /dev/null +++ b/ui/src/pages/monitoring/components/StatsPanel.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, +} from "@elastic/eui"; +import type { FeatureMetric } from "../../../queries/useMonitoringApi"; + +const formatNumber = (val: number | null, decimals = 4): string => { + if (val === null || val === undefined) return "—"; + if (Number.isInteger(val)) return val.toLocaleString(); + return val.toFixed(decimals); +}; + +const formatPercent = (val: number | null): string => { + if (val === null || val === undefined) return "—"; + return `${(val * 100).toFixed(2)}%`; +}; + +const StatsPanel = ({ + metric, + baseline, +}: { + metric: FeatureMetric; + baseline?: FeatureMetric | null; +}) => { + const isNumeric = metric.feature_type === "numeric"; + + return ( + + + + +

Statistics

+
+
+ + + {metric.feature_type} + + +
+ + + Row Count + + {formatNumber(metric.row_count, 0)} + {baseline && ( + + (baseline: {formatNumber(baseline.row_count, 0)}) + + )} + + + Null Rate + + 0.1 ? "#BD271E" : "inherit", + fontWeight: metric.null_rate > 0.1 ? 600 : 400, + }} + > + {formatPercent(metric.null_rate)} + + {baseline && ( + + (baseline: {formatPercent(baseline.null_rate)}) + + )} + + + {isNumeric && ( + <> + Mean + + {formatNumber(metric.mean)} + {baseline && ( + + (baseline: {formatNumber(baseline.mean)}) + + )} + + + Std Dev + + {formatNumber(metric.stddev)} + + + Min / Max + + {formatNumber(metric.min_val)} / {formatNumber(metric.max_val)} + + + Percentiles + + P50: {formatNumber(metric.p50)} | P75: {formatNumber(metric.p75)}{" "} + | P90: {formatNumber(metric.p90)} | P95:{" "} + {formatNumber(metric.p95)} | P99: {formatNumber(metric.p99)} + + + )} + + Data Source + + {metric.data_source_type} + + + Granularity + + {metric.granularity} + + + Computed At + + {metric.computed_at + ? new Date(metric.computed_at).toLocaleString() + : "—"} + + +
+ ); +}; + +export default StatsPanel; diff --git a/ui/src/pages/monitoring/components/TimeSeriesAnalysis.tsx b/ui/src/pages/monitoring/components/TimeSeriesAnalysis.tsx new file mode 100644 index 00000000000..23acd0be434 --- /dev/null +++ b/ui/src/pages/monitoring/components/TimeSeriesAnalysis.tsx @@ -0,0 +1,651 @@ +import React, { useState, useMemo } from "react"; +import { + EuiPanel, + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiSuperSelect, +} from "@elastic/eui"; +import type { + FeatureMetric, + CategoricalHistogram, +} from "../../../queries/useMonitoringApi"; + +const COLORS = [ + "#006BB4", + "#54B399", + "#E7664C", + "#9170B8", + "#D36086", + "#6092C0", + "#D6BF57", + "#B9A888", +]; + +const RANGE_OPTIONS = [ + { value: "24h", inputDisplay: "Last 24 hours" }, + { value: "7d", inputDisplay: "Last 7 days" }, + { value: "30d", inputDisplay: "Last 30 days" }, + { value: "90d", inputDisplay: "Last 90 days" }, + { value: "all", inputDisplay: "All time" }, +]; + +const rangeToMs: Record = { + "24h": 24 * 3600_000, + "7d": 7 * 86400_000, + "30d": 30 * 86400_000, + "90d": 90 * 86400_000, + all: Infinity, +}; + +interface ChartDims { + width: number; + height: number; + padLeft: number; + padRight: number; + padTop: number; + padBottom: number; +} + +const DIMS: ChartDims = { + width: 860, + height: 220, + padLeft: 60, + padRight: 20, + padTop: 10, + padBottom: 30, +}; + +const formatDate = (d: string): string => { + const dt = new Date(d); + const mm = String(dt.getMonth() + 1).padStart(2, "0"); + const dd = String(dt.getDate()).padStart(2, "0"); + const hh = String(dt.getHours()).padStart(2, "0"); + const mi = String(dt.getMinutes()).padStart(2, "0"); + return `${mm}-${dd} ${hh}:${mi}`; +}; + +const formatAxisVal = (v: number): string => { + if (v === 0) return "0"; + const abs = Math.abs(v); + if (abs >= 1_000_000) return (v / 1_000_000).toFixed(1) + "M"; + if (abs >= 1_000) return (v / 1_000).toFixed(1) + "K"; + if (abs >= 1) return v.toFixed(1); + return (v * 100).toFixed(1) + "%"; +}; + +const niceYTicks = (min: number, max: number, count = 5): number[] => { + if (max === min) return [min]; + const step = (max - min) / (count - 1); + return Array.from({ length: count }, (_, i) => min + step * i); +}; + +interface LineSeriesData { + label: string; + color: string; + dashArray?: string; + points: { x: number; y: number; date: string; value: number }[]; +} + +const renderMultiLineChart = ( + series: LineSeriesData[], + dims: ChartDims, + yLabel: string, + yFormatter: (v: number) => string = formatAxisVal, +) => { + const allPoints = series.flatMap((s) => s.points); + if (allPoints.length === 0) return null; + + const xMin = Math.min(...allPoints.map((p) => p.x)); + const xMax = Math.max(...allPoints.map((p) => p.x)); + const yMin = Math.min(...allPoints.map((p) => p.value), 0); + const yMax = Math.max(...allPoints.map((p) => p.value), 0.01); + + const plotW = dims.width - dims.padLeft - dims.padRight; + const plotH = dims.height - dims.padTop - dims.padBottom; + + const scaleX = (x: number) => + xMax === xMin + ? dims.padLeft + plotW / 2 + : dims.padLeft + ((x - xMin) / (xMax - xMin)) * plotW; + const scaleY = (v: number) => + dims.padTop + plotH - ((v - yMin) / (yMax - yMin || 1)) * plotH; + + const yTicks = niceYTicks(yMin, yMax); + const xDates = allPoints + .map((p) => ({ x: p.x, date: p.date })) + .filter((v, i, arr) => arr.findIndex((a) => a.date === v.date) === i) + .sort((a, b) => a.x - b.x); + + const maxXLabels = Math.floor(plotW / 80); + const xStep = Math.max(1, Math.ceil(xDates.length / maxXLabels)); + const xLabels = xDates.filter((_, i) => i % xStep === 0); + + return ( + + {/* Y axis grid + labels */} + {yTicks.map((v, i) => { + const y = scaleY(v); + return ( + + + + {yFormatter(v)} + + + ); + })} + + {/* Y axis label */} + + {yLabel} + + + {/* X axis labels */} + {xLabels.map((xl, i) => ( + + {formatDate(xl.date)} + + ))} + + {/* Lines */} + {series.map((s, si) => { + if (s.points.length === 0) return null; + const sorted = [...s.points].sort((a, b) => a.x - b.x); + const pathD = sorted + .map( + (p, i) => + `${i === 0 ? "M" : "L"} ${scaleX(p.x)} ${scaleY(p.value)}`, + ) + .join(" "); + return ( + + + {sorted.map((p, i) => ( + + ))} + + ); + })} + + ); +}; + +const renderAreaChart = ( + points: { x: number; value: number; date: string }[], + dims: ChartDims, + yLabel: string, + lineColor: string, + fillColor: string, +) => { + if (points.length === 0) return null; + + const sorted = [...points].sort((a, b) => a.x - b.x); + const xMin = Math.min(...sorted.map((p) => p.x)); + const xMax = Math.max(...sorted.map((p) => p.x)); + const yMin = 0; + const yMax = Math.max(...sorted.map((p) => p.value), 0.01); + + const plotW = dims.width - dims.padLeft - dims.padRight; + const plotH = dims.height - dims.padTop - dims.padBottom; + + const scaleX = (x: number) => + xMax === xMin + ? dims.padLeft + plotW / 2 + : dims.padLeft + ((x - xMin) / (xMax - xMin)) * plotW; + const scaleY = (v: number) => + dims.padTop + plotH - ((v - yMin) / (yMax - yMin || 1)) * plotH; + + const yTicks = niceYTicks(yMin, yMax); + const baseline = scaleY(0); + + const areaPath = + `M ${scaleX(sorted[0].x)} ${baseline} ` + + sorted.map((p) => `L ${scaleX(p.x)} ${scaleY(p.value)}`).join(" ") + + ` L ${scaleX(sorted[sorted.length - 1].x)} ${baseline} Z`; + + const linePath = sorted + .map((p, i) => `${i === 0 ? "M" : "L"} ${scaleX(p.x)} ${scaleY(p.value)}`) + .join(" "); + + const maxXLabels = Math.floor(plotW / 80); + const xStep = Math.max(1, Math.ceil(sorted.length / maxXLabels)); + const xLabels = sorted.filter((_, i) => i % xStep === 0); + + return ( + + {yTicks.map((v, i) => { + const y = scaleY(v); + return ( + + + + {(v * 100).toFixed(0)}% + + + ); + })} + + + {yLabel} + + + {xLabels.map((xl, i) => ( + + {formatDate(xl.date)} + + ))} + + + + + ); +}; + +const Legend = ({ + items, +}: { + items: { label: string; color: string; dashed?: boolean }[]; +}) => ( +
+ {items.map((item, i) => ( +
+ {item.dashed ? ( + + + + ) : ( +
+ )} + {item.label} +
+ ))} +
+); + +interface TimeSeriesAnalysisProps { + metrics: FeatureMetric[]; + featureType: string; +} + +const TimeSeriesAnalysis = ({ + metrics, + featureType, +}: TimeSeriesAnalysisProps) => { + const [range, setRange] = useState("all"); + + const filteredMetrics = useMemo(() => { + if (range === "all") return metrics; + const cutoff = Date.now() - rangeToMs[range]; + return metrics.filter((m) => new Date(m.metric_date).getTime() >= cutoff); + }, [metrics, range]); + + const sorted = useMemo( + () => + [...filteredMetrics] + .filter((m) => m.row_count > 0) + .sort((a, b) => a.metric_date.localeCompare(b.metric_date)), + [filteredMetrics], + ); + + const toX = (m: FeatureMetric) => new Date(m.metric_date).getTime(); + const isNumeric = featureType === "numeric"; + const hasData = sorted.length >= 1; + + return ( + + + + +

Time-Series Analysis

+
+

+ Historical trends for central aggregates and quality signals. +

+
+ + + +
+ + + + {!hasData ? ( +

+ No data points available for the selected time range. Try a wider + range. +

+ ) : isNumeric ? ( + + ) : ( + + )} +
+ ); +}; + +const NumericTimeSeries = ({ + metrics, + toX, +}: { + metrics: FeatureMetric[]; + toX: (m: FeatureMetric) => number; +}) => { + const driftSeries: LineSeriesData[] = useMemo(() => { + const build = ( + label: string, + color: string, + accessor: (m: FeatureMetric) => number | null, + dashArray?: string, + ): LineSeriesData => ({ + label, + color, + dashArray, + points: metrics + .filter((m) => accessor(m) !== null) + .map((m) => ({ + x: toX(m), + y: 0, + value: accessor(m)!, + date: m.metric_date, + })), + }); + return [ + build("Mean", COLORS[0], (m) => m.mean, "6,3"), + build("P50", COLORS[1], (m) => m.p50), + build("P95", COLORS[2], (m) => m.p95), + ]; + }, [metrics, toX]); + + const nullPoints = useMemo( + () => + metrics.map((m) => ({ + x: toX(m), + value: m.null_rate, + date: m.metric_date, + })), + [metrics, toX], + ); + + return ( + <> +

+ Aggregate Metrics Drift (Mean/P50/P95) +

+
+ {renderMultiLineChart(driftSeries, DIMS, "Metric Value")} +
+ + + + +

+ Null Rate Evolution (%) +

+
+ {renderAreaChart( + nullPoints, + { ...DIMS, height: 160 }, + "Percentage (%)", + "#BD271E", + "rgba(189, 39, 30, 0.15)", + )} +
+ + ); +}; + +const CategoricalTimeSeries = ({ + metrics, + toX, +}: { + metrics: FeatureMetric[]; + toX: (m: FeatureMetric) => number; +}) => { + const { cardinalitySeries, shareSeries, topCategories } = useMemo(() => { + const catCounts = new Map(); + for (const m of metrics) { + const hist = m.histogram as CategoricalHistogram | null; + if (!hist) continue; + for (const v of hist.values) { + catCounts.set(v.value, (catCounts.get(v.value) || 0) + v.count); + } + } + const topCats = Array.from(catCounts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([v]) => v); + + const cardSeries: LineSeriesData[] = [ + { + label: "Cardinality", + color: COLORS[0], + points: metrics + .filter((m) => m.histogram) + .map((m) => ({ + x: toX(m), + y: 0, + value: (m.histogram as CategoricalHistogram).unique_count || 0, + date: m.metric_date, + })), + }, + ...topCats.map((cat, i) => ({ + label: cat, + color: COLORS[(i + 1) % COLORS.length], + points: metrics + .filter((m) => m.histogram) + .map((m) => { + const hist = m.histogram as CategoricalHistogram; + const entry = hist.values.find((v) => v.value === cat); + return { + x: toX(m), + y: 0, + value: entry?.count || 0, + date: m.metric_date, + }; + }), + })), + ]; + + const shrSeries: LineSeriesData[] = topCats.map((cat, i) => ({ + label: cat, + color: COLORS[(i + 1) % COLORS.length], + points: metrics + .filter((m) => m.histogram && m.row_count > 0) + .map((m) => { + const hist = m.histogram as CategoricalHistogram; + const entry = hist.values.find((v) => v.value === cat); + return { + x: toX(m), + y: 0, + value: ((entry?.count || 0) / m.row_count) * 100, + date: m.metric_date, + }; + }), + })); + + return { + cardinalitySeries: cardSeries, + shareSeries: shrSeries, + topCategories: topCats, + }; + }, [metrics, toX]); + + const nullPoints = useMemo( + () => + metrics.map((m) => ({ + x: toX(m), + value: m.null_rate, + date: m.metric_date, + })), + [metrics, toX], + ); + + const shareYFormatter = (v: number) => `${v.toFixed(0)}%`; + + return ( + <> +

+ Cardinality over time +

+
+ {renderMultiLineChart(cardinalitySeries, DIMS, "Count")} +
+ ({ + label: s.label, + color: s.color, + }))} + /> + + + +

+ Top category share over time (%) +

+
+ {renderMultiLineChart( + shareSeries, + DIMS, + "Percentage (%)", + shareYFormatter, + )} +
+ ({ + label: cat, + color: COLORS[(i + 1) % COLORS.length], + }))} + /> + + + +

+ Null Rate Evolution (%) +

+
+ {renderAreaChart( + nullPoints, + { ...DIMS, height: 160 }, + "Percentage (%)", + "#BD271E", + "rgba(189, 39, 30, 0.15)", + )} +
+ + ); +}; + +export default TimeSeriesAnalysis; diff --git a/ui/src/pages/permissions/Index.tsx b/ui/src/pages/permissions/Index.tsx index 76dde026e90..6ef6dea9bd7 100644 --- a/ui/src/pages/permissions/Index.tsx +++ b/ui/src/pages/permissions/Index.tsx @@ -1,96 +1,561 @@ -import React from "react"; +import React, { useState, useMemo } from "react"; import { EuiPageTemplate, - EuiTitle, EuiSpacer, - EuiPanel, + EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, + EuiButton, + EuiCallOut, + EuiFieldSearch, + EuiTitle, + EuiBasicTable, + EuiBadge, + EuiButtonIcon, + EuiConfirmModal, EuiText, - EuiLoadingSpinner, - EuiHorizontalRule, + EuiToolTip, EuiSelect, EuiFormRow, + EuiEmptyPrompt, } from "@elastic/eui"; -import { useContext, useState } from "react"; import { useParams } from "react-router-dom"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; -import PermissionsDisplay from "../../components/PermissionsDisplay"; -import { filterPermissionsByAction } from "../../utils/permissionUtils"; +import { PermissionsIcon } from "../../graphics/PermissionsIcon"; +import PermissionFormModal, { + PermissionFormData, +} from "../../components/PermissionFormModal"; +import { + useApplyPermission, + useDeletePermission, + ApplyPermissionPayload, +} from "../../queries/mutations/usePermissionMutations"; +import useResourceQuery, { + permissionListPath, +} from "../../queries/useResourceQuery"; + +const ACTION_NAMES = [ + "CREATE", + "DESCRIBE", + "UPDATE", + "DELETE", + "READ_ONLINE", + "READ_OFFLINE", + "WRITE_ONLINE", + "WRITE_OFFLINE", +]; + +const TYPE_DISPLAY_NAMES: Record = { + FEATURE_VIEW: "Feature View", + ON_DEMAND_FEATURE_VIEW: "On-Demand Feature View", + BATCH_FEATURE_VIEW: "Batch Feature View", + STREAM_FEATURE_VIEW: "Stream Feature View", + ENTITY: "Entity", + FEATURE_SERVICE: "Feature Service", + DATA_SOURCE: "Data Source", + VALIDATION_REFERENCE: "Validation Reference", + SAVED_DATASET: "Saved Dataset", + PERMISSION: "Permission", + PROJECT: "Project", + LABEL_VIEW: "Label View", +}; + +const resolveType = (t: string | number): string => { + if (typeof t === "string") return t; + const numericMap: Record = { + 0: "FEATURE_VIEW", + 1: "ON_DEMAND_FEATURE_VIEW", + 2: "BATCH_FEATURE_VIEW", + 3: "STREAM_FEATURE_VIEW", + 4: "ENTITY", + 5: "FEATURE_SERVICE", + 6: "DATA_SOURCE", + 7: "VALIDATION_REFERENCE", + 8: "SAVED_DATASET", + 9: "PERMISSION", + 10: "PROJECT", + 11: "LABEL_VIEW", + }; + return numericMap[t] || `Type ${t}`; +}; + +const resolveAction = (a: string | number): string => { + if (typeof a === "string") return a; + return ACTION_NAMES[a] || `Action ${a}`; +}; + +const getActionColor = (action: string) => { + if (action.startsWith("READ")) return "success"; + if (action.startsWith("WRITE")) return "warning"; + if (action === "CREATE") return "primary"; + if (action === "UPDATE") return "accent"; + if (action === "DELETE") return "danger"; + if (action === "DESCRIBE") return "hollow"; + return "default"; +}; + +const getPolicyDescription = (policy: any): string => { + if (!policy) return "Allow All"; + if (policy.roleBasedPolicy?.roles) { + return `Roles: ${policy.roleBasedPolicy.roles.join(", ")}`; + } + if (policy.groupBasedPolicy?.groups) { + return `Groups: ${policy.groupBasedPolicy.groups.join(", ")}`; + } + if (policy.namespaceBasedPolicy?.namespaces) { + return `Namespaces: ${policy.namespaceBasedPolicy.namespaces.join(", ")}`; + } + if (policy.combinedGroupNamespacePolicy) { + const parts = []; + if (policy.combinedGroupNamespacePolicy.groups?.length) { + parts.push( + `Groups: ${policy.combinedGroupNamespacePolicy.groups.join(", ")}`, + ); + } + if (policy.combinedGroupNamespacePolicy.namespaces?.length) { + parts.push( + `Namespaces: ${policy.combinedGroupNamespacePolicy.namespaces.join(", ")}`, + ); + } + return parts.join(" | "); + } + return "Allow All"; +}; + +const getPolicyType = ( + policy: any, +): "role_based" | "group_based" | "namespace_based" | "combined" => { + if (!policy) return "role_based"; + if (policy.roleBasedPolicy) return "role_based"; + if (policy.groupBasedPolicy) return "group_based"; + if (policy.namespaceBasedPolicy) return "namespace_based"; + if (policy.combinedGroupNamespacePolicy) return "combined"; + return "role_based"; +}; + +const permissionToFormData = (permission: any): PermissionFormData => { + const spec = permission.spec || permission; + const policy = spec.policy; + + const rawTypes: string[] = (spec.types || []).map(resolveType); + const rawActions: string[] = (spec.actions || []).map(resolveAction); + + return { + name: spec.name || "", + types: Array.from(new Set(rawTypes)), + namePatterns: spec.namePatterns || spec.name_patterns || [], + actions: Array.from(new Set(rawActions)), + policyType: getPolicyType(policy), + roles: policy?.roleBasedPolicy?.roles || [], + groups: + policy?.groupBasedPolicy?.groups || + policy?.combinedGroupNamespacePolicy?.groups || + [], + namespaces: + policy?.namespaceBasedPolicy?.namespaces || + policy?.combinedGroupNamespacePolicy?.namespaces || + [], + tags: Object.entries(spec.tags || {}).map(([key, value]) => ({ + key, + value: value as string, + })), + requiredTags: Object.entries( + spec.requiredTags || spec.required_tags || {}, + ).map(([key, value]) => ({ + key, + value: value as string, + })), + }; +}; + +const formDataToPayload = ( + formData: PermissionFormData, + project: string, +): ApplyPermissionPayload => { + const policy: ApplyPermissionPayload["policy"] = {}; + + if (formData.policyType === "role_based") { + policy.role_based_policy = { + roles: formData.roles.filter((r) => r.trim()), + }; + } else if (formData.policyType === "group_based") { + policy.group_based_policy = { + groups: formData.groups.filter((g) => g.trim()), + }; + } else if (formData.policyType === "namespace_based") { + policy.namespace_based_policy = { + namespaces: formData.namespaces.filter((n) => n.trim()), + }; + } else if (formData.policyType === "combined") { + policy.combined_group_namespace_policy = { + groups: formData.groups.filter((g) => g.trim()), + namespaces: formData.namespaces.filter((n) => n.trim()), + }; + } + + return { + name: formData.name, + project, + types: formData.types, + name_patterns: formData.namePatterns.filter((p) => p.trim()), + actions: formData.actions, + policy, + tags: Object.fromEntries( + formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]), + ), + required_tags: Object.fromEntries( + formData.requiredTags + .filter((t) => t.key.trim()) + .map((t) => [t.key, t.value]), + ), + }; +}; + +const useLoadPermissions = () => { + const { projectName } = useParams(); + return useResourceQuery({ + resourceType: "permissions-list", + project: projectName, + restPath: permissionListPath(projectName), + restSelect: (d) => d.permissions, + }); +}; const PermissionsIndex = () => { - const registryUrl = useContext(RegistryPathContext); const { projectName } = useParams(); - const { isLoading, isSuccess, isError, data } = useLoadRegistry( - registryUrl, - projectName, - ); - const [selectedPermissionAction, setSelectedPermissionAction] = useState(""); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadPermissions(); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingPermission, setEditingPermission] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [searchString, setSearchString] = useState(""); + const [actionFilter, setActionFilter] = useState(""); + + const applyPermission = useApplyPermission(); + const deletePermissionMutation = useDeletePermission(); + + const permissions = useMemo(() => { + if (!data) return []; + let filtered = data; + + if (searchString.trim()) { + const lower = searchString.toLowerCase(); + filtered = filtered.filter((p) => + (p.spec?.name || "").toLowerCase().includes(lower), + ); + } + + if (actionFilter) { + filtered = filtered.filter((p) => { + const actions = (p.spec?.actions || []).map(resolveAction); + return actions.includes(actionFilter); + }); + } + + return filtered; + }, [data, searchString, actionFilter]); + + const handleCreate = () => { + setEditingPermission(null); + setIsModalOpen(true); + }; + + const handleEdit = (permission: any) => { + setEditingPermission(permission); + setIsModalOpen(true); + }; + + const handleDelete = (permission: any) => { + setDeleteTarget(permission); + }; + + const confirmDelete = () => { + if (!deleteTarget) return; + const name = deleteTarget.spec?.name || deleteTarget.name; + deletePermissionMutation.mutate( + { name, project: projectName || "" }, + { + onSuccess: () => { + setDeleteTarget(null); + setErrorMessage(null); + setSuccessMessage(`Permission "${name}" deleted successfully.`); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + setDeleteTarget(null); + const message = + err instanceof Error + ? err.message + : "An unexpected error occurred."; + setErrorMessage(message); + setTimeout(() => setErrorMessage(null), 5000); + }, + }, + ); + }; + + const handleFormSubmit = (formData: PermissionFormData) => { + const payload = formDataToPayload(formData, projectName || ""); + applyPermission.mutate(payload, { + onSuccess: () => { + setIsModalOpen(false); + setEditingPermission(null); + setErrorMessage(null); + const verb = editingPermission ? "updated" : "created"; + setSuccessMessage( + `Permission "${formData.name}" ${verb} successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + }, + onError: (err: unknown) => { + const message = + err instanceof Error ? err.message : "An unexpected error occurred."; + setErrorMessage(message); + }, + }); + }; + + const columns = [ + { + field: "spec.name", + name: "Name", + sortable: true, + render: (_: string, item: any) => ( + {item.spec?.name || "—"} + ), + }, + { + field: "spec.types", + name: "Resource Types", + render: (_: any, item: any) => { + const rawTypes: string[] = (item.spec?.types || []).map(resolveType); + const types = Array.from(new Set(rawTypes)); + if (types.length === 0) return All; + const display = (t: string) => TYPE_DISPLAY_NAMES[t] || t; + if (types.length > 3) { + return ( + + + {types.slice(0, 3).map(display).join(", ")} +{types.length - 3}{" "} + more + + + ); + } + return {types.map(display).join(", ")}; + }, + }, + { + field: "spec.actions", + name: "Actions", + render: (_: any, item: any) => { + const rawActions: string[] = (item.spec?.actions || []).map( + resolveAction, + ); + const actions = Array.from(new Set(rawActions)); + return ( + + {actions.map((action: string, i: number) => ( + + {action} + + ))} + + ); + }, + }, + { + field: "spec.policy", + name: "Policy", + render: (_: any, item: any) => { + const desc = getPolicyDescription(item.spec?.policy); + return ( + + + {desc.length > 40 ? desc.substring(0, 40) + "..." : desc} + + + ); + }, + }, + { + name: "Actions", + width: "100px", + render: (item: any) => ( + + + + handleEdit(item)} + color="primary" + /> + + + + + handleDelete(item)} + color="danger" + /> + + + + ), + }, + ]; + + const hasPermissions = isSuccess && data && data.length > 0; + const isEmpty = isSuccess && (!data || data.length === 0); return ( - + + Create Permission + , + ]} /> + {successMessage && ( + <> + + + + )} + {errorMessage && !isModalOpen && ( + <> + + + + )} + {isLoading && ( - +

Loading - +

+ )} + {isPermissionDenied && ( + +

You do not have permission to view permissions.

+
+ )} + {isError && !isPermissionDenied &&

Error loading permissions.

} + + {isEmpty && ( + No permissions yet} + body={ +

+ Permissions let you control who can perform specific actions on + your Feast resources. Create your first permission to get + started. +

+ } + actions={ + + Create Permission + + } + /> )} - {isError &&

Error loading permissions

} - {isSuccess && data && ( - - - + + {hasPermissions && ( + <> + + + +

Search

+
+ setSearchString(e.target.value)} + /> +
+ ({ value: a, text: a })), ]} - value={selectedPermissionAction} - onChange={(e) => - setSelectedPermissionAction(e.target.value) - } - aria-label="Filter by action" + value={actionFilter} + onChange={(e) => setActionFilter(e.target.value)} />
- - -

Permissions

-
- - {data.permissions && data.permissions.length > 0 ? ( - - ) : ( - No permissions defined in this project. - )} -
-
+ + )}
+ + {isModalOpen && ( + { + setIsModalOpen(false); + setEditingPermission(null); + setErrorMessage(null); + }} + onSubmit={handleFormSubmit} + isEdit={!!editingPermission} + initialData={ + editingPermission + ? permissionToFormData(editingPermission) + : undefined + } + isSubmitting={applyPermission.isLoading} + submitError={errorMessage} + /> + )} + + {deleteTarget && ( + setDeleteTarget(null)} + onConfirm={confirmDelete} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deletePermissionMutation.isLoading} + > +

+ Are you sure you want to delete the permission{" "} + + "{deleteTarget.spec?.name || deleteTarget.name}" + + ? This action cannot be undone. +

+
+ )}
); }; diff --git a/ui/src/pages/saved-data-sets/AddToCatalogModal.tsx b/ui/src/pages/saved-data-sets/AddToCatalogModal.tsx new file mode 100644 index 00000000000..7f9d80a6bd8 --- /dev/null +++ b/ui/src/pages/saved-data-sets/AddToCatalogModal.tsx @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import { + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiTabbedContent, + EuiTabbedContentTab, + EuiIcon, +} from "@elastic/eui"; +import RegisterDatasetModal from "./RegisterDatasetModal"; +import type { RegisterDatasetPayload } from "./RegisterDatasetModal"; +import CreateDatasetForm from "./CreateDatasetForm"; + +interface AddToCatalogModalProps { + onClose: () => void; + onLinkSubmit: (data: RegisterDatasetPayload) => Promise; + isLinkSubmitting: boolean; + linkError?: string | null; +} + +const AddToCatalogModal = ({ + onClose, + onLinkSubmit, + isLinkSubmitting, + linkError, +}: AddToCatalogModalProps) => { + const [selectedTab, setSelectedTab] = useState("link"); + + const tabs: EuiTabbedContentTab[] = [ + { + id: "link", + name: "Link Existing", + prepend: , + content: ( + + ), + }, + { + id: "create", + name: "Create Dataset", + prepend: , + content: , + }, + ]; + + return ( + + + Add to Catalog + + + t.id === selectedTab)} + onTabClick={(tab) => setSelectedTab(tab.id)} + /> + + + ); +}; + +export default AddToCatalogModal; diff --git a/ui/src/pages/saved-data-sets/CreateDatasetForm.tsx b/ui/src/pages/saved-data-sets/CreateDatasetForm.tsx new file mode 100644 index 00000000000..88a9d3e0201 --- /dev/null +++ b/ui/src/pages/saved-data-sets/CreateDatasetForm.tsx @@ -0,0 +1,725 @@ +import React, { useState, useMemo, useCallback, useContext } from "react"; +import { + EuiSpacer, + EuiFormRow, + EuiFieldText, + EuiRadioGroup, + EuiComboBox, + EuiComboBoxOptionOption, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiCallOut, + EuiPanel, + EuiTitle, + EuiHorizontalRule, + EuiSuperSelect, + EuiSuperSelectOption, + EuiDatePicker, + EuiDatePickerRange, + EuiTextArea, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import { useMutation, useQueryClient } from "react-query"; +import moment, { Moment } from "moment"; +import useResourceQuery, { + featureServiceListPath, + featureViewListPath, + dataSourceListPath, +} from "../../queries/useResourceQuery"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; +import { restPost } from "../../queries/restApiClient"; +import TagsEditor, { TagEntry } from "../../components/forms/TagsEditor"; +import JobStatusPanel from "./JobStatusPanel"; + +interface CreateDatasetFormProps { + onClose: () => void; +} + +const FEATURE_MODE_OPTIONS = [ + { id: "service", label: "Use a Feature Service" }, + { id: "individual", label: "Select individual features" }, +]; + +const ENTITY_SOURCE_OPTIONS = [ + { id: "inline", label: "Define entity keys and time range manually" }, + { id: "reference", label: "Reference an existing data source path" }, +]; + +const STORAGE_TYPES = [ + { + value: "file", + label: "File (Parquet)", + placeholder: "s3://bucket/path/output.parquet", + }, + { + value: "bigquery", + label: "BigQuery", + placeholder: "project.dataset.table", + }, + { + value: "snowflake", + label: "Snowflake", + placeholder: "database.schema.table", + }, + { value: "redshift", label: "Redshift", placeholder: "schema.table" }, + { value: "spark", label: "Spark", placeholder: "s3://bucket/path/" }, + { value: "trino", label: "Trino", placeholder: "catalog.schema.table" }, + { value: "athena", label: "Athena", placeholder: "database.table" }, + { + value: "postgres", + label: "PostgreSQL", + placeholder: "schema.table_name", + }, + { + value: "clickhouse", + label: "ClickHouse", + placeholder: "database.table_name", + }, + { + value: "couchbase", + label: "Couchbase Columnar", + placeholder: "database.scope.collection", + }, +]; + +const CreateDatasetForm = ({ onClose }: CreateDatasetFormProps) => { + const { projectName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + const queryClient = useQueryClient(); + + // Step 1: Feature selection + const [featureMode, setFeatureMode] = useState("service"); + const [selectedService, setSelectedService] = useState< + EuiComboBoxOptionOption[] + >([]); + const [selectedFeatures, setSelectedFeatures] = useState< + EuiComboBoxOptionOption[] + >([]); + + // Step 2: Entity source + const [entitySourceType, setEntitySourceType] = useState("inline"); + + // Inline mode state + const [entityKeys, setEntityKeys] = useState([]); + const [entityValues, setEntityValues] = useState(""); + const [startDate, setStartDate] = useState( + moment().subtract(30, "days"), + ); + const [endDate, setEndDate] = useState(moment()); + const [extraColumns, setExtraColumns] = useState(""); + + // Reference mode state + const [entitySourcePath, setEntitySourcePath] = useState(""); + + // Step 3: Storage & metadata + const [datasetName, setDatasetName] = useState(""); + const [namespace, setNamespace] = useState(""); + const [collection, setCollection] = useState(""); + const [description, setDescription] = useState(""); + const [storageType, setStorageType] = useState("file"); + const [storagePath, setStoragePath] = useState(""); + const [storageFileFormat, setStorageFileFormat] = useState("parquet"); + const [tags, setTags] = useState([]); + const [allowOverwrite] = useState(false); + + // Job tracking + const [jobId, setJobId] = useState(null); + const [submitError, setSubmitError] = useState(null); + + // Fetch data for suggestions + const { data: featureServicesRaw } = useResourceQuery({ + resourceType: "create-dataset-fs", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices || [], + }); + + const { data: featureViewsRaw } = useResourceQuery({ + resourceType: "create-dataset-fv", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: (d) => d.featureViews || [], + }); + + const { data: dataSourcesRaw } = useResourceQuery({ + resourceType: "create-dataset-ds", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources || [], + }); + + const featureServiceOptions: EuiComboBoxOptionOption[] = useMemo( + () => + (featureServicesRaw || []).map((fs: any) => ({ + label: fs.spec?.name || fs.name || "", + })), + [featureServicesRaw], + ); + + const featureOptions: EuiComboBoxOptionOption[] = useMemo( + () => + (featureViewsRaw || []) + .filter((fv: any) => fv.type !== "labelView") + .flatMap((fv: any) => { + const fvName = fv.spec?.name || ""; + const features = fv.spec?.features || []; + return features.map((f: any) => ({ + label: `${fvName}:${f.name || f}`, + })); + }), + [featureViewsRaw], + ); + + // Extract entity/join key options from feature views + const joinKeyOptions: EuiComboBoxOptionOption[] = useMemo(() => { + const seen = new Set(); + (featureViewsRaw || []).forEach((fv: any) => { + const entities = fv.spec?.entities || []; + entities.forEach((e: string) => { + if (e && !seen.has(e)) seen.add(e); + }); + }); + return Array.from(seen).map((k) => ({ label: k })); + }, [featureViewsRaw]); + + // Filter storage types based on configured data sources + const storageOptions: EuiSuperSelectOption[] = useMemo(() => { + if (!dataSourcesRaw || dataSourcesRaw.length === 0) { + return STORAGE_TYPES.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: {st.label}, + })); + } + + const detectedTypes = new Set(); + for (const ds of dataSourcesRaw) { + const spec = ds.spec || ds; + if (spec.fileOptions || ds.fileOptions) detectedTypes.add("file"); + if (spec.bigqueryOptions || ds.bigqueryOptions) + detectedTypes.add("bigquery"); + if (spec.snowflakeOptions || ds.snowflakeOptions) + detectedTypes.add("snowflake"); + if (spec.redshiftOptions || ds.redshiftOptions) + detectedTypes.add("redshift"); + if (spec.sparkOptions || ds.sparkOptions) detectedTypes.add("spark"); + if (spec.trinoOptions || ds.trinoOptions) detectedTypes.add("trino"); + if (spec.athenaOptions || ds.athenaOptions) detectedTypes.add("athena"); + const dsType = spec.type || ds.type; + if (dsType === 1) detectedTypes.add("file"); + if (dsType === 2) detectedTypes.add("bigquery"); + if (dsType === 3) detectedTypes.add("redshift"); + if (dsType === 5) detectedTypes.add("snowflake"); + if (dsType === 7) detectedTypes.add("spark"); + if (dsType === 8) detectedTypes.add("trino"); + if (dsType === 9) detectedTypes.add("athena"); + const classType = + spec.dataSourceClassType || ds.dataSourceClassType || ""; + if (classType.includes("postgres")) detectedTypes.add("postgres"); + if (classType.includes("clickhouse")) detectedTypes.add("clickhouse"); + if (classType.includes("couchbase")) detectedTypes.add("couchbase"); + } + + // Always include file as a fallback + detectedTypes.add("file"); + + const filtered = STORAGE_TYPES.filter((st) => detectedTypes.has(st.value)); + return filtered.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: {st.label}, + })); + }, [dataSourcesRaw]); + + // Extract available data source paths for the reference entity source option + const dataSourcePathOptions: EuiComboBoxOptionOption[] = useMemo(() => { + if (!dataSourcesRaw) return []; + const paths: EuiComboBoxOptionOption[] = []; + for (const ds of dataSourcesRaw) { + const spec = ds.spec || ds; + const name = spec.name || ds.name || ""; + const fileOpts = spec.fileOptions || ds.fileOptions; + const bqOpts = spec.bigqueryOptions || ds.bigqueryOptions; + const sfOpts = spec.snowflakeOptions || ds.snowflakeOptions; + const rsOpts = spec.redshiftOptions || ds.redshiftOptions; + const sparkOpts = spec.sparkOptions || ds.sparkOptions; + const trinoOpts = spec.trinoOptions || ds.trinoOptions; + const athenaOpts = spec.athenaOptions || ds.athenaOptions; + + let path = ""; + if (fileOpts?.uri) path = fileOpts.uri; + else if (fileOpts?.path) path = fileOpts.path; + else if (bqOpts?.table) path = bqOpts.table; + else if (sfOpts?.table) path = sfOpts.table; + else if (rsOpts?.table) path = rsOpts.table; + else if (sparkOpts?.path) path = sparkOpts.path; + else if (sparkOpts?.table) path = sparkOpts.table; + else if (trinoOpts?.table) path = trinoOpts.table; + else if (athenaOpts?.table) path = athenaOpts.table; + + if (path) { + paths.push({ label: path, key: name }); + } + } + return paths; + }, [dataSourcesRaw]); + + const currentStorageType = + STORAGE_TYPES.find((s) => s.value === storageType) || STORAGE_TYPES[0]; + + // Submit create job + const createMutation = useMutation( + async () => { + const payload: any = { + name: datasetName.trim(), + project: projectName || "", + storage_type: storageType, + storage_path: storagePath.trim(), + storage_file_format: + storageType === "spark" ? storageFileFormat : undefined, + entity_source_type: entitySourceType, + allow_overwrite: allowOverwrite, + tags: tags.reduce( + (acc, t) => { + if (t.key.trim() && t.value.trim()) + acc[t.key.trim()] = t.value.trim(); + return acc; + }, + {} as Record, + ), + }; + + if (namespace.trim()) payload.namespace = namespace.trim(); + if (collection.trim()) payload.collection = collection.trim(); + if (description.trim()) payload.description = description.trim(); + + if (featureMode === "service" && selectedService.length > 0) { + payload.feature_service_name = selectedService[0].label; + } else if (featureMode === "individual" && selectedFeatures.length > 0) { + payload.features = selectedFeatures.map((f) => f.label); + } + + if (entitySourceType === "inline") { + payload.entity_keys = entityKeys.map((k) => k.label); + payload.entity_values = entityValues.trim(); + if (startDate) payload.start_date = startDate.toISOString(); + if (endDate) payload.end_date = endDate.toISOString(); + } else if (entitySourceType === "reference") { + payload.entity_source_path = entitySourcePath.trim(); + } + + // Extra columns apply to all entity source methods + if (extraColumns.trim()) { + payload.extra_columns = extraColumns.trim(); + } + + return restPost( + registryUrl, + "/saved_datasets/create", + payload, + fetchOptions, + ); + }, + { + onSuccess: (data: any) => { + setJobId(data.job_id); + setSubmitError(null); + }, + onError: (err: Error) => { + setSubmitError(err.message); + }, + }, + ); + + const handleJobComplete = useCallback(() => { + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + }, [queryClient]); + + // Validation + const canProceedStep0 = + featureMode === "service" + ? selectedService.length > 0 + : selectedFeatures.length > 0; + + const canProceedStep1 = (() => { + if (entitySourceType === "inline") { + return entityKeys.length > 0 && entityValues.trim().length > 0; + } + return entitySourcePath.trim().length > 0; + })(); + + const canSubmit = + datasetName.trim().length > 0 && + storagePath.trim().length > 0 && + /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(datasetName.trim()); + + const handleRetry = useCallback(() => { + setJobId(null); + setSubmitError(null); + }, []); + + if (jobId) { + return ( +
+ +
+ ); + } + + return ( +
+ {submitError && ( + <> + +

{submitError}

+
+ + + )} + + + + Create a new dataset by running a feature retrieval job. Feast will + execute get_historical_features, persist the results to your chosen + storage, and register the dataset in the catalog. + + + + + {/* Step 1: Feature Selection */} + +

Step 1: Define Features

+
+ + + setFeatureMode(id)} + /> + + + {featureMode === "service" ? ( + + + + ) : ( + + + setSelectedFeatures([...selectedFeatures, { label: val }]) + } + placeholder="Search or type features..." + isClearable + fullWidth + /> + + )} + + + + + {/* Step 2: Entity Source */} + +

Step 2: Entity Source

+
+ + + setEntitySourceType(id)} + /> + + + {entitySourceType === "inline" && ( + <> + + + setEntityKeys([...entityKeys, { label: val }]) + } + placeholder="Select or type entity keys (e.g. driver_id, customer_id)..." + isClearable + fullWidth + /> + + + + setEntityValues(e.target.value)} + placeholder={ + "1001, 1002, 1003, 1004\nor for multiple keys:\n1001,A\n1002,B\n1003,C" + } + rows={4} + fullWidth + /> + + + + + } + endDateControl={ + + } + fullWidth + /> + + + )} + + {entitySourceType === "reference" && ( + + + setEntitySourcePath(selected.length > 0 ? selected[0].label : "") + } + onCreateOption={(val) => setEntitySourcePath(val)} + placeholder="Select a data source or type a path..." + isClearable + fullWidth + /> + + )} + + + + setExtraColumns(e.target.value)} + placeholder={"val_to_add=10\ndiscount_pct=0.15"} + rows={2} + fullWidth + /> + + + + + + {/* Step 3: Storage Destination & Metadata */} + +

Step 3: Output Destination

+
+ + + 0 && + !/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(datasetName) + } + error="Must start with letter/underscore, contain only letters, numbers, underscores, hyphens." + > + setDatasetName(e.target.value)} + placeholder="e.g. driver_training_2024_q1" + fullWidth + /> + + + + setDescription(e.target.value)} + placeholder="e.g. Training data for driver fraud model" + fullWidth + /> + + + + + + setNamespace(e.target.value)} + placeholder="e.g. fraud" + /> + + + + + setCollection(e.target.value)} + placeholder="e.g. training" + /> + + + + + + + + + + + + + + + setStoragePath(e.target.value)} + placeholder={currentStorageType.placeholder} + fullWidth + /> + + + + + {storageType === "spark" && ( + <> + + + Parquet, + }, + { + value: "avro", + inputDisplay: "Avro", + dropdownDisplay: Avro, + }, + { + value: "csv", + inputDisplay: "CSV", + dropdownDisplay: CSV, + }, + { + value: "json", + inputDisplay: "JSON", + dropdownDisplay: JSON, + }, + ]} + valueOfSelected={storageFileFormat} + onChange={setStorageFileFormat} + fullWidth + /> + + + )} + + + + + {/* Actions */} + + + + Cancel + + + createMutation.mutate()} + isLoading={createMutation.isLoading} + disabled={!canProceedStep0 || !canProceedStep1 || !canSubmit} + iconType="playFilled" + > + Create Dataset + + + +
+ ); +}; + +export default CreateDatasetForm; diff --git a/ui/src/pages/saved-data-sets/DatasetCatalogBrowser.tsx b/ui/src/pages/saved-data-sets/DatasetCatalogBrowser.tsx new file mode 100644 index 00000000000..bcb2bd844a5 --- /dev/null +++ b/ui/src/pages/saved-data-sets/DatasetCatalogBrowser.tsx @@ -0,0 +1,846 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiText, + EuiBadge, + EuiSpacer, + EuiIcon, + EuiBreadcrumbs, + EuiEmptyPrompt, + EuiTreeView, + EuiToolTip, + EuiButtonIcon, + EuiCopy, +} from "@elastic/eui"; +import type { Node as EuiTreeNode } from "@elastic/eui/src/components/tree_view/tree_view"; +import { useNavigate, useParams } from "react-router-dom"; + +/* ────────────────────────── types ────────────────────────── */ + +interface BrowsePath { + namespace?: string; + collection?: string; +} + +interface DatasetCatalogBrowserProps { + datasets: any[]; + onDelete?: (name: string) => void; +} + +/* ──────────────────── hierarchy builder ──────────────────── */ + +function buildHierarchy(datasets: any[]) { + const tree: Record> = {}; + for (const ds of datasets) { + const ns = ds.spec?.namespace || ""; + const col = ds.spec?.collection || ""; + if (!tree[ns]) tree[ns] = {}; + if (!tree[ns][col]) tree[ns][col] = []; + tree[ns][col].push(ds); + } + return tree; +} + +/* ──────────────────── colors ──────────────────── */ + +const NS_COLOR = "#0077CC"; +const COL_COLOR = "#8B5CF6"; + +const STORAGE_TYPE_CONFIG: Record< + string, + { label: string; color: string; icon: string } +> = { + file: { label: "File", color: "#4CAF50", icon: "document" }, + bigquery: { label: "BigQuery", color: "#4285F4", icon: "storage" }, + snowflake: { label: "Snowflake", color: "#29B5E8", icon: "snowflake" }, + redshift: { label: "Redshift", color: "#205B97", icon: "compute" }, + spark: { label: "Spark", color: "#E25A1C", icon: "bolt" }, + trino: { label: "Trino", color: "#DD00A1", icon: "database" }, + athena: { label: "Athena", color: "#8C4FFF", icon: "database" }, + custom: { label: "Custom", color: "#607D8B", icon: "gear" }, + unknown: { label: "Storage", color: "#98A2B3", icon: "database" }, +}; + +function detectStorageType(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return "unknown"; + if (storage.fileStorage) return "file"; + if (storage.bigqueryStorage) return "bigquery"; + if (storage.snowflakeStorage) return "snowflake"; + if (storage.redshiftStorage) return "redshift"; + if (storage.sparkStorage) return "spark"; + if (storage.trinoStorage) return "trino"; + if (storage.athenaStorage) return "athena"; + if (storage.customStorage) return "custom"; + return "unknown"; +} + +function extractStoragePath(dataset: any): string | undefined { + const storage = dataset?.spec?.storage; + if (!storage) return undefined; + if (storage.fileStorage?.uri) return storage.fileStorage.uri; + if (storage.bigqueryStorage?.table) return storage.bigqueryStorage.table; + if (storage.snowflakeStorage?.table) return storage.snowflakeStorage.table; + if (storage.redshiftStorage?.table) return storage.redshiftStorage.table; + if (storage.sparkStorage?.path) return storage.sparkStorage.path; + if (storage.sparkStorage?.table) return storage.sparkStorage.table; + if (storage.trinoStorage?.table) return storage.trinoStorage.table; + if (storage.athenaStorage?.table) return storage.athenaStorage.table; + if (storage.customStorage?.configuration) + return storage.customStorage.configuration; + return undefined; +} + +function truncatePath(path: string, maxLen: number = 42): string { + if (path.length <= maxLen) return path; + return `${path.slice(0, 18)}...${path.slice(-20)}`; +} + +function getRelativeTime(date: Date): string { + const diffMs = Date.now() - date.getTime(); + const diffDays = Math.floor(diffMs / 86400000); + if (diffDays === 0) return "Today"; + if (diffDays === 1) return "Yesterday"; + if (diffDays < 7) return `${diffDays}d ago`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; + if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`; + return `${Math.floor(diffDays / 365)}y ago`; +} + +/* ──────────────────── tile: dataset card ──────────────────── */ + +const DatasetCard: React.FC<{ + dataset: any; + onNavigate: () => void; + onDelete?: (name: string) => void; +}> = ({ dataset, onNavigate, onDelete }) => { + const [isHovered, setIsHovered] = useState(false); + const spec = dataset.spec || {}; + const meta = dataset.meta || {}; + const name = spec.name || "unknown"; + const features = spec.features || []; + const joinKeys = spec.joinKeys || spec.join_keys || []; + const tags = spec.tags || {}; + const description = spec.description || ""; + const featureServiceName = + spec.featureServiceName || spec.feature_service_name; + const createdTimestamp = meta.createdTimestamp || meta.created_timestamp; + const storagePath = extractStoragePath(dataset); + const storageType = detectStorageType(dataset); + const storageInfo = + STORAGE_TYPE_CONFIG[storageType] || STORAGE_TYPE_CONFIG.unknown; + + const formattedDate = createdTimestamp + ? new Date(createdTimestamp).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + : null; + const relativeTime = createdTimestamp + ? getRelativeTime(new Date(createdTimestamp)) + : null; + + const codeSnippet = `dataset = store.get_saved_dataset("${name}")\ndf = dataset.to_df()`; + + return ( + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onNavigate} + style={{ + height: "100%", + display: "flex", + flexDirection: "column", + transition: "all 0.2s ease", + transform: isHovered ? "translateY(-2px)" : "none", + borderTop: `3px solid ${storageInfo.color}`, + cursor: "pointer", + }} + > + {/* Header */} + + + +

+ {name} +

+
+
+ + + {storageInfo.label} + + +
+ + {/* Description */} + {description && ( + +

{description}

+
+ )} + + + + {/* Storage path */} + {storagePath && ( + + + {" "} + + {truncatePath(storagePath)} + + + + )} + + + + {/* Metrics */} + + + + Features + + + {features.length} + + + + + Retrieval Keys + + + {joinKeys.length} + + + {featureServiceName && ( + + + Service + + + {featureServiceName} + + + )} + + +
+ + + {/* Tags */} + {Object.keys(tags).length > 0 && ( + <> + + {Object.entries(tags) + .slice(0, 3) + .map(([key, value]) => ( + + + {key}: {value as string} + + + ))} + {Object.keys(tags).length > 3 && ( + + + +{Object.keys(tags).length - 3} more + + + )} + + + + )} + + {/* Footer */} + + + {formattedDate && ( + + + {relativeTime} + + + )} + + + + + + {(copy) => ( + + { + e.stopPropagation(); + copy(); + }} + /> + + )} + + + {onDelete && ( + + + { + e.stopPropagation(); + onDelete(name); + }} + /> + + + )} + + + + + + ); +}; + +/* ──────────────────── tile: folder card (same width as dataset) ──────────────────── */ + +const FolderCard: React.FC<{ + name: string; + type: "namespace" | "collection"; + datasetCount: number; + collectionCount?: number; + onClick: () => void; +}> = ({ name, type, datasetCount, collectionCount, onClick }) => { + const [isHovered, setIsHovered] = useState(false); + const color = type === "namespace" ? NS_COLOR : COL_COLOR; + + return ( + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={onClick} + style={{ + cursor: "pointer", + transition: "all 0.2s ease", + transform: isHovered ? "translateY(-2px)" : "none", + borderTop: `3px solid ${color}`, + display: "flex", + flexDirection: "column", + }} + > + + + + + + +

{name}

+
+
+
+ + + + + {type === "namespace" && + collectionCount !== undefined && + collectionCount > 0 && ( + + + Collections + + + {collectionCount} + + + )} + + + Datasets + + + {datasetCount} + + + +
+
+ ); +}; + +/* ──────────────────── tree styles ──────────────────── */ + +const TREE_CSS = ` + .catalogTree .euiTreeView__node { + margin-bottom: 1px; + } + .catalogTree .euiTreeView__node--expanded > .euiTreeView__nodeInner { + background: rgba(0, 119, 204, 0.05); + border-radius: 4px; + } + .catalogTree .euiTreeView__nodeInner { + padding: 4px 6px; + border-radius: 4px; + transition: background 0.15s ease; + } + .catalogTree .euiTreeView__nodeInner:hover { + background: rgba(0, 119, 204, 0.08); + } + .catalogTree .euiTreeView__nodeInner--withArrow .euiTreeView__nodeInner__arrow { + margin-right: 2px; + } + .catalogTree .euiTreeView__nodeLabel { + font-size: 13px; + } +`; + +/* ──────────────────── main component ──────────────────── */ + +const DatasetCatalogBrowser: React.FC = ({ + datasets, + onDelete, +}) => { + const { projectName } = useParams(); + const navigate = useNavigate(); + const [browsePath, setBrowsePath] = useState({}); + const [showTree, setShowTree] = useState(true); + + const hierarchy = useMemo(() => buildHierarchy(datasets), [datasets]); + + const namespaceList = useMemo(() => { + const entries: Array<{ + name: string; + collections: string[]; + totalDatasets: number; + }> = []; + for (const [ns, cols] of Object.entries(hierarchy)) { + if (ns === "") continue; + const colNames = Object.keys(cols) + .filter((c) => c !== "") + .sort(); + const total = Object.values(cols).reduce((s, a) => s + a.length, 0); + entries.push({ name: ns, collections: colNames, totalDatasets: total }); + } + entries.sort((a, b) => a.name.localeCompare(b.name)); + return entries; + }, [hierarchy]); + + const rootDatasets = useMemo(() => { + const cols = hierarchy[""]; + if (!cols) return []; + const all: any[] = []; + for (const arr of Object.values(cols)) all.push(...arr); + return all; + }, [hierarchy]); + + const goToDataset = useCallback( + (dataset: any) => { + const p = dataset.project || dataset.spec?.project || projectName; + const n = dataset.spec?.name || dataset.name; + navigate(`/p/${p}/data-set/${n}`); + }, + [navigate, projectName], + ); + + /* ── tree sidebar (EuiTreeView) ── */ + + // Clickable label for parent nodes — onClick navigates, stopPropagation prevents toggle + const navLabel = useCallback( + ( + text: string, + onClick: () => void, + isActive: boolean, + activeColor?: string, + ) => ( + { + e.stopPropagation(); + onClick(); + }} + style={{ + cursor: "pointer", + fontWeight: isActive ? 600 : 400, + color: isActive ? activeColor || NS_COLOR : undefined, + }} + > + {text} + + ), + [], + ); + + const treeItems: EuiTreeNode[] = useMemo(() => { + const isRootSelected = browsePath.namespace === undefined; + const items: EuiTreeNode[] = [ + { + id: "_root", + label: navLabel( + "All Datasets", + () => setBrowsePath({}), + isRootSelected, + ), + icon: , + isExpanded: true, + }, + ]; + + for (const ns of namespaceList) { + const nsData = hierarchy[ns.name] || {}; + const nsChildren: EuiTreeNode[] = []; + const nsSelected = + browsePath.namespace === ns.name && !browsePath.collection; + + // Collection sub-folders + for (const col of ns.collections) { + const colDatasets = nsData[col] || []; + const colSelected = + browsePath.namespace === ns.name && browsePath.collection === col; + + const dsLeaves: EuiTreeNode[] = colDatasets.map((ds: any) => ({ + id: `ds:${ns.name}/${col}/${ds.spec?.name || ds.name}`, + label: ds.spec?.name || ds.name || "unknown", + icon: , + callback: () => { + goToDataset(ds); + return `ds:${ns.name}/${col}/${ds.spec?.name}`; + }, + })); + + nsChildren.push({ + id: `col:${ns.name}/${col}`, + label: navLabel( + `${col} (${colDatasets.length})`, + () => setBrowsePath({ namespace: ns.name, collection: col }), + colSelected, + COL_COLOR, + ), + icon: , + iconWhenExpanded: ( + + ), + children: dsLeaves.length > 0 ? dsLeaves : undefined, + }); + } + + // Direct datasets under namespace (no collection) + const directDatasets = nsData[""] || []; + for (const ds of directDatasets) { + nsChildren.push({ + id: `ds:${ns.name}/_/${ds.spec?.name || ds.name}`, + label: ds.spec?.name || ds.name || "unknown", + icon: , + callback: () => { + goToDataset(ds); + return `ds:${ns.name}/_/${ds.spec?.name}`; + }, + }); + } + + items.push({ + id: `ns:${ns.name}`, + label: navLabel( + `${ns.name} (${ns.totalDatasets})`, + () => setBrowsePath({ namespace: ns.name }), + nsSelected, + NS_COLOR, + ), + icon: , + iconWhenExpanded: ( + + ), + children: nsChildren.length > 0 ? nsChildren : undefined, + }); + } + + // Ungrouped datasets + if (rootDatasets.length > 0) { + const ungroupedLeaves: EuiTreeNode[] = rootDatasets.map((ds: any) => ({ + id: `ds:_ungrouped/${ds.spec?.name || ds.name}`, + label: ds.spec?.name || ds.name || "unknown", + icon: , + callback: () => { + goToDataset(ds); + return `ds:_ungrouped/${ds.spec?.name}`; + }, + })); + + items.push({ + id: "_ungrouped", + label: navLabel( + `Ungrouped (${rootDatasets.length})`, + () => setBrowsePath({}), + false, + "#98A2B3", + ), + icon: , + children: ungroupedLeaves, + }); + } + + return items; + }, [ + namespaceList, + rootDatasets, + hierarchy, + browsePath, + goToDataset, + navLabel, + ]); + + /* ── breadcrumbs ── */ + const breadcrumbs = useMemo(() => { + const crumbs: Array<{ text: string; onClick?: () => void }> = [ + { + text: "All Datasets", + onClick: + browsePath.namespace !== undefined + ? () => setBrowsePath({}) + : undefined, + }, + ]; + if (browsePath.namespace) { + crumbs.push({ + text: browsePath.namespace, + onClick: browsePath.collection + ? () => setBrowsePath({ namespace: browsePath.namespace }) + : undefined, + }); + if (browsePath.collection) { + crumbs.push({ text: browsePath.collection }); + } + } + return crumbs; + }, [browsePath]); + + /* ── content ── */ + const renderContent = () => { + // ─── Root level: namespace folders + datasets mixed in one grid ─── + if (browsePath.namespace === undefined) { + if (namespaceList.length === 0 && rootDatasets.length === 0) { + return ( + No datasets yet} + body={ +

+ Add datasets and organize them into namespaces and collections. +

+ } + /> + ); + } + + return ( + + {namespaceList.map((ns) => ( + setBrowsePath({ namespace: ns.name })} + /> + ))} + {rootDatasets.map((d: any) => ( + goToDataset(d)} + onDelete={onDelete} + /> + ))} + + ); + } + + // ─── Namespace level: collection folders + direct datasets mixed ─── + if (browsePath.namespace && !browsePath.collection) { + const ns = browsePath.namespace; + const cols = hierarchy[ns] || {}; + const colNames = Object.keys(cols) + .filter((c) => c !== "") + .sort(); + const directDs = cols[""] || []; + + return ( + + {colNames.map((col) => ( + setBrowsePath({ namespace: ns, collection: col })} + /> + ))} + {directDs.map((d: any) => ( + goToDataset(d)} + onDelete={onDelete} + /> + ))} + {colNames.length === 0 && directDs.length === 0 && ( + + + This namespace is empty. + + + )} + + ); + } + + // ─── Collection level: datasets only ─── + if (browsePath.namespace && browsePath.collection) { + const dsList = + hierarchy[browsePath.namespace]?.[browsePath.collection] || []; + + return ( + + {dsList.map((d: any) => ( + goToDataset(d)} + onDelete={onDelete} + /> + ))} + {dsList.length === 0 && ( + + + No datasets in this collection. + + + )} + + ); + } + + return null; + }; + + /* ──────────────────── render ──────────────────── */ + + const hasTree = namespaceList.length > 0; + + return ( +
+ {/* Tree sidebar */} + {hasTree && ( +
+ {showTree ? ( + <> +
+ + + Catalog + + + + setShowTree(false)} + /> + +
+ +
+ + +
+ + ) : ( + + setShowTree(true)} + /> + + )} +
+ )} + + {/* Content */} +
+ + + {renderContent()} +
+
+ ); +}; + +export default DatasetCatalogBrowser; diff --git a/ui/src/pages/saved-data-sets/DatasetInstance.tsx b/ui/src/pages/saved-data-sets/DatasetInstance.tsx index 0f7e3ab8984..2856af5f342 100644 --- a/ui/src/pages/saved-data-sets/DatasetInstance.tsx +++ b/ui/src/pages/saved-data-sets/DatasetInstance.tsx @@ -1,33 +1,116 @@ -import React from "react"; +import React, { useState, useContext, useCallback } from "react"; import { Route, Routes, useNavigate, useParams } from "react-router-dom"; -import { EuiPageTemplate } from "@elastic/eui"; +import { + EuiPageTemplate, + EuiButton, + EuiButtonEmpty, + EuiConfirmModal, + EuiCallOut, + EuiSpacer, +} from "@elastic/eui"; +import { useMutation, useQueryClient } from "react-query"; import { DatasetIcon } from "../../graphics/DatasetIcon"; - import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import DatasetOverviewTab from "./DatasetOverviewTab"; +import DatasetUsageTab from "./DatasetUsageTab"; +import DatasetSampleTab from "./DatasetSampleTab"; +import EditDatasetModal from "./EditDatasetModal"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import DatasetExpectationsTab from "./DatasetExpectationsTab"; import { useDatasetCustomTabs, useDataSourceCustomTabRoutes, } from "../../custom-tabs/TabsRegistryContext"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; +import { restPost, restDelete } from "../../queries/restApiClient"; +import useLoadDataset from "./useLoadDataset"; const DatasetInstance = () => { const navigate = useNavigate(); - let { datasetName } = useParams(); + const { datasetName, projectName } = useParams(); - useDocumentTitle(`${datasetName} | Saved Datasets | Feast`); + useDocumentTitle(`${datasetName} | Datasets | Feast`); const { customNavigationTabs } = useDatasetCustomTabs(navigate); const CustomTabRoutes = useDataSourceCustomTabRoutes(); + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + const queryClient = useQueryClient(); + + const { data: datasetData } = useLoadDataset(datasetName || ""); + + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [editError, setEditError] = useState(null); + + const deleteMutation = useMutation( + () => + restDelete( + registryUrl, + `/saved_datasets/${encodeURIComponent(datasetName || "")}?project=${encodeURIComponent(projectName || "")}`, + fetchOptions, + ), + { + onSuccess: () => { + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + navigate(`/p/${projectName}/data-set`); + }, + }, + ); + + const editMutation = useMutation( + (payload: any) => + restPost(registryUrl, "/saved_datasets", payload, fetchOptions), + { + onSuccess: () => { + setShowEditModal(false); + setEditError(null); + queryClient.invalidateQueries(["rest", `saved-dataset:${datasetName}`]); + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + }, + onError: (err: Error) => { + setEditError(err.message); + }, + }, + ); + + const handleEditSubmit = useCallback( + async (payload: any) => { + payload.project = projectName || ""; + await editMutation.mutateAsync(payload); + }, + [projectName, editMutation], + ); + return ( { + setEditError(null); + setShowEditModal(true); + }} + > + Edit + , + setShowDeleteConfirm(true)} + > + Delete + , + ]} tabs={[ { label: "Overview", @@ -37,22 +120,72 @@ const DatasetInstance = () => { }, }, { - label: "Expectations", - isSelected: useMatchSubpath("expectations"), + label: "Preview Data", + isSelected: useMatchSubpath("sample"), onClick: () => { - navigate("expectations"); + navigate("sample"); + }, + }, + { + label: "Usage", + isSelected: useMatchSubpath("usage"), + onClick: () => { + navigate("usage"); }, }, ...customNavigationTabs, ]} /> + {deleteMutation.isError && ( + <> + +

{(deleteMutation.error as Error)?.message}

+
+ + + )} } /> - } /> + } /> + } /> {CustomTabRoutes}
+ + {showDeleteConfirm && ( + setShowDeleteConfirm(false)} + onConfirm={() => deleteMutation.mutate()} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteMutation.isLoading} + > +

+ Are you sure you want to delete {datasetName}? +

+

+ This removes the dataset metadata from the registry. The underlying + data at the storage location will not be deleted. +

+
+ )} + + {showEditModal && datasetData && ( + setShowEditModal(false)} + onSubmit={handleEditSubmit} + isSubmitting={editMutation.isLoading} + error={editError} + /> + )}
); }; diff --git a/ui/src/pages/saved-data-sets/DatasetOverviewTab.tsx b/ui/src/pages/saved-data-sets/DatasetOverviewTab.tsx index 9ee7dd1aa42..65e3cc2732b 100644 --- a/ui/src/pages/saved-data-sets/DatasetOverviewTab.tsx +++ b/ui/src/pages/saved-data-sets/DatasetOverviewTab.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { EuiFlexGroup, EuiHorizontalRule, @@ -9,16 +10,54 @@ import { EuiDescriptionList, EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiBadge, + EuiText, + EuiCallOut, } from "@elastic/eui"; -import React from "react"; import { useParams } from "react-router-dom"; +import EuiCustomLink from "../../components/EuiCustomLink"; import DatasetFeaturesTable from "./DatasetFeaturesTable"; import DatasetJoinKeysTable from "./DatasetJoinKeysTable"; import useLoadDataset from "./useLoadDataset"; -import { toDate } from "../../utils/timestamp"; -const EntityOverviewTab = () => { - let { datasetName } = useParams(); +function extractStorageInfo(data: any): { type: string; path: string } { + const storage = data?.spec?.storage; + if (!storage) return { type: "Unknown", path: "—" }; + if (storage.fileStorage?.uri) + return { type: "File", path: storage.fileStorage.uri }; + if (storage.bigqueryStorage?.table) + return { type: "BigQuery", path: storage.bigqueryStorage.table }; + if (storage.snowflakeStorage?.table) + return { type: "Snowflake", path: storage.snowflakeStorage.table }; + if (storage.redshiftStorage?.table) + return { type: "Redshift", path: storage.redshiftStorage.table }; + if (storage.sparkStorage?.path) + return { type: "Spark", path: storage.sparkStorage.path }; + if (storage.sparkStorage?.table) + return { type: "Spark", path: storage.sparkStorage.table }; + if (storage.trinoStorage?.table) + return { type: "Trino", path: storage.trinoStorage.table }; + if (storage.athenaStorage?.table) + return { type: "Athena", path: storage.athenaStorage.table }; + if (storage.customStorage?.configuration) + return { type: "Custom", path: storage.customStorage.configuration }; + return { type: "Unknown", path: "—" }; +} + +function formatTimestamp(ts: any): string { + if (!ts) return "—"; + try { + return new Date(ts).toLocaleString("en-US", { + dateStyle: "medium", + timeStyle: "short", + }); + } catch { + return "—"; + } +} + +const DatasetOverviewTab = () => { + const { datasetName, projectName } = useParams(); if (!datasetName) { throw new Error( @@ -26,87 +65,226 @@ const EntityOverviewTab = () => { ); } - const { isLoading, isSuccess, isError, data } = useLoadDataset(datasetName); - const isEmpty = data === undefined; + const { isLoading, isError, data } = useLoadDataset(datasetName); + + if (isLoading) { + return ( + + + + + + Loading dataset details... + + + ); + } + + if (isError || !data) { + return ( + +

+ Could not load dataset {datasetName}. It may have + been deleted or the registry may be unavailable. +

+
+ ); + } + + const storageInfo = extractStorageInfo(data); + const features = data.spec?.features || []; + const joinKeys = data.spec?.joinKeys || data.spec?.join_keys || []; + const tags = data.spec?.tags || {}; + const featureServiceName = + data.spec?.featureServiceName || data.spec?.feature_service_name; + const namespace = data.spec?.namespace || ""; + const collection = data.spec?.collection || ""; + const description = data.spec?.description || ""; + const dataSources: string[] = data.spec?.dataSources || []; + const createdTs = data.meta?.createdTimestamp || data.meta?.created_timestamp; + const minEventTs = + data.meta?.minEventTimestamp || data.meta?.min_event_timestamp; + const maxEventTs = + data.meta?.maxEventTimestamp || data.meta?.max_event_timestamp; + + // Determine provenance: if min/max event timestamps exist, the dataset + // was likely created via SDK (persist), otherwise it was linked manually. + const provenance = minEventTs + ? "Created (feature retrieval)" + : "Linked (existing data)"; return ( - - {isLoading && ( - - Loading - - )} - {isEmpty &&

No dataset with name: {datasetName}

} - {isError &&

Error loading dataset: {datasetName}

} - {isSuccess && data && ( - - - - - -

Features

-
- - { - const [featureViewName, featureName] = - joinedName.split(":"); - - return { - featureViewName, - featureName, - }; - })! - } - /> -
- - - -

Join Keys

-
- - { - return { name: joinKey }; - })! - } - /> -
-
- - - -

Properties

-
- - - - Source Feature Service - - - {data?.spec?.featureServiceName!} - - -
- - - - Created - - {toDate(data?.meta?.createdTimestamp!).toLocaleDateString( - "en-CA", - )} - - - -
-
-
- )} -
+ + {/* Left: Schema */} + + + +

Features ({features.length})

+
+ + {features.length > 0 ? ( + { + const parts = joinedName.split(":"); + return { + featureViewName: parts.length > 1 ? parts[0] : "—", + featureName: parts.length > 1 ? parts[1] : joinedName, + }; + })} + /> + ) : ( + + No features defined + + )} +
+ + + + + +

Retrieval Keys ({joinKeys.length})

+
+ + {joinKeys.length > 0 ? ( + ({ name: joinKey }))} + /> + ) : ( + + No retrieval keys defined + + )} +
+
+ + {/* Right: Properties */} + + + +

Properties

+
+ + + Origin + + + {provenance} + + + + {description && ( + <> + Description + + {description} + + + )} + + {namespace && ( + <> + Namespace + + {namespace} + + + )} + + {collection && ( + <> + Collection + + {collection} + + + )} + + Storage Type + + {storageInfo.type} + + + Storage Path + + + {storageInfo.path} + + + + {dataSources.length > 0 && ( + <> + + Data Source{dataSources.length > 1 ? "s" : ""} + + + {dataSources.map((dsName, idx) => ( + + {idx > 0 && ", "} + + {dsName} + + + ))} + + + )} + + {featureServiceName && ( + <> + + Feature Service + + + {featureServiceName} + + + )} + + Created + + {formatTimestamp(createdTs)} + + + {minEventTs && ( + <> + + Event Time Range + + + {formatTimestamp(minEventTs)} → {formatTimestamp(maxEventTs)} + + + )} + +
+ + {Object.keys(tags).length > 0 && ( + <> + + + +

Tags

+
+ + + {Object.entries(tags).map(([key, value]) => ( + + {key} + + {value as string} + + + ))} + +
+ + )} +
+
); }; -export default EntityOverviewTab; + +export default DatasetOverviewTab; diff --git a/ui/src/pages/saved-data-sets/DatasetSampleTab.tsx b/ui/src/pages/saved-data-sets/DatasetSampleTab.tsx new file mode 100644 index 00000000000..66f41ea131d --- /dev/null +++ b/ui/src/pages/saved-data-sets/DatasetSampleTab.tsx @@ -0,0 +1,190 @@ +import React, { useState, useContext, useCallback } from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiText, + EuiCallOut, + EuiButton, + EuiSpacer, + EuiPanel, + EuiBasicTable, + EuiBasicTableColumn, + EuiFieldNumber, + EuiFormRow, + EuiBadge, + EuiTitle, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; + +const DatasetSampleTab = () => { + const { datasetName, projectName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [sampleData, setSampleData] = useState<{ + columns: string[]; + rows: Record[]; + total_rows: number; + sample_size: number; + } | null>(null); + const [limit, setLimit] = useState(10); + + const fetchSample = useCallback(async () => { + setLoading(true); + setError(null); + + try { + const response = await fetch( + `${registryUrl}/saved_datasets/data/${encodeURIComponent(datasetName || "")}?project=${encodeURIComponent(projectName || "")}&limit=${limit}`, + { method: "GET", ...fetchOptions }, + ); + + if (!response.ok) { + const err = await response + .json() + .catch(() => ({ detail: "Failed to load sample" })); + throw new Error(err.detail || `Request failed: ${response.status}`); + } + + const data = await response.json(); + setSampleData(data); + } catch (err: any) { + setError(err.message || "Failed to load dataset sample."); + } finally { + setLoading(false); + } + }, [registryUrl, datasetName, projectName, limit, fetchOptions]); + + const columns: EuiBasicTableColumn>[] = sampleData + ? sampleData.columns.map((col) => ({ + field: col, + name: col, + sortable: true, + truncateText: true, + render: (value: any) => { + if (value === null || value === undefined || value === "") { + return ( + + null + + ); + } + return String(value); + }, + })) + : []; + + return ( + <> + + + + + setLimit(parseInt(e.target.value) || 10)} + min={1} + max={100} + compressed + style={{ width: 80 }} + /> + + + + + Load Preview + + + {sampleData && ( + + + {sampleData.sample_size} of {sampleData.total_rows} rows + + + )} + + + + + + {error && ( + <> + +

{error}

+
+ + + )} + + {loading && ( + + + + + + Reading dataset... + + + )} + + {!loading && !sampleData && !error && ( + + + + +

Preview dataset contents

+
+
+ + + Click "Load Preview" to read and display actual rows from the + dataset's storage location. This executes a read query against + the configured offline store. + + +
+
+ )} + + {sampleData && sampleData.rows.length > 0 && ( + + )} + + {sampleData && sampleData.rows.length === 0 && ( + +

No rows found in this dataset's storage location.

+
+ )} + + ); +}; + +export default DatasetSampleTab; diff --git a/ui/src/pages/saved-data-sets/DatasetUsageTab.tsx b/ui/src/pages/saved-data-sets/DatasetUsageTab.tsx new file mode 100644 index 00000000000..01a9778213e --- /dev/null +++ b/ui/src/pages/saved-data-sets/DatasetUsageTab.tsx @@ -0,0 +1,253 @@ +import React from "react"; +import { + EuiPanel, + EuiTitle, + EuiText, + EuiSpacer, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiLoadingSpinner, + EuiIcon, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import useLoadDataset from "./useLoadDataset"; + +const DatasetUsageTab = () => { + const { datasetName } = useParams(); + + if (!datasetName) { + throw new Error("Unable to get dataset name."); + } + + const { isLoading, isSuccess, data } = useLoadDataset(datasetName); + + if (isLoading) { + return ; + } + + if (!isSuccess || !data) { + return ( + +

Could not load dataset details.

+
+ ); + } + + const name = data.spec?.name || datasetName; + + const loadCode = `from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +# Load the saved dataset +dataset = store.get_saved_dataset("${name}") + +# Convert to pandas DataFrame +df = dataset.to_df() +print(f"Loaded {len(df)} rows, {len(df.columns)} columns") +print(df.head())`; + + const loadArrowCode = `# Load as PyArrow Table (more efficient for large datasets) +dataset = store.get_saved_dataset("${name}") +table = dataset.to_arrow() +print(f"Schema: {table.schema}") +print(f"Rows: {table.num_rows}")`; + + const torchCode = `import torch +from torch.utils.data import TensorDataset, DataLoader + +dataset = store.get_saved_dataset("${name}") +df = dataset.to_df() + +# Select your feature columns and target +feature_cols = df.select_dtypes(include=["number"]).columns.tolist() +X = torch.tensor(df[feature_cols].values, dtype=torch.float32) + +# Create DataLoader +torch_dataset = TensorDataset(X) +loader = DataLoader(torch_dataset, batch_size=32, shuffle=True) + +for batch in loader: + # Your training loop here + pass`; + + const registerCode = `from feast import FeatureStore +from feast.infra.offline_stores.file_source import SavedDatasetFileStorage + +store = FeatureStore(repo_path=".") + +# Create from a historical retrieval job +entity_df = ... # Your entity DataFrame with timestamps +job = store.get_historical_features( + entity_df=entity_df, + features=[ + "feature_view:feature_1", + "feature_view:feature_2", + ], +) + +# Persist and register +dataset = store.create_saved_dataset( + from_=job, + name="${name}", + storage=SavedDatasetFileStorage(path="data/${name}.parquet"), + tags={"team": "ml", "version": "1"}, +)`; + + const validationCode = `from feast.dqm.profilers.ge_profiler import GEProfiler + +dataset = store.get_saved_dataset("${name}") + +# Create a validation reference +profiler = GEProfiler() +ref = dataset.as_reference(name="${name}_ref", profiler=profiler) + +# Apply the reference to the registry +store.apply(ref) + +# Later: validate logged features against this reference +store.validate_logged_features( + source=feature_service, + start=start_time, + end=end_time, + reference=ref, +)`; + + return ( + + {/* Load Dataset */} + + + + + + + + +

Load Dataset

+
+
+
+ +

+ Retrieve this dataset as a pandas DataFrame or PyArrow Table for + analysis or training. +

+
+ + + {loadCode} + + + + {loadArrowCode} + +
+
+ + {/* Training Integration */} + + + + + + + + +

Use for Training (PyTorch)

+
+
+
+ +

Convert the dataset into PyTorch tensors for model training.

+
+ + + {torchCode} + +
+
+ + {/* Register via SDK */} + + + + + + + + +

Register via SDK

+
+
+
+ +

+ Create this dataset programmatically from a historical feature + retrieval job. +

+
+ + + {registerCode} + +
+
+ + {/* Validation */} + + + + + + + + +

Data Validation

+
+
+
+ +

+ Use this dataset as a reference for validating feature quality and + detecting drift. +

+
+ + + {validationCode} + +
+
+
+ ); +}; + +export default DatasetUsageTab; diff --git a/ui/src/pages/saved-data-sets/DatasetsCardGrid.tsx b/ui/src/pages/saved-data-sets/DatasetsCardGrid.tsx new file mode 100644 index 00000000000..d5c69082978 --- /dev/null +++ b/ui/src/pages/saved-data-sets/DatasetsCardGrid.tsx @@ -0,0 +1,396 @@ +import React, { useState } from "react"; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiText, + EuiBadge, + EuiSpacer, + EuiButtonIcon, + EuiToolTip, + EuiIcon, + EuiCopy, + EuiLink, +} from "@elastic/eui"; +import { useNavigate, useParams } from "react-router-dom"; + +const STORAGE_TYPE_CONFIG: Record< + string, + { label: string; color: string; icon: string } +> = { + file: { label: "File", color: "#4CAF50", icon: "document" }, + bigquery: { label: "BigQuery", color: "#4285F4", icon: "storage" }, + snowflake: { label: "Snowflake", color: "#29B5E8", icon: "snowflake" }, + redshift: { label: "Redshift", color: "#205B97", icon: "compute" }, + spark: { label: "Spark", color: "#E25A1C", icon: "bolt" }, + trino: { label: "Trino", color: "#DD00A1", icon: "database" }, + athena: { label: "Athena", color: "#8C4FFF", icon: "database" }, + custom: { label: "Custom", color: "#607D8B", icon: "gear" }, + unknown: { label: "Storage", color: "#98A2B3", icon: "database" }, +}; + +function detectStorageType(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return "unknown"; + if (storage.fileStorage) return "file"; + if (storage.bigqueryStorage) return "bigquery"; + if (storage.snowflakeStorage) return "snowflake"; + if (storage.redshiftStorage) return "redshift"; + if (storage.sparkStorage) return "spark"; + if (storage.trinoStorage) return "trino"; + if (storage.athenaStorage) return "athena"; + if (storage.customStorage) return "custom"; + return "unknown"; +} + +function extractStoragePath(dataset: any): string | undefined { + const storage = dataset?.spec?.storage; + if (!storage) return undefined; + if (storage.fileStorage?.uri) return storage.fileStorage.uri; + if (storage.bigqueryStorage?.table) return storage.bigqueryStorage.table; + if (storage.snowflakeStorage?.table) return storage.snowflakeStorage.table; + if (storage.redshiftStorage?.table) return storage.redshiftStorage.table; + if (storage.sparkStorage?.path) return storage.sparkStorage.path; + if (storage.sparkStorage?.table) return storage.sparkStorage.table; + if (storage.trinoStorage?.table) return storage.trinoStorage.table; + if (storage.athenaStorage?.table) return storage.athenaStorage.table; + if (storage.customStorage?.configuration) + return storage.customStorage.configuration; + return undefined; +} + +function truncatePath(path: string, maxLen: number = 42): string { + if (path.length <= maxLen) return path; + const start = path.slice(0, 18); + const end = path.slice(-20); + return `${start}...${end}`; +} + +function getRelativeTime(date: Date): string { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) return "Today"; + if (diffDays === 1) return "Yesterday"; + if (diffDays < 7) return `${diffDays}d ago`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; + if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`; + return `${Math.floor(diffDays / 365)}y ago`; +} + +interface DatasetCardProps { + dataset: any; + isHovered: boolean; + onHover: (name: string | null) => void; + onDelete?: (name: string) => void; +} + +const DatasetCard: React.FC = ({ + dataset, + isHovered, + onHover, + onDelete, +}) => { + const { projectName } = useParams(); + const navigate = useNavigate(); + const spec = dataset.spec || dataset; + const meta = dataset.meta || {}; + const name = spec.name || "unknown"; + const datasetProject = dataset.project || spec.project || projectName; + const features = spec.features || []; + const joinKeys = spec.joinKeys || spec.join_keys || []; + const tags = spec.tags || {}; + const featureServiceName = + spec.featureServiceName || spec.feature_service_name; + const namespace = spec.namespace || ""; + const collection = spec.collection || ""; + const description = spec.description || ""; + const createdTimestamp = meta.createdTimestamp || meta.created_timestamp; + const storagePath = extractStoragePath(dataset); + const storageType = detectStorageType(dataset); + const storageInfo = + STORAGE_TYPE_CONFIG[storageType] || STORAGE_TYPE_CONFIG.unknown; + + const formattedDate = createdTimestamp + ? new Date(createdTimestamp).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + : null; + const relativeTime = createdTimestamp + ? getRelativeTime(new Date(createdTimestamp)) + : null; + + const codeSnippet = `dataset = store.get_saved_dataset("${name}")\ndf = dataset.to_df()`; + + return ( + + onHover(name)} + onMouseLeave={() => onHover(null)} + onClick={() => navigate(`/p/${datasetProject}/data-set/${name}`)} + style={{ + height: "100%", + display: "flex", + flexDirection: "column", + transition: "all 0.2s ease", + transform: isHovered ? "translateY(-2px)" : "none", + borderTop: `3px solid ${storageInfo.color}`, + cursor: "pointer", + }} + > + {/* Header: Name + Storage badge */} + + + +

+ {name} +

+
+
+ + + {storageInfo.label} + + +
+ + {/* Description */} + {description && ( + +

{description}

+
+ )} + + {/* Namespace / Collection badges */} + {(namespace || collection) && ( + <> + + + {namespace && ( + + {namespace} + + )} + {collection && ( + + {collection} + + )} + + + )} + + + + {/* Storage path */} + {storagePath && ( + + + {" "} + + {truncatePath(storagePath)} + + + + )} + + + + {/* Metrics */} + + + + Features + + + {features.length} + + + + + Retrieval Keys + + + {joinKeys.length} + + + {featureServiceName && ( + + + Service + + + {featureServiceName} + + + )} + {spec.dataSources && spec.dataSources.length > 0 && ( + + + Source + + + {spec.dataSources.map((dsName: string, idx: number) => ( + + {idx > 0 && ", "} + { + e.stopPropagation(); + navigate(`/p/${datasetProject}/data-source/${dsName}`); + }} + > + {dsName} + + + ))} + + + )} + + + {/* Spacer to push footer */} +
+ + + + {/* Tags row */} + {Object.keys(tags).length > 0 && ( + <> + + {Object.entries(tags) + .slice(0, 3) + .map(([key, value]) => ( + + + {key}: {value as string} + + + ))} + {Object.keys(tags).length > 3 && ( + + + +{Object.keys(tags).length - 3} more + + + )} + + + + )} + + {/* Footer: Timestamp + actions */} + + + {formattedDate && ( + + + {relativeTime} + + + )} + + + + + + {(copy) => ( + + { + e.stopPropagation(); + copy(); + }} + /> + + )} + + + {onDelete && ( + + + { + e.stopPropagation(); + onDelete(name); + }} + /> + + + )} + + + + + + ); +}; + +interface DatasetsCardGridProps { + datasets: any[]; + onDelete?: (name: string) => void; +} + +const DatasetsCardGrid = ({ datasets, onDelete }: DatasetsCardGridProps) => { + const [hoveredId, setHoveredId] = useState(null); + + if (datasets.length === 0) { + return ( + +

No datasets match your search.

+
+ ); + } + + return ( + + {datasets.map((dataset: any) => { + const name = dataset.spec?.name || dataset.name || "unknown"; + return ( + + ); + })} + + ); +}; + +export default DatasetsCardGrid; diff --git a/ui/src/pages/saved-data-sets/DatasetsIndexEmptyState.tsx b/ui/src/pages/saved-data-sets/DatasetsIndexEmptyState.tsx index 9f1a34be2a5..a8f039bc28f 100644 --- a/ui/src/pages/saved-data-sets/DatasetsIndexEmptyState.tsx +++ b/ui/src/pages/saved-data-sets/DatasetsIndexEmptyState.tsx @@ -1,29 +1,54 @@ import React from "react"; -import { EuiEmptyPrompt, EuiTitle, EuiLink, EuiButton } from "@elastic/eui"; +import { + EuiEmptyPrompt, + EuiTitle, + EuiLink, + EuiButton, + EuiFlexGroup, + EuiFlexItem, +} from "@elastic/eui"; import FeastIconBlue from "../../graphics/FeastIconBlue"; -const DatasetsIndexEmptyState = () => { +interface DatasetsIndexEmptyStateProps { + onRegister?: () => void; +} + +const DatasetsIndexEmptyState = ({ + onRegister, +}: DatasetsIndexEmptyStateProps) => { return ( There are no saved datasets} + title={

Your Data Catalog is empty

} body={

- You currently do not have any saved datasets. Learn more about - creating saved datasets in Feast Docs. + The Data Catalog lets you create, link, share, and reuse curated + feature datasets for model training and validation. Link an existing + data artifact or create a new one by running a feature retrieval job.

} actions={ - { - window.open( - "https://docs.feast.dev/getting-started/concepts/dataset#creating-saved-dataset-from-historical-retrieval", - "_blank", - ); - }} - > - Open Dataset Docs - + + {onRegister && ( + + + Add to Catalog + + + )} + + { + window.open( + "https://docs.feast.dev/getting-started/concepts/dataset#creating-saved-dataset-from-historical-retrieval", + "_blank", + ); + }} + > + Open Dataset Docs + + + } footer={ <> diff --git a/ui/src/pages/saved-data-sets/DatasetsListingTable.tsx b/ui/src/pages/saved-data-sets/DatasetsListingTable.tsx index 7b73e9cd6dc..0ce44b35dc0 100644 --- a/ui/src/pages/saved-data-sets/DatasetsListingTable.tsx +++ b/ui/src/pages/saved-data-sets/DatasetsListingTable.tsx @@ -1,12 +1,24 @@ import React from "react"; -import { EuiBasicTable } from "@elastic/eui"; +import { EuiBasicTable, EuiBadge } from "@elastic/eui"; import EuiCustomLink from "../../components/EuiCustomLink"; import { useParams } from "react-router-dom"; -import { feast } from "../../protos"; -import { toDate } from "../../utils/timestamp"; interface DatasetsListingTableProps { - datasets: feast.core.ISavedDataset[]; + datasets: any[]; +} + +function detectStorageType(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return "—"; + if (storage.fileStorage) return "File"; + if (storage.bigqueryStorage) return "BigQuery"; + if (storage.snowflakeStorage) return "Snowflake"; + if (storage.redshiftStorage) return "Redshift"; + if (storage.sparkStorage) return "Spark"; + if (storage.trinoStorage) return "Trino"; + if (storage.athenaStorage) return "Athena"; + if (storage.customStorage) return "Custom"; + return "—"; } const DatasetsListingTable = ({ datasets }: DatasetsListingTableProps) => { @@ -17,27 +29,116 @@ const DatasetsListingTable = ({ datasets }: DatasetsListingTableProps) => { name: "Name", field: "spec.name", sortable: true, - render: (name: string) => { + render: (name: string, item: any) => { + const itemProject = item.project || item.spec?.project || projectName; return ( - - {name} + + {name} ); }, }, { - name: "Source Feature Service", - field: "spec.featureService", + name: "Namespace", + render: (item: any) => { + const ns = item.spec?.namespace; + return ns ? ( + {ns} + ) : ( + + ); + }, + width: "140px", + }, + { + name: "Collection", + render: (item: any) => { + const col = item.spec?.collection; + return col ? ( + {col} + ) : ( + + ); + }, + width: "140px", + }, + { + name: "Description", + render: (item: any) => { + const desc = item.spec?.description; + return desc ? ( + + {desc.length > 50 ? desc.slice(0, 50) + "…" : desc} + + ) : ( + + ); + }, + width: "200px", + }, + { + name: "Features", + render: (item: any) => (item.spec?.features || []).length, + width: "90px", + }, + { + name: "Retrieval Keys", + render: (item: any) => + (item.spec?.joinKeys || item.spec?.join_keys || []).length, + width: "90px", + }, + { + name: "Storage", + render: (item: any) => ( + {detectStorageType(item)} + ), + width: "120px", + }, + { + name: "Data Source", + render: (item: any) => { + const dsList: string[] = item.spec?.dataSources || []; + if (dsList.length === 0) return "—"; + const itemProject = item.project || item.spec?.project || projectName; + return ( + <> + {dsList.map((dsName, idx) => ( + + {idx > 0 && ", "} + + {dsName} + + + ))} + + ); + }, + }, + { + name: "Feature Service", + render: (item: any) => + item.spec?.featureServiceName || item.spec?.feature_service_name || "—", }, { name: "Created", - render: (item: feast.core.ISavedDataset) => { - return toDate(item?.meta?.createdTimestamp!).toLocaleString("en-CA")!; + render: (item: any) => { + const ts = item.meta?.createdTimestamp || item.meta?.created_timestamp; + if (!ts) return "—"; + try { + return new Date(ts).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + } catch { + return "—"; + } }, + width: "140px", }, ]; - const getRowProps = (item: feast.core.ISavedDataset) => { + const getRowProps = (item: any) => { return { "data-test-subj": `row-${item.spec?.name}`, }; diff --git a/ui/src/pages/saved-data-sets/EditDatasetModal.tsx b/ui/src/pages/saved-data-sets/EditDatasetModal.tsx new file mode 100644 index 00000000000..507827e6dd0 --- /dev/null +++ b/ui/src/pages/saved-data-sets/EditDatasetModal.tsx @@ -0,0 +1,708 @@ +import React, { useState, useMemo } from "react"; +import { + EuiFormRow, + EuiFieldText, + EuiSpacer, + EuiHorizontalRule, + EuiText, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiComboBox, + EuiComboBoxOptionOption, + EuiCheckbox, + EuiSuperSelect, + EuiSuperSelectOption, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import FormModal from "../../components/forms/FormModal"; +import TagsEditor, { TagEntry } from "../../components/forms/TagsEditor"; +import useResourceQuery, { + featureServiceListPath, + featureViewListPath, + dataSourceListPath, +} from "../../queries/useResourceQuery"; + +interface EditDatasetModalProps { + dataset: any; + onClose: () => void; + onSubmit: (data: any) => Promise; + isSubmitting: boolean; + error?: string | null; +} + +interface StorageTypeDef { + value: string; + label: string; + description: string; + placeholder: string; + helpText: string; + sourceTypeMatch: string[]; +} + +const ALL_STORAGE_TYPES: StorageTypeDef[] = [ + { + value: "file", + label: "File (Parquet / CSV)", + description: "Local or remote file path (S3, GCS, HDFS)", + placeholder: "s3://my-bucket/datasets/training_v1.parquet", + helpText: "Path to the data file accessible by the Feast server.", + sourceTypeMatch: ["BATCH_FILE"], + }, + { + value: "bigquery", + label: "BigQuery", + description: "Google BigQuery table reference", + placeholder: "project_id.dataset.table_name", + helpText: "Full BigQuery table reference: project:dataset.table", + sourceTypeMatch: ["BATCH_BIGQUERY"], + }, + { + value: "snowflake", + label: "Snowflake", + description: "Snowflake table reference", + placeholder: "database.schema.table_name", + helpText: "Snowflake table: database.schema.table", + sourceTypeMatch: ["BATCH_SNOWFLAKE"], + }, + { + value: "redshift", + label: "Redshift", + description: "Amazon Redshift table reference", + placeholder: "schema.table_name", + helpText: "Redshift table: schema.table", + sourceTypeMatch: ["BATCH_REDSHIFT"], + }, + { + value: "spark", + label: "Spark", + description: "Apache Spark table or path", + placeholder: "s3://bucket/path/ or catalog.database.table", + helpText: "Spark path or catalog table reference.", + sourceTypeMatch: ["BATCH_SPARK"], + }, + { + value: "trino", + label: "Trino", + description: "Trino table reference", + placeholder: "catalog.schema.table", + helpText: "Trino table: catalog.schema.table", + sourceTypeMatch: ["BATCH_TRINO"], + }, + { + value: "athena", + label: "AWS Athena", + description: "AWS Athena table reference", + placeholder: "database.table_name", + helpText: "Athena table reference.", + sourceTypeMatch: ["BATCH_ATHENA"], + }, + { + value: "postgres", + label: "PostgreSQL", + description: "PostgreSQL table reference", + placeholder: "schema.table_name", + helpText: + "PostgreSQL table reference. Data is read via the PostgreSQL offline store.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "clickhouse", + label: "ClickHouse", + description: "ClickHouse table reference", + placeholder: "database.table_name", + helpText: + "ClickHouse table reference. Data is read via the ClickHouse offline store.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "couchbase", + label: "Couchbase Columnar", + description: "Couchbase Columnar collection reference", + placeholder: "database.scope.collection", + helpText: + "Couchbase Columnar reference in format: database.scope.collection", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "custom", + label: "Custom", + description: "Custom storage configuration", + placeholder: '{"class": "my.CustomStorage", "config": {}}', + helpText: "Serialized configuration for a custom storage implementation.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, +]; + +function detectDataSourceTypes(dataSources: any[]): Set { + const types = new Set(); + for (const ds of dataSources) { + const dsType = ds.spec?.type || ds.type; + if (dsType != null) { + const typeName = dataSourceTypeToName(dsType); + if (typeName) types.add(typeName); + } + if (ds.spec?.fileOptions || ds.fileOptions) types.add("BATCH_FILE"); + if (ds.spec?.bigqueryOptions || ds.bigqueryOptions) + types.add("BATCH_BIGQUERY"); + if (ds.spec?.snowflakeOptions || ds.snowflakeOptions) + types.add("BATCH_SNOWFLAKE"); + if (ds.spec?.redshiftOptions || ds.redshiftOptions) + types.add("BATCH_REDSHIFT"); + if (ds.spec?.sparkOptions || ds.sparkOptions) types.add("BATCH_SPARK"); + if (ds.spec?.trinoOptions || ds.trinoOptions) types.add("BATCH_TRINO"); + if (ds.spec?.athenaOptions || ds.athenaOptions) types.add("BATCH_ATHENA"); + if (ds.spec?.customOptions || ds.customOptions) types.add("CUSTOM_SOURCE"); + const classType = + ds.spec?.dataSourceClassType || ds.dataSourceClassType || ""; + if (classType.includes("postgres")) types.add("CUSTOM_SOURCE"); + if (classType.includes("clickhouse")) types.add("CUSTOM_SOURCE"); + if (classType.includes("couchbase")) types.add("CUSTOM_SOURCE"); + } + return types; +} + +function dataSourceTypeToName(typeNum: number | string): string | null { + const map: Record = { + "1": "BATCH_FILE", + "2": "BATCH_BIGQUERY", + "3": "BATCH_REDSHIFT", + "5": "BATCH_SNOWFLAKE", + "7": "BATCH_SPARK", + "8": "BATCH_TRINO", + "9": "BATCH_ATHENA", + "6": "STREAM_KAFKA", + "10": "STREAM_KINESIS", + "4": "REQUEST_SOURCE", + "12": "PUSH_SOURCE", + "11": "CUSTOM_SOURCE", + }; + return map[String(typeNum)] || null; +} + +function detectStorageType(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return "file"; + if (storage.fileStorage) return "file"; + if (storage.bigqueryStorage) return "bigquery"; + if (storage.snowflakeStorage) return "snowflake"; + if (storage.redshiftStorage) return "redshift"; + if (storage.sparkStorage) return "spark"; + if (storage.trinoStorage) return "trino"; + if (storage.athenaStorage) return "athena"; + if (storage.customStorage) { + try { + const config = storage.customStorage.configuration || ""; + const parsed = typeof config === "string" ? JSON.parse(config) : config; + if (parsed.database && parsed.scope && parsed.collection) + return "couchbase"; + if (parsed.table) { + const classType = + dataset?.spec?.dataSourceClassType || + dataset?.dataSourceClassType || + ""; + if (classType.includes("postgres")) return "postgres"; + if (classType.includes("clickhouse")) return "clickhouse"; + } + } catch { + // fall through + } + return "custom"; + } + return "file"; +} + +function extractStoragePath(dataset: any): string { + const storage = dataset?.spec?.storage; + if (!storage) return ""; + if (storage.fileStorage?.uri) return storage.fileStorage.uri; + if (storage.bigqueryStorage?.table) return storage.bigqueryStorage.table; + if (storage.snowflakeStorage?.table) return storage.snowflakeStorage.table; + if (storage.redshiftStorage?.table) return storage.redshiftStorage.table; + if (storage.sparkStorage?.path) return storage.sparkStorage.path; + if (storage.sparkStorage?.table) return storage.sparkStorage.table; + if (storage.trinoStorage?.table) return storage.trinoStorage.table; + if (storage.athenaStorage?.table) return storage.athenaStorage.table; + if (storage.customStorage?.configuration) + return storage.customStorage.configuration; + return ""; +} + +function extractStorageFileFormat(dataset: any): string { + const storage = dataset?.spec?.storage; + if (storage?.sparkStorage?.fileFormat) return storage.sparkStorage.fileFormat; + if (storage?.sparkStorage?.file_format) + return storage.sparkStorage.file_format; + return "parquet"; +} + +const EditDatasetModal = ({ + dataset, + onClose, + onSubmit, + isSubmitting, + error, +}: EditDatasetModalProps) => { + const { projectName } = useParams(); + const spec = dataset?.spec || {}; + const datasetName = spec.name || ""; + + // Load data sources to filter storage type options + const { data: dataSourcesRaw } = useResourceQuery({ + resourceType: "edit-modal-ds", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources || [], + }); + + // Load feature services for dropdown + const { data: featureServicesRaw } = useResourceQuery({ + resourceType: "edit-modal-fs", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices || [], + }); + + // Load feature views for features/join key suggestions + const { data: featureViewsRaw } = useResourceQuery({ + resourceType: "edit-modal-fv", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: (d) => d.featureViews || [], + }); + + // Derive available storage types from project's data sources + const availableStorageOptions: EuiSuperSelectOption[] = + useMemo(() => { + const currentType = detectStorageType(dataset); + + if (!dataSourcesRaw || dataSourcesRaw.length === 0) { + return ALL_STORAGE_TYPES.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: ( + <> + {st.label} + +

{st.description}

+
+ + ), + })); + } + + const detectedTypes = detectDataSourceTypes(dataSourcesRaw); + + let matched = ALL_STORAGE_TYPES.filter((st) => + st.sourceTypeMatch.some((match) => detectedTypes.has(match)), + ); + + // Always include File as a fallback + if (!matched.some((st) => st.value === "file")) { + matched = [ALL_STORAGE_TYPES[0], ...matched]; + } + + // Always include the dataset's current storage type so it's visible + if (!matched.some((st) => st.value === currentType)) { + const currentDef = ALL_STORAGE_TYPES.find( + (st) => st.value === currentType, + ); + if (currentDef) matched = [currentDef, ...matched]; + } + + return matched.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: ( + <> + {st.label} + +

{st.description}

+
+ + ), + })); + }, [dataSourcesRaw, dataset]); + + // Build feature suggestions from loaded feature views + const featureOptions: EuiComboBoxOptionOption[] = useMemo( + () => + (featureViewsRaw || []) + .filter((fv: any) => fv.type !== "labelView") + .flatMap((fv: any) => { + const fvName = fv.spec?.name || ""; + const features = fv.spec?.features || []; + return features.map((f: any) => ({ + label: `${fvName}:${f.name || f}`, + })); + }), + [featureViewsRaw], + ); + + // Build join key suggestions from feature views' entities + const joinKeyOptions: EuiComboBoxOptionOption[] = useMemo(() => { + const seen = new Set(); + (featureViewsRaw || []).forEach((fv: any) => { + const entities = fv.spec?.entities || []; + entities.forEach((e: string) => { + if (!seen.has(e)) seen.add(e); + }); + }); + return Array.from(seen).map((k) => ({ label: k })); + }, [featureViewsRaw]); + + // Build feature service options for dropdown + const featureServiceOptions: EuiComboBoxOptionOption[] = useMemo( + () => + (featureServicesRaw || []).map((fs: any) => ({ + label: fs.spec?.name || fs.name || "", + })), + [featureServicesRaw], + ); + + // Form state + const [storagePath, setStoragePath] = useState(extractStoragePath(dataset)); + const [storageType, setStorageType] = useState(detectStorageType(dataset)); + const [namespace, setNamespace] = useState(spec.namespace || ""); + const [collection, setCollection] = useState(spec.collection || ""); + const [description, setDescription] = useState(spec.description || ""); + const [storageFileFormat, setStorageFileFormat] = useState( + extractStorageFileFormat(dataset), + ); + const [featuresInput, setFeaturesInput] = useState( + (spec.features || []).map((f: string) => ({ label: f })), + ); + const [joinKeysInput, setJoinKeysInput] = useState( + (spec.joinKeys || spec.join_keys || []).map((k: string) => ({ label: k })), + ); + const [tags, setTags] = useState( + Object.entries(spec.tags || {}).map(([key, value]) => ({ + key, + value: value as string, + })), + ); + const [featureServiceName, setFeatureServiceName] = useState( + spec.featureServiceName || spec.feature_service_name || "", + ); + const [fullFeatureNames, setFullFeatureNames] = useState( + spec.fullFeatureNames || spec.full_feature_names || false, + ); + const [errors, setErrors] = useState>({}); + const [submitted, setSubmitted] = useState(false); + + // Get current storage type config + const currentStorageConfig = + ALL_STORAGE_TYPES.find((st) => st.value === storageType) || + ALL_STORAGE_TYPES[0]; + + const validate = (): boolean => { + const newErrors: Record = {}; + + if (!storagePath.trim()) { + newErrors.storagePath = "Storage path is required."; + } + + const tagKeys = tags.map((t) => t.key).filter((k) => k.trim()); + if (new Set(tagKeys).size !== tagKeys.length) { + newErrors.tags = "Tag keys must be unique."; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + setSubmitted(true); + if (!validate()) return; + + const tagsObj: Record = {}; + tags.forEach(({ key, value }) => { + if (key.trim() && value.trim()) tagsObj[key.trim()] = value.trim(); + }); + + const payload = { + name: datasetName, + project: "", + features: featuresInput.map((o) => o.label), + join_keys: joinKeysInput.map((o) => o.label), + storage_path: storagePath.trim(), + storage_type: storageType, + storage_file_format: + storageType === "spark" ? storageFileFormat : undefined, + tags: tagsObj, + full_feature_names: fullFeatureNames, + feature_service_name: featureServiceName || undefined, + namespace: namespace.trim() || undefined, + collection: collection.trim() || undefined, + description: description.trim() || undefined, + allow_override: true, + }; + await onSubmit(payload); + }; + + const clearFieldError = (field: string) => { + if (submitted) { + setErrors((prev) => { + const next = { ...prev }; + delete next[field]; + return next; + }); + } + }; + + return ( + + {error && ( + <> + +

{error}

+
+ + + )} + + {/* Identity (read-only name) */} + +

Identity

+
+ + + + + + + + + + + setDescription(e.target.value)} + placeholder="e.g. Training data for driver fraud model" + /> + + + + + + + + setFeatureServiceName( + selected.length > 0 ? selected[0].label : "", + ) + } + onCreateOption={(val) => setFeatureServiceName(val)} + placeholder="Select or type..." + isClearable + /> + + + + + + + {/* Organization */} + +

Organization (optional)

+
+ + + + + + setNamespace(e.target.value)} + placeholder="e.g. fraud" + /> + + + + + setCollection(e.target.value)} + placeholder="e.g. training" + /> + + + + + + + + {/* Storage */} + +

Storage Location

+
+ + + + + Only storage types matching your project's configured data sources are + shown. + + + + + + setStorageType(value)} + fullWidth + /> + + + + { + setStoragePath(e.target.value); + clearFieldError("storagePath"); + }} + isInvalid={!!errors.storagePath} + placeholder={currentStorageConfig.placeholder} + icon={storageType === "file" ? "document" : "storage"} + fullWidth + /> + + + {storageType === "spark" && ( + + Parquet, + }, + { + value: "avro", + inputDisplay: "Avro", + dropdownDisplay: Avro, + }, + { + value: "csv", + inputDisplay: "CSV", + dropdownDisplay: CSV, + }, + { + value: "json", + inputDisplay: "JSON", + dropdownDisplay: JSON, + }, + ]} + valueOfSelected={storageFileFormat} + onChange={setStorageFileFormat} + fullWidth + /> + + )} + + + + + {/* Schema */} + +

Schema

+
+ + + + { + setFeaturesInput([...featuresInput, { label: val }]); + }} + onChange={(selected) => setFeaturesInput(selected)} + placeholder="Search or type features..." + isClearable + fullWidth + /> + + + + { + setJoinKeysInput([...joinKeysInput, { label: val }]); + }} + onChange={(selected) => setJoinKeysInput(selected)} + placeholder="Search or type join keys..." + isClearable + fullWidth + /> + + + + setFullFeatureNames(e.target.checked)} + /> + + {/* Tags */} + + setTags(newTags)} + error={errors.tags} + /> +
+ ); +}; + +export default EditDatasetModal; diff --git a/ui/src/pages/saved-data-sets/Index.tsx b/ui/src/pages/saved-data-sets/Index.tsx index c6cc81f4146..1c587d83883 100644 --- a/ui/src/pages/saved-data-sets/Index.tsx +++ b/ui/src/pages/saved-data-sets/Index.tsx @@ -1,52 +1,623 @@ -import React, { useContext } from "react"; - -import { EuiPageTemplate, EuiLoadingSpinner } from "@elastic/eui"; +import React, { + useState, + useContext, + useCallback, + useMemo, + useEffect, +} from "react"; +import { useParams } from "react-router-dom"; +import { + EuiPageTemplate, + EuiLoadingSpinner, + EuiButton, + EuiSpacer, + EuiConfirmModal, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiFieldSearch, + EuiPanel, + EuiSelect, + EuiText, + EuiButtonGroup, + EuiBadge, + EuiAccordion, + EuiIcon, + EuiToolTip, +} from "@elastic/eui"; +import { useMutation, useQuery, useQueryClient } from "react-query"; import { DatasetIcon } from "../../graphics/DatasetIcon"; - -import useLoadRegistry from "../../queries/useLoadRegistry"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; +import DatasetsCardGrid from "./DatasetsCardGrid"; import DatasetsListingTable from "./DatasetsListingTable"; +import DatasetCatalogBrowser from "./DatasetCatalogBrowser"; import DatasetsIndexEmptyState from "./DatasetsIndexEmptyState"; +import AddToCatalogModal from "./AddToCatalogModal"; +import type { RegisterDatasetPayload } from "./RegisterDatasetModal"; +import ExportButton from "../../components/ExportButton"; +import useResourceQuery, { + savedDatasetListPath, +} from "../../queries/useResourceQuery"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; +import { restPost, restDelete } from "../../queries/restApiClient"; const useLoadSavedDataSets = () => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); + const { projectName } = useParams(); + return useResourceQuery({ + resourceType: "saved-datasets-list", + project: projectName, + restPath: savedDatasetListPath(projectName), + restSelect: (d) => d.savedDatasets, + }); +}; - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.savedDatasets; +const SORT_OPTIONS = [ + { value: "name_asc", text: "Name (A → Z)" }, + { value: "name_desc", text: "Name (Z → A)" }, + { value: "created_desc", text: "Newest first" }, + { value: "created_asc", text: "Oldest first" }, + { value: "features_desc", text: "Most features" }, +]; - return { - ...registryQuery, - data, - }; -}; +const VIEW_TOGGLE_BUTTONS = [ + { id: "catalog", label: "Catalog", iconType: "folderClosed" }, + { id: "cards", label: "Cards", iconType: "grid" }, + { id: "table", label: "Table", iconType: "list" }, +]; + +function getDatasetSortValue(dataset: any, key: string): any { + const spec = dataset.spec || dataset; + const meta = dataset.meta || {}; + if (key === "name") return (spec.name || "").toLowerCase(); + if (key === "created") + return meta.createdTimestamp || meta.created_timestamp || ""; + if (key === "features") return (spec.features || []).length; + return ""; +} const Index = () => { - const { isLoading, isSuccess, isError, data } = useLoadSavedDataSets(); + const { projectName } = useParams(); + const { isLoading, isSuccess, isError, isPermissionDenied, data } = + useLoadSavedDataSets(); + const [showRegisterModal, setShowRegisterModal] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState("created_desc"); + const [viewMode, setViewMode] = useState("catalog"); + const [namespaceFilter, setNamespaceFilter] = useState("all"); + const [submitError, setSubmitError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + const queryClient = useQueryClient(); + + useDocumentTitle(`Data Catalog | Feast`); + + const registerMutation = useMutation( + (payload: RegisterDatasetPayload) => + restPost(registryUrl, "/saved_datasets", payload, fetchOptions), + { + onSuccess: (_: any, variables: RegisterDatasetPayload) => { + setShowRegisterModal(false); + setSubmitError(null); + setSuccessMessage( + `Dataset "${variables.name}" registered successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + }, + onError: (err: Error) => { + setSubmitError(err.message); + }, + }, + ); + + const deleteMutation = useMutation( + (name: string) => + restDelete( + registryUrl, + `/saved_datasets/${encodeURIComponent(name)}?project=${encodeURIComponent(projectName || "")}`, + fetchOptions, + ), + { + onSuccess: () => { + setDeleteTarget(null); + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + }, + }, + ); + + // Poll active jobs + const { data: jobsData } = useQuery( + ["dataset-jobs", projectName], + async () => { + const response = await fetch( + `${registryUrl}/saved_datasets/jobs?project=${encodeURIComponent(projectName || "")}`, + fetchOptions, + ); + if (!response.ok) return { jobs: [] }; + return response.json(); + }, + { + enabled: !!registryUrl, + refetchInterval: 5000, + staleTime: 3000, + }, + ); + + const activeJobs = useMemo( + () => + (jobsData?.jobs || []).filter( + (j: any) => j.status === "pending" || j.status === "running", + ), + [jobsData], + ); + + const recentJobs = useMemo( + () => (jobsData?.jobs || []).slice(0, 10), + [jobsData], + ); + + // Toast for completed jobs + useEffect(() => { + const completedJobs = (jobsData?.jobs || []).filter( + (j: any) => j.status === "completed", + ); + if (completedJobs.length > 0) { + const latest = completedJobs[0]; + if (latest.completed_at) { + const completedTime = new Date(latest.completed_at).getTime(); + const now = Date.now(); + if (now - completedTime < 10000) { + setSuccessMessage( + `Dataset "${latest.dataset_name}" created successfully.`, + ); + setTimeout(() => setSuccessMessage(null), 5000); + queryClient.invalidateQueries(["rest", "saved-datasets-list"]); + } + } + } + }, [jobsData, queryClient]); + + const handleRegisterSubmit = useCallback( + async (payload: RegisterDatasetPayload) => { + payload.project = projectName || ""; + await registerMutation.mutateAsync(payload); + }, + [projectName, registerMutation], + ); + + const handleDeleteConfirm = useCallback(() => { + if (deleteTarget) { + deleteMutation.mutate(deleteTarget); + } + }, [deleteTarget, deleteMutation]); + + // Compute summary stats + const stats = useMemo(() => { + if (!data) + return { + total: 0, + totalFeatures: 0, + storageTypes: new Set(), + namespaces: [] as string[], + }; + const totalFeatures = data.reduce( + (acc: number, ds: any) => acc + (ds.spec?.features?.length || 0), + 0, + ); + const storageTypes = new Set(); + const namespacesSet = new Set(); + data.forEach((ds: any) => { + const storage = ds.spec?.storage; + if (storage?.fileStorage) storageTypes.add("File"); + else if (storage?.bigqueryStorage) storageTypes.add("BigQuery"); + else if (storage?.snowflakeStorage) storageTypes.add("Snowflake"); + else if (storage?.redshiftStorage) storageTypes.add("Redshift"); + else if (storage?.sparkStorage) storageTypes.add("Spark"); + else if (storage?.trinoStorage) storageTypes.add("Trino"); + else if (storage?.athenaStorage) storageTypes.add("Athena"); + else if (storage?.customStorage) storageTypes.add("Custom"); + const ns = ds.spec?.namespace; + if (ns) namespacesSet.add(ns); + }); + const namespaces = Array.from(namespacesSet).sort(); + return { total: data.length, totalFeatures, storageTypes, namespaces }; + }, [data]); + + // Filter and sort + const processedData = useMemo(() => { + if (!data) return []; + let filtered = data; + + // Namespace filter + if (namespaceFilter !== "all") { + if (namespaceFilter === "_none") { + filtered = filtered.filter((ds: any) => !ds.spec?.namespace); + } else { + filtered = filtered.filter( + (ds: any) => ds.spec?.namespace === namespaceFilter, + ); + } + } + + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase(); + filtered = filtered.filter((ds: any) => { + const name = (ds.spec?.name || "").toLowerCase(); + const tags = JSON.stringify(ds.spec?.tags || {}).toLowerCase(); + const features = (ds.spec?.features || []).join(" ").toLowerCase(); + const service = ( + ds.spec?.featureServiceName || + ds.spec?.feature_service_name || + "" + ).toLowerCase(); + const ns = (ds.spec?.namespace || "").toLowerCase(); + const col = (ds.spec?.collection || "").toLowerCase(); + const desc = (ds.spec?.description || "").toLowerCase(); + return ( + name.includes(q) || + tags.includes(q) || + features.includes(q) || + service.includes(q) || + ns.includes(q) || + col.includes(q) || + desc.includes(q) + ); + }); + } - useDocumentTitle(`Saved Datasets | Feast`); + const [key, order] = sortBy.split("_"); + filtered = [...filtered].sort((a, b) => { + const aVal = getDatasetSortValue(a, key); + const bVal = getDatasetSortValue(b, key); + if (typeof aVal === "number" && typeof bVal === "number") { + return order === "asc" ? aVal - bVal : bVal - aVal; + } + const cmp = String(aVal).localeCompare(String(bVal)); + return order === "asc" ? cmp : -cmp; + }); + + return filtered; + }, [data, searchQuery, sortBy, namespaceFilter]); + + const hasData = data && data.length > 0; return ( + {activeJobs.length > 0 && ( + + + + {activeJobs.length} active + + + + )} + + { + setSubmitError(null); + setShowRegisterModal(true); + }} + > + Add to Catalog + + + , + , + ]} /> + {/* Success toast */} + {successMessage && ( + <> + + + + )} + + {/* Active Jobs Panel */} + {recentJobs.length > 0 && ( + <> + + + + + + + Recent Activity + {activeJobs.length > 0 && ( + + {activeJobs.length} running + + )} + + + + } + paddingSize="s" + initialIsOpen={activeJobs.length > 0} + > + + {recentJobs.map((job: any) => ( + + + {job.status === "running" || job.status === "pending" ? ( + + ) : job.status === "completed" ? ( + + ) : ( + + )} + + + + {job.dataset_name} + + + + + {job.status} + + + + ))} + + + + + )} + + {/* Delete error */} + {deleteMutation.isError && ( + <> + +

{(deleteMutation.error as Error)?.message}

+
+ + + )} + {isLoading && ( -

- Loading -

+ + + + + + Loading datasets... + + + )} + + {isPermissionDenied && ( + +

You do not have permission to view saved datasets.

+
+ )} + {isError && !isPermissionDenied && ( + +

+ We encountered an error while loading datasets. Please check that + the registry server is running. +

+
+ )} + + {isSuccess && hasData && ( + <> + {/* View mode toggle — always visible */} + + + + + + {stats.total} datasets + {stats.namespaces.length > 0 && ( + <> + {" "} + across {stats.namespaces.length}{" "} + namespaces + + )} + + + + + + setViewMode(id)} + isIconOnly + buttonSize="compressed" + /> + + + + + + {/* Search + Namespace Filter + Sort — shared toolbar */} + + + setSearchQuery(e.target.value)} + isClearable + fullWidth + /> + + {stats.namespaces.length > 0 && ( + + ({ + value: ns, + text: ns, + })), + ]} + value={namespaceFilter} + onChange={(e) => setNamespaceFilter(e.target.value)} + compressed + prepend="Namespace" + /> + + )} + + setSortBy(e.target.value)} + compressed + prepend="Sort" + /> + + + + + + {/* Results count when filtering */} + {(searchQuery.trim() || namespaceFilter !== "all") && ( + <> + + Showing {processedData.length} of {data.length} datasets + {namespaceFilter !== "all" && namespaceFilter !== "_none" && ( + <> + {" "} + in namespace {namespaceFilter} + + )} + {namespaceFilter === "_none" && <> with no namespace} + + + + )} + + {/* Catalog (hierarchical) view */} + {viewMode === "catalog" && ( + setDeleteTarget(name)} + /> + )} + + {/* Flat views (cards / table) */} + {viewMode === "cards" && ( + setDeleteTarget(name)} + /> + )} + {viewMode === "table" && ( + + )} + + )} + + {isSuccess && !hasData && ( + { + setSubmitError(null); + setShowRegisterModal(true); + }} + /> )} - {isError &&

We encountered an error while loading.

} - {isSuccess && data && } - {isSuccess && !data && }
+ + {showRegisterModal && ( + setShowRegisterModal(false)} + onLinkSubmit={handleRegisterSubmit} + isLinkSubmitting={registerMutation.isLoading} + linkError={submitError} + /> + )} + + {deleteTarget && ( + setDeleteTarget(null)} + onConfirm={handleDeleteConfirm} + cancelButtonText="Cancel" + confirmButtonText="Delete" + buttonColor="danger" + isLoading={deleteMutation.isLoading} + > +

+ Are you sure you want to delete {deleteTarget}? +

+

+ + This removes the dataset metadata from the registry. The + underlying data at the storage location will not be deleted. + +

+
+ )}
); }; diff --git a/ui/src/pages/saved-data-sets/JobStatusPanel.tsx b/ui/src/pages/saved-data-sets/JobStatusPanel.tsx new file mode 100644 index 00000000000..bf6ed49fe06 --- /dev/null +++ b/ui/src/pages/saved-data-sets/JobStatusPanel.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useState, useContext, useCallback } from "react"; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiText, + EuiCallOut, + EuiButton, + EuiButtonEmpty, + EuiSpacer, + EuiProgress, +} from "@elastic/eui"; +import { useNavigate, useParams } from "react-router-dom"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import { useDataMode } from "../../contexts/DataModeContext"; + +interface JobStatusPanelProps { + jobId: string; + datasetName: string; + onComplete?: () => void; + onClose: () => void; + onRetry?: () => void; +} + +interface JobStatus { + job_id: string; + status: "pending" | "running" | "completed" | "failed"; + dataset_name?: string; + error?: string; + created_at?: string; + completed_at?: string; +} + +const JobStatusPanel = ({ + jobId, + datasetName, + onComplete, + onClose, + onRetry, +}: JobStatusPanelProps) => { + const { projectName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + const navigate = useNavigate(); + + const [status, setStatus] = useState({ + job_id: jobId, + status: "pending", + dataset_name: datasetName, + }); + const [pollError, setPollError] = useState(null); + + const pollStatus = useCallback(async () => { + try { + const response = await fetch( + `${registryUrl}/saved_datasets/jobs/${encodeURIComponent(jobId)}`, + { method: "GET", ...fetchOptions }, + ); + if (!response.ok) { + const err = await response + .json() + .catch(() => ({ detail: "Unknown error" })); + throw new Error( + err.detail || `Status check failed: ${response.status}`, + ); + } + const data: JobStatus = await response.json(); + setStatus(data); + + if (data.status === "completed" && onComplete) { + onComplete(); + } + } catch (err: any) { + setPollError(err.message); + } + }, [jobId, registryUrl, fetchOptions, onComplete]); + + useEffect(() => { + const interval = setInterval(() => { + if (status.status === "pending" || status.status === "running") { + pollStatus(); + } + }, 3000); + + pollStatus(); + + return () => clearInterval(interval); + }, [pollStatus, status.status]); + + const isRunning = status.status === "pending" || status.status === "running"; + const isCompleted = status.status === "completed"; + const isFailed = status.status === "failed"; + + return ( + + {isRunning && ( + <> + + + + + + +

Creating dataset: {datasetName}

+

+ + Running feature retrieval and persisting results... + +

+
+
+
+ + + + + Job ID: {jobId} | Status: {status.status} + + + + Close (job continues in background) + + + )} + + {isCompleted && ( + <> + +

+ {datasetName} has been created and registered in + the catalog. +

+
+ + + + { + onClose(); + navigate(`/p/${projectName}/data-set/${datasetName}`); + }} + > + View Dataset + + + + Close + + + + )} + + {isFailed && ( + <> + +

+ {status.error || + "An unknown error occurred during dataset creation."} +

+
+ + + Job ID: {jobId} + + + + {onRetry && ( + + + Back to Form + + + )} + + Close + + + + )} + + {pollError && ( + <> + + +

{pollError}

+
+ + )} +
+ ); +}; + +export default JobStatusPanel; diff --git a/ui/src/pages/saved-data-sets/RegisterDatasetModal.tsx b/ui/src/pages/saved-data-sets/RegisterDatasetModal.tsx new file mode 100644 index 00000000000..e7543ddadf1 --- /dev/null +++ b/ui/src/pages/saved-data-sets/RegisterDatasetModal.tsx @@ -0,0 +1,702 @@ +import React, { useState, useMemo } from "react"; +import { + EuiFormRow, + EuiFieldText, + EuiSpacer, + EuiHorizontalRule, + EuiText, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiComboBox, + EuiComboBoxOptionOption, + EuiCheckbox, + EuiSuperSelect, + EuiSuperSelectOption, + EuiButton, + EuiButtonEmpty, +} from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import FormModal from "../../components/forms/FormModal"; +import TagsEditor, { TagEntry } from "../../components/forms/TagsEditor"; +import useResourceQuery, { + featureServiceListPath, + featureViewListPath, + dataSourceListPath, +} from "../../queries/useResourceQuery"; + +export interface RegisterDatasetPayload { + name: string; + project: string; + features: string[]; + join_keys: string[]; + storage_path: string; + storage_type: string; + storage_file_format?: string; + tags: Record; + full_feature_names: boolean; + feature_service_name?: string; + namespace?: string; + collection?: string; + description?: string; +} + +interface RegisterDatasetModalProps { + onClose: () => void; + onSubmit: (data: RegisterDatasetPayload) => Promise; + isSubmitting: boolean; + error?: string | null; + embedded?: boolean; +} + +interface StorageTypeDefinition { + value: string; + label: string; + description: string; + placeholder: string; + helpText: string; + sourceTypeMatch: string[]; +} + +const ALL_STORAGE_TYPES: StorageTypeDefinition[] = [ + { + value: "file", + label: "File (Parquet / CSV)", + description: "Local or remote file path (S3, GCS, HDFS)", + placeholder: "s3://my-bucket/datasets/training_v1.parquet", + helpText: + "Path to the data file accessible by the Feast server (e.g. s3://bucket/path/data.parquet, gs://bucket/data.csv).", + sourceTypeMatch: ["BATCH_FILE"], + }, + { + value: "bigquery", + label: "BigQuery", + description: "Google BigQuery table reference", + placeholder: "project_id.dataset.table_name", + helpText: "Full BigQuery table reference: project:dataset.table", + sourceTypeMatch: ["BATCH_BIGQUERY"], + }, + { + value: "snowflake", + label: "Snowflake", + description: "Snowflake table reference", + placeholder: "database.schema.table_name", + helpText: "Snowflake table: database.schema.table", + sourceTypeMatch: ["BATCH_SNOWFLAKE"], + }, + { + value: "redshift", + label: "Redshift", + description: "Amazon Redshift table reference", + placeholder: "schema.table_name", + helpText: "Redshift table: schema.table", + sourceTypeMatch: ["BATCH_REDSHIFT"], + }, + { + value: "spark", + label: "Spark", + description: "Apache Spark table or path", + placeholder: "s3://bucket/path/ or catalog.database.table", + helpText: + "Spark path or catalog table reference (the data will be read via Spark).", + sourceTypeMatch: ["BATCH_SPARK"], + }, + { + value: "trino", + label: "Trino", + description: "Trino table reference", + placeholder: "catalog.schema.table", + helpText: "Trino table: catalog.schema.table", + sourceTypeMatch: ["BATCH_TRINO"], + }, + { + value: "athena", + label: "AWS Athena", + description: "AWS Athena table reference", + placeholder: "database.table_name", + helpText: "Athena table reference. Data is queried via Athena.", + sourceTypeMatch: ["BATCH_ATHENA"], + }, + { + value: "postgres", + label: "PostgreSQL", + description: "PostgreSQL table reference", + placeholder: "schema.table_name", + helpText: + "PostgreSQL table reference. Data is read via the PostgreSQL offline store.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "clickhouse", + label: "ClickHouse", + description: "ClickHouse table reference", + placeholder: "database.table_name", + helpText: + "ClickHouse table reference. Data is read via the ClickHouse offline store.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "couchbase", + label: "Couchbase Columnar", + description: "Couchbase Columnar collection reference", + placeholder: "database.scope.collection", + helpText: + "Couchbase Columnar reference in format: database.scope.collection", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, + { + value: "custom", + label: "Custom", + description: "Custom storage configuration", + placeholder: '{"class": "my.CustomStorage", "config": {}}', + helpText: "Serialized configuration for a custom storage implementation.", + sourceTypeMatch: ["CUSTOM_SOURCE"], + }, +]; + +function detectDataSourceTypes(dataSources: any[]): Set { + const types = new Set(); + for (const ds of dataSources) { + const dsType = ds.spec?.type || ds.type; + if (dsType != null) { + const typeName = dataSourceTypeToName(dsType); + if (typeName) types.add(typeName); + } + if (ds.spec?.fileOptions || ds.fileOptions) types.add("BATCH_FILE"); + if (ds.spec?.bigqueryOptions || ds.bigqueryOptions) + types.add("BATCH_BIGQUERY"); + if (ds.spec?.snowflakeOptions || ds.snowflakeOptions) + types.add("BATCH_SNOWFLAKE"); + if (ds.spec?.redshiftOptions || ds.redshiftOptions) + types.add("BATCH_REDSHIFT"); + if (ds.spec?.sparkOptions || ds.sparkOptions) types.add("BATCH_SPARK"); + if (ds.spec?.trinoOptions || ds.trinoOptions) types.add("BATCH_TRINO"); + if (ds.spec?.athenaOptions || ds.athenaOptions) types.add("BATCH_ATHENA"); + if (ds.spec?.customOptions || ds.customOptions) types.add("CUSTOM_SOURCE"); + const classType = + ds.spec?.dataSourceClassType || ds.dataSourceClassType || ""; + if (classType.includes("postgres")) types.add("CUSTOM_SOURCE"); + if (classType.includes("clickhouse")) types.add("CUSTOM_SOURCE"); + if (classType.includes("couchbase")) types.add("CUSTOM_SOURCE"); + } + return types; +} + +function dataSourceTypeToName(typeNum: number | string): string | null { + const map: Record = { + "1": "BATCH_FILE", + "2": "BATCH_BIGQUERY", + "3": "BATCH_REDSHIFT", + "5": "BATCH_SNOWFLAKE", + "7": "BATCH_SPARK", + "8": "BATCH_TRINO", + "9": "BATCH_ATHENA", + "6": "STREAM_KAFKA", + "10": "STREAM_KINESIS", + "4": "REQUEST_SOURCE", + "12": "PUSH_SOURCE", + "11": "CUSTOM_SOURCE", + }; + return map[String(typeNum)] || null; +} + +const RegisterDatasetModal = ({ + onClose, + onSubmit, + isSubmitting, + error, + embedded = false, +}: RegisterDatasetModalProps) => { + const { projectName } = useParams(); + + const { data: featureServicesRaw } = useResourceQuery({ + resourceType: "register-modal-fs", + project: projectName, + restPath: featureServiceListPath(projectName), + restSelect: (d) => d.featureServices || [], + }); + + const { data: featureViewsRaw } = useResourceQuery({ + resourceType: "register-modal-fv", + project: projectName, + restPath: featureViewListPath(projectName), + restSelect: (d) => d.featureViews || [], + }); + + const { data: dataSourcesRaw } = useResourceQuery({ + resourceType: "register-modal-ds", + project: projectName, + restPath: dataSourceListPath(projectName), + restSelect: (d) => d.dataSources || [], + }); + + // Derive available storage types from project's data sources + const availableStorageOptions: EuiSuperSelectOption[] = + useMemo(() => { + if (!dataSourcesRaw || dataSourcesRaw.length === 0) { + // Fallback: show all storage types if no data sources loaded yet + return ALL_STORAGE_TYPES.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: ( + <> + {st.label} + +

{st.description}

+
+ + ), + })); + } + + const detectedTypes = detectDataSourceTypes(dataSourcesRaw); + + const matched = ALL_STORAGE_TYPES.filter((st) => + st.sourceTypeMatch.some((match) => detectedTypes.has(match)), + ); + + // Always include File as a fallback (datasets can be stored as standalone files) + const hasFile = matched.some((st) => st.value === "file"); + const result = hasFile ? matched : [ALL_STORAGE_TYPES[0], ...matched]; + + return result.map((st) => ({ + value: st.value, + inputDisplay: st.label, + dropdownDisplay: ( + <> + {st.label} + +

{st.description}

+
+ + ), + })); + }, [dataSourcesRaw]); + + // Form state + const [name, setName] = useState(""); + const [namespace, setNamespace] = useState(""); + const [collection, setCollection] = useState(""); + const [description, setDescription] = useState(""); + const [storagePath, setStoragePath] = useState(""); + const [storageType, setStorageType] = useState("file"); + const [storageFileFormat, setStorageFileFormat] = useState("parquet"); + const [featuresInput, setFeaturesInput] = useState( + [], + ); + const [joinKeysInput, setJoinKeysInput] = useState( + [], + ); + const [tags, setTags] = useState([]); + const [featureServiceName, setFeatureServiceName] = useState(""); + const [fullFeatureNames, setFullFeatureNames] = useState(false); + const [errors, setErrors] = useState>({}); + const [submitted, setSubmitted] = useState(false); + + // Get current storage type config + const currentStorageConfig = + ALL_STORAGE_TYPES.find((st) => st.value === storageType) || + ALL_STORAGE_TYPES[0]; + + // Build suggestions from live data + const featureOptions: EuiComboBoxOptionOption[] = (featureViewsRaw || []) + .filter((fv: any) => fv.type !== "labelView") + .flatMap((fv: any) => { + const fvName = fv.spec?.name || ""; + const features = fv.spec?.features || []; + return features.map((f: any) => ({ + label: `${fvName}:${f.name || f}`, + })); + }); + + const joinKeyOptions: EuiComboBoxOptionOption[] = (() => { + const seen = new Set(); + (featureViewsRaw || []).forEach((fv: any) => { + const entities = fv.spec?.entities || []; + entities.forEach((e: string) => { + if (!seen.has(e)) seen.add(e); + }); + }); + return Array.from(seen).map((k) => ({ label: k })); + })(); + + const featureServiceOptions: EuiComboBoxOptionOption[] = ( + featureServicesRaw || [] + ).map((fs: any) => ({ + label: fs.spec?.name || fs.name || "", + })); + + const validate = (): boolean => { + const newErrors: Record = {}; + + if (!name.trim()) { + newErrors.name = "Dataset name is required."; + } else if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(name.trim())) { + newErrors.name = + "Must start with a letter or underscore, and contain only letters, numbers, underscores, and hyphens."; + } + + if (!storagePath.trim()) { + newErrors.storagePath = + "Storage path is required. Provide a file path or table reference."; + } else if (storageType === "file") { + if ( + !/^(s3|gs|gcs|hdfs|abfs|file):\/\/\S+$/.test(storagePath.trim()) && + !storagePath.trim().startsWith("/") + ) { + newErrors.storagePath = + "Should be a valid URI (s3://, gs://, hdfs://, file://) or absolute path."; + } + } + + const tagKeys = tags.map((t) => t.key).filter((k) => k.trim()); + if (new Set(tagKeys).size !== tagKeys.length) { + newErrors.tags = "Tag keys must be unique."; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + setSubmitted(true); + if (!validate()) return; + + const tagsObj: Record = {}; + tags.forEach(({ key, value }) => { + if (key.trim() && value.trim()) tagsObj[key.trim()] = value.trim(); + }); + + const payload: RegisterDatasetPayload = { + name: name.trim(), + project: projectName || "", + features: featuresInput.map((o) => o.label), + join_keys: joinKeysInput.map((o) => o.label), + storage_path: storagePath.trim(), + storage_type: storageType, + storage_file_format: + storageType === "spark" ? storageFileFormat : undefined, + tags: tagsObj, + full_feature_names: fullFeatureNames, + feature_service_name: featureServiceName || undefined, + namespace: namespace.trim() || undefined, + collection: collection.trim() || undefined, + description: description.trim() || undefined, + }; + await onSubmit(payload); + }; + + const clearFieldError = (field: string) => { + if (submitted) { + setErrors((prev) => { + const next = { ...prev }; + delete next[field]; + return next; + }); + } + }; + + const formContent = ( + <> + {error && ( + <> + +

{error}

+
+ + + )} + + {/* Section: Identity */} + +

Identity

+
+ + + + + + { + setName(e.target.value); + clearFieldError("name"); + }} + isInvalid={!!errors.name} + placeholder="e.g. driver_training_v1" + autoFocus + /> + + + + + setDescription(e.target.value)} + placeholder="e.g. Training data for driver fraud model" + /> + + + + + + setFeatureServiceName( + selected.length > 0 ? selected[0].label : "", + ) + } + onCreateOption={(val) => setFeatureServiceName(val)} + placeholder="Select or type..." + isClearable + /> + + + + + + + {/* Section: Organization */} + +

Organization (optional)

+
+ + + + + Group datasets into namespaces and collections for hierarchical + organization. Leave empty to keep the dataset at the top level. + + + + + + + + setNamespace(e.target.value)} + placeholder="e.g. fraud" + /> + + + + + setCollection(e.target.value)} + placeholder="e.g. training" + /> + + + + + + + + {/* Section: Storage */} + +

Storage Location

+
+ + + + + Point to where the dataset data already exists. Only storage types + matching your project's configured data sources are shown. + + + + + + setStorageType(value)} + fullWidth + /> + + + + { + setStoragePath(e.target.value); + clearFieldError("storagePath"); + }} + isInvalid={!!errors.storagePath} + placeholder={currentStorageConfig.placeholder} + icon={storageType === "file" ? "document" : "storage"} + fullWidth + /> + + + {storageType === "spark" && ( + + Parquet, + }, + { + value: "avro", + inputDisplay: "Avro", + dropdownDisplay: Avro, + }, + { + value: "csv", + inputDisplay: "CSV", + dropdownDisplay: CSV, + }, + { + value: "json", + inputDisplay: "JSON", + dropdownDisplay: JSON, + }, + ]} + valueOfSelected={storageFileFormat} + onChange={setStorageFileFormat} + fullWidth + /> + + )} + + + + + {/* Section: Schema */} + +

Schema

+
+ + + + { + setFeaturesInput([...featuresInput, { label: val }]); + }} + onChange={(selected) => setFeaturesInput(selected)} + placeholder="Search or type features..." + isClearable + fullWidth + /> + + + + { + setJoinKeysInput([...joinKeysInput, { label: val }]); + }} + onChange={(selected) => setJoinKeysInput(selected)} + placeholder="Search or type join keys..." + isClearable + fullWidth + /> + + + + setFullFeatureNames(e.target.checked)} + /> + + {/* Section: Tags */} + + setTags(newTags)} + error={errors.tags} + /> + + ); + + if (embedded) { + return ( +
+ {formContent} + + + + Cancel + + + + Link Existing Dataset + + + +
+ ); + } + + return ( + + {formContent} + + ); +}; + +export default RegisterDatasetModal; +export type { RegisterDatasetModalProps }; diff --git a/ui/src/pages/saved-data-sets/useLoadDataset.ts b/ui/src/pages/saved-data-sets/useLoadDataset.ts index 40f8a8ebd48..c560f7f9eb6 100644 --- a/ui/src/pages/saved-data-sets/useLoadDataset.ts +++ b/ui/src/pages/saved-data-sets/useLoadDataset.ts @@ -1,22 +1,18 @@ -import { useContext } from "react"; -import RegistryPathContext from "../../contexts/RegistryPathContext"; -import useLoadRegistry from "../../queries/useLoadRegistry"; +import { useParams } from "react-router-dom"; +import useResourceQuery, { + savedDatasetDetailPath, +} from "../../queries/useResourceQuery"; -const useLoadEntity = (entityName: string) => { - const registryUrl = useContext(RegistryPathContext); - const registryQuery = useLoadRegistry(registryUrl); +const useLoadDataset = (datasetName: string) => { + const { projectName } = useParams(); - const data = - registryQuery.data === undefined - ? undefined - : registryQuery.data.objects.savedDatasets?.find( - (fv) => fv.spec?.name === entityName, - ); - - return { - ...registryQuery, - data, - }; + return useResourceQuery({ + resourceType: `saved-dataset:${datasetName}`, + project: projectName, + restPath: savedDatasetDetailPath(datasetName, projectName || ""), + restSelect: (d) => d, + enabled: !!datasetName, + }); }; -export default useLoadEntity; +export default useLoadDataset; diff --git a/ui/src/parsers/mergedFVTypes.ts b/ui/src/parsers/mergedFVTypes.ts index 1c6b759be1e..0c65dea54f6 100644 --- a/ui/src/parsers/mergedFVTypes.ts +++ b/ui/src/parsers/mergedFVTypes.ts @@ -4,6 +4,7 @@ enum FEAST_FV_TYPES { regular = "regular", ondemand = "ondemand", stream = "stream", + label = "label", } interface regularFVInterface { @@ -27,7 +28,18 @@ interface SFVInterface { object: feast.core.IStreamFeatureView; } -type genericFVType = regularFVInterface | ODFVInterface | SFVInterface; +interface LabelViewInterface { + name: string; + type: FEAST_FV_TYPES.label; + features: feast.core.IFeatureSpecV2[]; + object: any; +} + +type genericFVType = + | regularFVInterface + | ODFVInterface + | SFVInterface + | LabelViewInterface; const mergedFVTypes = (objects: feast.core.Registry) => { const mergedFVMap: Record = {}; @@ -75,4 +87,10 @@ const mergedFVTypes = (objects: feast.core.Registry) => { export default mergedFVTypes; export { FEAST_FV_TYPES }; -export type { genericFVType, regularFVInterface, ODFVInterface, SFVInterface }; +export type { + genericFVType, + regularFVInterface, + ODFVInterface, + SFVInterface, + LabelViewInterface, +}; diff --git a/ui/src/parsers/parseEntityRelationships.ts b/ui/src/parsers/parseEntityRelationships.ts index 579374e30ff..b1a014044a0 100644 --- a/ui/src/parsers/parseEntityRelationships.ts +++ b/ui/src/parsers/parseEntityRelationships.ts @@ -14,12 +14,21 @@ interface EntityRelation { const parseEntityRelationships = (objects: feast.core.Registry) => { const links: EntityRelation[] = []; + const labelViewNames = new Set( + ((objects as any).labelViews || []).map((lv: any) => lv.spec?.name), + ); + objects.featureServices?.forEach((fs) => { - fs.spec?.features!.forEach((feature) => { + fs.spec?.features!.forEach((feature: any) => { + const viewName = feature?.featureViewName!; + const isLabelView = + feature?.viewType === "labelView" || labelViewNames.has(viewName); links.push({ source: { - type: FEAST_FCO_TYPES["featureView"], - name: feature?.featureViewName!, + type: isLabelView + ? FEAST_FCO_TYPES["labelView"] + : FEAST_FCO_TYPES["featureView"], + name: viewName, }, target: { type: FEAST_FCO_TYPES["featureService"], @@ -134,6 +143,58 @@ const parseEntityRelationships = (objects: feast.core.Registry) => { }); }); + (objects as any).labelViews?.forEach((lv: any) => { + lv.spec?.entities?.forEach((ent: string) => { + links.push({ + source: { + type: FEAST_FCO_TYPES["entity"], + name: ent, + }, + target: { + type: FEAST_FCO_TYPES["labelView"], + name: lv.spec?.name!, + }, + }); + }); + + if (lv.spec?.source?.name) { + links.push({ + source: { + type: FEAST_FCO_TYPES["dataSource"], + name: lv.spec.source.name, + }, + target: { + type: FEAST_FCO_TYPES["labelView"], + name: lv.spec?.name!, + }, + }); + } + if (lv.spec?.source?.batchSource?.name) { + links.push({ + source: { + type: FEAST_FCO_TYPES["dataSource"], + name: lv.spec.source.batchSource.name, + }, + target: { + type: FEAST_FCO_TYPES["labelView"], + name: lv.spec?.name!, + }, + }); + } + if (lv.spec?.batchSource?.name) { + links.push({ + source: { + type: FEAST_FCO_TYPES["dataSource"], + name: lv.spec.batchSource.name, + }, + target: { + type: FEAST_FCO_TYPES["labelView"], + name: lv.spec?.name!, + }, + }); + } + }); + return links; }; diff --git a/ui/src/parsers/types.ts b/ui/src/parsers/types.ts index 1e515f23f34..fc9f88d045f 100644 --- a/ui/src/parsers/types.ts +++ b/ui/src/parsers/types.ts @@ -3,6 +3,11 @@ enum FEAST_FCO_TYPES { entity = "entity", featureView = "featureView", featureService = "featureService", + labelView = "labelView", + mlflowRun = "mlflowRun", + mlflowModel = "mlflowModel", + openlineageJob = "openlineageJob", + openlineageDataset = "openlineageDataset", } export { FEAST_FCO_TYPES }; diff --git a/ui/src/queries/mutations/useDataSourceMutations.ts b/ui/src/queries/mutations/useDataSourceMutations.ts new file mode 100644 index 00000000000..a59313bf7a7 --- /dev/null +++ b/ui/src/queries/mutations/useDataSourceMutations.ts @@ -0,0 +1,105 @@ +import { useMutation, useQueryClient } from "react-query"; + +interface ApplyDataSourcePayload { + name: string; + project: string; + type?: number; + timestamp_field?: string; + created_timestamp_column?: string; + description?: string; + tags?: Record; + owner?: string; + file_options?: { uri: string }; + bigquery_options?: { table: string; query: string }; + snowflake_options?: { table: string; database: string; schema_: string }; + redshift_options?: { table: string; database: string; schema_: string }; + kafka_options?: { kafka_bootstrap_servers: string; topic: string }; + spark_options?: { table: string; path: string }; + custom_options?: { + configuration?: string; + class_name?: string; + config?: string; + }; + data_source_class_type?: string; +} + +interface DeleteDataSourcePayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const applyDataSource = async ( + payload: ApplyDataSourcePayload, +): Promise => { + const response = await fetch(`${API_BASE}/data_sources`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to apply data source: ${response.status}`, + ); + } + + return response.json(); +}; + +const deleteDataSource = async ( + payload: DeleteDataSourcePayload, +): Promise => { + const response = await fetch( + `${API_BASE}/data_sources/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + { method: "DELETE" }, + ); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to delete data source: ${response.status}`, + ); + } + + return response.json(); +}; + +const useApplyDataSource = () => { + const queryClient = useQueryClient(); + + return useMutation(applyDataSource, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["data-sources-rest"]); + queryClient.invalidateQueries(["data-source-rest"]); + }, + }); +}; + +const useDeleteDataSource = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteDataSource, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["data-sources-rest"]); + queryClient.invalidateQueries(["data-source-rest"]); + }, + }); +}; + +export { useApplyDataSource, useDeleteDataSource }; +export type { ApplyDataSourcePayload, DeleteDataSourcePayload }; diff --git a/ui/src/queries/mutations/useEntityMutations.ts b/ui/src/queries/mutations/useEntityMutations.ts new file mode 100644 index 00000000000..659c367558e --- /dev/null +++ b/ui/src/queries/mutations/useEntityMutations.ts @@ -0,0 +1,92 @@ +import { useMutation, useQueryClient } from "react-query"; + +interface ApplyEntityPayload { + name: string; + project: string; + join_key?: string; + value_type?: number; + description?: string; + tags?: Record; + owner?: string; +} + +interface DeleteEntityPayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const applyEntity = async ( + payload: ApplyEntityPayload, +): Promise => { + const response = await fetch(`${API_BASE}/entities`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to apply entity: ${response.status}`, + ); + } + + return response.json(); +}; + +const deleteEntity = async ( + payload: DeleteEntityPayload, +): Promise => { + const response = await fetch( + `${API_BASE}/entities/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + { method: "DELETE" }, + ); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to delete entity: ${response.status}`, + ); + } + + return response.json(); +}; + +const useApplyEntity = () => { + const queryClient = useQueryClient(); + + return useMutation(applyEntity, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["entities-rest"]); + queryClient.invalidateQueries(["entity-rest"]); + }, + }); +}; + +const useDeleteEntity = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteEntity, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["entities-rest"]); + queryClient.invalidateQueries(["entity-rest"]); + }, + }); +}; + +export { useApplyEntity, useDeleteEntity }; +export type { ApplyEntityPayload, DeleteEntityPayload }; diff --git a/ui/src/queries/mutations/useFeatureServiceMutations.ts b/ui/src/queries/mutations/useFeatureServiceMutations.ts new file mode 100644 index 00000000000..85bd3f4fd44 --- /dev/null +++ b/ui/src/queries/mutations/useFeatureServiceMutations.ts @@ -0,0 +1,100 @@ +import { useMutation, useQueryClient } from "react-query"; + +interface FeatureViewProjectionPayload { + feature_view_name: string; + feature_names?: string[]; +} + +interface ApplyFeatureServicePayload { + name: string; + project: string; + features: FeatureViewProjectionPayload[]; + description?: string; + tags?: Record; + owner?: string; +} + +interface DeleteFeatureServicePayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const applyFeatureService = async ( + payload: ApplyFeatureServicePayload, +): Promise => { + const response = await fetch(`${API_BASE}/feature_services`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to apply feature service: ${response.status}`, + ); + } + + return response.json(); +}; + +const deleteFeatureService = async ( + payload: DeleteFeatureServicePayload, +): Promise => { + const response = await fetch( + `${API_BASE}/feature_services/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + { method: "DELETE" }, + ); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to delete feature service: ${response.status}`, + ); + } + + return response.json(); +}; + +const useApplyFeatureService = () => { + const queryClient = useQueryClient(); + + return useMutation(applyFeatureService, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["feature-services-rest"]); + queryClient.invalidateQueries(["feature-service-rest"]); + }, + }); +}; + +const useDeleteFeatureService = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteFeatureService, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["feature-services-rest"]); + queryClient.invalidateQueries(["feature-service-rest"]); + }, + }); +}; + +export { useApplyFeatureService, useDeleteFeatureService }; +export type { + ApplyFeatureServicePayload, + DeleteFeatureServicePayload, + FeatureViewProjectionPayload, +}; diff --git a/ui/src/queries/mutations/useFeatureViewMutations.ts b/ui/src/queries/mutations/useFeatureViewMutations.ts new file mode 100644 index 00000000000..6ca31208093 --- /dev/null +++ b/ui/src/queries/mutations/useFeatureViewMutations.ts @@ -0,0 +1,101 @@ +import { useMutation, useQueryClient } from "react-query"; + +interface FeaturePayload { + name: string; + value_type: number; + description?: string; +} + +interface ApplyFeatureViewPayload { + name: string; + project: string; + entities?: string[]; + features?: FeaturePayload[]; + batch_source?: string; + ttl_seconds?: number; + online?: boolean; + description?: string; + tags?: Record; + owner?: string; +} + +interface DeleteFeatureViewPayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const applyFeatureView = async ( + payload: ApplyFeatureViewPayload, +): Promise => { + const response = await fetch(`${API_BASE}/feature_views`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to apply feature view: ${response.status}`, + ); + } + + return response.json(); +}; + +const deleteFeatureView = async ( + payload: DeleteFeatureViewPayload, +): Promise => { + const response = await fetch( + `${API_BASE}/feature_views/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + { method: "DELETE" }, + ); + + if (!response.ok) { + const error = await response + .json() + .catch(() => ({ detail: response.statusText })); + throw new Error( + error.detail || `Failed to delete feature view: ${response.status}`, + ); + } + + return response.json(); +}; + +const useApplyFeatureView = () => { + const queryClient = useQueryClient(); + + return useMutation(applyFeatureView, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["feature-views-rest"]); + queryClient.invalidateQueries(["feature-view-rest"]); + }, + }); +}; + +const useDeleteFeatureView = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteFeatureView, { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["feature-views-rest"]); + queryClient.invalidateQueries(["feature-view-rest"]); + }, + }); +}; + +export { useApplyFeatureView, useDeleteFeatureView }; +export type { ApplyFeatureViewPayload, DeleteFeatureViewPayload }; diff --git a/ui/src/queries/mutations/usePermissionMutations.ts b/ui/src/queries/mutations/usePermissionMutations.ts new file mode 100644 index 00000000000..aa3961c8ff9 --- /dev/null +++ b/ui/src/queries/mutations/usePermissionMutations.ts @@ -0,0 +1,74 @@ +import { useMutation, useQueryClient } from "react-query"; +import { restPost, restDelete } from "../restApiClient"; + +interface PolicyPayload { + role_based_policy?: { roles: string[] }; + group_based_policy?: { groups: string[] }; + namespace_based_policy?: { namespaces: string[] }; + combined_group_namespace_policy?: { + groups: string[]; + namespaces: string[]; + }; +} + +interface ApplyPermissionPayload { + name: string; + project: string; + types: string[]; + name_patterns: string[]; + actions: string[]; + policy: PolicyPayload; + tags?: Record; + required_tags?: Record; +} + +interface DeletePermissionPayload { + name: string; + project: string; +} + +interface MutationResult { + name: string; + project: string; + status: string; +} + +const API_BASE = "/api/v1"; + +const useApplyPermission = () => { + const queryClient = useQueryClient(); + + return useMutation( + (payload: ApplyPermissionPayload) => + restPost(API_BASE, "/permissions", payload), + { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["permissions-rest"]); + queryClient.invalidateQueries(["registry-rest-bulk"]); + }, + }, + ); +}; + +const useDeletePermission = () => { + const queryClient = useQueryClient(); + + return useMutation( + (payload: DeletePermissionPayload) => + restDelete( + API_BASE, + `/permissions/${encodeURIComponent(payload.name)}?project=${encodeURIComponent(payload.project)}`, + ), + { + onSuccess: () => { + queryClient.invalidateQueries(["rest"]); + queryClient.invalidateQueries(["permissions-rest"]); + queryClient.invalidateQueries(["registry-rest-bulk"]); + }, + }, + ); +}; + +export { useApplyPermission, useDeletePermission }; +export type { ApplyPermissionPayload, DeletePermissionPayload, PolicyPayload }; diff --git a/ui/src/queries/restApi.ts b/ui/src/queries/restApi.ts new file mode 100644 index 00000000000..f3734962a00 --- /dev/null +++ b/ui/src/queries/restApi.ts @@ -0,0 +1,19 @@ +const API_BASE = "/api/v1"; + +export async function fetchApi( + path: string, + params?: Record, +): Promise { + const url = new URL(`${API_BASE}${path}`, window.location.origin); + if (params) { + Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); + } + const res = await fetch(url.toString(), { + headers: { "Content-Type": "application/json" }, + }); + if (!res.ok) { + const body = await res.json().catch(() => ({ detail: res.statusText })); + throw new Error(body.detail || `API error: ${res.status}`); + } + return res.json(); +} diff --git a/ui/src/queries/restApiClient.ts b/ui/src/queries/restApiClient.ts new file mode 100644 index 00000000000..c253020eed9 --- /dev/null +++ b/ui/src/queries/restApiClient.ts @@ -0,0 +1,93 @@ +import type { FetchOptions } from "../contexts/DataModeContext"; + +class RestApiError extends Error { + status: number; + constructor(message: string, status: number) { + super(message); + this.name = "RestApiError"; + this.status = status; + } +} + +const restFetch = async ( + baseUrl: string, + path: string, + fetchOptions?: FetchOptions, +): Promise => { + const url = `${baseUrl}${path}`; + const headers: Record = { + Accept: "application/json", + ...fetchOptions?.headers, + }; + + const res = await fetch(url, { + method: "GET", + headers, + credentials: fetchOptions?.credentials, + }); + + if (!res.ok) { + throw new RestApiError( + `REST API error: ${res.status} ${res.statusText}`, + res.status, + ); + } + + return res.json(); +}; + +const restPost = async ( + baseUrl: string, + path: string, + body: unknown, + fetchOptions?: FetchOptions, +): Promise => { + const url = `${baseUrl}${path}`; + const headers: Record = { + Accept: "application/json", + "Content-Type": "application/json", + ...fetchOptions?.headers, + }; + + const res = await fetch(url, { + method: "POST", + headers, + credentials: fetchOptions?.credentials, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const text = await res.text().catch(() => res.statusText); + throw new RestApiError(`REST API error: ${res.status} ${text}`, res.status); + } + + return res.json(); +}; + +const restDelete = async ( + baseUrl: string, + path: string, + fetchOptions?: FetchOptions, +): Promise => { + const url = `${baseUrl}${path}`; + const headers: Record = { + Accept: "application/json", + ...fetchOptions?.headers, + }; + + const res = await fetch(url, { + method: "DELETE", + headers, + credentials: fetchOptions?.credentials, + }); + + if (!res.ok) { + const text = await res.text().catch(() => res.statusText); + throw new RestApiError(`REST API error: ${res.status} ${text}`, res.status); + } + + return res.json(); +}; + +export default restFetch; +export { RestApiError, restPost, restDelete }; diff --git a/ui/src/queries/useLoadComputeEngine.ts b/ui/src/queries/useLoadComputeEngine.ts new file mode 100644 index 00000000000..dc9dfae73bd --- /dev/null +++ b/ui/src/queries/useLoadComputeEngine.ts @@ -0,0 +1,121 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../contexts/RegistryPathContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch, { RestApiError } from "./restApiClient"; + +export interface ComputeEngineConfig { + type: string; + [key: string]: any; +} + +export interface ComputeEngineInfo { + engineType: string; + engineClass: string; + config: ComputeEngineConfig; + featureViewCount: number; +} + +export interface FeatureViewEngineInfo { + name: string; + type: string; + online: boolean; + lastMaterialized?: string; + hasOverride: boolean; + overrides?: Record; + materializationIntervals: Array<{ + startTime?: string; + endTime?: string; + start_time?: string; + end_time?: string; + }>; +} + +const ENGINE_TYPE_TO_CLASS: Record = { + local: "LocalComputeEngine", + "spark.engine": "SparkComputeEngine", + "ray.engine": "RayComputeEngine", + "flink.engine": "FlinkComputeEngine", + "snowflake.engine": "SnowflakeComputeEngine", + lambda: "LambdaComputeEngine", + k8s: "KubernetesComputeEngine", +}; + +function extractFeatureViewInfos(featureViews: any[]): FeatureViewEngineInfo[] { + return featureViews.map((fv: any) => ({ + name: fv.name, + type: fv.type || "Batch", + online: fv.online ?? true, + lastMaterialized: fv.lastMaterialized, + hasOverride: fv.hasOverride ?? false, + overrides: fv.overrides, + materializationIntervals: fv.materializationIntervals || [], + })); +} + +export function useLoadComputeEngine(projectName?: string) { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const enginePath = + projectName && projectName !== "all" + ? `/compute_engines?project=${encodeURIComponent(projectName)}` + : "/compute_engines/all?limit=100"; + + const engineQuery = useQuery( + ["rest", "compute-engine", registryUrl, projectName || "all"], + () => restFetch(registryUrl, enginePath, fetchOptions), + { + enabled: !!registryUrl, + staleTime: 30_000, + retry: (failureCount, error) => { + if (error instanceof RestApiError && error.status === 403) return false; + return failureCount < 3; + }, + }, + ); + + const isPermissionDenied = + engineQuery.isError && + engineQuery.error instanceof RestApiError && + engineQuery.error.status === 403; + + let engineInfo: ComputeEngineInfo | null = null; + let featureViewInfos: FeatureViewEngineInfo[] = []; + + if (engineQuery.isSuccess && engineQuery.data) { + const engine = engineQuery.data.engine || engineQuery.data.engines?.[0]; + if (engine) { + engineInfo = { + engineType: engine.engineType || "local", + engineClass: + engine.engineClass || + ENGINE_TYPE_TO_CLASS[engine.engineType] || + "LocalComputeEngine", + config: engine.config || { type: engine.engineType || "local" }, + featureViewCount: engine.featureViewCount || 0, + }; + } + + const rawFvs = engineQuery.data.featureViews || []; + featureViewInfos = extractFeatureViewInfos(rawFvs); + } + + if (!engineInfo) { + engineInfo = { + engineType: "local", + engineClass: "LocalComputeEngine", + config: { type: "local" }, + featureViewCount: featureViewInfos.length, + }; + } + + return { + isLoading: engineQuery.isLoading, + isSuccess: engineQuery.isSuccess, + isError: engineQuery.isError, + isPermissionDenied, + engineInfo, + featureViewInfos, + }; +} diff --git a/ui/src/queries/useLoadDataSourcesREST.ts b/ui/src/queries/useLoadDataSourcesREST.ts new file mode 100644 index 00000000000..5d3890cc503 --- /dev/null +++ b/ui/src/queries/useLoadDataSourcesREST.ts @@ -0,0 +1,41 @@ +import { useQuery } from "react-query"; +import { fetchApi } from "./restApi"; + +interface DataSourceListResponse { + dataSources: any[]; + pagination: Record; + relationships?: Record; +} + +const useLoadDataSourcesREST = (project: string) => { + return useQuery( + ["data-sources-rest", project], + () => + fetchApi("/data_sources", { + project, + allow_cache: "false", + }), + { + enabled: !!project, + staleTime: 30000, + }, + ); +}; + +const useLoadDataSourceREST = (name: string, project: string) => { + return useQuery( + ["data-source-rest", name, project], + () => + fetchApi(`/data_sources/${encodeURIComponent(name)}`, { + project, + include_relationships: "true", + allow_cache: "false", + }), + { + enabled: !!name && !!project, + staleTime: 30000, + }, + ); +}; + +export { useLoadDataSourcesREST, useLoadDataSourceREST }; diff --git a/ui/src/queries/useLoadEntitiesREST.ts b/ui/src/queries/useLoadEntitiesREST.ts new file mode 100644 index 00000000000..7127de656ee --- /dev/null +++ b/ui/src/queries/useLoadEntitiesREST.ts @@ -0,0 +1,41 @@ +import { useQuery } from "react-query"; +import { fetchApi } from "./restApi"; + +interface EntityListResponse { + entities: any[]; + pagination: Record; + relationships?: Record; +} + +const useLoadEntitiesREST = (project: string) => { + return useQuery( + ["entities-rest", project], + () => + fetchApi("/entities", { + project, + allow_cache: "false", + }), + { + enabled: !!project, + staleTime: 30000, + }, + ); +}; + +const useLoadEntityREST = (name: string, project: string) => { + return useQuery( + ["entity-rest", name, project], + () => + fetchApi(`/entities/${encodeURIComponent(name)}`, { + project, + include_relationships: "true", + allow_cache: "false", + }), + { + enabled: !!name && !!project, + staleTime: 30000, + }, + ); +}; + +export { useLoadEntitiesREST, useLoadEntityREST }; diff --git a/ui/src/queries/useLoadFeatureModels.ts b/ui/src/queries/useLoadFeatureModels.ts new file mode 100644 index 00000000000..dc6f97843d0 --- /dev/null +++ b/ui/src/queries/useLoadFeatureModels.ts @@ -0,0 +1,37 @@ +import { useQuery } from "react-query"; + +export interface FeatureModelInfo { + model_name: string; + version: string; + stage: string; + mlflow_url: string; +} + +interface FeatureModelsResponse { + feature_models: Record; + error?: string; +} + +const useLoadFeatureModels = () => { + return useQuery( + "feature-models", + () => { + return fetch("/api/mlflow-feature-models") + .then((res) => { + if (!res.ok) { + return { feature_models: {} }; + } + return res.json(); + }) + .catch(() => { + return { feature_models: {} }; + }); + }, + { + staleTime: 60000, + retry: false, + }, + ); +}; + +export default useLoadFeatureModels; diff --git a/ui/src/queries/useLoadFeatureUsage.ts b/ui/src/queries/useLoadFeatureUsage.ts new file mode 100644 index 00000000000..c797abe88ce --- /dev/null +++ b/ui/src/queries/useLoadFeatureUsage.ts @@ -0,0 +1,35 @@ +import { useQuery } from "react-query"; + +interface FeatureUsageEntry { + run_count: number; + last_used: number | null; + models: string[]; +} + +interface FeatureUsageResponse { + feature_usage: Record; + mlflow_enabled?: boolean; + error?: string; +} + +const fetchFeatureUsage = async (): Promise => { + const response = await fetch("/api/mlflow-feature-usage"); + if (!response.ok) { + throw new Error(`Failed to fetch feature usage: ${response.statusText}`); + } + return response.json(); +}; + +const useLoadFeatureUsage = () => { + return useQuery( + "mlflowFeatureUsage", + fetchFeatureUsage, + { + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + }, + ); +}; + +export default useLoadFeatureUsage; +export type { FeatureUsageEntry, FeatureUsageResponse }; diff --git a/ui/src/queries/useLoadFeatureViewsREST.ts b/ui/src/queries/useLoadFeatureViewsREST.ts new file mode 100644 index 00000000000..0b67b960e11 --- /dev/null +++ b/ui/src/queries/useLoadFeatureViewsREST.ts @@ -0,0 +1,41 @@ +import { useQuery } from "react-query"; +import { fetchApi } from "./restApi"; + +interface FeatureViewListResponse { + featureViews: any[]; + pagination: Record; + relationships?: Record; +} + +const useLoadFeatureViewsREST = (project: string) => { + return useQuery( + ["feature-views-rest", project], + () => + fetchApi("/feature_views", { + project, + allow_cache: "false", + }), + { + enabled: !!project, + staleTime: 30000, + }, + ); +}; + +const useLoadFeatureViewREST = (name: string, project: string) => { + return useQuery( + ["feature-view-rest", name, project], + () => + fetchApi(`/feature_views/${encodeURIComponent(name)}`, { + project, + include_relationships: "true", + allow_cache: "false", + }), + { + enabled: !!name && !!project, + staleTime: 30000, + }, + ); +}; + +export { useLoadFeatureViewsREST, useLoadFeatureViewREST }; diff --git a/ui/src/queries/useLoadMlflowRuns.ts b/ui/src/queries/useLoadMlflowRuns.ts new file mode 100644 index 00000000000..041fd41137d --- /dev/null +++ b/ui/src/queries/useLoadMlflowRuns.ts @@ -0,0 +1,52 @@ +import { useQuery } from "react-query"; + +export interface RegisteredModelInfo { + model_name: string; + version: string; + stage: string; + mlflow_url: string; +} + +export interface MlflowRunData { + run_id: string; + run_name: string; + status: string; + start_time: number; + feature_service: string | null; + feature_views: string[]; + feature_refs: string[]; + retrieval_type: string | null; + entity_count: string | null; + mlflow_url: string; + registered_models: RegisteredModelInfo[]; +} + +interface MlflowRunsResponse { + runs: MlflowRunData[]; + mlflow_uri: string | null; + error?: string; +} + +const useLoadMlflowRuns = () => { + return useQuery( + "mlflow-runs", + () => { + return fetch("/api/mlflow-runs") + .then((res) => { + if (!res.ok) { + return { runs: [], mlflow_uri: null }; + } + return res.json(); + }) + .catch(() => { + return { runs: [], mlflow_uri: null }; + }); + }, + { + staleTime: 30000, + retry: false, + }, + ); +}; + +export default useLoadMlflowRuns; diff --git a/ui/src/queries/useLoadOpenLineageGraph.ts b/ui/src/queries/useLoadOpenLineageGraph.ts new file mode 100644 index 00000000000..25bd4e12389 --- /dev/null +++ b/ui/src/queries/useLoadOpenLineageGraph.ts @@ -0,0 +1,132 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../contexts/RegistryPathContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch from "./restApiClient"; + +export interface OpenLineageNode { + type: string; + namespace: string; + name: string; + producer?: string; + feast_object_type?: string; + feast_object_name?: string; + feast_project?: string; + schema?: any; + description?: string; + job_type?: string; + source_type?: string; + facets?: Record; +} + +export interface OpenLineageEdge { + source_type: string; + source_namespace: string; + source_name: string; + target_type: string; + target_namespace: string; + target_name: string; + edge_type?: string; + updated_at?: number; +} + +export interface OpenLineageSymlink { + dataset_namespace: string; + dataset_name: string; + linked_namespace: string; + linked_name: string; + link_type: string; +} + +export interface OpenLineageGraphData { + nodes: OpenLineageNode[]; + edges: OpenLineageEdge[]; + symlinks?: OpenLineageSymlink[]; +} + +export interface OpenLineageEvent { + event_id: string; + event_type: string; + event_time: number; + producer?: string; + job_namespace: string; + job_name: string; + run_id?: string; + event_json: string; + created_at: number; +} + +export interface RegistryRelationship { + source: { type: string; name: string }; + target: { type: string; name: string }; + type: string; + project?: string; +} + +export interface RegistryLineageData { + relationships: RegistryRelationship[]; + indirect_relationships: RegistryRelationship[]; +} + +const useLoadOpenLineageGraph = () => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + return useQuery( + ["openlineage-graph"], + () => + restFetch( + registryUrl, + "/lineage/openlineage/graph", + fetchOptions, + ), + { enabled: !!registryUrl }, + ); +}; + +const useLoadOpenLineageEvents = ( + namespace?: string, + jobName?: string, + limit: number = 100, +) => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const params = new URLSearchParams(); + if (namespace) params.set("namespace", namespace); + if (jobName) params.set("job_name", jobName); + params.set("limit", limit.toString()); + + return useQuery<{ events: OpenLineageEvent[]; total: number }>( + ["openlineage-events", namespace, jobName, limit], + () => + restFetch( + registryUrl, + `/lineage/openlineage/events?${params.toString()}`, + fetchOptions, + ), + { enabled: !!registryUrl }, + ); +}; + +const useLoadRegistryLineage = (project?: string) => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + return useQuery( + ["registry-lineage", project], + () => + restFetch( + registryUrl, + `/lineage/registry?project=${project}`, + fetchOptions, + ), + { enabled: !!registryUrl && !!project }, + ); +}; + +export { + useLoadOpenLineageGraph, + useLoadOpenLineageEvents, + useLoadRegistryLineage, +}; diff --git a/ui/src/queries/useLoadRegistry.ts b/ui/src/queries/useLoadRegistry.ts index e3f5ac87a1d..9e7cf2db2d5 100644 --- a/ui/src/queries/useLoadRegistry.ts +++ b/ui/src/queries/useLoadRegistry.ts @@ -4,18 +4,20 @@ import parseEntityRelationships, { EntityRelation, } from "../parsers/parseEntityRelationships"; import parseIndirectRelationships from "../parsers/parseIndirectRelationships"; -import { feast } from "../protos"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch from "./restApiClient"; +import type { FetchOptions } from "../contexts/DataModeContext"; interface FeatureStoreAllData { project: string; description?: string; - objects: feast.core.Registry; + objects: any; relationships: EntityRelation[]; mergedFVMap: Record; mergedFVList: genericFVType[]; indirectRelationships: EntityRelation[]; allFeatures: Feature[]; - permissions?: any[]; // Add permissions field + permissions?: any[]; } interface Feature { @@ -25,251 +27,224 @@ interface Feature { project?: string; } -const useLoadRegistry = (url: string, projectName?: string) => { - return useQuery( - `registry:${url}:${projectName || "all"}`, - () => { - return fetch(url, { - headers: { - "Content-Type": "application/json", - }, - }) - .then((res) => { - const contentType = res.headers.get("content-type"); - if (contentType && contentType.includes("application/json")) { - return res.json(); - } else { - return res.arrayBuffer(); - } - }) - .then((data) => { - let objects; - - if (data instanceof ArrayBuffer) { - objects = feast.core.Registry.decode(new Uint8Array(data)); - } else { - objects = data; - } - // const objects = FeastRegistrySchema.parse(json); - - if (!objects.featureViews) { - objects.featureViews = []; - } - - // Filter objects by project if projectName is provided - // Skip filtering if projectName is "all" (All Projects view) - // Only filter if we detect that the registry contains multiple projects - if (projectName && projectName !== "all") { - // Check if the registry actually has multiple projects - const projectsInRegistry = new Set(); - objects.featureViews?.forEach((fv: any) => { - if (fv?.spec?.project) projectsInRegistry.add(fv.spec.project); - }); - objects.entities?.forEach((entity: any) => { - if (entity?.spec?.project) - projectsInRegistry.add(entity.spec.project); - }); - - // Only apply filtering if there are actually multiple projects in the registry - // OR if the projectName matches one of the projects in the registry - const shouldFilter = - projectsInRegistry.size > 1 || - projectsInRegistry.has(projectName); - - if (shouldFilter && projectsInRegistry.has(projectName)) { - if (objects.featureViews) { - objects.featureViews = objects.featureViews.filter( - (fv: any) => fv?.spec?.project === projectName, - ); - } - if (objects.entities) { - objects.entities = objects.entities.filter( - (entity: any) => entity?.spec?.project === projectName, - ); - } - if (objects.dataSources) { - objects.dataSources = objects.dataSources.filter( - (ds: any) => ds?.project === projectName, - ); - } - if (objects.featureServices) { - objects.featureServices = objects.featureServices.filter( - (fs: any) => fs?.spec?.project === projectName, - ); - } - if (objects.onDemandFeatureViews) { - objects.onDemandFeatureViews = - objects.onDemandFeatureViews.filter( - (odfv: any) => odfv?.spec?.project === projectName, - ); - } - if (objects.streamFeatureViews) { - objects.streamFeatureViews = objects.streamFeatureViews.filter( - (sfv: any) => sfv?.spec?.project === projectName, - ); - } - if (objects.savedDatasets) { - objects.savedDatasets = objects.savedDatasets.filter( - (sd: any) => sd?.spec?.project === projectName, - ); - } - if (objects.validationReferences) { - objects.validationReferences = - objects.validationReferences.filter( - (vr: any) => vr?.project === projectName, - ); - } - if (objects.permissions) { - objects.permissions = objects.permissions.filter( - (perm: any) => - perm?.spec?.project === projectName || !perm?.spec?.project, - ); - } - } - } - - if ( - process.env.NODE_ENV === "test" && - objects.featureViews.length === 0 - ) { - try { - const fs = require("fs"); - const path = require("path"); - const { feast } = require("../protos"); - - const registry = fs.readFileSync( - path.resolve(__dirname, "../../public/registry.db"), - ); - const parsedRegistry = feast.core.Registry.decode(registry); - - if ( - parsedRegistry.featureViews && - parsedRegistry.featureViews.length > 0 - ) { - objects.featureViews = parsedRegistry.featureViews; - } - } catch (e) { - console.error("Error loading test registry:", e); - } - } - - const { mergedFVMap, mergedFVList } = mergedFVTypes(objects); - - const relationships = parseEntityRelationships(objects); +// --------------------------------------------------------------------------- +// Shared post-processing (used by the bulk REST fetch) +// --------------------------------------------------------------------------- + +const assembleFeatureStoreData = ( + objects: any, + projectName?: string, +): FeatureStoreAllData => { + const { mergedFVMap, mergedFVList } = mergedFVTypes(objects); + const relationships = parseEntityRelationships(objects); + const indirectRelationships = parseIndirectRelationships( + relationships, + objects, + ); - // Only contains Entity -> FS or DS -> FS relationships - const indirectRelationships = parseIndirectRelationships( - relationships, - objects, - ); + const allFeatures: Feature[] = + objects.featureViews?.flatMap( + (fv: any) => + fv?.spec?.features?.map((feature: any) => ({ + name: feature.name ?? "Unknown", + featureView: fv?.spec?.name || "Unknown FeatureView", + type: + feature.valueType != null + ? typeof feature.valueType === "number" + ? String(feature.valueType) + : feature.valueType + : "Unknown Type", + project: fv?.spec?.project || fv?.project, + })) || [], + ) || []; + + let resolvedProjectName: string = + projectName === "all" + ? "All Projects" + : projectName || + (objects.projects && + objects.projects.length > 0 && + objects.projects[0].spec && + objects.projects[0].spec.name + ? objects.projects[0].spec.name + : objects.project + ? objects.project + : "default"); + + let projectDescription: string | undefined; + if (projectName === "all") { + projectDescription = "View data across all projects"; + } else if (objects.projects && objects.projects.length > 0) { + const currentProject = objects.projects.find( + (p: any) => p?.spec?.name === resolvedProjectName, + ); + if (currentProject?.spec) { + projectDescription = currentProject.spec.description; + } + } + + return { + project: resolvedProjectName, + description: projectDescription, + objects, + mergedFVMap, + mergedFVList, + relationships, + indirectRelationships, + allFeatures, + permissions: objects.permissions || [], + }; +}; - // console.log({ - // objects, - // mergedFVMap, - // mergedFVList, - // relationships, - // indirectRelationships, - // }); - const allFeatures: Feature[] = - objects.featureViews?.flatMap( - (fv: any) => - fv?.spec?.features?.map((feature: any) => ({ - name: feature.name ?? "Unknown", - featureView: fv?.spec?.name || "Unknown FeatureView", - type: - feature.valueType != null - ? feast.types.ValueType.Enum[feature.valueType] - : "Unknown Type", - project: fv?.spec?.project, // Include project from parent feature view - })) || [], - ) || []; +// --------------------------------------------------------------------------- +// REST fetch strategy +// --------------------------------------------------------------------------- + +const permissionSafeFetch = async ( + apiBaseUrl: string, + path: string, + fallback: T, + fetchOptions?: FetchOptions, +): Promise => { + try { + return await restFetch(apiBaseUrl, path, fetchOptions); + } catch (err: any) { + if (err?.status === 403 || err?.status === 401) { + return fallback; + } + throw err; + } +}; - // Use the provided projectName parameter if available, otherwise try to determine from registry - let resolvedProjectName: string = - projectName === "all" - ? "All Projects" - : projectName || - (process.env.NODE_ENV === "test" - ? "credit_scoring_aws" - : objects.projects && - objects.projects.length > 0 && - objects.projects[0].spec && - objects.projects[0].spec.name - ? objects.projects[0].spec.name - : objects.project - ? objects.project - : "credit_scoring_aws"); +const fetchREST = async ( + apiBaseUrl: string, + projectName?: string, + fetchOptions?: FetchOptions, +): Promise => { + const projectParam = + projectName && projectName !== "all" + ? `?project=${encodeURIComponent(projectName)}` + : ""; + const useAllEndpoint = !projectParam; + + const emptyList = (key: string) => ({ [key]: [] }); + + const [ + entitiesResp, + featureViewsResp, + labelViewsResp, + featureServicesResp, + dataSourcesResp, + savedDatasetsResp, + projectsResp, + ] = await Promise.all([ + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/entities/all?include_relationships=true" + : `/entities${projectParam}&include_relationships=true`, + emptyList("entities"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/feature_views/all?include_relationships=true" + : `/feature_views${projectParam}&include_relationships=true`, + emptyList("featureViews"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/label_views/all?include_relationships=true" + : `/label_views${projectParam}&include_relationships=true`, + emptyList("featureViews"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/feature_services/all?include_relationships=true" + : `/feature_services${projectParam}&include_relationships=true`, + emptyList("featureServices"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/data_sources/all?include_relationships=true" + : `/data_sources${projectParam}&include_relationships=true`, + emptyList("dataSources"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + useAllEndpoint + ? "/saved_datasets/all?include_relationships=true" + : `/saved_datasets${projectParam}&include_relationships=true`, + emptyList("savedDatasets"), + fetchOptions, + ), + permissionSafeFetch( + apiBaseUrl, + "/projects", + emptyList("projects"), + fetchOptions, + ), + ]); + + const entities = entitiesResp.entities || []; + const allFeatureViews = featureViewsResp.featureViews || []; + const labelViews: any[] = labelViewsResp.featureViews || []; + const featureServices = featureServicesResp.featureServices || []; + const dataSources = dataSourcesResp.dataSources || []; + const savedDatasets = savedDatasetsResp.savedDatasets || []; + const projects = projectsResp.projects || []; + + const featureViews: any[] = []; + const onDemandFeatureViews: any[] = []; + const streamFeatureViews: any[] = []; + + for (const fv of allFeatureViews) { + const fvType = fv.type; + if (fvType === "onDemandFeatureView") { + onDemandFeatureViews.push(fv); + } else if (fvType === "streamFeatureView") { + streamFeatureViews.push(fv); + } else { + featureViews.push(fv); + } + } + + const objects: any = { + entities, + featureViews, + onDemandFeatureViews, + streamFeatureViews, + labelViews, + featureServices, + dataSources, + savedDatasets, + projects, + }; + + return assembleFeatureStoreData(objects, projectName); +}; - let projectDescription = undefined; +// --------------------------------------------------------------------------- +// Public hook +// --------------------------------------------------------------------------- - // Find project description from the projects array - if (projectName === "all") { - projectDescription = "View data across all projects"; - } else if (objects.projects && objects.projects.length > 0) { - const currentProject = objects.projects.find( - (p: any) => p?.spec?.name === resolvedProjectName, - ); - if (currentProject?.spec) { - projectDescription = currentProject.spec.description; - } - } +const useLoadRegistry = (url: string, projectName?: string) => { + const { fetchOptions } = useDataMode(); - return { - project: resolvedProjectName, - description: projectDescription, - objects, - mergedFVMap, - mergedFVList, - relationships, - indirectRelationships, - allFeatures, - permissions: - objects.permissions && objects.permissions.length > 0 - ? objects.permissions - : [ - { - spec: { - name: "zipcode-features-reader", - types: [2], // FeatureView - name_patterns: ["zipcode_features"], - policy: { roles: ["analyst", "data_scientist"] }, - actions: [1, 4, 5], // DESCRIBE, READ_ONLINE, READ_OFFLINE - }, - }, - { - spec: { - name: "zipcode-source-writer", - types: [7], // FileSource - name_patterns: ["zipcode"], - policy: { roles: ["admin", "data_engineer"] }, - actions: [0, 2, 7], // CREATE, UPDATE, WRITE_OFFLINE - }, - }, - { - spec: { - name: "credit-score-v1-reader", - types: [6], // FeatureService - name_patterns: ["credit_score_v1"], - policy: { roles: ["model_user", "data_scientist"] }, - actions: [1, 4], // DESCRIBE, READ_ONLINE - }, - }, - { - spec: { - name: "risky-features-reader", - types: [2, 6], // FeatureView, FeatureService - name_patterns: [], - required_tags: { stage: "prod" }, - policy: { roles: ["trusted_analyst"] }, - actions: [5], // READ_OFFLINE - }, - }, - ], - }; - }); - }, + return useQuery( + ["registry-rest-bulk", url, projectName || "all"], + () => fetchREST(url, projectName, fetchOptions), { - staleTime: Infinity, // Given that we are reading from a registry dump, this seems reasonable for now. + staleTime: 30_000, + enabled: !!url, }, ); }; diff --git a/ui/src/queries/useLoadRunHistory.ts b/ui/src/queries/useLoadRunHistory.ts new file mode 100644 index 00000000000..eddc82a1be9 --- /dev/null +++ b/ui/src/queries/useLoadRunHistory.ts @@ -0,0 +1,66 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../contexts/RegistryPathContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch from "./restApiClient"; + +export interface RunSummary { + run_id: string; + job_namespace: string; + job_name: string; + state: string; + started_at: number | null; + ended_at: number | null; + updated_at: number; +} + +export interface RunIOEntry { + namespace: string; + name: string; + facets?: Record | null; +} + +export interface RunDetail extends RunSummary { + inputs: RunIOEntry[]; + outputs: RunIOEntry[]; + facets?: Record | null; +} + +const useRunHistory = (jobNamespace?: string, jobName?: string) => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const params = new URLSearchParams(); + if (jobNamespace) params.set("job_namespace", jobNamespace); + if (jobName) params.set("job_name", jobName); + params.set("limit", "50"); + + return useQuery<{ runs: RunSummary[]; total: number }>( + ["openlineage-runs", jobNamespace, jobName], + () => + restFetch( + registryUrl, + `/lineage/openlineage/runs?${params.toString()}`, + fetchOptions, + ), + { enabled: !!registryUrl && !!jobNamespace && !!jobName }, + ); +}; + +const useRunDetail = (runId?: string) => { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + return useQuery( + ["openlineage-run-detail", runId], + () => + restFetch( + registryUrl, + `/lineage/openlineage/runs/${runId}`, + fetchOptions, + ), + { enabled: !!registryUrl && !!runId }, + ); +}; + +export { useRunHistory, useRunDetail }; diff --git a/ui/src/queries/useMonitoringApi.ts b/ui/src/queries/useMonitoringApi.ts new file mode 100644 index 00000000000..73a9b16e3fd --- /dev/null +++ b/ui/src/queries/useMonitoringApi.ts @@ -0,0 +1,318 @@ +import { useContext } from "react"; +import { useQuery, useMutation, useQueryClient } from "react-query"; +import MonitoringContext from "../contexts/MonitoringContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import type { FetchOptions } from "../contexts/DataModeContext"; + +interface FeatureMetric { + project_id: string; + feature_view_name: string; + feature_name: string; + metric_date: string; + granularity: string; + data_source_type: string; + computed_at: string; + is_baseline: boolean; + feature_type: string; + row_count: number; + null_count: number; + null_rate: number; + mean: number | null; + stddev: number | null; + min_val: number | null; + max_val: number | null; + p50: number | null; + p75: number | null; + p90: number | null; + p95: number | null; + p99: number | null; + histogram: NumericHistogram | CategoricalHistogram | null; +} + +interface NumericHistogram { + bins: number[]; + counts: number[]; + bin_width: number; +} + +interface CategoricalHistogram { + values: { value: string; count: number }[]; + other_count: number; + unique_count: number; +} + +interface FeatureViewMetric { + project_id: string; + feature_view_name: string; + metric_date: string; + granularity: string; + data_source_type: string; + computed_at: string; + is_baseline: boolean; + total_row_count: number; + total_features: number; + features_with_nulls: number; + avg_null_rate: number; + max_null_rate: number; +} + +interface FeatureServiceMetric { + project_id: string; + feature_service_name: string; + metric_date: string; + granularity: string; + data_source_type: string; + computed_at: string; + is_baseline: boolean; + total_feature_views: number; + total_features: number; + avg_null_rate: number; + max_null_rate: number; +} + +interface MonitoringFilters { + project: string; + feature_view_name?: string; + feature_name?: string; + feature_service_name?: string; + granularity?: string; + data_source_type?: string; + start_date?: string; + end_date?: string; + is_baseline?: boolean; +} + +const toQueryParams = ( + filters: MonitoringFilters, +): Record => { + return { + project: filters.project, + feature_view_name: filters.feature_view_name, + feature_name: filters.feature_name, + feature_service_name: filters.feature_service_name, + granularity: filters.granularity, + data_source_type: filters.data_source_type, + start_date: filters.start_date, + end_date: filters.end_date, + is_baseline: filters.is_baseline ? "true" : undefined, + }; +}; + +const buildQueryString = (params: Record) => { + const entries = Object.entries(params).filter( + ([, v]) => v !== undefined && v !== "", + ); + if (entries.length === 0) return ""; + return ( + "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(v!)}`).join("&") + ); +}; + +class MonitoringApiError extends Error { + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +const fetchMonitoring = async ( + baseUrl: string, + path: string, + params: Record, + fetchOptions?: FetchOptions, +): Promise => { + const qs = buildQueryString(params); + const res = await fetch(`${baseUrl}${path}${qs}`, { + method: "GET", + headers: { + Accept: "application/json", + ...fetchOptions?.headers, + }, + credentials: fetchOptions?.credentials, + }); + if (!res.ok) { + throw new MonitoringApiError( + res.status, + `Failed to fetch ${path}: ${res.status} ${res.statusText}`, + ); + } + const text = await res.text(); + const sanitized = text + .replace(/:\s*NaN/g, ": null") + .replace(/:\s*Infinity/g, ": null") + .replace(/:\s*-Infinity/g, ": null"); + return JSON.parse(sanitized); +}; + +const isServiceUnavailable = (error: unknown): boolean => + error instanceof MonitoringApiError && error.status === 503; + +const STALE_TIME = 30_000; + +const useFeatureMetrics = (filters: MonitoringFilters) => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + const path = filters.is_baseline + ? "/monitoring/metrics/baseline" + : "/monitoring/metrics/features"; + return useQuery( + ["monitoring-features", filters], + () => + fetchMonitoring( + apiBaseUrl, + path, + toQueryParams(filters), + fetchOptions, + ), + { staleTime: STALE_TIME, retry: 1 }, + ); +}; + +const aggregateToFeatureViewMetrics = ( + features: FeatureMetric[], +): FeatureViewMetric[] => { + const grouped = new Map(); + for (const f of features) { + const key = f.feature_view_name; + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key)!.push(f); + } + return Array.from(grouped.entries()).map(([fvName, feats]) => { + const nullRates = feats.map((f) => f.null_rate ?? 0); + const maxRowCount = Math.max(...feats.map((f) => f.row_count ?? 0)); + return { + project_id: feats[0].project_id, + feature_view_name: fvName, + metric_date: feats[0].metric_date, + granularity: feats[0].granularity, + data_source_type: feats[0].data_source_type, + computed_at: feats[0].computed_at, + is_baseline: feats[0].is_baseline, + total_row_count: maxRowCount, + total_features: feats.length, + features_with_nulls: feats.filter((f) => (f.null_count ?? 0) > 0).length, + avg_null_rate: + nullRates.length > 0 + ? nullRates.reduce((a, b) => a + b, 0) / nullRates.length + : 0, + max_null_rate: nullRates.length > 0 ? Math.max(...nullRates) : 0, + }; + }); +}; + +const useFeatureViewMetrics = (filters: MonitoringFilters) => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + const isBaseline = !!filters.is_baseline; + return useQuery( + ["monitoring-feature-views", filters], + async () => { + if (isBaseline) { + const features = await fetchMonitoring( + apiBaseUrl, + "/monitoring/metrics/baseline", + toQueryParams(filters), + fetchOptions, + ); + return aggregateToFeatureViewMetrics(features); + } + return fetchMonitoring( + apiBaseUrl, + "/monitoring/metrics/feature_views", + toQueryParams(filters), + fetchOptions, + ); + }, + { staleTime: STALE_TIME, retry: 1 }, + ); +}; + +const useFeatureServiceMetrics = (filters: MonitoringFilters) => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + return useQuery( + ["monitoring-feature-services", filters], + () => + fetchMonitoring( + apiBaseUrl, + "/monitoring/metrics/feature_services", + toQueryParams(filters), + fetchOptions, + ), + { staleTime: STALE_TIME, retry: 1 }, + ); +}; + +const useBaselineMetrics = ( + project: string, + featureViewName?: string, + featureName?: string, + dataSourceType?: string, +) => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + return useQuery( + ["monitoring-baseline", project, featureViewName, featureName], + () => + fetchMonitoring( + apiBaseUrl, + "/monitoring/metrics/baseline", + { + project, + feature_view_name: featureViewName, + feature_name: featureName, + data_source_type: dataSourceType, + }, + fetchOptions, + ), + { staleTime: STALE_TIME, retry: 1 }, + ); +}; + +const useComputeMetrics = () => { + const { apiBaseUrl } = useContext(MonitoringContext); + const { fetchOptions } = useDataMode(); + const queryClient = useQueryClient(); + return useMutation( + async (body: { project: string; feature_view_name?: string }) => { + const res = await fetch(`${apiBaseUrl}/monitoring/auto_compute`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...fetchOptions?.headers, + }, + credentials: fetchOptions?.credentials, + body: JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(`Failed to trigger compute: ${res.status}`); + } + return res.json(); + }, + { + onSuccess: () => { + queryClient.invalidateQueries("monitoring-features"); + queryClient.invalidateQueries("monitoring-feature-views"); + queryClient.invalidateQueries("monitoring-feature-services"); + }, + }, + ); +}; + +export { + isServiceUnavailable, + useFeatureMetrics, + useFeatureViewMetrics, + useFeatureServiceMetrics, + useBaselineMetrics, + useComputeMetrics, +}; +export type { + FeatureMetric, + FeatureViewMetric, + FeatureServiceMetric, + NumericHistogram, + CategoricalHistogram, + MonitoringFilters, +}; diff --git a/ui/src/queries/useResourceQuery.ts b/ui/src/queries/useResourceQuery.ts new file mode 100644 index 00000000000..b099d04fc4d --- /dev/null +++ b/ui/src/queries/useResourceQuery.ts @@ -0,0 +1,242 @@ +import { useContext } from "react"; +import { useQuery } from "react-query"; +import RegistryPathContext from "../contexts/RegistryPathContext"; +import { useDataMode } from "../contexts/DataModeContext"; +import restFetch from "./restApiClient"; +import { RestApiError } from "./restApiClient"; +import { FEAST_FV_TYPES, genericFVType } from "../parsers/mergedFVTypes"; + +interface ResourceQueryOptions { + resourceType: string; + project?: string; + restPath: string; + restSelect?: (data: any) => T | undefined; + enabled?: boolean; +} + +/** + * Generic hook for fetching a specific resource slice via REST API. + * + * Each caller fires its own lightweight endpoint request, and react-query + * deduplicates identical keys automatically. + */ +function useResourceQuery({ + resourceType, + project, + restPath, + restSelect, + enabled = true, +}: ResourceQueryOptions) { + const registryUrl = useContext(RegistryPathContext); + const { fetchOptions } = useDataMode(); + + const query = useQuery( + ["rest", resourceType, registryUrl, project || "all"], + () => restFetch(registryUrl, restPath, fetchOptions), + { + enabled: !!registryUrl && enabled, + staleTime: 30_000, + select: restSelect, + retry: (failureCount, error) => { + if (error instanceof RestApiError && error.status === 403) return false; + return failureCount < 3; + }, + }, + ); + + const isPermissionDenied = + query.isError && + query.error instanceof RestApiError && + query.error.status === 403; + + return { ...query, isPermissionDenied }; +} + +// --------------------------------------------------------------------------- +// REST endpoint path builders +// --------------------------------------------------------------------------- + +function entityListPath(project?: string): string { + if (project && project !== "all") { + return `/entities?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/entities/all?limit=100&include_relationships=true"; +} + +function entityDetailPath(name: string, project: string): string { + return `/entities/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function featureViewListPath(project?: string): string { + if (project && project !== "all") { + return `/feature_views?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/feature_views/all?limit=100&include_relationships=true"; +} + +function featureViewDetailPath(name: string, project: string): string { + return `/feature_views/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function featureServiceListPath(project?: string): string { + if (project && project !== "all") { + return `/feature_services?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/feature_services/all?limit=100&include_relationships=true"; +} + +function featureServiceDetailPath(name: string, project: string): string { + return `/feature_services/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function dataSourceListPath(project?: string): string { + if (project && project !== "all") { + return `/data_sources?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/data_sources/all?limit=100&include_relationships=true"; +} + +function dataSourceDetailPath(name: string, project: string): string { + return `/data_sources/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function savedDatasetListPath(project?: string): string { + if (project && project !== "all") { + return `/saved_datasets?project=${encodeURIComponent(project)}`; + } + return "/saved_datasets/all?limit=100"; +} + +function savedDatasetDetailPath(name: string, project: string): string { + return `/saved_datasets/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}`; +} + +function labelViewListPath(project?: string): string { + if (project && project !== "all") { + return `/label_views?project=${encodeURIComponent(project)}&include_relationships=true`; + } + return "/label_views/all?limit=100&include_relationships=true"; +} + +function labelViewDetailPath(name: string, project: string): string { + return `/label_views/${encodeURIComponent(name)}?project=${encodeURIComponent(project)}&include_relationships=true`; +} + +function permissionListPath(project?: string): string { + if (project && project !== "all") { + return `/permissions?project=${encodeURIComponent(project)}`; + } + return `/permissions?project=default`; +} + +function featuresListPath(project?: string): string { + if (project && project !== "all") { + return `/features?project=${encodeURIComponent(project)}`; + } + return "/features/all?limit=100"; +} + +function featureDetailPath( + featureViewName: string, + featureName: string, + project: string, +): string { + return `/features/${encodeURIComponent(featureViewName)}/${encodeURIComponent(featureName)}?project=${encodeURIComponent(project)}`; +} + +// --------------------------------------------------------------------------- +// REST response → mergedFVList converter +// --------------------------------------------------------------------------- + +function restFeatureViewsToMergedList(resp: any): genericFVType[] { + const featureViews = resp?.featureViews || []; + return featureViews + .filter((fv: any) => fv.type !== "labelView") + .map((fv: any) => { + const fvType = fv.type; + if (fvType === "onDemandFeatureView") { + return { + name: fv.spec?.name, + type: FEAST_FV_TYPES.ondemand, + features: fv.spec?.features || [], + object: fv, + }; + } + if (fvType === "streamFeatureView") { + return { + name: fv.spec?.name, + type: FEAST_FV_TYPES.stream, + features: fv.spec?.features || [], + object: fv, + }; + } + return { + name: fv.spec?.name, + type: FEAST_FV_TYPES.regular, + features: fv.spec?.features || [], + object: fv, + }; + }); +} + +function restLabelViewsFromResponse(resp: any): any[] { + const featureViews = resp?.featureViews || []; + return featureViews.filter((fv: any) => fv.type === "labelView"); +} + +function restFeatureViewDetailToGeneric(resp: any): genericFVType | undefined { + if (!resp || !resp.spec) return undefined; + const fvType = resp.type; + if (fvType === "onDemandFeatureView") { + return { + name: resp.spec.name, + type: FEAST_FV_TYPES.ondemand, + features: resp.spec.features || [], + object: resp, + }; + } + if (fvType === "streamFeatureView") { + return { + name: resp.spec.name, + type: FEAST_FV_TYPES.stream, + features: resp.spec.features || [], + object: resp, + }; + } + if (fvType === "labelView") { + return { + name: resp.spec.name, + type: FEAST_FV_TYPES.label, + features: resp.spec.features || [], + object: resp, + }; + } + return { + name: resp.spec.name, + type: FEAST_FV_TYPES.regular, + features: resp.spec.features || [], + object: resp, + }; +} + +export default useResourceQuery; +export { + entityListPath, + entityDetailPath, + featureViewListPath, + featureViewDetailPath, + featureServiceListPath, + featureServiceDetailPath, + dataSourceListPath, + dataSourceDetailPath, + savedDatasetListPath, + savedDatasetDetailPath, + labelViewListPath, + labelViewDetailPath, + permissionListPath, + featuresListPath, + featureDetailPath, + restFeatureViewsToMergedList, + restFeatureViewDetailToGeneric, + restLabelViewsFromResponse, +}; diff --git a/ui/src/setupProxy.js b/ui/src/setupProxy.js new file mode 100644 index 00000000000..94762f63557 --- /dev/null +++ b/ui/src/setupProxy.js @@ -0,0 +1,332 @@ +const fs = require("fs"); +const path = require("path"); +const express = require("express"); +const { feast } = require("./protos"); + +const registryBuf = fs.readFileSync( + path.resolve(__dirname, "../public/registry.db"), +); +const parsedRegistry = feast.core.Registry.decode(registryBuf); +const projectsList = JSON.parse( + fs.readFileSync(path.resolve(__dirname, "../public/projects-list.json")), +); + +const toJSON = (obj) => (obj && obj.toJSON ? obj.toJSON() : obj); + +const withType = (type) => (fv) => ({ + ...toJSON(fv), + type, +}); + +const state = { + entities: (parsedRegistry.entities || []).map(toJSON), + featureViews: (parsedRegistry.featureViews || []).map( + withType("featureView"), + ), + onDemandFeatureViews: (parsedRegistry.onDemandFeatureViews || []).map( + withType("onDemandFeatureView"), + ), + streamFeatureViews: (parsedRegistry.streamFeatureViews || []).map( + withType("streamFeatureView"), + ), + featureServices: (parsedRegistry.featureServices || []).map(toJSON), + dataSources: (parsedRegistry.dataSources || []).map(toJSON), + savedDatasets: (parsedRegistry.savedDatasets || []).map(toJSON), + projects: (parsedRegistry.projects || []).map(toJSON), +}; + +const allFeatureViews = () => [ + ...state.featureViews, + ...state.onDemandFeatureViews, + ...state.streamFeatureViews, +]; + +const objectProject = (obj) => obj?.spec?.project || obj?.project; + +const filterByProject = (items, project) => { + if (!project || project === "all") return items; + return items.filter((item) => objectProject(item) === project); +}; + +const allFeatures = (project) => + filterByProject(allFeatureViews(), project).flatMap((fv) => + (fv?.spec?.features || []).map((feature) => ({ + name: feature.name, + featureViewName: fv.spec?.name, + valueType: feature.valueType, + project: fv.spec?.project, + })), + ); + +const responseList = (res, key, items) => { + res.json({ + [key]: items, + pagination: {}, + relationships: {}, + }); +}; + +const findByName = (items, name) => + items.find((item) => item?.spec?.name === name || item?.name === name); + +const entityPayloadToResource = (payload) => ({ + spec: { + name: payload.name, + joinKey: payload.join_key || payload.name, + valueType: payload.value_type, + description: payload.description || "", + tags: payload.tags || {}, + owner: payload.owner || "", + project: payload.project, + }, + meta: {}, +}); + +const dataSourcePayloadToResource = (payload) => ({ + name: payload.name, + type: payload.type, + timestampField: payload.timestamp_field, + fieldMapping: payload.field_mapping || {}, + description: payload.description || "", + tags: payload.tags || {}, + owner: payload.owner || "", + project: payload.project, + fileOptions: payload.file_options, + bigqueryOptions: payload.bigquery_options, + snowflakeOptions: payload.snowflake_options, + redshiftOptions: payload.redshift_options, + kafkaOptions: payload.kafka_options, + sparkOptions: payload.spark_options, +}); + +const featureViewPayloadToResource = (payload) => ({ + spec: { + name: payload.name, + description: payload.description || "", + owner: payload.owner || "", + entities: payload.entities || [], + features: payload.features || [], + ttl: payload.ttl, + online: payload.online, + tags: payload.tags || {}, + project: payload.project, + batchSource: payload.batch_source + ? { name: payload.batch_source } + : undefined, + }, + meta: {}, + type: "featureView", +}); + +module.exports = function setupProxy(app) { + app.use("/api/v1", express.json()); + + app.get("/projects-list.json", (_req, res) => { + res.json({ + ...projectsList, + projects: projectsList.projects.map((project) => + project.id === "credit_scoring_aws" + ? { ...project, registryPath: "/api/v1" } + : project, + ), + }); + }); + + app.get("/api/v1/entities/all", (_req, res) => + responseList(res, "entities", state.entities), + ); + app.get("/api/v1/feature_views/all", (_req, res) => + responseList(res, "featureViews", allFeatureViews()), + ); + app.get("/api/v1/feature_services/all", (_req, res) => + responseList(res, "featureServices", state.featureServices), + ); + app.get("/api/v1/data_sources/all", (_req, res) => + responseList(res, "dataSources", state.dataSources), + ); + app.get("/api/v1/saved_datasets/all", (_req, res) => + responseList(res, "savedDatasets", state.savedDatasets), + ); + app.get("/api/v1/features/all", (_req, res) => + responseList(res, "features", allFeatures()), + ); + app.get("/api/v1/label_views/all", (_req, res) => + responseList(res, "featureViews", []), + ); + + app.get("/api/v1/entities", (req, res) => + responseList( + res, + "entities", + filterByProject(state.entities, req.query.project), + ), + ); + app.get("/api/v1/feature_views", (req, res) => + responseList( + res, + "featureViews", + filterByProject(allFeatureViews(), req.query.project), + ), + ); + app.get("/api/v1/feature_services", (req, res) => + responseList( + res, + "featureServices", + filterByProject(state.featureServices, req.query.project), + ), + ); + app.get("/api/v1/data_sources", (req, res) => + responseList( + res, + "dataSources", + filterByProject(state.dataSources, req.query.project), + ), + ); + app.get("/api/v1/saved_datasets", (req, res) => + responseList( + res, + "savedDatasets", + filterByProject(state.savedDatasets, req.query.project), + ), + ); + app.get("/api/v1/features", (req, res) => + responseList(res, "features", allFeatures(req.query.project)), + ); + app.get("/api/v1/label_views", (_req, res) => + responseList(res, "featureViews", []), + ); + app.get("/api/v1/labels", (_req, res) => responseList(res, "labels", [])); + app.get("/api/v1/projects", (_req, res) => + responseList(res, "projects", state.projects), + ); + app.get("/api/v1/permissions", (_req, res) => + responseList(res, "permissions", []), + ); + app.get("/api/v1/metrics/:type", (_req, res) => res.json({})); + + app.get("/api/v1/entities/:name", (req, res) => { + const entity = findByName(state.entities, req.params.name); + if (!entity) return res.status(404).json({ detail: "Not found" }); + return res.json(entity); + }); + app.get("/api/v1/feature_views/:name", (req, res) => { + const featureView = findByName(allFeatureViews(), req.params.name); + if (!featureView) return res.status(404).json({ detail: "Not found" }); + return res.json(featureView); + }); + app.get("/api/v1/feature_services/:name", (req, res) => { + const featureService = findByName(state.featureServices, req.params.name); + if (!featureService) return res.status(404).json({ detail: "Not found" }); + return res.json(featureService); + }); + app.get("/api/v1/data_sources/:name", (req, res) => { + const dataSource = findByName(state.dataSources, req.params.name); + if (!dataSource) return res.status(404).json({ detail: "Not found" }); + return res.json(dataSource); + }); + app.get("/api/v1/saved_datasets/:name", (req, res) => { + const savedDataset = findByName(state.savedDatasets, req.params.name); + if (!savedDataset) return res.status(404).json({ detail: "Not found" }); + return res.json(savedDataset); + }); + app.get("/api/v1/features/:fvName/:featureName", (req, res) => { + const featureView = findByName(allFeatureViews(), req.params.fvName); + const feature = featureView?.spec?.features?.find( + (f) => f.name === req.params.featureName, + ); + if (!feature) return res.status(404).json({ detail: "Not found" }); + return res.json({ + featureViewName: req.params.fvName, + featureName: req.params.featureName, + feature, + featureView, + }); + }); + + app.post("/api/v1/entities", (req, res) => { + const body = req.body || {}; + const existingIndex = state.entities.findIndex( + (entity) => entity?.spec?.name === body.name, + ); + const entity = entityPayloadToResource(body); + if (existingIndex >= 0) { + state.entities[existingIndex] = entity; + } else { + state.entities.push(entity); + } + res.json({ + name: body.name, + project: body.project, + status: "applied", + }); + }); + + app.post("/api/v1/data_sources", (req, res) => { + const body = req.body || {}; + const existingIndex = state.dataSources.findIndex( + (dataSource) => dataSource?.name === body.name, + ); + const dataSource = dataSourcePayloadToResource(body); + if (existingIndex >= 0) { + state.dataSources[existingIndex] = dataSource; + } else { + state.dataSources.push(dataSource); + } + res.json({ + name: body.name, + project: body.project, + status: "applied", + }); + }); + + app.post("/api/v1/feature_views", (req, res) => { + const body = req.body || {}; + const existingIndex = state.featureViews.findIndex( + (featureView) => featureView?.spec?.name === body.name, + ); + const featureView = featureViewPayloadToResource(body); + if (existingIndex >= 0) { + state.featureViews[existingIndex] = featureView; + } else { + state.featureViews.push(featureView); + } + res.json({ + name: body.name, + project: body.project, + status: "applied", + }); + }); + + app.delete("/api/v1/entities/:name", (req, res) => { + state.entities = state.entities.filter( + (entity) => entity?.spec?.name !== req.params.name, + ); + res.json({ + name: req.params.name, + project: req.query.project, + status: "deleted", + }); + }); + + app.delete("/api/v1/data_sources/:name", (req, res) => { + state.dataSources = state.dataSources.filter( + (dataSource) => dataSource?.name !== req.params.name, + ); + res.json({ + name: req.params.name, + project: req.query.project, + status: "deleted", + }); + }); + + app.delete("/api/v1/feature_views/:name", (req, res) => { + state.featureViews = state.featureViews.filter( + (featureView) => featureView?.spec?.name !== req.params.name, + ); + res.json({ + name: req.params.name, + project: req.query.project, + status: "deleted", + }); + }); +}; diff --git a/ui/src/utils/permissionUtils.ts b/ui/src/utils/permissionUtils.ts index 9caa162ec1c..2f1f8e06628 100644 --- a/ui/src/utils/permissionUtils.ts +++ b/ui/src/utils/permissionUtils.ts @@ -1,5 +1,15 @@ import { FEAST_FCO_TYPES } from "../parsers/types"; -import { feast } from "../protos"; + +/** + * Test if a regex pattern is potentially vulnerable to catastrophic backtracking. + * Rejects patterns with nested quantifiers like (a+)+ or (a*)* + */ +const isSafePattern = (pattern: string): boolean => { + if (pattern.length > 1000) return false; + // Reject nested quantifiers: a quantifier applied to a group containing a quantifier + if (/(\([^)]*[+*][^)]*\))[+*{]/.test(pattern)) return false; + return true; +}; /** * Get permissions for a specific entity @@ -43,6 +53,9 @@ export const getEntityPermissions = ( matchesName = true; // If no name patterns, matches all names } else { matchesName = permission.spec?.name_patterns?.some((pattern: string) => { + if (!pattern || !isSafePattern(pattern)) { + return pattern === entityName; + } try { const regex = new RegExp(pattern); return regex.test(entityName); diff --git a/ui/src/utils/timestamp.ts b/ui/src/utils/timestamp.ts index 390195f467b..5c85bc0de65 100644 --- a/ui/src/utils/timestamp.ts +++ b/ui/src/utils/timestamp.ts @@ -1,13 +1,20 @@ import Long from "long"; import { google } from "../protos"; -export function toDate(ts: google.protobuf.ITimestamp) { - var seconds: number; - if (ts.seconds instanceof Long) { - seconds = ts.seconds.low; - } else { - seconds = ts.seconds!; +export function toDate(ts: google.protobuf.ITimestamp | string | any): Date { + if (typeof ts === "string") { + return new Date(ts); } - return new Date(seconds * 1000); + if (ts && ts.seconds != null) { + var seconds: number; + if (ts.seconds instanceof Long) { + seconds = ts.seconds.low; + } else { + seconds = ts.seconds; + } + return new Date(seconds * 1000); + } + + return new Date(NaN); } diff --git a/ui/yarn.lock b/ui/yarn.lock index 3c378b59918..cb5589a81f9 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -4,20 +4,12 @@ "@adobe/css-tools@^4.4.0": version "4.4.4" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.4.tgz#2856c55443d3d461693f32d2b96fb6ea92e1ffa9" + resolved "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz" integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - "@apideck/better-ajv-errors@^0.3.1": version "0.3.6" - resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" + resolved "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz" integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== dependencies: json-schema "^0.4.0" @@ -26,33 +18,33 @@ "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.8.3": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== dependencies: "@babel/helper-validator-identifier" "^7.27.1" js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790" - integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== +"@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz" + integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== "@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.21.3", "@babel/core@^7.23.9", "@babel/core@^7.25.8": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.3.tgz#aceddde69c5d1def69b839d09efa3e3ff59c97cb" - integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ== + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz" + integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== dependencies: - "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.3" + "@babel/generator" "^7.28.5" "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-module-transforms" "^7.28.3" - "@babel/helpers" "^7.28.3" - "@babel/parser" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.5" "@babel/template" "^7.27.2" - "@babel/traverse" "^7.28.3" - "@babel/types" "^7.28.2" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -60,35 +52,35 @@ semver "^6.3.1" "@babel/eslint-parser@^7.16.3": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.28.0.tgz#c1b3fbba070f5bac32e3d02f244201add4afdd6e" - integrity sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w== + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.5.tgz" + integrity sha512-fcdRcWahONYo+JRnJg1/AekOacGvKx12Gu0qXJXFi2WBqQA1i7+O5PaxRB7kxE/Op94dExnCiiar6T09pvdHpA== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" semver "^6.3.1" -"@babel/generator@^7.28.3", "@babel/generator@^7.7.2": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" - integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== +"@babel/generator@^7.28.5", "@babel/generator@^7.7.2": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz" + integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== dependencies: - "@babel/parser" "^7.28.3" - "@babel/types" "^7.28.2" + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" "@jridgewell/gen-mapping" "^0.3.12" "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" "@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz" integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== dependencies: "@babel/types" "^7.27.3" "@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz" integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== dependencies: "@babel/compat-data" "^7.27.2" @@ -97,31 +89,31 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz#3e747434ea007910c320c4d39a6b46f20f371d46" - integrity sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg== +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3", "@babel/helper-create-class-features-plugin@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz" + integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" - "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-member-expression-to-functions" "^7.28.5" "@babel/helper-optimise-call-expression" "^7.27.1" "@babel/helper-replace-supers" "^7.27.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" - "@babel/traverse" "^7.28.3" + "@babel/traverse" "^7.28.5" semver "^6.3.1" "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" - integrity sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ== + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz" + integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== dependencies: - "@babel/helper-annotate-as-pure" "^7.27.1" - regexpu-core "^6.2.0" + "@babel/helper-annotate-as-pure" "^7.27.3" + regexpu-core "^6.3.1" semver "^6.3.1" "@babel/helper-define-polyfill-provider@^0.6.5": version "0.6.5" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz#742ccf1cb003c07b48859fc9fa2c1bbe40e5f753" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz" integrity sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg== dependencies: "@babel/helper-compilation-targets" "^7.27.2" @@ -132,20 +124,20 @@ "@babel/helper-globals@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== -"@babel/helper-member-expression-to-functions@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" - integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA== +"@babel/helper-member-expression-to-functions@^7.27.1", "@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== dependencies: - "@babel/traverse" "^7.27.1" - "@babel/types" "^7.27.1" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz" integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== dependencies: "@babel/traverse" "^7.27.1" @@ -153,7 +145,7 @@ "@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz" integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== dependencies: "@babel/helper-module-imports" "^7.27.1" @@ -162,19 +154,19 @@ "@babel/helper-optimise-call-expression@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz" integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== dependencies: "@babel/types" "^7.27.1" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz" integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== "@babel/helper-remap-async-to-generator@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz" integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.1" @@ -183,7 +175,7 @@ "@babel/helper-replace-supers@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz" integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== dependencies: "@babel/helper-member-expression-to-functions" "^7.27.1" @@ -192,7 +184,7 @@ "@babel/helper-skip-transparent-expression-wrappers@^7.20.0", "@babel/helper-skip-transparent-expression-wrappers@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz" integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== dependencies: "@babel/traverse" "^7.27.1" @@ -200,68 +192,68 @@ "@babel/helper-string-parser@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@babel/helper-validator-identifier@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" - integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== +"@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== "@babel/helper-validator-option@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== "@babel/helper-wrap-function@^7.27.1": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz#fe4872092bc1438ffd0ce579e6f699609f9d0a7a" + resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz" integrity sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g== dependencies: "@babel/template" "^7.27.2" "@babel/traverse" "^7.28.3" "@babel/types" "^7.28.2" -"@babel/helpers@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.3.tgz#b83156c0a2232c133d1b535dd5d3452119c7e441" - integrity sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw== +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== dependencies: "@babel/template" "^7.27.2" - "@babel/types" "^7.28.2" + "@babel/types" "^7.28.4" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.15", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" - integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.15", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== dependencies: - "@babel/types" "^7.28.2" + "@babel/types" "^7.28.5" -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" - integrity sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz" + integrity sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" - "@babel/traverse" "^7.27.1" + "@babel/traverse" "^7.28.5" "@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz" integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz" integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz" integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -270,7 +262,7 @@ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz#373f6e2de0016f73caf8f27004f61d167743742a" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz" integrity sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -278,7 +270,7 @@ "@babel/plugin-proposal-class-properties@^7.16.0": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz" integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== dependencies: "@babel/helper-create-class-features-plugin" "^7.18.6" @@ -286,7 +278,7 @@ "@babel/plugin-proposal-decorators@^7.16.4": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz#419c8acc31088e05a774344c021800f7ddc39bf0" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz" integrity sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg== dependencies: "@babel/helper-create-class-features-plugin" "^7.27.1" @@ -295,7 +287,7 @@ "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz" integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== dependencies: "@babel/helper-plugin-utils" "^7.18.6" @@ -303,7 +295,7 @@ "@babel/plugin-proposal-numeric-separator@^7.16.0": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz" integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== dependencies: "@babel/helper-plugin-utils" "^7.18.6" @@ -311,7 +303,7 @@ "@babel/plugin-proposal-optional-chaining@^7.16.0": version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz#886f5c8978deb7d30f678b2e24346b287234d3ea" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz" integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== dependencies: "@babel/helper-plugin-utils" "^7.20.2" @@ -320,7 +312,7 @@ "@babel/plugin-proposal-private-methods@^7.16.0": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz" integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== dependencies: "@babel/helper-create-class-features-plugin" "^7.18.6" @@ -328,12 +320,12 @@ "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz" integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== "@babel/plugin-proposal-private-property-in-object@^7.16.7", "@babel/plugin-proposal-private-property-in-object@^7.21.11": version "7.21.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz" integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw== dependencies: "@babel/helper-annotate-as-pure" "^7.18.6" @@ -343,147 +335,147 @@ "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-bigint@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-class-static-block@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-decorators@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz#ee7dd9590aeebc05f9d4c8c0560007b05979a63d" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz" integrity sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-flow@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz#6c83cf0d7d635b716827284b7ecd5aead9237662" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz" integrity sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-import-assertions@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz#88894aefd2b03b5ee6ad1562a7c8e1587496aecd" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz" integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-import-attributes@^7.24.7", "@babel/plugin-syntax-import-attributes@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz" integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-jsx@^7.27.1", "@babel/plugin-syntax-jsx@^7.7.2": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz" integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-private-property-in-object@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-top-level-await@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.27.1", "@babel/plugin-syntax-typescript@^7.7.2": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz" integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz" integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.18.6" @@ -491,14 +483,14 @@ "@babel/plugin-transform-arrow-functions@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz" integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-async-generator-functions@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz#1276e6c7285ab2cd1eccb0bc7356b7a69ff842c2" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz" integrity sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -507,7 +499,7 @@ "@babel/plugin-transform-async-to-generator@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz" integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== dependencies: "@babel/helper-module-imports" "^7.27.1" @@ -516,21 +508,21 @@ "@babel/plugin-transform-block-scoped-functions@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz" integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-block-scoping@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz#e7c50cbacc18034f210b93defa89638666099451" - integrity sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q== +"@babel/plugin-transform-block-scoping@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz" + integrity sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-class-properties@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + resolved "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz" integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== dependencies: "@babel/helper-create-class-features-plugin" "^7.27.1" @@ -538,43 +530,43 @@ "@babel/plugin-transform-class-static-block@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz#d1b8e69b54c9993bc558203e1f49bfc979bfd852" + resolved "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz" integrity sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg== dependencies: "@babel/helper-create-class-features-plugin" "^7.28.3" "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-classes@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz#598297260343d0edbd51cb5f5075e07dee91963a" - integrity sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg== +"@babel/plugin-transform-classes@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz" + integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-globals" "^7.28.0" "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-replace-supers" "^7.27.1" - "@babel/traverse" "^7.28.3" + "@babel/traverse" "^7.28.4" "@babel/plugin-transform-computed-properties@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" + resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz" integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/template" "^7.27.1" -"@babel/plugin-transform-destructuring@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz#0f156588f69c596089b7d5b06f5af83d9aa7f97a" - integrity sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A== +"@babel/plugin-transform-destructuring@^7.28.0", "@babel/plugin-transform-destructuring@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz" + integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" - "@babel/traverse" "^7.28.0" + "@babel/traverse" "^7.28.5" "@babel/plugin-transform-dotall-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz#aa6821de864c528b1fecf286f0a174e38e826f4d" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz" integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -582,14 +574,14 @@ "@babel/plugin-transform-duplicate-keys@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz" integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz#5043854ca620a94149372e69030ff8cb6a9eb0ec" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz" integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -597,36 +589,36 @@ "@babel/plugin-transform-dynamic-import@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz" integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-explicit-resource-management@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz#45be6211b778dbf4b9d54c4e8a2b42fa72e09a1a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz" integrity sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-destructuring" "^7.28.0" -"@babel/plugin-transform-exponentiation-operator@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz#fc497b12d8277e559747f5a3ed868dd8064f83e1" - integrity sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ== +"@babel/plugin-transform-exponentiation-operator@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz" + integrity sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-export-namespace-from@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + resolved "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz" integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-flow-strip-types@^7.16.0": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz#5def3e1e7730f008d683144fb79b724f92c5cdf9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz" integrity sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -634,7 +626,7 @@ "@babel/plugin-transform-for-of@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz" integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -642,7 +634,7 @@ "@babel/plugin-transform-function-name@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz" integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== dependencies: "@babel/helper-compilation-targets" "^7.27.1" @@ -651,35 +643,35 @@ "@babel/plugin-transform-json-strings@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz#a2e0ce6ef256376bd527f290da023983527a4f4c" + resolved "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz" integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-literals@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz" integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-logical-assignment-operators@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz#890cb20e0270e0e5bebe3f025b434841c32d5baa" - integrity sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw== +"@babel/plugin-transform-logical-assignment-operators@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz" + integrity sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-member-expression-literals@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz" integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-modules-amd@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz" integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== dependencies: "@babel/helper-module-transforms" "^7.27.1" @@ -687,25 +679,25 @@ "@babel/plugin-transform-modules-commonjs@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz" integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== dependencies: "@babel/helper-module-transforms" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-systemjs@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz#00e05b61863070d0f3292a00126c16c0e024c4ed" - integrity sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA== +"@babel/plugin-transform-modules-systemjs@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz" + integrity sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew== dependencies: - "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-module-transforms" "^7.28.3" "@babel/helper-plugin-utils" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" - "@babel/traverse" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.5" "@babel/plugin-transform-modules-umd@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz" integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== dependencies: "@babel/helper-module-transforms" "^7.27.1" @@ -713,7 +705,7 @@ "@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz" integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -721,39 +713,39 @@ "@babel/plugin-transform-new-target@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz" integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + resolved "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz" integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-numeric-separator@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz" integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-object-rest-spread@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz#d23021857ffd7cd809f54d624299b8086402ed8d" - integrity sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA== +"@babel/plugin-transform-object-rest-spread@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz" + integrity sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew== dependencies: "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-destructuring" "^7.28.0" "@babel/plugin-transform-parameters" "^7.27.7" - "@babel/traverse" "^7.28.0" + "@babel/traverse" "^7.28.4" "@babel/plugin-transform-object-super@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz" integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -761,29 +753,29 @@ "@babel/plugin-transform-optional-catch-binding@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" + resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz" integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-optional-chaining@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f" - integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg== +"@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz" + integrity sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" "@babel/plugin-transform-parameters@^7.27.7": version "7.27.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" + resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz" integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-private-methods@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" + resolved "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz" integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== dependencies: "@babel/helper-create-class-features-plugin" "^7.27.1" @@ -791,7 +783,7 @@ "@babel/plugin-transform-private-property-in-object@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" + resolved "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz" integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== dependencies: "@babel/helper-annotate-as-pure" "^7.27.1" @@ -800,35 +792,35 @@ "@babel/plugin-transform-property-literals@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz" integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-react-constant-elements@^7.21.3": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz#6c6b50424e749a6e48afd14cf7b92f98cb9383f9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz" integrity sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug== dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.27.1": +"@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz#6f20a7295fea7df42eb42fed8f896813f5b934de" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz" integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-react-jsx-development@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz#47ff95940e20a3a70e68ad3d4fcb657b647f6c98" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz" integrity sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q== dependencies: "@babel/plugin-transform-react-jsx" "^7.27.1" "@babel/plugin-transform-react-jsx@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz#1023bc94b78b0a2d68c82b5e96aed573bcfb9db0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz" integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== dependencies: "@babel/helper-annotate-as-pure" "^7.27.1" @@ -839,22 +831,22 @@ "@babel/plugin-transform-react-pure-annotations@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz#339f1ce355eae242e0649f232b1c68907c02e879" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz" integrity sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regenerator@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz#b8eee0f8aed37704bbcc932fd0b1a0a34d0b7344" - integrity sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A== +"@babel/plugin-transform-regenerator@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz" + integrity sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-regexp-modifiers@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz" integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -862,15 +854,15 @@ "@babel/plugin-transform-reserved-words@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz" integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-runtime@^7.16.4": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz#f5990a1b2d2bde950ed493915e0719841c8d0eaa" - integrity sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg== + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz" + integrity sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w== dependencies: "@babel/helper-module-imports" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" @@ -881,14 +873,14 @@ "@babel/plugin-transform-shorthand-properties@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz" integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-spread@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz" integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== dependencies: "@babel/helper-plugin-utils" "^7.27.1" @@ -896,46 +888,46 @@ "@babel/plugin-transform-sticky-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz" integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-template-literals@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz" integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-typeof-symbol@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz" integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-typescript@^7.27.1": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz#796cbd249ab56c18168b49e3e1d341b72af04a6b" - integrity sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg== +"@babel/plugin-transform-typescript@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz" + integrity sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA== dependencies: "@babel/helper-annotate-as-pure" "^7.27.3" - "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.28.5" "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" "@babel/plugin-syntax-typescript" "^7.27.1" "@babel/plugin-transform-unicode-escapes@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz" integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-unicode-property-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz#bdfe2d3170c78c5691a3c3be934c8c0087525956" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz" integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -943,7 +935,7 @@ "@babel/plugin-transform-unicode-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz" integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" @@ -951,22 +943,22 @@ "@babel/plugin-transform-unicode-sets-regex@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz#6ab706d10f801b5c72da8bb2548561fa04193cd1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz" integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.27.1" "@babel/helper-plugin-utils" "^7.27.1" "@babel/preset-env@^7.11.0", "@babel/preset-env@^7.16.4", "@babel/preset-env@^7.20.2", "@babel/preset-env@^7.25.8": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.28.3.tgz#2b18d9aff9e69643789057ae4b942b1654f88187" - integrity sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg== + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz" + integrity sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg== dependencies: - "@babel/compat-data" "^7.28.0" + "@babel/compat-data" "^7.28.5" "@babel/helper-compilation-targets" "^7.27.2" "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-validator-option" "^7.27.1" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.28.5" "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" @@ -979,42 +971,42 @@ "@babel/plugin-transform-async-generator-functions" "^7.28.0" "@babel/plugin-transform-async-to-generator" "^7.27.1" "@babel/plugin-transform-block-scoped-functions" "^7.27.1" - "@babel/plugin-transform-block-scoping" "^7.28.0" + "@babel/plugin-transform-block-scoping" "^7.28.5" "@babel/plugin-transform-class-properties" "^7.27.1" "@babel/plugin-transform-class-static-block" "^7.28.3" - "@babel/plugin-transform-classes" "^7.28.3" + "@babel/plugin-transform-classes" "^7.28.4" "@babel/plugin-transform-computed-properties" "^7.27.1" - "@babel/plugin-transform-destructuring" "^7.28.0" + "@babel/plugin-transform-destructuring" "^7.28.5" "@babel/plugin-transform-dotall-regex" "^7.27.1" "@babel/plugin-transform-duplicate-keys" "^7.27.1" "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" "@babel/plugin-transform-dynamic-import" "^7.27.1" "@babel/plugin-transform-explicit-resource-management" "^7.28.0" - "@babel/plugin-transform-exponentiation-operator" "^7.27.1" + "@babel/plugin-transform-exponentiation-operator" "^7.28.5" "@babel/plugin-transform-export-namespace-from" "^7.27.1" "@babel/plugin-transform-for-of" "^7.27.1" "@babel/plugin-transform-function-name" "^7.27.1" "@babel/plugin-transform-json-strings" "^7.27.1" "@babel/plugin-transform-literals" "^7.27.1" - "@babel/plugin-transform-logical-assignment-operators" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.28.5" "@babel/plugin-transform-member-expression-literals" "^7.27.1" "@babel/plugin-transform-modules-amd" "^7.27.1" "@babel/plugin-transform-modules-commonjs" "^7.27.1" - "@babel/plugin-transform-modules-systemjs" "^7.27.1" + "@babel/plugin-transform-modules-systemjs" "^7.28.5" "@babel/plugin-transform-modules-umd" "^7.27.1" "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" "@babel/plugin-transform-new-target" "^7.27.1" "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" "@babel/plugin-transform-numeric-separator" "^7.27.1" - "@babel/plugin-transform-object-rest-spread" "^7.28.0" + "@babel/plugin-transform-object-rest-spread" "^7.28.4" "@babel/plugin-transform-object-super" "^7.27.1" "@babel/plugin-transform-optional-catch-binding" "^7.27.1" - "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.28.5" "@babel/plugin-transform-parameters" "^7.27.7" "@babel/plugin-transform-private-methods" "^7.27.1" "@babel/plugin-transform-private-property-in-object" "^7.27.1" "@babel/plugin-transform-property-literals" "^7.27.1" - "@babel/plugin-transform-regenerator" "^7.28.3" + "@babel/plugin-transform-regenerator" "^7.28.4" "@babel/plugin-transform-regexp-modifiers" "^7.27.1" "@babel/plugin-transform-reserved-words" "^7.27.1" "@babel/plugin-transform-shorthand-properties" "^7.27.1" @@ -1035,7 +1027,7 @@ "@babel/preset-modules@0.1.6-no-external-plugins": version "0.1.6-no-external-plugins" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz" integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -1043,103 +1035,81 @@ esutils "^2.0.2" "@babel/preset-react@^7.16.0", "@babel/preset-react@^7.18.6", "@babel/preset-react@^7.25.7": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.27.1.tgz#86ea0a5ca3984663f744be2fd26cb6747c3fd0ec" - integrity sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA== + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz" + integrity sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-validator-option" "^7.27.1" - "@babel/plugin-transform-react-display-name" "^7.27.1" + "@babel/plugin-transform-react-display-name" "^7.28.0" "@babel/plugin-transform-react-jsx" "^7.27.1" "@babel/plugin-transform-react-jsx-development" "^7.27.1" "@babel/plugin-transform-react-pure-annotations" "^7.27.1" "@babel/preset-typescript@^7.16.0", "@babel/preset-typescript@^7.21.0": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912" - integrity sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ== + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz" + integrity sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/helper-validator-option" "^7.27.1" "@babel/plugin-syntax-jsx" "^7.27.1" "@babel/plugin-transform-modules-commonjs" "^7.27.1" - "@babel/plugin-transform-typescript" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.28.5" "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.8", "@babel/runtime@^7.24.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.9.2": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.3.tgz#75c5034b55ba868121668be5d5bb31cc64e6e61a" - integrity sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA== + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== "@babel/template@^7.27.1", "@babel/template@^7.27.2", "@babel/template@^7.3.3": version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== dependencies: "@babel/code-frame" "^7.27.1" "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" -"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.3.tgz#6911a10795d2cce43ec6a28cffc440cca2593434" - integrity sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ== +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== dependencies: "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.3" + "@babel/generator" "^7.28.5" "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.3" + "@babel/parser" "^7.28.5" "@babel/template" "^7.27.2" - "@babel/types" "^7.28.2" + "@babel/types" "^7.28.5" debug "^4.3.1" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.28.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" - integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== dependencies: "@babel/helper-string-parser" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" "@base2/pretty-print-object@1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" + resolved "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz" integrity sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA== "@bcoe/v8-coverage@^0.2.3": version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bundled-es-modules/cookie@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz#b41376af6a06b3e32a15241d927b840a9b4de507" - integrity sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw== - dependencies: - cookie "^0.7.2" - -"@bundled-es-modules/statuses@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz#761d10f44e51a94902c4da48675b71a76cc98872" - integrity sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg== - dependencies: - statuses "^2.0.1" - -"@bundled-es-modules/tough-cookie@^0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz#fa9cd3cedfeecd6783e8b0d378b4a99e52bde5d3" - integrity sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw== - dependencies: - "@types/tough-cookie" "^4.0.5" - tough-cookie "^4.1.4" - "@csstools/normalize.css@*": version "12.1.1" - resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.1.1.tgz#f0ad221b7280f3fc814689786fd9ee092776ef8f" + resolved "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz" integrity sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ== "@csstools/postcss-cascade-layers@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz#8a997edf97d34071dd2e37ea6022447dd9e795ad" + resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz" integrity sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA== dependencies: "@csstools/selector-specificity" "^2.0.2" @@ -1147,7 +1117,7 @@ "@csstools/postcss-color-function@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz#2bd36ab34f82d0497cfacdc9b18d34b5e6f64b6b" + resolved "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz" integrity sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -1155,21 +1125,21 @@ "@csstools/postcss-font-format-keywords@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz#677b34e9e88ae997a67283311657973150e8b16a" + resolved "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz" integrity sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-hwb-function@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz#ab54a9fce0ac102c754854769962f2422ae8aa8b" + resolved "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz" integrity sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-ic-unit@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz#28237d812a124d1a16a5acc5c3832b040b303e58" + resolved "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz" integrity sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -1177,7 +1147,7 @@ "@csstools/postcss-is-pseudo-class@^2.0.7": version "2.0.7" - resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz#846ae6c0d5a1eaa878fce352c544f9c295509cd1" + resolved "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz" integrity sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA== dependencies: "@csstools/selector-specificity" "^2.0.0" @@ -1185,21 +1155,21 @@ "@csstools/postcss-nested-calc@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz#d7e9d1d0d3d15cf5ac891b16028af2a1044d0c26" + resolved "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz" integrity sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-normalize-display-values@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz#15da54a36e867b3ac5163ee12c1d7f82d4d612c3" + resolved "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz" integrity sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-oklab-function@^1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz#88cee0fbc8d6df27079ebd2fa016ee261eecf844" + resolved "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz" integrity sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -1207,57 +1177,57 @@ "@csstools/postcss-progressive-custom-properties@^1.1.0", "@csstools/postcss-progressive-custom-properties@^1.3.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz#542292558384361776b45c85226b9a3a34f276fa" + resolved "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz" integrity sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-stepped-value-functions@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz#f8772c3681cc2befed695e2b0b1d68e22f08c4f4" + resolved "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz" integrity sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-text-decoration-shorthand@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz#ea96cfbc87d921eca914d3ad29340d9bcc4c953f" + resolved "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz" integrity sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-trigonometric-functions@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz#94d3e4774c36d35dcdc88ce091336cb770d32756" + resolved "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz" integrity sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-unset-value@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz#c99bb70e2cdc7312948d1eb41df2412330b81f77" + resolved "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz" integrity sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g== "@csstools/selector-specificity@^2.0.0", "@csstools/selector-specificity@^2.0.2": version "2.2.0" - resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016" + resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz" integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== "@elastic/datemath@^5.0.3": version "5.0.3" - resolved "https://registry.yarnpkg.com/@elastic/datemath/-/datemath-5.0.3.tgz#7baccdab672b9a3ecb7fe8387580670936b58573" + resolved "https://registry.npmjs.org/@elastic/datemath/-/datemath-5.0.3.tgz" integrity sha512-8Hbr1Uyjm5OcYBfEB60K7sCP6U3IXuWDaLaQmYv3UxgI4jqBWbakoemwWvsqPVUvnwEjuX6z7ghPZbefs8xiaA== dependencies: tslib "^1.9.3" "@elastic/eui-theme-borealis@1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@elastic/eui-theme-borealis/-/eui-theme-borealis-1.0.0.tgz#f85679d2d72dfc43a620241cbf4161d4e4e81841" + resolved "https://registry.npmjs.org/@elastic/eui-theme-borealis/-/eui-theme-borealis-1.0.0.tgz" integrity sha512-Zf3ZX5siUhF+TNOdP0FZ3PNEpVmfe3DDXFm5biAKFlGp4e5yrR1FKPYOzkOdJtPWlOoNaedawnALXNVjp1UH8w== "@elastic/eui@^95.12.0": version "95.12.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-95.12.0.tgz#862f2be8b72248a62b40704b9e62f2f5d7d43853" + resolved "https://registry.npmjs.org/@elastic/eui/-/eui-95.12.0.tgz" integrity sha512-SW4ru97FY2VitSqyCgURrM5OMk1W+Ww12b6S+VZN5ex50aNT296DfED/ByidlYaAoVihqjZuoB3HlQBBXydFpA== dependencies: "@hello-pangea/dnd" "^16.6.0" @@ -1296,7 +1266,7 @@ "@emotion/babel-plugin@^11.13.5": version "11.13.5" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz#eab8d65dbded74e0ecfd28dc218e75607c4e7bc0" + resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz" integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== dependencies: "@babel/helper-module-imports" "^7.16.7" @@ -1313,7 +1283,7 @@ "@emotion/cache@^11.13.5", "@emotion/cache@^11.14.0": version "11.14.0" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.14.0.tgz#ee44b26986eeb93c8be82bb92f1f7a9b21b2ed76" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz" integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== dependencies: "@emotion/memoize" "^0.9.0" @@ -1324,7 +1294,7 @@ "@emotion/css@^11.13.0": version "11.13.5" - resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.13.5.tgz#db2d3be6780293640c082848e728a50544b9dfa4" + resolved "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz" integrity sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w== dependencies: "@emotion/babel-plugin" "^11.13.5" @@ -1335,29 +1305,29 @@ "@emotion/hash@^0.9.2": version "0.9.2" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz" integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== "@emotion/is-prop-valid@1.2.2": version "1.2.2" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz" integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== dependencies: "@emotion/memoize" "^0.8.1" "@emotion/memoize@^0.8.1": version "0.8.1" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz" integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== "@emotion/memoize@^0.9.0": version "0.9.0" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz" integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== "@emotion/react@^11.13.3": version "11.14.0" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.14.0.tgz#cfaae35ebc67dd9ef4ea2e9acc6cd29e157dd05d" + resolved "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz" integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== dependencies: "@babel/runtime" "^7.18.3" @@ -1371,7 +1341,7 @@ "@emotion/serialize@^1.3.3": version "1.3.3" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.3.tgz#d291531005f17d704d0463a032fe679f376509e8" + resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz" integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== dependencies: "@emotion/hash" "^0.9.2" @@ -1382,49 +1352,49 @@ "@emotion/sheet@^1.4.0": version "1.4.0" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz" integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== "@emotion/unitless@0.8.1": version "0.8.1" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz" integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== "@emotion/unitless@^0.10.0": version "0.10.0" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz" integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== "@emotion/use-insertion-effect-with-fallbacks@^1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz#8a8cb77b590e09affb960f4ff1e9a89e532738bf" + resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz" integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== "@emotion/utils@^1.4.2": version "1.4.2" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.2.tgz#6df6c45881fcb1c412d6688a311a98b7f59c1b52" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz" integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== "@emotion/weak-memoize@^0.4.0": version "0.4.0" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== "@eslint-community/eslint-utils@^4.2.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" - integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + version "4.9.0" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz" + integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== dependencies: eslint-visitor-keys "^3.4.3" "@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": - version "4.12.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" - integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + version "4.12.2" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== "@eslint/eslintrc@^2.1.4": version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz" integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" @@ -1439,12 +1409,12 @@ "@eslint/js@8.57.1": version "8.57.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== "@happy-dom/jest-environment@^16.7.3": version "16.8.1" - resolved "https://registry.yarnpkg.com/@happy-dom/jest-environment/-/jest-environment-16.8.1.tgz#c2c49923d7aec45123361d7756615884fddb4756" + resolved "https://registry.npmjs.org/@happy-dom/jest-environment/-/jest-environment-16.8.1.tgz" integrity sha512-TNzYvCYyNySVM+an4fM5l4FIiRtBFiu7m5eyiXtIDWHkS3WLGvJ9yrRkzHtSEqDA7YOYdTIsEn5SQNQ7LCNn0Q== dependencies: "@jest/environment" "^29.4.0" @@ -1456,7 +1426,7 @@ "@hello-pangea/dnd@^16.6.0": version "16.6.0" - resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.6.0.tgz#7509639c7bd13f55e537b65a9dcfcd54e7c99ac7" + resolved "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-16.6.0.tgz" integrity sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ== dependencies: "@babel/runtime" "^7.24.1" @@ -1469,7 +1439,7 @@ "@humanwhocodes/config-array@^0.13.0": version "0.13.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz" integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: "@humanwhocodes/object-schema" "^2.0.3" @@ -1478,61 +1448,66 @@ "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/object-schema@^2.0.3": version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@inquirer/ansi@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz" + integrity sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ== + "@inquirer/confirm@^5.0.0": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.14.tgz#e6321edf51a3a5f54dc548b80ef6ba89891351ad" - integrity sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q== + version "5.1.21" + resolved "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz" + integrity sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ== dependencies: - "@inquirer/core" "^10.1.15" - "@inquirer/type" "^3.0.8" + "@inquirer/core" "^10.3.2" + "@inquirer/type" "^3.0.10" -"@inquirer/core@^10.1.15": - version "10.1.15" - resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.15.tgz#8feb69fd536786181a2b6bfb84d8674faa9d2e59" - integrity sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA== +"@inquirer/core@^10.3.2": + version "10.3.2" + resolved "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz" + integrity sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A== dependencies: - "@inquirer/figures" "^1.0.13" - "@inquirer/type" "^3.0.8" - ansi-escapes "^4.3.2" + "@inquirer/ansi" "^1.0.2" + "@inquirer/figures" "^1.0.15" + "@inquirer/type" "^3.0.10" cli-width "^4.1.0" mute-stream "^2.0.0" signal-exit "^4.1.0" wrap-ansi "^6.2.0" - yoctocolors-cjs "^2.1.2" + yoctocolors-cjs "^2.1.3" -"@inquirer/figures@^1.0.13": - version "1.0.13" - resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.13.tgz#ad0afd62baab1c23175115a9b62f511b6a751e45" - integrity sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw== +"@inquirer/figures@^1.0.15": + version "1.0.15" + resolved "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz" + integrity sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g== -"@inquirer/type@^3.0.8": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.8.tgz#efc293ba0ed91e90e6267f1aacc1c70d20b8b4e8" - integrity sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw== +"@inquirer/type@^3.0.10": + version "3.0.10" + resolved "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz" + integrity sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA== "@isaacs/balanced-match@^4.0.1": version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + resolved "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz" integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== "@isaacs/brace-expansion@^5.0.0": version "5.0.0" - resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + resolved "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz" integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== dependencies: "@isaacs/balanced-match" "^4.0.1" "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== dependencies: camelcase "^5.3.1" @@ -1543,12 +1518,12 @@ "@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== "@jest/console@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + resolved "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz" integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== dependencies: "@jest/types" "^29.6.3" @@ -1560,7 +1535,7 @@ "@jest/core@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + resolved "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz" integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== dependencies: "@jest/console" "^29.7.0" @@ -1594,7 +1569,7 @@ "@jest/environment@^29.4.0", "@jest/environment@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz" integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== dependencies: "@jest/fake-timers" "^29.7.0" @@ -1604,14 +1579,14 @@ "@jest/expect-utils@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz" integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== dependencies: jest-get-type "^29.6.3" "@jest/expect@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + resolved "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz" integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== dependencies: expect "^29.7.0" @@ -1619,7 +1594,7 @@ "@jest/fake-timers@^29.4.0", "@jest/fake-timers@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz" integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== dependencies: "@jest/types" "^29.6.3" @@ -1631,7 +1606,7 @@ "@jest/globals@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz" integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== dependencies: "@jest/environment" "^29.7.0" @@ -1641,7 +1616,7 @@ "@jest/reporters@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz" integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== dependencies: "@bcoe/v8-coverage" "^0.2.3" @@ -1671,14 +1646,14 @@ "@jest/schemas@^29.6.3": version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== dependencies: "@sinclair/typebox" "^0.27.8" "@jest/source-map@^29.6.3": version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz" integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== dependencies: "@jridgewell/trace-mapping" "^0.3.18" @@ -1687,7 +1662,7 @@ "@jest/test-result@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz" integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== dependencies: "@jest/console" "^29.7.0" @@ -1697,7 +1672,7 @@ "@jest/test-sequencer@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz" integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== dependencies: "@jest/test-result" "^29.7.0" @@ -1707,7 +1682,7 @@ "@jest/transform@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz" integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== dependencies: "@babel/core" "^7.1.0" @@ -1728,7 +1703,7 @@ "@jest/transform@^29.7.0": version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== dependencies: "@babel/core" "^7.11.6" @@ -1749,7 +1724,7 @@ "@jest/types@^27.5.1": version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + resolved "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz" integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" @@ -1760,7 +1735,7 @@ "@jest/types@^29.4.0", "@jest/types@^29.6.3": version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== dependencies: "@jest/schemas" "^29.6.3" @@ -1772,20 +1747,28 @@ "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/source-map@^0.3.3": version "0.3.11" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz" integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== dependencies: "@jridgewell/gen-mapping" "^0.3.5" @@ -1793,40 +1776,40 @@ "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.5" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": - version "0.3.30" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99" - integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q== + version "0.3.31" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" "@jsdoc/salty@^0.2.1": version "0.2.9" - resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.9.tgz#4d8c147f7ca011532681ce86352a77a0178f1dec" + resolved "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz" integrity sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw== dependencies: lodash "^4.17.21" "@leichtgewicht/ip-codec@^2.0.1": version "2.0.5" - resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz" integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== "@mapbox/hast-util-table-cell-style@^0.2.0": version "0.2.1" - resolved "https://registry.yarnpkg.com/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz#b8e92afdd38b668cf0762400de980073d2ade101" + resolved "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz" integrity sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw== dependencies: unist-util-visit "^1.4.1" -"@mswjs/interceptors@^0.39.1": - version "0.39.6" - resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.39.6.tgz#44094a578f20da4749d1a0eaf3cdb7973604004b" - integrity sha512-bndDP83naYYkfayr/qhBHMhk0YGwS1iv6vaEGcr0SQbO0IZtbOPqjKjds/WcG+bJA+1T5vCx6kprKOzn5Bg+Vw== +"@mswjs/interceptors@^0.40.0": + version "0.40.0" + resolved "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz" + integrity sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ== dependencies: "@open-draft/deferred-promise" "^2.2.0" "@open-draft/logger" "^0.3.0" @@ -1837,14 +1820,14 @@ "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" - resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" + resolved "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz" integrity sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg== dependencies: eslint-scope "5.1.1" "@nodelib/fs.scandir@2.1.5": version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" @@ -1852,12 +1835,12 @@ "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" @@ -1865,25 +1848,25 @@ "@open-draft/deferred-promise@^2.2.0": version "2.2.0" - resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" + resolved "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz" integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA== "@open-draft/logger@^0.3.0": version "0.3.0" - resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954" + resolved "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz" integrity sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ== dependencies: is-node-process "^1.2.0" outvariant "^1.4.0" -"@open-draft/until@^2.0.0", "@open-draft/until@^2.1.0": +"@open-draft/until@^2.0.0": version "2.1.0" - resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" + resolved "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz" integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.17" - resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz#8c2f34ca8651df74895422046e11ce5a120e7930" + resolved "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz" integrity sha512-tXDyE1/jzFsHXjhRZQ3hMl0IVhYe5qula43LDWIhVfjp9G/nT5OQY5AORVOrkEGAUltBJOfOWeETbmhm6kHhuQ== dependencies: ansi-html "^0.0.9" @@ -1896,27 +1879,27 @@ "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== "@protobufjs/base64@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz" integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== "@protobufjs/codegen@^2.0.4": version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz" integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== "@protobufjs/eventemitter@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== "@protobufjs/fetch@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz" integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== dependencies: "@protobufjs/aspromise" "^1.1.1" @@ -1924,32 +1907,32 @@ "@protobufjs/float@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz" integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== "@protobufjs/inquire@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz" integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== "@protobufjs/path@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz" integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== "@protobufjs/pool@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz" integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== "@protobufjs/utf8@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== "@reactflow/background@11.3.14": version "11.3.14" - resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.3.14.tgz#778ca30174f3de77fc321459ab3789e66e71a699" + resolved "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz" integrity sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA== dependencies: "@reactflow/core" "11.11.4" @@ -1958,7 +1941,7 @@ "@reactflow/controls@11.2.14": version "11.2.14" - resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.2.14.tgz#508ed2c40d23341b3b0919dd11e76fd49cf850c7" + resolved "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz" integrity sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw== dependencies: "@reactflow/core" "11.11.4" @@ -1967,7 +1950,7 @@ "@reactflow/core@11.11.4": version "11.11.4" - resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.11.4.tgz#89bd86d1862aa1416f3f49926cede7e8c2aab6a7" + resolved "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz" integrity sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q== dependencies: "@types/d3" "^7.4.0" @@ -1982,7 +1965,7 @@ "@reactflow/minimap@11.7.14": version "11.7.14" - resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.7.14.tgz#298d7a63cb1da06b2518c99744f716560c88ca73" + resolved "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz" integrity sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ== dependencies: "@reactflow/core" "11.11.4" @@ -1995,7 +1978,7 @@ "@reactflow/node-resizer@2.2.14": version "2.2.14" - resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz#1810c0ce51aeb936f179466a6660d1e02c7a77a8" + resolved "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz" integrity sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA== dependencies: "@reactflow/core" "11.11.4" @@ -2006,21 +1989,21 @@ "@reactflow/node-toolbar@1.3.14": version "1.3.14" - resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz#c6ffc76f82acacdce654f2160dc9852162d6e7c9" + resolved "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz" integrity sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ== dependencies: "@reactflow/core" "11.11.4" classcat "^5.0.3" zustand "^4.4.1" -"@remix-run/router@1.23.0": - version "1.23.0" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.0.tgz#35390d0e7779626c026b11376da6789eb8389242" - integrity sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA== +"@remix-run/router@1.23.1": + version "1.23.1" + resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz" + integrity sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ== "@rollup/plugin-babel@^5.2.0", "@rollup/plugin-babel@^5.3.1": version "5.3.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz" integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== dependencies: "@babel/helper-module-imports" "^7.10.4" @@ -2028,7 +2011,7 @@ "@rollup/plugin-commonjs@^21.0.2": version "21.1.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-21.1.0.tgz#45576d7b47609af2db87f55a6d4b46e44fc3a553" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-21.1.0.tgz" integrity sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2041,14 +2024,14 @@ "@rollup/plugin-json@^4.1.0": version "4.1.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" + resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz" integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== dependencies: "@rollup/pluginutils" "^3.0.8" "@rollup/plugin-node-resolve@^11.2.1": version "11.2.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz" integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2060,7 +2043,7 @@ "@rollup/plugin-node-resolve@^13.1.3": version "13.3.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz" integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2072,7 +2055,7 @@ "@rollup/plugin-replace@^2.4.1": version "2.4.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" + resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz" integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2080,7 +2063,7 @@ "@rollup/plugin-typescript@^8.3.1": version "8.5.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz#7ea11599a15b0a30fa7ea69ce3b791d41b862515" + resolved "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz" integrity sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ== dependencies: "@rollup/pluginutils" "^3.1.0" @@ -2088,7 +2071,7 @@ "@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0": version "3.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== dependencies: "@types/estree" "0.0.39" @@ -2096,9 +2079,9 @@ picomatch "^2.2.2" "@rollup/pluginutils@^5.1.3": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.2.0.tgz#eac25ca5b0bdda4ba735ddaca5fbf26bd435f602" - integrity sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw== + version "5.3.0" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz" + integrity sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q== dependencies: "@types/estree" "^1.0.0" estree-walker "^2.0.2" @@ -2106,36 +2089,36 @@ "@rtsao/scc@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + resolved "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== "@rushstack/eslint-patch@^1.1.0": - version "1.12.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz#326a7b46f6d4cfa54ae25bb888551697873069b4" - integrity sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw== + version "1.15.0" + resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz" + integrity sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw== "@sinclair/typebox@^0.27.8": version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== "@sinonjs/commons@^3.0.0": version "3.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz" integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== dependencies: type-detect "4.0.8" "@sinonjs/fake-timers@^10.0.2": version "10.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz" integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== dependencies: "@sinonjs/commons" "^3.0.0" "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" - resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" + resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz" integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== dependencies: ejs "^3.1.6" @@ -2145,47 +2128,47 @@ "@svgr/babel-plugin-add-jsx-attribute@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz" integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== "@svgr/babel-plugin-remove-jsx-attribute@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz" integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== "@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz" integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== "@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz" integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== "@svgr/babel-plugin-svg-dynamic-title@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz" integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== "@svgr/babel-plugin-svg-em-dimensions@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz" integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== "@svgr/babel-plugin-transform-react-native-svg@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz" integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== "@svgr/babel-plugin-transform-svg-component@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz" integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== "@svgr/babel-preset@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" + resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz" integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== dependencies: "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" @@ -2199,7 +2182,7 @@ "@svgr/core@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" + resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz" integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== dependencies: "@babel/core" "^7.21.3" @@ -2210,7 +2193,7 @@ "@svgr/hast-util-to-babel-ast@8.0.0": version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" + resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz" integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== dependencies: "@babel/types" "^7.21.3" @@ -2218,7 +2201,7 @@ "@svgr/plugin-jsx@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" + resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz" integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== dependencies: "@babel/core" "^7.21.3" @@ -2228,7 +2211,7 @@ "@svgr/plugin-svgo@8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz#b115b7b967b564f89ac58feae89b88c3decd0f00" + resolved "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz" integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== dependencies: cosmiconfig "^8.1.3" @@ -2237,7 +2220,7 @@ "@svgr/webpack@^8.1.0": version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-8.1.0.tgz#16f1b5346f102f89fda6ec7338b96a701d8be0c2" + resolved "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz" integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== dependencies: "@babel/core" "^7.21.3" @@ -2251,7 +2234,7 @@ "@testing-library/dom@^10.4.0": version "10.4.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz" integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== dependencies: "@babel/code-frame" "^7.10.4" @@ -2264,9 +2247,9 @@ pretty-format "^27.0.2" "@testing-library/jest-dom@^6.5.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz#cff6452ba03b215c16d97b48adfc4e1ac54c3e03" - integrity sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA== + version "6.9.1" + resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz" + integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA== dependencies: "@adobe/css-tools" "^4.4.0" aria-query "^5.0.0" @@ -2277,34 +2260,34 @@ "@testing-library/react@^16.0.1": version "16.3.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.0.tgz#3a85bb9bdebf180cd76dba16454e242564d598a6" + resolved "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz" integrity sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw== dependencies: "@babel/runtime" "^7.12.5" "@testing-library/user-event@^14.5.2": version "14.6.1" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" + resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz" integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== "@tootallnate/once@2": version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== "@trysound/sax@0.2.0": version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== "@types/aria-query@^5.0.1": version "5.0.4" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz" integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== dependencies: "@babel/parser" "^7.20.7" @@ -2315,14 +2298,14 @@ "@types/babel__generator@*": version "7.27.0" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz" integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== dependencies: "@babel/parser" "^7.1.0" @@ -2330,14 +2313,14 @@ "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": version "7.28.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz" integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== dependencies: "@babel/types" "^7.28.2" "@types/body-parser@*": version "1.19.6" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz" integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== dependencies: "@types/connect" "*" @@ -2345,14 +2328,14 @@ "@types/bonjour@^3.5.9": version "3.5.13" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz" integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== dependencies: "@types/node" "*" "@types/connect-history-api-fallback@^1.3.5": version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz" integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== dependencies: "@types/express-serve-static-core" "*" @@ -2360,48 +2343,43 @@ "@types/connect@*": version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== dependencies: "@types/node" "*" -"@types/cookie@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" - integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== - "@types/d3-array@*": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" - integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + version "3.2.2" + resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz" + integrity sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw== "@types/d3-axis@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" + resolved "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz" integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== dependencies: "@types/d3-selection" "*" "@types/d3-brush@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" + resolved "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz" integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== dependencies: "@types/d3-selection" "*" "@types/d3-chord@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" + resolved "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz" integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== "@types/d3-color@*": version "3.1.3" - resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz" integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== "@types/d3-contour@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" + resolved "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz" integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== dependencies: "@types/d3-array" "*" @@ -2409,136 +2387,136 @@ "@types/d3-delaunay@*": version "6.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + resolved "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz" integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== "@types/d3-dispatch@*": version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz#ef004d8a128046cfce434d17182f834e44ef95b2" + resolved "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz" integrity sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA== "@types/d3-drag@*", "@types/d3-drag@^3.0.1": version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + resolved "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz" integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== dependencies: "@types/d3-selection" "*" "@types/d3-dsv@*": version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" + resolved "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz" integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== "@types/d3-ease@*": version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + resolved "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz" integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== "@types/d3-fetch@*": version "3.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" + resolved "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz" integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== dependencies: "@types/d3-dsv" "*" "@types/d3-force@*": version "3.0.10" - resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a" + resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz" integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw== "@types/d3-format@*": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" + resolved "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz" integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== "@types/d3-geo@*": version "3.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" + resolved "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz" integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== dependencies: "@types/geojson" "*" "@types/d3-hierarchy@*": version "3.1.7" - resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" + resolved "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz" integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== "@types/d3-interpolate@*": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz" integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== dependencies: "@types/d3-color" "*" "@types/d3-path@*": version "3.1.1" - resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a" + resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz" integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== "@types/d3-polygon@*": version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" + resolved "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz" integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== "@types/d3-quadtree@*": version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" + resolved "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz" integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== "@types/d3-random@*": version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" + resolved "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz" integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== "@types/d3-scale-chromatic@*": version "3.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39" + resolved "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz" integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ== "@types/d3-scale@*": version "4.0.9" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb" + resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz" integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== dependencies: "@types/d3-time" "*" "@types/d3-selection@*", "@types/d3-selection@^3.0.3": version "3.0.11" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3" + resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz" integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w== "@types/d3-shape@*": version "3.1.7" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz" integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== dependencies: "@types/d3-path" "*" "@types/d3-time-format@*": version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" + resolved "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz" integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== "@types/d3-time@*": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz" integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== "@types/d3-timer@*": version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz" integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== "@types/d3-transition@*": version "3.0.9" - resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706" + resolved "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz" integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg== dependencies: "@types/d3-selection" "*" "@types/d3-zoom@*", "@types/d3-zoom@^3.0.1": version "3.0.8" - resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz" integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== dependencies: "@types/d3-interpolate" "*" @@ -2546,7 +2524,7 @@ "@types/d3@^7.4.0": version "7.4.3" - resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" + resolved "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz" integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== dependencies: "@types/d3-array" "*" @@ -2582,47 +2560,39 @@ "@types/dagre@^0.7.52": version "0.7.53" - resolved "https://registry.yarnpkg.com/@types/dagre/-/dagre-0.7.53.tgz#4dab441bf31b6fb08af0b3e2a3f5ab0c0217a701" + resolved "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz" integrity sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ== "@types/eslint-scope@^3.7.7": version "3.7.7" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz" integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== dependencies: "@types/eslint" "*" "@types/estree" "*" -"@types/eslint@*": - version "9.6.1" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" - integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/eslint@^7.29.0 || ^8.4.1": +"@types/eslint@*", "@types/eslint@^7.29.0 || ^8.4.1": version "8.56.12" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.12.tgz#1657c814ffeba4d2f84c0d4ba0f44ca7ea1ca53a" + resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz" integrity sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g== dependencies: "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" - integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== - -"@types/estree@0.0.39": +"@types/estree@*", "@types/estree@0.0.39": version "0.0.39" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": - version "5.0.7" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6" - integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ== +"@types/estree@^1.0.0", "@types/estree@^1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/express-serve-static-core@*": + version "5.1.0" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz" + integrity sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA== dependencies: "@types/node" "*" "@types/qs" "*" @@ -2630,49 +2600,40 @@ "@types/send" "*" "@types/express-serve-static-core@^4.17.33": - version "4.19.6" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" - integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + version "4.19.7" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz" + integrity sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" "@types/send" "*" -"@types/express@*": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" - integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^5.0.0" - "@types/serve-static" "*" - -"@types/express@^4.17.13": - version "4.17.23" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef" - integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ== +"@types/express@*", "@types/express@^4.17.13": + version "4.17.25" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz" + integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" "@types/qs" "*" - "@types/serve-static" "*" + "@types/serve-static" "^1" "@types/fs-extra@^8.0.1": version "8.1.5" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.5.tgz#33aae2962d3b3ec9219b5aca2555ee00274f5927" + resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz" integrity sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ== dependencies: "@types/node" "*" "@types/geojson@*": version "7946.0.16" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" + resolved "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz" integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== "@types/glob@^7.1.1": version "7.2.0" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + resolved "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz" integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== dependencies: "@types/minimatch" "*" @@ -2680,64 +2641,64 @@ "@types/graceful-fs@^4.1.2", "@types/graceful-fs@^4.1.3": version "4.1.9" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz" integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== dependencies: "@types/node" "*" "@types/hast@^2.0.0": version "2.3.10" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" + resolved "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz" integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== dependencies: "@types/unist" "^2" "@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.1": version "3.3.7" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz#306e3a3a73828522efa1341159da4846e7573a6c" + resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz" integrity sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g== dependencies: hoist-non-react-statics "^3.3.0" "@types/html-minifier-terser@^6.0.0": version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" + resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== "@types/http-errors@*": version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz" integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== "@types/http-proxy@^1.17.8": - version "1.17.16" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.16.tgz#dee360707b35b3cc85afcde89ffeebff7d7f9240" - integrity sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w== + version "1.17.17" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz" + integrity sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw== dependencies: "@types/node" "*" "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== "@types/istanbul-lib-report@*": version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz" integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz" integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" "@types/jest@^27.0.1": version "27.5.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.2.tgz#ec49d29d926500ffb9fd22b84262e862049c026c" + resolved "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz" integrity sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA== dependencies: jest-matcher-utils "^27.0.0" @@ -2745,7 +2706,7 @@ "@types/jsdom@^20.0.0": version "20.0.1" - resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" + resolved "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz" integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== dependencies: "@types/node" "*" @@ -2754,27 +2715,27 @@ "@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/json5@^0.0.29": version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/linkify-it@^5": version "5.0.0" - resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz" integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== "@types/lodash@^4.14.202": version "4.17.20" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz" integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA== "@types/markdown-it@^14.1.1": version "14.1.2" - resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz" integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== dependencies: "@types/linkify-it" "^5" @@ -2782,239 +2743,232 @@ "@types/mdast@^3.0.0": version "3.0.15" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz" integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== dependencies: "@types/unist" "^2" "@types/mdurl@^2": version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz" integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== "@types/mime@^1": version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/minimatch@*", "@types/minimatch@^6.0.0": version "6.0.0" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-6.0.0.tgz#4d207b1cc941367bdcd195a3a781a7e4fc3b1e03" + resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-6.0.0.tgz" integrity sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA== dependencies: minimatch "*" "@types/node-forge@^1.3.0": - version "1.3.13" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.13.tgz#1797af20f7eccaf5f37b4d1739923bb0519d95b6" - integrity sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww== + version "1.3.14" + resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz" + integrity sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw== dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=13.7.0": - version "24.3.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec" - integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== - dependencies: - undici-types "~7.10.0" - -"@types/node@^22.12.0": - version "22.17.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.17.2.tgz#47a93d6f4b79327da63af727e7c54e8cab8c4d33" - integrity sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w== +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^22.12.0": + version "22.19.1" + resolved "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz" + integrity sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ== dependencies: undici-types "~6.21.0" "@types/numeral@^2.0.5": version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.5.tgz#388e5c4ff4b0e1787f130753cbbe83d3ba770858" + resolved "https://registry.npmjs.org/@types/numeral/-/numeral-2.0.5.tgz" integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== "@types/parse-json@^4.0.0": version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== "@types/parse5@^5.0.0": version "5.0.3" - resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" + resolved "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz" integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== "@types/prismjs@*": version "1.26.5" - resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.5.tgz#72499abbb4c4ec9982446509d2f14fb8483869d6" + resolved "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz" integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ== "@types/prop-types@*": version "15.7.15" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz" integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== "@types/qs@*": version "6.14.0" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz" integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== "@types/range-parser@*": version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/react-dom@^18.3.0": version "18.3.7" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz" integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ== "@types/react-window@^1.8.8": version "1.8.8" - resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + resolved "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz" integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== dependencies: "@types/react" "*" -"@types/react@*": - version "19.1.10" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.10.tgz#a05015952ef328e1b85579c839a71304b07d21d9" - integrity sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg== - dependencies: - csstype "^3.0.2" - -"@types/react@^18.3.11": - version "18.3.23" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.23.tgz#86ae6f6b95a48c418fecdaccc8069e0fbb63696a" - integrity sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w== +"@types/react@*", "@types/react@^18.3.11": + version "18.3.27" + resolved "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz" + integrity sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w== dependencies: "@types/prop-types" "*" - csstype "^3.0.2" + csstype "^3.2.2" "@types/refractor@^3.4.0": version "3.4.1" - resolved "https://registry.yarnpkg.com/@types/refractor/-/refractor-3.4.1.tgz#8b109804f77b3da8fad543d3f575fef1ece8835a" + resolved "https://registry.npmjs.org/@types/refractor/-/refractor-3.4.1.tgz" integrity sha512-wYuorIiCTSuvRT9srwt+taF6mH/ww+SyN2psM0sjef2qW+sS8GmshgDGTEDgWB1sTVGgYVE6EK7dBA2MxQxibg== dependencies: "@types/prismjs" "*" "@types/resolve@1.17.1": version "1.17.1" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz" integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== dependencies: "@types/node" "*" "@types/retry@0.12.0": version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== "@types/semver@^7.3.12": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e" - integrity sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA== + version "7.7.1" + resolved "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz" + integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA== "@types/send@*": - version "0.17.5" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" - integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w== + version "1.2.1" + resolved "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz" + integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== + dependencies: + "@types/node" "*" + +"@types/send@<1": + version "0.17.6" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz" + integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og== dependencies: "@types/mime" "^1" "@types/node" "*" "@types/serve-index@^1.9.1": version "1.9.4" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz" integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== dependencies: "@types/express" "*" -"@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.8" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" - integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== +"@types/serve-static@^1", "@types/serve-static@^1.13.10": + version "1.15.10" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz" + integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== dependencies: "@types/http-errors" "*" "@types/node" "*" - "@types/send" "*" + "@types/send" "<1" "@types/sockjs@^0.3.33": version "0.3.36" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz" integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== dependencies: "@types/node" "*" "@types/stack-utils@^2.0.0": version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== "@types/statuses@^2.0.4": version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa" + resolved "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz" integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA== "@types/styled-components@^5.1.34": - version "5.1.34" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.34.tgz#4107df8ef8a7eaba4fa6b05f78f93fba4daf0300" - integrity sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA== + version "5.1.36" + resolved "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.36.tgz" + integrity sha512-pGMRNY5G2rNDKEv2DOiFYa7Ft1r0jrhmgBwHhOMzPTgCjO76bCot0/4uEfqj7K0Jf1KdQmDtAuaDk9EAs9foSw== dependencies: "@types/hoist-non-react-statics" "*" "@types/react" "*" - csstype "^3.0.2" + csstype "^3.2.2" "@types/stylis@4.2.5": version "4.2.5" - resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df" + resolved "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz" integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw== -"@types/tough-cookie@*", "@types/tough-cookie@^4.0.5": +"@types/tough-cookie@*": version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz" integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== "@types/trusted-types@^2.0.2": version "2.0.7" - resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== "@types/unist@^2", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": version "2.0.11" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz" integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== "@types/use-sync-external-store@^0.0.3": version "0.0.3" - resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== "@types/ws@^8.5.5": version "8.18.1" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz" integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" "@types/yargs-parser@*": version "21.0.3" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== "@types/yargs@^16.0.0": - version "16.0.9" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.9.tgz#ba506215e45f7707e6cbcaf386981155b7ab956e" - integrity sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA== + version "16.0.11" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz" + integrity sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g== dependencies: "@types/yargs-parser" "*" "@types/yargs@^17.0.8": - version "17.0.33" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" - integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== + version "17.0.35" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz" + integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== dependencies: "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^5.5.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz" integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== dependencies: "@eslint-community/regexpp" "^4.4.0" @@ -3030,14 +2984,14 @@ "@typescript-eslint/experimental-utils@^5.0.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz#14559bf73383a308026b427a4a6129bae2146741" + resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz" integrity sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw== dependencies: "@typescript-eslint/utils" "5.62.0" "@typescript-eslint/parser@^5.5.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz" integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== dependencies: "@typescript-eslint/scope-manager" "5.62.0" @@ -3047,7 +3001,7 @@ "@typescript-eslint/scope-manager@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz" integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== dependencies: "@typescript-eslint/types" "5.62.0" @@ -3055,7 +3009,7 @@ "@typescript-eslint/type-utils@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz" integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== dependencies: "@typescript-eslint/typescript-estree" "5.62.0" @@ -3065,12 +3019,12 @@ "@typescript-eslint/types@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz" integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== dependencies: "@typescript-eslint/types" "5.62.0" @@ -3083,7 +3037,7 @@ "@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.58.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz" integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" @@ -3097,7 +3051,7 @@ "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz" integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== dependencies: "@typescript-eslint/types" "5.62.0" @@ -3105,12 +3059,12 @@ "@ungap/structured-clone@^1.2.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== "@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz" integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== dependencies: "@webassemblyjs/helper-numbers" "1.13.2" @@ -3118,22 +3072,22 @@ "@webassemblyjs/floating-point-hex-parser@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz" integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== "@webassemblyjs/helper-api-error@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz" integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== "@webassemblyjs/helper-buffer@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz" integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== "@webassemblyjs/helper-numbers@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz" integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== dependencies: "@webassemblyjs/floating-point-hex-parser" "1.13.2" @@ -3142,12 +3096,12 @@ "@webassemblyjs/helper-wasm-bytecode@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz" integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== "@webassemblyjs/helper-wasm-section@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz" integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3157,26 +3111,26 @@ "@webassemblyjs/ieee754@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz" integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== dependencies: "@xtuc/ieee754" "^1.2.0" "@webassemblyjs/leb128@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz" integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== dependencies: "@xtuc/long" "4.2.2" "@webassemblyjs/utf8@1.13.2": version "1.13.2" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz" integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== "@webassemblyjs/wasm-edit@^1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz" integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3190,7 +3144,7 @@ "@webassemblyjs/wasm-gen@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz" integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3201,7 +3155,7 @@ "@webassemblyjs/wasm-opt@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz" integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3211,7 +3165,7 @@ "@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz" integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3223,7 +3177,7 @@ "@webassemblyjs/wast-printer@1.14.1": version "1.14.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz" integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== dependencies: "@webassemblyjs/ast" "1.14.1" @@ -3231,22 +3185,22 @@ "@xtuc/ieee754@^1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== "@xtuc/long@4.2.2": version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== abab@^2.0.5, abab@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: mime-types "~2.1.34" @@ -3254,7 +3208,7 @@ accepts@~1.3.4, accepts@~1.3.8: acorn-globals@^7.0.0: version "7.0.1" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" + resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz" integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== dependencies: acorn "^8.1.0" @@ -3262,34 +3216,34 @@ acorn-globals@^7.0.0: acorn-import-phases@^1.0.3: version "1.0.4" - resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" + resolved "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz" integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.0.2: version "8.3.4" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== dependencies: acorn "^8.11.0" -acorn@^8.1.0, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.1, acorn@^8.9.0: +acorn@^8.1.0, acorn@^8.11.0, acorn@^8.15.0, acorn@^8.8.1, acorn@^8.9.0: version "8.15.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== address@^1.0.1, address@^1.1.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" + resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz" integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== adjust-sourcemap-loader@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" + resolved "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== dependencies: loader-utils "^2.0.0" @@ -3297,33 +3251,33 @@ adjust-sourcemap-loader@^4.0.0: agent-base@6: version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" ajv-formats@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: ajv "^8.0.0" ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== ajv-keywords@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== dependencies: fast-deep-equal "^3.1.3" ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -3333,7 +3287,7 @@ ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: version "8.17.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: fast-deep-equal "^3.1.3" @@ -3341,53 +3295,53 @@ ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" -ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: +ansi-escapes@^4.2.1: version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: type-fest "^0.21.3" ansi-escapes@^6.0.0: version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz" integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== ansi-html-community@^0.0.8: version "0.0.8" - resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== ansi-html@^0.0.9: version "0.0.9" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.9.tgz#6512d02342ae2cc68131952644a129cb734cd3f0" + resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz" integrity sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg== ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + version "6.2.2" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" ansi-styles@^5.0.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" @@ -3395,38 +3349,38 @@ anymatch@^3.0.3, anymatch@~3.1.2: argparse@^1.0.7: version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" argparse@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== aria-hidden@^1.2.5: version "1.2.6" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" + resolved "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz" integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== dependencies: tslib "^2.0.0" -aria-query@5.3.0: +aria-query@5.3.0, aria-query@^5.0.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz" integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== dependencies: dequal "^2.0.3" -aria-query@^5.0.0, aria-query@^5.3.2: +aria-query@^5.3.2: version "5.3.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz" integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== dependencies: call-bound "^1.0.3" @@ -3434,12 +3388,12 @@ array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: array-flatten@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: version "3.1.9" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" + resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz" integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== dependencies: call-bind "^1.0.8" @@ -3453,12 +3407,12 @@ array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: array-union@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== array.prototype.findlast@^1.2.5: version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + resolved "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz" integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== dependencies: call-bind "^1.0.7" @@ -3470,7 +3424,7 @@ array.prototype.findlast@^1.2.5: array.prototype.findlastindex@^1.2.6: version "1.2.6" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564" + resolved "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz" integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== dependencies: call-bind "^1.0.8" @@ -3483,7 +3437,7 @@ array.prototype.findlastindex@^1.2.6: array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz" integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== dependencies: call-bind "^1.0.8" @@ -3493,7 +3447,7 @@ array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: array.prototype.flatmap@^1.3.2, array.prototype.flatmap@^1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz" integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== dependencies: call-bind "^1.0.8" @@ -3503,7 +3457,7 @@ array.prototype.flatmap@^1.3.2, array.prototype.flatmap@^1.3.3: array.prototype.tosorted@^1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + resolved "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz" integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== dependencies: call-bind "^1.0.7" @@ -3514,7 +3468,7 @@ array.prototype.tosorted@^1.1.4: arraybuffer.prototype.slice@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + resolved "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz" integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== dependencies: array-buffer-byte-length "^1.0.1" @@ -3527,71 +3481,71 @@ arraybuffer.prototype.slice@^1.0.4: asap@~2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== ast-types-flow@^0.0.8: version "0.0.8" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== async-function@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + resolved "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz" integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== async@^3.2.6: version "3.2.6" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== at-least-node@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== attr-accept@^2.2.2: version "2.2.5" - resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e" + resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz" integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ== autoprefixer@^10.4.13: - version "10.4.21" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.21.tgz#77189468e7a8ad1d9a37fbc08efc9f480cf0a95d" - integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ== + version "10.4.22" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz" + integrity sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg== dependencies: - browserslist "^4.24.4" - caniuse-lite "^1.0.30001702" - fraction.js "^4.3.7" + browserslist "^4.27.0" + caniuse-lite "^1.0.30001754" + fraction.js "^5.3.4" normalize-range "^0.1.2" picocolors "^1.1.1" postcss-value-parser "^4.2.0" available-typed-arrays@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== dependencies: possible-typed-array-names "^1.0.0" axe-core@^4.10.0: - version "4.10.3" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.3.tgz#04145965ac7894faddbac30861e5d8f11bfd14fc" - integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== + version "4.11.0" + resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz" + integrity sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ== axobject-query@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" + resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz" integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== babel-jest@^27.4.2: version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz" integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== dependencies: "@jest/transform" "^27.5.1" @@ -3605,7 +3559,7 @@ babel-jest@^27.4.2: babel-jest@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== dependencies: "@jest/transform" "^29.7.0" @@ -3618,7 +3572,7 @@ babel-jest@^29.7.0: babel-loader@^8.2.3: version "8.4.1" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.4.1.tgz#6ccb75c66e62c3b144e1c5f2eaec5b8f6c08c675" + resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz" integrity sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA== dependencies: find-cache-dir "^3.3.1" @@ -3628,7 +3582,7 @@ babel-loader@^8.2.3: babel-plugin-istanbul@^6.1.1: version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -3639,7 +3593,7 @@ babel-plugin-istanbul@^6.1.1: babel-plugin-jest-hoist@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz" integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== dependencies: "@babel/template" "^7.3.3" @@ -3649,7 +3603,7 @@ babel-plugin-jest-hoist@^27.5.1: babel-plugin-jest-hoist@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz" integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== dependencies: "@babel/template" "^7.3.3" @@ -3659,7 +3613,7 @@ babel-plugin-jest-hoist@^29.6.3: babel-plugin-macros@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== dependencies: "@babel/runtime" "^7.12.5" @@ -3668,12 +3622,12 @@ babel-plugin-macros@^3.1.0: babel-plugin-named-asset-import@^0.3.8: version "0.3.8" - resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz#6b7fa43c59229685368683c28bc9734f24524cc2" + resolved "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz" integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q== babel-plugin-polyfill-corejs2@^0.4.14: version "0.4.14" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz#8101b82b769c568835611542488d463395c2ef8f" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz" integrity sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg== dependencies: "@babel/compat-data" "^7.27.7" @@ -3682,7 +3636,7 @@ babel-plugin-polyfill-corejs2@^0.4.14: babel-plugin-polyfill-corejs3@^0.13.0: version "0.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz#bb7f6aeef7addff17f7602a08a6d19a128c30164" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz" integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== dependencies: "@babel/helper-define-polyfill-provider" "^0.6.5" @@ -3690,19 +3644,19 @@ babel-plugin-polyfill-corejs3@^0.13.0: babel-plugin-polyfill-regenerator@^0.6.5: version "0.6.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz#32752e38ab6f6767b92650347bf26a31b16ae8c5" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz" integrity sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg== dependencies: "@babel/helper-define-polyfill-provider" "^0.6.5" babel-plugin-transform-react-remove-prop-types@^0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" + resolved "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz" integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== babel-preset-current-node-syntax@^1.0.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz" integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" @@ -3723,7 +3677,7 @@ babel-preset-current-node-syntax@^1.0.0: babel-preset-jest@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz" integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== dependencies: babel-plugin-jest-hoist "^27.5.1" @@ -3731,7 +3685,7 @@ babel-preset-jest@^27.5.1: babel-preset-jest@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz" integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== dependencies: babel-plugin-jest-hoist "^29.6.3" @@ -3739,7 +3693,7 @@ babel-preset-jest@^29.6.3: babel-preset-react-app@^10.0.1: version "10.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-10.1.0.tgz#e367f223f6c27878e6cc28471d0d506a9ab9f96c" + resolved "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.1.0.tgz" integrity sha512-f9B1xMdnkCIqe+2dHrJsoQFRz7reChaAHE/65SdaykPklQqhme2WaC08oD3is77x9ff98/9EazAKFDZv5rFEQg== dependencies: "@babel/core" "^7.16.0" @@ -3762,22 +3716,27 @@ babel-preset-react-app@^10.0.1: bail@^1.0.0: version "1.0.5" - resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" + resolved "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz" integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +baseline-browser-mapping@^2.8.25: + version "2.8.29" + resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz" + integrity sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA== + batch@0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== bfj@^7.0.2: version "7.1.0" - resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.1.0.tgz#c5177d522103f9040e1b12980fe8c38cf41d3f8b" + resolved "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz" integrity sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw== dependencies: bluebird "^3.7.2" @@ -3788,27 +3747,27 @@ bfj@^7.0.2: big-integer@^1.6.16: version "1.6.52" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz" integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== big.js@^5.2.2: version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^2.0.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bluebird@^3.7.2: version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== body-parser@1.20.3: version "1.20.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" @@ -3826,7 +3785,7 @@ body-parser@1.20.3: bonjour-service@^1.0.11: version "1.3.0" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.3.0.tgz#80d867430b5a0da64e82a8047fc1e355bdb71722" + resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz" integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA== dependencies: fast-deep-equal "^3.1.3" @@ -3834,12 +3793,12 @@ bonjour-service@^1.0.11: boolbase@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== brace-expansion@^1.1.7: version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" @@ -3847,21 +3806,21 @@ brace-expansion@^1.1.7: brace-expansion@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" braces@^3.0.3, braces@~3.0.2: version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: fill-range "^7.1.1" broadcast-channel@^3.4.1: version "3.7.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937" + resolved "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz" integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg== dependencies: "@babel/runtime" "^7.7.2" @@ -3873,41 +3832,42 @@ broadcast-channel@^3.4.1: rimraf "3.0.2" unload "2.2.0" -browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.4, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.1: - version "4.25.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.2.tgz#90c1507143742d743544ae6e92bca3348adff667" - integrity sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA== +browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.4, browserslist@^4.24.0, browserslist@^4.26.3, browserslist@^4.27.0, browserslist@^4.28.0: + version "4.28.0" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz" + integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ== dependencies: - caniuse-lite "^1.0.30001733" - electron-to-chromium "^1.5.199" - node-releases "^2.0.19" - update-browserslist-db "^1.1.3" + baseline-browser-mapping "^2.8.25" + caniuse-lite "^1.0.30001754" + electron-to-chromium "^1.5.249" + node-releases "^2.0.27" + update-browserslist-db "^1.1.4" bser@2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" buffer-from@^1.0.0: version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== builtin-modules@^3.1.0, builtin-modules@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== bytes@3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: es-errors "^1.3.0" @@ -3915,7 +3875,7 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply- call-bind@^1.0.7, call-bind@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== dependencies: call-bind-apply-helpers "^1.0.0" @@ -3925,7 +3885,7 @@ call-bind@^1.0.7, call-bind@^1.0.8: call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== dependencies: call-bind-apply-helpers "^1.0.2" @@ -3933,12 +3893,12 @@ call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camel-case@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz" integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== dependencies: pascal-case "^3.1.2" @@ -3946,22 +3906,22 @@ camel-case@^4.1.2: camelcase@^5.3.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== camelcase@^6.2.0, camelcase@^6.2.1: version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== camelize@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz" integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== caniuse-api@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + resolved "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz" integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== dependencies: browserslist "^4.0.0" @@ -3969,79 +3929,79 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001733: - version "1.0.30001735" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz#ba658fd3fd24a4106fd68d5ce472a2c251494dbe" - integrity sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001754: + version "1.0.30001756" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz" + integrity sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" - resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" + resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz" integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== catharsis@^0.9.0: version "0.9.0" - resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" + resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz" integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== dependencies: lodash "^4.17.15" ccount@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" + resolved "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" chalk@^5.2.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.5.0.tgz#67ada1df5ca809dc84c9b819d76418ddcf128428" - integrity sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg== + version "5.6.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz" + integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== char-regex@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== char-regex@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.2.tgz#81385bb071af4df774bff8721d0ca15ef29ea0bb" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz" integrity sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg== character-entities-html4@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125" + resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz" integrity sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g== character-entities-legacy@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" + resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz" integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== character-entities@^1.0.0: version "1.2.4" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" + resolved "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz" integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== character-reference-invalid@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" + resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== check-types@^11.2.3: version "11.2.3" - resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.2.3.tgz#1ffdf68faae4e941fce252840b1787b8edc93b71" + resolved "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz" integrity sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg== chokidar@^3.4.2, chokidar@^3.5.3: version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" @@ -4056,49 +4016,49 @@ chokidar@^3.4.2, chokidar@^3.5.3: chroma-js@^2.4.2: version "2.6.0" - resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.6.0.tgz#578743dd359698a75067a19fa5571dec54d0b70b" + resolved "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz" integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== chrome-trace-event@^1.0.2: version "1.0.4" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== ci-info@^3.2.0: version "3.9.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== cjs-module-lexer@^1.0.0: version "1.4.3" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz" integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== classcat@^5.0.3, classcat@^5.0.4: version "5.0.5" - resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77" + resolved "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz" integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w== classnames@^2.5.1: version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== clean-css@^5.2.2: version "5.3.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" + resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz" integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== dependencies: source-map "~0.6.0" cli-width@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz" integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== cliui@^8.0.1: version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== dependencies: string-width "^4.2.0" @@ -4107,93 +4067,93 @@ cliui@^8.0.1: co@^4.6.0: version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== collapse-white-space@^1.0.2: version "1.0.6" - resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" + resolved "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz" integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== collect-v8-coverage@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" - integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + version "1.0.3" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz" + integrity sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw== color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colord@^2.9.1: version "2.9.3" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== colorette@^1.1.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + resolved "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz" integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== colorette@^2.0.10: version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== combined-stream@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" comma-separated-tokens@^1.0.0: version "1.0.8" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" + resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== commander@^2.20.0: version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commander@^7.2.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== commander@^8.3.0: version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== common-tags@^1.8.0: version "1.8.2" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz" integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== commondir@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== compressible@~2.0.18: version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== dependencies: mime-db ">= 1.43.0 < 2" compression@^1.7.4: version "1.8.1" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + resolved "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz" integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== dependencies: bytes "3.1.2" @@ -4206,81 +4166,81 @@ compression@^1.7.4: concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== confusing-browser-globals@^1.0.11: version "1.0.11" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" + resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== connect-history-api-fallback@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz" integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== content-disposition@0.5.4: version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: safe-buffer "5.2.1" content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.7.0: version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== convert-source-map@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== cookie-signature@1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== cookie@0.7.1: version "0.7.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== -cookie@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" - integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +cookie@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz" + integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== core-js-compat@^3.43.0: - version "3.45.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.45.0.tgz#bc0017525dcb7a42ba3241d02f6fce9bae8e5c33" - integrity sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA== + version "3.47.0" + resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz" + integrity sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ== dependencies: - browserslist "^4.25.1" + browserslist "^4.28.0" core-js-pure@^3.23.3: - version "3.45.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.45.0.tgz#c753b80daf1bf732e56bf0b8cbd62797c0c1f235" - integrity sha512-OtwjqcDpY2X/eIIg1ol/n0y/X8A9foliaNt1dSK0gV3J2/zw+89FcNG3mPK+N8YWts4ZFUPxnrAzsxs/lf8yDA== + version "3.47.0" + resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz" + integrity sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw== core-js@^3.19.2: - version "3.45.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.45.0.tgz#556c2af44a2d9c73ea7b49504392474a9f7c947e" - integrity sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA== + version "3.47.0" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz" + integrity sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg== core-util-is@~1.0.0: version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== cosmiconfig@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz" integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== dependencies: "@types/parse-json" "^4.0.0" @@ -4291,7 +4251,7 @@ cosmiconfig@^6.0.0: cosmiconfig@^7.0.0: version "7.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== dependencies: "@types/parse-json" "^4.0.0" @@ -4302,7 +4262,7 @@ cosmiconfig@^7.0.0: cosmiconfig@^8.1.3: version "8.3.6" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz" integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== dependencies: import-fresh "^3.3.0" @@ -4312,7 +4272,7 @@ cosmiconfig@^8.1.3: create-jest@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + resolved "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz" integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== dependencies: "@jest/types" "^29.6.3" @@ -4325,7 +4285,7 @@ create-jest@^29.7.0: cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" @@ -4334,43 +4294,43 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: crypto-random-string@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== css-blank-pseudo@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561" + resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz" integrity sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ== dependencies: postcss-selector-parser "^6.0.9" css-box-model@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + resolved "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== dependencies: tiny-invariant "^1.0.6" css-color-keywords@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz" integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== css-declaration-sorter@^6.3.1: version "6.4.1" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" + resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz" integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== css-has-pseudo@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz#57f6be91ca242d5c9020ee3e51bbb5b89fc7af73" + resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz" integrity sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw== dependencies: postcss-selector-parser "^6.0.9" css-loader@^6.5.1: version "6.11.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" + resolved "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz" integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== dependencies: icss-utils "^5.1.0" @@ -4384,7 +4344,7 @@ css-loader@^6.5.1: css-minimizer-webpack-plugin@^3.2.0: version "3.4.1" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz#ab78f781ced9181992fe7b6e4f3422e76429878f" + resolved "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz" integrity sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q== dependencies: cssnano "^5.0.6" @@ -4396,12 +4356,12 @@ css-minimizer-webpack-plugin@^3.2.0: css-prefers-color-scheme@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz#ca8a22e5992c10a5b9d315155e7caee625903349" + resolved "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz" integrity sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA== css-select@^4.1.3: version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + resolved "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz" integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== dependencies: boolbase "^1.0.0" @@ -4412,7 +4372,7 @@ css-select@^4.1.3: css-select@^5.1.0: version "5.2.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e" + resolved "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz" integrity sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw== dependencies: boolbase "^1.0.0" @@ -4423,7 +4383,7 @@ css-select@^5.1.0: css-to-react-native@3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz" integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== dependencies: camelize "^1.0.0" @@ -4432,7 +4392,7 @@ css-to-react-native@3.2.0: css-tree@^1.1.2, css-tree@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz" integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== dependencies: mdn-data "2.0.14" @@ -4440,7 +4400,7 @@ css-tree@^1.1.2, css-tree@^1.1.3: css-tree@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz" integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== dependencies: mdn-data "2.0.30" @@ -4448,7 +4408,7 @@ css-tree@^2.3.1: css-tree@~2.2.0: version "2.2.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz" integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== dependencies: mdn-data "2.0.28" @@ -4456,27 +4416,27 @@ css-tree@~2.2.0: css-what@^6.0.1, css-what@^6.1.0: version "6.2.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" + resolved "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz" integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== css.escape@^1.5.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz" integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== cssdb@^7.1.0: version "7.11.2" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.2.tgz#127a2f5b946ee653361a5af5333ea85a39df5ae5" + resolved "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz" integrity sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A== cssesc@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== cssnano-preset-default@^5.2.14: version "5.2.14" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" + resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz" integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== dependencies: css-declaration-sorter "^6.3.1" @@ -4511,12 +4471,12 @@ cssnano-preset-default@^5.2.14: cssnano-utils@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" + resolved "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz" integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== cssnano@^5.0.6: version "5.1.15" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" + resolved "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz" integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== dependencies: cssnano-preset-default "^5.2.14" @@ -4525,53 +4485,58 @@ cssnano@^5.0.6: csso@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + resolved "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz" integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== dependencies: css-tree "^1.1.2" csso@^5.0.5: version "5.0.5" - resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + resolved "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz" integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== dependencies: css-tree "~2.2.0" cssom@^0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz" integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== cssom@~0.3.6: version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz" integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== cssstyle@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz" integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== dependencies: cssom "~0.3.6" -csstype@3.1.3, csstype@^3.0.2: +csstype@3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +csstype@^3.0.2, csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + "d3-color@1 - 3": version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== "d3-dispatch@1 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz" integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== "d3-drag@2 - 3", d3-drag@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz" integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== dependencies: d3-dispatch "1 - 3" @@ -4579,29 +4544,29 @@ csstype@3.1.3, csstype@^3.0.2: "d3-ease@1 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz" integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== "d3-interpolate@1 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== dependencies: d3-color "1 - 3" "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== "d3-timer@1 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz" integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== "d3-transition@2 - 3": version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz" integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== dependencies: d3-color "1 - 3" @@ -4612,7 +4577,7 @@ csstype@3.1.3, csstype@^3.0.2: d3-zoom@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz" integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== dependencies: d3-dispatch "1 - 3" @@ -4623,7 +4588,7 @@ d3-zoom@^3.0.0: dagre@^0.8.5: version "0.8.5" - resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee" + resolved "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz" integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw== dependencies: graphlib "^2.1.8" @@ -4631,12 +4596,12 @@ dagre@^0.8.5: damerau-levenshtein@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== data-urls@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + resolved "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz" integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== dependencies: abab "^2.0.6" @@ -4645,7 +4610,7 @@ data-urls@^3.0.2: data-view-buffer@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + resolved "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz" integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== dependencies: call-bound "^1.0.3" @@ -4654,7 +4619,7 @@ data-view-buffer@^1.0.2: data-view-byte-length@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + resolved "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz" integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== dependencies: call-bound "^1.0.3" @@ -4663,7 +4628,7 @@ data-view-byte-length@^1.0.2: data-view-byte-offset@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + resolved "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz" integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== dependencies: call-bound "^1.0.2" @@ -4672,60 +4637,60 @@ data-view-byte-offset@^1.0.1: debug@2.6.9, debug@^2.6.0: version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" - integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + version "4.4.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" debug@^3.2.7: version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" decimal.js@^10.4.2: version "10.6.0" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz" integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== decode-uri-component@^0.2.2: version "0.2.2" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== dedent@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.6.0.tgz#79d52d6389b1ffa67d2bcef59ba51847a9d503b2" - integrity sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA== + version "1.7.0" + resolved "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz" + integrity sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ== deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== deepmerge@^4.2.2, deepmerge@^4.3.1: version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== default-gateway@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== dependencies: execa "^5.0.0" define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: es-define-property "^1.0.0" @@ -4734,12 +4699,12 @@ define-data-property@^1.0.1, define-data-property@^1.1.4: define-lazy-prop@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== define-properties@^1.1.3, define-properties@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== dependencies: define-data-property "^1.0.1" @@ -4748,47 +4713,47 @@ define-properties@^1.1.3, define-properties@^1.2.1: delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== depd@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== depd@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== dequal@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== destroy@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== detect-newline@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== detect-node-es@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + resolved "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== detect-node@^2.0.4, detect-node@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== detect-port-alt@^1.1.6: version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" + resolved "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz" integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== dependencies: address "^1.0.1" @@ -4796,62 +4761,62 @@ detect-port-alt@^1.1.6: diff-sequences@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== diff-sequences@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== dir-glob@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: path-type "^4.0.0" dns-packet@^5.2.2: version "5.6.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz" integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== dependencies: "@leichtgewicht/ip-codec" "^2.0.1" doctrine@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== dependencies: esutils "^2.0.2" doctrine@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: esutils "^2.0.2" dom-accessibility-api@^0.5.9: version "0.5.16" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== dom-accessibility-api@^0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz" integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== dom-converter@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz" integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== dependencies: utila "~0.4" dom-serializer@^1.0.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz" integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== dependencies: domelementtype "^2.0.1" @@ -4860,7 +4825,7 @@ dom-serializer@^1.0.1: dom-serializer@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz" integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== dependencies: domelementtype "^2.3.0" @@ -4869,33 +4834,33 @@ dom-serializer@^2.0.0: domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== domexception@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + resolved "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz" integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== dependencies: webidl-conversions "^7.0.0" domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz" integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== dependencies: domelementtype "^2.2.0" domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz" integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== dependencies: domelementtype "^2.3.0" domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== dependencies: dom-serializer "^1.0.1" @@ -4904,7 +4869,7 @@ domutils@^2.5.2, domutils@^2.8.0: domutils@^3.0.1: version "3.2.2" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + resolved "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz" integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== dependencies: dom-serializer "^2.0.0" @@ -4913,7 +4878,7 @@ domutils@^3.0.1: dot-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== dependencies: no-case "^3.0.4" @@ -4921,17 +4886,17 @@ dot-case@^3.0.4: dotenv-expand@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== dotenv@^10.0.0: version "10.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: call-bind-apply-helpers "^1.0.1" @@ -4940,64 +4905,64 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1: duplexer@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== ee-first@1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== ejs@^3.1.6: version "3.1.10" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz" integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" -electron-to-chromium@^1.5.199: - version "1.5.203" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.203.tgz#ef7fc2f7e1b816fa4535c861d1ec1348204142b6" - integrity sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g== +electron-to-chromium@^1.5.249: + version "1.5.258" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.258.tgz" + integrity sha512-rHUggNV5jKQ0sSdWwlaRDkFc3/rRJIVnOSe9yR4zrR07m3ZxhP4N27Hlg8VeJGGYgFTxK5NqDmWI4DSH72vIJg== emittery@^0.13.1: version "0.13.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== emojis-list@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== emoticon@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-3.2.0.tgz#c008ca7d7620fac742fe1bf4af8ff8fed154ae7f" + resolved "https://registry.npmjs.org/emoticon/-/emoticon-3.2.0.tgz" integrity sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg== encodeurl@~1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== encodeurl@~2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== enhanced-resolve@^5.17.3: version "5.18.3" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz" integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww== dependencies: graceful-fs "^4.2.4" @@ -5005,36 +4970,36 @@ enhanced-resolve@^5.17.3: entities@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== entities@^4.2.0, entities@^4.4.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== entities@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" + resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz" integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + version "1.3.4" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== dependencies: is-arrayish "^0.2.1" error-stack-parser@^2.0.6: version "2.1.4" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + resolved "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz" integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== dependencies: stackframe "^1.3.4" es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0: version "1.24.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz" integrity sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg== dependencies: array-buffer-byte-length "^1.0.2" @@ -5094,17 +5059,17 @@ es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23 es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-iterator-helpers@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz#d1dd0f58129054c0ad922e6a9a1e65eef435fe75" + resolved "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz" integrity sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w== dependencies: call-bind "^1.0.8" @@ -5126,19 +5091,19 @@ es-iterator-helpers@^1.2.1: es-module-lexer@^1.2.1: version "1.7.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz" integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== dependencies: es-errors "^1.3.0" @@ -5148,14 +5113,14 @@ es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" + resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz" integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== dependencies: hasown "^2.0.2" es-to-primitive@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz" integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== dependencies: is-callable "^1.2.7" @@ -5164,27 +5129,27 @@ es-to-primitive@^1.3.0: escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-html@~1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== escape-string-regexp@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== escodegen@^1.13.0, escodegen@^1.8.1: version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz" integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== dependencies: esprima "^4.0.1" @@ -5196,7 +5161,7 @@ escodegen@^1.13.0, escodegen@^1.8.1: escodegen@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz" integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== dependencies: esprima "^4.0.1" @@ -5207,7 +5172,7 @@ escodegen@^2.0.0: eslint-config-react-app@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz#73ba3929978001c5c86274c017ea57eb5fa644b4" + resolved "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz" integrity sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA== dependencies: "@babel/core" "^7.16.0" @@ -5227,7 +5192,7 @@ eslint-config-react-app@^7.0.1: eslint-import-resolver-node@^0.3.9: version "0.3.9" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz" integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== dependencies: debug "^3.2.7" @@ -5236,14 +5201,14 @@ eslint-import-resolver-node@^0.3.9: eslint-module-utils@^2.12.1: version "2.12.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff" + resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz" integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== dependencies: debug "^3.2.7" eslint-plugin-flowtype@^8.0.3: version "8.0.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz#e1557e37118f24734aa3122e7536a038d34a4912" + resolved "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz" integrity sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ== dependencies: lodash "^4.17.21" @@ -5251,7 +5216,7 @@ eslint-plugin-flowtype@^8.0.3: eslint-plugin-import@^2.25.3: version "2.32.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz" integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== dependencies: "@rtsao/scc" "^1.1.0" @@ -5276,14 +5241,14 @@ eslint-plugin-import@^2.25.3: eslint-plugin-jest@^25.3.0: version "25.7.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz#ff4ac97520b53a96187bad9c9814e7d00de09a6a" + resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz" integrity sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ== dependencies: "@typescript-eslint/experimental-utils" "^5.0.0" eslint-plugin-jsx-a11y@^6.5.1: version "6.10.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" + resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz" integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== dependencies: aria-query "^5.3.2" @@ -5304,12 +5269,12 @@ eslint-plugin-jsx-a11y@^6.5.1: eslint-plugin-react-hooks@^4.3.0: version "4.6.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz" integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== eslint-plugin-react@^7.27.1: version "7.37.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz" integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== dependencies: array-includes "^3.1.8" @@ -5333,14 +5298,14 @@ eslint-plugin-react@^7.27.1: eslint-plugin-testing-library@^5.0.1: version "5.11.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz#5b46cdae96d4a78918711c0b4792f90088e62d20" + resolved "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz" integrity sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw== dependencies: "@typescript-eslint/utils" "^5.58.0" eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: esrecurse "^4.3.0" @@ -5348,7 +5313,7 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: eslint-scope@^7.2.2: version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" @@ -5356,17 +5321,17 @@ eslint-scope@^7.2.2: eslint-visitor-keys@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint-webpack-plugin@^3.1.1: version "3.2.0" - resolved "https://registry.yarnpkg.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz#1978cdb9edc461e4b0195a20da950cf57988347c" + resolved "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz" integrity sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w== dependencies: "@types/eslint" "^7.29.0 || ^8.4.1" @@ -5377,7 +5342,7 @@ eslint-webpack-plugin@^3.1.1: eslint@^8.3.0: version "8.57.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" @@ -5421,7 +5386,7 @@ eslint@^8.3.0: espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: acorn "^8.9.0" @@ -5430,76 +5395,76 @@ espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: esprima@1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b" + resolved "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz" integrity sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A== esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.4.2: version "1.6.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz" integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" esrecurse@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: estraverse "^5.2.0" estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== estree-walker@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-0.2.1.tgz" integrity sha512-6/I1dwNKk0N9iGOU3ydzAAurz4NPo/ttxZNCqgIVbWFvWyzWBSNonRrJ5CpjDuyBfmM7ENN7WCzUi9aT/UPXXQ== estree-walker@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== estree-walker@^2.0.1, estree-walker@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== esutils@^2.0.2: version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== etag@~1.8.1: version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== eventemitter3@^4.0.0: version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.2.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== execa@^5.0.0: version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -5514,12 +5479,12 @@ execa@^5.0.0: exit@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== expect@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + resolved "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz" integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== dependencies: "@jest/expect-utils" "^29.7.0" @@ -5530,7 +5495,7 @@ expect@^29.7.0: express@^4.17.3: version "4.21.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" @@ -5567,17 +5532,17 @@ express@^4.17.3: extend@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.0.3, fast-glob@^3.2.9: version "3.3.3" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== dependencies: "@nodelib/fs.stat" "^2.0.2" @@ -5588,57 +5553,57 @@ fast-glob@^3.0.3, fast-glob@^3.2.9: fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" - integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + version "3.1.0" + resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== fastq@^1.6.0: version "1.19.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== dependencies: reusify "^1.0.4" fault@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" + resolved "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz" integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== dependencies: format "^0.2.0" faye-websocket@^0.11.3: version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== dependencies: websocket-driver ">=0.5.1" fb-watchman@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: bser "2.1.1" file-entry-cache@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: flat-cache "^3.0.4" file-loader@^6.2.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== dependencies: loader-utils "^2.0.0" @@ -5646,38 +5611,38 @@ file-loader@^6.2.0: file-selector@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17" + resolved "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz" integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg== dependencies: tslib "^2.0.3" filelist@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz" integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== dependencies: minimatch "^5.0.1" filesize@^8.0.6: version "8.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" + resolved "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz" integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== fill-range@^7.1.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" filter-obj@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz" integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== finalhandler@1.3.1: version "1.3.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" @@ -5690,7 +5655,7 @@ finalhandler@1.3.1: find-cache-dir@^3.3.1: version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz" integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== dependencies: commondir "^1.0.1" @@ -5699,19 +5664,19 @@ find-cache-dir@^3.3.1: find-root@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== find-up@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz" integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== dependencies: locate-path "^3.0.0" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: locate-path "^5.0.0" @@ -5719,7 +5684,7 @@ find-up@^4.0.0, find-up@^4.1.0: find-up@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" @@ -5727,7 +5692,7 @@ find-up@^5.0.0: flat-cache@^3.0.4: version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz" integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: flatted "^3.2.9" @@ -5736,31 +5701,31 @@ flat-cache@^3.0.4: flatted@^3.2.9: version "3.3.3" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== focus-lock@^1.3.6: version "1.3.6" - resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-1.3.6.tgz#955eec1e10591d56f679258edb94aedb11d691cd" + resolved "https://registry.npmjs.org/focus-lock/-/focus-lock-1.3.6.tgz" integrity sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg== dependencies: tslib "^2.0.3" follow-redirects@^1.0.0: version "1.15.11" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz" integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== dependencies: is-callable "^1.2.7" fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.3" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" + resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz" integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== dependencies: "@babel/code-frame" "^7.8.3" @@ -5778,9 +5743,9 @@ fork-ts-checker-webpack-plugin@^6.5.0: tapable "^1.0.0" form-data@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" - integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== + version "4.0.5" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz" + integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" @@ -5790,27 +5755,27 @@ form-data@^4.0.0: format@^0.2.0: version "0.2.2" - resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + resolved "https://registry.npmjs.org/format/-/format-0.2.2.tgz" integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== forwarded@0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fraction.js@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" - integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== +fraction.js@^5.3.4: + version "5.3.4" + resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz" + integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== fresh@0.5.2: version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== fs-extra@^10.0.0: version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== dependencies: graceful-fs "^4.2.0" @@ -5819,7 +5784,7 @@ fs-extra@^10.0.0: fs-extra@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: graceful-fs "^4.2.0" @@ -5828,7 +5793,7 @@ fs-extra@^8.1.0: fs-extra@^9.0.0, fs-extra@^9.0.1: version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: at-least-node "^1.0.0" @@ -5838,27 +5803,27 @@ fs-extra@^9.0.0, fs-extra@^9.0.1: fs-monkey@^1.0.4: version "1.1.0" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.1.0.tgz#632aa15a20e71828ed56b24303363fb1414e5997" + resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz" integrity sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw== fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: version "1.1.8" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz" integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== dependencies: call-bind "^1.0.8" @@ -5870,22 +5835,27 @@ function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: functions-have-names@^1.2.3: version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +generator-function@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz" + integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-caller-file@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: call-bind-apply-helpers "^1.0.2" @@ -5901,22 +5871,22 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@ get-nonce@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + resolved "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== get-package-type@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== get-proto@^1.0.0, get-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== dependencies: dunder-proto "^1.0.1" @@ -5924,12 +5894,12 @@ get-proto@^1.0.0, get-proto@^1.0.1: get-stream@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== get-symbol-description@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz" integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== dependencies: call-bound "^1.0.3" @@ -5938,26 +5908,26 @@ get-symbol-description@^1.1.0: glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" glob-parent@^6.0.2: version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" glob-to-regexp@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -5969,7 +5939,7 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: glob@^8.0.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" @@ -5980,14 +5950,14 @@ glob@^8.0.0: global-modules@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + resolved "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz" integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== dependencies: global-prefix "^3.0.0" global-prefix@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz" integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== dependencies: ini "^1.3.5" @@ -5996,14 +5966,14 @@ global-prefix@^3.0.0: globals@^13.19.0: version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" globalthis@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: define-properties "^1.2.1" @@ -6011,7 +5981,7 @@ globalthis@^1.0.4: globby@10.0.1: version "10.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22" + resolved "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz" integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A== dependencies: "@types/glob" "^7.1.1" @@ -6025,7 +5995,7 @@ globby@10.0.1: globby@^11.0.4, globby@^11.1.0: version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: array-union "^2.1.0" @@ -6037,46 +6007,46 @@ globby@^11.0.4, globby@^11.1.0: gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== graphemer@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== graphlib@^2.1.8: version "2.1.8" - resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" + resolved "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz" integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A== dependencies: lodash "^4.17.15" graphql@^16.8.1: - version "16.11.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.11.0.tgz#96d17f66370678027fdf59b2d4c20b4efaa8a633" - integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw== + version "16.12.0" + resolved "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz" + integrity sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ== gzip-size@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz" integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== dependencies: duplexer "^0.1.2" handle-thing@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== happy-dom@^16.8.1: version "16.8.1" - resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-16.8.1.tgz#43d7e998fd36aa6062acbdfa88262ef0bc0a105e" + resolved "https://registry.npmjs.org/happy-dom/-/happy-dom-16.8.1.tgz" integrity sha512-n0QrmT9lD81rbpKsyhnlz3DgnMZlaOkJPpgi746doA+HvaMC79bdWkwjrNnGJRvDrWTI8iOcJiVTJ5CdT/AZRw== dependencies: webidl-conversions "^7.0.0" @@ -6084,55 +6054,55 @@ happy-dom@^16.8.1: harmony-reflect@^1.4.6: version "1.6.2" - resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" + resolved "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz" integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== has-bigints@^1.0.2: version "1.1.0" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz" integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: es-define-property "^1.0.0" has-proto@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz" integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== dependencies: dunder-proto "^1.0.0" has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" hasown@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" hast-to-hyperscript@^9.0.0: version "9.0.1" - resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" + resolved "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz" integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA== dependencies: "@types/unist" "^2.0.3" @@ -6145,7 +6115,7 @@ hast-to-hyperscript@^9.0.0: hast-util-from-parse5@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz#554e34abdeea25ac76f5bd950a1f0180e0b3bc2a" + resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz" integrity sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA== dependencies: "@types/parse5" "^5.0.0" @@ -6157,17 +6127,17 @@ hast-util-from-parse5@^6.0.0: hast-util-is-element@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz#3b3ed5159a2707c6137b48637fbfe068e175a425" + resolved "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.1.0.tgz" integrity sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ== hast-util-parse-selector@^2.0.0: version "2.2.5" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" + resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz" integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== hast-util-raw@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-6.1.0.tgz#e16a3c2642f65cc7c480c165400a40d604ab75d0" + resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-6.1.0.tgz" integrity sha512-5FoZLDHBpka20OlZZ4I/+RBw5piVQ8iI1doEvffQhx5CbCyTtP8UCq8Tw6NmTAMtXgsQxmhW7Ly8OdFre5/YMQ== dependencies: "@types/hast" "^2.0.0" @@ -6184,7 +6154,7 @@ hast-util-raw@^6.1.0: hast-util-to-html@^7.1.1: version "7.1.3" - resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-7.1.3.tgz#9f339ca9bea71246e565fc79ff7dbfe98bb50f5e" + resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-7.1.3.tgz" integrity sha512-yk2+1p3EJTEE9ZEUkgHsUSVhIpCsL/bvT8E5GzmWc+N1Po5gBw+0F8bo7dpxXR0nu0bQVxVZGX2lBGF21CmeDw== dependencies: ccount "^1.0.0" @@ -6200,7 +6170,7 @@ hast-util-to-html@^7.1.1: hast-util-to-parse5@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz#1ec44650b631d72952066cea9b1445df699f8479" + resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz" integrity sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ== dependencies: hast-to-hyperscript "^9.0.0" @@ -6211,12 +6181,12 @@ hast-util-to-parse5@^6.0.0: hast-util-whitespace@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41" + resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz" integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A== hastscript@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz" integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w== dependencies: "@types/hast" "^2.0.0" @@ -6227,39 +6197,39 @@ hastscript@^6.0.0: he@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== headers-polyfill@^4.0.2: version "4.0.3" - resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07" + resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz" integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ== highlight.js@^10.4.1, highlight.js@~10.7.0: version "10.7.3" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== highlightjs-vue@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" + resolved "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz" integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: react-is "^16.7.0" hoopy@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" + resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz" integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== hpack.js@^2.1.6: version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== dependencies: inherits "^2.0.1" @@ -6269,24 +6239,24 @@ hpack.js@^2.1.6: html-encoding-sniffer@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz" integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== dependencies: whatwg-encoding "^2.0.0" html-entities@^2.1.0, html-entities@^2.3.2: version "2.6.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.6.0.tgz#7c64f1ea3b36818ccae3d3fb48b6974208e984f8" + resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz" integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ== html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== html-minifier-terser@^6.0.2: version "6.1.0" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== dependencies: camel-case "^4.1.2" @@ -6299,13 +6269,13 @@ html-minifier-terser@^6.0.2: html-void-elements@^1.0.0: version "1.0.5" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" + resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-1.0.5.tgz" integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w== html-webpack-plugin@^5.5.0: - version "5.6.4" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz#d8cb0f7edff7745ae7d6cccb0bff592e9f7f7959" - integrity sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw== + version "5.6.5" + resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz" + integrity sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -6315,7 +6285,7 @@ html-webpack-plugin@^5.5.0: htmlparser2@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== dependencies: domelementtype "^2.0.1" @@ -6325,12 +6295,12 @@ htmlparser2@^6.1.0: http-deceiver@^1.2.7: version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== http-errors@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== dependencies: depd "2.0.0" @@ -6341,7 +6311,7 @@ http-errors@2.0.0: http-errors@~1.6.2: version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== dependencies: depd "~1.1.2" @@ -6351,12 +6321,12 @@ http-errors@~1.6.2: http-parser-js@>=0.5.1: version "0.5.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" + resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz" integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== http-proxy-agent@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== dependencies: "@tootallnate/once" "2" @@ -6365,7 +6335,7 @@ http-proxy-agent@^5.0.0: http-proxy-middleware@^2.0.3: version "2.0.9" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz" integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== dependencies: "@types/http-proxy" "^1.17.8" @@ -6376,7 +6346,7 @@ http-proxy-middleware@^2.0.3: http-proxy@^1.18.1: version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== dependencies: eventemitter3 "^4.0.0" @@ -6385,7 +6355,7 @@ http-proxy@^1.18.1: https-proxy-agent@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" @@ -6393,53 +6363,53 @@ https-proxy-agent@^5.0.1: human-signals@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== iconv-lite@0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" iconv-lite@0.6.3, iconv-lite@^0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== idb@^7.0.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + resolved "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz" integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== identity-obj-proxy@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" + resolved "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz" integrity sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA== dependencies: harmony-reflect "^1.4.6" ignore@^5.1.1, ignore@^5.2.0: version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== immer@^9.0.7: version "9.0.21" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz" integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" @@ -6447,7 +6417,7 @@ import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: import-local@^3.0.2: version "3.2.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz" integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== dependencies: pkg-dir "^4.2.0" @@ -6455,17 +6425,17 @@ import-local@^3.0.2: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== indent-string@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" @@ -6473,32 +6443,32 @@ inflight@^1.0.4: inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== ini@^1.3.5: version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== inline-style-parser@0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== inter-ui@^3.19.3: version "3.19.3" - resolved "https://registry.yarnpkg.com/inter-ui/-/inter-ui-3.19.3.tgz#cf4b4b6d30de8d5463e2462588654b325206488c" + resolved "https://registry.npmjs.org/inter-ui/-/inter-ui-3.19.3.tgz" integrity sha512-5FG9fjuYOXocIfjzcCBhICL5cpvwEetseL3FU6tP3d6Bn7g8wODhB+I9RNGRTizCT7CUG4GOK54OPxqq3msQgg== internal-slot@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz" integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== dependencies: es-errors "^1.3.0" @@ -6507,22 +6477,22 @@ internal-slot@^1.1.0: ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== ipaddr.js@^2.0.1: version "2.2.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== is-alphabetical@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" + resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz" integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== is-alphanumerical@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" + resolved "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz" integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== dependencies: is-alphabetical "^1.0.0" @@ -6530,7 +6500,7 @@ is-alphanumerical@^1.0.0: is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: version "3.0.5" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz" integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== dependencies: call-bind "^1.0.8" @@ -6539,12 +6509,12 @@ is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-async-function@^2.0.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + resolved "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz" integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== dependencies: async-function "^1.0.0" @@ -6555,21 +6525,21 @@ is-async-function@^2.0.0: is-bigint@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz" integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== dependencies: has-bigints "^1.0.2" is-binary-path@~2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== dependencies: binary-extensions "^2.0.0" is-boolean-object@^1.2.1: version "1.2.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz" integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== dependencies: call-bound "^1.0.3" @@ -6577,31 +6547,31 @@ is-boolean-object@^1.2.1: is-buffer@^2.0.0: version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== is-builtin-module@^3.1.0: version "3.2.1" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + resolved "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz" integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== dependencies: builtin-modules "^3.3.0" is-callable@^1.2.7: version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.16.0, is-core-module@^2.16.1: +is-core-module@^2.13.0, is-core-module@^2.16.1: version "2.16.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: hasown "^2.0.2" is-data-view@^1.0.1, is-data-view@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + resolved "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz" integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== dependencies: call-bound "^1.0.2" @@ -6610,7 +6580,7 @@ is-data-view@^1.0.1, is-data-view@^1.0.2: is-date-object@^1.0.5, is-date-object@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz" integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== dependencies: call-bound "^1.0.2" @@ -6618,81 +6588,82 @@ is-date-object@^1.0.5, is-date-object@^1.1.0: is-decimal@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" + resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-finalizationregistry@^1.1.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + resolved "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz" integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== dependencies: call-bound "^1.0.3" is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-generator-fn@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-generator-function@^1.0.10: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca" - integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ== + version "1.1.2" + resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz" + integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== dependencies: - call-bound "^1.0.3" - get-proto "^1.0.0" + call-bound "^1.0.4" + generator-function "^2.0.0" + get-proto "^1.0.1" has-tostringtag "^1.0.2" safe-regex-test "^1.1.0" is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" is-hexadecimal@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" + resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== is-map@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== is-module@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== is-negative-zero@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== is-node-process@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" + resolved "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz" integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw== is-number-object@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz" integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== dependencies: call-bound "^1.0.3" @@ -6700,54 +6671,54 @@ is-number-object@^1.1.1: is-number@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-obj@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz" integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== is-path-inside@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-obj@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== is-plain-obj@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== is-plain-object@5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== is-plain-object@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz" integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== is-potential-custom-element-name@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== is-reference@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz" integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== dependencies: "@types/estree" "*" is-regex@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz" integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== dependencies: call-bound "^1.0.2" @@ -6757,34 +6728,34 @@ is-regex@^1.2.1: is-regexp@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz" integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== is-root@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" + resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== is-set@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== is-shared-array-buffer@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz" integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== dependencies: call-bound "^1.0.3" is-stream@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== is-string@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + resolved "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz" integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== dependencies: call-bound "^1.0.3" @@ -6792,7 +6763,7 @@ is-string@^1.1.1: is-symbol@^1.0.4, is-symbol@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz" integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== dependencies: call-bound "^1.0.2" @@ -6801,31 +6772,31 @@ is-symbol@^1.0.4, is-symbol@^1.1.1: is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: version "1.1.15" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz" integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== dependencies: which-typed-array "^1.1.16" is-typedarray@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== is-weakmap@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz" integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== is-weakref@^1.0.2, is-weakref@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz" integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== dependencies: call-bound "^1.0.3" is-weakset@^2.0.3: version "2.0.4" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz" integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== dependencies: call-bound "^1.0.3" @@ -6833,44 +6804,44 @@ is-weakset@^2.0.3: is-whitespace-character@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7" + resolved "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz" integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w== is-word-character@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230" + resolved "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz" integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA== is-wsl@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" isarray@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== isarray@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz" integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== istanbul-lib-instrument@^5.0.4: version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== dependencies: "@babel/core" "^7.12.3" @@ -6881,7 +6852,7 @@ istanbul-lib-instrument@^5.0.4: istanbul-lib-instrument@^6.0.0: version "6.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz" integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== dependencies: "@babel/core" "^7.23.9" @@ -6892,7 +6863,7 @@ istanbul-lib-instrument@^6.0.0: istanbul-lib-report@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz" integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== dependencies: istanbul-lib-coverage "^3.0.0" @@ -6901,7 +6872,7 @@ istanbul-lib-report@^3.0.0: istanbul-lib-source-maps@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" @@ -6909,16 +6880,16 @@ istanbul-lib-source-maps@^4.0.0: source-map "^0.6.1" istanbul-reports@^3.1.3: - version "3.1.7" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" - integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + version "3.2.0" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz" + integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" iterator.prototype@^1.1.4: version "1.1.5" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" + resolved "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz" integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== dependencies: define-data-property "^1.1.4" @@ -6930,7 +6901,7 @@ iterator.prototype@^1.1.4: jake@^10.8.5: version "10.9.4" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6" + resolved "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz" integrity sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA== dependencies: async "^3.2.6" @@ -6939,7 +6910,7 @@ jake@^10.8.5: jest-changed-files@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz" integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== dependencies: execa "^5.0.0" @@ -6948,7 +6919,7 @@ jest-changed-files@^29.7.0: jest-circus@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz" integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== dependencies: "@jest/environment" "^29.7.0" @@ -6974,7 +6945,7 @@ jest-circus@^29.7.0: jest-cli@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz" integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== dependencies: "@jest/core" "^29.7.0" @@ -6991,7 +6962,7 @@ jest-cli@^29.7.0: jest-config@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz" integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== dependencies: "@babel/core" "^7.11.6" @@ -7019,7 +6990,7 @@ jest-config@^29.7.0: jest-diff@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz" integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== dependencies: chalk "^4.0.0" @@ -7029,7 +7000,7 @@ jest-diff@^27.5.1: jest-diff@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz" integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== dependencies: chalk "^4.0.0" @@ -7039,14 +7010,14 @@ jest-diff@^29.7.0: jest-docblock@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz" integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== dependencies: detect-newline "^3.0.0" jest-each@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz" integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== dependencies: "@jest/types" "^29.6.3" @@ -7057,7 +7028,7 @@ jest-each@^29.7.0: jest-environment-jsdom@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz#d206fa3551933c3fd519e5dfdb58a0f5139a837f" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz" integrity sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA== dependencies: "@jest/environment" "^29.7.0" @@ -7071,7 +7042,7 @@ jest-environment-jsdom@^29.7.0: jest-environment-node@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz" integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== dependencies: "@jest/environment" "^29.7.0" @@ -7083,17 +7054,17 @@ jest-environment-node@^29.7.0: jest-get-type@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz" integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== jest-get-type@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz" integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== jest-haste-map@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz" integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== dependencies: "@jest/types" "^27.5.1" @@ -7113,7 +7084,7 @@ jest-haste-map@^27.5.1: jest-haste-map@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz" integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== dependencies: "@jest/types" "^29.6.3" @@ -7132,7 +7103,7 @@ jest-haste-map@^29.7.0: jest-leak-detector@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz" integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== dependencies: jest-get-type "^29.6.3" @@ -7140,7 +7111,7 @@ jest-leak-detector@^29.7.0: jest-matcher-utils@^27.0.0: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz" integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== dependencies: chalk "^4.0.0" @@ -7150,7 +7121,7 @@ jest-matcher-utils@^27.0.0: jest-matcher-utils@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz" integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== dependencies: chalk "^4.0.0" @@ -7160,7 +7131,7 @@ jest-matcher-utils@^29.7.0: jest-message-util@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz" integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== dependencies: "@babel/code-frame" "^7.12.13" @@ -7175,7 +7146,7 @@ jest-message-util@^29.7.0: jest-mock@^29.4.0, jest-mock@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz" integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== dependencies: "@jest/types" "^29.6.3" @@ -7184,22 +7155,22 @@ jest-mock@^29.4.0, jest-mock@^29.7.0: jest-pnp-resolver@^1.2.2: version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== jest-regex-util@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz" integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== jest-regex-util@^29.0.0, jest-regex-util@^29.6.3: version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz" integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== jest-resolve-dependencies@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz" integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== dependencies: jest-regex-util "^29.6.3" @@ -7207,7 +7178,7 @@ jest-resolve-dependencies@^29.7.0: jest-resolve@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== dependencies: chalk "^4.0.0" @@ -7222,7 +7193,7 @@ jest-resolve@^29.7.0: jest-runner@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz" integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== dependencies: "@jest/console" "^29.7.0" @@ -7249,7 +7220,7 @@ jest-runner@^29.7.0: jest-runtime@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz" integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== dependencies: "@jest/environment" "^29.7.0" @@ -7277,7 +7248,7 @@ jest-runtime@^29.7.0: jest-serializer@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz" integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== dependencies: "@types/node" "*" @@ -7285,7 +7256,7 @@ jest-serializer@^27.5.1: jest-snapshot@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz" integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== dependencies: "@babel/core" "^7.11.6" @@ -7311,7 +7282,7 @@ jest-snapshot@^29.7.0: jest-util@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz" integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== dependencies: "@jest/types" "^27.5.1" @@ -7323,7 +7294,7 @@ jest-util@^27.5.1: jest-util@^29.4.0, jest-util@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== dependencies: "@jest/types" "^29.6.3" @@ -7335,7 +7306,7 @@ jest-util@^29.4.0, jest-util@^29.7.0: jest-validate@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz" integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== dependencies: "@jest/types" "^29.6.3" @@ -7347,7 +7318,7 @@ jest-validate@^29.7.0: jest-watch-typeahead@^2.2.2: version "2.2.2" - resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-2.2.2.tgz#5516d3cd006485caa5cfc9bd1de40f1f8b136abf" + resolved "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-2.2.2.tgz" integrity sha512-+QgOFW4o5Xlgd6jGS5X37i08tuuXNW8X0CV9WNFi+3n8ExCIP+E1melYhvYLjv5fE6D0yyzk74vsSO8I6GqtvQ== dependencies: ansi-escapes "^6.0.0" @@ -7360,7 +7331,7 @@ jest-watch-typeahead@^2.2.2: jest-watcher@^29.0.0, jest-watcher@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz" integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== dependencies: "@jest/test-result" "^29.7.0" @@ -7374,7 +7345,7 @@ jest-watcher@^29.0.0, jest-watcher@^29.7.0: jest-worker@^26.2.1: version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== dependencies: "@types/node" "*" @@ -7383,7 +7354,7 @@ jest-worker@^26.2.1: jest-worker@^27.0.2, jest-worker@^27.4.5, jest-worker@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: "@types/node" "*" @@ -7392,7 +7363,7 @@ jest-worker@^27.0.2, jest-worker@^27.4.5, jest-worker@^27.5.1: jest-worker@^28.0.2: version "28.1.3" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.3.tgz#7e3c4ce3fa23d1bb6accb169e7f396f98ed4bb98" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz" integrity sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g== dependencies: "@types/node" "*" @@ -7401,7 +7372,7 @@ jest-worker@^28.0.2: jest-worker@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz" integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== dependencies: "@types/node" "*" @@ -7411,7 +7382,7 @@ jest-worker@^29.7.0: jest@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== dependencies: "@jest/core" "^29.7.0" @@ -7421,40 +7392,40 @@ jest@^29.7.0: js-sha3@0.8.0: version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + version "3.14.2" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz" + integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== dependencies: argparse "^1.0.7" esprima "^4.0.0" js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + version "4.1.1" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" js2xmlparser@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" + resolved "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz" integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== dependencies: xmlcreate "^2.0.4" jsdoc@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-4.0.4.tgz#86565a9e39cc723a3640465b3fb189a22d1206ca" - integrity sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw== + version "4.0.5" + resolved "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz" + integrity sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g== dependencies: "@babel/parser" "^7.20.15" "@jsdoc/salty" "^0.2.1" @@ -7474,7 +7445,7 @@ jsdoc@^4.0.0: jsdom@^20.0.0: version "20.0.3" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" + resolved "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz" integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== dependencies: abab "^2.0.6" @@ -7504,68 +7475,63 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" -jsesc@^3.0.2: +jsesc@^3.0.2, jsesc@~3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== -jsesc@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - json-buffer@3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-schema-traverse@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-schema-traverse@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== json-schema@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json5@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" json5@^2.1.2, json5@^2.2.0, json5@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== optionalDependencies: graceful-fs "^4.1.6" jsonfile@^6.0.1: version "6.2.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz" integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== dependencies: universalify "^2.0.0" @@ -7574,7 +7540,7 @@ jsonfile@^6.0.1: jsonpath@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" + resolved "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz" integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== dependencies: esprima "1.2.2" @@ -7583,12 +7549,12 @@ jsonpath@^1.1.1: jsonpointer@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" + resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz" integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz" integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== dependencies: array-includes "^3.1.6" @@ -7596,63 +7562,68 @@ jsonpointer@^5.0.0: object.assign "^4.1.4" object.values "^1.1.6" +keycloak-js@^26.2.4: + version "26.2.4" + resolved "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.4.tgz" + integrity sha512-PnXpR3ubETGOt0B/Qt2lxmPbkZr5bc3vlQsOqDoTPPQsZRp7JjhTKxlJ187uWh8qJhvBab6Gsjb06a8ayOPfuw== + keyv@^4.5.3: version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" kind-of@^6.0.2: version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== klaw@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" + resolved "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz" integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== dependencies: graceful-fs "^4.1.9" kleur@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== klona@^2.0.4, klona@^2.0.5: version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" + resolved "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== language-subtag-registry@^0.3.20: version "0.3.23" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" + resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz" integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== language-tags@^1.0.9: version "1.0.9" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz" integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== dependencies: language-subtag-registry "^0.3.20" launch-editor@^2.6.0: - version "2.11.1" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.11.1.tgz#61a0b7314a42fd84a6cbb564573d9e9ffcf3d72b" - integrity sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg== + version "2.12.0" + resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz" + integrity sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg== dependencies: picocolors "^1.1.1" shell-quote "^1.8.3" leven@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== levn@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: prelude-ls "^1.2.1" @@ -7660,7 +7631,7 @@ levn@^0.4.1: levn@~0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== dependencies: prelude-ls "~1.1.2" @@ -7668,29 +7639,29 @@ levn@~0.3.0: lilconfig@^2.0.3: version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== lines-and-columns@^1.1.6: version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== linkify-it@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz" integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== dependencies: uc.micro "^2.0.0" -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== +loader-runner@^4.3.1: + version "4.3.1" + resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz" + integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q== loader-utils@^2.0.0, loader-utils@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== dependencies: big.js "^5.2.2" @@ -7699,12 +7670,12 @@ loader-utils@^2.0.0, loader-utils@^2.0.4: loader-utils@^3.2.0: version "3.3.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz" integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== locate-path@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz" integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== dependencies: p-locate "^3.0.0" @@ -7712,70 +7683,70 @@ locate-path@^3.0.0: locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" locate-path@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: p-locate "^5.0.0" lodash.debounce@^4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== lodash.memoize@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== lodash.merge@^4.6.2: version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash.sortby@^4.7.0: version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== lodash.uniq@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== long@^5.0.0, long@^5.2.3: version "5.3.2" - resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + resolved "https://registry.npmjs.org/long/-/long-5.3.2.tgz" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== dependencies: js-tokens "^3.0.0 || ^4.0.0" lower-case@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== dependencies: tslib "^2.0.3" lowlight@^1.17.0: version "1.20.0" - resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" + resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz" integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== dependencies: fault "^1.0.0" @@ -7783,57 +7754,57 @@ lowlight@^1.17.0: lru-cache@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" lz-string@^1.5.0: version "1.5.0" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.9" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz" integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== dependencies: sourcemap-codec "^1.4.8" make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" make-dir@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz" integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== dependencies: semver "^7.5.3" makeerror@1.0.12: version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: tmpl "1.0.5" markdown-escapes@^1.0.0: version "1.0.4" - resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" + resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== markdown-it-anchor@^8.6.7: version "8.6.7" - resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" + resolved "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz" integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== markdown-it@^14.1.0: version "14.1.0" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz" integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== dependencies: argparse "^2.0.1" @@ -7845,32 +7816,32 @@ markdown-it@^14.1.0: marked@^4.0.10: version "4.3.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== match-sorter@^6.0.2: - version "6.4.0" - resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.4.0.tgz#ae9c166cb3c9efd337690b3160c0e28cb8377c13" - integrity sha512-d4664ahzdL1QTTvmK1iI0JsrxWeJ6gn33qkYtnPg3mcn+naBLtXSgSPOe+X2vUgtgGwaAk3eiaj7gwKjjMAq+Q== + version "6.3.4" + resolved "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz" + integrity sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg== dependencies: "@babel/runtime" "^7.23.8" remove-accents "0.5.0" math-intrinsics@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== mdast-util-definitions@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" + resolved "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz" integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ== dependencies: unist-util-visit "^2.0.0" mdast-util-to-hast@^10.2.0: version "10.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz#61875526a017d8857b71abc9333942700b2d3604" + resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz" integrity sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ== dependencies: "@types/mdast" "^3.0.0" @@ -7884,74 +7855,74 @@ mdast-util-to-hast@^10.2.0: mdn-data@2.0.14: version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== mdn-data@2.0.28: version "2.0.28" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz" integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== mdn-data@2.0.30: version "2.0.30" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz" integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== mdurl@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== mdurl@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz" integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== media-typer@0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== memfs@^3.1.2, memfs@^3.4.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" - integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + version "3.5.3" + resolved "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz" + integrity sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw== dependencies: fs-monkey "^1.0.4" "memoize-one@>=3.1.1 <6": version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== memoize-one@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== merge-descriptors@1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== methods@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" @@ -7959,44 +7930,39 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: microseconds@0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" + resolved "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz" integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== -mime-db@1.52.0: +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -"mime-db@>= 1.43.0 < 2": - version "1.54.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" - integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== - mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" mime@1.6.0: version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== min-indent@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== mini-css-extract-plugin@^2.4.5: version "2.9.4" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz#cafa1a42f8c71357f49cd1566810d74ff1cb0200" + resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz" integrity sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ== dependencies: schema-utils "^4.0.0" @@ -8004,82 +7970,82 @@ mini-css-extract-plugin@^2.4.5: minimalistic-assert@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== minimatch@*, minimatch@^10.0.3: - version "10.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" - integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== + version "10.1.1" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz" + integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== dependencies: "@isaacs/brace-expansion" "^5.0.0" minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" minimatch@^5.0.1: version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== mkdirp@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== moment@^2.29.1: version "2.30.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== ms@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== msw@^2.7.0: - version "2.10.5" - resolved "https://registry.yarnpkg.com/msw/-/msw-2.10.5.tgz#3e43f12e97581c260bf38d8817732b9fec3bfdb0" - integrity sha512-0EsQCrCI1HbhpBWd89DvmxY6plmvrM96b0sCIztnvcNHQbXn5vqwm1KlXslo6u4wN9LFGLC1WFjjgljcQhe40A== + version "2.12.2" + resolved "https://registry.npmjs.org/msw/-/msw-2.12.2.tgz" + integrity sha512-Fsr8AR5Yu6C0thoWa1Z8qGBFQLDvLsWlAn/v3CNLiUizoRqBYArK3Ex3thXpMWRr1Li5/MKLOEZ5mLygUmWi1A== dependencies: - "@bundled-es-modules/cookie" "^2.0.1" - "@bundled-es-modules/statuses" "^1.0.1" - "@bundled-es-modules/tough-cookie" "^0.1.6" "@inquirer/confirm" "^5.0.0" - "@mswjs/interceptors" "^0.39.1" + "@mswjs/interceptors" "^0.40.0" "@open-draft/deferred-promise" "^2.2.0" - "@open-draft/until" "^2.1.0" - "@types/cookie" "^0.6.0" "@types/statuses" "^2.0.4" + cookie "^1.0.2" graphql "^16.8.1" headers-polyfill "^4.0.2" is-node-process "^1.2.0" outvariant "^1.4.3" path-to-regexp "^6.3.0" picocolors "^1.1.1" + rettime "^0.7.0" + statuses "^2.0.2" strict-event-emitter "^0.5.1" + tough-cookie "^6.0.0" type-fest "^4.26.1" + until-async "^3.0.2" yargs "^17.7.2" multicast-dns@^7.2.5: version "7.2.5" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz" integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== dependencies: dns-packet "^5.2.2" @@ -8087,49 +8053,49 @@ multicast-dns@^7.2.5: mute-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" + resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz" integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== nano-time@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + resolved "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz" integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA== dependencies: big-integer "^1.6.16" nanoid@^3.3.11, nanoid@^3.3.7: version "3.3.11" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== natural-compare-lite@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== negotiator@0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== negotiator@~0.6.4: version "0.6.4" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz" integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== neo-async@^2.6.2: version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== no-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== dependencies: lower-case "^2.0.2" @@ -8137,58 +8103,58 @@ no-case@^3.0.4: node-emoji@^1.10.0: version "1.11.0" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz" integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== dependencies: lodash "^4.17.21" node-forge@^1: version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== node-int64@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== +node-releases@^2.0.27: + version "2.0.27" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz" + integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== normalize-range@^0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== normalize-url@^6.0.1: version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== npm-run-path@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" nth-check@^2.0.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz" integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== dependencies: boolbase "^1.0.0" numeral@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/numeral/-/numeral-2.0.6.tgz#4ad080936d443c2561aed9f2197efffe25f4e506" + resolved "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz" integrity sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA== nwsapi@2.2.13, nwsapi@^2.2.2: @@ -8198,22 +8164,22 @@ nwsapi@2.2.13, nwsapi@^2.2.2: object-assign@^4.1.1: version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-inspect@^1.13.3, object-inspect@^1.13.4: version "1.13.4" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== object-keys@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object.assign@^4.1.4, object.assign@^4.1.7: version "4.1.7" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz" integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== dependencies: call-bind "^1.0.8" @@ -8225,7 +8191,7 @@ object.assign@^4.1.4, object.assign@^4.1.7: object.entries@^1.1.9: version "1.1.9" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" + resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz" integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== dependencies: call-bind "^1.0.8" @@ -8235,7 +8201,7 @@ object.entries@^1.1.9: object.fromentries@^2.0.8: version "2.0.8" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz" integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== dependencies: call-bind "^1.0.7" @@ -8245,7 +8211,7 @@ object.fromentries@^2.0.8: object.groupby@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + resolved "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz" integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== dependencies: call-bind "^1.0.7" @@ -8254,7 +8220,7 @@ object.groupby@^1.0.3: object.values@^1.1.6, object.values@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + resolved "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz" integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== dependencies: call-bind "^1.0.8" @@ -8264,43 +8230,43 @@ object.values@^1.1.6, object.values@^1.2.1: oblivious-set@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" + resolved "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz" integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== on-finished@2.4.1: version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== dependencies: ee-first "1.1.1" on-headers@~1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz" integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== once@^1.3.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" onetime@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" open@^8.0.9, open@^8.4.0: version "8.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== dependencies: define-lazy-prop "^2.0.0" @@ -8309,7 +8275,7 @@ open@^8.0.9, open@^8.4.0: optionator@^0.8.1: version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: deep-is "~0.1.3" @@ -8321,7 +8287,7 @@ optionator@^0.8.1: optionator@^0.9.3: version "0.9.4" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" @@ -8333,12 +8299,12 @@ optionator@^0.9.3: outvariant@^1.4.0, outvariant@^1.4.3: version "1.4.3" - resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873" + resolved "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz" integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA== own-keys@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + resolved "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz" integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== dependencies: get-intrinsic "^1.2.6" @@ -8347,42 +8313,42 @@ own-keys@^1.0.1: p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" p-locate@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz" integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== dependencies: p-limit "^2.0.0" p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== dependencies: p-limit "^2.2.0" p-locate@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: p-limit "^3.0.2" p-retry@^4.5.0: version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz" integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== dependencies: "@types/retry" "0.12.0" @@ -8390,12 +8356,12 @@ p-retry@^4.5.0: p-try@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== param-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz" integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== dependencies: dot-case "^3.0.4" @@ -8403,14 +8369,14 @@ param-case@^3.0.4: parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" parse-entities@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz" integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== dependencies: character-entities "^1.0.0" @@ -8422,7 +8388,7 @@ parse-entities@^2.0.0: parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" @@ -8432,24 +8398,24 @@ parse-json@^5.0.0, parse-json@^5.2.0: parse5@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== parse5@^7.0.0, parse5@^7.1.1: version "7.3.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz" integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== dependencies: entities "^6.0.0" parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== pascal-case@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz" integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== dependencies: no-case "^3.0.4" @@ -8457,103 +8423,103 @@ pascal-case@^3.1.2: path-exists@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-to-regexp@0.1.12: version "0.1.12" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== path-to-regexp@^6.3.0: version "6.3.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz" integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== path-type@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== performance-now@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== picocolors@1.1.1, picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== picomatch@^4.0.2: version "4.0.3" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== pirates@^4.0.4: version "4.0.7" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz" integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== dependencies: find-up "^4.0.0" pkg-up@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz" integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== dependencies: find-up "^3.0.0" possible-typed-array-names@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz" integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== postcss-attribute-case-insensitive@^5.0.2: version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz#03d761b24afc04c09e757e92ff53716ae8ea2741" + resolved "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz" integrity sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ== dependencies: postcss-selector-parser "^6.0.10" postcss-browser-comments@^4: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz#bcfc86134df5807f5d3c0eefa191d42136b5e72a" + resolved "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz" integrity sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg== postcss-calc@^8.2.3: version "8.2.4" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" + resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz" integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== dependencies: postcss-selector-parser "^6.0.9" @@ -8561,35 +8527,35 @@ postcss-calc@^8.2.3: postcss-clamp@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + resolved "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz" integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== dependencies: postcss-value-parser "^4.2.0" postcss-color-functional-notation@^4.2.4: version "4.2.4" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz#21a909e8d7454d3612d1659e471ce4696f28caec" + resolved "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz" integrity sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg== dependencies: postcss-value-parser "^4.2.0" postcss-color-hex-alpha@^8.0.4: version "8.0.4" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz#c66e2980f2fbc1a63f5b079663340ce8b55f25a5" + resolved "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz" integrity sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ== dependencies: postcss-value-parser "^4.2.0" postcss-color-rebeccapurple@^7.1.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz#63fdab91d878ebc4dd4b7c02619a0c3d6a56ced0" + resolved "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz" integrity sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg== dependencies: postcss-value-parser "^4.2.0" postcss-colormin@^5.3.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" + resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz" integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== dependencies: browserslist "^4.21.4" @@ -8599,7 +8565,7 @@ postcss-colormin@^5.3.1: postcss-convert-values@^5.1.3: version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" + resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz" integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== dependencies: browserslist "^4.21.4" @@ -8607,55 +8573,55 @@ postcss-convert-values@^5.1.3: postcss-custom-media@^8.0.2: version "8.0.2" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz#c8f9637edf45fef761b014c024cee013f80529ea" + resolved "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz" integrity sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg== dependencies: postcss-value-parser "^4.2.0" postcss-custom-properties@^12.1.10: version "12.1.11" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz#d14bb9b3989ac4d40aaa0e110b43be67ac7845cf" + resolved "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz" integrity sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ== dependencies: postcss-value-parser "^4.2.0" postcss-custom-selectors@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz#1ab4684d65f30fed175520f82d223db0337239d9" + resolved "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz" integrity sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg== dependencies: postcss-selector-parser "^6.0.4" postcss-dir-pseudo-class@^6.0.5: version "6.0.5" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz#2bf31de5de76added44e0a25ecf60ae9f7c7c26c" + resolved "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz" integrity sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA== dependencies: postcss-selector-parser "^6.0.10" postcss-discard-comments@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" + resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz" integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== postcss-discard-duplicates@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz" integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== postcss-discard-empty@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" + resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz" integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== postcss-discard-overridden@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" + resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz" integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== postcss-double-position-gradients@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz#b96318fdb477be95997e86edd29c6e3557a49b91" + resolved "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz" integrity sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -8663,55 +8629,55 @@ postcss-double-position-gradients@^3.1.2: postcss-env-function@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-4.0.6.tgz#7b2d24c812f540ed6eda4c81f6090416722a8e7a" + resolved "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz" integrity sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA== dependencies: postcss-value-parser "^4.2.0" postcss-flexbugs-fixes@^5.0.2: version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d" + resolved "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz" integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== postcss-focus-visible@^6.0.4: version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz#50c9ea9afa0ee657fb75635fabad25e18d76bf9e" + resolved "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz" integrity sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw== dependencies: postcss-selector-parser "^6.0.9" postcss-focus-within@^5.0.4: version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz#5b1d2ec603195f3344b716c0b75f61e44e8d2e20" + resolved "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz" integrity sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ== dependencies: postcss-selector-parser "^6.0.9" postcss-font-variant@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + resolved "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz" integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== postcss-gap-properties@^3.0.5: version "3.0.5" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz#f7e3cddcf73ee19e94ccf7cb77773f9560aa2fff" + resolved "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz" integrity sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg== postcss-image-set-function@^4.0.7: version "4.0.7" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz#08353bd756f1cbfb3b6e93182c7829879114481f" + resolved "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz" integrity sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw== dependencies: postcss-value-parser "^4.2.0" postcss-initial@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" + resolved "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz" integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== postcss-lab-function@^4.2.1: version "4.2.1" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz#6fe4c015102ff7cd27d1bd5385582f67ebdbdc98" + resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz" integrity sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w== dependencies: "@csstools/postcss-progressive-custom-properties" "^1.1.0" @@ -8719,7 +8685,7 @@ postcss-lab-function@^4.2.1: postcss-loader@^6.2.1: version "6.2.1" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.1.tgz#0895f7346b1702103d30fdc66e4d494a93c008ef" + resolved "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz" integrity sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q== dependencies: cosmiconfig "^7.0.0" @@ -8728,17 +8694,17 @@ postcss-loader@^6.2.1: postcss-logical@^5.0.4: version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-5.0.4.tgz#ec75b1ee54421acc04d5921576b7d8db6b0e6f73" + resolved "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz" integrity sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g== postcss-media-minmax@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz#7140bddec173e2d6d657edbd8554a55794e2a5b5" + resolved "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz" integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== postcss-merge-longhand@^5.1.7: version "5.1.7" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" + resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz" integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== dependencies: postcss-value-parser "^4.2.0" @@ -8746,7 +8712,7 @@ postcss-merge-longhand@^5.1.7: postcss-merge-rules@^5.1.4: version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" + resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz" integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== dependencies: browserslist "^4.21.4" @@ -8756,14 +8722,14 @@ postcss-merge-rules@^5.1.4: postcss-minify-font-values@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" + resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz" integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== dependencies: postcss-value-parser "^4.2.0" postcss-minify-gradients@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" + resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz" integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== dependencies: colord "^2.9.1" @@ -8772,7 +8738,7 @@ postcss-minify-gradients@^5.1.1: postcss-minify-params@^5.1.4: version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" + resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz" integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== dependencies: browserslist "^4.21.4" @@ -8781,19 +8747,19 @@ postcss-minify-params@^5.1.4: postcss-minify-selectors@^5.2.1: version "5.2.1" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" + resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz" integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== dependencies: postcss-selector-parser "^6.0.5" postcss-modules-extract-imports@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + resolved "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz" integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== postcss-modules-local-by-default@^4.0.5: version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" + resolved "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz" integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== dependencies: icss-utils "^5.0.0" @@ -8802,21 +8768,21 @@ postcss-modules-local-by-default@^4.0.5: postcss-modules-scope@^3.2.0: version "3.2.1" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c" + resolved "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz" integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== dependencies: postcss-selector-parser "^7.0.0" postcss-modules-values@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + resolved "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz" integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== dependencies: icss-utils "^5.0.0" postcss-nesting@^10.2.0: version "10.2.0" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.2.0.tgz#0b12ce0db8edfd2d8ae0aaf86427370b898890be" + resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz" integrity sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA== dependencies: "@csstools/selector-specificity" "^2.0.0" @@ -8824,47 +8790,47 @@ postcss-nesting@^10.2.0: postcss-normalize-charset@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" + resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz" integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== postcss-normalize-display-values@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" + resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz" integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-positions@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" + resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz" integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-repeat-style@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" + resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz" integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-string@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" + resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz" integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-timing-functions@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" + resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz" integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== dependencies: postcss-value-parser "^4.2.0" postcss-normalize-unicode@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" + resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz" integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== dependencies: browserslist "^4.21.4" @@ -8872,7 +8838,7 @@ postcss-normalize-unicode@^5.1.1: postcss-normalize-url@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" + resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz" integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== dependencies: normalize-url "^6.0.1" @@ -8880,14 +8846,14 @@ postcss-normalize-url@^5.1.0: postcss-normalize-whitespace@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" + resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz" integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== dependencies: postcss-value-parser "^4.2.0" postcss-normalize@^10.0.1: version "10.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize/-/postcss-normalize-10.0.1.tgz#464692676b52792a06b06880a176279216540dd7" + resolved "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz" integrity sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA== dependencies: "@csstools/normalize.css" "*" @@ -8896,12 +8862,12 @@ postcss-normalize@^10.0.1: postcss-opacity-percentage@^1.1.2: version "1.1.3" - resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz#5b89b35551a556e20c5d23eb5260fbfcf5245da6" + resolved "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz" integrity sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A== postcss-ordered-values@^5.1.3: version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" + resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz" integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== dependencies: cssnano-utils "^3.1.0" @@ -8909,26 +8875,26 @@ postcss-ordered-values@^5.1.3: postcss-overflow-shorthand@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz#7ed6486fec44b76f0eab15aa4866cda5d55d893e" + resolved "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz" integrity sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A== dependencies: postcss-value-parser "^4.2.0" postcss-page-break@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + resolved "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz" integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== postcss-place@^7.0.5: version "7.0.5" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-7.0.5.tgz#95dbf85fd9656a3a6e60e832b5809914236986c4" + resolved "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz" integrity sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g== dependencies: postcss-value-parser "^4.2.0" postcss-preset-env@^7.0.1: version "7.8.3" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz#2a50f5e612c3149cc7af75634e202a5b2ad4f1e2" + resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz" integrity sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag== dependencies: "@csstools/postcss-cascade-layers" "^1.1.1" @@ -8983,14 +8949,14 @@ postcss-preset-env@^7.0.1: postcss-pseudo-class-any-link@^7.1.6: version "7.1.6" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz#2693b221902da772c278def85a4d9a64b6e617ab" + resolved "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz" integrity sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w== dependencies: postcss-selector-parser "^6.0.10" postcss-reduce-initial@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" + resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz" integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== dependencies: browserslist "^4.21.4" @@ -8998,26 +8964,26 @@ postcss-reduce-initial@^5.1.2: postcss-reduce-transforms@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" + resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz" integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== dependencies: postcss-value-parser "^4.2.0" postcss-replace-overflow-wrap@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + resolved "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz" integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== postcss-selector-not@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz#8f0a709bf7d4b45222793fc34409be407537556d" + resolved "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz" integrity sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ== dependencies: postcss-selector-parser "^6.0.10" postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: version "6.1.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== dependencies: cssesc "^3.0.0" @@ -9025,7 +8991,7 @@ postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.4, postcss-selecto postcss-selector-parser@^7.0.0: version "7.1.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz#4d6af97eba65d73bc4d84bcb343e865d7dd16262" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz" integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA== dependencies: cssesc "^3.0.0" @@ -9033,7 +8999,7 @@ postcss-selector-parser@^7.0.0: postcss-svgo@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" + resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz" integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== dependencies: postcss-value-parser "^4.2.0" @@ -9041,19 +9007,19 @@ postcss-svgo@^5.1.0: postcss-unique-selectors@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" + resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz" integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== dependencies: postcss-selector-parser "^6.0.5" postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@8.4.49: version "8.4.49" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz" integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== dependencies: nanoid "^3.3.7" @@ -9062,7 +9028,7 @@ postcss@8.4.49: postcss@^8.2.14, postcss@^8.3.5, postcss@^8.4.33, postcss@^8.4.4: version "8.5.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: nanoid "^3.3.11" @@ -9071,27 +9037,27 @@ postcss@^8.2.14, postcss@^8.3.5, postcss@^8.4.33, postcss@^8.4.4: prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prelude-ls@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== prettier@^3.5.3: version "3.6.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" + resolved "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz" integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== pretty-error@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz" integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== dependencies: lodash "^4.17.20" @@ -9099,7 +9065,7 @@ pretty-error@^4.0.0: pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== dependencies: ansi-regex "^5.0.1" @@ -9108,38 +9074,38 @@ pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: pretty-format@^29.7.0: version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz" integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== dependencies: "@jest/schemas" "^29.6.3" ansi-styles "^5.0.0" react-is "^18.0.0" -prismjs@^1.27.0: +prismjs@^1.30.0: version "1.30.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.30.0.tgz#d9709969d9d4e16403f6f348c63553b19f0975a9" + resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz" integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw== prismjs@~1.27.0: version "1.27.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" + resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz" integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== process-nextick-args@~2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== promise@^8.1.0: version "8.3.0" - resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" + resolved "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz" integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== dependencies: asap "~2.0.6" prompts@^2.0.1, prompts@^2.4.2: version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== dependencies: kleur "^3.0.3" @@ -9147,7 +9113,7 @@ prompts@^2.0.1, prompts@^2.4.2: prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: loose-envify "^1.4.0" @@ -9156,14 +9122,14 @@ prop-types@^15.6.2, prop-types@^15.8.1: property-information@^5.0.0, property-information@^5.3.0: version "5.6.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" + resolved "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz" integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== dependencies: xtend "^4.0.0" protobufjs-cli@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz#c58b8566784f0fa1aff11e8d875a31de999637fe" + resolved "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.3.tgz" integrity sha512-MqD10lqF+FMsOayFiNOdOGNlXc4iKDCf0ZQPkPR+gizYh9gqUeGTWulABUCdI+N67w5RfJ6xhgX4J8pa8qmMXQ== dependencies: chalk "^4.0.0" @@ -9179,7 +9145,7 @@ protobufjs-cli@^1.1.3: protobufjs@^7.1.1: version "7.5.4" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz" integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== dependencies: "@protobufjs/aspromise" "^1.1.2" @@ -9197,7 +9163,7 @@ protobufjs@^7.1.1: proxy-addr@~2.0.7: version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: forwarded "0.2.0" @@ -9205,36 +9171,36 @@ proxy-addr@~2.0.7: psl@^1.1.33: version "1.15.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6" + resolved "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz" integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== dependencies: punycode "^2.3.1" punycode.js@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz" integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== pure-rand@^6.0.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== qs@6.13.0: version "6.13.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: side-channel "^1.0.6" query-string@^7.1.1: version "7.1.3" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" + resolved "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz" integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== dependencies: decode-uri-component "^0.2.2" @@ -9244,41 +9210,41 @@ query-string@^7.1.1: querystringify@^2.1.1: version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== queue-microtask@^1.2.2: version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== raf-schd@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== raf@^3.4.1: version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== dependencies: performance-now "^2.1.0" randombytes@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== raw-body@2.5.2: version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" @@ -9288,7 +9254,7 @@ raw-body@2.5.2: react-app-polyfill@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz#95221e0a9bd259e5ca6b177c7bb1cb6768f68fd7" + resolved "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz" integrity sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w== dependencies: core-js "^3.19.2" @@ -9300,14 +9266,14 @@ react-app-polyfill@^3.0.0: react-clientside-effect@^1.2.7: version "1.2.8" - resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.8.tgz#0b90a9d7b2a1823a3a10ed1ea3f651f7e0301cb7" + resolved "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.8.tgz" integrity sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw== dependencies: "@babel/runtime" "^7.12.13" react-code-blocks@^0.1.6: version "0.1.6" - resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.1.6.tgz#ec64e7899223d3e910eb916465a66d95ce1ae1b2" + resolved "https://registry.npmjs.org/react-code-blocks/-/react-code-blocks-0.1.6.tgz" integrity sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg== dependencies: "@babel/runtime" "^7.10.4" @@ -9317,7 +9283,7 @@ react-code-blocks@^0.1.6: react-dev-utils@^12.0.1: version "12.0.1" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" + resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz" integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== dependencies: "@babel/code-frame" "^7.16.0" @@ -9347,7 +9313,7 @@ react-dev-utils@^12.0.1: react-dom@^18.3.1: version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== dependencies: loose-envify "^1.1.0" @@ -9355,7 +9321,7 @@ react-dom@^18.3.1: react-dropzone@^11.7.1: version "11.7.1" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356" + resolved "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.7.1.tgz" integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ== dependencies: attr-accept "^2.2.2" @@ -9364,7 +9330,7 @@ react-dropzone@^11.7.1: react-element-to-jsx-string@^15.0.0: version "15.0.0" - resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz#1cafd5b6ad41946ffc8755e254da3fc752a01ac6" + resolved "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz" integrity sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ== dependencies: "@base2/pretty-print-object" "1.0.1" @@ -9373,12 +9339,12 @@ react-element-to-jsx-string@^15.0.0: react-error-overlay@^6.0.11: version "6.1.0" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.1.0.tgz#22b86256beb1c5856f08a9a228adb8121dd985f2" + resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz" integrity sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ== react-focus-lock@^2.13.6: version "2.13.6" - resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.13.6.tgz#29751bf2e4e30f6248673cd87a347c74ff2af672" + resolved "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.13.6.tgz" integrity sha512-ehylFFWyYtBKXjAO9+3v8d0i+cnc1trGS0vlTGhzFW1vbFXVUTmR8s2tt/ZQG8x5hElg6rhENlLG1H3EZK0Llg== dependencies: "@babel/runtime" "^7.0.0" @@ -9390,7 +9356,7 @@ react-focus-lock@^2.13.6: react-focus-on@^3.9.1: version "3.10.0" - resolved "https://registry.yarnpkg.com/react-focus-on/-/react-focus-on-3.10.0.tgz#60f6af03b59be5a0901f86cf9e24799c33e90327" + resolved "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.10.0.tgz" integrity sha512-r2yQchO6QfV5zB3J4Gj6cTYBoxD369vkt0oKj1NJLA5ChQzxjko6V/dqQ7nvmaUBm5pHC+pa8tzHT9jtsVRFMQ== dependencies: aria-hidden "^1.2.5" @@ -9402,27 +9368,27 @@ react-focus-on@^3.9.1: react-is@18.1.0: version "18.1.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz" integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-is@^18.0.0: version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== react-query@^3.39.3: version "3.39.3" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" + resolved "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz" integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== dependencies: "@babel/runtime" "^7.5.5" @@ -9431,7 +9397,7 @@ react-query@^3.39.3: react-redux@^8.1.3: version "8.1.3" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" + resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz" integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== dependencies: "@babel/runtime" "^7.12.1" @@ -9443,12 +9409,12 @@ react-redux@^8.1.3: react-refresh@^0.11.0: version "0.11.0" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.7: version "2.3.8" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" + resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz" integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== dependencies: react-style-singleton "^2.2.2" @@ -9456,7 +9422,7 @@ react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.7: react-remove-scroll@^2.6.3: version "2.7.1" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz#d2101d414f6d81d7d3bf033f3c1cb4785789f753" + resolved "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz" integrity sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA== dependencies: react-remove-scroll-bar "^2.3.7" @@ -9466,48 +9432,48 @@ react-remove-scroll@^2.6.3: use-sidecar "^1.1.3" react-router-dom@^6.28.0: - version "6.30.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.30.1.tgz#da2580c272ddb61325e435478566be9563a4a237" - integrity sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw== + version "6.30.2" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz" + integrity sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q== dependencies: - "@remix-run/router" "1.23.0" - react-router "6.30.1" + "@remix-run/router" "1.23.1" + react-router "6.30.2" -react-router@6.30.1: - version "6.30.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.30.1.tgz#ecb3b883c9ba6dbf5d319ddbc996747f4ab9f4c3" - integrity sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ== +react-router@6.30.2: + version "6.30.2" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz" + integrity sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA== dependencies: - "@remix-run/router" "1.23.0" + "@remix-run/router" "1.23.1" react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" + resolved "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz" integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== dependencies: get-nonce "^1.0.0" tslib "^2.0.0" react-syntax-highlighter@^15.5.0: - version "15.6.1" - resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz#fa567cb0a9f96be7bbccf2c13a3c4b5657d9543e" - integrity sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg== + version "15.6.6" + resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz" + integrity sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw== dependencies: "@babel/runtime" "^7.3.1" highlight.js "^10.4.1" highlightjs-vue "^1.0.0" lowlight "^1.17.0" - prismjs "^1.27.0" + prismjs "^1.30.0" refractor "^3.6.0" react-virtualized-auto-sizer@^1.0.24: version "1.0.26" - resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz#e9470ef6a778dc4f1d5fd76305fa2d8b610c357a" + resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz" integrity sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A== react-window@^1.8.10: version "1.8.11" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525" + resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz" integrity sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ== dependencies: "@babel/runtime" "^7.0.0" @@ -9515,14 +9481,14 @@ react-window@^1.8.10: react@^18.3.1: version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== dependencies: loose-envify "^1.1.0" reactflow@^11.11.4: version "11.11.4" - resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.4.tgz#e3593e313420542caed81aecbd73fb9bc6576653" + resolved "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz" integrity sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og== dependencies: "@reactflow/background" "11.3.14" @@ -9534,7 +9500,7 @@ reactflow@^11.11.4: readable-stream@^2.0.1: version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== dependencies: core-util-is "~1.0.0" @@ -9547,7 +9513,7 @@ readable-stream@^2.0.1: readable-stream@^3.0.6: version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" @@ -9556,21 +9522,21 @@ readable-stream@^3.0.6: readdirp@~3.6.0: version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" recursive-readdir@^2.2.2: version "2.2.3" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" + resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz" integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== dependencies: minimatch "^3.0.5" redent@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== dependencies: indent-string "^4.0.0" @@ -9578,14 +9544,14 @@ redent@^3.0.0: redux@^4.2.1: version "4.2.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== dependencies: "@babel/runtime" "^7.9.2" reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + resolved "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz" integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== dependencies: call-bind "^1.0.8" @@ -9599,38 +9565,38 @@ reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: refractor@^3.6.0: version "3.6.0" - resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" + resolved "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz" integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== dependencies: hastscript "^6.0.0" parse-entities "^2.0.0" prismjs "~1.27.0" -regenerate-unicode-properties@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" - integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== dependencies: regenerate "^1.4.2" regenerate@^1.4.2: version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== regenerator-runtime@^0.13.9: version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regex-parser@^2.2.11: version "2.3.1" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.3.1.tgz#ee3f70e50bdd81a221d505242cb9a9c275a2ad91" + resolved "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz" integrity sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ== regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: version "1.5.4" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz" integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== dependencies: call-bind "^1.0.8" @@ -9640,40 +9606,40 @@ regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: gopd "^1.2.0" set-function-name "^2.0.2" -regexpu-core@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" - integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== dependencies: regenerate "^1.4.2" - regenerate-unicode-properties "^10.2.0" + regenerate-unicode-properties "^10.2.2" regjsgen "^0.8.0" - regjsparser "^0.12.0" + regjsparser "^0.13.0" unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" + unicode-match-property-value-ecmascript "^2.2.1" regjsgen@^0.8.0: version "0.8.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz" integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== -regjsparser@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" - integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== +regjsparser@^0.13.0: + version "0.13.0" + resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz" + integrity sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q== dependencies: - jsesc "~3.0.2" + jsesc "~3.1.0" rehype-raw@^5.1.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-5.1.0.tgz#66d5e8d7188ada2d31bc137bc19a1000cf2c6b7e" + resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-5.1.0.tgz" integrity sha512-MDvHAb/5mUnif2R+0IPCYJU8WjHa9UzGtM/F4AVy5GixPlDZ1z3HacYy4xojDU+uBa+0X/3PIfyQI26/2ljJNA== dependencies: hast-util-raw "^6.1.0" rehype-react@^6.2.1: version "6.2.1" - resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a" + resolved "https://registry.npmjs.org/rehype-react/-/rehype-react-6.2.1.tgz" integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg== dependencies: "@mapbox/hast-util-table-cell-style" "^0.2.0" @@ -9681,26 +9647,26 @@ rehype-react@^6.2.1: rehype-stringify@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-8.0.0.tgz#9b6afb599bcf3165f10f93fc8548f9a03d2ec2ba" + resolved "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-8.0.0.tgz" integrity sha512-VkIs18G0pj2xklyllrPSvdShAV36Ff3yE5PUO9u36f6+2qJFnn22Z5gKwBOwgXviux4UC7K+/j13AnZfPICi/g== dependencies: hast-util-to-html "^7.1.1" relateurl@^0.2.7: version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + resolved "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz" integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== remark-breaks@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/remark-breaks/-/remark-breaks-2.0.2.tgz#55fdec6c7da84f659aa7fdb1aa95b632870cee8d" + resolved "https://registry.npmjs.org/remark-breaks/-/remark-breaks-2.0.2.tgz" integrity sha512-LsQnPPQ7Fzp9RTjj4IwdEmjPOr9bxe9zYKWhs9ZQOg9hMg8rOfeeqQ410cvVdIK87Famqza1CKRxNkepp2EvUA== dependencies: unist-util-visit "^2.0.0" remark-emoji@^2.1.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-2.2.0.tgz#1c702090a1525da5b80e15a8f963ef2c8236cac7" + resolved "https://registry.npmjs.org/remark-emoji/-/remark-emoji-2.2.0.tgz" integrity sha512-P3cj9s5ggsUvWw5fS2uzCHJMGuXYRb0NnZqYlNecewXt8QBU9n5vW3DUUKOhepS8F9CwdMx9B8a3i7pqFWAI5w== dependencies: emoticon "^3.2.0" @@ -9709,7 +9675,7 @@ remark-emoji@^2.1.0: remark-parse-no-trim@^8.0.4: version "8.0.4" - resolved "https://registry.yarnpkg.com/remark-parse-no-trim/-/remark-parse-no-trim-8.0.4.tgz#f5c9531644284071d4a57a49e19a42ad4e8040bd" + resolved "https://registry.npmjs.org/remark-parse-no-trim/-/remark-parse-no-trim-8.0.4.tgz" integrity sha512-WtqeHNTZ0LSdyemmY1/G6y9WoEFblTtgckfKF5/NUnri919/0/dEu8RCDfvXtJvu96soMvT+mLWWgYVUaiHoag== dependencies: ccount "^1.0.0" @@ -9730,19 +9696,19 @@ remark-parse-no-trim@^8.0.4: remark-rehype@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-8.1.0.tgz#610509a043484c1e697437fa5eb3fd992617c945" + resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-8.1.0.tgz" integrity sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA== dependencies: mdast-util-to-hast "^10.2.0" remove-accents@0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" + resolved "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz" integrity sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A== renderkid@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + resolved "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz" integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== dependencies: css-select "^4.1.3" @@ -9753,51 +9719,51 @@ renderkid@^3.0.0: repeat-string@^1.5.4: version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-from-string@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== requires-port@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== requizzle@^0.2.3: version "0.2.4" - resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" + resolved "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz" integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== dependencies: lodash "^4.17.21" resolve-cwd@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== dependencies: resolve-from "^5.0.0" resolve-from@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-from@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve-url-loader@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795" + resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz" integrity sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg== dependencies: adjust-sourcemap-loader "^4.0.0" @@ -9808,21 +9774,21 @@ resolve-url-loader@^5.0.0: resolve.exports@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz" integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.10, resolve@^1.22.4: - version "1.22.10" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" - integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + version "1.22.11" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== dependencies: - is-core-module "^2.16.0" + is-core-module "^2.16.1" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" resolve@^2.0.0-next.5: version "2.0.0-next.5" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz" integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== dependencies: is-core-module "^2.13.0" @@ -9831,24 +9797,29 @@ resolve@^2.0.0-next.5: retry@^0.13.1: version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== +rettime@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz" + integrity sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw== + reusify@^1.0.4: version "1.1.0" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" rollup-plugin-copy@^3.5.0: version "3.5.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz#7ffa2a7a8303e143876fa64fb5eed9022d304eeb" + resolved "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.5.0.tgz" integrity sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA== dependencies: "@types/fs-extra" "^8.0.1" @@ -9859,28 +9830,28 @@ rollup-plugin-copy@^3.5.0: rollup-plugin-import-css@^3.0.2: version "3.5.8" - resolved "https://registry.yarnpkg.com/rollup-plugin-import-css/-/rollup-plugin-import-css-3.5.8.tgz#f1f7b61ae56c3e1edc9c71dcfde85e5464500cf8" + resolved "https://registry.npmjs.org/rollup-plugin-import-css/-/rollup-plugin-import-css-3.5.8.tgz" integrity sha512-a3YsZnwHz66mRHCKHjaPCSfWczczvS/HTkgDc+Eogn0mt/0JZXz0WjK0fzM5WwBpVtOqHB4/gHdmEY40ILsaVg== dependencies: "@rollup/pluginutils" "^5.1.3" rollup-plugin-svg@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-svg/-/rollup-plugin-svg-2.0.0.tgz#ce11b55e915d5b2190328c4e6632bd6b4fe12ee9" + resolved "https://registry.npmjs.org/rollup-plugin-svg/-/rollup-plugin-svg-2.0.0.tgz" integrity sha512-DmE7dSQHo1SC5L2uH2qul3Mjyd5oV6U1aVVkyvTLX/mUsRink7f1b1zaIm+32GEBA6EHu8H/JJi3DdWqM53ySQ== dependencies: rollup-pluginutils "^1.3.1" rollup-plugin-svgo@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-svgo/-/rollup-plugin-svgo-2.0.0.tgz#d182c145fd11f3f8a43de804e3f5b4b70d580a7a" + resolved "https://registry.npmjs.org/rollup-plugin-svgo/-/rollup-plugin-svgo-2.0.0.tgz" integrity sha512-0ryWbGY3sP62brw5p8md5W+1WUMrLUE8d437nGh9gQ+fZFFjlYbyIkctBrTvCm3bIdqN4gxbxg1Xxen1tteD+A== dependencies: svgo "2.8.0" rollup-plugin-terser@^7.0.0, rollup-plugin-terser@^7.0.2: version "7.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + resolved "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz" integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== dependencies: "@babel/code-frame" "^7.10.4" @@ -9890,7 +9861,7 @@ rollup-plugin-terser@^7.0.0, rollup-plugin-terser@^7.0.2: rollup-pluginutils@^1.3.1: version "1.5.2" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408" + resolved "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz" integrity sha512-SjdWWWO/CUoMpDy8RUbZ/pSpG68YHmhk5ROKNIoi2En9bJ8bTt3IhYi254RWiTclQmL7Awmrq+rZFOhZkJAHmQ== dependencies: estree-walker "^0.2.1" @@ -9898,21 +9869,21 @@ rollup-pluginutils@^1.3.1: rollup@^2.43.1, rollup@^2.68.0: version "2.79.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz" integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ== optionalDependencies: fsevents "~2.3.2" run-parallel@^1.1.9: version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: queue-microtask "^1.2.2" safe-array-concat@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz" integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== dependencies: call-bind "^1.0.8" @@ -9923,17 +9894,17 @@ safe-array-concat@^1.1.3: safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-push-apply@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + resolved "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz" integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== dependencies: es-errors "^1.3.0" @@ -9941,7 +9912,7 @@ safe-push-apply@^1.0.0: safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz" integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== dependencies: call-bound "^1.0.2" @@ -9950,17 +9921,17 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sanitize.css@*: version "13.0.0" - resolved "https://registry.yarnpkg.com/sanitize.css/-/sanitize.css-13.0.0.tgz#2675553974b27964c75562ade3bd85d79879f173" + resolved "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz" integrity sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA== sass-loader@^12.3.0: version "12.6.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb" + resolved "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz" integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA== dependencies: klona "^2.0.4" @@ -9968,21 +9939,21 @@ sass-loader@^12.3.0: saxes@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + resolved "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz" integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== dependencies: xmlchars "^2.2.0" scheduler@^0.23.2: version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz" integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== dependencies: loose-envify "^1.1.0" schema-utils@2.7.0: version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== dependencies: "@types/json-schema" "^7.0.4" @@ -9991,7 +9962,7 @@ schema-utils@2.7.0: schema-utils@^2.6.5: version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== dependencies: "@types/json-schema" "^7.0.5" @@ -10000,17 +9971,17 @@ schema-utils@^2.6.5: schema-utils@^3.0.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== dependencies: "@types/json-schema" "^7.0.8" ajv "^6.12.5" ajv-keywords "^3.5.2" -schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" - integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== +schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.3: + version "4.3.3" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz" + integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -10019,12 +9990,12 @@ schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3 select-hose@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== selfsigned@^2.1.1: version "2.4.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz" integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== dependencies: "@types/node-forge" "^1.3.0" @@ -10032,17 +10003,17 @@ selfsigned@^2.1.1: semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.1.2, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4: - version "7.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" - integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + version "7.7.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== send@0.19.0: version "0.19.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" @@ -10061,26 +10032,26 @@ send@0.19.0: serialize-javascript@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz" integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== dependencies: randombytes "^2.1.0" serialize-javascript@^6.0.0, serialize-javascript@^6.0.2: version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" serialize-query-params@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/serialize-query-params/-/serialize-query-params-2.0.2.tgz#598a3fb9e13f4ea1c1992fbd20231aa16b31db81" + resolved "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.2.tgz" integrity sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q== serve-index@^1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== dependencies: accepts "~1.3.4" @@ -10093,7 +10064,7 @@ serve-index@^1.9.1: serve-static@1.16.2: version "1.16.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: encodeurl "~2.0.0" @@ -10103,7 +10074,7 @@ serve-static@1.16.2: set-function-length@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: define-data-property "^1.1.4" @@ -10115,7 +10086,7 @@ set-function-length@^1.2.2: set-function-name@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + resolved "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz" integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== dependencies: define-data-property "^1.1.4" @@ -10125,7 +10096,7 @@ set-function-name@^2.0.2: set-proto@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + resolved "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz" integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== dependencies: dunder-proto "^1.0.1" @@ -10134,39 +10105,39 @@ set-proto@^1.0.0: setprototypeof@1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== setprototypeof@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shallowequal@1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.7.3, shell-quote@^1.8.3: version "1.8.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" + resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz" integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== side-channel-list@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz" integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: es-errors "^1.3.0" @@ -10174,7 +10145,7 @@ side-channel-list@^1.0.0: side-channel-map@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + resolved "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz" integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== dependencies: call-bound "^1.0.2" @@ -10184,7 +10155,7 @@ side-channel-map@^1.0.1: side-channel-weakmap@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz" integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== dependencies: call-bound "^1.0.2" @@ -10195,7 +10166,7 @@ side-channel-weakmap@^1.0.2: side-channel@^1.0.6, side-channel@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz" integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== dependencies: es-errors "^1.3.0" @@ -10206,32 +10177,32 @@ side-channel@^1.0.6, side-channel@^1.1.0: signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== signal-exit@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== sisteransi@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== slash@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== slash@^5.0.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" + resolved "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz" integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== snake-case@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz" integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== dependencies: dot-case "^3.0.4" @@ -10239,7 +10210,7 @@ snake-case@^3.0.4: sockjs@^0.3.24: version "0.3.24" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz" integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== dependencies: faye-websocket "^0.11.3" @@ -10248,17 +10219,17 @@ sockjs@^0.3.24: source-list-map@^2.0.0, source-list-map@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== source-map-js@^1.0.1, source-map-js@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== source-map-loader@^3.0.0: version "3.0.2" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.2.tgz#af23192f9b344daa729f6772933194cc5fa54fee" + resolved "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz" integrity sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg== dependencies: abab "^2.0.5" @@ -10267,7 +10238,7 @@ source-map-loader@^3.0.0: source-map-support@0.5.13: version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== dependencies: buffer-from "^1.0.0" @@ -10275,7 +10246,7 @@ source-map-support@0.5.13: source-map-support@~0.5.20: version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" @@ -10283,39 +10254,39 @@ source-map-support@~0.5.20: source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== source-map@^0.5.7: version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== source-map@^0.7.3: version "0.7.6" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.6.tgz#a3658ab87e5b6429c8a1f3ba0083d4c61ca3ef02" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz" integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ== source-map@^0.8.0-beta.0: version "0.8.0-beta.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz" integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== dependencies: whatwg-url "^7.0.0" sourcemap-codec@^1.4.8: version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== space-separated-tokens@^1.0.0: version "1.1.5" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" + resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz" integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== spdy-transport@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== dependencies: debug "^4.1.0" @@ -10327,7 +10298,7 @@ spdy-transport@^3.0.0: spdy@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== dependencies: debug "^4.1.0" @@ -10338,61 +10309,61 @@ spdy@^4.0.2: split-on-first@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== stable@^0.1.8: version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== stack-utils@^2.0.3: version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== dependencies: escape-string-regexp "^2.0.0" stackframe@^1.3.4: version "1.3.4" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz" integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== state-toggle@^1.0.0: version "1.0.3" - resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" + resolved "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz" integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ== static-eval@2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.2.tgz#2d1759306b1befa688938454c546b7871f806a42" + resolved "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz" integrity sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== dependencies: escodegen "^1.8.1" statuses@2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== "statuses@>= 1.4.0 < 2": version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -statuses@^2.0.1: +statuses@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz" integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== stop-iteration-iterator@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz" integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== dependencies: es-errors "^1.3.0" @@ -10400,17 +10371,17 @@ stop-iteration-iterator@^1.1.0: strict-event-emitter@^0.5.1: version "0.5.1" - resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93" + resolved "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz" integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ== strict-uri-encode@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== string-length@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== dependencies: char-regex "^1.0.2" @@ -10418,7 +10389,7 @@ string-length@^4.0.1: string-length@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-5.0.1.tgz#3d647f497b6e8e8d41e422f7e0b23bc536c8381e" + resolved "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz" integrity sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow== dependencies: char-regex "^2.0.0" @@ -10426,12 +10397,12 @@ string-length@^5.0.1: string-natural-compare@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" + resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -10440,7 +10411,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: string.prototype.includes@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92" + resolved "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz" integrity sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== dependencies: call-bind "^1.0.7" @@ -10449,7 +10420,7 @@ string.prototype.includes@^2.0.1: string.prototype.matchall@^4.0.12, string.prototype.matchall@^4.0.6: version "4.0.12" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz" integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== dependencies: call-bind "^1.0.8" @@ -10468,7 +10439,7 @@ string.prototype.matchall@^4.0.12, string.prototype.matchall@^4.0.6: string.prototype.repeat@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + resolved "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz" integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== dependencies: define-properties "^1.1.3" @@ -10476,7 +10447,7 @@ string.prototype.repeat@^1.0.0: string.prototype.trim@^1.2.10: version "1.2.10" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz" integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== dependencies: call-bind "^1.0.8" @@ -10489,7 +10460,7 @@ string.prototype.trim@^1.2.10: string.prototype.trimend@^1.0.9: version "1.0.9" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz" integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== dependencies: call-bind "^1.0.8" @@ -10499,7 +10470,7 @@ string.prototype.trimend@^1.0.9: string.prototype.trimstart@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz" integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: call-bind "^1.0.7" @@ -10508,21 +10479,21 @@ string.prototype.trimstart@^1.0.8: string_decoder@^1.1.1: version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" string_decoder@~1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" stringify-entities@^3.0.1: version "3.1.0" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-3.1.0.tgz#b8d3feac256d9ffcc9fa1fefdcf3ca70576ee903" + resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.1.0.tgz" integrity sha512-3FP+jGMmMV/ffZs86MoghGqAoqXAdxLrJP4GUdrDN1aIScYih5tuIO3eF4To5AJZ79KDZ8Fpdy7QJnK8SsL1Vg== dependencies: character-entities-html4 "^1.0.0" @@ -10531,7 +10502,7 @@ stringify-entities@^3.0.1: stringify-object@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + resolved "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz" integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== dependencies: get-own-enumerable-property-symbols "^3.0.0" @@ -10540,65 +10511,65 @@ stringify-object@^3.3.0: strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + version "7.1.2" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== dependencies: ansi-regex "^6.0.1" strip-bom@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== strip-bom@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== strip-comments@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" + resolved "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz" integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== strip-final-newline@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-indent@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== dependencies: min-indent "^1.0.0" strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== style-loader@^3.3.1: version "3.3.4" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.4.tgz#f30f786c36db03a45cbd55b6a70d930c479090e7" + resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz" integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== style-to-object@^0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" + resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz" integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== dependencies: inline-style-parser "0.1.1" styled-components@^6.1.0: version "6.1.19" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.19.tgz#9a41b4db79a3b7a2477daecabe8dd917235263d6" + resolved "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz" integrity sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA== dependencies: "@emotion/is-prop-valid" "1.2.2" @@ -10613,7 +10584,7 @@ styled-components@^6.1.0: stylehacks@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" + resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz" integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== dependencies: browserslist "^4.21.4" @@ -10621,41 +10592,41 @@ stylehacks@^5.1.1: stylis@4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== stylis@4.3.2: version "4.3.2" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz" integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" supports-color@^8.0.0: version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== svg-parser@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== svgo@2.8.0, svgo@^2.7.0: version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz" integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== dependencies: "@trysound/sax" "0.2.0" @@ -10668,7 +10639,7 @@ svgo@2.8.0, svgo@^2.7.0: svgo@^3.0.2: version "3.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.2.tgz#ad58002652dffbb5986fc9716afe52d869ecbda8" + resolved "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz" integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw== dependencies: "@trysound/sax" "0.2.0" @@ -10681,32 +10652,32 @@ svgo@^3.0.2: symbol-tree@^3.2.4: version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== tabbable@^5.3.3: version "5.3.3" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" + resolved "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz" integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== tapable@^1.0.0: version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.2.tgz#ab4984340d30cb9989a490032f086dbb8b56d872" - integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg== +tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz" + integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== temp-dir@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz" integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== tempy@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" + resolved "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz" integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== dependencies: is-stream "^2.0.0" @@ -10716,7 +10687,7 @@ tempy@^0.6.0: terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.11: version "5.3.14" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz" integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== dependencies: "@jridgewell/trace-mapping" "^0.3.25" @@ -10726,18 +10697,18 @@ terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.11: terser "^5.31.1" terser@^5.0.0, terser@^5.10.0, terser@^5.31.1: - version "5.43.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.43.1.tgz#88387f4f9794ff1a29e7ad61fb2932e25b4fdb6d" - integrity sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg== + version "5.44.1" + resolved "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz" + integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw== dependencies: "@jridgewell/source-map" "^0.3.3" - acorn "^8.14.0" + acorn "^8.15.0" commander "^2.20.0" source-map-support "~0.5.20" test-exclude@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== dependencies: "@istanbuljs/schema" "^0.1.2" @@ -10746,49 +10717,61 @@ test-exclude@^6.0.0: text-diff@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/text-diff/-/text-diff-1.0.1.tgz#6c105905435e337857375c9d2f6ca63e453ff565" + resolved "https://registry.npmjs.org/text-diff/-/text-diff-1.0.1.tgz" integrity sha512-jAnlP3ggZk7FeLX1awaMR8Y2sMyil9P9FXvNjaIJIQBAom1zvpKGGH31htOVrUFp0vlyygmJJpNrbJ4rfjsxrA== text-table@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== thunky@^1.0.2: version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== tiny-invariant@^1.0.6: version "1.3.3" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== +tldts-core@^7.0.18: + version "7.0.18" + resolved "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.18.tgz" + integrity sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q== + +tldts@^7.0.5: + version "7.0.18" + resolved "https://registry.npmjs.org/tldts/-/tldts-7.0.18.tgz" + integrity sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw== + dependencies: + tldts-core "^7.0.18" + tmp@^0.2.1: version "0.2.5" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz" integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== tmpl@1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" toidentifier@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -tough-cookie@^4.1.2, tough-cookie@^4.1.4: +tough-cookie@^4.1.2: version "4.1.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz" integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== dependencies: psl "^1.1.33" @@ -10796,38 +10779,45 @@ tough-cookie@^4.1.2, tough-cookie@^4.1.4: universalify "^0.2.0" url-parse "^1.5.3" +tough-cookie@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz" + integrity sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w== + dependencies: + tldts "^7.0.5" + tr46@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz" integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== dependencies: punycode "^2.1.0" tr46@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + resolved "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz" integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== dependencies: punycode "^2.1.1" trim-trailing-lines@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0" + resolved "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz" integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ== trough@^1.0.0: version "1.0.5" - resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" + resolved "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== tryer@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" + resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== tsconfig-paths@^3.15.0: version "3.15.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" @@ -10835,70 +10825,70 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2: +tslib@2.6.2, tslib@^2.6.0: version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.6.0: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tsutils@^3.21.0: version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: prelude-ls "^1.2.1" type-check@~0.3.2: version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== dependencies: prelude-ls "~1.1.2" type-detect@4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.16.0: version "0.16.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz" integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== type-fest@^0.20.2: version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== type-fest@^0.21.3: version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-fest@^4.26.1: version "4.41.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== type-is@~1.6.18: version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" @@ -10906,7 +10896,7 @@ type-is@~1.6.18: typed-array-buffer@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz" integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== dependencies: call-bound "^1.0.3" @@ -10915,7 +10905,7 @@ typed-array-buffer@^1.0.3: typed-array-byte-length@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + resolved "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz" integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== dependencies: call-bind "^1.0.8" @@ -10926,7 +10916,7 @@ typed-array-byte-length@^1.0.3: typed-array-byte-offset@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + resolved "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz" integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== dependencies: available-typed-arrays "^1.0.7" @@ -10939,7 +10929,7 @@ typed-array-byte-offset@^1.0.4: typed-array-length@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz" integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== dependencies: call-bind "^1.0.7" @@ -10951,29 +10941,29 @@ typed-array-length@^1.0.7: typedarray-to-buffer@^3.1.5: version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== dependencies: is-typedarray "^1.0.0" typescript@~5.7.2: version "5.7.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz" integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz" integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== uglify-js@^3.7.7: version "3.19.3" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== unbox-primitive@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz" integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== dependencies: call-bound "^1.0.3" @@ -10983,27 +10973,22 @@ unbox-primitive@^1.1.0: underscore@1.12.1: version "1.12.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== underscore@~1.13.2: version "1.13.7" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz" integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== undici-types@~6.21.0: version "6.21.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== -undici-types@~7.10.0: - version "7.10.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" - integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== - unherit@^1.0.4: version "1.1.3" - resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" + resolved "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz" integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ== dependencies: inherits "^2.0.0" @@ -11011,30 +10996,30 @@ unherit@^1.0.4: unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz" integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== dependencies: unicode-canonical-property-names-ecmascript "^2.0.0" unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" - integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + version "2.2.0" + resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz" + integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== unified@^9.2.2: version "9.2.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" + resolved "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz" integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== dependencies: bail "^1.0.0" @@ -11046,60 +11031,60 @@ unified@^9.2.2: unique-string@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz" integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== dependencies: crypto-random-string "^2.0.0" unist-builder@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" + resolved "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz" integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== unist-util-generated@^1.0.0: version "1.1.6" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" + resolved "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz" integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg== unist-util-is@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz" integrity sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A== unist-util-is@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz" integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== unist-util-position@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" + resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz" integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== unist-util-remove-position@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc" + resolved "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz" integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA== dependencies: unist-util-visit "^2.0.0" unist-util-stringify-position@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" + resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz" integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== dependencies: "@types/unist" "^2.0.2" unist-util-visit-parents@^2.0.0: version "2.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz#25e43e55312166f3348cae6743588781d112c1e9" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz" integrity sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g== dependencies: unist-util-is "^3.0.0" unist-util-visit-parents@^3.0.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz" integrity sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== dependencies: "@types/unist" "^2.0.0" @@ -11107,14 +11092,14 @@ unist-util-visit-parents@^3.0.0: unist-util-visit@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz" integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw== dependencies: unist-util-visit-parents "^2.0.0" unist-util-visit@^2.0.0, unist-util-visit@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz" integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== dependencies: "@types/unist" "^2.0.0" @@ -11123,22 +11108,22 @@ unist-util-visit@^2.0.0, unist-util-visit@^2.0.3: universalify@^0.1.0: version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== unload@2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + resolved "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz" integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== dependencies: "@babel/runtime" "^7.6.2" @@ -11146,32 +11131,37 @@ unload@2.2.0: unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +until-async@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz" + integrity sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw== + upath@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-browserslist-db@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" - integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== +update-browserslist-db@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz" + integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A== dependencies: escalade "^3.2.0" picocolors "^1.1.1" uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" url-parse@^1.5.10, url-parse@^1.5.3: version "1.5.10" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== dependencies: querystringify "^2.1.1" @@ -11179,59 +11169,59 @@ url-parse@^1.5.10, url-parse@^1.5.3: use-callback-ref@^1.3.3: version "1.3.3" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" + resolved "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz" integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== dependencies: tslib "^2.0.0" use-memo-one@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz" integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== use-query-params@^2.2.1: version "2.2.1" - resolved "https://registry.yarnpkg.com/use-query-params/-/use-query-params-2.2.1.tgz#c558ab70706f319112fbccabf6867b9f904e947d" + resolved "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz" integrity sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q== dependencies: serialize-query-params "^2.0.2" use-sidecar@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" + resolved "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz" integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== dependencies: detect-node-es "^1.1.0" tslib "^2.0.0" use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" - integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== + version "1.6.0" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== utila@~0.4: version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz" integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== utils-merge@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-to-istanbul@^9.0.1: version "9.3.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz" integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" @@ -11240,17 +11230,17 @@ v8-to-istanbul@^9.0.1: vary@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== vfile-location@^3.0.0, vfile-location@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.2.0.tgz#d8e41fbcbd406063669ebf6c33d56ae8721d0f3c" + resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-3.2.0.tgz" integrity sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA== vfile-message@^2.0.0: version "2.0.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz" integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== dependencies: "@types/unist" "^2.0.0" @@ -11258,7 +11248,7 @@ vfile-message@^2.0.0: vfile@^4.0.0, vfile@^4.2.1: version "4.2.1" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" + resolved "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz" integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== dependencies: "@types/unist" "^2.0.0" @@ -11268,21 +11258,21 @@ vfile@^4.0.0, vfile@^4.2.1: w3c-xmlserializer@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz" integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== dependencies: xml-name-validator "^4.0.0" walker@^1.0.7, walker@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: makeerror "1.0.12" -watchpack@^2.4.1: +watchpack@^2.4.4: version "2.4.4" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947" + resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz" integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA== dependencies: glob-to-regexp "^0.4.1" @@ -11290,29 +11280,29 @@ watchpack@^2.4.1: wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== dependencies: minimalistic-assert "^1.0.0" web-namespaces@^1.0.0: version "1.1.4" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" + resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== webidl-conversions@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== webidl-conversions@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz" integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== webpack-dev-middleware@^5.3.4: version "5.3.4" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz" integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" @@ -11323,7 +11313,7 @@ webpack-dev-middleware@^5.3.4: webpack-dev-server@^4.6.0: version "4.15.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz" integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== dependencies: "@types/bonjour" "^3.5.9" @@ -11359,7 +11349,7 @@ webpack-dev-server@^4.6.0: webpack-manifest-plugin@^4.0.2: version "4.1.1" - resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz#10f8dbf4714ff93a215d5a45bcc416d80506f94f" + resolved "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz" integrity sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow== dependencies: tapable "^2.0.0" @@ -11367,7 +11357,7 @@ webpack-manifest-plugin@^4.0.2: webpack-sources@^1.4.3: version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== dependencies: source-list-map "^2.0.0" @@ -11375,7 +11365,7 @@ webpack-sources@^1.4.3: webpack-sources@^2.2.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz" integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA== dependencies: source-list-map "^2.0.1" @@ -11383,13 +11373,13 @@ webpack-sources@^2.2.0: webpack-sources@^3.3.3: version "3.3.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz" integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== webpack@^5.64.4: - version "5.101.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.2.tgz#08c222b7acfce7da95c593e2f88ea1638a07b344" - integrity sha512-4JLXU0tD6OZNVqlwzm3HGEhAHufSiyv+skb7q0d2367VDMzrU1Q/ZeepvkcHH0rZie6uqEtTQQe0OEOOluH3Mg== + version "5.103.0" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz" + integrity sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw== dependencies: "@types/eslint-scope" "^3.7.7" "@types/estree" "^1.0.8" @@ -11399,7 +11389,7 @@ webpack@^5.64.4: "@webassemblyjs/wasm-parser" "^1.14.1" acorn "^8.15.0" acorn-import-phases "^1.0.3" - browserslist "^4.24.0" + browserslist "^4.26.3" chrome-trace-event "^1.0.2" enhanced-resolve "^5.17.3" es-module-lexer "^1.2.1" @@ -11408,18 +11398,18 @@ webpack@^5.64.4: glob-to-regexp "^0.4.1" graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" - loader-runner "^4.2.0" + loader-runner "^4.3.1" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^4.3.2" - tapable "^2.1.1" + schema-utils "^4.3.3" + tapable "^2.3.0" terser-webpack-plugin "^5.3.11" - watchpack "^2.4.1" + watchpack "^2.4.4" webpack-sources "^3.3.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== dependencies: http-parser-js ">=0.5.1" @@ -11428,29 +11418,29 @@ websocket-driver@>=0.5.1, websocket-driver@^0.7.4: websocket-extensions@>=0.1.1: version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whatwg-encoding@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz" integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== dependencies: iconv-lite "0.6.3" whatwg-fetch@^3.6.2: version "3.6.20" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz" integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== whatwg-mimetype@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== whatwg-url@^11.0.0: version "11.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz" integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== dependencies: tr46 "^3.0.0" @@ -11458,7 +11448,7 @@ whatwg-url@^11.0.0: whatwg-url@^7.0.0: version "7.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz" integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== dependencies: lodash.sortby "^4.7.0" @@ -11467,7 +11457,7 @@ whatwg-url@^7.0.0: which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz" integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== dependencies: is-bigint "^1.1.0" @@ -11478,7 +11468,7 @@ which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: which-builtin-type@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + resolved "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz" integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== dependencies: call-bound "^1.0.2" @@ -11497,7 +11487,7 @@ which-builtin-type@^1.2.1: which-collection@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz" integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== dependencies: is-map "^2.0.3" @@ -11507,7 +11497,7 @@ which-collection@^1.0.2: which-typed-array@^1.1.16, which-typed-array@^1.1.19: version "1.1.19" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz" integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== dependencies: available-typed-arrays "^1.0.7" @@ -11520,42 +11510,42 @@ which-typed-array@^1.1.16, which-typed-array@^1.1.19: which@^1.3.1: version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" word-wrap@^1.2.5, word-wrap@~1.2.3: version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -workbox-background-sync@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f" - integrity sha512-trJd3ovpWCvzu4sW0E8rV3FUyIcC0W8G+AZ+VcqzzA890AsWZlUGOTSxIMmIHVusUw/FDq1HFWfy/kC/WTRqSg== +workbox-background-sync@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz" + integrity sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw== dependencies: idb "^7.0.1" - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-broadcast-update@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.6.1.tgz#0fad9454cf8e4ace0c293e5617c64c75d8a8c61e" - integrity sha512-fBhffRdaANdeQ1V8s692R9l/gzvjjRtydBOvR6WCSB0BNE2BacA29Z4r9/RHd9KaXCPl6JTdI9q0bR25YKP8TQ== +workbox-broadcast-update@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz" + integrity sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-build@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.6.1.tgz#6010e9ce550910156761448f2dbea8cfcf759cb0" - integrity sha512-INPgDx6aRycAugUixbKgiEQBWD0MPZqU5r0jyr24CehvNuLPSXp/wGOpdRJmts656lNiXwqV7dC2nzyrzWEDnw== +workbox-build@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz" + integrity sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ== dependencies: "@apideck/better-ajv-errors" "^0.3.1" "@babel/core" "^7.11.1" @@ -11579,136 +11569,136 @@ workbox-build@6.6.1: strip-comments "^2.0.1" tempy "^0.6.0" upath "^1.2.0" - workbox-background-sync "6.6.1" - workbox-broadcast-update "6.6.1" - workbox-cacheable-response "6.6.1" - workbox-core "6.6.1" - workbox-expiration "6.6.1" - workbox-google-analytics "6.6.1" - workbox-navigation-preload "6.6.1" - workbox-precaching "6.6.1" - workbox-range-requests "6.6.1" - workbox-recipes "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" - workbox-streams "6.6.1" - workbox-sw "6.6.1" - workbox-window "6.6.1" - -workbox-cacheable-response@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.6.1.tgz#284c2b86be3f4fd191970ace8c8e99797bcf58e9" - integrity sha512-85LY4veT2CnTCDxaVG7ft3NKaFbH6i4urZXgLiU4AiwvKqS2ChL6/eILiGRYXfZ6gAwDnh5RkuDbr/GMS4KSag== - dependencies: - workbox-core "6.6.1" - -workbox-core@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.6.1.tgz#7184776d4134c5ed2f086878c882728fc9084265" - integrity sha512-ZrGBXjjaJLqzVothoE12qTbVnOAjFrHDXpZe7coCb6q65qI/59rDLwuFMO4PcZ7jcbxY+0+NhUVztzR/CbjEFw== - -workbox-expiration@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.6.1.tgz#a841fa36676104426dbfb9da1ef6a630b4f93739" - integrity sha512-qFiNeeINndiOxaCrd2DeL1Xh1RFug3JonzjxUHc5WkvkD2u5abY3gZL1xSUNt3vZKsFFGGORItSjVTVnWAZO4A== + workbox-background-sync "6.6.0" + workbox-broadcast-update "6.6.0" + workbox-cacheable-response "6.6.0" + workbox-core "6.6.0" + workbox-expiration "6.6.0" + workbox-google-analytics "6.6.0" + workbox-navigation-preload "6.6.0" + workbox-precaching "6.6.0" + workbox-range-requests "6.6.0" + workbox-recipes "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" + workbox-streams "6.6.0" + workbox-sw "6.6.0" + workbox-window "6.6.0" + +workbox-cacheable-response@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz" + integrity sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw== + dependencies: + workbox-core "6.6.0" + +workbox-core@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz" + integrity sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ== + +workbox-expiration@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz" + integrity sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw== dependencies: idb "^7.0.1" - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-google-analytics@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.6.1.tgz#a07a6655ab33d89d1b0b0a935ffa5dea88618c5d" - integrity sha512-1TjSvbFSLmkpqLcBsF7FuGqqeDsf+uAXO/pjiINQKg3b1GN0nBngnxLcXDYo1n/XxK4N7RaRrpRlkwjY/3ocuA== +workbox-google-analytics@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz" + integrity sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q== dependencies: - workbox-background-sync "6.6.1" - workbox-core "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" + workbox-background-sync "6.6.0" + workbox-core "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" -workbox-navigation-preload@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.6.1.tgz#61a34fe125558dd88cf09237f11bd966504ea059" - integrity sha512-DQCZowCecO+wRoIxJI2V6bXWK6/53ff+hEXLGlQL4Rp9ZaPDLrgV/32nxwWIP7QpWDkVEtllTAK5h6cnhxNxDA== +workbox-navigation-preload@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz" + integrity sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-precaching@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.6.1.tgz#dedeeba10a2d163d990bf99f1c2066ac0d1a19e2" - integrity sha512-K4znSJ7IKxCnCYEdhNkMr7X1kNh8cz+mFgx9v5jFdz1MfI84pq8C2zG+oAoeE5kFrUf7YkT5x4uLWBNg0DVZ5A== +workbox-precaching@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz" + integrity sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw== dependencies: - workbox-core "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" + workbox-core "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" -workbox-range-requests@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.6.1.tgz#ddaf7e73af11d362fbb2f136a9063a4c7f507a39" - integrity sha512-4BDzk28govqzg2ZpX0IFkthdRmCKgAKreontYRC5YsAPB2jDtPNxqx3WtTXgHw1NZalXpcH/E4LqUa9+2xbv1g== +workbox-range-requests@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz" + integrity sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-recipes@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.6.1.tgz#ea70d2b2b0b0bce8de0a9d94f274d4a688e69fae" - integrity sha512-/oy8vCSzromXokDA+X+VgpeZJvtuf8SkQ8KL0xmRivMgJZrjwM3c2tpKTJn6PZA6TsbxGs3Sc7KwMoZVamcV2g== +workbox-recipes@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz" + integrity sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A== dependencies: - workbox-cacheable-response "6.6.1" - workbox-core "6.6.1" - workbox-expiration "6.6.1" - workbox-precaching "6.6.1" - workbox-routing "6.6.1" - workbox-strategies "6.6.1" + workbox-cacheable-response "6.6.0" + workbox-core "6.6.0" + workbox-expiration "6.6.0" + workbox-precaching "6.6.0" + workbox-routing "6.6.0" + workbox-strategies "6.6.0" -workbox-routing@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.6.1.tgz#cba9a1c7e0d1ea11e24b6f8c518840efdc94f581" - integrity sha512-j4ohlQvfpVdoR8vDYxTY9rA9VvxTHogkIDwGdJ+rb2VRZQ5vt1CWwUUZBeD/WGFAni12jD1HlMXvJ8JS7aBWTg== +workbox-routing@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz" + integrity sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-strategies@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.6.1.tgz#38d0f0fbdddba97bd92e0c6418d0b1a2ccd5b8bf" - integrity sha512-WQLXkRnsk4L81fVPkkgon1rZNxnpdO5LsO+ws7tYBC6QQQFJVI6v98klrJEjFtZwzw/mB/HT5yVp7CcX0O+mrw== +workbox-strategies@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz" + integrity sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ== dependencies: - workbox-core "6.6.1" + workbox-core "6.6.0" -workbox-streams@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.6.1.tgz#b2f7ba7b315c27a6e3a96a476593f99c5d227d26" - integrity sha512-maKG65FUq9e4BLotSKWSTzeF0sgctQdYyTMq529piEN24Dlu9b6WhrAfRpHdCncRS89Zi2QVpW5V33NX8PgH3Q== +workbox-streams@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz" + integrity sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg== dependencies: - workbox-core "6.6.1" - workbox-routing "6.6.1" + workbox-core "6.6.0" + workbox-routing "6.6.0" -workbox-sw@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.6.1.tgz#d4c4ca3125088e8b9fd7a748ed537fa0247bd72c" - integrity sha512-R7whwjvU2abHH/lR6kQTTXLHDFU2izht9kJOvBRYK65FbwutT4VvnUAJIgHvfWZ/fokrOPhfoWYoPCMpSgUKHQ== +workbox-sw@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz" + integrity sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ== workbox-webpack-plugin@^6.4.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.1.tgz#4f81cc1ad4e5d2cd7477a86ba83c84ee2d187531" - integrity sha512-zpZ+ExFj9NmiI66cFEApyjk7hGsfJ1YMOaLXGXBoZf0v7Iu6hL0ZBe+83mnDq3YYWAfA3fnyFejritjOHkFcrA== + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz" + integrity sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A== dependencies: fast-json-stable-stringify "^2.1.0" pretty-bytes "^5.4.1" upath "^1.2.0" webpack-sources "^1.4.3" - workbox-build "6.6.1" + workbox-build "6.6.0" -workbox-window@6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.6.1.tgz#f22a394cbac36240d0dadcbdebc35f711bb7b89e" - integrity sha512-wil4nwOY58nTdCvif/KEZjQ2NP8uk3gGeRNy2jPBbzypU4BT4D9L8xiwbmDBpZlSgJd2xsT9FvSNU0gsxV51JQ== +workbox-window@6.6.0: + version "6.6.0" + resolved "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz" + integrity sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw== dependencies: "@types/trusted-types" "^2.0.2" - workbox-core "6.6.1" + workbox-core "6.6.0" wrap-ansi@^6.2.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" @@ -11717,7 +11707,7 @@ wrap-ansi@^6.2.0: wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -11726,12 +11716,12 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^3.0.0: version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== dependencies: imurmurhash "^0.1.4" @@ -11741,7 +11731,7 @@ write-file-atomic@^3.0.0: write-file-atomic@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== dependencies: imurmurhash "^0.1.4" @@ -11749,52 +11739,52 @@ write-file-atomic@^4.0.2: ws@^8.11.0, ws@^8.13.0: version "8.18.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== xml-name-validator@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== xmlchars@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== xmlcreate@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" + resolved "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz" integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== xtend@^4.0.0, xtend@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^5.0.5: version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== yallist@^3.0.2: version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yargs-parser@^21.1.1: version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" @@ -11807,27 +11797,27 @@ yargs@^17.3.1, yargs@^17.7.2: yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yoctocolors-cjs@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" - integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== +yoctocolors-cjs@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz" + integrity sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw== zod@^3.11.6: version "3.25.76" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + resolved "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== zustand@^4.4.1: version "4.5.7" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55" + resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz" integrity sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw== dependencies: use-sync-external-store "^1.2.2" zwitch@^1.0.0: version "1.0.5" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" + resolved "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz" integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==